Commit inicial - WordPress Análisis de Precios Unitarios

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

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

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

View File

@@ -0,0 +1,143 @@
<?php
/**
* Class Google\Site_Kit\Core\Util\Activation_Flag
*
* @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\Util;
use Google\Site_Kit\Context;
use Google\Site_Kit\Core\Storage\Options;
/**
* Class handling plugin activation.
*
* @since 1.10.0
* @access private
* @ignore
*/
final class Activation_Flag {
const OPTION_SHOW_ACTIVATION_NOTICE = 'googlesitekit_show_activation_notice';
const OPTION_NEW_SITE_POSTS = 'googlesitekit_new_site_posts';
/**
* Plugin context.
*
* @since 1.10.0
* @var Context
*/
private $context;
/**
* Option API instance.
*
* @since 1.10.0
* @var Options
*/
protected $options;
/**
* Constructor.
*
* @since 1.10.0
*
* @param Context $context Plugin context.
* @param Options $options Optional. The Option API instance. Default is a new instance.
*/
public function __construct(
Context $context,
?Options $options = null
) {
$this->context = $context;
$this->options = $options ?: new Options( $this->context );
}
/**
* Registers functionality through WordPress hooks.
*
* @since 1.10.0
*/
public function register() {
add_action(
'googlesitekit_activation',
function ( $network_wide ) {
// Set activation flag.
$this->set_activation_flag( $network_wide );
}
);
add_filter(
'googlesitekit_admin_data',
function ( $data ) {
return $this->inline_js_admin_data( $data );
}
);
}
/**
* Sets the flag that the plugin has just been activated.
*
* @since 1.10.0 Migrated from Activation class.
*
* @param bool $network_wide Whether the plugin is being activated network-wide.
*/
public function set_activation_flag( $network_wide ) {
if ( $network_wide ) {
update_network_option( null, self::OPTION_SHOW_ACTIVATION_NOTICE, '1' );
return;
}
update_option( self::OPTION_SHOW_ACTIVATION_NOTICE, '1', false );
}
/**
* Gets the flag that the plugin has just been activated.
*
* @since 1.10.0 Migrated from Activation class.
*
* @param bool $network_wide Whether to check the flag network-wide.
* @return bool True if just activated, false otherwise.
*/
public function get_activation_flag( $network_wide ) {
if ( $network_wide ) {
return (bool) get_network_option( null, self::OPTION_SHOW_ACTIVATION_NOTICE );
}
return (bool) get_option( self::OPTION_SHOW_ACTIVATION_NOTICE );
}
/**
* Deletes the flag that the plugin has just been activated.
*
* @since 1.10.0 Migrated from Activation class.
*
* @param bool $network_wide Whether the plugin is being activated network-wide.
*/
public function delete_activation_flag( $network_wide ) {
if ( $network_wide ) {
delete_network_option( null, self::OPTION_SHOW_ACTIVATION_NOTICE );
return;
}
delete_option( self::OPTION_SHOW_ACTIVATION_NOTICE );
}
/**
* Modifies the admin data to pass to JS.
*
* @since 1.10.0 Migrated from Activation class.
*
* @param array $data Inline JS data.
* @return array Filtered $data.
*/
private function inline_js_admin_data( $data ) {
$data['newSitePosts'] = $this->options->get( self::OPTION_NEW_SITE_POSTS );
return $data;
}
}

View File

@@ -0,0 +1,157 @@
<?php
/**
* Class Google\Site_Kit\Core\Util\Activation_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\Util;
use Google\Site_Kit\Context;
use Google\Site_Kit\Core\Admin\Notice;
use Google\Site_Kit\Core\Assets\Assets;
use Google\Site_Kit\Core\Util\Requires_Javascript_Trait;
/**
* Class handling plugin activation.
*
* @since 1.10.0 Renamed from Activation.
* @access private
* @ignore
*/
final class Activation_Notice {
use Requires_Javascript_Trait;
/**
* Plugin context.
*
* @since 1.10.0
* @var Context
*/
private $context;
/**
* Activation flag instance.
*
* @since 1.10.0
* @var Activation_Flag
*/
protected $activation_flag;
/**
* Assets API instance.
*
* @since 1.10.0
* @var Assets
*/
protected $assets;
/**
* Constructor.
*
* @since 1.10.0
*
* @param Context $context Plugin context.
* @param Activation_Flag $activation_flag Activation flag instance.
* @param Assets $assets Optional. The Assets API instance. Default is a new instance.
*/
public function __construct(
Context $context,
Activation_Flag $activation_flag,
?Assets $assets = null
) {
$this->context = $context;
$this->activation_flag = $activation_flag;
$this->assets = $assets ?: new Assets( $this->context );
}
/**
* Registers functionality through WordPress hooks.
*
* @since 1.10.0
*/
public function register() {
add_filter(
'googlesitekit_admin_notices',
function ( $notices ) {
$notices[] = $this->get_activation_notice();
return $notices;
}
);
add_action(
'admin_enqueue_scripts',
function ( $hook_suffix ) {
if ( 'plugins.php' !== $hook_suffix || ! $this->activation_flag->get_activation_flag( is_network_admin() ) ) {
return;
}
/**
* Prevent the default WordPress "Plugin Activated" notice from rendering.
*
* @link https://github.com/WordPress/WordPress/blob/e1996633228749cdc2d92bc04cc535d45367bfa4/wp-admin/plugins.php#L569-L570
*/
unset( $_GET['activate'] ); // phpcs:ignore WordPress.Security.NonceVerification, WordPress.VIP.SuperGlobalInputUsage
$this->assets->enqueue_asset( 'googlesitekit-admin-css' );
$this->assets->enqueue_asset( 'googlesitekit-activation' );
}
);
}
/**
* Gets the admin notice indicating that the plugin has just been activated.
*
* @since 1.10.0
*
* @return Notice Admin notice instance.
*/
private function get_activation_notice() {
return new Notice(
'activated',
array(
'content' => function () {
ob_start();
?>
<div class="googlesitekit-plugin">
<?php $this->render_noscript_html(); ?>
<div id="js-googlesitekit-activation" class="googlesitekit-activation googlesitekit-activation--loading">
<div class="googlesitekit-activation__loading">
<div role="progressbar" class="mdc-linear-progress mdc-linear-progress--indeterminate">
<div class="mdc-linear-progress__buffering-dots"></div>
<div class="mdc-linear-progress__buffer"></div>
<div class="mdc-linear-progress__bar mdc-linear-progress__primary-bar">
<span class="mdc-linear-progress__bar-inner"></span>
</div>
<div class="mdc-linear-progress__bar mdc-linear-progress__secondary-bar">
<span class="mdc-linear-progress__bar-inner"></span>
</div>
</div>
</div>
</div>
</div>
<?php
return ob_get_clean();
},
'type' => Notice::TYPE_SUCCESS,
'active_callback' => function ( $hook_suffix ) {
if ( 'plugins.php' !== $hook_suffix ) {
return false;
}
$network_wide = is_network_admin();
$flag = $this->activation_flag->get_activation_flag( $network_wide );
if ( $flag ) {
// Unset the flag so that the notice only shows once.
$this->activation_flag->delete_activation_flag( $network_wide );
}
return $flag;
},
'dismissible' => true,
)
);
}
}

View File

@@ -0,0 +1,163 @@
<?php
/**
* Class Google\Site_Kit\Core\Util\Auto_Updates
*
* @package Google\Site_Kit\Core\Util
* @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\Util;
use stdClass;
/**
* Utility class for auto-updates settings.
*
* @since 1.93.0
* @access private
* @ignore
*/
class Auto_Updates {
/**
* Auto updated forced enabled.
*
* @since 1.93.0
* @var true
*/
const AUTO_UPDATE_FORCED_ENABLED = true;
/**
* Auto updated forced disabled.
*
* @since 1.93.0
* @var false
*/
const AUTO_UPDATE_FORCED_DISABLED = false;
/**
* Auto updated not forced.
*
* @since 1.93.0
* @var false
*/
const AUTO_UPDATE_NOT_FORCED = null;
/**
* Checks whether plugin auto-updates are enabled for the site.
*
* @since 1.93.0
*
* @return bool `false` if auto-updates are disabled, `true` otherwise.
*/
public static function is_plugin_autoupdates_enabled() {
if ( self::AUTO_UPDATE_FORCED_DISABLED === self::sitekit_forced_autoupdates_status() ) {
return false;
}
if ( function_exists( 'wp_is_auto_update_enabled_for_type' ) ) {
return wp_is_auto_update_enabled_for_type( 'plugin' );
}
return false;
}
/**
* Check whether the site has auto updates enabled for Site Kit.
*
* @since 1.93.0
*
* @return bool `true` if auto updates are enabled, otherwise `false`.
*/
public static function is_sitekit_autoupdates_enabled() {
if ( self::AUTO_UPDATE_FORCED_ENABLED === self::sitekit_forced_autoupdates_status() ) {
return true;
}
if ( self::AUTO_UPDATE_FORCED_DISABLED === self::sitekit_forced_autoupdates_status() ) {
return false;
}
$enabled_auto_updates = (array) get_site_option( 'auto_update_plugins', array() );
if ( ! $enabled_auto_updates ) {
return false;
}
// Check if the Site Kit is in the list of auto-updated plugins.
return in_array( GOOGLESITEKIT_PLUGIN_BASENAME, $enabled_auto_updates, true );
}
/**
* Checks whether auto-updates are forced for Site Kit.
*
* @since 1.93.0
*
* @return bool|null
*/
public static function sitekit_forced_autoupdates_status() {
if ( ! function_exists( 'wp_is_auto_update_forced_for_item' ) ) {
return self::AUTO_UPDATE_NOT_FORCED;
}
if ( ! function_exists( 'get_plugin_data' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
$sitekit_plugin_data = get_plugin_data( GOOGLESITEKIT_PLUGIN_MAIN_FILE );
$sitekit_update_data = self::get_sitekit_update_data();
$item = (object) array_merge( $sitekit_plugin_data, $sitekit_update_data );
$is_auto_update_forced_for_sitekit = wp_is_auto_update_forced_for_item( 'plugin', null, $item );
if ( true === $is_auto_update_forced_for_sitekit ) {
return self::AUTO_UPDATE_FORCED_ENABLED;
}
if ( false === $is_auto_update_forced_for_sitekit ) {
return self::AUTO_UPDATE_FORCED_DISABLED;
}
return self::AUTO_UPDATE_NOT_FORCED;
}
/**
* Merges plugin update data in the site transient with some default plugin data.
*
* @since 1.113.0
*
* @return array Site Kit plugin update data.
*/
protected static function get_sitekit_update_data() {
$sitekit_update_data = array(
'id' => 'w.org/plugins/' . dirname( GOOGLESITEKIT_PLUGIN_BASENAME ),
'slug' => dirname( GOOGLESITEKIT_PLUGIN_BASENAME ),
'plugin' => GOOGLESITEKIT_PLUGIN_BASENAME,
'new_version' => '',
'url' => '',
'package' => '',
'icons' => array(),
'banners' => array(),
'banners_rtl' => array(),
'tested' => '',
'requires_php' => GOOGLESITEKIT_PHP_MINIMUM,
'compatibility' => new stdClass(),
);
$plugin_updates = get_site_transient( 'update_plugins' );
$transient_data = array();
if ( isset( $plugin_updates->noupdate[ GOOGLESITEKIT_PLUGIN_BASENAME ] ) ) {
$transient_data = $plugin_updates->noupdate[ GOOGLESITEKIT_PLUGIN_BASENAME ];
}
if ( isset( $plugin_updates->response[ GOOGLESITEKIT_PLUGIN_BASENAME ] ) ) {
$transient_data = $plugin_updates->response[ GOOGLESITEKIT_PLUGIN_BASENAME ];
}
return array_merge( $sitekit_update_data, (array) $transient_data );
}
}

View File

@@ -0,0 +1,150 @@
<?php
/**
* Class Google\Site_Kit\Core\Util\BC_Functions
*
* @package Google\Site_Kit\Core\Util
* @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\Util;
use BadMethodCallException;
use WP_REST_Request;
/**
* Class for providing backwards compatible core functions, without polyfilling.
*
* @since 1.7.0
* @access private
* @ignore
*/
class BC_Functions {
/**
* Proxies calls to global functions, while falling back to the internal method by the same name.
*
* @since 1.7.0
*
* @param string $function_name Function name to call.
* @param array $arguments Arguments passed to function.
* @return mixed
* @throws BadMethodCallException Thrown if no method exists by the same name as the function.
*/
public static function __callStatic( $function_name, $arguments ) {
if ( function_exists( $function_name ) ) {
return call_user_func_array( $function_name, $arguments );
}
if ( method_exists( __CLASS__, $function_name ) ) {
return self::{ $function_name }( ...$arguments );
}
throw new BadMethodCallException( "$function_name does not exist." );
}
/**
* Basic implementation of the wp_sanitize_script_attributes function introduced in the WordPress version 5.7.0.
*
* @since 1.41.0
*
* @param array $attributes Key-value pairs representing `<script>` tag attributes.
* @return string String made of sanitized `<script>` tag attributes.
*/
protected static function wp_sanitize_script_attributes( $attributes ) {
$attributes_string = '';
foreach ( $attributes as $attribute_name => $attribute_value ) {
if ( is_bool( $attribute_value ) ) {
if ( $attribute_value ) {
$attributes_string .= ' ' . esc_attr( $attribute_name );
}
} else {
$attributes_string .= sprintf( ' %1$s="%2$s"', esc_attr( $attribute_name ), esc_attr( $attribute_value ) );
}
}
return $attributes_string;
}
/**
* A fallback for the wp_get_script_tag function introduced in the WordPress version 5.7.0.
*
* @since 1.41.0
*
* @param array $attributes Key-value pairs representing `<script>` tag attributes.
* @return string String containing `<script>` opening and closing tags.
*/
protected static function wp_get_script_tag( $attributes ) {
return sprintf( "<script %s></script>\n", self::wp_sanitize_script_attributes( $attributes ) );
}
/**
* A fallback for the wp_print_script_tag function introduced in the WordPress version 5.7.0.
*
* @since 1.41.0
*
* @param array $attributes Key-value pairs representing `<script>` tag attributes.
*/
protected static function wp_print_script_tag( $attributes ) {
echo self::wp_get_script_tag( $attributes ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
/**
* A fallback for the wp_get_inline_script_tag function introduced in the WordPress version 5.7.0.
*
* @since 1.41.0
*
* @param string $javascript Inline JavaScript code.
* @param array $attributes Optional. Key-value pairs representing `<script>` tag attributes.
* @return string String containing inline JavaScript code wrapped around `<script>` tag.
*/
protected static function wp_get_inline_script_tag( $javascript, $attributes = array() ) {
$javascript = "\n" . trim( $javascript, "\n\r " ) . "\n";
return sprintf( "<script%s>%s</script>\n", self::wp_sanitize_script_attributes( $attributes ), $javascript );
}
/**
* A fallback for the wp_get_inline_script_tag function introduced in the WordPress version 5.7.0.
*
* @since 1.41.0
*
* @param string $javascript Inline JavaScript code.
* @param array $attributes Optional. Key-value pairs representing `<script>` tag attributes.
*/
protected static function wp_print_inline_script_tag( $javascript, $attributes = array() ) {
echo self::wp_get_inline_script_tag( $javascript, $attributes ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
/**
* A fallback for the wp_get_sidebar function introduced in the WordPress version 5.9.0.
*
* Retrieves the registered sidebar with the given ID.
*
* @since 1.86.0
*
* @global array $wp_registered_sidebars The registered sidebars.
*
* @param string $id The sidebar ID.
* @return array|null The discovered sidebar, or null if it is not registered.
*/
protected static function wp_get_sidebar( $id ) {
global $wp_registered_sidebars;
foreach ( (array) $wp_registered_sidebars as $sidebar ) {
if ( $sidebar['id'] === $id ) {
return $sidebar;
}
}
if ( 'wp_inactive_widgets' === $id ) {
return array(
'id' => 'wp_inactive_widgets',
'name' => __( 'Inactive widgets', 'default' ),
);
}
return null;
}
}

View File

@@ -0,0 +1,35 @@
<?php
/**
* Class Google\Site_Kit\Core\Util\Block_Support
*
* @package Google\Site_Kit\Core\Util
* @copyright 2025 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Core\Util;
/**
* Utility class for block support checks.
*
* @since 1.148.0
* @access private
* @ignore
*/
class Block_Support {
/**
* Checks whether blocks are supported in Site Kit based on WordPress version.
*
* We currently require version WP 5.8 or higher to support blocks, as this is the version
* where the `block.json` configuration format was introduced.
*
* @since 1.148.0
*
* @return bool True if blocks are supported, false otherwise.
*/
public static function has_block_support() {
return (bool) version_compare( '5.8', get_bloginfo( 'version' ), '<=' );
}
}

View File

@@ -0,0 +1,68 @@
<?php
/**
* Class Google\Site_Kit\Core\Util\Collection_Key_Cap_Filter
*
* @package Google\Site_Kit\Core\Util
* @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\Util;
/**
* Class for filtering a specific key of a collection based on a capability.
*
* @since 1.77.0
* @access private
* @ignore
*/
class Collection_Key_Cap_Filter {
/**
* Collection key.
*
* @since 1.77.0
* @var string
*/
private $key;
/**
* Capability.
*
* @since 1.77.0
* @var string
*/
private $cap;
/**
* Constructor.
*
* @since 1.77.0.
*
* @param string $key Target collection key to filter.
* @param string $capability Required capability to filter by.
*/
public function __construct( $key, $capability ) {
$this->key = $key;
$this->cap = $capability;
}
/**
* Filters the given value of a specific key in each item of the given collection
* based on the key and capability.
*
* @since 1.77.0
*
* @param array[] $collection Array of arrays.
* @return array[] Filtered collection.
*/
public function filter_key_by_cap( array $collection ) {
foreach ( $collection as $meta_arg => &$value ) {
if ( ! current_user_can( $this->cap, $meta_arg ) ) {
unset( $value[ $this->key ] );
}
}
return $collection;
}
}

View File

@@ -0,0 +1,50 @@
<?php
/**
* Class Google\Site_Kit\Core\Util\URL
*
* @package Google\Site_Kit\Core\Util
* @copyright 2023 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Core\Util;
/**
* Class for custom date parsing methods.
*
* @since 1.99.0
* @access private
* @ignore
*/
class Date {
/**
* Parses a date range string into a start date and an end date.
*
* @since 1.99.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'.
*/
public static 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 );
}
}

View File

@@ -0,0 +1,194 @@
<?php
/**
* Class Google\Site_Kit\Core\Util\DeveloperPluginInstaller
*
* @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\Util;
use Google\Site_Kit\Context;
use Google\Site_Kit\Core\Permissions\Permissions;
use Google\Site_Kit\Core\REST_API\REST_Route;
use WP_REST_Server;
use WP_REST_Request;
use WP_REST_Response;
/**
* Class responsible for providing the helper plugin via the automatic updater.
*
* @since 1.3.0
*/
class Developer_Plugin_Installer {
const SLUG = 'google-site-kit-dev-settings';
/**
* Plugin context.
*
* @since 1.3.0
* @var Context
*/
private $context;
/**
* Constructor.
*
* @since 1.3.0
*
* @param Context $context Plugin context.
*/
public function __construct( Context $context ) {
$this->context = $context;
}
/**
* Registers functionality through WordPress hooks.
*
* @since 1.3.0
*/
public function register() {
// Only filter plugins API response if the developer plugin is not already active.
if ( ! defined( 'GOOGLESITEKITDEVSETTINGS_VERSION' ) ) {
add_filter(
'plugins_api',
function ( $value, $action, $args ) {
return $this->plugin_info( $value, $action, $args );
},
10,
3
);
}
add_filter(
'googlesitekit_rest_routes',
function ( $routes ) {
return array_merge( $routes, $this->get_rest_routes() );
}
);
}
/**
* Gets related REST routes.
*
* @since 1.3.0
*
* @return array List of REST_Route objects.
*/
private function get_rest_routes() {
$can_setup = function () {
return current_user_can( Permissions::SETUP );
};
return array(
new REST_Route(
'core/site/data/developer-plugin',
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => function () {
$is_active = defined( 'GOOGLESITEKITDEVSETTINGS_VERSION' );
$installed = $is_active;
$slug = self::SLUG;
$plugin = "$slug/$slug.php";
if ( ! $is_active ) {
if ( ! function_exists( 'get_plugins' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
foreach ( array_keys( get_plugins() ) as $installed_plugin ) {
if ( $installed_plugin === $plugin ) {
$installed = true;
break;
}
}
}
// Alternate wp_nonce_url without esc_html breaking query parameters.
$nonce_url = function ( $action_url, $action ) {
return add_query_arg( '_wpnonce', wp_create_nonce( $action ), $action_url );
};
$activate_url = $nonce_url( self_admin_url( 'plugins.php?action=activate&plugin=' . $plugin ), 'activate-plugin_' . $plugin );
$install_url = $nonce_url( self_admin_url( 'update.php?action=install-plugin&plugin=' . $slug ), 'install-plugin_' . $slug );
return new WP_REST_Response(
array(
'active' => $is_active,
'installed' => $installed,
'activateURL' => current_user_can( 'activate_plugin', $plugin ) ? esc_url_raw( $activate_url ) : false,
'installURL' => current_user_can( 'install_plugins' ) ? esc_url_raw( $install_url ) : false,
'configureURL' => $is_active ? esc_url_raw( $this->context->admin_url( 'dev-settings' ) ) : false,
)
);
},
'permission_callback' => $can_setup,
),
)
),
);
}
/**
* Retrieves plugin information data from the Site Kit REST API.
*
* @since 1.3.0
*
* @param false|object|array $value The result object or array. Default false.
* @param string $action The type of information being requested from the Plugin Installation API.
* @param object $args Plugin API arguments.
* @return false|object|array Updated $value, or passed-through $value on failure.
*/
private function plugin_info( $value, $action, $args ) {
if ( 'plugin_information' !== $action || self::SLUG !== $args->slug ) {
return $value;
}
$data = $this->fetch_plugin_data();
if ( ! $data ) {
return $value;
}
$new_data = array(
'slug' => self::SLUG,
'name' => $data['name'],
'version' => $data['version'],
'author' => '<a href="https://opensource.google.com">Google</a>',
'download_link' => $data['download_url'],
'trunk' => $data['download_url'],
'tested' => $data['tested'],
'requires' => $data['requires'],
'requires_php' => $data['requires_php'],
'last_updated' => $data['last_updated'],
);
if ( ! empty( $data['icons'] ) ) {
$new_data['icons'] = $data['icons'];
}
if ( ! empty( $data['banners'] ) ) {
$new_data['banners'] = $data['banners'];
}
if ( ! empty( $data['banners_rtl'] ) ) {
$new_data['banners_rtl'] = $data['banners_rtl'];
}
return (object) $new_data;
}
/**
* Gets plugin data from the API.
*
* @since 1.3.0
* @since 1.99.0 Update plugin data to pull from GCS bucket.
*
* @return array|null Associative array of plugin data, or null on failure.
*/
private function fetch_plugin_data() {
// phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.wp_remote_get_wp_remote_get
$response = wp_remote_get( 'https://storage.googleapis.com/site-kit-dev-plugins/google-site-kit-dev-settings/updates.json' );
// Retrieve data from the body and decode json format.
return json_decode( wp_remote_retrieve_body( $response ), true );
}
}

View File

@@ -0,0 +1,166 @@
<?php
/**
* Class Google\Site_Kit\Core\Util\Entity
*
* @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\Util;
/**
* Class representing an entity.
*
* An entity in Site Kit terminology is based on a canonical URL, i.e. every
* canonical frontend URL has an associated entity.
*
* An entity may also have a type, if it can be determined.
* Possible types are e.g. 'post' for a WordPress post (of any post type!),
* 'term' for a WordPress term (of any taxonomy!), 'blog' for the blog archive,
* 'date' for a date-based archive etc.
*
* For specific entity types, the entity will also have a title, and it may
* even have an ID. For example:
* * For a type of 'post', the entity ID will be the post ID and the entity
* title will be the post title.
* * For a type of 'term', the entity ID will be the term ID and the entity
* title will be the term title.
* * For a type of 'date', there will be no entity ID, but the entity title
* will be the title of the date-based archive.
*
* @since 1.7.0
* @access private
* @ignore
*/
final class Entity {
/**
* The entity URL.
*
* @since 1.7.0
* @var string
*/
private $url;
/**
* The entity type.
*
* @since 1.7.0
* @var string
*/
private $type;
/**
* The entity title.
*
* @since 1.7.0
* @var string
*/
private $title;
/**
* The entity ID.
*
* @since 1.7.0
* @var int
*/
private $id;
/**
* Entity URL sub-variant.
*
* @since 1.42.0
* @var string
*/
private $mode;
/**
* Constructor.
*
* @since 1.7.0
*
* @param string $url The entity URL.
* @param array $args {
* Optional. Additional entity arguments.
*
* @type string $type The entity type.
* @type string $title The entity title.
* @type int $id The entity ID.
* @type string $mode Entity URL sub-variant.
* }
*/
public function __construct( $url, array $args = array() ) {
$args = array_merge(
array(
'type' => '',
'title' => '',
'id' => 0,
'mode' => '',
),
$args
);
$this->url = $url;
$this->type = (string) $args['type'];
$this->title = (string) $args['title'];
$this->id = (int) $args['id'];
$this->mode = (string) $args['mode'];
}
/**
* Gets the entity URL.
*
* @since 1.7.0
*
* @return string The entity URL.
*/
public function get_url() {
return $this->url;
}
/**
* Gets the entity type.
*
* @since 1.7.0
*
* @return string The entity type, or empty string if unknown.
*/
public function get_type() {
return $this->type;
}
/**
* Gets the entity title.
*
* @since 1.7.0
*
* @return string The entity title, or empty string if unknown.
*/
public function get_title() {
return $this->title;
}
/**
* Gets the entity ID.
*
* @since 1.7.0
*
* @return int The entity ID, or 0 if unknown.
*/
public function get_id() {
return $this->id;
}
/**
* Gets the entity URL sub-variant.
*
* @since 1.42.0
*
* @return string The entity title, or empty string if unknown.
*/
public function get_mode() {
return $this->mode;
}
}

View File

@@ -0,0 +1,677 @@
<?php
/**
* Class Google\Site_Kit\Core\Util\Entity_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\Util;
use Google\Site_Kit\Plugin;
use WP_Query;
use WP_Post;
use WP_Term;
use WP_User;
use WP_Post_Type;
use WP_Screen;
/**
* Class providing access to entities.
*
* This class entirely relies on WordPress core behavior and is technically decoupled from Site Kit. For example,
* entities returned by this factory rely on the regular WordPress home URL and ignore Site Kit-specific details, such
* as an alternative "reference site URL".
*
* Instead of relying on this class directly, use {@see Context::get_reference_entity()} or
* {@see Context::get_reference_entity_from_url()}.
*
* @since 1.15.0
* @access private
* @ignore
*/
final class Entity_Factory {
/**
* Gets the entity for the current WordPress context, if available.
*
* @since 1.15.0
*
* @return Entity|null The entity for the current context, or null if none could be determined.
*/
public static function from_context() {
global $wp_the_query;
// If currently in WP admin, run admin-specific checks.
if ( is_admin() ) {
$screen = get_current_screen();
if ( ! $screen instanceof WP_Screen || 'post' !== $screen->base ) {
return null;
}
$post = get_post();
if ( $post instanceof WP_Post && self::is_post_public( $post ) ) {
return self::create_entity_for_post( $post, 1 );
}
return null;
}
// Otherwise, run frontend-specific `WP_Query` logic.
if ( $wp_the_query instanceof WP_Query ) {
$entity = self::from_wp_query( $wp_the_query );
$request_uri = Plugin::instance()->context()->input()->filter( INPUT_SERVER, 'REQUEST_URI' );
return self::maybe_convert_to_amp_entity( $request_uri, $entity );
}
return null;
}
/**
* Gets the entity for the given URL, if available.
*
* Calling this method is expensive, so it should only be used in certain admin contexts where this is acceptable.
*
* @since 1.15.0
*
* @param string $url URL to determine the entity from.
* @return Entity|null The entity for the URL, or null if none could be determined.
*/
public static function from_url( $url ) {
$query = WP_Query_Factory::from_url( $url );
if ( ! $query ) {
return null;
}
$query->get_posts();
$entity = self::from_wp_query( $query );
return self::maybe_convert_to_amp_entity( $url, $entity );
}
/**
* Gets the entity for the given `WP_Query` object, if available.
*
* @since 1.15.0
*
* @param WP_Query $query WordPress query object. Must already have run the actual database query.
* @return Entity|null The entity for the query, or null if none could be determined.
*/
public static function from_wp_query( WP_Query $query ) {
// A singular post (possibly the static front page).
if ( $query->is_singular() ) {
$post = $query->get_queried_object();
if ( $post instanceof WP_Post && self::is_post_public( $post ) ) {
return self::create_entity_for_post( $post, self::get_query_pagenum( $query, 'page' ) );
}
return null;
}
$page = self::get_query_pagenum( $query );
// The blog.
if ( $query->is_home() ) {
// The blog is either the front page...
if ( $query->is_front_page() ) {
return self::create_entity_for_front_blog( $page );
}
// ...or it is a separate post assigned as 'page_for_posts'.
return self::create_entity_for_posts_blog( $page );
}
// A taxonomy term archive.
if ( $query->is_category() || $query->is_tag() || $query->is_tax() ) {
$term = $query->get_queried_object();
if ( $term instanceof WP_Term ) {
return self::create_entity_for_term( $term, $page );
}
}
// An author archive.
if ( $query->is_author() ) {
$user = $query->get_queried_object();
if ( $user instanceof WP_User ) {
return self::create_entity_for_author( $user, $page );
}
}
// A post type archive.
if ( $query->is_post_type_archive() ) {
$post_type = $query->get( 'post_type' );
if ( is_array( $post_type ) ) {
$post_type = reset( $post_type );
}
$post_type_object = get_post_type_object( $post_type );
if ( $post_type_object instanceof WP_Post_Type ) {
return self::create_entity_for_post_type( $post_type_object, $page );
}
}
// A date-based archive.
if ( $query->is_date() ) {
$queried_post = self::get_first_query_post( $query );
if ( ! $queried_post ) {
return null;
}
if ( $query->is_year() ) {
return self::create_entity_for_date( $queried_post, 'year', $page );
}
if ( $query->is_month() ) {
return self::create_entity_for_date( $queried_post, 'month', $page );
}
if ( $query->is_day() ) {
return self::create_entity_for_date( $queried_post, 'day', $page );
}
// Time archives are not covered for now. While they can theoretically be used in WordPress, they
// aren't fully supported, and WordPress does not link to them anywhere.
return null;
}
return null;
}
/**
* Creates the entity for a given post object.
*
* @since 1.15.0
* @since 1.68.0 Method access modifier changed to public.
*
* @param WP_Post $post A WordPress post object.
* @param int $page Page number.
* @return Entity The entity for the post.
*/
public static function create_entity_for_post( WP_Post $post, $page ) {
$url = self::paginate_post_url( get_permalink( $post ), $post, $page );
return new Entity(
urldecode( $url ),
array(
'type' => 'post',
'title' => $post->post_title,
'id' => $post->ID,
)
);
}
/**
* Creates the entity for the posts page blog archive.
*
* This method should only be used when the blog is handled via a separate page, i.e. when 'show_on_front' is set
* to 'page' and the 'page_for_posts' option is set. In this case the blog is technically a post itself, therefore
* its entity also includes an ID.
*
* @since 1.15.0
*
* @param int $page Page number.
* @return Entity|null The entity for the posts blog archive, or null if not set.
*/
private static function create_entity_for_posts_blog( $page ) {
$post_id = (int) get_option( 'page_for_posts' );
if ( ! $post_id ) {
return null;
}
$post = get_post( $post_id );
if ( ! $post ) {
return null;
}
return new Entity(
self::paginate_entity_url( get_permalink( $post ), $page ),
array(
'type' => 'blog',
'title' => $post->post_title,
'id' => $post->ID,
)
);
}
/**
* Creates the entity for the front page blog archive.
*
* This method should only be used when the front page is set to display the
* blog archive, i.e. is not technically a post itself.
*
* @since 1.15.0
*
* @param int $page Page number.
* @return Entity The entity for the front blog archive.
*/
private static function create_entity_for_front_blog( $page ) {
// The translation string intentionally omits the 'google-site-kit' text domain since it should use
// WordPress core translations.
return new Entity(
self::paginate_entity_url( user_trailingslashit( home_url() ), $page ),
array(
'type' => 'blog',
'title' => __( 'Home', 'default' ),
)
);
}
/**
* Creates the entity for a given term object, i.e. for a taxonomy term archive.
*
* @since 1.15.0
*
* @param WP_Term $term A WordPress term object.
* @param int $page Page number.
* @return Entity The entity for the term.
*/
private static function create_entity_for_term( WP_Term $term, $page ) {
// See WordPress's `get_the_archive_title()` function for this behavior. The strings here intentionally omit
// the 'google-site-kit' text domain since they should use WordPress core translations.
switch ( $term->taxonomy ) {
case 'category':
$title = $term->name;
$prefix = _x( 'Category:', 'category archive title prefix', 'default' );
break;
case 'post_tag':
$title = $term->name;
$prefix = _x( 'Tag:', 'tag archive title prefix', 'default' );
break;
case 'post_format':
$prefix = '';
switch ( $term->slug ) {
case 'post-format-aside':
$title = _x( 'Asides', 'post format archive title', 'default' );
break;
case 'post-format-gallery':
$title = _x( 'Galleries', 'post format archive title', 'default' );
break;
case 'post-format-image':
$title = _x( 'Images', 'post format archive title', 'default' );
break;
case 'post-format-video':
$title = _x( 'Videos', 'post format archive title', 'default' );
break;
case 'post-format-quote':
$title = _x( 'Quotes', 'post format archive title', 'default' );
break;
case 'post-format-link':
$title = _x( 'Links', 'post format archive title', 'default' );
break;
case 'post-format-status':
$title = _x( 'Statuses', 'post format archive title', 'default' );
break;
case 'post-format-audio':
$title = _x( 'Audio', 'post format archive title', 'default' );
break;
case 'post-format-chat':
$title = _x( 'Chats', 'post format archive title', 'default' );
break;
}
break;
default:
$tax = get_taxonomy( $term->taxonomy );
$title = $term->name;
$prefix = sprintf(
/* translators: %s: Taxonomy singular name. */
_x( '%s:', 'taxonomy term archive title prefix', 'default' ),
$tax->labels->singular_name
);
}
return new Entity(
self::paginate_entity_url( get_term_link( $term ), $page ),
array(
'type' => 'term',
'title' => self::prefix_title( $title, $prefix ),
'id' => $term->term_id,
)
);
}
/**
* Creates the entity for a given user object, i.e. for an author archive.
*
* @since 1.15.0
*
* @param WP_User $user A WordPress user object.
* @param int $page Page number.
* @return Entity The entity for the user.
*/
private static function create_entity_for_author( WP_User $user, $page ) {
// See WordPress's `get_the_archive_title()` function for this behavior. The string here intentionally omits
// the 'google-site-kit' text domain since it should use WordPress core translations.
$title = $user->display_name;
$prefix = _x( 'Author:', 'author archive title prefix', 'default' );
return new Entity(
self::paginate_entity_url( get_author_posts_url( $user->ID, $user->user_nicename ), $page ),
array(
'type' => 'user',
'title' => self::prefix_title( $title, $prefix ),
'id' => $user->ID,
)
);
}
/**
* Creates the entity for a given post type object.
*
* @since 1.15.0
*
* @param WP_Post_Type $post_type A WordPress post type object.
* @param int $page Page number.
* @return Entity The entity for the post type.
*/
private static function create_entity_for_post_type( WP_Post_Type $post_type, $page ) {
// See WordPress's `get_the_archive_title()` function for this behavior. The string here intentionally omits
// the 'google-site-kit' text domain since it should use WordPress core translations.
$title = $post_type->labels->name;
$prefix = _x( 'Archives:', 'post type archive title prefix', 'default' );
return new Entity(
self::paginate_entity_url( get_post_type_archive_link( $post_type->name ), $page ),
array(
'type' => 'post_type',
'title' => self::prefix_title( $title, $prefix ),
)
);
}
/**
* Creates the entity for a date-based archive.
*
* The post specified has to any post from the query, in order to extract the relevant date information.
*
* @since 1.15.0
*
* @param WP_Post $queried_post A WordPress post object from the query.
* @param string $type Type of the date-based archive. Either 'year', 'month', or 'day'.
* @param int $page Page number.
* @return Entity|null The entity for the date archive, or null if unable to parse date.
*/
private static function create_entity_for_date( WP_Post $queried_post, $type, $page ) {
// See WordPress's `get_the_archive_title()` function for this behavior. The strings here intentionally omit
// the 'google-site-kit' text domain since they should use WordPress core translations.
switch ( $type ) {
case 'year':
$prefix = _x( 'Year:', 'date archive title prefix', 'default' );
$format = _x( 'Y', 'yearly archives date format', 'default' );
$url_func = 'get_year_link';
$url_func_format = 'Y';
break;
case 'month':
$prefix = _x( 'Month:', 'date archive title prefix', 'default' );
$format = _x( 'F Y', 'monthly archives date format', 'default' );
$url_func = 'get_month_link';
$url_func_format = 'Y/m';
break;
default:
$type = 'day';
$prefix = _x( 'Day:', 'date archive title prefix', 'default' );
$format = _x( 'F j, Y', 'daily archives date format', 'default' );
$url_func = 'get_day_link';
$url_func_format = 'Y/m/j';
}
$title = get_post_time( $format, false, $queried_post, true );
$url_func_args = get_post_time( $url_func_format, false, $queried_post );
if ( ! $url_func_args ) {
return null; // Unable to parse date, likely there is none set.
}
$url_func_args = array_map( 'absint', explode( '/', $url_func_args ) );
return new Entity(
self::paginate_entity_url( call_user_func_array( $url_func, $url_func_args ), $page ),
array(
'type' => $type,
'title' => self::prefix_title( $title, $prefix ),
)
);
}
/**
* Checks whether a given post is public, i.e. has a public URL.
*
* @since 1.15.0
*
* @param WP_Post $post A WordPress post object.
* @return bool True if the post is public, false otherwise.
*/
private static function is_post_public( WP_Post $post ) {
// If post status isn't 'publish', the post is not public.
if ( 'publish' !== get_post_status( $post ) ) {
return false;
}
// If the post type overall is not publicly viewable, the post is not public.
if ( ! is_post_type_viewable( $post->post_type ) ) {
return false;
}
// Otherwise, the post is public.
return true;
}
/**
* Gets the first post from a WordPress query.
*
* @since 1.15.0
*
* @param WP_Query $query WordPress query object. Must already have run the actual database query.
* @return WP_Post|null WordPress post object, or null if none found.
*/
private static function get_first_query_post( WP_Query $query ) {
if ( ! $query->posts ) {
return null;
}
$post = reset( $query->posts );
if ( $post instanceof WP_Post ) {
return $post;
}
if ( is_numeric( $post ) ) {
return get_post( $post );
}
return null;
}
/**
* Combines an entity title and prefix.
*
* This is based on the WordPress core function `get_the_archive_title()`.
*
* @since 1.15.0
*
* @param string $title The title.
* @param string $prefix The prefix to add, should end in a colon.
* @return string Resulting entity title.
*/
private static function prefix_title( $title, $prefix ) {
if ( empty( $prefix ) ) {
return $title;
}
// See WordPress's `get_the_archive_title()` function for this behavior. The string here intentionally omits
// the 'google-site-kit' text domain since it should use WordPress core translations.
return sprintf(
/* translators: 1: Title prefix. 2: Title. */
_x( '%1$s %2$s', 'archive title', 'default' ),
$prefix,
$title
);
}
/**
* Converts given entity to AMP entity if the given URL is an AMP URL.
*
* @since 1.42.0
*
* @param string $url URL to determine the entity from.
* @param Entity $entity The initial entity.
* @return Entity The initial or new entity for the given URL.
*/
private static function maybe_convert_to_amp_entity( $url, $entity ) {
if ( is_null( $entity ) || ! defined( 'AMP__VERSION' ) ) {
return $entity;
}
$url_parts = URL::parse( $url );
$current_url = $entity->get_url();
if ( ! empty( $url_parts['query'] ) ) {
$url_query_params = array();
wp_parse_str( $url_parts['query'], $url_query_params );
// check if the $url has amp query param.
if ( array_key_exists( 'amp', $url_query_params ) ) {
$new_url = add_query_arg( 'amp', '1', $current_url );
return self::convert_to_amp_entity( $new_url, $entity );
}
}
if ( ! empty( $url_parts['path'] ) ) {
// We need to correctly add trailing slash if the original url had trailing slash.
// That's the reason why we need to check for both version.
if ( '/amp' === substr( $url_parts['path'], -4 ) ) { // -strlen('/amp') is -4
$new_url = untrailingslashit( $current_url ) . '/amp';
return self::convert_to_amp_entity( $new_url, $entity );
}
if ( '/amp/' === substr( $url_parts['path'], -5 ) ) { // -strlen('/amp/') is -5
$new_url = untrailingslashit( $current_url ) . '/amp/';
return self::convert_to_amp_entity( $new_url, $entity );
}
}
return $entity;
}
/**
* Converts given entity to AMP entity by changing the entity URL and adding correct mode.
*
* @since 1.42.0
*
* @param string $new_url URL of the new entity.
* @param Entity $entity The initial entity.
* @return Entity The new entity.
*/
private static function convert_to_amp_entity( $new_url, $entity ) {
$new_entity = new Entity(
$new_url,
array(
'id' => $entity->get_id(),
'type' => $entity->get_type(),
'title' => $entity->get_title(),
'mode' => 'amp_secondary',
)
);
return $new_entity;
}
/**
* Gets the page number for a query, via the specified query var. Defaults to 1.
*
* @since 1.68.0
*
* @param WP_Query $query A WordPress query object.
* @param string $query_var Optional. Query var to look for, expects 'paged' or 'page'. Default 'paged'.
* @return int The page number.
*/
private static function get_query_pagenum( $query, $query_var = 'paged' ) {
return $query->get( $query_var ) ? (int) $query->get( $query_var ) : 1;
}
/**
* Paginates an entity URL.
*
* Logic extracted from `paginate_links` in WordPress core.
* https://github.com/WordPress/WordPress/blob/7f5d7f1b56087c3eb718da4bd81deb06e077bbbb/wp-includes/general-template.php#L4203
*
* @since 1.68.0
*
* @param string $url The URL to paginate.
* @param int $pagenum The page number to add to the URL.
* @return string The paginated URL.
*/
private static function paginate_entity_url( $url, $pagenum ) {
global $wp_rewrite;
if ( 1 === $pagenum ) {
return $url;
}
// Setting up default values based on the given URL.
$url_parts = explode( '?', $url );
// Append the format placeholder to the base URL.
$base = trailingslashit( $url_parts[0] ) . '%_%';
// URL base depends on permalink settings.
$format = $wp_rewrite->using_index_permalinks() && ! strpos( $base, 'index.php' ) ? 'index.php/' : '';
$format .= $wp_rewrite->using_permalinks() ? user_trailingslashit( $wp_rewrite->pagination_base . '/%#%', 'paged' ) : '?paged=%#%';
// Array of query args to add.
$add_args = array();
// Merge additional query vars found in the original URL into 'add_args' array.
if ( isset( $url_parts[1] ) ) {
// Find the format argument.
$format_parts = explode( '?', str_replace( '%_%', $format, $base ) );
$format_query = isset( $format_parts[1] ) ? $format_parts[1] : '';
wp_parse_str( $format_query, $format_args );
// Find the query args of the requested URL.
$url_query_args = array();
wp_parse_str( $url_parts[1], $url_query_args );
// Remove the format argument from the array of query arguments, to avoid overwriting custom format.
foreach ( $format_args as $format_arg => $format_arg_value ) {
unset( $url_query_args[ $format_arg ] );
}
$add_args = array_merge( $add_args, urlencode_deep( $url_query_args ) );
}
$link = str_replace( '%_%', $format, $base );
$link = str_replace( '%#%', $pagenum, $link );
if ( $add_args ) {
$link = add_query_arg( $add_args, $link );
}
return $link;
}
/**
* Paginates a post URL.
*
* Logic extracted from `_wp_link_page` in WordPress core.
* https://github.com/WordPress/WordPress/blob/7f5d7f1b56087c3eb718da4bd81deb06e077bbbb/wp-includes/post-template.php#L1031
*
* @since 1.68.0
*
* @param string $url The URL to paginate.
* @param WP_Post $post The WordPress post object.
* @param int $pagenum The page number to add to the URL.
* @return string The paginated URL.
*/
private static function paginate_post_url( $url, $post, $pagenum ) {
global $wp_rewrite;
if ( 1 === $pagenum ) {
return $url;
}
if ( ! get_option( 'permalink_structure' ) || in_array( $post->post_status, array( 'draft', 'pending' ), true ) ) {
$url = add_query_arg( 'page', $pagenum, $url );
} elseif ( 'page' === get_option( 'show_on_front' ) && (int) get_option( 'page_on_front' ) === (int) $post->ID ) {
$url = trailingslashit( $url ) . user_trailingslashit( "$wp_rewrite->pagination_base/" . $pagenum, 'single_paged' );
} else {
$url = trailingslashit( $url ) . user_trailingslashit( $pagenum, 'single_paged' );
}
return $url;
}
}

View File

@@ -0,0 +1,45 @@
<?php
/**
* Exit_Handler
*
* @package Google\Site_Kit\Core\Util
* @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\Util;
/**
* Exit_Handler class.
*
* @since 1.1.0
* @access private
* @ignore
*/
class Exit_Handler {
/**
* Invokes the handler.
*
* @since 1.1.0
*/
public function invoke() {
$callback = static function () {
exit;
};
if ( defined( 'GOOGLESITEKIT_TESTS' ) ) {
/**
* Allows the callback to be filtered during tests.
*
* @since 1.1.0
* @param \Closure $callback Exit handler callback.
*/
$callback = apply_filters( 'googlesitekit_exit_handler', $callback );
}
if ( $callback instanceof \Closure ) {
$callback();
}
}
}

View File

@@ -0,0 +1,101 @@
<?php
/**
* Class Google\Site_Kit\Core\Util\Feature_Flags
*
* @package Google\Site_Kit\Core\Util
* @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\Util;
use ArrayAccess;
/**
* Class for interacting with feature flag configuration.
*
* @since 1.22.0
* @access private
* @ignore
*/
class Feature_Flags {
/**
* Feature flag definitions.
*
* @since 1.22.0
* @var array|ArrayAccess
*/
private static $features = array();
/**
* Checks if the given feature is enabled.
*
* @since 1.22.0
*
* @param string $feature Feature key path to check.
* @return bool
*/
public static function enabled( $feature ) {
if ( ! $feature || ! is_string( $feature ) || empty( static::$features ) ) {
return false;
}
/**
* Filters a feature flag's status (on or off).
*
* Mainly this is used by E2E tests to allow certain features to be disabled or
* enabled for testing, but is also useful to switch features on/off on-the-fly.
*
* @since 1.25.0
*
* @param bool $feature_enabled The current status of this feature flag (`true` or `false`).
* @param string $feature The feature name.
*/
return apply_filters( 'googlesitekit_is_feature_enabled', false, $feature );
}
/**
* Gets all enabled feature flags.
*
* @since 1.25.0
*
* @return string[] An array of all enabled features.
*/
public static function get_enabled_features() {
$enabled_features = array();
foreach ( static::$features as $feature_name ) {
if ( static::enabled( $feature_name ) ) {
$enabled_features[] = $feature_name;
}
}
return $enabled_features;
}
/**
* Sets the feature configuration.
*
* @since 1.22.0
*
* @param array|ArrayAccess $features Feature configuration.
*/
public static function set_features( $features ) {
if ( is_array( $features ) || $features instanceof ArrayAccess ) {
static::$features = $features;
}
}
/**
* Gets all available feature flags.
*
* @since 1.26.0
*
* @return array An array of all available features.
*/
public static function get_available_features() {
return static::$features;
}
}

View File

@@ -0,0 +1,53 @@
<?php
/**
* Class Google\Site_Kit\Core\Util\Google_Icon
*
* @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\Util;
/**
* Class for the Google SVG Icon
*
* @since 1.28.0
* @access private
* @ignore
*/
final class Google_Icon {
/**
* We use fill="white" as a placeholder attribute that we replace in with_fill()
* to match the colorscheme that the user has set.
*
* See the comment in includes/Core/Admin/Screen.php::add() for more information.
*/
const XML = '<svg width="20" height="20" viewbox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill="white" d="M17.6 8.5h-7.5v3h4.4c-.4 2.1-2.3 3.5-4.4 3.4-2.6-.1-4.6-2.1-4.7-4.7-.1-2.7 2-5 4.7-5.1 1.1 0 2.2.4 3.1 1.2l2.3-2.2C14.1 2.7 12.1 2 10.2 2c-4.4 0-8 3.6-8 8s3.6 8 8 8c4.6 0 7.7-3.2 7.7-7.8-.1-.6-.1-1.1-.3-1.7z" fillrule="evenodd" cliprule="evenodd"></path></svg>';
/**
* Returns a base64 encoded version of the SVG.
*
* @since 1.28.0
*
* @param string $source SVG icon source.
* @return string Base64 representation of SVG
*/
public static function to_base64( $source = self::XML ) {
return base64_encode( $source );
}
/**
* Returns SVG XML with fill color replaced.
*
* @since 1.28.0
*
* @param string $color Any valid color for css, either word or hex code.
* @return string SVG XML with the fill color replaced
*/
public static function with_fill( $color ) {
return str_replace( 'white', esc_attr( $color ), self::XML );
}
}

View File

@@ -0,0 +1,93 @@
<?php
/**
* Trait Google\Site_Kit\Core\Util\Google_URL_Matcher_Trait
*
* @package Google\Site_Kit\Core\Util
* @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\Util;
/**
* Trait for matching URLs and domains for Google Site Verification and Search Console.
*
* @since 1.6.0
* @access private
* @ignore
*/
trait Google_URL_Matcher_Trait {
/**
* Compares two URLs for whether they qualify for a Site Verification or Search Console URL match.
*
* In order for the URLs to be considered a match, they have to be fully equal, except for a potential
* trailing slash in one of them, which will be ignored.
*
* @since 1.6.0
*
* @param string $url The URL.
* @param string $compare The URL to compare.
* @return bool True if the URLs are considered a match, false otherwise.
*/
protected function is_url_match( $url, $compare ) {
$url = untrailingslashit( $url );
$compare = untrailingslashit( $compare );
$url_normalizer = new Google_URL_Normalizer();
$url = $url_normalizer->normalize_url( $url );
$compare = $url_normalizer->normalize_url( $compare );
return $url === $compare;
}
/**
* Compares two domains for whether they qualify for a Site Verification or Search Console domain match.
*
* The value to compare may be either a domain or a full URL. If the latter, its scheme and a potential trailing
* slash will be stripped out before the comparison.
*
* In order for the comparison to be considered a match then, the domains have to fully match, except for a
* potential "www." prefix, which will be ignored. If the value to compare is a full URL and includes a path other
* than just a trailing slash, it will not be a match.
*
* @since 1.6.0
*
* @param string $domain A domain.
* @param string $compare The domain or URL to compare.
* @return bool True if the URLs/domains are considered a match, false otherwise.
*/
protected function is_domain_match( $domain, $compare ) {
$domain = $this->strip_domain_www( $domain );
$compare = $this->strip_domain_www( $this->strip_url_scheme( untrailingslashit( $compare ) ) );
$url_normalizer = new Google_URL_Normalizer();
$domain = $url_normalizer->normalize_url( $domain );
$compare = $url_normalizer->normalize_url( $compare );
return $domain === $compare;
}
/**
* Strips the scheme from a URL.
*
* @since 1.6.0
*
* @param string $url URL with or without scheme.
* @return string The passed $url without its scheme.
*/
protected function strip_url_scheme( $url ) {
return preg_replace( '#^(\w+:)?//#', '', $url );
}
/**
* Strips the "www." prefix from a domain.
*
* @since 1.6.0
*
* @param string $domain Domain with or without "www." prefix.
* @return string The passed $domain without "www." prefix.
*/
protected function strip_domain_www( $domain ) {
return preg_replace( '/^www\./', '', $domain );
}
}

View File

@@ -0,0 +1,57 @@
<?php
/**
* Class Google\Site_Kit\Core\Util\Google_URL_Normalizer
*
* @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\Util;
/**
* Class handling URL normalization for comparisons and API requests.
*
* @since 1.18.0
* @access private
* @ignore
*/
final class Google_URL_Normalizer {
/**
* Normalizes a URL by converting to all lowercase, converting Unicode characters
* to punycode, and removing bidirectional control characters.
*
* @since 1.18.0
*
* @param string $url The URL or domain to normalize.
* @return string The normalized URL or domain.
*/
public function normalize_url( $url ) {
// Remove bidirectional control characters.
$url = preg_replace( array( '/\xe2\x80\xac/', '/\xe2\x80\xab/' ), '', $url );
$url = $this->decode_unicode_url_or_domain( $url );
$url = strtolower( $url );
return $url;
}
/**
* Returns the Punycode version of a Unicode URL or domain name.
*
* @since 1.18.0
*
* @param string $url The URL or domain name to decode.
*/
protected function decode_unicode_url_or_domain( $url ) {
$encoder_class = class_exists( '\WpOrg\Requests\IdnaEncoder' ) ? '\WpOrg\Requests\IdnaEncoder' : '\Requests_IDNAEncoder';
$parts = URL::parse( $url );
if ( ! $parts || ! isset( $parts['host'] ) || '' === $parts['host'] ) {
return $encoder_class::encode( $url );
}
$decoded_host = $encoder_class::encode( $parts['host'] );
return str_replace( $parts['host'], $decoded_host, $url );
}
}

View File

@@ -0,0 +1,166 @@
<?php
/**
* Class Google\Site_Kit\Core\Util\Health_Checks
*
* @package Google\Site_Kit\Core\Util
* @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\Util;
use Exception;
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_Dependencies\Google\Service\SearchConsole as Google_Service_SearchConsole;
use Google\Site_Kit_Dependencies\Google_Service_Exception;
use WP_REST_Server;
/**
* Class for performing health checks.
*
* @since 1.14.0
* @access private
* @ignore
*/
class Health_Checks {
/**
* Authentication instance.
*
* @var Authentication
*/
protected $authentication;
/**
* Google_Proxy instance.
*
* @var Google_Proxy
*/
protected $google_proxy;
/**
* Constructor.
*
* @param Authentication $authentication Authentication instance.
*/
public function __construct( Authentication $authentication ) {
$this->authentication = $authentication;
$this->google_proxy = $authentication->get_google_proxy();
}
/**
* Registers functionality through WordPress hooks.
*
* @since 1.14.0
*/
public function register() {
add_filter(
'googlesitekit_rest_routes',
function ( $rest_routes ) {
$health_check_routes = $this->get_rest_routes();
return array_merge( $rest_routes, $health_check_routes );
}
);
}
/**
* Gets all health check REST routes.
*
* @since 1.14.0
*
* @return REST_Route[] List of REST_Route objects.
*/
private function get_rest_routes() {
return array(
new REST_Route(
'core/site/data/health-checks',
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => function () {
$checks = array(
'googleAPI' => $this->check_google_api(),
'skService' => $this->check_service_connectivity(),
);
return compact( 'checks' );
},
'permission_callback' => function () {
return current_user_can( Permissions::VIEW_SHARED_DASHBOARD ) || current_user_can( Permissions::SETUP );
},
),
)
),
);
}
/**
* Checks connection to Google APIs.
*
* @since 1.14.0
*
* @return array Results data.
*/
private function check_google_api() {
$client = $this->authentication->get_oauth_client()->get_client();
$restore_defer = $client->withDefer( false );
$error_msg = '';
// Make a request to the Search API.
// This request is bound to fail but this is okay as long as the error response comes
// from a Google API endpoint (Google_Service_exception). The test is only intended
// to check that the server is capable of connecting to the Google API (at all)
// regardless of valid authentication, which will likely be missing here.
try {
( new Google_Service_SearchConsole( $client ) )->sites->listSites();
$pass = true;
} catch ( Google_Service_Exception $e ) {
if ( ! empty( $e->getErrors() ) ) {
$pass = true;
} else {
$pass = false;
$error_msg = $e->getMessage();
}
} catch ( Exception $e ) {
$pass = false;
$error_msg = $e->getMessage();
}
$restore_defer();
return array(
'pass' => $pass,
'errorMsg' => $error_msg,
);
}
/**
* Checks connection to Site Kit service.
*
* @since 1.85.0
*
* @return array Results data.
*/
private function check_service_connectivity() {
$service_url = $this->google_proxy->url();
$response = wp_remote_head( $service_url );
if ( is_wp_error( $response ) ) {
return array(
'pass' => false,
'errorMsg' => $response->get_error_message(),
);
}
$status_code = wp_remote_retrieve_response_code( $response );
$pass = is_int( $status_code ) && $status_code < 400;
return array(
'pass' => $pass,
'errorMsg' => $pass ? '' : 'connection_fail',
);
}
}

View File

@@ -0,0 +1,81 @@
<?php
/**
* Class Google\Site_Kit\Core\Util\Input
*
* @package Google\Site_Kit\Core\Util
* @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\Util;
/**
* Class for input superglobal access.
*
* @since 1.1.2
* @access private
* @ignore
*/
class Input {
/**
* Map of input type to superglobal array.
*
* For use as fallback only.
*
* @since 1.1.4
* @var array
*/
protected $fallback_map;
/**
* Constructor.
*
* @since 1.1.4
*/
public function __construct() {
// Fallback map for environments where filter_input may not work with ENV or SERVER types.
$this->fallback_map = array(
INPUT_ENV => $_ENV,
INPUT_SERVER => $_SERVER, // phpcs:ignore WordPress.VIP.SuperGlobalInputUsage
);
}
/**
* Gets a specific external variable by name and optionally filters it.
*
* @since 1.1.2
* @since 1.92.0 Changed default value of $options parameter to 0.
*
* @link https://php.net/manual/en/function.filter-input.php
*
* @param int $type One of INPUT_GET, INPUT_POST, INPUT_COOKIE, INPUT_SERVER, or INPUT_ENV.
* @param string $variable_name Name of a variable to get.
* @param int $filter [optional] The ID of the filter to apply. The manual page lists the available filters.
* @param mixed $options [optional] Associative array of options or bitwise disjunction of flags.
* If filter accepts options, flags can be provided in "flags" field of array.
* @return mixed Value of the requested variable on success,
* FALSE if the filter fails,
* NULL if the $variable_name variable is not set.
*
* If the flag FILTER_NULL_ON_FAILURE is used, it returns FALSE if the variable is not set
* and NULL if the filter fails.
*/
public function filter( $type, $variable_name, $filter = FILTER_DEFAULT, $options = 0 ) {
$value = filter_input( $type, $variable_name, $filter, $options );
// Fallback for environments where filter_input may not work with specific types.
if (
// Only use this fallback for affected input types.
isset( $this->fallback_map[ $type ] )
// Only use the fallback if the value is not-set (could be either depending on FILTER_NULL_ON_FAILURE).
&& in_array( $value, array( null, false ), true )
// Only use the fallback if the key exists in the input map.
&& array_key_exists( $variable_name, $this->fallback_map[ $type ] )
) {
return filter_var( $this->fallback_map[ $type ][ $variable_name ], $filter, $options );
}
return $value;
}
}

View File

@@ -0,0 +1,50 @@
<?php
/**
* Class Google\Site_Kit\Core\Util\Method_Proxy_Trait
*
* @package Google\Site_Kit\Core\Util
* @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\Util;
trait Method_Proxy_Trait {
/**
* Gets a proxy function for a class method.
*
* @since 1.17.0
*
* @param string $method Method name.
* @return callable A proxy function.
*/
private function get_method_proxy( $method ) {
return function ( ...$args ) use ( $method ) {
return $this->{ $method }( ...$args );
};
}
/**
* Gets a proxy function for a class method which can be executed only once.
*
* @since 1.24.0
*
* @param string $method Method name.
* @return callable A proxy function.
*/
private function get_method_proxy_once( $method ) {
return function ( ...$args ) use ( $method ) {
static $called;
static $return_value;
if ( ! $called ) {
$called = true;
$return_value = $this->{ $method }( ...$args );
}
return $return_value;
};
}
}

View File

@@ -0,0 +1,41 @@
<?php
/**
* Trait Google\Site_Kit\Core\Util\Migrate_Legacy_Keys
*
* @package Google\Site_Kit\Core\Util
* @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\Util;
/**
* Trait for a class that migrates array keys from old to new.
*
* @since 1.2.0
* @access private
* @ignore
*/
trait Migrate_Legacy_Keys {
/**
* Migrates legacy array keys to the current key.
*
* @since 1.2.0
*
* @param array $legacy_array Input associative array to migrate keys for.
* @param array $key_mapping Map of legacy key to current key.
* @return array Updated array.
*/
protected function migrate_legacy_keys( array $legacy_array, array $key_mapping ) {
foreach ( $key_mapping as $legacy_key => $current_key ) {
if ( ! isset( $legacy_array[ $current_key ] ) && isset( $legacy_array[ $legacy_key ] ) ) {
$legacy_array[ $current_key ] = $legacy_array[ $legacy_key ];
}
unset( $legacy_array[ $legacy_key ] );
}
return $legacy_array;
}
}

View File

@@ -0,0 +1,212 @@
<?php
/**
* Migration for 1.123.0
*
* @package Google\Site_Kit\Core\Util
* @copyright 2024 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Core\Util;
use Google\Site_Kit\Context;
use Google\Site_Kit\Core\Modules\Module_Sharing_Settings;
use Google\Site_Kit\Core\Modules\Modules;
use Google\Site_Kit\Core\Storage\Options;
use Google\Site_Kit\Modules\Analytics_4;
use Google\Site_Kit\Modules\Analytics_4\Settings as Analytics_Settings;
/**
* Class Migration_1_123_0
*
* @since 1.123.0
* @access private
* @ignore
*/
class Migration_1_123_0 {
/**
* Target DB version.
*/
const DB_VERSION = '1.123.0';
/**
* DB version option name.
*/
const DB_VERSION_OPTION = 'googlesitekit_db_version';
/**
* Legacy analytics module slug.
*/
const LEGACY_ANALYTICS_MODULE_SLUG = 'analytics';
/**
* Legacy analytics option name.
*/
const LEGACY_ANALYTICS_OPTION = 'googlesitekit_analytics_settings';
/**
* Context instance.
*
* @since 1.123.0
* @var Context
*/
protected $context;
/**
* Options instance.
*
* @since 1.123.0
* @var Options
*/
protected $options;
/**
* Analytics_Settings instance.
*
* @since 1.123.0
* @var Analytics_Settings
*/
protected $analytics_settings;
/**
* Constructor.
*
* @since 1.123.0
*
* @param Context $context Plugin context instance.
* @param Options $options Optional. Options instance.
*/
public function __construct(
Context $context,
?Options $options = null
) {
$this->context = $context;
$this->options = $options ?: new Options( $context );
$this->analytics_settings = new Analytics_Settings( $this->options );
}
/**
* Registers hooks.
*
* @since 1.123.0
*/
public function register() {
add_action( 'admin_init', array( $this, 'migrate' ) );
}
/**
* Migrates the DB.
*
* @since 1.123.0
*/
public function migrate() {
$db_version = $this->options->get( self::DB_VERSION_OPTION );
if ( ! $db_version || version_compare( $db_version, self::DB_VERSION, '<' ) ) {
$this->migrate_legacy_analytics_settings();
$this->activate_analytics();
$this->migrate_legacy_analytics_sharing_settings();
$this->options->set( self::DB_VERSION_OPTION, self::DB_VERSION );
}
}
/**
* Migrates the legacy analytics settings over to analytics-4.
*
* @since 1.123.0
*/
protected function migrate_legacy_analytics_settings() {
if ( ! $this->analytics_settings->has() ) {
return;
}
$legacy_settings = $this->options->get( self::LEGACY_ANALYTICS_OPTION );
if ( empty( $legacy_settings ) ) {
return;
}
$recovered_settings = array();
$options_to_migrate = array(
'accountID',
'adsConversionID',
'trackingDisabled',
);
array_walk(
$options_to_migrate,
function ( $setting ) use ( &$recovered_settings, $legacy_settings ) {
$recovered_settings[ $setting ] = $legacy_settings[ $setting ];
}
);
if ( ! empty( $recovered_settings ) ) {
$this->analytics_settings->merge(
$recovered_settings
);
}
}
/**
* Activates the analytics-4 module if the legacy analytics module was active.
*
* @since 1.123.0
*/
protected function activate_analytics() {
$option = $this->options->get( Modules::OPTION_ACTIVE_MODULES );
// Check legacy option.
if ( ! is_array( $option ) ) {
$option = $this->options->get( 'googlesitekit-active-modules' );
}
if ( ! is_array( $option ) ) {
return;
}
$analytics_active = in_array( Analytics_4::MODULE_SLUG, $option, true );
// If analytics-4 is already active, bail.
if ( $analytics_active ) {
return;
}
$legacy_analytics_active = in_array(
self::LEGACY_ANALYTICS_MODULE_SLUG,
$option,
true
);
if ( $legacy_analytics_active ) {
$option[] = Analytics_4::MODULE_SLUG;
$this->options->set( Modules::OPTION_ACTIVE_MODULES, $option );
}
}
/**
* Replicates sharing settings from the legacy analytics module to analytics-4.
*
* @since 1.123.0
*/
protected function migrate_legacy_analytics_sharing_settings() {
$option = $this->options->get( Module_Sharing_Settings::OPTION );
if ( ! is_array( $option ) ) {
return;
}
// If sharing settings for analytics-4 already exist, bail.
if ( isset( $option[ Analytics_4::MODULE_SLUG ] ) ) {
return;
}
if ( isset( $option[ self::LEGACY_ANALYTICS_MODULE_SLUG ] ) ) {
$option[ Analytics_4::MODULE_SLUG ] = $option[ self::LEGACY_ANALYTICS_MODULE_SLUG ];
$this->options->set( Module_Sharing_Settings::OPTION, $option );
}
}
}

View File

@@ -0,0 +1,169 @@
<?php
/**
* Migration for Conversion ID.
*
* @package Google\Site_Kit\Core\Util
* @copyright 2024 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Core\Util;
use Google\Site_Kit\Context;
use Google\Site_Kit\Core\Modules\Modules;
use Google\Site_Kit\Core\Storage\Options;
use Google\Site_Kit\Modules\Analytics_4\Settings as Analytics_Settings;
use Google\Site_Kit\Modules\Ads;
use Google\Site_Kit\Modules\Ads\Settings as Ads_Settings;
/**
* Class Migration_1_129_0
*
* @since 1.129.0
* @access private
* @ignore
*/
class Migration_1_129_0 {
/**
* Target DB version.
*/
const DB_VERSION = '1.129.0';
/**
* DB version option name.
*/
const DB_VERSION_OPTION = 'googlesitekit_db_version';
/**
* Context instance.
*
* @since 1.129.0
* @var Context
*/
protected $context;
/**
* Options instance.
*
* @since 1.129.0
* @var Options
*/
protected $options;
/**
* Analytics_Settings instance.
*
* @since 1.129.0
* @var Analytics_Settings
*/
protected $analytics_settings;
/**
* Ads_Settings instance.
*
* @since 1.129.0
* @var Ads_Settings
*/
protected $ads_settings;
/**
* Constructor.
*
* @since 1.129.0
*
* @param Context $context Plugin context instance.
* @param Options $options Optional. Options instance.
*/
public function __construct(
Context $context,
?Options $options = null
) {
$this->context = $context;
$this->options = $options ?: new Options( $context );
$this->analytics_settings = new Analytics_Settings( $this->options );
$this->ads_settings = new Ads_Settings( $this->options );
}
/**
* Registers hooks.
*
* @since 1.129.0
*/
public function register() {
add_action( 'admin_init', array( $this, 'migrate' ) );
}
/**
* Migrates the DB.
*
* @since 1.129.0
*/
public function migrate() {
$db_version = $this->options->get( self::DB_VERSION_OPTION );
if ( ! $db_version || version_compare( $db_version, self::DB_VERSION, '<' ) ) {
$this->migrate_analytics_conversion_id_setting();
$this->activate_ads_module();
$this->options->set( self::DB_VERSION_OPTION, self::DB_VERSION );
}
}
/**
* Migrates the Ads Conversion ID to the new Ads module.
*
* @since 1.129.0
*/
protected function migrate_analytics_conversion_id_setting() {
if ( ! $this->analytics_settings->has() ) {
return;
}
$analytics_settings = $this->analytics_settings->get();
if ( empty( $analytics_settings ) || ! array_key_exists( 'adsConversionID', $analytics_settings ) || empty( $analytics_settings['adsConversionID'] ) ) {
return;
}
$ads_settings = $this->ads_settings->get();
if ( array_key_exists( 'conversionID', $ads_settings ) && ! empty( $ads_settings['conversionID'] ) ) {
// If there is already an adsConversionID set in the Ads module, do not overwrite it, remove it from the Analytics module.
unset( $analytics_settings['adsConversionID'] );
$this->analytics_settings->set( $analytics_settings );
return;
}
$ads_settings['conversionID'] = $analytics_settings['adsConversionID'];
$this->ads_settings->set( $ads_settings );
unset( $analytics_settings['adsConversionID'] );
$analytics_settings['adsConversionIDMigratedAtMs'] = time() * 1000;
$this->analytics_settings->set( $analytics_settings );
}
/**
* Activates the ads module if the Ads Conversion ID was previously set.
*
* @since 1.129.0
*/
protected function activate_ads_module() {
$active_modules = $this->options->get( Modules::OPTION_ACTIVE_MODULES );
if ( is_array( $active_modules ) && in_array( 'ads', $active_modules, true ) ) {
return;
}
$ads_settings = $this->ads_settings->get();
// Activate the Ads module if the Ads Conversion ID was previously set
// and the Ads module is not already active.
if ( ! empty( $ads_settings['conversionID'] ) ) {
$active_modules[] = Ads::MODULE_SLUG;
$this->options->set( Modules::OPTION_ACTIVE_MODULES, $active_modules );
}
}
}

View File

@@ -0,0 +1,149 @@
<?php
/**
* Migration for Audience Settings.
*
* @package Google\Site_Kit\Core\Util
* @copyright 2025 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Core\Util;
use Google\Site_Kit\Context;
use Google\Site_Kit\Core\Storage\Options;
use Google\Site_Kit\Modules\Analytics_4\Settings as Analytics_Settings;
use Google\Site_Kit\Modules\Analytics_4\Audience_Settings;
/**
* Class Migration_1_150_0
*
* @since 1.151.0
* @access private
* @ignore
*/
class Migration_1_150_0 {
/**
* Target DB version.
*/
const DB_VERSION = '1.150.0';
/**
* DB version option name.
*/
const DB_VERSION_OPTION = 'googlesitekit_db_version';
/**
* Context instance.
*
* @since 1.151.0
* @var Context
*/
protected $context;
/**
* Options instance.
*
* @since 1.151.0
* @var Options
*/
protected $options;
/**
* Analytics_Settings instance.
*
* @since 1.151.0
* @var Analytics_Settings
*/
protected $analytics_settings;
/**
* Audience_Settings instance.
*
* @since 1.151.0
* @var Audience_Settings
*/
protected $audience_settings;
/**
* Constructor.
*
* @since 1.151.0
*
* @param Context $context Plugin context instance.
* @param Options $options Optional. Options instance.
*/
public function __construct(
Context $context,
?Options $options = null
) {
$this->context = $context;
$this->options = $options ?: new Options( $context );
$this->analytics_settings = new Analytics_Settings( $this->options );
$this->audience_settings = new Audience_Settings( $this->options );
}
/**
* Registers hooks.
*
* @since 1.151.0
*/
public function register() {
add_action( 'admin_init', array( $this, 'migrate' ) );
}
/**
* Migrates the DB.
*
* @since 1.151.0
*/
public function migrate() {
$db_version = $this->options->get( self::DB_VERSION_OPTION );
if ( ! $db_version || version_compare( $db_version, self::DB_VERSION, '<' ) ) {
$this->migrate_audience_settings();
$this->options->set( self::DB_VERSION_OPTION, self::DB_VERSION );
}
}
/**
* Migrates the Audience Settings from Analytics settings to new Audience settings.
*
* @since 1.151.0
*/
protected function migrate_audience_settings() {
if ( ! $this->analytics_settings->has() ) {
return;
}
$analytics_settings = $this->analytics_settings->get();
if ( ! is_array( $analytics_settings ) || empty( $analytics_settings ) ) {
return;
}
$audience_settings = (array) $this->audience_settings->get();
$keys_to_migrate = array(
'availableAudiences',
'availableAudiencesLastSyncedAt',
'audienceSegmentationSetupCompletedBy',
);
$has_migration = false;
foreach ( $keys_to_migrate as $key ) {
if ( array_key_exists( $key, $analytics_settings ) ) {
$audience_settings[ $key ] = $analytics_settings[ $key ];
unset( $analytics_settings[ $key ] );
$has_migration = true;
}
}
if ( $has_migration ) {
$this->audience_settings->set( $audience_settings );
$this->analytics_settings->set( $analytics_settings );
}
}
}

View File

@@ -0,0 +1,132 @@
<?php
/**
* Migration for Audience Settings.
*
* @package Google\Site_Kit\Core\Util
* @copyright 2025 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Core\Util;
use Google\Site_Kit\Context;
use Google\Site_Kit\Core\Storage\Options;
use Google\Site_Kit\Modules\Sign_In_With_Google\Settings as Sign_In_With_Google_Settings;
/**
* Class Migration_1_163_0
*
* @since 1.163.0
* @access private
* @ignore
*/
class Migration_1_163_0 {
/**
* Target DB version.
*/
const DB_VERSION = '1.163.0';
/**
* DB version option name.
*/
const DB_VERSION_OPTION = 'googlesitekit_db_version';
/**
* Context instance.
*
* @since 1.163.0
* @var Context
*/
protected $context;
/**
* Options instance.
*
* @since 1.163.0
* @var Options
*/
protected $options;
/**
* Sign_In_With_Google_Settings instance.
*
* @since 1.163.0
* @var Sign_In_With_Google_Settings
*/
protected $sign_in_with_google_settings;
/**
* Constructor.
*
* @since 1.163.0
*
* @param Context $context Plugin context instance.
* @param Options $options Optional. Options instance.
*/
public function __construct(
Context $context,
?Options $options = null
) {
$this->context = $context;
$this->options = $options ?: new Options( $context );
$this->sign_in_with_google_settings = new Sign_In_With_Google_Settings( $this->options );
}
/**
* Registers hooks.
*
* @since 1.163.0
*/
public function register() {
add_action( 'admin_init', array( $this, 'migrate' ) );
}
/**
* Migrates the DB.
*
* @since 1.163.0
*/
public function migrate() {
$db_version = $this->options->get( self::DB_VERSION_OPTION );
if ( ! $db_version || version_compare( $db_version, self::DB_VERSION, '<' ) ) {
$this->migrate_one_tap_enabled_setting();
$this->options->set( self::DB_VERSION_OPTION, self::DB_VERSION );
}
}
/**
* Migrates the One Tap Setting to the most conservative value based
* on previous user settings.
*
* Given the new setting is equivalent to the old setting of
* "One Tap on all pages", we only set One Tap to be enabled if
* the no-longer-used "One Tap on all pages" setting was set to true.
*
* @since 1.163.0
*/
protected function migrate_one_tap_enabled_setting() {
if ( ! $this->sign_in_with_google_settings->has() ) {
return;
}
$sign_in_with_google_settings = $this->sign_in_with_google_settings->get();
if ( ! is_array( $sign_in_with_google_settings ) || empty( $sign_in_with_google_settings ) ) {
return;
}
if ( true === $sign_in_with_google_settings['oneTapOnAllPages'] ) {
$sign_in_with_google_settings['oneTapEnabled'] = true;
} else {
$sign_in_with_google_settings['oneTapEnabled'] = false;
}
unset( $sign_in_with_google_settings['oneTapOnAllPages'] );
// Save the updated settings.
$this->sign_in_with_google_settings->set( $sign_in_with_google_settings );
}
}

View File

@@ -0,0 +1,133 @@
<?php
/**
* Migration for 1.3.0
*
* @package Google\Site_Kit\Core\Util
* @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\Util;
use Google\Site_Kit\Context;
use Google\Site_Kit\Core\Authentication\Clients\OAuth_Client;
use Google\Site_Kit\Core\Storage\Options;
use Google\Site_Kit\Core\Storage\User_Options;
use Google\Site_Kit\Core\Tracking\Tracking_Consent;
/**
* Class Migration_1_3_0
*
* @since 1.3.0
* @access private
* @ignore
*/
class Migration_1_3_0 {
/**
* Target DB version.
*/
const DB_VERSION = '1.3.0';
/**
* Context instance.
*
* @var Context
*/
protected $context;
/**
* Options instance.
*
* @var Options
*/
protected $options;
/**
* User_Options instance.
*
* @var User_Options
*/
protected $user_options;
/**
* Constructor.
*
* @since 1.3.0
*
* @param Context $context Plugin context instance.
* @param Options $options Optional. Options instance.
* @param User_Options $user_options Optional. User_Options instance.
*/
public function __construct(
Context $context,
?Options $options = null,
?User_Options $user_options = null
) {
$this->context = $context;
$this->options = $options ?: new Options( $context );
$this->user_options = $user_options ?: new User_Options( $context );
}
/**
* Registers hooks.
*
* @since 1.3.0
*/
public function register() {
add_action( 'admin_init', array( $this, 'migrate' ) );
}
/**
* Migrates the DB.
*
* @since 1.3.0
*/
public function migrate() {
$db_version = $this->options->get( 'googlesitekit_db_version' );
if ( ! $db_version || version_compare( $db_version, self::DB_VERSION, '<' ) ) {
$this->migrate_tracking_opt_in();
$this->options->set( 'googlesitekit_db_version', self::DB_VERSION );
}
}
/**
* Migrates the global tracking opt-in to a user option.
*
* @since 1.3.0
* @since 1.4.0 Migrates preference for up to 20 users.
*/
private function migrate_tracking_opt_in() {
// Only migrate if tracking was opted-in.
if ( $this->options->get( Tracking_Consent::OPTION ) ) {
$backup_user_id = $this->user_options->get_user_id();
foreach ( $this->get_authenticated_users() as $user_id ) {
$this->user_options->switch_user( $user_id );
$this->user_options->set( Tracking_Consent::OPTION, 1 );
}
$this->user_options->switch_user( $backup_user_id );
}
}
/**
* Gets the authenticated users connected to Site Kit.
*
* @since 1.4.0
*
* @return string[] User IDs of authenticated users. Maximum of 20.
*/
private function get_authenticated_users() {
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',
'number' => 20,
'fields' => 'ID',
)
);
}
}

View File

@@ -0,0 +1,249 @@
<?php
/**
* Migration for 1.8.1
*
* @package Google\Site_Kit\Core\Util
* @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\Util;
use Google\Site_Kit\Context;
use Google\Site_Kit\Core\Authentication\Authentication;
use Google\Site_Kit\Core\Authentication\Google_Proxy;
use Google\Site_Kit\Core\Authentication\Profile;
use Google\Site_Kit\Core\Authentication\Verification_File;
use Google\Site_Kit\Core\Authentication\Verification_Meta;
use Google\Site_Kit\Core\Permissions\Permissions;
use Google\Site_Kit\Core\Storage\Options;
use Google\Site_Kit\Core\Storage\User_Options;
use WP_User;
use WP_Error;
/**
* Class Migration_1_8_1
*
* @since 1.8.1
* @access private
* @ignore
*/
class Migration_1_8_1 {
/**
* Target DB version.
*/
const DB_VERSION = '1.8.1';
/**
* Context instance.
*
* @since 1.8.1
* @var Context
*/
protected $context;
/**
* Options instance.
*
* @since 1.8.1
* @var Options
*/
protected $options;
/**
* User_Options instance.
*
* @since 1.8.1
* @var User_Options
*/
protected $user_options;
/**
* Authentication instance.
*
* @since 1.8.1
* @var Authentication
*/
protected $authentication;
/**
* Constructor.
*
* @since 1.8.1
*
* @param Context $context Plugin context instance.
* @param Options $options Optional. Options instance.
* @param User_Options $user_options Optional. User_Options instance.
* @param Authentication $authentication Optional. Authentication instance. Default is a new instance.
*/
public function __construct(
Context $context,
?Options $options = null,
?User_Options $user_options = null,
?Authentication $authentication = 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 );
}
/**
* Registers hooks.
*
* @since 1.8.1
*/
public function register() {
add_action( 'admin_init', array( $this, 'migrate' ) );
}
/**
* Migrates the DB.
*
* @since 1.8.1
*/
public function migrate() {
$db_version = $this->options->get( 'googlesitekit_db_version' );
// Do not run if database version already updated.
if ( $db_version && version_compare( $db_version, self::DB_VERSION, '>=' ) ) {
return;
}
// Only run routine if using the authentication service, otherwise it
// is irrelevant.
if ( ! $this->authentication->credentials()->using_proxy() ) {
return;
}
// Only run routine once site credentials present, otherwise it is not
// possible to connect to the authentication service.
if ( ! $this->authentication->credentials()->has() ) {
return;
}
$this->clear_and_flag_unauthorized_verified_users();
// Update database version.
$this->options->set( 'googlesitekit_db_version', self::DB_VERSION );
}
/**
* Checks whether there are any users that are verified without proper
* authorization, clear their Site Kit data, and flag them on the
* authentication service.
*
* @since 1.8.1
*
* @return boolean|WP_Error True on success, WP_Error on failure.
*/
private function clear_and_flag_unauthorized_verified_users() {
// Detect all unauthorized verified users and clean their Site Kit data.
$unauthorized_identifiers = $this->clear_unauthorized_verified_users();
// If no unauthorized verified users found, all is well, no need to
// show a notification.
if ( empty( $unauthorized_identifiers ) ) {
return true;
}
// Flag site as affected so that the notification to inform and explain
// steps to resolve will be shown.
$credentials = $this->authentication->credentials()->get();
$google_proxy = new Google_Proxy( $this->context );
$response = wp_remote_post(
$google_proxy->url( '/notifications/mark/' ),
array(
'body' => array(
'site_id' => $credentials['oauth2_client_id'],
'site_secret' => $credentials['oauth2_client_secret'],
'notification_id' => 'verification_leak',
'notification_state' => 'required',
// This is a special parameter only supported for this
// particular notification.
'identifiers' => implode( ',', $unauthorized_identifiers ),
),
)
);
if ( is_wp_error( $response ) ) {
return $response;
}
$response_code = wp_remote_retrieve_response_code( $response );
if ( 200 !== $response_code ) {
$body = wp_remote_retrieve_body( $response );
$decoded = json_decode( $body, true );
return new WP_Error( $response_code, ! empty( $decoded['error'] ) ? $decoded['error'] : $body );
}
return true;
}
/**
* Checks for any users that are verified without proper authorization and
* clears all their Site Kit data.
*
* @since 1.8.1
*
* @return array List of email addresses for the unauthorized users.
*/
private function clear_unauthorized_verified_users() {
global $wpdb;
$unauthorized_identifiers = array();
$profile = new Profile( $this->user_options );
// Store original user ID to switch back later.
$backup_user_id = $this->user_options->get_user_id();
// Iterate through all users verified via Site Kit.
foreach ( $this->get_verified_user_ids() as $user_id ) {
$this->user_options->switch_user( $user_id );
// If the user has setup access, there is no problem.
if ( user_can( $user_id, Permissions::SETUP ) ) {
continue;
}
// Try to get profile email, otherwise fall back to WP email.
if ( $this->authentication->profile()->has() ) {
$unauthorized_identifiers[] = $this->authentication->profile()->get()['email'];
} else {
$user = get_user_by( 'id', $user_id );
$unauthorized_identifiers[] = $user->user_email;
}
$prefix = $this->user_options->get_meta_key( 'googlesitekit\_%' );
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
$wpdb->query(
$wpdb->prepare( "DELETE FROM $wpdb->usermeta WHERE user_id = %d AND meta_key LIKE %s", $user_id, $prefix )
);
wp_cache_delete( $user_id, 'user_meta' );
}
// Restore original user ID.
$this->user_options->switch_user( $backup_user_id );
return $unauthorized_identifiers;
}
/**
* Gets all user IDs that are verified via Site Kit.
*
* @since @1.31.0
*
* @return array List of user ids of verified users. Maximum of 20.
*/
private function get_verified_user_ids() {
global $wpdb;
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
return $wpdb->get_col(
$wpdb->prepare(
"SELECT user_id FROM $wpdb->usermeta WHERE meta_key IN (%s, %s) LIMIT 20",
$this->user_options->get_meta_key( Verification_File::OPTION ),
$this->user_options->get_meta_key( Verification_Meta::OPTION )
)
);
}
}

View File

@@ -0,0 +1,51 @@
<?php
/**
* Class Google\Site_Kit\Core\Util\Plugin_Status
*
* @package Google\Site_Kit
* @copyright 2025 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Core\Util;
/**
* Utility class for checking the status of plugins.
*
* @since 1.148.0
* @access private
* @ignore
*/
class Plugin_Status {
/**
* Determines whether a plugin is installed.
*
* @since 1.148.0
*
* @param string|callable $plugin_or_predicate String plugin file to check or
* a function that accepts plugin header data and plugin file name to test.
*
* @return bool|string Boolean if checking by plugin file or plugin not found,
* String plugin file if checking using a predicate function.
*/
public static function is_plugin_installed( $plugin_or_predicate ) {
if ( ! function_exists( 'get_plugins' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
if ( is_string( $plugin_or_predicate ) ) {
return array_key_exists( $plugin_or_predicate, get_plugins() );
}
if ( ! is_callable( $plugin_or_predicate ) ) {
return false;
}
foreach ( get_plugins() as $plugin_file => $data ) {
if ( $plugin_or_predicate( $data, $plugin_file ) ) {
return $plugin_file;
}
}
return false;
}
}

View File

@@ -0,0 +1,141 @@
<?php
/**
* Class Google\Site_Kit\Core\Util\REST_Entity_Search_Controller
*
* @package Google\Site_Kit\Core\Util
* @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\Util;
use Google\Site_Kit\Context;
use Google\Site_Kit\Core\Permissions\Permissions;
use Google\Site_Kit\Core\REST_API\REST_Route;
use WP_REST_Server;
use WP_REST_Request;
use WP_REST_Response;
/**
* Class for handling entity search REST routes.
*
* @since 1.68.0
* @access private
* @ignore
*/
class REST_Entity_Search_Controller {
/**
* Plugin context.
*
* @since 1.68.0
* @var Context
*/
private $context;
/**
* Constructor.
*
* @since 1.68.0
*
* @param Context $context Plugin context.
*/
public function __construct( Context $context ) {
$this->context = $context;
}
/**
* Registers functionality through WordPress hooks.
*
* @since 1.68.0
*/
public function register() {
add_filter(
'googlesitekit_rest_routes',
function ( $routes ) {
return array_merge( $routes, $this->get_rest_routes() );
}
);
}
/**
* Gets REST route instances.
*
* @since 1.68.0
*
* @return REST_Route[] List of REST_Route objects.
*/
protected function get_rest_routes() {
$can_search = function () {
return current_user_can( Permissions::AUTHENTICATE ) || current_user_can( Permissions::VIEW_SHARED_DASHBOARD );
};
return array(
new REST_Route(
'core/search/data/entity-search',
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => function ( WP_REST_Request $request ) {
$query = rawurldecode( $request['query'] );
$entities = array();
if ( filter_var( $query, FILTER_VALIDATE_URL ) ) {
$entity = $this->context->get_reference_entity_from_url( $query );
if ( $entity && $entity->get_id() ) {
$entities = array(
array(
'id' => $entity->get_id(),
'title' => $entity->get_title(),
'url' => $entity->get_url(),
'type' => $entity->get_type(),
),
);
}
} else {
$args = array(
'posts_per_page' => 10,
'google-site-kit' => 1,
's' => $query,
'no_found_rows' => true,
'update_post_meta_cache' => false,
'update_post_term_cache' => false,
'post_status' => array( 'publish' ),
);
$posts = ( new \WP_Query( $args ) )->posts;
if ( ! empty( $posts ) ) {
$entities = array_map(
function ( $post ) {
$entity = Entity_Factory::create_entity_for_post( $post, 1 );
return array(
'id' => $entity->get_id(),
'title' => $entity->get_title(),
'url' => $entity->get_url(),
'type' => $entity->get_type(),
);
},
$posts
);
}
}
return new WP_REST_Response( $entities );
},
'permission_callback' => $can_search,
),
),
array(
'args' => array(
'query' => array(
'type' => 'string',
'description' => __( 'Text content to search for.', 'google-site-kit' ),
'required' => true,
),
),
)
),
);
}
}

View File

@@ -0,0 +1,46 @@
<?php
/**
* Trait Google\Site_Kit\Core\Util\Requires_Javascript_Trait
*
* @package Google\Site_Kit\Core\Util
* @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\Util;
/**
* Trait to display no javascript fallback message.
*
* @since 1.5.0
* @access private
* @ignore
*/
trait Requires_Javascript_Trait {
/**
* Outputs a fallback message when Javascript is disabled.
*
* @since 1.5.0
*/
protected function render_noscript_html() {
?>
<noscript>
<div class="googlesitekit-noscript notice notice-warning">
<div class="mdc-layout-grid">
<div class="mdc-layout-grid__inner">
<div class="mdc-layout-grid__cell mdc-layout-grid__cell--span-12">
<p class="googlesitekit-noscript__text">
<?php
esc_html_e( 'The Site Kit by Google plugin requires JavaScript to be enabled in your browser.', 'google-site-kit' )
?>
</p>
</div>
</div>
</div>
</div>
</noscript>
<?php
}
}

View File

@@ -0,0 +1,355 @@
<?php
/**
* Class Google\Site_Kit\Core\Util\Reset
*
* @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\Util;
use Google\Site_Kit\Context;
use Google\Site_Kit\Core\Authentication\Authentication;
use Google\Site_Kit\Core\Permissions\Permissions;
use Google\Site_Kit\Core\REST_API\REST_Route;
use WP_REST_Server;
use WP_REST_Request;
use WP_REST_Response;
/**
* Class providing functions to reset the plugin.
*
* @since 1.0.0
* @since 1.1.1 Removed delete_all_plugin_options(), delete_all_user_metas() and delete_all_transients() methods.
* @access private
* @ignore
*/
class Reset {
/**
* MySQL key pattern for all Site Kit keys.
*/
const KEY_PATTERN = 'googlesitekit\_%';
/**
* REST API endpoint.
*/
const REST_ROUTE = 'core/site/data/reset';
/**
* Action for triggering a reset.
*/
const ACTION = 'googlesitekit_reset';
/**
* Plugin context.
*
* @since 1.0.0
* @var Context
*/
private $context;
/**
* Gets the URL to handle a reset action.
*
* @since 1.30.0
*
* @return string
*/
public static function url() {
return add_query_arg(
array(
'action' => static::ACTION,
'nonce' => wp_create_nonce( static::ACTION ),
),
admin_url( 'index.php' )
);
}
/**
* Constructor.
*
* @since 1.0.0
* @since 1.1.1 Removed $options and $transients params.
*
* @param Context $context Plugin context.
*/
public function __construct( Context $context ) {
$this->context = $context;
}
/**
* Registers functionality through WordPress hooks.
*
* @since 1.3.0
*/
public function register() {
add_filter(
'googlesitekit_rest_routes',
function ( $routes ) {
return array_merge( $routes, $this->get_rest_routes() );
}
);
add_action(
'admin_action_' . static::ACTION,
function () {
$this->handle_reset_action(
$this->context->input()->filter( INPUT_GET, 'nonce' )
);
}
);
}
/**
* Deletes options, user stored options, transients and clears object cache for stored options.
*
* @since 1.0.0
*/
public function all() {
$this->delete_options( 'site' );
$this->delete_user_options( 'site' );
$this->delete_post_meta( 'site' );
$this->delete_term_meta( 'site' );
if ( $this->context->is_network_mode() ) {
$this->delete_options( 'network' );
$this->delete_user_options( 'network' );
$this->delete_post_meta( 'network' );
$this->delete_term_meta( 'network' );
}
wp_cache_flush();
}
/**
* Deletes all Site Kit options and transients.
*
* @since 1.3.0
*
* @param string $scope Scope of the deletion ('site' or 'network').
*/
private function delete_options( $scope ) {
global $wpdb;
if ( 'site' === $scope ) {
list ( $table_name, $column_name, $transient_prefix ) = array( $wpdb->options, 'option_name', '_transient_' );
} elseif ( 'network' === $scope ) {
list ( $table_name, $column_name, $transient_prefix ) = array( $wpdb->sitemeta, 'meta_key', '_site_transient_' );
} else {
return;
}
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
$wpdb->query(
$wpdb->prepare(
/* phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared */
"
DELETE FROM {$table_name}
WHERE {$column_name} LIKE %s
OR {$column_name} LIKE %s
OR {$column_name} LIKE %s
OR {$column_name} = %s
", /* phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared */
static::KEY_PATTERN,
$transient_prefix . static::KEY_PATTERN,
$transient_prefix . 'timeout_' . static::KEY_PATTERN,
'googlesitekit-active-modules'
)
);
}
/**
* Deletes all Site Kit user options.
*
* @param string $scope Scope of the deletion ('site' or 'network').
*/
private function delete_user_options( $scope ) {
global $wpdb;
if ( 'site' === $scope ) {
$meta_prefix = $wpdb->get_blog_prefix();
} elseif ( 'network' === $scope ) {
$meta_prefix = '';
} else {
return;
}
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
$wpdb->query(
$wpdb->prepare(
"DELETE FROM {$wpdb->usermeta} WHERE meta_key LIKE %s",
$meta_prefix . static::KEY_PATTERN
)
);
}
/**
* Deletes all Site Kit post meta settings.
*
* @since 1.33.0
*
* @param string $scope Scope of the deletion ('site' or 'network').
*/
private function delete_post_meta( $scope ) {
global $wpdb;
$sites = array();
if ( 'network' === $scope ) {
$sites = get_sites(
array(
'fields' => 'ids',
'number' => 9999999,
)
);
} else {
$sites[] = get_current_blog_id();
}
foreach ( $sites as $site_id ) {
$prefix = $wpdb->get_blog_prefix( $site_id );
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
$wpdb->query(
$wpdb->prepare(
"DELETE FROM {$prefix}postmeta WHERE `meta_key` LIKE %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
static::KEY_PATTERN
)
);
}
}
/**
* Deletes all Site Kit term meta settings.
*
* @since 1.146.0
*
* @param string $scope Scope of the deletion ('site' or 'network').
*/
private function delete_term_meta( $scope ) {
global $wpdb;
$sites = array();
if ( 'network' === $scope ) {
$sites = get_sites(
array(
'fields' => 'ids',
'number' => 9999999,
)
);
} else {
$sites[] = get_current_blog_id();
}
foreach ( $sites as $site_id ) {
$prefix = $wpdb->get_blog_prefix( $site_id );
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
$wpdb->query(
$wpdb->prepare(
"DELETE FROM {$prefix}termmeta WHERE `meta_key` LIKE %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
static::KEY_PATTERN
)
);
}
}
/**
* Gets related REST routes.
*
* @since 1.3.0
*
* @return array List of REST_Route objects.
*/
private function get_rest_routes() {
$can_setup = function () {
return current_user_can( Permissions::SETUP );
};
return array(
new REST_Route(
static::REST_ROUTE,
array(
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => function () {
$this->all();
$this->maybe_hard_reset();
// Call hooks on plugin reset. This is used to reset the ad blocking recovery notification.
do_action( 'googlesitekit_reset' );
return new WP_REST_Response( true );
},
'permission_callback' => $can_setup,
),
)
),
);
}
/**
* Handles the reset admin action.
*
* @since 1.30.0
*
* @param string $nonce WP nonce for action.
*/
private function handle_reset_action( $nonce ) {
if ( ! wp_verify_nonce( $nonce, static::ACTION ) ) {
$authentication = new Authentication( $this->context );
$authentication->invalid_nonce_error( static::ACTION );
}
if ( ! current_user_can( Permissions::SETUP ) ) {
wp_die( esc_html__( 'You dont have permissions to set up Site Kit.', 'google-site-kit' ), 403 );
}
// Call hooks on plugin reset. This is used to reset the ad blocking recovery notification.
do_action( 'googlesitekit_reset' );
$this->all();
$this->maybe_hard_reset();
wp_safe_redirect(
$this->context->admin_url(
'splash',
array(
// Trigger client-side storage reset.
'googlesitekit_reset_session' => 1,
// Show reset-success notification.
'notification' => 'reset_success',
)
)
);
exit;
}
/**
* Performs hard reset if it is enabled programmatically.
*
* @since 1.46.0
*/
public function maybe_hard_reset() {
/**
* Filters the hard reset option, which is `false` by default.
*
* By default, when Site Kit is reset it does not delete "persistent" data
* (options prefixed with `googlesitekitpersistent_`). If this filter returns `true`,
* all options belonging to Site Kit, including those with the above "persistent"
* prefix, will be deleted.
*
* @since 1.46.0
*
* @param bool $hard_reset_enabled If a hard reset is enabled. `false` by default.
*/
$hard_reset_enabled = apply_filters( 'googlesitekit_hard_reset_enabled', false );
if ( ! $hard_reset_enabled ) {
return;
}
$reset_persistent = new Reset_Persistent( $this->context );
$reset_persistent->all();
}
}

View File

@@ -0,0 +1,31 @@
<?php
/**
* Class Google\Site_Kit\Core\Util\Reset_Persistent
*
* @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\Util;
/**
* Class providing functions to reset the persistent plugin settings.
*
* @since 1.27.0
* @access private
* @ignore
*/
class Reset_Persistent extends Reset {
/**
* MySQL key pattern for all persistent Site Kit keys.
*/
const KEY_PATTERN = 'googlesitekitpersistent\_%';
/**
* REST API endpoint.
*/
const REST_ROUTE = 'core/site/data/reset-persistent';
}

View File

@@ -0,0 +1,48 @@
<?php
/**
* Class Google\Site_Kit\Core\Util\Sanitize
*
* @package Google\Site_Kit\Core\Util
* @copyright 2023 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Core\Util;
/**
* Utility class for sanitizing data.
*
* @since 1.93.0
* @access private
* @ignore
*/
class Sanitize {
/**
* Filters empty or non-string elements from a given array.
*
* @since 1.93.0
*
* @param array $elements Array to check.
* @return array Empty array or a filtered array containing only non-empty strings.
*/
public static 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 );
}
}

View File

@@ -0,0 +1,93 @@
<?php
/**
* Class Google\Site_Kit\Core\Util\Scopes
*
* @package Google\Site_Kit\Core\Util
* @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\Util;
/**
* Utility class for handling generic OAuth scope functions.
*
* @since 1.9.0
* @access private
* @ignore
*/
class Scopes {
/**
* Mapping of requested scope to satisfying scopes.
*
* @since 1.9.0
*
* @var array
*/
protected static $map = array(
'https://www.googleapis.com/auth/adsense.readonly' => array(
'https://www.googleapis.com/auth/adsense',
),
'https://www.googleapis.com/auth/analytics.readonly' => array(
'requires_all' => true,
'https://www.googleapis.com/auth/analytics',
'https://www.googleapis.com/auth/analytics.edit',
),
'https://www.googleapis.com/auth/tagmanager.readonly' => array(
'https://www.googleapis.com/auth/tagmanager.edit.containers',
),
'https://www.googleapis.com/auth/webmasters.readonly' => array(
'https://www.googleapis.com/auth/webmasters',
),
);
/**
* Tests if the given scope is satisfied by the given list of granted scopes.
*
* @since 1.9.0
*
* @param string $scope OAuth scope to test for.
* @param string[] $granted_scopes Available OAuth scopes to test the individual scope against.
* @return bool True if the given scope is satisfied, otherwise false.
*/
public static function is_satisfied_by( $scope, array $granted_scopes ) {
if ( in_array( $scope, $granted_scopes, true ) ) {
return true;
}
if ( empty( self::$map[ $scope ] ) ) {
return false;
}
$satisfying_scopes = array_filter( self::$map[ $scope ], 'is_string' );
if ( ! empty( self::$map[ $scope ]['requires_all'] ) ) {
// Return true if all satisfying scopes are present, otherwise false.
return ! array_diff( $satisfying_scopes, $granted_scopes );
}
// Return true if any of the scopes are present, otherwise false.
return (bool) array_intersect( $satisfying_scopes, $granted_scopes );
}
/**
* Tests if all the given scopes are satisfied by the list of granted scopes.
*
* @since 1.9.0
*
* @param string[] $scopes OAuth scopes to test.
* @param string[] $granted_scopes OAuth scopes to test $scopes against.
* @return bool True if all given scopes are satisfied, otherwise false.
*/
public static function are_satisfied_by( array $scopes, array $granted_scopes ) {
foreach ( $scopes as $scope ) {
if ( ! self::is_satisfied_by( $scope, $granted_scopes ) ) {
return false;
}
}
return true;
}
}

View File

@@ -0,0 +1,55 @@
<?php
/**
* Class Google\Site_Kit\Core\Util\Sort
*
* @package Google\Site_Kit\Core\Util
* @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\Util;
/**
* Utility class for sorting lists.
*
* @since 1.90.0
* @access private
* @ignore
*/
class Sort {
/**
* Sorts the provided list in a case-insensitive manner.
*
* @since 1.90.0
*
* @param array $list_to_sort The list to sort.
* @param string $orderby The field by which the list should be ordered by.
*
* @return array The sorted list.
*/
public static function case_insensitive_list_sort( array $list_to_sort, $orderby ) {
usort(
$list_to_sort,
function ( $a, $b ) use ( $orderby ) {
if ( is_array( $a ) && is_array( $b ) ) {
return strcasecmp(
$a[ $orderby ],
$b[ $orderby ]
);
}
if ( is_object( $a ) && is_object( $b ) ) {
return strcasecmp(
$a->$orderby,
$b->$orderby
);
}
return 0;
}
);
return $list_to_sort;
}
}

View File

@@ -0,0 +1,155 @@
<?php
/**
* Class Google\Site_Kit\Core\Util\Synthetic_WP_Query
*
* @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\Util;
use WP_Query;
use WP_Post;
/**
* Class extending WordPress core's `WP_Query` for more self-contained behavior.
*
* @since 1.16.0
* @access private
* @ignore
*/
final class Synthetic_WP_Query extends WP_Query {
/**
* The hash of the `$query` last parsed into `$query_vars`.
*
* @since 1.16.0
* @var string
*/
private $parsed_query_hash = '';
/**
* Whether automatic 404 detection in `get_posts()` method is enabled.
*
* @since 1.16.0
* @var bool
*/
private $enable_404_detection = false;
/**
* Sets whether 404 detection in `get_posts()` method should be enabled.
*
* @since 1.16.0
*
* @param bool $enable Whether or not to enable 404 detection.
*/
public function enable_404_detection( $enable ) {
$this->enable_404_detection = (bool) $enable;
}
/**
* Initiates object properties and sets default values.
*
* @since 1.16.0
*/
public function init() {
parent::init();
$this->parsed_query_hash = '';
}
/**
* Extends `WP_Query::parse_query()` to ensure it is not unnecessarily run twice.
*
* @since 1.16.0
*
* @param string|array $query Optional. Array or string of query parameters. See `WP_Query::parse_query()`.
*/
public function parse_query( $query = '' ) {
if ( ! empty( $query ) ) {
$query_to_hash = wp_parse_args( $query );
} elseif ( ! isset( $this->query ) ) {
$query_to_hash = $this->query_vars;
} else {
$query_to_hash = $this->query;
}
// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
$query_hash = md5( serialize( $query_to_hash ) );
// If this query was parsed before, bail early.
if ( $query_hash === $this->parsed_query_hash ) {
return;
}
parent::parse_query( $query );
// Set query hash for current `$query` and `$query_vars` properties.
$this->parsed_query_hash = $query_hash;
}
/**
* Extends `WP_Query::get_posts()` to include supplemental logic such as detecting a 404 state.
*
* The majority of the code is a copy of `WP::handle_404()`.
*
* @since 1.16.0
*
* @return WP_Post[]|int[] Array of post objects or post IDs.
*/
public function get_posts() {
$results = parent::get_posts();
// If 404 detection is not enabled, just return the results.
if ( ! $this->enable_404_detection ) {
return $results;
}
// Check if this is a single paginated post query.
if ( $this->posts && $this->is_singular() && $this->post && ! empty( $this->query_vars['page'] ) ) {
// If the post is actually paged and the 'page' query var is within bounds, it's all good.
$next = '<!--nextpage-->';
if ( false !== strpos( $this->post->post_content, $next ) && (int) trim( $this->query_vars['page'], '/' ) <= ( substr_count( $this->post->post_content, $next ) + 1 ) ) {
return $results;
}
// Otherwise, this query is out of bounds, so set a 404.
$this->set_404();
return $results;
}
// If no posts were found, this is technically a 404.
if ( ! $this->posts ) {
// If this is a paginated query (i.e. out of bounds), always consider it a 404.
if ( $this->is_paged() ) {
$this->set_404();
return $results;
}
// If this is an author archive, don't consider it a 404 if the author exists.
if ( $this->is_author() ) {
$author = $this->get( 'author' );
if ( is_numeric( $author ) && $author > 0 && is_user_member_of_blog( $author ) ) {
return $results;
}
}
// If this is a valid taxonomy or post type archive, don't consider it a 404.
if ( ( $this->is_category() || $this->is_tag() || $this->is_tax() || $this->is_post_type_archive() ) && $this->get_queried_object() ) {
return $results;
}
// If this is a search results page or the home index, don't consider it a 404.
if ( $this->is_home() || $this->is_search() ) {
return $results;
}
// Otherwise, set a 404.
$this->set_404();
}
return $results;
}
}

View File

@@ -0,0 +1,177 @@
<?php
/**
* Class Google\Site_Kit\Core\Util\URL
*
* @package Google\Site_Kit\Core\Util
* @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\Util;
/**
* Class for custom URL parsing methods.
*
* @since 1.84.0
* @access private
* @ignore
*/
class URL {
/**
* Prefix for Punycode-encoded hostnames.
*/
const PUNYCODE_PREFIX = 'xn--';
/**
* Parses URLs with UTF-8 multi-byte characters,
* otherwise similar to `wp_parse_url()`.
*
* @since 1.84.0
*
* @param string $url The URL to parse.
* @param int $component The specific component to retrieve. Use one of the PHP
* predefined constants to specify which one.
* Defaults to -1 (= return all parts as an array).
* @return mixed False on parse failure; Array of URL components on success;
* When a specific component has been requested: null if the component
* doesn't exist in the given URL; a string or - in the case of
* PHP_URL_PORT - integer when it does. See parse_url()'s return values.
*/
public static function parse( $url, $component = -1 ) {
$url = (string) $url;
if ( mb_strlen( $url, 'UTF-8' ) === strlen( $url ) ) {
return wp_parse_url( $url, $component );
}
$to_unset = array();
if ( '//' === mb_substr( $url, 0, 2 ) ) {
$to_unset[] = 'scheme';
$url = 'placeholder:' . $url;
} elseif ( '/' === mb_substr( $url, 0, 1 ) ) {
$to_unset[] = 'scheme';
$to_unset[] = 'host';
$url = 'placeholder://placeholder' . $url;
}
$parts = self::mb_parse_url( $url );
if ( false === $parts ) {
// Parsing failure.
return $parts;
}
// Remove the placeholder values.
foreach ( $to_unset as $key ) {
unset( $parts[ $key ] );
}
return _get_component_from_parsed_url_array( $parts, $component );
}
/**
* Replacement for parse_url which is UTF-8 multi-byte character aware.
*
* @since 1.84.0
*
* @param string $url The URL to parse.
* @return mixed False on parse failure; Array of URL components on success
*/
private static function mb_parse_url( $url ) {
$enc_url = preg_replace_callback(
'%[^:/@?&=#]+%usD',
function ( $matches ) {
return rawurlencode( $matches[0] );
},
$url
);
$parts = parse_url( $enc_url ); // phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url
if ( false === $parts ) {
return $parts;
}
foreach ( $parts as $name => $value ) {
$parts[ $name ] = urldecode( $value );
}
return $parts;
}
/**
* Permutes site URL to cover all different variants of it (not considering the path).
*
* @since 1.99.0
*
* @param string $site_url Site URL to get permutations for.
* @return array List of permutations.
*/
public static function permute_site_url( $site_url ) {
$hostname = self::parse( $site_url, PHP_URL_HOST );
$path = self::parse( $site_url, PHP_URL_PATH );
return array_reduce(
self::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.99.0
*
* @param string $hostname Hostname to generate variations of.
* @return string[] Hostname variations.
*/
public static function permute_site_hosts( $hostname ) {
if ( ! $hostname || ! is_string( $hostname ) ) {
return array();
}
// 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, self::PUNYCODE_PREFIX ) || false !== strpos( $hostname, '.' . self::PUNYCODE_PREFIX ) ) {
// Ignoring phpcs here, and not passing the variant so that the correct default can be selected by PHP based on the
// version. INTL_IDNA_VARIANT_UTS46 for PHP>=7.4, INTL_IDNA_VARIANT_2003 for PHP<7.4.
// phpcs:ignore PHPCompatibility.ParameterValues.NewIDNVariantDefault.NotSet
$host_decoded = idn_to_utf8( $hostname );
array_push( $hosts, $host_decoded, "www.$host_decoded" );
}
} else {
// If it's not ASCII, then add the punycode encoded version.
// Ignoring phpcs here, and not passing the variant so that the correct default can be selected by PHP based on the
// version. INTL_IDNA_VARIANT_UTS46 for PHP>=7.4, INTL_IDNA_VARIANT_2003 for PHP<7.4.
// phpcs:ignore PHPCompatibility.ParameterValues.NewIDNVariantDefault.NotSet
$host_encoded = idn_to_ascii( $hostname );
array_push( $hosts, $host_encoded, "www.$host_encoded" );
}
} catch ( Exception $exception ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
// Do nothing.
}
return $hosts;
}
}

View File

@@ -0,0 +1,145 @@
<?php
/**
* Class Google\Site_Kit\Core\Util\Uninstallation
*
* @package Google\Site_Kit\Core\Util
* @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\Util;
use Google\Site_Kit\Context;
use Google\Site_Kit\Core\Storage\Options;
use Google\Site_Kit\Core\Storage\Encrypted_Options;
use Google\Site_Kit\Core\Authentication\Credentials;
use Google\Site_Kit\Core\Authentication\Google_Proxy;
use Google\Site_Kit\Core\Authentication\Clients\OAuth_Client;
use Google\Site_Kit\Core\Remote_Features\Remote_Features_Cron;
use Google\Site_Kit\Core\Tags\Google_Tag_Gateway\Google_Tag_Gateway_Cron;
use Google\Site_Kit\Modules\Analytics_4\Conversion_Reporting\Conversion_Reporting_Cron;
use Google\Site_Kit\Modules\Analytics_4\Synchronize_AdSenseLinked;
use Google\Site_Kit\Modules\Analytics_4\Synchronize_AdsLinked;
use Google\Site_Kit\Modules\Analytics_4\Synchronize_Property;
/**
* Utility class for handling uninstallation of the plugin.
*
* @since 1.20.0
* @access private
* @ignore
*/
class Uninstallation {
/**
* Plugin context.
*
* @since 1.20.0
* @var Context
*/
private $context;
/**
* Options instance.
*
* @since 1.20.0
* @var Options
*/
private $options;
/**
* List of scheduled events.
*
* @since 1.136.0
* @var array
*/
const SCHEDULED_EVENTS = array(
Conversion_Reporting_Cron::CRON_ACTION,
OAuth_Client::CRON_REFRESH_PROFILE_DATA,
Remote_Features_Cron::CRON_ACTION,
Synchronize_AdSenseLinked::CRON_SYNCHRONIZE_ADSENSE_LINKED,
Synchronize_AdsLinked::CRON_SYNCHRONIZE_ADS_LINKED,
Synchronize_Property::CRON_SYNCHRONIZE_PROPERTY,
Google_Tag_Gateway_Cron::CRON_ACTION,
);
/**
* Constructor.
*
* This class and its logic must be instantiated early in the WordPress
* bootstrap lifecycle because the 'uninstall.php' script runs decoupled
* from regular action hooks like 'init'.
*
* @since 1.20.0
*
* @param Context $context Plugin context.
* @param Options $options Optional. Options instance. Default is a new instance.
*/
public function __construct(
Context $context,
?Options $options = null
) {
$this->context = $context;
$this->options = $options ?: new Options( $this->context );
}
/**
* Registers functionality through WordPress hooks.
*
* @since 1.20.0
*/
public function register() {
add_action(
'googlesitekit_uninstallation',
function () {
$this->uninstall();
$this->clear_scheduled_events();
}
);
add_action(
'googlesitekit_deactivation',
function () {
$this->clear_scheduled_events();
}
);
add_action(
'googlesitekit_reset',
function () {
$this->clear_scheduled_events();
}
);
}
/**
* Runs necessary logic for uninstallation of the plugin.
*
* If connected to the proxy, it will issue a request to unregister the site.
*
* @since 1.20.0
*/
private function uninstall() {
$credentials = new Credentials( new Encrypted_Options( $this->options ) );
if ( $credentials->has() && $credentials->using_proxy() ) {
$google_proxy = new Google_Proxy( $this->context );
$google_proxy->unregister_site( $credentials );
}
}
/**
* Clears all scheduled events.
*
* @since 1.136.0
*/
private function clear_scheduled_events() {
foreach ( self::SCHEDULED_EVENTS as $event ) {
// Only clear scheduled events that are set, important in E2E
// testing.
if ( (bool) wp_next_scheduled( $event ) ) {
wp_unschedule_hook( $event );
}
}
}
}

View File

@@ -0,0 +1,80 @@
<?php
/**
* Trait Google\Site_Kit\Core\Util\WP_Context_Switcher_Trait
*
* @package Google\Site_Kit\Core\Util
* @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\Util;
/**
* Trait for temporarily switching WordPress context, e.g. from admin to frontend.
*
* @since 1.16.0
* @access private
* @ignore
*/
trait WP_Context_Switcher_Trait {
/**
* Switches to WordPress frontend context if necessary.
*
* Context is only switched if WordPress is not already in frontend context. Context should only ever be switched
* temporarily. Call the returned closure as soon as possible after to restore the original context.
*
* @since 1.16.0
*
* @return callable Closure that restores context.
*/
protected static function with_frontend_context() {
$restore = self::get_restore_current_screen_closure();
if ( ! is_admin() ) {
return $restore;
}
self::switch_current_screen( 'front' );
return $restore;
}
/**
* Switches the current WordPress screen via the given screen ID or hook name.
*
* @since 1.16.0
*
* @param string $screen_id WordPress screen ID.
*/
private static function switch_current_screen( $screen_id ) {
global $current_screen;
require_once ABSPATH . 'wp-admin/includes/class-wp-screen.php';
require_once ABSPATH . 'wp-admin/includes/screen.php';
$current_screen = \WP_Screen::get( $screen_id ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
}
/**
* Returns the closure to restore the current screen.
*
* Calling the closure will restore the `$current_screen` global to what it was set to at the time of calling
* this method.
*
* @since 1.16.0
*
* @return callable Closure that restores context.
*/
private static function get_restore_current_screen_closure() {
global $current_screen;
$original_screen = $current_screen;
return static function () use ( $original_screen ) {
global $current_screen;
$current_screen = $original_screen; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
};
}
}

View File

@@ -0,0 +1,290 @@
<?php
/**
* Class Google\Site_Kit\Core\Util\WP_Query_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\Util;
use WP_Query;
/**
* Class creating `WP_Query` instances.
*
* @since 1.15.0
* @access private
* @ignore
*/
final class WP_Query_Factory {
use WP_Context_Switcher_Trait;
/**
* Creates a `WP_Query` instance to use for a given URL.
*
* The `WP_Query` instance returned is initialized with the correct query arguments, but the actual query will not
* have run yet. The `WP_Query::get_posts()` method should be used to do that.
*
* This is an expensive function that works similarly to WordPress core's `url_to_postid()` function, however also
* covering non-post URLs. It follows logic used in `WP::parse_request()` to cover the other kinds of URLs. The
* majority of the code is a direct copy of certain parts of these functions.
*
* @since 1.15.0
*
* @param string $url URL to get WordPress query object for.
* @return WP_Query|null WordPress query instance, or null if unable to parse query from URL.
*/
public static function from_url( $url ) {
$url = self::normalize_url( $url );
if ( empty( $url ) ) {
return null;
}
$url_path_vars = self::get_url_path_vars( $url );
$url_query_vars = self::get_url_query_vars( $url );
$query_args = self::parse_wp_query_args( $url_path_vars, $url_query_vars );
$restore_context = self::with_frontend_context();
// Return extended version of `WP_Query` with self-contained 404 detection.
$query = new Synthetic_WP_Query();
$query->parse_query( $query_args );
$query->enable_404_detection( true );
$restore_context();
return $query;
}
/**
* Normalizes the URL for further processing.
*
* @since 1.15.0
*
* @param string $url URL to normalize.
* @return string Normalized URL, or empty string if URL is irrelevant for parsing into `WP_Query` arguments.
*/
private static function normalize_url( $url ) {
global $wp_rewrite;
$url_host = str_replace( 'www.', '', URL::parse( $url, PHP_URL_HOST ) );
$home_url_host = str_replace( 'www.', '', URL::parse( home_url(), PHP_URL_HOST ) );
// Bail early if the URL does not belong to this site.
if ( $url_host && $url_host !== $home_url_host ) {
return '';
}
// Strip 'index.php/' if we're not using path info permalinks.
if ( ! $wp_rewrite->using_index_permalinks() ) {
$url = str_replace( $wp_rewrite->index . '/', '', $url );
}
return $url;
}
/**
* Parses the path segment of a URL to get variables based on WordPress rewrite rules.
*
* The variables returned from this method are not necessarily all relevant for a `WP_Query`, they will still need
* to go through sanitization against the available public query vars from WordPress.
*
* This code is mostly a partial copy of `WP::parse_request()` which is used to parse the current request URL
* into variables in a similar way.
*
* @since 1.15.0
*
* @param string $url URL to parse path vars from.
* @return array Associative array of path vars.
*/
private static function get_url_path_vars( $url ) {
global $wp_rewrite;
$url_path = URL::parse( $url, PHP_URL_PATH );
// Strip potential home URL path segment from URL path.
$home_path = untrailingslashit( URL::parse( home_url( '/' ), PHP_URL_PATH ) );
if ( ! empty( $home_path ) ) {
$url_path = substr( $url_path, strlen( $home_path ) );
}
// Strip leading and trailing slashes.
if ( is_string( $url_path ) ) {
$url_path = trim( $url_path, '/' );
}
// Fetch the rewrite rules.
$rewrite = $wp_rewrite->wp_rewrite_rules();
// Match path against rewrite rules.
$matched_rule = '';
$query = '';
$matches = array();
if ( empty( $url_path ) || $url_path === $wp_rewrite->index ) {
if ( isset( $rewrite['$'] ) ) {
$matched_rule = '$';
$query = $rewrite['$'];
$matches = array( '' );
}
} else {
foreach ( (array) $rewrite as $match => $query ) {
if ( preg_match( "#^$match#", $url_path, $matches ) ) {
if ( $wp_rewrite->use_verbose_page_rules && preg_match( '/pagename=\$matches\[([0-9]+)\]/', $query, $varmatch ) ) {
// This is a verbose page match, let's check to be sure about it.
// We'll rely 100% on WP core functions here.
// phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions
$page = get_page_by_path( $matches[ $varmatch[1] ] );
if ( ! $page ) {
continue;
}
$post_status_obj = get_post_status_object( $page->post_status );
if ( ! $post_status_obj->public && ! $post_status_obj->protected
&& ! $post_status_obj->private && $post_status_obj->exclude_from_search ) {
continue;
}
}
$matched_rule = $match;
break;
}
}
}
// If rewrite rules matched, populate $url_path_vars.
$url_path_vars = array();
if ( $matched_rule ) {
// Trim the query of everything up to the '?'.
$query = preg_replace( '!^.+\?!', '', $query );
// Substitute the substring matches into the query.
$query = addslashes( \WP_MatchesMapRegex::apply( $query, $matches ) );
parse_str( $query, $url_path_vars );
}
return $url_path_vars;
}
/**
* Parses the query segment of a URL to get variables.
*
* The variables returned from this method are not necessarily all relevant for a `WP_Query`, they will still need
* to go through sanitization against the available public query vars from WordPress.
*
* @since 1.15.0
*
* @param string $url URL to parse query vars from.
* @return array Associative array of query vars.
*/
private static function get_url_query_vars( $url ) {
$url_query = URL::parse( $url, PHP_URL_QUERY );
$url_query_vars = array();
if ( $url_query ) {
parse_str( $url_query, $url_query_vars );
}
return $url_query_vars;
}
/**
* Returns arguments for a `WP_Query` instance based on URL path vars and URL query vars.
*
* This method essentially sanitizes the passed vars, allowing only WordPress public query vars to be used as
* actual arguments for `WP_Query`. When combining URL path vars and URL query vars, the latter take precedence.
*
* This code is mostly a partial copy of `WP::parse_request()` which is used to parse the current request URL
* into query arguments in a similar way.
*
* @since 1.15.0
*
* @param array $url_path_vars Associative array as returned from {@see WP_Query_Factory::get_url_path_vars()}.
* @param array $url_query_vars Associative array as returned from {@see WP_Query_Factory::get_url_query_vars()}.
* @return array Associative array of arguments to pass to a `WP_Query` instance.
*/
private static function parse_wp_query_args( array $url_path_vars, array $url_query_vars ) {
global $wp;
// Determine available post type query vars.
$post_type_query_vars = array();
foreach ( get_post_types( array(), 'objects' ) as $post_type => $post_type_obj ) {
if ( is_post_type_viewable( $post_type_obj ) && $post_type_obj->query_var ) {
$post_type_query_vars[ $post_type_obj->query_var ] = $post_type;
}
}
// Depending on whether WordPress already parsed the main request (and thus filtered 'query_vars'), we should
// either manually trigger the filter or not.
if ( did_action( 'parse_request' ) ) {
$public_query_vars = $wp->public_query_vars;
} else {
$public_query_vars = apply_filters( 'query_vars', $wp->public_query_vars );
}
// Populate `WP_Query` arguments.
$query_args = array();
foreach ( $public_query_vars as $wpvar ) {
if ( isset( $url_query_vars[ $wpvar ] ) ) {
$query_args[ $wpvar ] = $url_query_vars[ $wpvar ];
} elseif ( isset( $url_path_vars[ $wpvar ] ) ) {
$query_args[ $wpvar ] = $url_path_vars[ $wpvar ];
}
if ( ! empty( $query_args[ $wpvar ] ) ) {
if ( ! is_array( $query_args[ $wpvar ] ) ) {
$query_args[ $wpvar ] = (string) $query_args[ $wpvar ];
} else {
foreach ( $query_args[ $wpvar ] as $key => $value ) {
if ( is_scalar( $value ) ) {
$query_args[ $wpvar ][ $key ] = (string) $value;
}
}
}
if ( isset( $post_type_query_vars[ $wpvar ] ) ) {
$query_args['post_type'] = $post_type_query_vars[ $wpvar ];
$query_args['name'] = $query_args[ $wpvar ];
}
}
}
// Convert urldecoded spaces back into '+'.
foreach ( get_taxonomies( array(), 'objects' ) as $taxonomy => $taxonomy_obj ) {
if ( $taxonomy_obj->query_var && isset( $query_args[ $taxonomy_obj->query_var ] ) ) {
$query_args[ $taxonomy_obj->query_var ] = str_replace( ' ', '+', $query_args[ $taxonomy_obj->query_var ] );
}
}
// Don't allow non-publicly queryable taxonomies to be queried from the front end.
foreach ( get_taxonomies( array( 'publicly_queryable' => false ), 'objects' ) as $taxonomy => $t ) {
if ( isset( $query_args['taxonomy'] ) && $taxonomy === $query_args['taxonomy'] ) {
unset( $query_args['taxonomy'], $query_args['term'] );
}
}
// Limit publicly queried post_types to those that are 'publicly_queryable'.
if ( isset( $query_args['post_type'] ) ) {
$queryable_post_types = get_post_types( array( 'publicly_queryable' => true ) );
if ( ! is_array( $query_args['post_type'] ) ) {
if ( ! in_array( $query_args['post_type'], $queryable_post_types, true ) ) {
unset( $query_args['post_type'] );
}
} else {
$query_args['post_type'] = array_intersect( $query_args['post_type'], $queryable_post_types );
}
}
// Resolve conflicts between posts with numeric slugs and date archive queries.
$query_args = wp_resolve_numeric_slug_conflicts( $query_args );
// This is a WordPress core filter applied here to allow for the same modifications (e.g. for post formats).
$query_args = apply_filters( 'request', $query_args );
return $query_args;
}
}