├── changelog.txt ├── includes ├── Pods_GF.php ├── Pods_GF_Addon.php ├── Pods_GF_CLI.php ├── Pods_GF_UI.php └── functions.php ├── license.txt ├── pods-gravity-forms.php ├── readme.txt ├── ui ├── pods-gf-admin.js ├── pods-gf.css └── pods-gf.js └── vendor-prefixed ├── autoload-classmap.php ├── autoload.php └── erusev └── parsedown ├── LICENSE.txt └── Parsedown.php /changelog.txt: -------------------------------------------------------------------------------- 1 | Found a bug? Have a great feature idea? Get on GitHub and tell us about it and we'll get right on it: https://github.com/pods-framework/pods-gravity-forms/issues/new 2 | 3 | Our GitHub has the full list of all prior releases of Pods Gravity Forms: https://github.com/pods-framework/pods-gravity-forms/releases 4 | 5 | = 1.5.0 - March 29th, 2024 = 6 | 7 | * New requirements that match Pods: WP 6.0+, PHP 7.2+, and Pods 3.0+ to prep for Pods Gravity Forms 2.0 (@sc0ttkclark) 8 | * Added: Support value overrides for checkbox GF fields when prepopulating. (@sc0ttkclark) 9 | * Added: Allow for passing `?pods_gf_debug=1` to a form submit URL to debug the form submission mapping to Pods which outputs debug information and stops before the save runs). (@sc0ttkclark) 10 | * Added: New hook `pods_gf_dynamic_select_show_empty_option` lets you disable showing the "empty option" in dynamic fields being prepopulated. (@sc0ttkclark) 11 | * Added: New hook `pods_gf_addon_options_{$form_id}` that lets you filter the options built for a feed for the `Pods_GF` object. (@sc0ttkclark) 12 | * Tweak: New `pods-gf-ui-view-only` class added to the view-only mode. (@sc0ttkclark) 13 | * Tweak: Expanded secondary submit handling with the ability to have cancel button. (@sc0ttkclark) 14 | * Fixed: Resolved various PHP notices. (@sc0ttkclark) 15 | * Fixed: Removed "comments" from the field mapping options in the feed. (@sc0ttkclark) 16 | * Fixed: Remove extra HTML in the feed labels. (@sc0ttkclark) 17 | * Fixed: For non-select GF field types, trim the dashes on the custom `select_text` option used. (@sc0ttkclark) 18 | * Fixed: Prepopulating field values works more consistently now when passing the prepopulated filter pre-chunked arrays of values. (@sc0ttkclark) 19 | * Fixed: View-only mode forced to use page zero. (@sc0ttkclark) 20 | * Fixed: Prevent duplicate submissions during feed processing. (@sc0ttkclark) 21 | * Fixed: Only allow working with active leads in `Pods_GF_UI`. (@sc0ttkclark) 22 | * Fixed: Prevent Markdown conflicts with other plugins and update PHP 8 compatibility by switching to the Parsedown library. #166 (@sc0ttkclark) 23 | * Fixed: Stop prepopulating fields that aren't opted-in to it. #168 (@sc0ttkclark) 24 | 25 | ALSO: Pods Gravity Forms 2.0 is still in development and it brings complete compatibility with the latest Gravity Forms releases. We could use your support to help it get over the finish line this year. Please consider [donating to the Pods project](https://friends.pods.io/) to help us get there more quickly. 26 | 27 | = 1.4.5 - July 22nd, 2022 = 28 | 29 | * Tested against WP 6.0 30 | * Added: Not seeing something map correctly? As a site admin, you now have the power to debug the form submission and see what might be going on. Add `?pods_gf_debug_gf_to_pods=1` to the URL of the form action before submitting to take advantage of the admin-only debug mode. This will output the values as they would be sent to Pods, the entry information used to reference it, and the feed options used at the time. It will stop the form from completely saving to Pods so you can tweak and debug your form feeds however much you'd like to perfect them. (@sc0ttkclark) 31 | * Fixed: Conditional checks for feeds has been resolved and now won't get confused when there are multiple feeds for the same form in certain cases. (@sc0ttkclark) 32 | * Fixed: Additional compatibility with Gravity Flow. #157 (@JoryHogeveen) 33 | * Pods 2.9 is in beta and after it is released, new mapping of GF List Fields to Pods repeatable fields will be added. This add-on will also be updated with minimum version requirements updated for WP 5.5+, Pods 2.8+, and Gravity Forms 2.5+. Complete testing will be done at that time to ensure complete compatibility. 34 | 35 | = 1.4.4 - October 6th, 2021 = 36 | 37 | * Tested against WP 5.8 38 | * Get ready for Pods 2.8 in just a week! This add-on will receive updates to ensure it is compatible with the latest Gravity Forms and the changes in Pods 2.8 39 | 40 | = 1.4.3 - March 26th, 2020 = 41 | 42 | * Added: Now requiring PHP 5.4+ 43 | * Added: Freemius support when running Pods 2.7.17 44 | * Fixed: Prepopulate handling for relationship fields. 45 | * Fixed: Prevent errors when form doesn't exist by the time it gets to our hook. 46 | 47 | = 1.4.2 - March 2nd, 2020 = 48 | 49 | * Fixed: Ajax handling for various callbacks that hook into `gform_pre_render`. 50 | * Fixed: Cleaned up logic and prevent PHP notices with multi-select arrays when setting up choices arrays. 51 | * Fixed: Make sure `Pods_GF_UI` does not return false on UI callbacks to prevent access errors. 52 | * Fixed: Add mapping feeds to the import/export! (props @travislopes) 53 | 54 | = 1.4.1 - October 16th, 2018 = 55 | 56 | * Fixed: When syncing multiple entries, the field values were caching and not unique per entry resulting in what appeared to be duplicated content inserts/updates. 57 | 58 | = 1.4 - October 16th, 2018 = 59 | 60 | * Support: Added support for Gravity Forms 2.3 database tables changes (You may see a warning on the Edit Pod screen but this is a false positive because we cache a list of all tables to transients and it triggers the warning solved by removing those old "rg" tables) 61 | * Changed: Backwards compatibility issue -- You can now more easily set custom override values, however the old style was not able to be brought over -- you'll want to update your feeds when possible, the old values will not show up and you'll have to select the custom override value option once more, then fill it in 62 | * Changed: Backwards compatibility issue -- Now requiring WordPress 4.6+ 63 | * Feature: When editing entries in the admin area, changes now sync to the associated Pod item (except trash/deletes) 64 | * Feature: New Bulk Entry Syncing to Pods WP-CLI command `wp pods-gf sync --form=123` or you can specify which feed (even if it is not active) with `wp pods-gf sync --form=123 --feed=2` 65 | * Feature: Support for List field mapping to a Pod field which ends up serializing the value, but can be prepopulated back into the Gravity Form 66 | * Feature: List field mapping to relationship fields related to another Pod (list columns map to individual fields in the related Pod) with new filters `pods_gf_field_columns_mapping` and `pods_gf_field_column_row` 67 | * Feature: Support for Chained Select field mapping to a Pod field 68 | * Feature: New Custom fields section added for Pods that support meta (Posts, Terms, Users, Media, and Comments), you can set additional custom fields including ability to set custom values there too 69 | * Feature: Ability to set conditional processing per feed, based on specific values submitted 70 | * Added: Whenever you create a new feed, mapping will automatically be associated between a Gravity Form field and a Pod field if the labels match 71 | * Added: Custom override values now support GF merge tags by default (no insert UI yet) like `{form_id}` and any other merge tag 72 | * Added: Required WP Object Fields in mapping are no longer required if you choose to 'Enable editing with this form using ____' option for Post/Media or User pod types 73 | * Added: Support for E-mail field mappings with 'Confirm E-mail' enabled 74 | * Added: Support for Date fields with multiple inputs (date dropdown / text fields) 75 | * Added: Smarter requirement handling for WP object fields based on object type (only require what the WP insert API requires) 76 | * Added: New mapping fields are now available for more Entry and Payment fields 77 | * Added: New merge tags `{pods.id}` and `{pods.permalink}` are available for usage and in the merge tag selection dropdowns 78 | * Improved: Added headings to each group of feed options so they are easier to work with 79 | * Improved: Address field mapping for Country, State, and CA Provinces now convert properly to their Pods counterparts 80 | * Updated: PHP Markdown library updated to 1.0.2 81 | * Fixed: Issues with using 'bypass' as a save action 82 | * Fixed: Dynamic select options should set the current value (as posted in form) properly 83 | * Fixed: Date/time fields shouldn't auto populate with empty dates such as 0000-00-00 anymore 84 | * Fixed: Additional attachment processing fixes 85 | * Fixed: Lots of Pods GF UI issues resolved 86 | * Fixed: Removed Autocomplete limit (was 30) that was being enforced, now all data from related field will show 87 | * Fixed: Dynamic mapping value checking to support arrays of values 88 | * Fixed: Lots of Prepopulating fixes 89 | * Fixed: Now supports multi page form validation and prepopulating 90 | 91 | = 1.3 - June 2nd, 2017 = 92 | 93 | * Added: When creating new feeds mapping will automatically be detected based on matching field labels 94 | * Added: New option to prepopulate the form fields with data based on the field mapping in the feed (same type of logic as edit). Limitations with certain field types, please submit issues with problems you find here. 95 | * Added: Rewrote the whole File Upload field mapping logic and tested against Single/Multi file fields (props @mika31, @copperleaf, @zanematthew, @zorog, @chriswagoner for testing help, props @spivurno for official GF support code help) 96 | * Added: Support for feeds with submissions from the forms embedded on the dashboard and in the admin area (props @richardW8k) 97 | * Added: Field names to field mapping screen 98 | * Added: Ability to define a custom override value for each field mapping 99 | * Added: Ability to enable editing of user data using current logged in user ID (only for User pod feeds) 100 | * Added: Ability to enable editing of post data using current post ID on singular templates (only for Post type pod feeds) 101 | * Added: Ability to define custom 'content' in Pods GF UI custom actions instead of including a form 102 | * Added: Ability to relate to GF forms using a relationship field (new option: Gravity Forms > Forms) 103 | * Added: Ability to map Address and List fields 104 | * Added: Ability to map Category and Post Tag fields 105 | * Added: Ability to map sub fields to a pod field (Name [First Name], Address [Street Line 1], etc) 106 | * Fixed: Ensure time fields get mapped correctly (props @mmarvin1) 107 | * Fixed: Ensure default pods-gf-ui shortcode is only added/run on content within the loop (props @jamesgol) 108 | * Fixed: Empty id used for Pods GF UI 109 | * Fixed: Callback handling for Pods GF UI 110 | * Fixed: Default Post Author mapping 111 | 112 | = 1.2 - October 4th, 2016 = 113 | 114 | * Added: When using a custom action and setting the form ID option in Pods GF UI, a new custom action will be used which embeds the GF form (if no callback provided in action_data option) 115 | * Added: New Pods GF UI option, specific to each action, for `action_link` which corresponds to the `action_links` Pods UI option 116 | * Fixed: Support for recent GF versions where pre_save_id hook uses a different Form-specific naming convention 117 | * Fixed: Custom confirmation handling may have not been functioning properly in some cases 118 | * Fixed: Removed some issues that were causing PHP notices 119 | 120 | 121 | = 1.1 - June 13th, 2016 = 122 | 123 | * Added: Support for edit mode when using the Pods GF add-on mapping in the GF UI -- Use the new filter `pods_gf_addon_edit_id`, just return the ID to edit and the options will automatically be set for you 124 | * Added: When filtering the Pods data in `Pods_GF::gf_to_pods()` (via the `pods_gf_to_pods_data` and related filters), if you set the proper ID field in that array it will now be used to *save* over the existing item; Helpful for dynamic editing configurations based upon different processes and workflows in the code 125 | * Added: `Pods_GF::confirmation()` now supports `{@gf_to_pods_id}` replacement in confirmation URLs, replacing the variable properly to the resulting saved ID 126 | * Fixed: `Pods_GF::_gf_to_pods_handler()` would sometimes get the action improperly set to `edit`, but only `add`, `save`, or `bypass` are valid 127 | * Fixed: When an invalid pod is called in `Pods_GF::_gf_to_pods_handler()`, there's now a proper fallback to avoid PHP errors/warnings/notices 128 | * Fixed: When an invalid pod is called in `Pods_GF::_gf_field_validation()`, there's now a proper fallback to avoid PHP errors/warnings/notices 129 | * Fixed: `Pods_GF::confirmation()` would add the `gform_confirmation_{$form_id}` filter incorrectly and would cause PHP warnings about the callback, causing the confirmation functionality to not work properly 130 | * Fixed: `Pods_GF::confirmation()` confirmation URL replacement now handles a few more cases where previously PHP notices would result 131 | * Changed: `Pods_GF` is now storing multiple instances statically, cannot be called with `new Pods_GF()`, must be called with `Pods_GF::get_instance()` but more importantly should be called through the standard `pods_gf()` helper function to remain backwards compatible with previous versions 132 | * Changed: `Pods_GF::$gf_to_pods_id` is no longer an integer, but an array of integers keyed by the GF Form ID 133 | * Changed: `Pods_GF::$keep_files` is no longer an boolean, but an array of booleans keyed by the GF Form ID 134 | 135 | = 1.0 - March 4th, 2016 = 136 | 137 | * Initial release 138 | -------------------------------------------------------------------------------- /includes/Pods_GF_Addon.php: -------------------------------------------------------------------------------- 1 | 'pods_gf_admin', 110 | 'enqueue' => array( array( 'admin_page' => array( 'form_settings' ) ) ), 111 | 'src' => PODS_GF_URL . '/ui/pods-gf-admin.js', 112 | 'version' => $this->_version, 113 | 'deps' => array( 'jquery' ), 114 | ), 115 | ); 116 | 117 | return array_merge( parent::scripts(), $scripts ); 118 | 119 | } 120 | 121 | /*public function plugin_page() { 122 | 123 | ?> 124 | This page appears in the Forms menu 125 | __( 'Name', 'pods-gravity-forms' ), 136 | 'type' => 'text', 137 | 'name' => 'feedName', 138 | 'tooltip' => __( 'Name for this feed', 'pods-gravity-forms' ), 139 | 'class' => 'medium', 140 | ); 141 | 142 | $gf_fields = array(); 143 | 144 | $gf_form = GFAPI::get_form( pods_v( 'id' ) ); 145 | 146 | if ( ! empty( $gf_form ) && ! empty( $gf_form['fields'] ) ) { 147 | $gf_fields = $gf_form['fields']; 148 | } 149 | 150 | $pods_api = pods_api(); 151 | $all_pods = $pods_api->load_pods( array( 'names' => true ) ); 152 | 153 | $pod_choice_list = array(); 154 | $pod_choice_list[] = array( 155 | 'label' => __( 'Select a Pod', 'pods-gravity-forms' ), 156 | 'value' => '', 157 | ); 158 | 159 | foreach ( $all_pods as $name => $label ) { 160 | $pod_choice_list[] = array( 161 | 'label' => $label . ' (' . $name . ')', 162 | 'value' => $name, 163 | ); 164 | } 165 | 166 | $feed_field_pod = array( 167 | 'label' => __( 'Pod', 'pods-gravity-forms' ), 168 | 'type' => 'select', 169 | 'name' => 'pod', 170 | 'tooltip' => __( 'Select the pod', 'pods-gravity-forms' ), 171 | 'choices' => $pod_choice_list, 172 | 'onchange' => "jQuery(this).parents('form').submit();", 173 | 'required' => true, 174 | ); 175 | 176 | $selected_pod = $this->get_setting( 'pod' ); 177 | $enable_current_post = (int) $this->get_setting( 'enable_current_post' ); 178 | $enable_current_user = (int) $this->get_setting( 'enable_current_user' ); 179 | 180 | $posted_settings = $this->get_posted_settings(); 181 | 182 | if ( isset( $posted_settings['enable_current_post'] ) ) { 183 | $enable_current_post = (int) $posted_settings['enable_current_post']; 184 | } 185 | 186 | if ( isset( $posted_settings['enable_current_user'] ) ) { 187 | $enable_current_user = (int) $posted_settings['enable_current_user']; 188 | } 189 | 190 | $pod_fields = array(); 191 | $pod_type = ''; 192 | 193 | if ( ! empty( $selected_pod ) ) { 194 | $pod_object = $pods_api->load_pod( array( 'name' => $selected_pod ) ); 195 | 196 | if ( ! empty( $pod_object ) ) { 197 | $pod_type = $pod_object['type']; 198 | 199 | foreach ( $pod_object['fields'] as $name => $field ) { 200 | $pod_fields[] = array( 201 | 'needs_process' => true, 202 | 'name' => $name, 203 | 'field' => $field, 204 | ); 205 | } 206 | } 207 | } 208 | 209 | $feed_field_pod_fields = array( 210 | 'name' => 'pod_fields', 211 | 'label' => __( 'Pod Fields', 'pods-gravity-forms' ), 212 | 'type' => 'field_map', 213 | 'dependency' => 'pod', 214 | 'field_map' => $pod_fields, 215 | ); 216 | 217 | $ignore_object_fields = array( 218 | 'ID', 219 | 'post_type', 220 | 'comment_type', 221 | 'taxonomy', 222 | 'guid', 223 | 'menu_order', 224 | 'post_mime_type', 225 | 'comment_count', 226 | 'comment_status', 227 | 'ping_status', 228 | 'post_date_gmt', 229 | 'post_modified_gmt', 230 | 'post_password', 231 | 'post_status', 232 | 'post_content_filtered', 233 | 'pinged', 234 | 'to_ping', 235 | 'comments', 236 | ); 237 | 238 | $wp_object_fields = array(); 239 | 240 | if ( ! empty( $pod_object ) ) { 241 | foreach ( $pod_object['object_fields'] as $name => $field ) { 242 | if ( in_array( $name, $ignore_object_fields, true ) ) { 243 | continue; 244 | } 245 | 246 | if ( in_array( $pod_type, array( 'post_type', 'media' ), true ) ) { 247 | if ( 1 === $enable_current_post ) { 248 | $field['options']['required'] = 0; 249 | } elseif ( in_array( $name, array( 'post_title', 'post_content' ), true ) ) { 250 | $field['options']['required'] = 1; 251 | } 252 | } elseif ( 'taxonomy' === $pod_type ) { 253 | if ( 'name' === $name ) { 254 | $field['options']['required'] = 1; 255 | } 256 | } elseif ( 'user' === $pod_type ) { 257 | if ( 1 === $enable_current_user ) { 258 | $field['options']['required'] = 0; 259 | } elseif ( 'user_login' === $name ) { 260 | $field['options']['required'] = 1; 261 | } 262 | } 263 | 264 | $wp_object_fields[ $name ] = array( 265 | 'needs_process' => true, 266 | 'name' => $name, 267 | 'field' => $field, 268 | ); 269 | } 270 | } 271 | 272 | if ( 'post_type' === $pod_type ) { 273 | $wp_object_fields['_thumbnail_id'] = array( 274 | 'name' => '_thumbnail_id', 275 | 'label' => __( 'Featured Image', 'pods-gravity-forms' ), 276 | ); 277 | } 278 | 279 | $feed_field_wp_object_fields = array( 280 | 'name' => 'wp_object_fields', 281 | 'label' => __( 'WP Object Fields', 'pods-gravity-forms' ), 282 | 'type' => 'field_map', 283 | 'dependency' => 'pod', 284 | 'field_map' => array_values( $wp_object_fields ), 285 | ); 286 | 287 | $settings = array(); 288 | 289 | /////////////////// 290 | // Pod feed mapping 291 | /////////////////// 292 | $settings['pod_mapping'] = array( 293 | 'title' => __( 'Pod Feed Mapping', 'pods-gravity-forms' ), 294 | 'fields' => array( 295 | $feed_field_name, 296 | $feed_field_pod, 297 | $feed_field_pod_fields, 298 | ), 299 | ); 300 | 301 | if ( ! empty( $feed_field_wp_object_fields['field_map'] ) ) { 302 | $settings['pod_mapping']['fields'][] = $feed_field_wp_object_fields; 303 | } 304 | 305 | $blacklisted_keys = array(); 306 | 307 | // Build field mapping data arrays 308 | foreach ( $settings['pod_mapping']['fields'] as $k => $field_set ) { 309 | if ( empty( $field_set['field_map'] ) ) { 310 | continue; 311 | } 312 | 313 | foreach ( $field_set['field_map'] as $kf => $field_map ) { 314 | $blacklisted_keys[] = $field_map['name']; 315 | 316 | if ( ! empty( $field_map['needs_process'] ) ) { 317 | $name = $field_map['name']; 318 | $field = $field_map['field']; 319 | 320 | $field_required = false; 321 | 322 | if ( isset( $field['options']['required'] ) && 1 === (int) $field['options']['required'] ) { 323 | $field_required = true; 324 | 325 | if ( isset( $wp_object_fields[ $name ] ) ) { 326 | if ( in_array( $pod_type, array( 'post_type', 'media' ), true ) && 1 === $enable_current_post ) { 327 | $field_required = false; 328 | } elseif ( 'user' === $pod_type && 1 === $enable_current_user ) { 329 | $field_required = false; 330 | } 331 | } 332 | } 333 | 334 | $field_map = array( 335 | 'name' => $name, 336 | 'label' => $field['label'], 337 | 'required' => $field_required, 338 | ); 339 | 340 | if ( 0 === (int) pods_v( 'fid' ) ) { 341 | foreach ( $gf_fields as $gf_field ) { 342 | if ( strtolower( $field['label'] ) === strtolower( $gf_field['label'] ) ) { 343 | $field_map['default_value'] = $gf_field['id']; 344 | } 345 | } 346 | } 347 | } 348 | 349 | // Add field names to labels 350 | $field_map['label'] = sprintf( 351 | '%s (%s)', 352 | esc_html( $field_map['label'] ), 353 | esc_html( $field_map['name'] ) 354 | ); 355 | 356 | $settings['pod_mapping']['fields'][ $k ]['field_map'][ $kf ] = $field_map; 357 | } 358 | } 359 | 360 | /////////////////// 361 | // Custom fields 362 | /////////////////// 363 | if ( in_array( $pod_type, array( 'post_type', 'taxonomy', 'user', 'media', 'comment' ), true ) ) { 364 | $settings['custom_fields'] = array( 365 | 'title' => esc_html__( 'Custom Fields', 'pods-gravity-forms' ), 366 | 'fields' => array( 367 | array( 368 | 'name' => 'custom_fields', 369 | 'label' => esc_html__( 'Custom Fields', 'pods-gravity-forms' ), 370 | 'type' => 'generic_map', 371 | 'key_field' => array( 372 | 'choices' => $this->get_meta_field_map( $selected_pod, $pod_type, $blacklisted_keys ), 373 | 'placeholder' => esc_html__( 'Custom Field Name', 'pods-gravity-forms' ), 374 | 'title' => esc_html__( 'Name', 'pods-gravity-forms' ), 375 | ), 376 | 'value_field' => array( 377 | 'choices' => 'form_fields', 378 | 'custom_value' => false, 379 | 'merge_tags' => true, 380 | 'placeholder' => esc_html__( 'Custom Field Value', 'pods-gravity-forms' ), 381 | ), 382 | ), 383 | ), 384 | ); 385 | } 386 | 387 | /////////////////// 388 | // Advanced 389 | /////////////////// 390 | $settings['advanced'] = array( 391 | 'title' => __( 'Advanced', 'pods-gravity-forms' ), 392 | 'fields' => array(), 393 | ); 394 | 395 | $settings['advanced']['fields'][] = array( 396 | 'name' => 'update_pod_item', 397 | 'label' => __( 'Support entry updates', 'pods-gravity-forms' ), 398 | 'type' => 'checkbox', 399 | 'choices' => array( 400 | array( 401 | 'value' => 1, 402 | 'label' => __( 'Update pod item if the entry is updated', 'pods-gravity-forms' ), 403 | 'name' => 'update_pod_item', 404 | ), 405 | ), 406 | ); 407 | 408 | $settings['advanced']['fields'][] = array( 409 | 'name' => 'enable_markdown', 410 | 'label' => __( 'Enable Markdown', 'pods-gravity-forms' ), 411 | 'type' => 'checkbox', 412 | 'choices' => array( 413 | array( 414 | 'value' => 1, 415 | 'label' => __( 'Enable Markdown in HTML Fields', 'pods-gravity-forms' ), 416 | 'name' => 'enable_markdown', 417 | ), 418 | ), 419 | ); 420 | 421 | if ( 'user' === $pod_type ) { 422 | $settings['advanced']['fields'][] = array( 423 | 'name' => 'enable_current_user', 424 | 'label' => __( 'Enable editing with this form using logged in user', 'pods-gravity-forms' ), 425 | 'type' => 'checkbox', 426 | 'choices' => array( 427 | array( 428 | 'value' => 1, 429 | 'label' => __( 'Enable editing with this form using the logged in user data', 'pods-gravity-forms' ), 430 | 'name' => 'enable_current_user', 431 | ), 432 | ), 433 | ); 434 | 435 | $settings['advanced']['fields'][] = array( 436 | 'name' => 'enable_prepopulate', 437 | 'label' => __( 'Enable populating field values for this form using logged in user', 'pods-gravity-forms' ), 438 | 'type' => 'checkbox', 439 | 'choices' => array( 440 | array( 441 | 'value' => 1, 442 | 'label' => __( 'Enable populating field values for this form using the logged in user data', 'pods-gravity-forms' ), 443 | 'name' => 'enable_prepopulate', 444 | ), 445 | ), 446 | ); 447 | } elseif ( in_array( $pod_type, array( 'post_type', 'media' ), true ) ) { 448 | $settings['advanced']['fields'][] = array( 449 | 'name' => 'enable_current_post', 450 | 'label' => __( 'Enable editing with this form using current post', 'pods-gravity-forms' ), 451 | 'type' => 'checkbox', 452 | 'choices' => array( 453 | array( 454 | 'value' => 1, 455 | 'label' => __( 'Enable editing with this form using the current post ID (only works on singular template)', 'pods-gravity-forms' ), 456 | 'name' => 'enable_current_post', 457 | ), 458 | ), 459 | ); 460 | 461 | $settings['advanced']['fields'][] = array( 462 | 'name' => 'enable_prepopulate', 463 | 'label' => __( 'Enable populating field values for this form using current post', 'pods-gravity-forms' ), 464 | 'type' => 'checkbox', 465 | 'choices' => array( 466 | array( 467 | 'value' => 1, 468 | 'label' => __( 'Enable populating field values for this form using the current post ID (only works on singular template)', 'pods-gravity-forms' ), 469 | 'name' => 'enable_prepopulate', 470 | ), 471 | ), 472 | ); 473 | } 474 | 475 | $settings['advanced']['fields'][] = array( 476 | 'name' => 'delete_entry', 477 | 'label' => __( 'Delete Gravity Form Entry on submission', 'pods-gravity-forms' ), 478 | 'type' => 'checkbox', 479 | 'choices' => array( 480 | array( 481 | 'value' => 1, 482 | 'label' => __( 'Delete entry after processing', 'pods-gravity-forms' ), 483 | 'name' => 'delete_entry', 484 | ), 485 | ), 486 | ); 487 | 488 | $addon_slug = $this->get_slug(); 489 | 490 | add_filter( "gform_{$addon_slug}_field_map_choices", array( $this, 'add_field_map_choices' ) ); 491 | 492 | $settings['advanced']['fields'][] = array( 493 | 'name' => 'feed_condition', 494 | 'label' => __( 'Conditional Logic', 'pods-gravity-forms' ), 495 | 'checkbox_label' => __( 'Enable', 'pods-gravity-forms' ), 496 | 'type' => 'feed_condition', 497 | ); 498 | 499 | return $settings; 500 | 501 | } 502 | 503 | /** 504 | * Prepare fields for meta field mapping. 505 | * 506 | * Props go to GF Post Creation add-on for the initial source of this method. 507 | * 508 | * @param string $pod_name Current pod name 509 | * @param string $pod_type Current pod type 510 | * @param string[] $blacklisted_keys Meta keys to exclude 511 | * 512 | * @uses GFFormsModel::get_custom_field_names() 513 | * 514 | * @return array 515 | */ 516 | public function get_meta_field_map( $pod_name = '', $pod_type = '', $blacklisted_keys = array() ) { 517 | 518 | // Setup meta fields array. 519 | $meta_fields = array( 520 | array( 521 | 'label' => esc_html__( 'Select a Custom Field Name', 'pods-gravity-forms' ), 522 | 'value' => '', 523 | ), 524 | ); 525 | 526 | /////////////////// 527 | // Custom fields 528 | /////////////////// 529 | 530 | // Get most used post meta keys 531 | $meta_keys = $this->get_custom_field_names( $pod_name, $pod_type ); 532 | 533 | // If no meta keys exist, return an empty array. 534 | if ( empty( $meta_keys ) ) { 535 | return array(); 536 | } 537 | 538 | // Add post meta keys to the meta fields array. 539 | foreach ( $meta_keys as $meta_key ) { 540 | $meta_fields[] = array( 541 | 'label' => $meta_key, 542 | 'value' => $meta_key, 543 | ); 544 | } 545 | 546 | /////////////////// 547 | // Custom key 548 | /////////////////// 549 | $meta_fields[] = array( 550 | 'label' => esc_html__( 'Add New Custom Field Name', 'pods-gravity-forms' ), 551 | 'value' => 'gf_custom', 552 | ); 553 | 554 | return $meta_fields; 555 | 556 | } 557 | 558 | /** 559 | * Get most common custom field names from DB. 560 | * 561 | * @param string $pod_name Current pod name 562 | * @param string $pod_type Current pod type 563 | * @param string[] $blacklisted_keys Meta keys to exclude 564 | * 565 | * @return string[] 566 | */ 567 | public function get_custom_field_names( $pod_name = '', $pod_type = '', $blacklisted_keys = array() ) { 568 | 569 | global $wpdb; 570 | 571 | $object_table = $wpdb->posts; 572 | $meta_table = $wpdb->postmeta; 573 | $id_col = 'ID'; 574 | $meta_id_col = 'post_id'; 575 | $blacklist_where = " 576 | AND `object`.`post_type` LIKE '_pods_%' 577 | "; 578 | 579 | if ( 'taxonomy' === $pod_type ) { 580 | $object_table = $wpdb->terms; 581 | $meta_table = $wpdb->termmeta; 582 | $id_col = 'term_id'; 583 | $meta_id_col = 'term_id'; 584 | $blacklist_where = ''; 585 | } elseif ( 'user' === $pod_type ) { 586 | $object_table = $wpdb->users; 587 | $meta_table = $wpdb->usermeta; 588 | $id_col = 'ID'; 589 | $meta_id_col = 'user_id'; 590 | $blacklist_where = ''; 591 | } elseif ( 'comment' === $pod_type ) { 592 | $object_table = $wpdb->users; 593 | $meta_table = $wpdb->usermeta; 594 | $id_col = 'comment_ID'; 595 | $meta_id_col = 'comment_id'; 596 | $blacklist_where = ''; 597 | } 598 | 599 | $where = ''; 600 | 601 | $pods_blacklist_keys = array(); 602 | 603 | if ( $blacklist_where ) { 604 | $sql = " 605 | SELECT `meta`.`meta_key` 606 | FROM `{$meta_table}` AS `meta` 607 | LEFT JOIN `{$object_table}` AS `object` ON `object`.`{$id_col}` = `meta`.`{$meta_id_col}` 608 | WHERE 609 | `meta`.`meta_key` NOT LIKE '\_%' {$blacklist_where} 610 | GROUP BY `meta`.`meta_key` 611 | "; 612 | 613 | $pods_blacklist_keys = $wpdb->get_col( $sql ); 614 | } 615 | 616 | $pods_blacklist_keys = array_merge( $pods_blacklist_keys, $blacklisted_keys ); 617 | $pods_blacklist_keys = array_unique( $pods_blacklist_keys ); 618 | $pods_blacklist_keys = array_filter( $pods_blacklist_keys ); 619 | 620 | if ( $pods_blacklist_keys ) { 621 | $placeholders = array_fill( 0, count( $pods_blacklist_keys ), '%s' ); 622 | 623 | $where = " 624 | AND `meta`.`meta_key` NOT IN ( " . implode( ", ", $placeholders ) . " ) 625 | "; 626 | 627 | $where = $wpdb->prepare( $where, $pods_blacklist_keys ); 628 | } 629 | 630 | $sql = " 631 | SELECT `meta`.`meta_key`, COUNT(*) AS `total_count` 632 | FROM `{$meta_table}` AS `meta` 633 | WHERE 634 | `meta`.`meta_key` NOT LIKE '\_%' {$where} 635 | GROUP BY `meta`.`meta_key` 636 | ORDER BY `total_count` DESC 637 | LIMIT 50 638 | "; 639 | 640 | $meta_keys = $wpdb->get_col( $sql ); 641 | 642 | if ( $meta_keys ) { 643 | natcasesort( $meta_keys ); 644 | } 645 | 646 | return $meta_keys; 647 | 648 | } 649 | 650 | /** 651 | * @param $choices 652 | * 653 | * @return array 654 | */ 655 | public function add_field_map_choices( $choices ) { 656 | 657 | // Remove first choice 658 | array_shift( $choices ); 659 | 660 | $choices = array_merge( 661 | array( 662 | // Add first choice back 663 | array( 664 | 'value' => '', 665 | 'label' => __( 'Select a Field', 'gravityforms' ), // Use gravtiyforms text domain here 666 | ), 667 | // Make custom override first option 668 | array( 669 | 'value' => 'gf_custom', 670 | 'label' => __( 'Custom override value', 'pods-gravity-forms' ), 671 | ), 672 | ), 673 | $choices, 674 | array( 675 | array( 676 | 'value' => 'transaction_id', 677 | 'label' => 'Transaction ID', 678 | ), 679 | array( 680 | 'value' => 'payment_amount', 681 | 'label' => 'Payment Amount', 682 | ), 683 | array( 684 | 'value' => 'payment_date', 685 | 'label' => 'Payment Date', 686 | ), 687 | array( 688 | 'value' => 'payment_status', 689 | 'label' => 'Payment Status', 690 | ), 691 | ) 692 | ); 693 | 694 | foreach ( $choices as $k => $choice ) { 695 | if ( '_pods_item_id' === $choice['value'] ) { 696 | unset( $choices[ $k ] ); 697 | 698 | $choices = array_values( $choices ); 699 | 700 | break; 701 | } 702 | } 703 | 704 | return $choices; 705 | 706 | } 707 | 708 | /*** 709 | * Renders and initializes a drop down field based on the $field array 710 | * 711 | * @param array $field - Field array containing the configuration options of this field 712 | * @param bool $echo = true - true to echo the output to the screen, false to simply return the contents as a string 713 | * 714 | * @return string The HTML for the field 715 | */ 716 | public function settings_select( $field, $echo = true ) { 717 | 718 | $has_gf_custom = false; 719 | 720 | if ( ! empty( $field['choices'] ) ) { 721 | foreach ( $field['choices'] as $choice ) { 722 | if ( isset( $choice['value'] ) && 'gf_custom' === $choice['value'] ) { 723 | $has_gf_custom = true; 724 | 725 | break; 726 | } 727 | } 728 | } 729 | 730 | // Select has no custom choice or we already took over the first select. 731 | if ( empty( $field['choices'] ) || ! $has_gf_custom || ! empty( $field['_pods_custom_select'] ) ) { 732 | return parent::settings_select( $field, $echo ); 733 | } 734 | 735 | // Already doing custom select. 736 | if ( ! empty( $field['type'] ) && 'generic_map' === $field['type'] ) { 737 | return parent::settings_select( $field, $echo ); 738 | } 739 | 740 | $field['_pods_custom_select'] = true; 741 | 742 | return parent::settings_select_custom( $field, $echo ); 743 | 744 | } 745 | 746 | /** 747 | * @return string|void 748 | */ 749 | public function field_map_title() { 750 | 751 | return __( 'Pod Field', 'pods-gravity-forms' ); 752 | 753 | } 754 | 755 | /** 756 | * @return array 757 | */ 758 | public function feed_list_columns() { 759 | 760 | return array( 761 | 'feedName' => __( 'Name', 'pods-gravity-forms' ), 762 | 'pod' => __( 'Pod', 'pods-gravity-forms' ) 763 | ); 764 | 765 | } 766 | 767 | /** 768 | * @param $feed 769 | * 770 | * @return string 771 | */ 772 | public function get_column_value_pod( $feed ) { 773 | 774 | return '' . $feed['meta']['pod'] . ''; 775 | 776 | } 777 | 778 | /** 779 | * 780 | */ 781 | public function init_admin() { 782 | 783 | parent::init_admin(); 784 | 785 | add_action( 'gform_field_standard_settings', array( $this, 'populate_related_items_settings' ), 10, 2 ); 786 | add_filter( 'gform_tooltips', array( $this, 'populate_related_items_tooltip' ) ); 787 | add_action( 'gform_editor_js', array( $this, 'populate_related_items_editor_script' ) ); 788 | 789 | } 790 | 791 | /** 792 | * @param $position 793 | * @param $form_id 794 | */ 795 | public function populate_related_items_settings( $position, $form_id ) { 796 | 797 | if ( -1 === $position ) { 798 | ?> 799 |
%s
', esc_html__( 'Pods Gravity Forms requires that the Pods and Gravity Forms core plugins be installed and activated.', 'pods-gravity-forms' ) ); 120 | } 121 | 122 | } 123 | 124 | /** 125 | * Add Advanced Related Objects 126 | * 127 | * @since 1.3 128 | */ 129 | function pods_gf_add_related_objects() { 130 | 131 | PodsField_Pick::$related_objects['gf-forms'] = [ 132 | 'label' => __( 'Forms', 'pods' ), 133 | 'group' => __( 'Gravity Forms', 'pods' ), 134 | 'simple' => true, 135 | 'data_callback' => 'pods_gf_add_related_objects_forms', 136 | ]; 137 | 138 | } 139 | 140 | add_action( 'pods_form_ui_field_pick_related_objects_other', 'pods_gf_add_related_objects' ); 141 | 142 | /** 143 | * Pods related data callback for GF Forms 144 | * 145 | * @param string $name The name of the field 146 | * @param string|array $value The value of the field 147 | * @param array $options Field options 148 | * @param array $pod Pod data 149 | * @param int $id Item ID 150 | * 151 | * @return array 152 | * 153 | * @since 1.3 154 | */ 155 | function pods_gf_add_related_objects_forms( $name = null, $value = null, $options = null, $pod = null, $id = null ) { 156 | 157 | $data = []; 158 | 159 | // Get all forms. 160 | $forms = RGFormsModel::get_forms( null, 'title' ); 161 | 162 | foreach ( $forms as $form ) { 163 | $form_title = $form->title; 164 | 165 | if ( 1 !== (int) $form->is_active ) { 166 | $form_title = sprintf( '%s (%s)', $form_title, __( 'inactive', 'pods-gravity-forms' ) ); 167 | } 168 | 169 | $data[ $form->id ] = $form_title; 170 | } 171 | 172 | return apply_filters( 'pods_form_ui_field_pick_' . __FUNCTION__, $data, $name, $value, $options, $pod, $id ); 173 | 174 | } 175 | 176 | /** 177 | * Register add-on with Pods Freemius connection. 178 | */ 179 | function pods_gravity_forms_freemius() { 180 | try { 181 | fs_dynamic_init( [ 182 | 'id' => '5754', 183 | 'slug' => 'pods-gravity-forms', 184 | 'type' => 'plugin', 185 | 'public_key' => 'pk_1aaaee6bf8963f2077405e84f2ac5', 186 | 'is_premium' => false, 187 | 'has_paid_plans' => false, 188 | 'is_org_compliant' => true, 189 | 'parent' => [ 190 | 'id' => '5347', 191 | 'slug' => 'pods', 192 | 'public_key' => 'pk_737105490825babae220297e18920', 193 | 'name' => 'Pods', 194 | ], 195 | 'menu' => [ 196 | 'slug' => 'pods-settings', 197 | 'contact' => false, 198 | 'support' => false, 199 | 'affiliation' => false, 200 | 'account' => true, 201 | 'pricing' => false, 202 | 'addons' => true, 203 | 'parent' => [ 204 | 'slug' => 'pods', 205 | ], 206 | ], 207 | ] ); 208 | } catch ( \Exception $exception ) { 209 | return; 210 | } 211 | } 212 | 213 | add_action( 'pods_freemius_init', 'pods_gravity_forms_freemius' ); 214 | -------------------------------------------------------------------------------- /readme.txt: -------------------------------------------------------------------------------- 1 | === Pods Gravity Forms Add-On === 2 | Contributors: sc0ttkclark, jimtrue, naomicbush, gravityplus 3 | Donate link: https://friends.pods.io/ 4 | Tags: pods, gravity forms, form mapping 5 | Requires at least: 6.0 6 | Tested up to: 6.8 7 | Requires PHP: 7.2 8 | Stable tag: 1.5.1 9 | License: GPLv2 or later 10 | License URI: http://www.gnu.org/licenses/gpl-2.0.html 11 | 12 | Integrate with Gravity Forms to create a Pod item from a form submission. 13 | 14 | == Description == 15 | 16 | * **Requires:** [Pods](https://wordpress.org/plugins/pods/) 3.0+, [Gravity Forms](https://pods.io/gravityforms/) 1.9+ 17 | * **Demo:** Want to try Pods GF out? Check out the [Gravity Forms Live Demo](https://www.gravityforms.com/gravity-forms-demo/) and install the Pods and Pods Gravity Forms plugins once you're there 18 | * **Bugs/Ideas:** Please report bugs or request features on [GitHub](https://github.com/pods-framework/pods-gravity-forms/) 19 | 20 | Special thanks to Rocketgenius for their sponsorship support and to Naomi C. Bush for her help in the initial add-on UI work. 21 | 22 | = WP-CLI Command for Syncing Entries = 23 | 24 | This add-on provides the ability to sync entries from a Form Submission and Entry Edit screen. To bulk sync all entries even prior to setting up a Pods Gravity Form Feed, you can run a WP-CLI command. 25 | 26 | **Example 1: Sync all entries for Form 123 first active Pod feed** 27 | 28 | `wp pods-gf sync --form=123` 29 | 30 | **Example 2: Sync all entries for Form 123 using a specific feed (even if it is inactive)** 31 | 32 | `wp pods-gf sync --form=123 --feed=2` 33 | 34 | = Mapping GF List Fields to a Pods Relationship field = 35 | 36 | You can map a GF List field to a Relationship field related to another Pod. Using the below examples you can customize how the automatic mapping works. By default, the list columns will map to the pod fields with the same labels. 37 | 38 | **Example 1: Customize what columns map to which Related Pod fields for Form ID 1, Field ID 2** 39 | 40 | Customizing a list field row can be done by using the `pods_gf_field_columns_mapping` filter, which has Form ID and Field ID variations (`pods_gf_field_columns_mapping_{form_id}` and `pods_gf_field_columns_mapping_{form_id}_{field_id}`). 41 | 42 | ` 43 | add_filter( 'pods_gf_field_columns_mapping_1_2', 'my_columns_mapping', 10, 4 ); 44 | 45 | /** 46 | * Filter list columns mapping for related pod fields. 47 | * 48 | * @param array $columns List field columns. 49 | * @param array $form GF form. 50 | * @param GF_Field $gf_field GF field data. 51 | * @param Pods $pod Pods object. 52 | * 53 | * @return array 54 | */ 55 | function my_columns_mapping( $columns, $form, $gf_field, $related_obj ) { 56 | 57 | $columns[0] = 'first_field'; 58 | $columns[1] = 'second_field'; 59 | $columns[2] = 'third_field'; 60 | 61 | return $columns; 62 | 63 | } 64 | ` 65 | 66 | **Example 2: Customize a List row for Form ID 1, Field ID 2** 67 | 68 | Customizing a list field row can be done by using the `pods_gf_field_column_row` filter, which has Form ID and Field ID variations (`pods_gf_field_column_row_{form_id}` and `pods_gf_field_column_row_{form_id}_{field_id}`). 69 | 70 | ` 71 | add_filter( 'pods_gf_field_column_row_1_2', 'my_column_row_override', 10, 6 ); 72 | 73 | /** 74 | * Filter list field row for relationship field saving purposes. 75 | * 76 | * @param array $row List field row. 77 | * @param array $columns List field columns. 78 | * @param array $form GF form. 79 | * @param GF_Field $gf_field GF field data. 80 | * @param array $options Pods GF options. 81 | * @param Pods|false $related_obj Related Pod object. 82 | * 83 | * @return array 84 | */ 85 | function my_column_row_override( $row, $columns, $form, $gf_field, $options, $related_obj ) { 86 | 87 | // Update certain row fields based on the value of specific column. 88 | if ( ! empty( $row['user_relationship_field'] ) ) { 89 | $user = get_userdata( (int) $row['user'] ); 90 | 91 | // Set the post_title to match the User display name. 92 | if ( $user && ! is_wp_error( $user ) ) { 93 | $row['post_title'] = $user->display_name; 94 | } 95 | } 96 | 97 | return $row; 98 | 99 | } 100 | ` 101 | 102 | == Screenshots == 103 | 104 | 1. In the Pods Admin, create your Pods and Pod Fields: Pods Admin -> Add New 105 | 2. In the Pods Admin, create your Pods and Pod Fields: Pod Edit Screen 106 | 3. Create your Gravity Form that will be used to create a Pod item 107 | 4. Form Settings->Pods menu 108 | 5. Pods feed page 109 | 6. Map form fields to Pod fields 110 | 7. Example form 111 | 8. New Pod item created from form submission 112 | 9. Form entries page showing Pod ID 113 | 114 | == Changelog == 115 | 116 | = 1.5.1 - March 30th, 2025 = 117 | 118 | * Fixed: Resolved issue with auto-delete option when using Pods feeds. (@sc0ttkclark) 119 | * Tested against WP 6.8. (@sc0ttkclark) 120 | 121 | = 1.5.0 - March 29th, 2024 = 122 | 123 | * New requirements that match Pods: WP 6.0+, PHP 7.2+, and Pods 3.0+ to prep for Pods Gravity Forms 2.0 (@sc0ttkclark) 124 | * Added: Support value overrides for checkbox GF fields when prepopulating. (@sc0ttkclark) 125 | * Added: Allow for passing `?pods_gf_debug=1` to a form submit URL to debug the form submission mapping to Pods which outputs debug information and stops before the save runs). (@sc0ttkclark) 126 | * Added: New hook `pods_gf_dynamic_select_show_empty_option` lets you disable showing the "empty option" in dynamic fields being prepopulated. (@sc0ttkclark) 127 | * Added: New hook `pods_gf_addon_options_{$form_id}` that lets you filter the options built for a feed for the `Pods_GF` object. (@sc0ttkclark) 128 | * Tweak: New `pods-gf-ui-view-only` class added to the view-only mode. (@sc0ttkclark) 129 | * Tweak: Expanded secondary submit handling with the ability to have cancel button. (@sc0ttkclark) 130 | * Fixed: Resolved various PHP notices. (@sc0ttkclark) 131 | * Fixed: Removed "comments" from the field mapping options in the feed. (@sc0ttkclark) 132 | * Fixed: Remove extra HTML in the feed labels. (@sc0ttkclark) 133 | * Fixed: For non-select GF field types, trim the dashes on the custom `select_text` option used. (@sc0ttkclark) 134 | * Fixed: Prepopulating field values works more consistently now when passing the prepopulated filter pre-chunked arrays of values. (@sc0ttkclark) 135 | * Fixed: View-only mode forced to use page zero. (@sc0ttkclark) 136 | * Fixed: Prevent duplicate submissions during feed processing. (@sc0ttkclark) 137 | * Fixed: Only allow working with active leads in `Pods_GF_UI`. (@sc0ttkclark) 138 | * Fixed: Prevent Markdown conflicts with other plugins and update PHP 8 compatibility by switching to the Parsedown library. #166 (@sc0ttkclark) 139 | * Fixed: Stop prepopulating fields that aren't opted-in to it. #168 (@sc0ttkclark) 140 | 141 | ALSO: Pods Gravity Forms 2.0 is still in development and it brings complete compatibility with the latest Gravity Forms releases. We could use your support to help it get over the finish line this year. Please consider [donating to the Pods project](https://friends.pods.io/) to help us get there more quickly. 142 | 143 | = 1.4.5 - July 22nd, 2022 = 144 | 145 | * Tested against WP 6.0 146 | * Added: Not seeing something map correctly? As a site admin, you now have the power to debug the form submission and see what might be going on. Add `?pods_gf_debug_gf_to_pods=1` to the URL of the form action before submitting to take advantage of the admin-only debug mode. This will output the values as they would be sent to Pods, the entry information used to reference it, and the feed options used at the time. It will stop the form from completely saving to Pods so you can tweak and debug your form feeds however much you'd like to perfect them. (@sc0ttkclark) 147 | * Fixed: Conditional checks for feeds has been resolved and now won't get confused when there are multiple feeds for the same form in certain cases. (@sc0ttkclark) 148 | * Fixed: Additional compatibility with Gravity Flow. #157 (@JoryHogeveen) 149 | * Pods 2.9 is in beta and after it is released, new mapping of GF List Fields to Pods repeatable fields will be added. This add-on will also be updated with minimum version requirements updated for WP 5.5+, Pods 2.8+, and Gravity Forms 2.5+. Complete testing will be done at that time to ensure complete compatibility. 150 | 151 | = 1.4.4 - October 6th, 2021 = 152 | 153 | * Tested against WP 5.8 154 | * Get ready for Pods 2.8 in just a week! This add-on will receive updates to ensure it is compatible with the latest Gravity Forms and the changes in Pods 2.8 155 | 156 | = 1.4.3 - March 26th, 2020 = 157 | 158 | * Added: Now requiring PHP 5.4+ 159 | * Added: Freemius support when running Pods 2.7.17 160 | * Fixed: Prepopulate handling for relationship fields. 161 | * Fixed: Prevent errors when form doesn't exist by the time it gets to our hook. 162 | 163 | = 1.4.2 - March 2nd, 2020 = 164 | 165 | * Fixed: Ajax handling for various callbacks that hook into `gform_pre_render`. 166 | * Fixed: Cleaned up logic and prevent PHP notices with multi-select arrays when setting up choices arrays. 167 | * Fixed: Make sure `Pods_GF_UI` does not return false on UI callbacks to prevent access errors. 168 | * Fixed: Add mapping feeds to the import/export! (props @travislopes) 169 | 170 | = 1.4.1 - October 16th, 2018 = 171 | 172 | * Fixed: When syncing multiple entries, the field values were caching and not unique per entry resulting in what appeared to be duplicated content inserts/updates. 173 | 174 | = 1.4 - October 16th, 2018 = 175 | 176 | * Support: Added support for Gravity Forms 2.3 database tables changes (You may see a warning on the Edit Pod screen but this is a false positive because we cache a list of all tables to transients and it triggers the warning solved by removing those old "rg" tables) 177 | * Changed: Backwards compatibility issue -- You can now more easily set custom override values, however the old style was not able to be brought over -- you'll want to update your feeds when possible, the old values will not show up and you'll have to select the custom override value option once more, then fill it in 178 | * Changed: Backwards compatibility issue -- Now requiring WordPress 4.6+ 179 | * Feature: When editing entries in the admin area, changes now sync to the associated Pod item (except trash/deletes) 180 | * Feature: New Bulk Entry Syncing to Pods WP-CLI command `wp pods-gf sync --form=123` or you can specify which feed (even if it is not active) with `wp pods-gf sync --form=123 --feed=2` 181 | * Feature: Support for List field mapping to a Pod field which ends up serializing the value, but can be prepopulated back into the Gravity Form 182 | * Feature: List field mapping to relationship fields related to another Pod (list columns map to individual fields in the related Pod) with new filters `pods_gf_field_columns_mapping` and `pods_gf_field_column_row` 183 | * Feature: Support for Chained Select field mapping to a Pod field 184 | * Feature: New Custom fields section added for Pods that support meta (Posts, Terms, Users, Media, and Comments), you can set additional custom fields including ability to set custom values there too 185 | * Feature: Ability to set conditional processing per feed, based on specific values submitted 186 | * Added: Whenever you create a new feed, mapping will automatically be associated between a Gravity Form field and a Pod field if the labels match 187 | * Added: Custom override values now support GF merge tags by default (no insert UI yet) like `{form_id}` and any other merge tag 188 | * Added: Required WP Object Fields in mapping are no longer required if you choose to 'Enable editing with this form using ____' option for Post/Media or User pod types 189 | * Added: Support for E-mail field mappings with 'Confirm E-mail' enabled 190 | * Added: Support for Date fields with multiple inputs (date dropdown / text fields) 191 | * Added: Smarter requirement handling for WP object fields based on object type (only require what the WP insert API requires) 192 | * Added: New mapping fields are now available for more Entry and Payment fields 193 | * Added: New merge tags `{pods.id}` and `{pods.permalink}` are available for usage and in the merge tag selection dropdowns 194 | * Improved: Added headings to each group of feed options so they are easier to work with 195 | * Improved: Address field mapping for Country, State, and CA Provinces now convert properly to their Pods counterparts 196 | * Updated: PHP Markdown library updated to 1.0.2 197 | * Fixed: Issues with using 'bypass' as a save action 198 | * Fixed: Dynamic select options should set the current value (as posted in form) properly 199 | * Fixed: Date/time fields shouldn't auto populate with empty dates such as 0000-00-00 anymore 200 | * Fixed: Additional attachment processing fixes 201 | * Fixed: Lots of Pods GF UI issues resolved 202 | * Fixed: Removed Autocomplete limit (was 30) that was being enforced, now all data from related field will show 203 | * Fixed: Dynamic mapping value checking to support arrays of values 204 | * Fixed: Lots of Prepopulating fixes 205 | * Fixed: Now supports multi page form validation and prepopulating 206 | -------------------------------------------------------------------------------- /ui/pods-gf-admin.js: -------------------------------------------------------------------------------- 1 | jQuery( function() { 2 | 3 | // Handle showing/hiding override value 4 | var $form_settings = jQuery( '.gforms_form_settings' ); 5 | 6 | if ( $form_settings[0] ) { 7 | $form_settings.on( 'change', '.gaddon-setting.gaddon-select', function() { 8 | 9 | var $this = jQuery( this ), 10 | value = $this.val(), 11 | $parent_td = $this.closest( 'td' ), 12 | $custom_override = jQuery( '.pods-custom-override', $parent_td ); 13 | 14 | if ( $custom_override[0] ) { 15 | $custom_override.toggleClass( 'hidden', ( '_pods_custom' !== value ) ); 16 | } 17 | 18 | } ); 19 | } 20 | 21 | } ); -------------------------------------------------------------------------------- /ui/pods-gf.css: -------------------------------------------------------------------------------- 1 | .pods-gf-secondary-submit { 2 | float: right; 3 | } -------------------------------------------------------------------------------- /ui/pods-gf.js: -------------------------------------------------------------------------------- 1 | jQuery( function() { 2 | if ( jQuery( '.pods-gf-save-for-later' )[ 0 ] && 'undefined' != typeof ajaxurl ) { 3 | jQuery( '.gform_page_footer' ).on( 'click', '.pods-gf-save-for-later', function( e ) { 4 | e.preventDefault(); 5 | 6 | var $this = jQuery( this ), 7 | $form = $this.closest( 'form' ); 8 | 9 | // Save all current $_POST 10 | $form.prop( 'action', ajaxurl + '?action=pods_gf_save_for_later&form_id=' + $form.prop( 'id' ) ); 11 | $form.submit(); 12 | } ).on( 'click', '.pods-gf-save-for-later-reset', function( e ) { 13 | e.preventDefault(); 14 | 15 | if ( !confirm( 'Are you sure you want to reset your saved form?' ) ) { 16 | return; 17 | } 18 | 19 | var $this = jQuery( this ), 20 | $form = $this.closest( 'form' ); 21 | 22 | // Clear saved form 23 | $form.prop( 'action', ajaxurl + '?action=pods_gf_save_for_later&form_id=' + $form.prop( 'id' ) + '&pods_gf_clear_saved_form=1&pods_gf_save_for_later_redirect=' + encodeURI( document.location.href ) ); 24 | $form.submit(); 25 | } ); 26 | 27 | jQuery( 'div.gform_wrapper' ).each( function() { 28 | var $this = jQuery( this ), 29 | $pods_save_for_later = jQuery( '.pods-gf-save-for-later', $this ), 30 | $pods_save_for_later_reset = jQuery( '.pods-gf-save-for-later-reset', $this ); 31 | 32 | if ( $pods_save_for_later[ 0 ] ) { 33 | jQuery( '.gform_page_footer', $this ).each( function() { 34 | var $t = jQuery( this ); 35 | 36 | if ( !jQuery( '.pods-gf-save-for-later', $t )[ 0 ] ) { 37 | var $new_save_button = $pods_save_for_later.clone(); 38 | 39 | $new_save_button.appendTo( $t ); 40 | 41 | if ( $pods_save_for_later_reset[ 0 ] ) { 42 | var $new_reset_button = $pods_save_for_later_reset.clone(); 43 | 44 | $new_reset_button.appendTo( $t ); 45 | } 46 | } 47 | } ); 48 | } 49 | } ); 50 | } 51 | 52 | if ( jQuery( '.pods-gf-secondary-submit' )[ 0 ] ) { 53 | jQuery( 'div.gform_wrapper' ).each( function() { 54 | var $this = jQuery( this ), 55 | $pods_secondary_submit = jQuery( '.pods-gf-secondary-submit', $this ); 56 | 57 | if ( $pods_secondary_submit[ 0 ] ) { 58 | jQuery( '.gform_page_footer', $this ).each( function() { 59 | var $t = jQuery( this ), 60 | $secondary_submit = jQuery( '.pods-gf-secondary-submit', $t ); 61 | 62 | $secondary_submit.each( function() { 63 | var $secondary_t = jQuery( this ), 64 | $submit = $this.find( 'input[id^="gform_submit_button_"]' ), 65 | events = $submit.data( 'events' ); 66 | 67 | if ( 'undefined' != typeof events ) { 68 | // Iterate through all event types 69 | jQuery.each( events, function( eventType, eventArray ) { 70 | if ( 'undefined' != typeof eventArray ) { 71 | // Iterate through every bound handler 72 | jQuery.each( eventArray, function( index, event ) { 73 | // Take event namespaces into account 74 | var eventToBind = ( '' != event.namespace ) 75 | ? ( event.type + '.' + event.namespace ) 76 | : ( event.type ); 77 | 78 | // Bind event 79 | $secondary_t.on( eventToBind, event.data, event.handler ); 80 | } ); 81 | } 82 | } ); 83 | } 84 | } ); 85 | } ); 86 | 87 | // Secondary submits for each page of a form 88 | $pods_secondary_submits = jQuery( 'div.gform_page_footer.top_label .pods-gf-secondary-submit', $this ); 89 | 90 | $pods_secondary_submits.each( function() { 91 | var $secondary_submit = jQuery( this ); 92 | 93 | jQuery( '.gform_page_footer' ).not( '.top_label' ).each( function() { 94 | var $page_footer = jQuery( this ); 95 | 96 | $new_secondary = $secondary_submit.clone(); 97 | 98 | $new_secondary.insertBefore( jQuery( 'img.spinner', $page_footer ) ); 99 | } ); 100 | } ); 101 | } 102 | } ); 103 | } 104 | 105 | // Set target page number to zero if we are in a view-only form. 106 | if ( jQuery( '.pods-ui.pods-gf-ui-view-only' )[ 0 ] && 1 < jQuery( '.gform_page' ).length ) { 107 | jQuery( '.pods-ui.pods-gf-ui-view-only' ).find( 'input[id^="gform_target_page_number_"]' ).val( 0 ); 108 | } 109 | } ); -------------------------------------------------------------------------------- /vendor-prefixed/autoload-classmap.php: -------------------------------------------------------------------------------- 1 | $strauss_src . '/erusev/parsedown/Parsedown.php', 9 | ); -------------------------------------------------------------------------------- /vendor-prefixed/autoload.php: -------------------------------------------------------------------------------- 1 | DefinitionData = array(); 28 | 29 | # standardize line breaks 30 | $text = str_replace(array("\r\n", "\r"), "\n", $text); 31 | 32 | # remove surrounding line breaks 33 | $text = trim($text, "\n"); 34 | 35 | # split text into lines 36 | $lines = explode("\n", $text); 37 | 38 | # iterate through lines to identify blocks 39 | $markup = $this->lines($lines); 40 | 41 | # trim line breaks 42 | $markup = trim($markup, "\n"); 43 | 44 | return $markup; 45 | } 46 | 47 | # 48 | # Setters 49 | # 50 | 51 | function setBreaksEnabled($breaksEnabled) 52 | { 53 | $this->breaksEnabled = $breaksEnabled; 54 | 55 | return $this; 56 | } 57 | 58 | protected $breaksEnabled; 59 | 60 | function setMarkupEscaped($markupEscaped) 61 | { 62 | $this->markupEscaped = $markupEscaped; 63 | 64 | return $this; 65 | } 66 | 67 | protected $markupEscaped; 68 | 69 | function setUrlsLinked($urlsLinked) 70 | { 71 | $this->urlsLinked = $urlsLinked; 72 | 73 | return $this; 74 | } 75 | 76 | protected $urlsLinked = true; 77 | 78 | function setSafeMode($safeMode) 79 | { 80 | $this->safeMode = (bool) $safeMode; 81 | 82 | return $this; 83 | } 84 | 85 | protected $safeMode; 86 | 87 | protected $safeLinksWhitelist = array( 88 | 'http://', 89 | 'https://', 90 | 'ftp://', 91 | 'ftps://', 92 | 'mailto:', 93 | 'data:image/png;base64,', 94 | 'data:image/gif;base64,', 95 | 'data:image/jpeg;base64,', 96 | 'irc:', 97 | 'ircs:', 98 | 'git:', 99 | 'ssh:', 100 | 'news:', 101 | 'steam:', 102 | ); 103 | 104 | # 105 | # Lines 106 | # 107 | 108 | protected $BlockTypes = array( 109 | '#' => array('Header'), 110 | '*' => array('Rule', 'List'), 111 | '+' => array('List'), 112 | '-' => array('SetextHeader', 'Table', 'Rule', 'List'), 113 | '0' => array('List'), 114 | '1' => array('List'), 115 | '2' => array('List'), 116 | '3' => array('List'), 117 | '4' => array('List'), 118 | '5' => array('List'), 119 | '6' => array('List'), 120 | '7' => array('List'), 121 | '8' => array('List'), 122 | '9' => array('List'), 123 | ':' => array('Table'), 124 | '<' => array('Comment', 'Markup'), 125 | '=' => array('SetextHeader'), 126 | '>' => array('Quote'), 127 | '[' => array('Reference'), 128 | '_' => array('Rule'), 129 | '`' => array('FencedCode'), 130 | '|' => array('Table'), 131 | '~' => array('FencedCode'), 132 | ); 133 | 134 | # ~ 135 | 136 | protected $unmarkedBlockTypes = array( 137 | 'Code', 138 | ); 139 | 140 | # 141 | # Blocks 142 | # 143 | 144 | protected function lines(array $lines) 145 | { 146 | $CurrentBlock = null; 147 | 148 | foreach ($lines as $line) 149 | { 150 | if (chop($line) === '') 151 | { 152 | if (isset($CurrentBlock)) 153 | { 154 | $CurrentBlock['interrupted'] = true; 155 | } 156 | 157 | continue; 158 | } 159 | 160 | if (strpos($line, "\t") !== false) 161 | { 162 | $parts = explode("\t", $line); 163 | 164 | $line = $parts[0]; 165 | 166 | unset($parts[0]); 167 | 168 | foreach ($parts as $part) 169 | { 170 | $shortage = 4 - mb_strlen($line, 'utf-8') % 4; 171 | 172 | $line .= str_repeat(' ', $shortage); 173 | $line .= $part; 174 | } 175 | } 176 | 177 | $indent = 0; 178 | 179 | while (isset($line[$indent]) and $line[$indent] === ' ') 180 | { 181 | $indent ++; 182 | } 183 | 184 | $text = $indent > 0 ? substr($line, $indent) : $line; 185 | 186 | # ~ 187 | 188 | $Line = array('body' => $line, 'indent' => $indent, 'text' => $text); 189 | 190 | # ~ 191 | 192 | if (isset($CurrentBlock['continuable'])) 193 | { 194 | $Block = $this->{'block'.$CurrentBlock['type'].'Continue'}($Line, $CurrentBlock); 195 | 196 | if (isset($Block)) 197 | { 198 | $CurrentBlock = $Block; 199 | 200 | continue; 201 | } 202 | else 203 | { 204 | if ($this->isBlockCompletable($CurrentBlock['type'])) 205 | { 206 | $CurrentBlock = $this->{'block'.$CurrentBlock['type'].'Complete'}($CurrentBlock); 207 | } 208 | } 209 | } 210 | 211 | # ~ 212 | 213 | $marker = $text[0]; 214 | 215 | # ~ 216 | 217 | $blockTypes = $this->unmarkedBlockTypes; 218 | 219 | if (isset($this->BlockTypes[$marker])) 220 | { 221 | foreach ($this->BlockTypes[$marker] as $blockType) 222 | { 223 | $blockTypes []= $blockType; 224 | } 225 | } 226 | 227 | # 228 | # ~ 229 | 230 | foreach ($blockTypes as $blockType) 231 | { 232 | $Block = $this->{'block'.$blockType}($Line, $CurrentBlock); 233 | 234 | if (isset($Block)) 235 | { 236 | $Block['type'] = $blockType; 237 | 238 | if ( ! isset($Block['identified'])) 239 | { 240 | $Blocks []= $CurrentBlock; 241 | 242 | $Block['identified'] = true; 243 | } 244 | 245 | if ($this->isBlockContinuable($blockType)) 246 | { 247 | $Block['continuable'] = true; 248 | } 249 | 250 | $CurrentBlock = $Block; 251 | 252 | continue 2; 253 | } 254 | } 255 | 256 | # ~ 257 | 258 | if (isset($CurrentBlock) and ! isset($CurrentBlock['type']) and ! isset($CurrentBlock['interrupted'])) 259 | { 260 | $CurrentBlock['element']['text'] .= "\n".$text; 261 | } 262 | else 263 | { 264 | $Blocks []= $CurrentBlock; 265 | 266 | $CurrentBlock = $this->paragraph($Line); 267 | 268 | $CurrentBlock['identified'] = true; 269 | } 270 | } 271 | 272 | # ~ 273 | 274 | if (isset($CurrentBlock['continuable']) and $this->isBlockCompletable($CurrentBlock['type'])) 275 | { 276 | $CurrentBlock = $this->{'block'.$CurrentBlock['type'].'Complete'}($CurrentBlock); 277 | } 278 | 279 | # ~ 280 | 281 | $Blocks []= $CurrentBlock; 282 | 283 | unset($Blocks[0]); 284 | 285 | # ~ 286 | 287 | $markup = ''; 288 | 289 | foreach ($Blocks as $Block) 290 | { 291 | if (isset($Block['hidden'])) 292 | { 293 | continue; 294 | } 295 | 296 | $markup .= "\n"; 297 | $markup .= isset($Block['markup']) ? $Block['markup'] : $this->element($Block['element']); 298 | } 299 | 300 | $markup .= "\n"; 301 | 302 | # ~ 303 | 304 | return $markup; 305 | } 306 | 307 | protected function isBlockContinuable($Type) 308 | { 309 | return method_exists($this, 'block'.$Type.'Continue'); 310 | } 311 | 312 | protected function isBlockCompletable($Type) 313 | { 314 | return method_exists($this, 'block'.$Type.'Complete'); 315 | } 316 | 317 | # 318 | # Code 319 | 320 | protected function blockCode($Line, $Block = null) 321 | { 322 | if (isset($Block) and ! isset($Block['type']) and ! isset($Block['interrupted'])) 323 | { 324 | return; 325 | } 326 | 327 | if ($Line['indent'] >= 4) 328 | { 329 | $text = substr($Line['body'], 4); 330 | 331 | $Block = array( 332 | 'element' => array( 333 | 'name' => 'pre', 334 | 'handler' => 'element', 335 | 'text' => array( 336 | 'name' => 'code', 337 | 'text' => $text, 338 | ), 339 | ), 340 | ); 341 | 342 | return $Block; 343 | } 344 | } 345 | 346 | protected function blockCodeContinue($Line, $Block) 347 | { 348 | if ($Line['indent'] >= 4) 349 | { 350 | if (isset($Block['interrupted'])) 351 | { 352 | $Block['element']['text']['text'] .= "\n"; 353 | 354 | unset($Block['interrupted']); 355 | } 356 | 357 | $Block['element']['text']['text'] .= "\n"; 358 | 359 | $text = substr($Line['body'], 4); 360 | 361 | $Block['element']['text']['text'] .= $text; 362 | 363 | return $Block; 364 | } 365 | } 366 | 367 | protected function blockCodeComplete($Block) 368 | { 369 | $text = $Block['element']['text']['text']; 370 | 371 | $Block['element']['text']['text'] = $text; 372 | 373 | return $Block; 374 | } 375 | 376 | # 377 | # Comment 378 | 379 | protected function blockComment($Line) 380 | { 381 | if ($this->markupEscaped or $this->safeMode) 382 | { 383 | return; 384 | } 385 | 386 | if (isset($Line['text'][3]) and $Line['text'][3] === '-' and $Line['text'][2] === '-' and $Line['text'][1] === '!') 387 | { 388 | $Block = array( 389 | 'markup' => $Line['body'], 390 | ); 391 | 392 | if (preg_match('/-->$/', $Line['text'])) 393 | { 394 | $Block['closed'] = true; 395 | } 396 | 397 | return $Block; 398 | } 399 | } 400 | 401 | protected function blockCommentContinue($Line, array $Block) 402 | { 403 | if (isset($Block['closed'])) 404 | { 405 | return; 406 | } 407 | 408 | $Block['markup'] .= "\n" . $Line['body']; 409 | 410 | if (preg_match('/-->$/', $Line['text'])) 411 | { 412 | $Block['closed'] = true; 413 | } 414 | 415 | return $Block; 416 | } 417 | 418 | # 419 | # Fenced Code 420 | 421 | protected function blockFencedCode($Line) 422 | { 423 | if (preg_match('/^['.$Line['text'][0].']{3,}[ ]*([^`]+)?[ ]*$/', $Line['text'], $matches)) 424 | { 425 | $Element = array( 426 | 'name' => 'code', 427 | 'text' => '', 428 | ); 429 | 430 | if (isset($matches[1])) 431 | { 432 | /** 433 | * https://www.w3.org/TR/2011/WD-html5-20110525/elements.html#classes 434 | * Every HTML element may have a class attribute specified. 435 | * The attribute, if specified, must have a value that is a set 436 | * of space-separated tokens representing the various classes 437 | * that the element belongs to. 438 | * [...] 439 | * The space characters, for the purposes of this specification, 440 | * are U+0020 SPACE, U+0009 CHARACTER TABULATION (tab), 441 | * U+000A LINE FEED (LF), U+000C FORM FEED (FF), and 442 | * U+000D CARRIAGE RETURN (CR). 443 | * 444 | @license MIT 445 | Modified by Pods Framework Team on 29-March-2024 using {@see https://github.com/BrianHenryIE/strauss}. 446 | */ 447 | $language = substr($matches[1], 0, strcspn($matches[1], " \t\n\f\r")); 448 | 449 | $class = 'language-'.$language; 450 | 451 | $Element['attributes'] = array( 452 | 'class' => $class, 453 | ); 454 | } 455 | 456 | $Block = array( 457 | 'char' => $Line['text'][0], 458 | 'element' => array( 459 | 'name' => 'pre', 460 | 'handler' => 'element', 461 | 'text' => $Element, 462 | ), 463 | ); 464 | 465 | return $Block; 466 | } 467 | } 468 | 469 | protected function blockFencedCodeContinue($Line, $Block) 470 | { 471 | if (isset($Block['complete'])) 472 | { 473 | return; 474 | } 475 | 476 | if (isset($Block['interrupted'])) 477 | { 478 | $Block['element']['text']['text'] .= "\n"; 479 | 480 | unset($Block['interrupted']); 481 | } 482 | 483 | if (preg_match('/^'.$Block['char'].'{3,}[ ]*$/', $Line['text'])) 484 | { 485 | $Block['element']['text']['text'] = substr($Block['element']['text']['text'], 1); 486 | 487 | $Block['complete'] = true; 488 | 489 | return $Block; 490 | } 491 | 492 | $Block['element']['text']['text'] .= "\n".$Line['body']; 493 | 494 | return $Block; 495 | } 496 | 497 | protected function blockFencedCodeComplete($Block) 498 | { 499 | $text = $Block['element']['text']['text']; 500 | 501 | $Block['element']['text']['text'] = $text; 502 | 503 | return $Block; 504 | } 505 | 506 | # 507 | # Header 508 | 509 | protected function blockHeader($Line) 510 | { 511 | if (isset($Line['text'][1])) 512 | { 513 | $level = 1; 514 | 515 | while (isset($Line['text'][$level]) and $Line['text'][$level] === '#') 516 | { 517 | $level ++; 518 | } 519 | 520 | if ($level > 6) 521 | { 522 | return; 523 | } 524 | 525 | $text = trim($Line['text'], '# '); 526 | 527 | $Block = array( 528 | 'element' => array( 529 | 'name' => 'h' . min(6, $level), 530 | 'text' => $text, 531 | 'handler' => 'line', 532 | ), 533 | ); 534 | 535 | return $Block; 536 | } 537 | } 538 | 539 | # 540 | # List 541 | 542 | protected function blockList($Line) 543 | { 544 | list($name, $pattern) = $Line['text'][0] <= '-' ? array('ul', '[*+-]') : array('ol', '[0-9]+[.]'); 545 | 546 | if (preg_match('/^('.$pattern.'[ ]+)(.*)/', $Line['text'], $matches)) 547 | { 548 | $Block = array( 549 | 'indent' => $Line['indent'], 550 | 'pattern' => $pattern, 551 | 'element' => array( 552 | 'name' => $name, 553 | 'handler' => 'elements', 554 | ), 555 | ); 556 | 557 | if($name === 'ol') 558 | { 559 | $listStart = stristr($matches[0], '.', true); 560 | 561 | if($listStart !== '1') 562 | { 563 | $Block['element']['attributes'] = array('start' => $listStart); 564 | } 565 | } 566 | 567 | $Block['li'] = array( 568 | 'name' => 'li', 569 | 'handler' => 'li', 570 | 'text' => array( 571 | $matches[2], 572 | ), 573 | ); 574 | 575 | $Block['element']['text'] []= & $Block['li']; 576 | 577 | return $Block; 578 | } 579 | } 580 | 581 | protected function blockListContinue($Line, array $Block) 582 | { 583 | if ($Block['indent'] === $Line['indent'] and preg_match('/^'.$Block['pattern'].'(?:[ ]+(.*)|$)/', $Line['text'], $matches)) 584 | { 585 | if (isset($Block['interrupted'])) 586 | { 587 | $Block['li']['text'] []= ''; 588 | 589 | $Block['loose'] = true; 590 | 591 | unset($Block['interrupted']); 592 | } 593 | 594 | unset($Block['li']); 595 | 596 | $text = isset($matches[1]) ? $matches[1] : ''; 597 | 598 | $Block['li'] = array( 599 | 'name' => 'li', 600 | 'handler' => 'li', 601 | 'text' => array( 602 | $text, 603 | ), 604 | ); 605 | 606 | $Block['element']['text'] []= & $Block['li']; 607 | 608 | return $Block; 609 | } 610 | 611 | if ($Line['text'][0] === '[' and $this->blockReference($Line)) 612 | { 613 | return $Block; 614 | } 615 | 616 | if ( ! isset($Block['interrupted'])) 617 | { 618 | $text = preg_replace('/^[ ]{0,4}/', '', $Line['body']); 619 | 620 | $Block['li']['text'] []= $text; 621 | 622 | return $Block; 623 | } 624 | 625 | if ($Line['indent'] > 0) 626 | { 627 | $Block['li']['text'] []= ''; 628 | 629 | $text = preg_replace('/^[ ]{0,4}/', '', $Line['body']); 630 | 631 | $Block['li']['text'] []= $text; 632 | 633 | unset($Block['interrupted']); 634 | 635 | return $Block; 636 | } 637 | } 638 | 639 | protected function blockListComplete(array $Block) 640 | { 641 | if (isset($Block['loose'])) 642 | { 643 | foreach ($Block['element']['text'] as &$li) 644 | { 645 | if (end($li['text']) !== '') 646 | { 647 | $li['text'] []= ''; 648 | } 649 | } 650 | } 651 | 652 | return $Block; 653 | } 654 | 655 | # 656 | # Quote 657 | 658 | protected function blockQuote($Line) 659 | { 660 | if (preg_match('/^>[ ]?(.*)/', $Line['text'], $matches)) 661 | { 662 | $Block = array( 663 | 'element' => array( 664 | 'name' => 'blockquote', 665 | 'handler' => 'lines', 666 | 'text' => (array) $matches[1], 667 | ), 668 | ); 669 | 670 | return $Block; 671 | } 672 | } 673 | 674 | protected function blockQuoteContinue($Line, array $Block) 675 | { 676 | if ($Line['text'][0] === '>' and preg_match('/^>[ ]?(.*)/', $Line['text'], $matches)) 677 | { 678 | if (isset($Block['interrupted'])) 679 | { 680 | $Block['element']['text'] []= ''; 681 | 682 | unset($Block['interrupted']); 683 | } 684 | 685 | $Block['element']['text'] []= $matches[1]; 686 | 687 | return $Block; 688 | } 689 | 690 | if ( ! isset($Block['interrupted'])) 691 | { 692 | $Block['element']['text'] []= $Line['text']; 693 | 694 | return $Block; 695 | } 696 | } 697 | 698 | # 699 | # Rule 700 | 701 | protected function blockRule($Line) 702 | { 703 | if (preg_match('/^(['.$Line['text'][0].'])([ ]*\1){2,}[ ]*$/', $Line['text'])) 704 | { 705 | $Block = array( 706 | 'element' => array( 707 | 'name' => 'hr' 708 | ), 709 | ); 710 | 711 | return $Block; 712 | } 713 | } 714 | 715 | # 716 | # Setext 717 | 718 | protected function blockSetextHeader($Line, array $Block = null) 719 | { 720 | if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted'])) 721 | { 722 | return; 723 | } 724 | 725 | if (chop($Line['text'], $Line['text'][0]) === '') 726 | { 727 | $Block['element']['name'] = $Line['text'][0] === '=' ? 'h1' : 'h2'; 728 | 729 | return $Block; 730 | } 731 | } 732 | 733 | # 734 | # Markup 735 | 736 | protected function blockMarkup($Line) 737 | { 738 | if ($this->markupEscaped or $this->safeMode) 739 | { 740 | return; 741 | } 742 | 743 | if (preg_match('/^<(\w[\w-]*)(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*(\/)?>/', $Line['text'], $matches)) 744 | { 745 | $element = strtolower($matches[1]); 746 | 747 | if (in_array($element, $this->textLevelElements)) 748 | { 749 | return; 750 | } 751 | 752 | $Block = array( 753 | 'name' => $matches[1], 754 | 'depth' => 0, 755 | 'markup' => $Line['text'], 756 | ); 757 | 758 | $length = strlen($matches[0]); 759 | 760 | $remainder = substr($Line['text'], $length); 761 | 762 | if (trim($remainder) === '') 763 | { 764 | if (isset($matches[2]) or in_array($matches[1], $this->voidElements)) 765 | { 766 | $Block['closed'] = true; 767 | 768 | $Block['void'] = true; 769 | } 770 | } 771 | else 772 | { 773 | if (isset($matches[2]) or in_array($matches[1], $this->voidElements)) 774 | { 775 | return; 776 | } 777 | 778 | if (preg_match('/<\/'.$matches[1].'>[ ]*$/i', $remainder)) 779 | { 780 | $Block['closed'] = true; 781 | } 782 | } 783 | 784 | return $Block; 785 | } 786 | } 787 | 788 | protected function blockMarkupContinue($Line, array $Block) 789 | { 790 | if (isset($Block['closed'])) 791 | { 792 | return; 793 | } 794 | 795 | if (preg_match('/^<'.$Block['name'].'(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*>/i', $Line['text'])) # open 796 | { 797 | $Block['depth'] ++; 798 | } 799 | 800 | if (preg_match('/(.*?)<\/'.$Block['name'].'>[ ]*$/i', $Line['text'], $matches)) # close 801 | { 802 | if ($Block['depth'] > 0) 803 | { 804 | $Block['depth'] --; 805 | } 806 | else 807 | { 808 | $Block['closed'] = true; 809 | } 810 | } 811 | 812 | if (isset($Block['interrupted'])) 813 | { 814 | $Block['markup'] .= "\n"; 815 | 816 | unset($Block['interrupted']); 817 | } 818 | 819 | $Block['markup'] .= "\n".$Line['body']; 820 | 821 | return $Block; 822 | } 823 | 824 | # 825 | # Reference 826 | 827 | protected function blockReference($Line) 828 | { 829 | if (preg_match('/^\[(.+?)\]:[ ]*(\S+?)>?(?:[ ]+["\'(](.+)["\')])?[ ]*$/', $Line['text'], $matches)) 830 | { 831 | $id = strtolower($matches[1]); 832 | 833 | $Data = array( 834 | 'url' => $matches[2], 835 | 'title' => null, 836 | ); 837 | 838 | if (isset($matches[3])) 839 | { 840 | $Data['title'] = $matches[3]; 841 | } 842 | 843 | $this->DefinitionData['Reference'][$id] = $Data; 844 | 845 | $Block = array( 846 | 'hidden' => true, 847 | ); 848 | 849 | return $Block; 850 | } 851 | } 852 | 853 | # 854 | # Table 855 | 856 | protected function blockTable($Line, array $Block = null) 857 | { 858 | if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted'])) 859 | { 860 | return; 861 | } 862 | 863 | if (strpos($Block['element']['text'], '|') !== false and chop($Line['text'], ' -:|') === '') 864 | { 865 | $alignments = array(); 866 | 867 | $divider = $Line['text']; 868 | 869 | $divider = trim($divider); 870 | $divider = trim($divider, '|'); 871 | 872 | $dividerCells = explode('|', $divider); 873 | 874 | foreach ($dividerCells as $dividerCell) 875 | { 876 | $dividerCell = trim($dividerCell); 877 | 878 | if ($dividerCell === '') 879 | { 880 | continue; 881 | } 882 | 883 | $alignment = null; 884 | 885 | if ($dividerCell[0] === ':') 886 | { 887 | $alignment = 'left'; 888 | } 889 | 890 | if (substr($dividerCell, - 1) === ':') 891 | { 892 | $alignment = $alignment === 'left' ? 'center' : 'right'; 893 | } 894 | 895 | $alignments []= $alignment; 896 | } 897 | 898 | # ~ 899 | 900 | $HeaderElements = array(); 901 | 902 | $header = $Block['element']['text']; 903 | 904 | $header = trim($header); 905 | $header = trim($header, '|'); 906 | 907 | $headerCells = explode('|', $header); 908 | 909 | foreach ($headerCells as $index => $headerCell) 910 | { 911 | $headerCell = trim($headerCell); 912 | 913 | $HeaderElement = array( 914 | 'name' => 'th', 915 | 'text' => $headerCell, 916 | 'handler' => 'line', 917 | ); 918 | 919 | if (isset($alignments[$index])) 920 | { 921 | $alignment = $alignments[$index]; 922 | 923 | $HeaderElement['attributes'] = array( 924 | 'style' => 'text-align: '.$alignment.';', 925 | ); 926 | } 927 | 928 | $HeaderElements []= $HeaderElement; 929 | } 930 | 931 | # ~ 932 | 933 | $Block = array( 934 | 'alignments' => $alignments, 935 | 'identified' => true, 936 | 'element' => array( 937 | 'name' => 'table', 938 | 'handler' => 'elements', 939 | ), 940 | ); 941 | 942 | $Block['element']['text'] []= array( 943 | 'name' => 'thead', 944 | 'handler' => 'elements', 945 | ); 946 | 947 | $Block['element']['text'] []= array( 948 | 'name' => 'tbody', 949 | 'handler' => 'elements', 950 | 'text' => array(), 951 | ); 952 | 953 | $Block['element']['text'][0]['text'] []= array( 954 | 'name' => 'tr', 955 | 'handler' => 'elements', 956 | 'text' => $HeaderElements, 957 | ); 958 | 959 | return $Block; 960 | } 961 | } 962 | 963 | protected function blockTableContinue($Line, array $Block) 964 | { 965 | if (isset($Block['interrupted'])) 966 | { 967 | return; 968 | } 969 | 970 | if ($Line['text'][0] === '|' or strpos($Line['text'], '|')) 971 | { 972 | $Elements = array(); 973 | 974 | $row = $Line['text']; 975 | 976 | $row = trim($row); 977 | $row = trim($row, '|'); 978 | 979 | preg_match_all('/(?:(\\\\[|])|[^|`]|`[^`]+`|`)+/', $row, $matches); 980 | 981 | foreach ($matches[0] as $index => $cell) 982 | { 983 | $cell = trim($cell); 984 | 985 | $Element = array( 986 | 'name' => 'td', 987 | 'handler' => 'line', 988 | 'text' => $cell, 989 | ); 990 | 991 | if (isset($Block['alignments'][$index])) 992 | { 993 | $Element['attributes'] = array( 994 | 'style' => 'text-align: '.$Block['alignments'][$index].';', 995 | ); 996 | } 997 | 998 | $Elements []= $Element; 999 | } 1000 | 1001 | $Element = array( 1002 | 'name' => 'tr', 1003 | 'handler' => 'elements', 1004 | 'text' => $Elements, 1005 | ); 1006 | 1007 | $Block['element']['text'][1]['text'] []= $Element; 1008 | 1009 | return $Block; 1010 | } 1011 | } 1012 | 1013 | # 1014 | # ~ 1015 | # 1016 | 1017 | protected function paragraph($Line) 1018 | { 1019 | $Block = array( 1020 | 'element' => array( 1021 | 'name' => 'p', 1022 | 'text' => $Line['text'], 1023 | 'handler' => 'line', 1024 | ), 1025 | ); 1026 | 1027 | return $Block; 1028 | } 1029 | 1030 | # 1031 | # Inline Elements 1032 | # 1033 | 1034 | protected $InlineTypes = array( 1035 | '"' => array('SpecialCharacter'), 1036 | '!' => array('Image'), 1037 | '&' => array('SpecialCharacter'), 1038 | '*' => array('Emphasis'), 1039 | ':' => array('Url'), 1040 | '<' => array('UrlTag', 'EmailTag', 'Markup', 'SpecialCharacter'), 1041 | '>' => array('SpecialCharacter'), 1042 | '[' => array('Link'), 1043 | '_' => array('Emphasis'), 1044 | '`' => array('Code'), 1045 | '~' => array('Strikethrough'), 1046 | '\\' => array('EscapeSequence'), 1047 | ); 1048 | 1049 | # ~ 1050 | 1051 | protected $inlineMarkerList = '!"*_&[:<>`~\\'; 1052 | 1053 | # 1054 | # ~ 1055 | # 1056 | 1057 | public function line($text, $nonNestables=array()) 1058 | { 1059 | $markup = ''; 1060 | 1061 | # $excerpt is based on the first occurrence of a marker 1062 | 1063 | while ($excerpt = strpbrk($text, $this->inlineMarkerList)) 1064 | { 1065 | $marker = $excerpt[0]; 1066 | 1067 | $markerPosition = strpos($text, $marker); 1068 | 1069 | $Excerpt = array('text' => $excerpt, 'context' => $text); 1070 | 1071 | foreach ($this->InlineTypes[$marker] as $inlineType) 1072 | { 1073 | # check to see if the current inline type is nestable in the current context 1074 | 1075 | if ( ! empty($nonNestables) and in_array($inlineType, $nonNestables)) 1076 | { 1077 | continue; 1078 | } 1079 | 1080 | $Inline = $this->{'inline'.$inlineType}($Excerpt); 1081 | 1082 | if ( ! isset($Inline)) 1083 | { 1084 | continue; 1085 | } 1086 | 1087 | # makes sure that the inline belongs to "our" marker 1088 | 1089 | if (isset($Inline['position']) and $Inline['position'] > $markerPosition) 1090 | { 1091 | continue; 1092 | } 1093 | 1094 | # sets a default inline position 1095 | 1096 | if ( ! isset($Inline['position'])) 1097 | { 1098 | $Inline['position'] = $markerPosition; 1099 | } 1100 | 1101 | # cause the new element to 'inherit' our non nestables 1102 | 1103 | foreach ($nonNestables as $non_nestable) 1104 | { 1105 | $Inline['element']['nonNestables'][] = $non_nestable; 1106 | } 1107 | 1108 | # the text that comes before the inline 1109 | $unmarkedText = substr($text, 0, $Inline['position']); 1110 | 1111 | # compile the unmarked text 1112 | $markup .= $this->unmarkedText($unmarkedText); 1113 | 1114 | # compile the inline 1115 | $markup .= isset($Inline['markup']) ? $Inline['markup'] : $this->element($Inline['element']); 1116 | 1117 | # remove the examined text 1118 | $text = substr($text, $Inline['position'] + $Inline['extent']); 1119 | 1120 | continue 2; 1121 | } 1122 | 1123 | # the marker does not belong to an inline 1124 | 1125 | $unmarkedText = substr($text, 0, $markerPosition + 1); 1126 | 1127 | $markup .= $this->unmarkedText($unmarkedText); 1128 | 1129 | $text = substr($text, $markerPosition + 1); 1130 | } 1131 | 1132 | $markup .= $this->unmarkedText($text); 1133 | 1134 | return $markup; 1135 | } 1136 | 1137 | # 1138 | # ~ 1139 | # 1140 | 1141 | protected function inlineCode($Excerpt) 1142 | { 1143 | $marker = $Excerpt['text'][0]; 1144 | 1145 | if (preg_match('/^('.$marker.'+)[ ]*(.+?)[ ]*(? strlen($matches[0]), 1152 | 'element' => array( 1153 | 'name' => 'code', 1154 | 'text' => $text, 1155 | ), 1156 | ); 1157 | } 1158 | } 1159 | 1160 | protected function inlineEmailTag($Excerpt) 1161 | { 1162 | if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<((mailto:)?\S+?@\S+?)>/i', $Excerpt['text'], $matches)) 1163 | { 1164 | $url = $matches[1]; 1165 | 1166 | if ( ! isset($matches[2])) 1167 | { 1168 | $url = 'mailto:' . $url; 1169 | } 1170 | 1171 | return array( 1172 | 'extent' => strlen($matches[0]), 1173 | 'element' => array( 1174 | 'name' => 'a', 1175 | 'text' => $matches[1], 1176 | 'attributes' => array( 1177 | 'href' => $url, 1178 | ), 1179 | ), 1180 | ); 1181 | } 1182 | } 1183 | 1184 | protected function inlineEmphasis($Excerpt) 1185 | { 1186 | if ( ! isset($Excerpt['text'][1])) 1187 | { 1188 | return; 1189 | } 1190 | 1191 | $marker = $Excerpt['text'][0]; 1192 | 1193 | if ($Excerpt['text'][1] === $marker and preg_match($this->StrongRegex[$marker], $Excerpt['text'], $matches)) 1194 | { 1195 | $emphasis = 'strong'; 1196 | } 1197 | elseif (preg_match($this->EmRegex[$marker], $Excerpt['text'], $matches)) 1198 | { 1199 | $emphasis = 'em'; 1200 | } 1201 | else 1202 | { 1203 | return; 1204 | } 1205 | 1206 | return array( 1207 | 'extent' => strlen($matches[0]), 1208 | 'element' => array( 1209 | 'name' => $emphasis, 1210 | 'handler' => 'line', 1211 | 'text' => $matches[1], 1212 | ), 1213 | ); 1214 | } 1215 | 1216 | protected function inlineEscapeSequence($Excerpt) 1217 | { 1218 | if (isset($Excerpt['text'][1]) and in_array($Excerpt['text'][1], $this->specialCharacters)) 1219 | { 1220 | return array( 1221 | 'markup' => $Excerpt['text'][1], 1222 | 'extent' => 2, 1223 | ); 1224 | } 1225 | } 1226 | 1227 | protected function inlineImage($Excerpt) 1228 | { 1229 | if ( ! isset($Excerpt['text'][1]) or $Excerpt['text'][1] !== '[') 1230 | { 1231 | return; 1232 | } 1233 | 1234 | $Excerpt['text']= substr($Excerpt['text'], 1); 1235 | 1236 | $Link = $this->inlineLink($Excerpt); 1237 | 1238 | if ($Link === null) 1239 | { 1240 | return; 1241 | } 1242 | 1243 | $Inline = array( 1244 | 'extent' => $Link['extent'] + 1, 1245 | 'element' => array( 1246 | 'name' => 'img', 1247 | 'attributes' => array( 1248 | 'src' => $Link['element']['attributes']['href'], 1249 | 'alt' => $Link['element']['text'], 1250 | ), 1251 | ), 1252 | ); 1253 | 1254 | $Inline['element']['attributes'] += $Link['element']['attributes']; 1255 | 1256 | unset($Inline['element']['attributes']['href']); 1257 | 1258 | return $Inline; 1259 | } 1260 | 1261 | protected function inlineLink($Excerpt) 1262 | { 1263 | $Element = array( 1264 | 'name' => 'a', 1265 | 'handler' => 'line', 1266 | 'nonNestables' => array('Url', 'Link'), 1267 | 'text' => null, 1268 | 'attributes' => array( 1269 | 'href' => null, 1270 | 'title' => null, 1271 | ), 1272 | ); 1273 | 1274 | $extent = 0; 1275 | 1276 | $remainder = $Excerpt['text']; 1277 | 1278 | if (preg_match('/\[((?:[^][]++|(?R))*+)\]/', $remainder, $matches)) 1279 | { 1280 | $Element['text'] = $matches[1]; 1281 | 1282 | $extent += strlen($matches[0]); 1283 | 1284 | $remainder = substr($remainder, $extent); 1285 | } 1286 | else 1287 | { 1288 | return; 1289 | } 1290 | 1291 | if (preg_match('/^[(]\s*+((?:[^ ()]++|[(][^ )]+[)])++)(?:[ ]+("[^"]*"|\'[^\']*\'))?\s*[)]/', $remainder, $matches)) 1292 | { 1293 | $Element['attributes']['href'] = $matches[1]; 1294 | 1295 | if (isset($matches[2])) 1296 | { 1297 | $Element['attributes']['title'] = substr($matches[2], 1, - 1); 1298 | } 1299 | 1300 | $extent += strlen($matches[0]); 1301 | } 1302 | else 1303 | { 1304 | if (preg_match('/^\s*\[(.*?)\]/', $remainder, $matches)) 1305 | { 1306 | $definition = strlen($matches[1]) ? $matches[1] : $Element['text']; 1307 | $definition = strtolower($definition); 1308 | 1309 | $extent += strlen($matches[0]); 1310 | } 1311 | else 1312 | { 1313 | $definition = strtolower($Element['text']); 1314 | } 1315 | 1316 | if ( ! isset($this->DefinitionData['Reference'][$definition])) 1317 | { 1318 | return; 1319 | } 1320 | 1321 | $Definition = $this->DefinitionData['Reference'][$definition]; 1322 | 1323 | $Element['attributes']['href'] = $Definition['url']; 1324 | $Element['attributes']['title'] = $Definition['title']; 1325 | } 1326 | 1327 | return array( 1328 | 'extent' => $extent, 1329 | 'element' => $Element, 1330 | ); 1331 | } 1332 | 1333 | protected function inlineMarkup($Excerpt) 1334 | { 1335 | if ($this->markupEscaped or $this->safeMode or strpos($Excerpt['text'], '>') === false) 1336 | { 1337 | return; 1338 | } 1339 | 1340 | if ($Excerpt['text'][1] === '/' and preg_match('/^<\/\w[\w-]*[ ]*>/s', $Excerpt['text'], $matches)) 1341 | { 1342 | return array( 1343 | 'markup' => $matches[0], 1344 | 'extent' => strlen($matches[0]), 1345 | ); 1346 | } 1347 | 1348 | if ($Excerpt['text'][1] === '!' and preg_match('/^/s', $Excerpt['text'], $matches)) 1349 | { 1350 | return array( 1351 | 'markup' => $matches[0], 1352 | 'extent' => strlen($matches[0]), 1353 | ); 1354 | } 1355 | 1356 | if ($Excerpt['text'][1] !== ' ' and preg_match('/^<\w[\w-]*(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*\/?>/s', $Excerpt['text'], $matches)) 1357 | { 1358 | return array( 1359 | 'markup' => $matches[0], 1360 | 'extent' => strlen($matches[0]), 1361 | ); 1362 | } 1363 | } 1364 | 1365 | protected function inlineSpecialCharacter($Excerpt) 1366 | { 1367 | if ($Excerpt['text'][0] === '&' and ! preg_match('/^?\w+;/', $Excerpt['text'])) 1368 | { 1369 | return array( 1370 | 'markup' => '&', 1371 | 'extent' => 1, 1372 | ); 1373 | } 1374 | 1375 | $SpecialCharacter = array('>' => 'gt', '<' => 'lt', '"' => 'quot'); 1376 | 1377 | if (isset($SpecialCharacter[$Excerpt['text'][0]])) 1378 | { 1379 | return array( 1380 | 'markup' => '&'.$SpecialCharacter[$Excerpt['text'][0]].';', 1381 | 'extent' => 1, 1382 | ); 1383 | } 1384 | } 1385 | 1386 | protected function inlineStrikethrough($Excerpt) 1387 | { 1388 | if ( ! isset($Excerpt['text'][1])) 1389 | { 1390 | return; 1391 | } 1392 | 1393 | if ($Excerpt['text'][1] === '~' and preg_match('/^~~(?=\S)(.+?)(?<=\S)~~/', $Excerpt['text'], $matches)) 1394 | { 1395 | return array( 1396 | 'extent' => strlen($matches[0]), 1397 | 'element' => array( 1398 | 'name' => 'del', 1399 | 'text' => $matches[1], 1400 | 'handler' => 'line', 1401 | ), 1402 | ); 1403 | } 1404 | } 1405 | 1406 | protected function inlineUrl($Excerpt) 1407 | { 1408 | if ($this->urlsLinked !== true or ! isset($Excerpt['text'][2]) or $Excerpt['text'][2] !== '/') 1409 | { 1410 | return; 1411 | } 1412 | 1413 | if (preg_match('/\bhttps?:[\/]{2}[^\s<]+\b\/*/ui', $Excerpt['context'], $matches, PREG_OFFSET_CAPTURE)) 1414 | { 1415 | $url = $matches[0][0]; 1416 | 1417 | $Inline = array( 1418 | 'extent' => strlen($matches[0][0]), 1419 | 'position' => $matches[0][1], 1420 | 'element' => array( 1421 | 'name' => 'a', 1422 | 'text' => $url, 1423 | 'attributes' => array( 1424 | 'href' => $url, 1425 | ), 1426 | ), 1427 | ); 1428 | 1429 | return $Inline; 1430 | } 1431 | } 1432 | 1433 | protected function inlineUrlTag($Excerpt) 1434 | { 1435 | if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<(\w+:\/{2}[^ >]+)>/i', $Excerpt['text'], $matches)) 1436 | { 1437 | $url = $matches[1]; 1438 | 1439 | return array( 1440 | 'extent' => strlen($matches[0]), 1441 | 'element' => array( 1442 | 'name' => 'a', 1443 | 'text' => $url, 1444 | 'attributes' => array( 1445 | 'href' => $url, 1446 | ), 1447 | ), 1448 | ); 1449 | } 1450 | } 1451 | 1452 | # ~ 1453 | 1454 | protected function unmarkedText($text) 1455 | { 1456 | if ($this->breaksEnabled) 1457 | { 1458 | $text = preg_replace('/[ ]*\n/', "') 1565 | { 1566 | $markup = $trimmedMarkup; 1567 | $markup = substr($markup, 3); 1568 | 1569 | $position = strpos($markup, "
"); 1570 | 1571 | $markup = substr_replace($markup, '', $position, 4); 1572 | } 1573 | 1574 | return $markup; 1575 | } 1576 | 1577 | # 1578 | # Deprecated Methods 1579 | # 1580 | 1581 | function parse($text) 1582 | { 1583 | $markup = $this->text($text); 1584 | 1585 | return $markup; 1586 | } 1587 | 1588 | protected function sanitiseElement(array $Element) 1589 | { 1590 | static $goodAttribute = '/^[a-zA-Z0-9][a-zA-Z0-9-_]*+$/'; 1591 | static $safeUrlNameToAtt = array( 1592 | 'a' => 'href', 1593 | 'img' => 'src', 1594 | ); 1595 | 1596 | if (isset($safeUrlNameToAtt[$Element['name']])) 1597 | { 1598 | $Element = $this->filterUnsafeUrlInAttribute($Element, $safeUrlNameToAtt[$Element['name']]); 1599 | } 1600 | 1601 | if ( ! empty($Element['attributes'])) 1602 | { 1603 | foreach ($Element['attributes'] as $att => $val) 1604 | { 1605 | # filter out badly parsed attribute 1606 | if ( ! preg_match($goodAttribute, $att)) 1607 | { 1608 | unset($Element['attributes'][$att]); 1609 | } 1610 | # dump onevent attribute 1611 | elseif (self::striAtStart($att, 'on')) 1612 | { 1613 | unset($Element['attributes'][$att]); 1614 | } 1615 | } 1616 | } 1617 | 1618 | return $Element; 1619 | } 1620 | 1621 | protected function filterUnsafeUrlInAttribute(array $Element, $attribute) 1622 | { 1623 | foreach ($this->safeLinksWhitelist as $scheme) 1624 | { 1625 | if (self::striAtStart($Element['attributes'][$attribute], $scheme)) 1626 | { 1627 | return $Element; 1628 | } 1629 | } 1630 | 1631 | $Element['attributes'][$attribute] = str_replace(':', '%3A', $Element['attributes'][$attribute]); 1632 | 1633 | return $Element; 1634 | } 1635 | 1636 | # 1637 | # Static Methods 1638 | # 1639 | 1640 | protected static function escape($text, $allowQuotes = false) 1641 | { 1642 | return htmlspecialchars($text, $allowQuotes ? ENT_NOQUOTES : ENT_QUOTES, 'UTF-8'); 1643 | } 1644 | 1645 | protected static function striAtStart($string, $needle) 1646 | { 1647 | $len = strlen($needle); 1648 | 1649 | if ($len > strlen($string)) 1650 | { 1651 | return false; 1652 | } 1653 | else 1654 | { 1655 | return strtolower(substr($string, 0, $len)) === strtolower($needle); 1656 | } 1657 | } 1658 | 1659 | static function instance($name = 'default') 1660 | { 1661 | if (isset(self::$instances[$name])) 1662 | { 1663 | return self::$instances[$name]; 1664 | } 1665 | 1666 | $instance = new static(); 1667 | 1668 | self::$instances[$name] = $instance; 1669 | 1670 | return $instance; 1671 | } 1672 | 1673 | private static $instances = array(); 1674 | 1675 | # 1676 | # Fields 1677 | # 1678 | 1679 | protected $DefinitionData; 1680 | 1681 | # 1682 | # Read-Only 1683 | 1684 | protected $specialCharacters = array( 1685 | '\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!', '|', 1686 | ); 1687 | 1688 | protected $StrongRegex = array( 1689 | '*' => '/^[*]{2}((?:\\\\\*|[^*]|[*][^*]*[*])+?)[*]{2}(?![*])/s', 1690 | '_' => '/^__((?:\\\\_|[^_]|_[^_]*_)+?)__(?!_)/us', 1691 | ); 1692 | 1693 | protected $EmRegex = array( 1694 | '*' => '/^[*]((?:\\\\\*|[^*]|[*][*][^*]+?[*][*])+?)[*](?![*])/s', 1695 | '_' => '/^_((?:\\\\_|[^_]|__[^_]*__)+?)_(?!_)\b/us', 1696 | ); 1697 | 1698 | protected $regexHtmlAttribute = '[a-zA-Z_:][\w:.-]*(?:\s*=\s*(?:[^"\'=<>`\s]+|"[^"]*"|\'[^\']*\'))?'; 1699 | 1700 | protected $voidElements = array( 1701 | 'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 1702 | ); 1703 | 1704 | protected $textLevelElements = array( 1705 | 'a', 'br', 'bdo', 'abbr', 'blink', 'nextid', 'acronym', 'basefont', 1706 | 'b', 'em', 'big', 'cite', 'small', 'spacer', 'listing', 1707 | 'i', 'rp', 'del', 'code', 'strike', 'marquee', 1708 | 'q', 'rt', 'ins', 'font', 'strong', 1709 | 's', 'tt', 'kbd', 'mark', 1710 | 'u', 'xm', 'sub', 'nobr', 1711 | 'sup', 'ruby', 1712 | 'var', 'span', 1713 | 'wbr', 'time', 1714 | ); 1715 | } 1716 | --------------------------------------------------------------------------------