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

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