self::ID, 'name' => __( 'Google tools', 'forensix-digital' ), 'description' => __( 'Step-by-step help to connect your site to Google Search Console and Google Analytics, and a check that finds missing or doubled tags.', 'forensix-digital' ), 'category' => 'tools', 'edition' => Edition::FREE, 'features' => array( 'F-GOOGLE-01' => Edition::FREE, 'F-GOOGLE-02' => Edition::FREE, 'F-GOOGLE-03' => Edition::FREE, ), 'deactivation_effects' => array( __( 'The tags and the verification line this app added are removed from your pages. Tags added by other plugins stay.', 'forensix-digital' ), __( 'Your saved codes stay, so turning the app on again brings the tags back.', 'forensix-digital' ), ), 'factory' => static fn(): self => new self(), ) ); } /** * Settings service with the scope declared. */ private static function settings(): Settings { $settings = Plugin::container()->get( Settings::class ); $settings->register( GoogleConfig::SCOPE, GoogleConfig::defaults(), array( GoogleConfig::class, 'sanitize' ) ); return $settings; } /** * Hooks the app while it is active. */ public function register(): void { add_action( 'wp_head', array( $this, 'print_head' ), 1 ); add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_tags' ), 1 ); add_action( 'wp_body_open', array( $this, 'print_body' ), 1 ); add_action( 'rest_api_init', array( $this, 'register_routes' ) ); add_filter( 'forensix_admin_screens', array( $this, 'add_screen' ) ); } /** * Whether tags may be printed for this visitor. * * @param array $values Saved settings. */ private static function should_print( array $values ): bool { if ( is_admin() || is_feed() || is_robots() || is_customize_preview() ) { return false; } return ! empty( $values['track_editors'] ) || ! ( is_user_logged_in() && current_user_can( 'edit_posts' ) ); } /** * Prints the verification line. It goes to everyone, including editors, because Google reads * it as a visitor. */ public function print_head(): void { $tag = GoogleOutput::meta( self::settings()->all( GoogleConfig::SCOPE ), GoogleCheck::foreign() ); echo $tag; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped inside GoogleOutput; the code is validated against an exact pattern. } /** * Hands the Analytics and Tag Manager scripts to WordPress. */ public function enqueue_tags(): void { $values = self::settings()->all( GoogleConfig::SCOPE ); if ( ! self::should_print( $values ) ) { return; } $args = array( 'strategy' => 'async', 'in_footer' => false, ); foreach ( GoogleOutput::scripts( $values, GoogleCheck::foreign() ) as $script ) { wp_enqueue_script( $script['handle'], $script['src'], array(), null, $args ); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion -- A Google-hosted loader has no version of ours; null keeps the address as Google documents it. wp_add_inline_script( $script['handle'], $script['inline'] ); } } /** * Prints the Tag Manager fallback after the opening body tag. */ public function print_body(): void { $values = self::settings()->all( GoogleConfig::SCOPE ); if ( ! self::should_print( $values ) ) { return; } echo GoogleOutput::body( $values, GoogleCheck::foreign() ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped inside GoogleOutput. } /** * Registers the REST routes. */ public function register_routes(): void { $can = static fn(): bool => current_user_can( Capabilities::MANAGE_SETTINGS ); register_rest_route( Api::NAMESPACE, '/google', array( array( 'methods' => 'GET', 'callback' => array( $this, 'show' ), 'permission_callback' => $can, ), array( 'methods' => 'POST', 'callback' => array( $this, 'save' ), 'permission_callback' => $can, ), ) ); register_rest_route( Api::NAMESPACE, '/google/check', array( 'methods' => 'POST', 'callback' => array( $this, 'check' ), 'permission_callback' => $can, ) ); } /** * Saved codes, the last check and what the screen needs for its guidance. */ public function show(): \WP_REST_Response { return new \WP_REST_Response( array( 'values' => self::settings()->all( GoogleConfig::SCOPE ), 'check' => GoogleCheck::last(), 'host' => (string) wp_parse_url( home_url(), PHP_URL_HOST ), 'url' => home_url( '/' ), ) ); } /** * Saves the codes. * * @param \WP_REST_Request $request Request. * @phpstan-param \WP_REST_Request> $request */ public function save( \WP_REST_Request $request ): \WP_REST_Response { $settings = self::settings(); $settings->update( GoogleConfig::SCOPE, (array) $request->get_json_params() ); return new \WP_REST_Response( array( 'ok' => true, 'message' => __( 'Saved. Empty the cache of your caching plugin, then run the check below.', 'forensix-digital' ), 'values' => $settings->all( GoogleConfig::SCOPE ), ) ); } /** * Checks the pages now. */ public function check(): \WP_REST_Response { $values = self::settings()->all( GoogleConfig::SCOPE ); $outcome = ( new GoogleCheck( Plugin::container()->get( Http::class ) ) )->run( $values ); return new \WP_REST_Response( array( 'ok' => true, 'check' => $outcome, ) ); } /** * Adds the page to the Forensix menu. * * @param array $screens Screens. * @phpstan-param array $screens * @return array */ public function add_screen( array $screens ): array { $screens['forensix-google'] = array( 'key' => 'google', 'title' => __( 'Google tools', 'forensix-digital' ), 'capability' => Capabilities::MANAGE_SETTINGS, ); return $screens; } /** * Nothing to set up. */ public function activate(): void {} /** * Removes the hooks, so the tags disappear from the pages. */ public function deactivate(): void { remove_action( 'wp_head', array( $this, 'print_head' ), 1 ); remove_action( 'wp_enqueue_scripts', array( $this, 'enqueue_tags' ), 1 ); remove_action( 'wp_body_open', array( $this, 'print_body' ), 1 ); remove_action( 'rest_api_init', array( $this, 'register_routes' ) ); remove_filter( 'forensix_admin_screens', array( $this, 'add_screen' ) ); } /** * Deletes the saved codes and the last check. */ public function delete_data(): void { delete_option( Settings::OPTION_PREFIX . GoogleConfig::SCOPE ); delete_option( GoogleCheck::OPTION ); } }