initial commit

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

View File

@@ -0,0 +1,513 @@
<?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.
*
* This mode is currently unused due to Tag Manager setup not showing the Web Container dropdown
* when AMP is in standard mode and some urls have AMP disabled.
*
* @since 1.0.0 Originally introduced.
* @since 1.36.0 Marked as unused, see description.
* @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.
*
* Network mode is active under the following conditions:
* * Multisite is enabled.
* * The plugin is network-active.
* * The site's domain matches the network's domain (which means it is a subdirectory site).
*
* @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;
}
$site = get_site( get_current_blog_id() );
$network = get_network( $site->network_id );
// Use network mode when the site's domain is the same as the network's domain.
return $network && $site->domain === $network->domain;
}
/**
* 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
*
* @return bool|string 'primary' if in standard mode,
* 'secondary' if in transitional or reader modes
* false if AMP not active, or unknown mode
*/
public function get_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;
}
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 (
in_array(
$mode,
array(
AMP_Theme_Support::STANDARD_MODE_SLUG,
AMP_Theme_Support::TRANSITIONAL_MODE_SLUG,
AMP_Theme_Support::READER_MODE_SLUG,
),
true
)
) {
return self::AMP_MODE_SECONDARY;
}
} elseif ( function_exists( 'amp_is_canonical' ) ) {
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,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,131 @@
<?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\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;
/**
* 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 );
}
/**
* Registers functionality through WordPress hooks.
*
* @since 1.0.0
*/
public function register() {
add_action(
'wp_dashboard_setup',
function () {
$this->add_widgets();
}
);
$wp_dashboard_callback = function() {
if ( 'dashboard' === get_current_screen()->id && current_user_can( Permissions::VIEW_DASHBOARD ) ) {
// Enqueue styles.
$this->assets->enqueue_asset( 'googlesitekit-wp-dashboard-css' );
// Enqueue scripts.
$this->assets->enqueue_asset( 'googlesitekit-wp-dashboard' );
$this->modules->enqueue_assets();
}
};
add_action( 'admin_enqueue_scripts', $wp_dashboard_callback, 30 );
}
/**
* Add a Site Kit by Google widget to the WordPress admin dashboard.
*
* @since 1.0.0
*/
private function add_widgets() {
// Only show the Dashboard Widget if the current user has access.
if ( ! current_user_can( Permissions::VIEW_DASHBOARD ) ) {
return;
}
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
*/
private function render_googlesitekit_wp_dashboard() {
$this->render_noscript_html();
?>
<div id="js-googlesitekit-wp-dashboard" class="googlesitekit-plugin"></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,72 @@
<?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,55 @@
<?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,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['render_callback'] || ! $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' );
if ( $this->args['enqueue_callback'] ) {
call_user_func( $this->args['enqueue_callback'], $assets );
}
}
/**
* Renders the screen content.
*
* @since 1.0.0
*
* @param Context $context Plugin context.
*/
private function render( Context $context ) {
if ( ! $this->args['render_callback'] ) {
return;
}
?>
<div class="googlesitekit-plugin">
<?php
$this->render_noscript_html();
call_user_func( $this->args['render_callback'], $context );
?>
</div>
<?php
}
}

View File

@@ -0,0 +1,476 @@
<?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\Modules\Modules;
use Google\Site_Kit\Core\Permissions\Permissions;
use Google\Site_Kit\Core\Util\Feature_Flags;
/**
* Class managing admin screens.
*
* @since 1.0.0
* @access private
* @ignore
*/
final class Screens {
const PREFIX = 'googlesitekit-';
/**
* 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;
/**
* 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.
*/
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 );
}
/**
* 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 module pages to dashboard if unifiedDashboard is enabled.
if ( Feature_Flags::enabled( 'unifiedDashboard' ) ) {
$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;
}
// Redirect to splash screen if user is allowed to authenticate.
if ( current_user_can( Permissions::AUTHENTICATE ) ) {
wp_safe_redirect(
$this->context->admin_url( 'splash' )
);
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::AUTHENTICATE ) ) {
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() {
$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-dashboard-details' );
} else {
$assets->enqueue_asset( 'googlesitekit-dashboard' );
}
},
'render_callback' => function( Context $context ) {
$setup_slug = $context->input()->filter( INPUT_GET, 'slug', FILTER_SANITIZE_STRING );
$reauth = $context->input()->filter( INPUT_GET, 'reAuth', FILTER_VALIDATE_BOOLEAN );
if ( $context->input()->filter( INPUT_GET, 'permaLink' ) ) {
?>
<div id="js-googlesitekit-dashboard-details" 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-dashboard" data-setup-module-slug="<?php echo esc_attr( $setup_module_slug ); ?>" class="googlesitekit-page"></div>
<?php
}
},
)
),
);
// Wrap this simply to save some unnecessary filter firing and screen instantiation.
if ( current_user_can( Permissions::VIEW_MODULE_DETAILS ) ) {
/**
* Filters the admin screens for modules.
*
* By default this is an empty array, but can be expanded.
*
* @since 1.0.0
*
* @param array $module_screens List of Screen instances.
*/
$module_screens = apply_filters( 'googlesitekit_module_screens', array() );
$screens = array_merge( $screens, $module_screens );
}
$screens[] = new Screen(
self::PREFIX . 'settings',
array(
'title' => __( 'Settings', 'google-site-kit' ),
'capability' => Permissions::MANAGE_OPTIONS,
'enqueue_callback' => function( Assets $assets ) {
$assets->enqueue_asset( 'googlesitekit-settings' );
},
'render_callback' => function( Context $context ) {
?>
<div id="googlesitekit-settings-wrapper" class="googlesitekit-page"></div>
<?php
},
)
);
$show_splash_in_menu = ! current_user_can( Permissions::VIEW_DASHBOARD ) && ! current_user_can( Permissions::VIEW_MODULE_DETAILS ) && ! current_user_can( Permissions::MANAGE_OPTIONS );
$screens[] = new Screen(
self::PREFIX . 'splash',
array(
'title' => __( 'Dashboard', 'google-site-kit' ),
'capability' => Permissions::AUTHENTICATE,
'parent_slug' => $show_splash_in_menu ? Screen::MENU_SLUG : null,
// This callback will redirect to the dashboard on successful authentication.
'initialize_callback' => function( Context $context ) {
$splash_context = $context->input()->filter( INPUT_GET, 'googlesitekit_context' );
$reset_session = $context->input()->filter( INPUT_GET, 'googlesitekit_reset_session', FILTER_VALIDATE_BOOLEAN );
$authentication = new Authentication( $context );
// If the user is authenticated, redirect them to the disconnect URL and then send them back here.
if ( ! $reset_session && 'revoked' === $splash_context && $authentication->is_authenticated() ) {
$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.
if ( $authentication->is_authenticated() ) {
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;
}
},
'enqueue_callback' => function( Assets $assets ) {
$assets->enqueue_asset( 'googlesitekit-dashboard-splash' );
},
'render_callback' => function( Context $context ) {
?>
<div id="js-googlesitekit-dashboard-splash" class="googlesitekit-page"></div>
<?php
},
)
);
$screens[] = new Screen(
self::PREFIX . 'user-input',
array(
'title' => __( 'User Input', 'google-site-kit' ),
'capability' => Permissions::MANAGE_OPTIONS,
'parent_slug' => null,
'enqueue_callback' => function( Assets $assets ) {
$assets->enqueue_asset( 'googlesitekit-user-input' );
},
'render_callback' => function( Context $context ) {
?>
<div id="js-googlesitekit-user-input" class="googlesitekit-page"></div>
<?php
},
)
);
return $screens;
}
}

View File

@@ -0,0 +1,123 @@
<?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 = $this->context->input()->filter( INPUT_GET, 'page', FILTER_SANITIZE_STRING );
$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,394 @@
<?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\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, 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;
/**
* 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 );
}
/**
* 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;
}
$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;
}
} else {
// Otherwise use more general permission check (typically admin-only).
if ( ! current_user_can( Permissions::VIEW_DASHBOARD ) ) {
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
*
* @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();
?>
<div class="googlesitekit-plugin ab-sub-wrapper">
<?php $this->render_noscript_html(); ?>
<div id="js-googlesitekit-adminbar" 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,73 @@
<?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,131 @@
<?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_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 );
}
}

View File

@@ -0,0 +1,934 @@
<?php
/**
* Class Google\Site_Kit\Core\Assets\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\Assets;
use Google\Site_Kit\Context;
use Google\Site_Kit\Core\Modules\Module_Sharing_Settings;
use Google\Site_Kit\Core\Permissions\Permissions;
use Google\Site_Kit\Core\Storage\Options;
use Google\Site_Kit\Core\Util\BC_Functions;
use Google\Site_Kit\Core\Util\Feature_Flags;
use WP_Dependencies;
/**
* Class managing assets.
*
* @since 1.0.0
* @access private
* @ignore
*/
final class Assets {
/**
* Plugin context.
*
* @since 1.0.0
* @var Context
*/
private $context;
/**
* Lazy-loaded assets as $handle => $instance pairs.
*
* @since 1.0.0
* @var array
*/
private $assets = array();
/**
* Internal flag for whether assets have been registered yet.
*
* @since 1.2.0
* @var bool
*/
private $assets_registered = false;
/**
* Internal list of print callbacks already done.
*
* @since 1.2.0
* @var array
*/
private $print_callbacks_done = array();
/**
* Constructor.
*
* @since 1.0.0
*
* @param Context $context Plugin context.
*/
public function __construct( Context $context ) {
$this->context = $context;
}
/**
* Registers functionality through WordPress hooks.
*
* @since 1.0.0
* @since 1.37.0 Enqueues Block Editor assets.
*/
public function register() {
$register_callback = function() {
if ( ! is_admin() ) {
return;
}
if ( $this->assets_registered ) {
return;
}
$this->assets_registered = true;
$this->register_assets();
};
add_action( 'admin_enqueue_scripts', $register_callback );
add_action( 'wp_enqueue_scripts', $register_callback );
add_filter(
'script_loader_tag',
function( $tag, $handle ) {
return $this->add_async_defer_attribute( $tag, $handle );
},
10,
2
);
// All other asset-related general logic should only be active when the
// current user can actually use Site Kit (which only is so if they can
// authenticate).
if ( ! current_user_can( Permissions::AUTHENTICATE ) ) {
return;
}
$this->add_amp_dev_mode_attributes( $this->get_assets() );
add_action(
'admin_enqueue_scripts',
function() {
$this->enqueue_minimal_admin_script();
}
);
add_action(
'admin_print_scripts-edit.php',
function() {
global $post_type;
if ( 'post' !== $post_type ) {
// For CONTEXT_ADMIN_POSTS we only load scripts for the 'post' post type.
return;
}
$assets = $this->get_assets();
array_walk(
$assets,
function( Asset $asset ) {
if ( $asset->has_context( Asset::CONTEXT_ADMIN_POSTS ) ) {
$this->enqueue_asset( $asset->get_handle() );
}
}
);
}
);
add_action(
'enqueue_block_editor_assets',
function() {
$assets = $this->get_assets();
array_walk(
$assets,
function( $asset ) {
if ( $asset->has_context( Asset::CONTEXT_ADMIN_POST_EDITOR ) ) {
$this->enqueue_asset( $asset->get_handle() );
}
}
);
}
);
$scripts_print_callback = function() {
$scripts = wp_scripts();
$this->run_before_print_callbacks( $scripts, $scripts->queue );
};
add_action( 'wp_print_scripts', $scripts_print_callback );
add_action( 'admin_print_scripts', $scripts_print_callback );
$styles_print_callback = function() {
$styles = wp_styles();
$this->run_before_print_callbacks( $styles, $styles->queue );
};
add_action( 'wp_print_styles', $styles_print_callback );
add_action( 'admin_print_styles', $styles_print_callback );
}
/**
* Enqueues the given plugin asset (script or stylesheet).
*
* The asset must already be registered in order to be enqueued.
*
* @since 1.0.0
*
* @param string $handle Asset handle.
*/
public function enqueue_asset( $handle ) {
// Register assets on-the-fly if necessary (currently the case for admin bar in frontend).
if ( ! $this->assets_registered ) {
$this->assets_registered = true;
$this->register_assets();
}
$assets = $this->get_assets();
if ( empty( $assets[ $handle ] ) ) {
return;
}
$assets[ $handle ]->enqueue();
}
/**
* Enqueues Google fonts.
*
* @since 1.0.0
* @deprecated 1.41.0 This method is no longer used as fonts are loaded as a normal style dependency now.
*/
public function enqueue_fonts() {
_deprecated_function( __METHOD__, '1.41.0' );
$assets = $this->get_assets();
if ( ! empty( $assets['googlesitekit-fonts'] ) && $assets['googlesitekit-fonts'] instanceof Asset ) {
$assets['googlesitekit-fonts']->enqueue();
}
}
/**
* Get Google fonts src for CSS.
*
* @since 1.41.0
*
* @return string String URL src.
*/
protected function get_fonts_src() {
$font_families = array(
'Google+Sans:300,300i,400,400i,500,500i,700,700i',
'Roboto:300,300i,400,400i,500,500i,700,700i',
);
$filtered_font_families = apply_filters( 'googlesitekit_font_families', $font_families );
if ( empty( $filtered_font_families ) ) {
return '';
}
return add_query_arg(
array(
'family' => implode( '|', $filtered_font_families ),
'subset' => 'latin-ext',
'display' => 'fallback',
),
'https://fonts.googleapis.com/css'
);
}
/**
* Registers all plugin assets.
*
* @since 1.0.0
*/
private function register_assets() {
$assets = $this->get_assets();
foreach ( $assets as $asset ) {
$asset->register( $this->context );
}
}
/**
* Add data-ampdevmode attributes to assets.
*
* @todo What about dependencies?
*
* @param Asset[] $assets Assets.
*/
private function add_amp_dev_mode_attributes( $assets ) {
add_filter(
'script_loader_tag',
function ( $tag, $handle ) use ( $assets ) {
// TODO: 'hoverintent-js' can be removed from here at some point, see https://github.com/ampproject/amp-wp/pull/3928.
if ( $this->context->is_amp() && ( isset( $assets[ $handle ] ) && $assets[ $handle ] instanceof Script || 'hoverintent-js' === $handle ) ) {
$tag = preg_replace( '/(?<=<script)(?=\s|>)/i', ' data-ampdevmode', $tag );
}
return $tag;
},
10,
2
);
add_filter(
'style_loader_tag',
function ( $tag, $handle ) use ( $assets ) {
if ( $this->context->is_amp() && isset( $assets[ $handle ] ) && $assets[ $handle ] instanceof Stylesheet ) {
$tag = preg_replace( '/(?<=<link)(?=\s|>)/i', ' data-ampdevmode', $tag );
}
return $tag;
},
10,
2
);
}
/**
* Enqueues the minimal admin script for the entire admin.
*
* @since 1.0.0
*/
private function enqueue_minimal_admin_script() {
$this->enqueue_asset( 'googlesitekit-base' );
}
/**
* Gets all plugin assets.
*
* The method will lazy-load assets in an internal property so that the processing only happens once.
*
* @since 1.0.0
*
* @return Asset[] Associative array of asset $handle => $instance pairs.
*/
private function get_assets() {
if ( $this->assets ) {
return $this->assets;
}
$base_url = $this->context->url( 'dist/assets/' );
$dependencies = array(
'googlesitekit-runtime',
'googlesitekit-i18n',
'googlesitekit-vendor',
'googlesitekit-commons',
'googlesitekit-base',
'googlesitekit-data',
'googlesitekit-datastore-forms',
'googlesitekit-datastore-location',
'googlesitekit-datastore-site',
'googlesitekit-datastore-user',
'googlesitekit-datastore-ui',
'googlesitekit-widgets',
);
$dependencies_for_dashboard_sharing = Feature_Flags::enabled( 'dashboardSharing' )
? array_merge( $dependencies, array( 'googlesitekit-dashboard-sharing-data' ) )
: $dependencies;
// Register plugin scripts.
$assets = array(
new Script_Data(
'googlesitekit-commons',
array(
'global' => '_googlesitekitLegacyData',
'data_callback' => function () {
return $this->get_inline_data();
},
)
),
new Script_Data(
'googlesitekit-base-data',
array(
'global' => '_googlesitekitBaseData',
'data_callback' => function () {
return $this->get_inline_base_data();
},
)
),
new Script_Data(
'googlesitekit-entity-data',
array(
'global' => '_googlesitekitEntityData',
'data_callback' => function () {
return $this->get_inline_entity_data();
},
)
),
new Script_Data(
'googlesitekit-user-data',
array(
'global' => '_googlesitekitUserData',
'data_callback' => function() {
return $this->get_inline_user_data();
},
)
),
new Script_Data(
'googlesitekit-apifetch-data',
array(
'global' => '_googlesitekitAPIFetchData',
'data_callback' => function () {
/**
* Preload common data by specifying an array of REST API paths that will be preloaded.
*
* Filters the array of paths that will be preloaded.
*
* @since 1.7.0
*
* @param array $preload_paths Array of paths to preload.
*/
$preload_paths = apply_filters( 'googlesitekit_apifetch_preload_paths', array() );
$preloaded = array_reduce(
array_unique( $preload_paths ),
array( BC_Functions::class, 'rest_preload_api_request' ),
array()
);
return array(
'nonce' => ( wp_installing() && ! is_multisite() ) ? '' : wp_create_nonce( 'wp_rest' ),
'nonceEndpoint' => admin_url( 'admin-ajax.php?action=rest-nonce' ),
'preloadedData' => $preloaded,
'rootURL' => esc_url_raw( get_rest_url() ),
);
},
)
),
new Script_Data(
'googlesitekit-dashboard-sharing-data',
array(
'global' => '_googlesitekitDashboardSharingData',
'data_callback' => function() {
return $this->get_inline_dashboard_sharing_data();
},
)
),
new Script(
'googlesitekit-runtime',
array(
'src' => $base_url . 'js/runtime.js',
)
),
new Script(
'googlesitekit-polyfills',
array(
'src' => $base_url . 'js/googlesitekit-polyfills.js',
'dependencies' => array(
'googlesitekit-base-data',
),
)
),
new Script(
'googlesitekit-i18n',
array(
'src' => $base_url . 'js/googlesitekit-i18n.js',
)
),
new Script(
'googlesitekit-vendor',
array(
'src' => $base_url . 'js/googlesitekit-vendor.js',
'dependencies' => array(
'googlesitekit-i18n',
'googlesitekit-runtime',
'googlesitekit-polyfills',
),
)
),
// Admin assets.
new Script(
'googlesitekit-activation',
array(
'src' => $base_url . 'js/googlesitekit-activation.js',
'dependencies' => $dependencies,
)
),
new Script(
'googlesitekit-base',
array(
'src' => $base_url . 'js/googlesitekit-base.js',
'dependencies' => array(
'googlesitekit-base-data',
'googlesitekit-i18n',
),
'execution' => 'defer',
)
),
// Begin JSR Assets.
new Script(
'googlesitekit-api',
array(
'src' => $base_url . 'js/googlesitekit-api.js',
'dependencies' => array(
'googlesitekit-vendor',
'googlesitekit-apifetch-data',
),
)
),
new Script(
'googlesitekit-data',
array(
'src' => $base_url . 'js/googlesitekit-data.js',
'dependencies' => array(
'googlesitekit-vendor',
'googlesitekit-api',
),
)
),
new Script(
'googlesitekit-datastore-user',
array(
'src' => $base_url . 'js/googlesitekit-datastore-user.js',
'dependencies' => array(
'googlesitekit-data',
'googlesitekit-api',
'googlesitekit-user-data',
),
)
),
new Script(
'googlesitekit-datastore-location',
array(
'src' => $base_url . 'js/googlesitekit-datastore-location.js',
'dependencies' => array(
'googlesitekit-vendor',
'googlesitekit-data',
),
)
),
new Script(
'googlesitekit-datastore-site',
array(
'src' => $base_url . 'js/googlesitekit-datastore-site.js',
'dependencies' => array(
'googlesitekit-vendor',
'googlesitekit-api',
'googlesitekit-data',
'googlesitekit-base-data',
'googlesitekit-entity-data',
),
)
),
new Script(
'googlesitekit-datastore-forms',
array(
'src' => $base_url . 'js/googlesitekit-datastore-forms.js',
'dependencies' => array(
'googlesitekit-data',
),
)
),
new Script(
'googlesitekit-datastore-ui',
array(
'src' => $base_url . 'js/googlesitekit-datastore-ui.js',
'dependencies' => array(
'googlesitekit-data',
),
)
),
new Script(
'googlesitekit-modules',
array(
'src' => $base_url . 'js/googlesitekit-modules.js',
'dependencies' => array(
'googlesitekit-vendor',
'googlesitekit-api',
'googlesitekit-data',
'googlesitekit-datastore-site',
'googlesitekit-datastore-user',
),
)
),
new Script(
'googlesitekit-widgets',
array(
'src' => $base_url . 'js/googlesitekit-widgets.js',
'dependencies' => array(
'googlesitekit-data',
'googlesitekit-i18n',
),
)
),
new Script(
'googlesitekit-user-input',
array(
'src' => $base_url . 'js/googlesitekit-user-input.js',
'dependencies' => $dependencies,
)
),
// End JSR Assets.
new Script(
'googlesitekit-dashboard-splash',
array(
'src' => $base_url . 'js/googlesitekit-dashboard-splash.js',
'dependencies' => $dependencies,
)
),
new Script(
'googlesitekit-dashboard-details',
array(
'src' => $base_url . 'js/googlesitekit-dashboard-details.js',
'dependencies' => $dependencies_for_dashboard_sharing,
)
),
new Script(
'googlesitekit-dashboard',
array(
'src' => $base_url . 'js/googlesitekit-dashboard.js',
'dependencies' => $dependencies_for_dashboard_sharing,
)
),
new Script(
'googlesitekit-module',
array(
'src' => $base_url . 'js/googlesitekit-module.js',
'dependencies' => $dependencies,
)
),
new Script(
'googlesitekit-settings',
array(
'src' => $base_url . 'js/googlesitekit-settings.js',
'dependencies' => $dependencies,
)
),
new Stylesheet(
'googlesitekit-admin-css',
array(
'src' => $base_url . 'css/googlesitekit-admin-css.css',
'dependencies' => array(
'googlesitekit-fonts',
),
)
),
// WP Dashboard assets.
new Script(
'googlesitekit-wp-dashboard',
array(
'src' => $base_url . 'js/googlesitekit-wp-dashboard.js',
'dependencies' => $dependencies,
'execution' => 'defer',
)
),
new Stylesheet(
'googlesitekit-wp-dashboard-css',
array(
'src' => $base_url . 'css/googlesitekit-wp-dashboard-css.css',
'dependencies' => array(
'googlesitekit-fonts',
),
)
),
// Admin bar assets.
new Script(
'googlesitekit-adminbar',
array(
'src' => $base_url . 'js/googlesitekit-adminbar.js',
'dependencies' => $dependencies,
'execution' => 'defer',
)
),
new Stylesheet(
'googlesitekit-adminbar-css',
array(
'src' => $base_url . 'css/googlesitekit-adminbar-css.css',
'dependencies' => array(
'googlesitekit-fonts',
),
)
),
new Stylesheet(
'googlesitekit-fonts',
array(
'src' => $this->get_fonts_src(),
'version' => null,
)
),
);
/**
* Filters the list of assets that Site Kit should register.
*
* This filter covers both scripts and stylesheets.
*
* @since 1.7.0
*
* @param Asset[] $assets List of Asset objects.
*/
$assets = apply_filters( 'googlesitekit_assets', $assets );
$this->assets = array();
foreach ( $assets as $asset ) {
$this->assets[ $asset->get_handle() ] = $asset;
}
return $this->assets;
}
/**
* Gets the most basic inline data needed for JS files.
*
* This should not include anything remotely expensive to compute.
*
* @since 1.2.0
*
* @return array The base inline data to be output.
*/
private function get_inline_base_data() {
global $wpdb;
$site_url = $this->context->get_reference_site_url();
$current_user = wp_get_current_user();
$inline_data = array(
'homeURL' => trailingslashit( $this->context->get_canonical_home_url() ),
'referenceSiteURL' => esc_url_raw( trailingslashit( $site_url ) ),
'userIDHash' => md5( $site_url . $current_user->ID ),
'adminURL' => esc_url_raw( trailingslashit( admin_url() ) ),
'assetsURL' => esc_url_raw( $this->context->url( 'dist/assets/' ) ),
'blogPrefix' => $wpdb->get_blog_prefix(),
'ampMode' => $this->context->get_amp_mode(),
'isNetworkMode' => $this->context->is_network_mode(),
'timezone' => get_option( 'timezone_string' ),
'siteName' => get_bloginfo( 'name' ),
'enabledFeatures' => Feature_Flags::get_enabled_features(),
'webStoriesActive' => defined( 'WEBSTORIES_VERSION' ),
);
/**
* Filters the most basic inline data to pass to JS.
*
* This should not include anything remotely expensive to compute.
*
* @since 1.2.0
*
* @param array $data Base data.
*/
return apply_filters( 'googlesitekit_inline_base_data', $inline_data );
}
/**
* Gets the inline data specific to the current entity.
*
* @since 1.7.0
*
* @return array The site inline data to be output.
*/
private function get_inline_entity_data() {
$current_entity = $this->context->get_reference_entity();
return array(
'currentEntityURL' => $current_entity ? $current_entity->get_url() : null,
'currentEntityType' => $current_entity ? $current_entity->get_type() : null,
'currentEntityTitle' => $current_entity ? $current_entity->get_title() : null,
'currentEntityID' => $current_entity ? $current_entity->get_id() : null,
);
}
/**
* Gets the inline data specific to the current user
*
* @since 1.9.0
*
* @return array The user inline data to be output.
*/
private function get_inline_user_data() {
$current_user = wp_get_current_user();
$inline_data = array(
'user' => array(
'id' => $current_user->ID,
'email' => $current_user->user_email,
'name' => $current_user->display_name,
'picture' => get_avatar_url( $current_user->user_email ),
'roles' => $current_user->roles,
),
);
/**
* Filters the user inline data to pass to JS.
*
* This should not include anything remotely expensive to compute.
*
* @since 1.9.0
*
* @param array $data User data.
*/
return apply_filters( 'googlesitekit_user_data', $inline_data );
}
/**
* Gets the inline dashboard sharing data
*
* @since 1.49.0
*
* @return array The dashboard sharing inline data to be output.
*/
private function get_inline_dashboard_sharing_data() {
$all_roles = wp_roles()->roles;
$inline_data = array( 'roles' => array() );
foreach ( $all_roles as $role_slug => $role_details ) {
$role = get_role( $role_slug );
// Filter the role that has `edit_posts` capability.
if ( $role->has_cap( 'edit_posts' ) ) {
$inline_data['roles'][] = array(
'id' => $role_slug,
'displayName' => translate_user_role( $role_details['name'] ),
);
}
}
$settings = new Module_Sharing_Settings( new Options( $this->context ) );
$inline_data['settings'] = $settings->get();
/**
* Filters the dashboard sharing inline data to pass to JS.
*
* @since 1.49.0
*
* @param array $data dashboard sharing data.
*/
return apply_filters( 'googlesitekit_dashboard_sharing_data', $inline_data );
}
/**
* Gets the inline data needed for core plugin scripts.
*
* @since 1.0.0
*
* @return array The inline data to be output.
*/
private function get_inline_data() {
$current_user = wp_get_current_user();
$site_url = $this->context->get_reference_site_url();
$input = $this->context->input();
$page = $input->filter( INPUT_GET, 'page', FILTER_SANITIZE_STRING );
$admin_data = array(
'siteURL' => esc_url_raw( $site_url ),
'resetSession' => $input->filter( INPUT_GET, 'googlesitekit_reset_session', FILTER_VALIDATE_BOOLEAN ),
);
$current_entity = $this->context->get_reference_entity();
return array(
/**
* Filters the admin data to pass to JS.
*
* @since 1.0.0
*
* @param array $data Admin data.
*/
'admin' => apply_filters( 'googlesitekit_admin_data', $admin_data ),
'locale' => $this->context->get_locale( 'user' ),
/**
* Filters the setup data to pass to JS, needed during the dashboard page load.
*
* Get the setup data from the options table.
*
* @since 1.0.0
*
* @param array $data Authentication Data.
*/
'setup' => apply_filters( 'googlesitekit_setup_data', array() ),
);
}
/**
* Adds support for async and defer attributes to enqueued scripts.
*
* @since 1.0.0
*
* @param string $tag The script tag.
* @param string $handle The script handle.
* @return string Modified script tag.
*/
private function add_async_defer_attribute( $tag, $handle ) {
$script_execution = wp_scripts()->get_data( $handle, 'script_execution' );
if ( ! $script_execution ) {
return $tag;
}
if ( 'async' !== $script_execution && 'defer' !== $script_execution ) {
return $tag;
}
// Abort adding async/defer for scripts that have this script as a dependency.
foreach ( wp_scripts()->registered as $script ) {
if ( in_array( $handle, $script->deps, true ) ) {
return $tag;
}
}
// Add the attribute if it hasn't already been added.
if ( ! preg_match( ":\s$script_execution(=|>|\s):", $tag ) ) {
$tag = preg_replace( ':(?=></script>):', " $script_execution", $tag, 1 );
}
return $tag;
}
/**
* Executes all extra callbacks before printing a list of dependencies.
*
* This method ensures that such callbacks that run e.g. `wp_add_inline_script()` are executed just-in-time,
* only when the asset is actually loaded in the current request.
*
* This method works recursively, also looking at dependencies, and supports both scripts and stylesheets.
*
* @since 1.2.0
*
* @param WP_Dependencies $dependencies WordPress dependencies class instance.
* @param array $handles List of handles to run before print callbacks for.
*/
private function run_before_print_callbacks( WP_Dependencies $dependencies, array $handles ) {
$is_amp = $this->context->is_amp();
foreach ( $handles as $handle ) {
if ( isset( $this->print_callbacks_done[ $handle ] ) ) {
continue;
}
$this->print_callbacks_done[ $handle ] = true;
if ( isset( $this->assets[ $handle ] ) ) {
$this->assets[ $handle ]->before_print();
// TODO: This can be removed at some point, see https://github.com/ampproject/amp-wp/pull/4001.
if ( $is_amp && $this->assets[ $handle ] instanceof Script ) {
$this->add_extra_script_amp_dev_mode( $handle );
}
}
if ( isset( $dependencies->registered[ $handle ] ) && is_array( $dependencies->registered[ $handle ]->deps ) ) {
$this->run_before_print_callbacks( $dependencies, $dependencies->registered[ $handle ]->deps );
}
}
}
/**
* Adds a comment to all extra scripts so that they are considered compatible with AMP dev mode.
*
* {@see Assets::add_amp_dev_mode_attributes()} makes all registered scripts and stylesheets compatible, including
* their potential inline additions. This method does the same for extra scripts, which are registered under the
* 'data' key.
*
* @since 1.4.0
*
* @param string $handle The handle of a registered script.
*/
private function add_extra_script_amp_dev_mode( $handle ) {
$data = wp_scripts()->get_data( $handle, 'data' ) ?: '';
if ( ! empty( $data ) && is_string( $data ) ) {
wp_scripts()->add_data( $handle, 'data', '/*googlesitekit*/ ' . $data );
}
}
}

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,131 @@
<?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 ) {
list( $filename, $hash ) = Manifest::get( $this->handle );
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 );
}
/**
* Sets locale data for the script, if it has translations.
*
* @since 1.21.0
*/
private function set_locale_data() {
$json_translations = BC_Functions::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 );
}
}

View File

@@ -0,0 +1,162 @@
<?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 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 ) );
// 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.
$http_client = $client->getHttpClient();
$http_client->setDefaultOption( '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 ) {
$http_client->setDefaultOption( 'verify', ABSPATH . WPINC . '/certificates/ca-bundle.crt' );
} else {
$http_client->setDefaultOption( 'verify', false );
}
// Configure the Google_Client's HTTP client to use to use the same HTTP proxy as WordPress HTTP, if set.
$http_proxy = new WP_HTTP_Proxy();
if ( $http_proxy->is_enabled() ) {
// See http://docs.guzzlephp.org/en/5.3/clients.html#proxy for reference.
$auth = $http_proxy->use_authentication() ? "{$http_proxy->authentication()}@" : '';
$http_client->setDefaultOption( 'proxy', "{$auth}{$http_proxy->host()}:{$http_proxy->port()}" );
}
$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'] );
$client->prepareScopes();
// 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;
}
/**
* 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/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,282 @@
<?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\Exception\Google_OAuth_Exception;
use Google\Site_Kit_Dependencies\Google_Client;
use Google\Site_Kit_Dependencies\Google\Auth\OAuth2;
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 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.
*
* @param string $code Temporary authorization code, or undelegated token code.
* @return array Access token.
*
* @throws InvalidArgumentException Thrown when the passed code is empty.
*/
public function fetchAccessTokenWithAuthCode( $code ) {
if ( strlen( $code ) === 0 ) {
throw new InvalidArgumentException( 'Invalid code' );
}
$auth = $this->getOAuth2Service();
$auth->setCode( $code );
$auth->setRedirectUri( $this->getRedirectUri() );
$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.
* @return array Access token.
*
* @throws LogicException Thrown when no refresh token is available.
*/
public function fetchAccessTokenWithRefreshToken( $refresh_token = null ) {
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 );
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 = wp_parse_url( $url, PHP_URL_SCHEME );
$host = wp_parse_url( $url, PHP_URL_HOST );
$path = wp_parse_url( $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.
* @return array Access token.
*/
protected function fetchAuthToken( OAuth2 $auth, callable $http_handler = null ) {
if ( is_null( $http_handler ) ) {
$http_handler = HttpHandlerFactory::build( HttpClientCache::getHttpClient() );
}
$request = $auth->generateCredentialsRequest();
$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 );
}
}

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\Exception\Google_Proxy_Code_Exception;
use Google\Site_Kit_Dependencies\Google\Auth\OAuth2;
use Google\Site_Kit_Dependencies\Google\Auth\HttpHandler\HttpHandlerFactory;
use Google\Site_Kit_Dependencies\GuzzleHttp\Psr7;
use Google\Site_Kit_Dependencies\GuzzleHttp\Psr7\Request;
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 = Psr7\stream_for(
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,630 @@
<?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\User_Options;
use Google\Site_Kit\Core\Util\Feature_Flags;
use Google\Site_Kit\Core\Util\Scopes;
use Google\Site_Kit_Dependencies\Google\Service\PeopleService as Google_Service_PeopleService;
/**
* 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 CRON_REFRESH_PROFILE_DATA = 'googlesitekit_cron_refresh_profile_data';
/**
* Owner_ID instance.
*
* @since 1.16.0
* @var Owner_ID
*/
private $owner_id;
/**
* 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.
*/
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
) {
parent::__construct(
$context,
$options,
$user_options,
$credentials,
$google_proxy,
$profile,
$token
);
$this->owner_id = new Owner_ID( $this->options );
}
/**
* 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[] $additional_scopes List of additional scopes to request.
* @return string Authentication URL.
*/
public function get_authentication_url( $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();
}
$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 );
// 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 = $this->context->input()->filter( INPUT_GET, 'code', FILTER_SANITIZE_STRING );
$error_code = $this->context->input()->filter( INPUT_GET, 'error', FILTER_SANITIZE_STRING );
// 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( admin_url() );
exit();
}
if ( ! $this->credentials->has() ) {
$this->user_options->set( self::OPTION_ERROR_CODE, 'oauth_credentials_not_exist' );
wp_safe_redirect( admin_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.
if ( Feature_Flags::enabled( 'serviceSetupV2' ) ) {
$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_v2( $params );
} else {
$url = $this->get_proxy_setup_url( $e->getAccessCode(), $e->getMessage() );
}
wp_safe_redirect( $url );
exit();
} catch ( Exception $e ) {
$this->handle_fetch_token_exception( $e );
wp_safe_redirect( admin_url() );
exit();
}
if ( ! isset( $token_response['access_token'] ) ) {
$this->user_options->set( self::OPTION_ERROR_CODE, 'access_token_not_received' );
wp_safe_redirect( admin_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 = $this->context->input()->filter( INPUT_GET, 'scope', FILTER_SANITIZE_STRING );
$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 ) {
$parts = wp_parse_url( $redirect_url );
$reauth = strpos( $parts['query'], 'reAuth=true' );
if ( false === $reauth ) {
$redirect_url = add_query_arg( array( 'notification' => 'authentication_success' ), $redirect_url );
}
$this->user_options->delete( self::OPTION_REDIRECT_URL );
} else {
// No redirect_url is set, use default page.
$redirect_url = $this->context->admin_url( 'splash', array( 'notification' => 'authentication_success' ) );
}
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 ) {
try {
$people_service = new Google_Service_PeopleService( $this->get_client() );
$response = $people_service->people->get( 'people/me', array( 'personFields' => 'emailAddresses,photos' ) );
if ( isset( $response['emailAddresses'][0]['value'], $response['photos'][0]['url'] ) ) {
$this->profile->set(
array(
'email' => $response['emailAddresses'][0]['value'],
'photo' => $response['photos'][0]['url'],
)
);
}
// 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() )
);
}
}
/**
* 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.0.0
* @deprecated 1.9.0
*
* @return bool True if proxy authentication is used, false otherwise.
*/
public function using_proxy() {
_deprecated_function( __METHOD__, '1.9.0', Credentials::class . '::using_proxy' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
return $this->credentials->using_proxy();
}
/**
* Returns the setup URL to the authentication proxy.
*
* @since 1.0.0
* @since 1.1.2 Added googlesitekit_proxy_setup_url_params filter.
* @since 1.27.0 Error code is no longer used.
*
* @param string $access_code Optional. Temporary access code for an undelegated access token. Default empty string.
* @return string URL to the setup page on the authentication proxy.
*
* @since 1.49.0
* @throws Exception Thrown if called when the `serviceSetupV2` feature flag is enabled.
*/
public function get_proxy_setup_url( $access_code = '' ) {
if ( Feature_Flags::enabled( 'serviceSetupV2' ) ) {
throw new Exception( __( 'Unexpected method call: get_proxy_setup_url should not be called when the serviceSetupV2 feature flag is enabled.', 'google-site-kit' ) );
}
$scope = rawurlencode( implode( ' ', $this->get_required_scopes() ) );
$query_params = array( 'scope' => $scope );
if ( ! empty( $access_code ) ) {
$query_params['code'] = $access_code;
}
return $this->google_proxy->setup_url( $this->credentials, $query_params );
}
/**
* 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_ADDITIONAL_AUTH_SCOPES );
}
}

View File

@@ -0,0 +1,348 @@
<?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 ) {
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 __( 'The Site Kit 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,54 @@
<?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,44 @@
<?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,50 @@
<?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 {
/**
* 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,672 @@
<?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 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 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 = '/site-management/setup/';
const SETUP_URI_V2 = '/v2/site-management/setup/';
const PERMISSIONS_URI = '/site-management/permissions/';
const USER_INPUT_SETTINGS_URI = '/site-management/settings/';
const FEATURES_URI = '/site-management/features/';
const SURVEY_TRIGGER_URI = '/survey/trigger/';
const SURVEY_EVENT_URI = '/survey/event/';
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',
// Informs the proxy the user input feature is generally supported.
'user_input_flow',
);
$home_path = wp_parse_url( $this->context->get_canonical_home_url(), PHP_URL_PATH );
if ( ! $home_path || '/' === $home_path ) {
$supports[] = 'file_verification';
}
// Informs the proxy the user input feature is already enabled locally.
// TODO: Remove once the feature is fully rolled out.
if ( Feature_Flags::enabled( 'userInput' ) ) {
$supports[] = 'user_input_flow_feature';
}
return $supports;
}
/**
* Returns the setup URL to the authentication proxy.
*
* @since 1.27.0
*
* @param Credentials $credentials Credentials instance.
* @param array $query_params Optional. Additional query parameters.
* @return string URL to the setup page on the authentication proxy.
*/
public function setup_url( Credentials $credentials, array $query_params = array() ) {
$params = array_merge(
$query_params,
array(
'supports' => rawurlencode( implode( ' ', $this->get_supports() ) ),
'nonce' => rawurlencode( wp_create_nonce( self::NONCE_ACTION ) ),
)
);
if ( $credentials->has() ) {
$creds = $credentials->get();
$params['site_id'] = $creds['oauth2_client_id'];
}
// If no site identification information is present, we need to provide details for a new site.
if ( empty( $params['site_id'] ) && empty( $params['site_code'] ) ) {
$site_fields = array_map( 'rawurlencode', $this->get_site_fields() );
$params = array_merge( $params, $site_fields );
}
$user_fields = array_map( 'rawurlencode', $this->get_user_fields() );
$params = array_merge( $params, $user_fields );
$params['application_name'] = rawurlencode( self::get_application_name() );
$params['hl'] = $this->context->get_locale( 'user' );
return add_query_arg( $params, $this->url( self::SETUP_URI ) );
}
/**
* Returns the setup URL to the authentication proxy.
*
* TODO: Rename this function to replace `setup_url` once the `serviceSetupV2` feature is fully developed and the feature flag is removed.
*
* @since 1.49.0
*
* @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_v2( 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( self::SETUP_URI_V2 ) );
}
/**
* 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 = defined( 'GOOGLESITEKIT_PROXY_URL' ) && self::STAGING_BASE_URL === GOOGLESITEKIT_PROXY_URL
? self::STAGING_BASE_URL
: self::PRODUCTION_BASE_URL;
$url = untrailingslashit( $url );
if ( $path && is_string( $path ) ) {
$url .= '/' . ltrim( $path, '/' );
}
return $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' ) ),
'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' => Feature_Flags::enabled( 'serviceSetupV2' ) ? 'v2' : '',
);
/**
* 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;
}
/**
* Fetch site fields
*
* @since 1.22.0
*
* @param Credentials $credentials Credentials instance.
* @return array|WP_Error The response as an associative array or WP_Error on failure.
*/
public function fetch_site_fields( Credentials $credentials ) {
return $this->request( self::OAUTH2_SITE_URI, $credentials );
}
/**
* Are site fields synced
*
* @since 1.22.0
*
* @param Credentials $credentials Credentials instance.
*
* @return boolean|WP_Error Boolean do the site fields match or WP_Error on failure.
*/
public function are_site_fields_synced( Credentials $credentials ) {
$site_fields = $this->fetch_site_fields( $credentials );
if ( is_wp_error( $site_fields ) ) {
return $site_fields;
}
$get_site_fields = $this->get_site_fields();
foreach ( $get_site_fields as $key => $site_field ) {
if ( ! array_key_exists( $key, $site_fields ) || $site_fields[ $key ] !== $site_field ) {
return false;
}
}
return true;
}
/**
* 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;
}
/**
* Synchronizes user input settings with the proxy.
*
* @since 1.27.0
*
* @param Credentials $credentials Credentials instance.
* @param string $access_token Access token.
* @param array|null $settings Settings array.
* @return array|WP_Error Response of the wp_remote_post request.
*/
public function sync_user_input_settings( Credentials $credentials, $access_token, $settings = null ) {
$body = array();
if ( ! empty( $settings ) ) {
$body = array(
'settings' => $settings,
'client_user_id' => (string) get_current_user_id(),
);
}
return $this->request(
self::USER_INPUT_SETTINGS_URI,
$credentials,
array(
'json_request' => true,
'access_token' => $access_token,
'body' => $body,
)
);
}
/**
* 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
*
* @param Credentials $credentials Credentials instance.
* @return array|WP_Error Response of the wp_remote_post request.
*/
public function get_features( Credentials $credentials ) {
$platform = self::get_platform();
return $this->request(
self::FEATURES_URI,
$credentials,
array(
'body' => array(
'platform' => $platform . '/google-site-kit',
'version' => GOOGLESITEKIT_VERSION,
),
)
);
}
/**
* 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.Misspelled
}
/**
* 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,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 ) {
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,74 @@
<?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,76 @@
<?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,208 @@
<?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\Storage\User_Options;
use Google\Site_Kit\Core\Util\Feature_Flags;
/**
* Base class for authentication setup.
*
* @since 1.48.0
* @access private
* @ignore
*/
abstract 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;
/**
* 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();
}
/**
* Registers functionality through WordPress hooks.
*
* @since 1.48.0
*/
abstract public function register();
/**
* 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 ) ) {
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() ) {
if ( Feature_Flags::enabled( 'serviceSetupV2' ) ) {
$params['code'] = $code;
$url = $this->authentication->get_google_proxy()->setup_url_v2( $params );
} else {
$url = $this->authentication->get_oauth_client()->get_proxy_setup_url( $code );
$url = add_query_arg( $params, $url );
}
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,114 @@
<?php
/**
* Class Google\Site_Kit\Core\Authentication\Setup_V1
*
* @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\Authentication\Exception\Exchange_Site_Code_Exception;
use Google\Site_Kit\Core\Authentication\Exception\Missing_Verification_Exception;
use Google\Site_Kit\Core\Permissions\Permissions;
/**
* Class for v1 authentication setup.
*
* @since 1.48.0
* @access private
* @ignore
*/
class Setup_V1 extends Setup {
/**
* 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_SETUP, array( $this, 'handle_action_setup' ) );
}
/**
* Handles the setup start action, taking the user to the proxy setup screen.
*
* @since 1.48.0
*/
public function handle_action_setup_start() {
$nonce = $this->context->input()->filter( INPUT_GET, 'nonce', FILTER_SANITIZE_STRING );
$redirect_url = $this->context->input()->filter( INPUT_GET, 'redirect', FILTER_SANITIZE_URL );
$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' ) );
}
if ( false === $this->google_proxy->are_site_fields_synced( $this->credentials ) ) {
$this->google_proxy->sync_site_fields( $this->credentials, 'sync' );
}
if ( $redirect_url ) {
$this->user_options->set( OAuth_Client::OPTION_REDIRECT_URL, $redirect_url );
}
$this->redirect_to_proxy();
}
/**
* Handles the setup action, which is used for all intermediate proxy redirect requests.
*
* @since 1.48.0
* @since 1.49.0 Sets the `verify` and `verification_method` query params.
*/
public function handle_action_setup() {
$input = $this->context->input();
$nonce = $input->filter( INPUT_GET, 'nonce', FILTER_SANITIZE_STRING );
$code = $input->filter( INPUT_GET, 'googlesitekit_code', FILTER_SANITIZE_STRING );
$site_code = $input->filter( INPUT_GET, 'googlesitekit_site_code', FILTER_SANITIZE_STRING );
$verification_token = $input->filter( INPUT_GET, 'googlesitekit_verification_token', FILTER_SANITIZE_STRING );
$verification_method = $input->filter( INPUT_GET, 'googlesitekit_verification_token_type', FILTER_SANITIZE_STRING );
$this->verify_nonce( $nonce );
if ( ! current_user_can( Permissions::SETUP ) ) {
wp_die( esc_html__( 'You don\'t have permissions to set up Site Kit.', 'google-site-kit' ), 403 );
}
if ( ! $code ) {
wp_die( esc_html__( 'Invalid request.', 'google-site-kit' ), 400 );
}
$proxy_query_params = array();
if ( $verification_token && $verification_method ) {
$this->handle_verification( $verification_token, $verification_method );
$proxy_query_params = array(
'verify' => 'true',
'verification_method' => $verification_method,
);
}
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();
}
}
$this->redirect_to_proxy( $code, $proxy_query_params );
}
}

View File

@@ -0,0 +1,171 @@
<?php
/**
* Class Google\Site_Kit\Core\Authentication\Setup_V2
*
* @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\Authentication\Exception\Exchange_Site_Code_Exception;
use Google\Site_Kit\Core\Authentication\Exception\Missing_Verification_Exception;
use Google\Site_Kit\Core\Permissions\Permissions;
/**
* Class for v2 authentication setup.
*
* @since 1.48.0
* @access private
* @ignore
*/
class Setup_V2 extends Setup {
/**
* 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' ) );
}
/**
* Handles the setup start action, taking the user to the proxy setup screen.
*
* @since 1.48.0
*/
public function handle_action_setup_start() {
$nonce = $this->context->input()->filter( INPUT_GET, 'nonce', FILTER_SANITIZE_STRING );
$redirect_url = $this->context->input()->filter( INPUT_GET, 'redirect', FILTER_SANITIZE_URL );
$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' );
if ( is_wp_error( $oauth_setup_redirect ) || ! filter_var( $oauth_setup_redirect, FILTER_VALIDATE_URL ) ) {
wp_die( esc_html__( 'The request to the authentication proxy has failed. Please, try again later.', 'google-site-kit' ) );
}
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 = $input->filter( INPUT_GET, 'step', FILTER_SANITIZE_STRING );
$nonce = $input->filter( INPUT_GET, 'nonce', FILTER_SANITIZE_STRING );
$code = $input->filter( INPUT_GET, 'googlesitekit_code', FILTER_SANITIZE_STRING );
$site_code = $input->filter( INPUT_GET, 'googlesitekit_site_code', FILTER_SANITIZE_STRING );
$verification_token = $input->filter( INPUT_GET, 'googlesitekit_verification_token', FILTER_SANITIZE_STRING );
$verification_method = $input->filter( INPUT_GET, 'googlesitekit_verification_token_type', FILTER_SANITIZE_STRING );
$this->verify_nonce( $nonce );
if ( ! current_user_can( Permissions::SETUP ) ) {
wp_die( esc_html__( 'You don\'t 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 = $input->filter( INPUT_GET, 'step', FILTER_SANITIZE_STRING );
$nonce = $input->filter( INPUT_GET, 'nonce', FILTER_SANITIZE_STRING );
$code = $input->filter( INPUT_GET, 'googlesitekit_code', FILTER_SANITIZE_STRING );
$site_code = $input->filter( INPUT_GET, 'googlesitekit_site_code', FILTER_SANITIZE_STRING );
$this->verify_nonce( $nonce );
if ( ! current_user_can( Permissions::SETUP ) ) {
wp_die( esc_html__( 'You don\'t 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' ) );
}
}

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,60 @@
<?php
/**
* Class Google\Site_Kit\Core\Authentication\User_Input_State
*
* @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 user_input_state for the user.
*
* @since 1.20.0
* @access private
* @ignore
*/
final class User_Input_State extends User_Setting {
/**
* User option key.
*/
const OPTION = 'googlesitekit_user_input_state';
/**
* Value required key.
*/
const VALUE_REQUIRED = 'required';
/**
* Value completed key.
*/
const VALUE_COMPLETED = 'completed';
/**
* Value missing key.
*/
const VALUE_MISSING = 'missing';
/**
* Gets the callback for sanitizing the setting's value before saving.
*
* @since 1.23.0
*
* @return callable|null
*/
protected function get_sanitize_callback() {
return function( $value ) {
if ( ! in_array( $value, array( self::VALUE_COMPLETED, self::VALUE_MISSING, self::VALUE_REQUIRED, '' ), true ) ) {
return false;
}
return $value;
};
}
}

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,61 @@
<?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,45 @@
<?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,55 @@
<?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,57 @@
<?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,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,75 @@
<?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,154 @@
<?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 );
}
/**
* 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,135 @@
<?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\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
*
* @return REST_Route[] List of REST_Route objects.
*/
protected function get_rest_routes() {
$can_authenticate = function () {
return current_user_can( Permissions::AUTHENTICATE );
};
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_authenticate,
)
),
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_authenticate,
'args' => array(
'data' => array(
'type' => 'object',
'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,64 @@
<?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,129 @@
<?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_authenticate = function () {
return current_user_can( Permissions::AUTHENTICATE );
};
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_authenticate,
)
),
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_authenticate,
'args' => array(
'data' => array(
'type' => 'object',
'required' => true,
),
),
)
),
);
}
}

View File

@@ -0,0 +1,33 @@
<?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,840 @@
<?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\Storage\Options;
use Google\Site_Kit\Core\Storage\User_Options;
use Google\Site_Kit\Core\Storage\Cache;
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\Util\Feature_Flags;
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 Google\Site_Kit_Dependencies\TrueBV\Punycode;
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;
/**
* 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->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 ];
}
/**
* Returns all module information data for passing it to JavaScript.
*
* @since 1.0.0
*
* @return array Module information data.
*/
public function prepare_info_for_js() {
// TODO: Modify this to ditch unnecessary backward-compatibility.
return array(
'slug' => $this->slug,
'name' => $this->name,
'description' => $this->description,
'sort' => $this->order,
'homepage' => $this->homepage,
'required' => $this->depends_on,
'autoActivate' => $this->force_active,
'internal' => $this->internal,
'screenID' => $this instanceof Module_With_Screen ? $this->get_screen()->get_slug() : false,
'settings' => $this instanceof Module_With_Settings ? $this->get_settings()->get() : false,
);
}
/**
* 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();
}
/**
* 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 ) {
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.
*
* phpcs:disable Squiz.Commenting.FunctionCommentThrowTag.Missing
*/
final protected function execute_data_request( Data_Request $data ) {
try {
$this->validate_data_request( $data );
$request = $this->make_data_request( $data );
if ( is_wp_error( $request ) ) {
return $request;
} elseif ( $request instanceof Closure ) {
$response = $request();
} elseif ( $request instanceof RequestInterface ) {
$response = $this->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 );
}
if ( is_wp_error( $response ) ) {
return $response;
}
return $this->parse_data_response( $data, $response );
}
/**
* Validates the given data request.
*
* @since 1.9.0
*
* @param Data_Request $data Data request object.
*
* @throws Invalid_Datapoint_Exception Thrown if the datapoint does not exist.
* @throws Insufficient_Scopes_Exception Thrown if the user has not granted
* necessary scopes required by the datapoint.
*/
private function validate_data_request( Data_Request $data ) {
$definitions = $this->get_datapoint_definitions();
$datapoint_key = "$data->method:$data->datapoint";
// All datapoints must be defined.
if ( empty( $definitions[ $datapoint_key ] ) ) {
throw new Invalid_Datapoint_Exception();
}
if ( ! $this instanceof Module_With_Scopes ) {
return;
}
$datapoint = $definitions[ $datapoint_key ];
$oauth_client = $this->authentication->get_oauth_client();
if ( ! empty( $datapoint['scopes'] ) && ! $oauth_client->has_sufficient_scopes( $datapoint['scopes'] ) ) {
// Otherwise, if the datapoint doesn't rely on a service but requires
// specific scopes, ensure they are satisfied.
$message = ! empty( $datapoint['request_scopes_message'] )
? $datapoint['request_scopes_message']
: __( 'Youll need to grant Site Kit permission to do this.', 'google-site-kit' );
throw new Insufficient_Scopes_Exception( $message, 0, null, $datapoint['scopes'] );
}
$requires_service = ! empty( $datapoint['service'] );
if ( $requires_service && ! $oauth_client->has_sufficient_scopes( $this->get_scopes() ) ) {
// If the datapoint relies on a service which requires scopes and
// these have not been granted, fail the request with a permissions
// error (see issue #3227).
/* translators: %s: module name */
$message = sprintf( __( '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() );
}
}
/**
* Facilitates the creation of a request object for execution.
*
* @since 1.9.0
*
* @param Data_Request $data Data request object.
* @return RequestInterface|Closure|WP_Error
*/
private function make_data_request( Data_Request $data ) {
$definitions = $this->get_datapoint_definitions();
// We only need to initialize the client if this datapoint relies on a service.
$requires_client = ! empty( $definitions[ "$data->method:$data->datapoint" ]['service'] );
if ( $requires_client ) {
$restore_defer = $this->with_client_defer( true );
}
$request = $this->create_data_request( $data );
if ( isset( $restore_defer ) ) {
$restore_defer();
}
return $request;
}
/**
* Parses a date range string into a start date and an end date.
*
* @since 1.0.0
*
* @param string $range Date range string. Either 'last-7-days', 'last-14-days', 'last-90-days', or
* 'last-28-days' (default).
* @param string $multiplier Optional. How many times the date range to get. This value can be specified if the
* range should be request multiple times back. Default 1.
* @param int $offset Days the range should be offset by. Default 1. Used by Search Console where
* data is delayed by two days.
* @param bool $previous Whether to select the previous period. Default false.
*
* @return array List with two elements, the first with the start date and the second with the end date, both as
* 'Y-m-d'.
*/
protected function parse_date_range( $range, $multiplier = 1, $offset = 1, $previous = false ) {
preg_match( '*-(\d+)-*', $range, $matches );
$number_of_days = $multiplier * ( isset( $matches[1] ) ? $matches[1] : 28 );
// Calculate the end date. For previous period requests, offset period by the number of days in the request.
$end_date_offset = $previous ? $offset + $number_of_days : $offset;
$date_end = gmdate( 'Y-m-d', strtotime( $end_date_offset . ' days ago' ) );
// Set the start date.
$start_date_offset = $end_date_offset + $number_of_days - 1;
$date_start = gmdate( 'Y-m-d', strtotime( $start_date_offset . ' days ago' ) );
return array( $date_start, $date_end );
}
/**
* 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;
}
/**
* Permutes site URL to cover all different variants of it (not considering the path).
*
* @since 1.0.0
*
* @param string $site_url Site URL to get permutations for.
* @return array List of permutations.
*/
final protected function permute_site_url( $site_url ) {
$hostname = wp_parse_url( $site_url, PHP_URL_HOST );
$path = wp_parse_url( $site_url, PHP_URL_PATH );
return array_reduce(
$this->permute_site_hosts( $hostname ),
function ( $urls, $host ) use ( $path ) {
$host_with_path = $host . $path;
array_push( $urls, "https://$host_with_path", "http://$host_with_path" );
return $urls;
},
array()
);
}
/**
* Generates common variations of the given hostname.
*
* Returns a list of hostnames that includes:
* - (if IDN) in Punycode encoding
* - (if IDN) in Unicode encoding
* - with and without www. subdomain (including IDNs)
*
* @since 1.38.0
*
* @param string $hostname Hostname to generate variations of.
* @return string[] Hostname variations.
*/
protected function permute_site_hosts( $hostname ) {
$punycode = new Punycode();
// See \Requests_IDNAEncoder::is_ascii.
$is_ascii = preg_match( '/(?:[^\x00-\x7F])/', $hostname ) !== 1;
$is_www = 0 === strpos( $hostname, 'www.' );
// Normalize hostname without www.
$hostname = $is_www ? substr( $hostname, strlen( 'www.' ) ) : $hostname;
$hosts = array( $hostname, "www.$hostname" );
try {
// An ASCII hostname can only be non-IDN or punycode-encoded.
if ( $is_ascii ) {
// If the hostname is in punycode encoding, add the decoded version to the list of hosts.
if ( 0 === strpos( $hostname, Punycode::PREFIX ) || false !== strpos( $hostname, '.' . Punycode::PREFIX ) ) {
$host_decoded = $punycode->decode( $hostname );
array_push( $hosts, $host_decoded, "www.$host_decoded" );
}
} else {
// If it's not ASCII, then add the punycode encoded version.
$host_encoded = $punycode->encode( $hostname );
array_push( $hosts, $host_encoded, "www.$host_encoded" );
}
} catch ( Exception $exception ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
// Do nothing.
}
return $hosts;
}
/**
* 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 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 ) {
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 = '' ) {
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 );
if ( Feature_Flags::enabled( 'serviceSetupV2' ) ) {
$google_proxy = $this->authentication->get_google_proxy();
$credentials = $this->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_v2( $params );
} else {
$reconnect_url = $auth_client->get_proxy_setup_url( $e->getAccessCode(), $code );
}
}
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 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 $details ) {
if ( ! empty( $details['shareable'] ) ) {
return true;
}
}
}
return false;
}
}

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,180 @@
<?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;
/**
* 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'] ) ) {
$sanitized_option[ $module_slug ]['sharedRoles'] = $this->sanitize_string_list( $sharing_settings['sharedRoles'] );
}
if ( isset( $sharing_settings['management'] ) ) {
$sanitized_option[ $module_slug ]['management'] = (string) $sharing_settings['management'];
}
}
return $sanitized_option;
};
}
/**
* Filters empty or non-string elements from a given array.
*
* @since 1.50.0
*
* @param array $elements Array to check.
* @return array Empty array or a filtered array containing only non-empty strings.
*/
private function sanitize_string_list( $elements = array() ) {
if ( ! is_array( $elements ) ) {
$elements = array( $elements );
}
if ( empty( $elements ) ) {
return array();
}
$filtered_elements = array_filter(
$elements,
function( $element ) {
return is_string( $element ) && ! empty( $element );
}
);
// Avoid index gaps for filtered values.
return array_values( $filtered_elements );
}
/**
* 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';
}
}
return $settings;
}
/**
* 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();
}
}

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 );
}

View File

@@ -0,0 +1,79 @@
<?php
/**
* Trait Google\Site_Kit\Core\Modules\Module_With_Assets_Trait
*
* @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;
/**
* Trait for a module that includes assets.
*
* @since 1.7.0
* @access private
* @ignore
*/
trait Module_With_Assets_Trait {
/**
* List of the module's Asset objects to register.
*
* @since 1.7.0
* @var array
*/
protected $registerable_assets;
/**
* Gets the assets to register for the module.
*
* @since 1.7.0
*
* @return Asset[] List of Asset objects.
*/
public function get_assets() {
if ( null === $this->registerable_assets ) {
$this->registerable_assets = $this->setup_assets();
}
return $this->registerable_assets;
}
/**
* Enqueues all assets necessary for the module.
*
* This default implementation simply enqueues all assets that the module
* has registered.
*
* @since 1.7.0
* @since 1.37.0 Added the $asset_context argument; only enqueue assets in the correct context.
*
* @param string $asset_context The page context to load this asset, see `Asset::CONTEXT_*` constants.
*/
public function enqueue_assets( $asset_context = Asset::CONTEXT_ADMIN_SITEKIT ) {
$assets = $this->get_assets();
array_walk(
$assets,
function( Asset $asset, $index, $asset_context ) {
if ( $asset->has_context( $asset_context ) ) {
$asset->enqueue();
}
},
$asset_context
);
}
/**
* Sets up the module's assets to register.
*
* @since 1.7.0
*
* @return Asset[] List of Asset objects.
*/
abstract protected function setup_assets();
}

View File

@@ -0,0 +1,27 @@
<?php
/**
* Interface Google\Site_Kit\Core\Modules\Module_With_Deactivation
*
* @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 deactivated.
*
* @since 1.36.0
* @access private
* @ignore
*/
interface Module_With_Deactivation {
/**
* Handles module deactivation.
*
* @since 1.36.0
*/
public function on_deactivation();
}

View File

@@ -0,0 +1,28 @@
<?php
/**
* Class Google\Site_Kit\Core\Modules\Module_With_Debug_Fields
*
* @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;
/**
* Interface Module_With_Debug_Fields
*
* @since 1.5.0
*/
interface Module_With_Debug_Fields {
/**
* Gets an array of debug field definitions.
*
* @since 1.5.0
*
* @return array
*/
public function get_debug_fields();
}

View File

@@ -0,0 +1,31 @@
<?php
/**
* Interface Google\Site_Kit\Core\Modules\Module_With_Owner
*
* @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 includes an owner.
*
* @since 1.16.0
* @access private
* @ignore
*/
interface Module_With_Owner {
/**
* Gets an owner ID for the module.
*
* @since 1.16.0
*
* @return int Owner ID.
*/
public function get_owner_id();
}

View File

@@ -0,0 +1,44 @@
<?php
/**
* Trait Google\Site_Kit\Core\Modules\Module_With_Owner_Trait
*
* @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\Modules\Module_With_Settings;
/**
* Trait for a module that includes an owner ID.
*
* @since 1.16.0
* @access private
* @ignore
*/
trait Module_With_Owner_Trait {
/**
* Gets an owner ID for the module.
*
* @since 1.16.0
*
* @return int Owner ID.
*/
public function get_owner_id() {
if ( ! $this instanceof Module_With_Settings ) {
return 0;
}
$settings = $this->get_settings()->get();
if ( empty( $settings['ownerID'] ) ) {
return 0;
}
return $settings['ownerID'];
}
}

View File

@@ -0,0 +1,29 @@
<?php
/**
* Interface Google\Site_Kit\Core\Modules\Module_With_Persistent_Registration
*
* @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 requires persistent registration.
*
* @since 1.38.0
* @access private
* @ignore
*/
interface Module_With_Persistent_Registration {
/**
* The registration method that is called even if the module is not activated.
*
* @since 1.38.0
*/
public function register_persistent();
}

View File

@@ -0,0 +1,30 @@
<?php
/**
* Interface Google\Site_Kit\Core\Modules\Module_With_Scopes
*
* @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 requires Google OAuth scopes.
*
* @since 1.0.0
* @access private
* @ignore
*/
interface Module_With_Scopes {
/**
* Gets required Google OAuth scopes for the module.
*
* @since 1.0.0
*
* @return array List of Google OAuth scopes.
*/
public function get_scopes();
}

View File

@@ -0,0 +1,35 @@
<?php
/**
* Trait Google\Site_Kit\Core\Modules\Module_With_Scopes_Trait
*
* @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;
/**
* Trait for a module that requires Google OAuth scopes.
*
* @since 1.0.0
* @access private
* @ignore
*/
trait Module_With_Scopes_Trait {
/**
* Registers the hook to add required scopes.
*
* @since 1.0.0
*/
private function register_scopes_hook() {
add_filter(
'googlesitekit_auth_scopes',
function( array $scopes ) {
return array_merge( $scopes, $this->get_scopes() );
}
);
}
}

View File

@@ -0,0 +1,32 @@
<?php
/**
* Interface Google\Site_Kit\Core\Modules\Module_With_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\Modules;
use Google\Site_Kit\Core\Admin\Screen;
/**
* Interface for a module that includes a screen.
*
* @since 1.0.0
* @access private
* @ignore
*/
interface Module_With_Screen {
/**
* Gets the screen instance to add for the module.
*
* @since 1.0.0
*
* @return Screen Screen instance.
*/
public function get_screen();
}

View File

@@ -0,0 +1,101 @@
<?php
/**
* Trait Google\Site_Kit\Core\Modules\Module_With_Screen_Trait
*
* @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\Context;
use Google\Site_Kit\Core\Permissions\Permissions;
use Google\Site_Kit\Core\Assets\Assets;
use Google\Site_Kit\Core\Admin\Screens;
use Google\Site_Kit\Core\Admin\Screen;
/**
* Trait for a module that includes a screen.
*
* @since 1.0.0
* @access private
* @ignore
*/
trait Module_With_Screen_Trait {
/**
* Module screen instance.
*
* @since 1.0.0
* @var Screen|null
*/
private $screen = null;
/**
* Gets the screen instance to add for the module.
*
* @since 1.0.0
*
* @return Screen Screen instance.
*/
final public function get_screen() {
if ( null === $this->screen ) {
$module_screen_slug = 'module-' . $this->slug;
$this->screen = new Screen(
Screens::PREFIX . $module_screen_slug,
array(
'title' => $this->name,
'capability' => Permissions::VIEW_MODULE_DETAILS,
'enqueue_callback' => function ( Assets $assets ) {
$assets->enqueue_asset( 'googlesitekit-module' );
},
'initialize_callback' => function () use ( $module_screen_slug ) {
$reauth = $this->context->input()->filter( INPUT_GET, 'reAuth', FILTER_VALIDATE_BOOLEAN );
// If the module is not set up yet, and `reAuth` is not enabled
// via the query parameter, then redirect to this URL.
if ( ! $reauth && ! $this->is_connected() ) {
wp_safe_redirect(
$this->context->admin_url(
$module_screen_slug,
array(
'slug' => $this->slug,
'reAuth' => true,
)
)
);
exit();
}
},
'render_callback' => function ( Context $context ) {
$module_info = $this->prepare_info_for_js();
$setup_slug = $this->context->input()->filter( INPUT_GET, 'slug', FILTER_SANITIZE_STRING );
$reauth = $this->context->input()->filter( INPUT_GET, 'reAuth', FILTER_VALIDATE_BOOLEAN );
$setup_module_slug = $setup_slug && $reauth ? $setup_slug : '';
?>
<div id="js-googlesitekit-module" data-setup-module-slug="<?php echo esc_attr( $setup_module_slug ); ?>" data-module-slug="<?php echo esc_attr( $module_info['slug'] ); ?>" class="googlesitekit-page"></div>
<?php
},
)
);
}
return $this->screen;
}
/**
* Registers the hook to add the module screen.
*
* @since 1.0.0
*/
private function register_screen_hook() {
add_filter(
'googlesitekit_module_screens',
function( array $screens ) {
$screens[] = $this->get_screen();
return $screens;
}
);
}
}

View File

@@ -0,0 +1,32 @@
<?php
/**
* Interface Google\Site_Kit\Core\Modules\Module_With_Service_Entity
*
* @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\Modules;
use WP_Error;
/**
* Interface for a module that includes a service entity.
*
* @since 1.70.0
* @access private
* @ignore
*/
interface Module_With_Service_Entity {
/**
* Checks if the current user has access to the current configured service entity.
*
* @since 1.70.0
*
* @return boolean|WP_Error
*/
public function check_service_entity_access();
}

View File

@@ -0,0 +1,23 @@
<?php
/**
* Interface Google\Site_Kit\Core\Modules\Module_With_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;
interface Module_With_Settings {
/**
* Gets the module's Setting instance.
*
* @since 1.2.0
*
* @return Module_Settings The Setting instance for the current module.
*/
public function get_settings();
}

View File

@@ -0,0 +1,54 @@
<?php
/**
* Trait Google\Site_Kit\Core\Modules\Module_With_Settings_Trait
*
* @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;
/**
* Trait for a module that includes a screen.
*
* @since 1.2.0
* @access private
* @ignore
*/
trait Module_With_Settings_Trait {
/**
* Settings instance.
*
* @since 1.2.0
*
* @var Module_Settings
*/
protected $settings;
/**
* Sets up the module's settings instance.
*
* @since 1.2.0
*
* @return Module_Settings
*/
abstract protected function setup_settings();
/**
* Gets the module's Settings instance.
*
* @since 1.2.0
*
* @return Module_Settings Module_Settings instance.
*/
public function get_settings() {
if ( ! $this->settings instanceof Module_Settings ) {
$this->settings = $this->setup_settings();
}
return $this->settings;
}
}

View File

@@ -0,0 +1,120 @@
<?php
/**
* Class Google\Site_Kit\Core\Modules\Tags\Module_AMP_Tag
*
* @package Google\Site_Kit\Core\Tags
* @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\Tags;
use Google\Site_Kit\Core\Tags\Blockable_Tag_Interface;
/**
* Base class for AMP tag.
*
* @since 1.24.0
* @access private
* @ignore
*/
abstract class Module_AMP_Tag extends Module_Tag implements Blockable_Tag_Interface {
/**
* Checks whether or not the tag should be blocked from rendering.
*
* @since 1.24.0
*
* @return bool TRUE if the tag should be blocked, otherwise FALSE.
*/
public function is_tag_blocked() {
/**
* Filters whether or not the AMP tag should be blocked from rendering.
*
* @since 1.24.0
*
* @param bool $blocked Whether or not the tag output is suppressed. Default: false.
*/
return (bool) apply_filters( "googlesitekit_{$this->module_slug}_tag_amp_blocked", false );
}
/**
* Gets the HTML attributes for a script tag that may potentially require user consent before loading.
*
* @since 1.24.0
*
* @return string HTML attributes to add if the tag requires consent to load, or an empty string.
*/
public function get_tag_blocked_on_consent_attribute() {
// @see https://amp.dev/documentation/components/amp-consent/#advanced-predefined-consent-blocking-behaviors
$allowed_amp_block_on_consent_values = array(
'_till_responded',
'_till_accepted',
'_auto_reject',
);
/**
* Filters whether the tag requires user consent before loading.
*
* @since 1.24.0
*
* @param bool|string $blocked Whether or not the tag requires user consent to load. Alternatively, this can also be one of
* the special string values '_till_responded', '_till_accepted', or '_auto_reject'. Default: false.
*/
$block_on_consent = apply_filters( "googlesitekit_{$this->module_slug}_tag_amp_block_on_consent", false );
if ( in_array( $block_on_consent, $allowed_amp_block_on_consent_values, true ) ) {
return sprintf( ' data-block-on-consent="%s"', $block_on_consent );
}
if ( filter_var( $block_on_consent, FILTER_VALIDATE_BOOLEAN ) ) {
return ' data-block-on-consent';
}
return '';
}
/**
* Enqueues a component script for AMP Reader.
*
* @since 1.24.0
*
* @param string $handle Script handle.
* @param string $src Script source URL.
* @return callable Hook function.
*/
protected function enqueue_amp_reader_component_script( $handle, $src ) {
$component_script_hook = function( $data ) use ( $handle, $src ) {
if ( ! isset( $data['amp_component_scripts'] ) || ! is_array( $data['amp_component_scripts'] ) ) {
$data['amp_component_scripts'] = array();
}
if ( ! isset( $data['amp_component_scripts'][ $handle ] ) ) {
$data['amp_component_scripts'][ $handle ] = $src;
}
return $data;
};
add_filter( 'amp_post_template_data', $component_script_hook );
return $component_script_hook;
}
/**
* Fires the "googlesitekit_{module_slug}_init_tag_amp" action to let 3rd party plugins to perform required setup.
*
* @since 1.24.0
*/
protected function do_init_tag_action() {
/**
* Fires when the tag has been initialized which means that the tag will be rendered in the current request.
*
* @since 1.24.0
*
* @param string $tag_id Tag ID.
*/
do_action( "googlesitekit_{$this->module_slug}_init_tag_amp", $this->tag_id );
}
}

View File

@@ -0,0 +1,52 @@
<?php
/**
* Class Google\Site_Kit\Core\Modules\Tags\Module_Tag
*
* @package Google\Site_Kit\Core\Tags
* @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\Tags;
use Google\Site_Kit\Core\Tags\Tag;
/**
* Base class for a module tag.
*
* @since 1.24.0
* @access private
* @ignore
*/
abstract class Module_Tag extends Tag {
/**
* Module slug.
*
* @since 1.24.0
* @var string
*/
protected $slug;
/**
* Constructor.
*
* @since 1.24.0
*
* @param string $tag_id Tag ID.
* @param string $module_slug Module slug.
*/
public function __construct( $tag_id, $module_slug ) {
parent::__construct( $tag_id );
$this->module_slug = $module_slug;
}
/**
* Outputs the tag.
*
* @since 1.24.0
*/
abstract protected function render();
}

View File

@@ -0,0 +1,54 @@
<?php
/**
* Class Google\Site_Kit\Core\Modules\Tags\Module_Tag_Guard
*
* @package Google\Site_Kit\Core\Tags
* @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\Tags;
use Google\Site_Kit\Core\Guards\Guard_Interface;
use Google\Site_Kit\Core\Modules\Module_Settings;
use WP_Error;
/**
* Base class for a module tag guard.
*
* @since 1.24.0
* @access private
* @ignore
*/
abstract class Module_Tag_Guard implements Guard_Interface {
/**
* Module settings.
*
* @since 1.24.0
* @var Module_Settings
*/
protected $settings;
/**
* Constructor.
*
* @since 1.24.0
*
* @param Module_Settings $settings Module settings.
*/
public function __construct( Module_Settings $settings ) {
$this->settings = $settings;
}
/**
* Determines whether the guarded tag can be activated or not.
*
* @since 1.24.0
*
* @return bool|WP_Error TRUE if guarded tag can be activated, otherwise FALSE or an error.
*/
abstract public function can_activate();
}

View File

@@ -0,0 +1,105 @@
<?php
/**
* Class Google\Site_Kit\Core\Modules\Tags\Module_Web_Tag
*
* @package Google\Site_Kit\Core\Modules\Tags
* @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\Tags;
use Google\Site_Kit\Core\Tags\Blockable_Tag_Interface;
/**
* Base class for Web tag.
*
* @since 1.24.0
* @access private
* @ignore
*/
abstract class Module_Web_Tag extends Module_Tag implements Blockable_Tag_Interface {
/**
* Checks whether or not the tag should be blocked from rendering.
*
* @since 1.24.0
*
* @return bool TRUE if the tag should be blocked, otherwise FALSE.
*/
public function is_tag_blocked() {
/**
* Filters whether or not the tag should be blocked from rendering.
*
* @since 1.24.0
*
* @param bool $blocked Whether or not the tag output is suppressed. Default: false.
*/
return (bool) apply_filters( "googlesitekit_{$this->module_slug}_tag_blocked", false );
}
/**
* Gets the HTML attributes for a script tag that may potentially require user consent before loading.
*
* @since 1.24.0
*
* @return string HTML attributes to add if the tag requires consent to load, or an empty string.
*/
public function get_tag_blocked_on_consent_attribute() {
/**
* Filters whether the tag requires user consent before loading.
*
* @since 1.24.0
*
* @param bool $blocked Whether or not the tag requires user consent to load. Default: false.
*/
if ( apply_filters( "googlesitekit_{$this->module_slug}_tag_block_on_consent", false ) ) {
return ' type="text/plain" data-block-on-consent';
}
return '';
}
/**
* Gets the array of HTML attributes for a script tag that may potentially require user consent before loading.
*
* @since 1.41.0
*
* @return array containing HTML attributes to add if the tag requires consent to load, or an empty array.
*/
public function get_tag_blocked_on_consent_attribute_array() {
/**
* Filters whether the tag requires user consent before loading.
*
* @since 1.24.0
*
* @param bool $blocked Whether or not the tag requires user consent to load. Default: false.
*/
if ( apply_filters( "googlesitekit_{$this->module_slug}_tag_block_on_consent", false ) ) {
return array(
'type' => 'text/plain',
'data-block-on-consent' => true,
);
}
return array();
}
/**
* Fires the "googlesitekit_{module_slug}_init_tag" action to let 3rd party plugins to perform required setup.
*
* @since 1.24.0
*/
protected function do_init_tag_action() {
/**
* Fires when the tag has been initialized which means that the tag will be rendered in the current request.
*
* @since 1.24.0
*
* @param string $tag_id Tag ID.
*/
do_action( "googlesitekit_{$this->module_slug}_init_tag", $this->tag_id );
}
}

View File

@@ -0,0 +1,107 @@
<?php
/**
* Class Google\Site_Kit\Core\Notifications\Notification
*
* @package Google\Site_Kit\Core\Notifications
* @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\Notifications;
/**
* Class for representing a notification.
*
* @since 1.4.0
* @access private
* @ignore
*/
class Notification {
/**
* Unique notification slug.
*
* @since 1.4.0
* @var string
*/
private $slug;
/**
* Notification arguments.
*
* @since 1.4.0
* @var array
*/
private $args;
/**
* Constructor.
*
* @since 1.4.0
*
* @param string $slug Unique notification slug.
* @param array $args {
* Associative array of notification arguments.
*
* @type string $title Required notification title.
* @type string $content Required notification content. May contain inline HTML tags.
* @type string $cta_url Call to action URL.
* @type string $cta_label Call to action anchor text.
* @type string $cta_target Call to action anchor target.
* @type string $learn_more_url Learn more URL.
* @type string $learn_more_label Learn more anchor text.
* @type bool $dismissible Whether the notice should be dismissible. Default false.
* @type string $dismiss_label Dismiss anchor text.
* }
*/
public function __construct( $slug, array $args ) {
$this->slug = (string) $slug;
$this->args = array_merge(
array(
'title' => '',
'content' => '',
'cta_url' => '',
'cta_label' => '',
'cta_target' => '',
'learn_more_url' => '',
'learn_more_label' => '',
'dismissible' => false,
'dismiss_label' => __( 'Dismiss', 'google-site-kit' ),
),
$args
);
}
/**
* Gets the notification's slug.
*
* @since 1.4.0
*
* @return string Unique notification slug.
*/
public function get_slug() {
return $this->slug;
}
/**
* Prepares the JS representation of the Notification.
*
* @since 1.4.0
*
* @return array
*/
public function prepare_for_js() {
return array(
'id' => $this->get_slug(),
'title' => $this->args['title'],
'content' => $this->args['content'],
'ctaURL' => $this->args['cta_url'],
'ctaLabel' => $this->args['cta_label'],
'ctaTarget' => $this->args['cta_target'],
'learnMoreURL' => $this->args['learn_more_url'],
'learnMoreLabel' => $this->args['learn_more_label'],
'dismissible' => $this->args['dismissible'],
'dismissLabel' => $this->args['dismiss_label'],
);
}
}

View File

@@ -0,0 +1,289 @@
<?php
/**
* Class Google\Site_Kit\Core\Notifications\Notifications.php
*
* @package Google\Site_Kit\Core\Notifications
* @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\Notifications;
use Exception;
use Google\Site_Kit\Context;
use Google\Site_Kit\Core\Authentication\Credentials;
use Google\Site_Kit\Core\Authentication\Google_Proxy;
use Google\Site_Kit\Core\Authentication\Authentication;
use Google\Site_Kit\Core\Permissions\Permissions;
use Google\Site_Kit\Core\REST_API\REST_Route;
use Google\Site_Kit\Core\Storage\Encrypted_Options;
use Google\Site_Kit\Core\Storage\Options;
use WP_Error;
use WP_REST_Request;
use WP_REST_Response;
use WP_REST_Server;
/**
* Class for managing core notifications.
*
* @since 1.4.0
* @access private
* @ignore
*/
class Notifications {
/**
* Context instance.
*
* @since 1.4.0
* @var Context
*/
private $context;
/**
* Options instance.
*
* @since 1.4.0
* @var Options
*/
private $options;
/**
* Authentication instance.
*
* @since 1.8.0
* @var Authentication
*/
private $authentication;
/**
* Google_Proxy instance.
*
* @since 1.4.0
* @var Google_Proxy
*/
private $google_proxy;
/**
* Credentials instance.
*
* @since 1.4.0
* @var Credentials
*/
private $credentials;
/**
* Constructor.
*
* @since 1.4.0
*
* @param Context $context Context instance.
* @param Options $options Options instance.
* @param Authentication $authentication Authentication instance.
*/
public function __construct( Context $context, Options $options = null, Authentication $authentication = null ) {
$this->context = $context;
$this->options = $options ?: new Options( $context );
$this->google_proxy = new Google_Proxy( $this->context );
$this->authentication = $authentication ?: new Authentication( $this->context );
$this->credentials = $this->authentication->credentials();
}
/**
* Registers core notifications.
*
* @since 1.4.0
*/
public function register() {
add_filter(
'googlesitekit_rest_routes',
function ( $routes ) {
return array_merge( $routes, $this->get_rest_routes() );
}
);
}
/**
* Gets related REST routes.
*
* @since 1.4.0
*
* @return array List of REST_Route objects.
*/
private function get_rest_routes() {
$can_use_notifications = function () {
return current_user_can( Permissions::SETUP ) && $this->credentials->has();
};
return array(
new REST_Route(
'core/site/data/notifications',
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => function ( WP_REST_Request $request ) {
$endpoint = add_query_arg(
array(
'site_id' => $this->credentials->get()['oauth2_client_id'],
),
$this->google_proxy->url( '/notifications/' )
);
// Return an empty array of notifications if the user isn't using the proxy.
if ( ! $this->credentials->using_proxy() ) {
return new WP_REST_Response( array() );
}
$response = wp_remote_get( $endpoint ); // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.wp_remote_get_wp_remote_get
if ( is_wp_error( $response ) ) {
return $response;
}
try {
$response = $this->parse_response( $response );
} catch ( Exception $e ) {
return new WP_Error( 'exception', $e->getMessage() );
}
$data = array_map(
function ( Notification $notification ) {
return $notification->prepare_for_js();
},
$this->map_response_to_notifications( $response )
);
return new WP_REST_Response( $data );
},
'permission_callback' => $can_use_notifications,
),
)
),
new REST_Route(
'core/site/data/mark-notification',
array(
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => function ( WP_REST_Request $request ) {
$data = $request['data'];
if ( empty( $data['notificationID'] ) ) {
return $this->missing_required_param( 'data.notificationID' );
}
if ( empty( $data['notificationState'] ) ) {
return $this->missing_required_param( 'data.notificationState' );
}
$credentials = $this->credentials->get();
$response = wp_remote_post(
$this->google_proxy->url( '/notifications/mark/' ),
array(
'body' => array(
'site_id' => $credentials['oauth2_client_id'],
'site_secret' => $credentials['oauth2_client_secret'],
'notification_id' => $data['notificationID'],
'notification_state' => $data['notificationState'],
),
)
);
if ( is_wp_error( $response ) ) {
return $response;
}
try {
$response = $this->parse_response( $response );
} catch ( Exception $e ) {
return new WP_Error( 'exception', $e->getMessage() );
}
return new WP_REST_Response(
array(
'success' => isset( $response['success'] ) ? (bool) $response['success'] : false,
)
);
},
'args' => array(
'data' => array(
'required' => true,
'type' => 'object',
),
),
'permission_callback' => $can_use_notifications,
),
)
),
);
}
/**
* Validates and parses the given JSON response into an array.
*
* @since 1.4.0
*
* @param array $response HTTP response array.
* @return mixed JSON decoded response.
* @throws Exception Throws exception if response cannot be parsed or if an error is returned.
*/
private function parse_response( $response ) {
$body = wp_remote_retrieve_body( $response );
$decoded = json_decode( $body, true );
if ( json_last_error() ) {
throw new Exception( 'Error while decoding response: ' . json_last_error() );
}
if ( ! empty( $decoded['error'] ) ) {
throw new Exception( $decoded['error'] );
}
return $decoded;
}
/**
* Maps the response objects into Notification objects.
*
* @since 1.4.0
*
* @param array $response Array of notification objects from API.
* @return Notification[] Array of Notification objects.
*/
private function map_response_to_notifications( array $response ) {
return array_map(
function ( $notification ) {
return new Notification(
$notification['id'],
array(
'title' => $notification['title'],
'content' => $notification['content'],
'cta_url' => $notification['ctaURL'],
'cta_label' => $notification['ctaLabel'],
'cta_target' => $notification['ctaTarget'],
'learn_more_url' => $notification['learnMoreURL'],
'learn_more_label' => $notification['learnMoreLabel'],
'dismissible' => $notification['dismissible'],
'dismiss_label' => $notification['dismissLabel'],
)
);
},
$response
);
}
/**
* Gets a WP_Error instance for the given missing required parameter.
*
* @since 1.4.0
*
* @param string $param Missing required parameter.
* @return WP_Error
*/
private function missing_required_param( $param ) {
return new WP_Error(
'missing_required_param',
/* translators: %s: Missing parameter name */
sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), $param ),
array( 'status' => 400 )
);
}
}

View File

@@ -0,0 +1,628 @@
<?php
/**
* Class Google\Site_Kit\Core\Permissions\Permissions
*
* @package Google\Site_Kit\Core\Permissions
* @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\Permissions;
use Exception;
use Google\Site_Kit\Context;
use Google\Site_Kit\Core\Authentication\Authentication;
use Google\Site_Kit\Core\Dismissals\Dismissed_Items;
use Google\Site_Kit\Core\Modules\Module_With_Owner;
use Google\Site_Kit\Core\Modules\Modules;
use Google\Site_Kit\Core\Storage\User_Options;
use Google\Site_Kit\Core\Util\Feature_Flags;
/**
* Class managing plugin permissions.
*
* @since 1.0.0
* @access private
* @ignore
*/
final class Permissions {
/*
* Custom base capabilities.
*/
const AUTHENTICATE = 'googlesitekit_authenticate';
const SETUP = 'googlesitekit_setup';
const VIEW_POSTS_INSIGHTS = 'googlesitekit_view_posts_insights';
const VIEW_DASHBOARD = 'googlesitekit_view_dashboard';
const VIEW_MODULE_DETAILS = 'googlesitekit_view_module_details';
const MANAGE_OPTIONS = 'googlesitekit_manage_options';
const VIEW_SHARED_DASHBOARD = 'googlesitekit_view_shared_dashboard';
/*
* Custom meta capabilities.
*/
const VIEW_POST_INSIGHTS = 'googlesitekit_view_post_insights';
const READ_SHARED_MODULE_DATA = 'googlesitekit_read_shared_module_data';
const MANAGE_MODULE_SHARING_OPTIONS = 'googlesitekit_manage_module_sharing_options';
const DELEGATE_MODULE_SHARING_MANAGEMENT = 'googlesitekit_delegate_module_sharing_management';
/**
* Plugin context.
*
* @since 1.0.0
* @var Context
*/
private $context;
/**
* Authentication instance.
*
* @since 1.0.0
* @var Authentication
*/
protected $authentication;
/**
* Modules instance.
*
* @since 1.69.0
* @var Modules
*/
private $modules;
/**
* User_Options instance.
*
* @since 1.69.0
* @var User_Options
*/
private $user_options;
/**
* Dismissed_Items instance.
*
* @since 1.69.0
* @var Dismissed_Items
*/
private $dismissed_items;
/**
* Mappings for custom base capabilities to WordPress core built-in ones.
*
* @since 1.30.0
* @var array
*/
private $base_to_core = array();
/**
* Mappings for custom meta capabilities to WordPress core built-in ones.
*
* @since 1.0.0
* @var array
*/
private $meta_to_core = array();
/**
* Mappings for custom meta capabilities to custom base capabilities.
*
* @since 1.30.0
* @var array
*/
private $meta_to_base = array();
/**
* List of custom base capabilities that should require network access if the plugin is in network mode.
*
* @since 1.30.0
* @var array
*/
private $network_base = array();
/**
* Constructor.
*
* Sets up the capability mappings.
*
* @since 1.0.0
*
* @param Context $context Plugin context.
* @param Authentication $authentication Authentication instance.
* @param Modules $modules Modules instance.
* @param User_Options $user_options User_Options instance.
* @param Dismissed_Items $dismissed_items Dismissed_Items instance.
*/
public function __construct( Context $context, Authentication $authentication, Modules $modules, User_Options $user_options, Dismissed_Items $dismissed_items ) {
$this->context = $context;
$this->authentication = $authentication;
$this->modules = $modules;
$this->user_options = $user_options;
$this->dismissed_items = $dismissed_items;
// TODO Remove the temporary assignment of these capabilities when Dashboard Sharing feature flag is removed.
$editor_capability = 'manage_options';
$admin_network_capability = 'manage_options';
if ( Feature_Flags::enabled( 'dashboardSharing' ) ) {
$editor_capability = 'edit_posts';
$admin_network_capability = 'manage_network';
}
$this->base_to_core = array(
// By default, only allow administrators to authenticate.
self::AUTHENTICATE => 'manage_options',
// Allow contributors and up to view their own post's insights.
// TODO change to map to edit_posts when Dashboard Sharing feature flag is removed.
self::VIEW_POSTS_INSIGHTS => $editor_capability,
// Allow editors and up to view the dashboard and module details.
// TODO change to map to edit_posts when Dashboard Sharing feature flag is removed.
self::VIEW_DASHBOARD => $editor_capability,
self::VIEW_MODULE_DETAILS => $editor_capability,
// Allow administrators and up to manage options and set up the plugin.
self::MANAGE_OPTIONS => 'manage_options',
self::SETUP => 'manage_options',
);
// TODO Add the element assigned below into $this->base_to_core above when the dashboard sharing feature flag is removed.
if ( Feature_Flags::enabled( 'dashboardSharing' ) ) {
// Allow editors and up to view shared dashboard data.
$this->base_to_core[ self::VIEW_SHARED_DASHBOARD ] = 'edit_posts';
}
$this->meta_to_core = array(
// Allow users that can edit a post to view that post's insights.
self::VIEW_POST_INSIGHTS => 'edit_post',
);
$this->meta_to_base = array(
// Allow users that can generally view posts insights to view a specific post's insights.
self::VIEW_POST_INSIGHTS => self::VIEW_POSTS_INSIGHTS,
);
// TODO Merge the array below into $this->meta_to_base above when the dashboard sharing feature flag is removed.
if ( Feature_Flags::enabled( 'dashboardSharing' ) ) {
$this->meta_to_base = array_merge(
$this->meta_to_base,
array(
// Allow users that can generally view the shared dashboard to read shared module data.
self::READ_SHARED_MODULE_DATA => self::VIEW_SHARED_DASHBOARD,
// Admins who can manage options for SK can generally manage module sharing options.
self::MANAGE_MODULE_SHARING_OPTIONS => self::MANAGE_OPTIONS,
self::DELEGATE_MODULE_SHARING_MANAGEMENT => self::MANAGE_OPTIONS,
)
);
}
$this->network_base = array(
// Require network admin access to view the dashboard and module details in network mode.
// TODO change to map to manage_network when Dashboard Sharing feature flag is removed.
self::VIEW_DASHBOARD => $admin_network_capability,
self::VIEW_MODULE_DETAILS => $admin_network_capability,
// Require network admin access to manage options and set up the plugin in network mode.
self::MANAGE_OPTIONS => 'manage_network_options',
self::SETUP => 'manage_network_options',
);
}
/**
* Registers functionality through WordPress hooks.
*
* @since 1.0.0
*/
public function register() {
add_filter(
'map_meta_cap',
function( array $caps, $cap, $user_id, $args ) {
return $this->map_meta_capabilities( $caps, $cap, $user_id, $args );
},
10,
4
);
add_filter(
'googlesitekit_user_data',
function( $data ) {
$data['permissions'] = $this->check_all_for_current_user();
return $data;
}
);
// This constant can be set if an alternative mechanism to grant these capabilities is in place.
if ( defined( 'GOOGLESITEKIT_DISABLE_DYNAMIC_CAPABILITIES' ) && GOOGLESITEKIT_DISABLE_DYNAMIC_CAPABILITIES ) {
return;
}
add_filter(
'user_has_cap',
function( array $allcaps ) {
return $this->grant_additional_caps( $allcaps );
}
);
}
/**
* Get dashboard sharing meta permissions for current user.
*
* @since 1.70.0
*
* @return array List meta capabilities as keys and current user permission as value.
*/
public function get_dashboard_sharing_meta_permissions() {
if ( ! Feature_Flags::enabled( 'dashboardSharing' ) ) {
return array();
}
$dashboard_sharing_meta_capabilities = self::get_dashboard_sharing_meta_capabilities();
$shareable_modules = array_keys( $this->modules->get_shareable_modules() );
$dashboard_sharing_meta_permissions = array();
foreach ( $dashboard_sharing_meta_capabilities as $cap ) {
foreach ( $shareable_modules as $module ) {
$dashboard_sharing_meta_permissions[ "{$cap}::" . wp_json_encode( array( $module ) ) ] = current_user_can( $cap, $module );
}
}
return $dashboard_sharing_meta_permissions;
}
/**
* Check permissions for current user.
*
* @since 1.21.0
*
* @return array List of base capabilities and meta capabilities as keys and current user permission as value.
*/
public function check_all_for_current_user() {
$permissions = self::get_capabilities();
return array_merge(
array_combine(
$permissions,
array_map( 'current_user_can', $permissions )
),
self::get_dashboard_sharing_meta_permissions()
);
}
/**
* Resolves meta capabilities to their base capabilities.
*
* This method first maps plugin meta capabilities to their base capabilities. In addition, if the meta
* capability should also map to a core meta capability, that mapping is taken care of as well.
*
* If in network mode and the custom base capability requires network access, it is checked that the user
* has that access, and if not, the method bails early causing in a result of false.
*
* It also prevents access to Site Kit's custom capabilities based on additional rules. These additional
* checks ideally could be done within the `user_has_cap` filter. However, the `user_has_cap` filter is
* applied after a check for multi-site admins which could potentially grant the capability without
* executing these additional checks.
*
* @see WP_User::has_cap() To see the order of execution mentioned above.
*
* @since 1.0.0
*
* @param array $caps List of resolved capabilities.
* @param string $cap Capability checked.
* @param int $user_id Current user ID.
* @param array $args Additional arguments passed to the capability check.
* @return array Filtered value of $caps.
*/
private function map_meta_capabilities( array $caps, $cap, $user_id, $args ) {
// Bail early under these circumstances as we already know for sure the check will result in false.
if ( isset( $this->network_base[ $cap ] ) && $this->context->is_network_mode() && ! is_super_admin( $user_id ) ) {
return array( 'do_not_allow' );
}
if ( isset( $this->meta_to_base[ $cap ] ) ) {
$caps = (array) $this->meta_to_base[ $cap ];
}
if ( isset( $this->meta_to_core[ $cap ] ) ) {
$required_core_caps = call_user_func_array(
'map_meta_cap',
array_merge(
array( $this->meta_to_core[ $cap ], $user_id ),
$args
)
);
$caps = array_merge( $caps, $required_core_caps );
}
// Special setup and authentication rules.
if ( ( isset( $this->base_to_core[ $cap ] ) || isset( $this->meta_to_core[ $cap ] ) ) ) {
// If setup has not yet been completed, require administrator capabilities for everything.
if ( self::SETUP !== $cap && ! $this->authentication->is_setup_completed() ) {
$caps[] = self::SETUP;
}
if ( ! in_array( $cap, array( self::AUTHENTICATE, self::SETUP ), true ) ) {
// For regular users, require being authenticated.
if ( ! Feature_Flags::enabled( 'dashboardSharing' ) && ! $this->is_user_authenticated( $user_id ) ) {
return array_merge( $caps, array( 'do_not_allow' ) );
}
// For admin users, also require being verified.
if ( user_can( $user_id, self::SETUP ) && ! $this->is_user_verified( $user_id ) ) {
return array_merge( $caps, array( 'do_not_allow' ) );
}
// For all users, require setup to have been completed.
if ( ! $this->authentication->is_setup_completed() ) {
return array_merge( $caps, array( 'do_not_allow' ) );
}
}
}
if ( in_array( $cap, self::get_dashboard_sharing_capabilities(), true ) ) {
$caps = array_merge( $caps, $this->check_dashboard_sharing_capability( $cap, $user_id, $args ) );
}
return $caps;
}
/**
* Checks a dashboard sharing capability based on rules of dashboard sharing.
*
* @since 1.69.0
*
* @param string $cap Capability to be checked.
* @param int $user_id User ID of the user the capability is checked for.
* @param array $args Additional arguments passed to check a meta capability.
* @return array Array with a 'do_not_allow' element if checks fail, empty array if checks pass.
*/
private function check_dashboard_sharing_capability( $cap, $user_id, $args ) {
// TODO remove this check when Dashboard Sharing feature flag is removed.
if ( ! Feature_Flags::enabled( 'dashboardSharing' ) ) {
return array( 'do_not_allow' );
}
if ( isset( $args[0] ) ) {
$module_slug = $args[0];
}
switch ( $cap ) {
case self::VIEW_SHARED_DASHBOARD:
return $this->check_view_shared_dashboard_capability( $user_id );
case self::READ_SHARED_MODULE_DATA:
return $this->check_read_shared_module_data_capability( $user_id, $module_slug );
case self::MANAGE_MODULE_SHARING_OPTIONS:
case self::DELEGATE_MODULE_SHARING_MANAGEMENT:
return $this->check_module_sharing_admin_capability( $cap, $user_id, $module_slug );
default:
return array();
}
}
/**
* Checks if the VIEW_SHARED_DASHBOARD capability should be denied.
*
* Prevents access to the VIEW_SHARED_DASHBOARD capability if a user does not
* have any of the shared roles set for any shareable module or if they have
* not dismissed the dashboard sharing splash screen message.
*
* @since 1.69.0
*
* @param int $user_id User ID of the user the capability is checked for.
* @return array Array with a 'do_not_allow' element if checks fail, empty array if checks pass.
*/
private function check_view_shared_dashboard_capability( $user_id ) {
$module_sharing_settings = $this->modules->get_module_sharing_settings();
$shared_roles = $module_sharing_settings->get_all_shared_roles();
$user = get_userdata( $user_id );
$user_has_shared_role = ! empty( array_intersect( $shared_roles, $user->roles ) );
if ( ! $user_has_shared_role ) {
return array( 'do_not_allow' );
}
if ( ! $this->is_shared_dashboard_splash_dismissed( $user_id ) ) {
return array( 'do_not_allow' );
}
return array();
}
/**
* Checks if the READ_SHARED_MODULE_DATA capability should be denied.
*
* Prevents access to the READ_SHARED_MODULE_DATA capability if a user does not
* have the shared roles set for the given module slug.
*
* @since 1.69.0
*
* @param int $user_id User ID of the user the capability is checked for.
* @param string $module_slug Module for which the meta capability is checked for.
* @return array Array with a 'do_not_allow' element if checks fail, empty array if checks pass.
*/
private function check_read_shared_module_data_capability( $user_id, $module_slug ) {
$module_sharing_settings = $this->modules->get_module_sharing_settings();
$sharing_settings = $module_sharing_settings->get();
$user = get_userdata( $user_id );
if ( ! isset( $sharing_settings[ $module_slug ]['sharedRoles'] ) ) {
return array( 'do_not_allow' );
}
$user_has_module_shared_role = ! empty( array_intersect( $sharing_settings[ $module_slug ]['sharedRoles'], $user->roles ) );
if ( ! $user_has_module_shared_role ) {
return array( 'do_not_allow' );
}
return array();
}
/**
* Checks if the MANAGE_MODULE_SHARING_OPTIONS or the DELEGATE_MODULE_SHARING_MANAGEMENT
* capability should be denied.
*
* Prevents access to MANAGE_MODULE_SHARING_OPTIONS or the DELEGATE_MODULE_SHARING_MANAGEMENT
* capability if a user is not an authenticated admin.
*
* Furthermore, it prevents access for these capabilities if the user is not the owner
* of the given module slug. This check is skipped for MANAGE_MODULE_SHARING_OPTIONS if the
* module settings allow all admins to manage sharing options for that module.
*
* @since 1.69.0
*
* @param string $cap Capability to be checked.
* @param int $user_id User ID of the user the capability is checked for.
* @param string $module_slug Module for which the meta capability is checked for.
* @return array Array with a 'do_not_allow' element if checks fail, empty array if checks pass.
*/
private function check_module_sharing_admin_capability( $cap, $user_id, $module_slug ) {
$module_sharing_settings = $this->modules->get_module_sharing_settings();
$sharing_settings = $module_sharing_settings->get();
if ( ! $this->is_user_authenticated( $user_id ) ) {
return array( 'do_not_allow' );
}
if ( self::MANAGE_MODULE_SHARING_OPTIONS === $cap &&
isset( $sharing_settings[ $module_slug ]['management'] ) &&
'all_admins' === $sharing_settings[ $module_slug ]['management']
) {
return array();
}
try {
$module = $this->modules->get_module( $module_slug );
if ( ! ( $module instanceof Module_With_Owner ) ) {
return array( 'do_not_allow' );
}
if ( $module->get_owner_id() !== $user_id ) {
return array( 'do_not_allow' );
}
} catch ( Exception $e ) {
return array( 'do_not_allow' );
}
return array();
}
/**
* Checks if a user is authenticated in Site Kit.
*
* @since 1.69.0
*
* @param int $user_id User ID of the user to be checked.
* @return bool True if the user is authenticated, false if not.
*/
public function is_user_authenticated( $user_id ) {
$restore_user = $this->user_options->switch_user( $user_id );
$is_user_authenticated = $this->authentication->is_authenticated();
$restore_user();
return $is_user_authenticated;
}
/**
* Checks if a user is verified in Site Kit.
*
* @since 1.69.0
*
* @param int $user_id User ID of the user to be checked.
* @return bool True if the user is verified, false if not.
*/
public function is_user_verified( $user_id ) {
$restore_user = $this->user_options->switch_user( $user_id );
$is_user_verfied = $this->authentication->verification()->has();
$restore_user();
return $is_user_verfied;
}
/**
* Checks if a user has dimissed the shared dashboard splash screen message.
*
* @since 1.69.0
*
* @param int $user_id User ID of the user to be checked.
* @return bool True if the user has dismissed the splash message, false if not.
*/
private function is_shared_dashboard_splash_dismissed( $user_id ) {
$restore_user = $this->user_options->switch_user( $user_id );
$is_splash_dismissed = $this->dismissed_items->is_dismissed( 'shared_dashboard_splash' );
$restore_user();
return $is_splash_dismissed;
}
/**
* Grants custom capabilities on-the-fly, based on core capabilities.
*
* If you want to instead set up your own custom role or mechanism to grant these capabilities, you can set a
* constant flag `GOOGLESITEKIT_DISABLE_DYNAMIC_CAPABILITIES` to ensure this function is not hooked in.
*
* @since 1.0.0
*
* @param array $allcaps Associative array of $capability => $grant pairs.
* @return array Filtered value of $allcaps.
*/
private function grant_additional_caps( array $allcaps ) {
foreach ( $this->base_to_core as $custom_cap => $core_cap ) {
if ( isset( $allcaps[ $core_cap ] ) ) {
$allcaps[ $custom_cap ] = $allcaps[ $core_cap ];
}
}
return $allcaps;
}
/**
* Gets all the base capabilities used in Google Site Kit.
*
* @since 1.31.0
*
* @return array
*/
public static function get_capabilities() {
$capabilities = array(
self::AUTHENTICATE,
self::SETUP,
self::VIEW_POSTS_INSIGHTS,
self::VIEW_DASHBOARD,
self::VIEW_MODULE_DETAILS,
self::MANAGE_OPTIONS,
);
if ( Feature_Flags::enabled( 'dashboardSharing' ) ) {
$capabilities[] = self::VIEW_SHARED_DASHBOARD;
}
return $capabilities;
}
/**
* Gets all the capabilities specifically added for dashboard sharing.
*
* @since 1.69.0
*
* @return array List of capabilities specific to dashboard sharing.
*/
public static function get_dashboard_sharing_capabilities() {
return array(
self::VIEW_SHARED_DASHBOARD,
self::READ_SHARED_MODULE_DATA,
self::MANAGE_MODULE_SHARING_OPTIONS,
self::DELEGATE_MODULE_SHARING_MANAGEMENT,
);
}
/**
* Gets all the meta capabilities specifically added for dashboard sharing.
*
* @since 1.70.0
*
* @return array List of meta capabilities specific to dashboard sharing.
*/
public static function get_dashboard_sharing_meta_capabilities() {
return array(
self::READ_SHARED_MODULE_DATA,
self::MANAGE_MODULE_SHARING_OPTIONS,
self::DELEGATE_MODULE_SHARING_MANAGEMENT,
);
}
}

View File

@@ -0,0 +1,161 @@
<?php
/**
* Data_Request
*
* @package Google\Site_Kit\Core\REST_API
* @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\REST_API;
/**
* Class Data_Request
*
* @since 1.0.0
*
* @property-read string $method Request method.
* @property-read string $type Request type.
* @property-read string $identifier Request identifier.
* @property-read string $datapoint Request datapoint.
* @property-read array $data Request data parameters.
* @property-read string $key Request key.
*/
class Data_Request implements \ArrayAccess {
/**
* Request method.
*
* @var string
*/
protected $method;
/**
* Request type.
*
* @var string
*/
protected $type;
/**
* Request identifier.
*
* @var string
*/
protected $identifier;
/**
* Request datapoint.
*
* @var string
*/
protected $datapoint;
/**
* Request data parameters.
*
* @var array
*/
protected $data;
/**
* Request key.
*
* @var string
*/
protected $key;
/**
* Data_Request constructor.
*
* @param string $method Request method.
* @param string $type Request type.
* @param string $identifier Request identifier.
* @param string $datapoint Request datapoint.
* @param array|self $data Request data parameters.
* @param string $key Request cache key.
*/
public function __construct(
$method = null,
$type = null,
$identifier = null,
$datapoint = null,
$data = array(),
$key = null
) {
$this->method = strtoupper( $method );
$this->type = $type;
$this->identifier = $identifier;
$this->datapoint = $datapoint;
$this->data = $data instanceof self ? $data->data : (array) $data;
$this->key = $key;
}
/**
* Gets the accessed property by the given name.
*
* @param string $name Property name.
*
* @return mixed
*/
public function __get( $name ) {
return isset( $this->$name ) ? $this->$name : null;
}
/**
* Checks whether or not the given magic property is set.
*
* @param string $name Property name.
*
* @return bool
*/
public function __isset( $name ) {
return isset( $this->$name );
}
/**
* Checks whether the given key exists.
*
* @param string|int $key Key to check.
*
* @return bool
*/
public function offsetExists( $key ) {
return array_key_exists( $key, $this->data );
}
/**
* Gets the value at the given key.
*
* @param string|int $key Key to return the value for.
*
* @return mixed
*/
public function offsetGet( $key ) {
if ( $this->offsetExists( $key ) ) {
return $this->data[ $key ];
}
return null;
}
/**
* Sets the given key to the given value.
*
* @param string|int $key Key to set the value for.
* @param mixed $value New value for the given key.
*/
public function offsetSet( $key, $value ) {
// Data is immutable.
}
/**
* Unsets the given key.
*
* @param string|int $key Key to unset.
*/
public function offsetUnset( $key ) {
// Data is immutable.
}
}

View File

@@ -0,0 +1,44 @@
<?php
/**
* Class Google\Site_Kit\Core\REST_API\Exception\Invalid_Datapoint_Exception
*
* @package Google\Site_Kit\Core\REST_API\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\REST_API\Exception;
use Google\Site_Kit\Core\Contracts\WP_Errorable;
use Exception;
use WP_Error;
/**
* Exception thrown when a request to an invalid datapoint is made.
*
* @since 1.9.0
* @access private
* @ignore
*/
class Invalid_Datapoint_Exception extends Exception implements WP_Errorable {
const WP_ERROR_CODE = 'invalid_datapoint';
/**
* 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,
__( 'Invalid datapoint.', 'google-site-kit' ),
array(
'status' => 400, // Bad request.
)
);
}
}

View File

@@ -0,0 +1,171 @@
<?php
/**
* Class Google\Site_Kit\Core\REST_API\REST_Route
*
* @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\REST_API;
use WP_REST_Server;
/**
* Class representing a single REST API route.
*
* @since 1.0.0
* @access private
* @ignore
*/
final class REST_Route {
/**
* Unique route URI.
*
* @since 1.0.0
* @var string
*/
private $uri;
/**
* Route arguments.
*
* @since 1.0.0
* @var array
*/
private $args = array();
/**
* Constructor.
*
* @since 1.0.0
*
* @param string $uri Unique route URI.
* @param array $endpoints {
* List of one or more endpoint arrays for a specific method, with the following data.
*
* @type string|array $methods One or more methods that the endpoint applies to.
* @type callable $callback Callback handling a request to the endpoint.
* @type callable $permission_callback Callback to check permissions for a request to the endpoint.
* @type array $args Associative array of supported parameters and their requirements.
* }
* @param array $args {
* Optional. Route options that typically include the following keys.
*
* @type array $args Associative array of globally supported parameters, e.g. those that are part of the URI.
* Default none.
* @type array $schema Public item schema for the route. Default none.
*/
public function __construct( $uri, array $endpoints, array $args = array() ) {
$this->uri = trim( $uri, '/' );
$this->args = $args;
if ( isset( $this->args['args'] ) ) {
$this->args['args'] = $this->parse_param_args( $this->args['args'] );
}
// In case there are string arguments, this is only a single endpoint and needs to be turned into a list.
if ( ! wp_is_numeric_array( $endpoints ) ) {
$endpoints = array( $endpoints );
}
$endpoint_defaults = array(
'methods' => WP_REST_Server::READABLE,
'callback' => null,
'args' => array(),
);
foreach ( $endpoints as $endpoint ) {
$endpoint = wp_parse_args( $endpoint, $endpoint_defaults );
$endpoint['args'] = $this->parse_param_args( $endpoint['args'] );
if ( ! empty( $this->args['args'] ) ) {
$endpoint['args'] = array_merge( $this->args['args'], $endpoint['args'] );
}
$this->args[] = $endpoint;
}
}
/**
* Registers the REST route.
*
* @since 1.16.0
*/
public function register() {
register_rest_route( REST_Routes::REST_ROOT, $this->get_uri(), $this->get_args() );
}
/**
* Gets the route URI.
*
* @since 1.0.0
*
* @return string Unique route URI.
*/
public function get_uri() {
return $this->uri;
}
/**
* Gets the route arguments, including endpoints and schema.
*
* @since 1.0.0
*
* @return array Route arguments.
*/
public function get_args() {
return $this->args;
}
/**
* Parses all supported request arguments and their data.
*
* @since 1.0.0
*
* @param array $args Associative array of $arg => $data pairs.
* @return array Parsed arguments.
*/
protected function parse_param_args( array $args ) {
return array_map( array( $this, 'parse_param_arg' ), $args );
}
/**
* Parses data for a supported request argument.
*
* @since 1.0.0
*
* @param array $data {
* Request argument data.
*
* @type string $type Data type of the argument. Default 'string'.
* @type string $description Public description of the argument. Default empty string.
* @†ype callable $validate_callback Callback to validate the argument. Default
* {@see rest_validate_rest_arg()}.
* @type callable $sanitize_callback Callback to sanitize the argument. Default
* {@see rest_sanitize_rest_arg()}.
* @type bool $required Whether the argument is required. Default false.
* @type mixed $default Default value for the argument, if any. Default none.
* @type array $enum Allowlist of possible values to validate against. Default none.
* @type array $items Only if $type is 'array': Similar specification that applies to each item.
* @type array $properties Only if $type is 'object'. Similar specification per property.
* }
* @return array Parsed data.
*/
protected function parse_param_arg( array $data ) {
return wp_parse_args(
$data,
array(
'type' => 'string',
'description' => '',
'validate_callback' => 'rest_validate_request_arg',
'sanitize_callback' => 'rest_sanitize_request_arg',
'required' => false,
'default' => null,
)
);
}
}

View File

@@ -0,0 +1,250 @@
<?php
/**
* Class Google\Site_Kit\Core\REST_API\REST_Routes
*
* @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\REST_API;
use Google\Site_Kit\Context;
use Google\Site_Kit\Core\Modules\Modules;
use Google\Site_Kit\Core\Modules\Module;
use Google\Site_Kit\Core\Permissions\Permissions;
use Google\Site_Kit\Core\Authentication\Authentication;
use Google\Site_Kit\Core\Authentication\Google_Proxy;
use Google\Site_Kit\Core\Util\Developer_Plugin_Installer;
use Google\Site_Kit\Core\Util\Reset;
use Google\Site_Kit\Core\Util\User_Input_Settings;
use WP_Post;
use WP_REST_Server;
use WP_REST_Request;
use WP_REST_Response;
use WP_Error;
/**
* Class managing REST API routes.
*
* @since 1.0.0
* @access private
* @ignore
*/
final class REST_Routes {
const REST_ROOT = 'google-site-kit/v1';
/**
* Plugin context.
*
* @since 1.0.0
* @var Context
*/
private $context;
/**
* Authentication instance.
*
* @since 1.0.0
* @var Authentication
*/
protected $authentication;
/**
* Modules instance.
*
* @since 1.0.0
* @var Modules
*/
protected $modules;
/**
* Constructor.
*
* @since 1.0.0
*
* @param Context $context Plugin context.
* @param Authentication $authentication Optional. Authentication instance. Default is a new instance.
* @param Modules $modules Optional. Modules instance. Default is a new instance.
*/
public function __construct( Context $context, Authentication $authentication = null, Modules $modules = null ) {
$this->context = $context;
if ( ! $authentication ) {
$authentication = new Authentication( $this->context );
}
$this->authentication = $authentication;
if ( ! $modules ) {
$modules = new Modules( $this->context, null, null, $this->authentication );
}
$this->modules = $modules;
}
/**
* Registers functionality through WordPress hooks.
*
* @since 1.0.0
*/
public function register() {
add_action(
'rest_api_init',
function() {
$this->register_routes();
}
);
add_filter(
'do_parse_request',
function( $do_parse_request, $wp ) {
add_filter(
'query_vars',
function( $vars ) use ( $wp ) {
// Unsets standard public query vars to escape conflicts between WordPress core
// and Google Site Kit APIs which happen when WordPress incorrectly parses request
// arguments.
$unset_vars = ( $wp->request && stripos( $wp->request, trailingslashit( rest_get_url_prefix() ) . self::REST_ROOT ) !== false ) // Check regular permalinks.
|| ( empty( $wp->request ) && stripos( $this->context->input()->filter( INPUT_GET, 'rest_route' ), self::REST_ROOT ) !== false ); // Check plain permalinks.
if ( $unset_vars ) {
// List of variable names to remove from public query variables list.
return array_values(
array_diff(
$vars,
array(
'orderby',
)
)
);
}
return $vars;
}
);
return $do_parse_request;
},
10,
2
);
}
/**
* Registers all REST routes.
*
* @since 1.0.0
* @since 1.16.0 Reworked to use REST_Route::register method to register a route.
*/
private function register_routes() {
$routes = $this->get_routes();
foreach ( $routes as $route ) {
$route->register();
}
}
/**
* Gets available REST routes.
*
* @since 1.0.0
* @since 1.3.0 Moved most routes into individual classes and introduced {@see 'googlesitekit_rest_routes'} filter.
*
* @return array List of REST_Route instances.
*/
private function get_routes() {
$can_view_insights = function() {
// This accounts for routes that need to be called before user has completed setup flow.
if ( current_user_can( Permissions::SETUP ) ) {
return true;
}
return current_user_can( Permissions::VIEW_POSTS_INSIGHTS );
};
$can_authenticate = function() {
return current_user_can( Permissions::AUTHENTICATE );
};
$routes = array(
new REST_Route(
'core/user/data/user-input-settings',
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => function( WP_REST_Request $request ) {
$user_input_settings = new User_Input_Settings( $this->context, $this->authentication );
return rest_ensure_response( $user_input_settings->get_settings() );
},
'permission_callback' => $can_authenticate,
),
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => function( WP_REST_Request $request ) {
$user_input_settings = new User_Input_Settings( $this->context, $this->authentication );
$data = $request->get_param( 'data' );
if ( ! isset( $data['settings'] ) || ! is_array( $data['settings'] ) ) {
return new WP_Error(
'rest_missing_callback_param',
__( 'Missing settings data.', 'google-site-kit' ),
array( 'status' => 400 )
);
}
return rest_ensure_response(
$user_input_settings->set_settings(
$data['settings']
)
);
},
'permission_callback' => $can_authenticate,
'args' => array(
'data' => array(
'type' => 'object',
'required' => true,
'properties' => array(
'settings' => array(
'type' => 'object',
'required' => true,
'properties' => array(
'role' => array(
'type' => 'array',
'items' => array( 'type' => 'string' ),
),
'postFrequency' => array(
'type' => 'array',
'items' => array( 'type' => 'string' ),
),
'goals' => array(
'type' => 'array',
'items' => array( 'type' => 'string' ),
),
'helpNeeded' => array(
'type' => 'array',
'items' => array( 'type' => 'string' ),
),
'searchTerms' => array(
'type' => 'array',
'items' => array( 'type' => 'string' ),
),
),
),
),
),
),
),
)
),
);
/**
* Filters the list of available REST routes.
*
* @since 1.3.0
*
* @param array $routes List of REST_Route objects.
*/
return apply_filters( 'googlesitekit_rest_routes', $routes );
}
}

View File

@@ -0,0 +1,137 @@
<?php
/**
* Class Google\Site_Kit\Core\Storage\Cache
*
* @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\Storage;
use Google\Site_Kit_Dependencies\Google\Service\Exception as Google_Service_Exception;
/**
* Class providing a server side caching framework.
*
* @since 1.0.0
* @access private
* @ignore
*/
final class Cache {
/**
* The key for saving the global cache keys.
*
* @var string $global_cache_keys_key The key.
*/
private static $global_cache_keys_key = 'googlesitekit_global_cache_keys';
/**
* The global record of cache keys used on the site.
*
* @var array
*/
private $global_cache_keys;
/**
* Construct the Cache class.
*/
public function __construct() {
$this->global_cache_keys = get_option( self::$global_cache_keys_key ) ?: array();
}
/**
* Helper function to get the cache data.
*/
public function get_current_cache_data() {
$cache_data = array();
// Add the global cache data.
$keys = $this->get_global_cache_keys();
foreach ( $keys as $key ) {
// This only retrieves fresh data because transients expire.
$cache = get_transient( 'googlesitekit_' . $key );
if ( $cache ) {
$cache_data[ $key ] = $cache;
} else {
// Remove the expired key from the global cache.
$this->remove_global_cache_key( $key );
}
}
return $cache_data;
}
/**
* Remove a cache key to the global record of cache keys.
*
* @param string $key The key to add.
*/
private function remove_global_cache_key( $key ) {
$key_index = array_search( $key, $this->global_cache_keys, true );
if ( $key_index ) {
unset( $this->global_cache_keys[ $key_index ] );
update_option( self::$global_cache_keys_key, $this->global_cache_keys, false );
}
}
/**
* Add a cache key to the global record of cache keys.
*
* @param string $key The key to add.
*/
private function add_global_cache_key( $key ) {
// Only add the key if it isn't already present.
if ( ! in_array( $key, $this->global_cache_keys, true ) ) {
$this->global_cache_keys[] = $key;
update_option( self::$global_cache_keys_key, $this->global_cache_keys, false );
}
}
/**
* Retrieve the global record of cache keys.
*
* @return array The array of cache keys used on the site.
*/
private function get_global_cache_keys() {
return $this->global_cache_keys;
}
/**
* Cache some data.
*
* @param Object $key The original data key.
* @param Object $data The data to cache.
*/
public function set_cache_data( $key, $data ) {
set_transient( 'googlesitekit_' . $key, $data, HOUR_IN_SECONDS );
$this->add_global_cache_key( $key );
}
/**
* Cache the results of a batch operation.
*
* @param array $batch_requests The original requests.
* @param array $results The results to cache.
*/
public function cache_batch_results( $batch_requests, $results ) {
$request_keys = wp_list_pluck( $batch_requests, 'key' );
foreach ( $results as $key => $result ) {
if ( $result instanceof \Exception || $result instanceof Google_Service_Exception ) {
continue;
}
$key = str_replace( 'response-', '', $key );
if ( in_array( $key, $request_keys, true ) ) {
$this->set_cache_data( $key, $result );
}
}
}
}

View File

@@ -0,0 +1,145 @@
<?php
/**
* Class Google\Site_Kit\Core\Storage\Data_Encryption
*
* @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\Storage;
/**
* Class responsible for encrypting and decrypting data.
*
* @since 1.0.0
* @access private
* @ignore
*/
final class Data_Encryption {
/**
* Key to use for encryption.
*
* @since 1.0.0
* @var string
*/
private $key;
/**
* Salt to use for encryption.
*
* @since 1.0.0
* @var string
*/
private $salt;
/**
* Constructor.
*
* @since 1.0.0
*/
public function __construct() {
$this->key = $this->get_default_key();
$this->salt = $this->get_default_salt();
}
/**
* Encrypts a value.
*
* If a user-based key is set, that key is used. Otherwise the default key is used.
*
* @since 1.0.0
*
* @param string $value Value to encrypt.
* @return string|bool Encrypted value, or false on failure.
*/
public function encrypt( $value ) {
if ( ! extension_loaded( 'openssl' ) ) {
return $value;
}
$method = 'aes-256-ctr';
$ivlen = openssl_cipher_iv_length( $method );
$iv = openssl_random_pseudo_bytes( $ivlen );
$raw_value = openssl_encrypt( $value . $this->salt, $method, $this->key, 0, $iv );
if ( ! $raw_value ) {
return false;
}
return base64_encode( $iv . $raw_value );
}
/**
* Decrypts a value.
*
* If a user-based key is set, that key is used. Otherwise the default key is used.
*
* @since 1.0.0
*
* @param string $raw_value Value to decrypt.
* @return string|bool Decrypted value, or false on failure.
*/
public function decrypt( $raw_value ) {
if ( ! extension_loaded( 'openssl' ) ) {
return $raw_value;
}
$raw_value = base64_decode( $raw_value, true );
$method = 'aes-256-ctr';
$ivlen = openssl_cipher_iv_length( $method );
$iv = substr( $raw_value, 0, $ivlen );
$raw_value = substr( $raw_value, $ivlen );
$value = openssl_decrypt( $raw_value, $method, $this->key, 0, $iv );
if ( ! $value || substr( $value, - strlen( $this->salt ) ) !== $this->salt ) {
return false;
}
return substr( $value, 0, - strlen( $this->salt ) );
}
/**
* Gets the default encryption key to use.
*
* @since 1.0.0
*
* @return string Default (not user-based) encryption key.
*/
private function get_default_key() {
if ( defined( 'GOOGLESITEKIT_ENCRYPTION_KEY' ) && '' !== GOOGLESITEKIT_ENCRYPTION_KEY ) {
return GOOGLESITEKIT_ENCRYPTION_KEY;
}
if ( defined( 'LOGGED_IN_KEY' ) && '' !== LOGGED_IN_KEY ) {
return LOGGED_IN_KEY;
}
// If this is reached, you're either not on a live site or have a serious security issue.
return 'das-ist-kein-geheimer-schluessel';
}
/**
* Gets the default encryption salt to use.
*
* @since 1.0.0
*
* @return string Encryption salt.
*/
private function get_default_salt() {
if ( defined( 'GOOGLESITEKIT_ENCRYPTION_SALT' ) && '' !== GOOGLESITEKIT_ENCRYPTION_SALT ) {
return GOOGLESITEKIT_ENCRYPTION_SALT;
}
if ( defined( 'LOGGED_IN_SALT' ) && '' !== LOGGED_IN_SALT ) {
return LOGGED_IN_SALT;
}
// If this is reached, you're either not on a live site or have a serious security issue.
return 'das-ist-kein-geheimes-salz';
}
}

View File

@@ -0,0 +1,115 @@
<?php
/**
* Class Google\Site_Kit\Core\Storage\Encrypted_Options
*
* @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\Storage;
/**
* Class providing access to encrypted options.
*
* @since 1.0.0
* @access private
* @ignore
*/
final class Encrypted_Options implements Options_Interface {
/**
* Data Encryption API instance.
*
* @since 1.0.0
* @var Data_Encryption
*/
private $encryption;
/**
* Option API instance.
*
* @since 1.0.0
* @var Options
*/
private $options;
/**
* Constructor.
*
* @since 1.0.0
*
* @param Options $options Option API instance.
*/
public function __construct( Options $options ) {
$this->encryption = new Data_Encryption();
$this->options = $options;
}
/**
* Checks whether or not a value is set for the given option.
*
* @since 1.3.0
*
* @param string $option Option name.
* @return bool True if value set, false otherwise.
*/
public function has( $option ) {
return $this->options->has( $option );
}
/**
* Gets the value of the given option.
*
* @since 1.0.0
*
* @param string $option Option name.
* @return mixed Value set for the option, or false if not set.
*/
public function get( $option ) {
$raw_value = $this->options->get( $option );
// If there is no value stored, return the default which will not be encrypted.
if ( ! $this->options->has( $option ) ) {
return $raw_value;
}
$data = $this->encryption->decrypt( $raw_value );
return maybe_unserialize( $data );
}
/**
* Sets the value for a option.
*
* @since 1.0.0
*
* @param string $option Option name.
* @param mixed $value Option value. Must be serializable if non-scalar.
* @return bool True on success, false on failure.
*/
public function set( $option, $value ) {
if ( ! is_scalar( $value ) ) {
$value = maybe_serialize( $value );
}
$raw_value = $this->encryption->encrypt( $value );
if ( ! $raw_value ) {
return false;
}
return $this->options->set( $option, $raw_value );
}
/**
* Deletes the given option.
*
* @since 1.0.0
*
* @param string $option Option name.
* @return bool True on success, false on failure.
*/
public function delete( $option ) {
return $this->options->delete( $option );
}
}

View File

@@ -0,0 +1,135 @@
<?php
/**
* Class Google\Site_Kit\Core\Storage\Encrypted_User_Options
*
* @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\Storage;
/**
* Class providing access to encrypted per-user options.
*
* @since 1.0.0
* @access private
* @ignore
*/
final class Encrypted_User_Options implements User_Options_Interface {
/**
* Data Encryption API instance.
*
* @since 1.0.0
* @var Data_Encryption
*/
private $encryption;
/**
* User Option API instance.
*
* @since 1.0.0
* @var User_Options
*/
private $user_options;
/**
* Constructor.
*
* @since 1.0.0
*
* @param User_Options $user_options User Option API instance.
*/
public function __construct( User_Options $user_options ) {
$this->encryption = new Data_Encryption();
$this->user_options = $user_options;
}
/**
* Gets the value of the given user option.
*
* @since 1.0.0
*
* @param string $option User option name.
* @return mixed Value set for the user option, or false if not set.
*/
public function get( $option ) {
$raw_value = $this->user_options->get( $option );
if ( ! $raw_value ) {
return false;
}
$data = $this->encryption->decrypt( $raw_value );
return maybe_unserialize( $data );
}
/**
* Sets the value for a user option.
*
* @since 1.0.0
*
* @param string $option User option name.
* @param mixed $value User option value. Must be serializable if non-scalar.
* @return bool True on success, false on failure.
*/
public function set( $option, $value ) {
if ( ! is_scalar( $value ) ) {
$value = maybe_serialize( $value );
}
$raw_value = $this->encryption->encrypt( $value );
if ( ! $raw_value ) {
return false;
}
return $this->user_options->set( $option, $raw_value );
}
/**
* Deletes the given user option.
*
* @since 1.0.0
*
* @param string $option User option name.
* @return bool True on success, false on failure.
*/
public function delete( $option ) {
return $this->user_options->delete( $option );
}
/**
* Gets the underlying meta key for the given option.
*
* @since 1.4.0
*
* @param string $option Option name.
* @return string Meta key name.
*/
public function get_meta_key( $option ) {
return $this->user_options->get_meta_key( $option );
}
/**
* Gets the ID of the user that options are controlled for.
*
* @since 1.4.0
*
* @return int User ID.
*/
public function get_user_id() {
return $this->user_options->get_user_id();
}
/**
* Switches the user that options are controlled for to the one with the given ID.
*
* @since 1.4.0
*
* @param int $user_id User ID.
*/
public function switch_user( $user_id ) {
$this->user_options->switch_user( $user_id );
}
}

View File

@@ -0,0 +1,116 @@
<?php
/**
* Class Google\Site_Kit\Core\Storage\Options
*
* @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\Storage;
use Google\Site_Kit\Context;
/**
* Class providing access to options.
*
* It uses regular options or network options, depending on in which mode the plugin is running.
*
* @since 1.0.0
* @access private
* @ignore
*/
final class Options implements Options_Interface {
/**
* Plugin context.
*
* @since 1.0.0
* @var Context
*/
private $context;
/**
* Constructor.
*
* @since 1.0.0
*
* @param Context $context Plugin context.
*/
public function __construct( Context $context ) {
$this->context = $context;
}
/**
* Checks whether or not a value is set for the given option.
*
* @since 1.3.0
*
* @param string $option Option name.
* @return bool True if value set, false otherwise.
*/
public function has( $option ) {
// Call without getting the value to ensure 'notoptions' cache is fresh for the option.
$this->get( $option );
if ( $this->context->is_network_mode() ) {
$network_id = get_current_network_id();
$notoptions = wp_cache_get( "$network_id:notoptions", 'site-options' );
} else {
$notoptions = wp_cache_get( 'notoptions', 'options' );
}
return ! isset( $notoptions[ $option ] );
}
/**
* Gets the value of the given option.
*
* @since 1.0.0
*
* @param string $option Option name.
* @return mixed Value set for the option, or false if not set.
*/
public function get( $option ) {
if ( $this->context->is_network_mode() ) {
return get_network_option( null, $option );
}
return get_option( $option );
}
/**
* Sets the value for a option.
*
* @since 1.0.0
*
* @param string $option Option name.
* @param mixed $value Option value. Must be serializable if non-scalar.
* @return bool True on success, false on failure.
*/
public function set( $option, $value ) {
if ( $this->context->is_network_mode() ) {
return update_network_option( null, $option, $value );
}
return update_option( $option, $value );
}
/**
* Deletes the given option.
*
* @since 1.0.0
*
* @param string $option Option name.
* @return bool True on success, false on failure.
*/
public function delete( $option ) {
if ( $this->context->is_network_mode() ) {
return delete_network_option( null, $option );
}
return delete_option( $option );
}
}

View File

@@ -0,0 +1,62 @@
<?php
/**
* Interface Google\Site_Kit\Core\Storage\Options_Interface
*
* @package Google\Site_Kit\Core\Storage
* @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\Storage;
/**
* Interface for Options implementations.
*
* @since 1.2.0
* @access private
* @ignore
*/
interface Options_Interface {
/**
* Checks whether or not a value is set for the given option.
*
* @since 1.3.0
*
* @param string $option Option name.
* @return bool True if value set, false otherwise.
*/
public function has( $option );
/**
* Gets the value of the given option.
*
* @since 1.2.0
*
* @param string $option Option name.
* @return mixed Value set for the option, or false if not set.
*/
public function get( $option );
/**
* Sets the value for a option.
*
* @since 1.2.0
*
* @param string $option Option name.
* @param mixed $value Option value. Must be serializable if non-scalar.
* @return bool True on success, false on failure.
*/
public function set( $option, $value );
/**
* Deletes the given option.
*
* @since 1.2.0
*
* @param string $option Option name.
* @return bool True on success, false on failure.
*/
public function delete( $option );
}

View File

@@ -0,0 +1,80 @@
<?php
/**
* Class Google\Site_Kit\Core\Storage\Post_Meta
*
* @package Google\Site_Kit\Core\Storage
* @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\Storage;
/**
* Post metadata storage class.
*
* @since 1.33.0
* @access private
* @ignore
*/
final class Post_Meta implements Post_Meta_Interface {
/**
* Gets post meta.
*
* @since 1.33.0
*
* @param int $post_id Post ID.
* @param string $key Metadata key.
* @param bool $single Whether to return a single value.
* @return mixed Post meta value.
*/
public function get( $post_id, $key, $single = false ) {
return get_post_meta( $post_id, $key, $single );
}
/**
* Updates a post meta field based on the given post ID.
*
* @since 1.33.0
*
* @param int $post_id Post ID.
* @param string $key Metadata key.
* @param mixed $value Metadata value.
* @param mixed $prev_value Previous value to check before updating. If specified, only update existing metadata entries with this value. Otherwise, update all entries.
* @return bool TRUE on success, otherwise FALSE.
*/
public function update( $post_id, $key, $value, $prev_value = '' ) {
return update_post_meta( $post_id, $key, $value, $prev_value );
}
/**
* Adds a meta field to the given post.
*
* @since 1.33.0
*
* @param int $post_id Post ID.
* @param string $key Metadata key.
* @param mixed $value Metadata value.
* @param bool $unique Whether the same key should not be added.
* @return int|bool Meta id on success, otherwise FALSE.
*/
public function add( $post_id, $key, $value, $unique = false ) {
return add_post_meta( $post_id, $key, $value, $unique );
}
/**
* Deletes a post meta field for the given post ID.
*
* @since 1.33.0
*
* @param int $post_id Post ID.
* @param string $key Metadata key.
* @param mixed $value Metadata value. If provided, rows will only be removed that match the value.
* @return bool TRUE on success, otherwise FALSE.
*/
public function delete( $post_id, $key, $value = '' ) {
return delete_post_meta( $post_id, $key, $value );
}
}

View File

@@ -0,0 +1,72 @@
<?php
/**
* Interface Google\Site_Kit\Core\Storage\Post_Meta_Interface
*
* @package Google\Site_Kit\Core\Storage
* @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\Storage;
/**
* Interface for Post_Meta implementations.
*
* @since 1.33.0
* @access private
* @ignore
*/
interface Post_Meta_Interface {
/**
* Gets post meta.
*
* @since 1.33.0
*
* @param int $post_id Post ID.
* @param string $key Metadata key.
* @param bool $single Whether to return a single value.
* @return mixed Post meta value.
*/
public function get( $post_id, $key, $single = false );
/**
* Updates a post meta field based on the given post ID.
*
* @since 1.33.0
*
* @param int $post_id Post ID.
* @param string $key Metadata key.
* @param mixed $value Metadata value.
* @param mixed $prev_value Previous value to check before updating. If specified, only update existing metadata entries with this value. Otherwise, update all entries.
* @return bool TRUE on success, otherwise FALSE.
*/
public function update( $post_id, $key, $value, $prev_value = '' );
/**
* Adds a meta field to the given post.
*
* @since 1.33.0
*
* @param int $post_id Post ID.
* @param string $key Metadata key.
* @param mixed $value Metadata value.
* @param bool $unique Whether the same key should not be added.
* @return int|bool Meta id on success, otherwise FALSE.
*/
public function add( $post_id, $key, $value, $unique = false );
/**
* Deletes a post meta field for the given post ID.
*
* @since 1.33.0
*
* @param int $post_id Post ID.
* @param string $key Metadata key.
* @param mixed $value Metadata value. If provided, rows will only be removed that match the value.
* @return bool TRUE on success, otherwise FALSE.
*/
public function delete( $post_id, $key, $value = '' );
}

View File

@@ -0,0 +1,174 @@
<?php
/**
* Class Google\Site_Kit\Core\Storage\Post_Meta_Setting
*
* @package Google\Site_Kit\Core\Storage
* @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\Storage;
/**
* Base class for a single post meta setting.
*
* @since 1.33.0
* @access private
* @ignore
*/
abstract class Post_Meta_Setting {
/**
* The post meta key for this setting.
* Override in a sub-class.
*/
const META_KEY = '';
/**
* Post_Meta_Interface implementation.
*
* @since 1.33.0
* @var Post_Meta_Interface
*/
protected $post_meta;
/**
* Post_Meta_Setting constructor.
*
* @since 1.33.0
*
* @param Post_Meta_Interface $post_meta Post_Meta_Interface instance.
*/
public function __construct( Post_Meta_Interface $post_meta ) {
$this->post_meta = $post_meta;
}
/**
* Registers the post setting in WordPress.
*
* @since 1.33.0
*/
public function register() {
register_meta(
'post',
static::META_KEY,
array(
'type' => $this->get_type(),
'sanitize_callback' => $this->get_sanitize_callback(),
'single' => true,
'show_in_rest' => $this->get_show_in_rest(),
)
);
}
/**
* Gets the expected value type.
*
* Returns 'string' by default for consistency with register_meta.
* Override in a sub-class if different.
*
* Valid values are 'string', 'boolean', 'integer', 'number', 'array', and 'object'.
*
* @since 1.33.0
*
* @return string The type name.
*/
protected function get_type() {
return 'string';
}
/**
* Gets the default value.
*
* Returns an empty string by default.
* Override in a sub-class if different.
*
* @since 1.33.0
*
* @return mixed The default value.
*/
protected function get_default() {
return '';
}
/**
* Gets the callback for sanitizing the setting's value before saving.
*
* For use internally with register_meta.
* Returns `null` for consistency with the default in register_meta.
* Override in a sub-class.
*
* @since 1.33.0
*
* @return callable|null Sanitize callback function.
*/
protected function get_sanitize_callback() {
return null;
}
/**
* Gets the `show_in_rest` value for this postmeta setting value.
*
* @since 1.37.0
*
* @return bool|Array Any valid value for the `show_in_rest`
*/
protected function get_show_in_rest() {
return false;
}
/**
* Checks whether a post meta exists or not.
*
* @since 1.33.0
*
* @param int $post_id Post ID.
* @return bool True if the meta key exists, otherwise false.
*/
public function has( $post_id ) {
return metadata_exists( 'post', $post_id, static::META_KEY );
}
/**
* Gets the value of the setting.
*
* @since 1.33.0
*
* @param int $post_id Post ID.
* @return mixed Value set for the setting, or default if not set.
*/
public function get( $post_id ) {
if ( ! $this->has( $post_id ) ) {
return $this->get_default();
}
return $this->post_meta->get( $post_id, static::META_KEY, true );
}
/**
* Updates the post setting for the given post ID.
*
* @since 1.33.0
*
* @param int $post_id Post ID.
* @param mixed $value Metadata value.
* @return bool TRUE on success, otherwise FALSE.
*/
public function set( $post_id, $value ) {
return $this->post_meta->update( $post_id, static::META_KEY, $value );
}
/**
* Deletes the post setting for the given post ID.
*
* @since 1.33.0
*
* @param int $post_id Post ID.
* @return bool TRUE on success, otherwise FALSE.
*/
public function delete( $post_id ) {
return $this->post_meta->delete( $post_id, static::META_KEY );
}
}

View File

@@ -0,0 +1,154 @@
<?php
/**
* Class Google\Site_Kit\Core\Storage
*
* @package Google\Site_Kit\Core\Storage
* @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\Storage;
/**
* Base class for a single setting.
*
* @since 1.2.0
* @access private
* @ignore
*/
abstract class Setting {
/**
* The option_name for this setting.
* Override in a sub-class.
*/
const OPTION = '';
/**
* Options instance implementing Options_Interface.
*
* @since 1.2.0
* @var Options_Interface
*/
protected $options;
/**
* Setting constructor.
*
* @since 1.2.0
*
* @param Options_Interface $options Options_Interface instance.
*/
public function __construct( Options_Interface $options ) {
$this->options = $options;
}
/**
* Registers the setting in WordPress.
*
* @since 1.2.0
*/
public function register() {
register_setting(
static::OPTION,
static::OPTION,
array(
'type' => $this->get_type(),
'sanitize_callback' => $this->get_sanitize_callback(),
'default' => $this->get_default(),
)
);
}
/**
* Checks whether or not the option is set with a valid value.
*
* @since 1.2.0
* @since 1.3.0 Now relies on {@see Options_Interface::has()}.
*
* @return bool True on success, false on failure.
*/
public function has() {
return $this->options->has( static::OPTION );
}
/**
* Gets the value of the setting.
*
* @since 1.2.0
*
* @return mixed Value set for the option, or registered default if not set.
*/
public function get() {
return $this->options->get( static::OPTION );
}
/**
* Sets the value of the setting with the given value.
*
* @since 1.2.0
*
* @param mixed $value Setting value. Must be serializable if non-scalar.
*
* @return bool True on success, false on failure.
*/
public function set( $value ) {
return $this->options->set( static::OPTION, $value );
}
/**
* Deletes the setting.
*
* @since 1.2.0
*
* @return bool True on success, false on failure.
*/
public function delete() {
return $this->options->delete( static::OPTION );
}
/**
* Gets the expected value type.
*
* Returns 'string' by default for consistency with register_setting.
* Override in a sub-class if different.
*
* @since 1.2.0
*
* @return string The type name.
*/
protected function get_type() {
return 'string';
}
/**
* Gets the default value.
*
* For use with register_setting and fetching the default directly.
* Returns false by default for consistency with get_option.
* Override in a sub-class if different.
*
* @since 1.2.0
*
* @return mixed The default value.
*/
protected function get_default() {
return false;
}
/**
* Gets the callback for sanitizing the setting's value before saving.
*
* For use internally with register_setting.
* Returns `null` for consistency with the default in register_setting.
* Override in a sub-class.
*
* @since 1.2.0
*
* @return callable|null
*/
protected function get_sanitize_callback() {
return null;
}
}

View File

@@ -0,0 +1,44 @@
<?php
/**
* Trait Google\Site_Kit\Core\Storage\Setting_With_Legacy_Keys_Trait
*
* @package Google\Site_Kit\Core\Storage
* @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\Storage;
use \Google\Site_Kit\Core\Util\Migrate_Legacy_Keys;
/**
* Trait for a Setting that has legacy option keys to migrate.
*
* @since 1.2.0
* @access private
* @ignore
*/
trait Setting_With_Legacy_Keys_Trait {
use Migrate_Legacy_Keys;
/**
* Registers an option filter for the setting to migrate legacy keys.
*
* @param array $legacy_key_map Mapping of legacy keys to current key.
*
* @since 1.2.0
*/
protected function register_legacy_keys_migration( array $legacy_key_map ) {
add_filter(
'option_' . static::OPTION,
function ( $option ) use ( $legacy_key_map ) {
if ( is_array( $option ) ) {
return $this->migrate_legacy_keys( $option, $legacy_key_map );
}
return $option;
},
0
);
}
}

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