PluginProbe
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz / 2.11.0
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz v2.11.0
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 0.0.2 0.0.3 All 96 releases
sureforms / inc / rest-api.php

rest-api.php in SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz 2.11.0, at inc/rest-api.php

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