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

1,404 lines 43.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Rest API Manager Class.
4 *
5 * @package sureforms.
6 */
7
8 namespace SRFM\Inc;
9
10 use SRFM\Inc\AI_Form_Builder\AI_Auth;
11 use SRFM\Inc\AI_Form_Builder\AI_Form_Builder;
12 use SRFM\Inc\AI_Form_Builder\Field_Mapping;
13 use SRFM\Inc\Database\Tables\Entries;
14 use SRFM\Inc\Entries as Entries_Class;
15 use SRFM\Inc\Traits\Get_Instance;
16
17 if ( ! defined( 'ABSPATH' ) ) {
18 exit; // Exit if accessed directly.
19 }
20
21 /**
22 * Rest API handler class.
23 *
24 * @since 0.0.7
25 */
26 class Rest_Api {
27 use Get_Instance;
28
29 /**
30 * 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( __( 'Nonce verification failed.', 'sureforms' ) );
92 }
93
94 $params = $request->get_params();
95
96 if ( empty( $params ) ) {
97 wp_send_json_error( __( 'Request could not be processed.', '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.', '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( __( 'Nonce verification failed.', 'sureforms' ) );
151 }
152
153 $forms = Helper::get_instance()->get_sureforms();
154
155 return ! empty( $forms ) ? $forms : [];
156 }
157
158 /**
159 * Set onboarding completion status.
160 *
161 * @param \WP_REST_Request $request Full details about the request.
162 * @since 1.9.1
163 * @return \WP_REST_Response
164 */
165 public function set_onboarding_status( $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 // Set the onboarding status to yes always.
176 Onboarding::get_instance()->set_onboarding_status( 'yes' );
177
178 // Get analytics data from request.
179 $analytics_data = $request->get_param( 'analyticsData' );
180
181 // Save analytics data if provided.
182 if ( $analytics_data ) {
183 // Use Helper::update_srfm_option instead of update_option.
184 Helper::update_srfm_option( 'onboarding_analytics', $analytics_data );
185 }
186
187 return new \WP_REST_Response( [ 'success' => true ] );
188 }
189
190 /**
191 * Get onboarding completion status.
192 *
193 * @param \WP_REST_Request $request Full details about the request.
194 * @since 1.9.1
195 * @return \WP_REST_Response
196 */
197 public function get_onboarding_status( $request ) {
198 $nonce = Helper::get_string_value( $request->get_header( 'X-WP-Nonce' ) );
199
200 if ( ! wp_verify_nonce( sanitize_text_field( $nonce ), 'wp_rest' ) ) {
201 return new \WP_REST_Response(
202 [ 'error' => __( 'Nonce verification failed.', 'sureforms' ) ],
203 403
204 );
205 }
206
207 $status = Onboarding::get_instance()->get_onboarding_status();
208
209 return new \WP_REST_Response( [ 'completed' => $status ] );
210 }
211
212 /**
213 * Get plugin status for specified plugin.
214 *
215 * @param \WP_REST_Request $request Full details about the request.
216 * @since 1.9.1
217 * @return \WP_REST_Response
218 */
219 public function get_plugin_status( $request ) {
220 $nonce = Helper::get_string_value( $request->get_header( 'X-WP-Nonce' ) );
221
222 if ( ! wp_verify_nonce( sanitize_text_field( $nonce ), 'wp_rest' ) ) {
223 return new \WP_REST_Response(
224 [ 'error' => __( 'Nonce verification failed.', 'sureforms' ) ],
225 403
226 );
227 }
228
229 $params = $request->get_params();
230 $plugin_slug = is_array( $params ) && isset( $params['plugin'] ) ?
231 sanitize_text_field( Helper::get_string_value( $params['plugin'] ) ) : '';
232
233 if ( empty( $plugin_slug ) ) {
234 return new \WP_REST_Response(
235 [ 'error' => __( 'Plugin slug is required.', 'sureforms' ) ],
236 400
237 );
238 }
239
240 $integrations = Helper::sureforms_get_integration();
241
242 if ( ! isset( $integrations[ $plugin_slug ] ) ) {
243 return new \WP_REST_Response(
244 [ 'error' => __( 'Plugin not found.', 'sureforms' ) ],
245 404
246 );
247 }
248
249 $plugin_data = $integrations[ $plugin_slug ];
250
251 // Get fresh status.
252 if ( is_array( $plugin_data ) && isset( $plugin_data['path'] ) ) {
253 $plugin_data['status'] = Helper::get_plugin_status( Helper::get_string_value( $plugin_data['path'] ) );
254 }
255
256 return new \WP_REST_Response( $plugin_data );
257 }
258
259 /**
260 * Sanitize entry IDs.
261 *
262 * @param mixed $value Value to sanitize.
263 * @since 2.0.0
264 * @return array<int>
265 */
266 public function sanitize_entry_ids( $value ) {
267 if ( is_array( $value ) ) {
268 return array_filter( array_map( 'absint', $value ) );
269 }
270 if ( is_numeric( $value ) ) {
271 return [ absint( $value ) ];
272 }
273 if ( is_string( $value ) ) {
274 // Handle comma-separated values.
275 $ids = explode( ',', $value );
276 return array_filter( array_map( 'absint', $ids ) );
277 }
278 return [];
279 }
280
281 /**
282 * Validate read action parameter.
283 *
284 * @param string $param Action parameter value.
285 * @since 2.0.0
286 * @return bool
287 */
288 public function validate_read_action( $param ) {
289 return in_array( $param, [ 'read', 'unread' ], true );
290 }
291
292 /**
293 * Validate trash action parameter.
294 *
295 * @param string $param Action parameter value.
296 * @since 2.0.0
297 * @return bool
298 */
299 public function validate_trash_action( $param ) {
300 return in_array( $param, [ 'trash', 'restore' ], true );
301 }
302
303 /**
304 * Get entries list with filters and pagination.
305 *
306 * @param \WP_REST_Request $request Full details about the request.
307 * @since 2.0.0
308 * @return \WP_REST_Response
309 */
310 public function get_entries_list( $request ) {
311 $nonce = Helper::get_string_value( $request->get_header( 'X-WP-Nonce' ) );
312
313 if ( ! wp_verify_nonce( sanitize_text_field( $nonce ), 'wp_rest' ) ) {
314 return new \WP_REST_Response(
315 [ 'error' => __( 'Nonce verification failed.', 'sureforms' ) ],
316 403
317 );
318 }
319
320 $params = $request->get_params();
321
322 $args = [
323 'form_id' => isset( $params['form_id'] ) ? absint( $params['form_id'] ) : 0,
324 'status' => isset( $params['status'] ) ? sanitize_text_field( $params['status'] ) : 'all',
325 'search' => isset( $params['search'] ) ? sanitize_text_field( $params['search'] ) : '',
326 'date_from' => isset( $params['date_from'] ) ? sanitize_text_field( $params['date_from'] ) : '',
327 'date_to' => isset( $params['date_to'] ) ? sanitize_text_field( $params['date_to'] ) : '',
328 'orderby' => isset( $params['orderby'] ) ? sanitize_text_field( $params['orderby'] ) : 'created_at',
329 'order' => isset( $params['order'] ) ? sanitize_text_field( $params['order'] ) : 'DESC',
330 'per_page' => isset( $params['per_page'] ) ? absint( $params['per_page'] ) : 20,
331 'page' => isset( $params['page'] ) ? absint( $params['page'] ) : 1,
332 ];
333
334 $result = Entries_Class::get_entries( $args );
335
336 // Add form permalink to each entry.
337 if ( isset( $result['entries'] ) && is_array( $result['entries'] ) ) {
338 foreach ( $result['entries'] as &$entry ) {
339 if ( isset( $entry['form_id'] ) ) {
340 $entry['form_permalink'] = get_permalink( absint( $entry['form_id'] ) );
341 }
342 }
343 }
344
345 return new \WP_REST_Response( $result, 200 );
346 }
347
348 /**
349 * Update entries read status (read/unread).
350 *
351 * @param \WP_REST_Request $request Full details about the request.
352 * @since 2.0.0
353 * @return \WP_REST_Response
354 */
355 public function update_entries_read_status( $request ) {
356 $nonce = Helper::get_string_value( $request->get_header( 'X-WP-Nonce' ) );
357
358 if ( ! wp_verify_nonce( sanitize_text_field( $nonce ), 'wp_rest' ) ) {
359 return new \WP_REST_Response(
360 [ 'error' => __( 'Nonce verification failed.', 'sureforms' ) ],
361 403
362 );
363 }
364
365 $entry_ids = $request->get_param( 'entry_ids' );
366 $action = $request->get_param( 'action' );
367
368 if ( empty( $entry_ids ) ) {
369 return new \WP_REST_Response(
370 [ 'error' => __( 'No entry IDs provided.', 'sureforms' ) ],
371 400
372 );
373 }
374
375 if ( empty( $action ) ) {
376 return new \WP_REST_Response(
377 [ 'error' => __( 'No action provided.', 'sureforms' ) ],
378 400
379 );
380 }
381
382 // Validate action.
383 if ( ! $this->validate_read_action( $action ) ) {
384 return new \WP_REST_Response(
385 [ 'error' => __( 'Invalid action. Must be "read" or "unread".', 'sureforms' ) ],
386 400
387 );
388 }
389
390 $result = Entries_Class::update_status( $entry_ids, $action );
391
392 $status_code = $result['success'] ? 200 : 400;
393
394 return new \WP_REST_Response( $result, $status_code );
395 }
396
397 /**
398 * Update entries trash status (trash/restore).
399 *
400 * @param \WP_REST_Request $request Full details about the request.
401 * @since 2.0.0
402 * @return \WP_REST_Response
403 */
404 public function update_entries_trash_status( $request ) {
405 $nonce = Helper::get_string_value( $request->get_header( 'X-WP-Nonce' ) );
406
407 if ( ! wp_verify_nonce( sanitize_text_field( $nonce ), 'wp_rest' ) ) {
408 return new \WP_REST_Response(
409 [ 'error' => __( 'Nonce verification failed.', 'sureforms' ) ],
410 403
411 );
412 }
413
414 $entry_ids = $request->get_param( 'entry_ids' );
415 $action = $request->get_param( 'action' );
416
417 if ( empty( $entry_ids ) ) {
418 return new \WP_REST_Response(
419 [ 'error' => __( 'No entry IDs provided.', 'sureforms' ) ],
420 400
421 );
422 }
423
424 if ( empty( $action ) ) {
425 return new \WP_REST_Response(
426 [ 'error' => __( 'No action provided.', 'sureforms' ) ],
427 400
428 );
429 }
430
431 // Validate action.
432 if ( ! $this->validate_trash_action( $action ) ) {
433 return new \WP_REST_Response(
434 [ 'error' => __( 'Invalid action. Must be "trash" or "restore".', 'sureforms' ) ],
435 400
436 );
437 }
438
439 $result = Entries_Class::update_status( $entry_ids, $action );
440
441 $status_code = $result['success'] ? 200 : 400;
442
443 return new \WP_REST_Response( $result, $status_code );
444 }
445
446 /**
447 * Permanently delete entries.
448 *
449 * @param \WP_REST_Request $request Full details about the request.
450 * @since 2.0.0
451 * @return \WP_REST_Response
452 */
453 public function delete_entries( $request ) {
454 $nonce = Helper::get_string_value( $request->get_header( 'X-WP-Nonce' ) );
455
456 if ( ! wp_verify_nonce( sanitize_text_field( $nonce ), 'wp_rest' ) ) {
457 return new \WP_REST_Response(
458 [ 'error' => __( 'Nonce verification failed.', 'sureforms' ) ],
459 403
460 );
461 }
462
463 $entry_ids = $request->get_param( 'entry_ids' );
464
465 if ( empty( $entry_ids ) ) {
466 return new \WP_REST_Response(
467 [ 'error' => __( 'No entry IDs provided.', 'sureforms' ) ],
468 400
469 );
470 }
471
472 $result = Entries_Class::delete_entries( $entry_ids );
473
474 $status_code = $result['success'] ? 200 : 400;
475
476 return new \WP_REST_Response( $result, $status_code );
477 }
478
479 /**
480 * Get entry details with form data, submission info, and metadata.
481 *
482 * @param \WP_REST_Request $request Full details about the request.
483 * @since 2.0.0
484 * @return \WP_REST_Response
485 */
486 public function get_entry_details( $request ) {
487 $nonce = Helper::get_string_value( $request->get_header( 'X-WP-Nonce' ) );
488
489 if ( ! wp_verify_nonce( sanitize_text_field( $nonce ), 'wp_rest' ) ) {
490 return new \WP_REST_Response(
491 [ 'error' => __( 'Nonce verification failed.', 'sureforms' ) ],
492 403
493 );
494 }
495
496 $entry_id = absint( $request->get_param( 'id' ) );
497
498 if ( empty( $entry_id ) ) {
499 return new \WP_REST_Response(
500 [ 'error' => __( 'Entry ID is required.', 'sureforms' ) ],
501 400
502 );
503 }
504
505 $entry = Entries::get( $entry_id );
506
507 if ( ! $entry ) {
508 return new \WP_REST_Response(
509 [ 'error' => __( 'Entry not found.', 'sureforms' ) ],
510 404
511 );
512 }
513
514 // Get adjacent entry IDs for navigation (all entries in chronological order).
515 $adjacent_entries = Entries_Class::get_adjacent_entry_ids( $entry_id );
516
517 // Process form data.
518 $form_data = [];
519 $excluded_fields = [ 'srfm-honeypot-field', 'g-recaptcha-response', 'srfm-sender-email-field' ];
520 $entry_form_data = $entry['form_data'] ?? [];
521
522 if ( is_array( $entry_form_data ) ) {
523 foreach ( $entry_form_data as $field_name => $value ) {
524 if ( ! is_string( $field_name ) || in_array( $field_name, $excluded_fields, true ) ) {
525 continue;
526 }
527 if ( false === str_contains( $field_name, '-lbl-' ) ) {
528 continue;
529 }
530
531 $label_parts = explode( '-lbl-', $field_name );
532 $label = isset( $label_parts[1] ) ? explode( '-', $label_parts[1] )[0] : '';
533 $label = $label ? Helper::decrypt( $label ) : '';
534 $field_block_name = Helper::get_block_name_from_field( $field_name );
535
536 /**
537 * Filter: 'srfm_entry_value'
538 *
539 * This filter is used to allow 3rd party plugins or custom code to modify
540 * the entry field value in the entry details REST API response, if required.
541 * For example, you may want to decrypt, format, or mask sensitive data before output.
542 *
543 * @since 2.0.0
544 *
545 * @param mixed $value The original value for the field.
546 * @param array $context An array of context, including:
547 * - field_name (string)
548 * - label (string)
549 * - field_block_name (string)
550 *
551 * @return mixed
552 */
553 $value = apply_filters(
554 'srfm_entry_value',
555 $value,
556 [
557 'field_name' => $field_name,
558 'label' => $label,
559 'field_block_name' => $field_block_name,
560 ]
561 );
562
563 $form_data[] = [
564 'field_name' => $field_name,
565 'label' => $label,
566 'value' => $value,
567 'block_name' => $field_block_name,
568 ];
569 }
570 }
571
572 // Get user info.
573 $user_id = Helper::get_integer_value( $entry['user_id'] );
574 $user_info = 0 !== $user_id ? get_userdata( $user_id ) : null;
575
576 // Get form info.
577 $form_title = get_post_field( 'post_title', $entry['form_id'] );
578 // Translators: %d is the form ID.
579 $form_name = ! empty( $form_title ) ? $form_title : sprintf( __( 'SureForms Form #%d', 'sureforms' ), intval( $entry['form_id'] ) );
580
581 // Parse form content to get structured field data.
582 $form_content = get_post_field( 'post_content', $entry['form_id'] );
583 $form_fields = $this->parse_form_fields( $form_content, $entry['form_data'] ?? [] );
584
585 $response_data = [
586 'id' => $entry_id,
587 'form_id' => $entry['form_id'],
588 'form_name' => $form_name,
589 'form_permalink' => get_permalink( $entry['form_id'] ),
590 'status' => $entry['status'],
591 'created_at' => $entry['created_at'],
592 'form_data' => $form_data,
593 'form_content' => $form_fields,
594 'submission_info' => [
595 'user_ip' => $entry['submission_info']['user_ip'] ?? '',
596 'browser_name' => $entry['submission_info']['browser_name'] ?? '',
597 'device_name' => $entry['submission_info']['device_name'] ?? '',
598 ],
599 'user' => $user_info ? [
600 'id' => $user_id,
601 'display_name' => $user_info->display_name,
602 'profile_url' => get_author_posts_url( $user_id ),
603 ] : null,
604 'extras' => $entry['extras'] ?? [],
605 'navigation' => [
606 'previous_entry_id' => $adjacent_entries['previous_id'] ?? null,
607 'next_entry_id' => $adjacent_entries['next_id'] ?? null,
608 ],
609 ];
610
611 return new \WP_REST_Response( $response_data, 200 );
612 }
613
614 /**
615 * Get entry logs with pagination support.
616 *
617 * @param \WP_REST_Request $request Full details about the request.
618 * @since 2.0.0
619 * @return \WP_REST_Response
620 */
621 public function get_entry_logs( $request ) {
622 $nonce = Helper::get_string_value( $request->get_header( 'X-WP-Nonce' ) );
623
624 if ( ! wp_verify_nonce( sanitize_text_field( $nonce ), 'wp_rest' ) ) {
625 return new \WP_REST_Response(
626 [ 'error' => __( 'Nonce verification failed.', 'sureforms' ) ],
627 403
628 );
629 }
630
631 $entry_id = absint( $request->get_param( 'id' ) );
632 $per_page = absint( $request->get_param( 'per_page' ) );
633 $per_page = $per_page ? $per_page : 3;
634 $page = absint( $request->get_param( 'page' ) );
635 $page = $page ? $page : 1;
636
637 if ( empty( $entry_id ) ) {
638 return new \WP_REST_Response(
639 [ 'error' => __( 'Entry ID is required.', 'sureforms' ) ],
640 400
641 );
642 }
643
644 $entry = Entries::get( $entry_id );
645
646 if ( ! $entry ) {
647 return new \WP_REST_Response(
648 [ 'error' => __( 'Entry not found.', 'sureforms' ) ],
649 404
650 );
651 }
652
653 $logs = $entry['logs'] ?? [];
654 $logs = is_array( $logs ) ? $logs : [];
655 $total_logs = count( $logs );
656 $total_pages = ceil( $total_logs / $per_page );
657 $offset = ( $page - 1 ) * $per_page;
658
659 // Paginate logs.
660 $paginated_logs = array_slice( $logs, $offset, $per_page );
661
662 // Format logs with unique IDs for deletion.
663 $formatted_logs = [];
664 foreach ( $paginated_logs as $index => $log ) {
665 if ( ! is_array( $log ) ) {
666 continue;
667 }
668 $formatted_logs[] = [
669 'id' => $offset + $index, // Use offset-based ID for consistent deletion.
670 'title' => $log['title'] ?? '',
671 'timestamp' => $log['timestamp'] ?? time(),
672 'messages' => $log['messages'] ?? [],
673 ];
674 }
675
676 $response_data = [
677 'logs' => $formatted_logs,
678 'current_page' => $page,
679 'per_page' => $per_page,
680 'total' => $total_logs,
681 'total_pages' => $total_pages,
682 ];
683
684 return new \WP_REST_Response( $response_data, 200 );
685 }
686
687 /**
688 * Export entries to CSV or ZIP.
689 *
690 * @param \WP_REST_Request $request Full details about the request.
691 * @since 2.0.0
692 * @return \WP_REST_Response
693 */
694 public function export_entries( $request ) {
695 $nonce = Helper::get_string_value( $request->get_header( 'X-WP-Nonce' ) );
696
697 if ( ! wp_verify_nonce( sanitize_text_field( $nonce ), 'wp_rest' ) ) {
698 return new \WP_REST_Response(
699 [ 'error' => __( 'Nonce verification failed.', 'sureforms' ) ],
700 403
701 );
702 }
703
704 $params = $request->get_params();
705
706 $args = [
707 'entry_ids' => isset( $params['entry_ids'] ) ? $this->sanitize_entry_ids( $params['entry_ids'] ) : [],
708 'form_id' => isset( $params['form_id'] ) ? absint( $params['form_id'] ) : 0,
709 'status' => isset( $params['status'] ) ? sanitize_text_field( $params['status'] ) : 'all',
710 'search' => isset( $params['search'] ) ? sanitize_text_field( $params['search'] ) : '',
711 'date_from' => isset( $params['date_from'] ) ? sanitize_text_field( $params['date_from'] ) : '',
712 'date_to' => isset( $params['date_to'] ) ? sanitize_text_field( $params['date_to'] ) : '',
713 ];
714
715 /**
716 * Export result with success status and either error message or file details.
717 *
718 * @var array{success: false, error: string} | array{success: true, filename: string, filepath: string, type: string} $result
719 */
720 $result = Entries_Class::export_entries( $args );
721
722 if ( ! $result['success'] ) {
723 return new \WP_REST_Response(
724 [ 'error' => $result['error'] ],
725 400
726 );
727 }
728
729 // Return file information for download.
730 $filepath = Helper::get_string_value( $result['filepath'] );
731 return new \WP_REST_Response(
732 [
733 'success' => true,
734 'filename' => $result['filename'],
735 'filepath' => $result['filepath'],
736 'type' => $result['type'],
737 'download_url' => add_query_arg(
738 '_wpnonce',
739 wp_create_nonce( 'srfm_download_export' ),
740 admin_url( 'admin-ajax.php?action=srfm_download_export&file=' . rawurlencode( basename( $filepath ) ) )
741 ),
742 ],
743 200
744 );
745 }
746 /**
747 * Manage form lifecycle operations (trash, restore, delete).
748 *
749 * @param \WP_REST_Request $request Full details about the request.
750 * @since 2.0.0
751 * @return \WP_REST_Response|\WP_Error
752 */
753 public function manage_form_lifecycle( $request ) {
754 $nonce = Helper::get_string_value( $request->get_header( 'X-WP-Nonce' ) );
755
756 if ( ! wp_verify_nonce( sanitize_text_field( $nonce ), 'wp_rest' ) ) {
757 return new \WP_Error(
758 'invalid_nonce',
759 __( 'Nonce verification failed.', 'sureforms' ),
760 [ 'status' => 403 ]
761 );
762 }
763
764 $params = $request->get_params();
765 $form_ids = isset( $params['form_ids'] ) && is_array( $params['form_ids'] ) ?
766 array_map( 'intval', $params['form_ids'] ) :
767 [ intval( $params['form_ids'] ) ];
768 $action = isset( $params['action'] ) ? sanitize_text_field( Helper::get_string_value( $params['action'] ) ) : '';
769
770 if ( empty( $form_ids ) || empty( $action ) ) {
771 return new \WP_Error(
772 'missing_parameters',
773 __( 'Form IDs and action are required.', 'sureforms' ),
774 [ 'status' => 400 ]
775 );
776 }
777
778 $results = [];
779 $errors = [];
780
781 foreach ( $form_ids as $form_id ) {
782 $post = get_post( $form_id );
783
784 // Validate that the post exists and is a sureforms_form.
785 if ( ! $post || 'sureforms_form' !== $post->post_type ) {
786 $errors[] = [
787 'form_id' => $form_id,
788 'error' => __( 'Form not found or invalid post type.', 'sureforms' ),
789 ];
790 continue;
791 }
792
793 $result = false;
794
795 switch ( $action ) {
796 case 'trash':
797 if ( 'trash' === $post->post_status ) {
798 $errors[] = [
799 'form_id' => $form_id,
800 'error' => __( 'Form is already in trash.', 'sureforms' ),
801 ];
802 } else {
803 $result = wp_trash_post( $form_id );
804 }
805 break;
806
807 case 'restore':
808 if ( 'trash' !== $post->post_status ) {
809 $errors[] = [
810 'form_id' => $form_id,
811 'error' => __( 'Form is not in trash.', 'sureforms' ),
812 ];
813 } else {
814 $result = wp_untrash_post( $form_id );
815 }
816 break;
817
818 case 'delete':
819 // Force delete permanently.
820 $result = wp_delete_post( $form_id, true );
821 break;
822
823 default:
824 $errors[] = [
825 'form_id' => $form_id,
826 'error' => __( 'Invalid action specified.', 'sureforms' ),
827 ];
828 break;
829 }
830
831 if ( $result ) {
832 $results[] = [
833 'form_id' => $form_id,
834 'action' => $action,
835 'success' => true,
836 ];
837 } elseif ( ! isset( $errors[ array_search( $form_id, array_column( $errors, 'form_id' ), true ) ] ) ) {
838 $errors[] = [
839 'form_id' => $form_id,
840 /* translators: %s: action name */
841 'error' => sprintf( __( 'Failed to %s form.', 'sureforms' ), $action ),
842 ];
843 }
844 }
845
846 $response_data = [
847 'success' => ! empty( $results ),
848 'action' => $action,
849 'processed_ids' => array_column( $results, 'form_id' ),
850 'success_count' => count( $results ),
851 'results' => $results,
852 ];
853
854 if ( ! empty( $errors ) ) {
855 $response_data['errors'] = $errors;
856 $response_data['error_count'] = count( $errors );
857 }
858
859 return new \WP_REST_Response( $response_data );
860 }
861
862 /**
863 * Recursively extract form fields from blocks.
864 *
865 * @param array<mixed> $blocks The blocks array.
866 * @param array<string, array<mixed>> $sureforms_blocks Registered SureForms block attributes.
867 * @param array<string, array<mixed>> &$form_fields Reference to form fields array.
868 * @param array<mixed> $entry_data The entry form data.
869 * @param bool $is_special_block Whether the current block is a special block (like address).
870 * @param int|null $base_counter Base counter for unique field naming.
871 * @since 2.0.0
872 * @return void
873 */
874 public function extract_form_fields( $blocks, $sureforms_blocks, &$form_fields, $entry_data = [], $is_special_block = false, $base_counter = null ) {
875 if ( null !== $base_counter ) {
876 self::$dropdown_counter = $base_counter;
877 }
878 $block_type = '';
879
880 foreach ( $blocks as $block ) {
881 if ( ! is_array( $block ) || ! isset( $block['blockName'] ) || ! is_string( $block['blockName'] ) ) {
882 continue;
883 }
884
885 // Check if it's a SureForms block.
886 if ( strpos( $block['blockName'], 'srfm/' ) === 0 ) {
887 $block_type = str_replace( 'srfm/', '', $block['blockName'] );
888 // Skip inline button or fields inside nested blocks except address.
889 if ( 'inline-button' === $block_type || ( $is_special_block && 'address' !== $block_type ) ) {
890 continue;
891 }
892
893 if ( isset( $sureforms_blocks[ $block_type ] ) && is_array( $sureforms_blocks[ $block_type ] ) ) {
894 $block_attributes = isset( $block['attrs'] ) && is_array( $block['attrs'] ) ? $block['attrs'] : [];
895 $default_attributes = $sureforms_blocks[ $block_type ];
896
897 // Merge block instance attributes with defaults.
898 $merged_attributes = [];
899 foreach ( $default_attributes as $attr_name => $attr_config ) {
900 if ( ! is_string( $attr_name ) ) {
901 continue;
902 }
903 $default_value = null;
904 if ( is_array( $attr_config ) && isset( $attr_config['default'] ) ) {
905 $default_value = $attr_config['default'];
906 }
907 $merged_attributes[ $attr_name ] = $block_attributes[ $attr_name ] ?? $default_value;
908 }
909
910 // Generate field name.
911 $label = is_string( $merged_attributes['label'] ?? '' ) ? $merged_attributes['label'] : '';
912 $slug = is_string( $merged_attributes['slug'] ?? '' ) ? $merged_attributes['slug'] : '';
913 $block_id = is_string( $merged_attributes['block_id'] ?? '' ) ? $merged_attributes['block_id'] : '';
914 $field_name = '';
915 $base_field_name = '';
916
917 if ( ! empty( $label ) && ! empty( $slug ) && ! empty( $block_id ) ) {
918 $input_label = '-lbl-' . Helper::encrypt( $label );
919 $base_field_name = $input_label . '-' . $slug;
920
921 // Handle special case for dropdown with instance counter.
922 if ( 'dropdown' === $block_type ) {
923 self::$dropdown_counter++;
924 $unique_slug = $block_type . '-' . self::$dropdown_counter;
925 $field_name = 'srfm-' . $unique_slug . '-' . $block_id . $base_field_name;
926 } elseif ( 'multi-choice' === $block_type ) {
927 // Multi-choice uses standard pattern.
928 $field_name = 'srfm-input-' . $block_type . '-' . $block_id . $base_field_name;
929 } else {
930 // Standard field name for other blocks.
931 $field_name = 'srfm-' . $block_type . '-' . $block_id . $base_field_name;
932 }
933 }
934
935 // Allow pro plugin to modify field_name.
936 $field_name = apply_filters( 'srfm_extract_form_fields_field_name', $field_name, $base_field_name, $block_type, $block_id );
937
938 // Get the value from entry data or use default.
939 $field_value = $entry_data[ $field_name ] ?? ( $merged_attributes['defaultValue'] ?? '' );
940
941 // Special handling for address blocks - extract inner fields.
942 if ( 'address' === $block_type && isset( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) {
943 $inner_fields = [];
944 $this->extract_form_fields( $block['innerBlocks'], $sureforms_blocks, $inner_fields, $entry_data, false );
945 $field_value = $inner_fields;
946 }
947
948 // Allow plugins to handle special blocks.
949 $field_value = apply_filters( 'srfm_handle_special_block', $field_value, $block_type, $block, $sureforms_blocks, $this );
950
951 $form_fields[] = [
952 'field_name' => $field_name,
953 'block_name' => 'multi-choice' === $block_type ? 'srfm-multi' : Helper::get_block_name_from_field( $field_name ),
954 'value' => $field_value,
955 'attributes' => $merged_attributes,
956 ];
957 }
958 }
959
960 // Recursively process inner blocks but skip for address blocks.
961 if ( isset( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) && ! empty( $block['innerBlocks'] ) && 'address' !== $block_type ) {
962 // Pass true if current block has inner blocks and it doesn't need to be duplicated in the main fields array.
963 $inner_is_special_block = apply_filters( 'srfm_is_special_block', false, $block_type );
964 $this->extract_form_fields( $block['innerBlocks'], $sureforms_blocks, $form_fields, $entry_data, $inner_is_special_block );
965 }
966 }
967 }
968
969 /**
970 * Get current dropdown counter value.
971 *
972 * @since 2.0.0
973 * @return int Current dropdown counter value.
974 */
975 public function get_dropdown_counter() {
976 return self::$dropdown_counter;
977 }
978
979 /**
980 * Parse form content and return structured field data with attributes.
981 *
982 * @param string $form_content The form post content.
983 * @param array<mixed> $entry_data The entry form data.
984 * @since 2.0.0
985 * @return array<string, array<mixed>>
986 */
987 private function parse_form_fields( $form_content, $entry_data = [] ) {
988 if ( empty( $form_content ) ) {
989 return [];
990 }
991
992 // Parse blocks from form content.
993 $blocks = parse_blocks( $form_content );
994 if ( empty( $blocks ) ) {
995 return [];
996 }
997
998 // Get registered SureForms block attributes.
999 $registry = \WP_Block_Type_Registry::get_instance();
1000 $registered_blocks = $registry->get_all_registered();
1001
1002 $sureforms_blocks = [];
1003 foreach ( $registered_blocks as $block_name => $block_type ) {
1004 if ( strpos( $block_name, 'srfm/' ) === 0 && is_array( $block_type->attributes ) ) {
1005 $block_key = str_replace( 'srfm/', '', $block_name );
1006 $sureforms_blocks[ $block_key ] = $block_type->attributes;
1007 }
1008 }
1009
1010 $form_fields = [];
1011 $this->extract_form_fields( $blocks, $sureforms_blocks, $form_fields, $entry_data, false );
1012
1013 return $form_fields;
1014 }
1015
1016 /**
1017 * Get endpoints
1018 *
1019 * @since 0.0.7
1020 * @return array<array<mixed>>
1021 */
1022 private function get_endpoints() {
1023 /*
1024 * @internal This filter is used to add custom endpoints.
1025 * @since 1.2.0
1026 * @param array<array<mixed>> $endpoints Endpoints.
1027 */
1028 return apply_filters(
1029 'srfm_rest_api_endpoints',
1030 [
1031 'generate-form' => [
1032 'methods' => 'POST',
1033 'callback' => [ AI_Form_Builder::get_instance(), 'generate_ai_form' ],
1034 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1035 'args' => [
1036 'use_system_message' => [
1037 'sanitize_callback' => [ $this, 'sanitize_boolean_field' ],
1038 ],
1039 ],
1040 ],
1041 // This route is used to map the AI response to SureForms fields markup.
1042 'map-fields' => [
1043 'methods' => 'POST',
1044 'callback' => [ Field_Mapping::get_instance(), 'generate_gutenberg_fields_from_questions' ],
1045 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1046 ],
1047 // This route is used to initiate auth process when user tries to authenticate on billing portal.
1048 'initiate-auth' => [
1049 'methods' => 'GET',
1050 'callback' => [ AI_Auth::get_instance(), 'get_auth_url' ],
1051 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1052 ],
1053 // This route is to used to decrypt the access key and save it in the database.
1054 'handle-access-key' => [
1055 'methods' => 'POST',
1056 'callback' => [ AI_Auth::get_instance(), 'handle_access_key' ],
1057 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1058 ],
1059 // This route is to get the form submissions for the last 30 days.
1060 'entries-chart-data' => [
1061 'methods' => 'GET',
1062 'callback' => [ $this, 'get_entries_chart_data' ],
1063 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1064 ],
1065 // This route is to get all forms data.
1066 'form-data' => [
1067 'methods' => 'GET',
1068 'callback' => [ $this, 'get_form_data' ],
1069 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1070 ],
1071 // Onboarding endpoints.
1072 'onboarding/set-status' => [
1073 'methods' => 'POST',
1074 'callback' => [ $this, 'set_onboarding_status' ],
1075 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1076 ],
1077 'onboarding/get-status' => [
1078 'methods' => 'GET',
1079 'callback' => [ $this, 'get_onboarding_status' ],
1080 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1081 ],
1082 // Plugin status endpoint.
1083 'plugin-status' => [
1084 'methods' => 'GET',
1085 'callback' => [ $this, 'get_plugin_status' ],
1086 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1087 'args' => [
1088 'plugin' => [
1089 'required' => true,
1090 'sanitize_callback' => 'sanitize_text_field',
1091 ],
1092 ],
1093 ],
1094 // Entries endpoints.
1095 'entries/list' => [
1096 'methods' => 'GET',
1097 'callback' => [ $this, 'get_entries_list' ],
1098 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1099 'args' => [
1100 'form_id' => [
1101 'sanitize_callback' => 'absint',
1102 'default' => 0,
1103 ],
1104 'status' => [
1105 'sanitize_callback' => 'sanitize_text_field',
1106 'default' => 'all',
1107 ],
1108 'search' => [
1109 'sanitize_callback' => 'sanitize_text_field',
1110 'default' => '',
1111 ],
1112 'date_from' => [
1113 'sanitize_callback' => 'sanitize_text_field',
1114 'default' => '',
1115 ],
1116 'date_to' => [
1117 'sanitize_callback' => 'sanitize_text_field',
1118 'default' => '',
1119 ],
1120 'orderby' => [
1121 'sanitize_callback' => 'sanitize_text_field',
1122 'default' => 'created_at',
1123 ],
1124 'order' => [
1125 'sanitize_callback' => 'sanitize_text_field',
1126 'default' => 'DESC',
1127 ],
1128 'per_page' => [
1129 'sanitize_callback' => 'absint',
1130 'default' => 20,
1131 ],
1132 'page' => [
1133 'sanitize_callback' => 'absint',
1134 'default' => 1,
1135 ],
1136 ],
1137 ],
1138 'entries/read-status' => [
1139 'methods' => 'POST',
1140 'callback' => [ $this, 'update_entries_read_status' ],
1141 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1142 'args' => [
1143 'entry_ids' => [
1144 'required' => true,
1145 'sanitize_callback' => [ $this, 'sanitize_entry_ids' ],
1146 ],
1147 'action' => [
1148 'required' => true,
1149 'sanitize_callback' => 'sanitize_text_field',
1150 'validate_callback' => [ $this, 'validate_read_action' ],
1151 ],
1152 ],
1153 ],
1154 'entries/trash' => [
1155 'methods' => 'POST',
1156 'callback' => [ $this, 'update_entries_trash_status' ],
1157 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1158 'args' => [
1159 'entry_ids' => [
1160 'required' => true,
1161 'sanitize_callback' => [ $this, 'sanitize_entry_ids' ],
1162 ],
1163 'action' => [
1164 'required' => true,
1165 'sanitize_callback' => 'sanitize_text_field',
1166 'validate_callback' => [ $this, 'validate_trash_action' ],
1167 ],
1168 ],
1169 ],
1170 'entries/delete' => [
1171 'methods' => 'POST',
1172 'callback' => [ $this, 'delete_entries' ],
1173 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1174 'args' => [
1175 'entry_ids' => [
1176 'required' => true,
1177 'sanitize_callback' => [ $this, 'sanitize_entry_ids' ],
1178 ],
1179 ],
1180 ],
1181 'entries/export' => [
1182 'methods' => 'POST',
1183 'callback' => [ $this, 'export_entries' ],
1184 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1185 'args' => [
1186 'entry_ids' => [
1187 'sanitize_callback' => [ $this, 'sanitize_entry_ids' ],
1188 'default' => [],
1189 ],
1190 'form_id' => [
1191 'sanitize_callback' => 'absint',
1192 'default' => 0,
1193 ],
1194 'status' => [
1195 'sanitize_callback' => 'sanitize_text_field',
1196 'default' => 'all',
1197 ],
1198 'search' => [
1199 'sanitize_callback' => 'sanitize_text_field',
1200 'default' => '',
1201 ],
1202 'date_from' => [
1203 'sanitize_callback' => 'sanitize_text_field',
1204 'default' => '',
1205 ],
1206 'date_to' => [
1207 'sanitize_callback' => 'sanitize_text_field',
1208 'default' => '',
1209 ],
1210 ],
1211 ],
1212 // Get Single Entry Form Data.
1213 'entry/(?P<id>\d+)/details' => [
1214 'methods' => 'GET',
1215 'callback' => [ $this, 'get_entry_details' ],
1216 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1217 'args' => [
1218 'id' => [
1219 'required' => true,
1220 'sanitize_callback' => 'absint',
1221 ],
1222 ],
1223 ],
1224 // Get Single Entry Logs.
1225 'entry/(?P<id>\d+)/logs' => [
1226 'methods' => 'GET',
1227 'callback' => [ $this, 'get_entry_logs' ],
1228 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1229 'args' => [
1230 'id' => [
1231 'required' => true,
1232 'sanitize_callback' => 'absint',
1233 ],
1234 'per_page' => [
1235 'sanitize_callback' => 'absint',
1236 'default' => 3,
1237 ],
1238 'page' => [
1239 'sanitize_callback' => 'absint',
1240 'default' => 1,
1241 ],
1242 ],
1243 ],
1244 // Forms listing endpoint.
1245 'forms' => [
1246 'methods' => 'GET',
1247 'callback' => [ Forms_Data::get_instance(), 'get_forms_list' ],
1248 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1249 'args' => [
1250 'page' => [
1251 'type' => 'integer',
1252 'default' => 1,
1253 'minimum' => 1,
1254 ],
1255 'per_page' => [
1256 'type' => 'integer',
1257 'minimum' => 1,
1258 'maximum' => 100,
1259 ],
1260 'search' => [
1261 'type' => 'string',
1262 ],
1263 'status' => [
1264 'type' => 'string',
1265 'enum' => [ 'publish', 'draft', 'trash', 'any' ],
1266 'default' => 'publish',
1267 ],
1268 'orderby' => [
1269 'type' => 'string',
1270 'default' => 'date',
1271 'enum' => [ 'date', 'id', 'title', 'modified' ],
1272 ],
1273 'order' => [
1274 'type' => 'string',
1275 'default' => 'desc',
1276 'enum' => [ 'asc', 'desc' ],
1277 ],
1278 'date_from' => [
1279 'type' => 'string',
1280 'format' => 'date',
1281 'sanitize_callback' => 'sanitize_text_field',
1282 'validate_callback' => static function( $value ) {
1283 if ( empty( $value ) ) {
1284 return true;
1285 }
1286 return (bool) strtotime( $value );
1287 },
1288 ],
1289 'date_to' => [
1290 'type' => 'string',
1291 'format' => 'date',
1292 'sanitize_callback' => 'sanitize_text_field',
1293 'validate_callback' => static function( $value ) {
1294 if ( empty( $value ) ) {
1295 return true;
1296 }
1297 return (bool) strtotime( $value );
1298 },
1299 ],
1300 ],
1301 ],
1302 // Export forms endpoint.
1303 'forms/export' => [
1304 'methods' => 'POST',
1305 'callback' => [ Export::get_instance(), 'handle_export_form_rest' ],
1306 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1307 'args' => [
1308 'post_ids' => [
1309 'required' => true,
1310 'type' => [ 'array', 'string' ],
1311 'sanitize_callback' => static function( $value ) {
1312 if ( is_array( $value ) ) {
1313 return array_map( 'intval', $value );
1314 }
1315 return sanitize_text_field( $value );
1316 },
1317 'validate_callback' => static function( $value ) {
1318 if ( is_array( $value ) ) {
1319 return ! empty( $value );
1320 }
1321 return ! empty( trim( $value ) );
1322 },
1323 ],
1324 ],
1325 ],
1326 // Import forms endpoint.
1327 'forms/import' => [
1328 'methods' => 'POST',
1329 'callback' => [ Export::get_instance(), 'handle_import_form_rest' ],
1330 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1331 'args' => [
1332 'forms_data' => [
1333 'required' => true,
1334 'type' => 'array',
1335 'validate_callback' => static function( $value ) {
1336 return is_array( $value ) && ! empty( $value );
1337 },
1338 ],
1339 'default_status' => [
1340 'required' => false,
1341 'type' => 'string',
1342 'default' => 'draft',
1343 'enum' => [ 'draft', 'publish', 'private' ],
1344 'sanitize_callback' => 'sanitize_text_field',
1345 ],
1346 ],
1347 ],
1348 // Form lifecycle management endpoint (trash/restore/delete).
1349 'forms/manage' => [
1350 'methods' => 'POST',
1351 'callback' => [ $this, 'manage_form_lifecycle' ],
1352 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1353 'args' => [
1354 'form_ids' => [
1355 'required' => true,
1356 'type' => [ 'array', 'integer' ],
1357 'sanitize_callback' => static function( $value ) {
1358 if ( is_array( $value ) ) {
1359 return array_map( 'intval', $value );
1360 }
1361 return [ intval( $value ) ];
1362 },
1363 'validate_callback' => static function( $value ) {
1364 if ( is_array( $value ) ) {
1365 return ! empty( $value );
1366 }
1367 return $value > 0;
1368 },
1369 ],
1370 'action' => [
1371 'required' => true,
1372 'type' => 'string',
1373 'enum' => [ 'trash', 'restore', 'delete' ],
1374 'sanitize_callback' => 'sanitize_text_field',
1375 ],
1376 ],
1377 ],
1378 // Form duplication endpoint.
1379 'forms/duplicate' => [
1380 'methods' => 'POST',
1381 'callback' => [ Duplicate_Form::get_instance(), 'handle_duplicate_form_rest' ],
1382 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1383 'args' => [
1384 'form_id' => [
1385 'required' => true,
1386 'type' => 'integer',
1387 'sanitize_callback' => 'absint',
1388 'validate_callback' => static function( $value ) {
1389 return $value > 0;
1390 },
1391 ],
1392 'title_suffix' => [
1393 'required' => false,
1394 'type' => 'string',
1395 'default' => __( ' (Copy)', 'sureforms' ),
1396 'sanitize_callback' => 'sanitize_text_field',
1397 ],
1398 ],
1399 ],
1400 ]
1401 );
1402 }
1403 }
1404