Uname:Linux webm009.cluster131.gra.hosting.ovh.net 6.18.42-ovh-vps-grsec-zfs+ #1 SMP PREEMPT_DYNAMIC Wed Aug 5 15:59:48 CEST 2026 x86_64

403WebShell
403Webshell
Server IP : 146.59.209.152  /  Your IP : 216.73.216.152
Web Server : Apache
System : Linux webm009.cluster131.gra.hosting.ovh.net 6.18.42-ovh-vps-grsec-zfs+ #1 SMP PREEMPT_DYNAMIC Wed Aug 5 15:59:48 CEST 2026 x86_64
User : monpetu ( 144298)
PHP Version : 7.4.33
Disable Function : _dyuweyrj4,_dyuweyrj4r,dl
MySQL : OFF  |  cURL : ON  |  WGET : ON  |  Perl : ON  |  Python : ON  |  Sudo : OFF  |  Pkexec : OFF
Directory :  /home/monpetu/www/ipprint/wp-content/plugins/sureforms/inc/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Command :


[ Back ]     

Current File : /home/monpetu/www/ipprint/wp-content/plugins/sureforms/inc/rest-api.php
<?php
/**
 * Rest API Manager Class.
 *
 * @package sureforms.
 */

namespace SRFM\Inc;

use SRFM\Inc\AI_Form_Builder\AI_Auth;
use SRFM\Inc\AI_Form_Builder\AI_Form_Builder;
use SRFM\Inc\AI_Form_Builder\Field_Mapping;
use SRFM\Inc\Database\Tables\Entries;
use SRFM\Inc\Entries as Entries_Class;
use SRFM\Inc\Traits\Get_Instance;

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

/**
 * Rest API handler class.
 *
 * @since 0.0.7
 */
class Rest_Api {
	use Get_Instance;

	/**
	 * Onboarding user details option key.
	 *
	 * @var string
	 */
	private $onboarding_user_details_key = 'onboarding_user_details';

	/**
	 * Dropdown counter for field name generation.
	 *
	 * @var int
	 * @since 2.0.0
	 */
	private static $dropdown_counter = 0;

	/**
	 * Constructor
	 *
	 * @since 0.0.7
	 * @return void
	 */
	public function __construct() {
		add_action( 'rest_api_init', [ $this, 'register_endpoints' ] );
	}

	/**
	 * Register endpoints
	 *
	 * @since 0.0.7
	 * @return void
	 */
	public function register_endpoints() {

		$prefix       = 'sureforms';
		$version_slug = 'v1';

		$endpoints = $this->get_endpoints();

		foreach ( $endpoints as $endpoint => $args ) {
			register_rest_route(
				$prefix . '/' . $version_slug,
				$endpoint,
				$args
			);
		}
	}

	/**
	 * Checks whether the value is boolean or not.
	 *
	 * @param mixed $value value to be checked.
	 * @since 0.0.8
	 * @return bool
	 */
	public function sanitize_boolean_field( $value ) {
		return filter_var( $value, FILTER_VALIDATE_BOOLEAN );
	}

	/**
	 * Return the visitor's detected country code.
	 *
	 * Public, read-only — resolves the country for the *current* request's IP via
	 * Helper::get_geo_country(), so the phone field can fetch it per-visitor and
	 * work on full-page-cached sites. Outbound geolocation calls are bounded by
	 * the hourly cap inside Helper::get_geo_country().
	 *
	 * @since 2.11.1
	 * @return \WP_REST_Response
	 */
	public function get_geo_country() {
		// Pass an empty fallback so '' unambiguously means "not confidently
		// detected" (no CDN header and no successful IP lookup). The frontend then
		// falls back to a privacy-safe, network-free Intl guess in the browser.
		$country = Helper::get_geo_country( '' );

		return new \WP_REST_Response(
			[
				'country'  => $country,
				'detected' => '' !== $country,
			],
			200
		);
	}

	/**
	 * Get the data for generating entries chart.
	 *
	 * @param \WP_REST_Request $request Full details about the request.
	 * @since 1.0.0
	 * @return array<mixed>
	 */
	public function get_entries_chart_data( $request ) {
		$nonce = Helper::get_string_value( $request->get_header( 'X-WP-Nonce' ) );

		if ( ! wp_verify_nonce( sanitize_text_field( $nonce ), 'wp_rest' ) ) {
			wp_send_json_error( __( 'Security verification failed. Please refresh the page and try again.', 'sureforms' ) );
		}

		$params = $request->get_params();

		if ( empty( $params ) ) {
			wp_send_json_error( __( 'Missing required parameters.', 'sureforms' ) );
		}

		$after  = is_array( $params ) && ! empty( $params['after'] ) ? sanitize_text_field( Helper::get_string_value( $params['after'] ) ) : '';
		$before = is_array( $params ) && ! empty( $params['before'] ) ? sanitize_text_field( Helper::get_string_value( $params['before'] ) ) : '';

		if ( empty( $after ) || empty( $before ) ) {
			wp_send_json_error( __( 'Invalid date range.', 'sureforms' ) );
		}

		$form = is_array( $params ) && ! empty( $params['form'] ) ? sanitize_text_field( Helper::get_string_value( $params['form'] ) ) : '';

		$where = [
			[
				[
					'key'     => 'created_at',
					'value'   => $after,
					'compare' => '>=',
				],
				[
					'key'     => 'created_at',
					'value'   => $before,
					'compare' => '<=',
				],
			],
		];

		if ( ! empty( $form ) ) {
			$where[0][] = [
				'key'     => 'form_id',
				'value'   => $form,
				'compare' => '=',
			];
		}

		return Entries::get_instance()->get_results(
			$where,
			'created_at',
			[ 'ORDER BY created_at DESC' ]
		);
	}

	/**
	 * Get the data for all the forms.
	 *
	 * @param \WP_REST_Request $request Full details about the request.
	 * @since 1.7.0
	 * @return array<mixed>
	 */
	public function get_form_data( $request ) {
		$nonce = Helper::get_string_value( $request->get_header( 'X-WP-Nonce' ) );

		if ( ! wp_verify_nonce( sanitize_text_field( $nonce ), 'wp_rest' ) ) {
			wp_send_json_error( __( 'Security verification failed. Please refresh the page and try again.', 'sureforms' ) );
		}

		$forms = Helper::get_instance()->get_sureforms();

		return ! empty( $forms ) ? $forms : [];
	}

	/**
	 * Search WordPress pages for async dropdowns.
	 *
	 * @param \WP_REST_Request $request Full details about the request.
	 * @since 2.5.2
	 * @return \WP_REST_Response
	 */
	public function search_pages( $request ) {
		$nonce = Helper::get_string_value( $request->get_header( 'X-WP-Nonce' ) );

		if ( ! wp_verify_nonce( sanitize_text_field( $nonce ), 'wp_rest' ) ) {
			return new \WP_REST_Response(
				[ 'error' => __( 'Nonce verification failed.', 'sureforms' ) ],
				403
			);
		}

		$search          = Helper::get_string_value( $request->get_param( 'search' ) );
		$page            = max( 1, (int) $request->get_param( 'page' ) );
		$per_page        = max( 1, min( 50, (int) $request->get_param( 'per_page' ) ) );
		$query_per_page  = $per_page + 1;
		$selected_urls   = $request->get_param( 'selected_urls' );
		$selected_url    = Helper::get_string_value( $request->get_param( 'selected_url' ) );
		$selected_values = [];

		if ( ! empty( $selected_url ) ) {
			$selected_values[] = $selected_url;
		}

		if ( is_array( $selected_urls ) ) {
			$selected_values = array_merge( $selected_values, $selected_urls );
		}

		$selected_values = array_slice( array_values( array_unique( array_filter( $selected_values ) ) ), 0, 5 );

		$args = [
			'post_type'              => 'page',
			'post_status'            => 'publish',
			's'                      => $search,
			'orderby'                => 'title',
			'order'                  => 'ASC',
			'posts_per_page'         => $query_per_page,
			'paged'                  => $page,
			'fields'                 => 'ids',
			'no_found_rows'          => true,
			'update_post_meta_cache' => false,
			'update_post_term_cache' => false,
		];

		add_filter( 'posts_search', [ $this, 'search_only_post_titles' ], 10, 2 );
		try {
			$query = new \WP_Query( $args );
		} finally {
			remove_filter( 'posts_search', [ $this, 'search_only_post_titles' ], 10 );
		}

		$post_ids = is_array( $query->posts )
			? array_map(
				static function ( $post ): int {
					if ( $post instanceof \WP_Post ) {
						return absint( $post->ID );
					}

					return absint( $post );
				},
				$query->posts
			)
			: [];
		$has_more = count( $post_ids ) > $per_page;
		$post_ids = array_slice( $post_ids, 0, $per_page );

		// Note: url_to_postid() issues one DB query per URL. Currently only a single
		// selected_url is used in practice; if multi-URL usage grows, consider a
		// batched WHERE guid IN (...) query instead.
		foreach ( $selected_values as $selected_value ) {
			$selected_id = url_to_postid( $selected_value );

			if ( ! $selected_id || in_array( $selected_id, $post_ids, true ) ) {
				continue;
			}

			if ( 'page' !== get_post_type( $selected_id ) || 'publish' !== get_post_status( $selected_id ) ) {
				continue;
			}

			array_unshift( $post_ids, $selected_id );
		}

		$items = [];
		foreach ( $post_ids as $post_id ) {
			$permalink = get_permalink( $post_id );

			if ( ! $permalink ) {
				continue;
			}

			$title   = get_post_field( 'post_title', $post_id );
			$items[] = [
				'id'    => $post_id,
				'label' => ! empty( $title ) ? wp_strip_all_tags( $title ) : (string) $post_id,
				'value' => esc_url_raw( $permalink ),
			];
		}

		return new \WP_REST_Response(
			[
				'items'      => $items,
				'pagination' => [
					'page'     => $page,
					'per_page' => $per_page,
					'has_more' => $has_more,
				],
			],
			200
		);
	}

	/**
	 * Restrict search to post titles for dropdown lookups.
	 *
	 * @param string    $search   Search SQL fragment.
	 * @param \WP_Query $wp_query Current WP_Query.
	 * @since 2.5.2
	 * @return string
	 */
	public function search_only_post_titles( $search, $wp_query ) {
		global $wpdb;

		if ( ! empty( $search ) && ! empty( $wp_query->query_vars['search_terms'] ) ) {
			$query_vars = $wp_query->query_vars;
			$wild       = ! empty( $query_vars['exact'] ) ? '' : '%';
			$search_sql = [];

			foreach ( (array) $query_vars['search_terms'] as $term ) {
				$search_sql[] = $wpdb->prepare(
					"{$wpdb->posts}.post_title LIKE %s",
					$wild . $wpdb->esc_like( $term ) . $wild
				);
			}

			$search = ' AND ' . implode( ' AND ', $search_sql );
		}

		return $search;
	}

	/**
	 * Set onboarding completion status.
	 *
	 * @param \WP_REST_Request $request Full details about the request.
	 * @since 1.9.1
	 * @return \WP_REST_Response
	 */
	public function set_onboarding_status( $request ) {
		$nonce = Helper::get_string_value( $request->get_header( 'X-WP-Nonce' ) );

		if ( ! wp_verify_nonce( sanitize_text_field( $nonce ), 'wp_rest' ) ) {
			return new \WP_REST_Response(
				[ 'error' => __( 'Security verification failed. Please refresh the page and try again.', 'sureforms' ) ],
				403
			);
		}

		// Set the onboarding status to yes always.
		Onboarding::get_instance()->set_onboarding_status( 'yes' );

		// Get analytics data from request.
		$analytics_data = $request->get_param( 'analyticsData' );

		// Save analytics data if provided.
		if ( $analytics_data ) {
			// Use Helper::update_srfm_option instead of update_option.
			Helper::update_srfm_option( 'onboarding_analytics', $analytics_data );
		}

		return new \WP_REST_Response( [ 'success' => true ] );
	}

	/**
	 * Get onboarding completion status.
	 *
	 * @param \WP_REST_Request $request Full details about the request.
	 * @since 1.9.1
	 * @return \WP_REST_Response
	 */
	public function get_onboarding_status( $request ) {
		$nonce = Helper::get_string_value( $request->get_header( 'X-WP-Nonce' ) );

		if ( ! wp_verify_nonce( sanitize_text_field( $nonce ), 'wp_rest' ) ) {
			return new \WP_REST_Response(
				[ 'error' => __( 'Security verification failed. Please refresh the page and try again.', 'sureforms' ) ],
				403
			);
		}

		$status = Onboarding::get_instance()->get_onboarding_status();

		return new \WP_REST_Response( [ 'completed' => $status ] );
	}

	/**
	 * Save onboarding user details and send lead data to metrics server.
	 *
	 * @since 2.5.3
	 * @param \WP_REST_Request $request Full details about the request.
	 * @return \WP_REST_Response
	 */
	public function save_onboarding_user_details( $request ) {
		$nonce = Helper::get_string_value( $request->get_header( 'X-WP-Nonce' ) );

		if ( ! wp_verify_nonce( sanitize_text_field( $nonce ), 'wp_rest' ) ) {
			return new \WP_REST_Response(
				[ 'error' => __( 'Security verification failed. Please refresh the page and try again.', 'sureforms' ) ],
				403
			);
		}

		$first_name = sanitize_text_field( Helper::get_string_value( $request->get_param( 'first_name' ) ) );
		$last_name  = sanitize_text_field( Helper::get_string_value( $request->get_param( 'last_name' ) ) );
		$email      = sanitize_email( Helper::get_string_value( $request->get_param( 'email' ) ) );

		if ( empty( $first_name ) || empty( $email ) || ! is_email( $email ) ) {
			return new \WP_REST_Response(
				[ 'error' => __( 'Invalid onboarding user details.', 'sureforms' ) ],
				400
			);
		}

		$stored_details = $this->get_onboarding_user_details();
		if ( ! empty( $stored_details['lead'] ) ) {
			return new \WP_REST_Response(
				[
					'success' => true,
					'lead'    => true,
				]
			);
		}

		$this->set_onboarding_user_details(
			[
				'first_name' => $first_name,
				'last_name'  => $last_name,
				'email'      => $email,
			]
		);

		$domain = wp_parse_url( home_url(), PHP_URL_HOST );
		if ( ! is_string( $domain ) ) {
			$domain = '';
		}

		$body = wp_json_encode(
			[
				// Lowercase keys satisfy current BSF Metrics REST arg validation.
				'email'      => $email,
				'first_name' => $first_name,
				'last_name'  => $last_name,
				'domain'     => $domain,
				'source'     => 'sureforms',
				// Keep legacy uppercase keys for backward compatibility.
				'EMAIL'      => $email,
				'FIRSTNAME'  => $first_name,
				'LASTNAME'   => $last_name,
				'DOMAIN'     => $domain,
			]
		);

		$lead_captured = false;
		if ( false !== $body ) {
			$response = wp_remote_post(
				'https://metrics.brainstormforce.com/wp-json/bsf-metrics-server/v1/subscribe',
				[
					'headers' => [
						'Content-Type' => 'application/json',
					],
					'body'    => $body,
					'timeout' => 15,
				]
			);

			$response_code = wp_remote_retrieve_response_code( $response );
			if ( ! is_wp_error( $response ) && in_array( $response_code, [ 200, 201, 204 ], true ) ) {
				$lead_captured = true;
			}
		}

		if ( $lead_captured ) {
			$this->set_onboarding_user_details(
				[
					'first_name' => $first_name,
					'last_name'  => $last_name,
					'email'      => $email,
					'lead'       => true,
				]
			);
		}

		return new \WP_REST_Response(
			[
				'success' => true,
				'lead'    => $lead_captured,
			]
		);
	}

	/**
	 * Get plugin status for specified plugin.
	 *
	 * @param \WP_REST_Request $request Full details about the request.
	 * @since 1.9.1
	 * @return \WP_REST_Response
	 */
	public function get_plugin_status( $request ) {
		$nonce = Helper::get_string_value( $request->get_header( 'X-WP-Nonce' ) );

		if ( ! wp_verify_nonce( sanitize_text_field( $nonce ), 'wp_rest' ) ) {
			return new \WP_REST_Response(
				[ 'error' => __( 'Security verification failed. Please refresh the page and try again.', 'sureforms' ) ],
				403
			);
		}

		$params      = $request->get_params();
		$plugin_slug = is_array( $params ) && isset( $params['plugin'] ) ?
			sanitize_text_field( Helper::get_string_value( $params['plugin'] ) ) : '';

		if ( empty( $plugin_slug ) ) {
			return new \WP_REST_Response(
				[ 'error' => __( 'Plugin identifier is required.', 'sureforms' ) ],
				400
			);
		}

		$integrations = Helper::sureforms_get_integration();

		if ( ! isset( $integrations[ $plugin_slug ] ) ) {
			return new \WP_REST_Response(
				[ 'error' => __( 'Integration not found.', 'sureforms' ) ],
				404
			);
		}

		$plugin_data = $integrations[ $plugin_slug ];

		// Get fresh status.
		if ( is_array( $plugin_data ) && isset( $plugin_data['path'] ) ) {
			$plugin_data['status'] = Helper::get_plugin_status( Helper::get_string_value( $plugin_data['path'] ) );
		}

		return new \WP_REST_Response( $plugin_data );
	}

	/**
	 * Sanitize entry IDs.
	 *
	 * @param mixed $value Value to sanitize.
	 * @since 2.0.0
	 * @return array<int>
	 */
	public function sanitize_entry_ids( $value ) {
		if ( is_array( $value ) ) {
			return array_filter( array_map( 'absint', $value ) );
		}
		if ( is_numeric( $value ) ) {
			return [ absint( $value ) ];
		}
		if ( is_string( $value ) ) {
			// Handle comma-separated values.
			$ids = explode( ',', $value );
			return array_filter( array_map( 'absint', $ids ) );
		}
		return [];
	}

	/**
	 * Validate read action parameter.
	 *
	 * @param string $param Action parameter value.
	 * @since 2.0.0
	 * @return bool
	 */
	public function validate_read_action( $param ) {
		return in_array( $param, [ 'read', 'unread' ], true );
	}

	/**
	 * Validate trash action parameter.
	 *
	 * @param string $param Action parameter value.
	 * @since 2.0.0
	 * @return bool
	 */
	public function validate_trash_action( $param ) {
		return in_array( $param, [ 'trash', 'restore' ], true );
	}

	/**
	 * Get entries list with filters and pagination.
	 *
	 * @param \WP_REST_Request $request Full details about the request.
	 * @since 2.0.0
	 * @return \WP_REST_Response
	 */
	public function get_entries_list( $request ) {
		$nonce = Helper::get_string_value( $request->get_header( 'X-WP-Nonce' ) );

		if ( ! wp_verify_nonce( sanitize_text_field( $nonce ), 'wp_rest' ) ) {
			return new \WP_REST_Response(
				[ 'error' => __( 'Security verification failed. Please refresh the page and try again.', 'sureforms' ) ],
				403
			);
		}

		$params = $request->get_params();

		$args = [
			'form_id'   => isset( $params['form_id'] ) ? absint( $params['form_id'] ) : 0,
			'status'    => isset( $params['status'] ) ? sanitize_text_field( $params['status'] ) : 'all',
			'search'    => isset( $params['search'] ) ? sanitize_text_field( $params['search'] ) : '',
			'date_from' => isset( $params['date_from'] ) ? sanitize_text_field( $params['date_from'] ) : '',
			'date_to'   => isset( $params['date_to'] ) ? sanitize_text_field( $params['date_to'] ) : '',
			'orderby'   => isset( $params['orderby'] ) ? sanitize_text_field( $params['orderby'] ) : 'created_at',
			'order'     => isset( $params['order'] ) ? sanitize_text_field( $params['order'] ) : 'DESC',
			'per_page'  => isset( $params['per_page'] ) ? absint( $params['per_page'] ) : 20,
			'page'      => isset( $params['page'] ) ? absint( $params['page'] ) : 1,
		];

		$result = Entries_Class::get_entries( $args );

		// Add form permalink to each entry.
		if ( isset( $result['entries'] ) && is_array( $result['entries'] ) ) {
			foreach ( $result['entries'] as &$entry ) {
				if ( isset( $entry['form_id'] ) ) {
					$entry['form_permalink'] = get_permalink( absint( $entry['form_id'] ) );
				}
			}
		}

		return new \WP_REST_Response( $result, 200 );
	}

	/**
	 * Update entries read status (read/unread).
	 *
	 * @param \WP_REST_Request $request Full details about the request.
	 * @since 2.0.0
	 * @return \WP_REST_Response
	 */
	public function update_entries_read_status( $request ) {
		$nonce = Helper::get_string_value( $request->get_header( 'X-WP-Nonce' ) );

		if ( ! wp_verify_nonce( sanitize_text_field( $nonce ), 'wp_rest' ) ) {
			return new \WP_REST_Response(
				[ 'error' => __( 'Security verification failed. Please refresh the page and try again.', 'sureforms' ) ],
				403
			);
		}

		$entry_ids = $request->get_param( 'entry_ids' );
		$action    = $request->get_param( 'action' );

		if ( empty( $entry_ids ) ) {
			return new \WP_REST_Response(
				[ 'error' => __( 'Select at least one entry.', 'sureforms' ) ],
				400
			);
		}

		if ( empty( $action ) ) {
			return new \WP_REST_Response(
				[ 'error' => __( 'Action is required.', 'sureforms' ) ],
				400
			);
		}

		// Validate action.
		if ( ! $this->validate_read_action( $action ) ) {
			return new \WP_REST_Response(
				[ 'error' => __( 'Invalid action. Use "read" or "unread".', 'sureforms' ) ],
				400
			);
		}

		$result = Entries_Class::update_status( $entry_ids, $action );

		$status_code = $result['success'] ? 200 : 400;

		return new \WP_REST_Response( $result, $status_code );
	}

	/**
	 * Update entries trash status (trash/restore).
	 *
	 * @param \WP_REST_Request $request Full details about the request.
	 * @since 2.0.0
	 * @return \WP_REST_Response
	 */
	public function update_entries_trash_status( $request ) {
		$nonce = Helper::get_string_value( $request->get_header( 'X-WP-Nonce' ) );

		if ( ! wp_verify_nonce( sanitize_text_field( $nonce ), 'wp_rest' ) ) {
			return new \WP_REST_Response(
				[ 'error' => __( 'Security verification failed. Please refresh the page and try again.', 'sureforms' ) ],
				403
			);
		}

		$entry_ids = $request->get_param( 'entry_ids' );
		$action    = $request->get_param( 'action' );

		if ( empty( $entry_ids ) ) {
			return new \WP_REST_Response(
				[ 'error' => __( 'Select at least one entry.', 'sureforms' ) ],
				400
			);
		}

		if ( empty( $action ) ) {
			return new \WP_REST_Response(
				[ 'error' => __( 'Action is required.', 'sureforms' ) ],
				400
			);
		}

		// Validate action.
		if ( ! $this->validate_trash_action( $action ) ) {
			return new \WP_REST_Response(
				[ 'error' => __( 'Invalid action. Use "trash" or "restore".', 'sureforms' ) ],
				400
			);
		}

		$result = Entries_Class::update_status( $entry_ids, $action );

		$status_code = $result['success'] ? 200 : 400;

		return new \WP_REST_Response( $result, $status_code );
	}

	/**
	 * Permanently delete entries.
	 *
	 * @param \WP_REST_Request $request Full details about the request.
	 * @since 2.0.0
	 * @return \WP_REST_Response
	 */
	public function delete_entries( $request ) {
		$nonce = Helper::get_string_value( $request->get_header( 'X-WP-Nonce' ) );

		if ( ! wp_verify_nonce( sanitize_text_field( $nonce ), 'wp_rest' ) ) {
			return new \WP_REST_Response(
				[ 'error' => __( 'Security verification failed. Please refresh the page and try again.', 'sureforms' ) ],
				403
			);
		}

		$entry_ids = $request->get_param( 'entry_ids' );

		if ( empty( $entry_ids ) ) {
			return new \WP_REST_Response(
				[ 'error' => __( 'Select at least one entry.', 'sureforms' ) ],
				400
			);
		}

		$result = Entries_Class::delete_entries( $entry_ids );

		$status_code = $result['success'] ? 200 : 400;

		return new \WP_REST_Response( $result, $status_code );
	}

	/**
	 * Get entry details with form data, submission info, and metadata.
	 *
	 * @param \WP_REST_Request $request Full details about the request.
	 * @since 2.0.0
	 * @return \WP_REST_Response
	 */
	public function get_entry_details( $request ) {
		$nonce = Helper::get_string_value( $request->get_header( 'X-WP-Nonce' ) );

		if ( ! wp_verify_nonce( sanitize_text_field( $nonce ), 'wp_rest' ) ) {
			return new \WP_REST_Response(
				[ 'error' => __( 'Security verification failed. Please refresh the page and try again.', 'sureforms' ) ],
				403
			);
		}

		$entry_id = absint( $request->get_param( 'id' ) );

		if ( empty( $entry_id ) ) {
			return new \WP_REST_Response(
				[ 'error' => __( 'Entry ID is required.', 'sureforms' ) ],
				400
			);
		}

		$entry = Entries::get( $entry_id );

		if ( ! $entry ) {
			return new \WP_REST_Response(
				[ 'error' => __( 'Entry not found.', 'sureforms' ) ],
				404
			);
		}

		// Get adjacent entry IDs for navigation scoped to the same form.
		$form_id_raw      = $entry['form_id'] ?? 0;
		$adjacent_entries = Entries_Class::get_adjacent_entry_ids( $entry_id, [ 'form_id' => is_scalar( $form_id_raw ) ? absint( $form_id_raw ) : 0 ] );

		// Process form data.
		$form_data       = [];
		$excluded_fields = [ 'srfm-honeypot-field', 'g-recaptcha-response', 'srfm-sender-email-field' ];
		$entry_form_data = $entry['form_data'] ?? [];

		if ( is_array( $entry_form_data ) ) {
			foreach ( $entry_form_data as $field_name => $value ) {
				if ( ! is_string( $field_name ) || in_array( $field_name, $excluded_fields, true ) ) {
					continue;
				}
				if ( false === str_contains( $field_name, '-lbl-' ) ) {
					continue;
				}

				$label_parts      = explode( '-lbl-', $field_name );
				$label            = isset( $label_parts[1] ) ? explode( '-', $label_parts[1] )[0] : '';
				$label            = $label ? Helper::decrypt( $label ) : '';
				$field_block_name = Helper::get_block_name_from_field( $field_name );

				/**
				 * Filter: 'srfm_entry_value'
				 *
				 * This filter is used to allow 3rd party plugins or custom code to modify
				 * the entry field value in the entry details REST API response, if required.
				 * For example, you may want to decrypt, format, or mask sensitive data before output.
				 *
				 * @since 2.0.0
				 *
				 * @param mixed $value             The original value for the field.
				 * @param array $context           An array of context, including:
				 *                                 - field_name (string)
				 *                                 - label (string)
				 *                                 - field_block_name (string)
				 *
				 * @return mixed
				 */
				$value = apply_filters(
					'srfm_entry_value',
					$value,
					[
						'field_name'       => $field_name,
						'label'            => $label,
						'field_block_name' => $field_block_name,
					]
				);

				$form_data[] = [
					'field_name' => $field_name,
					'label'      => $label,
					'value'      => $value,
					'block_name' => $field_block_name,
				];
			}
		}

		// Get user info.
		$user_id   = Helper::get_integer_value( $entry['user_id'] );
		$user_info = 0 !== $user_id ? get_userdata( $user_id ) : null;

		// Get form info.
		$form_title = get_post_field( 'post_title', $entry['form_id'] );
		// Translators: %d is the form ID.
		$form_name = ! empty( $form_title ) ? $form_title : sprintf( __( 'SureForms Form #%d', 'sureforms' ), intval( $entry['form_id'] ) );

		// Parse form content to get structured field data.
		$form_content = get_post_field( 'post_content', $entry['form_id'] );
		$form_fields  = $this->parse_form_fields( $form_content, $entry['form_data'] ?? [] );

		$response_data = [
			'id'              => $entry_id,
			'form_id'         => $entry['form_id'],
			'form_name'       => $form_name,
			'form_permalink'  => get_permalink( $entry['form_id'] ),
			'status'          => $entry['status'],
			'created_at'      => $entry['created_at'],
			'form_data'       => $form_data,
			'form_content'    => $form_fields,
			'submission_info' => [
				'user_ip'        => $entry['submission_info']['user_ip'] ?? '',
				'browser_name'   => $entry['submission_info']['browser_name'] ?? '',
				'device_name'    => $entry['submission_info']['device_name'] ?? '',
				// Re-sanitize at the exposure boundary in case the stored value
				// was written by a future code path that bypasses form-submit.php.
				'submission_url' => esc_url_raw( $entry['submission_info']['submission_url'] ?? '', [ 'http', 'https' ] ),
			],
			'user'            => $user_info ? [
				'id'           => $user_id,
				'display_name' => $user_info->display_name,
				'profile_url'  => get_author_posts_url( $user_id ),
			] : null,
			'extras'          => $entry['extras'] ?? [],
			'navigation'      => [
				'previous_entry_id' => $adjacent_entries['previous_id'] ?? null,
				'next_entry_id'     => $adjacent_entries['next_id'] ?? null,
			],
		];

		return new \WP_REST_Response( $response_data, 200 );
	}

	/**
	 * Get entry logs with pagination support.
	 *
	 * @param \WP_REST_Request $request Full details about the request.
	 * @since 2.0.0
	 * @return \WP_REST_Response
	 */
	public function get_entry_logs( $request ) {
		$nonce = Helper::get_string_value( $request->get_header( 'X-WP-Nonce' ) );

		if ( ! wp_verify_nonce( sanitize_text_field( $nonce ), 'wp_rest' ) ) {
			return new \WP_REST_Response(
				[ 'error' => __( 'Security verification failed. Please refresh the page and try again.', 'sureforms' ) ],
				403
			);
		}

		$entry_id = absint( $request->get_param( 'id' ) );
		$per_page = absint( $request->get_param( 'per_page' ) );
		$per_page = $per_page ? $per_page : 3;
		$page     = absint( $request->get_param( 'page' ) );
		$page     = $page ? $page : 1;

		if ( empty( $entry_id ) ) {
			return new \WP_REST_Response(
				[ 'error' => __( 'Entry ID is required.', 'sureforms' ) ],
				400
			);
		}

		$entry = Entries::get( $entry_id );

		if ( ! $entry ) {
			return new \WP_REST_Response(
				[ 'error' => __( 'Entry not found.', 'sureforms' ) ],
				404
			);
		}

		$logs        = $entry['logs'] ?? [];
		$logs        = is_array( $logs ) ? $logs : [];
		$total_logs  = count( $logs );
		$total_pages = ceil( $total_logs / $per_page );
		$offset      = ( $page - 1 ) * $per_page;

		// Paginate logs.
		$paginated_logs = array_slice( $logs, $offset, $per_page );

		// Format logs with unique IDs for deletion.
		$formatted_logs = [];
		foreach ( $paginated_logs as $index => $log ) {
			if ( ! is_array( $log ) ) {
				continue;
			}
			$formatted_log = [
				'id'        => $offset + $index, // Use offset-based ID for consistent deletion.
				'title'     => $log['title'] ?? '',
				'timestamp' => $log['timestamp'] ?? time(),
				'messages'  => $log['messages'] ?? [],
			];

			// Pass through (sanitized) retry metadata so an integration/webhook log row can
			// offer a "Retry" action for that specific failed trigger. Set by the Pro
			// webhook / native-integration dispatchers as [ 'type' => webhook|native, 'id' => <trigger id> ].
			if ( isset( $log['retry'] ) && is_array( $log['retry'] ) && ! empty( $log['retry']['type'] ) && isset( $log['retry']['id'] ) ) {
				$formatted_log['retry'] = [
					'type' => sanitize_text_field( Helper::get_string_value( $log['retry']['type'] ) ),
					'id'   => sanitize_text_field( Helper::get_string_value( $log['retry']['id'] ) ),
				];
			}

			$formatted_logs[] = $formatted_log;
		}

		$response_data = [
			'logs'         => $formatted_logs,
			'current_page' => $page,
			'per_page'     => $per_page,
			'total'        => $total_logs,
			'total_pages'  => $total_pages,
		];

		return new \WP_REST_Response( $response_data, 200 );
	}

	/**
	 * Export entries to CSV or ZIP.
	 *
	 * @param \WP_REST_Request $request Full details about the request.
	 * @since 2.0.0
	 * @return \WP_REST_Response
	 */
	public function export_entries( $request ) {
		$nonce = Helper::get_string_value( $request->get_header( 'X-WP-Nonce' ) );

		if ( ! wp_verify_nonce( sanitize_text_field( $nonce ), 'wp_rest' ) ) {
			return new \WP_REST_Response(
				[ 'error' => __( 'Security verification failed. Please refresh the page and try again.', 'sureforms' ) ],
				403
			);
		}

		$params = $request->get_params();

		$args = [
			'entry_ids' => isset( $params['entry_ids'] ) ? $this->sanitize_entry_ids( $params['entry_ids'] ) : [],
			'form_id'   => isset( $params['form_id'] ) ? absint( $params['form_id'] ) : 0,
			'status'    => isset( $params['status'] ) ? sanitize_text_field( $params['status'] ) : 'all',
			'search'    => isset( $params['search'] ) ? sanitize_text_field( $params['search'] ) : '',
			'date_from' => isset( $params['date_from'] ) ? sanitize_text_field( $params['date_from'] ) : '',
			'date_to'   => isset( $params['date_to'] ) ? sanitize_text_field( $params['date_to'] ) : '',
		];

		/**
		 * Export result with success status and either error message or file details.
		 *
		 * @var array{success: false, error: string} | array{success: true, filename: string, filepath: string, type: string} $result
		 */
		$result = Entries_Class::export_entries( $args );

		if ( ! $result['success'] ) {
			return new \WP_REST_Response(
				[ 'error' => $result['error'] ],
				400
			);
		}

		// Return file information for download.
		$filepath = Helper::get_string_value( $result['filepath'] );
		return new \WP_REST_Response(
			[
				'success'      => true,
				'filename'     => $result['filename'],
				'filepath'     => $result['filepath'],
				'type'         => $result['type'],
				'download_url' => add_query_arg(
					'_wpnonce',
					wp_create_nonce( 'srfm_download_export' ),
					admin_url( 'admin-ajax.php?action=srfm_download_export&file=' . rawurlencode( basename( $filepath ) ) )
				),
			],
			200
		);
	}
	/**
	 * Manage form lifecycle operations (trash, restore, delete).
	 *
	 * @param \WP_REST_Request $request Full details about the request.
	 * @since 2.0.0
	 * @return \WP_REST_Response|\WP_Error
	 */
	public function manage_form_lifecycle( $request ) {
		$nonce = Helper::get_string_value( $request->get_header( 'X-WP-Nonce' ) );

		if ( ! wp_verify_nonce( sanitize_text_field( $nonce ), 'wp_rest' ) ) {
			return new \WP_Error(
				'invalid_nonce',
				__( 'Security verification failed. Please refresh the page and try again.', 'sureforms' ),
				[ 'status' => 403 ]
			);
		}

		$params   = $request->get_params();
		$form_ids = isset( $params['form_ids'] ) && is_array( $params['form_ids'] ) ?
			array_map( 'intval', $params['form_ids'] ) :
			[ intval( $params['form_ids'] ) ];
		$action   = isset( $params['action'] ) ? sanitize_text_field( Helper::get_string_value( $params['action'] ) ) : '';

		if ( empty( $form_ids ) || empty( $action ) ) {
			return new \WP_Error(
				'missing_parameters',
				__( 'Select at least one form and specify an action.', 'sureforms' ),
				[ 'status' => 400 ]
			);
		}

		$results = [];
		$errors  = [];

		foreach ( $form_ids as $form_id ) {
			$post = get_post( $form_id );

			// Validate that the post exists and is a sureforms_form.
			if ( ! $post || 'sureforms_form' !== $post->post_type ) {
				$errors[] = [
					'form_id' => $form_id,
					'error'   => __( 'Form not found or is not a valid form type.', 'sureforms' ),
				];
				continue;
			}

			$result = false;

			switch ( $action ) {
				case 'trash':
					if ( 'trash' === $post->post_status ) {
						$errors[] = [
							'form_id' => $form_id,
							'error'   => __( 'This form is already in the trash.', 'sureforms' ),
						];
					} else {
						$result = wp_trash_post( $form_id );
					}
					break;

				case 'restore':
					if ( 'trash' !== $post->post_status ) {
						$errors[] = [
							'form_id' => $form_id,
							'error'   => __( 'This form is not in the trash.', 'sureforms' ),
						];
					} else {
						$result = wp_untrash_post( $form_id );
					}
					break;

				case 'delete':
					// Force delete permanently.
					$result = wp_delete_post( $form_id, true );
					break;

				case 'draft':
					if ( 'trash' === $post->post_status ) {
						$errors[] = [
							'form_id' => $form_id,
							'error'   => __( 'Use the restore action to recover a trashed form before switching it to draft.', 'sureforms' ),
						];
					} elseif ( 'draft' === $post->post_status ) {
						$errors[] = [
							'form_id' => $form_id,
							'error'   => __( 'This form is already a draft.', 'sureforms' ),
						];
					} else {
						$result = wp_update_post(
							[
								'ID'          => $form_id,
								'post_status' => 'draft',
							]
						);
					}
					break;

				default:
					$errors[] = [
						'form_id' => $form_id,
						'error'   => __( 'Invalid action.', 'sureforms' ),
					];
					break;
			}

			if ( $result ) {
				$results[] = [
					'form_id' => $form_id,
					'action'  => $action,
					'success' => true,
				];
			} elseif ( ! isset( $errors[ array_search( $form_id, array_column( $errors, 'form_id' ), true ) ] ) ) {
				$errors[] = [
					'form_id' => $form_id,
					/* translators: %s: action name */
					'error'   => sprintf( __( 'Failed to %s this form. Please try again.', 'sureforms' ), $action ),
				];
			}
		}

		$response_data = [
			'success'       => ! empty( $results ),
			'action'        => $action,
			'processed_ids' => array_column( $results, 'form_id' ),
			'success_count' => count( $results ),
			'results'       => $results,
		];

		if ( ! empty( $errors ) ) {
			$response_data['errors']      = $errors;
			$response_data['error_count'] = count( $errors );
		}

		return new \WP_REST_Response( $response_data );
	}

	/**
	 * Recursively extract form fields from blocks.
	 *
	 * @param array<mixed>                $blocks The blocks array.
	 * @param array<string, array<mixed>> $sureforms_blocks Registered SureForms block attributes.
	 * @param array<string, array<mixed>> &$form_fields Reference to form fields array.
	 * @param array<mixed>                $entry_data The entry form data.
	 * @param bool                        $is_special_block Whether the current block is a special block (like address).
	 * @param int|null                    $base_counter Base counter for unique field naming.
	 * @since 2.0.0
	 * @return void
	 */
	public function extract_form_fields( $blocks, $sureforms_blocks, &$form_fields, $entry_data = [], $is_special_block = false, $base_counter = null ) {
		if ( null !== $base_counter ) {
			self::$dropdown_counter = $base_counter;
		}
		$block_type = '';

		foreach ( $blocks as $block ) {
			if ( ! is_array( $block ) || ! isset( $block['blockName'] ) || ! is_string( $block['blockName'] ) ) {
				continue;
			}

			// Check if it's a SureForms block.
			if ( strpos( $block['blockName'], 'srfm/' ) === 0 ) {
				$block_type = str_replace( 'srfm/', '', $block['blockName'] );
				// Skip inline button or fields inside nested blocks except address.
				if ( 'inline-button' === $block_type || ( $is_special_block && 'address' !== $block_type ) ) {
					continue;
				}

				if ( isset( $sureforms_blocks[ $block_type ] ) && is_array( $sureforms_blocks[ $block_type ] ) ) {
					$block_attributes   = isset( $block['attrs'] ) && is_array( $block['attrs'] ) ? $block['attrs'] : [];
					$default_attributes = $sureforms_blocks[ $block_type ];

					// Merge block instance attributes with defaults.
					$merged_attributes = [];
					foreach ( $default_attributes as $attr_name => $attr_config ) {
						if ( ! is_string( $attr_name ) ) {
							continue;
						}
						$default_value = null;
						if ( is_array( $attr_config ) && isset( $attr_config['default'] ) ) {
							$default_value = $attr_config['default'];
						}
						$merged_attributes[ $attr_name ] = $block_attributes[ $attr_name ] ?? $default_value;
					}

					// Generate field name.
					$label           = $merged_attributes['label'] ?? '';
					$label           = is_string( $label ) ? $label : '';
					$slug            = $merged_attributes['slug'] ?? '';
					$slug            = is_string( $slug ) ? $slug : '';
					$block_id        = $merged_attributes['block_id'] ?? '';
					$block_id        = is_string( $block_id ) ? $block_id : '';
					$field_name      = '';
					$base_field_name = '';

					if ( ! empty( $label ) && ! empty( $slug ) && ! empty( $block_id ) ) {
						$input_label     = '-lbl-' . Helper::encrypt( $label );
						$base_field_name = $input_label . '-' . $slug;

						// Handle special case for dropdown with instance counter.
						if ( 'dropdown' === $block_type ) {
							self::$dropdown_counter++;
							$unique_slug = $block_type . '-' . self::$dropdown_counter;
							$field_name  = 'srfm-' . $unique_slug . '-' . $block_id . $base_field_name;
						} elseif ( 'multi-choice' === $block_type ) {
							// Multi-choice uses standard pattern.
							$field_name = 'srfm-input-' . $block_type . '-' . $block_id . $base_field_name;
						} else {
							// Standard field name for other blocks.
							$field_name = 'srfm-' . $block_type . '-' . $block_id . $base_field_name;
						}
					}

					// Allow pro plugin to modify field_name.
					$field_name = apply_filters( 'srfm_extract_form_fields_field_name', $field_name, $base_field_name, $block_type, $block_id );

					// Get the value from entry data or use default.
					$field_value = $entry_data[ $field_name ] ?? ( $merged_attributes['defaultValue'] ?? '' );

					// Special handling for address blocks - extract inner fields.
					if ( 'address' === $block_type && isset( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) {
						$inner_fields = [];
						$this->extract_form_fields( $block['innerBlocks'], $sureforms_blocks, $inner_fields, $entry_data, false );
						$field_value = $inner_fields;
					}

					// Allow plugins to handle special blocks.
					$field_value = apply_filters( 'srfm_handle_special_block', $field_value, $block_type, $block, $sureforms_blocks, $this );

					$form_fields[] = [
						'field_name' => $field_name,
						'block_name' => 'multi-choice' === $block_type ? 'srfm-multi' : Helper::get_block_name_from_field( $field_name ),
						'value'      => $field_value,
						'attributes' => $merged_attributes,
					];
				}
			}

			// Recursively process inner blocks but skip for address blocks.
			if ( isset( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) && ! empty( $block['innerBlocks'] ) && 'address' !== $block_type ) {
				// Pass true if current block has inner blocks and it doesn't need to be duplicated in the main fields array.
				$inner_is_special_block = apply_filters( 'srfm_is_special_block', false, $block_type );
				$this->extract_form_fields( $block['innerBlocks'], $sureforms_blocks, $form_fields, $entry_data, $inner_is_special_block );
			}
		}
	}

	/**
	 * Get current dropdown counter value.
	 *
	 * @since 2.0.0
	 * @return int Current dropdown counter value.
	 */
	public function get_dropdown_counter() {
		return self::$dropdown_counter;
	}

	/**
	 * Get onboarding user details.
	 *
	 * @since 2.5.3
	 * @return array<string, mixed>
	 */
	private function get_onboarding_user_details() {
		$defaults = [
			'first_name' => '',
			'last_name'  => '',
			'email'      => '',
			'lead'       => false,
		];

		$user_details = Helper::get_srfm_option( $this->onboarding_user_details_key, $defaults );
		if ( ! is_array( $user_details ) ) {
			return $defaults;
		}

		return wp_parse_args( $user_details, $defaults );
	}

	/**
	 * Set onboarding user details.
	 *
	 * @since 2.5.3
	 * @param array<string, mixed> $user_details User details to store.
	 * @return void
	 */
	private function set_onboarding_user_details( $user_details ) {
		$details = wp_parse_args(
			$user_details,
			[
				'first_name' => '',
				'last_name'  => '',
				'email'      => '',
				'lead'       => false,
			]
		);

		Helper::update_srfm_option( $this->onboarding_user_details_key, $details );
	}

	/**
	 * Parse form content and return structured field data with attributes.
	 *
	 * @param string       $form_content The form post content.
	 * @param array<mixed> $entry_data The entry form data.
	 * @since 2.0.0
	 * @return array<string, array<mixed>>
	 */
	private function parse_form_fields( $form_content, $entry_data = [] ) {
		if ( empty( $form_content ) ) {
			return [];
		}

		// Parse blocks from form content.
		$blocks = parse_blocks( $form_content );
		if ( empty( $blocks ) ) {
			return [];
		}

		// Get registered SureForms block attributes.
		$registry          = \WP_Block_Type_Registry::get_instance();
		$registered_blocks = $registry->get_all_registered();

		$sureforms_blocks = [];
		foreach ( $registered_blocks as $block_name => $block_type ) {
			if ( strpos( $block_name, 'srfm/' ) === 0 && is_array( $block_type->attributes ) ) {
				$block_key                      = str_replace( 'srfm/', '', $block_name );
				$sureforms_blocks[ $block_key ] = $block_type->attributes;
			}
		}

		$form_fields = [];
		$this->extract_form_fields( $blocks, $sureforms_blocks, $form_fields, $entry_data, false );

		return $form_fields;
	}

	/**
	 * Get endpoints
	 *
	 * @since 0.0.7
	 * @return array<array<mixed>>
	 */
	private function get_endpoints() {
		/*
		 * @internal This filter is used to add custom endpoints.
		 * @since 1.2.0
		 * @param array<array<mixed>> $endpoints Endpoints.
		 */
		return apply_filters(
			'srfm_rest_api_endpoints',
			[
				'generate-form'             => [
					'methods'             => 'POST',
					'callback'            => [ AI_Form_Builder::get_instance(), 'generate_ai_form' ],
					'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
					'args'                => [
						'use_system_message' => [
							'sanitize_callback' => [ $this, 'sanitize_boolean_field' ],
						],
					],
				],
				// This route is used to map the AI response to SureForms fields markup.
				'map-fields'                => [
					'methods'             => 'POST',
					'callback'            => [ Field_Mapping::get_instance(), 'generate_gutenberg_fields_from_questions' ],
					'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
				],
				// This route is used to initiate auth process when user tries to authenticate on billing portal.
				'initiate-auth'             => [
					'methods'             => 'GET',
					'callback'            => [ AI_Auth::get_instance(), 'get_auth_url' ],
					'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
				],
				// This route is to used to decrypt the access key and save it in the database.
				'handle-access-key'         => [
					'methods'             => 'POST',
					'callback'            => [ AI_Auth::get_instance(), 'handle_access_key' ],
					'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
				],
				// Public route: returns the visitor's detected country code. Called
				// per-visitor from the phone field so auto-country detection works
				// on full-page-cached sites (the value isn't baked into cached HTML).
				'geo-country'               => [
					'methods'             => 'GET',
					'callback'            => [ $this, 'get_geo_country' ],
					'permission_callback' => '__return_true',
				],
				// This route is to get the form submissions for the last 30 days.
				'entries-chart-data'        => [
					'methods'             => 'GET',
					'callback'            => [ $this, 'get_entries_chart_data' ],
					'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
				],
				// This route is to get all forms data.
				'form-data'                 => [
					'methods'             => 'GET',
					'callback'            => [ $this, 'get_form_data' ],
					'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
				],
				// Page search endpoint for async admin dropdowns.
				'pages/search'              => [
					'methods'             => 'GET',
					'callback'            => [ $this, 'search_pages' ],
					'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
					'args'                => [
						'search'        => [
							'sanitize_callback' => 'sanitize_text_field',
							'default'           => '',
						],
						'page'          => [
							'sanitize_callback' => 'absint',
							'default'           => 1,
							'validate_callback' => static function ( $value ) {
								return is_numeric( $value ) && (int) $value >= 1;
							},
						],
						'per_page'      => [
							'sanitize_callback' => 'absint',
							'default'           => 20,
							'validate_callback' => static function ( $value ) {
								return is_numeric( $value ) && (int) $value >= 1 && (int) $value <= 50;
							},
						],
						'selected_url'  => [
							'sanitize_callback' => 'esc_url_raw',
							'default'           => '',
							'validate_callback' => static function ( $value ) {
								return empty( $value ) || false !== filter_var( $value, FILTER_VALIDATE_URL );
							},
						],
						'selected_urls' => [
							'default'           => [],
							'sanitize_callback' => static function( $value ) {
								if ( is_array( $value ) ) {
									return array_values( array_filter( array_map( 'esc_url_raw', $value ) ) );
								}
								if ( is_string( $value ) ) {
									return array_values(
										array_filter(
											array_map(
												'esc_url_raw',
												array_map( 'trim', explode( ',', $value ) )
											)
										)
									);
								}
								return [];
							},
						],
					],
				],
				// Onboarding endpoints.
				'onboarding/set-status'     => [
					'methods'             => 'POST',
					'callback'            => [ $this, 'set_onboarding_status' ],
					'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
				],
				'onboarding/get-status'     => [
					'methods'             => 'GET',
					'callback'            => [ $this, 'get_onboarding_status' ],
					'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
				],
				'onboarding/user-details'   => [
					'methods'             => 'POST',
					'callback'            => [ $this, 'save_onboarding_user_details' ],
					'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
					'args'                => [
						'first_name' => [
							'required'          => true,
							'sanitize_callback' => 'sanitize_text_field',
							'validate_callback' => static function( $value ) {
								return is_string( $value ) && '' !== trim( $value );
							},
						],
						'last_name'  => [
							'required'          => false,
							'sanitize_callback' => 'sanitize_text_field',
						],
						'email'      => [
							'required'          => true,
							'sanitize_callback' => 'sanitize_email',
							'validate_callback' => static function( $value ) {
								return is_string( $value ) && is_email( $value );
							},
						],
					],
				],
				// Plugin status endpoint.
				'plugin-status'             => [
					'methods'             => 'GET',
					'callback'            => [ $this, 'get_plugin_status' ],
					'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
					'args'                => [
						'plugin' => [
							'required'          => true,
							'sanitize_callback' => 'sanitize_text_field',
						],
					],
				],
				// Entries endpoints.
				'entries/list'              => [
					'methods'             => 'GET',
					'callback'            => [ $this, 'get_entries_list' ],
					'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
					'args'                => [
						'form_id'   => [
							'sanitize_callback' => 'absint',
							'default'           => 0,
						],
						'status'    => [
							'sanitize_callback' => 'sanitize_text_field',
							'default'           => 'all',
						],
						'search'    => [
							'sanitize_callback' => 'sanitize_text_field',
							'default'           => '',
						],
						'date_from' => [
							'sanitize_callback' => 'sanitize_text_field',
							'default'           => '',
						],
						'date_to'   => [
							'sanitize_callback' => 'sanitize_text_field',
							'default'           => '',
						],
						'orderby'   => [
							'type'              => 'string',
							'sanitize_callback' => 'sanitize_text_field',
							'default'           => 'created_at',
							'enum'              => [ 'ID', 'id', 'form_id', 'user_id', 'status', 'type', 'created_at', 'updated_at' ],
						],
						'order'     => [
							'type'              => 'string',
							'sanitize_callback' => 'sanitize_text_field',
							'default'           => 'DESC',
							'enum'              => [ 'ASC', 'DESC' ],
						],
						'per_page'  => [
							'sanitize_callback' => 'absint',
							'default'           => 20,
						],
						'page'      => [
							'sanitize_callback' => 'absint',
							'default'           => 1,
						],
					],
				],
				'entries/read-status'       => [
					'methods'             => 'POST',
					'callback'            => [ $this, 'update_entries_read_status' ],
					'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
					'args'                => [
						'entry_ids' => [
							'required'          => true,
							'sanitize_callback' => [ $this, 'sanitize_entry_ids' ],
						],
						'action'    => [
							'required'          => true,
							'sanitize_callback' => 'sanitize_text_field',
							'validate_callback' => [ $this, 'validate_read_action' ],
						],
					],
				],
				'entries/trash'             => [
					'methods'             => 'POST',
					'callback'            => [ $this, 'update_entries_trash_status' ],
					'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
					'args'                => [
						'entry_ids' => [
							'required'          => true,
							'sanitize_callback' => [ $this, 'sanitize_entry_ids' ],
						],
						'action'    => [
							'required'          => true,
							'sanitize_callback' => 'sanitize_text_field',
							'validate_callback' => [ $this, 'validate_trash_action' ],
						],
					],
				],
				'entries/delete'            => [
					'methods'             => 'POST',
					'callback'            => [ $this, 'delete_entries' ],
					'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
					'args'                => [
						'entry_ids' => [
							'required'          => true,
							'sanitize_callback' => [ $this, 'sanitize_entry_ids' ],
						],
					],
				],
				'entries/export'            => [
					'methods'             => 'POST',
					'callback'            => [ $this, 'export_entries' ],
					'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
					'args'                => [
						'entry_ids' => [
							'sanitize_callback' => [ $this, 'sanitize_entry_ids' ],
							'default'           => [],
						],
						'form_id'   => [
							'sanitize_callback' => 'absint',
							'default'           => 0,
						],
						'status'    => [
							'sanitize_callback' => 'sanitize_text_field',
							'default'           => 'all',
						],
						'search'    => [
							'sanitize_callback' => 'sanitize_text_field',
							'default'           => '',
						],
						'date_from' => [
							'sanitize_callback' => 'sanitize_text_field',
							'default'           => '',
						],
						'date_to'   => [
							'sanitize_callback' => 'sanitize_text_field',
							'default'           => '',
						],
					],
				],
				// Get Single Entry Form Data.
				'entry/(?P<id>\d+)/details' => [
					'methods'             => 'GET',
					'callback'            => [ $this, 'get_entry_details' ],
					'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
					'args'                => [
						'id' => [
							'required'          => true,
							'sanitize_callback' => 'absint',
						],
					],
				],
				// Get Single Entry Logs.
				'entry/(?P<id>\d+)/logs'    => [
					'methods'             => 'GET',
					'callback'            => [ $this, 'get_entry_logs' ],
					'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
					'args'                => [
						'id'       => [
							'required'          => true,
							'sanitize_callback' => 'absint',
						],
						'per_page' => [
							'sanitize_callback' => 'absint',
							'default'           => 3,
						],
						'page'     => [
							'sanitize_callback' => 'absint',
							'default'           => 1,
						],
					],
				],
				// Forms listing endpoint.
				'forms'                     => [
					'methods'             => 'GET',
					'callback'            => [ Forms_Data::get_instance(), 'get_forms_list' ],
					'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
					'args'                => [
						'page'      => [
							'type'    => 'integer',
							'default' => 1,
							'minimum' => 1,
						],
						'per_page'  => [
							'type'    => 'integer',
							'minimum' => 1,
							'maximum' => 100,
						],
						'search'    => [
							'type' => 'string',
						],
						'status'    => [
							'type'    => 'string',
							'enum'    => [ 'publish', 'draft', 'trash', 'any' ],
							'default' => 'publish',
						],
						'orderby'   => [
							'type'    => 'string',
							'default' => 'date',
							'enum'    => [ 'date', 'id', 'title', 'modified' ],
						],
						'order'     => [
							'type'    => 'string',
							'default' => 'desc',
							'enum'    => [ 'asc', 'desc' ],
						],
						'date_from' => [
							'type'              => 'string',
							'format'            => 'date',
							'sanitize_callback' => 'sanitize_text_field',
							'validate_callback' => static function( $value ) {
								if ( empty( $value ) ) {
									return true;
								}
								return (bool) strtotime( $value );
							},
						],
						'date_to'   => [
							'type'              => 'string',
							'format'            => 'date',
							'sanitize_callback' => 'sanitize_text_field',
							'validate_callback' => static function( $value ) {
								if ( empty( $value ) ) {
									return true;
								}
								return (bool) strtotime( $value );
							},
						],
					],
				],
				// Export forms endpoint.
				'forms/export'              => [
					'methods'             => 'POST',
					'callback'            => [ Export::get_instance(), 'handle_export_form_rest' ],
					'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
					'args'                => [
						'post_ids' => [
							'required'          => true,
							'type'              => [ 'array', 'string' ],
							'sanitize_callback' => static function( $value ) {
								if ( is_array( $value ) ) {
									return array_map( 'intval', $value );
								}
								return sanitize_text_field( $value );
							},
							'validate_callback' => static function( $value ) {
								if ( is_array( $value ) ) {
									return ! empty( $value );
								}
								return ! empty( trim( $value ) );
							},
						],
					],
				],
				// Import forms endpoint.
				'forms/import'              => [
					'methods'             => 'POST',
					'callback'            => [ Export::get_instance(), 'handle_import_form_rest' ],
					'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
					'args'                => [
						'forms_data'     => [
							'required'          => true,
							'type'              => 'array',
							'validate_callback' => static function( $value ) {
								return is_array( $value ) && ! empty( $value );
							},
						],
						'default_status' => [
							'required'          => false,
							'type'              => 'string',
							'default'           => 'draft',
							'enum'              => [ 'draft', 'publish', 'private' ],
							'sanitize_callback' => 'sanitize_text_field',
						],
					],
				],
				// Form lifecycle management endpoint (trash/restore/delete/draft).
				'forms/manage'              => [
					'methods'             => 'POST',
					'callback'            => [ $this, 'manage_form_lifecycle' ],
					'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
					'args'                => [
						'form_ids' => [
							'required'          => true,
							'type'              => [ 'array', 'integer' ],
							'sanitize_callback' => static function( $value ) {
								if ( is_array( $value ) ) {
									return array_map( 'intval', $value );
								}
								return [ intval( $value ) ];
							},
							'validate_callback' => static function( $value ) {
								if ( is_array( $value ) ) {
									return ! empty( $value );
								}
								return $value > 0;
							},
						],
						'action'   => [
							'required'          => true,
							'type'              => 'string',
							'enum'              => [ 'trash', 'restore', 'delete', 'draft' ],
							'sanitize_callback' => 'sanitize_text_field',
						],
					],
				],
				// Form duplication endpoint.
				'forms/duplicate'           => [
					'methods'             => 'POST',
					'callback'            => [ Duplicate_Form::get_instance(), 'handle_duplicate_form_rest' ],
					'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
					'args'                => [
						'form_id'      => [
							'required'          => true,
							'type'              => 'integer',
							'sanitize_callback' => 'absint',
							'validate_callback' => static function( $value ) {
								return $value > 0;
							},
						],
						'title_suffix' => [
							'required'          => false,
							'type'              => 'string',
							'default'           => __( ' (Copy)', 'sureforms' ),
							'sanitize_callback' => 'sanitize_text_field',
						],
					],
				],
			]
		);
	}
}

Youez - 2016 - github.com/yon3zu
LinuXploit