PluginProbe
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz / 2.12.8
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz v2.12.8
2.12.8 2.12.7 2.12.6 2.12.5 2.12.4 2.12.3 2.12.2 2.12.1 2.12.0 2.11.1 2.11.0 2.10.1 2.10.0 2.9.1 2.9.0 2.8.2 2.8.1 2.7.0 2.7.1 2.8.0 trunk 0.0.10 0.0.11 0.0.12 0.0.13 All 98 releases
← All changes | inc/export.php +261 -76 0.0.12 → 2.12.8 View file →
@@ -8,10 +8,8 @@
8 8
9 9 namespace SRFM\Inc;
10 10
11 11 use SRFM\Inc\Traits\Get_Instance;
12 -use WP_REST_Server;
13 -use SRFM\Inc\Helper;
14 12
15 13 if ( ! defined( 'ABSPATH' ) ) {
16 14 exit; // Exit if accessed directly.
17 15 }
@@ -37,8 +35,12 @@
37 35 '_srfm_forms_styling',
38 36 '_srfm_integrations_webhooks',
39 37 '_srfm_instant_form_settings',
40 38 '_srfm_page_break_settings',
39 + '_srfm_conversational_form',
40 + '_srfm_premium_common',
41 + '_srfm_forms_styling_starter',
42 + '_srfm_user_registration_settings',
41 43 ];
42 44
43 45 /**
44 46 * Constructor
@@ -45,35 +47,34 @@
45 47 *
46 48 * @since 0.0.1
47 49 */
48 50 public function __construct() {
49 - add_action( 'wp_ajax_export_form', [ $this, 'handle_export_form' ] );
50 - add_action( 'wp_ajax_nopriv_export_form', [ $this, 'handle_export_form' ] );
51 - add_action( 'rest_api_init', [ $this, 'register_custom_endpoint' ] );
51 + // Modern REST API endpoints are registered in rest-api.php.
52 52 }
53 53
54 54 /**
55 - * Handle Export form
55 + * Get unserialized post meta keys.
56 56 *
57 - * @since 0.0.1
58 - * @return void
57 + * Retrieves the list of post meta keys that need to be unserialized during export.
58 + * Allows filtering of meta keys via 'srfm_export_and_import_post_meta_keys' filter.
59 + *
60 + * @since 1.9.0
61 + * @return array<string> Array of post meta keys to unserialize.
59 62 */
60 - public function handle_export_form() {
61 - if ( isset( $_POST['nonce'] ) && ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'export_form_nonce' ) ) {
62 - $error_message = 'Nonce verification failed.';
63 + public function get_unserialized_post_metas() {
64 + return Helper::apply_filters_as_array( 'srfm_export_and_import_post_meta_keys', $this->unserialized_post_metas );
65 + }
63 66
64 - $error_data = [
65 - 'error' => $error_message,
66 - ];
67 - wp_send_json_error( $error_data );
68 - }
69 -
70 - if ( isset( $_POST['post_id'] ) ) {
71 - $post_ids = explode( ',', sanitize_text_field( wp_unslash( $_POST['post_id'] ) ) );
72 - } else {
73 - $post_ids = [];
74 - }
75 -
67 + /**
68 + * Get forms with meta by post IDs.
69 + * Uses:
70 + * - On websitedemos.net, for exporting the Spectra Block Patterns & Pages with SureForms form.
71 + *
72 + * @since 1.13.0
73 + * @param array<int,string>|array<int, int> $post_ids Array of post IDs to retrieve forms for.
74 + * @return array Array of forms with their post data and meta data.
75 + */
76 + public function get_forms_with_meta( $post_ids = [] ) {
76 77 $posts = [];
77 78
78 79 foreach ( $post_ids as $post_id ) {
79 80 $post_id = intval( $post_id );
@@ -78,9 +79,17 @@
78 79 foreach ( $post_ids as $post_id ) {
79 80 $post_id = intval( $post_id );
80 81 $post = get_post( $post_id );
81 82 $post_meta = get_post_meta( $post_id );
82 - $posts[] = [
83 +
84 + // The view counter belongs to this site's traffic, not to the form. These
85 + // payloads feed shared starter templates, so shipping it would hand every
86 + // importer a stranger's numbers. The import side already refuses the key,
87 + // so this is about not exporting it in the first place.
88 + if ( is_array( $post_meta ) ) {
89 + unset( $post_meta[ \SRFM\Inc\Form_Views::META_KEY ] );
90 + }
91 + $posts[] = [
83 92 'post' => $post,
84 93 'post_meta' => $post_meta,
85 94 ];
86 95 }
@@ -88,9 +97,10 @@
88 97 // Unserialize the post metas that are serialized.
89 98 // This is needed because the post metas are serialized before saving.
90 99 foreach ( $posts as $key => $post ) {
91 100 $post_metas = isset( $post['post_meta'] ) && is_array( $post['post_meta'] ) ? $post['post_meta'] : [];
92 - foreach ( $this->unserialized_post_metas as $meta_key ) {
101 +
102 + foreach ( $this->get_unserialized_post_metas() as $meta_key ) {
93 103 if ( isset( $post_metas[ $meta_key ] ) && is_array( $post_metas[ $meta_key ] ) ) {
94 104 $post_metas[ $meta_key ] = maybe_unserialize( $post_metas[ $meta_key ][0] );
95 105 }
96 106 }
@@ -96,58 +106,169 @@
96 106 }
97 107 $posts[ $key ]['post_meta'] = $post_metas;
98 108 }
99 109
100 - wp_send_json( $posts );
110 + return $posts;
101 111 }
102 112
113 + /**
114 + * Handle Export form via REST API
115 + *
116 + * @param \WP_REST_Request $request Full details about the request.
117 + * @since 2.0.0
118 + * @return \WP_REST_Response|\WP_Error
119 + */
120 + public function handle_export_form_rest( $request ) {
121 + $nonce = sanitize_text_field( Helper::get_string_value( $request->get_header( 'X-WP-Nonce' ) ) );
103 122
123 + if ( ! wp_verify_nonce( $nonce, 'wp_rest' ) ) {
124 + return new \WP_Error(
125 + 'invalid_nonce',
126 + __( 'Nonce verification failed.', 'sureforms' ),
127 + [ 'status' => 403 ]
128 + );
129 + }
130 +
131 + $params = $request->get_params();
132 + $post_ids = [];
133 +
134 + // Handle post_ids parameter - can be array or comma-separated string.
135 + if ( isset( $params['post_ids'] ) ) {
136 + if ( is_array( $params['post_ids'] ) ) {
137 + $post_ids = array_map( 'intval', $params['post_ids'] );
138 + } else {
139 + $post_ids = array_map( 'intval', explode( ',', sanitize_text_field( Helper::get_string_value( $params['post_ids'] ) ) ) );
140 + }
141 + }
142 +
143 + // Validate that all post IDs are valid sureforms_form posts.
144 + $validated_post_ids = [];
145 + foreach ( $post_ids as $post_id ) {
146 + $post = get_post( $post_id );
147 + if ( $post && 'sureforms_form' === $post->post_type ) {
148 + $validated_post_ids[] = $post_id;
149 + }
150 + }
151 +
152 + if ( empty( $validated_post_ids ) ) {
153 + return new \WP_Error(
154 + 'no_valid_forms',
155 + __( 'No valid forms found for export.', 'sureforms' ),
156 + [ 'status' => 400 ]
157 + );
158 + }
159 +
160 + $posts = $this->get_forms_with_meta( $validated_post_ids );
161 +
162 + return new \WP_REST_Response(
163 + [
164 + 'success' => true,
165 + 'data' => $posts,
166 + 'count' => count( $posts ),
167 + ]
168 + );
169 + }
170 +
104 171 /**
105 - * Handle Import Form
172 + * Handle Import form via REST API
106 173 *
107 174 * @param \WP_REST_Request $request Full details about the request.
108 - *
109 - * @since 0.0.1
110 - * @return void
175 + * @since 2.0.0
176 + * @return \WP_REST_Response|\WP_Error
111 177 */
112 - public function handle_import_form( $request ) {
178 + public function handle_import_form_rest( $request ) {
179 + $nonce = sanitize_text_field( Helper::get_string_value( $request->get_header( 'X-WP-Nonce' ) ) );
113 180
114 - $nonce = Helper::get_string_value( $request->get_header( 'X-WP-Nonce' ) );
181 + if ( ! wp_verify_nonce( $nonce, 'wp_rest' ) ) {
182 + return new \WP_Error(
183 + 'invalid_nonce',
184 + __( 'Nonce verification failed.', 'sureforms' ),
185 + [ 'status' => 403 ]
186 + );
187 + }
115 188
116 - if ( ! wp_verify_nonce( sanitize_text_field( $nonce ), 'wp_rest' ) ) {
117 - wp_send_json_error(
118 - [
119 - 'data' => __( 'Nonce verification failed.', 'sureforms' ),
120 - 'status' => false,
121 - ]
189 + $params = $request->get_params();
190 +
191 + // Get forms data from the request.
192 + $forms_data = isset( $params['forms_data'] ) && is_array( $params['forms_data'] ) ? $params['forms_data'] : [];
193 + $default_status = isset( $params['default_status'] ) ? sanitize_text_field( Helper::get_string_value( $params['default_status'] ) ) : 'draft';
194 +
195 + if ( empty( $forms_data ) ) {
196 + return new \WP_Error(
197 + 'no_forms_data',
198 + __( 'No forms data provided for import.', 'sureforms' ),
199 + [ 'status' => 400 ]
122 200 );
123 201 }
124 202
125 - // Get the raw POST data.
126 - $post_data = file_get_contents( 'php://input' );
127 - if ( ! $post_data ) {
128 - wp_send_json_error( __( 'Failed to import form.', 'sureforms' ) );
203 + // Validate forms data structure.
204 + foreach ( $forms_data as $form_data ) {
205 + if ( ! is_array( $form_data ) || ! isset( $form_data['post'] ) || ! isset( $form_data['post_meta'] ) ) {
206 + return new \WP_Error(
207 + 'invalid_form_data',
208 + __( 'Invalid form data structure provided.', 'sureforms' ),
209 + [ 'status' => 400 ]
210 + );
211 + }
129 212 }
130 - $data = json_decode( $post_data, true );
131 - $responses = [];
132 - if ( ! is_iterable( $data ) ) {
133 - wp_send_json_error( __( 'Failed to import form.', 'sureforms' ) );
213 +
214 + $result = $this->import_forms_with_meta( $forms_data, $default_status );
215 +
216 + if ( is_wp_error( $result ) ) {
217 + return $result;
134 218 }
219 +
220 + return new \WP_REST_Response(
221 + [
222 + 'success' => true,
223 + 'message' => __( 'Forms imported successfully!', 'sureforms' ),
224 + 'forms_mapping' => $result,
225 + 'imported_count' => count( $result ),
226 + ]
227 + );
228 + }
229 +
230 + /**
231 + * Import Forms with Meta
232 + * Uses:
233 + * - In Design Library for importing the Spectra Block Patterns and Pages with SureForms form.
234 + *
235 + * @param array<array<array<string>>> $data Form data to import.
236 + * @param string $default_status Default post status for imported forms. Default is 'draft'.
237 + *
238 + * @since 1.13.0
239 + * @return array<int, int>|\WP_Error Returns mapping array on success, WP_Error on failure.
240 + */
241 + public function import_forms_with_meta( $data, $default_status = 'draft' ) {
242 + $forms_mapping = [];
135 243 foreach ( $data as $form_data ) {
136 -
137 244 // sanitize the data before saving.
245 + $old_id = intval( $form_data['post']['ID'] );
138 246 $post_content = wp_kses_post( $form_data['post']['post_content'] );
139 247 $post_title = sanitize_text_field( $form_data['post']['post_title'] );
140 248 $post_meta = $form_data['post_meta'];
141 249 $post_type = sanitize_text_field( $form_data['post']['post_type'] );
142 250
143 - $post_content = addslashes( $post_content );
251 + // Remove percent-encoded slugs from imported form content.
252 + // Non-Latin labels produce broken slugs like %e3%83%95%e3%83%aa
253 + // via sanitize_title(). Clearing them lets process_blocks()
254 + // regenerate clean block-name-based slugs on save.
255 + $cleaned_content = preg_replace(
256 + '/"slug":"(%[a-fA-F0-9]{2}[^"]*)"/',
257 + '"slug":""',
258 + $post_content
259 + );
260 + if ( is_string( $cleaned_content ) ) {
261 + $post_content = $cleaned_content;
262 + }
144 263
264 + $post_content = wp_slash( $post_content );
265 +
145 266 // Check if sureforms/form exists in post_content.
146 267 if ( 'sureforms_form' === $post_type ) {
147 268 $new_post = [
148 269 'post_title' => $post_title,
149 - 'post_status' => 'draft',
270 + 'post_status' => $default_status,
150 271 'post_type' => 'sureforms_form',
151 272 ];
152 273
153 274 $post_id = wp_insert_post( $new_post );
@@ -152,9 +273,13 @@
152 273
153 274 $post_id = wp_insert_post( $new_post );
154 275
155 276 // Update the post content formId to the new post id.
156 - $post_content = str_replace( '\"formId\":' . $form_data['post']['ID'], '\"formId\":' . $post_id, $post_content );
277 + $post_content = str_replace(
278 + '\"formId\":' . intval( $form_data['post']['ID'] ),
279 + '\"formId\":' . intval( $post_id ),
280 + $post_content
281 + );
157 282
158 283 // update the post content.
159 284 wp_update_post(
160 285 [
@@ -163,49 +288,109 @@
163 288 ]
164 289 );
165 290
166 291 if ( ! $post_id ) {
167 - http_response_code( 400 );
168 - wp_send_json_error( __( 'Failed to import form.', 'sureforms' ) );
292 + return new \WP_Error( 'import_forms_failed', __( 'Unable to import form.', 'sureforms' ) );
169 293 }
294 +
295 + $forms_mapping[ $old_id ] = $post_id;
296 +
170 297 // Update post meta.
298 + $allowed_keys = $this->get_allowed_import_meta_keys();
299 + $unserialized_meta_keys = $this->get_unserialized_post_metas();
300 + $registered = get_registered_meta_keys( 'post', SRFM_FORMS_POST_TYPE );
171 301 foreach ( $post_meta as $meta_key => $meta_value ) {
172 - // Check if the meta key is one of the unserialized post metas then add it as is.
173 - if ( in_array( $meta_key, $this->unserialized_post_metas, true ) ) {
174 - add_post_meta( $post_id, $meta_key, $meta_value );
302 + // 1. Whitelist check — skip unknown keys from crafted import files.
303 + if ( ! in_array( $meta_key, $allowed_keys, true ) ) {
304 + continue;
305 + }
306 +
307 + // Note: add_post_meta() internally runs wp_unslash() on the value before
308 + // invoking the registered sanitize_callback. Imported values are unslashed,
309 + // so without re-slashing, backslashes are stripped — corrupting JSON-string
310 + // metas (e.g. _srfm_save_resume, _srfm_conditional_confirmation) whose escaped
311 + // quotes (\") then fail json_decode() in their sanitizers, wiping the value to
312 + // an empty string. wp_slash() pre-escapes so wp_unslash() restores the original.
313 + if ( in_array( $meta_key, $unserialized_meta_keys, true ) ) {
314 + // Complex array metas — sanitize_callback registered via register_post_meta()
315 + // is automatically invoked by add_post_meta() → update_metadata() pipeline.
316 + // When Pro is inactive, some keys may lack a registered callback — apply fallback.
317 + if ( empty( $registered[ $meta_key ]['sanitize_callback'] ) ) {
318 + $meta_value = Helper::sanitize_by_type( $meta_value );
319 + }
320 + add_post_meta( $post_id, $meta_key, wp_slash( $meta_value ) );
175 321 } else {
176 - if ( is_array( $meta_value ) && isset( $meta_value[0] ) ) {
177 - add_post_meta( $post_id, $meta_key, $meta_value[0] );
178 - } else {
179 - add_post_meta( $post_id, $meta_key, $meta_value );
322 + // Scalar metas — unwrap single-element arrays produced by get_post_meta().
323 + $raw_value = is_array( $meta_value ) && isset( $meta_value[0] ) ? $meta_value[0] : $meta_value;
324 + // Fallback sanitization — skip when a registered callback already handles it.
325 + if ( is_string( $raw_value ) && empty( $registered[ $meta_key ]['sanitize_callback'] ) ) {
326 + $raw_value = sanitize_text_field( $raw_value );
180 327 }
328 + add_post_meta( $post_id, $meta_key, wp_slash( $raw_value ) );
181 329 }
182 330 }
183 331 } else {
184 - http_response_code( 400 );
185 - wp_send_json_error( __( 'Failed to import form.', 'sureforms' ) );
332 + return new \WP_Error( 'import_forms_invalid_post_type', __( 'Unable to import form.', 'sureforms' ) );
186 333 }
187 334 }
188 335
189 - // Return the responses.
190 - wp_send_json_success( $responses );
336 + return $forms_mapping;
191 337 }
192 338
193 339 /**
194 - * Add custom API Route submit-form
340 + * Get the list of meta keys allowed during import.
195 341 *
196 - * @return void
197 - * @since 0.0.1
342 + * Only meta keys present in this list will be written to the DB during import.
343 + * Unknown keys from crafted import files are silently ignored.
344 + *
345 + * @since 2.8.0
346 + * @return array<string>
198 347 */
199 - public function register_custom_endpoint() {
200 - $helper = Helper::get_instance();
201 - register_rest_route(
202 - 'sureforms/v1',
203 - '/sureforms_import',
204 - [
205 - 'methods' => WP_REST_Server::EDITABLE,
206 - 'callback' => [ $this, 'handle_import_form' ],
207 - 'permission_callback' => [ $helper, 'current_user_can' ],
208 - ]
348 + private function get_allowed_import_meta_keys(): array {
349 + $scalar_metas = [
350 + '_srfm_additional_classes',
351 + '_srfm_bg_color',
352 + '_srfm_bg_image',
353 + '_srfm_bg_type',
354 + '_srfm_button_border_radius',
355 + '_srfm_captcha_security_type',
356 + '_srfm_cover_image',
357 + '_srfm_form_container_width',
358 + '_srfm_form_custom_css',
359 + '_srfm_form_recaptcha',
360 + '_srfm_form_restriction',
361 + '_srfm_inherit_theme_button',
362 + '_srfm_instant_form',
363 + '_srfm_is_ai_generated',
364 + '_srfm_is_inline_button',
365 + '_srfm_single_page_form_title',
366 + '_srfm_submit_alignment',
367 + '_srfm_submit_alignment_backend',
368 + '_srfm_submit_button_text',
369 + '_srfm_submit_type',
370 + '_srfm_submit_width',
371 + '_srfm_submit_width_backend',
372 + '_srfm_use_label_as_placeholder',
373 + ];
374 +
375 + /**
376 + * Filter the list of scalar meta keys allowed during import.
377 + *
378 + * Pro and other extensions can hook into this to add their own scalar meta keys.
379 + *
380 + * @since 2.8.0
381 + * @param array<string> $scalar_metas List of scalar meta keys.
382 + */
383 + $scalar_metas = apply_filters( 'srfm_import_scalar_meta_keys', $scalar_metas );
384 +
385 + // Ensure filter consumers cannot inject non-SureForms meta keys.
386 + $scalar_metas = array_filter(
387 + $scalar_metas,
388 + static function ( $key ) {
389 + return str_starts_with( $key, '_srfm_' );
390 + }
209 391 );
392 +
393 + return array_merge( $this->get_unserialized_post_metas(), $scalar_metas );
210 394 }
395 +
211 396 }