prefix . self::SNAP_TABLE; } public static function disavow_table() { global $wpdb; return $wpdb->prefix . self::DISAVOW_TABLE; } public static function ensure_tables() { global $wpdb; $charset = aieo_get_charset_collate(); $snap = self::snap_table(); $dis = self::disavow_table(); if ( (string) $wpdb->get_var( "SHOW TABLES LIKE '{$snap}'" ) !== $snap ) { $wpdb->query( "CREATE TABLE {$snap} ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT, domain varchar(191) NOT NULL, backlinks int unsigned NOT NULL DEFAULT 0, spam_score tinyint unsigned NOT NULL DEFAULT 0, domain_rank smallint unsigned NOT NULL DEFAULT 0, dofollow int unsigned NOT NULL DEFAULT 0, first_seen varchar(32) DEFAULT NULL, lost tinyint(1) NOT NULL DEFAULT 0, fetched_at datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY domain (domain), KEY spam_score (spam_score), KEY domain_rank (domain_rank) ) {$charset};" ); } if ( (string) $wpdb->get_var( "SHOW TABLES LIKE '{$dis}'" ) !== $dis ) { $wpdb->query( "CREATE TABLE {$dis} ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT, domain varchar(191) NOT NULL, reason varchar(255) DEFAULT NULL, source varchar(20) NOT NULL DEFAULT 'manual', spam_score tinyint unsigned DEFAULT NULL, added_at datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY domain (domain) ) {$charset};" ); } } private static function table_exists( $t ) { global $wpdb; return (string) $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $t ) ) === $t; } private static function result( $decoded ) { if ( is_wp_error( $decoded ) ) { return $decoded; } $task = isset( $decoded['tasks'][0] ) && is_array( $decoded['tasks'][0] ) ? $decoded['tasks'][0] : array(); if ( isset( $task['cost'] ) && (float) $task['cost'] > 0 && class_exists( 'AIEO_DFS_Rankings' ) ) { AIEO_DFS_Rankings::record_spend( (float) $task['cost'] ); } return isset( $task['result'][0] ) && is_array( $task['result'][0] ) ? $task['result'][0] : array(); } private static function budget_ok() { return ! class_exists( 'AIEO_DFS_Rankings' ) || AIEO_DFS_Rankings::spend_this_month() < AIEO_DFS_Rankings::budget(); } public static function fetch_summary() { if ( ! self::available() ) { return new WP_Error( 'aieo_dfs_off', __( 'DataForSEO is not connected (SEO → General).', 'ai-eshop-optimizer' ) ); } if ( ! self::budget_ok() ) { return new WP_Error( 'aieo_dfs_budget', __( 'The monthly DataForSEO budget is exhausted.', 'ai-eshop-optimizer' ) ); } $res = self::result( AIEO_DataForSEO::post( '/v3/backlinks/summary/live', array( 'target' => self::target(), 'include_subdomains' => true, 'backlinks_status_type' => 'live', 'rank_scale' => 'one_hundred', ) ) ); if ( is_wp_error( $res ) ) { return $res; } $summary = array( 'target' => (string) ( $res['target'] ?? self::target() ), 'rank' => (int) ( $res['rank'] ?? 0 ), 'backlinks' => (int) ( $res['backlinks'] ?? 0 ), 'referring_domains' => (int) ( $res['referring_domains'] ?? 0 ), 'referring_main_domains' => (int) ( $res['referring_main_domains'] ?? 0 ), 'broken_backlinks' => (int) ( $res['broken_backlinks'] ?? 0 ), 'spam_score' => (int) ( $res['backlinks_spam_score'] ?? $res['target_spam_score'] ?? 0 ), 'fetched_at' => current_time( 'mysql', true ), ); update_option( self::SUMMARY_OPT, $summary, false ); return $summary; } public static function cached_summary() { $s = get_option( self::SUMMARY_OPT, null ); return is_array( $s ) ? $s : null; } public static function refresh( $limit = 300 ) { if ( ! self::available() ) { return new WP_Error( 'aieo_dfs_off', __( 'DataForSEO is not connected (SEO → General).', 'ai-eshop-optimizer' ) ); } if ( ! self::budget_ok() ) { return new WP_Error( 'aieo_dfs_budget', __( 'The monthly DataForSEO budget is exhausted.', 'ai-eshop-optimizer' ) ); } self::ensure_tables(); self::fetch_summary(); $limit = min( 1000, max( 1, (int) $limit ) ); $res = self::result( AIEO_DataForSEO::post( '/v3/backlinks/referring_domains/live', array( 'target' => self::target(), 'limit' => $limit, 'order_by' => array( 'backlinks_spam_score,desc' ), 'backlinks_status_type' => 'live', 'include_subdomains' => true, 'rank_scale' => 'one_hundred', ) ) ); if ( is_wp_error( $res ) ) { return $res; } $items = isset( $res['items'] ) && is_array( $res['items'] ) ? $res['items'] : array(); global $wpdb; $snap = self::snap_table(); $wpdb->query( "TRUNCATE TABLE `{$snap}`" ); $now = current_time( 'mysql', true ); $stored = 0; foreach ( $items as $it ) { $domain = self::normalize_domain( (string) ( $it['domain'] ?? '' ) ); if ( '' === $domain ) { continue; } $wpdb->insert( $snap, array( 'domain' => $domain, 'backlinks' => (int) ( $it['backlinks'] ?? 0 ), 'spam_score' => min( 100, max( 0, (int) ( $it['backlinks_spam_score'] ?? 0 ) ) ), 'domain_rank' => min( 65535, max( 0, (int) ( $it['rank'] ?? 0 ) ) ), 'dofollow' => (int) ( $it['dofollow'] ?? 0 ), 'first_seen' => isset( $it['first_seen'] ) ? substr( (string) $it['first_seen'], 0, 32 ) : null, 'lost' => ! empty( $it['is_lost'] ) ? 1 : 0, 'fetched_at' => $now, ), array( '%s', '%d', '%d', '%d', '%d', '%s', '%d', '%s' ) ); $stored++; } update_option( self::LAST_OPT, $now, false ); return array( 'stored' => $stored ); } public static function referring_domains( $limit = 200, array $args = array() ) { global $wpdb; $snap = self::snap_table(); $dis = self::disavow_table(); if ( ! self::table_exists( $snap ) ) { return array(); } $limit = min( 1000, max( 1, (int) $limit ) ); $min_spam = max( 0, (int) ( $args['min_spam'] ?? 0 ) ); $order = (string) ( $args['order'] ?? 'spam' ); $order = in_array( $order, array( 'spam', 'rank', 'backlinks' ), true ) ? $order : 'spam'; $order_by = 'spam' === $order ? 's.spam_score DESC, s.backlinks DESC' : ( 'rank' === $order ? 's.domain_rank DESC' : 's.backlinks DESC' ); $dis_join = self::table_exists( $dis ) ? "LEFT JOIN `{$dis}` d ON d.domain = s.domain" : ''; $dis_col = self::table_exists( $dis ) ? 'd.domain IS NOT NULL' : '0'; $rows = $wpdb->get_results( $wpdb->prepare( "SELECT s.domain, s.backlinks, s.spam_score, s.domain_rank, s.dofollow, s.first_seen, s.lost, ({$dis_col}) AS is_disavowed FROM `{$snap}` s {$dis_join} WHERE s.spam_score >= %d ORDER BY {$order_by} LIMIT %d", $min_spam, $limit ), ARRAY_A ); return is_array( $rows ) ? array_map( array( __CLASS__, 'cast_row' ), $rows ) : array(); } private static function cast_row( $r ) { return array( 'domain' => (string) $r['domain'], 'backlinks' => (int) $r['backlinks'], 'spam_score' => (int) $r['spam_score'], 'domain_rank' => (int) $r['domain_rank'], 'dofollow' => (int) $r['dofollow'], 'first_seen' => (string) $r['first_seen'], 'lost' => (bool) (int) $r['lost'], 'is_disavowed' => (bool) (int) $r['is_disavowed'], ); } public static function toxic( $threshold = null, $limit = 1000 ) { $threshold = null === $threshold ? self::toxic_threshold() : (int) $threshold; return self::referring_domains( $limit, array( 'min_spam' => $threshold, 'order' => 'spam' ) ); } public static function toxic_threshold() { $t = class_exists( 'AIEO_DMM_Settings' ) ? AIEO_DMM_Settings::get( 'seo.disavow_spam_threshold', self::TOXIC_DEFAULT ) : self::TOXIC_DEFAULT; return min( 100, max( 1, (int) $t ) ); } public static function is_disavowed( $domain ) { global $wpdb; $dis = self::disavow_table(); if ( ! self::table_exists( $dis ) ) { return false; } $domain = self::normalize_domain( $domain ); return (bool) $wpdb->get_var( $wpdb->prepare( "SELECT 1 FROM `{$dis}` WHERE domain = %s", $domain ) ); } public static function disavow_add( $domain, $reason = '', $source = 'manual' ) { global $wpdb; self::ensure_tables(); $domain = self::normalize_domain( $domain ); if ( '' === $domain ) { return false; } $snap = self::snap_table(); $spam = self::table_exists( $snap ) ? $wpdb->get_var( $wpdb->prepare( "SELECT spam_score FROM `{$snap}` WHERE domain = %s", $domain ) ) : null; $dis = self::disavow_table(); $spam_sql = ( null === $spam ) ? 'NULL' : (int) $spam; $wpdb->query( $wpdb->prepare( "INSERT INTO `{$dis}` (domain, reason, source, spam_score, added_at) VALUES (%s, %s, %s, {$spam_sql}, %s) ON DUPLICATE KEY UPDATE reason = VALUES(reason), source = VALUES(source), spam_score = COALESCE(VALUES(spam_score), spam_score)", $domain, mb_substr( (string) $reason, 0, 255 ), in_array( $source, array( 'manual', 'auto' ), true ) ? $source : 'manual', current_time( 'mysql', true ) ) ); return true; } public static function disavow_remove( $domain ) { global $wpdb; $dis = self::disavow_table(); if ( ! self::table_exists( $dis ) ) { return false; } $domain = self::normalize_domain( $domain ); return (bool) $wpdb->query( $wpdb->prepare( "DELETE FROM `{$dis}` WHERE domain = %s", $domain ) ); } public static function disavow_list() { global $wpdb; $dis = self::disavow_table(); if ( ! self::table_exists( $dis ) ) { return array(); } $rows = $wpdb->get_results( "SELECT domain, reason, source, spam_score, added_at FROM `{$dis}` ORDER BY spam_score DESC, domain ASC", ARRAY_A ); return is_array( $rows ) ? $rows : array(); } public static function disavow_count() { global $wpdb; $dis = self::disavow_table(); if ( ! self::table_exists( $dis ) ) { return 0; } return (int) $wpdb->get_var( "SELECT COUNT(*) FROM `{$dis}`" ); } public static function disavow_all_toxic( $threshold = null ) { $n = 0; foreach ( self::toxic( $threshold ) as $d ) { if ( self::disavow_add( $d['domain'], sprintf( 'spam_score %d', $d['spam_score'] ), 'auto' ) ) { $n++; } } return $n; } public static function disavow_file() { $lines = array(); $lines[] = '# Disavow file generated by Rooster One'; $lines[] = '# Site: ' . self::target(); $lines[] = '# Generated: ' . current_time( 'mysql', true ) . ' UTC'; $lines[] = '# Upload at: ' . self::disavow_tool_url(); $lines[] = ''; $seen = array(); foreach ( self::disavow_list() as $row ) { $domain = self::normalize_domain( (string) $row['domain'] ); if ( '' === $domain || isset( $seen[ $domain ] ) ) { continue; } $seen[ $domain ] = true; $note = trim( (string) ( $row['reason'] ?? '' ) ); if ( '' !== $note ) { $lines[] = '# ' . preg_replace( '/[\r\n]+/', ' ', $note ); } $lines[] = 'domain:' . $domain; } return implode( "\n", $lines ) . "\n"; } public static function disavow_tool_url() { return 'https://search.google.com/search-console/disavow-links'; } public static function normalize_domain( $raw ) { $raw = trim( strtolower( (string) $raw ) ); if ( '' === $raw ) { return ''; } if ( false !== strpos( $raw, '/' ) || false !== strpos( $raw, ':' ) ) { $host = (string) wp_parse_url( ( false === strpos( $raw, '://' ) ? 'http://' . $raw : $raw ), PHP_URL_HOST ); if ( '' !== $host ) { $raw = $host; } } $raw = preg_replace( '/^www\./', '', $raw ); return preg_match( '/^[a-z0-9.-]+\.[a-z]{2,}$/', $raw ) ? $raw : ''; } }