| @@ -35,9 +35,8 @@ | ||
| 35 | 35 | add_action( 'init', [ $this, 'register_post_types' ] ); |
| 36 | 36 | add_action( 'init', [ $this, 'register_post_metas' ] ); |
| 37 | 37 | add_shortcode( 'sureforms', [ $this, 'forms_shortcode' ] ); |
| 38 | 38 | add_action( 'manage_posts_extra_tablenav', [ $this, 'maybe_render_blank_form_state' ] ); |
| 39 | - add_action( 'admin_bar_menu', [ $this, 'remove_admin_bar_menu_item' ], 80, 1 ); | |
| 40 | 39 | add_action( 'template_redirect', [ $this, 'srfm_instant_form_redirect' ] ); |
| 41 | 40 | add_action( 'template_redirect', [ $this, 'disable_sureforms_archive_page' ], 9 ); |
| 42 | 41 | add_action( 'load-edit.php', [ $this, 'redirect_forms_listing_page' ] ); |
| 43 | 42 | |
| @@ -42,8 +41,9 @@ | ||
| 42 | 41 | add_action( 'load-edit.php', [ $this, 'redirect_forms_listing_page' ] ); |
| 43 | 42 | |
| 44 | 43 | add_filter( 'rest_prepare_sureforms_form', [ $this, 'sureforms_normalize_meta_for_rest' ], 10, 2 ); |
| 45 | 44 | add_action( 'admin_bar_menu', [ $this, 'add_edit_form_to_admin_bar_menu' ], 100 ); |
| 45 | + add_action( 'admin_bar_menu', [ $this, 'add_new_form_to_admin_bar_menu' ], 100 ); | |
| 46 | 46 | } |
| 47 | 47 | |
| 48 | 48 | /** |
| 49 | 49 | * Redirect the forms listing page to the updated forms page. |
| @@ -103,8 +103,47 @@ | ||
| 103 | 103 | ); |
| 104 | 104 | } |
| 105 | 105 | |
| 106 | 106 | /** |
| 107 | + * Add a "Form" shortcut to the admin bar "+ New" menu (#3026). | |
| 108 | + * | |
| 109 | + * Mirrors how core post types appear under "+ New", but added manually rather | |
| 110 | + * than via `show_in_admin_bar` so it does not also register a second front-end | |
| 111 | + * "Edit" node alongside the custom one in add_edit_form_to_admin_bar_menu(). | |
| 112 | + * Gated on the form CPT's own create capability (manage_options for this CPT), | |
| 113 | + * so it only shows for users who can actually create a form. | |
| 114 | + * | |
| 115 | + * @param WP_Admin_Bar $wp_admin_bar WP_Admin_Bar instance. | |
| 116 | + * @since 2.12.4 | |
| 117 | + * @return void | |
| 118 | + */ | |
| 119 | + public function add_new_form_to_admin_bar_menu( $wp_admin_bar ) { | |
| 120 | + if ( ! is_admin_bar_showing() || ! $wp_admin_bar instanceof WP_Admin_Bar ) { | |
| 121 | + return; | |
| 122 | + } | |
| 123 | + | |
| 124 | + $post_type = get_post_type_object( SRFM_FORMS_POST_TYPE ); | |
| 125 | + | |
| 126 | + if ( ! $post_type || empty( $post_type->cap->create_posts ) || ! current_user_can( $post_type->cap->create_posts ) ) { | |
| 127 | + return; | |
| 128 | + } | |
| 129 | + | |
| 130 | + // Core registers the "+ New" (new-content) group at priority 70, so running at | |
| 131 | + // 100 places this node inside it. If core skipped the group (the user can create | |
| 132 | + // nothing else), WP_Admin_Bar::_bind() drops this orphan node silently. | |
| 133 | + // name_admin_bar is the label core uses for "+ New" children; escaped because | |
| 134 | + // WP_Admin_Bar echoes node titles unescaped. | |
| 135 | + $wp_admin_bar->add_node( | |
| 136 | + [ | |
| 137 | + 'id' => 'new-' . SRFM_FORMS_POST_TYPE, | |
| 138 | + 'parent' => 'new-content', | |
| 139 | + 'title' => esc_html( $post_type->labels->name_admin_bar ), | |
| 140 | + 'href' => esc_url( admin_url( 'post-new.php?post_type=' . SRFM_FORMS_POST_TYPE ) ), | |
| 141 | + ] | |
| 142 | + ); | |
| 143 | + } | |
| 144 | + | |
| 145 | + /** | |
| 107 | 146 | * Remove this method in the future once _srfm_form_confirmation meta is updated. |
| 108 | 147 | * Normalize the _srfm_form_confirmation meta before it's sent to the REST API. |
| 109 | 148 | * Ensures the meta data is type-safe and includes necessary defaults like `hide_copy`. |
| 110 | 149 | * |
| @@ -114,22 +153,43 @@ | ||
| 114 | 153 | * @return WP_REST_Response Modified REST response with normalized meta. |
| 115 | 154 | * @since 1.7.3 |
| 116 | 155 | */ |
| 117 | 156 | public function sureforms_normalize_meta_for_rest( $response, $post ) { |
| 118 | - $meta_raw = get_post_meta( $post->ID, '_srfm_form_confirmation', true ); | |
| 119 | - $form_confirmation = maybe_unserialize( is_string( $meta_raw ) ? $meta_raw : '' ); | |
| 157 | + $meta_raw = get_post_meta( $post->ID, '_srfm_form_confirmation', true ); | |
| 158 | + // Meta may be a PHP array (new forms stored via update_post_meta with an array) | |
| 159 | + // or a serialized/JSON string (legacy forms). Handle both. | |
| 160 | + $form_confirmation = is_array( $meta_raw ) ? $meta_raw : maybe_unserialize( is_string( $meta_raw ) ? $meta_raw : '' ); | |
| 120 | 161 | |
| 121 | 162 | if ( ! is_array( $form_confirmation ) ) { |
| 122 | 163 | return $response; |
| 123 | 164 | } |
| 124 | 165 | |
| 166 | + // Only normalize keys that extensions (e.g. SureForms Pro) have actually | |
| 167 | + // declared in the REST schema; otherwise REST PUT validation rejects them | |
| 168 | + // with "<key> is not a valid property of Object.". | |
| 169 | + $registered = get_registered_meta_keys( 'post', SRFM_FORMS_POST_TYPE ); | |
| 170 | + $item_properties = $registered['_srfm_form_confirmation']['show_in_rest']['schema']['items']['properties'] ?? []; | |
| 171 | + | |
| 125 | 172 | foreach ( $form_confirmation as $index => $item ) { |
| 126 | 173 | if ( ! is_array( $item ) ) { |
| 127 | 174 | continue; |
| 128 | 175 | } |
| 129 | 176 | |
| 130 | - $form_confirmation[ $index ]['hide_copy'] = ! empty( $item['hide_copy'] ); | |
| 131 | - $form_confirmation[ $index ]['hide_download_all'] = ! empty( $item['hide_download_all'] ); | |
| 177 | + if ( isset( $item_properties['hide_copy'] ) ) { | |
| 178 | + $form_confirmation[ $index ]['hide_copy'] = ! empty( $item['hide_copy'] ); | |
| 179 | + } | |
| 180 | + if ( isset( $item_properties['hide_download_all'] ) ) { | |
| 181 | + $form_confirmation[ $index ]['hide_download_all'] = ! empty( $item['hide_download_all'] ); | |
| 182 | + } | |
| 183 | + | |
| 184 | + // DOMDocument::saveHTML() strips the "data:" prefix from data URIs in src attributes. | |
| 185 | + // Restore it so the editor displays SVG images correctly. | |
| 186 | + if ( isset( $item['message'] ) && is_string( $item['message'] ) && false !== strpos( $item['message'], 'src="image/svg+xml;base64' ) ) { | |
| 187 | + $normalized = preg_replace( '/src="image\/svg\+xml;base64/', 'src="data:image/svg+xml;base64', $item['message'] ); | |
| 188 | + if ( is_string( $normalized ) ) { | |
| 189 | + $form_confirmation[ $index ]['message'] = $normalized; | |
| 190 | + } | |
| 191 | + } | |
| 132 | 192 | } |
| 133 | 193 | |
| 134 | 194 | $response_data = $response->get_data(); |
| 135 | 195 | if ( is_array( $response_data ) ) { |
| @@ -271,20 +331,8 @@ | ||
| 271 | 331 | } |
| 272 | 332 | } |
| 273 | 333 | |
| 274 | 334 | /** |
| 275 | - * Remove add new form menu item. | |
| 276 | - * | |
| 277 | - * @param WP_Admin_Bar $wp_admin_bar WP_Admin_Bar instance. | |
| 278 | - * | |
| 279 | - * @return void | |
| 280 | - * @since 0.0.1 | |
| 281 | - */ | |
| 282 | - public function remove_admin_bar_menu_item( $wp_admin_bar ) { | |
| 283 | - $wp_admin_bar->remove_node( 'new-sureforms_form' ); | |
| 284 | - } | |
| 285 | - | |
| 286 | - /** | |
| 287 | 335 | * Show blank slate styles. |
| 288 | 336 | * |
| 289 | 337 | * @return void |
| 290 | 338 | * @since 0.0.1 |
| @@ -399,8 +447,13 @@ | ||
| 399 | 447 | '_srfm_is_ai_generated' => 'boolean', |
| 400 | 448 | ] |
| 401 | 449 | ); |
| 402 | 450 | |
| 451 | + // NOTE: `_srfm_form_views` is intentionally NOT registered here. It is a | |
| 452 | + // server-side counter written only by Form_Views (get/add_post_meta + atomic | |
| 453 | + // SQL). Exposing it to the block editor via show_in_rest let a form save / | |
| 454 | + // autosave round-trip a stale value and clobber the live count back to 0. | |
| 455 | + | |
| 403 | 456 | // Form Custom CSS meta. |
| 404 | 457 | register_post_meta( |
| 405 | 458 | 'sureforms_form', |
| 406 | 459 | '_srfm_form_custom_css', |
| @@ -460,14 +513,35 @@ | ||
| 460 | 513 | register_post_meta( |
| 461 | 514 | SRFM_FORMS_POST_TYPE, |
| 462 | 515 | '_srfm_instant_form_settings', |
| 463 | 516 | [ |
| 464 | - 'single' => true, | |
| 465 | - 'type' => 'object', | |
| 466 | - 'auth_callback' => static function() { | |
| 517 | + 'single' => true, | |
| 518 | + 'type' => 'object', | |
| 519 | + 'auth_callback' => static function() { | |
| 467 | 520 | return Helper::current_user_can(); |
| 468 | 521 | }, |
| 469 | - 'show_in_rest' => [ | |
| 522 | + 'sanitize_callback' => static function( $meta_value ) { | |
| 523 | + if ( ! is_array( $meta_value ) ) { | |
| 524 | + return []; | |
| 525 | + } | |
| 526 | + return [ | |
| 527 | + 'site_logo' => isset( $meta_value['site_logo'] ) ? esc_url_raw( $meta_value['site_logo'] ) : '', | |
| 528 | + 'site_logo_id' => isset( $meta_value['site_logo_id'] ) ? absint( $meta_value['site_logo_id'] ) : 0, | |
| 529 | + 'cover_type' => isset( $meta_value['cover_type'] ) ? sanitize_text_field( $meta_value['cover_type'] ) : '', | |
| 530 | + 'cover_color' => isset( $meta_value['cover_color'] ) ? sanitize_text_field( $meta_value['cover_color'] ) : '', | |
| 531 | + 'cover_image' => isset( $meta_value['cover_image'] ) ? esc_url_raw( $meta_value['cover_image'] ) : '', | |
| 532 | + 'cover_image_id' => isset( $meta_value['cover_image_id'] ) ? absint( $meta_value['cover_image_id'] ) : 0, | |
| 533 | + 'bg_type' => isset( $meta_value['bg_type'] ) ? sanitize_text_field( $meta_value['bg_type'] ) : '', | |
| 534 | + 'bg_color' => isset( $meta_value['bg_color'] ) ? sanitize_text_field( $meta_value['bg_color'] ) : '', | |
| 535 | + 'bg_image' => isset( $meta_value['bg_image'] ) ? esc_url_raw( $meta_value['bg_image'] ) : '', | |
| 536 | + 'bg_image_id' => isset( $meta_value['bg_image_id'] ) ? absint( $meta_value['bg_image_id'] ) : 0, | |
| 537 | + 'enable_instant_form' => isset( $meta_value['enable_instant_form'] ) ? filter_var( $meta_value['enable_instant_form'], FILTER_VALIDATE_BOOLEAN ) : false, | |
| 538 | + 'form_container_width' => isset( $meta_value['form_container_width'] ) ? absint( $meta_value['form_container_width'] ) : 620, | |
| 539 | + 'single_page_form_title' => isset( $meta_value['single_page_form_title'] ) ? filter_var( $meta_value['single_page_form_title'], FILTER_VALIDATE_BOOLEAN ) : true, | |
| 540 | + 'use_banner_as_page_background' => isset( $meta_value['use_banner_as_page_background'] ) ? filter_var( $meta_value['use_banner_as_page_background'], FILTER_VALIDATE_BOOLEAN ) : false, | |
| 541 | + ]; | |
| 542 | + }, | |
| 543 | + 'show_in_rest' => [ | |
| 470 | 544 | 'schema' => [ |
| 471 | 545 | 'type' => 'object', |
| 472 | 546 | 'context' => [ 'edit' ], |
| 473 | 547 | 'properties' => [ |
| @@ -517,9 +591,9 @@ | ||
| 517 | 591 | ], |
| 518 | 592 | ], |
| 519 | 593 | ], |
| 520 | 594 | ], |
| 521 | - 'default' => [ | |
| 595 | + 'default' => [ | |
| 522 | 596 | 'bg_type' => 'color', |
| 523 | 597 | 'bg_color' => '#ffffff', |
| 524 | 598 | 'bg_image' => '', |
| 525 | 599 | 'site_logo' => '', |
| @@ -537,14 +611,17 @@ | ||
| 537 | 611 | register_post_meta( |
| 538 | 612 | SRFM_FORMS_POST_TYPE, |
| 539 | 613 | '_srfm_forms_styling', |
| 540 | 614 | [ |
| 541 | - 'single' => true, | |
| 542 | - 'type' => 'object', | |
| 543 | - 'auth_callback' => static function() { | |
| 615 | + 'single' => true, | |
| 616 | + 'type' => 'object', | |
| 617 | + 'auth_callback' => static function() { | |
| 544 | 618 | return Helper::current_user_can(); |
| 545 | 619 | }, |
| 546 | - 'show_in_rest' => [ | |
| 620 | + 'sanitize_callback' => static function( $meta_value ) { | |
| 621 | + return Helper::sanitize_by_type( $meta_value ); | |
| 622 | + }, | |
| 623 | + 'show_in_rest' => [ | |
| 547 | 624 | 'schema' => [ |
| 548 | 625 | 'type' => 'object', |
| 549 | 626 | 'context' => [ 'edit' ], |
| 550 | 627 | 'properties' => [ |
| @@ -777,12 +854,16 @@ | ||
| 777 | 854 | ], |
| 778 | 855 | 'instant_form_border_radius_link' => [ |
| 779 | 856 | 'type' => 'boolean', |
| 780 | 857 | ], |
| 858 | + // Disable default SureForms styling. | |
| 859 | + 'disable_default_styles' => [ | |
| 860 | + 'type' => 'boolean', | |
| 861 | + ], | |
| 781 | 862 | ], |
| 782 | 863 | ], |
| 783 | 864 | ], |
| 784 | - 'default' => [ | |
| 865 | + 'default' => [ | |
| 785 | 866 | 'primary_color' => '#111C44', |
| 786 | 867 | 'text_color' => '#1E1E1E', |
| 787 | 868 | 'text_color_on_primary' => '#FFFFFF', |
| 788 | 869 | 'field_spacing' => 'medium', |
| @@ -857,8 +938,10 @@ | ||
| 857 | 938 | 'instant_form_border_radius_bottom' => 12, |
| 858 | 939 | 'instant_form_border_radius_left' => 12, |
| 859 | 940 | 'instant_form_border_radius_unit' => 'px', |
| 860 | 941 | 'instant_form_border_radius_link' => true, |
| 942 | + // Disable default SureForms styling. | |
| 943 | + 'disable_default_styles' => false, | |
| 861 | 944 | ], |
| 862 | 945 | ] |
| 863 | 946 | ); |
| 864 | 947 | |
| @@ -866,14 +949,40 @@ | ||
| 866 | 949 | register_post_meta( |
| 867 | 950 | 'sureforms_form', |
| 868 | 951 | '_srfm_email_notification', |
| 869 | 952 | [ |
| 870 | - 'single' => true, | |
| 871 | - 'type' => 'array', | |
| 872 | - 'auth_callback' => static function() { | |
| 953 | + 'single' => true, | |
| 954 | + 'type' => 'array', | |
| 955 | + 'auth_callback' => static function() { | |
| 873 | 956 | return Helper::current_user_can(); |
| 874 | 957 | }, |
| 875 | - 'show_in_rest' => [ | |
| 958 | + 'sanitize_callback' => static function( $meta_value ) { | |
| 959 | + if ( ! is_array( $meta_value ) ) { | |
| 960 | + return []; | |
| 961 | + } | |
| 962 | + $sanitized = []; | |
| 963 | + foreach ( $meta_value as $item ) { | |
| 964 | + if ( ! is_array( $item ) ) { | |
| 965 | + continue; | |
| 966 | + } | |
| 967 | + $sanitized[] = [ | |
| 968 | + 'id' => isset( $item['id'] ) ? intval( $item['id'] ) : 0, | |
| 969 | + 'status' => isset( $item['status'] ) ? filter_var( $item['status'], FILTER_VALIDATE_BOOLEAN ) : false, | |
| 970 | + 'is_raw_format' => isset( $item['is_raw_format'] ) ? filter_var( $item['is_raw_format'], FILTER_VALIDATE_BOOLEAN ) : false, | |
| 971 | + 'name' => isset( $item['name'] ) ? sanitize_text_field( $item['name'] ) : '', | |
| 972 | + 'email_to' => isset( $item['email_to'] ) ? sanitize_text_field( $item['email_to'] ) : '', | |
| 973 | + 'email_reply_to' => isset( $item['email_reply_to'] ) ? sanitize_text_field( $item['email_reply_to'] ) : '', | |
| 974 | + 'from_name' => isset( $item['from_name'] ) ? sanitize_text_field( $item['from_name'] ) : '', | |
| 975 | + 'from_email' => isset( $item['from_email'] ) ? sanitize_text_field( $item['from_email'] ) : '', | |
| 976 | + 'email_cc' => isset( $item['email_cc'] ) ? sanitize_text_field( $item['email_cc'] ) : '', | |
| 977 | + 'email_bcc' => isset( $item['email_bcc'] ) ? sanitize_text_field( $item['email_bcc'] ) : '', | |
| 978 | + 'subject' => isset( $item['subject'] ) ? sanitize_text_field( $item['subject'] ) : '', | |
| 979 | + 'email_body' => isset( $item['email_body'] ) ? wp_kses_post( $item['email_body'] ) : '', | |
| 980 | + ]; | |
| 981 | + } | |
| 982 | + return $sanitized; | |
| 983 | + }, | |
| 984 | + 'show_in_rest' => [ | |
| 876 | 985 | 'schema' => [ |
| 877 | 986 | 'type' => 'array', |
| 878 | 987 | 'context' => [ 'edit' ], |
| 879 | 988 | 'items' => [ |
| @@ -918,9 +1027,9 @@ | ||
| 918 | 1027 | ], |
| 919 | 1028 | ], |
| 920 | 1029 | ], |
| 921 | 1030 | ], |
| 922 | - 'default' => [ | |
| 1031 | + 'default' => [ | |
| 923 | 1032 | [ |
| 924 | 1033 | 'id' => 1, |
| 925 | 1034 | 'status' => true, |
| 926 | 1035 | 'is_raw_format' => false, |
| @@ -942,14 +1051,33 @@ | ||
| 942 | 1051 | register_post_meta( |
| 943 | 1052 | 'sureforms_form', |
| 944 | 1053 | '_srfm_compliance', |
| 945 | 1054 | [ |
| 946 | - 'single' => true, | |
| 947 | - 'type' => 'array', | |
| 948 | - 'auth_callback' => static function() { | |
| 1055 | + 'single' => true, | |
| 1056 | + 'type' => 'array', | |
| 1057 | + 'auth_callback' => static function() { | |
| 949 | 1058 | return Helper::current_user_can(); |
| 950 | 1059 | }, |
| 951 | - 'show_in_rest' => [ | |
| 1060 | + 'sanitize_callback' => static function( $meta_value ) { | |
| 1061 | + if ( ! is_array( $meta_value ) ) { | |
| 1062 | + return []; | |
| 1063 | + } | |
| 1064 | + $sanitized = []; | |
| 1065 | + foreach ( $meta_value as $item ) { | |
| 1066 | + if ( ! is_array( $item ) ) { | |
| 1067 | + continue; | |
| 1068 | + } | |
| 1069 | + $sanitized[] = [ | |
| 1070 | + 'id' => isset( $item['id'] ) ? sanitize_text_field( $item['id'] ) : '', | |
| 1071 | + 'gdpr' => isset( $item['gdpr'] ) ? filter_var( $item['gdpr'], FILTER_VALIDATE_BOOLEAN ) : false, | |
| 1072 | + 'do_not_store_entries' => isset( $item['do_not_store_entries'] ) ? filter_var( $item['do_not_store_entries'], FILTER_VALIDATE_BOOLEAN ) : false, | |
| 1073 | + 'auto_delete_entries' => isset( $item['auto_delete_entries'] ) ? filter_var( $item['auto_delete_entries'], FILTER_VALIDATE_BOOLEAN ) : false, | |
| 1074 | + 'auto_delete_days' => isset( $item['auto_delete_days'] ) ? sanitize_text_field( $item['auto_delete_days'] ) : '', | |
| 1075 | + ]; | |
| 1076 | + } | |
| 1077 | + return $sanitized; | |
| 1078 | + }, | |
| 1079 | + 'show_in_rest' => [ | |
| 952 | 1080 | 'schema' => [ |
| 953 | 1081 | 'type' => 'array', |
| 954 | 1082 | 'context' => [ 'edit' ], |
| 955 | 1083 | 'items' => [ |
| @@ -973,9 +1101,9 @@ | ||
| 973 | 1101 | ], |
| 974 | 1102 | ], |
| 975 | 1103 | ], |
| 976 | 1104 | ], |
| 977 | - 'default' => [ | |
| 1105 | + 'default' => [ | |
| 978 | 1106 | [ |
| 979 | 1107 | 'id' => 'gdpr', |
| 980 | 1108 | 'gdpr' => false, |
| 981 | 1109 | 'do_not_store_entries' => false, |
| @@ -1015,9 +1143,9 @@ | ||
| 1015 | 1143 | 'id' => isset( $item['id'] ) ? intval( $item['id'] ) : 0, |
| 1016 | 1144 | 'confirmation_type' => isset( $item['confirmation_type'] ) ? sanitize_text_field( $item['confirmation_type'] ) : '', |
| 1017 | 1145 | 'page_url' => isset( $item['page_url'] ) ? esc_url_raw( $item['page_url'] ) : '', |
| 1018 | 1146 | 'custom_url' => isset( $item['custom_url'] ) ? esc_url_raw( $item['custom_url'] ) : '', |
| 1019 | - 'message' => isset( $item['message'] ) ? Helper::strip_js_attributes( $item['message'] ) : '', | |
| 1147 | + 'message' => isset( $item['message'] ) ? wp_kses_post( $item['message'] ) : '', | |
| 1020 | 1148 | 'submission_action' => isset( $item['submission_action'] ) ? sanitize_text_field( $item['submission_action'] ) : '', |
| 1021 | 1149 | 'enable_query_params' => isset( $item['enable_query_params'] ) ? filter_var( $item['enable_query_params'], FILTER_VALIDATE_BOOLEAN ) : false, |
| 1022 | 1150 | 'query_params' => isset( $item['query_params'] ) && is_array( $item['query_params'] ) |
| 1023 | 1151 | ? array_map( |