Commit inicial - WordPress Análisis de Precios Unitarios

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

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

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,53 @@
<?php
/**
* Class Google\Site_Kit\Core\Authentication\Connected_Proxy_URL
*
* @package Google\Site_Kit\Core\Authentication
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Core\Authentication;
use Google\Site_Kit\Core\Storage\Setting;
/**
* Connected_Proxy_URL class.
*
* @since 1.17.0
* @access private
* @ignore
*/
class Connected_Proxy_URL extends Setting {
/**
* The option_name for this setting.
*/
const OPTION = 'googlesitekit_connected_proxy_url';
/**
* Matches provided URL with the current proxy URL in the settings.
*
* @since 1.17.0
*
* @param string $url URL to match against the current one in the settings.
* @return bool TRUE if URL matches the current one, otherwise FALSE.
*/
public function matches_url( $url ) {
$sanitize = $this->get_sanitize_callback();
$normalized = $sanitize( $url );
return $normalized === $this->get();
}
/**
* Gets the callback for sanitizing the setting's value before saving.
*
* @since 1.17.0
*
* @return callable A sanitizing function.
*/
protected function get_sanitize_callback() {
return 'trailingslashit';
}
}

View File

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

View File

@@ -0,0 +1,43 @@
<?php
/**
* Class Google\Site_Kit\Core\Authentication\Disconnected_Reason
*
* @package Google\Site_Kit\Core\Authentication
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Core\Authentication;
use Google\Site_Kit\Core\Storage\User_Setting;
/**
* Disconnected_Reason class.
*
* @since 1.17.0
* @access private
* @ignore
*/
class Disconnected_Reason extends User_Setting {
/**
* The option_name for this setting.
*/
const OPTION = 'googlesitekit_disconnected_reason';
/**
* Available reasons.
*/
const REASON_CONNECTED_URL_MISMATCH = 'connected_url_mismatch';
/**
* Registers the setting in WordPress.
*
* @since 1.17.0
*/
public function register() {
parent::register();
add_action( 'googlesitekit_authorize_user', array( $this, 'delete' ) );
}
}

View File

@@ -0,0 +1,24 @@
<?php
/**
* Class Google\Site_Kit\Core\Authentication\Exception\Exchange_Site_Code_Exception
*
* @package Google\Site_Kit
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Core\Authentication\Exception;
use Exception;
/**
* Exception thrown when exchanging the site code fails.
*
* @since 1.48.0
* @access private
* @ignore
*/
class Exchange_Site_Code_Exception extends Exception {
}

View File

@@ -0,0 +1,24 @@
<?php
/**
* Class Google\Site_Kit\Core\Authentication\Exception\Google_OAuth_Exception
*
* @package Google\Site_Kit
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Core\Authentication\Exception;
use Exception;
/**
* Exception thrown when a Google OAuth response contains an OAuth error.
*
* @since 1.2.0
* @access private
* @ignore
*/
class Google_OAuth_Exception extends Exception {
}

View File

@@ -0,0 +1,59 @@
<?php
/**
* Class Google\Site_Kit\Core\Authentication\Exception\Google_Proxy_Code_Exception
*
* @package Google\Site_Kit
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Core\Authentication\Exception;
use Exception;
/**
* Exception thrown when Google proxy returns an error accompanied with a temporary access code.
*
* @since 1.0.0
* @since 1.2.0 Renamed to Google_Proxy_Code_Exception.
* @access private
* @ignore
*/
class Google_Proxy_Code_Exception extends Exception {
/**
* Temporary code for an undelegated proxy token.
*
* @since 1.109.0 Explicitly declared; previously, it was dynamically declared.
*
* @var string
*/
protected $access_code;
/**
* Constructor.
*
* @since 1.0.0
*
* @param string $message Optional. The exception message. Default empty string.
* @param integer $code Optional. The numeric exception code. Default 0.
* @param string $access_code Optional. Temporary code for an undelegated proxy token. Default empty string.
*/
public function __construct( $message = '', $code = 0, $access_code = '' ) {
parent::__construct( $message, $code );
$this->access_code = $access_code;
}
/**
* Gets the temporary access code for an undelegated proxy token.
*
* @since 1.0.0
*
* @return string Temporary code.
*/
public function getAccessCode() {
return $this->access_code;
}
}

View File

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

View File

@@ -0,0 +1,22 @@
<?php
/**
* Class Google\Site_Kit\Core\Authentication\Exception\Missing_Verification_Exception
*
* @package Google\Site_Kit
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Core\Authentication\Exception;
/**
* Exception thrown when the a missing verification error is encountered when exchanging the site code.
*
* @since 1.48.0
* @access private
* @ignore
*/
class Missing_Verification_Exception extends Exchange_Site_Code_Exception {
}

View File

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

View File

@@ -0,0 +1,51 @@
<?php
/**
* Class Google\Site_Kit\Core\Authentication\Guards\Site_Connected_Guard
*
* @package Google\Site_Kit
* @copyright 2024 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Core\Authentication\Guards;
use Google\Site_Kit\Core\Authentication\Credentials;
use Google\Site_Kit\Core\Guards\Guard_Interface;
/**
* Class providing guard logic for site connection.
*
* @since 1.133.0
* @access private
* @ignore
*/
class Site_Connected_Guard implements Guard_Interface {
/**
* Credentials instance.
*
* @var Credentials
*/
private Credentials $credentials;
/**
* Constructor.
*
* @since 1.133.0
* @param Credentials $credentials Credentials instance.
*/
public function __construct( Credentials $credentials ) {
$this->credentials = $credentials;
}
/**
* Determines whether the guarded entity can be activated or not.
*
* @since 1.133.0
* @return bool|\WP_Error
*/
public function can_activate() {
return $this->credentials->has();
}
}

View File

@@ -0,0 +1,51 @@
<?php
/**
* Class Google\Site_Kit\Core\Authentication\Guards\Using_Proxy_Connection_Guard
*
* @package Google\Site_Kit
* @copyright 2024 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Core\Authentication\Guards;
use Google\Site_Kit\Core\Authentication\Credentials;
use Google\Site_Kit\Core\Guards\Guard_Interface;
/**
* Class providing guard logic based on proxy connection.
*
* @since 1.133.0
* @access private
* @ignore
*/
class Using_Proxy_Connection_Guard implements Guard_Interface {
/**
* Credentials instance.
*
* @var Credentials
*/
private Credentials $credentials;
/**
* Constructor.
*
* @since 1.133.0
* @param Credentials $credentials Credentials instance.
*/
public function __construct( Credentials $credentials ) {
$this->credentials = $credentials;
}
/**
* Determines whether the guarded entity can be activated or not.
*
* @since 1.133.0
* @return bool|\WP_Error
*/
public function can_activate() {
return $this->credentials->using_proxy();
}
}

View File

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

View File

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

View File

@@ -0,0 +1,28 @@
<?php
/**
* Class Google\Site_Kit\Core\Authentication\Initial_Version
*
* @package Google\Site_Kit
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Core\Authentication;
use Google\Site_Kit\Core\Storage\User_Setting;
/**
* Class representing the initial Site Kit version the user started with.
*
* @since 1.25.0
* @access private
* @ignore
*/
final class Initial_Version extends User_Setting {
/**
* User option key.
*/
const OPTION = 'googlesitekitpersistent_initial_version';
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,28 @@
<?php
/**
* Class Google\Site_Kit\Core\Authentication\Verification_File
*
* @package Google\Site_Kit
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Core\Authentication;
use Google\Site_Kit\Core\Storage\User_Setting;
/**
* Class representing the site verification file token for a user.
*
* @since 1.1.0
* @access private
* @ignore
*/
final class Verification_File extends User_Setting {
/**
* User option key.
*/
const OPTION = 'googlesitekit_site_verification_file';
}

View File

@@ -0,0 +1,28 @@
<?php
/**
* Class Google\Site_Kit\Core\Authentication\Verification_Meta
*
* @package Google\Site_Kit
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Core\Authentication;
use Google\Site_Kit\Core\Storage\User_Setting;
/**
* Class representing the site verification meta tag for a user.
*
* @since 1.1.0
* @access private
* @ignore
*/
final class Verification_Meta extends User_Setting {
/**
* User option key.
*/
const OPTION = 'googlesitekit_site_verification_meta';
}