PluginProbe
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz / 2.9.1
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz v2.9.1
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.9.1, at inc/rest-api.php

1,811 lines 55.2 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 ],
858 'user' => $user_info ? [
859 'id' => $user_id,
860 'display_name' => $user_info->display_name,
861 'profile_url' => get_author_posts_url( $user_id ),
862 ] : null,
863 'extras' => $entry['extras'] ?? [],
864 'navigation' => [
865 'previous_entry_id' => $adjacent_entries['previous_id'] ?? null,
866 'next_entry_id' => $adjacent_entries['next_id'] ?? null,
867 ],
868 ];
869
870 return new \WP_REST_Response( $response_data, 200 );
871 }
872
873 /**
874 * Get entry logs with pagination support.
875 *
876 * @param \WP_REST_Request $request Full details about the request.
877 * @since 2.0.0
878 * @return \WP_REST_Response
879 */
880 public function get_entry_logs( $request ) {
881 $nonce = Helper::get_string_value( $request->get_header( 'X-WP-Nonce' ) );
882
883 if ( ! wp_verify_nonce( sanitize_text_field( $nonce ), 'wp_rest' ) ) {
884 return new \WP_REST_Response(
885 [ 'error' => __( 'Security verification failed. Please refresh the page and try again.', 'sureforms' ) ],
886 403
887 );
888 }
889
890 $entry_id = absint( $request->get_param( 'id' ) );
891 $per_page = absint( $request->get_param( 'per_page' ) );
892 $per_page = $per_page ? $per_page : 3;
893 $page = absint( $request->get_param( 'page' ) );
894 $page = $page ? $page : 1;
895
896 if ( empty( $entry_id ) ) {
897 return new \WP_REST_Response(
898 [ 'error' => __( 'Entry ID is required.', 'sureforms' ) ],
899 400
900 );
901 }
902
903 $entry = Entries::get( $entry_id );
904
905 if ( ! $entry ) {
906 return new \WP_REST_Response(
907 [ 'error' => __( 'Entry not found.', 'sureforms' ) ],
908 404
909 );
910 }
911
912 $logs = $entry['logs'] ?? [];
913 $logs = is_array( $logs ) ? $logs : [];
914 $total_logs = count( $logs );
915 $total_pages = ceil( $total_logs / $per_page );
916 $offset = ( $page - 1 ) * $per_page;
917
918 // Paginate logs.
919 $paginated_logs = array_slice( $logs, $offset, $per_page );
920
921 // Format logs with unique IDs for deletion.
922 $formatted_logs = [];
923 foreach ( $paginated_logs as $index => $log ) {
924 if ( ! is_array( $log ) ) {
925 continue;
926 }
927 $formatted_logs[] = [
928 'id' => $offset + $index, // Use offset-based ID for consistent deletion.
929 'title' => $log['title'] ?? '',
930 'timestamp' => $log['timestamp'] ?? time(),
931 'messages' => $log['messages'] ?? [],
932 ];
933 }
934
935 $response_data = [
936 'logs' => $formatted_logs,
937 'current_page' => $page,
938 'per_page' => $per_page,
939 'total' => $total_logs,
940 'total_pages' => $total_pages,
941 ];
942
943 return new \WP_REST_Response( $response_data, 200 );
944 }
945
946 /**
947 * Export entries to CSV or ZIP.
948 *
949 * @param \WP_REST_Request $request Full details about the request.
950 * @since 2.0.0
951 * @return \WP_REST_Response
952 */
953 public function export_entries( $request ) {
954 $nonce = Helper::get_string_value( $request->get_header( 'X-WP-Nonce' ) );
955
956 if ( ! wp_verify_nonce( sanitize_text_field( $nonce ), 'wp_rest' ) ) {
957 return new \WP_REST_Response(
958 [ 'error' => __( 'Security verification failed. Please refresh the page and try again.', 'sureforms' ) ],
959 403
960 );
961 }
962
963 $params = $request->get_params();
964
965 $args = [
966 'entry_ids' => isset( $params['entry_ids'] ) ? $this->sanitize_entry_ids( $params['entry_ids'] ) : [],
967 'form_id' => isset( $params['form_id'] ) ? absint( $params['form_id'] ) : 0,
968 'status' => isset( $params['status'] ) ? sanitize_text_field( $params['status'] ) : 'all',
969 'search' => isset( $params['search'] ) ? sanitize_text_field( $params['search'] ) : '',
970 'date_from' => isset( $params['date_from'] ) ? sanitize_text_field( $params['date_from'] ) : '',
971 'date_to' => isset( $params['date_to'] ) ? sanitize_text_field( $params['date_to'] ) : '',
972 ];
973
974 /**
975 * Export result with success status and either error message or file details.
976 *
977 * @var array{success: false, error: string} | array{success: true, filename: string, filepath: string, type: string} $result
978 */
979 $result = Entries_Class::export_entries( $args );
980
981 if ( ! $result['success'] ) {
982 return new \WP_REST_Response(
983 [ 'error' => $result['error'] ],
984 400
985 );
986 }
987
988 // Return file information for download.
989 $filepath = Helper::get_string_value( $result['filepath'] );
990 return new \WP_REST_Response(
991 [
992 'success' => true,
993 'filename' => $result['filename'],
994 'filepath' => $result['filepath'],
995 'type' => $result['type'],
996 'download_url' => add_query_arg(
997 '_wpnonce',
998 wp_create_nonce( 'srfm_download_export' ),
999 admin_url( 'admin-ajax.php?action=srfm_download_export&file=' . rawurlencode( basename( $filepath ) ) )
1000 ),
1001 ],
1002 200
1003 );
1004 }
1005 /**
1006 * Manage form lifecycle operations (trash, restore, delete).
1007 *
1008 * @param \WP_REST_Request $request Full details about the request.
1009 * @since 2.0.0
1010 * @return \WP_REST_Response|\WP_Error
1011 */
1012 public function manage_form_lifecycle( $request ) {
1013 $nonce = Helper::get_string_value( $request->get_header( 'X-WP-Nonce' ) );
1014
1015 if ( ! wp_verify_nonce( sanitize_text_field( $nonce ), 'wp_rest' ) ) {
1016 return new \WP_Error(
1017 'invalid_nonce',
1018 __( 'Security verification failed. Please refresh the page and try again.', 'sureforms' ),
1019 [ 'status' => 403 ]
1020 );
1021 }
1022
1023 $params = $request->get_params();
1024 $form_ids = isset( $params['form_ids'] ) && is_array( $params['form_ids'] ) ?
1025 array_map( 'intval', $params['form_ids'] ) :
1026 [ intval( $params['form_ids'] ) ];
1027 $action = isset( $params['action'] ) ? sanitize_text_field( Helper::get_string_value( $params['action'] ) ) : '';
1028
1029 if ( empty( $form_ids ) || empty( $action ) ) {
1030 return new \WP_Error(
1031 'missing_parameters',
1032 __( 'Select at least one form and specify an action.', 'sureforms' ),
1033 [ 'status' => 400 ]
1034 );
1035 }
1036
1037 $results = [];
1038 $errors = [];
1039
1040 foreach ( $form_ids as $form_id ) {
1041 $post = get_post( $form_id );
1042
1043 // Validate that the post exists and is a sureforms_form.
1044 if ( ! $post || 'sureforms_form' !== $post->post_type ) {
1045 $errors[] = [
1046 'form_id' => $form_id,
1047 'error' => __( 'Form not found or is not a valid form type.', 'sureforms' ),
1048 ];
1049 continue;
1050 }
1051
1052 $result = false;
1053
1054 switch ( $action ) {
1055 case 'trash':
1056 if ( 'trash' === $post->post_status ) {
1057 $errors[] = [
1058 'form_id' => $form_id,
1059 'error' => __( 'This form is already in the trash.', 'sureforms' ),
1060 ];
1061 } else {
1062 $result = wp_trash_post( $form_id );
1063 }
1064 break;
1065
1066 case 'restore':
1067 if ( 'trash' !== $post->post_status ) {
1068 $errors[] = [
1069 'form_id' => $form_id,
1070 'error' => __( 'This form is not in the trash.', 'sureforms' ),
1071 ];
1072 } else {
1073 $result = wp_untrash_post( $form_id );
1074 }
1075 break;
1076
1077 case 'delete':
1078 // Force delete permanently.
1079 $result = wp_delete_post( $form_id, true );
1080 break;
1081
1082 case 'draft':
1083 if ( 'trash' === $post->post_status ) {
1084 $errors[] = [
1085 'form_id' => $form_id,
1086 'error' => __( 'Use the restore action to recover a trashed form before switching it to draft.', 'sureforms' ),
1087 ];
1088 } elseif ( 'draft' === $post->post_status ) {
1089 $errors[] = [
1090 'form_id' => $form_id,
1091 'error' => __( 'This form is already a draft.', 'sureforms' ),
1092 ];
1093 } else {
1094 $result = wp_update_post(
1095 [
1096 'ID' => $form_id,
1097 'post_status' => 'draft',
1098 ]
1099 );
1100 }
1101 break;
1102
1103 default:
1104 $errors[] = [
1105 'form_id' => $form_id,
1106 'error' => __( 'Invalid action.', 'sureforms' ),
1107 ];
1108 break;
1109 }
1110
1111 if ( $result ) {
1112 $results[] = [
1113 'form_id' => $form_id,
1114 'action' => $action,
1115 'success' => true,
1116 ];
1117 } elseif ( ! isset( $errors[ array_search( $form_id, array_column( $errors, 'form_id' ), true ) ] ) ) {
1118 $errors[] = [
1119 'form_id' => $form_id,
1120 /* translators: %s: action name */
1121 'error' => sprintf( __( 'Failed to %s this form. Please try again.', 'sureforms' ), $action ),
1122 ];
1123 }
1124 }
1125
1126 $response_data = [
1127 'success' => ! empty( $results ),
1128 'action' => $action,
1129 'processed_ids' => array_column( $results, 'form_id' ),
1130 'success_count' => count( $results ),
1131 'results' => $results,
1132 ];
1133
1134 if ( ! empty( $errors ) ) {
1135 $response_data['errors'] = $errors;
1136 $response_data['error_count'] = count( $errors );
1137 }
1138
1139 return new \WP_REST_Response( $response_data );
1140 }
1141
1142 /**
1143 * Recursively extract form fields from blocks.
1144 *
1145 * @param array<mixed> $blocks The blocks array.
1146 * @param array<string, array<mixed>> $sureforms_blocks Registered SureForms block attributes.
1147 * @param array<string, array<mixed>> &$form_fields Reference to form fields array.
1148 * @param array<mixed> $entry_data The entry form data.
1149 * @param bool $is_special_block Whether the current block is a special block (like address).
1150 * @param int|null $base_counter Base counter for unique field naming.
1151 * @since 2.0.0
1152 * @return void
1153 */
1154 public function extract_form_fields( $blocks, $sureforms_blocks, &$form_fields, $entry_data = [], $is_special_block = false, $base_counter = null ) {
1155 if ( null !== $base_counter ) {
1156 self::$dropdown_counter = $base_counter;
1157 }
1158 $block_type = '';
1159
1160 foreach ( $blocks as $block ) {
1161 if ( ! is_array( $block ) || ! isset( $block['blockName'] ) || ! is_string( $block['blockName'] ) ) {
1162 continue;
1163 }
1164
1165 // Check if it's a SureForms block.
1166 if ( strpos( $block['blockName'], 'srfm/' ) === 0 ) {
1167 $block_type = str_replace( 'srfm/', '', $block['blockName'] );
1168 // Skip inline button or fields inside nested blocks except address.
1169 if ( 'inline-button' === $block_type || ( $is_special_block && 'address' !== $block_type ) ) {
1170 continue;
1171 }
1172
1173 if ( isset( $sureforms_blocks[ $block_type ] ) && is_array( $sureforms_blocks[ $block_type ] ) ) {
1174 $block_attributes = isset( $block['attrs'] ) && is_array( $block['attrs'] ) ? $block['attrs'] : [];
1175 $default_attributes = $sureforms_blocks[ $block_type ];
1176
1177 // Merge block instance attributes with defaults.
1178 $merged_attributes = [];
1179 foreach ( $default_attributes as $attr_name => $attr_config ) {
1180 if ( ! is_string( $attr_name ) ) {
1181 continue;
1182 }
1183 $default_value = null;
1184 if ( is_array( $attr_config ) && isset( $attr_config['default'] ) ) {
1185 $default_value = $attr_config['default'];
1186 }
1187 $merged_attributes[ $attr_name ] = $block_attributes[ $attr_name ] ?? $default_value;
1188 }
1189
1190 // Generate field name.
1191 $label = $merged_attributes['label'] ?? '';
1192 $label = is_string( $label ) ? $label : '';
1193 $slug = $merged_attributes['slug'] ?? '';
1194 $slug = is_string( $slug ) ? $slug : '';
1195 $block_id = $merged_attributes['block_id'] ?? '';
1196 $block_id = is_string( $block_id ) ? $block_id : '';
1197 $field_name = '';
1198 $base_field_name = '';
1199
1200 if ( ! empty( $label ) && ! empty( $slug ) && ! empty( $block_id ) ) {
1201 $input_label = '-lbl-' . Helper::encrypt( $label );
1202 $base_field_name = $input_label . '-' . $slug;
1203
1204 // Handle special case for dropdown with instance counter.
1205 if ( 'dropdown' === $block_type ) {
1206 self::$dropdown_counter++;
1207 $unique_slug = $block_type . '-' . self::$dropdown_counter;
1208 $field_name = 'srfm-' . $unique_slug . '-' . $block_id . $base_field_name;
1209 } elseif ( 'multi-choice' === $block_type ) {
1210 // Multi-choice uses standard pattern.
1211 $field_name = 'srfm-input-' . $block_type . '-' . $block_id . $base_field_name;
1212 } else {
1213 // Standard field name for other blocks.
1214 $field_name = 'srfm-' . $block_type . '-' . $block_id . $base_field_name;
1215 }
1216 }
1217
1218 // Allow pro plugin to modify field_name.
1219 $field_name = apply_filters( 'srfm_extract_form_fields_field_name', $field_name, $base_field_name, $block_type, $block_id );
1220
1221 // Get the value from entry data or use default.
1222 $field_value = $entry_data[ $field_name ] ?? ( $merged_attributes['defaultValue'] ?? '' );
1223
1224 // Special handling for address blocks - extract inner fields.
1225 if ( 'address' === $block_type && isset( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) {
1226 $inner_fields = [];
1227 $this->extract_form_fields( $block['innerBlocks'], $sureforms_blocks, $inner_fields, $entry_data, false );
1228 $field_value = $inner_fields;
1229 }
1230
1231 // Allow plugins to handle special blocks.
1232 $field_value = apply_filters( 'srfm_handle_special_block', $field_value, $block_type, $block, $sureforms_blocks, $this );
1233
1234 $form_fields[] = [
1235 'field_name' => $field_name,
1236 'block_name' => 'multi-choice' === $block_type ? 'srfm-multi' : Helper::get_block_name_from_field( $field_name ),
1237 'value' => $field_value,
1238 'attributes' => $merged_attributes,
1239 ];
1240 }
1241 }
1242
1243 // Recursively process inner blocks but skip for address blocks.
1244 if ( isset( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) && ! empty( $block['innerBlocks'] ) && 'address' !== $block_type ) {
1245 // Pass true if current block has inner blocks and it doesn't need to be duplicated in the main fields array.
1246 $inner_is_special_block = apply_filters( 'srfm_is_special_block', false, $block_type );
1247 $this->extract_form_fields( $block['innerBlocks'], $sureforms_blocks, $form_fields, $entry_data, $inner_is_special_block );
1248 }
1249 }
1250 }
1251
1252 /**
1253 * Get current dropdown counter value.
1254 *
1255 * @since 2.0.0
1256 * @return int Current dropdown counter value.
1257 */
1258 public function get_dropdown_counter() {
1259 return self::$dropdown_counter;
1260 }
1261
1262 /**
1263 * Get onboarding user details.
1264 *
1265 * @since 2.5.3
1266 * @return array<string, mixed>
1267 */
1268 private function get_onboarding_user_details() {
1269 $defaults = [
1270 'first_name' => '',
1271 'last_name' => '',
1272 'email' => '',
1273 'lead' => false,
1274 ];
1275
1276 $user_details = Helper::get_srfm_option( $this->onboarding_user_details_key, $defaults );
1277 if ( ! is_array( $user_details ) ) {
1278 return $defaults;
1279 }
1280
1281 return wp_parse_args( $user_details, $defaults );
1282 }
1283
1284 /**
1285 * Set onboarding user details.
1286 *
1287 * @since 2.5.3
1288 * @param array<string, mixed> $user_details User details to store.
1289 * @return void
1290 */
1291 private function set_onboarding_user_details( $user_details ) {
1292 $details = wp_parse_args(
1293 $user_details,
1294 [
1295 'first_name' => '',
1296 'last_name' => '',
1297 'email' => '',
1298 'lead' => false,
1299 ]
1300 );
1301
1302 Helper::update_srfm_option( $this->onboarding_user_details_key, $details );
1303 }
1304
1305 /**
1306 * Parse form content and return structured field data with attributes.
1307 *
1308 * @param string $form_content The form post content.
1309 * @param array<mixed> $entry_data The entry form data.
1310 * @since 2.0.0
1311 * @return array<string, array<mixed>>
1312 */
1313 private function parse_form_fields( $form_content, $entry_data = [] ) {
1314 if ( empty( $form_content ) ) {
1315 return [];
1316 }
1317
1318 // Parse blocks from form content.
1319 $blocks = parse_blocks( $form_content );
1320 if ( empty( $blocks ) ) {
1321 return [];
1322 }
1323
1324 // Get registered SureForms block attributes.
1325 $registry = \WP_Block_Type_Registry::get_instance();
1326 $registered_blocks = $registry->get_all_registered();
1327
1328 $sureforms_blocks = [];
1329 foreach ( $registered_blocks as $block_name => $block_type ) {
1330 if ( strpos( $block_name, 'srfm/' ) === 0 && is_array( $block_type->attributes ) ) {
1331 $block_key = str_replace( 'srfm/', '', $block_name );
1332 $sureforms_blocks[ $block_key ] = $block_type->attributes;
1333 }
1334 }
1335
1336 $form_fields = [];
1337 $this->extract_form_fields( $blocks, $sureforms_blocks, $form_fields, $entry_data, false );
1338
1339 return $form_fields;
1340 }
1341
1342 /**
1343 * Get endpoints
1344 *
1345 * @since 0.0.7
1346 * @return array<array<mixed>>
1347 */
1348 private function get_endpoints() {
1349 /*
1350 * @internal This filter is used to add custom endpoints.
1351 * @since 1.2.0
1352 * @param array<array<mixed>> $endpoints Endpoints.
1353 */
1354 return apply_filters(
1355 'srfm_rest_api_endpoints',
1356 [
1357 'generate-form' => [
1358 'methods' => 'POST',
1359 'callback' => [ AI_Form_Builder::get_instance(), 'generate_ai_form' ],
1360 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1361 'args' => [
1362 'use_system_message' => [
1363 'sanitize_callback' => [ $this, 'sanitize_boolean_field' ],
1364 ],
1365 ],
1366 ],
1367 // This route is used to map the AI response to SureForms fields markup.
1368 'map-fields' => [
1369 'methods' => 'POST',
1370 'callback' => [ Field_Mapping::get_instance(), 'generate_gutenberg_fields_from_questions' ],
1371 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1372 ],
1373 // This route is used to initiate auth process when user tries to authenticate on billing portal.
1374 'initiate-auth' => [
1375 'methods' => 'GET',
1376 'callback' => [ AI_Auth::get_instance(), 'get_auth_url' ],
1377 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1378 ],
1379 // This route is to used to decrypt the access key and save it in the database.
1380 'handle-access-key' => [
1381 'methods' => 'POST',
1382 'callback' => [ AI_Auth::get_instance(), 'handle_access_key' ],
1383 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1384 ],
1385 // This route is to get the form submissions for the last 30 days.
1386 'entries-chart-data' => [
1387 'methods' => 'GET',
1388 'callback' => [ $this, 'get_entries_chart_data' ],
1389 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1390 ],
1391 // This route is to get all forms data.
1392 'form-data' => [
1393 'methods' => 'GET',
1394 'callback' => [ $this, 'get_form_data' ],
1395 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1396 ],
1397 // Page search endpoint for async admin dropdowns.
1398 'pages/search' => [
1399 'methods' => 'GET',
1400 'callback' => [ $this, 'search_pages' ],
1401 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1402 'args' => [
1403 'search' => [
1404 'sanitize_callback' => 'sanitize_text_field',
1405 'default' => '',
1406 ],
1407 'page' => [
1408 'sanitize_callback' => 'absint',
1409 'default' => 1,
1410 'validate_callback' => static function ( $value ) {
1411 return is_numeric( $value ) && (int) $value >= 1;
1412 },
1413 ],
1414 'per_page' => [
1415 'sanitize_callback' => 'absint',
1416 'default' => 20,
1417 'validate_callback' => static function ( $value ) {
1418 return is_numeric( $value ) && (int) $value >= 1 && (int) $value <= 50;
1419 },
1420 ],
1421 'selected_url' => [
1422 'sanitize_callback' => 'esc_url_raw',
1423 'default' => '',
1424 'validate_callback' => static function ( $value ) {
1425 return empty( $value ) || false !== filter_var( $value, FILTER_VALIDATE_URL );
1426 },
1427 ],
1428 'selected_urls' => [
1429 'default' => [],
1430 'sanitize_callback' => static function( $value ) {
1431 if ( is_array( $value ) ) {
1432 return array_values( array_filter( array_map( 'esc_url_raw', $value ) ) );
1433 }
1434 if ( is_string( $value ) ) {
1435 return array_values(
1436 array_filter(
1437 array_map(
1438 'esc_url_raw',
1439 array_map( 'trim', explode( ',', $value ) )
1440 )
1441 )
1442 );
1443 }
1444 return [];
1445 },
1446 ],
1447 ],
1448 ],
1449 // Onboarding endpoints.
1450 'onboarding/set-status' => [
1451 'methods' => 'POST',
1452 'callback' => [ $this, 'set_onboarding_status' ],
1453 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1454 ],
1455 'onboarding/get-status' => [
1456 'methods' => 'GET',
1457 'callback' => [ $this, 'get_onboarding_status' ],
1458 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1459 ],
1460 'onboarding/user-details' => [
1461 'methods' => 'POST',
1462 'callback' => [ $this, 'save_onboarding_user_details' ],
1463 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1464 'args' => [
1465 'first_name' => [
1466 'required' => true,
1467 'sanitize_callback' => 'sanitize_text_field',
1468 'validate_callback' => static function( $value ) {
1469 return is_string( $value ) && '' !== trim( $value );
1470 },
1471 ],
1472 'last_name' => [
1473 'required' => false,
1474 'sanitize_callback' => 'sanitize_text_field',
1475 ],
1476 'email' => [
1477 'required' => true,
1478 'sanitize_callback' => 'sanitize_email',
1479 'validate_callback' => static function( $value ) {
1480 return is_string( $value ) && is_email( $value );
1481 },
1482 ],
1483 ],
1484 ],
1485 // Plugin status endpoint.
1486 'plugin-status' => [
1487 'methods' => 'GET',
1488 'callback' => [ $this, 'get_plugin_status' ],
1489 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1490 'args' => [
1491 'plugin' => [
1492 'required' => true,
1493 'sanitize_callback' => 'sanitize_text_field',
1494 ],
1495 ],
1496 ],
1497 // Entries endpoints.
1498 'entries/list' => [
1499 'methods' => 'GET',
1500 'callback' => [ $this, 'get_entries_list' ],
1501 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1502 'args' => [
1503 'form_id' => [
1504 'sanitize_callback' => 'absint',
1505 'default' => 0,
1506 ],
1507 'status' => [
1508 'sanitize_callback' => 'sanitize_text_field',
1509 'default' => 'all',
1510 ],
1511 'search' => [
1512 'sanitize_callback' => 'sanitize_text_field',
1513 'default' => '',
1514 ],
1515 'date_from' => [
1516 'sanitize_callback' => 'sanitize_text_field',
1517 'default' => '',
1518 ],
1519 'date_to' => [
1520 'sanitize_callback' => 'sanitize_text_field',
1521 'default' => '',
1522 ],
1523 'orderby' => [
1524 'type' => 'string',
1525 'sanitize_callback' => 'sanitize_text_field',
1526 'default' => 'created_at',
1527 'enum' => [ 'ID', 'id', 'form_id', 'user_id', 'status', 'type', 'created_at', 'updated_at' ],
1528 ],
1529 'order' => [
1530 'type' => 'string',
1531 'sanitize_callback' => 'sanitize_text_field',
1532 'default' => 'DESC',
1533 'enum' => [ 'ASC', 'DESC' ],
1534 ],
1535 'per_page' => [
1536 'sanitize_callback' => 'absint',
1537 'default' => 20,
1538 ],
1539 'page' => [
1540 'sanitize_callback' => 'absint',
1541 'default' => 1,
1542 ],
1543 ],
1544 ],
1545 'entries/read-status' => [
1546 'methods' => 'POST',
1547 'callback' => [ $this, 'update_entries_read_status' ],
1548 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1549 'args' => [
1550 'entry_ids' => [
1551 'required' => true,
1552 'sanitize_callback' => [ $this, 'sanitize_entry_ids' ],
1553 ],
1554 'action' => [
1555 'required' => true,
1556 'sanitize_callback' => 'sanitize_text_field',
1557 'validate_callback' => [ $this, 'validate_read_action' ],
1558 ],
1559 ],
1560 ],
1561 'entries/trash' => [
1562 'methods' => 'POST',
1563 'callback' => [ $this, 'update_entries_trash_status' ],
1564 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1565 'args' => [
1566 'entry_ids' => [
1567 'required' => true,
1568 'sanitize_callback' => [ $this, 'sanitize_entry_ids' ],
1569 ],
1570 'action' => [
1571 'required' => true,
1572 'sanitize_callback' => 'sanitize_text_field',
1573 'validate_callback' => [ $this, 'validate_trash_action' ],
1574 ],
1575 ],
1576 ],
1577 'entries/delete' => [
1578 'methods' => 'POST',
1579 'callback' => [ $this, 'delete_entries' ],
1580 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1581 'args' => [
1582 'entry_ids' => [
1583 'required' => true,
1584 'sanitize_callback' => [ $this, 'sanitize_entry_ids' ],
1585 ],
1586 ],
1587 ],
1588 'entries/export' => [
1589 'methods' => 'POST',
1590 'callback' => [ $this, 'export_entries' ],
1591 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1592 'args' => [
1593 'entry_ids' => [
1594 'sanitize_callback' => [ $this, 'sanitize_entry_ids' ],
1595 'default' => [],
1596 ],
1597 'form_id' => [
1598 'sanitize_callback' => 'absint',
1599 'default' => 0,
1600 ],
1601 'status' => [
1602 'sanitize_callback' => 'sanitize_text_field',
1603 'default' => 'all',
1604 ],
1605 'search' => [
1606 'sanitize_callback' => 'sanitize_text_field',
1607 'default' => '',
1608 ],
1609 'date_from' => [
1610 'sanitize_callback' => 'sanitize_text_field',
1611 'default' => '',
1612 ],
1613 'date_to' => [
1614 'sanitize_callback' => 'sanitize_text_field',
1615 'default' => '',
1616 ],
1617 ],
1618 ],
1619 // Get Single Entry Form Data.
1620 'entry/(?P<id>\d+)/details' => [
1621 'methods' => 'GET',
1622 'callback' => [ $this, 'get_entry_details' ],
1623 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1624 'args' => [
1625 'id' => [
1626 'required' => true,
1627 'sanitize_callback' => 'absint',
1628 ],
1629 ],
1630 ],
1631 // Get Single Entry Logs.
1632 'entry/(?P<id>\d+)/logs' => [
1633 'methods' => 'GET',
1634 'callback' => [ $this, 'get_entry_logs' ],
1635 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1636 'args' => [
1637 'id' => [
1638 'required' => true,
1639 'sanitize_callback' => 'absint',
1640 ],
1641 'per_page' => [
1642 'sanitize_callback' => 'absint',
1643 'default' => 3,
1644 ],
1645 'page' => [
1646 'sanitize_callback' => 'absint',
1647 'default' => 1,
1648 ],
1649 ],
1650 ],
1651 // Forms listing endpoint.
1652 'forms' => [
1653 'methods' => 'GET',
1654 'callback' => [ Forms_Data::get_instance(), 'get_forms_list' ],
1655 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1656 'args' => [
1657 'page' => [
1658 'type' => 'integer',
1659 'default' => 1,
1660 'minimum' => 1,
1661 ],
1662 'per_page' => [
1663 'type' => 'integer',
1664 'minimum' => 1,
1665 'maximum' => 100,
1666 ],
1667 'search' => [
1668 'type' => 'string',
1669 ],
1670 'status' => [
1671 'type' => 'string',
1672 'enum' => [ 'publish', 'draft', 'trash', 'any' ],
1673 'default' => 'publish',
1674 ],
1675 'orderby' => [
1676 'type' => 'string',
1677 'default' => 'date',
1678 'enum' => [ 'date', 'id', 'title', 'modified' ],
1679 ],
1680 'order' => [
1681 'type' => 'string',
1682 'default' => 'desc',
1683 'enum' => [ 'asc', 'desc' ],
1684 ],
1685 'date_from' => [
1686 'type' => 'string',
1687 'format' => 'date',
1688 'sanitize_callback' => 'sanitize_text_field',
1689 'validate_callback' => static function( $value ) {
1690 if ( empty( $value ) ) {
1691 return true;
1692 }
1693 return (bool) strtotime( $value );
1694 },
1695 ],
1696 'date_to' => [
1697 'type' => 'string',
1698 'format' => 'date',
1699 'sanitize_callback' => 'sanitize_text_field',
1700 'validate_callback' => static function( $value ) {
1701 if ( empty( $value ) ) {
1702 return true;
1703 }
1704 return (bool) strtotime( $value );
1705 },
1706 ],
1707 ],
1708 ],
1709 // Export forms endpoint.
1710 'forms/export' => [
1711 'methods' => 'POST',
1712 'callback' => [ Export::get_instance(), 'handle_export_form_rest' ],
1713 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1714 'args' => [
1715 'post_ids' => [
1716 'required' => true,
1717 'type' => [ 'array', 'string' ],
1718 'sanitize_callback' => static function( $value ) {
1719 if ( is_array( $value ) ) {
1720 return array_map( 'intval', $value );
1721 }
1722 return sanitize_text_field( $value );
1723 },
1724 'validate_callback' => static function( $value ) {
1725 if ( is_array( $value ) ) {
1726 return ! empty( $value );
1727 }
1728 return ! empty( trim( $value ) );
1729 },
1730 ],
1731 ],
1732 ],
1733 // Import forms endpoint.
1734 'forms/import' => [
1735 'methods' => 'POST',
1736 'callback' => [ Export::get_instance(), 'handle_import_form_rest' ],
1737 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1738 'args' => [
1739 'forms_data' => [
1740 'required' => true,
1741 'type' => 'array',
1742 'validate_callback' => static function( $value ) {
1743 return is_array( $value ) && ! empty( $value );
1744 },
1745 ],
1746 'default_status' => [
1747 'required' => false,
1748 'type' => 'string',
1749 'default' => 'draft',
1750 'enum' => [ 'draft', 'publish', 'private' ],
1751 'sanitize_callback' => 'sanitize_text_field',
1752 ],
1753 ],
1754 ],
1755 // Form lifecycle management endpoint (trash/restore/delete/draft).
1756 'forms/manage' => [
1757 'methods' => 'POST',
1758 'callback' => [ $this, 'manage_form_lifecycle' ],
1759 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1760 'args' => [
1761 'form_ids' => [
1762 'required' => true,
1763 'type' => [ 'array', 'integer' ],
1764 'sanitize_callback' => static function( $value ) {
1765 if ( is_array( $value ) ) {
1766 return array_map( 'intval', $value );
1767 }
1768 return [ intval( $value ) ];
1769 },
1770 'validate_callback' => static function( $value ) {
1771 if ( is_array( $value ) ) {
1772 return ! empty( $value );
1773 }
1774 return $value > 0;
1775 },
1776 ],
1777 'action' => [
1778 'required' => true,
1779 'type' => 'string',
1780 'enum' => [ 'trash', 'restore', 'delete', 'draft' ],
1781 'sanitize_callback' => 'sanitize_text_field',
1782 ],
1783 ],
1784 ],
1785 // Form duplication endpoint.
1786 'forms/duplicate' => [
1787 'methods' => 'POST',
1788 'callback' => [ Duplicate_Form::get_instance(), 'handle_duplicate_form_rest' ],
1789 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1790 'args' => [
1791 'form_id' => [
1792 'required' => true,
1793 'type' => 'integer',
1794 'sanitize_callback' => 'absint',
1795 'validate_callback' => static function( $value ) {
1796 return $value > 0;
1797 },
1798 ],
1799 'title_suffix' => [
1800 'required' => false,
1801 'type' => 'string',
1802 'default' => __( ' (Copy)', 'sureforms' ),
1803 'sanitize_callback' => 'sanitize_text_field',
1804 ],
1805 ],
1806 ],
1807 ]
1808 );
1809 }
1810 }
1811