'GET', 'callback' => array( $this, 'get_state' ), 'permission_callback' => $perm, ) ); register_rest_route( self::NS, '/bot-stats', array( 'methods' => 'GET', 'callback' => array( $this, 'get_bot_stats' ), 'permission_callback' => $perm, 'args' => array( 'days' => array( 'type' => 'integer' ) ), ) ); register_rest_route( self::NS, '/scan', array( 'methods' => 'POST', 'callback' => array( $this, 'post_scan' ), 'permission_callback' => $perm, ) ); register_rest_route( self::NS, '/scan-batch', array( 'methods' => 'POST', 'callback' => array( $this, 'post_scan_batch' ), 'permission_callback' => $perm, ) ); register_rest_route( self::NS, '/faq-generate', array( 'methods' => 'POST', 'callback' => array( $this, 'post_faq_generate' ), 'permission_callback' => $perm, 'args' => array( 'pageId' => array( 'required' => true, 'type' => 'string' ) ), ) ); register_rest_route( self::NS, '/faq-apply', array( 'methods' => 'POST', 'callback' => array( $this, 'post_faq_apply' ), 'permission_callback' => $perm, 'args' => array( 'pageId' => array( 'required' => true, 'type' => 'string' ) ), ) ); register_rest_route( self::NS, '/fix-preview', array( 'methods' => 'POST', 'callback' => array( $this, 'post_fix_preview' ), 'permission_callback' => $perm, 'args' => array( 'id' => array( 'required' => true, 'type' => 'string' ) ), ) ); register_rest_route( self::NS, '/gsc-save', array( 'methods' => 'POST', 'callback' => array( $this, 'post_gsc_save' ), 'permission_callback' => $perm, ) ); register_rest_route( self::NS, '/gsc-disconnect', array( 'methods' => 'POST', 'callback' => array( $this, 'post_gsc_disconnect' ), 'permission_callback' => $perm, ) ); register_rest_route( self::NS, '/gsc-test', array( 'methods' => 'GET', 'callback' => array( $this, 'get_gsc_test' ), 'permission_callback' => $perm, ) ); register_rest_route( self::NS, '/gsc-data', array( 'methods' => 'GET', 'callback' => array( $this, 'get_gsc_data' ), 'permission_callback' => $perm, ) ); register_rest_route( self::NS, '/fix-bulk', array( 'methods' => 'POST', 'callback' => array( $this, 'post_fix_bulk' ), 'permission_callback' => $perm, ) ); register_rest_route( self::NS, '/unfix', array( 'methods' => 'POST', 'callback' => array( $this, 'post_unfix' ), 'permission_callback' => $perm, 'args' => array( 'id' => array( 'required' => true, 'type' => 'string' ) ), ) ); register_rest_route( self::NS, '/fix', array( 'methods' => 'POST', 'callback' => array( $this, 'post_fix' ), 'permission_callback' => $perm, 'args' => array( 'id' => array( 'required' => true, 'type' => 'string' ) ), ) ); register_rest_route( self::NS, '/fix-all', array( 'methods' => 'POST', 'callback' => array( $this, 'post_fix_all' ), 'permission_callback' => $perm, 'args' => array( 'pageId' => array( 'required' => true, 'type' => 'string' ) ), ) ); register_rest_route( self::NS, '/bot', array( 'methods' => 'POST', 'callback' => array( $this, 'post_bot' ), 'permission_callback' => $perm, ) ); register_rest_route( self::NS, '/settings', array( 'methods' => 'POST', 'callback' => array( $this, 'post_settings' ), 'permission_callback' => $perm, ) ); register_rest_route( self::NS, '/visibility-check', array( 'methods' => 'POST', 'callback' => array( $this, 'post_visibility_check' ), 'permission_callback' => $perm, ) ); register_rest_route( self::NS, '/prompts', array( 'methods' => 'POST', 'callback' => array( $this, 'post_prompts' ), 'permission_callback' => $perm, ) ); register_rest_route( self::NS, '/competitors', array( 'methods' => 'POST', 'callback' => array( $this, 'post_competitors' ), 'permission_callback' => $perm, ) ); register_rest_route( self::NS, '/send-report', array( 'methods' => 'POST', 'callback' => array( $this, 'post_send_report' ), 'permission_callback' => $perm, ) ); register_rest_route( self::NS, '/report-preview', array( 'methods' => 'GET', 'callback' => array( $this, 'get_report_preview' ), 'permission_callback' => $perm, ) ); register_rest_route( self::NS, '/logs-clear', array( 'methods' => 'POST', 'callback' => array( $this, 'post_logs_clear' ), 'permission_callback' => $perm, ) ); register_rest_route( self::NS, '/reset-data', array( 'methods' => 'POST', 'callback' => array( $this, 'post_reset_data' ), 'permission_callback' => $perm, ) ); // Dev-only Docker log viewer — registered only when explicitly enabled // (define AICITARE_ENABLE_DOCKER in wp-config.php). Off in distributed builds. if ( defined( 'AICITARE_ENABLE_DOCKER' ) && AICITARE_ENABLE_DOCKER && class_exists( 'AICitare_Docker' ) ) { register_rest_route( self::NS, '/docker', array( 'methods' => 'GET', 'callback' => array( $this, 'get_docker' ), 'permission_callback' => $perm, ) ); register_rest_route( self::NS, '/docker-cutoff', array( 'methods' => 'POST', 'callback' => array( $this, 'post_docker_cutoff' ), 'permission_callback' => $perm, ) ); } register_rest_route( self::NS, '/import', array( 'methods' => 'POST', 'callback' => array( $this, 'post_import' ), 'permission_callback' => $perm, 'args' => array( 'url' => array( 'required' => true, 'type' => 'string' ) ), ) ); register_rest_route( self::NS, '/amazon-suggest', array( 'methods' => 'POST', 'callback' => array( $this, 'post_amazon_suggest' ), 'permission_callback' => $perm, 'args' => array( 'query' => array( 'required' => true, 'type' => 'string' ) ), ) ); register_rest_route( self::NS, '/test-amazon', array( 'methods' => 'POST', 'callback' => array( $this, 'post_test_amazon' ), 'permission_callback' => $perm, ) ); register_rest_route( self::NS, '/validate-schema', array( 'methods' => 'GET', 'callback' => array( $this, 'get_validate_schema' ), 'permission_callback' => $perm, 'args' => array( 'pageId' => array( 'type' => 'string' ), 'mode' => array( 'type' => 'string' ), ), ) ); register_rest_route( self::NS, '/schema-fix', array( 'methods' => 'POST', 'callback' => array( $this, 'post_schema_fix' ), 'permission_callback' => $perm, ) ); register_rest_route( self::NS, '/page-content', array( 'methods' => 'POST', 'callback' => array( $this, 'post_page_content' ), 'permission_callback' => $perm, 'args' => array( 'pageId' => array( 'required' => true, 'type' => 'string' ) ), ) ); register_rest_route( self::NS, '/rephrase-draft', array( 'methods' => 'POST', 'callback' => array( $this, 'post_rephrase_draft' ), 'permission_callback' => $perm, 'args' => array( 'pageId' => array( 'required' => true, 'type' => 'string' ) ), ) ); register_rest_route( self::NS, '/rephrase', array( 'methods' => 'POST', 'callback' => array( $this, 'post_rephrase' ), 'permission_callback' => $perm, 'args' => array( 'text' => array( 'required' => true, 'type' => 'string' ) ), ) ); register_rest_route( self::NS, '/import-preview', array( 'methods' => 'POST', 'callback' => array( $this, 'post_import_preview' ), 'permission_callback' => $perm, 'args' => array( 'url' => array( 'required' => true, 'type' => 'string' ) ), ) ); register_rest_route( self::NS, '/import-category', array( 'methods' => 'POST', 'callback' => array( $this, 'post_import_category' ), 'permission_callback' => $perm, ) ); register_rest_route( self::NS, '/test-key', array( 'methods' => 'POST', 'callback' => array( $this, 'post_test_key' ), 'permission_callback' => $perm, ) ); register_rest_route( self::NS, '/list-models', array( 'methods' => 'POST', 'callback' => array( $this, 'post_list_models' ), 'permission_callback' => $perm, ) ); } /** Assemble the full state payload the SPA needs. */ public function get_state( $request ) { $pages = AICitare_Data::get_pages(); $settings = AICitare_Data::get_settings(); // Decrypt to mask, then send only the masked form to the browser. $masked = array(); foreach ( $settings['keys'] as $k => $v ) { $plain = AICitare_Crypto::decrypt( $v ); $masked[ $k ] = $plain ? ( 'sk-•••••••••••••' . substr( $plain, -4 ) ) : ''; } $settings_out = $settings; $settings_out['keys'] = $masked; // Mask the Amazon secret; expose only whether it is set. if ( isset( $settings['amazon'] ) && is_array( $settings['amazon'] ) ) { $asecret = AICitare_Crypto::decrypt( isset( $settings['amazon']['secret'] ) ? $settings['amazon']['secret'] : '' ); $settings_out['amazon'] = array( 'access' => isset( $settings['amazon']['access'] ) ? $settings['amazon']['access'] : '', 'partner' => isset( $settings['amazon']['partner'] ) ? $settings['amazon']['partner'] : '', 'secret' => $asecret ? ( '••••••••' . substr( $asecret, -4 ) ) : '', ); } $data = array( 'pages' => $pages, 'fixed' => (object) AICitare_Data::get_fixed(), 'bots' => (object) AICitare_Data::get_bots(), 'botMeta' => AICitare_Data::bot_meta(), 'settings' => $settings_out, 'weights' => (object) AICitare_Data::weights(), 'cats' => AICitare_Data::categories(), 'history' => array_values( AICitare_Data::get_history() ), 'pageHist' => (object) get_option( 'aicitare_page_hist', array() ), 'citations' => AICitare_Visibility::get_citations(), 'citationsLive' => (bool) get_option( AICitare_Visibility::OPT_RESULTS ), 'visChecked' => AICitare_Visibility::last_checked(), 'engines' => $this->engines_payload(), 'engineStatus' => (object) AICitare_Visibility::engine_status(), 'answers' => AICitare_Visibility::answers(), 'prompts' => array_values( AICitare_Visibility::prompts() ), 'visHistory' => array_values( AICitare_Visibility::vis_history() ), 'competitors' => AICitare_Visibility::get_share(), 'competitorsList' => array_values( AICitare_Visibility::competitors_list() ), 'competitorsLive' => (bool) get_option( AICitare_Visibility::OPT_SHARE ), 'competitorGaps' => AICitare_Visibility::competitor_gaps(), 'gsc' => array( 'configured' => AICitare_GSC::configured(), 'connected' => AICitare_GSC::connected(), 'site' => AICitare_GSC::creds()['site'], 'clientId' => AICitare_GSC::creds()['client_id'], 'authUrl' => AICitare_GSC::auth_url(), 'redirectUri' => AICitare_GSC::redirect_uri(), ), 'discovered' => AICitare_Visibility::discovered(), 'botLog' => AICitare_LLMS::recent_hits( 12 ), 'botStats' => AICitare_LLMS::bot_stats( 30 ), 'alerts' => AICitare_Alerts::current(), 'logs' => AICitare_Log::recent( 200 ), 'schedule' => array( 'scanFreq' => $settings['frequency'], 'visFreq' => $settings['visibility_freq'], 'lastScanTs' => (int) ( isset( $settings['last_scan_ts'] ) ? $settings['last_scan_ts'] : 0 ), 'nextScan' => (int) wp_next_scheduled( 'aicitare_daily_scan' ), 'nextVis' => (int) wp_next_scheduled( 'aicitare_weekly_visibility' ), 'threshold' => (int) ( isset( $settings['score_drop_threshold'] ) ? $settings['score_drop_threshold'] : 5 ), 'now' => time(), ), 'site' => array( 'name' => get_bloginfo( 'name' ), 'host' => wp_parse_url( home_url(), PHP_URL_HOST ), ), ); return rest_ensure_response( $data ); } /** Bot Activity analytics for a rolling window (days param, default 30). */ public function get_bot_stats( $request ) { $days = (int) $request->get_param( 'days' ); if ( $days <= 0 ) { $days = 30; } return rest_ensure_response( AICitare_LLMS::bot_stats( $days ) ); } /** Engines as [{ id, label, active }] in column order. */ private function engines_payload() { $active = AICitare_Visibility::active_engines(); $out = array(); foreach ( AICitare_Visibility::ENGINES as $id => $label ) { $out[] = array( 'id' => $id, 'label' => $label, 'active' => ! empty( $active[ $id ] ) ); } return $out; } public function post_reset_data() { AICitare_Activator::reset(); return $this->get_state( null ); } public function post_scan_batch( $request ) { $body = $request->get_json_params(); $offset = isset( $body['offset'] ) ? (int) $body['offset'] : (int) $request->get_param( 'offset' ); try { $prog = AICitare_Scanner::batch( $offset, 12 ); } catch ( \Throwable $e ) { AICitare_Log::add( 'scan', 'Scan error: ' . $e->getMessage() . ' @ ' . basename( $e->getFile() ) . ':' . $e->getLine() ); return new WP_Error( 'aicitare_scan_error', $e->getMessage() . ' (' . basename( $e->getFile() ) . ':' . $e->getLine() . ')', array( 'status' => 500 ) ); } if ( ! empty( $prog['done'] ) ) { $res = $this->get_state( null ); $data = $res->get_data(); $data['scanProgress'] = $prog; $res->set_data( $data ); return $res; } return rest_ensure_response( array( 'scanProgress' => $prog ) ); } public function post_scan() { try { AICitare_Scanner::run(); } catch ( \Throwable $e ) { AICitare_Log::add( 'scan', 'Scan error: ' . $e->getMessage() . ' @ ' . $e->getFile() . ':' . $e->getLine() ); return new WP_Error( 'aicitare_scan_error', $e->getMessage() . ' (' . basename( $e->getFile() ) . ':' . $e->getLine() . ')', array( 'status' => 500 ) ); } return $this->get_state( null ); } public function post_visibility_check() { ( new AICitare_Visibility() )->run(); return $this->get_state( null ); } public function post_prompts( $request ) { $body = $request->get_json_params(); $prompts = AICitare_Visibility::save_prompts( isset( $body['prompts'] ) ? $body['prompts'] : array() ); AICitare_Log::add( 'prompts', sprintf( 'Tracked prompts updated (%d)', count( $prompts ) ) ); return $this->get_state( null ); } public function post_send_report( $request ) { $body = $request->get_json_params(); $to = isset( $body['email'] ) ? sanitize_email( $body['email'] ) : ''; $reports = new AICitare_Reports(); $ok = $reports->send( $to ); AICitare_Log::add( 'report', $ok ? 'Manual report sent' : 'Manual report failed' ); $res = $this->get_state( null ); $data = $res->get_data(); $data['reportSent'] = (bool) $ok; if ( ! $ok ) { $data['reportError'] = $reports->last_error; } $res->set_data( $data ); return $res; } public function get_report_preview() { return rest_ensure_response( array( 'html' => AICitare_Reports::build_html() ) ); } public function post_logs_clear() { AICitare_Log::clear(); return $this->get_state( null ); } public function post_list_models( $request ) { $body = $request->get_json_params(); $eng = isset( $body['engine'] ) ? sanitize_text_field( $body['engine'] ) : ''; if ( ! array_key_exists( $eng, AICitare_Visibility::ENGINES ) ) { return rest_ensure_response( array( 'ok' => false, 'msg' => 'Unknown engine' ) ); } return rest_ensure_response( AICitare_Visibility::list_models( $eng ) ); } public function post_test_key( $request ) { $body = $request->get_json_params(); $eng = isset( $body['engine'] ) ? sanitize_text_field( $body['engine'] ) : ''; if ( ! array_key_exists( $eng, AICitare_Visibility::ENGINES ) ) { return rest_ensure_response( array( 'ok' => false, 'msg' => 'Unknown engine' ) ); } return rest_ensure_response( AICitare_Visibility::test( $eng ) ); } public function post_import( $request ) { $body = $request->get_json_params(); $url = isset( $body['url'] ) ? $body['url'] : $request->get_param( 'url' ); $o = ( isset( $body['opts'] ) && is_array( $body['opts'] ) ) ? $body['opts'] : array(); $opts = array( 'images' => isset( $o['images'] ) ? (bool) $o['images'] : true, 'alt' => isset( $o['alt'] ) ? (bool) $o['alt'] : true, 'featured' => isset( $o['featured'] ) ? (bool) $o['featured'] : true, 'links' => isset( $o['links'] ) ? sanitize_text_field( $o['links'] ) : 'nofollow', ); $r = AICitare_Import::import( $url, $opts ); if ( is_wp_error( $r ) ) { return new WP_REST_Response( array( 'ok' => false, 'error' => $r->get_error_message() ), 200 ); } return rest_ensure_response( $r ); } public function post_test_amazon( $request ) { return rest_ensure_response( AICitare_Amazon::test() ); } public function post_amazon_suggest( $request ) { $body = $request->get_json_params(); $query = isset( $body['query'] ) ? $body['query'] : $request->get_param( 'query' ); $host = isset( $body['host'] ) ? sanitize_text_field( $body['host'] ) : 'amazon.com'; return rest_ensure_response( AICitare_Amazon::search( $query, $host, 5 ) ); } public function post_rephrase_draft( $request ) { $body = $request->get_json_params(); $pid = isset( $body['pageId'] ) ? $body['pageId'] : $request->get_param( 'pageId' ); $changes = ( isset( $body['changes'] ) && is_array( $body['changes'] ) ) ? $body['changes'] : array(); $r = AICitare_Visibility::create_rephrase_draft( $pid, $changes ); if ( is_wp_error( $r ) ) { return new WP_REST_Response( array( 'ok' => false, 'error' => $r->get_error_message() ), 200 ); } return rest_ensure_response( $r ); } /** Validate structured data for a post — Rich-Results style, fully on-site. */ public function get_validate_schema( $request ) { $pid = sanitize_text_field( (string) $request->get_param( 'pageId' ) ); $id = (int) preg_replace( '/[^0-9]/', '', $pid ); $post = $id ? get_post( $id ) : null; if ( ! $post instanceof WP_Post ) { return new WP_REST_Response( array( 'ok' => false, 'error' => 'Post not found. Pick a real post/page.' ), 200 ); } $mode = sanitize_text_field( (string) $request->get_param( 'mode' ) ); $note = ''; if ( 'emitted' === $mode ) { $results = AICitare_Validator::validate_post( $post ); $source = 'emitted'; } else { $fetched = AICitare_Validator::validate_url( get_permalink( $post ) ); if ( isset( $fetched['error'] ) ) { // Fetch failed (cache/firewall/auth) — fall back to AICitare's own schema. $results = AICitare_Validator::validate_post( $post ); $source = 'emitted'; /* translators: %s: reason */ $note = sprintf( __( 'Could not fetch the live page (%s) — validated AICitare\'s own schema instead.', 'aicitare' ), $fetched['error'] ); } else { $results = AICitare_Validator::validate_nodes( $fetched['nodes'] ); $source = 'rendered'; } } $crit = 0; $warn = 0; foreach ( $results as $node ) { foreach ( $node['issues'] as $i ) { if ( 'critical' === $i['level'] ) { $crit++; } else { $warn++; } } } return rest_ensure_response( array( 'ok' => true, 'title' => get_the_title( $post ), 'url' => get_permalink( $post ), 'source' => $source, 'note' => $note, 'nodes' => $results, 'critical' => $crit, 'warnings' => $warn, 'clean' => ( 0 === $crit ), 'empty' => empty( $results ), ) ); } /** Save a real rating and/or offer so AICitare's Product schema becomes valid. */ public function post_schema_fix( $request ) { $body = $request->get_json_params(); $pid = sanitize_text_field( isset( $body['pageId'] ) ? $body['pageId'] : '' ); $id = (int) preg_replace( '/[^0-9]/', '', $pid ); $post = $id ? get_post( $id ) : null; if ( ! $post instanceof WP_Post ) { return new WP_REST_Response( array( 'ok' => false, 'error' => 'Post not found.' ), 200 ); } $rating = isset( $body['rating'] ) ? (float) $body['rating'] : 0; $count = isset( $body['ratingCount'] ) ? (int) $body['ratingCount'] : 0; $price = isset( $body['price'] ) ? sanitize_text_field( (string) $body['price'] ) : ''; $currency = isset( $body['currency'] ) ? strtoupper( sanitize_text_field( (string) $body['currency'] ) ) : 'USD'; if ( $rating > 0 ) { $rating = min( 5, max( 1, $rating ) ); update_post_meta( $id, '_aicitare_rating', $rating ); update_post_meta( $id, '_aicitare_rating_count', $count > 0 ? $count : 1 ); } if ( '' !== $price && is_numeric( $price ) ) { update_post_meta( $id, '_aicitare_price', $price ); update_post_meta( $id, '_aicitare_currency', $currency ? $currency : 'USD' ); } AICitare_Log::add( 'fix', 'Schema data added (rating/offer) for post ' . $id ); // Re-validate AICitare's schema so the UI can show the result immediately. $results = AICitare_Validator::validate_post( $post ); $crit = 0; foreach ( $results as $node ) { foreach ( $node['issues'] as $i ) { if ( 'critical' === $i['level'] ) { $crit++; } } } return rest_ensure_response( array( 'ok' => true, 'nodes' => $results, 'critical' => $crit, 'clean' => ( 0 === $crit ) ) ); } public function post_page_content( $request ) { $body = $request->get_json_params(); $pid = isset( $body['pageId'] ) ? $body['pageId'] : $request->get_param( 'pageId' ); $r = AICitare_Visibility::page_paragraphs( $pid ); if ( is_wp_error( $r ) ) { return new WP_REST_Response( array( 'ok' => false, 'error' => $r->get_error_message() ), 200 ); } return rest_ensure_response( $r ); } public function post_rephrase( $request ) { $body = $request->get_json_params(); $text = isset( $body['text'] ) ? $body['text'] : $request->get_param( 'text' ); $engine = isset( $body['engine'] ) ? sanitize_text_field( $body['engine'] ) : ''; $n = isset( $body['n'] ) ? (int) $body['n'] : 3; $r = AICitare_Visibility::rephrase( $text, $engine, $n ); if ( is_wp_error( $r ) ) { return new WP_REST_Response( array( 'ok' => false, 'error' => $r->get_error_message() ), 200 ); } return rest_ensure_response( $r ); } public function post_import_preview( $request ) { $body = $request->get_json_params(); $url = isset( $body['url'] ) ? $body['url'] : $request->get_param( 'url' ); $limit = isset( $body['limit'] ) ? (int) $body['limit'] : 20; $r = AICitare_Import::preview_category( $url, $limit ); if ( is_wp_error( $r ) ) { return new WP_REST_Response( array( 'ok' => false, 'error' => $r->get_error_message() ), 200 ); } return rest_ensure_response( $r ); } public function post_import_category( $request ) { $body = $request->get_json_params(); $url = isset( $body['url'] ) ? $body['url'] : $request->get_param( 'url' ); $limit = isset( $body['limit'] ) ? (int) $body['limit'] : 50; $urls = ( isset( $body['urls'] ) && is_array( $body['urls'] ) ) ? $body['urls'] : null; $o = ( isset( $body['opts'] ) && is_array( $body['opts'] ) ) ? $body['opts'] : array(); $opts = array( 'images' => isset( $o['images'] ) ? (bool) $o['images'] : true, 'alt' => isset( $o['alt'] ) ? (bool) $o['alt'] : true, 'featured' => isset( $o['featured'] ) ? (bool) $o['featured'] : true, 'links' => isset( $o['links'] ) ? sanitize_text_field( $o['links'] ) : 'nofollow', ); $r = AICitare_Import::import_category( $url, $opts, $limit, $urls ); if ( is_wp_error( $r ) ) { return new WP_REST_Response( array( 'ok' => false, 'error' => $r->get_error_message() ), 200 ); } return rest_ensure_response( $r ); } const OPT_DOCKER_CUTOFF = 'aicitare_docker_cutoff'; private function docker_payload( $tail = 200 ) { if ( ! AICitare_Docker::available() ) { return array( 'available' => false, 'containers' => array(), 'lines' => array(), 'cutoff' => 0 ); } $cutoff = (int) get_option( self::OPT_DOCKER_CUTOFF, 0 ); $snap = AICitare_Docker::snapshot( $tail, $cutoff ); $snap['available'] = true; $snap['cutoff'] = $cutoff; return $snap; } public function get_docker( $request ) { $tail = (int) $request->get_param( 'tail' ); $tail = $tail > 0 ? min( 1000, $tail ) : 200; return rest_ensure_response( $this->docker_payload( $tail ) ); } public function post_docker_cutoff( $request ) { $body = $request->get_json_params(); if ( ! empty( $body['reset'] ) ) { delete_option( self::OPT_DOCKER_CUTOFF ); } else { update_option( self::OPT_DOCKER_CUTOFF, time() ); } return rest_ensure_response( $this->docker_payload( 200 ) ); } public function post_competitors( $request ) { $body = $request->get_json_params(); $comps = AICitare_Visibility::save_competitors( isset( $body['competitors'] ) ? $body['competitors'] : array() ); // Mark the competitor-tracking setup step as done. $settings = AICitare_Data::get_settings(); $settings['competitor_setup'] = true; update_option( AICITARE_OPT_SETTINGS, $settings ); AICitare_Log::add( 'competitors', sprintf( 'Competitors updated (%d)', count( $comps ) ) ); return $this->get_state( null ); } public function post_fix( $request ) { $id = sanitize_text_field( $request->get_param( 'id' ) ); $fixed = AICitare_Data::get_fixed(); $fixed[ $id ] = true; update_option( AICITARE_OPT_FIXED, $fixed ); AICitare_Log::add( 'fix', 'Fixed issue ' . $id ); return $this->get_state( null ); } public function post_faq_generate( $request ) { $body = $request->get_json_params(); $pid = ( preg_match( '#^wp(\d+)$#', (string) ( isset( $body['pageId'] ) ? $body['pageId'] : $request->get_param( 'pageId' ) ), $m ) ) ? (int) $m[1] : 0; if ( ! $pid ) { return new WP_REST_Response( array( 'ok' => false, 'error' => 'FAQ generation needs a real WordPress post.' ), 200 ); } $post = get_post( $pid ); if ( ! $post ) { return new WP_REST_Response( array( 'ok' => false, 'error' => 'Post not found.' ), 200 ); } $content = do_shortcode( $post->post_content ); $r = AICitare_Visibility::generate_faq( $content, '', 5 ); if ( is_wp_error( $r ) ) { return new WP_REST_Response( array( 'ok' => false, 'error' => $r->get_error_message() ), 200 ); } return rest_ensure_response( $r ); } public function post_faq_apply( $request ) { $body = $request->get_json_params(); $pid = ( preg_match( '#^wp(\d+)$#', (string) ( isset( $body['pageId'] ) ? $body['pageId'] : '' ), $m ) ) ? (int) $m[1] : 0; $issue = isset( $body['issueId'] ) ? sanitize_text_field( $body['issueId'] ) : ''; $raw = ( isset( $body['faqs'] ) && is_array( $body['faqs'] ) ) ? $body['faqs'] : array(); if ( ! $pid ) { return new WP_REST_Response( array( 'ok' => false, 'error' => 'Invalid post.' ), 200 ); } $clean = array(); foreach ( $raw as $qa ) { $q = isset( $qa['q'] ) ? sanitize_text_field( $qa['q'] ) : ''; $a = isset( $qa['a'] ) ? sanitize_textarea_field( $qa['a'] ) : ''; if ( '' !== $q && '' !== $a ) { $clean[] = array( 'q' => $q, 'a' => $a ); } } if ( empty( $clean ) ) { return new WP_REST_Response( array( 'ok' => false, 'error' => 'No valid FAQ pairs to save.' ), 200 ); } update_post_meta( $pid, '_aicitare_faq', wp_json_encode( $clean ) ); if ( '' !== $issue ) { $fixed = AICitare_Data::get_fixed(); $fixed[ $issue ] = true; update_option( AICITARE_OPT_FIXED, $fixed ); } AICitare_Log::add( 'faq', 'Saved ' . count( $clean ) . ' FAQ pairs to post #' . $pid . '.' ); return $this->get_state( null ); } public function post_fix_preview( $request ) { $body = $request->get_json_params(); $id = sanitize_text_field( isset( $body['id'] ) ? $body['id'] : $request->get_param( 'id' ) ); // Locate the issue (and its page) in the stored dataset. $page = null; $issue = null; foreach ( AICitare_Data::get_pages() as $pg ) { foreach ( $pg['issues'] as $is ) { if ( $is['id'] === $id ) { $page = $pg; $issue = $is; break 2; } } } if ( ! $issue ) { return new WP_REST_Response( array( 'ok' => false, 'error' => 'Issue not found.' ), 200 ); } $blocks = array(); $pid = ( preg_match( '#^wp(\d+)$#', $page['id'], $m ) ) ? (int) $m[1] : 0; $post = $pid ? get_post( $pid ) : null; if ( $post ) { try { $json = ( new AICitare_Schema() )->preview_json( $post, $issue['cat'], $issue['title'] ); if ( '' !== $json ) { $blocks[] = array( 'kind' => 'schema', 'label' => 'JSON-LD (added to )', 'content' => $json ); } $html = ( new AICitare_Content() )->preview_html( $post, $issue['cat'], $issue['title'] ); if ( '' !== trim( (string) $html ) ) { $blocks[] = array( 'kind' => 'html', 'label' => 'HTML (added to the post body)', 'content' => $html ); } } catch ( \Throwable $e ) { AICitare_Log::add( 'fix', 'Preview error: ' . $e->getMessage() . ' @ ' . basename( $e->getFile() ) . ':' . $e->getLine() ); return new WP_REST_Response( array( 'ok' => false, 'error' => $e->getMessage() . ' (' . basename( $e->getFile() ) . ':' . $e->getLine() . ')' ), 200 ); } } return rest_ensure_response( array( 'ok' => true, 'title' => $issue['title'], 'after' => isset( $issue['after'] ) ? $issue['after'] : '', 'demo' => ( 0 === $pid ), 'blocks' => $blocks, ) ); } public function post_gsc_save( $request ) { $b = $request->get_json_params(); AICitare_GSC::save_creds( isset( $b['client_id'] ) ? $b['client_id'] : '', isset( $b['client_secret'] ) ? $b['client_secret'] : '', isset( $b['site'] ) ? $b['site'] : home_url( '/' ) ); return $this->get_state( null ); } public function post_gsc_disconnect( $request ) { AICitare_GSC::disconnect(); AICitare_Log::add( 'gsc', 'Disconnected from Search Console.' ); return $this->get_state( null ); } public function get_gsc_data( $request ) { return rest_ensure_response( AICitare_GSC::query() ); } public function get_gsc_test( $request ) { return rest_ensure_response( AICitare_GSC::test() ); } public function post_fix_bulk( $request ) { $fixed = AICitare_Data::get_fixed(); $n = 0; foreach ( AICitare_Data::get_pages() as $pg ) { foreach ( $pg['issues'] as $is ) { if ( empty( $is['manual'] ) && empty( $fixed[ $is['id'] ] ) ) { $fixed[ $is['id'] ] = true; $n++; } } } update_option( AICITARE_OPT_FIXED, $fixed ); AICitare_Log::add( 'fix', 'Bulk-resolved ' . $n . ' auto-fixable issues.' ); return $this->get_state( null ); } public function post_unfix( $request ) { $id = sanitize_text_field( $request->get_param( 'id' ) ); $fixed = AICitare_Data::get_fixed(); if ( isset( $fixed[ $id ] ) ) { unset( $fixed[ $id ] ); update_option( AICITARE_OPT_FIXED, $fixed ); AICitare_Log::add( 'fix', 'Reverted issue ' . $id ); } return $this->get_state( null ); } public function post_fix_all( $request ) { $page_id = sanitize_text_field( $request->get_param( 'pageId' ) ); $fixed = AICitare_Data::get_fixed(); foreach ( AICitare_Data::get_pages() as $p ) { if ( $p['id'] === $page_id ) { foreach ( $p['issues'] as $is ) { $fixed[ $is['id'] ] = true; } } } update_option( AICITARE_OPT_FIXED, $fixed ); AICitare_Log::add( 'fix_all', 'Fixed all issues on ' . $page_id ); return $this->get_state( null ); } public function post_bot( $request ) { $id = sanitize_text_field( $request->get_param( 'id' ) ); $allowed = (bool) $request->get_param( 'allowed' ); $bots = AICitare_Data::get_bots(); $bots[ $id ] = $allowed; update_option( AICITARE_OPT_BOTS, $bots ); AICitare_Log::add( 'crawler', sprintf( 'Crawler %s %s', $id, $allowed ? 'allowed' : 'blocked' ) ); return $this->get_state( null ); } public function post_settings( $request ) { $settings = AICitare_Data::get_settings(); $body = $request->get_json_params(); if ( isset( $body['schema'] ) && in_array( $body['schema'], array( 'complementary', 'full' ), true ) ) { $settings['schema'] = $body['schema']; } if ( isset( $body['demo_mode'] ) ) { $settings['demo_mode'] = (bool) $body['demo_mode']; } if ( isset( $body['include_drafts'] ) ) { $settings['include_drafts'] = (bool) $body['include_drafts']; } if ( isset( $body['check_links'] ) ) { $settings['check_links'] = (bool) $body['check_links']; } if ( isset( $body['amazon_enabled'] ) ) { $settings['amazon_enabled'] = (bool) $body['amazon_enabled']; } if ( isset( $body['amazon'] ) && is_array( $body['amazon'] ) ) { if ( ! isset( $settings['amazon'] ) || ! is_array( $settings['amazon'] ) ) { $settings['amazon'] = array(); } if ( isset( $body['amazon']['access'] ) ) { $settings['amazon']['access'] = sanitize_text_field( $body['amazon']['access'] ); } if ( isset( $body['amazon']['partner'] ) ) { $settings['amazon']['partner'] = sanitize_text_field( $body['amazon']['partner'] ); } if ( isset( $body['amazon']['secret'] ) ) { $v = $body['amazon']['secret']; if ( '__clear__' === $v ) { $settings['amazon']['secret'] = ''; } elseif ( $v && false === strpos( $v, '•' ) ) { $settings['amazon']['secret'] = AICitare_Crypto::encrypt( sanitize_text_field( $v ) ); } } } if ( isset( $body['competitor_setup'] ) ) { $settings['competitor_setup'] = (bool) $body['competitor_setup']; } if ( isset( $body['report_email'] ) ) { $settings['report_email'] = sanitize_email( $body['report_email'] ); } if ( isset( $body['reports_enabled'] ) ) { $settings['reports_enabled'] = (bool) $body['reports_enabled']; } if ( isset( $body['alerts_enabled'] ) ) { $settings['alerts_enabled'] = (bool) $body['alerts_enabled']; } if ( isset( $body['score_drop_threshold'] ) ) { $settings['score_drop_threshold'] = max( 1, (int) $body['score_drop_threshold'] ); } if ( isset( $body['frequency'] ) && in_array( $body['frequency'], array( 'hourly', 'twicedaily', 'daily', 'weekly' ), true ) ) { $settings['frequency'] = $body['frequency']; $this->reschedule( 'aicitare_daily_scan', $body['frequency'] ); } if ( isset( $body['visibility_freq'] ) && in_array( $body['visibility_freq'], array( 'daily', 'weekly' ), true ) ) { $settings['visibility_freq'] = $body['visibility_freq']; $this->reschedule( 'aicitare_weekly_visibility', $body['visibility_freq'] ); } if ( isset( $body['ollama_enabled'] ) ) { $settings['ollama_enabled'] = (bool) $body['ollama_enabled']; } if ( isset( $body['ollama_url'] ) ) { $settings['ollama_url'] = esc_url_raw( trim( $body['ollama_url'] ) ); } if ( isset( $body['ollama_model'] ) ) { $settings['ollama_model'] = sanitize_text_field( $body['ollama_model'] ); } if ( isset( $body['models'] ) && is_array( $body['models'] ) ) { if ( ! isset( $settings['models'] ) || ! is_array( $settings['models'] ) ) { $settings['models'] = array(); } foreach ( $body['models'] as $eng => $model ) { $settings['models'][ sanitize_key( $eng ) ] = sanitize_text_field( $model ); } } // API keys: only overwrite when a non-masked value is provided. if ( isset( $body['keys'] ) && is_array( $body['keys'] ) ) { foreach ( $body['keys'] as $k => $v ) { if ( ! isset( $settings['keys'][ $k ] ) ) { continue; } if ( '__clear__' === $v ) { $settings['keys'][ $k ] = ''; // explicit removal. AICitare_Log::add( 'apikey', $k . ' API key removed' ); } elseif ( $v && false === strpos( $v, '•' ) ) { // Encrypt at rest; never store the raw key. $settings['keys'][ $k ] = AICitare_Crypto::encrypt( sanitize_text_field( $v ) ); AICitare_Log::add( 'apikey', $k . ' API key saved' ); } } } update_option( AICITARE_OPT_SETTINGS, $settings ); $changed = array_values( array_diff( array_keys( (array) $body ), array( 'keys' ) ) ); if ( $changed ) { AICitare_Log::add( 'settings', 'Updated: ' . implode( ', ', $changed ) ); } return $this->get_state( null ); } /** Unschedule then re-schedule a cron hook with a new recurrence. */ private function reschedule( $hook, $recurrence ) { $ts = wp_next_scheduled( $hook ); if ( $ts ) { wp_unschedule_event( $ts, $hook ); } $offsets = array( 'hourly' => HOUR_IN_SECONDS, 'twicedaily' => 12 * HOUR_IN_SECONDS, 'daily' => DAY_IN_SECONDS, 'weekly' => WEEK_IN_SECONDS, ); $first = time() + ( isset( $offsets[ $recurrence ] ) ? $offsets[ $recurrence ] : DAY_IN_SECONDS ); wp_schedule_event( $first, $recurrence, $hook ); } }