namespace, '/' . $this->rest_base, array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_items' ), 'permission_callback' => array( $this, 'get_items_permissions_check' ), 'args' => array( 'schema_type' => array( 'description' => __( 'Filter templates by schema type.', 'ai-schema-gen' ), 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', ), ), ), ) ); // POST /templates - Create a user template. register_rest_route( $this->namespace, '/' . $this->rest_base, array( array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'create_item' ), 'permission_callback' => array( $this, 'create_item_permissions_check' ), 'args' => array( 'name' => array( 'description' => __( 'Template name.', 'ai-schema-gen' ), 'type' => 'string', 'required' => true, 'sanitize_callback' => 'sanitize_text_field', ), 'description' => array( 'description' => __( 'Template description.', 'ai-schema-gen' ), 'type' => 'string', 'sanitize_callback' => 'sanitize_textarea_field', ), 'schema_type' => array( 'description' => __( 'The schema.org type this template is for.', 'ai-schema-gen' ), 'type' => 'string', 'required' => true, 'sanitize_callback' => 'sanitize_text_field', ), 'template_data' => array( 'description' => __( 'The template schema data.', 'ai-schema-gen' ), 'type' => array( 'object', 'string' ), 'required' => true, ), 'is_public' => array( 'description' => __( 'Whether the template is publicly visible.', 'ai-schema-gen' ), 'type' => 'boolean', 'default' => false, ), ), ), ) ); // GET /templates/{id} - Get a single template. register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\w-]+)', array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_item' ), 'permission_callback' => array( $this, 'get_items_permissions_check' ), 'args' => array( 'id' => array( 'description' => __( 'Template ID (numeric) or built-in template slug.', 'ai-schema-gen' ), 'type' => 'string', 'required' => true, 'sanitize_callback' => 'sanitize_text_field', ), ), ), ) ); // DELETE /templates/{id} - Delete a user template. register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( array( 'methods' => WP_REST_Server::DELETABLE, 'callback' => array( $this, 'delete_item' ), 'permission_callback' => array( $this, 'delete_item_permissions_check' ), 'args' => array( 'id' => array( 'description' => __( 'Template ID to delete.', 'ai-schema-gen' ), 'type' => 'integer', 'required' => true, 'sanitize_callback' => 'absint', ), ), ), ) ); } /** * List all templates, merging built-in and user templates. * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Response Response object. */ public function get_items( $request ) { $schema_type = sanitize_text_field( $request->get_param( 'schema_type' ) ); // Load built-in templates from JSON files. $builtin = $this->get_builtin_templates(); // Load user templates from the database. $user_templates = $this->get_user_templates(); // Merge them. $all_templates = array_merge( $builtin, $user_templates ); // Filter by schema type if requested. if ( $schema_type ) { $all_templates = array_filter( $all_templates, function ( $template ) use ( $schema_type ) { return isset( $template['schema_type'] ) && $template['schema_type'] === $schema_type; } ); $all_templates = array_values( $all_templates ); } return $this->success_response( array( 'templates' => $all_templates, 'total' => count( $all_templates ), ) ); } /** * Create a new user template. * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Response|WP_Error Response object or error. */ public function create_item( $request ) { global $wpdb; $name = sanitize_text_field( $request->get_param( 'name' ) ); $description = sanitize_textarea_field( $request->get_param( 'description' ) ); $schema_type = sanitize_text_field( $request->get_param( 'schema_type' ) ); $template_data = $request->get_param( 'template_data' ); $is_public = (bool) $request->get_param( 'is_public' ); // Encode template data if it is an array. if ( is_array( $template_data ) ) { $template_data = wp_json_encode( $template_data ); } if ( ! $template_data || null === json_decode( $template_data ) ) { return $this->error_response( 'schema_ai_invalid_template_data', __( 'Invalid template data. Must be valid JSON.', 'ai-schema-gen' ), 400 ); } $table = esc_sql( $wpdb->prefix . 'schema_ai_templates' ); $now = current_time( 'mysql' ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Required for custom table; write operation (INSERT), caching not applicable. $inserted = $wpdb->insert( $table, array( 'user_id' => get_current_user_id(), 'name' => $name, 'description' => $description, 'schema_type' => $schema_type, 'template_data' => $template_data, 'is_public' => $is_public ? 1 : 0, 'created_at' => $now, 'updated_at' => $now, ), array( '%d', '%s', '%s', '%s', '%s', '%d', '%s', '%s' ) ); if ( false === $inserted ) { return $this->error_response( 'schema_ai_db_error', __( 'Failed to save template.', 'ai-schema-gen' ), 500 ); } $template_id = $wpdb->insert_id; return $this->success_response( array( 'template' => array( 'id' => $template_id, 'name' => $name, 'description' => $description, 'schema_type' => $schema_type, 'template_data' => json_decode( $template_data, true ), 'is_public' => $is_public, 'is_builtin' => false, 'user_id' => get_current_user_id(), 'created_at' => $now, 'updated_at' => $now, ), 'message' => __( 'Template created successfully.', 'ai-schema-gen' ), ), 201 ); } /** * Get a single template by ID or slug. * * Numeric IDs look up user templates in the database; string slugs * look up built-in templates from the JSON files. * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Response|WP_Error Response object or error. */ public function get_item( $request ) { $id = $request->get_param( 'id' ); // Numeric ID: look up in the database. if ( is_numeric( $id ) ) { global $wpdb; $table = esc_sql( $wpdb->prefix . 'schema_ai_templates' ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Required for custom table; no WP API alternative exists. $row = $wpdb->get_row( $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is sanitized via esc_sql() and not user input. "SELECT * FROM {$table} WHERE id = %d", absint( $id ) ) ); if ( ! $row ) { return $this->error_response( 'schema_ai_not_found', __( 'Template not found.', 'ai-schema-gen' ), 404 ); } // Visibility check: only the owner, admins, or public templates are accessible. $is_owner = (int) $row->user_id === get_current_user_id(); $is_public = (bool) $row->is_public; if ( ! $is_owner && ! $is_public && ! current_user_can( 'manage_options' ) ) { return $this->error_response( 'schema_ai_not_found', __( 'Template not found.', 'ai-schema-gen' ), 404 ); } return $this->success_response( array( 'template' => array( 'id' => (int) $row->id, 'name' => $row->name, 'description' => $row->description, 'schema_type' => $row->schema_type, 'template_data' => json_decode( $row->template_data, true ), 'is_public' => (bool) $row->is_public, 'is_builtin' => false, 'user_id' => (int) $row->user_id, 'created_at' => $row->created_at, 'updated_at' => $row->updated_at, ), ) ); } // String slug: look up in built-in templates. $slug = sanitize_text_field( $id ); $builtin = $this->get_builtin_templates(); foreach ( $builtin as $template ) { if ( isset( $template['slug'] ) && $template['slug'] === $slug ) { return $this->success_response( array( 'template' => $template, ) ); } } return $this->error_response( 'schema_ai_not_found', __( 'Template not found.', 'ai-schema-gen' ), 404 ); } /** * Delete a user template. * * Built-in templates cannot be deleted. * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Response|WP_Error Response object or error. */ public function delete_item( $request ) { global $wpdb; $id = absint( $request->get_param( 'id' ) ); $table = esc_sql( $wpdb->prefix . 'schema_ai_templates' ); // Verify the template exists. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Required for custom table; no WP API alternative exists. $existing = $wpdb->get_row( $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is sanitized via esc_sql() and not user input. "SELECT * FROM {$table} WHERE id = %d", $id ) ); if ( ! $existing ) { return $this->error_response( 'schema_ai_not_found', __( 'Template not found. Built-in templates cannot be deleted.', 'ai-schema-gen' ), 404 ); } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Required for custom table; write operation (DELETE), caching not applicable. $deleted = $wpdb->delete( $table, array( 'id' => $id ), array( '%d' ) ); if ( false === $deleted ) { return $this->error_response( 'schema_ai_db_error', __( 'Failed to delete template.', 'ai-schema-gen' ), 500 ); } return $this->success_response( array( 'deleted' => 1, 'message' => __( 'Template deleted successfully.', 'ai-schema-gen' ), ) ); } /** * Load all built-in templates from JSON files. * * Scans the templates/schema-types/ directory for JSON files and * parses each one into a template array. * * @return array Array of built-in template arrays. */ private function get_builtin_templates() { $templates_dir = dirname( dirname( dirname( dirname( __FILE__ ) ) ) ) . '/templates/schema-types'; if ( ! is_dir( $templates_dir ) ) { return array(); } $files = glob( $templates_dir . '/*.json' ); $templates = array(); if ( ! $files ) { return array(); } foreach ( $files as $file ) { $content = file_get_contents( $file ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents $data = json_decode( $content, true ); if ( ! is_array( $data ) ) { continue; } $slug = basename( $file, '.json' ); $templates[] = array( 'id' => 'builtin_' . $slug, 'slug' => $slug, 'name' => isset( $data['name'] ) ? $data['name'] : $slug, 'description' => isset( $data['description'] ) ? $data['description'] : '', 'schema_type' => isset( $data['schema_type'] ) ? $data['schema_type'] : $slug, 'template_data' => isset( $data['template'] ) ? $data['template'] : $data, 'is_public' => true, 'is_builtin' => true, 'user_id' => 0, 'created_at' => null, 'updated_at' => null, ); } return $templates; } /** * Load user-created templates from the database. * * Returns all templates visible to the current user: their own * templates plus any marked as public. * * @return array Array of user template arrays. */ private function get_user_templates() { global $wpdb; $table = esc_sql( $wpdb->prefix . 'schema_ai_templates' ); $user_id = get_current_user_id(); // Get the user's own templates and public templates from others. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Required for custom table; no WP API alternative exists. $results = $wpdb->get_results( $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is sanitized via esc_sql() and not user input. "SELECT * FROM {$table} WHERE user_id = %d OR is_public = 1 ORDER BY created_at DESC", $user_id ) ); if ( ! $results ) { return array(); } $templates = array(); foreach ( $results as $row ) { $templates[] = array( 'id' => (int) $row->id, 'slug' => null, 'name' => $row->name, 'description' => $row->description, 'schema_type' => $row->schema_type, 'template_data' => json_decode( $row->template_data, true ), 'is_public' => (bool) $row->is_public, 'is_builtin' => false, 'user_id' => (int) $row->user_id, 'created_at' => $row->created_at, 'updated_at' => $row->updated_at, ); } return $templates; } /** * Permission check for reading templates. * * @param WP_REST_Request $request Full details about the request. * @return true|WP_Error True if allowed. */ public function get_items_permissions_check( $request ) { if ( ! current_user_can( 'edit_posts' ) ) { return new WP_Error( 'schema_ai_forbidden', __( 'You do not have permission to view templates.', 'ai-schema-gen' ), array( 'status' => 403 ) ); } return true; } /** * Permission check for creating templates. * * @param WP_REST_Request $request Full details about the request. * @return true|WP_Error True if allowed. */ public function create_item_permissions_check( $request ) { if ( ! current_user_can( 'manage_options' ) ) { return new WP_Error( 'schema_ai_forbidden', __( 'You do not have permission to create templates.', 'ai-schema-gen' ), array( 'status' => 403 ) ); } return true; } /** * Permission check for deleting templates. * * @param WP_REST_Request $request Full details about the request. * @return true|WP_Error True if allowed. */ public function delete_item_permissions_check( $request ) { if ( ! current_user_can( 'manage_options' ) ) { return new WP_Error( 'schema_ai_forbidden', __( 'You do not have permission to delete templates.', 'ai-schema-gen' ), array( 'status' => 403 ) ); } return true; } }