Commit inicial - WordPress Análisis de Precios Unitarios

- WordPress core y plugins
- Tema Twenty Twenty-Four configurado
- Plugin allow-unfiltered-html.php simplificado
- .gitignore configurado para excluir wp-config.php y uploads

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
root
2025-11-03 21:04:30 -06:00
commit a22573bf0b
24068 changed files with 4993111 additions and 0 deletions

File diff suppressed because it is too large Load Diff

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,97 @@
<?php
/**
* Class Google\Site_Kit\Modules\AdSense\Ad_Blocking_Recovery_Tag
*
* @package Google\Site_Kit\Modules\AdSense
* @copyright 2023 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\Storage\Setting;
/**
* Class for AdSense Ad blocking recovery Tag.
*
* @since 1.104.0
* @access private
* @ignore
*/
class Ad_Blocking_Recovery_Tag extends Setting {
const OPTION = 'googlesitekit_adsense_ad_blocking_recovery_tag';
/**
* Gets ad blocking recovery tag.
*
* @since 1.104.0
*
* @return array Array with tag and error protection code.
*/
public function get() {
$option = parent::get();
if ( ! $this->is_valid_tag_object( $option ) ) {
return $this->get_default();
}
return $option;
}
/**
* Sets ad blocking recovery tag.
*
* @since 1.104.0
*
* @param array $value Array with tag and error protection code.
*
* @return bool True on success, false on failure.
*/
public function set( $value ) {
if ( ! $this->is_valid_tag_object( $value ) ) {
return false;
}
return parent::set( $value );
}
/**
* Gets the expected value type.
*
* @since 1.104.0
*
* @return string The type name.
*/
protected function get_type() {
return 'object';
}
/**
* Gets the default value.
*
* @since 1.104.0
*
* @return array
*/
protected function get_default() {
return array(
'tag' => '',
'error_protection_code' => '',
);
}
/**
* Determines whether the given value is a valid tag object.
*
* @since 1.104.0
*
* @param mixed $tag Tag object.
*
* @return bool TRUE if valid, otherwise FALSE.
*/
private function is_valid_tag_object( $tag ) {
return is_array( $tag ) && isset( $tag['tag'] ) && isset( $tag['error_protection_code'] ) && is_string( $tag['tag'] ) && is_string( $tag['error_protection_code'] );
}
}

View File

@@ -0,0 +1,36 @@
<?php
/**
* Class Google\Site_Kit\Modules\AdSense\Ad_Blocking_Recovery_Tag_Guard
*
* @package Google\Site_Kit\Modules\AdSense
* @copyright 2023 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 Ad Blocking Recovery tag guard.
*
* @since 1.105.0
* @access private
* @ignore
*/
class Ad_Blocking_Recovery_Tag_Guard extends Module_Tag_Guard {
/**
* Determines whether the guarded tag can be activated or not.
*
* @since 1.105.0
*
* @return bool TRUE if guarded tag can be activated, otherwise FALSE or an error.
*/
public function can_activate() {
$settings = $this->settings->get();
return ! empty( $settings['adBlockingRecoverySetupStatus'] ) && $settings['useAdBlockingRecoverySnippet'];
}
}

View File

@@ -0,0 +1,95 @@
<?php
/**
* Class Google\Site_Kit\Modules\AdSense\Ad_Blocking_Recovery_Web_Tag
*
* @package Google\Site_Kit\Modules\AdSense
* @copyright 2023 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\Tags\Tag;
use Google\Site_Kit\Core\Util\Method_Proxy_Trait;
use Google\Site_Kit\Core\Tags\Tag_With_DNS_Prefetch_Trait;
/**
* Class for Ad Blocking Recovery tag.
*
* @since 1.105.0
* @access private
* @ignore
*/
class Ad_Blocking_Recovery_Web_Tag extends Tag {
use Method_Proxy_Trait;
use Tag_With_DNS_Prefetch_Trait;
/**
* Ad_Blocking_Recovery_Tag instance.
*
* @since 1.105.0
* @var Ad_Blocking_Recovery_Tag
*/
protected $ad_blocking_recovery_tag;
/**
* Use Error Protection Snippet.
*
* @since 1.105.0
* @var bool
*/
protected $use_error_protection_snippet;
/**
* Constructor.
*
* @since 1.105.0
*
* @param Ad_Blocking_Recovery_Tag $ad_blocking_recovery_tag Ad_Blocking_Recovery_Tag instance.
* @param bool $use_error_protection_snippet Use Error Protection Snippet.
*/
public function __construct( Ad_Blocking_Recovery_Tag $ad_blocking_recovery_tag, $use_error_protection_snippet ) {
$this->ad_blocking_recovery_tag = $ad_blocking_recovery_tag;
$this->use_error_protection_snippet = $use_error_protection_snippet;
}
/**
* Registers tag hooks.
*
* @since 1.105.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( '//fundingchoicesmessages.google.com' ),
10,
2
);
}
/**
* Outputs the AdSense script tag.
*
* @since 1.105.0
*/
protected function render() {
$tags = $this->ad_blocking_recovery_tag->get();
if ( empty( $tags['tag'] ) || empty( $tags['error_protection_code'] ) ) {
return;
}
printf( "\n<!-- %s -->\n", esc_html__( 'Google AdSense Ad Blocking Recovery snippet added by Site Kit', 'google-site-kit' ) );
echo $tags['tag']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
printf( "\n<!-- %s -->\n", esc_html__( 'End Google AdSense Ad Blocking Recovery snippet added by Site Kit', 'google-site-kit' ) );
if ( $this->use_error_protection_snippet ) {
printf( "\n<!-- %s -->\n", esc_html__( 'Google AdSense Ad Blocking Recovery Error Protection snippet added by Site Kit', 'google-site-kit' ) );
echo $tags['error_protection_code']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
printf( "\n<!-- %s -->\n", esc_html__( 'End Google AdSense Ad Blocking Recovery Error Protection snippet added by Site Kit', 'google-site-kit' ) );
}
}
}

View File

@@ -0,0 +1,46 @@
<?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,256 @@
<?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;
use Google\Site_Kit\Core\Storage\Setting_With_ViewOnly_Keys_Interface;
/**
* Class for AdSense settings.
*
* @since 1.2.0
* @access private
* @ignore
*/
class Settings extends Module_Settings implements Setting_With_Owned_Keys_Interface, Setting_With_ViewOnly_Keys_Interface {
use Setting_With_Legacy_Keys_Trait;
use Setting_With_Owned_Keys_Trait;
const OPTION = 'googlesitekit_adsense_settings';
/**
* Various ad blocking recovery setup statuses.
*/
const AD_BLOCKING_RECOVERY_SETUP_STATUS_TAG_PLACED = 'tag-placed';
const AD_BLOCKING_RECOVERY_SETUP_STATUS_SETUP_CONFIRMED = 'setup-confirmed';
/**
* 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;
}
);
add_filter(
'pre_update_option_' . self::OPTION,
function ( $value, $old_value ) {
if ( isset( $old_value['setupCompletedTimestamp'] ) ) {
return $value;
}
if ( ! empty( $old_value['accountStatus'] ) && ! empty( $old_value['siteStatus'] ) && 'ready' === $old_value['accountStatus'] && 'ready' === $old_value['siteStatus'] ) {
$value['setupCompletedTimestamp'] = strtotime( '-1 month' );
} elseif ( ! empty( $value['accountStatus'] ) && ! empty( $value['siteStatus'] ) && 'ready' === $value['accountStatus'] && 'ready' === $value['siteStatus'] ) {
$value['setupCompletedTimestamp'] = time();
}
return $value;
},
10,
2
);
}
/**
* 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',
);
}
/**
* Returns keys for view-only settings.
*
* @since 1.122.0
*
* @return array An array of keys for view-only settings.
*/
public function get_view_only_keys() {
return array( 'accountID' );
}
/**
* Gets the default value.
*
* @since 1.2.0
* @since 1.102.0 Added settings for the Ad Blocking Recovery feature.
*
* @return array
*/
protected function get_default() {
return array(
'ownerID' => 0,
'accountID' => '',
'autoAdsDisabled' => array(),
'clientID' => '',
'accountStatus' => '',
'siteStatus' => '',
'accountSetupComplete' => false,
'siteSetupComplete' => false,
'useSnippet' => true,
'webStoriesAdUnit' => '',
'setupCompletedTimestamp' => null,
'useAdBlockingRecoverySnippet' => false,
'useAdBlockingRecoveryErrorSnippet' => false,
'adBlockingRecoverySetupStatus' => '',
);
}
/**
* 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'];
}
if ( isset( $option['useAdBlockingRecoverySnippet'] ) ) {
$option['useAdBlockingRecoverySnippet'] = (bool) $option['useAdBlockingRecoverySnippet'];
}
if ( isset( $option['useAdBlockingRecoveryErrorSnippet'] ) ) {
$option['useAdBlockingRecoveryErrorSnippet'] = (bool) $option['useAdBlockingRecoveryErrorSnippet'];
}
if (
isset( $option['adBlockingRecoverySetupStatus'] ) &&
! in_array(
$option['adBlockingRecoverySetupStatus'],
array(
'',
self::AD_BLOCKING_RECOVERY_SETUP_STATUS_TAG_PLACED,
self::AD_BLOCKING_RECOVERY_SETUP_STATUS_SETUP_CONFIRMED,
),
true
)
) {
$option['adBlockingRecoverySetupStatus'] = $this->get()['adBlockingRecoverySetupStatus'];
}
}
return $option;
};
}
}

View File

@@ -0,0 +1,43 @@
<?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.
* @since 1.105.0 Extract the check for 404 pages to dedicated Guard.
*
* @return bool|WP_Error TRUE if guarded tag can be activated, otherwise FALSE or an error.
*/
public function can_activate() {
$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,44 @@
<?php
/**
* Class Google\Site_Kit\Core\Modules\AdSense\Tag_Matchers
*
* @package Google\Site_Kit\Core\Modules\AdSense
* @copyright 2024 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Core\Modules\AdSense;
use Google\Site_Kit\Core\Modules\Tags\Module_Tag_Matchers;
use Google\Site_Kit\Core\Tags\Tag_Matchers_Interface;
/**
* Class for Tag matchers.
*
* @since 1.119.0
* @access private
* @ignore
*/
class Tag_Matchers extends Module_Tag_Matchers implements Tag_Matchers_Interface {
/**
* Holds array of regex tag matchers.
*
* @since 1.119.0
*
* @return array Array of regex matchers.
*/
public function regex_matchers() {
return array(
// Detect google_ad_client.
"/google_ad_client: ?[\"|'](.*?)[\"|']/",
// Detect old style auto-ads tags.
'/<(?:script|amp-auto-ads) [^>]*data-ad-client="([^"]+)"/',
// Detect new style auto-ads tags.
'/<(?:script|amp-auto-ads)[^>]*src="[^"]*\\?client=(ca-pub-[^"]+)"[^>]*>/',
);
}
}

View File

@@ -0,0 +1,99 @@
<?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;
use 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&host=%s',
esc_attr( $this->tag_id ), // Site owner's web property code.
'ca-host-pub-2644536267352236' // SiteKit's web property code.
);
$adsense_script_attributes = array(
'async' => true,
'src' => $adsense_script_src,
'crossorigin' => 'anonymous',
);
$adsense_attributes = $this->get_tag_blocked_on_consent_attribute_array();
$auto_ads_opt = array();
$auto_ads_opt_filtered = apply_filters( 'googlesitekit_auto_ads_opt', $auto_ads_opt, $this->tag_id );
if ( is_array( $auto_ads_opt_filtered ) && ! empty( $auto_ads_opt_filtered ) ) {
$strip_attributes = array(
'google_ad_client' => '',
'enable_page_level_ads' => '',
);
$auto_ads_opt_filtered = array_diff_key( $auto_ads_opt_filtered, $strip_attributes );
$auto_ads_opt_sanitized = array();
foreach ( $auto_ads_opt_filtered as $key => $value ) {
$new_key = 'data-';
$new_key .= str_replace( '_', '-', $key );
$auto_ads_opt_sanitized[ $new_key ] = $value;
}
$adsense_attributes = array_merge( $adsense_attributes, $auto_ads_opt_sanitized );
}
printf( "\n<!-- %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,469 @@
<?php
/**
* Class Google\Site_Kit\Modules\Ads
*
* @package Google\Site_Kit
* @copyright 2024 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\Context;
use Google\Site_Kit\Core\Assets\Asset;
use Google\Site_Kit\Core\Assets\Assets;
use Google\Site_Kit\Core\Assets\Script;
use Google\Site_Kit\Core\Assets\Script_Data;
use Google\Site_Kit\Core\Authentication\Authentication;
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_Debug_Fields;
use Google\Site_Kit\Core\Modules\Module_With_Deactivation;
use Google\Site_Kit\Core\Modules\Module_With_Persistent_Registration;
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_Tag;
use Google\Site_Kit\Core\Modules\Module_With_Tag_Trait;
use Google\Site_Kit\Core\Modules\Tags\Module_Tag_Matchers;
use Google\Site_Kit\Core\Permissions\Permissions;
use Google\Site_Kit\Core\Site_Health\Debug_Data;
use Google\Site_Kit\Core\Storage\Options;
use Google\Site_Kit\Core\Storage\User_Options;
use Google\Site_Kit\Core\Util\Plugin_Status;
use Google\Site_Kit\Modules\Ads\PAX_Config;
use Google\Site_Kit\Modules\Ads\Settings;
use Google\Site_Kit\Modules\Ads\Has_Tag_Guard;
use Google\Site_Kit\Modules\Ads\Tag_Matchers;
use Google\Site_Kit\Modules\Ads\Web_Tag;
use Google\Site_Kit\Core\Tags\Guards\Tag_Environment_Type_Guard;
use Google\Site_Kit\Core\Tags\Guards\Tag_Verify_Guard;
use Google\Site_Kit\Core\Util\Feature_Flags;
use Google\Site_Kit\Core\Util\Method_Proxy_Trait;
use Google\Site_Kit\Core\Util\URL;
use Google\Site_Kit\Modules\Ads\AMP_Tag;
use Google\Site_Kit\Core\Conversion_Tracking\Conversion_Tracking;
use Google\Site_Kit\Core\Modules\Module_With_Inline_Data;
use Google\Site_Kit\Core\Modules\Module_With_Inline_Data_Trait;
use Google\Site_Kit\Core\Tracking\Feature_Metrics_Trait;
use Google\Site_Kit\Core\Tracking\Provides_Feature_Metrics;
/**
* Class representing the Ads module.
*
* @since 1.121.0
* @access private
* @ignore
*/
final class Ads extends Module implements Module_With_Inline_Data, Module_With_Assets, Module_With_Debug_Fields, Module_With_Scopes, Module_With_Settings, Module_With_Tag, Module_With_Deactivation, Module_With_Persistent_Registration, Provides_Feature_Metrics {
use Module_With_Assets_Trait;
use Module_With_Scopes_Trait;
use Module_With_Settings_Trait;
use Module_With_Tag_Trait;
use Method_Proxy_Trait;
use Module_With_Inline_Data_Trait;
use Feature_Metrics_Trait;
/**
* Module slug name.
*/
const MODULE_SLUG = 'ads';
const SCOPE = 'https://www.googleapis.com/auth/adwords';
const SUPPORT_CONTENT_SCOPE = 'https://www.googleapis.com/auth/supportcontent';
/**
* Conversion_Tracking instance.
*
* @since 1.147.0
* @var Conversion_Tracking
*/
protected $conversion_tracking;
/**
* Class constructor.
*
* @since 1.147.0
*
* @param Context $context Context object.
* @param Options|null $options Options object.
* @param User_Options|null $user_options User options object.
* @param Authentication|null $authentication Authentication object.
* @param Assets|null $assets Assets object.
*/
public function __construct( Context $context, ?Options $options = null, ?User_Options $user_options = null, ?Authentication $authentication = null, ?Assets $assets = null ) {
parent::__construct( $context, $options, $user_options, $authentication, $assets );
$this->conversion_tracking = new Conversion_Tracking( $context );
}
/**
* Registers functionality through WordPress hooks.
*
* @since 1.121.0
*/
public function register() {
$this->register_scopes_hook();
$this->register_inline_data();
$this->register_feature_metrics();
// Ads tag placement logic.
add_action( 'template_redirect', array( $this, 'register_tag' ) );
add_filter(
'googlesitekit_ads_measurement_connection_checks',
function ( $checks ) {
$checks[] = array( $this, 'check_ads_measurement_connection' );
return $checks;
},
10
);
}
/**
* Registers functionality independent of module activation.
*
* @since 1.148.0
*/
public function register_persistent() {
add_filter( 'googlesitekit_inline_modules_data', fn ( $data ) => $this->persistent_inline_modules_data( $data ) );
}
/**
* Checks if the Ads module is connected and contributing to Ads measurement.
*
* @since 1.151.0
*
* @return bool True if the Ads module is connected, false otherwise.
*/
public function check_ads_measurement_connection() {
return $this->is_connected();
}
/**
* Sets up the module's assets to register.
*
* @since 1.122.0
* @since 1.126.0 Added PAX assets.
*
* @return Asset[] List of Asset objects.
*/
protected function setup_assets() {
$base_url = $this->context->url( 'dist/assets/' );
$assets = array(
new Script(
'googlesitekit-modules-ads',
array(
'src' => $base_url . 'js/googlesitekit-modules-ads.js',
'dependencies' => array(
'googlesitekit-vendor',
'googlesitekit-api',
'googlesitekit-data',
'googlesitekit-modules',
'googlesitekit-notifications',
'googlesitekit-datastore-site',
'googlesitekit-datastore-user',
'googlesitekit-components',
),
)
),
);
if ( Feature_Flags::enabled( 'adsPax' ) ) {
$input = $this->context->input();
$is_googlesitekit_dashboard = 'googlesitekit-dashboard' === $input->filter( INPUT_GET, 'page' );
$is_ads_slug = 'ads' === $input->filter( INPUT_GET, 'slug' );
$is_re_auth = $input->filter( INPUT_GET, 'reAuth' );
$assets[] = new Script_Data(
'googlesitekit-ads-pax-config',
array(
'global' => '_googlesitekitPAXConfig',
'data_callback' => function () {
if ( ! current_user_can( Permissions::VIEW_AUTHENTICATED_DASHBOARD ) ) {
return array();
}
$config = new PAX_Config( $this->context, $this->authentication->token() );
return $config->get();
},
)
);
// Integrator should be included if either Ads module is connected already,
// or we are on the Ads module setup screen.
if (
current_user_can( Permissions::VIEW_AUTHENTICATED_DASHBOARD ) &&
(
// Integrator should be included if either:
// The Ads module is already connected.
$this->is_connected() ||
// Or the user is on the Ads module setup screen.
( ( ( is_admin() && $is_googlesitekit_dashboard ) && $is_ads_slug ) && $is_re_auth )
)
) {
$assets[] = new Script(
'googlesitekit-ads-pax-integrator',
array(
// When updating, mirror the fixed version for google-pax-sdk in package.json.
'src' => 'https://www.gstatic.com/pax/1.1.10/pax_integrator.js',
'execution' => 'async',
'dependencies' => array(
'googlesitekit-ads-pax-config',
'googlesitekit-modules-data',
),
'version' => null,
)
);
}
}
return $assets;
}
/**
* Populates module data needed independent of Ads module activation.
*
* @since 1.148.0
*
* @param array $modules_data Inline modules data.
* @return array Inline modules data.
*/
protected function persistent_inline_modules_data( $modules_data ) {
if ( ! Feature_Flags::enabled( 'adsPax' ) ) {
return $modules_data;
}
if ( empty( $modules_data['ads'] ) ) {
$modules_data['ads'] = array();
}
$active_wc = class_exists( 'WooCommerce' );
$active_gla = defined( 'WC_GLA_VERSION' );
$gla_ads_conversion_action = get_option( 'gla_ads_conversion_action' );
$modules_data['ads']['plugins'] = array(
'woocommerce' => array(
'active' => $active_wc,
'installed' => $active_wc || Plugin_Status::is_plugin_installed( 'woocommerce/woocommerce.php' ),
),
'google-listings-and-ads' => array(
'active' => $active_gla,
'installed' => $active_gla || Plugin_Status::is_plugin_installed( 'google-listings-and-ads/google-listings-and-ads.php' ),
'adsConnected' => $active_gla && get_option( 'gla_ads_id' ),
'conversionID' => is_array( $gla_ads_conversion_action ) ? $gla_ads_conversion_action['conversion_id'] : '',
),
);
return $modules_data;
}
/**
* Gets required Google OAuth scopes for the module.
*
* @since 1.126.0
*
* @return array List of Google OAuth scopes.
*/
public function get_scopes() {
if ( Feature_Flags::enabled( 'adsPax' ) ) {
$granted_scopes = $this->authentication->get_oauth_client()->get_granted_scopes();
$options = $this->get_settings()->get();
if ( in_array( self::SCOPE, $granted_scopes, true ) || ! empty( $options['extCustomerID'] ) ) {
return array( self::SCOPE, self::SUPPORT_CONTENT_SCOPE );
}
}
return array();
}
/**
* Sets up information about the module.
*
* @since 1.121.0
*
* @return array Associative array of module info.
*/
protected function setup_info() {
return array(
'slug' => 'ads',
'name' => _x( 'Ads', 'Service name', 'google-site-kit' ),
'description' => Feature_Flags::enabled( 'adsPax' ) ? __( 'Grow sales, leads or awareness for your business by advertising with Google Ads', 'google-site-kit' ) : __( 'Track conversions for your existing Google Ads campaigns', 'google-site-kit' ),
'homepage' => __( 'https://google.com/ads', 'google-site-kit' ),
);
}
/**
* Sets up the module's settings instance.
*
* @since 1.122.0
*
* @return Module_Settings
*/
protected function setup_settings() {
return new Settings( $this->options );
}
/**
* Checks whether the module is connected.
*
* A module being connected means that all steps required as part of its activation are completed.
*
* @since 1.122.0
* @since 1.127.0 Add additional check to account for paxConversionID and extCustomerID as well when feature flag is enabled.
*
* @return bool True if module is connected, false otherwise.
*/
public function is_connected() {
$options = $this->get_settings()->get();
if ( Feature_Flags::enabled( 'adsPax' ) ) {
if ( empty( $options['conversionID'] ) && empty( $options['paxConversionID'] ) && empty( $options['extCustomerID'] ) ) {
return false;
}
return parent::is_connected();
}
if ( empty( $options['conversionID'] ) ) {
return false;
}
return parent::is_connected();
}
/**
* Cleans up when the module is deactivated.
*
* @since 1.122.0
*/
public function on_deactivation() {
$this->get_settings()->delete();
}
/**
* Registers the Ads tag.
*
* @since 1.124.0
*/
public function register_tag() {
$ads_conversion_id = $this->get_settings()->get()['conversionID'];
$pax_conversion_id = $this->get_settings()->get()['paxConversionID'];
// The PAX-supplied Conversion ID should take precedence over the
// user-supplied one, if both exist.
if ( Feature_Flags::enabled( 'adsPax' ) && ! empty( $pax_conversion_id ) ) {
$ads_conversion_id = $pax_conversion_id;
}
$tag = $this->context->is_amp()
? new AMP_Tag( $ads_conversion_id, self::MODULE_SLUG )
: new Web_Tag( $ads_conversion_id, self::MODULE_SLUG );
if ( $tag->is_tag_blocked() ) {
return;
}
$tag->use_guard( new Tag_Verify_Guard( $this->context->input() ) );
$tag->use_guard( new Has_Tag_Guard( $ads_conversion_id ) );
$tag->use_guard( new Tag_Environment_Type_Guard() );
if ( ! $tag->can_register() ) {
return;
}
$home_domain = URL::parse( $this->context->get_canonical_home_url(), PHP_URL_HOST );
$tag->set_home_domain( $home_domain );
$tag->register();
}
/**
* Gets an array of debug field definitions.
*
* @since 1.124.0
*
* @return array An array of all debug fields.
*/
public function get_debug_fields() {
$settings = $this->get_settings()->get();
return array(
'ads_conversion_tracking_id' => array(
'label' => __( 'Ads: Conversion ID', 'google-site-kit' ),
'value' => $settings['conversionID'],
'debug' => Debug_Data::redact_debug_value( $settings['conversionID'] ),
),
);
}
/**
* Returns the Module_Tag_Matchers instance.
*
* @since 1.124.0
*
* @return Module_Tag_Matchers Module_Tag_Matchers instance.
*/
public function get_tag_matchers() {
return new Tag_Matchers();
}
/**
* Gets required inline data for the module.
*
* @since 1.158.0
* @since 1.160.0 Include $modules_data parameter to match the interface.
*
* @param array $modules_data Inline modules data.
* @return array An array of the module's inline data.
*/
public function get_inline_data( $modules_data ) {
if ( ! Feature_Flags::enabled( 'adsPax' ) ) {
return $modules_data;
}
if ( empty( $modules_data['ads'] ) ) {
$modules_data['ads'] = array();
}
$modules_data[ self::MODULE_SLUG ]['supportedConversionEvents'] = $this->conversion_tracking->get_supported_conversion_events();
return $modules_data;
}
/**
* Gets an array of internal feature metrics.
*
* @since 1.162.0
*
* @return array
*/
public function get_feature_metrics() {
$is_connected = $this->is_connected();
if ( ! $is_connected ) {
return array(
'ads_connection' => '',
);
}
$settings = $this->get_settings()->get();
if ( Feature_Flags::enabled( 'adsPax' ) && ! empty( $settings['paxConversionID'] ) ) {
return array(
'ads_connection' => 'pax',
);
}
return array(
'ads_connection' => 'manual',
);
}
}

View File

@@ -0,0 +1,110 @@
<?php
/**
* Class Google\Site_Kit\Modules\Ads\AMP_Tag
*
* @package Google\Site_Kit\Modules\Ads
* @copyright 2024 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\Ads;
use Google\Site_Kit\Core\Modules\Tags\Module_AMP_Tag;
use Google\Site_Kit\Core\Tags\Tag_With_Linker_Interface;
use Google\Site_Kit\Core\Util\Method_Proxy_Trait;
use Google\Site_Kit\Core\Tags\Tag_With_Linker_Trait;
/**
* Class for AMP tag.
*
* @since 1.125.0
* @access private
* @ignore
*/
class AMP_Tag extends Module_AMP_Tag implements Tag_With_Linker_Interface {
use Method_Proxy_Trait;
use Tag_With_Linker_Trait;
/**
* Sets the current home domain.
*
* @since 1.125.0
*
* @param string $domain Domain name.
*/
public function set_home_domain( $domain ) {
$this->home_domain = $domain;
}
/**
* Registers tag hooks.
*
* @since 1.125.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.125.0
*/
protected function render() {
$config = $this->get_tag_config();
$gtag_amp_opt = array(
'optoutElementId' => '__gaOptOutExtension',
'vars' => array(
'gtag_id' => $this->tag_id,
'config' => $config,
),
);
printf( "\n<!-- %s -->\n", esc_html__( 'Google Ads 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 )
);
printf( "\n<!-- %s -->\n", esc_html__( 'End Google Ads AMP snippet added by Site Kit', 'google-site-kit' ) );
}
/**
* Gets the tag config as used in the gtag data vars.
*
* @since 1.125.0
*
* @return array Tag configuration.
*/
protected function get_tag_config() {
$config = array(
$this->tag_id => array(
'groups' => 'default',
),
);
return $this->add_linker_to_tag_config( $config );
}
}

View File

@@ -0,0 +1,55 @@
<?php
/**
* Class Google\Site_Kit\Modules\Ads\Has_Tag_Guard
*
* @package Google\Site_Kit\Modules\Ads
* @copyright 2024 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\Ads;
use Google\Site_Kit\Core\Modules\Tags\Module_Tag_Guard;
/**
* Class for the Ads tag guard.
*
* @since 1.124.0
* @since 1.128.0 Renamed class to be specific to presence of web tag.
* @access private
* @ignore
*/
class Has_Tag_Guard extends Module_Tag_Guard {
/**
* Modules tag_id value.
*
* @since 1.128.0
*
* @var String
*/
protected $tag_id;
/**
* Class constructor.
*
* @since 1.128.0
*
* @param string $tag_id Modules web tag string value.
*/
public function __construct( $tag_id = '' ) {
$this->tag_id = $tag_id;
}
/**
* Determines whether the guarded tag can be activated or not.
*
* @since 1.124.0
* @since 1.128.0 Updated logic to check modules tag_id value..
*
* @return bool TRUE if guarded tag can be activated, otherwise FALSE or an error.
*/
public function can_activate() {
return ! empty( $this->tag_id );
}
}

View File

@@ -0,0 +1,91 @@
<?php
/**
* Class Google\Site_Kit\Modules\Ads\PAX_Config
*
* @package Google\Site_Kit
* @copyright 2024 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\Ads;
use Google\Site_Kit\Context;
use Google\Site_Kit\Core\Authentication\Token;
/**
* Class representing PAX configuration.
*
* @since 1.128.0
* @access private
* @ignore
*/
class PAX_Config {
/**
* Context instance.
*
* @since 1.128.0
* @var Context
*/
private $context;
/**
* Token instance.
*
* @since 1.128.0
* @var Token
*/
private $token;
/**
* Constructor.
*
* @since 1.128.0
*
* @param Context $context Context instance.
* @param Token $token Token instance.
*/
public function __construct( Context $context, Token $token ) {
$this->context = $context;
$this->token = $token;
}
/**
* Gets the configuration data.
*
* @since 1.128.0
* @return array
*/
public function get() {
$token = $this->token->get();
return array(
'authAccess' => array(
'oauthTokenAccess' => array(
'token' => $token['access_token'] ?? '',
),
),
'locale' => substr( $this->context->get_locale( 'user' ), 0, 2 ),
'debuggingConfig' => array(
'env' => $this->get_env(),
),
);
}
/**
* Gets the environment configuration.
*
* @since 1.128.0
* @return string
*/
protected function get_env() {
$allowed = array( 'PROD', 'QA_PROD' );
if ( defined( 'GOOGLESITEKIT_PAX_ENV' ) && in_array( GOOGLESITEKIT_PAX_ENV, $allowed, true ) ) {
return GOOGLESITEKIT_PAX_ENV;
}
return 'PROD';
}
}

View File

@@ -0,0 +1,79 @@
<?php
/**
* Class Google\Site_Kit\Modules\Ads\Settings
*
* @package Google\Site_Kit\Modules\Ads
* @copyright 2024 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\Ads;
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 Ads settings.
*
* @since 1.122.0
* @access private
* @ignore
*/
class Settings extends Module_Settings implements Setting_With_Owned_Keys_Interface {
use Setting_With_Owned_Keys_Trait;
const OPTION = 'googlesitekit_ads_settings';
/**
* Registers the setting in WordPress.
*
* @since 1.122.0
*/
public function register() {
parent::register();
$this->register_owned_keys();
}
/**
* Gets the default value.
*
* @since 1.122.0
* @since 1.126.0 Added new settings fields for PAX.
* @since 1.149.0 Added new settings fields for PAX.
*
* @return array An array of default settings values.
*/
protected function get_default() {
return array(
'conversionID' => '',
'paxConversionID' => '',
'customerID' => '',
'extCustomerID' => '',
'formattedExtCustomerID' => '',
'userID' => '',
'accountOverviewURL' => '',
);
}
/**
* Returns keys for owned settings.
*
* @since 1.122.0
* @since 1.126.0 Added new settings fields for PAX.
* @since 1.149.0 Added customerID & userID settings fields for PAX.
*
* @return array An array of keys for owned settings.
*/
public function get_owned_keys() {
return array(
'conversionID',
'paxConversionID',
'extCustomerID',
'customerID',
'userID',
);
}
}

View File

@@ -0,0 +1,37 @@
<?php
/**
* Class Google\Site_Kit\Modules\Ads\Tag_Matchers
*
* @package Google\Site_Kit\Core\Modules\Ads
* @copyright 2024 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\Ads;
use Google\Site_Kit\Core\Modules\Tags\Module_Tag_Matchers;
use Google\Site_Kit\Core\Tags\Tag_Matchers_Interface;
/**
* Class for Tag matchers.
*
* @since 1.124.0
* @access private
* @ignore
*/
class Tag_Matchers extends Module_Tag_Matchers implements Tag_Matchers_Interface {
/**
* Holds array of regex tag matchers.
*
* @since 1.124.0
*
* @return array Array of regex matchers.
*/
public function regex_matchers() {
return array(
"/gtag\\s*\\(\\s*['|\"]config['|\"]\\s*,\\s*['|\"](AW-[0-9]+)['|\"]\\s*\\)/i",
);
}
}

View File

@@ -0,0 +1,87 @@
<?php
/**
* Class Google\Site_Kit\Modules\Ads\Web_Tag
*
* @package Google\Site_Kit\Modules\Ads
* @copyright 2024 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\Ads;
use Google\Site_Kit\Core\Modules\Tags\Module_Web_Tag;
use Google\Site_Kit\Core\Tags\GTag;
use Google\Site_Kit\Core\Tags\Tag_With_Linker_Interface;
use Google\Site_Kit\Core\Tags\Tag_With_Linker_Trait;
use Google\Site_Kit\Core\Util\Method_Proxy_Trait;
/**
* Class for Web tag.
*
* @since 1.124.0
* @access private
* @ignore
*/
class Web_Tag extends Module_Web_Tag implements Tag_With_Linker_Interface {
use Method_Proxy_Trait;
use Tag_With_Linker_Trait;
/**
* Registers tag hooks.
*
* @since 1.124.0
*/
public function register() {
// Set a lower priority here to let Analytics sets up its tag first.
add_action(
'googlesitekit_setup_gtag',
$this->get_method_proxy( 'setup_gtag' ),
20
);
add_filter( 'script_loader_tag', $this->get_method_proxy( 'filter_tag_output' ), 10, 2 );
$this->do_init_tag_action();
}
/**
* Outputs gtag snippet.
*
* @since 1.124.0
*/
protected function render() {
// Do nothing, gtag script is enqueued.
}
/**
* Configures gtag script.
*
* @since 1.124.0
*
* @param GTag $gtag GTag instance.
*/
protected function setup_gtag( $gtag ) {
$gtag->add_tag( $this->tag_id );
}
/**
* Filters output of tag HTML.
*
* @param string $tag Tag HTML.
* @param string $handle WP script handle of given tag.
* @return string
*/
protected function filter_tag_output( $tag, $handle ) {
// The tag will either have its own handle or use the common GTag handle, not both.
if ( GTag::get_handle_for_tag( $this->tag_id ) !== $handle && GTag::HANDLE !== $handle ) {
return $tag;
}
// Retain this comment for detection of Site Kit placed tag.
$snippet_comment = sprintf( "<!-- %s -->\n", esc_html__( 'Google Ads snippet added by Site Kit', 'google-site-kit' ) );
return $snippet_comment . $tag;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,178 @@
<?php
/**
* Class Google\Site_Kit\Modules\Analytics_4\AMP_Tag
*
* @package Google\Site_Kit\Modules\Analytics_4
* @copyright 2023 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_AMP_Tag;
use Google\Site_Kit\Core\Tags\Tag_With_Linker_Interface;
use Google\Site_Kit\Core\Util\Method_Proxy_Trait;
use Google\Site_Kit\Core\Tags\Tag_With_Linker_Trait;
/**
* Class for AMP tag.
*
* @since 1.104.0
* @access private
* @ignore
*/
class AMP_Tag extends Module_AMP_Tag implements Tag_Interface, Tag_With_Linker_Interface {
use Method_Proxy_Trait;
use Tag_With_Linker_Trait;
/**
* Custom dimensions data.
*
* @since 1.113.0
* @var array
*/
private $custom_dimensions;
/**
* Sets the current home domain.
*
* @since 1.118.0
*
* @param string $domain Domain name.
*/
public function set_home_domain( $domain ) {
$this->home_domain = $domain;
}
/**
* Sets custom dimensions data.
*
* @since 1.113.0
*
* @param string $custom_dimensions Custom dimensions data.
*/
public function set_custom_dimensions( $custom_dimensions ) {
$this->custom_dimensions = $custom_dimensions;
}
/**
* Registers tag hooks.
*
* @since 1.104.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.104.0
*/
protected function render() {
$config = $this->get_tag_config();
$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'];
}
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' ) );
}
/**
* Extends gtag vars config with the GA4 tag config.
*
* @since 1.104.0
*
* @param array $opt AMP gtag config.
* @return array
*/
protected function extend_gtag_opt( $opt ) {
$opt['vars']['config'] = array_merge(
$opt['vars']['config'],
$this->get_tag_config()
);
// `gtag_id` isn't used in a multi-destination configuration.
// See https://developers.google.com/analytics/devguides/collection/amp-analytics/#sending_data_to_multiple_destinations.
unset( $opt['vars']['gtag_id'] );
return $opt;
}
/**
* Gets the tag config as used in the gtag data vars.
*
* @since 1.113.0
*
* @return array Tag configuration.
*/
protected function get_tag_config() {
$config = array(
$this->tag_id => array(
'groups' => 'default',
),
);
if ( ! empty( $this->custom_dimensions ) ) {
$config[ $this->tag_id ] = array_merge(
$config[ $this->tag_id ],
$this->custom_dimensions
);
}
return $this->add_linker_to_tag_config( $config );
}
}

View File

@@ -0,0 +1,200 @@
<?php
/**
* Class Google\Site_Kit\Modules\Analytics_4\Account_Ticket
*
* @package Google\Site_Kit
* @copyright 2023 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;
/**
* Class representing an account ticket for Analytics 4 account provisioning with associated parameters.
*
* @since 1.98.0
* @access private
* @ignore
*/
class Account_Ticket {
/**
* Account ticket ID.
*
* @since 1.98.0
* @var string
*/
protected $id;
/**
* Property name.
*
* @since 1.98.0
* @var string
*/
protected $property_name;
/**
* Data stream name.
*
* @since 1.98.0
* @var string
*/
protected $data_stream_name;
/**
* Timezone.
*
* @since 1.98.0
* @var string
*/
protected $timezone;
/**
* Whether or not enhanced measurement should be enabled.
*
* @since 1.111.0
* @var boolean
*/
protected $enhanced_measurement_stream_enabled;
/**
* Constructor.
*
* @since 1.98.0
*
* @param array $data Data to hydrate properties with.
*/
public function __construct( $data = null ) {
if ( ! is_array( $data ) ) {
return;
}
foreach ( $data as $key => $value ) {
if ( property_exists( $this, $key ) ) {
$this->{"set_$key"}( $value );
}
}
}
/**
* Gets the account ticket ID.
*
* @since 1.98.0
*
* @return string
*/
public function get_id() {
return $this->id;
}
/**
* Sets the account ticket ID.
*
* @since 1.98.0
*
* @param string $id Account ticket ID.
*/
public function set_id( $id ) {
$this->id = (string) $id;
}
/**
* Gets the property name.
*
* @since 1.98.0
*
* @return string
*/
public function get_property_name() {
return $this->property_name;
}
/**
* Sets the property name.
*
* @since 1.98.0
*
* @param string $property_name Property name.
*/
public function set_property_name( $property_name ) {
$this->property_name = (string) $property_name;
}
/**
* Gets the data stream name.
*
* @since 1.98.0
*
* @return string
*/
public function get_data_stream_name() {
return $this->data_stream_name;
}
/**
* Sets the data stream name.
*
* @since 1.98.0
*
* @param string $data_stream_name Data stream name.
*/
public function set_data_stream_name( $data_stream_name ) {
$this->data_stream_name = (string) $data_stream_name;
}
/**
* Gets the timezone.
*
* @since 1.98.0
*
* @return string
*/
public function get_timezone() {
return $this->timezone;
}
/**
* Sets the timezone.
*
* @since 1.98.0
*
* @param string $timezone Timezone.
*/
public function set_timezone( $timezone ) {
$this->timezone = (string) $timezone;
}
/**
* Gets the enabled state of enhanced measurement for the data stream.
*
* @since 1.111.0
*
* @return bool $enabled Enabled state.
*/
public function get_enhanced_measurement_stream_enabled() {
return $this->enhanced_measurement_stream_enabled;
}
/**
* Sets the enabled state of enhanced measurement for the data stream.
*
* @since 1.111.0
*
* @param bool $enabled Enabled state.
*/
public function set_enhanced_measurement_stream_enabled( $enabled ) {
$this->enhanced_measurement_stream_enabled = (bool) $enabled;
}
/**
* Gets the array representation of the instance values.
*
* @since 1.98.0
*
* @return array
*/
public function to_array() {
return get_object_vars( $this );
}
}

View File

@@ -0,0 +1,171 @@
<?php
/**
* Class Google\Site_Kit\Modules\Analytics_4\Advanced_Tracking
*
* @package Google\Site_Kit\Modules\Analytics_4
* @copyright 2024 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\Context;
use Google\Site_Kit\Modules\Analytics_4\Advanced_Tracking\Event_List;
use Google\Site_Kit\Modules\Analytics_4\Advanced_Tracking\Script_Injector;
use Google\Site_Kit\Modules\Analytics_4\Advanced_Tracking\AMP_Config_Injector;
use Google\Site_Kit\Modules\Analytics_4\Advanced_Tracking\Event_List_Registry;
use Google\Site_Kit\Modules\Analytics_4;
/**
* Class for Google Analytics Advanced Event Tracking.
*
* @since 1.18.0.
* @since 1.121.0 Migrated from the Analytics (UA) namespace.
* @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.
* @since 1.118.0 Renamed hooks to target Analytics 4 module.
*/
public function register() {
$slug_name = Analytics_4::MODULE_SLUG;
add_action(
"googlesitekit_{$slug_name}_init_tag",
function () {
$this->register_event_lists();
add_action(
'wp_footer',
function () {
$this->set_up_advanced_tracking();
}
);
}
);
add_action(
"googlesitekit_{$slug_name}_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,65 @@
<?php
/**
* Class Google\Site_Kit\Modules\Analytics_4\Advanced_Tracking\AMP_Config_Injector
*
* @package Google\Site_Kit\Modules\Analytics_4
* @copyright 2024 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\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.
* @since 1.121.0 Migrated from the Analytics (UA) namespace.
*
* @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,111 @@
<?php
/**
* Class Google\Site_Kit\Modules\Analytics_4\Advanced_Tracking\Event
*
* @package Google\Site_Kit
* @copyright 2024 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\Advanced_Tracking;
use Exception;
/**
* Class for representing a single tracking event that Advanced_Tracking tracks.
*
* @since 1.18.0.
* @since 1.121.0 Migrated from the Analytics (UA) namespace.
* @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.
*/
#[\ReturnTypeWillChange]
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,63 @@
<?php
/**
* Class Google\Site_Kit\Modules\Analytics_4\Advanced_Tracking\Event_List
*
* @package Google\Site_Kit
* @copyright 2024 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\Advanced_Tracking;
/**
* Base class representing a tracking event list.
*
* @since 1.18.0.
* @since 1.121.0 Migrated from the Analytics (UA) namespace.
* @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,52 @@
<?php
/**
* Class Google\Site_Kit\Modules\Analytics_4\Advanced_Tracking\Event_List_Registry
*
* @package Google\Site_Kit\Modules\Analytics_4
* @copyright 2024 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\Advanced_Tracking;
/**
* Class for registering third party event lists.
*
* @since 1.18.0.
* @since 1.121.0 Migrated from the Analytics (UA) namespace.
* @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,79 @@
<?php
/**
* Class Google\Site_Kit\Modules\Analytics_4\Advanced_Tracking\Script_Injector
*
* @package Google\Site_Kit\Modules\Analytics_4
* @copyright 2024 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\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.
* @since 1.121.0 Migrated from the Analytics (UA) namespace.
* @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,140 @@
<?php
/**
* Class Google\Site_Kit\Modules\Analytics_4\Audience_Settings
*
* @package Google\Site_Kit\Modules\Analytics_4
* @copyright 2025 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\Storage\Setting;
use Google\Site_Kit\Core\Storage\Setting_With_ViewOnly_Keys_Interface;
/**
* Class for Audience_Settings.
*
* @since 1.148.0
* @access private
* @ignore
*/
class Audience_Settings extends Setting implements Setting_With_ViewOnly_Keys_Interface {
/**
* The option name for this setting.
*/
const OPTION = 'googlesitekit_analytics-4_audience_settings';
/**
* Gets the default value for settings.
*
* @since 1.148.0
*
* @return mixed The default value.
*/
public function get_default() {
return array(
'availableAudiences' => null,
'availableAudiencesLastSyncedAt' => 0,
'audienceSegmentationSetupCompletedBy' => null,
);
}
/**
* Gets the type of the setting.
*
* @since 1.148.0
*
* @return string The type of the setting.
*/
public function get_type() {
return 'array';
}
/**
* Gets the callback for sanitizing the setting's value before saving.
*
* @since 1.148.0
*
* @return callable|null
*/
protected function get_sanitize_callback() {
return function ( $option ) {
return $this->sanitize( $option );
};
}
/**
* Gets the view-only keys for the setting.
*
* @since 1.148.0
*
* @return array List of view-only keys.
*/
public function get_view_only_keys() {
return array(
'availableAudiences',
'audienceSegmentationSetupCompletedBy',
);
}
/**
* Merges the given settings with the existing ones. It will keep the old settings
* value for the properties that are not present in the given settings.
*
* @since 1.148.0
*
* @param array $settings The settings to merge.
*
* @return array The merged settings.
*/
public function merge( $settings ) {
$existing_settings = $this->get();
$updated_settings = array_merge( $existing_settings, $settings );
$this->set( $updated_settings );
return $updated_settings;
}
/**
* Sanitizes the settings.
*
* @since 1.148.0
*
* @param array $option The option to sanitize.
*
* @return array The sanitized settings.
*/
private function sanitize( $option ) {
$new_option = $this->get();
if ( isset( $option['availableAudiences'] ) ) {
if ( is_array( $option['availableAudiences'] ) ) {
$new_option['availableAudiences'] = $option['availableAudiences'];
} else {
$new_option['availableAudiences'] = null;
}
}
if ( isset( $option['availableAudiencesLastSyncedAt'] ) ) {
if ( is_int( $option['availableAudiencesLastSyncedAt'] ) ) {
$new_option['availableAudiencesLastSyncedAt'] = $option['availableAudiencesLastSyncedAt'];
} else {
$new_option['availableAudiencesLastSyncedAt'] = 0;
}
}
if ( isset( $option['audienceSegmentationSetupCompletedBy'] ) ) {
if ( is_int( $option['audienceSegmentationSetupCompletedBy'] ) ) {
$new_option['audienceSegmentationSetupCompletedBy'] = $option['audienceSegmentationSetupCompletedBy'];
} else {
$new_option['audienceSegmentationSetupCompletedBy'] = null;
}
}
return $new_option;
}
}

View File

@@ -0,0 +1,61 @@
<?php
/**
* Class Google\Site_Kit\Modules\Analytics_4\Conversion_Reporting\Conversion_Reporting_Cron
*
* @package Google\Site_Kit
* @copyright 2024 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\Conversion_Reporting;
/**
* Class providing cron implementation for conversion reporting.
*
* @since 1.135.0
* @access private
* @ignore
*/
class Conversion_Reporting_Cron {
const CRON_ACTION = 'googlesitekit_cron_conversion_reporting_events';
/**
* Cron callback reference.
*
* @var callable
*/
private $cron_callback;
/**
* Constructor.
*
* @since 1.135.0
*
* @param callable $callback Function to call on the cron action.
*/
public function __construct( callable $callback ) {
$this->cron_callback = $callback;
}
/**
* Registers functionality through WordPress hooks.
*
* @since 1.133.0
*/
public function register() {
add_action( self::CRON_ACTION, $this->cron_callback );
}
/**
* Schedules cron if not already set.
*
* @since 1.135.0
*/
public function maybe_schedule_cron() {
if ( ! wp_next_scheduled( self::CRON_ACTION ) && ! wp_installing() ) {
wp_schedule_single_event( time() + DAY_IN_SECONDS, self::CRON_ACTION );
}
}
}

View File

@@ -0,0 +1,204 @@
<?php
/**
* Class Google\Site_Kit\Modules\Analytics_4\Conversion_Reporting\Conversion_Reporting_Events_Sync
*
* @package Google\Site_Kit
* @copyright 2024 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\Conversion_Reporting;
use Google\Site_Kit\Modules\Analytics_4;
use Google\Site_Kit\Modules\Analytics_4\Settings;
use Google\Site_Kit\Core\Storage\Transients;
/**
* Class providing report implementation for available events for conversion reporting.
*
* @since 1.135.0
* @access private
* @ignore
*/
class Conversion_Reporting_Events_Sync {
/**
* The detected events transient name.
*/
public const DETECTED_EVENTS_TRANSIENT = 'googlesitekit_conversion_reporting_detected_events';
/**
* The lost events transient name.
*/
public const LOST_EVENTS_TRANSIENT = 'googlesitekit_conversion_reporting_lost_events';
const EVENT_NAMES = array(
'add_to_cart',
'purchase',
'submit_lead_form',
'generate_lead',
'contact',
);
/**
* Settings instance.
*
* @var Settings
*/
private $settings;
/**
* Analytics_4 instance.
*
* @var Analytics_4
*/
private $analytics;
/**
* Conversion_Reporting_New_Badge_Events_Sync instance.
*
* @var Conversion_Reporting_New_Badge_Events_Sync
*/
private $new_badge_events_sync;
/**
* Transients instance.
*
* @since 1.139.0
* @var Transients
*/
protected $transients;
/**
* Constructor.
*
* @since 1.135.0
* @since 1.139.0 Added $context param to constructor.
* @since 1.144.0 Added $transients and $new_badge_events_sync params to constructor, and removed $context.
*
* @param Settings $settings Settings module settings instance.
* @param Transients $transients Transients instance.
* @param Analytics_4 $analytics Analytics 4 module instance.
* @param Conversion_Reporting_New_Badge_Events_Sync $new_badge_events_sync Conversion_Reporting_New_Badge_Events_Sync instance.
*/
public function __construct(
Settings $settings,
Transients $transients,
Analytics_4 $analytics,
Conversion_Reporting_New_Badge_Events_Sync $new_badge_events_sync
) {
$this->settings = $settings;
$this->transients = $transients;
$this->analytics = $analytics;
$this->new_badge_events_sync = $new_badge_events_sync;
}
/**
* Syncs detected events into settings.
*
* @since 1.135.0
*/
public function sync_detected_events() {
$report = $this->get_report();
$detected_events = array();
if ( is_wp_error( $report ) ) {
return;
}
// Get current stored detected events.
$settings = $this->settings->get();
$saved_detected_events = isset( $settings['detectedEvents'] ) ? $settings['detectedEvents'] : array();
// phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
if ( empty( $report->rowCount ) ) {
$this->settings->merge( array( 'detectedEvents' => array() ) );
$this->transients->delete( self::DETECTED_EVENTS_TRANSIENT );
if ( ! empty( $saved_detected_events ) ) {
$this->transients->set( self::LOST_EVENTS_TRANSIENT, $saved_detected_events );
}
return;
}
foreach ( $report->rows as $row ) {
$detected_events[] = $row['dimensionValues'][0]['value'];
}
$settings_partial = array( 'detectedEvents' => $detected_events );
$this->maybe_update_new_and_lost_events(
$detected_events,
$saved_detected_events,
$settings_partial
);
$this->settings->merge( $settings_partial );
}
/**
* Saves new and lost events transients.
*
* @since 1.144.0
*
* @param array $detected_events Currently detected events array.
* @param array $saved_detected_events Previously saved detected events array.
* @param array $settings_partial Analaytics settings partial.
*/
protected function maybe_update_new_and_lost_events( $detected_events, $saved_detected_events, &$settings_partial ) {
$new_events = array_diff( $detected_events, $saved_detected_events );
$lost_events = array_diff( $saved_detected_events, $detected_events );
if ( ! empty( $new_events ) ) {
$this->transients->set( self::DETECTED_EVENTS_TRANSIENT, array_values( $new_events ) );
$this->new_badge_events_sync->sync_new_badge_events( $new_events );
$settings_partial['newConversionEventsLastUpdateAt'] = time();
// Remove new events from lost events if present.
$saved_lost_events = $this->transients->get( self::LOST_EVENTS_TRANSIENT );
if ( $saved_lost_events ) {
$filtered_lost_events = array_diff( $saved_lost_events, $new_events );
$lost_events = array_merge( $lost_events, $filtered_lost_events );
}
}
if ( ! empty( $lost_events ) ) {
$this->transients->set( self::LOST_EVENTS_TRANSIENT, array_values( $lost_events ) );
$settings_partial['lostConversionEventsLastUpdateAt'] = time();
}
if ( empty( $saved_detected_events ) ) {
$this->transients->set( self::DETECTED_EVENTS_TRANSIENT, $detected_events );
}
}
/**
* Retrieves the GA4 report for filtered events.
*
* @since 1.135.0
*/
protected function get_report() {
$options = array(
// The 'metrics' parameter is required. 'eventCount' is used to ensure the request succeeds.
'metrics' => array( array( 'name' => 'eventCount' ) ),
'dimensions' => array(
array(
'name' => 'eventName',
),
),
'startDate' => gmdate( 'Y-m-d', strtotime( '-90 days' ) ),
'endDate' => gmdate( 'Y-m-d', strtotime( '-1 day' ) ),
'dimensionFilters' => array(
'eventName' => array(
'filterType' => 'inListFilter',
'value' => self::EVENT_NAMES,
),
),
'limit' => '20',
);
return $this->analytics->get_data( 'report', $options );
}
}

View File

@@ -0,0 +1,97 @@
<?php
/**
* Class Google\Site_Kit\Modules\Analytics_4\Conversion_Reporting\Conversion_Reporting_New_Badge_Events_Sync
*
* @package Google\Site_Kit
* @copyright 2024 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\Conversion_Reporting;
use Google\Site_Kit\Core\Storage\Transients;
/**
* Class providing implementation of "new" badge for detected conversion reporting events.
*
* @since 1.144.0
* @access private
* @ignore
*/
class Conversion_Reporting_New_Badge_Events_Sync {
/**
* The detected events transient name.
*/
public const NEW_EVENTS_BADGE_TRANSIENT = 'googlesitekit_conversion_reporting_new_badge_events';
/**
* The skip new badge events transient name.
*/
public const SKIP_NEW_BADGE_TRANSIENT = 'googlesitekit_conversion_reporting_skip_new_badge_events';
/**
* Transients instance.
*
* @since 1.144.0
* @var Transients
*/
protected $transients;
/**
* Constructor.
*
* @since 1.144.0
*
* @param Transients $transients Transients instance.
*/
public function __construct(
Transients $transients
) {
$this->transients = $transients;
}
/**
* Saves new events badge to the expirable items.
*
* @since 1.144.0
*
* @param array $new_events New events array.
*/
public function sync_new_badge_events( $new_events ) {
$skip_events_badge = $this->transients->get( self::SKIP_NEW_BADGE_TRANSIENT );
if ( $skip_events_badge ) {
$this->transients->delete( self::SKIP_NEW_BADGE_TRANSIENT );
return;
}
$new_events_badge = $this->transients->get( self::NEW_EVENTS_BADGE_TRANSIENT );
$save_new_badge_transient = fn( $events ) => $this->transients->set(
self::NEW_EVENTS_BADGE_TRANSIENT,
array(
'created_at' => time(),
'events' => $events,
),
7 * DAY_IN_SECONDS
);
if ( ! $new_events_badge ) {
$save_new_badge_transient( $new_events );
return;
}
$new_events_badge_elapsed_time = time() - $new_events_badge['created_at'];
// If the transient existed for 3 days or less, prevent scenarios where
// a new event is detected shortly after (within 1-3 days) the previous events.
// This avoids shortening the "new badge" time for previous events.
// Instead, we merge the new events with the previous ones to ensure the user sees all of them.
if ( $new_events_badge_elapsed_time > ( 3 * DAY_IN_SECONDS ) ) {
$save_new_badge_transient( $new_events );
return;
}
$events = array_merge( $new_events_badge['events'], $new_events );
$save_new_badge_transient( $events );
}
}

View File

@@ -0,0 +1,120 @@
<?php
/**
* Class Google\Site_Kit\Modules\Analytics_4\Conversion_Reporting\Conversion_Reporting_Provider
*
* @package Google\Site_Kit
* @copyright 2024 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\Conversion_Reporting;
use Google\Site_Kit\Context;
use Google\Site_Kit\Core\Storage\Transients;
use Google\Site_Kit\Core\Storage\User_Options;
use Google\Site_Kit\Modules\Analytics_4;
use Google\Site_Kit\Modules\Analytics_4\Settings;
/**
* Class providing the integration of conversion reporting.
*
* @since 1.135.0
* @access private
* @ignore
*/
class Conversion_Reporting_Provider {
/**
* User_Options instance.
*
* @var User_Options
*/
private $user_options;
/**
* Analytics_4 instance.
*
* @var Analytics_4
*/
private $analytics;
/**
* Conversion_Reporting_Cron instance.
*
* @var Conversion_Reporting_Cron
*/
private Conversion_Reporting_Cron $cron;
/**
* Conversion_Reporting_Events_Sync instance.
*
* @var Conversion_Reporting_Events_Sync
*/
private Conversion_Reporting_Events_Sync $events_sync;
/**
* Constructor.
*
* @since 1.135.0
* @since 1.139.0 Added Context to constructor.
*
* @param Context $context Plugin context.
* @param Settings $settings Settings instance.
* @param User_Options $user_options User_Options instance.
* @param Analytics_4 $analytics analytics_4 instance.
*/
public function __construct(
Context $context,
Settings $settings,
User_Options $user_options,
Analytics_4 $analytics
) {
$this->user_options = $user_options;
$this->analytics = $analytics;
$transients = new Transients( $context );
$new_badge_events_sync = new Conversion_Reporting_New_Badge_Events_Sync( $transients );
$this->events_sync = new Conversion_Reporting_Events_Sync(
$settings,
$transients,
$this->analytics,
$new_badge_events_sync
);
$this->cron = new Conversion_Reporting_Cron( fn() => $this->cron_callback() );
}
/**
* Registers functionality through WordPress hooks.
*
* @since 1.135.0
*/
public function register() {
$this->cron->register();
add_action( 'load-toplevel_page_googlesitekit-dashboard', fn () => $this->on_dashboard_load() );
}
/**
* Handles the googlesitekit-dashboard page load callback.
*
* @since 1.135.0
*/
protected function on_dashboard_load() {
$this->cron->maybe_schedule_cron();
}
/**
* Handles the cron callback.
*
* @since 1.135.0
*/
protected function cron_callback() {
$owner_id = $this->analytics->get_owner_id();
$restore_user = $this->user_options->switch_user( $owner_id );
$this->events_sync->sync_detected_events();
$restore_user();
}
}

View File

@@ -0,0 +1,137 @@
<?php
/**
* Class Google\Site_Kit\Modules\Analytics_4\Custom_Dimensions_Data_Available
*
* @package Google\Site_Kit\Modules\Analytics_4
* @copyright 2023 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\Storage\Transients;
/**
* Class for updating Analytics 4 custom dimension data availability state.
*
* @since 1.113.0
* @access private
* @ignore
*/
class Custom_Dimensions_Data_Available {
/**
* List of valid custom dimension slugs.
*
* @since 1.113.0
* @var array
*/
const CUSTOM_DIMENSION_SLUGS = array(
'googlesitekit_post_date',
'googlesitekit_post_author',
'googlesitekit_post_categories',
'googlesitekit_post_type',
);
/**
* Transients instance.
*
* @since 1.113.0
* @var Transients
*/
protected $transients;
/**
* Constructor.
*
* @since 1.113.0
*
* @param Transients $transients Transients instance.
*/
public function __construct( Transients $transients ) {
$this->transients = $transients;
}
/**
* Gets data available transient name for the custom dimension.
*
* @since 1.113.0
*
* @param string $custom_dimension Custom dimension slug.
* @return string Data available transient name.
*/
protected function get_data_available_transient_name( $custom_dimension ) {
return "googlesitekit_custom_dimension_{$custom_dimension}_data_available";
}
/**
* Gets data availability for all custom dimensions.
*
* @since 1.113.0
*
* @return array Associative array of custom dimension names and their data availability state.
*/
public function get_data_availability() {
return array_reduce(
self::CUSTOM_DIMENSION_SLUGS,
function ( $data_availability, $custom_dimension ) {
$data_availability[ $custom_dimension ] = $this->is_data_available( $custom_dimension );
return $data_availability;
},
array()
);
}
/**
* Checks whether the data is available for the custom dimension.
*
* @since 1.113.0
*
* @param string $custom_dimension Custom dimension slug.
* @return bool True if data is available, false otherwise.
*/
protected function is_data_available( $custom_dimension ) {
return (bool) $this->transients->get( $this->get_data_available_transient_name( $custom_dimension ) );
}
/**
* Sets the data available state for the custom dimension.
*
* @since 1.113.0
*
* @param string $custom_dimension Custom dimension slug.
* @return bool True on success, false otherwise.
*/
public function set_data_available( $custom_dimension ) {
return $this->transients->set( $this->get_data_available_transient_name( $custom_dimension ), true );
}
/**
* Resets the data available state for all custom dimensions.
*
* @since 1.113.0
* @since 1.114.0 Added optional $custom_dimensions parameter.
*
* @param array $custom_dimensions Optional. List of custom dimension slugs to reset.
*/
public function reset_data_available(
$custom_dimensions = self::CUSTOM_DIMENSION_SLUGS
) {
foreach ( $custom_dimensions as $custom_dimension ) {
$this->transients->delete( $this->get_data_available_transient_name( $custom_dimension ) );
}
}
/**
* Checks whether the custom dimension is valid.
*
* @since 1.113.0
*
* @param string $custom_dimension Custom dimension slug.
* @return bool True if valid, false otherwise.
*/
public function is_valid_custom_dimension( $custom_dimension ) {
return in_array( $custom_dimension, self::CUSTOM_DIMENSION_SLUGS, true );
}
}

View File

@@ -0,0 +1,58 @@
<?php
/**
* Class AccountProvisioningService
*
* @package Google\Site_Kit\Modules\Analytics_4\GoogleAnalyticsAdmin
* @copyright 2023 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\GoogleAnalyticsAdmin;
use Google\Site_Kit_Dependencies\Google\Service\GoogleAnalyticsAdmin;
use Google\Site_Kit_Dependencies\Google_Client;
/**
* Class for Analytics account provisioning service of the GoogleAnalytics Admin API.
*
* @since 1.98.0
* @access private
* @ignore
*/
class AccountProvisioningService extends GoogleAnalyticsAdmin {
/**
* Accounts resource instance.
*
* @var AccountsResource
*/
public $accounts;
/**
* Constructor.
*
* @since 1.98.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->accounts = new AccountsResource(
$this,
$this->serviceName, // phpcs:ignore WordPress.NamingConventions.ValidVariableName
'accounts',
array(
'methods' => array(
'provisionAccountTicket' => array(
'path' => 'v1beta/accounts:provisionAccountTicket',
'httpMethod' => 'POST',
'parameters' => array(),
),
),
)
);
}
}

View File

@@ -0,0 +1,44 @@
<?php
/**
* Class AccountsResource
*
* @package Google\Site_Kit\Modules\Analytics_4\GoogleAnalyticsAdmin
* @copyright 2023 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\GoogleAnalyticsAdmin;
use Google\Site_Kit_Dependencies\Google\Service\GoogleAnalyticsAdmin\GoogleAnalyticsAdminV1betaProvisionAccountTicketResponse;
use Google\Site_Kit_Dependencies\Google\Service\Resource;
/**
* Class for representing the Accounts resource of the GoogleAnalytics Admin API for provisioning.
*
* @since 1.98.0
* @access private
* @ignore
*/
class AccountsResource extends Resource {
/**
* Requests a ticket for creating an account.
*
* @since 1.98.0
*
* @param Proxy_GoogleAnalyticsAdminProvisionAccountTicketRequest $post_body The post body to send.
* @param array $opt_params Optional parameters.
* @return GoogleAnalyticsAdminV1betaProvisionAccountTicketResponse
*/
public function provisionAccountTicket( Proxy_GoogleAnalyticsAdminProvisionAccountTicketRequest $post_body, $opt_params = array() ) {
$params = array( 'postBody' => $post_body );
$params = array_merge( $params, $opt_params );
return $this->call(
'provisionAccountTicket',
array( $params ),
GoogleAnalyticsAdminV1betaProvisionAccountTicketResponse::class
);
}
}

View File

@@ -0,0 +1,93 @@
<?php
// phpcs:ignoreFile
// Suppress coding standards checks for this file.
// Reason: This file is a copy of the `GoogleAnalyticsAdminV1alphaEnhancedMeasurementSettings` class
// from the Google API PHP Client library with a slight modification.
/**
* Class EnhancedMeasurementSettingsModel
*
* @package Google\Site_Kit\Modules\Analytics_4\GoogleAnalyticsAdmin
* @copyright 2023 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\GoogleAnalyticsAdmin;
/**
* The EnhancedMeasurementSettingsModel class.
*/
class EnhancedMeasurementSettingsModel extends \Google\Site_Kit_Dependencies\Google\Model {
public $fileDownloadsEnabled;
public $name;
public $outboundClicksEnabled;
public $pageChangesEnabled;
public $scrollsEnabled;
public $searchQueryParameter;
public $siteSearchEnabled;
public $streamEnabled;
public $uriQueryParameter;
public $videoEngagementEnabled;
public function setFileDownloadsEnabled( $fileDownloadsEnabled ) {
$this->fileDownloadsEnabled = $fileDownloadsEnabled;
}
public function getFileDownloadsEnabled() {
return $this->fileDownloadsEnabled;
}
public function setName( $name ) {
$this->name = $name;
}
public function getName() {
return $this->name;
}
public function setOutboundClicksEnabled( $outboundClicksEnabled ) {
$this->outboundClicksEnabled = $outboundClicksEnabled;
}
public function getOutboundClicksEnabled() {
return $this->outboundClicksEnabled;
}
public function setPageChangesEnabled( $pageChangesEnabled ) {
$this->pageChangesEnabled = $pageChangesEnabled;
}
public function getPageChangesEnabled() {
return $this->pageChangesEnabled;
}
public function setScrollsEnabled( $scrollsEnabled ) {
$this->scrollsEnabled = $scrollsEnabled;
}
public function getScrollsEnabled() {
return $this->scrollsEnabled;
}
public function setSearchQueryParameter( $searchQueryParameter ) {
$this->searchQueryParameter = $searchQueryParameter;
}
public function getSearchQueryParameter() {
return $this->searchQueryParameter;
}
public function setSiteSearchEnabled( $siteSearchEnabled ) {
$this->siteSearchEnabled = $siteSearchEnabled;
}
public function getSiteSearchEnabled() {
return $this->siteSearchEnabled;
}
public function setStreamEnabled( $streamEnabled ) {
$this->streamEnabled = $streamEnabled;
}
public function getStreamEnabled() {
return $this->streamEnabled;
}
public function setUriQueryParameter( $uriQueryParameter ) {
$this->uriQueryParameter = $uriQueryParameter;
}
public function getUriQueryParameter() {
return $this->uriQueryParameter;
}
public function setVideoEngagementEnabled( $videoEngagementEnabled ) {
$this->videoEngagementEnabled = $videoEngagementEnabled;
}
public function getVideoEngagementEnabled() {
return $this->videoEngagementEnabled;
}
}

View File

@@ -0,0 +1,108 @@
<?php
/**
* Class PropertiesAdSenseLinksService
*
* @package Google\Site_Kit\Modules\Analytics_4\GoogleAnalyticsAdmin
* @copyright 2024 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\GoogleAnalyticsAdmin;
use Google\Site_Kit_Dependencies\Google\Service\GoogleAnalyticsAdmin;
use Google\Site_Kit_Dependencies\Google_Client;
use Google\Site_Kit_Dependencies\Google_Service_GoogleAnalyticsAdmin_PropertiesAdSenseLinks_Resource as PropertiesAdSenseLinksResource;
/**
* Class for managing GA4 AdSense Links.
*
* @since 1.119.0
* @access private
* @ignore
*/
class PropertiesAdSenseLinksService extends GoogleAnalyticsAdmin {
/**
* PropertiesAdSenseLinksResource instance.
*
* @var PropertiesAdSenseLinksResource
*/
public $properties_adSenseLinks; // phpcs:ignore WordPress.NamingConventions.ValidVariableName
/**
* Constructor.
*
* @since 1.119.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->version = 'v1alpha';
// phpcs:ignore WordPress.NamingConventions.ValidVariableName
$this->properties_adSenseLinks = new PropertiesAdSenseLinksResource(
$this,
$this->serviceName, // phpcs:ignore WordPress.NamingConventions.ValidVariableName
'adSenseLinks',
array(
'methods' => array(
'create' => array(
'path' => 'v1alpha/{+parent}/adSenseLinks',
'httpMethod' => 'POST',
'parameters' => array(
'parent' => array(
'location' => 'path',
'type' => 'string',
'required' => true,
),
),
),
'delete' => array(
'path' => 'v1alpha/{+name}',
'httpMethod' => 'DELETE',
'parameters' => array(
'name' => array(
'location' => 'path',
'type' => 'string',
'required' => true,
),
),
),
'get' => array(
'path' => 'v1alpha/{+name}',
'httpMethod' => 'GET',
'parameters' => array(
'name' => array(
'location' => 'path',
'type' => 'string',
'required' => true,
),
),
),
'list' => array(
'path' => 'v1alpha/{+parent}/adSenseLinks',
'httpMethod' => 'GET',
'parameters' => array(
'parent' => array(
'location' => 'path',
'type' => 'string',
'required' => true,
),
'pageSize' => array(
'location' => 'query',
'type' => 'integer',
),
'pageToken' => array(
'location' => 'query',
'type' => 'string',
),
),
),
),
)
);
}
}

View File

@@ -0,0 +1,85 @@
<?php
/**
* Class PropertiesAudiencesService
*
* @package Google\Site_Kit\Modules\Analytics_4\GoogleAnalyticsAdmin
* @copyright 2024 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\GoogleAnalyticsAdmin;
use Google\Site_Kit_Dependencies\Google\Service\GoogleAnalyticsAdmin;
use Google\Site_Kit_Dependencies\Google\Service\GoogleAnalyticsAdmin\Resource\PropertiesAudiences;
use Google\Site_Kit_Dependencies\Google_Client;
/**
* Class for managing GA4 audiences.
*
* @since 1.120.0
* @access private
* @ignore
*/
class PropertiesAudiencesService extends GoogleAnalyticsAdmin {
/**
* PropertiesAudiences instance.
*
* @var PropertiesAudiences
*/
public $properties_audiences;
/**
* Constructor.
*
* @since 1.120.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->version = 'v1alpha';
$this->properties_audiences = new PropertiesAudiences(
$this,
$this->serviceName, // phpcs:ignore WordPress.NamingConventions.ValidVariableName
'audiences',
array(
'methods' => array(
'create' => array(
'path' => 'v1alpha/{+parent}/audiences',
'httpMethod' => 'POST',
'parameters' => array(
'parent' => array(
'location' => 'path',
'type' => 'string',
'required' => true,
),
),
),
'list' => array(
'path' => 'v1alpha/{+parent}/audiences',
'httpMethod' => 'GET',
'parameters' => array(
'parent' => array(
'location' => 'path',
'type' => 'string',
'required' => true,
),
'pageSize' => array(
'location' => 'query',
'type' => 'integer',
),
'pageToken' => array(
'location' => 'query',
'type' => 'string',
),
),
),
),
)
);
}
}

View File

@@ -0,0 +1,63 @@
<?php
/**
* Class PropertiesEnhancedMeasurementResource
*
* @package Google\Site_Kit\Modules\Analytics_4\GoogleAnalyticsAdmin
* @copyright 2023 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\GoogleAnalyticsAdmin;
use Google\Site_Kit\Modules\Analytics_4\GoogleAnalyticsAdmin\EnhancedMeasurementSettingsModel;
use Google\Site_Kit_Dependencies\Google\Service\Resource;
/**
* The "enhancedMeasurementSettings" collection of methods.
*/
class PropertiesEnhancedMeasurementResource extends Resource {
/**
* Returns the singleton enhanced measurement settings for this web stream. Note
* that the stream must enable enhanced measurement for these settings to take
* effect. (webDataStreams.getEnhancedMeasurementSettings)
*
* @since 1.110.0
*
* @param string $name Required. The name of the settings to lookup. Format: properties/{property_id}/webDataStreams/{stream_id}/enhancedMeasurementSettings
* Example: "properties/1000/webDataStreams/2000/enhancedMeasurementSettings".
* @param array $opt_params Optional parameters.
* @return EnhancedMeasurementSettingsModel
*/
public function getEnhancedMeasurementSettings( $name, $opt_params = array() ) {
$params = array( 'name' => $name );
$params = array_merge( $params, $opt_params );
return $this->call( 'getEnhancedMeasurementSettings', array( $params ), EnhancedMeasurementSettingsModel::class );
}
/**
* Updates the singleton enhanced measurement settings for this web stream. Note
* that the stream must enable enhanced measurement for these settings to take
* effect. (webDataStreams.updateEnhancedMeasurementSettings)
*
* @param string $name Output only. Resource name of this Data Stream. Format: properties/{property_id}/webDataStreams/{stream_id}/enhancedMeasurementSettings
* Example: "properties/1000/webDataStreams/2000/enhancedMeasurementSettings".
* @param EnhancedMeasurementSettingsModel $post_body The body of the request.
* @param array $opt_params Optional parameters.
*
* @opt_param string updateMask Required. The list of fields to be updated.
* Field names must be in snake case (e.g., "field_to_update"). Omitted fields
* will not be updated. To replace the entire entity, use one path with the
* string "*" to match all fields.
* @return EnhancedMeasurementSettingsModel
*/
public function updateEnhancedMeasurementSettings( $name, EnhancedMeasurementSettingsModel $post_body, $opt_params = array() ) {
$params = array(
'name' => $name,
'postBody' => $post_body,
);
$params = array_merge( $params, $opt_params );
return $this->call( 'updateEnhancedMeasurementSettings', array( $params ), EnhancedMeasurementSettingsModel::class );
}
}

View File

@@ -0,0 +1,81 @@
<?php
/**
* Class PropertiesEnhancedMeasurementService
*
* @package Google\Site_Kit\Modules\Analytics_4\GoogleAnalyticsAdmin
* @copyright 2023 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\GoogleAnalyticsAdmin;
use Google\Site_Kit_Dependencies\Google\Service\GoogleAnalyticsAdmin;
use Google\Site_Kit_Dependencies\Google_Client;
/**
* Class for managing GA4 datastream enhanced measurement settings.
*
* @since 1.110.0
* @access private
* @ignore
*/
class PropertiesEnhancedMeasurementService extends GoogleAnalyticsAdmin {
/**
* PropertiesEnhancedMeasurementResource instance.
*
* @var PropertiesEnhancedMeasurementResource
*/
public $properties_enhancedMeasurements; // phpcs:ignore WordPress.NamingConventions.ValidVariableName
/**
* Constructor.
*
* @since 1.110.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->version = 'v1alpha';
// phpcs:ignore WordPress.NamingConventions.ValidVariableName
$this->properties_enhancedMeasurements = new PropertiesEnhancedMeasurementResource(
$this,
$this->serviceName, // phpcs:ignore WordPress.NamingConventions.ValidVariableName
'enhancedMeasurements',
array(
'methods' => array(
'getEnhancedMeasurementSettings' => array(
'path' => 'v1alpha/{+name}',
'httpMethod' => 'GET',
'parameters' => array(
'name' => array(
'location' => 'path',
'type' => 'string',
'required' => true,
),
),
),
'updateEnhancedMeasurementSettings' => array(
'path' => 'v1alpha/{+name}',
'httpMethod' => 'PATCH',
'parameters' => array(
'name' => array(
'location' => 'path',
'type' => 'string',
'required' => true,
),
'updateMask' => array(
'location' => 'query',
'type' => 'string',
),
),
),
),
)
);
}
}

View File

@@ -0,0 +1,106 @@
<?php
/**
* Class Proxy_GoogleAnalyticsAdminProvisionAccountTicketRequest
*
* @package Google\Site_Kit\Modules\Analytics_4\GoogleAnalyticsAdmin
* @copyright 2023 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\GoogleAnalyticsAdmin;
use Google\Site_Kit_Dependencies\Google\Service\GoogleAnalyticsAdmin\GoogleAnalyticsAdminV1betaProvisionAccountTicketRequest;
/**
* Class for representing a proxied account ticket provisioning request body.
*
* @since 1.98.0
* @access private
* @ignore
*/
class Proxy_GoogleAnalyticsAdminProvisionAccountTicketRequest extends GoogleAnalyticsAdminV1betaProvisionAccountTicketRequest {
/**
* The site ID.
*
* @since 1.98.0
* @var string
*/
public $site_id = '';
/**
* The site secret.
*
* @since 1.98.0
* @var string
*/
public $site_secret = '';
/**
* The state of the show progress flag.
*
* @since 1.165.0
* @var bool
*/
public $show_progress = false;
/**
* Gets the site ID.
*
* @since 1.98.0
*/
public function getSiteId() {
return $this->site_id;
}
/**
* Sets the site ID.
*
* @since 1.98.0
*
* @param string $id The site id.
*/
public function setSiteId( $id ) {
$this->site_id = $id;
}
/**
* Gets the site secret.
*
* @since 1.98.0
*/
public function getSiteSecret() {
return $this->site_secret;
}
/**
* Sets the site secret.
*
* @since 1.98.0
*
* @param string $secret The site secret.
*/
public function setSiteSecret( $secret ) {
$this->site_secret = $secret;
}
/**
* Sets the show progress flag.
*
* @since 1.165.0
*
* @param bool $show_progress The show progress flag.
*/
public function setShowProgress( $show_progress ) {
$this->show_progress = $show_progress;
}
/**
* Gets the show progress flag.
*
* @since 1.165.0
*/
public function getShowProgress() {
return $this->show_progress;
}
}

View File

@@ -0,0 +1,46 @@
<?php
/**
* Class Google\Site_Kit\Modules\Analytics_4\Report
*
* @package Google\Site_Kit\Modules\Analytics_4
* @copyright 2023 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\Context;
use Google\Site_Kit\Modules\Analytics_4\Report\ReportParsers;
/**
* The base class for Analytics 4 reports.
*
* @since 1.99.0
* @access private
* @ignore
*/
class Report extends ReportParsers {
/**
* Plugin context.
*
* @since 1.99.0
* @var Context
*/
protected $context;
/**
* Constructor.
*
* @since 1.99.0
*
* @param Context $context Plugin context.
*/
public function __construct( Context $context ) {
$this->context = $context;
}
// NOTE: The majority of this classes logic has been abstracted to
// ReportParsers which contains the methods for the Report class.
}

View File

@@ -0,0 +1,57 @@
<?php
/**
* Class Google\Site_Kit\Modules\Analytics_4\Report\Metric_Filter\Between_Filter
*
* @package Google\Site_Kit\Modules\Analytics_4\Report\Metric_Filter
* @copyright 2023 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\Report\Filters;
use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\Filter as Google_Service_AnalyticsData_Filter;
use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\FilterExpression as Google_Service_AnalyticsData_FilterExpression;
use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\BetweenFilter as Google_Service_AnalyticsData_BetweenFilter;
use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\NumericValue;
/**
* Class for parsing the metric between filter.
*
* @since 1.111.0
* @access private
* @ignore
*/
class Between_Filter {
/**
* Converts the metric filter into the GA4 compatible metric filter expression.
*
* @since 1.111.0
*
* @param string $metric_name The metric name.
* @param integer $from_value The filter from value.
* @param integer $to_value The filter to value.
* @return Google_Service_AnalyticsData_FilterExpression The filter expression instance.
*/
public function parse_filter_expression( $metric_name, $from_value, $to_value ) {
$numeric_from_value = new NumericValue();
$numeric_from_value->setInt64Value( $from_value );
$numeric_to_value = new NumericValue();
$numeric_to_value->setInt64Value( $to_value );
$between_filter = new Google_Service_AnalyticsData_BetweenFilter();
$between_filter->setFromValue( $numeric_from_value );
$between_filter->setToValue( $numeric_to_value );
$filter = new Google_Service_AnalyticsData_Filter();
$filter->setFieldName( $metric_name );
$filter->setBetweenFilter( $between_filter );
$expression = new Google_Service_AnalyticsData_FilterExpression();
$expression->setFilter( $filter );
return $expression;
}
}

View File

@@ -0,0 +1,48 @@
<?php
/**
* Class Google\Site_Kit\Modules\Analytics_4\Report\Filters\Empty_Filter
*
* @package Google\Site_Kit\Modules\Analytics_4\Report\Filters
* @copyright 2025 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\Report\Filters;
use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\Filter as Google_Service_AnalyticsData_Filter;
use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\FilterExpression as Google_Service_AnalyticsData_FilterExpression;
use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\EmptyFilter as Google_Service_AnalyticsData_EmptyFilter;
/**
* Class for parsing the empty filter.
*
* @since 1.147.0
* @access private
* @ignore
*/
class Empty_Filter implements Filter {
/**
* Parses the empty filter.
*
* @since 1.147.0
* @param string $name The filter field name.
* @param string $value The filter value (not used).
*
* @return Google_Service_AnalyticsData_FilterExpression The filter expression.
*/
public function parse_filter_expression( $name, $value ) {
$empty_filter = new Google_Service_AnalyticsData_EmptyFilter();
$filter = new Google_Service_AnalyticsData_Filter();
$filter->setFieldName( $name );
$filter->setEmptyFilter( $empty_filter );
$expression = new Google_Service_AnalyticsData_FilterExpression();
$expression->setFilter( $filter );
return $expression;
}
}

View File

@@ -0,0 +1,33 @@
<?php
/**
* Class Google\Site_Kit\Modules\Analytics_4\Report\Filter
*
* @package Google\Site_Kit\Modules\Analytics_4\Report\Filters
* @copyright 2023 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\Report\Filters;
use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\FilterExpression as Google_Service_AnalyticsData_FilterExpression;
/**
* Interface for a filter class.
*
* @since 1.106.0
* @since 1.147.0 Moved from `Analytics_4\Report\Dimension_Filters` to `Analytics_4\Report\Filters` for use with both dimensions and metrics.
*/
interface Filter {
/**
* Converts the filter into the GA4 compatible filter expression.
*
* @since 1.106.0
*
* @param string $name Filter name.
* @param mixed $value Filter value.
* @return Google_Service_AnalyticsData_FilterExpression The filter expression instance.
*/
public function parse_filter_expression( $name, $value );
}

View File

@@ -0,0 +1,49 @@
<?php
/**
* Class Google\Site_Kit\Modules\Analytics_4\Report\In_List_Filter
*
* @package Google\Site_Kit\Modules\Analytics_4\Report\Filters
* @copyright 2023 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\Report\Filters;
use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\Filter as Google_Service_AnalyticsData_Filter;
use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\FilterExpression as Google_Service_AnalyticsData_FilterExpression;
use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\InListFilter as Google_Service_AnalyticsData_InListFilter;
/**
* Class for parsing the in-list filter.
*
* @since 1.106.0
* @since 1.147.0 Moved from `Analytics_4\Report\Dimension_Filters` to `Analytics_4\Report\Filters` for use with both dimensions and metrics.
* @access private
* @ignore
*/
class In_List_Filter implements Filter {
/**
* Converts the filter into the GA4 compatible filter expression.
*
* @since 1.106.0
*
* @param string $name The filter field name.
* @param mixed $value The filter value.
* @return Google_Service_AnalyticsData_FilterExpression The filter expression instance.
*/
public function parse_filter_expression( $name, $value ) {
$in_list_filter = new Google_Service_AnalyticsData_InListFilter();
$in_list_filter->setValues( $value );
$filter = new Google_Service_AnalyticsData_Filter();
$filter->setFieldName( $name );
$filter->setInListFilter( $in_list_filter );
$expression = new Google_Service_AnalyticsData_FilterExpression();
$expression->setFilter( $filter );
return $expression;
}
}

View File

@@ -0,0 +1,54 @@
<?php
/**
* Class Google\Site_Kit\Modules\Analytics_4\Report\Metric_Filter\Numeric_Filter
*
* @package Google\Site_Kit\Modules\Analytics_4\Report\Metric_Filter
* @copyright 2023 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\Report\Filters;
use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\Filter as Google_Service_AnalyticsData_Filter;
use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\FilterExpression as Google_Service_AnalyticsData_FilterExpression;
use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\NumericFilter as Google_Service_AnalyticsData_NumericFilter;
use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\NumericValue;
/**
* Class for parsing the metric numeric filter.
*
* @since 1.111.0
* @access private
* @ignore
*/
class Numeric_Filter {
/**
* Converts the metric filter into the GA4 compatible metric filter expression.
*
* @since 1.111.0
*
* @param string $metric_name The metric name.
* @param string $operation The filter operation.
* @param integer $value The filter value.
* @return Google_Service_AnalyticsData_FilterExpression The filter expression instance.
*/
public function parse_filter_expression( $metric_name, $operation, $value ) {
$numeric_value = new NumericValue();
$numeric_value->setInt64Value( $value );
$numeric_filter = new Google_Service_AnalyticsData_NumericFilter();
$numeric_filter->setOperation( $operation );
$numeric_filter->setValue( $numeric_value );
$filter = new Google_Service_AnalyticsData_Filter();
$filter->setFieldName( $metric_name );
$filter->setNumericFilter( $numeric_filter );
$expression = new Google_Service_AnalyticsData_FilterExpression();
$expression->setFilter( $filter );
return $expression;
}
}

View File

@@ -0,0 +1,102 @@
<?php
/**
* Class Google\Site_Kit\Modules\Analytics_4\Report\String_Filter
*
* @package Google\Site_Kit\Modules\Analytics_4\Report\Filters
* @copyright 2023 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\Report\Filters;
use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\Filter as Google_Service_AnalyticsData_Filter;
use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\FilterExpression as Google_Service_AnalyticsData_FilterExpression;
use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\FilterExpressionList as Google_Service_AnalyticsData_FilterExpressionList;
use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\StringFilter as Google_Service_AnalyticsData_StringFilter;
/**
* Class for parsing the string filter.
*
* @since 1.106.0
* @since 1.147.0 Moved from `Analytics_4\Report\Dimension_Filters` to `Analytics_4\Report\Filters` for use with both dimensions and metrics.
* @access private
* @ignore
*/
class String_Filter implements Filter {
/**
* Converts the filter into the GA4 compatible filter expression.
*
* @since 1.106.0
*
* @param string $name The filter field name.
* @param mixed $value The filter value.
* @return Google_Service_AnalyticsData_FilterExpression The filter expression instance.
*/
public function parse_filter_expression( $name, $value ) {
$match_type = isset( $value['matchType'] )
? $value['matchType']
: 'EXACT';
$filter_value = isset( $value['value'] )
? $value['value']
: $value;
// If there are many values for this filter, then it means that we want to find
// rows where values are included in the list of provided values. In this case,
// we need to create a nested filter expression that contains separate string filters
// for each item in the list and combined into the "OR" group.
if ( is_array( $filter_value ) ) {
$expressions = array();
foreach ( $filter_value as $value ) {
$expressions[] = $this->compose_individual_filter_expression(
$name,
$match_type,
$value
);
}
$expression_list = new Google_Service_AnalyticsData_FilterExpressionList();
$expression_list->setExpressions( $expressions );
$filter_expression = new Google_Service_AnalyticsData_FilterExpression();
$filter_expression->setOrGroup( $expression_list );
return $filter_expression;
}
// If we have a single value for the filter, then we should use just a single
// string filter expression and there is no need to create a nested one.
return $this->compose_individual_filter_expression(
$name,
$match_type,
$filter_value
);
}
/**
* Composes individual filter expression and returns it.
*
* @since 1.106.0
*
* @param string $name Filter name.
* @param string $match_type Filter match type.
* @param mixed $value Filter value.
* @return Google_Service_AnalyticsData_FilterExpression The filter expression instance.
*/
protected function compose_individual_filter_expression( $name, $match_type, $value ) {
$string_filter = new Google_Service_AnalyticsData_StringFilter();
$string_filter->setMatchType( $match_type );
$string_filter->setValue( $value );
$filter = new Google_Service_AnalyticsData_Filter();
$filter->setFieldName( $name );
$filter->setStringFilter( $string_filter );
$filter_expression = new Google_Service_AnalyticsData_FilterExpression();
$filter_expression->setFilter( $filter );
return $filter_expression;
}
}

View File

@@ -0,0 +1,173 @@
<?php
/**
* Class Google\Site_Kit\Modules\Analytics_4\Report\ReportParsers
*
* @package Google\Site_Kit\Modules\Analytics_4\Report
* @copyright 2024 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\Report;
use Google\Site_Kit\Core\REST_API\Data_Request;
use Google\Site_Kit\Core\Util\Date;
use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\DateRange as Google_Service_AnalyticsData_DateRange;
use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\Dimension as Google_Service_AnalyticsData_Dimension;
use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\DimensionOrderBy as Google_Service_AnalyticsData_DimensionOrderBy;
use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\MetricOrderBy as Google_Service_AnalyticsData_MetricOrderBy;
use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\OrderBy as Google_Service_AnalyticsData_OrderBy;
/**
* A class with helper methods to parse report properties
*
* @since 1.130.0
* @access private
* @ignore
*/
class ReportParsers {
/**
* Parses report dimensions received in the request params.
*
* @since 1.99.0
* @since 1.130.0 Moved into `ReportParsers` for shared used (originally between `Report` and `PivotReport`). `PivotReport` has since been removed.
*
* @param Data_Request $data Data request object.
* @return Google_Service_AnalyticsData_Dimension[] An array of AnalyticsData Dimension objects.
*/
protected function parse_dimensions( Data_Request $data ) {
$dimensions = $data['dimensions'];
if ( empty( $dimensions ) || ( ! is_string( $dimensions ) && ! is_array( $dimensions ) ) ) {
return array();
}
if ( is_string( $dimensions ) ) {
$dimensions = explode( ',', $dimensions );
} elseif ( is_array( $dimensions ) && ! wp_is_numeric_array( $dimensions ) ) { // If single object is passed.
$dimensions = array( $dimensions );
}
$dimensions = array_filter(
array_map(
function ( $dimension_def ) {
$dimension = new Google_Service_AnalyticsData_Dimension();
if ( is_string( $dimension_def ) ) {
$dimension->setName( $dimension_def );
} elseif ( is_array( $dimension_def ) && ! empty( $dimension_def['name'] ) ) {
$dimension->setName( $dimension_def['name'] );
} else {
return null;
}
return $dimension;
},
array_filter( $dimensions )
)
);
return $dimensions;
}
/**
* Parses report date ranges received in the request params.
*
* @since 1.99.0
* @since 1.130.0 Moved into `ReportParsers` for shared used (originally between `Report` and `PivotReport`). `PivotReport` has since been removed.
* @since 1.157.0 Added support for dateRangeName and compareDateRangeName parameters.
*
* @param Data_Request $data Data request object.
* @return Google_Service_AnalyticsData_DateRange[] An array of AnalyticsData DateRange objects.
*/
public function parse_dateranges( Data_Request $data ) {
$date_ranges = array();
$start_date = $data['startDate'] ?? '';
$end_date = $data['endDate'] ?? '';
if ( strtotime( $start_date ) && strtotime( $end_date ) ) {
$compare_start_date = $data['compareStartDate'] ?? '';
$compare_end_date = $data['compareEndDate'] ?? '';
$date_ranges[] = array( $start_date, $end_date );
// When using multiple date ranges, it changes the structure of the response:
// Aggregate properties (minimum, maximum, totals) will have an entry per date range.
// The rows property will have additional row entries for each date range.
if ( strtotime( $compare_start_date ) && strtotime( $compare_end_date ) ) {
$date_ranges[] = array( $compare_start_date, $compare_end_date );
}
} else {
// Default the date range to the last 28 days.
$date_ranges[] = Date::parse_date_range( 'last-28-days', 1 );
}
// Get date range names if provided.
$date_range_name = $data['dateRangeName'] ?? '';
$compare_date_range_name = $data['compareDateRangeName'] ?? '';
$date_ranges = array_map(
function ( $date_range, $index ) use ( $date_range_name, $compare_date_range_name ) {
list ( $start_date, $end_date ) = $date_range;
$date_range_obj = new Google_Service_AnalyticsData_DateRange();
$date_range_obj->setStartDate( $start_date );
$date_range_obj->setEndDate( $end_date );
// Set date range names if provided.
if ( 0 === $index && ! empty( $date_range_name ) ) {
$date_range_obj->setName( $date_range_name );
} elseif ( 1 === $index && ! empty( $compare_date_range_name ) ) {
$date_range_obj->setName( $compare_date_range_name );
}
return $date_range_obj;
},
$date_ranges,
array_keys( $date_ranges )
);
return $date_ranges;
}
/**
* Parses the orderby value of the data request into an array of AnalyticsData OrderBy object instances.
*
* @since 1.99.0
* @since 1.130.0 Moved into `ReportParsers` for shared used (originally between `Report` and `PivotReport`). `PivotReport` has since been removed.
*
* @param Data_Request $data Data request object.
* @return Google_Service_AnalyticsData_OrderBy[] An array of AnalyticsData OrderBy objects.
*/
protected function parse_orderby( Data_Request $data ) {
$orderby = $data['orderby'];
if ( empty( $orderby ) || ! is_array( $orderby ) || ! wp_is_numeric_array( $orderby ) ) {
return array();
}
$results = array_map(
function ( $order_def ) {
$order_by = new Google_Service_AnalyticsData_OrderBy();
$order_by->setDesc( ! empty( $order_def['desc'] ) );
if ( isset( $order_def['metric'] ) && isset( $order_def['metric']['metricName'] ) ) {
$metric_order_by = new Google_Service_AnalyticsData_MetricOrderBy();
$metric_order_by->setMetricName( $order_def['metric']['metricName'] );
$order_by->setMetric( $metric_order_by );
} elseif ( isset( $order_def['dimension'] ) && isset( $order_def['dimension']['dimensionName'] ) ) {
$dimension_order_by = new Google_Service_AnalyticsData_DimensionOrderBy();
$dimension_order_by->setDimensionName( $order_def['dimension']['dimensionName'] );
$order_by->setDimension( $dimension_order_by );
} else {
return null;
}
return $order_by;
},
$orderby
);
$results = array_filter( $results );
$results = array_values( $results );
return $results;
}
}

View File

@@ -0,0 +1,73 @@
<?php
/**
* Class Google\Site_Kit\Modules\Analytics_4\Report\Request
*
* @package Google\Site_Kit\Modules\Analytics_4\Report
* @copyright 2023 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\Report;
use Google\Site_Kit\Core\REST_API\Data_Request;
use Google\Site_Kit\Core\Validation\Exception\Invalid_Report_Dimensions_Exception;
use Google\Site_Kit\Modules\Analytics_4\Report;
use Google\Site_Kit\Modules\Analytics_4\Report\RequestHelpers;
use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\RunReportRequest as Google_Service_AnalyticsData_RunReportRequest;
use WP_Error;
/**
* Class for Analytics 4 report requests.
*
* @since 1.99.0
* @access private
* @ignore
*/
class Request extends Report {
/**
* Creates and executes a new Analytics 4 report request.
*
* @since 1.99.0
*
* @param Data_Request $data Data request object.
* @param bool $is_shared_request Determines whether the current request is shared or not.
* @return RequestInterface|WP_Error Request object on success, or WP_Error on failure.
*/
public function create_request( Data_Request $data, $is_shared_request ) {
$request_helpers = new RequestHelpers( $this->context );
$request = new Google_Service_AnalyticsData_RunReportRequest();
$request->setMetricAggregations( array( 'TOTAL', 'MINIMUM', 'MAXIMUM' ) );
if ( ! empty( $data['limit'] ) ) {
$request->setLimit( $data['limit'] );
}
$dimensions = $this->parse_dimensions( $data );
if ( ! empty( $dimensions ) ) {
if ( $is_shared_request ) {
try {
$request_helpers->validate_shared_dimensions( $dimensions );
} catch ( Invalid_Report_Dimensions_Exception $exception ) {
return new WP_Error(
'invalid_analytics_4_report_dimensions',
$exception->getMessage()
);
}
}
$request->setDimensions( (array) $dimensions );
}
$request = $request_helpers->shared_create_request( $data, $request, $is_shared_request );
$orderby = $this->parse_orderby( $data );
if ( ! empty( $orderby ) ) {
$request->setOrderBys( $orderby );
}
return $request;
}
}

View File

@@ -0,0 +1,537 @@
<?php
/**
* Class Google\Site_Kit\Modules\Analytics_4\Report\SharedRequestHelpers
*
* @package Google\Site_Kit\Modules\Analytics_4\Report
* @copyright 2024 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\Report;
use Google\Site_Kit\Context;
use Google\Site_Kit\Core\REST_API\Data_Request;
use Google\Site_Kit\Core\Validation\Exception\Invalid_Report_Dimensions_Exception;
use Google\Site_Kit\Core\Validation\Exception\Invalid_Report_Metrics_Exception;
use Google\Site_Kit\Core\Util\URL;
use Google\Site_Kit\Modules\Analytics_4\Report\Filters\Empty_Filter;
use Google\Site_Kit\Modules\Analytics_4\Report\Filters\In_List_Filter;
use Google\Site_Kit\Modules\Analytics_4\Report\Filters\String_Filter;
use Google\Site_Kit\Modules\Analytics_4\Report\Filters\Numeric_Filter;
use Google\Site_Kit\Modules\Analytics_4\Report\Filters\Between_Filter;
use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\Dimension as Google_Service_AnalyticsData_Dimension;
use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\FilterExpression as Google_Service_AnalyticsData_FilterExpression;
use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\FilterExpressionList as Google_Service_AnalyticsData_FilterExpressionList;
use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\RunReportRequest as Google_Service_AnalyticsData_RunReportRequest;
use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\Metric as Google_Service_AnalyticsData_Metric;
use WP_Error;
/**
* A class containing shared methods for creating AnalyticsData Report requests.
*
* @since 1.130.0
* @access private
* @ignore
*/
class RequestHelpers {
/**
* Plugin context.
*
* @since 1.130.0
* @var Context
*/
private $context;
/**
* Constructs a new instance of the class.
*
* @param Context $context Plugin context.
*/
public function __construct( $context ) {
$this->context = $context;
}
/**
* Builds a Analytics Data Report request's shared properties.
*
* @since 1.130.0
*
* @param Data_Request $data Data request object.
* @param Google_Service_AnalyticsData_RunReportRequest $request The report request object.
* @param bool $is_shared_request Determines whether the current request is shared or not.
* @return Google_Service_AnalyticsData_RunReportRequest The report request object.
*/
public function shared_create_request( Data_Request $data, $request, $is_shared_request = false ) {
$keep_empty_rows = is_array( $data->data ) && array_key_exists( 'keepEmptyRows', $data->data ) ? filter_var( $data->data['keepEmptyRows'], FILTER_VALIDATE_BOOLEAN ) : true;
$request->setKeepEmptyRows( $keep_empty_rows );
$dimension_filters = $this->parse_dimension_filters( $data );
$request->setDimensionFilter( $dimension_filters );
$metric_filters = $this->parse_metric_filters( $data );
if ( ! empty( $metric_filters ) ) {
$request->setMetricFilter( $metric_filters );
}
$report_parsers = new ReportParsers();
$date_ranges = $report_parsers->parse_dateranges( $data );
$request->setDateRanges( $date_ranges );
$metrics = $data['metrics'];
if ( is_string( $metrics ) || is_array( $metrics ) ) {
if ( is_string( $metrics ) ) {
$metrics = explode( ',', $data['metrics'] );
} elseif ( is_array( $metrics ) && ! wp_is_numeric_array( $metrics ) ) { // If single object is passed.
$metrics = array( $metrics );
}
$metrics = array_filter(
array_map(
function ( $metric_def ) {
$metric = new Google_Service_AnalyticsData_Metric();
if ( is_string( $metric_def ) ) {
$metric->setName( $metric_def );
} elseif ( is_array( $metric_def ) ) {
$metric->setName( $metric_def['name'] );
if ( ! empty( $metric_def['expression'] ) ) {
$metric->setExpression( $metric_def['expression'] );
}
} else {
return null;
}
return $metric;
},
$metrics
)
);
if ( ! empty( $metrics ) ) {
try {
$this->validate_metrics( $metrics );
} catch ( Invalid_Report_Metrics_Exception $exception ) {
return new WP_Error(
'invalid_analytics_4_report_metrics',
$exception->getMessage()
);
}
if ( $is_shared_request ) {
try {
$this->validate_shared_metrics( $metrics );
} catch ( Invalid_Report_Metrics_Exception $exception ) {
return new WP_Error(
'invalid_analytics_4_report_metrics',
$exception->getMessage()
);
}
}
$request->setMetrics( $metrics );
}
}
return $request;
}
/**
* Validates the given metrics for a report.
*
* Metrics must have valid names, matching the regular expression ^[a-zA-Z0-9_]+$ in keeping with the GA4 API.
*
* @since 1.99.0
* @since 1.130.0 Moved into RequestHelpers for shared use in reports.
*
* @param Google_Service_AnalyticsData_Metric[] $metrics The metrics to validate.
* @throws Invalid_Report_Metrics_Exception Thrown if the metrics are invalid.
*/
protected function validate_metrics( $metrics ) {
$valid_name_expression = '^[a-zA-Z0-9_]+$';
$invalid_metrics = array_map(
function ( $metric ) {
return $metric->getName();
},
array_filter(
$metrics,
function ( $metric ) use ( $valid_name_expression ) {
return ! preg_match( "#$valid_name_expression#", $metric->getName() ?? '' );
}
)
);
if ( count( $invalid_metrics ) > 0 ) {
$message = count( $invalid_metrics ) > 1 ? sprintf(
/* translators: 1: the regular expression for a valid name, 2: a comma separated list of the invalid metrics. */
__(
'Metric names should match the expression %1$s: %2$s',
'google-site-kit'
),
$valid_name_expression,
join(
/* translators: used between list items, there is a space after the comma. */
__( ', ', 'google-site-kit' ),
$invalid_metrics
)
) : sprintf(
/* translators: 1: the regular expression for a valid name, 2: the invalid metric. */
__(
'Metric name should match the expression %1$s: %2$s',
'google-site-kit'
),
$valid_name_expression,
$invalid_metrics[0]
);
throw new Invalid_Report_Metrics_Exception( $message );
}
}
/**
* Validates the report metrics for a shared request.
*
* @since 1.99.0
* @since 1.130.0 Moved into RequestHelpers for shared use in reports.
*
* @param Google_Service_AnalyticsData_Metric[] $metrics The metrics to validate.
* @throws Invalid_Report_Metrics_Exception Thrown if the metrics are invalid.
*/
protected function validate_shared_metrics( $metrics ) {
$valid_metrics = apply_filters(
'googlesitekit_shareable_analytics_4_metrics',
array(
'activeUsers',
'addToCarts',
'averageSessionDuration',
'bounceRate',
'keyEvents',
'ecommercePurchases',
'engagedSessions',
'engagementRate',
'eventCount',
'screenPageViews',
'screenPageViewsPerSession',
'sessions',
'sessionKeyEventRate',
'sessionsPerUser',
'totalAdRevenue',
'totalUsers',
)
);
$invalid_metrics = array_diff(
array_map(
function ( $metric ) {
// If there is an expression, it means the name is there as an alias, otherwise the name should be a valid metric name.
// Therefore, the expression takes precedence to the name for the purpose of allow-list validation.
return ! empty( $metric->getExpression() ) ? $metric->getExpression() : $metric->getName();
},
$metrics
),
$valid_metrics
);
if ( count( $invalid_metrics ) > 0 ) {
$message = count( $invalid_metrics ) > 1 ? sprintf(
/* translators: %s: is replaced with a comma separated list of the invalid metrics. */
__(
'Unsupported metrics requested: %s',
'google-site-kit'
),
join(
/* translators: used between list items, there is a space after the comma. */
__( ', ', 'google-site-kit' ),
$invalid_metrics
)
) : sprintf(
/* translators: %s: is replaced with the invalid metric. */
__(
'Unsupported metric requested: %s',
'google-site-kit'
),
$invalid_metrics[0]
);
throw new Invalid_Report_Metrics_Exception( $message );
}
}
/**
* Validates the report dimensions for a shared request.
*
* @since 1.99.0
* @since 1.130.0 Moved into RequestHelpers for shared use in reports.
*
* @param Google_Service_AnalyticsData_Dimension[] $dimensions The dimensions to validate.
* @throws Invalid_Report_Dimensions_Exception Thrown if the dimensions are invalid.
*/
public function validate_shared_dimensions( $dimensions ) {
$valid_dimensions = apply_filters(
'googlesitekit_shareable_analytics_4_dimensions',
array(
'audienceResourceName',
'adSourceName',
'city',
'country',
'date',
'deviceCategory',
'eventName',
'newVsReturning',
'pagePath',
'pageTitle',
'sessionDefaultChannelGroup',
'sessionDefaultChannelGrouping',
'customEvent:googlesitekit_post_author',
'customEvent:googlesitekit_post_categories',
'customEvent:googlesitekit_post_date',
'customEvent:googlesitekit_post_type',
)
);
$invalid_dimensions = array_diff(
array_map(
function ( $dimension ) {
return $dimension->getName();
},
$dimensions
),
$valid_dimensions
);
if ( count( $invalid_dimensions ) > 0 ) {
$message = count( $invalid_dimensions ) > 1 ? sprintf(
/* translators: %s: is replaced with a comma separated list of the invalid dimensions. */
__(
'Unsupported dimensions requested: %s',
'google-site-kit'
),
join(
/* translators: used between list items, there is a space after the comma. */
__( ', ', 'google-site-kit' ),
$invalid_dimensions
)
) : sprintf(
/* translators: %s: is replaced with the invalid dimension. */
__(
'Unsupported dimension requested: %s',
'google-site-kit'
),
$invalid_dimensions[0]
);
throw new Invalid_Report_Dimensions_Exception( $message );
}
}
/**
* Parses dimension filters and returns a filter expression that should be added to the report request.
*
* @since 1.106.0
* @since 1.130.0 Moved into RequestHelpers for shared use in reports.
*
* @param Data_Request $data Data request object.
* @return Google_Service_AnalyticsData_FilterExpression The filter expression to use with the report request.
*/
protected function parse_dimension_filters( Data_Request $data ) {
$expressions = array();
$reference_url = trim( $this->context->get_reference_site_url(), '/' );
$hostnames = URL::permute_site_hosts( URL::parse( $reference_url, PHP_URL_HOST ) );
$expressions[] = $this->parse_dimension_filter( 'hostName', $hostnames );
if ( ! empty( $data['url'] ) ) {
$url = str_replace( $reference_url, '', esc_url_raw( $data['url'] ) );
$expressions[] = $this->parse_dimension_filter( 'pagePath', $url );
}
if ( is_array( $data['dimensionFilters'] ) ) {
foreach ( $data['dimensionFilters'] as $key => $value ) {
$expressions[] = $this->parse_dimension_filter( $key, $value );
}
}
$filter_expression_list = new Google_Service_AnalyticsData_FilterExpressionList();
$filter_expression_list->setExpressions( array_filter( $expressions ) );
$dimension_filters = new Google_Service_AnalyticsData_FilterExpression();
$dimension_filters->setAndGroup( $filter_expression_list );
return $dimension_filters;
}
/**
* Parses and returns a single dimension filter.
*
* @since 1.106.0
* @since 1.130.0 Moved into RequestHelpers for shared use in reports.
*
* @param string $dimension_name The dimension name.
* @param mixed $dimension_value The dimension fileter settings.
* @return Google_Service_AnalyticsData_FilterExpression The filter expression instance.
*/
protected function parse_dimension_filter( $dimension_name, $dimension_value ) {
// Use the string filter type by default.
$filter_type = 'stringFilter';
if ( isset( $dimension_value['filterType'] ) ) {
// If the filterType property is provided, use the explicit filter type then.
$filter_type = $dimension_value['filterType'];
} elseif ( wp_is_numeric_array( $dimension_value ) ) {
// Otherwise, if the dimension has a numeric array of values, we should fall
// back to the "in list" filter type.
$filter_type = 'inListFilter';
}
if ( 'stringFilter' === $filter_type ) {
$filter_class = String_Filter::class;
} elseif ( 'inListFilter' === $filter_type ) {
$filter_class = In_List_Filter::class;
// Ensure that the 'inListFilter' is provided a flat array of values.
// Extract the actual values from the 'value' key if present.
if ( isset( $dimension_value['value'] ) ) {
$dimension_value = $dimension_value['value'];
}
} elseif ( 'emptyFilter' === $filter_type ) {
$filter_class = Empty_Filter::class;
} else {
return null;
}
$filter = new $filter_class();
$filter_expression = $filter->parse_filter_expression( $dimension_name, $dimension_value );
if ( ! empty( $dimension_value['notExpression'] ) ) {
$not_filter_expression = new Google_Service_AnalyticsData_FilterExpression();
$not_filter_expression->setNotExpression( $filter_expression );
return $not_filter_expression;
}
return $filter_expression;
}
/**
* Parses metric filters and returns a filter expression that should be added to the report request.
*
* @since 1.111.0
* @since 1.130.0 Moved into RequestHelpers for shared use in reports.
*
* @param Data_Request $data Data request object.
* @return Google_Service_AnalyticsData_FilterExpression The filter expression to use with the report request.
*/
protected function parse_metric_filters( Data_Request $data ) {
$expressions = array();
if ( is_array( $data['metricFilters'] ) ) {
foreach ( $data['metricFilters'] as $key => $value ) {
$expressions[] = $this->parse_metric_filter( $key, $value );
}
}
if ( ! empty( $expressions ) ) {
$filter_expression_list = new Google_Service_AnalyticsData_FilterExpressionList();
$filter_expression_list->setExpressions( array_filter( $expressions ) );
$metric_filters = new Google_Service_AnalyticsData_FilterExpression();
$metric_filters->setAndGroup( $filter_expression_list );
return $metric_filters;
}
return null;
}
/**
* Parses and returns a single metric filter.
*
* @since 1.111.0
* @since 1.130.0 Moved into RequestHelpers for shared use in reports.
*
* @param string $metric_name The metric name.
* @param mixed $metric_value The metric filter settings.
* @return Google_Service_AnalyticsData_FilterExpression The filter expression instance.
*/
protected function parse_metric_filter( $metric_name, $metric_value ) {
// Use the numeric filter type by default.
$filter_type = 'numericFilter';
if ( isset( $metric_value['filterType'] ) ) {
// If the filterType property is provided, use the explicit filter type then.
$filter_type = $metric_value['filterType'];
}
if ( 'numericFilter' === $filter_type ) {
if ( ! isset( $metric_value['operation'] ) || ! isset( $metric_value['value'] ) ) {
return null;
}
if ( ! isset( $metric_value['value']['int64Value'] ) ) {
return null;
}
$filter = new Numeric_Filter();
} elseif ( 'betweenFilter' === $filter_type ) {
if ( ! isset( $metric_value['from_value'] ) || ! isset( $metric_value['to_value'] ) ) {
return null;
}
if (
! isset( $metric_value['from_value']['int64Value'] ) ||
! isset( $metric_value['to_value']['int64Value'] )
) {
return null;
}
$filter = new Between_Filter();
} else {
return null;
}
$filter_expression = $this->get_metric_filter_expression(
$filter,
$metric_name,
$metric_value
);
return $filter_expression;
}
/**
* Returns correct filter expression instance based on the metric filter instance.
*
* @since 1.111.0
* @since 1.130.0 Moved into RequestHelpers for shared use in reports.
*
* @param Numeric_Filter|Between_Filter $filter The metric filter instance.
* @param string $metric_name The metric name.
* @param mixed $metric_value The metric filter settings.
* @return Google_Service_AnalyticsData_FilterExpression The filter expression instance.
*/
protected function get_metric_filter_expression( $filter, $metric_name, $metric_value ) {
if ( $filter instanceof Numeric_Filter ) {
$value = $metric_value['value']['int64Value'];
$filter_expression = $filter->parse_filter_expression(
$metric_name,
$metric_value['operation'],
$value
);
} elseif ( $filter instanceof Between_Filter ) {
$from_value = $metric_value['from_value']['int64Value'];
$to_value = $metric_value['to_value']['int64Value'];
$filter_expression = $filter->parse_filter_expression(
$metric_name,
$from_value,
$to_value
);
} else {
return null;
}
return $filter_expression;
}
}

View File

@@ -0,0 +1,235 @@
<?php
/**
* Class Google\Site_Kit\Modules\Analytics_4\Report\Response
*
* @package Google\Site_Kit\Modules\Analytics_4\Report
* @copyright 2023 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\Report;
use Google\Site_Kit\Core\REST_API\Data_Request;
use Google\Site_Kit\Modules\Analytics_4\Report;
use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\DateRange as Google_Service_AnalyticsData_DateRange;
use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\Row as Google_Service_AnalyticsData_Row;
use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\RunReportResponse as Google_Service_AnalyticsData_RunReportResponse;
/**
* Class for Analytics 4 report responses.
*
* @since 1.99.0
* @access private
* @ignore
*/
class Response extends Report {
use Row_Trait;
/**
* Parses the report response, and pads the report data with zero-data rows where rows are missing. This only applies for reports which request a single `date` dimension.
*
* @since 1.99.0
*
* @param Data_Request $data Data request object.
* @param Google_Service_AnalyticsData_RunReportResponse $response Request response.
* @return mixed Parsed response data on success, or WP_Error on failure.
*/
public function parse_response( Data_Request $data, $response ) {
// Return early if the response is not of the expected type.
if ( ! $response instanceof Google_Service_AnalyticsData_RunReportResponse ) {
return $response;
}
// Get report dimensions and return early if there is either more than one dimension or
// the only dimension is not "date".
$dimensions = $this->parse_dimensions( $data );
if ( count( $dimensions ) !== 1 || $dimensions[0]->getName() !== 'date' ) {
return $response;
}
// Get date ranges and return early if there are no date ranges for this report.
$date_ranges = $this->get_sorted_dateranges( $data );
if ( empty( $date_ranges ) ) {
return $response;
}
// Get all available dates in the report.
$existing_rows = array();
foreach ( $response->getRows() as $row ) {
$dimension_values = $row->getDimensionValues();
$range = 'date_range_0';
if ( count( $dimension_values ) > 1 ) {
// Considering this code will only be run when we are requesting a single dimension, `date`,
// the implication is that the row will _only_ have an additional dimension when multiple
// date ranges are requested.
//
// In this scenario, the dimension at index 1 will have a value of `date_range_{i}`, where
// `i` is the zero-based index of the date range.
$range = $dimension_values[1]->getValue();
}
$range = str_replace( 'date_range_', '', $range );
$date = $dimension_values[0]->getValue();
$key = self::get_response_row_key( $date, is_numeric( $range ) ? $range : false );
$existing_rows[ $key ] = $row;
}
$metric_headers = $response->getMetricHeaders();
$ranges_count = count( $date_ranges );
$multiple_ranges = $ranges_count > 1;
$rows = array();
// Add rows for the current date for each date range.
self::iterate_date_ranges(
$date_ranges,
function ( $date ) use ( &$rows, $existing_rows, $date_ranges, $ranges_count, $metric_headers, $multiple_ranges ) {
for ( $i = 0; $i < $ranges_count; $i++ ) {
$date_range_name = $date_ranges[ $i ]->getName();
if ( empty( $date_range_name ) ) {
$date_range_name = $i;
}
// Copy the existing row if it is available, otherwise create a new zero-value row.
$key = self::get_response_row_key( $date, $i );
$rows[ $key ] = isset( $existing_rows[ $key ] )
? $existing_rows[ $key ]
: $this->create_report_row( $metric_headers, $date, $multiple_ranges ? $date_range_name : false );
}
}
);
// If we have the same number of rows as in the response at the moment, then
// we can return the response without setting the new rows back into the response.
$new_rows_count = count( $rows );
if ( $new_rows_count <= $response->getRowCount() ) {
return $response;
}
// If we have multiple date ranges, we need to sort rows to have them in
// the correct order.
if ( $multiple_ranges ) {
$rows = self::sort_response_rows( $rows, $date_ranges );
}
// Set updated rows back to the response object.
$response->setRows( array_values( $rows ) );
$response->setRowCount( $new_rows_count );
return $response;
}
/**
* Gets the response row key composed from the date and the date range index values.
*
* @since 1.99.0
*
* @param string $date The date of the row to return key for.
* @param int|bool $date_range_index The date range index, or FALSE if no index is available.
* @return string The row key.
*/
protected static function get_response_row_key( $date, $date_range_index ) {
return "{$date}_{$date_range_index}";
}
/**
* Returns sorted and filtered date ranges received in the request params. All corrupted date ranges
* are ignored and not included in the returning list.
*
* @since 1.99.0
*
* @param Data_Request $data Data request object.
* @return Google_Service_AnalyticsData_DateRange[] An array of AnalyticsData DateRange objects.
*/
protected function get_sorted_dateranges( Data_Request $data ) {
$date_ranges = $this->parse_dateranges( $data );
if ( empty( $date_ranges ) ) {
return $date_ranges;
}
// Filter out all corrupted date ranges.
$date_ranges = array_filter(
$date_ranges,
function ( $range ) {
$start = strtotime( $range->getStartDate() );
$end = strtotime( $range->getEndDate() );
return ! empty( $start ) && ! empty( $end );
}
);
// Sort date ranges preserving keys to have the oldest date range at the beginning and
// the latest date range at the end.
uasort(
$date_ranges,
function ( $a, $b ) {
$a_start = strtotime( $a->getStartDate() );
$b_start = strtotime( $b->getStartDate() );
return $a_start - $b_start;
}
);
return $date_ranges;
}
/**
* Sorts response rows using the algorithm similar to the one that Analytics 4 uses internally
* and returns sorted rows.
*
* @since 1.99.0
*
* @param Google_Service_AnalyticsData_Row[] $rows The current report rows.
* @param Google_Service_AnalyticsData_DateRange[] $date_ranges The report date ranges.
* @return Google_Service_AnalyticsData_Row[] Sorted rows.
*/
protected static function sort_response_rows( $rows, $date_ranges ) {
$sorted_rows = array();
$ranges_count = count( $date_ranges );
self::iterate_date_ranges(
$date_ranges,
function ( $date, $range_index ) use ( &$sorted_rows, $ranges_count, $rows ) {
// First take the main date range row.
$key = self::get_response_row_key( $date, $range_index );
$sorted_rows[ $key ] = $rows[ $key ];
// Then take all remaining rows.
for ( $i = 0; $i < $ranges_count; $i++ ) {
if ( $i !== $range_index ) {
$key = self::get_response_row_key( $date, $i );
$sorted_rows[ $key ] = $rows[ $key ];
}
}
}
);
return $sorted_rows;
}
/**
* Iterates over the date ranges and calls callback for each date in each range.
*
* @since 1.99.0
*
* @param Google_Service_AnalyticsData_DateRange[] $date_ranges The report date ranges.
* @param callable $callback The callback to execute for each date.
*/
protected static function iterate_date_ranges( $date_ranges, $callback ) {
foreach ( $date_ranges as $date_range_index => $date_range ) {
$now = strtotime( $date_range->getStartDate() );
$end = strtotime( $date_range->getEndDate() );
do {
call_user_func(
$callback,
gmdate( 'Ymd', $now ),
$date_range_index
);
$now += DAY_IN_SECONDS;
} while ( $now <= $end );
}
}
}

View File

@@ -0,0 +1,70 @@
<?php
/**
* Class Google\Site_Kit\Modules\Analytics_4\Report\Row_Trait
*
* @package Google\Site_Kit\Modules\Analytics_4\Report
* @copyright 2023 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\Report;
use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\DimensionValue as Google_Service_AnalyticsData_DimensionValue;
use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\MetricHeader as Google_Service_AnalyticsData_MetricHeader;
use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\MetricValue as Google_Service_AnalyticsData_MetricValue;
use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\Row as Google_Service_AnalyticsData_Row;
/**
* A trait that adds a helper method to create report rows.
*
* @since 1.99.0
* @access private
* @ignore
*/
trait Row_Trait {
/**
* Creates and returns a new zero-value row for provided date and metrics.
*
* @since 1.99.0
*
* @param Google_Service_AnalyticsData_MetricHeader[] $metric_headers Metric headers from the report response.
* @param string $current_date The current date to create a zero-value row for.
* @param int|bool $date_range_index The date range index for the current date.
* @param string $default_value The default value to use for metric values in the row.
* @return Google_Service_AnalyticsData_Row A new zero-value row instance.
*/
protected function create_report_row( $metric_headers, $current_date, $date_range_index, $default_value = '0' ) {
$dimension_values = array();
$current_date_dimension_value = new Google_Service_AnalyticsData_DimensionValue();
$current_date_dimension_value->setValue( $current_date );
$dimension_values[] = $current_date_dimension_value;
// If we have multiple date ranges, we need to add "date_range_{i}" index to dimension values.
if ( false !== $date_range_index ) {
$date_range_dimension_value = new Google_Service_AnalyticsData_DimensionValue();
$date_range_dimension_value->setValue(
is_numeric( $date_range_index )
? "date_range_{$date_range_index}"
: $date_range_index
);
$dimension_values[] = $date_range_dimension_value;
}
$metric_values = array();
foreach ( $metric_headers as $metric_header ) {
$metric_value = new Google_Service_AnalyticsData_MetricValue();
$metric_value->setValue( $default_value );
$metric_values[] = $metric_value;
}
$row = new Google_Service_AnalyticsData_Row();
$row->setDimensionValues( $dimension_values );
$row->setMetricValues( $metric_values );
return $row;
}
}

View File

@@ -0,0 +1,147 @@
<?php
/**
* Class Google\Site_Kit\Modules\Analytics_4\Reset_Audiences
*
* @package Google\Site_Kit\Modules\Analytics_4
* @copyright 2024 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\Dismissals\Dismissed_Items;
use Google\Site_Kit\Core\Prompts\Dismissed_Prompts;
use Google\Site_Kit\Core\Storage\User_Options;
use Google\Site_Kit\Core\User\Audience_Settings;
use Google\Site_Kit\Modules\Analytics_4;
/**
* Class to reset Audience Segmentation Settings across multiple users.
*
* @since 1.137.0
* @access private
* @ignore
*/
class Reset_Audiences {
/**
* User_Options instance.
*
* @since 1.137.0
* @var User_Options
*/
protected $user_options;
/**
* Dismissed_Prompts instance.
*
* @since 1.137.0
* @var Dismissed_Prompts
*/
protected $dismissed_prompts;
/**
* Dismissed_Items instance.
*
* @since 1.137.0
* @var Dismissed_Items
*/
protected $dismissed_items;
/**
* Audience Settings instance.
*
* @since 1.137.0
* @var Audience_Settings
*/
protected $audience_settings;
const AUDIENCE_SEGMENTATION_DISMISSED_PROMPTS = array( 'audience_segmentation_setup_cta-notification' );
const AUDIENCE_SEGMENTATION_DISMISSED_ITEMS = array(
'audience-segmentation-add-group-notice',
'setup-success-notification-audiences',
'settings_visitor_groups_setup_success_notification',
'audience-segmentation-no-audiences-banner',
'audience-tile-*',
);
/**
* Constructor.
*
* @since 1.137.0
*
* @param User_Options $user_options User option API.
*/
public function __construct( ?User_Options $user_options = null ) {
$this->user_options = $user_options;
$this->dismissed_prompts = new Dismissed_Prompts( $this->user_options );
$this->dismissed_items = new Dismissed_Items( $this->user_options );
$this->audience_settings = new Audience_Settings( $this->user_options );
}
/**
* Reset audience specific settings for all SK users.
*
* @since 1.137.0
*/
public function reset_audience_data() {
global $wpdb;
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
$users = $wpdb->get_col(
$wpdb->prepare(
"SELECT DISTINCT user_id
FROM $wpdb->usermeta
WHERE meta_key IN (%s, %s)
LIMIT 100 -- Arbitrary limit to avoid unbounded user iteration.",
$this->user_options->get_meta_key( Dismissed_Items::OPTION ),
$this->user_options->get_meta_key( Dismissed_Prompts::OPTION ),
)
);
if ( $users ) {
$backup_user_id = $this->user_options->get_user_id();
foreach ( $users as $user_id ) {
$this->user_options->switch_user( $user_id );
// Remove Audience Segmentation specific dismissed prompts.
foreach ( self::AUDIENCE_SEGMENTATION_DISMISSED_PROMPTS as $prompt ) {
$this->dismissed_prompts->remove( $prompt );
}
// Remove Audience Segmentation specific dismissed items.
foreach ( self::AUDIENCE_SEGMENTATION_DISMISSED_ITEMS as $item ) {
// Support wildcard matches, in order to delete all dismissed items prefixed with audience-tile-*.
if ( strpos( $item, '*' ) !== false ) {
$dismissed_items = $this->dismissed_items->get();
foreach ( array_keys( $dismissed_items ) as $existing_item ) {
if ( str_starts_with( $existing_item, rtrim( $item, '*' ) ) ) {
$this->dismissed_items->remove( $existing_item );
}
}
} else {
// For non-wildcard items, remove them directly.
$this->dismissed_items->remove( $item );
}
}
// Reset the user's audience settings.
if ( $this->audience_settings->has() ) {
$this->audience_settings->merge(
array(
'configuredAudiences' => null,
'didSetAudiences' => false,
),
);
}
}
// Restore original user.
$this->user_options->switch_user( $backup_user_id );
}
}
}

View File

@@ -0,0 +1,266 @@
<?php
/**
* Class Google\Site_Kit\Modules\Analytics_4\Resource_Data_Availability_Date
*
* @package Google\Site_Kit\Modules\Analytics_4
* @copyright 2024 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\Options_Interface;
use Google\Site_Kit\Core\Storage\Transients;
use Google\Site_Kit\Core\Util\Feature_Flags;
use Google\Site_Kit\Modules\Analytics_4\Audience_Settings;
/**
* Class for managing Analytics 4 resource data availability date.
*
* @since 1.127.0
* @access private
* @ignore
*/
class Resource_Data_Availability_Date {
/**
* List of valid custom dimension slugs.
*
* @since 1.127.0
* @var array
*/
const CUSTOM_DIMENSION_SLUGS = array(
'googlesitekit_post_type',
);
const RESOURCE_TYPE_AUDIENCE = 'audience';
const RESOURCE_TYPE_CUSTOM_DIMENSION = 'customDimension';
const RESOURCE_TYPE_PROPERTY = 'property';
/**
* Transients instance.
*
* @since 1.127.0
* @var Transients
*/
protected $transients;
/**
* Module settings.
*
* @since 1.127.0
* @var Module_Settings
*/
protected $settings;
/**
* Options instance.
*
* @since 1.151.0
* @var Audience_Settings
*/
protected $audience_settings;
/**
* Constructor.
*
* @since 1.127.0
*
* @param Transients $transients Transients instance.
* @param Module_Settings $settings Module settings instance.
* @param Audience_Settings $audience_settings Audience_Settings instance.
*/
public function __construct( Transients $transients, Module_Settings $settings, Audience_Settings $audience_settings ) {
$this->transients = $transients;
$this->settings = $settings;
$this->audience_settings = $audience_settings;
}
/**
* Gets the data availability date for the given resource.
*
* @since 1.127.0
*
* @param string $resource_slug Resource slug.
* @param string $resource_type Resource type.
* @return int Data availability date in YYYYMMDD format on success, 0 otherwise.
*/
public function get_resource_date( $resource_slug, $resource_type ) {
return (int) $this->transients->get( $this->get_resource_transient_name( $resource_slug, $resource_type ) );
}
/**
* Sets the data availability date for the given resource.
*
* @since 1.127.0
*
* @param string $resource_slug Resource slug.
* @param string $resource_type Resource type.
* @param int $date Data availability date.
* @return bool True on success, false otherwise.
*/
public function set_resource_date( $resource_slug, $resource_type, $date ) {
return $this->transients->set( $this->get_resource_transient_name( $resource_slug, $resource_type ), $date );
}
/**
* Resets the data availability date for the given resource.
*
* @since 1.127.0
*
* @param string $resource_slug Resource slug.
* @param string $resource_type Resource type.
* @return bool True on success, false otherwise.
*/
public function reset_resource_date( $resource_slug, $resource_type ) {
return $this->transients->delete( $this->get_resource_transient_name( $resource_slug, $resource_type ) );
}
/**
* Gets data availability dates for all resources.
*
* @since 1.127.0
*
* @return array Associative array of resource names and their data availability date.
*/
public function get_all_resource_dates() {
$property_id = $this->get_property_id();
$available_audiences = $this->get_available_audience_resource_names();
return array_map(
// Filter out falsy values (0) from every resource's data availability dates.
fn( $data_availability_dates ) => array_filter( $data_availability_dates ),
array(
// Get data availability dates for the available audiences.
self::RESOURCE_TYPE_AUDIENCE => array_reduce(
$available_audiences,
function ( $audience_data_availability_dates, $audience ) {
$audience_data_availability_dates[ $audience ] = $this->get_resource_date( $audience, self::RESOURCE_TYPE_AUDIENCE );
return $audience_data_availability_dates;
},
array()
),
// Get data availability dates for the custom dimensions.
self::RESOURCE_TYPE_CUSTOM_DIMENSION => array_reduce(
self::CUSTOM_DIMENSION_SLUGS,
function ( $custom_dimension_data_availability_dates, $custom_dimension ) {
$custom_dimension_data_availability_dates[ $custom_dimension ] = $this->get_resource_date( $custom_dimension, self::RESOURCE_TYPE_CUSTOM_DIMENSION );
return $custom_dimension_data_availability_dates;
},
array()
),
// Get data availability date for the current property.
self::RESOURCE_TYPE_PROPERTY => array(
$property_id => $this->get_resource_date(
$property_id,
self::RESOURCE_TYPE_PROPERTY
),
),
)
);
}
/**
* Resets the data availability date for all resources.
*
* @since 1.127.0
*
* @param array/null $available_audience_names Optional. List of available audience resource names. If not provided, it will be fetched from settings.
* @param string/null $property_id Optional. Property ID. If not provided, it will be fetched from settings.
*/
public function reset_all_resource_dates( $available_audience_names = null, $property_id = null ) {
foreach ( self::CUSTOM_DIMENSION_SLUGS as $custom_dimension ) {
$this->reset_resource_date( $custom_dimension, self::RESOURCE_TYPE_CUSTOM_DIMENSION );
}
$available_audience_names = $available_audience_names ?: $this->get_available_audience_resource_names();
foreach ( $available_audience_names as $audience_name ) {
$this->reset_resource_date( $audience_name, self::RESOURCE_TYPE_AUDIENCE );
}
$property_id = $property_id ?: $this->get_property_id();
$this->reset_resource_date( $property_id, self::RESOURCE_TYPE_PROPERTY );
}
/**
* Checks whether the given resource type is valid.
*
* @since 1.127.0
*
* @param string $resource_type Resource type.
* @return bool True if valid, false otherwise.
*/
public function is_valid_resource_type( $resource_type ) {
return in_array( $resource_type, array( self::RESOURCE_TYPE_AUDIENCE, self::RESOURCE_TYPE_CUSTOM_DIMENSION, self::RESOURCE_TYPE_PROPERTY ), true );
}
/**
* Checks whether the given resource slug is valid.
*
* @since 1.127.0
*
* @param string $resource_slug Resource slug.
* @param string $resource_type Resource type.
* @return bool True if valid, false otherwise.
*/
public function is_valid_resource_slug( $resource_slug, $resource_type ) {
switch ( $resource_type ) {
case self::RESOURCE_TYPE_AUDIENCE:
return in_array( $resource_slug, $this->get_available_audience_resource_names(), true );
case self::RESOURCE_TYPE_CUSTOM_DIMENSION:
return in_array( $resource_slug, self::CUSTOM_DIMENSION_SLUGS, true );
case self::RESOURCE_TYPE_PROPERTY:
return $resource_slug === $this->get_property_id();
default:
return false;
}
}
/**
* Gets data available date transient name for the given resource.
*
* @since 1.127.0
*
* @param string $resource_slug Resource slug.
* @param string $resource_type Resource type.
* @return string Data available date transient name.
*/
protected function get_resource_transient_name( $resource_slug, $resource_type ) {
return "googlesitekit_{$resource_type}_{$resource_slug}_data_availability_date";
}
/**
* Gets available audience resource names.
*
* @since 1.127.0
*
* @return array List of available audience resource names.
*/
private function get_available_audience_resource_names() {
$available_audiences = $this->audience_settings->get();
$available_audiences = $available_audiences['availableAudiences'] ?? array();
return array_map(
function ( $audience ) {
return $audience['name'];
},
$available_audiences
);
}
/**
* Gets the property ID from settings instance.
*
* @since 1.127.0
*
* @return string Property ID.
*/
private function get_property_id() {
return $this->settings->get()['propertyID'];
}
}

View File

@@ -0,0 +1,216 @@
<?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
*/
// phpcs:disable Generic.Metrics.CyclomaticComplexity.MaxExceeded
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;
use Google\Site_Kit\Core\Storage\Setting_With_ViewOnly_Keys_Interface;
use Google\Site_Kit\Core\Util\Method_Proxy_Trait;
/**
* Class for Analytics 4 settings.
*
* @since 1.30.0
* @access private
* @ignore
*/
class Settings extends Module_Settings implements Setting_With_Owned_Keys_Interface, Setting_With_ViewOnly_Keys_Interface {
use Setting_With_Owned_Keys_Trait;
use Method_Proxy_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(
'accountID',
'propertyID',
'webDataStreamID',
'measurementID',
'googleTagID',
'googleTagAccountID',
'googleTagContainerID',
);
}
/**
* Returns keys for view-only settings.
*
* @since 1.113.0
*
* @return array An array of keys for view-only settings.
*/
public function get_view_only_keys() {
return array(
'availableCustomDimensions',
'adSenseLinked',
'detectedEvents',
'newConversionEventsLastUpdateAt',
'lostConversionEventsLastUpdateAt',
);
}
/**
* Gets the default value.
*
* @since 1.30.0
*
* @return array
*/
protected function get_default() {
return array(
'ownerID' => 0,
'accountID' => '',
'adsConversionID' => '',
'propertyID' => '',
'webDataStreamID' => '',
'measurementID' => '',
'trackingDisabled' => array( 'loggedinUsers' ),
'useSnippet' => true,
'googleTagID' => '',
'googleTagAccountID' => '',
'googleTagContainerID' => '',
'googleTagContainerDestinationIDs' => null,
'googleTagLastSyncedAtMs' => 0,
'availableCustomDimensions' => null,
'propertyCreateTime' => 0,
'adSenseLinked' => false,
'adSenseLinkedLastSyncedAt' => 0,
'adsConversionIDMigratedAtMs' => 0,
'adsLinked' => false,
'adsLinkedLastSyncedAt' => 0,
'detectedEvents' => array(),
'newConversionEventsLastUpdateAt' => 0,
'lostConversionEventsLastUpdateAt' => 0,
);
}
/**
* Gets the callback for sanitizing the setting's value before saving.
*
* @since 1.30.0
*
* @return callable|null
*/
protected function get_sanitize_callback() {
return function ( $option ) {
if ( is_array( $option ) ) {
if ( isset( $option['useSnippet'] ) ) {
$option['useSnippet'] = (bool) $option['useSnippet'];
}
if ( isset( $option['googleTagID'] ) ) {
if ( ! preg_match( '/^(G|GT|AW)-[a-zA-Z0-9]+$/', $option['googleTagID'] ) ) {
$option['googleTagID'] = '';
}
}
if ( isset( $option['trackingDisabled'] ) ) {
// Prevent other options from being saved if 'loggedinUsers' is selected.
if ( in_array( 'loggedinUsers', $option['trackingDisabled'], true ) ) {
$option['trackingDisabled'] = array( 'loggedinUsers' );
} else {
$option['trackingDisabled'] = (array) $option['trackingDisabled'];
}
}
$numeric_properties = array( 'googleTagAccountID', 'googleTagContainerID' );
foreach ( $numeric_properties as $numeric_property ) {
if ( isset( $option[ $numeric_property ] ) ) {
if ( ! is_numeric( $option[ $numeric_property ] ) || ! $option[ $numeric_property ] > 0 ) {
$option[ $numeric_property ] = '';
}
}
}
if ( isset( $option['googleTagContainerDestinationIDs'] ) ) {
if ( ! is_array( $option['googleTagContainerDestinationIDs'] ) ) {
$option['googleTagContainerDestinationIDs'] = null;
}
}
if ( isset( $option['availableCustomDimensions'] ) ) {
if ( is_array( $option['availableCustomDimensions'] ) ) {
$valid_dimensions = array_filter(
$option['availableCustomDimensions'],
function ( $dimension ) {
return is_string( $dimension ) && strpos( $dimension, 'googlesitekit_' ) === 0;
}
);
$option['availableCustomDimensions'] = array_values( $valid_dimensions );
} else {
$option['availableCustomDimensions'] = null;
}
}
if ( isset( $option['adSenseLinked'] ) ) {
$option['adSenseLinked'] = (bool) $option['adSenseLinked'];
}
if ( isset( $option['adSenseLinkedLastSyncedAt'] ) ) {
if ( ! is_int( $option['adSenseLinkedLastSyncedAt'] ) ) {
$option['adSenseLinkedLastSyncedAt'] = 0;
}
}
if ( isset( $option['adsConversionIDMigratedAtMs'] ) ) {
if ( ! is_int( $option['adsConversionIDMigratedAtMs'] ) ) {
$option['adsConversionIDMigratedAtMs'] = 0;
}
}
if ( isset( $option['adsLinked'] ) ) {
$option['adsLinked'] = (bool) $option['adsLinked'];
}
if ( isset( $option['adsLinkedLastSyncedAt'] ) ) {
if ( ! is_int( $option['adsLinkedLastSyncedAt'] ) ) {
$option['adsLinkedLastSyncedAt'] = 0;
}
}
if ( isset( $option['newConversionEventsLastUpdateAt'] ) ) {
if ( ! is_int( $option['newConversionEventsLastUpdateAt'] ) ) {
$option['newConversionEventsLastUpdateAt'] = 0;
}
}
if ( isset( $option['lostConversionEventsLastUpdateAt'] ) ) {
if ( ! is_int( $option['lostConversionEventsLastUpdateAt'] ) ) {
$option['lostConversionEventsLastUpdateAt'] = 0;
}
}
}
return $option;
};
}
}

View File

@@ -0,0 +1,161 @@
<?php
/**
* Class Google\Site_Kit\Modules\Analytics_4\Synchronize_AdSenseLinked
*
* @package Google\Site_Kit\Modules\Analytics_4
* @copyright 2024 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\Permissions\Permissions;
use Google\Site_Kit\Core\Storage\Options;
use Google\Site_Kit\Core\Storage\User_Options;
use Google\Site_Kit\Modules\Adsense;
use Google\Site_Kit\Modules\Analytics_4;
use Google\Site_Kit\Modules\AdSense\Settings as Adsense_Settings;
/**
* The base class for Synchronizing the adSenseLinked status.
*
* @since 1.123.0
* @access private
* @ignore
*/
class Synchronize_AdSenseLinked {
const CRON_SYNCHRONIZE_ADSENSE_LINKED = 'googlesitekit_cron_synchronize_adsense_linked_data';
/**
* Analytics_4 instance.
*
* @since 1.123.0
* @var Analytics_4
*/
protected $analytics_4;
/**
* User_Options instance.
*
* @since 1.123.0
* @var User_Options
*/
protected $user_options;
/**
* Options instance.
*
* @since 1.123.0
* @var Options
*/
protected $options;
/**
* Constructor.
*
* @since 1.123.0
*
* @param Analytics_4 $analytics_4 Analytics 4 instance.
* @param User_Options $user_options User_Options instance.
* @param Options $options Options instance.
*/
public function __construct( Analytics_4 $analytics_4, User_Options $user_options, Options $options ) {
$this->analytics_4 = $analytics_4;
$this->user_options = $user_options;
$this->options = $options;
}
/**
* Registers functionality through WordPress hooks.
*
* @since 1.123.0
*/
public function register() {
add_action(
self::CRON_SYNCHRONIZE_ADSENSE_LINKED,
function () {
$this->synchronize_adsense_linked_data();
}
);
}
/**
* Cron callback for synchronizing the adsense linked data.
*
* @since 1.123.0
* @since 1.130.0 Added check for property ID, so it can return early if property ID is not set.
*/
protected function synchronize_adsense_linked_data() {
$owner_id = $this->analytics_4->get_owner_id();
$restore_user = $this->user_options->switch_user( $owner_id );
$settings_ga4 = $this->analytics_4->get_settings()->get();
if ( empty( $settings_ga4['propertyID'] ) ) {
return;
}
if ( user_can( $owner_id, Permissions::VIEW_AUTHENTICATED_DASHBOARD ) ) {
$this->synchronize_adsense_linked_status();
}
$restore_user();
}
/**
* Schedules single cron which will synchronize the adSenseLinked status.
*
* @since 1.123.0
*/
public function maybe_schedule_synchronize_adsense_linked() {
$analytics_4_connected = apply_filters( 'googlesitekit_is_module_connected', false, Analytics_4::MODULE_SLUG );
$adsense_connected = apply_filters( 'googlesitekit_is_module_connected', false, AdSense::MODULE_SLUG );
$cron_already_scheduled = wp_next_scheduled( self::CRON_SYNCHRONIZE_ADSENSE_LINKED );
if ( $analytics_4_connected && $adsense_connected && ! $cron_already_scheduled ) {
wp_schedule_single_event(
// Schedule the task to run in 24 hours.
time() + ( DAY_IN_SECONDS ),
self::CRON_SYNCHRONIZE_ADSENSE_LINKED
);
}
}
/**
* Synchronize the AdSenseLinked status.
*
* @since 1.123.0
*
* @return null
*/
protected function synchronize_adsense_linked_status() {
$settings_ga4 = $this->analytics_4->get_settings()->get();
$property_id = $settings_ga4['propertyID'];
$property_adsense_links = $this->analytics_4->get_data( 'adsense-links', array( 'propertyID' => $property_id ) );
$current_adsense_options = ( new AdSense_Settings( $this->options ) )->get();
$current_adsense_client_id = ! empty( $current_adsense_options['clientID'] ) ? $current_adsense_options['clientID'] : '';
if ( is_wp_error( $property_adsense_links ) || empty( $property_adsense_links ) ) {
return null;
}
$found_adsense_linked_for_client_id = false;
// Iterate over returned AdSense links and set true if one is found
// matching the same client ID.
foreach ( $property_adsense_links as $property_adsense_link ) {
if ( $current_adsense_client_id === $property_adsense_link['adClientCode'] ) {
$found_adsense_linked_for_client_id = true;
break;
}
}
// Update the AdSenseLinked status and timestamp.
$this->analytics_4->get_settings()->merge(
array(
'adSenseLinked' => $found_adsense_linked_for_client_id,
'adSenseLinkedLastSyncedAt' => time(),
)
);
}
}

View File

@@ -0,0 +1,137 @@
<?php
/**
* Class Google\Site_Kit\Modules\Analytics_4\Synchronize_AdsLinked
*
* @package Google\Site_Kit\Modules\Analytics_4
* @copyright 2024 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\Permissions\Permissions;
use Google\Site_Kit\Core\Storage\User_Options;
use Google\Site_Kit\Modules\Ads;
use Google\Site_Kit\Modules\Analytics_4;
/**
* The base class for Synchronizing the adsLinked status.
*
* @since 1.124.0
* @access private
* @ignore
*/
class Synchronize_AdsLinked {
const CRON_SYNCHRONIZE_ADS_LINKED = 'googlesitekit_cron_synchronize_ads_linked_data';
/**
* Analytics_4 instance.
*
* @since 1.124.0
* @var Analytics_4
*/
protected $analytics_4;
/**
* User_Options instance.
*
* @since 1.124.0
* @var User_Options
*/
protected $user_options;
/**
* Constructor.
*
* @since 1.124.0
*
* @param Analytics_4 $analytics_4 Analytics 4 instance.
* @param User_Options $user_options User_Options instance.
*/
public function __construct( Analytics_4 $analytics_4, User_Options $user_options ) {
$this->analytics_4 = $analytics_4;
$this->user_options = $user_options;
}
/**
* Registers functionality through WordPress hooks.
*
* @since 1.124.0
*/
public function register() {
add_action(
self::CRON_SYNCHRONIZE_ADS_LINKED,
function () {
$this->synchronize_ads_linked_data();
}
);
}
/**
* Cron callback for synchronizing the ads linked data.
*
* @since 1.124.0
*/
protected function synchronize_ads_linked_data() {
$ads_connected = apply_filters( 'googlesitekit_is_module_connected', false, Ads::MODULE_SLUG );
if ( $ads_connected ) {
return;
}
$owner_id = $this->analytics_4->get_owner_id();
$restore_user = $this->user_options->switch_user( $owner_id );
if ( user_can( $owner_id, Permissions::VIEW_AUTHENTICATED_DASHBOARD ) ) {
$this->synchronize_ads_linked_status();
}
$restore_user();
}
/**
* Synchronize the adsLinked status.
*
* @since 1.124.0
*
* @return null
*/
protected function synchronize_ads_linked_status() {
$settings_ga4 = $this->analytics_4->get_settings()->get();
$property_id = $settings_ga4['propertyID'];
$property_ads_links = $this->analytics_4->get_data(
'ads-links',
array( 'propertyID' => $property_id )
);
if ( is_wp_error( $property_ads_links ) || ! is_array( $property_ads_links ) ) {
return null;
}
// Update the adsLinked status and timestamp.
$this->analytics_4->get_settings()->merge(
array(
'adsLinked' => ! empty( $property_ads_links ),
'adsLinkedLastSyncedAt' => time(),
)
);
}
/**
* Schedules single cron which will synchronize the adsLinked status.
*
* @since 1.124.0
*/
public function maybe_schedule_synchronize_ads_linked() {
$analytics_4_connected = $this->analytics_4->is_connected();
$cron_already_scheduled = wp_next_scheduled( self::CRON_SYNCHRONIZE_ADS_LINKED );
if ( $analytics_4_connected && ! $cron_already_scheduled ) {
wp_schedule_single_event(
time() + ( WEEK_IN_SECONDS ),
self::CRON_SYNCHRONIZE_ADS_LINKED
);
}
}
}

View File

@@ -0,0 +1,171 @@
<?php
/**
* Class Google\Site_Kit\Modules\Analytics_4\Synchronize_Property
*
* @package Google\Site_Kit\Modules\Analytics_4
* @copyright 2023 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\Permissions\Permissions;
use Google\Site_Kit\Core\Storage\User_Options;
use Google\Site_Kit\Modules\Analytics_4;
use Google\Site_Kit_Dependencies\Google\Service\GoogleAnalyticsAdmin\GoogleAnalyticsAdminV1betaProperty;
/**
* The base class for Synchronizing the Analytics 4 property.
*
* @since 1.116.0
* @access private
* @ignore
*/
class Synchronize_Property {
const CRON_SYNCHRONIZE_PROPERTY = 'googlesitekit_cron_synchronize_property_data';
/**
* Analytics_4 instance.
*
* @since 1.116.0
* @var Analytics_4
*/
protected $analytics_4;
/**
* User_Options instance.
*
* @since 1.116.0
* @var User_Options
*/
protected $user_options;
/**
* Constructor.
*
* @since 1.116.0
*
* @param Analytics_4 $analytics_4 Analytics 4 instance.
* @param User_Options $user_options User_Options instance.
*/
public function __construct( Analytics_4 $analytics_4, User_Options $user_options ) {
$this->analytics_4 = $analytics_4;
$this->user_options = $user_options;
}
/**
* Registers functionality through WordPress hooks.
*
* @since 1.116.0
*/
public function register() {
add_action(
self::CRON_SYNCHRONIZE_PROPERTY,
function () {
$this->synchronize_property_data();
}
);
}
/**
* Cron callback for synchronizing the property.
*
* @since 1.116.0
*/
protected function synchronize_property_data() {
$owner_id = $this->analytics_4->get_owner_id();
$restore_user = $this->user_options->switch_user( $owner_id );
if ( user_can( $owner_id, Permissions::VIEW_AUTHENTICATED_DASHBOARD ) ) {
$property = $this->retrieve_property();
$this->synchronize_property_create_time( $property );
}
$restore_user();
}
/**
* Schedules single cron which will synchronize the property data.
*
* @since 1.116.0
*/
public function maybe_schedule_synchronize_property() {
$settings = $this->analytics_4->get_settings()->get();
$create_time_has_value = (bool) $settings['propertyCreateTime'];
$analytics_4_connected = $this->analytics_4->is_connected();
$cron_already_scheduled = wp_next_scheduled( self::CRON_SYNCHRONIZE_PROPERTY );
if ( ! $create_time_has_value && $analytics_4_connected && ! $cron_already_scheduled ) {
wp_schedule_single_event(
// Schedule the task to run in 30 minutes.
time() + ( 30 * MINUTE_IN_SECONDS ),
self::CRON_SYNCHRONIZE_PROPERTY
);
}
}
/**
* Retrieve the Analytics 4 property.
*
* @since 1.116.0
*
* @return GoogleAnalyticsAdminV1betaProperty|null $property Analytics 4 property object, or null if property is not found.
*/
protected function retrieve_property() {
$settings = $this->analytics_4->get_settings()->get();
$property_id = $settings['propertyID'];
$has_property_access = $this->analytics_4->has_property_access( $property_id );
if ( is_wp_error( $has_property_access ) || ! $has_property_access ) {
return null;
}
$property = $this->analytics_4->get_data( 'property', array( 'propertyID' => $property_id ) );
if ( is_wp_error( $property ) ) {
return null;
}
return $property;
}
/**
* Synchronize the property create time data.
*
* @since 1.116.0
*
* @param GoogleAnalyticsAdminV1betaProperty|null $property Analytics 4 property object, or null if property is not found.
*/
protected function synchronize_property_create_time( $property ) {
if ( ! $property ) {
return;
}
$create_time_ms = self::convert_time_to_unix_ms( $property->createTime ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
$this->analytics_4->get_settings()->merge(
array(
'propertyCreateTime' => $create_time_ms,
)
);
}
/**
* Convert to Unix timestamp and then to milliseconds.
*
* @since 1.116.0
*
* @param string $date_time Date in date-time format.
*/
public static function convert_time_to_unix_ms( $date_time ) {
$date_time_object = new \DateTime( $date_time, new \DateTimeZone( 'UTC' ) );
return (int) ( $date_time_object->getTimestamp() * 1000 );
}
}

View File

@@ -0,0 +1,35 @@
<?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,29 @@
<?php
/**
* Class Google\Site_Kit\Modules\Analytics_4\Tag_Interface
*
* @package Google\Site_Kit\Modules\Analytics_4
* @copyright 2023 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;
/**
* Interface for an Analytics 4 tag.
*
* @since 1.113.0
* @access private
* @ignore
*/
interface Tag_Interface {
/**
* Sets custom dimensions data.
*
* @since 1.113.0
*
* @param string $custom_dimensions Custom dimensions data.
*/
public function set_custom_dimensions( $custom_dimensions );
}

View File

@@ -0,0 +1,55 @@
<?php
/**
* Class Google\Site_Kit\Core\Modules\Analytics_4\Tag_Matchers
*
* @package Google\Site_Kit\Core\Modules\Analytics_4
* @copyright 2024 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Core\Modules\Analytics_4;
use Google\Site_Kit\Core\Modules\Tags\Module_Tag_Matchers;
use Google\Site_Kit\Core\Tags\Tag_Matchers_Interface;
/**
* Class for Tag matchers.
*
* @since 1.119.0
* @access private
* @ignore
*/
class Tag_Matchers extends Module_Tag_Matchers implements Tag_Matchers_Interface {
/**
* Holds array of regex tag matchers.
*
* @since 1.119.0
*
* @return array Array of regex matchers.
*/
public function regex_matchers() {
$tag_matchers = array(
"/__gaTracker\s*\(\s*['|\"]create['|\"]\s*,\s*['|\"](G-[a-zA-Z0-9]+)['|\"]\, ?['|\"]auto['|\"]\s*\)/i",
"/_gaq\.push\s*\(\s*\[\s*['|\"][^_]*_setAccount['|\"]\s*,\s*['|\"](G-[a-zA-Z0-9]+)['|\"]\s*],?\s*\)/i",
'/<amp-analytics\s+[^>]*type="gtag"[^>]*>[^<]*<script\s+type="application\/json">[^<]*"gtag_id"\s*:\s*"(G-[a-zA-Z0-9]+)"/i',
'/<amp-analytics\s+[^>]*type="googleanalytics"[^>]*>[^<]*<script\s+type="application\/json">[^<]*"account"\s*:\s*"(G-[a-zA-Z0-9]+)"/i',
);
$subdomains = array( '', 'www\\.' );
foreach ( $subdomains as $subdomain ) {
$tag_matchers[] = "/<script\\s+[^>]*src=['|\"]https?:\\/\\/" . $subdomain . "googletagmanager\\.com\\/gtag\\/js\\?id=(G-[a-zA-Z0-9]+)['|\"][^>]*><\\/script>/i";
$tag_matchers[] = "/<script\\s+[^>]*src=['|\"]https?:\/\/" . $subdomain . "googletagmanager\\.com\\/gtag\\/js\\?id=(G-[a-zA-Z0-9]+)['|\"][^\\/]*\/>/i";
}
$funcs = array( '__gaTracker', 'ga', 'gtag' );
foreach ( $funcs as $func ) {
$tag_matchers[] = "/$func\\s*\\(\\s*['|\"]create['|\"]\\s*,\\s*['|\"](G-[a-zA-Z0-9]+)['|\"]\\,\\s*['|\"]auto['|\"]\\s*\\)/i";
$tag_matchers[] = "/$func\\s*\\(\\s*['|\"]config['|\"]\\s*,\\s*['|\"](G-[a-zA-Z0-9]+)['|\"]\\s*\\)/i";
$tag_matchers[] = "/$func\\s*\\(\\s*['|\"]config['|\"]\\s*,\\s*['|\"](GT-[a-zA-Z0-9]+)['|\"]\\s*\\)/i";
}
return $tag_matchers;
}
}

View File

@@ -0,0 +1,197 @@
<?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\Modules\Tags\Module_Web_Tag;
use Google\Site_Kit\Core\Tags\GTag;
use Google\Site_Kit\Core\Tags\Tag_With_Linker_Trait;
use Google\Site_Kit\Core\Util\Method_Proxy_Trait;
use Google\Site_Kit\Core\Tags\Tag_With_Linker_Interface;
/**
* Class for Web tag.
*
* @since 1.31.0
* @access private
* @ignore
*/
class Web_Tag extends Module_Web_Tag implements Tag_Interface, Tag_With_Linker_Interface {
use Method_Proxy_Trait;
use Tag_With_Linker_Trait;
/**
* Custom dimensions data.
*
* @since 1.113.0
* @var array
*/
private $custom_dimensions;
/**
* Sets custom dimensions data.
*
* @since 1.113.0
*
* @param string $custom_dimensions Custom dimensions data.
*/
public function set_custom_dimensions( $custom_dimensions ) {
$this->custom_dimensions = $custom_dimensions;
}
/**
* Sets the current home domain.
*
* @since 1.24.0
*
* @param string $domain Domain name.
*/
public function set_home_domain( $domain ) {
$this->home_domain = $domain;
}
/**
* Gets args to use if blocked_on_consent is deprecated.
*
* @since 1.122.0
*
* @return array args to pass to apply_filters_deprecated if deprecated ($version, $replacement, $message)
*/
protected function get_tag_blocked_on_consent_deprecated_args() {
return array(
'1.122.0', // Deprecated in this version.
'',
__( 'Please use the consent mode feature instead.', 'google-site-kit' ),
);
}
/**
* Registers tag hooks.
*
* @since 1.31.0
*/
public function register() {
add_action( 'googlesitekit_setup_gtag', $this->get_method_proxy( 'setup_gtag' ) );
add_filter( 'script_loader_tag', $this->get_method_proxy( 'filter_tag_output' ), 10, 2 );
$this->do_init_tag_action();
}
/**
* Outputs gtag snippet.
*
* @since 1.24.0
*/
protected function render() {
// Do nothing, gtag script is enqueued.
}
/**
* Configures gtag script.
*
* @since 1.24.0
* @since 1.124.0 Renamed and refactored to use new GTag infrastructure.
*
* @param GTag $gtag GTag instance.
*/
protected function setup_gtag( GTag $gtag ) {
$gtag_opt = $this->get_tag_config();
/**
* 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'] ) ) {
$gtag->add_command( 'set', array( 'linker', $gtag_opt['linker'] ) );
unset( $gtag_opt['linker'] );
}
$gtag->add_tag( $this->tag_id, $gtag_opt );
}
/**
* Filters output of tag HTML.
*
* @param string $tag Tag HTML.
* @param string $handle WP script handle of given tag.
* @return string
*/
protected function filter_tag_output( $tag, $handle ) {
// The tag will either have its own handle or use the common GTag handle, not both.
if ( GTag::get_handle_for_tag( $this->tag_id ) !== $handle && GTag::HANDLE !== $handle ) {
return $tag;
}
// Retain this comment for detection of Site Kit placed tag.
$snippet_comment = sprintf( "<!-- %s -->\n", esc_html__( 'Google Analytics snippet added by Site Kit', 'google-site-kit' ) );
$block_on_consent_attrs = $this->get_tag_blocked_on_consent_attribute();
if ( $block_on_consent_attrs ) {
$tag = $this->add_legacy_block_on_consent_attributes( $tag, $block_on_consent_attrs );
}
return $snippet_comment . $tag;
}
/**
* Gets the tag config as used in the gtag data vars.
*
* @since 1.113.0
*
* @return array Tag configuration.
*/
protected function get_tag_config() {
$config = array();
if ( ! empty( $this->custom_dimensions ) ) {
$config = array_merge( $config, $this->custom_dimensions );
}
return $this->add_linker_to_tag_config( $config );
}
/**
* Adds HTML attributes to the gtag script tag to block it until user consent is granted.
*
* This mechanism for blocking the tag is deprecated and the consent mode feature should be used instead.
*
* @since 1.122.0
* @since 1.158.0 Remove src from signature & replacement.
*
* @param string $tag The script tag.
* @param string $block_on_consent_attrs The attributes to add to the script tag to block it until user consent is granted.
*
* @return string The script tag with the added attributes.
*/
protected function add_legacy_block_on_consent_attributes( $tag, $block_on_consent_attrs ) {
return str_replace(
array(
'<script src=', // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript
"<script type='text/javascript' src=", // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript
'<script type="text/javascript" src=', // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript
),
// `type` attribute intentionally excluded in replacements.
"<script{$block_on_consent_attrs} src=", // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript
$tag
);
}
}

View File

@@ -0,0 +1,229 @@
<?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\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;
use Module_With_Assets_Trait;
use Module_With_Settings_Trait;
use 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' => true,
),
);
}
/**
* 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-notifications',
'googlesitekit-datastore-site',
'googlesitekit-components',
),
)
),
);
}
/**
* Sets up information about the module.
*
* @since 1.0.0
*
* @return array Associative array of module info.
*/
protected function setup_info() {
return array(
'slug' => 'pagespeed-insights',
'name' => _x( 'PageSpeed Insights', 'Service name', 'google-site-kit' ),
'description' => __( 'Google PageSpeed Insights gives you metrics about performance, accessibility, SEO and PWA', 'google-site-kit' ),
'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,825 @@
<?php
/**
* Class Google\Site_Kit\Modules\Reader_Revenue_Manager
*
* @package Google\Site_Kit
* @copyright 2024 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\Assets;
use Google\Site_Kit\Core\Assets\Script;
use Google\Site_Kit\Core\Assets\Stylesheet;
use Google\Site_Kit\Core\Authentication\Authentication;
use Google\Site_Kit\Core\Authentication\Clients\Google_Site_Kit_Client;
use Google\Site_Kit\Core\Dismissals\Dismissed_Items;
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_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_Service_Entity;
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_Tag;
use Google\Site_Kit\Core\Modules\Module_With_Tag_Trait;
use Google\Site_Kit\Core\Permissions\Permissions;
use Google\Site_Kit\Core\REST_API\Data_Request;
use Google\Site_Kit\Core\REST_API\Exception\Missing_Required_Param_Exception;
use Google\Site_Kit\Core\Site_Health\Debug_Data;
use Google\Site_Kit\Core\Storage\Options;
use Google\Site_Kit\Core\Storage\Post_Meta;
use Google\Site_Kit\Core\Storage\User_Options;
use Google\Site_Kit\Core\Tags\Guards\Tag_Environment_Type_Guard;
use Google\Site_Kit\Core\Tags\Guards\Tag_Verify_Guard;
use Google\Site_Kit\Core\Tracking\Feature_Metrics_Trait;
use Google\Site_Kit\Core\Tracking\Provides_Feature_Metrics;
use Google\Site_Kit\Core\Util\Block_Support;
use Google\Site_Kit\Core\Util\Method_Proxy_Trait;
use Google\Site_Kit\Core\Util\URL;
use Google\Site_Kit\Modules\Reader_Revenue_Manager\Admin_Post_List;
use Google\Site_Kit\Modules\Reader_Revenue_Manager\Contribute_With_Google_Block;
use Google\Site_Kit\Modules\Reader_Revenue_Manager\Subscribe_With_Google_Block;
use Google\Site_Kit\Modules\Reader_Revenue_Manager\Post_Product_ID;
use Google\Site_Kit\Modules\Reader_Revenue_Manager\Settings;
use Google\Site_Kit\Modules\Reader_Revenue_Manager\Synchronize_Publication;
use Google\Site_Kit\Modules\Reader_Revenue_Manager\Tag_Guard;
use Google\Site_Kit\Modules\Reader_Revenue_Manager\Tag_Matchers;
use Google\Site_Kit\Modules\Reader_Revenue_Manager\Web_Tag;
use Google\Site_Kit\Modules\Search_Console\Settings as Search_Console_Settings;
use Google\Site_Kit_Dependencies\Google\Service\SubscribewithGoogle as Google_Service_SubscribewithGoogle;
use WP_Error;
/**
* Class representing the Reader Revenue Manager module.
*
* @since 1.130.0
* @access private
* @ignore
*/
final class Reader_Revenue_Manager extends Module implements Module_With_Scopes, Module_With_Assets, Module_With_Service_Entity, Module_With_Deactivation, Module_With_Owner, Module_With_Settings, Module_With_Tag, Module_With_Debug_Fields, Provides_Feature_Metrics {
use Module_With_Assets_Trait;
use Module_With_Owner_Trait;
use Module_With_Scopes_Trait;
use Module_With_Settings_Trait;
use Module_With_Tag_Trait;
use Method_Proxy_Trait;
use Feature_Metrics_Trait;
/**
* Module slug name.
*/
const MODULE_SLUG = 'reader-revenue-manager';
/**
* Post_Product_ID instance.
*
* @since 1.148.0
*
* @var Post_Product_ID
*/
private $post_product_id;
/**
* Contribute_With_Google_Block instance.
*
* @since 1.148.0
*
* @var Contribute_With_Google_Block
*/
private $contribute_with_google_block;
/**
* Subscribe_With_Google_Block instance.
*
* @since 1.148.0
*
* @var Subscribe_With_Google_Block
*/
private $subscribe_with_google_block;
/**
* Tag_Guard instance.
*
* @since 1.148.0
*
* @var Tag_Guard
*/
private $tag_guard;
const PRODUCT_ID_NOTIFICATIONS = array(
'rrm-product-id-contributions-notification',
'rrm-product-id-subscriptions-notification',
);
/**
* Constructor.
*
* @since 1.148.0
*
* @param Context $context Plugin context.
* @param Options $options Optional. Option API instance. Default is a new instance.
* @param User_Options $user_options Optional. User Option API instance. Default is a new instance.
* @param Authentication $authentication Optional. Authentication instance. Default is a new instance.
* @param Assets $assets Optional. Assets API instance. Default is a new instance.
*/
public function __construct(
Context $context,
?Options $options = null,
?User_Options $user_options = null,
?Authentication $authentication = null,
?Assets $assets = null
) {
parent::__construct( $context, $options, $user_options, $authentication, $assets );
$post_meta = new Post_Meta();
$settings = $this->get_settings();
$this->post_product_id = new Post_Product_ID( $post_meta, $settings );
$this->tag_guard = new Tag_Guard( $settings, $this->post_product_id );
$this->contribute_with_google_block = new Contribute_With_Google_Block( $this->context, $this->tag_guard, $settings );
$this->subscribe_with_google_block = new Subscribe_With_Google_Block( $this->context, $this->tag_guard, $settings );
}
/**
* Registers functionality through WordPress hooks.
*
* @since 1.130.0
*/
public function register() {
$this->register_scopes_hook();
$this->register_feature_metrics();
$synchronize_publication = new Synchronize_Publication(
$this,
$this->user_options
);
$synchronize_publication->register();
if ( $this->is_connected() ) {
$this->post_product_id->register();
$admin_post_list = new Admin_Post_List(
$this->get_settings(),
$this->post_product_id
);
$admin_post_list->register();
if ( Block_Support::has_block_support() ) {
$this->contribute_with_google_block->register();
$this->subscribe_with_google_block->register();
add_action(
'enqueue_block_assets',
$this->get_method_proxy(
'enqueue_block_assets_for_non_sitekit_user'
),
40
);
add_action(
'enqueue_block_editor_assets',
$this->get_method_proxy(
'enqueue_block_editor_assets_for_non_sitekit_user'
),
40
);
}
}
add_action( 'load-toplevel_page_googlesitekit-dashboard', array( $synchronize_publication, 'maybe_schedule_synchronize_publication' ) );
add_action( 'load-toplevel_page_googlesitekit-settings', array( $synchronize_publication, 'maybe_schedule_synchronize_publication' ) );
// Reader Revenue Manager tag placement logic.
add_action( 'template_redirect', array( $this, 'register_tag' ) );
// If the publication ID changes, clear the dismissed state for product ID notifications.
$this->get_settings()->on_change(
function ( $old_value, $new_value ) {
if ( $old_value['publicationID'] !== $new_value['publicationID'] ) {
$dismissed_items = new Dismissed_Items( $this->user_options );
foreach ( self::PRODUCT_ID_NOTIFICATIONS as $notification ) {
$dismissed_items->remove( $notification );
}
}
}
);
}
/**
* Gets required Google OAuth scopes for the module.
*
* @since 1.130.0
*
* @return array List of Google OAuth scopes.
*/
public function get_scopes() {
return array(
'https://www.googleapis.com/auth/subscribewithgoogle.publications.readonly',
);
}
/**
* 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.131.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.
*/
public function setup_services( Google_Site_Kit_Client $client ) {
return array(
'subscribewithgoogle' => new Google_Service_SubscribewithGoogle( $client ),
);
}
/**
* Checks whether the module is connected.
*
* @since 1.132.0
*
* @return bool True if module is connected, false otherwise.
*/
public function is_connected() {
$options = $this->get_settings()->get();
if ( ! empty( $options['publicationID'] ) ) {
return true;
}
return false;
}
/**
* Sets up the module's settings instance.
*
* @since 1.132.0
*
* @return Module_Settings
*/
protected function setup_settings() {
return new Settings( $this->options );
}
/**
* Cleans up when the module is deactivated.
*
* @since 1.132.0
*/
public function on_deactivation() {
$this->get_settings()->delete();
}
/**
* Checks if the current user has access to the current configured service entity.
*
* @since 1.131.0
* @since 1.134.0 Checks if the user's publications includes the saved publication.
*
* @return boolean|WP_Error
*/
public function check_service_entity_access() {
/**
* Get the SubscribewithGoogle service instance.
*
* @var Google_Service_SubscribewithGoogle
*/
$subscribewithgoogle = $this->get_service( 'subscribewithgoogle' );
try {
$response = $subscribewithgoogle->publications->listPublications();
} catch ( Exception $e ) {
if ( $e->getCode() === 403 ) {
return false;
}
return $this->exception_to_error( $e );
}
$publications = array_values( $response->getPublications() );
$settings = $this->get_settings()->get();
$publication_id = $settings['publicationID'];
// Check if the $publications array contains a publication with the saved
// publication ID.
foreach ( $publications as $publication ) {
if (
isset( $publication['publicationId'] ) &&
$publication_id === $publication['publicationId']
) {
return true;
}
}
return false;
}
/**
* Gets map of datapoint to definition data for each.
*
* @since 1.131.0
*
* @return array Map of datapoints to their definitions.
*/
protected function get_datapoint_definitions() {
return array(
'GET:publications' => array(
'service' => 'subscribewithgoogle',
),
'POST:sync-publication-onboarding-state' => array(
'service' => 'subscribewithgoogle',
),
);
}
/**
* Creates a request object for the given datapoint.
*
* @since 1.131.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|Missing_Required_Param_Exception Thrown if the datapoint does not exist or parameters are missing.
*/
protected function create_data_request( Data_Request $data ) {
switch ( "{$data->method}:{$data->datapoint}" ) {
case 'GET:publications':
/**
* Get the SubscribewithGoogle service instance.
*
* @var Google_Service_SubscribewithGoogle
*/
$subscribewithgoogle = $this->get_service( 'subscribewithgoogle' );
return $subscribewithgoogle->publications->listPublications( array( 'filter' => $this->get_publication_filter() ) );
case 'POST:sync-publication-onboarding-state':
if ( empty( $data['publicationID'] ) ) {
throw new Missing_Required_Param_Exception( 'publicationID' );
}
if ( empty( $data['publicationOnboardingState'] ) ) {
throw new Missing_Required_Param_Exception( 'publicationOnboardingState' );
}
$publications = $this->get_data( 'publications' );
if ( is_wp_error( $publications ) ) {
return $publications;
}
if ( empty( $publications ) ) {
return new WP_Error(
'publication_not_found',
__( 'Publication not found.', 'google-site-kit' ),
array( 'status' => 404 )
);
}
$publication = array_filter(
$publications,
function ( $publication ) use ( $data ) {
return $publication->getPublicationId() === $data['publicationID'];
}
);
if ( empty( $publication ) ) {
return new WP_Error(
'publication_not_found',
__( 'Publication not found.', 'google-site-kit' ),
array( 'status' => 404 )
);
}
$publication = reset( $publication );
$new_onboarding_state = $publication->getOnboardingState();
if ( $new_onboarding_state === $data['publicationOnboardingState'] ) {
return function () {
return (object) array();
};
}
$settings = $this->get_settings();
if ( $data['publicationID'] === $settings->get()['publicationID'] ) {
$settings->merge(
array(
'publicationOnboardingState' => $new_onboarding_state,
)
);
}
return function () use ( $data, $new_onboarding_state ) {
return (object) array(
'publicationID' => $data['publicationID'],
'publicationOnboardingState' => $new_onboarding_state,
);
};
}
return parent::create_data_request( $data );
}
/**
* Parses a response for the given datapoint.
*
* @since 1.131.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:publications':
$publications = $response->getPublications();
return array_values( $publications );
}
return parent::parse_data_response( $data, $response );
}
/**
* Sets up information about the module.
*
* @since 1.130.0
*
* @return array Associative array of module info.
*/
protected function setup_info() {
return array(
'slug' => self::MODULE_SLUG,
'name' => _x( 'Reader Revenue Manager', 'Service name', 'google-site-kit' ),
'description' => __( 'Reader Revenue Manager helps publishers grow, retain, and engage their audiences, creating new revenue opportunities', 'google-site-kit' ),
'homepage' => 'https://publishercenter.google.com',
);
}
/**
* Gets the filter for retrieving publications for the current site.
*
* @since 1.131.0
*
* @return string Permutations for site hosts or URL.
*/
private function get_publication_filter() {
$sc_settings = $this->options->get( Search_Console_Settings::OPTION );
$sc_property_id = $sc_settings['propertyID'];
if ( 0 === strpos( $sc_property_id, 'sc-domain:' ) ) { // Domain property.
$host = str_replace( 'sc-domain:', '', $sc_property_id );
$filter = join(
' OR ',
array_map(
function ( $domain ) {
return sprintf( 'domain = "%s"', $domain );
},
URL::permute_site_hosts( $host )
)
);
} else { // URL property.
$filter = join(
' OR ',
array_map(
function ( $url ) {
return sprintf( 'site_url = "%s"', $url );
},
URL::permute_site_url( $sc_property_id )
)
);
}
return $filter;
}
/**
* Sets up the module's assets to register.
*
* @since 1.131.0
*
* @return Asset[] List of Asset objects.
*/
protected function setup_assets() {
$base_url = $this->context->url( 'dist/assets/' );
$assets = array(
new Script(
'googlesitekit-modules-reader-revenue-manager',
array(
'src' => $base_url . 'js/googlesitekit-modules-reader-revenue-manager.js',
'dependencies' => array(
'googlesitekit-vendor',
'googlesitekit-api',
'googlesitekit-data',
'googlesitekit-modules',
'googlesitekit-notifications',
'googlesitekit-datastore-site',
'googlesitekit-datastore-user',
'googlesitekit-components',
),
)
),
);
if ( Block_Support::has_block_support() && $this->is_connected() ) {
$assets[] = new Script(
'blocks-reader-revenue-manager-block-editor-plugin',
array(
'src' => $base_url . 'blocks/reader-revenue-manager/block-editor-plugin/index.js',
'dependencies' => array(
'googlesitekit-components',
'googlesitekit-data',
'googlesitekit-i18n',
'googlesitekit-modules',
'googlesitekit-modules-reader-revenue-manager',
),
'execution' => 'defer',
'load_contexts' => array( Asset::CONTEXT_ADMIN_POST_EDITOR ),
)
);
$assets[] = new Stylesheet(
'blocks-reader-revenue-manager-block-editor-plugin-styles',
array(
'src' => $base_url . 'blocks/reader-revenue-manager/block-editor-plugin/editor-styles.css',
'dependencies' => array(),
'load_contexts' => array( Asset::CONTEXT_ADMIN_POST_EDITOR ),
)
);
$assets[] = new Script(
'blocks-contribute-with-google',
array(
'src' => $base_url . 'blocks/reader-revenue-manager/contribute-with-google/index.js',
'dependencies' => array(
'googlesitekit-components',
'googlesitekit-data',
'googlesitekit-i18n',
'googlesitekit-modules',
'googlesitekit-modules-reader-revenue-manager',
),
'load_contexts' => array( Asset::CONTEXT_ADMIN_POST_EDITOR ),
'execution' => 'defer',
)
);
$assets[] = new Script(
'blocks-subscribe-with-google',
array(
'src' => $base_url . 'blocks/reader-revenue-manager/subscribe-with-google/index.js',
'dependencies' => array(
'googlesitekit-components',
'googlesitekit-data',
'googlesitekit-i18n',
'googlesitekit-modules',
'googlesitekit-modules-reader-revenue-manager',
),
'load_contexts' => array( Asset::CONTEXT_ADMIN_POST_EDITOR ),
'execution' => 'defer',
)
);
if ( $this->is_non_sitekit_user() ) {
$assets[] = new Script(
'blocks-contribute-with-google-non-sitekit-user',
array(
'src' => $base_url . 'blocks/reader-revenue-manager/contribute-with-google/non-site-kit-user.js',
'dependencies' => array(
'googlesitekit-i18n',
),
'load_contexts' => array( Asset::CONTEXT_ADMIN_POST_EDITOR ),
'execution' => 'defer',
)
);
$assets[] = new Script(
'blocks-subscribe-with-google-non-sitekit-user',
array(
'src' => $base_url . 'blocks/reader-revenue-manager/subscribe-with-google/non-site-kit-user.js',
'dependencies' => array( 'googlesitekit-i18n' ),
'load_contexts' => array( Asset::CONTEXT_ADMIN_POST_EDITOR ),
'execution' => 'defer',
)
);
}
$assets[] = new Stylesheet(
'blocks-reader-revenue-manager-common-editor-styles',
array(
'src' => $base_url . 'blocks/reader-revenue-manager/common/editor-styles.css',
'dependencies' => array(),
'load_contexts' => array( Asset::CONTEXT_ADMIN_BLOCK_EDITOR ),
)
);
}
return $assets;
}
/**
* Returns the Module_Tag_Matchers instance.
*
* @since 1.132.0
*
* @return Module_Tag_Matchers Module_Tag_Matchers instance.
*/
public function get_tag_matchers() {
return new Tag_Matchers();
}
/**
* Registers the Reader Revenue Manager tag.
*
* @since 1.132.0
*/
public function register_tag() {
$module_settings = $this->get_settings();
$settings = $module_settings->get();
$tag = new Web_Tag( $settings['publicationID'], self::MODULE_SLUG );
if ( $tag->is_tag_blocked() ) {
return;
}
$tag->use_guard( new Tag_Verify_Guard( $this->context->input() ) );
$tag->use_guard( $this->tag_guard );
$tag->use_guard( new Tag_Environment_Type_Guard() );
if ( ! $tag->can_register() ) {
return;
}
$product_id = $settings['productID'];
$post_product_id = '';
if ( is_singular() ) {
$post_product_id = $this->post_product_id->get( get_the_ID() );
if ( ! empty( $post_product_id ) ) {
$product_id = $post_product_id;
}
}
// Extract the product ID from the setting, which is in the format
// of `publicationID:productID`.
if ( 'openaccess' !== $product_id ) {
$separator_index = strpos( $product_id, ':' );
if ( false !== $separator_index ) {
$product_id = substr( $product_id, $separator_index + 1 );
}
}
$tag->set_product_id( $product_id );
$tag->register();
}
/**
* Checks if the current user is a non-Site Kit user.
*
* @since 1.150.0
*
* @return bool True if the current user is a non-Site Kit user, false otherwise.
*/
private function is_non_sitekit_user() {
return ! ( current_user_can( Permissions::VIEW_SPLASH ) || current_user_can( Permissions::VIEW_DASHBOARD ) );
}
/**
* Enqueues block assets for non-Site Kit users.
*
* This is used for enqueueing styles to ensure they are loaded in all block editor contexts including iframes.
*
* @since 1.150.0
*
* @return void
*/
private function enqueue_block_assets_for_non_sitekit_user() {
// Include a check for is_admin() to ensure the styles are only enqueued on admin screens.
if ( is_admin() && $this->is_non_sitekit_user() ) {
// Enqueue styles.
$this->assets->enqueue_asset( 'blocks-reader-revenue-manager-common-editor-styles' );
}
}
/**
* Enqueues block editor assets for non-Site Kit users.
*
* @since 1.150.0
*
* @return void
*/
private function enqueue_block_editor_assets_for_non_sitekit_user() {
if ( $this->is_non_sitekit_user() ) {
// Enqueue scripts.
$this->assets->enqueue_asset( 'blocks-contribute-with-google-non-sitekit-user' );
$this->assets->enqueue_asset( 'blocks-subscribe-with-google-non-sitekit-user' );
}
}
/**
* Gets an array of debug field definitions.
*
* @since 1.132.0
*
* @return array An array of all debug fields.
*/
public function get_debug_fields() {
$settings = $this->get_settings()->get();
$snippet_mode_values = array(
'post_types' => __( 'Post types', 'google-site-kit' ),
'per_post' => __( 'Per post', 'google-site-kit' ),
'sitewide' => __( 'Sitewide', 'google-site-kit' ),
);
$extract_product_id = function ( $product_id ) {
$parts = explode( ':', $product_id );
return isset( $parts[1] ) ? $parts[1] : $product_id;
};
$redact_pub_in_product_id = function ( $product_id ) {
$parts = explode( ':', $product_id );
if ( isset( $parts[1] ) ) {
return Debug_Data::redact_debug_value( $parts[0] ) . ':' . $parts[1];
}
return $product_id;
};
$debug_fields = array(
'reader_revenue_manager_publication_id' => array(
'label' => __( 'Reader Revenue Manager: Publication ID', 'google-site-kit' ),
'value' => $settings['publicationID'],
'debug' => Debug_Data::redact_debug_value( $settings['publicationID'] ),
),
'reader_revenue_manager_publication_onboarding_state' => array(
'label' => __( 'Reader Revenue Manager: Publication onboarding state', 'google-site-kit' ),
'value' => $settings['publicationOnboardingState'],
'debug' => $settings['publicationOnboardingState'],
),
'reader_revenue_manager_available_product_ids' => array(
'label' => __( 'Reader Revenue Manager: Available product IDs', 'google-site-kit' ),
'value' => implode( ', ', array_map( $extract_product_id, $settings['productIDs'] ) ),
'debug' => implode( ', ', array_map( $redact_pub_in_product_id, $settings['productIDs'] ) ),
),
'reader_revenue_manager_payment_option' => array(
'label' => __( 'Reader Revenue Manager: Payment option', 'google-site-kit' ),
'value' => $settings['paymentOption'],
'debug' => $settings['paymentOption'],
),
'reader_revenue_manager_snippet_mode' => array(
'label' => __( 'Reader Revenue Manager: Snippet placement', 'google-site-kit' ),
'value' => $snippet_mode_values[ $settings['snippetMode'] ],
'debug' => $settings['snippetMode'],
),
'reader_revenue_manager_product_id' => array(
'label' => __( 'Reader Revenue Manager: Product ID', 'google-site-kit' ),
'value' => $extract_product_id( $settings['productID'] ),
'debug' => $redact_pub_in_product_id( $settings['productID'] ),
),
);
if ( 'post_types' === $settings['snippetMode'] ) {
$debug_fields['reader_revenue_manager_post_types'] = array(
'label' => __( 'Reader Revenue Manager: Post types', 'google-site-kit' ),
'value' => implode( ', ', $settings['postTypes'] ),
'debug' => implode( ', ', $settings['postTypes'] ),
);
}
return $debug_fields;
}
/**
* Gets an array of internal feature metrics.
*
* @since 1.163.0
*
* @return array
*/
public function get_feature_metrics() {
$settings = $this->get_settings()->get();
return array(
'rrm_publication_onboarding_state' => $settings['publicationOnboardingState'],
);
}
}

View File

@@ -0,0 +1,246 @@
<?php
/**
* Class Google\Site_Kit\Modules\Reader_Revenue_Manager\Admin_Post_List
*
* @package Google\Site_Kit\Modules\Reader_Revenue_Manager
* @copyright 2025 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\Reader_Revenue_Manager;
use Google\Site_Kit\Modules\Reader_Revenue_Manager\Post_Product_ID;
/**
* Class for adding RRM elements to the WP Admin post list.
*
* @since 1.148.0
* @access private
* @ignore
*/
class Admin_Post_List {
/**
* Post_Product_ID instance.
*
* @since 1.148.0
*
* @var Post_Product_ID
*/
private $post_product_id;
/**
* Settings instance.
*
* @since 1.148.0
*
* @var Settings
*/
private $settings;
/**
* Constructor.
*
* @since 1.148.0
*
* @param Settings $settings Module settings instance.
* @param Post_Product_ID $post_product_id Post Product ID.
*/
public function __construct( Settings $settings, Post_Product_ID $post_product_id ) {
$this->settings = $settings;
$this->post_product_id = $post_product_id;
}
/**
* Registers functionality through WordPress hooks.
*
* @since 1.148.0
*/
public function register() {
$post_types = $this->get_post_types();
foreach ( $post_types as $post_type ) {
add_filter(
"manage_{$post_type}_posts_columns",
array( $this, 'add_column' )
);
add_action(
"manage_{$post_type}_posts_custom_column",
array( $this, 'fill_column' ),
10,
2
);
}
add_action(
'bulk_edit_custom_box',
array( $this, 'bulk_edit_field' ),
10,
2
);
add_action( 'save_post', array( $this, 'save_field' ) );
}
/**
* Adds a custom column to the post list.
*
* @since 1.148.0
*
* @param array $columns Columns.
* @return array Modified columns.
*/
public function add_column( $columns ) {
$columns['rrm_product_id'] = __( 'Reader Revenue CTA', 'google-site-kit' );
return $columns;
}
/**
* Fills the custom column with data.
*
* @since 1.148.0
*
* @param string $column Column name.
* @param int $post_id Post ID.
*/
public function fill_column( $column, $post_id ) {
if ( 'rrm_product_id' !== $column ) {
return;
}
$post_product_id = $this->post_product_id->get( $post_id );
if ( ! empty( $post_product_id ) ) {
switch ( $post_product_id ) {
case 'none':
esc_html_e( 'None', 'google-site-kit' );
break;
case 'openaccess':
esc_html_e( 'Open access', 'google-site-kit' );
break;
default:
$separator_index = strpos( $post_product_id, ':' );
if ( false === $separator_index ) {
echo esc_html( $post_product_id );
} else {
echo esc_html(
substr( $post_product_id, $separator_index + 1 )
);
}
}
return;
}
$settings = $this->settings->get();
$snippet_mode = $settings['snippetMode'];
$cta_post_types = apply_filters( 'googlesitekit_reader_revenue_manager_cta_post_types', $settings['postTypes'] );
if ( 'per_post' === $snippet_mode || ( 'post_types' === $snippet_mode && ! in_array( get_post_type(), $cta_post_types, true ) ) ) {
esc_html_e( 'None', 'google-site-kit' );
return;
}
esc_html_e( 'Default', 'google-site-kit' );
}
/**
* Adds a custom field to the bulk edit form.
*
* @since 1.148.0
*/
public function bulk_edit_field() {
$settings = $this->settings->get();
$product_ids = $settings['productIDs'] ?? array();
$default_options = array(
'-1' => __( '— No Change —', 'google-site-kit' ),
'' => __( 'Default', 'google-site-kit' ),
'none' => __( 'None', 'google-site-kit' ),
'openaccess' => __( 'Open access', 'google-site-kit' ),
);
?>
<fieldset class="inline-edit-col-right">
<div class="inline-edit-col">
<label style="align-items: center; display: flex; gap: 16px; line-height: 1;">
<span><?php esc_html_e( 'Reader Revenue CTA', 'google-site-kit' ); ?></span>
<select name="rrm_product_id">
<?php foreach ( $default_options as $value => $label ) : ?>
<option value="<?php echo esc_attr( $value ); ?>">
<?php echo esc_html( $label ); ?>
</option>
<?php endforeach; ?>
<?php foreach ( $product_ids as $product_id ) : ?>
<?php list( , $label ) = explode( ':', $product_id, 2 ); ?>
<option value="<?php echo esc_attr( $product_id ); ?>">
<?php
echo esc_html( $label );
?>
</option>
<?php endforeach; ?>
</select>
</label>
</div>
</fieldset>
<?php
}
/**
* Saves the custom field value from the bulk edit form.
*
* @since 1.148.0
*
* @param int $post_id Post ID.
*/
public function save_field( $post_id ) {
if ( ! current_user_can( 'edit_post', $post_id ) ) {
return;
}
if ( ! isset( $_REQUEST['_wpnonce'] ) ) {
return;
}
$nonce = sanitize_key( wp_unslash( $_REQUEST['_wpnonce'] ) );
if ( ! wp_verify_nonce( $nonce, 'bulk-posts' ) ) {
return;
}
if ( isset( $_REQUEST['rrm_product_id'] ) && '-1' !== $_REQUEST['rrm_product_id'] ) {
$post_product_id = sanitize_text_field(
wp_unslash( $_REQUEST['rrm_product_id'] )
);
$this->post_product_id->set(
$post_id,
$post_product_id
);
}
}
/**
* Retrieves the public post types that support the block editor.
*
* @since 1.148.0
*
* @return array Array of post types.
*/
protected function get_post_types() {
$post_types = get_post_types( array( 'public' => true ), 'objects' );
$supported_post_types = array();
foreach ( $post_types as $post_type => $post_type_obj ) {
if (
post_type_supports( $post_type, 'editor' ) &&
! empty( $post_type_obj->show_in_rest )
) {
$supported_post_types[] = $post_type;
}
}
return $supported_post_types;
}
}

View File

@@ -0,0 +1,108 @@
<?php
/**
* Class Google\Site_Kit\Modules\Reader_Revenue_Manager\Contribute_With_Google_Block
*
* @package Google\Site_Kit\Modules\Reader_Revenue_Manager
* @copyright 2025 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\Reader_Revenue_Manager;
use Google\Site_Kit\Context;
use Google\Site_Kit\Core\Modules\Module_Settings;
use Google\Site_Kit\Modules\Reader_Revenue_Manager\Tag_Guard;
/**
* Contribute with Google Gutenberg block.
*
* @since 1.148.0
* @access private
* @ignore
*/
class Contribute_With_Google_Block {
/**
* Context instance.
*
* @since 1.148.0
*
* @var Context
*/
protected $context;
/**
* Tag_Guard instance.
*
* @since 1.148.0
*
* @var Tag_Guard
*/
private $tag_guard;
/**
* Settings instance.
*
* @since 1.148.0
*
* @var Module_Settings
*/
private $settings;
/**
* Constructor.
*
* @since 1.148.0
*
* @param Context $context Plugin context.
* @param Tag_Guard $tag_guard Tag_Guard instance.
* @param Module_Settings $settings Module_Settings instance.
*/
public function __construct( Context $context, Tag_Guard $tag_guard, Module_Settings $settings ) {
$this->context = $context;
$this->tag_guard = $tag_guard;
$this->settings = $settings;
}
/**
* Register this block.
*
* @since 1.148.0
*/
public function register() {
add_action(
'init',
function () {
register_block_type(
dirname( GOOGLESITEKIT_PLUGIN_MAIN_FILE ) . '/dist/assets/blocks/reader-revenue-manager/contribute-with-google/block.json',
array(
'render_callback' => array( $this, 'render_callback' ),
)
);
},
99
);
}
/**
* Render callback for the block.
*
* @since 1.148.0
*
* @return string Rendered block.
*/
public function render_callback() {
// If the payment option is not `contributions` or the tag is not placed, do not render the block.
$settings = $this->settings->get();
$is_contributions_payment_option = isset( $settings['paymentOption'] ) && 'contributions' === $settings['paymentOption'];
if ( ! ( $is_contributions_payment_option && $this->tag_guard->can_activate() ) ) {
return '';
}
// Ensure the button is centered to match the editor preview.
// TODO: Add a stylesheet to the page and style the button container using a class.
return '<div style="margin: 0 auto;"><button swg-standard-button="contribution"></button></div>';
}
}

View File

@@ -0,0 +1,83 @@
<?php
/**
* Class Google\Site_Kit\Modules\Reader_Revenue_Manager\Post_Product_ID
*
* @package Google\Site_Kit\Modules\Reader_Revenue_Manager
* @copyright 2025 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\Reader_Revenue_Manager;
use Google\Site_Kit\Core\Storage\Meta_Setting_Trait;
use Google\Site_Kit\Core\Storage\Post_Meta;
use Google\Site_Kit\Modules\Reader_Revenue_Manager\Settings;
/**
* Class for associating product ID to post meta.
*
* @since 1.145.0
* @access private
* @ignore
*/
class Post_Product_ID {
use Meta_Setting_Trait;
/**
* Settings instance.
*
* @since 1.148.0
*
* @var Settings
*/
private $settings;
/**
* Post_Product_ID constructor.
*
* @since 1.145.0
*
* @param Post_Meta $post_meta Post_Meta instance.
* @param Settings $settings Reader Revenue Manager module settings instance.
*/
public function __construct( Post_Meta $post_meta, Settings $settings ) {
$this->meta = $post_meta;
$this->settings = $settings;
}
/**
* Gets the meta key for the setting.
*
* @since 1.145.0
*
* @return string Meta key.
*/
protected function get_meta_key(): string {
$publication_id = $this->settings->get()['publicationID'];
return 'googlesitekit_rrm_' . $publication_id . ':productID';
}
/**
* Returns the object type.
*
* @since 1.146.0
*
* @return string Object type.
*/
protected function get_object_type(): string {
return 'post';
}
/**
* Gets the `show_in_rest` value for this postmeta setting value.
*
* @since 1.145.0
*
* @return bool|Array Any valid value for the `show_in_rest`
*/
protected function get_show_in_rest() {
return true;
}
}

View File

@@ -0,0 +1,189 @@
<?php
/**
* Class Google\Site_Kit\Modules\Reader_Revenue_Manager\Settings
*
* @package Google\Site_Kit\Modules\Reader_Revenue_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\Reader_Revenue_Manager;
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;
use Google\Site_Kit\Core\Storage\Setting_With_ViewOnly_Keys_Interface;
use Google\Site_Kit\Core\Util\Method_Proxy_Trait;
/**
* Class for RRM settings.
*
* @since 1.132.0
* @access private
* @ignore
*/
class Settings extends Module_Settings implements Setting_With_Owned_Keys_Interface, Setting_With_ViewOnly_Keys_Interface {
use Setting_With_Owned_Keys_Trait;
use Method_Proxy_Trait;
const OPTION = 'googlesitekit_reader-revenue-manager_settings';
/**
* Various Reader Revenue Manager onboarding statuses.
*/
const ONBOARDING_STATE_UNSPECIFIED = 'ONBOARDING_STATE_UNSPECIFIED';
const ONBOARDING_STATE_ACTION_REQUIRED = 'ONBOARDING_ACTION_REQUIRED';
const ONBOARDING_STATE_PENDING_VERIFICATION = 'PENDING_VERIFICATION';
const ONBOARDING_STATE_COMPLETE = 'ONBOARDING_COMPLETE';
/**
* Registers the setting in WordPress.
*
* @since 1.132.0
*/
public function register() {
parent::register();
$this->register_owned_keys();
}
/**
* Returns keys for owned settings.
*
* @since 1.132.0
*
* @return array An array of keys for owned settings.
*/
public function get_owned_keys() {
return array( 'publicationID' );
}
/**
* Gets the default value.
*
* @since 1.132.0
*
* @return array
*/
protected function get_default() {
$defaults = array(
'ownerID' => 0,
'publicationID' => '',
'publicationOnboardingState' => '',
'publicationOnboardingStateChanged' => false,
'productIDs' => array(),
'paymentOption' => '',
'snippetMode' => 'post_types',
'postTypes' => array( 'post' ),
'productID' => 'openaccess',
);
return $defaults;
}
/**
* Returns keys for view-only settings.
*
* @since 1.132.0
*
* @return array An array of keys for view-only settings.
*/
public function get_view_only_keys() {
$keys = array(
'publicationID',
'snippetMode',
'postTypes',
'paymentOption',
);
return $keys;
}
/**
* Gets the callback for sanitizing the setting's value before saving.
*
* @since 1.132.0
*
* @return callable|null
*/
protected function get_sanitize_callback() {
return function ( $option ) {
if ( isset( $option['publicationID'] ) ) {
if ( ! preg_match( '/^[a-zA-Z0-9_-]+$/', $option['publicationID'] ) ) {
$option['publicationID'] = '';
}
}
if ( isset( $option['publicationOnboardingStateChanged'] ) ) {
if ( ! is_bool( $option['publicationOnboardingStateChanged'] ) ) {
$option['publicationOnboardingStateChanged'] = false;
}
}
if ( isset( $option['publicationOnboardingState'] ) ) {
$valid_onboarding_states = array(
self::ONBOARDING_STATE_UNSPECIFIED,
self::ONBOARDING_STATE_ACTION_REQUIRED,
self::ONBOARDING_STATE_PENDING_VERIFICATION,
self::ONBOARDING_STATE_COMPLETE,
);
if ( ! in_array( $option['publicationOnboardingState'], $valid_onboarding_states, true ) ) {
$option['publicationOnboardingState'] = '';
}
}
if ( isset( $option['productIDs'] ) ) {
if ( ! is_array( $option['productIDs'] ) ) {
$option['productIDs'] = array();
} else {
$option['productIDs'] = array_values(
array_filter(
$option['productIDs'],
'is_string'
)
);
}
}
if ( isset( $option['paymentOption'] ) ) {
if ( ! is_string( $option['paymentOption'] ) ) {
$option['paymentOption'] = '';
}
}
if ( isset( $option['snippetMode'] ) ) {
$valid_snippet_modes = array( 'post_types', 'per_post', 'sitewide' );
if ( ! in_array( $option['snippetMode'], $valid_snippet_modes, true ) ) {
$option['snippetMode'] = 'post_types';
}
}
if ( isset( $option['postTypes'] ) ) {
if ( ! is_array( $option['postTypes'] ) ) {
$option['postTypes'] = array( 'post' );
} else {
$filtered_post_types = array_values(
array_filter(
$option['postTypes'],
'is_string'
)
);
$option['postTypes'] = ! empty( $filtered_post_types )
? $filtered_post_types
: array( 'post' );
}
}
if ( isset( $option['productID'] ) ) {
if ( ! is_string( $option['productID'] ) ) {
$option['productID'] = 'openaccess';
}
}
return $option;
};
}
}

View File

@@ -0,0 +1,108 @@
<?php
/**
* Class Google\Site_Kit\Modules\Reader_Revenue_Manager\Subscribe_With_Google_Block
*
* @package Google\Site_Kit\Modules\Reader_Revenue_Manager
* @copyright 2025 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\Reader_Revenue_Manager;
use Google\Site_Kit\Context;
use Google\Site_Kit\Core\Modules\Module_Settings;
use Google\Site_Kit\Modules\Reader_Revenue_Manager\Tag_Guard;
/**
* Subscribe with Google Gutenberg block.
*
* @since 1.148.0
* @access private
* @ignore
*/
class Subscribe_With_Google_Block {
/**
* Context instance.
*
* @since 1.148.0
*
* @var Context
*/
protected $context;
/**
* Tag_Guard instance.
*
* @since 1.148.0
*
* @var Tag_Guard
*/
private $tag_guard;
/**
* Settings instance.
*
* @since 1.148.0
*
* @var Module_Settings
*/
private $settings;
/**
* Constructor.
*
* @since 1.148.0
*
* @param Context $context Plugin context.
* @param Tag_Guard $tag_guard Tag_Guard instance.
* @param Module_Settings $settings Module_Settings instance.
*/
public function __construct( Context $context, Tag_Guard $tag_guard, Module_Settings $settings ) {
$this->context = $context;
$this->tag_guard = $tag_guard;
$this->settings = $settings;
}
/**
* Register this block.
*
* @since 1.148.0
*/
public function register() {
add_action(
'init',
function () {
register_block_type(
dirname( GOOGLESITEKIT_PLUGIN_MAIN_FILE ) . '/dist/assets/blocks/reader-revenue-manager/subscribe-with-google/block.json',
array(
'render_callback' => array( $this, 'render_callback' ),
)
);
},
99
);
}
/**
* Render callback for the block.
*
* @since 1.148.0
*
* @return string Rendered block.
*/
public function render_callback() {
// If the payment option is not `subscriptions` or the tag is not placed, do not render the block.
$settings = $this->settings->get();
$is_subscriptions_payment_option = isset( $settings['paymentOption'] ) && 'subscriptions' === $settings['paymentOption'];
if ( ! ( $is_subscriptions_payment_option && $this->tag_guard->can_activate() ) ) {
return '';
}
// Ensure the button is centered to match the editor preview.
// TODO: Add a stylesheet to the page and style the button container using a class.
return '<div style="margin: 0 auto;"><button swg-standard-button="subscription"></button></div>';
}
}

View File

@@ -0,0 +1,204 @@
<?php
/**
* Class Google\Site_Kit\Modules\Reader_Revenue_Manager\Synchronize_Publication
*
* @package Google\Site_Kit\Modules\Reader_Revenue_Manager
* @copyright 2025 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\Reader_Revenue_Manager;
use Google\Site_Kit\Core\Permissions\Permissions;
use Google\Site_Kit\Core\Storage\User_Options;
use Google\Site_Kit\Core\Util\Feature_Flags;
use Google\Site_Kit\Modules\Reader_Revenue_Manager;
use Google\Site_Kit_Dependencies\Google\Service\SubscribewithGoogle\Publication;
use Google\Site_Kit_Dependencies\Google\Service\SubscribewithGoogle\PaymentOptions;
/**
* Class for synchronizing the onboarding state.
*
* @since 1.146.0
* @access private
* @ignore
*/
class Synchronize_Publication {
/**
* Cron event name for synchronizing the publication info.
*/
const CRON_SYNCHRONIZE_PUBLICATION = 'googlesitekit_cron_synchronize_publication';
/**
* Reader_Revenue_Manager instance.
*
* @var Reader_Revenue_Manager
*/
protected $reader_revenue_manager;
/**
* User_Options instance.
*
* @var User_Options
*/
protected $user_options;
/**
* Constructor.
*
* @since 1.146.0
*
* @param Reader_Revenue_Manager $reader_revenue_manager Reader Revenue Manager instance.
* @param User_Options $user_options User_Options instance.
*/
public function __construct( Reader_Revenue_Manager $reader_revenue_manager, User_Options $user_options ) {
$this->reader_revenue_manager = $reader_revenue_manager;
$this->user_options = $user_options;
}
/**
* Registers functionality through WordPress hooks.
*
* @since 1.146.0
*
* @return void
*/
public function register() {
add_action(
self::CRON_SYNCHRONIZE_PUBLICATION,
function () {
$this->synchronize_publication_data();
}
);
}
/**
* Cron callback for synchronizing the publication.
*
* @since 1.146.0
*
* @return void
*/
protected function synchronize_publication_data() {
$owner_id = $this->reader_revenue_manager->get_owner_id();
$restore_user = $this->user_options->switch_user( $owner_id );
if ( user_can( $owner_id, Permissions::VIEW_AUTHENTICATED_DASHBOARD ) ) {
$connected = $this->reader_revenue_manager->is_connected();
// If not connected, return early.
if ( ! $connected ) {
return;
}
$publications = $this->reader_revenue_manager->get_data( 'publications' );
// If publications is empty, return early.
if ( empty( $publications ) ) {
return;
}
$settings = $this->reader_revenue_manager->get_settings()->get();
$publication_id = $settings['publicationID'];
$filtered_publications = array_filter(
$publications,
function ( $pub ) use ( $publication_id ) {
return $pub->getPublicationId() === $publication_id;
}
);
// If there are no filtered publications, return early.
if ( empty( $filtered_publications ) ) {
return;
}
// Re-index the filtered array to ensure sequential keys.
$filtered_publications = array_values( $filtered_publications );
$publication = $filtered_publications[0];
$onboarding_state = $settings['publicationOnboardingState'];
$new_onboarding_state = $publication->getOnboardingState();
$new_settings = array(
'publicationOnboardingState' => $new_onboarding_state,
'productIDs' => $this->get_product_ids( $publication ),
'paymentOption' => $this->get_payment_option( $publication ),
);
// Let the client know if the onboarding state has changed.
if ( $new_onboarding_state !== $onboarding_state ) {
$new_settings['publicationOnboardingStateChanged'] = true;
}
$this->reader_revenue_manager->get_settings()->merge( $new_settings );
}
$restore_user();
}
/**
* Returns the products IDs for the given publication.
*
* @since 1.146.0
*
* @param Publication $publication Publication object.
* @return array Product IDs.
*/
protected function get_product_ids( Publication $publication ) {
$products = $publication->getProducts();
$product_ids = array();
if ( ! empty( $products ) ) {
foreach ( $products as $product ) {
$product_ids[] = $product->getName();
}
}
return $product_ids;
}
/**
* Returns the payment option for the given publication.
*
* @since 1.146.0
*
* @param Publication $publication Publication object.
* @return string Payment option.
*/
protected function get_payment_option( Publication $publication ) {
$payment_options = $publication->getPaymentOptions();
$payment_option = '';
if ( $payment_options instanceof PaymentOptions ) {
foreach ( $payment_options as $option => $value ) {
if ( true === $value ) {
$payment_option = $option;
break;
}
}
}
return $payment_option;
}
/**
* Maybe schedule the synchronize onboarding state cron event.
*
* @since 1.146.0
*
* @return void
*/
public function maybe_schedule_synchronize_publication() {
$connected = $this->reader_revenue_manager->is_connected();
$cron_already_scheduled = wp_next_scheduled( self::CRON_SYNCHRONIZE_PUBLICATION );
if ( $connected && ! $cron_already_scheduled ) {
wp_schedule_single_event(
time() + HOUR_IN_SECONDS,
self::CRON_SYNCHRONIZE_PUBLICATION
);
}
}
}

View File

@@ -0,0 +1,117 @@
<?php
/**
* Class Google\Site_Kit\Modules\Reader_Revenue_Manager\Tag_Guard
*
* @package Google\Site_Kit\Modules\Reader_Revenue_Manager
* @copyright 2024 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\Reader_Revenue_Manager;
use Google\Site_Kit\Core\Modules\Module_Settings;
use Google\Site_Kit\Core\Modules\Tags\Module_Tag_Guard;
use Google\Site_Kit\Modules\Reader_Revenue_Manager\Post_Product_ID;
/**
* Class for the Reader Revenue Manager tag guard.
*
* @since 1.132.0
* @access private
* @ignore
*/
class Tag_Guard extends Module_Tag_Guard {
/**
* Post_Product_ID instance.
*
* @since 1.148.0
*
* @var Post_Product_ID
*/
private $post_product_id;
/**
* Constructor.
*
* @since 1.148.0
*
* @param Module_Settings $settings Module settings instance.
* @param Post_Product_ID $post_product_id Post_Product_ID instance.
*/
public function __construct( Module_Settings $settings, $post_product_id ) {
parent::__construct( $settings );
$this->post_product_id = $post_product_id;
}
/**
* Determines whether the guarded tag can be activated or not.
*
* @since 1.132.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();
if ( empty( $settings['publicationID'] ) ) {
return false;
}
if ( is_singular() ) {
return $this->can_activate_for_singular_post();
}
return 'sitewide' === $settings['snippetMode'];
}
/**
* Determines whether the guarded tag can be activated for a singular post or not.
*
* @since 1.148.0
*
* @return bool TRUE if guarded tag can be activated for a singular post, otherwise FALSE.
*/
private function can_activate_for_singular_post() {
$post_product_id = $this->post_product_id->get( get_the_ID() );
if ( 'none' === $post_product_id ) {
return false;
}
if ( ! empty( $post_product_id ) ) {
return true;
}
$settings = $this->settings->get();
// If the snippet mode is `per_post` and there is no post product ID,
// we don't want to render the tag.
if ( 'per_post' === $settings['snippetMode'] ) {
return false;
}
// If the snippet mode is `post_types`, we only want to render the tag
// if the current post type is in the list of allowed post types.
if ( 'post_types' === $settings['snippetMode'] ) {
/**
* Filters the post types where Reader Revenue Manager CTAs should appear.
*
* @since 1.140.0
*
* @param array $cta_post_types The array of post types.
*/
$cta_post_types = apply_filters(
'googlesitekit_reader_revenue_manager_cta_post_types',
$settings['postTypes']
);
return in_array( get_post_type(), $cta_post_types, true );
}
// Snippet mode is `sitewide` at this point, so we want to render the tag.
return true;
}
}

View File

@@ -0,0 +1,38 @@
<?php
/**
* Class Google\Site_Kit\Modules\Reader_Revenue_Manager\Tag_Matchers
*
* @package Google\Site_Kit\Modules\Reader_Revenue_Manager
* @copyright 2024 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\Reader_Revenue_Manager;
use Google\Site_Kit\Core\Modules\Tags\Module_Tag_Matchers;
use Google\Site_Kit\Core\Tags\Tag_Matchers_Interface;
/**
* Class for Tag matchers.
*
* @since 1.132.0
* @access private
* @ignore
*/
class Tag_Matchers extends Module_Tag_Matchers implements Tag_Matchers_Interface {
/**
* Holds array of regex tag matchers.
*
* @since 1.132.0
*
* @return array Array of regex matchers.
*/
public function regex_matchers() {
return array(
"/<script\s+[^>]*src=['|\"]https?:\/\/news\.google\.com\/swg\/js\/v1\/swg-basic\.js['|\"][^>]*>/",
'/\(self\.SWG_BASIC=self\.SWG_BASIC\|\|\[\]\)\.push/',
);
}
}

View File

@@ -0,0 +1,152 @@
<?php
/**
* Class Google\Site_Kit\Modules\Reader_Revenue_Manager\Web_Tag
*
* @package Google\Site_Kit\Modules\Reader_Revenue_Manager
* @copyright 2024 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\Reader_Revenue_Manager;
use Google\Site_Kit\Core\Modules\Tags\Module_Web_Tag;
use Google\Site_Kit\Core\Tags\Tag_With_DNS_Prefetch_Trait;
use Google\Site_Kit\Core\Util\Method_Proxy_Trait;
/**
* Class for Web tag.
*
* @since 1.132.0
* @access private
* @ignore
*/
class Web_Tag extends Module_Web_Tag {
use Method_Proxy_Trait;
use Tag_With_DNS_Prefetch_Trait;
/**
* Product ID.
*
* @since 1.148.0
*
* @var string
*/
private $product_id;
/**
* Sets the product ID.
*
* @since 1.148.0
*
* @param string $product_id Product ID.
*/
public function set_product_id( $product_id ) {
$this->product_id = $product_id;
}
/**
* Registers tag hooks.
*
* @since 1.132.0
*/
public function register() {
add_action( 'wp_enqueue_scripts', $this->get_method_proxy( 'enqueue_swg_script' ) );
add_filter(
'script_loader_tag',
$this->get_method_proxy( 'add_snippet_comments' ),
10,
2
);
add_filter(
'wp_resource_hints',
$this->get_dns_prefetch_hints_callback( '//news.google.com' ),
10,
2
);
$this->do_init_tag_action();
}
/**
* Enqueues the Reader Revenue Manager (SWG) script.
*
* @since 1.132.0
* @since 1.140.0 Updated to enqueue the script only on singular posts.
*/
protected function enqueue_swg_script() {
$locale = str_replace( '_', '-', get_locale() );
/**
* Filters the Reader Revenue Manager product ID.
*
* @since 1.148.0
*
* @param string $product_id The array of post types.
*/
$product_id = apply_filters(
'googlesitekit_reader_revenue_manager_product_id',
$this->product_id
);
$subscription = array(
'type' => 'NewsArticle',
'isPartOfType' => array( 'Product' ),
'isPartOfProductId' => $this->tag_id . ':' . $product_id,
'clientOptions' => array(
'theme' => 'light',
'lang' => $locale,
),
);
$json_encoded_subscription = wp_json_encode( $subscription );
if ( ! $json_encoded_subscription ) {
$json_encoded_subscription = 'null';
}
$swg_inline_script = sprintf(
'(self.SWG_BASIC=self.SWG_BASIC||[]).push(basicSubscriptions=>{basicSubscriptions.init(%s);});',
$json_encoded_subscription
);
// phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion
wp_register_script( 'google_swgjs', 'https://news.google.com/swg/js/v1/swg-basic.js', array(), null, true );
wp_script_add_data( 'google_swgjs', 'strategy', 'async' );
wp_add_inline_script( 'google_swgjs', $swg_inline_script, 'before' );
wp_enqueue_script( 'google_swgjs' );
}
/**
* Add snippet comments around the tag.
*
* @since 1.132.0
*
* @param string $tag The tag.
* @param string $handle The script handle.
*
* @return string The tag with snippet comments.
*/
protected function add_snippet_comments( $tag, $handle ) {
if ( 'google_swgjs' !== $handle ) {
return $tag;
}
$before = sprintf( "\n<!-- %s -->\n", esc_html__( 'Google Reader Revenue Manager snippet added by Site Kit', 'google-site-kit' ) );
$after = sprintf( "\n<!-- %s -->\n", esc_html__( 'End Google Reader Revenue Manager snippet added by Site Kit', 'google-site-kit' ) );
return $before . $tag . $after;
}
/**
* Outputs snippet.
*
* @since 1.132.0
*/
protected function render() {
// Do nothing, script is enqueued.
}
}

View File

@@ -0,0 +1,619 @@
<?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\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_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\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\Modules\Module_With_Data_Available_State;
use Google\Site_Kit\Core\Modules\Module_With_Data_Available_State_Trait;
use Google\Site_Kit\Core\Permissions\Permissions;
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\Util\Date;
use Google\Site_Kit\Core\Util\Google_URL_Matcher_Trait;
use Google\Site_Kit\Core\Util\Google_URL_Normalizer;
use Google\Site_Kit\Core\Util\Sort;
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_Scopes, Module_With_Settings, Module_With_Assets, Module_With_Debug_Fields, Module_With_Owner, Module_With_Service_Entity, Module_With_Data_Available_State {
use Module_With_Scopes_Trait;
use Module_With_Settings_Trait;
use Google_URL_Matcher_Trait;
use Module_With_Assets_Trait;
use Module_With_Owner_Trait;
use Module_With_Data_Available_State_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();
// Detect and store Search Console property when receiving token for the first time.
add_action(
'googlesitekit_authorize_user',
function ( array $token_response ) {
if ( ! current_user_can( Permissions::SETUP ) ) {
return;
}
// If the response includes the Search Console property, set that.
// But only if it is being set for the first time or if Search Console
// has no owner or the current user is the owner.
if ( ! empty( $token_response['search_console_property'] ) &&
( empty( $this->get_property_id() ) || ( in_array( $this->get_owner_id(), array( 0, get_current_user_id() ), true ) ) ) ) {
$this->get_settings()->merge(
array( 'propertyID' => $token_response['search_console_property'] )
);
return;
}
// Otherwise try to detect if there isn't one set already.
$property_id = $this->get_property_id() ?: $this->detect_property_id();
if ( ! $property_id ) {
return;
}
$this->get_settings()->merge(
array( 'propertyID' => $property_id )
);
}
);
// Ensure that the data available state is reset when the property changes.
$this->get_settings()->on_change(
function ( $old_value, $new_value ) {
if (
is_array( $old_value ) &&
is_array( $new_value ) &&
isset( array_diff_assoc( $new_value, $old_value )['propertyID'] )
) {
$this->reset_data_available();
}
}
);
// 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' => true,
),
'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 ) = Date::parse_date_range( 'last-28-days', 1, 1 );
}
$data_request = array(
'start_date' => $start_date,
'end_date' => $end_date,
);
if ( ! empty( $data['url'] ) ) {
$data_request['page'] = ( new Google_URL_Normalizer() )->normalize_url( $data['url'] );
}
if ( isset( $data['limit'] ) ) {
$data_request['row_limit'] = $data['limit'];
}
$dimensions = $this->parse_string_list( $data['dimensions'] );
if ( is_array( $dimensions ) && ! empty( $dimensions ) ) {
$data_request['dimensions'] = $dimensions;
}
return $this->create_search_analytics_data_request( $data_request );
case 'POST:site':
if ( empty( $data['siteURL'] ) ) {
return new WP_Error(
'missing_required_param',
/* translators: %s: Missing parameter name */
sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'siteURL' ),
array( 'status' => 400 )
);
}
$url_normalizer = new Google_URL_Normalizer();
$site_url = $data['siteURL'];
if ( 0 === strpos( $site_url, 'sc-domain:' ) ) { // Domain property.
$site_url = 'sc-domain:' . $url_normalizer->normalize_url( str_replace( 'sc-domain:', '', $site_url, 1 ) );
} else { // URL property.
$site_url = $url_normalizer->normalize_url( trailingslashit( $site_url ) );
}
return function () use ( $site_url ) {
$restore_defer = $this->with_client_defer( false );
try {
// If the site does not exist in the account, an exception will be thrown.
$site = $this->get_searchconsole_service()->sites->get( $site_url );
} catch ( Google_Service_Exception $exception ) {
// If we got here, the site does not exist in the account, so we will add it.
/* @var ResponseInterface $response Response object. */
$response = $this->get_searchconsole_service()->sites->add( $site_url );
if ( 204 !== $response->getStatusCode() ) {
return new WP_Error(
'failed_to_add_site_to_search_console',
__( 'Error adding the site to Search Console.', 'google-site-kit' ),
array( 'status' => 500 )
);
}
// Fetch the site again now that it exists.
$site = $this->get_searchconsole_service()->sites->get( $site_url );
}
$restore_defer();
$this->get_settings()->merge( array( 'propertyID' => $site_url ) );
return array(
'siteURL' => $site->getSiteUrl(),
'permissionLevel' => $site->getPermissionLevel(),
);
};
case 'GET:sites':
return $this->get_searchconsole_service()->sites->listSites();
}
return parent::create_data_request( $data );
}
/**
* Parses a response for the given datapoint.
*
* @since 1.0.0
*
* @param Data_Request $data Data request object.
* @param mixed $response Request response.
*
* @return mixed Parsed response data on success, or WP_Error on failure.
*/
protected function parse_data_response( Data_Request $data, $response ) {
switch ( "{$data->method}:{$data->datapoint}" ) {
case 'GET:matched-sites':
/* @var Google_Service_SearchConsole_SitesListResponse $response Response object. */
$entries = Sort::case_insensitive_list_sort(
$this->map_sites( (array) $response->getSiteEntry() ),
'siteURL' // Must match the mapped value.
);
$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-datastore-user',
'googlesitekit-modules',
'googlesitekit-components',
'googlesitekit-modules-data',
),
)
),
);
}
/**
* 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,79 @@
<?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_option ) {
if ( ! is_array( $default_option ) ) {
$default_option = $this->get_default();
}
$default_option['propertyID'] = $this->options->get( 'googlesitekit_search_console_property' ) ?: '';
return $default_option;
}
);
}
/**
* 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,930 @@
<?php
/**
* Class Google\Site_Kit\Modules\Sign_In_With_Google
*
* @package Google\Site_Kit
* @copyright 2024 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\Context;
use Google\Site_Kit\Core\Assets\Asset;
use Google\Site_Kit\Core\Assets\Assets;
use Google\Site_Kit\Core\Assets\Script;
use Google\Site_Kit\Core\Assets\Stylesheet;
use Google\Site_Kit\Core\Authentication\Authentication;
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_Debug_Fields;
use Google\Site_Kit\Core\Modules\Module_With_Inline_Data;
use Google\Site_Kit\Core\Modules\Module_With_Inline_Data_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_Tag;
use Google\Site_Kit\Core\Modules\Module_With_Tag_Trait;
use Google\Site_Kit\Core\Modules\Tags\Module_Tag_Matchers;
use Google\Site_Kit\Core\REST_API\REST_Routes;
use Google\Site_Kit\Core\Site_Health\Debug_Data;
use Google\Site_Kit\Core\Storage\Options;
use Google\Site_Kit\Core\Storage\User_Options;
use Google\Site_Kit\Core\Tracking\Feature_Metrics_Trait;
use Google\Site_Kit\Core\Tracking\Provides_Feature_Metrics;
use Google\Site_Kit\Core\Util\BC_Functions;
use Google\Site_Kit\Core\Util\Method_Proxy_Trait;
use Google\Site_Kit\Modules\Sign_In_With_Google\Authenticator;
use Google\Site_Kit\Modules\Sign_In_With_Google\Authenticator_Interface;
use Google\Site_Kit\Modules\Sign_In_With_Google\Existing_Client_ID;
use Google\Site_Kit\Modules\Sign_In_With_Google\Hashed_User_ID;
use Google\Site_Kit\Modules\Sign_In_With_Google\Profile_Reader;
use Google\Site_Kit\Modules\Sign_In_With_Google\Settings;
use Google\Site_Kit\Modules\Sign_In_With_Google\Sign_In_With_Google_Block;
use Google\Site_Kit\Modules\Sign_In_With_Google\Tag_Guard;
use Google\Site_Kit\Modules\Sign_In_With_Google\Tag_Matchers;
use Google\Site_Kit\Modules\Sign_In_With_Google\Web_Tag;
use Google\Site_Kit\Modules\Sign_In_With_Google\WooCommerce_Authenticator;
use Google\Site_Kit\Modules\Sign_In_With_Google\Compatibility_Checks\Compatibility_Checks;
use Google\Site_Kit\Modules\Sign_In_With_Google\Compatibility_Checks\WP_Login_Accessible_Check;
use Google\Site_Kit\Modules\Sign_In_With_Google\Compatibility_Checks\WP_COM_Check;
use Google\Site_Kit\Modules\Sign_In_With_Google\Compatibility_Checks\Conflicting_Plugins_Check;
use Google\Site_Kit\Modules\Sign_In_With_Google\Datapoint\Compatibility_Checks as Compatibility_Checks_Datapoint;
use WP_Error;
use WP_User;
/**
* Class representing the Sign in with Google module.
*
* @since 1.137.0
* @access private
* @ignore
*/
final class Sign_In_With_Google extends Module implements Module_With_Inline_Data, Module_With_Assets, Module_With_Settings, Module_With_Deactivation, Module_With_Debug_Fields, Module_With_Tag, Provides_Feature_Metrics {
use Method_Proxy_Trait;
use Module_With_Assets_Trait;
use Module_With_Settings_Trait;
use Module_With_Tag_Trait;
use Module_With_Inline_Data_Trait;
use Feature_Metrics_Trait;
/**
* Module slug name.
*/
const MODULE_SLUG = 'sign-in-with-google';
/**
* Authentication action name.
*/
const ACTION_AUTH = 'googlesitekit_auth';
/**
* Disconnect action name.
*/
const ACTION_DISCONNECT = 'googlesitekit_auth_disconnect';
/**
* Existing_Client_ID instance.
*
* @since 1.142.0
* @var Existing_Client_ID
*/
protected $existing_client_id;
/**
* Sign in with Google Block instance.
*
* @since 1.147.0
* @var Sign_In_With_Google_Block
*/
protected $sign_in_with_google_block;
/**
* Stores the active state of the WooCommerce plugin.
*
* @since 1.148.0
* @var bool Whether WooCommerce is active or not.
*/
protected $is_woocommerce_active;
/**
* Constructor.
*
* @since 1.142.0
*
* @param Context $context Plugin context.
* @param Options $options Optional. Option API instance. Default is a new instance.
* @param User_Options $user_options Optional. User Option API instance. Default is a new instance.
* @param Authentication $authentication Optional. Authentication instance. Default is a new instance.
* @param Assets $assets Optional. Assets API instance. Default is a new instance.
*/
public function __construct(
Context $context,
?Options $options = null,
?User_Options $user_options = null,
?Authentication $authentication = null,
?Assets $assets = null
) {
parent::__construct( $context, $options, $user_options, $authentication, $assets );
$this->existing_client_id = new Existing_Client_ID( $this->options );
$this->sign_in_with_google_block = new Sign_In_With_Google_Block( $this->context );
}
/**
* Registers functionality through WordPress hooks.
*
* @since 1.137.0
* @since 1.141.0 Add functionality to allow users to disconnect their own account and admins to disconnect any user.
*/
public function register() {
$this->register_inline_data();
$this->register_feature_metrics();
add_filter( 'wp_login_errors', array( $this, 'handle_login_errors' ) );
add_action( 'googlesitekit_render_sign_in_with_google_button', array( $this, 'render_sign_in_with_google_button' ), 10, 1 );
// Add support for a shortcode to render the Sign in with Google button.
add_shortcode( 'site_kit_sign_in_with_google', array( $this, 'render_siwg_shortcode' ) );
add_action(
'login_form_' . self::ACTION_AUTH,
function () {
$settings = $this->get_settings();
$profile_reader = new Profile_Reader( $settings );
$integration = $this->context->input()->filter( INPUT_POST, 'integration' );
$authenticator_class = Authenticator::class;
if ( 'woocommerce' === $integration && class_exists( 'woocommerce' ) ) {
$authenticator_class = WooCommerce_Authenticator::class;
}
$this->handle_auth_callback( new $authenticator_class( $this->user_options, $profile_reader ) );
}
);
add_action( 'admin_action_' . self::ACTION_DISCONNECT, array( $this, 'handle_disconnect_user' ) );
add_action( 'show_user_profile', $this->get_method_proxy( 'render_disconnect_profile' ) ); // This action shows the disconnect section on the users own profile page.
add_action( 'edit_user_profile', $this->get_method_proxy( 'render_disconnect_profile' ) ); // This action shows the disconnect section on other users profile page to allow admins to disconnect others.
// Output the Sign in with Google <div> in the WooCommerce login form.
add_action( 'woocommerce_login_form_start', $this->get_method_proxy( 'render_signinwithgoogle_woocommerce' ) );
// Output the Sign in with Google <div> in any use of wp_login_form.
add_filter( 'login_form_top', $this->get_method_proxy( 'render_button_in_wp_login_form' ) );
// Delete client ID stored from previous module connection on module reconnection.
add_action(
'googlesitekit_save_settings_' . self::MODULE_SLUG,
function () {
if ( $this->is_connected() ) {
$this->existing_client_id->delete();
}
}
);
add_action( 'woocommerce_before_customer_login_form', array( $this, 'handle_woocommerce_errors' ), 1 );
// Sign in with Google tag placement logic.
add_action( 'template_redirect', array( $this, 'register_tag' ) );
// Used to add the tag registration to the login footer in
// `/wp-login.php`, which doesn't use the `template_redirect` action
// like most WordPress pages.
add_action( 'login_init', array( $this, 'register_tag' ) );
// Place Sign in with Google button next to comments form if the
// setting is enabled.
add_action( 'comment_form_after_fields', array( $this, 'handle_comments_form' ) );
// Add the Sign in with Google compatibility checks datapoint to our
// preloaded paths.
add_filter(
'googlesitekit_apifetch_preload_paths',
function ( $paths ) {
return array_merge(
$paths,
array(
'/' . REST_Routes::REST_ROOT . '/modules/sign-in-with-google/data/compatibility-checks',
)
);
}
);
// Check to see if the module is connected before registering the block.
if ( $this->is_connected() ) {
$this->sign_in_with_google_block->register();
}
}
/**
* Handles the callback request after the user signs in with Google.
*
* @since 1.140.0
*
* @param Authenticator_Interface $authenticator Authenticator instance.
*/
private function handle_auth_callback( Authenticator_Interface $authenticator ) {
$input = $this->context->input();
// Ignore the request if the request method is not POST.
$request_method = $input->filter( INPUT_SERVER, 'REQUEST_METHOD' );
if ( 'POST' !== $request_method ) {
return;
}
$redirect_to = $authenticator->authenticate_user( $input );
if ( ! empty( $redirect_to ) ) {
wp_safe_redirect( $redirect_to );
exit;
}
}
/**
* Conditionally show the Sign in with Google button in a comments form.
*
* @since 1.165.0
*/
public function handle_comments_form() {
$settings = $this->get_settings()->get();
$anyone_can_register = (bool) get_option( 'users_can_register' );
// Only show the button if:
// - the comments form setting is enabled
// - open user registration is enabled
//
// If the comments form setting is not enabled, do nothing.
if ( empty( $settings['showNextToCommentsEnabled'] ) || ! $anyone_can_register ) {
return;
}
// Output the post ID to allow identitifying the post for this comment.
$post_id = get_the_ID();
// Output the Sign in with Google button in the comments form.
do_action(
'googlesitekit_render_sign_in_with_google_button',
array(
'class' => array(
'googlesitekit-sign-in-with-google__comments-form-button',
"googlesitekit-sign-in-with-google__comments-form-button-postid-{$post_id}",
),
)
);
}
/**
* Adds custom errors if Google auth flow failed.
*
* @since 1.140.0
*
* @param WP_Error $error WP_Error instance.
* @return WP_Error $error WP_Error instance.
*/
public function handle_login_errors( $error ) {
$error_code = $this->context->input()->filter( INPUT_GET, 'error' );
if ( ! $error_code ) {
return $error;
}
switch ( $error_code ) {
case Authenticator::ERROR_INVALID_REQUEST:
/* translators: %s: Sign in with Google service name */
$error->add( self::MODULE_SLUG, sprintf( __( 'Login with %s failed.', 'google-site-kit' ), _x( 'Sign in with Google', 'Service name', 'google-site-kit' ) ) );
break;
case Authenticator::ERROR_SIGNIN_FAILED:
$error->add( self::MODULE_SLUG, __( 'The user is not registered on this site.', 'google-site-kit' ) );
break;
default:
break;
}
return $error;
}
/**
* Adds custom errors if Google auth flow failed on WooCommerce login.
*
* @since 1.145.0
*/
public function handle_woocommerce_errors() {
$err = $this->handle_login_errors( new WP_Error() );
if ( is_wp_error( $err ) && $err->has_errors() ) {
wc_add_notice( $err->get_error_message(), 'error' );
}
}
/**
* Cleans up when the module is deactivated.
*
* Persist the clientID on module disconnection, so it can be
* reused if the module were to be reconnected.
*
* @since 1.137.0
*/
public function on_deactivation() {
$pre_deactivation_settings = $this->get_settings()->get();
if ( ! empty( $pre_deactivation_settings['clientID'] ) ) {
$this->existing_client_id->set( $pre_deactivation_settings['clientID'] );
}
$this->get_settings()->delete();
}
/**
* Sets up information about the module.
*
* @since 1.137.0
*
* @return array Associative array of module info.
*/
protected function setup_info() {
return array(
'slug' => self::MODULE_SLUG,
'name' => _x( 'Sign in with Google', 'Service name', 'google-site-kit' ),
'description' => __( 'Improve user engagement, trust and data privacy, while creating a simple, secure and personalized experience for your visitors', 'google-site-kit' ),
'homepage' => __( 'https://developers.google.com/identity/gsi/web/guides/overview', 'google-site-kit' ),
);
}
/**
* Sets up the module's assets to register.
*
* @since 1.137.0
*
* @return Asset[] List of Asset objects.
*/
protected function setup_assets() {
$assets = array(
new Script(
'googlesitekit-modules-sign-in-with-google',
array(
'src' => $this->context->url( 'dist/assets/js/googlesitekit-modules-sign-in-with-google.js' ),
'dependencies' => array(
'googlesitekit-vendor',
'googlesitekit-api',
'googlesitekit-data',
'googlesitekit-modules',
'googlesitekit-notifications',
'googlesitekit-datastore-site',
'googlesitekit-datastore-user',
'googlesitekit-components',
),
)
),
);
if ( Sign_In_With_Google_Block::can_register() && $this->is_connected() ) {
$assets[] = new Script(
'blocks-sign-in-with-google',
array(
'src' => $this->context->url( 'dist/assets/blocks/sign-in-with-google/index.js' ),
'dependencies' => array(),
'load_contexts' => array( Asset::CONTEXT_ADMIN_POST_EDITOR ),
)
);
$assets[] = new Stylesheet(
'blocks-sign-in-with-google-editor-styles',
array(
'src' => $this->context->url( 'dist/assets/blocks/sign-in-with-google/editor-styles.css' ),
'dependencies' => array(),
'load_contexts' => array( Asset::CONTEXT_ADMIN_POST_EDITOR ),
)
);
}
return $assets;
}
/**
* Sets up the module's settings instance.
*
* @since 1.137.0
*
* @return Settings
*/
protected function setup_settings() {
return new Settings( $this->options );
}
/**
* Checks whether the module is connected.
*
* A module being connected means that all steps required as part of its activation are completed.
*
* @since 1.139.0
*
* @return bool True if module is connected, false otherwise.
*/
public function is_connected() {
$options = $this->get_settings()->get();
if ( empty( $options['clientID'] ) ) {
return false;
}
return parent::is_connected();
}
/**
* Gets the datapoint definitions for the module.
*
* @since 1.164.0
*
* @return array List of datapoint definitions.
*/
protected function get_datapoint_definitions() {
$checks = new Compatibility_Checks();
$checks->add_check( new WP_Login_Accessible_Check() );
$checks->add_check( new WP_COM_Check() );
$checks->add_check( new Conflicting_Plugins_Check() );
return array(
'GET:compatibility-checks' => new Compatibility_Checks_Datapoint( array( 'checks' => $checks ) ),
);
}
/**
* Renders the placeholder Sign in with Google div for the WooCommerce
* login form.
*
* @since 1.147.0
*/
private function render_signinwithgoogle_woocommerce() {
/**
* Only render the button in a WooCommerce login page if:
*
* - the Sign in with Google module is connected
* - the user is not logged in
*/
if ( ! $this->is_connected() || is_user_logged_in() ) {
return;
}
/**
* Display the Sign in with Google button.
*
* @since 1.164.0
*
* @param array $args Optional arguments to customize button attributes.
*/
do_action(
'googlesitekit_render_sign_in_with_google_button',
array(
'class' => 'woocommerce-form-row form-row',
)
);
}
/**
* Checks if the Sign in with Google button can be rendered.
*
* @since 1.149.0
*
* @return bool True if the button can be rendered, false otherwise.
*/
private function can_render_signinwithgoogle() {
$settings = $this->get_settings()->get();
// If there's no client ID available, don't render the button.
if ( ! $settings['clientID'] ) {
return false;
}
if ( substr( wp_login_url(), 0, 5 ) !== 'https' ) {
return false;
}
return true;
}
/**
* Appends the Sign in with Google button to content of a WordPress filter.
*
* @since 1.149.0
*
* @param string $content Existing content.
* @return string Possibly modified content.
*/
private function render_button_in_wp_login_form( $content ) {
if ( $this->can_render_signinwithgoogle() ) {
ob_start();
/**
* Display the Sign in with Google button.
*
* @since 1.164.0
*
* @param array $args Optional arguments to customize button attributes.
*/
do_action( 'googlesitekit_render_sign_in_with_google_button' );
$content .= ob_get_clean();
}
return $content;
}
/**
* Renders the Sign in with Google button markup.
*
* @since 1.164.0
*
* @param array $args Optional arguments to customize button attributes.
*/
public function render_sign_in_with_google_button( $args = array() ) {
if ( ! is_array( $args ) ) {
$args = array();
}
$default_classes = array( 'googlesitekit-sign-in-with-google__frontend-output-button' );
$classes_from_args = array();
if ( ! empty( $args['class'] ) ) {
$classes_from_args = is_array( $args['class'] ) ? $args['class'] : preg_split( '/\s+/', (string) $args['class'] );
}
// Merge default HTML class names and class names passed as arguments
// to the action, then sanitize each class name.
$merged_classes = array_merge( $default_classes, $classes_from_args );
$sanitized_classes = array_map( 'sanitize_html_class', $merged_classes );
// Remove duplicates, empty values, and reindex array.
$classes = array_values( array_unique( array_filter( $sanitized_classes ) ) );
$attributes = array(
// HTML class attribute should be a string.
'class' => implode( ' ', $classes ),
);
$data_attributes = array( 'for-comment-form', 'post-id', 'shape', 'text', 'theme' );
foreach ( $data_attributes as $attribute ) {
if ( empty( $args[ $attribute ] ) || ! is_scalar( $args[ $attribute ] ) ) {
continue;
}
$attributes[ 'data-googlesitekit-siwg-' . strtolower( $attribute ) ] = (string) $args[ $attribute ];
}
$attribute_strings = array();
foreach ( $attributes as $key => $value ) {
$attribute_strings[] = sprintf( '%s="%s"', $key, esc_attr( $value ) );
}
echo '<div ' . implode( ' ', $attribute_strings ) . '></div>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
/**
* Renders the Sign in with Google button for shortcode usage.
*
* This method captures the Sign in with Google button output
* and returns it as a string for use in shortcodes.
*
* @since 1.165.0
*
* @param array $atts Shortcode attributes.
* @return string The rendered button markup.
*/
public function render_siwg_shortcode( $atts ) {
$args = shortcode_atts(
array(
'class' => '',
'shape' => '',
'text' => '',
'theme' => '',
),
$atts,
'site_kit_sign_in_with_google'
);
// Remove empty attributes.
$args = array_filter( $args );
ob_start();
do_action( 'googlesitekit_render_sign_in_with_google_button', $args );
$markup = ob_get_clean();
return $markup;
}
/**
* Gets the absolute number of users who have authenticated using Sign in with Google.
*
* @since 1.140.0
*
* @return int
*/
public function get_authenticated_users_count() {
global $wpdb;
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
return (int) $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT( user_id ) FROM $wpdb->usermeta WHERE meta_key = %s",
$this->user_options->get_meta_key( Hashed_User_ID::OPTION )
)
);
}
/**
* Gets an array of debug field definitions.
*
* @since 1.140.0
*
* @return array
*/
public function get_debug_fields() {
$settings = $this->get_settings()->get();
$authenticated_user_count = $this->get_authenticated_users_count();
$debug_fields = array(
'sign_in_with_google_client_id' => array(
/* translators: %s: Sign in with Google service name */
'label' => sprintf( __( '%s: Client ID', 'google-site-kit' ), _x( 'Sign in with Google', 'Service name', 'google-site-kit' ) ),
'value' => $settings['clientID'],
'debug' => Debug_Data::redact_debug_value( $settings['clientID'] ),
),
'sign_in_with_google_shape' => array(
/* translators: %s: Sign in with Google service name */
'label' => sprintf( __( '%s: Shape', 'google-site-kit' ), _x( 'Sign in with Google', 'Service name', 'google-site-kit' ) ),
'value' => $this->get_settings()->get_label( 'shape', $settings['shape'] ),
'debug' => $settings['shape'],
),
'sign_in_with_google_text' => array(
/* translators: %s: Sign in with Google service name */
'label' => sprintf( __( '%s: Text', 'google-site-kit' ), _x( 'Sign in with Google', 'Service name', 'google-site-kit' ) ),
'value' => $this->get_settings()->get_label( 'text', $settings['text'] ),
'debug' => $settings['text'],
),
'sign_in_with_google_theme' => array(
/* translators: %s: Sign in with Google service name */
'label' => sprintf( __( '%s: Theme', 'google-site-kit' ), _x( 'Sign in with Google', 'Service name', 'google-site-kit' ) ),
'value' => $this->get_settings()->get_label( 'theme', $settings['theme'] ),
'debug' => $settings['theme'],
),
'sign_in_with_google_use_snippet' => array(
/* translators: %s: Sign in with Google service name */
'label' => sprintf( __( '%s: One Tap Enabled', 'google-site-kit' ), _x( 'Sign in with Google', 'Service name', 'google-site-kit' ) ),
'value' => $settings['oneTapEnabled'] ? __( 'Yes', 'google-site-kit' ) : __( 'No', 'google-site-kit' ),
'debug' => $settings['oneTapEnabled'] ? 'yes' : 'no',
),
'sign_in_with_google_comments' => array(
/* translators: %s: Sign in with Google service name */
'label' => sprintf( __( '%s: Show next to comments', 'google-site-kit' ), _x( 'Sign in with Google', 'Service name', 'google-site-kit' ) ),
'value' => (bool) get_option( 'users_can_register' ) && $settings['showNextToCommentsEnabled'] ? __( 'Yes', 'google-site-kit' ) : __( 'No', 'google-site-kit' ),
'debug' => (bool) get_option( 'users_can_register' ) && $settings['showNextToCommentsEnabled'] ? 'yes' : 'no',
),
'sign_in_with_google_authenticated_user_count' => array(
/* translators: %1$s: Sign in with Google service name */
'label' => sprintf( __( '%1$s: Number of users who have authenticated using %1$s', 'google-site-kit' ), _x( 'Sign in with Google', 'Service name', 'google-site-kit' ) ),
'value' => $authenticated_user_count,
'debug' => $authenticated_user_count,
),
);
return $debug_fields;
}
/**
* Registers the Sign in with Google tag.
*
* @since 1.159.0
*/
public function register_tag() {
$settings = $this->get_settings()->get();
$client_id = $settings['clientID'];
$tag = new Web_Tag( $client_id, self::MODULE_SLUG );
if ( $tag->is_tag_blocked() ) {
return;
}
$tag->use_guard( new Tag_Guard( $this->get_settings() ) );
if ( ! $tag->can_register() ) {
return;
}
$tag->set_settings( $this->get_settings()->get() );
$tag->set_is_wp_login( false !== stripos( wp_login_url(), $_SERVER['SCRIPT_NAME'] ?? '' ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
$tag->set_redirect_to( $this->context->input()->filter( INPUT_GET, 'redirect_to' ) );
$tag->register();
}
/**
* Returns the Module_Tag_Matchers instance.
*
* @since 1.140.0
*
* @return Module_Tag_Matchers Module_Tag_Matchers instance.
*/
public function get_tag_matchers() {
return new Tag_Matchers();
}
/**
* Gets the URL of the page(s) where a tag for the module would be placed.
*
* For all modules like Analytics, Tag Manager, AdSense, Ads, etc. except for
* Sign in with Google, tags can be detected on the home page. SiwG places its
* snippet on the login page and thus, overrides this method.
*
* @since 1.140.0
*
* @return string|array
*/
public function get_content_url() {
$wp_login_url = wp_login_url();
if ( $this->is_woocommerce_active() ) {
$wc_login_page_id = wc_get_page_id( 'myaccount' );
$wc_login_url = get_permalink( $wc_login_page_id );
return array(
'WordPress Login Page' => $wp_login_url,
'WooCommerce Login Page' => $wc_login_url,
);
}
return $wp_login_url;
}
/**
* Checks if the Sign in with Google button, specifically inserted by Site Kit,
* is found in the provided content.
*
* This method overrides the `Module_With_Tag_Trait` implementation since the HTML
* comment inserted for SiwG's button is different to the standard comment inserted
* for other modules' script snippets. This should be improved as speicified in the
* TODO within the trait method.
*
* @since 1.140.0
*
* @param string $content Content to search for the button.
* @return bool TRUE if tag is found, FALSE if not.
*/
public function has_placed_tag_in_content( $content ) {
$search_string = 'Sign in with Google button added by Site Kit';
$search_translatable_string =
/* translators: %s: Sign in with Google service name */
sprintf( __( '%s button added by Site Kit', 'google-site-kit' ), _x( 'Sign in with Google', 'Service name', 'google-site-kit' ) );
if ( strpos( $content, $search_string ) !== false || strpos( $content, $search_translatable_string ) !== false ) {
return Module_Tag_Matchers::TAG_EXISTS_WITH_COMMENTS;
}
return Module_Tag_Matchers::NO_TAG_FOUND;
}
/**
* Returns the disconnect URL for the specified user.
*
* @since 1.141.0
*
* @param int $user_id WordPress User ID.
*/
public static function disconnect_url( $user_id ) {
return add_query_arg(
array(
'action' => self::ACTION_DISCONNECT,
'nonce' => wp_create_nonce( self::ACTION_DISCONNECT . '-' . $user_id ),
'user_id' => $user_id,
),
admin_url( 'index.php' )
);
}
/**
* Handles the disconnect action.
*
* @since 1.141.0
*/
public function handle_disconnect_user() {
$input = $this->context->input();
$nonce = $input->filter( INPUT_GET, 'nonce' );
$user_id = (int) $input->filter( INPUT_GET, 'user_id' );
$action = self::ACTION_DISCONNECT . '-' . $user_id;
if ( ! wp_verify_nonce( $nonce, $action ) ) {
$this->authentication->invalid_nonce_error( $action );
}
// Only allow this action for admins or users own setting.
if ( current_user_can( 'edit_user', $user_id ) ) {
$hashed_user_id = new Hashed_User_ID( new User_Options( $this->context, $user_id ) );
$hashed_user_id->delete();
wp_safe_redirect( add_query_arg( 'updated', true, get_edit_user_link( $user_id ) ) );
exit;
}
wp_safe_redirect( get_edit_user_link( $user_id ) );
exit;
}
/**
* Displays a disconnect button on user profile pages.
*
* @since 1.141.0
*
* @param WP_User $user WordPress user object.
*/
private function render_disconnect_profile( WP_User $user ) {
if ( ! current_user_can( 'edit_user', $user->ID ) ) {
return;
}
$hashed_user_id = new Hashed_User_ID( new User_Options( $this->context, $user->ID ) );
$current_user_google_id = $hashed_user_id->get();
// Don't show if the user does not have a Google ID saved in user meta.
if ( empty( $current_user_google_id ) ) {
return;
}
?>
<div id="googlesitekit-sign-in-with-google-disconnect">
<h2>
<?php
/* translators: %1$s: Sign in with Google service name, %2$s: Plugin name */
echo esc_html( sprintf( __( '%1$s (via %2$s)', 'google-site-kit' ), _x( 'Sign in with Google', 'Service name', 'google-site-kit' ), __( 'Site Kit by Google', 'google-site-kit' ) ) );
?>
</h2>
<p>
<?php
if ( get_current_user_id() === $user->ID ) {
esc_html_e(
'You can sign in with your Google account.',
'google-site-kit'
);
} else {
esc_html_e(
'This user can sign in with their Google account.',
'google-site-kit'
);
}
?>
</p>
<p>
<a class="button button-secondary" href="<?php echo esc_url( self::disconnect_url( $user->ID ) ); ?>">
<?php esc_html_e( 'Disconnect Google Account', 'google-site-kit' ); ?>
</a>
</p>
</div>
<?php
}
/**
* Gets required inline data for the module.
*
* @since 1.142.0
* @since 1.146.0 Added isWooCommerceActive and isWooCommerceRegistrationEnabled to the inline data.
* @since 1.158.0 Renamed method to `get_inline_data()`, and modified it to return a new array rather than populating a passed filter value.
*
* @param array $modules_data Inline modules data.
* @return array An array of the module's inline data.
*/
public function get_inline_data( $modules_data ) {
$inline_data = array();
$existing_client_id = $this->existing_client_id->get();
if ( $existing_client_id ) {
$inline_data['existingClientID'] = $existing_client_id;
}
$is_woocommerce_active = $this->is_woocommerce_active();
$woocommerce_registration_enabled = $is_woocommerce_active ? get_option( 'woocommerce_enable_myaccount_registration' ) : null;
$inline_data['isWooCommerceActive'] = $is_woocommerce_active;
$inline_data['isWooCommerceRegistrationEnabled'] = $is_woocommerce_active && 'yes' === $woocommerce_registration_enabled;
$modules_data[ self::MODULE_SLUG ] = $inline_data;
return $modules_data;
}
/**
* Helper method to determine if the WooCommerce plugin is active.
*
* @since 1.148.0
*
* @return bool True if active, false if not.
*/
protected function is_woocommerce_active() {
return class_exists( 'WooCommerce' );
}
/**
* Gets an array of internal feature metrics.
*
* @since 1.165.0
*
* @return array
*/
public function get_feature_metrics() {
return array(
'siwg_onetap' => $this->get_settings()->get()['oneTapEnabled'] ? 1 : 0,
);
}
}

View File

@@ -0,0 +1,350 @@
<?php
/**
* Class Google\Site_Kit\Modules\Sign_In_With_Google\Authenticator
*
* @package Google\Site_Kit\Modules\Sign_In_With_Google
* @copyright 2024 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\Sign_In_With_Google;
use Google\Site_Kit\Core\Storage\User_Options;
use Google\Site_Kit\Core\Util\Input;
use WP_Error;
use WP_User;
/**
* The authenticator class that processes SiwG callback requests to authenticate users.
*
* @since 1.141.0
* @access private
* @ignore
*/
class Authenticator implements Authenticator_Interface {
/**
* Cookie name to store the redirect URL before the user signs in with Google.
*/
const COOKIE_REDIRECT_TO = 'googlesitekit_auth_redirect_to';
/**
* Error codes.
*/
const ERROR_INVALID_REQUEST = 'googlesitekit_auth_invalid_request';
const ERROR_SIGNIN_FAILED = 'googlesitekit_auth_failed';
/**
* User options instance.
*
* @since 1.141.0
* @var User_Options
*/
private $user_options;
/**
* Profile reader instance.
*
* @since 1.141.0
* @var Profile_Reader_Interface
*/
private $profile_reader;
/**
* Constructor.
*
* @since 1.141.0
*
* @param User_Options $user_options User options instance.
* @param Profile_Reader_Interface $profile_reader Profile reader instance.
*/
public function __construct( User_Options $user_options, Profile_Reader_Interface $profile_reader ) {
$this->user_options = $user_options;
$this->profile_reader = $profile_reader;
}
/**
* Authenticates the user using the provided input data.
*
* @since 1.141.0
*
* @param Input $input Input instance.
* @return string Redirect URL.
*/
public function authenticate_user( Input $input ) {
$credential = $input->filter( INPUT_POST, 'credential' );
$user = null;
$payload = $this->profile_reader->get_profile_data( $credential );
if ( ! is_wp_error( $payload ) ) {
$user = $this->find_user( $payload );
if ( ! $user instanceof WP_User ) {
// We haven't found the user using their Google user id and email. Thus we need to create
// a new user. But if the registration is closed, we need to return an error to identify
// that the sign in process failed.
if ( ! $this->is_registration_open() ) {
return $this->get_error_redirect_url( self::ERROR_SIGNIN_FAILED );
} else {
$user = $this->create_user( $payload );
}
}
}
// Redirect to the error page if the user is not found.
if ( is_wp_error( $user ) ) {
return $this->get_error_redirect_url( $user->get_error_code() );
} elseif ( ! $user instanceof WP_User ) {
return $this->get_error_redirect_url( self::ERROR_INVALID_REQUEST );
}
// Sign in the user.
$err = $this->sign_in_user( $user );
if ( is_wp_error( $err ) ) {
return $this->get_error_redirect_url( $err->get_error_code() );
}
return $this->get_redirect_url( $user, $input );
}
/**
* Gets the redirect URL for the error page.
*
* @since 1.145.0
*
* @param string $code Error code.
* @return string Redirect URL.
*/
protected function get_error_redirect_url( $code ) {
return add_query_arg( 'error', $code, wp_login_url() );
}
/**
* Gets the redirect URL after the user signs in with Google.
*
* @since 1.145.0
*
* @param WP_User $user User object.
* @param Input $input Input instance.
* @return string Redirect URL.
*/
protected function get_redirect_url( $user, $input ) {
// Use the admin dashboard URL as the redirect URL by default.
$redirect_to = admin_url();
// If we have the redirect URL in the cookie, use it as the main redirect_to URL.
$cookie_redirect_to = $this->get_cookie_redirect( $input );
if ( ! empty( $cookie_redirect_to ) ) {
$redirect_to = $cookie_redirect_to;
}
// Redirect to HTTPS if user wants SSL.
if ( get_user_option( 'use_ssl', $user->ID ) && str_contains( $redirect_to, 'wp-admin' ) ) {
$redirect_to = preg_replace( '|^http://|', 'https://', $redirect_to );
}
/** This filter is documented in wp-login.php */
$redirect_to = apply_filters( 'login_redirect', $redirect_to, $redirect_to, $user );
if ( ( empty( $redirect_to ) || 'wp-admin/' === $redirect_to || admin_url() === $redirect_to ) ) {
// If the user doesn't belong to a blog, send them to user admin. If the user can't edit posts, send them to their profile.
if ( is_multisite() && ! get_active_blog_for_user( $user->ID ) && ! is_super_admin( $user->ID ) ) {
$redirect_to = user_admin_url();
} elseif ( is_multisite() && ! $user->has_cap( 'read' ) ) {
$redirect_to = get_dashboard_url( $user->ID );
} elseif ( ! $user->has_cap( 'edit_posts' ) ) {
$redirect_to = $user->has_cap( 'read' ) ? admin_url( 'profile.php' ) : home_url();
}
}
return $redirect_to;
}
/**
* Signs in the user.
*
* @since 1.145.0
*
* @param WP_User $user User object.
* @return WP_Error|null WP_Error if an error occurred, null otherwise.
*/
protected function sign_in_user( $user ) {
// Redirect to the error page if the user is not a member of the current blog in multisite.
if ( is_multisite() ) {
$blog_id = get_current_blog_id();
if ( ! is_user_member_of_blog( $user->ID, $blog_id ) ) {
if ( $this->is_registration_open() ) {
add_user_to_blog( $blog_id, $user->ID, $this->get_default_role() );
} else {
return new WP_Error( self::ERROR_INVALID_REQUEST );
}
}
}
// Set the user to be the current user.
wp_set_current_user( $user->ID, $user->user_login );
// Set the authentication cookies and trigger the wp_login action.
wp_set_auth_cookie( $user->ID );
/** This filter is documented in wp-login.php */
do_action( 'wp_login', $user->user_login, $user );
return null;
}
/**
* Finds an existing user using the Google user ID and email.
*
* @since 1.145.0
*
* @param array $payload Google auth payload.
* @return WP_User|null User object if found, null otherwise.
*/
protected function find_user( $payload ) {
// Check if there are any existing WordPress users connected to this Google account.
// The user ID is used as the unique identifier because users can change the email on their Google account.
$g_user_hid = $this->get_hashed_google_user_id( $payload );
$users = get_users(
array(
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
'meta_key' => $this->user_options->get_meta_key( Hashed_User_ID::OPTION ),
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
'meta_value' => $g_user_hid,
'number' => 1,
)
);
if ( ! empty( $users ) ) {
return $users[0];
}
// Find an existing user that matches the email and link to their Google account by store their user ID in user meta.
$user = get_user_by( 'email', $payload['email'] );
if ( $user ) {
$user_options = clone $this->user_options;
$user_options->switch_user( $user->ID );
$user_options->set( Hashed_User_ID::OPTION, $g_user_hid );
return $user;
}
return null;
}
/**
* Create a new user using the Google auth payload.
*
* @since 1.145.0
*
* @param array $payload Google auth payload.
* @return WP_User|WP_Error User object if found or created, WP_Error otherwise.
*/
protected function create_user( $payload ) {
$g_user_hid = $this->get_hashed_google_user_id( $payload );
// Get the default role for new users.
$default_role = $this->get_default_role();
// Create a new user.
$user_id = wp_insert_user(
array(
'user_pass' => wp_generate_password( 64 ),
'user_login' => $payload['email'],
'user_email' => $payload['email'],
'display_name' => $payload['name'],
'first_name' => $payload['given_name'],
'last_name' => $payload['family_name'],
'role' => $default_role,
'meta_input' => array(
$this->user_options->get_meta_key( Hashed_User_ID::OPTION ) => $g_user_hid,
),
)
);
if ( is_wp_error( $user_id ) ) {
return new WP_Error( self::ERROR_SIGNIN_FAILED );
}
// Add the user to the current site if it is a multisite.
if ( is_multisite() ) {
add_user_to_blog( get_current_blog_id(), $user_id, $default_role );
}
// Send the new user notification.
wp_send_new_user_notifications( $user_id );
return get_user_by( 'id', $user_id );
}
/**
* Gets the hashed Google user ID from the provided payload.
*
* @since 1.145.0
*
* @param array $payload Google auth payload.
* @return string Hashed Google user ID.
*/
private function get_hashed_google_user_id( $payload ) {
return md5( $payload['sub'] );
}
/**
* Checks if the registration is open.
*
* @since 1.145.0
*
* @return bool True if registration is open, false otherwise.
*/
protected function is_registration_open() {
// No need to check the multisite settings because it is already
// incorporated in the following users_can_register check.
// See: https://github.com/WordPress/WordPress/blob/505b7c55f5363d51e7e28d512ce7dcb2d5f45894/wp-includes/ms-default-filters.php#L20.
return get_option( 'users_can_register' );
}
/**
* Gets the default role for new users.
*
* @since 1.141.0
* @since 1.145.0 Updated the function visibility to protected.
*
* @return string Default role.
*/
protected function get_default_role() {
$default_role = get_option( 'default_role' );
if ( empty( $default_role ) ) {
$default_role = 'subscriber';
}
return $default_role;
}
/**
* Gets the path for the redirect cookie.
*
* @since 1.141.0
*
* @return string Cookie path.
*/
public static function get_cookie_path() {
return dirname( wp_parse_url( wp_login_url(), PHP_URL_PATH ) );
}
/**
* Gets the redirect URL from the cookie and clears the cookie.
*
* @since 1.146.0
*
* @param Input $input Input instance.
* @return string Redirect URL.
*/
protected function get_cookie_redirect( $input ) {
$cookie_redirect_to = $input->filter( INPUT_COOKIE, self::COOKIE_REDIRECT_TO );
if ( ! empty( $cookie_redirect_to ) && ! headers_sent() ) {
// phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.cookies_setcookie
setcookie( self::COOKIE_REDIRECT_TO, '', time() - 3600, self::get_cookie_path(), COOKIE_DOMAIN );
}
return $cookie_redirect_to;
}
}

View File

@@ -0,0 +1,33 @@
<?php
/**
* Class Google\Site_Kit\Modules\Sign_In_With_Google\Authenticator_Interface
*
* @package Google\Site_Kit\Modules\Sign_In_With_Google
* @copyright 2024 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\Sign_In_With_Google;
use Google\Site_Kit\Core\Util\Input;
/**
* Defines methods that must be implemented by an authenticator class.
*
* @since 1.141.0
* @access private
* @ignore
*/
interface Authenticator_Interface {
/**
* Authenticates the user using the provided input data.
*
* @since 1.141.0
*
* @param Input $input Input instance.
* @return string Redirect URL.
*/
public function authenticate_user( Input $input );
}

View File

@@ -0,0 +1,37 @@
<?php
/**
* Class Google\Site_Kit\Modules\Sign_In_With_Google\Compatibility_Checks\Compatibility_Check
*
* @package Google\Site_Kit\Modules\Sign_In_With_Google
* @copyright 2025 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\Sign_In_With_Google\Compatibility_Checks;
/**
* Abstract base class for compatibility checks.
*
* @since 1.164.0
*/
abstract class Compatibility_Check {
/**
* Gets the unique slug for this compatibility check.
*
* @since 1.164.0
*
* @return string The unique slug for this compatibility check.
*/
abstract public function get_slug();
/**
* Runs the compatibility check.
*
* @since 1.164.0
*
* @return array The result of the compatibility check.
*/
abstract public function run();
}

View File

@@ -0,0 +1,60 @@
<?php
/**
* Class Google\Site_Kit\Modules\Sign_In_With_Google\Compatibility_Checks\Compatibility_Checks
*
* @package Google\Site_Kit\Modules\Sign_In_With_Google
* @copyright 2025 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\Sign_In_With_Google\Compatibility_Checks;
/**
* Manager class for compatibility checks.
*
* @since 1.164.0
*/
class Compatibility_Checks {
/**
* Collection of compatibility checks.
*
* @since 1.164.0
*
* @var array
*/
private $checks = array();
/**
* Adds a compatibility check to the collection.
*
* @since 1.164.0
*
* @param Compatibility_Check $check The compatibility check to add.
*/
public function add_check( Compatibility_Check $check ) {
$this->checks[] = $check;
}
/**
* Runs all compatibility checks.
*
* @since 1.164.0
*
* @return array Results of the compatibility checks.
*/
public function run_checks() {
$results = array();
foreach ( $this->checks as $check ) {
$result = $check->run();
if ( $result ) {
$results[ $check->get_slug() ] = $result;
}
}
return $results;
}
}

View File

@@ -0,0 +1,76 @@
<?php
/**
* Class Google\Site_Kit\Modules\Sign_In_With_Google\Compatibility_Checks\Conflicting_Plugins_Check
*
* @package Google\Site_Kit\Modules\Sign_In_With_Google
* @copyright 2025 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\Sign_In_With_Google\Compatibility_Checks;
/**
* Compatibility check for conflicting plugins.
*
* @since 1.164.0
*/
class Conflicting_Plugins_Check extends Compatibility_Check {
/**
* Gets the unique slug for this compatibility check.
*
* @since 1.164.0
*
* @return string The unique slug for this compatibility check.
*/
public function get_slug() {
return 'conflicting_plugins';
}
/**
* Runs the compatibility check.
*
* @since 1.164.0
*
* @return array|false Array of conflicting plugins data if found, false otherwise.
*/
public function run() {
$conflicting_plugins = array();
$active_plugins = get_option( 'active_plugins', array() );
$security_plugins = array(
'better-wp-security/better-wp-security.php',
'security-malware-firewall/security-malware-firewall.php',
'sg-security/sg-security.php',
'hide-my-wp/index.php',
'hide-wp-login/hide-wp-login.php',
'all-in-one-wp-security-and-firewall/wp-security.php',
'sucuri-scanner/sucuri.php',
'wordfence/wordfence.php',
'wps-hide-login/wps-hide-login.php',
);
foreach ( $active_plugins as $plugin_slug ) {
// If the plugin isn't in our array of known plugins with issues,
// try the next plugin slug in the list of active plugins
// (eg. "exit early").
if ( ! in_array( $plugin_slug, $security_plugins, true ) ) {
continue;
}
$plugin_data = get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin_slug );
$plugin_name = $plugin_data['Name'];
$conflicting_plugins[ $plugin_slug ] = array(
'pluginName' => $plugin_name,
'conflictMessage' => sprintf(
/* translators: %s: plugin name */
__( '%s may prevent Sign in with Google from working properly.', 'google-site-kit' ),
$plugin_name
),
);
}
return ! empty( $conflicting_plugins ) ? $conflicting_plugins : false;
}
}

View File

@@ -0,0 +1,41 @@
<?php
/**
* Class Google\Site_Kit\Modules\Sign_In_With_Google\Compatibility_Checks\WP_COM_Check
*
* @package Google\Site_Kit\Modules\Sign_In_With_Google
* @copyright 2025 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\Sign_In_With_Google\Compatibility_Checks;
/**
* Compatibility check for WordPress.com hosting.
*
* @since 1.164.0
*/
class WP_COM_Check extends Compatibility_Check {
/**
* Gets the unique slug for this compatibility check.
*
* @since 1.164.0
*
* @return string The unique slug for this compatibility check.
*/
public function get_slug() {
return 'host_wordpress_dot_com';
}
/**
* Runs the compatibility check.
*
* @since 1.164.0
*
* @return bool True if hosted on WordPress.com, false otherwise.
*/
public function run() {
return defined( 'WPCOMSH_VERSION' );
}
}

View File

@@ -0,0 +1,52 @@
<?php
/**
* Class Google\Site_Kit\Modules\Sign_In_With_Google\Compatibility_Checks\WP_Login_Accessible_Check
*
* @package Google\Site_Kit\Modules\Sign_In_With_Google
* @copyright 2025 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\Sign_In_With_Google\Compatibility_Checks;
/**
* Compatibility check for WordPress login accessibility.
*
* @since 1.164.0
*/
class WP_Login_Accessible_Check extends Compatibility_Check {
/**
* Gets the unique slug for this compatibility check.
*
* @since 1.164.0
*
* @return string The unique slug for this compatibility check.
*/
public function get_slug() {
return 'wp_login_inaccessible';
}
/**
* Runs the compatibility check.
*
* @since 1.164.0
*
* @return bool True if login is inaccessible (404), false otherwise.
*/
public function run() {
// Hardcode the wp-login at the end to avoid issues with filters - plugins modifying the wp-login page
// also override the URL request which skips the correct detection.
$login_url = site_url() . '/wp-login.php';
$response = wp_remote_head( $login_url );
if ( is_wp_error( $response ) ) {
return false;
}
$status_code = wp_remote_retrieve_response_code( $response );
return 404 === $status_code;
}
}

View File

@@ -0,0 +1,84 @@
<?php
/**
* Class Google\Site_Kit\Modules\Sign_In_With_Google\Datapoint\Compatibility_Checks
*
* @package Google\Site_Kit\Modules\Sign_In_With_Google\Datapoint
* @copyright 2025 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\Sign_In_With_Google\Datapoint;
use Google\Site_Kit\Core\Modules\Datapoint;
use Google\Site_Kit\Core\Modules\Executable_Datapoint;
use Google\Site_Kit\Core\Permissions\Permissions;
use Google\Site_Kit\Core\REST_API\Data_Request;
use Google\Site_Kit\Modules\Sign_In_With_Google\Compatibility_Checks\Compatibility_Checks as Checks;
use WP_Error;
/**
* Class for the compatibility-check datapoint.
*
* @since 1.164.0
* @access private
* @ignore
*/
class Compatibility_Checks extends Datapoint implements Executable_Datapoint {
/**
* Compatibilty checks instance.
*
* @since 1.164.0
* @var Checks
*/
private $checks;
/**
* Constructor.
*
* @since 1.164.0
*
* @param array $definition Definition fields.
*/
public function __construct( array $definition ) {
parent::__construct( $definition );
if ( isset( $definition['checks'] ) ) {
$this->checks = $definition['checks'];
}
}
/**
* Creates a request object.
*
* @since 1.164.0
*
* @param Data_Request $data Data request object.
*/
public function create_request( Data_Request $data ) {
if ( ! current_user_can( Permissions::MANAGE_OPTIONS ) ) {
return new WP_Error( 'rest_forbidden', __( 'You are not allowed to access this resource.', 'google-site-kit' ), array( 'status' => 403 ) );
}
return function () {
return array(
'checks' => $this->checks->run_checks(),
'timestamp' => time(),
);
};
}
/**
* Parses a response.
*
* @since 1.164.0
*
* @param mixed $response Request response.
* @param Data_Request $data Data request object.
* @return mixed The original response without any modifications.
*/
public function parse_response( $response, Data_Request $data ) {
return $response;
}
}

View File

@@ -0,0 +1,29 @@
<?php
/**
* Class Google\Site_Kit\Modules\Sign_In_With_Google\Existing_Client_ID
*
* @package Google\Site_Kit\Modules\Sign_In_With_Google
* @copyright 2024 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\Sign_In_With_Google;
use Google\Site_Kit\Core\Storage\Setting;
/**
* Class for persisting the client ID between module disconnection and
* reconnection.
*
* @since 1.142.0
* @access private
* @ignore
*/
class Existing_Client_ID extends Setting {
/**
* The option_name for this setting.
*/
const OPTION = 'googlesitekit_siwg_existing_client_id';
}

View File

@@ -0,0 +1,28 @@
<?php
/**
* Class Google\Site_Kit\Modules\Sign_In_With_Google\Hashed_User_ID
*
* @package Google\Site_Kit\Modules\Sign_In_With_Google
* @copyright 2024 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\Sign_In_With_Google;
use Google\Site_Kit\Core\Storage\User_Setting;
/**
* Class representing the hashed Google user ID.
*
* @since 1.141.0
* @access private
* @ignore
*/
final class Hashed_User_ID extends User_Setting {
/**
* User option key.
*/
const OPTION = 'googlesitekitpersistent_siwg_google_user_hid';
}

View File

@@ -0,0 +1,68 @@
<?php
/**
* Class Google\Site_Kit\Modules\Sign_In_With_Google\Profile_Reader
*
* @package Google\Site_Kit\Modules\Sign_In_With_Google
* @copyright 2024 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\Sign_In_With_Google;
use Exception;
use Google\Site_Kit_Dependencies\Google_Client;
use WP_Error;
/**
* Reads Google user profile data.
*
* @since 1.141.0
* @access private
* @ignore
*/
class Profile_Reader implements Profile_Reader_Interface {
/**
* Settings instance.
*
* @since 1.141.0
* @var Settings
*/
private $settings;
/**
* Constructor.
*
* @since 1.141.0
*
* @param Settings $settings Settings instance.
*/
public function __construct( Settings $settings ) {
$this->settings = $settings;
}
/**
* Gets the user profile data using the provided ID token.
*
* @since 1.141.0
*
* @param string $id_token ID token.
* @return array|WP_Error User profile data or WP_Error on failure.
*/
public function get_profile_data( $id_token ) {
try {
$settings = $this->settings->get();
$google_client = new Google_Client( array( 'client_id' => $settings['clientID'] ) );
$payload = $google_client->verifyIdToken( $id_token );
if ( empty( $payload['sub'] ) || empty( $payload['email'] ) || empty( $payload['email_verified'] ) ) {
return new WP_Error( 'googlesitekit_siwg_bad_payload' );
}
return $payload;
} catch ( Exception $e ) {
return new WP_Error( 'googlesitekit_siwg_failed_to_get_payload', $e->getMessage() );
}
}
}

View File

@@ -0,0 +1,33 @@
<?php
/**
* Class Google\Site_Kit\Modules\Sign_In_With_Google\Profile_Reader_Interface
*
* @package Google\Site_Kit\Modules\Sign_In_With_Google
* @copyright 2024 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\Sign_In_With_Google;
use WP_Error;
/**
* Defines methods that must be implemented by a profile reader class.
*
* @since 1.141.0
* @access private
* @ignore
*/
interface Profile_Reader_Interface {
/**
* Gets the user profile data using the provided ID token.
*
* @since 1.141.0
*
* @param string $id_token ID token.
* @return array|WP_Error User profile data or WP_Error on failure.
*/
public function get_profile_data( $id_token );
}

View File

@@ -0,0 +1,199 @@
<?php
/**
* Class Google\Site_Kit\Modules\Sign_In_With_Google\Settings
*
* @package Google\Site_Kit\Modules\Sign_In_With_Google
* @copyright 2024 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\Sign_In_With_Google;
use Google\Site_Kit\Core\Modules\Module_Settings;
/**
* Class for Sign_In_With_Google settings.
*
* @since 1.137.0
* @access private
* @ignore
*/
class Settings extends Module_Settings {
const OPTION = 'googlesitekit_sign-in-with-google_settings';
const TEXT_CONTINUE_WITH_GOOGLE = array(
'value' => 'continue_with',
'label' => 'Continue with Google',
);
const TEXT_SIGN_IN = array(
'value' => 'signin',
'label' => 'Sign in',
);
const TEXT_SIGN_IN_WITH_GOOGLE = array(
'value' => 'signin_with',
'label' => 'Sign in with Google',
);
const TEXT_SIGN_UP_WITH_GOOGLE = array(
'value' => 'signup_with',
'label' => 'Sign up with Google',
);
const TEXTS = array(
self::TEXT_CONTINUE_WITH_GOOGLE,
self::TEXT_SIGN_IN,
self::TEXT_SIGN_IN_WITH_GOOGLE,
self::TEXT_SIGN_UP_WITH_GOOGLE,
);
const THEME_LIGHT = array(
'value' => 'outline',
'label' => 'Light',
);
const THEME_NEUTRAL = array(
'value' => 'filled_blue',
'label' => 'Neutral',
);
const THEME_DARK = array(
'value' => 'filled_black',
'label' => 'Dark',
);
const THEMES = array(
self::THEME_LIGHT,
self::THEME_NEUTRAL,
self::THEME_DARK,
);
const SHAPE_RECTANGULAR = array(
'value' => 'rectangular',
'label' => 'Rectangular',
);
const SHAPE_PILL = array(
'value' => 'pill',
'label' => 'Pill',
);
const SHAPES = array(
self::SHAPE_RECTANGULAR,
self::SHAPE_PILL,
);
/**
* Gets the default value.
*
* @since 1.137.0
*
* @return array An array of default settings values.
*/
protected function get_default() {
return array(
'clientID' => '',
'text' => self::TEXT_SIGN_IN_WITH_GOOGLE['value'],
'theme' => self::THEME_LIGHT['value'],
'shape' => self::SHAPE_RECTANGULAR['value'],
'oneTapEnabled' => false,
'showNextToCommentsEnabled' => false,
);
}
/**
* Gets the callback for sanitizing the setting's value before saving.
*
* @since 1.137.0
*
* @return callable|null
*/
protected function get_sanitize_callback() {
return function ( $option ) {
if ( ! is_array( $option ) ) {
return $option;
}
if ( isset( $option['clientID'] ) ) {
$option['clientID'] = (string) $option['clientID'];
}
if ( isset( $option['text'] ) ) {
$text_options = array(
self::TEXT_CONTINUE_WITH_GOOGLE['value'],
self::TEXT_SIGN_IN['value'],
self::TEXT_SIGN_IN_WITH_GOOGLE['value'],
self::TEXT_SIGN_UP_WITH_GOOGLE['value'],
);
if ( ! in_array( $option['text'], $text_options, true ) ) {
$option['text'] = self::TEXT_SIGN_IN_WITH_GOOGLE['value'];
}
}
if ( isset( $option['theme'] ) ) {
$theme_options = array(
self::THEME_LIGHT['value'],
self::THEME_NEUTRAL['value'],
self::THEME_DARK['value'],
);
if ( ! in_array( $option['theme'], $theme_options, true ) ) {
$option['theme'] = self::THEME_LIGHT['value'];
}
}
if ( isset( $option['shape'] ) ) {
$shape_options = array(
self::SHAPE_RECTANGULAR['value'],
self::SHAPE_PILL['value'],
);
if ( ! in_array( $option['shape'], $shape_options, true ) ) {
$option['shape'] = self::SHAPE_RECTANGULAR['value'];
}
}
if ( isset( $option['oneTapEnabled'] ) ) {
$option['oneTapEnabled'] = (bool) $option['oneTapEnabled'];
}
if ( isset( $option['showNextToCommentsEnabled'] ) ) {
$option['showNextToCommentsEnabled'] = (bool) $option['showNextToCommentsEnabled'];
}
return $option;
};
}
/**
* Gets the label for a given Sign in with Google setting value.
*
* @since 1.140.0
*
* @param string $setting_name The slug for the Sign in with Google setting.
* @param string $value The setting value to look up the label for.
* @return string The label for the given setting value.
*/
public function get_label( $setting_name, $value ) {
switch ( $setting_name ) {
case 'text':
$constant = self::TEXTS;
break;
case 'theme':
$constant = self::THEMES;
break;
case 'shape':
$constant = self::SHAPES;
break;
}
if ( ! isset( $constant ) ) {
return '';
}
$key = array_search( $value, array_column( $constant, 'value' ), true );
if ( false === $key ) {
return '';
}
return $constant[ $key ]['label'];
}
}

View File

@@ -0,0 +1,128 @@
<?php
/**
* Class Google\Site_Kit\Modules\Sign_In_With_Google\Sign_In_With_Google_Block
*
* @package Google\Site_Kit\Modules\Sign_In_With_Google
* @copyright 2025 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\Sign_In_With_Google;
use Google\Site_Kit\Context;
use Google\Site_Kit\Core\Util\Block_Support;
/**
* Sign in with Google Gutenberg Block.
*
* @since 1.147.0
*/
class Sign_In_With_Google_Block {
/**
* Context instance.
*
* @since 1.147.0
* @var Context
*/
protected $context;
/**
* Constructor.
*
* @since 1.147.0
*
* @param Context $context Plugin context.
*/
public function __construct( Context $context ) {
$this->context = $context;
}
/**
* Checks whether the block can be registered.
*
* @since 1.147.0
*
* @return bool
*/
public static function can_register() {
return Block_Support::has_block_support();
}
/**
* Register this block.
*
* @since 1.147.0
*/
public function register() {
if ( ! self::can_register() ) {
return;
}
add_action(
'init',
function () {
register_block_type(
dirname( GOOGLESITEKIT_PLUGIN_MAIN_FILE ) . '/dist/assets/blocks/sign-in-with-google/block.json',
array(
'render_callback' => array( $this, 'render_callback' ),
)
);
},
99
);
}
/**
* Render callback for the Sign in with Google block.
*
* @since 1.147.0
* @since 1.165.0 Added the `$attributes` parameter.
*
* @param array $attributes Block attributes.
* @return string Rendered block.
*/
public function render_callback( $attributes = array() ) {
// If the user is already signed in, do not render a Sign in
// with Google button.
if ( is_user_logged_in() ) {
return '';
}
$attributes = is_array( $attributes ) ? $attributes : array();
$button_args = array();
$allowed_attributes = array(
'text' => wp_list_pluck( Settings::TEXTS, 'value' ),
'theme' => wp_list_pluck( Settings::THEMES, 'value' ),
'shape' => wp_list_pluck( Settings::SHAPES, 'value' ),
);
foreach ( array( 'text', 'theme', 'shape' ) as $key ) {
if ( ! empty( $attributes[ $key ] ) && in_array( $attributes[ $key ], $allowed_attributes[ $key ], true ) ) {
$button_args[ $key ] = $attributes[ $key ];
}
}
if ( ! empty( $attributes['buttonClassName'] ) && is_string( $attributes['buttonClassName'] ) ) {
$classes = array_filter(
preg_split( '/\s+/', trim( $attributes['buttonClassName'] ) )
);
if ( ! empty( $classes ) ) {
$button_args['class'] = $classes;
}
}
ob_start();
/**
* Display the Sign in with Google button.
*
* @since 1.164.0
*
* @param array $args Optional arguments to customize button attributes.
*/
do_action( 'googlesitekit_render_sign_in_with_google_button', $button_args );
return ob_get_clean();
}
}

View File

@@ -0,0 +1,46 @@
<?php
/**
* Class Google\Site_Kit\Modules\Sign_In_With_Google\Tag_Guard
*
* @package Google\Site_Kit\Modules\Sign_In_With_Google
* @copyright 2024 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\Sign_In_With_Google;
use Google\Site_Kit\Core\Modules\Module_Settings;
use Google\Site_Kit\Core\Modules\Tags\Module_Tag_Guard;
/**
* Class for the Sign in with Google tag guard.
*
* @since 1.159.0
* @access private
* @ignore
*/
class Tag_Guard extends Module_Tag_Guard {
/**
* Determines whether the guarded tag can be activated or not.
*
* @since 1.159.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();
// If there's no client ID available, don't render the button.
if ( ! $settings['clientID'] ) {
return false;
}
// If the site does not use https, don't render the button.
if ( substr( wp_login_url(), 0, 5 ) !== 'https' ) {
return false;
}
return true;
}
}

View File

@@ -0,0 +1,35 @@
<?php
/**
* Class Google\Site_Kit\Modules\Sign_In_With_Google\Tag_Matchers
*
* @package Google\Site_Kit\Core\Modules\Sign_In_With_Google
* @copyright 2024 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\Sign_In_With_Google;
use Google\Site_Kit\Core\Modules\Tags\Module_Tag_Matchers;
use Google\Site_Kit\Core\Tags\Tag_Matchers_Interface;
/**
* Class for Tag matchers.
*
* @since 1.140.0
* @access private
* @ignore
*/
class Tag_Matchers extends Module_Tag_Matchers implements Tag_Matchers_Interface {
/**
* Holds array of regex tag matchers.
*
* @since 1.140.0
*
* @return array Array of regex matchers.
*/
public function regex_matchers() {
return array();
}
}

View File

@@ -0,0 +1,263 @@
<?php
/**
* Class Google\Site_Kit\Modules\Sign_In_With_Google\Web_Tag
*
* @package Google\Site_Kit\Modules\Sign_In_With_Google
* @copyright 2024 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\Sign_In_With_Google;
use Google\Site_Kit\Core\Modules\Tags\Module_Web_Tag;
use Google\Site_Kit\Core\Util\BC_Functions;
use Google\Site_Kit\Core\Util\Method_Proxy_Trait;
use Google\Site_Kit\Modules\Sign_In_With_Google\Authenticator;
/**
* Class for Web tag.
*
* @since 1.159.0
* @access private
* @ignore
*/
class Web_Tag extends Module_Web_Tag {
use Method_Proxy_Trait;
/**
* Module settings.
*
* @since 1.159.0
* @var Settings
*/
private $settings;
/**
* Whether the current page is the WordPress login page.
*
* `is_login()` isn't available until WP 6.1.
*
* @since 1.159.0
* @var bool
*/
private $is_wp_login;
/**
* Redirect to URL.
*
* @since 1.159.0
* @var string
*/
private $redirect_to;
/**
* Sets the module settings.
*
* @since 1.159.0
*
* @param array $settings Module settings as array.
*/
public function set_settings( array $settings ) {
$this->settings = $settings;
}
/**
* Sets whether the current page is the WordPress login page.
*
* @since 1.159.0
*
* @param bool $is_wp_login Whether the current page is the WordPress login page.
*/
public function set_is_wp_login( $is_wp_login ) {
$this->is_wp_login = $is_wp_login;
}
/**
* Sets the redirect to URL.
*
* @since 1.159.0
*
* @param string $redirect_to Redirect to URL.
*/
public function set_redirect_to( $redirect_to ) {
if ( ! empty( $redirect_to ) ) {
$this->redirect_to = trim( $redirect_to );
}
}
/**
* Registers tag hooks.
*
* @since 1.159.0
*/
public function register() {
// Render the Sign in with Google script that converts placeholder
// <div>s with Sign in with Google buttons.
add_action( 'wp_footer', $this->get_method_proxy( 'render' ) );
// Output the Sign in with Google JS on the WordPress login page.
add_action( 'login_footer', $this->get_method_proxy( 'render' ) );
$this->do_init_tag_action();
}
/**
* Renders the Sign in with Google JS script tags, One Tap code, and
* buttons.
*
* @since 1.139.0
* @since 1.144.0 Renamed to `render_signinwithgoogle` and conditionally
* rendered the code to replace buttons.
* @since 1.159.0 moved from main Sign_In_With_Google class to Web_Tag.
*/
protected function render() {
$is_woocommerce = class_exists( 'woocommerce' );
$is_woocommerce_login = did_action( 'woocommerce_login_form_start' );
$login_uri = add_query_arg( 'action', 'googlesitekit_auth', wp_login_url() );
$btn_args = array(
'theme' => $this->settings['theme'],
'text' => $this->settings['text'],
'shape' => $this->settings['shape'],
);
// Whether this is a WordPress/WooCommerce login page.
$is_login_page = $this->is_wp_login || $is_woocommerce_login;
// Check to see if we should show the One Tap prompt on this page.
//
// Show the One Tap prompt if:
// 1. One Tap is enabled in settings.
// 2. The user is not logged in.
$should_show_one_tap_prompt = ! empty( $this->settings['oneTapEnabled'] ) && ! is_user_logged_in();
// Set the cookie time to live to 5 minutes. If the redirect_to is
// empty, set the cookie to expire immediately.
$cookie_expire_time = 300000;
if ( empty( $this->redirect_to ) ) {
$cookie_expire_time *= -1;
}
// Render the Sign in with Google script.
ob_start();
?>
( () => {
async function handleCredentialResponse( response ) {
<?php if ( $is_woocommerce && ! $this->is_wp_login ) : // phpcs:ignore Generic.WhiteSpace.ScopeIndent.Incorrect ?>
response.integration = 'woocommerce';
<?php endif; // phpcs:ignore Generic.WhiteSpace.ScopeIndent.Incorrect ?>
try {
const res = await fetch( '<?php echo esc_js( $login_uri ); ?>', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams( response )
} );
/*
Preserve comment text in case of redirect after login on a page
with a Sign in with Google button in the WordPress comments.
*/
const commentText = document.querySelector( '#comment' )?.value;
const postId = document.querySelectorAll( '.googlesitekit-sign-in-with-google__comments-form-button' )?.[0]?.className?.match(/googlesitekit-sign-in-with-google__comments-form-button-postid-(\d+)/)?.[1];
if ( !! commentText?.length ) {
sessionStorage.setItem( `siwg-comment-text-${postId}`, commentText );
}
<?php if ( empty( $this->redirect_to ) && ! $is_login_page ) : // phpcs:ignore Generic.WhiteSpace.ScopeIndent.Incorrect ?>
location.reload();
<?php else : // phpcs:ignore Generic.WhiteSpace.ScopeIndent.Incorrect ?>
if ( res.ok && res.redirected ) {
location.assign( res.url );
}
<?php endif; // phpcs:ignore Generic.WhiteSpace.ScopeIndent.Incorrect ?>
} catch( error ) {
console.error( error );
}
}
if (typeof google !== 'undefined') {
google.accounts.id.initialize( {
client_id: '<?php echo esc_js( $this->settings['clientID'] ); ?>',
callback: handleCredentialResponse,
library_name: 'Site-Kit'
} );
}
<?php if ( $this->is_wp_login ) : // phpcs:ignore Generic.WhiteSpace.ScopeIndent.Incorrect ?>
const buttonDivToAddToLoginForm = document.createElement( 'div' );
buttonDivToAddToLoginForm.classList.add( 'googlesitekit-sign-in-with-google__frontend-output-button' );
document.getElementById( 'login' ).insertBefore( buttonDivToAddToLoginForm, document.getElementById( 'loginform' ) );
<?php endif; // phpcs:ignore Generic.WhiteSpace.ScopeIndent.Incorrect ?>
<?php if ( ! is_user_logged_in() || $this->is_wp_login ) : // phpcs:ignore Generic.WhiteSpace.ScopeIndent.Incorrect ?>
<?php
/**
* Render SiwG buttons for all `<div>` elements with the "magic
* class" on the page.
*
* Mainly used by Gutenberg blocks.
*/
?>
const defaultButtonOptions = <?php echo wp_json_encode( $btn_args ); ?>;
document.querySelectorAll( '.googlesitekit-sign-in-with-google__frontend-output-button' ).forEach( ( siwgButtonDiv ) => {
const buttonOptions = {
shape: siwgButtonDiv.getAttribute( 'data-googlesitekit-siwg-shape' ) || defaultButtonOptions.shape,
text: siwgButtonDiv.getAttribute( 'data-googlesitekit-siwg-text' ) || defaultButtonOptions.text,
theme: siwgButtonDiv.getAttribute( 'data-googlesitekit-siwg-theme' ) || defaultButtonOptions.theme,
};
if (typeof google !== 'undefined') {
google.accounts.id.renderButton( siwgButtonDiv, buttonOptions );
}
});
<?php endif; // phpcs:ignore Generic.WhiteSpace.ScopeIndent.Incorrect ?>
<?php if ( $should_show_one_tap_prompt ) : // phpcs:ignore Generic.WhiteSpace.ScopeIndent.Incorrect ?>
if (typeof google !== 'undefined') {
google.accounts.id.prompt();
}
<?php endif; // phpcs:ignore Generic.WhiteSpace.ScopeIndent.Incorrect ?>
<?php if ( ! empty( $this->redirect_to ) ) : // phpcs:ignore Generic.WhiteSpace.ScopeIndent.Incorrect ?>
const expires = new Date();
expires.setTime( expires.getTime() + <?php echo esc_js( $cookie_expire_time ); ?> );
document.cookie = "<?php echo esc_js( Authenticator::COOKIE_REDIRECT_TO ); ?>=<?php echo esc_js( $this->redirect_to ); ?>;expires=" + expires.toUTCString() + ";path=<?php echo esc_js( Authenticator::get_cookie_path() ); ?>";
<?php endif; // phpcs:ignore Generic.WhiteSpace.ScopeIndent.Incorrect ?>
/*
If there is a matching saved comment text in sessionStorage, restore it
to the comment field and remove it from sessionStorage.
*/
const postId = document.body.className.match(/postid-(\d+)/)?.[1];
const commentField = document.querySelector( '#comment' );
const commentText = sessionStorage.getItem( `siwg-comment-text-${postId}` );
if ( commentText?.length && commentField && !! postId ) {
commentField.value = commentText;
sessionStorage.removeItem( `siwg-comment-text-${postId}` );
}
} )();
<?php
// Strip all whitespace and unnecessary spaces.
$inline_script = preg_replace( '/\s+/', ' ', ob_get_clean() );
$inline_script = preg_replace( '/\s*([{};\(\)\+:,=])\s*/', '$1', $inline_script );
// Output the Sign in with Google script.
printf( "\n<!-- %s -->\n", esc_html__( 'Sign in with Google button added by Site Kit', 'google-site-kit' ) );
?>
<style>
.googlesitekit-sign-in-with-google__frontend-output-button{max-width:320px}
</style>
<?php
BC_Functions::wp_print_script_tag( array( 'src' => 'https://accounts.google.com/gsi/client' ) );
BC_Functions::wp_print_inline_script_tag( $inline_script );
printf( "\n<!-- %s -->\n", esc_html__( 'End Sign in with Google button added by Site Kit', 'google-site-kit' ) );
}
}

View File

@@ -0,0 +1,62 @@
<?php
/**
* Class Google\Site_Kit\Modules\Sign_In_With_Google\WooCommerce_Authenticator
*
* @package Google\Site_Kit\Modules\Sign_In_With_Google
* @copyright 2024 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\Sign_In_With_Google;
use Google\Site_Kit\Core\Storage\User_Options;
use Google\Site_Kit\Core\Util\Input;
use WP_Error;
use WP_User;
/**
* The authenticator class that processes Sign in with Google callback
* requests to authenticate users when WooCommerce is activated.
*
* @since 1.145.0
* @access private
* @ignore
*/
class WooCommerce_Authenticator extends Authenticator {
/**
* Gets the redirect URL for the error page.
*
* @since 1.145.0
*
* @param string $code Error code.
* @return string Redirect URL.
*/
protected function get_error_redirect_url( $code ) {
do_action( 'woocommerce_login_failed' );
return add_query_arg( 'error', $code, wc_get_page_permalink( 'myaccount' ) );
}
/**
* Gets the redirect URL after the user signs in with Google.
*
* @since 1.145.0
* @since 1.146.0 Updated to take into account redirect URL from cookie.
*
* @param WP_User $user User object.
* @param Input $input Input instance.
* @return string Redirect URL.
*/
protected function get_redirect_url( $user, $input ) {
$redirect_to = wc_get_page_permalink( 'myaccount' );
// If we have the redirect URL in the cookie, use it as the main redirect_to URL.
$cookie_redirect_to = $this->get_cookie_redirect( $input );
if ( ! empty( $cookie_redirect_to ) ) {
$redirect_to = $cookie_redirect_to;
}
return apply_filters( 'woocommerce_login_redirect', $redirect_to, $user );
}
}

View File

@@ -0,0 +1,505 @@
<?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\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\Core\Util\URL;
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 ) {
$this->transients->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 ( URL::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;
$meta_tags = $this->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 )
);
$this->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,677 @@
<?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_Service_Entity;
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_Tag;
use Google\Site_Kit\Core\Modules\Module_With_Tag_Trait;
use Google\Site_Kit\Core\Modules\Tag_Manager\Tag_Matchers;
use Google\Site_Kit\Core\Modules\Tags\Module_Tag_Matchers;
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_Environment_Type_Guard;
use Google\Site_Kit\Core\Tags\Guards\Tag_Verify_Guard;
use Google\Site_Kit\Core\Site_Health\Debug_Data;
use Google\Site_Kit\Core\Tags\Google_Tag_Gateway\Google_Tag_Gateway_Settings;
use Google\Site_Kit\Core\Util\Feature_Flags;
use Google\Site_Kit\Core\Util\Method_Proxy_Trait;
use Google\Site_Kit\Core\Util\Sort;
use Google\Site_Kit\Core\Util\URL;
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\Container as Google_Service_TagManager_Container;
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_Service_Entity, Module_With_Deactivation, Module_With_Tag {
use Method_Proxy_Trait;
use Module_With_Assets_Trait;
use Module_With_Owner_Trait;
use Module_With_Scopes_Trait;
use Module_With_Settings_Trait;
use Module_With_Tag_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', array( $this, 'register_tag' ) );
add_filter(
'googlesitekit_ads_measurement_connection_checks',
function ( $checks ) {
$checks[] = array( $this, 'check_ads_measurement_connection' );
return $checks;
},
30
);
}
/**
* Checks if the Tag Manager module is connected and contains an Ads Conversion Tracking (AWCT) tag.
*
* @since 1.151.0
*
* @return bool Whether or not Ads measurement is connected via this module.
*/
public function check_ads_measurement_connection() {
if ( ! $this->is_connected() ) {
return false;
}
$settings = $this->get_settings()->get();
$live_containers_versions = $this->get_data(
'live-container-version',
array(
'accountID' => $settings['accountID'],
'internalContainerID' => $settings['internalContainerID'],
)
);
if ( empty( $live_containers_versions->tag ) ) {
return false;
}
return in_array(
'awct',
array_column( $live_containers_versions->tag, 'type' ),
true
);
}
/**
* Gets required Google OAuth scopes for the module.
*
* @since 1.0.0
*
* @return array List of Google OAuth scopes.
*/
public function get_scopes() {
return array(
'https://www.googleapis.com/auth/tagmanager.readonly',
);
}
/**
* Checks whether the module is connected.
*
* A module being connected means that all steps required as part of its activation are completed.
*
* @since 1.0.0
*
* @return bool True if module is connected, false otherwise.
*/
public function is_connected() {
$settings = $this->get_settings()->get();
$amp_mode = $this->context->get_amp_mode();
switch ( $amp_mode ) {
case Context::AMP_MODE_PRIMARY:
$container_ids = array( $settings['ampContainerID'] );
break;
case Context::AMP_MODE_SECONDARY:
$container_ids = array( $settings['containerID'], $settings['ampContainerID'] );
break;
default:
$container_ids = array( $settings['containerID'] );
}
$container_id_errors = array_filter(
$container_ids,
function ( $container_id ) {
return ! $container_id;
}
);
if ( ! empty( $container_id_errors ) ) {
return false;
}
return parent::is_connected();
}
/**
* Cleans up when the module is deactivated.
*
* @since 1.0.0
*/
public function on_deactivation() {
$this->get_settings()->delete();
}
/**
* Gets an array of debug field definitions.
*
* @since 1.5.0
*
* @return array
*/
public function get_debug_fields() {
$settings = $this->get_settings()->get();
return array(
'tagmanager_account_id' => array(
'label' => __( 'Tag Manager: Account ID', 'google-site-kit' ),
'value' => $settings['accountID'],
'debug' => Debug_Data::redact_debug_value( $settings['accountID'] ),
),
'tagmanager_container_id' => array(
'label' => __( 'Tag Manager: Container ID', 'google-site-kit' ),
'value' => $settings['containerID'],
'debug' => Debug_Data::redact_debug_value( $settings['containerID'], 7 ),
),
'tagmanager_amp_container_id' => array(
'label' => __( 'Tag Manager: AMP Container ID', 'google-site-kit' ),
'value' => $settings['ampContainerID'],
'debug' => Debug_Data::redact_debug_value( $settings['ampContainerID'], 7 ),
),
'tagmanager_use_snippet' => array(
'label' => __( 'Tag Manager: Snippet placed', 'google-site-kit' ),
'value' => $settings['useSnippet'] ? __( 'Yes', 'google-site-kit' ) : __( 'No', 'google-site-kit' ),
'debug' => $settings['useSnippet'] ? 'yes' : 'no',
),
);
}
/**
* Sanitizes a string to be used for a container name.
*
* @since 1.0.4
*
* @param string $name String to sanitize.
*
* @return string
*/
public static function sanitize_container_name( $name ) {
// Remove any leading or trailing whitespace.
$name = trim( $name );
// Must not start with an underscore.
$name = ltrim( $name, '_' );
// Decode entities for special characters so that they are stripped properly.
$name = wp_specialchars_decode( $name, ENT_QUOTES );
// Convert accents to basic characters to prevent them from being stripped.
$name = remove_accents( $name );
// Strip all non-simple characters.
$name = preg_replace( '/[^a-zA-Z0-9_., -]/', '', $name );
// Collapse multiple whitespaces.
$name = preg_replace( '/\s+/', ' ', $name );
return $name;
}
/**
* Gets map of datapoint to definition data for each.
*
* @since 1.9.0
*
* @return array Map of datapoints to their definitions.
*/
protected function get_datapoint_definitions() {
return array(
'GET:accounts' => array( 'service' => 'tagmanager' ),
'GET:accounts-containers' => array( 'service' => 'tagmanager' ),
'GET:containers' => array( 'service' => 'tagmanager' ),
'POST:create-container' => array(
'service' => 'tagmanager',
'scopes' => array( 'https://www.googleapis.com/auth/tagmanager.edit.containers' ),
'request_scopes_message' => __( 'Additional permissions are required to create a new Tag Manager container on your behalf.', 'google-site-kit' ),
),
'GET:live-container-version' => array( 'service' => 'tagmanager' ),
);
}
/**
* Creates a request object for the given datapoint.
*
* @since 1.0.0
*
* @param Data_Request $data Data request object.
* @return RequestInterface|callable|WP_Error Request object or callable on success, or WP_Error on failure.
*
* @throws Invalid_Datapoint_Exception Thrown if the datapoint does not exist.
*/
protected function create_data_request( Data_Request $data ) {
switch ( "{$data->method}:{$data->datapoint}" ) {
// Intentional fallthrough.
case 'GET:accounts':
case 'GET:accounts-containers':
return $this->get_tagmanager_service()->accounts->listAccounts();
case 'GET:containers':
if ( ! isset( $data['accountID'] ) ) {
/* translators: %s: Missing parameter name */
return new WP_Error( 'missing_required_param', sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'accountID' ), array( 'status' => 400 ) );
}
return $this->get_tagmanager_service()->accounts_containers->listAccountsContainers( "accounts/{$data['accountID']}" );
case 'POST:create-container':
if ( ! isset( $data['accountID'] ) ) {
return new WP_Error(
'missing_required_param',
/* translators: %s: Missing parameter name */
sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'accountID' ),
array( 'status' => 400 )
);
}
$usage_context = $data['usageContext'] ?: array( self::USAGE_CONTEXT_WEB, self::USAGE_CONTEXT_AMP );
if ( empty( $this->context_map[ $usage_context ] ) ) {
return new WP_Error(
'invalid_param',
sprintf(
/* translators: 1: Invalid parameter name, 2: list of valid values */
__( 'Request parameter %1$s is not one of %2$s', 'google-site-kit' ),
'usageContext',
implode( ', ', array_keys( $this->context_map ) )
),
array( 'status' => 400 )
);
}
$account_id = $data['accountID'];
if ( $data['name'] ) {
$container_name = $data['name'];
} else {
// Use site name for container, fallback to domain of reference URL.
$container_name = get_bloginfo( 'name' ) ?: URL::parse( $this->context->get_reference_site_url(), PHP_URL_HOST );
// Prevent naming conflict (Tag Manager does not allow more than one with same name).
if ( self::USAGE_CONTEXT_AMP === $usage_context ) {
$container_name .= ' AMP';
}
}
$container = new Google_Service_TagManager_Container();
$container->setName( self::sanitize_container_name( $container_name ) );
$container->setUsageContext( (array) $usage_context );
return $this->get_tagmanager_service()->accounts_containers->create( "accounts/{$account_id}", $container );
case 'GET:live-container-version':
if ( ! isset( $data['accountID'] ) ) {
return new WP_Error(
'missing_required_param',
/* translators: %s: Missing parameter name */
sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'accountID' ),
array( 'status' => 400 )
);
}
if ( ! isset( $data['internalContainerID'] ) ) {
return new WP_Error(
'missing_required_param',
/* translators: %s: Missing parameter name */
sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'internalContainerID' ),
array( 'status' => 400 )
);
}
return $this->get_tagmanager_service()->accounts_containers_versions->live(
"accounts/{$data['accountID']}/containers/{$data['internalContainerID']}"
);
}
return parent::create_data_request( $data );
}
/**
* Creates GTM Container.
*
* @since 1.0.0
* @param string $account_id The account ID.
* @param string|array $usage_context The container usage context(s).
*
* @return string Container public ID.
* @throws Exception Throws an exception if raised during container creation.
*/
protected function create_container( $account_id, $usage_context = self::USAGE_CONTEXT_WEB ) {
$restore_defer = $this->with_client_defer( false );
// Use site name for container, fallback to domain of reference URL.
$container_name = get_bloginfo( 'name' ) ?: URL::parse( $this->context->get_reference_site_url(), PHP_URL_HOST );
// Prevent naming conflict (Tag Manager does not allow more than one with same name).
if ( self::USAGE_CONTEXT_AMP === $usage_context ) {
$container_name .= ' AMP';
}
$container_name = self::sanitize_container_name( $container_name );
$container = new Google_Service_TagManager_Container();
$container->setName( $container_name );
$container->setUsageContext( (array) $usage_context );
try {
$new_container = $this->get_tagmanager_service()->accounts_containers->create( "accounts/{$account_id}", $container );
} catch ( Exception $exception ) {
$restore_defer();
throw $exception;
}
$restore_defer();
return $new_container->getPublicId();
}
/**
* Parses a response for the given datapoint.
*
* @since 1.0.0
*
* @param Data_Request $data Data request object.
* @param mixed $response Request response.
*
* @return mixed Parsed response data on success, or WP_Error on failure.
*/
protected function parse_data_response( Data_Request $data, $response ) {
switch ( "{$data->method}:{$data->datapoint}" ) {
case 'GET:accounts':
/* @var Google_Service_TagManager_ListAccountsResponse $response List accounts response. */
return Sort::case_insensitive_list_sort(
$response->getAccount(),
'name'
);
case 'GET:accounts-containers':
/* @var Google_Service_TagManager_ListAccountsResponse $response List accounts response. */
$accounts = Sort::case_insensitive_list_sort(
$response->getAccount(),
'name'
);
$response = array(
// TODO: Parse this response to a regular array.
'accounts' => $accounts,
'containers' => array(),
);
if ( 0 === count( $response['accounts'] ) ) {
return $response;
}
if ( $data['accountID'] ) {
$account_id = $data['accountID'];
} else {
$account_id = $response['accounts'][0]->getAccountId();
}
$containers = $this->get_data(
'containers',
array(
'accountID' => $account_id,
'usageContext' => $data['usageContext'] ?: self::USAGE_CONTEXT_WEB,
)
);
if ( is_wp_error( $containers ) ) {
return $response;
}
return array_merge( $response, compact( 'containers' ) );
case 'GET:containers':
/* @var Google_Service_TagManager_ListContainersResponse $response Response object. */
$usage_context = $data['usageContext'] ?: array( self::USAGE_CONTEXT_WEB, self::USAGE_CONTEXT_AMP );
/* @var Google_Service_TagManager_Container[] $containers Filtered containers. */
$containers = array_filter(
(array) $response->getContainer(),
function ( Google_Service_TagManager_Container $container ) use ( $usage_context ) {
return array_intersect( (array) $usage_context, $container->getUsageContext() );
}
);
return Sort::case_insensitive_list_sort(
array_values( $containers ),
'name'
);
}
return parent::parse_data_response( $data, $response );
}
/**
* Gets the configured TagManager service instance.
*
* @since 1.2.0
* @since 1.142.0 Made method public.
*
* @return Google_Service_TagManager instance.
* @throws Exception Thrown if the module did not correctly set up the service.
*/
public 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' ),
'homepage' => __( 'https://tagmanager.google.com/', 'google-site-kit' ),
);
}
/**
* Sets up the Google services the module should use.
*
* This method is invoked once by {@see Module::get_service()} to lazily set up the services when one is requested
* for the first time.
*
* @since 1.0.0
* @since 1.2.0 Now requires Google_Site_Kit_Client instance.
*
* @param Google_Site_Kit_Client $client Google client instance.
* @return array Google services as $identifier => $service_instance pairs. Every $service_instance must be an
* instance of Google_Service.
*/
protected function setup_services( Google_Site_Kit_Client $client ) {
return array(
'tagmanager' => new Google_Service_TagManager( $client ),
);
}
/**
* Sets up the module's settings instance.
*
* @since 1.2.0
*
* @return Module_Settings
*/
protected function setup_settings() {
return new Settings( $this->options );
}
/**
* Sets up the module's assets to register.
*
* @since 1.11.0
*
* @return Asset[] List of Asset objects.
*/
protected function setup_assets() {
$base_url = $this->context->url( 'dist/assets/' );
$dependencies = array(
'googlesitekit-api',
'googlesitekit-data',
'googlesitekit-datastore-site',
'googlesitekit-modules',
'googlesitekit-vendor',
'googlesitekit-components',
);
$analytics_exists = apply_filters( 'googlesitekit_module_exists', false, 'analytics-4' );
// Note that the Tag Manager bundle will make use of the Analytics bundle if it's available,
// but can also function without it, hence the conditional include of the Analytics bundle here.
if ( $analytics_exists ) {
$dependencies[] = 'googlesitekit-modules-analytics-4';
}
return array(
new Script(
'googlesitekit-modules-tagmanager',
array(
'src' => $base_url . 'js/googlesitekit-modules-tagmanager.js',
'dependencies' => $dependencies,
)
),
);
}
/**
* Registers the Tag Manager tag.
*
* @since 1.24.0
* @since 1.119.0 Made method public.
* @since 1.162.0 Updated to pass Google tag gateway status to Web_Tag.
*/
public 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 ( ! $is_amp ) {
$tag->set_is_google_tag_gateway_active( $this->is_google_tag_gateway_active() );
}
if ( ! $tag->is_tag_blocked() ) {
$tag->use_guard( new Tag_Verify_Guard( $this->context->input() ) );
$tag->use_guard( new Tag_Guard( $module_settings, $is_amp ) );
$tag->use_guard( new Tag_Environment_Type_Guard() );
if ( $tag->can_register() ) {
$tag->register();
}
}
}
/**
* Returns the Module_Tag_Matchers instance.
*
* @since 1.119.0
*
* @return Module_Tag_Matchers Module_Tag_Matchers instance.
*/
public function get_tag_matchers() {
return new Tag_Matchers();
}
/**
* Checks if the current user has access to the current configured service entity.
*
* @since 1.77.0
*
* @return boolean|WP_Error
*/
public function check_service_entity_access() {
$is_amp_mode = in_array( $this->context->get_amp_mode(), array( Context::AMP_MODE_PRIMARY, Context::AMP_MODE_SECONDARY ), true );
$settings = $this->get_settings()->get();
$account_id = $settings['accountID'];
$configured_containers = $is_amp_mode ? array( $settings['containerID'], $settings['ampContainerID'] ) : array( $settings['containerID'] );
try {
$containers = $this->get_tagmanager_service()->accounts_containers->listAccountsContainers( "accounts/{$account_id}" );
} catch ( Exception $e ) {
if ( $e->getCode() === 404 ) {
return false;
}
return $this->exception_to_error( $e );
}
$all_containers = array_map(
function ( $container ) {
return $container->getPublicId();
},
$containers->getContainer()
);
return empty( array_diff( $configured_containers, $all_containers ) );
}
/**
* Checks if Google tag gateway is active.
*
* @since 1.162.0
*
* @return bool True if Google tag gateway is active, false otherwise.
*/
protected function is_google_tag_gateway_active() {
if ( ! Feature_Flags::enabled( 'googleTagGateway' ) ) {
return false;
}
$google_tag_gateway_settings = new Google_Tag_Gateway_Settings( $this->options );
return $google_tag_gateway_settings->is_google_tag_gateway_active();
}
}

View File

@@ -0,0 +1,77 @@
<?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;
use 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,
);
}
/**
* 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,58 @@
<?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 );
}
}

Some files were not shown because too many files have changed in this diff Show More