PluginProbe
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz / 1.9.1
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz v1.9.1
2.12.6 2.12.5 2.12.4 2.12.3 2.12.2 2.12.1 2.12.0 2.11.1 2.11.0 2.10.1 2.10.0 2.9.1 2.9.0 2.8.2 2.8.1 2.7.0 2.7.1 2.8.0 trunk 0.0.10 0.0.11 0.0.12 0.0.13 0.0.2 0.0.3 All 96 releases
← All changes | inc/rest-api.php +37 -1636 trunk1.9.1 View file →
@@ -10,9 +10,8 @@
10 10 use SRFM\Inc\AI_Form_Builder\AI_Auth;
11 11 use SRFM\Inc\AI_Form_Builder\AI_Form_Builder;
12 12 use SRFM\Inc\AI_Form_Builder\Field_Mapping;
13 13 use SRFM\Inc\Database\Tables\Entries;
14 -use SRFM\Inc\Entries as Entries_Class;
15 14 use SRFM\Inc\Traits\Get_Instance;
16 15
17 16 if ( ! defined( 'ABSPATH' ) ) {
18 17 exit; // Exit if accessed directly.
@@ -26,23 +25,8 @@
26 25 class Rest_Api {
27 26 use Get_Instance;
28 27
29 28 /**
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 29 * Constructor
46 30 *
47 31 * @since 0.0.7
48 32 * @return void
@@ -73,8 +57,18 @@
73 57 }
74 58 }
75 59
76 60 /**
61 + * Check if user can edit posts
62 + *
63 + * @since 0.0.7
64 + * @return bool
65 + */
66 + public function can_edit_posts() {
67 + return current_user_can( 'edit_posts' );
68 + }
69 +
70 + /**
77 71 * Checks whether the value is boolean or not.
78 72 *
79 73 * @param mixed $value value to be checked.
80 74 * @since 0.0.8
@@ -84,34 +78,8 @@
84 78 return filter_var( $value, FILTER_VALIDATE_BOOLEAN );
85 79 }
86 80
87 81 /**
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 82 * Get the data for generating entries chart.
115 83 *
116 84 * @param \WP_REST_Request $request Full details about the request.
117 85 * @since 1.0.0
@@ -120,15 +88,15 @@
120 88 public function get_entries_chart_data( $request ) {
121 89 $nonce = Helper::get_string_value( $request->get_header( 'X-WP-Nonce' ) );
122 90
123 91 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' ) );
92 + wp_send_json_error( __( 'Nonce verification failed.', 'sureforms' ) );
125 93 }
126 94
127 95 $params = $request->get_params();
128 96
129 97 if ( empty( $params ) ) {
130 - wp_send_json_error( __( 'Missing required parameters.', 'sureforms' ) );
98 + wp_send_json_error( __( 'Request could not be processed.', 'sureforms' ) );
131 99 }
132 100
133 101 $after = is_array( $params ) && ! empty( $params['after'] ) ? sanitize_text_field( Helper::get_string_value( $params['after'] ) ) : '';
134 102 $before = is_array( $params ) && ! empty( $params['before'] ) ? sanitize_text_field( Helper::get_string_value( $params['before'] ) ) : '';
@@ -133,9 +101,9 @@
133 101 $after = is_array( $params ) && ! empty( $params['after'] ) ? sanitize_text_field( Helper::get_string_value( $params['after'] ) ) : '';
134 102 $before = is_array( $params ) && ! empty( $params['before'] ) ? sanitize_text_field( Helper::get_string_value( $params['before'] ) ) : '';
135 103
136 104 if ( empty( $after ) || empty( $before ) ) {
137 - wp_send_json_error( __( 'Invalid date range.', 'sureforms' ) );
105 + wp_send_json_error( __( 'Invalid date.', 'sureforms' ) );
138 106 }
139 107
140 108 $form = is_array( $params ) && ! empty( $params['form'] ) ? sanitize_text_field( Helper::get_string_value( $params['form'] ) ) : '';
141 109
@@ -179,9 +147,9 @@
179 147 public function get_form_data( $request ) {
180 148 $nonce = Helper::get_string_value( $request->get_header( 'X-WP-Nonce' ) );
181 149
182 150 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' ) );
151 + wp_send_json_error( __( 'Nonce verification failed.', 'sureforms' ) );
184 152 }
185 153
186 154 $forms = Helper::get_instance()->get_sureforms();
187 155
@@ -188,154 +156,8 @@
188 156 return ! empty( $forms ) ? $forms : [];
189 157 }
190 158
191 159 /**
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 160 * Set onboarding completion status.
339 161 *
340 162 * @param \WP_REST_Request $request Full details about the request.
341 163 * @since 1.9.1
@@ -345,9 +167,9 @@
345 167 $nonce = Helper::get_string_value( $request->get_header( 'X-WP-Nonce' ) );
346 168
347 169 if ( ! wp_verify_nonce( sanitize_text_field( $nonce ), 'wp_rest' ) ) {
348 170 return new \WP_REST_Response(
349 - [ 'error' => __( 'Security verification failed. Please refresh the page and try again.', 'sureforms' ) ],
171 + [ 'error' => __( 'Nonce verification failed.', 'sureforms' ) ],
350 172 403
351 173 );
352 174 }
353 175
@@ -377,9 +199,9 @@
377 199 $nonce = Helper::get_string_value( $request->get_header( 'X-WP-Nonce' ) );
378 200
379 201 if ( ! wp_verify_nonce( sanitize_text_field( $nonce ), 'wp_rest' ) ) {
380 202 return new \WP_REST_Response(
381 - [ 'error' => __( 'Security verification failed. Please refresh the page and try again.', 'sureforms' ) ],
203 + [ 'error' => __( 'Nonce verification failed.', 'sureforms' ) ],
382 204 403
383 205 );
384 206 }
385 207
@@ -388,113 +210,8 @@
388 210 return new \WP_REST_Response( [ 'completed' => $status ] );
389 211 }
390 212
391 213 /**
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 214 * Get plugin status for specified plugin.
498 215 *
499 216 * @param \WP_REST_Request $request Full details about the request.
500 217 * @since 1.9.1
@@ -504,9 +221,9 @@
504 221 $nonce = Helper::get_string_value( $request->get_header( 'X-WP-Nonce' ) );
505 222
506 223 if ( ! wp_verify_nonce( sanitize_text_field( $nonce ), 'wp_rest' ) ) {
507 224 return new \WP_REST_Response(
508 - [ 'error' => __( 'Security verification failed. Please refresh the page and try again.', 'sureforms' ) ],
225 + [ 'error' => __( 'Nonce verification failed.', 'sureforms' ) ],
509 226 403
510 227 );
511 228 }
512 229
@@ -515,9 +232,9 @@
515 232 sanitize_text_field( Helper::get_string_value( $params['plugin'] ) ) : '';
516 233
517 234 if ( empty( $plugin_slug ) ) {
518 235 return new \WP_REST_Response(
519 - [ 'error' => __( 'Plugin identifier is required.', 'sureforms' ) ],
236 + [ 'error' => __( 'Plugin slug is required.', 'sureforms' ) ],
520 237 400
521 238 );
522 239 }
523 240
@@ -524,9 +241,9 @@
524 241 $integrations = Helper::sureforms_get_integration();
525 242
526 243 if ( ! isset( $integrations[ $plugin_slug ] ) ) {
527 244 return new \WP_REST_Response(
528 - [ 'error' => __( 'Integration not found.', 'sureforms' ) ],
245 + [ 'error' => __( 'Plugin not found.', 'sureforms' ) ],
529 246 404
530 247 );
531 248 }
532 249
@@ -540,850 +257,8 @@
540 257 return new \WP_REST_Response( $plugin_data );
541 258 }
542 259
543 260 /**
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::decode( $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 format, mask, or otherwise transform sensitive
827 - * data before output. Note the value reaching this filter is not encrypted by
828 - * SureForms — labels and values are carried as unkeyed base64 at most.
829 - *
830 - * @since 2.0.0
831 - *
832 - * @param mixed $value The original value for the field.
833 - * @param array $context An array of context, including:
834 - * - field_name (string)
835 - * - label (string)
836 - * - field_block_name (string)
837 - *
838 - * @return mixed
839 - */
840 - $value = apply_filters(
841 - 'srfm_entry_value',
842 - $value,
843 - [
844 - 'field_name' => $field_name,
845 - 'label' => $label,
846 - 'field_block_name' => $field_block_name,
847 - ]
848 - );
849 -
850 - $form_data[] = [
851 - 'field_name' => $field_name,
852 - 'label' => $label,
853 - 'value' => $value,
854 - 'block_name' => $field_block_name,
855 - ];
856 - }
857 - }
858 -
859 - // Get user info.
860 - $user_id = Helper::get_integer_value( $entry['user_id'] );
861 - $user_info = 0 !== $user_id ? get_userdata( $user_id ) : null;
862 -
863 - // Get form info.
864 - $form_title = get_post_field( 'post_title', $entry['form_id'] );
865 - // Translators: %d is the form ID.
866 - $form_name = ! empty( $form_title ) ? $form_title : sprintf( __( 'SureForms Form #%d', 'sureforms' ), intval( $entry['form_id'] ) );
867 -
868 - // Parse form content to get structured field data.
869 - $form_content = get_post_field( 'post_content', $entry['form_id'] );
870 - $form_fields = $this->parse_form_fields( $form_content, $entry['form_data'] ?? [] );
871 -
872 - $response_data = [
873 - 'id' => $entry_id,
874 - 'form_id' => $entry['form_id'],
875 - 'form_name' => $form_name,
876 - 'form_permalink' => get_permalink( $entry['form_id'] ),
877 - 'status' => $entry['status'],
878 - 'created_at' => $entry['created_at'],
879 - 'form_data' => $form_data,
880 - 'form_content' => $form_fields,
881 - 'submission_info' => [
882 - 'user_ip' => $entry['submission_info']['user_ip'] ?? '',
883 - 'browser_name' => $entry['submission_info']['browser_name'] ?? '',
884 - 'device_name' => $entry['submission_info']['device_name'] ?? '',
885 - // Re-sanitize at the exposure boundary in case the stored value
886 - // was written by a future code path that bypasses form-submit.php.
887 - 'submission_url' => esc_url_raw( $entry['submission_info']['submission_url'] ?? '', [ 'http', 'https' ] ),
888 - ],
889 - 'user' => $user_info ? [
890 - 'id' => $user_id,
891 - 'display_name' => $user_info->display_name,
892 - 'profile_url' => get_author_posts_url( $user_id ),
893 - ] : null,
894 - 'extras' => $entry['extras'] ?? [],
895 - 'navigation' => [
896 - 'previous_entry_id' => $adjacent_entries['previous_id'] ?? null,
897 - 'next_entry_id' => $adjacent_entries['next_id'] ?? null,
898 - ],
899 - ];
900 -
901 - return new \WP_REST_Response( $response_data, 200 );
902 - }
903 -
904 - /**
905 - * Get entry logs with pagination support.
906 - *
907 - * @param \WP_REST_Request $request Full details about the request.
908 - * @since 2.0.0
909 - * @return \WP_REST_Response
910 - */
911 - public function get_entry_logs( $request ) {
912 - $nonce = Helper::get_string_value( $request->get_header( 'X-WP-Nonce' ) );
913 -
914 - if ( ! wp_verify_nonce( sanitize_text_field( $nonce ), 'wp_rest' ) ) {
915 - return new \WP_REST_Response(
916 - [ 'error' => __( 'Security verification failed. Please refresh the page and try again.', 'sureforms' ) ],
917 - 403
918 - );
919 - }
920 -
921 - $entry_id = absint( $request->get_param( 'id' ) );
922 - $per_page = absint( $request->get_param( 'per_page' ) );
923 - $per_page = $per_page ? $per_page : 3;
924 - $page = absint( $request->get_param( 'page' ) );
925 - $page = $page ? $page : 1;
926 -
927 - if ( empty( $entry_id ) ) {
928 - return new \WP_REST_Response(
929 - [ 'error' => __( 'Entry ID is required.', 'sureforms' ) ],
930 - 400
931 - );
932 - }
933 -
934 - $entry = Entries::get( $entry_id );
935 -
936 - if ( ! $entry ) {
937 - return new \WP_REST_Response(
938 - [ 'error' => __( 'Entry not found.', 'sureforms' ) ],
939 - 404
940 - );
941 - }
942 -
943 - $logs = $entry['logs'] ?? [];
944 - $logs = is_array( $logs ) ? $logs : [];
945 - $total_logs = count( $logs );
946 - $total_pages = ceil( $total_logs / $per_page );
947 - $offset = ( $page - 1 ) * $per_page;
948 -
949 - // Paginate logs.
950 - $paginated_logs = array_slice( $logs, $offset, $per_page );
951 -
952 - // Format logs with unique IDs for deletion.
953 - $formatted_logs = [];
954 - foreach ( $paginated_logs as $index => $log ) {
955 - if ( ! is_array( $log ) ) {
956 - continue;
957 - }
958 - $formatted_log = [
959 - 'id' => $offset + $index, // Use offset-based ID for consistent deletion.
960 - 'title' => $log['title'] ?? '',
961 - 'timestamp' => $log['timestamp'] ?? time(),
962 - 'messages' => $log['messages'] ?? [],
963 - ];
964 -
965 - // Pass through (sanitized) retry metadata so an integration/webhook log row can
966 - // offer a "Retry" action for that specific failed trigger. Set by the Pro
967 - // webhook / native-integration dispatchers as [ 'type' => webhook|native, 'id' => <trigger id> ].
968 - if ( isset( $log['retry'] ) && is_array( $log['retry'] ) && ! empty( $log['retry']['type'] ) && isset( $log['retry']['id'] ) ) {
969 - $formatted_log['retry'] = [
970 - 'type' => sanitize_text_field( Helper::get_string_value( $log['retry']['type'] ) ),
971 - 'id' => sanitize_text_field( Helper::get_string_value( $log['retry']['id'] ) ),
972 - ];
973 - }
974 -
975 - $formatted_logs[] = $formatted_log;
976 - }
977 -
978 - $response_data = [
979 - 'logs' => $formatted_logs,
980 - 'current_page' => $page,
981 - 'per_page' => $per_page,
982 - 'total' => $total_logs,
983 - 'total_pages' => $total_pages,
984 - ];
985 -
986 - return new \WP_REST_Response( $response_data, 200 );
987 - }
988 -
989 - /**
990 - * Export entries to CSV or ZIP.
991 - *
992 - * @param \WP_REST_Request $request Full details about the request.
993 - * @since 2.0.0
994 - * @return \WP_REST_Response
995 - */
996 - public function export_entries( $request ) {
997 - $nonce = Helper::get_string_value( $request->get_header( 'X-WP-Nonce' ) );
998 -
999 - if ( ! wp_verify_nonce( sanitize_text_field( $nonce ), 'wp_rest' ) ) {
1000 - return new \WP_REST_Response(
1001 - [ 'error' => __( 'Security verification failed. Please refresh the page and try again.', 'sureforms' ) ],
1002 - 403
1003 - );
1004 - }
1005 -
1006 - $params = $request->get_params();
1007 -
1008 - $args = [
1009 - 'entry_ids' => isset( $params['entry_ids'] ) ? $this->sanitize_entry_ids( $params['entry_ids'] ) : [],
1010 - 'form_id' => isset( $params['form_id'] ) ? absint( $params['form_id'] ) : 0,
1011 - 'status' => isset( $params['status'] ) ? sanitize_text_field( $params['status'] ) : 'all',
1012 - 'search' => isset( $params['search'] ) ? sanitize_text_field( $params['search'] ) : '',
1013 - 'date_from' => isset( $params['date_from'] ) ? sanitize_text_field( $params['date_from'] ) : '',
1014 - 'date_to' => isset( $params['date_to'] ) ? sanitize_text_field( $params['date_to'] ) : '',
1015 - ];
1016 -
1017 - /**
1018 - * Export result with success status and either error message or file details.
1019 - *
1020 - * @var array{success: false, error: string} | array{success: true, filename: string, filepath: string, type: string} $result
1021 - */
1022 - $result = Entries_Class::export_entries( $args );
1023 -
1024 - if ( ! $result['success'] ) {
1025 - return new \WP_REST_Response(
1026 - [ 'error' => $result['error'] ],
1027 - 400
1028 - );
1029 - }
1030 -
1031 - // Return file information for download.
1032 - $filepath = Helper::get_string_value( $result['filepath'] );
1033 - return new \WP_REST_Response(
1034 - [
1035 - 'success' => true,
1036 - 'filename' => $result['filename'],
1037 - 'filepath' => $result['filepath'],
1038 - 'type' => $result['type'],
1039 - 'download_url' => add_query_arg(
1040 - '_wpnonce',
1041 - wp_create_nonce( 'srfm_download_export' ),
1042 - admin_url( 'admin-ajax.php?action=srfm_download_export&file=' . rawurlencode( basename( $filepath ) ) )
1043 - ),
1044 - ],
1045 - 200
1046 - );
1047 - }
1048 - /**
1049 - * Manage form lifecycle operations (trash, restore, delete).
1050 - *
1051 - * @param \WP_REST_Request $request Full details about the request.
1052 - * @since 2.0.0
1053 - * @return \WP_REST_Response|\WP_Error
1054 - */
1055 - public function manage_form_lifecycle( $request ) {
1056 - $nonce = Helper::get_string_value( $request->get_header( 'X-WP-Nonce' ) );
1057 -
1058 - if ( ! wp_verify_nonce( sanitize_text_field( $nonce ), 'wp_rest' ) ) {
1059 - return new \WP_Error(
1060 - 'invalid_nonce',
1061 - __( 'Security verification failed. Please refresh the page and try again.', 'sureforms' ),
1062 - [ 'status' => 403 ]
1063 - );
1064 - }
1065 -
1066 - $params = $request->get_params();
1067 - $form_ids = isset( $params['form_ids'] ) && is_array( $params['form_ids'] ) ?
1068 - array_map( 'intval', $params['form_ids'] ) :
1069 - [ intval( $params['form_ids'] ) ];
1070 - $action = isset( $params['action'] ) ? sanitize_text_field( Helper::get_string_value( $params['action'] ) ) : '';
1071 -
1072 - if ( empty( $form_ids ) || empty( $action ) ) {
1073 - return new \WP_Error(
1074 - 'missing_parameters',
1075 - __( 'Select at least one form and specify an action.', 'sureforms' ),
1076 - [ 'status' => 400 ]
1077 - );
1078 - }
1079 -
1080 - $results = [];
1081 - $errors = [];
1082 -
1083 - foreach ( $form_ids as $form_id ) {
1084 - $post = get_post( $form_id );
1085 -
1086 - // Validate that the post exists and is a sureforms_form.
1087 - if ( ! $post || 'sureforms_form' !== $post->post_type ) {
1088 - $errors[] = [
1089 - 'form_id' => $form_id,
1090 - 'error' => __( 'Form not found or is not a valid form type.', 'sureforms' ),
1091 - ];
1092 - continue;
1093 - }
1094 -
1095 - $result = false;
1096 -
1097 - switch ( $action ) {
1098 - case 'trash':
1099 - if ( 'trash' === $post->post_status ) {
1100 - $errors[] = [
1101 - 'form_id' => $form_id,
1102 - 'error' => __( 'This form is already in the trash.', 'sureforms' ),
1103 - ];
1104 - } else {
1105 - $result = wp_trash_post( $form_id );
1106 - }
1107 - break;
1108 -
1109 - case 'restore':
1110 - if ( 'trash' !== $post->post_status ) {
1111 - $errors[] = [
1112 - 'form_id' => $form_id,
1113 - 'error' => __( 'This form is not in the trash.', 'sureforms' ),
1114 - ];
1115 - } else {
1116 - $result = wp_untrash_post( $form_id );
1117 - }
1118 - break;
1119 -
1120 - case 'delete':
1121 - // Force delete permanently.
1122 - $result = wp_delete_post( $form_id, true );
1123 - break;
1124 -
1125 - case 'draft':
1126 - if ( 'trash' === $post->post_status ) {
1127 - $errors[] = [
1128 - 'form_id' => $form_id,
1129 - 'error' => __( 'Use the restore action to recover a trashed form before switching it to draft.', 'sureforms' ),
1130 - ];
1131 - } elseif ( 'draft' === $post->post_status ) {
1132 - $errors[] = [
1133 - 'form_id' => $form_id,
1134 - 'error' => __( 'This form is already a draft.', 'sureforms' ),
1135 - ];
1136 - } else {
1137 - $result = wp_update_post(
1138 - [
1139 - 'ID' => $form_id,
1140 - 'post_status' => 'draft',
1141 - ]
1142 - );
1143 - }
1144 - break;
1145 -
1146 - default:
1147 - $errors[] = [
1148 - 'form_id' => $form_id,
1149 - 'error' => __( 'Invalid action.', 'sureforms' ),
1150 - ];
1151 - break;
1152 - }
1153 -
1154 - if ( $result ) {
1155 - $results[] = [
1156 - 'form_id' => $form_id,
1157 - 'action' => $action,
1158 - 'success' => true,
1159 - ];
1160 - } elseif ( ! isset( $errors[ array_search( $form_id, array_column( $errors, 'form_id' ), true ) ] ) ) {
1161 - $errors[] = [
1162 - 'form_id' => $form_id,
1163 - /* translators: %s: action name */
1164 - 'error' => sprintf( __( 'Failed to %s this form. Please try again.', 'sureforms' ), $action ),
1165 - ];
1166 - }
1167 - }
1168 -
1169 - $response_data = [
1170 - 'success' => ! empty( $results ),
1171 - 'action' => $action,
1172 - 'processed_ids' => array_column( $results, 'form_id' ),
1173 - 'success_count' => count( $results ),
1174 - 'results' => $results,
1175 - ];
1176 -
1177 - if ( ! empty( $errors ) ) {
1178 - $response_data['errors'] = $errors;
1179 - $response_data['error_count'] = count( $errors );
1180 - }
1181 -
1182 - return new \WP_REST_Response( $response_data );
1183 - }
1184 -
1185 - /**
1186 - * Recursively extract form fields from blocks.
1187 - *
1188 - * @param array<mixed> $blocks The blocks array.
1189 - * @param array<string, array<mixed>> $sureforms_blocks Registered SureForms block attributes.
1190 - * @param array<string, array<mixed>> &$form_fields Reference to form fields array.
1191 - * @param array<mixed> $entry_data The entry form data.
1192 - * @param bool $is_special_block Whether the current block is a special block (like address).
1193 - * @param int|null $base_counter Base counter for unique field naming.
1194 - * @since 2.0.0
1195 - * @return void
1196 - */
1197 - public function extract_form_fields( $blocks, $sureforms_blocks, &$form_fields, $entry_data = [], $is_special_block = false, $base_counter = null ) {
1198 - if ( null !== $base_counter ) {
1199 - self::$dropdown_counter = $base_counter;
1200 - }
1201 - $block_type = '';
1202 -
1203 - foreach ( $blocks as $block ) {
1204 - if ( ! is_array( $block ) || ! isset( $block['blockName'] ) || ! is_string( $block['blockName'] ) ) {
1205 - continue;
1206 - }
1207 -
1208 - // Check if it's a SureForms block.
1209 - if ( strpos( $block['blockName'], 'srfm/' ) === 0 ) {
1210 - $block_type = str_replace( 'srfm/', '', $block['blockName'] );
1211 - // Skip inline button or fields inside nested blocks except address.
1212 - if ( 'inline-button' === $block_type || ( $is_special_block && 'address' !== $block_type ) ) {
1213 - continue;
1214 - }
1215 -
1216 - if ( isset( $sureforms_blocks[ $block_type ] ) && is_array( $sureforms_blocks[ $block_type ] ) ) {
1217 - $block_attributes = isset( $block['attrs'] ) && is_array( $block['attrs'] ) ? $block['attrs'] : [];
1218 - $default_attributes = $sureforms_blocks[ $block_type ];
1219 -
1220 - // Merge block instance attributes with defaults.
1221 - $merged_attributes = [];
1222 - foreach ( $default_attributes as $attr_name => $attr_config ) {
1223 - if ( ! is_string( $attr_name ) ) {
1224 - continue;
1225 - }
1226 - $default_value = null;
1227 - if ( is_array( $attr_config ) && isset( $attr_config['default'] ) ) {
1228 - $default_value = $attr_config['default'];
1229 - }
1230 - $merged_attributes[ $attr_name ] = $block_attributes[ $attr_name ] ?? $default_value;
1231 - }
1232 -
1233 - // Generate field name.
1234 - $label = $merged_attributes['label'] ?? '';
1235 - $label = is_string( $label ) ? $label : '';
1236 - $slug = $merged_attributes['slug'] ?? '';
1237 - $slug = is_string( $slug ) ? $slug : '';
1238 - $block_id = $merged_attributes['block_id'] ?? '';
1239 - $block_id = is_string( $block_id ) ? $block_id : '';
1240 - $field_name = '';
1241 - $base_field_name = '';
1242 -
1243 - if ( ! empty( $label ) && ! empty( $slug ) && ! empty( $block_id ) ) {
1244 - $input_label = '-lbl-' . Helper::encode( $label );
1245 - $base_field_name = $input_label . '-' . $slug;
1246 -
1247 - // Handle special case for dropdown with instance counter.
1248 - if ( 'dropdown' === $block_type ) {
1249 - self::$dropdown_counter++;
1250 - $unique_slug = $block_type . '-' . self::$dropdown_counter;
1251 - $field_name = 'srfm-' . $unique_slug . '-' . $block_id . $base_field_name;
1252 - } elseif ( 'multi-choice' === $block_type ) {
1253 - // Multi-choice uses standard pattern.
1254 - $field_name = 'srfm-input-' . $block_type . '-' . $block_id . $base_field_name;
1255 - } else {
1256 - // Standard field name for other blocks.
1257 - $field_name = 'srfm-' . $block_type . '-' . $block_id . $base_field_name;
1258 - }
1259 - }
1260 -
1261 - // Allow pro plugin to modify field_name.
1262 - $field_name = apply_filters( 'srfm_extract_form_fields_field_name', $field_name, $base_field_name, $block_type, $block_id );
1263 -
1264 - // Get the value from entry data or use default.
1265 - $field_value = $entry_data[ $field_name ] ?? ( $merged_attributes['defaultValue'] ?? '' );
1266 -
1267 - // Special handling for address blocks - extract inner fields.
1268 - if ( 'address' === $block_type && isset( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) {
1269 - $inner_fields = [];
1270 - $this->extract_form_fields( $block['innerBlocks'], $sureforms_blocks, $inner_fields, $entry_data, false );
1271 - $field_value = $inner_fields;
1272 - }
1273 -
1274 - // Allow plugins to handle special blocks.
1275 - $field_value = apply_filters( 'srfm_handle_special_block', $field_value, $block_type, $block, $sureforms_blocks, $this );
1276 -
1277 - $form_fields[] = [
1278 - 'field_name' => $field_name,
1279 - 'block_name' => 'multi-choice' === $block_type ? 'srfm-multi' : Helper::get_block_name_from_field( $field_name ),
1280 - 'value' => $field_value,
1281 - 'attributes' => $merged_attributes,
1282 - ];
1283 - }
1284 - }
1285 -
1286 - // Recursively process inner blocks but skip for address blocks.
1287 - if ( isset( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) && ! empty( $block['innerBlocks'] ) && 'address' !== $block_type ) {
1288 - // Pass true if current block has inner blocks and it doesn't need to be duplicated in the main fields array.
1289 - $inner_is_special_block = apply_filters( 'srfm_is_special_block', false, $block_type );
1290 - $this->extract_form_fields( $block['innerBlocks'], $sureforms_blocks, $form_fields, $entry_data, $inner_is_special_block );
1291 - }
1292 - }
1293 - }
1294 -
1295 - /**
1296 - * Get current dropdown counter value.
1297 - *
1298 - * @since 2.0.0
1299 - * @return int Current dropdown counter value.
1300 - */
1301 - public function get_dropdown_counter() {
1302 - return self::$dropdown_counter;
1303 - }
1304 -
1305 - /**
1306 - * Get onboarding user details.
1307 - *
1308 - * @since 2.5.3
1309 - * @return array<string, mixed>
1310 - */
1311 - private function get_onboarding_user_details() {
1312 - $defaults = [
1313 - 'first_name' => '',
1314 - 'last_name' => '',
1315 - 'email' => '',
1316 - 'lead' => false,
1317 - ];
1318 -
1319 - $user_details = Helper::get_srfm_option( $this->onboarding_user_details_key, $defaults );
1320 - if ( ! is_array( $user_details ) ) {
1321 - return $defaults;
1322 - }
1323 -
1324 - return wp_parse_args( $user_details, $defaults );
1325 - }
1326 -
1327 - /**
1328 - * Set onboarding user details.
1329 - *
1330 - * @since 2.5.3
1331 - * @param array<string, mixed> $user_details User details to store.
1332 - * @return void
1333 - */
1334 - private function set_onboarding_user_details( $user_details ) {
1335 - $details = wp_parse_args(
1336 - $user_details,
1337 - [
1338 - 'first_name' => '',
1339 - 'last_name' => '',
1340 - 'email' => '',
1341 - 'lead' => false,
1342 - ]
1343 - );
1344 -
1345 - Helper::update_srfm_option( $this->onboarding_user_details_key, $details );
1346 - }
1347 -
1348 - /**
1349 - * Parse form content and return structured field data with attributes.
1350 - *
1351 - * @param string $form_content The form post content.
1352 - * @param array<mixed> $entry_data The entry form data.
1353 - * @since 2.0.0
1354 - * @return array<string, array<mixed>>
1355 - */
1356 - private function parse_form_fields( $form_content, $entry_data = [] ) {
1357 - if ( empty( $form_content ) ) {
1358 - return [];
1359 - }
1360 -
1361 - // Parse blocks from form content.
1362 - $blocks = parse_blocks( $form_content );
1363 - if ( empty( $blocks ) ) {
1364 - return [];
1365 - }
1366 -
1367 - // Get registered SureForms block attributes.
1368 - $registry = \WP_Block_Type_Registry::get_instance();
1369 - $registered_blocks = $registry->get_all_registered();
1370 -
1371 - $sureforms_blocks = [];
1372 - foreach ( $registered_blocks as $block_name => $block_type ) {
1373 - if ( strpos( $block_name, 'srfm/' ) === 0 && is_array( $block_type->attributes ) ) {
1374 - $block_key = str_replace( 'srfm/', '', $block_name );
1375 - $sureforms_blocks[ $block_key ] = $block_type->attributes;
1376 - }
1377 - }
1378 -
1379 - $form_fields = [];
1380 - $this->extract_form_fields( $blocks, $sureforms_blocks, $form_fields, $entry_data, false );
1381 -
1382 - return $form_fields;
1383 - }
1384 -
1385 - /**
1386 261 * Get endpoints
1387 262 *
1388 263 * @since 0.0.7
1389 264 * @return array<array<mixed>>
@@ -1396,12 +271,12 @@
1396 271 */
1397 272 return apply_filters(
1398 273 'srfm_rest_api_endpoints',
1399 274 [
1400 - 'generate-form' => [
275 + 'generate-form' => [
1401 276 'methods' => 'POST',
1402 277 'callback' => [ AI_Form_Builder::get_instance(), 'generate_ai_form' ],
1403 - 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
278 + 'permission_callback' => [ $this, 'can_edit_posts' ],
1404 279 'args' => [
1405 280 'use_system_message' => [
1406 281 'sanitize_callback' => [ $this, 'sanitize_boolean_field' ],
1407 282 ],
@@ -1407,530 +282,56 @@
1407 282 ],
1408 283 ],
1409 284 ],
1410 285 // This route is used to map the AI response to SureForms fields markup.
1411 - 'map-fields' => [
286 + 'map-fields' => [
1412 287 'methods' => 'POST',
1413 288 'callback' => [ Field_Mapping::get_instance(), 'generate_gutenberg_fields_from_questions' ],
1414 - 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
289 + 'permission_callback' => [ $this, 'can_edit_posts' ],
1415 290 ],
1416 - // Recreate the entries table when it has gone missing. The repair is
1417 - // idempotent (CREATE TABLE IF NOT EXISTS) so a double-click is safe.
1418 - 'database/repair-entries-table' => [
1419 - 'methods' => 'POST',
1420 - /**
1421 - * Resolved at dispatch, not while the route table is built:
1422 - * get_endpoints() runs on rest_api_init for every REST request,
1423 - * and Admin is only constructed under is_admin(). Naming the
1424 - * instance here would run Admin's constructor on the front-end
1425 - * submit path too.
1426 - *
1427 - * @param \WP_REST_Request<array<string,mixed>> $request Request.
1428 - * @return \WP_REST_Response|\WP_Error
1429 - */
1430 - 'callback' => static function ( $request ) {
1431 - $nonce = Helper::get_string_value( $request->get_header( 'X-WP-Nonce' ) );
1432 -
1433 - if ( ! wp_verify_nonce( sanitize_text_field( $nonce ), 'wp_rest' ) ) {
1434 - return new \WP_Error(
1435 - 'rest_cookie_invalid_nonce',
1436 - __( 'Security verification failed. Please refresh the page and try again.', 'sureforms' ),
1437 - [ 'status' => 403 ]
1438 - );
1439 - }
1440 -
1441 - // @phpstan-ignore-next-line -- PHPStan resolves SRFM\Admin\Admin via tests/php/stubs/srfm-stubs.php (admin/ is outside its `paths`) and that generated stub predates this method. Real location: admin/admin.php.
1442 - $repaired = \SRFM\Admin\Admin::get_instance()->do_database_repair();
1443 -
1444 - if ( ! $repaired ) {
1445 - return new \WP_Error(
1446 - 'srfm_database_repair_failed',
1447 - __( 'SureForms could not finish updating the database. Your hosting may not allow SureForms to create database tables — please contact your hosting provider or SureForms support.', 'sureforms' ),
1448 - [ 'status' => 500 ]
1449 - );
1450 - }
1451 -
1452 - return new \WP_REST_Response( [ 'success' => true ], 200 );
1453 - },
1454 - 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1455 - ],
1456 - // Record a "Finish setting up" card CTA click for a form (#3031).
1457 - // Per-form capability is re-checked in the handler.
1458 - 'dismiss-form-setup-card' => [
1459 - 'methods' => 'POST',
1460 - /**
1461 - * Resolve Admin at dispatch rather than while the route table is
1462 - * built. get_endpoints() runs on rest_api_init for *every* REST
1463 - * request, and plugin-loader.php only constructs Admin under
1464 - * is_admin() — which REST dispatch is not. Naming the instance
1465 - * here would therefore run Admin's constructor (40 admin hook
1466 - * registrations, an option read, the notices library, and the
1467 - * wpforms_current_user_can filter) on the front-end
1468 - * submit-form path too.
1469 - *
1470 - * @param \WP_REST_Request<array<string,mixed>> $request Request.
1471 - * @return \WP_REST_Response|\WP_Error
1472 - */
1473 - 'callback' => static function ( $request ) {
1474 - // @phpstan-ignore-next-line -- PHPStan resolves SRFM\Admin\Admin via tests/php/stubs/srfm-stubs.php (admin/ is outside its `paths`) and that generated stub predates this method. Real location: admin/admin.php:470.
1475 - return \SRFM\Admin\Admin::get_instance()->dismiss_form_setup_card( $request );
1476 - },
1477 - 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1478 - 'args' => [
1479 - 'form_id' => [
1480 - 'required' => true,
1481 - 'sanitize_callback' => 'absint',
1482 - ],
1483 - 'action' => [
1484 - 'required' => true,
1485 - 'type' => 'string',
1486 - 'enum' => [ 'edit_form', 'edit_thankyou', 'set_up_email', 'view_form' ],
1487 - // Core only enforces `enum` via the default arg sanitizer, which
1488 - // is skipped once a sanitize_callback is set — so pair it with an
1489 - // explicit validate_callback, matching this file's other routes.
1490 - 'validate_callback' => 'rest_validate_request_arg',
1491 - 'sanitize_callback' => 'sanitize_text_field',
1492 - ],
1493 - ],
1494 - ],
1495 291 // This route is used to initiate auth process when user tries to authenticate on billing portal.
1496 - 'initiate-auth' => [
292 + 'initiate-auth' => [
1497 293 'methods' => 'GET',
1498 294 'callback' => [ AI_Auth::get_instance(), 'get_auth_url' ],
1499 - 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
295 + 'permission_callback' => [ $this, 'can_edit_posts' ],
1500 296 ],
1501 297 // This route is to used to decrypt the access key and save it in the database.
1502 - 'handle-access-key' => [
298 + 'handle-access-key' => [
1503 299 'methods' => 'POST',
1504 300 'callback' => [ AI_Auth::get_instance(), 'handle_access_key' ],
1505 - 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
301 + 'permission_callback' => [ $this, 'can_edit_posts' ],
1506 302 ],
1507 - // Public route: returns the visitor's detected country code. Called
1508 - // per-visitor from the phone field so auto-country detection works
1509 - // on full-page-cached sites (the value isn't baked into cached HTML).
1510 - 'geo-country' => [
1511 - 'methods' => 'GET',
1512 - 'callback' => [ $this, 'get_geo_country' ],
1513 - 'permission_callback' => '__return_true',
1514 - ],
1515 303 // This route is to get the form submissions for the last 30 days.
1516 - 'entries-chart-data' => [
304 + 'entries-chart-data' => [
1517 305 'methods' => 'GET',
1518 306 'callback' => [ $this, 'get_entries_chart_data' ],
1519 - 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
307 + 'permission_callback' => [ $this, 'can_edit_posts' ],
1520 308 ],
1521 309 // This route is to get all forms data.
1522 - 'form-data' => [
310 + 'form-data' => [
1523 311 'methods' => 'GET',
1524 312 'callback' => [ $this, 'get_form_data' ],
1525 - 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
313 + 'permission_callback' => [ $this, 'can_edit_posts' ],
1526 314 ],
1527 - // Page search endpoint for async admin dropdowns.
1528 - 'pages/search' => [
1529 - 'methods' => 'GET',
1530 - 'callback' => [ $this, 'search_pages' ],
1531 - 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1532 - 'args' => [
1533 - 'search' => [
1534 - 'sanitize_callback' => 'sanitize_text_field',
1535 - 'default' => '',
1536 - ],
1537 - 'page' => [
1538 - 'sanitize_callback' => 'absint',
1539 - 'default' => 1,
1540 - 'validate_callback' => static function ( $value ) {
1541 - return is_numeric( $value ) && (int) $value >= 1;
1542 - },
1543 - ],
1544 - 'per_page' => [
1545 - 'sanitize_callback' => 'absint',
1546 - 'default' => 20,
1547 - 'validate_callback' => static function ( $value ) {
1548 - return is_numeric( $value ) && (int) $value >= 1 && (int) $value <= 50;
1549 - },
1550 - ],
1551 - 'selected_url' => [
1552 - 'sanitize_callback' => 'esc_url_raw',
1553 - 'default' => '',
1554 - 'validate_callback' => static function ( $value ) {
1555 - return empty( $value ) || false !== filter_var( $value, FILTER_VALIDATE_URL );
1556 - },
1557 - ],
1558 - 'selected_urls' => [
1559 - 'default' => [],
1560 - 'sanitize_callback' => static function( $value ) {
1561 - if ( is_array( $value ) ) {
1562 - return array_values( array_filter( array_map( 'esc_url_raw', $value ) ) );
1563 - }
1564 - if ( is_string( $value ) ) {
1565 - return array_values(
1566 - array_filter(
1567 - array_map(
1568 - 'esc_url_raw',
1569 - array_map( 'trim', explode( ',', $value ) )
1570 - )
1571 - )
1572 - );
1573 - }
1574 - return [];
1575 - },
1576 - ],
1577 - ],
1578 - ],
1579 315 // Onboarding endpoints.
1580 - 'onboarding/set-status' => [
316 + 'onboarding/set-status' => [
1581 317 'methods' => 'POST',
1582 318 'callback' => [ $this, 'set_onboarding_status' ],
1583 - 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
319 + 'permission_callback' => [ $this, 'can_edit_posts' ],
1584 320 ],
1585 - 'onboarding/get-status' => [
321 + 'onboarding/get-status' => [
1586 322 'methods' => 'GET',
1587 323 'callback' => [ $this, 'get_onboarding_status' ],
1588 - 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
324 + 'permission_callback' => [ $this, 'can_edit_posts' ],
1589 325 ],
1590 - 'onboarding/user-details' => [
1591 - 'methods' => 'POST',
1592 - 'callback' => [ $this, 'save_onboarding_user_details' ],
1593 - 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1594 - 'args' => [
1595 - 'first_name' => [
1596 - 'required' => true,
1597 - 'sanitize_callback' => 'sanitize_text_field',
1598 - 'validate_callback' => static function( $value ) {
1599 - return is_string( $value ) && '' !== trim( $value );
1600 - },
1601 - ],
1602 - 'last_name' => [
1603 - 'required' => false,
1604 - 'sanitize_callback' => 'sanitize_text_field',
1605 - ],
1606 - 'email' => [
1607 - 'required' => true,
1608 - 'sanitize_callback' => 'sanitize_email',
1609 - 'validate_callback' => static function( $value ) {
1610 - return is_string( $value ) && is_email( $value );
1611 - },
1612 - ],
1613 - ],
1614 - ],
1615 326 // Plugin status endpoint.
1616 - 'plugin-status' => [
327 + 'plugin-status' => [
1617 328 'methods' => 'GET',
1618 329 'callback' => [ $this, 'get_plugin_status' ],
1619 - 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
330 + 'permission_callback' => [ $this, 'can_edit_posts' ],
1620 331 'args' => [
1621 332 'plugin' => [
1622 333 'required' => true,
1623 - 'sanitize_callback' => 'sanitize_text_field',
1624 - ],
1625 - ],
1626 - ],
1627 - // Entries endpoints.
1628 - 'entries/list' => [
1629 - 'methods' => 'GET',
1630 - 'callback' => [ $this, 'get_entries_list' ],
1631 - 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1632 - 'args' => [
1633 - 'form_id' => [
1634 - 'sanitize_callback' => 'absint',
1635 - 'default' => 0,
1636 - ],
1637 - 'status' => [
1638 - 'sanitize_callback' => 'sanitize_text_field',
1639 - 'default' => 'all',
1640 - ],
1641 - 'search' => [
1642 - 'sanitize_callback' => 'sanitize_text_field',
1643 - 'default' => '',
1644 - ],
1645 - 'date_from' => [
1646 - 'sanitize_callback' => 'sanitize_text_field',
1647 - 'default' => '',
1648 - ],
1649 - 'date_to' => [
1650 - 'sanitize_callback' => 'sanitize_text_field',
1651 - 'default' => '',
1652 - ],
1653 - 'orderby' => [
1654 - 'type' => 'string',
1655 - 'sanitize_callback' => 'sanitize_text_field',
1656 - 'default' => 'created_at',
1657 - 'enum' => [ 'ID', 'id', 'form_id', 'user_id', 'status', 'type', 'created_at', 'updated_at' ],
1658 - ],
1659 - 'order' => [
1660 - 'type' => 'string',
1661 - 'sanitize_callback' => 'sanitize_text_field',
1662 - 'default' => 'DESC',
1663 - 'enum' => [ 'ASC', 'DESC' ],
1664 - ],
1665 - 'per_page' => [
1666 - 'sanitize_callback' => 'absint',
1667 - 'default' => 20,
1668 - ],
1669 - 'page' => [
1670 - 'sanitize_callback' => 'absint',
1671 - 'default' => 1,
1672 - ],
1673 - ],
1674 - ],
1675 - 'entries/read-status' => [
1676 - 'methods' => 'POST',
1677 - 'callback' => [ $this, 'update_entries_read_status' ],
1678 - 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1679 - 'args' => [
1680 - 'entry_ids' => [
1681 - 'required' => true,
1682 - 'sanitize_callback' => [ $this, 'sanitize_entry_ids' ],
1683 - ],
1684 - 'action' => [
1685 - 'required' => true,
1686 - 'sanitize_callback' => 'sanitize_text_field',
1687 - 'validate_callback' => [ $this, 'validate_read_action' ],
1688 - ],
1689 - ],
1690 - ],
1691 - 'entries/trash' => [
1692 - 'methods' => 'POST',
1693 - 'callback' => [ $this, 'update_entries_trash_status' ],
1694 - 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1695 - 'args' => [
1696 - 'entry_ids' => [
1697 - 'required' => true,
1698 - 'sanitize_callback' => [ $this, 'sanitize_entry_ids' ],
1699 - ],
1700 - 'action' => [
1701 - 'required' => true,
1702 - 'sanitize_callback' => 'sanitize_text_field',
1703 - 'validate_callback' => [ $this, 'validate_trash_action' ],
1704 - ],
1705 - ],
1706 - ],
1707 - 'entries/delete' => [
1708 - 'methods' => 'POST',
1709 - 'callback' => [ $this, 'delete_entries' ],
1710 - 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1711 - 'args' => [
1712 - 'entry_ids' => [
1713 - 'required' => true,
1714 - 'sanitize_callback' => [ $this, 'sanitize_entry_ids' ],
1715 - ],
1716 - ],
1717 - ],
1718 - 'entries/export' => [
1719 - 'methods' => 'POST',
1720 - 'callback' => [ $this, 'export_entries' ],
1721 - 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1722 - 'args' => [
1723 - 'entry_ids' => [
1724 - 'sanitize_callback' => [ $this, 'sanitize_entry_ids' ],
1725 - 'default' => [],
1726 - ],
1727 - 'form_id' => [
1728 - 'sanitize_callback' => 'absint',
1729 - 'default' => 0,
1730 - ],
1731 - 'status' => [
1732 - 'sanitize_callback' => 'sanitize_text_field',
1733 - 'default' => 'all',
1734 - ],
1735 - 'search' => [
1736 - 'sanitize_callback' => 'sanitize_text_field',
1737 - 'default' => '',
1738 - ],
1739 - 'date_from' => [
1740 - 'sanitize_callback' => 'sanitize_text_field',
1741 - 'default' => '',
1742 - ],
1743 - 'date_to' => [
1744 - 'sanitize_callback' => 'sanitize_text_field',
1745 - 'default' => '',
1746 - ],
1747 - ],
1748 - ],
1749 - // Get Single Entry Form Data.
1750 - 'entry/(?P<id>\d+)/details' => [
1751 - 'methods' => 'GET',
1752 - 'callback' => [ $this, 'get_entry_details' ],
1753 - 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1754 - 'args' => [
1755 - 'id' => [
1756 - 'required' => true,
1757 - 'sanitize_callback' => 'absint',
1758 - ],
1759 - ],
1760 - ],
1761 - // Get Single Entry Logs.
1762 - 'entry/(?P<id>\d+)/logs' => [
1763 - 'methods' => 'GET',
1764 - 'callback' => [ $this, 'get_entry_logs' ],
1765 - 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1766 - 'args' => [
1767 - 'id' => [
1768 - 'required' => true,
1769 - 'sanitize_callback' => 'absint',
1770 - ],
1771 - 'per_page' => [
1772 - 'sanitize_callback' => 'absint',
1773 - 'default' => 3,
1774 - ],
1775 - 'page' => [
1776 - 'sanitize_callback' => 'absint',
1777 - 'default' => 1,
1778 - ],
1779 - ],
1780 - ],
1781 - // Forms listing endpoint.
1782 - 'forms' => [
1783 - 'methods' => 'GET',
1784 - 'callback' => [ Forms_Data::get_instance(), 'get_forms_list' ],
1785 - 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1786 - 'args' => [
1787 - 'page' => [
1788 - 'type' => 'integer',
1789 - 'default' => 1,
1790 - 'minimum' => 1,
1791 - ],
1792 - 'per_page' => [
1793 - 'type' => 'integer',
1794 - 'minimum' => 1,
1795 - 'maximum' => 100,
1796 - ],
1797 - 'search' => [
1798 - 'type' => 'string',
1799 - ],
1800 - 'status' => [
1801 - 'type' => 'string',
1802 - 'enum' => [ 'publish', 'draft', 'trash', 'any' ],
1803 - 'default' => 'publish',
1804 - ],
1805 - 'orderby' => [
1806 - 'type' => 'string',
1807 - 'default' => 'date',
1808 - 'enum' => [ 'date', 'id', 'title', 'modified', 'views', 'conversion_rate' ],
1809 - ],
1810 - 'order' => [
1811 - 'type' => 'string',
1812 - 'default' => 'desc',
1813 - 'enum' => [ 'asc', 'desc' ],
1814 - ],
1815 - 'date_from' => [
1816 - 'type' => 'string',
1817 - 'format' => 'date',
1818 - 'sanitize_callback' => 'sanitize_text_field',
1819 - 'validate_callback' => static function( $value ) {
1820 - if ( empty( $value ) ) {
1821 - return true;
1822 - }
1823 - return (bool) strtotime( $value );
1824 - },
1825 - ],
1826 - 'date_to' => [
1827 - 'type' => 'string',
1828 - 'format' => 'date',
1829 - 'sanitize_callback' => 'sanitize_text_field',
1830 - 'validate_callback' => static function( $value ) {
1831 - if ( empty( $value ) ) {
1832 - return true;
1833 - }
1834 - return (bool) strtotime( $value );
1835 - },
1836 - ],
1837 - ],
1838 - ],
1839 - // Export forms endpoint.
1840 - 'forms/export' => [
1841 - 'methods' => 'POST',
1842 - 'callback' => [ Export::get_instance(), 'handle_export_form_rest' ],
1843 - 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1844 - 'args' => [
1845 - 'post_ids' => [
1846 - 'required' => true,
1847 - 'type' => [ 'array', 'string' ],
1848 - 'sanitize_callback' => static function( $value ) {
1849 - if ( is_array( $value ) ) {
1850 - return array_map( 'intval', $value );
1851 - }
1852 - return sanitize_text_field( $value );
1853 - },
1854 - 'validate_callback' => static function( $value ) {
1855 - if ( is_array( $value ) ) {
1856 - return ! empty( $value );
1857 - }
1858 - return ! empty( trim( $value ) );
1859 - },
1860 - ],
1861 - ],
1862 - ],
1863 - // Import forms endpoint.
1864 - 'forms/import' => [
1865 - 'methods' => 'POST',
1866 - 'callback' => [ Export::get_instance(), 'handle_import_form_rest' ],
1867 - 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1868 - 'args' => [
1869 - 'forms_data' => [
1870 - 'required' => true,
1871 - 'type' => 'array',
1872 - 'validate_callback' => static function( $value ) {
1873 - return is_array( $value ) && ! empty( $value );
1874 - },
1875 - ],
1876 - 'default_status' => [
1877 - 'required' => false,
1878 - 'type' => 'string',
1879 - 'default' => 'draft',
1880 - 'enum' => [ 'draft', 'publish', 'private' ],
1881 - 'sanitize_callback' => 'sanitize_text_field',
1882 - ],
1883 - ],
1884 - ],
1885 - // Form lifecycle management endpoint (trash/restore/delete/draft).
1886 - 'forms/manage' => [
1887 - 'methods' => 'POST',
1888 - 'callback' => [ $this, 'manage_form_lifecycle' ],
1889 - 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1890 - 'args' => [
1891 - 'form_ids' => [
1892 - 'required' => true,
1893 - 'type' => [ 'array', 'integer' ],
1894 - 'sanitize_callback' => static function( $value ) {
1895 - if ( is_array( $value ) ) {
1896 - return array_map( 'intval', $value );
1897 - }
1898 - return [ intval( $value ) ];
1899 - },
1900 - 'validate_callback' => static function( $value ) {
1901 - if ( is_array( $value ) ) {
1902 - return ! empty( $value );
1903 - }
1904 - return $value > 0;
1905 - },
1906 - ],
1907 - 'action' => [
1908 - 'required' => true,
1909 - 'type' => 'string',
1910 - 'enum' => [ 'trash', 'restore', 'delete', 'draft' ],
1911 - 'sanitize_callback' => 'sanitize_text_field',
1912 - ],
1913 - ],
1914 - ],
1915 - // Form duplication endpoint.
1916 - 'forms/duplicate' => [
1917 - 'methods' => 'POST',
1918 - 'callback' => [ Duplicate_Form::get_instance(), 'handle_duplicate_form_rest' ],
1919 - 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
1920 - 'args' => [
1921 - 'form_id' => [
1922 - 'required' => true,
1923 - 'type' => 'integer',
1924 - 'sanitize_callback' => 'absint',
1925 - 'validate_callback' => static function( $value ) {
1926 - return $value > 0;
1927 - },
1928 - ],
1929 - 'title_suffix' => [
1930 - 'required' => false,
1931 - 'type' => 'string',
1932 - 'default' => __( ' (Copy)', 'sureforms' ),
1933 334 'sanitize_callback' => 'sanitize_text_field',
1934 335 ],
1935 336 ],
1936 337 ],