PluginProbe
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz / 2.0.2
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz v2.0.2
2.12.6 2.12.5 2.12.4 2.12.3 2.12.2 2.12.1 2.12.0 2.11.1 2.11.0 2.10.1 2.10.0 2.9.1 2.9.0 2.8.2 2.8.1 2.7.0 2.7.1 2.8.0 trunk 0.0.10 0.0.11 0.0.12 0.0.13 0.0.2 0.0.3 All 96 releases
sureforms / inc / rest-api.php
rest-api.php
1,375 lines 42.1 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 // Process form data.
515 $form_data = [];
516 $excluded_fields = [ 'srfm-honeypot-field', 'g-recaptcha-response', 'srfm-sender-email-field' ];
517 $entry_form_data = $entry['form_data'] ?? [];
518
519 if ( is_array( $entry_form_data ) ) {
520 foreach ( $entry_form_data as $field_name => $value ) {
521 if ( ! is_string( $field_name ) || in_array( $field_name, $excluded_fields, true ) ) {
522 continue;
523 }
524 if ( false === str_contains( $field_name, '-lbl-' ) ) {
525 continue;
526 }
527
528 $label_parts = explode( '-lbl-', $field_name );
529 $label = isset( $label_parts[1] ) ? explode( '-', $label_parts[1] )[0] : '';
530 $label = $label ? Helper::decrypt( $label ) : '';
531 $field_block_name = Helper::get_block_name_from_field( $field_name );
532
533 /**
534 * Filter: 'srfm_entry_value'
535 *
536 * This filter is used to allow 3rd party plugins or custom code to modify
537 * the entry field value in the entry details REST API response, if required.
538 * For example, you may want to decrypt, format, or mask sensitive data before output.
539 *
540 * @since 2.0.0
541 *
542 * @param mixed $value The original value for the field.
543 * @param array $context An array of context, including:
544 * - field_name (string)
545 * - label (string)
546 * - field_block_name (string)
547 *
548 * @return mixed
549 */
550 $value = apply_filters(
551 'srfm_entry_value',
552 $value,
553 [
554 'field_name' => $field_name,
555 'label' => $label,
556 'field_block_name' => $field_block_name,
557 ]
558 );
559
560 $form_data[] = [
561 'field_name' => $field_name,
562 'label' => $label,
563 'value' => $value,
564 'block_name' => $field_block_name,
565 ];
566 }
567 }
568
569 // Get user info.
570 $user_id = Helper::get_integer_value( $entry['user_id'] );
571 $user_info = 0 !== $user_id ? get_userdata( $user_id ) : null;
572
573 // Get form info.
574 $form_title = get_post_field( 'post_title', $entry['form_id'] );
575 // Translators: %d is the form ID.
576 $form_name = ! empty( $form_title ) ? $form_title : sprintf( __( 'SureForms Form #%d', 'sureforms' ), intval( $entry['form_id'] ) );
577
578 // Parse form content to get structured field data.
579 $form_content = get_post_field( 'post_content', $entry['form_id'] );
580 $form_fields = $this->parse_form_fields( $form_content, $entry['form_data'] ?? [] );
581
582 $response_data = [
583 'id' => $entry_id,
584 'form_id' => $entry['form_id'],
585 'form_name' => $form_name,
586 'form_permalink' => get_permalink( $entry['form_id'] ),
587 'status' => $entry['status'],
588 'created_at' => $entry['created_at'],
589 'form_data' => $form_data,
590 'form_content' => $form_fields,
591 'submission_info' => [
592 'user_ip' => $entry['submission_info']['user_ip'] ?? '',
593 'browser_name' => $entry['submission_info']['browser_name'] ?? '',
594 'device_name' => $entry['submission_info']['device_name'] ?? '',
595 ],
596 'user' => $user_info ? [
597 'id' => $user_id,
598 'display_name' => $user_info->display_name,
599 'profile_url' => get_author_posts_url( $user_id ),
600 ] : null,
601 'extras' => $entry['extras'] ?? [],
602 ];
603
604 return new \WP_REST_Response( $response_data, 200 );
605 }
606
607 /**
608 * Get entry logs with pagination support.
609 *
610 * @param \WP_REST_Request $request Full details about the request.
611 * @since 2.0.0
612 * @return \WP_REST_Response
613 */
614 public function get_entry_logs( $request ) {
615 $nonce = Helper::get_string_value( $request->get_header( 'X-WP-Nonce' ) );
616
617 if ( ! wp_verify_nonce( sanitize_text_field( $nonce ), 'wp_rest' ) ) {
618 return new \WP_REST_Response(
619 [ 'error' => __( 'Nonce verification failed.', 'sureforms' ) ],
620 403
621 );
622 }
623
624 $entry_id = absint( $request->get_param( 'id' ) );
625 $per_page = absint( $request->get_param( 'per_page' ) );
626 $per_page = $per_page ? $per_page : 3;
627 $page = absint( $request->get_param( 'page' ) );
628 $page = $page ? $page : 1;
629
630 if ( empty( $entry_id ) ) {
631 return new \WP_REST_Response(
632 [ 'error' => __( 'Entry ID is required.', 'sureforms' ) ],
633 400
634 );
635 }
636
637 $entry = Entries::get( $entry_id );
638
639 if ( ! $entry ) {
640 return new \WP_REST_Response(
641 [ 'error' => __( 'Entry not found.', 'sureforms' ) ],
642 404
643 );
644 }
645
646 $logs = $entry['logs'] ?? [];
647 $logs = is_array( $logs ) ? $logs : [];
648 $total_logs = count( $logs );
649 $total_pages = ceil( $total_logs / $per_page );
650 $offset = ( $page - 1 ) * $per_page;
651
652 // Paginate logs.
653 $paginated_logs = array_slice( $logs, $offset, $per_page );
654
655 // Format logs with unique IDs for deletion.
656 $formatted_logs = [];
657 foreach ( $paginated_logs as $index => $log ) {
658 if ( ! is_array( $log ) ) {
659 continue;
660 }
661 $formatted_logs[] = [
662 'id' => $offset + $index, // Use offset-based ID for consistent deletion.
663 'title' => $log['title'] ?? '',
664 'timestamp' => $log['timestamp'] ?? time(),
665 'messages' => $log['messages'] ?? [],
666 ];
667 }
668
669 $response_data = [
670 'logs' => $formatted_logs,
671 'current_page' => $page,
672 'per_page' => $per_page,
673 'total' => $total_logs,
674 'total_pages' => $total_pages,
675 ];
676
677 return new \WP_REST_Response( $response_data, 200 );
678 }
679
680 /**
681 * Export entries to CSV or ZIP.
682 *
683 * @param \WP_REST_Request $request Full details about the request.
684 * @since 2.0.0
685 * @return \WP_REST_Response
686 */
687 public function export_entries( $request ) {
688 $nonce = Helper::get_string_value( $request->get_header( 'X-WP-Nonce' ) );
689
690 if ( ! wp_verify_nonce( sanitize_text_field( $nonce ), 'wp_rest' ) ) {
691 return new \WP_REST_Response(
692 [ 'error' => __( 'Nonce verification failed.', 'sureforms' ) ],
693 403
694 );
695 }
696
697 $params = $request->get_params();
698
699 $args = [
700 'entry_ids' => isset( $params['entry_ids'] ) ? $this->sanitize_entry_ids( $params['entry_ids'] ) : [],
701 'form_id' => isset( $params['form_id'] ) ? absint( $params['form_id'] ) : 0,
702 'status' => isset( $params['status'] ) ? sanitize_text_field( $params['status'] ) : 'all',
703 'search' => isset( $params['search'] ) ? sanitize_text_field( $params['search'] ) : '',
704 'date_from' => isset( $params['date_from'] ) ? sanitize_text_field( $params['date_from'] ) : '',
705 'date_to' => isset( $params['date_to'] ) ? sanitize_text_field( $params['date_to'] ) : '',
706 ];
707
708 /**
709 * Export result with success status and either error message or file details.
710 *
711 * @var array{success: false, error: string} | array{success: true, filename: string, filepath: string, type: string} $result
712 */
713 $result = Entries_Class::export_entries( $args );
714
715 if ( ! $result['success'] ) {
716 return new \WP_REST_Response(
717 [ 'error' => $result['error'] ],
718 400
719 );
720 }
721
722 // Return file information for download.
723 $filepath = Helper::get_string_value( $result['filepath'] );
724 return new \WP_REST_Response(
725 [
726 'success' => true,
727 'filename' => $result['filename'],
728 'filepath' => $result['filepath'],
729 'type' => $result['type'],
730 'download_url' => add_query_arg(
731 '_wpnonce',
732 wp_create_nonce( 'srfm_download_export' ),
733 admin_url( 'admin-ajax.php?action=srfm_download_export&file=' . rawurlencode( basename( $filepath ) ) )
734 ),
735 ],
736 200
737 );
738 }
739 /**
740 * Manage form lifecycle operations (trash, restore, delete).
741 *
742 * @param \WP_REST_Request $request Full details about the request.
743 * @since 2.0.0
744 * @return \WP_REST_Response|\WP_Error
745 */
746 public function manage_form_lifecycle( $request ) {
747 $nonce = Helper::get_string_value( $request->get_header( 'X-WP-Nonce' ) );
748
749 if ( ! wp_verify_nonce( sanitize_text_field( $nonce ), 'wp_rest' ) ) {
750 return new \WP_Error(
751 'invalid_nonce',
752 __( 'Nonce verification failed.', 'sureforms' ),
753 [ 'status' => 403 ]
754 );
755 }
756
757 $params = $request->get_params();
758 $form_ids = isset( $params['form_ids'] ) && is_array( $params['form_ids'] ) ?
759 array_map( 'intval', $params['form_ids'] ) :
760 [ intval( $params['form_ids'] ) ];
761 $action = isset( $params['action'] ) ? sanitize_text_field( Helper::get_string_value( $params['action'] ) ) : '';
762
763 if ( empty( $form_ids ) || empty( $action ) ) {
764 return new \WP_Error(
765 'missing_parameters',
766 __( 'Form IDs and action are required.', 'sureforms' ),
767 [ 'status' => 400 ]
768 );
769 }
770
771 $results = [];
772 $errors = [];
773
774 foreach ( $form_ids as $form_id ) {
775 $post = get_post( $form_id );
776
777 // Validate that the post exists and is a sureforms_form.
778 if ( ! $post || 'sureforms_form' !== $post->post_type ) {
779 $errors[] = [
780 'form_id' => $form_id,
781 'error' => __( 'Form not found or invalid post type.', 'sureforms' ),
782 ];
783 continue;
784 }
785
786 $result = false;
787
788 switch ( $action ) {
789 case 'trash':
790 if ( 'trash' === $post->post_status ) {
791 $errors[] = [
792 'form_id' => $form_id,
793 'error' => __( 'Form is already in trash.', 'sureforms' ),
794 ];
795 } else {
796 $result = wp_trash_post( $form_id );
797 }
798 break;
799
800 case 'restore':
801 if ( 'trash' !== $post->post_status ) {
802 $errors[] = [
803 'form_id' => $form_id,
804 'error' => __( 'Form is not in trash.', 'sureforms' ),
805 ];
806 } else {
807 $result = wp_untrash_post( $form_id );
808 }
809 break;
810
811 case 'delete':
812 // Force delete permanently.
813 $result = wp_delete_post( $form_id, true );
814 break;
815
816 default:
817 $errors[] = [
818 'form_id' => $form_id,
819 'error' => __( 'Invalid action specified.', 'sureforms' ),
820 ];
821 break;
822 }
823
824 if ( $result ) {
825 $results[] = [
826 'form_id' => $form_id,
827 'action' => $action,
828 'success' => true,
829 ];
830 } elseif ( ! isset( $errors[ array_search( $form_id, array_column( $errors, 'form_id' ), true ) ] ) ) {
831 $errors[] = [
832 'form_id' => $form_id,
833 /* translators: %s: action name */
834 'error' => sprintf( __( 'Failed to %s form.', 'sureforms' ), $action ),
835 ];
836 }
837 }
838
839 $response_data = [
840 'success' => ! empty( $results ),
841 'action' => $action,
842 'processed_ids' => array_column( $results, 'form_id' ),
843 'success_count' => count( $results ),
844 'results' => $results,
845 ];
846
847 if ( ! empty( $errors ) ) {
848 $response_data['errors'] = $errors;
849 $response_data['error_count'] = count( $errors );
850 }
851
852 return new \WP_REST_Response( $response_data );
853 }
854
855 /**
856 * Recursively extract form fields from blocks.
857 *
858 * @param array<mixed> $blocks The blocks array.
859 * @param array<string, array<mixed>> $sureforms_blocks Registered SureForms block attributes.
860 * @param array<string, array<mixed>> &$form_fields Reference to form fields array.
861 * @param array<mixed> $entry_data The entry form data.
862 * @param bool $is_special_block Whether the current block is a special block (like address).
863 * @param int|null $base_counter Base counter for unique field naming.
864 * @since 2.0.0
865 * @return void
866 */
867 public function extract_form_fields( $blocks, $sureforms_blocks, &$form_fields, $entry_data = [], $is_special_block = false, $base_counter = null ) {
868 if ( null !== $base_counter ) {
869 self::$dropdown_counter = $base_counter;
870 }
871 $block_type = '';
872
873 foreach ( $blocks as $block ) {
874 if ( ! is_array( $block ) || ! isset( $block['blockName'] ) || ! is_string( $block['blockName'] ) ) {
875 continue;
876 }
877
878 // Check if it's a SureForms block.
879 if ( strpos( $block['blockName'], 'srfm/' ) === 0 ) {
880 $block_type = str_replace( 'srfm/', '', $block['blockName'] );
881 // Skip inline button or fields inside nested blocks except address.
882 if ( 'inline-button' === $block_type || ( $is_special_block && 'address' !== $block_type ) ) {
883 continue;
884 }
885
886 if ( isset( $sureforms_blocks[ $block_type ] ) && is_array( $sureforms_blocks[ $block_type ] ) ) {
887 $block_attributes = isset( $block['attrs'] ) && is_array( $block['attrs'] ) ? $block['attrs'] : [];
888 $default_attributes = $sureforms_blocks[ $block_type ];
889
890 // Merge block instance attributes with defaults.
891 $merged_attributes = [];
892 foreach ( $default_attributes as $attr_name => $attr_config ) {
893 if ( ! is_string( $attr_name ) ) {
894 continue;
895 }
896 $default_value = null;
897 if ( is_array( $attr_config ) && isset( $attr_config['default'] ) ) {
898 $default_value = $attr_config['default'];
899 }
900 $merged_attributes[ $attr_name ] = $block_attributes[ $attr_name ] ?? $default_value;
901 }
902
903 // Generate field name.
904 $label = is_string( $merged_attributes['label'] ?? '' ) ? $merged_attributes['label'] : '';
905 $slug = is_string( $merged_attributes['slug'] ?? '' ) ? $merged_attributes['slug'] : '';
906 $block_id = is_string( $merged_attributes['block_id'] ?? '' ) ? $merged_attributes['block_id'] : '';
907 $field_name = '';
908 $base_field_name = '';
909
910 if ( ! empty( $label ) && ! empty( $slug ) && ! empty( $block_id ) ) {
911 $input_label = '-lbl-' . Helper::encrypt( $label );
912 $base_field_name = $input_label . '-' . $slug;
913
914 // Handle special case for dropdown with instance counter.
915 if ( 'dropdown' === $block_type ) {
916 self::$dropdown_counter++;
917 $unique_slug = $block_type . '-' . self::$dropdown_counter;
918 $field_name = 'srfm-' . $unique_slug . '-' . $block_id . $base_field_name;
919 } elseif ( 'multi-choice' === $block_type ) {
920 // Multi-choice uses standard pattern.
921 $field_name = 'srfm-input-' . $block_type . '-' . $block_id . $base_field_name;
922 } else {
923 // Standard field name for other blocks.
924 $field_name = 'srfm-' . $block_type . '-' . $block_id . $base_field_name;
925 }
926 }
927
928 // Allow pro plugin to modify field_name.
929 $field_name = apply_filters( 'srfm_extract_form_fields_field_name', $field_name, $base_field_name, $block_type, $block_id );
930
931 // Get the value from entry data or use default.
932 $field_value = $entry_data[ $field_name ] ?? ( $merged_attributes['defaultValue'] ?? '' );
933
934 // Special handling for address blocks - extract inner fields.
935 if ( 'address' === $block_type && isset( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) {
936 $inner_fields = [];
937 $this->extract_form_fields( $block['innerBlocks'], $sureforms_blocks, $inner_fields, $entry_data, false );
938 $field_value = $inner_fields;
939 }
940
941 // Allow plugins to handle special blocks.
942 $field_value = apply_filters( 'srfm_handle_special_block', $field_value, $block_type, $block, $sureforms_blocks, $this );
943
944 $form_fields[] = [
945 'field_name' => $field_name,
946 'block_name' => 'multi-choice' === $block_type ? 'srfm-multi' : Helper::get_block_name_from_field( $field_name ),
947 'value' => $field_value,
948 'attributes' => $merged_attributes,
949 ];
950 }
951 }
952
953 // Recursively process inner blocks but skip for address blocks.
954 if ( isset( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) && ! empty( $block['innerBlocks'] ) && 'address' !== $block_type ) {
955 // Pass true if current block has inner blocks and it doesn't need to be duplicated in the main fields array.
956 $inner_is_special_block = apply_filters( 'srfm_is_special_block', false, $block_type );
957 $this->extract_form_fields( $block['innerBlocks'], $sureforms_blocks, $form_fields, $entry_data, $inner_is_special_block );
958 }
959 }
960 }
961
962 /**
963 * Get current dropdown counter value.
964 *
965 * @since 2.0.0
966 * @return int Current dropdown counter value.
967 */
968 public function get_dropdown_counter() {
969 return self::$dropdown_counter;
970 }
971
972 /**
973 * Parse form content and return structured field data with attributes.
974 *
975 * @param string $form_content The form post content.
976 * @param array<mixed> $entry_data The entry form data.
977 * @since 2.0.0
978 * @return array<string, array<mixed>>
979 */
980 private function parse_form_fields( $form_content, $entry_data = [] ) {
981 if ( empty( $form_content ) ) {
982 return [];
983 }
984
985 // Parse blocks from form content.
986 $blocks = parse_blocks( $form_content );
987 if ( empty( $blocks ) ) {
988 return [];
989 }
990
991 // Get registered SureForms block attributes.
992 $registry = \WP_Block_Type_Registry::get_instance();
993 $registered_blocks = $registry->get_all_registered();
994
995 $sureforms_blocks = [];
996 foreach ( $registered_blocks as $block_name => $block_type ) {
997 if ( strpos( $block_name, 'srfm/' ) === 0 && is_array( $block_type->attributes ) ) {
998 $block_key = str_replace( 'srfm/', '', $block_name );
999 $sureforms_blocks[ $block_key ] = $block_type->attributes;
1000 }
1001 }
1002
1003 $form_fields = [];
1004 $this->extract_form_fields( $blocks, $sureforms_blocks, $form_fields, $entry_data, false );
1005
1006 return $form_fields;
1007 }
1008
1009 /**
1010 * Get endpoints
1011 *
1012 * @since 0.0.7
1013 * @return array<array<mixed>>
1014 */
1015 private function get_endpoints() {
1016 /*
1017 * @internal This filter is used to add custom endpoints.
1018 * @since 1.2.0
1019 * @param array<array<mixed>> $endpoints Endpoints.
1020 */
1021 return apply_filters(
1022 'srfm_rest_api_endpoints',
1023 [
1024 'generate-form' => [
1025 'methods' => 'POST',
1026 'callback' => [ AI_Form_Builder::get_instance(), 'generate_ai_form' ],
1027 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1028 'args' => [
1029 'use_system_message' => [
1030 'sanitize_callback' => [ $this, 'sanitize_boolean_field' ],
1031 ],
1032 ],
1033 ],
1034 // This route is used to map the AI response to SureForms fields markup.
1035 'map-fields' => [
1036 'methods' => 'POST',
1037 'callback' => [ Field_Mapping::get_instance(), 'generate_gutenberg_fields_from_questions' ],
1038 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1039 ],
1040 // This route is used to initiate auth process when user tries to authenticate on billing portal.
1041 'initiate-auth' => [
1042 'methods' => 'GET',
1043 'callback' => [ AI_Auth::get_instance(), 'get_auth_url' ],
1044 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1045 ],
1046 // This route is to used to decrypt the access key and save it in the database.
1047 'handle-access-key' => [
1048 'methods' => 'POST',
1049 'callback' => [ AI_Auth::get_instance(), 'handle_access_key' ],
1050 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1051 ],
1052 // This route is to get the form submissions for the last 30 days.
1053 'entries-chart-data' => [
1054 'methods' => 'GET',
1055 'callback' => [ $this, 'get_entries_chart_data' ],
1056 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1057 ],
1058 // This route is to get all forms data.
1059 'form-data' => [
1060 'methods' => 'GET',
1061 'callback' => [ $this, 'get_form_data' ],
1062 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1063 ],
1064 // Onboarding endpoints.
1065 'onboarding/set-status' => [
1066 'methods' => 'POST',
1067 'callback' => [ $this, 'set_onboarding_status' ],
1068 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1069 ],
1070 'onboarding/get-status' => [
1071 'methods' => 'GET',
1072 'callback' => [ $this, 'get_onboarding_status' ],
1073 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1074 ],
1075 // Plugin status endpoint.
1076 'plugin-status' => [
1077 'methods' => 'GET',
1078 'callback' => [ $this, 'get_plugin_status' ],
1079 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1080 'args' => [
1081 'plugin' => [
1082 'required' => true,
1083 'sanitize_callback' => 'sanitize_text_field',
1084 ],
1085 ],
1086 ],
1087 // Entries endpoints.
1088 'entries/list' => [
1089 'methods' => 'GET',
1090 'callback' => [ $this, 'get_entries_list' ],
1091 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1092 'args' => [
1093 'form_id' => [
1094 'sanitize_callback' => 'absint',
1095 'default' => 0,
1096 ],
1097 'status' => [
1098 'sanitize_callback' => 'sanitize_text_field',
1099 'default' => 'all',
1100 ],
1101 'search' => [
1102 'sanitize_callback' => 'sanitize_text_field',
1103 'default' => '',
1104 ],
1105 'date_from' => [
1106 'sanitize_callback' => 'sanitize_text_field',
1107 'default' => '',
1108 ],
1109 'date_to' => [
1110 'sanitize_callback' => 'sanitize_text_field',
1111 'default' => '',
1112 ],
1113 'orderby' => [
1114 'sanitize_callback' => 'sanitize_text_field',
1115 'default' => 'created_at',
1116 ],
1117 'order' => [
1118 'sanitize_callback' => 'sanitize_text_field',
1119 'default' => 'DESC',
1120 ],
1121 'per_page' => [
1122 'sanitize_callback' => 'absint',
1123 'default' => 20,
1124 ],
1125 'page' => [
1126 'sanitize_callback' => 'absint',
1127 'default' => 1,
1128 ],
1129 ],
1130 ],
1131 'entries/read-status' => [
1132 'methods' => 'POST',
1133 'callback' => [ $this, 'update_entries_read_status' ],
1134 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1135 'args' => [
1136 'entry_ids' => [
1137 'required' => true,
1138 'sanitize_callback' => [ $this, 'sanitize_entry_ids' ],
1139 ],
1140 'action' => [
1141 'required' => true,
1142 'sanitize_callback' => 'sanitize_text_field',
1143 'validate_callback' => [ $this, 'validate_read_action' ],
1144 ],
1145 ],
1146 ],
1147 'entries/trash' => [
1148 'methods' => 'POST',
1149 'callback' => [ $this, 'update_entries_trash_status' ],
1150 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1151 'args' => [
1152 'entry_ids' => [
1153 'required' => true,
1154 'sanitize_callback' => [ $this, 'sanitize_entry_ids' ],
1155 ],
1156 'action' => [
1157 'required' => true,
1158 'sanitize_callback' => 'sanitize_text_field',
1159 'validate_callback' => [ $this, 'validate_trash_action' ],
1160 ],
1161 ],
1162 ],
1163 'entries/delete' => [
1164 'methods' => 'POST',
1165 'callback' => [ $this, 'delete_entries' ],
1166 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1167 'args' => [
1168 'entry_ids' => [
1169 'required' => true,
1170 'sanitize_callback' => [ $this, 'sanitize_entry_ids' ],
1171 ],
1172 ],
1173 ],
1174 'entries/export' => [
1175 'methods' => 'POST',
1176 'callback' => [ $this, 'export_entries' ],
1177 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1178 'args' => [
1179 'entry_ids' => [
1180 'sanitize_callback' => [ $this, 'sanitize_entry_ids' ],
1181 'default' => [],
1182 ],
1183 'form_id' => [
1184 'sanitize_callback' => 'absint',
1185 'default' => 0,
1186 ],
1187 'status' => [
1188 'sanitize_callback' => 'sanitize_text_field',
1189 'default' => 'all',
1190 ],
1191 'search' => [
1192 'sanitize_callback' => 'sanitize_text_field',
1193 'default' => '',
1194 ],
1195 'date_from' => [
1196 'sanitize_callback' => 'sanitize_text_field',
1197 'default' => '',
1198 ],
1199 'date_to' => [
1200 'sanitize_callback' => 'sanitize_text_field',
1201 'default' => '',
1202 ],
1203 ],
1204 ],
1205 // Get Single Entry Form Data.
1206 'entry/(?P<id>\d+)/details' => [
1207 'methods' => 'GET',
1208 'callback' => [ $this, 'get_entry_details' ],
1209 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1210 'args' => [
1211 'id' => [
1212 'required' => true,
1213 'sanitize_callback' => 'absint',
1214 ],
1215 ],
1216 ],
1217 // Get Single Entry Logs.
1218 'entry/(?P<id>\d+)/logs' => [
1219 'methods' => 'GET',
1220 'callback' => [ $this, 'get_entry_logs' ],
1221 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1222 'args' => [
1223 'id' => [
1224 'required' => true,
1225 'sanitize_callback' => 'absint',
1226 ],
1227 'per_page' => [
1228 'sanitize_callback' => 'absint',
1229 'default' => 3,
1230 ],
1231 'page' => [
1232 'sanitize_callback' => 'absint',
1233 'default' => 1,
1234 ],
1235 ],
1236 ],
1237 // Forms listing endpoint.
1238 'forms' => [
1239 'methods' => 'GET',
1240 'callback' => [ Forms_Data::get_instance(), 'get_forms_list' ],
1241 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1242 'args' => [
1243 'page' => [
1244 'type' => 'integer',
1245 'default' => 1,
1246 'minimum' => 1,
1247 ],
1248 'per_page' => [
1249 'type' => 'integer',
1250 'minimum' => 1,
1251 'maximum' => 100,
1252 ],
1253 'search' => [
1254 'type' => 'string',
1255 ],
1256 'status' => [
1257 'type' => 'string',
1258 'enum' => [ 'publish', 'draft', 'trash', 'any' ],
1259 'default' => 'publish',
1260 ],
1261 'orderby' => [
1262 'type' => 'string',
1263 'default' => 'date',
1264 'enum' => [ 'date', 'id', 'title', 'modified' ],
1265 ],
1266 'order' => [
1267 'type' => 'string',
1268 'default' => 'desc',
1269 'enum' => [ 'asc', 'desc' ],
1270 ],
1271 'date_from' => [
1272 'type' => 'string',
1273 'format' => 'date',
1274 'sanitize_callback' => 'sanitize_text_field',
1275 'validate_callback' => static function( $value ) {
1276 if ( empty( $value ) ) {
1277 return true;
1278 }
1279 return (bool) strtotime( $value );
1280 },
1281 ],
1282 'date_to' => [
1283 'type' => 'string',
1284 'format' => 'date',
1285 'sanitize_callback' => 'sanitize_text_field',
1286 'validate_callback' => static function( $value ) {
1287 if ( empty( $value ) ) {
1288 return true;
1289 }
1290 return (bool) strtotime( $value );
1291 },
1292 ],
1293 ],
1294 ],
1295 // Export forms endpoint.
1296 'forms/export' => [
1297 'methods' => 'POST',
1298 'callback' => [ Export::get_instance(), 'handle_export_form_rest' ],
1299 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1300 'args' => [
1301 'post_ids' => [
1302 'required' => true,
1303 'type' => [ 'array', 'string' ],
1304 'sanitize_callback' => static function( $value ) {
1305 if ( is_array( $value ) ) {
1306 return array_map( 'intval', $value );
1307 }
1308 return sanitize_text_field( $value );
1309 },
1310 'validate_callback' => static function( $value ) {
1311 if ( is_array( $value ) ) {
1312 return ! empty( $value );
1313 }
1314 return ! empty( trim( $value ) );
1315 },
1316 ],
1317 ],
1318 ],
1319 // Import forms endpoint.
1320 'forms/import' => [
1321 'methods' => 'POST',
1322 'callback' => [ Export::get_instance(), 'handle_import_form_rest' ],
1323 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1324 'args' => [
1325 'forms_data' => [
1326 'required' => true,
1327 'type' => 'array',
1328 'validate_callback' => static function( $value ) {
1329 return is_array( $value ) && ! empty( $value );
1330 },
1331 ],
1332 'default_status' => [
1333 'required' => false,
1334 'type' => 'string',
1335 'default' => 'draft',
1336 'enum' => [ 'draft', 'publish', 'private' ],
1337 'sanitize_callback' => 'sanitize_text_field',
1338 ],
1339 ],
1340 ],
1341 // Form lifecycle management endpoint (trash/restore/delete).
1342 'forms/manage' => [
1343 'methods' => 'POST',
1344 'callback' => [ $this, 'manage_form_lifecycle' ],
1345 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1346 'args' => [
1347 'form_ids' => [
1348 'required' => true,
1349 'type' => [ 'array', 'integer' ],
1350 'sanitize_callback' => static function( $value ) {
1351 if ( is_array( $value ) ) {
1352 return array_map( 'intval', $value );
1353 }
1354 return [ intval( $value ) ];
1355 },
1356 'validate_callback' => static function( $value ) {
1357 if ( is_array( $value ) ) {
1358 return ! empty( $value );
1359 }
1360 return $value > 0;
1361 },
1362 ],
1363 'action' => [
1364 'required' => true,
1365 'type' => 'string',
1366 'enum' => [ 'trash', 'restore', 'delete' ],
1367 'sanitize_callback' => 'sanitize_text_field',
1368 ],
1369 ],
1370 ],
1371 ]
1372 );
1373 }
1374 }
1375