KOK - MANAGER
Edit File: RestAPI.php
<?php /** * REST API setup. * * @copyright (c) 2024, Code Atlantic LLC. * @package PopupMaker */ namespace PopupMaker\Controllers; use PopupMaker\Plugin\Controller; use PopupMaker\Plugin\Container; use WP_Error; use WP_REST_Server; use WP_REST_Request; /** * REST controller. * * @since 1.21.0 */ class RestAPI extends Controller { /** * Init controller. * * @return void */ public function init() { // Register custom REST API fields. add_action( 'init', [ $this, 'register_popup_rest_fields' ] ); add_action( 'init', [ $this, 'register_cta_rest_fields' ] ); // Register custom REST API endpoints. add_action( 'rest_api_init', [ $this, 'register_routes' ] ); // Register Pro upgrader REST routes. add_action( 'rest_api_init', [ $this, 'register_pro_upgrader_routes' ] ); // Authentication. add_filter( 'rest_pre_dispatch', [ $this, 'rest_pre_dispatch' ], 10, 3 ); // Legacy AJAX handlers for license status polling. add_action( 'wp_ajax_pum_check_license_status', [ $this, 'ajax_check_license_status' ] ); // Sanitize and validate filters. // add_filter( 'popup_maker/sanitize_popup_settings', [ $this, 'sanitize_popup_settings' ], 10, 2 ); // add_filter( 'popup_maker/validate_popup_settings', [ $this, 'validate_popup_settings' ], 10, 2 ); add_filter( 'popup_maker/sanitize_call_to_action_settings', [ $this, 'sanitize_call_to_action_settings' ], 10, 2 ); add_filter( 'popup_maker/validate_call_to_action_settings', [ $this, 'validate_call_to_action_settings' ], 10, 2 ); } /** * Register Rest API routes. * * @return void */ public function register_routes() { ( new \PopupMaker\RestAPI\Connect() )->register_routes(); ( new \PopupMaker\RestAPI\License() )->register_routes(); ( new \PopupMaker\RestAPI\ObjectSearch() )->register_routes(); } /** * Register Pro upgrader REST API routes. * * Registers endpoints for Pro upgrade workflow including license validation, * connection verification, and upgrade processing. * * @return void */ public function register_pro_upgrader_routes() { // Register v2 REST API controllers. $this->register_v2_controllers(); // Legacy v1 namespace for backward compatibility. $namespace = 'popup-maker/v1'; // License validation endpoint. register_rest_route( $namespace, '/license/validate', [ [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'rest_validate_license' ], 'permission_callback' => [ $this, 'rest_pro_upgrade_permissions' ], 'args' => [ 'license_key' => [ 'required' => false, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', 'validate_callback' => function ( $param ) { return is_string( $param ) && ! empty( trim( $param ) ); }, ], ], ], ] ); // Connection verification endpoint. register_rest_route( $namespace, '/connect/verify', [ [ 'methods' => WP_REST_Server::CREATABLE, 'callback' => [ $this, 'rest_verify_connection' ], 'permission_callback' => '__return_true', // Webhook uses own authentication. 'args' => [ 'token' => [ 'required' => true, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', ], 'nonce' => [ 'required' => true, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', ], ], ], ] ); // Pro upgrade installation endpoint. register_rest_route( $namespace, '/upgrade/install', [ [ 'methods' => WP_REST_Server::CREATABLE, 'callback' => [ $this, 'rest_install_pro' ], 'permission_callback' => '__return_true', // Webhook uses own authentication. 'args' => [ 'token' => [ 'required' => true, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', ], 'nonce' => [ 'required' => true, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', ], 'file' => [ 'required' => true, 'type' => 'string', 'sanitize_callback' => 'esc_url_raw', 'validate_callback' => function ( $param ) { return filter_var( $param, FILTER_VALIDATE_URL ) !== false; }, ], 'slug' => [ 'required' => true, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', 'validate_callback' => function ( $param ) { return is_string( $param ) && ! empty( trim( $param ) ); }, ], 'force' => [ 'required' => false, 'type' => 'boolean', 'default' => false, 'sanitize_callback' => function ( $param ) { return (bool) $param; }, ], ], ], ] ); // Connection info generation endpoint. register_rest_route( $namespace, '/connect/info', [ [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'rest_get_connect_info' ], 'permission_callback' => [ $this, 'rest_pro_upgrade_permissions' ], ], ] ); } /** * Register v2 REST API controllers. * * @return void */ private function register_v2_controllers() { // License controller. $license_controller = new \PopupMaker\RestAPI\License(); $license_controller->register_routes(); // Connect controller. $connect_controller = new \PopupMaker\RestAPI\Connect(); $connect_controller->register_routes(); } /** * Permission callback for Pro upgrade endpoints. * * @return bool True if user has permission, false otherwise. */ public function rest_pro_upgrade_permissions() { return current_user_can( $this->container->get_permission( 'edit_popups' ) ); } /** * REST endpoint: Validate license for upgrade. * * @param WP_REST_Request $request REST request object. * @return \WP_REST_Response|\WP_Error */ public function rest_validate_license( $request ) { $license_service = $this->container->get( 'license' ); // If license key provided, update it first. $license_key = $request->get_param( 'license_key' ); if ( ! empty( $license_key ) ) { $license_service->maybe_update_license_key( $license_key ); } // Validate license for upgrade. $validation_result = $license_service->validate_for_upgrade(); return rest_ensure_response( $validation_result ); } /** * REST endpoint: Verify webhook connection. * * @param WP_REST_Request $request REST request object. * @return \WP_REST_Response|\WP_Error */ public function rest_verify_connection( $request ) { $connect_service = $this->container->get( 'connect' ); // Verify the webhook request. $verification_result = $connect_service->verify_webhook_request( $request ); if ( ! $verification_result['valid'] ) { return new WP_Error( 'verification_failed', $verification_result['error'], [ 'status' => 403 ] ); } return rest_ensure_response( [ 'success' => true, 'message' => 'Connection verified successfully', ] ); } /** * REST endpoint: Install Pro plugin. * * @param WP_REST_Request $request REST request object. * @return \WP_REST_Response|\WP_Error */ public function rest_install_pro( $request ) { $connect_service = $this->container->get( 'connect' ); // Verify the webhook request. $verification_result = $connect_service->verify_webhook_request( $request ); if ( ! $verification_result['valid'] ) { return new WP_Error( 'verification_failed', $verification_result['error'], [ 'status' => 403 ] ); } // Prepare installation arguments. $args = [ 'file' => $request->get_param( 'file' ), 'type' => 'plugin', 'slug' => $request->get_param( 'slug' ), 'force' => $request->get_param( 'force' ), ]; // Verify webhook args. $connect_service->verify_webhook_args( $args ); // Set the current screen to avoid undefined notices. set_current_screen( 'settings_page_popup-maker-settings' ); try { // Install the plugin using the connect service. $connect_service->install_plugin( $args ); } catch ( \Exception $e ) { return new WP_Error( 'installation_failed', $e->getMessage(), [ 'status' => 500 ] ); } // If we reach here, installation was successful. return rest_ensure_response( [ 'success' => true, 'message' => 'Plugin installed and activated successfully', ] ); } /** * REST endpoint: Get connection info. * * @param WP_REST_Request $request REST request object. * @return \WP_REST_Response|\WP_Error */ public function rest_get_connect_info( $request ) { $license_service = $this->container->get( 'license' ); // Generate connection info. $connect_info = $license_service->generate_connect_info(); if ( null === $connect_info ) { return new WP_Error( 'connection_unavailable', 'Connection info not available. License may not be active or Pro may already be installed.', [ 'status' => 400 ] ); } return rest_ensure_response( $connect_info ); } protected function register_data_version_field( $post_type, $update_permission ) { register_rest_field( $post_type, 'data_version', [ 'get_callback' => function ( $obj ) { return get_post_meta( $obj['id'], 'data_version', true ); }, 'update_callback' => function ( $value, $obj ) { // Update the field/meta value. update_post_meta( $obj->ID, 'data_version', $value ); }, 'permission_callback' => function () use ( $update_permission ) { return current_user_can( $update_permission ); }, ] ); } /** * Register common REST fields for a given post type * * @return void */ public function register_popup_rest_fields() { $post_type = $this->container->get_controller( 'PostTypes' )->get_type_key( 'popup' ); $edit_permission = $this->container->get_permission( 'edit_popups' ); register_rest_field( $post_type, 'enabled', [ 'get_callback' => function ( $obj ) { return get_post_meta( $obj['id'], 'enabled', true ); }, 'update_callback' => function ( $value, $obj ) { update_post_meta( $obj->ID, 'enabled', $value ); }, 'permission_callback' => function () use ( $edit_permission ) { return current_user_can( $edit_permission ); }, ] ); register_rest_field( $post_type, 'settings', [ 'get_callback' => function ( $obj, $field, $request ) { $popup = pum_get_popup( $obj['id'] ); // If edit context, return the current settings. if ( 'edit' === $request['context'] ) { $settings = $popup->get_settings(); } else { // Otherwise, return the public settings. $settings = $popup->get_public_settings(); } return $settings; }, 'update_callback' => function ( $value, $obj ) { $popup = pum_get_popup( $obj->ID ); $popup->update_settings( $value ); }, 'schema' => [ 'type' => 'object', 'arg_options' => [ 'sanitize_callback' => function ( $settings, $request ) { /** * Sanitize the popup settings. * * @param array<string,mixed> $settings The settings to sanitize. * @param int $id The popup ID. * @param \WP_REST_Request $request The request object. * * @return array<string,mixed> The sanitized settings. */ return apply_filters( 'popup_maker/sanitize_popup_settings', $settings, $request->get_param( 'id' ), $request ); }, 'validate_callback' => function ( $settings, $request ) { /** * Validate the popup settings. * * @param array<string,mixed> $settings The settings to validate. * @param int $id The popup ID. * @param \WP_REST_Request $request The request object. * * @return bool|\WP_Error True if valid, WP_Error if not. */ return apply_filters( 'popup_maker/validate_popup_settings', $settings, $request->get_param( 'id' ), $request ); }, ], ], 'permission_callback' => function () use ( $edit_permission ) { return current_user_can( $edit_permission ); }, ] ); register_rest_field( $post_type, 'priority', [ 'get_callback' => function ( $obj ) { return (int) get_post_field( 'menu_order', $obj['id'], 'raw' ); }, 'update_callback' => function ( $value, $obj ) { wp_update_post( [ 'ID' => $obj->ID, 'menu_order' => $value, ] ); }, 'permission_callback' => function () use ( $edit_permission ) { return current_user_can( $edit_permission ); }, 'schema' => [ 'type' => 'integer', 'arg_options' => [ 'sanitize_callback' => function ( $priority ) { return absint( $priority ); }, 'validate_callback' => function ( $priority ) { return is_int( $priority ); }, ], ], ] ); // Register data version field. $this->register_data_version_field( $post_type, $edit_permission ); } /** * Sanitize popup settings. * * @param array<string,mixed> $settings The settings to sanitize. * @param int $id The popup ID. * * @return array<string,mixed> The sanitized settings. */ public function sanitize_popup_settings( $settings, $id ) { return $settings; } /** * Validate popup settings. * * @param array<string,mixed> $settings The settings to validate. * @param int $id The popup ID. * * @return bool|\WP_Error True if valid, WP_Error if not. */ public function validate_popup_settings( $settings, $id ) { // TODO Validate all known settings by type. return true; } /** * Registers custom REST API fields for call to action post type. * * @return void */ public function register_cta_rest_fields() { $post_type = $this->container->get_controller( 'PostTypes' )->get_type_key( 'pum_cta' ); $edit_permission = $this->container->get_permission( 'edit_ctas' ); $ctas = $this->container->get( 'ctas' ); $valid_statuses = [ 'publish', 'future', 'draft', 'pending', 'private', 'trash' ]; register_rest_field( $post_type, 'status', [ 'get_callback' => function ( $obj ) { return get_post_status( $obj['id'] ); }, 'update_callback' => function ( $value, $obj ) { wp_update_post( [ 'ID' => $obj->ID, 'post_status' => $value, ] ); }, 'permission_callback' => function () use ( $edit_permission ) { return current_user_can( $edit_permission ); }, 'schema' => [ 'type' => 'string', 'enum' => [ 'publish', 'trash', 'draft' ], 'default' => 'publish', 'arg_options' => [ 'sanitize_callback' => function ( $status ) use ( $valid_statuses ) { return in_array( $status, $valid_statuses, true ) ? $status : 'publish'; }, 'validate_callback' => function ( $status ) use ( $valid_statuses ) { return in_array( $status, $valid_statuses, true ); }, ], ], ] ); // Register uuid field. Should be read-only &restricted to admins similar to edit only props. register_rest_field( $post_type, 'uuid', [ 'get_callback' => function ( $obj ) { $cta = \PopupMaker\get_cta_by_id( $obj['id'] ); if ( ! $cta ) { return null; } $uuid = $cta->get_uuid(); return $uuid; }, 'permission_callback' => function () use ( $edit_permission ) { return current_user_can( $edit_permission ); }, ] ); // Register conversion counts field. register_rest_field( $post_type, 'stats', [ 'get_callback' => function ( $obj ) { $cta = \PopupMaker\get_cta_by_id( $obj['id'] ); return [ 'conversions' => $cta->get_event_count( 'conversion' ), ]; }, 'update_callback' => null, 'schema' => [ 'description' => __( 'Stats for this CTA.', 'popup-maker' ), 'type' => 'object', 'properties' => [ 'conversion' => [ 'type' => 'integer', 'minimum' => 0, ], ], ], ] ); // Register settings field. register_rest_field( $post_type, 'settings', [ 'get_callback' => function ( $obj, $field, $request ) use ( $ctas ) { $cta = \PopupMaker\get_cta_by_id( $obj['id'] ); if ( ! $cta ) { return []; } $settings = []; // If edit context, return the current settings. if ( 'edit' === $request['context'] ) { $settings = get_post_meta( $obj['id'], 'cta_settings', true ); if ( empty( $settings ) ) { $settings = \PopupMaker\get_default_call_to_action_settings(); } } else { // Otherwise, return the public settings. $settings = $cta->get_public_settings(); } return $settings; }, 'update_callback' => function ( $value, $obj ) { update_post_meta( $obj->ID, 'cta_settings', $value ); }, 'schema' => [ 'type' => 'object', 'arg_options' => [ 'sanitize_callback' => function ( $settings, $request ) { /** * Sanitize the call to action settings. * * @param array<string,mixed> $settings The settings to sanitize. * @param int $id The call to action ID. * @param \WP_REST_Request $request The request object. * * @return array<string,mixed> The sanitized settings. */ return apply_filters( 'popup_maker/sanitize_call_to_action_settings', $settings, $request->get_param( 'id' ), $request ); }, 'validate_callback' => function ( $settings, $request ) { /** * Validate the popup settings. * * @param array<string,mixed> $settings The settings to validate. * @param int $id The popup ID. * @param \WP_REST_Request $request The request object. * * @return bool|\WP_Error True if valid, WP_Error if not. */ return apply_filters( 'popup_maker/validate_call_to_action_settings', $settings, $request->get_param( 'id' ), $request ); }, ], ], 'permission_callback' => function () use ( $edit_permission ) { return current_user_can( $edit_permission ); }, ] ); // Register data version field. $this->register_data_version_field( $post_type, $edit_permission ); } /** * Sanitize call to action settings. * * @param array<string,mixed> $settings The settings to sanitize. * @param int $id The call to action ID. * * @return array<string,mixed> The sanitized settings. */ public function sanitize_call_to_action_settings( $settings, $id ) { return $settings; } /** * Validate call to action settings. * * @param array<string,mixed> $settings The settings to validate. * @param int $id The call to action ID. * * @return bool|\WP_Error True if valid, WP_Error if not. */ public function validate_call_to_action_settings( $settings, $id ) { if ( empty( $settings['type'] ) ) { return new \WP_Error( 'missing_type', __( 'CTA type is required', 'popup-maker' ) ); } // Get the CTA type handler $cta_types = $this->container->get( 'cta_types' ); $cta_type = $cta_types->get( $settings['type'] ); if ( ! $cta_type ) { return new \WP_Error( 'invalid_type', __( 'Invalid CTA type', 'popup-maker' ), [ 'status' => 400 ] ); } // Validate settings using the CTA type's validation method $validation_result = $cta_type->validate_settings( $settings ); if ( is_array( $validation_result ) ) { // Merge each field error into a single error. $error = new \WP_Error(); foreach ( $validation_result as $field_error ) { $error->add( $field_error->get_error_code(), $field_error->get_error_message(), $field_error->get_error_data() ); } return $error; } if ( is_wp_error( $validation_result ) ) { return $validation_result; } return true; } /** * Prevent access to the popups endpoint. * * @param mixed $result Response to replace the requested version with. * @param \WP_REST_Server $server Server instance. * @param \WP_REST_Request<array<string,mixed>> $request Request used to generate the response. * @return mixed */ public function rest_pre_dispatch( $result, $server, $request ) { // Get the route being requested. $route = $request->get_route(); if ( false === strpos( $route, '/popup-maker/v2' ) ) { return $result; } $current_user_can = true; // Only proceed if the current user has permission. if ( false !== strpos( $route, '/popup-maker/v2/popups' ) ) { $current_user_can = current_user_can( $this->container->get_permission( 'edit_popups' ) ); } elseif ( false !== strpos( $route, '/popup-maker/v2/ctas' ) ) { $current_user_can = current_user_can( $this->container->get_permission( 'edit_ctas' ) ); } // Prevent discovery of the endpoints data from unauthorized users. if ( ! $current_user_can ) { return new \WP_Error( 'rest_forbidden', __( 'Access to this endpoint requires authorization.', 'popup-maker' ), [ 'status' => rest_authorization_required_code(), ] ); } // Return data to the client to parse. return $result; } /** * AJAX handler for license status checking. * * Legacy handler for license status polling system. * * @return void */ public function ajax_check_license_status() { // Verify nonce for security. if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'popup-maker-settings' ) ) { wp_send_json_error( 'Invalid nonce' ); } // Check user permissions. if ( ! current_user_can( $this->container->get_permission( 'edit_popups' ) ) ) { wp_send_json_error( 'Insufficient permissions' ); } $license_service = $this->container->get( 'license' ); // Get license status data. $license_status = $license_service->get_license_status_data(); $license_key = $license_service->get_license_key(); $is_active = $license_service->is_license_active(); // Check if Pro is installed. $is_pro_installed = $this->container->is_pro_installed(); $is_pro_active = $is_pro_installed && is_plugin_active( 'popup-maker-pro/popup-maker-pro.php' ); $response_data = [ 'is_valid' => $is_active, 'license_key' => $license_key, 'status' => $license_service->get_license_status(), 'expires' => ! empty( $license_status['expires'] ) ? $license_status['expires'] : null, 'pro_installed' => $is_pro_installed, 'is_pro_installed' => $is_pro_installed, 'is_pro_active' => $is_pro_active, ]; wp_send_json_success( $response_data ); } }