diff --git a/.babelrc b/.babelrc
new file mode 100644
index 00000000000..a578ae4fbbe
--- /dev/null
+++ b/.babelrc
@@ -0,0 +1,19 @@
+{
+ "presets": [
+ [ "@babel/env", {
+ "useBuiltIns": "entry",
+ "corejs": 2
+ } ],
+ "@babel/preset-react"
+ ],
+ "plugins": [
+ "@babel/plugin-syntax-dynamic-import",
+ "@babel/plugin-transform-runtime",
+ [
+ "@wordpress/babel-plugin-makepot",
+ {
+ "output": "languages/translation.pot"
+ }
+ ]
+ ]
+}
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 00000000000..d2e1a374487
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,16 @@
+root = true
+
+[*.php]
+charset = utf-8
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+indent_style = tab
+indent_size = 4
+
+[{*.json,*.yml}]
+indent_style = space
+indent_size = 2
+
+[*.txt,wp-config-sample.php]
+end_of_line = crlf
diff --git a/.eslintrc.json b/.eslintrc.json
new file mode 100644
index 00000000000..eab040db0a5
--- /dev/null
+++ b/.eslintrc.json
@@ -0,0 +1,48 @@
+{
+ "extends": [
+ "eslint:recommended",
+ "plugin:react/recommended",
+ "wordpress"
+ ],
+ "parser": "babel-eslint",
+ "env": {
+ "browser": true,
+ "es6": true
+ },
+ "rules": {
+ "yoda": [ 1 ],
+ "comma-dangle": [ 0 ],
+ "indent": [ 1, "tab", { "SwitchCase": 2 } ],
+ "linebreak-style": [ 1, "unix" ],
+ "quotes": [ 1, "single" ],
+ "space-in-parens": [ 1, "always" ],
+ "object-curly-spacing": [ 1, "always" ],
+ "no-console": [ 1 ],
+ "no-alert": [ 1 ],
+ "camelcase": [ 1 ],
+ "no-debugger": [ 1 ],
+ "no-extra-boolean-cast": [ 1 ],
+ "react/react-in-jsx-scope": [ 0 ],
+ "react/jsx-curly-spacing": [ 2, { "when": "always", "children": true } ],
+ "no-unused-vars": [ "error", { "varsIgnorePattern": "React" } ],
+ "react/prop-types": [ 0 ],
+ "function-paren-newline": [ "error", "consistent" ],
+ "array-bracket-spacing": [ "error", "always" ]
+ },
+ "settings": {
+ "react": {
+ "version": "16.2.0"
+ }
+ },
+ "globals": {
+ "React": true,
+ "googlesitekit": true,
+ "googlesitekitAdminbar": true,
+ "googlesitekitDashboard": true,
+ "googlesitekitSettings": true,
+ "lodash": true,
+ "googlesitekitCurrentModule": true,
+ "gtag": true,
+ "process": true
+ }
+}
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 00000000000..64017080cda
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,31 @@
+---
+name: Bug report
+about: Create a report to help us improve
+
+---
+
+**Describe the bug**
+A clear and concise description of what the bug is.
+
+**To Reproduce**
+Steps to reproduce the behavior:
+1. Go to '...'
+2. Click on '....'
+3. Scroll down to '....'
+4. See error
+
+**Expected behavior**
+A clear and concise description of what you expected to happen.
+
+**Screenshots**
+If applicable, add screenshots to help explain your problem.
+
+**System Information (please complete the following information):**
+ - PHP Version:
+ - OS: [e.g. iOS]
+ - Browser [e.g. chrome, safari]
+ - Plugin Version [e.g. 22]
+ - Device: [e.g. iPhone6]
+
+**Additional context**
+- Please add any additional information about the bug.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 00000000000..066b2d920a2
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,17 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+
+---
+
+**Is your feature request related to a problem? Please describe.**
+A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+
+**Describe the solution you'd like**
+A clear and concise description of what you want to happen.
+
+**Describe alternatives you've considered**
+A clear and concise description of any alternative solutions or features you've considered.
+
+**Additional context**
+Add any other context or screenshots about the feature request here.
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 00000000000..3680f326f32
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,20 @@
+## Summary
+
+
+This PR can be summarized in the following changelog entry:
+
+*
+
+
+Addresses issue #
+
+## Relevant technical choices
+
+
+## Checklist:
+- [ ] My code is tested and passes existing unit tests.
+- [ ] My code has an appropriate set of unit tests which all pass.
+- [ ] My code is backward-compatible with WordPress 4.7 and PHP 5.4.
+- [ ] My code follows the [WordPress](https://make.wordpress.org/core/handbook/best-practices/coding-standards/) coding standards.
+- [ ] My code has proper inline documentation.
+- [ ] I have signed the Contributor License Agreement (see
+ { description } +
+ } ++ { content } +
++ { content } +
+ { linkText } +{ description }
+ } ++ { subtitle } +
+ } ++ { __( 'Note: ', 'google-site-kit' ) }{ dependentModules } +
+ } + +{ __( 'Loading chart...', 'google-site-kit' ) }
} ++ { description } +
+ } + { ctaLabel && + + { ctaLabel } + + } +
+ { description }
+ { learnMoreLabel &&
+
+ { __( 'Site Kit is connected', 'google-site-kit' ) } + + + { __( 'Connected', 'google-site-kit' ) } + + +
++ { __( 'Client ID', 'google-site-kit' ) } +
++ { __( 'Client Secret', 'google-site-kit' ) } +
++ { __( 'API Key', 'google-site-kit' ) } +
++ { __( 'Project ID', 'google-site-kit' ) } +
+{ description }
+ } ++ { description } +
+ ++ + { + ! blockedByParentModule ? + sprintf( __( 'Set up %s', 'google-site-kit' ), name ) : + sprintf( __( 'Setup Analytics to gain access to %s', 'google-site-kit' ), name ) + } + +
+ } ++ { title } +
++ { __( 'Please sign into your Google account to begin.', 'google-site-kit' ) } +
+ { + needReauthenticate && ++ { __( 'You did not grant access to one or more of the requested scopes. Please grant all scopes that you are prompted for.', 'google-site-kit' ) } +
+ } ++ + { resetAndRestart && + + { __( 'Back', 'google-site-kit' ) } + + } +
++ { __( 'To complete the setup, it will help if you\'re familiar with Google Cloud Platform and OAuth.', 'google-site-kit' ) } +
++ { __( 'If that sounds like you, get started by creating a client configuration on ', 'google-site-kit' ) } + + { externalCredentialsURLLabel } + +
++ { __( 'Once you paste it below, it will be valid for all other plugin users.', 'google-site-kit' ) } +
+ { + errorMsg && errorMsg.length && ++ { errorMsg } +
+ } + ++ { __( 'You successfully completed the Site Kit setup and connected Search Console. Check the dashboard for more services to connect.', 'google-site-kit' ) } +
++ { __( 'Please sign into your Google account to begin.', 'google-site-kit' ) } +
+ +{ __( 'Your Search Console is set up with Site Kit.', 'google-site-kit' ) }
+ { /* TODO This needs a continue button or redirect. */ } +{ __( 'We’re locating your Search Console account.', 'google-site-kit' ) }
+{ __( 'We will connect Search Console. No account? Don’t worry, we will create one here.', 'google-site-kit' ) }
} + + { + errorMsg && 0 < errorMsg.length && ++ { errorMsg } +
+ } + + { isAuthenticated && shouldSetup && this.renderForm() } + ++ { __( 'Connect Service', 'google-site-kit' ) } +
+ { setupModule } +{ loadingMsg }
+ } +{ __( 'Congratulations, your site has been verified!', 'google-site-kit' ) }
+{ __( 'We will need to verify your URL for Site Kit.', 'google-site-kit' ) }
+ + { + errorMsg && 0 < errorMsg.length && ++ { errorMsg } +
+ } + + { isAuthenticated && this.renderForm() } + ++ { subHeaderList[ status ] } +
+ ); + + const actionList = 'incomplete' === status && ( +
+
{ statusMessage }
+ { profile && ++ { + picture && + + } + + { email } + +
+ } + { + ( 'account-connected' === accountStatus ) && +{ __( 'Error:', 'google-site-kit' ) } { message }
++ { footerText } { footerCTA && { footerCTA } } { footerAppendedText } +
+ } +{ __( 'Error:', 'google-site-kit' ) } { message }
+{ __( 'Currently there is no Analytics snippet placed on your site, so no stats are being gathered. Would you like Site Kit to insert the Analytics snippet? You can change this setting later.', 'google-site-kit' ) }
+{ __( 'Do you want to remove the Analytics snippet inserted by Site Kit?', 'google-site-kit' ) }
+ } +{ sprintf( __( 'If the code snippet is removed, you will no longer be able to gather Analytics insights about your site.', 'google-site-kit' ), existingTag ) }
+ } +{ sprintf( __( 'Placing two tags at the same time is not recommended.', 'google-site-kit' ), existingTag ) }
+ } ++ { ampClientIdOptIn ? + __( 'Sessions will be combined across AMP/non-AMP pages.', 'google-site-kit' ) + ' ' : + __( 'Sessions will be tracked separately between AMP/non-AMP pages.', 'google-site-kit' ) + ' ' + } + { __( 'Learn more', 'google-site-kit' ) } +
++ { __( 'Account', 'google-site-kit' ) } +
++ { __( 'Property', 'google-site-kit' ) } +
++ { __( 'View', 'google-site-kit' ) } +
++ { __( 'Analytics Code Snippet', 'google-site-kit' ) } +
+{ __( 'Please select the account information below. You can change this view later in your settings.', 'google-site-kit' ) }
+ } +{ __( 'Error:', 'google-site-kit' ) } { message }
+{ message }
+{ sprintf( __( 'An existing analytics tag was found on your site with the id %s. If later on you decide to replace this tag, Site Kit can place the new tag for you. Make sure you remove the old tag first.', 'google-site-kit' ), existingTag ) }
+ } + + { this.renderErrorOrNotice() } + + { this.renderForm() } +{ __( 'You are using auto insert snippet with Tag Manager', 'google-site-kit' ) }
+{ __( 'Click here', 'google-site-kit' ) } { __( 'for how to implement Optimize tag through your Tag Manager', 'google-site-kit' ) }
+{ __( 'You disabled analytics auto insert snippet. If You are using Google Analytics code snippet, add the code below:', 'google-site-kit' ) }
++ ga("require", "{ optimizeId ? optimizeId : 'GTM-XXXXXXX' }"); ++
{ __( 'Click here', 'google-site-kit' ) } { __( 'for how to implement Optimize tag in Google Analytics Code Snippet', 'google-site-kit' ) }
+{ __( 'Please input your AMP experiment settings in JSON format below.', 'google-site-kit' ) } { __( 'Learn More.', 'google-site-kit' ) }
+{ __( 'Error: AMP experiment settings are not in a valid JSON format.', 'google-site-kit' ) }
+ } +{ __( 'Please copy and paste your Optimize ID to complete your setup.', 'google-site-kit' ) } { __( 'You can locate this here.', 'google-site-kit' ) }
+ + { + errorCode && 0 < errorMsg.length && ++ { __( 'Error:', 'google-site-kit' ) } { errorMsg } +
+ } + +{ __( 'Error: Not a valid Optimize ID.', 'google-site-kit' ) }
+ } + + { + this.renderAMPSnippet() + } + + { + this.renderInstructionInfo() + } +{ __( 'PageSpeed Insights is preparing data for your home page…', 'google-site-kit' ) }
++ { __( 'API connected.', 'google-site-kit' ) } + { __( 'Edit key', 'google-site-kit' ) } + +
: ++ { __( 'Please generate an API key on ', 'google-site-kit' ) } + + { externalAPIKeyURLLabel } + +
+{ __( 'Enter it below to complete the setup for PageSpeed Insights.', 'google-site-kit' ) }
++ { __( 'Account', 'google-site-kit' ) } +
++ { __( 'Container ID', 'google-site-kit' ) } +
+{ __( 'Please select your Tag Manager account and container below, the snippet will be inserted automatically into your site.', 'google-site-kit' ) }
+{ __( 'Error:', 'google-site-kit' ) } { message }
+' . esc_html( $message ) . '
'; + }, + 'type' => Notice::TYPE_ERROR, + 'active_callback' => function() { + if ( isset( $_GET['notification'] ) && 'authentication_success' === $_GET['notification'] && ! empty( $_GET['error'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.NoNonceVerification + return true; + } + + return (bool) $this->user_options->get( Clients\OAuth_Client::OPTION_ERROR_CODE ); + }, + ) + ); + } + + /** + * Checks if the current user needs to reauthenticate (e.g. because of new requested scopes). + * + * @since 1.0.0 + * + * @return bool TRUE if need reauthenticate and FALSE otherwise. + */ + private function need_reauthenticate() { + $auth_client = $this->get_oauth_client(); + + $access_token = $auth_client->get_access_token(); + if ( empty( $access_token ) ) { + return false; + } + + $granted_scopes = $auth_client->get_granted_scopes(); + $required_scopes = $auth_client->get_required_scopes(); + + $required_and_granted_scopes = array_intersect( $granted_scopes, $required_scopes ); + + return count( $required_and_granted_scopes ) < count( $required_scopes ); + } +} diff --git a/includes/Core/Authentication/Clients/API_Key_Client.php b/includes/Core/Authentication/Clients/API_Key_Client.php new file mode 100644 index 00000000000..72eb1affc9d --- /dev/null +++ b/includes/Core/Authentication/Clients/API_Key_Client.php @@ -0,0 +1,154 @@ +context = $context; + + if ( ! $options ) { + $options = new Options( $this->context ); + } + $this->options = $options; + + if ( ! $api_key ) { + $api_key = new API_Key( $this->options ); + } + $this->api_key = $api_key; + } + + /** + * Gets the Google client object. + * + * @since 1.0.0 + * + * @return Google_Client Google client object. + */ + public function get_client() { + if ( $this->google_client instanceof Google_Client ) { + return $this->google_client; + } + + $this->google_client = new Google_Client(); + + $api_key = $this->get_api_key(); + if ( ! empty( $api_key ) ) { + $this->google_client->setDeveloperKey( $api_key ); + } + + return $this->google_client; + } + + /** + * Gets the API key. + * + * @since 1.0.0 + * + * @return string|bool API key if it exists, false otherwise. + */ + public function get_api_key() { + /** + * Filters the API key that Site Kit should use. + * + * @since 1.0.0 + * + * @param string $api_key API key, empty by default as it will use the corresponding option. + */ + $api_key = trim( apply_filters( 'googlesitekit_api_key', '' ) ); + + if ( ! empty( $api_key ) ) { + return $api_key; + } + + if ( ! $this->api_key->has() ) { + return false; + } + + return $this->api_key->get(); + } + + /** + * Sets the API key. + * + * @since 1.0.0 + * + * @param string $api_key New API key. + * @return bool True on success, false on failure. + */ + public function set_api_key( $api_key ) { + // Bail early if nothing change. + if ( $this->get_api_key() === $api_key ) { + return true; + } + + $this->get_client()->setDeveloperKey( $api_key ); + + return $this->api_key->set( $api_key ); + } +} diff --git a/includes/Core/Authentication/Clients/OAuth_Client.php b/includes/Core/Authentication/Clients/OAuth_Client.php new file mode 100644 index 00000000000..53ceec87fe4 --- /dev/null +++ b/includes/Core/Authentication/Clients/OAuth_Client.php @@ -0,0 +1,633 @@ +context = $context; + + if ( ! $options ) { + $options = new Options( $this->context ); + } + $this->options = $options; + + if ( ! $user_options ) { + $user_options = new User_Options( $this->context ); + } + $this->user_options = $user_options; + + $this->encrypted_options = new Encrypted_Options( $this->options ); + $this->encrypted_user_options = new Encrypted_User_Options( $this->user_options ); + + if ( ! $credentials ) { + $credentials = new Credentials( $this->options ); + } + $this->credentials = $credentials; + } + + /** + * Gets the Google client object. + * + * @since 1.0.0 + * + * @return Google_Client Google client object. + */ + public function get_client() { + if ( $this->google_client instanceof Google_Client ) { + return $this->google_client; + } + + $this->google_client = new Google_Client(); + + // Return unconfigured client if credentials not yet set. + $client_credentials = $this->get_client_credentials(); + if ( ! $client_credentials ) { + return $this->google_client; + } + + try { + $this->google_client->setAuthConfig( (array) $client_credentials->web ); + } catch ( Exception $e ) { + return $this->google_client; + } + + // Offline access so we can access the refresh token even when the user is logged out. + $this->google_client->setAccessType( 'offline' ); + $this->google_client->setApprovalPrompt( 'force' ); + $this->google_client->setPrompt( 'consent' ); + + $this->google_client->setRedirectUri( $this->get_redirect_uri() ); + + $this->google_client->setScopes( $this->get_required_scopes() ); + $this->google_client->prepareScopes(); + + $access_token = $this->get_access_token(); + + // Return unconfigured client if access token not yet set. + if ( empty( $access_token ) ) { + return $this->google_client; + } + + $token = array( + 'access_token' => $access_token, + 'refresh_token' => $this->get_refresh_token(), + 'expires_in' => $this->user_options->get( self::OPTION_ACCESS_TOKEN_EXPIRES_IN ), + 'created' => $this->user_options->get( self::OPTION_ACCESS_TOKEN_CREATED ), + ); + + $this->google_client->setAccessToken( $token ); + + // If the token expired or is going to expire in the next 30 seconds. + if ( $this->google_client->isAccessTokenExpired() ) { + $this->refresh_token(); + } + + return $this->google_client; + } + + /** + * Refreshes the access token. + * + * @since 1.0.0 + */ + public function refresh_token() { + // Check for a valid stored refresh token. If it's been set, grab the authentication token. + $refresh_token = $this->get_refresh_token(); + + if ( empty( $refresh_token ) ) { + $this->user_options->set( self::OPTION_ERROR_CODE, 'refresh_token_not_exist' ); + } + + // Stop if google_client not initialized yet. + if ( ! $this->google_client instanceof Google_Client ) { + return; + } + + try { + $authentication_token = $this->google_client->fetchAccessTokenWithRefreshToken( $refresh_token ); + + // Refresh token is expired or revoked. + if ( ! empty( $authentication_token['error'] ) ) { + $this->user_options->set( self::OPTION_ERROR_CODE, $authentication_token['error'] ); + return; + } + + if ( ! isset( $authentication_token['access_token'] ) ) { + $this->user_options->set( self::OPTION_ERROR_CODE, 'access_token_not_received' ); + return; + } + + $this->set_access_token( + $authentication_token['access_token'], + isset( $authentication_token['expires_in'] ) ? $authentication_token['expires_in'] : '', + isset( $authentication_token['created'] ) ? $authentication_token['created'] : 0 + ); + } catch ( \Exception $e ) { + $this->user_options->set( self::OPTION_ERROR_CODE, $e->getCode() ); + } + } + + /** + * Revokes the access token. + * + * @since 1.0.0 + */ + public function revoke_token() { + // Stop if google_client not initialized yet. + if ( ! $this->google_client instanceof Google_Client ) { + return; + } + + $this->google_client->revokeToken(); + } + + /** + * Gets the list of currently required Google OAuth scopes. + * + * @since 1.0.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.0.0 + * + * @param array $scopes List of scopes. + */ + $scopes = (array) apply_filters( 'googlesitekit_auth_scopes', array() ); + + // These are always required. + $default_scopes = array( + 'https://www.googleapis.com/auth/userinfo.profile', + 'https://www.googleapis.com/auth/userinfo.email', + ); + + return array_unique( array_merge( $default_scopes, $scopes ) ); + } + + /** + * 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 array List of Google OAuth scopes. + */ + public function get_granted_scopes() { + return array_values( (array) $this->user_options->get( self::OPTION_AUTH_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 array $scopes List of Google OAuth scopes. + * @return bool True on success, false on failure. + */ + public function set_granted_scopes( $scopes ) { + $scopes = array_filter( $scopes, 'is_string' ); + + return $this->user_options->set( self::OPTION_AUTH_SCOPES, $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() { + if ( ! empty( $this->access_token ) ) { + return $this->access_token; + } + + $access_token = $this->encrypted_user_options->get( self::OPTION_ACCESS_TOKEN ); + + if ( ! $access_token ) { + return false; + } + + $this->access_token = $access_token; + + return $this->access_token; + } + + /** + * Sets the current user's OAuth access token. + * + * @since 1.0.0 + * + * @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 ) { + // Bail early if nothing change. + if ( $this->get_access_token() === $access_token ) { + return true; + } + + $this->access_token = $access_token; + + // If not provided, assume current GMT time. + if ( empty( $created ) ) { + $created = current_time( 'timestamp', 1 ); + } + + $this->user_options->set( self::OPTION_ACCESS_TOKEN_EXPIRES_IN, $expires_in ); + $this->user_options->set( self::OPTION_ACCESS_TOKEN_CREATED, $created ); + + return $this->encrypted_user_options->set( self::OPTION_ACCESS_TOKEN, $this->access_token ); + } + + /** + * Gets the current user's OAuth refresh token. + * + * @since 1.0.0 + * + * @return string|bool Refresh token if it exists, false otherwise. + */ + public function get_refresh_token() { + if ( ! empty( $this->refresh_token ) ) { + return $this->refresh_token; + } + + $refresh_token = $this->encrypted_user_options->get( self::OPTION_REFRESH_TOKEN ); + + if ( ! $refresh_token ) { + return false; + } + + $this->refresh_token = $refresh_token; + + return $this->refresh_token; + } + + /** + * Sets the current user's OAuth refresh token. + * + * @since 1.0.0 + * + * @param string $refresh_token New refresh token. + * @return bool True on success, false on failure. + */ + public function set_refresh_token( $refresh_token ) { + // Bail early if nothing change. + if ( $this->get_refresh_token() === $refresh_token ) { + return true; + } + + $this->refresh_token = $refresh_token; + + return $this->encrypted_user_options->set( self::OPTION_REFRESH_TOKEN, $this->refresh_token ); + } + + /** + * Gets the authentication URL. + * + * @since 1.0.0 + * + * @param string $redirect_url Redirect URL after authentication. + * @return string Authentication URL. + */ + public function get_authentication_url( $redirect_url = '' ) { + if ( empty( $redirect_url ) ) { + $redirect_url = $this->context->admin_url( 'splash' ); + } + + $redirect_url = add_query_arg( array( 'notification' => 'authentication_success' ), $redirect_url ); + // Ensure we remove error query string. + $redirect_url = remove_query_arg( 'error', $redirect_url ); + + $this->user_options->set( self::OPTION_REDIRECT_URL, $redirect_url ); + + // Ensure the latest required scopes are requested. + $this->get_client()->setScopes( $this->get_required_scopes() ); + + return $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 + */ + public function authorize_user() { + if ( ! isset( $_GET['code'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.NoNonceVerification + $auth_url = $this->get_client()->createAuthUrl(); + $auth_url = filter_var( $auth_url, FILTER_SANITIZE_URL ); + + wp_safe_redirect( $auth_url ); + exit(); + } + + if ( ! $this->credentials->has() ) { + $this->user_options->set( self::OPTION_ERROR_CODE, 'oauth_credentials_not_exist' ); + wp_safe_redirect( admin_url() ); + exit(); + } + + try { + $authentication_token = $this->get_client()->fetchAccessTokenWithAuthCode( $_GET['code'] ); // phpcs:ignore WordPress.Security.NonceVerification.NoNonceVerification + } catch ( Exception $e ) { + $this->user_options->set( self::OPTION_ERROR_CODE, 'invalid_code' ); + wp_safe_redirect( admin_url() ); + exit(); + } + + if ( ! empty( $authentication_token['error'] ) ) { + $this->user_options->set( self::OPTION_ERROR_CODE, $authentication_token['error'] ); + wp_safe_redirect( admin_url() ); + exit(); + } + + if ( ! isset( $authentication_token['access_token'] ) ) { + $this->user_options->set( self::OPTION_ERROR_CODE, 'access_token_not_received' ); + wp_safe_redirect( admin_url() ); + exit(); + } + + $this->set_access_token( + $authentication_token['access_token'], + isset( $authentication_token['expires_in'] ) ? $authentication_token['expires_in'] : '', + isset( $authentication_token['created'] ) ? $authentication_token['created'] : 0 + ); + + // Update the site refresh token. + $refresh_token = $this->get_client()->getRefreshToken(); + $this->set_refresh_token( $refresh_token ); + + // Update granted scopes. + if ( isset( $authentication_token['scope'] ) ) { + $scopes = explode( ' ', $authentication_token['scope'] ); + } elseif ( isset( $_GET['scope'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.NoNonceVerification + $scopes = explode( ' ', $_GET['scope'] ); // phpcs:ignore WordPress.Security.NonceVerification.NoNonceVerification + } else { + $scopes = $this->get_required_scopes(); + } + $scopes = array_filter( + $scopes, + function( $scope ) { + if ( ! is_string( $scope ) ) { + return false; + } + return 0 === strpos( $scope, 'https://www.googleapis.com/auth/' ); + } + ); + $this->set_granted_scopes( $scopes ); + + $redirect_url = $this->user_options->get( self::OPTION_REDIRECT_URL ); + + if ( $redirect_url ) { + $parts = wp_parse_url( $redirect_url ); + $reauth = strpos( $parts['query'], 'reAuth=true' ); + if ( false === $reauth ) { + $redirect_url = add_query_arg( array( 'notification' => 'authentication_success' ), $redirect_url ); + } + $this->user_options->delete( self::OPTION_REDIRECT_URL ); + } else { + // No redirect_url is set, use default page. + $redirect_url = $this->context->admin_url( 'splash', array( 'notification' => 'authentication_success' ) ); + } + + wp_safe_redirect( $redirect_url ); + exit(); + } + + /** + * Converts the given error code to a user-facing message. + * + * @since 1.0.0 + * + * @param string $error_code Error code. + * @return string Error message. + */ + public function get_error_message( $error_code ) { + switch ( $error_code ) { + case 'oauth_credentials_not_exist': + $message = __( 'Unable to authenticate Site Kit. Check your client configuration is in the correct JSON format.', 'google-site-kit' ); + break; + case 'refresh_token_not_exist': + $message = __( 'Unable to refresh access token, as no refresh token exists.', 'google-site-kit' ); + break; + case 'cannot_log_in': + $message = __( 'Internal error that the Google login redirect failed.', 'google-site-kit' ); + break; + case 'invalid_code': + $message = __( 'Unable to receive access token because of an empty authorization code.', 'google-site-kit' ); + break; + case 'access_token_not_received': + $message = __( 'Unable to receive access token because of an unknown error.', 'google-site-kit' ); + break; + // The following messages are based on https://tools.ietf.org/html/rfc6749#section-5.2. + case 'invalid_request': + $message = __( 'Unable to receive access token because of an invalid OAuth request.', 'google-site-kit' ); + break; + case 'invalid_client': + $message = __( 'Unable to receive access token because of an invalid client.', 'google-site-kit' ); + break; + case 'invalid_grant': + $message = __( 'Unable to receive access token because of an invalid authorization code or refresh token.', 'google-site-kit' ); + break; + case 'unauthorized_client': + $message = __( 'Unable to receive access token because of an unauthorized client.', 'google-site-kit' ); + break; + case 'unsupported_grant_type': + $message = __( 'Unable to receive access token because of an unsupported grant type.', 'google-site-kit' ); + break; + default: + $message = __( 'Unknown Error', 'google-site-kit' ); + break; + } + + return $message; + } + + /** + * Gets the OAuth redirect URI that listens to the callback request. + * + * @since 1.0.0 + * + * @return string OAuth redirect URI. + */ + private function get_redirect_uri() { + return add_query_arg( 'oauth2callback', '1', untrailingslashit( home_url() ) ); + } + + /** + * Retrieve the Site Kit oAuth secret. + */ + private function get_client_credentials() { + if ( false !== $this->client_credentials ) { + return $this->client_credentials; + } + + /** + * Site Kit oAuth Secret is a string of the JSON for 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://+ warning screen that your website app is not yet verified.', 'google-site-kit' ), + array( + 'strong' => array(), + ) + ); + ?> +
++ does not stop you from using Site Kit. To continue the authentication process from this screen:', 'google-site-kit' ), + array( + 'strong' => array(), + ) + ); + ?> +
++ read the docs on the Site Kit website.', 'google-site-kit' ), + esc_url( 'https://sitekit.withgoogle.com/documentation/gcp-app-verification/' ) + ), + array( + 'a' => array( + 'href' => array(), + 'target' => array(), + ), + ) + ); + ?> +
++ not yet compatible for use in a WordPress multisite network, but we’re actively working on that.', 'google-site-kit' ), + array( + 'strong' => array(), + ) + ); + ?> +
++ +
++ +
++ +
++ +
++ +
++ +
++ + Default Link + +
++ + VRT: Default Link Hovered + +
++ + Default Link Button + +
++ + Inherited Link + +
++ + Small Link + +
++ + Inverse Link + +
++ + Back Link + +
++ + External Link + +
++ + All Caps Link + +
++ + All Caps Link with Arrow + +
++ + Inverse All Caps Link with Arrow + +
++ + Danger Link + +
++ + Disabled Link + +
+
+
+
Default
+Small
+Small Compress
+