PluginProbe
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz / 2.6.0
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz v2.6.0
2.12.6 2.12.5 2.12.4 2.12.3 2.12.2 2.12.1 2.12.0 2.11.1 2.11.0 2.10.1 2.10.0 2.9.1 2.9.0 2.8.2 2.8.1 2.7.0 2.7.1 2.8.0 trunk 0.0.10 0.0.11 0.0.12 0.0.13 0.0.2 0.0.3 All 96 releases
sureforms / inc / rest-api.php

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

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