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/rest-api.php +1844 -62 0.0.12 → 2.12.8 View file →
@@ -6,12 +6,14 @@
6 6 */
7 7
8 8 namespace SRFM\Inc;
9 9
10 -use SRFM\Inc\Traits\Get_Instance;
10 +use SRFM\Inc\AI_Form_Builder\AI_Auth;
11 11 use SRFM\Inc\AI_Form_Builder\AI_Form_Builder;
12 12 use SRFM\Inc\AI_Form_Builder\Field_Mapping;
13 -use SRFM\Inc\AI_Form_Builder\AI_Auth;
13 +use SRFM\Inc\Database\Tables\Entries;
14 +use SRFM\Inc\Entries as Entries_Class;
15 +use SRFM\Inc\Traits\Get_Instance;
14 16
15 17 if ( ! defined( 'ABSPATH' ) ) {
16 18 exit; // Exit if accessed directly.
17 19 }
@@ -21,12 +23,26 @@
21 23 *
22 24 * @since 0.0.7
23 25 */
24 26 class Rest_Api {
27 + use Get_Instance;
25 28
26 - use Get_Instance;
29 + /**
30 + * Onboarding user details option key.
31 + *
32 + * @var string
33 + */
34 + private $onboarding_user_details_key = 'onboarding_user_details';
27 35
28 36 /**
37 + * Dropdown counter for field name generation.
38 + *
39 + * @var int
40 + * @since 2.0.0
41 + */
42 + private static $dropdown_counter = 0;
43 +
44 + /**
29 45 * Constructor
30 46 *
31 47 * @since 0.0.7
32 48 * @return void
@@ -57,102 +73,1868 @@
57 73 }
58 74 }
59 75
60 76 /**
61 - * Check if user can edit posts
77 + * Checks whether the value is boolean or not.
62 78 *
63 - * @since 0.0.7
79 + * @param mixed $value value to be checked.
80 + * @since 0.0.8
64 81 * @return bool
65 82 */
66 - public function can_edit_posts() {
67 - return current_user_can( 'edit_posts' );
83 + public function sanitize_boolean_field( $value ) {
84 + return filter_var( $value, FILTER_VALIDATE_BOOLEAN );
68 85 }
69 86
70 87 /**
71 - * Get endpoints
88 + * Return the visitor's detected country code.
72 89 *
73 - * @since 0.0.7
74 - * @return array<array<mixed>>
90 + * Public, read-only — resolves the country for the *current* request's IP via
91 + * Helper::get_geo_country(), so the phone field can fetch it per-visitor and
92 + * work on full-page-cached sites. Outbound geolocation calls are bounded by
93 + * the hourly cap inside Helper::get_geo_country().
94 + *
95 + * @since 2.11.1
96 + * @return \WP_REST_Response
75 97 */
76 - private function get_endpoints() {
77 - return [
78 - 'generate-block-slugs' => [
79 - 'methods' => 'POST',
80 - 'callback' => [ $this, 'generate_block_slugs_by_content' ],
81 - 'permission_callback' => [ $this, 'can_edit_posts' ],
98 + public function get_geo_country() {
99 + // Pass an empty fallback so '' unambiguously means "not confidently
100 + // detected" (no CDN header and no successful IP lookup). The frontend then
101 + // falls back to a privacy-safe, network-free Intl guess in the browser.
102 + $country = Helper::get_geo_country( '' );
103 +
104 + return new \WP_REST_Response(
105 + [
106 + 'country' => $country,
107 + 'detected' => '' !== $country,
82 108 ],
83 - 'generate-form' => [
84 - 'methods' => 'POST',
85 - 'callback' => [ AI_Form_Builder::get_instance(), 'generate_ai_form' ],
86 - 'permission_callback' => [ $this, 'can_edit_posts' ],
87 - 'args' => [
88 - 'use_system_message' => [
89 - 'sanitize_callback' => [ $this, 'sanitize_boolean_field' ],
90 - ],
109 + 200
110 + );
111 + }
112 +
113 + /**
114 + * Get the data for generating entries chart.
115 + *
116 + * @param \WP_REST_Request $request Full details about the request.
117 + * @since 1.0.0
118 + * @return array<mixed>
119 + */
120 + public function get_entries_chart_data( $request ) {
121 + $nonce = Helper::get_string_value( $request->get_header( 'X-WP-Nonce' ) );
122 +
123 + if ( ! wp_verify_nonce( sanitize_text_field( $nonce ), 'wp_rest' ) ) {
124 + wp_send_json_error( __( 'Security verification failed. Please refresh the page and try again.', 'sureforms' ) );
125 + }
126 +
127 + $params = $request->get_params();
128 +
129 + if ( empty( $params ) ) {
130 + wp_send_json_error( __( 'Missing required parameters.', 'sureforms' ) );
131 + }
132 +
133 + $after = is_array( $params ) && ! empty( $params['after'] ) ? sanitize_text_field( Helper::get_string_value( $params['after'] ) ) : '';
134 + $before = is_array( $params ) && ! empty( $params['before'] ) ? sanitize_text_field( Helper::get_string_value( $params['before'] ) ) : '';
135 +
136 + if ( empty( $after ) || empty( $before ) ) {
137 + wp_send_json_error( __( 'Invalid date range.', 'sureforms' ) );
138 + }
139 +
140 + $form = is_array( $params ) && ! empty( $params['form'] ) ? sanitize_text_field( Helper::get_string_value( $params['form'] ) ) : '';
141 +
142 + $where = [
143 + [
144 + [
145 + 'key' => 'created_at',
146 + 'value' => $after,
147 + 'compare' => '>=',
91 148 ],
149 + [
150 + 'key' => 'created_at',
151 + 'value' => $before,
152 + 'compare' => '<=',
153 + ],
92 154 ],
93 - // This route is used to map the AI response to SureForms fields markup.
94 - 'map-fields' => [
95 - 'methods' => 'POST',
96 - 'callback' => [ Field_Mapping::get_instance(), 'generate_gutenberg_fields_from_questions' ],
97 - 'permission_callback' => [ $this, 'can_edit_posts' ],
155 + ];
156 +
157 + if ( ! empty( $form ) ) {
158 + $where[0][] = [
159 + 'key' => 'form_id',
160 + 'value' => $form,
161 + 'compare' => '=',
162 + ];
163 + }
164 +
165 + return Entries::get_instance()->get_results(
166 + $where,
167 + 'created_at',
168 + [ 'ORDER BY created_at DESC' ]
169 + );
170 + }
171 +
172 + /**
173 + * Get the data for all the forms.
174 + *
175 + * @param \WP_REST_Request $request Full details about the request.
176 + * @since 1.7.0
177 + * @return array<mixed>
178 + */
179 + public function get_form_data( $request ) {
180 + $nonce = Helper::get_string_value( $request->get_header( 'X-WP-Nonce' ) );
181 +
182 + if ( ! wp_verify_nonce( sanitize_text_field( $nonce ), 'wp_rest' ) ) {
183 + wp_send_json_error( __( 'Security verification failed. Please refresh the page and try again.', 'sureforms' ) );
184 + }
185 +
186 + $forms = Helper::get_instance()->get_sureforms();
187 +
188 + return ! empty( $forms ) ? $forms : [];
189 + }
190 +
191 + /**
192 + * Search WordPress pages for async dropdowns.
193 + *
194 + * @param \WP_REST_Request $request Full details about the request.
195 + * @since 2.5.2
196 + * @return \WP_REST_Response
197 + */
198 + public function search_pages( $request ) {
199 + $nonce = Helper::get_string_value( $request->get_header( 'X-WP-Nonce' ) );
200 +
201 + if ( ! wp_verify_nonce( sanitize_text_field( $nonce ), 'wp_rest' ) ) {
202 + return new \WP_REST_Response(
203 + [ 'error' => __( 'Nonce verification failed.', 'sureforms' ) ],
204 + 403
205 + );
206 + }
207 +
208 + $search = Helper::get_string_value( $request->get_param( 'search' ) );
209 + $page = max( 1, (int) $request->get_param( 'page' ) );
210 + $per_page = max( 1, min( 50, (int) $request->get_param( 'per_page' ) ) );
211 + $query_per_page = $per_page + 1;
212 + $selected_urls = $request->get_param( 'selected_urls' );
213 + $selected_url = Helper::get_string_value( $request->get_param( 'selected_url' ) );
214 + $selected_values = [];
215 +
216 + if ( ! empty( $selected_url ) ) {
217 + $selected_values[] = $selected_url;
218 + }
219 +
220 + if ( is_array( $selected_urls ) ) {
221 + $selected_values = array_merge( $selected_values, $selected_urls );
222 + }
223 +
224 + $selected_values = array_slice( array_values( array_unique( array_filter( $selected_values ) ) ), 0, 5 );
225 +
226 + $args = [
227 + 'post_type' => 'page',
228 + 'post_status' => 'publish',
229 + 's' => $search,
230 + 'orderby' => 'title',
231 + 'order' => 'ASC',
232 + 'posts_per_page' => $query_per_page,
233 + 'paged' => $page,
234 + 'fields' => 'ids',
235 + 'no_found_rows' => true,
236 + 'update_post_meta_cache' => false,
237 + 'update_post_term_cache' => false,
238 + ];
239 +
240 + add_filter( 'posts_search', [ $this, 'search_only_post_titles' ], 10, 2 );
241 + try {
242 + $query = new \WP_Query( $args );
243 + } finally {
244 + remove_filter( 'posts_search', [ $this, 'search_only_post_titles' ], 10 );
245 + }
246 +
247 + $post_ids = is_array( $query->posts )
248 + ? array_map(
249 + static function ( $post ): int {
250 + if ( $post instanceof \WP_Post ) {
251 + return absint( $post->ID );
252 + }
253 +
254 + return absint( $post );
255 + },
256 + $query->posts
257 + )
258 + : [];
259 + $has_more = count( $post_ids ) > $per_page;
260 + $post_ids = array_slice( $post_ids, 0, $per_page );
261 +
262 + // Note: url_to_postid() issues one DB query per URL. Currently only a single
263 + // selected_url is used in practice; if multi-URL usage grows, consider a
264 + // batched WHERE guid IN (...) query instead.
265 + foreach ( $selected_values as $selected_value ) {
266 + $selected_id = url_to_postid( $selected_value );
267 +
268 + if ( ! $selected_id || in_array( $selected_id, $post_ids, true ) ) {
269 + continue;
270 + }
271 +
272 + if ( 'page' !== get_post_type( $selected_id ) || 'publish' !== get_post_status( $selected_id ) ) {
273 + continue;
274 + }
275 +
276 + array_unshift( $post_ids, $selected_id );
277 + }
278 +
279 + $items = [];
280 + foreach ( $post_ids as $post_id ) {
281 + $permalink = get_permalink( $post_id );
282 +
283 + if ( ! $permalink ) {
284 + continue;
285 + }
286 +
287 + $title = get_post_field( 'post_title', $post_id );
288 + $items[] = [
289 + 'id' => $post_id,
290 + 'label' => ! empty( $title ) ? wp_strip_all_tags( $title ) : (string) $post_id,
291 + 'value' => esc_url_raw( $permalink ),
292 + ];
293 + }
294 +
295 + return new \WP_REST_Response(
296 + [
297 + 'items' => $items,
298 + 'pagination' => [
299 + 'page' => $page,
300 + 'per_page' => $per_page,
301 + 'has_more' => $has_more,
302 + ],
98 303 ],
99 - // This route is used to initiate auth process when user tries to authenticate on billing portal.
100 - 'initiate-auth' => [
101 - 'methods' => 'GET',
102 - 'callback' => [ AI_Auth::get_instance(), 'get_auth_url' ],
103 - 'permission_callback' => [ $this, 'can_edit_posts' ],
304 + 200
305 + );
306 + }
307 +
308 + /**
309 + * Restrict search to post titles for dropdown lookups.
310 + *
311 + * @param string $search Search SQL fragment.
312 + * @param \WP_Query $wp_query Current WP_Query.
313 + * @since 2.5.2
314 + * @return string
315 + */
316 + public function search_only_post_titles( $search, $wp_query ) {
317 + global $wpdb;
318 +
319 + if ( ! empty( $search ) && ! empty( $wp_query->query_vars['search_terms'] ) ) {
320 + $query_vars = $wp_query->query_vars;
321 + $wild = ! empty( $query_vars['exact'] ) ? '' : '%';
322 + $search_sql = [];
323 +
324 + foreach ( (array) $query_vars['search_terms'] as $term ) {
325 + $search_sql[] = $wpdb->prepare(
326 + "{$wpdb->posts}.post_title LIKE %s",
327 + $wild . $wpdb->esc_like( $term ) . $wild
328 + );
329 + }
330 +
331 + $search = ' AND ' . implode( ' AND ', $search_sql );
332 + }
333 +
334 + return $search;
335 + }
336 +
337 + /**
338 + * Set onboarding completion status.
339 + *
340 + * @param \WP_REST_Request $request Full details about the request.
341 + * @since 1.9.1
342 + * @return \WP_REST_Response
343 + */
344 + public function set_onboarding_status( $request ) {
345 + $nonce = Helper::get_string_value( $request->get_header( 'X-WP-Nonce' ) );
346 +
347 + if ( ! wp_verify_nonce( sanitize_text_field( $nonce ), 'wp_rest' ) ) {
348 + return new \WP_REST_Response(
349 + [ 'error' => __( 'Security verification failed. Please refresh the page and try again.', 'sureforms' ) ],
350 + 403
351 + );
352 + }
353 +
354 + // Set the onboarding status to yes always.
355 + Onboarding::get_instance()->set_onboarding_status( 'yes' );
356 +
357 + // Get analytics data from request.
358 + $analytics_data = $request->get_param( 'analyticsData' );
359 +
360 + // Save analytics data if provided.
361 + if ( $analytics_data ) {
362 + // Use Helper::update_srfm_option instead of update_option.
363 + Helper::update_srfm_option( 'onboarding_analytics', $analytics_data );
364 + }
365 +
366 + return new \WP_REST_Response( [ 'success' => true ] );
367 + }
368 +
369 + /**
370 + * Get onboarding completion status.
371 + *
372 + * @param \WP_REST_Request $request Full details about the request.
373 + * @since 1.9.1
374 + * @return \WP_REST_Response
375 + */
376 + public function get_onboarding_status( $request ) {
377 + $nonce = Helper::get_string_value( $request->get_header( 'X-WP-Nonce' ) );
378 +
379 + if ( ! wp_verify_nonce( sanitize_text_field( $nonce ), 'wp_rest' ) ) {
380 + return new \WP_REST_Response(
381 + [ 'error' => __( 'Security verification failed. Please refresh the page and try again.', 'sureforms' ) ],
382 + 403
383 + );
384 + }
385 +
386 + $status = Onboarding::get_instance()->get_onboarding_status();
387 +
388 + return new \WP_REST_Response( [ 'completed' => $status ] );
389 + }
390 +
391 + /**
392 + * Save onboarding user details and send lead data to metrics server.
393 + *
394 + * @since 2.5.3
395 + * @param \WP_REST_Request $request Full details about the request.
396 + * @return \WP_REST_Response
397 + */
398 + public function save_onboarding_user_details( $request ) {
399 + $nonce = Helper::get_string_value( $request->get_header( 'X-WP-Nonce' ) );
400 +
401 + if ( ! wp_verify_nonce( sanitize_text_field( $nonce ), 'wp_rest' ) ) {
402 + return new \WP_REST_Response(
403 + [ 'error' => __( 'Security verification failed. Please refresh the page and try again.', 'sureforms' ) ],
404 + 403
405 + );
406 + }
407 +
408 + $first_name = sanitize_text_field( Helper::get_string_value( $request->get_param( 'first_name' ) ) );
409 + $last_name = sanitize_text_field( Helper::get_string_value( $request->get_param( 'last_name' ) ) );
410 + $email = sanitize_email( Helper::get_string_value( $request->get_param( 'email' ) ) );
411 +
412 + if ( empty( $first_name ) || empty( $email ) || ! is_email( $email ) ) {
413 + return new \WP_REST_Response(
414 + [ 'error' => __( 'Invalid onboarding user details.', 'sureforms' ) ],
415 + 400
416 + );
417 + }
418 +
419 + $stored_details = $this->get_onboarding_user_details();
420 + if ( ! empty( $stored_details['lead'] ) ) {
421 + return new \WP_REST_Response(
422 + [
423 + 'success' => true,
424 + 'lead' => true,
425 + ]
426 + );
427 + }
428 +
429 + $this->set_onboarding_user_details(
430 + [
431 + 'first_name' => $first_name,
432 + 'last_name' => $last_name,
433 + 'email' => $email,
434 + ]
435 + );
436 +
437 + $domain = wp_parse_url( home_url(), PHP_URL_HOST );
438 + if ( ! is_string( $domain ) ) {
439 + $domain = '';
440 + }
441 +
442 + $body = wp_json_encode(
443 + [
444 + // Lowercase keys satisfy current BSF Metrics REST arg validation.
445 + 'email' => $email,
446 + 'first_name' => $first_name,
447 + 'last_name' => $last_name,
448 + 'domain' => $domain,
449 + 'source' => 'sureforms',
450 + // Keep legacy uppercase keys for backward compatibility.
451 + 'EMAIL' => $email,
452 + 'FIRSTNAME' => $first_name,
453 + 'LASTNAME' => $last_name,
454 + 'DOMAIN' => $domain,
455 + ]
456 + );
457 +
458 + $lead_captured = false;
459 + if ( false !== $body ) {
460 + $response = wp_remote_post(
461 + 'https://metrics.brainstormforce.com/wp-json/bsf-metrics-server/v1/subscribe',
462 + [
463 + 'headers' => [
464 + 'Content-Type' => 'application/json',
465 + ],
466 + 'body' => $body,
467 + 'timeout' => 15,
468 + ]
469 + );
470 +
471 + $response_code = wp_remote_retrieve_response_code( $response );
472 + if ( ! is_wp_error( $response ) && in_array( $response_code, [ 200, 201, 204 ], true ) ) {
473 + $lead_captured = true;
474 + }
475 + }
476 +
477 + if ( $lead_captured ) {
478 + $this->set_onboarding_user_details(
479 + [
480 + 'first_name' => $first_name,
481 + 'last_name' => $last_name,
482 + 'email' => $email,
483 + 'lead' => true,
484 + ]
485 + );
486 + }
487 +
488 + return new \WP_REST_Response(
489 + [
490 + 'success' => true,
491 + 'lead' => $lead_captured,
492 + ]
493 + );
494 + }
495 +
496 + /**
497 + * Get plugin status for specified plugin.
498 + *
499 + * @param \WP_REST_Request $request Full details about the request.
500 + * @since 1.9.1
501 + * @return \WP_REST_Response
502 + */
503 + public function get_plugin_status( $request ) {
504 + $nonce = Helper::get_string_value( $request->get_header( 'X-WP-Nonce' ) );
505 +
506 + if ( ! wp_verify_nonce( sanitize_text_field( $nonce ), 'wp_rest' ) ) {
507 + return new \WP_REST_Response(
508 + [ 'error' => __( 'Security verification failed. Please refresh the page and try again.', 'sureforms' ) ],
509 + 403
510 + );
511 + }
512 +
513 + $params = $request->get_params();
514 + $plugin_slug = is_array( $params ) && isset( $params['plugin'] ) ?
515 + sanitize_text_field( Helper::get_string_value( $params['plugin'] ) ) : '';
516 +
517 + if ( empty( $plugin_slug ) ) {
518 + return new \WP_REST_Response(
519 + [ 'error' => __( 'Plugin identifier is required.', 'sureforms' ) ],
520 + 400
521 + );
522 + }
523 +
524 + $integrations = Helper::sureforms_get_integration();
525 +
526 + if ( ! isset( $integrations[ $plugin_slug ] ) ) {
527 + return new \WP_REST_Response(
528 + [ 'error' => __( 'Integration not found.', 'sureforms' ) ],
529 + 404
530 + );
531 + }
532 +
533 + $plugin_data = $integrations[ $plugin_slug ];
534 +
535 + // Get fresh status.
536 + if ( is_array( $plugin_data ) && isset( $plugin_data['path'] ) ) {
537 + $plugin_data['status'] = Helper::get_plugin_status( Helper::get_string_value( $plugin_data['path'] ) );
538 + }
539 +
540 + return new \WP_REST_Response( $plugin_data );
541 + }
542 +
543 + /**
544 + * Sanitize entry IDs.
545 + *
546 + * @param mixed $value Value to sanitize.
547 + * @since 2.0.0
548 + * @return array<int>
549 + */
550 + public function sanitize_entry_ids( $value ) {
551 + if ( is_array( $value ) ) {
552 + return array_filter( array_map( 'absint', $value ) );
553 + }
554 + if ( is_numeric( $value ) ) {
555 + return [ absint( $value ) ];
556 + }
557 + if ( is_string( $value ) ) {
558 + // Handle comma-separated values.
559 + $ids = explode( ',', $value );
560 + return array_filter( array_map( 'absint', $ids ) );
561 + }
562 + return [];
563 + }
564 +
565 + /**
566 + * Validate read action parameter.
567 + *
568 + * @param string $param Action parameter value.
569 + * @since 2.0.0
570 + * @return bool
571 + */
572 + public function validate_read_action( $param ) {
573 + return in_array( $param, [ 'read', 'unread' ], true );
574 + }
575 +
576 + /**
577 + * Validate trash action parameter.
578 + *
579 + * @param string $param Action parameter value.
580 + * @since 2.0.0
581 + * @return bool
582 + */
583 + public function validate_trash_action( $param ) {
584 + return in_array( $param, [ 'trash', 'restore' ], true );
585 + }
586 +
587 + /**
588 + * Get entries list with filters and pagination.
589 + *
590 + * @param \WP_REST_Request $request Full details about the request.
591 + * @since 2.0.0
592 + * @return \WP_REST_Response
593 + */
594 + public function get_entries_list( $request ) {
595 + $nonce = Helper::get_string_value( $request->get_header( 'X-WP-Nonce' ) );
596 +
597 + if ( ! wp_verify_nonce( sanitize_text_field( $nonce ), 'wp_rest' ) ) {
598 + return new \WP_REST_Response(
599 + [ 'error' => __( 'Security verification failed. Please refresh the page and try again.', 'sureforms' ) ],
600 + 403
601 + );
602 + }
603 +
604 + $params = $request->get_params();
605 +
606 + $args = [
607 + 'form_id' => isset( $params['form_id'] ) ? absint( $params['form_id'] ) : 0,
608 + 'status' => isset( $params['status'] ) ? sanitize_text_field( $params['status'] ) : 'all',
609 + 'search' => isset( $params['search'] ) ? sanitize_text_field( $params['search'] ) : '',
610 + 'date_from' => isset( $params['date_from'] ) ? sanitize_text_field( $params['date_from'] ) : '',
611 + 'date_to' => isset( $params['date_to'] ) ? sanitize_text_field( $params['date_to'] ) : '',
612 + 'orderby' => isset( $params['orderby'] ) ? sanitize_text_field( $params['orderby'] ) : 'created_at',
613 + 'order' => isset( $params['order'] ) ? sanitize_text_field( $params['order'] ) : 'DESC',
614 + 'per_page' => isset( $params['per_page'] ) ? absint( $params['per_page'] ) : 20,
615 + 'page' => isset( $params['page'] ) ? absint( $params['page'] ) : 1,
616 + ];
617 +
618 + $result = Entries_Class::get_entries( $args );
619 +
620 + // Add form permalink to each entry.
621 + if ( isset( $result['entries'] ) && is_array( $result['entries'] ) ) {
622 + foreach ( $result['entries'] as &$entry ) {
623 + if ( isset( $entry['form_id'] ) ) {
624 + $entry['form_permalink'] = get_permalink( absint( $entry['form_id'] ) );
625 + }
626 + }
627 + }
628 +
629 + return new \WP_REST_Response( $result, 200 );
630 + }
631 +
632 + /**
633 + * Update entries read status (read/unread).
634 + *
635 + * @param \WP_REST_Request $request Full details about the request.
636 + * @since 2.0.0
637 + * @return \WP_REST_Response
638 + */
639 + public function update_entries_read_status( $request ) {
640 + $nonce = Helper::get_string_value( $request->get_header( 'X-WP-Nonce' ) );
641 +
642 + if ( ! wp_verify_nonce( sanitize_text_field( $nonce ), 'wp_rest' ) ) {
643 + return new \WP_REST_Response(
644 + [ 'error' => __( 'Security verification failed. Please refresh the page and try again.', 'sureforms' ) ],
645 + 403
646 + );
647 + }
648 +
649 + $entry_ids = $request->get_param( 'entry_ids' );
650 + $action = $request->get_param( 'action' );
651 +
652 + if ( empty( $entry_ids ) ) {
653 + return new \WP_REST_Response(
654 + [ 'error' => __( 'Select at least one entry.', 'sureforms' ) ],
655 + 400
656 + );
657 + }
658 +
659 + if ( empty( $action ) ) {
660 + return new \WP_REST_Response(
661 + [ 'error' => __( 'Action is required.', 'sureforms' ) ],
662 + 400
663 + );
664 + }
665 +
666 + // Validate action.
667 + if ( ! $this->validate_read_action( $action ) ) {
668 + return new \WP_REST_Response(
669 + [ 'error' => __( 'Invalid action. Use "read" or "unread".', 'sureforms' ) ],
670 + 400
671 + );
672 + }
673 +
674 + $result = Entries_Class::update_status( $entry_ids, $action );
675 +
676 + $status_code = $result['success'] ? 200 : 400;
677 +
678 + return new \WP_REST_Response( $result, $status_code );
679 + }
680 +
681 + /**
682 + * Update entries trash status (trash/restore).
683 + *
684 + * @param \WP_REST_Request $request Full details about the request.
685 + * @since 2.0.0
686 + * @return \WP_REST_Response
687 + */
688 + public function update_entries_trash_status( $request ) {
689 + $nonce = Helper::get_string_value( $request->get_header( 'X-WP-Nonce' ) );
690 +
691 + if ( ! wp_verify_nonce( sanitize_text_field( $nonce ), 'wp_rest' ) ) {
692 + return new \WP_REST_Response(
693 + [ 'error' => __( 'Security verification failed. Please refresh the page and try again.', 'sureforms' ) ],
694 + 403
695 + );
696 + }
697 +
698 + $entry_ids = $request->get_param( 'entry_ids' );
699 + $action = $request->get_param( 'action' );
700 +
701 + if ( empty( $entry_ids ) ) {
702 + return new \WP_REST_Response(
703 + [ 'error' => __( 'Select at least one entry.', 'sureforms' ) ],
704 + 400
705 + );
706 + }
707 +
708 + if ( empty( $action ) ) {
709 + return new \WP_REST_Response(
710 + [ 'error' => __( 'Action is required.', 'sureforms' ) ],
711 + 400
712 + );
713 + }
714 +
715 + // Validate action.
716 + if ( ! $this->validate_trash_action( $action ) ) {
717 + return new \WP_REST_Response(
718 + [ 'error' => __( 'Invalid action. Use "trash" or "restore".', 'sureforms' ) ],
719 + 400
720 + );
721 + }
722 +
723 + $result = Entries_Class::update_status( $entry_ids, $action );
724 +
725 + $status_code = $result['success'] ? 200 : 400;
726 +
727 + return new \WP_REST_Response( $result, $status_code );
728 + }
729 +
730 + /**
731 + * Permanently delete entries.
732 + *
733 + * @param \WP_REST_Request $request Full details about the request.
734 + * @since 2.0.0
735 + * @return \WP_REST_Response
736 + */
737 + public function delete_entries( $request ) {
738 + $nonce = Helper::get_string_value( $request->get_header( 'X-WP-Nonce' ) );
739 +
740 + if ( ! wp_verify_nonce( sanitize_text_field( $nonce ), 'wp_rest' ) ) {
741 + return new \WP_REST_Response(
742 + [ 'error' => __( 'Security verification failed. Please refresh the page and try again.', 'sureforms' ) ],
743 + 403
744 + );
745 + }
746 +
747 + $entry_ids = $request->get_param( 'entry_ids' );
748 +
749 + if ( empty( $entry_ids ) ) {
750 + return new \WP_REST_Response(
751 + [ 'error' => __( 'Select at least one entry.', 'sureforms' ) ],
752 + 400
753 + );
754 + }
755 +
756 + $result = Entries_Class::delete_entries( $entry_ids );
757 +
758 + $status_code = $result['success'] ? 200 : 400;
759 +
760 + return new \WP_REST_Response( $result, $status_code );
761 + }
762 +
763 + /**
764 + * Get entry details with form data, submission info, and metadata.
765 + *
766 + * @param \WP_REST_Request $request Full details about the request.
767 + * @since 2.0.0
768 + * @return \WP_REST_Response
769 + */
770 + public function get_entry_details( $request ) {
771 + $nonce = Helper::get_string_value( $request->get_header( 'X-WP-Nonce' ) );
772 +
773 + if ( ! wp_verify_nonce( sanitize_text_field( $nonce ), 'wp_rest' ) ) {
774 + return new \WP_REST_Response(
775 + [ 'error' => __( 'Security verification failed. Please refresh the page and try again.', 'sureforms' ) ],
776 + 403
777 + );
778 + }
779 +
780 + $entry_id = absint( $request->get_param( 'id' ) );
781 +
782 + if ( empty( $entry_id ) ) {
783 + return new \WP_REST_Response(
784 + [ 'error' => __( 'Entry ID is required.', 'sureforms' ) ],
785 + 400
786 + );
787 + }
788 +
789 + $entry = Entries::get( $entry_id );
790 +
791 + if ( ! $entry ) {
792 + return new \WP_REST_Response(
793 + [ 'error' => __( 'Entry not found.', 'sureforms' ) ],
794 + 404
795 + );
796 + }
797 +
798 + // Get adjacent entry IDs for navigation scoped to the same form.
799 + $form_id_raw = $entry['form_id'] ?? 0;
800 + $adjacent_entries = Entries_Class::get_adjacent_entry_ids( $entry_id, [ 'form_id' => is_scalar( $form_id_raw ) ? absint( $form_id_raw ) : 0 ] );
801 +
802 + // Process form data.
803 + $form_data = [];
804 + $excluded_fields = [ 'srfm-honeypot-field', 'g-recaptcha-response', 'srfm-sender-email-field' ];
805 + $entry_form_data = $entry['form_data'] ?? [];
806 +
807 + if ( is_array( $entry_form_data ) ) {
808 + foreach ( $entry_form_data as $field_name => $value ) {
809 + if ( ! is_string( $field_name ) || in_array( $field_name, $excluded_fields, true ) ) {
810 + continue;
811 + }
812 + if ( false === str_contains( $field_name, '-lbl-' ) ) {
813 + continue;
814 + }
815 +
816 + $label_parts = explode( '-lbl-', $field_name );
817 + $label = isset( $label_parts[1] ) ? explode( '-', $label_parts[1] )[0] : '';
818 + $label = $label ? Helper::decode( $label ) : '';
819 + $field_block_name = Helper::get_block_name_from_field( $field_name );
820 +
821 + /**
822 + * Filter: 'srfm_entry_value'
823 + *
824 + * This filter is used to allow 3rd party plugins or custom code to modify
825 + * the entry field value in the entry details REST API response, if required.
826 + * For example, you may want to format, mask, or otherwise transform sensitive
827 + * data before output. Note the value reaching this filter is not encrypted by
828 + * SureForms — labels and values are carried as unkeyed base64 at most.
829 + *
830 + * @since 2.0.0
831 + *
832 + * @param mixed $value The original value for the field.
833 + * @param array $context An array of context, including:
834 + * - field_name (string)
835 + * - label (string)
836 + * - field_block_name (string)
837 + *
838 + * @return mixed
839 + */
840 + $value = apply_filters(
841 + 'srfm_entry_value',
842 + $value,
843 + [
844 + 'field_name' => $field_name,
845 + 'label' => $label,
846 + 'field_block_name' => $field_block_name,
847 + ]
848 + );
849 +
850 + $form_data[] = [
851 + 'field_name' => $field_name,
852 + 'label' => $label,
853 + 'value' => $value,
854 + 'block_name' => $field_block_name,
855 + ];
856 + }
857 + }
858 +
859 + // Get user info.
860 + $user_id = Helper::get_integer_value( $entry['user_id'] );
861 + $user_info = 0 !== $user_id ? get_userdata( $user_id ) : null;
862 +
863 + // Get form info.
864 + $form_title = get_post_field( 'post_title', $entry['form_id'] );
865 + // Translators: %d is the form ID.
866 + $form_name = ! empty( $form_title ) ? $form_title : sprintf( __( 'SureForms Form #%d', 'sureforms' ), intval( $entry['form_id'] ) );
867 +
868 + // Parse form content to get structured field data.
869 + $form_content = get_post_field( 'post_content', $entry['form_id'] );
870 + $form_fields = $this->parse_form_fields( $form_content, $entry['form_data'] ?? [] );
871 +
872 + $response_data = [
873 + 'id' => $entry_id,
874 + 'form_id' => $entry['form_id'],
875 + 'form_name' => $form_name,
876 + 'form_permalink' => get_permalink( $entry['form_id'] ),
877 + 'status' => $entry['status'],
878 + 'created_at' => $entry['created_at'],
879 + 'form_data' => $form_data,
880 + 'form_content' => $form_fields,
881 + 'submission_info' => [
882 + 'user_ip' => $entry['submission_info']['user_ip'] ?? '',
883 + 'browser_name' => $entry['submission_info']['browser_name'] ?? '',
884 + 'device_name' => $entry['submission_info']['device_name'] ?? '',
885 + // Re-sanitize at the exposure boundary in case the stored value
886 + // was written by a future code path that bypasses form-submit.php.
887 + 'submission_url' => esc_url_raw( $entry['submission_info']['submission_url'] ?? '', [ 'http', 'https' ] ),
104 888 ],
105 - // This route is to used to decrypt the access key and save it in the database.
106 - 'handle-access-key' => [
107 - 'methods' => 'POST',
108 - 'callback' => [ AI_Auth::get_instance(), 'handle_access_key' ],
109 - 'permission_callback' => [ $this, 'can_edit_posts' ],
889 + 'user' => $user_info ? [
890 + 'id' => $user_id,
891 + 'display_name' => $user_info->display_name,
892 + 'profile_url' => get_author_posts_url( $user_id ),
893 + ] : null,
894 + 'extras' => $entry['extras'] ?? [],
895 + 'navigation' => [
896 + 'previous_entry_id' => $adjacent_entries['previous_id'] ?? null,
897 + 'next_entry_id' => $adjacent_entries['next_id'] ?? null,
110 898 ],
111 899 ];
900 +
901 + return new \WP_REST_Response( $response_data, 200 );
112 902 }
113 903
114 904 /**
115 - * Checks whether the value is boolean or not.
905 + * Get entry logs with pagination support.
116 906 *
117 - * @param mixed $value value to be checked.
118 - * @since 0.0.8
119 - * @return boolean
907 + * @param \WP_REST_Request $request Full details about the request.
908 + * @since 2.0.0
909 + * @return \WP_REST_Response
120 910 */
121 - public function sanitize_boolean_field( $value ) {
122 - return filter_var( $value, FILTER_VALIDATE_BOOLEAN );
911 + public function get_entry_logs( $request ) {
912 + $nonce = Helper::get_string_value( $request->get_header( 'X-WP-Nonce' ) );
913 +
914 + if ( ! wp_verify_nonce( sanitize_text_field( $nonce ), 'wp_rest' ) ) {
915 + return new \WP_REST_Response(
916 + [ 'error' => __( 'Security verification failed. Please refresh the page and try again.', 'sureforms' ) ],
917 + 403
918 + );
919 + }
920 +
921 + $entry_id = absint( $request->get_param( 'id' ) );
922 + $per_page = absint( $request->get_param( 'per_page' ) );
923 + $per_page = $per_page ? $per_page : 3;
924 + $page = absint( $request->get_param( 'page' ) );
925 + $page = $page ? $page : 1;
926 +
927 + if ( empty( $entry_id ) ) {
928 + return new \WP_REST_Response(
929 + [ 'error' => __( 'Entry ID is required.', 'sureforms' ) ],
930 + 400
931 + );
932 + }
933 +
934 + $entry = Entries::get( $entry_id );
935 +
936 + if ( ! $entry ) {
937 + return new \WP_REST_Response(
938 + [ 'error' => __( 'Entry not found.', 'sureforms' ) ],
939 + 404
940 + );
941 + }
942 +
943 + $logs = $entry['logs'] ?? [];
944 + $logs = is_array( $logs ) ? $logs : [];
945 + $total_logs = count( $logs );
946 + $total_pages = ceil( $total_logs / $per_page );
947 + $offset = ( $page - 1 ) * $per_page;
948 +
949 + // Paginate logs.
950 + $paginated_logs = array_slice( $logs, $offset, $per_page );
951 +
952 + // Format logs with unique IDs for deletion.
953 + $formatted_logs = [];
954 + foreach ( $paginated_logs as $index => $log ) {
955 + if ( ! is_array( $log ) ) {
956 + continue;
957 + }
958 + $formatted_log = [
959 + 'id' => $offset + $index, // Use offset-based ID for consistent deletion.
960 + 'title' => $log['title'] ?? '',
961 + 'timestamp' => $log['timestamp'] ?? time(),
962 + 'messages' => $log['messages'] ?? [],
963 + ];
964 +
965 + // Pass through (sanitized) retry metadata so an integration/webhook log row can
966 + // offer a "Retry" action for that specific failed trigger. Set by the Pro
967 + // webhook / native-integration dispatchers as [ 'type' => webhook|native, 'id' => <trigger id> ].
968 + if ( isset( $log['retry'] ) && is_array( $log['retry'] ) && ! empty( $log['retry']['type'] ) && isset( $log['retry']['id'] ) ) {
969 + $formatted_log['retry'] = [
970 + 'type' => sanitize_text_field( Helper::get_string_value( $log['retry']['type'] ) ),
971 + 'id' => sanitize_text_field( Helper::get_string_value( $log['retry']['id'] ) ),
972 + ];
973 + }
974 +
975 + $formatted_logs[] = $formatted_log;
976 + }
977 +
978 + $response_data = [
979 + 'logs' => $formatted_logs,
980 + 'current_page' => $page,
981 + 'per_page' => $per_page,
982 + 'total' => $total_logs,
983 + 'total_pages' => $total_pages,
984 + ];
985 +
986 + return new \WP_REST_Response( $response_data, 200 );
123 987 }
124 988
125 989 /**
126 - * Generate the block slugs as per the request by parsing the post content.
990 + * Export entries to CSV or ZIP.
127 991 *
128 - * @param \WP_REST_Request $request Full details about the request.
129 - * @since 0.0.7
130 - * @return void
992 + * @param \WP_REST_Request $request Full details about the request.
993 + * @since 2.0.0
994 + * @return \WP_REST_Response
131 995 */
132 - public function generate_block_slugs_by_content( $request ) {
996 + public function export_entries( $request ) {
133 997 $nonce = Helper::get_string_value( $request->get_header( 'X-WP-Nonce' ) );
134 998
135 999 if ( ! wp_verify_nonce( sanitize_text_field( $nonce ), 'wp_rest' ) ) {
136 - wp_send_json_error( __( 'Nonce verification failed.', 'sureforms' ) );
1000 + return new \WP_REST_Response(
1001 + [ 'error' => __( 'Security verification failed. Please refresh the page and try again.', 'sureforms' ) ],
1002 + 403
1003 + );
137 1004 }
138 1005
139 - $slugs = [];
140 - $updated = false;
141 - $params = $request->get_params();
1006 + $params = $request->get_params();
142 1007
143 - if ( empty( $params['formID'] ) ) {
144 - wp_send_json_error( __( 'Invalid request. Form ID missing.', 'sureforms' ) );
1008 + $args = [
1009 + 'entry_ids' => isset( $params['entry_ids'] ) ? $this->sanitize_entry_ids( $params['entry_ids'] ) : [],
1010 + 'form_id' => isset( $params['form_id'] ) ? absint( $params['form_id'] ) : 0,
1011 + 'status' => isset( $params['status'] ) ? sanitize_text_field( $params['status'] ) : 'all',
1012 + 'search' => isset( $params['search'] ) ? sanitize_text_field( $params['search'] ) : '',
1013 + 'date_from' => isset( $params['date_from'] ) ? sanitize_text_field( $params['date_from'] ) : '',
1014 + 'date_to' => isset( $params['date_to'] ) ? sanitize_text_field( $params['date_to'] ) : '',
1015 + ];
1016 +
1017 + /**
1018 + * Export result with success status and either error message or file details.
1019 + *
1020 + * @var array{success: false, error: string} | array{success: true, filename: string, filepath: string, type: string} $result
1021 + */
1022 + $result = Entries_Class::export_entries( $args );
1023 +
1024 + if ( ! $result['success'] ) {
1025 + return new \WP_REST_Response(
1026 + [ 'error' => $result['error'] ],
1027 + 400
1028 + );
145 1029 }
146 1030
147 - $form = get_post( absint( $params['formID'] ) );
148 - $content = ! empty( $params['content'] ) ? wp_kses_post( $params['content'] ) : '';
1031 + // Return file information for download.
1032 + $filepath = Helper::get_string_value( $result['filepath'] );
1033 + return new \WP_REST_Response(
1034 + [
1035 + 'success' => true,
1036 + 'filename' => $result['filename'],
1037 + 'filepath' => $result['filepath'],
1038 + 'type' => $result['type'],
1039 + 'download_url' => add_query_arg(
1040 + '_wpnonce',
1041 + wp_create_nonce( 'srfm_download_export' ),
1042 + admin_url( 'admin-ajax.php?action=srfm_download_export&file=' . rawurlencode( basename( $filepath ) ) )
1043 + ),
1044 + ],
1045 + 200
1046 + );
1047 + }
1048 + /**
1049 + * Manage form lifecycle operations (trash, restore, delete).
1050 + *
1051 + * @param \WP_REST_Request $request Full details about the request.
1052 + * @since 2.0.0
1053 + * @return \WP_REST_Response|\WP_Error
1054 + */
1055 + public function manage_form_lifecycle( $request ) {
1056 + $nonce = Helper::get_string_value( $request->get_header( 'X-WP-Nonce' ) );
149 1057
150 - if ( ! is_null( $form ) ) {
151 - Helper::process_blocks( parse_blocks( $form->post_content ), $slugs, $updated );
1058 + if ( ! wp_verify_nonce( sanitize_text_field( $nonce ), 'wp_rest' ) ) {
1059 + return new \WP_Error(
1060 + 'invalid_nonce',
1061 + __( 'Security verification failed. Please refresh the page and try again.', 'sureforms' ),
1062 + [ 'status' => 403 ]
1063 + );
152 1064 }
153 1065
154 - Helper::process_blocks( parse_blocks( $content ), $slugs, $updated, '', true );
1066 + $params = $request->get_params();
1067 + $form_ids = isset( $params['form_ids'] ) && is_array( $params['form_ids'] ) ?
1068 + array_map( 'intval', $params['form_ids'] ) :
1069 + [ intval( $params['form_ids'] ) ];
1070 + $action = isset( $params['action'] ) ? sanitize_text_field( Helper::get_string_value( $params['action'] ) ) : '';
155 1071
156 - wp_send_json_success( $slugs );
1072 + if ( empty( $form_ids ) || empty( $action ) ) {
1073 + return new \WP_Error(
1074 + 'missing_parameters',
1075 + __( 'Select at least one form and specify an action.', 'sureforms' ),
1076 + [ 'status' => 400 ]
1077 + );
1078 + }
1079 +
1080 + $results = [];
1081 + $errors = [];
1082 +
1083 + foreach ( $form_ids as $form_id ) {
1084 + $post = get_post( $form_id );
1085 +
1086 + // Validate that the post exists and is a sureforms_form.
1087 + if ( ! $post || 'sureforms_form' !== $post->post_type ) {
1088 + $errors[] = [
1089 + 'form_id' => $form_id,
1090 + 'error' => __( 'Form not found or is not a valid form type.', 'sureforms' ),
1091 + ];
1092 + continue;
1093 + }
1094 +
1095 + $result = false;
1096 +
1097 + switch ( $action ) {
1098 + case 'trash':
1099 + if ( 'trash' === $post->post_status ) {
1100 + $errors[] = [
1101 + 'form_id' => $form_id,
1102 + 'error' => __( 'This form is already in the trash.', 'sureforms' ),
1103 + ];
1104 + } else {
1105 + $result = wp_trash_post( $form_id );
1106 + }
1107 + break;
1108 +
1109 + case 'restore':
1110 + if ( 'trash' !== $post->post_status ) {
1111 + $errors[] = [
1112 + 'form_id' => $form_id,
1113 + 'error' => __( 'This form is not in the trash.', 'sureforms' ),
1114 + ];
1115 + } else {
1116 + $result = wp_untrash_post( $form_id );
1117 + }
1118 + break;
1119 +
1120 + case 'delete':
1121 + // Force delete permanently.
1122 + $result = wp_delete_post( $form_id, true );
1123 + break;
1124 +
1125 + case 'draft':
1126 + if ( 'trash' === $post->post_status ) {
1127 + $errors[] = [
1128 + 'form_id' => $form_id,
1129 + 'error' => __( 'Use the restore action to recover a trashed form before switching it to draft.', 'sureforms' ),
1130 + ];
1131 + } elseif ( 'draft' === $post->post_status ) {
1132 + $errors[] = [
1133 + 'form_id' => $form_id,
1134 + 'error' => __( 'This form is already a draft.', 'sureforms' ),
1135 + ];
1136 + } else {
1137 + $result = wp_update_post(
1138 + [
1139 + 'ID' => $form_id,
1140 + 'post_status' => 'draft',
1141 + ]
1142 + );
1143 + }
1144 + break;
1145 +
1146 + default:
1147 + $errors[] = [
1148 + 'form_id' => $form_id,
1149 + 'error' => __( 'Invalid action.', 'sureforms' ),
1150 + ];
1151 + break;
1152 + }
1153 +
1154 + if ( $result ) {
1155 + $results[] = [
1156 + 'form_id' => $form_id,
1157 + 'action' => $action,
1158 + 'success' => true,
1159 + ];
1160 + } elseif ( ! isset( $errors[ array_search( $form_id, array_column( $errors, 'form_id' ), true ) ] ) ) {
1161 + $errors[] = [
1162 + 'form_id' => $form_id,
1163 + /* translators: %s: action name */
1164 + 'error' => sprintf( __( 'Failed to %s this form. Please try again.', 'sureforms' ), $action ),
1165 + ];
1166 + }
1167 + }
1168 +
1169 + $response_data = [
1170 + 'success' => ! empty( $results ),
1171 + 'action' => $action,
1172 + 'processed_ids' => array_column( $results, 'form_id' ),
1173 + 'success_count' => count( $results ),
1174 + 'results' => $results,
1175 + ];
1176 +
1177 + if ( ! empty( $errors ) ) {
1178 + $response_data['errors'] = $errors;
1179 + $response_data['error_count'] = count( $errors );
1180 + }
1181 +
1182 + return new \WP_REST_Response( $response_data );
1183 + }
1184 +
1185 + /**
1186 + * Recursively extract form fields from blocks.
1187 + *
1188 + * @param array<mixed> $blocks The blocks array.
1189 + * @param array<string, array<mixed>> $sureforms_blocks Registered SureForms block attributes.
1190 + * @param array<string, array<mixed>> &$form_fields Reference to form fields array.
1191 + * @param array<mixed> $entry_data The entry form data.
1192 + * @param bool $is_special_block Whether the current block is a special block (like address).
1193 + * @param int|null $base_counter Base counter for unique field naming.
1194 + * @since 2.0.0
1195 + * @return void
1196 + */
1197 + public function extract_form_fields( $blocks, $sureforms_blocks, &$form_fields, $entry_data = [], $is_special_block = false, $base_counter = null ) {
1198 + if ( null !== $base_counter ) {
1199 + self::$dropdown_counter = $base_counter;
1200 + }
1201 + $block_type = '';
1202 +
1203 + foreach ( $blocks as $block ) {
1204 + if ( ! is_array( $block ) || ! isset( $block['blockName'] ) || ! is_string( $block['blockName'] ) ) {
1205 + continue;
1206 + }
1207 +
1208 + // Check if it's a SureForms block.
1209 + if ( strpos( $block['blockName'], 'srfm/' ) === 0 ) {
1210 + $block_type = str_replace( 'srfm/', '', $block['blockName'] );
1211 + // Skip inline button or fields inside nested blocks except address.
1212 + if ( 'inline-button' === $block_type || ( $is_special_block && 'address' !== $block_type ) ) {
1213 + continue;
1214 + }
1215 +
1216 + if ( isset( $sureforms_blocks[ $block_type ] ) && is_array( $sureforms_blocks[ $block_type ] ) ) {
1217 + $block_attributes = isset( $block['attrs'] ) && is_array( $block['attrs'] ) ? $block['attrs'] : [];
1218 + $default_attributes = $sureforms_blocks[ $block_type ];
1219 +
1220 + // Merge block instance attributes with defaults.
1221 + $merged_attributes = [];
1222 + foreach ( $default_attributes as $attr_name => $attr_config ) {
1223 + if ( ! is_string( $attr_name ) ) {
1224 + continue;
1225 + }
1226 + $default_value = null;
1227 + if ( is_array( $attr_config ) && isset( $attr_config['default'] ) ) {
1228 + $default_value = $attr_config['default'];
1229 + }
1230 + $merged_attributes[ $attr_name ] = $block_attributes[ $attr_name ] ?? $default_value;
1231 + }
1232 +
1233 + // Generate field name.
1234 + $label = $merged_attributes['label'] ?? '';
1235 + $label = is_string( $label ) ? $label : '';
1236 + $slug = $merged_attributes['slug'] ?? '';
1237 + $slug = is_string( $slug ) ? $slug : '';
1238 + $block_id = $merged_attributes['block_id'] ?? '';
1239 + $block_id = is_string( $block_id ) ? $block_id : '';
1240 + $field_name = '';
1241 + $base_field_name = '';
1242 +
1243 + if ( ! empty( $label ) && ! empty( $slug ) && ! empty( $block_id ) ) {
1244 + $input_label = '-lbl-' . Helper::encode( $label );
1245 + $base_field_name = $input_label . '-' . $slug;
1246 +
1247 + // Handle special case for dropdown with instance counter.
1248 + if ( 'dropdown' === $block_type ) {
1249 + self::$dropdown_counter++;
1250 + $unique_slug = $block_type . '-' . self::$dropdown_counter;
1251 + $field_name = 'srfm-' . $unique_slug . '-' . $block_id . $base_field_name;
1252 + } elseif ( 'multi-choice' === $block_type ) {
1253 + // Multi-choice uses standard pattern.
1254 + $field_name = 'srfm-input-' . $block_type . '-' . $block_id . $base_field_name;
1255 + } else {
1256 + // Standard field name for other blocks.
1257 + $field_name = 'srfm-' . $block_type . '-' . $block_id . $base_field_name;
1258 + }
1259 + }
1260 +
1261 + // Allow pro plugin to modify field_name.
1262 + $field_name = apply_filters( 'srfm_extract_form_fields_field_name', $field_name, $base_field_name, $block_type, $block_id );
1263 +
1264 + // Get the value from entry data or use default.
1265 + $field_value = $entry_data[ $field_name ] ?? ( $merged_attributes['defaultValue'] ?? '' );
1266 +
1267 + // Special handling for address blocks - extract inner fields.
1268 + if ( 'address' === $block_type && isset( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) {
1269 + $inner_fields = [];
1270 + $this->extract_form_fields( $block['innerBlocks'], $sureforms_blocks, $inner_fields, $entry_data, false );
1271 + $field_value = $inner_fields;
1272 + }
1273 +
1274 + // Allow plugins to handle special blocks.
1275 + $field_value = apply_filters( 'srfm_handle_special_block', $field_value, $block_type, $block, $sureforms_blocks, $this );
1276 +
1277 + $form_fields[] = [
1278 + 'field_name' => $field_name,
1279 + 'block_name' => 'multi-choice' === $block_type ? 'srfm-multi' : Helper::get_block_name_from_field( $field_name ),
1280 + 'value' => $field_value,
1281 + 'attributes' => $merged_attributes,
1282 + ];
1283 + }
1284 + }
1285 +
1286 + // Recursively process inner blocks but skip for address blocks.
1287 + if ( isset( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) && ! empty( $block['innerBlocks'] ) && 'address' !== $block_type ) {
1288 + // Pass true if current block has inner blocks and it doesn't need to be duplicated in the main fields array.
1289 + $inner_is_special_block = apply_filters( 'srfm_is_special_block', false, $block_type );
1290 + $this->extract_form_fields( $block['innerBlocks'], $sureforms_blocks, $form_fields, $entry_data, $inner_is_special_block );
1291 + }
1292 + }
1293 + }
1294 +
1295 + /**
1296 + * Get current dropdown counter value.
1297 + *
1298 + * @since 2.0.0
1299 + * @return int Current dropdown counter value.
1300 + */
1301 + public function get_dropdown_counter() {
1302 + return self::$dropdown_counter;
1303 + }
1304 +
1305 + /**
1306 + * Get onboarding user details.
1307 + *
1308 + * @since 2.5.3
1309 + * @return array<string, mixed>
1310 + */
1311 + private function get_onboarding_user_details() {
1312 + $defaults = [
1313 + 'first_name' => '',
1314 + 'last_name' => '',
1315 + 'email' => '',
1316 + 'lead' => false,
1317 + ];
1318 +
1319 + $user_details = Helper::get_srfm_option( $this->onboarding_user_details_key, $defaults );
1320 + if ( ! is_array( $user_details ) ) {
1321 + return $defaults;
1322 + }
1323 +
1324 + return wp_parse_args( $user_details, $defaults );
1325 + }
1326 +
1327 + /**
1328 + * Set onboarding user details.
1329 + *
1330 + * @since 2.5.3
1331 + * @param array<string, mixed> $user_details User details to store.
1332 + * @return void
1333 + */
1334 + private function set_onboarding_user_details( $user_details ) {
1335 + $details = wp_parse_args(
1336 + $user_details,
1337 + [
1338 + 'first_name' => '',
1339 + 'last_name' => '',
1340 + 'email' => '',
1341 + 'lead' => false,
1342 + ]
1343 + );
1344 +
1345 + Helper::update_srfm_option( $this->onboarding_user_details_key, $details );
1346 + }
1347 +
1348 + /**
1349 + * Parse form content and return structured field data with attributes.
1350 + *
1351 + * @param string $form_content The form post content.
1352 + * @param array<mixed> $entry_data The entry form data.
1353 + * @since 2.0.0
1354 + * @return array<string, array<mixed>>
1355 + */
1356 + private function parse_form_fields( $form_content, $entry_data = [] ) {
1357 + if ( empty( $form_content ) ) {
1358 + return [];
1359 + }
1360 +
1361 + // Parse blocks from form content.
1362 + $blocks = parse_blocks( $form_content );
1363 + if ( empty( $blocks ) ) {
1364 + return [];
1365 + }
1366 +
1367 + // Get registered SureForms block attributes.
1368 + $registry = \WP_Block_Type_Registry::get_instance();
1369 + $registered_blocks = $registry->get_all_registered();
1370 +
1371 + $sureforms_blocks = [];
1372 + foreach ( $registered_blocks as $block_name => $block_type ) {
1373 + if ( strpos( $block_name, 'srfm/' ) === 0 && is_array( $block_type->attributes ) ) {
1374 + $block_key = str_replace( 'srfm/', '', $block_name );
1375 + $sureforms_blocks[ $block_key ] = $block_type->attributes;
1376 + }
1377 + }
1378 +
1379 + $form_fields = [];
1380 + $this->extract_form_fields( $blocks, $sureforms_blocks, $form_fields, $entry_data, false );
1381 +
1382 + return $form_fields;
1383 + }
1384 +
1385 + /**
1386 + * Get endpoints
1387 + *
1388 + * @since 0.0.7
1389 + * @return array<array<mixed>>
1390 + */
1391 + private function get_endpoints() {
1392 + /*
1393 + * @internal This filter is used to add custom endpoints.
1394 + * @since 1.2.0
1395 + * @param array<array<mixed>> $endpoints Endpoints.
1396 + */
1397 + return apply_filters(
1398 + 'srfm_rest_api_endpoints',
1399 + [
1400 + 'generate-form' => [
1401 + 'methods' => 'POST',
1402 + 'callback' => [ AI_Form_Builder::get_instance(), 'generate_ai_form' ],
1403 + 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1404 + 'args' => [
1405 + 'use_system_message' => [
1406 + 'sanitize_callback' => [ $this, 'sanitize_boolean_field' ],
1407 + ],
1408 + ],
1409 + ],
1410 + // This route is used to map the AI response to SureForms fields markup.
1411 + 'map-fields' => [
1412 + 'methods' => 'POST',
1413 + 'callback' => [ Field_Mapping::get_instance(), 'generate_gutenberg_fields_from_questions' ],
1414 + 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1415 + ],
1416 + // Recreate the entries table when it has gone missing. The repair is
1417 + // idempotent (CREATE TABLE IF NOT EXISTS) so a double-click is safe.
1418 + 'database/repair-entries-table' => [
1419 + 'methods' => 'POST',
1420 + /**
1421 + * Resolved at dispatch, not while the route table is built:
1422 + * get_endpoints() runs on rest_api_init for every REST request,
1423 + * and Admin is only constructed under is_admin(). Naming the
1424 + * instance here would run Admin's constructor on the front-end
1425 + * submit path too.
1426 + *
1427 + * @param \WP_REST_Request<array<string,mixed>> $request Request.
1428 + * @return \WP_REST_Response|\WP_Error
1429 + */
1430 + 'callback' => static function ( $request ) {
1431 + $nonce = Helper::get_string_value( $request->get_header( 'X-WP-Nonce' ) );
1432 +
1433 + if ( ! wp_verify_nonce( sanitize_text_field( $nonce ), 'wp_rest' ) ) {
1434 + return new \WP_Error(
1435 + 'rest_cookie_invalid_nonce',
1436 + __( 'Security verification failed. Please refresh the page and try again.', 'sureforms' ),
1437 + [ 'status' => 403 ]
1438 + );
1439 + }
1440 +
1441 + // @phpstan-ignore-next-line -- PHPStan resolves SRFM\Admin\Admin via tests/php/stubs/srfm-stubs.php (admin/ is outside its `paths`) and that generated stub predates this method. Real location: admin/admin.php.
1442 + $repaired = \SRFM\Admin\Admin::get_instance()->do_database_repair();
1443 +
1444 + if ( ! $repaired ) {
1445 + return new \WP_Error(
1446 + 'srfm_database_repair_failed',
1447 + __( 'SureForms could not finish updating the database. Your hosting may not allow SureForms to create database tables — please contact your hosting provider or SureForms support.', 'sureforms' ),
1448 + [ 'status' => 500 ]
1449 + );
1450 + }
1451 +
1452 + return new \WP_REST_Response( [ 'success' => true ], 200 );
1453 + },
1454 + 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1455 + ],
1456 + // Record a "Finish setting up" card CTA click for a form (#3031).
1457 + // Per-form capability is re-checked in the handler.
1458 + 'dismiss-form-setup-card' => [
1459 + 'methods' => 'POST',
1460 + /**
1461 + * Resolve Admin at dispatch rather than while the route table is
1462 + * built. get_endpoints() runs on rest_api_init for *every* REST
1463 + * request, and plugin-loader.php only constructs Admin under
1464 + * is_admin() — which REST dispatch is not. Naming the instance
1465 + * here would therefore run Admin's constructor (40 admin hook
1466 + * registrations, an option read, the notices library, and the
1467 + * wpforms_current_user_can filter) on the front-end
1468 + * submit-form path too.
1469 + *
1470 + * @param \WP_REST_Request<array<string,mixed>> $request Request.
1471 + * @return \WP_REST_Response|\WP_Error
1472 + */
1473 + 'callback' => static function ( $request ) {
1474 + // @phpstan-ignore-next-line -- PHPStan resolves SRFM\Admin\Admin via tests/php/stubs/srfm-stubs.php (admin/ is outside its `paths`) and that generated stub predates this method. Real location: admin/admin.php:470.
1475 + return \SRFM\Admin\Admin::get_instance()->dismiss_form_setup_card( $request );
1476 + },
1477 + 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1478 + 'args' => [
1479 + 'form_id' => [
1480 + 'required' => true,
1481 + 'sanitize_callback' => 'absint',
1482 + ],
1483 + 'action' => [
1484 + 'required' => true,
1485 + 'type' => 'string',
1486 + 'enum' => [ 'edit_form', 'edit_thankyou', 'set_up_email', 'view_form' ],
1487 + // Core only enforces `enum` via the default arg sanitizer, which
1488 + // is skipped once a sanitize_callback is set — so pair it with an
1489 + // explicit validate_callback, matching this file's other routes.
1490 + 'validate_callback' => 'rest_validate_request_arg',
1491 + 'sanitize_callback' => 'sanitize_text_field',
1492 + ],
1493 + ],
1494 + ],
1495 + // This route is used to initiate auth process when user tries to authenticate on billing portal.
1496 + 'initiate-auth' => [
1497 + 'methods' => 'GET',
1498 + 'callback' => [ AI_Auth::get_instance(), 'get_auth_url' ],
1499 + 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1500 + ],
1501 + // This route is to used to decrypt the access key and save it in the database.
1502 + 'handle-access-key' => [
1503 + 'methods' => 'POST',
1504 + 'callback' => [ AI_Auth::get_instance(), 'handle_access_key' ],
1505 + 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1506 + ],
1507 + // Public route: returns the visitor's detected country code. Called
1508 + // per-visitor from the phone field so auto-country detection works
1509 + // on full-page-cached sites (the value isn't baked into cached HTML).
1510 + 'geo-country' => [
1511 + 'methods' => 'GET',
1512 + 'callback' => [ $this, 'get_geo_country' ],
1513 + 'permission_callback' => '__return_true',
1514 + ],
1515 + // This route is to get the form submissions for the last 30 days.
1516 + 'entries-chart-data' => [
1517 + 'methods' => 'GET',
1518 + 'callback' => [ $this, 'get_entries_chart_data' ],
1519 + 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1520 + ],
1521 + // This route is to get all forms data.
1522 + 'form-data' => [
1523 + 'methods' => 'GET',
1524 + 'callback' => [ $this, 'get_form_data' ],
1525 + 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1526 + ],
1527 + // Page search endpoint for async admin dropdowns.
1528 + 'pages/search' => [
1529 + 'methods' => 'GET',
1530 + 'callback' => [ $this, 'search_pages' ],
1531 + 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1532 + 'args' => [
1533 + 'search' => [
1534 + 'sanitize_callback' => 'sanitize_text_field',
1535 + 'default' => '',
1536 + ],
1537 + 'page' => [
1538 + 'sanitize_callback' => 'absint',
1539 + 'default' => 1,
1540 + 'validate_callback' => static function ( $value ) {
1541 + return is_numeric( $value ) && (int) $value >= 1;
1542 + },
1543 + ],
1544 + 'per_page' => [
1545 + 'sanitize_callback' => 'absint',
1546 + 'default' => 20,
1547 + 'validate_callback' => static function ( $value ) {
1548 + return is_numeric( $value ) && (int) $value >= 1 && (int) $value <= 50;
1549 + },
1550 + ],
1551 + 'selected_url' => [
1552 + 'sanitize_callback' => 'esc_url_raw',
1553 + 'default' => '',
1554 + 'validate_callback' => static function ( $value ) {
1555 + return empty( $value ) || false !== filter_var( $value, FILTER_VALIDATE_URL );
1556 + },
1557 + ],
1558 + 'selected_urls' => [
1559 + 'default' => [],
1560 + 'sanitize_callback' => static function( $value ) {
1561 + if ( is_array( $value ) ) {
1562 + return array_values( array_filter( array_map( 'esc_url_raw', $value ) ) );
1563 + }
1564 + if ( is_string( $value ) ) {
1565 + return array_values(
1566 + array_filter(
1567 + array_map(
1568 + 'esc_url_raw',
1569 + array_map( 'trim', explode( ',', $value ) )
1570 + )
1571 + )
1572 + );
1573 + }
1574 + return [];
1575 + },
1576 + ],
1577 + ],
1578 + ],
1579 + // Onboarding endpoints.
1580 + 'onboarding/set-status' => [
1581 + 'methods' => 'POST',
1582 + 'callback' => [ $this, 'set_onboarding_status' ],
1583 + 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1584 + ],
1585 + 'onboarding/get-status' => [
1586 + 'methods' => 'GET',
1587 + 'callback' => [ $this, 'get_onboarding_status' ],
1588 + 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1589 + ],
1590 + 'onboarding/user-details' => [
1591 + 'methods' => 'POST',
1592 + 'callback' => [ $this, 'save_onboarding_user_details' ],
1593 + 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1594 + 'args' => [
1595 + 'first_name' => [
1596 + 'required' => true,
1597 + 'sanitize_callback' => 'sanitize_text_field',
1598 + 'validate_callback' => static function( $value ) {
1599 + return is_string( $value ) && '' !== trim( $value );
1600 + },
1601 + ],
1602 + 'last_name' => [
1603 + 'required' => false,
1604 + 'sanitize_callback' => 'sanitize_text_field',
1605 + ],
1606 + 'email' => [
1607 + 'required' => true,
1608 + 'sanitize_callback' => 'sanitize_email',
1609 + 'validate_callback' => static function( $value ) {
1610 + return is_string( $value ) && is_email( $value );
1611 + },
1612 + ],
1613 + ],
1614 + ],
1615 + // Plugin status endpoint.
1616 + 'plugin-status' => [
1617 + 'methods' => 'GET',
1618 + 'callback' => [ $this, 'get_plugin_status' ],
1619 + 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1620 + 'args' => [
1621 + 'plugin' => [
1622 + 'required' => true,
1623 + 'sanitize_callback' => 'sanitize_text_field',
1624 + ],
1625 + ],
1626 + ],
1627 + // Entries endpoints.
1628 + 'entries/list' => [
1629 + 'methods' => 'GET',
1630 + 'callback' => [ $this, 'get_entries_list' ],
1631 + 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1632 + 'args' => [
1633 + 'form_id' => [
1634 + 'sanitize_callback' => 'absint',
1635 + 'default' => 0,
1636 + ],
1637 + 'status' => [
1638 + 'sanitize_callback' => 'sanitize_text_field',
1639 + 'default' => 'all',
1640 + ],
1641 + 'search' => [
1642 + 'sanitize_callback' => 'sanitize_text_field',
1643 + 'default' => '',
1644 + ],
1645 + 'date_from' => [
1646 + 'sanitize_callback' => 'sanitize_text_field',
1647 + 'default' => '',
1648 + ],
1649 + 'date_to' => [
1650 + 'sanitize_callback' => 'sanitize_text_field',
1651 + 'default' => '',
1652 + ],
1653 + 'orderby' => [
1654 + 'type' => 'string',
1655 + 'sanitize_callback' => 'sanitize_text_field',
1656 + 'default' => 'created_at',
1657 + 'enum' => [ 'ID', 'id', 'form_id', 'user_id', 'status', 'type', 'created_at', 'updated_at' ],
1658 + ],
1659 + 'order' => [
1660 + 'type' => 'string',
1661 + 'sanitize_callback' => 'sanitize_text_field',
1662 + 'default' => 'DESC',
1663 + 'enum' => [ 'ASC', 'DESC' ],
1664 + ],
1665 + 'per_page' => [
1666 + 'sanitize_callback' => 'absint',
1667 + 'default' => 20,
1668 + ],
1669 + 'page' => [
1670 + 'sanitize_callback' => 'absint',
1671 + 'default' => 1,
1672 + ],
1673 + ],
1674 + ],
1675 + 'entries/read-status' => [
1676 + 'methods' => 'POST',
1677 + 'callback' => [ $this, 'update_entries_read_status' ],
1678 + 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1679 + 'args' => [
1680 + 'entry_ids' => [
1681 + 'required' => true,
1682 + 'sanitize_callback' => [ $this, 'sanitize_entry_ids' ],
1683 + ],
1684 + 'action' => [
1685 + 'required' => true,
1686 + 'sanitize_callback' => 'sanitize_text_field',
1687 + 'validate_callback' => [ $this, 'validate_read_action' ],
1688 + ],
1689 + ],
1690 + ],
1691 + 'entries/trash' => [
1692 + 'methods' => 'POST',
1693 + 'callback' => [ $this, 'update_entries_trash_status' ],
1694 + 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1695 + 'args' => [
1696 + 'entry_ids' => [
1697 + 'required' => true,
1698 + 'sanitize_callback' => [ $this, 'sanitize_entry_ids' ],
1699 + ],
1700 + 'action' => [
1701 + 'required' => true,
1702 + 'sanitize_callback' => 'sanitize_text_field',
1703 + 'validate_callback' => [ $this, 'validate_trash_action' ],
1704 + ],
1705 + ],
1706 + ],
1707 + 'entries/delete' => [
1708 + 'methods' => 'POST',
1709 + 'callback' => [ $this, 'delete_entries' ],
1710 + 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1711 + 'args' => [
1712 + 'entry_ids' => [
1713 + 'required' => true,
1714 + 'sanitize_callback' => [ $this, 'sanitize_entry_ids' ],
1715 + ],
1716 + ],
1717 + ],
1718 + 'entries/export' => [
1719 + 'methods' => 'POST',
1720 + 'callback' => [ $this, 'export_entries' ],
1721 + 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1722 + 'args' => [
1723 + 'entry_ids' => [
1724 + 'sanitize_callback' => [ $this, 'sanitize_entry_ids' ],
1725 + 'default' => [],
1726 + ],
1727 + 'form_id' => [
1728 + 'sanitize_callback' => 'absint',
1729 + 'default' => 0,
1730 + ],
1731 + 'status' => [
1732 + 'sanitize_callback' => 'sanitize_text_field',
1733 + 'default' => 'all',
1734 + ],
1735 + 'search' => [
1736 + 'sanitize_callback' => 'sanitize_text_field',
1737 + 'default' => '',
1738 + ],
1739 + 'date_from' => [
1740 + 'sanitize_callback' => 'sanitize_text_field',
1741 + 'default' => '',
1742 + ],
1743 + 'date_to' => [
1744 + 'sanitize_callback' => 'sanitize_text_field',
1745 + 'default' => '',
1746 + ],
1747 + ],
1748 + ],
1749 + // Get Single Entry Form Data.
1750 + 'entry/(?P<id>\d+)/details' => [
1751 + 'methods' => 'GET',
1752 + 'callback' => [ $this, 'get_entry_details' ],
1753 + 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1754 + 'args' => [
1755 + 'id' => [
1756 + 'required' => true,
1757 + 'sanitize_callback' => 'absint',
1758 + ],
1759 + ],
1760 + ],
1761 + // Get Single Entry Logs.
1762 + 'entry/(?P<id>\d+)/logs' => [
1763 + 'methods' => 'GET',
1764 + 'callback' => [ $this, 'get_entry_logs' ],
1765 + 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1766 + 'args' => [
1767 + 'id' => [
1768 + 'required' => true,
1769 + 'sanitize_callback' => 'absint',
1770 + ],
1771 + 'per_page' => [
1772 + 'sanitize_callback' => 'absint',
1773 + 'default' => 3,
1774 + ],
1775 + 'page' => [
1776 + 'sanitize_callback' => 'absint',
1777 + 'default' => 1,
1778 + ],
1779 + ],
1780 + ],
1781 + // Forms listing endpoint.
1782 + 'forms' => [
1783 + 'methods' => 'GET',
1784 + 'callback' => [ Forms_Data::get_instance(), 'get_forms_list' ],
1785 + 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1786 + 'args' => [
1787 + 'page' => [
1788 + 'type' => 'integer',
1789 + 'default' => 1,
1790 + 'minimum' => 1,
1791 + ],
1792 + 'per_page' => [
1793 + 'type' => 'integer',
1794 + 'minimum' => 1,
1795 + 'maximum' => 100,
1796 + ],
1797 + 'search' => [
1798 + 'type' => 'string',
1799 + ],
1800 + 'status' => [
1801 + 'type' => 'string',
1802 + 'enum' => [ 'publish', 'draft', 'trash', 'any' ],
1803 + 'default' => 'publish',
1804 + ],
1805 + 'orderby' => [
1806 + 'type' => 'string',
1807 + 'default' => 'date',
1808 + 'enum' => [ 'date', 'id', 'title', 'modified', 'views', 'conversion_rate' ],
1809 + ],
1810 + 'order' => [
1811 + 'type' => 'string',
1812 + 'default' => 'desc',
1813 + 'enum' => [ 'asc', 'desc' ],
1814 + ],
1815 + 'date_from' => [
1816 + 'type' => 'string',
1817 + 'format' => 'date',
1818 + 'sanitize_callback' => 'sanitize_text_field',
1819 + 'validate_callback' => static function( $value ) {
1820 + if ( empty( $value ) ) {
1821 + return true;
1822 + }
1823 + return (bool) strtotime( $value );
1824 + },
1825 + ],
1826 + 'date_to' => [
1827 + 'type' => 'string',
1828 + 'format' => 'date',
1829 + 'sanitize_callback' => 'sanitize_text_field',
1830 + 'validate_callback' => static function( $value ) {
1831 + if ( empty( $value ) ) {
1832 + return true;
1833 + }
1834 + return (bool) strtotime( $value );
1835 + },
1836 + ],
1837 + ],
1838 + ],
1839 + // Export forms endpoint.
1840 + 'forms/export' => [
1841 + 'methods' => 'POST',
1842 + 'callback' => [ Export::get_instance(), 'handle_export_form_rest' ],
1843 + 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1844 + 'args' => [
1845 + 'post_ids' => [
1846 + 'required' => true,
1847 + 'type' => [ 'array', 'string' ],
1848 + 'sanitize_callback' => static function( $value ) {
1849 + if ( is_array( $value ) ) {
1850 + return array_map( 'intval', $value );
1851 + }
1852 + return sanitize_text_field( $value );
1853 + },
1854 + 'validate_callback' => static function( $value ) {
1855 + if ( is_array( $value ) ) {
1856 + return ! empty( $value );
1857 + }
1858 + return ! empty( trim( $value ) );
1859 + },
1860 + ],
1861 + ],
1862 + ],
1863 + // Import forms endpoint.
1864 + 'forms/import' => [
1865 + 'methods' => 'POST',
1866 + 'callback' => [ Export::get_instance(), 'handle_import_form_rest' ],
1867 + 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1868 + 'args' => [
1869 + 'forms_data' => [
1870 + 'required' => true,
1871 + 'type' => 'array',
1872 + 'validate_callback' => static function( $value ) {
1873 + return is_array( $value ) && ! empty( $value );
1874 + },
1875 + ],
1876 + 'default_status' => [
1877 + 'required' => false,
1878 + 'type' => 'string',
1879 + 'default' => 'draft',
1880 + 'enum' => [ 'draft', 'publish', 'private' ],
1881 + 'sanitize_callback' => 'sanitize_text_field',
1882 + ],
1883 + ],
1884 + ],
1885 + // Form lifecycle management endpoint (trash/restore/delete/draft).
1886 + 'forms/manage' => [
1887 + 'methods' => 'POST',
1888 + 'callback' => [ $this, 'manage_form_lifecycle' ],
1889 + 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1890 + 'args' => [
1891 + 'form_ids' => [
1892 + 'required' => true,
1893 + 'type' => [ 'array', 'integer' ],
1894 + 'sanitize_callback' => static function( $value ) {
1895 + if ( is_array( $value ) ) {
1896 + return array_map( 'intval', $value );
1897 + }
1898 + return [ intval( $value ) ];
1899 + },
1900 + 'validate_callback' => static function( $value ) {
1901 + if ( is_array( $value ) ) {
1902 + return ! empty( $value );
1903 + }
1904 + return $value > 0;
1905 + },
1906 + ],
1907 + 'action' => [
1908 + 'required' => true,
1909 + 'type' => 'string',
1910 + 'enum' => [ 'trash', 'restore', 'delete', 'draft' ],
1911 + 'sanitize_callback' => 'sanitize_text_field',
1912 + ],
1913 + ],
1914 + ],
1915 + // Form duplication endpoint.
1916 + 'forms/duplicate' => [
1917 + 'methods' => 'POST',
1918 + 'callback' => [ Duplicate_Form::get_instance(), 'handle_duplicate_form_rest' ],
1919 + 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1920 + 'args' => [
1921 + 'form_id' => [
1922 + 'required' => true,
1923 + 'type' => 'integer',
1924 + 'sanitize_callback' => 'absint',
1925 + 'validate_callback' => static function( $value ) {
1926 + return $value > 0;
1927 + },
1928 + ],
1929 + 'title_suffix' => [
1930 + 'required' => false,
1931 + 'type' => 'string',
1932 + 'default' => __( ' (Copy)', 'sureforms' ),
1933 + 'sanitize_callback' => 'sanitize_text_field',
1934 + ],
1935 + ],
1936 + ],
1937 + ]
1938 + );
157 1939 }
158 1940 }