This commit is contained in:
Greta Thunberg 2022-12-15 17:34:08 +01:00
parent a37aed4694
commit 549be0e03c
1254 changed files with 144502 additions and 0 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
!function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=11)}({11:function(e,t,n){"use strict";function r(e,t){var n=e.matches||e.matchesSelector||e.webkitMatchesSelector||e.mozMatchesSelector||e.msMatchesSelector||e.oMatchesSelector||function(e){for(var t=(this.document||this.ownerDocument).querySelectorAll(e),n=t.length;--n>=0&&t.item(n)!==this;);return n>-1};return!!n&&n.call(e,t)}n.r(t);var o=window._googlesitekitAnalyticsTrackingData||[];Array.isArray(o)&&function(e,t){var n=[];e.forEach((function(e){var o=function(n){("DOMContentLoaded"===e.on||r(n.target,e.selector)||r(n.target,e.selector.concat(" *")))&&t(e.action,e.metadata)};document.addEventListener(e.on,o,!0),n.push([e.on,o,!0])}))}(o,(function(e,t){window.gtag("event",e,t||void 0)}))}});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
(window.__googlesitekit_webpackJsonp=window.__googlesitekit_webpackJsonp||[]).push([[5],{1120:function(t,r,e){"use strict";e.r(r);var n=e(83),o=document.querySelector("#wp-admin-bar-logout a");o||(o=document.querySelector(".sidebar__me-signout button")),o&&o.addEventListener("click",(function(){Object(n.a)()}))},83:function(t,r,e){"use strict";(function(t){e.d(r,"a",(function(){return c})),e.d(r,"b",(function(){return l}));var n=e(14),o=e.n(n);function a(t,r){var e="undefined"!=typeof Symbol&&t[Symbol.iterator]||t["@@iterator"];if(!e){if(Array.isArray(t)||(e=function(t,r){if(!t)return;if("string"==typeof t)return i(t,r);var e=Object.prototype.toString.call(t).slice(8,-1);"Object"===e&&t.constructor&&(e=t.constructor.name);if("Map"===e||"Set"===e)return Array.from(t);if("Arguments"===e||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(e))return i(t,r)}(t))||r&&t&&"number"==typeof t.length){e&&(t=e);var n=0,o=function(){};return{s:o,n:function(){return n>=t.length?{done:!0}:{done:!1,value:t[n++]}},e:function(t){throw t},f:o}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var a,c=!0,u=!1;return{s:function(){e=e.call(t)},n:function(){var t=e.next();return c=t.done,t},e:function(t){u=!0,a=t},f:function(){try{c||null==e.return||e.return()}finally{if(u)throw a}}}}function i(t,r){(null==r||r>t.length)&&(r=t.length);for(var e=0,n=new Array(r);e<r;e++)n[e]=t[e];return n}var c=function(){t.localStorage&&t.localStorage.clear(),t.sessionStorage&&t.sessionStorage.clear()},u=function(t){for(var r=location.search.substr(1).split("&"),e={},n=0;n<r.length;n++)e[r[n].split("=")[0]]=decodeURIComponent(r[n].split("=")[1]);return t?e.hasOwnProperty(t)?decodeURIComponent(e[t].replace(/\+/g," ")):"":e},l=function(t){var r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:location,e=new URL(r.href);if(t)return e.searchParams&&e.searchParams.get?e.searchParams.get(t):u(t);var n,i={},c=a(e.searchParams.entries());try{for(c.s();!(n=c.n()).done;){var l=o()(n.value,2),s=l[0],f=l[1];i[s]=f}}catch(t){c.e(t)}finally{c.f()}return i}}).call(this,e(22))}},[[1120,1,0]]]);

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
(window.__googlesitekit_webpackJsonp=window.__googlesitekit_webpackJsonp||[]).push([[7],{1110:function(n,t,o){"use strict";o.r(t),function(n){o.d(t,"Button",(function(){return i})),o.d(t,"Checkbox",(function(){return r})),o.d(t,"Chip",(function(){return e})),o.d(t,"Dialog",(function(){return c})),o.d(t,"Menu",(function(){return l})),o.d(t,"ProgressBar",(function(){return a})),o.d(t,"Radio",(function(){return s})),o.d(t,"Switch",(function(){return f})),o.d(t,"Tooltip",(function(){return g}));var u=o(337);void 0===n.googlesitekit&&(n.googlesitekit={}),n.googlesitekit.components=u.a;var i=u.a.Button,r=u.a.Checkbox,e=u.a.Chip,c=u.a.Dialog,l=u.a.Menu,a=u.a.ProgressBar,s=u.a.Radio,f=u.a.Switch,g=u.a.Tooltip}.call(this,o(22))},337:function(n,t,o){"use strict";var u={Button:function Button(){return null},Checkbox:function Checkbox(){return null},Chip:function Chip(){return null},Dialog:function Dialog(){return null},Menu:function Menu(){return null},ProgressBar:function ProgressBar(){return null},Radio:function Radio(){return null},Switch:function Switch(){return null},Tooltip:function Tooltip(){return null}};t.a=u}},[[1110,1,0]]]);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
(window.__googlesitekit_webpackJsonp=window.__googlesitekit_webpackJsonp||[]).push([[27],{1107:function(t,n,e){"use strict";e.r(n),function(t){var n=e(5),i=e.n(n),s=e(16),o=e.n(s);function a(){return(a=o()(i.a.mark((function n(){return i.a.wrap((function(n){for(;;)switch(n.prev=n.next){case 0:if(void 0!==t.IntersectionObserver){n.next=3;break}return n.next=3,e.e(40).then(e.t.bind(null,1144,7));case 3:case"end":return n.stop()}}),n)})))).apply(this,arguments)}e.p="".concat(t._googlesitekitBaseData.assetsURL,"js/"),function(){a.apply(this,arguments)}()}.call(this,e(22))}},[[1107,1,0]]]);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
!function(e){function r(r){for(var t,a,u=r[0],i=r[1],p=r[2],f=0,l=[];f<u.length;f++)a=u[f],Object.prototype.hasOwnProperty.call(n,a)&&n[a]&&l.push(n[a][0]),n[a]=0;for(t in i)Object.prototype.hasOwnProperty.call(i,t)&&(e[t]=i[t]);for(c&&c(r);l.length;)l.shift()();return o.push.apply(o,p||[]),_()}function _(){for(var e,r=0;r<o.length;r++){for(var _=o[r],t=!0,a=1;a<_.length;a++){var u=_[a];0!==n[u]&&(t=!1)}t&&(o.splice(r--,1),e=__webpack_require__(__webpack_require__.s=_[0]))}return e}var t={},n={1:0},o=[];function __webpack_require__(r){if(t[r])return t[r].exports;var _=t[r]={i:r,l:!1,exports:{}};return e[r].call(_.exports,_,_.exports,__webpack_require__),_.l=!0,_.exports}__webpack_require__.e=function(e){var r=[],_=n[e];if(0!==_)if(_)r.push(_[2]);else{var t=new Promise((function(r,t){_=n[e]=[r,t]}));r.push(_[2]=t);var o,a=document.createElement("script");a.charset="utf-8",a.timeout=120,__webpack_require__.nc&&a.setAttribute("nonce",__webpack_require__.nc),a.src=function(e){return __webpack_require__.p+""+({}[e]||e)+"-"+{33:"91e4cd835b2da520b8c7",34:"f237ecaf303dba7fc6f1",35:"c51187d8eb542aa0af4f",36:"8de7a347165d649b30fe",37:"316a819905d700e31894",38:"46221647afba80e686e8",39:"cb776233770cbfdfe280",40:"8fe4615c4459c5663f24"}[e]+".js"}(e);var u=new Error;o=function(r){a.onerror=a.onload=null,clearTimeout(i);var _=n[e];if(0!==_){if(_){var t=r&&("load"===r.type?"missing":r.type),o=r&&r.target&&r.target.src;u.message="Loading chunk "+e+" failed.\n("+t+": "+o+")",u.name="ChunkLoadError",u.type=t,u.request=o,_[1](u)}n[e]=void 0}};var i=setTimeout((function(){o({type:"timeout",target:a})}),12e4);a.onerror=a.onload=o,document.head.appendChild(a)}return Promise.all(r)},__webpack_require__.m=e,__webpack_require__.c=t,__webpack_require__.d=function(e,r,_){__webpack_require__.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:_})},__webpack_require__.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},__webpack_require__.t=function(e,r){if(1&r&&(e=__webpack_require__(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var _=Object.create(null);if(__webpack_require__.r(_),Object.defineProperty(_,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var t in e)__webpack_require__.d(_,t,function(r){return e[r]}.bind(null,t));return _},__webpack_require__.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return __webpack_require__.d(r,"a",r),r},__webpack_require__.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},__webpack_require__.p="",__webpack_require__.oe=function(e){throw console.error(e),e};var a=window.__googlesitekit_webpackJsonp=window.__googlesitekit_webpackJsonp||[],u=a.push.bind(a);a.push=r,a=a.slice();for(var i=0;i<a.length;i++)r(a[i]);var c=u;_()}([]);

View File

@ -0,0 +1,12 @@
<?php
/**
* @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
*/
return array(
'buildMode' => 'production',
'features' => array( 'adsenseSetupV2','dashboardSharing','ga4ActivationBanner','ga4Reporting','gm3Components','ideaHubModule','twgModule','userInput' ),
);

View File

@ -0,0 +1,48 @@
<?php
/**
* @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
*/
return array(
'analytics-advanced-tracking' => array( "analytics-advanced-tracking-10a628cd2acd13ac224f.js", null ),
'googlesitekit-i18n' => array( "googlesitekit-i18n-e29c32171c9d96a19747.js", null ),
'googlesitekit-idea-hub-notice' => array( "googlesitekit-idea-hub-notice-02c15332ebcac425ae19.js", null ),
'googlesitekit-admin-css' => array( "googlesitekit-admin-css-98d54febeadaa74bad87.min.css", null ),
'googlesitekit-adminbar-css' => array( "googlesitekit-adminbar-css-c818e88b67a50dcb8813.min.css", null ),
'googlesitekit-wp-dashboard-css' => array( "googlesitekit-wp-dashboard-css-832e935928ddcc45c765.min.css", null ),
'googlesitekit-vendor' => array( "googlesitekit-vendor-8f4431ca84690dee165e.js", null ),
'googlesitekit-runtime' => array( "runtime-c3794a2c50ed91b7c718.js", null ),
'googlesitekit-activation' => array( "googlesitekit-activation-928104701ab1efd3fdff.js", null ),
'googlesitekit-adminbar' => array( "googlesitekit-adminbar-9ded54f46f7fa6f7c891.js", null ),
'googlesitekit-api' => array( "googlesitekit-api-f180ed348a6edaf5cb80.js", null ),
'googlesitekit-base' => array( "googlesitekit-base-428f2a8fce19afd07680.js", null ),
'googlesitekit-components' => array( array( "googlesitekit-components-gm2-ab3df926960d2b494fba.js", null ), array( "googlesitekit-components-gm3-96c09bc4fc8583c632af.js", null ) ),
'googlesitekit-data' => array( "googlesitekit-data-07c00a76bf006711abf5.js", null ),
'googlesitekit-datastore-forms' => array( "googlesitekit-datastore-forms-e7ade08a4368d1e7d694.js", null ),
'googlesitekit-datastore-location' => array( "googlesitekit-datastore-location-69d862ffceaf2bd3ffc3.js", null ),
'googlesitekit-datastore-site' => array( "googlesitekit-datastore-site-88b4060a2525c03512d6.js", null ),
'googlesitekit-datastore-ui' => array( "googlesitekit-datastore-ui-d342703a58294f4990ee.js", null ),
'googlesitekit-datastore-user' => array( "googlesitekit-datastore-user-e7326c251084d187c933.js", null ),
'googlesitekit-entity-dashboard' => array( "googlesitekit-entity-dashboard-4dd03814fce85da2ef55.js", null ),
'googlesitekit-idea-hub-post-list' => array( "googlesitekit-idea-hub-post-list-8a7c89f7ca82d07e3a38.js", null ),
'googlesitekit-main-dashboard' => array( "googlesitekit-main-dashboard-4e9b0b8171cac87a5dd8.js", null ),
'googlesitekit-modules' => array( "googlesitekit-modules-708e889a42eed0b55b70.js", null ),
'googlesitekit-modules-adsense' => array( "googlesitekit-modules-adsense-2f0297e2232b9af71927.js", null ),
'googlesitekit-modules-analytics' => array( "googlesitekit-modules-analytics-e400c6d08e8fb85b1524.js", null ),
'googlesitekit-modules-analytics-4' => array( "googlesitekit-modules-analytics-4-75d4679dfdde59f2d87a.js", null ),
'googlesitekit-modules-idea-hub' => array( "googlesitekit-modules-idea-hub-eb8287c7b857acb6ca3a.js", null ),
'googlesitekit-modules-optimize' => array( "googlesitekit-modules-optimize-a96d95f7e119b3efd19a.js", null ),
'googlesitekit-modules-pagespeed-insights' => array( "googlesitekit-modules-pagespeed-insights-2286854cc017238ec4e9.js", null ),
'googlesitekit-modules-search-console' => array( "googlesitekit-modules-search-console-b9d2a1ee7240e9d69c89.js", null ),
'googlesitekit-modules-tagmanager' => array( "googlesitekit-modules-tagmanager-804e256ecc34267ae56d.js", null ),
'googlesitekit-modules-thank-with-google' => array( "googlesitekit-modules-thank-with-google-a79d3699d1180a52625b.js", null ),
'googlesitekit-polyfills' => array( "googlesitekit-polyfills-c446451bb1fde8c4da00.js", null ),
'googlesitekit-settings' => array( "googlesitekit-settings-7442c730a1dc8ae8c469.js", null ),
'googlesitekit-splash' => array( "googlesitekit-splash-54f43c97c17eb26b56a8.js", null ),
'googlesitekit-user-input' => array( "googlesitekit-user-input-595a65d4191fddce7c2a.js", null ),
'googlesitekit-widgets' => array( "googlesitekit-widgets-cdff224864f7db677b78.js", null ),
'googlesitekit-wp-dashboard' => array( "googlesitekit-wp-dashboard-e7cf0b1d4ec008577819.js", null ),
);

View File

@ -0,0 +1,124 @@
<?php
/**
* Plugin main 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
*
* @wordpress-plugin
* Plugin Name: Site Kit by Google
* Plugin URI: https://sitekit.withgoogle.com
* Description: Site Kit is a one-stop solution for WordPress users to use everything Google has to offer to make them successful on the web.
* Version: 1.89.0
* Requires at least: 5.2
* Requires PHP: 5.6
* Author: Google
* Author URI: https://opensource.google.com
* License: Apache License 2.0
* License URI: https://www.apache.org/licenses/LICENSE-2.0
* Text Domain: google-site-kit
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
// Define most essential constants.
define( 'GOOGLESITEKIT_VERSION', '1.89.0' );
define( 'GOOGLESITEKIT_PLUGIN_MAIN_FILE', __FILE__ );
define( 'GOOGLESITEKIT_PHP_MINIMUM', '5.6.0' );
define( 'GOOGLESITEKIT_WP_MINIMUM', '5.2.0' );
/**
* Handles plugin activation.
*
* Throws an error if the plugin is activated with an insufficient version of PHP.
*
* @since 1.0.0
* @since 1.3.0 Minimum required version of PHP raised to 5.6
* @access private
*
* @param bool $network_wide Whether to activate network-wide.
*/
function googlesitekit_activate_plugin( $network_wide ) {
if ( version_compare( PHP_VERSION, GOOGLESITEKIT_PHP_MINIMUM, '<' ) ) {
wp_die(
/* translators: %s: version number */
esc_html( sprintf( __( 'Site Kit requires PHP version %s or higher', 'google-site-kit' ), GOOGLESITEKIT_PHP_MINIMUM ) ),
esc_html__( 'Error Activating', 'google-site-kit' )
);
}
if ( version_compare( get_bloginfo( 'version' ), GOOGLESITEKIT_WP_MINIMUM, '<' ) ) {
wp_die(
/* translators: %s: version number */
esc_html( sprintf( __( 'Site Kit requires WordPress version %s or higher', 'google-site-kit' ), GOOGLESITEKIT_WP_MINIMUM ) ),
esc_html__( 'Error Activating', 'google-site-kit' )
);
}
if ( $network_wide ) {
return;
}
do_action( 'googlesitekit_activation', $network_wide );
}
register_activation_hook( __FILE__, 'googlesitekit_activate_plugin' );
/**
* Handles plugin deactivation.
*
* @since 1.0.0
* @access private
*
* @param bool $network_wide Whether to deactivate network-wide.
*/
function googlesitekit_deactivate_plugin( $network_wide ) {
if ( version_compare( PHP_VERSION, GOOGLESITEKIT_PHP_MINIMUM, '<' ) ) {
return;
}
if ( $network_wide ) {
return;
}
do_action( 'googlesitekit_deactivation', $network_wide );
}
register_deactivation_hook( __FILE__, 'googlesitekit_deactivate_plugin' );
/**
* Resets opcache if possible.
*
* @since 1.3.0
* @access private
*/
function googlesitekit_opcache_reset() {
if ( version_compare( PHP_VERSION, GOOGLESITEKIT_PHP_MINIMUM, '<' ) ) {
return;
}
if ( ! function_exists( 'opcache_reset' ) ) {
return;
}
if ( ! empty( ini_get( 'opcache.restrict_api' ) ) && strpos( __FILE__, ini_get( 'opcache.restrict_api' ) ) !== 0 ) {
return;
}
// `opcache_reset` is prohibited on the WordPress VIP platform due to memory corruption.
if ( defined( 'WPCOM_IS_VIP_ENV' ) && WPCOM_IS_VIP_ENV ) {
return;
}
opcache_reset(); // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.opcache_opcache_reset
}
add_action( 'upgrader_process_complete', 'googlesitekit_opcache_reset' );
if (
version_compare( PHP_VERSION, GOOGLESITEKIT_PHP_MINIMUM, '>=' ) &&
version_compare( get_bloginfo( 'version' ), GOOGLESITEKIT_WP_MINIMUM, '>=' )
) {
require_once plugin_dir_path( __FILE__ ) . 'includes/loader.php';
}

View File

@ -0,0 +1,513 @@
<?php
/**
* Class Google\Site_Kit\Context
*
* @package Google\Site_Kit
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit;
use AMP_Options_Manager;
use AMP_Theme_Support;
use Google\Site_Kit\Core\Util\Input;
use Google\Site_Kit\Core\Util\Entity;
use Google\Site_Kit\Core\Util\Entity_Factory;
/**
* Class representing the context in which the plugin is running.
*
* @since 1.0.0
* @access private
* @ignore
*/
class Context {
/**
* Primary "standard" AMP website mode.
*
* This mode is currently unused due to Tag Manager setup not showing the Web Container dropdown
* when AMP is in standard mode and some urls have AMP disabled.
*
* @since 1.0.0 Originally introduced.
* @since 1.36.0 Marked as unused, see description.
* @var string
*/
const AMP_MODE_PRIMARY = 'primary';
/**
* Secondary AMP website mode.
*
* @since 1.0.0
* @var string
*/
const AMP_MODE_SECONDARY = 'secondary';
/**
* Absolute path to the plugin main file.
*
* @since 1.0.0
* @var string
*/
private $main_file;
/**
* Internal storage for whether the plugin is network active or not.
*
* @since 1.0.0
* @var bool|null
*/
private $network_active = null;
/**
* Input access abstraction.
*
* @since 1.1.2
* @var Input
*/
private $input;
/**
* Constructor.
*
* @since 1.0.0
* @since 1.1.2 Added optional $input instance.
*
* @param string $main_file Absolute path to the plugin main file.
* @param Input $input Input instance.
*/
public function __construct( $main_file, Input $input = null ) {
$this->main_file = $main_file;
$this->input = $input ?: new Input();
}
/**
* Gets the absolute path for a path relative to the plugin directory.
*
* @since 1.0.0
*
* @param string $relative_path Optional. Relative path. Default '/'.
* @return string Absolute path.
*/
public function path( $relative_path = '/' ) {
return plugin_dir_path( $this->main_file ) . ltrim( $relative_path, '/' );
}
/**
* Gets the full URL for a path relative to the plugin directory.
*
* @since 1.0.0
*
* @param string $relative_path Optional. Relative path. Default '/'.
* @return string Full URL.
*/
public function url( $relative_path = '/' ) {
return plugin_dir_url( $this->main_file ) . ltrim( $relative_path, '/' );
}
/**
* Gets the Input instance.
*
* @since 1.1.2
*
* @return Input
*/
public function input() {
return $this->input;
}
/**
* Gets the full URL to an admin screen part of the plugin.
*
* @since 1.0.0
*
* @param string $slug Optional. Plugin admin screen slug. Default 'dashboard'.
* @param array $query_args Optional. Additional query args. Default empty array.
* @return string Full admin screen URL.
*/
public function admin_url( $slug = 'dashboard', array $query_args = array() ) {
unset( $query_args['page'] );
if ( $this->is_network_mode() ) {
$base_url = network_admin_url( 'admin.php' );
} else {
$base_url = admin_url( 'admin.php' );
}
return add_query_arg(
array_merge(
array( 'page' => Core\Admin\Screens::PREFIX . $slug ),
$query_args
),
$base_url
);
}
/**
* Determines whether the plugin is running in network mode.
*
* @since 1.0.0
*
* @return bool True if the plugin is in network mode, false otherwise.
*/
public function is_network_mode() {
// Bail if plugin is not network-active.
if ( ! $this->is_network_active() ) {
return false;
}
/**
* Filters whether network mode is active in Site Kit.
*
* This is always false by default since Site Kit does not support a network mode yet.
*
* @since 1.86.0
*
* @param bool $active Whether network mode is active.
*/
return (bool) apply_filters( 'googlesitekit_is_network_mode', false );
}
/**
* Gets the cannonical "home" URL.
*
* Returns the value from the `"googlesitekit_canonical_home_url"` filter.
*
* @since 1.18.0
*
* @return string Cannonical home URL.
*/
public function get_canonical_home_url() {
/**
* Filters the canonical home URL considered by Site Kit.
*
* Typically this is okay to be the unmodified `home_url()`, but certain plugins (e.g. multilingual plugins)
* that dynamically modify that value based on context can use this filter to ensure that the URL considered
* by Site Kit remains stable.
*
* @since 1.18.0
*
* @param string $home_url The value of `home_url()`.
*/
return apply_filters( 'googlesitekit_canonical_home_url', home_url() );
}
/**
* Gets the site URL of the reference site to use for stats.
*
* @since 1.0.0
*
* @return string Reference site URL.
*/
public function get_reference_site_url() {
return $this->filter_reference_url();
}
/**
* Gets the entity for the current request context.
*
* An entity in Site Kit terminology is based on a canonical URL, i.e. every
* canonical URL has an associated entity.
*
* An entity may also have a type, a title, and an ID.
*
* @since 1.7.0
*
* @return Entity|null The current entity, or null if none could be determined.
*/
public function get_reference_entity() {
// Support specific URL stats being checked in Site Kit dashboard details view.
if ( is_admin() && 'googlesitekit-dashboard' === $this->input()->filter( INPUT_GET, 'page' ) ) {
$entity_url_query_param = $this->input()->filter( INPUT_GET, 'permaLink' );
if ( ! empty( $entity_url_query_param ) ) {
return $this->get_reference_entity_from_url( $entity_url_query_param );
}
}
$entity = Entity_Factory::from_context();
return $this->filter_entity_reference_url( $entity );
}
/**
* Gets the entity for the given URL, if available.
*
* An entity in Site Kit terminology is based on a canonical URL, i.e. every
* canonical URL has an associated entity.
*
* An entity may also have a type, a title, and an ID.
*
* @since 1.10.0
*
* @param string $url URL to determine the entity from.
* @return Entity|null The current entity, or null if none could be determined.
*/
public function get_reference_entity_from_url( $url ) {
// Ensure local URL is used for lookup.
$url = str_replace(
$this->get_reference_site_url(),
untrailingslashit( $this->get_canonical_home_url() ),
$url
);
$entity = Entity_Factory::from_url( $url );
return $this->filter_entity_reference_url( $entity );
}
/**
* Gets the permalink of the reference site to use for stats.
*
* @since 1.0.0
*
* @param int|WP_Post $post Optional. Post ID or post object. Default is the global `$post`.
*
* @return string|false The reference permalink URL or false if post does not exist.
*/
public function get_reference_permalink( $post = 0 ) {
// If post is provided, get URL for that.
if ( $post ) {
$permalink = get_permalink( $post );
if ( false === $permalink ) {
return false;
}
return $this->filter_reference_url( $permalink );
}
// Otherwise use entity detection.
$entity = $this->get_reference_entity();
if ( ! $entity || 'post' !== $entity->get_type() ) {
return false;
}
return $entity->get_url();
}
/**
* Gets the canonical url for the current request.
*
* @since 1.0.0
*
* @return string|false The reference canonical URL or false if no URL was identified.
*/
public function get_reference_canonical() {
$entity = $this->get_reference_entity();
if ( ! $entity ) {
return false;
}
return $entity->get_url();
}
/**
* Checks whether AMP content is being served.
*
* @since 1.0.0
*
* @return bool True if an AMP request, false otherwise.
*/
public function is_amp() {
if ( is_singular( 'web-story' ) ) {
return true;
}
return function_exists( 'is_amp_endpoint' ) && is_amp_endpoint();
}
/**
* Gets the current AMP mode.
*
* @since 1.0.0
*
* @return bool|string 'primary' if in standard mode,
* 'secondary' if in transitional or reader modes
* false if AMP not active, or unknown mode
*/
public function get_amp_mode() {
// If the Web Stories plugin is enabled, consider the site to be running
// in Secondary AMP mode.
if ( defined( 'WEBSTORIES_VERSION' ) ) {
return self::AMP_MODE_SECONDARY;
}
if ( ! class_exists( 'AMP_Theme_Support' ) ) {
return false;
}
$exposes_support_mode = defined( 'AMP_Theme_Support::STANDARD_MODE_SLUG' )
&& defined( 'AMP_Theme_Support::TRANSITIONAL_MODE_SLUG' )
&& defined( 'AMP_Theme_Support::READER_MODE_SLUG' );
if ( defined( 'AMP__VERSION' ) ) {
$amp_plugin_version = AMP__VERSION;
if ( strpos( $amp_plugin_version, '-' ) !== false ) {
$amp_plugin_version = explode( '-', $amp_plugin_version )[0];
}
$amp_plugin_version_2_or_higher = version_compare( $amp_plugin_version, '2.0.0', '>=' );
} else {
$amp_plugin_version_2_or_higher = false;
}
if ( $amp_plugin_version_2_or_higher ) {
$exposes_support_mode = class_exists( 'AMP_Options_Manager' )
&& method_exists( 'AMP_Options_Manager', 'get_option' )
&& $exposes_support_mode;
} else {
$exposes_support_mode = class_exists( 'AMP_Theme_Support' )
&& method_exists( 'AMP_Theme_Support', 'get_support_mode' )
&& $exposes_support_mode;
}
if ( $exposes_support_mode ) {
// If recent version, we can properly detect the mode.
if ( $amp_plugin_version_2_or_higher ) {
$mode = AMP_Options_Manager::get_option( 'theme_support' );
} else {
$mode = AMP_Theme_Support::get_support_mode();
}
if (
in_array(
$mode,
array(
AMP_Theme_Support::STANDARD_MODE_SLUG,
AMP_Theme_Support::TRANSITIONAL_MODE_SLUG,
AMP_Theme_Support::READER_MODE_SLUG,
),
true
)
) {
return self::AMP_MODE_SECONDARY;
}
} elseif ( function_exists( 'amp_is_canonical' ) ) {
return self::AMP_MODE_SECONDARY;
}
return false;
}
/**
* Checks whether the plugin is network active.
*
* @since 1.0.0
*
* @return bool True if plugin is network active, false otherwise.
*/
public function is_network_active() {
// Determine $network_active property just once per request, to not unnecessarily run this complex logic on every call.
if ( null === $this->network_active ) {
if ( is_multisite() ) {
$network_active_plugins = wp_get_active_network_plugins();
// Consider MU plugins and network-activated plugins as network-active.
$this->network_active = strpos( wp_normalize_path( __FILE__ ), wp_normalize_path( WPMU_PLUGIN_DIR ) ) === 0
|| in_array( WP_PLUGIN_DIR . '/' . GOOGLESITEKIT_PLUGIN_BASENAME, $network_active_plugins, true );
} else {
$this->network_active = false;
}
}
return $this->network_active;
}
/**
* Filters the given entity's reference URL, effectively creating a copy of
* the entity with the reference URL accounted for.
*
* @since 1.15.0
*
* @param Entity|null $entity Entity to filter reference ID for, or null.
* @return Entity|null Filtered entity or null, based on $entity.
*/
private function filter_entity_reference_url( Entity $entity = null ) {
if ( ! $entity ) {
return null;
}
return new Entity(
$this->filter_reference_url( $entity->get_url() ),
array(
'type' => $entity->get_type(),
'title' => $entity->get_title(),
'id' => $entity->get_id(),
)
);
}
/**
* Filters the given URL to ensure the reference URL is used as part of it.
*
* If the site reference URL differs from the home URL (e.g. via filters),
* this method performs the necessary replacement.
*
* @since 1.7.0
*
* @param string $url Optional. Input URL. If not provided, returns the plain reference site URL.
* @return string URL that starts with the reference site URL.
*/
private function filter_reference_url( $url = '' ) {
$site_url = untrailingslashit( $this->get_canonical_home_url() );
/**
* Filters the reference site URL to use for stats.
*
* This can be used to override the current site URL, for example when using the plugin on a non-public site,
* such as in a staging environment.
*
* @since 1.0.0
*
* @param string $site_url Reference site URL, typically the WordPress home URL.
*/
$reference_site_url = apply_filters( 'googlesitekit_site_url', $site_url );
$reference_site_url = untrailingslashit( $reference_site_url );
// Ensure this is not empty.
if ( empty( $reference_site_url ) ) {
$reference_site_url = $site_url;
}
// If no URL given, just return the reference site URL.
if ( empty( $url ) ) {
return $reference_site_url;
}
// Replace site URL with the reference site URL.
if ( $reference_site_url !== $site_url ) {
$url = str_replace( $site_url, $reference_site_url, $url );
}
return $url;
}
/**
* Calls the WordPress core functions to get the locale and return it in the required format.
*
* @since 1.32.0
*
* @param string $context Optional. Defines which WordPress core locale function to call.
* @param string $format Optional. Defines the format the locale is returned in.
* @return string Locale in the required format.
*/
public function get_locale( $context = 'site', $format = 'default' ) {
// Get the site or user locale.
if ( 'user' === $context ) {
$wp_locale = get_user_locale();
} else {
$wp_locale = get_locale();
}
// Return locale in the required format.
if ( 'language-code' === $format ) {
$code_array = explode( '_', $wp_locale );
return $code_array[0];
} elseif ( 'language-variant' === $format ) {
$variant_array = explode( '_', $wp_locale );
$variant_string = implode( '_', array_slice( $variant_array, 0, 2 ) );
return $variant_string;
}
return $wp_locale;
}
}

View File

@ -0,0 +1,67 @@
<?php
/**
* Class Google\Site_Kit\Core\Admin\Available_Tools
*
* @package Google\Site_Kit\Core\Admin
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Core\Admin;
use Google\Site_Kit\Core\Permissions\Permissions;
use Google\Site_Kit\Core\Util\Method_Proxy_Trait;
use Google\Site_Kit\Core\Util\Reset;
/**
* Class for extending available tools for Site Kit.
*
* @since 1.30.0
* @access private
* @ignore
*/
class Available_Tools {
use Method_Proxy_Trait;
/**
* Registers functionality through WordPress hooks.
*
* @since 1.30.0
*/
public function register() {
add_action( 'tool_box', $this->get_method_proxy( 'render_tool_box' ) );
}
/**
* Renders tool box output.
*
* @since 1.30.0
*/
private function render_tool_box() {
if ( ! current_user_can( Permissions::SETUP ) ) {
return;
}
?>
<div class="card">
<h2 class="title"><?php esc_html_e( 'Reset Site Kit', 'google-site-kit' ); ?></h2>
<p>
<?php
esc_html_e(
'Resetting will disconnect all users and remove all Site Kit settings and data within WordPress. You and any other users who wish to use Site Kit will need to reconnect to restore access.',
'google-site-kit'
)
?>
</p>
<p>
<a
class="button button-primary"
href="<?php echo esc_url( Reset::url() ); ?>"
>
<?php esc_html_e( 'Reset Site Kit', 'google-site-kit' ); ?>
</a>
</p>
</div>
<?php
}
}

View File

@ -0,0 +1,124 @@
<?php
/**
* Class Google\Site_Kit\Core\Admin\Dashboard
*
* @package Google\Site_Kit
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Core\Admin;
use Google\Site_Kit\Context;
use Google\Site_Kit\Core\Assets\Assets;
use Google\Site_Kit\Core\Modules\Modules;
use Google\Site_Kit\Core\Permissions\Permissions;
use Google\Site_Kit\Core\Util\Requires_Javascript_Trait;
/**
* Class to handle all wp-admin Dashboard related functionality.
*
* @since 1.0.0
* @access private
* @ignore
*/
final class Dashboard {
use Requires_Javascript_Trait;
/**
* Plugin context.
*
* @since 1.0.0
* @var Context
*/
private $context;
/**
* Assets Instance.
*
* @since 1.0.0
* @var Assets
*/
private $assets;
/**
* Modules instance.
*
* @since 1.7.0
* @var Modules
*/
private $modules;
/**
* Constructor.
*
* @since 1.0.0
*
* @param Context $context Plugin context.
* @param Assets $assets Optional. Assets API instance. Default is a new instance.
* @param Modules $modules Optional. Modules instance. Default is a new instance.
*/
public function __construct(
Context $context,
Assets $assets = null,
Modules $modules = null
) {
$this->context = $context;
$this->assets = $assets ?: new Assets( $this->context );
$this->modules = $modules ?: new Modules( $this->context );
}
/**
* Registers functionality through WordPress hooks.
*
* @since 1.0.0
*/
public function register() {
add_action(
'wp_dashboard_setup',
function () {
$this->add_widgets();
}
);
}
/**
* Add a Site Kit by Google widget to the WordPress admin dashboard.
*
* @since 1.0.0
*/
private function add_widgets() {
if ( ! current_user_can( Permissions::VIEW_WP_DASHBOARD_WIDGET ) ) {
return;
}
// Enqueue styles.
$this->assets->enqueue_asset( 'googlesitekit-wp-dashboard-css' );
// Enqueue scripts.
$this->assets->enqueue_asset( 'googlesitekit-wp-dashboard' );
$this->modules->enqueue_assets();
wp_add_dashboard_widget(
'google_dashboard_widget',
__( 'Site Kit Summary', 'google-site-kit' ),
function () {
$this->render_googlesitekit_wp_dashboard();
}
);
}
/**
* Render the Site Kit WordPress Dashboard widget.
*
* @since 1.0.0
*/
private function render_googlesitekit_wp_dashboard() {
$this->render_noscript_html();
?>
<div id="js-googlesitekit-wp-dashboard" class="googlesitekit-plugin"></div>
<?php
}
}

View File

@ -0,0 +1,132 @@
<?php
/**
* Class Google\Site_Kit\Core\Admin\Notice
*
* @package Google\Site_Kit
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Core\Admin;
/**
* Class representing a single notice.
*
* @since 1.0.0
* @access private
* @ignore
*/
final class Notice {
const TYPE_SUCCESS = 'success';
const TYPE_INFO = 'info';
const TYPE_WARNING = 'warning';
const TYPE_ERROR = 'error';
/**
* Unique notice slug.
*
* @since 1.0.0
* @var string
*/
private $slug;
/**
* Notice arguments.
*
* @since 1.0.0
* @var array
*/
private $args = array();
/**
* Constructor.
*
* @since 1.0.0
*
* @param string $slug Unique notice slug.
* @param array $args {
* Associative array of notice arguments.
*
* @type string $content Required notice content. May contain inline HTML tags.
* @type string $type Notice type. Either 'success', 'info', 'warning', 'error'. Default 'info'.
* @type callable $active_callback Callback function to determine whether the notice is active in the
* current context. The current admin screen's hook suffix is passed to
* the callback. Default is that the notice is active unconditionally.
* @type bool $dismissible Whether the notice should be dismissible. Default false.
* }
*/
public function __construct( $slug, array $args ) {
$this->slug = $slug;
$this->args = wp_parse_args(
$args,
array(
'content' => '',
'type' => self::TYPE_INFO,
'active_callback' => null,
'dismissible' => false,
)
);
}
/**
* Gets the notice slug.
*
* @since 1.0.0
*
* @return string Unique notice slug.
*/
public function get_slug() {
return $this->slug;
}
/**
* Checks whether the notice is active.
*
* This method executes the active callback in order to determine whether the notice should be active or not.
*
* @since 1.0.0
*
* @param string $hook_suffix The current admin screen hook suffix.
* @return bool True if the notice is active, false otherwise.
*/
public function is_active( $hook_suffix ) {
if ( ! $this->args['content'] ) {
return false;
}
if ( ! $this->args['active_callback'] ) {
return true;
}
return (bool) call_user_func( $this->args['active_callback'], $hook_suffix );
}
/**
* Renders the notice.
*
* @since 1.0.0
*/
public function render() {
if ( is_callable( $this->args['content'] ) ) {
$content = call_user_func( $this->args['content'] );
if ( empty( $content ) ) {
return;
}
} else {
$content = '<p>' . wp_kses( $this->args['content'], 'googlesitekit_admin_notice' ) . '</p>';
}
$class = 'notice notice-' . $this->args['type'];
if ( $this->args['dismissible'] ) {
$class .= ' is-dismissible';
}
?>
<div id="<?php echo esc_attr( 'googlesitekit-notice-' . $this->slug ); ?>" class="<?php echo esc_attr( $class ); ?>">
<?php echo $content; /* phpcs:ignore WordPress.Security.EscapeOutput */ ?>
</div>
<?php
}
}

View File

@ -0,0 +1,93 @@
<?php
/**
* Class Google\Site_Kit\Core\Admin\Notices
*
* @package Google\Site_Kit
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Core\Admin;
/**
* Class managing admin notices.
*
* @since 1.0.0
* @access private
* @ignore
*/
final class Notices {
/**
* Registers functionality through WordPress hooks.
*
* @since 1.0.0
*/
public function register() {
$callback = function() {
global $hook_suffix;
if ( empty( $hook_suffix ) ) {
return;
}
$this->render_notices( $hook_suffix );
};
add_action( 'admin_notices', $callback );
add_action( 'network_admin_notices', $callback );
}
/**
* Renders admin notices.
*
* @since 1.0.0
*
* @param string $hook_suffix The current admin screen hook suffix.
*/
private function render_notices( $hook_suffix ) {
$notices = $this->get_notices();
if ( empty( $notices ) ) {
return;
}
/**
* Notice object.
*
* @var Notice $notice Notice object.
*/
foreach ( $notices as $notice ) {
if ( ! $notice->is_active( $hook_suffix ) ) {
continue;
}
$notice->render();
}
}
/**
* Gets available admin notices.
*
* @since 1.0.0
*
* @return array List of Notice instances.
*/
private function get_notices() {
/**
* Filters the list of available admin notices.
*
* @since 1.0.0
*
* @param array $notices List of Notice instances.
*/
$notices = apply_filters( 'googlesitekit_admin_notices', array() );
return array_filter(
$notices,
function( $notice ) {
return $notice instanceof Notice;
}
);
}
}

View File

@ -0,0 +1,72 @@
<?php
/**
* Class Google\Site_Kit\Core\Admin\Plugin_Action_Links
*
* @package Google\Site_Kit
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Core\Admin;
use Google\Site_Kit\Context;
use Google\Site_Kit\Core\Permissions\Permissions;
/**
* Class for managing plugin action links.
*
* @since 1.41.0
* @access private
* @ignore
*/
class Plugin_Action_Links {
/**
* Plugin context.
*
* @since 1.41.0
* @var Context
*/
private $context;
/**
* Constructor.
*
* @since 1.41.0
*
* @param Context $context Plugin context.
*/
public function __construct(
Context $context
) {
$this->context = $context;
}
/**
* Registers functionality through WordPress hooks.
*
* @since 1.41.0
*/
public function register() {
add_filter(
'plugin_action_links_' . GOOGLESITEKIT_PLUGIN_BASENAME,
function ( $links ) {
if ( current_user_can( Permissions::MANAGE_OPTIONS ) ) {
$settings_link = sprintf(
'<a href="%s">%s</a>',
esc_url( $this->context->admin_url( 'settings' ) ),
esc_html__( 'Settings', 'google-site-kit' )
);
array_unshift( $links, $settings_link );
};
return $links;
}
);
}
}

View File

@ -0,0 +1,55 @@
<?php
/**
* Class Google\Site_Kit\Core\Admin\Plugin_Row_Meta
*
* @package Google\Site_Kit
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Core\Admin;
/**
* Class for managing plugin row meta.
*
* @since 1.24.0
* @access private
* @ignore
*/
class Plugin_Row_Meta {
/**
* Registers functionality through WordPress hooks.
*
* @since 1.24.0
*/
public function register() {
add_filter(
'plugin_row_meta',
function ( $meta, $plugin_file ) {
if ( GOOGLESITEKIT_PLUGIN_BASENAME === $plugin_file ) {
return array_merge( $meta, $this->get_plugin_row_meta() );
}
return $meta;
},
10,
2
);
}
/**
* Builds an array of anchor elements to be shown in the plugin row.
*
* @since 1.24.0
*
* @return string[] Array of links as HTML strings.
*/
private function get_plugin_row_meta() {
return array(
'<a href="https://wordpress.org/support/plugin/google-site-kit/reviews/#new-post">' . __( 'Rate Site Kit', 'google-site-kit' ) . '</a>',
'<a href="https://wordpress.org/support/plugin/google-site-kit/#new-post">' . __( 'Support', 'google-site-kit' ) . '</a>',
);
}
}

View File

@ -0,0 +1,151 @@
<?php
/**
* Class Google\Site_Kit\Core\Admin\Pointer
*
* @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\Admin;
/**
* Class representing a single pointer.
*
* @since 1.83.0
* @access private
* @ignore
*/
final class Pointer {
/**
* Unique pointer slug.
*
* @since 1.83.0
* @var string
*/
private $slug;
/**
* Pointer arguments.
*
* @since 1.83.0
* @var array
*/
private $args = array();
/**
* Constructor.
*
* @since 1.83.0
*
* @param string $slug Unique pointer slug.
* @param array $args {
* Associative array of pointer arguments.
*
* @type string $title Required. Pointer title.
* @type string $content Required. Pointer content. May contain inline HTML tags.
* @type string $target_id Required. ID of the element the pointer should be attached to.
* @type string|array $position Optional. Position of the pointer. Can be 'top', 'bottom', 'left', 'right',
* or an array of `edge` and `align`. Default 'top'.
* @type callable $active_callback Optional. Callback function to determine whether the pointer is active in
* the current context. The current admin screen's hook suffix is passed to
* the callback. Default is that the pointer is active unconditionally.
* }
*/
public function __construct( $slug, array $args ) {
$this->slug = $slug;
$this->args = wp_parse_args(
$args,
array(
'title' => '',
'content' => '',
'target_id' => '',
'position' => 'top',
'active_callback' => null,
)
);
}
/**
* Gets the pointer slug.
*
* @since 1.83.0
*
* @return string Unique pointer slug.
*/
public function get_slug() {
return $this->slug;
}
/**
* Gets the pointer title.
*
* @since 1.83.0
*
* @return string Pointer title.
*/
public function get_title() {
return $this->args['title'];
}
/**
* Gets the pointer content.
*
* @since 1.83.0
*
* @return string Pointer content.
*/
public function get_content() {
if ( is_callable( $this->args['content'] ) ) {
return call_user_func( $this->args['content'] );
} else {
return '<p>' . wp_kses( $this->args['content'], 'googlesitekit_admin_pointer' ) . '</p>';
}
}
/**
* Gets the pointer target ID.
*
* @since 1.83.0
*
* @return string Pointer target ID.
*/
public function get_target_id() {
return $this->args['target_id'];
}
/**
* Gets the pointer position.
*
* @since 1.83.0
*
* @return string|array Pointer position.
*/
public function get_position() {
return $this->args['position'];
}
/**
* Checks whether the pointer is active.
*
* This method executes the active callback in order to determine whether the pointer should be active or not.
*
* @since 1.83.0
*
* @param string $hook_suffix The current admin screen hook suffix.
* @return bool True if the pointer is active, false otherwise.
*/
public function is_active( $hook_suffix ) {
if ( empty( $this->args['title'] ) || empty( $this->args['content'] ) || empty( $this->args['target_id'] ) ) {
return false;
}
if ( ! is_callable( $this->args['active_callback'] ) ) {
return true;
}
return (bool) call_user_func( $this->args['active_callback'], $hook_suffix );
}
}

View File

@ -0,0 +1,150 @@
<?php
/**
* Class Google\Site_Kit\Core\Admin\Pointers
*
* @package Google\Site_Kit\Core\Admin
* @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\Admin;
use Google\Site_Kit\Core\Util\BC_Functions;
use Google\Site_Kit\Core\Util\Method_Proxy_Trait;
/**
* Class for managing pointers.
*
* @since 1.83.0
* @access private
* @ignore
*/
class Pointers {
use Method_Proxy_Trait;
/**
* Registers functionality through WordPress hooks.
*
* @since 1.83.0
*/
public function register() {
add_action( 'admin_enqueue_scripts', $this->get_method_proxy( 'enqueue_pointers' ) );
}
/**
* Enqueues pointer scripts.
*
* @since 1.83.0
*
* @param string $hook_suffix The current admin page.
*/
private function enqueue_pointers( $hook_suffix ) {
if ( empty( $hook_suffix ) ) {
return;
}
$pointers = $this->get_pointers();
if ( empty( $pointers ) ) {
return;
}
$active_pointers = array_filter(
$pointers,
function( Pointer $pointer ) use ( $hook_suffix ) {
return $pointer->is_active( $hook_suffix );
}
);
if ( empty( $active_pointers ) ) {
return;
}
wp_enqueue_style( 'wp-pointer' );
wp_enqueue_script( 'wp-pointer' );
add_action(
'admin_print_footer_scripts',
function() use ( $active_pointers ) {
foreach ( $active_pointers as $pointer ) {
$this->print_pointer_script( $pointer );
}
}
);
}
/**
* Gets pointers.
*
* @since 1.83.0
*
* @return Pointer[] Array of pointers.
*/
private function get_pointers() {
/**
* Filters the list of available pointers.
*
* @since 1.83.0
*
* @param array $pointers List of Pointer instances.
*/
$pointers = apply_filters( 'googlesitekit_admin_pointers', array() );
return array_filter(
$pointers,
function( $pointer ) {
return $pointer instanceof Pointer;
}
);
}
/**
* Prints script for a given pointer.
*
* @since 1.83.0
*
* @param Pointer $pointer Pointer to print.
*/
private function print_pointer_script( $pointer ) {
$content = $pointer->get_content();
if ( empty( $content ) ) {
return;
}
$slug = $pointer->get_slug();
BC_Functions::wp_print_inline_script_tag(
sprintf(
'
jQuery( function() {
var options = {
content: "<h3>%s</h3>%s",
position: %s,
pointerWidth: 420,
close: function() {
jQuery.post(
window.ajaxurl,
{
pointer: "%s",
action: "dismiss-wp-pointer",
}
);
}
};
jQuery( "#%s" ).pointer( options ).pointer( "open" );
} );
',
esc_js( $pointer->get_title() ),
$content,
wp_json_encode( $pointer->get_position() ),
esc_js( $slug ),
esc_js( $pointer->get_target_id() )
),
array(
'id' => $slug,
)
);
}
}

View File

@ -0,0 +1,260 @@
<?php
/**
* Class Google\Site_Kit\Core\Admin\Screen
*
* @package Google\Site_Kit
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Core\Admin;
use Google\Site_Kit\Context;
use Google\Site_Kit\Core\Assets\Assets;
use Google\Site_Kit\Core\Util\Google_Icon;
use Google\Site_Kit\Core\Util\Requires_Javascript_Trait;
/**
* Class representing a single screen.
*
* @since 1.0.0
* @access private
* @ignore
*/
final class Screen {
use Requires_Javascript_Trait;
const MENU_SLUG = 'googlesitekit';
/**
* Unique screen slug.
*
* @since 1.0.0
* @var string
*/
private $slug;
/**
* Screen arguments.
*
* @since 1.0.0
* @var array
*/
private $args = array();
/**
* Constructor.
*
* @since 1.0.0
*
* @param string $slug Unique screen slug.
* @param array $args {
* Associative array of screen arguments.
*
* @type callable $render_callback Required callback to render the page content.
* @type string $title Required screen title.
* @type string $capability Capability required to access the screen. Default is 'manage_options'.
* @type string $menu_title Title to display in the menu (only if $add_to_menu is true). Default is
* the value of $title.
* @type string $parent_slug Slug of the parent menu screen (only if $add_to_menu is true). Default
* empty string (which means it will be a top-level page).
* @type callable $enqueue_callback Callback to enqueue additional scripts or stylesheets. The base admin
* script and stylesheet will always be enqueued. Default null.
* @type callable $initialize_callback Callback to run actions when initializing the screen, before headers are
* sent and markup is generated. Default null.
* }
*/
public function __construct( $slug, array $args ) {
$this->slug = $slug;
$this->args = wp_parse_args(
$args,
array(
'render_callback' => null,
'title' => '',
'capability' => 'manage_options',
'menu_title' => '',
'parent_slug' => self::MENU_SLUG,
'enqueue_callback' => null,
'initialize_callback' => null,
)
);
if ( empty( $this->args['menu_title'] ) ) {
$this->args['menu_title'] = $this->args['title'];
}
$this->args['title'] = __( 'Site Kit by Google', 'google-site-kit' ) . ' ' . $this->args['title'];
}
/**
* Gets the unique screen slug.
*
* @since 1.0.0
*
* @return string Unique screen slug.
*/
public function get_slug() {
return $this->slug;
}
/**
* Adds the screen to the WordPress admin backend.
*
* @since 1.0.0
*
* @param Context $context Plugin context, used for URL generation.
* @return string Hook suffix of the screen, or empty string if not added.
*/
public function add( Context $context ) {
static $menu_slug = null;
if ( ! $this->args['render_callback'] || ! $this->args['title'] ) {
return '';
}
// A parent slug of null means the screen will not appear in the menu.
$parent_slug = null;
// If parent slug is provided, use it as parent.
if ( ! empty( $this->args['parent_slug'] ) ) {
$parent_slug = $this->args['parent_slug'];
// If parent slug is 'googlesitekit', append to main Site Kit menu.
if ( self::MENU_SLUG === $parent_slug ) {
// If this is null, it means no menu has been added yet.
if ( null === $menu_slug ) {
add_menu_page(
$this->args['title'],
__( 'Site Kit', 'google-site-kit' ),
$this->args['capability'],
$this->slug,
'',
'data:image/svg+xml;base64,' . Google_Icon::to_base64()
);
$menu_slug = $this->slug;
/**
* An SVG icon file needs to be colored (filled) based on the theme color setting.
*
* This exists in js as wp.svgPainter() per:
* https://github.com/WordPress/WordPress/blob/5.7/wp-admin/js/svg-painter.js
*
* The downside of the js approach is that we get a brief flash of an unstyled icon
* until the JS runs.
*
* A user can pick a custom Admin Color Scheme, which is only available in admin_init
* or later actions. add_menu_page runs on the admin_menu action, which precedes admin_init
* per https://codex.wordpress.org/Plugin_API/Action_Reference
*
* WordPress provides some color schemes out of the box, but they can also be added via
* wp_admin_css_color()
*
* Our workaround is to set the icon and subsequently replace it in current_screen, which is
* what we do in the following action.
*/
add_action(
'current_screen',
function() {
global $menu, $_wp_admin_css_colors;
if ( ! is_array( $menu ) ) {
return;
}
$color_scheme = get_user_option( 'admin_color' ) ?: 'fresh';
// If we're on one of the sitekit pages, use the 'current' color, otherwise use the 'base' color.
// @see wp_admin_css_color().
$color_key = false === strpos( get_current_screen()->id, 'googlesitekit' ) ? 'base' : 'current';
if ( empty( $_wp_admin_css_colors[ $color_scheme ]->icon_colors[ $color_key ] ) ) {
return;
}
$color = $_wp_admin_css_colors[ $color_scheme ]->icon_colors[ $color_key ];
foreach ( $menu as &$item ) {
if ( 'googlesitekit-dashboard' === $item[2] ) {
$item[6] = 'data:image/svg+xml;base64,' . Google_Icon::to_base64( Google_Icon::with_fill( $color ) );
break;
}
}
},
100
);
}
// Set parent slug to actual slug of main Site Kit menu.
$parent_slug = $menu_slug;
}
}
// If submenu item or not in menu, use add_submenu_page().
return (string) add_submenu_page(
$parent_slug,
$this->args['title'],
$this->args['menu_title'],
$this->args['capability'],
$this->slug,
function() use ( $context ) {
$this->render( $context );
}
);
}
/**
* Runs actions when initializing the screen, before sending headers and generating markup.
*
* @since 1.0.0
*
* @param Context $context Plugin context.
*/
public function initialize( Context $context ) {
if ( ! $this->args['initialize_callback'] ) {
return;
}
call_user_func( $this->args['initialize_callback'], $context );
}
/**
* Enqueues assets for the screen.
*
* @since 1.0.0
*
* @param Assets $assets Assets instance to rely on for enqueueing assets.
*/
public function enqueue_assets( Assets $assets ) {
// Enqueue base admin screen stylesheet.
$assets->enqueue_asset( 'googlesitekit-admin-css' );
if ( $this->args['enqueue_callback'] ) {
call_user_func( $this->args['enqueue_callback'], $assets );
}
}
/**
* Renders the screen content.
*
* @since 1.0.0
*
* @param Context $context Plugin context.
*/
private function render( Context $context ) {
if ( ! $this->args['render_callback'] ) {
return;
}
?>
<div class="googlesitekit-plugin">
<?php
$this->render_noscript_html();
call_user_func( $this->args['render_callback'], $context );
?>
</div>
<?php
}
}

View File

@ -0,0 +1,512 @@
<?php
/**
* Class Google\Site_Kit\Core\Admin\Screens
*
* @package Google\Site_Kit
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Core\Admin;
use Google\Site_Kit\Context;
use Google\Site_Kit\Core\Assets\Assets;
use Google\Site_Kit\Core\Authentication\Authentication;
use Google\Site_Kit\Core\Dismissals\Dismissed_Items;
use Google\Site_Kit\Core\Modules\Modules;
use Google\Site_Kit\Core\Permissions\Permissions;
use Google\Site_Kit\Core\Storage\User_Options;
use Google\Site_Kit\Core\Util\Feature_Flags;
/**
* Class managing admin screens.
*
* @since 1.0.0
* @access private
* @ignore
*/
final class Screens {
const PREFIX = 'googlesitekit-';
/**
* Plugin context.
*
* @since 1.0.0
* @var Context
*/
private $context;
/**
* Assets API instance.
*
* @since 1.0.0
* @var Assets
*/
private $assets;
/**
* Modules instance.
*
* @since 1.7.0
* @var Modules
*/
private $modules;
/**
* Authentication instance.
*
* @since 1.72.0
* @var Authentication
*/
private $authentication;
/**
* Associative array of $hook_suffix => $screen pairs.
*
* @since 1.0.0
* @var array
*/
private $screens = array();
/**
* Constructor.
*
* @since 1.0.0
*
* @param Context $context Plugin context.
* @param Assets $assets Optional. Assets API instance. Default is a new instance.
* @param Modules $modules Optional. Modules instance. Default is a new instance.
* @param Authentication $authentication Optional. Authentication instance. Default is a new instance.
*/
public function __construct(
Context $context,
Assets $assets = null,
Modules $modules = null,
Authentication $authentication = null
) {
$this->context = $context;
$this->assets = $assets ?: new Assets( $this->context );
$this->modules = $modules ?: new Modules( $this->context );
$this->authentication = $authentication ?: new Authentication( $this->context );
}
/**
* Registers functionality through WordPress hooks.
*
* @since 1.0.0
*/
public function register() {
if ( $this->context->is_network_mode() ) {
add_action(
'network_admin_menu',
function() {
$this->add_screens();
}
);
}
add_action(
'admin_menu',
function() {
$this->add_screens();
}
);
add_action(
'admin_enqueue_scripts',
function( $hook_suffix ) {
$this->enqueue_screen_assets( $hook_suffix );
}
);
add_action(
'admin_page_access_denied',
function() {
// Redirect dashboard to splash if no dashboard access (yet).
$this->no_access_redirect_dashboard_to_splash();
// Redirect splash to (shared) dashboard if splash is dismissed.
$this->no_access_redirect_splash_to_dashboard();
// Redirect module pages to dashboard.
$this->no_access_redirect_module_to_dashboard();
}
);
// Ensure the menu icon always is rendered correctly, without enqueueing a global CSS file.
add_action(
'admin_head',
function() {
?>
<style type="text/css">
#adminmenu .toplevel_page_googlesitekit-dashboard img {
width: 16px;
}
#adminmenu .toplevel_page_googlesitekit-dashboard.current img,
#adminmenu .toplevel_page_googlesitekit-dashboard.wp-has-current-submenu img {
opacity: 1;
}
</style>
<?php
}
);
$remove_notices_callback = function() {
global $hook_suffix;
if ( empty( $hook_suffix ) ) {
return;
}
if ( isset( $this->screens[ $hook_suffix ] ) ) {
remove_all_actions( current_action() );
}
};
add_action( 'admin_notices', $remove_notices_callback, -9999 );
add_action( 'network_admin_notices', $remove_notices_callback, -9999 );
add_action( 'all_admin_notices', $remove_notices_callback, -9999 );
add_filter( 'custom_menu_order', '__return_true' );
add_filter(
'menu_order',
function( array $menu_order ) {
// Move the Site Kit dashboard menu item to be one after the index.php item if it exists.
$dashboard_index = array_search( 'index.php', $menu_order, true );
$sitekit_index = false;
foreach ( $menu_order as $key => $value ) {
if ( strpos( $value, self::PREFIX ) === 0 ) {
$sitekit_index = $key;
$sitekit_value = $value;
break;
}
}
if ( false === $dashboard_index || false === $sitekit_index ) {
return $menu_order;
}
unset( $menu_order[ $sitekit_index ] );
array_splice( $menu_order, $dashboard_index + 1, 0, $sitekit_value );
return $menu_order;
}
);
}
/**
* Gets the Screen instance for a given hook suffix.
*
* @since 1.11.0
*
* @param string $hook_suffix The hook suffix associated with the screen to retrieve.
* @return Screen|null Screen instance if available, otherwise null;
*/
public function get_screen( $hook_suffix ) {
return isset( $this->screens[ $hook_suffix ] ) ? $this->screens[ $hook_suffix ] : null;
}
/**
* Adds all screens to the admin.
*
* @since 1.0.0
*/
private function add_screens() {
$screens = $this->get_screens();
array_walk( $screens, array( $this, 'add_screen' ) );
}
/**
* Adds the given screen to the admin.
*
* @since 1.0.0
*
* @param Screen $screen Screen to add.
*/
private function add_screen( Screen $screen ) {
$hook_suffix = $screen->add( $this->context );
if ( empty( $hook_suffix ) ) {
return;
}
add_action(
"load-{$hook_suffix}",
function() use ( $screen ) {
$screen->initialize( $this->context );
}
);
$this->screens[ $hook_suffix ] = $screen;
}
/**
* Enqueues assets if a plugin screen matches the given hook suffix.
*
* @since 1.0.0
*
* @param string $hook_suffix Hook suffix for the current admin screen.
*/
private function enqueue_screen_assets( $hook_suffix ) {
if ( ! isset( $this->screens[ $hook_suffix ] ) ) {
return;
}
$this->screens[ $hook_suffix ]->enqueue_assets( $this->assets );
$this->modules->enqueue_assets();
}
/**
* Redirects from the dashboard to the splash screen if permissions to access the dashboard are currently not met.
*
* Dashboard permission access is conditional based on whether the user has successfully authenticated. When
* e.g. accessing the dashboard manually or having it open in a separate tab while disconnecting in the other tab,
* it is a better user experience to redirect to the splash screen so that the user can re-authenticate.
*
* The only time the dashboard should fail with the regular WordPress permissions error is when the current user is
* not eligible for accessing Site Kit entirely, i.e. if they are not allowed to authenticate.
*
* @since 1.12.0
*/
private function no_access_redirect_dashboard_to_splash() {
global $plugin_page;
// At this point, our preferred `$hook_suffix` is not set, and the dashboard page will not even be registered,
// so we need to rely on the `$plugin_page` global here.
if ( ! isset( $plugin_page ) || self::PREFIX . 'dashboard' !== $plugin_page ) {
return;
}
if ( current_user_can( Permissions::VIEW_SPLASH ) ) {
wp_safe_redirect(
$this->context->admin_url( 'splash' )
);
exit;
}
}
/**
* Redirects from the splash to the dashboard screen if permissions to access the splash are currently not met.
*
* Admins always have the ability to view the splash page, so this redirects non-admins who have access
* to view the shared dashboard if the splash has been dismissed.
* Currently the dismissal check is built into the capability for VIEW_SPLASH so this is implied.
*
* @since 1.77.0
*/
private function no_access_redirect_splash_to_dashboard() {
global $plugin_page;
if ( ! isset( $plugin_page ) || self::PREFIX . 'splash' !== $plugin_page ) {
return;
}
if ( current_user_can( Permissions::VIEW_DASHBOARD ) ) {
wp_safe_redirect(
$this->context->admin_url()
);
exit;
}
}
/**
* Redirects module pages to the dashboard or splash based on user capability.
*
* @since 1.69.0
*/
private function no_access_redirect_module_to_dashboard() {
global $plugin_page;
$legacy_module_pages = array(
self::PREFIX . 'module-adsense',
self::PREFIX . 'module-analytics',
self::PREFIX . 'module-search-console',
);
if ( ! in_array( $plugin_page, $legacy_module_pages, true ) ) {
return;
}
// Note: the use of add_query_arg is intentional below because it preserves
// the current query parameters in the URL.
if ( current_user_can( Permissions::VIEW_DASHBOARD ) ) {
wp_safe_redirect(
add_query_arg( 'page', self::PREFIX . 'dashboard' )
);
exit;
}
if ( current_user_can( Permissions::VIEW_SPLASH ) ) {
wp_safe_redirect(
add_query_arg( 'page', self::PREFIX . 'splash' )
);
exit;
}
}
/**
* Gets available admin screens.
*
* @since 1.0.0
*
* @return array List of Screen instances.
*/
private function get_screens() {
$screens = array(
new Screen(
self::PREFIX . 'dashboard',
array(
'title' => __( 'Dashboard', 'google-site-kit' ),
'capability' => Permissions::VIEW_DASHBOARD,
'enqueue_callback' => function( Assets $assets ) {
if ( $this->context->input()->filter( INPUT_GET, 'permaLink' ) ) {
$assets->enqueue_asset( 'googlesitekit-entity-dashboard' );
} else {
$assets->enqueue_asset( 'googlesitekit-main-dashboard' );
}
},
'render_callback' => function( Context $context ) {
$is_view_only = ! $this->authentication->is_authenticated();
$setup_slug = $context->input()->filter( INPUT_GET, 'slug', FILTER_SANITIZE_STRING );
$reauth = $context->input()->filter( INPUT_GET, 'reAuth', FILTER_VALIDATE_BOOLEAN );
if ( $context->input()->filter( INPUT_GET, 'permaLink' ) ) {
?>
<div id="js-googlesitekit-entity-dashboard" data-view-only="<?php echo esc_attr( $is_view_only ); ?>" class="googlesitekit-page"></div>
<?php
} else {
$setup_module_slug = $setup_slug && $reauth ? $setup_slug : '';
if ( $setup_module_slug ) {
$active_modules = $this->modules->get_active_modules();
if ( ! array_key_exists( $setup_module_slug, $active_modules ) ) {
try {
$module_details = $this->modules->get_module( $setup_module_slug );
/* translators: %s: The module name */
$message = sprintf( __( 'The %s module cannot be set up as it has not been activated yet.', 'google-site-kit' ), $module_details->name );
} catch ( \Exception $e ) {
$message = $e->getMessage();
}
wp_die( sprintf( '<span class="googlesitekit-notice">%s</span>', esc_html( $message ) ), 403 );
}
}
?>
<div id="js-googlesitekit-main-dashboard" data-view-only="<?php echo esc_attr( $is_view_only ); ?>" data-setup-module-slug="<?php echo esc_attr( $setup_module_slug ); ?>" class="googlesitekit-page"></div>
<?php
}
},
)
),
);
$show_splash_in_menu = current_user_can( Permissions::VIEW_SPLASH ) && ! current_user_can( Permissions::VIEW_DASHBOARD );
$screens[] = new Screen(
self::PREFIX . 'splash',
array(
'title' => __( 'Dashboard', 'google-site-kit' ),
'capability' => Permissions::VIEW_SPLASH,
'parent_slug' => $show_splash_in_menu ? Screen::MENU_SLUG : null,
// This callback will redirect to the dashboard on successful authentication.
'initialize_callback' => function( Context $context ) {
if ( Feature_Flags::enabled( 'dashboardSharing' ) ) {
// Get the dismissed items for this user.
$user_options = new User_Options( $context );
$dismissed_items = new Dismissed_Items( $user_options );
}
$splash_context = $context->input()->filter( INPUT_GET, 'googlesitekit_context' );
$reset_session = $context->input()->filter( INPUT_GET, 'googlesitekit_reset_session', FILTER_VALIDATE_BOOLEAN );
// If the user is authenticated, redirect them to the disconnect URL and then send them back here.
if ( ! $reset_session && 'revoked' === $splash_context && $this->authentication->is_authenticated() ) {
$this->authentication->disconnect();
wp_safe_redirect( add_query_arg( array( 'googlesitekit_reset_session' => 1 ) ) );
exit;
}
// Don't consider redirect if the current user cannot access the dashboard (yet).
if ( ! current_user_can( Permissions::VIEW_DASHBOARD ) ) {
return;
}
// Redirect to dashboard if user is authenticated or if
// they have already accessed the shared dashboard.
if (
$this->authentication->is_authenticated() ||
(
Feature_Flags::enabled( 'dashboardSharing' ) &&
! current_user_can( Permissions::AUTHENTICATE ) &&
$dismissed_items->is_dismissed( 'shared_dashboard_splash' ) &&
current_user_can( Permissions::VIEW_SHARED_DASHBOARD )
)
) {
wp_safe_redirect(
$context->admin_url(
'dashboard',
array(
// Pass through the notification parameter, or removes it if none.
'notification' => $context->input()->filter( INPUT_GET, 'notification' ),
)
)
);
exit;
}
},
'enqueue_callback' => function( Assets $assets ) {
$assets->enqueue_asset( 'googlesitekit-splash' );
},
'render_callback' => function( Context $context ) {
?>
<div id="js-googlesitekit-splash" class="googlesitekit-page"></div>
<?php
},
)
);
$screens[] = new Screen(
self::PREFIX . 'settings',
array(
'title' => __( 'Settings', 'google-site-kit' ),
'capability' => Permissions::MANAGE_OPTIONS,
'enqueue_callback' => function( Assets $assets ) {
$assets->enqueue_asset( 'googlesitekit-settings' );
},
'render_callback' => function( Context $context ) {
?>
<div id="googlesitekit-settings-wrapper" class="googlesitekit-page"></div>
<?php
},
)
);
$screens[] = new Screen(
self::PREFIX . 'user-input',
array(
'title' => __( 'User Input', 'google-site-kit' ),
'capability' => Permissions::MANAGE_OPTIONS,
'parent_slug' => null,
'enqueue_callback' => function( Assets $assets ) {
$assets->enqueue_asset( 'googlesitekit-user-input' );
},
'render_callback' => function( Context $context ) {
?>
<div id="js-googlesitekit-user-input" class="googlesitekit-page"></div>
<?php
},
)
);
return $screens;
}
}

View File

@ -0,0 +1,123 @@
<?php
/**
* Class Google\Site_Kit\Core\Admin\Standalone
*
* @package Google\Site_Kit
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Core\Admin;
use Google\Site_Kit\Context;
use Google\Site_Kit\Core\Assets\Stylesheet;
/**
* Class managing standalone mode.
*
* @since 1.8.0
* @access private
* @ignore
*/
final class Standalone {
/**
* Plugin context.
*
* @since 1.8.0
*
* @var Context
*/
private $context;
/**
* Constructor.
*
* @since 1.8.0
*
* @param Context $context Plugin context.
*/
public function __construct( Context $context ) {
$this->context = $context;
}
/**
* Standalone mode
*
* @since 1.8.0
*/
public function register() {
if ( ! $this->is_standalone() ) {
return;
}
/**
* Appends the standalone admin body class.
*
* @since 1.8.0
*
* @param string $admin_body_classes Admin body classes.
* @return string Filtered admin body classes.
*/
add_filter(
'admin_body_class',
function( $admin_body_classes ) {
return "{$admin_body_classes} googlesitekit-standalone";
}
);
remove_action( 'in_admin_header', 'wp_admin_bar_render', 0 );
add_filter( 'admin_footer_text', '__return_empty_string', PHP_INT_MAX );
add_filter( 'update_footer', '__return_empty_string', PHP_INT_MAX );
add_action(
'admin_head',
function() {
$this->print_standalone_styles();
}
);
}
/**
* Detects if we are in Google Site Kit standalone mode.
*
* @since 1.8.0
*
* @return boolean True when in standalone mode, else false.
*/
public function is_standalone() {
global $pagenow;
$page = $this->context->input()->filter( INPUT_GET, 'page', FILTER_SANITIZE_STRING );
$standalone = $this->context->input()->filter( INPUT_GET, 'googlesitekit-standalone', FILTER_VALIDATE_BOOLEAN );
return ( 'admin.php' === $pagenow && false !== strpos( $page, 'googlesitekit' ) && $standalone );
}
/**
* Enqueues styles for standalone mode.
*
* @since 1.8.0
*/
private function print_standalone_styles() {
?>
<style type="text/css">
html {
padding-top: 0 !important;
}
body.googlesitekit-standalone #adminmenumain {
display: none;
}
body.googlesitekit-standalone #wpcontent {
margin-left: 0;
}
</style>
<?php
}
}

View File

@ -0,0 +1,393 @@
<?php
/**
* Class Google\Site_Kit\Core\Admin_Bar\Admin_Bar
*
* @package Google\Site_Kit
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Core\Admin_Bar;
use Google\Site_Kit\Context;
use Google\Site_Kit\Core\Modules\Modules;
use Google\Site_Kit\Core\Permissions\Permissions;
use Google\Site_Kit\Core\Assets\Assets;
use Google\Site_Kit\Core\REST_API\REST_Route;
use Google\Site_Kit\Core\REST_API\REST_Routes;
use Google\Site_Kit\Core\Storage\Options;
use Google\Site_Kit\Core\Util\Method_Proxy_Trait;
use Google\Site_Kit\Core\Util\Requires_Javascript_Trait;
use WP_REST_Server;
use WP_REST_Request;
/**
* Class handling the plugin's admin bar menu.
*
* @since 1.0.0
* @access private
* @ignore
*/
final class Admin_Bar {
use Requires_Javascript_Trait, Method_Proxy_Trait;
/**
* Plugin context.
*
* @since 1.0.0
* @var Context
*/
private $context;
/**
* Assets Instance.
*
* @since 1.0.0
* @var Assets
*/
private $assets;
/**
* Modules instance.
*
* @since 1.4.0
* @var Modules
*/
private $modules;
/**
* Admin_Bar_Enabled instance.
*
* @since 1.39.0
* @var Admin_Bar_Enabled
*/
private $admin_bar_enabled;
/**
* Constructor.
*
* @since 1.0.0
*
* @param Context $context Plugin context.
* @param Assets $assets Optional. Assets API instance. Default is a new instance.
* @param Modules $modules Optional. Modules instance. Default is a new instance.
*/
public function __construct(
Context $context,
Assets $assets = null,
Modules $modules = null
) {
$this->context = $context;
$this->assets = $assets ?: new Assets( $this->context );
$this->modules = $modules ?: new Modules( $this->context );
$options = new Options( $this->context );
$this->admin_bar_enabled = new Admin_Bar_Enabled( $options );
}
/**
* Registers functionality through WordPress hooks.
*
* @since 1.0.0
*/
public function register() {
add_action( 'admin_bar_menu', $this->get_method_proxy( 'add_menu_button' ), 99 );
add_action( 'admin_enqueue_scripts', $this->get_method_proxy( 'enqueue_assets' ), 40 );
add_action( 'wp_enqueue_scripts', $this->get_method_proxy( 'enqueue_assets' ), 40 );
// TODO: This can be removed at some point, see https://github.com/ampproject/amp-wp/pull/4001.
add_filter( 'amp_dev_mode_element_xpaths', array( $this, 'add_amp_dev_mode' ) );
add_filter(
'googlesitekit_rest_routes',
function( $routes ) {
return array_merge( $routes, $this->get_rest_routes() );
}
);
add_filter(
'googlesitekit_apifetch_preload_paths',
function( $routes ) {
return array_merge(
$routes,
array(
'/' . REST_Routes::REST_ROOT . '/core/site/data/admin-bar-settings',
)
);
}
);
$this->admin_bar_enabled->register();
}
/**
* Add data-ampdevmode attributes to the elements that need it.
*
* @see \Google\Site_Kit\Core\Assets\Assets::get_assets() The 'googlesitekit' string is added to all inline scripts.
* @see \Google\Site_Kit\Core\Assets\Assets::add_amp_dev_mode_attributes() The data-ampdevmode attribute is added to registered scripts/styles here.
*
* @param string[] $xpath_queries XPath queries for elements that should get the data-ampdevmode attribute.
* @return string[] XPath queries.
*/
public function add_amp_dev_mode( $xpath_queries ) {
$xpath_queries[] = '//script[ contains( text(), "googlesitekit" ) ]';
return $xpath_queries;
}
/**
* Render the Adminbar button.
*
* @since 1.0.0
*
* @param object $wp_admin_bar The WP AdminBar object.
*/
private function add_menu_button( $wp_admin_bar ) {
if ( ! $this->is_active() ) {
return;
}
$args = array(
'id' => 'google-site-kit',
'title' => '<span class="googlesitekit-wp-adminbar__icon"></span> <span class="googlesitekit-wp-adminbar__label">Site Kit</span>',
'href' => '#',
'meta' => array(
'class' => 'menupop googlesitekit-wp-adminbar',
),
);
if ( $this->context->is_amp() && ! $this->is_amp_dev_mode() ) {
$post = get_post();
if ( ! $post || ! current_user_can( 'edit_post', $post->ID ) ) {
return;
}
$args['href'] = add_query_arg( 'googlesitekit_adminbar_open', 'true', get_edit_post_link( $post->ID ) );
} else {
$args['meta']['html'] = $this->menu_markup();
}
$wp_admin_bar->add_node( $args );
}
/**
* Checks if admin bar menu is active and displaying.
*
* @since 1.0.0
*
* @return bool True if Admin bar should display, False when it's not.
*/
public function is_active() {
// Only active if the admin bar is showing.
if ( ! is_admin_bar_showing() ) {
return false;
}
// In the admin, never show the admin bar except for the post editing screen.
if ( is_admin() && ! $this->is_admin_post_screen() ) {
return false;
}
if ( ! current_user_can( Permissions::VIEW_ADMIN_BAR_MENU ) ) {
return false;
}
$enabled = $this->admin_bar_enabled->get();
if ( ! $enabled ) {
return false;
}
// No entity was identified - don't display the admin bar menu.
$entity = $this->context->get_reference_entity();
if ( ! $entity ) {
return false;
}
// Check permissions for viewing post data.
if ( in_array( $entity->get_type(), array( 'post', 'blog' ), true ) && $entity->get_id() ) {
// If a post entity, check permissions for that post.
if ( ! current_user_can( Permissions::VIEW_POST_INSIGHTS, $entity->get_id() ) ) {
return false;
}
}
$current_url = $entity->get_url();
/**
* Filters whether the Site Kit admin bar menu should be displayed.
*
* The admin bar menu is only shown when there is data for the current URL and the current
* user has the correct capability to view the data. Modules use this filter to indicate the
* presence of valid data.
*
* @since 1.0.0
*
* @param bool $display Whether to display the admin bar menu.
* @param string $current_url The URL of the current request.
*/
return apply_filters( 'googlesitekit_show_admin_bar_menu', true, $current_url );
}
/**
* Checks if current screen is an admin edit post screen.
*
* @since 1.0.0
*/
private function is_admin_post_screen() {
$current_screen = function_exists( 'get_current_screen' ) ? get_current_screen() : false;
// No screen context available.
if ( ! $current_screen instanceof \WP_Screen ) {
return false;
}
// Only show for post screens.
if ( 'post' !== $current_screen->base ) {
return false;
}
// Don't show for new post screen.
if ( 'add' === $current_screen->action ) {
return false;
}
return true;
}
/**
* Checks whether AMP dev mode is enabled.
*
* This is only relevant if the current context is AMP.
*
* @since 1.1.0
*
* @return bool True if AMP dev mode is enabled, false otherwise.
*/
private function is_amp_dev_mode() {
return function_exists( 'amp_is_dev_mode' ) && amp_is_dev_mode();
}
/**
* Return the Adminbar content markup.
*
* @since 1.0.0
*/
private function menu_markup() {
// Start buffer output.
ob_start();
?>
<div class="googlesitekit-plugin ab-sub-wrapper">
<?php $this->render_noscript_html(); ?>
<div id="js-googlesitekit-adminbar" class="googlesitekit-adminbar">
<?php
/**
* Display server rendered content before JS-based adminbar modules.
*
* @since 1.0.0
*/
do_action( 'googlesitekit_adminbar_modules_before' );
?>
<section id="js-googlesitekit-adminbar-modules" class="googlesitekit-adminbar-modules"></section>
<?php
/**
* Display server rendered content after JS-based adminbar modules.
*
* @since 1.0.0
*/
do_action( 'googlesitekit_adminbar_modules_after' );
?>
</div>
</div>
<?php
// Get the buffer output.
$markup = ob_get_clean();
return $markup;
}
/**
* Enqueues assets.
*
* @since 1.39.0
*/
private function enqueue_assets() {
if ( ! $this->is_active() ) {
return;
}
// Enqueue styles.
$this->assets->enqueue_asset( 'googlesitekit-adminbar-css' );
if ( $this->context->is_amp() && ! $this->is_amp_dev_mode() ) {
// AMP Dev Mode support was added in v1.4, and if it is not enabled then short-circuit since scripts will be invalid.
return;
}
// Enqueue scripts.
$this->assets->enqueue_asset( 'googlesitekit-adminbar' );
$this->modules->enqueue_assets();
}
/**
* Gets related REST routes.
*
* @since 1.39.0
*
* @return array List of REST_Route objects.
*/
private function get_rest_routes() {
$can_authenticate = function() {
return current_user_can( Permissions::AUTHENTICATE );
};
$settings_callback = function() {
return array(
'enabled' => $this->admin_bar_enabled->get(),
);
};
return array(
new REST_Route(
'core/site/data/admin-bar-settings',
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => $settings_callback,
'permission_callback' => $can_authenticate,
),
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => function( WP_REST_Request $request ) use ( $settings_callback ) {
$data = $request->get_param( 'data' );
if ( isset( $data['enabled'] ) ) {
$this->admin_bar_enabled->set( ! empty( $data['enabled'] ) );
}
return $settings_callback( $request );
},
'permission_callback' => $can_authenticate,
'args' => array(
'data' => array(
'type' => 'object',
'required' => true,
'properties' => array(
'enabled' => array(
'type' => 'boolean',
'required' => false,
),
),
),
),
),
)
),
);
}
}

View File

@ -0,0 +1,73 @@
<?php
/**
* Class Google\Site_Kit\Core\Admin_Bar\Admin_Bar_Enabled
*
* @package Google\Site_Kit
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Core\Admin_Bar;
use Google\Site_Kit\Core\Storage\Setting;
/**
* Class handling the admin bar menu settings.
*
* @since 1.39.0
* @access private
* @ignore
*/
class Admin_Bar_Enabled extends Setting {
/**
* The option_name for this setting.
*/
const OPTION = 'googlesitekit_admin_bar_menu_enabled';
/**
* Gets the value of the setting.
*
* @since 1.39.0
*
* @return bool Value set for the option, or registered default if not set.
*/
public function get() {
return (bool) parent::get();
}
/**
* Gets the expected value type.
*
* @since 1.39.0
*
* @return string The type name.
*/
protected function get_type() {
return 'boolean';
}
/**
* Gets the default value.
*
* @since 1.39.0
*
* @return boolean The default value.
*/
protected function get_default() {
return true;
}
/**
* Gets the callback for sanitizing the setting's value before saving.
*
* @since 1.39.0
*
* @return callable The callable sanitize callback.
*/
protected function get_sanitize_callback() {
return 'boolval';
}
}

View File

@ -0,0 +1,131 @@
<?php
/**
* Class Google\Site_Kit\Core\Assets\Asset
*
* @package Google\Site_Kit
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Core\Assets;
use Google\Site_Kit\Context;
/**
* Class representing a single asset.
*
* @since 1.0.0
* @access private
* @ignore
*/
abstract class Asset {
// Various page contexts for Site Kit in the WordPress Admin.
const CONTEXT_ADMIN_GLOBAL = 'admin-global';
const CONTEXT_ADMIN_POST_EDITOR = 'admin-post-editor';
const CONTEXT_ADMIN_POSTS = 'admin-posts';
const CONTEXT_ADMIN_SITEKIT = 'admin-sitekit';
/**
* Unique asset handle.
*
* @since 1.0.0
* @var string
*/
protected $handle;
/**
* Asset arguments.
*
* @since 1.0.0
* @var array
*/
protected $args = array();
/**
* Constructor.
*
* @since 1.0.0
* @since 1.37.0 Add the 'load_contexts' argument.
*
* @param string $handle Unique asset handle.
* @param array $args {
* Associative array of asset arguments.
*
* @type string $src Required asset source URL.
* @type array $dependencies List of asset dependencies. Default empty array.
* @type string $version Asset version. Default is the version of Site Kit.
* @type bool $fallback Whether to only register as a fallback. Default false.
* @type callable $before_print Optional callback to execute before printing. Default none.
* @type string[] $load_contexts Optional array of page context values to determine on which page types to load this asset (see the `CONTEXT_` variables above).
* }
*/
public function __construct( $handle, array $args ) {
$this->handle = $handle;
$this->args = wp_parse_args(
$args,
array(
'src' => '',
'dependencies' => array(),
'version' => GOOGLESITEKIT_VERSION,
'fallback' => false,
'before_print' => null,
'load_contexts' => array( self::CONTEXT_ADMIN_SITEKIT ),
)
);
}
/**
* Gets the notice handle.
*
* @since 1.0.0
*
* @return string Unique notice handle.
*/
public function get_handle() {
return $this->handle;
}
/**
* Checks to see if the specified context exists for the current request.
*
* @since 1.37.0
*
* @param string $context Context value (see the `CONTEXT_` variables above).
* @return bool TRUE if context exists; FALSE otherwise.
*/
public function has_context( $context ) {
return in_array( $context, $this->args['load_contexts'], true );
}
/**
* Registers the asset.
*
* @since 1.0.0
* @since 1.15.0 Adds $context parameter.
*
* @param Context $context Plugin context.
*/
abstract public function register( Context $context );
/**
* Enqueues the asset.
*
* @since 1.0.0
*/
abstract public function enqueue();
/**
* Executes the extra callback if defined before printing the asset.
*
* @since 1.2.0
*/
final public function before_print() {
if ( ! is_callable( $this->args['before_print'] ) ) {
return;
}
call_user_func( $this->args['before_print'], $this->handle );
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,66 @@
<?php
/**
* Class Google\Site_Kit\Core\Assets\Manifest
*
* @package GoogleSite_Kit
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Core\Assets;
use Google\Site_Kit\Plugin;
/**
* Assets manifest.
*
* @since 1.15.0
* @access private
* @ignore
*/
class Manifest {
/**
* Entries as $handle => [ $filename, $hash ] map.
*
* @since 1.48.0
* @var array
*/
private static $data;
/**
* Gets the manifest entry for the given handle.
*
* @since 1.48.0
*
* @param string $handle Asset handle to get manifest data for.
* @return array List of $filename and $hash, or `null` for both if not found.
*/
public static function get( $handle ) {
if ( null === self::$data ) {
self::load();
}
if ( isset( self::$data[ $handle ] ) ) {
return self::$data[ $handle ];
}
return array( null, null );
}
/**
* Loads the generated manifest file.
*
* @since 1.48.0
*/
private static function load() {
$path = Plugin::instance()->context()->path( 'dist/manifest.php' );
if ( file_exists( $path ) ) {
// If the include fails, $data will be `false`
// so this should only be attempted once.
self::$data = include $path;
}
}
}

View File

@ -0,0 +1,175 @@
<?php
/**
* Class Google\Site_Kit\Core\Assets\Script
*
* @package Google\Site_Kit
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Core\Assets;
use Google\Site_Kit\Context;
use Google\Site_Kit\Core\Util\BC_Functions;
/**
* Class representing a single script.
*
* @since 1.0.0
* @access private
* @ignore
*/
class Script extends Asset {
/**
* Constructor.
*
* @since 1.0.0
*
* @param string $handle Unique script handle.
* @param array $args {
* Associative array of script arguments.
*
* @type string $src Required script source URL.
* @type array $dependencies List of script dependencies. Default empty array.
* @type string $version Script version. Default is the version of Site Kit.
* @type bool $fallback Whether to only register as a fallback. Default false.
* @type callable $before_print Optional callback to execute before printing. Default none.
* @type bool $in_footer Whether to load script in footer. Default true.
* @type string $execution How to handle script execution, e.g. 'defer'. Default empty string.
* }
*/
public function __construct( $handle, array $args ) {
parent::__construct( $handle, $args );
$this->args = wp_parse_args(
$this->args,
array(
'in_footer' => true,
'execution' => '',
)
);
}
/**
* Registers the script.
*
* @since 1.0.0
* @since 1.15.0 Adds $context parameter.
*
* @param Context $context Plugin context.
*/
public function register( Context $context ) {
if ( $this->args['fallback'] && wp_script_is( $this->handle, 'registered' ) ) {
return;
}
$src = $this->args['src'];
$version = $this->args['version'];
if ( $src ) {
$entry = Manifest::get( $this->handle );
if ( is_array( $entry[0] ) ) {
// If the first entry item is an array, we can assume `$entry` is an array of entries in the format filename => hash.
// In this scenario we want to match the nested entry against the filename provided in `$src`.
$src_filename = basename( $src );
foreach ( $entry as $entry_pair ) {
if ( $this->is_matching_manifest_entry( $entry_pair, $src_filename ) ) {
list( $filename, $hash ) = $entry_pair;
break;
}
}
} else {
// Otherwise, `$entry` will be a single entry in the format filename => hash.
list( $filename, $hash ) = $entry;
}
if ( $filename ) {
$src = $context->url( 'dist/assets/js/' . $filename );
$version = $hash;
}
}
wp_register_script(
$this->handle,
$src,
(array) $this->args['dependencies'],
$version,
$this->args['in_footer']
);
if ( ! empty( $this->args['execution'] ) ) {
wp_script_add_data( $this->handle, 'script_execution', $this->args['execution'] );
}
if ( ! empty( $src ) ) {
$this->set_locale_data();
}
}
/**
* Enqueues the script.
*
* @since 1.0.0
*/
public function enqueue() {
wp_enqueue_script( $this->handle );
}
/**
* Checks if the provided manifest entry matches the given filename.
*
* @since 1.89.0
*
* @param array $entry Array of filename, hash.
* @param string $src_filename Filename to check.
* @return bool
*/
private function is_matching_manifest_entry( array $entry, $src_filename ) {
list ( $filename, $hash ) = $entry;
if ( ! isset( $hash ) ) {
// If the hash is not set, it means the hash is embedded in the entry filename.
// Remove the hash then compare to the src filename.
$entry_filename_without_hash = preg_replace( '/-[a-f0-9]+\.js$/', '.js', $filename );
if ( $src_filename === $entry_filename_without_hash ) {
return true;
}
}
if ( $filename === $src_filename ) {
return true;
}
return false;
}
/**
* Sets locale data for the script, if it has translations.
*
* @since 1.21.0
*/
private function set_locale_data() {
$json_translations = load_script_textdomain( $this->handle, 'google-site-kit' );
if ( ! $json_translations ) {
return;
}
$output = <<<JS
( function( domain, translations ) {
try {
var localeData = translations.locale_data[ domain ] || translations.locale_data.messages;
localeData[""].domain = domain;
googlesitekit.i18n.setLocaleData( localeData, domain );
} catch {
}
} )( "google-site-kit", {$json_translations} );
JS;
wp_add_inline_script( $this->handle, $output, 'before' );
}
}

View File

@ -0,0 +1,80 @@
<?php
/**
* Class Google\Site_Kit\Core\Assets\Script_Data
*
* @package Google\Site_Kit\Core\Assets
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Core\Assets;
/**
* Class for virtual "data-only" scripts.
*
* @since 1.5.0
* @access private
* @ignore
*/
class Script_Data extends Script {
/**
* Constructor.
*
* @since 1.5.0
*
* @param string $handle Unique script handle.
* @param array $args {
* Associative array of script arguments.
*
* @type callable $data_callback Required. Function to return JSON-encodable data.
* @type string $global Required. Name of global variable to assign data to in Javascript.
* @type array $dependencies Optional. List of script dependencies. Default empty array.
* }
*/
public function __construct( $handle, array $args ) {
// Ensure required keys are always set.
$args = $args + array(
'data_callback' => null,
'global' => '',
);
// SRC will always be false.
$args['src'] = false;
parent::__construct( $handle, $args );
// Lazy-load script data before handle is to be printed.
$this->args['before_print'] = function ( $handle ) {
if ( empty( $this->args['global'] ) || ! is_callable( $this->args['data_callback'] ) ) {
return;
}
$data = call_user_func( $this->args['data_callback'], $handle );
$this->add_script_data( $data );
};
}
/**
* Adds the given data to the script handle's 'data' key.
*
* 'data' is the key used by `wp_localize_script`, which is output
* in older versions of WP even if the handle has no src (such as an alias).
* This is done manually instead of using `wp_localize_script` to avoid casting
* top-level keys to strings as this function is primarily intended for
* providing an array of translations to Javascript rather than arbitrary data.
*
* @see \WP_Scripts::localize
*
* @since 1.5.0
*
* @param mixed $data Data to be assigned to the defined global.
*/
private function add_script_data( $data ) {
$script_data = wp_scripts()->get_data( $this->handle, 'data' ) ?: '';
$js = sprintf(
'var %s = %s;',
preg_replace( '[^\w\d_-]', '', $this->args['global'] ), // Ensure only a-zA-Z0-9_- are allowed.
wp_json_encode( $data )
);
wp_scripts()->add_data( $this->handle, 'data', trim( "$script_data\n$js" ) );
}
}

View File

@ -0,0 +1,91 @@
<?php
/**
* Class Google\Site_Kit\Core\Assets\Stylesheet
*
* @package Google\Site_Kit
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Core\Assets;
use Google\Site_Kit\Context;
/**
* Class representing a single stylesheet.
*
* @since 1.0.0
* @access private
* @ignore
*/
final class Stylesheet extends Asset {
/**
* Constructor.
*
* @since 1.0.0
*
* @param string $handle Unique stylesheet handle.
* @param array $args {
* Associative array of stylesheet arguments.
*
* @type string $src Required stylesheet source URL.
* @type array $dependencies List of stylesheet dependencies. Default empty array.
* @type string $version Stylesheet version. Default is the version of Site Kit.
* @type bool $fallback Whether to only register as a fallback. Default false.
* @type callable $before_print Optional callback to execute before printing. Default none.
* @type string $media Media for which the stylesheet is defined. Default 'all'.
* }
*/
public function __construct( $handle, array $args ) {
parent::__construct( $handle, $args );
$this->args = wp_parse_args(
$this->args,
array(
'media' => 'all',
)
);
}
/**
* Registers the stylesheet.
*
* @since 1.0.0
* @since 1.15.0 Adds $context parameter.
*
* @param Context $context Plugin context.
*/
public function register( Context $context ) {
if ( $this->args['fallback'] && wp_style_is( $this->handle, 'registered' ) ) {
return;
}
$src = $this->args['src'];
$version = $this->args['version'];
list( $filename, $hash ) = Manifest::get( $this->handle );
if ( $filename ) {
$src = $context->url( 'dist/assets/css/' . $filename );
$version = $hash;
}
wp_register_style(
$this->handle,
$src,
(array) $this->args['dependencies'],
$version,
$this->args['media']
);
}
/**
* Enqueues the stylesheet.
*
* @since 1.0.0
*/
public function enqueue() {
wp_enqueue_style( $this->handle );
}
}

View File

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

View File

@ -0,0 +1,307 @@
<?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.
*
* @param string $code Temporary authorization code, or undelegated token code.
* @return array Access token.
*
* @throws InvalidArgumentException Thrown when the passed code is empty.
*/
public function fetchAccessTokenWithAuthCode( $code ) {
if ( strlen( $code ) === 0 ) {
throw new InvalidArgumentException( 'Invalid code' );
}
$auth = $this->getOAuth2Service();
$auth->setCode( $code );
$auth->setRedirectUri( $this->getRedirectUri() );
$http_handler = HttpHandlerFactory::build( $this->getHttpClient() );
$token_response = $this->fetchAuthToken( $auth, $http_handler );
if ( $token_response && isset( $token_response['access_token'] ) ) {
$token_response['created'] = time();
$this->setAccessToken( $token_response );
}
return $token_response;
}
/**
* Fetches a fresh OAuth 2.0 access token by using a refresh token.
*
* @since 1.0.0
* @since 1.2.0 Ported from Google_Site_Kit_Proxy_Client.
*
* @param string $refresh_token Optional. Refresh token. Unused here.
* @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;
use Google\Site_Kit_Dependencies\GuzzleHttp\Psr7\Request;
use Exception;
/**
* Modified Google Site Kit API client relying on the authentication proxy.
*
* @since 1.0.0
* @since 1.2.0 Renamed to Google_Site_Kit_Proxy_Client.
* @access private
* @ignore
*/
class Google_Site_Kit_Proxy_Client extends Google_Site_Kit_Client {
/**
* Base URL to the proxy.
*
* @since 1.1.2
* @var string
*/
protected $proxy_base_path = Google_Proxy::PRODUCTION_BASE_URL;
/**
* Construct the Google client.
*
* @since 1.1.2
*
* @param array $config Proxy client configuration.
*/
public function __construct( array $config = array() ) {
if ( ! empty( $config['proxy_base_path'] ) ) {
$this->setProxyBasePath( $config['proxy_base_path'] );
}
unset( $config['proxy_base_path'] );
parent::__construct( $config );
$this->setApplicationName( Google_Proxy::get_application_name() );
}
/**
* Sets the base URL to the proxy.
*
* @since 1.2.0
*
* @param string $base_path Proxy base URL.
*/
public function setProxyBasePath( $base_path ) {
$this->proxy_base_path = untrailingslashit( $base_path );
}
/**
* Revokes an OAuth2 access token using the authentication proxy.
*
* @since 1.0.0
*
* @param string|array|null $token Optional. Access token. Default is the current one.
* @return bool True on success, false on failure.
*/
public function revokeToken( $token = null ) {
if ( ! $token ) {
$token = $this->getAccessToken();
}
if ( is_array( $token ) ) {
$token = $token['access_token'];
}
$body = Psr7\stream_for(
http_build_query(
array(
'client_id' => $this->getClientId(),
'token' => $token,
)
)
);
$request = new Request(
'POST',
$this->proxy_base_path . Google_Proxy::OAUTH2_REVOKE_URI,
array(
'Cache-Control' => 'no-store',
'Content-Type' => 'application/x-www-form-urlencoded',
),
$body
);
$http_handler = HttpHandlerFactory::build( $this->getHttpClient() );
$response = $http_handler( $request );
return 200 === (int) $response->getStatusCode();
}
/**
* Creates a Google auth object for the authentication proxy.
*
* @since 1.0.0
*/
protected function createOAuth2Service() {
return new OAuth2(
array(
'clientId' => $this->getClientId(),
'clientSecret' => $this->getClientSecret(),
'authorizationUri' => $this->proxy_base_path . Google_Proxy::OAUTH2_AUTH_URI,
'tokenCredentialUri' => $this->proxy_base_path . Google_Proxy::OAUTH2_TOKEN_URI,
'redirectUri' => $this->getRedirectUri(),
'issuer' => $this->getClientId(),
'signingKey' => null,
'signingAlgorithm' => null,
)
);
}
/**
* Handles an erroneous response from a request to fetch an auth token.
*
* @since 1.2.0
*
* @param string $error Error code / error message.
* @param array $data Associative array of full response data.
*
* @throws Google_Proxy_Code_Exception Thrown when proxy returns an error accompanied by a temporary access code.
*/
protected function handleAuthTokenErrorResponse( $error, array $data ) {
if ( ! empty( $data['code'] ) ) {
throw new Google_Proxy_Code_Exception( $error, 0, $data['code'] );
}
parent::handleAuthTokenErrorResponse( $error, $data );
}
}

View File

@ -0,0 +1,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,661 @@
<?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\Dashboard_Sharing\Activity_Metrics\Activity_Metrics;
use Google\Site_Kit\Core\Dashboard_Sharing\Activity_Metrics\Active_Consumers;
use Google\Site_Kit\Core\Permissions\Permissions;
use Google\Site_Kit\Core\Storage\Options;
use Google\Site_Kit\Core\Storage\User_Options;
use Google\Site_Kit\Core\Util\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 CRON_REFRESH_PROFILE_DATA = 'googlesitekit_cron_refresh_profile_data';
/**
* Owner_ID instance.
*
* @since 1.16.0
* @var Owner_ID
*/
private $owner_id;
/**
* Activity_Metrics instance.
*
* @since 1.87.0
* @var Activity_Metrics
*/
private $activity_metrics;
/**
* Active_Consumers instance.
*
* @since 1.87.0
* @var Active_Consumers
*/
private $active_consumers;
/**
* Constructor.
*
* @since 1.0.0
*
* @param Context $context Plugin context.
* @param Options $options Optional. Option API instance. Default is a new instance.
* @param User_Options $user_options Optional. User Option API instance. Default is a new instance.
* @param Credentials $credentials Optional. Credentials instance. Default is a new instance from $options.
* @param Google_Proxy $google_proxy Optional. Google proxy instance. Default is a new instance.
* @param Profile $profile Optional. Profile instance. Default is a new instance.
* @param Token $token Optional. Token instance. Default is a new instance.
*/
public function __construct(
Context $context,
Options $options = null,
User_Options $user_options = null,
Credentials $credentials = null,
Google_Proxy $google_proxy = null,
Profile $profile = null,
Token $token = null
) {
parent::__construct(
$context,
$options,
$user_options,
$credentials,
$google_proxy,
$profile,
$token
);
$this->owner_id = new Owner_ID( $this->options );
$this->activity_metrics = new Activity_Metrics( $this->context, $this->user_options );
$this->active_consumers = new Active_Consumers( $this->user_options );
}
/**
* Refreshes the access token.
*
* While this method can be used to explicitly refresh the current access token, the preferred way
* should be to rely on the Google_Site_Kit_Client to do that automatically whenever the current access token
* has expired.
*
* @since 1.0.0
*/
public function refresh_token() {
$token = $this->get_token();
if ( empty( $token['refresh_token'] ) ) {
$this->delete_token();
$this->user_options->set( self::OPTION_ERROR_CODE, 'refresh_token_not_exist' );
return;
}
$active_consumers = $this->activity_metrics->get_for_refresh_token();
try {
$token_response = $this->get_client()->fetchAccessTokenWithRefreshToken( $token['refresh_token'], $active_consumers );
} 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->active_consumers->delete();
$this->set_token( $token_response );
}
/**
* Revokes the access token.
*
* @since 1.0.0
*/
public function revoke_token() {
try {
$this->get_client()->revokeToken();
} catch ( \Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement
// No special handling, we just need to make sure this goes through.
}
$this->delete_token();
}
/**
* Gets the list of currently granted Google OAuth scopes for the current user.
*
* @since 1.0.0
* @see https://developers.google.com/identity/protocols/googlescopes
*
* @return string[] List of Google OAuth scopes.
*/
public function get_granted_scopes() {
$base_scopes = parent::get_granted_scopes();
$extra_scopes = $this->get_granted_additional_scopes();
return array_unique(
array_merge( $base_scopes, $extra_scopes )
);
}
/**
* Gets the list of currently granted additional Google OAuth scopes for the current user.
*
* Scopes are considered "additional scopes" if they were granted to perform a specific action,
* rather than being granted as an overall required scope.
*
* @since 1.9.0
* @see https://developers.google.com/identity/protocols/googlescopes
*
* @return string[] List of Google OAuth scopes.
*/
public function get_granted_additional_scopes() {
return array_values( $this->user_options->get( self::OPTION_ADDITIONAL_AUTH_SCOPES ) ?: array() );
}
/**
* Checks if new scopes are required that are not yet granted for the current user.
*
* @since 1.9.0
*
* @return bool true if any required scopes are not satisfied, otherwise false.
*/
public function needs_reauthentication() {
if ( ! $this->token->has() ) {
return false;
}
return ! $this->has_sufficient_scopes();
}
/**
* Gets the list of scopes which are not satisfied by the currently granted scopes.
*
* @since 1.9.0
*
* @param string[] $scopes Optional. List of scopes to test against granted scopes.
* Default is the list of required scopes.
* @return string[] Filtered $scopes list, only including scopes that are not satisfied.
*/
public function get_unsatisfied_scopes( array $scopes = null ) {
if ( null === $scopes ) {
$scopes = $this->get_required_scopes();
}
$granted_scopes = $this->get_granted_scopes();
$unsatisfied_scopes = array_filter(
$scopes,
function( $scope ) use ( $granted_scopes ) {
return ! Scopes::is_satisfied_by( $scope, $granted_scopes );
}
);
return array_values( $unsatisfied_scopes );
}
/**
* Checks whether or not currently granted scopes are sufficient for the given list.
*
* @since 1.9.0
*
* @param string[] $scopes Optional. List of scopes to test against granted scopes.
* Default is the list of required scopes.
* @return bool True if all $scopes are satisfied, false otherwise.
*/
public function has_sufficient_scopes( array $scopes = null ) {
if ( null === $scopes ) {
$scopes = $this->get_required_scopes();
}
return Scopes::are_satisfied_by( $scopes, $this->get_granted_scopes() );
}
/**
* Sets the list of currently granted Google OAuth scopes for the current user.
*
* @since 1.0.0
* @see https://developers.google.com/identity/protocols/googlescopes
*
* @param string[] $scopes List of Google OAuth scopes.
*/
public function set_granted_scopes( $scopes ) {
$required_scopes = $this->get_required_scopes();
$base_scopes = array();
$extra_scopes = array();
foreach ( $scopes as $scope ) {
if ( in_array( $scope, $required_scopes, true ) ) {
$base_scopes[] = $scope;
} else {
$extra_scopes[] = $scope;
}
}
parent::set_granted_scopes( $base_scopes );
$this->user_options->set( self::OPTION_ADDITIONAL_AUTH_SCOPES, $extra_scopes );
}
/**
* Gets the current user's OAuth access token.
*
* @since 1.0.0
*
* @return string|bool Access token if it exists, false otherwise.
*/
public function get_access_token() {
$token = $this->get_token();
if ( empty( $token['access_token'] ) ) {
return false;
}
return $token['access_token'];
}
/**
* Sets the current user's OAuth access token.
*
* @since 1.0.0
* @deprecated 1.39.0 Use `OAuth_Client::set_token` instead.
*
* @param string $access_token New access token.
* @param int $expires_in TTL of the access token in seconds.
* @param int $created Optional. Timestamp when the token was created, in GMT. Default is the current time.
* @return bool True on success, false on failure.
*/
public function set_access_token( $access_token, $expires_in, $created = 0 ) {
_deprecated_function( __METHOD__, '1.39.0', self::class . '::set_token' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
return $this->set_token(
array(
'access_token' => $access_token,
'expires_in' => $expires_in,
'created' => $created,
)
);
}
/**
* Gets the current user's OAuth refresh token.
*
* @since 1.0.0
* @deprecated 1.39.0 Use `OAuth_Client::get_token` instead.
*
* @return string|bool Refresh token if it exists, false otherwise.
*/
public function get_refresh_token() {
_deprecated_function( __METHOD__, '1.39.0', self::class . '::get_token' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
$token = $this->get_token();
if ( empty( $token['refresh_token'] ) ) {
return false;
}
return $token['refresh_token'];
}
/**
* Sets the current user's OAuth refresh token.
*
* @since 1.0.0
* @deprecated 1.39.0 Use `OAuth_Client::set_token` instead.
*
* @param string $refresh_token New refresh token.
* @return bool True on success, false on failure.
*/
public function set_refresh_token( $refresh_token ) {
_deprecated_function( __METHOD__, '1.39.0', self::class . '::set_token' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
$token = $this->get_token();
$token['refresh_token'] = $refresh_token;
return $this->set_token( $token );
}
/**
* Gets the authentication URL.
*
* @since 1.0.0
* @since 1.9.0 Added $additional_scopes parameter.
* @since 1.34.1 Updated handling of $additional_scopes to restore rewritten scope.
*
* @param string $redirect_url Redirect URL after authentication.
* @param string[] $additional_scopes List of additional scopes to request.
* @return string Authentication URL.
*/
public function get_authentication_url( $redirect_url = '', $additional_scopes = array() ) {
if ( empty( $redirect_url ) ) {
$redirect_url = $this->context->admin_url( 'splash' );
}
if ( is_array( $additional_scopes ) ) {
// Rewrite each scope to convert `gttp` -> `http`, if it starts with this placeholder scheme.
// This restores the original scope rewritten by getConnectURL.
$additional_scopes = array_map(
function ( $scope ) {
return preg_replace( '/^gttp(s)?:/', 'http$1:', $scope );
},
$additional_scopes
);
} else {
$additional_scopes = array();
}
$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 );
// Ensure the latest required scopes are requested.
$scopes = array_merge( $this->get_required_scopes(), $additional_scopes );
$this->get_client()->setScopes( array_unique( $scopes ) );
return add_query_arg(
$this->google_proxy->get_metadata_fields(),
$this->get_client()->createAuthUrl()
);
}
/**
* Redirects the current user to the Google OAuth consent screen, or processes a response from that consent
* screen if present.
*
* @since 1.0.0
* @since 1.49.0 Uses the new `Google_Proxy::setup_url_v2` method when the `serviceSetupV2` feature flag is enabled.
*/
public function authorize_user() {
$code = $this->context->input()->filter( INPUT_GET, 'code', FILTER_SANITIZE_STRING );
$error_code = $this->context->input()->filter( INPUT_GET, 'error', FILTER_SANITIZE_STRING );
// If the OAuth redirects with an error code, handle it.
if ( ! empty( $error_code ) ) {
$this->user_options->set( self::OPTION_ERROR_CODE, $error_code );
wp_safe_redirect( $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 = $this->context->input()->filter( INPUT_GET, 'scope', FILTER_SANITIZE_STRING );
$scopes = explode( ' ', $scope );
} else {
$scopes = $this->get_required_scopes();
}
$scopes = array_filter(
$scopes,
function( $scope ) {
if ( ! is_string( $scope ) ) {
return false;
}
if ( in_array( $scope, array( 'openid', 'profile', 'email' ), true ) ) {
return true;
}
return 0 === strpos( $scope, 'https://www.googleapis.com/auth/' );
}
);
$this->set_granted_scopes( $scopes );
$this->refresh_profile_data( 2 * MINUTE_IN_SECONDS );
/**
* Fires when the current user has just been authorized to access Google APIs.
*
* In other words, this action fires whenever Site Kit has just obtained a new set of access token and
* refresh token for the current user, which may happen to set up the initial connection or to request
* access to further scopes.
*
* @since 1.3.0
* @since 1.6.0 The $token_response parameter was added.
* @since 1.30.0 The $scopes and $previous_scopes parameters were added.
*
* @param array $token_response Token response data.
* @param string[] $scopes List of scopes.
* @param string[] $previous_scopes List of previous scopes.
*/
do_action( 'googlesitekit_authorize_user', $token_response, $scopes, $previous_scopes );
// This must happen after googlesitekit_authorize_user as the permissions checks depend on
// values set which affect the meta capability mapping.
$current_user_id = get_current_user_id();
if ( $this->should_update_owner_id( $current_user_id ) ) {
$this->owner_id->set( $current_user_id );
}
$redirect_url = $this->user_options->get( self::OPTION_REDIRECT_URL );
if ( $redirect_url ) {
$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 );
} else {
// No redirect_url is set, use default page.
$redirect_url = $this->context->admin_url( 'splash', array( 'notification' => 'authentication_success' ) );
}
wp_safe_redirect( $redirect_url );
exit();
}
/**
* Fetches and updates the user profile data for the currently authenticated Google account.
*
* @since 1.1.4
* @since 1.13.0 Added $retry_after param, also made public.
*
* @param int $retry_after Optional. Number of seconds to retry data fetch if unsuccessful.
*/
public function refresh_profile_data( $retry_after = 0 ) {
try {
$people_service = new Google_Service_PeopleService( $this->get_client() );
$response = $people_service->people->get( 'people/me', array( 'personFields' => 'emailAddresses,photos,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'],
)
);
}
// Clear any scheduled job to refresh this data later, if any.
wp_clear_scheduled_hook(
self::CRON_REFRESH_PROFILE_DATA,
array( $this->user_options->get_user_id() )
);
} catch ( Exception $e ) {
$retry_after = absint( $retry_after );
if ( $retry_after < 1 ) {
return;
}
wp_schedule_single_event(
time() + $retry_after,
self::CRON_REFRESH_PROFILE_DATA,
array( $this->user_options->get_user_id() )
);
}
}
/**
* Determines whether the authentication proxy is used.
*
* In order to streamline the setup and authentication flow, the plugin uses a proxy mechanism based on an external
* service. This can be overridden by providing actual GCP credentials with the {@see 'googlesitekit_oauth_secret'}
* filter.
*
* @since 1.0.0
* @deprecated 1.9.0
*
* @return bool True if proxy authentication is used, false otherwise.
*/
public function using_proxy() {
_deprecated_function( __METHOD__, '1.9.0', Credentials::class . '::using_proxy' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
return $this->credentials->using_proxy();
}
/**
* Determines whether the current owner ID must be changed or not.
*
* @since 1.16.0
*
* @param int $user_id Current user ID.
* @return bool TRUE if owner needs to be changed, otherwise FALSE.
*/
private function should_update_owner_id( $user_id ) {
$current_owner_id = $this->owner_id->get();
if ( $current_owner_id === $user_id ) {
return false;
}
if ( ! empty( $current_owner_id ) && user_can( $current_owner_id, Permissions::MANAGE_OPTIONS ) ) {
return false;
}
if ( ! user_can( $user_id, Permissions::MANAGE_OPTIONS ) ) {
return false;
}
return true;
}
/**
* Returns the permissions URL to the authentication proxy.
*
* This only returns a URL if the user already has an access token set.
*
* @since 1.0.0
*
* @return string URL to the permissions page on the authentication proxy on success,
* or empty string on failure.
*/
public function get_proxy_permissions_url() {
$access_token = $this->get_access_token();
if ( empty( $access_token ) ) {
return '';
}
return $this->google_proxy->permissions_url(
$this->credentials,
array( 'token' => $access_token )
);
}
/**
* Deletes the current user's token and all associated data.
*
* @since 1.0.3
*/
protected function delete_token() {
parent::delete_token();
$this->user_options->delete( self::OPTION_REDIRECT_URL );
$this->user_options->delete( self::OPTION_ADDITIONAL_AUTH_SCOPES );
}
/**
* 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() {
return current_user_can( Permissions::VIEW_DASHBOARD )
? $this->context->admin_url( 'dashboard' )
: $this->context->admin_url( 'splash' );
}
/**
* Adds a user to the active consumers list.
*
* @since 1.87.0
*
* @param WP_User $user User object.
*/
public function add_active_consumer( WP_User $user ) {
$this->active_consumers->add( $user->ID, $user->roles );
}
}

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 __( 'The Site Kit setup was interrupted because you did not grant the necessary permissions.', 'google-site-kit' );
case 'access_token_not_received':
return __( 'Unable to receive access token because of an unknown error.', 'google-site-kit' );
case 'cannot_log_in':
return __( 'Internal error that the Google login redirect failed.', 'google-site-kit' );
case 'invalid_client':
return __( 'Unable to receive access token because of an invalid client.', 'google-site-kit' );
case 'invalid_code':
return __( 'Unable to receive access token because of an empty authorization code.', 'google-site-kit' );
case 'invalid_grant':
return __( 'Unable to receive access token because of an invalid authorization code or refresh token.', 'google-site-kit' );
case 'invalid_request':
return __( 'Unable to receive access token because of an invalid OAuth request.', 'google-site-kit' );
case 'missing_delegation_consent':
return __( 'Looks like your site is not allowed access to Google account data and cant display stats in the dashboard.', 'google-site-kit' );
case 'missing_search_console_property':
return __( 'Looks like there is no Search Console property for your site.', 'google-site-kit' );
case 'missing_verification':
return __( 'Looks like the verification token for your site is missing.', 'google-site-kit' );
case 'oauth_credentials_not_exist':
return __( 'Unable to authenticate Site Kit, as no client credentials exist.', 'google-site-kit' );
case 'refresh_token_not_exist':
return __( 'Unable to refresh access token, as no refresh token exists.', 'google-site-kit' );
case 'unauthorized_client':
return __( 'Unable to receive access token because of an unauthorized client.', 'google-site-kit' );
case 'unsupported_grant_type':
return __( 'Unable to receive access token because of an unsupported grant type.', 'google-site-kit' );
default:
/* translators: %s: error code from API */
return sprintf( __( 'Unknown Error (code: %s).', 'google-site-kit' ), $error_code );
}
}
/**
* Handles an exception thrown when fetching an access token.
*
* @since 1.2.0
*
* @param Exception $e Exception thrown.
*/
protected function handle_fetch_token_exception( Exception $e ) {
$error_code = $e->getMessage();
// Revoke and delete user connection data on 'invalid_grant'.
// This typically happens during refresh if the refresh token is invalid or expired.
if ( 'invalid_grant' === $error_code ) {
$this->delete_token();
}
$this->user_options->set( self::OPTION_ERROR_CODE, $error_code );
if ( $e instanceof Google_Proxy_Code_Exception ) {
$this->user_options->set( self::OPTION_PROXY_ACCESS_CODE, $e->getAccessCode() );
}
}
/**
* Gets the OAuth redirect URI that listens to the callback request.
*
* @since 1.39.0
*
* @return string OAuth redirect URI.
*/
protected function get_redirect_uri() {
return add_query_arg( 'oauth2callback', '1', admin_url( 'index.php' ) );
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,672 @@
<?php
/**
* Class Google\Site_Kit\Core\Authentication\Google_Proxy
*
* @package Google\Site_Kit\Core\Authentication
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Core\Authentication;
use Google\Site_Kit\Context;
use Google\Site_Kit\Core\Util\Feature_Flags;
use Exception;
use 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 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 PERMISSIONS_URI = '/site-management/permissions/';
const USER_INPUT_SETTINGS_URI = '/site-management/settings/';
const FEATURES_URI = '/site-management/features/';
const SURVEY_TRIGGER_URI = '/survey/trigger/';
const SURVEY_EVENT_URI = '/survey/event/';
const 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',
// Informs the proxy the user input feature is generally supported.
'user_input_flow',
);
$home_path = URL::parse( $this->context->get_canonical_home_url(), PHP_URL_PATH );
if ( ! $home_path || '/' === $home_path ) {
$supports[] = 'file_verification';
}
// Informs the proxy the user input feature is already enabled locally.
// TODO: Remove once the feature is fully rolled out.
if ( Feature_Flags::enabled( 'userInput' ) ) {
$supports[] = 'user_input_flow_feature';
}
return $supports;
}
/**
* Returns the setup URL to the authentication proxy.
*
* @since 1.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( 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 = defined( 'GOOGLESITEKIT_PROXY_URL' ) && self::STAGING_BASE_URL === GOOGLESITEKIT_PROXY_URL
? self::STAGING_BASE_URL
: self::PRODUCTION_BASE_URL;
$url = untrailingslashit( $url );
if ( $path && is_string( $path ) ) {
$url .= '/' . ltrim( $path, '/' );
}
return $url;
}
/**
* Sends a POST request to the Google Proxy server.
*
* @since 1.27.0
*
* @param string $uri Endpoint to send the request to.
* @param Credentials $credentials Credentials instance.
* @param array $args Array of request arguments.
* @return array|WP_Error The response as an associative array or WP_Error on failure.
*/
private function request( $uri, $credentials, array $args = array() ) {
$request_args = array(
'headers' => ! empty( $args['headers'] ) && is_array( $args['headers'] ) ? $args['headers'] : array(),
'body' => ! empty( $args['body'] ) && is_array( $args['body'] ) ? $args['body'] : array(),
'timeout' => isset( $args['timeout'] ) ? $args['timeout'] : 15,
);
if ( $credentials && $credentials instanceof Credentials ) {
if ( ! $credentials->has() ) {
return new WP_Error(
'oauth_credentials_not_exist',
__( 'OAuth credentials haven\'t been found.', 'google-site-kit' ),
array( 'status' => 401 )
);
}
$creds = $credentials->get();
$request_args['body']['site_id'] = $creds['oauth2_client_id'];
$request_args['body']['site_secret'] = $creds['oauth2_client_secret'];
}
if ( ! empty( $args['access_token'] ) && is_string( $args['access_token'] ) ) {
$request_args['headers']['Authorization'] = 'Bearer ' . $args['access_token'];
}
if ( isset( $args['mode'] ) && 'async' === $args['mode'] ) {
$request_args['timeout'] = 0.01;
$request_args['blocking'] = false;
}
if ( ! empty( $args['json_request'] ) ) {
$request_args['headers']['Content-Type'] = 'application/json';
$request_args['body'] = wp_json_encode( $request_args['body'] );
}
$url = $this->url( $uri );
$response = wp_remote_post( $url, $request_args );
if ( is_wp_error( $response ) ) {
return $response;
}
$code = wp_remote_retrieve_response_code( $response );
$body = wp_remote_retrieve_body( $response );
$body = json_decode( $body, true );
if ( $code < 200 || 299 < $code ) {
$message = is_array( $body ) && ! empty( $body['error'] ) ? $body['error'] : '';
return new WP_Error( 'request_failed', $message, array( 'status' => $code ) );
}
if ( ! empty( $args['return'] ) && 'response' === $args['return'] ) {
return $response;
}
if ( is_null( $body ) ) {
return new WP_Error(
'failed_to_parse_response',
__( 'Failed to parse response.', 'google-site-kit' ),
array( 'status' => 500 )
);
}
return $body;
}
/**
* Gets site fields.
*
* @since 1.5.0
*
* @return array Associative array of $query_arg => $value pairs.
*/
public function get_site_fields() {
return array(
'name' => wp_specialchars_decode( get_bloginfo( 'name' ), 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',
);
/**
* Filters the setup mode.
*
* @since 1.68.0
*
* @param string $mode An initial setup mode.
*/
$metadata['mode'] = apply_filters( 'googlesitekit_proxy_setup_mode', $metadata['mode'] );
return $metadata;
}
/**
* Fetch site fields
*
* @since 1.22.0
*
* @param Credentials $credentials Credentials instance.
* @return array|WP_Error The response as an associative array or WP_Error on failure.
*/
public function fetch_site_fields( Credentials $credentials ) {
return $this->request( self::OAUTH2_SITE_URI, $credentials );
}
/**
* Are site fields synced
*
* @since 1.22.0
*
* @param Credentials $credentials Credentials instance.
*
* @return boolean|WP_Error Boolean do the site fields match or WP_Error on failure.
*/
public function are_site_fields_synced( Credentials $credentials ) {
$site_fields = $this->fetch_site_fields( $credentials );
if ( is_wp_error( $site_fields ) ) {
return $site_fields;
}
$get_site_fields = $this->get_site_fields();
foreach ( $get_site_fields as $key => $site_field ) {
if ( ! array_key_exists( $key, $site_fields ) || $site_fields[ $key ] !== $site_field ) {
return false;
}
}
return true;
}
/**
* Gets user fields.
*
* @since 1.10.0
*
* @return array Associative array of $query_arg => $value pairs.
*/
public function get_user_fields() {
$user_roles = wp_get_current_user()->roles;
// If multisite, also consider network administrators.
if ( is_multisite() && current_user_can( 'manage_network' ) ) {
$user_roles[] = 'network_administrator';
}
$user_roles = array_unique( $user_roles );
return array(
'user_roles' => implode( ',', $user_roles ),
);
}
/**
* Unregisters the site on the proxy.
*
* @since 1.20.0
*
* @param Credentials $credentials Credentials instance.
* @return array|WP_Error Response data on success, otherwise WP_Error object.
*/
public function unregister_site( Credentials $credentials ) {
return $this->request( self::OAUTH2_DELETE_SITE_URI, $credentials );
}
/**
* Registers the site on the proxy.
*
* @since 1.68.0
*
* @param string $mode Sync mode.
* @return string|WP_Error Redirect URL on success, otherwise an error.
*/
public function register_site( $mode = 'async' ) {
return $this->send_site_fields( null, $mode );
}
/**
* Synchronizes site fields with the proxy.
*
* @since 1.5.0
* @since 1.68.0 Updated the function to return redirect URL.
*
* @param Credentials $credentials Credentials instance.
* @param string $mode Sync mode.
* @return string|WP_Error Redirect URL on success, otherwise an error.
*/
public function sync_site_fields( Credentials $credentials, $mode = 'async' ) {
return $this->send_site_fields( $credentials, $mode );
}
/**
* Sends site fields to the proxy.
*
* @since 1.68.0
*
* @param Credentials $credentials Credentials instance.
* @param string $mode Sync mode.
* @return string|WP_Error Redirect URL on success, otherwise an error.
*/
private function send_site_fields( Credentials $credentials = null, $mode = 'async' ) {
$response = $this->request(
self::OAUTH2_SITE_URI,
$credentials,
array(
'return' => 'response',
'mode' => $mode,
'body' => array_merge(
$this->get_site_fields(),
$this->get_user_fields(),
$this->get_metadata_fields(),
array(
'scope' => implode( ' ', $this->required_scopes ),
)
),
)
);
if ( is_wp_error( $response ) ) {
return $response;
}
$redirect_to = wp_remote_retrieve_header( $response, self::HEADER_REDIRECT_TO );
if ( empty( $redirect_to ) ) {
return new WP_Error(
'failed_to_retrive_redirect',
__( 'Failed to retrieve redirect URL.', 'google-site-kit' ),
array( 'status' => 500 )
);
}
return $redirect_to;
}
/**
* Synchronizes user input settings with the proxy.
*
* @since 1.27.0
*
* @param Credentials $credentials Credentials instance.
* @param string $access_token Access token.
* @param array|null $settings Settings array.
* @return array|WP_Error Response of the wp_remote_post request.
*/
public function sync_user_input_settings( Credentials $credentials, $access_token, $settings = null ) {
$body = array();
if ( ! empty( $settings ) ) {
$body = array(
'settings' => $settings,
'client_user_id' => (string) get_current_user_id(),
);
}
return $this->request(
self::USER_INPUT_SETTINGS_URI,
$credentials,
array(
'json_request' => true,
'access_token' => $access_token,
'body' => $body,
)
);
}
/**
* Exchanges a site code for client credentials from the proxy.
*
* @since 1.1.2
*
* @param string $site_code Site code identifying the site.
* @param string $undelegated_code Undelegated code identifying the undelegated token.
* @return array|WP_Error Response data containing site_id and site_secret on success, WP_Error object on failure.
*/
public function exchange_site_code( $site_code, $undelegated_code ) {
$response_data = $this->request(
self::OAUTH2_SITE_URI,
null,
array(
'body' => array(
'code' => $undelegated_code,
'site_code' => $site_code,
),
)
);
if ( is_wp_error( $response_data ) ) {
return $response_data;
}
if ( ! isset( $response_data['site_id'], $response_data['site_secret'] ) ) {
return new WP_Error(
'oauth_credentials_not_exist',
__( 'OAuth credentials haven\'t been found.', 'google-site-kit' ),
array( 'status' => 401 )
);
}
return $response_data;
}
/**
* Gets remote features.
*
* @since 1.27.0
*
* @param Credentials $credentials Credentials instance.
* @return array|WP_Error Response of the wp_remote_post request.
*/
public function get_features( Credentials $credentials ) {
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,
'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.Misspelled
}
/**
* Sends survey trigger ID to the proxy.
*
* @since 1.35.0
*
* @param Credentials $credentials Credentials instance.
* @param string $access_token Access token.
* @param string $trigger_id Token ID.
* @return array|WP_Error Response of the wp_remote_post request.
*/
public function send_survey_trigger( Credentials $credentials, $access_token, $trigger_id ) {
return $this->request(
self::SURVEY_TRIGGER_URI,
$credentials,
array(
'access_token' => $access_token,
'json_request' => true,
'body' => array(
'trigger_context' => array(
'trigger_id' => $trigger_id,
'language' => get_user_locale(),
),
),
)
);
}
/**
* Sends survey event to the proxy.
*
* @since 1.35.0
*
* @param Credentials $credentials Credentials instance.
* @param string $access_token Access token.
* @param array|\stdClass $session Session object.
* @param array|\stdClass $event Event object.
* @return array|WP_Error Response of the wp_remote_post request.
*/
public function send_survey_event( Credentials $credentials, $access_token, $session, $event ) {
return $this->request(
self::SURVEY_EVENT_URI,
$credentials,
array(
'access_token' => $access_token,
'json_request' => true,
'body' => array(
'session' => $session,
'event' => $event,
),
)
);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,399 @@
<?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\Feature_Flags;
/**
* 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;
/**
* 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 = $this->context->input()->filter( INPUT_GET, 'nonce', FILTER_SANITIZE_STRING );
$redirect_url = $this->context->input()->filter( INPUT_GET, 'redirect', FILTER_SANITIZE_URL );
$this->verify_nonce( $nonce, Google_Proxy::ACTION_SETUP_START );
if ( ! current_user_can( Permissions::SETUP ) ) {
wp_die( esc_html__( 'You have insufficient permissions to connect Site Kit.', 'google-site-kit' ) );
}
if ( ! $this->credentials->using_proxy() ) {
wp_die( esc_html__( 'Site Kit is not configured to use the authentication proxy.', 'google-site-kit' ) );
}
$required_scopes = $this->authentication->get_oauth_client()->get_required_scopes();
$this->google_proxy->with_scopes( $required_scopes );
$oauth_setup_redirect = $this->credentials->has()
? $this->google_proxy->sync_site_fields( $this->credentials, 'sync' )
: $this->google_proxy->register_site( 'sync' );
$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 = $input->filter( INPUT_GET, 'step', FILTER_SANITIZE_STRING );
$nonce = $input->filter( INPUT_GET, 'nonce', FILTER_SANITIZE_STRING );
$code = $input->filter( INPUT_GET, 'googlesitekit_code', FILTER_SANITIZE_STRING );
$site_code = $input->filter( INPUT_GET, 'googlesitekit_site_code', FILTER_SANITIZE_STRING );
$verification_token = $input->filter( INPUT_GET, 'googlesitekit_verification_token', FILTER_SANITIZE_STRING );
$verification_method = $input->filter( INPUT_GET, 'googlesitekit_verification_token_type', FILTER_SANITIZE_STRING );
$this->verify_nonce( $nonce );
if ( ! current_user_can( Permissions::SETUP ) ) {
wp_die( esc_html__( 'You don\'t have permissions to set up Site Kit.', 'google-site-kit' ), 403 );
}
if ( ! $code ) {
wp_die( esc_html__( 'Invalid request.', 'google-site-kit' ), 400 );
}
if ( ! $verification_token || ! $verification_method ) {
wp_die( esc_html__( 'Verifying site ownership requires a token and verification method.', 'google-site-kit' ), 400 );
}
$this->handle_verification( $verification_token, $verification_method );
$proxy_query_params = array(
'step' => $step,
'verify' => 'true',
'verification_method' => $verification_method,
);
// If the site does not have a site ID yet, a site code will be passed.
// Handling the site code here will save the extra redirect from the proxy if successful.
if ( $site_code ) {
try {
$this->handle_site_code( $code, $site_code );
} catch ( Missing_Verification_Exception $exception ) {
$proxy_query_params['site_code'] = $site_code;
$this->redirect_to_proxy( $code, $proxy_query_params );
} catch ( Exchange_Site_Code_Exception $exception ) {
$this->redirect_to_splash();
}
}
$credentials = $this->credentials->get();
$proxy_query_params['site_id'] = ! empty( $credentials['oauth2_client_id'] ) ? $credentials['oauth2_client_id'] : '';
$this->redirect_to_proxy( $code, $proxy_query_params );
}
/**
* Handles the action for exchanging the site code for site credentials.
*
* This action will only be called if the site code failed to be handled
* during the verification step.
*
* @since 1.48.0
*/
public function handle_action_exchange_site_code() {
$input = $this->context->input();
$step = $input->filter( INPUT_GET, 'step', FILTER_SANITIZE_STRING );
$nonce = $input->filter( INPUT_GET, 'nonce', FILTER_SANITIZE_STRING );
$code = $input->filter( INPUT_GET, 'googlesitekit_code', FILTER_SANITIZE_STRING );
$site_code = $input->filter( INPUT_GET, 'googlesitekit_site_code', FILTER_SANITIZE_STRING );
$this->verify_nonce( $nonce );
if ( ! current_user_can( Permissions::SETUP ) ) {
wp_die( esc_html__( 'You don\'t have permissions to set up Site Kit.', 'google-site-kit' ), 403 );
}
if ( ! $code || ! $site_code ) {
wp_die( esc_html__( 'Invalid request.', 'google-site-kit' ), 400 );
}
try {
$this->handle_site_code( $code, $site_code );
} catch ( Missing_Verification_Exception $exception ) {
$this->redirect_to_proxy( $code, compact( 'site_code', 'step' ) );
} catch ( Exchange_Site_Code_Exception $exception ) {
$this->redirect_to_splash();
}
$credentials = $this->credentials->get();
$site_id = ! empty( $credentials['oauth2_client_id'] ) ? $credentials['oauth2_client_id'] : '';
$this->redirect_to_proxy( $code, compact( 'site_id', 'step' ) );
}
/**
* 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,60 @@
<?php
/**
* Class Google\Site_Kit\Core\Authentication\User_Input_State
*
* @package Google\Site_Kit
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Core\Authentication;
use Google\Site_Kit\Core\Storage\User_Setting;
/**
* Class representing user_input_state for the user.
*
* @since 1.20.0
* @access private
* @ignore
*/
final class User_Input_State extends User_Setting {
/**
* User option key.
*/
const OPTION = 'googlesitekit_user_input_state';
/**
* Value required key.
*/
const VALUE_REQUIRED = 'required';
/**
* Value completed key.
*/
const VALUE_COMPLETED = 'completed';
/**
* Value missing key.
*/
const VALUE_MISSING = 'missing';
/**
* Gets the callback for sanitizing the setting's value before saving.
*
* @since 1.23.0
*
* @return callable|null
*/
protected function get_sanitize_callback() {
return function( $value ) {
if ( ! in_array( $value, array( self::VALUE_COMPLETED, self::VALUE_MISSING, self::VALUE_REQUIRED, '' ), true ) ) {
return false;
}
return $value;
};
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,61 @@
<?php
/**
* Site Kit Authentication CLI Commands
*
* @package Google\Site_Kit\Core\CLI
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Core\CLI;
use Google\Site_Kit\Core\Storage\Options;
use Google\Site_Kit\Core\Storage\User_Options;
use Google\Site_Kit\Core\Storage\Transients;
use Google\Site_Kit\Core\Authentication\Authentication;
use WP_CLI;
/**
* Manages Site Kit user authentication for Google APIs.
*
* @since 1.11.0
* @access private
* @ignore
*/
class Authentication_CLI_Command extends CLI_Command {
/**
* Disconnects a user from Site Kit, removing their relevant user options and revoking their token.
*
* ## OPTIONS
*
* --id=<id>
* : User ID to disconnect.
*
* ## EXAMPLES
*
* wp google-site-kit auth disconnect --id=11
*
* @alias revoke
*
* @since 1.11.0
*
* @param array $args Array of arguments.
* @param array $assoc_args Array of associated arguments.
*/
public function disconnect( $args, $assoc_args ) {
$user_id = absint( $assoc_args['id'] );
$authentication = new Authentication(
$this->context,
new Options( $this->context ),
new User_Options( $this->context, $user_id ),
new Transients( $this->context )
);
$authentication->disconnect();
WP_CLI::success( sprintf( 'User with ID %d successfully disconnected.', $user_id ) );
}
}

View File

@ -0,0 +1,45 @@
<?php
/**
* Site Kit CLI Command
*
* @package Google\Site_Kit\Core\CLI
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Core\CLI;
use Google\Site_Kit\Context;
use WP_CLI_Command;
/**
* Base CLI Command class.
*
* @since 1.11.0
* @access private
* @ignore
*/
class CLI_Command extends WP_CLI_Command {
/**
* Plugin context.
*
* @since 1.11.0
*
* @var Context
*/
protected $context;
/**
* Constructor.
*
* @since 1.11.0
*
* @param Context $context Plugin context.
*/
public function __construct( Context $context ) {
$this->context = $context;
}
}

View File

@ -0,0 +1,55 @@
<?php
/**
* Class Google\Site_Kit\Core\CLI\CLI_Commands
*
* @package Google\Site_Kit\Core\CLI
* @copyright 2021 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Core\CLI;
use Google\Site_Kit\Context;
use WP_CLI;
/**
* CLI commands hub class.
*
* @since 1.11.0
* @access private
* @ignore
*/
class CLI_Commands {
/**
* Plugin context.
*
* @since 1.11.0
*
* @var Context
*/
private $context;
/**
* Constructor.
*
* @since 1.11.0
*
* @param Context $context Plugin context.
*/
public function __construct( Context $context ) {
$this->context = $context;
}
/**
* Registers WP CLI commands.
*
* @since 1.11.0
*/
public function register() {
WP_CLI::add_command( 'google-site-kit auth', new Authentication_CLI_Command( $this->context ) );
WP_CLI::add_command( 'google-site-kit reset', new Reset_CLI_Command( $this->context ) );
}
}

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