PluginProbe
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz / 2.8.1
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz v2.8.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
1,790 lines 54.6 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 default:
1083 $errors[] = [
1084 'form_id' => $form_id,
1085 'error' => __( 'Invalid action.', 'sureforms' ),
1086 ];
1087 break;
1088 }
1089
1090 if ( $result ) {
1091 $results[] = [
1092 'form_id' => $form_id,
1093 'action' => $action,
1094 'success' => true,
1095 ];
1096 } elseif ( ! isset( $errors[ array_search( $form_id, array_column( $errors, 'form_id' ), true ) ] ) ) {
1097 $errors[] = [
1098 'form_id' => $form_id,
1099 /* translators: %s: action name */
1100 'error' => sprintf( __( 'Failed to %s this form. Please try again.', 'sureforms' ), $action ),
1101 ];
1102 }
1103 }
1104
1105 $response_data = [
1106 'success' => ! empty( $results ),
1107 'action' => $action,
1108 'processed_ids' => array_column( $results, 'form_id' ),
1109 'success_count' => count( $results ),
1110 'results' => $results,
1111 ];
1112
1113 if ( ! empty( $errors ) ) {
1114 $response_data['errors'] = $errors;
1115 $response_data['error_count'] = count( $errors );
1116 }
1117
1118 return new \WP_REST_Response( $response_data );
1119 }
1120
1121 /**
1122 * Recursively extract form fields from blocks.
1123 *
1124 * @param array<mixed> $blocks The blocks array.
1125 * @param array<string, array<mixed>> $sureforms_blocks Registered SureForms block attributes.
1126 * @param array<string, array<mixed>> &$form_fields Reference to form fields array.
1127 * @param array<mixed> $entry_data The entry form data.
1128 * @param bool $is_special_block Whether the current block is a special block (like address).
1129 * @param int|null $base_counter Base counter for unique field naming.
1130 * @since 2.0.0
1131 * @return void
1132 */
1133 public function extract_form_fields( $blocks, $sureforms_blocks, &$form_fields, $entry_data = [], $is_special_block = false, $base_counter = null ) {
1134 if ( null !== $base_counter ) {
1135 self::$dropdown_counter = $base_counter;
1136 }
1137 $block_type = '';
1138
1139 foreach ( $blocks as $block ) {
1140 if ( ! is_array( $block ) || ! isset( $block['blockName'] ) || ! is_string( $block['blockName'] ) ) {
1141 continue;
1142 }
1143
1144 // Check if it's a SureForms block.
1145 if ( strpos( $block['blockName'], 'srfm/' ) === 0 ) {
1146 $block_type = str_replace( 'srfm/', '', $block['blockName'] );
1147 // Skip inline button or fields inside nested blocks except address.
1148 if ( 'inline-button' === $block_type || ( $is_special_block && 'address' !== $block_type ) ) {
1149 continue;
1150 }
1151
1152 if ( isset( $sureforms_blocks[ $block_type ] ) && is_array( $sureforms_blocks[ $block_type ] ) ) {
1153 $block_attributes = isset( $block['attrs'] ) && is_array( $block['attrs'] ) ? $block['attrs'] : [];
1154 $default_attributes = $sureforms_blocks[ $block_type ];
1155
1156 // Merge block instance attributes with defaults.
1157 $merged_attributes = [];
1158 foreach ( $default_attributes as $attr_name => $attr_config ) {
1159 if ( ! is_string( $attr_name ) ) {
1160 continue;
1161 }
1162 $default_value = null;
1163 if ( is_array( $attr_config ) && isset( $attr_config['default'] ) ) {
1164 $default_value = $attr_config['default'];
1165 }
1166 $merged_attributes[ $attr_name ] = $block_attributes[ $attr_name ] ?? $default_value;
1167 }
1168
1169 // Generate field name.
1170 $label = $merged_attributes['label'] ?? '';
1171 $label = is_string( $label ) ? $label : '';
1172 $slug = $merged_attributes['slug'] ?? '';
1173 $slug = is_string( $slug ) ? $slug : '';
1174 $block_id = $merged_attributes['block_id'] ?? '';
1175 $block_id = is_string( $block_id ) ? $block_id : '';
1176 $field_name = '';
1177 $base_field_name = '';
1178
1179 if ( ! empty( $label ) && ! empty( $slug ) && ! empty( $block_id ) ) {
1180 $input_label = '-lbl-' . Helper::encrypt( $label );
1181 $base_field_name = $input_label . '-' . $slug;
1182
1183 // Handle special case for dropdown with instance counter.
1184 if ( 'dropdown' === $block_type ) {
1185 self::$dropdown_counter++;
1186 $unique_slug = $block_type . '-' . self::$dropdown_counter;
1187 $field_name = 'srfm-' . $unique_slug . '-' . $block_id . $base_field_name;
1188 } elseif ( 'multi-choice' === $block_type ) {
1189 // Multi-choice uses standard pattern.
1190 $field_name = 'srfm-input-' . $block_type . '-' . $block_id . $base_field_name;
1191 } else {
1192 // Standard field name for other blocks.
1193 $field_name = 'srfm-' . $block_type . '-' . $block_id . $base_field_name;
1194 }
1195 }
1196
1197 // Allow pro plugin to modify field_name.
1198 $field_name = apply_filters( 'srfm_extract_form_fields_field_name', $field_name, $base_field_name, $block_type, $block_id );
1199
1200 // Get the value from entry data or use default.
1201 $field_value = $entry_data[ $field_name ] ?? ( $merged_attributes['defaultValue'] ?? '' );
1202
1203 // Special handling for address blocks - extract inner fields.
1204 if ( 'address' === $block_type && isset( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) {
1205 $inner_fields = [];
1206 $this->extract_form_fields( $block['innerBlocks'], $sureforms_blocks, $inner_fields, $entry_data, false );
1207 $field_value = $inner_fields;
1208 }
1209
1210 // Allow plugins to handle special blocks.
1211 $field_value = apply_filters( 'srfm_handle_special_block', $field_value, $block_type, $block, $sureforms_blocks, $this );
1212
1213 $form_fields[] = [
1214 'field_name' => $field_name,
1215 'block_name' => 'multi-choice' === $block_type ? 'srfm-multi' : Helper::get_block_name_from_field( $field_name ),
1216 'value' => $field_value,
1217 'attributes' => $merged_attributes,
1218 ];
1219 }
1220 }
1221
1222 // Recursively process inner blocks but skip for address blocks.
1223 if ( isset( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) && ! empty( $block['innerBlocks'] ) && 'address' !== $block_type ) {
1224 // Pass true if current block has inner blocks and it doesn't need to be duplicated in the main fields array.
1225 $inner_is_special_block = apply_filters( 'srfm_is_special_block', false, $block_type );
1226 $this->extract_form_fields( $block['innerBlocks'], $sureforms_blocks, $form_fields, $entry_data, $inner_is_special_block );
1227 }
1228 }
1229 }
1230
1231 /**
1232 * Get current dropdown counter value.
1233 *
1234 * @since 2.0.0
1235 * @return int Current dropdown counter value.
1236 */
1237 public function get_dropdown_counter() {
1238 return self::$dropdown_counter;
1239 }
1240
1241 /**
1242 * Get onboarding user details.
1243 *
1244 * @since 2.5.3
1245 * @return array<string, mixed>
1246 */
1247 private function get_onboarding_user_details() {
1248 $defaults = [
1249 'first_name' => '',
1250 'last_name' => '',
1251 'email' => '',
1252 'lead' => false,
1253 ];
1254
1255 $user_details = Helper::get_srfm_option( $this->onboarding_user_details_key, $defaults );
1256 if ( ! is_array( $user_details ) ) {
1257 return $defaults;
1258 }
1259
1260 return wp_parse_args( $user_details, $defaults );
1261 }
1262
1263 /**
1264 * Set onboarding user details.
1265 *
1266 * @since 2.5.3
1267 * @param array<string, mixed> $user_details User details to store.
1268 * @return void
1269 */
1270 private function set_onboarding_user_details( $user_details ) {
1271 $details = wp_parse_args(
1272 $user_details,
1273 [
1274 'first_name' => '',
1275 'last_name' => '',
1276 'email' => '',
1277 'lead' => false,
1278 ]
1279 );
1280
1281 Helper::update_srfm_option( $this->onboarding_user_details_key, $details );
1282 }
1283
1284 /**
1285 * Parse form content and return structured field data with attributes.
1286 *
1287 * @param string $form_content The form post content.
1288 * @param array<mixed> $entry_data The entry form data.
1289 * @since 2.0.0
1290 * @return array<string, array<mixed>>
1291 */
1292 private function parse_form_fields( $form_content, $entry_data = [] ) {
1293 if ( empty( $form_content ) ) {
1294 return [];
1295 }
1296
1297 // Parse blocks from form content.
1298 $blocks = parse_blocks( $form_content );
1299 if ( empty( $blocks ) ) {
1300 return [];
1301 }
1302
1303 // Get registered SureForms block attributes.
1304 $registry = \WP_Block_Type_Registry::get_instance();
1305 $registered_blocks = $registry->get_all_registered();
1306
1307 $sureforms_blocks = [];
1308 foreach ( $registered_blocks as $block_name => $block_type ) {
1309 if ( strpos( $block_name, 'srfm/' ) === 0 && is_array( $block_type->attributes ) ) {
1310 $block_key = str_replace( 'srfm/', '', $block_name );
1311 $sureforms_blocks[ $block_key ] = $block_type->attributes;
1312 }
1313 }
1314
1315 $form_fields = [];
1316 $this->extract_form_fields( $blocks, $sureforms_blocks, $form_fields, $entry_data, false );
1317
1318 return $form_fields;
1319 }
1320
1321 /**
1322 * Get endpoints
1323 *
1324 * @since 0.0.7
1325 * @return array<array<mixed>>
1326 */
1327 private function get_endpoints() {
1328 /*
1329 * @internal This filter is used to add custom endpoints.
1330 * @since 1.2.0
1331 * @param array<array<mixed>> $endpoints Endpoints.
1332 */
1333 return apply_filters(
1334 'srfm_rest_api_endpoints',
1335 [
1336 'generate-form' => [
1337 'methods' => 'POST',
1338 'callback' => [ AI_Form_Builder::get_instance(), 'generate_ai_form' ],
1339 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1340 'args' => [
1341 'use_system_message' => [
1342 'sanitize_callback' => [ $this, 'sanitize_boolean_field' ],
1343 ],
1344 ],
1345 ],
1346 // This route is used to map the AI response to SureForms fields markup.
1347 'map-fields' => [
1348 'methods' => 'POST',
1349 'callback' => [ Field_Mapping::get_instance(), 'generate_gutenberg_fields_from_questions' ],
1350 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1351 ],
1352 // This route is used to initiate auth process when user tries to authenticate on billing portal.
1353 'initiate-auth' => [
1354 'methods' => 'GET',
1355 'callback' => [ AI_Auth::get_instance(), 'get_auth_url' ],
1356 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1357 ],
1358 // This route is to used to decrypt the access key and save it in the database.
1359 'handle-access-key' => [
1360 'methods' => 'POST',
1361 'callback' => [ AI_Auth::get_instance(), 'handle_access_key' ],
1362 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1363 ],
1364 // This route is to get the form submissions for the last 30 days.
1365 'entries-chart-data' => [
1366 'methods' => 'GET',
1367 'callback' => [ $this, 'get_entries_chart_data' ],
1368 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1369 ],
1370 // This route is to get all forms data.
1371 'form-data' => [
1372 'methods' => 'GET',
1373 'callback' => [ $this, 'get_form_data' ],
1374 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1375 ],
1376 // Page search endpoint for async admin dropdowns.
1377 'pages/search' => [
1378 'methods' => 'GET',
1379 'callback' => [ $this, 'search_pages' ],
1380 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1381 'args' => [
1382 'search' => [
1383 'sanitize_callback' => 'sanitize_text_field',
1384 'default' => '',
1385 ],
1386 'page' => [
1387 'sanitize_callback' => 'absint',
1388 'default' => 1,
1389 'validate_callback' => static function ( $value ) {
1390 return is_numeric( $value ) && (int) $value >= 1;
1391 },
1392 ],
1393 'per_page' => [
1394 'sanitize_callback' => 'absint',
1395 'default' => 20,
1396 'validate_callback' => static function ( $value ) {
1397 return is_numeric( $value ) && (int) $value >= 1 && (int) $value <= 50;
1398 },
1399 ],
1400 'selected_url' => [
1401 'sanitize_callback' => 'esc_url_raw',
1402 'default' => '',
1403 'validate_callback' => static function ( $value ) {
1404 return empty( $value ) || false !== filter_var( $value, FILTER_VALIDATE_URL );
1405 },
1406 ],
1407 'selected_urls' => [
1408 'default' => [],
1409 'sanitize_callback' => static function( $value ) {
1410 if ( is_array( $value ) ) {
1411 return array_values( array_filter( array_map( 'esc_url_raw', $value ) ) );
1412 }
1413 if ( is_string( $value ) ) {
1414 return array_values(
1415 array_filter(
1416 array_map(
1417 'esc_url_raw',
1418 array_map( 'trim', explode( ',', $value ) )
1419 )
1420 )
1421 );
1422 }
1423 return [];
1424 },
1425 ],
1426 ],
1427 ],
1428 // Onboarding endpoints.
1429 'onboarding/set-status' => [
1430 'methods' => 'POST',
1431 'callback' => [ $this, 'set_onboarding_status' ],
1432 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1433 ],
1434 'onboarding/get-status' => [
1435 'methods' => 'GET',
1436 'callback' => [ $this, 'get_onboarding_status' ],
1437 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1438 ],
1439 'onboarding/user-details' => [
1440 'methods' => 'POST',
1441 'callback' => [ $this, 'save_onboarding_user_details' ],
1442 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1443 'args' => [
1444 'first_name' => [
1445 'required' => true,
1446 'sanitize_callback' => 'sanitize_text_field',
1447 'validate_callback' => static function( $value ) {
1448 return is_string( $value ) && '' !== trim( $value );
1449 },
1450 ],
1451 'last_name' => [
1452 'required' => false,
1453 'sanitize_callback' => 'sanitize_text_field',
1454 ],
1455 'email' => [
1456 'required' => true,
1457 'sanitize_callback' => 'sanitize_email',
1458 'validate_callback' => static function( $value ) {
1459 return is_string( $value ) && is_email( $value );
1460 },
1461 ],
1462 ],
1463 ],
1464 // Plugin status endpoint.
1465 'plugin-status' => [
1466 'methods' => 'GET',
1467 'callback' => [ $this, 'get_plugin_status' ],
1468 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1469 'args' => [
1470 'plugin' => [
1471 'required' => true,
1472 'sanitize_callback' => 'sanitize_text_field',
1473 ],
1474 ],
1475 ],
1476 // Entries endpoints.
1477 'entries/list' => [
1478 'methods' => 'GET',
1479 'callback' => [ $this, 'get_entries_list' ],
1480 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1481 'args' => [
1482 'form_id' => [
1483 'sanitize_callback' => 'absint',
1484 'default' => 0,
1485 ],
1486 'status' => [
1487 'sanitize_callback' => 'sanitize_text_field',
1488 'default' => 'all',
1489 ],
1490 'search' => [
1491 'sanitize_callback' => 'sanitize_text_field',
1492 'default' => '',
1493 ],
1494 'date_from' => [
1495 'sanitize_callback' => 'sanitize_text_field',
1496 'default' => '',
1497 ],
1498 'date_to' => [
1499 'sanitize_callback' => 'sanitize_text_field',
1500 'default' => '',
1501 ],
1502 'orderby' => [
1503 'type' => 'string',
1504 'sanitize_callback' => 'sanitize_text_field',
1505 'default' => 'created_at',
1506 'enum' => [ 'ID', 'id', 'form_id', 'user_id', 'status', 'type', 'created_at', 'updated_at' ],
1507 ],
1508 'order' => [
1509 'type' => 'string',
1510 'sanitize_callback' => 'sanitize_text_field',
1511 'default' => 'DESC',
1512 'enum' => [ 'ASC', 'DESC' ],
1513 ],
1514 'per_page' => [
1515 'sanitize_callback' => 'absint',
1516 'default' => 20,
1517 ],
1518 'page' => [
1519 'sanitize_callback' => 'absint',
1520 'default' => 1,
1521 ],
1522 ],
1523 ],
1524 'entries/read-status' => [
1525 'methods' => 'POST',
1526 'callback' => [ $this, 'update_entries_read_status' ],
1527 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1528 'args' => [
1529 'entry_ids' => [
1530 'required' => true,
1531 'sanitize_callback' => [ $this, 'sanitize_entry_ids' ],
1532 ],
1533 'action' => [
1534 'required' => true,
1535 'sanitize_callback' => 'sanitize_text_field',
1536 'validate_callback' => [ $this, 'validate_read_action' ],
1537 ],
1538 ],
1539 ],
1540 'entries/trash' => [
1541 'methods' => 'POST',
1542 'callback' => [ $this, 'update_entries_trash_status' ],
1543 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1544 'args' => [
1545 'entry_ids' => [
1546 'required' => true,
1547 'sanitize_callback' => [ $this, 'sanitize_entry_ids' ],
1548 ],
1549 'action' => [
1550 'required' => true,
1551 'sanitize_callback' => 'sanitize_text_field',
1552 'validate_callback' => [ $this, 'validate_trash_action' ],
1553 ],
1554 ],
1555 ],
1556 'entries/delete' => [
1557 'methods' => 'POST',
1558 'callback' => [ $this, 'delete_entries' ],
1559 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1560 'args' => [
1561 'entry_ids' => [
1562 'required' => true,
1563 'sanitize_callback' => [ $this, 'sanitize_entry_ids' ],
1564 ],
1565 ],
1566 ],
1567 'entries/export' => [
1568 'methods' => 'POST',
1569 'callback' => [ $this, 'export_entries' ],
1570 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1571 'args' => [
1572 'entry_ids' => [
1573 'sanitize_callback' => [ $this, 'sanitize_entry_ids' ],
1574 'default' => [],
1575 ],
1576 'form_id' => [
1577 'sanitize_callback' => 'absint',
1578 'default' => 0,
1579 ],
1580 'status' => [
1581 'sanitize_callback' => 'sanitize_text_field',
1582 'default' => 'all',
1583 ],
1584 'search' => [
1585 'sanitize_callback' => 'sanitize_text_field',
1586 'default' => '',
1587 ],
1588 'date_from' => [
1589 'sanitize_callback' => 'sanitize_text_field',
1590 'default' => '',
1591 ],
1592 'date_to' => [
1593 'sanitize_callback' => 'sanitize_text_field',
1594 'default' => '',
1595 ],
1596 ],
1597 ],
1598 // Get Single Entry Form Data.
1599 'entry/(?P<id>\d+)/details' => [
1600 'methods' => 'GET',
1601 'callback' => [ $this, 'get_entry_details' ],
1602 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1603 'args' => [
1604 'id' => [
1605 'required' => true,
1606 'sanitize_callback' => 'absint',
1607 ],
1608 ],
1609 ],
1610 // Get Single Entry Logs.
1611 'entry/(?P<id>\d+)/logs' => [
1612 'methods' => 'GET',
1613 'callback' => [ $this, 'get_entry_logs' ],
1614 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1615 'args' => [
1616 'id' => [
1617 'required' => true,
1618 'sanitize_callback' => 'absint',
1619 ],
1620 'per_page' => [
1621 'sanitize_callback' => 'absint',
1622 'default' => 3,
1623 ],
1624 'page' => [
1625 'sanitize_callback' => 'absint',
1626 'default' => 1,
1627 ],
1628 ],
1629 ],
1630 // Forms listing endpoint.
1631 'forms' => [
1632 'methods' => 'GET',
1633 'callback' => [ Forms_Data::get_instance(), 'get_forms_list' ],
1634 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1635 'args' => [
1636 'page' => [
1637 'type' => 'integer',
1638 'default' => 1,
1639 'minimum' => 1,
1640 ],
1641 'per_page' => [
1642 'type' => 'integer',
1643 'minimum' => 1,
1644 'maximum' => 100,
1645 ],
1646 'search' => [
1647 'type' => 'string',
1648 ],
1649 'status' => [
1650 'type' => 'string',
1651 'enum' => [ 'publish', 'draft', 'trash', 'any' ],
1652 'default' => 'publish',
1653 ],
1654 'orderby' => [
1655 'type' => 'string',
1656 'default' => 'date',
1657 'enum' => [ 'date', 'id', 'title', 'modified' ],
1658 ],
1659 'order' => [
1660 'type' => 'string',
1661 'default' => 'desc',
1662 'enum' => [ 'asc', 'desc' ],
1663 ],
1664 'date_from' => [
1665 'type' => 'string',
1666 'format' => 'date',
1667 'sanitize_callback' => 'sanitize_text_field',
1668 'validate_callback' => static function( $value ) {
1669 if ( empty( $value ) ) {
1670 return true;
1671 }
1672 return (bool) strtotime( $value );
1673 },
1674 ],
1675 'date_to' => [
1676 'type' => 'string',
1677 'format' => 'date',
1678 'sanitize_callback' => 'sanitize_text_field',
1679 'validate_callback' => static function( $value ) {
1680 if ( empty( $value ) ) {
1681 return true;
1682 }
1683 return (bool) strtotime( $value );
1684 },
1685 ],
1686 ],
1687 ],
1688 // Export forms endpoint.
1689 'forms/export' => [
1690 'methods' => 'POST',
1691 'callback' => [ Export::get_instance(), 'handle_export_form_rest' ],
1692 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1693 'args' => [
1694 'post_ids' => [
1695 'required' => true,
1696 'type' => [ 'array', 'string' ],
1697 'sanitize_callback' => static function( $value ) {
1698 if ( is_array( $value ) ) {
1699 return array_map( 'intval', $value );
1700 }
1701 return sanitize_text_field( $value );
1702 },
1703 'validate_callback' => static function( $value ) {
1704 if ( is_array( $value ) ) {
1705 return ! empty( $value );
1706 }
1707 return ! empty( trim( $value ) );
1708 },
1709 ],
1710 ],
1711 ],
1712 // Import forms endpoint.
1713 'forms/import' => [
1714 'methods' => 'POST',
1715 'callback' => [ Export::get_instance(), 'handle_import_form_rest' ],
1716 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1717 'args' => [
1718 'forms_data' => [
1719 'required' => true,
1720 'type' => 'array',
1721 'validate_callback' => static function( $value ) {
1722 return is_array( $value ) && ! empty( $value );
1723 },
1724 ],
1725 'default_status' => [
1726 'required' => false,
1727 'type' => 'string',
1728 'default' => 'draft',
1729 'enum' => [ 'draft', 'publish', 'private' ],
1730 'sanitize_callback' => 'sanitize_text_field',
1731 ],
1732 ],
1733 ],
1734 // Form lifecycle management endpoint (trash/restore/delete).
1735 'forms/manage' => [
1736 'methods' => 'POST',
1737 'callback' => [ $this, 'manage_form_lifecycle' ],
1738 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1739 'args' => [
1740 'form_ids' => [
1741 'required' => true,
1742 'type' => [ 'array', 'integer' ],
1743 'sanitize_callback' => static function( $value ) {
1744 if ( is_array( $value ) ) {
1745 return array_map( 'intval', $value );
1746 }
1747 return [ intval( $value ) ];
1748 },
1749 'validate_callback' => static function( $value ) {
1750 if ( is_array( $value ) ) {
1751 return ! empty( $value );
1752 }
1753 return $value > 0;
1754 },
1755 ],
1756 'action' => [
1757 'required' => true,
1758 'type' => 'string',
1759 'enum' => [ 'trash', 'restore', 'delete' ],
1760 'sanitize_callback' => 'sanitize_text_field',
1761 ],
1762 ],
1763 ],
1764 // Form duplication endpoint.
1765 'forms/duplicate' => [
1766 'methods' => 'POST',
1767 'callback' => [ Duplicate_Form::get_instance(), 'handle_duplicate_form_rest' ],
1768 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1769 'args' => [
1770 'form_id' => [
1771 'required' => true,
1772 'type' => 'integer',
1773 'sanitize_callback' => 'absint',
1774 'validate_callback' => static function( $value ) {
1775 return $value > 0;
1776 },
1777 ],
1778 'title_suffix' => [
1779 'required' => false,
1780 'type' => 'string',
1781 'default' => __( ' (Copy)', 'sureforms' ),
1782 'sanitize_callback' => 'sanitize_text_field',
1783 ],
1784 ],
1785 ],
1786 ]
1787 );
1788 }
1789 }
1790