soa_tracker = $soa_tracker; } /** * Get all active competitor domains. * * @return string[] */ public function get_competitor_domains(): array { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared return $wpdb->get_col( "SELECT competitor_domain FROM {$wpdb->prefix}aicommerce_soa_competitors WHERE is_active = 1" ) ?: array(); } /** * Get all competitors with metadata. * * @return array */ public function get_all_competitors(): array { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared return $wpdb->get_results( "SELECT id, competitor_domain, competitor_name, is_active, added_at FROM {$wpdb->prefix}aicommerce_soa_competitors ORDER BY added_at DESC", ARRAY_A ) ?: array(); } /** * Add a competitor domain. * * @param string $domain Competitor domain. * @param string $name Optional display name. * * @return int Insert ID, or 0 on failure/duplicate. */ public function add_competitor( string $domain, string $name = '' ): int { $domain = $this->sanitize_domain( $domain ); if ( empty( $domain ) ) { return 0; } global $wpdb; // Check for duplicate. // Safe: internal $wpdb->prefix table names; user values use placeholders. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter $exists = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$wpdb->prefix}aicommerce_soa_competitors WHERE competitor_domain = %s", $domain ) ); // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter if ( $exists ) { return 0; } // Safe: internal $wpdb->prefix table names; user values use placeholders. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter $wpdb->insert( $wpdb->prefix . 'aicommerce_soa_competitors', array( 'competitor_domain' => $domain, 'competitor_name' => sanitize_text_field( $name ), 'is_active' => 1, 'added_at' => current_time( 'mysql' ), ), array( '%s', '%s', '%d', '%s' ) ); // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter return (int) $wpdb->insert_id; } /** * Remove a competitor by ID. * * @param int $id Competitor ID. * * @return bool True if deleted. */ public function remove_competitor( int $id ): bool { global $wpdb; // Safe: internal $wpdb->prefix table names; user values use placeholders. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter $deleted = $wpdb->delete( $wpdb->prefix . 'aicommerce_soa_competitors', array( 'id' => $id ), array( '%d' ) ); // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter return (bool) $deleted; } /** * Get SOA comparison between own store and competitors. * * @param int $days Number of days to analyze. * * @return array Comparison data. */ public function get_competitor_soa_comparison( int $days = 30 ): array { $own_soa = $this->soa_tracker->get_soa_summary( $days ); $competitors = $this->get_all_competitors(); // Calculate own average SOA across platforms. $own_avg = 0.0; if ( ! empty( $own_soa['by_platform'] ) ) { $values = array_column( $own_soa['by_platform'], 'current' ); $own_avg = count( $values ) > 0 ? array_sum( $values ) / count( $values ) : 0.0; } // Get competitor appearance data from soa_results. global $wpdb; // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date $since = date( 'Y-m-d', time() - ( $days * DAY_IN_SECONDS ) ); // Safe: internal $wpdb->prefix table names; user values use placeholders. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter $total_results = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}aicommerce_soa_results WHERE checked_at >= %s", $since . ' 00:00:00' ) ); // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter $competitor_data = array(); foreach ( $competitors as $comp ) { $domain = $comp['competitor_domain']; $search_term = strtolower( str_replace( 'www.', '', $domain ) ); // Safe: internal $wpdb->prefix table names; user values use placeholders. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter $appearances = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}aicommerce_soa_results WHERE checked_at >= %s AND LOWER(competitor_mentions) LIKE %s", $since . ' 00:00:00', '%' . $wpdb->esc_like( $search_term ) . '%' ) ); // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter $appearance_pct = $total_results > 0 ? round( ( $appearances / $total_results ) * 100, 2 ) : 0; $competitor_data[] = array( 'id' => (int) $comp['id'], 'domain' => $domain, 'name' => $comp['competitor_name'] ?: $domain, 'appearances' => $appearances, 'appearance_pct' => $appearance_pct, ); } // Sort by appearance_pct descending. usort( $competitor_data, static function ( $a, $b ) { return $b['appearance_pct'] <=> $a['appearance_pct']; } ); return array( 'own' => array( 'domain' => wp_parse_url( home_url(), PHP_URL_HOST ), 'name' => get_bloginfo( 'name' ), 'soa' => round( $own_avg, 2 ), ), 'competitors' => $competitor_data, 'period_days' => $days, ); } /** * Sanitize a domain input. * * @param string $domain Raw domain input. * * @return string Cleaned domain. */ private function sanitize_domain( string $domain ): string { $domain = strtolower( trim( $domain ) ); $domain = preg_replace( '#^https?://#', '', $domain ); $domain = str_replace( 'www.', '', $domain ); $domain = rtrim( $domain, '/' ); return sanitize_text_field( $domain ); } }