8889841cTag_Manager/Web_Tag.php000064400000006037150515556660010756 0ustar00get_method_proxy_once( 'render_no_js' ); add_action( 'wp_head', $this->get_method_proxy( 'render' ) ); // For non-AMP (if `wp_body_open` supported). add_action( 'wp_body_open', $render_no_js, -9999 ); // For non-AMP (as fallback). add_action( 'wp_footer', $render_no_js ); add_filter( 'wp_resource_hints', $this->get_dns_prefetch_hints_callback( '//www.googletagmanager.com' ), 10, 2 ); $this->do_init_tag_action(); } /** * Outputs Tag Manager script. * * @since 1.24.0 */ protected function render() { $tag_manager_inline_script = sprintf( " ( function( w, d, s, l, i ) { w[l] = w[l] || []; w[l].push( {'gtm.start': new Date().getTime(), event: 'gtm.js'} ); var f = d.getElementsByTagName( s )[0], j = d.createElement( s ), dl = l != 'dataLayer' ? '&l=' + l : ''; j.async = true; j.src = 'https://www.googletagmanager.com/gtm.js?id=' + i + dl; f.parentNode.insertBefore( j, f ); } )( window, document, 'script', 'dataLayer', '%s' ); ", esc_js( $this->tag_id ) ); $tag_manager_consent_attribute = $this->get_tag_blocked_on_consent_attribute_array(); printf( "\n\n", esc_html__( 'Google Tag Manager snippet added by Site Kit', 'google-site-kit' ) ); BC_Functions::wp_print_inline_script_tag( $tag_manager_inline_script, $tag_manager_consent_attribute ); printf( "\n\n", esc_html__( 'End Google Tag Manager snippet added by Site Kit', 'google-site-kit' ) ); } /** * Outputs Tag Manager iframe for when the browser has JavaScript disabled. * * @since 1.24.0 */ private function render_no_js() { // Consent-based blocking requires JS to be enabled so we need to bail here if present. if ( $this->get_tag_blocked_on_consent_attribute() ) { return; } $iframe_src = 'https://www.googletagmanager.com/ns.html?id=' . rawurlencode( $this->tag_id ); ?> register_legacy_keys_migration( array( 'account_id' => 'accountID', 'accountId' => 'accountID', 'container_id' => 'containerID', 'containerId' => 'containerID', ) ); $this->register_owned_keys(); } /** * Returns keys for owned settings. * * @since 1.16.0 * * @return array An array of keys for owned settings. */ public function get_owned_keys() { return array( 'accountID', 'ampContainerID', 'containerID', 'internalAMPContainerID', 'internalContainerID', ); } /** * Gets the default value. * * @since 1.2.0 * * @return array */ protected function get_default() { return array( 'ownerID' => 0, 'accountID' => '', 'ampContainerID' => '', 'containerID' => '', 'internalContainerID' => '', 'internalAMPContainerID' => '', 'useSnippet' => true, 'gaPropertyID' => '', ); } /** * Gets the callback for sanitizing the setting's value before saving. * * @since 1.6.0 * * @return callable|null */ protected function get_sanitize_callback() { return function( $option ) { if ( is_array( $option ) ) { if ( isset( $option['useSnippet'] ) ) { $option['useSnippet'] = (bool) $option['useSnippet']; } } return $option; }; } } Tag_Manager/AMP_Tag.php000064400000005004150515556660010647 0ustar00get_method_proxy_once( 'render' ); // Which actions are run depends on the version of the AMP Plugin // (https://amp-wp.org/) available. Version >=1.3 exposes a // new, `amp_print_analytics` action. // For all AMP modes, AMP plugin version >=1.3. add_action( 'amp_print_analytics', $render ); // For AMP Standard and Transitional, AMP plugin version <1.3. add_action( 'wp_footer', $render, 20 ); // For AMP Reader, AMP plugin version <1.3. add_action( 'amp_post_template_footer', $render, 20 ); // For Web Stories plugin. add_action( 'web_stories_print_analytics', $render ); // Load amp-analytics component for AMP Reader. $this->enqueue_amp_reader_component_script( 'amp-analytics', 'https://cdn.ampproject.org/v0/amp-analytics-0.1.js' ); $this->do_init_tag_action(); } /** * Outputs Tag Manager tag. * * @since 1.24.0 */ protected function render() { // Add the optoutElementId for compatibility with our Analytics opt-out mechanism. // This configuration object will be merged with the configuration object returned // by the `config` attribute URL. $gtm_amp_opt = array( 'optoutElementId' => '__gaOptOutExtension', ); printf( "\n\n", esc_html__( 'Google Tag Manager AMP snippet added by Site Kit', 'google-site-kit' ) ); printf( '', esc_url( 'https://www.googletagmanager.com/amp.json?id=' . rawurlencode( $this->tag_id ) ), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped $this->get_tag_blocked_on_consent_attribute(), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped wp_json_encode( $gtm_amp_opt ) ); printf( "\n\n", esc_html__( 'End Google Tag Manager AMP snippet added by Site Kit', 'google-site-kit' ) ); } } Tag_Manager/Tag_Guard.php000064400000002560150515556660011300 0ustar00is_amp = $is_amp; } /** * Determines whether the guarded tag can be activated or not. * * @since 1.24.0 * * @return bool|WP_Error TRUE if guarded tag can be activated, otherwise FALSE or an error. */ public function can_activate() { $settings = $this->settings->get(); $container_id = $this->is_amp ? $settings['ampContainerID'] : $settings['containerID']; return ! empty( $settings['useSnippet'] ) && ! empty( $container_id ); } } AdSense.php000064400000072716150515556660006632 0ustar00register_scopes_hook(); add_action( 'wp_head', $this->get_method_proxy_once( 'render_platform_meta_tags' ) ); if ( $this->is_connected() ) { /** * Release filter forcing unlinked state. * * This is hooked into 'init' (default priority of 10), so that it * runs after the original filter is added. * * @see \Google\Site_Kit\Modules\Analytics::register() * @see \Google\Site_Kit\Modules\Analytics\Settings::register() */ add_action( 'googlesitekit_init', function () { remove_filter( 'googlesitekit_analytics_adsense_linked', '__return_false' ); } ); } // AdSense tag placement logic. add_action( 'template_redirect', $this->get_method_proxy( 'register_tag' ) ); } /** * Gets required Google OAuth scopes for the module. * * @since 1.0.0 * @since 1.9.0 Changed to `adsense.readonly` variant. * * @return array List of Google OAuth scopes. */ public function get_scopes() { return array( 'https://www.googleapis.com/auth/adsense.readonly', ); } /** * Checks whether the module is connected. * * A module being connected means that all steps required as part of its activation are completed. * * @since 1.0.0 * * @return bool True if module is connected, false otherwise. */ public function is_connected() { $settings = $this->get_settings()->get(); if ( empty( $settings['accountSetupComplete'] ) || empty( $settings['siteSetupComplete'] ) ) { return false; } return parent::is_connected(); } /** * Cleans up when the module is deactivated. * * @since 1.0.0 */ public function on_deactivation() { $this->get_settings()->delete(); } /** * Gets an array of debug field definitions. * * @since 1.5.0 * * @return array */ public function get_debug_fields() { $settings = $this->get_settings()->get(); return array( 'adsense_account_id' => array( 'label' => __( 'AdSense account ID', 'google-site-kit' ), 'value' => $settings['accountID'], 'debug' => Debug_Data::redact_debug_value( $settings['accountID'], 7 ), ), 'adsense_client_id' => array( 'label' => __( 'AdSense client ID', 'google-site-kit' ), 'value' => $settings['clientID'], 'debug' => Debug_Data::redact_debug_value( $settings['clientID'], 10 ), ), 'adsense_account_status' => array( 'label' => __( 'AdSense account status', 'google-site-kit' ), 'value' => $settings['accountStatus'], ), 'adsense_site_status' => array( 'label' => __( 'AdSense site status', 'google-site-kit' ), 'value' => $settings['siteStatus'], ), 'adsense_use_snippet' => array( 'label' => __( 'AdSense snippet placed', 'google-site-kit' ), 'value' => $settings['useSnippet'] ? __( 'Yes', 'google-site-kit' ) : __( 'No', 'google-site-kit' ), 'debug' => $settings['useSnippet'] ? 'yes' : 'no', ), 'adsense_web_stories_adunit_id' => array( 'label' => __( 'Web Stories Ad Unit ID', 'google-site-kit' ), 'value' => $settings['webStoriesAdUnit'], 'debug' => $settings['webStoriesAdUnit'], ), ); } /** * Gets map of datapoint to definition data for each. * * @since 1.12.0 * * @return array Map of datapoints to their definitions. */ protected function get_datapoint_definitions() { return array( 'GET:adunits' => array( 'service' => 'adsense' ), 'GET:accounts' => array( 'service' => 'adsense' ), 'GET:alerts' => array( 'service' => 'adsense' ), 'GET:clients' => array( 'service' => 'adsense' ), 'GET:report' => array( 'service' => 'adsense', 'shareable' => Feature_Flags::enabled( 'dashboardSharing' ), ), 'GET:notifications' => array( 'service' => '' ), 'GET:urlchannels' => array( 'service' => 'adsense' ), 'GET:sites' => array( 'service' => 'adsense' ), ); } /** * Creates a request object for the given datapoint. * * @since 1.0.0 * * @param Data_Request $data Data request object. * @return RequestInterface|callable|WP_Error Request object or callable on success, or WP_Error on failure. * * @throws Invalid_Datapoint_Exception Thrown if the datapoint does not exist. */ protected function create_data_request( Data_Request $data ) { switch ( "{$data->method}:{$data->datapoint}" ) { case 'GET:accounts': $service = $this->get_service( 'adsense' ); return $service->accounts->listAccounts(); case 'GET:adunits': if ( ! isset( $data['accountID'] ) || ! isset( $data['clientID'] ) ) { $option = $this->get_settings()->get(); $data['accountID'] = $option['accountID']; if ( empty( $data['accountID'] ) ) { /* translators: %s: Missing parameter name */ return new WP_Error( 'missing_required_param', sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'accountID' ), array( 'status' => 400 ) ); } $data['clientID'] = $option['clientID']; if ( empty( $data['clientID'] ) ) { /* translators: %s: Missing parameter name */ return new WP_Error( 'missing_required_param', sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'clientID' ), array( 'status' => 400 ) ); } } $service = $this->get_service( 'adsense' ); return $service->accounts_adclients_adunits->listAccountsAdclientsAdunits( self::normalize_client_id( $data['accountID'], $data['clientID'] ) ); case 'GET:alerts': if ( ! isset( $data['accountID'] ) ) { $option = $this->get_settings()->get(); $data['accountID'] = $option['accountID']; if ( empty( $data['accountID'] ) ) { /* translators: %s: Missing parameter name */ return new WP_Error( 'missing_required_param', sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'accountID' ), array( 'status' => 400 ) ); } } $service = $this->get_service( 'adsense' ); return $service->accounts_alerts->listAccountsAlerts( self::normalize_account_id( $data['accountID'] ) ); case 'GET:clients': if ( ! isset( $data['accountID'] ) ) { return new WP_Error( 'missing_required_param', /* translators: %s: Missing parameter name */ sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'accountID' ), array( 'status' => 400 ) ); } $service = $this->get_service( 'adsense' ); return $service->accounts_adclients->listAccountsAdclients( self::normalize_account_id( $data['accountID'] ) ); case 'GET:report': $start_date = $data['startDate']; $end_date = $data['endDate']; if ( ! strtotime( $start_date ) || ! strtotime( $end_date ) ) { $dates = $this->date_range_to_dates( $data['dateRange'] ?: 'last-28-days' ); if ( is_wp_error( $dates ) ) { return $dates; } list ( $start_date, $end_date ) = $dates; } $args = array( 'start_date' => $start_date, 'end_date' => $end_date, ); $metrics = $this->parse_string_list( $data['metrics'] ); if ( ! empty( $metrics ) ) { try { $this->validate_report_metrics( $metrics ); } catch ( Invalid_Report_Metrics_Exception $exception ) { return new WP_Error( 'invalid_adsense_report_metrics', $exception->getMessage() ); } $args['metrics'] = $metrics; } $dimensions = $this->parse_string_list( $data['dimensions'] ); if ( ! empty( $dimensions ) ) { try { $this->validate_report_dimensions( $dimensions ); } catch ( Invalid_Report_Dimensions_Exception $exception ) { return new WP_Error( 'invalid_adsense_report_dimensions', $exception->getMessage() ); } $args['dimensions'] = $dimensions; } $orderby = $this->parse_earnings_orderby( $data['orderby'] ); if ( ! empty( $orderby ) ) { $args['sort'] = $orderby; } if ( ! empty( $data['limit'] ) ) { $args['limit'] = $data['limit']; } return $this->create_adsense_earning_data_request( array_filter( $args ) ); case 'GET:notifications': return function() { $alerts = $this->get_data( 'alerts' ); if ( is_wp_error( $alerts ) || empty( $alerts ) ) { return array(); } $alerts = array_filter( $alerts, function( Google_Service_Adsense_Alert $alert ) { return 'SEVERE' === $alert->getSeverity(); } ); // There is no SEVERE alert, return empty. if ( empty( $alerts ) ) { return array(); } /** * First Alert * * @var Google_Service_Adsense_Alert $alert */ $alert = array_shift( $alerts ); return array( array( 'id' => 'adsense-notification', 'description' => $alert->getMessage(), 'isDismissible' => true, 'format' => 'large', 'severity' => 'win-info', 'ctaURL' => $this->get_account_url(), 'ctaLabel' => __( 'Go to AdSense', 'google-site-kit' ), 'ctaTarget' => '_blank', ), ); }; case 'GET:sites': if ( ! isset( $data['accountID'] ) ) { return new WP_Error( 'missing_required_param', /* translators: %s: Missing parameter name */ sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'accountID' ), array( 'status' => 400 ) ); } $service = $this->get_service( 'adsense' ); return $service->accounts_sites->listAccountsSites( self::normalize_account_id( $data['accountID'] ) ); case 'GET:urlchannels': if ( ! isset( $data['accountID'] ) ) { return new WP_Error( 'missing_required_param', /* translators: %s: Missing parameter name */ sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'accountID' ), array( 'status' => 400 ) ); } if ( ! isset( $data['clientID'] ) ) { return new WP_Error( 'missing_required_param', /* translators: %s: Missing parameter name */ sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'clientID' ), array( 'status' => 400 ) ); } $service = $this->get_service( 'adsense' ); return $service->accounts_adclients_urlchannels->listAccountsAdclientsUrlchannels( self::normalize_client_id( $data['accountID'], $data['clientID'] ) ); } return parent::create_data_request( $data ); } /** * Parses a response for the given datapoint. * * @since 1.0.0 * * @param Data_Request $data Data request object. * @param mixed $response Request response. * * @return mixed Parsed response data on success, or WP_Error on failure. */ protected function parse_data_response( Data_Request $data, $response ) { switch ( "{$data->method}:{$data->datapoint}" ) { case 'GET:accounts': $accounts = array_filter( $response->getAccounts(), array( self::class, 'is_account_not_closed' ) ); return Sort::case_insensitive_list_sort( array_map( array( self::class, 'filter_account_with_ids' ), $accounts ), 'displayName' ); case 'GET:adunits': return array_map( array( self::class, 'filter_adunit_with_ids' ), $response->getAdUnits() ); case 'GET:alerts': return $response->getAlerts(); case 'GET:clients': return array_map( array( self::class, 'filter_client_with_ids' ), $response->getAdClients() ); case 'GET:urlchannels': return $response->getUrlChannels(); case 'GET:report': return $response; case 'GET:sites': return $response->getSites(); } return parent::parse_data_response( $data, $response ); } /** * Checks for the state of an Account, whether closed or not. * * @since 1.73.0 * * @param Google_Model $account Account model. * @return bool Whether the account is not closed. */ public static function is_account_not_closed( $account ) { return 'CLOSED' !== $account->getState(); } /** * Gets the service URL for the current account or signup if none. * * @since 1.25.0 * * @return string */ protected function get_account_url() { $profile = $this->authentication->profile(); $option = $this->get_settings()->get(); $query = array( 'source' => 'site-kit', 'utm_source' => 'site-kit', 'utm_medium' => 'wordpress_signup', 'url' => rawurlencode( $this->context->get_reference_site_url() ), ); if ( ! empty( $option['accountID'] ) ) { $url = sprintf( 'https://www.google.com/adsense/new/%s/home', $option['accountID'] ); } else { $url = 'https://www.google.com/adsense/signup'; } if ( $profile->has() ) { $query['authuser'] = $profile->get()['email']; } return add_query_arg( $query, $url ); } /** * Parses the orderby value of the data request into an array of earning orderby format. * * @since 1.15.0 * * @param array|null $orderby Data request orderby value. * @return string[] An array of reporting orderby strings. */ protected function parse_earnings_orderby( $orderby ) { if ( empty( $orderby ) || ! is_array( $orderby ) ) { return array(); } $results = array_map( function ( $order_def ) { $order_def = array_merge( array( 'fieldName' => '', 'sortOrder' => '', ), (array) $order_def ); if ( empty( $order_def['fieldName'] ) || empty( $order_def['sortOrder'] ) ) { return null; } return ( 'ASCENDING' === $order_def['sortOrder'] ? '+' : '-' ) . $order_def['fieldName']; }, // When just object is passed we need to convert it to an array of objects. wp_is_numeric_array( $orderby ) ? $orderby : array( $orderby ) ); $results = array_filter( $results ); $results = array_values( $results ); return $results; } /** * Gets an array of dates for the given named date range. * * @param string $date_range Named date range. * E.g. 'last-28-days'. * * @return array|WP_Error Array of [startDate, endDate] or WP_Error if invalid named range. */ private function date_range_to_dates( $date_range ) { switch ( $date_range ) { case 'today': return array( gmdate( 'Y-m-d', strtotime( 'today' ) ), gmdate( 'Y-m-d', strtotime( 'today' ) ), ); // Intentional fallthrough. case 'last-7-days': case 'last-14-days': case 'last-28-days': case 'last-90-days': return $this->parse_date_range( $date_range ); } return new WP_Error( 'invalid_date_range', __( 'Invalid date range.', 'google-site-kit' ) ); } /** * Creates a new AdSense earning request for the current account, site and given arguments. * * @since 1.0.0 * * @param array $args { * Optional. Additional arguments. * * @type array $dimensions List of request dimensions. Default empty array. * @type array $metrics List of request metrics. Default empty array. * @type string $start_date Start date in 'Y-m-d' format. Default empty string. * @type string $end_date End date in 'Y-m-d' format. Default empty string. * @type int $row_limit Limit of rows to return. Default none (will be skipped). * } * @return RequestInterface|WP_Error AdSense earning request instance. */ protected function create_adsense_earning_data_request( array $args = array() ) { $args = wp_parse_args( $args, array( 'dimensions' => array(), 'metrics' => array(), 'start_date' => '', 'end_date' => '', 'limit' => '', 'sort' => array(), ) ); $option = $this->get_settings()->get(); $account_id = $option['accountID']; if ( empty( $account_id ) ) { return new WP_Error( 'account_id_not_set', __( 'AdSense account ID not set.', 'google-site-kit' ) ); } list( $start_year, $start_month, $start_day ) = explode( '-', $args['start_date'] ); list( $end_year, $end_month, $end_day ) = explode( '-', $args['end_date'] ); $opt_params = array( // In the AdSense API v2, date parameters require the individual pieces to be specified as integers. // See https://developers.google.com/adsense/management/reference/rest/v2/accounts.reports/generate. 'dateRange' => 'CUSTOM', 'startDate.year' => (int) $start_year, 'startDate.month' => (int) $start_month, 'startDate.day' => (int) $start_day, 'endDate.year' => (int) $end_year, 'endDate.month' => (int) $end_month, 'endDate.day' => (int) $end_day, 'languageCode' => $this->context->get_locale( 'site', 'language-code' ), // Include default metrics only for backward-compatibility. 'metrics' => array( 'ESTIMATED_EARNINGS', 'PAGE_VIEWS_RPM', 'IMPRESSIONS' ), ); if ( ! empty( $args['dimensions'] ) ) { $opt_params['dimensions'] = (array) $args['dimensions']; } if ( ! empty( $args['metrics'] ) ) { $opt_params['metrics'] = (array) $args['metrics']; } if ( ! empty( $args['sort'] ) ) { $opt_params['orderBy'] = (array) $args['sort']; } if ( ! empty( $args['limit'] ) ) { $opt_params['limit'] = (int) $args['limit']; } // @see https://developers.google.com/adsense/management/reporting/filtering?hl=en#OR $site_hostname = URL::parse( $this->context->get_reference_site_url(), PHP_URL_HOST ); $opt_params['filters'] = join( ',', array_map( function ( $hostname ) { return 'DOMAIN_NAME==' . $hostname; }, $this->permute_site_hosts( $site_hostname ) ) ); return $this->get_service( 'adsense' ) ->accounts_reports ->generate( self::normalize_account_id( $account_id ), $opt_params ); } /** * Sets up information about the module. * * @since 1.0.0 * * @return array Associative array of module info. */ protected function setup_info() { $idenfifier_args = array( 'source' => 'site-kit', 'url' => $this->context->get_reference_site_url(), ); return array( 'slug' => self::MODULE_SLUG, 'name' => _x( 'AdSense', 'Service name', 'google-site-kit' ), 'description' => __( 'Earn money by placing ads on your website. It’s free and easy.', 'google-site-kit' ), 'order' => 2, 'homepage' => add_query_arg( $idenfifier_args, 'https://www.google.com/adsense/start' ), ); } /** * Sets up the Google services the module should use. * * This method is invoked once by {@see Module::get_service()} to lazily set up the services when one is requested * for the first time. * * @since 1.0.0 * @since 1.2.0 Now requires Google_Site_Kit_Client instance. * * @param Google_Site_Kit_Client $client Google client instance. * @return array Google services as $identifier => $service_instance pairs. Every $service_instance must be an * instance of Google_Service. */ protected function setup_services( Google_Site_Kit_Client $client ) { return array( 'adsense' => new Google_Service_Adsense( $client ), ); } /** * Sets up the module's settings instance. * * @since 1.2.0 * * @return Module_Settings */ protected function setup_settings() { return new Settings( $this->options ); } /** * Sets up the module's assets to register. * * @since 1.9.0 * * @return Asset[] List of Asset objects. */ protected function setup_assets() { $base_url = $this->context->url( 'dist/assets/' ); return array( new Script( 'googlesitekit-modules-adsense', array( 'src' => $base_url . 'js/googlesitekit-modules-adsense.js', 'dependencies' => array( 'googlesitekit-vendor', 'googlesitekit-api', 'googlesitekit-data', 'googlesitekit-modules', 'googlesitekit-datastore-site', 'googlesitekit-datastore-user', 'googlesitekit-components', ), ) ), ); } /** * Registers the AdSense tag. * * @since 1.24.0 */ private function register_tag() { // TODO: 'amp_story' support can be phased out in the long term. if ( is_singular( array( 'amp_story' ) ) ) { return; } $module_settings = $this->get_settings(); $settings = $module_settings->get(); if ( $this->context->is_amp() ) { $tag = new AMP_Tag( $settings['clientID'], self::MODULE_SLUG ); $tag->set_story_ad_slot_id( $settings['webStoriesAdUnit'] ); } else { $tag = new Web_Tag( $settings['clientID'], self::MODULE_SLUG ); } if ( ! $tag->is_tag_blocked() ) { $tag->use_guard( new Tag_Verify_Guard( $this->context->input() ) ); $tag->use_guard( new Tag_Guard( $module_settings ) ); $tag->use_guard( new Auto_Ad_Guard( $module_settings ) ); $tag->use_guard( new Tag_Environment_Type_Guard() ); if ( $tag->can_register() ) { $tag->register(); } } } /** * Parses account ID, adds it to the model object and returns updated model. * * @since 1.36.0 * * @param Google_Model $account Account model. * @param string $id_key Attribute name that contains account ID. * @return \stdClass Updated model with _id attribute. */ public static function filter_account_with_ids( $account, $id_key = 'name' ) { $obj = $account->toSimpleObject(); $matches = array(); if ( preg_match( '#accounts/([^/]+)#', $account[ $id_key ], $matches ) ) { $obj->_id = $matches[1]; } return $obj; } /** * Parses account and client IDs, adds it to the model object and returns updated model. * * @since 1.36.0 * * @param Google_Model $client Client model. * @param string $id_key Attribute name that contains client ID. * @return \stdClass Updated model with _id and _accountID attributes. */ public static function filter_client_with_ids( $client, $id_key = 'name' ) { $obj = $client->toSimpleObject(); $matches = array(); if ( preg_match( '#accounts/([^/]+)/adclients/([^/]+)#', $client[ $id_key ], $matches ) ) { $obj->_id = $matches[2]; $obj->_accountID = $matches[1]; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase } return $obj; } /** * Parses account, client and ad unit IDs, adds it to the model object and returns updated model. * * @since 1.36.0 * * @param Google_Model $adunit Ad unit model. * @param string $id_key Attribute name that contains ad unit ID. * @return \stdClass Updated model with _id, _clientID and _accountID attributes. */ public static function filter_adunit_with_ids( $adunit, $id_key = 'name' ) { $obj = $adunit->toSimpleObject(); $matches = array(); if ( preg_match( '#accounts/([^/]+)/adclients/([^/]+)/adunits/([^/]+)#', $adunit[ $id_key ], $matches ) ) { $obj->_id = $matches[3]; $obj->_clientID = $matches[2]; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase $obj->_accountID = $matches[1]; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase } return $obj; } /** * Normalizes account ID and returns it. * * @since 1.36.0 * * @param string $account_id Account ID. * @return string Updated account ID with "accounts/" prefix. */ public static function normalize_account_id( $account_id ) { return 'accounts/' . $account_id; } /** * Normalizes ad client ID and returns it. * * @since 1.36.0 * * @param string $account_id Account ID. * @param string $client_id Ad client ID. * @return string Account ID and ad client ID in "accounts/{accountID}/adclients/{clientID}" format. */ public static function normalize_client_id( $account_id, $client_id ) { return 'accounts/' . $account_id . '/adclients/' . $client_id; } /** * Outputs the Adsense for Platforms meta tags. * * @since 1.43.0 */ private function render_platform_meta_tags() { printf( "\n\n", esc_html__( 'Google AdSense snippet added by Site Kit', 'google-site-kit' ) ); echo ''; echo "\n"; echo ''; printf( "\n\n", esc_html__( 'End Google AdSense snippet added by Site Kit', 'google-site-kit' ) ); } /** * Checks if the current user has access to the current configured service entity. * * @since 1.70.0 * * @return boolean|WP_Error */ public function check_service_entity_access() { $data_request = array( 'start_date' => gmdate( 'Y-m-d' ), 'end_date' => gmdate( 'Y-m-d' ), 'limit' => 1, ); try { $request = $this->create_adsense_earning_data_request( $data_request ); if ( is_wp_error( $request ) ) { return $request; } } catch ( Exception $e ) { if ( $e->getCode() === 403 ) { return false; } return $this->exception_to_error( $e ); } return true; } /** * Validates the report metrics. * * @since 1.83.0 * * @param string[] $metrics The metrics to validate. * @throws Invalid_Report_Metrics_Exception Thrown if the metrics are invalid. */ protected function validate_report_metrics( $metrics ) { if ( false === $this->is_using_shared_credentials ) { return; } $valid_metrics = apply_filters( 'googlesitekit_shareable_adsense_metrics', array( 'ESTIMATED_EARNINGS', 'IMPRESSIONS', 'PAGE_VIEWS_CTR', 'PAGE_VIEWS_RPM', ) ); $invalid_metrics = array_diff( $metrics, $valid_metrics ); if ( count( $invalid_metrics ) > 0 ) { $message = count( $invalid_metrics ) > 1 ? sprintf( /* translators: %s: is replaced with a comma separated list of the invalid metrics. */ __( 'Unsupported metrics requested: %s', 'google-site-kit' ), join( /* translators: used between list items, there is a space after the comma. */ __( ', ', 'google-site-kit' ), $invalid_metrics ) ) : sprintf( /* translators: %s: is replaced with the invalid metric. */ __( 'Unsupported metric requested: %s', 'google-site-kit' ), $invalid_metrics ); throw new Invalid_Report_Metrics_Exception( $message ); } } /** * Validates the report dimensions. * * @since 1.83.0 * * @param string[] $dimensions The dimensions to validate. * @throws Invalid_Report_Dimensions_Exception Thrown if the dimensions are invalid. */ protected function validate_report_dimensions( $dimensions ) { if ( false === $this->is_using_shared_credentials ) { return; } $valid_dimensions = apply_filters( 'googlesitekit_shareable_adsense_dimensions', array( 'DATE', ) ); $invalid_dimensions = array_diff( $dimensions, $valid_dimensions ); if ( count( $invalid_dimensions ) > 0 ) { $message = count( $invalid_dimensions ) > 1 ? sprintf( /* translators: %s: is replaced with a comma separated list of the invalid dimensions. */ __( 'Unsupported dimensions requested: %s', 'google-site-kit' ), join( /* translators: used between list items, there is a space after the comma. */ __( ', ', 'google-site-kit' ), $invalid_dimensions ) ) : sprintf( /* translators: %s: is replaced with the invalid dimension. */ __( 'Unsupported dimension requested: %s', 'google-site-kit' ), $invalid_dimensions ); throw new Invalid_Report_Dimensions_Exception( $message ); } } } PageSpeed_Insights.php000064400000014435150515556660011007 0ustar00get_settings()->delete(); } /** * Gets map of datapoint to definition data for each. * * @since 1.12.0 * * @return array Map of datapoints to their definitions. */ protected function get_datapoint_definitions() { return array( 'GET:pagespeed' => array( 'service' => 'pagespeedonline', 'shareable' => Feature_Flags::enabled( 'dashboardSharing' ), ), ); } /** * Creates a request object for the given datapoint. * * @since 1.0.0 * * @param Data_Request $data Data request object. * @return RequestInterface|callable|WP_Error Request object or callable on success, or WP_Error on failure. * * @throws Invalid_Datapoint_Exception Thrown if the datapoint does not exist. */ protected function create_data_request( Data_Request $data ) { switch ( "{$data->method}:{$data->datapoint}" ) { case 'GET:pagespeed': if ( empty( $data['strategy'] ) ) { return new WP_Error( 'missing_required_param', sprintf( /* translators: %s: Missing parameter name */ __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'strategy' ), array( 'status' => 400 ) ); } $valid_strategies = array( 'mobile', 'desktop' ); if ( ! in_array( $data['strategy'], $valid_strategies, true ) ) { return new WP_Error( 'invalid_param', sprintf( /* translators: 1: Invalid parameter name, 2: list of valid values */ __( 'Request parameter %1$s is not one of %2$s', 'google-site-kit' ), 'strategy', implode( ', ', $valid_strategies ) ), array( 'status' => 400 ) ); } if ( ! empty( $data['url'] ) ) { $page_url = $data['url']; } else { $page_url = $this->context->get_reference_site_url(); } $service = $this->get_service( 'pagespeedonline' ); return $service->pagespeedapi->runpagespeed( $page_url, array( 'locale' => $this->context->get_locale( 'site', 'language-code' ), 'strategy' => $data['strategy'], ) ); } return parent::create_data_request( $data ); } /** * Sets up the module's assets to register. * * @since 1.9.0 * * @return Asset[] List of Asset objects. */ protected function setup_assets() { $base_url = $this->context->url( 'dist/assets/' ); return array( new Script( 'googlesitekit-modules-pagespeed-insights', array( 'src' => $base_url . 'js/googlesitekit-modules-pagespeed-insights.js', 'dependencies' => array( 'googlesitekit-vendor', 'googlesitekit-api', 'googlesitekit-data', 'googlesitekit-modules', 'googlesitekit-datastore-site', 'googlesitekit-components', ), ) ), ); } /** * Sets up information about the module. * * @since 1.0.0 * * @return array Associative array of module info. */ protected function setup_info() { return array( 'slug' => 'pagespeed-insights', 'name' => _x( 'PageSpeed Insights', 'Service name', 'google-site-kit' ), 'description' => __( 'Google PageSpeed Insights gives you metrics about performance, accessibility, SEO and PWA', 'google-site-kit' ), 'order' => 4, 'homepage' => __( 'https://pagespeed.web.dev', 'google-site-kit' ), ); } /** * Sets up the module's settings instance. * * @since 1.49.0 * * @return Module_Settings */ protected function setup_settings() { return new Settings( $this->options ); } /** * Sets up the Google services the module should use. * * This method is invoked once by {@see Module::get_service()} to lazily set up the services when one is requested * for the first time. * * @since 1.0.0 * @since 1.2.0 Now requires Google_Site_Kit_Client instance. * * @param Google_Site_Kit_Client $client Google client instance. * @return array Google services as $identifier => $service_instance pairs. Every $service_instance must be an * instance of Google_Service. */ protected function setup_services( Google_Site_Kit_Client $client ) { return array( 'pagespeedonline' => new Google_Service_PagespeedInsights( $client ), ); } /** * Gets required Google OAuth scopes for the module. * * @return array List of Google OAuth scopes. * @since 1.0.0 */ public function get_scopes() { return array( 'openid', ); } } Tag_Manager.php000064400000052066150515556660007451 0ustar00 'containerID', self::USAGE_CONTEXT_AMP => 'ampContainerID', ); /** * Registers functionality through WordPress hooks. * * @since 1.0.0 */ public function register() { $this->register_scopes_hook(); // Tag Manager tag placement logic. add_action( 'template_redirect', $this->get_method_proxy( 'register_tag' ) ); // Filter the Analytics `canUseSnippet` value. add_filter( 'googlesitekit_analytics_can_use_snippet', $this->get_method_proxy( 'can_analytics_use_snippet' ), 10, 2 ); // Filter whether certain users can be excluded from tracking. add_action( 'googlesitekit_allow_tracking_disabled', $this->get_method_proxy( 'filter_analytics_allow_tracking_disabled' ) ); add_action( 'googlesitekit_analytics_tracking_opt_out', $this->get_method_proxy( 'analytics_tracking_opt_out' ) ); } /** * Gets required Google OAuth scopes for the module. * * @since 1.0.0 * * @return array List of Google OAuth scopes. */ public function get_scopes() { return array( 'https://www.googleapis.com/auth/tagmanager.readonly', ); } /** * Checks whether the module is connected. * * A module being connected means that all steps required as part of its activation are completed. * * @since 1.0.0 * * @return bool True if module is connected, false otherwise. */ public function is_connected() { $settings = $this->get_settings()->get(); $amp_mode = $this->context->get_amp_mode(); switch ( $amp_mode ) { case Context::AMP_MODE_PRIMARY: $container_ids = array( $settings['ampContainerID'] ); break; case Context::AMP_MODE_SECONDARY: $container_ids = array( $settings['containerID'], $settings['ampContainerID'] ); break; default: $container_ids = array( $settings['containerID'] ); } $container_id_errors = array_filter( $container_ids, function( $container_id ) { return ! $container_id; } ); if ( ! empty( $container_id_errors ) ) { return false; } return parent::is_connected(); } /** * Cleans up when the module is deactivated. * * @since 1.0.0 */ public function on_deactivation() { $this->get_settings()->delete(); } /** * Gets an array of debug field definitions. * * @since 1.5.0 * * @return array */ public function get_debug_fields() { $settings = $this->get_settings()->get(); return array( 'tagmanager_account_id' => array( 'label' => __( 'Tag Manager account ID', 'google-site-kit' ), 'value' => $settings['accountID'], 'debug' => Debug_Data::redact_debug_value( $settings['accountID'] ), ), 'tagmanager_container_id' => array( 'label' => __( 'Tag Manager container ID', 'google-site-kit' ), 'value' => $settings['containerID'], 'debug' => Debug_Data::redact_debug_value( $settings['containerID'], 7 ), ), 'tagmanager_amp_container_id' => array( 'label' => __( 'Tag Manager AMP container ID', 'google-site-kit' ), 'value' => $settings['ampContainerID'], 'debug' => Debug_Data::redact_debug_value( $settings['ampContainerID'], 7 ), ), 'tagmanager_use_snippet' => array( 'label' => __( 'Tag Manager snippet placed', 'google-site-kit' ), 'value' => $settings['useSnippet'] ? __( 'Yes', 'google-site-kit' ) : __( 'No', 'google-site-kit' ), 'debug' => $settings['useSnippet'] ? 'yes' : 'no', ), ); } /** * Sanitizes a string to be used for a container name. * * @since 1.0.4 * * @param string $name String to sanitize. * * @return string */ public static function sanitize_container_name( $name ) { // Remove any leading or trailing whitespace. $name = trim( $name ); // Must not start with an underscore. $name = ltrim( $name, '_' ); // Decode entities for special characters so that they are stripped properly. $name = wp_specialchars_decode( $name, ENT_QUOTES ); // Convert accents to basic characters to prevent them from being stripped. $name = remove_accents( $name ); // Strip all non-simple characters. $name = preg_replace( '/[^a-zA-Z0-9_., -]/', '', $name ); // Collapse multiple whitespaces. $name = preg_replace( '/\s+/', ' ', $name ); return $name; } /** * Gets map of datapoint to definition data for each. * * @since 1.9.0 * * @return array Map of datapoints to their definitions. */ protected function get_datapoint_definitions() { return array( 'GET:accounts' => array( 'service' => 'tagmanager' ), 'GET:accounts-containers' => array( 'service' => 'tagmanager' ), 'GET:containers' => array( 'service' => 'tagmanager' ), 'POST:create-container' => array( 'service' => 'tagmanager', 'scopes' => array( 'https://www.googleapis.com/auth/tagmanager.edit.containers' ), 'request_scopes_message' => __( 'Additional permissions are required to create a new Tag Manager container on your behalf.', 'google-site-kit' ), ), 'GET:live-container-version' => array( 'service' => 'tagmanager' ), ); } /** * Creates a request object for the given datapoint. * * @since 1.0.0 * * @param Data_Request $data Data request object. * @return RequestInterface|callable|WP_Error Request object or callable on success, or WP_Error on failure. * * @throws Invalid_Datapoint_Exception Thrown if the datapoint does not exist. */ protected function create_data_request( Data_Request $data ) { switch ( "{$data->method}:{$data->datapoint}" ) { // Intentional fallthrough. case 'GET:accounts': case 'GET:accounts-containers': return $this->get_tagmanager_service()->accounts->listAccounts(); case 'GET:containers': if ( ! isset( $data['accountID'] ) ) { /* translators: %s: Missing parameter name */ return new WP_Error( 'missing_required_param', sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'accountID' ), array( 'status' => 400 ) ); } return $this->get_tagmanager_service()->accounts_containers->listAccountsContainers( "accounts/{$data['accountID']}" ); case 'POST:create-container': if ( ! isset( $data['accountID'] ) ) { return new WP_Error( 'missing_required_param', /* translators: %s: Missing parameter name */ sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'accountID' ), array( 'status' => 400 ) ); } $usage_context = $data['usageContext'] ?: array( self::USAGE_CONTEXT_WEB, self::USAGE_CONTEXT_AMP ); if ( empty( $this->context_map[ $usage_context ] ) ) { return new WP_Error( 'invalid_param', sprintf( /* translators: 1: Invalid parameter name, 2: list of valid values */ __( 'Request parameter %1$s is not one of %2$s', 'google-site-kit' ), 'usageContext', implode( ', ', array_keys( $this->context_map ) ) ), array( 'status' => 400 ) ); } $account_id = $data['accountID']; if ( $data['name'] ) { $container_name = $data['name']; } else { // Use site name for container, fallback to domain of reference URL. $container_name = get_bloginfo( 'name' ) ?: URL::parse( $this->context->get_reference_site_url(), PHP_URL_HOST ); // Prevent naming conflict (Tag Manager does not allow more than one with same name). if ( self::USAGE_CONTEXT_AMP === $usage_context ) { $container_name .= ' AMP'; } } $container = new Google_Service_TagManager_Container(); $container->setName( self::sanitize_container_name( $container_name ) ); $container->setUsageContext( (array) $usage_context ); return $this->get_tagmanager_service()->accounts_containers->create( "accounts/{$account_id}", $container ); case 'GET:live-container-version': if ( ! isset( $data['accountID'] ) ) { return new WP_Error( 'missing_required_param', /* translators: %s: Missing parameter name */ sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'accountID' ), array( 'status' => 400 ) ); } if ( ! isset( $data['internalContainerID'] ) ) { return new WP_Error( 'missing_required_param', /* translators: %s: Missing parameter name */ sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'internalContainerID' ), array( 'status' => 400 ) ); } return $this->get_tagmanager_service()->accounts_containers_versions->live( "accounts/{$data['accountID']}/containers/{$data['internalContainerID']}" ); } return parent::create_data_request( $data ); } /** * Creates GTM Container. * * @since 1.0.0 * @param string $account_id The account ID. * @param string|array $usage_context The container usage context(s). * * @return string Container public ID. * @throws Exception Throws an exception if raised during container creation. */ protected function create_container( $account_id, $usage_context = self::USAGE_CONTEXT_WEB ) { $restore_defer = $this->with_client_defer( false ); // Use site name for container, fallback to domain of reference URL. $container_name = get_bloginfo( 'name' ) ?: URL::parse( $this->context->get_reference_site_url(), PHP_URL_HOST ); // Prevent naming conflict (Tag Manager does not allow more than one with same name). if ( self::USAGE_CONTEXT_AMP === $usage_context ) { $container_name .= ' AMP'; } $container_name = self::sanitize_container_name( $container_name ); $container = new Google_Service_TagManager_Container(); $container->setName( $container_name ); $container->setUsageContext( (array) $usage_context ); try { $new_container = $this->get_tagmanager_service()->accounts_containers->create( "accounts/{$account_id}", $container ); } catch ( Exception $exception ) { $restore_defer(); throw $exception; } $restore_defer(); return $new_container->getPublicId(); } /** * Parses a response for the given datapoint. * * @since 1.0.0 * * @param Data_Request $data Data request object. * @param mixed $response Request response. * * @return mixed Parsed response data on success, or WP_Error on failure. */ protected function parse_data_response( Data_Request $data, $response ) { switch ( "{$data->method}:{$data->datapoint}" ) { case 'GET:accounts': /* @var Google_Service_TagManager_ListAccountsResponse $response List accounts response. */ return Sort::case_insensitive_list_sort( $response->getAccount(), 'name' ); case 'GET:accounts-containers': /* @var Google_Service_TagManager_ListAccountsResponse $response List accounts response. */ $accounts = Sort::case_insensitive_list_sort( $response->getAccount(), 'name' ); $response = array( // TODO: Parse this response to a regular array. 'accounts' => $accounts, 'containers' => array(), ); if ( 0 === count( $response['accounts'] ) ) { return $response; } if ( $data['accountID'] ) { $account_id = $data['accountID']; } else { $account_id = $response['accounts'][0]->getAccountId(); } $containers = $this->get_data( 'containers', array( 'accountID' => $account_id, 'usageContext' => $data['usageContext'] ?: self::USAGE_CONTEXT_WEB, ) ); if ( is_wp_error( $containers ) ) { return $response; } return array_merge( $response, compact( 'containers' ) ); case 'GET:containers': /* @var Google_Service_TagManager_ListContainersResponse $response Response object. */ $usage_context = $data['usageContext'] ?: array( self::USAGE_CONTEXT_WEB, self::USAGE_CONTEXT_AMP ); /* @var Google_Service_TagManager_Container[] $containers Filtered containers. */ $containers = array_filter( (array) $response->getContainer(), function ( Google_Service_TagManager_Container $container ) use ( $usage_context ) { return array_intersect( (array) $usage_context, $container->getUsageContext() ); } ); return Sort::case_insensitive_list_sort( array_values( $containers ), 'name' ); } return parent::parse_data_response( $data, $response ); } /** * Gets the configured TagManager service instance. * * @since 1.2.0 * * @return Google_Service_TagManager instance. * @throws Exception Thrown if the module did not correctly set up the service. */ private function get_tagmanager_service() { return $this->get_service( 'tagmanager' ); } /** * Sets up information about the module. * * @since 1.0.0 * * @return array Associative array of module info. */ protected function setup_info() { return array( 'slug' => self::MODULE_SLUG, 'name' => _x( 'Tag Manager', 'Service name', 'google-site-kit' ), 'description' => __( 'Tag Manager creates an easy to manage way to create tags on your site without updating code', 'google-site-kit' ), 'order' => 6, 'homepage' => __( 'https://tagmanager.google.com/', 'google-site-kit' ), ); } /** * Sets up the Google services the module should use. * * This method is invoked once by {@see Module::get_service()} to lazily set up the services when one is requested * for the first time. * * @since 1.0.0 * @since 1.2.0 Now requires Google_Site_Kit_Client instance. * * @param Google_Site_Kit_Client $client Google client instance. * @return array Google services as $identifier => $service_instance pairs. Every $service_instance must be an * instance of Google_Service. */ protected function setup_services( Google_Site_Kit_Client $client ) { return array( 'tagmanager' => new Google_Service_TagManager( $client ), ); } /** * Sets up the module's settings instance. * * @since 1.2.0 * * @return Module_Settings */ protected function setup_settings() { return new Settings( $this->options ); } /** * Sets up the module's assets to register. * * @since 1.11.0 * * @return Asset[] List of Asset objects. */ protected function setup_assets() { $base_url = $this->context->url( 'dist/assets/' ); $dependencies = array( 'googlesitekit-api', 'googlesitekit-data', 'googlesitekit-datastore-site', 'googlesitekit-modules', 'googlesitekit-vendor', 'googlesitekit-components', ); $analytics_exists = apply_filters( 'googlesitekit_module_exists', false, 'analytics' ); // Note that the Tag Manager bundle will make use of the Analytics bundle if it's available, // but can also function without it, hence the conditional include of the Analytics bundle here. if ( $analytics_exists ) { $dependencies[] = 'googlesitekit-modules-analytics'; } return array( new Script( 'googlesitekit-modules-tagmanager', array( 'src' => $base_url . 'js/googlesitekit-modules-tagmanager.js', 'dependencies' => $dependencies, ) ), ); } /** * Registers the Tag Manager tag. * * @since 1.24.0 */ private function register_tag() { $is_amp = $this->context->is_amp(); $module_settings = $this->get_settings(); $settings = $module_settings->get(); $tag = $is_amp ? new AMP_Tag( $settings['ampContainerID'], self::MODULE_SLUG ) : new Web_Tag( $settings['containerID'], self::MODULE_SLUG ); if ( ! $tag->is_tag_blocked() ) { $tag->use_guard( new Tag_Verify_Guard( $this->context->input() ) ); $tag->use_guard( new Tag_Guard( $module_settings, $is_amp ) ); $tag->use_guard( new Tag_Environment_Type_Guard() ); if ( $tag->can_register() ) { $tag->register(); } } } /** * Filters whether or not the Analytics module's snippet should be controlled by its `useSnippet` setting. * * @since 1.28.0 * @since 1.75.0 Now requires current UA property ID as second parameter. * * @param boolean $original_value Original value of useSnippet setting. * @param string $ua_property_id Current UA property. * @return boolean Filtered value. */ private function can_analytics_use_snippet( $original_value, $ua_property_id ) { $settings = $this->get_settings()->get(); if ( ! empty( $settings['gaPropertyID'] ) && $settings['useSnippet'] && $settings['gaPropertyID'] === $ua_property_id ) { return false; } return $original_value; } /** * Handles Analytics measurement opt-out for the configured Analytics property in the container(s). * * @since 1.41.0 * * @param string $property_id Analytics property_id. */ private function analytics_tracking_opt_out( $property_id ) { $settings = $this->get_settings()->get(); $ga_property_id = $settings['gaPropertyID']; if ( ! $ga_property_id || $ga_property_id === $property_id ) { return; } BC_Functions::wp_print_inline_script_tag( sprintf( 'window["ga-disable-%s"] = true;', esc_attr( $ga_property_id ) ) ); } /** * Filters whether or not the option to exclude certain users from tracking should be displayed. * * If Site Kit does not place the Analytics snippet (neither via Analytics nor via Tag Manager), * the option to exclude certain users from tracking should not be displayed. * * @since 1.36.0 * * @param boolean $allowed Whether to allow tracking exclusion. * @return boolean Filtered value. */ private function filter_analytics_allow_tracking_disabled( $allowed ) { if ( $allowed ) { return true; } $settings = $this->get_settings()->get(); if ( ! empty( $settings['gaPropertyID'] ) && $settings['useSnippet'] ) { return true; } return $allowed; } /** * Checks if the current user has access to the current configured service entity. * * @since 1.77.0 * * @return boolean|WP_Error */ public function check_service_entity_access() { $is_amp_mode = in_array( $this->context->get_amp_mode(), array( Context::AMP_MODE_PRIMARY, Context::AMP_MODE_SECONDARY ), true ); $settings = $this->get_settings()->get(); $account_id = $settings['accountID']; $configured_containers = $is_amp_mode ? array( $settings['containerID'], $settings['ampContainerID'] ) : array( $settings['containerID'] ); try { $containers = $this->get_tagmanager_service()->accounts_containers->listAccountsContainers( "accounts/{$account_id}" ); } catch ( Exception $e ) { if ( $e->getCode() === 403 ) { return false; } return $this->exception_to_error( $e ); } $all_containers = array_map( function( $container ) { return $container->getPublicId(); }, $containers->getContainer() ); return empty( array_diff( $configured_containers, $all_containers ) ); } } Site_Verification.php000064400000037573150515556660010720 0ustar00register_scopes_hook(); add_action( 'googlesitekit_verify_site_ownership', $this->get_method_proxy( 'handle_verification_token' ), 10, 2 ); $print_site_verification_meta = function() { $this->print_site_verification_meta(); }; add_action( 'wp_head', $print_site_verification_meta ); add_action( 'login_head', $print_site_verification_meta ); add_action( 'googlesitekit_authorize_user', function() { if ( ! $this->authentication->credentials()->using_proxy() ) { return; } $this->user_options->set( Verification::OPTION, 'verified' ); } ); add_action( 'init', function () { $request_uri = $this->context->input()->filter( INPUT_SERVER, 'REQUEST_URI' ); $request_method = $this->context->input()->filter( INPUT_SERVER, 'REQUEST_METHOD' ); if ( ( $request_uri && $request_method ) && 'GET' === strtoupper( $request_method ) && preg_match( '/^\/google(?P[a-z0-9]+)\.html$/', $request_uri, $matches ) ) { $this->serve_verification_file( $matches['token'] ); } } ); $clear_verification_meta_cache = function ( $meta_id, $object_id, $meta_key ) { if ( $this->user_options->get_meta_key( Verification_Meta::OPTION ) === $meta_key ) { ( new Transients( $this->context ) )->delete( self::TRANSIENT_VERIFICATION_META_TAGS ); } }; add_action( 'added_user_meta', $clear_verification_meta_cache, 10, 3 ); add_action( 'updated_user_meta', $clear_verification_meta_cache, 10, 3 ); add_action( 'deleted_user_meta', $clear_verification_meta_cache, 10, 3 ); } /** * Gets required Google OAuth scopes for the module. * * @since 1.0.0 * * @return array List of Google OAuth scopes. */ public function get_scopes() { return array( 'https://www.googleapis.com/auth/siteverification', ); } /** * Gets map of datapoint to definition data for each. * * @since 1.12.0 * * @return array Map of datapoints to their definitions. */ protected function get_datapoint_definitions() { return array( 'GET:verification' => array( 'service' => 'siteverification' ), 'POST:verification' => array( 'service' => 'siteverification' ), 'GET:verification-token' => array( 'service' => 'siteverification' ), 'GET:verified-sites' => array( 'service' => 'siteverification' ), ); } /** * Creates a request object for the given datapoint. * * @since 1.0.0 * * @param Data_Request $data Data request object. * @return RequestInterface|callable|WP_Error Request object or callable on success, or WP_Error on failure. * * @throws Invalid_Datapoint_Exception Thrown if the datapoint does not exist. */ protected function create_data_request( Data_Request $data ) { switch ( "{$data->method}:{$data->datapoint}" ) { case 'GET:verification': return $this->get_siteverification_service()->webResource->listWebResource(); case 'POST:verification': if ( ! isset( $data['siteURL'] ) ) { /* translators: %s: Missing parameter name */ return new WP_Error( 'missing_required_param', sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'siteURL' ), array( 'status' => 400 ) ); } return function() use ( $data ) { $current_user = wp_get_current_user(); if ( ! $current_user || ! $current_user->exists() ) { return new WP_Error( 'unknown_user', __( 'Unknown user.', 'google-site-kit' ) ); } $site = $this->get_data( 'verification', $data ); if ( is_wp_error( $site ) ) { return $site; } $sites = array(); if ( ! empty( $site['verified'] ) ) { $this->authentication->verification()->set( true ); return $site; } else { $token = $this->get_data( 'verification-token', $data ); if ( is_wp_error( $token ) ) { return $token; } $this->authentication->verification_meta()->set( $token['token'] ); $restore_defer = $this->with_client_defer( false ); $errors = new WP_Error(); foreach ( $this->permute_site_url( $data['siteURL'] ) as $url ) { $site = new Google_Service_SiteVerification_SiteVerificationWebResourceResourceSite(); $site->setType( 'SITE' ); $site->setIdentifier( $url ); $resource = new Google_Service_SiteVerification_SiteVerificationWebResourceResource(); $resource->setSite( $site ); try { $sites[] = $this->get_siteverification_service()->webResource->insert( 'META', $resource ); } catch ( Google_Service_Exception $e ) { $messages = wp_list_pluck( $e->getErrors(), 'message' ); $message = array_shift( $messages ); $errors->add( $e->getCode(), $message, array( 'url' => $url ) ); } catch ( Exception $e ) { $errors->add( $e->getCode(), $e->getMessage(), array( 'url' => $url ) ); } } $restore_defer(); if ( empty( $sites ) ) { return $errors; } } $this->authentication->verification()->set( true ); try { $verification = $this->get_siteverification_service()->webResource->get( $data['siteURL'] ); } catch ( Google_Service_Exception $e ) { $verification = array_shift( $sites ); } return array( 'identifier' => $verification->getSite()->getIdentifier(), 'type' => $verification->getSite()->getType(), 'verified' => true, ); }; case 'GET:verification-token': $existing_token = $this->authentication->verification_meta()->get(); if ( ! empty( $existing_token ) ) { return function() use ( $existing_token ) { return array( 'method' => 'META', 'token' => $existing_token, ); }; } $current_url = ! empty( $data['siteURL'] ) ? $data['siteURL'] : $this->context->get_reference_site_url(); $site = new Google_Service_SiteVerification_SiteVerificationWebResourceGettokenRequestSite(); $site->setIdentifier( $current_url ); $site->setType( 'SITE' ); $request = new Google_Service_SiteVerification_SiteVerificationWebResourceGettokenRequest(); $request->setSite( $site ); $request->setVerificationMethod( 'META' ); return $this->get_siteverification_service()->webResource->getToken( $request ); case 'GET:verified-sites': return $this->get_siteverification_service()->webResource->listWebResource(); } return parent::create_data_request( $data ); } /** * Parses a response for the given datapoint. * * @since 1.0.0 * * @param Data_Request $data Data request object. * @param mixed $response Request response. * * @return mixed Parsed response data on success, or WP_Error on failure. */ protected function parse_data_response( Data_Request $data, $response ) { switch ( "{$data->method}:{$data->datapoint}" ) { case 'GET:verification': if ( $data['siteURL'] ) { $current_url = $data['siteURL']; } else { $current_url = $this->context->get_reference_site_url(); } $items = $response->getItems(); foreach ( $items as $item ) { $site = $item->getSite(); $match = false; if ( 'INET_DOMAIN' === $site->getType() ) { $match = $this->is_domain_match( $site->getIdentifier(), $current_url ); } elseif ( 'SITE' === $site->getType() ) { $match = $this->is_url_match( $site->getIdentifier(), $current_url ); } if ( $match ) { return array( 'identifier' => $site->getIdentifier(), 'type' => $site->getType(), 'verified' => true, ); } } return array( 'identifier' => $current_url, 'type' => 'SITE', 'verified' => false, ); case 'GET:verification-token': if ( is_array( $response ) ) { return $response; } return array( 'method' => $response->getMethod(), 'token' => $response->getToken(), ); case 'GET:verified-sites': $items = $response->getItems(); $data = array(); foreach ( $items as $item ) { $site = $item->getSite(); $data[ $item->getId() ] = array( 'identifier' => $site->getIdentifier(), 'type' => $site->getType(), ); } return $data; } return parent::parse_data_response( $data, $response ); } /** * Sets up information about the module. * * @since 1.0.0 * * @return array Associative array of module info. */ protected function setup_info() { return array( 'slug' => 'site-verification', 'name' => _x( 'Site Verification', 'Service name', 'google-site-kit' ), 'description' => __( 'Google Site Verification allows you to manage ownership of your site.', 'google-site-kit' ), 'order' => 0, 'homepage' => __( 'https://www.google.com/webmasters/verification/home', 'google-site-kit' ), 'internal' => true, ); } /** * Get the configured siteverification service instance. * * @return Google_Service_SiteVerification The Site Verification API service. */ private function get_siteverification_service() { return $this->get_service( 'siteverification' ); } /** * Sets up the Google services the module should use. * * This method is invoked once by {@see Module::get_service()} to lazily set up the services when one is requested * for the first time. * * @since 1.0.0 * @since 1.2.0 Now requires Google_Site_Kit_Client instance. * * @param Google_Site_Kit_Client $client Google client instance. * @return array Google services as $identifier => $service_instance pairs. Every $service_instance must be an * instance of Google_Service. */ protected function setup_services( Google_Site_Kit_Client $client ) { return array( 'siteverification' => new Google_Service_SiteVerification( $client ), ); } /** * Handles receiving a verification token for a user by the authentication proxy. * * @since 1.1.0 * @since 1.1.2 Runs on `admin_action_googlesitekit_proxy_setup` and no longer redirects directly. * @since 1.48.0 Token and method are now passed as arguments. * @since 1.49.0 No longer uses the `googlesitekit_proxy_setup_url_params` filter to set the `verify` and `verification_method` query params. * * @param string $token Verification token. * @param string $method Verification method type. */ private function handle_verification_token( $token, $method ) { switch ( $method ) { case self::VERIFICATION_TYPE_FILE: $this->authentication->verification_file()->set( $token ); break; case self::VERIFICATION_TYPE_META: $this->authentication->verification_meta()->set( $token ); } } /** * Prints site verification meta in wp_head(). * * @since 1.1.0 */ private function print_site_verification_meta() { // Get verification meta tags for all users. $verification_tags = $this->get_all_verification_tags(); $allowed_html = array( 'meta' => array( 'name' => array(), 'content' => array(), ), ); foreach ( $verification_tags as $verification_tag ) { $verification_tag = html_entity_decode( $verification_tag ); if ( 0 !== strpos( $verification_tag, ''; } echo wp_kses( $verification_tag, $allowed_html ); } } /** * Gets all available verification tags for all users. * * This is a special method needed for printing all meta tags in the frontend. * * @since 1.4.0 * * @return array List of verification meta tags. */ private function get_all_verification_tags() { global $wpdb; $transients = new Transients( $this->context ); $meta_tags = $transients->get( self::TRANSIENT_VERIFICATION_META_TAGS ); if ( ! is_array( $meta_tags ) ) { $meta_key = $this->user_options->get_meta_key( Verification_Meta::OPTION ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery $meta_tags = $wpdb->get_col( $wpdb->prepare( "SELECT DISTINCT meta_value FROM {$wpdb->usermeta} WHERE meta_key = %s", $meta_key ) ); $transients->set( self::TRANSIENT_VERIFICATION_META_TAGS, $meta_tags ); } return array_filter( $meta_tags ); } /** * Serves the verification file response. * * @param string $verification_token Token portion of verification. * * @since 1.1.0 */ private function serve_verification_file( $verification_token ) { $user_ids = ( new \WP_User_Query( array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key 'meta_key' => $this->user_options->get_meta_key( Verification_File::OPTION ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value 'meta_value' => $verification_token, 'fields' => 'id', 'number' => 1, ) ) )->get_results(); $user_id = array_shift( $user_ids ) ?: 0; if ( $user_id && user_can( $user_id, Permissions::SETUP ) ) { printf( 'google-site-verification: google%s.html', esc_html( $verification_token ) ); ( new Exit_Handler() )->invoke(); } // If the user does not have the necessary permissions then let the request pass through. } /** * Returns TRUE to indicate that this module should be always active. * * @since 1.49.0 * * @return bool Returns `true` indicating that this module should be activated all the time. */ public static function is_force_active() { return true; } } Search_Console/Settings.php000064400000003121150515556660011757 0ustar00register_owned_keys(); // Backwards compatibility with previous dedicated option. add_filter( 'default_option_' . self::OPTION, function ( $default ) { $default['propertyID'] = $this->options->get( 'googlesitekit_search_console_property' ) ?: ''; return $default; } ); } /** * Gets the default value. * * @since 1.3.0 * * @return array */ protected function get_default() { return array( 'propertyID' => '', 'ownerID' => '', ); } /** * Returns keys for owned settings. * * @since 1.31.0 * * @return array An array of keys for owned settings. */ public function get_owned_keys() { return array( 'propertyID', ); } } Search_Console.php000064400000045233150515556660010171 0ustar00register_scopes_hook(); // Detect and store Search Console property when receiving token for the first time. add_action( 'googlesitekit_authorize_user', function( array $token_response ) { if ( ! current_user_can( Permissions::SETUP ) ) { return; } // If the response includes the Search Console property, set that. // But only if it is being set for the first time or if Search Console // has no owner or the current user is the owner. if ( ! empty( $token_response['search_console_property'] ) && ( empty( $this->get_property_id() ) || ( in_array( $this->get_owner_id(), array( 0, get_current_user_id() ), true ) ) ) ) { $this->get_settings()->merge( array( 'propertyID' => $token_response['search_console_property'] ) ); return; } // Otherwise try to detect if there isn't one set already. $property_id = $this->get_property_id() ?: $this->detect_property_id(); if ( ! $property_id ) { return; } $this->get_settings()->merge( array( 'propertyID' => $property_id ) ); } ); // Ensure that a Search Console property must be set at all times. add_filter( 'googlesitekit_setup_complete', function( $complete ) { if ( ! $complete ) { return $complete; } return (bool) $this->get_property_id(); } ); // Provide Search Console property information to JavaScript. add_filter( 'googlesitekit_setup_data', function ( $data ) { $data['hasSearchConsoleProperty'] = (bool) $this->get_property_id(); return $data; }, 11 ); } /** * Gets required Google OAuth scopes for the module. * * @since 1.0.0 * * @return array List of Google OAuth scopes. */ public function get_scopes() { return array( 'https://www.googleapis.com/auth/webmasters', // The scope for the Search Console remains the legacy webmasters scope. ); } /** * Gets an array of debug field definitions. * * @since 1.5.0 * * @return array */ public function get_debug_fields() { return array( 'search_console_property' => array( 'label' => __( 'Search Console property', 'google-site-kit' ), 'value' => $this->get_property_id(), ), ); } /** * Gets map of datapoint to definition data for each. * * @since 1.12.0 * * @return array Map of datapoints to their definitions. */ protected function get_datapoint_definitions() { return array( 'GET:matched-sites' => array( 'service' => 'searchconsole' ), 'GET:searchanalytics' => array( 'service' => 'searchconsole', 'shareable' => Feature_Flags::enabled( 'dashboardSharing' ), ), 'POST:site' => array( 'service' => 'searchconsole' ), 'GET:sites' => array( 'service' => 'searchconsole' ), ); } /** * Creates a request object for the given datapoint. * * @since 1.0.0 * * @param Data_Request $data Data request object. * @return RequestInterface|callable|WP_Error Request object or callable on success, or WP_Error on failure. * * @throws Invalid_Datapoint_Exception Thrown if the datapoint does not exist. */ protected function create_data_request( Data_Request $data ) { switch ( "{$data->method}:{$data->datapoint}" ) { case 'GET:matched-sites': return $this->get_searchconsole_service()->sites->listSites(); case 'GET:searchanalytics': $start_date = $data['startDate']; $end_date = $data['endDate']; if ( ! strtotime( $start_date ) || ! strtotime( $end_date ) ) { list ( $start_date, $end_date ) = $this->parse_date_range( $data['dateRange'] ?: 'last-28-days', $data['compareDateRanges'] ? 2 : 1, 1 // Offset. ); } $data_request = array( 'start_date' => $start_date, 'end_date' => $end_date, ); if ( ! empty( $data['url'] ) ) { $data_request['page'] = ( new Google_URL_Normalizer() )->normalize_url( $data['url'] ); } if ( isset( $data['limit'] ) ) { $data_request['row_limit'] = $data['limit']; } $dimensions = $this->parse_string_list( $data['dimensions'] ); if ( is_array( $dimensions ) && ! empty( $dimensions ) ) { $data_request['dimensions'] = $dimensions; } return $this->create_search_analytics_data_request( $data_request ); case 'POST:site': if ( empty( $data['siteURL'] ) ) { return new WP_Error( 'missing_required_param', /* translators: %s: Missing parameter name */ sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'siteURL' ), array( 'status' => 400 ) ); } $url_normalizer = new Google_URL_Normalizer(); $site_url = $data['siteURL']; if ( 0 === strpos( $site_url, 'sc-domain:' ) ) { // Domain property. $site_url = 'sc-domain:' . $url_normalizer->normalize_url( str_replace( 'sc-domain:', '', $site_url, 1 ) ); } else { // URL property. $site_url = $url_normalizer->normalize_url( trailingslashit( $site_url ) ); } return function () use ( $site_url ) { $restore_defer = $this->with_client_defer( false ); try { // If the site does not exist in the account, an exception will be thrown. $site = $this->get_searchconsole_service()->sites->get( $site_url ); } catch ( Google_Service_Exception $exception ) { // If we got here, the site does not exist in the account, so we will add it. /* @var ResponseInterface $response Response object. */ $response = $this->get_searchconsole_service()->sites->add( $site_url ); if ( 204 !== $response->getStatusCode() ) { return new WP_Error( 'failed_to_add_site_to_search_console', __( 'Error adding the site to Search Console.', 'google-site-kit' ), array( 'status' => 500 ) ); } // Fetch the site again now that it exists. $site = $this->get_searchconsole_service()->sites->get( $site_url ); } $restore_defer(); $this->get_settings()->merge( array( 'propertyID' => $site_url ) ); return array( 'siteURL' => $site->getSiteUrl(), 'permissionLevel' => $site->getPermissionLevel(), ); }; case 'GET:sites': return $this->get_searchconsole_service()->sites->listSites(); } return parent::create_data_request( $data ); } /** * Parses a response for the given datapoint. * * @since 1.0.0 * * @param Data_Request $data Data request object. * @param mixed $response Request response. * * @return mixed Parsed response data on success, or WP_Error on failure. */ protected function parse_data_response( Data_Request $data, $response ) { switch ( "{$data->method}:{$data->datapoint}" ) { case 'GET:matched-sites': /* @var Google_Service_SearchConsole_SitesListResponse $response Response object. */ $entries = Sort::case_insensitive_list_sort( $this->map_sites( (array) $response->getSiteEntry() ), 'name' ); $strict = filter_var( $data['strict'], FILTER_VALIDATE_BOOLEAN ); $current_url = $this->context->get_reference_site_url(); if ( ! $strict ) { $current_url = untrailingslashit( $current_url ); $current_url = $this->strip_url_scheme( $current_url ); $current_url = $this->strip_domain_www( $current_url ); } $sufficient_permission_levels = array( 'siteRestrictedUser', 'siteOwner', 'siteFullUser', ); return array_values( array_filter( $entries, function ( array $entry ) use ( $current_url, $sufficient_permission_levels, $strict ) { if ( 0 === strpos( $entry['siteURL'], 'sc-domain:' ) ) { $match = $this->is_domain_match( substr( $entry['siteURL'], strlen( 'sc-domain:' ) ), $current_url ); } else { $site_url = untrailingslashit( $entry['siteURL'] ); if ( ! $strict ) { $site_url = $this->strip_url_scheme( $site_url ); $site_url = $this->strip_domain_www( $site_url ); } $match = $this->is_url_match( $site_url, $current_url ); } return $match && in_array( $entry['permissionLevel'], $sufficient_permission_levels, true ); } ) ); case 'GET:searchanalytics': return $response->getRows(); case 'GET:sites': /* @var Google_Service_SearchConsole_SitesListResponse $response Response object. */ return $this->map_sites( (array) $response->getSiteEntry() ); } return parent::parse_data_response( $data, $response ); } /** * Map Site model objects to associative arrays used for API responses. * * @param array $sites Site objects. * * @return array */ private function map_sites( $sites ) { return array_map( function ( Google_Service_SearchConsole_WmxSite $site ) { return array( 'siteURL' => $site->getSiteUrl(), 'permissionLevel' => $site->getPermissionLevel(), ); }, $sites ); } /** * Creates a new Search Console analytics request for the current site and given arguments. * * @since 1.0.0 * * @param array $args { * Optional. Additional arguments. * * @type array $dimensions List of request dimensions. Default empty array. * @type string $start_date Start date in 'Y-m-d' format. Default empty string. * @type string $end_date End date in 'Y-m-d' format. Default empty string. * @type string $page Specific page URL to filter by. Default empty string. * @type int $row_limit Limit of rows to return. Default 1000. * } * @return RequestInterface Search Console analytics request instance. */ protected function create_search_analytics_data_request( array $args = array() ) { $args = wp_parse_args( $args, array( 'dimensions' => array(), 'start_date' => '', 'end_date' => '', 'page' => '', 'row_limit' => 1000, ) ); $property_id = $this->get_property_id(); $request = new Google_Service_SearchConsole_SearchAnalyticsQueryRequest(); if ( ! empty( $args['dimensions'] ) ) { $request->setDimensions( (array) $args['dimensions'] ); } if ( ! empty( $args['start_date'] ) ) { $request->setStartDate( $args['start_date'] ); } if ( ! empty( $args['end_date'] ) ) { $request->setEndDate( $args['end_date'] ); } $request->setDataState( 'all' ); $filters = array(); // If domain property, limit data to URLs that are part of the current site. if ( 0 === strpos( $property_id, 'sc-domain:' ) ) { $scope_site_filter = new Google_Service_SearchConsole_ApiDimensionFilter(); $scope_site_filter->setDimension( 'page' ); $scope_site_filter->setOperator( 'contains' ); $scope_site_filter->setExpression( esc_url_raw( $this->context->get_reference_site_url() ) ); $filters[] = $scope_site_filter; } // If specific URL requested, limit data to that URL. if ( ! empty( $args['page'] ) ) { $single_url_filter = new Google_Service_SearchConsole_ApiDimensionFilter(); $single_url_filter->setDimension( 'page' ); $single_url_filter->setOperator( 'equals' ); $single_url_filter->setExpression( rawurldecode( esc_url_raw( $args['page'] ) ) ); $filters[] = $single_url_filter; } // If there are relevant filters, add them to the request. if ( ! empty( $filters ) ) { $filter_group = new Google_Service_SearchConsole_ApiDimensionFilterGroup(); $filter_group->setGroupType( 'and' ); $filter_group->setFilters( $filters ); $request->setDimensionFilterGroups( array( $filter_group ) ); } if ( ! empty( $args['row_limit'] ) ) { $request->setRowLimit( $args['row_limit'] ); } return $this->get_searchconsole_service() ->searchanalytics ->query( $property_id, $request ); } /** * Gets the property ID. * * @since 1.3.0 * * @return string Property ID URL if set, or empty string. */ protected function get_property_id() { $option = $this->get_settings()->get(); return $option['propertyID']; } /** * Detects the property ID to use for this site. * * This method runs a Search Console API request. The determined ID should therefore be stored and accessed through * {@see Search_Console::get_property_id()} instead. * * @since 1.3.0 * * @return string Property ID, or empty string if none found. */ protected function detect_property_id() { $properties = $this->get_data( 'matched-sites', array( 'strict' => 'yes' ) ); if ( is_wp_error( $properties ) || ! $properties ) { return ''; } // If there are multiple, prefer URL property over domain property. if ( count( $properties ) > 1 ) { $url_properties = array_filter( $properties, function( $property ) { return 0 !== strpos( $property['siteURL'], 'sc-domain:' ); } ); if ( count( $url_properties ) > 0 ) { $properties = $url_properties; } } $property = array_shift( $properties ); return $property['siteURL']; } /** * Sets up information about the module. * * @since 1.0.0 * * @return array Associative array of module info. */ protected function setup_info() { return array( 'slug' => 'search-console', 'name' => _x( 'Search Console', 'Service name', 'google-site-kit' ), 'description' => __( 'Google Search Console and helps you understand how Google views your site and optimize its performance in search results.', 'google-site-kit' ), 'order' => 1, 'homepage' => __( 'https://search.google.com/search-console', 'google-site-kit' ), ); } /** * Get the configured SearchConsole service instance. * * @since 1.25.0 * * @return Google_Service_SearchConsole The Search Console API service. */ private function get_searchconsole_service() { return $this->get_service( 'searchconsole' ); } /** * Sets up the Google services the module should use. * * This method is invoked once by {@see Module::get_service()} to lazily set up the services when one is requested * for the first time. * * @since 1.0.0 * @since 1.2.0 Now requires Google_Site_Kit_Client instance. * * @param Google_Site_Kit_Client $client Google client instance. * @return array Google services as $identifier => $service_instance pairs. Every $service_instance must be an * instance of Google_Service. */ protected function setup_services( Google_Site_Kit_Client $client ) { return array( 'searchconsole' => new Google_Service_SearchConsole( $client ), ); } /** * Sets up the module's settings instance. * * @since 1.3.0 * * @return Module_Settings */ protected function setup_settings() { return new Settings( $this->options ); } /** * Sets up the module's assets to register. * * @since 1.9.0 * * @return Asset[] List of Asset objects. */ protected function setup_assets() { $base_url = $this->context->url( 'dist/assets/' ); return array( new Script( 'googlesitekit-modules-search-console', array( 'src' => $base_url . 'js/googlesitekit-modules-search-console.js', 'dependencies' => array( 'googlesitekit-vendor', 'googlesitekit-api', 'googlesitekit-data', 'googlesitekit-modules', 'googlesitekit-components', ), ) ), ); } /** * Returns TRUE to indicate that this module should be always active. * * @since 1.49.0 * * @return bool Returns `true` indicating that this module should be activated all the time. */ public static function is_force_active() { return true; } /** * Checks if the current user has access to the current configured service entity. * * @since 1.70.0 * * @return boolean|WP_Error */ public function check_service_entity_access() { $data_request = array( 'start_date' => gmdate( 'Y-m-d' ), 'end_date' => gmdate( 'Y-m-d' ), 'row_limit' => 1, ); try { $this->create_search_analytics_data_request( $data_request ); } catch ( Exception $e ) { if ( $e->getCode() === 403 ) { return false; } return $this->exception_to_error( $e ); } return true; } } Analytics_4.php000064400000063425150515556660007457 0ustar00register_scopes_hook(); add_action( 'googlesitekit_analytics_handle_provisioning_callback', $this->get_method_proxy( 'handle_provisioning_callback' ) ); // Analytics 4 tag placement logic. add_action( 'template_redirect', $this->get_method_proxy( 'register_tag' ) ); add_action( 'googlesitekit_analytics_tracking_opt_out', $this->get_method_proxy( 'analytics_tracking_opt_out' ) ); } /** * Gets required Google OAuth scopes for the module. * * @since 1.30.0 * * @return array List of Google OAuth scopes. */ public function get_scopes() { return array( Analytics::READONLY_SCOPE, ); } /** * Checks whether the module is connected. * * A module being connected means that all steps required as part of its activation are completed. * * @since 1.30.0 * * @return bool True if module is connected, false otherwise. */ public function is_connected() { $required_keys = array( // TODO: These can be uncommented when Analytics and Analytics 4 modules are officially separated. /* 'accountID', */ // phpcs:ignore Squiz.PHP.CommentedOutCode.Found /* 'adsConversionID', */ // phpcs:ignore Squiz.PHP.CommentedOutCode.Found 'propertyID', 'webDataStreamID', 'measurementID', ); $options = $this->get_settings()->get(); foreach ( $required_keys as $required_key ) { if ( empty( $options[ $required_key ] ) ) { return false; } } return parent::is_connected(); } /** * Cleans up when the module is deactivated. * * @since 1.30.0 */ public function on_deactivation() { $this->get_settings()->delete(); } /** * Gets an array of debug field definitions. * * @since 1.30.0 * * @return array */ public function get_debug_fields() { $settings = $this->get_settings()->get(); return array( // phpcs:disable /* TODO: This can be uncommented when Analytics and Analytics 4 modules are officially separated. 'analytics_4_account_id' => array( 'label' => __( 'Analytics 4 account ID', 'google-site-kit' ), 'value' => $settings['accountID'], 'debug' => Debug_Data::redact_debug_value( $settings['accountID'] ), ), 'analytics_4_ads_conversion_id' => array( 'label' => __( 'Analytics 4 ads conversion ID', 'google-site-kit' ), 'value' => $settings['adsConversionID'], 'debug' => Debug_Data::redact_debug_value( $settings['adsConversionID'] ), ), */ // phpcs:enable 'analytics_4_property_id' => array( 'label' => __( 'Analytics 4 property ID', 'google-site-kit' ), 'value' => $settings['propertyID'], 'debug' => Debug_Data::redact_debug_value( $settings['propertyID'], 7 ), ), 'analytics_4_web_data_stream_id' => array( 'label' => __( 'Analytics 4 web data stream ID', 'google-site-kit' ), 'value' => $settings['webDataStreamID'], 'debug' => Debug_Data::redact_debug_value( $settings['webDataStreamID'] ), ), 'analytics_4_measurement_id' => array( 'label' => __( 'Analytics 4 measurement ID', 'google-site-kit' ), 'value' => $settings['measurementID'], 'debug' => Debug_Data::redact_debug_value( $settings['measurementID'] ), ), 'analytics_4_use_snippet' => array( 'label' => __( 'Analytics 4 snippet placed', 'google-site-kit' ), 'value' => $settings['useSnippet'] ? __( 'Yes', 'google-site-kit' ) : __( 'No', 'google-site-kit' ), 'debug' => $settings['useSnippet'] ? 'yes' : 'no', ), ); } /** * Gets map of datapoint to definition data for each. * * @since 1.30.0 * * @return array Map of datapoints to their definitions. */ protected function get_datapoint_definitions() { return array( 'GET:account-summaries' => array( 'service' => 'analyticsadmin' ), 'GET:accounts' => array( 'service' => 'analyticsadmin' ), 'POST:create-property' => array( 'service' => 'analyticsadmin', 'scopes' => array( Analytics::EDIT_SCOPE ), 'request_scopes_message' => __( 'You’ll need to grant Site Kit permission to create a new Analytics 4 property on your behalf.', 'google-site-kit' ), ), 'POST:create-webdatastream' => array( 'service' => 'analyticsadmin', 'scopes' => array( Analytics::EDIT_SCOPE ), 'request_scopes_message' => __( 'You’ll need to grant Site Kit permission to create a new Analytics 4 Measurement ID for this site on your behalf.', 'google-site-kit' ), ), 'GET:properties' => array( 'service' => 'analyticsadmin' ), 'GET:property' => array( 'service' => 'analyticsadmin' ), 'GET:webdatastreams' => array( 'service' => 'analyticsadmin' ), 'GET:webdatastreams-batch' => array( 'service' => 'analyticsadmin' ), ); } /** * Creates a new property for provided account. * * @since 1.35.0 * * @param string $account_id Account ID. * @return Google_Service_GoogleAnalyticsAdmin_GoogleAnalyticsAdminV1betaProperty A new property. */ private function create_property( $account_id ) { $timezone = get_option( 'timezone_string' ); if ( empty( $timezone ) ) { $timezone = 'UTC'; } $property = new Google_Service_GoogleAnalyticsAdmin_GoogleAnalyticsAdminV1betaProperty(); $property->setParent( self::normalize_account_id( $account_id ) ); $property->setDisplayName( URL::parse( $this->context->get_reference_site_url(), PHP_URL_HOST ) ); $property->setTimeZone( $timezone ); return $this->get_service( 'analyticsadmin' )->properties->create( $property ); } /** * Creates a new web data stream for provided property. * * @since 1.35.0 * * @param string $property_id Property ID. * @return GoogleAnalyticsAdminV1betaDataStream A new web data stream. */ private function create_webdatastream( $property_id ) { $site_url = $this->context->get_reference_site_url(); $data = new GoogleAnalyticsAdminV1betaDataStreamWebStreamData(); $data->setDefaultUri( $site_url ); $datastream = new GoogleAnalyticsAdminV1betaDataStream(); $datastream->setDisplayName( URL::parse( $site_url, PHP_URL_HOST ) ); $datastream->setType( 'WEB_DATA_STREAM' ); $datastream->setWebStreamData( $data ); /* @var Google_Service_GoogleAnalyticsAdmin $analyticsadmin phpcs:ignore Squiz.PHP.CommentedOutCode.Found */ $analyticsadmin = $this->get_service( 'analyticsadmin' ); return $analyticsadmin ->properties_dataStreams // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase ->create( self::normalize_property_id( $property_id ), $datastream ); } /** * Handles Analytics measurement opt-out for a GA4 property. * * @since 1.41.0 */ private function analytics_tracking_opt_out() { $settings = $this->get_settings()->get(); $measurement_id = $settings['measurementID']; if ( ! $measurement_id ) { return; } BC_Functions::wp_print_inline_script_tag( sprintf( 'window["ga-disable-%s"] = true;', esc_attr( $measurement_id ) ) ); } /** * Provisions new GA4 property and web data stream for provided account. * * @since 1.35.0 * * @param string $account_id Account ID. */ private function handle_provisioning_callback( $account_id ) { // TODO: remove this try/catch once GA4 API stabilizes. try { // Reset the current GA4 settings. $this->get_settings()->merge( array( 'propertyID' => '', 'webDataStreamID' => '', 'measurementID' => '', ) ); $property = $this->create_property( $account_id ); $property = self::filter_property_with_ids( $property ); if ( empty( $property->_id ) ) { return; } $this->get_settings()->merge( array( 'propertyID' => $property->_id ) ); $web_datastream = $this->create_webdatastream( $property->_id ); $web_datastream = self::filter_webdatastream_with_ids( $web_datastream ); if ( empty( $web_datastream->_id ) ) { return; } $this->get_settings()->merge( array( 'webDataStreamID' => $web_datastream->_id, 'measurementID' => $web_datastream->webStreamData->measurementId, // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase ) ); } catch ( Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch // Suppress this exception because it might be caused by unstable GA4 API. } } /** * Creates a request object for the given datapoint. * * @since 1.30.0 * * @param Data_Request $data Data request object. * @return RequestInterface|callable|WP_Error Request object or callable on success, or WP_Error on failure. * * @throws Invalid_Datapoint_Exception Thrown if the datapoint does not exist. */ protected function create_data_request( Data_Request $data ) { switch ( "{$data->method}:{$data->datapoint}" ) { case 'GET:accounts': return $this->get_service( 'analyticsadmin' )->accounts->listAccounts(); case 'GET:account-summaries': return $this->get_service( 'analyticsadmin' )->accountSummaries->listAccountSummaries( array( 'pageSize' => 200 ) ); case 'POST:create-property': if ( ! isset( $data['accountID'] ) ) { return new WP_Error( 'missing_required_param', /* translators: %s: Missing parameter name */ sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'accountID' ), array( 'status' => 400 ) ); } return $this->create_property( $data['accountID'] ); case 'POST:create-webdatastream': if ( ! isset( $data['propertyID'] ) ) { return new WP_Error( 'missing_required_param', /* translators: %s: Missing parameter name */ sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'propertyID' ), array( 'status' => 400 ) ); } return $this->create_webdatastream( $data['propertyID'] ); case 'GET:properties': if ( ! isset( $data['accountID'] ) ) { return new WP_Error( 'missing_required_param', /* translators: %s: Missing parameter name */ sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'accountID' ), array( 'status' => 400 ) ); } return $this->get_service( 'analyticsadmin' )->properties->listProperties( array( 'filter' => 'parent:' . self::normalize_account_id( $data['accountID'] ), ) ); case 'GET:property': if ( ! isset( $data['propertyID'] ) ) { return new WP_Error( 'missing_required_param', /* translators: %s: Missing parameter name */ sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'propertyID' ), array( 'status' => 400 ) ); } return $this->get_service( 'analyticsadmin' )->properties->get( self::normalize_property_id( $data['propertyID'] ) ); case 'GET:webdatastreams': if ( ! isset( $data['propertyID'] ) ) { return new WP_Error( 'missing_required_param', /* translators: %s: Missing parameter name */ sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'propertyID' ), array( 'status' => 400 ) ); } /* @var Google_Service_GoogleAnalyticsAdmin $analyticsadmin phpcs:ignore Squiz.PHP.CommentedOutCode.Found */ $analyticsadmin = $this->get_service( 'analyticsadmin' ); return $analyticsadmin ->properties_dataStreams // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase ->listPropertiesDataStreams( self::normalize_property_id( $data['propertyID'] ) ); case 'GET:webdatastreams-batch': if ( ! isset( $data['propertyIDs'] ) ) { return new WP_Error( 'missing_required_param', /* translators: %s: Missing parameter name */ sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'propertyIDs' ), array( 'status' => 400 ) ); } if ( ! is_array( $data['propertyIDs'] ) || count( $data['propertyIDs'] ) > 10 ) { return new WP_Error( 'rest_invalid_param', /* translators: %s: List of invalid parameters. */ sprintf( __( 'Invalid parameter(s): %s', 'google-site-kit' ), 'propertyIDs' ), array( 'status' => 400 ) ); } /* @var Google_Service_GoogleAnalyticsAdmin $analyticsadmin phpcs:ignore Squiz.PHP.CommentedOutCode.Found */ $analyticsadmin = $this->get_service( 'analyticsadmin' ); $batch_request = $analyticsadmin->createBatch(); foreach ( $data['propertyIDs'] as $property_id ) { $batch_request->add( $analyticsadmin ->properties_dataStreams // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase ->listPropertiesDataStreams( self::normalize_property_id( $property_id ) ) ); } return function() use ( $batch_request ) { return $batch_request->execute(); }; } return parent::create_data_request( $data ); } /** * Parses a response for the given datapoint. * * @since 1.30.0 * * @param Data_Request $data Data request object. * @param mixed $response Request response. * * @return mixed Parsed response data on success, or WP_Error on failure. */ protected function parse_data_response( Data_Request $data, $response ) { switch ( "{$data->method}:{$data->datapoint}" ) { case 'GET:accounts': return array_map( array( self::class, 'filter_account_with_ids' ), $response->getAccounts() ); case 'GET:account-summaries': return array_map( function( $account ) { $obj = self::filter_account_with_ids( $account, 'account' ); $obj->propertySummaries = array_map( // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase function( $property ) { return self::filter_property_with_ids( $property, 'property' ); }, $account->getPropertySummaries() ); return $obj; }, $response->getAccountSummaries() ); case 'POST:create-property': return self::filter_property_with_ids( $response ); case 'POST:create-webdatastream': return self::filter_webdatastream_with_ids( $response ); case 'GET:properties': return Sort::case_insensitive_list_sort( array_map( array( self::class, 'filter_property_with_ids' ), $response->getProperties() ), 'displayName' ); case 'GET:property': return self::filter_property_with_ids( $response ); case 'GET:webdatastreams': /* @var GoogleAnalyticsAdminV1betaListDataStreamsResponse $response phpcs:ignore Squiz.PHP.CommentedOutCode.Found */ $webdatastreams = self::filter_web_datastreams( $response->getDataStreams() ); return array_map( array( self::class, 'filter_webdatastream_with_ids' ), $webdatastreams ); case 'GET:webdatastreams-batch': return self::parse_webdatastreams_batch( $response ); } return parent::parse_data_response( $data, $response ); } /** * Sets up information about the module. * * @since 1.30.0 * * @return array Associative array of module info. */ protected function setup_info() { return array( 'slug' => self::MODULE_SLUG, 'name' => _x( 'Analytics 4', 'Service name', 'google-site-kit' ), 'description' => __( 'Get a deeper understanding of your customers. Google Analytics gives you the free tools you need to analyze data for your business in one place.', 'google-site-kit' ), 'order' => 3, 'homepage' => __( 'https://analytics.google.com/analytics/web', 'google-site-kit' ), 'internal' => true, 'depends_on' => array( 'analytics' ), ); } /** * Sets up the Google services the module should use. * * This method is invoked once by {@see Module::get_service()} to lazily set up the services when one is requested * for the first time. * * @since 1.30.0 * * @param Google_Site_Kit_Client $client Google client instance. * @return array Google services as $identifier => $service_instance pairs. Every $service_instance must be an * instance of Google_Service. */ protected function setup_services( Google_Site_Kit_Client $client ) { return array( 'analyticsadmin' => new Google_Service_GoogleAnalyticsAdmin( $client ), ); } /** * Sets up the module's settings instance. * * @since 1.30.0 * * @return Module_Settings */ protected function setup_settings() { return new Settings( $this->options ); } /** * Sets up the module's assets to register. * * @since 1.31.0 * * @return Asset[] List of Asset objects. */ protected function setup_assets() { $base_url = $this->context->url( 'dist/assets/' ); return array( new Script( 'googlesitekit-modules-analytics-4', array( 'src' => $base_url . 'js/googlesitekit-modules-analytics-4.js', 'dependencies' => array( 'googlesitekit-vendor', 'googlesitekit-api', 'googlesitekit-data', 'googlesitekit-modules', 'googlesitekit-datastore-site', 'googlesitekit-datastore-forms', 'googlesitekit-components', ), ) ), ); } /** * Registers the Analytics 4 tag. * * @since 1.31.0 */ private function register_tag() { if ( $this->context->is_amp() ) { return; } $settings = $this->get_settings()->get(); $tag = new Web_Tag( $settings['measurementID'], self::MODULE_SLUG ); if ( $tag->is_tag_blocked() ) { return; } $tag->use_guard( new Tag_Verify_Guard( $this->context->input() ) ); $tag->use_guard( new Tag_Guard( $this->get_settings() ) ); $tag->use_guard( new Tag_Environment_Type_Guard() ); if ( $tag->can_register() ) { // Here we need to retrieve the ads conversion ID from the // classic/UA Analytics settings as it does not exist yet for this module. // TODO: Update the value to be sourced from GA4 module settings once decoupled. $ua_settings = ( new Analytics_Settings( $this->options ) )->get(); $tag->set_ads_conversion_id( $ua_settings['adsConversionID'] ); $tag->register(); } } /** * Parses account ID, adds it to the model object and returns updated model. * * @since 1.31.0 * * @param Google_Model $account Account model. * @param string $id_key Attribute name that contains account id. * @return stdClass Updated model with _id attribute. */ public static function filter_account_with_ids( $account, $id_key = 'name' ) { $obj = $account->toSimpleObject(); $matches = array(); if ( preg_match( '#accounts/([^/]+)#', $account[ $id_key ], $matches ) ) { $obj->_id = $matches[1]; } return $obj; } /** * Parses account and property IDs, adds it to the model object and returns updated model. * * @since 1.31.0 * * @param Google_Model $property Property model. * @param string $id_key Attribute name that contains property id. * @return stdClass Updated model with _id and _accountID attributes. */ public static function filter_property_with_ids( $property, $id_key = 'name' ) { $obj = $property->toSimpleObject(); $matches = array(); if ( preg_match( '#properties/([^/]+)#', $property[ $id_key ], $matches ) ) { $obj->_id = $matches[1]; } $matches = array(); if ( preg_match( '#accounts/([^/]+)#', $property['parent'], $matches ) ) { $obj->_accountID = $matches[1]; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase } return $obj; } /** * Parses property and web datastream IDs, adds it to the model object and returns updated model. * * @since 1.31.0 * * @param Google_Model $webdatastream Web datastream model. * @return stdClass Updated model with _id and _propertyID attributes. */ public static function filter_webdatastream_with_ids( $webdatastream ) { $obj = $webdatastream->toSimpleObject(); $matches = array(); if ( preg_match( '#properties/([^/]+)/dataStreams/([^/]+)#', $webdatastream['name'], $matches ) ) { $obj->_id = $matches[2]; $obj->_propertyID = $matches[1]; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase } return $obj; } /** * Filters a list of data stream objects and returns only web data streams. * * @since 1.49.1 * * @param GoogleAnalyticsAdminV1betaDataStream[] $datastreams Data streams to filter. * @return GoogleAnalyticsAdminV1betaDataStream[] Web data streams. */ public static function filter_web_datastreams( array $datastreams ) { return array_filter( $datastreams, function ( GoogleAnalyticsAdminV1betaDataStream $datastream ) { return $datastream->getType() === 'WEB_DATA_STREAM'; } ); } /** * Parses a response, adding the _id and _propertyID params and converting to an array keyed by the propertyID and web datastream IDs. * * @since 1.39.0 * * @param GoogleAnalyticsAdminV1betaListDataStreamsResponse[] $batch_response Array of GoogleAnalyticsAdminV1betaListWebDataStreamsResponse objects. * @return stdClass[] Array of models containing _id and _propertyID attributes, keyed by the propertyID. */ public static function parse_webdatastreams_batch( $batch_response ) { $mapped = array(); foreach ( $batch_response as $response ) { if ( $response instanceof Exception ) { continue; } $webdatastreams = self::filter_web_datastreams( $response->getDataStreams() ); foreach ( $webdatastreams as $webdatastream ) { $value = self::filter_webdatastream_with_ids( $webdatastream ); $key = $value->_propertyID; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase $mapped[ $key ] = isset( $mapped[ $key ] ) ? $mapped[ $key ] : array(); $mapped[ $key ][] = $value; } } return $mapped; } /** * Normalizes account ID and returns it. * * @since 1.31.0 * * @param string $account_id Account ID. * @return string Updated account ID with "accounts/" prefix. */ public static function normalize_account_id( $account_id ) { return 'accounts/' . $account_id; } /** * Normalizes property ID and returns it. * * @since 1.31.0 * * @param string $property_id Property ID. * @return string Updated property ID with "properties/" prefix. */ public static function normalize_property_id( $property_id ) { return 'properties/' . $property_id; } /** * Checks if the current user has access to the current configured service entity. * * @since 1.70.0 * * @return boolean|WP_Error */ public function check_service_entity_access() { /* @var Google_Service_GoogleAnalyticsAdmin $analyticsadmin phpcs:ignore Squiz.PHP.CommentedOutCode.Found */ $analyticsadmin = $this->get_service( 'analyticsadmin' ); $settings = $this->settings->get(); try { $analyticsadmin ->properties_dataStreams // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase ->listPropertiesDataStreams( self::normalize_property_id( $settings['propertyID'] ) ); } catch ( Exception $e ) { if ( $e->getCode() === 403 ) { return false; } return $this->exception_to_error( $e ); } return true; } } Optimize/Web_Tag.php000064400000003256150515556660010411 0ustar00get_method_proxy_once( 'render' ), 1 ); $this->do_init_tag_action(); } /** * Outputs the Optimize anti-flicker script tag. * * @since 1.39.0 */ protected function render() { $anti_flicker_script = sprintf( "(function(a,s,y,n,c,h,i,d,e){s.className+=' '+y;h.start=1*new Date; h.end=i=function(){s.className=s.className.replace(RegExp(' ?'+y),'')}; (a[n]=a[n]||[]).hide=h;setTimeout(function(){i();h.end=null},c);h.timeout=c; })(window,document.documentElement,'async-hide','dataLayer',4000, {'%s':true});", esc_js( $this->tag_id ) ); printf( "\n\n", esc_html__( 'Anti-flicker snippet added by Site Kit', 'google-site-kit' ) ); echo ''; BC_Functions::wp_print_inline_script_tag( $anti_flicker_script ); printf( "\n\n", esc_html__( 'End Anti-flicker snippet added by Site Kit', 'google-site-kit' ) ); } } Optimize/Settings.php000064400000005200150515556660010670 0ustar00register_legacy_keys_migration( array( 'AMPExperimentJson' => 'ampExperimentJSON', 'ampExperimentJson' => 'ampExperimentJSON', 'optimize_id' => 'optimizeID', 'optimizeId' => 'optimizeID', ) ); $this->register_owned_keys(); add_filter( 'option_' . self::OPTION, function ( $option ) { // Migrate legacy values where this was saved as decoded JSON object. if ( is_array( $option ) && array_key_exists( 'ampExperimentJSON', $option ) && ! is_string( $option['ampExperimentJSON'] ) ) { if ( empty( $option['ampExperimentJSON'] ) ) { $option['ampExperimentJSON'] = ''; } else { $option['ampExperimentJSON'] = wp_json_encode( $option['ampExperimentJSON'] ); } } return $option; } ); } /** * Returns keys for owned settings. * * @since 1.16.0 * * @return array An array of keys for owned settings. */ public function get_owned_keys() { return array( 'optimizeID', ); } /** * Gets the default value. * * @since 1.2.0 * * @return array */ protected function get_default() { return array( 'ownerID' => 0, 'ampExperimentJSON' => '', 'optimizeID' => '', 'placeAntiFlickerSnippet' => false, ); } /** * Gets the callback for sanitizing the setting's value before saving. * * @since 1.39.0 * * @return callable|null */ protected function get_sanitize_callback() { return function( $option ) { if ( is_array( $option ) ) { if ( isset( $option['placeAntiFlickerSnippet'] ) ) { $option['placeAntiFlickerSnippet'] = (bool) $option['placeAntiFlickerSnippet']; } } return $option; }; } } Optimize/Tag_Guard.php000064400000001660150515556660010733 0ustar00settings->get(); if ( ! isset( $settings['placeAntiFlickerSnippet'] ) ) { return false; } return $settings['placeAntiFlickerSnippet']; } } PageSpeed_Insights/Settings.php000064400000001363150515556660012603 0ustar00 0 ); } } AdSense/Web_Tag.php000064400000005361150515556660010132 0ustar00get_method_proxy_once( 'render' ) ); add_filter( 'wp_resource_hints', $this->get_dns_prefetch_hints_callback( '//pagead2.googlesyndication.com' ), 10, 2 ); $this->do_init_tag_action(); } /** * Outputs the AdSense script tag. * * @since 1.24.0 */ protected function render() { // If we haven't completed the account connection yet, we still insert the AdSense tag // because it is required for account verification. $adsense_script_src = sprintf( 'https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=%s&host=%s', esc_attr( $this->tag_id ), // Site owner's web property code. 'ca-host-pub-2644536267352236' // SiteKit's web property code. ); $adsense_script_attributes = array( 'async' => true, 'src' => $adsense_script_src, 'crossorigin' => 'anonymous', ); $adsense_attributes = $this->get_tag_blocked_on_consent_attribute_array(); $auto_ads_opt = array(); $auto_ads_opt_filtered = apply_filters( 'googlesitekit_auto_ads_opt', $auto_ads_opt, $this->tag_id ); if ( is_array( $auto_ads_opt_filtered ) && ! empty( $auto_ads_opt_filtered ) ) { $strip_attributes = array( 'google_ad_client' => '', 'enable_page_level_ads' => '', ); $auto_ads_opt_filtered = array_diff_key( $auto_ads_opt_filtered, $strip_attributes ); $auto_ads_opt_sanitized = array(); foreach ( $auto_ads_opt_filtered as $key => $value ) { $new_key = 'data-'; $new_key .= str_replace( '_', '-', $key ); $auto_ads_opt_sanitized[ $new_key ] = $value; } $adsense_attributes = array_merge( $adsense_attributes, $auto_ads_opt_sanitized ); } printf( "\n\n", esc_html__( 'Google AdSense snippet added by Site Kit', 'google-site-kit' ) ); BC_Functions::wp_print_script_tag( array_merge( $adsense_script_attributes, $adsense_attributes ) ); printf( "\n\n", esc_html__( 'End Google AdSense snippet added by Site Kit', 'google-site-kit' ) ); } } AdSense/Auto_Ad_Guard.php000064400000002226150515556660011255 0ustar00settings->get(); if ( ! isset( $settings['autoAdsDisabled'] ) ) { return true; } if ( ( in_array( 'loggedinUsers', $settings['autoAdsDisabled'], true ) && is_user_logged_in() ) || ( in_array( 'contentCreators', $settings['autoAdsDisabled'], true ) && current_user_can( 'edit_posts' ) ) ) { return false; } return true; } } AdSense/Settings.php000064400000011611150515556660010415 0ustar00 array( 'accountStatus' => 'approved', 'siteStatus' => 'added', ), 'account-connected-nonmatching' => array( 'accountStatus' => 'approved', 'siteStatus' => 'added', ), 'account-connected-no-data' => array( 'accountStatus' => 'approved', 'siteStatus' => 'added', ), 'account-pending-review' => array( 'accountStatus' => 'approved', 'siteStatus' => 'none', ), 'account-required-action' => array( 'accountStatus' => 'no-client', ), 'disapproved-account-afc' => array( 'accountStatus' => 'no-client', ), 'ads-display-pending' => array( 'accountStatus' => 'pending', ), 'disapproved-account' => array( 'accountStatus' => 'disapproved', ), 'no-account' => array( 'accountStatus' => 'none', ), 'no-account-tag-found' => array( 'accountStatus' => 'none', ), ); /** * Registers the setting in WordPress. * * @since 1.2.0 */ public function register() { parent::register(); $this->register_legacy_keys_migration( array( 'account_id' => 'accountID', 'accountId' => 'accountID', 'account_status' => 'accountStatus', 'adsenseTagEnabled' => 'useSnippet', 'client_id' => 'clientID', 'clientId' => 'clientID', 'setup_complete' => 'setupComplete', ) ); $this->register_owned_keys(); add_filter( 'option_' . self::OPTION, function ( $option ) { /** * Filters the AdSense account ID to use. * * @since 1.0.0 * * @param string $account_id Empty by default, will fall back to the option value if not set. */ $account_id = apply_filters( 'googlesitekit_adsense_account_id', '' ); if ( $account_id ) { $option['accountID'] = $account_id; } // Migrate legacy account statuses (now split into account status and site status). if ( ! empty( $option['accountStatus'] ) && isset( $this->legacy_account_statuses[ $option['accountStatus'] ] ) ) { foreach ( $this->legacy_account_statuses[ $option['accountStatus'] ] as $key => $value ) { $option[ $key ] = $value; } } // Migration of legacy setting. if ( ! empty( $option['setupComplete'] ) ) { $option['accountSetupComplete'] = $option['setupComplete']; $option['siteSetupComplete'] = $option['setupComplete']; } unset( $option['setupComplete'] ); return $option; } ); } /** * Returns keys for owned settings. * * @since 1.16.0 * * @return array An array of keys for owned settings. */ public function get_owned_keys() { return array( 'accountID', 'clientID', ); } /** * Gets the default value. * * @since 1.2.0 * * @return array */ protected function get_default() { return array( 'ownerID' => 0, 'accountID' => '', 'autoAdsDisabled' => array(), 'clientID' => '', 'accountStatus' => '', 'siteStatus' => '', 'accountSetupComplete' => false, 'siteSetupComplete' => false, 'useSnippet' => true, 'webStoriesAdUnit' => '', ); } /** * Gets the callback for sanitizing the setting's value before saving. * * @since 1.6.0 * * @return callable|null */ protected function get_sanitize_callback() { return function( $option ) { if ( is_array( $option ) ) { if ( isset( $option['accountSetupComplete'] ) ) { $option['accountSetupComplete'] = (bool) $option['accountSetupComplete']; } if ( isset( $option['siteStatusComplete'] ) ) { $option['siteStatusComplete'] = (bool) $option['siteStatusComplete']; } if ( isset( $option['useSnippet'] ) ) { $option['useSnippet'] = (bool) $option['useSnippet']; } if ( isset( $option['autoAdsDisabled'] ) ) { $option['autoAdsDisabled'] = (array) $option['autoAdsDisabled']; } } return $option; }; } } AdSense/AMP_Tag.php000064400000013140150515556660010024 0ustar00get_method_proxy( 'render_story_auto_ads' ) ); } else { // For AMP Native and Transitional (if `wp_body_open` supported). add_action( 'wp_body_open', $this->get_method_proxy( 'render' ), -9999 ); // For AMP Native and Transitional (as fallback). add_filter( 'the_content', $this->get_method_proxy( 'amp_content_add_auto_ads' ) ); // For AMP Reader (if `amp_post_template_body_open` supported). add_action( 'amp_post_template_body_open', $this->get_method_proxy( 'render' ), -9999 ); // For AMP Reader (as fallback). add_action( 'amp_post_template_footer', $this->get_method_proxy( 'render' ), -9999 ); // Load amp-auto-ads component for AMP Reader. $this->enqueue_amp_reader_component_script( 'amp-auto-ads', 'https://cdn.ampproject.org/v0/amp-auto-ads-0.1.js' ); } $this->do_init_tag_action(); } /** * Gets the attributes for amp-story-auto-ads and amp-auto-ads tags. * * @since 1.39.0 * * @param string $type Whether it's for web stories. Can be `web-story` or ``. * @return array Filtered $options. */ private function get_auto_ads_attributes( $type = '' ) { $options = array( 'ad-client' => $this->tag_id, ); if ( 'web-story' === $type && ! empty( $this->story_ad_slot_id ) ) { $options['ad-slot'] = $this->story_ad_slot_id; } $filtered_options = 'web-story' === $type ? apply_filters( 'googlesitekit_amp_story_auto_ads_attributes', $options, $this->tag_id, $this->story_ad_slot_id ) : apply_filters( 'googlesitekit_amp_auto_ads_attributes', $options, $this->tag_id, $this->story_ad_slot_id ); if ( is_array( $filtered_options ) && ! empty( $filtered_options ) ) { $options = $filtered_options; $options['ad-client'] = $this->tag_id; } return $options; } /** * Outputs the tag. * * @since 1.24.0 */ protected function render() { if ( $this->adsense_tag_printed ) { return; } $this->adsense_tag_printed = true; $attributes = ''; foreach ( $this->get_auto_ads_attributes() as $amp_auto_ads_opt_key => $amp_auto_ads_opt_value ) { $attributes .= sprintf( ' data-%s="%s"', esc_attr( $amp_auto_ads_opt_key ), esc_attr( $amp_auto_ads_opt_value ) ); } printf( "\n\n", esc_html__( 'Google AdSense AMP snippet added by Site Kit', 'google-site-kit' ) ); printf( '', $attributes, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped $this->get_tag_blocked_on_consent_attribute() // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ); printf( "\n\n", esc_html__( 'End Google AdSense AMP snippet added by Site Kit', 'google-site-kit' ) ); } /** * Adds the AMP auto ads tag if opted in. * * @since 1.24.0 * * @param string $content The page content. * @return string Filtered $content. */ private function amp_content_add_auto_ads( $content ) { // Only run for the primary application of the `the_content` filter. if ( $this->adsense_tag_printed || ! in_the_loop() ) { return $content; } $this->adsense_tag_printed = true; $snippet_comment_begin = sprintf( "\n\n", esc_html__( 'Google AdSense AMP snippet added by Site Kit', 'google-site-kit' ) ); $snippet_comment_end = sprintf( "\n\n", esc_html__( 'End Google AdSense AMP snippet added by Site Kit', 'google-site-kit' ) ); $tag = sprintf( '', esc_attr( $this->tag_id ), $this->get_tag_blocked_on_consent_attribute() ); return $snippet_comment_begin . $tag . $snippet_comment_end . $content; } /** * Set Web Story Ad Slot ID * * @since 1.27.0 * * @param string $ad_slot_id The Ad Slot ID. */ public function set_story_ad_slot_id( $ad_slot_id ) { $this->story_ad_slot_id = $ad_slot_id; } /** * Adds the AMP Web Story auto ads code if enabled. * * @since 1.27.0 */ private function render_story_auto_ads() { $config = array( 'ad-attributes' => array( 'type' => 'adsense', ), ); $attributes = array(); foreach ( $this->get_auto_ads_attributes( 'web-story' ) as $key => $value ) { $attributes[ 'data-' . $key ] = $value; } $config['ad-attributes'] = array_merge( $config['ad-attributes'], $attributes ); printf( "\n\n", esc_html__( 'Google AdSense AMP snippet added by Site Kit', 'google-site-kit' ) ); printf( '', wp_json_encode( $config ) ); printf( "\n\n", esc_html__( 'End Google AdSense AMP snippet added by Site Kit', 'google-site-kit' ) ); } } AdSense/Tag_Guard.php000064400000002271150515556660010454 0ustar00settings->get(); // For web stories, the tag must only be rendered if a story-specific ad unit is provided. if ( is_singular( 'web-story' ) && empty( $settings['webStoriesAdUnit'] ) ) { return false; } return ! empty( $settings['useSnippet'] ) && ! empty( $settings['clientID'] ); } } Idea_Hub.php000064400000077514150515556660006751 0ustar00 self::TRANSIENT_NEW_IDEAS, 'saved-ideas' => self::TRANSIENT_SAVED_IDEAS, ); /** * Post_Idea_Name instance. * * @since 1.32.0 * @var Post_Idea_Name */ private $post_name_setting; /** * Post_Idea_Text instance. * * @since 1.32.0 * @var Post_Idea_Text */ private $post_text_setting; /** * Post_Idea_Topics instance. * * @since 1.32.0 * @var Post_Idea_Topics */ private $post_topic_setting; /** * Transients instance. * * @since 1.40.0 * @var Transients */ private $transients; /** * Idea_Interaction_Count instance. * * @since 1.42.0 * @var Idea_Interaction_Count */ private $interaction_count; /** * Constructor. * * @since 1.38.0 * * @param Context $context Plugin context. * @param Options $options Optional. Option API instance. Default is a new instance. * @param User_Options $user_options Optional. User Option API instance. Default is a new instance. * @param Authentication $authentication Optional. Authentication instance. Default is a new instance. * @param Assets $assets Optional. Assets API instance. Default is a new instance. */ public function __construct( Context $context, Options $options = null, User_Options $user_options = null, Authentication $authentication = null, Assets $assets = null ) { parent::__construct( $context, $options, $user_options, $authentication, $assets ); $post_meta = new Post_Meta(); $this->post_name_setting = new Post_Idea_Name( $post_meta ); $this->post_text_setting = new Post_Idea_Text( $post_meta ); $this->post_topic_setting = new Post_Idea_Topics( $post_meta ); $this->transients = new Transients( $this->context ); $this->interaction_count = new Idea_Interaction_Count( $this->user_options ); } /** * Registers functionality through WordPress hooks. * * @since 1.38.0 */ public function register_persistent() { /** * Changes the posts view to have a custom label in place of Draft for Idea Hub Drafts. */ add_filter( 'display_post_states', function( $post_states, $post ) { if ( ! $this->is_idea_post( $post->ID ) ) { return $post_states; } $idea = $this->get_post_idea( $post->ID ); if ( '' === $post->post_title && 'draft' === $post->post_status ) { /* translators: %s: Idea Hub Idea Title */ $post_states['draft'] = sprintf( __( 'Idea Hub Draft “%s”', 'google-site-kit' ), $idea['text'] ); } else { $post_states['idea-hub'] = __( 'inspired by Idea Hub', 'google-site-kit' ); } return $post_states; }, 10, 2 ); /** * Allows us to trash / modify empty idea posts. */ add_filter( 'wp_insert_post_empty_content', function( $maybe_empty, $postarr ) { if ( isset( $postarr['ID'] ) && $this->is_idea_post( $postarr['ID'] ) ) { return false; } return $maybe_empty; }, 10, 2 ); } /** * Registers functionality through WordPress hooks. * * @since 1.32.0 */ public function register() { $this->register_scopes_hook(); if ( $this->is_connected() ) { /** * Show admin notices on the posts page if we have saved / new ideas. */ add_filter( 'googlesitekit_admin_notices', $this->get_method_proxy( 'admin_notice_idea_hub_ideas' ) ); /** * Adds a special class name to idea posts. */ add_filter( 'post_class', $this->get_method_proxy( 'update_post_classes' ), 10, 3 ); add_action( 'admin_footer-edit.php', function() { $screen = get_current_screen(); if ( ! is_null( $screen ) && 'post' === $screen->post_type ) { echo '
'; } } ); add_action( 'before_delete_post', function( $post_id ) { $this->track_idea_activity( $post_id, self::ACTIVITY_POST_DELETED ); } ); /** * Watches for Idea Hub post state changes. */ add_action( 'transition_post_status', $this->get_method_proxy( 'on_idea_hub_post_status_transition' ), 10, 3 ); } $this->post_name_setting->register(); $this->post_text_setting->register(); $this->post_topic_setting->register(); $this->interaction_count->register(); } /** * Shows admin notification for idea hub ideas on post list screen. * * @since 1.38.0 * * @param array $notices Array of admin notices. * @return array Array of admin notices. */ private function admin_notice_idea_hub_ideas( $notices ) { $screen = get_current_screen(); if ( is_null( $screen ) || 'edit-post' !== $screen->id || 'post' !== $screen->post_type ) { return $notices; } $dismissed_items = new Dismissed_Items( $this->user_options ); $escape_and_wrap_notice_content = function( $message ) { $message = wp_kses( $message, array( 'a' => array( 'href' => array(), ), ) ); return '

' . $message . '

'; }; $notices[] = new Notice( self::SLUG_SAVED_IDEAS, array( 'content' => function() use ( $escape_and_wrap_notice_content ) { $message_body = sprintf( /* translators: %s: URL to saved ideas */ __( 'Want some inspiration for a new post? Revisit your saved ideas in Site Kit.', 'google-site-kit' ), esc_url( $this->context->admin_url( 'dashboard', array( 'idea-hub-tab' => 'saved-ideas' ) ) ) ); $message = sprintf( /* translators: 1: Plugin name. 2: Message. */ __( '%1$s: %2$s', 'google-site-kit' ), __( 'Site Kit by Google', 'google-site-kit' ), $message_body ); return $escape_and_wrap_notice_content( $message ); }, 'type' => Notice::TYPE_INFO, 'active_callback' => function() use ( $dismissed_items ) { try { $saved_ideas = $this->get_cached_ideas( 'saved-ideas' ); $has_saved_ideas = ! empty( $saved_ideas ); if ( ! $has_saved_ideas && $dismissed_items->is_dismissed( self::SLUG_SAVED_IDEAS ) ) { // Saved items no longer need to be dismissed as there are none currently. $dismissed_items->add( self::SLUG_SAVED_IDEAS, -1 ); } if ( $dismissed_items->is_dismissed( self::SLUG_SAVED_IDEAS ) ) { return false; } return $has_saved_ideas; } catch ( Exception $exception ) { return false; } }, 'dismissible' => true, ) ); $notices[] = new Notice( self::SLUG_NEW_IDEAS, array( 'content' => function() use ( $escape_and_wrap_notice_content ) { $message_body = sprintf( /* translators: %s: URL to saved ideas */ __( 'Want some inspiration for a new post? Review your new ideas in Site Kit.', 'google-site-kit' ), esc_url( $this->context->admin_url( 'dashboard', array( 'idea-hub-tab' => 'new-ideas' ) ) ) ); $message = sprintf( /* translators: 1: Plugin name. 2: Message. */ __( '%1$s: %2$s', 'google-site-kit' ), __( 'Site Kit by Google', 'google-site-kit' ), $message_body ); return $escape_and_wrap_notice_content( $message ); }, 'type' => Notice::TYPE_INFO, 'active_callback' => function() use ( $dismissed_items ) { if ( $dismissed_items->is_dismissed( self::SLUG_NEW_IDEAS ) || $dismissed_items->is_dismissed( self::SLUG_SAVED_IDEAS ) ) { return false; } try { $saved_ideas = $this->get_cached_ideas( 'saved-ideas' ); if ( ! empty( $saved_ideas ) ) { // Don't show new ideas notice if there are saved ideas, // irrespective of whether we show them the saved ideas notice. return false; } $new_ideas = $this->get_cached_ideas( 'new-ideas' ); return ! empty( $new_ideas ); } catch ( Exception $exception ) { return false; } }, 'dismissible' => true, ) ); return $notices; } /** * Gets ideas from cache. * * If cache has expired, ideas will be fetched and cached if successful. * * @since 1.50.0 * * @param string $datapoint The datapoint to fetch ideas from. * @return array Cached or freshly cached ideas. * @throws InvalidArgumentException Thrown if invalid datapoint is given. * @throws RuntimeException Thrown if get_data returns a WP_Error. */ protected function get_cached_ideas( $datapoint ) { if ( empty( self::DATAPOINT_TRANSIENT_MAP[ $datapoint ] ) ) { throw new InvalidArgumentException( "Invalid datapoint $datapoint" ); } $transient = self::DATAPOINT_TRANSIENT_MAP[ $datapoint ]; $ideas = $this->transients->get( $transient ); if ( is_array( $ideas ) ) { return $ideas; } $ideas = $this->get_data( $datapoint ); if ( is_wp_error( $ideas ) ) { throw new RuntimeException( $ideas->get_error_message() ); } $this->transients->set( $transient, $ideas, HOUR_IN_SECONDS ); return $ideas; } /** * Gets required Google OAuth scopes for the module. * * @since 1.32.0 * * @return array List of Google OAuth scopes. */ public function get_scopes() { return array( 'https://www.googleapis.com/auth/ideahub.read', 'https://www.googleapis.com/auth/ideahub.full', ); } /** * Checks whether the module is connected. * * A module being connected means that all steps required as part of its activation are completed. * * @since 1.32.0 * * @return bool True if module is connected, false otherwise. */ public function is_connected() { $required_keys = array( 'tosAccepted' ); $options = $this->get_settings()->get(); foreach ( $required_keys as $required_key ) { if ( empty( $options[ $required_key ] ) ) { return false; } } return parent::is_connected(); } /** * Cleans up when the module is deactivated. * * @since 1.32.0 */ public function on_deactivation() { $this->get_settings()->delete(); $this->transients->delete( self::TRANSIENT_NEW_IDEAS ); $this->transients->delete( self::TRANSIENT_SAVED_IDEAS ); } /** * Gets an array of debug field definitions. * * @since 1.32.0 * * @return array */ public function get_debug_fields() { $settings = $this->get_settings()->get(); return array(); } /** * Gets map of datapoint to definition data for each. * * @since 1.32.0 * * @return array Map of datapoints to their definitions. */ protected function get_datapoint_definitions() { return array( 'POST:create-idea-draft-post' => array( 'service' => '' ), 'GET:draft-post-ideas' => array( 'service' => '', 'shareable' => Feature_Flags::enabled( 'dashboardSharing' ), ), 'GET:new-ideas' => array( 'service' => 'ideahub', 'shareable' => Feature_Flags::enabled( 'dashboardSharing' ), ), 'GET:published-post-ideas' => array( 'service' => '', 'shareable' => Feature_Flags::enabled( 'dashboardSharing' ), ), 'GET:saved-ideas' => array( 'service' => 'ideahub', 'shareable' => Feature_Flags::enabled( 'dashboardSharing' ), ), 'POST:update-idea-state' => array( 'service' => 'ideahub' ), ); } /** * Creates a request object for the given datapoint. * * @since 1.32.0 * * @param Data_Request $data Data request object. * @return RequestInterface|callable|WP_Error Request object or callable on success, or WP_Error on failure. * * @throws Invalid_Datapoint_Exception Thrown if the datapoint does not exist. */ protected function create_data_request( Data_Request $data ) { switch ( "{$data->method}:{$data->datapoint}" ) { case 'POST:create-idea-draft-post': $expected_parameters = array( 'name' => 'string', 'text' => 'string', 'topics' => 'array', ); if ( ! isset( $data['idea'] ) ) { return new WP_Error( 'missing_required_param', /* translators: %s: Missing parameter name */ sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'idea' ), array( 'status' => 400 ) ); } $idea = $data['idea']; foreach ( $expected_parameters as $parameter_name => $expected_parameter_type ) { if ( ! isset( $idea[ $parameter_name ] ) ) { return new WP_Error( 'missing_required_param', /* translators: %s: Missing parameter name */ sprintf( __( 'Request idea parameter is empty: %s.', 'google-site-kit' ), $parameter_name ), array( 'status' => 400 ) ); } $parameter_type = gettype( $idea[ $parameter_name ] ); if ( $parameter_type !== $expected_parameter_type ) { return new WP_Error( 'wrong_parameter_type', sprintf( /* translators: 1: parameter name, 2: expected type, 3: received type */ __( 'Wrong parameter type for %1$s, expected %2$s, received %3$s', 'google-site-kit' ), $parameter_name, $expected_parameter_type, $parameter_type ), array( 'status' => 400 ) ); } } return function() use ( $idea ) { // Allows us to create a blank post. add_filter( 'wp_insert_post_empty_content', '__return_false' ); $post_id = wp_insert_post( array(), false ); remove_filter( 'wp_insert_post_empty_content', '__return_false' ); if ( 0 === $post_id ) { return new WP_Error( 'unable_to_draft_post', __( 'Unable to draft post.', 'google-site-kit' ), array( 'status' => 400 ) ); } $this->set_post_idea( $post_id, $idea ); $this->track_idea_activity( $post_id, self::ACTIVITY_POST_DRAFTED ); $this->transients->delete( self::TRANSIENT_SAVED_IDEAS ); $this->transients->delete( self::TRANSIENT_NEW_IDEAS ); return $post_id; }; case 'GET:draft-post-ideas': return function() { return $this->query_idea_posts( 'draft' ); }; case 'GET:new-ideas': return $this->fetch_ideas( 'new' ); case 'GET:published-post-ideas': return function() { $statuses = array( 'publish', 'future', 'private' ); return $this->query_idea_posts( $statuses ); }; case 'GET:saved-ideas': return $this->fetch_ideas( 'saved' ); case 'POST:update-idea-state': if ( ! isset( $data['name'] ) ) { return new WP_Error( 'missing_required_param', /* translators: %s: Missing parameter name */ sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'name' ), array( 'status' => 400 ) ); } if ( ! isset( $data['saved'] ) && ! isset( $data['dismissed'] ) ) { return new WP_Error( 'missing_required_param', __( 'Either "saved" or "dismissed" parameter must be provided.', 'google-site-kit' ), array( 'status' => 400 ) ); } $idea_name = $data['name']; $idea_name_parts = explode( '/', $data['name'] ); $parent = $this->get_parent_slug(); $parent = sprintf( '%s/ideaStates/%s', untrailingslashit( $parent ), array_pop( $idea_name_parts ) ); $update_mask = array(); $body = new Google_Service_Ideahub_GoogleSearchIdeahubV1betaIdeaState(); $body->setName( $idea_name ); if ( isset( $data['saved'] ) ) { $body->setSaved( filter_var( $data['saved'], FILTER_VALIDATE_BOOLEAN ) ); $update_mask[] = 'saved'; } if ( isset( $data['dismissed'] ) ) { $body->setDismissed( filter_var( $data['dismissed'], FILTER_VALIDATE_BOOLEAN ) ); $update_mask[] = 'dismissed'; } $params = array( 'updateMask' => implode( ',', $update_mask ), ); return $this->get_service( 'ideahub' )->platforms_properties_ideaStates->patch( $parent, $body, $params ); } return parent::create_data_request( $data ); } /** * Parses a response for the given datapoint. * * @since 1.34.0 * * @param Data_Request $data Data request object. * @param mixed $response Request response. * * @return mixed Parsed response data on success, or WP_Error on failure. */ protected function parse_data_response( Data_Request $data, $response ) { $filter_draft_post_response = function( $post_id ) { return array_merge( array( 'postID' => $post_id, 'postEditURL' => get_edit_post_link( $post_id, null ), ), $this->get_post_idea( $post_id ) ); }; switch ( "{$data->method}:{$data->datapoint}" ) { case 'POST:create-idea-draft-post': $this->interaction_count->increment(); return $filter_draft_post_response( $response ); case 'GET:draft-post-ideas': return array_filter( array_map( $filter_draft_post_response, is_array( $response ) ? $response : array( $response ) ) ); case 'GET:new-ideas': $ideas = $this->filter_out_ideas_with_posts( $response->getIdeas() ); return array_map( array( self::class, 'filter_idea_with_id' ), $ideas ); case 'GET:published-post-ideas': return array_filter( array_map( function( $post_id ) { return array_merge( array( 'postID' => $post_id, 'postEditURL' => get_edit_post_link( $post_id ), 'postURL' => get_permalink( $post_id ), ), $this->get_post_idea( $post_id ) ); }, is_array( $response ) ? $response : array( $response ) ) ); case 'GET:saved-ideas': $ideas = $this->filter_out_ideas_with_posts( $response->getIdeas() ); return array_map( array( self::class, 'filter_idea_with_id' ), $ideas ); case 'POST:update-idea-state': $this->interaction_count->increment(); $this->transients->delete( self::TRANSIENT_SAVED_IDEAS ); $this->transients->delete( self::TRANSIENT_NEW_IDEAS ); return self::filter_idea_state_with_id( $response ); } return parent::parse_data_response( $data, $response ); } /** * Sets up information about the module. * * @since 1.32.0 * * @return array Associative array of module info. */ protected function setup_info() { return array( 'slug' => self::MODULE_SLUG, 'name' => _x( 'Idea Hub', 'Service name', 'google-site-kit' ), 'description' => __( 'Idea Hub suggests what you can write about next, from actual questions people asked on Google Search', 'google-site-kit' ), 'order' => 7, ); } /** * Sets up the module's settings instance. * * @since 1.32.0 * * @return Module_Settings */ protected function setup_settings() { return new Settings( $this->options ); } /** * Sets up the module's assets to register. * * @since 1.32.0 * * @return Asset[] List of Asset objects. */ protected function setup_assets() { $base_url = $this->context->url( 'dist/assets/' ); return array( new Script( 'googlesitekit-modules-idea-hub', array( 'src' => $base_url . 'js/googlesitekit-modules-idea-hub.js', 'dependencies' => array( 'googlesitekit-vendor', 'googlesitekit-api', 'googlesitekit-data', 'googlesitekit-modules', 'googlesitekit-components', ), ) ), new Script( 'googlesitekit-idea-hub-post-list', array( 'src' => $base_url . 'js/googlesitekit-idea-hub-post-list.js', 'load_contexts' => array( Asset::CONTEXT_ADMIN_POSTS ), 'dependencies' => array( 'googlesitekit-i18n', 'googlesitekit-datastore-location', 'googlesitekit-datastore-ui', 'googlesitekit-datastore-user', 'googlesitekit-modules', 'googlesitekit-components', ), ) ), new Script( 'googlesitekit-idea-hub-notice', array( 'src' => $base_url . 'js/googlesitekit-idea-hub-notice.js', 'dependencies' => array( 'googlesitekit-i18n', 'wp-data', 'wp-api-fetch', 'wp-polyfill', 'wp-url', 'googlesitekit-components', ), 'load_contexts' => array( Asset::CONTEXT_ADMIN_POST_EDITOR ), ) ), new Script_Data( 'googlesitekit-idea-hub-data', array( 'global' => '_googlesitekitIdeaHub', 'data_callback' => function () { return array( 'lastIdeaPostUpdatedAt' => $this->transients->get( self::IDEA_HUB_LAST_CHANGED ), 'interactionCount' => $this->interaction_count->get(), ); }, ) ), ); } /** * Sets up the Google services the module should use. * * This method is invoked once by {@see Module::get_service()} to lazily set up the services when one is requested * for the first time. * * @since 1.40.0 * * @param Google_Site_Kit_Client $client Google client instance. * @return array Google services as $identifier => $service_instance pairs. */ protected function setup_services( Google_Site_Kit_Client $client ) { return array( 'ideahub' => new Google_Service_Ideahub( $client ), ); } /** * Saves post idea settings. * * @since 1.33.0 * * @param int $post_id Post ID. * @param array $idea Idea settings. */ public function set_post_idea( $post_id, array $idea ) { $idea = wp_parse_args( $idea, array( 'name' => '', 'text' => '', 'topics' => array(), ) ); $this->post_name_setting->set( $post_id, $idea['name'] ); $this->post_text_setting->set( $post_id, $idea['text'] ); $this->post_topic_setting->set( $post_id, $idea['topics'] ); } /** * Parses an idea ID, adds it to the model object and returns updated model. * * @since 1.40.0 * * @param Google_Model $idea Idea model. * @return \stdClass Updated model with _id attribute. */ public static function filter_idea_with_id( $idea ) { $obj = $idea->toSimpleObject(); $matches = array(); if ( preg_match( '#ideas/([^/]+)#', $idea['name'], $matches ) ) { $obj->_id = $matches[1]; } return $obj; } /** * Parses an idea state ID, adds it to the model object and returns updated model. * * @since 1.40.0 * * @param Google_Model $idea_state Idea state model. * @return \stdClass Updated model with _id attribute. */ public static function filter_idea_state_with_id( $idea_state ) { $obj = $idea_state->toSimpleObject(); $matches = array(); if ( preg_match( '#platforms/([^/]+)/properties/([^/]+)/ideaStates/([^/]+)#', $idea_state['name'], $matches ) ) { $obj->_id = $matches[3]; } return $obj; } /** * Gets post idea settings. * * @since 1.33.0 * * @param int $post_id Post ID. * @return array|null Post idea settings array. Returns NULL if a post doesn't have an associated idea. */ public function get_post_idea( $post_id ) { $name = $this->post_name_setting->get( $post_id ); $text = $this->post_text_setting->get( $post_id ); $topics = $this->post_topic_setting->get( $post_id ); if ( empty( $name ) || empty( $text ) || empty( $topics ) ) { return null; } return array( 'name' => $name, 'text' => $text, 'topics' => $topics, ); } /** * Checks whether the post is an Idea Hub post. * * @since 1.36.0 * * @param int $post_id Post ID. * @return bool True if the post with supplied ID is an Idea Hub post. */ private function is_idea_post( $post_id ) { return is_array( $this->get_post_idea( $post_id ) ); } /** * Hook to check whether an Idea Hub post status has changed. * * @since 1.40.0 * * @param string $new_status Updated post status. * @param string $old_status Previous post status. * @param WP_Post $post The post in question. */ private function on_idea_hub_post_status_transition( $new_status, $old_status, $post ) { if ( $new_status === $old_status || ! $this->is_idea_post( $post->ID ) ) { return; } $this->transients->set( self::IDEA_HUB_LAST_CHANGED, time() ); if ( 'publish' === $new_status ) { $this->track_idea_activity( $post->ID, self::ACTIVITY_POST_PUBLISHED ); } elseif ( 'publish' === $old_status ) { $this->track_idea_activity( $post->ID, self::ACTIVITY_POST_UNPUBLISHED ); } } /** * Adds .googlesitekit-idea-hub__draft class to idea posts on the posts page. * * @since 1.40.0 * * @param array $classes An array of post class names. * @param array $class An array of additional class names added to the post. * @param int $post_id The post ID. * @return array An array of post class names. */ private function update_post_classes( $classes, $class, $post_id ) { // Do nothing on the frontend. if ( ! is_admin() ) { return $classes; } $screen = get_current_screen(); if ( is_null( $screen ) || 'edit-post' !== $screen->id || 'post' !== $screen->post_type ) { return $classes; } if ( $this->is_idea_post( $post_id ) ) { $classes[] = 'googlesitekit-idea-hub__post'; if ( ! wp_style_is( 'googlesitekit-admin-css' ) ) { $this->assets->enqueue_asset( 'googlesitekit-admin-css' ); } } return $classes; } /** * Gets the parent slug to use for Idea Hub API requests. * * @since 1.40.0 * * @return string Parent slug. */ private function get_parent_slug() { $reference_url = $this->context->get_reference_site_url(); $reference_url = rawurlencode( $reference_url ); return "platforms/sitekit/properties/{$reference_url}"; } /** * Pulls posts created for an idea from the database. * * @since 1.40.0 * * @param string|array $post_status Post status or statuses. * @return array An array of post IDs. */ private function query_idea_posts( $post_status ) { $wp_query = new \WP_Query(); return $wp_query->query( array( 'fields' => 'ids', 'post_status' => $post_status, 'posts_per_page' => 500, // phpcs:ignore WordPress.WP.PostsPerPage.posts_per_page_posts_per_page 'no_found_rows' => true, 'update_post_term_cache' => false, 'order' => 'DESC', 'orderby' => 'ID', 'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query array( 'key' => Post_Idea_Name::META_KEY, 'compare' => 'EXISTS', ), ), ) ); } /** * Fetches ideas from the Idea Hub API. * * @since 1.40.0 * * @param string $type Ideas type. Valid values "saved", "new" or an empty string which means all ideas. * @return mixed List ideas request. */ private function fetch_ideas( $type ) { $parent = $this->get_parent_slug(); $params = array( 'pageSize' => 100, ); if ( 'saved' === $type ) { $params['filter'] = 'saved(true)'; } elseif ( 'new' === $type ) { $params['filter'] = 'saved(false)'; } return $this->get_service( 'ideahub' ) ->platforms_properties_ideas ->listPlatformsPropertiesIdeas( $parent, $params ); } /** * Filters out ideas for which we have already created a post. * * @since 1.40.0 * * @param array $ideas Ideas list to filter. * @return array Filtered ideas list. */ private function filter_out_ideas_with_posts( $ideas ) { if ( empty( $ideas ) ) { return $ideas; } $names = wp_list_pluck( $ideas, 'name' ); $statuses = array( 'publish', 'pending', 'draft', 'future', 'private' ); $posts = $this->query_idea_posts( $statuses ); if ( empty( $posts ) ) { return $ideas; } $ideas_with_posts = array(); foreach ( $posts as $post_id ) { $idea = $this->get_post_idea( $post_id ); if ( ! empty( $idea['name'] ) ) { $ideas_with_posts[] = $idea['name']; } } $ideas = array_filter( $ideas, function( $idea ) use ( $ideas_with_posts ) { return ! in_array( $idea->getName(), $ideas_with_posts, true ); } ); return array_values( $ideas ); } /** * Tracks an idea activity. * * @since 1.42.0 * * @param int $post_id Post ID. * @param string $type Activity type. */ private function track_idea_activity( $post_id, $type ) { $post = get_post( $post_id ); $name = $this->post_name_setting->get( $post->ID ); if ( empty( $name ) ) { return; } $parent = $this->get_parent_slug(); $activity = new Google_Service_Ideahub_GoogleSearchIdeahubV1betaIdeaActivity(); $activity->setIdeas( array( $name ) ); $activity->setTopics( array() ); $activity->setType( $type ); if ( 'publish' === $post->post_status ) { $uri = get_permalink( $post ); if ( ! empty( $uri ) ) { $activity->setUri( $uri ); } } try { $this->get_service( 'ideahub' ) ->platforms_properties_ideaActivities ->create( $parent, $activity ); } catch ( Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch // Do nothing. } } } Optimize.php000064400000016464150515556660007106 0ustar00gtag_config_add_optimize_id( $gtag_config ); } ); $print_amp_optimize_experiment = function() { $this->print_amp_optimize_experiment(); }; add_action( 'wp_footer', $print_amp_optimize_experiment ); // For AMP Native and Transitional. add_action( 'amp_post_template_footer', $print_amp_optimize_experiment ); // For AMP Reader. add_filter( // Load amp-experiment component for AMP Reader. 'amp_post_template_data', function( $data ) { return $this->amp_data_load_experiment_component( $data ); } ); // Optimize tag placement logic. add_action( 'template_redirect', $this->get_method_proxy( 'register_tag' ) ); } /** * Checks whether the module is connected. * * A module being connected means that all steps required as part of its activation are completed. * * @since 1.0.0 * * @return bool True if module is connected, false otherwise. */ public function is_connected() { $settings = $this->get_settings()->get(); if ( ! $settings['optimizeID'] ) { return false; } return parent::is_connected(); } /** * Cleans up when the module is deactivated. * * @since 1.0.0 */ public function on_deactivation() { $this->get_settings()->delete(); } /** * Expands gtag config options with Optimize Container ID. * * @since 1.0.0 * * @param array $gtag_config Associative array of gtag config options. * @return array Filtered $gtag_config. */ protected function gtag_config_add_optimize_id( $gtag_config ) { $settings = $this->get_settings()->get(); if ( $settings['optimizeID'] ) { $gtag_config['optimize_id'] = $settings['optimizeID']; } return $gtag_config; } /** * Outputs Optimize experiment script in AMP if opted in. * * @since 1.0.0 */ protected function print_amp_optimize_experiment() { if ( ! $this->context->is_amp() ) { return; } $settings = $this->get_settings()->get(); if ( ! $settings['ampExperimentJSON'] ) { return; } ?> get_settings()->get(); return array( 'optimize_id' => array( 'label' => __( 'Optimize Container ID', 'google-site-kit' ), 'value' => $settings['optimizeID'], 'debug' => Debug_Data::redact_debug_value( $settings['optimizeID'], 7 ), ), ); } /** * Adds AMP experiment script if opted in. * * @since 1.0.0 * * @param array $data AMP template data. * @return array Filtered $data. */ protected function amp_data_load_experiment_component( $data ) { $settings = $this->get_settings()->get(); if ( $settings['ampExperimentJSON'] ) { $data['amp_component_scripts']['amp-experiment'] = 'https://cdn.ampproject.org/v0/amp-experiment-0.1.js'; } return $data; } /** * Sets up information about the module. * * @since 1.0.0 * * @return array Associative array of module info. */ protected function setup_info() { return array( 'slug' => 'optimize', 'name' => _x( 'Optimize', 'Service name', 'google-site-kit' ), 'description' => __( 'Create free A/B tests that help you drive metric-based design solutions to your site', 'google-site-kit' ), 'order' => 5, 'homepage' => __( 'https://optimize.google.com/optimize/home/', 'google-site-kit' ), 'depends_on' => array( 'analytics' ), ); } /** * Sets up the Google services the module should use. * * This method is invoked once by {@see Module::get_service()} to lazily set up the services when one is requested * for the first time. * * @since 1.0.0 * @since 1.2.0 Now requires Google_Site_Kit_Client instance. * * @param Google_Site_Kit_Client $client Google client instance. * @return array Google services as $identifier => $service_instance pairs. Every $service_instance must be an * instance of Google_Service. */ protected function setup_services( Google_Site_Kit_Client $client ) { return array(); } /** * Sets up the module's settings instance. * * @since 1.2.0 * * @return Module_Settings */ protected function setup_settings() { return new Settings( $this->options ); } /** * Sets up the module's assets to register. * * @since 1.10.0 * * @return Asset[] List of Asset objects. */ protected function setup_assets() { $base_url = $this->context->url( 'dist/assets/' ); return array( new Script( 'googlesitekit-modules-optimize', array( 'src' => $base_url . 'js/googlesitekit-modules-optimize.js', 'dependencies' => array( 'googlesitekit-vendor', 'googlesitekit-api', 'googlesitekit-data', 'googlesitekit-modules', 'googlesitekit-datastore-site', 'googlesitekit-datastore-forms', 'googlesitekit-components', ), ) ), ); } /** * Registers the Optimize tag. * * @since 1.39.0 */ private function register_tag() { $is_amp = $this->context->is_amp(); $module_settings = $this->get_settings(); $settings = $module_settings->get(); if ( $is_amp ) { return false; } $tag = new Web_Tag( $settings['optimizeID'], self::MODULE_SLUG ); if ( ! $tag->is_tag_blocked() ) { $tag->use_guard( new Tag_Guard( $module_settings ) ); if ( $tag->can_register() ) { $tag->register(); } } } } Analytics_4/Web_Tag.php000064400000002756150515556660010767 0ustar00get_method_proxy( 'enqueue_gtag_script' ), 20 ); $this->do_init_tag_action(); } /** * Enqueues gtag script. * * @since 1.31.0 */ protected function enqueue_gtag_script() { if ( did_action( 'googlesitekit_analytics_init_tag' ) ) { // If the gtag script is already registered in the Analytics module, then we need to add configuration only. $config = sprintf( 'gtag("config", "%s");', esc_js( $this->tag_id ) ); wp_add_inline_script( 'google_gtagjs', $config ); } else { // Otherwise register gtag as in the Analytics module knowing that we used Measurement ID from GA4 instead of Property ID. parent::enqueue_gtag_script(); } } } Analytics_4/Settings.php000064400000005273150515556660011254 0ustar00register_owned_keys(); } /** * Returns keys for owned settings. * * @since 1.30.0 * * @return array An array of keys for owned settings. */ public function get_owned_keys() { return array( // TODO: These can be uncommented when Analytics and Analytics 4 modules are officially separated. /* 'accountID', */ // phpcs:ignore Squiz.PHP.CommentedOutCode.Found /* 'adsConversionID', */ // phpcs:ignore Squiz.PHP.CommentedOutCode.Found 'propertyID', 'webDataStreamID', 'measurementID', 'googleTagID', 'googleTagAccountID', 'googleTagContainerID', ); } /** * Gets the default value. * * @since 1.30.0 * * @return array */ protected function get_default() { return array( 'ownerID' => 0, // TODO: These can be uncommented when Analytics and Analytics 4 modules are officially separated. /* 'accountID' => '', */ // phpcs:ignore Squiz.PHP.CommentedOutCode.Found /* 'adsConversionID' => '', */ // phpcs:ignore Squiz.PHP.CommentedOutCode.Found 'propertyID' => '', 'webDataStreamID' => '', 'measurementID' => '', 'useSnippet' => true, 'googleTagID' => '', 'googleTagAccountID' => '', 'googleTagContainerID' => '', ); } /** * Gets the callback for sanitizing the setting's value before saving. * * @since 1.30.0 * * @return callable|null */ protected function get_sanitize_callback() { return function( $option ) { if ( is_array( $option ) ) { if ( isset( $option['useSnippet'] ) ) { $option['useSnippet'] = (bool) $option['useSnippet']; } if ( isset( $option['googleTagID'] ) ) { if ( ! preg_match( '/^(G|GT|AW)-[a-zA-Z0-9]+$/', $option['googleTagID'] ) ) { $option['googleTagID'] = ''; } } } return $option; }; } } Analytics_4/Tag_Guard.php000064400000001557150515556660011312 0ustar00settings->get(); return ! empty( $settings['useSnippet'] ) && ! empty( $settings['measurementID'] ); } } Analytics.php000064400000144516150515556660007235 0ustar00register_scopes_hook(); /** * This filter only exists to be unhooked by the AdSense module if active. * * @see \Google\Site_Kit\Modules\Analytics\Settings::register */ add_filter( 'googlesitekit_analytics_adsense_linked', '__return_false' ); add_action( 'admin_init', $this->get_method_proxy( 'handle_provisioning_callback' ) ); add_action( 'googlesitekit_authorize_user', array( $this, 'handle_token_response_data' ) ); // For non-AMP and AMP. add_action( 'wp_head', $this->get_method_proxy( 'print_tracking_opt_out' ), 0 ); // For Web Stories plugin. add_action( 'web_stories_story_head', $this->get_method_proxy( 'print_tracking_opt_out' ), 0 ); // Analytics tag placement logic. add_action( 'template_redirect', $this->get_method_proxy( 'register_tag' ) ); add_filter( 'googlesitekit_proxy_setup_mode', function( $original_mode ) { return ! $this->is_connected() ? 'analytics-step' : $original_mode; } ); ( new Advanced_Tracking( $this->context ) )->register(); } /** * Checks whether or not tracking snippet should be contextually disabled for this request. * * @since 1.1.0 * * @return bool */ protected function is_tracking_disabled() { $settings = $this->get_settings()->get(); // This filter is documented in Tag_Manager::filter_analytics_allow_tracking_disabled. if ( ! apply_filters( 'googlesitekit_allow_tracking_disabled', $settings['useSnippet'] ) ) { return false; } $option = $this->get_settings()->get(); $disable_logged_in_users = in_array( 'loggedinUsers', $option['trackingDisabled'], true ) && is_user_logged_in(); $disable_content_creators = in_array( 'contentCreators', $option['trackingDisabled'], true ) && current_user_can( 'edit_posts' ); $disabled = $disable_logged_in_users || $disable_content_creators; /** * Filters whether or not the Analytics tracking snippet is output for the current request. * * @since 1.1.0 * * @param $disabled bool Whether to disable tracking or not. */ return (bool) apply_filters( 'googlesitekit_analytics_tracking_disabled', $disabled ); } /** * Gets required Google OAuth scopes for the module. * * @since 1.0.0 * * @return array List of Google OAuth scopes. */ public function get_scopes() { return array( self::READONLY_SCOPE, ); } /** * Checks whether the module is connected. * * A module being connected means that all steps required as part of its activation are completed. * * @since 1.0.0 * * @return bool True if module is connected, false otherwise. */ public function is_connected() { $required_keys = array( 'accountID', 'propertyID', 'profileID', 'internalWebPropertyID', ); $options = $this->get_settings()->get(); foreach ( $required_keys as $required_key ) { if ( empty( $options[ $required_key ] ) ) { return false; } } return parent::is_connected(); } /** * Cleans up when the module is deactivated. * * @since 1.0.0 */ public function on_deactivation() { $this->get_settings()->delete(); $this->options->delete( 'googlesitekit_analytics_adsense_linked' ); } /** * Gets an array of debug field definitions. * * @since 1.5.0 * * @return array */ public function get_debug_fields() { $settings = $this->get_settings()->get(); return array( 'analytics_account_id' => array( 'label' => __( 'Analytics account ID', 'google-site-kit' ), 'value' => $settings['accountID'], 'debug' => Debug_Data::redact_debug_value( $settings['accountID'] ), ), 'analytics_property_id' => array( 'label' => __( 'Analytics property ID', 'google-site-kit' ), 'value' => $settings['propertyID'], 'debug' => Debug_Data::redact_debug_value( $settings['propertyID'], 7 ), ), 'analytics_profile_id' => array( 'label' => __( 'Analytics view ID', 'google-site-kit' ), 'value' => $settings['profileID'], 'debug' => Debug_Data::redact_debug_value( $settings['profileID'] ), ), 'analytics_use_snippet' => array( 'label' => __( 'Analytics snippet placed', 'google-site-kit' ), 'value' => $settings['useSnippet'] ? __( 'Yes', 'google-site-kit' ) : __( 'No', 'google-site-kit' ), 'debug' => $settings['useSnippet'] ? 'yes' : 'no', ), ); } /** * Handles the provisioning callback after the user completes the terms of service. * * @since 1.9.0 */ protected function handle_provisioning_callback() { if ( defined( 'WP_CLI' ) && WP_CLI ) { return; } if ( ! current_user_can( Permissions::MANAGE_OPTIONS ) ) { return; } $input = $this->context->input(); if ( ! $input->filter( INPUT_GET, 'gatoscallback' ) ) { return; } // The handler should check the received Account Ticket id parameter against the id stored in the provisioning step. $account_ticket_id = $input->filter( INPUT_GET, 'accountTicketId', FILTER_SANITIZE_STRING ); $stored_account_ticket_id = get_transient( self::PROVISION_ACCOUNT_TICKET_ID . '::' . get_current_user_id() ); delete_transient( self::PROVISION_ACCOUNT_TICKET_ID . '::' . get_current_user_id() ); if ( $stored_account_ticket_id !== $account_ticket_id ) { wp_safe_redirect( $this->context->admin_url( 'module-analytics', array( 'error_code' => 'account_ticket_id_mismatch' ) ) ); exit; } // Check for a returned error. $error = $input->filter( INPUT_GET, 'error', FILTER_SANITIZE_STRING ); if ( ! empty( $error ) ) { wp_safe_redirect( $this->context->admin_url( 'module-analytics', array( 'error_code' => $error ) ) ); exit; } $account_id = $input->filter( INPUT_GET, 'accountId', FILTER_SANITIZE_STRING ); $web_property_id = $input->filter( INPUT_GET, 'webPropertyId', FILTER_SANITIZE_STRING ); $profile_id = $input->filter( INPUT_GET, 'profileId', FILTER_SANITIZE_STRING ); if ( empty( $account_id ) || empty( $web_property_id ) || empty( $profile_id ) ) { wp_safe_redirect( $this->context->admin_url( 'module-analytics', array( 'error_code' => 'callback_missing_parameter' ) ) ); exit; } // Retrieve the internal web property id. try { $web_property = $this->get_service( 'analytics' )->management_webproperties->get( $account_id, $web_property_id ); } catch ( Exception $e ) { wp_safe_redirect( $this->context->admin_url( 'module-analytics', array( 'error_code' => 'property_not_found' ) ) ); exit; } $internal_web_property_id = $web_property->getInternalWebPropertyId(); $this->get_settings()->merge( array( 'accountID' => $account_id, 'propertyID' => $web_property_id, 'profileID' => $profile_id, 'internalWebPropertyID' => $internal_web_property_id, ) ); do_action( 'googlesitekit_analytics_handle_provisioning_callback', $account_id, $web_property_id, $internal_web_property_id, $profile_id ); wp_safe_redirect( $this->context->admin_url( 'dashboard', array( 'notification' => 'authentication_success', 'slug' => 'analytics', ) ) ); exit; } /** * Gets map of datapoint to definition data for each. * * @since 1.9.0 * * @return array Map of datapoints to their definitions. */ protected function get_datapoint_definitions() { return array( 'GET:accounts-properties-profiles' => array( 'service' => 'analytics' ), 'POST:create-account-ticket' => array( 'service' => 'analyticsprovisioning', 'scopes' => array( self::PROVISION_SCOPE ), 'request_scopes_message' => __( 'You’ll need to grant Site Kit permission to create a new Analytics account on your behalf.', 'google-site-kit' ), ), 'POST:create-profile' => array( 'service' => 'analytics', 'scopes' => array( self::EDIT_SCOPE ), 'request_scopes_message' => __( 'You’ll need to grant Site Kit permission to create a new Analytics view on your behalf.', 'google-site-kit' ), ), 'POST:create-property' => array( 'service' => 'analytics', 'scopes' => array( self::EDIT_SCOPE ), 'request_scopes_message' => __( 'You’ll need to grant Site Kit permission to create a new Analytics property on your behalf.', 'google-site-kit' ), ), 'GET:goals' => array( 'service' => 'analytics', 'shareable' => Feature_Flags::enabled( 'dashboardSharing' ), ), 'GET:profiles' => array( 'service' => 'analytics' ), 'GET:properties-profiles' => array( 'service' => 'analytics' ), 'GET:report' => array( 'service' => 'analyticsreporting', 'shareable' => Feature_Flags::enabled( 'dashboardSharing' ), ), ); } /** * Creates a request object for the given datapoint. * * @since 1.0.0 * * @param Data_Request $data Data request object. * @return RequestInterface|callable|WP_Error Request object or callable on success, or WP_Error on failure. * * @throws Invalid_Datapoint_Exception Thrown if the datapoint does not exist. */ protected function create_data_request( Data_Request $data ) { switch ( "{$data->method}:{$data->datapoint}" ) { case 'GET:accounts-properties-profiles': return function () use ( $data ) { $restore_defer = $this->with_client_defer( false ); try { return $this->get_service( 'analytics' )->management_accounts->listManagementAccounts(); } catch ( Google_Service_Exception $exception ) { // The exception message is a JSON object of all errors, so we'll convert it to our WP Error first. $wp_error = $this->exception_to_error( $exception, $data->datapoint ); // Unfortunately there isn't a better way to identify this without checking the message. if ( 'User does not have any Google Analytics account.' === $wp_error->get_error_message() ) { return new Google_Service_Analytics_Accounts(); } // If any other exception was caught, re-throw it. throw $exception; } finally { $restore_defer(); // Will be called before returning in all cases. } }; case 'POST:create-account-ticket': if ( ! isset( $data['accountName'] ) ) { /* translators: %s: Missing parameter name */ return new WP_Error( 'missing_required_param', sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'accountName' ), array( 'status' => 400 ) ); } if ( ! isset( $data['propertyName'] ) ) { /* translators: %s: Missing parameter name */ return new WP_Error( 'missing_required_param', sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'propertyName' ), array( 'status' => 400 ) ); } if ( ! isset( $data['profileName'] ) ) { /* translators: %s: Missing parameter name */ return new WP_Error( 'missing_required_param', sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'profileName' ), array( 'status' => 400 ) ); } if ( ! isset( $data['timezone'] ) ) { /* translators: %s: Missing parameter name */ return new WP_Error( 'missing_required_param', sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'timezone' ), array( 'status' => 400 ) ); } if ( ! $this->authentication->credentials()->using_proxy() ) { return new WP_Error( 'requires_service', __( 'Analytics provisioning requires connecting via the Site Kit Service.', 'google-site-kit' ), array( 'status' => 400 ) ); } $account = new Google_Service_Analytics_Account(); $account->setName( $data['accountName'] ); $property = new Google_Service_Analytics_Webproperty(); $property->setName( $data['propertyName'] ); $property->setWebsiteUrl( $this->context->get_reference_site_url() ); $profile = new Google_Service_Analytics_Profile(); $profile->setName( $data['profileName'] ); $profile->setTimezone( $data['timezone'] ); $account_ticket = new Proxy_AccountTicket(); $account_ticket->setAccount( $account ); $account_ticket->setWebproperty( $property ); $account_ticket->setProfile( $profile ); $account_ticket->setRedirectUri( $this->get_provisioning_redirect_uri() ); // Add site id and secret. $creds = $this->authentication->credentials()->get(); $account_ticket->setSiteId( $creds['oauth2_client_id'] ); $account_ticket->setSiteSecret( $creds['oauth2_client_secret'] ); return $this->get_service( 'analyticsprovisioning' ) ->provisioning->createAccountTicket( $account_ticket ); case 'GET:goals': $connection = $this->get_settings()->get(); if ( empty( $connection['accountID'] ) || empty( $connection['internalWebPropertyID'] ) || empty( $connection['profileID'] ) ) { // This is needed to return and emulate the same error format from Analytics API. return function() { return array( 'error' => array( 'code' => 400, 'message' => __( 'Analytics module needs to be configured.', 'google-site-kit' ), 'status' => 'INVALID_ARGUMENT', ), ); }; } $service = $this->get_service( 'analytics' ); return $service->management_goals->listManagementGoals( $connection['accountID'], $connection['propertyID'], $connection['profileID'] ); case 'GET:profiles': if ( ! isset( $data['accountID'] ) ) { return new WP_Error( 'missing_required_param', /* translators: %s: Missing parameter name */ sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'accountID' ), array( 'status' => 400 ) ); } if ( ! isset( $data['propertyID'] ) ) { return new WP_Error( 'missing_required_param', /* translators: %s: Missing parameter name */ sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'propertyID' ), array( 'status' => 400 ) ); } return $this->get_service( 'analytics' )->management_profiles->listManagementProfiles( $data['accountID'], $data['propertyID'] ); case 'GET:properties-profiles': if ( ! isset( $data['accountID'] ) ) { return new WP_Error( 'missing_required_param', /* translators: %s: Missing parameter name */ sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'accountID' ), array( 'status' => 400 ) ); } return $this->get_service( 'analytics' )->management_webproperties->listManagementWebproperties( $data['accountID'] ); case 'GET:report': $request_args = array(); if ( empty( $data['metrics'] ) ) { /* translators: %s: Missing parameter name */ return new WP_Error( 'missing_required_param', sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'metrics' ), array( 'status' => 400 ) ); } if ( ! empty( $data['url'] ) ) { $request_args['page'] = $data['url']; } if ( ! empty( $data['limit'] ) ) { $request_args['row_limit'] = $data['limit']; } $dimensions = $data['dimensions']; if ( ! empty( $dimensions ) && ( is_string( $dimensions ) || is_array( $dimensions ) ) ) { if ( is_string( $dimensions ) ) { $dimensions = explode( ',', $dimensions ); } elseif ( is_array( $dimensions ) && ! wp_is_numeric_array( $dimensions ) ) { // If single object is passed. $dimensions = array( $dimensions ); } $dimensions = array_filter( array_map( function ( $dimension_def ) { $dimension = new Google_Service_AnalyticsReporting_Dimension(); if ( is_string( $dimension_def ) ) { $dimension->setName( $dimension_def ); } elseif ( is_array( $dimension_def ) && ! empty( $dimension_def['name'] ) ) { $dimension->setName( $dimension_def['name'] ); } else { return null; } return $dimension; }, array_filter( $dimensions ) ) ); if ( ! empty( $dimensions ) ) { try { $this->validate_report_dimensions( $dimensions ); } catch ( Invalid_Report_Dimensions_Exception $exception ) { return new WP_Error( 'invalid_analytics_report_dimensions', $exception->getMessage() ); } $request_args['dimensions'] = $dimensions; } } $dimension_filters = $data['dimensionFilters']; $dimension_filter_instances = array(); if ( ! empty( $dimension_filters ) && is_array( $dimension_filters ) ) { foreach ( $dimension_filters as $dimension_name => $dimension_value ) { $dimension_filter = new Google_Service_AnalyticsReporting_DimensionFilter(); $dimension_filter->setDimensionName( $dimension_name ); if ( is_array( $dimension_value ) ) { $dimension_filter->setOperator( 'IN_LIST' ); $dimension_filter->setExpressions( $dimension_value ); } else { $dimension_filter->setOperator( 'EXACT' ); $dimension_filter->setExpressions( array( $dimension_value ) ); } $dimension_filter_instances[] = $dimension_filter; } if ( ! empty( $dimension_filter_instances ) ) { $request_args['dimension_filters'] = $dimension_filter_instances; } } $request = $this->create_analytics_site_data_request( $request_args ); if ( is_wp_error( $request ) ) { return $request; } $date_ranges = array(); $start_date = $data['startDate']; $end_date = $data['endDate']; if ( strtotime( $start_date ) && strtotime( $end_date ) ) { $compare_start_date = $data['compareStartDate']; $compare_end_date = $data['compareEndDate']; $date_ranges[] = array( $start_date, $end_date ); // When using multiple date ranges, it changes the structure of the response, // where each date range becomes an item in a list. if ( strtotime( $compare_start_date ) && strtotime( $compare_end_date ) ) { $date_ranges[] = array( $compare_start_date, $compare_end_date ); } } else { $date_range = $data['dateRange'] ?: 'last-28-days'; $date_ranges[] = $this->parse_date_range( $date_range, $data['compareDateRanges'] ? 2 : 1 ); // When using multiple date ranges, it changes the structure of the response, // where each date range becomes an item in a list. if ( ! empty( $data['multiDateRange'] ) ) { $date_ranges[] = $this->parse_date_range( $date_range, 1, 1, true ); } } $date_ranges = array_map( function ( $date_range ) { list ( $start_date, $end_date ) = $date_range; $date_range = new Google_Service_AnalyticsReporting_DateRange(); $date_range->setStartDate( $start_date ); $date_range->setEndDate( $end_date ); return $date_range; }, $date_ranges ); $request->setDateRanges( $date_ranges ); $metrics = $data['metrics']; if ( is_string( $metrics ) || is_array( $metrics ) ) { if ( is_string( $metrics ) ) { $metrics = explode( ',', $data['metrics'] ); } elseif ( is_array( $metrics ) && ! wp_is_numeric_array( $metrics ) ) { // If single object is passed. $metrics = array( $metrics ); } $metrics = array_filter( array_map( function ( $metric_def ) { $metric = new Google_Service_AnalyticsReporting_Metric(); if ( is_string( $metric_def ) ) { $metric->setAlias( $metric_def ); $metric->setExpression( $metric_def ); } elseif ( is_array( $metric_def ) && ! empty( $metric_def['expression'] ) ) { $metric->setExpression( $metric_def['expression'] ); $metric->setAlias( ! empty( $metric_def['alias'] ) ? $metric_def['alias'] : $metric_def['expression'] ); } else { return null; } return $metric; }, array_filter( $metrics ) ) ); if ( ! empty( $metrics ) ) { try { $this->validate_report_metrics( $metrics ); } catch ( Invalid_Report_Metrics_Exception $exception ) { return new WP_Error( 'invalid_analytics_report_metrics', $exception->getMessage() ); } $request->setMetrics( $metrics ); } } // Order by. $orderby = $this->parse_reporting_orderby( $data['orderby'] ); if ( ! empty( $orderby ) ) { $request->setOrderBys( $orderby ); } // Batch reports requests. $body = new Google_Service_AnalyticsReporting_GetReportsRequest(); $body->setReportRequests( array( $request ) ); return $this->get_analyticsreporting_service()->reports->batchGet( $body ); case 'POST:create-profile': if ( ! isset( $data['accountID'] ) ) { return new WP_Error( 'missing_required_param', /* translators: %s: Missing parameter name */ sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'accountID' ), array( 'status' => 400 ) ); } if ( ! isset( $data['propertyID'] ) ) { return new WP_Error( 'missing_required_param', /* translators: %s: Missing parameter name */ sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'propertyID' ), array( 'status' => 400 ) ); } $profile_name = trim( $data['profileName'] ); if ( empty( $profile_name ) ) { $profile_name = _x( 'All Web Site Data', 'default Analytics view name', 'google-site-kit' ); } $profile = new Google_Service_Analytics_Profile(); $profile->setName( $profile_name ); return $profile = $this->get_service( 'analytics' )->management_profiles->insert( $data['accountID'], $data['propertyID'], $profile ); case 'POST:create-property': if ( ! isset( $data['accountID'] ) ) { return new WP_Error( 'missing_required_param', /* translators: %s: Missing parameter name */ sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'accountID' ), array( 'status' => 400 ) ); } $property = new Google_Service_Analytics_Webproperty(); $property->setName( URL::parse( $this->context->get_reference_site_url(), PHP_URL_HOST ) ); $property->setWebsiteUrl( $this->context->get_reference_site_url() ); return $this->get_service( 'analytics' )->management_webproperties->insert( $data['accountID'], $property ); } return parent::create_data_request( $data ); } /** * Parses the orderby value of the data request into an array of reporting orderby object instances. * * @since 1.13.1 * * @param array|null $orderby Data request orderby value. * @return Google_Service_AnalyticsReporting_OrderBy[] An array of reporting orderby objects. */ protected function parse_reporting_orderby( $orderby ) { if ( empty( $orderby ) || ! is_array( $orderby ) ) { return array(); } $results = array_map( function ( $order_def ) { $order_def = array_merge( array( 'fieldName' => '', 'sortOrder' => '', ), (array) $order_def ); if ( empty( $order_def['fieldName'] ) || empty( $order_def['sortOrder'] ) ) { return null; } $order_by = new Google_Service_AnalyticsReporting_OrderBy(); $order_by->setFieldName( $order_def['fieldName'] ); $order_by->setSortOrder( $order_def['sortOrder'] ); return $order_by; }, // When just object is passed we need to convert it to an array of objects. wp_is_numeric_array( $orderby ) ? $orderby : array( $orderby ) ); $results = array_filter( $results ); $results = array_values( $results ); return $results; } /** * Parses a response for the given datapoint. * * @since 1.0.0 * * @param Data_Request $data Data request object. * @param mixed $response Request response. * * @return mixed Parsed response data on success, or WP_Error on failure. */ protected function parse_data_response( Data_Request $data, $response ) { switch ( "{$data->method}:{$data->datapoint}" ) { case 'GET:accounts-properties-profiles': /* @var Google_Service_Analytics_Accounts $response listManagementAccounts response. */ $accounts = Sort::case_insensitive_list_sort( (array) $response->getItems(), 'name' ); $account_ids = array_map( function ( Google_Service_Analytics_Account $account ) { return $account->getId(); }, $accounts ); $properties_profiles = array( 'properties' => array(), 'profiles' => array(), ); if ( empty( $accounts ) ) { return array_merge( compact( 'accounts' ), $properties_profiles ); } // Get the account ID from the saved settings. $option = $this->get_settings()->get(); $account_id = $option['accountID']; // If the saved account ID is in the list of accounts the user has access to, it's a match. if ( in_array( $account_id, $account_ids, true ) ) { $properties_profiles = $this->get_data( 'properties-profiles', array( 'accountID' => $account_id ) ); } else { $account_summaries = $this->get_service( 'analytics' )->management_accountSummaries->listManagementAccountSummaries(); $current_url = $this->context->get_reference_site_url(); $current_urls = $this->permute_site_url( $current_url ); foreach ( $account_summaries as $account_summary ) { $found_property = $this->find_property( $account_summary->getWebProperties(), '', $current_urls ); if ( ! is_null( $found_property ) ) { $properties_profiles = $this->get_data( 'properties-profiles', array( 'accountID' => $account_summary->getId() ) ); break; } } } if ( is_wp_error( $properties_profiles ) || ! $properties_profiles ) { $properties_profiles = array( 'properties' => array(), 'profiles' => array(), ); } return array_merge( compact( 'accounts' ), $properties_profiles ); case 'GET:goals': if ( is_array( $response ) ) { return $response; } // TODO: Parse this response to a regular array. break; case 'GET:profiles': // TODO: Parse this response to a regular array. $response = Sort::case_insensitive_list_sort( $response->getItems(), 'name' ); return $response; case 'GET:properties-profiles': /* @var Google_Service_Analytics_Webproperties $response listManagementWebproperties response. */ $properties = Sort::case_insensitive_list_sort( (array) $response->getItems(), 'name' ); $found_property = null; $response = array( 'properties' => $properties, 'profiles' => array(), ); if ( 0 === count( $properties ) ) { return $response; } $current_url = $this->context->get_reference_site_url(); $current_urls = $this->permute_site_url( $current_url ); $found_property = $this->find_property( $properties, '', $current_urls ); if ( ! is_null( $found_property ) ) { $response['matchedProperty'] = $found_property; } else { $found_property = new Google_Service_Analytics_Webproperty(); } // If no match is found, fetch profiles for the first property if available. if ( ! $found_property->getAccountId() && $properties ) { $found_property = array_shift( $properties ); } elseif ( ! $found_property->getAccountId() ) { // If no found property, skip the call to 'profiles' as it would be empty/fail. return $response; } $profiles = $this->get_data( 'profiles', array( 'accountID' => $found_property->getAccountId(), 'propertyID' => $found_property->getId(), ) ); if ( is_wp_error( $profiles ) ) { return $profiles; } $response['profiles'] = $profiles; return $response; case 'GET:report': // If AdSense metric successfully requested, set adsenseLinked to true. if ( $this->is_adsense_request( $data ) ) { $this->get_settings()->merge( array( 'adsenseLinked' => true ) ); } return $response->getReports(); case 'POST:create-account-ticket': // Cache the create ticket id long enough to verify it upon completion of the terms of service. set_transient( self::PROVISION_ACCOUNT_TICKET_ID . '::' . get_current_user_id(), $response->getId(), 15 * MINUTE_IN_SECONDS ); return $response; } return parent::parse_data_response( $data, $response ); } /** * Creates a new Analytics site request for the current site and given arguments. * * @since 1.0.0 * @since 1.24.0 Added $dimension_filters * * @param array $args { * Optional. Additional arguments. * * @type array $dimensions List of request dimensions. Default empty array. * @type Google_Service_AnalyticsReporting_DimensionFilter[] $dimension_filters List of dimension filter instances for the specified request dimensions. Default empty array. * @type string $start_date Start date in 'Y-m-d' format. Default empty string. * @type string $end_date End date in 'Y-m-d' format. Default empty string. * @type string $page Specific page URL to filter by. Default empty string. * @type int $row_limit Limit of rows to return. Default empty string. * } * @return Google_Service_AnalyticsReporting_ReportRequest|WP_Error Analytics site request instance. */ protected function create_analytics_site_data_request( array $args = array() ) { $args = wp_parse_args( $args, array( 'dimensions' => array(), 'dimension_filters' => array(), 'start_date' => '', 'end_date' => '', 'page' => '', 'row_limit' => '', ) ); $option = $this->get_settings()->get(); $profile_id = $option['profileID']; $request = new Google_Service_AnalyticsReporting_ReportRequest(); $request->setIncludeEmptyRows( true ); $request->setViewId( $profile_id ); if ( ! empty( $args['dimensions'] ) ) { $request->setDimensions( (array) $args['dimensions'] ); } if ( ! empty( $args['start_date'] ) && ! empty( $args['end_date'] ) ) { $date_range = new Google_Service_AnalyticsReporting_DateRange(); $date_range->setStartDate( $args['start_date'] ); $date_range->setEndDate( $args['end_date'] ); $request->setDateRanges( array( $date_range ) ); } $dimension_filter_clauses = array(); $hostnames = array_values( array_unique( array_map( function ( $site_url ) { return URL::parse( $site_url, PHP_URL_HOST ); }, $this->permute_site_url( $this->context->get_reference_site_url() ) ) ) ); $dimension_filter = new Google_Service_AnalyticsReporting_DimensionFilter(); $dimension_filter->setDimensionName( 'ga:hostname' ); $dimension_filter->setOperator( 'IN_LIST' ); $dimension_filter->setExpressions( $hostnames ); $dimension_filter_clause = new Google_Service_AnalyticsReporting_DimensionFilterClause(); $dimension_filter_clause->setFilters( array( $dimension_filter ) ); $dimension_filter_clauses[] = $dimension_filter_clause; if ( ! empty( $args['dimension_filters'] ) ) { $dimension_filters = $args['dimension_filters']; $dimension_filter_clause = new Google_Service_AnalyticsReporting_DimensionFilterClause(); $dimension_filter_clause->setFilters( $dimension_filters ); $dimension_filter_clause->setOperator( 'AND' ); $dimension_filter_clauses[] = $dimension_filter_clause; } if ( ! empty( $args['page'] ) ) { $dimension_filter = new Google_Service_AnalyticsReporting_DimensionFilter(); $dimension_filter->setDimensionName( 'ga:pagePath' ); $dimension_filter->setOperator( 'EXACT' ); $args['page'] = str_replace( trim( $this->context->get_reference_site_url(), '/' ), '', esc_url_raw( $args['page'] ) ); $dimension_filter->setExpressions( array( rawurldecode( $args['page'] ) ) ); $dimension_filter_clause = new Google_Service_AnalyticsReporting_DimensionFilterClause(); $dimension_filter_clause->setFilters( array( $dimension_filter ) ); $dimension_filter_clauses[] = $dimension_filter_clause; } $request->setDimensionFilterClauses( $dimension_filter_clauses ); if ( ! empty( $args['row_limit'] ) ) { $request->setPageSize( $args['row_limit'] ); } return $request; } /** * Sets up information about the module. * * @since 1.0.0 * * @return array Associative array of module info. */ protected function setup_info() { return array( 'slug' => 'analytics', 'name' => _x( 'Analytics', 'Service name', 'google-site-kit' ), 'description' => __( 'Get a deeper understanding of your customers. Google Analytics gives you the free tools you need to analyze data for your business in one place.', 'google-site-kit' ), 'order' => 3, 'homepage' => __( 'https://analytics.google.com/analytics/web', 'google-site-kit' ), ); } /** * Gets the configured Analytics Reporting service object instance. * * @return Google_Service_AnalyticsReporting The Analytics Reporting API service. */ private function get_analyticsreporting_service() { return $this->get_service( 'analyticsreporting' ); } /** * Sets up the Google services the module should use. * * This method is invoked once by {@see Module::get_service()} to lazily set up the services when one is requested * for the first time. * * @since 1.0.0 * @since 1.2.0 Now requires Google_Site_Kit_Client instance. * * @param Google_Site_Kit_Client $client Google client instance. * @return array Google services as $identifier => $service_instance pairs. Every $service_instance must be an * instance of Google_Service. */ protected function setup_services( Google_Site_Kit_Client $client ) { $google_proxy = new Google_Proxy( $this->context ); return array( 'analytics' => new Google_Service_Analytics( $client ), 'analyticsreporting' => new Google_Service_AnalyticsReporting( $client ), 'analyticsprovisioning' => new Google_Service_AnalyticsProvisioning( $client, $google_proxy->url() ), ); } /** * Gets the provisioning redirect URI that listens for the Terms of Service redirect. * * @since 1.9.0 * * @return string Provisioning redirect URI. */ private function get_provisioning_redirect_uri() { $google_proxy = new Google_Proxy( $this->context ); return $google_proxy->get_site_fields()['analytics_redirect_uri']; } /** * Transforms an exception into a WP_Error object. * * @since 1.0.0 * @since 1.70.0 $datapoint parameter is optional. * * @param Exception $e Exception object. * @param string $datapoint Optional. Datapoint originally requested. Default is an empty string. * @return WP_Error WordPress error object. */ protected function exception_to_error( Exception $e, $datapoint = '' ) { $cache_ttl = false; if ( 'report' === $datapoint && $e instanceof Google_Service_Exception ) { $errors = $e->getErrors(); // If error is because of AdSense metric being requested, set adsenseLinked to false. if ( isset( $errors[0]['message'] ) ) { if ( $this->is_adsense_metric( substr( $errors[0]['message'], strlen( 'Restricted metric(s): ' ) ) ) ) { $this->get_settings()->merge( array( 'adsenseLinked' => false ) ); } if ( preg_match( '#^Restricted metric\(s\)\:#im', $errors[0]['message'] ) ) { $cache_ttl = ( 10 * MINUTE_IN_SECONDS ); } } } $error = parent::exception_to_error( $e, $datapoint ); if ( $cache_ttl && is_wp_error( $error ) ) { $error_code = $error->get_error_code(); if ( ! empty( $error->error_data[ $error_code ] ) ) { $error->error_data[ $error_code ]['cacheTTL'] = $cache_ttl; } else { $error->add_data( array( 'cacheTTL' => $cache_ttl, ), $error_code ); } } return $error; } /** * Determines whether the given request is for an adsense request. * * @param Data_Request $data Data request object. * * @return bool */ private function is_adsense_request( $data ) { foreach ( (array) $data['metrics'] as $metric ) { $metric = (array) $metric; if ( isset( $metric['expression'] ) && $this->is_adsense_metric( $metric['expression'] ) ) { return true; } } return false; } /** * Determines whether the given metric expression is for an AdSense metric. * * @since 1.8.0 * * @param string $metric Metric expression. * @return bool True if AdSense metric, false otherwise. */ private function is_adsense_metric( $metric ) { return 0 === strpos( $metric, 'ga:adsense' ); } /** * Outputs the user tracking opt-out script. * * This script opts out of all Google Analytics tracking, for all measurement IDs, regardless of implementation. * E.g. via Tag Manager, etc. * * @since 1.5.0 * @link https://developers.google.com/analytics/devguides/collection/analyticsjs/user-opt-out */ private function print_tracking_opt_out() { if ( ! $this->is_tracking_disabled() ) { return; } $settings = $this->get_settings()->get(); $account_id = $settings['accountID']; $property_id = $settings['propertyID']; if ( $this->context->is_amp() ) : ?> options ); } /** * Sets up the module's assets to register. * * @since 1.8.0 * * @return Asset[] List of Asset objects. */ protected function setup_assets() { $base_url = $this->context->url( 'dist/assets/' ); return array( new Script( 'googlesitekit-modules-analytics', array( 'src' => $base_url . 'js/googlesitekit-modules-analytics.js', 'dependencies' => array( 'googlesitekit-vendor', 'googlesitekit-api', 'googlesitekit-data', 'googlesitekit-modules', 'googlesitekit-datastore-site', 'googlesitekit-datastore-user', 'googlesitekit-datastore-forms', 'googlesitekit-components', ), ) ), ); } /** * Determines the Analytics account ID from a given Analytics property ID. * * @since 1.8.0 * * @param string $property_id Analytics property ID. * @return string Analytics account ID, or empty string if invalid property ID. */ protected function parse_account_id( $property_id ) { if ( ! preg_match( '/^UA-([0-9]+)-[0-9]+$/', $property_id, $matches ) ) { return ''; } return $matches[1]; } /** * Registers the Analytics tag. * * @since 1.24.0 */ private function register_tag() { $settings = $this->get_settings()->get(); if ( $this->context->is_amp() ) { $tag = new AMP_Tag( $settings['propertyID'], self::MODULE_SLUG ); } else { $tag = new Web_Tag( $settings['propertyID'], self::MODULE_SLUG ); } if ( $tag->is_tag_blocked() ) { return; } $tag->use_guard( new Tag_Verify_Guard( $this->context->input() ) ); $tag->use_guard( new Tag_Guard( $this->get_settings() ) ); $tag->use_guard( new Tag_Environment_Type_Guard() ); if ( $tag->can_register() ) { $tag->set_anonymize_ip( $settings['anonymizeIP'] ); $tag->set_home_domain( URL::parse( $this->context->get_canonical_home_url(), PHP_URL_HOST ) ); $tag->set_ads_conversion_id( $settings['adsConversionID'] ); $tag->register(); } } /** * Finds a property in the properties list. * * @since 1.31.0 * * @param array $properties An array of Analytics properties to search in. * @param string $property_id Optional. The Analytics property ID. Default is the current property ID from the Analytics settings. * @param array $urls Optional. An array of URLs that searched property can have. * @return mixed A property instance on success, otherwise NULL. */ protected function find_property( array $properties, $property_id = '', array $urls = array() ) { if ( strlen( $property_id ) === 0 ) { $option = $this->get_settings()->get(); $property_id = $option['propertyID']; } foreach ( $properties as $property ) { /* @var Google_Service_Analytics_Webproperty $property Property instance. */ $id = $property->getId(); $website_url = $property->getWebsiteUrl(); $website_url = untrailingslashit( $website_url ); if ( $id === $property_id || ( 0 < count( $urls ) && in_array( $website_url, $urls, true ) ) ) { return $property; } } return null; } /** * Populates Analytics settings using the incoming token response data. * * @since 1.50.0 * * @param array $token_response Token response data. */ public function handle_token_response_data( $token_response ) { if ( empty( $token_response['analytics_configuration'] ) || $this->is_connected() ) { return; } $configuration = $token_response['analytics_configuration']; if ( ! is_array( $configuration ) ) { return; } $keys_map = array( 'ga_account_id' => 'accountID', 'ua_property_id' => 'propertyID', 'ua_internal_web_property_id' => 'internalWebPropertyID', 'ua_profile_id' => 'profileID', ); $settings = array(); foreach ( $keys_map as $key => $setting ) { if ( ! empty( $configuration[ $key ] ) && is_string( $configuration[ $key ] ) ) { $settings[ $setting ] = $configuration[ $key ]; } } // Save new settings only if all keys are not empty. if ( ! empty( $settings ) && count( $settings ) === 4 ) { $this->get_settings()->merge( $settings ); } } /** * Checks if the current user has access to the current configured service entity. * * @since 1.70.0 * * @return boolean|WP_Error */ public function check_service_entity_access() { $data_request = array( 'row_limit' => 1, ); $request = $this->create_analytics_site_data_request( $data_request ); if ( is_wp_error( $request ) ) { return $request; } try { $body = new Google_Service_AnalyticsReporting_GetReportsRequest(); $body->setReportRequests( array( $request ) ); $this->get_analyticsreporting_service()->reports->batchGet( $body ); } catch ( Exception $e ) { if ( $e->getCode() === 403 ) { return false; } return $this->exception_to_error( $e ); } return true; } /** * Validates the report metrics. * * @since 1.82.0 * * @param Google_Service_AnalyticsReporting_Metric[] $metrics The metrics to validate. * @throws Invalid_Report_Metrics_Exception Thrown if the metrics are invalid. */ protected function validate_report_metrics( $metrics ) { if ( false === $this->is_using_shared_credentials ) { return; } $valid_metrics = apply_filters( 'googlesitekit_shareable_analytics_metrics', array( 'ga:sessions', 'ga:users', 'ga:pageviews', 'ga:uniquePageviews', 'ga:bounceRate', 'ga:avgSessionDuration', 'ga:adsenseRevenue', 'ga:adsenseECPM', 'ga:adsensePageImpressions', 'ga:goalCompletionsAll', ) ); $invalid_metrics = array_diff( array_map( function ( $metric ) { return $metric->getExpression(); }, $metrics ), $valid_metrics ); if ( count( $invalid_metrics ) > 0 ) { $message = count( $invalid_metrics ) > 1 ? sprintf( /* translators: %s: is replaced with a comma separated list of the invalid metrics. */ __( 'Unsupported metrics requested: %s', 'google-site-kit' ), join( /* translators: used between list items, there is a space after the comma. */ __( ', ', 'google-site-kit' ), $invalid_metrics ) ) : sprintf( /* translators: %s: is replaced with the invalid metric. */ __( 'Unsupported metric requested: %s', 'google-site-kit' ), $invalid_metrics ); throw new Invalid_Report_Metrics_Exception( $message ); } } /** * Validates the report dimensions. * * @since 1.82.0 * * @param Google_Service_AnalyticsReporting_Dimension[] $dimensions The dimensions to validate. * @throws Invalid_Report_Dimensions_Exception Thrown if the dimensions are invalid. */ protected function validate_report_dimensions( $dimensions ) { if ( false === $this->is_using_shared_credentials ) { return; } $valid_dimensions = apply_filters( 'googlesitekit_shareable_analytics_dimensions', array( 'ga:date', 'ga:pagePath', 'ga:pageTitle', 'ga:channelGrouping', 'ga:country', 'ga:deviceCategory', 'ga:hostname', ) ); $invalid_dimensions = array_diff( array_map( function ( $dimension ) { return $dimension->getName(); }, $dimensions ), $valid_dimensions ); if ( count( $invalid_dimensions ) > 0 ) { $message = count( $invalid_dimensions ) > 1 ? sprintf( /* translators: %s: is replaced with a comma separated list of the invalid dimensions. */ __( 'Unsupported dimensions requested: %s', 'google-site-kit' ), join( /* translators: used between list items, there is a space after the comma. */ __( ', ', 'google-site-kit' ), $invalid_dimensions ) ) : sprintf( /* translators: %s: is replaced with the invalid dimension. */ __( 'Unsupported dimension requested: %s', 'google-site-kit' ), $invalid_dimensions ); throw new Invalid_Report_Dimensions_Exception( $message ); } } } Analytics/Web_Tag.php000064400000014301150515556660010531 0ustar00home_domain = $domain; } /** * Sets whether or not to anonymize IP addresses. * * @since 1.24.0 * * @param bool $anonymize_ip Whether to anonymize IP addresses or not. */ public function set_anonymize_ip( $anonymize_ip ) { $this->anonymize_ip = (bool) $anonymize_ip; } /** * Sets the ads conversion ID. * * @since 1.32.0 * * @param string $ads_conversion_id Ads ID. */ public function set_ads_conversion_id( $ads_conversion_id ) { $this->ads_conversion_id = $ads_conversion_id; } /** * Registers tag hooks. * * @since 1.24.0 */ public function register() { add_action( 'wp_enqueue_scripts', $this->get_method_proxy( 'enqueue_gtag_script' ) ); add_filter( 'wp_resource_hints', $this->get_dns_prefetch_hints_callback( '//www.googletagmanager.com' ), 10, 2 ); $this->do_init_tag_action(); } /** * Outputs gtag snippet. * * @since 1.24.0 */ protected function render() { // Do nothing, gtag script is enqueued. } /** * Enqueues gtag script. * * @since 1.24.0 */ protected function enqueue_gtag_script() { $gtag_opt = array(); $gtag_src = 'https://www.googletagmanager.com/gtag/js?id=' . rawurlencode( $this->tag_id ); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion wp_enqueue_script( 'google_gtagjs', $gtag_src, false, null, false ); wp_script_add_data( 'google_gtagjs', 'script_execution', 'async' ); wp_add_inline_script( 'google_gtagjs', 'window.dataLayer = window.dataLayer || [];function gtag(){dataLayer.push(arguments);}' ); if ( ! empty( $this->home_domain ) ) { $gtag_opt['linker'] = array( 'domains' => array( $this->home_domain ) ); } if ( $this->anonymize_ip ) { // See https://developers.google.com/analytics/devguides/collection/gtagjs/ip-anonymization. $gtag_opt['anonymize_ip'] = true; } /** * Filters the gtag configuration options for the Analytics snippet. * * You can use the {@see 'googlesitekit_amp_gtag_opt'} filter to do the same for gtag in AMP. * * @since 1.24.0 * * @see https://developers.google.com/gtagjs/devguide/configure * * @param array $gtag_opt gtag config options. */ $gtag_opt = apply_filters( 'googlesitekit_gtag_opt', $gtag_opt ); if ( ! empty( $gtag_opt['linker'] ) ) { $linker = wp_json_encode( $gtag_opt['linker'] ); $linker = sprintf( "gtag('set', 'linker', %s );", $linker ); wp_add_inline_script( 'google_gtagjs', $linker ); } unset( $gtag_opt['linker'] ); wp_add_inline_script( 'google_gtagjs', 'gtag("js", new Date());' ); wp_add_inline_script( 'google_gtagjs', 'gtag("set", "developer_id.dZTNiMT", true);' ); // Site Kit developer ID. if ( empty( $gtag_opt ) ) { $config = sprintf( 'gtag("config", "%s");', esc_js( $this->tag_id ) ); wp_add_inline_script( 'google_gtagjs', $config ); } else { $config = sprintf( 'gtag("config", "%s", %s);', esc_js( $this->tag_id ), wp_json_encode( $gtag_opt ) ); wp_add_inline_script( 'google_gtagjs', $config ); } $this->add_inline_ads_conversion_id_config(); $block_on_consent_attrs = $this->get_tag_blocked_on_consent_attribute(); $filter_google_gtagjs = function ( $tag, $handle ) use ( $block_on_consent_attrs, $gtag_src ) { if ( 'google_gtagjs' !== $handle ) { return $tag; } $snippet_comment_begin = sprintf( "\n\n", esc_html__( 'Google Analytics snippet added by Site Kit', 'google-site-kit' ) ); $snippet_comment_end = sprintf( "\n\n", esc_html__( 'End Google Analytics snippet added by Site Kit', 'google-site-kit' ) ); if ( $block_on_consent_attrs ) { $tag = str_replace( array( "
', $this->get_tag_blocked_on_consent_attribute(), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped wp_json_encode( $gtag_amp_opt_filtered ) ); printf( "\n\n", esc_html__( 'End Google Analytics AMP snippet added by Site Kit', 'google-site-kit' ) ); } } Analytics/Tag_Guard.php000064400000001602150515556660011056 0ustar00settings->get(); return $settings['canUseSnippet'] && ! empty( $settings['useSnippet'] ) && ! empty( $settings['propertyID'] ); } } Analytics/Proxy_Account_Ticket.php000064400000002572150515556660013330 0ustar00site_id; } /** * Sets the site ID. * * @since 1.9.0 * * @param string $id The site id. */ public function setSiteId( $id ) { $this->site_id = $id; } /** * Gets the site secret. * * @since 1.9.0 */ public function getSiteSecret() { return $this->site_secret; } /** * Sets the site secret. * * @since 1.9.0 * * @param string $secret The site secret. */ public function setSiteSecret( $secret ) { $this->site_secret = $secret; } } Analytics/Advanced_Tracking/Event_List.php000064400000002636150515556660014634 0ustar00events[ $hash ] = $event; } /** * Gets the measurement events array. * * @since 1.18.0. * * @return array The map of events for this list, keyed by their unique ID. */ public function get_events() { return $this->events; } } Analytics/Advanced_Tracking/Event_List_Registry.php000064400000002060150515556660016513 0ustar00event_lists[] = $event_list; } /** * Gets the list of registered event lists. * * @since 1.18.0. * * @return Event_List[] The list of registered event lists. */ public function get_lists() { return $this->event_lists; } } Analytics/Advanced_Tracking/Script_Injector.php000064400000003624150515556660015657 0ustar00context = $context; } /** * Creates list of measurement event configurations and javascript to inject. * * @since 1.18.0. * * @param array $events The map of Event objects, keyed by their unique ID. */ public function inject_event_script( $events ) { if ( empty( $events ) ) { return; } list( $filename ) = Manifest::get( 'analytics-advanced-tracking' ); if ( ! $filename ) { // Get file contents of script and add it to the page, injecting event configurations into it. $filename = 'analytics-advanced-tracking.js'; } $script_path = $this->context->path( "dist/assets/js/{$filename}" ); // phpcs:ignore WordPress.WP.AlternativeFunctions, WordPressVIPMinimum.Performance.FetchingRemoteData $script_content = file_get_contents( $script_path ); if ( ! $script_content ) { return; } $data_var = sprintf( 'var _googlesitekitAnalyticsTrackingData = %s;', wp_json_encode( array_values( $events ) ) ); BC_Functions::wp_print_inline_script_tag( $data_var . "\n" . $script_content ); } } Analytics/Advanced_Tracking/AMP_Config_Injector.php000064400000003404150515556660016311 0ustar00 $event ) { $event_config = $event->get_config(); $amp_trigger = array(); if ( 'DOMContentLoaded' === $event_config['on'] ) { $amp_trigger['on'] = 'visible'; } else { $amp_trigger['on'] = $event_config['on']; $amp_trigger['selector'] = $event_config['selector']; } $amp_trigger['vars'] = array(); $amp_trigger['vars']['event_name'] = $event_config['action']; if ( is_array( $event_config['metadata'] ) ) { foreach ( $event_config['metadata'] as $key => $value ) { $amp_trigger['vars'][ $key ] = $value; } } $gtag_amp_opt['triggers'][ $amp_trigger_key ] = $amp_trigger; } return $gtag_amp_opt; } } Analytics/Advanced_Tracking/Event.php000064400000005421150515556660013634 0ustar00config = $this->validate_config( $config ); } /** * Returns an associative event containing the event attributes. * * @since 1.18.0. * * @return array The configuration in JSON-serializable format. */ public function jsonSerialize() { return $this->config; } /** * Returns the measurement event configuration. * * @since 1.18.0. * * @return array The config. */ public function get_config() { return $this->config; } /** * Validates the configuration keys and value types. * * @since 1.18.0. * * @param array $config The event's configuration. * @return array The event's configuration. * @throws Exception Thrown when invalid keys or value type. */ private function validate_config( $config ) { $valid_keys = array( 'action', 'selector', 'on', 'metadata', ); foreach ( $config as $key => $value ) { if ( ! in_array( $key, $valid_keys, true ) ) { throw new Exception( 'Invalid configuration parameter: ' . $key ); } } if ( ! array_key_exists( 'metadata', $config ) ) { $config['metadata'] = null; } if ( array_key_exists( 'on', $config ) && 'DOMContentLoaded' === $config['on'] ) { $config['selector'] = ''; } foreach ( $valid_keys as $key ) { if ( ! array_key_exists( $key, $config ) ) { throw new Exception( 'Missed configuration parameter: ' . $key ); } } return $config; } } Idea_Hub/Post_Idea_Text.php000064400000001450150515556660011606 0ustar00set( $this->get() + 1 ); } } Idea_Hub/Settings.php000064400000001346150515556660010537 0ustar00 false, 'ownerID' => 0, ); } } Idea_Hub/Post_Idea_Topics.php000064400000002351150515556660012124 0ustar00 $display_name ) { if ( is_string( $mid ) && is_string( $display_name ) ) { $sanitized[ sanitize_text_field( $mid ) ] = sanitize_text_field( $display_name ); } } return $option; }; } } Idea_Hub/Post_Idea_Name.php000064400000001076150515556660011546 0ustar00