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

View File

@@ -0,0 +1,40 @@
<?php
/**
* Class Google\Site_Kit\Core\Modules\Executable_Datapoint
*
* @package Google\Site_Kit\Core\Modules
* @copyright 2025 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Core\Modules;
use Google\Site_Kit\Core\REST_API\Data_Request;
/**
* Interface for a datapoint that can be executed.
*
* @since 1.160.0
*/
interface Executable_Datapoint {
/**
* Creates a request object.
*
* @since 1.160.0
*
* @param Data_Request $data Data request object.
*/
public function create_request( Data_Request $data );
/**
* Parses a response.
*
* @since 1.160.0
*
* @param mixed $response Request response.
* @param Data_Request $data Data request object.
*/
public function parse_response( $response, Data_Request $data );
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,27 @@
<?php
/**
* Interface Google\Site_Kit\Core\Modules\Module_With_Activation
*
* @package Google\Site_Kit
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Core\Modules;
/**
* Interface for a module that has additional behavior when activated.
*
* @since 1.36.0
* @access private
* @ignore
*/
interface Module_With_Activation {
/**
* Handles module activation.
*
* @since 1.36.0
*/
public function on_activation();
}

View File

@@ -0,0 +1,42 @@
<?php
/**
* Interface Google\Site_Kit\Core\Modules\Module_With_Assets
*
* @package Google\Site_Kit
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Core\Modules;
use Google\Site_Kit\Core\Assets\Asset;
/**
* Interface for a module that includes assets.
*
* @since 1.7.0
* @access private
* @ignore
*/
interface Module_With_Assets {
/**
* Gets the assets to register for the module.
*
* @since 1.7.0
*
* @return Asset[] List of Asset objects.
*/
public function get_assets();
/**
* Enqueues all assets necessary for the module.
*
* @since 1.7.0
* @since 1.37.0 Added the $asset_context argument.
*
* @param string $asset_context Context for page, see `Asset::CONTEXT_*` constants.
*/
public function enqueue_assets( $asset_context = Asset::CONTEXT_ADMIN_SITEKIT );
}

View File

@@ -0,0 +1,79 @@
<?php
/**
* Trait Google\Site_Kit\Core\Modules\Module_With_Assets_Trait
*
* @package Google\Site_Kit
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Core\Modules;
use Google\Site_Kit\Core\Assets\Asset;
/**
* Trait for a module that includes assets.
*
* @since 1.7.0
* @access private
* @ignore
*/
trait Module_With_Assets_Trait {
/**
* List of the module's Asset objects to register.
*
* @since 1.7.0
* @var array
*/
protected $registerable_assets;
/**
* Gets the assets to register for the module.
*
* @since 1.7.0
*
* @return Asset[] List of Asset objects.
*/
public function get_assets() {
if ( null === $this->registerable_assets ) {
$this->registerable_assets = $this->setup_assets();
}
return $this->registerable_assets;
}
/**
* Enqueues all assets necessary for the module.
*
* This default implementation simply enqueues all assets that the module
* has registered.
*
* @since 1.7.0
* @since 1.37.0 Added the $asset_context argument; only enqueue assets in the correct context.
*
* @param string $asset_context The page context to load this asset, see `Asset::CONTEXT_*` constants.
*/
public function enqueue_assets( $asset_context = Asset::CONTEXT_ADMIN_SITEKIT ) {
$assets = $this->get_assets();
array_walk(
$assets,
function ( Asset $asset, $index, $asset_context ) {
if ( $asset->has_context( $asset_context ) ) {
$asset->enqueue();
}
},
$asset_context
);
}
/**
* Sets up the module's assets to register.
*
* @since 1.7.0
*
* @return Asset[] List of Asset objects.
*/
abstract protected function setup_assets();
}

View File

@@ -0,0 +1,48 @@
<?php
/**
* Interface Google\Site_Kit\Core\Modules\Module_With_Data_Available_State
*
* @package Google\Site_Kit
* @copyright 2023 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Core\Modules;
/**
* Interface for a module that have data available state.
*
* @since 1.96.0
* @access private
* @ignore
*/
interface Module_With_Data_Available_State {
/**
* Checks whether the data is available for the module.
*
* @since 1.96.0
*
* @return bool True if data is available, false otherwise.
*/
public function is_data_available();
/**
* Sets the data available state for the module.
*
* @since 1.96.0
*
* @return bool True on success, false otherwise.
*/
public function set_data_available();
/**
* Resets the data available state for the module.
*
* @since 1.96.0
*
* @return bool True on success, false otherwise.
*/
public function reset_data_available();
}

View File

@@ -0,0 +1,65 @@
<?php
/**
* Trait Google\Site_Kit\Core\Modules\Module_With_Data_Available_State_Trait
*
* @package Google\Site_Kit
* @copyright 2023 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Core\Modules;
/**
* Trait for a module that has data available state.
*
* @since 1.96.0
* @access private
* @ignore
*/
trait Module_With_Data_Available_State_Trait {
/**
* Gets data available transient name of the module.
*
* @since 1.96.0
*
* @return string Data available transient name.
*/
protected function get_data_available_transient_name() {
return "googlesitekit_{$this->slug}_data_available";
}
/**
* Checks whether the data is available for the module.
*
* @since 1.96.0
*
* @return bool True if data is available, false otherwise.
*/
public function is_data_available() {
return (bool) $this->transients->get( $this->get_data_available_transient_name() );
}
/**
* Sets the data available state for the module.
*
* @since 1.96.0
*
* @return bool True on success, false otherwise.
*/
public function set_data_available() {
return $this->transients->set( $this->get_data_available_transient_name(), true );
}
/**
* Resets the data available state for the module.
*
* @since 1.96.0
*
* @return bool True on success, false otherwise.
*/
public function reset_data_available() {
return $this->transients->delete( $this->get_data_available_transient_name() );
}
}

View File

@@ -0,0 +1,27 @@
<?php
/**
* Interface Google\Site_Kit\Core\Modules\Module_With_Deactivation
*
* @package Google\Site_Kit
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Core\Modules;
/**
* Interface for a module that has additional behavior when deactivated.
*
* @since 1.36.0
* @access private
* @ignore
*/
interface Module_With_Deactivation {
/**
* Handles module deactivation.
*
* @since 1.36.0
*/
public function on_deactivation();
}

View File

@@ -0,0 +1,28 @@
<?php
/**
* Class Google\Site_Kit\Core\Modules\Module_With_Debug_Fields
*
* @package Google\Site_Kit\Core\Modules
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Core\Modules;
/**
* Interface Module_With_Debug_Fields
*
* @since 1.5.0
*/
interface Module_With_Debug_Fields {
/**
* Gets an array of debug field definitions.
*
* @since 1.5.0
*
* @return array
*/
public function get_debug_fields();
}

View File

@@ -0,0 +1,31 @@
<?php
/**
* Interface Google\Site_Kit\Core\Modules\Module_With_Inline_Data
*
* @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\Modules;
/**
* Interface for a module that sets inline data.
*
* @since 1.158.0
* @access private
* @ignore
*/
interface Module_With_Inline_Data {
/**
* Gets required inline data for the module.
*
* @since 1.158.0
* @since 1.160.0 Include `$modules_data` parameter.
*
* @param array $modules_data Inline modules data.
* @return array An array of the module's inline data.
*/
public function get_inline_data( $modules_data );
}

View File

@@ -0,0 +1,33 @@
<?php
/**
* Trait Google\Site_Kit\Core\Modules\Module_With_Inline_Data_Trait
*
* @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\Modules;
/**
* Trait for a module that sets inline data.
*
* @since 1.158.0
* @access private
* @ignore
*/
trait Module_With_Inline_Data_Trait {
/**
* Registers the hook to add required scopes.
*
* @since 1.158.0
*/
private function register_inline_data() {
add_filter(
'googlesitekit_inline_modules_data',
array( $this, 'get_inline_data' ),
);
}
}

View File

@@ -0,0 +1,30 @@
<?php
/**
* Interface Google\Site_Kit\Core\Modules\Module_With_Owner
*
* @package Google\Site_Kit
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Core\Modules;
/**
* Interface for a module that includes an owner.
*
* @since 1.16.0
* @access private
* @ignore
*/
interface Module_With_Owner {
/**
* Gets an owner ID for the module.
*
* @since 1.16.0
*
* @return int Owner ID.
*/
public function get_owner_id();
}

View File

@@ -0,0 +1,80 @@
<?php
/**
* Trait Google\Site_Kit\Core\Modules\Module_With_Owner_Trait
*
* @package Google\Site_Kit
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Core\Modules;
use Google\Site_Kit\Core\Authentication\Clients\OAuth_Client;
use Google\Site_Kit\Core\Authentication\Profile;
use Google\Site_Kit\Core\Authentication\Token;
use Google\Site_Kit\Core\Storage\User_Options;
/**
* Trait for a module that includes an owner ID.
*
* @since 1.16.0
* @access private
* @ignore
*/
trait Module_With_Owner_Trait {
/**
* OAuth_Client instance.
*
* @since 1.77.0.
* @var OAuth_Client
*/
protected $owner_oauth_client;
/**
* Gets an owner ID for the module.
*
* @since 1.16.0
*
* @return int Owner ID.
*/
public function get_owner_id() {
if ( ! $this instanceof Module_With_Settings ) {
return 0;
}
$settings = $this->get_settings()->get();
if ( empty( $settings['ownerID'] ) ) {
return 0;
}
return $settings['ownerID'];
}
/**
* Gets the OAuth_Client instance for the module owner.
*
* @since 1.77.0
*
* @return OAuth_Client OAuth_Client instance.
*/
public function get_owner_oauth_client() {
if ( $this->owner_oauth_client instanceof OAuth_Client ) {
return $this->owner_oauth_client;
}
$user_options = new User_Options( $this->context, $this->get_owner_id() );
$this->owner_oauth_client = new OAuth_Client(
$this->context,
$this->options,
$user_options,
$this->authentication->credentials(),
$this->authentication->get_google_proxy(),
new Profile( $user_options ),
new Token( $user_options )
);
return $this->owner_oauth_client;
}
}

View File

@@ -0,0 +1,28 @@
<?php
/**
* Interface Google\Site_Kit\Core\Modules\Module_With_Persistent_Registration
*
* @package Google\Site_Kit
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Core\Modules;
/**
* Interface for a module that requires persistent registration.
*
* @since 1.38.0
* @access private
* @ignore
*/
interface Module_With_Persistent_Registration {
/**
* The registration method that is called even if the module is not activated.
*
* @since 1.38.0
*/
public function register_persistent();
}

View File

@@ -0,0 +1,30 @@
<?php
/**
* Interface Google\Site_Kit\Core\Modules\Module_With_Scopes
*
* @package Google\Site_Kit
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Core\Modules;
/**
* Interface for a module that requires Google OAuth scopes.
*
* @since 1.0.0
* @access private
* @ignore
*/
interface Module_With_Scopes {
/**
* Gets required Google OAuth scopes for the module.
*
* @since 1.0.0
*
* @return array List of Google OAuth scopes.
*/
public function get_scopes();
}

View File

@@ -0,0 +1,35 @@
<?php
/**
* Trait Google\Site_Kit\Core\Modules\Module_With_Scopes_Trait
*
* @package Google\Site_Kit
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Core\Modules;
/**
* Trait for a module that requires Google OAuth scopes.
*
* @since 1.0.0
* @access private
* @ignore
*/
trait Module_With_Scopes_Trait {
/**
* Registers the hook to add required scopes.
*
* @since 1.0.0
*/
private function register_scopes_hook() {
add_filter(
'googlesitekit_auth_scopes',
function ( array $scopes ) {
return array_merge( $scopes, $this->get_scopes() );
}
);
}
}

View File

@@ -0,0 +1,32 @@
<?php
/**
* Interface Google\Site_Kit\Core\Modules\Module_With_Service_Entity
*
* @package Google\Site_Kit
* @copyright 2022 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Core\Modules;
use WP_Error;
/**
* Interface for a module that includes a service entity.
*
* @since 1.70.0
* @access private
* @ignore
*/
interface Module_With_Service_Entity {
/**
* Checks if the current user has access to the current configured service entity.
*
* @since 1.70.0
*
* @return boolean|WP_Error
*/
public function check_service_entity_access();
}

View File

@@ -0,0 +1,23 @@
<?php
/**
* Interface Google\Site_Kit\Core\Modules\Module_With_Settings
*
* @package Google\Site_Kit\Core\Modules
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Core\Modules;
interface Module_With_Settings {
/**
* Gets the module's Setting instance.
*
* @since 1.2.0
*
* @return Module_Settings The Setting instance for the current module.
*/
public function get_settings();
}

View File

@@ -0,0 +1,54 @@
<?php
/**
* Trait Google\Site_Kit\Core\Modules\Module_With_Settings_Trait
*
* @package Google\Site_Kit
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Core\Modules;
/**
* Trait for a module that includes a screen.
*
* @since 1.2.0
* @access private
* @ignore
*/
trait Module_With_Settings_Trait {
/**
* Settings instance.
*
* @since 1.2.0
*
* @var Module_Settings
*/
protected $settings;
/**
* Sets up the module's settings instance.
*
* @since 1.2.0
*
* @return Module_Settings
*/
abstract protected function setup_settings();
/**
* Gets the module's Settings instance.
*
* @since 1.2.0
*
* @return Module_Settings Module_Settings instance.
*/
public function get_settings() {
if ( ! $this->settings instanceof Module_Settings ) {
$this->settings = $this->setup_settings();
}
return $this->settings;
}
}

View File

@@ -0,0 +1,32 @@
<?php
/**
* Trait Google\Site_Kit\Core\Modules\Module_With_Tag
*
* @package Google\Site_Kit\Core\Modules
* @copyright 2024 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Core\Modules;
use Google\Site_Kit\Core\Modules\Tags\Module_Tag_Matchers;
interface Module_With_Tag {
/**
* Registers the tag.
*
* @since 1.119.0
*/
public function register_tag();
/**
* Returns the Module_Tag_Matchers instance.
*
* @since 1.119.0
*
* @return Module_Tag_Matchers Module_Tag_Matchers instance.
*/
public function get_tag_matchers();
}

View File

@@ -0,0 +1,69 @@
<?php
/**
* Trait Google\Site_Kit\Core\Modules\Module_With_Tag_Trait
*
* @package Google\Site_Kit\Core\Modules
* @copyright 2024 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Core\Modules;
use Google\Site_Kit\Core\Modules\Tags\Module_Tag_Matchers;
trait Module_With_Tag_Trait {
/**
* Checks if the module tag is found in the provided content.
*
* @since 1.119.0
*
* @param string $content Content to search for the tags.
* @return bool TRUE if tag is found, FALSE if not.
*/
public function has_placed_tag_in_content( $content ) {
$tag_matchers = $this->get_tag_matchers()->regex_matchers();
$module_name = $this->name;
// Remove 4 from translatable string name of the module if present.
if ( strpos( $module_name, '4' ) !== false ) {
$module_name = trim( str_replace( '4', '', $module_name ) );
}
$search_string = 'Google ' . $module_name . ' snippet added by Site Kit';
// @TODO Replace the comment text around the module name with methods that should expose it.
$search_translatable_string = sprintf(
/* translators: %s: translatable module name */
__( 'Google %s snippet added by Site Kit', 'google-site-kit' ),
$module_name
);
if ( strpos( $content, $search_string ) !== false || strpos( $content, $search_translatable_string ) !== false ) {
return Module_Tag_Matchers::TAG_EXISTS_WITH_COMMENTS;
} else {
foreach ( $tag_matchers as $pattern ) {
if ( preg_match( $pattern, $content ) ) {
return Module_Tag_Matchers::TAG_EXISTS;
}
}
}
return Module_Tag_Matchers::NO_TAG_FOUND;
}
/**
* Gets the URL of the page where a tag for the module would be placed.
*
* For all modules like Analytics, Tag Manager, AdSense, Ads, etc. except for
* Sign in with Google, tags can be detected on the home page. SiwG places its
* snippet on the login page and thus, overrides this method.
*
* @since 1.140.0
*
* @return string The home page URL string where tags are placed for most modules.
*/
public function get_content_url() {
return home_url();
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,138 @@
<?php
/**
* Class Google\Site_Kit\Core\Modules\REST_Dashboard_Sharing_Controller
*
* @package Google\Site_Kit\Core\Modules
* @copyright 2022 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Core\Modules;
use Google\Site_Kit\Core\Permissions\Permissions;
use Google\Site_Kit\Core\REST_API\REST_Route;
use Google\Site_Kit\Core\Util\Collection_Key_Cap_Filter;
use WP_REST_Request;
use WP_REST_Response;
use WP_REST_Server;
/**
* Class for handling dashboard sharing rest routes.
*
* @since 1.75.0
* @access private
* @ignore
*/
class REST_Dashboard_Sharing_Controller {
/**
* Modules instance.
*
* @since 1.75.0
* @var Modules
*/
protected $modules;
/**
* Constructor.
*
* @since 1.75.0
*
* @param Modules $modules Modules instance.
*/
public function __construct( Modules $modules ) {
$this->modules = $modules;
}
/**
* Registers functionality through WordPress hooks.
*
* @since 1.75.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.75.0
*
* @return REST_Route[] List of REST_Route objects.
*/
protected function get_rest_routes() {
$can_manage_options = function () {
return current_user_can( Permissions::MANAGE_OPTIONS );
};
return array(
new REST_Route(
'core/modules/data/sharing-settings',
array(
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => function ( WP_REST_Request $request ) {
$original_module_owners = $this->modules->get_shareable_modules_owners();
$sharing_settings = $this->modules->get_module_sharing_settings();
$new_sharing_settings = array_reduce(
array(
new Collection_Key_Cap_Filter( 'sharedRoles', Permissions::MANAGE_MODULE_SHARING_OPTIONS ),
new Collection_Key_Cap_Filter( 'management', Permissions::DELEGATE_MODULE_SHARING_MANAGEMENT ),
),
function ( $settings, Collection_Key_Cap_Filter $filter ) {
return $filter->filter_key_by_cap( $settings );
},
(array) $request['data']
);
$sharing_settings->merge( $new_sharing_settings );
$new_module_owners = $this->modules->get_shareable_modules_owners();
$changed_module_owners = array_filter(
$new_module_owners,
function ( $new_owner_id, $module_slug ) use ( $original_module_owners ) {
return $new_owner_id !== $original_module_owners[ $module_slug ];
},
ARRAY_FILTER_USE_BOTH
);
return new WP_REST_Response(
array(
'settings' => $sharing_settings->get(),
// Cast array to an object so JSON encoded response is always an object,
// even when the array is empty.
'newOwnerIDs' => (object) $changed_module_owners,
)
);
},
'permission_callback' => $can_manage_options,
'args' => array(
'data' => array(
'type' => 'object',
'required' => true,
),
),
),
array(
'methods' => WP_REST_Server::DELETABLE,
'callback' => function () {
$delete_settings = $this->modules->delete_dashboard_sharing_settings();
return new WP_REST_Response(
$delete_settings
);
},
'permission_callback' => $can_manage_options,
),
)
),
);
}
}

View File

@@ -0,0 +1,811 @@
<?php
/**
* Class Google\Site_Kit\Core\Modules\REST_Modules_Controller
*
* @package Google\Site_Kit\Core\Modules
* @copyright 2022 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
// phpcs:disable Generic.Metrics.CyclomaticComplexity.MaxExceeded
namespace Google\Site_Kit\Core\Modules;
use Google\Site_Kit\Core\Permissions\Permissions;
use Google\Site_Kit\Core\REST_API\REST_Routes;
use Google\Site_Kit\Core\REST_API\REST_Route;
use Google\Site_Kit\Core\REST_API\Exception\Invalid_Datapoint_Exception;
use Google\Site_Kit\Core\Storage\Setting_With_ViewOnly_Keys_Interface;
use WP_REST_Server;
use WP_REST_Request;
use WP_REST_Response;
use WP_Error;
use Exception;
/**
* Class for handling modules rest routes.
*
* @since 1.92.0
* @access private
* @ignore
*/
class REST_Modules_Controller {
const REST_ROUTE_CHECK_ACCESS = 'core/modules/data/check-access';
/**
* Modules instance.
*
* @since 1.92.0
* @var Modules
*/
protected $modules;
/**
* Constructor.
*
* @since 1.92.0
*
* @param Modules $modules Modules instance.
*/
public function __construct( Modules $modules ) {
$this->modules = $modules;
}
/**
* Registers functionality through WordPress hooks.
*
* @since 1.92.0
*/
public function register() {
add_filter(
'googlesitekit_rest_routes',
function ( $routes ) {
return array_merge( $routes, $this->get_rest_routes() );
}
);
add_filter(
'googlesitekit_apifetch_preload_paths',
function ( $paths ) {
$modules_routes = array(
'/' . REST_Routes::REST_ROOT . '/core/modules/data/list',
);
$settings_routes = array_map(
function ( Module $module ) {
if ( $module instanceof Module_With_Settings ) {
return '/' . REST_Routes::REST_ROOT . "/modules/{$module->slug}/data/settings";
}
return null;
},
$this->modules->get_active_modules()
);
return array_merge( $paths, $modules_routes, array_filter( $settings_routes ) );
}
);
}
/**
* Gets the REST schema for a module.
*
* @since 1.92.0
*
* @return array Module REST schema.
*/
private function get_module_schema() {
return array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'module',
'type' => 'object',
'properties' => array(
'slug' => array(
'type' => 'string',
'description' => __( 'Identifier for the module.', 'google-site-kit' ),
'readonly' => true,
),
'name' => array(
'type' => 'string',
'description' => __( 'Name of the module.', 'google-site-kit' ),
'readonly' => true,
),
'description' => array(
'type' => 'string',
'description' => __( 'Description of the module.', 'google-site-kit' ),
'readonly' => true,
),
'homepage' => array(
'type' => 'string',
'description' => __( 'The module homepage.', 'google-site-kit' ),
'format' => 'uri',
'readonly' => true,
),
'internal' => array(
'type' => 'boolean',
'description' => __( 'Whether the module is internal, thus without any UI.', 'google-site-kit' ),
'readonly' => true,
),
'active' => array(
'type' => 'boolean',
'description' => __( 'Whether the module is active.', 'google-site-kit' ),
),
'connected' => array(
'type' => 'boolean',
'description' => __( 'Whether the module setup has been completed.', 'google-site-kit' ),
'readonly' => true,
),
'dependencies' => array(
'type' => 'array',
'description' => __( 'List of slugs of other modules that the module depends on.', 'google-site-kit' ),
'items' => array(
'type' => 'string',
),
'readonly' => true,
),
'dependants' => array(
'type' => 'array',
'description' => __( 'List of slugs of other modules depending on the module.', 'google-site-kit' ),
'items' => array(
'type' => 'string',
),
'readonly' => true,
),
'shareable' => array(
'type' => 'boolean',
'description' => __( 'Whether the module is shareable.', 'google-site-kit' ),
),
'recoverable' => array(
'type' => 'boolean',
'description' => __( 'Whether the module is recoverable.', 'google-site-kit' ),
),
'owner' => array(
'type' => 'object',
'properties' => array(
'id' => array(
'type' => 'integer',
'description' => __( 'Owner ID.', 'google-site-kit' ),
'readonly' => true,
),
'login' => array(
'type' => 'string',
'description' => __( 'Owner login.', 'google-site-kit' ),
'readonly' => true,
),
),
),
),
);
}
/**
* Gets related REST routes.
*
* @since 1.92.0
*
* @return array List of REST_Route objects.
*/
private function get_rest_routes() {
$can_setup = function () {
return current_user_can( Permissions::SETUP );
};
$can_authenticate = function () {
return current_user_can( Permissions::AUTHENTICATE );
};
$can_list_data = function () {
return current_user_can( Permissions::VIEW_SPLASH ) || current_user_can( Permissions::VIEW_DASHBOARD );
};
$can_view_insights = function () {
// This accounts for routes that need to be called before user has completed setup flow.
if ( current_user_can( Permissions::SETUP ) ) {
return true;
}
return current_user_can( Permissions::VIEW_POSTS_INSIGHTS );
};
$can_manage_options = function () {
// This accounts for routes that need to be called before user has completed setup flow.
if ( current_user_can( Permissions::SETUP ) ) {
return true;
}
return current_user_can( Permissions::MANAGE_OPTIONS );
};
$get_module_schema = function () {
return $this->get_module_schema();
};
return array(
new REST_Route(
'core/modules/data/list',
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => function () {
$modules = array_map(
array( $this, 'prepare_module_data_for_response' ),
$this->modules->get_available_modules()
);
return new WP_REST_Response( array_values( $modules ) );
},
'permission_callback' => $can_list_data,
),
),
array(
'schema' => $get_module_schema,
)
),
new REST_Route(
'core/modules/data/activation',
array(
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => function ( WP_REST_Request $request ) {
$data = $request['data'];
$slug = isset( $data['slug'] ) ? $data['slug'] : '';
try {
$this->modules->get_module( $slug );
} catch ( Exception $e ) {
return new WP_Error( 'invalid_module_slug', $e->getMessage() );
}
$modules = $this->modules->get_available_modules();
if ( ! empty( $data['active'] ) ) {
// Prevent activation if one of the dependencies is not active.
$dependency_slugs = $this->modules->get_module_dependencies( $slug );
foreach ( $dependency_slugs as $dependency_slug ) {
if ( ! $this->modules->is_module_active( $dependency_slug ) ) {
/* translators: %s: module name */
return new WP_Error( 'inactive_dependencies', sprintf( __( 'Module cannot be activated because of inactive dependency %s.', 'google-site-kit' ), $modules[ $dependency_slug ]->name ), array( 'status' => 500 ) );
}
}
if ( ! $this->modules->activate_module( $slug ) ) {
return new WP_Error( 'cannot_activate_module', __( 'An internal error occurred while trying to activate the module.', 'google-site-kit' ), array( 'status' => 500 ) );
}
} else {
// Automatically deactivate dependants.
$dependant_slugs = $this->modules->get_module_dependants( $slug );
foreach ( $dependant_slugs as $dependant_slug ) {
if ( $this->modules->is_module_active( $dependant_slug ) ) {
if ( ! $this->modules->deactivate_module( $dependant_slug ) ) {
/* translators: %s: module name */
return new WP_Error( 'cannot_deactivate_dependant', sprintf( __( 'Module cannot be deactivated because deactivation of dependant %s failed.', 'google-site-kit' ), $modules[ $dependant_slug ]->name ), array( 'status' => 500 ) );
}
}
}
if ( ! $this->modules->deactivate_module( $slug ) ) {
return new WP_Error( 'cannot_deactivate_module', __( 'An internal error occurred while trying to deactivate the module.', 'google-site-kit' ), array( 'status' => 500 ) );
}
}
return new WP_REST_Response( array( 'success' => true ) );
},
'permission_callback' => $can_manage_options,
'args' => array(
'data' => array(
'type' => 'object',
'required' => true,
),
),
),
),
array(
'schema' => $get_module_schema,
)
),
new REST_Route(
'core/modules/data/info',
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => function ( WP_REST_Request $request ) {
try {
$module = $this->modules->get_module( $request['slug'] );
} catch ( Exception $e ) {
return new WP_Error( 'invalid_module_slug', $e->getMessage() );
}
return new WP_REST_Response( $this->prepare_module_data_for_response( $module ) );
},
'permission_callback' => $can_authenticate,
'args' => array(
'slug' => array(
'type' => 'string',
'description' => __( 'Identifier for the module.', 'google-site-kit' ),
'sanitize_callback' => 'sanitize_key',
),
),
),
),
array(
'schema' => $get_module_schema,
)
),
new REST_Route(
self::REST_ROUTE_CHECK_ACCESS,
array(
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => function ( WP_REST_Request $request ) {
$data = $request['data'];
$slug = isset( $data['slug'] ) ? $data['slug'] : '';
try {
$module = $this->modules->get_module( $slug );
} catch ( Exception $e ) {
return new WP_Error( 'invalid_module_slug', __( 'Invalid module slug.', 'google-site-kit' ), array( 'status' => 404 ) );
}
if ( ! $module->is_connected() ) {
return new WP_Error( 'module_not_connected', __( 'Module is not connected.', 'google-site-kit' ), array( 'status' => 500 ) );
}
if ( ! $module instanceof Module_With_Service_Entity ) {
if ( $module->is_shareable() ) {
return new WP_REST_Response(
array(
'access' => true,
)
);
}
return new WP_Error( 'invalid_module', __( 'Module access cannot be checked.', 'google-site-kit' ), array( 'status' => 500 ) );
}
$access = $module->check_service_entity_access();
if ( is_wp_error( $access ) ) {
return $access;
}
return new WP_REST_Response(
array(
'access' => $access,
)
);
},
'permission_callback' => $can_setup,
'args' => array(
'slug' => array(
'type' => 'string',
'description' => __( 'Identifier for the module.', 'google-site-kit' ),
'sanitize_callback' => 'sanitize_key',
),
),
),
)
),
new REST_Route(
'modules/(?P<slug>[a-z0-9\-]+)/data/notifications',
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => function ( WP_REST_Request $request ) {
$slug = $request['slug'];
$modules = $this->modules->get_available_modules();
if ( ! isset( $modules[ $slug ] ) ) {
return new WP_Error( 'invalid_module_slug', __( 'Invalid module slug.', 'google-site-kit' ), array( 'status' => 404 ) );
}
$notifications = array();
if ( $this->modules->is_module_active( $slug ) ) {
$notifications = $modules[ $slug ]->get_data( 'notifications' );
if ( is_wp_error( $notifications ) ) {
// Don't consider it an error if the module does not have a 'notifications' datapoint.
if ( Invalid_Datapoint_Exception::WP_ERROR_CODE === $notifications->get_error_code() ) {
$notifications = array();
}
return $notifications;
}
}
return new WP_REST_Response( $notifications );
},
'permission_callback' => $can_authenticate,
),
),
array(
'args' => array(
'slug' => array(
'type' => 'string',
'description' => __( 'Identifier for the module.', 'google-site-kit' ),
'sanitize_callback' => 'sanitize_key',
),
),
)
),
new REST_Route(
'modules/(?P<slug>[a-z0-9\-]+)/data/settings',
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => function ( WP_REST_Request $request ) use ( $can_manage_options ) {
$slug = $request['slug'];
try {
$module = $this->modules->get_module( $slug );
} catch ( Exception $e ) {
return new WP_Error( 'invalid_module_slug', __( 'Invalid module slug.', 'google-site-kit' ), array( 'status' => 404 ) );
}
if ( ! $module instanceof Module_With_Settings ) {
return new WP_Error( 'invalid_module_slug', __( 'Module does not support settings.', 'google-site-kit' ), array( 'status' => 400 ) );
}
$settings = $module->get_settings();
if ( $can_manage_options() ) {
return new WP_REST_Response( $settings->get() );
}
if ( $settings instanceof Setting_With_ViewOnly_Keys_Interface ) {
$view_only_settings = array_intersect_key(
$settings->get(),
array_flip( $settings->get_view_only_keys() )
);
return new WP_REST_Response( $view_only_settings );
}
return new WP_Error( 'no_view_only_settings' );
},
'permission_callback' => $can_list_data,
),
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => function ( WP_REST_Request $request ) {
$slug = $request['slug'];
try {
$module = $this->modules->get_module( $slug );
} catch ( Exception $e ) {
return new WP_Error( 'invalid_module_slug', __( 'Invalid module slug.', 'google-site-kit' ), array( 'status' => 404 ) );
}
if ( ! $module instanceof Module_With_Settings ) {
return new WP_Error( 'invalid_module_slug', __( 'Module does not support settings.', 'google-site-kit' ), array( 'status' => 400 ) );
}
do_action( 'googlesitekit_pre_save_settings_' . $slug );
$module->get_settings()->merge( (array) $request['data'] );
do_action( 'googlesitekit_save_settings_' . $slug );
return new WP_REST_Response( $module->get_settings()->get() );
},
'permission_callback' => $can_manage_options,
'args' => array(
'data' => array(
'type' => 'object',
'description' => __( 'Settings to set.', 'google-site-kit' ),
'validate_callback' => function ( $value ) {
return is_array( $value );
},
),
),
),
),
array(
'args' => array(
'slug' => array(
'type' => 'string',
'description' => __( 'Identifier for the module.', 'google-site-kit' ),
'sanitize_callback' => 'sanitize_key',
),
),
)
),
new REST_Route(
'modules/(?P<slug>[a-z0-9\-]+)/data/data-available',
array(
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => function ( WP_REST_Request $request ) {
$slug = $request['slug'];
try {
$module = $this->modules->get_module( $slug );
} catch ( Exception $e ) {
return new WP_Error( 'invalid_module_slug', __( 'Invalid module slug.', 'google-site-kit' ), array( 'status' => 404 ) );
}
if ( ! $this->modules->is_module_connected( $slug ) ) {
return new WP_Error( 'module_not_connected', __( 'Module is not connected.', 'google-site-kit' ), array( 'status' => 500 ) );
}
if ( ! $module instanceof Module_With_Data_Available_State ) {
return new WP_Error( 'invalid_module_slug', __( 'Module does not support setting data available state.', 'google-site-kit' ), array( 'status' => 500 ) );
}
return new WP_REST_Response( $module->set_data_available() );
},
'permission_callback' => $can_list_data,
),
),
array(
'args' => array(
'slug' => array(
'type' => 'string',
'description' => __( 'Identifier for the module.', 'google-site-kit' ),
'sanitize_callback' => 'sanitize_key',
),
),
)
),
new REST_Route(
'modules/(?P<slug>[a-z0-9\-]+)/data/(?P<datapoint>[a-z\-]+)',
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => function ( WP_REST_Request $request ) {
$slug = $request['slug'];
try {
$module = $this->modules->get_module( $slug );
} catch ( Exception $e ) {
return new WP_Error( 'invalid_module_slug', __( 'Invalid module slug.', 'google-site-kit' ), array( 'status' => 404 ) );
}
if ( ! $this->modules->is_module_active( $slug ) ) {
return new WP_Error( 'module_not_active', __( 'Module must be active to request data.', 'google-site-kit' ), array( 'status' => 403 ) );
}
$data = $module->get_data( $request['datapoint'], $request->get_params() );
if ( is_wp_error( $data ) ) {
return $data;
}
return new WP_REST_Response( $data );
},
'permission_callback' => $can_view_insights,
),
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => function ( WP_REST_Request $request ) {
$slug = $request['slug'];
try {
$module = $this->modules->get_module( $slug );
} catch ( Exception $e ) {
return new WP_Error( 'invalid_module_slug', __( 'Invalid module slug.', 'google-site-kit' ), array( 'status' => 404 ) );
}
if ( ! $this->modules->is_module_active( $slug ) ) {
return new WP_Error( 'module_not_active', __( 'Module must be active to request data.', 'google-site-kit' ), array( 'status' => 403 ) );
}
$data = isset( $request['data'] ) ? (array) $request['data'] : array();
$data = $module->set_data( $request['datapoint'], $data );
if ( is_wp_error( $data ) ) {
return $data;
}
return new WP_REST_Response( $data );
},
'permission_callback' => $can_manage_options,
'args' => array(
'data' => array(
'type' => 'object',
'description' => __( 'Data to set.', 'google-site-kit' ),
'validate_callback' => function ( $value ) {
return is_array( $value );
},
),
),
),
),
array(
'args' => array(
'slug' => array(
'type' => 'string',
'description' => __( 'Identifier for the module.', 'google-site-kit' ),
'sanitize_callback' => 'sanitize_key',
),
'datapoint' => array(
'type' => 'string',
'description' => __( 'Module data point to address.', 'google-site-kit' ),
'sanitize_callback' => 'sanitize_key',
),
),
)
),
new REST_Route(
'core/modules/data/recover-modules',
array(
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => function ( WP_REST_Request $request ) {
$data = $request['data'];
$slugs = isset( $data['slugs'] ) ? $data['slugs'] : array();
if ( ! is_array( $slugs ) || empty( $slugs ) ) {
return new WP_Error(
'invalid_param',
__( 'Request parameter slugs is not valid.', 'google-site-kit' ),
array( 'status' => 400 )
);
}
$response = array(
'success' => array(),
'error' => array(),
);
foreach ( $slugs as $slug ) {
try {
$module = $this->modules->get_module( $slug );
} catch ( Exception $e ) {
$response = $this->handle_module_recovery_error(
$slug,
$response,
new WP_Error(
'invalid_module_slug',
$e->getMessage(),
array( 'status' => 404 )
)
);
continue;
}
if ( ! $module->is_shareable() ) {
$response = $this->handle_module_recovery_error(
$slug,
$response,
new WP_Error(
'module_not_shareable',
__( 'Module is not shareable.', 'google-site-kit' ),
array( 'status' => 404 )
)
);
continue;
}
if ( ! $this->modules->is_module_recoverable( $module ) ) {
$response = $this->handle_module_recovery_error(
$slug,
$response,
new WP_Error(
'module_not_recoverable',
__( 'Module is not recoverable.', 'google-site-kit' ),
array( 'status' => 403 )
)
);
continue;
}
$check_access_endpoint = '/' . REST_Routes::REST_ROOT . '/' . self::REST_ROUTE_CHECK_ACCESS;
$check_access_request = new WP_REST_Request( 'POST', $check_access_endpoint );
$check_access_request->set_body_params(
array(
'data' => array(
'slug' => $slug,
),
)
);
$check_access_response = rest_do_request( $check_access_request );
if ( is_wp_error( $check_access_response ) ) {
$response = $this->handle_module_recovery_error(
$slug,
$response,
$check_access_response
);
continue;
}
$access = isset( $check_access_response->data['access'] ) ? $check_access_response->data['access'] : false;
if ( ! $access ) {
$response = $this->handle_module_recovery_error(
$slug,
$response,
new WP_Error(
'module_not_accessible',
__( 'Module is not accessible by current user.', 'google-site-kit' ),
array( 'status' => 403 )
)
);
continue;
}
// Update the module's ownerID to the ID of the user making the request.
$module_setting_updates = array(
'ownerID' => get_current_user_id(),
);
$recovered_module = $module->get_settings()->merge( $module_setting_updates );
if ( $recovered_module ) {
$response['success'][ $slug ] = true;
}
}
// Cast error array to an object so JSON encoded response is
// always an object, even when the error array is empty.
if ( ! $response['error'] ) {
$response['error'] = (object) array();
}
return new WP_REST_Response( $response );
},
'permission_callback' => $can_setup,
),
),
array(
'schema' => $get_module_schema,
)
),
);
}
/**
* Prepares module data for a REST response according to the schema.
*
* @since 1.92.0
*
* @param Module $module Module instance.
* @return array Module REST response data.
*/
private function prepare_module_data_for_response( Module $module ) {
$module_data = array(
'slug' => $module->slug,
'name' => $module->name,
'description' => $module->description,
'homepage' => $module->homepage,
'internal' => $module->internal,
'order' => $module->order,
'forceActive' => $module->force_active,
'recoverable' => $module->is_recoverable(),
'shareable' => $this->modules->is_module_shareable( $module->slug ),
'active' => $this->modules->is_module_active( $module->slug ),
'connected' => $this->modules->is_module_connected( $module->slug ),
'dependencies' => $this->modules->get_module_dependencies( $module->slug ),
'dependants' => $this->modules->get_module_dependants( $module->slug ),
'owner' => null,
);
if ( current_user_can( 'list_users' ) && $module instanceof Module_With_Owner ) {
$owner_id = $module->get_owner_id();
if ( $owner_id ) {
$module_data['owner'] = array(
'id' => $owner_id,
'login' => get_the_author_meta( 'user_login', $owner_id ),
);
}
}
return $module_data;
}
/**
* Prepares error data to pass with WP_REST_Response.
*
* @since 1.92.0
*
* @param WP_Error $error Error (WP_Error) to prepare.
*
* @return array Formatted error response suitable for the client.
*/
protected function prepare_error_response( $error ) {
return array(
'code' => $error->get_error_code(),
'message' => $error->get_error_message(),
'data' => $error->get_error_data(),
);
}
/**
* Updates response with error encounted during module recovery.
*
* @since 1.92.0
*
* @param string $slug The module slug.
* @param array $response The existing response.
* @param WP_Error $error The error encountered.
*
* @return array The updated response with error included.
*/
protected function handle_module_recovery_error( $slug, $response, $error ) {
$response['success'][ $slug ] = false;
$response['error'][ $slug ] = $this->prepare_error_response(
$error
);
return $response;
}
}

View File

@@ -0,0 +1,32 @@
<?php
/**
* Class Google\Site_Kit\Core\Modules\Shareable_Datapoint
*
* @package Google\Site_Kit\Core\Modules
* @copyright 2025 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Core\Modules;
/**
* Class representing a shareable datapoint definition.
*
* @since 1.160.0
* @access private
* @ignore
*/
class Shareable_Datapoint extends Datapoint {
/**
* Checks if the datapoint is shareable.
*
* @since 1.160.0
*
* @return bool
*/
public function is_shareable() {
return true;
}
}

View File

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

View File

@@ -0,0 +1,53 @@
<?php
/**
* Class Google\Site_Kit\Core\Modules\Tags\Module_Tag
*
* @package Google\Site_Kit\Core\Tags
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Core\Modules\Tags;
use Google\Site_Kit\Core\Tags\Tag;
/**
* Base class for a module tag.
*
* @since 1.24.0
* @access private
* @ignore
*/
abstract class Module_Tag extends Tag {
/**
* Module slug.
*
* @since 1.24.0
* @since 1.109.0 Renamed from slug to module_slug.
*
* @var string
*/
protected $module_slug;
/**
* Constructor.
*
* @since 1.24.0
*
* @param string $tag_id Tag ID.
* @param string $module_slug Module slug.
*/
public function __construct( $tag_id, $module_slug ) {
parent::__construct( $tag_id );
$this->module_slug = $module_slug;
}
/**
* Outputs the tag.
*
* @since 1.24.0
*/
abstract protected function render();
}

View File

@@ -0,0 +1,53 @@
<?php
/**
* Class Google\Site_Kit\Core\Modules\Tags\Module_Tag_Guard
*
* @package Google\Site_Kit\Core\Tags
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Core\Modules\Tags;
use Google\Site_Kit\Core\Guards\Guard_Interface;
use Google\Site_Kit\Core\Modules\Module_Settings;
use WP_Error;
/**
* Base class for a module tag guard.
*
* @since 1.24.0
* @access private
* @ignore
*/
abstract class Module_Tag_Guard implements Guard_Interface {
/**
* Module settings.
*
* @since 1.24.0
* @var Module_Settings
*/
protected $settings;
/**
* Constructor.
*
* @since 1.24.0
*
* @param Module_Settings $settings Module settings.
*/
public function __construct( Module_Settings $settings ) {
$this->settings = $settings;
}
/**
* Determines whether the guarded tag can be activated or not.
*
* @since 1.24.0
*
* @return bool|WP_Error TRUE if guarded tag can be activated, otherwise FALSE or an error.
*/
abstract public function can_activate();
}

View File

@@ -0,0 +1,36 @@
<?php
/**
* Class Google\Site_Kit\Core\Modules\Tags\Module_Tag_Matchers
*
* @package Google\Site_Kit\Core\Modules\Tags
* @copyright 2024 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Core\Modules\Tags;
use Google\Site_Kit\Core\Tags\Tag_Matchers_Interface;
/**
* Base class for Tag matchers.
*
* @since 1.119.0
* @access private
* @ignore
*/
abstract class Module_Tag_Matchers implements Tag_Matchers_Interface {
const NO_TAG_FOUND = 0;
const TAG_EXISTS = 1;
const TAG_EXISTS_WITH_COMMENTS = 2;
/**
* Holds array of regex tag matchers.
*
* @since 1.119.0
*
* @return array Array of regex matchers.
*/
abstract public function regex_matchers();
}

View File

@@ -0,0 +1,129 @@
<?php
/**
* Class Google\Site_Kit\Core\Modules\Tags\Module_Web_Tag
*
* @package Google\Site_Kit\Core\Modules\Tags
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Core\Modules\Tags;
use Google\Site_Kit\Core\Tags\Blockable_Tag_Interface;
/**
* Base class for Web tag.
*
* @since 1.24.0
* @access private
* @ignore
*/
abstract class Module_Web_Tag extends Module_Tag implements Blockable_Tag_Interface {
/**
* Checks whether or not the tag should be blocked from rendering.
*
* @since 1.24.0
*
* @return bool TRUE if the tag should be blocked, otherwise FALSE.
*/
public function is_tag_blocked() {
/**
* Filters whether or not the tag should be blocked from rendering.
*
* @since 1.24.0
*
* @param bool $blocked Whether or not the tag output is suppressed. Default: false.
*/
return (bool) apply_filters( "googlesitekit_{$this->module_slug}_tag_blocked", false );
}
/**
* Gets the HTML attributes for a script tag that may potentially require user consent before loading.
*
* @since 1.24.0
*
* @return string HTML attributes to add if the tag requires consent to load, or an empty string.
*/
public function get_tag_blocked_on_consent_attribute() {
if ( $this->is_tag_blocked_on_consent() ) {
return ' type="text/plain" data-block-on-consent';
}
return '';
}
/**
* Gets the array of HTML attributes for a script tag that may potentially require user consent before loading.
*
* @since 1.41.0
*
* @return array containing HTML attributes to add if the tag requires consent to load, or an empty array.
*/
public function get_tag_blocked_on_consent_attribute_array() {
if ( $this->is_tag_blocked_on_consent() ) {
return array(
'type' => 'text/plain',
'data-block-on-consent' => true,
);
}
return array();
}
/**
* Check if the tag is set to be manually blocked for consent.
*
* @since 1.122.0
*
* @return bool
*/
protected function is_tag_blocked_on_consent() {
$deprecated_args = (array) $this->get_tag_blocked_on_consent_deprecated_args();
/**
* Filters whether the tag requires user consent before loading.
*
* @since 1.24.0
*
* @param bool $blocked Whether or not the tag requires user consent to load. Default: false.
*/
if ( $deprecated_args ) {
return (bool) apply_filters_deprecated(
"googlesitekit_{$this->module_slug}_tag_block_on_consent",
array( false ),
...$deprecated_args
);
}
return (bool) apply_filters( "googlesitekit_{$this->module_slug}_tag_block_on_consent", false );
}
/**
* Get contextual arguments for apply_filters_deprecated if block_on_consent is deprecated.
*
* @since 1.122.0
*
* @return array
*/
protected function get_tag_blocked_on_consent_deprecated_args() {
return array();
}
/**
* Fires the "googlesitekit_{module_slug}_init_tag" action to let 3rd party plugins to perform required setup.
*
* @since 1.24.0
*/
protected function do_init_tag_action() {
/**
* Fires when the tag has been initialized which means that the tag will be rendered in the current request.
*
* @since 1.24.0
*
* @param string $tag_id Tag ID.
*/
do_action( "googlesitekit_{$this->module_slug}_init_tag", $this->tag_id );
}
}