namespace, '/cannibalization', array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'check_cannibalization' ), 'permission_callback' => array( $this, 'can_edit_posts' ), 'args' => array( 'keyword' => array( 'required' => true, 'sanitize_callback' => 'sanitize_text_field', ), 'post_id' => array( 'required' => false, 'default' => 0, 'sanitize_callback' => 'absint', ), ), ) ); register_rest_route( $this->namespace, '/ai/generate', array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'handle_ai_generate' ), 'permission_callback' => array( $this, 'can_edit_posts' ), ) ); register_rest_route( $this->namespace, '/bulk-save', array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'handle_bulk_save' ), 'permission_callback' => array( $this, 'can_edit_posts' ), ) ); register_rest_route( $this->namespace, '/modules', array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'toggle_module' ), 'permission_callback' => array( $this, 'can_manage_options' ), ) ); register_rest_route( $this->namespace, '/redirects', array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'create_redirect' ), 'permission_callback' => array( $this, 'can_manage_options' ), ) ); register_rest_route( $this->namespace, '/redirects/(?P\d+)', array( 'methods' => WP_REST_Server::DELETABLE, 'callback' => array( $this, 'delete_redirect' ), 'permission_callback' => array( $this, 'can_manage_options' ), ) ); register_rest_route( $this->namespace, '/404-convert', array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'convert_404_to_redirect' ), 'permission_callback' => array( $this, 'can_manage_options' ), ) ); register_rest_route( $this->namespace, '/migrate', array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'run_competitor_migration' ), 'permission_callback' => array( $this, 'can_manage_options' ), ) ); } /** * Permission check for editors and authors. * * @return bool */ public function can_edit_posts(): bool { return current_user_can( 'edit_posts' ); } /** * Permission check for site administrators. * * @return bool */ public function can_manage_options(): bool { return current_user_can( 'manage_options' ); } /** * Queries `wp_modulustools_meta` in real time to detect keyword cannibalization. * * @param WP_REST_Request $request Incoming request. * @return WP_REST_Response */ public function check_cannibalization( WP_REST_Request $request ): WP_REST_Response { global $wpdb; $keyword = strtolower( trim( (string) $request->get_param( 'keyword' ) ) ); $post_id = absint( $request->get_param( 'post_id' ) ); if ( '' === $keyword ) { return new WP_REST_Response( array( 'conflicts' => array() ), 200 ); } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter $rows = $wpdb->get_results( $wpdb->prepare( 'SELECT object_id, focus_keyword, seo_score FROM %i WHERE LOWER(focus_keyword) = %s AND object_type = %s AND object_id != %d LIMIT 5', $wpdb->prefix . 'modulustools_meta', $keyword, 'post', $post_id ) ); $conflicts = array(); if ( is_array( $rows ) ) { foreach ( $rows as $row ) { $conflict_post = get_post( (int) $row->object_id ); if ( $conflict_post && 'publish' === $conflict_post->post_status ) { $conflicts[] = array( 'id' => (int) $conflict_post->ID, 'title' => get_the_title( $conflict_post ), 'edit_link' => get_edit_post_link( $conflict_post->ID, 'raw' ), 'permalink' => get_permalink( $conflict_post->ID ), 'seo_score' => (int) $row->seo_score, ); } } } return new WP_REST_Response( array( 'keyword' => $keyword, 'keyword_type' => Core::classify_keyword_type( $keyword ), 'conflicts' => $conflicts, ), 200 ); } /** * Saves bulk-edited SEO rows from the Bulk Quick-Editor Grid. * * @param WP_REST_Request $request Incoming request. * @return WP_REST_Response */ public function handle_bulk_save( WP_REST_Request $request ): WP_REST_Response { $items = $request->get_param( 'items' ); if ( ! is_array( $items ) ) { return new WP_REST_Response( array( 'success' => false, 'message' => __( 'Invalid payload.', 'modulustools-seo-suite' ) ), 400 ); } $saved_count = 0; foreach ( $items as $item ) { if ( ! is_array( $item ) || empty( $item['post_id'] ) ) { continue; } $post_id = absint( $item['post_id'] ); if ( ! current_user_can( 'edit_post', $post_id ) ) { continue; } Core::save_meta( $post_id, 'post', array( 'title' => isset( $item['title'] ) ? sanitize_text_field( (string) $item['title'] ) : '', 'description' => isset( $item['description'] ) ? sanitize_textarea_field( (string) $item['description'] ) : '', 'focus_keyword' => isset( $item['focus_keyword'] ) ? sanitize_text_field( (string) $item['focus_keyword'] ) : '', 'robots_index' => isset( $item['robots_index'] ) && 'noindex' === $item['robots_index'] ? 'noindex' : 'index', 'is_pillar' => ! empty( $item['is_pillar'] ) ? 1 : 0, ) ); ++$saved_count; } return new WP_REST_Response( array( 'success' => true, 'saved_count' => $saved_count, ), 200 ); } /** * Invokes the AI Engine to generate SEO drafts. * * @param WP_REST_Request $request Incoming request. * @return WP_REST_Response */ public function handle_ai_generate( WP_REST_Request $request ): WP_REST_Response { $task = sanitize_key( (string) $request->get_param( 'task' ) ); $title = sanitize_text_field( (string) $request->get_param( 'title' ) ); $content = wp_strip_all_tags( (string) $request->get_param( 'content' ) ); $keyword = sanitize_text_field( (string) $request->get_param( 'keyword' ) ); $result = AI_BYOK::generate_content( $task, $title, $content, $keyword ); if ( is_wp_error( $result ) ) { return new WP_REST_Response( array( 'success' => false, 'message' => $result->get_error_message(), ), 400 ); } return new WP_REST_Response( array( 'success' => true, 'task' => $task, 'output' => $result, ), 200 ); } /** * Toggles a module ON or OFF via REST. * * @param WP_REST_Request $request Incoming request. * @return WP_REST_Response */ public function toggle_module( WP_REST_Request $request ): WP_REST_Response { $module = sanitize_key( (string) $request->get_param( 'module' ) ); $enabled = (bool) $request->get_param( 'enabled' ); $available = Core::get_available_modules(); if ( ! isset( $available[ $module ] ) ) { return new WP_REST_Response( array( 'success' => false, 'message' => __( 'Invalid module.', 'modulustools-seo-suite' ) ), 400 ); } $modules = get_option( 'modulustools_seo_modules', array() ); $modules[ $module ] = $enabled; update_option( 'modulustools_seo_modules', $modules ); return new WP_REST_Response( array( 'success' => true, 'module' => $module, 'enabled' => $enabled, ), 200 ); } /** * Creates a new redirect rule in `wp_modulustools_redirects`. * * @param WP_REST_Request $request Incoming request. * @return WP_REST_Response */ public function create_redirect( WP_REST_Request $request ): WP_REST_Response { $source = sanitize_text_field( (string) $request->get_param( 'source_url' ) ); $target = esc_url_raw( (string) $request->get_param( 'target_url' ) ); $status_code = absint( $request->get_param( 'status_code' ) ); $is_regex = (bool) $request->get_param( 'is_regex' ); $id = Redirects_404::add_redirect( $source, $target, $status_code, $is_regex ); if ( ! $id ) { return new WP_REST_Response( array( 'success' => false, 'message' => __( 'Could not create redirect.', 'modulustools-seo-suite' ) ), 400 ); } return new WP_REST_Response( array( 'success' => true, 'id' => $id ), 201 ); } /** * Deletes a redirect rule by ID. * * @param WP_REST_Request $request Incoming request. * @return WP_REST_Response */ public function delete_redirect( WP_REST_Request $request ): WP_REST_Response { $id = absint( $request->get_param( 'id' ) ); $deleted = Redirects_404::delete_redirect( $id ); return new WP_REST_Response( array( 'success' => $deleted ), 200 ); } /** * Converts a 404 log entry into a 301 redirect and removes the 404 log. * * @param WP_REST_Request $request Incoming request. * @return WP_REST_Response */ public function convert_404_to_redirect( WP_REST_Request $request ): WP_REST_Response { $log_id = absint( $request->get_param( 'log_id' ) ); $source_url = sanitize_text_field( (string) $request->get_param( 'source_url' ) ); $target_url = esc_url_raw( (string) $request->get_param( 'target_url' ) ); $redirect_id = Redirects_404::add_redirect( $source_url, $target_url, 301, false ); if ( $redirect_id && $log_id > 0 ) { Redirects_404::delete_404_log( $log_id ); } return new WP_REST_Response( array( 'success' => (bool) $redirect_id, 'redirect_id' => $redirect_id, ), 200 ); } /** * Runs a batch migration step. * * @param WP_REST_Request $request Incoming request. * @return WP_REST_Response */ public function run_competitor_migration( WP_REST_Request $request ): WP_REST_Response { $source = sanitize_key( (string) $request->get_param( 'source' ) ); $offset = absint( $request->get_param( 'offset' ) ); $limit = max( 10, min( 100, absint( $request->get_param( 'limit' ) ? $request->get_param( 'limit' ) : 50 ) ) ); $result = Migrator::process_batch( $source, $offset, $limit ); return new WP_REST_Response( $result, 200 ); } }