initial commit

This commit is contained in:
2024-04-29 13:12:44 +05:45
commit 34887303c5
19300 changed files with 5268802 additions and 0 deletions

View File

@ -0,0 +1,936 @@
<?php
/**
* Class Google\Site_Kit\Modules\AdSense
*
* @package Google\Site_Kit
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Modules;
use Google\Site_Kit\Core\Modules\Module;
use Google\Site_Kit\Core\Modules\Module_Settings;
use Google\Site_Kit\Core\Modules\Module_With_Deactivation;
use Google\Site_Kit\Core\Modules\Module_With_Debug_Fields;
use Google\Site_Kit\Core\Modules\Module_With_Screen;
use Google\Site_Kit\Core\Modules\Module_With_Screen_Trait;
use Google\Site_Kit\Core\Modules\Module_With_Scopes;
use Google\Site_Kit\Core\Modules\Module_With_Scopes_Trait;
use Google\Site_Kit\Core\Modules\Module_With_Settings;
use Google\Site_Kit\Core\Modules\Module_With_Settings_Trait;
use Google\Site_Kit\Core\Modules\Module_With_Assets;
use Google\Site_Kit\Core\Modules\Module_With_Assets_Trait;
use Google\Site_Kit\Core\Modules\Module_With_Owner;
use Google\Site_Kit\Core\Modules\Module_With_Owner_Trait;
use Google\Site_Kit\Core\REST_API\Exception\Invalid_Datapoint_Exception;
use Google\Site_Kit\Core\Assets\Asset;
use Google\Site_Kit\Core\Assets\Script;
use Google\Site_Kit\Core\Authentication\Clients\Google_Site_Kit_Client;
use Google\Site_Kit\Core\Modules\Module_With_Service_Entity;
use Google\Site_Kit\Core\REST_API\Data_Request;
use Google\Site_Kit\Core\Tags\Guards\Tag_Production_Guard;
use Google\Site_Kit\Core\Tags\Guards\Tag_Verify_Guard;
use Google\Site_Kit\Core\Util\Debug_Data;
use Google\Site_Kit\Core\Util\Feature_Flags;
use Google\Site_Kit\Core\Util\Method_Proxy_Trait;
use Google\Site_Kit\Modules\AdSense\AMP_Tag;
use Google\Site_Kit\Modules\AdSense\Settings;
use Google\Site_Kit\Modules\AdSense\Tag_Guard;
use Google\Site_Kit\Modules\AdSense\Auto_Ad_Guard;
use Google\Site_Kit\Modules\AdSense\Web_Tag;
use Google\Site_Kit_Dependencies\Google\Model as Google_Model;
use Google\Site_Kit_Dependencies\Google\Service\Adsense as Google_Service_Adsense;
use Google\Site_Kit_Dependencies\Google\Service\Adsense\Alert as Google_Service_Adsense_Alert;
use Google\Site_Kit_Dependencies\Psr\Http\Message\RequestInterface;
use Exception;
use WP_Error;
/**
* Class representing the AdSense module.
*
* @since 1.0.0
* @access private
* @ignore
*/
final class AdSense extends Module
implements Module_With_Screen, Module_With_Scopes, Module_With_Settings, Module_With_Assets, Module_With_Debug_Fields, Module_With_Owner, Module_With_Service_Entity, Module_With_Deactivation {
use Method_Proxy_Trait;
use Module_With_Assets_Trait;
use Module_With_Owner_Trait;
use Module_With_Scopes_Trait;
use Module_With_Screen_Trait;
use Module_With_Settings_Trait;
/**
* Module slug name.
*/
const MODULE_SLUG = 'adsense';
/**
* Registers functionality through WordPress hooks.
*
* @since 1.0.0
*/
public function register() {
$this->register_scopes_hook();
if ( ! Feature_Flags::enabled( 'unifiedDashboard' ) ) {
$this->register_screen_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:earnings' => array(
'service' => 'adsense',
'shareable' => Feature_Flags::enabled( 'dashboardSharing' ),
),
'GET:notifications' => array( 'service' => '' ),
'GET:tag-permission' => 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:earnings':
$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 ) ) {
$args['metrics'] = $metrics;
}
$dimensions = $this->parse_string_list( $data['dimensions'] );
if ( ! empty( $dimensions ) ) {
$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:tag-permission':
return function() use ( $data ) {
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 )
);
}
return array_merge(
array( 'clientID' => $data['clientID'] ),
$this->has_access_to_client( $data['clientID'] )
);
};
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':
return array_map( array( self::class, 'filter_account_with_ids' ), $response->getAccounts() );
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:earnings':
return $response;
case 'GET:sites':
return $response->getSites();
}
return parent::parse_data_response( $data, $response );
}
/**
* 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 = wp_parse_url( $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. Its 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',
),
)
),
);
}
/**
* Verifies that user has access to the given client and account.
*
* @since 1.9.0
*
* @param string $client_id Client found in the existing tag.
* @return array {
* AdSense account access data.
* @type string $account_id The AdSense account ID for the given client.
* @type bool $permission Whether the user has access to this account and client.
* }
*/
protected function has_access_to_client( $client_id ) {
if ( empty( $client_id ) ) {
return array(
'account_id' => '',
'permission' => false,
);
}
$account_has_client = function ( $account_id ) use ( $client_id ) {
// Try to get clients for that account.
$clients = $this->get_data( 'clients', array( 'accountID' => $account_id ) );
if ( is_wp_error( $clients ) ) {
// No access to the account.
return false;
}
// Ensure there is access to the client.
foreach ( $clients as $client ) {
if ( $client->_id === $client_id ) {
return true;
}
}
return false;
};
$parsed_account_id = $this->parse_account_id( $client_id );
if ( $account_has_client( $parsed_account_id ) ) {
return array(
'account_id' => $parsed_account_id,
'permission' => true,
);
}
$accounts = $this->get_data( 'accounts' );
if ( is_wp_error( $accounts ) ) {
$accounts = array();
}
foreach ( $accounts as $account ) {
if ( $account->_id === $parsed_account_id ) {
continue;
}
if ( $account_has_client( $account->_id ) ) {
return array(
'account_id' => $account->_id,
'permission' => true,
);
}
}
return array(
'account_id' => $parsed_account_id,
'permission' => false,
);
}
/**
* Determines the AdSense account ID from a given AdSense client ID.
*
* @since 1.9.0
*
* @param string $client_id AdSense client ID.
* @return string AdSense account ID, or empty string if invalid client ID.
*/
protected function parse_account_id( $client_id ) {
if ( ! preg_match( '/^ca-(pub-[0-9]+)$/', $client_id, $matches ) ) {
return '';
}
return $matches[1];
}
/**
* 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_Production_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<!-- %s -->\n", esc_html__( 'Google AdSense snippet added by Site Kit', 'google-site-kit' ) );
echo '<meta name="google-adsense-platform-account" content="ca-host-pub-2644536267352236">';
echo "\n";
echo '<meta name="google-adsense-platform-domain" content="sitekit.withgoogle.com">';
printf( "\n<!-- %s -->\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;
}
}

View File

@ -0,0 +1,190 @@
<?php
/**
* Class Google\Site_Kit\Modules\AdSense\AMP_Tag
*
* @package Google\Site_Kit\Modules\AdSense
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Modules\AdSense;
use Google\Site_Kit\Core\Modules\Tags\Module_AMP_Tag;
use Google\Site_Kit\Core\Util\Method_Proxy_Trait;
/**
* Class for AMP tag.
*
* @since 1.24.0
* @access private
* @ignore
*/
class AMP_Tag extends Module_AMP_Tag {
use Method_Proxy_Trait;
/**
* Internal flag for whether the AdSense tag has been printed.
*
* @since 1.24.0
* @var bool
*/
private $adsense_tag_printed = false;
/**
* Web Story Ad Slot ID.
*
* @since 1.27.0
* @var string
*/
private $story_ad_slot_id = '';
/**
* Registers tag hooks.
*
* @since 1.24.0
*/
public function register() {
if ( is_singular( 'web-story' ) ) {
// If Web Stories are enabled, render the auto ads code.
add_action( 'web_stories_print_analytics', $this->get_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 <amp-auto-ads> 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<!-- %s -->\n", esc_html__( 'Google AdSense AMP snippet added by Site Kit', 'google-site-kit' ) );
printf(
'<amp-auto-ads type="adsense" %s%s></amp-auto-ads>',
$attributes, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
$this->get_tag_blocked_on_consent_attribute() // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
);
printf( "\n<!-- %s -->\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<!-- %s -->\n", esc_html__( 'Google AdSense AMP snippet added by Site Kit', 'google-site-kit' ) );
$snippet_comment_end = sprintf( "\n<!-- %s -->\n", esc_html__( 'End Google AdSense AMP snippet added by Site Kit', 'google-site-kit' ) );
$tag = sprintf(
'<amp-auto-ads type="adsense" data-ad-client="%s"%s></amp-auto-ads>',
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<!-- %s -->\n", esc_html__( 'Google AdSense AMP snippet added by Site Kit', 'google-site-kit' ) );
printf( '<amp-story-auto-ads><script type="application/json">%s</script></amp-story-auto-ads>', wp_json_encode( $config ) );
printf( "\n<!-- %s -->\n", esc_html__( 'End Google AdSense AMP snippet added by Site Kit', 'google-site-kit' ) );
}
}

View File

@ -0,0 +1,47 @@
<?php
/**
* Class Google\Site_Kit\Modules\AdSense\Auto_Ad_Guard
*
* @package Google\Site_Kit\Modules\Analytics
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Modules\AdSense;
use Google\Site_Kit\Core\Modules\Tags\Module_Tag_Guard;
/**
* Tag guard class for the AdSense module that blocks the tag placement if it is disabled for a certain user group.
*
* @since 1.39.0
* @access private
* @ignore
*/
class Auto_Ad_Guard extends Module_Tag_Guard {
/**
* Determines whether the guarded tag can be activated or not.
*
* @since 1.39.0
*
* @return bool TRUE if guarded tag can be activated, otherwise FALSE.
*/
public function can_activate() {
$settings = $this->settings->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;
}
}

View File

@ -0,0 +1,192 @@
<?php
/**
* Class Google\Site_Kit\Modules\AdSense\Settings
*
* @package Google\Site_Kit\Modules\AdSense
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Modules\AdSense;
use Google\Site_Kit\Core\Modules\Module_Settings;
use Google\Site_Kit\Core\Storage\Setting_With_Legacy_Keys_Trait;
use Google\Site_Kit\Core\Storage\Setting_With_Owned_Keys_Interface;
use Google\Site_Kit\Core\Storage\Setting_With_Owned_Keys_Trait;
/**
* Class for AdSense settings.
*
* @since 1.2.0
* @access private
* @ignore
*/
class Settings extends Module_Settings implements Setting_With_Owned_Keys_Interface {
use Setting_With_Legacy_Keys_Trait, Setting_With_Owned_Keys_Trait;
const OPTION = 'googlesitekit_adsense_settings';
/**
* Legacy account statuses to be migrated on-the-fly.
*
* @since 1.9.0
* @var array
*/
protected $legacy_account_statuses = array(
'account-connected' => 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;
};
}
}

View File

@ -0,0 +1,48 @@
<?php
/**
* Class Google\Site_Kit\Modules\AdSense\Tag_Guard
*
* @package Google\Site_Kit\Modules\AdSense
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Modules\AdSense;
use Google\Site_Kit\Core\Modules\Tags\Module_Tag_Guard;
/**
* Class for the AdSense tag guard.
*
* @since 1.24.0
* @access private
* @ignore
*/
class Tag_Guard extends Module_Tag_Guard {
/**
* Determines whether the guarded tag can be activated or not.
*
* @since 1.24.0
* @since 1.30.0 Update to return FALSE on 404 pages deliberately.
*
* @return bool|WP_Error TRUE if guarded tag can be activated, otherwise FALSE or an error.
*/
public function can_activate() {
// Do not allow AdSense tags on 404 pages.
if ( is_404() ) {
return false;
}
$settings = $this->settings->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'] );
}
}

View File

@ -0,0 +1,98 @@
<?php
/**
* Class Google\Site_Kit\Modules\AdSense\Web_Tag
*
* @package Google\Site_Kit\Modules\AdSense
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Modules\AdSense;
use Google\Site_Kit\Core\Modules\Tags\Module_Web_Tag;
use Google\Site_Kit\Core\Util\Method_Proxy_Trait;
use Google\Site_Kit\Core\Tags\Tag_With_DNS_Prefetch_Trait;
use Google\Site_Kit\Core\Util\BC_Functions;
/**
* Class for Web tag.
*
* @since 1.24.0
* @access private
* @ignore
*/
class Web_Tag extends Module_Web_Tag {
use Method_Proxy_Trait, Tag_With_DNS_Prefetch_Trait;
/**
* Registers tag hooks.
*
* @since 1.24.0
*/
public function register() {
add_action( 'wp_head', $this->get_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',
esc_attr( $this->tag_id )
);
$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<!-- %s -->\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<!-- %s -->\n", esc_html__( 'End Google AdSense snippet added by Site Kit', 'google-site-kit' ) );
}
}

View File

@ -0,0 +1,166 @@
<?php
/**
* Class Google\Site_Kit\Modules\Analytics\AMP_Tag
*
* @package Google\Site_Kit\Modules\Analytics
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Modules\Analytics;
use Google\Site_Kit\Core\Modules\Tags\Module_AMP_Tag;
use Google\Site_Kit\Core\Util\Method_Proxy_Trait;
/**
* Class for AMP tag.
*
* @since 1.24.0
* @access private
* @ignore
*/
class AMP_Tag extends Module_AMP_Tag implements Tag_Interface {
use Method_Proxy_Trait;
/**
* Home domain name.
*
* @since 1.24.0
* @var string
*/
private $home_domain;
/**
* Ads conversion ID.
*
* @since 1.32.0
* @var string
*/
private $ads_conversion_id;
/**
* Sets the current home domain.
*
* @since 1.24.0
*
* @param string $domain Domain name.
*/
public function set_home_domain( $domain ) {
$this->home_domain = $domain;
}
/**
* Sets whether or not to anonymize IP addresses.
*
* @since 1.32.0
*
* @param bool $anonymize_ip Whether to anonymize IP addresses or not.
*/
public function set_anonymize_ip( $anonymize_ip ) {
// Data from AMP documents is always IP anonymized.
// See https://support.google.com/analytics/answer/6343176.
}
/**
* 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() {
$render = $this->get_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 gtag <amp-analytics> tag.
*
* @since 1.24.0
*/
protected function render() {
$config = array(
$this->tag_id => array(
'groups' => 'default',
'linker' => array(
'domains' => array( $this->home_domain ),
),
),
);
if ( ! empty( $this->ads_conversion_id ) ) {
$config[ $this->ads_conversion_id ] = array(
'groups' => 'default',
);
}
$gtag_amp_opt = array(
'optoutElementId' => '__gaOptOutExtension',
'vars' => array(
'gtag_id' => $this->tag_id,
'config' => $config,
),
);
/**
* Filters the gtag configuration options for the amp-analytics tag.
*
* You can use the {@see 'googlesitekit_gtag_opt'} filter to do the same for gtag in non-AMP.
*
* @since 1.24.0
* @see https://developers.google.com/gtagjs/devguide/amp
*
* @param array $gtag_amp_opt gtag config options for AMP.
*/
$gtag_amp_opt_filtered = apply_filters( 'googlesitekit_amp_gtag_opt', $gtag_amp_opt );
// Ensure gtag_id is set to the correct value.
if ( ! is_array( $gtag_amp_opt_filtered ) ) {
$gtag_amp_opt_filtered = $gtag_amp_opt;
}
if ( ! isset( $gtag_amp_opt_filtered['vars'] ) || ! is_array( $gtag_amp_opt_filtered['vars'] ) ) {
$gtag_amp_opt_filtered['vars'] = $gtag_amp_opt['vars'];
}
$gtag_amp_opt_filtered['vars']['gtag_id'] = $this->tag_id;
printf( "\n<!-- %s -->\n", esc_html__( 'Google Analytics AMP snippet added by Site Kit', 'google-site-kit' ) );
printf(
'<amp-analytics type="gtag" data-credentials="include"%s><script type="application/json">%s</script></amp-analytics>',
$this->get_tag_blocked_on_consent_attribute(), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
wp_json_encode( $gtag_amp_opt_filtered )
);
printf( "\n<!-- %s -->\n", esc_html__( 'End Google Analytics AMP snippet added by Site Kit', 'google-site-kit' ) );
}
}

View File

@ -0,0 +1,167 @@
<?php
/**
* Class Google\Site_Kit\Modules\Analytics\Advanced_Tracking
*
* @package Google\Site_Kit\Modules\Analytics
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Modules\Analytics;
use Google\Site_Kit\Context;
use Google\Site_Kit\Modules\Analytics\Advanced_Tracking\Event_List;
use Google\Site_Kit\Modules\Analytics\Advanced_Tracking\Script_Injector;
use Google\Site_Kit\Modules\Analytics\Advanced_Tracking\AMP_Config_Injector;
use Google\Site_Kit\Modules\Analytics\Advanced_Tracking\Event_List_Registry;
use Google\Site_Kit\Modules\Analytics\Advanced_Tracking\Event;
/**
* Class for Google Analytics Advanced Event Tracking.
*
* @since 1.18.0.
* @access private
* @ignore
*/
final class Advanced_Tracking {
/**
* Plugin context.
*
* @since 1.18.0.
* @var Context
*/
protected $context;
/**
* Map of events to be tracked.
*
* @since 1.18.0.
* @var array Map of Event instances, keyed by their unique ID.
*/
private $events;
/**
* Main class event list registry instance.
*
* @since 1.18.0.
* @var Event_List_Registry
*/
private $event_list_registry;
/**
* Advanced_Tracking constructor.
*
* @since 1.18.0.
*
* @param Context $context Plugin context.
*/
public function __construct( Context $context ) {
$this->context = $context;
$this->event_list_registry = new Event_List_Registry();
}
/**
* Registers functionality through WordPress hooks.
*
* @since 1.18.0.
*/
public function register() {
add_action(
'googlesitekit_analytics_init_tag',
function() {
$this->register_event_lists();
add_action(
'wp_footer',
function() {
$this->set_up_advanced_tracking();
}
);
}
);
add_action(
'googlesitekit_analytics_init_tag_amp',
function() {
$this->register_event_lists();
add_filter(
'googlesitekit_amp_gtag_opt',
function( $gtag_amp_opt ) {
return $this->set_up_advanced_tracking_amp( $gtag_amp_opt );
}
);
}
);
}
/**
* Returns the map of unique events.
*
* @since 1.18.0.
*
* @return array Map of Event instances, keyed by their unique ID.
*/
public function get_events() {
return $this->events;
}
/**
* Injects javascript to track active events.
*
* @since 1.18.0.
*/
private function set_up_advanced_tracking() {
$this->compile_events();
( new Script_Injector( $this->context ) )->inject_event_script( $this->events );
}
/**
* Adds triggers to AMP configuration.
*
* @since 1.18.0.
*
* @param array $gtag_amp_opt gtag config options for AMP.
* @return array Filtered $gtag_amp_opt.
*/
private function set_up_advanced_tracking_amp( $gtag_amp_opt ) {
$this->compile_events();
return ( new AMP_Config_Injector() )->inject_event_configurations( $gtag_amp_opt, $this->events );
}
/**
* Instantiates and registers event lists.
*
* @since 1.18.0.
*/
private function register_event_lists() {
/**
* Fires when the Advanced_Tracking class is ready to receive event lists.
*
* This means that Advanced_Tracking class stores the event lists in the Event_List_Registry instance.
*
* @since 1.18.0.
*
* @param Event_List_Registry $event_list_registry
*/
do_action( 'googlesitekit_analytics_register_event_lists', $this->event_list_registry );
foreach ( $this->event_list_registry->get_lists() as $event_list ) {
$event_list->register();
}
}
/**
* Compiles the list of Event objects.
*
* @since 1.18.0.
*/
private function compile_events() {
$this->events = array_reduce(
$this->event_list_registry->get_lists(),
function ( $events, Event_List $event_list ) {
return array_merge( $events, $event_list->get_events() );
},
array()
);
}
}

View File

@ -0,0 +1,64 @@
<?php
/**
* Class Google\Site_Kit\Modules\Analytics\Advanced_Tracking\AMP_Config_Injector
*
* @package Google\Site_Kit\Modules\Analytics
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Modules\Analytics\Advanced_Tracking;
/**
* Class for injecting JavaScript based on the registered event configurations.
*
* @since 1.18.0.
* @access private
* @ignore
*/
final class AMP_Config_Injector {
/**
* Creates list of measurement event configurations and javascript to inject.
*
* @since 1.18.0.
*
* @param array $gtag_amp_opt gtag config options for AMP.
* @param array $events The map of Event objects, keyed by their unique ID.
* @return array Filtered $gtag_amp_opt.
*/
public function inject_event_configurations( $gtag_amp_opt, $events ) {
if ( empty( $events ) ) {
return $gtag_amp_opt;
}
if ( ! array_key_exists( 'triggers', $gtag_amp_opt ) ) {
$gtag_amp_opt['triggers'] = array();
}
foreach ( $events as $amp_trigger_key => $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;
}
}

View File

@ -0,0 +1,109 @@
<?php
/**
* Class Google\Site_Kit\Modules\Analytics\Advanced_Tracking\Event
*
* @package Google\Site_Kit
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Modules\Analytics\Advanced_Tracking;
use Exception;
/**
* Class for representing a single tracking event that Advanced_Tracking tracks.
*
* @since 1.18.0.
* @access private
* @ignore
*/
final class Event implements \JsonSerializable {
/**
* The measurement event's configuration.
*
* @since 1.18.0.
* @var array
*/
private $config;
/**
* Event constructor.
*
* @since 1.18.0.
*
* @param array $config {
* The event's configuration.
*
* @type string $action Required. The event action / event name to send.
* @type string $on Required. The DOM event to send the event for.
* @type string $selector Required, unless $on is 'DOMContentLoaded'. The DOM selector on which to listen
* to the $on event.
* @type array|null $metadata Optional. Associative array of event metadata to send, such as 'event_category',
* 'event_label' etc, or null to not send any extra event data.
* }
* @throws Exception Thrown when config param is undefined.
*/
public function __construct( $config ) {
$this->config = $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;
}
}

View File

@ -0,0 +1,62 @@
<?php
/**
* Class Google\Site_Kit\Modules\Analytics\Advanced_Tracking\Event_List
*
* @package Google\Site_Kit
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Modules\Analytics\Advanced_Tracking;
/**
* Base class representing a tracking event list.
*
* @since 1.18.0.
* @access private
* @ignore
*/
abstract class Event_List {
/**
* Container for events.
*
* @since 1.18.0.
* @var array Map of events for this list, keyed by their unique ID.
*/
private $events = array();
/**
* Adds events or registers WordPress hook callbacks to add events.
*
* Children classes should extend this to add their events, either generically or by dynamically collecting
* metadata through WordPress hooks.
*
* @since 1.18.0.
*/
abstract public function register();
/**
* Adds a measurement event to the measurement events array.
*
* @since 1.18.0.
*
* @param Event $event The measurement event object.
*/
protected function add_event( Event $event ) {
$hash = md5( wp_json_encode( $event ) );
$this->events[ $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;
}
}

View File

@ -0,0 +1,51 @@
<?php
/**
* Class Google\Site_Kit\Modules\Analytics\Advanced_Tracking\Event_List_Registry
*
* @package Google\Site_Kit\Modules\Analytics
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Modules\Analytics\Advanced_Tracking;
/**
* Class for registering third party event lists.
*
* @since 1.18.0.
* @access private
* @ignore
*/
class Event_List_Registry {
/**
* The list of registered event lists.
*
* @since 1.18.0.
* @var Event_List[]
*/
private $event_lists = array();
/**
* Registers an event list.
*
* @since 1.18.0.
*
* @param Event_List $event_list The event list to be registered.
*/
public function register_list( Event_List $event_list ) {
$this->event_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;
}
}

View File

@ -0,0 +1,78 @@
<?php
/**
* Class Google\Site_Kit\Modules\Analytics\Advanced_Tracking\Script_Injector
*
* @package Google\Site_Kit\Modules\Analytics
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Modules\Analytics\Advanced_Tracking;
use Google\Site_Kit\Context;
use Google\Site_Kit\Core\Assets\Manifest;
use Google\Site_Kit\Core\Util\BC_Functions;
/**
* Class for injecting JavaScript based on the registered event configurations.
*
* @since 1.18.0.
* @access private
* @ignore
*/
final class Script_Injector {
/**
* Plugin context.
*
* @since 1.18.0.
* @var Context
*/
protected $context;
/**
* Constructor.
*
* @since 1.18.0.
*
* @param Context $context Plugin context.
*/
public function __construct( Context $context ) {
$this->context = $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 );
}
}

View File

@ -0,0 +1,49 @@
<?php
/**
* Class Google\Site_Kit\Modules\Analytics\Google_Service_AnalyticsProvisioning
*
* @package Google\Site_Kit\Modules\Analytics
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Modules\Analytics;
use Google\Site_Kit_Dependencies\Google_Service_Analytics;
use Google\Site_Kit_Dependencies\Google_Client;
/**
* Class for Analytics Provisioning service.
*
* @since 1.9.0
* @access private
* @ignore
*/
class Google_Service_AnalyticsProvisioning extends Google_Service_Analytics {
/**
* Constructs the internal representation of the Analytics service.
*
* @since 1.9.0
*
* @param Google_Client $client The client used to deliver requests.
* @param string $rootUrl The root URL used for requests to the service.
*/
public function __construct( Google_Client $client, $rootUrl = null ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName
parent::__construct( $client, $rootUrl ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName
$this->provisioning = new Proxy_Provisioning(
$this,
$this->serviceName, // phpcs:ignore WordPress.NamingConventions.ValidVariableName
'provisioning',
array(
'methods' => array(
'createAccountTicket' => array(
'path' => 'provisioning/createAccountTicket',
'httpMethod' => 'POST',
'parameters' => array(),
),
),
)
);
}
}

View File

@ -0,0 +1,79 @@
<?php
/**
* Class Google\Site_Kit\Modules\Analytics\Proxy_AccountTicket
*
* @package Google\Site_Kit\Modules\Analytics
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Modules\Analytics;
use Google\Site_Kit_Dependencies\Google\Service\Analytics\AccountTicket as Google_Service_Analytics_AccountTicket;
/**
* Class for the Analytics provisioning Account Ticket object.
*
* @since 1.9.0
* @access private
* @ignore
*/
class Proxy_AccountTicket extends Google_Service_Analytics_AccountTicket {
/**
* The site ID.
*
* @since 1.9.0
* @var String
*/
public $site_id = '';
/**
* The site secret.
*
* @since 1.9.0
* @var String
*/
public $site_secret = '';
/**
* Gets the site ID.
*
* @since 1.9.0
*/
public function getSiteId() {
return $this->site_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;
}
}

View File

@ -0,0 +1,40 @@
<?php
/**
* Class Google\Site_Kit\Modules\Analytics\Proxy_Provisioning
*
* @package Google\Site_Kit\Modules\Analytics
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Modules\Analytics;
use Google\Site_Kit\Core\Modules\Module_Settings;
use Google\Site_Kit\Core\Storage\Setting_With_Legacy_Keys_Trait;
use Google\Site_Kit\Modules\Analytics\Proxy_AccountTicket;
use \Google\Site_Kit_Dependencies\Google_Service_Resource;
/**
* Class for Analytics provisioning.
*
* @since 1.9.0
* @access private
* @ignore
*/
class Proxy_Provisioning extends Google_Service_Resource {
/**
* Creates an account ticket. (provisioning.createAccountTicket)
*
* @since 1.9.0
*
* @param Proxy_AccountTicket $post_body The post body to send.
* @param array $opt_params Optional parameters. Pass site_id and site_secret here.
* @return Google_Service_Analytics_AccountTicket
*/
public function createAccountTicket( Proxy_AccountTicket $post_body, $opt_params = array() ) {
$params = array( 'postBody' => $post_body );
$params = array_merge( $params, $opt_params );
return $this->call( 'createAccountTicket', array( $params ), 'Google\Site_Kit_Dependencies\Google_Service_Analytics_AccountTicket' );
}
}

View File

@ -0,0 +1,222 @@
<?php
/**
* Class Google\Site_Kit\Modules\Analytics\Settings
*
* @package Google\Site_Kit\Modules\Analytics
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Modules\Analytics;
use Google\Site_Kit\Core\Modules\Module_Settings;
use Google\Site_Kit\Core\Storage\Setting_With_Legacy_Keys_Trait;
use Google\Site_Kit\Core\Storage\Setting_With_Owned_Keys_Interface;
use Google\Site_Kit\Core\Storage\Setting_With_Owned_Keys_Trait;
/**
* Class for Analytics settings.
*
* @since 1.2.0
* @access private
* @ignore
*/
class Settings extends Module_Settings implements Setting_With_Owned_Keys_Interface {
use Setting_With_Legacy_Keys_Trait, Setting_With_Owned_Keys_Trait;
const OPTION = 'googlesitekit_analytics_settings';
/**
* Registers the setting in WordPress.
*
* @since 1.2.0
*/
public function register() {
parent::register();
$this->register_legacy_keys_migration(
array(
'accountId' => 'accountID',
'profileId' => 'profileID',
'propertyId' => 'propertyID',
'internalWebPropertyId' => 'internalWebPropertyID',
)
);
$this->register_owned_keys();
// Backwards compatibility with previous dedicated option.
add_filter(
'default_option_' . self::OPTION,
function ( $default ) {
// Only fallback to the legacy option if the linked state is not filtered.
// This filter is documented below.
if ( is_null( apply_filters( 'googlesitekit_analytics_adsense_linked', null ) ) ) {
$default['adsenseLinked'] = (bool) $this->options->get( 'googlesitekit_analytics_adsense_linked' );
}
// `canUseSnippet` is a computed setting, so this sets the value if settings have not been saved yet.
// This filter is documented below.
$can_use_snippet = apply_filters( 'googlesitekit_analytics_can_use_snippet', true );
if ( is_bool( $can_use_snippet ) ) {
$default['canUseSnippet'] = $can_use_snippet;
}
return $default;
}
);
add_filter(
'option_' . self::OPTION,
function ( $option ) {
/**
* Filters the Google Analytics 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_analytics_account_id', '' );
if ( ! empty( $account_id ) ) {
$option['accountID'] = $account_id;
}
/**
* Filters the Google Analytics property ID to use.
*
* @since 1.0.0
*
* @param string $property_id Empty by default, will fall back to the option value if not set.
*/
$property_id = apply_filters( 'googlesitekit_analytics_property_id', '' );
if ( ! empty( $property_id ) ) {
$option['propertyID'] = $property_id;
}
/**
* Filters the Google Analytics internal web property ID to use.
*
* @since 1.0.0
*
* @param string $internal_web_property_id Empty by default, will fall back to the option value if not set.
*/
$internal_web_property_id = apply_filters( 'googlesitekit_analytics_internal_web_property_id', '' );
if ( ! empty( $internal_web_property_id ) ) {
$option['internalWebPropertyID'] = $internal_web_property_id;
}
/**
* Filters the Google Analytics profile / view ID to use.
*
* @since 1.0.0
*
* @param string $profile_id Empty by default, will fall back to the option value if not set.
*/
$profile_id = apply_filters( 'googlesitekit_analytics_view_id', '' );
if ( ! empty( $profile_id ) ) {
$option['profileID'] = $profile_id;
}
/**
* Filters the linked state of AdSense with Analytics.
*
* This filter exists so that adsenseLinked can only be truthy if the AdSense module is active,
* regardless of the saved setting.
*
* @since 1.3.0
* @param bool $adsense_linked Null by default, will fallback to the option value if not set.
*/
$adsense_linked = apply_filters( 'googlesitekit_analytics_adsense_linked', null );
if ( is_bool( $adsense_linked ) ) {
$option['adsenseLinked'] = $adsense_linked;
}
/**
* Filters the state of the can use snipped setting.
*
* This filter exists so that useSnippet can be restored to true when the Tag Manager module
* is disconnected, ensuring the Analytics snippet is always included.
*
* @since 1.28.0
* @param bool $can_use_snippet Whether or not `useSnippet` can control snippet output. Default: `true`.
*/
$can_use_snippet = apply_filters( 'googlesitekit_analytics_can_use_snippet', true );
if ( is_bool( $can_use_snippet ) ) {
$option['canUseSnippet'] = $can_use_snippet;
}
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',
'internalWebPropertyID',
'profileID',
'propertyID',
);
}
/**
* Gets the default value.
*
* @since 1.2.0
*
* @return array
*/
protected function get_default() {
return array(
'ownerID' => 0,
'accountID' => '',
'adsenseLinked' => false,
'adsConversionID' => '',
'anonymizeIP' => true,
'internalWebPropertyID' => '',
'profileID' => '',
'propertyID' => '',
'trackingDisabled' => array( 'loggedinUsers' ),
'useSnippet' => true,
'canUseSnippet' => true,
);
}
/**
* 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'];
}
if ( isset( $option['canUseSnippet'] ) ) {
$option['canUseSnippet'] = (bool) $option['canUseSnippet'];
}
if ( isset( $option['anonymizeIP'] ) ) {
$option['anonymizeIP'] = (bool) $option['anonymizeIP'];
}
if ( isset( $option['trackingDisabled'] ) ) {
$option['trackingDisabled'] = (array) $option['trackingDisabled'];
}
if ( isset( $option['adsenseLinked'] ) ) {
$option['adsenseLinked'] = (bool) $option['adsenseLinked'];
}
}
return $option;
};
}
}

View File

@ -0,0 +1,36 @@
<?php
/**
* Class Google\Site_Kit\Modules\Analytics\Tag_Guard
*
* @package Google\Site_Kit\Modules\Analytics
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Modules\Analytics;
use Google\Site_Kit\Core\Modules\Tags\Module_Tag_Guard;
/**
* Class for the Analytics tag guard.
*
* @since 1.24.0
* @access private
* @ignore
*/
class Tag_Guard extends Module_Tag_Guard {
/**
* 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();
return $settings['canUseSnippet'] && ! empty( $settings['useSnippet'] ) && ! empty( $settings['propertyID'] );
}
}

View File

@ -0,0 +1,47 @@
<?php
/**
* Class Google\Site_Kit\Modules\Analytics\Tag_Interface
*
* @package Google\Site_Kit\Modules\Analytics
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Modules\Analytics;
/**
* Interface for an Analytics tag.
*
* @since 1.32.0
* @access private
* @ignore
*/
interface Tag_Interface {
/**
* Sets the current home domain.
*
* @since 1.32.0
*
* @param string $domain Domain name.
*/
public function set_home_domain( $domain );
/**
* Sets whether or not to anonymize IP addresses.
*
* @since 1.32.0
*
* @param bool $anonymize_ip Whether to anonymize IP addresses or not.
*/
public function set_anonymize_ip( $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 );
}

View File

@ -0,0 +1,218 @@
<?php
/**
* Class Google\Site_Kit\Modules\Analytics\Web_Tag
*
* @package Google\Site_Kit\Modules\Analytics
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Modules\Analytics;
use Google\Site_Kit\Core\Modules\Tags\Module_Web_Tag;
use Google\Site_Kit\Core\Util\Method_Proxy_Trait;
use Google\Site_Kit\Core\Tags\Tag_With_DNS_Prefetch_Trait;
/**
* Class for Web tag.
*
* @since 1.24.0
* @access private
* @ignore
*/
class Web_Tag extends Module_Web_Tag implements Tag_Interface {
use Method_Proxy_Trait, Tag_With_DNS_Prefetch_Trait;
/**
* Home domain name.
*
* @since 1.24.0
* @var string
*/
private $home_domain;
/**
* Whether or not to anonymize IP addresses.
*
* @since 1.24.0
* @var bool
*/
private $anonymize_ip;
/**
* Ads conversion ID.
*
* @since 1.32.0
* @var string
*/
private $ads_conversion_id;
/**
* Sets the current home domain.
*
* @since 1.24.0
*
* @param string $domain Domain name.
*/
public function set_home_domain( $domain ) {
$this->home_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<!-- %s -->\n", esc_html__( 'Google Analytics snippet added by Site Kit', 'google-site-kit' ) );
$snippet_comment_end = sprintf( "\n<!-- %s -->\n", esc_html__( 'End Google Analytics snippet added by Site Kit', 'google-site-kit' ) );
if ( $block_on_consent_attrs ) {
$tag = str_replace(
array(
"<script src='$gtag_src'", // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript
"<script src=\"$gtag_src\"", // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript
"<script type='text/javascript' src='$gtag_src'", // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript
"<script type=\"text/javascript\" src=\"$gtag_src\"", // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript
),
array( // `type` attribute intentionally excluded in replacements.
"<script{$block_on_consent_attrs} src='$gtag_src'", // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript
"<script{$block_on_consent_attrs} src=\"$gtag_src\"", // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript
"<script{$block_on_consent_attrs} src='$gtag_src'", // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript
"<script{$block_on_consent_attrs} src=\"$gtag_src\"", // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript
),
$tag
);
}
return $snippet_comment_begin . $tag . $snippet_comment_end;
};
add_filter( 'script_loader_tag', $filter_google_gtagjs, 10, 2 );
}
/**
* Adds an inline script to configure ads conversion tracking.
*
* @since 1.32.0
*/
protected function add_inline_ads_conversion_id_config() {
if ( $this->ads_conversion_id ) {
wp_add_inline_script(
'google_gtagjs',
sprintf( 'gtag("config", "%s");', esc_js( $this->ads_conversion_id ) )
);
}
}
}

View File

@ -0,0 +1,762 @@
<?php
/**
* Class Google\Site_Kit\Modules\Analytics_4
*
* @package Google\Site_Kit
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Modules;
use Exception;
use Google\Site_Kit\Core\Assets\Asset;
use Google\Site_Kit\Core\Assets\Script;
use Google\Site_Kit\Core\Authentication\Clients\Google_Site_Kit_Client;
use Google\Site_Kit\Core\Modules\Module;
use Google\Site_Kit\Core\Modules\Module_Settings;
use Google\Site_Kit\Core\Modules\Module_With_Deactivation;
use Google\Site_Kit\Core\Modules\Module_With_Debug_Fields;
use Google\Site_Kit\Core\Modules\Module_With_Assets;
use Google\Site_Kit\Core\Modules\Module_With_Assets_Trait;
use Google\Site_Kit\Core\Modules\Module_With_Scopes;
use Google\Site_Kit\Core\Modules\Module_With_Scopes_Trait;
use Google\Site_Kit\Core\Modules\Module_With_Settings;
use Google\Site_Kit\Core\Modules\Module_With_Settings_Trait;
use Google\Site_Kit\Core\Modules\Module_With_Owner;
use Google\Site_Kit\Core\Modules\Module_With_Owner_Trait;
use Google\Site_Kit\Core\Modules\Module_With_Service_Entity;
use Google\Site_Kit\Core\REST_API\Exception\Invalid_Datapoint_Exception;
use Google\Site_Kit\Core\REST_API\Data_Request;
use Google\Site_Kit\Core\Tags\Guards\Tag_Production_Guard;
use Google\Site_Kit\Core\Tags\Guards\Tag_Verify_Guard;
use Google\Site_Kit\Core\Util\BC_Functions;
use Google\Site_Kit\Core\Util\Debug_Data;
use Google\Site_Kit\Core\Util\Method_Proxy_Trait;
use Google\Site_Kit\Modules\Analytics\Settings as Analytics_Settings;
use Google\Site_Kit\Modules\Analytics_4\Settings;
use Google\Site_Kit\Modules\Analytics_4\Tag_Guard;
use Google\Site_Kit\Modules\Analytics_4\Web_Tag;
use Google\Site_Kit_Dependencies\Google\Model as Google_Model;
use Google\Site_Kit_Dependencies\Google\Service\GoogleAnalyticsAdmin as Google_Service_GoogleAnalyticsAdmin;
use Google\Site_Kit_Dependencies\Google\Service\GoogleAnalyticsAdmin\GoogleAnalyticsAdminV1alphaDataStream;
use Google\Site_Kit_Dependencies\Google\Service\GoogleAnalyticsAdmin\GoogleAnalyticsAdminV1alphaDataStreamWebStreamData;
use Google\Site_Kit_Dependencies\Google\Service\GoogleAnalyticsAdmin\GoogleAnalyticsAdminV1alphaListDataStreamsResponse;
use Google\Site_Kit_Dependencies\Google\Service\GoogleAnalyticsAdmin\GoogleAnalyticsAdminV1alphaProperty as Google_Service_GoogleAnalyticsAdmin_GoogleAnalyticsAdminV1alphaProperty;
use Google\Site_Kit_Dependencies\Psr\Http\Message\RequestInterface;
use stdClass;
use WP_Error;
/**
* Class representing the Analytics 4 module.
*
* @since 1.30.0
* @access private
* @ignore
*/
final class Analytics_4 extends Module
implements Module_With_Scopes, Module_With_Settings, Module_With_Debug_Fields, Module_With_Owner, Module_With_Assets, Module_With_Service_Entity, Module_With_Deactivation {
use Method_Proxy_Trait;
use Module_With_Assets_Trait;
use Module_With_Owner_Trait;
use Module_With_Scopes_Trait;
use Module_With_Settings_Trait;
/**
* Module slug name.
*/
const MODULE_SLUG = 'analytics-4';
/**
* Registers functionality through WordPress hooks.
*
* @since 1.30.0
*/
public function register() {
$this->register_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' => __( 'Youll 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' => __( 'Youll 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_GoogleAnalyticsAdminV1alphaProperty A new property.
*/
private function create_property( $account_id ) {
$timezone = get_option( 'timezone_string' );
if ( empty( $timezone ) ) {
$timezone = 'UTC';
}
$property = new Google_Service_GoogleAnalyticsAdmin_GoogleAnalyticsAdminV1alphaProperty();
$property->setParent( self::normalize_account_id( $account_id ) );
$property->setDisplayName( wp_parse_url( $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 GoogleAnalyticsAdminV1alphaDataStream A new web data stream.
*/
private function create_webdatastream( $property_id ) {
$site_url = $this->context->get_reference_site_url();
$data = new GoogleAnalyticsAdminV1alphaDataStreamWebStreamData();
$data->setDefaultUri( $site_url );
$datastream = new GoogleAnalyticsAdminV1alphaDataStream();
$datastream->setDisplayName( wp_parse_url( $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 array_map( array( self::class, 'filter_property_with_ids' ), $response->getProperties() );
case 'GET:property':
return self::filter_property_with_ids( $response );
case 'GET:webdatastreams':
/* @var GoogleAnalyticsAdminV1alphaListDataStreamsResponse $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 (Alpha)', '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,
);
}
/**
* 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',
),
)
),
);
}
/**
* 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_Production_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 GoogleAnalyticsAdminV1alphaDataStream[] $datastreams Data streams to filter.
* @return GoogleAnalyticsAdminV1alphaDataStream[] Web data streams.
*/
public static function filter_web_datastreams( array $datastreams ) {
return array_filter(
$datastreams,
function ( GoogleAnalyticsAdminV1alphaDataStream $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 GoogleAnalyticsAdminV1alphaListDataStreamsResponse[] $batch_response Array of GoogleAnalyticsAdminV1alphaListWebDataStreamsResponse 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 ) {
$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;
}
}

View File

@ -0,0 +1,95 @@
<?php
/**
* Class Google\Site_Kit\Modules\Analytics_4\Settings
*
* @package Google\Site_Kit\Modules\Analytics_4
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Modules\Analytics_4;
use Google\Site_Kit\Core\Modules\Module_Settings;
use Google\Site_Kit\Core\Storage\Setting_With_Owned_Keys_Interface;
use Google\Site_Kit\Core\Storage\Setting_With_Owned_Keys_Trait;
/**
* Class for Analytics 4 settings.
*
* @since 1.30.0
* @access private
* @ignore
*/
class Settings extends Module_Settings implements Setting_With_Owned_Keys_Interface {
use Setting_With_Owned_Keys_Trait;
const OPTION = 'googlesitekit_analytics-4_settings';
/**
* Registers the setting in WordPress.
*
* @since 1.30.0
*/
public function register() {
parent::register();
$this->register_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',
);
}
/**
* 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,
);
}
/**
* 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'];
}
}
return $option;
};
}
}

View File

@ -0,0 +1,36 @@
<?php
/**
* Class Google\Site_Kit\Modules\Analytics_4\Tag_Guard
*
* @package Google\Site_Kit\Modules\Analytics_4
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Modules\Analytics_4;
use Google\Site_Kit\Core\Modules\Tags\Module_Tag_Guard;
/**
* Class for the Analytics 4 tag guard.
*
* @since 1.31.0
* @access private
* @ignore
*/
class Tag_Guard extends Module_Tag_Guard {
/**
* Determines whether the guarded tag can be activated or not.
*
* @since 1.31.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();
return ! empty( $settings['useSnippet'] ) && ! empty( $settings['measurementID'] );
}
}

View File

@ -0,0 +1,54 @@
<?php
/**
* Class Google\Site_Kit\Modules\Analytics_4\Web_Tag
*
* @package Google\Site_Kit\Modules\Analytics_4
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Modules\Analytics_4;
use Google\Site_Kit\Core\Util\Method_Proxy_Trait;
use Google\Site_Kit\Modules\Analytics\Web_Tag as Analytics_Web_Tag;
/**
* Class for Web tag.
*
* @since 1.31.0
* @access private
* @ignore
*/
class Web_Tag extends Analytics_Web_Tag {
use Method_Proxy_Trait;
/**
* Registers tag hooks.
*
* @since 1.31.0
*/
public function register() {
// Do not call parent::register() because we need to override what is registered in the Analytics::Web_Tag class.
add_action( 'wp_enqueue_scripts', $this->get_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 <MEASUREMENT_ID> 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();
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,57 @@
<?php
/**
* Class Google\Site_Kit\Modules\Idea_Hub\Idea_Interaction_Count
*
* @package Google\Site_Kit\Modules\Idea_Hub
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Modules\Idea_Hub;
use Google\Site_Kit\Core\Storage\User_Setting;
/**
* Class for Idea Hub iteraction tracking.
*
* @since 1.42.0
* @access private
* @ignore
*/
class Idea_Interaction_Count extends User_Setting {
const OPTION = 'googlesitekit_idea-hub_interaction_count';
/**
* Gets the expected value type.
*
* @since 1.42.0
*
* @return string The type name.
*/
protected function get_type() {
return 'integer';
}
/**
* Gets the default value.
*
* @since 1.42.0
*
* @return mixed The default value.
*/
protected function get_default() {
return 0;
}
/**
* Increments the current count by 1.
*
* @since 1.42.0
*/
public function increment() {
$this->set( $this->get() + 1 );
}
}

View File

@ -0,0 +1,26 @@
<?php
/**
* Class Google\Site_Kit\Modules\Idea_Hub\Post_Idea_Name
*
* @package Google\Site_Kit\Modules\Idea_Hub
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Modules\Idea_Hub;
use Google\Site_Kit\Core\Storage\Post_Meta_Setting;
/**
* Class for Idea Hub name setting.
*
* @since 1.33.0
* @access private
* @ignore
*/
class Post_Idea_Name extends Post_Meta_Setting {
const META_KEY = 'googlesitekitpersistent_idea_name';
}

View File

@ -0,0 +1,37 @@
<?php
/**
* Class Google\Site_Kit\Modules\Idea_Hub\Post_Idea_Text
*
* @package Google\Site_Kit\Modules\Idea_Hub
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Modules\Idea_Hub;
use Google\Site_Kit\Core\Storage\Post_Meta_Setting;
/**
* Class for Idea Hub text setting.
*
* @since 1.33.0
* @access private
* @ignore
*/
class Post_Idea_Text extends Post_Meta_Setting {
const META_KEY = 'googlesitekitpersistent_idea_text';
/**
* Gets the `show_in_rest` value for this setting, which should be true.
*
* @since 1.37.0
*
* @return bool Always returns true for this postmeta setting.
*/
protected function get_show_in_rest() {
return true;
}
}

View File

@ -0,0 +1,61 @@
<?php
/**
* Class Google\Site_Kit\Modules\Idea_Hub\Post_Idea_Topics
*
* @package Google\Site_Kit\Modules\Idea_Hub
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Modules\Idea_Hub;
use Google\Site_Kit\Core\Storage\Post_Meta_Setting;
/**
* Class for Idea Hub topics setting.
*
* @since 1.33.0
* @access private
* @ignore
*/
class Post_Idea_Topics extends Post_Meta_Setting {
const META_KEY = 'googlesitekitpersistent_idea_topics';
/**
* Gets the setting type.
*
* @since 1.33.0
*
* @return string The array type.
*/
protected function get_type() {
return 'array';
}
/**
* Returns a sanitize callback.
*
* @since 1.33.0
*
* @return callable Sanitize callback function.
*/
protected function get_sanitize_callback() {
return function ( $option ) {
$sanitized = array();
if ( ! is_array( $option ) ) {
return $sanitized;
}
foreach ( $option as $mid => $display_name ) {
if ( is_string( $mid ) && is_string( $display_name ) ) {
$sanitized[ sanitize_text_field( $mid ) ] = sanitize_text_field( $display_name );
}
}
return $option;
};
}
}

View File

@ -0,0 +1,39 @@
<?php
/**
* Class Google\Site_Kit\Modules\Idea_Hub\Settings
*
* @package Google\Site_Kit\Modules\Idea_Hub
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Modules\Idea_Hub;
use Google\Site_Kit\Core\Modules\Module_Settings;
/**
* Class for Idea Hub settings.
*
* @since 1.32.0
* @access private
* @ignore
*/
class Settings extends Module_Settings {
const OPTION = 'googlesitekit_idea-hub_settings';
/**
* Gets the default value.
*
* @since 1.32.0
*
* @return array
*/
protected function get_default() {
return array(
'tosAccepted' => false,
'ownerID' => 0,
);
}
}

View File

@ -0,0 +1,297 @@
<?php
/**
* Class Google\Site_Kit\Modules\Optimize
*
* @package Google\Site_Kit
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Modules;
use Google\Site_Kit\Core\Assets\Asset;
use Google\Site_Kit\Core\Assets\Script;
use Google\Site_Kit\Core\Modules\Module;
use Google\Site_Kit\Core\Modules\Module_Settings;
use Google\Site_Kit\Core\Modules\Module_With_Deactivation;
use Google\Site_Kit\Core\Modules\Module_With_Debug_Fields;
use Google\Site_Kit\Core\Modules\Module_With_Settings;
use Google\Site_Kit\Core\Modules\Module_With_Settings_Trait;
use Google\Site_Kit\Core\Modules\Module_With_Assets;
use Google\Site_Kit\Core\Modules\Module_With_Assets_Trait;
use Google\Site_Kit\Core\Modules\Module_With_Owner;
use Google\Site_Kit\Core\Modules\Module_With_Owner_Trait;
use Google\Site_Kit\Core\Authentication\Clients\Google_Site_Kit_Client;
use Google\Site_Kit\Core\Modules\Module_With_Service_Entity;
use Google\Site_Kit\Core\Util\Debug_Data;
use Google\Site_Kit\Core\Util\Method_Proxy_Trait;
use Google\Site_Kit\Modules\Optimize\Settings;
use Google\Site_Kit\Modules\Optimize\Web_Tag;
use Google\Site_Kit\Modules\Optimize\Tag_Guard;
/**
* Class representing the Optimize module.
*
* @since 1.0.0
* @access private
* @ignore
*/
final class Optimize extends Module
implements Module_With_Settings, Module_With_Debug_Fields, Module_With_Assets, Module_With_Owner, Module_With_Service_Entity, Module_With_Deactivation {
use Module_With_Settings_Trait, Module_With_Assets_Trait, Module_With_Owner_Trait, Method_Proxy_Trait;
/**
* Module slug name.
*/
const MODULE_SLUG = 'optimize';
/**
* Registers functionality through WordPress hooks.
*
* @since 1.0.0
*/
public function register() {
add_filter( // For non-AMP.
'googlesitekit_gtag_opt',
function( $gtag_config ) {
return $this->gtag_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 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;
}
?>
<amp-experiment>
<script type="application/json">
<?php echo $settings['ampExperimentJSON']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
</script>
</amp-experiment>
<?php
}
/**
* 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(
'optimize_id' => array(
'label' => __( 'Optimize 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',
),
)
),
);
}
/**
* 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();
}
}
}
/**
* 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() {
// TODO: For Optimize module, there is no API to check service entity access. This is a no-op for now that always returns true.
return true;
}
}

View File

@ -0,0 +1,112 @@
<?php
/**
* Class Google\Site_Kit\Modules\Optimize\Settings
*
* @package Google\Site_Kit\Modules\Optimize
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Modules\Optimize;
use Google\Site_Kit\Core\Modules\Module_Settings;
use Google\Site_Kit\Core\Storage\Setting_With_Legacy_Keys_Trait;
use Google\Site_Kit\Core\Storage\Setting_With_Owned_Keys_Interface;
use Google\Site_Kit\Core\Storage\Setting_With_Owned_Keys_Trait;
/**
* Class for Optimize settings.
*
* @since 1.2.0
* @access private
* @ignore
*/
class Settings extends Module_Settings implements Setting_With_Owned_Keys_Interface {
use Setting_With_Legacy_Keys_Trait, Setting_With_Owned_Keys_Trait;
const OPTION = 'googlesitekit_optimize_settings';
/**
* Registers the setting in WordPress.
*
* @since 1.2.0
*/
public function register() {
parent::register();
$this->register_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;
};
}
}

View File

@ -0,0 +1,41 @@
<?php
/**
* Class Google\Site_Kit\Modules\Optimize\Tag_Guard
*
* @package Google\Site_Kit\Modules\Optimize
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Modules\Optimize;
use Google\Site_Kit\Core\Modules\Tags\Module_Tag_Guard;
/**
* Tag guard class for the Optimize module that blocks the tag placement if it is disabled.
*
* @since 1.39.0
* @access private
* @ignore
*/
class Tag_Guard extends Module_Tag_Guard {
/**
* Determines whether the guarded tag can be activated or not.
*
* @since 1.39.0
*
* @return bool TRUE if guarded tag can be activated, otherwise FALSE.
*/
public function can_activate() {
$settings = $this->settings->get();
if ( ! isset( $settings['placeAntiFlickerSnippet'] ) ) {
return false;
}
return $settings['placeAntiFlickerSnippet'];
}
}

View File

@ -0,0 +1,62 @@
<?php
/**
* Class Google\Site_Kit\Modules\Optimize\Web_Tag
*
* @package Google\Site_Kit\Modules\Optimize
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Modules\Optimize;
use Google\Site_Kit\Core\Modules\Tags\Module_Web_Tag;
use Google\Site_Kit\Core\Util\Method_Proxy_Trait;
use Google\Site_Kit\Core\Tags\Tag_With_DNS_Prefetch_Trait;
use Google\Site_Kit\Core\Util\BC_Functions;
/**
* Class for Web tag.
*
* @since 1.39.0
* @access private
* @ignore
*/
class Web_Tag extends Module_Web_Tag {
use Method_Proxy_Trait;
/**
* Registers tag hooks.
*
* @since 1.39.0
*/
public function register() {
add_action( 'wp_head', $this->get_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<!-- %s -->\n", esc_html__( 'Anti-flicker snippet added by Site Kit', 'google-site-kit' ) );
echo '<style>.async-hide { opacity: 0 !important} </style>';
BC_Functions::wp_print_inline_script_tag( $anti_flicker_script );
printf( "\n<!-- %s -->\n", esc_html__( 'End Anti-flicker snippet added by Site Kit', 'google-site-kit' ) );
}
}

View File

@ -0,0 +1,227 @@
<?php
/**
* Class Google\Site_Kit\Modules\PageSpeed_Insights
*
* @package Google\Site_Kit
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Modules;
use Google\Site_Kit\Core\Assets\Script;
use Google\Site_Kit\Core\Modules\Module;
use Google\Site_Kit\Core\Modules\Module_With_Assets;
use Google\Site_Kit\Core\Modules\Module_With_Assets_Trait;
use Google\Site_Kit\Core\Modules\Module_With_Deactivation;
use Google\Site_Kit\Core\Modules\Module_With_Owner;
use Google\Site_Kit\Core\Modules\Module_With_Owner_Trait;
use Google\Site_Kit\Core\Modules\Module_With_Settings;
use Google\Site_Kit\Core\Modules\Module_With_Settings_Trait;
use Google\Site_Kit\Core\Modules\Module_With_Scopes;
use Google\Site_Kit\Core\Modules\Module_With_Scopes_Trait;
use Google\Site_Kit\Core\REST_API\Exception\Invalid_Datapoint_Exception;
use Google\Site_Kit\Core\Authentication\Clients\Google_Site_Kit_Client;
use Google\Site_Kit\Core\REST_API\Data_Request;
use Google\Site_Kit\Core\Util\Feature_Flags;
use Google\Site_Kit\Modules\PageSpeed_Insights\Settings;
use Google\Site_Kit_Dependencies\Google\Service\PagespeedInsights as Google_Service_PagespeedInsights;
use Google\Site_Kit_Dependencies\Psr\Http\Message\RequestInterface;
use WP_Error;
/**
* Class representing the PageSpeed Insights module.
*
* @since 1.0.0
* @access private
* @ignore
*/
final class PageSpeed_Insights extends Module
implements Module_With_Scopes, Module_With_Assets, Module_With_Deactivation, Module_With_Settings, Module_With_Owner {
use Module_With_Scopes_Trait, Module_With_Assets_Trait, Module_With_Settings_Trait, Module_With_Owner_Trait;
/**
* Module slug name.
*/
const MODULE_SLUG = 'pagespeed-insights';
/**
* Registers functionality through WordPress hooks.
*
* @since 1.0.0
*/
public function register() {}
/**
* Cleans up when the module is deactivated.
*
* @since 1.0.0
*/
public function on_deactivation() {
$this->get_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',
),
)
),
);
}
/**
* 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',
);
}
}

View File

@ -0,0 +1,36 @@
<?php
/**
* Class Google\Site_Kit\Modules\PageSpeed_Insights\Settings
*
* @package Google\Site_Kit\Modules\PageSpeed_Insights
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Modules\PageSpeed_Insights;
use Google\Site_Kit\Core\Modules\Module_Settings;
/**
* Class for PageSpeed Insights settings.
*
* @since 1.49.0
* @access private
* @ignore
*/
class Settings extends Module_Settings {
const OPTION = 'googlesitekit_pagespeed-insights_settings';
/**
* Gets the default value.
*
* @since 1.49.0
*
* @return array
*/
protected function get_default() {
return array( 'ownerID' => 0 );
}
}

View File

@ -0,0 +1,602 @@
<?php
/**
* Class Google\Site_Kit\Modules\Search_Console
*
* @package Google\Site_Kit
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Modules;
use Google\Site_Kit\Core\Modules\Module;
use Google\Site_Kit\Core\Modules\Module_Settings;
use Google\Site_Kit\Core\Modules\Module_With_Debug_Fields;
use Google\Site_Kit\Core\Modules\Module_With_Owner;
use Google\Site_Kit\Core\Modules\Module_With_Owner_Trait;
use Google\Site_Kit\Core\Modules\Module_With_Screen;
use Google\Site_Kit\Core\Modules\Module_With_Screen_Trait;
use Google\Site_Kit\Core\Modules\Module_With_Scopes;
use Google\Site_Kit\Core\Modules\Module_With_Scopes_Trait;
use Google\Site_Kit\Core\Authentication\Clients\Google_Site_Kit_Client;
use Google\Site_Kit\Core\Modules\Module_With_Settings;
use Google\Site_Kit\Core\Modules\Module_With_Settings_Trait;
use Google\Site_Kit\Core\Modules\Module_With_Assets;
use Google\Site_Kit\Core\Modules\Module_With_Assets_Trait;
use Google\Site_Kit\Core\Modules\Module_With_Service_Entity;
use Google\Site_Kit\Core\Permissions\Permissions;
use Google\Site_Kit\Core\REST_API\Exception\Invalid_Datapoint_Exception;
use Google\Site_Kit\Core\Assets\Script;
use Google\Site_Kit\Core\REST_API\Data_Request;
use Google\Site_Kit\Core\Util\Feature_Flags;
use Google\Site_Kit\Core\Util\Google_URL_Matcher_Trait;
use Google\Site_Kit\Core\Util\Google_URL_Normalizer;
use Google\Site_Kit\Modules\Search_Console\Settings;
use Google\Site_Kit_Dependencies\Google\Service\Exception as Google_Service_Exception;
use Google\Site_Kit_Dependencies\Google\Service\SearchConsole as Google_Service_SearchConsole;
use Google\Site_Kit_Dependencies\Google\Service\SearchConsole\SitesListResponse as Google_Service_SearchConsole_SitesListResponse;
use Google\Site_Kit_Dependencies\Google\Service\SearchConsole\WmxSite as Google_Service_SearchConsole_WmxSite;
use Google\Site_Kit_Dependencies\Google\Service\SearchConsole\SearchAnalyticsQueryRequest as Google_Service_SearchConsole_SearchAnalyticsQueryRequest;
use Google\Site_Kit_Dependencies\Google\Service\SearchConsole\ApiDimensionFilter as Google_Service_SearchConsole_ApiDimensionFilter;
use Google\Site_Kit_Dependencies\Google\Service\SearchConsole\ApiDimensionFilterGroup as Google_Service_SearchConsole_ApiDimensionFilterGroup;
use Google\Site_Kit_Dependencies\Psr\Http\Message\ResponseInterface;
use Google\Site_Kit_Dependencies\Psr\Http\Message\RequestInterface;
use WP_Error;
use Exception;
/**
* Class representing the Search Console module.
*
* @since 1.0.0
* @access private
* @ignore
*/
final class Search_Console extends Module
implements Module_With_Screen, Module_With_Scopes, Module_With_Settings, Module_With_Assets, Module_With_Debug_Fields, Module_With_Owner, Module_With_Service_Entity {
use Module_With_Screen_Trait, Module_With_Scopes_Trait, Module_With_Settings_Trait, Google_URL_Matcher_Trait, Module_With_Assets_Trait, Module_With_Owner_Trait;
/**
* Module slug name.
*/
const MODULE_SLUG = 'search-console';
/**
* Registers functionality through WordPress hooks.
*
* @since 1.0.0
*/
public function register() {
$this->register_scopes_hook();
if ( ! Feature_Flags::enabled( 'unifiedDashboard' ) ) {
$this->register_screen_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.
if ( ! empty( $token_response['search_console_property'] ) ) {
$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 = $this->map_sites( (array) $response->getSiteEntry() );
$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',
),
)
),
);
}
/**
* 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;
}
}

View File

@ -0,0 +1,77 @@
<?php
/**
* Class Google\Site_Kit\Modules\Search_Console\Settings
*
* @package Google\Site_Kit\Modules\Search_Console
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Modules\Search_Console;
use Google\Site_Kit\Core\Modules\Module_Settings;
use Google\Site_Kit\Core\Storage\Setting_With_Owned_Keys_Interface;
use Google\Site_Kit\Core\Storage\Setting_With_Owned_Keys_Trait;
/**
* Class for Search Console settings.
*
* @since 1.3.0
* @access private
* @ignore
*/
class Settings extends Module_Settings implements Setting_With_Owned_Keys_Interface {
use Setting_With_Owned_Keys_Trait;
const OPTION = 'googlesitekit_search-console_settings';
/**
* Registers the setting in WordPress.
*
* @since 1.3.0
*/
public function register() {
parent::register();
$this->register_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',
);
}
}

View File

@ -0,0 +1,506 @@
<?php
/**
* Class Google\Site_Kit\Modules\Site_Verification
*
* @package Google\Site_Kit
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Modules;
use Google\Site_Kit\Core\Authentication\Clients\Google_Site_Kit_Client;
use Google\Site_Kit\Core\Authentication\Verification;
use Google\Site_Kit\Core\Authentication\Verification_File;
use Google\Site_Kit\Core\Authentication\Verification_Meta;
use Google\Site_Kit\Core\Modules\Module;
use Google\Site_Kit\Core\Modules\Module_With_Scopes;
use Google\Site_Kit\Core\Modules\Module_With_Scopes_Trait;
use Google\Site_Kit\Core\REST_API\Exception\Invalid_Datapoint_Exception;
use Google\Site_Kit\Core\Permissions\Permissions;
use Google\Site_Kit\Core\REST_API\Data_Request;
use Google\Site_Kit\Core\Storage\Transients;
use Google\Site_Kit\Core\Util\Exit_Handler;
use Google\Site_Kit\Core\Util\Google_URL_Matcher_Trait;
use Google\Site_Kit\Core\Util\Method_Proxy_Trait;
use Google\Site_Kit_Dependencies\Google\Service\Exception as Google_Service_Exception;
use Google\Site_Kit_Dependencies\Google\Service\SiteVerification as Google_Service_SiteVerification;
use Google\Site_Kit_Dependencies\Google\Service\SiteVerification\SiteVerificationWebResourceGettokenRequest as Google_Service_SiteVerification_SiteVerificationWebResourceGettokenRequest;
use Google\Site_Kit_Dependencies\Google\Service\SiteVerification\SiteVerificationWebResourceGettokenRequestSite as Google_Service_SiteVerification_SiteVerificationWebResourceGettokenRequestSite;
use Google\Site_Kit_Dependencies\Google\Service\SiteVerification\SiteVerificationWebResourceResource as Google_Service_SiteVerification_SiteVerificationWebResourceResource;
use Google\Site_Kit_Dependencies\Google\Service\SiteVerification\SiteVerificationWebResourceResourceSite as Google_Service_SiteVerification_SiteVerificationWebResourceResourceSite;
use Google\Site_Kit_Dependencies\Psr\Http\Message\RequestInterface;
use WP_Error;
use Exception;
/**
* Class representing the Site Verification module.
*
* @since 1.0.0
* @access private
* @ignore
*/
final class Site_Verification extends Module implements Module_With_Scopes {
use Method_Proxy_Trait;
use Module_With_Scopes_Trait;
use Google_URL_Matcher_Trait;
/**
* Module slug name.
*/
const MODULE_SLUG = 'site-verification';
/**
* Meta site verification type.
*/
const VERIFICATION_TYPE_META = 'META';
/**
* File site verification type.
*/
const VERIFICATION_TYPE_FILE = 'FILE';
/**
* Verification meta tag cache key.
*/
const TRANSIENT_VERIFICATION_META_TAGS = 'googlesitekit_verification_meta_tags';
/**
* Registers functionality through WordPress hooks.
*
* @since 1.0.0
*/
public function register() {
$this->register_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<token>[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, '<meta ' ) ) {
$verification_tag = '<meta name="google-site-verification" content="' . esc_attr( $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;
}
}

View File

@ -0,0 +1,154 @@
<?php
/**
* Class Google\Site_Kit\Modules\Subscribe_With_Google
*
* @package Google\Site_Kit
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Modules;
use Google\Site_Kit\Core\Assets\Asset;
use Google\Site_Kit\Core\Assets\Script;
use Google\Site_Kit\Core\Modules\Module;
use Google\Site_Kit\Core\Modules\Module_Settings;
use Google\Site_Kit\Core\Modules\Module_With_Assets;
use Google\Site_Kit\Core\Modules\Module_With_Assets_Trait;
use Google\Site_Kit\Core\Modules\Module_With_Deactivation;
use Google\Site_Kit\Core\Modules\Module_With_Settings;
use Google\Site_Kit\Core\Modules\Module_With_Settings_Trait;
use Google\Site_Kit\Core\Modules\Module_With_Owner;
use Google\Site_Kit\Core\Modules\Module_With_Owner_Trait;
use Google\Site_Kit\Core\Util\Method_Proxy_Trait;
use Google\Site_Kit\Modules\Subscribe_With_Google\Settings;
/**
* Class representing the Subscribe with Google module.
*
* @since 1.41.0
* @access private
* @ignore
*/
final class Subscribe_With_Google extends Module
implements Module_With_Assets, Module_With_Deactivation, Module_With_Owner, Module_With_Settings {
use Method_Proxy_Trait;
use Module_With_Assets_Trait;
use Module_With_Owner_Trait;
use Module_With_Settings_Trait;
/**
* Module slug name.
*/
const MODULE_SLUG = 'subscribe-with-google';
/**
* Registers functionality through WordPress hooks.
*
* @since 1.41.0
*/
public function register() {
if ( ! $this->is_connected() ) {
return;
}
// TODO: Bring back SwG functionality after #3120 is merged.
}
/**
* Checks whether the module is connected.
*
* A module being connected means that all steps required as part of its activation are completed.
*
* @since 1.41.0
*
* @return bool True if module is connected, false otherwise.
*/
public function is_connected() {
$settings = $this->get_settings()->get();
if ( ! $settings ) {
return false;
}
if ( ! $settings['products'] ) {
return false;
}
if ( ! $settings['publicationID'] ) {
return false;
}
if ( ! $settings['revenueModel'] ) {
return false;
}
return parent::is_connected();
}
/**
* Cleans up when the module is deactivated.
*
* @since 1.41.0
*/
public function on_deactivation() {
$this->get_settings()->delete();
}
/**
* Sets up information about the module.
*
* @since 1.41.0
*
* @return array Associative array of module info.
*/
protected function setup_info() {
return array(
'slug' => 'subscribe-with-google',
'name' => _x( 'Subscribe with Google', 'Service name', 'google-site-kit' ),
'description' => __( 'Generate revenue through your content by adding subscriptions or contributions to your publication', 'google-site-kit' ),
'order' => 7,
'homepage' => __( 'https://publishercenter.google.com/', 'google-site-kit' ),
);
}
/**
* Sets up the module's settings instance.
*
* @since 1.41.0
*
* @return Module_Settings
*/
protected function setup_settings() {
return new Settings( $this->options );
}
/**
* Sets up the module's assets to register.
*
* @since 1.41.0
*
* @return Asset[] List of Asset objects.
*/
protected function setup_assets() {
$base_url = $this->context->url( 'dist/assets/' );
return array(
new Script(
'googlesitekit-modules-subscribe-with-google',
array(
'src' => $base_url . 'js/googlesitekit-modules-subscribe-with-google.js',
'dependencies' => array(
'googlesitekit-api',
'googlesitekit-data',
'googlesitekit-datastore-site',
'googlesitekit-modules',
'googlesitekit-vendor',
),
)
),
);
}
}

View File

@ -0,0 +1,70 @@
<?php
/**
* Class Google\Site_Kit\Modules\Subscribe_With_Google\Settings
*
* @package Google\Site_Kit\Modules\Subscribe_With_Google
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Modules\Subscribe_With_Google;
use Google\Site_Kit\Core\Modules\Module_Settings;
use Google\Site_Kit\Core\Storage\Setting_With_Owned_Keys_Interface;
use Google\Site_Kit\Core\Storage\Setting_With_Owned_Keys_Trait;
/**
* Class for Tag Manager settings.
*
* @since 1.41.0
* @access private
* @ignore
*/
class Settings extends Module_Settings implements Setting_With_Owned_Keys_Interface {
use Setting_With_Owned_Keys_Trait;
const OPTION = 'googlesitekit_subscribe-with-google_settings';
/**
* Registers the setting in WordPress.
*
* @since 1.41.0
*/
public function register() {
parent::register();
$this->register_owned_keys();
}
/**
* Returns keys for owned settings.
*
* @since 1.41.0
*
* @return array An array of keys for owned settings.
*/
public function get_owned_keys() {
return array(
'products',
'publicationID',
'revenueModel',
);
}
/**
* Gets the default value.
*
* @since 1.41.0
*
* @return array
*/
protected function get_default() {
return array(
'ownerID' => '',
'products' => array(),
'publicationID' => '',
'revenueModel' => '',
);
}
}

View File

@ -0,0 +1,696 @@
<?php
/**
* Class Google\Site_Kit\Modules\Tag_Manager
*
* @package Google\Site_Kit
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Modules;
use Exception;
use Google\Site_Kit\Context;
use Google\Site_Kit\Core\Assets\Asset;
use Google\Site_Kit\Core\Assets\Script;
use Google\Site_Kit\Core\Authentication\Clients\Google_Site_Kit_Client;
use Google\Site_Kit\Core\Modules\Module;
use Google\Site_Kit\Core\Modules\Module_Settings;
use Google\Site_Kit\Core\Modules\Module_With_Assets;
use Google\Site_Kit\Core\Modules\Module_With_Assets_Trait;
use Google\Site_Kit\Core\Modules\Module_With_Deactivation;
use Google\Site_Kit\Core\Modules\Module_With_Debug_Fields;
use Google\Site_Kit\Core\Modules\Module_With_Owner;
use Google\Site_Kit\Core\Modules\Module_With_Owner_Trait;
use Google\Site_Kit\Core\Modules\Module_With_Scopes;
use Google\Site_Kit\Core\Modules\Module_With_Scopes_Trait;
use Google\Site_Kit\Core\Modules\Module_With_Settings;
use Google\Site_Kit\Core\Modules\Module_With_Settings_Trait;
use Google\Site_Kit\Core\REST_API\Data_Request;
use Google\Site_Kit\Core\REST_API\Exception\Invalid_Datapoint_Exception;
use Google\Site_Kit\Core\Tags\Guards\Tag_Production_Guard;
use Google\Site_Kit\Core\Tags\Guards\Tag_Verify_Guard;
use Google\Site_Kit\Core\Util\BC_Functions;
use Google\Site_Kit\Core\Util\Debug_Data;
use Google\Site_Kit\Core\Util\Method_Proxy_Trait;
use Google\Site_Kit\Modules\Tag_Manager\AMP_Tag;
use Google\Site_Kit\Modules\Tag_Manager\Settings;
use Google\Site_Kit\Modules\Tag_Manager\Tag_Guard;
use Google\Site_Kit\Modules\Tag_Manager\Web_Tag;
use Google\Site_Kit_Dependencies\Google\Service\TagManager as Google_Service_TagManager;
use Google\Site_Kit_Dependencies\Google\Service\TagManager\Account as Google_Service_TagManager_Account;
use Google\Site_Kit_Dependencies\Google\Service\TagManager\Container as Google_Service_TagManager_Container;
use Google\Site_Kit_Dependencies\Google\Service\TagManager\ListAccountsResponse as Google_Service_TagManager_ListAccountsResponse;
use Google\Site_Kit_Dependencies\Google\Service\TagManager\ListContainersResponse as Google_Service_TagManager_ListContainersResponse;
use Google\Site_Kit_Dependencies\Psr\Http\Message\RequestInterface;
use WP_Error;
/**
* Class representing the Tag Manager module.
*
* @since 1.0.0
* @access private
* @ignore
*/
final class Tag_Manager extends Module
implements Module_With_Scopes, Module_With_Settings, Module_With_Assets, Module_With_Debug_Fields, Module_With_Owner, Module_With_Deactivation {
use Method_Proxy_Trait;
use Module_With_Assets_Trait;
use Module_With_Owner_Trait;
use Module_With_Scopes_Trait;
use Module_With_Settings_Trait;
/**
* Module slug name.
*/
const MODULE_SLUG = 'tagmanager';
/**
* Container usage context for web.
*/
const USAGE_CONTEXT_WEB = 'web';
/**
* Container usage context for AMP.
*/
const USAGE_CONTEXT_AMP = 'amp';
/**
* Map of container usageContext to option key for containerID.
*
* @var array
*/
protected $context_map = array(
self::USAGE_CONTEXT_WEB => '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_action( 'googlesitekit_analytics_can_use_snippet', $this->get_method_proxy( 'can_analytics_use_snippet' ) );
// 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' ),
'GET:tag-permission' => 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' ) ?: wp_parse_url( $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']}"
);
case 'GET:tag-permission':
return function () use ( $data ) {
$container_id = $data['containerID'];
if ( ! $container_id ) {
return new WP_Error(
'missing_required_param',
/* translators: %s: Missing parameter name */
sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'containerID' ),
array( 'status' => 400 )
);
}
$accounts = $this->get_data( 'accounts' );
if ( is_wp_error( $accounts ) ) {
return $accounts;
}
$response = array(
'accountID' => '',
'containerID' => $container_id,
'permission' => false,
);
try {
$account_container = $this->get_account_for_container( $container_id, $accounts );
$response['accountID'] = $account_container['account']['accountId'];
$response['permission'] = true;
// Return full `account` and `container` for backwards compat with legacy setup component.
// TODO: Remove $account_container from response.
return array_merge( $response, $account_container );
} catch ( Exception $exception ) {
return $response;
}
};
}
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' ) ?: wp_parse_url( $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 $response->getAccount();
case 'GET:accounts-containers':
/* @var Google_Service_TagManager_ListAccountsResponse $response List accounts response. */
$response = array(
// TODO: Parse this response to a regular array.
'accounts' => $response->getAccount(),
'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 array_values( $containers );
}
return parent::parse_data_response( $data, $response );
}
/**
* Finds the account for the given container *public ID* from the given list of accounts.
*
* There is no way to query a container by its public ID (the ID that identifies the container on the client)
* so we must find it by listing the containers of the available accounts and matching on the public ID.
*
* @since 1.2.0
*
* @param string $container_id Container public ID (e.g. GTM-ABCDEFG).
* @param Google_Service_TagManager_Account[] $accounts All accounts available to the current user.
*
* @return array {
* @type Google_Service_TagManager_Account $account Account model instance.
* @type Google_Service_TagManager_Container $container Container model instance.
* }
* @throws Exception Thrown if the given container ID does not belong to any of the given accounts.
*/
private function get_account_for_container( $container_id, $accounts ) {
foreach ( (array) $accounts as $account ) {
/* @var Google_Service_TagManager_Account $account Tag manager account */
$containers = $this->get_data(
'containers',
array(
'accountID' => $account->getAccountId(),
'usageContext' => array_keys( $this->context_map ),
)
);
if ( is_wp_error( $containers ) ) {
break;
}
foreach ( (array) $containers as $container ) {
/* @var Google_Service_TagManager_Container $container Container instance */
if ( $container_id === $container->getPublicId() ) {
return compact( 'account', 'container' );
}
}
}
throw new Exception( __( 'No account found for given container', 'google-site-kit' ) );
}
/**
* 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/' );
return array(
new Script(
'googlesitekit-modules-tagmanager',
array(
'src' => $base_url . 'js/googlesitekit-modules-tagmanager.js',
'dependencies' => array(
'googlesitekit-api',
'googlesitekit-data',
'googlesitekit-datastore-site',
'googlesitekit-modules',
'googlesitekit-modules-analytics',
'googlesitekit-vendor',
),
)
),
);
}
/**
* 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_Production_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
*
* @param boolean $original_value Original value of useSnippet setting.
* @return boolean Filtered value.
*/
private function can_analytics_use_snippet( $original_value ) {
$settings = $this->get_settings()->get();
// This disables the Analytics snippet if there is a GA tag in the
// configured containers, and the GTM snippet is enabled.
if ( ! empty( $settings['gaPropertyID'] ) && $settings['useSnippet'] ) {
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;
}
}

View File

@ -0,0 +1,78 @@
<?php
/**
* Class Google\Site_Kit\Modules\Tag_Manager\AMP_Tag
*
* @package Google\Site_Kit\Modules\Tag_Manager
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Modules\Tag_Manager;
use Google\Site_Kit\Core\Modules\Tags\Module_AMP_Tag;
use Google\Site_Kit\Core\Util\Method_Proxy_Trait;
/**
* Class for AMP tag.
*
* @since 1.24.0
* @access private
* @ignore
*/
class AMP_Tag extends Module_AMP_Tag {
use Method_Proxy_Trait;
/**
* Registers tag hooks.
*
* @since 1.24.0
*/
public function register() {
$render = $this->get_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 <amp-analytics> 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<!-- %s -->\n", esc_html__( 'Google Tag Manager AMP snippet added by Site Kit', 'google-site-kit' ) );
printf(
'<amp-analytics config="%s" data-credentials="include"%s><script type="application/json">%s</script></amp-analytics>',
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<!-- %s -->\n", esc_html__( 'End Google Tag Manager AMP snippet added by Site Kit', 'google-site-kit' ) );
}
}

View File

@ -0,0 +1,104 @@
<?php
/**
* Class Google\Site_Kit\Modules\Tag_Manager\Settings
*
* @package Google\Site_Kit\Modules\Tag_Manager
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Modules\Tag_Manager;
use Google\Site_Kit\Core\Modules\Module_Settings;
use Google\Site_Kit\Core\Storage\Setting_With_Legacy_Keys_Trait;
use Google\Site_Kit\Core\Storage\Setting_With_Owned_Keys_Interface;
use Google\Site_Kit\Core\Storage\Setting_With_Owned_Keys_Trait;
/**
* Class for Tag Manager settings.
*
* @since 1.2.0
* @access private
* @ignore
*/
class Settings extends Module_Settings implements Setting_With_Owned_Keys_Interface {
use Setting_With_Legacy_Keys_Trait, Setting_With_Owned_Keys_Trait;
const OPTION = 'googlesitekit_tagmanager_settings';
/**
* 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',
'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;
};
}
}

View File

@ -0,0 +1,59 @@
<?php
/**
* Class Google\Site_Kit\Modules\Tag_Manager\Tag_Guard
*
* @package Google\Site_Kit\Modules\Tag_Manager
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Modules\Tag_Manager;
use Google\Site_Kit\Core\Modules\Module_Settings;
use Google\Site_Kit\Core\Modules\Tags\Module_Tag_Guard;
/**
* Class for the Tag Manager tag guard.
*
* @since 1.24.0
* @access private
* @ignore
*/
class Tag_Guard extends Module_Tag_Guard {
/**
* Determines AMP mode.
*
* @since 1.24.0
* @var bool
*/
protected $is_amp;
/**
* Constructor.
*
* @since 1.24.0
*
* @param Module_Settings $settings Module settings.
* @param bool $is_amp AMP mode.
*/
public function __construct( Module_Settings $settings, $is_amp ) {
parent::__construct( $settings );
$this->is_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 );
}
}

View File

@ -0,0 +1,104 @@
<?php
/**
* Class Google\Site_Kit\Modules\Tag_Manager\Web_Tag
*
* @package Google\Site_Kit\Modules\Tag_Manager
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Modules\Tag_Manager;
use Google\Site_Kit\Core\Modules\Tags\Module_Web_Tag;
use Google\Site_Kit\Core\Util\Method_Proxy_Trait;
use Google\Site_Kit\Core\Tags\Tag_With_DNS_Prefetch_Trait;
use Google\Site_Kit\Core\Util\BC_Functions;
/**
* Class for Web tag.
*
* @since 1.24.0
* @access private
* @ignore
*/
class Web_Tag extends Module_Web_Tag {
use Method_Proxy_Trait, Tag_With_DNS_Prefetch_Trait;
/**
* Registers tag hooks.
*
* @since 1.24.0
*/
public function register() {
$render_no_js = $this->get_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<!-- %s -->\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<!-- %s -->\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 );
?>
<!-- <?php esc_html_e( 'Google Tag Manager (noscript) snippet added by Site Kit', 'google-site-kit' ); ?> -->
<noscript>
<iframe src="<?php echo esc_url( $iframe_src ); ?>" height="0" width="0" style="display:none;visibility:hidden"></iframe>
</noscript>
<!-- <?php esc_html_e( 'End Google Tag Manager (noscript) snippet added by Site Kit', 'google-site-kit' ); ?> -->
<?php
}
}