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

View File

@@ -0,0 +1,529 @@
<?php
/**
* Class Google\Site_Kit\Context
*
* @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;
use AMP_Options_Manager;
use AMP_Theme_Support;
use Google\Site_Kit\Core\Util\Input;
use Google\Site_Kit\Core\Util\Entity;
use Google\Site_Kit\Core\Util\Entity_Factory;
/**
* Class representing the context in which the plugin is running.
*
* @since 1.0.0
* @access private
* @ignore
*/
class Context {
/**
* Primary "standard" AMP website mode.
*
* @since 1.0.0 Originally introduced.
* @since 1.36.0 Marked as unused, see description.
* @since 1.108.0 Removed the description and reinstated.
* @var string
*/
const AMP_MODE_PRIMARY = 'primary';
/**
* Secondary AMP website mode.
*
* @since 1.0.0
* @var string
*/
const AMP_MODE_SECONDARY = 'secondary';
/**
* Absolute path to the plugin main file.
*
* @since 1.0.0
* @var string
*/
private $main_file;
/**
* Internal storage for whether the plugin is network active or not.
*
* @since 1.0.0
* @var bool|null
*/
private $network_active = null;
/**
* Input access abstraction.
*
* @since 1.1.2
* @var Input
*/
private $input;
/**
* Constructor.
*
* @since 1.0.0
* @since 1.1.2 Added optional $input instance.
*
* @param string $main_file Absolute path to the plugin main file.
* @param Input $input Input instance.
*/
public function __construct( $main_file, ?Input $input = null ) {
$this->main_file = $main_file;
$this->input = $input ?: new Input();
}
/**
* Gets the absolute path for a path relative to the plugin directory.
*
* @since 1.0.0
*
* @param string $relative_path Optional. Relative path. Default '/'.
* @return string Absolute path.
*/
public function path( $relative_path = '/' ) {
return plugin_dir_path( $this->main_file ) . ltrim( $relative_path, '/' );
}
/**
* Gets the full URL for a path relative to the plugin directory.
*
* @since 1.0.0
*
* @param string $relative_path Optional. Relative path. Default '/'.
* @return string Full URL.
*/
public function url( $relative_path = '/' ) {
return plugin_dir_url( $this->main_file ) . ltrim( $relative_path, '/' );
}
/**
* Gets the Input instance.
*
* @since 1.1.2
*
* @return Input
*/
public function input() {
return $this->input;
}
/**
* Gets the full URL to an admin screen part of the plugin.
*
* @since 1.0.0
*
* @param string $slug Optional. Plugin admin screen slug. Default 'dashboard'.
* @param array $query_args Optional. Additional query args. Default empty array.
* @return string Full admin screen URL.
*/
public function admin_url( $slug = 'dashboard', array $query_args = array() ) {
unset( $query_args['page'] );
if ( $this->is_network_mode() ) {
$base_url = network_admin_url( 'admin.php' );
} else {
$base_url = admin_url( 'admin.php' );
}
return add_query_arg(
array_merge(
array( 'page' => Core\Admin\Screens::PREFIX . $slug ),
$query_args
),
$base_url
);
}
/**
* Determines whether the plugin is running in network mode.
*
* @since 1.0.0
*
* @return bool True if the plugin is in network mode, false otherwise.
*/
public function is_network_mode() {
// Bail if plugin is not network-active.
if ( ! $this->is_network_active() ) {
return false;
}
/**
* Filters whether network mode is active in Site Kit.
*
* This is always false by default since Site Kit does not support a network mode yet.
*
* @since 1.86.0
*
* @param bool $active Whether network mode is active.
*/
return (bool) apply_filters( 'googlesitekit_is_network_mode', false );
}
/**
* Gets the cannonical "home" URL.
*
* Returns the value from the `"googlesitekit_canonical_home_url"` filter.
*
* @since 1.18.0
*
* @return string Cannonical home URL.
*/
public function get_canonical_home_url() {
/**
* Filters the canonical home URL considered by Site Kit.
*
* Typically this is okay to be the unmodified `home_url()`, but certain plugins (e.g. multilingual plugins)
* that dynamically modify that value based on context can use this filter to ensure that the URL considered
* by Site Kit remains stable.
*
* @since 1.18.0
*
* @param string $home_url The value of `home_url()`.
*/
return apply_filters( 'googlesitekit_canonical_home_url', home_url() );
}
/**
* Gets the site URL of the reference site to use for stats.
*
* @since 1.0.0
*
* @return string Reference site URL.
*/
public function get_reference_site_url() {
return $this->filter_reference_url();
}
/**
* Gets the entity for the current request context.
*
* An entity in Site Kit terminology is based on a canonical URL, i.e. every
* canonical URL has an associated entity.
*
* An entity may also have a type, a title, and an ID.
*
* @since 1.7.0
*
* @return Entity|null The current entity, or null if none could be determined.
*/
public function get_reference_entity() {
// Support specific URL stats being checked in Site Kit dashboard details view.
if ( is_admin() && 'googlesitekit-dashboard' === $this->input()->filter( INPUT_GET, 'page' ) ) {
$entity_url_query_param = $this->input()->filter( INPUT_GET, 'permaLink' );
if ( ! empty( $entity_url_query_param ) ) {
return $this->get_reference_entity_from_url( $entity_url_query_param );
}
}
$entity = Entity_Factory::from_context();
return $this->filter_entity_reference_url( $entity );
}
/**
* Gets the entity for the given URL, if available.
*
* An entity in Site Kit terminology is based on a canonical URL, i.e. every
* canonical URL has an associated entity.
*
* An entity may also have a type, a title, and an ID.
*
* @since 1.10.0
*
* @param string $url URL to determine the entity from.
* @return Entity|null The current entity, or null if none could be determined.
*/
public function get_reference_entity_from_url( $url ) {
// Ensure local URL is used for lookup.
$url = str_replace(
$this->get_reference_site_url(),
untrailingslashit( $this->get_canonical_home_url() ),
$url
);
$entity = Entity_Factory::from_url( $url );
return $this->filter_entity_reference_url( $entity );
}
/**
* Gets the permalink of the reference site to use for stats.
*
* @since 1.0.0
*
* @param int|WP_Post $post Optional. Post ID or post object. Default is the global `$post`.
*
* @return string|false The reference permalink URL or false if post does not exist.
*/
public function get_reference_permalink( $post = 0 ) {
// If post is provided, get URL for that.
if ( $post ) {
$permalink = get_permalink( $post );
if ( false === $permalink ) {
return false;
}
return $this->filter_reference_url( $permalink );
}
// Otherwise use entity detection.
$entity = $this->get_reference_entity();
if ( ! $entity || 'post' !== $entity->get_type() ) {
return false;
}
return $entity->get_url();
}
/**
* Gets the canonical url for the current request.
*
* @since 1.0.0
*
* @return string|false The reference canonical URL or false if no URL was identified.
*/
public function get_reference_canonical() {
$entity = $this->get_reference_entity();
if ( ! $entity ) {
return false;
}
return $entity->get_url();
}
/**
* Checks whether AMP content is being served.
*
* @since 1.0.0
*
* @return bool True if an AMP request, false otherwise.
*/
public function is_amp() {
if ( is_singular( 'web-story' ) ) {
return true;
}
return function_exists( 'is_amp_endpoint' ) && is_amp_endpoint();
}
/**
* Gets the current AMP mode.
*
* @since 1.0.0
* @since 1.108.0 Extracted AMP plugin related logic to `get_amp_mode_from_amp_plugin` function.
*
* @return bool|string 'primary' if in standard mode,
* 'secondary' if in transitional or reader modes, or the Web Stories plugin is active
* false if AMP not active, or unknown mode
*/
public function get_amp_mode() {
$amp_mode = $this->get_amp_mode_from_amp_plugin();
if ( false === $amp_mode ) {
// If the Web Stories plugin is enabled, consider the site to be running
// in Secondary AMP mode.
if ( defined( 'WEBSTORIES_VERSION' ) ) {
return self::AMP_MODE_SECONDARY;
}
}
return $amp_mode;
}
/**
* Gets the current AMP mode from the AMP plugin.
*
* @since 1.108.0
*
* @return bool|string 'primary' if in standard mode,
* 'secondary' if in transitional or reader modes
* false if AMP not active, or unknown mode
*/
private function get_amp_mode_from_amp_plugin() {
if ( ! class_exists( 'AMP_Theme_Support' ) ) {
return false;
}
$exposes_support_mode = defined( 'AMP_Theme_Support::STANDARD_MODE_SLUG' )
&& defined( 'AMP_Theme_Support::TRANSITIONAL_MODE_SLUG' )
&& defined( 'AMP_Theme_Support::READER_MODE_SLUG' );
if ( defined( 'AMP__VERSION' ) ) {
$amp_plugin_version = AMP__VERSION;
if ( strpos( $amp_plugin_version, '-' ) !== false ) {
$amp_plugin_version = explode( '-', $amp_plugin_version )[0];
}
$amp_plugin_version_2_or_higher = version_compare( $amp_plugin_version, '2.0.0', '>=' );
} else {
$amp_plugin_version_2_or_higher = false;
}
if ( $amp_plugin_version_2_or_higher ) {
$exposes_support_mode = class_exists( 'AMP_Options_Manager' )
&& method_exists( 'AMP_Options_Manager', 'get_option' )
&& $exposes_support_mode;
} else {
$exposes_support_mode = class_exists( 'AMP_Theme_Support' )
&& method_exists( 'AMP_Theme_Support', 'get_support_mode' )
&& $exposes_support_mode;
}
if ( $exposes_support_mode ) {
// If recent version, we can properly detect the mode.
if ( $amp_plugin_version_2_or_higher ) {
$mode = AMP_Options_Manager::get_option( 'theme_support' );
} else {
$mode = AMP_Theme_Support::get_support_mode();
}
if ( AMP_Theme_Support::STANDARD_MODE_SLUG === $mode ) {
return self::AMP_MODE_PRIMARY;
}
if ( in_array( $mode, array( AMP_Theme_Support::TRANSITIONAL_MODE_SLUG, AMP_Theme_Support::READER_MODE_SLUG ), true ) ) {
return self::AMP_MODE_SECONDARY;
}
} elseif ( function_exists( 'amp_is_canonical' ) ) {
// On older versions, if it is not primary AMP, it is definitely secondary AMP (transitional or reader mode).
if ( amp_is_canonical() ) {
return self::AMP_MODE_PRIMARY;
}
return self::AMP_MODE_SECONDARY;
}
return false;
}
/**
* Checks whether the plugin is network active.
*
* @since 1.0.0
*
* @return bool True if plugin is network active, false otherwise.
*/
public function is_network_active() {
// Determine $network_active property just once per request, to not unnecessarily run this complex logic on every call.
if ( null === $this->network_active ) {
if ( is_multisite() ) {
$network_active_plugins = wp_get_active_network_plugins();
// Consider MU plugins and network-activated plugins as network-active.
$this->network_active = strpos( wp_normalize_path( __FILE__ ), wp_normalize_path( WPMU_PLUGIN_DIR ) ) === 0
|| in_array( WP_PLUGIN_DIR . '/' . GOOGLESITEKIT_PLUGIN_BASENAME, $network_active_plugins, true );
} else {
$this->network_active = false;
}
}
return $this->network_active;
}
/**
* Filters the given entity's reference URL, effectively creating a copy of
* the entity with the reference URL accounted for.
*
* @since 1.15.0
*
* @param Entity|null $entity Entity to filter reference ID for, or null.
* @return Entity|null Filtered entity or null, based on $entity.
*/
private function filter_entity_reference_url( ?Entity $entity = null ) {
if ( ! $entity ) {
return null;
}
return new Entity(
$this->filter_reference_url( $entity->get_url() ),
array(
'type' => $entity->get_type(),
'title' => $entity->get_title(),
'id' => $entity->get_id(),
)
);
}
/**
* Filters the given URL to ensure the reference URL is used as part of it.
*
* If the site reference URL differs from the home URL (e.g. via filters),
* this method performs the necessary replacement.
*
* @since 1.7.0
*
* @param string $url Optional. Input URL. If not provided, returns the plain reference site URL.
* @return string URL that starts with the reference site URL.
*/
private function filter_reference_url( $url = '' ) {
$site_url = untrailingslashit( $this->get_canonical_home_url() );
/**
* Filters the reference site URL to use for stats.
*
* This can be used to override the current site URL, for example when using the plugin on a non-public site,
* such as in a staging environment.
*
* @since 1.0.0
*
* @param string $site_url Reference site URL, typically the WordPress home URL.
*/
$reference_site_url = apply_filters( 'googlesitekit_site_url', $site_url );
$reference_site_url = untrailingslashit( $reference_site_url );
// Ensure this is not empty.
if ( empty( $reference_site_url ) ) {
$reference_site_url = $site_url;
}
// If no URL given, just return the reference site URL.
if ( empty( $url ) ) {
return $reference_site_url;
}
// Replace site URL with the reference site URL.
if ( $reference_site_url !== $site_url ) {
$url = str_replace( $site_url, $reference_site_url, $url );
}
return $url;
}
/**
* Calls the WordPress core functions to get the locale and return it in the required format.
*
* @since 1.32.0
*
* @param string $context Optional. Defines which WordPress core locale function to call.
* @param string $format Optional. Defines the format the locale is returned in.
* @return string Locale in the required format.
*/
public function get_locale( $context = 'site', $format = 'default' ) {
// Get the site or user locale.
if ( 'user' === $context ) {
$wp_locale = get_user_locale();
} else {
$wp_locale = get_locale();
}
// Return locale in the required format.
if ( 'language-code' === $format ) {
$code_array = explode( '_', $wp_locale );
return $code_array[0];
} elseif ( 'language-variant' === $format ) {
$variant_array = explode( '_', $wp_locale );
$variant_string = implode( '_', array_slice( $variant_array, 0, 2 ) );
return $variant_string;
}
return $wp_locale;
}
}

View File

@@ -0,0 +1,129 @@
<?php
/**
* Class Google\Site_Kit\Core\Admin\Authorize_Application
*
* @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\Core\Admin;
use Google\Site_Kit\Context;
use Google\Site_Kit\Core\Assets\Assets;
use Google\Site_Kit\Core\Util\Method_Proxy_Trait;
/**
* Class to handle all wp-admin Authorize Application related functionality.
*
* @since 1.126.0
* @access private
* @ignore
*/
final class Authorize_Application {
use Method_Proxy_Trait;
/**
* Plugin context.
*
* @since 1.126.0
* @var Context
*/
private $context;
/**
* Assets instance.
*
* @since 1.126.0
* @var Assets
*/
private $assets;
/**
* Constructor.
*
* @since 1.126.0
*
* @param Context $context Plugin context.
* @param Assets $assets Optional. Assets API instance. Default is a new instance.
*/
public function __construct(
Context $context,
?Assets $assets = null
) {
$this->context = $context;
$this->assets = $assets ?: new Assets( $this->context );
}
/**
* Registers functionality through WordPress hooks.
*
* @since 1.126.0
*/
public function register() {
add_action( 'admin_enqueue_scripts', $this->get_method_proxy( 'enqueue_assets' ) );
add_action( 'admin_footer', $this->get_method_proxy( 'render_custom_footer' ) );
}
/**
* Checks if the current screen is the Authorize Application screen.
*
* @since 1.126.0
*
* @return bool True if the current screen is the Authorize Application screen, false otherwise.
*/
protected function is_authorize_application_screen() {
$current_screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
if ( $current_screen instanceof \WP_Screen && 'authorize-application' === $current_screen->id ) {
return true;
}
return false;
}
/**
* Checks if the current service is a Google service.
*
* @since 1.126.0
*
* @return bool True if the current service is a Google service, false otherwise.
*/
protected function is_google_service() {
$success_url = isset( $_GET['success_url'] ) ? esc_url_raw( wp_unslash( $_GET['success_url'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification
$success_url = sanitize_text_field( $success_url );
$parsed_url = wp_parse_url( $success_url );
if ( empty( $parsed_url['host'] ) ) {
return false;
}
// Check if the domain is a '*.google.com' domain.
return preg_match( '/\.google\.com$/', $parsed_url['host'] ) === 1;
}
/**
* Enqueues assets for the Authorize Application screen.
*
* @since 1.126.0
*/
private function enqueue_assets() {
if ( $this->is_authorize_application_screen() && $this->is_google_service() ) {
$this->assets->enqueue_asset( 'googlesitekit-authorize-application-css' );
}
}
/**
* Renders custom footer for the Authorize Application screen if the service is a Google service.
*
* @since 1.126.0
*/
private function render_custom_footer() {
if ( $this->is_authorize_application_screen() && $this->is_google_service() ) {
echo '<div class="googlesitekit-authorize-application__footer"><p>' . esc_html__( 'Powered by Site Kit', 'google-site-kit' ) . '</p></div>';
}
}
}

View File

@@ -0,0 +1,67 @@
<?php
/**
* Class Google\Site_Kit\Core\Admin\Available_Tools
*
* @package Google\Site_Kit\Core\Admin
* @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\Core\Admin;
use Google\Site_Kit\Core\Permissions\Permissions;
use Google\Site_Kit\Core\Util\Method_Proxy_Trait;
use Google\Site_Kit\Core\Util\Reset;
/**
* Class for extending available tools for Site Kit.
*
* @since 1.30.0
* @access private
* @ignore
*/
class Available_Tools {
use Method_Proxy_Trait;
/**
* Registers functionality through WordPress hooks.
*
* @since 1.30.0
*/
public function register() {
add_action( 'tool_box', $this->get_method_proxy( 'render_tool_box' ) );
}
/**
* Renders tool box output.
*
* @since 1.30.0
*/
private function render_tool_box() {
if ( ! current_user_can( Permissions::SETUP ) ) {
return;
}
?>
<div class="card">
<h2 class="title"><?php esc_html_e( 'Reset Site Kit', 'google-site-kit' ); ?></h2>
<p>
<?php
esc_html_e(
'Resetting will disconnect all users and remove all Site Kit settings and data within WordPress. You and any other users who wish to use Site Kit will need to reconnect to restore access.',
'google-site-kit'
)
?>
</p>
<p>
<a
class="button button-primary"
href="<?php echo esc_url( Reset::url() ); ?>"
>
<?php esc_html_e( 'Reset Site Kit', 'google-site-kit' ); ?>
</a>
</p>
</div>
<?php
}
}

View File

@@ -0,0 +1,203 @@
<?php
/**
* Class Google\Site_Kit\Core\Admin\Dashboard
*
* @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\Core\Admin;
use Google\Site_Kit\Context;
use Google\Site_Kit\Core\Assets\Assets;
use Google\Site_Kit\Core\Authentication\Authentication;
use Google\Site_Kit\Core\Modules\Modules;
use Google\Site_Kit\Core\Permissions\Permissions;
use Google\Site_Kit\Core\Util\Requires_Javascript_Trait;
/**
* Class to handle all wp-admin Dashboard related functionality.
*
* @since 1.0.0
* @access private
* @ignore
*/
final class Dashboard {
use Requires_Javascript_Trait;
/**
* Plugin context.
*
* @since 1.0.0
* @var Context
*/
private $context;
/**
* Assets Instance.
*
* @since 1.0.0
* @var Assets
*/
private $assets;
/**
* Modules instance.
*
* @since 1.7.0
* @var Modules
*/
private $modules;
/**
* Authentication instance.
*
* @since 1.120.0
* @var Authentication
*/
private $authentication;
/**
* Constructor.
*
* @since 1.0.0
*
* @param Context $context Plugin context.
* @param Assets $assets Optional. Assets API instance. Default is a new instance.
* @param Modules $modules Optional. Modules instance. Default is a new instance.
*/
public function __construct(
Context $context,
?Assets $assets = null,
?Modules $modules = null
) {
$this->context = $context;
$this->assets = $assets ?: new Assets( $this->context );
$this->modules = $modules ?: new Modules( $this->context );
$this->authentication = new Authentication( $this->context );
}
/**
* Registers functionality through WordPress hooks.
*
* @since 1.0.0
*/
public function register() {
add_action(
'wp_dashboard_setup',
function () {
$this->add_widgets();
}
);
}
/**
* Add a Site Kit by Google widget to the WordPress admin dashboard.
*
* @since 1.0.0
*/
private function add_widgets() {
if ( ! current_user_can( Permissions::VIEW_WP_DASHBOARD_WIDGET ) ) {
return;
}
// Enqueue styles.
$this->assets->enqueue_asset( 'googlesitekit-wp-dashboard-css' );
// Enqueue scripts.
$this->assets->enqueue_asset( 'googlesitekit-wp-dashboard' );
$this->modules->enqueue_assets();
wp_add_dashboard_widget(
'google_dashboard_widget',
__( 'Site Kit Summary', 'google-site-kit' ),
function () {
$this->render_googlesitekit_wp_dashboard();
}
);
}
/**
* Render the Site Kit WordPress Dashboard widget.
*
* @since 1.0.0
* @since 1.120.0 Added the `data-view-only` attribute.
*/
private function render_googlesitekit_wp_dashboard() {
$active_modules = $this->modules->get_active_modules();
$analytics_connected = isset( $active_modules['analytics-4'] ) && $active_modules['analytics-4']->is_connected();
$search_console_connected = isset( $active_modules['search-console'] ) && $active_modules['search-console']->is_connected();
$is_view_only = ! $this->authentication->is_authenticated();
$can_view_shared_analytics = current_user_can( Permissions::READ_SHARED_MODULE_DATA, 'analytics-4' );
$can_view_shared_search_console = current_user_can( Permissions::READ_SHARED_MODULE_DATA, 'search-console' );
$display_analytics_data = ( ! $is_view_only && $analytics_connected ) || ( $is_view_only && $can_view_shared_analytics );
$display_search_console_data = ( ! $is_view_only && $search_console_connected ) || ( $is_view_only && $can_view_shared_search_console );
$class_names = array();
if ( $analytics_connected && $display_analytics_data ) {
$class_names[] = 'googlesitekit-wp-dashboard-analytics_active_and_connected';
}
if ( $search_console_connected && $display_search_console_data ) {
$class_names[] = 'googlesitekit-wp-dashboard-search_console_active_and_connected';
}
if ( ! $analytics_connected && ! $is_view_only ) {
$class_names[] = 'googlesitekit-wp-dashboard-analytics-activate-cta';
}
$class_names = implode( ' ', $class_names );
$this->render_noscript_html();
?>
<div id="js-googlesitekit-wp-dashboard" data-view-only="<?php echo esc_attr( $is_view_only ); ?>" class="googlesitekit-plugin <?php echo esc_attr( $class_names ); ?>">
<div class="googlesitekit-wp-dashboard googlesitekit-wp-dashboard-loading">
<?php
$this->render_loading_container( 'googlesitekit-wp-dashboard__cta' );
?>
<div class="googlesitekit-wp-dashboard-stats">
<?php
if ( $display_analytics_data ) {
$this->render_loading_container( 'googlesitekit-wp-dashboard-loading__can_view_analytics' );
}
if ( $display_search_console_data ) {
$this->render_loading_container( 'googlesitekit-wp-dashboard-loading__search_console_active_and_connected' );
}
if ( ! $analytics_connected && ! $is_view_only ) {
$this->render_loading_container( 'googlesitekit-wp-dashboard-stats__cta' );
}
if ( $display_analytics_data ) {
$this->render_loading_container( 'googlesitekit-unique-visitors-chart-widget' );
$this->render_loading_container( 'googlesitekit-search-console-widget' );
}
?>
</div>
</div>
</div>
<?php
}
/**
* Render the loading container when data is not available and being fetched.
*
* @since 1.144.0
* @param string $class_names Class names to add to the container.
* @return void
*/
private function render_loading_container( $class_names ) {
?>
<div class="googlesitekit-preview-block <?php echo esc_attr( $class_names ); ?>">
<div class="googlesitekit-preview-block__wrapper"></div>
</div>
<?php
}
}

View File

@@ -0,0 +1,132 @@
<?php
/**
* Class Google\Site_Kit\Core\Admin\Notice
*
* @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\Core\Admin;
/**
* Class representing a single notice.
*
* @since 1.0.0
* @access private
* @ignore
*/
final class Notice {
const TYPE_SUCCESS = 'success';
const TYPE_INFO = 'info';
const TYPE_WARNING = 'warning';
const TYPE_ERROR = 'error';
/**
* Unique notice slug.
*
* @since 1.0.0
* @var string
*/
private $slug;
/**
* Notice arguments.
*
* @since 1.0.0
* @var array
*/
private $args = array();
/**
* Constructor.
*
* @since 1.0.0
*
* @param string $slug Unique notice slug.
* @param array $args {
* Associative array of notice arguments.
*
* @type string $content Required notice content. May contain inline HTML tags.
* @type string $type Notice type. Either 'success', 'info', 'warning', 'error'. Default 'info'.
* @type callable $active_callback Callback function to determine whether the notice is active in the
* current context. The current admin screen's hook suffix is passed to
* the callback. Default is that the notice is active unconditionally.
* @type bool $dismissible Whether the notice should be dismissible. Default false.
* }
*/
public function __construct( $slug, array $args ) {
$this->slug = $slug;
$this->args = wp_parse_args(
$args,
array(
'content' => '',
'type' => self::TYPE_INFO,
'active_callback' => null,
'dismissible' => false,
)
);
}
/**
* Gets the notice slug.
*
* @since 1.0.0
*
* @return string Unique notice slug.
*/
public function get_slug() {
return $this->slug;
}
/**
* Checks whether the notice is active.
*
* This method executes the active callback in order to determine whether the notice should be active or not.
*
* @since 1.0.0
*
* @param string $hook_suffix The current admin screen hook suffix.
* @return bool True if the notice is active, false otherwise.
*/
public function is_active( $hook_suffix ) {
if ( ! $this->args['content'] ) {
return false;
}
if ( ! $this->args['active_callback'] ) {
return true;
}
return (bool) call_user_func( $this->args['active_callback'], $hook_suffix );
}
/**
* Renders the notice.
*
* @since 1.0.0
*/
public function render() {
if ( is_callable( $this->args['content'] ) ) {
$content = call_user_func( $this->args['content'] );
if ( empty( $content ) ) {
return;
}
} else {
$content = '<p>' . wp_kses( $this->args['content'], 'googlesitekit_admin_notice' ) . '</p>';
}
$class = 'notice notice-' . $this->args['type'];
if ( $this->args['dismissible'] ) {
$class .= ' is-dismissible';
}
?>
<div id="<?php echo esc_attr( 'googlesitekit-notice-' . $this->slug ); ?>" class="<?php echo esc_attr( $class ); ?>">
<?php echo $content; /* phpcs:ignore WordPress.Security.EscapeOutput */ ?>
</div>
<?php
}
}

View File

@@ -0,0 +1,93 @@
<?php
/**
* Class Google\Site_Kit\Core\Admin\Notices
*
* @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\Core\Admin;
/**
* Class managing admin notices.
*
* @since 1.0.0
* @access private
* @ignore
*/
final class Notices {
/**
* Registers functionality through WordPress hooks.
*
* @since 1.0.0
*/
public function register() {
$callback = function () {
global $hook_suffix;
if ( empty( $hook_suffix ) ) {
return;
}
$this->render_notices( $hook_suffix );
};
add_action( 'admin_notices', $callback );
add_action( 'network_admin_notices', $callback );
}
/**
* Renders admin notices.
*
* @since 1.0.0
*
* @param string $hook_suffix The current admin screen hook suffix.
*/
private function render_notices( $hook_suffix ) {
$notices = $this->get_notices();
if ( empty( $notices ) ) {
return;
}
/**
* Notice object.
*
* @var Notice $notice Notice object.
*/
foreach ( $notices as $notice ) {
if ( ! $notice->is_active( $hook_suffix ) ) {
continue;
}
$notice->render();
}
}
/**
* Gets available admin notices.
*
* @since 1.0.0
*
* @return array List of Notice instances.
*/
private function get_notices() {
/**
* Filters the list of available admin notices.
*
* @since 1.0.0
*
* @param array $notices List of Notice instances.
*/
$notices = apply_filters( 'googlesitekit_admin_notices', array() );
return array_filter(
$notices,
function ( $notice ) {
return $notice instanceof Notice;
}
);
}
}

View File

@@ -0,0 +1,71 @@
<?php
/**
* Class Google\Site_Kit\Core\Admin\Plugin_Action_Links
*
* @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\Core\Admin;
use Google\Site_Kit\Context;
use Google\Site_Kit\Core\Permissions\Permissions;
/**
* Class for managing plugin action links.
*
* @since 1.41.0
* @access private
* @ignore
*/
class Plugin_Action_Links {
/**
* Plugin context.
*
* @since 1.41.0
* @var Context
*/
private $context;
/**
* Constructor.
*
* @since 1.41.0
*
* @param Context $context Plugin context.
*/
public function __construct(
Context $context
) {
$this->context = $context;
}
/**
* Registers functionality through WordPress hooks.
*
* @since 1.41.0
*/
public function register() {
add_filter(
'plugin_action_links_' . GOOGLESITEKIT_PLUGIN_BASENAME,
function ( $links ) {
if ( current_user_can( Permissions::MANAGE_OPTIONS ) ) {
$settings_link = sprintf(
'<a href="%s">%s</a>',
esc_url( $this->context->admin_url( 'settings' ) ),
esc_html__( 'Settings', 'google-site-kit' )
);
array_unshift( $links, $settings_link );
}
return $links;
}
);
}
}

View File

@@ -0,0 +1,54 @@
<?php
/**
* Class Google\Site_Kit\Core\Admin\Plugin_Row_Meta
*
* @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\Core\Admin;
/**
* Class for managing plugin row meta.
*
* @since 1.24.0
* @access private
* @ignore
*/
class Plugin_Row_Meta {
/**
* Registers functionality through WordPress hooks.
*
* @since 1.24.0
*/
public function register() {
add_filter(
'plugin_row_meta',
function ( $meta, $plugin_file ) {
if ( GOOGLESITEKIT_PLUGIN_BASENAME === $plugin_file ) {
return array_merge( $meta, $this->get_plugin_row_meta() );
}
return $meta;
},
10,
2
);
}
/**
* Builds an array of anchor elements to be shown in the plugin row.
*
* @since 1.24.0
*
* @return string[] Array of links as HTML strings.
*/
private function get_plugin_row_meta() {
return array(
'<a href="https://wordpress.org/support/plugin/google-site-kit/reviews/#new-post">' . __( 'Rate Site Kit', 'google-site-kit' ) . '</a>',
'<a href="https://wordpress.org/support/plugin/google-site-kit/#new-post">' . __( 'Support', 'google-site-kit' ) . '</a>',
);
}
}

View File

@@ -0,0 +1,151 @@
<?php
/**
* Class Google\Site_Kit\Core\Admin\Pointer
*
* @package Google\Site_Kit
* @copyright 2022 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\Admin;
/**
* Class representing a single pointer.
*
* @since 1.83.0
* @access private
* @ignore
*/
final class Pointer {
/**
* Unique pointer slug.
*
* @since 1.83.0
* @var string
*/
private $slug;
/**
* Pointer arguments.
*
* @since 1.83.0
* @var array
*/
private $args = array();
/**
* Constructor.
*
* @since 1.83.0
*
* @param string $slug Unique pointer slug.
* @param array $args {
* Associative array of pointer arguments.
*
* @type string $title Required. Pointer title.
* @type string $content Required. Pointer content. May contain inline HTML tags.
* @type string $target_id Required. ID of the element the pointer should be attached to.
* @type string|array $position Optional. Position of the pointer. Can be 'top', 'bottom', 'left', 'right',
* or an array of `edge` and `align`. Default 'top'.
* @type callable $active_callback Optional. Callback function to determine whether the pointer is active in
* the current context. The current admin screen's hook suffix is passed to
* the callback. Default is that the pointer is active unconditionally.
* }
*/
public function __construct( $slug, array $args ) {
$this->slug = $slug;
$this->args = wp_parse_args(
$args,
array(
'title' => '',
'content' => '',
'target_id' => '',
'position' => 'top',
'active_callback' => null,
)
);
}
/**
* Gets the pointer slug.
*
* @since 1.83.0
*
* @return string Unique pointer slug.
*/
public function get_slug() {
return $this->slug;
}
/**
* Gets the pointer title.
*
* @since 1.83.0
*
* @return string Pointer title.
*/
public function get_title() {
return $this->args['title'];
}
/**
* Gets the pointer content.
*
* @since 1.83.0
*
* @return string Pointer content.
*/
public function get_content() {
if ( is_callable( $this->args['content'] ) ) {
return call_user_func( $this->args['content'] );
} else {
return '<p>' . wp_kses( $this->args['content'], 'googlesitekit_admin_pointer' ) . '</p>';
}
}
/**
* Gets the pointer target ID.
*
* @since 1.83.0
*
* @return string Pointer target ID.
*/
public function get_target_id() {
return $this->args['target_id'];
}
/**
* Gets the pointer position.
*
* @since 1.83.0
*
* @return string|array Pointer position.
*/
public function get_position() {
return $this->args['position'];
}
/**
* Checks whether the pointer is active.
*
* This method executes the active callback in order to determine whether the pointer should be active or not.
*
* @since 1.83.0
*
* @param string $hook_suffix The current admin screen hook suffix.
* @return bool True if the pointer is active, false otherwise.
*/
public function is_active( $hook_suffix ) {
if ( empty( $this->args['title'] ) || empty( $this->args['content'] ) || empty( $this->args['target_id'] ) ) {
return false;
}
if ( ! is_callable( $this->args['active_callback'] ) ) {
return true;
}
return (bool) call_user_func( $this->args['active_callback'], $hook_suffix );
}
}

View File

@@ -0,0 +1,150 @@
<?php
/**
* Class Google\Site_Kit\Core\Admin\Pointers
*
* @package Google\Site_Kit\Core\Admin
* @copyright 2022 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\Admin;
use Google\Site_Kit\Core\Util\BC_Functions;
use Google\Site_Kit\Core\Util\Method_Proxy_Trait;
/**
* Class for managing pointers.
*
* @since 1.83.0
* @access private
* @ignore
*/
class Pointers {
use Method_Proxy_Trait;
/**
* Registers functionality through WordPress hooks.
*
* @since 1.83.0
*/
public function register() {
add_action( 'admin_enqueue_scripts', $this->get_method_proxy( 'enqueue_pointers' ) );
}
/**
* Enqueues pointer scripts.
*
* @since 1.83.0
*
* @param string $hook_suffix The current admin page.
*/
private function enqueue_pointers( $hook_suffix ) {
if ( empty( $hook_suffix ) ) {
return;
}
$pointers = $this->get_pointers();
if ( empty( $pointers ) ) {
return;
}
$active_pointers = array_filter(
$pointers,
function ( Pointer $pointer ) use ( $hook_suffix ) {
return $pointer->is_active( $hook_suffix );
}
);
if ( empty( $active_pointers ) ) {
return;
}
wp_enqueue_style( 'wp-pointer' );
wp_enqueue_script( 'wp-pointer' );
add_action(
'admin_print_footer_scripts',
function () use ( $active_pointers ) {
foreach ( $active_pointers as $pointer ) {
$this->print_pointer_script( $pointer );
}
}
);
}
/**
* Gets pointers.
*
* @since 1.83.0
*
* @return Pointer[] Array of pointers.
*/
private function get_pointers() {
/**
* Filters the list of available pointers.
*
* @since 1.83.0
*
* @param array $pointers List of Pointer instances.
*/
$pointers = apply_filters( 'googlesitekit_admin_pointers', array() );
return array_filter(
$pointers,
function ( $pointer ) {
return $pointer instanceof Pointer;
}
);
}
/**
* Prints script for a given pointer.
*
* @since 1.83.0
*
* @param Pointer $pointer Pointer to print.
*/
private function print_pointer_script( $pointer ) {
$content = $pointer->get_content();
if ( empty( $content ) ) {
return;
}
$slug = $pointer->get_slug();
BC_Functions::wp_print_inline_script_tag(
sprintf(
'
jQuery( function() {
var options = {
content: "<h3>%s</h3>%s",
position: %s,
pointerWidth: 420,
close: function() {
jQuery.post(
window.ajaxurl,
{
pointer: "%s",
action: "dismiss-wp-pointer",
}
);
}
};
jQuery( "#%s" ).pointer( options ).pointer( "open" );
} );
',
esc_js( $pointer->get_title() ),
$content,
wp_json_encode( $pointer->get_position() ),
esc_js( $slug ),
esc_js( $pointer->get_target_id() )
),
array(
'id' => $slug,
)
);
}
}

View File

@@ -0,0 +1,260 @@
<?php
/**
* Class Google\Site_Kit\Core\Admin\Screen
*
* @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\Core\Admin;
use Google\Site_Kit\Context;
use Google\Site_Kit\Core\Assets\Assets;
use Google\Site_Kit\Core\Util\Google_Icon;
use Google\Site_Kit\Core\Util\Requires_Javascript_Trait;
/**
* Class representing a single screen.
*
* @since 1.0.0
* @access private
* @ignore
*/
final class Screen {
use Requires_Javascript_Trait;
const MENU_SLUG = 'googlesitekit';
/**
* Unique screen slug.
*
* @since 1.0.0
* @var string
*/
private $slug;
/**
* Screen arguments.
*
* @since 1.0.0
* @var array
*/
private $args = array();
/**
* Constructor.
*
* @since 1.0.0
*
* @param string $slug Unique screen slug.
* @param array $args {
* Associative array of screen arguments.
*
* @type callable $render_callback Required callback to render the page content.
* @type string $title Required screen title.
* @type string $capability Capability required to access the screen. Default is 'manage_options'.
* @type string $menu_title Title to display in the menu (only if $add_to_menu is true). Default is
* the value of $title.
* @type string $parent_slug Slug of the parent menu screen (only if $add_to_menu is true). Default
* empty string (which means it will be a top-level page).
* @type callable $enqueue_callback Callback to enqueue additional scripts or stylesheets. The base admin
* script and stylesheet will always be enqueued. Default null.
* @type callable $initialize_callback Callback to run actions when initializing the screen, before headers are
* sent and markup is generated. Default null.
* }
*/
public function __construct( $slug, array $args ) {
$this->slug = $slug;
$this->args = wp_parse_args(
$args,
array(
'render_callback' => null,
'title' => '',
'capability' => 'manage_options',
'menu_title' => '',
'parent_slug' => self::MENU_SLUG,
'enqueue_callback' => null,
'initialize_callback' => null,
)
);
if ( empty( $this->args['menu_title'] ) ) {
$this->args['menu_title'] = $this->args['title'];
}
$this->args['title'] = __( 'Site Kit by Google', 'google-site-kit' ) . ' ' . $this->args['title'];
}
/**
* Gets the unique screen slug.
*
* @since 1.0.0
*
* @return string Unique screen slug.
*/
public function get_slug() {
return $this->slug;
}
/**
* Adds the screen to the WordPress admin backend.
*
* @since 1.0.0
*
* @param Context $context Plugin context, used for URL generation.
* @return string Hook suffix of the screen, or empty string if not added.
*/
public function add( Context $context ) {
static $menu_slug = null;
if ( ! $this->args['title'] ) {
return '';
}
// A parent slug of null means the screen will not appear in the menu.
$parent_slug = null;
// If parent slug is provided, use it as parent.
if ( ! empty( $this->args['parent_slug'] ) ) {
$parent_slug = $this->args['parent_slug'];
// If parent slug is 'googlesitekit', append to main Site Kit menu.
if ( self::MENU_SLUG === $parent_slug ) {
// If this is null, it means no menu has been added yet.
if ( null === $menu_slug ) {
add_menu_page(
$this->args['title'],
__( 'Site Kit', 'google-site-kit' ),
$this->args['capability'],
$this->slug,
'',
'data:image/svg+xml;base64,' . Google_Icon::to_base64()
);
$menu_slug = $this->slug;
/**
* An SVG icon file needs to be colored (filled) based on the theme color setting.
*
* This exists in js as wp.svgPainter() per:
* https://github.com/WordPress/WordPress/blob/5.7/wp-admin/js/svg-painter.js
*
* The downside of the js approach is that we get a brief flash of an unstyled icon
* until the JS runs.
*
* A user can pick a custom Admin Color Scheme, which is only available in admin_init
* or later actions. add_menu_page runs on the admin_menu action, which precedes admin_init
* per https://codex.wordpress.org/Plugin_API/Action_Reference
*
* WordPress provides some color schemes out of the box, but they can also be added via
* wp_admin_css_color()
*
* Our workaround is to set the icon and subsequently replace it in current_screen, which is
* what we do in the following action.
*/
add_action(
'current_screen',
function () {
global $menu, $_wp_admin_css_colors;
if ( ! is_array( $menu ) ) {
return;
}
$color_scheme = get_user_option( 'admin_color' ) ?: 'fresh';
// If we're on one of the sitekit pages, use the 'current' color, otherwise use the 'base' color.
// @see wp_admin_css_color().
$color_key = false === strpos( get_current_screen()->id, 'googlesitekit' ) ? 'base' : 'current';
if ( empty( $_wp_admin_css_colors[ $color_scheme ]->icon_colors[ $color_key ] ) ) {
return;
}
$color = $_wp_admin_css_colors[ $color_scheme ]->icon_colors[ $color_key ];
foreach ( $menu as &$item ) {
if ( 'googlesitekit-dashboard' === $item[2] ) {
$item[6] = 'data:image/svg+xml;base64,' . Google_Icon::to_base64( Google_Icon::with_fill( $color ) );
break;
}
}
},
100
);
}
// Set parent slug to actual slug of main Site Kit menu.
$parent_slug = $menu_slug;
}
}
// If submenu item or not in menu, use add_submenu_page().
return (string) add_submenu_page(
$parent_slug,
$this->args['title'],
$this->args['menu_title'],
$this->args['capability'],
$this->slug,
function () use ( $context ) {
$this->render( $context );
}
);
}
/**
* Runs actions when initializing the screen, before sending headers and generating markup.
*
* @since 1.0.0
*
* @param Context $context Plugin context.
*/
public function initialize( Context $context ) {
if ( ! $this->args['initialize_callback'] ) {
return;
}
call_user_func( $this->args['initialize_callback'], $context );
}
/**
* Enqueues assets for the screen.
*
* @since 1.0.0
*
* @param Assets $assets Assets instance to rely on for enqueueing assets.
*/
public function enqueue_assets( Assets $assets ) {
// Enqueue base admin screen stylesheet.
$assets->enqueue_asset( 'googlesitekit-admin-css' );
$cb = is_callable( $this->args['enqueue_callback'] )
? $this->args['enqueue_callback']
: function ( Assets $assets ) {
$assets->enqueue_asset( $this->slug );
};
call_user_func( $cb, $assets );
}
/**
* Renders the screen content.
*
* @since 1.0.0
*
* @param Context $context Plugin context.
*/
private function render( Context $context ) {
$cb = is_callable( $this->args['render_callback'] )
? $this->args['render_callback']
: function () {
printf( '<div id="js-%s" class="googlesitekit-page"></div>', esc_attr( $this->slug ) );
};
echo '<div class="googlesitekit-plugin">';
$this->render_noscript_html();
call_user_func( $cb, $context );
echo '</div>';
}
}

View File

@@ -0,0 +1,517 @@
<?php
/**
* Class Google\Site_Kit\Core\Admin\Screens
*
* @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\Core\Admin;
use Google\Site_Kit\Context;
use Google\Site_Kit\Core\Assets\Assets;
use Google\Site_Kit\Core\Authentication\Authentication;
use Google\Site_Kit\Core\Dismissals\Dismissed_Items;
use Google\Site_Kit\Core\Key_Metrics\Key_Metrics_Setup_Completed_By;
use Google\Site_Kit\Core\Modules\Modules;
use Google\Site_Kit\Core\Permissions\Permissions;
use Google\Site_Kit\Core\Storage\Options;
use Google\Site_Kit\Core\Storage\User_Options;
/**
* Class managing admin screens.
*
* @since 1.0.0
* @access private
* @ignore
*/
final class Screens {
const PREFIX = 'googlesitekit-';
const PARENT_SLUG_NULL = self::PREFIX . 'null';
/**
* Plugin context.
*
* @since 1.0.0
* @var Context
*/
private $context;
/**
* Assets API instance.
*
* @since 1.0.0
* @var Assets
*/
private $assets;
/**
* Modules instance.
*
* @since 1.7.0
* @var Modules
*/
private $modules;
/**
* Authentication instance.
*
* @since 1.72.0
* @var Authentication
*/
private $authentication;
/**
* Associative array of $hook_suffix => $screen pairs.
*
* @since 1.0.0
* @var array
*/
private $screens = array();
/**
* Constructor.
*
* @since 1.0.0
*
* @param Context $context Plugin context.
* @param Assets $assets Optional. Assets API instance. Default is a new instance.
* @param Modules $modules Optional. Modules instance. Default is a new instance.
* @param Authentication $authentication Optional. Authentication instance. Default is a new instance.
*/
public function __construct(
Context $context,
?Assets $assets = null,
?Modules $modules = null,
?Authentication $authentication = null
) {
$this->context = $context;
$this->assets = $assets ?: new Assets( $this->context );
$this->modules = $modules ?: new Modules( $this->context );
$this->authentication = $authentication ?: new Authentication( $this->context );
}
/**
* Registers functionality through WordPress hooks.
*
* @since 1.0.0
*/
public function register() {
if ( $this->context->is_network_mode() ) {
add_action(
'network_admin_menu',
function () {
$this->add_screens();
}
);
}
add_action(
'admin_menu',
function () {
$this->add_screens();
}
);
add_action(
'admin_enqueue_scripts',
function ( $hook_suffix ) {
$this->enqueue_screen_assets( $hook_suffix );
}
);
add_action(
'admin_page_access_denied',
function () {
// Redirect dashboard to splash if no dashboard access (yet).
$this->no_access_redirect_dashboard_to_splash();
// Redirect splash to (shared) dashboard if splash is dismissed.
$this->no_access_redirect_splash_to_dashboard();
// Redirect module pages to dashboard.
$this->no_access_redirect_module_to_dashboard();
}
);
// Ensure the menu icon always is rendered correctly, without enqueueing a global CSS file.
add_action(
'admin_head',
function () {
?>
<style type="text/css">
#adminmenu .toplevel_page_googlesitekit-dashboard img {
width: 16px;
}
#adminmenu .toplevel_page_googlesitekit-dashboard.current img,
#adminmenu .toplevel_page_googlesitekit-dashboard.wp-has-current-submenu img {
opacity: 1;
}
</style>
<?php
}
);
$remove_notices_callback = function () {
global $hook_suffix;
if ( empty( $hook_suffix ) ) {
return;
}
if ( isset( $this->screens[ $hook_suffix ] ) ) {
remove_all_actions( current_action() );
}
};
add_action( 'admin_notices', $remove_notices_callback, -9999 );
add_action( 'network_admin_notices', $remove_notices_callback, -9999 );
add_action( 'all_admin_notices', $remove_notices_callback, -9999 );
add_filter( 'custom_menu_order', '__return_true' );
add_filter(
'menu_order',
function ( array $menu_order ) {
// Move the Site Kit dashboard menu item to be one after the index.php item if it exists.
$dashboard_index = array_search( 'index.php', $menu_order, true );
$sitekit_index = false;
foreach ( $menu_order as $key => $value ) {
if ( strpos( $value, self::PREFIX ) === 0 ) {
$sitekit_index = $key;
$sitekit_value = $value;
break;
}
}
if ( false === $dashboard_index || false === $sitekit_index ) {
return $menu_order;
}
unset( $menu_order[ $sitekit_index ] );
array_splice( $menu_order, $dashboard_index + 1, 0, $sitekit_value );
return $menu_order;
}
);
}
/**
* Gets the Screen instance for a given hook suffix.
*
* @since 1.11.0
*
* @param string $hook_suffix The hook suffix associated with the screen to retrieve.
* @return Screen|null Screen instance if available, otherwise null;
*/
public function get_screen( $hook_suffix ) {
return isset( $this->screens[ $hook_suffix ] ) ? $this->screens[ $hook_suffix ] : null;
}
/**
* Adds all screens to the admin.
*
* @since 1.0.0
*/
private function add_screens() {
$screens = $this->get_screens();
array_walk( $screens, array( $this, 'add_screen' ) );
}
/**
* Adds the given screen to the admin.
*
* @since 1.0.0
*
* @param Screen $screen Screen to add.
*/
private function add_screen( Screen $screen ) {
$hook_suffix = $screen->add( $this->context );
if ( empty( $hook_suffix ) ) {
return;
}
add_action(
"load-{$hook_suffix}",
function () use ( $screen ) {
$screen->initialize( $this->context );
}
);
$this->screens[ $hook_suffix ] = $screen;
}
/**
* Enqueues assets if a plugin screen matches the given hook suffix.
*
* @since 1.0.0
*
* @param string $hook_suffix Hook suffix for the current admin screen.
*/
private function enqueue_screen_assets( $hook_suffix ) {
if ( ! isset( $this->screens[ $hook_suffix ] ) ) {
return;
}
$this->screens[ $hook_suffix ]->enqueue_assets( $this->assets );
$this->modules->enqueue_assets();
}
/**
* Redirects from the dashboard to the splash screen if permissions to access the dashboard are currently not met.
*
* Dashboard permission access is conditional based on whether the user has successfully authenticated. When
* e.g. accessing the dashboard manually or having it open in a separate tab while disconnecting in the other tab,
* it is a better user experience to redirect to the splash screen so that the user can re-authenticate.
*
* The only time the dashboard should fail with the regular WordPress permissions error is when the current user is
* not eligible for accessing Site Kit entirely, i.e. if they are not allowed to authenticate.
*
* @since 1.12.0
*/
private function no_access_redirect_dashboard_to_splash() {
global $plugin_page;
// At this point, our preferred `$hook_suffix` is not set, and the dashboard page will not even be registered,
// so we need to rely on the `$plugin_page` global here.
if ( ! isset( $plugin_page ) || self::PREFIX . 'dashboard' !== $plugin_page ) {
return;
}
if ( current_user_can( Permissions::VIEW_SPLASH ) ) {
wp_safe_redirect(
$this->context->admin_url( 'splash' )
);
exit;
}
}
/**
* Redirects from the splash to the dashboard screen if permissions to access the splash are currently not met.
*
* Admins always have the ability to view the splash page, so this redirects non-admins who have access
* to view the shared dashboard if the splash has been dismissed.
* Currently the dismissal check is built into the capability for VIEW_SPLASH so this is implied.
*
* @since 1.77.0
*/
private function no_access_redirect_splash_to_dashboard() {
global $plugin_page;
if ( ! isset( $plugin_page ) || self::PREFIX . 'splash' !== $plugin_page ) {
return;
}
if ( current_user_can( Permissions::VIEW_DASHBOARD ) ) {
wp_safe_redirect(
$this->context->admin_url()
);
exit;
}
}
/**
* Redirects module pages to the dashboard or splash based on user capability.
*
* @since 1.69.0
*/
private function no_access_redirect_module_to_dashboard() {
global $plugin_page;
$legacy_module_pages = array(
self::PREFIX . 'module-adsense',
self::PREFIX . 'module-analytics',
self::PREFIX . 'module-search-console',
);
if ( ! in_array( $plugin_page, $legacy_module_pages, true ) ) {
return;
}
// Note: the use of add_query_arg is intentional below because it preserves
// the current query parameters in the URL.
if ( current_user_can( Permissions::VIEW_DASHBOARD ) ) {
wp_safe_redirect(
add_query_arg( 'page', self::PREFIX . 'dashboard' )
);
exit;
}
if ( current_user_can( Permissions::VIEW_SPLASH ) ) {
wp_safe_redirect(
add_query_arg( 'page', self::PREFIX . 'splash' )
);
exit;
}
}
/**
* Gets available admin screens.
*
* @since 1.0.0
*
* @return array List of Screen instances.
*/
private function get_screens() {
$show_splash_in_menu = current_user_can( Permissions::VIEW_SPLASH ) && ! current_user_can( Permissions::VIEW_DASHBOARD );
$screens = array(
new Screen(
self::PREFIX . 'dashboard',
array(
'title' => __( 'Dashboard', 'google-site-kit' ),
'capability' => Permissions::VIEW_DASHBOARD,
'enqueue_callback' => function ( Assets $assets ) {
if ( $this->context->input()->filter( INPUT_GET, 'permaLink' ) ) {
$assets->enqueue_asset( 'googlesitekit-entity-dashboard' );
} else {
$assets->enqueue_asset( 'googlesitekit-main-dashboard' );
}
},
'render_callback' => function ( Context $context ) {
$is_view_only = ! $this->authentication->is_authenticated();
$setup_slug = htmlspecialchars( $context->input()->filter( INPUT_GET, 'slug' ) ?: '' );
$reauth = $context->input()->filter( INPUT_GET, 'reAuth', FILTER_VALIDATE_BOOLEAN );
if ( $context->input()->filter( INPUT_GET, 'permaLink' ) ) {
?>
<div id="js-googlesitekit-entity-dashboard" data-view-only="<?php echo esc_attr( $is_view_only ); ?>" class="googlesitekit-page"></div>
<?php
} else {
$setup_module_slug = $setup_slug && $reauth ? $setup_slug : '';
if ( $setup_module_slug ) {
$active_modules = $this->modules->get_active_modules();
if ( ! array_key_exists( $setup_module_slug, $active_modules ) ) {
try {
$module_details = $this->modules->get_module( $setup_module_slug );
/* translators: %s: The module name */
$message = sprintf( __( 'The %s module cannot be set up as it has not been activated yet.', 'google-site-kit' ), $module_details->name );
} catch ( \Exception $e ) {
$message = $e->getMessage();
}
wp_die( sprintf( '<span class="googlesitekit-notice">%s</span>', esc_html( $message ) ), 403 );
}
}
?>
<div id="js-googlesitekit-main-dashboard" data-view-only="<?php echo esc_attr( $is_view_only ); ?>" data-setup-module-slug="<?php echo esc_attr( $setup_module_slug ); ?>" class="googlesitekit-page"></div>
<?php
}
},
)
),
new Screen(
self::PREFIX . 'splash',
array(
'title' => __( 'Dashboard', 'google-site-kit' ),
'capability' => Permissions::VIEW_SPLASH,
'parent_slug' => $show_splash_in_menu ? Screen::MENU_SLUG : self::PARENT_SLUG_NULL,
// This callback will redirect to the dashboard on successful authentication.
'initialize_callback' => function ( Context $context ) {
// Get the dismissed items for this user.
$user_options = new User_Options( $context );
$dismissed_items = new Dismissed_Items( $user_options );
$splash_context = $context->input()->filter( INPUT_GET, 'googlesitekit_context' );
$reset_session = $context->input()->filter( INPUT_GET, 'googlesitekit_reset_session', FILTER_VALIDATE_BOOLEAN );
// If the user is authenticated, redirect them to the disconnect URL and then send them back here.
if ( ! $reset_session && 'revoked' === $splash_context && $this->authentication->is_authenticated() ) {
$this->authentication->disconnect();
wp_safe_redirect( add_query_arg( array( 'googlesitekit_reset_session' => 1 ) ) );
exit;
}
// Don't consider redirect if the current user cannot access the dashboard (yet).
if ( ! current_user_can( Permissions::VIEW_DASHBOARD ) ) {
return;
}
// Redirect to dashboard if user is authenticated or if
// they have already accessed the shared dashboard.
if (
$this->authentication->is_authenticated() ||
(
! current_user_can( Permissions::AUTHENTICATE ) &&
$dismissed_items->is_dismissed( 'shared_dashboard_splash' ) &&
current_user_can( Permissions::VIEW_SHARED_DASHBOARD )
)
) {
wp_safe_redirect(
$context->admin_url(
'dashboard',
array(
// Pass through the notification parameter, or removes it if none.
'notification' => $context->input()->filter( INPUT_GET, 'notification' ),
)
)
);
exit;
}
},
)
),
new Screen(
self::PREFIX . 'settings',
array(
'title' => __( 'Settings', 'google-site-kit' ),
'capability' => Permissions::MANAGE_OPTIONS,
)
),
);
$screens[] = new Screen(
self::PREFIX . 'user-input',
array(
'title' => __( 'User Input', 'google-site-kit' ),
'capability' => Permissions::MANAGE_OPTIONS,
'parent_slug' => self::PARENT_SLUG_NULL,
)
);
$screens[] = new Screen(
self::PREFIX . 'ad-blocking-recovery',
array(
'title' => __( 'Ad Blocking Recovery', 'google-site-kit' ),
'capability' => Permissions::MANAGE_OPTIONS,
'parent_slug' => self::PARENT_SLUG_NULL,
)
);
$screens[] = new Screen(
self::PREFIX . 'metric-selection',
array(
'title' => __( 'Select Key Metrics', 'google-site-kit' ),
'capability' => Permissions::MANAGE_OPTIONS,
'parent_slug' => self::PARENT_SLUG_NULL,
// This callback will redirect to the dashboard if key metrics is already set up.
'initialize_callback' => function ( Context $context ) {
$options = new Options( $context );
$is_key_metrics_setup = ( new Key_Metrics_Setup_Completed_By( $options ) )->get();
if ( $is_key_metrics_setup ) {
wp_safe_redirect(
$context->admin_url( 'dashboard' )
);
exit;
}
},
)
);
$screens[] = new Screen(
self::PREFIX . 'key-metrics-setup',
array(
'title' => __( 'Key Metrics Setup', 'google-site-kit' ),
'capability' => Permissions::MANAGE_OPTIONS,
'parent_slug' => self::PARENT_SLUG_NULL,
)
);
return $screens;
}
}

View File

@@ -0,0 +1,122 @@
<?php
/**
* Class Google\Site_Kit\Core\Admin\Standalone
*
* @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\Core\Admin;
use Google\Site_Kit\Context;
use Google\Site_Kit\Core\Assets\Stylesheet;
/**
* Class managing standalone mode.
*
* @since 1.8.0
* @access private
* @ignore
*/
final class Standalone {
/**
* Plugin context.
*
* @since 1.8.0
*
* @var Context
*/
private $context;
/**
* Constructor.
*
* @since 1.8.0
*
* @param Context $context Plugin context.
*/
public function __construct( Context $context ) {
$this->context = $context;
}
/**
* Standalone mode
*
* @since 1.8.0
*/
public function register() {
if ( ! $this->is_standalone() ) {
return;
}
/**
* Appends the standalone admin body class.
*
* @since 1.8.0
*
* @param string $admin_body_classes Admin body classes.
* @return string Filtered admin body classes.
*/
add_filter(
'admin_body_class',
function ( $admin_body_classes ) {
return "{$admin_body_classes} googlesitekit-standalone";
}
);
remove_action( 'in_admin_header', 'wp_admin_bar_render', 0 );
add_filter( 'admin_footer_text', '__return_empty_string', PHP_INT_MAX );
add_filter( 'update_footer', '__return_empty_string', PHP_INT_MAX );
add_action(
'admin_head',
function () {
$this->print_standalone_styles();
}
);
}
/**
* Detects if we are in Google Site Kit standalone mode.
*
* @since 1.8.0
*
* @return boolean True when in standalone mode, else false.
*/
public function is_standalone() {
global $pagenow;
$page = htmlspecialchars( $this->context->input()->filter( INPUT_GET, 'page' ) ?: '' );
$standalone = $this->context->input()->filter( INPUT_GET, 'googlesitekit-standalone', FILTER_VALIDATE_BOOLEAN );
return ( 'admin.php' === $pagenow && false !== strpos( $page, 'googlesitekit' ) && $standalone );
}
/**
* Enqueues styles for standalone mode.
*
* @since 1.8.0
*/
private function print_standalone_styles() {
?>
<style type="text/css">
html {
padding-top: 0 !important;
}
body.googlesitekit-standalone #adminmenumain {
display: none;
}
body.googlesitekit-standalone #wpcontent {
margin-left: 0;
}
</style>
<?php
}
}

View File

@@ -0,0 +1,406 @@
<?php
/**
* Class Google\Site_Kit\Core\Admin_Bar\Admin_Bar
*
* @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\Core\Admin_Bar;
use Google\Site_Kit\Context;
use Google\Site_Kit\Core\Modules\Modules;
use Google\Site_Kit\Core\Permissions\Permissions;
use Google\Site_Kit\Core\Assets\Assets;
use Google\Site_Kit\Core\Authentication\Authentication;
use Google\Site_Kit\Core\REST_API\REST_Route;
use Google\Site_Kit\Core\REST_API\REST_Routes;
use Google\Site_Kit\Core\Storage\Options;
use Google\Site_Kit\Core\Util\Method_Proxy_Trait;
use Google\Site_Kit\Core\Util\Requires_Javascript_Trait;
use WP_REST_Server;
use WP_REST_Request;
/**
* Class handling the plugin's admin bar menu.
*
* @since 1.0.0
* @access private
* @ignore
*/
final class Admin_Bar {
use Requires_Javascript_Trait;
use Method_Proxy_Trait;
/**
* Plugin context.
*
* @since 1.0.0
* @var Context
*/
private $context;
/**
* Assets Instance.
*
* @since 1.0.0
* @var Assets
*/
private $assets;
/**
* Modules instance.
*
* @since 1.4.0
* @var Modules
*/
private $modules;
/**
* Admin_Bar_Enabled instance.
*
* @since 1.39.0
* @var Admin_Bar_Enabled
*/
private $admin_bar_enabled;
/**
* Authentication instance.
*
* @since 1.120.0
* @var Authentication
*/
private $authentication;
/**
* Constructor.
*
* @since 1.0.0
*
* @param Context $context Plugin context.
* @param Assets $assets Optional. Assets API instance. Default is a new instance.
* @param Modules $modules Optional. Modules instance. Default is a new instance.
*/
public function __construct(
Context $context,
?Assets $assets = null,
?Modules $modules = null
) {
$this->context = $context;
$this->assets = $assets ?: new Assets( $this->context );
$this->modules = $modules ?: new Modules( $this->context );
$options = new Options( $this->context );
$this->admin_bar_enabled = new Admin_Bar_Enabled( $options );
$this->authentication = new Authentication( $this->context );
}
/**
* Registers functionality through WordPress hooks.
*
* @since 1.0.0
*/
public function register() {
add_action( 'admin_bar_menu', $this->get_method_proxy( 'add_menu_button' ), 99 );
add_action( 'admin_enqueue_scripts', $this->get_method_proxy( 'enqueue_assets' ), 40 );
add_action( 'wp_enqueue_scripts', $this->get_method_proxy( 'enqueue_assets' ), 40 );
// TODO: This can be removed at some point, see https://github.com/ampproject/amp-wp/pull/4001.
add_filter( 'amp_dev_mode_element_xpaths', array( $this, 'add_amp_dev_mode' ) );
add_filter(
'googlesitekit_rest_routes',
function ( $routes ) {
return array_merge( $routes, $this->get_rest_routes() );
}
);
add_filter(
'googlesitekit_apifetch_preload_paths',
function ( $routes ) {
return array_merge(
$routes,
array(
'/' . REST_Routes::REST_ROOT . '/core/site/data/admin-bar-settings',
)
);
}
);
$this->admin_bar_enabled->register();
}
/**
* Add data-ampdevmode attributes to the elements that need it.
*
* @see \Google\Site_Kit\Core\Assets\Assets::get_assets() The 'googlesitekit' string is added to all inline scripts.
* @see \Google\Site_Kit\Core\Assets\Assets::add_amp_dev_mode_attributes() The data-ampdevmode attribute is added to registered scripts/styles here.
*
* @param string[] $xpath_queries XPath queries for elements that should get the data-ampdevmode attribute.
* @return string[] XPath queries.
*/
public function add_amp_dev_mode( $xpath_queries ) {
$xpath_queries[] = '//script[ contains( text(), "googlesitekit" ) ]';
return $xpath_queries;
}
/**
* Render the Adminbar button.
*
* @since 1.0.0
*
* @param object $wp_admin_bar The WP AdminBar object.
*/
private function add_menu_button( $wp_admin_bar ) {
if ( ! $this->is_active() ) {
return;
}
$args = array(
'id' => 'google-site-kit',
'title' => '<span class="googlesitekit-wp-adminbar__icon"></span> <span class="googlesitekit-wp-adminbar__label">Site Kit</span>',
'href' => '#',
'meta' => array(
'class' => 'menupop googlesitekit-wp-adminbar',
),
);
if ( $this->context->is_amp() && ! $this->is_amp_dev_mode() ) {
$post = get_post();
if ( ! $post || ! current_user_can( 'edit_post', $post->ID ) ) {
return;
}
$args['href'] = add_query_arg( 'googlesitekit_adminbar_open', 'true', get_edit_post_link( $post->ID ) );
} else {
$args['meta']['html'] = $this->menu_markup();
}
$wp_admin_bar->add_node( $args );
}
/**
* Checks if admin bar menu is active and displaying.
*
* @since 1.0.0
*
* @return bool True if Admin bar should display, False when it's not.
*/
public function is_active() {
// Only active if the admin bar is showing.
if ( ! is_admin_bar_showing() ) {
return false;
}
// In the admin, never show the admin bar except for the post editing screen.
if ( is_admin() && ! $this->is_admin_post_screen() ) {
return false;
}
if ( ! current_user_can( Permissions::VIEW_ADMIN_BAR_MENU ) ) {
return false;
}
$enabled = $this->admin_bar_enabled->get();
if ( ! $enabled ) {
return false;
}
// No entity was identified - don't display the admin bar menu.
$entity = $this->context->get_reference_entity();
if ( ! $entity ) {
return false;
}
// Check permissions for viewing post data.
if ( in_array( $entity->get_type(), array( 'post', 'blog' ), true ) && $entity->get_id() ) {
// If a post entity, check permissions for that post.
if ( ! current_user_can( Permissions::VIEW_POST_INSIGHTS, $entity->get_id() ) ) {
return false;
}
}
$current_url = $entity->get_url();
/**
* Filters whether the Site Kit admin bar menu should be displayed.
*
* The admin bar menu is only shown when there is data for the current URL and the current
* user has the correct capability to view the data. Modules use this filter to indicate the
* presence of valid data.
*
* @since 1.0.0
*
* @param bool $display Whether to display the admin bar menu.
* @param string $current_url The URL of the current request.
*/
return apply_filters( 'googlesitekit_show_admin_bar_menu', true, $current_url );
}
/**
* Checks if current screen is an admin edit post screen.
*
* @since 1.0.0
*/
private function is_admin_post_screen() {
$current_screen = function_exists( 'get_current_screen' ) ? get_current_screen() : false;
// No screen context available.
if ( ! $current_screen instanceof \WP_Screen ) {
return false;
}
// Only show for post screens.
if ( 'post' !== $current_screen->base ) {
return false;
}
// Don't show for new post screen.
if ( 'add' === $current_screen->action ) {
return false;
}
return true;
}
/**
* Checks whether AMP dev mode is enabled.
*
* This is only relevant if the current context is AMP.
*
* @since 1.1.0
* @since 1.120.0 Added the `data-view-only` attribute.
*
* @return bool True if AMP dev mode is enabled, false otherwise.
*/
private function is_amp_dev_mode() {
return function_exists( 'amp_is_dev_mode' ) && amp_is_dev_mode();
}
/**
* Return the Adminbar content markup.
*
* @since 1.0.0
*/
private function menu_markup() {
// Start buffer output.
ob_start();
$is_view_only = ! $this->authentication->is_authenticated();
?>
<div class="googlesitekit-plugin ab-sub-wrapper">
<?php $this->render_noscript_html(); ?>
<div id="js-googlesitekit-adminbar" data-view-only="<?php echo esc_attr( $is_view_only ); ?>" class="googlesitekit-adminbar">
<?php
/**
* Display server rendered content before JS-based adminbar modules.
*
* @since 1.0.0
*/
do_action( 'googlesitekit_adminbar_modules_before' );
?>
<section id="js-googlesitekit-adminbar-modules" class="googlesitekit-adminbar-modules"></section>
<?php
/**
* Display server rendered content after JS-based adminbar modules.
*
* @since 1.0.0
*/
do_action( 'googlesitekit_adminbar_modules_after' );
?>
</div>
</div>
<?php
// Get the buffer output.
$markup = ob_get_clean();
return $markup;
}
/**
* Enqueues assets.
*
* @since 1.39.0
*/
private function enqueue_assets() {
if ( ! $this->is_active() ) {
return;
}
// Enqueue styles.
$this->assets->enqueue_asset( 'googlesitekit-adminbar-css' );
if ( $this->context->is_amp() && ! $this->is_amp_dev_mode() ) {
// AMP Dev Mode support was added in v1.4, and if it is not enabled then short-circuit since scripts will be invalid.
return;
}
// Enqueue scripts.
$this->assets->enqueue_asset( 'googlesitekit-adminbar' );
$this->modules->enqueue_assets();
}
/**
* Gets related REST routes.
*
* @since 1.39.0
*
* @return array List of REST_Route objects.
*/
private function get_rest_routes() {
$can_authenticate = function () {
return current_user_can( Permissions::AUTHENTICATE );
};
$settings_callback = function () {
return array(
'enabled' => $this->admin_bar_enabled->get(),
);
};
return array(
new REST_Route(
'core/site/data/admin-bar-settings',
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => $settings_callback,
'permission_callback' => $can_authenticate,
),
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => function ( WP_REST_Request $request ) use ( $settings_callback ) {
$data = $request->get_param( 'data' );
if ( isset( $data['enabled'] ) ) {
$this->admin_bar_enabled->set( ! empty( $data['enabled'] ) );
}
return $settings_callback( $request );
},
'permission_callback' => $can_authenticate,
'args' => array(
'data' => array(
'type' => 'object',
'required' => true,
'properties' => array(
'enabled' => array(
'type' => 'boolean',
'required' => false,
),
),
),
),
),
)
),
);
}
}

View File

@@ -0,0 +1,72 @@
<?php
/**
* Class Google\Site_Kit\Core\Admin_Bar\Admin_Bar_Enabled
*
* @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\Core\Admin_Bar;
use Google\Site_Kit\Core\Storage\Setting;
/**
* Class handling the admin bar menu settings.
*
* @since 1.39.0
* @access private
* @ignore
*/
class Admin_Bar_Enabled extends Setting {
/**
* The option_name for this setting.
*/
const OPTION = 'googlesitekit_admin_bar_menu_enabled';
/**
* Gets the value of the setting.
*
* @since 1.39.0
*
* @return bool Value set for the option, or registered default if not set.
*/
public function get() {
return (bool) parent::get();
}
/**
* Gets the expected value type.
*
* @since 1.39.0
*
* @return string The type name.
*/
protected function get_type() {
return 'boolean';
}
/**
* Gets the default value.
*
* @since 1.39.0
*
* @return boolean The default value.
*/
protected function get_default() {
return true;
}
/**
* Gets the callback for sanitizing the setting's value before saving.
*
* @since 1.39.0
*
* @return callable The callable sanitize callback.
*/
protected function get_sanitize_callback() {
return 'boolval';
}
}

View File

@@ -0,0 +1,132 @@
<?php
/**
* Class Google\Site_Kit\Core\Assets\Asset
*
* @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\Core\Assets;
use Google\Site_Kit\Context;
/**
* Class representing a single asset.
*
* @since 1.0.0
* @access private
* @ignore
*/
abstract class Asset {
// Various page contexts for Site Kit in the WordPress Admin.
const CONTEXT_ADMIN_GLOBAL = 'admin-global';
const CONTEXT_ADMIN_POST_EDITOR = 'admin-post-editor';
const CONTEXT_ADMIN_BLOCK_EDITOR = 'admin-block-editor';
const CONTEXT_ADMIN_POSTS = 'admin-posts';
const CONTEXT_ADMIN_SITEKIT = 'admin-sitekit';
/**
* Unique asset handle.
*
* @since 1.0.0
* @var string
*/
protected $handle;
/**
* Asset arguments.
*
* @since 1.0.0
* @var array
*/
protected $args = array();
/**
* Constructor.
*
* @since 1.0.0
* @since 1.37.0 Add the 'load_contexts' argument.
*
* @param string $handle Unique asset handle.
* @param array $args {
* Associative array of asset arguments.
*
* @type string $src Required asset source URL.
* @type array $dependencies List of asset dependencies. Default empty array.
* @type string $version Asset version. Default is the version of Site Kit.
* @type bool $fallback Whether to only register as a fallback. Default false.
* @type callable $before_print Optional callback to execute before printing. Default none.
* @type string[] $load_contexts Optional array of page context values to determine on which page types to load this asset (see the `CONTEXT_` variables above).
* }
*/
public function __construct( $handle, array $args ) {
$this->handle = $handle;
$this->args = wp_parse_args(
$args,
array(
'src' => '',
'dependencies' => array(),
'version' => GOOGLESITEKIT_VERSION,
'fallback' => false,
'before_print' => null,
'load_contexts' => array( self::CONTEXT_ADMIN_SITEKIT ),
)
);
}
/**
* Gets the notice handle.
*
* @since 1.0.0
*
* @return string Unique notice handle.
*/
public function get_handle() {
return $this->handle;
}
/**
* Checks to see if the specified context exists for the current request.
*
* @since 1.37.0
*
* @param string $context Context value (see the `CONTEXT_` variables above).
* @return bool TRUE if context exists; FALSE otherwise.
*/
public function has_context( $context ) {
return in_array( $context, $this->args['load_contexts'], true );
}
/**
* Registers the asset.
*
* @since 1.0.0
* @since 1.15.0 Adds $context parameter.
*
* @param Context $context Plugin context.
*/
abstract public function register( Context $context );
/**
* Enqueues the asset.
*
* @since 1.0.0
*/
abstract public function enqueue();
/**
* Executes the extra callback if defined before printing the asset.
*
* @since 1.2.0
*/
final public function before_print() {
if ( ! is_callable( $this->args['before_print'] ) ) {
return;
}
call_user_func( $this->args['before_print'], $this->handle );
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,66 @@
<?php
/**
* Class Google\Site_Kit\Core\Assets\Manifest
*
* @package GoogleSite_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\Core\Assets;
use Google\Site_Kit\Plugin;
/**
* Assets manifest.
*
* @since 1.15.0
* @access private
* @ignore
*/
class Manifest {
/**
* Entries as $handle => [ $filename, $hash ] map.
*
* @since 1.48.0
* @var array
*/
private static $data;
/**
* Gets the manifest entry for the given handle.
*
* @since 1.48.0
*
* @param string $handle Asset handle to get manifest data for.
* @return array List of $filename and $hash, or `null` for both if not found.
*/
public static function get( $handle ) {
if ( null === self::$data ) {
self::load();
}
if ( isset( self::$data[ $handle ] ) ) {
return self::$data[ $handle ];
}
return array( null, null );
}
/**
* Loads the generated manifest file.
*
* @since 1.48.0
*/
private static function load() {
$path = Plugin::instance()->context()->path( 'dist/manifest.php' );
if ( file_exists( $path ) ) {
// If the include fails, $data will be `false`
// so this should only be attempted once.
self::$data = include $path;
}
}
}

View File

@@ -0,0 +1,174 @@
<?php
/**
* Class Google\Site_Kit\Core\Assets\Script
*
* @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\Core\Assets;
use Google\Site_Kit\Context;
use Google\Site_Kit\Core\Util\BC_Functions;
/**
* Class representing a single script.
*
* @since 1.0.0
* @access private
* @ignore
*/
class Script extends Asset {
/**
* Constructor.
*
* @since 1.0.0
*
* @param string $handle Unique script handle.
* @param array $args {
* Associative array of script arguments.
*
* @type string $src Required script source URL.
* @type array $dependencies List of script dependencies. Default empty array.
* @type string $version Script version. Default is the version of Site Kit.
* @type bool $fallback Whether to only register as a fallback. Default false.
* @type callable $before_print Optional callback to execute before printing. Default none.
* @type bool $in_footer Whether to load script in footer. Default true.
* @type string $execution How to handle script execution, e.g. 'defer'. Default empty string.
* }
*/
public function __construct( $handle, array $args ) {
parent::__construct( $handle, $args );
$this->args = wp_parse_args(
$this->args,
array(
'in_footer' => true,
'execution' => '',
)
);
}
/**
* Registers the script.
*
* @since 1.0.0
* @since 1.15.0 Adds $context parameter.
*
* @param Context $context Plugin context.
*/
public function register( Context $context ) {
if ( $this->args['fallback'] && wp_script_is( $this->handle, 'registered' ) ) {
return;
}
$src = $this->args['src'];
$version = $this->args['version'];
if ( $src ) {
$entry = Manifest::get( $this->handle );
if ( is_array( $entry[0] ) ) {
// If the first entry item is an array, we can assume `$entry` is an array of entries in the format filename => hash.
// In this scenario we want to match the nested entry against the filename provided in `$src`.
$src_filename = basename( $src );
foreach ( $entry as $entry_pair ) {
if ( $this->is_matching_manifest_entry( $entry_pair, $src_filename ) ) {
list( $filename, $hash ) = $entry_pair;
break;
}
}
} else {
// Otherwise, `$entry` will be a single entry in the format filename => hash.
list( $filename, $hash ) = $entry;
}
if ( $filename ) {
$src = $context->url( 'dist/assets/js/' . $filename );
$version = $hash;
}
}
wp_register_script(
$this->handle,
$src,
(array) $this->args['dependencies'],
$version,
$this->args['in_footer']
);
if ( ! empty( $this->args['execution'] ) ) {
wp_script_add_data( $this->handle, 'script_execution', $this->args['execution'] );
}
if ( ! empty( $src ) ) {
$this->set_locale_data();
}
}
/**
* Enqueues the script.
*
* @since 1.0.0
*/
public function enqueue() {
wp_enqueue_script( $this->handle );
}
/**
* Checks if the provided manifest entry matches the given filename.
*
* @since 1.89.0
*
* @param array $entry Array of filename, hash.
* @param string $src_filename Filename to check.
* @return bool
*/
private function is_matching_manifest_entry( array $entry, $src_filename ) {
list ( $filename, $hash ) = $entry;
if ( ! isset( $hash ) ) {
// If the hash is not set, it means the hash is embedded in the entry filename.
// Remove the hash then compare to the src filename.
$entry_filename_without_hash = preg_replace( '/-[a-f0-9]+\.js$/', '.js', $filename );
if ( $src_filename === $entry_filename_without_hash ) {
return true;
}
}
if ( $filename === $src_filename ) {
return true;
}
return false;
}
/**
* Sets locale data for the script, if it has translations.
*
* @since 1.21.0
*/
private function set_locale_data() {
$json_translations = load_script_textdomain( $this->handle, 'google-site-kit' );
if ( ! $json_translations ) {
return;
}
$output = <<<JS
( function( domain, translations ) {
try {
var localeData = translations.locale_data[ domain ] || translations.locale_data.messages;
localeData[""].domain = domain;
googlesitekit.i18n.setLocaleData( localeData, domain );
} catch {
}
} )( "google-site-kit", {$json_translations} );
JS;
wp_add_inline_script( $this->handle, $output, 'before' );
}
}

View File

@@ -0,0 +1,80 @@
<?php
/**
* Class Google\Site_Kit\Core\Assets\Script_Data
*
* @package Google\Site_Kit\Core\Assets
* @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\Core\Assets;
/**
* Class for virtual "data-only" scripts.
*
* @since 1.5.0
* @access private
* @ignore
*/
class Script_Data extends Script {
/**
* Constructor.
*
* @since 1.5.0
*
* @param string $handle Unique script handle.
* @param array $args {
* Associative array of script arguments.
*
* @type callable $data_callback Required. Function to return JSON-encodable data.
* @type string $global Required. Name of global variable to assign data to in Javascript.
* @type array $dependencies Optional. List of script dependencies. Default empty array.
* }
*/
public function __construct( $handle, array $args ) {
// Ensure required keys are always set.
$args = $args + array(
'data_callback' => null,
'global' => '',
);
// SRC will always be false.
$args['src'] = false;
parent::__construct( $handle, $args );
// Lazy-load script data before handle is to be printed.
$this->args['before_print'] = function ( $handle ) {
if ( empty( $this->args['global'] ) || ! is_callable( $this->args['data_callback'] ) ) {
return;
}
$data = call_user_func( $this->args['data_callback'], $handle );
$this->add_script_data( $data );
};
}
/**
* Adds the given data to the script handle's 'data' key.
*
* 'data' is the key used by `wp_localize_script`, which is output
* in older versions of WP even if the handle has no src (such as an alias).
* This is done manually instead of using `wp_localize_script` to avoid casting
* top-level keys to strings as this function is primarily intended for
* providing an array of translations to Javascript rather than arbitrary data.
*
* @see \WP_Scripts::localize
*
* @since 1.5.0
*
* @param mixed $data Data to be assigned to the defined global.
*/
private function add_script_data( $data ) {
$script_data = wp_scripts()->get_data( $this->handle, 'data' ) ?: '';
$js = sprintf(
'var %s = %s;',
preg_replace( '[^\w\d_-]', '', $this->args['global'] ), // Ensure only a-zA-Z0-9_- are allowed.
wp_json_encode( $data )
);
wp_scripts()->add_data( $this->handle, 'data', trim( "$script_data\n$js" ) );
}
}

View File

@@ -0,0 +1,91 @@
<?php
/**
* Class Google\Site_Kit\Core\Assets\Stylesheet
*
* @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\Core\Assets;
use Google\Site_Kit\Context;
/**
* Class representing a single stylesheet.
*
* @since 1.0.0
* @access private
* @ignore
*/
final class Stylesheet extends Asset {
/**
* Constructor.
*
* @since 1.0.0
*
* @param string $handle Unique stylesheet handle.
* @param array $args {
* Associative array of stylesheet arguments.
*
* @type string $src Required stylesheet source URL.
* @type array $dependencies List of stylesheet dependencies. Default empty array.
* @type string $version Stylesheet version. Default is the version of Site Kit.
* @type bool $fallback Whether to only register as a fallback. Default false.
* @type callable $before_print Optional callback to execute before printing. Default none.
* @type string $media Media for which the stylesheet is defined. Default 'all'.
* }
*/
public function __construct( $handle, array $args ) {
parent::__construct( $handle, $args );
$this->args = wp_parse_args(
$this->args,
array(
'media' => 'all',
)
);
}
/**
* Registers the stylesheet.
*
* @since 1.0.0
* @since 1.15.0 Adds $context parameter.
*
* @param Context $context Plugin context.
*/
public function register( Context $context ) {
if ( $this->args['fallback'] && wp_style_is( $this->handle, 'registered' ) ) {
return;
}
$src = $this->args['src'];
$version = $this->args['version'];
list( $filename, $hash ) = Manifest::get( $this->handle );
if ( $filename ) {
$src = $context->url( 'dist/assets/css/' . $filename );
$version = $hash;
}
wp_register_style(
$this->handle,
$src,
(array) $this->args['dependencies'],
$version,
$this->args['media']
);
}
/**
* Enqueues the stylesheet.
*
* @since 1.0.0
*/
public function enqueue() {
wp_enqueue_style( $this->handle );
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,197 @@
<?php
/**
* Class Google\Site_Kit\Core\Authentication\Clients\Client_Factory
*
* @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\Core\Authentication\Clients;
use Exception;
use Google\Site_Kit\Core\Authentication\Google_Proxy;
use Google\Site_Kit\Core\HTTP\Middleware;
use Google\Site_Kit_Dependencies\GuzzleHttp\Client;
use WP_HTTP_Proxy;
/**
* Class for creating Site Kit-specific Google_Client instances.
*
* @since 1.39.0
* @access private
* @ignore
*/
final class Client_Factory {
/**
* Creates a new Google client instance for the given arguments.
*
* @since 1.39.0
*
* @param array $args Associative array of arguments.
* @return Google_Site_Kit_Client|Google_Site_Kit_Proxy_Client The created Google client instance.
*/
public static function create_client( array $args ) {
$args = array_merge(
array(
'client_id' => '',
'client_secret' => '',
'redirect_uri' => '',
'token' => array(),
'token_callback' => null,
'token_exception_callback' => null,
'required_scopes' => array(),
'login_hint_email' => '',
'using_proxy' => true,
'proxy_url' => Google_Proxy::PRODUCTION_BASE_URL,
),
$args
);
if ( $args['using_proxy'] ) {
$client = new Google_Site_Kit_Proxy_Client(
array( 'proxy_base_path' => $args['proxy_url'] )
);
} else {
$client = new Google_Site_Kit_Client();
}
// Enable exponential retries, try up to three times.
$client->setConfig( 'retry', array( 'retries' => 3 ) );
$http_client = $client->getHttpClient();
$http_client_config = self::get_http_client_config( $http_client->getConfig() );
// In Guzzle 6+, the HTTP client is immutable, so only a new instance can be set.
$client->setHttpClient( new Client( $http_client_config ) );
$auth_config = self::get_auth_config( $args['client_id'], $args['client_secret'], $args['redirect_uri'] );
if ( ! empty( $auth_config ) ) {
try {
$client->setAuthConfig( $auth_config );
} catch ( Exception $e ) {
return $client;
}
}
// Offline access so we can access the refresh token even when the user is logged out.
$client->setAccessType( 'offline' );
$client->setPrompt( 'consent' );
$client->setRedirectUri( $args['redirect_uri'] );
$client->setScopes( (array) $args['required_scopes'] );
// Set the full token data.
if ( ! empty( $args['token'] ) ) {
$client->setAccessToken( $args['token'] );
}
// Set the callback which is called when the client refreshes the access token on-the-fly.
$token_callback = $args['token_callback'];
if ( $token_callback ) {
$client->setTokenCallback(
function ( $cache_key, $access_token ) use ( $client, $token_callback ) {
// The same token from this callback should also already be set in the client object, which is useful
// to get the full token data, all of which needs to be saved. Just in case, if that is not the same,
// we save the passed token only, relying on defaults for the other values.
$token = $client->getAccessToken();
if ( $access_token !== $token['access_token'] ) {
$token = array( 'access_token' => $access_token );
}
$token_callback( $token );
}
);
}
// Set the callback which is called when refreshing the access token on-the-fly fails.
$token_exception_callback = $args['token_exception_callback'];
if ( ! empty( $token_exception_callback ) ) {
$client->setTokenExceptionCallback( $token_exception_callback );
}
if ( ! empty( $args['login_hint_email'] ) ) {
$client->setLoginHint( $args['login_hint_email'] );
}
return $client;
}
/**
* Get HTTP client configuration.
*
* @since 1.115.0
*
* @param array $config Initial configuration.
* @return array The new HTTP client configuration.
*/
private static function get_http_client_config( $config ) {
// Override the default user-agent for the Guzzle client. This is used for oauth/token requests.
// By default this header uses the generic Guzzle client's user-agent and includes
// Guzzle, cURL, and PHP versions as it is normally shared.
// In our case however, the client is namespaced to be used by Site Kit only.
$config['headers']['User-Agent'] = Google_Proxy::get_application_name();
/** This filter is documented in wp-includes/class-http.php */
$ssl_verify = apply_filters( 'https_ssl_verify', true, null );
// If SSL verification is enabled (default) use the SSL certificate bundle included with WP.
if ( $ssl_verify ) {
$config['verify'] = ABSPATH . WPINC . '/certificates/ca-bundle.crt';
} else {
$config['verify'] = false;
}
// Configure the Google_Client's HTTP client to use the same HTTP proxy as WordPress HTTP, if set.
$http_proxy = new WP_HTTP_Proxy();
if ( $http_proxy->is_enabled() ) {
// See https://docs.guzzlephp.org/en/6.5/request-options.html#proxy for reference.
$auth = $http_proxy->use_authentication() ? "{$http_proxy->authentication()}@" : '';
$config['proxy'] = "{$auth}{$http_proxy->host()}:{$http_proxy->port()}";
}
// Respect WordPress HTTP request blocking settings.
$config['handler']->push(
Middleware::block_external_request()
);
/**
* Filters the IP version to force hostname resolution with.
*
* @since 1.115.0
*
* @param $force_ip_resolve null|string IP version to force. Default: null.
*/
$force_ip_resolve = apply_filters( 'googlesitekit_force_ip_resolve', null );
if ( in_array( $force_ip_resolve, array( null, 'v4', 'v6' ), true ) ) {
$config['force_ip_resolve'] = $force_ip_resolve;
}
return $config;
}
/**
* Returns the full OAuth credentials configuration data based on the given client ID and secret.
*
* @since 1.39.0
*
* @param string $client_id OAuth client ID.
* @param string $client_secret OAuth client secret.
* @param string $redirect_uri OAuth redirect URI.
* @return array Credentials data, or empty array if any of the given values is empty.
*/
private static function get_auth_config( $client_id, $client_secret, $redirect_uri ) {
if ( ! $client_id || ! $client_secret || ! $redirect_uri ) {
return array();
}
return array(
'client_id' => $client_id,
'client_secret' => $client_secret,
'auth_uri' => 'https://accounts.google.com/o/oauth2/v2/auth',
'token_uri' => 'https://oauth2.googleapis.com/token',
'auth_provider_x509_cert_url' => 'https://www.googleapis.com/oauth2/v1/certs',
'redirect_uris' => array( $redirect_uri ),
);
}
}

View File

@@ -0,0 +1,312 @@
<?php
/**
* Class Google\Site_Kit\Core\Authentication\Clients\Google_Site_Kit_Client
*
* @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\Core\Authentication\Clients;
use Google\Site_Kit\Core\Authentication\Clients\OAuth2;
use Google\Site_Kit\Core\Authentication\Exception\Google_OAuth_Exception;
use Google\Site_Kit_Dependencies\Google_Client;
use Google\Site_Kit_Dependencies\Google\Auth\HttpHandler\HttpHandlerFactory;
use Google\Site_Kit_Dependencies\Google\Auth\HttpHandler\HttpClientCache;
use Google\Site_Kit_Dependencies\GuzzleHttp\ClientInterface;
use Google\Site_Kit_Dependencies\Psr\Http\Message\RequestInterface;
use Google\Site_Kit\Core\Util\URL;
use Exception;
use InvalidArgumentException;
use LogicException;
use WP_User;
/**
* Extended Google API client with custom functionality for Site Kit.
*
* @since 1.2.0
* @access private
* @ignore
*/
class Google_Site_Kit_Client extends Google_Client {
/**
* Callback to pass a potential exception to while refreshing an access token.
*
* @since 1.2.0
* @var callable|null
*/
protected $token_exception_callback;
/**
* Construct the Google client.
*
* @since 1.2.0
*
* @param array $config Client configuration.
*/
public function __construct( array $config = array() ) {
if ( isset( $config['token_exception_callback'] ) ) {
$this->setTokenExceptionCallback( $config['token_exception_callback'] );
}
unset( $config['token_exception_callback'] );
parent::__construct( $config );
}
/**
* Sets the function to be called when fetching an access token results in an exception.
*
* @since 1.2.0
*
* @param callable $exception_callback Function accepting an exception as single parameter.
*/
public function setTokenExceptionCallback( callable $exception_callback ) {
$this->token_exception_callback = $exception_callback;
}
/**
* Sets whether or not to return raw requests and returns a callback to reset to the previous value.
*
* @since 1.2.0
*
* @param bool $defer Whether or not to return raw requests.
* @return callable Callback function that resets to the original $defer value.
*/
public function withDefer( $defer ) {
$orig_defer = $this->shouldDefer();
$this->setDefer( $defer );
// Return a function to restore the original refer value.
return function () use ( $orig_defer ) {
$this->setDefer( $orig_defer );
};
}
/**
* Adds auth listeners to the HTTP client based on the credentials set in the Google API Client object.
*
* @since 1.2.0
*
* @param ClientInterface $http The HTTP client object.
* @return ClientInterface The HTTP client object.
*
* @throws Exception Thrown when fetching a new access token via refresh token on-the-fly fails.
*/
public function authorize( ?ClientInterface $http = null ) {
if ( $this->isUsingApplicationDefaultCredentials() ) {
return parent::authorize( $http );
}
$token = $this->getAccessToken();
if ( isset( $token['refresh_token'] ) && $this->isAccessTokenExpired() ) {
$callback = $this->getConfig( 'token_callback' );
try {
$token_response = $this->fetchAccessTokenWithRefreshToken( $token['refresh_token'] );
if ( $callback ) {
// Due to original callback signature this can only accept the token itself.
call_user_func( $callback, '', $token_response['access_token'] );
}
} catch ( Exception $e ) {
// Pass exception to special callback if provided.
if ( $this->token_exception_callback ) {
call_user_func( $this->token_exception_callback, $e );
}
throw $e;
}
}
return parent::authorize( $http );
}
/**
* Fetches an OAuth 2.0 access token by using a temporary code.
*
* @since 1.0.0
* @since 1.2.0 Ported from Google_Site_Kit_Proxy_Client.
* @since 1.149.0 Added $code_verifier param for client v2.15.0 compatibility. (@link https://github.com/googleapis/google-api-php-client/commit/bded223ece445a6130cde82417b20180b1d6698a)
*
* @param string $code Temporary authorization code, or undelegated token code.
* @param string $code_verifier The code verifier used for PKCE (if applicable).
*
* @return array Access token.
*
* @throws InvalidArgumentException Thrown when the passed code is empty.
*/
public function fetchAccessTokenWithAuthCode( $code, $code_verifier = null ) {
if ( strlen( $code ) === 0 ) {
throw new InvalidArgumentException( 'Invalid code' );
}
$auth = $this->getOAuth2Service();
$auth->setCode( $code );
$auth->setRedirectUri( $this->getRedirectUri() );
if ( $code_verifier ) {
$auth->setCodeVerifier( $code_verifier );
}
$http_handler = HttpHandlerFactory::build( $this->getHttpClient() );
$token_response = $this->fetchAuthToken( $auth, $http_handler );
if ( $token_response && isset( $token_response['access_token'] ) ) {
$token_response['created'] = time();
$this->setAccessToken( $token_response );
}
return $token_response;
}
/**
* Fetches a fresh OAuth 2.0 access token by using a refresh token.
*
* @since 1.0.0
* @since 1.2.0 Ported from Google_Site_Kit_Proxy_Client.
*
* @param string $refresh_token Optional. Refresh token. Unused here.
* @param array $extra_params Optional. Array of extra parameters to fetch with.
* @return array Access token.
*
* @throws LogicException Thrown when no refresh token is available.
*/
public function fetchAccessTokenWithRefreshToken( $refresh_token = null, $extra_params = array() ) {
if ( null === $refresh_token ) {
$refresh_token = $this->getRefreshToken();
if ( ! $refresh_token ) {
throw new LogicException( 'refresh token must be passed in or set as part of setAccessToken' );
}
}
$this->getLogger()->info( 'OAuth2 access token refresh' );
$auth = $this->getOAuth2Service();
$auth->setRefreshToken( $refresh_token );
$http_handler = HttpHandlerFactory::build( $this->getHttpClient() );
$token_response = $this->fetchAuthToken( $auth, $http_handler, $extra_params );
if ( $token_response && isset( $token_response['access_token'] ) ) {
$token_response['created'] = time();
if ( ! isset( $token_response['refresh_token'] ) ) {
$token_response['refresh_token'] = $refresh_token;
}
$this->setAccessToken( $token_response );
/**
* Fires when the current user has just been reauthorized to access Google APIs with a refreshed access token.
*
* In other words, this action fires whenever Site Kit has just obtained a new access token based on
* the refresh token for the current user, which typically happens once every hour when using Site Kit,
* since that is the lifetime of every access token.
*
* @since 1.25.0
*
* @param array $token_response Token response data.
*/
do_action( 'googlesitekit_reauthorize_user', $token_response );
}
return $token_response;
}
/**
* Executes deferred HTTP requests.
*
* @since 1.38.0
*
* @param RequestInterface $request Request object to execute.
* @param string $expected_class Expected class to return.
* @return object An object of the type of the expected class or Psr\Http\Message\ResponseInterface.
*/
public function execute( RequestInterface $request, $expected_class = null ) {
$request = $request->withHeader( 'X-Goog-Quota-User', self::getQuotaUser() );
return parent::execute( $request, $expected_class );
}
/**
* Returns a string that uniquely identifies a user of the application.
*
* @since 1.38.0
*
* @return string Unique user identifier.
*/
public static function getQuotaUser() {
$user_id = get_current_user_id();
$url = get_home_url();
$scheme = URL::parse( $url, PHP_URL_SCHEME );
$host = URL::parse( $url, PHP_URL_HOST );
$path = URL::parse( $url, PHP_URL_PATH );
return "{$scheme}://{$user_id}@{$host}{$path}";
}
/**
* Fetches an OAuth 2.0 access token using a given auth object and HTTP handler.
*
* This method is used in place of {@see OAuth2::fetchAuthToken()}.
*
* @since 1.0.0
* @since 1.2.0 Ported from Google_Site_Kit_Proxy_Client.
*
* @param OAuth2 $auth OAuth2 instance.
* @param callable|null $http_handler Optional. HTTP handler callback. Default null.
* @param array $extra_params Optional. Array of extra parameters to fetch with.
* @return array Access token.
*/
protected function fetchAuthToken( OAuth2 $auth, ?callable $http_handler = null, $extra_params = array() ) {
if ( is_null( $http_handler ) ) {
$http_handler = HttpHandlerFactory::build( HttpClientCache::getHttpClient() );
}
$request = $auth->generateCredentialsRequest( $extra_params );
$response = $http_handler( $request );
$credentials = $auth->parseTokenResponse( $response );
if ( ! empty( $credentials['error'] ) ) {
$this->handleAuthTokenErrorResponse( $credentials['error'], $credentials );
}
$auth->updateToken( $credentials );
return $credentials;
}
/**
* Handles an erroneous response from a request to fetch an auth token.
*
* @since 1.2.0
*
* @param string $error Error code / error message.
* @param array $data Associative array of full response data.
*
* @throws Google_OAuth_Exception Thrown with the given $error as message.
*/
protected function handleAuthTokenErrorResponse( $error, array $data ) {
throw new Google_OAuth_Exception( $error );
}
/**
* Create a default Google OAuth2 object.
*
* @return OAuth2 Created OAuth2 instance.
*/
protected function createOAuth2Service() {
$auth = new OAuth2(
array(
'clientId' => $this->getClientId(),
'clientSecret' => $this->getClientSecret(),
'authorizationUri' => self::OAUTH2_AUTH_URL,
'tokenCredentialUri' => self::OAUTH2_TOKEN_URI,
'redirectUri' => $this->getRedirectUri(),
'issuer' => $this->getConfig( 'client_id' ),
'signingKey' => $this->getConfig( 'signing_key' ),
'signingAlgorithm' => $this->getConfig( 'signing_algorithm' ),
)
);
return $auth;
}
}

View File

@@ -0,0 +1,148 @@
<?php
/**
* Class Google\Site_Kit\Core\Authentication\Clients\Google_Site_Kit_Proxy_Client
*
* @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\Core\Authentication\Clients;
use Google\Site_Kit\Core\Authentication\Google_Proxy;
use Google\Site_Kit\Core\Authentication\Clients\OAuth2;
use Google\Site_Kit\Core\Authentication\Exception\Google_Proxy_Code_Exception;
use Google\Site_Kit_Dependencies\Google\Auth\HttpHandler\HttpHandlerFactory;
use Google\Site_Kit_Dependencies\GuzzleHttp\Psr7\Request;
use Google\Site_Kit_Dependencies\GuzzleHttp\Psr7\Utils;
use Exception;
/**
* Modified Google Site Kit API client relying on the authentication proxy.
*
* @since 1.0.0
* @since 1.2.0 Renamed to Google_Site_Kit_Proxy_Client.
* @access private
* @ignore
*/
class Google_Site_Kit_Proxy_Client extends Google_Site_Kit_Client {
/**
* Base URL to the proxy.
*
* @since 1.1.2
* @var string
*/
protected $proxy_base_path = Google_Proxy::PRODUCTION_BASE_URL;
/**
* Construct the Google client.
*
* @since 1.1.2
*
* @param array $config Proxy client configuration.
*/
public function __construct( array $config = array() ) {
if ( ! empty( $config['proxy_base_path'] ) ) {
$this->setProxyBasePath( $config['proxy_base_path'] );
}
unset( $config['proxy_base_path'] );
parent::__construct( $config );
$this->setApplicationName( Google_Proxy::get_application_name() );
}
/**
* Sets the base URL to the proxy.
*
* @since 1.2.0
*
* @param string $base_path Proxy base URL.
*/
public function setProxyBasePath( $base_path ) {
$this->proxy_base_path = untrailingslashit( $base_path );
}
/**
* Revokes an OAuth2 access token using the authentication proxy.
*
* @since 1.0.0
*
* @param string|array|null $token Optional. Access token. Default is the current one.
* @return bool True on success, false on failure.
*/
public function revokeToken( $token = null ) {
if ( ! $token ) {
$token = $this->getAccessToken();
}
if ( is_array( $token ) ) {
$token = $token['access_token'];
}
$body = Utils::streamFor(
http_build_query(
array(
'client_id' => $this->getClientId(),
'token' => $token,
)
)
);
$request = new Request(
'POST',
$this->proxy_base_path . Google_Proxy::OAUTH2_REVOKE_URI,
array(
'Cache-Control' => 'no-store',
'Content-Type' => 'application/x-www-form-urlencoded',
),
$body
);
$http_handler = HttpHandlerFactory::build( $this->getHttpClient() );
$response = $http_handler( $request );
return 200 === (int) $response->getStatusCode();
}
/**
* Creates a Google auth object for the authentication proxy.
*
* @since 1.0.0
*/
protected function createOAuth2Service() {
return new OAuth2(
array(
'clientId' => $this->getClientId(),
'clientSecret' => $this->getClientSecret(),
'authorizationUri' => $this->proxy_base_path . Google_Proxy::OAUTH2_AUTH_URI,
'tokenCredentialUri' => $this->proxy_base_path . Google_Proxy::OAUTH2_TOKEN_URI,
'redirectUri' => $this->getRedirectUri(),
'issuer' => $this->getClientId(),
'signingKey' => null,
'signingAlgorithm' => null,
)
);
}
/**
* Handles an erroneous response from a request to fetch an auth token.
*
* @since 1.2.0
*
* @param string $error Error code / error message.
* @param array $data Associative array of full response data.
*
* @throws Google_Proxy_Code_Exception Thrown when proxy returns an error accompanied by a temporary access code.
*/
protected function handleAuthTokenErrorResponse( $error, array $data ) {
if ( ! empty( $data['code'] ) ) {
throw new Google_Proxy_Code_Exception( $error, 0, $data['code'] );
}
parent::handleAuthTokenErrorResponse( $error, $data );
}
}

View File

@@ -0,0 +1,54 @@
<?php
/**
* Class Google\Site_Kit\Core\Authentication\Clients\OAuth2
*
* @package Google\Site_Kit
* @copyright 2022 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\Authentication\Clients;
use Google\Site_Kit_Dependencies\Google\Auth\OAuth2 as Google_Service_OAuth2;
use Google\Site_Kit_Dependencies\GuzzleHttp\Psr7\Utils;
use Google\Site_Kit_Dependencies\GuzzleHttp\Psr7\Query;
use Google\Site_Kit_Dependencies\Psr\Http\Message\RequestInterface;
/**
* Class for connecting to Google APIs via OAuth2.
*
* @since 1.87.0
* @access private
* @ignore
*/
class OAuth2 extends Google_Service_OAuth2 {
/**
* Overrides generateCredentialsRequest with additional parameters.
*
* @since 1.87.0
*
* @param array $extra_params Optional. Array of extra parameters to fetch with.
* @return RequestInterface Token credentials request.
*/
public function generateCredentialsRequest( $extra_params = array() ) {
$request = parent::generateCredentialsRequest();
$grant_type = $this->getGrantType();
if ( empty( $extra_params ) || 'refresh_token' !== $grant_type ) {
return $request;
}
$params = array(
'body' => Query::build(
array_merge(
Query::parse( Utils::copyToString( $request->getBody() ) ),
$extra_params
)
),
);
return Utils::modifyRequest( $request, $params );
}
}

View File

@@ -0,0 +1,668 @@
<?php
/**
* Class Google\Site_Kit\Core\Authentication\Clients\OAuth_Client
*
* @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\Core\Authentication\Clients;
use Exception;
use Google\Site_Kit\Context;
use Google\Site_Kit\Core\Authentication\Credentials;
use Google\Site_Kit\Core\Authentication\Exception\Google_Proxy_Code_Exception;
use Google\Site_Kit\Core\Authentication\Google_Proxy;
use Google\Site_Kit\Core\Authentication\Owner_ID;
use Google\Site_Kit\Core\Authentication\Profile;
use Google\Site_Kit\Core\Authentication\Token;
use Google\Site_Kit\Core\Permissions\Permissions;
use Google\Site_Kit\Core\Storage\Options;
use Google\Site_Kit\Core\Storage\Transients;
use Google\Site_Kit\Core\Storage\User_Options;
use Google\Site_Kit\Core\Util\Scopes;
use Google\Site_Kit\Core\Util\URL;
use Google\Site_Kit_Dependencies\Google\Service\PeopleService as Google_Service_PeopleService;
use WP_User;
/**
* Class for connecting to Google APIs via OAuth.
*
* @since 1.0.0
* @since 1.39.0 Now extends `OAuth_Client_Base`.
* @access private
* @ignore
*/
final class OAuth_Client extends OAuth_Client_Base {
const OPTION_ADDITIONAL_AUTH_SCOPES = 'googlesitekit_additional_auth_scopes';
const OPTION_REDIRECT_URL = 'googlesitekit_redirect_url';
const OPTION_ERROR_REDIRECT_URL = 'googlesitekit_error_redirect_url';
const CRON_REFRESH_PROFILE_DATA = 'googlesitekit_cron_refresh_profile_data';
/**
* Owner_ID instance.
*
* @since 1.16.0
* @var Owner_ID
*/
private $owner_id;
/**
* Transients instance.
*
* @since 1.150.0
* @var Transients
*/
private $transients;
/**
* Constructor.
*
* @since 1.0.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 Credentials $credentials Optional. Credentials instance. Default is a new instance from $options.
* @param Google_Proxy $google_proxy Optional. Google proxy instance. Default is a new instance.
* @param Profile $profile Optional. Profile instance. Default is a new instance.
* @param Token $token Optional. Token instance. Default is a new instance.
* @param Transients $transients Optional. Transients instance. Default is a new instance.
*/
public function __construct(
Context $context,
?Options $options = null,
?User_Options $user_options = null,
?Credentials $credentials = null,
?Google_Proxy $google_proxy = null,
?Profile $profile = null,
?Token $token = null,
?Transients $transients = null
) {
parent::__construct(
$context,
$options,
$user_options,
$credentials,
$google_proxy,
$profile,
$token
);
$this->owner_id = new Owner_ID( $this->options );
$this->transients = $transients ?: new Transients( $this->context );
}
/**
* Refreshes the access token.
*
* While this method can be used to explicitly refresh the current access token, the preferred way
* should be to rely on the Google_Site_Kit_Client to do that automatically whenever the current access token
* has expired.
*
* @since 1.0.0
*/
public function refresh_token() {
$token = $this->get_token();
if ( empty( $token['refresh_token'] ) ) {
$this->delete_token();
$this->user_options->set( self::OPTION_ERROR_CODE, 'refresh_token_not_exist' );
return;
}
try {
$token_response = $this->get_client()->fetchAccessTokenWithRefreshToken( $token['refresh_token'] );
} catch ( \Exception $e ) {
$this->handle_fetch_token_exception( $e );
return;
}
if ( ! isset( $token_response['access_token'] ) ) {
$this->user_options->set( self::OPTION_ERROR_CODE, 'access_token_not_received' );
return;
}
$this->set_token( $token_response );
}
/**
* Revokes the access token.
*
* @since 1.0.0
*/
public function revoke_token() {
try {
$this->get_client()->revokeToken();
} catch ( \Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement
// No special handling, we just need to make sure this goes through.
}
$this->delete_token();
}
/**
* Gets the list of currently granted Google OAuth scopes for the current user.
*
* @since 1.0.0
* @see https://developers.google.com/identity/protocols/googlescopes
*
* @return string[] List of Google OAuth scopes.
*/
public function get_granted_scopes() {
$base_scopes = parent::get_granted_scopes();
$extra_scopes = $this->get_granted_additional_scopes();
return array_unique(
array_merge( $base_scopes, $extra_scopes )
);
}
/**
* Gets the list of currently granted additional Google OAuth scopes for the current user.
*
* Scopes are considered "additional scopes" if they were granted to perform a specific action,
* rather than being granted as an overall required scope.
*
* @since 1.9.0
* @see https://developers.google.com/identity/protocols/googlescopes
*
* @return string[] List of Google OAuth scopes.
*/
public function get_granted_additional_scopes() {
return array_values( $this->user_options->get( self::OPTION_ADDITIONAL_AUTH_SCOPES ) ?: array() );
}
/**
* Checks if new scopes are required that are not yet granted for the current user.
*
* @since 1.9.0
*
* @return bool true if any required scopes are not satisfied, otherwise false.
*/
public function needs_reauthentication() {
if ( ! $this->token->has() ) {
return false;
}
return ! $this->has_sufficient_scopes();
}
/**
* Gets the list of scopes which are not satisfied by the currently granted scopes.
*
* @since 1.9.0
*
* @param string[] $scopes Optional. List of scopes to test against granted scopes.
* Default is the list of required scopes.
* @return string[] Filtered $scopes list, only including scopes that are not satisfied.
*/
public function get_unsatisfied_scopes( ?array $scopes = null ) {
if ( null === $scopes ) {
$scopes = $this->get_required_scopes();
}
$granted_scopes = $this->get_granted_scopes();
$unsatisfied_scopes = array_filter(
$scopes,
function ( $scope ) use ( $granted_scopes ) {
return ! Scopes::is_satisfied_by( $scope, $granted_scopes );
}
);
return array_values( $unsatisfied_scopes );
}
/**
* Checks whether or not currently granted scopes are sufficient for the given list.
*
* @since 1.9.0
*
* @param string[] $scopes Optional. List of scopes to test against granted scopes.
* Default is the list of required scopes.
* @return bool True if all $scopes are satisfied, false otherwise.
*/
public function has_sufficient_scopes( ?array $scopes = null ) {
if ( null === $scopes ) {
$scopes = $this->get_required_scopes();
}
return Scopes::are_satisfied_by( $scopes, $this->get_granted_scopes() );
}
/**
* Sets the list of currently granted Google OAuth scopes for the current user.
*
* @since 1.0.0
* @see https://developers.google.com/identity/protocols/googlescopes
*
* @param string[] $scopes List of Google OAuth scopes.
*/
public function set_granted_scopes( $scopes ) {
$required_scopes = $this->get_required_scopes();
$base_scopes = array();
$extra_scopes = array();
foreach ( $scopes as $scope ) {
if ( in_array( $scope, $required_scopes, true ) ) {
$base_scopes[] = $scope;
} else {
$extra_scopes[] = $scope;
}
}
parent::set_granted_scopes( $base_scopes );
$this->user_options->set( self::OPTION_ADDITIONAL_AUTH_SCOPES, $extra_scopes );
}
/**
* Gets the current user's OAuth access token.
*
* @since 1.0.0
*
* @return string|bool Access token if it exists, false otherwise.
*/
public function get_access_token() {
$token = $this->get_token();
if ( empty( $token['access_token'] ) ) {
return false;
}
return $token['access_token'];
}
/**
* Sets the current user's OAuth access token.
*
* @since 1.0.0
* @deprecated 1.39.0 Use `OAuth_Client::set_token` instead.
*
* @param string $access_token New access token.
* @param int $expires_in TTL of the access token in seconds.
* @param int $created Optional. Timestamp when the token was created, in GMT. Default is the current time.
* @return bool True on success, false on failure.
*/
public function set_access_token( $access_token, $expires_in, $created = 0 ) {
_deprecated_function( __METHOD__, '1.39.0', self::class . '::set_token' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
return $this->set_token(
array(
'access_token' => $access_token,
'expires_in' => $expires_in,
'created' => $created,
)
);
}
/**
* Gets the current user's OAuth refresh token.
*
* @since 1.0.0
* @deprecated 1.39.0 Use `OAuth_Client::get_token` instead.
*
* @return string|bool Refresh token if it exists, false otherwise.
*/
public function get_refresh_token() {
_deprecated_function( __METHOD__, '1.39.0', self::class . '::get_token' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
$token = $this->get_token();
if ( empty( $token['refresh_token'] ) ) {
return false;
}
return $token['refresh_token'];
}
/**
* Sets the current user's OAuth refresh token.
*
* @since 1.0.0
* @deprecated 1.39.0 Use `OAuth_Client::set_token` instead.
*
* @param string $refresh_token New refresh token.
* @return bool True on success, false on failure.
*/
public function set_refresh_token( $refresh_token ) {
_deprecated_function( __METHOD__, '1.39.0', self::class . '::set_token' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
$token = $this->get_token();
$token['refresh_token'] = $refresh_token;
return $this->set_token( $token );
}
/**
* Gets the authentication URL.
*
* @since 1.0.0
* @since 1.9.0 Added $additional_scopes parameter.
* @since 1.34.1 Updated handling of $additional_scopes to restore rewritten scope.
*
* @param string $redirect_url Redirect URL after authentication.
* @param string $error_redirect_url Redirect URL after authentication error.
* @param string[] $additional_scopes List of additional scopes to request.
* @return string Authentication URL.
*/
public function get_authentication_url( $redirect_url = '', $error_redirect_url = '', $additional_scopes = array() ) {
if ( empty( $redirect_url ) ) {
$redirect_url = $this->context->admin_url( 'splash' );
}
if ( is_array( $additional_scopes ) ) {
// Rewrite each scope to convert `gttp` -> `http`, if it starts with this placeholder scheme.
// This restores the original scope rewritten by getConnectURL.
$additional_scopes = array_map(
function ( $scope ) {
return preg_replace( '/^gttp(s)?:/', 'http$1:', $scope );
},
$additional_scopes
);
} else {
$additional_scopes = array();
}
$url_query = URL::parse( $redirect_url, PHP_URL_QUERY );
if ( $url_query ) {
parse_str( $url_query, $query_args );
}
if ( empty( $query_args['notification'] ) ) {
$redirect_url = add_query_arg( array( 'notification' => 'authentication_success' ), $redirect_url );
}
// Ensure we remove error query string.
$redirect_url = remove_query_arg( 'error', $redirect_url );
$this->user_options->set( self::OPTION_REDIRECT_URL, $redirect_url );
$this->user_options->set( self::OPTION_ERROR_REDIRECT_URL, $error_redirect_url );
// Ensure the latest required scopes are requested.
$scopes = array_merge( $this->get_required_scopes(), $additional_scopes );
$this->get_client()->setScopes( array_unique( $scopes ) );
return add_query_arg(
$this->google_proxy->get_metadata_fields(),
$this->get_client()->createAuthUrl()
);
}
/**
* Redirects the current user to the Google OAuth consent screen, or processes a response from that consent
* screen if present.
*
* @since 1.0.0
* @since 1.49.0 Uses the new `Google_Proxy::setup_url_v2` method when the `serviceSetupV2` feature flag is enabled.
*/
public function authorize_user() {
$code = htmlspecialchars( $this->context->input()->filter( INPUT_GET, 'code' ) ?? '' );
$error_code = htmlspecialchars( $this->context->input()->filter( INPUT_GET, 'error' ) ?? '' );
// If we have a code, check if there's a stored redirect URL to prevent duplicate setups.
// The OAuth2 spec requires that an authorization code can only be used once.
// If `fetchAccessTokenWithAuthCode()` is called more than once with the same code, Google will return an error.
// This may happen when users click the final setup button multiple times or
// if there are concurrent requests with the same authorization code.
// By storing the successful redirect URL in transients and reusing it for duplicate
// requests with the same code, we ensure a smooth setup experience even when
// the same code is encountered multiple times.
if ( ! empty( $code ) ) {
$code_hash = md5( $code );
$stored_redirect = $this->transients->get( $code_hash );
// If we have a stored redirect URL and valid credentials, redirect to prevent duplicate setup.
if ( ! empty( $stored_redirect ) && $this->credentials->has() ) {
wp_safe_redirect( $stored_redirect );
exit();
}
}
// If the OAuth redirects with an error code, handle it.
if ( ! empty( $error_code ) ) {
$this->user_options->set( self::OPTION_ERROR_CODE, $error_code );
wp_safe_redirect( $this->authorize_user_redirect_url() );
exit();
}
if ( ! $this->credentials->has() ) {
$this->user_options->set( self::OPTION_ERROR_CODE, 'oauth_credentials_not_exist' );
wp_safe_redirect( $this->authorize_user_redirect_url() );
exit();
}
try {
$token_response = $this->get_client()->fetchAccessTokenWithAuthCode( $code );
} catch ( Google_Proxy_Code_Exception $e ) {
// Redirect back to proxy immediately with the access code.
$credentials = $this->credentials->get();
$params = array(
'code' => $e->getAccessCode(),
'site_id' => ! empty( $credentials['oauth2_client_id'] ) ? $credentials['oauth2_client_id'] : '',
);
$params = $this->google_proxy->add_setup_step_from_error_code( $params, $e->getMessage() );
$url = $this->google_proxy->setup_url( $params );
wp_safe_redirect( $url );
exit();
} catch ( Exception $e ) {
$this->handle_fetch_token_exception( $e );
wp_safe_redirect( $this->authorize_user_redirect_url() );
exit();
}
if ( ! isset( $token_response['access_token'] ) ) {
$this->user_options->set( self::OPTION_ERROR_CODE, 'access_token_not_received' );
wp_safe_redirect( $this->authorize_user_redirect_url() );
exit();
}
// Update the access token and refresh token.
$this->set_token( $token_response );
// Store the previously granted scopes for use in the action below before they're updated.
$previous_scopes = $this->get_granted_scopes();
// Update granted scopes.
if ( isset( $token_response['scope'] ) ) {
$scopes = explode( ' ', sanitize_text_field( $token_response['scope'] ) );
} elseif ( $this->context->input()->filter( INPUT_GET, 'scope' ) ) {
$scope = htmlspecialchars( $this->context->input()->filter( INPUT_GET, 'scope' ) );
$scopes = explode( ' ', $scope );
} else {
$scopes = $this->get_required_scopes();
}
$scopes = array_filter(
$scopes,
function ( $scope ) {
if ( ! is_string( $scope ) ) {
return false;
}
if ( in_array( $scope, array( 'openid', 'profile', 'email' ), true ) ) {
return true;
}
return 0 === strpos( $scope, 'https://www.googleapis.com/auth/' );
}
);
$this->set_granted_scopes( $scopes );
$this->refresh_profile_data( 2 * MINUTE_IN_SECONDS );
/**
* Fires when the current user has just been authorized to access Google APIs.
*
* In other words, this action fires whenever Site Kit has just obtained a new set of access token and
* refresh token for the current user, which may happen to set up the initial connection or to request
* access to further scopes.
*
* @since 1.3.0
* @since 1.6.0 The $token_response parameter was added.
* @since 1.30.0 The $scopes and $previous_scopes parameters were added.
*
* @param array $token_response Token response data.
* @param string[] $scopes List of scopes.
* @param string[] $previous_scopes List of previous scopes.
*/
do_action( 'googlesitekit_authorize_user', $token_response, $scopes, $previous_scopes );
// This must happen after googlesitekit_authorize_user as the permissions checks depend on
// values set which affect the meta capability mapping.
$current_user_id = get_current_user_id();
if ( $this->should_update_owner_id( $current_user_id ) ) {
$this->owner_id->set( $current_user_id );
}
$redirect_url = $this->user_options->get( self::OPTION_REDIRECT_URL );
if ( $redirect_url ) {
$url_query = URL::parse( $redirect_url, PHP_URL_QUERY );
if ( $url_query ) {
parse_str( $url_query, $query_args );
}
$reauth = isset( $query_args['reAuth'] ) && 'true' === $query_args['reAuth'];
if ( false === $reauth && empty( $query_args['notification'] ) ) {
$redirect_url = add_query_arg( array( 'notification' => 'authentication_success' ), $redirect_url );
}
$this->user_options->delete( self::OPTION_REDIRECT_URL );
$this->user_options->delete( self::OPTION_ERROR_REDIRECT_URL );
} else {
// No redirect_url is set, use default page.
$redirect_url = $this->context->admin_url( 'splash', array( 'notification' => 'authentication_success' ) );
}
// Store the redirect URL in transients using the authorization code hash as the key.
// This prevents duplicate setup attempts if the user clicks the setup CTA button multiple times,
// as subsequent requests with the same code will be redirected to the stored URL.
// Must be done before the redirect to ensure the URL is available for any duplicate requests.
if ( ! empty( $code ) && ! empty( $redirect_url ) ) {
$code_hash = md5( $code );
$this->transients->set( $code_hash, $redirect_url, 5 * MINUTE_IN_SECONDS );
}
wp_safe_redirect( $redirect_url );
exit();
}
/**
* Fetches and updates the user profile data for the currently authenticated Google account.
*
* @since 1.1.4
* @since 1.13.0 Added $retry_after param, also made public.
*
* @param int $retry_after Optional. Number of seconds to retry data fetch if unsuccessful.
*/
public function refresh_profile_data( $retry_after = 0 ) {
$client = $this->get_client();
$restore_defer = $client->withDefer( false );
try {
$people_service = new Google_Service_PeopleService( $client );
$response = $people_service->people->get( 'people/me', array( 'personFields' => 'emailAddresses,photos,names' ) );
if ( isset( $response['emailAddresses'][0]['value'], $response['photos'][0]['url'], $response['names'][0]['displayName'] ) ) {
$this->profile->set(
array(
'email' => $response['emailAddresses'][0]['value'],
'photo' => $response['photos'][0]['url'],
'full_name' => $response['names'][0]['displayName'],
'last_updated' => time(),
)
);
}
// Clear any scheduled job to refresh this data later, if any.
wp_clear_scheduled_hook(
self::CRON_REFRESH_PROFILE_DATA,
array( $this->user_options->get_user_id() )
);
} catch ( Exception $e ) {
$retry_after = absint( $retry_after );
if ( $retry_after < 1 ) {
return;
}
wp_schedule_single_event(
time() + $retry_after,
self::CRON_REFRESH_PROFILE_DATA,
array( $this->user_options->get_user_id() )
);
} finally {
$restore_defer();
}
}
/**
* Determines whether the current owner ID must be changed or not.
*
* @since 1.16.0
*
* @param int $user_id Current user ID.
* @return bool TRUE if owner needs to be changed, otherwise FALSE.
*/
private function should_update_owner_id( $user_id ) {
$current_owner_id = $this->owner_id->get();
if ( $current_owner_id === $user_id ) {
return false;
}
if ( ! empty( $current_owner_id ) && user_can( $current_owner_id, Permissions::MANAGE_OPTIONS ) ) {
return false;
}
if ( ! user_can( $user_id, Permissions::MANAGE_OPTIONS ) ) {
return false;
}
return true;
}
/**
* Returns the permissions URL to the authentication proxy.
*
* This only returns a URL if the user already has an access token set.
*
* @since 1.0.0
*
* @return string URL to the permissions page on the authentication proxy on success,
* or empty string on failure.
*/
public function get_proxy_permissions_url() {
$access_token = $this->get_access_token();
if ( empty( $access_token ) ) {
return '';
}
return $this->google_proxy->permissions_url(
$this->credentials,
array( 'token' => $access_token )
);
}
/**
* Deletes the current user's token and all associated data.
*
* @since 1.0.3
*/
protected function delete_token() {
parent::delete_token();
$this->user_options->delete( self::OPTION_REDIRECT_URL );
$this->user_options->delete( self::OPTION_ERROR_REDIRECT_URL );
$this->user_options->delete( self::OPTION_ADDITIONAL_AUTH_SCOPES );
}
/**
* Return the URL for the user to view the dashboard/splash
* page based on their permissions.
*
* @since 1.77.0
*/
private function authorize_user_redirect_url() {
$error_redirect_url = $this->user_options->get( self::OPTION_ERROR_REDIRECT_URL );
if ( $error_redirect_url ) {
$this->user_options->delete( self::OPTION_ERROR_REDIRECT_URL );
return $error_redirect_url;
}
return current_user_can( Permissions::VIEW_DASHBOARD )
? $this->context->admin_url( 'dashboard' )
: $this->context->admin_url( 'splash' );
}
}

View File

@@ -0,0 +1,352 @@
<?php
/**
* Class Google\Site_Kit\Core\Authentication\Clients\OAuth_Client_Base
*
* @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\Core\Authentication\Clients;
use Exception;
use Google\Site_Kit\Context;
use Google\Site_Kit\Core\Authentication\Credentials;
use Google\Site_Kit\Core\Authentication\Exception\Google_Proxy_Code_Exception;
use Google\Site_Kit\Core\Authentication\Google_Proxy;
use Google\Site_Kit\Core\Authentication\Profile;
use Google\Site_Kit\Core\Authentication\Token;
use Google\Site_Kit\Core\Permissions\Permissions;
use Google\Site_Kit\Core\Storage\Encrypted_Options;
use Google\Site_Kit\Core\Storage\Options;
use Google\Site_Kit\Core\Storage\User_Options;
/**
* Base class for connecting to Google APIs via OAuth.
*
* @since 1.39.0
* @access private
* @ignore
*/
abstract class OAuth_Client_Base {
const OPTION_ACCESS_TOKEN = 'googlesitekit_access_token';
const OPTION_ACCESS_TOKEN_EXPIRES_IN = 'googlesitekit_access_token_expires_in';
const OPTION_ACCESS_TOKEN_CREATED = 'googlesitekit_access_token_created_at';
const OPTION_REFRESH_TOKEN = 'googlesitekit_refresh_token';
const OPTION_AUTH_SCOPES = 'googlesitekit_auth_scopes';
const OPTION_ERROR_CODE = 'googlesitekit_error_code';
const OPTION_PROXY_ACCESS_CODE = 'googlesitekit_proxy_access_code';
/**
* Plugin context.
*
* @since 1.39.0
* @var Context
*/
protected $context;
/**
* Options instance
*
* @since 1.39.0
* @var Options
*/
protected $options;
/**
* User_Options instance
*
* @since 1.39.0
* @var User_Options
*/
protected $user_options;
/**
* OAuth credentials instance.
*
* @since 1.39.0
* @var Credentials
*/
protected $credentials;
/**
* Google_Proxy instance.
*
* @since 1.39.0
* @var Google_Proxy
*/
protected $google_proxy;
/**
* Google Client object.
*
* @since 1.39.0
* @var Google_Site_Kit_Client
*/
protected $google_client;
/**
* Profile instance.
*
* @since 1.39.0
* @var Profile
*/
protected $profile;
/**
* Token instance.
*
* @since 1.39.0
* @var Token
*/
protected $token;
/**
* Constructor.
*
* @since 1.39.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 Credentials $credentials Optional. Credentials instance. Default is a new instance from $options.
* @param Google_Proxy $google_proxy Optional. Google proxy instance. Default is a new instance.
* @param Profile $profile Optional. Profile instance. Default is a new instance.
* @param Token $token Optional. Token instance. Default is a new instance.
*/
public function __construct(
Context $context,
?Options $options = null,
?User_Options $user_options = null,
?Credentials $credentials = null,
?Google_Proxy $google_proxy = null,
?Profile $profile = null,
?Token $token = null
) {
$this->context = $context;
$this->options = $options ?: new Options( $this->context );
$this->user_options = $user_options ?: new User_Options( $this->context );
$this->credentials = $credentials ?: new Credentials( new Encrypted_Options( $this->options ) );
$this->google_proxy = $google_proxy ?: new Google_Proxy( $this->context );
$this->profile = $profile ?: new Profile( $this->user_options );
$this->token = $token ?: new Token( $this->user_options );
}
/**
* Gets the Google client object.
*
* @since 1.39.0
* @since 1.2.0 Now always returns a Google_Site_Kit_Client.
*
* @return Google_Site_Kit_Client Google client object.
*/
public function get_client() {
if ( ! $this->google_client instanceof Google_Site_Kit_Client ) {
$credentials = $this->credentials->get();
$this->google_client = Client_Factory::create_client(
array(
'client_id' => $credentials['oauth2_client_id'],
'client_secret' => $credentials['oauth2_client_secret'],
'redirect_uri' => $this->get_redirect_uri(),
'token' => $this->get_token(),
'token_callback' => array( $this, 'set_token' ),
'token_exception_callback' => function ( Exception $e ) {
$this->handle_fetch_token_exception( $e );
},
'required_scopes' => $this->get_required_scopes(),
'login_hint_email' => $this->profile->has() ? $this->profile->get()['email'] : '',
'using_proxy' => $this->credentials->using_proxy(),
'proxy_url' => $this->google_proxy->url(),
)
);
}
return $this->google_client;
}
/**
* Gets the list of currently required Google OAuth scopes.
*
* @since 1.39.0
* @see https://developers.google.com/identity/protocols/googlescopes
*
* @return array List of Google OAuth scopes.
*/
public function get_required_scopes() {
/**
* Filters the list of required Google OAuth scopes.
*
* See all Google oauth scopes here: https://developers.google.com/identity/protocols/googlescopes
*
* @since 1.39.0
*
* @param array $scopes List of scopes.
*/
$scopes = (array) apply_filters( 'googlesitekit_auth_scopes', array() );
return array_unique(
array_merge(
// Default scopes that are always required.
array(
'openid',
'https://www.googleapis.com/auth/userinfo.profile',
'https://www.googleapis.com/auth/userinfo.email',
),
$scopes
)
);
}
/**
* Gets the list of currently granted Google OAuth scopes for the current user.
*
* @since 1.39.0
* @see https://developers.google.com/identity/protocols/googlescopes
*
* @return string[] List of Google OAuth scopes.
*/
public function get_granted_scopes() {
return $this->user_options->get( self::OPTION_AUTH_SCOPES ) ?: array();
}
/**
* Sets the list of currently granted Google OAuth scopes for the current user.
*
* @since 1.39.0
* @see https://developers.google.com/identity/protocols/googlescopes
*
* @param string[] $scopes List of Google OAuth scopes.
*/
public function set_granted_scopes( $scopes ) {
$required_scopes = $this->get_required_scopes();
$scopes = array_values( array_unique( array_intersect( $scopes, $required_scopes ) ) );
$this->user_options->set( self::OPTION_AUTH_SCOPES, $scopes );
}
/**
* Gets the current user's full OAuth token data, including access token and optional refresh token.
*
* @since 1.39.0
*
* @return array Associative array with 'access_token', 'expires_in', 'created', and 'refresh_token' keys, or empty
* array if no token available.
*/
public function get_token() {
return $this->token->get();
}
/**
* Sets the current user's full OAuth token data, including access token and optional refresh token.
*
* @since 1.39.0
*
* @param array $token {
* Full token data, optionally including the refresh token.
*
* @type string $access_token Required. The access token.
* @type int $expires_in Number of seconds in which the token expires. Default 3600 (1 hour).
* @type int $created Timestamp in seconds when the token was created. Default is the current time.
* @type string $refresh_token The refresh token, if relevant. If passed, it is set as well.
* }
* @return bool True on success, false on failure.
*/
public function set_token( array $token ) {
// Remove the error code from the user options so it doesn't
// appear again.
$this->user_options->delete( OAuth_Client::OPTION_ERROR_CODE );
return $this->token->set( $token );
}
/**
* Deletes the current user's token and all associated data.
*
* @since 1.0.3
*/
protected function delete_token() {
$this->token->delete();
$this->user_options->delete( self::OPTION_AUTH_SCOPES );
}
/**
* Converts the given error code to a user-facing message.
*
* @since 1.39.0
*
* @param string $error_code Error code.
* @return string Error message.
*/
public function get_error_message( $error_code ) {
switch ( $error_code ) {
case 'access_denied':
return __( 'Setup was interrupted because you did not grant the necessary permissions.', 'google-site-kit' );
case 'access_token_not_received':
return __( 'Unable to receive access token because of an unknown error.', 'google-site-kit' );
case 'cannot_log_in':
return __( 'Internal error that the Google login redirect failed.', 'google-site-kit' );
case 'invalid_client':
return __( 'Unable to receive access token because of an invalid client.', 'google-site-kit' );
case 'invalid_code':
return __( 'Unable to receive access token because of an empty authorization code.', 'google-site-kit' );
case 'invalid_grant':
return __( 'Unable to receive access token because of an invalid authorization code or refresh token.', 'google-site-kit' );
case 'invalid_request':
return __( 'Unable to receive access token because of an invalid OAuth request.', 'google-site-kit' );
case 'missing_delegation_consent':
return __( 'Looks like your site is not allowed access to Google account data and cant display stats in the dashboard.', 'google-site-kit' );
case 'missing_search_console_property':
return __( 'Looks like there is no Search Console property for your site.', 'google-site-kit' );
case 'missing_verification':
return __( 'Looks like the verification token for your site is missing.', 'google-site-kit' );
case 'oauth_credentials_not_exist':
return __( 'Unable to authenticate Site Kit, as no client credentials exist.', 'google-site-kit' );
case 'refresh_token_not_exist':
return __( 'Unable to refresh access token, as no refresh token exists.', 'google-site-kit' );
case 'unauthorized_client':
return __( 'Unable to receive access token because of an unauthorized client.', 'google-site-kit' );
case 'unsupported_grant_type':
return __( 'Unable to receive access token because of an unsupported grant type.', 'google-site-kit' );
default:
/* translators: %s: error code from API */
return sprintf( __( 'Unknown Error (code: %s).', 'google-site-kit' ), $error_code );
}
}
/**
* Handles an exception thrown when fetching an access token.
*
* @since 1.2.0
*
* @param Exception $e Exception thrown.
*/
protected function handle_fetch_token_exception( Exception $e ) {
$error_code = $e->getMessage();
// Revoke and delete user connection data on 'invalid_grant'.
// This typically happens during refresh if the refresh token is invalid or expired.
if ( 'invalid_grant' === $error_code ) {
$this->delete_token();
}
$this->user_options->set( self::OPTION_ERROR_CODE, $error_code );
if ( $e instanceof Google_Proxy_Code_Exception ) {
$this->user_options->set( self::OPTION_PROXY_ACCESS_CODE, $e->getAccessCode() );
}
}
/**
* Gets the OAuth redirect URI that listens to the callback request.
*
* @since 1.39.0
*
* @return string OAuth redirect URI.
*/
protected function get_redirect_uri() {
return add_query_arg( 'oauth2callback', '1', admin_url( 'index.php' ) );
}
}

View File

@@ -0,0 +1,53 @@
<?php
/**
* Class Google\Site_Kit\Core\Authentication\Connected_Proxy_URL
*
* @package Google\Site_Kit\Core\Authentication
* @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\Core\Authentication;
use Google\Site_Kit\Core\Storage\Setting;
/**
* Connected_Proxy_URL class.
*
* @since 1.17.0
* @access private
* @ignore
*/
class Connected_Proxy_URL extends Setting {
/**
* The option_name for this setting.
*/
const OPTION = 'googlesitekit_connected_proxy_url';
/**
* Matches provided URL with the current proxy URL in the settings.
*
* @since 1.17.0
*
* @param string $url URL to match against the current one in the settings.
* @return bool TRUE if URL matches the current one, otherwise FALSE.
*/
public function matches_url( $url ) {
$sanitize = $this->get_sanitize_callback();
$normalized = $sanitize( $url );
return $normalized === $this->get();
}
/**
* Gets the callback for sanitizing the setting's value before saving.
*
* @since 1.17.0
*
* @return callable A sanitizing function.
*/
protected function get_sanitize_callback() {
return 'trailingslashit';
}
}

View File

@@ -0,0 +1,137 @@
<?php
/**
* Class Google\Site_Kit\Core\Authentication\Credentials
*
* @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\Core\Authentication;
use Google\Site_Kit\Core\Storage\Setting;
/**
* Class representing the OAuth client ID and secret credentials.
*
* @since 1.0.0
* @access private
* @ignore
*/
final class Credentials extends Setting {
/**
* Option key in options table.
*/
const OPTION = 'googlesitekit_credentials';
/**
* Retrieves Site Kit credentials.
*
* @since 1.0.0
*
* @return array|bool Value set for the credentials, or false if not set.
*/
public function get() {
/**
* Site Kit oAuth Secret is a JSON string of the Google Cloud Platform web application used for Site Kit
* that will be associated with this account. This is meant to be a temporary way to specify the client secret
* until the authentication proxy has been completed. This filter can be specified from a separate theme or plugin.
*
* To retrieve the JSON secret, use the following instructions:
* - Go to the Google Cloud Platform and create a new project or use an existing one
* - In the APIs & Services section, enable the APIs that are used within Site Kit
* - Under 'credentials' either create new oAuth Client ID credentials or use an existing set of credentials
* - Set the authorizes redirect URIs to be the URL to the oAuth callback for Site Kit, eg. https://<domainname>?oauth2callback=1 (this must be public)
* - Click the 'Download JSON' button to download the JSON file that can be copied and pasted into the filter
*/
$credentials = apply_filters( 'googlesitekit_oauth_secret', '' );
if ( is_string( $credentials ) && trim( $credentials ) ) {
$credentials = json_decode( $credentials, true );
}
if ( isset( $credentials['web']['client_id'], $credentials['web']['client_secret'] ) ) {
return $this->parse_defaults(
array(
'oauth2_client_id' => $credentials['web']['client_id'],
'oauth2_client_secret' => $credentials['web']['client_secret'],
)
);
}
return $this->parse_defaults(
$this->options->get( self::OPTION )
);
}
/**
* Checks whether Site Kit has been setup with client ID and secret.
*
* @since 1.0.0
*
* @return bool True if credentials are set, false otherwise.
*/
public function has() {
$credentials = (array) $this->get();
if ( ! empty( $credentials ) && ! empty( $credentials['oauth2_client_id'] ) && ! empty( $credentials['oauth2_client_secret'] ) ) {
return true;
}
return false;
}
/**
* Parses Credentials data and merges with its defaults.
*
* @since 1.0.0
*
* @param mixed $data Credentials data.
* @return array Parsed $data.
*/
private function parse_defaults( $data ) {
$defaults = $this->get_default();
if ( ! is_array( $data ) ) {
return $defaults;
}
return wp_parse_args( $data, $defaults );
}
/**
* Gets the default value.
*
* @since 1.2.0
*
* @return array
*/
protected function get_default() {
return array(
'oauth2_client_id' => '',
'oauth2_client_secret' => '',
);
}
/**
* Determines whether the authentication proxy is used.
*
* In order to streamline the setup and authentication flow, the plugin uses a proxy mechanism based on an external
* service. This can be overridden by providing actual GCP credentials with the {@see 'googlesitekit_oauth_secret'}
* filter.
*
* @since 1.9.0
*
* @return bool True if proxy authentication is used, false otherwise.
*/
public function using_proxy() {
$creds = $this->get();
if ( ! $this->has() ) {
return true;
}
return (bool) preg_match( '/\.apps\.sitekit\.withgoogle\.com$/', $creds['oauth2_client_id'] );
}
}

View File

@@ -0,0 +1,43 @@
<?php
/**
* Class Google\Site_Kit\Core\Authentication\Disconnected_Reason
*
* @package Google\Site_Kit\Core\Authentication
* @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\Core\Authentication;
use Google\Site_Kit\Core\Storage\User_Setting;
/**
* Disconnected_Reason class.
*
* @since 1.17.0
* @access private
* @ignore
*/
class Disconnected_Reason extends User_Setting {
/**
* The option_name for this setting.
*/
const OPTION = 'googlesitekit_disconnected_reason';
/**
* Available reasons.
*/
const REASON_CONNECTED_URL_MISMATCH = 'connected_url_mismatch';
/**
* Registers the setting in WordPress.
*
* @since 1.17.0
*/
public function register() {
parent::register();
add_action( 'googlesitekit_authorize_user', array( $this, 'delete' ) );
}
}

View File

@@ -0,0 +1,24 @@
<?php
/**
* Class Google\Site_Kit\Core\Authentication\Exception\Exchange_Site_Code_Exception
*
* @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\Core\Authentication\Exception;
use Exception;
/**
* Exception thrown when exchanging the site code fails.
*
* @since 1.48.0
* @access private
* @ignore
*/
class Exchange_Site_Code_Exception extends Exception {
}

View File

@@ -0,0 +1,24 @@
<?php
/**
* Class Google\Site_Kit\Core\Authentication\Exception\Google_OAuth_Exception
*
* @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\Core\Authentication\Exception;
use Exception;
/**
* Exception thrown when a Google OAuth response contains an OAuth error.
*
* @since 1.2.0
* @access private
* @ignore
*/
class Google_OAuth_Exception extends Exception {
}

View File

@@ -0,0 +1,59 @@
<?php
/**
* Class Google\Site_Kit\Core\Authentication\Exception\Google_Proxy_Code_Exception
*
* @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\Core\Authentication\Exception;
use Exception;
/**
* Exception thrown when Google proxy returns an error accompanied with a temporary access code.
*
* @since 1.0.0
* @since 1.2.0 Renamed to Google_Proxy_Code_Exception.
* @access private
* @ignore
*/
class Google_Proxy_Code_Exception extends Exception {
/**
* Temporary code for an undelegated proxy token.
*
* @since 1.109.0 Explicitly declared; previously, it was dynamically declared.
*
* @var string
*/
protected $access_code;
/**
* Constructor.
*
* @since 1.0.0
*
* @param string $message Optional. The exception message. Default empty string.
* @param integer $code Optional. The numeric exception code. Default 0.
* @param string $access_code Optional. Temporary code for an undelegated proxy token. Default empty string.
*/
public function __construct( $message = '', $code = 0, $access_code = '' ) {
parent::__construct( $message, $code );
$this->access_code = $access_code;
}
/**
* Gets the temporary access code for an undelegated proxy token.
*
* @since 1.0.0
*
* @return string Temporary code.
*/
public function getAccessCode() {
return $this->access_code;
}
}

View File

@@ -0,0 +1,91 @@
<?php
/**
* Class Google\Site_Kit\Core\Authentication\Exception\Insufficient_Scopes_Exception
*
* @package Google\Site_Kit\Core\Authentication\Exception
* @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\Core\Authentication\Exception;
use Exception;
use Google\Site_Kit\Core\Contracts\WP_Errorable;
use WP_Error;
/**
* Exception thrown when authentication scopes are insufficient for a request.
*
* @since 1.9.0
* @access private
* @ignore
*/
class Insufficient_Scopes_Exception extends Exception implements WP_Errorable {
const WP_ERROR_CODE = 'missing_required_scopes';
/**
* OAuth scopes that are required but not yet granted.
*
* @since 1.9.0
*
* @var array
*/
protected $scopes = array();
/**
* Constructor.
*
* @since 1.9.0
*
* @param string $message Optional. Exception message.
* @param int $code Optional. Exception code.
* @param Throwable $previous Optional. Previous exception used for chaining.
* @param array $scopes Optional. Scopes that are missing.
*/
public function __construct( $message = '', $code = 0, $previous = null, $scopes = array() ) {
parent::__construct( $message, $code, $previous );
$this->set_scopes( $scopes );
}
/**
* Sets the missing scopes that raised this exception.
*
* @since 1.9.0
*
* @param array $scopes OAuth scopes that are required but not yet granted.
*/
public function set_scopes( array $scopes ) {
$this->scopes = $scopes;
}
/**
* Gets the missing scopes that raised this exception.
*
* @since 1.9.0
*
* @return array
*/
public function get_scopes() {
return $this->scopes;
}
/**
* Gets the WP_Error representation of this exception.
*
* @since 1.9.0
*
* @return WP_Error
*/
public function to_wp_error() {
return new WP_Error(
static::WP_ERROR_CODE,
$this->getMessage(),
array(
'status' => 403, // Forbidden.
'scopes' => $this->scopes,
)
);
}
}

View File

@@ -0,0 +1,22 @@
<?php
/**
* Class Google\Site_Kit\Core\Authentication\Exception\Missing_Verification_Exception
*
* @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\Core\Authentication\Exception;
/**
* Exception thrown when the a missing verification error is encountered when exchanging the site code.
*
* @since 1.48.0
* @access private
* @ignore
*/
class Missing_Verification_Exception extends Exchange_Site_Code_Exception {
}

View File

@@ -0,0 +1,636 @@
<?php
/**
* Class Google\Site_Kit\Core\Authentication\Google_Proxy
*
* @package Google\Site_Kit\Core\Authentication
* @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\Core\Authentication;
use Google\Site_Kit\Context;
use Google\Site_Kit\Core\Util\Feature_Flags;
use Exception;
use Google\Site_Kit\Core\Authentication\Clients\OAuth_Client;
use Google\Site_Kit\Core\Storage\User_Options;
use Google\Site_Kit\Core\Util\URL;
use WP_Error;
/**
* Class for authentication service.
*
* @since 1.1.2
* @access private
* @ignore
*/
class Google_Proxy {
const PRODUCTION_BASE_URL = 'https://sitekit.withgoogle.com';
const STAGING_BASE_URL = 'https://site-kit-dev.appspot.com';
const DEVELOPMENT_BASE_URL = 'https://site-kit-local.appspot.com';
const OAUTH2_SITE_URI = '/o/oauth2/site/';
const OAUTH2_REVOKE_URI = '/o/oauth2/revoke/';
const OAUTH2_TOKEN_URI = '/o/oauth2/token/';
const OAUTH2_AUTH_URI = '/o/oauth2/auth/';
const OAUTH2_DELETE_SITE_URI = '/o/oauth2/delete-site/';
const SETUP_URI = '/v2/site-management/setup/';
const SETUP_V3_URI = '/v3/site-management/setup/';
const PERMISSIONS_URI = '/site-management/permissions/';
const FEATURES_URI = '/site-management/features/';
const SURVEY_TRIGGER_URI = '/survey/trigger/';
const SURVEY_EVENT_URI = '/survey/event/';
const SUPPORT_LINK_URI = '/support';
const ACTION_EXCHANGE_SITE_CODE = 'googlesitekit_proxy_exchange_site_code';
const ACTION_SETUP = 'googlesitekit_proxy_setup';
const ACTION_SETUP_START = 'googlesitekit_proxy_setup_start';
const ACTION_PERMISSIONS = 'googlesitekit_proxy_permissions';
const ACTION_VERIFY = 'googlesitekit_proxy_verify';
const NONCE_ACTION = 'googlesitekit_proxy_nonce';
const HEADER_REDIRECT_TO = 'Redirect-To';
/**
* Plugin context.
*
* @since 1.1.2
* @var Context
*/
private $context;
/**
* Required scopes list.
*
* @since 1.68.0
* @var array
*/
private $required_scopes = array();
/**
* Google_Proxy constructor.
*
* @since 1.1.2
*
* @param Context $context Plugin context.
*/
public function __construct( Context $context ) {
$this->context = $context;
}
/**
* Sets required scopes to use when the site is registering at proxy.
*
* @since 1.68.0
*
* @param array $scopes List of scopes.
*/
public function with_scopes( array $scopes ) {
$this->required_scopes = $scopes;
}
/**
* Returns the application name: a combination of the namespace and version.
*
* @since 1.27.0
*
* @return string The application name.
*/
public static function get_application_name() {
$platform = self::get_platform();
return $platform . '/google-site-kit/' . GOOGLESITEKIT_VERSION;
}
/**
* Gets the list of features to declare support for when setting up with the proxy.
*
* @since 1.27.0
*
* @return array Array of supported features.
*/
private function get_supports() {
$supports = array(
'credentials_retrieval',
'short_verification_token',
);
$home_path = URL::parse( $this->context->get_canonical_home_url(), PHP_URL_PATH );
if ( ! $home_path || '/' === $home_path ) {
$supports[] = 'file_verification';
}
return $supports;
}
/**
* Returns the setup URL to the authentication proxy.
*
* @since 1.49.0
* @since 1.71.0 Uses the V2 setup flow by default.
*
* @param array $query_params Query parameters to include in the URL.
* @return string URL to the setup page on the authentication proxy.
*
* @throws Exception Thrown if called without the required query parameters.
*/
public function setup_url( array $query_params = array() ) {
if ( empty( $query_params['code'] ) ) {
throw new Exception( __( 'Missing code parameter for setup URL.', 'google-site-kit' ) );
}
if ( empty( $query_params['site_id'] ) && empty( $query_params['site_code'] ) ) {
throw new Exception( __( 'Missing site_id or site_code parameter for setup URL.', 'google-site-kit' ) );
}
return add_query_arg(
$query_params,
$this->url(
Feature_Flags::enabled( 'setupFlowRefresh' ) ? self::SETUP_V3_URI : self::SETUP_URI
)
);
}
/**
* Conditionally adds the `step` parameter to the passed query parameters, depending on the given error code.
*
* @since 1.49.0
*
* @param array $query_params Query parameters.
* @param string $error_code Error code.
* @return array Query parameters with `step` included, depending on the error code.
*/
public function add_setup_step_from_error_code( $query_params, $error_code ) {
switch ( $error_code ) {
case 'missing_verification':
$query_params['step'] = 'verification';
break;
case 'missing_delegation_consent':
$query_params['step'] = 'delegation_consent';
break;
case 'missing_search_console_property':
$query_params['step'] = 'search_console_property';
break;
}
return $query_params;
}
/**
* Returns the permissions URL to the authentication proxy.
*
* This only returns a URL if the user already has an access token set.
*
* @since 1.27.0
*
* @param Credentials $credentials Credentials instance.
* @param array $query_args Optional. Additional query parameters.
* @return string URL to the permissions page on the authentication proxy on success, or an empty string on failure.
*/
public function permissions_url( Credentials $credentials, array $query_args = array() ) {
if ( $credentials->has() ) {
$creds = $credentials->get();
$query_args['site_id'] = $creds['oauth2_client_id'];
}
$query_args['application_name'] = rawurlencode( self::get_application_name() );
$query_args['hl'] = $this->context->get_locale( 'user' );
return add_query_arg( $query_args, $this->url( self::PERMISSIONS_URI ) );
}
/**
* Gets a URL to the proxy with optional path.
*
* @since 1.1.2
*
* @param string $path Optional. Path to append to the base URL.
* @return string Complete proxy URL.
*/
public function url( $path = '' ) {
$url = self::PRODUCTION_BASE_URL;
if ( defined( 'GOOGLESITEKIT_PROXY_URL' ) ) {
$url = $this->sanitize_base_url( GOOGLESITEKIT_PROXY_URL );
}
$url = untrailingslashit( $url );
if ( $path && is_string( $path ) ) {
$url .= '/' . ltrim( $path, '/' );
}
return $url;
}
/**
* Sanitizes the given base URL.
*
* @since 1.154.0
*
* @param string $url Base URL to sanitize.
* @return string Sanitized base URL.
*/
public function sanitize_base_url( $url ) {
$allowed_urls = array(
self::PRODUCTION_BASE_URL,
self::STAGING_BASE_URL,
self::DEVELOPMENT_BASE_URL,
);
if ( in_array( $url, $allowed_urls, true ) ) {
return $url;
}
// Allow for version-specific URLs to application instances.
if ( preg_match( '#^https://(?:\d{8}t\d{6}-dot-)?site-kit(?:-dev|-local)?(?:\.[a-z]{2}\.r)?\.appspot\.com/?$#', $url, $_ ) ) {
return $url;
}
return self::PRODUCTION_BASE_URL;
}
/**
* Sends a POST request to the Google Proxy server.
*
* @since 1.27.0
*
* @param string $uri Endpoint to send the request to.
* @param Credentials $credentials Credentials instance.
* @param array $args Array of request arguments.
* @return array|WP_Error The response as an associative array or WP_Error on failure.
*/
private function request( $uri, $credentials, array $args = array() ) {
$request_args = array(
'headers' => ! empty( $args['headers'] ) && is_array( $args['headers'] ) ? $args['headers'] : array(),
'body' => ! empty( $args['body'] ) && is_array( $args['body'] ) ? $args['body'] : array(),
'timeout' => isset( $args['timeout'] ) ? $args['timeout'] : 15,
);
if ( $credentials && $credentials instanceof Credentials ) {
if ( ! $credentials->has() ) {
return new WP_Error(
'oauth_credentials_not_exist',
__( 'OAuth credentials haven\'t been found.', 'google-site-kit' ),
array( 'status' => 401 )
);
}
$creds = $credentials->get();
$request_args['body']['site_id'] = $creds['oauth2_client_id'];
$request_args['body']['site_secret'] = $creds['oauth2_client_secret'];
}
if ( ! empty( $args['access_token'] ) && is_string( $args['access_token'] ) ) {
$request_args['headers']['Authorization'] = 'Bearer ' . $args['access_token'];
}
if ( isset( $args['mode'] ) && 'async' === $args['mode'] ) {
$request_args['timeout'] = 0.01;
$request_args['blocking'] = false;
}
if ( ! empty( $args['json_request'] ) ) {
$request_args['headers']['Content-Type'] = 'application/json';
$request_args['body'] = wp_json_encode( $request_args['body'] );
}
$url = $this->url( $uri );
$response = wp_remote_post( $url, $request_args );
if ( is_wp_error( $response ) ) {
return $response;
}
$code = wp_remote_retrieve_response_code( $response );
$body = wp_remote_retrieve_body( $response );
$body = json_decode( $body, true );
if ( $code < 200 || 299 < $code ) {
$message = is_array( $body ) && ! empty( $body['error'] ) ? $body['error'] : '';
return new WP_Error( 'request_failed', $message, array( 'status' => $code ) );
}
if ( ! empty( $args['return'] ) && 'response' === $args['return'] ) {
return $response;
}
if ( is_null( $body ) ) {
return new WP_Error(
'failed_to_parse_response',
__( 'Failed to parse response.', 'google-site-kit' ),
array( 'status' => 500 )
);
}
return $body;
}
/**
* Gets site fields.
*
* @since 1.5.0
*
* @return array Associative array of $query_arg => $value pairs.
*/
public function get_site_fields() {
return array(
'name' => wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES ),
'url' => $this->context->get_canonical_home_url(),
'redirect_uri' => add_query_arg( 'oauth2callback', 1, admin_url( 'index.php' ) ),
'action_uri' => admin_url( 'index.php' ),
'return_uri' => $this->context->admin_url( 'splash' ),
'analytics_redirect_uri' => add_query_arg( 'gatoscallback', 1, admin_url( 'index.php' ) ),
);
}
/**
* Gets metadata fields.
*
* @since 1.68.0
*
* @return array Metadata fields array.
*/
public function get_metadata_fields() {
$metadata = array(
'supports' => implode( ' ', $this->get_supports() ),
'nonce' => wp_create_nonce( self::NONCE_ACTION ),
'mode' => '',
'hl' => $this->context->get_locale( 'user' ),
'application_name' => self::get_application_name(),
'service_version' => 'v2',
);
if ( Feature_Flags::enabled( 'setupFlowRefresh' ) ) {
$metadata['service_version'] = 'v3';
}
/**
* Filters the setup mode.
*
* @since 1.68.0
*
* @param string $mode An initial setup mode.
*/
$metadata['mode'] = apply_filters( 'googlesitekit_proxy_setup_mode', $metadata['mode'] );
return $metadata;
}
/**
* Gets user fields.
*
* @since 1.10.0
*
* @return array Associative array of $query_arg => $value pairs.
*/
public function get_user_fields() {
$user_roles = wp_get_current_user()->roles;
// If multisite, also consider network administrators.
if ( is_multisite() && current_user_can( 'manage_network' ) ) {
$user_roles[] = 'network_administrator';
}
$user_roles = array_unique( $user_roles );
return array(
'user_roles' => implode( ',', $user_roles ),
);
}
/**
* Unregisters the site on the proxy.
*
* @since 1.20.0
*
* @param Credentials $credentials Credentials instance.
* @return array|WP_Error Response data on success, otherwise WP_Error object.
*/
public function unregister_site( Credentials $credentials ) {
return $this->request( self::OAUTH2_DELETE_SITE_URI, $credentials );
}
/**
* Registers the site on the proxy.
*
* @since 1.68.0
*
* @param string $mode Sync mode.
* @return string|WP_Error Redirect URL on success, otherwise an error.
*/
public function register_site( $mode = 'async' ) {
return $this->send_site_fields( null, $mode );
}
/**
* Synchronizes site fields with the proxy.
*
* @since 1.5.0
* @since 1.68.0 Updated the function to return redirect URL.
*
* @param Credentials $credentials Credentials instance.
* @param string $mode Sync mode.
* @return string|WP_Error Redirect URL on success, otherwise an error.
*/
public function sync_site_fields( Credentials $credentials, $mode = 'async' ) {
return $this->send_site_fields( $credentials, $mode );
}
/**
* Sends site fields to the proxy.
*
* @since 1.68.0
*
* @param Credentials $credentials Credentials instance.
* @param string $mode Sync mode.
* @return string|WP_Error Redirect URL on success, otherwise an error.
*/
private function send_site_fields( ?Credentials $credentials = null, $mode = 'async' ) {
$response = $this->request(
self::OAUTH2_SITE_URI,
$credentials,
array(
'return' => 'response',
'mode' => $mode,
'body' => array_merge(
$this->get_site_fields(),
$this->get_user_fields(),
$this->get_metadata_fields(),
array(
'scope' => implode( ' ', $this->required_scopes ),
)
),
)
);
if ( is_wp_error( $response ) ) {
return $response;
}
$redirect_to = wp_remote_retrieve_header( $response, self::HEADER_REDIRECT_TO );
if ( empty( $redirect_to ) ) {
return new WP_Error(
'failed_to_retrive_redirect',
__( 'Failed to retrieve redirect URL.', 'google-site-kit' ),
array( 'status' => 500 )
);
}
return $redirect_to;
}
/**
* Exchanges a site code for client credentials from the proxy.
*
* @since 1.1.2
*
* @param string $site_code Site code identifying the site.
* @param string $undelegated_code Undelegated code identifying the undelegated token.
* @return array|WP_Error Response data containing site_id and site_secret on success, WP_Error object on failure.
*/
public function exchange_site_code( $site_code, $undelegated_code ) {
$response_data = $this->request(
self::OAUTH2_SITE_URI,
null,
array(
'body' => array(
'code' => $undelegated_code,
'site_code' => $site_code,
),
)
);
if ( is_wp_error( $response_data ) ) {
return $response_data;
}
if ( ! isset( $response_data['site_id'], $response_data['site_secret'] ) ) {
return new WP_Error(
'oauth_credentials_not_exist',
__( 'OAuth credentials haven\'t been found.', 'google-site-kit' ),
array( 'status' => 401 )
);
}
return $response_data;
}
/**
* Gets remote features.
*
* @since 1.27.0
* @since 1.104.0 Added `php_version` to request.
*
* @param Credentials $credentials Credentials instance.
* @return array|WP_Error Response of the wp_remote_post request.
*/
public function get_features( Credentials $credentials ) {
global $wp_version;
$platform = self::get_platform();
$user_count = count_users();
$connectable_user_count = isset( $user_count['avail_roles']['administrator'] ) ? $user_count['avail_roles']['administrator'] : 0;
$body = array(
'platform' => $platform . '/google-site-kit',
'version' => GOOGLESITEKIT_VERSION,
'platform_version' => $wp_version,
'php_version' => phpversion(),
'user_count' => $user_count['total_users'],
'connectable_user_count' => $connectable_user_count,
'connected_user_count' => $this->count_connected_users(),
);
/**
* Filters additional context data sent with the body of a remote-controlled features request.
*
* @since 1.71.0
*
* @param array $body Context data to be sent with the features request.
*/
$body = apply_filters( 'googlesitekit_features_request_data', $body );
return $this->request( self::FEATURES_URI, $credentials, array( 'body' => $body ) );
}
/**
* Gets the number of users who are connected (i.e. authenticated /
* have an access token).
*
* @since 1.71.0
*
* @return int Number of WordPress user accounts connected to SiteKit.
*/
public function count_connected_users() {
$user_options = new User_Options( $this->context );
$connected_users = get_users(
array(
'meta_key' => $user_options->get_meta_key( OAuth_Client::OPTION_ACCESS_TOKEN ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
'meta_compare' => 'EXISTS',
'role' => 'administrator',
'fields' => 'ID',
)
);
return count( $connected_users );
}
/**
* Gets the platform.
*
* @since 1.37.0
*
* @return string WordPress multisite or WordPress.
*/
public static function get_platform() {
if ( is_multisite() ) {
return 'wordpress-multisite';
}
return 'wordpress'; // phpcs:ignore WordPress.WP.CapitalPDangit.MisspelledInText
}
/**
* Sends survey trigger ID to the proxy.
*
* @since 1.35.0
*
* @param Credentials $credentials Credentials instance.
* @param string $access_token Access token.
* @param string $trigger_id Token ID.
* @return array|WP_Error Response of the wp_remote_post request.
*/
public function send_survey_trigger( Credentials $credentials, $access_token, $trigger_id ) {
return $this->request(
self::SURVEY_TRIGGER_URI,
$credentials,
array(
'access_token' => $access_token,
'json_request' => true,
'body' => array(
'trigger_context' => array(
'trigger_id' => $trigger_id,
'language' => get_user_locale(),
),
),
)
);
}
/**
* Sends survey event to the proxy.
*
* @since 1.35.0
*
* @param Credentials $credentials Credentials instance.
* @param string $access_token Access token.
* @param array|\stdClass $session Session object.
* @param array|\stdClass $event Event object.
* @return array|WP_Error Response of the wp_remote_post request.
*/
public function send_survey_event( Credentials $credentials, $access_token, $session, $event ) {
return $this->request(
self::SURVEY_EVENT_URI,
$credentials,
array(
'access_token' => $access_token,
'json_request' => true,
'body' => array(
'session' => $session,
'event' => $event,
),
)
);
}
}

View File

@@ -0,0 +1,51 @@
<?php
/**
* Class Google\Site_Kit\Core\Authentication\Guards\Site_Connected_Guard
*
* @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\Core\Authentication\Guards;
use Google\Site_Kit\Core\Authentication\Credentials;
use Google\Site_Kit\Core\Guards\Guard_Interface;
/**
* Class providing guard logic for site connection.
*
* @since 1.133.0
* @access private
* @ignore
*/
class Site_Connected_Guard implements Guard_Interface {
/**
* Credentials instance.
*
* @var Credentials
*/
private Credentials $credentials;
/**
* Constructor.
*
* @since 1.133.0
* @param Credentials $credentials Credentials instance.
*/
public function __construct( Credentials $credentials ) {
$this->credentials = $credentials;
}
/**
* Determines whether the guarded entity can be activated or not.
*
* @since 1.133.0
* @return bool|\WP_Error
*/
public function can_activate() {
return $this->credentials->has();
}
}

View File

@@ -0,0 +1,51 @@
<?php
/**
* Class Google\Site_Kit\Core\Authentication\Guards\Using_Proxy_Connection_Guard
*
* @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\Core\Authentication\Guards;
use Google\Site_Kit\Core\Authentication\Credentials;
use Google\Site_Kit\Core\Guards\Guard_Interface;
/**
* Class providing guard logic based on proxy connection.
*
* @since 1.133.0
* @access private
* @ignore
*/
class Using_Proxy_Connection_Guard implements Guard_Interface {
/**
* Credentials instance.
*
* @var Credentials
*/
private Credentials $credentials;
/**
* Constructor.
*
* @since 1.133.0
* @param Credentials $credentials Credentials instance.
*/
public function __construct( Credentials $credentials ) {
$this->credentials = $credentials;
}
/**
* Determines whether the guarded entity can be activated or not.
*
* @since 1.133.0
* @return bool|\WP_Error
*/
public function can_activate() {
return $this->credentials->using_proxy();
}
}

View File

@@ -0,0 +1,139 @@
<?php
/**
* Class Google\Site_Kit\Core\Authentication\Has_Connected_Admins
*
* @package Google\Site_Kit\Core\Authentication
* @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\Core\Authentication;
use Google\Site_Kit\Core\Authentication\Clients\OAuth_Client;
use Google\Site_Kit\Core\Storage\Options_Interface;
use Google\Site_Kit\Core\Storage\Setting;
use Google\Site_Kit\Core\Storage\User_Options_Interface;
use WP_User;
/**
* Has_Connected_Admins class.
*
* @since 1.14.0
* @access private
* @ignore
*/
class Has_Connected_Admins extends Setting {
/**
* The option_name for this setting.
*/
const OPTION = 'googlesitekit_has_connected_admins';
/**
* User options instance implementing User_Options_Interface.
*
* @since 1.14.0
* @var User_Options_Interface
*/
protected $user_options;
/**
* Constructor.
*
* @since 1.14.0
*
* @param Options_Interface $options Options instance.
* @param User_Options_Interface $user_options User options instance.
*/
public function __construct( Options_Interface $options, User_Options_Interface $user_options ) {
parent::__construct( $options );
$this->user_options = $user_options;
}
/**
* Registers the setting in WordPress.
*
* @since 1.14.0
*/
public function register() {
parent::register();
$access_token_meta_key = $this->user_options->get_meta_key( OAuth_Client::OPTION_ACCESS_TOKEN );
add_action(
'added_user_meta',
function ( $mid, $uid, $meta_key ) use ( $access_token_meta_key ) {
// phpcs:ignore WordPress.WP.Capabilities.RoleFound
if ( $meta_key === $access_token_meta_key && user_can( $uid, 'administrator' ) ) {
$this->set( true );
}
},
10,
3
);
add_action(
'deleted_user_meta',
function ( $mid, $uid, $meta_key ) use ( $access_token_meta_key ) {
if ( $meta_key === $access_token_meta_key ) {
$this->delete();
}
},
10,
3
);
}
/**
* Gets the value of the setting. If the option is not set yet, it pulls connected
* admins from the database and sets the option.
*
* @since 1.14.0
*
* @return boolean TRUE if the site kit already has connected admins, otherwise FALSE.
*/
public function get() {
// If the option doesn't exist, query the fresh value, set it and return it.
if ( ! $this->has() ) {
$users = $this->query_connected_admins();
$has_connected_admins = count( $users ) > 0;
$this->set( (int) $has_connected_admins );
return $has_connected_admins;
}
return (bool) parent::get();
}
/**
* Queries connected admins and returns an array of connected admin IDs.
*
* @since 1.14.0
*
* @return array The array of connected admin IDs.
*/
protected function query_connected_admins() {
return get_users(
array(
'meta_key' => $this->user_options->get_meta_key( OAuth_Client::OPTION_ACCESS_TOKEN ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
'meta_compare' => 'EXISTS',
'role' => 'administrator',
'number' => 1,
'fields' => 'ID',
)
);
}
/**
* Gets the expected value type.
*
* @since 1.14.0
*
* @return string The type name.
*/
protected function get_type() {
return 'boolean';
}
}

View File

@@ -0,0 +1,73 @@
<?php
/**
* Class Google\Site_Kit\Core\Authentication\Has_Multiple_Admins
*
* @package Google\Site_Kit\Core\Authentication
* @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\Core\Authentication;
use Google\Site_Kit\Core\Storage\Transients;
use WP_User_Query;
/**
* Has_Multiple_Admins class.
*
* @since 1.29.0
* @access private
* @ignore
*/
class Has_Multiple_Admins {
/**
* The option_name for this transient.
*/
const OPTION = 'googlesitekit_has_multiple_admins';
/**
* Transients instance.
*
* @since 1.29.0
* @var Transients
*/
protected $transients;
/**
* Constructor.
*
* @since 1.29.0
*
* @param Transients $transients Transients instance.
*/
public function __construct( Transients $transients ) {
$this->transients = $transients;
}
/**
* Returns a flag indicating whether the current site has multiple users.
*
* @since 1.29.0
*
* @return boolean TRUE if the site kit has multiple admins, otherwise FALSE.
*/
public function get() {
$admins_count = $this->transients->get( self::OPTION );
if ( false === $admins_count ) {
$user_query_args = array(
'number' => 1,
'role__in' => array( 'Administrator' ),
'count_total' => true,
);
$user_query = new WP_User_Query( $user_query_args );
$admins_count = $user_query->get_total();
$this->transients->get( self::OPTION, $admins_count, HOUR_IN_SECONDS );
}
return $admins_count > 1;
}
}

View File

@@ -0,0 +1,28 @@
<?php
/**
* Class Google\Site_Kit\Core\Authentication\Initial_Version
*
* @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\Core\Authentication;
use Google\Site_Kit\Core\Storage\User_Setting;
/**
* Class representing the initial Site Kit version the user started with.
*
* @since 1.25.0
* @access private
* @ignore
*/
final class Initial_Version extends User_Setting {
/**
* User option key.
*/
const OPTION = 'googlesitekitpersistent_initial_version';
}

View File

@@ -0,0 +1,75 @@
<?php
/**
* Class Google\Site_Kit\Core\Authentication\Owner_ID
*
* @package Google\Site_Kit\Core\Authentication
* @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\Core\Authentication;
use Google\Site_Kit\Core\Storage\Setting;
/**
* Owner_ID class.
*
* @since 1.16.0
* @access private
* @ignore
*/
class Owner_ID extends Setting {
/**
* The option_name for this setting.
*/
const OPTION = 'googlesitekit_owner_id';
/**
* Gets the value of the setting.
*
* @since 1.16.0
*
* @return mixed Value set for the option, or registered default if not set.
*/
public function get() {
return (int) parent::get();
}
/**
* Gets the expected value type.
*
* @since 1.16.0
*
* @return string The type name.
*/
protected function get_type() {
return 'integer';
}
/**
* Gets the default value.
*
* We use the old "googlesitekit_first_admin" option here as it used to store the ID
* of the first admin user to use the plugin. If this option doesn't exist, it will return 0.
*
* @since 1.16.0
*
* @return int The default value.
*/
protected function get_default() {
return (int) $this->options->get( 'googlesitekit_first_admin' );
}
/**
* Gets the callback for sanitizing the setting's value before saving.
*
* @since 1.16.0
*
* @return callable The callable sanitize callback.
*/
protected function get_sanitize_callback() {
return 'intval';
}
}

View File

@@ -0,0 +1,85 @@
<?php
/**
* Class Google\Site_Kit\Core\Authentication\Profile
*
* @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\Core\Authentication;
use Google\Site_Kit\Core\Storage\User_Options;
/**
* Class controlling the user's Google profile.
*
* @since 0.1.0
*/
final class Profile {
/**
* Option key in options table.
*/
const OPTION = 'googlesitekit_profile';
/**
* User_Options instance.
*
* @since 1.0.0
* @var User_Options
*/
private $user_options;
/**
* Constructor.
*
* @since 1.0.0
*
* @param User_Options $user_options User_Options instance.
*/
public function __construct( User_Options $user_options ) {
$this->user_options = $user_options;
}
/**
* Retrieves user profile data.
*
* @since 1.0.0
*
* @return array|bool Value set for the profile, or false if not set.
*/
public function get() {
return $this->user_options->get( self::OPTION );
}
/**
* Saves user profile data.
*
* @since 1.0.0
*
* @param array $data User profile data: email and photo.
* @return bool True on success, false on failure.
*/
public function set( $data ) {
return $this->user_options->set( self::OPTION, $data );
}
/**
* Verifies if user has their profile information stored.
*
* @since 1.0.0
*
* @return bool True if profile is set, false otherwise.
*/
public function has() {
$profile = (array) $this->get();
if ( ! empty( $profile['email'] ) && ! empty( $profile['photo'] ) ) {
return true;
}
return false;
}
}

View File

@@ -0,0 +1,178 @@
<?php
/**
* Class Google\Site_Kit\Core\Authentication\REST_Authentication_Controller
*
* @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\Core\Authentication;
use Google\Site_Kit\Core\Permissions\Permissions;
use Google\Site_Kit\Core\REST_API\REST_Route;
use Google\Site_Kit\Core\REST_API\REST_Routes;
use WP_REST_Server;
use WP_REST_Request;
use WP_REST_Response;
/**
* REST Authentication Controller Class.
*
* @since 1.131.0
* @access private
* @ignore
*/
final class REST_Authentication_Controller {
/**
* Authentication instance.
*
* @since 1.131.0
* @var Authentication
*/
protected $authentication;
/**
* Constructor.
*
* @since 1.131.0
*
* @param Authentication $authentication Authentication instance.
*/
public function __construct( Authentication $authentication ) {
$this->authentication = $authentication;
}
/**
* Registers functionality through WordPress hooks.
*
* @since 1.131.0
*/
public function register() {
add_filter(
'googlesitekit_rest_routes',
function ( $routes ) {
return array_merge( $routes, $this->get_rest_routes() );
}
);
add_filter(
'googlesitekit_apifetch_preload_paths',
function ( $routes ) {
$authentication_routes = array(
'/' . REST_Routes::REST_ROOT . '/core/site/data/connection',
'/' . REST_Routes::REST_ROOT . '/core/user/data/authentication',
);
return array_merge( $routes, $authentication_routes );
}
);
}
/**
* Gets related REST routes.
*
* @since 1.3.0
* @since 1.131.0 Moved to REST_Authentication_Controller class.
*
* @return array List of REST_Route objects.
*/
private function get_rest_routes() {
$can_setup = function () {
return current_user_can( Permissions::SETUP );
};
$can_access_authentication = function () {
return current_user_can( Permissions::VIEW_SPLASH ) || current_user_can( Permissions::VIEW_DASHBOARD );
};
$can_disconnect = function () {
return current_user_can( Permissions::AUTHENTICATE );
};
$can_view_authenticated_dashboard = function () {
return current_user_can( Permissions::VIEW_AUTHENTICATED_DASHBOARD );
};
return array(
new REST_Route(
'core/site/data/connection',
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => function () {
$data = array(
'connected' => $this->authentication->credentials()->has(),
'resettable' => $this->authentication->get_options_instance()->has( Credentials::OPTION ),
'setupCompleted' => $this->authentication->is_setup_completed(),
'hasConnectedAdmins' => $this->authentication->get_has_connected_admins_instance()->get(),
'hasMultipleAdmins' => $this->authentication->get_has_multiple_admins_instance()->get(),
'ownerID' => $this->authentication->get_owner_id_instance()->get(),
);
return new WP_REST_Response( $data );
},
'permission_callback' => $can_setup,
),
)
),
new REST_Route(
'core/user/data/authentication',
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => function () {
$oauth_client = $this->authentication->get_oauth_client();
$is_authenticated = $this->authentication->is_authenticated();
$data = array(
'authenticated' => $is_authenticated,
'requiredScopes' => $oauth_client->get_required_scopes(),
'grantedScopes' => $is_authenticated ? $oauth_client->get_granted_scopes() : array(),
'unsatisfiedScopes' => $is_authenticated ? $oauth_client->get_unsatisfied_scopes() : array(),
'needsReauthentication' => $oauth_client->needs_reauthentication(),
'disconnectedReason' => $this->authentication->get_disconnected_reason_instance()->get(),
'connectedProxyURL' => $this->authentication->get_connected_proxy_url_instance()->get(),
);
return new WP_REST_Response( $data );
},
'permission_callback' => $can_access_authentication,
),
)
),
new REST_Route(
'core/user/data/disconnect',
array(
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => function () {
$this->authentication->disconnect();
return new WP_REST_Response( true );
},
'permission_callback' => $can_disconnect,
),
)
),
new REST_Route(
'core/user/data/get-token',
array(
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => function () {
$this->authentication->do_refresh_user_token();
return new WP_REST_Response(
array(
'token' => $this->authentication->get_oauth_client()->get_access_token(),
)
);
},
'permission_callback' => $can_view_authenticated_dashboard,
),
)
),
);
}
}

View File

@@ -0,0 +1,408 @@
<?php
/**
* Class Google\Site_Kit\Core\Authentication\Setup
*
* @package Google\Site_Kit\Core\Authentication
* @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\Core\Authentication;
use Google\Site_Kit\Context;
use Google\Site_Kit\Core\Authentication\Clients\OAuth_Client;
use Google\Site_Kit\Core\Authentication\Exception\Exchange_Site_Code_Exception;
use Google\Site_Kit\Core\Authentication\Exception\Missing_Verification_Exception;
use Google\Site_Kit\Core\Permissions\Permissions;
use Google\Site_Kit\Core\Storage\User_Options;
use Google\Site_Kit\Core\Util\Remote_Features;
/**
* Base class for authentication setup.
*
* @since 1.48.0
* @access private
* @ignore
*/
class Setup {
/**
* Context instance.
*
* @since 1.48.0
*
* @var Context
*/
protected $context;
/**
* User_Options instance.
*
* @since 1.48.0
*
* @var User_Options
*/
protected $user_options;
/**
* Authentication instance.
*
* @since 1.48.0
*
* @var Authentication
*/
protected $authentication;
/**
* Google_Proxy instance.
*
* @since 1.48.0
*
* @var Google_Proxy
*/
protected $google_proxy;
/**
* Proxy support URL.
*
* @since 1.109.0 Explicitly declared; previously, it was dynamically declared.
*
* @var string
*/
protected $proxy_support_link_url;
/**
* Credentials instance.
*
* @since 1.48.0
*
* @var Credentials
*/
protected $credentials;
/**
* Constructor.
*
* @since 1.48.0
*
* @param Context $context Context instance.
* @param User_Options $user_options User_Options instance.
* @param Authentication $authentication Authentication instance.
*/
public function __construct(
Context $context,
User_Options $user_options,
Authentication $authentication
) {
$this->context = $context;
$this->user_options = $user_options;
$this->authentication = $authentication;
$this->credentials = $authentication->credentials();
$this->google_proxy = $authentication->get_google_proxy();
$this->proxy_support_link_url = $authentication->get_proxy_support_link_url();
}
/**
* Registers functionality through WordPress hooks.
*
* @since 1.48.0
*/
public function register() {
add_action( 'admin_action_' . Google_Proxy::ACTION_SETUP_START, array( $this, 'handle_action_setup_start' ) );
add_action( 'admin_action_' . Google_Proxy::ACTION_VERIFY, array( $this, 'handle_action_verify' ) );
add_action( 'admin_action_' . Google_Proxy::ACTION_EXCHANGE_SITE_CODE, array( $this, 'handle_action_exchange_site_code' ) );
}
/**
* Composes the oAuth proxy get help link.
*
* @since 1.81.0
*
* @return string The get help link.
*/
private function get_oauth_proxy_failed_help_link() {
return sprintf(
/* translators: 1: Support link URL. 2: Get help string. */
__( '<a href="%1$s" target="_blank">%2$s</a>', 'google-site-kit' ),
esc_url( add_query_arg( 'error_id', 'request_to_auth_proxy_failed', $this->proxy_support_link_url ) ),
esc_html__( 'Get help', 'google-site-kit' )
);
}
/**
* Handles the setup start action, taking the user to the proxy setup screen.
*
* @since 1.48.0
*/
public function handle_action_setup_start() {
$nonce = htmlspecialchars( $this->context->input()->filter( INPUT_GET, 'nonce' ) ?? '' );
$redirect_url = $this->context->input()->filter( INPUT_GET, 'redirect', FILTER_DEFAULT );
$this->verify_nonce( $nonce, Google_Proxy::ACTION_SETUP_START );
if ( ! current_user_can( Permissions::SETUP ) ) {
wp_die( esc_html__( 'You have insufficient permissions to connect Site Kit.', 'google-site-kit' ) );
}
if ( ! $this->credentials->using_proxy() ) {
wp_die( esc_html__( 'Site Kit is not configured to use the authentication proxy.', 'google-site-kit' ) );
}
$required_scopes = $this->authentication->get_oauth_client()->get_required_scopes();
$this->google_proxy->with_scopes( $required_scopes );
$oauth_setup_redirect = $this->credentials->has()
? $this->google_proxy->sync_site_fields( $this->credentials, 'sync' )
: $this->google_proxy->register_site( 'sync' );
$oauth_proxy_failed_help_link = $this->get_oauth_proxy_failed_help_link();
if ( is_wp_error( $oauth_setup_redirect ) ) {
$error_message = $oauth_setup_redirect->get_error_message();
if ( empty( $error_message ) ) {
$error_message = $oauth_setup_redirect->get_error_code();
}
wp_die(
sprintf(
/* translators: 1: Error message or error code. 2: Get help link. */
esc_html__( 'The request to the authentication proxy has failed with an error: %1$s %2$s.', 'google-site-kit' ),
esc_html( $error_message ),
wp_kses(
$oauth_proxy_failed_help_link,
array(
'a' => array(
'href' => array(),
'target' => array(),
),
)
)
)
);
}
if ( ! filter_var( $oauth_setup_redirect, FILTER_VALIDATE_URL ) ) {
wp_die(
sprintf(
/* translators: %s: Get help link. */
esc_html__( 'The request to the authentication proxy has failed. Please, try again later. %s.', 'google-site-kit' ),
wp_kses(
$oauth_proxy_failed_help_link,
array(
'a' => array(
'href' => array(),
'target' => array(),
),
)
)
)
);
}
if ( $redirect_url ) {
$this->user_options->set( OAuth_Client::OPTION_REDIRECT_URL, $redirect_url );
}
wp_safe_redirect( $oauth_setup_redirect );
exit;
}
/**
* Handles the action for verifying site ownership.
*
* @since 1.48.0
* @since 1.49.0 Sets the `verify` and `verification_method` and `site_id` query params.
*/
public function handle_action_verify() {
$input = $this->context->input();
$step = htmlspecialchars( $input->filter( INPUT_GET, 'step' ) ?? '' );
$nonce = htmlspecialchars( $input->filter( INPUT_GET, 'nonce' ) ?? '' );
$code = htmlspecialchars( $input->filter( INPUT_GET, 'googlesitekit_code' ) ?? '' );
$site_code = htmlspecialchars( $input->filter( INPUT_GET, 'googlesitekit_site_code' ) ?? '' );
$verification_token = htmlspecialchars( $input->filter( INPUT_GET, 'googlesitekit_verification_token' ) ?? '' );
$verification_method = htmlspecialchars( $input->filter( INPUT_GET, 'googlesitekit_verification_token_type' ) ?? '' );
$this->verify_nonce( $nonce );
if ( ! current_user_can( Permissions::SETUP ) ) {
wp_die( esc_html__( 'You dont have permissions to set up Site Kit.', 'google-site-kit' ), 403 );
}
if ( ! $code ) {
wp_die( esc_html__( 'Invalid request.', 'google-site-kit' ), 400 );
}
if ( ! $verification_token || ! $verification_method ) {
wp_die( esc_html__( 'Verifying site ownership requires a token and verification method.', 'google-site-kit' ), 400 );
}
$this->handle_verification( $verification_token, $verification_method );
$proxy_query_params = array(
'step' => $step,
'verify' => 'true',
'verification_method' => $verification_method,
);
// If the site does not have a site ID yet, a site code will be passed.
// Handling the site code here will save the extra redirect from the proxy if successful.
if ( $site_code ) {
try {
$this->handle_site_code( $code, $site_code );
} catch ( Missing_Verification_Exception $exception ) {
$proxy_query_params['site_code'] = $site_code;
$this->redirect_to_proxy( $code, $proxy_query_params );
} catch ( Exchange_Site_Code_Exception $exception ) {
$this->redirect_to_splash();
}
}
$credentials = $this->credentials->get();
$proxy_query_params['site_id'] = ! empty( $credentials['oauth2_client_id'] ) ? $credentials['oauth2_client_id'] : '';
$this->redirect_to_proxy( $code, $proxy_query_params );
}
/**
* Handles the action for exchanging the site code for site credentials.
*
* This action will only be called if the site code failed to be handled
* during the verification step.
*
* @since 1.48.0
*/
public function handle_action_exchange_site_code() {
$input = $this->context->input();
$step = htmlspecialchars( $input->filter( INPUT_GET, 'step' ) ?? '' );
$nonce = htmlspecialchars( $input->filter( INPUT_GET, 'nonce' ) ?? '' );
$code = htmlspecialchars( $input->filter( INPUT_GET, 'googlesitekit_code' ) ?? '' );
$site_code = htmlspecialchars( $input->filter( INPUT_GET, 'googlesitekit_site_code' ) ?? '' );
$this->verify_nonce( $nonce );
if ( ! current_user_can( Permissions::SETUP ) ) {
wp_die( esc_html__( 'You dont have permissions to set up Site Kit.', 'google-site-kit' ), 403 );
}
if ( ! $code || ! $site_code ) {
wp_die( esc_html__( 'Invalid request.', 'google-site-kit' ), 400 );
}
try {
$this->handle_site_code( $code, $site_code );
} catch ( Missing_Verification_Exception $exception ) {
$this->redirect_to_proxy( $code, compact( 'site_code', 'step' ) );
} catch ( Exchange_Site_Code_Exception $exception ) {
$this->redirect_to_splash();
}
$credentials = $this->credentials->get();
$site_id = ! empty( $credentials['oauth2_client_id'] ) ? $credentials['oauth2_client_id'] : '';
$this->redirect_to_proxy( $code, compact( 'site_id', 'step' ) );
}
/**
* Verifies the given nonce for a setup action.
*
* The nonce passed from the proxy will always be the one initially provided to it.
* {@see Google_Proxy::setup_url()}
*
* @since 1.48.0
*
* @param string $nonce Action nonce.
* @param string $action Action name. Optional. Defaults to the action for the nonce given to the proxy.
*/
protected function verify_nonce( $nonce, $action = Google_Proxy::NONCE_ACTION ) {
if ( ! wp_verify_nonce( $nonce, $action ) ) {
$this->authentication->invalid_nonce_error( $action );
}
}
/**
* Handles site verification.
*
* @since 1.48.0
*
* @param string $token Verification token.
* @param string $method Verification method.
*/
protected function handle_verification( $token, $method ) {
/**
* Verifies site ownership using the given token and verification method.
*
* @since 1.48.0
*
* @param string $token Verification token.
* @param string $method Verification method.
*/
do_action( 'googlesitekit_verify_site_ownership', $token, $method );
}
/**
* Handles the exchange of a code and site code for client credentials from the proxy.
*
* @since 1.48.0
*
* @param string $code Code ('googlesitekit_code') provided by proxy.
* @param string $site_code Site code ('googlesitekit_site_code') provided by proxy.
*
* @throws Missing_Verification_Exception Thrown if exchanging the site code fails due to missing site verification.
* @throws Exchange_Site_Code_Exception Thrown if exchanging the site code fails for any other reason.
*/
protected function handle_site_code( $code, $site_code ) {
$data = $this->google_proxy->exchange_site_code( $site_code, $code );
if ( is_wp_error( $data ) ) {
$error_code = $data->get_error_message() ?: $data->get_error_code();
$error_code = $error_code ?: 'unknown_error';
if ( 'missing_verification' === $error_code ) {
throw new Missing_Verification_Exception();
}
$this->user_options->set( OAuth_Client::OPTION_ERROR_CODE, $error_code );
throw new Exchange_Site_Code_Exception( $error_code );
}
$this->credentials->set(
array(
'oauth2_client_id' => $data['site_id'],
'oauth2_client_secret' => $data['site_secret'],
)
);
}
/**
* Redirects back to the authentication service with any added parameters.
*
* For v2 of the proxy, this method now has to ensure that the user is redirected back to the correct step on the
* proxy, based on which action was received.
*
* @since 1.48.0
* @since 1.49.0 Uses the new `Google_Proxy::setup_url_v2` method when the `serviceSetupV2` feature flag is enabled.
*
* @param string $code Code ('googlesitekit_code') provided by proxy.
* @param array $params Additional query parameters to include in the proxy redirect URL.
*/
protected function redirect_to_proxy( $code = '', $params = array() ) {
$params['code'] = $code;
$url = $this->authentication->get_google_proxy()->setup_url( $params );
wp_safe_redirect( $url );
exit;
}
/**
* Redirects to the Site Kit splash page.
*
* @since 1.48.0
*/
protected function redirect_to_splash() {
wp_safe_redirect( $this->context->admin_url( 'splash' ) );
exit;
}
}

View File

@@ -0,0 +1,147 @@
<?php
/**
* Class Google\Site_Kit\Core\Authentication\Token
*
* @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\Core\Authentication;
use Google\Site_Kit\Core\Storage\User_Options;
use Google\Site_Kit\Core\Storage\Encrypted_User_Options;
use Google\Site_Kit\Core\Authentication\Clients\OAuth_Client;
/**
* Class representing the OAuth token for a user.
*
* This includes the access token, its creation and expiration data, and the refresh token.
* This class is compatible with `Google\Site_Kit\Core\Storage\User_Setting`, as it should in the future be adjusted
* so that the four pieces of data become a single user setting.
*
* @since 1.39.0
* @access private
* @ignore
*/
final class Token {
/**
* User_Options instance.
*
* @since 1.39.0
* @var User_Options
*/
protected $user_options;
/**
* Encrypted_User_Options instance.
*
* @since 1.39.0
* @var Encrypted_User_Options
*/
private $encrypted_user_options;
/**
* Constructor.
*
* @since 1.39.0
*
* @param User_Options $user_options User_Options instance.
*/
public function __construct( User_Options $user_options ) {
$this->user_options = $user_options;
$this->encrypted_user_options = new Encrypted_User_Options( $this->user_options );
}
/**
* Checks whether or not the setting exists.
*
* @since 1.39.0
*
* @return bool True on success, false on failure.
*/
public function has() {
if ( ! $this->get() ) {
return false;
}
return true;
}
/**
* Gets the value of the setting.
*
* @since 1.39.0
*
* @return mixed Value set for the option, or default if not set.
*/
public function get() {
$access_token = $this->encrypted_user_options->get( OAuth_Client::OPTION_ACCESS_TOKEN );
if ( empty( $access_token ) ) {
return array();
}
$token = array(
'access_token' => $access_token,
'expires_in' => (int) $this->user_options->get( OAuth_Client::OPTION_ACCESS_TOKEN_EXPIRES_IN ),
'created' => (int) $this->user_options->get( OAuth_Client::OPTION_ACCESS_TOKEN_CREATED ),
);
$refresh_token = $this->encrypted_user_options->get( OAuth_Client::OPTION_REFRESH_TOKEN );
if ( ! empty( $refresh_token ) ) {
$token['refresh_token'] = $refresh_token;
}
return $token;
}
/**
* Sets the value of the setting with the given value.
*
* @since 1.39.0
*
* @param mixed $value Setting value. Must be serializable if non-scalar.
*
* @return bool True on success, false on failure.
*/
public function set( $value ) {
if ( empty( $value['access_token'] ) ) {
return false;
}
// Use reasonable defaults for these fields.
if ( empty( $value['expires_in'] ) ) {
$value['expires_in'] = HOUR_IN_SECONDS;
}
if ( empty( $value['created'] ) ) {
$value['created'] = time();
}
$this->encrypted_user_options->set( OAuth_Client::OPTION_ACCESS_TOKEN, $value['access_token'] );
$this->user_options->set( OAuth_Client::OPTION_ACCESS_TOKEN_EXPIRES_IN, $value['expires_in'] );
$this->user_options->set( OAuth_Client::OPTION_ACCESS_TOKEN_CREATED, $value['created'] );
if ( ! empty( $value['refresh_token'] ) ) {
$this->encrypted_user_options->set( OAuth_Client::OPTION_REFRESH_TOKEN, $value['refresh_token'] );
}
return true;
}
/**
* Deletes the setting.
*
* @since 1.39.0
*
* @return bool True on success, false on failure.
*/
public function delete() {
$this->user_options->delete( OAuth_Client::OPTION_ACCESS_TOKEN );
$this->user_options->delete( OAuth_Client::OPTION_ACCESS_TOKEN_EXPIRES_IN );
$this->user_options->delete( OAuth_Client::OPTION_ACCESS_TOKEN_CREATED );
$this->user_options->delete( OAuth_Client::OPTION_REFRESH_TOKEN );
return true;
}
}

View File

@@ -0,0 +1,79 @@
<?php
/**
* Class Google\Site_Kit\Core\Authentication\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\Core\Authentication;
use Google\Site_Kit\Core\Storage\User_Setting;
/**
* Class representing the status of whether a user is verified as an owner of the site.
*
* @since 1.0.0
* @access private
* @ignore
*/
final class Verification extends User_Setting {
/**
* User option key.
*/
const OPTION = 'googlesitekit_site_verified_meta';
/**
* Gets the value of the setting.
*
* @since 1.4.0
*
* @return mixed Value set for the option, or default if not set.
*/
public function get() {
return (bool) parent::get();
}
/**
* Flags the user as verified or unverified.
*
* @since 1.0.0
*
* @param bool $verified Whether to flag the user as verified or unverified.
* @return bool True on success, false on failure.
*/
public function set( $verified ) {
if ( ! $verified ) {
return $this->delete();
}
return parent::set( '1' );
}
/**
* Gets the expected value type.
*
* @since 1.4.0
*
* @return string The type name.
*/
protected function get_type() {
return 'boolean';
}
/**
* Gets the default value.
*
* Returns an empty string by default for consistency with get_user_meta.
*
* @since 1.4.0
*
* @return mixed The default value.
*/
protected function get_default() {
return false;
}
}

View File

@@ -0,0 +1,28 @@
<?php
/**
* Class Google\Site_Kit\Core\Authentication\Verification_File
*
* @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\Core\Authentication;
use Google\Site_Kit\Core\Storage\User_Setting;
/**
* Class representing the site verification file token for a user.
*
* @since 1.1.0
* @access private
* @ignore
*/
final class Verification_File extends User_Setting {
/**
* User option key.
*/
const OPTION = 'googlesitekit_site_verification_file';
}

View File

@@ -0,0 +1,28 @@
<?php
/**
* Class Google\Site_Kit\Core\Authentication\Verification_Meta
*
* @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\Core\Authentication;
use Google\Site_Kit\Core\Storage\User_Setting;
/**
* Class representing the site verification meta tag for a user.
*
* @since 1.1.0
* @access private
* @ignore
*/
final class Verification_Meta extends User_Setting {
/**
* User option key.
*/
const OPTION = 'googlesitekit_site_verification_meta';
}

View File

@@ -0,0 +1,60 @@
<?php
/**
* Site Kit Authentication CLI Commands
*
* @package Google\Site_Kit\Core\CLI
* @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\Core\CLI;
use Google\Site_Kit\Core\Storage\Options;
use Google\Site_Kit\Core\Storage\User_Options;
use Google\Site_Kit\Core\Storage\Transients;
use Google\Site_Kit\Core\Authentication\Authentication;
use WP_CLI;
/**
* Manages Site Kit user authentication for Google APIs.
*
* @since 1.11.0
* @access private
* @ignore
*/
class Authentication_CLI_Command extends CLI_Command {
/**
* Disconnects a user from Site Kit, removing their relevant user options and revoking their token.
*
* ## OPTIONS
*
* --id=<id>
* : User ID to disconnect.
*
* ## EXAMPLES
*
* wp google-site-kit auth disconnect --id=11
*
* @alias revoke
*
* @since 1.11.0
*
* @param array $args Array of arguments.
* @param array $assoc_args Array of associated arguments.
*/
public function disconnect( $args, $assoc_args ) {
$user_id = absint( $assoc_args['id'] );
$authentication = new Authentication(
$this->context,
new Options( $this->context ),
new User_Options( $this->context, $user_id ),
new Transients( $this->context )
);
$authentication->disconnect();
WP_CLI::success( sprintf( 'User with ID %d successfully disconnected.', $user_id ) );
}
}

View File

@@ -0,0 +1,44 @@
<?php
/**
* Site Kit CLI Command
*
* @package Google\Site_Kit\Core\CLI
* @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\Core\CLI;
use Google\Site_Kit\Context;
use WP_CLI_Command;
/**
* Base CLI Command class.
*
* @since 1.11.0
* @access private
* @ignore
*/
class CLI_Command extends WP_CLI_Command {
/**
* Plugin context.
*
* @since 1.11.0
*
* @var Context
*/
protected $context;
/**
* Constructor.
*
* @since 1.11.0
*
* @param Context $context Plugin context.
*/
public function __construct( Context $context ) {
$this->context = $context;
}
}

View File

@@ -0,0 +1,54 @@
<?php
/**
* Class Google\Site_Kit\Core\CLI\CLI_Commands
*
* @package Google\Site_Kit\Core\CLI
* @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\Core\CLI;
use Google\Site_Kit\Context;
use WP_CLI;
/**
* CLI commands hub class.
*
* @since 1.11.0
* @access private
* @ignore
*/
class CLI_Commands {
/**
* Plugin context.
*
* @since 1.11.0
*
* @var Context
*/
private $context;
/**
* Constructor.
*
* @since 1.11.0
*
* @param Context $context Plugin context.
*/
public function __construct( Context $context ) {
$this->context = $context;
}
/**
* Registers WP CLI commands.
*
* @since 1.11.0
*/
public function register() {
WP_CLI::add_command( 'google-site-kit auth', new Authentication_CLI_Command( $this->context ) );
WP_CLI::add_command( 'google-site-kit reset', new Reset_CLI_Command( $this->context ) );
}
}

View File

@@ -0,0 +1,56 @@
<?php
/**
* Site Kit Cache CLI Commands
*
* @package Google\Site_Kit\Core\CLI
* @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\Core\CLI;
use Google\Site_Kit\Core\Util\Reset;
use Google\Site_Kit\Core\Util\Reset_Persistent;
use WP_CLI;
/**
* Resets Site Kit Settings and Data.
*
* @since 1.11.0
* @access private
* @ignore
*/
class Reset_CLI_Command extends CLI_Command {
/**
* Deletes options, user stored options, transients and clears object cache for stored options.
*
* ## OPTIONS
*
* [--persistent]
* : Additionally deletes persistent options.
*
* ## EXAMPLES
*
* wp google-site-kit reset
* wp google-site-kit reset --persistent
*
* @since 1.11.0
* @since 1.27.0 Added --persistent flag to delete persistent options.
*
* @param array $args Positional args.
* @param array $assoc_args Additional flags.
*/
public function __invoke( $args, $assoc_args ) {
$reset = new Reset( $this->context );
$reset->all();
if ( isset( $assoc_args['persistent'] ) && true === $assoc_args['persistent'] ) {
$reset_persistent = new Reset_Persistent( $this->context );
$reset_persistent->all();
}
WP_CLI::success( 'Settings successfully reset.' );
}
}

View File

@@ -0,0 +1,301 @@
<?php
/**
* Class Google\Site_Kit\Core\Consent_Mode\Consent_Mode
*
* @package Google\Site_Kit\Core\Consent_Mode
* @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\Consent_Mode;
use Google\Site_Kit\Context;
use Google\Site_Kit\Core\Assets\Script;
use Google\Site_Kit\Core\Modules\Modules;
use Google\Site_Kit\Core\Storage\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 Plugin_Upgrader;
use Plugin_Installer_Skin;
/**
* Class for handling consent mode.
*
* @since 1.122.0
* @access private
* @ignore
*/
class Consent_Mode implements Provides_Feature_Metrics {
use Method_Proxy_Trait;
use Feature_Metrics_Trait;
/**
* Context instance.
*
* @since 1.132.0
* @var Context
*/
protected $context;
/**
* Consent_Mode_Settings instance.
*
* @since 1.122.0
* @var Consent_Mode_Settings
*/
protected $consent_mode_settings;
/**
* REST_Consent_Mode_Controller instance.
*
* @since 1.122.0
* @var REST_Consent_Mode_Controller
*/
protected $rest_controller;
/**
* Constructor.
*
* @since 1.122.0
* @since 1.142.0 Introduced Modules instance as an argument.
*
* @param Context $context Plugin context.
* @param Modules $modules Modules instance.
* @param Options $options Optional. Option API instance. Default is a new instance.
*/
public function __construct(
Context $context,
Modules $modules,
?Options $options = null
) {
$this->context = $context;
$options = $options ?: new Options( $context );
$this->consent_mode_settings = new Consent_Mode_Settings( $options );
$this->rest_controller = new REST_Consent_Mode_Controller(
$modules,
$this->consent_mode_settings,
$options
);
}
/**
* Registers functionality through WordPress hooks.
*
* @since 1.122.0
*/
public function register() {
$this->consent_mode_settings->register();
$this->rest_controller->register();
$this->register_feature_metrics();
// Declare that the plugin is compatible with the WP Consent API.
$plugin = GOOGLESITEKIT_PLUGIN_BASENAME;
add_filter( "wp_consent_api_registered_{$plugin}", '__return_true' );
$consent_mode_enabled = $this->consent_mode_settings->is_consent_mode_enabled();
if ( $consent_mode_enabled ) {
// The `wp_head` action is used to ensure the snippets are printed in the head on the front-end only, not admin pages.
add_action(
'wp_head',
$this->get_method_proxy( 'render_gtag_consent_data_layer_snippet' ),
1 // Set priority to 1 to ensure the snippet is printed with top priority in the head.
);
add_action( 'wp_enqueue_scripts', fn () => $this->register_and_enqueue_script() );
}
add_filter(
'googlesitekit_consent_mode_status',
function () use ( $consent_mode_enabled ) {
return $consent_mode_enabled ? 'enabled' : 'disabled';
}
);
add_filter( 'googlesitekit_inline_base_data', $this->get_method_proxy( 'inline_js_base_data' ) );
add_action( 'wp_ajax_install_activate_wp_consent_api', array( $this, 'install_activate_wp_consent_api' ) );
}
/**
* AJAX callback that installs and activates the WP Consent API plugin.
*
* This function utilizes an AJAX approach instead of the standardized REST approach
* due to the requirement of the Plugin_Upgrader class, which relies on functions
* from `admin.php` among others. These functions are properly loaded during the
* AJAX callback, ensuring the installation and activation processes can execute correctly.
*
* @since 1.132.0
*/
public function install_activate_wp_consent_api() {
check_ajax_referer( 'updates' );
$slug = 'wp-consent-api';
$plugin = "$slug/$slug.php";
if ( ! current_user_can( 'activate_plugin', $plugin ) ) {
wp_send_json( array( 'error' => __( 'You do not have permission to activate plugins on this site.', 'google-site-kit' ) ) );
}
/** WordPress Administration Bootstrap */
require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; // For Plugin_Upgrader and Plugin_Installer_Skin.
require_once ABSPATH . 'wp-admin/includes/plugin-install.php'; // For plugins_api.
$api = plugins_api(
'plugin_information',
array(
'slug' => $slug,
'fields' => array(
'sections' => false,
),
)
);
if ( is_wp_error( $api ) ) {
wp_send_json( array( 'error' => $api->get_error_message() ) );
}
$title = '';
$nonce = 'install-plugin_' . $plugin;
$url = 'update.php?action=install-plugin&plugin=' . rawurlencode( $plugin );
$upgrader = new Plugin_Upgrader( new Plugin_Installer_Skin( compact( 'title', 'url', 'nonce', 'plugin', 'api' ) ) );
$install_plugin = $upgrader->install( $api->download_link );
if ( is_wp_error( $install_plugin ) ) {
wp_send_json( array( 'error' => $install_plugin->get_error_message() ) );
}
$activated = activate_plugin( $plugin );
if ( is_wp_error( $activated ) ) {
wp_send_json( array( 'error' => $activated->get_error_message() ) );
}
wp_send_json( array( 'success' => true ) );
}
/**
* Registers and Enqueues the consent mode script.
*
* @since 1.132.0
*/
protected function register_and_enqueue_script() {
$consent_mode_script = new Script(
'googlesitekit-consent-mode',
array(
'src' => $this->context->url( 'dist/assets/js/googlesitekit-consent-mode.js' ),
)
);
$consent_mode_script->register( $this->context );
$consent_mode_script->enqueue();
}
/**
* Prints the gtag consent snippet.
*
* @since 1.122.0
* @since 1.132.0 Refactored core script to external js file transpiled with webpack.
*/
protected function render_gtag_consent_data_layer_snippet() {
/**
* Filters the consent mode defaults.
*
* Allows these defaults to be modified, thus allowing users complete control over the consent mode parameters.
*
* @since 1.126.0
*
* @param array $consent_mode_defaults Default values for consent mode.
*/
$consent_defaults = apply_filters(
'googlesitekit_consent_defaults',
array(
'ad_personalization' => 'denied',
'ad_storage' => 'denied',
'ad_user_data' => 'denied',
'analytics_storage' => 'denied',
'functionality_storage' => 'denied',
'security_storage' => 'denied',
'personalization_storage' => 'denied',
// TODO: The value for `region` should be retrieved from $this->consent_mode_settings->get_regions(),
// but we'll need to migrate/clean up the incorrect values that were set from the initial release.
// See https://github.com/google/site-kit-wp/issues/8444.
'region' => Regions::get_regions(),
'wait_for_update' => 500, // Allow 500ms for Consent Management Platforms (CMPs) to update the consent status.
)
);
/**
* Filters the consent category mapping.
*
* @since 1.124.0
*
* @param array $consent_category_map Default consent category mapping.
*/
$consent_category_map = apply_filters(
'googlesitekit_consent_category_map',
array(
'statistics' => array( 'analytics_storage' ),
'marketing' => array( 'ad_storage', 'ad_user_data', 'ad_personalization' ),
'functional' => array( 'functionality_storage', 'security_storage' ),
'preferences' => array( 'personalization_storage' ),
)
);
// The core consent mode code is in assets/js/consent-mode/consent-mode.js.
// Only code that passes data from PHP to JS should be in this file.
printf( "<!-- %s -->\n", esc_html__( 'Google tag (gtag.js) consent mode dataLayer added by Site Kit', 'google-site-kit' ) );
BC_Functions::wp_print_inline_script_tag(
join(
"\n",
array(
'window.dataLayer = window.dataLayer || [];function gtag(){dataLayer.push(arguments);}',
sprintf( "gtag('consent', 'default', %s);", wp_json_encode( $consent_defaults ) ),
sprintf( 'window._googlesitekitConsentCategoryMap = %s;', wp_json_encode( $consent_category_map ) ),
sprintf( 'window._googlesitekitConsents = %s;', wp_json_encode( $consent_defaults ) ),
)
),
array( 'id' => 'google_gtagjs-js-consent-mode-data-layer' )
);
printf( "<!-- %s -->\n", esc_html__( 'End Google tag (gtag.js) consent mode dataLayer added by Site Kit', 'google-site-kit' ) );
}
/**
* Extends base data with a static list of consent mode regions.
*
* @since 1.128.0
*
* @param array $data Inline base data.
* @return array Filtered $data.
*/
protected function inline_js_base_data( $data ) {
$data['consentModeRegions'] = Regions::get_regions();
return $data;
}
/**
* Gets an array of internal feature metrics.
*
* @since 1.163.0
*
* @return array
*/
public function get_feature_metrics() {
$wp_consent_api_status = 'none';
if ( function_exists( 'wp_consent_api' ) ) {
$wp_consent_api_status = 'active';
} elseif ( $this->rest_controller->get_consent_api_plugin_file() ) {
$wp_consent_api_status = 'installed';
}
return array(
'consent_mode_enabled' => $this->consent_mode_settings->is_consent_mode_enabled(),
'wp_consent_api' => $wp_consent_api_status,
);
}
}

View File

@@ -0,0 +1,114 @@
<?php
/**
* Class Google\Site_Kit\Core\Consent_Mode\Consent_Mode_Settings
*
* @package Google\Site_Kit\Core\Consent_Mode
* @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\Consent_Mode;
use Google\Site_Kit\Core\Storage\Setting;
/**
* Class to store user consent mode settings.
*
* @since 1.122.0
* @access private
* @ignore
*/
class Consent_Mode_Settings extends Setting {
/**
* The user option name for this setting.
*/
const OPTION = 'googlesitekit_consent_mode';
/**
* Gets the expected value type.
*
* @since 1.122.0
*
* @return string The type name.
*/
protected function get_type() {
return 'object';
}
/**
* Gets the default value.
*
* @since 1.122.0
*
* @return array The default value.
*/
protected function get_default() {
return array(
'enabled' => false,
'regions' => Regions::get_regions(),
);
}
/**
* Gets the callback for sanitizing the setting's value before saving.
*
* @since 1.122.0
*
* @return callable Sanitize callback.
*/
protected function get_sanitize_callback() {
return function ( $value ) {
$new_value = $this->get();
if ( isset( $value['enabled'] ) ) {
$new_value['enabled'] = (bool) $value['enabled'];
}
if ( ! empty( $value['regions'] ) && is_array( $value['regions'] ) ) {
$region_codes = array_reduce(
$value['regions'],
static function ( $regions, $region_code ) {
$region_code = strtoupper( $region_code );
// Match ISO 3166-2 (`AB` or `CD-EF`).
if ( ! preg_match( '#^[A-Z]{2}(-[A-Z]{2})?$#', $region_code ) ) {
return $regions;
}
// Store as keys to remove duplicates.
$regions[ $region_code ] = true;
return $regions;
},
array()
);
$new_value['regions'] = array_keys( $region_codes );
}
return $new_value;
};
}
/**
* Accessor for the `enabled` setting.
*
* @since 1.122.0
*
* @return bool TRUE if consent mode is enabled, otherwise FALSE.
*/
public function is_consent_mode_enabled() {
return $this->get()['enabled'];
}
/**
* Accessor for the `regions` setting.
*
* @since 1.122.0
*
* @return array<string> Array of ISO 3166-2 region codes.
*/
public function get_regions() {
return $this->get()['regions'];
}
}

View File

@@ -0,0 +1,300 @@
<?php
/**
* Class Google\Site_Kit\Core\Consent_Mode\REST_Consent_Mode_Controller
*
* @package Google\Site_Kit\Core\Consent_Mode
* @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\Consent_Mode;
use Google\Site_Kit\Core\Modules\Modules;
use Google\Site_Kit\Core\Permissions\Permissions;
use Google\Site_Kit\Core\REST_API\REST_Route;
use Google\Site_Kit\Core\REST_API\REST_Routes;
use Google\Site_Kit\Core\Storage\Options;
use Google\Site_Kit\Core\Util\Plugin_Status;
use Google\Site_Kit\Modules\Ads;
use Google\Site_Kit\Modules\Analytics_4;
use Google\Site_Kit\Modules\Analytics_4\Settings as Analytics_Settings;
use Google\Site_Kit\Modules\Tag_Manager\Settings as Tag_Manager_Settings;
use Google\Site_Kit\Modules\Tag_Manager;
use WP_REST_Request;
use WP_REST_Response;
use WP_REST_Server;
use WP_Error;
/**
* Class for handling consent mode.
*
* @since 1.122.0
* @access private
* @ignore
*/
class REST_Consent_Mode_Controller {
/**
* Consent_Mode_Settings instance.
*
* @since 1.122.0
* @var Consent_Mode_Settings
*/
private $consent_mode_settings;
/**
* Modules instance.
*
* @since 1.142.0
* @var Modules
*/
protected $modules;
/**
* Options instance.
*
* @since 1.142.0
* @var Options
*/
protected $options;
/**
* Constructor.
*
* @since 1.122.0
* @since 1.142.0 Introduces Modules as an argument.
*
* @param Modules $modules Modules instance.
* @param Consent_Mode_Settings $consent_mode_settings Consent_Mode_Settings instance.
* @param Options $options Optional. Option API instance. Default is a new instance.
*/
public function __construct(
Modules $modules,
Consent_Mode_Settings $consent_mode_settings,
Options $options
) {
$this->modules = $modules;
$this->consent_mode_settings = $consent_mode_settings;
$this->options = $options;
}
/**
* Registers functionality through WordPress hooks.
*
* @since 1.122.0
*/
public function register() {
add_filter(
'googlesitekit_rest_routes',
function ( $routes ) {
return array_merge( $routes, $this->get_rest_routes() );
}
);
add_filter(
'googlesitekit_apifetch_preload_paths',
function ( $paths ) {
return array_merge(
$paths,
array(
'/' . REST_Routes::REST_ROOT . '/core/site/data/consent-mode',
)
);
}
);
add_filter(
'googlesitekit_apifetch_preload_paths',
function ( $paths ) {
return array_merge(
$paths,
array(
'/' . REST_Routes::REST_ROOT . '/core/site/data/consent-api-info',
)
);
}
);
}
/**
* Gets REST route instances.
*
* @since 1.122.0
*
* @return REST_Route[] List of REST_Route objects.
*/
protected function get_rest_routes() {
$can_manage_options = function () {
return current_user_can( Permissions::MANAGE_OPTIONS );
};
$can_update_plugins = function () {
return current_user_can( Permissions::UPDATE_PLUGINS );
};
return array(
new REST_Route(
'core/site/data/consent-mode',
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => function () {
return new WP_REST_Response( $this->consent_mode_settings->get() );
},
'permission_callback' => $can_manage_options,
),
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => function ( WP_REST_Request $request ) {
$this->consent_mode_settings->set(
$request['data']['settings']
);
return new WP_REST_Response( $this->consent_mode_settings->get() );
},
'permission_callback' => $can_manage_options,
'args' => array(
'data' => array(
'type' => 'object',
'required' => true,
'properties' => array(
'settings' => array(
'type' => 'object',
'required' => true,
'minProperties' => 1,
'additionalProperties' => false,
'properties' => array(
'enabled' => array(
'type' => 'boolean',
),
'regions' => array(
'type' => 'array',
'items' => array(
'type' => 'string',
),
),
),
),
),
),
),
),
)
),
new REST_Route(
'core/site/data/consent-api-info',
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => function () {
// Here we intentionally use a non-plugin-specific detection strategy.
$is_active = function_exists( 'wp_set_consent' );
$response = array(
'hasConsentAPI' => $is_active,
);
// Alternate wp_nonce_url without esc_html breaking query parameters.
$nonce_url = function ( $action_url, $action ) {
return add_query_arg( '_wpnonce', wp_create_nonce( $action ), $action_url );
};
if ( ! $is_active ) {
$installed_plugin = $this->get_consent_api_plugin_file();
$consent_plugin = array(
'installed' => (bool) $installed_plugin,
'installURL' => false,
'activateURL' => false,
);
if ( ! $installed_plugin && current_user_can( 'install_plugins' ) ) {
$consent_plugin['installURL'] = $nonce_url( self_admin_url( 'update.php?action=install-plugin&plugin=wp-consent-api' ), 'install-plugin_wp-consent-api' );
}
if ( $installed_plugin && current_user_can( 'activate_plugin', $installed_plugin ) ) {
$consent_plugin['activateURL'] = $nonce_url( self_admin_url( 'plugins.php?action=activate&plugin=' . $installed_plugin ), 'activate-plugin_' . $installed_plugin );
}
$response['wpConsentPlugin'] = $consent_plugin;
}
return new WP_REST_Response( $response );
},
'permission_callback' => $can_manage_options,
),
)
),
new REST_Route(
'core/site/data/consent-api-activate',
array(
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => function () {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
$slug = 'wp-consent-api';
$plugin = "$slug/$slug.php";
$activated = activate_plugin( $plugin );
if ( is_wp_error( $activated ) ) {
return new WP_Error( 'invalid_module_slug', $activated->get_error_message() );
}
return new WP_REST_Response( array( 'success' => true ) );
},
'permission_callback' => $can_update_plugins,
),
),
),
new REST_Route(
'core/site/data/ads-measurement-status',
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => function () {
$checks = apply_filters( 'googlesitekit_ads_measurement_connection_checks', array() );
if ( ! is_array( $checks ) ) {
return new WP_REST_Response( array( 'connected' => false ) );
}
foreach ( $checks as $check ) {
if ( ! is_callable( $check ) ) {
continue;
}
if ( $check() ) {
return new WP_REST_Response( array( 'connected' => true ) );
}
}
return new WP_REST_Response( array( 'connected' => false ) );
},
'permission_callback' => $can_manage_options,
),
),
),
);
}
/**
* Gets the plugin file of the installed WP Consent API if found.
*
* @since 1.148.0
*
* @return false|string
*/
public function get_consent_api_plugin_file() {
// Check the default location first.
if ( Plugin_Status::is_plugin_installed( 'wp-consent-api/wp-consent-api.php' ) ) {
return 'wp-consent-api/wp-consent-api.php';
}
// Here we make an extra effort to attempt to detect the plugin if installed in a non-standard location.
return Plugin_Status::is_plugin_installed(
fn ( $installed_plugin ) => 'https://wordpress.org/plugins/wp-consent-api' === $installed_plugin['PluginURI']
);
}
}

View File

@@ -0,0 +1,73 @@
<?php
/**
* Class Google\Site_Kit\Core\Consent_Mode\Regions
*
* @package Google\Site_Kit\Core\Consent_Mode
* @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\Consent_Mode;
use Google\Site_Kit\Core\Util\Feature_Flags;
/**
* Class containing consent mode Regions.
*
* @since 1.122.0
* @access private
* @ignore
*/
class Regions {
/**
* List of countries that Google's EU user consent policy applies to, which are the
* countries in the European Economic Area (EEA) plus the UK.
*/
const EU_USER_CONSENT_POLICY = array(
'AT',
'BE',
'BG',
'CH',
'CY',
'CZ',
'DE',
'DK',
'EE',
'ES',
'FI',
'FR',
'GB',
'GR',
'HR',
'HU',
'IE',
'IS',
'IT',
'LI',
'LT',
'LU',
'LV',
'MT',
'NL',
'NO',
'PL',
'PT',
'RO',
'SE',
'SI',
'SK',
);
/**
* Returns the list of regions that Google's EU user consent policy applies to.
*
* @since 1.128.0
*
* @return array<string> List of regions.
*/
public static function get_regions() {
return self::EU_USER_CONSENT_POLICY;
}
}

View File

@@ -0,0 +1,30 @@
<?php
/**
* Interface Google\Site_Kit\Core\Contracts\WP_Errorable.
*
* @package Google\Site_Kit\Core\Contracts
* @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\Core\Contracts;
use WP_Error;
/**
* Interface for a class which can be represented as a WP_Error.
*
* @since 1.9.0
*/
interface WP_Errorable {
/**
* Gets the WP_Error representation of this entity.
*
* @since 1.9.0
*
* @return WP_Error
*/
public function to_wp_error();
}

View File

@@ -0,0 +1,69 @@
<?php
/**
* Class Google\Site_Kit\Core\Conversion_Tracking\Conversion_Event_Providers\Contact_Form_7
*
* @package Google\Site_Kit\Core\Conversion_Tracking\Conversion_Event_Providers
* @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\Conversion_Tracking\Conversion_Event_Providers;
use Google\Site_Kit\Core\Assets\Script;
use Google\Site_Kit\Core\Conversion_Tracking\Conversion_Events_Provider;
/**
* Class for handling Contact Form 7 conversion events.
*
* @since 1.127.0
* @access private
* @ignore
*/
class Contact_Form_7 extends Conversion_Events_Provider {
const CONVERSION_EVENT_PROVIDER_SLUG = 'contact-form-7';
/**
* Checks if the Contact Form 7 plugin is active.
*
* @since 1.127.0
*
* @return bool True if Contact Form 7 is active, false otherwise.
*/
public function is_active() {
return defined( 'WPCF7_VERSION' );
}
/**
* Gets the conversion event names that are tracked by this provider.
*
* @since 1.127.0
*
* @return array List of event names.
*/
public function get_event_names() {
return array( 'contact' );
}
/**
* Registers the script for the provider.
*
* @since 1.127.0
*
* @return Script Script instance.
*/
public function register_script() {
$script = new Script(
'googlesitekit-events-provider-' . self::CONVERSION_EVENT_PROVIDER_SLUG,
array(
'src' => $this->context->url( 'dist/assets/js/googlesitekit-events-provider-contact-form-7.js' ),
'execution' => 'defer',
)
);
$script->register( $this->context );
return $script;
}
}

View File

@@ -0,0 +1,229 @@
<?php
/**
* Class Google\Site_Kit\Core\Conversion_Tracking\Conversion_Event_Providers\Easy_Digital_Downloads
*
* @package Google\Site_Kit\Core\Conversion_Tracking\Conversion_Event_Providers
* @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\Conversion_Tracking\Conversion_Event_Providers;
use Google\Site_Kit\Core\Assets\Script;
use Google\Site_Kit\Core\Conversion_Tracking\Conversion_Events_Provider;
use Google\Site_Kit\Core\Util\Feature_Flags;
use Google\Site_Kit\Core\Util\Method_Proxy_Trait;
use Google\Site_Kit\Core\Tags\Enhanced_Conversions\Enhanced_Conversions;
/**
* Class for handling Easy Digital Downloads conversion events.
*
* @since 1.130.0
* @access private
* @ignore
*/
class Easy_Digital_Downloads extends Conversion_Events_Provider {
use Method_Proxy_Trait;
const CONVERSION_EVENT_PROVIDER_SLUG = 'easy-digital-downloads';
/**
* Checks if the Easy Digital Downloads plugin is active.
*
* @since 1.130.0
*
* @return bool True if Easy Digital Downloads is active, false otherwise.
*/
public function is_active() {
return defined( 'EDD_VERSION' );
}
/**
* Gets the conversion event names that are tracked by this provider.
*
* @since 1.130.0
*
* @return array List of event names.
*/
public function get_event_names() {
$event_names = array( 'add_to_cart' );
if ( Feature_Flags::enabled( 'gtagUserData' ) ) {
$event_names[] = 'purchase';
}
return $event_names;
}
/**
* Gets the enhanced conversion event names that are tracked by this provider.
*
* @since 1.165.0
*
* @return array List of enhanced conversion event names.
*/
public function get_enhanced_event_names() {
return array( 'add_to_cart' );
}
/**
* Registers the script for the provider.
*
* @since 1.130.0
*
* @return Script Script instance.
*/
public function register_script() {
$script = new Script(
'googlesitekit-events-provider-' . self::CONVERSION_EVENT_PROVIDER_SLUG,
array(
'src' => $this->context->url( 'dist/assets/js/googlesitekit-events-provider-easy-digital-downloads.js' ),
'execution' => 'defer',
'dependencies' => array( 'edd-ajax' ),
)
);
$script->register( $this->context );
return $script;
}
/**
* Registers hooks for the Easy Digital Downloads provider.
*
* @since 1.164.0
*/
public function register_hooks() {
if ( Feature_Flags::enabled( 'gtagUserData' ) ) {
add_action(
'wp_footer',
$this->get_method_proxy( 'maybe_add_purchase_data_from_session' )
);
}
}
/**
* Prints the purchase data.
*
* @since 1.164.0
*/
protected function maybe_add_purchase_data_from_session() {
if ( ! function_exists( 'edd_get_purchase_session' ) || ! function_exists( 'edd_is_success_page' ) || ! edd_is_success_page() ) {
return;
}
$purchase_session = edd_get_purchase_session();
$purchase_data = $this->get_enhanced_conversions_data_from_session( $purchase_session );
wp_add_inline_script(
'googlesitekit-events-provider-' . self::CONVERSION_EVENT_PROVIDER_SLUG,
join(
"\n",
array(
'window._googlesitekit.edddata = window._googlesitekit.edddata || {};',
sprintf( 'window._googlesitekit.edddata.purchase = %s;', wp_json_encode( $purchase_data ) ),
)
),
'before'
);
}
/**
* Extracts Enhanced Conversions data from an EDD session.
*
* @since 1.164.0
*
* @param mixed|array|null $session_data An array containing EDD purchase session data.
*
* @return array
*/
protected function get_enhanced_conversions_data_from_session( $session_data ) {
if ( ! is_array( $session_data ) ) {
return array();
}
$user_data = $this->extract_user_data_from_session( $session_data );
if ( empty( $user_data ) ) {
return array();
}
return array(
'user_data' => $user_data,
);
}
/**
* Extracts user data from an EDD session.
*
* @since 1.164.0
*
* @param array $session_data An array containing EDD purchase session data.
*
* @return array
*/
protected function extract_user_data_from_session( $session_data ) {
$user_data = array();
$address_data = array();
if ( isset( $session_data['user_info'] ) ) {
$email = $session_data['user_info']['email'] ?? $session_data['user_email'] ?? '';
if ( ! empty( $email ) ) {
$user_data['email'] = Enhanced_Conversions::get_normalized_email( $email );
}
if ( ! empty( $session_data['user_info']['first_name'] ) ) {
$address_data['first_name'] = Enhanced_Conversions::get_normalized_value( $session_data['user_info']['first_name'] );
}
if ( ! empty( $session_data['user_info']['last_name'] ) ) {
$address_data['last_name'] = Enhanced_Conversions::get_normalized_value( $session_data['user_info']['last_name'] );
}
if ( isset( $session_data['user_info']['address'] ) ) {
if ( ! empty( $session_data['user_info']['address']['phone'] ) ) {
$user_data['phone_number'] = Enhanced_Conversions::get_normalized_value( $session_data['user_info']['address']['phone'] );
}
if ( ! empty( $session_data['user_info']['address']['line1'] ) ) {
$address_data['street'] = Enhanced_Conversions::get_normalized_value( $session_data['user_info']['address']['line1'] );
}
if ( ! empty( $session_data['user_info']['address']['city'] ) ) {
$address_data['city'] = Enhanced_Conversions::get_normalized_value( $session_data['user_info']['address']['city'] );
}
if ( ! empty( $session_data['user_info']['address']['state'] ) ) {
$region = $session_data['user_info']['address']['state'];
// Attempt to get full region name.
if ( function_exists( 'edd_get_state_name' ) && ! empty( $session_data['user_info']['address']['country'] ) ) {
$region = edd_get_state_name( $session_data['user_info']['address']['country'], $region );
}
$address_data['region'] = Enhanced_Conversions::get_normalized_value( $region );
}
if ( ! empty( $session_data['user_info']['address']['zip'] ) ) {
$address_data['postal_code'] = Enhanced_Conversions::get_normalized_value( $session_data['user_info']['address']['zip'] );
}
if ( ! empty( $session_data['user_info']['address']['country'] ) ) {
$address_data['country'] = $session_data['user_info']['address']['country'];
}
}
}
if ( ! empty( $address_data ) ) {
$user_data['address'] = $address_data;
}
return $user_data;
}
}

View File

@@ -0,0 +1,70 @@
<?php
/**
* Class Google\Site_Kit\Core\Conversion_Tracking\Conversion_Event_Providers\Mailchimp
*
* @package Google\Site_Kit\Core\Conversion_Tracking\Conversion_Event_Providers
* @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\Conversion_Tracking\Conversion_Event_Providers;
use Google\Site_Kit\Core\Assets\Script;
use Google\Site_Kit\Core\Conversion_Tracking\Conversion_Events_Provider;
/**
* Class for handling Mailchimp conversion events.
*
* @since 1.127.0
* @access private
* @ignore
*/
class Mailchimp extends Conversion_Events_Provider {
const CONVERSION_EVENT_PROVIDER_SLUG = 'mailchimp';
/**
* Checks if the Mailchimp plugin is active.
*
* @since 1.127.0
*
* @return bool True if Mailchimp is active, false otherwise.
*/
public function is_active() {
return defined( 'MC4WP_VERSION' );
}
/**
* Gets the conversion event names that are tracked by this provider.
*
* @since 1.127.0
*
* @return array List of event names.
*/
public function get_event_names() {
return array( 'submit_lead_form' );
}
/**
* Registers the script for the provider.
*
* @since 1.127.0
*
* @return Script Script instance.
*/
public function register_script() {
$script = new Script(
'googlesitekit-events-provider-' . self::CONVERSION_EVENT_PROVIDER_SLUG,
array(
'src' => $this->context->url( 'dist/assets/js/googlesitekit-events-provider-mailchimp.js' ),
'execution' => 'defer',
'dependencies' => array( 'mc4wp-forms-api' ),
)
);
$script->register( $this->context );
return $script;
}
}

View File

@@ -0,0 +1,70 @@
<?php
/**
* Class Google\Site_Kit\Core\Conversion_Tracking\Conversion_Event_Providers\Ninja_Forms
*
* @package Google\Site_Kit\Core\Conversion_Tracking\Conversion_Event_Providers
* @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\Conversion_Tracking\Conversion_Event_Providers;
use Google\Site_Kit\Core\Assets\Script;
use Google\Site_Kit\Core\Conversion_Tracking\Conversion_Events_Provider;
/**
* Class for handling Ninja Forms conversion events.
*
* @since 1.130.0
* @access private
* @ignore
*/
class Ninja_Forms extends Conversion_Events_Provider {
const CONVERSION_EVENT_PROVIDER_SLUG = 'ninja-forms';
/**
* Checks if the Ninja Forms plugin is active.
*
* @since 1.130.0
*
* @return bool True if Ninja Forms is active, false otherwise.
*/
public function is_active() {
return defined( 'NF_PLUGIN_URL' );
}
/**
* Gets the conversion event names that are tracked by this provider.
*
* @since 1.130.0
*
* @return array List of event names.
*/
public function get_event_names() {
return array( 'submit_lead_form' );
}
/**
* Registers the script for the provider.
*
* @since 1.130.0
*
* @return Script Script instance.
*/
public function register_script() {
$script = new Script(
'googlesitekit-events-provider-' . self::CONVERSION_EVENT_PROVIDER_SLUG,
array(
'src' => $this->context->url( 'dist/assets/js/googlesitekit-events-provider-ninja-forms.js' ),
'execution' => 'defer',
'dependencies' => array( 'backbone' ),
)
);
$script->register( $this->context );
return $script;
}
}

View File

@@ -0,0 +1,69 @@
<?php
/**
* Class Google\Site_Kit\Core\Conversion_Tracking\Conversion_Event_Providers\OptinMonster
*
* @package Google\Site_Kit\Core\Conversion_Tracking\Conversion_Event_Providers
* @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\Conversion_Tracking\Conversion_Event_Providers;
use Google\Site_Kit\Core\Assets\Script;
use Google\Site_Kit\Core\Conversion_Tracking\Conversion_Events_Provider;
/**
* Class for handling OptinMonster conversion events.
*
* @since 1.127.0
* @access private
* @ignore
*/
class OptinMonster extends Conversion_Events_Provider {
const CONVERSION_EVENT_PROVIDER_SLUG = 'optin-monster';
/**
* Checks if the OptinMonster plugin is active.
*
* @since 1.127.0
*
* @return bool True if OptinMonster is active, false otherwise.
*/
public function is_active() {
return defined( 'OMAPI_FILE' );
}
/**
* Gets the conversion event names that are tracked by this provider.
*
* @since 1.127.0
*
* @return array List of event names.
*/
public function get_event_names() {
return array( 'submit_lead_form' );
}
/**
* Registers the script for the provider.
*
* @since 1.127.0
*
* @return Script Script instance.
*/
public function register_script() {
$script = new Script(
'googlesitekit-events-provider-' . self::CONVERSION_EVENT_PROVIDER_SLUG,
array(
'src' => $this->context->url( 'dist/assets/js/googlesitekit-events-provider-optin-monster.js' ),
'execution' => 'defer',
)
);
$script->register( $this->context );
return $script;
}
}

View File

@@ -0,0 +1,70 @@
<?php
/**
* Class Google\Site_Kit\Core\Conversion_Tracking\Conversion_Event_Providers\PopupMaker
*
* @package Google\Site_Kit\Core\Conversion_Tracking\Conversion_Event_Providers
* @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\Conversion_Tracking\Conversion_Event_Providers;
use Google\Site_Kit\Core\Assets\Script;
use Google\Site_Kit\Core\Conversion_Tracking\Conversion_Events_Provider;
/**
* Class for handling PopupMaker conversion events.
*
* @since 1.127.0
* @access private
* @ignore
*/
class PopupMaker extends Conversion_Events_Provider {
const CONVERSION_EVENT_PROVIDER_SLUG = 'popup-maker';
/**
* Checks if the PopupMaker plugin is active.
*
* @since 1.127.0
*
* @return bool True if PopupMaker is active, false otherwise.
*/
public function is_active() {
return defined( 'POPMAKE_VERSION' );
}
/**
* Gets the conversion event names that are tracked by this provider.
*
* @since 1.127.0
*
* @return array List of event names.
*/
public function get_event_names() {
return array( 'submit_lead_form' );
}
/**
* Registers the script for the provider.
*
* @since 1.127.0
*
* @return Script Script instance.
*/
public function register_script() {
$script = new Script(
'googlesitekit-events-provider-' . self::CONVERSION_EVENT_PROVIDER_SLUG,
array(
'src' => $this->context->url( 'dist/assets/js/googlesitekit-events-provider-popup-maker.js' ),
'dependencies' => array( 'popup-maker-site' ),
'execution' => 'defer',
)
);
$script->register( $this->context );
return $script;
}
}

View File

@@ -0,0 +1,69 @@
<?php
/**
* Class Google\Site_Kit\Core\Conversion_Tracking\Conversion_Event_Providers\WPForms
*
* @package Google\Site_Kit\Core\Conversion_Tracking\Conversion_Event_Providers
* @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\Conversion_Tracking\Conversion_Event_Providers;
use Google\Site_Kit\Core\Assets\Script;
use Google\Site_Kit\Core\Conversion_Tracking\Conversion_Events_Provider;
/**
* Class for handling WPForms conversion events.
*
* @since 1.127.0
* @access private
* @ignore
*/
class WPForms extends Conversion_Events_Provider {
const CONVERSION_EVENT_PROVIDER_SLUG = 'wpforms';
/**
* Checks if the WPForms plugin is active.
*
* @since 1.127.0
*
* @return bool True if WPForms is active, false otherwise.
*/
public function is_active() {
return defined( 'WPFORMS_VERSION' );
}
/**
* Gets the conversion event names that are tracked by this provider.
*
* @since 1.127.0
*
* @return array List of event names.
*/
public function get_event_names() {
return array( 'submit_lead_form' );
}
/**
* Registers the script for the provider.
*
* @since 1.127.0
*
* @return Script Script instance.
*/
public function register_script() {
$script = new Script(
'googlesitekit-events-provider-' . self::CONVERSION_EVENT_PROVIDER_SLUG,
array(
'src' => $this->context->url( 'dist/assets/js/googlesitekit-events-provider-wpforms.js' ),
'execution' => 'defer',
)
);
$script->register( $this->context );
return $script;
}
}

View File

@@ -0,0 +1,564 @@
<?php
/**
* Class Google\Site_Kit\Core\Conversion_Tracking\Conversion_Event_Providers\WooCommerce
*
* @package Google\Site_Kit\Core\Conversion_Tracking\Conversion_Event_Providers
* @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\Conversion_Tracking\Conversion_Event_Providers;
use Google\Site_Kit\Core\Assets\Script;
use Google\Site_Kit\Core\Conversion_Tracking\Conversion_Events_Provider;
use Google\Site_Kit\Core\Util\Feature_Flags;
use Google\Site_Kit\Core\Tags\Enhanced_Conversions\Enhanced_Conversions;
/**
* Class for handling WooCommerce conversion events.
*
* @since 1.127.0
* @access private
* @ignore
*/
class WooCommerce extends Conversion_Events_Provider {
const CONVERSION_EVENT_PROVIDER_SLUG = 'woocommerce';
/**
* Avaialble products on the page.
*
* @var Array
*
* @since 1.153.0
*/
protected $products = array();
/**
* Current product added to the cart.
*
* @since 1.153.0
* @var WC_Product
*/
protected $add_to_cart;
/**
* Checks if the WooCommerce plugin is active.
*
* @since 1.127.0
*
* @return bool True if WooCommerce is active, false otherwise.
*/
public function is_active() {
return class_exists( 'WooCommerce' );
}
/**
* Gets the conversion event names that are tracked by this provider, and Google Analytics for WooCommerce addon when active.
*
* @since 1.127.0
*
* @return array List of event names.
*/
public function get_event_names() {
$wgai_event_names = $this->get_wgai_event_names();
$events_to_track = $this->events_to_track();
return array_unique( array_merge( $events_to_track, $wgai_event_names ) );
}
/**
* Gets the enhanced conversion event names that are tracked by this provider.
*
* @since 1.165.0
*
* @return array List of enhanced conversion event names.
*/
public function get_enhanced_event_names() {
return array( 'add_to_cart', 'purchase' );
}
/**
* Gets the conversion event names that are tracked by Google Analytics for WooCommerce provider.
*
* @since 1.154.0
*
* @return array List of event names.
*/
protected function get_wgai_event_names() {
if ( ! class_exists( 'WC_Google_Analytics_Integration' ) ) {
return array();
}
$settings = get_option( 'woocommerce_google_analytics_settings' );
$event_names = array();
// If only product identifier is availabe in the saved settings, it means default options are used.
// And by default all events are tracked.
if ( isset( $settings['ga_product_identifier'] ) && count( $settings ) === 1 ) {
return array(
'purchase',
'add_to_cart',
'remove_from_cart',
'view_item_list',
'select_content',
'view_item',
'begin_checkout',
);
}
$event_mapping = array(
'ga_ecommerce_tracking_enabled' => 'purchase',
'ga_event_tracking_enabled' => 'add_to_cart',
'ga_enhanced_remove_from_cart_enabled' => 'remove_from_cart',
'ga_enhanced_product_impression_enabled' => 'view_item_list',
'ga_enhanced_product_click_enabled' => 'select_content',
'ga_enhanced_product_detail_view_enabled' => 'view_item',
'ga_enhanced_checkout_process_enabled' => 'begin_checkout',
);
$event_names = array();
foreach ( $event_mapping as $setting_key => $event_name ) {
if ( isset( $settings[ $setting_key ] ) && 'yes' === $settings[ $setting_key ] ) {
$event_names[] = $event_name;
}
}
return $event_names;
}
/**
* Gets the conversion event names that should be tracked by this provider.
*
* @since 1.154.0
*
* @return array List of event names.
*/
protected function events_to_track() {
$event_names = array( 'add_to_cart', 'purchase' );
$wgai_event_name = $this->get_wgai_event_names();
if ( ! empty( $wgai_event_name ) ) {
return array_values( array_diff( $event_names, $wgai_event_name ) );
}
return $event_names;
}
/**
* Gets the conversion event names that are tracked by this provider.
*
* @since 1.154.0
*
* @return string Comma separated list of event names.
*/
public function get_debug_data() {
if ( empty( $this->events_to_track() ) ) {
return esc_html__( 'Events tracked through Analytics integration addon', 'google-site-kit' );
}
return parent::get_debug_data();
}
/**
* Registers the script for the provider.
*
* @since 1.127.0
*
* @return Script Script instance.
*/
public function register_script() {
$script = new Script(
'googlesitekit-events-provider-' . self::CONVERSION_EVENT_PROVIDER_SLUG,
array(
'src' => $this->context->url( 'dist/assets/js/googlesitekit-events-provider-woocommerce.js' ),
'execution' => 'defer',
'dependencies' => array( 'woocommerce' ),
)
);
$script->register( $this->context );
return $script;
}
/**
* Adds a hook for a purchase event.
*
* @since 1.129.0
*/
public function register_hooks() {
add_filter(
'woocommerce_loop_add_to_cart_link',
function ( $button, $product ) {
$this->products[] = $this->get_formatted_product( $product );
return $button;
},
10,
2
);
add_action(
'woocommerce_add_to_cart',
function ( $cart_item_key, $product_id, $quantity, $variation_id, $variation ) {
$this->add_to_cart = $this->get_formatted_product(
wc_get_product( $product_id ),
$variation_id,
$variation,
$quantity
);
},
10,
5
);
add_action(
'woocommerce_thankyou',
fn( $order_id ) => $this->maybe_add_purchase_inline_script( $order_id, wp_is_block_theme() ),
10,
1
);
add_action(
'wp_footer',
function () {
$script_slug = 'googlesitekit-events-provider-' . self::CONVERSION_EVENT_PROVIDER_SLUG;
$events_to_track = $this->events_to_track();
$inline_script = join(
"\n",
array(
'window._googlesitekit.wcdata = window._googlesitekit.wcdata || {};',
sprintf( 'window._googlesitekit.wcdata.products = %s;', wp_json_encode( $this->products ) ),
sprintf( 'window._googlesitekit.wcdata.add_to_cart = %s;', wp_json_encode( $this->add_to_cart ) ),
sprintf( 'window._googlesitekit.wcdata.currency = "%s";', esc_js( get_woocommerce_currency() ) ),
sprintf( 'window._googlesitekit.wcdata.eventsToTrack = %s;', wp_json_encode( $events_to_track ) ),
)
);
if ( is_wc_endpoint_url( 'order-received' ) && wp_is_block_theme() ) {
$order_id = absint( get_query_var( 'order-received' ) );
$this->maybe_add_purchase_inline_script( $order_id );
}
wp_add_inline_script( $script_slug, $inline_script, 'before' );
}
);
}
/**
* Returns an array of product data in the required format.
* Adapted from https://github.com/woocommerce/woocommerce-google-analytics-integration
*
* @since 1.153.0
*
* @param WC_Product $product The product to format.
* @param int $variation_id Variation product ID.
* @param array|bool $variation An array containing product variation attributes to include in the product data.
* For the "variation" type products, we'll use product->get_attributes.
* @param bool|int $quantity Quantity to include in the formatted product object.
*
* @return array
*/
public function get_formatted_product( $product, $variation_id = 0, $variation = false, $quantity = false ) {
$product_id = $product->is_type( 'variation' ) ? $product->get_parent_id() : $product->get_id();
$price = $product->get_price();
// Get product price from chosen variation if set.
if ( $variation_id ) {
$variation_product = wc_get_product( $variation_id );
if ( $variation_product ) {
$price = $variation_product->get_price();
}
}
// Integration with Product Bundles.
// Get the minimum price, as `get_price` may return 0 if the product is a bundle and the price is potentially a range.
// Even a range containing a single value.
if ( $product->is_type( 'bundle' ) && is_callable( array( $product, 'get_bundle_price' ) ) ) {
$price = $product->get_bundle_price( 'min' );
}
$formatted = array(
'id' => $product_id,
'name' => $product->get_title(),
'categories' => array_map(
fn( $category ) => array( 'name' => $category->name ),
wc_get_product_terms( $product_id, 'product_cat', array( 'number' => 5 ) )
),
'price' => $this->get_formatted_price( $price ),
);
if ( $quantity ) {
$formatted['quantity'] = (int) $quantity;
}
if ( $product->is_type( 'variation' ) ) {
$variation = $product->get_attributes();
}
if ( is_array( $variation ) ) {
$formatted['variation'] = implode(
', ',
array_map(
function ( $attribute, $value ) {
return sprintf(
'%s: %s',
str_replace( 'attribute_', '', $attribute ),
$value
);
},
array_keys( $variation ),
array_values( $variation )
)
);
}
return $formatted;
}
/**
* Returns an array of order data in the required format.
* Adapted from https://github.com/woocommerce/woocommerce-google-analytics-integration
*
* @since 1.153.0
*
* @param WC_Abstract_Order $order An instance of the WooCommerce Order object.
*
* @return array
*/
public function get_formatted_order( $order ) {
$order_data = array(
'id' => $order->get_id(),
'affiliation' => get_bloginfo( 'name' ),
'totals' => array(
'currency_code' => $order->get_currency(),
'tax_total' => $this->get_formatted_price( $order->get_total_tax() ),
'shipping_total' => $this->get_formatted_price( $order->get_total_shipping() ),
'total_price' => $this->get_formatted_price( $order->get_total() ),
),
'items' => array_map(
function ( $item ) {
return array_merge(
$this->get_formatted_product( $item->get_product() ),
array(
'quantity' => $item->get_quantity(),
'price_after_coupon_discount' => $this->get_formatted_price( $item->get_total() ),
)
);
},
array_values( $order->get_items() ),
),
);
if ( Feature_Flags::enabled( 'gtagUserData' ) ) {
$user_data = $this->extract_user_data_from_order( $order );
if ( ! empty( $user_data ) ) {
$order_data['user_data'] = $user_data;
}
}
return $order_data;
}
/**
* Extracts and normalizes user data from WooCommerce order for Enhanced Conversions.
*
* @since 1.161.0
*
* @param WC_Abstract_Order $order An instance of the WooCommerce Order object.
*
* @return array Normalized user data or empty array if no supported fields are available.
*/
protected function extract_user_data_from_order( $order ) {
$user_data = array();
// Extract billing information from the order.
$billing_email = $order->get_billing_email();
$billing_phone = $order->get_billing_phone();
$billing_first_name = $order->get_billing_first_name();
$billing_last_name = $order->get_billing_last_name();
$billing_address_1 = $order->get_billing_address_1();
$billing_city = $order->get_billing_city();
$billing_state = $order->get_billing_state();
$billing_postcode = $order->get_billing_postcode();
$billing_country = $order->get_billing_country();
// Normalize and add email if available.
if ( ! empty( $billing_email ) ) {
$user_data['email'] = Enhanced_Conversions::get_normalized_email( $billing_email );
}
// Normalize and add phone number if available.
if ( ! empty( $billing_phone ) ) {
$normalized_phone = $this->get_normalized_phone( $billing_phone, $billing_country );
if ( ! empty( $normalized_phone ) ) {
$user_data['phone_number'] = $normalized_phone;
}
}
// Build address object if any address fields are available.
$address_data = array();
if ( ! empty( $billing_first_name ) ) {
$address_data['first_name'] = Enhanced_Conversions::get_normalized_value( $billing_first_name );
}
if ( ! empty( $billing_last_name ) ) {
$address_data['last_name'] = Enhanced_Conversions::get_normalized_value( $billing_last_name );
}
if ( ! empty( $billing_address_1 ) ) {
$address_data['street'] = Enhanced_Conversions::get_normalized_value( $billing_address_1 );
}
if ( ! empty( $billing_city ) ) {
$address_data['city'] = Enhanced_Conversions::get_normalized_value( $billing_city );
}
if ( ! empty( $billing_state ) ) {
$address_data['region'] = Enhanced_Conversions::get_normalized_value( $billing_state );
}
if ( ! empty( $billing_postcode ) ) {
$address_data['postal_code'] = Enhanced_Conversions::get_normalized_value( $billing_postcode );
}
if ( ! empty( $billing_country ) ) {
$address_data['country'] = Enhanced_Conversions::get_normalized_value( $billing_country );
}
// Only include address if it has at least one field.
if ( ! empty( $address_data ) ) {
$user_data['address'] = $address_data;
}
return $user_data;
}
/**
* Gets a normalized phone number for Enhanced Conversions.
*
* @since 1.161.0
*
* @param string $phone The phone number to normalize.
* @param string $country The country code (2-letter ISO 3166-1 alpha-2).
* @return string Normalized phone number (E.164 format if possible, basic normalization otherwise).
*/
protected function get_normalized_phone( $phone, $country ) {
if ( empty( $phone ) ) {
return '';
}
// Try to use WooCommerce's country calling codes for proper E.164 formatting.
if ( class_exists( 'WC_Countries' ) && ! empty( $country ) ) {
$countries = new \WC_Countries();
$calling_code = $countries->get_country_calling_code( $country );
// If we have a valid calling code, format to E.164.
if ( ! empty( $calling_code ) ) {
// Remove any non-digit characters and leading zeros.
$phone = ltrim( preg_replace( '/[^0-9]/', '', $phone ), '0' );
// Skip if phone is empty after cleaning.
if ( empty( $phone ) ) {
return '';
}
// Prepend the calling code (which already includes the + sign).
$phone = $calling_code . $phone;
// Validate the number is the correct length (11-15 digits including +).
if ( strlen( $phone ) < 11 || strlen( $phone ) > 15 ) {
return '';
}
return $phone;
}
}
// Fallback: use Enhanced_Conversions basic normalization if WooCommerce is unavailable
// or country calling code cannot be determined.
return Enhanced_Conversions::get_normalized_value( $phone );
}
/**
* Formats a price the same way WooCommerce Blocks does.
* Taken from https://github.com/woocommerce/woocommerce-google-analytics-integration
*
* @since 1.153.0
*
* @param mixed $value The price value for format.
*
* @return int
*/
public function get_formatted_price( $value ) {
return intval(
round(
( (float) wc_format_decimal( $value ) ) * ( 10 ** absint( wc_get_price_decimals() ) ),
0
)
);
}
/**
* Prints the purchase event details.
*
* @since 1.154.0
*
* @param int $order_id The order ID.
* @param bool $skip_meta_value_save Whether to skip saving the _googlesitekit_ga_purchase_event_tracked meta value.
*/
protected function maybe_add_purchase_inline_script( $order_id, $skip_meta_value_save = false ) {
$wgai_event_names = $this->get_wgai_event_names();
// If purchase event is tracked by the Google Analytics for WooCommerce addon,
// don't output the script tag to track the purchase event.
if ( in_array( 'purchase', $wgai_event_names, true ) ) {
return;
}
$input = $this->context->input();
$order = wc_get_order( $order_id );
// If there isn't a valid order for this ID, or if this order
// already has a purchase event tracked for it, return early
// and don't output the script tag to track the purchase event.
if ( ! $order || $order->get_meta( '_googlesitekit_ga_purchase_event_tracked' ) === '1' ) {
return;
}
// Ensure the order key in the query param is valid for this
// order.
$order_key = $input->filter( INPUT_GET, 'key' );
// Don't output the script tag if the order key is invalid.
if ( ! $order->key_is_valid( (string) $order_key ) ) {
return;
}
// In case we are on block theme, this hook running on thank you page will not attach the script.
// So we need to skip it, and apply this on the later hook.
if ( ! $skip_meta_value_save ) {
// Mark the order as tracked by Site Kit.
$order->update_meta_data( '_googlesitekit_ga_purchase_event_tracked', 1 );
$order->save();
}
wp_add_inline_script(
'googlesitekit-events-provider-' . self::CONVERSION_EVENT_PROVIDER_SLUG,
join(
"\n",
array(
'window._googlesitekit.wcdata = window._googlesitekit.wcdata || {};',
sprintf( 'window._googlesitekit.wcdata.purchase = %s;', wp_json_encode( $this->get_formatted_order( $order ) ) ),
)
),
'before'
);
}
}

View File

@@ -0,0 +1,105 @@
<?php
/**
* Class Google\Site_Kit\Core\Conversion_Tracking\Conversion_Events_Provider
*
* @package Google\Site_Kit\Core\Conversion_Tracking
* @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\Conversion_Tracking;
use Google\Site_Kit\Context;
use Google\Site_Kit\Core\Assets\Script;
/**
* Base class for conversion events provider.
*
* @since 1.125.0
* @since 1.126.0 Changed from interface to abstract class.
* @access private
* @ignore
*/
abstract class Conversion_Events_Provider {
/**
* Plugin context.
*
* @since 1.126.0
* @var Context
*/
protected $context;
/**
* Constructor.
*
* @since 1.126.0
*
* @param Context $context Plugin context.
*/
public function __construct( Context $context ) {
$this->context = $context;
}
/**
* Checks if the provider is active.
*
* @since 1.125.0
*
* @return bool True if the provider is active, false otherwise.
*/
public function is_active() {
return false;
}
/**
* Gets the conversion event names that are tracked by this provider.
*
* @since 1.154.0
*
* @return string Comma separated list of event names.
*/
public function get_debug_data() {
return implode( ', ', $this->get_event_names() );
}
/**
* Gets the event names.
*
* @since 1.125.0
*
* @return array List of event names.
*/
abstract public function get_event_names();
/**
* Gets the enhanced conversion event names.
*
* @since 1.165.0
*
* @return array List of enhanced conversion event names. Default empty array.
*/
public function get_enhanced_event_names() {
return array();
}
/**
* Registers any actions/hooks for this provider.
*
* @since 1.129.0
*/
public function register_hooks() {
// No-op by default, but left here so subclasses can implement
// their own `add_action`/hook calls.
}
/**
* Registers the script for the provider.
*
* @since 1.125.0
*
* @return Script|null Script instance, or null if no script is registered.
*/
abstract public function register_script();
}

View File

@@ -0,0 +1,288 @@
<?php
/**
* Class Google\Site_Kit\Core\Conversion_Tracking
*
* @package Google\Site_Kit\Core\Modules
* @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\Conversion_Tracking;
use Google\Site_Kit\Context;
use Google\Site_Kit\Core\Assets\Script;
use Google\Site_Kit\Core\Conversion_Tracking\Conversion_Event_Providers\Contact_Form_7;
use Google\Site_Kit\Core\Conversion_Tracking\Conversion_Event_Providers\Easy_Digital_Downloads;
use Google\Site_Kit\Core\Conversion_Tracking\Conversion_Event_Providers\Mailchimp;
use Google\Site_Kit\Core\Conversion_Tracking\Conversion_Event_Providers\Ninja_Forms;
use Google\Site_Kit\Core\Conversion_Tracking\Conversion_Event_Providers\OptinMonster;
use Google\Site_Kit\Core\Conversion_Tracking\Conversion_Event_Providers\PopupMaker;
use Google\Site_Kit\Core\Conversion_Tracking\Conversion_Event_Providers\WooCommerce;
use Google\Site_Kit\Core\Conversion_Tracking\Conversion_Event_Providers\WPForms;
use Google\Site_Kit\Core\Storage\Options;
use Google\Site_Kit\Core\Tags\GTag;
use Google\Site_Kit\Core\Tracking\Feature_Metrics_Trait;
use Google\Site_Kit\Core\Tracking\Provides_Feature_Metrics;
use Google\Site_Kit\Core\Util\Feature_Flags;
use LogicException;
/**
* Class for managing conversion tracking.
*
* @since 1.126.0
* @access private
* @ignore
*/
class Conversion_Tracking implements Provides_Feature_Metrics {
use Feature_Metrics_Trait;
/**
* Context object.
*
* @var Context
*/
private $context;
/**
* Conversion_Tracking_Settings instance.
*
* @since 1.127.0
* @var Conversion_Tracking_Settings
*/
protected $conversion_tracking_settings;
/**
* REST_Conversion_Tracking_Controller instance.
*
* @since 1.127.0
* @var REST_Conversion_Tracking_Controller
*/
protected $rest_conversion_tracking_controller;
/**
* Supported conversion event providers.
*
* @since 1.126.0
* @since 1.130.0 Added Ninja Forms class.
* @var array
*/
public static $providers = array(
Contact_Form_7::CONVERSION_EVENT_PROVIDER_SLUG => Contact_Form_7::class,
Easy_Digital_Downloads::CONVERSION_EVENT_PROVIDER_SLUG => Easy_Digital_Downloads::class,
Mailchimp::CONVERSION_EVENT_PROVIDER_SLUG => Mailchimp::class,
Ninja_Forms::CONVERSION_EVENT_PROVIDER_SLUG => Ninja_Forms::class,
OptinMonster::CONVERSION_EVENT_PROVIDER_SLUG => OptinMonster::class,
PopupMaker::CONVERSION_EVENT_PROVIDER_SLUG => PopupMaker::class,
WooCommerce::CONVERSION_EVENT_PROVIDER_SLUG => WooCommerce::class,
WPForms::CONVERSION_EVENT_PROVIDER_SLUG => WPForms::class,
);
/**
* Constructor.
*
* @since 1.126.0
*
* @param Context $context Plugin context.
* @param Options $options Optional. Option API instance. Default is a new instance.
*/
public function __construct( Context $context, ?Options $options = null ) {
$this->context = $context;
$options = $options ?: new Options( $context );
$this->conversion_tracking_settings = new Conversion_Tracking_Settings( $options );
$this->rest_conversion_tracking_controller = new REST_Conversion_Tracking_Controller( $this->conversion_tracking_settings );
}
/**
* Registers the class functionality.
*
* @since 1.126.0
*/
public function register() {
$this->conversion_tracking_settings->register();
$this->rest_conversion_tracking_controller->register();
$this->register_feature_metrics();
add_action( 'wp_enqueue_scripts', fn () => $this->maybe_enqueue_scripts(), 30 );
$active_providers = $this->get_active_providers();
array_walk(
$active_providers,
function ( Conversion_Events_Provider $active_provider ) {
$active_provider->register_hooks();
}
);
}
/**
* Enqueues conversion tracking scripts if conditions are satisfied.
*/
protected function maybe_enqueue_scripts() {
if (
// Do nothing if neither Ads nor Analytics *web* snippet has been inserted.
! ( did_action( 'googlesitekit_ads_init_tag' ) || did_action( 'googlesitekit_analytics-4_init_tag' ) )
|| ! $this->conversion_tracking_settings->is_conversion_tracking_enabled()
) {
return;
}
$active_providers = $this->get_active_providers();
array_walk(
$active_providers,
function ( Conversion_Events_Provider $active_provider ) {
$script_asset = $active_provider->register_script();
if ( $script_asset instanceof Script ) {
$script_asset->enqueue();
}
}
);
$gtag_event = '
window._googlesitekit = window._googlesitekit || {};
window._googlesitekit.throttledEvents = [];
window._googlesitekit.gtagEvent = (name, data) => {
var key = JSON.stringify( { name, data } );
if ( !! window._googlesitekit.throttledEvents[ key ] ) {
return;
}
window._googlesitekit.throttledEvents[ key ] = true;
setTimeout( () => {
delete window._googlesitekit.throttledEvents[ key ];
}, 5 );
gtag( "event", name, { ...data, event_source: "site-kit" } );
};
';
if ( function_exists( 'edd_get_currency' ) ) {
$gtag_event .= "window._googlesitekit.easyDigitalDownloadsCurrency = '" . edd_get_currency() . "';";
}
if ( Feature_Flags::enabled( 'gtagUserData' ) ) {
$gtag_event .= 'window._googlesitekit.gtagUserData = true;';
}
wp_add_inline_script( GTag::HANDLE, preg_replace( '/\s+/', ' ', $gtag_event ) );
}
/**
* Gets the instances of active conversion event providers.
*
* @since 1.126.0
*
* @return array List of active Conversion_Events_Provider instances.
* @throws LogicException Thrown if an invalid conversion event provider class name is provided.
*/
public function get_active_providers() {
$active_providers = array();
foreach ( self::$providers as $provider_slug => $provider_class ) {
if ( ! is_string( $provider_class ) || ! $provider_class ) {
throw new LogicException(
sprintf(
/* translators: %s: provider slug */
__( 'A conversion event provider class name is required to instantiate a provider: %s', 'google-site-kit' ),
$provider_slug
)
);
}
if ( ! class_exists( $provider_class ) ) {
throw new LogicException(
sprintf(
/* translators: %s: provider classname */
__( "The '%s' class does not exist", 'google-site-kit' ),
$provider_class
)
);
}
if ( ! is_subclass_of( $provider_class, Conversion_Events_Provider::class ) ) {
throw new LogicException(
sprintf(
/* translators: 1: provider classname 2: Conversion_Events_Provider classname */
__( "The '%1\$s' class must extend the base conversion event provider class: %2\$s", 'google-site-kit' ),
$provider_class,
Conversion_Events_Provider::class
)
);
}
$instance = new $provider_class( $this->context );
if ( $instance->is_active() ) {
$active_providers[ $provider_slug ] = $instance;
}
}
return $active_providers;
}
/**
* Returns events supported by active providers from the conversion tracking infrastructure.
*
* @since 1.163.0 Moved this method here from the Ads class.
*
* @return array Array of supported conversion events, or empty array.
*/
public function get_supported_conversion_events() {
$providers = $this->get_active_providers();
if ( empty( $providers ) ) {
return array();
}
$events = array();
foreach ( $providers as $provider ) {
$events = array_merge( $events, array_values( $provider->get_event_names() ) );
}
return array_unique( $events );
}
/**
* Returns enhanced conversion events supported by active providers from the conversion tracking infrastructure.
*
* @since 1.165.0
*
* @return array Array of supported enhanced conversion events, or empty array.
*/
public function get_enhanced_conversion_events() {
$providers = $this->get_active_providers();
if ( empty( $providers ) ) {
return array();
}
$events = array();
foreach ( $providers as $provider ) {
$supported_enhanced_events = array_intersect( $provider->get_enhanced_event_names(), $provider->get_event_names() );
$events = array_merge( $events, array_values( $supported_enhanced_events ) );
}
return array_unique( $events );
}
/**
* Gets an array of internal feature metrics.
*
* @since 1.163.0
*
* @return array
*/
public function get_feature_metrics() {
return array(
'conversion_tracking_enabled' => $this->conversion_tracking_settings->is_conversion_tracking_enabled(),
'conversion_tracking_providers' => array_keys( $this->get_active_providers() ),
'conversion_tracking_events' => $this->get_supported_conversion_events(),
'conversion_tracking_events_enh' => $this->get_enhanced_conversion_events(),
);
}
}

View File

@@ -0,0 +1,83 @@
<?php
/**
* Class Google\Site_Kit\Core\Conversion_Tracking\Conversion_Tracking_Settings
*
* @package Google\Site_Kit\Core\Conversion_Tracking
* @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\Conversion_Tracking;
use Google\Site_Kit\Core\Storage\Setting;
/**
* Class to store conversion tracking settings.
*
* @since 1.127.0
* @access private
* @ignore
*/
class Conversion_Tracking_Settings extends Setting {
/**
* The option name for this setting.
*/
const OPTION = 'googlesitekit_conversion_tracking';
/**
* Gets the expected value type.
*
* @since 1.127.0
*
* @return string The expected type of the setting option.
*/
protected function get_type() {
return 'object';
}
/**
* Gets the default value.
*
* @since 1.127.0
*
* @return array The default value.
*/
protected function get_default() {
return array(
'enabled' => false,
);
}
/**
* Gets the callback for sanitizing the setting's value before saving.
*
* @since 1.127.0
*
* @return callable Sanitize callback.
*/
protected function get_sanitize_callback() {
return function ( $value ) {
$new_value = $this->get();
if ( isset( $value['enabled'] ) ) {
$new_value['enabled'] = (bool) $value['enabled'];
}
return $new_value;
};
}
/**
* Accessor for the `enabled` setting.
*
* @since 1.127.0
*
* @return bool TRUE if conversion tracking is enabled, otherwise FALSE.
*/
public function is_conversion_tracking_enabled() {
return $this->get()['enabled'];
}
}

View File

@@ -0,0 +1,131 @@
<?php
/**
* Class Google\Site_Kit\Core\Conversion_Tracking\REST_Conversion_Tracking_Controller
*
* @package Google\Site_Kit\Core\Conversion_Tracking
* @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\Conversion_Tracking;
use Google\Site_Kit\Core\Permissions\Permissions;
use Google\Site_Kit\Core\REST_API\REST_Route;
use Google\Site_Kit\Core\REST_API\REST_Routes;
use WP_REST_Request;
use WP_REST_Response;
use WP_REST_Server;
/**
* Class for handling rest routes for Conversion Tracking settings.
*
* @since 1.127.0
* @access private
* @ignore
*/
class REST_Conversion_Tracking_Controller {
/**
* Conversion_Tracking_Settings instance.
*
* @since 1.127.0
* @var Conversion_Tracking_Settings
*/
protected $settings;
/**
* Constructor.
*
* @since 1.127.0
*
* @param Conversion_Tracking_Settings $settings Conversion Tracking settings.
*/
public function __construct( Conversion_Tracking_Settings $settings ) {
$this->settings = $settings;
}
/**
* Registers functionality through WordPress hooks.
*
* @since 1.127.0
*/
public function register() {
add_filter(
'googlesitekit_rest_routes',
function ( $routes ) {
return array_merge( $routes, $this->get_rest_routes() );
}
);
add_filter(
'googlesitekit_apifetch_preload_paths',
function ( $paths ) {
return array_merge(
$paths,
array(
'/' . REST_Routes::REST_ROOT . '/core/site/data/conversion-tracking',
)
);
}
);
}
/**
* Gets REST route instances.
*
* @since 1.127.0
*
* @return REST_Route[] List of REST_Route objects.
*/
protected function get_rest_routes() {
$has_capabilities = function () {
return current_user_can( Permissions::MANAGE_OPTIONS );
};
return array(
new REST_Route(
'core/site/data/conversion-tracking',
array(
'methods' => WP_REST_Server::READABLE,
'callback' => function () {
return new WP_REST_Response( $this->settings->get() );
},
'permission_callback' => $has_capabilities,
)
),
new REST_Route(
'core/site/data/conversion-tracking',
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => function ( WP_REST_Request $request ) {
$this->settings->set(
$request['data']['settings']
);
return new WP_REST_Response( $this->settings->get() );
},
'permission_callback' => $has_capabilities,
'args' => array(
'data' => array(
'type' => 'object',
'required' => true,
'properties' => array(
'settings' => array(
'type' => 'object',
'required' => true,
'properties' => array(
'enabled' => array(
'type' => 'boolean',
'required' => true,
),
),
),
),
),
),
)
),
);
}
}

View File

@@ -0,0 +1,47 @@
<?php
/**
* Class Google\Site_Kit\Core\Dashboard_Sharing\Dashboard_Sharing
*
* @package Google\Site_Kit\Core\Dashboard_Sharing
* @copyright 2022 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\Dashboard_Sharing;
/**
* Class for handling Dashboard Sharing.
*
* @since 1.82.0
* @access private
* @ignore
*/
class Dashboard_Sharing {
/**
* View_Only_Pointer instance.
*
* @since 1.83.0
* @var View_Only_Pointer
*/
protected $view_only_pointer;
/**
* Constructor.
*
* @since 1.82.0
* @since 1.158.0 Remove $user_options and $context params.
*/
public function __construct() {
$this->view_only_pointer = new View_Only_Pointer();
}
/**
* Registers functionality.
*
* @since 1.82.0
*/
public function register() {
$this->view_only_pointer->register();
}
}

View File

@@ -0,0 +1,80 @@
<?php
/**
* Class Google\Site_Kit\Core\Dashboard_Sharing\View_Only_Pointer
*
* @package Google\Site_Kit
* @copyright 2022 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\Dashboard_Sharing;
use Google\Site_Kit\Core\Admin\Pointer;
use Google\Site_Kit\Core\Permissions\Permissions;
/**
* Class for view-only pointer.
*
* @since 1.83.0.
* @access private
* @ignore
*/
final class View_Only_Pointer {
const SLUG = 'googlesitekit-view-only-pointer';
/**
* Registers functionality through WordPress hooks.
*
* @since 1.83.0
*/
public function register() {
add_filter(
'googlesitekit_admin_pointers',
function ( $pointers ) {
$pointers[] = $this->get_view_only_pointer();
return $pointers;
}
);
}
/**
* Gets the view-only pointer.
*
* @since 1.83.0.
*
* @return Pointer Admin notice instance.
*/
private function get_view_only_pointer() {
return new Pointer(
self::SLUG,
array(
'title' => __( 'You now have access to Site Kit', 'google-site-kit' ),
'content' => __( 'Check Site Kits dashboard to find out how much traffic your site is getting, your most popular pages, top keywords people use to find your site on Search, and more.', 'google-site-kit' ),
'target_id' => 'toplevel_page_googlesitekit-dashboard',
'active_callback' => function ( $hook_suffix ) {
if ( 'index.php' !== $hook_suffix
|| current_user_can( Permissions::AUTHENTICATE )
|| ! current_user_can( Permissions::VIEW_SPLASH )
) {
return false;
}
$dismissed_wp_pointers = get_user_meta( get_current_user_id(), 'dismissed_wp_pointers', true );
if ( ! $dismissed_wp_pointers ) {
return true;
}
$dismissed_wp_pointers = explode( ',', $dismissed_wp_pointers );
if ( in_array( self::SLUG, $dismissed_wp_pointers, true ) ) {
return false;
}
return true;
},
)
);
}
}

View File

@@ -0,0 +1,74 @@
<?php
/**
* Class Google\Site_Kit\Core\Dismissals\Dismissals
*
* @package Google\Site_Kit\Core\Dismissals
* @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\Core\Dismissals;
use Google\Site_Kit\Context;
use Google\Site_Kit\Core\Storage\User_Options;
/**
* Class for handling dismissals.
*
* @since 1.37.0
* @access private
* @ignore
*/
class Dismissals {
/**
* Dismissed_Items instance.
*
* @since 1.37.0
* @var Dismissed_Items
*/
protected $dismissed_items;
/**
* REST_Dismissals_Controller instance.
*
* @since 1.37.0
* @var REST_Dismissals_Controller
*/
protected $rest_controller;
/**
* Constructor.
*
* @since 1.37.0
*
* @param Context $context Plugin context.
* @param User_Options $user_options Optional. User option API. Default is a new instance.
*/
public function __construct( Context $context, ?User_Options $user_options = null ) {
$this->dismissed_items = new Dismissed_Items( $user_options ?: new User_Options( $context ) );
$this->rest_controller = new REST_Dismissals_Controller( $this->dismissed_items );
}
/**
* Gets the reference to the Dismissed_Items instance.
*
* @since 1.69.0
*
* @return Dismissed_Items An instance of the Dismissed_Items class.
*/
public function get_dismissed_items() {
return $this->dismissed_items;
}
/**
* Registers functionality through WordPress hooks.
*
* @since 1.37.0
*/
public function register() {
$this->dismissed_items->register();
$this->rest_controller->register();
}
}

View File

@@ -0,0 +1,173 @@
<?php
/**
* Class Google\Site_Kit\Core\Dismissals\Dismissed_Items
*
* @package Google\Site_Kit\Core\Dismissals
* @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\Core\Dismissals;
use Closure;
use Google\Site_Kit\Core\Storage\User_Setting;
/**
* Class for representing a user's dismissed items.
*
* @since 1.37.0
* @access private
* @ignore
*/
class Dismissed_Items extends User_Setting {
/**
* The user option name for this setting.
*
* @note This option is prefixed differently so that it will persist across disconnect/reset.
*/
const OPTION = 'googlesitekitpersistent_dismissed_items';
const DISMISS_ITEM_PERMANENTLY = 0;
/**
* Adds one or more items to the list of dismissed items.
*
* @since 1.37.0
*
* @param string $item Item to dismiss.
* @param int $expires_in_seconds TTL for the item.
*/
public function add( $item, $expires_in_seconds = self::DISMISS_ITEM_PERMANENTLY ) {
$items = $this->get();
$items[ $item ] = $expires_in_seconds ? time() + $expires_in_seconds : 0;
$this->set( $items );
}
/**
* Removes one or more items from the list of dismissed items.
*
* @since 1.107.0
*
* @param string $item Item to remove.
*/
public function remove( $item ) {
$items = $this->get();
// If the item is not in dismissed items, there's nothing to do.
if ( ! array_key_exists( $item, $items ) ) {
return;
}
unset( $items[ $item ] );
$this->set( $items );
}
/**
* Gets the value of the setting.
*
* @since 1.37.0
*
* @return array Value set for the option, or default if not set.
*/
public function get() {
$value = parent::get();
return is_array( $value ) ? $value : $this->get_default();
}
/**
* Gets the expected value type.
*
* @since 1.37.0
*
* @return string The type name.
*/
protected function get_type() {
return 'array';
}
/**
* Gets the default value.
*
* @since 1.37.0
*
* @return array The default value.
*/
protected function get_default() {
return array();
}
/**
* Gets the callback for sanitizing the setting's value before saving.
*
* @since 1.37.0
*
* @return callable Sanitize callback.
*/
protected function get_sanitize_callback() {
return function ( $items ) {
return $this->filter_dismissed_items( $items );
};
}
/**
* Determines whether the item is dismissed.
*
* @since 1.37.0
*
* @param string $item The item to check.
* @return bool TRUE if item is dismissed, otherwise FALSE.
*/
public function is_dismissed( $item ) {
$items = $this->get();
if ( ! array_key_exists( $item, $items ) ) {
return false;
}
$ttl = $items[ $item ];
if ( $ttl > 0 && $ttl < time() ) {
return false;
}
return true;
}
/**
* Gets dismissed items.
*
* @since 1.37.0
*
* @return array Dismissed items array.
*/
public function get_dismissed_items() {
$dismissed_items = $this->get();
$dismissed_items = $this->filter_dismissed_items( $dismissed_items );
return array_keys( $dismissed_items );
}
/**
* Filters dismissed items.
*
* @since 1.37.0
*
* @param array $items Dismissed items list.
* @return array Filtered dismissed items.
*/
private function filter_dismissed_items( $items ) {
$dismissed = array();
if ( is_array( $items ) ) {
foreach ( $items as $item => $ttl ) {
if ( self::DISMISS_ITEM_PERMANENTLY === $ttl || $ttl > time() ) {
$dismissed[ $item ] = $ttl;
}
}
}
return $dismissed;
}
}

View File

@@ -0,0 +1,173 @@
<?php
/**
* Class Google\Site_Kit\Core\Dismissals\REST_Dismissals_Controller
*
* @package Google\Site_Kit\Core\Dismissals
* @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\Core\Dismissals;
use Google\Site_Kit\Core\Permissions\Permissions;
use Google\Site_Kit\Core\REST_API\Exception\Invalid_Param_Exception;
use Google\Site_Kit\Core\REST_API\REST_Route;
use Google\Site_Kit\Core\REST_API\REST_Routes;
use WP_Error;
use WP_REST_Request;
use WP_REST_Response;
use WP_REST_Server;
/**
* Class for handling dismissed items rest routes.
*
* @since 1.37.0
* @access private
* @ignore
*/
class REST_Dismissals_Controller {
/**
* Dismissed_Items instance.
*
* @since 1.37.0
* @var Dismissed_Items
*/
protected $dismissed_items;
/**
* Constructor.
*
* @since 1.37.0
*
* @param Dismissed_Items $dismissed_items Dismissed items instance.
*/
public function __construct( Dismissed_Items $dismissed_items ) {
$this->dismissed_items = $dismissed_items;
}
/**
* Registers functionality through WordPress hooks.
*
* @since 1.37.0
*/
public function register() {
add_filter(
'googlesitekit_rest_routes',
function ( $routes ) {
return array_merge( $routes, $this->get_rest_routes() );
}
);
add_filter(
'googlesitekit_apifetch_preload_paths',
function ( $paths ) {
return array_merge(
$paths,
array(
'/' . REST_Routes::REST_ROOT . '/core/user/data/dismissed-items',
)
);
}
);
}
/**
* Gets REST route instances.
*
* @since 1.37.0
* @since 1.133.0 Added the `DELETE dismissed-items` route.
*
* @return REST_Route[] List of REST_Route objects.
*/
protected function get_rest_routes() {
$can_dismiss_item = function () {
return current_user_can( Permissions::VIEW_SPLASH ) || current_user_can( Permissions::VIEW_DASHBOARD );
};
return array(
new REST_Route(
'core/user/data/dismissed-items',
array(
'methods' => WP_REST_Server::READABLE,
'callback' => function () {
return new WP_REST_Response( $this->dismissed_items->get_dismissed_items() );
},
'permission_callback' => $can_dismiss_item,
)
),
new REST_Route(
'core/user/data/dismissed-items',
array(
'methods' => WP_REST_Server::DELETABLE,
'callback' => function ( WP_REST_Request $request ) {
if ( empty( $request['data']['slugs'] ) ) {
// Schema validation does not catch empty object params
// in older versions of WP.
return ( new Invalid_Param_Exception( 'data' ) )->to_wp_error();
}
foreach ( $request['data']['slugs'] as $slug ) {
$this->dismissed_items->remove( $slug );
}
return new WP_REST_Response( $this->dismissed_items->get_dismissed_items() );
},
'permission_callback' => $can_dismiss_item,
'args' => array(
'data' => array(
'type' => 'object',
'required' => true,
'minProperties' => 1,
'additionalProperties' => false,
'properties' => array(
'slugs' => array(
'type' => 'array',
'required' => true,
'items' => array(
'type' => 'string',
),
),
),
),
),
),
),
new REST_Route(
'core/user/data/dismiss-item',
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => function ( WP_REST_Request $request ) {
$data = $request['data'];
if ( empty( $data['slug'] ) ) {
return new WP_Error(
'missing_required_param',
/* translators: %s: Missing parameter name */
sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'slug' ),
array( 'status' => 400 )
);
}
$expiration = Dismissed_Items::DISMISS_ITEM_PERMANENTLY;
if ( isset( $data['expiration'] ) && intval( $data['expiration'] ) > 0 ) {
$expiration = $data['expiration'];
}
$this->dismissed_items->add( $data['slug'], $expiration );
return new WP_REST_Response( $this->dismissed_items->get_dismissed_items() );
},
'permission_callback' => $can_dismiss_item,
'args' => array(
'data' => array(
'type' => 'object',
'required' => true,
),
),
)
),
);
}
}

View File

@@ -0,0 +1,72 @@
<?php
/**
* Class Google\Site_Kit\Core\Email_Reporting\Email_Reporting
*
* @package Google\Site_Kit\Core\Email_Reporting
* @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\Core\Email_Reporting;
use Google\Site_Kit\Context;
use Google\Site_Kit\Core\Storage\Options;
/**
* Base class for Email Reporting feature.
*
* @since 1.162.0
* @access private
* @ignore
*/
class Email_Reporting {
/**
* Context instance.
*
* @since 1.162.0
* @var Context
*/
protected $context;
/**
* Email_Reporting_Settings instance.
*
* @since 1.162.0
* @var Email_Reporting_Settings
*/
protected $settings;
/**
* REST_Email_Reporting_Controller instance.
*
* @since 1.162.0
* @var REST_Email_Reporting_Controller|null
*/
protected $rest_controller;
/**
* Constructor.
*
* @since 1.162.0
*
* @param Context $context Plugin context.
* @param Options|null $options Optional. Options instance. Default is a new instance.
*/
public function __construct( Context $context, ?Options $options = null ) {
$options = $options ?: new Options( $context );
$this->settings = new Email_Reporting_Settings( $options );
$this->rest_controller = new REST_Email_Reporting_Controller( $this->settings );
}
/**
* Registers functionality through WordPress hooks.
*
* @since 1.162.0
*/
public function register() {
$this->settings->register();
$this->rest_controller->register();
}
}

View File

@@ -0,0 +1,83 @@
<?php
/**
* Class Google\Site_Kit\Core\Email_Reporting\Email_Reporting_Settings
*
* @package Google\Site_Kit
* @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\Core\Email_Reporting;
use Google\Site_Kit\Core\Storage\Setting;
/**
* Class for Email Reporting settings.
*
* @since 1.161.0
* @access private
* @ignore
*/
class Email_Reporting_Settings extends Setting {
/**
* The option name for this setting.
*/
const OPTION = 'googlesitekit_email_reporting';
/**
* Returns the expected value type.
*
* @since 1.161.0
*
* @return string The type of the setting.
*/
public function get_type() {
return 'object';
}
/**
* Gets the default value.
*
* @since 1.161.0
*
* @return array The default value.
*/
protected function get_default() {
return array(
'enabled' => true,
);
}
/**
* Gets the sanitize callback.
*
* @since 1.161.0
*
* @return callable The sanitize callback.
*/
protected function get_sanitize_callback() {
return function ( $value ) {
$new_value = $this->get();
if ( isset( $value['enabled'] ) ) {
$new_value['enabled'] = (bool) $value['enabled'];
}
return $new_value;
};
}
/**
* Checks if email reporting is enabled.
*
* @since 1.161.0
*
* @return bool True if email reporting is enabled, false otherwise.
*/
public function is_email_reporting_enabled() {
$settings = $this->get();
return $settings['enabled'];
}
}

View File

@@ -0,0 +1,130 @@
<?php
/**
* Class Google\Site_Kit\Core\Email_Reporting\REST_Email_Reporting_Controller
*
* @package Google\Site_Kit
* @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\Core\Email_Reporting;
use Google\Site_Kit\Core\Permissions\Permissions;
use Google\Site_Kit\Core\REST_API\REST_Route;
use Google\Site_Kit\Core\REST_API\REST_Routes;
use WP_REST_Request;
use WP_REST_Response;
use WP_REST_Server;
/**
* Class for handling Email Reporting site settings via REST API.
*
* @since 1.162.0
* @access private
* @ignore
*/
class REST_Email_Reporting_Controller {
/**
* Email_Reporting_Settings instance.
*
* @since 1.162.0
* @var Email_Reporting_Settings
*/
private $settings;
/**
* Constructor.
*
* @since 1.162.0
*
* @param Email_Reporting_Settings $settings Email_Reporting_Settings instance.
*/
public function __construct( Email_Reporting_Settings $settings ) {
$this->settings = $settings;
}
/**
* Registers functionality through WordPress hooks.
*
* @since 1.162.0
*/
public function register() {
add_filter(
'googlesitekit_rest_routes',
function ( $routes ) {
return array_merge( $routes, $this->get_rest_routes() );
}
);
add_filter(
'googlesitekit_apifetch_preload_paths',
function ( $paths ) {
return array_merge(
$paths,
array(
'/' . REST_Routes::REST_ROOT . '/core/site/data/email-reporting',
)
);
}
);
}
/**
* Gets REST route instances.
*
* @since 1.162.0
*
* @return REST_Route[] List of REST_Route objects.
*/
protected function get_rest_routes() {
$can_access = function () {
return current_user_can( Permissions::VIEW_SPLASH ) || current_user_can( Permissions::VIEW_DASHBOARD );
};
return array(
new REST_Route(
'core/site/data/email-reporting',
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => function () {
return new WP_REST_Response( $this->settings->get() );
},
'permission_callback' => $can_access,
),
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => function ( WP_REST_Request $request ) {
$this->settings->set( $request['data']['settings'] );
return new WP_REST_Response( $this->settings->get() );
},
'permission_callback' => $can_access,
'args' => array(
'data' => array(
'type' => 'object',
'required' => true,
'properties' => array(
'settings' => array(
'type' => 'object',
'required' => true,
'minProperties' => 1,
'additionalProperties' => false,
'properties' => array(
'enabled' => array(
'type' => 'boolean',
'required' => true,
),
),
),
),
),
),
),
)
),
);
}
}

View File

@@ -0,0 +1,133 @@
<?php
/**
* Class Google\Site_Kit\Core\Expirables\Expirable_Items
*
* @package Google\Site_Kit\Core\Expirables
* @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\Expirables;
use Google\Site_Kit\Core\Storage\User_Setting;
/**
* Class for handling expirables items.
*
* @since 1.128.0
* @access private
* @ignore
*/
class Expirable_Items extends User_Setting {
/**
* The user option name for this setting.
*
* @note This option is prefixed differently so that it will persist across disconnect/reset.
*/
const OPTION = 'googlesitekitpersistent_expirable_items';
/**
* Adds one or more items to the list of expired items.
*
* @since 1.128.0
*
* @param string $item Item to set expiration for.
* @param int $expires_in_seconds TTL for the item.
*/
public function add( $item, $expires_in_seconds ) {
$items = $this->get();
$items[ $item ] = time() + $expires_in_seconds;
$this->set( $items );
}
/**
* Removes one or more items from the list of expirable items.
*
* @since 1.128.0
*
* @param string $item Item to remove.
*/
public function remove( $item ) {
$items = $this->get();
// If the item is not in expirable items, there's nothing to do.
if ( ! array_key_exists( $item, $items ) ) {
return;
}
unset( $items[ $item ] );
$this->set( $items );
}
/**
* Gets the value of the setting.
*
* @since 1.128.0
*
* @return array Value set for the option, or default if not set.
*/
public function get() {
$value = parent::get();
return is_array( $value ) ? $value : $this->get_default();
}
/**
* Gets the expected value type.
*
* @since 1.128.0
*
* @return string The type name.
*/
protected function get_type() {
return 'array';
}
/**
* Gets the default value.
*
* @since 1.128.0
*
* @return array The default value.
*/
protected function get_default() {
return array();
}
/**
* Gets the callback for sanitizing the setting's value before saving.
*
* @since 1.128.0
*
* @return callable Sanitize callback.
*/
protected function get_sanitize_callback() {
return function ( $items ) {
return $this->filter_expirable_items( $items );
};
}
/**
* Filters expirable items.
*
* @since 1.128.0
*
* @param array $items Expirable items list.
* @return array Filtered expirable items.
*/
private function filter_expirable_items( $items ) {
$expirables = array();
if ( is_array( $items ) ) {
foreach ( $items as $item => $ttl ) {
if ( is_integer( $ttl ) ) {
$expirables[ $item ] = $ttl;
}
}
}
return $expirables;
}
}

View File

@@ -0,0 +1,76 @@
<?php
/**
* Class Google\Site_Kit\Core\Expirables\Expirables
*
* @package Google\Site_Kit\Core\Expirables
* @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\Expirables;
use Google\Site_Kit\Core\Expirables\Expirable_Items;
use Google\Site_Kit\Core\Expirables\REST_Expirable_Items_Controller;
use Google\Site_Kit\Context;
use Google\Site_Kit\Core\Storage\User_Options;
/**
* Class for handling expirables.
*
* @since 1.128.0
* @access private
* @ignore
*/
class Expirables {
/**
* Expirable_Items instance.
*
* @since 1.128.0
* @var Expirable_Items
*/
protected $expirable_items;
/**
* REST_Expirable_Items_Controller instance.
*
* @since 1.128.0
* @var REST_Expirable_Items_Controller
*/
protected $rest_controller;
/**
* Constructor.
*
* @since 1.128.0
*
* @param Context $context Plugin context.
* @param User_Options $user_options Optional. User option API. Default is a new instance.
*/
public function __construct( Context $context, ?User_Options $user_options = null ) {
$this->expirable_items = new Expirable_Items( $user_options ?: new User_Options( $context ) );
$this->rest_controller = new REST_Expirable_Items_Controller( $this->expirable_items );
}
/**
* Gets the reference to the Expirable_Items instance.
*
* @since 1.128.0
*
* @return Expirable_Items An instance of the Expirable_Items class.
*/
public function get_expirable_items() {
return $this->expirable_items;
}
/**
* Registers functionality through WordPress hooks.
*
* @since 1.128.0
*/
public function register() {
$this->expirable_items->register();
$this->rest_controller->register();
}
}

View File

@@ -0,0 +1,169 @@
<?php
/**
* Class Google\Site_Kit\Core\Expirables\REST_Expirable_Items_Controller
*
* @package Google\Site_Kit\Core\Expirables
* @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\Expirables;
use Google\Site_Kit\Core\Expirables\Expirable_Items;
use Google\Site_Kit\Core\Permissions\Permissions;
use Google\Site_Kit\Core\REST_API\REST_Route;
use Google\Site_Kit\Core\REST_API\REST_Routes;
use WP_Error;
use WP_REST_Request;
use WP_REST_Response;
use WP_REST_Server;
/**
* Class for handling expirable items rest routes.
*
* @since 1.128.0
* @access private
* @ignore
*/
class REST_Expirable_Items_Controller {
/**
* Expirable_Items instance.
*
* @since 1.128.0
* @var Expirable_Items
*/
protected $expirable_items;
/**
* Constructor.
*
* @since 1.128.0
*
* @param Expirable_Items $expirable_items Expirable items instance.
*/
public function __construct( Expirable_Items $expirable_items ) {
$this->expirable_items = $expirable_items;
}
/**
* Registers functionality through WordPress hooks.
*
* @since 1.128.0
*/
public function register() {
add_filter(
'googlesitekit_rest_routes',
function ( $routes ) {
return array_merge( $routes, $this->get_rest_routes() );
}
);
add_filter(
'googlesitekit_apifetch_preload_paths',
function ( $paths ) {
return array_merge(
$paths,
array(
'/' . REST_Routes::REST_ROOT . '/core/user/data/expirable-items',
)
);
}
);
}
/**
* Gets REST route instances.
*
* @since 1.128.0
*
* @return REST_Route[] List of REST_Route objects.
*/
protected function get_rest_routes() {
$can_manage_expirable_item = function () {
return current_user_can( Permissions::VIEW_DASHBOARD );
};
return array(
new REST_Route(
'core/user/data/expirable-items',
array(
'methods' => WP_REST_Server::READABLE,
'callback' => function () {
return new WP_REST_Response( $this->expirable_items->get() );
},
'permission_callback' => $can_manage_expirable_item,
)
),
new REST_Route(
'core/user/data/set-expirable-item-timers',
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => function ( WP_REST_Request $request ) {
$data = $request['data'];
if ( empty( $data ) || ! is_array( $data ) ) {
return new WP_Error(
'missing_required_param',
/* translators: %s: Missing parameter name */
sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'items' ),
array( 'status' => 400 )
);
}
foreach ( $data as $datum ) {
if ( empty( $datum['slug'] ) ) {
return new WP_Error(
'missing_required_param',
/* translators: %s: Missing parameter name */
sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'slug' ),
array( 'status' => 400 )
);
}
$expiration = null;
if ( isset( $datum['expiration'] ) && intval( $datum['expiration'] ) > 0 ) {
$expiration = $datum['expiration'];
}
if ( ! $expiration ) {
return new WP_Error(
'missing_required_param',
/* translators: %s: Missing parameter name */
sprintf( __( 'Request parameter is invalid: %s.', 'google-site-kit' ), 'expiration' ),
array( 'status' => 400 )
);
}
$this->expirable_items->add( $datum['slug'], $expiration );
}
return new WP_REST_Response( $this->expirable_items->get() );
},
'permission_callback' => $can_manage_expirable_item,
'args' => array(
'data' => array(
'type' => 'array',
'required' => true,
'items' => array(
'type' => 'object',
'additionalProperties' => false,
'properties' => array(
'slug' => array(
'type' => 'string',
'required' => true,
),
'expiration' => array(
'type' => 'integer',
'required' => true,
),
),
),
),
),
)
),
);
}
}

View File

@@ -0,0 +1,95 @@
<?php
/**
* Class Google\Site_Kit\Core\Feature_Tours\Dismissed_Tours
*
* @package Google\Site_Kit\Core\Feature_Tours
* @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\Core\Feature_Tours;
use Closure;
use Google\Site_Kit\Core\Storage\User_Setting;
/**
* Class for representing a user's dismissed feature tours.
*
* @since 1.27.0
* @access private
* @ignore
*/
class Dismissed_Tours extends User_Setting {
/**
* The user option name for this setting.
*
* @note This option is prefixed differently
* so that it will persist across disconnect/reset.
*/
const OPTION = 'googlesitekitpersistent_dismissed_tours';
/**
* Adds one or more tours to the list of dismissed tours.
*
* @since 1.27.0
*
* @param string ...$tour_slug The tour identifier to dismiss.
*/
public function add( ...$tour_slug ) {
$value = array_merge( $this->get(), $tour_slug );
$this->set( $value );
}
/**
* Gets the value of the setting.
*
* @since 1.27.0
*
* @return array Value set for the option, or default if not set.
*/
public function get() {
$value = parent::get();
return is_array( $value ) ? $value : array();
}
/**
* Gets the expected value type.
*
* @since 1.27.0
*
* @return string The type name.
*/
protected function get_type() {
return 'array';
}
/**
* Gets the default value.
*
* @since 1.27.0
*
* @return array The default value.
*/
protected function get_default() {
return array();
}
/**
* Gets the callback for sanitizing the setting's value before saving.
*
* @since 1.27.0
*
* @return Closure
*/
protected function get_sanitize_callback() {
return function ( $value ) {
return is_array( $value )
? array_values( array_unique( $value ) )
: $this->get();
};
}
}

View File

@@ -0,0 +1,63 @@
<?php
/**
* Class Google\Site_Kit\Core\Feature_Tours\Feature_Tours
*
* @package Google\Site_Kit\Core\Feature_Tours
* @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\Core\Feature_Tours;
use Google\Site_Kit\Context;
use Google\Site_Kit\Core\Storage\User_Options;
/**
* Class for handling feature tours.
*
* @since 1.27.0
* @access private
* @ignore
*/
class Feature_Tours {
/**
* Dismissed_Tours instance.
*
* @since 1.27.0
* @var Dismissed_Tours
*/
protected $dismissed_tours;
/**
* REST_Feature_Tours_Controller instance.
*
* @since 1.27.0
* @var REST_Feature_Tours_Controller
*/
protected $rest_controller;
/**
* Constructor.
*
* @since 1.27.0
*
* @param Context $context Plugin context.
* @param User_Options $user_options Optional. User option API. Default is a new instance.
*/
public function __construct( Context $context, ?User_Options $user_options = null ) {
$this->dismissed_tours = new Dismissed_Tours( $user_options ?: new User_Options( $context ) );
$this->rest_controller = new REST_Feature_Tours_Controller( $this->dismissed_tours );
}
/**
* Registers functionality through WordPress hooks.
*
* @since 1.27.0
*/
public function register() {
$this->dismissed_tours->register();
$this->rest_controller->register();
}
}

View File

@@ -0,0 +1,128 @@
<?php
/**
* Class Google\Site_Kit\Core\Feature_Tours\REST_Feature_Tours_Controller
*
* @package Google\Site_Kit\Core\Feature_Tours
* @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\Core\Feature_Tours;
use Google\Site_Kit\Core\Permissions\Permissions;
use Google\Site_Kit\Core\REST_API\REST_Route;
use Google\Site_Kit\Core\REST_API\REST_Routes;
use WP_Error;
use WP_REST_Request;
use WP_REST_Response;
use WP_REST_Server;
/**
* Class for handling feature tour rest routes.
*
* @since 1.27.0
* @access private
* @ignore
*/
class REST_Feature_Tours_Controller {
/**
* Dismissed_Tours instance.
*
* @since 1.27.0
* @var Dismissed_Tours
*/
protected $dismissed_tours;
/**
* Constructor.
*
* @since 1.27.0
*
* @param Dismissed_Tours $dismissed_tours Dismissed tours instance.
*/
public function __construct( Dismissed_Tours $dismissed_tours ) {
$this->dismissed_tours = $dismissed_tours;
}
/**
* Registers functionality through WordPress hooks.
*
* @since 1.27.0
*/
public function register() {
add_filter(
'googlesitekit_rest_routes',
function ( $routes ) {
return array_merge( $routes, $this->get_rest_routes() );
}
);
add_filter(
'googlesitekit_apifetch_preload_paths',
function ( $paths ) {
$feature_tour_routes = array(
'/' . REST_Routes::REST_ROOT . '/core/user/data/dismissed-tours',
);
return array_merge( $paths, $feature_tour_routes );
}
);
}
/**
* Gets REST route instances.
*
* @since 1.27.0
*
* @return REST_Route[] List of REST_Route objects.
*/
protected function get_rest_routes() {
$can_dismiss_tour = function () {
return current_user_can( Permissions::AUTHENTICATE ) || current_user_can( Permissions::VIEW_SHARED_DASHBOARD );
};
return array(
new REST_Route(
'core/user/data/dismissed-tours',
array(
'methods' => WP_REST_Server::READABLE,
'callback' => function () {
return new WP_REST_Response( $this->dismissed_tours->get() );
},
'permission_callback' => $can_dismiss_tour,
)
),
new REST_Route(
'core/user/data/dismiss-tour',
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => function ( WP_REST_Request $request ) {
$data = $request['data'];
if ( empty( $data['slug'] ) ) {
return new WP_Error(
'missing_required_param',
/* translators: %s: Missing parameter name */
sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'slug' ),
array( 'status' => 400 )
);
}
$this->dismissed_tours->add( $data['slug'] );
return new WP_REST_Response( $this->dismissed_tours->get() );
},
'permission_callback' => $can_dismiss_tour,
'args' => array(
'data' => array(
'type' => 'object',
'required' => true,
),
),
)
),
);
}
}

View File

@@ -0,0 +1,32 @@
<?php
/**
* Interface Google\Site_Kit\Core\Guards\Guard_Interface
*
* @package Google\Site_Kit\Core\Guards
* @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\Core\Guards;
use WP_Error;
/**
* Interface for a guard.
*
* @since 1.24.0
* @access private
* @ignore
*/
interface Guard_Interface {
/**
* Determines whether the guarded entity can be activated or not.
*
* @since 1.24.0
*
* @return bool|WP_Error TRUE if guarded entity can be activated, otherwise FALSE or an error.
*/
public function can_activate();
}

View File

@@ -0,0 +1,47 @@
<?php
/**
* Class Google\Site_Kit\Core\HTTP\Middleware
*
* @package Google\Site_Kit
* @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\Core\HTTP;
use Google\Site_Kit_Dependencies\GuzzleHttp\Exception\RequestException;
use WP_Http;
/**
* Guzzle Middleware.
*
* @since 1.159.0
*/
class Middleware {
/**
* Middleware for blocking external requests using WordPress block_request.
*
* @since 1.159.0
*
* @return callable Returns a function that blocks external requests using WordPress block_request.
*/
public static function block_external_request() {
return static function ( callable $handler ) {
return function ( $request, $options ) use ( $handler ) {
$uri = $request->getUri();
$wp_http = new WP_Http();
if ( $wp_http->block_request( $uri ) ) {
throw new RequestException(
__( 'User has blocked requests through HTTP.', 'default' ),
$request
);
}
return $handler( $request, $options );
};
};
}
}

View File

@@ -0,0 +1,123 @@
<?php
/**
* Class Google\Site_Kit\Core\Key_Metrics\Key_Metrics
*
* @package Google\Site_Kit\Core\Key_Metrics
* @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\Core\Key_Metrics;
use Google\Site_Kit\Context;
use Google\Site_Kit\Core\Storage\Options;
use Google\Site_Kit\Core\Storage\Transients;
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\Method_Proxy_Trait;
/**
* Class for handling Key_Metrics.
*
* @since 1.93.0
* @access private
* @ignore
*/
class Key_Metrics implements Provides_Feature_Metrics {
use Method_Proxy_Trait;
use Feature_Metrics_Trait;
/**
* Key_Metrics_Settings instance.
*
* @since 1.93.0
* @var Key_Metrics_Settings
*/
protected $key_metrics_settings;
/**
* Key_Metrics_Setup_Completed_By instance.
*
* @since 1.113.0
* @var Key_Metrics_Setup_Completed_By
*/
protected $key_metrics_setup_completed_by;
/**
* REST_Key_Metrics_Controller instance.
*
* @since 1.93.0
* @var REST_Key_Metrics_Controller
*/
protected $rest_controller;
/**
* Key_Metrics_Setup_New instance.
*
* @since 1.115.0
* @var Key_Metrics_Setup_New
*/
protected $key_metrics_setup_new;
/**
* Constructor.
*
* @since 1.93.0
*
* @param Context $context Plugin context.
* @param User_Options $user_options Optional. User option API. Default is a new instance.
* @param Options $options Optional. Option API instance. Default is a new instance.
*/
public function __construct( Context $context, ?User_Options $user_options = null, ?Options $options = null ) {
$this->key_metrics_settings = new Key_Metrics_Settings( $user_options ?: new User_Options( $context ) );
$this->key_metrics_setup_completed_by = new Key_Metrics_Setup_Completed_By( $options ?: new Options( $context ) );
$this->key_metrics_setup_new = new Key_Metrics_Setup_New( new Transients( $context ) );
$this->rest_controller = new REST_Key_Metrics_Controller( $this->key_metrics_settings, $this->key_metrics_setup_completed_by );
}
/**
* Registers functionality through WordPress hooks.
*
* @since 1.93.0
*/
public function register() {
$this->key_metrics_settings->register();
$this->key_metrics_setup_completed_by->register();
$this->key_metrics_setup_new->register();
$this->rest_controller->register();
$this->register_feature_metrics();
add_filter( 'googlesitekit_inline_base_data', $this->get_method_proxy( 'inline_js_base_data' ) );
}
/**
* Adds the status of the Key Metrics widget setup to the inline JS data.
*
* @since 1.108.0
* @since 1.113.0 Add keyMetricsSetupCompletedBy (id) instead of keyMetricsSetupCompleted boolean.
*
* @param array $data Inline JS data.
* @return array Filtered $data.
*/
private function inline_js_base_data( $data ) {
$data['keyMetricsSetupCompletedBy'] = (int) $this->key_metrics_setup_completed_by->get();
return $data;
}
/**
* Gets an array of internal feature metrics.
*
* @since 1.163.0
*
* @return array
*/
public function get_feature_metrics() {
return array(
'km_setup' => (bool) $this->key_metrics_setup_completed_by->get(),
);
}
}

View File

@@ -0,0 +1,108 @@
<?php
/**
* Class Google\Site_Kit\Core\Key_Metrics\Key_Metrics_Settings
*
* @package Google\Site_Kit\Core\Key_Metrics
* @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\Core\Key_Metrics;
use Google\Site_Kit\Core\Storage\User_Setting;
use Google\Site_Kit\Core\Util\Sanitize;
/**
* Class to store user key metrics settings.
*
* @since 1.93.0
* @access private
* @ignore
*/
class Key_Metrics_Settings extends User_Setting {
/**
* The user option name for this setting.
*/
const OPTION = 'googlesitekit_key_metrics_settings';
/**
* Gets the expected value type.
*
* @since 1.93.0
*
* @return string The type name.
*/
protected function get_type() {
return 'object';
}
/**
* Gets the default value.
*
* @since 1.93.0
*
* @return array The default value.
*/
protected function get_default() {
return array(
'widgetSlugs' => array(),
'isWidgetHidden' => false,
);
}
/**
* Merges an array of settings to update.
*
* @since 1.93.0
*
* @param array $partial Partial settings array to save.
* @return bool True on success, false on failure.
*/
public function merge( array $partial ) {
$settings = $this->get();
$partial = array_filter(
$partial,
function ( $value ) {
return null !== $value;
}
);
$allowed_settings = array(
'widgetSlugs' => true,
'isWidgetHidden' => true,
);
$updated = array_intersect_key( $partial, $allowed_settings );
return $this->set( array_merge( $settings, $updated ) );
}
/**
* Gets the callback for sanitizing the setting's value before saving.
*
* @since 1.93.0
*
* @return callable Sanitize callback.
*/
protected function get_sanitize_callback() {
return function ( $settings ) {
if ( ! is_array( $settings ) ) {
return array();
}
$sanitized_settings = array();
if ( isset( $settings['widgetSlugs'] ) ) {
$sanitized_settings['widgetSlugs'] = Sanitize::sanitize_string_list( $settings['widgetSlugs'] );
}
if ( isset( $settings['isWidgetHidden'] ) ) {
$sanitized_settings['isWidgetHidden'] = false !== $settings['isWidgetHidden'];
}
return $sanitized_settings;
};
}
}

View File

@@ -0,0 +1,39 @@
<?php
/**
* Class Google\Site_Kit\Core\Key_Metrics\Key_Metrics_Setup_Completed_By
*
* @package Google\Site_Kit\Core\Key_Metrics
* @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\Core\Key_Metrics;
use Google\Site_Kit\Core\Storage\Setting;
/**
* Class for handling the setup completion state of Key Metrics.
*
* @since 1.113.0
* @access private
* @ignore
*/
class Key_Metrics_Setup_Completed_By extends Setting {
/**
* The option_name for this setting.
*/
const OPTION = 'googlesitekit_key_metrics_setup_completed_by';
/**
* Gets the expected value type.
*
* @since 1.113.0
*
* @return string The type name.
*/
protected function get_type() {
return 'integer';
}
}

View File

@@ -0,0 +1,90 @@
<?php
/**
* Class Google\Site_Kit\Core\Key_Metrics\Key_Metrics_Setup_New
*
* @package Google\Site_Kit\Core\Key_Metrics
* @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\Core\Key_Metrics;
use Google\Site_Kit\Core\Storage\Transients;
use Google\Site_Kit\Core\Util\Method_Proxy_Trait;
/**
* Class for handling Key_Metrics_Setup_New state.
*
* @since 1.115.0
* @access private
* @ignore
*/
class Key_Metrics_Setup_New {
use Method_Proxy_Trait;
const TRANSIENT = 'googlesitekit_key_metrics_setup_new';
/**
* Transients instance.
*
* @var Transients
*/
private $transients;
/**
* Constructor.
*
* @since 1.115.0
*
* @param Transients $transients Transients instance.
*/
public function __construct( Transients $transients ) {
$this->transients = $transients;
}
/**
* Registers functionality through WordPress hooks.
*
* @since 1.115.0
*/
public function register() {
add_action(
'add_option_' . Key_Metrics_Setup_Completed_By::OPTION,
$this->get_method_proxy( 'mark_setup_completed' ),
10,
2
);
add_filter( 'googlesitekit_inline_base_data', $this->get_method_proxy( 'inline_js_base_data' ) );
}
/**
* Marks Key Metrics setup as just completed for a limited period of time.
*
* @since 1.115.0
*
* @param string $option Key_Metrics_Setup_Completed_By option name.
* @param mixed $value Option value added.
*/
protected function mark_setup_completed( $option, $value ) {
if ( $value ) {
$this->transients->set( self::TRANSIENT, true, 2 * WEEK_IN_SECONDS );
}
}
/**
* Extends base data with setup new state.
*
* @since 1.115.0
*
* @param array $data Inline base data.
* @return array
*/
protected function inline_js_base_data( $data ) {
$data['keyMetricsSetupNew'] = (bool) $this->transients->get( self::TRANSIENT );
return $data;
}
}

View File

@@ -0,0 +1,188 @@
<?php
/**
* Class Google\Site_Kit\Core\Dismissals\REST_Key_Metrics_Controller
*
* @package Google\Site_Kit\Core\Key_Metrics
* @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\Core\Key_Metrics;
use Google\Site_Kit\Core\Permissions\Permissions;
use Google\Site_Kit\Core\REST_API\REST_Route;
use Google\Site_Kit\Core\REST_API\REST_Routes;
use WP_Error;
use WP_REST_Request;
use WP_REST_Response;
use WP_REST_Server;
/**
* Class for handling rest routes for Key Metrics settings.
*
* @since 1.93.0
* @access private
* @ignore
*/
class REST_Key_Metrics_Controller {
/**
* Key_Metrics_Settings instance.
*
* @since 1.93.0
* @var Key_Metrics_Settings
*/
protected $settings;
/**
* Key_Metrics_Setup_Completed_By instance.
*
* @since 1.113.0
* @var Key_Metrics_Setup_Completed_By
*/
protected $key_metrics_setup_completed_by;
/**
* Constructor.
*
* @since 1.93.0
*
* @param Key_Metrics_Settings $settings Key Metrics settings.
* @param Key_Metrics_Setup_Completed_By $key_metrics_setup_completed_by Site-wide option to check if key metrics set up is complete.
*/
public function __construct(
Key_Metrics_Settings $settings,
Key_Metrics_Setup_Completed_By $key_metrics_setup_completed_by
) {
$this->settings = $settings;
$this->key_metrics_setup_completed_by = $key_metrics_setup_completed_by;
}
/**
* Registers functionality through WordPress hooks.
*
* @since 1.93.0
*/
public function register() {
add_filter(
'googlesitekit_rest_routes',
function ( $routes ) {
return array_merge( $routes, $this->get_rest_routes() );
}
);
add_filter(
'googlesitekit_apifetch_preload_paths',
function ( $paths ) {
return array_merge(
$paths,
array(
'/' . REST_Routes::REST_ROOT . '/core/user/data/key-metrics',
)
);
}
);
}
/**
* Gets REST route instances.
*
* @since 1.93.0
*
* @return REST_Route[] List of REST_Route objects.
*/
protected function get_rest_routes() {
$has_capabilities = function () {
return current_user_can( Permissions::VIEW_SPLASH ) || current_user_can( Permissions::VIEW_DASHBOARD );
};
return array(
new REST_Route(
'core/user/data/key-metrics',
array(
'methods' => WP_REST_Server::READABLE,
'callback' => function () {
return new WP_REST_Response( $this->settings->get() );
},
'permission_callback' => $has_capabilities,
)
),
new REST_Route(
'core/user/data/key-metrics',
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => function ( WP_REST_Request $request ) {
// Data is already validated because we've defined the detailed schema.
// If the incoming data param doesn't match the schema, then WordPress
// will automatically return the rest_invalid_param error and we will
// never get to here.
$data = $request->get_param( 'data' );
$settings = $data['settings'];
if ( isset( $settings['widgetSlugs'] ) ) {
$num_widgets = count( $settings['widgetSlugs'] );
if ( ! $num_widgets ) {
return new WP_Error(
'rest_invalid_param',
__( 'Selected metrics cannot be empty.', 'google-site-kit' ),
array( 'status' => 400 )
);
}
// Additional check is needed to ensure that we have no more than 4 widget
// slugs provided. This is required until we drop support for WP versions below 5.5.0, after
// which we can solely rely on `maxItems` in the schema validation (see below).
// See https://github.com/WordPress/WordPress/blob/965fcddcf68cf4fd122ae24b992e242dfea1d773/wp-includes/rest-api.php#L1922-L1925.
$max_num_widgets = 8;
if ( $num_widgets > $max_num_widgets ) {
return new WP_Error(
'rest_invalid_param',
__( 'No more than 4 key metrics can be selected.', 'google-site-kit' ),
array( 'status' => 400 )
);
}
$key_metrics_setup_already_done_by_user = $this->key_metrics_setup_completed_by->get();
if ( empty( $key_metrics_setup_already_done_by_user ) ) {
$current_user_id = get_current_user_id();
$this->key_metrics_setup_completed_by->set( $current_user_id );
}
}
$this->settings->merge( $data['settings'] );
return new WP_REST_Response( $this->settings->get() );
},
'permission_callback' => $has_capabilities,
'args' => array(
'data' => array(
'type' => 'object',
'required' => true,
'properties' => array(
'settings' => array(
'type' => 'object',
'required' => true,
'properties' => array(
'isWidgetHidden' => array(
'type' => 'boolean',
'required' => true,
),
'widgetSlugs' => array(
'type' => 'array',
'required' => false,
'maxItems' => 8,
'items' => array(
'type' => 'string',
),
),
),
),
),
),
),
)
),
);
}
}

View File

@@ -0,0 +1,137 @@
<?php
/**
* Class Google\Site_Kit\Core\Modules\Datapoint
*
* @package Google\Site_Kit\Core\Modules
* @copyright 2022 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;
/**
* Class representing a datapoint definition.
*
* @since 1.77.0
* @access private
* @ignore
*/
class Datapoint {
/**
* Service identifier.
*
* @since 1.77.0
* @since 1.160.0 Updated to allow a function to return the service identifier.
* @var string|callable
*/
private $service = '';
/**
* Required scopes.
*
* @since 1.77.0
* @var string[]
*/
private $scopes = array();
/**
* Shareable status.
*
* @since 1.77.0
* @var bool
*/
private $shareable;
/**
* Request scopes message.
*
* @since 1.77.0
* @var string
*/
private $request_scopes_message;
/**
* Constructor.
*
* @since 1.77.0
*
* @param array $definition Definition fields.
*/
public function __construct( array $definition ) {
$this->shareable = ! empty( $definition['shareable'] );
if (
isset( $definition['service'] ) &&
(
is_string( $definition['service'] ) ||
is_callable( $definition['service'] )
)
) {
$this->service = $definition['service'];
}
if ( isset( $definition['scopes'] ) && is_array( $definition['scopes'] ) ) {
$this->scopes = $definition['scopes'];
}
if ( isset( $definition['request_scopes_message'] ) && is_string( $definition['request_scopes_message'] ) ) {
$this->request_scopes_message = $definition['request_scopes_message'];
}
}
/**
* Checks if the datapoint is shareable.
*
* @since 1.77.0
*
* @return bool
*/
public function is_shareable() {
return $this->shareable;
}
/**
* Gets the service identifier.
*
* @since 1.77.0
*
* @return string
*/
protected function get_service() {
$service = $this->service;
if ( is_callable( $this->service ) ) {
$service = call_user_func( $this->service );
}
return $service;
}
/**
* Gets the list of required scopes.
*
* @since 1.77.0
*
* @return string[]
*/
public function get_required_scopes() {
return $this->scopes;
}
/**
* Gets the request scopes message.
*
* @since 1.77.0
*
* @return string
*/
public function get_request_scopes_message() {
if ( $this->request_scopes_message ) {
return $this->request_scopes_message;
}
return __( 'Youll need to grant Site Kit permission to do this.', 'google-site-kit' );
}
}

View File

@@ -0,0 +1,40 @@
<?php
/**
* Class Google\Site_Kit\Core\Modules\Executable_Datapoint
*
* @package Google\Site_Kit\Core\Modules
* @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\Core\Modules;
use Google\Site_Kit\Core\REST_API\Data_Request;
/**
* Interface for a datapoint that can be executed.
*
* @since 1.160.0
*/
interface Executable_Datapoint {
/**
* Creates a request object.
*
* @since 1.160.0
*
* @param Data_Request $data Data request object.
*/
public function create_request( Data_Request $data );
/**
* Parses a response.
*
* @since 1.160.0
*
* @param mixed $response Request response.
* @param Data_Request $data Data request object.
*/
public function parse_response( $response, Data_Request $data );
}

View File

@@ -0,0 +1,818 @@
<?php
/**
* Class Google\Site_Kit\Core\Modules\Module
*
* @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\Core\Modules;
use Closure;
use Exception;
use Google\Site_Kit\Context;
use Google\Site_Kit\Core\Assets\Assets;
use Google\Site_Kit\Core\Authentication\Clients\OAuth_Client;
use Google\Site_Kit\Core\Authentication\Exception\Insufficient_Scopes_Exception;
use Google\Site_Kit\Core\Authentication\Exception\Google_Proxy_Code_Exception;
use Google\Site_Kit\Core\Contracts\WP_Errorable;
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\Core\Authentication\Authentication;
use Google\Site_Kit\Core\Authentication\Clients\Google_Site_Kit_Client;
use Google\Site_Kit\Core\REST_API\Exception\Invalid_Datapoint_Exception;
use Google\Site_Kit\Core\REST_API\Data_Request;
use Google\Site_Kit\Core\Storage\Transients;
use Google\Site_Kit_Dependencies\Google\Service as Google_Service;
use Google\Site_Kit_Dependencies\Google_Service_Exception;
use Google\Site_Kit_Dependencies\Psr\Http\Message\RequestInterface;
use WP_Error;
/**
* Base class for a module.
*
* @since 1.0.0
* @access private
* @ignore
*
* @property-read string $slug Unique module identifier.
* @property-read string $name Module name.
* @property-read string $description Module description.
* @property-read int $order Module order within module lists.
* @property-read string $homepage External module homepage URL.
* @property-read array $depends_on List of other module slugs the module depends on.
* @property-read bool $force_active Whether the module cannot be disabled.
* @property-read bool $internal Whether the module is internal, thus without any UI.
*/
abstract class Module {
/**
* Plugin context.
*
* @since 1.0.0
* @var Context
*/
protected $context;
/**
* Option API instance.
*
* @since 1.0.0
* @var Options
*/
protected $options;
/**
* User Option API instance.
*
* @since 1.0.0
* @var User_Options
*/
protected $user_options;
/**
* Authentication instance.
*
* @since 1.0.0
* @var Authentication
*/
protected $authentication;
/**
* Assets API instance.
*
* @since 1.40.0
* @var Assets
*/
protected $assets;
/**
* Transients instance.
*
* @since 1.96.0
* @var Transients
*/
protected $transients;
/**
* Module information.
*
* @since 1.0.0
* @var array
*/
private $info = array();
/**
* Google API client instance.
*
* @since 1.0.0
* @var Google_Site_Kit_Client|null
*/
private $google_client;
/**
* Google services as $identifier => $service_instance pairs.
*
* @since 1.0.0
* @var array|null
*/
private $google_services;
/**
* Constructor.
*
* @since 1.0.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
) {
$this->context = $context;
$this->options = $options ?: new Options( $this->context );
$this->user_options = $user_options ?: new User_Options( $this->context );
$this->authentication = $authentication ?: new Authentication( $this->context, $this->options, $this->user_options );
$this->assets = $assets ?: new Assets( $this->context );
$this->transients = new Transients( $this->context );
$this->info = $this->parse_info( (array) $this->setup_info() );
}
/**
* Registers functionality through WordPress hooks.
*
* @since 1.0.0
*/
abstract public function register();
/**
* Magic isset-er.
*
* Allows checking for existence of module information.
*
* @since 1.0.0
*
* @param string $key Key to check..
* @return bool True if value for $key is available, false otherwise.
*/
final public function __isset( $key ) {
return isset( $this->info[ $key ] );
}
/**
* Magic getter.
*
* Allows reading module information.
*
* @since 1.0.0
*
* @param string $key Key to get value for.
* @return mixed Value for $key, or null if not available.
*/
final public function __get( $key ) {
if ( ! isset( $this->info[ $key ] ) ) {
return null;
}
return $this->info[ $key ];
}
/**
* 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() {
return true;
}
/**
* Gets data for the given datapoint.
*
* @since 1.0.0
*
* @param string $datapoint Datapoint to get data for.
* @param array|Data_Request $data Optional. Contextual data to provide. Default empty array.
* @return mixed Data on success, or WP_Error on failure.
*/
final public function get_data( $datapoint, $data = array() ) {
return $this->execute_data_request(
new Data_Request( 'GET', 'modules', $this->slug, $datapoint, $data )
);
}
/**
* Sets data for the given datapoint.
*
* @since 1.0.0
*
* @param string $datapoint Datapoint to get data for.
* @param array|Data_Request $data Data to set.
* @return mixed Response data on success, or WP_Error on failure.
*/
final public function set_data( $datapoint, $data ) {
return $this->execute_data_request(
new Data_Request( 'POST', 'modules', $this->slug, $datapoint, $data )
);
}
/**
* Returns the list of datapoints the class provides data for.
*
* @since 1.0.0
*
* @return array List of datapoints.
*/
final public function get_datapoints() {
$keys = array();
$definitions = $this->get_datapoint_definitions();
foreach ( array_keys( $definitions ) as $key ) {
$parts = explode( ':', $key );
$name = end( $parts );
if ( ! empty( $name ) ) {
$keys[ $name ] = $name;
}
}
return array_values( $keys );
}
/**
* Returns the mapping between available datapoints and their services.
*
* @since 1.0.0
* @since 1.9.0 No longer abstract.
* @deprecated 1.12.0
*
* @return array Associative array of $datapoint => $service_identifier pairs.
*/
protected function get_datapoint_services() {
_deprecated_function( __METHOD__, '1.12.0', static::class . '::get_datapoint_definitions' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
return array();
}
/**
* 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();
}
/**
* Gets the datapoint definition instance.
*
* @since 1.77.0
*
* @param string $datapoint_id Datapoint ID.
* @return Datapoint Datapoint instance.
* @throws Invalid_Datapoint_Exception Thrown if no datapoint exists by the given ID.
*/
protected function get_datapoint_definition( $datapoint_id ) {
$definitions = $this->get_datapoint_definitions();
// All datapoints must be defined.
if ( empty( $definitions[ $datapoint_id ] ) ) {
throw new Invalid_Datapoint_Exception();
}
$datapoint = $definitions[ $datapoint_id ];
if ( $datapoint instanceof Datapoint ) {
return $datapoint;
}
return new Datapoint( $datapoint );
}
/**
* Creates a request object for the given datapoint.
*
* @since 1.0.0
*
* @param Data_Request $data Data request object.
*
* // phpcs:ignore Squiz.Commenting.FunctionComment.InvalidNoReturn
* @return RequestInterface|callable|WP_Error Request object or callable on success, or WP_Error on failure.
* @throws Invalid_Datapoint_Exception Override in a sub-class.
*/
protected function create_data_request( Data_Request $data ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found,Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
throw new Invalid_Datapoint_Exception();
}
/**
* 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 ) {
return $response;
}
/**
* Creates a request object for the given datapoint.
*
* @since 1.0.0
*
* @param Data_Request $data Data request object.
* @return mixed Data on success, or WP_Error on failure.
*/
final protected function execute_data_request( Data_Request $data ) {
$restore_defers = array();
try {
$datapoint = $this->get_datapoint_definition( "{$data->method}:{$data->datapoint}" );
$oauth_client = $this->get_oauth_client_for_datapoint( $datapoint );
$this->validate_datapoint_scopes( $datapoint, $oauth_client );
$this->validate_base_scopes( $oauth_client );
// In order for a request to leverage a client other than the default
// it must return a RequestInterface (Google Services return this when defer = true).
// If not deferred, the request will be executed immediately with the client
// the service instance was instantiated with, which will always be the
// default client, configured for the current user and provided in `get_service`.
// Client defer is false by default, so we need to configure the default to defer
// even if a different client will be the one to execute the request because
// the default instance is what services are setup with.
$restore_defers[] = $this->get_client()->withDefer( true );
if ( $this->authentication->get_oauth_client() !== $oauth_client ) {
$restore_defers[] = $oauth_client->get_client()->withDefer( true );
$current_user = wp_get_current_user();
}
if ( $datapoint instanceof Executable_Datapoint ) {
$request = $datapoint->create_request( $data );
} else {
$request = $this->create_data_request( $data );
}
if ( is_wp_error( $request ) ) {
return $request;
} elseif ( $request instanceof Closure ) {
$response = $request();
} elseif ( $request instanceof RequestInterface ) {
$response = $oauth_client->get_client()->execute( $request );
} else {
return new WP_Error(
'invalid_datapoint_request',
__( 'Invalid datapoint request.', 'google-site-kit' ),
array( 'status' => 400 )
);
}
} catch ( Exception $e ) {
return $this->exception_to_error( $e, $data->datapoint );
} finally {
foreach ( $restore_defers as $restore_defer ) {
$restore_defer();
}
}
if ( is_wp_error( $response ) ) {
return $response;
}
if ( $datapoint instanceof Executable_Datapoint ) {
return $datapoint->parse_response( $response, $data );
}
return $this->parse_data_response( $data, $response );
}
/**
* Validates necessary scopes for the given datapoint.
*
* @since 1.77.0
*
* @param Datapoint $datapoint Datapoint instance.
* @param OAuth_Client $oauth_client OAuth_Client instance.
* @throws Insufficient_Scopes_Exception Thrown if required scopes are not satisfied.
*/
private function validate_datapoint_scopes( Datapoint $datapoint, OAuth_Client $oauth_client ) {
$required_scopes = $datapoint->get_required_scopes();
if ( $required_scopes && ! $oauth_client->has_sufficient_scopes( $required_scopes ) ) {
$message = $datapoint->get_request_scopes_message();
throw new Insufficient_Scopes_Exception( $message, 0, null, $required_scopes );
}
}
/**
* Validates necessary scopes for the module.
*
* @since 1.77.0
*
* @param OAuth_Client $oauth_client OAuth_Client instance.
* @throws Insufficient_Scopes_Exception Thrown if required scopes are not satisfied.
*/
private function validate_base_scopes( OAuth_Client $oauth_client ) {
if ( ! $this instanceof Module_With_Scopes ) {
return;
}
if ( ! $oauth_client->has_sufficient_scopes( $this->get_scopes() ) ) {
$message = sprintf(
/* translators: %s: module name */
__( 'Site Kit cant access the relevant data from %s because you havent granted all permissions requested during setup.', 'google-site-kit' ),
$this->name
);
throw new Insufficient_Scopes_Exception( $message, 0, null, $this->get_scopes() );
}
}
/**
* Gets the output for a specific frontend hook.
*
* @since 1.0.0
*
* @param string $hook Frontend hook name, e.g. 'wp_head', 'wp_footer', etc.
* @return string Output the hook generates.
*/
final protected function get_frontend_hook_output( $hook ) {
$current_user_id = get_current_user_id();
// Unset current user to make WordPress behave as if nobody was logged in.
wp_set_current_user( false );
ob_start();
do_action( $hook );
$output = ob_get_clean();
// Restore the current user.
wp_set_current_user( $current_user_id );
return $output;
}
/**
* Gets the Google client the module uses.
*
* This method should be used to access the client.
*
* @since 1.0.0
* @since 1.2.0 Now returns Google_Site_Kit_Client instance.
* @since 1.35.0 Updated to be public.
*
* @return Google_Site_Kit_Client Google client instance.
*
* @throws Exception Thrown when the module did not correctly set up the client.
*/
final public function get_client() {
if ( null === $this->google_client ) {
$client = $this->setup_client();
if ( ! $client instanceof Google_Site_Kit_Client ) {
throw new Exception( __( 'Google client not set up correctly.', 'google-site-kit' ) );
}
$this->google_client = $client;
}
return $this->google_client;
}
/**
* Gets the oAuth client instance to use for the given datapoint.
*
* @since 1.77.0
*
* @param Datapoint $datapoint Datapoint definition.
* @return OAuth_Client OAuth_Client instance.
*/
private function get_oauth_client_for_datapoint( Datapoint $datapoint ) {
if (
$this instanceof Module_With_Owner
&& $this->is_shareable()
&& $datapoint->is_shareable()
&& $this->get_owner_id() !== get_current_user_id()
&& ! $this->is_recoverable()
&& current_user_can( Permissions::READ_SHARED_MODULE_DATA, $this->slug )
) {
$oauth_client = $this->get_owner_oauth_client();
try {
$this->validate_base_scopes( $oauth_client );
return $oauth_client;
} catch ( Exception $exception ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
// Fallthrough to default oauth client if scopes are unsatisfied.
}
}
return $this->authentication->get_oauth_client();
}
/**
* Gets the Google service for the given identifier.
*
* This method should be used to access Google services.
*
* @since 1.0.0
*
* @param string $identifier Identifier for the service.
* @return Google_Service Google service instance.
*
* @throws Exception Thrown when the module did not correctly set up the services or when the identifier is invalid.
*/
final protected function get_service( $identifier ) {
if ( null === $this->google_services ) {
$services = $this->setup_services( $this->get_client() );
if ( ! is_array( $services ) ) {
throw new Exception( __( 'Google services not set up correctly.', 'google-site-kit' ) );
}
foreach ( $services as $service ) {
if ( ! $service instanceof Google_Service ) {
throw new Exception( __( 'Google services not set up correctly.', 'google-site-kit' ) );
}
}
$this->google_services = $services;
}
if ( ! isset( $this->google_services[ $identifier ] ) ) {
/* translators: %s: service identifier */
throw new Exception( sprintf( __( 'Google service identified by %s does not exist.', 'google-site-kit' ), $identifier ) );
}
return $this->google_services[ $identifier ];
}
/**
* Sets up information about the module.
*
* @since 1.0.0
*
* @return array Associative array of module info.
*/
abstract protected function setup_info();
/**
* Sets up the Google client the module should use.
*
* This method is invoked once by {@see Module::get_client()} to lazily set up the client when it is requested
* for the first time.
*
* @since 1.0.0
* @since 1.2.0 Now returns Google_Site_Kit_Client instance.
*
* @return Google_Site_Kit_Client Google client instance.
*/
protected function setup_client() {
return $this->authentication->get_oauth_client()->get_client();
}
/**
* 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 ) {// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found
return array();
}
/**
* Sets whether or not to return raw requests and returns a callback to reset to the previous value.
*
* @since 1.2.0
*
* @param bool $defer Whether or not to return raw requests.
* @return callable Callback function that resets to the original $defer value.
*/
protected function with_client_defer( $defer ) {
return $this->get_client()->withDefer( $defer );
}
/**
* Parses information about the module.
*
* @since 1.0.0
*
* @param array $info Associative array of module info.
* @return array Parsed $info.
*/
private function parse_info( array $info ) {
$info = wp_parse_args(
$info,
array(
'slug' => '',
'name' => '',
'description' => '',
'order' => 10,
'homepage' => '',
'feature' => '',
'depends_on' => array(),
'force_active' => static::is_force_active(),
'internal' => false,
)
);
if ( empty( $info['name'] ) && ! empty( $info['slug'] ) ) {
$info['name'] = $info['slug'];
}
$info['depends_on'] = (array) $info['depends_on'];
return $info;
}
/**
* Transforms an exception into a WP_Error object.
*
* @since 1.0.0
* @since 1.49.0 Uses the new `Google_Proxy::setup_url_v2` method when the `serviceSetupV2` feature flag is enabled.
* @since 1.70.0 $datapoint parameter is optional.
*
* @param Exception $e Exception object.
* @param string $datapoint Optional. Datapoint originally requested. Default is an empty string.
* @return WP_Error WordPress error object.
*/
protected function exception_to_error( Exception $e, $datapoint = '' ) { // phpcs:ignore phpcs:enable Generic.CodeAnalysis.UnusedFunctionParameter.Found,Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
if ( $e instanceof WP_Errorable ) {
return $e->to_wp_error();
}
$code = $e->getCode();
$message = $e->getMessage();
$status = is_numeric( $code ) && $code ? (int) $code : 500;
$reason = '';
$reconnect_url = '';
if ( $e instanceof Google_Service_Exception ) {
$errors = $e->getErrors();
if ( isset( $errors[0]['message'] ) ) {
$message = $errors[0]['message'];
}
if ( isset( $errors[0]['reason'] ) ) {
$reason = $errors[0]['reason'];
}
} elseif ( $e instanceof Google_Proxy_Code_Exception ) {
$status = 401;
$code = $message;
$auth_client = $this->authentication->get_oauth_client();
$message = $auth_client->get_error_message( $code );
$google_proxy = $this->authentication->get_google_proxy();
$credentials = $this->authentication->credentials()->get();
$params = array(
'code' => $e->getAccessCode(),
'site_id' => ! empty( $credentials['oauth2_client_id'] ) ? $credentials['oauth2_client_id'] : '',
);
$params = $google_proxy->add_setup_step_from_error_code( $params, $code );
$reconnect_url = $google_proxy->setup_url( $params );
}
if ( empty( $code ) ) {
$code = 'unknown';
}
$data = array(
'status' => $status,
'reason' => $reason,
);
if ( ! empty( $reconnect_url ) ) {
$data['reconnectURL'] = $reconnect_url;
}
return new WP_Error( $code, $message, $data );
}
/**
* Parses the string list into an array of strings.
*
* @since 1.15.0
*
* @param string|array $items Items to parse.
* @return array An array of string items.
*/
protected function parse_string_list( $items ) {
if ( is_string( $items ) ) {
$items = explode( ',', $items );
}
if ( ! is_array( $items ) || empty( $items ) ) {
return array();
}
$items = array_map(
function ( $item ) {
if ( ! is_string( $item ) ) {
return false;
}
$item = trim( $item );
if ( empty( $item ) ) {
return false;
}
return $item;
},
$items
);
$items = array_filter( $items );
$items = array_values( $items );
return $items;
}
/**
* Determines whether the current request is for shared data.
*
* @since 1.98.0
*
* @param Data_Request $data Data request object.
* @return bool TRUE if the request is for shared data, otherwise FALSE.
*/
protected function is_shared_data_request( Data_Request $data ) {
$datapoint = $this->get_datapoint_definition( "{$data->method}:{$data->datapoint}" );
$oauth_client = $this->get_oauth_client_for_datapoint( $datapoint );
if ( $this->authentication->get_oauth_client() !== $oauth_client ) {
return true;
}
return false;
}
/**
* Determines whether the current module is forced to be active or not.
*
* @since 1.49.0
*
* @return bool TRUE if the module forced to be active, otherwise FALSE.
*/
public static function is_force_active() {
return false;
}
/**
* Checks whether the module is shareable.
*
* @since 1.50.0
*
* @return bool True if module is shareable, false otherwise.
*/
public function is_shareable() {
if ( $this instanceof Module_With_Owner && $this->is_connected() ) {
$datapoints = $this->get_datapoint_definitions();
foreach ( $datapoints as $datapoint ) {
if ( $datapoint instanceof Shareable_Datapoint ) {
return $datapoint->is_shareable();
}
if ( ! empty( $datapoint['shareable'] ) ) {
return true;
}
}
}
return false;
}
/**
* Checks whether the module is recoverable.
*
* @since 1.78.0
*
* @return bool
*/
public function is_recoverable() {
/**
* Filters the recoverable status of the module.
*
* @since 1.78.0
* @param bool $_ Whether or not the module is recoverable. Default: false
* @param string $slug Module slug.
*/
return (bool) apply_filters( 'googlesitekit_is_module_recoverable', false, $this->slug );
}
}

View File

@@ -0,0 +1,67 @@
<?php
/**
* Class Google\Site_Kit\Core\Modules\Module_Registry
*
* @package Google\Site_Kit\Core\Modules
* @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\Core\Modules;
use InvalidArgumentException;
/**
* Class for managing module registration.
*
* @since 1.21.0
* @access private
* @ignore
*/
class Module_Registry {
/**
* Registered modules.
*
* @since 1.21.0
* @var array
*/
private $registry = array();
/**
* Registers a module class on the registry.
*
* @since 1.21.0
*
* @param string $module_classname Fully-qualified module class name to register.
* @throws InvalidArgumentException Thrown if an invalid module class name is provided.
*/
public function register( $module_classname ) {
if ( ! is_string( $module_classname ) || ! $module_classname ) {
throw new InvalidArgumentException( 'A module class name is required to register a module.' );
}
if ( ! class_exists( $module_classname ) ) {
throw new InvalidArgumentException( "No class exists for '$module_classname'" );
}
if ( ! is_subclass_of( $module_classname, Module::class ) ) {
throw new InvalidArgumentException(
sprintf( 'All module classes must extend the base module class: %s', Module::class )
);
}
$this->registry[ $module_classname ] = $module_classname;
}
/**
* Gets all registered module class names.
*
* @since 1.21.0
*
* @return string[] Registered module class names.
*/
public function get_all() {
return array_keys( $this->registry );
}
}

View File

@@ -0,0 +1,99 @@
<?php
/**
* Class Google\Site_Kit\Core\Modules\Module_Settings
*
* @package Google\Site_Kit\Core\Modules
* @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\Core\Modules;
use Google\Site_Kit\Core\Storage\Setting;
/**
* Base class for module settings.
*
* @since 1.2.0
* @access private
* @ignore
*/
abstract class Module_Settings extends Setting {
/**
* Registers the setting in WordPress.
*
* @since 1.2.0
*/
public function register() {
parent::register();
$this->add_option_default_filters();
}
/**
* Merges an array of settings to update.
*
* Only existing keys will be updated.
*
* @since 1.3.0
*
* @param array $partial Partial settings array to save.
*
* @return bool True on success, false on failure.
*/
public function merge( array $partial ) {
$settings = $this->get();
$partial = array_filter(
$partial,
function ( $value ) {
return null !== $value;
}
);
$updated = array_intersect_key( $partial, $settings );
return $this->set( array_merge( $settings, $updated ) );
}
/**
* Registers a filter to ensure default values are present in the saved option.
*
* @since 1.2.0
*/
protected function add_option_default_filters() {
add_filter(
'option_' . static::OPTION,
function ( $option ) {
if ( ! is_array( $option ) ) {
return $this->get_default();
}
return $option;
},
0
);
// Fill in any missing keys with defaults.
// Must run later to not conflict with legacy key migration.
add_filter(
'option_' . static::OPTION,
function ( $option ) {
if ( is_array( $option ) ) {
return $option + $this->get_default();
}
return $option;
},
99
);
}
/**
* Gets the expected value type.
*
* @since 1.2.0
*
* @return string The type name.
*/
protected function get_type() {
return 'object';
}
}

View File

@@ -0,0 +1,263 @@
<?php
/**
* Class Google\Site_Kit\Core\Modules\Module_Sharing_Settings
*
* @package Google\Site_Kit\Core\Modules
* @copyright 2022 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;
use Google\Site_Kit\Core\Storage\Setting;
use Google\Site_Kit\Core\Util\Sanitize;
/**
* Class for module sharing settings.
*
* @since 1.50.0
* @access private
* @ignore
*/
class Module_Sharing_Settings extends Setting {
const OPTION = 'googlesitekit_dashboard_sharing';
/**
* Gets the default value.
*
* @since 1.50.0
*
* @return array
*/
protected function get_default() {
return array();
}
/**
* Gets the expected value type.
*
* @since 1.50.0
*
* @return string The type name.
*/
protected function get_type() {
return 'object';
}
/**
* Gets the callback for sanitizing the setting's value before saving.
*
* @since 1.50.0
*
* @return callable Callback method that filters or type casts invalid setting values.
*/
protected function get_sanitize_callback() {
return function ( $option ) {
if ( ! is_array( $option ) ) {
return array();
}
$sanitized_option = array();
foreach ( $option as $module_slug => $sharing_settings ) {
$sanitized_option[ $module_slug ] = array();
if ( isset( $sharing_settings['sharedRoles'] ) ) {
$filtered_shared_roles = $this->filter_shared_roles( Sanitize::sanitize_string_list( $sharing_settings['sharedRoles'] ) );
$sanitized_option[ $module_slug ]['sharedRoles'] = $filtered_shared_roles;
}
if ( isset( $sharing_settings['management'] ) ) {
$sanitized_option[ $module_slug ]['management'] = (string) $sharing_settings['management'];
}
}
return $sanitized_option;
};
}
/**
* Filters the shared roles to only include roles with the edit_posts capability.
*
* @since 1.85.0.
*
* @param array $shared_roles The shared roles list.
* @return string[] The sanitized shared roles list.
*/
private function filter_shared_roles( array $shared_roles ) {
$filtered_shared_roles = array_filter(
$shared_roles,
function ( $role_slug ) {
$role = get_role( $role_slug );
if ( empty( $role ) || ! $role->has_cap( 'edit_posts' ) ) {
return false;
}
return true;
}
);
return array_values( $filtered_shared_roles );
}
/**
* Gets the settings after filling in default values.
*
* @since 1.50.0
*
* @return array Value set for the option, or registered default if not set.
*/
public function get() {
$settings = parent::get();
foreach ( $settings as $module_slug => $sharing_settings ) {
if ( ! isset( $sharing_settings['sharedRoles'] ) || ! is_array( $sharing_settings['sharedRoles'] ) ) {
$settings[ $module_slug ]['sharedRoles'] = array();
}
if ( ! isset( $sharing_settings['management'] ) || ! in_array( $sharing_settings['management'], array( 'all_admins', 'owner' ), true ) ) {
$settings[ $module_slug ]['management'] = 'owner';
}
if ( isset( $sharing_settings['sharedRoles'] ) && is_array( $sharing_settings['sharedRoles'] ) ) {
$filtered_shared_roles = $this->filter_shared_roles( $sharing_settings['sharedRoles'] );
$settings[ $module_slug ]['sharedRoles'] = $filtered_shared_roles;
}
}
return $settings;
}
/**
* Merges a partial Module_Sharing_Settings option array into existing sharing settings.
*
* @since 1.75.0
* @since 1.77.0 Removed capability checks.
*
* @param array $partial Partial settings array to update existing settings with.
*
* @return bool True if sharing settings option was updated, false otherwise.
*/
public function merge( array $partial ) {
$settings = $this->get();
$partial = array_filter(
$partial,
function ( $value ) {
return ! empty( $value );
}
);
return $this->set( $this->array_merge_deep( $settings, $partial ) );
}
/**
* Gets the sharing settings for a given module, or the defaults.
*
* @since 1.95.0
*
* @param string $slug Module slug.
* @return array {
* Sharing settings for the given module.
* Default sharing settings do not grant any access so they
* are safe to return for a non-existent or non-shareable module.
*
* @type array $sharedRoles A list of WP Role IDs that the module is shared with.
* @type string $management Which users can manage the sharing settings.
* }
*/
public function get_module( $slug ) {
$settings = $this->get();
if ( isset( $settings[ $slug ] ) ) {
return $settings[ $slug ];
}
return array(
'sharedRoles' => array(),
'management' => 'owner',
);
}
/**
* Unsets the settings for a given module.
*
* @since 1.68.0
*
* @param string $slug Module slug.
*/
public function unset_module( $slug ) {
$settings = $this->get();
if ( isset( $settings[ $slug ] ) ) {
unset( $settings[ $slug ] );
$this->set( $settings );
}
}
/**
* Gets the combined roles that are set as shareable for all modules.
*
* @since 1.69.0
*
* @return array Combined array of shared roles for all modules.
*/
public function get_all_shared_roles() {
$shared_roles = array();
$settings = $this->get();
foreach ( $settings as $sharing_settings ) {
if ( ! isset( $sharing_settings['sharedRoles'] ) ) {
continue;
}
$shared_roles = array_merge( $shared_roles, $sharing_settings['sharedRoles'] );
}
return array_unique( $shared_roles );
}
/**
* Gets the shared roles for the given module slug.
*
* @since 1.69.0
*
* @param string $slug Module slug.
* @return array list of shared roles for the module, otherwise an empty list.
*/
public function get_shared_roles( $slug ) {
$settings = $this->get();
if ( isset( $settings[ $slug ]['sharedRoles'] ) ) {
return $settings[ $slug ]['sharedRoles'];
}
return array();
}
/**
* Merges two arrays recursively to a specific depth.
*
* When array1 and array2 have the same string keys, it overwrites
* the elements of array1 with elements of array2. Otherwise, it adds/appends
* elements of array2.
*
* @since 1.77.0
*
* @param array $array1 First array.
* @param array $array2 Second array.
* @param int $depth Optional. Depth to merge to. Default is 1.
*
* @return array Merged array.
*/
private function array_merge_deep( $array1, $array2, $depth = 1 ) {
foreach ( $array2 as $key => $value ) {
if ( $depth > 0 && is_array( $value ) ) {
$array1_key = isset( $array1[ $key ] ) ? $array1[ $key ] : null;
$array1[ $key ] = $this->array_merge_deep( $array1_key, $value, $depth - 1 );
} else {
$array1[ $key ] = $value;
}
}
return $array1;
}
}

View File

@@ -0,0 +1,27 @@
<?php
/**
* Interface Google\Site_Kit\Core\Modules\Module_With_Activation
*
* @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\Core\Modules;
/**
* Interface for a module that has additional behavior when activated.
*
* @since 1.36.0
* @access private
* @ignore
*/
interface Module_With_Activation {
/**
* Handles module activation.
*
* @since 1.36.0
*/
public function on_activation();
}

View File

@@ -0,0 +1,42 @@
<?php
/**
* Interface Google\Site_Kit\Core\Modules\Module_With_Assets
*
* @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\Core\Modules;
use Google\Site_Kit\Core\Assets\Asset;
/**
* Interface for a module that includes assets.
*
* @since 1.7.0
* @access private
* @ignore
*/
interface Module_With_Assets {
/**
* Gets the assets to register for the module.
*
* @since 1.7.0
*
* @return Asset[] List of Asset objects.
*/
public function get_assets();
/**
* Enqueues all assets necessary for the module.
*
* @since 1.7.0
* @since 1.37.0 Added the $asset_context argument.
*
* @param string $asset_context Context for page, see `Asset::CONTEXT_*` constants.
*/
public function enqueue_assets( $asset_context = Asset::CONTEXT_ADMIN_SITEKIT );
}

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