'platform_ai_visibility.microsoft', 'schema_version' => 'aio-evidence/1', 'source' => 'microsoft_ai_performance_export', 'state' => empty( $snapshot['imported_at'] ) ? 'not_imported' : 'available', 'retrieved_at' => isset( $snapshot['imported_at'] ) ? (string) $snapshot['imported_at'] : '', 'evidence_family' => 'platform_ai_visibility', ); } /** Normalise a CSV header into a comparison key. */ private static function header_key( $value ) { $value = strtolower( trim( (string) $value ) ); return preg_replace( '/[^a-z0-9]+/', '', $value ); } /** First matching column index for any known alias. */ private static function column_index( $headers, $aliases ) { $aliases = array_map( array( __CLASS__, 'header_key' ), $aliases ); foreach ( $headers as $index => $header ) { if ( in_array( self::header_key( $header ), $aliases, true ) ) { return (int) $index; } } return -1; } /** Parse Microsoft percentage/number presentation into a float. */ private static function numeric_value( $value ) { $value = str_replace( array( ',', '%', ' ' ), '', (string) $value ); return is_numeric( $value ) ? (float) $value : 0.0; } /** Canonical host comparison for cited-page ownership. */ private static function canonical_host( $host ) { $host = strtolower( trim( (string) $host ) ); return 0 === strpos( $host, 'www.' ) ? substr( $host, 4 ) : $host; } /** Validate one cited-page URL against this WordPress site's host. */ private static function cited_page_url( $value, $site_host ) { $url = esc_url_raw( trim( (string) $value ) ); if ( '' === $url ) { return ''; } $scheme = strtolower( (string) wp_parse_url( $url, PHP_URL_SCHEME ) ); $host = self::canonical_host( wp_parse_url( $url, PHP_URL_HOST ) ); if ( ! in_array( $scheme, array( 'http', 'https' ), true ) || '' === $host ) { return ''; } return self::canonical_host( $site_host ) === $host ? $url : ''; } /** * Parse one official Microsoft CSV export. * * Supported shapes are Overview, Grounding queries and Cited pages. The * parser is intentionally header-driven because Microsoft can reorder export * columns without changing their meaning. */ public static function parse_csv_path( $path, $site_host = '' ) { $result = array( 'type' => '', 'summary' => array(), 'queries' => array(), 'pages' => array(), 'rows_read' => 0, 'rows_rejected' => 0, ); $handle = @fopen( $path, 'rb' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen -- bounded uploaded CSV, not WordPress filesystem content. if ( ! is_resource( $handle ) ) { return new WP_Error( 'microsoft_ai_open_failed', __( 'The Microsoft export could not be opened.', 'ai-o-optimiser' ) ); } $headers = fgetcsv( $handle ); if ( ! is_array( $headers ) || empty( $headers ) ) { fclose( $handle ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- closes the bounded uploaded CSV handle opened directly above. return new WP_Error( 'microsoft_ai_empty_csv', __( 'The Microsoft export has no readable header row.', 'ai-o-optimiser' ) ); } if ( isset( $headers[0] ) ) { $headers[0] = preg_replace( '/^\xEF\xBB\xBF/', '', (string) $headers[0] ); } $query_col = self::column_index( $headers, array( 'Query', 'Grounding query', 'Grounding queries', 'Search query' ) ); $page_col = self::column_index( $headers, array( 'Page URL', 'Cited page', 'URL', 'Page' ) ); $cite_col = self::column_index( $headers, array( 'Citations', 'Citation count', 'Total citations' ) ); $share_col = self::column_index( $headers, array( 'Share of authority', 'SoA', 'Citation share', 'Citation share %', 'Share' ) ); $traffic_col = self::column_index( $headers, array( 'AI referral traffic', 'AI referrals', 'Referral traffic', 'AI-referred sessions' ) ); $type_col = self::column_index( $headers, array( 'Query type', 'Branded/non-branded', 'Brand type' ) ); $intent_col = self::column_index( $headers, array( 'Intent' ) ); $topic_col = self::column_index( $headers, array( 'Topic', 'Topics' ) ); if ( $query_col >= 0 ) { $result['type'] = 'queries'; } elseif ( $page_col >= 0 ) { $result['type'] = 'pages'; } elseif ( $cite_col >= 0 || $share_col >= 0 || $traffic_col >= 0 ) { $result['type'] = 'summary'; } else { fclose( $handle ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- closes the bounded uploaded CSV handle opened directly above. return new WP_Error( 'microsoft_ai_unknown_columns', __( 'This CSV does not look like a supported Microsoft AI Performance or Clarity citation export.', 'ai-o-optimiser' ) ); } $site_host = $site_host ?: (string) wp_parse_url( home_url( '/' ), PHP_URL_HOST ); $sum_citations = 0; while ( $result['rows_read'] < self::MAX_ROWS && false !== ( $row = fgetcsv( $handle ) ) ) { if ( ! is_array( $row ) ) { continue; } $result['rows_read']++; $citations = $cite_col >= 0 && isset( $row[ $cite_col ] ) ? (int) round( self::numeric_value( $row[ $cite_col ] ) ) : 0; $share = $share_col >= 0 && isset( $row[ $share_col ] ) ? self::numeric_value( $row[ $share_col ] ) : 0.0; if ( 'queries' === $result['type'] ) { $query = isset( $row[ $query_col ] ) ? sanitize_text_field( (string) $row[ $query_col ] ) : ''; if ( '' === $query ) { $result['rows_rejected']++; continue; } $sum_citations += $citations; if ( count( $result['queries'] ) < self::MAX_ITEMS ) { $result['queries'][] = array( 'query' => substr( $query, 0, 300 ), 'citations' => $citations, 'share' => $share, 'query_type' => $type_col >= 0 && isset( $row[ $type_col ] ) ? sanitize_text_field( (string) $row[ $type_col ] ) : '', 'intent' => $intent_col >= 0 && isset( $row[ $intent_col ] ) ? sanitize_text_field( (string) $row[ $intent_col ] ) : '', 'topic' => $topic_col >= 0 && isset( $row[ $topic_col ] ) ? sanitize_text_field( (string) $row[ $topic_col ] ) : '', ); } continue; } if ( 'pages' === $result['type'] ) { $url = isset( $row[ $page_col ] ) ? self::cited_page_url( $row[ $page_col ], $site_host ) : ''; if ( '' === $url ) { $result['rows_rejected']++; continue; } $sum_citations += $citations; if ( count( $result['pages'] ) < self::MAX_ITEMS ) { $result['pages'][] = array( 'url' => $url, 'citations' => $citations, 'share' => $share, ); } continue; } if ( 'summary' === $result['type'] ) { if ( $cite_col >= 0 && isset( $row[ $cite_col ] ) ) { $result['summary']['citations'] = $citations; } if ( $share_col >= 0 && isset( $row[ $share_col ] ) ) { $result['summary']['share_of_authority'] = $share; } if ( $traffic_col >= 0 && isset( $row[ $traffic_col ] ) ) { $result['summary']['ai_referral_traffic'] = (int) round( self::numeric_value( $row[ $traffic_col ] ) ); } break; } } fclose( $handle ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- closes the bounded uploaded CSV handle opened directly above. if ( 'queries' === $result['type'] ) { usort( $result['queries'], static function ( $a, $b ) { return (int) $b['citations'] <=> (int) $a['citations']; } ); $result['summary']['citations_from_query_export'] = $sum_citations; } if ( 'pages' === $result['type'] ) { usort( $result['pages'], static function ( $a, $b ) { return (int) $b['citations'] <=> (int) $a['citations']; } ); $result['summary']['citations_from_page_export'] = $sum_citations; } return $result; } /** Import handler for one official CSV export. */ public static function handle_import() { if ( ! current_user_can( self::capability() ) ) { wp_die( esc_html__( 'You are not allowed to import Microsoft AI visibility evidence.', 'ai-o-optimiser' ) ); } check_admin_referer( 'ai_o_optimiser_microsoft_ai_import' ); $file = isset( $_FILES['microsoft_ai_csv'] ) && is_array( $_FILES['microsoft_ai_csv'] ) ? $_FILES['microsoft_ai_csv'] : array(); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- file metadata validated below; contents parsed as CSV. if ( empty( $file['tmp_name'] ) || UPLOAD_ERR_OK !== (int) ( $file['error'] ?? UPLOAD_ERR_NO_FILE ) ) { self::redirect_with_state( 'upload_error' ); } $size = isset( $file['size'] ) ? (int) $file['size'] : 0; $name = isset( $file['name'] ) ? sanitize_file_name( (string) $file['name'] ) : ''; if ( $size < 1 || $size > self::MAX_FILE_BYTES || 'csv' !== strtolower( pathinfo( $name, PATHINFO_EXTENSION ) ) ) { self::redirect_with_state( 'invalid_file' ); } $parsed = self::parse_csv_path( (string) $file['tmp_name'] ); if ( is_wp_error( $parsed ) ) { self::redirect_with_state( $parsed->get_error_code() ); } $period_start = isset( $_POST['period_start'] ) ? sanitize_text_field( wp_unslash( $_POST['period_start'] ) ) : ''; $period_end = isset( $_POST['period_end'] ) ? sanitize_text_field( wp_unslash( $_POST['period_end'] ) ) : ''; if ( ! self::valid_date( $period_start ) || ! self::valid_date( $period_end ) || $period_start > $period_end ) { self::redirect_with_state( 'invalid_period' ); } $current = self::snapshot(); $same_period = isset( $current['period']['start'], $current['period']['end'] ) && $period_start === $current['period']['start'] && $period_end === $current['period']['end']; $snapshot = $same_period ? $current : array(); $snapshot['schema_version'] = 'aio-evidence/1'; $snapshot['source'] = 'microsoft_ai_performance_export'; $snapshot['evidence_family'] = 'platform_ai_visibility'; $snapshot['period'] = array( 'start' => $period_start, 'end' => $period_end ); $snapshot['imported_at'] = gmdate( 'c' ); $snapshot['summary'] = isset( $snapshot['summary'] ) && is_array( $snapshot['summary'] ) ? $snapshot['summary'] : array(); $snapshot['datasets'] = isset( $snapshot['datasets'] ) && is_array( $snapshot['datasets'] ) ? $snapshot['datasets'] : array(); if ( ! empty( $parsed['summary'] ) ) { $snapshot['summary'] = array_merge( $snapshot['summary'], $parsed['summary'] ); } if ( 'queries' === $parsed['type'] ) { $snapshot['queries'] = $parsed['queries']; } if ( 'pages' === $parsed['type'] ) { $snapshot['pages'] = $parsed['pages']; } $snapshot['datasets'][ $parsed['type'] ] = array( 'imported_at' => $snapshot['imported_at'], 'rows_read' => (int) $parsed['rows_read'], 'rows_rejected' => (int) $parsed['rows_rejected'], 'original_name' => $name, ); if ( false === get_option( self::OPTION, false ) ) { add_option( self::OPTION, $snapshot, '', 'no' ); } else { update_option( self::OPTION, $snapshot, false ); } self::redirect_with_state( 'imported' ); } /** Clear imported evidence without touching any other visibility data. */ public static function handle_clear() { if ( ! current_user_can( self::capability() ) ) { wp_die( esc_html__( 'You are not allowed to clear Microsoft AI visibility evidence.', 'ai-o-optimiser' ) ); } check_admin_referer( 'ai_o_optimiser_microsoft_ai_clear' ); delete_option( self::OPTION ); self::redirect_with_state( 'cleared' ); } private static function valid_date( $value ) { if ( ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', (string) $value ) ) { return false; } $date = DateTime::createFromFormat( '!Y-m-d', (string) $value ); return $date && $date->format( 'Y-m-d' ) === $value; } private static function redirect_with_state( $state ) { wp_safe_redirect( add_query_arg( array( 'page' => self::PAGE_SLUG, 'microsoft_ai' => sanitize_key( $state ) ), admin_url( 'admin.php' ) ) ); exit; } /** Render imported Microsoft AI citation evidence and the import control. */ public static function render_page() { if ( ! current_user_can( self::capability() ) ) { return; } $snapshot = self::snapshot(); $summary = isset( $snapshot['summary'] ) && is_array( $snapshot['summary'] ) ? $snapshot['summary'] : array(); $queries = isset( $snapshot['queries'] ) && is_array( $snapshot['queries'] ) ? $snapshot['queries'] : array(); $pages = isset( $snapshot['pages'] ) && is_array( $snapshot['pages'] ) ? $snapshot['pages'] : array(); $status = isset( $_GET['microsoft_ai'] ) ? sanitize_key( wp_unslash( $_GET['microsoft_ai'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only redirect result state. ?>