http = $http; } /** * Runs the check and keeps the outcome. * * @param array $values Saved Google settings. * @return array{time: int, results: array>, foreign: array{ga4: array, gtm: array, verification: array}} */ public function run( array $values ): array { $fetcher = new Fetcher( $this->http ); $home = home_url( '/' ); $mode = $fetcher->probe( $home ); $pages = array(); $failed = array(); if ( null === $mode ) { $failed[] = $home; } else { $fetcher = new Fetcher( $this->http, $mode ); $started = microtime( true ); foreach ( self::addresses() as $label => $url ) { if ( microtime( true ) - $started > self::BUDGET ) { break; } $response = $fetcher->fetch( $url ); if ( 200 === $response['status'] && '' !== $response['body'] ) { $pages[ $label ] = $response['body']; } else { $failed[] = $url; } } } $outcome = self::evaluate( $pages, $values, $failed ); update_option( self::OPTION, $outcome, false ); return $outcome; } /** * The last stored outcome, or null before the first check. * * @return array|null */ public static function last(): ?array { $saved = get_option( self::OPTION, null ); return is_array( $saved ) ? $saved : null; } /** * Tag codes that other plugins or the theme already print, from the last check. * * @return array{ga4: array, gtm: array, verification: array} */ public static function foreign(): array { $last = self::last(); $seen = is_array( $last ) && isset( $last['foreign'] ) && is_array( $last['foreign'] ) ? $last['foreign'] : array(); return array( 'ga4' => array_values( array_map( 'strval', (array) ( $seen['ga4'] ?? array() ) ) ), 'gtm' => array_values( array_map( 'strval', (array) ( $seen['gtm'] ?? array() ) ) ), 'verification' => array_values( array_map( 'strval', (array) ( $seen['verification'] ?? array() ) ) ), ); } /** * The pages to look at: the front page, the newest post and the newest page. * * @return array */ private static function addresses(): array { $urls = array( __( 'Front page', 'forensix-digital' ) => home_url( '/' ) ); foreach ( array( 'post' => __( 'A post', 'forensix-digital' ), 'page' => __( 'A page', 'forensix-digital' ), ) as $type => $label ) { $ids = get_posts( array( 'post_type' => $type, 'post_status' => 'publish', 'posts_per_page' => 1, 'fields' => 'ids', 'no_found_rows' => true, ) ); if ( array() !== $ids && (int) get_option( 'page_on_front' ) !== (int) $ids[0] ) { $urls[ $label ] = (string) get_permalink( (int) $ids[0] ); } } return $urls; } /** * Turns the fetched pages into plain statements. * * @param array $pages Page label => HTML. * @param array $values Saved Google settings. * @param array $failed Addresses that could not be read. * @return array{time: int, results: array>, foreign: array{ga4: array, gtm: array, verification: array}} */ public static function evaluate( array $pages, array $values, array $failed = array() ): array { $results = array(); $foreign = array( 'ga4' => array(), 'gtm' => array(), 'verification' => array(), ); $add = static function ( string $level, string $code, string $message ) use ( &$results ): void { $row = array( 'level' => $level, 'code' => $code, 'message' => $message, ); if ( ! in_array( $row, $results, true ) ) { $results[] = $row; } }; foreach ( $failed as $url ) { /* translators: %s: page address. */ $add( 'problem', 'unreachable', sprintf( __( 'This page could not be read for the check: %s', 'forensix-digital' ), $url ) ); } $verification = (string) ( $values['verification'] ?? '' ); $ga4 = (string) ( $values['ga4_id'] ?? '' ); $gtm = (string) ( $values['gtm_id'] ?? '' ); $tools = array(); foreach ( $pages as $label => $html ) { $page = TagDetector::inspect( $html ); $tools = array_merge( $tools, $page['consent_tools'] ); $count = static function ( string $kind, string $id ) use ( $page ): int { return (int) ( $page['ours'][ $kind ][ $id ] ?? 0 ) + (int) ( $page['other'][ $kind ][ $id ] ?? 0 ); }; if ( '' !== $verification && array_key_first( $pages ) === $label ) { $codes = array_merge( $page['ours']['verification'], $page['other']['verification'] ); if ( ! in_array( $verification, $codes, true ) ) { $add( 'problem', 'verification_missing', __( 'The Search Console verification tag is not on your front page. If you just saved it, empty the cache of your caching plugin and check again.', 'forensix-digital' ) ); } } $foreign['verification'] = array_merge( $foreign['verification'], $page['other']['verification'] ); foreach ( array_keys( $page['other']['ga4'] ) as $id ) { $foreign['ga4'][] = $id; } foreach ( array_keys( $page['other']['gtm'] ) as $id ) { $foreign['gtm'][] = $id; } foreach ( array( 'ga4' => $ga4, 'gtm' => $gtm, ) as $kind => $id ) { if ( '' === $id ) { continue; } $found = $count( $kind, $id ); if ( 0 === $found ) { /* translators: 1: page, 2: tag code. */ $add( 'problem', 'tag_missing', sprintf( __( '%1$s: the tag %2$s is not on this page.', 'forensix-digital' ), $label, $id ) ); } elseif ( $found > 1 ) { /* translators: 1: page, 2: tag code, 3: how many times. */ $add( 'problem', 'duplicate_tag', sprintf( __( '%1$s: the tag %2$s is loaded %3$d times, so visits are counted more than once. Another plugin or your theme probably adds it too.', 'forensix-digital' ), $label, $id, $found ) ); } foreach ( array_merge( array_keys( $page['other'][ $kind ] ), array_keys( $page['ours'][ $kind ] ) ) as $seen ) { if ( $seen !== $id ) { /* translators: 1: page, 2: tag code found, 3: tag code set here. */ $add( 'warning', 'other_property', sprintf( __( '%1$s: this page also loads %2$s, which is not the code set here (%3$s). Make sure both belong to you and that this is intended.', 'forensix-digital' ), $label, $seen, $id ) ); } } } if ( '' === $ga4 && '' === $gtm ) { foreach ( array_keys( $page['other']['ga4'] ) as $id ) { /* translators: %s: tag code. */ $add( 'ok', 'found_elsewhere', sprintf( __( 'A Google Analytics tag (%s) is added by another plugin or your theme. Forensix Digital adds nothing, so nothing is counted twice.', 'forensix-digital' ), $id ) ); } foreach ( array_keys( $page['other']['gtm'] ) as $id ) { /* translators: %s: container code. */ $add( 'ok', 'found_elsewhere', sprintf( __( 'A Google Tag Manager container (%s) is added by another plugin or your theme. Forensix Digital adds nothing.', 'forensix-digital' ), $id ) ); } } $gtm_here = array_merge( $page['ours']['gtm'], $page['other']['gtm'] ); $ga4_here = array_merge( $page['ours']['ga4'], $page['other']['ga4'] ); if ( array() !== $gtm_here && array() !== $ga4_here ) { /* translators: %s: page. */ $add( 'warning', 'ga4_and_gtm', sprintf( __( '%s: Tag Manager and a direct Analytics tag are both loaded. If your container also sends Analytics data, every visit is counted twice.', 'forensix-digital' ), $label ) ); } if ( array() !== array_merge( $page['ours']['universal'], $page['other']['universal'] ) ) { /* translators: %s: page. */ $add( 'warning', 'universal_analytics', sprintf( __( '%s: an old Universal Analytics code (UA-…) is still loaded. Google stopped collecting data with it, so it can be removed.', 'forensix-digital' ), $label ) ); } } $tools = array_values( array_unique( $tools ) ); if ( array() !== $tools ) { /* translators: %s: names of consent tools. */ $add( 'ok', 'consent_tool', sprintf( __( 'A consent tool is active (%s). Google tags may load only after a visitor accepts, so test in a private window and accept cookies first.', 'forensix-digital' ), implode( ', ', $tools ) ) ); } $configured = '' !== $verification || '' !== $ga4 || '' !== $gtm; if ( array() !== $pages && ! $configured && array() === $results ) { $add( 'ok', 'nothing', __( 'No Google tags were found on your pages, and none are set up here yet.', 'forensix-digital' ) ); } $trouble = array_filter( $results, static fn( array $row ): bool => 'ok' !== $row['level'] ); if ( array() !== $pages && $configured && array() === $trouble ) { $add( 'ok', 'all_good', __( 'Everything you set up here is on your pages exactly once.', 'forensix-digital' ) ); } return array( 'time' => time(), 'results' => $results, 'foreign' => array( 'ga4' => array_values( array_unique( $foreign['ga4'] ) ), 'gtm' => array_values( array_unique( $foreign['gtm'] ) ), 'verification' => array_values( array_unique( $foreign['verification'] ) ), ), ); } }