\WP_REST_Server::CREATABLE, 'args' => [ 'key' => [ 'type' => 'string', 'sanitize_callback' => function ( $param ) { return (string) esc_attr( $param ); }, 'validate_callback' => function ( $param ) { return is_string( $param ); }, ], 'action' => [ 'type' => 'string', 'sanitize_callback' => function ( $param ) { return (string) esc_attr( $param ); }, 'validate_callback' => function ( $param ) { return in_array( $param, [ 'activate', 'deactivate' ], true ); }, ], ], 'permission_callback' => function () { return current_user_can( 'manage_options' ); }, 'callback' => [ $this, 'license' ], ], ] ); register_rest_route( $namespace, '/settings', [ [ 'methods' => \WP_REST_Server::CREATABLE, 'args' => [ 'data' => [ 'type' => 'object', 'required' => true, 'sanitize_callback' => [ $this, 'sanitize_settings' ], 'validate_callback' => function ( $param ) { if ( ! is_array( $param ) ) { return false; } $schema = $this->get_settings_schema(); foreach ( array_keys( $param ) as $key ) { if ( ! isset( $schema[ $key ] ) ) { return false; } } return true; }, ], ], 'permission_callback' => function () { return current_user_can( 'manage_options' ); }, 'callback' => [ $this, 'save_settings' ], ], ] ); register_rest_route( $namespace, '/import', [ [ 'methods' => \WP_REST_Server::CREATABLE, 'permission_callback' => function () { return current_user_can( 'manage_options' ); }, 'callback' => [ $this, 'import_snippets' ], ], ] ); register_rest_route( $namespace, '/export', [ [ 'methods' => \WP_REST_Server::CREATABLE, 'args' => [ 'status' => [ 'type' => 'string', 'default' => 'all', 'sanitize_callback' => 'sanitize_text_field', ], 'types' => [ 'type' => 'array', 'default' => [], 'items' => [ 'type' => 'string', ], ], 'tags' => [ 'type' => 'array', 'default' => [], 'items' => [ 'type' => 'string', ], ], ], 'permission_callback' => function () { return current_user_can( 'manage_options' ); }, 'callback' => [ $this, 'export_snippets' ], ], ] ); register_rest_route( $namespace, '/sync', [ [ 'methods' => \WP_REST_Server::CREATABLE, 'args' => [ 'title' => [ 'type' => 'string', 'required' => true, 'sanitize_callback' => 'sanitize_text_field', 'validate_callback' => function ( $param ) { return is_string( $param ) && ! empty( trim( $param ) ); }, ], 'id' => [ 'type' => 'integer', 'required' => true, 'sanitize_callback' => 'absint', 'validate_callback' => function ( $param ) { return is_numeric( $param ) && $param > 0; }, ], ], 'permission_callback' => function () { return current_user_can( 'manage_options' ); }, 'callback' => [ $this, 'sync_snippet' ], ], ] ); } /** * Get settings schema (name => type mapping) * * @return array */ private function get_settings_schema() { $settings = WINP_Settings::get_settings(); $schema = []; foreach ( $settings as $setting ) { if ( isset( $setting['name'] ) && isset( $setting['type'] ) ) { $schema[ $setting['name'] ] = $setting['type']; } } return $schema; } /** * Sanitize settings data based on schema * * @param mixed $data Raw settings data. * * @return array Sanitized settings data. */ public function sanitize_settings( $data ) { if ( ! is_array( $data ) ) { return []; } $schema = $this->get_settings_schema(); $sanitized = []; foreach ( $data as $key => $value ) { if ( ! isset( $schema[ $key ] ) ) { continue; } switch ( $schema[ $key ] ) { case 'checkbox': if ( is_bool( $value ) ) { $sanitized[ $key ] = $value; } elseif ( is_numeric( $value ) ) { $sanitized[ $key ] = (bool) (int) $value; } elseif ( is_string( $value ) ) { $sanitized[ $key ] = in_array( strtolower( $value ), [ 'true', '1', 'yes', 'on' ], true ); } else { $sanitized[ $key ] = (bool) $value; } break; case 'integer': $sanitized[ $key ] = absint( $value ); break; case 'email': $sanitized_email = sanitize_email( $value ); if ( is_email( $sanitized_email ) ) { $sanitized[ $key ] = $sanitized_email; } break; case 'dropdown': case 'text': case 'textbox': default: $sanitized[ $key ] = sanitize_text_field( $value ); break; } } return $sanitized; } /** * Handle license activation/deactivation. * * @param \WP_REST_Request> $request Rest request. * * @return \WP_REST_Response */ public function license( \WP_REST_Request $request ) { $data = $request->get_param( 'data' ); if ( ! isset( $data['key'] ) || ! isset( $data['action'] ) ) { return new \WP_REST_Response( [ 'message' => __( 'This action is no longer valid. Please refresh the page and try again.', 'insert-php' ), 'success' => false, ] ); } $response = WINP_Plugin::app()->premium->toggle_license( $data['action'], $data['key'] ); if ( is_wp_error( $response ) ) { return new \WP_REST_Response( [ 'message' => $response->get_error_message(), 'success' => false, ] ); } return new \WP_REST_Response( $response ); } /** * Handle settings save. * * @param \WP_REST_Request> $request Rest request. * * @return \WP_REST_Response */ public function save_settings( \WP_REST_Request $request ) { $data = $request->get_param( 'data' ); if ( empty( $data ) ) { return new \WP_REST_Response( [ 'message' => __( 'No changes detected. Modify at least one setting before saving.', 'insert-php' ), 'success' => false, ] ); } foreach ( $data as $key => $value ) { update_option( 'wbcr_inp_' . $key, $value ); } return new \WP_REST_Response( [ 'success' => true, 'message' => __( 'Settings saved successfully.', 'insert-php' ), ] ); } /** * Handle snippet import. * * @param \WP_REST_Request> $request Rest request. * * @return \WP_REST_Response */ public function import_snippets( \WP_REST_Request $request ) { $files = $request->get_file_params(); $duplicate_action = $request->get_param( 'duplicate_action' ); // Validate duplicate action. if ( ! in_array( $duplicate_action, [ 'ignore', 'replace', 'skip' ], true ) ) { return new \WP_REST_Response( [ // translators: %s is the invalid duplicate action. 'message' => sprintf( __( 'Invalid duplicate action: "%s". Expected: ignore, replace, or skip.', 'insert-php' ), $duplicate_action ), 'success' => false, ], 400 ); } // Check if files were uploaded. if ( empty( $files ) ) { return new \WP_REST_Response( [ 'message' => __( 'No files were uploaded. Please select a file and try again.', 'insert-php' ), 'success' => false, ], 400 ); } $max_file_size = 2 * 1024 * 1024; // 2MB in bytes. $errors = []; // Normalize file array structure (WordPress may structure it differently). $normalized_files = []; if ( isset( $files['files'] ) ) { // files[] format - need to normalize. $file_count = count( $files['files']['name'] ); for ( $i = 0; $i < $file_count; $i++ ) { $normalized_files[] = [ 'name' => $files['files']['name'][ $i ], 'type' => $files['files']['type'][ $i ], 'tmp_name' => $files['files']['tmp_name'][ $i ], 'error' => $files['files']['error'][ $i ], 'size' => $files['files']['size'][ $i ], ]; } } else { $normalized_files = $files; } $validated_files = []; foreach ( $normalized_files as $file ) { if ( ! isset( $file['error'] ) || is_array( $file['error'] ) ) { $errors[] = __( 'The file could not be uploaded. Please ensure it\'s a valid .json or .zip file.', 'insert-php' ); continue; } if ( UPLOAD_ERR_OK !== $file['error'] ) { // translators: %s is the file name. $errors[] = sprintf( __( 'Upload error for file: %s', 'insert-php' ), $file['name'] ); continue; } if ( $file['size'] > $max_file_size ) { // translators: %s is the file name. $errors[] = sprintf( __( 'File too large: %s (maximum 2MB)', 'insert-php' ), $file['name'] ); continue; } $file_extension = strtolower( pathinfo( $file['name'], PATHINFO_EXTENSION ) ); if ( ! in_array( $file_extension, [ 'json', 'zip' ], true ) ) { // translators: %s is the file name. $errors[] = sprintf( __( 'Invalid file type: %s (only .json and .zip allowed)', 'insert-php' ), $file['name'] ); continue; } // Additional MIME type validation. $finfo = finfo_open( FILEINFO_MIME_TYPE ); if ( false === $finfo ) { // translators: %s is the file name. $errors[] = sprintf( __( 'Could not verify the file type for "%s". Please use a .json or .zip file.', 'insert-php' ), $file['name'] ); continue; } $mime_type = finfo_file( $finfo, $file['tmp_name'] ); finfo_close( $finfo ); $allowed_mime_types = [ 'application/json', 'text/plain', 'application/zip', 'application/x-zip-compressed', ]; if ( ! in_array( $mime_type, $allowed_mime_types, true ) ) { // translators: %s is the file name. $errors[] = sprintf( __( 'Invalid file MIME type: %s', 'insert-php' ), $file['name'] ); continue; } $validated_files[] = $file; } // If no valid files, return error. if ( empty( $validated_files ) ) { return new \WP_REST_Response( [ 'message' => __( 'No valid files to import.', 'insert-php' ), 'success' => false, 'errors' => $errors, ], 400 ); } // Process import using the import snippet class. if ( ! class_exists( 'WINP_Import_Snippet' ) ) { require_once WINP_PLUGIN_DIR . '/admin/includes/class.import.snippet.php'; } $import_handler = new WINP_Import_Snippet(); $result = $import_handler->process_import_files( $validated_files, $duplicate_action ); // Merge validation errors with import errors. $all_errors = array_merge( $errors, $result['errors'] ); if ( $result['count'] > 0 ) { // translators: %d is the number of imported snippets. $message = sprintf( // translators: %d is the number of imported snippets. _n( 'Successfully imported %d snippet.', 'Successfully imported %d snippets.', $result['count'], 'insert-php' ), $result['count'] ); if ( ! empty( $all_errors ) ) { $message .= ' ' . __( 'Some files had errors.', 'insert-php' ); } return new \WP_REST_Response( [ 'success' => true, 'message' => $message, 'errors' => $all_errors, 'count' => $result['count'], ] ); } return new \WP_REST_Response( [ 'message' => __( 'No snippets were imported. Please check your file contains valid snippet data.', 'insert-php' ), 'success' => false, 'errors' => $all_errors, ] ); } /** * Handle snippet export. * * @param \WP_REST_Request> $request Rest request. * * @return \WP_REST_Response */ public function export_snippets( \WP_REST_Request $request ) { $status = $request->get_param( 'status' ); $types = $request->get_param( 'types' ); $tags = $request->get_param( 'tags' ); $status = sanitize_text_field( $status ); $types = array_map( 'sanitize_text_field', (array) $types ); $tags = array_map( 'sanitize_text_field', (array) $tags ); // Build query conditions. $meta_query_conditions = []; $tax_query_conditions = []; // Status filter. if ( 'all' !== $status ) { if ( 'active' === $status ) { // Active: wbcr_inp_snippet_activate = 1. $meta_query_conditions[] = [ 'key' => 'wbcr_inp_snippet_activate', 'value' => 1, ]; } else { // Inactive: wbcr_inp_snippet_activate != 1 OR doesn't exist. $meta_query_conditions[] = [ 'relation' => 'OR', [ 'key' => 'wbcr_inp_snippet_activate', 'value' => 1, 'compare' => '!=', ], [ 'key' => 'wbcr_inp_snippet_activate', 'compare' => 'NOT EXISTS', ], ]; } } // Types filter. if ( ! empty( $types ) ) { if ( count( $types ) > 1 ) { $type_condition = [ 'relation' => 'OR' ]; foreach ( $types as $type ) { $type_condition[] = [ 'key' => 'wbcr_inp_snippet_type', 'value' => $type, ]; } } else { $type_condition = [ 'key' => 'wbcr_inp_snippet_type', 'value' => $types[0], ]; } $meta_query_conditions[] = $type_condition; } // Tags filter. if ( ! empty( $tags ) ) { // Ensure taxonomy exists before querying. if ( ! taxonomy_exists( WINP_SNIPPETS_TAXONOMY ) ) { register_taxonomy( WINP_SNIPPETS_TAXONOMY, WINP_SNIPPETS_POST_TYPE, [] ); } $tax_query_conditions = [ [ 'taxonomy' => WINP_SNIPPETS_TAXONOMY, 'field' => 'slug', 'terms' => $tags, 'operator' => 'IN', ], ]; } if ( count( $meta_query_conditions ) > 1 ) { $meta_query_conditions['relation'] = 'AND'; } // Build final query. $conditions = [ 'post_type' => WINP_SNIPPETS_POST_TYPE, 'post_status' => 'publish', 'numberposts' => -1, ]; if ( ! empty( $meta_query_conditions ) ) { $conditions['meta_query'] = $meta_query_conditions; } if ( ! empty( $tax_query_conditions ) ) { $conditions['tax_query'] = $tax_query_conditions; } // Query snippets. $snippets = get_posts( $conditions ); if ( empty( $snippets ) ) { return new \WP_REST_Response( [ 'message' => __( 'No snippets found. Try adjusting your filters or search terms.', 'insert-php' ), 'success' => false, ], 404 ); } $ids = wp_list_pluck( $snippets, 'ID' ); require_once WINP_PLUGIN_DIR . '/admin/includes/class.actions.snippet.php'; $exporter = new WINP_Actions_Snippet(); $result = $exporter->export_snippets( $ids, true ); if ( $result['is_zip'] ) { // For ZIP files, encode as base64 for JSON transport. $data = base64_encode( $result['data'] ); } else { // For JSON files, encode as pretty-printed JSON. $data = wp_json_encode( $result['data'], JSON_PRETTY_PRINT ); } return new \WP_REST_Response( [ 'success' => true, 'filename' => $result['filename'], 'data' => $data, 'count' => $result['count'], 'is_zip' => $result['is_zip'], ] ); } /** * Handle snippet sync to cloud. * * @param \WP_REST_Request $request Rest request. * @phpstan-param \WP_REST_Request> $request * * @return \WP_REST_Response */ public function sync_snippet( \WP_REST_Request $request ) { $title = $request->get_param( 'title' ); $snippet_id = absint( $request->get_param( 'id' ) ); // Verify the snippet exists. $snippet = get_post( $snippet_id ); if ( ! $snippet || WINP_SNIPPETS_POST_TYPE !== $snippet->post_type ) { return new \WP_REST_Response( [ 'message' => __( 'Snippet not found. It may have been deleted or moved.', 'insert-php' ), 'success' => false, ], 404 ); } // Verify the current user has permission to edit this specific snippet. if ( ! current_user_can( 'edit_post', $snippet_id ) ) { return new \WP_REST_Response( [ 'message' => __( 'You do not have permission to sync this snippet.', 'insert-php' ), 'success' => false, ], 403 ); } // Sync snippet using the API object. $result = WINP_Plugin::app()->get_api_object()->synchronization( $snippet_id, $title ); // synchronization() returns true on success, error string on failure, or false if post doesn't exist. if ( true === $result ) { return new \WP_REST_Response( [ 'success' => true, 'message' => __( 'Snippet saved as template successfully.', 'insert-php' ), ], 200 ); } // If result is a string, it's an error message. If false, it's a generic error. $error_message = is_string( $result ) ? $result : __( 'Failed to sync snippet. Please check your connection and try again.', 'insert-php' ); return new \WP_REST_Response( [ 'success' => false, 'message' => $error_message, ], 500 ); } }