__( 'List backlinks & toxic referring domains', 'ai-eshop-optimizer' ), 'description' => __( 'READ-ONLY, no AI credits and no DataForSEO call — reads the latest stored backlink snapshot. Returns the profile summary (domain rank, backlinks, referring domains, spam score) and the most toxic referring domains (domain, spam_score 0-100, backlinks, whether already on the disavow list). Params: limit, min_spam (default = the toxic threshold). If the snapshot is empty, tell the merchant to open SEO → Backlinks and click Refresh (that call is metered on the DataForSEO budget).', 'ai-eshop-optimizer' ), 'category' => \AIEO\Claude\Bootstrap::CATEGORY, 'input_schema' => array( 'default' => array(), 'type' => 'object', 'properties' => array( 'limit' => array( 'type' => 'integer', 'default' => 50, 'minimum' => 1, 'maximum' => 500 ), 'min_spam' => array( 'type' => 'integer', 'minimum' => 0, 'maximum' => 100, 'description' => 'Only domains with spam_score ≥ this. Omit for the toxic threshold.' ), ), ), 'output_schema' => array( 'type' => 'object' ), 'execute_callback' => array( __CLASS__, 'execute' ), 'permission_callback' => array( __CLASS__, 'permission_check' ), 'meta' => array( 'annotations' => array( 'readonly' => true, 'idempotent' => true ), 'show_in_rest' => true, ), ) ); } public static function permission_check( $input = null ) { unset( $input ); return current_user_can( 'manage_woocommerce' ); } public static function execute( $input ) { $input = is_array( $input ) ? $input : array(); if ( ! class_exists( 'AIEO_DFS_Backlinks' ) && defined( 'AIEO_DIR' ) ) { require_once AIEO_DIR . 'includes/seo/class-aieo-dfs-backlinks.php'; } if ( ! class_exists( 'AIEO_DFS_Backlinks' ) ) { return new \WP_Error( 'aieo_unavailable', __( 'The backlinks engine is unavailable on this site.', 'ai-eshop-optimizer' ) ); } $limit = min( 500, max( 1, (int) ( $input['limit'] ?? 50 ) ) ); $min_spam = isset( $input['min_spam'] ) ? max( 0, min( 100, (int) $input['min_spam'] ) ) : \AIEO_DFS_Backlinks::toxic_threshold(); $domains = \AIEO_DFS_Backlinks::referring_domains( $limit, array( 'min_spam' => $min_spam, 'order' => 'spam' ) ); return array( 'summary' => \AIEO_DFS_Backlinks::cached_summary(), 'toxic_threshold'=> \AIEO_DFS_Backlinks::toxic_threshold(), 'min_spam' => $min_spam, 'disavow_count' => \AIEO_DFS_Backlinks::disavow_count(), 'returned' => count( $domains ), 'domains' => $domains, 'note' => empty( $domains ) ? __( 'No backlink snapshot yet (or nothing at/above the spam threshold). Open SEO → Backlinks and click Refresh to pull from DataForSEO (metered).', 'ai-eshop-optimizer' ) : __( 'To disavow, use aieo/seo-disavow (it edits the LOCAL disavow list and returns the file). Google has no disavow API — the merchant uploads the file at the Disavow Links Tool.', 'ai-eshop-optimizer' ), ); } } class SeoDisavowAbility { const ABILITY_NAME = 'aieo/seo-disavow'; public static function register() { wp_register_ability( self::ABILITY_NAME, array( 'label' => __( 'Manage the disavow list & build the file', 'ai-eshop-optimizer' ), 'description' => __( 'Edits the LOCAL disavow list only — it does NOT submit anything to Google (Google exposes no disavow API; the merchant uploads the generated file at the Search Console Disavow Links Tool). Actions (op): "add" a domain, "remove" a domain, "add_all_toxic" (every referring domain at/above the spam threshold), or "file" (just return the current disavow file). Always returns the up-to-date disavow file text + the tool URL + the list count. No AI credits. Requires the plugin admin capability.', 'ai-eshop-optimizer' ), 'category' => \AIEO\Claude\Bootstrap::CATEGORY, 'input_schema' => array( 'default' => array(), 'type' => 'object', 'required' => array( 'op' ), 'properties' => array( 'op' => array( 'type' => 'string', 'enum' => array( 'add', 'remove', 'add_all_toxic', 'file' ) ), 'domain' => array( 'type' => 'string', 'description' => 'Required for add/remove.' ), 'reason' => array( 'type' => 'string', 'description' => 'Optional note stored as a # comment in the file.' ), ), ), 'output_schema' => array( 'type' => 'object' ), 'execute_callback' => array( __CLASS__, 'execute' ), 'permission_callback' => array( __CLASS__, 'permission_check' ), 'meta' => array( 'annotations' => array( 'readonly' => false, 'idempotent' => false, 'destructive' => false ), 'show_in_rest' => true, ), ) ); } public static function permission_check( $input = null ) { unset( $input ); return current_user_can( defined( 'AIEO_CAPABILITY' ) ? AIEO_CAPABILITY : 'manage_options' ); } public static function execute( $input ) { $input = is_array( $input ) ? $input : array(); if ( ! class_exists( 'AIEO_DFS_Backlinks' ) && defined( 'AIEO_DIR' ) ) { require_once AIEO_DIR . 'includes/seo/class-aieo-dfs-backlinks.php'; } if ( ! class_exists( 'AIEO_DFS_Backlinks' ) ) { return new \WP_Error( 'aieo_unavailable', __( 'The backlinks engine is unavailable on this site.', 'ai-eshop-optimizer' ) ); } $op = isset( $input['op'] ) ? sanitize_key( (string) $input['op'] ) : ''; $domain = isset( $input['domain'] ) ? (string) $input['domain'] : ''; $reason = isset( $input['reason'] ) ? (string) $input['reason'] : ''; $did = ''; switch ( $op ) { case 'add': if ( '' === trim( $domain ) ) { return new \WP_Error( 'aieo_bad_input', __( 'A domain is required for add.', 'ai-eshop-optimizer' ) ); } \AIEO_DFS_Backlinks::disavow_add( $domain, '' !== $reason ? $reason : __( 'Flagged via AI assistant', 'ai-eshop-optimizer' ) ); $did = 'added'; break; case 'remove': if ( '' === trim( $domain ) ) { return new \WP_Error( 'aieo_bad_input', __( 'A domain is required for remove.', 'ai-eshop-optimizer' ) ); } \AIEO_DFS_Backlinks::disavow_remove( $domain ); $did = 'removed'; break; case 'add_all_toxic': $n = \AIEO_DFS_Backlinks::disavow_all_toxic(); $did = sprintf( 'added_%d_toxic', $n ); break; case 'file': $did = 'file'; break; default: return new \WP_Error( 'aieo_bad_input', __( 'Unknown op.', 'ai-eshop-optimizer' ) ); } return array( 'op' => $op, 'result' => $did, 'disavow_count' => \AIEO_DFS_Backlinks::disavow_count(), 'disavow_file' => \AIEO_DFS_Backlinks::disavow_file(), 'upload_at' => \AIEO_DFS_Backlinks::disavow_tool_url(), 'note' => __( 'This edits only the local disavow list. Give the merchant the disavow_file text (or point them to SEO → Backlinks → Download disavow file) and have them upload it once at upload_at — Google accepts no automated submission.', 'ai-eshop-optimizer' ), ); } }