core = $core; $this->init(); $this->handle_authentication(); $this->tracking(); } #region Authentication public function get_redirect_url() { return admin_url( 'admin.php?page=mwseo_settings&nekoTab=settings' ); } /** * Initialize the module and load settings. */ public function init() { $this->use_cache = $this->core->get_option( 'analytics_cache', false ); $this->property_ids = $this->core->get_option( 'google_analytics_property_ids', array() ); $this->property_id = $this->core->get_option( 'google_analytics_property_id' ); if ( empty( $this->property_id ) && !empty( $this->property_ids ) ) { $this->property_id = $this->property_ids[0]; } $this->client_secret = $this->core->get_option( 'google_analytics_client_secret', '' ); $this->client_id = $this->core->get_option( 'google_analytics_client_id', '' ); if ( empty( $this->client_secret ) || empty( $this->client_id ) ) { //$this->core->log( "⚠️ Google Analytics not configured. Please set your Client ID and Client Secret in the settings." ); return; } $token_info = $this->get_token_info(); if ( $token_info && isset( $token_info['access_token'] ) ) { $this->current_access_token = $token_info['access_token']; } if ( $token_info && isset( $token_info['refresh_token'] ) ) { $this->current_refresh_token = $token_info['refresh_token']; } if ( $token_info && isset( $token_info['expires_at'] ) ) { $this->current_expires_at = $token_info['expires_at']; } } /** * Read the stored token info, migrating it out of the legacy transient on the way. */ private function get_token_info() { $token_info = get_option( self::OPTION_TOKEN_INFO ); if ( empty( $token_info ) ) { $legacy = get_transient( self::OPTION_TOKEN_INFO ); if ( is_array( $legacy ) ) { $token_info = $legacy; $this->set_token_info( $legacy ); } delete_transient( self::OPTION_TOKEN_INFO ); } return is_array( $token_info ) ? $token_info : null; } private function set_token_info( $token_info ) { update_option( self::OPTION_TOKEN_INFO, $token_info, false ); } private function check_properties() { if ( empty( $this->property_id ) || ! in_array( $this->property_id, $this->property_ids ) ) { $this->core->log( "⚠️ Google Analytics property not set or invalid." ); return false; } return true; } public function is_authenticated() { if ( empty( $this->client_secret ) || empty( $this->client_id ) ) { return false; } // We don't need a property to be authenticated // if ( empty( $this->property_id ) || ! in_array( $this->property_id, $this->property_ids ) ) { // $this->core->log( "⚠️ Google Analytics property not set or invalid." ); // return false; // } if ( ! $this->current_access_token || ! $this->current_refresh_token || ! $this->current_expires_at ) { return false; } if ( time() >= $this->current_expires_at ) { if ( ! $this->get_refresh_token() ) { return false; } } return true; } public function get_last_error() { return $this->last_api_error; } public function unlink() { $this->core->log( "🔗 Unlinking Google Analytics." ); delete_option( self::OPTION_TOKEN_INFO ); delete_transient( self::OPTION_TOKEN_INFO ); $this->current_access_token = null; $this->current_refresh_token = null; $this->current_expires_at = null; return true; } /** * Handle the authentication process when the user is redirected back from Google. */ public function handle_authentication() { if ( ! isset( $_GET['code'] ) ) { return false; } // Other modules (e.g. Search Console) use the same redirect URL and disambiguate // via the OAuth `state` parameter. Only handle callbacks intended for GA. if ( !empty( $_GET['state'] ) && $_GET['state'] !== 'mwseo_ga' ) { return false; } $code = sanitize_text_field( $_GET['code'] ); if ( empty( $code ) ) { return false; } if ( $this->get_access_token( $code ) ) { return true; } else { $this->core->log( "⚠️ Failed to authenticate with Google Analytics." ); return false; } } public function get_auth_url() { $params = array( 'response_type' => 'code', 'client_id' => $this->client_id, 'redirect_uri' => $this->get_redirect_url(), 'scope' => self::SCOPE_URL, 'access_type' => 'offline', 'prompt' => 'consent', 'state' => 'mwseo_ga' ); return self::AUTH_URL . '?' . http_build_query( $params ); } private function get_access_token( $code ) { if( $this->is_authenticated() ) { return true; } $options = array( 'body' => array( 'code' => $code, 'client_id' => $this->client_id, 'client_secret' => $this->client_secret, 'redirect_uri' => $this->get_redirect_url(), 'grant_type' => 'authorization_code' ) ); $result = wp_remote_post( self::TOKEN_URL, $options ); if ( is_wp_error( $result ) ) { $this->core->log( "⚠️ Error retrieving access token: " . $result->get_error_message() ); return false; } $json = json_decode( $result['body'] ); if ( isset( $json->error ) ) { $this->core->log( "⚠️ Error retrieving access token: " . $json->error ); return false; } $expires_in = isset( $json->expires_in ) ? $json->expires_in : 3600; $access_token = isset( $json->access_token ) ? $json->access_token : null; $refresh_token = isset( $json->refresh_token ) ? $json->refresh_token : null; $token_info = array( 'expires_in' => $expires_in, 'expires_at' => time() + $expires_in, 'access_token' => $access_token, 'refresh_token' => $refresh_token ); $this->set_token_info( $token_info ); // Update instance variables so current request can use the new token $this->current_access_token = $access_token; $this->current_refresh_token = $refresh_token; $this->current_expires_at = time() + $expires_in; return true; } public function get_refresh_token() { $options = array( 'body' => array( 'client_id' => $this->client_id, 'client_secret' => $this->client_secret, 'refresh_token' => $this->current_refresh_token, 'grant_type' => 'refresh_token' ) ); $result = wp_remote_post( self::TOKEN_URL, $options ); if ( is_wp_error( $result ) ) { $this->core->log( "⚠️ Error refreshing access token: " . $result->get_error_message() ); return false; } $json = json_decode( $result['body'] ); if ( isset( $json->error ) ) { $this->core->log( "⚠️ Error refreshing access token: " . $json->error ); return false; } $expires_in = isset( $json->expires_in ) ? $json->expires_in : 3600; $access_token = isset( $json->access_token ) ? $json->access_token : null; $token_info = array( 'expires_in' => $expires_in, 'expires_at' => time() + $expires_in, 'access_token' => $access_token, 'refresh_token' => $this->current_refresh_token ); $this->set_token_info( $token_info ); // Update instance variables so current request uses the new token $this->current_access_token = $access_token; $this->current_expires_at = time() + $expires_in; return true; } #endregion #region Tracking public function tracking() { if ( is_admin() ) { return; } $this->disabled_tracking = $this->core->get_option( 'google_analytics_tracking_disabled', false ); $this->measurement_ids = $this->core->get_option( 'google_analytics_tracking_ids', array() ); if ( $this->disabled_tracking || empty( $this->measurement_ids ) ) { return; } $this->main_measurement_id = $this->measurement_ids[0]; $this->other_measurement_ids = array_slice( $this->measurement_ids, 1 ); add_action( 'wp_enqueue_scripts', array( $this, 'wp_enqueue_tracking_scripts' ) ); } public function wp_enqueue_tracking_scripts() { // Don't track logged-in users $is_logged_in = is_user_logged_in(); if ( $is_logged_in ) { $track_logged_users = $this->core->get_option( 'google_analytics_track_logged_users', false ); if ( !$track_logged_users ) { return; } // Don't track editors and admins $is_power_user = current_user_can( 'editor' ) || current_user_can( 'administrator' ); if ( $is_power_user ) { $track_power_users = $this->core->get_option( 'google_analytics_track_power_users', false ); if ( !$track_power_users ) { return; } } } add_filter( 'script_loader_tag', array( $this, 'script_loader_tag' ), 10, 2 ); // Enqueue the Google Analytics script wp_register_script( 'mwseo-analytics-ga', 'https://www.googletagmanager.com/gtag/js?id=' . $this->main_measurement_id, array(), null, true ); wp_enqueue_script( 'mwseo-analytics-ga' ); // Add the inline script with the tracking code wp_add_inline_script( 'mwseo-analytics-ga', $this->build_js(), 'after' ); } function build_js( ) { $main_measurement_id = $this->main_measurement_id; $other_measurement_ids = $this->other_measurement_ids; $extra_ids = json_encode( $other_measurement_ids ); return " var extra_ids = {$extra_ids}; window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date()); gtag('config', '{$main_measurement_id}'); for (var i = 0; i < extra_ids.length; i++) { gtag('config', extra_ids[i]); } "; } function script_loader_tag($tag, $handle) { if ($handle === 'mwseo-analytics-ga') { $tag = str_replace('