initial
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
/**
|
||||
* Action Hooks for Jetpack connection assets.
|
||||
*
|
||||
* @package automattic/jetpack-connection
|
||||
*/
|
||||
|
||||
// If WordPress's plugin API is available already, use it. If not,
|
||||
// drop data into `$wp_filter` for `WP_Hook::build_preinitialized_hooks()`.
|
||||
if ( function_exists( 'add_action' ) ) {
|
||||
add_action(
|
||||
'plugins_loaded',
|
||||
array( Automattic\Jetpack\Connection\Connection_Assets::class, 'configure' ),
|
||||
1
|
||||
);
|
||||
} else {
|
||||
global $wp_filter;
|
||||
// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
|
||||
$wp_filter['plugins_loaded'][1][] = array(
|
||||
'accepted_args' => 0,
|
||||
'function' => array( Automattic\Jetpack\Connection\Connection_Assets::class, 'configure' ),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<?php return array('dependencies' => array('react', 'react-jsx-runtime', 'wp-components', 'wp-compose', 'wp-data', 'wp-element', 'wp-i18n', 'wp-polyfill', 'wp-url'), 'version' => '985abd39a545d956cbb2');
|
||||
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 54 KiB |
@@ -0,0 +1,17 @@
|
||||
<svg width="380" height="183" viewBox="0 0 380 183" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g opacity="0.75" filter="url(#filter0_f_5953_11924)">
|
||||
<path d="M360 -60C360 7.93102 304.931 63 237 63C169.069 63 114 7.93102 114 -60C114 -127.931 169.069 -183 237 -183C304.931 -183 360 -127.931 360 -60Z" fill="url(#paint0_linear_5953_11924)"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_f_5953_11924" x="-6" y="-303" width="486" height="486" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="60" result="effect1_foregroundBlur_5953_11924"/>
|
||||
</filter>
|
||||
<linearGradient id="paint0_linear_5953_11924" x1="360" y1="-60" x2="114" y2="-60" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#48FF50"/>
|
||||
<stop offset="0.471846" stop-color="#108642"/>
|
||||
<stop offset="1" stop-color="#2E38FA"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 990 B |
@@ -0,0 +1 @@
|
||||
<?php return array('dependencies' => array('jetpack-script-data', 'react', 'react-jsx-runtime', 'wp-a11y', 'wp-components', 'wp-data', 'wp-date', 'wp-element', 'wp-i18n', 'wp-polyfill', 'wp-primitives', 'wp-url'), 'version' => 'b16d63ffff51bb15c0b3');
|
||||
@@ -0,0 +1 @@
|
||||
<?php return array('dependencies' => array(), 'version' => 'a3e26c4e5b7f15f38c40');
|
||||
@@ -0,0 +1 @@
|
||||
.jetpack-sso-admin-create-user-invite-message{width:550px}.jetpack-sso-admin-create-user-invite-message-link-sso{text-decoration:none}#createuser .form-field textarea{width:25em}#createuser .form-field [type=checkbox]{width:1rem}#custom_email_message_description{color:#646970;font-size:12px;max-width:25rem}
|
||||
@@ -0,0 +1 @@
|
||||
document.addEventListener("DOMContentLoaded",function(){const e=document.getElementById("send_user_notification"),d=document.getElementById("user_external_contractor"),t=document.getElementById("invite_user_wpcom"),n=document.getElementById("custom_email_message_block");t&&e&&n&&(t.addEventListener("change",function(){e.disabled=t.checked,t.checked?(e.checked=!1,d&&(d.disabled=!1),n.style.display="table"):(d&&(d.disabled=!0,d.checked=!1),n.style.display="none")}),t.checked&&(e.disabled=!0,e.checked=!1,n.style.display="table"),t.checked||(d&&(d.disabled=!0),n.style.display="none"))});
|
||||
@@ -0,0 +1 @@
|
||||
.jetpack-sso-admin-create-user-invite-message{width:550px}.jetpack-sso-admin-create-user-invite-message-link-sso{text-decoration:none}#createuser .form-field textarea{width:25em}#createuser .form-field [type=checkbox]{width:1rem}#custom_email_message_description{color:#646970;font-size:12px;max-width:25rem}
|
||||
@@ -0,0 +1 @@
|
||||
<?php return array('dependencies' => array(), 'version' => '0a84fe0bec6dbd6ec0ac');
|
||||
@@ -0,0 +1 @@
|
||||
#loginform{padding-bottom:92px;position:relative!important}.jetpack-sso-repositioned #loginform{padding-bottom:26px}#loginform :is(#jetpack-sso-wrap,#jetpack-sso-wrap *){box-sizing:border-box}#jetpack-sso-wrap__action,#jetpack-sso-wrap__user{display:none}.jetpack-sso-form-display :is(#jetpack-sso-wrap__action,#jetpack-sso-wrap__user){display:block}#jetpack-sso-wrap{bottom:20px;margin-left:-24px;margin-right:-24px;padding:0 24px;position:absolute;width:100%}.jetpack-sso-repositioned #jetpack-sso-wrap{bottom:auto;margin-left:0;margin-right:0;margin-top:16px;padding:0;position:relative}.jetpack-sso-form-display #jetpack-sso-wrap{bottom:auto;margin-left:0;margin-right:0;margin-top:0;padding:0;position:relative}#loginform #jetpack-sso-wrap p{color:#777;margin-bottom:16px}#jetpack-sso-wrap a{display:block;text-align:center;text-decoration:none;width:100%}#jetpack-sso-wrap .jetpack-sso-toggle.wpcom{display:none}.jetpack-sso-form-display #jetpack-sso-wrap .jetpack-sso-toggle.wpcom{display:block}.jetpack-sso-form-display #jetpack-sso-wrap .jetpack-sso-toggle.default,.jetpack-sso-form-display #loginform>:is(div,p){display:none}.jetpack-sso-form-display #loginform #jetpack-sso-wrap{display:block}.jetpack-sso-form-display #loginform{padding:26px 24px}.jetpack-sso-or{margin-bottom:16px;position:relative;text-align:center}.jetpack-sso-or:before{background:#dcdcde;content:"";height:1px;left:0;position:absolute;top:50%;width:100%}.jetpack-sso-or span{background:#fff;color:#777;padding:0 8px;position:relative;text-transform:uppercase}#jetpack-sso-wrap .button{align-items:center;display:flex;height:36px;justify-content:center;margin-bottom:16px;width:100%}#jetpack-sso-wrap .button .genericon-wordpress{font-size:24px;margin-right:4px}#jetpack-sso-wrap__user img{border-radius:50%;display:block;margin:0 auto 16px}#jetpack-sso-wrap__user h2{font-size:21px;font-weight:300;margin-bottom:16px;text-align:center}#jetpack-sso-wrap__user h2 span{font-weight:700}.jetpack-sso-wrap__reauth{margin-bottom:16px}.jetpack-sso-form-display #nav{display:none}.jetpack-sso-form-display #backtoblog{margin:24px 0 0}.jetpack-sso-clear:after{clear:both;content:"";display:table}
|
||||
@@ -0,0 +1 @@
|
||||
document.addEventListener("DOMContentLoaded",()=>{const e=document.querySelector("body"),t=document.querySelector(".jetpack-sso-toggle"),d=document.getElementById("user_login"),o=document.getElementById("user_pass"),s=document.getElementById("jetpack-sso-wrap"),n=document.getElementById("loginform"),c=document.createElement("div");c.className="jetpack-sso-clear",n.appendChild(c),c.appendChild(document.querySelector("p.forgetmenot")),c.appendChild(document.querySelector("p.submit")),n.appendChild(s),e.classList.add("jetpack-sso-repositioned"),t.addEventListener("click",t=>{t.preventDefault(),e.classList.toggle("jetpack-sso-form-display"),e.classList.contains("jetpack-sso-form-display")||(d.focus(),o.disabled=!1)})});
|
||||
@@ -0,0 +1 @@
|
||||
#loginform{padding-bottom:92px;position:relative!important}.jetpack-sso-repositioned #loginform{padding-bottom:26px}#loginform :is(#jetpack-sso-wrap,#jetpack-sso-wrap *){box-sizing:border-box}#jetpack-sso-wrap__action,#jetpack-sso-wrap__user{display:none}.jetpack-sso-form-display :is(#jetpack-sso-wrap__action,#jetpack-sso-wrap__user){display:block}#jetpack-sso-wrap{bottom:20px;margin-left:-24px;margin-right:-24px;padding:0 24px;position:absolute;width:100%}.jetpack-sso-repositioned #jetpack-sso-wrap{bottom:auto;margin-left:0;margin-right:0;margin-top:16px;padding:0;position:relative}.jetpack-sso-form-display #jetpack-sso-wrap{bottom:auto;margin-left:0;margin-right:0;margin-top:0;padding:0;position:relative}#loginform #jetpack-sso-wrap p{color:#777;margin-bottom:16px}#jetpack-sso-wrap a{display:block;text-align:center;text-decoration:none;width:100%}#jetpack-sso-wrap .jetpack-sso-toggle.wpcom{display:none}.jetpack-sso-form-display #jetpack-sso-wrap .jetpack-sso-toggle.wpcom{display:block}.jetpack-sso-form-display #jetpack-sso-wrap .jetpack-sso-toggle.default,.jetpack-sso-form-display #loginform>:is(div,p){display:none}.jetpack-sso-form-display #loginform #jetpack-sso-wrap{display:block}.jetpack-sso-form-display #loginform{padding:26px 24px}.jetpack-sso-or{margin-bottom:16px;position:relative;text-align:center}.jetpack-sso-or:before{background:#dcdcde;content:"";height:1px;position:absolute;right:0;top:50%;width:100%}.jetpack-sso-or span{background:#fff;color:#777;padding:0 8px;position:relative;text-transform:uppercase}#jetpack-sso-wrap .button{align-items:center;display:flex;height:36px;justify-content:center;margin-bottom:16px;width:100%}#jetpack-sso-wrap .button .genericon-wordpress{font-size:24px;margin-left:4px}#jetpack-sso-wrap__user img{border-radius:50%;display:block;margin:0 auto 16px}#jetpack-sso-wrap__user h2{font-size:21px;font-weight:300;margin-bottom:16px;text-align:center}#jetpack-sso-wrap__user h2 span{font-weight:700}.jetpack-sso-wrap__reauth{margin-bottom:16px}.jetpack-sso-form-display #nav{display:none}.jetpack-sso-form-display #backtoblog{margin:24px 0 0}.jetpack-sso-clear:after{clear:both;content:"";display:table}
|
||||
@@ -0,0 +1 @@
|
||||
<?php return array('dependencies' => array('wp-polyfill'), 'version' => '9b04da34a7196789f44c');
|
||||
@@ -0,0 +1 @@
|
||||
document.addEventListener("DOMContentLoaded",function(){function t(){this.querySelector(".jetpack-sso-invitation-tooltip").style.display="block"}function e(t){t.target.ownerDocument.activeElement!==t.target&&(this.querySelector(".jetpack-sso-invitation-tooltip").style.display="none")}document.querySelectorAll(".jetpack-sso-invitation-tooltip-icon:not(.sso-disconnected-user)").forEach(function(t){t.innerHTML+=" [?]";const e=document.createElement("span");e.classList.add("jetpack-sso-invitation-tooltip");const n=window.Jetpack_SSOTooltip.tooltipString;function o(){t.appendChild(e),e.style.display="block"}function i(){t.ownerDocument.activeElement!==t&&t.removeChild(e)}e.innerHTML+=n,t.addEventListener("mouseenter",o),t.addEventListener("focus",o),t.addEventListener("mouseleave",i),t.addEventListener("blur",i)}),document.querySelectorAll(".jetpack-sso-invitation-tooltip-icon:not(.jetpack-sso-status-column)").forEach(function(n){n.addEventListener("mouseenter",t),n.addEventListener("focus",t),n.addEventListener("mouseleave",e),n.addEventListener("blur",e)})});
|
||||
@@ -0,0 +1 @@
|
||||
<?php return array('dependencies' => array('wp-polyfill'), 'version' => '2a9e9767fedce3c596e7');
|
||||
@@ -0,0 +1 @@
|
||||
document.addEventListener("DOMContentLoaded",function(){document.querySelectorAll(".jetpack-connection-tooltip").forEach(function(o){o.textContent=window.jetpackConnectionTooltips.columnTooltip})});
|
||||
@@ -0,0 +1 @@
|
||||
<?php return array('dependencies' => array(), 'version' => 'a63290feb29ef2daf665');
|
||||
@@ -0,0 +1 @@
|
||||
!function(t,a){window.jpTracksAJAX=window.jpTracksAJAX||{};const o="dops:analytics"===localStorage.getItem("debug");window.jpTracksAJAX.record_ajax_event=function(n,c,e){"object"==typeof e&&null!==e&&window.jpTracksContext&&window.jpTracksContext.blog_id&&!e.blog_id&&(e.blog_id=window.jpTracksContext.blog_id);const r={tracksNonce:a.jpTracksAJAX_nonce,action:"jetpack_tracks",tracksEventType:c,tracksEventName:n,tracksEventProp:e||!1};return t.ajax({type:"POST",url:a.ajaxurl,data:r,success:function(t){o&&console.log("AJAX tracks event recorded: ",r,t)}})},t(document).ready(function(){t("body").on("click",".jptracks a, a.jptracks",function(a){const o=t(a.target),n=o.closest(".jptracks"),c=n.attr("data-jptracks-name");if(void 0===c)return;const e=n.attr("data-jptracks-prop")||!1,r=o.attr("href"),s=o.get(0).target;let i=null;r&&s&&"_self"!==s&&(i=window.open("",s),i.opener=null),a.preventDefault(),window.jpTracksAJAX.record_ajax_event(c,"click",e).always(function(){if(r&&!o.hasClass("thickbox")){if(i)return void(i.location=r);window.location=r}})})})}(jQuery,jpTracksAJAX);
|
||||
@@ -0,0 +1 @@
|
||||
<?php return array('dependencies' => array(), 'version' => 'a1795bfc061f496e47cf');
|
||||
@@ -0,0 +1 @@
|
||||
(()=>{var e={7961(e){let n;window._tkq=window._tkq||[];const t=console.error;const o={initialize:function(e,n){o.setUser(e,n),o.identifyUser()},mc:{bumpStat:function(e,n){const t=function(e,n){let t="";if("object"==typeof e)for(const n in e)t+="&x_"+encodeURIComponent(n)+"="+encodeURIComponent(e[n]);else t="&x_"+encodeURIComponent(e)+"="+encodeURIComponent(n);return t}(e,n);(new Image).src=document.location.protocol+"//pixel.wp.com/g.gif?v=wpcom-no-pv"+t+"&t="+Math.random()}},tracks:{recordEvent:function(e,n){n=n||{},0===e.indexOf("jetpack_")?window._tkq.push(["recordEvent",e,n]):t('- Event name must be prefixed by "jetpack_"')},recordPageView:function(e){o.tracks.recordEvent("jetpack_page_view",{path:e})}},setUser:function(e,t){n={ID:e,username:t}},identifyUser:function(){n&&window._tkq.push(["identifyUser",n.ID,n.username])},clearedIdentity:function(){window._tkq.push(["clearIdentity"])}};e.exports=o}},n={};var t=function t(o){var r=n[o];if(void 0!==r)return r.exports;var i=n[o]={exports:{}};return e[o](i,i.exports,t),i.exports}(7961);window.analytics=t})();
|
||||
@@ -0,0 +1,187 @@
|
||||
<?php
|
||||
/**
|
||||
* IXR_Client
|
||||
*
|
||||
* @package automattic/jetpack-connection
|
||||
*
|
||||
* @since 1.7.0
|
||||
* @since-jetpack 1.5
|
||||
* @since-jetpack 7.7 Moved to the jetpack-connection package.
|
||||
*/
|
||||
|
||||
use Automattic\Jetpack\Connection\Client;
|
||||
use Automattic\Jetpack\Connection\Manager;
|
||||
|
||||
/**
|
||||
* Disable direct access.
|
||||
*/
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit( 0 );
|
||||
}
|
||||
|
||||
if ( ! class_exists( IXR_Client::class ) ) {
|
||||
require_once ABSPATH . WPINC . '/class-IXR.php';
|
||||
}
|
||||
|
||||
/**
|
||||
* A Jetpack implementation of the WordPress core IXR client.
|
||||
*/
|
||||
class Jetpack_IXR_Client extends IXR_Client {
|
||||
/**
|
||||
* Jetpack args, used for the remote requests.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $jetpack_args = null;
|
||||
|
||||
/**
|
||||
* Remote Response Headers.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $response_headers = null;
|
||||
|
||||
/**
|
||||
* Holds the raw remote response from the latest call to query().
|
||||
*
|
||||
* @var null|array|WP_Error
|
||||
*/
|
||||
public $last_response = null;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* Initialize a new Jetpack IXR client instance.
|
||||
*
|
||||
* @param array $args Jetpack args, used for the remote requests.
|
||||
* @param string|bool $path Path to perform the reuqest to.
|
||||
* @param int $port Port number.
|
||||
* @param int $timeout The connection timeout, in seconds.
|
||||
*/
|
||||
public function __construct( $args = array(), $path = false, $port = 80, $timeout = 15 ) {
|
||||
$connection = new Manager();
|
||||
|
||||
$defaults = array(
|
||||
'url' => $connection->xmlrpc_api_url(),
|
||||
'user_id' => 0,
|
||||
'headers' => array(),
|
||||
);
|
||||
|
||||
$args = wp_parse_args( $args, $defaults );
|
||||
$args['headers'] = array_merge( array( 'Content-Type' => 'text/xml' ), (array) $args['headers'] );
|
||||
|
||||
$this->jetpack_args = $args;
|
||||
|
||||
$this->IXR_Client( $args['url'], $path, $port, $timeout );
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the IXR request.
|
||||
*
|
||||
* @param mixed ...$args IXR method and args.
|
||||
*
|
||||
* @return bool True if request succeeded, false otherwise.
|
||||
*/
|
||||
public function query( ...$args ) {
|
||||
$method = array_shift( $args );
|
||||
$request = new IXR_Request( $method, $args );
|
||||
$xml = trim( $request->getXml() );
|
||||
|
||||
$response = Client::remote_request( $this->jetpack_args, $xml );
|
||||
|
||||
// Store response headers.
|
||||
$this->response_headers = wp_remote_retrieve_headers( $response );
|
||||
|
||||
$this->last_response = $response;
|
||||
if ( is_array( $this->last_response ) && isset( $this->last_response['http_response'] ) ) {
|
||||
// If the expected array response is received, format the data as plain arrays.
|
||||
$this->last_response = $this->last_response['http_response']->to_array();
|
||||
$this->last_response['headers'] = $this->last_response['headers']->getAll();
|
||||
}
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
$this->error = new IXR_Error( -10520, sprintf( 'Jetpack: [%s] %s', $response->get_error_code(), $response->get_error_message() ) );
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ! $response ) {
|
||||
$this->error = new IXR_Error( -10520, 'Jetpack: Unknown Error' );
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
|
||||
$this->error = new IXR_Error( -32300, 'transport error - HTTP status code was not 200' );
|
||||
return false;
|
||||
}
|
||||
|
||||
$content = wp_remote_retrieve_body( $response );
|
||||
|
||||
// Now parse what we've got back.
|
||||
$this->message = new IXR_Message( $content );
|
||||
if ( ! $this->message->parse() ) {
|
||||
// XML error.
|
||||
$this->error = new IXR_Error( -32700, 'parse error. not well formed' );
|
||||
return false;
|
||||
}
|
||||
|
||||
// Is the message a fault?
|
||||
if ( 'fault' === $this->message->messageType ) {
|
||||
$this->error = new IXR_Error( $this->message->faultCode, $this->message->faultString );
|
||||
return false;
|
||||
}
|
||||
|
||||
// Message must be OK.
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the Jetpack error from the result of the last request.
|
||||
*
|
||||
* @param int $fault_code Fault code.
|
||||
* @param string $fault_string Fault string.
|
||||
* @return WP_Error Error object.
|
||||
*/
|
||||
public function get_jetpack_error( $fault_code = null, $fault_string = null ) {
|
||||
if ( $fault_code === null ) {
|
||||
$fault_code = $this->error->code;
|
||||
}
|
||||
|
||||
if ( $fault_string === null ) {
|
||||
$fault_string = $this->error->message;
|
||||
}
|
||||
|
||||
if ( preg_match( '#jetpack:\s+\[(\w+)\]\s*(.*)?$#i', $fault_string, $match ) ) {
|
||||
$code = $match[1];
|
||||
$message = $match[2];
|
||||
$status = $fault_code;
|
||||
return new WP_Error( $code, $message, $status );
|
||||
}
|
||||
|
||||
return new WP_Error( "IXR_{$fault_code}", $fault_string );
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a response header if set.
|
||||
*
|
||||
* @param string $name header name.
|
||||
* @return string|bool Header value if set, false if not set.
|
||||
*/
|
||||
public function get_response_header( $name ) {
|
||||
if ( isset( $this->response_headers[ $name ] ) ) {
|
||||
return $this->response_headers[ $name ];
|
||||
}
|
||||
// case-insensitive header names: http://www.ietf.org/rfc/rfc2616.txt.
|
||||
if ( isset( $this->response_headers[ strtolower( $name ) ] ) ) {
|
||||
return $this->response_headers[ strtolower( $name ) ];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the raw response for the last query() call.
|
||||
*
|
||||
* @return null|array|WP_Error
|
||||
*/
|
||||
public function get_last_response() {
|
||||
return $this->last_response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
/**
|
||||
* IXR_ClientMulticall
|
||||
*
|
||||
* @package automattic/jetpack-connection
|
||||
*
|
||||
* @since 1.7.0
|
||||
* @since-jetpack 1.5
|
||||
* @since-jetpack 7.7 Moved to the jetpack-connection package.
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit( 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* A Jetpack implementation of the WordPress core IXR client, capable of multiple calls in a single request.
|
||||
*/
|
||||
class Jetpack_IXR_ClientMulticall extends Jetpack_IXR_Client {
|
||||
/**
|
||||
* Storage for the IXR calls.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $calls = array();
|
||||
|
||||
/**
|
||||
* Add a IXR call to the client.
|
||||
* First argument is the method name.
|
||||
* The rest of the arguments are the params specified to the method.
|
||||
*
|
||||
* @param string[] ...$args IXR args.
|
||||
*/
|
||||
public function addCall( ...$args ) {
|
||||
$method_name = array_shift( $args );
|
||||
$struct = array(
|
||||
'methodName' => $method_name,
|
||||
'params' => $args,
|
||||
);
|
||||
$this->calls[] = $struct;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the IXR multicall request.
|
||||
*
|
||||
* @param string[] ...$args IXR args.
|
||||
*
|
||||
* @return bool True if request succeeded, false otherwise.
|
||||
*/
|
||||
public function query( ...$args ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
$this->calls = $this->sort_calls( $this->calls );
|
||||
|
||||
// Prepare multicall, then call the parent::query() method.
|
||||
return parent::query( 'system.multicall', $this->calls );
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort the IXR calls.
|
||||
* Make sure syncs are always done first preserving relative order.
|
||||
*
|
||||
* @param array $calls Calls to sort.
|
||||
* @return array Sorted calls.
|
||||
*/
|
||||
public function sort_calls( $calls ) {
|
||||
$sync_calls = array();
|
||||
$other_calls = array();
|
||||
|
||||
foreach ( $calls as $call ) {
|
||||
if ( 'jetpack.syncContent' === $call['methodName'] ) {
|
||||
$sync_calls[] = $call;
|
||||
} else {
|
||||
$other_calls[] = $call;
|
||||
}
|
||||
}
|
||||
|
||||
return array_merge( $sync_calls, $other_calls );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,726 @@
|
||||
<?php
|
||||
/**
|
||||
* Legacy Jetpack_Options class.
|
||||
*
|
||||
* @package automattic/jetpack-connection
|
||||
*/
|
||||
|
||||
use Automattic\Jetpack\Constants;
|
||||
|
||||
/**
|
||||
* Class Jetpack_Options
|
||||
*/
|
||||
class Jetpack_Options {
|
||||
|
||||
/**
|
||||
* An array that maps a grouped option type to an option name.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static $grouped_options = array(
|
||||
'compact' => 'jetpack_options',
|
||||
'private' => 'jetpack_private_options',
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns an array of option names for a given type.
|
||||
*
|
||||
* @param string $type The type of option to return. Defaults to 'compact'.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_option_names( $type = 'compact' ) {
|
||||
switch ( $type ) {
|
||||
case 'non-compact':
|
||||
case 'non_compact':
|
||||
return array(
|
||||
'activated',
|
||||
'active_modules',
|
||||
'active_modules_initialized', // (bool) used to determine that all the default modules were activated, so we know how to act on a reconnection.
|
||||
'allowed_xsite_search_ids', // (array) Array of WP.com blog ids that are allowed to search the content of this site
|
||||
'available_modules',
|
||||
'do_activate',
|
||||
'log',
|
||||
'slideshow_background_color',
|
||||
'widget_twitter',
|
||||
'wpcc_options',
|
||||
'relatedposts',
|
||||
'file_data',
|
||||
'autoupdate_plugins', // (array) An array of plugin ids ( eg. jetpack/jetpack ) that should be autoupdated
|
||||
'autoupdate_plugins_translations', // (array) An array of plugin ids ( eg. jetpack/jetpack ) that should be autoupdated translation files.
|
||||
'autoupdate_themes', // (array) An array of theme ids ( eg. twentyfourteen ) that should be autoupdated
|
||||
'autoupdate_themes_translations', // (array) An array of theme ids ( eg. twentyfourteen ) that should autoupdated translation files.
|
||||
'autoupdate_core', // (bool) Whether or not to autoupdate core
|
||||
'autoupdate_translations', // (bool) Whether or not to autoupdate all translations
|
||||
'json_api_full_management', // (bool) Allow full management (eg. Activate, Upgrade plugins) of the site via the JSON API.
|
||||
'sync_non_public_post_stati', // (bool) Allow synchronisation of posts and pages with non-public status.
|
||||
'site_icon_url', // (string) url to the full site icon
|
||||
'site_icon_id', // (int) Attachment id of the site icon file
|
||||
'dismissed_manage_banner', // (bool) Dismiss Jetpack manage banner allows the user to dismiss the banner permanently
|
||||
'unique_connection', // (array) A flag to determine a unique connection to wordpress.com two values "connected" and "disconnected" with values for how many times each has occurred
|
||||
'unique_registrations', // (integer) A counter of how many times the site was registered
|
||||
'protect_whitelist', // (array) IP Address for the Protect module to ignore
|
||||
'sync_error_idc', // (bool|array) false or array containing the site's home and siteurl at time of IDC error
|
||||
'sync_health_status', // (bool|array) An array of data relating to Jetpack's sync health.
|
||||
'safe_mode_confirmed', // (bool) True if someone confirms that this site was correctly put into safe mode automatically after an identity crisis is discovered.
|
||||
'migrate_for_idc', // (bool) True if someone confirms that this site should migrate stats and subscribers from its previous URL
|
||||
'ab_connect_banner_green_bar', // (int) Version displayed of the A/B test for the green bar at the top of the connect banner.
|
||||
'tos_agreed', // (bool) Whether or not the TOS for connection has been agreed upon.
|
||||
'static_asset_cdn_files', // (array) An nested array of files that we can swap out for cdn versions.
|
||||
'mapbox_api_key', // (string) Mapbox API Key, for use with Map block.
|
||||
'mailchimp', // (string) Mailchimp keyring data, for mailchimp block.
|
||||
'xmlrpc_errors', // (array) Keys are XML-RPC signature error codes. Values are truthy.
|
||||
'dismissed_wizard_banner', // (int) (DEPRECATED) True if the Wizard banner has been dismissed.
|
||||
);
|
||||
|
||||
case 'private':
|
||||
return array(
|
||||
'blog_token', // (string) The Client Secret/Blog Token of this site.
|
||||
'user_token', // (string) The User Token of this site. (deprecated)
|
||||
'user_tokens', // (array) User Tokens for each user of this site who has connected to jetpack.wordpress.com.
|
||||
'purchase_token', // (string) Token for logged out user purchases.
|
||||
'token_lock', // (string) Token lock in format `expiration_date|||site_url`.
|
||||
);
|
||||
|
||||
case 'network':
|
||||
return array(
|
||||
'file_data', // (array) List of absolute paths to all Jetpack modules
|
||||
);
|
||||
}
|
||||
|
||||
return array(
|
||||
'id', // (int) The Client ID/WP.com Blog ID of this site.
|
||||
'publicize_connections', // (array) An array of Publicize connections from WordPress.com.
|
||||
'master_user', // (int) The local User ID of the user who connected this site to jetpack.wordpress.com.
|
||||
'version', // (string) Used during upgrade procedure to auto-activate new modules. version:time.
|
||||
'old_version', // (string) Used to determine which modules are the most recently added. previous_version:time.
|
||||
'fallback_no_verify_ssl_certs', // (int) Flag for determining if this host must skip SSL Certificate verification due to misconfigured SSL.
|
||||
'time_diff', // (int) Offset between Jetpack server's clocks and this server's clocks. Jetpack Server Time = time() + (int) Jetpack_Options::get_option( 'time_diff' )
|
||||
'public', // (int|bool) If we think this site is public or not (1, 0), false if we haven't yet tried to figure it out.
|
||||
'videopress', // (array) VideoPress options array.
|
||||
'is_network_site', // (int|bool) If we think this site is a network or a single blog (1, 0), false if we haven't yet tried to figue it out.
|
||||
'social_links', // (array) The specified links for each social networking site.
|
||||
'identity_crisis_whitelist', // (array) An array of options, each having an array of the values whitelisted for it.
|
||||
'gplus_authors', // (array) The Google+ authorship information for connected users.
|
||||
'last_heartbeat', // (int) The timestamp of the last heartbeat that fired.
|
||||
'hide_jitm', // (array) A list of just in time messages that we should not show because they have been dismissed by the user.
|
||||
'custom_css_4.7_migration', // (bool) Whether Custom CSS has scanned for and migrated any legacy CSS CPT entries to the new Core format.
|
||||
'image_widget_migration', // (bool) Whether any legacy Image Widgets have been converted to the new Core widget.
|
||||
'gallery_widget_migration', // (bool) Whether any legacy Gallery Widgets have been converted to the new Core widget.
|
||||
'sso_first_login', // (bool) Is this the first time the user logins via SSO.
|
||||
'dismissed_hints', // (array) Part of Plugin Search Hints. List of cards that have been dismissed.
|
||||
'first_admin_view', // (bool) Set to true the first time the user views the admin. Usually after the initial connection.
|
||||
'setup_wizard_questionnaire', // (array) (DEPRECATED) List of user choices from the setup wizard.
|
||||
'setup_wizard_status', // (string) (DEPRECATED) Status of the setup wizard.
|
||||
'licensing_error', // (string) Last error message occurred while attaching licenses that is yet to be surfaced to the user.
|
||||
'recommendations_data', // (array) The user choice and other data for the recommendations.
|
||||
'recommendations_step', // (string) The current step of the recommendations.
|
||||
'recommendations_conditional', // (array) An array of action-based recommendations.
|
||||
'licensing_activation_notice_dismiss', // (array) The `last_detached_count` and the `last_dismissed_time` for the user-license activation notice.
|
||||
'has_seen_wc_connection_modal', // (bool) Whether the site has displayed the WooCommerce Connection modal
|
||||
'partner_coupon', // (string) A Jetpack partner issued coupon to promote a sale together with Jetpack.
|
||||
'partner_coupon_added', // (string) A date for when `partner_coupon` was added, so we can auto-purge after a certain time interval.
|
||||
'dismissed_backup_review_restore', // (bool) Determines if the component review request is dismissed for successful restore requests.
|
||||
'dismissed_backup_review_backups', // (bool) Determines if the component review request is dismissed for successful backup requests.
|
||||
'identity_crisis_url_secret', // (array) The IDC URL secret and its expiration date.
|
||||
'identity_crisis_ip_requester', // (array) The IDC IP address and its expiration date.
|
||||
'dismissed_welcome_banner', // (bool) Determines if the welcome banner has been dismissed or not.
|
||||
'recommendations_evaluation', // (object) Catalog of recommended modules with corresponding score following successful site evaluation in Welcome Banner.
|
||||
'dismissed_recommendations', // (bool) Determines if the recommendations have been dismissed or not.
|
||||
'recommendations_first_run', // (bool) Determines if the current recommendations are the initial default auto-loaded ones (without user input).
|
||||
'historically_active_modules', // (array) List of installed plugins/enabled modules that have at one point in time been active and working
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the option name valid?
|
||||
*
|
||||
* @param string $name The name of the option.
|
||||
* @param string|null $group The name of the group that the option is in. Default to null, which will search non_compact.
|
||||
*
|
||||
* @return bool Is the option name valid?
|
||||
*/
|
||||
public static function is_valid( $name, $group = null ) {
|
||||
if ( is_array( $name ) ) {
|
||||
$compact_names = array();
|
||||
foreach ( array_keys( self::$grouped_options ) as $_group ) {
|
||||
$compact_names = array_merge( $compact_names, self::get_option_names( $_group ) );
|
||||
}
|
||||
|
||||
$result = array_diff( $name, self::get_option_names( 'non_compact' ), $compact_names );
|
||||
|
||||
return empty( $result );
|
||||
}
|
||||
|
||||
if ( $group === null || 'non_compact' === $group ) {
|
||||
if ( in_array( $name, self::get_option_names( $group ), true ) ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ( array_keys( self::$grouped_options ) as $_group ) {
|
||||
if ( $group === null || $group === $_group ) {
|
||||
if ( in_array( $name, self::get_option_names( $_group ), true ) ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an option must be saved for the whole network in WP Multisite
|
||||
*
|
||||
* @param string $option_name Option name. It must come _without_ `jetpack_%` prefix. The method will prefix the option name.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_network_option( $option_name ) {
|
||||
if ( ! is_multisite() ) {
|
||||
return false;
|
||||
}
|
||||
return in_array( $option_name, self::get_option_names( 'network' ), true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the requested option.
|
||||
* This is a wrapper around `get_option_from_database` so that we can filter the option.
|
||||
*
|
||||
* @param string $name Option name. It must come _without_ `jetpack_%` prefix. The method will prefix the option name.
|
||||
* @param mixed $default (optional).
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public static function get_option( $name, $default = false ) {
|
||||
// Check if external storage should be used for this option
|
||||
if ( self::should_use_external_storage( $name ) ) {
|
||||
// Try external storage
|
||||
if ( class_exists( 'Automattic\Jetpack\Connection\External_Storage' ) ) {
|
||||
$external_value = \Automattic\Jetpack\Connection\External_Storage::get_value( $name );
|
||||
if ( null !== $external_value ) {
|
||||
return $external_value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter Jetpack Options.
|
||||
* Can be useful in environments when Jetpack is running with a different setup
|
||||
*
|
||||
* @since 1.7.0
|
||||
*
|
||||
* @param string $value The value from the database.
|
||||
* @param string $name Option name, _without_ `jetpack_%` prefix.
|
||||
* @return string $value, unless the filters modify it.
|
||||
*/
|
||||
return apply_filters( 'jetpack_options', self::get_option_from_database( $name, $default ), $name );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the requested option. Looks in jetpack_options or jetpack_$name as appropriate.
|
||||
*
|
||||
* @param string $name Option name. It must come _without_ `jetpack_%` prefix. The method will prefix the option name.
|
||||
* @param mixed $default (optional).
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
private static function get_option_from_database( $name, $default = false ) {
|
||||
if ( self::is_valid( $name, 'non_compact' ) ) {
|
||||
if ( self::is_network_option( $name ) ) {
|
||||
return get_site_option( "jetpack_$name", $default );
|
||||
}
|
||||
|
||||
return get_option( "jetpack_$name", $default );
|
||||
}
|
||||
|
||||
foreach ( array_keys( self::$grouped_options ) as $group ) {
|
||||
if ( self::is_valid( $name, $group ) ) {
|
||||
return self::get_grouped_option( $group, $name, $default );
|
||||
}
|
||||
}
|
||||
|
||||
return $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options that can be stored in external storage.
|
||||
*
|
||||
* @since 6.18.0
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static $external_storage_allowlist = array( 'blog_token', 'id', 'master_user', 'user_tokens' );
|
||||
|
||||
/**
|
||||
* Determines if external storage should be used for a given option.
|
||||
* Simple allowlist check with global killswitch.
|
||||
*
|
||||
* @since 6.17.0
|
||||
*
|
||||
* @param string $name Option name, _without_ `jetpack_%` prefix.
|
||||
* @return bool True if external storage should be checked for this option.
|
||||
*/
|
||||
private static function should_use_external_storage( $name ) {
|
||||
// Check allowlist and global killswitch
|
||||
if ( ! in_array( $name, self::$external_storage_allowlist, true ) ||
|
||||
( defined( 'JETPACK_EXTERNAL_STORAGE_DISABLED' ) && constant( 'JETPACK_EXTERNAL_STORAGE_DISABLED' ) ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the requested option, and ensures it's autoloaded in the future.
|
||||
* This does _not_ adjust the prefix in any way (does not prefix jetpack_%)
|
||||
*
|
||||
* @param string $name Option name.
|
||||
* @param mixed $default (optional).
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public static function get_option_and_ensure_autoload( $name, $default ) {
|
||||
// In this function the name is not adjusted by prefixing jetpack_
|
||||
// so if it has already prefixed, we'll replace it and then
|
||||
// check if the option name is a network option or not.
|
||||
$jetpack_name = preg_replace( '/^jetpack_/', '', $name, 1 );
|
||||
$is_network_option = self::is_network_option( $jetpack_name );
|
||||
$value = $is_network_option ? get_site_option( $name ) : get_option( $name );
|
||||
|
||||
if ( false === $value && false !== $default ) {
|
||||
if ( $is_network_option ) {
|
||||
add_site_option( $name, $default );
|
||||
} else {
|
||||
add_option( $name, $default );
|
||||
}
|
||||
$value = $default;
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update grouped option
|
||||
*
|
||||
* @param string $group Options group.
|
||||
* @param string $name Options name.
|
||||
* @param mixed $value Options value.
|
||||
*
|
||||
* @return bool Success or failure.
|
||||
*/
|
||||
private static function update_grouped_option( $group, $name, $value ) {
|
||||
$options = get_option( self::$grouped_options[ $group ] );
|
||||
if ( ! is_array( $options ) ) {
|
||||
$options = array();
|
||||
}
|
||||
$options[ $name ] = $value;
|
||||
|
||||
return update_option( self::$grouped_options[ $group ], $options );
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the single given option. Updates jetpack_options or jetpack_$name as appropriate.
|
||||
*
|
||||
* @param string $name Option name. It must come _without_ `jetpack_%` prefix. The method will prefix the option name.
|
||||
* @param mixed $value Option value.
|
||||
* @param bool|null $autoload If not compact option, allows specifying whether to autoload or not.
|
||||
*
|
||||
* @return bool Was the option successfully updated?
|
||||
*/
|
||||
public static function update_option( $name, $value, $autoload = null ) {
|
||||
/**
|
||||
* Fires before Jetpack updates a specific option.
|
||||
*
|
||||
* @since 1.1.2
|
||||
* @since-jetpack 3.0.0
|
||||
*
|
||||
* @param string $name The name of the option being updated.
|
||||
* @param mixed $value The new value of the option.
|
||||
*/
|
||||
do_action( 'pre_update_jetpack_option_' . $name, $name, $value );
|
||||
if ( self::is_valid( $name, 'non_compact' ) ) {
|
||||
if ( self::is_network_option( $name ) ) {
|
||||
return update_site_option( "jetpack_$name", $value );
|
||||
}
|
||||
|
||||
return update_option( "jetpack_$name", $value, $autoload );
|
||||
|
||||
}
|
||||
|
||||
foreach ( array_keys( self::$grouped_options ) as $group ) {
|
||||
if ( self::is_valid( $name, $group ) ) {
|
||||
return self::update_grouped_option( $group, $name, $value );
|
||||
}
|
||||
}
|
||||
|
||||
trigger_error( sprintf( 'Invalid Jetpack option name: %s', esc_html( $name ) ), E_USER_WARNING ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error -- Don't want to change legacy behavior.
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the multiple given options. Updates jetpack_options and/or jetpack_$name as appropriate.
|
||||
*
|
||||
* @param array $array array( option name => option value, ... ).
|
||||
*/
|
||||
public static function update_options( $array ) {
|
||||
$names = array_keys( $array );
|
||||
|
||||
foreach ( array_diff( $names, self::get_option_names(), self::get_option_names( 'non_compact' ), self::get_option_names( 'private' ) ) as $unknown_name ) {
|
||||
trigger_error( sprintf( 'Invalid Jetpack option name: %s', esc_html( $unknown_name ) ), E_USER_WARNING ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error -- Don't change legacy behavior.
|
||||
unset( $array[ $unknown_name ] );
|
||||
}
|
||||
|
||||
foreach ( $names as $name ) {
|
||||
self::update_option( $name, $array[ $name ] );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given option. May be passed multiple option names as an array.
|
||||
* Updates jetpack_options and/or deletes jetpack_$name as appropriate.
|
||||
*
|
||||
* @param string|array $names Option names. They must come _without_ `jetpack_%` prefix. The method will prefix the option names.
|
||||
*
|
||||
* @return bool Was the option successfully deleted?
|
||||
*/
|
||||
public static function delete_option( $names ) {
|
||||
$result = true;
|
||||
$names = (array) $names;
|
||||
|
||||
if ( ! self::is_valid( $names ) ) {
|
||||
// phpcs:disable -- This line triggers a handful of errors; ignoring to avoid changing legacy behavior.
|
||||
trigger_error( sprintf( 'Invalid Jetpack option names: %s', print_r( $names, 1 ) ), E_USER_WARNING );
|
||||
// phpcs:enable
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ( array_intersect( $names, self::get_option_names( 'non_compact' ) ) as $name ) {
|
||||
if ( self::is_network_option( $name ) ) {
|
||||
$result = delete_site_option( "jetpack_$name" );
|
||||
} else {
|
||||
$result = delete_option( "jetpack_$name" );
|
||||
}
|
||||
}
|
||||
|
||||
foreach ( array_keys( self::$grouped_options ) as $group ) {
|
||||
if ( ! self::delete_grouped_option( $group, $names ) ) {
|
||||
$result = false;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get group option.
|
||||
*
|
||||
* @param string $group Option group name.
|
||||
* @param string $name Option name.
|
||||
* @param mixed $default Default option value.
|
||||
*
|
||||
* @return mixed Option.
|
||||
*/
|
||||
private static function get_grouped_option( $group, $name, $default ) {
|
||||
$options = get_option( self::$grouped_options[ $group ] );
|
||||
if ( is_array( $options ) && isset( $options[ $name ] ) ) {
|
||||
return $options[ $name ];
|
||||
}
|
||||
|
||||
return $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete grouped option.
|
||||
*
|
||||
* @param string $group Option group name.
|
||||
* @param array $names Option names.
|
||||
*
|
||||
* @return bool Success or failure.
|
||||
*/
|
||||
private static function delete_grouped_option( $group, $names ) {
|
||||
$options = get_option( self::$grouped_options[ $group ], array() );
|
||||
|
||||
$to_delete = array_intersect( $names, self::get_option_names( $group ), array_keys( $options ) );
|
||||
if ( $to_delete ) {
|
||||
foreach ( $to_delete as $name ) {
|
||||
unset( $options[ $name ] );
|
||||
}
|
||||
|
||||
return update_option( self::$grouped_options[ $group ], $options );
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* Raw option methods allow Jetpack to get / update / delete options via direct DB queries, including options
|
||||
* that are not created by the Jetpack plugin. This is helpful only in rare cases when we need to bypass
|
||||
* cache and filters.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Deletes an option via $wpdb query.
|
||||
*
|
||||
* @param string $name Option name.
|
||||
*
|
||||
* @return bool Is the option deleted?
|
||||
*/
|
||||
public static function delete_raw_option( $name ) {
|
||||
if ( self::bypass_raw_option( $name ) ) {
|
||||
return delete_option( $name );
|
||||
}
|
||||
global $wpdb;
|
||||
$result = $wpdb->query( $wpdb->prepare( "DELETE FROM $wpdb->options WHERE option_name = %s", $name ) );
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an option via $wpdb query.
|
||||
*
|
||||
* @param string $name Option name.
|
||||
* @param mixed $value Option value.
|
||||
* @param bool $autoload Specifying whether to autoload or not.
|
||||
*
|
||||
* @return bool Is the option updated?
|
||||
*/
|
||||
public static function update_raw_option( $name, $value, $autoload = false ) {
|
||||
if ( self::bypass_raw_option( $name ) ) {
|
||||
return update_option( $name, $value, $autoload );
|
||||
}
|
||||
global $wpdb;
|
||||
$autoload_value = $autoload ? 'yes' : 'no';
|
||||
|
||||
$old_value = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT option_value FROM $wpdb->options WHERE option_name = %s LIMIT 1",
|
||||
$name
|
||||
)
|
||||
);
|
||||
if ( $old_value === $value ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$serialized_value = maybe_serialize( $value );
|
||||
// below we used "insert ignore" to at least suppress the resulting error.
|
||||
$updated_num = $wpdb->query(
|
||||
$wpdb->prepare(
|
||||
"UPDATE $wpdb->options SET option_value = %s WHERE option_name = %s",
|
||||
$serialized_value,
|
||||
$name
|
||||
)
|
||||
);
|
||||
|
||||
// Try inserting the option if the value doesn't exits.
|
||||
if ( ! $updated_num ) {
|
||||
$updated_num = $wpdb->query(
|
||||
$wpdb->prepare(
|
||||
"INSERT IGNORE INTO $wpdb->options ( option_name, option_value, autoload ) VALUES ( %s, %s, %s )",
|
||||
$name,
|
||||
$serialized_value,
|
||||
$autoload_value
|
||||
)
|
||||
);
|
||||
}
|
||||
return (bool) $updated_num;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an option via $wpdb query.
|
||||
*
|
||||
* @since 1.1.2
|
||||
* @since-jetpack 5.4.0
|
||||
*
|
||||
* @param string $name Option name.
|
||||
* @param mixed $default Default option value if option is not found.
|
||||
*
|
||||
* @return mixed Option value, or null if option is not found and default is not specified.
|
||||
*/
|
||||
public static function get_raw_option( $name, $default = null ) {
|
||||
if ( self::bypass_raw_option( $name ) ) {
|
||||
return get_option( $name, $default );
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
$value = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT option_value FROM $wpdb->options WHERE option_name = %s LIMIT 1",
|
||||
$name
|
||||
)
|
||||
);
|
||||
$value = maybe_unserialize( $value );
|
||||
|
||||
if ( null === $value && null !== $default ) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function checks for a constant that, if present, will disable direct DB queries Jetpack uses to manage certain options and force Jetpack to always use Options API instead.
|
||||
* Options can be selectively managed via a blocklist by filtering option names via the jetpack_disabled_raw_option filter.
|
||||
*
|
||||
* @param string $name Option name.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function bypass_raw_option( $name ) {
|
||||
|
||||
if ( Constants::get_constant( 'JETPACK_DISABLE_RAW_OPTIONS' ) ) {
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* Allows to disable particular raw options.
|
||||
*
|
||||
* @since 1.1.2
|
||||
* @since-jetpack 5.5.0
|
||||
*
|
||||
* @param array $disabled_raw_options An array of option names that you can selectively blocklist from being managed via direct database queries.
|
||||
*/
|
||||
$disabled_raw_options = apply_filters( 'jetpack_disabled_raw_options', array() );
|
||||
return isset( $disabled_raw_options[ $name ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all known options that are used by Jetpack and managed by Jetpack_Options.
|
||||
*
|
||||
* @since 1.1.2
|
||||
* @since-jetpack 5.4.0
|
||||
*
|
||||
* @param boolean $strip_unsafe_options If true, and by default, will strip out options necessary for the connection to WordPress.com.
|
||||
* @return array An array of all options managed via the Jetpack_Options class.
|
||||
*/
|
||||
public static function get_all_jetpack_options( $strip_unsafe_options = true ) {
|
||||
$jetpack_options = self::get_option_names();
|
||||
$jetpack_options_non_compat = self::get_option_names( 'non_compact' );
|
||||
$jetpack_options_private = self::get_option_names( 'private' );
|
||||
|
||||
$all_jp_options = array_merge( $jetpack_options, $jetpack_options_non_compat, $jetpack_options_private );
|
||||
|
||||
if ( $strip_unsafe_options ) {
|
||||
// Flag some Jetpack options as unsafe.
|
||||
$unsafe_options = array(
|
||||
'id', // (int) The Client ID/WP.com Blog ID of this site.
|
||||
'master_user', // (int) The local User ID of the user who connected this site to jetpack.wordpress.com.
|
||||
'version', // (string) Used during upgrade procedure to auto-activate new modules. version:time
|
||||
|
||||
// non_compact.
|
||||
'activated',
|
||||
|
||||
// private.
|
||||
'register',
|
||||
'blog_token', // (string) The Client Secret/Blog Token of this site.
|
||||
'user_token', // (string) The User Token of this site. (deprecated)
|
||||
'user_tokens',
|
||||
);
|
||||
|
||||
// Remove the unsafe Jetpack options.
|
||||
foreach ( $unsafe_options as $unsafe_option ) {
|
||||
$key = array_search( $unsafe_option, $all_jp_options, true );
|
||||
if ( false !== $key ) {
|
||||
unset( $all_jp_options[ $key ] );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $all_jp_options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all options that are not managed by the Jetpack_Options class that are used by Jetpack.
|
||||
*
|
||||
* @since 1.1.2
|
||||
* @since-jetpack 5.4.0
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_all_wp_options() {
|
||||
// A manual build of the wp options.
|
||||
return array(
|
||||
'sharing-options',
|
||||
'disabled_likes',
|
||||
'disabled_reblogs',
|
||||
'jetpack_comments_likes_enabled',
|
||||
'stats_options',
|
||||
'stats_dashboard_widget',
|
||||
'safecss_preview_rev',
|
||||
'safecss_rev',
|
||||
'safecss_revision_migrated',
|
||||
'nova_menu_order',
|
||||
'jetpack_portfolio',
|
||||
'jetpack_portfolio_posts_per_page',
|
||||
'jetpack_testimonial',
|
||||
'jetpack_testimonial_posts_per_page',
|
||||
'sharedaddy_disable_resources',
|
||||
'sharing-options',
|
||||
'sharing-services',
|
||||
'site_icon_temp_data',
|
||||
'featured-content',
|
||||
'site_logo',
|
||||
'jetpack_dismissed_notices',
|
||||
'jetpack-twitter-cards-site-tag',
|
||||
'jetpack-sitemap-state',
|
||||
'jetpack_sitemap_post_types',
|
||||
'jetpack_sitemap_location',
|
||||
'jetpack_protect_key',
|
||||
'jetpack_protect_blocked_attempts',
|
||||
'jetpack_protect_activating',
|
||||
'jetpack_active_plan',
|
||||
'jetpack_activation_source',
|
||||
'jetpack_site_products',
|
||||
'jetpack_sso_match_by_email',
|
||||
'jetpack_sso_require_two_step',
|
||||
'jetpack_sso_remove_login_form',
|
||||
'jetpack_last_connect_url_check',
|
||||
'jetpack_excluded_extensions',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all options that can be safely reset by CLI.
|
||||
*
|
||||
* @since 1.1.2
|
||||
* @since-jetpack 5.4.0
|
||||
*
|
||||
* @return array array Associative array containing jp_options which are managed by the Jetpack_Options class and wp_options which are not.
|
||||
*/
|
||||
public static function get_options_for_reset() {
|
||||
$all_jp_options = self::get_all_jetpack_options();
|
||||
|
||||
$wp_options = self::get_all_wp_options();
|
||||
|
||||
$options = array(
|
||||
'jp_options' => $all_jp_options,
|
||||
'wp_options' => $wp_options,
|
||||
);
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all known options
|
||||
*
|
||||
* @since 1.1.2
|
||||
* @since-jetpack 5.4.0
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function delete_all_known_options() {
|
||||
// Delete all compact options.
|
||||
foreach ( (array) self::$grouped_options as $option_name ) {
|
||||
delete_option( $option_name );
|
||||
}
|
||||
|
||||
// Delete all non-compact Jetpack options.
|
||||
foreach ( (array) self::get_option_names( 'non-compact' ) as $option_name ) {
|
||||
self::delete_option( $option_name );
|
||||
}
|
||||
|
||||
// Delete all options that can be reset via CLI, that aren't Jetpack options.
|
||||
foreach ( (array) self::get_all_wp_options() as $option_name ) {
|
||||
delete_option( $option_name );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,412 @@
|
||||
<?php
|
||||
/**
|
||||
* The Jetpack Connection signature class file.
|
||||
*
|
||||
* @package automattic/jetpack-connection
|
||||
*/
|
||||
|
||||
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
|
||||
|
||||
/**
|
||||
* The Jetpack Connection signature class that is used to sign requests.
|
||||
*/
|
||||
class Jetpack_Signature {
|
||||
/**
|
||||
* Token part of the access token.
|
||||
*
|
||||
* @access public
|
||||
* @var string
|
||||
*/
|
||||
public $token;
|
||||
|
||||
/**
|
||||
* Access token secret.
|
||||
*
|
||||
* @access public
|
||||
* @var string
|
||||
*/
|
||||
public $secret;
|
||||
|
||||
/**
|
||||
* Timezone difference (in seconds).
|
||||
*
|
||||
* @access public
|
||||
* @var int
|
||||
*/
|
||||
public $time_diff;
|
||||
|
||||
/**
|
||||
* The current request URL.
|
||||
*
|
||||
* @access public
|
||||
* @var string
|
||||
*/
|
||||
public $current_request_url;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param string $access_token Access token.
|
||||
* @param int $time_diff Timezone difference (in seconds).
|
||||
*/
|
||||
public function __construct( $access_token, $time_diff = 0 ) {
|
||||
$secret = explode( '.', $access_token );
|
||||
if ( 2 !== count( $secret ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->token = $secret[0];
|
||||
$this->secret = $secret[1];
|
||||
$this->time_diff = $time_diff;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign the current request.
|
||||
*
|
||||
* @todo Implement a proper nonce verification.
|
||||
*
|
||||
* @param array $override Optional arguments to override the ones from the current request.
|
||||
* @return string|WP_Error Request signature, or a WP_Error on failure.
|
||||
*/
|
||||
public function sign_current_request( $override = array() ) {
|
||||
if ( isset( $override['scheme'] ) ) {
|
||||
$scheme = $override['scheme'];
|
||||
if ( ! in_array( $scheme, array( 'http', 'https' ), true ) ) {
|
||||
return new WP_Error( 'invalid_scheme', 'Invalid URL scheme' );
|
||||
}
|
||||
} elseif ( is_ssl() ) {
|
||||
$scheme = 'https';
|
||||
} else {
|
||||
$scheme = 'http';
|
||||
}
|
||||
|
||||
$port = $this->get_current_request_port();
|
||||
|
||||
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidatedNotSanitized -- Sniff misses the esc_url_raw wrapper.
|
||||
$this->current_request_url = esc_url_raw( wp_unslash( "{$scheme}://{$_SERVER['HTTP_HOST']}:{$port}" . ( $_SERVER['REQUEST_URI'] ?? '' ) ) );
|
||||
|
||||
if ( array_key_exists( 'body', $override ) && ! empty( $override['body'] ) ) {
|
||||
$body = $override['body'];
|
||||
} elseif ( isset( $_SERVER['REQUEST_METHOD'] ) && 'POST' === strtoupper( $_SERVER['REQUEST_METHOD'] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- This is validating.
|
||||
$body = $GLOBALS['HTTP_RAW_POST_DATA'] ?? null;
|
||||
|
||||
// Convert the $_POST to the body, if the body was empty. This is how arrays are hashed
|
||||
// and encoded on the Jetpack side.
|
||||
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Missing -- Used to generate a cryptographic signature of the post data. Not actually using any of it here.
|
||||
if ( empty( $body ) && is_array( $_POST ) && $_POST !== array() ) {
|
||||
$body = $_POST; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- We need all of $_POST in order to generate a cryptographic signature of the post data.
|
||||
}
|
||||
}
|
||||
} elseif ( isset( $_SERVER['REQUEST_METHOD'] ) && 'PUT' === strtoupper( $_SERVER['REQUEST_METHOD'] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- This is validating.
|
||||
// This is a little strange-looking, but there doesn't seem to be another way to get the PUT body.
|
||||
$raw_put_data = file_get_contents( 'php://input' );
|
||||
parse_str( $raw_put_data, $body );
|
||||
|
||||
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
|
||||
$put_data = json_decode( $raw_put_data, true );
|
||||
if ( is_array( $put_data ) && $put_data !== array() ) {
|
||||
$body = $put_data;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$body = null;
|
||||
}
|
||||
|
||||
if ( empty( $body ) ) {
|
||||
$body = null;
|
||||
}
|
||||
|
||||
$a = array();
|
||||
foreach ( array( 'token', 'timestamp', 'nonce', 'body-hash' ) as $parameter ) {
|
||||
if ( isset( $override[ $parameter ] ) ) {
|
||||
$a[ $parameter ] = $override[ $parameter ];
|
||||
} else {
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
$a[ $parameter ] = isset( $_GET[ $parameter ] ) ? filter_var( wp_unslash( $_GET[ $parameter ] ) ) : '';
|
||||
}
|
||||
}
|
||||
|
||||
$method = $override['method'] ?? ( isset( $_SERVER['REQUEST_METHOD'] ) ? filter_var( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) : null );
|
||||
return $this->sign_request( $a['token'], $a['timestamp'], $a['nonce'], $a['body-hash'], $method, $this->current_request_url, $body, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign a specified request.
|
||||
*
|
||||
* @todo Having body_hash v. body-hash is annoying. Refactor to accept an array?
|
||||
* @todo Use wp_json_encode() instead of json_encode()?
|
||||
*
|
||||
* @param string $token Request token.
|
||||
* @param int $timestamp Timestamp of the request.
|
||||
* @param string $nonce Request nonce.
|
||||
* @param string $body_hash Request body hash.
|
||||
* @param string $method Request method.
|
||||
* @param string $url Request URL.
|
||||
* @param mixed $body Request body.
|
||||
* @param bool $verify_body_hash Whether to verify the body hash against the body.
|
||||
* @return string|WP_Error Request signature, or a WP_Error on failure.
|
||||
*/
|
||||
public function sign_request( $token = '', $timestamp = 0, $nonce = '', $body_hash = '', $method = '', $url = '', $body = null, $verify_body_hash = true ) {
|
||||
if ( ! $this->secret ) {
|
||||
return new WP_Error( 'invalid_secret', 'Invalid secret' );
|
||||
}
|
||||
|
||||
if ( ! $this->token ) {
|
||||
return new WP_Error( 'invalid_token', 'Invalid token' );
|
||||
}
|
||||
|
||||
list( $token ) = explode( '.', $token );
|
||||
|
||||
$signature_details = compact( 'token', 'timestamp', 'nonce', 'body_hash', 'method', 'url' );
|
||||
|
||||
if ( ! str_starts_with( $token, "$this->token:" ) ) {
|
||||
return new WP_Error( 'token_mismatch', 'Incorrect token', compact( 'signature_details' ) );
|
||||
}
|
||||
|
||||
// If we got an array at this point, let's encode it, so we can see what it looks like as a string.
|
||||
if ( is_array( $body ) ) {
|
||||
if ( $body !== array() ) {
|
||||
// phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode
|
||||
$body = json_encode(
|
||||
$body,
|
||||
0 // phpcs:ignore Jetpack.Functions.JsonEncodeFlags.ZeroFound -- No `json_encode()` flags because this needs to match whatever is calculating the hash on the other end.
|
||||
);
|
||||
|
||||
} else {
|
||||
$body = '';
|
||||
}
|
||||
}
|
||||
|
||||
$required_parameters = array( 'token', 'timestamp', 'nonce', 'method', 'url' );
|
||||
if ( $body !== null ) {
|
||||
$required_parameters[] = 'body_hash';
|
||||
if ( ! is_string( $body ) ) {
|
||||
return new WP_Error( 'invalid_body', 'Body is malformed.', compact( 'signature_details' ) );
|
||||
}
|
||||
}
|
||||
|
||||
foreach ( $required_parameters as $required ) {
|
||||
if ( ! is_scalar( $$required ) ) {
|
||||
return new WP_Error( 'invalid_signature', sprintf( 'The required "%s" parameter is malformed.', str_replace( '_', '-', $required ) ), compact( 'signature_details' ) );
|
||||
}
|
||||
|
||||
if ( ! strlen( $$required ) ) {
|
||||
return new WP_Error( 'invalid_signature', sprintf( 'The required "%s" parameter is missing.', str_replace( '_', '-', $required ) ), compact( 'signature_details' ) );
|
||||
}
|
||||
}
|
||||
|
||||
if ( empty( $body ) ) {
|
||||
if ( $body_hash ) {
|
||||
return new WP_Error( 'invalid_body_hash', 'Invalid body hash for empty body.', compact( 'signature_details' ) );
|
||||
}
|
||||
} else {
|
||||
$connection = new Connection_Manager();
|
||||
if ( $verify_body_hash && $connection->sha1_base64( $body ) !== $body_hash ) {
|
||||
return new WP_Error( 'invalid_body_hash', 'The body hash does not match.', compact( 'signature_details' ) );
|
||||
}
|
||||
}
|
||||
|
||||
$parsed = wp_parse_url( $url );
|
||||
if ( ! isset( $parsed['host'] ) ) {
|
||||
return new WP_Error( 'invalid_signature', sprintf( 'The required "%s" parameter is malformed.', 'url' ), compact( 'signature_details' ) );
|
||||
}
|
||||
|
||||
if ( ! empty( $parsed['port'] ) ) {
|
||||
$port = $parsed['port'];
|
||||
} elseif ( 'http' === $parsed['scheme'] ) {
|
||||
$port = 80;
|
||||
} elseif ( 'https' === $parsed['scheme'] ) {
|
||||
$port = 443;
|
||||
} else {
|
||||
return new WP_Error( 'unknown_scheme_port', "The scheme's port is unknown", compact( 'signature_details' ) );
|
||||
}
|
||||
|
||||
if ( ! ctype_digit( "$timestamp" ) || 10 < strlen( (string) $timestamp ) ) { // If Jetpack is around in 275 years, you can blame mdawaffe for the bug.
|
||||
return new WP_Error( 'invalid_signature', sprintf( 'The required "%s" parameter is malformed.', 'timestamp' ), compact( 'signature_details' ) );
|
||||
}
|
||||
|
||||
$local_time = $timestamp - $this->time_diff;
|
||||
if ( $local_time < time() - 600 || $local_time > time() + 300 ) {
|
||||
return new WP_Error( 'invalid_signature', 'The timestamp is too old.', compact( 'signature_details' ) );
|
||||
}
|
||||
|
||||
if ( 12 < strlen( $nonce ) || preg_match( '/[^a-zA-Z0-9]/', $nonce ) ) {
|
||||
return new WP_Error( 'invalid_signature', sprintf( 'The required "%s" parameter is malformed.', 'nonce' ), compact( 'signature_details' ) );
|
||||
}
|
||||
|
||||
$normalized_request_pieces = array(
|
||||
$token,
|
||||
$timestamp,
|
||||
$nonce,
|
||||
$body_hash,
|
||||
strtoupper( $method ),
|
||||
strtolower( $parsed['host'] ),
|
||||
$port,
|
||||
empty( $parsed['path'] ) ? '' : $parsed['path'],
|
||||
// Normalized Query String.
|
||||
);
|
||||
|
||||
$normalized_request_pieces = array_merge( $normalized_request_pieces, $this->normalized_query_parameters( $parsed['query'] ?? '' ) );
|
||||
$flat_normalized_request_pieces = array();
|
||||
foreach ( $normalized_request_pieces as $piece ) {
|
||||
if ( is_array( $piece ) ) {
|
||||
foreach ( $piece as $subpiece ) {
|
||||
$flat_normalized_request_pieces[] = $subpiece;
|
||||
}
|
||||
} else {
|
||||
$flat_normalized_request_pieces[] = $piece;
|
||||
}
|
||||
}
|
||||
$normalized_request_pieces = $flat_normalized_request_pieces;
|
||||
|
||||
$normalized_request_string = implode( "\n", $normalized_request_pieces ) . "\n";
|
||||
|
||||
// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
|
||||
return base64_encode( hash_hmac( 'sha1', $normalized_request_string, $this->secret, true ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve and normalize the parameters from a query string.
|
||||
*
|
||||
* @param string $query_string Query string.
|
||||
* @return array Normalized query string parameters.
|
||||
*/
|
||||
public function normalized_query_parameters( $query_string ) {
|
||||
parse_str( $query_string, $array );
|
||||
|
||||
unset( $array['signature'] );
|
||||
|
||||
$names = array_keys( $array );
|
||||
$values = array_values( $array );
|
||||
|
||||
$names = array_map( array( $this, 'encode_3986' ), $names );
|
||||
$values = array_map( array( $this, 'encode_3986' ), $values );
|
||||
|
||||
$pairs = array_map( array( $this, 'join_with_equal_sign' ), $names, $values );
|
||||
|
||||
sort( $pairs );
|
||||
|
||||
return $pairs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes a string or array of strings according to RFC 3986.
|
||||
*
|
||||
* @param string|array $string_or_array String or array to encode.
|
||||
* @return string|array URL-encoded string or array.
|
||||
*/
|
||||
public function encode_3986( $string_or_array ) {
|
||||
if ( is_array( $string_or_array ) ) {
|
||||
return array_map( array( $this, 'encode_3986' ), $string_or_array );
|
||||
}
|
||||
|
||||
return rawurlencode( $string_or_array );
|
||||
}
|
||||
|
||||
/**
|
||||
* Concatenates a parameter name and a parameter value with an equals sign between them.
|
||||
*
|
||||
* @param string $name Parameter name.
|
||||
* @param string|array $value Parameter value.
|
||||
* @return string|array A string pair (e.g. `name=value`) or an array of string pairs.
|
||||
*/
|
||||
public function join_with_equal_sign( $name, $value ) {
|
||||
if ( is_array( $value ) ) {
|
||||
return $this->join_array_with_equal_sign( $name, $value );
|
||||
}
|
||||
return "{$name}={$value}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function for join_with_equal_sign for handling arrayed values.
|
||||
* Explicitly supports nested arrays.
|
||||
*
|
||||
* @param string $name Parameter name.
|
||||
* @param array $value Parameter value.
|
||||
* @return array An array of string pairs (e.g. `[ name[example]=value ]`).
|
||||
*/
|
||||
private function join_array_with_equal_sign( $name, $value ) {
|
||||
$result = array();
|
||||
foreach ( $value as $value_key => $value_value ) {
|
||||
$joined_value = $this->join_with_equal_sign( $name . '[' . $value_key . ']', $value_value );
|
||||
if ( is_array( $joined_value ) ) {
|
||||
foreach ( array_values( $joined_value ) as $individual_joined_value ) {
|
||||
$result[] = $individual_joined_value;
|
||||
}
|
||||
} elseif ( is_string( $joined_value ) ) {
|
||||
$result[] = $joined_value;
|
||||
}
|
||||
}
|
||||
|
||||
sort( $result );
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the port that should be considered to sign the current request.
|
||||
*
|
||||
* It will analyze the current request, as well as some Jetpack constants, to return the string
|
||||
* to be concatenated in the URL representing the port of the current request.
|
||||
*
|
||||
* @since 1.8.4
|
||||
*
|
||||
* @return string The port to be used in the signature
|
||||
*/
|
||||
public function get_current_request_port() {
|
||||
$host_port = isset( $_SERVER['HTTP_X_FORWARDED_PORT'] ) ? $this->sanitize_host_post( $_SERVER['HTTP_X_FORWARDED_PORT'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- This is validating.
|
||||
if ( '' === $host_port && isset( $_SERVER['SERVER_PORT'] ) ) {
|
||||
$host_port = $this->sanitize_host_post( $_SERVER['SERVER_PORT'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- This is validating.
|
||||
}
|
||||
|
||||
/**
|
||||
* Note: This port logic is tested in the Jetpack_Cxn_Tests->test__server_port_value() test.
|
||||
* Please update the test if any changes are made in this logic.
|
||||
*/
|
||||
if ( is_ssl() ) {
|
||||
// 443: Standard Port
|
||||
// 80: Assume we're behind a proxy without X-Forwarded-Port. Hardcoding "80" here means most sites
|
||||
// with SSL termination proxies (self-served, Cloudflare, etc.) don't need to fiddle with
|
||||
// the JETPACK_SIGNATURE__HTTPS_PORT constant. The code also implies we can't talk to a
|
||||
// site at https://example.com:80/ (which would be a strange configuration).
|
||||
// JETPACK_SIGNATURE__HTTPS_PORT: Set this constant in wp-config.php to the back end webserver's port
|
||||
// if the site is behind a proxy running on port 443 without
|
||||
// X-Forwarded-Port and the back end's port is *not* 80. It's better,
|
||||
// though, to configure the proxy to send X-Forwarded-Port.
|
||||
$https_port = defined( 'JETPACK_SIGNATURE__HTTPS_PORT' ) ? $this->sanitize_host_post( JETPACK_SIGNATURE__HTTPS_PORT ) : '443';
|
||||
$port = in_array( $host_port, array( '443', '80', $https_port ), true ) ? '' : $host_port;
|
||||
} else {
|
||||
// 80: Standard Port
|
||||
// JETPACK_SIGNATURE__HTTPS_PORT: Set this constant in wp-config.php to the back end webserver's port
|
||||
// if the site is behind a proxy running on port 80 without
|
||||
// X-Forwarded-Port. It's better, though, to configure the proxy to
|
||||
// send X-Forwarded-Port.
|
||||
$http_port = defined( 'JETPACK_SIGNATURE__HTTP_PORT' ) ? $this->sanitize_host_post( JETPACK_SIGNATURE__HTTP_PORT ) : '80';
|
||||
$port = in_array( $host_port, array( '80', $http_port ), true ) ? '' : $host_port;
|
||||
}
|
||||
return (string) $port;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes a variable checking if it's a valid port number, which can be an integer or a numeric string
|
||||
*
|
||||
* @since 1.8.4
|
||||
*
|
||||
* @param mixed $port_number Variable representing a port number.
|
||||
* @return string Always a string with a valid port number, or an empty string if input is invalid
|
||||
*/
|
||||
public function sanitize_host_post( $port_number ) {
|
||||
|
||||
if ( ! is_int( $port_number ) && ! is_string( $port_number ) ) {
|
||||
return '';
|
||||
}
|
||||
if ( is_string( $port_number ) && ! ctype_digit( $port_number ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if ( 0 >= (int) $port_number || 65535 < $port_number ) {
|
||||
return '';
|
||||
}
|
||||
return (string) $port_number;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
<?php
|
||||
/**
|
||||
* Legacy Jetpack Tracks Client
|
||||
*
|
||||
* @package automattic/jetpack-tracking
|
||||
*/
|
||||
|
||||
use Automattic\Jetpack\Connection\Manager;
|
||||
|
||||
/**
|
||||
* Jetpack_Tracks_Client
|
||||
*
|
||||
* Send Tracks events on behalf of a user
|
||||
*
|
||||
* Example Usage:
|
||||
```php
|
||||
require( dirname(__FILE__).'path/to/tracks/class-jetpack-tracks-client.php' );
|
||||
|
||||
$result = Jetpack_Tracks_Client::record_event( array(
|
||||
'_en' => $event_name, // required
|
||||
'_ui' => $user_id, // required unless _ul is provided
|
||||
'_ul' => $user_login, // required unless _ui is provided
|
||||
|
||||
// Optional, but recommended
|
||||
'_ts' => $ts_in_ms, // Default: now
|
||||
'_via_ip' => $client_ip, // we use it for geo, etc.
|
||||
|
||||
// Possibly useful to set some context for the event
|
||||
'_via_ua' => $client_user_agent,
|
||||
'_via_url' => $client_url,
|
||||
'_via_ref' => $client_referrer,
|
||||
|
||||
// For user-targeted tests
|
||||
'abtest_name' => $abtest_name,
|
||||
'abtest_variation' => $abtest_variation,
|
||||
|
||||
// Your application-specific properties
|
||||
'custom_property' => $some_value,
|
||||
) );
|
||||
|
||||
if ( is_wp_error( $result ) ) {
|
||||
// Handle the error in your app
|
||||
}
|
||||
```
|
||||
*/
|
||||
class Jetpack_Tracks_Client {
|
||||
const PIXEL = 'https://pixel.wp.com/t.gif';
|
||||
const BROWSER_TYPE = 'php-agent';
|
||||
const USER_AGENT_SLUG = 'tracks-client';
|
||||
const VERSION = '0.3';
|
||||
|
||||
/**
|
||||
* Stores the Terms of Service Object Reference.
|
||||
*
|
||||
* @var null
|
||||
*/
|
||||
private static $terms_of_service = null;
|
||||
|
||||
/**
|
||||
* Record an event.
|
||||
*
|
||||
* @param mixed $event Event object to send to Tracks. An array will be cast to object. Required.
|
||||
* Properties are included directly in the pixel query string after light validation.
|
||||
* @return mixed True on success, WP_Error on failure
|
||||
*/
|
||||
public static function record_event( $event ) {
|
||||
if ( ! self::$terms_of_service ) {
|
||||
self::$terms_of_service = new \Automattic\Jetpack\Terms_Of_Service();
|
||||
}
|
||||
|
||||
// Don't track users who have opted out or not agreed to our TOS, or are not running an active Jetpack.
|
||||
if ( ! self::$terms_of_service->has_agreed() || ! empty( $_COOKIE['tk_opt-out'] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ! $event instanceof Jetpack_Tracks_Event ) {
|
||||
$event = new Jetpack_Tracks_Event( $event );
|
||||
}
|
||||
if ( is_wp_error( $event ) ) {
|
||||
return $event;
|
||||
}
|
||||
|
||||
$pixel = $event->build_pixel_url();
|
||||
|
||||
if ( ! $pixel ) {
|
||||
return new WP_Error( 'invalid_pixel', 'cannot generate tracks pixel for given input', 400 );
|
||||
}
|
||||
|
||||
return self::record_pixel( $pixel );
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronously request the pixel.
|
||||
*
|
||||
* @param string $pixel The wp.com tracking pixel.
|
||||
* @return array|bool|WP_Error True if successful. wp_remote_get response or WP_Error if not.
|
||||
*/
|
||||
public static function record_pixel( $pixel ) {
|
||||
// Add the Request Timestamp and URL terminator just before the HTTP request.
|
||||
$pixel .= '&_rt=' . self::build_timestamp() . '&_=_';
|
||||
|
||||
$response = wp_remote_get(
|
||||
$pixel,
|
||||
array(
|
||||
'blocking' => true, // The default, but being explicit here :).
|
||||
'timeout' => 1,
|
||||
'redirection' => 2,
|
||||
'httpversion' => '1.1',
|
||||
'user-agent' => self::get_user_agent(),
|
||||
)
|
||||
);
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$code = $response['response']['code'] ?? 0;
|
||||
|
||||
if ( 200 !== $code ) {
|
||||
return new WP_Error( 'request_failed', 'Tracks pixel request failed', $code );
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user agent.
|
||||
*
|
||||
* @return string The user agent.
|
||||
*/
|
||||
public static function get_user_agent() {
|
||||
return self::USER_AGENT_SLUG . '-v' . self::VERSION;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an event and return its tracking URL
|
||||
*
|
||||
* @deprecated Call the `build_pixel_url` method on a Jetpack_Tracks_Event object instead.
|
||||
* @param array $event Event keys and values.
|
||||
* @return string URL of a tracking pixel.
|
||||
*/
|
||||
public static function build_pixel_url( $event ) {
|
||||
$_event = new Jetpack_Tracks_Event( $event );
|
||||
return $_event->build_pixel_url();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate input for a tracks event.
|
||||
*
|
||||
* @deprecated Instantiate a Jetpack_Tracks_Event object instead
|
||||
* @param array $event Event keys and values.
|
||||
* @return mixed Validated keys and values or WP_Error on failure
|
||||
*/
|
||||
private static function validate_and_sanitize( $event ) {
|
||||
$_event = new Jetpack_Tracks_Event( $event );
|
||||
if ( is_wp_error( $_event ) ) {
|
||||
return $_event;
|
||||
}
|
||||
return get_object_vars( $_event );
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a timestamp.
|
||||
*
|
||||
* Milliseconds since 1970-01-01.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function build_timestamp() {
|
||||
$ts = round( microtime( true ) * 1000 );
|
||||
return number_format( $ts, 0, '', '' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Grabs the user's anon id from cookies, or generates and sets a new one
|
||||
*
|
||||
* @return string An anon id for the user
|
||||
*/
|
||||
public static function get_anon_id() {
|
||||
static $anon_id = null;
|
||||
|
||||
if ( ! isset( $anon_id ) ) {
|
||||
|
||||
// Did the browser send us a cookie?
|
||||
if ( isset( $_COOKIE['tk_ai'] ) && preg_match( '#^[a-z]+:[A-Za-z0-9+/=]{24}$#', $_COOKIE['tk_ai'] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- This is validating.
|
||||
$anon_id = $_COOKIE['tk_ai']; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- This is validating.
|
||||
} else {
|
||||
|
||||
$binary = '';
|
||||
|
||||
// Generate a new anonId and try to save it in the browser's cookies.
|
||||
// Note that base64-encoding an 18 character string generates a 24-character anon id.
|
||||
for ( $i = 0; $i < 18; ++$i ) {
|
||||
$binary .= chr( wp_rand( 0, 255 ) );
|
||||
}
|
||||
|
||||
$anon_id = 'jetpack:' . base64_encode( $binary ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
|
||||
|
||||
if ( ! headers_sent()
|
||||
&& ! ( defined( 'REST_REQUEST' ) && REST_REQUEST )
|
||||
&& ! ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST )
|
||||
) {
|
||||
setcookie( 'tk_ai', $anon_id, 0, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), false ); // phpcs:ignore Jetpack.Functions.SetCookie -- This is a random value and should be fine.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $anon_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the WordPress.com user's Tracks identity, if connected.
|
||||
*
|
||||
* @return array|bool
|
||||
*/
|
||||
public static function get_connected_user_tracks_identity() {
|
||||
$user_data = ( new Manager() )->get_connected_user_data();
|
||||
if ( ! $user_data ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return array(
|
||||
'blogid' => Jetpack_Options::get_option( 'id', 0 ),
|
||||
'email' => $user_data['email'],
|
||||
'userid' => $user_data['ID'],
|
||||
'username' => $user_data['login'],
|
||||
'user_locale' => $user_data['user_locale'],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
<?php
|
||||
/**
|
||||
* Class Jetpack_Tracks_Event. Legacy.
|
||||
*
|
||||
* @package automattic/jetpack-sync
|
||||
*/
|
||||
|
||||
/*
|
||||
* Example Usage:
|
||||
```php
|
||||
require_once( dirname(__FILE__) . 'path/to/tracks/class-jetpack-tracks-event.php' );
|
||||
|
||||
$event = new Jetpack_Tracks_Event( array(
|
||||
'_en' => $event_name, // required
|
||||
'_ui' => $user_id, // required unless _ul is provided
|
||||
'_ul' => $user_login, // required unless _ui is provided
|
||||
|
||||
// Optional, but recommended
|
||||
'_via_ip' => $client_ip, // for geo, etc.
|
||||
|
||||
// Possibly useful to set some context for the event
|
||||
'_via_ua' => $client_user_agent,
|
||||
'_via_url' => $client_url,
|
||||
'_via_ref' => $client_referrer,
|
||||
|
||||
// For user-targeted tests
|
||||
'abtest_name' => $abtest_name,
|
||||
'abtest_variation' => $abtest_variation,
|
||||
|
||||
// Your application-specific properties
|
||||
'custom_property' => $some_value,
|
||||
) );
|
||||
|
||||
if ( is_wp_error( $event->error ) ) {
|
||||
// Handle the error in your app
|
||||
}
|
||||
|
||||
$bump_and_redirect_pixel = $event->build_signed_pixel_url();
|
||||
```
|
||||
*/
|
||||
|
||||
/**
|
||||
* Class Jetpack_Tracks_Event
|
||||
*/
|
||||
#[AllowDynamicProperties]
|
||||
class Jetpack_Tracks_Event {
|
||||
const EVENT_NAME_REGEX = '/^(([a-z0-9]+)_){2}([a-z0-9_]+)$/';
|
||||
const PROP_NAME_REGEX = '/^[a-z_][a-z0-9_]*$/';
|
||||
|
||||
/**
|
||||
* Tracks Event Error.
|
||||
*
|
||||
* @var mixed Error.
|
||||
*/
|
||||
public $error;
|
||||
|
||||
/**
|
||||
* Jetpack_Tracks_Event constructor.
|
||||
*
|
||||
* @param object $event Tracks event.
|
||||
*/
|
||||
public function __construct( $event ) {
|
||||
$_event = self::validate_and_sanitize( $event );
|
||||
if ( is_wp_error( $_event ) ) {
|
||||
$this->error = $_event;
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ( $_event as $key => $value ) {
|
||||
$this->{$key} = $value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a track event.
|
||||
*/
|
||||
public function record() {
|
||||
return Jetpack_Tracks_Client::record_event( $this );
|
||||
}
|
||||
|
||||
/**
|
||||
* Annotate the event with all relevant info.
|
||||
*
|
||||
* @param mixed $event Object or (flat) array.
|
||||
* @return mixed The transformed event array or WP_Error on failure.
|
||||
*/
|
||||
public static function validate_and_sanitize( $event ) {
|
||||
$event = (object) $event;
|
||||
|
||||
// Required.
|
||||
if ( ! $event->_en ) {
|
||||
return new WP_Error( 'invalid_event', 'A valid event must be specified via `_en`', 400 );
|
||||
}
|
||||
|
||||
// delete non-routable addresses otherwise geoip will discard the record entirely.
|
||||
if ( property_exists( $event, '_via_ip' ) && preg_match( '/^192\.168|^10\./', $event->_via_ip ) ) {
|
||||
unset( $event->_via_ip );
|
||||
}
|
||||
|
||||
$validated = array(
|
||||
'browser_type' => Jetpack_Tracks_Client::BROWSER_TYPE,
|
||||
'_aua' => Jetpack_Tracks_Client::get_user_agent(),
|
||||
);
|
||||
|
||||
$_event = (object) array_merge( (array) $event, $validated );
|
||||
|
||||
// If you want to block property names, do it here.
|
||||
|
||||
// Make sure we have an event timestamp.
|
||||
if ( ! isset( $_event->_ts ) ) {
|
||||
$_event->_ts = Jetpack_Tracks_Client::build_timestamp();
|
||||
}
|
||||
|
||||
return $_event;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a pixel URL that will send a Tracks event when fired.
|
||||
* On error, returns an empty string ('').
|
||||
*
|
||||
* @return string A pixel URL or empty string ('') if there were invalid args.
|
||||
*/
|
||||
public function build_pixel_url() {
|
||||
if ( $this->error ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$args = get_object_vars( $this );
|
||||
|
||||
// Request Timestamp and URL Terminator must be added just before the HTTP request or not at all.
|
||||
unset( $args['_rt'] );
|
||||
unset( $args['_'] );
|
||||
|
||||
$validated = self::validate_and_sanitize( $args );
|
||||
|
||||
if ( is_wp_error( $validated ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return Jetpack_Tracks_Client::PIXEL . '?' . http_build_query( $validated );
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the event name.
|
||||
*
|
||||
* @param string $name Event name.
|
||||
* @return false|int
|
||||
*/
|
||||
public static function event_name_is_valid( $name ) {
|
||||
return preg_match( self::EVENT_NAME_REGEX, $name );
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates prop name
|
||||
*
|
||||
* @param string $name Property name.
|
||||
*
|
||||
* @return false|int Truthy value.
|
||||
*/
|
||||
public static function prop_name_is_valid( $name ) {
|
||||
return preg_match( self::PROP_NAME_REGEX, $name );
|
||||
}
|
||||
|
||||
/**
|
||||
* Scrutinize event name.
|
||||
*
|
||||
* @param object $event Tracks event.
|
||||
*/
|
||||
public static function scrutinize_event_names( $event ) {
|
||||
if ( ! self::event_name_is_valid( $event->_en ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$whitelisted_key_names = array(
|
||||
'anonId',
|
||||
'Browser_Type',
|
||||
);
|
||||
|
||||
foreach ( array_keys( (array) $event ) as $key ) {
|
||||
if ( in_array( $key, $whitelisted_key_names, true ) ) {
|
||||
continue;
|
||||
}
|
||||
if ( ! self::prop_name_is_valid( $key ) ) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,845 @@
|
||||
<?php
|
||||
/**
|
||||
* Jetpack XMLRPC Server.
|
||||
*
|
||||
* @package automattic/jetpack-connection
|
||||
*/
|
||||
|
||||
use Automattic\Jetpack\Connection\Client;
|
||||
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
|
||||
use Automattic\Jetpack\Connection\Secrets;
|
||||
use Automattic\Jetpack\Connection\Tokens;
|
||||
use Automattic\Jetpack\Connection\Urls;
|
||||
use Automattic\Jetpack\Constants;
|
||||
use Automattic\Jetpack\Roles;
|
||||
|
||||
/**
|
||||
* Just a sack of functions. Not actually an IXR_Server
|
||||
*/
|
||||
class Jetpack_XMLRPC_Server {
|
||||
/**
|
||||
* The current error object
|
||||
*
|
||||
* @var \WP_Error
|
||||
*/
|
||||
public $error = null;
|
||||
|
||||
/**
|
||||
* The current user
|
||||
*
|
||||
* @var \WP_User
|
||||
*/
|
||||
public $user = null;
|
||||
|
||||
/**
|
||||
* The connection manager object.
|
||||
*
|
||||
* @var Automattic\Jetpack\Connection\Manager
|
||||
*/
|
||||
private $connection;
|
||||
|
||||
/**
|
||||
* Creates a new XMLRPC server object.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->connection = new Connection_Manager();
|
||||
}
|
||||
|
||||
/**
|
||||
* Whitelist of the XML-RPC methods available to the Jetpack Server. If the
|
||||
* user is not authenticated (->login()) then the methods are never added,
|
||||
* so they will get a "does not exist" error.
|
||||
*
|
||||
* @param array $core_methods Core XMLRPC methods.
|
||||
*/
|
||||
public function xmlrpc_methods( $core_methods ) {
|
||||
$jetpack_methods = array(
|
||||
'jetpack.verifyAction' => array( $this, 'verify_action' ),
|
||||
'jetpack.idcUrlValidation' => array( $this, 'validate_urls_for_idc_mitigation' ),
|
||||
'jetpack.unlinkUser' => array( $this, 'unlink_user' ),
|
||||
'jetpack.testConnection' => array( $this, 'test_connection' ),
|
||||
);
|
||||
|
||||
$jetpack_methods = array_merge( $jetpack_methods, $this->provision_xmlrpc_methods() );
|
||||
|
||||
$this->user = $this->login();
|
||||
|
||||
if ( $this->user ) {
|
||||
$jetpack_methods = array_merge(
|
||||
$jetpack_methods,
|
||||
array(
|
||||
'jetpack.testAPIUserCode' => array( $this, 'test_api_user_code' ),
|
||||
)
|
||||
);
|
||||
|
||||
if ( isset( $core_methods['metaWeblog.editPost'] ) ) {
|
||||
$jetpack_methods['metaWeblog.newMediaObject'] = $core_methods['metaWeblog.newMediaObject'];
|
||||
$jetpack_methods['jetpack.updateAttachmentParent'] = array( $this, 'update_attachment_parent' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the XML-RPC methods available to Jetpack for authenticated users.
|
||||
*
|
||||
* @since 1.7.0
|
||||
* @since-jetpack 1.1.0
|
||||
*
|
||||
* @param array $jetpack_methods XML-RPC methods available to the Jetpack Server.
|
||||
* @param array $core_methods Available core XML-RPC methods.
|
||||
* @param \WP_User $user Information about the user authenticated in the request.
|
||||
*/
|
||||
$jetpack_methods = apply_filters( 'jetpack_xmlrpc_methods', $jetpack_methods, $core_methods, $this->user );
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the XML-RPC methods available to Jetpack for requests signed both with a blog token or a user token.
|
||||
*
|
||||
* @since 1.7.0
|
||||
* @since 1.9.5 Introduced the $user parameter.
|
||||
* @since-jetpack 3.0.0
|
||||
*
|
||||
* @param array $jetpack_methods XML-RPC methods available to the Jetpack Server.
|
||||
* @param array $core_methods Available core XML-RPC methods.
|
||||
* @param \WP_User|bool $user Information about the user authenticated in the request. False if authenticated with blog token.
|
||||
*/
|
||||
return apply_filters( 'jetpack_xmlrpc_unauthenticated_methods', $jetpack_methods, $core_methods, $this->user );
|
||||
}
|
||||
|
||||
/**
|
||||
* Whitelist of the bootstrap XML-RPC methods
|
||||
*/
|
||||
public function bootstrap_xmlrpc_methods() {
|
||||
return array(
|
||||
'jetpack.remoteAuthorize' => array( $this, 'remote_authorize' ),
|
||||
'jetpack.remoteRegister' => array( $this, 'remote_register' ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Additional method needed for authorization calls.
|
||||
*/
|
||||
public function authorize_xmlrpc_methods() {
|
||||
return array(
|
||||
'jetpack.remoteAuthorize' => array( $this, 'remote_authorize' ),
|
||||
'jetpack.remoteRegister' => array( $this, 'remote_already_registered' ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remote provisioning methods.
|
||||
*/
|
||||
public function provision_xmlrpc_methods() {
|
||||
return array(
|
||||
'jetpack.remoteRegister' => array( $this, 'remote_register' ),
|
||||
'jetpack.remoteProvision' => array( $this, 'remote_provision' ),
|
||||
'jetpack.remoteConnect' => array( $this, 'remote_connect' ),
|
||||
'jetpack.getUser' => array( $this, 'get_user' ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to verify whether a local user exists and what role they have.
|
||||
*
|
||||
* @param int|string|array $request One of:
|
||||
* int|string The local User's ID, username, or email address.
|
||||
* array A request array containing:
|
||||
* 0: int|string The local User's ID, username, or email address.
|
||||
*
|
||||
* @return array|\IXR_Error Information about the user, or error if no such user found:
|
||||
* roles: string[] The user's rols.
|
||||
* login: string The user's username.
|
||||
* email_hash string[] The MD5 hash of the user's normalized email address.
|
||||
* caps string[] The user's capabilities.
|
||||
* allcaps string[] The user's granular capabilities, merged from role capabilities.
|
||||
* token_key string The Token Key of the user's Jetpack token. Empty string if none.
|
||||
*/
|
||||
public function get_user( $request ) {
|
||||
$user_id = is_array( $request ) ? $request[0] : $request;
|
||||
|
||||
if ( ! $user_id ) {
|
||||
return $this->error(
|
||||
new \WP_Error(
|
||||
'invalid_user',
|
||||
__( 'Invalid user identifier.', 'jetpack-connection' ),
|
||||
400
|
||||
),
|
||||
'get_user'
|
||||
);
|
||||
}
|
||||
|
||||
$user = $this->get_user_by_anything( $user_id );
|
||||
|
||||
if ( ! $user ) {
|
||||
return $this->error(
|
||||
new \WP_Error(
|
||||
'user_unknown',
|
||||
__( 'User not found.', 'jetpack-connection' ),
|
||||
404
|
||||
),
|
||||
'get_user'
|
||||
);
|
||||
}
|
||||
|
||||
$user_token = ( new Tokens() )->get_access_token( $user->ID );
|
||||
|
||||
if ( $user_token ) {
|
||||
list( $user_token_key ) = explode( '.', $user_token->secret );
|
||||
if ( $user_token_key === $user_token->secret ) {
|
||||
$user_token_key = '';
|
||||
}
|
||||
} else {
|
||||
$user_token_key = '';
|
||||
}
|
||||
|
||||
return array(
|
||||
'id' => $user->ID,
|
||||
'login' => $user->user_login,
|
||||
'email_hash' => md5( strtolower( trim( $user->user_email ) ) ),
|
||||
'roles' => $user->roles,
|
||||
'caps' => $user->caps,
|
||||
'allcaps' => $user->allcaps,
|
||||
'token_key' => $user_token_key,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remote authorization XMLRPC method handler.
|
||||
*
|
||||
* @param array $request the request.
|
||||
*/
|
||||
public function remote_authorize( $request ) {
|
||||
$user = get_user_by( 'id', $request['state'] );
|
||||
|
||||
/**
|
||||
* Happens on various request handling events in the Jetpack XMLRPC server.
|
||||
* The action combines several types of events:
|
||||
* - remote_authorize
|
||||
* - remote_provision
|
||||
* - get_user.
|
||||
*
|
||||
* @since 1.7.0
|
||||
* @since-jetpack 8.0.0
|
||||
*
|
||||
* @param String $action the action name, i.e., 'remote_authorize'.
|
||||
* @param String $stage the execution stage, can be 'begin', 'success', 'error', etc.
|
||||
* @param array $parameters extra parameters from the event.
|
||||
* @param WP_User $user the acting user.
|
||||
*/
|
||||
do_action( 'jetpack_xmlrpc_server_event', 'remote_authorize', 'begin', array(), $user );
|
||||
|
||||
foreach ( array( 'secret', 'state', 'redirect_uri', 'code' ) as $required ) {
|
||||
if ( ! isset( $request[ $required ] ) || empty( $request[ $required ] ) ) {
|
||||
return $this->error(
|
||||
new \WP_Error( 'missing_parameter', 'One or more parameters is missing from the request.', 400 ),
|
||||
'remote_authorize'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! $user ) {
|
||||
return $this->error( new \WP_Error( 'user_unknown', 'User not found.', 404 ), 'remote_authorize' );
|
||||
}
|
||||
|
||||
if ( $this->connection->has_connected_owner() && $this->connection->is_user_connected( $request['state'] ) ) {
|
||||
return $this->error( new \WP_Error( 'already_connected', 'User already connected.', 400 ), 'remote_authorize' );
|
||||
}
|
||||
|
||||
$verified = $this->verify_action( array( 'authorize', $request['secret'], $request['state'] ) );
|
||||
|
||||
if ( is_a( $verified, 'IXR_Error' ) ) {
|
||||
return $this->error( $verified, 'remote_authorize' );
|
||||
}
|
||||
|
||||
wp_set_current_user( $request['state'] );
|
||||
|
||||
$result = $this->connection->authorize( $request );
|
||||
|
||||
if ( is_wp_error( $result ) ) {
|
||||
return $this->error( $result, 'remote_authorize' );
|
||||
}
|
||||
|
||||
// This action is documented in class.jetpack-xmlrpc-server.php.
|
||||
do_action( 'jetpack_xmlrpc_server_event', 'remote_authorize', 'success' );
|
||||
|
||||
return array(
|
||||
'result' => $result,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* This XML-RPC method is called from the /jpphp/provision endpoint on WPCOM in order to
|
||||
* register this site so that a plan can be provisioned.
|
||||
*
|
||||
* @param array|ArrayAccess $request An array containing at minimum nonce and local_user keys.
|
||||
*
|
||||
* @return \WP_Error|array
|
||||
*/
|
||||
public function remote_register( $request ) {
|
||||
// This action is documented in class.jetpack-xmlrpc-server.php.
|
||||
do_action( 'jetpack_xmlrpc_server_event', 'remote_register', 'begin', array() );
|
||||
|
||||
$user = $this->fetch_and_verify_local_user( $request );
|
||||
|
||||
if ( ! $user ) {
|
||||
return $this->error(
|
||||
new WP_Error( 'input_error', __( 'Valid user is required', 'jetpack-connection' ), 400 ),
|
||||
'remote_register'
|
||||
);
|
||||
}
|
||||
|
||||
if ( is_wp_error( $user ) || is_a( $user, 'IXR_Error' ) ) {
|
||||
return $this->error( $user, 'remote_register' );
|
||||
}
|
||||
|
||||
if ( empty( $request['nonce'] ) ) {
|
||||
return $this->error(
|
||||
new \WP_Error(
|
||||
'nonce_missing',
|
||||
__( 'The required "nonce" parameter is missing.', 'jetpack-connection' ),
|
||||
400
|
||||
),
|
||||
'remote_register'
|
||||
);
|
||||
}
|
||||
|
||||
$nonce = sanitize_text_field( $request['nonce'] );
|
||||
unset( $request['nonce'] );
|
||||
|
||||
$api_url = $this->connection->api_url( 'partner_provision_nonce_check' );
|
||||
// @phan-suppress-next-line PhanAccessMethodInternal -- Phan is correct, but the usage is intentional.
|
||||
$response = Client::_wp_remote_request(
|
||||
esc_url_raw( add_query_arg( 'nonce', $nonce, $api_url ) ),
|
||||
array( 'method' => 'GET' ),
|
||||
true
|
||||
);
|
||||
|
||||
if (
|
||||
200 !== wp_remote_retrieve_response_code( $response ) ||
|
||||
'OK' !== trim( wp_remote_retrieve_body( $response ) )
|
||||
) {
|
||||
return $this->error(
|
||||
new \WP_Error(
|
||||
'invalid_nonce',
|
||||
__( 'There was an issue validating this request.', 'jetpack-connection' ),
|
||||
400
|
||||
),
|
||||
'remote_register'
|
||||
);
|
||||
}
|
||||
|
||||
if ( ! Jetpack_Options::get_option( 'id' ) || ! ( new Tokens() )->get_access_token() || ! empty( $request['force'] ) ) {
|
||||
wp_set_current_user( $user->ID );
|
||||
|
||||
// This code mostly copied from Jetpack::admin_page_load.
|
||||
if ( isset( $request['from'] ) ) {
|
||||
$this->connection->add_register_request_param( 'from', (string) $request['from'] );
|
||||
}
|
||||
$registered = $this->connection->try_registration();
|
||||
if ( is_wp_error( $registered ) ) {
|
||||
return $this->error( $registered, 'remote_register' );
|
||||
} elseif ( ! $registered ) {
|
||||
return $this->error(
|
||||
new \WP_Error(
|
||||
'registration_error',
|
||||
__( 'There was an unspecified error registering the site', 'jetpack-connection' ),
|
||||
400
|
||||
),
|
||||
'remote_register'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// This action is documented in class.jetpack-xmlrpc-server.php.
|
||||
do_action( 'jetpack_xmlrpc_server_event', 'remote_register', 'success' );
|
||||
|
||||
return array(
|
||||
'client_id' => Jetpack_Options::get_option( 'id' ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a substitute for remote_register() when the blog is already registered which returns an error code
|
||||
* signifying that state.
|
||||
* This is an unauthorized call and we should not be responding with any data other than the error code.
|
||||
*
|
||||
* @return \IXR_Error
|
||||
*/
|
||||
public function remote_already_registered() {
|
||||
return $this->error(
|
||||
new \WP_Error( 'already_registered', __( 'Blog is already registered', 'jetpack-connection' ), 400 ),
|
||||
'remote_register'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* This XML-RPC method is called from the /jpphp/provision endpoint on WPCOM in order to
|
||||
* register this site so that a plan can be provisioned.
|
||||
*
|
||||
* @param array|ArrayAccess $request An array containing at minimum a nonce key and a local_username key.
|
||||
*
|
||||
* @return \WP_Error|array
|
||||
*/
|
||||
public function remote_provision( $request ) {
|
||||
$user = $this->fetch_and_verify_local_user( $request );
|
||||
|
||||
if ( ! $user ) {
|
||||
return $this->error(
|
||||
new WP_Error( 'input_error', __( 'Valid user is required', 'jetpack-connection' ), 400 ),
|
||||
'remote_provision'
|
||||
);
|
||||
}
|
||||
|
||||
if ( is_wp_error( $user ) || is_a( $user, 'IXR_Error' ) ) {
|
||||
return $this->error( $user, 'remote_provision' );
|
||||
}
|
||||
|
||||
$site_icon = get_site_icon_url();
|
||||
|
||||
/**
|
||||
* Filters the Redirect URI returned by the remote_register XMLRPC method
|
||||
*
|
||||
* @param string $redirect_uri The Redirect URI
|
||||
*
|
||||
* @since 1.9.7
|
||||
*/
|
||||
$redirect_uri = apply_filters( 'jetpack_xmlrpc_remote_register_redirect_uri', admin_url() );
|
||||
|
||||
// Generate secrets.
|
||||
$roles = new Roles();
|
||||
$role = $roles->translate_user_to_role( $user );
|
||||
$secrets = ( new Secrets() )->generate( 'authorize', $user->ID );
|
||||
|
||||
$response = array(
|
||||
'jp_version' => Constants::get_constant( 'JETPACK__VERSION' ),
|
||||
'redirect_uri' => $redirect_uri,
|
||||
'user_id' => $user->ID,
|
||||
'user_email' => $user->user_email,
|
||||
'user_login' => $user->user_login,
|
||||
'scope' => $this->connection->sign_role( $role, $user->ID ),
|
||||
'secret' => $secrets['secret_1'],
|
||||
'is_active' => $this->connection->has_connected_owner(),
|
||||
);
|
||||
|
||||
if ( $site_icon ) {
|
||||
$response['site_icon'] = $site_icon;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the response of the remote_provision XMLRPC method
|
||||
*
|
||||
* @param array $response The response.
|
||||
* @param array $request An array containing at minimum a nonce key and a local_username key.
|
||||
* @param \WP_User $user The local authenticated user.
|
||||
*
|
||||
* @since 1.9.7
|
||||
*/
|
||||
$response = apply_filters( 'jetpack_remote_xmlrpc_provision_response', $response, $request, $user );
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an array containing a local user identifier and a nonce, will attempt to fetch and set
|
||||
* an access token for the given user.
|
||||
*
|
||||
* @param array|ArrayAccess $request An array containing local_user and nonce keys at minimum.
|
||||
* @param \IXR_Client $ixr_client The client object, optional.
|
||||
* @return mixed
|
||||
*/
|
||||
public function remote_connect( $request, $ixr_client = false ) {
|
||||
if ( $this->connection->has_connected_owner() ) {
|
||||
return $this->error(
|
||||
new WP_Error(
|
||||
'already_connected',
|
||||
__( 'Jetpack is already connected.', 'jetpack-connection' ),
|
||||
400
|
||||
),
|
||||
'remote_connect'
|
||||
);
|
||||
}
|
||||
|
||||
$user = $this->fetch_and_verify_local_user( $request );
|
||||
|
||||
if ( ! $user || is_wp_error( $user ) || is_a( $user, 'IXR_Error' ) ) {
|
||||
return $this->error(
|
||||
new WP_Error(
|
||||
'input_error',
|
||||
__( 'Valid user is required.', 'jetpack-connection' ),
|
||||
400
|
||||
),
|
||||
'remote_connect'
|
||||
);
|
||||
}
|
||||
|
||||
if ( empty( $request['nonce'] ) ) {
|
||||
return $this->error(
|
||||
new WP_Error(
|
||||
'input_error',
|
||||
__( 'A non-empty nonce must be supplied.', 'jetpack-connection' ),
|
||||
400
|
||||
),
|
||||
'remote_connect'
|
||||
);
|
||||
}
|
||||
|
||||
if ( ! $ixr_client ) {
|
||||
$ixr_client = new Jetpack_IXR_Client();
|
||||
}
|
||||
// TODO: move this query into the Tokens class?
|
||||
$ixr_client->query(
|
||||
'jetpack.getUserAccessToken',
|
||||
array(
|
||||
'nonce' => sanitize_text_field( $request['nonce'] ),
|
||||
'external_user_id' => $user->ID,
|
||||
)
|
||||
);
|
||||
|
||||
$token = $ixr_client->isError() ? false : $ixr_client->getResponse();
|
||||
if ( empty( $token ) ) {
|
||||
return $this->error(
|
||||
new WP_Error(
|
||||
'token_fetch_failed',
|
||||
__( 'Failed to fetch user token from WordPress.com.', 'jetpack-connection' ),
|
||||
400
|
||||
),
|
||||
'remote_connect'
|
||||
);
|
||||
}
|
||||
$token = sanitize_text_field( $token );
|
||||
|
||||
( new Tokens() )->update_user_token( $user->ID, sprintf( '%s.%d', $token, $user->ID ), true );
|
||||
|
||||
/**
|
||||
* Hook fired at the end of the jetpack.remoteConnect XML-RPC callback
|
||||
*
|
||||
* @since 1.9.7
|
||||
*/
|
||||
do_action( 'jetpack_remote_connect_end' );
|
||||
|
||||
return $this->connection->has_connected_owner();
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter for the local user to act as.
|
||||
*
|
||||
* @param array $request the current request data.
|
||||
* @return WP_User|IXR_Error|false IXR_Error if the request is missing a local_user field, WP_User object on success, or false on failure to find a user.
|
||||
*/
|
||||
private function fetch_and_verify_local_user( $request ) {
|
||||
if ( empty( $request['local_user'] ) ) {
|
||||
return $this->error(
|
||||
new \WP_Error(
|
||||
'local_user_missing',
|
||||
__( 'The required "local_user" parameter is missing.', 'jetpack-connection' ),
|
||||
400
|
||||
),
|
||||
'remote_provision'
|
||||
);
|
||||
}
|
||||
|
||||
// Local user is used to look up by login, email or ID.
|
||||
$local_user_info = $request['local_user'];
|
||||
|
||||
return $this->get_user_by_anything( $local_user_info );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the user object by its data.
|
||||
*
|
||||
* @param string $user_id can be any identifying user data.
|
||||
* @return WP_User|false WP_User object on success, false on failure.
|
||||
*/
|
||||
private function get_user_by_anything( $user_id ) {
|
||||
$user = get_user_by( 'login', $user_id );
|
||||
|
||||
if ( ! $user ) {
|
||||
$user = get_user_by( 'email', $user_id );
|
||||
}
|
||||
|
||||
if ( ! $user ) {
|
||||
$user = get_user_by( 'ID', $user_id );
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Possible error_codes:
|
||||
*
|
||||
* - verify_secret_1_missing
|
||||
* - verify_secret_1_malformed
|
||||
* - verify_secrets_missing: verification secrets are not found in database
|
||||
* - verify_secrets_incomplete: verification secrets are only partially found in database
|
||||
* - verify_secrets_expired: verification secrets have expired
|
||||
* - verify_secrets_mismatch: stored secret_1 does not match secret_1 sent by Jetpack.WordPress.com
|
||||
* - state_missing: required parameter of state not found
|
||||
* - state_malformed: state is not a digit
|
||||
* - invalid_state: state in request does not match the stored state
|
||||
*
|
||||
* The 'authorize' and 'register' actions have additional error codes
|
||||
*
|
||||
* state_missing: a state ( user id ) was not supplied
|
||||
* state_malformed: state is not the correct data type
|
||||
* invalid_state: supplied state does not match the stored state
|
||||
*
|
||||
* @param array $params action An array of 3 parameters:
|
||||
* [0]: string action. Possible values are `authorize`, `publicize` and `register`.
|
||||
* [1]: string secret_1.
|
||||
* [2]: int state.
|
||||
* @return \IXR_Error|string IXR_Error on failure, secret_2 on success.
|
||||
*/
|
||||
public function verify_action( $params ) {
|
||||
$action = $params[0] ?? '';
|
||||
$verify_secret = $params[1] ?? '';
|
||||
$state = $params[2] ?? '';
|
||||
|
||||
$result = ( new Secrets() )->verify( $action, $verify_secret, $state );
|
||||
|
||||
if ( is_wp_error( $result ) ) {
|
||||
return $this->error( $result );
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for wp_authenticate( $username, $password );
|
||||
*
|
||||
* @return \WP_User|bool
|
||||
*/
|
||||
public function login() {
|
||||
$this->connection->require_jetpack_authentication();
|
||||
$user = wp_authenticate( 'username', 'password' );
|
||||
if ( is_wp_error( $user ) ) {
|
||||
if ( 'authentication_failed' === $user->get_error_code() ) { // Generic error could mean most anything.
|
||||
$this->error = new \WP_Error( 'invalid_request', 'Invalid Request', 403 );
|
||||
} else {
|
||||
$this->error = $user;
|
||||
}
|
||||
return false;
|
||||
} elseif ( ! $user ) { // Shouldn't happen.
|
||||
$this->error = new \WP_Error( 'invalid_request', 'Invalid Request', 403 );
|
||||
return false;
|
||||
}
|
||||
|
||||
wp_set_current_user( $user->ID );
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current error as an \IXR_Error
|
||||
*
|
||||
* @param \WP_Error|\IXR_Error $error The error object, optional.
|
||||
* @param string $event_name The event name.
|
||||
* @param \WP_User $user The user object.
|
||||
* @return bool|\IXR_Error
|
||||
*/
|
||||
public function error( $error = null, $event_name = null, $user = null ) {
|
||||
if ( null !== $event_name ) {
|
||||
// This action is documented in class.jetpack-xmlrpc-server.php.
|
||||
do_action( 'jetpack_xmlrpc_server_event', $event_name, 'fail', $error, $user );
|
||||
}
|
||||
|
||||
if ( $error !== null ) {
|
||||
$this->error = $error;
|
||||
}
|
||||
|
||||
if ( is_wp_error( $this->error ) ) {
|
||||
$code = $this->error->get_error_data();
|
||||
if ( ! $code ) {
|
||||
$code = -10520;
|
||||
}
|
||||
$message = sprintf( 'Jetpack: [%s] %s', $this->error->get_error_code(), $this->error->get_error_message() );
|
||||
if ( ! class_exists( \IXR_Error::class ) ) {
|
||||
require_once ABSPATH . WPINC . '/class-IXR.php';
|
||||
}
|
||||
return new \IXR_Error( $code, $message );
|
||||
} elseif ( is_a( $this->error, 'IXR_Error' ) ) {
|
||||
return $this->error;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/* API Methods */
|
||||
|
||||
/**
|
||||
* Just authenticates with the given Jetpack credentials.
|
||||
*
|
||||
* @return string A success string. The Jetpack plugin filters it and make it return the Jetpack plugin version.
|
||||
*/
|
||||
public function test_connection() {
|
||||
/**
|
||||
* Filters the successful response of the XMLRPC test_connection method
|
||||
*
|
||||
* @param string $response The response string.
|
||||
*/
|
||||
return apply_filters( 'jetpack_xmlrpc_test_connection_response', 'success' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the API user code.
|
||||
*
|
||||
* @param array $args arguments identifying the test site.
|
||||
*/
|
||||
public function test_api_user_code( $args ) {
|
||||
$client_id = (int) $args[0];
|
||||
$user_id = (int) $args[1];
|
||||
$nonce = (string) $args[2];
|
||||
$verify = (string) $args[3];
|
||||
|
||||
if ( ! $client_id || ! $user_id || ! strlen( $nonce ) || 32 !== strlen( $verify ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$user = get_user_by( 'id', $user_id );
|
||||
if ( ! $user || is_wp_error( $user ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* phpcs:ignore
|
||||
debugging
|
||||
error_log( "CLIENT: $client_id" );
|
||||
error_log( "USER: $user_id" );
|
||||
error_log( "NONCE: $nonce" );
|
||||
error_log( "VERIFY: $verify" );
|
||||
*/
|
||||
|
||||
$jetpack_token = ( new Tokens() )->get_access_token( $user_id );
|
||||
|
||||
$api_user_code = get_user_meta( $user_id, "jetpack_json_api_$client_id", true );
|
||||
if ( ! $api_user_code ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$hmac = hash_hmac(
|
||||
'md5',
|
||||
json_encode( // phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode
|
||||
(object) array(
|
||||
'client_id' => $client_id,
|
||||
'user_id' => $user_id,
|
||||
'nonce' => $nonce,
|
||||
'code' => (string) $api_user_code,
|
||||
),
|
||||
0 // phpcs:ignore Jetpack.Functions.JsonEncodeFlags.ZeroFound -- No `json_encode()` flags because this needs to match whatever is calculating the hash on the other end.
|
||||
),
|
||||
$jetpack_token->secret
|
||||
);
|
||||
|
||||
if ( ! hash_equals( $hmac, $verify ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $user_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlink a user from WordPress.com
|
||||
*
|
||||
* When the request is done without any parameter, this XMLRPC callback gets an empty array as input.
|
||||
*
|
||||
* If $user_id is not provided, it will try to disconnect the current logged in user. This will fail if called by the Master User.
|
||||
*
|
||||
* If $user_id is is provided, it will try to disconnect the informed user, even if it's the Master User.
|
||||
*
|
||||
* @param mixed $user_id The user ID to disconnect from this site.
|
||||
*/
|
||||
public function unlink_user( $user_id = array() ) {
|
||||
$user_id = (int) $user_id;
|
||||
if ( $user_id < 1 ) {
|
||||
$user_id = null;
|
||||
}
|
||||
/**
|
||||
* Fired when we want to log an event to the Jetpack event log.
|
||||
*
|
||||
* @since 1.7.0
|
||||
* @since-jetpack 7.7.0
|
||||
*
|
||||
* @param string $code Unique name for the event.
|
||||
* @param string $data Optional data about the event.
|
||||
*/
|
||||
do_action( 'jetpack_event_log', 'unlink' );
|
||||
return $this->connection->disconnect_user(
|
||||
$user_id,
|
||||
(bool) $user_id
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the home URL, site URL, and URL secret for the current site which can be used on the WPCOM side for
|
||||
* IDC mitigation to decide whether sync should be allowed if the home and siteurl values differ between WPCOM
|
||||
* and the remote Jetpack site.
|
||||
*
|
||||
* @since 1.56.0 Additional data may be added via filter `jetpack_connection_validate_urls_for_idc_mitigation_response`.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function validate_urls_for_idc_mitigation() {
|
||||
$response = array(
|
||||
'home' => Urls::home_url(),
|
||||
'siteurl' => Urls::site_url(),
|
||||
);
|
||||
|
||||
/**
|
||||
* Allows modifying the response.
|
||||
*
|
||||
* @since 1.56.0
|
||||
*
|
||||
* @param array $response
|
||||
*/
|
||||
return apply_filters( 'jetpack_connection_validate_urls_for_idc_mitigation_response', $response );
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the attachment parent object.
|
||||
*
|
||||
* @param array $args attachment and parent identifiers.
|
||||
*/
|
||||
public function update_attachment_parent( $args ) {
|
||||
$attachment_id = (int) $args[0];
|
||||
$parent_id = (int) $args[1];
|
||||
|
||||
return wp_update_post(
|
||||
array(
|
||||
'ID' => $attachment_id,
|
||||
'post_parent' => $parent_id,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deprecated: This method is no longer part of the Connection package and now lives on the Jetpack plugin.
|
||||
*
|
||||
* Disconnect this blog from the connected wordpress.com account
|
||||
*
|
||||
* @deprecated since 1.25.0
|
||||
* @see Jetpack_XMLRPC_Methods::disconnect_blog() in the Jetpack plugin
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function disconnect_blog() {
|
||||
_deprecated_function( __METHOD__, '1.25.0', 'Jetpack_XMLRPC_Methods::disconnect_blog()' );
|
||||
if ( class_exists( 'Jetpack_XMLRPC_Methods' ) ) {
|
||||
return Jetpack_XMLRPC_Methods::disconnect_blog();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deprecated: This method is no longer part of the Connection package and now lives on the Jetpack plugin.
|
||||
*
|
||||
* Serve a JSON API request.
|
||||
*
|
||||
* @deprecated since 1.25.0
|
||||
* @see Jetpack_XMLRPC_Methods::json_api() in the Jetpack plugin
|
||||
*
|
||||
* @param array $args request arguments.
|
||||
*/
|
||||
public function json_api( $args = array() ) {
|
||||
_deprecated_function( __METHOD__, '1.25.0', 'Jetpack_XMLRPC_Methods::json_api()' );
|
||||
if ( class_exists( 'Jetpack_XMLRPC_Methods' ) ) {
|
||||
return Jetpack_XMLRPC_Methods::json_api( $args );
|
||||
}
|
||||
return array();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
<?php
|
||||
/**
|
||||
* Jetpack Connection Abilities Registration.
|
||||
*
|
||||
* Registers Jetpack Connection abilities with the WordPress Abilities API so
|
||||
* AI agents can inspect the site's connection state through the standard
|
||||
* `wp-abilities/v1` REST surface.
|
||||
*
|
||||
* @package automattic/jetpack-connection
|
||||
*/
|
||||
|
||||
// @phan-file-suppress PhanUndeclaredFunction, PhanUndeclaredClassMethod @phan-suppress-current-line UnusedSuppression -- Abilities API added in WP 6.9; suppressions needed for older-WP compatibility runs.
|
||||
|
||||
namespace Automattic\Jetpack\Connection\Abilities;
|
||||
|
||||
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
|
||||
use Automattic\Jetpack\Connection\Package_Version;
|
||||
use Automattic\Jetpack\WP_Abilities\Registrar;
|
||||
use Jetpack_Options;
|
||||
|
||||
/**
|
||||
* Registers Jetpack Connection abilities with the WordPress Abilities API.
|
||||
*
|
||||
* Exposes a single read-only ability for site-level connection state so AI
|
||||
* agents can answer "is this site registered?" without having to
|
||||
* reverse-engineer Jetpack_Options keys.
|
||||
*
|
||||
* Writes (registering a new site, disconnecting a user, transferring
|
||||
* ownership) are deliberately deferred to a follow-up PR.
|
||||
*/
|
||||
class Connection_Abilities extends Registrar {
|
||||
|
||||
const CATEGORY_SLUG = 'jetpack';
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public static function get_category_slug(): string {
|
||||
return self::CATEGORY_SLUG;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* The `jetpack` ability-category is shared with other Jetpack registrars
|
||||
* (e.g. the Modules_Abilities class in the Jetpack plugin). Only the first
|
||||
* registration wins, so the English source string is kept byte-identical
|
||||
* across registrars to keep the visible category text consistent
|
||||
* regardless of load order.
|
||||
*/
|
||||
public static function get_category_definition(): array {
|
||||
return array(
|
||||
// "Jetpack" is a product name and should not be translated.
|
||||
'label' => 'Jetpack',
|
||||
'description' => __( 'Abilities provided by Jetpack.', 'jetpack-connection' ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public static function get_abilities(): array {
|
||||
return array(
|
||||
'jetpack/get-connection-status' => self::spec_get_connection_status(),
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
---------------------------------------------------------------------
|
||||
* Ability specs
|
||||
* ---------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* Spec: jetpack/get-connection-status.
|
||||
*/
|
||||
private static function spec_get_connection_status(): array {
|
||||
return array(
|
||||
'label' => __( 'Get Jetpack connection status', 'jetpack-connection' ),
|
||||
'description' => __(
|
||||
'Return the site-level Jetpack connection state in one zero-argument call. Shape: { site_registered, user_connected, master_user, blog_id, registration_url, connection_version }. `site_registered` is true when the site has a blog id and a blog token. `user_connected` is true when at least one user has linked their WordPress.com account. `master_user` is the local user id of the connection owner (the user who registered the site), or null if there is no owner. `blog_id` is the WordPress.com site id, or null when the site has not been registered. `registration_url` is the wp-admin URL the site owner should visit to register the site when `site_registered` is false; null once the site is registered. `connection_version` is the running Jetpack Connection package version. Read-only and idempotent — safe to poll.',
|
||||
'jetpack-connection'
|
||||
),
|
||||
'input_schema' => array(
|
||||
'type' => 'object',
|
||||
'properties' => new \stdClass(),
|
||||
'additionalProperties' => false,
|
||||
),
|
||||
'output_schema' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'site_registered' => array( 'type' => 'boolean' ),
|
||||
'user_connected' => array( 'type' => 'boolean' ),
|
||||
'master_user' => array( 'type' => array( 'integer', 'null' ) ),
|
||||
'blog_id' => array( 'type' => array( 'integer', 'null' ) ),
|
||||
'registration_url' => array( 'type' => array( 'string', 'null' ) ),
|
||||
'connection_version' => array( 'type' => 'string' ),
|
||||
),
|
||||
),
|
||||
'execute_callback' => array( __CLASS__, 'get_connection_status' ),
|
||||
'permission_callback' => array( __CLASS__, 'can_view_connection' ),
|
||||
'meta' => array(
|
||||
'annotations' => array(
|
||||
'readonly' => true,
|
||||
'destructive' => false,
|
||||
'idempotent' => true,
|
||||
),
|
||||
'show_in_rest' => true,
|
||||
'mcp' => array(
|
||||
'public' => true,
|
||||
'type' => 'tool', // default is already "tool", but can be explicit.
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
---------------------------------------------------------------------
|
||||
* Permission callbacks
|
||||
* ---------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* Permission check: mirrors the capability used by the Jetpack admin page.
|
||||
*
|
||||
* Connection state is not sensitive in itself (the same data is exposed
|
||||
* on the Jetpack admin page and through several existing REST endpoints),
|
||||
* but subscribers and contributors have no legitimate need to inspect it.
|
||||
* Gating on `jetpack_admin_page` aligns with how {@see Modules_Abilities}
|
||||
* scopes its read.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function can_view_connection(): bool {
|
||||
return current_user_can( 'jetpack_admin_page' );
|
||||
}
|
||||
|
||||
/*
|
||||
---------------------------------------------------------------------
|
||||
* Execute callbacks
|
||||
* ---------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* Execute: get-connection-status.
|
||||
*
|
||||
* @param array|null $input Ignored — zero-arg ability.
|
||||
* @return array
|
||||
*/
|
||||
public static function get_connection_status( $input = null ) {
|
||||
unset( $input );
|
||||
|
||||
$manager = self::get_manager();
|
||||
$site_registered = (bool) $manager->is_connected();
|
||||
$user_connected = (bool) $manager->has_connected_user();
|
||||
|
||||
$master_user_raw = Jetpack_Options::get_option( 'master_user' );
|
||||
$master_user = is_numeric( $master_user_raw ) && (int) $master_user_raw > 0 ? (int) $master_user_raw : null;
|
||||
|
||||
$blog_id_raw = Jetpack_Options::get_option( 'id' );
|
||||
$blog_id = is_numeric( $blog_id_raw ) && (int) $blog_id_raw > 0 ? (int) $blog_id_raw : null;
|
||||
|
||||
return array(
|
||||
'site_registered' => $site_registered,
|
||||
'user_connected' => $user_connected,
|
||||
'master_user' => $master_user,
|
||||
'blog_id' => $blog_id,
|
||||
'registration_url' => $site_registered ? null : self::registration_url(),
|
||||
'connection_version' => Package_Version::PACKAGE_VERSION,
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
---------------------------------------------------------------------
|
||||
* Helpers
|
||||
* ---------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* Return a Connection_Manager instance. Filterable for tests so they can
|
||||
* inject a partial mock without having to seed Jetpack_Options + tokens.
|
||||
*
|
||||
* @return Connection_Manager
|
||||
*/
|
||||
protected static function get_manager(): Connection_Manager {
|
||||
/**
|
||||
* Filters the Connection_Manager instance used by the Connection abilities.
|
||||
*
|
||||
* Tests inject a partial mock here; production callers should leave
|
||||
* the default. The filter callback receives the package-default
|
||||
* instance and must return a Connection_Manager — non-Manager
|
||||
* returns are discarded.
|
||||
*
|
||||
* @since 8.4.0
|
||||
*
|
||||
* @param Connection_Manager $manager The default instance.
|
||||
*/
|
||||
$instance = apply_filters( 'jetpack_connection_abilities_manager', new Connection_Manager() );
|
||||
return $instance instanceof Connection_Manager ? $instance : new Connection_Manager();
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the wp-admin URL the site owner should visit to register the
|
||||
* site to WordPress.com. We deliberately return a stable admin URL (no
|
||||
* secret generation, no XML-RPC roundtrip) so this read stays side-effect
|
||||
* free and cheap to poll. The destination page handles the actual
|
||||
* registration handshake from there.
|
||||
*
|
||||
* WP 7.0+ ships a core "Connectors" screen at `wp-admin/options-connectors.php`
|
||||
* with a Jetpack card registered by {@see Jetpack_Connector}. We probe
|
||||
* for the file directly (rather than a `class_exists()` on the registry)
|
||||
* because the file is what actually serves the URL — if it isn't on
|
||||
* disk, the redirect 404s regardless of which classes have loaded.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private static function registration_url(): string {
|
||||
if ( defined( 'ABSPATH' ) && file_exists( ABSPATH . 'wp-admin/options-connectors.php' ) ) {
|
||||
return admin_url( 'options-connectors.php' );
|
||||
}
|
||||
return admin_url( 'admin.php?page=jetpack' );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
<?php
|
||||
/**
|
||||
* Authorize_Json_Api handler class.
|
||||
* Used to handle connections via JSON API.
|
||||
* Ported from the Jetpack class.
|
||||
*
|
||||
* @since 2.7.6 Ported from the Jetpack class.
|
||||
*
|
||||
* @package automattic/jetpack-connection
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Connection;
|
||||
|
||||
use Automattic\Jetpack\Redirect;
|
||||
use Automattic\Jetpack\Status\Host;
|
||||
use Jetpack_Options;
|
||||
|
||||
/**
|
||||
* Authorize_Json_Api handler class.
|
||||
*/
|
||||
class Authorize_Json_Api {
|
||||
/**
|
||||
* Verified data for JSON authorization request
|
||||
*
|
||||
* @since 2.7.6
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $json_api_authorization_request = array();
|
||||
|
||||
/**
|
||||
* Verifies the request by checking the signature
|
||||
*
|
||||
* @since jetpack-4.6.0 Method was updated to use `$_REQUEST` instead of `$_GET` and `$_POST`. Method also updated to allow
|
||||
* passing in an `$environment` argument that overrides `$_REQUEST`. This was useful for integrating with SSO.
|
||||
* @since 2.7.6 Ported from Jetpack to the Connection package.
|
||||
*
|
||||
* @param null|array $environment Value to override $_REQUEST.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function verify_json_api_authorization_request( $environment = null ) {
|
||||
$environment = $environment === null
|
||||
? $_REQUEST // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- nonce verification handled later in function and request data are 1) used to verify a cryptographic signature of the request data and 2) sanitized later in function.
|
||||
: $environment;
|
||||
|
||||
if ( ! isset( $environment['token'] ) ) {
|
||||
wp_die( esc_html__( 'You must connect your Jetpack plugin to WordPress.com to use this feature.', 'jetpack-connection' ) );
|
||||
}
|
||||
|
||||
list( $env_token,, $env_user_id ) = explode( ':', $environment['token'] );
|
||||
$token = ( new Tokens() )->get_access_token( (int) $env_user_id, $env_token );
|
||||
if ( ! $token || empty( $token->secret ) ) {
|
||||
wp_die( esc_html__( 'You must connect your Jetpack plugin to WordPress.com to use this feature.', 'jetpack-connection' ) );
|
||||
}
|
||||
|
||||
$die_error = __( 'Someone may be trying to trick you into giving them access to your site. Or it could be you just encountered a bug :). Either way, please close this window.', 'jetpack-connection' );
|
||||
|
||||
// Host has encoded the request URL, probably as a result of a bad http => https redirect.
|
||||
if (
|
||||
preg_match( '/https?%3A%2F%2F/i', esc_url_raw( wp_unslash( $_GET['redirect_to'] ) ) ) > 0 // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotValidated -- no site changes, we're erroring out.
|
||||
) {
|
||||
/**
|
||||
* Jetpack authorisation request Error.
|
||||
*
|
||||
* @since jetpack-7.5.0
|
||||
*/
|
||||
do_action( 'jetpack_verify_api_authorization_request_error_double_encode' );
|
||||
$die_error = sprintf(
|
||||
/* translators: %s is a URL */
|
||||
__( 'Your site is incorrectly double-encoding redirects from http to https. This is preventing Jetpack from authenticating your connection. Please visit our <a href="%s">support page</a> for details about how to resolve this.', 'jetpack-connection' ),
|
||||
esc_url( Redirect::get_url( 'jetpack-support-double-encoding' ) )
|
||||
);
|
||||
}
|
||||
|
||||
$jetpack_signature = new \Jetpack_Signature( $token->secret, (int) Jetpack_Options::get_option( 'time_diff' ) );
|
||||
|
||||
if ( isset( $environment['jetpack_json_api_original_query'] ) ) {
|
||||
$signature = $jetpack_signature->sign_request(
|
||||
$environment['token'],
|
||||
$environment['timestamp'],
|
||||
$environment['nonce'],
|
||||
'',
|
||||
'GET',
|
||||
$environment['jetpack_json_api_original_query'],
|
||||
null,
|
||||
true
|
||||
);
|
||||
} else {
|
||||
$signature = $jetpack_signature->sign_current_request(
|
||||
array(
|
||||
'body' => null,
|
||||
'method' => 'GET',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if ( ! $signature ) {
|
||||
wp_die(
|
||||
wp_kses(
|
||||
$die_error,
|
||||
array(
|
||||
'a' => array(
|
||||
'href' => array(),
|
||||
),
|
||||
)
|
||||
)
|
||||
);
|
||||
} elseif ( is_wp_error( $signature ) ) {
|
||||
wp_die(
|
||||
wp_kses(
|
||||
$die_error,
|
||||
array(
|
||||
'a' => array(
|
||||
'href' => array(),
|
||||
),
|
||||
)
|
||||
)
|
||||
);
|
||||
} elseif ( ! hash_equals( $signature, $environment['signature'] ) ) {
|
||||
if ( is_ssl() ) {
|
||||
// If we signed an HTTP request on the Jetpack Servers, but got redirected to HTTPS by the local blog, check the HTTP signature as well.
|
||||
$signature = $jetpack_signature->sign_current_request(
|
||||
array(
|
||||
'scheme' => 'http',
|
||||
'body' => null,
|
||||
'method' => 'GET',
|
||||
)
|
||||
);
|
||||
if ( ! $signature || is_wp_error( $signature ) || ! hash_equals( $signature, $environment['signature'] ) ) {
|
||||
wp_die(
|
||||
wp_kses(
|
||||
$die_error,
|
||||
array(
|
||||
'a' => array(
|
||||
'href' => array(),
|
||||
),
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
wp_die(
|
||||
wp_kses(
|
||||
$die_error,
|
||||
array(
|
||||
'a' => array(
|
||||
'href' => array(),
|
||||
),
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$timestamp = (int) $environment['timestamp'];
|
||||
$nonce = stripslashes( (string) $environment['nonce'] );
|
||||
|
||||
if ( ! ( new Nonce_Handler() )->add( $timestamp, $nonce ) ) {
|
||||
// De-nonce the nonce, at least for 5 minutes.
|
||||
// We have to reuse this nonce at least once (used the first time when the initial request is made, used a second time when the login form is POSTed).
|
||||
$old_nonce_time = get_option( "jetpack_nonce_{$timestamp}_{$nonce}" );
|
||||
if ( $old_nonce_time < time() - 300 ) {
|
||||
wp_die( esc_html__( 'The authorization process expired. Please go back and try again.', 'jetpack-connection' ) );
|
||||
}
|
||||
}
|
||||
|
||||
$data = json_decode(
|
||||
base64_decode( stripslashes( $environment['data'] ) ) // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
|
||||
);
|
||||
$data_filters = array(
|
||||
'state' => 'opaque',
|
||||
'client_id' => 'int',
|
||||
'client_title' => 'string',
|
||||
'client_image' => 'url',
|
||||
);
|
||||
|
||||
foreach ( $data_filters as $key => $sanitation ) {
|
||||
if ( ! isset( $data->$key ) ) {
|
||||
wp_die(
|
||||
wp_kses(
|
||||
$die_error,
|
||||
array(
|
||||
'a' => array(
|
||||
'href' => array(),
|
||||
),
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
switch ( $sanitation ) {
|
||||
case 'int':
|
||||
$this->json_api_authorization_request[ $key ] = (int) $data->$key;
|
||||
break;
|
||||
case 'opaque':
|
||||
$this->json_api_authorization_request[ $key ] = (string) $data->$key;
|
||||
break;
|
||||
case 'string':
|
||||
$this->json_api_authorization_request[ $key ] = wp_kses( (string) $data->$key, array() );
|
||||
break;
|
||||
case 'url':
|
||||
$this->json_api_authorization_request[ $key ] = esc_url_raw( (string) $data->$key );
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ( empty( $this->json_api_authorization_request['client_id'] ) ) {
|
||||
wp_die(
|
||||
wp_kses(
|
||||
$die_error,
|
||||
array(
|
||||
'a' => array(
|
||||
'href' => array(),
|
||||
),
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the Access Code details to the public-api.wordpress.com redirect.
|
||||
*
|
||||
* @since 2.7.6 Ported from Jetpack to the Connection package.
|
||||
*
|
||||
* @param string $redirect_to URL.
|
||||
* @param string $original_redirect_to URL.
|
||||
* @param \WP_User $user WP_User for the redirect.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function add_token_to_login_redirect_json_api_authorization( $redirect_to, $original_redirect_to, $user ) {
|
||||
return add_query_arg(
|
||||
urlencode_deep(
|
||||
array(
|
||||
'jetpack-code' => get_user_meta(
|
||||
$user->ID,
|
||||
'jetpack_json_api_' . $this->json_api_authorization_request['client_id'],
|
||||
true
|
||||
),
|
||||
'jetpack-user-id' => (int) $user->ID,
|
||||
'jetpack-state' => $this->json_api_authorization_request['state'],
|
||||
)
|
||||
),
|
||||
$redirect_to
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* If someone logs in to approve API access, store the Access Code in usermeta.
|
||||
*
|
||||
* @since 2.7.6 Ported from Jetpack to the Connection package.
|
||||
*
|
||||
* @param string $user_login Unused.
|
||||
* @param \WP_User $user User logged in.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function store_json_api_authorization_token( $user_login, $user ) {
|
||||
add_filter( 'login_redirect', array( $this, 'add_token_to_login_redirect_json_api_authorization' ), 10, 3 );
|
||||
add_filter( 'allowed_redirect_hosts', array( Host::class, 'allow_wpcom_public_api_domain' ) );
|
||||
$token = wp_generate_password( 32, false );
|
||||
update_user_meta( $user->ID, 'jetpack_json_api_' . $this->json_api_authorization_request['client_id'], $token );
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML for the JSON API authorization notice.
|
||||
*
|
||||
* @since 2.7.6 Ported from Jetpack to the Connection package.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function login_message_json_api_authorization() {
|
||||
return '<p class="message">' . sprintf(
|
||||
/* translators: Name/image of the client requesting authorization */
|
||||
esc_html__( '%s wants to access your site’s data. Log in to authorize that access.', 'jetpack-connection' ),
|
||||
'<strong>' . esc_html( $this->json_api_authorization_request['client_title'] ) . '</strong>'
|
||||
) . '<img src="' . esc_url( $this->json_api_authorization_request['client_image'] ) . '" /></p>';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,513 @@
|
||||
<?php
|
||||
/**
|
||||
* The Connection Client class file.
|
||||
*
|
||||
* @package automattic/jetpack-connection
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Connection;
|
||||
|
||||
use Automattic\Jetpack\Constants;
|
||||
use WP_Error;
|
||||
|
||||
// `wp_remote_request` returns an array with a particular format.
|
||||
'@phan-type _WP_Remote_Response_Array = array{headers:\WpOrg\Requests\Utility\CaseInsensitiveDictionary,body:string,response:array{code:int,message:string},cookies:\WP_HTTP_Cookie[],filename:?string,http_response:WP_HTTP_Requests_Response}';
|
||||
|
||||
/**
|
||||
* The Client class that is used to connect to WordPress.com Jetpack API.
|
||||
*/
|
||||
class Client {
|
||||
const WPCOM_JSON_API_VERSION = '1.1';
|
||||
|
||||
/**
|
||||
* Makes an authorized remote request using Jetpack_Signature
|
||||
*
|
||||
* @param array $args the arguments for the remote request.
|
||||
* @param array|string|null $body the request body.
|
||||
* @return array|WP_Error WP HTTP response on success
|
||||
* @phan-return _WP_Remote_Response_Array|WP_Error
|
||||
*/
|
||||
public static function remote_request( $args, $body = null ) {
|
||||
if ( isset( $args['url'] ) ) {
|
||||
/**
|
||||
* Filters the remote request url.
|
||||
*
|
||||
* @since 1.30.12
|
||||
*
|
||||
* @param string The remote request url.
|
||||
*/
|
||||
$args['url'] = apply_filters( 'jetpack_remote_request_url', $args['url'] );
|
||||
}
|
||||
|
||||
$result = self::build_signed_request( $args, $body );
|
||||
if ( is_wp_error( $result ) ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$response = self::_wp_remote_request( $result['url'], $result['request'] );
|
||||
|
||||
Error_Handler::get_instance()->check_api_response_for_errors(
|
||||
$response,
|
||||
$result['auth'],
|
||||
empty( $args['url'] ) ? '' : $args['url'],
|
||||
empty( $args['method'] ) ? 'POST' : $args['method'],
|
||||
'rest'
|
||||
);
|
||||
|
||||
/**
|
||||
* Fired when the remote request response has been received.
|
||||
*
|
||||
* @since 1.30.8
|
||||
*
|
||||
* @param array|WP_Error The HTTP response.
|
||||
*/
|
||||
do_action( 'jetpack_received_remote_request_response', $response );
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds authorization signature to a remote request using Jetpack_Signature
|
||||
*
|
||||
* @param array $args the arguments for the remote request.
|
||||
* @param array|string|null $body the request body.
|
||||
* @return WP_Error|array{url:string,request:array,auth:array} {
|
||||
* An array containing URL and request items.
|
||||
*
|
||||
* @type string $url The request URL.
|
||||
* @type array $request Request arguments.
|
||||
* @type array $auth Authorization data.
|
||||
* }
|
||||
*/
|
||||
public static function build_signed_request( $args, $body = null ) {
|
||||
add_filter(
|
||||
'jetpack_constant_default_value',
|
||||
__NAMESPACE__ . '\Utils::jetpack_api_constant_filter',
|
||||
10,
|
||||
2
|
||||
);
|
||||
|
||||
$defaults = array(
|
||||
'url' => '',
|
||||
'user_id' => 0,
|
||||
'blog_id' => 0,
|
||||
'auth_location' => Constants::get_constant( 'JETPACK_CLIENT__AUTH_LOCATION' ),
|
||||
'method' => 'POST',
|
||||
'format' => 'json',
|
||||
'timeout' => 10,
|
||||
'redirection' => 0,
|
||||
'headers' => array(),
|
||||
'stream' => false,
|
||||
'filename' => null,
|
||||
'sslverify' => true,
|
||||
);
|
||||
|
||||
$args = wp_parse_args( $args, $defaults );
|
||||
|
||||
$args['blog_id'] = (int) $args['blog_id'];
|
||||
|
||||
if ( 'header' !== $args['auth_location'] ) {
|
||||
$args['auth_location'] = 'query_string';
|
||||
}
|
||||
|
||||
$token = ( new Tokens() )->get_access_token( $args['user_id'] );
|
||||
if ( ! $token ) {
|
||||
return new WP_Error( 'missing_token' );
|
||||
}
|
||||
|
||||
$method = strtoupper( $args['method'] );
|
||||
|
||||
$timeout = (int) $args['timeout'];
|
||||
|
||||
$redirection = $args['redirection'];
|
||||
$stream = $args['stream'];
|
||||
$filename = $args['filename'];
|
||||
$sslverify = $args['sslverify'];
|
||||
|
||||
$request = compact( 'method', 'body', 'timeout', 'redirection', 'stream', 'filename', 'sslverify' );
|
||||
|
||||
@list( $token_key, $secret ) = explode( '.', $token->secret ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
|
||||
if ( ! $secret ) {
|
||||
return new WP_Error( 'malformed_token' );
|
||||
}
|
||||
|
||||
$token_key = sprintf(
|
||||
'%s:%d:%d',
|
||||
$token_key,
|
||||
Constants::get_constant( 'JETPACK__API_VERSION' ),
|
||||
$token->external_user_id
|
||||
);
|
||||
|
||||
$time_diff = (int) \Jetpack_Options::get_option( 'time_diff' );
|
||||
$jetpack_signature = new \Jetpack_Signature( $token->secret, $time_diff );
|
||||
|
||||
$timestamp = time() + $time_diff;
|
||||
|
||||
if ( function_exists( 'wp_generate_password' ) ) {
|
||||
$nonce = wp_generate_password( 10, false );
|
||||
} else {
|
||||
$nonce = substr( sha1( (string) wp_rand( 0, 1000000 ) ), 0, 10 );
|
||||
}
|
||||
|
||||
// Kind of annoying. Maybe refactor Jetpack_Signature to handle body-hashing.
|
||||
if ( $body === null ) {
|
||||
$body_hash = '';
|
||||
|
||||
} else {
|
||||
// Allow arrays to be used in passing data.
|
||||
$body_to_hash = $body;
|
||||
|
||||
if ( $args['format'] === 'jsonl' ) {
|
||||
parse_str( $body, $body_to_hash );
|
||||
}
|
||||
if ( is_array( $body_to_hash ) ) {
|
||||
// We cast this to a new variable, because the array form of $body needs to be
|
||||
// maintained so it can be passed into the request later on in the code.
|
||||
if ( array() !== $body_to_hash ) {
|
||||
$body_to_hash = wp_json_encode(
|
||||
self::_stringify_data( $body_to_hash ),
|
||||
0 // phpcs:ignore Jetpack.Functions.JsonEncodeFlags.ZeroFound -- No `json_encode()` flags because this needs to match whatever is calculating the hash on the other end.
|
||||
);
|
||||
} else {
|
||||
$body_to_hash = '';
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! is_string( $body_to_hash ) ) {
|
||||
return new WP_Error( 'invalid_body', 'Body is malformed.' );
|
||||
}
|
||||
$body_hash = base64_encode( sha1( $body_to_hash, true ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
|
||||
}
|
||||
|
||||
$auth = array(
|
||||
'token' => $token_key,
|
||||
'timestamp' => $timestamp,
|
||||
'nonce' => $nonce,
|
||||
'body-hash' => $body_hash,
|
||||
);
|
||||
|
||||
if ( false !== strpos( $args['url'], 'xmlrpc.php' ) ) {
|
||||
$url_args = array(
|
||||
'for' => 'jetpack',
|
||||
'wpcom_blog_id' => \Jetpack_Options::get_option( 'id' ),
|
||||
);
|
||||
} else {
|
||||
$url_args = array();
|
||||
}
|
||||
|
||||
if ( 'header' !== $args['auth_location'] ) {
|
||||
$url_args += $auth;
|
||||
}
|
||||
|
||||
$url = add_query_arg( urlencode_deep( $url_args ), $args['url'] );
|
||||
|
||||
$signature = $jetpack_signature->sign_request( $token_key, $timestamp, $nonce, $body_hash, $method, $url, $body, false );
|
||||
|
||||
if ( is_wp_error( $signature ) ) {
|
||||
return $signature;
|
||||
}
|
||||
|
||||
// Send an Authorization header so various caches/proxies do the right thing.
|
||||
$auth['signature'] = $signature;
|
||||
$auth['version'] = Constants::get_constant( 'JETPACK__VERSION' );
|
||||
$header_pieces = array();
|
||||
foreach ( $auth as $key => $value ) {
|
||||
$header_pieces[] = sprintf( '%s="%s"', $key, $value );
|
||||
}
|
||||
$request['headers'] = array_merge(
|
||||
$args['headers'],
|
||||
array(
|
||||
'Authorization' => 'X_JETPACK ' . implode( ' ', $header_pieces ),
|
||||
)
|
||||
);
|
||||
|
||||
if ( 'header' !== $args['auth_location'] ) {
|
||||
$url = add_query_arg( 'signature', rawurlencode( $signature ), $url );
|
||||
}
|
||||
|
||||
return compact( 'url', 'request', 'auth' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for wp_remote_request(). Turns off SSL verification for certain SSL errors.
|
||||
* This is lame, but many, many, many hosts have misconfigured SSL.
|
||||
*
|
||||
* When Jetpack is registered, the jetpack_fallback_no_verify_ssl_certs option is set to the current time if:
|
||||
* 1. a certificate error is found AND
|
||||
* 2. not verifying the certificate works around the problem.
|
||||
*
|
||||
* The option is checked on each request.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @param string $url the request URL.
|
||||
* @param array $args request arguments.
|
||||
* @param boolean $set_fallback whether to allow flagging this request to use a fallback certficate override.
|
||||
* @return array|WP_Error WP HTTP response on success
|
||||
* @phan-return _WP_Remote_Response_Array|WP_Error
|
||||
*/
|
||||
public static function _wp_remote_request( $url, $args, $set_fallback = false ) { // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore
|
||||
$fallback = \Jetpack_Options::get_option( 'fallback_no_verify_ssl_certs' );
|
||||
if ( false === $fallback ) {
|
||||
\Jetpack_Options::update_option( 'fallback_no_verify_ssl_certs', 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* SSL verification (`sslverify`) for the JetpackClient remote request
|
||||
* defaults to off, use this filter to force it on.
|
||||
*
|
||||
* Return `true` to ENABLE SSL verification, return `false`
|
||||
* to DISABLE SSL verification.
|
||||
*
|
||||
* @since 1.7.0
|
||||
* @since-jetpack 3.6.0
|
||||
*
|
||||
* @param bool Whether to force `sslverify` or not.
|
||||
*/
|
||||
if ( apply_filters( 'jetpack_client_verify_ssl_certs', false ) ) {
|
||||
return wp_remote_request( $url, $args );
|
||||
}
|
||||
|
||||
if ( (int) $fallback ) {
|
||||
// We're flagged to fallback.
|
||||
$args['sslverify'] = false;
|
||||
}
|
||||
|
||||
$response = wp_remote_request( $url, $args );
|
||||
|
||||
if (
|
||||
! $set_fallback // We're not allowed to set the flag on this request, so whatever happens happens.
|
||||
||
|
||||
isset( $args['sslverify'] ) && ! $args['sslverify'] // No verification - no point in doing it again.
|
||||
||
|
||||
! is_wp_error( $response ) // Let it ride.
|
||||
) {
|
||||
self::set_time_diff( $response, $set_fallback );
|
||||
return $response;
|
||||
}
|
||||
|
||||
// At this point, we're not flagged to fallback and we are allowed to set the flag on this request.
|
||||
|
||||
$message = $response->get_error_message();
|
||||
|
||||
// Is it an SSL Certificate verification error?
|
||||
if (
|
||||
false === strpos( $message, '14090086' ) // OpenSSL SSL3 certificate error.
|
||||
&&
|
||||
false === strpos( $message, '1407E086' ) // OpenSSL SSL2 certificate error.
|
||||
&&
|
||||
false === strpos( $message, 'error setting certificate verify locations' ) // cURL CA bundle not found.
|
||||
&&
|
||||
false === strpos( $message, 'Peer certificate cannot be authenticated with' ) // cURL CURLE_SSL_CACERT: CA bundle found, but not helpful
|
||||
// Different versions of curl have different error messages
|
||||
// this string should catch them all.
|
||||
&&
|
||||
false === strpos( $message, 'Problem with the SSL CA cert' ) // cURL CURLE_SSL_CACERT_BADFILE: probably access rights.
|
||||
) {
|
||||
// No, it is not.
|
||||
return $response;
|
||||
}
|
||||
|
||||
// Redo the request without SSL certificate verification.
|
||||
$args['sslverify'] = false;
|
||||
$response = wp_remote_request( $url, $args );
|
||||
|
||||
if ( ! is_wp_error( $response ) ) {
|
||||
// The request went through this time, flag for future fallbacks.
|
||||
\Jetpack_Options::update_option( 'fallback_no_verify_ssl_certs', time() );
|
||||
self::set_time_diff( $response, $set_fallback );
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the time difference for correct signature computation.
|
||||
*
|
||||
* @param array|WP_Error $response Response array from `wp_remote_request`, or WP_Error on error.
|
||||
* @param bool $force_set whether to force setting the time difference.
|
||||
* @phan-param _WP_Remote_Response_Array|WP_Error $response
|
||||
*/
|
||||
public static function set_time_diff( &$response, $force_set = false ) {
|
||||
$code = wp_remote_retrieve_response_code( $response );
|
||||
|
||||
// Only trust the Date header on some responses.
|
||||
if ( 200 != $code && 304 != $code && 400 != $code && 401 != $code ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseNotEqual
|
||||
return;
|
||||
}
|
||||
|
||||
$date = wp_remote_retrieve_header( $response, 'date' );
|
||||
if ( ! $date ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$time = (int) strtotime( $date );
|
||||
if ( 0 >= $time ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$time_diff = $time - time();
|
||||
|
||||
if ( $force_set ) { // During register.
|
||||
\Jetpack_Options::update_option( 'time_diff', $time_diff );
|
||||
} else { // Otherwise.
|
||||
$old_diff = \Jetpack_Options::get_option( 'time_diff' );
|
||||
if ( false === $old_diff || abs( $time_diff - (int) $old_diff ) > 10 ) {
|
||||
\Jetpack_Options::update_option( 'time_diff', $time_diff );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate and build arguments for a WordPress.com REST API request.
|
||||
*
|
||||
* @param string $path REST API path.
|
||||
* @param string $version REST API version. Default is `2`.
|
||||
* @param array $args Arguments to {@see WP_Http}. Default is `array()`.
|
||||
* @param string $base_api_path REST API root. Default is `wpcom`.
|
||||
*
|
||||
* @return array Validated arguments.
|
||||
*/
|
||||
public static function validate_args_for_wpcom_json_api_request(
|
||||
$path,
|
||||
$version = '2',
|
||||
$args = array(),
|
||||
$base_api_path = 'wpcom'
|
||||
) {
|
||||
$base_api_path = trim( $base_api_path, '/' );
|
||||
$version = ltrim( $version, 'v' );
|
||||
$path = ltrim( $path, '/' );
|
||||
|
||||
$filtered_args = array_intersect_key(
|
||||
$args,
|
||||
array(
|
||||
'headers' => 'array',
|
||||
'method' => 'string',
|
||||
'format' => 'string',
|
||||
'timeout' => 'int',
|
||||
'redirection' => 'int',
|
||||
'stream' => 'boolean',
|
||||
'filename' => 'string',
|
||||
'sslverify' => 'boolean',
|
||||
)
|
||||
);
|
||||
|
||||
// Use GET by default whereas `remote_request` uses POST.
|
||||
$request_method = isset( $filtered_args['method'] ) ? strtoupper( $filtered_args['method'] ) : 'GET';
|
||||
|
||||
$url = sprintf(
|
||||
'%s/%s/v%s/%s',
|
||||
Constants::get_constant( 'JETPACK__WPCOM_JSON_API_BASE' ),
|
||||
$base_api_path,
|
||||
$version,
|
||||
$path
|
||||
);
|
||||
|
||||
$validated_args = array_merge(
|
||||
$filtered_args,
|
||||
array(
|
||||
'url' => $url,
|
||||
'method' => $request_method,
|
||||
)
|
||||
);
|
||||
|
||||
return $validated_args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Queries the WordPress.com REST API with a user token.
|
||||
*
|
||||
* @param string $path REST API path.
|
||||
* @param string $version REST API version. Default is `2`.
|
||||
* @param array $args Arguments to {@see WP_Http}. Default is `array()`.
|
||||
* @param null|string|array $body Body passed to {@see WP_Http}. Default is `null`.
|
||||
* @param string $base_api_path REST API root. Default is `wpcom`.
|
||||
*
|
||||
* @return array|WP_Error $response Response data, else {@see WP_Error} on failure.
|
||||
* @phan-return _WP_Remote_Response_Array|WP_Error
|
||||
*/
|
||||
public static function wpcom_json_api_request_as_user(
|
||||
$path,
|
||||
$version = '2',
|
||||
$args = array(),
|
||||
$body = null,
|
||||
$base_api_path = 'wpcom'
|
||||
) {
|
||||
$args = self::validate_args_for_wpcom_json_api_request( $path, $version, $args, $base_api_path );
|
||||
$args['user_id'] = get_current_user_id();
|
||||
|
||||
if ( isset( $body ) && ! isset( $args['headers'] ) && in_array( $args['method'], array( 'POST', 'PUT', 'PATCH' ), true ) ) {
|
||||
$args['headers'] = array( 'Content-Type' => 'application/json' );
|
||||
}
|
||||
|
||||
if ( isset( $body ) && ! is_string( $body ) ) {
|
||||
$body = wp_json_encode( $body, JSON_UNESCAPED_SLASHES );
|
||||
}
|
||||
|
||||
return self::remote_request( $args, $body );
|
||||
}
|
||||
|
||||
/**
|
||||
* Query the WordPress.com REST API using the blog token
|
||||
*
|
||||
* @param string $path The API endpoint relative path.
|
||||
* @param string $version The API version.
|
||||
* @param array $args Request arguments.
|
||||
* @param array|string|null $body Request body.
|
||||
* @param string $base_api_path (optional) the API base path override, defaults to 'rest'.
|
||||
* @return array|WP_Error $response Data.
|
||||
* @phan-return _WP_Remote_Response_Array|WP_Error
|
||||
*/
|
||||
public static function wpcom_json_api_request_as_blog(
|
||||
$path,
|
||||
$version = self::WPCOM_JSON_API_VERSION,
|
||||
$args = array(),
|
||||
$body = null,
|
||||
$base_api_path = 'rest'
|
||||
) {
|
||||
$validated_args = self::validate_args_for_wpcom_json_api_request( $path, $version, $args, $base_api_path );
|
||||
$validated_args['blog_id'] = (int) \Jetpack_Options::get_option( 'id' );
|
||||
|
||||
// For Simple sites get the response directly without any HTTP requests.
|
||||
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
|
||||
add_filter( 'is_jetpack_authorized_for_site', '__return_true' );
|
||||
require_lib( 'wpcom-api-direct' );
|
||||
return \WPCOM_API_Direct::do_request( $validated_args, $body );
|
||||
}
|
||||
|
||||
return self::remote_request( $validated_args, $body );
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes an array or similar structure and recursively turns all values into strings. This is used to
|
||||
* make sure that body hashes are made ith the string version, which is what will be seen after a
|
||||
* server pulls up the data in the $_POST array.
|
||||
*
|
||||
* @param mixed $data the data that needs to be stringified.
|
||||
*
|
||||
* @return array|string
|
||||
*/
|
||||
public static function _stringify_data( $data ) { // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore
|
||||
|
||||
// Booleans are special, lets just makes them and explicit 1/0 instead of the 0 being an empty string.
|
||||
if ( is_bool( $data ) ) {
|
||||
return $data ? '1' : '0';
|
||||
}
|
||||
|
||||
// Cast objects into arrays.
|
||||
if ( is_object( $data ) ) {
|
||||
$data = (array) $data;
|
||||
}
|
||||
|
||||
// Non arrays at this point should be just converted to strings.
|
||||
if ( ! is_array( $data ) ) {
|
||||
return (string) $data;
|
||||
}
|
||||
|
||||
foreach ( $data as &$value ) {
|
||||
$value = self::_stringify_data( $value );
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
/**
|
||||
* Connection_Assets.
|
||||
*
|
||||
* @package automattic/jetpack-connection
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Connection;
|
||||
|
||||
use Automattic\Jetpack\Assets;
|
||||
|
||||
/**
|
||||
* Connection_Assets class.
|
||||
*/
|
||||
class Connection_Assets {
|
||||
|
||||
/**
|
||||
* Initialize the class.
|
||||
*/
|
||||
public static function configure() {
|
||||
add_action( 'wp_loaded', array( __CLASS__, 'register_assets' ) );
|
||||
|
||||
add_filter( 'jetpack_admin_js_script_data', array( Initial_State::class, 'set_connection_script_data' ), 10, 1 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register assets.
|
||||
*
|
||||
* NOTICE: Please think twice before including Connection scripts in the frontend.
|
||||
* Those scripts are intended to be used in WP admin area.
|
||||
*/
|
||||
public static function register_assets() {
|
||||
|
||||
Assets::register_script(
|
||||
'jetpack-connection',
|
||||
'../dist/jetpack-connection.js',
|
||||
__FILE__,
|
||||
array(
|
||||
'in_footer' => true,
|
||||
'textdomain' => 'jetpack-connection',
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
<?php
|
||||
/**
|
||||
* Admin connection notices.
|
||||
*
|
||||
* @package automattic/jetpack-admin-ui
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Connection;
|
||||
|
||||
use Automattic\Jetpack\Redirect;
|
||||
use Automattic\Jetpack\Tracking;
|
||||
|
||||
/**
|
||||
* Admin connection notices.
|
||||
*
|
||||
* @phan-constructor-used-for-side-effects
|
||||
*/
|
||||
class Connection_Notice {
|
||||
|
||||
/**
|
||||
* Whether the class has been initialized.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private static $is_initialized = false;
|
||||
|
||||
/**
|
||||
* The constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
if ( ! static::$is_initialized ) {
|
||||
add_action( 'current_screen', array( $this, 'initialize_notices' ) );
|
||||
static::$is_initialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the notices if needed.
|
||||
*
|
||||
* @param \WP_Screen $screen WP Core's screen object.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function initialize_notices( $screen ) {
|
||||
if ( in_array(
|
||||
$screen->id,
|
||||
array(
|
||||
'jetpack_page_akismet-key-config',
|
||||
'admin_page_jetpack_modules',
|
||||
),
|
||||
true
|
||||
) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* phpcs:disable WordPress.Security.NonceVerification.Recommended
|
||||
*
|
||||
* This function is firing within wp-admin and checks (below) if it is in the midst of a deletion on the users
|
||||
* page. Nonce will be already checked by WordPress, so we do not need to check ourselves.
|
||||
*/
|
||||
|
||||
if ( isset( $screen->base ) && 'users' === $screen->base
|
||||
&& isset( $_REQUEST['action'] ) && 'delete' === $_REQUEST['action']
|
||||
) {
|
||||
add_action( 'admin_notices', array( $this, 'delete_user_update_connection_owner_notice' ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is an entire admin notice dedicated to messaging and handling of the case where a user is trying to delete
|
||||
* the connection owner.
|
||||
*/
|
||||
public function delete_user_update_connection_owner_notice() {
|
||||
// Get connection owner or bail.
|
||||
$connection_manager = new Manager();
|
||||
$connection_owner_id = $connection_manager->get_connection_owner_id();
|
||||
if ( ! $connection_owner_id ) {
|
||||
return;
|
||||
}
|
||||
$connection_owner_userdata = get_userdata( $connection_owner_id );
|
||||
|
||||
// Bail if we're not trying to delete connection owner.
|
||||
$user_ids_to_delete = array();
|
||||
if ( isset( $_REQUEST['users'] ) ) {
|
||||
$user_ids_to_delete = array_map( 'sanitize_text_field', wp_unslash( $_REQUEST['users'] ) );
|
||||
} elseif ( isset( $_REQUEST['user'] ) ) {
|
||||
$user_ids_to_delete[] = sanitize_text_field( wp_unslash( $_REQUEST['user'] ) );
|
||||
}
|
||||
|
||||
// phpcs:enable
|
||||
$user_ids_to_delete = array_map( 'absint', $user_ids_to_delete );
|
||||
$deleting_connection_owner = in_array( $connection_owner_id, (array) $user_ids_to_delete, true );
|
||||
if ( ! $deleting_connection_owner ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Bail if they're trying to delete themselves to avoid confusion.
|
||||
if ( get_current_user_id() === $connection_owner_id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$tracking = new Tracking();
|
||||
|
||||
// Track it!
|
||||
if ( method_exists( $tracking, 'record_user_event' ) ) {
|
||||
$tracking->record_user_event( 'delete_connection_owner_notice_view' );
|
||||
}
|
||||
|
||||
$connected_admins = $connection_manager->get_connected_users( 'jetpack_disconnect' );
|
||||
$user = is_a( $connection_owner_userdata, 'WP_User' ) ? esc_html( $connection_owner_userdata->data->user_login ) : '';
|
||||
|
||||
echo "<div class='notice notice-warning' id='jetpack-notice-switch-connection-owner'>";
|
||||
echo '<h2>' . esc_html__( 'Important notice about your Jetpack connection:', 'jetpack-connection' ) . '</h2>';
|
||||
echo '<p>' . sprintf(
|
||||
/* translators: WordPress User, if available. */
|
||||
esc_html__( 'Warning! You are about to delete the Jetpack connection owner (%s) for this site, which may cause some of your Jetpack features to stop working.', 'jetpack-connection' ),
|
||||
esc_html( $user )
|
||||
) . '</p>';
|
||||
|
||||
if ( ! empty( $connected_admins ) && count( $connected_admins ) > 1 ) {
|
||||
echo '<form id="jp-switch-connection-owner" action="" method="post">';
|
||||
echo "<label for='owner'>" . esc_html__( 'You can choose to transfer connection ownership to one of these already-connected admins:', 'jetpack-connection' ) . ' </label>';
|
||||
|
||||
$connected_admin_ids = array_map(
|
||||
function ( $connected_admin ) {
|
||||
return $connected_admin->ID;
|
||||
},
|
||||
$connected_admins
|
||||
);
|
||||
|
||||
wp_dropdown_users(
|
||||
array(
|
||||
'name' => 'owner',
|
||||
'include' => array_diff( $connected_admin_ids, array( $connection_owner_id ) ),
|
||||
'show' => 'display_name_with_login',
|
||||
)
|
||||
);
|
||||
|
||||
echo '<p>';
|
||||
submit_button( esc_html__( 'Set new connection owner', 'jetpack-connection' ), 'primary', 'jp-switch-connection-owner-submit', false );
|
||||
echo '</p>';
|
||||
|
||||
echo "<div id='jp-switch-user-results'></div>";
|
||||
echo '</form>';
|
||||
?>
|
||||
<script type="text/javascript">
|
||||
( function() {
|
||||
const switchOwnerButton = document.getElementById('jp-switch-connection-owner');
|
||||
if ( ! switchOwnerButton ) {
|
||||
return;
|
||||
}
|
||||
|
||||
switchOwnerButton.addEventListener( 'submit', function ( e ) {
|
||||
e.preventDefault();
|
||||
|
||||
const submitBtn = document.getElementById('jp-switch-connection-owner-submit');
|
||||
submitBtn.disabled = true;
|
||||
|
||||
const results = document.getElementById('jp-switch-user-results');
|
||||
results.innerHTML = '';
|
||||
results.classList.remove( 'error-message' );
|
||||
|
||||
const handleAPIError = ( message ) => {
|
||||
submitBtn.disabled = false;
|
||||
|
||||
results.classList.add( 'error-message' );
|
||||
results.innerHTML = message || "<?php esc_html_e( 'Something went wrong. Please try again.', 'jetpack-connection' ); ?>";
|
||||
}
|
||||
|
||||
fetch(
|
||||
<?php echo wp_json_encode( esc_url_raw( get_rest_url() . 'jetpack/v4/connection/owner' ), JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ); ?>,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-WP-Nonce': <?php echo wp_json_encode( wp_create_nonce( 'wp_rest' ), JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ); ?>,
|
||||
},
|
||||
body: new URLSearchParams( new FormData( this ) ),
|
||||
}
|
||||
)
|
||||
.then( response => response.json() )
|
||||
.then( data => {
|
||||
if ( data.hasOwnProperty( 'code' ) && data.code === 'success' ) {
|
||||
// Owner successfully changed.
|
||||
results.innerHTML = <?php echo wp_json_encode( esc_html__( 'Success!', 'jetpack-connection' ), JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ); ?>;
|
||||
setTimeout(function () {
|
||||
document.getElementById( 'jetpack-notice-switch-connection-owner' ).style.display = 'none';
|
||||
}, 1000);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
handleAPIError( data?.message );
|
||||
} )
|
||||
.catch( () => handleAPIError() );
|
||||
});
|
||||
} )();
|
||||
</script>
|
||||
<?php
|
||||
} else {
|
||||
echo '<p>' . esc_html__( 'Every Jetpack site needs at least one connected admin for the features to work properly. Please connect to your WordPress.com account via the button below. Once you connect, you may refresh this page to see an option to change the connection owner.', 'jetpack-connection' ) . '</p>';
|
||||
$connect_url = $connection_manager->get_authorization_url();
|
||||
$connect_url = add_query_arg( 'from', 'delete_connection_owner_notice', $connect_url );
|
||||
echo "<a href='" . esc_url( $connect_url ) . "' target='_blank' rel='noopener noreferrer' class='button-primary'>" . esc_html__( 'Connect to WordPress.com', 'jetpack-connection' ) . '</a>';
|
||||
}
|
||||
|
||||
echo '<p>';
|
||||
printf(
|
||||
wp_kses(
|
||||
/* translators: URL to Jetpack support doc regarding the primary user. */
|
||||
__( "<a href='%s' target='_blank' rel='noopener noreferrer'>Learn more</a> about the connection owner and what will break if you do not have one.", 'jetpack-connection' ),
|
||||
array(
|
||||
'a' => array(
|
||||
'href' => true,
|
||||
'target' => true,
|
||||
'rel' => true,
|
||||
),
|
||||
)
|
||||
),
|
||||
esc_url( Redirect::get_url( 'jetpack-support-primary-user' ) )
|
||||
);
|
||||
echo '</p>';
|
||||
echo '<p>';
|
||||
printf(
|
||||
wp_kses(
|
||||
/* translators: URL to contact Jetpack support. */
|
||||
__( 'As always, feel free to <a href="%s" target="_blank" rel="noopener noreferrer">contact our support team</a> if you have any questions.', 'jetpack-connection' ),
|
||||
array(
|
||||
'a' => array(
|
||||
'href' => true,
|
||||
'target' => true,
|
||||
'rel' => true,
|
||||
),
|
||||
)
|
||||
),
|
||||
esc_url( Redirect::get_url( 'jetpack-contact-support' ) )
|
||||
);
|
||||
echo '</p>';
|
||||
echo '</div>';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
<?php
|
||||
/**
|
||||
* External Storage utilities for Jetpack Connection.
|
||||
*
|
||||
* Provides centralized logic for external storage implementations
|
||||
* across different environments (WoA, VIP, other).
|
||||
*
|
||||
* Usage Example:
|
||||
*
|
||||
* // 1. Create a storage provider class implementing the interface:
|
||||
* class My_Storage_Provider implements Storage_Provider_Interface {
|
||||
* public function is_available() { return true; }
|
||||
* public function should_handle( $option_name ) {
|
||||
* return in_array( $option_name, array( 'blog_token', 'id' ), true );
|
||||
* }
|
||||
* public function get( $option_name ) {
|
||||
* // Return value from your external storage or null
|
||||
* }
|
||||
* public function get_environment_id() { return 'my_env'; }
|
||||
* }
|
||||
*
|
||||
* // 2. Register the provider:
|
||||
* if ( class_exists( 'Automattic\Jetpack\Connection\External_Storage' ) ) {
|
||||
* \Automattic\Jetpack\Connection\External_Storage::register_provider( new My_Storage_Provider() );
|
||||
* }
|
||||
*
|
||||
* // 3. External storage is now automatically used by Jetpack_Options::get_option()
|
||||
*
|
||||
* @package automattic/jetpack-connection
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Connection;
|
||||
|
||||
/**
|
||||
* External Storage utilities class.
|
||||
*
|
||||
* @since 6.18.0
|
||||
*/
|
||||
class External_Storage {
|
||||
|
||||
/**
|
||||
* Registered storage provider.
|
||||
*
|
||||
* @since 6.18.0
|
||||
*
|
||||
* @var Storage_Provider_Interface|null
|
||||
*/
|
||||
private static $provider = null;
|
||||
|
||||
/**
|
||||
* Whether the init action has already fired.
|
||||
*
|
||||
* @since 8.3.0
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private static $init_fired = false;
|
||||
|
||||
/**
|
||||
* Static cache to prevent logging same event multiple times in single request.
|
||||
*
|
||||
* @since 7.0.0
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static $logged_events = array();
|
||||
|
||||
/**
|
||||
* Maximum delay threshold for empty state reporting (in seconds).
|
||||
* This also determines the transient expiry for tracking first empty state.
|
||||
* Provider custom thresholds must not exceed this value.
|
||||
*
|
||||
* @since 7.0.0
|
||||
*/
|
||||
private const EMPTY_STATE_TRANSIENT_EXPIRY = 15 * MINUTE_IN_SECONDS;
|
||||
|
||||
/**
|
||||
* Register a storage provider for external storage.
|
||||
*
|
||||
* @since 6.18.0
|
||||
*
|
||||
* @param Storage_Provider_Interface $provider Storage provider implementing the interface.
|
||||
* @return bool True if provider was registered successfully, false otherwise.
|
||||
*/
|
||||
public static function register_provider( Storage_Provider_Interface $provider ) {
|
||||
self::$provider = $provider;
|
||||
|
||||
/**
|
||||
* Fires after an external storage provider is registered.
|
||||
*
|
||||
* This allows dependent systems (like the connection status cache in Manager)
|
||||
* to invalidate state that may have been computed before the provider was available.
|
||||
*
|
||||
* @since 8.3.0
|
||||
*
|
||||
* @param Storage_Provider_Interface $provider The registered storage provider.
|
||||
*/
|
||||
do_action( 'jetpack_external_storage_provider_registered', $provider );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get value from external storage provider.
|
||||
*
|
||||
* Returns null if no provider is registered or if the provider can't provide the value (triggers database fallback).
|
||||
*
|
||||
* @since 6.18.0
|
||||
*
|
||||
* @param string $key The key to retrieve.
|
||||
* @return mixed The value from external storage, or null for database fallback.
|
||||
*/
|
||||
public static function get_value( $key ) {
|
||||
if ( ! self::$init_fired ) {
|
||||
self::$init_fired = true;
|
||||
|
||||
/**
|
||||
* Fires before the first external storage read.
|
||||
*
|
||||
* Use this hook to register your storage provider via
|
||||
* External_Storage::register_provider(). This fires after the connection
|
||||
* package classes are loaded but before any connection status checks read
|
||||
* from external storage.
|
||||
*
|
||||
* Useful for mu-plugins that load before the plugin providing External_Storage,
|
||||
* since add_action() does not require the action or any classes to exist at
|
||||
* hook-registration time.
|
||||
*
|
||||
* @since 8.3.0
|
||||
*/
|
||||
do_action( 'jetpack_external_storage_init' );
|
||||
}
|
||||
|
||||
$provider = self::$provider;
|
||||
|
||||
// Check if we have a registered provider
|
||||
if ( null === $provider ) {
|
||||
return null; // No provider registered, use database
|
||||
}
|
||||
|
||||
$environment = $provider->get_environment_id();
|
||||
|
||||
// Check if provider is available in current environment
|
||||
if ( ! $provider->is_available() ) {
|
||||
self::log_event( 'unavailable', $key, 'External storage not available', $environment );
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if provider should handle this option
|
||||
if ( ! $provider->should_handle( $key ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Try to get value from the provider
|
||||
try {
|
||||
$value = $provider->get( $key );
|
||||
|
||||
// Check if we got a valid value
|
||||
if ( null !== $value && false !== $value && '' !== $value && 0 !== $value ) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
// Empty value - log it
|
||||
self::log_event( 'empty', $key, '', $environment );
|
||||
|
||||
} catch ( \Exception $e ) {
|
||||
// Provider threw an exception
|
||||
self::log_event( 'error', $key, $e->getMessage(), $environment );
|
||||
}
|
||||
|
||||
// Provider couldn't provide value, return null for database fallback
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log events if WP_DEBUG is enabled and delegate to provider for error reporting.
|
||||
* Includes rate limiting to prevent log spam from noisy events.
|
||||
*
|
||||
* Storage providers can optionally implement handle_error_event() method to receive
|
||||
* notifications about storage errors and empty states for their own error reporting.
|
||||
*
|
||||
* @since 6.18.0
|
||||
*
|
||||
* @param string $event_type The event type (error, empty, unavailable).
|
||||
* @param string $key The key that triggered the event.
|
||||
* @param string $details Additional details about the event.
|
||||
* @param string $environment The environment identifier (atomic, vip, etc.).
|
||||
*/
|
||||
public static function log_event( $event_type, $key, $details = '', $environment = 'unknown' ) {
|
||||
// Only process 'error' and 'empty' events for provider error reporting
|
||||
if ( 'error' !== $event_type && 'empty' !== $event_type ) {
|
||||
// For non-reportable events, just do debug logging with rate limiting
|
||||
if ( self::should_log_event( $key, $event_type ) && defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||
error_log( // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
|
||||
sprintf(
|
||||
'Jetpack External Storage %s: %s in %s%s',
|
||||
$event_type,
|
||||
$key,
|
||||
$environment,
|
||||
$details ? ' - ' . $details : ''
|
||||
)
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// For 'empty' events, check delay mechanism first to avoid false positives
|
||||
// during sync between external storage and the database.
|
||||
// This is checked BEFORE rate limiting so we don't block legitimate reports.
|
||||
if ( 'empty' === $event_type && ! self::should_report_empty_state( $key ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Apply rate limiting only for events that will trigger provider notification
|
||||
if ( ! self::should_log_event( $key, $event_type ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Local debug logging (only when WP_DEBUG is enabled)
|
||||
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||
error_log( // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
|
||||
sprintf(
|
||||
'Jetpack External Storage %s: %s in %s%s',
|
||||
$event_type,
|
||||
$key,
|
||||
$environment,
|
||||
$details ? ' - ' . $details : ''
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Delegate to provider if it implements error handling
|
||||
if ( null !== self::$provider && method_exists( self::$provider, 'handle_error_event' ) ) {
|
||||
// @phan-suppress-next-line PhanUndeclaredMethod -- Optional method, checked via method_exists()
|
||||
self::$provider->handle_error_event( $event_type, $key, $details, $environment );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if we should report an empty state based on delay mechanism.
|
||||
*
|
||||
* This prevents false positives during storage sync delays. On first encounter
|
||||
* of empty state, sets a transient. On subsequent encounters after the delay
|
||||
* threshold, allows reporting (indicating likely disconnection, not sync delay).
|
||||
*
|
||||
* Providers can customize the delay threshold by implementing get_empty_state_delay_threshold().
|
||||
*
|
||||
* @since 6.18.0
|
||||
*
|
||||
* @param string $key The key that was empty.
|
||||
* @return bool True if we should report this empty state, false otherwise.
|
||||
*/
|
||||
private static function should_report_empty_state( $key ) {
|
||||
$delay_key = 'jetpack_external_storage_empty_delay_' . $key;
|
||||
$first_empty_time = get_transient( $delay_key );
|
||||
|
||||
if ( false === $first_empty_time ) {
|
||||
// First time encountering empty state - set delay transient and don't report yet
|
||||
set_transient( $delay_key, time(), self::EMPTY_STATE_TRANSIENT_EXPIRY );
|
||||
return false;
|
||||
}
|
||||
|
||||
// Default delay threshold (5 minutes)
|
||||
$delay_threshold = 5 * MINUTE_IN_SECONDS;
|
||||
|
||||
// Allow provider to customize delay threshold
|
||||
// A threshold of 0 is valid for providers where external storage is written first
|
||||
if ( null !== self::$provider && method_exists( self::$provider, 'get_empty_state_delay_threshold' ) ) {
|
||||
// @phan-suppress-next-line PhanUndeclaredMethod -- Optional method, checked via method_exists()
|
||||
$custom_threshold = self::$provider->get_empty_state_delay_threshold();
|
||||
if ( is_int( $custom_threshold ) && $custom_threshold >= 0 && $custom_threshold <= self::EMPTY_STATE_TRANSIENT_EXPIRY ) {
|
||||
$delay_threshold = $custom_threshold;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ( time() - $first_empty_time ) >= $delay_threshold ) {
|
||||
// Delay threshold passed - likely disconnection, report it
|
||||
delete_transient( $delay_key );
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if an event should be logged based on rate limiting rules.
|
||||
*
|
||||
* This prevents log spam from noisy events by applying a simple one-hour
|
||||
* rate limit per key and event type combination. Also uses a static cache
|
||||
* to prevent duplicate logs within the same request.
|
||||
*
|
||||
* @since 6.18.0
|
||||
*
|
||||
* @param string $key The key that triggered the event.
|
||||
* @param string $event_type The event type (error, empty, unavailable).
|
||||
* @return bool True if the event should be logged, false if rate limited.
|
||||
*/
|
||||
private static function should_log_event( $key, $event_type = '' ) {
|
||||
// Combine event type and key for unique tracking
|
||||
$event_cache_key = $event_type . '_' . $key;
|
||||
|
||||
// Check static cache first (prevents multiple logs in same request)
|
||||
if ( isset( self::$logged_events[ $event_cache_key ] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$rate_limit_key = 'jetpack_ext_storage_rate_limit_' . $event_cache_key;
|
||||
|
||||
// Check if we're still within the rate limit period
|
||||
if ( get_transient( $rate_limit_key ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Mark as logged in both caches
|
||||
self::$logged_events[ $event_cache_key ] = true;
|
||||
set_transient( $rate_limit_key, true, HOUR_IN_SECONDS );
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,310 @@
|
||||
<?php
|
||||
/**
|
||||
* Jetpack Heartbeat package.
|
||||
*
|
||||
* @package automattic/jetpack-connection
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack;
|
||||
|
||||
use Automattic\Jetpack\Connection\Rest_Authentication;
|
||||
use Automattic\Jetpack\Connection\REST_Connector;
|
||||
use Jetpack_Options;
|
||||
use WP_CLI;
|
||||
use WP_Error;
|
||||
use WP_REST_Request;
|
||||
use WP_REST_Server;
|
||||
|
||||
/**
|
||||
* Heartbeat sends a batch of stats to wp.com once a day
|
||||
*/
|
||||
class Heartbeat {
|
||||
|
||||
/**
|
||||
* Holds the singleton instance of this class
|
||||
*
|
||||
* @since 1.0.0
|
||||
* @since-jetpack 2.3.3
|
||||
* @var Heartbeat
|
||||
*/
|
||||
private static $instance = false;
|
||||
|
||||
/**
|
||||
* Cronjob identifier
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $cron_name = 'jetpack_v2_heartbeat';
|
||||
|
||||
/**
|
||||
* Singleton
|
||||
*
|
||||
* @since 1.0.0
|
||||
* @since-jetpack 2.3.3
|
||||
* @static
|
||||
* @return Heartbeat
|
||||
*/
|
||||
public static function init() {
|
||||
if ( ! self::$instance ) {
|
||||
self::$instance = new Heartbeat();
|
||||
}
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for singleton
|
||||
*
|
||||
* @since 1.0.0
|
||||
* @since-jetpack 2.3.3
|
||||
*/
|
||||
private function __construct() {
|
||||
|
||||
// Schedule the task.
|
||||
add_action( $this->cron_name, array( $this, 'cron_exec' ) );
|
||||
|
||||
if ( ! wp_next_scheduled( $this->cron_name ) ) {
|
||||
// Deal with the old pre-3.0 weekly one.
|
||||
$timestamp = wp_next_scheduled( 'jetpack_heartbeat' );
|
||||
if ( $timestamp ) {
|
||||
wp_unschedule_event( $timestamp, 'jetpack_heartbeat' );
|
||||
}
|
||||
|
||||
wp_schedule_event( time(), 'daily', $this->cron_name );
|
||||
}
|
||||
|
||||
add_filter( 'jetpack_xmlrpc_unauthenticated_methods', array( __CLASS__, 'jetpack_xmlrpc_methods' ) );
|
||||
|
||||
if ( defined( 'WP_CLI' ) && WP_CLI ) {
|
||||
WP_CLI::add_command( 'jetpack-heartbeat', array( $this, 'cli_callback' ) );
|
||||
}
|
||||
|
||||
add_action( 'rest_api_init', array( $this, 'initialize_rest_api' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that gets executed on the wp-cron call
|
||||
*
|
||||
* @since 1.0.0
|
||||
* @since-jetpack 2.3.3
|
||||
* @global string $wp_version
|
||||
*/
|
||||
public function cron_exec() {
|
||||
|
||||
$a8c_mc_stats = new A8c_Mc_Stats();
|
||||
|
||||
/*
|
||||
* This should run daily. Figuring in for variances in
|
||||
* WP_CRON, don't let it run more than every 23 hours at most.
|
||||
*
|
||||
* i.e. if it ran less than 23 hours ago, fail out.
|
||||
*/
|
||||
$last = (int) Jetpack_Options::get_option( 'last_heartbeat' );
|
||||
if ( $last && ( $last + DAY_IN_SECONDS - HOUR_IN_SECONDS > time() ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* Check for an identity crisis
|
||||
*
|
||||
* If one exists:
|
||||
* - Bump stat for ID crisis
|
||||
* - Email site admin about potential ID crisis
|
||||
*/
|
||||
|
||||
// Coming Soon!
|
||||
|
||||
foreach ( self::generate_stats_array( 'v2-' ) as $key => $value ) {
|
||||
if ( is_array( $value ) ) {
|
||||
foreach ( $value as $v ) {
|
||||
$a8c_mc_stats->add( $key, (string) $v );
|
||||
}
|
||||
} else {
|
||||
$a8c_mc_stats->add( $key, (string) $value );
|
||||
}
|
||||
}
|
||||
|
||||
Jetpack_Options::update_option( 'last_heartbeat', time() );
|
||||
|
||||
$a8c_mc_stats->do_server_side_stats();
|
||||
|
||||
/**
|
||||
* Fires when we synchronize all registered options on heartbeat.
|
||||
*
|
||||
* @since 3.3.0
|
||||
*/
|
||||
do_action( 'jetpack_heartbeat' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates heartbeat stats data.
|
||||
*
|
||||
* @param string $prefix Prefix to add before stats identifier.
|
||||
*
|
||||
* @return array The stats array.
|
||||
*/
|
||||
public static function generate_stats_array( $prefix = '' ) {
|
||||
|
||||
/**
|
||||
* This filter is used to build the array of stats that are bumped once a day by Jetpack Heartbeat.
|
||||
*
|
||||
* Filter the array and add key => value pairs where
|
||||
* * key is the stat group name
|
||||
* * value is the stat name.
|
||||
*
|
||||
* Example:
|
||||
* add_filter( 'jetpack_heartbeat_stats_array', function( $stats ) {
|
||||
* $stats['is-https'] = is_ssl() ? 'https' : 'http';
|
||||
* });
|
||||
*
|
||||
* This will bump the stats for the 'is-https/https' or 'is-https/http' stat.
|
||||
*
|
||||
* @param array $stats The stats to be filtered.
|
||||
* @param string $prefix The prefix that will automatically be added at the begining at each stat group name.
|
||||
*/
|
||||
$stats = apply_filters( 'jetpack_heartbeat_stats_array', array(), $prefix );
|
||||
$return = array();
|
||||
|
||||
// Apply prefix to stats.
|
||||
foreach ( $stats as $stat => $value ) {
|
||||
$return[ "$prefix$stat" ] = $value;
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers jetpack.getHeartbeatData xmlrpc method
|
||||
*
|
||||
* @param array $methods The list of methods to be filtered.
|
||||
* @return array $methods
|
||||
*/
|
||||
public static function jetpack_xmlrpc_methods( $methods ) {
|
||||
$methods['jetpack.getHeartbeatData'] = array( __CLASS__, 'xmlrpc_data_response' );
|
||||
return $methods;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the response for the jetpack.getHeartbeatData xmlrpc method
|
||||
*
|
||||
* @param array $params The parameters received in the request.
|
||||
* @return array $params all the stats that heartbeat handles.
|
||||
*/
|
||||
public static function xmlrpc_data_response( $params = array() ) {
|
||||
// The WordPress XML-RPC server sets a default param of array()
|
||||
// if no argument is passed on the request and the method handlers get this array in $params.
|
||||
// generate_stats_array() needs a string as first argument.
|
||||
$params = empty( $params ) ? '' : $params;
|
||||
return self::generate_stats_array( $params );
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear scheduled events
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function deactivate() {
|
||||
// Deal with the old pre-3.0 weekly one.
|
||||
$timestamp = wp_next_scheduled( 'jetpack_heartbeat' );
|
||||
if ( $timestamp ) {
|
||||
wp_unschedule_event( $timestamp, 'jetpack_heartbeat' );
|
||||
}
|
||||
|
||||
$timestamp = wp_next_scheduled( $this->cron_name );
|
||||
wp_unschedule_event( $timestamp, $this->cron_name );
|
||||
}
|
||||
|
||||
/**
|
||||
* Interact with the Heartbeat
|
||||
*
|
||||
* ## OPTIONS
|
||||
*
|
||||
* inspect (default): Gets the list of data that is going to be sent in the heartbeat and the date/time of the last heartbeat
|
||||
*
|
||||
* @param array $args Arguments passed via CLI.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function cli_callback( $args ) {
|
||||
|
||||
$allowed_args = array(
|
||||
'inspect',
|
||||
);
|
||||
|
||||
if ( isset( $args[0] ) && ! in_array( $args[0], $allowed_args, true ) ) {
|
||||
/* translators: %s is a command like "prompt" */
|
||||
WP_CLI::error( sprintf( __( '%s is not a valid command.', 'jetpack-connection' ), $args[0] ) );
|
||||
}
|
||||
|
||||
$stats = self::generate_stats_array();
|
||||
$formatted_stats = array();
|
||||
|
||||
foreach ( $stats as $stat_name => $bin ) {
|
||||
$formatted_stats[] = array(
|
||||
'Stat name' => $stat_name,
|
||||
'Bin' => $bin,
|
||||
);
|
||||
}
|
||||
|
||||
WP_CLI\Utils\format_items( 'table', $formatted_stats, array( 'Stat name', 'Bin' ) );
|
||||
|
||||
$last_heartbeat = Jetpack_Options::get_option( 'last_heartbeat' );
|
||||
|
||||
if ( $last_heartbeat ) {
|
||||
$last_date = gmdate( 'Y-m-d H:i:s', $last_heartbeat );
|
||||
/* translators: %s is the full datetime of the last heart beat e.g. 2020-01-01 12:21:23 */
|
||||
WP_CLI::line( sprintf( __( 'Last heartbeat sent at: %s', 'jetpack-connection' ), $last_date ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the heartbeat REST API.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function initialize_rest_api() {
|
||||
register_rest_route(
|
||||
'jetpack/v4',
|
||||
'/heartbeat/data',
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( $this, 'rest_heartbeat_data' ),
|
||||
'permission_callback' => array( $this, 'rest_heartbeat_data_permission_check' ),
|
||||
'args' => array(
|
||||
'prefix' => array(
|
||||
'description' => __( 'Prefix to add before the stats identifiers.', 'jetpack-connection' ),
|
||||
'type' => 'string',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Endpoint to retrieve the heartbeat data.
|
||||
*
|
||||
* @param WP_REST_Request $request The request data.
|
||||
*
|
||||
* @since 2.7.0
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function rest_heartbeat_data( WP_REST_Request $request ) {
|
||||
return static::generate_stats_array( $request->get_param( 'prefix' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check permissions for the `get_heartbeat_data` endpoint.
|
||||
*
|
||||
* @return true|WP_Error
|
||||
*/
|
||||
public function rest_heartbeat_data_permission_check() {
|
||||
if ( current_user_can( 'jetpack_connect' ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return Rest_Authentication::is_signed_with_blog_token()
|
||||
? true
|
||||
: new WP_Error( 'invalid_permission_heartbeat_data', REST_Connector::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
/**
|
||||
* The React initial state.
|
||||
*
|
||||
* @package automattic/jetpack-connection
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Connection;
|
||||
|
||||
use Automattic\Jetpack\Status;
|
||||
|
||||
/**
|
||||
* The React initial state.
|
||||
*/
|
||||
class Initial_State {
|
||||
|
||||
/**
|
||||
* Get the initial state data.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private static function get_data() {
|
||||
global $wp_version;
|
||||
|
||||
$status = new Status();
|
||||
|
||||
return array(
|
||||
'apiRoot' => esc_url_raw( rest_url() ),
|
||||
'apiNonce' => wp_create_nonce( 'wp_rest' ),
|
||||
'registrationNonce' => wp_create_nonce( 'jetpack-registration-nonce' ),
|
||||
'connectionStatus' => REST_Connector::connection_status( false ),
|
||||
'userConnectionData' => REST_Connector::get_user_connection_data( false ),
|
||||
'connectedPlugins' => REST_Connector::get_connection_plugins( false ),
|
||||
'wpVersion' => $wp_version,
|
||||
'siteSuffix' => $status->get_site_suffix(),
|
||||
'connectionErrors' => Error_Handler::get_instance()->get_displayable_errors(),
|
||||
'isOfflineMode' => $status->is_offline_mode(),
|
||||
'calypsoEnv' => ( new Status\Host() )->get_calypso_env(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the connection script data.
|
||||
*
|
||||
* @param array $data The script data.
|
||||
*/
|
||||
public static function set_connection_script_data( $data ) {
|
||||
|
||||
$data['connection'] = self::get_data();
|
||||
|
||||
if ( empty( $data['site']['wpcom']['blog_id'] ) ) {
|
||||
$data['site']['wpcom']['blog_id'] = absint( \Jetpack_Options::get_option( 'id', 0 ) );
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the initial state into a JavaScript variable.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function render() {
|
||||
/*
|
||||
* `window.jpTracksContext` is an intentionally minimal, Tracks-specific global used by the
|
||||
* @automattic/jetpack-analytics package to read `blog_id` at event-fire time. It exists
|
||||
* separately from `window.JetpackScriptData` because the analytics package is consumed in
|
||||
* contexts (e.g. Boost frontend) where `JetpackScriptData` is not reliably available, and
|
||||
* coupling the analytics package to that schema would widen its surface unnecessarily.
|
||||
* When both are present, `JetpackScriptData.site.wpcom.blog_id` is populated via
|
||||
* `set_connection_script_data()` above for other consumers.
|
||||
*/
|
||||
return 'var JP_CONNECTION_INITIAL_STATE; typeof JP_CONNECTION_INITIAL_STATE === "object" || (JP_CONNECTION_INITIAL_STATE = ' . wp_json_encode( self::get_data(), JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ) . ');'
|
||||
. sprintf( 'window.jpTracksContext = window.jpTracksContext || {}; window.jpTracksContext.blog_id = %s;', absint( \Jetpack_Options::get_option( 'id', 0 ) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the initial state using an inline script.
|
||||
*
|
||||
* @param string $handle The JS script handle.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function render_script( $handle ) {
|
||||
wp_add_inline_script( $handle, static::render(), 'before' );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
<?php
|
||||
/**
|
||||
* The nonce handler.
|
||||
*
|
||||
* @package automattic/jetpack-connection
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Connection;
|
||||
|
||||
/**
|
||||
* The nonce handler.
|
||||
*/
|
||||
class Nonce_Handler {
|
||||
|
||||
/**
|
||||
* How long the scheduled cleanup can run (in seconds).
|
||||
* Can be modified using the filter `jetpack_connection_nonce_scheduled_cleanup_limit`.
|
||||
*/
|
||||
const SCHEDULED_CLEANUP_TIME_LIMIT = 5;
|
||||
|
||||
/**
|
||||
* How many nonces should be removed per batch during the `clean_all()` run.
|
||||
*/
|
||||
const CLEAN_ALL_LIMIT_PER_BATCH = 1000;
|
||||
|
||||
/**
|
||||
* Nonce lifetime in seconds.
|
||||
*/
|
||||
const LIFETIME = HOUR_IN_SECONDS;
|
||||
|
||||
/**
|
||||
* The nonces used during the request are stored here to keep them valid.
|
||||
* The property is static to keep the nonces accessible between the `Nonce_Handler` instances.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static $nonces_used_this_request = array();
|
||||
|
||||
/**
|
||||
* The database object.
|
||||
*
|
||||
* @var \wpdb
|
||||
*/
|
||||
private $db;
|
||||
|
||||
/**
|
||||
* Initializing the object.
|
||||
*/
|
||||
public function __construct() {
|
||||
global $wpdb;
|
||||
|
||||
$this->db = $wpdb;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scheduling the WP-cron cleanup event.
|
||||
*/
|
||||
public function init_schedule() {
|
||||
add_action( 'jetpack_clean_nonces', array( __CLASS__, 'clean_scheduled' ) );
|
||||
if ( ! wp_next_scheduled( 'jetpack_clean_nonces' ) ) {
|
||||
wp_schedule_event( time(), 'hourly', 'jetpack_clean_nonces' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reschedule the WP-cron cleanup event to make it start sooner.
|
||||
*/
|
||||
public function reschedule() {
|
||||
wp_clear_scheduled_hook( 'jetpack_clean_nonces' );
|
||||
wp_schedule_event( time(), 'hourly', 'jetpack_clean_nonces' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a used nonce to a list of known nonces.
|
||||
*
|
||||
* @param int $timestamp the current request timestamp.
|
||||
* @param string $nonce the nonce value.
|
||||
*
|
||||
* @return bool whether the nonce is unique or not.
|
||||
*/
|
||||
public function add( $timestamp, $nonce ) {
|
||||
if ( isset( static::$nonces_used_this_request[ "$timestamp:$nonce" ] ) ) {
|
||||
return static::$nonces_used_this_request[ "$timestamp:$nonce" ];
|
||||
}
|
||||
|
||||
// This should always have gone through Jetpack_Signature::sign_request() first to check $timestamp and $nonce.
|
||||
$timestamp = (int) $timestamp;
|
||||
$nonce = esc_sql( $nonce );
|
||||
|
||||
// Raw query so we can avoid races: add_option will also update.
|
||||
$show_errors = $this->db->hide_errors();
|
||||
|
||||
$return = false;
|
||||
|
||||
// Running `try...finally` to make sure that we re-enable errors in case of an exception.
|
||||
try {
|
||||
$old_nonce = $this->db->get_row(
|
||||
$this->db->prepare( "SELECT 1 FROM `{$this->db->options}` WHERE option_name = %s", "jetpack_nonce_{$timestamp}_{$nonce}" )
|
||||
);
|
||||
|
||||
if ( $old_nonce === null ) {
|
||||
$return = (bool) $this->db->query(
|
||||
$this->db->prepare(
|
||||
"INSERT INTO `{$this->db->options}` (`option_name`, `option_value`, `autoload`) VALUES (%s, %s, %s)",
|
||||
"jetpack_nonce_{$timestamp}_{$nonce}",
|
||||
time(),
|
||||
'no'
|
||||
)
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
$this->db->show_errors( $show_errors );
|
||||
}
|
||||
|
||||
static::$nonces_used_this_request[ "$timestamp:$nonce" ] = $return;
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removing all existing nonces, or at least as many as possible.
|
||||
* Capped at 20 seconds to avoid breaking the site.
|
||||
*
|
||||
* @param int $cutoff_timestamp All nonces added before this timestamp will be removed.
|
||||
* @param int $time_limit How long the cleanup can run (in seconds).
|
||||
*
|
||||
* @return true
|
||||
*/
|
||||
public function clean_all( $cutoff_timestamp = PHP_INT_MAX, $time_limit = 20 ) {
|
||||
// phpcs:ignore Generic.CodeAnalysis.ForLoopWithTestFunctionCall.NotAllowed
|
||||
for ( $end_time = time() + $time_limit; time() < $end_time; ) {
|
||||
$result = $this->delete( static::CLEAN_ALL_LIMIT_PER_BATCH, $cutoff_timestamp );
|
||||
|
||||
if ( ! $result ) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scheduled clean up of the expired nonces.
|
||||
*/
|
||||
public static function clean_scheduled() {
|
||||
/**
|
||||
* Adjust the time limit for the scheduled cleanup.
|
||||
*
|
||||
* @since 9.5.0
|
||||
*
|
||||
* @param int $time_limit How long the cleanup can run (in seconds).
|
||||
*/
|
||||
$time_limit = apply_filters( 'jetpack_connection_nonce_cleanup_runtime_limit', static::SCHEDULED_CLEANUP_TIME_LIMIT );
|
||||
|
||||
( new static() )->clean_all( time() - static::LIFETIME, $time_limit );
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the nonces.
|
||||
*
|
||||
* @param int $limit How many nonces to delete.
|
||||
* @param null|int $cutoff_timestamp All nonces added before this timestamp will be removed.
|
||||
*
|
||||
* @return int|false Number of removed nonces, or `false` if nothing to remove (or in case of a database error).
|
||||
*/
|
||||
public function delete( $limit = 10, $cutoff_timestamp = null ) {
|
||||
global $wpdb;
|
||||
|
||||
$ids = $wpdb->get_col(
|
||||
$wpdb->prepare(
|
||||
"SELECT option_id FROM `{$wpdb->options}`"
|
||||
. " WHERE `option_name` >= 'jetpack_nonce_' AND `option_name` < %s"
|
||||
. ' LIMIT %d',
|
||||
'jetpack_nonce_' . $cutoff_timestamp,
|
||||
$limit
|
||||
)
|
||||
);
|
||||
|
||||
if ( ! is_array( $ids ) ) {
|
||||
// There's an error and we can't proceed.
|
||||
return false;
|
||||
}
|
||||
|
||||
// Removing zeroes in case AUTO_INCREMENT of the options table is broken, and all ID's are zeroes.
|
||||
$ids = array_filter( $ids );
|
||||
|
||||
if ( array() === $ids ) {
|
||||
// There's nothing to remove.
|
||||
return false;
|
||||
}
|
||||
|
||||
$ids_fill = implode( ', ', array_fill( 0, count( $ids ), '%d' ) );
|
||||
|
||||
$args = $ids;
|
||||
$args[] = 'jetpack_nonce_%';
|
||||
|
||||
// The Code Sniffer is unable to understand what's going on...
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
|
||||
return $wpdb->query( $wpdb->prepare( "DELETE FROM `{$wpdb->options}` WHERE `option_id` IN ( {$ids_fill} ) AND option_name LIKE %s", $args ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean the cached nonces valid during the current request, therefore making them invalid.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function invalidate_request_nonces() {
|
||||
static::$nonces_used_this_request = array();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
<?php
|
||||
/**
|
||||
* The Package_Version_Tracker class.
|
||||
*
|
||||
* @package automattic/jetpack-connection
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Connection;
|
||||
|
||||
use Jetpack_Options;
|
||||
|
||||
/**
|
||||
* The Package_Version_Tracker class.
|
||||
*/
|
||||
class Package_Version_Tracker {
|
||||
|
||||
const PACKAGE_VERSION_OPTION = 'jetpack_package_versions';
|
||||
|
||||
/**
|
||||
* The cache key for storing a failed request to update remote package versions.
|
||||
* The caching logic is that when a failed request occurs, we cache it temporarily
|
||||
* with a set expiration time.
|
||||
* Only after the key has expired, we'll be able to repeat a remote request.
|
||||
* This also implies that the cached value is redundant, however we chose the datetime
|
||||
* of the failed request to avoid using booleans.
|
||||
*/
|
||||
const CACHED_FAILED_REQUEST_KEY = 'jetpack_failed_update_remote_package_versions';
|
||||
|
||||
/**
|
||||
* The min time difference in seconds for attempting to
|
||||
* update remote tracked package versions after a failed remote request.
|
||||
*/
|
||||
const CACHED_FAILED_REQUEST_EXPIRATION = 1 * HOUR_IN_SECONDS;
|
||||
|
||||
/**
|
||||
* Transient key for rate limiting the package version requests;
|
||||
*/
|
||||
const RATE_LIMITER_KEY = 'jetpack_update_remote_package_last_query';
|
||||
|
||||
/**
|
||||
* Only allow one versions check (and request) per minute.
|
||||
*/
|
||||
const RATE_LIMITER_TIMEOUT = MINUTE_IN_SECONDS;
|
||||
|
||||
/**
|
||||
* Runs the package version check on the `shutdown` hook.
|
||||
*
|
||||
* Static so the callback can be unregistered, and so the tracker is only
|
||||
* constructed when the hook fires rather than during configuration.
|
||||
*/
|
||||
public static function update_on_shutdown() {
|
||||
( new self() )->maybe_update_package_versions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses the jetpack_package_versions filter to obtain the package versions from packages that need
|
||||
* version tracking. If the package versions have changed, updates the option and notifies WPCOM.
|
||||
*/
|
||||
public function maybe_update_package_versions() {
|
||||
// Do not run too early or all the modules may not be loaded.
|
||||
if ( ! did_action( 'init' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only attempt to update the option on POST requests.
|
||||
// This will prevent the option from being updated multiple times due to concurrent requests.
|
||||
if ( ! ( isset( $_SERVER['REQUEST_METHOD'] ) && 'POST' === $_SERVER['REQUEST_METHOD'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The version check is being rate limited.
|
||||
if ( $this->is_rate_limiting() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains the package versions.
|
||||
*
|
||||
* @since 1.30.2
|
||||
*
|
||||
* @param array An associative array of Jetpack package slugs and their corresponding versions as key/value pairs.
|
||||
*/
|
||||
$filter_versions = apply_filters( 'jetpack_package_versions', array() );
|
||||
|
||||
if ( ! is_array( $filter_versions ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$option_versions = get_option( self::PACKAGE_VERSION_OPTION, array() );
|
||||
|
||||
foreach ( $filter_versions as $package => $version ) {
|
||||
if ( ! is_string( $package ) || ! is_string( $version ) ) {
|
||||
unset( $filter_versions[ $package ] );
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! is_array( $option_versions )
|
||||
|| count( array_diff_assoc( $filter_versions, $option_versions ) )
|
||||
|| count( array_diff_assoc( $option_versions, $filter_versions ) )
|
||||
) {
|
||||
$this->update_package_versions_option( $filter_versions );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the package versions option.
|
||||
*
|
||||
* @param array $package_versions The package versions.
|
||||
*/
|
||||
protected function update_package_versions_option( $package_versions ) {
|
||||
if ( ! $this->is_sync_enabled() ) {
|
||||
$this->update_package_versions_via_remote_request( $package_versions );
|
||||
// Remove the checksum for package versions, so it gets recalculated when sync gets activated.
|
||||
$jetpack_callables_sync_checksum = Jetpack_Options::get_raw_option( 'jetpack_callables_sync_checksum' );
|
||||
if ( isset( $jetpack_callables_sync_checksum['jetpack_package_versions'] ) ) {
|
||||
unset( $jetpack_callables_sync_checksum['jetpack_package_versions'] );
|
||||
Jetpack_Options::update_raw_option( 'jetpack_callables_sync_checksum', $jetpack_callables_sync_checksum );
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
update_option( self::PACKAGE_VERSION_OPTION, $package_versions );
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether Jetpack Sync is enabled.
|
||||
*
|
||||
* @return boolean true if Sync is present and enabled, false otherwise
|
||||
*/
|
||||
protected function is_sync_enabled() {
|
||||
if ( class_exists( 'Automattic\Jetpack\Sync\Settings' ) && \Automattic\Jetpack\Sync\Settings::is_sync_enabled() ) {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback for updating the package versions via a remote request when Sync is not present.
|
||||
*
|
||||
* Updates the package versions as follows:
|
||||
* - Sends the updated package versions to wpcom.
|
||||
* - Updates the 'jetpack_package_versions' option.
|
||||
*
|
||||
* @param array $package_versions The package versions.
|
||||
*/
|
||||
protected function update_package_versions_via_remote_request( $package_versions ) {
|
||||
$connection = new Manager();
|
||||
if ( ! $connection->is_connected() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$site_id = \Jetpack_Options::get_option( 'id' );
|
||||
|
||||
$last_failed_attempt_within_hour = get_transient( self::CACHED_FAILED_REQUEST_KEY );
|
||||
|
||||
if ( $last_failed_attempt_within_hour ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$body = wp_json_encode(
|
||||
array(
|
||||
'package_versions' => $package_versions,
|
||||
),
|
||||
JSON_UNESCAPED_SLASHES
|
||||
);
|
||||
|
||||
$response = Client::wpcom_json_api_request_as_blog(
|
||||
sprintf( '/sites/%d/jetpack-package-versions', $site_id ),
|
||||
'2',
|
||||
array(
|
||||
'headers' => array( 'content-type' => 'application/json' ),
|
||||
'method' => 'POST',
|
||||
),
|
||||
$body,
|
||||
'wpcom'
|
||||
);
|
||||
|
||||
if ( 200 === wp_remote_retrieve_response_code( $response ) ) {
|
||||
update_option( self::PACKAGE_VERSION_OPTION, $package_versions );
|
||||
} else {
|
||||
set_transient( self::CACHED_FAILED_REQUEST_KEY, time(), self::CACHED_FAILED_REQUEST_EXPIRATION );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if version check is being rate limited, and update the rate limiting transient if needed.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function is_rate_limiting() {
|
||||
if ( get_transient( static::RATE_LIMITER_KEY ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
set_transient( static::RATE_LIMITER_KEY, time(), static::RATE_LIMITER_TIMEOUT );
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
/**
|
||||
* The Package_Version class.
|
||||
*
|
||||
* @package automattic/jetpack-connection
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Connection;
|
||||
|
||||
/**
|
||||
* The Package_Version class.
|
||||
*/
|
||||
class Package_Version {
|
||||
|
||||
const PACKAGE_VERSION = '8.7.5';
|
||||
|
||||
const PACKAGE_SLUG = 'connection';
|
||||
|
||||
/**
|
||||
* Adds the package slug and version to the package version tracker's data.
|
||||
*
|
||||
* @param array $package_versions The package version array.
|
||||
*
|
||||
* @return array The packge version array.
|
||||
*/
|
||||
public static function send_package_version_to_tracker( $package_versions ) {
|
||||
$package_versions[ self::PACKAGE_SLUG ] = self::PACKAGE_VERSION;
|
||||
return $package_versions;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,466 @@
|
||||
<?php
|
||||
/**
|
||||
* Class for the Jetpack partner coupon logic.
|
||||
*
|
||||
* @package automattic/jetpack-connection
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack;
|
||||
|
||||
use Automattic\Jetpack\Connection\Client as Connection_Client;
|
||||
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
|
||||
use Jetpack_Options;
|
||||
|
||||
/**
|
||||
* Disable direct access.
|
||||
*/
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit( 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Class Jetpack_Partner_Coupon
|
||||
*
|
||||
* @since partner-1.6.0
|
||||
* @since 2.0.0
|
||||
*/
|
||||
class Partner_Coupon {
|
||||
|
||||
/**
|
||||
* Name of the Jetpack_Option coupon option.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public static $coupon_option = 'partner_coupon';
|
||||
|
||||
/**
|
||||
* Name of the Jetpack_Option added option.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public static $added_option = 'partner_coupon_added';
|
||||
|
||||
/**
|
||||
* Name of "last availability check" transient.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public static $last_check_transient = 'jetpack_partner_coupon_last_check';
|
||||
|
||||
/**
|
||||
* Callable that executes a blog-authenticated request.
|
||||
*
|
||||
* @var callable
|
||||
*/
|
||||
protected $request_as_blog;
|
||||
|
||||
/**
|
||||
* Jetpack_Partner_Coupon
|
||||
*
|
||||
* @var Partner_Coupon|null
|
||||
**/
|
||||
private static $instance = null;
|
||||
|
||||
/**
|
||||
* A list of supported partners.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static $supported_partners = array(
|
||||
'IONOS' => array(
|
||||
'name' => 'IONOS',
|
||||
'logo' => array(
|
||||
'src' => '/images/ionos-logo.jpg',
|
||||
'width' => 119,
|
||||
'height' => 32,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
* A list of supported presets.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static $supported_presets = array(
|
||||
'IONA' => 'jetpack_backup_daily',
|
||||
);
|
||||
|
||||
/**
|
||||
* Get singleton instance of class.
|
||||
*
|
||||
* @return Partner_Coupon
|
||||
*/
|
||||
public static function get_instance() {
|
||||
if ( self::$instance === null ) {
|
||||
self::$instance = new Partner_Coupon( array( Connection_Client::class, 'wpcom_json_api_request_as_blog' ) );
|
||||
}
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param callable $request_as_blog Callable that executes a blog-authenticated request.
|
||||
*/
|
||||
public function __construct( $request_as_blog ) {
|
||||
$this->request_as_blog = $request_as_blog;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register hooks to catch and purge coupon.
|
||||
*
|
||||
* @param string $plugin_slug The plugin slug to differentiate between Jetpack connections.
|
||||
* @param string $redirect_location The location we should redirect to after catching the coupon.
|
||||
*/
|
||||
public static function register_coupon_admin_hooks( $plugin_slug, $redirect_location ) {
|
||||
$instance = self::get_instance();
|
||||
|
||||
// We have to use an anonymous function, so we can pass along relevant information
|
||||
// and not have to hardcode values for a single plugin.
|
||||
// This open up the opportunity for e.g. the "all-in-one" and backup plugins
|
||||
// to both implement partner coupon logic.
|
||||
add_action(
|
||||
'admin_init',
|
||||
function () use ( $plugin_slug, $redirect_location, $instance ) {
|
||||
$instance->catch_coupon( $plugin_slug, $redirect_location );
|
||||
$instance->maybe_purge_coupon( $plugin_slug );
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Catch partner coupon and redirect to claim component.
|
||||
*
|
||||
* @param string $plugin_slug The plugin slug to differentiate between Jetpack connections.
|
||||
* @param string $redirect_location The location we should redirect to after catching the coupon.
|
||||
*/
|
||||
public function catch_coupon( $plugin_slug, $redirect_location ) {
|
||||
// Accept and store a partner coupon if present, and redirect to Jetpack connection screen.
|
||||
$partner_coupon = isset( $_GET['jetpack-partner-coupon'] ) ? sanitize_text_field( wp_unslash( $_GET['jetpack-partner-coupon'] ) ) : false; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
if ( $partner_coupon ) {
|
||||
Jetpack_Options::update_options(
|
||||
array(
|
||||
self::$coupon_option => $partner_coupon,
|
||||
self::$added_option => time(),
|
||||
)
|
||||
);
|
||||
|
||||
$connection = new Connection_Manager( $plugin_slug );
|
||||
if ( $connection->is_connected() ) {
|
||||
$redirect_location = add_query_arg( array( 'showCouponRedemption' => 1 ), $redirect_location );
|
||||
wp_safe_redirect( $redirect_location );
|
||||
} else {
|
||||
wp_safe_redirect( $redirect_location );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Purge partner coupon.
|
||||
*
|
||||
* We try to remotely check if a coupon looks valid. We also automatically purge
|
||||
* partner coupons after a certain amount of time to prevent unnecessary look-ups
|
||||
* and/or promoting a product for months or years in the future due to unknown
|
||||
* errors.
|
||||
*
|
||||
* @param string $plugin_slug The plugin slug to differentiate between Jetpack connections.
|
||||
*/
|
||||
public function maybe_purge_coupon( $plugin_slug ) {
|
||||
// Only run coupon checks on Jetpack admin pages.
|
||||
// The "admin-ui" package is responsible for registering the Jetpack admin
|
||||
// page for all Jetpack plugins and has hardcoded the settings page to be
|
||||
// "jetpack", so we shouldn't need to allow for dynamic/custom values.
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
if ( ! isset( $_GET['page'] ) || 'jetpack' !== $_GET['page'] ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ( new Status() )->is_offline_mode() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$connection = new Connection_Manager( $plugin_slug );
|
||||
if ( ! $connection->is_connected() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( $this->maybe_purge_coupon_by_added_date() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Limit checks to happen once a minute at most.
|
||||
if ( get_transient( self::$last_check_transient ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
set_transient( self::$last_check_transient, true, MINUTE_IN_SECONDS );
|
||||
|
||||
$this->maybe_purge_coupon_by_availability_check();
|
||||
}
|
||||
|
||||
/**
|
||||
* Purge coupon based on local added date.
|
||||
*
|
||||
* We automatically remove the coupon after a month to "self-heal" if
|
||||
* something in the claim process has broken with the site.
|
||||
*
|
||||
* @return bool Return whether we should skip further purge checks.
|
||||
*/
|
||||
protected function maybe_purge_coupon_by_added_date() {
|
||||
$date = Jetpack_Options::get_option( self::$added_option, '' );
|
||||
|
||||
if ( empty( $date ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$expire_date = strtotime( '+30 days', $date );
|
||||
$today = time();
|
||||
|
||||
if ( $today >= $expire_date ) {
|
||||
$this->delete_coupon_data();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Purge coupon based on availability check.
|
||||
*
|
||||
* @return bool Return whether we deleted coupon data.
|
||||
*/
|
||||
protected function maybe_purge_coupon_by_availability_check() {
|
||||
$blog_id = Jetpack_Options::get_option( 'id', false );
|
||||
|
||||
if ( ! $blog_id ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$coupon = self::get_coupon();
|
||||
|
||||
if ( ! $coupon ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$response = call_user_func_array(
|
||||
$this->request_as_blog,
|
||||
array(
|
||||
add_query_arg(
|
||||
array( 'coupon_code' => $coupon['coupon_code'] ),
|
||||
sprintf(
|
||||
'/sites/%d/jetpack-partner/coupon/v1/site/coupon',
|
||||
$blog_id
|
||||
)
|
||||
),
|
||||
2,
|
||||
array( 'method' => 'GET' ),
|
||||
null,
|
||||
'wpcom',
|
||||
)
|
||||
);
|
||||
|
||||
$body = json_decode( wp_remote_retrieve_body( $response ), true );
|
||||
|
||||
if (
|
||||
200 === wp_remote_retrieve_response_code( $response ) &&
|
||||
is_array( $body ) &&
|
||||
isset( $body['available'] ) &&
|
||||
false === $body['available']
|
||||
) {
|
||||
$this->delete_coupon_data();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all coupon data.
|
||||
*/
|
||||
protected function delete_coupon_data() {
|
||||
Jetpack_Options::delete_option(
|
||||
array(
|
||||
self::$coupon_option,
|
||||
self::$added_option,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get partner coupon data.
|
||||
*
|
||||
* @return array|bool
|
||||
*/
|
||||
public static function get_coupon() {
|
||||
$coupon_code = Jetpack_Options::get_option( self::$coupon_option, '' );
|
||||
|
||||
if ( ! is_string( $coupon_code ) || empty( $coupon_code ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$instance = self::get_instance();
|
||||
$partner = $instance->get_coupon_partner( $coupon_code );
|
||||
|
||||
if ( ! $partner ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$preset = $instance->get_coupon_preset( $coupon_code );
|
||||
|
||||
if ( ! $preset ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$product = $instance->get_coupon_product( $preset );
|
||||
|
||||
if ( ! $product ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return array(
|
||||
'coupon_code' => $coupon_code,
|
||||
'partner' => $partner,
|
||||
'preset' => $preset,
|
||||
'product' => $product,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get coupon partner.
|
||||
*
|
||||
* @param string $coupon_code Coupon code to go through.
|
||||
* @return array|bool
|
||||
*/
|
||||
private function get_coupon_partner( $coupon_code ) {
|
||||
if ( ! is_string( $coupon_code ) || false === strpos( $coupon_code, '_' ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$prefix = strtok( $coupon_code, '_' );
|
||||
$supported_partners = $this->get_supported_partners();
|
||||
|
||||
if ( ! isset( $supported_partners[ $prefix ] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return array(
|
||||
'name' => $supported_partners[ $prefix ]['name'],
|
||||
'prefix' => $prefix,
|
||||
'logo' => $supported_partners[ $prefix ]['logo'] ?? null,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get coupon product.
|
||||
*
|
||||
* @param string $coupon_preset The preset we wish to find a product for.
|
||||
* @return array|bool
|
||||
*/
|
||||
private function get_coupon_product( $coupon_preset ) {
|
||||
if ( ! is_string( $coupon_preset ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow for plugins to register supported products.
|
||||
*
|
||||
* @since 1.6.0
|
||||
*
|
||||
* @param array A list of product details.
|
||||
* @return array
|
||||
*/
|
||||
$product_details = apply_filters( 'jetpack_partner_coupon_products', array() );
|
||||
$product_slug = $this->get_supported_presets()[ $coupon_preset ];
|
||||
|
||||
foreach ( $product_details as $product ) {
|
||||
if ( ! $this->array_keys_exist( array( 'title', 'slug', 'description', 'features' ), $product ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( $product_slug === $product['slug'] ) {
|
||||
return $product;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if multiple keys are present in an array.
|
||||
*
|
||||
* @param array $needles The keys we wish to check for.
|
||||
* @param array $haystack The array we want to compare keys against.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function array_keys_exist( $needles, $haystack ) {
|
||||
foreach ( $needles as $needle ) {
|
||||
if ( ! isset( $haystack[ $needle ] ) ) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get coupon preset.
|
||||
*
|
||||
* @param string $coupon_code Coupon code to go through.
|
||||
* @return string|bool
|
||||
*/
|
||||
private function get_coupon_preset( $coupon_code ) {
|
||||
if ( ! is_string( $coupon_code ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$regex = '/^.*?_(?P<slug>.*?)_.+$/';
|
||||
$matches = array();
|
||||
|
||||
if ( ! preg_match( $regex, $coupon_code, $matches ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return isset( $this->get_supported_presets()[ $matches['slug'] ] ) ? $matches['slug'] : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get supported partners.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function get_supported_partners() {
|
||||
/**
|
||||
* Allow external code to add additional supported partners.
|
||||
*
|
||||
* @since partner-1.6.0
|
||||
* @since 2.0.0
|
||||
*
|
||||
* @param array $supported_partners A list of supported partners.
|
||||
* @return array
|
||||
*/
|
||||
return apply_filters( 'jetpack_partner_coupon_supported_partners', self::$supported_partners );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get supported presets.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function get_supported_presets() {
|
||||
/**
|
||||
* Allow external code to add additional supported presets.
|
||||
*
|
||||
* @since partner-1.6.0
|
||||
* @since 2.0.0
|
||||
*
|
||||
* @param array $supported_presets A list of supported presets.
|
||||
* @return array
|
||||
*/
|
||||
return apply_filters( 'jetpack_partner_coupon_supported_presets', self::$supported_presets );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
<?php
|
||||
/**
|
||||
* Jetpack Partner utilities.
|
||||
*
|
||||
* @package automattic/jetpack-connection
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack;
|
||||
|
||||
/**
|
||||
* This class introduces functionality used by Jetpack hosting partners.
|
||||
*
|
||||
* @since partner-1.0.0
|
||||
* @since 2.0.0
|
||||
*/
|
||||
class Partner {
|
||||
|
||||
/**
|
||||
* Affiliate code.
|
||||
*/
|
||||
const AFFILIATE_CODE = 'affiliate';
|
||||
|
||||
/**
|
||||
* Subsidiary id code.
|
||||
*/
|
||||
const SUBSIDIARY_CODE = 'subsidiary';
|
||||
|
||||
/**
|
||||
* Singleton instance.
|
||||
*
|
||||
* @since partner-1.0.0
|
||||
* @since 2.0.0
|
||||
*
|
||||
* @var Partner This class instance.
|
||||
*/
|
||||
private static $instance = null;
|
||||
|
||||
/**
|
||||
* Partner constructor.
|
||||
*/
|
||||
private function __construct() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the class or returns the singleton.
|
||||
*
|
||||
* @since partner-1.0.0
|
||||
* @since 2.0.0
|
||||
*
|
||||
* @return Partner | false
|
||||
*/
|
||||
public static function init() {
|
||||
if ( self::$instance === null ) {
|
||||
self::$instance = new Partner();
|
||||
add_filter( 'jetpack_build_authorize_url', array( self::$instance, 'add_subsidiary_id_as_query_arg' ) );
|
||||
add_filter( 'jetpack_build_authorize_url', array( self::$instance, 'add_affiliate_code_as_query_arg' ) );
|
||||
add_filter( 'jetpack_build_connection_url', array( self::$instance, 'add_subsidiary_id_as_query_arg' ) );
|
||||
add_filter( 'jetpack_build_connection_url', array( self::$instance, 'add_affiliate_code_as_query_arg' ) );
|
||||
|
||||
add_filter( 'jetpack_register_request_body', array( self::$instance, 'add_subsidiary_id_to_params_array' ) );
|
||||
add_filter( 'jetpack_register_request_body', array( self::$instance, 'add_affiliate_code_to_params_array' ) );
|
||||
}
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the partner subsidiary code to the passed URL.
|
||||
*
|
||||
* @param string $url The URL.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function add_subsidiary_id_as_query_arg( $url ) {
|
||||
return $this->add_code_as_query_arg( self::SUBSIDIARY_CODE, $url );
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the affiliate code to the passed URL.
|
||||
*
|
||||
* @param string $url The URL.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function add_affiliate_code_as_query_arg( $url ) {
|
||||
return $this->add_code_as_query_arg( self::AFFILIATE_CODE, $url );
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the partner subsidiary code to the passed array.
|
||||
*
|
||||
* @since partner-1.5.0
|
||||
* @since 2.0.0
|
||||
*
|
||||
* @param array $params The parameters array.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function add_subsidiary_id_to_params_array( $params ) {
|
||||
if ( ! is_array( $params ) ) {
|
||||
return $params;
|
||||
}
|
||||
return array_merge( $params, $this->get_code_as_array( self::SUBSIDIARY_CODE ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the affiliate code to the passed array.
|
||||
*
|
||||
* @since partner-1.5.0
|
||||
* @since 2.0.0
|
||||
*
|
||||
* @param array $params The parameters array.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function add_affiliate_code_to_params_array( $params ) {
|
||||
if ( ! is_array( $params ) ) {
|
||||
return $params;
|
||||
}
|
||||
return array_merge( $params, $this->get_code_as_array( self::AFFILIATE_CODE ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the passed URL with the partner code added as a URL query arg.
|
||||
*
|
||||
* @since partner-1.0.0
|
||||
* @since 2.0.0
|
||||
*
|
||||
* @param string $type The partner code.
|
||||
* @param string $url The URL where the partner subsidiary id will be added.
|
||||
*
|
||||
* @return string The passed URL with the partner code added.
|
||||
*/
|
||||
public function add_code_as_query_arg( $type, $url ) {
|
||||
return add_query_arg( $this->get_code_as_array( $type ), $url );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the partner code in an associative array format
|
||||
*
|
||||
* @since partner-1.5.0
|
||||
* @since 2.0.0
|
||||
*
|
||||
* @param string $type The partner code.
|
||||
* @return array
|
||||
*/
|
||||
private function get_code_as_array( $type ) {
|
||||
switch ( $type ) {
|
||||
case self::AFFILIATE_CODE:
|
||||
$query_arg_name = 'aff';
|
||||
break;
|
||||
case self::SUBSIDIARY_CODE:
|
||||
$query_arg_name = 'subsidiaryId';
|
||||
break;
|
||||
default:
|
||||
return array();
|
||||
}
|
||||
|
||||
$code = $this->get_partner_code( $type );
|
||||
|
||||
if ( '' === $code ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
return array( $query_arg_name => $code );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a partner code.
|
||||
*
|
||||
* @since partner-1.0.0
|
||||
* @since 2.0.0
|
||||
*
|
||||
* @param string $type This can be either 'affiliate' or 'subsidiary'. Returns empty string when code is unknown.
|
||||
*
|
||||
* @return string The partner code.
|
||||
*/
|
||||
public function get_partner_code( $type ) {
|
||||
switch ( $type ) {
|
||||
case self::AFFILIATE_CODE:
|
||||
/**
|
||||
* Allow to filter the affiliate code.
|
||||
*
|
||||
* @since partner-1.0.0
|
||||
* @since-jetpack 6.9.0
|
||||
* @since 2.0.0
|
||||
*
|
||||
* @param string $affiliate_code The affiliate code, blank by default.
|
||||
*/
|
||||
return apply_filters( 'jetpack_affiliate_code', get_option( 'jetpack_affiliate_code', '' ) );
|
||||
case self::SUBSIDIARY_CODE:
|
||||
/**
|
||||
* Allow to filter the partner subsidiary id.
|
||||
*
|
||||
* @since partner-1.0.0
|
||||
* @since 2.0.0
|
||||
*
|
||||
* @param string $subsidiary_id The partner subsidiary id, blank by default.
|
||||
*/
|
||||
return apply_filters(
|
||||
'jetpack_partner_subsidiary_id',
|
||||
get_option( 'jetpack_partner_subsidiary_id', '' )
|
||||
);
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the singleton for testing purposes.
|
||||
*/
|
||||
public static function reset() {
|
||||
self::$instance = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
<?php
|
||||
/**
|
||||
* Storage for plugin connection information.
|
||||
*
|
||||
* @package automattic/jetpack-connection
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Connection;
|
||||
|
||||
use Jetpack_Options;
|
||||
use WP_Error;
|
||||
|
||||
/**
|
||||
* The class serves a single purpose - to store the data which plugins use the connection, along with some auxiliary information.
|
||||
*/
|
||||
class Plugin_Storage {
|
||||
|
||||
const ACTIVE_PLUGINS_OPTION_NAME = 'jetpack_connection_active_plugins';
|
||||
|
||||
/**
|
||||
* Transient name used as flag to indicate that the active connected plugins list needs refreshing.
|
||||
*/
|
||||
const ACTIVE_PLUGINS_REFRESH_FLAG = 'jetpack_connection_active_plugins_refresh';
|
||||
|
||||
/**
|
||||
* Whether this class was configured for the first time or not.
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
private static $configured = false;
|
||||
|
||||
/**
|
||||
* Connected plugins.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static $plugins = array();
|
||||
|
||||
/**
|
||||
* The blog ID the storage is setup for.
|
||||
* The data will be refreshed if the blog ID changes.
|
||||
* Used for the multisite networks.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private static $current_blog_id = null;
|
||||
|
||||
/**
|
||||
* Add or update the plugin information in the storage.
|
||||
*
|
||||
* @param string $slug Plugin slug.
|
||||
* @param array $args Plugin arguments, optional.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function upsert( $slug, array $args = array() ) {
|
||||
self::$plugins[ $slug ] = $args;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the plugin information by slug.
|
||||
* WARNING: the method cannot be called until Plugin_Storage::configure is called, which happens on plugins_loaded
|
||||
* Even if you don't use Jetpack Config, it may be introduced later by other plugins,
|
||||
* so please make sure not to run the method too early in the code.
|
||||
*
|
||||
* @param string $slug The plugin slug.
|
||||
*
|
||||
* @return array|null|WP_Error
|
||||
*/
|
||||
public static function get_one( $slug ) {
|
||||
$plugins = self::get_all();
|
||||
|
||||
if ( $plugins instanceof WP_Error ) {
|
||||
return $plugins;
|
||||
}
|
||||
|
||||
return empty( $plugins[ $slug ] ) ? null : $plugins[ $slug ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve info for all plugins that use the connection.
|
||||
* WARNING: the method cannot be called until Plugin_Storage::configure is called, which happens on plugins_loaded
|
||||
* Even if you don't use Jetpack Config, it may be introduced later by other plugins,
|
||||
* so please make sure not to run the method too early in the code.
|
||||
*
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public static function get_all() {
|
||||
$maybe_error = self::ensure_configured();
|
||||
|
||||
if ( $maybe_error instanceof WP_Error ) {
|
||||
return $maybe_error;
|
||||
}
|
||||
|
||||
return self::$plugins;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the plugin connection info from Jetpack.
|
||||
* WARNING: the method cannot be called until Plugin_Storage::configure is called, which happens on plugins_loaded
|
||||
* Even if you don't use Jetpack Config, it may be introduced later by other plugins,
|
||||
* so please make sure not to run the method too early in the code.
|
||||
*
|
||||
* @param string $slug The plugin slug.
|
||||
*
|
||||
* @return bool|WP_Error
|
||||
*/
|
||||
public static function delete( $slug ) {
|
||||
$maybe_error = self::ensure_configured();
|
||||
|
||||
if ( $maybe_error instanceof WP_Error ) {
|
||||
return $maybe_error;
|
||||
}
|
||||
|
||||
if ( array_key_exists( $slug, self::$plugins ) ) {
|
||||
unset( self::$plugins[ $slug ] );
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* The method makes sure that `Jetpack\Config` has finished, and it's now safe to retrieve the list of plugins.
|
||||
*
|
||||
* @return bool|WP_Error
|
||||
*/
|
||||
private static function ensure_configured() {
|
||||
if ( ! self::$configured ) {
|
||||
return new WP_Error( 'too_early', __( 'You cannot call this method until Jetpack Config is configured', 'jetpack-connection' ) );
|
||||
}
|
||||
|
||||
if ( is_multisite() && get_current_blog_id() !== self::$current_blog_id ) {
|
||||
if ( self::$current_blog_id ) {
|
||||
// If blog ID got changed, pull the list of active plugins for that blog from the database.
|
||||
self::$plugins = (array) get_option( self::ACTIVE_PLUGINS_OPTION_NAME, array() );
|
||||
}
|
||||
self::$current_blog_id = get_current_blog_id();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called once to configure this class after plugins_loaded.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function configure() {
|
||||
if ( self::$configured ) {
|
||||
return;
|
||||
}
|
||||
|
||||
self::$configured = true;
|
||||
|
||||
add_action( 'update_option_active_plugins', array( __CLASS__, 'set_flag_to_refresh_active_connected_plugins' ) );
|
||||
|
||||
self::maybe_update_active_connected_plugins();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a flag to indicate that the active connected plugins list needs to be updated.
|
||||
* This will happen when the `active_plugins` option is updated.
|
||||
*
|
||||
* @see configure
|
||||
*/
|
||||
public static function set_flag_to_refresh_active_connected_plugins() {
|
||||
set_transient( self::ACTIVE_PLUGINS_REFRESH_FLAG, time() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if we need to update the active connected plugins list.
|
||||
*/
|
||||
public static function maybe_update_active_connected_plugins() {
|
||||
$maybe_error = self::ensure_configured();
|
||||
|
||||
if ( $maybe_error instanceof WP_Error ) {
|
||||
return;
|
||||
}
|
||||
// Only attempt to update the option if the corresponding flag is set.
|
||||
if ( ! get_transient( self::ACTIVE_PLUGINS_REFRESH_FLAG ) ) {
|
||||
return;
|
||||
}
|
||||
// Only attempt to update the option on POST requests.
|
||||
// This will prevent the option from being updated multiple times due to concurrent requests.
|
||||
if ( ! ( isset( $_SERVER['REQUEST_METHOD'] ) && 'POST' === $_SERVER['REQUEST_METHOD'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
delete_transient( self::ACTIVE_PLUGINS_REFRESH_FLAG );
|
||||
|
||||
if ( is_multisite() ) {
|
||||
self::$current_blog_id = get_current_blog_id();
|
||||
}
|
||||
|
||||
// If a plugin was activated or deactivated.
|
||||
// self::$plugins is populated in Config::ensure_options_connection().
|
||||
$configured_plugin_keys = array_keys( self::$plugins );
|
||||
$stored_plugin_keys = array_keys( (array) get_option( self::ACTIVE_PLUGINS_OPTION_NAME, array() ) );
|
||||
sort( $configured_plugin_keys );
|
||||
sort( $stored_plugin_keys );
|
||||
|
||||
if ( $configured_plugin_keys !== $stored_plugin_keys ) {
|
||||
self::update_active_plugins_option();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the active plugins option with current list of active plugins.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function update_active_plugins_option() {
|
||||
// Note: Since this option is synced to wpcom, if you change its structure, you have to update the sanitizer at wpcom side.
|
||||
update_option( self::ACTIVE_PLUGINS_OPTION_NAME, self::$plugins );
|
||||
if ( ! class_exists( 'Automattic\Jetpack\Sync\Settings' ) || ! \Automattic\Jetpack\Sync\Settings::is_sync_enabled() ) {
|
||||
self::update_active_plugins_wpcom_no_sync_fallback();
|
||||
// Remove the checksum for active plugins, so it gets recalculated when sync gets activated.
|
||||
$jetpack_callables_sync_checksum = Jetpack_Options::get_raw_option( 'jetpack_callables_sync_checksum' );
|
||||
if ( isset( $jetpack_callables_sync_checksum['jetpack_connection_active_plugins'] ) ) {
|
||||
unset( $jetpack_callables_sync_checksum['jetpack_connection_active_plugins'] );
|
||||
Jetpack_Options::update_raw_option( 'jetpack_callables_sync_checksum', $jetpack_callables_sync_checksum );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update active plugins option with current list of active plugins on WPCOM.
|
||||
* This is a fallback to ensure this option is always up to date on WPCOM in case
|
||||
* Sync is not present or disabled.
|
||||
*
|
||||
* @since 1.34.0
|
||||
*/
|
||||
private static function update_active_plugins_wpcom_no_sync_fallback() {
|
||||
$connection = new Manager();
|
||||
if ( ! $connection->is_connected() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$site_id = \Jetpack_Options::get_option( 'id' );
|
||||
|
||||
$body = wp_json_encode(
|
||||
array(
|
||||
'active_connected_plugins' => self::$plugins,
|
||||
),
|
||||
JSON_UNESCAPED_SLASHES
|
||||
);
|
||||
|
||||
Client::wpcom_json_api_request_as_blog(
|
||||
sprintf( '/sites/%d/jetpack-active-connected-plugins', $site_id ),
|
||||
'2',
|
||||
array(
|
||||
'headers' => array( 'content-type' => 'application/json' ),
|
||||
'method' => 'POST',
|
||||
),
|
||||
$body,
|
||||
'wpcom'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin connection management class.
|
||||
*
|
||||
* @package automattic/jetpack-connection
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Connection;
|
||||
|
||||
/**
|
||||
* Plugin connection management class.
|
||||
* The class represents a single plugin that uses Jetpack connection.
|
||||
* Its functionality has been pretty simplistic so far: add to the storage (`Plugin_Storage`), remove it from there,
|
||||
* and determine whether it's the last active connection. As the component grows, there'll be more functionality added.
|
||||
*/
|
||||
class Plugin {
|
||||
|
||||
/**
|
||||
* List of the keys allowed as arguments
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $arguments_whitelist = array(
|
||||
'url_info',
|
||||
);
|
||||
|
||||
/**
|
||||
* Plugin slug.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $slug;
|
||||
|
||||
/**
|
||||
* Users Connection Admin instance.
|
||||
*
|
||||
* @var Users_Connection_Admin
|
||||
*/
|
||||
private $users_connection_admin;
|
||||
|
||||
/**
|
||||
* Initialize the plugin manager.
|
||||
*
|
||||
* @param string $slug Plugin slug.
|
||||
*/
|
||||
public function __construct( $slug ) {
|
||||
$this->slug = $slug;
|
||||
|
||||
// Initialize Users_Connection_Admin
|
||||
$this->users_connection_admin = new Users_Connection_Admin();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the plugin slug.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_slug() {
|
||||
return $this->slug;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the plugin connection info into Jetpack.
|
||||
*
|
||||
* @param string $name Plugin name, required.
|
||||
* @param array $args Plugin arguments, optional.
|
||||
*
|
||||
* @return $this
|
||||
* @see $this->arguments_whitelist
|
||||
*/
|
||||
public function add( $name, array $args = array() ) {
|
||||
$args = compact( 'name' ) + array_intersect_key( $args, array_flip( $this->arguments_whitelist ) );
|
||||
|
||||
Plugin_Storage::upsert( $this->slug, $args );
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the plugin connection info from Jetpack.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function remove() {
|
||||
Plugin_Storage::delete( $this->slug );
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if this plugin connection is the only one active at the moment, if any.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_only() {
|
||||
$plugins = Plugin_Storage::get_all();
|
||||
|
||||
if ( is_wp_error( $plugins ) ) {
|
||||
if ( 'too_early' === $plugins->get_error_code() ) {
|
||||
_doing_it_wrong( __METHOD__, esc_html( $plugins->get_error_code() . ': ' . $plugins->get_error_message() ), '6.16.1' );
|
||||
} else {
|
||||
wp_trigger_error( __METHOD__, $plugins->get_error_code() . ': ' . $plugins->get_error_message() );
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return ! $plugins || ( array_key_exists( $this->slug, $plugins ) && 1 === count( $plugins ) );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
<?php
|
||||
/**
|
||||
* The Jetpack Connection Rest Authentication file.
|
||||
*
|
||||
* @package automattic/jetpack-connection
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Connection;
|
||||
|
||||
use WP_Error;
|
||||
|
||||
/**
|
||||
* The Jetpack Connection Rest Authentication class.
|
||||
*/
|
||||
class Rest_Authentication {
|
||||
|
||||
/**
|
||||
* The rest authentication status.
|
||||
*
|
||||
* @since 1.17.0
|
||||
* @var boolean
|
||||
*/
|
||||
private $rest_authentication_status = null;
|
||||
|
||||
/**
|
||||
* The rest authentication type.
|
||||
* Can be either 'user' or 'blog' depending on whether the request
|
||||
* is signed with a user or a blog token.
|
||||
*
|
||||
* @since 1.29.0
|
||||
* @var string
|
||||
*/
|
||||
private $rest_authentication_type = null;
|
||||
|
||||
/**
|
||||
* The Manager object.
|
||||
*
|
||||
* @since 1.17.0
|
||||
* @var Object
|
||||
*/
|
||||
private $connection_manager = null;
|
||||
|
||||
/**
|
||||
* Holds the singleton instance of this class
|
||||
*
|
||||
* @since 1.17.0
|
||||
* @var Object
|
||||
*/
|
||||
private static $instance = false;
|
||||
|
||||
/**
|
||||
* Flag used to avoid determine_current_user filter to enter an infinite loop
|
||||
*
|
||||
* @since 1.26.0
|
||||
* @var boolean
|
||||
*/
|
||||
private $doing_determine_current_user_filter = false;
|
||||
|
||||
/**
|
||||
* The constructor.
|
||||
*/
|
||||
private function __construct() {
|
||||
$this->connection_manager = new Manager();
|
||||
}
|
||||
|
||||
/**
|
||||
* Controls the single instance of this class.
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
public static function init() {
|
||||
if ( ! self::$instance ) {
|
||||
self::$instance = new self();
|
||||
|
||||
add_filter( 'determine_current_user', array( self::$instance, 'wp_rest_authenticate' ) );
|
||||
add_filter( 'rest_authentication_errors', array( self::$instance, 'wp_rest_authentication_errors' ) );
|
||||
}
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticates requests from Jetpack server to WP REST API endpoints.
|
||||
* Uses the existing XMLRPC request signing implementation.
|
||||
*
|
||||
* @param int|bool $user User ID if one has been determined, false otherwise.
|
||||
*
|
||||
* @return int|null The user id or null if the request was authenticated via blog token, or not authenticated at all.
|
||||
*/
|
||||
public function wp_rest_authenticate( $user ) {
|
||||
if ( $this->doing_determine_current_user_filter ) {
|
||||
return $user;
|
||||
}
|
||||
|
||||
$this->doing_determine_current_user_filter = true;
|
||||
|
||||
try {
|
||||
if ( ! empty( $user ) ) {
|
||||
// Another authentication method is in effect.
|
||||
return $user;
|
||||
}
|
||||
|
||||
add_filter(
|
||||
'jetpack_constant_default_value',
|
||||
__NAMESPACE__ . '\Utils::jetpack_api_constant_filter',
|
||||
10,
|
||||
2
|
||||
);
|
||||
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
if ( ! isset( $_GET['_for'] ) || 'jetpack' !== $_GET['_for'] ) {
|
||||
// Nothing to do for this authentication method.
|
||||
return null;
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
if ( ! isset( $_GET['token'] ) && ! isset( $_GET['signature'] ) ) {
|
||||
// Nothing to do for this authentication method.
|
||||
return null;
|
||||
}
|
||||
|
||||
if ( ! isset( $_SERVER['REQUEST_METHOD'] ) ) {
|
||||
$this->rest_authentication_status = new WP_Error(
|
||||
'rest_invalid_request',
|
||||
__( 'The request method is missing.', 'jetpack-connection' ),
|
||||
array( 'status' => 400 )
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Only support specific request parameters that have been tested and
|
||||
// are known to work with signature verification. A different method
|
||||
// can be passed to the WP REST API via the '?_method=' parameter if
|
||||
// needed.
|
||||
if ( 'GET' !== $_SERVER['REQUEST_METHOD'] && 'POST' !== $_SERVER['REQUEST_METHOD'] ) {
|
||||
$this->rest_authentication_status = new WP_Error(
|
||||
'rest_invalid_request',
|
||||
__( 'This request method is not supported.', 'jetpack-connection' ),
|
||||
array( 'status' => 400 )
|
||||
);
|
||||
return null;
|
||||
}
|
||||
if ( 'POST' !== $_SERVER['REQUEST_METHOD'] && ! empty( file_get_contents( 'php://input' ) ) ) {
|
||||
$this->rest_authentication_status = new WP_Error(
|
||||
'rest_invalid_request',
|
||||
__( 'This request method does not support body parameters.', 'jetpack-connection' ),
|
||||
array( 'status' => 400 )
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
$verified = $this->connection_manager->verify_xml_rpc_signature();
|
||||
|
||||
if (
|
||||
$verified &&
|
||||
isset( $verified['type'] ) &&
|
||||
'blog' === $verified['type']
|
||||
) {
|
||||
// Site-level authentication successful.
|
||||
$this->rest_authentication_status = true;
|
||||
$this->rest_authentication_type = 'blog';
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
$verified &&
|
||||
isset( $verified['type'] ) &&
|
||||
'user' === $verified['type'] &&
|
||||
! empty( $verified['user_id'] )
|
||||
) {
|
||||
// User-level authentication successful.
|
||||
$this->rest_authentication_status = true;
|
||||
$this->rest_authentication_type = 'user';
|
||||
return $verified['user_id'];
|
||||
}
|
||||
|
||||
// Something else went wrong. Probably a signature error.
|
||||
$this->rest_authentication_status = new WP_Error(
|
||||
'rest_invalid_signature',
|
||||
__( 'The request is not signed correctly.', 'jetpack-connection' ),
|
||||
array( 'status' => 400 )
|
||||
);
|
||||
return null;
|
||||
} finally {
|
||||
$this->doing_determine_current_user_filter = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Report authentication status to the WP REST API.
|
||||
*
|
||||
* @param WP_Error|mixed $value Error from another authentication handler, null if we should handle it, or another value if not.
|
||||
* @return WP_Error|boolean|null {@see WP_JSON_Server::check_authentication}
|
||||
*/
|
||||
public function wp_rest_authentication_errors( $value ) {
|
||||
if ( null !== $value ) {
|
||||
return $value;
|
||||
}
|
||||
return $this->rest_authentication_status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the saved authentication state in between testing requests.
|
||||
*/
|
||||
public function reset_saved_auth_state() {
|
||||
$this->rest_authentication_status = null;
|
||||
$this->connection_manager->reset_saved_auth_state();
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the request was signed with a blog token.
|
||||
*
|
||||
* @since 1.29.0
|
||||
*
|
||||
* @return bool True if the request was signed with a valid blog token, false otherwise.
|
||||
*/
|
||||
public static function is_signed_with_blog_token() {
|
||||
$instance = self::init();
|
||||
|
||||
return true === $instance->rest_authentication_status && 'blog' === $instance->rest_authentication_type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the request was signed with a user token.
|
||||
*
|
||||
* @since 6.7.0
|
||||
*
|
||||
* @return bool True if the request was signed with a valid user token, false otherwise.
|
||||
*/
|
||||
public static function is_signed_with_user_token() {
|
||||
$instance = self::init();
|
||||
|
||||
return true === $instance->rest_authentication_status && 'user' === $instance->rest_authentication_type;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
<?php
|
||||
/**
|
||||
* The Jetpack Connection Secrets class file.
|
||||
*
|
||||
* @package automattic/jetpack-connection
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Connection;
|
||||
|
||||
use Jetpack_Options;
|
||||
use WP_Error;
|
||||
|
||||
/**
|
||||
* The Jetpack Connection Secrets class that is used to manage secrets.
|
||||
*/
|
||||
class Secrets {
|
||||
|
||||
const SECRETS_MISSING = 'secrets_missing';
|
||||
const SECRETS_EXPIRED = 'secrets_expired';
|
||||
const LEGACY_SECRETS_OPTION_NAME = 'jetpack_secrets';
|
||||
|
||||
/**
|
||||
* Deletes all connection secrets from the local Jetpack site.
|
||||
*/
|
||||
public function delete_all() {
|
||||
Jetpack_Options::delete_raw_option( 'jetpack_secrets' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the wp_generate_password function with the required parameters. This is the
|
||||
* default implementation of the secret callable, can be overridden using the
|
||||
* jetpack_connection_secret_generator filter.
|
||||
*
|
||||
* @return String $secret value.
|
||||
*/
|
||||
private function secret_callable_method() {
|
||||
$secret = wp_generate_password( 32, false );
|
||||
|
||||
// Some sites may hook into the random_password filter and make the password shorter, let's make sure our secret has the required length.
|
||||
$attempts = 1;
|
||||
$secret_length = strlen( $secret );
|
||||
while ( $secret_length < 32 && $attempts < 32 ) {
|
||||
++$attempts;
|
||||
$secret .= wp_generate_password( 32, false );
|
||||
$secret_length = strlen( $secret );
|
||||
}
|
||||
return (string) substr( $secret, 0, 32 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates two secret tokens and the end of life timestamp for them.
|
||||
*
|
||||
* @param String $action The action name.
|
||||
* @param Integer|bool $user_id The user identifier. Defaults to `false`.
|
||||
* @param Integer $exp Expiration time in seconds.
|
||||
*/
|
||||
public function generate( $action, $user_id = false, $exp = 600 ) {
|
||||
if ( false === $user_id ) {
|
||||
$user_id = get_current_user_id();
|
||||
}
|
||||
|
||||
$callable = apply_filters( 'jetpack_connection_secret_generator', array( static::class, 'secret_callable_method' ) );
|
||||
|
||||
$secrets = Jetpack_Options::get_raw_option(
|
||||
self::LEGACY_SECRETS_OPTION_NAME,
|
||||
array()
|
||||
);
|
||||
|
||||
$secret_name = 'jetpack_' . $action . '_' . $user_id;
|
||||
|
||||
if (
|
||||
isset( $secrets[ $secret_name ] ) &&
|
||||
$secrets[ $secret_name ]['exp'] > time()
|
||||
) {
|
||||
return $secrets[ $secret_name ];
|
||||
}
|
||||
|
||||
$secret_value = array(
|
||||
'secret_1' => call_user_func( $callable ),
|
||||
'secret_2' => call_user_func( $callable ),
|
||||
'exp' => time() + $exp,
|
||||
);
|
||||
|
||||
$secrets[ $secret_name ] = $secret_value;
|
||||
|
||||
$res = Jetpack_Options::update_raw_option( self::LEGACY_SECRETS_OPTION_NAME, $secrets );
|
||||
return $res ? $secrets[ $secret_name ] : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns two secret tokens and the end of life timestamp for them.
|
||||
*
|
||||
* @param String $action The action name.
|
||||
* @param Integer $user_id The user identifier.
|
||||
* @return string|array an array of secrets or an error string.
|
||||
* @phan-return string|array{secret_1:string,secret_2:string,exp:int}
|
||||
*/
|
||||
public function get( $action, $user_id ) {
|
||||
$secret_name = 'jetpack_' . $action . '_' . $user_id;
|
||||
$secrets = Jetpack_Options::get_raw_option(
|
||||
self::LEGACY_SECRETS_OPTION_NAME,
|
||||
array()
|
||||
);
|
||||
|
||||
if ( ! isset( $secrets[ $secret_name ] ) ) {
|
||||
return self::SECRETS_MISSING;
|
||||
}
|
||||
|
||||
if ( $secrets[ $secret_name ]['exp'] < time() ) {
|
||||
$this->delete( $action, $user_id );
|
||||
return self::SECRETS_EXPIRED;
|
||||
}
|
||||
|
||||
return $secrets[ $secret_name ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes secret tokens in case they, for example, have expired.
|
||||
*
|
||||
* @param String $action The action name.
|
||||
* @param Integer $user_id The user identifier.
|
||||
*/
|
||||
public function delete( $action, $user_id ) {
|
||||
$secret_name = 'jetpack_' . $action . '_' . $user_id;
|
||||
$secrets = Jetpack_Options::get_raw_option(
|
||||
self::LEGACY_SECRETS_OPTION_NAME,
|
||||
array()
|
||||
);
|
||||
if ( isset( $secrets[ $secret_name ] ) ) {
|
||||
unset( $secrets[ $secret_name ] );
|
||||
Jetpack_Options::update_raw_option( self::LEGACY_SECRETS_OPTION_NAME, $secrets );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify a Previously Generated Secret.
|
||||
*
|
||||
* @param string $action The type of secret to verify.
|
||||
* @param string $secret_1 The secret string to compare to what is stored.
|
||||
* @param int $user_id The user ID of the owner of the secret.
|
||||
* @return WP_Error|string WP_Error on failure, secret_2 on success.
|
||||
*/
|
||||
public function verify( $action, $secret_1, $user_id ) {
|
||||
$allowed_actions = array( 'register', 'authorize', 'publicize' );
|
||||
if ( ! in_array( $action, $allowed_actions, true ) ) {
|
||||
return new WP_Error( 'unknown_verification_action', 'Unknown Verification Action', 400 );
|
||||
}
|
||||
|
||||
$user = get_user_by( 'id', $user_id );
|
||||
|
||||
/**
|
||||
* We've begun verifying the previously generated secret.
|
||||
*
|
||||
* @since 1.7.0
|
||||
* @since-jetpack 7.5.0
|
||||
*
|
||||
* @param string $action The type of secret to verify.
|
||||
* @param \WP_User $user The user object.
|
||||
*/
|
||||
do_action( 'jetpack_verify_secrets_begin', $action, $user );
|
||||
|
||||
/** Closure to run the 'fail' action and return an error. */
|
||||
$return_error = function ( WP_Error $error ) use ( $action, $user ) {
|
||||
/**
|
||||
* Verifying of the previously generated secret has failed.
|
||||
*
|
||||
* @since 1.7.0
|
||||
* @since-jetpack 7.5.0
|
||||
*
|
||||
* @param string $action The type of secret to verify.
|
||||
* @param \WP_User $user The user object.
|
||||
* @param WP_Error $error The error object.
|
||||
*/
|
||||
do_action( 'jetpack_verify_secrets_fail', $action, $user, $error );
|
||||
|
||||
return $error;
|
||||
};
|
||||
|
||||
$stored_secrets = $this->get( $action, $user_id );
|
||||
$this->delete( $action, $user_id );
|
||||
|
||||
$error = null;
|
||||
if ( empty( $secret_1 ) ) {
|
||||
$error = $return_error(
|
||||
new WP_Error(
|
||||
'verify_secret_1_missing',
|
||||
/* translators: "%s" is the name of a paramter. It can be either "secret_1" or "state". */
|
||||
sprintf( __( 'The required "%s" parameter is missing.', 'jetpack-connection' ), 'secret_1' ),
|
||||
400
|
||||
)
|
||||
);
|
||||
} elseif ( ! is_string( $secret_1 ) ) {
|
||||
$error = $return_error(
|
||||
new WP_Error(
|
||||
'verify_secret_1_malformed',
|
||||
/* translators: "%s" is the name of a paramter. It can be either "secret_1" or "state". */
|
||||
sprintf( __( 'The required "%s" parameter is malformed.', 'jetpack-connection' ), 'secret_1' ),
|
||||
400
|
||||
)
|
||||
);
|
||||
} elseif ( empty( $user_id ) ) {
|
||||
// $user_id is passed around during registration as "state".
|
||||
$error = $return_error(
|
||||
new WP_Error(
|
||||
'state_missing',
|
||||
/* translators: "%s" is the name of a paramter. It can be either "secret_1" or "state". */
|
||||
sprintf( __( 'The required "%s" parameter is missing.', 'jetpack-connection' ), 'state' ),
|
||||
400
|
||||
)
|
||||
);
|
||||
} elseif ( ! ctype_digit( (string) $user_id ) ) {
|
||||
$error = $return_error(
|
||||
new WP_Error(
|
||||
'state_malformed',
|
||||
/* translators: "%s" is the name of a paramter. It can be either "secret_1" or "state". */
|
||||
sprintf( __( 'The required "%s" parameter is malformed.', 'jetpack-connection' ), 'state' ),
|
||||
400
|
||||
)
|
||||
);
|
||||
} elseif ( self::SECRETS_MISSING === $stored_secrets ) {
|
||||
$error = $return_error(
|
||||
new WP_Error(
|
||||
'verify_secrets_missing',
|
||||
__( 'Verification secrets not found', 'jetpack-connection' ),
|
||||
400
|
||||
)
|
||||
);
|
||||
} elseif ( self::SECRETS_EXPIRED === $stored_secrets ) {
|
||||
$error = $return_error(
|
||||
new WP_Error(
|
||||
'verify_secrets_expired',
|
||||
__( 'Verification took too long', 'jetpack-connection' ),
|
||||
400
|
||||
)
|
||||
);
|
||||
} elseif ( ! $stored_secrets ) {
|
||||
$error = $return_error(
|
||||
new WP_Error(
|
||||
'verify_secrets_empty',
|
||||
__( 'Verification secrets are empty', 'jetpack-connection' ),
|
||||
400
|
||||
)
|
||||
);
|
||||
} elseif ( is_wp_error( $stored_secrets ) ) {
|
||||
$stored_secrets->add_data( 400 );
|
||||
$error = $return_error( $stored_secrets );
|
||||
} elseif ( empty( $stored_secrets['secret_1'] ) || empty( $stored_secrets['secret_2'] ) || empty( $stored_secrets['exp'] ) ) {
|
||||
$error = $return_error(
|
||||
new WP_Error(
|
||||
'verify_secrets_incomplete',
|
||||
__( 'Verification secrets are incomplete', 'jetpack-connection' ),
|
||||
400
|
||||
)
|
||||
);
|
||||
} elseif ( ! hash_equals( $secret_1, $stored_secrets['secret_1'] ) ) {
|
||||
$error = $return_error(
|
||||
new WP_Error(
|
||||
'verify_secrets_mismatch',
|
||||
__( 'Secret mismatch', 'jetpack-connection' ),
|
||||
400
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Something went wrong during the checks, returning the error.
|
||||
if ( ! empty( $error ) ) {
|
||||
return $error;
|
||||
}
|
||||
|
||||
/**
|
||||
* We've succeeded at verifying the previously generated secret.
|
||||
*
|
||||
* @since 1.7.0
|
||||
* @since-jetpack 7.5.0
|
||||
*
|
||||
* @param string $action The type of secret to verify.
|
||||
* @param \WP_User $user The user object.
|
||||
*/
|
||||
do_action( 'jetpack_verify_secrets_success', $action, $user );
|
||||
|
||||
return $stored_secrets['secret_2'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
<?php
|
||||
/**
|
||||
* The Server_Sandbox class.
|
||||
*
|
||||
* This feature is only useful for Automattic developers.
|
||||
* It configures Jetpack to talk to staging/sandbox servers
|
||||
* on WordPress.com instead of production servers.
|
||||
*
|
||||
* @package automattic/jetpack-sandbox
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Connection;
|
||||
|
||||
use Automattic\Jetpack\Constants;
|
||||
|
||||
/**
|
||||
* The Server_Sandbox class.
|
||||
*/
|
||||
class Server_Sandbox {
|
||||
|
||||
/**
|
||||
* Sets up the action hooks for the server sandbox.
|
||||
*/
|
||||
public function init() {
|
||||
if ( did_action( 'jetpack_server_sandbox_init' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
add_action( 'requests-requests.before_request', array( $this, 'server_sandbox' ), 10, 4 );
|
||||
add_action( 'admin_bar_menu', array( $this, 'admin_bar_add_sandbox_item' ), 999 );
|
||||
|
||||
/**
|
||||
* Fires when the server sandbox is initialized. This action is used to ensure that
|
||||
* the server sandbox action hooks are set up only once.
|
||||
*
|
||||
* @since 1.30.7
|
||||
*/
|
||||
do_action( 'jetpack_server_sandbox_init' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the new url and host values.
|
||||
*
|
||||
* @param string $sandbox Sandbox domain.
|
||||
* @param string $url URL of request about to be made.
|
||||
* @param array $headers Headers of request about to be made.
|
||||
* @param string $data The body of request about to be made.
|
||||
* @param string $method The method of request about to be made.
|
||||
*
|
||||
* @return array [ 'url' => new URL, 'host' => new Host, 'new_signature => New signature if url was changed ]
|
||||
*/
|
||||
public function server_sandbox_request_parameters( $sandbox, $url, $headers, $data = null, $method = 'GET' ) {
|
||||
$host = '';
|
||||
$new_signature = '';
|
||||
|
||||
if ( ! is_string( $sandbox ) || ! is_string( $url ) ) {
|
||||
return array(
|
||||
'url' => $url,
|
||||
'host' => $host,
|
||||
'new_signature' => $new_signature,
|
||||
);
|
||||
}
|
||||
|
||||
$url_host = wp_parse_url( $url, PHP_URL_HOST );
|
||||
|
||||
switch ( $url_host ) {
|
||||
case 'public-api.wordpress.com':
|
||||
case 'jetpack.wordpress.com':
|
||||
case 'jetpack.com':
|
||||
case 'dashboard.wordpress.com':
|
||||
$host = $headers['Host'] ?? $url_host;
|
||||
$original_url = $url;
|
||||
$url = preg_replace(
|
||||
'@^(https?://)' . preg_quote( $url_host, '@' ) . '(?=[/?#].*|$)@',
|
||||
'${1}' . $sandbox,
|
||||
$url,
|
||||
1
|
||||
);
|
||||
|
||||
/**
|
||||
* Whether to add the X Debug query parameter to the request made to the Sandbox
|
||||
*
|
||||
* @since 1.36.0
|
||||
*
|
||||
* @param bool $add_parameter Whether to add the parameter to the request or not. Default is to false.
|
||||
* @param string $url The URL of the request being made.
|
||||
* @param string $host The host of the request being made.
|
||||
*/
|
||||
if ( apply_filters( 'jetpack_sandbox_add_profile_parameter', false, $url, $host ) ) {
|
||||
$url = add_query_arg( 'XDEBUG_PROFILE', 1, $url );
|
||||
|
||||
// URL has been modified since the signature was created. We'll need a new one.
|
||||
$original_url = add_query_arg( 'XDEBUG_PROFILE', 1, $original_url );
|
||||
$new_signature = $this->get_new_signature( $original_url, $headers, $data, $method );
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return compact( 'url', 'host', 'new_signature' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a new signature for the request
|
||||
*
|
||||
* @param string $url The new URL to be signed.
|
||||
* @param array $headers The headers of the request about to be made.
|
||||
* @param string $data The body of request about to be made.
|
||||
* @param string $method The method of the request about to be made.
|
||||
* @return string|null
|
||||
*/
|
||||
private function get_new_signature( $url, $headers, $data, $method ) {
|
||||
|
||||
if ( ! empty( $headers['Authorization'] ) ) {
|
||||
$a_headers = $this->extract_authorization_headers( $headers );
|
||||
if ( ! empty( $a_headers ) ) {
|
||||
$token_details = explode( ':', $a_headers['token'] );
|
||||
|
||||
if ( count( $token_details ) === 3 ) {
|
||||
$user_id = $token_details[2];
|
||||
$token = ( new Tokens() )->get_access_token( $user_id );
|
||||
$time_diff = (int) \Jetpack_Options::get_option( 'time_diff' );
|
||||
$jetpack_signature = new \Jetpack_Signature( $token->secret, $time_diff );
|
||||
|
||||
$signature = $jetpack_signature->sign_request(
|
||||
$a_headers['token'],
|
||||
$a_headers['timestamp'],
|
||||
$a_headers['nonce'],
|
||||
$a_headers['body-hash'],
|
||||
$method,
|
||||
$url,
|
||||
$data,
|
||||
false
|
||||
);
|
||||
|
||||
if ( $signature && ! is_wp_error( $signature ) ) {
|
||||
return $signature;
|
||||
} elseif ( is_wp_error( $signature ) ) {
|
||||
$this->log_new_signature_error( $signature->get_error_message() );
|
||||
}
|
||||
} else {
|
||||
$this->log_new_signature_error( 'Malformed token on Authorization Header' );
|
||||
}
|
||||
} else {
|
||||
$this->log_new_signature_error( 'Error extracting Authorization Header' );
|
||||
}
|
||||
} else {
|
||||
$this->log_new_signature_error( 'Empty Authorization Header' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs error if the attempt to create a new signature fails
|
||||
*
|
||||
* @param string $message The error message.
|
||||
* @return void
|
||||
*/
|
||||
private function log_new_signature_error( $message ) {
|
||||
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||
error_log( sprintf( "SANDBOXING: Error re-signing the request. '%s'", $message ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the values in the Authorization header into an array
|
||||
*
|
||||
* @param array $headers The headers of the request about to be made.
|
||||
* @return array|null
|
||||
*/
|
||||
public function extract_authorization_headers( $headers ) {
|
||||
if ( ! empty( $headers['Authorization'] ) && is_string( $headers['Authorization'] ) ) {
|
||||
$header = str_replace( 'X_JETPACK ', '', $headers['Authorization'] );
|
||||
$vars = explode( ' ', $header );
|
||||
$result = array();
|
||||
foreach ( $vars as $var ) {
|
||||
$elements = explode( '"', $var );
|
||||
if ( count( $elements ) === 3 ) {
|
||||
$result[ substr( $elements[0], 0, -1 ) ] = $elements[1];
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifies parameters of request in order to send the request to the
|
||||
* server specified by `JETPACK__SANDBOX_DOMAIN`.
|
||||
*
|
||||
* Attached to the `requests-requests.before_request` filter.
|
||||
*
|
||||
* @param string $url URL of request about to be made.
|
||||
* @param array $headers Headers of request about to be made.
|
||||
* @param array|string $data Data of request about to be made.
|
||||
* @param string $type Type of request about to be made.
|
||||
* @return void
|
||||
*/
|
||||
public function server_sandbox( &$url, &$headers, &$data = null, &$type = null ) {
|
||||
if ( ! Constants::get_constant( 'JETPACK__SANDBOX_DOMAIN' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$original_url = $url;
|
||||
|
||||
$request_parameters = $this->server_sandbox_request_parameters( Constants::get_constant( 'JETPACK__SANDBOX_DOMAIN' ), $url, $headers, $data, $type );
|
||||
|
||||
$url = $request_parameters['url'];
|
||||
|
||||
if ( $request_parameters['host'] ) {
|
||||
$headers['Host'] = $request_parameters['host'];
|
||||
|
||||
if ( $request_parameters['new_signature'] ) {
|
||||
$headers['Authorization'] = preg_replace( '/signature=\"[^\"]+\"/', 'signature="' . $request_parameters['new_signature'] . '"', $headers['Authorization'] );
|
||||
}
|
||||
|
||||
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||
error_log( sprintf( "SANDBOXING via '%s': '%s'", Constants::get_constant( 'JETPACK__SANDBOX_DOMAIN' ), $original_url ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a "Jetpack API Sandboxed" item to the admin bar if the JETPACK__SANDBOX_DOMAIN
|
||||
* constant is set.
|
||||
*
|
||||
* Attached to the `admin_bar_menu` action.
|
||||
*
|
||||
* @param \WP_Admin_Bar $wp_admin_bar The WP_Admin_Bar instance.
|
||||
*/
|
||||
public function admin_bar_add_sandbox_item( $wp_admin_bar ) {
|
||||
if ( ! Constants::get_constant( 'JETPACK__SANDBOX_DOMAIN' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$node = array(
|
||||
'id' => 'jetpack-connection-api-sandbox',
|
||||
'title' => 'Jetpack API Sandboxed',
|
||||
'meta' => array(
|
||||
'title' => 'Sandboxing via ' . Constants::get_constant( 'JETPACK__SANDBOX_DOMAIN' ),
|
||||
),
|
||||
);
|
||||
|
||||
$wp_admin_bar->add_menu( $node );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
<?php
|
||||
/**
|
||||
* WordPress Site Health integration for the Jetpack Connection package.
|
||||
*
|
||||
* @package automattic/jetpack-connection
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Connection;
|
||||
|
||||
/**
|
||||
* Integrates connection health tests into WordPress Site Health.
|
||||
*
|
||||
* This class defers to the Jetpack plugin's own debugger integration when it
|
||||
* is present (old Jetpack versions). When no legacy integration is detected,
|
||||
* it registers the connection health tests directly.
|
||||
*
|
||||
* @since 8.5.0
|
||||
*/
|
||||
class Site_Health {
|
||||
|
||||
/**
|
||||
* Whether the class has been initialized.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private static $initialized = false;
|
||||
|
||||
/**
|
||||
* Initialize Site Health integration.
|
||||
*
|
||||
* Should be called once, typically from the package's actions.php via a plugins_loaded hook.
|
||||
*
|
||||
* @since 8.5.0
|
||||
*/
|
||||
public static function init() {
|
||||
if ( self::$initialized ) {
|
||||
return;
|
||||
}
|
||||
self::$initialized = true;
|
||||
|
||||
add_action( 'admin_init', array( __CLASS__, 'maybe_register_site_health' ), 1 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Conditionally register Site Health hooks.
|
||||
*
|
||||
* Checks whether the legacy Jetpack debugger has already registered its
|
||||
* Site Health hooks. If so, we defer to avoid duplicate tests.
|
||||
*
|
||||
* @since 8.5.0
|
||||
*/
|
||||
public static function maybe_register_site_health() {
|
||||
// Defer to the old Jetpack plugin's debugger if it has already registered
|
||||
// its Site Health filter. Old Jetpack versions add this filter during plugin
|
||||
// loading, so by admin_init it is already present.
|
||||
if ( has_filter( 'site_status_tests', 'jetpack_debugger_site_status_tests' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
add_filter( 'site_status_tests', array( __CLASS__, 'register_site_health_tests' ) );
|
||||
add_action( 'wp_ajax_health-check-jetpack-connection-health', array( __CLASS__, 'ajax_local_testing_suite' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register connection tests with WordPress Site Health.
|
||||
*
|
||||
* @since 8.5.0
|
||||
*
|
||||
* @param array $core_tests Array of tests from Core's Site Health.
|
||||
*
|
||||
* @return array Modified array of tests.
|
||||
*/
|
||||
public static function register_site_health_tests( $core_tests ) {
|
||||
$cxn_tests = new Connection_Health_Tests();
|
||||
$tests = $cxn_tests->list_tests( 'direct' );
|
||||
|
||||
foreach ( $tests as $test ) {
|
||||
$core_tests['direct'][ $test['name'] ] = array(
|
||||
'label' => __( 'Jetpack: ', 'jetpack-connection' ) . $test['name'],
|
||||
'test' => self::make_site_health_callback( $test, $cxn_tests ),
|
||||
);
|
||||
}
|
||||
|
||||
$core_tests['async']['jetpack_connection_test_suite'] = array(
|
||||
'label' => __( 'Jetpack Connection Tests', 'jetpack-connection' ),
|
||||
'test' => 'jetpack-connection-health',
|
||||
);
|
||||
|
||||
return $core_tests;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a closure for a Site Health direct test.
|
||||
*
|
||||
* @since 8.5.0
|
||||
*
|
||||
* @param array $test Test definition array.
|
||||
* @param Connection_Health_Tests $cxn_tests Test suite instance.
|
||||
*
|
||||
* @return callable The Site Health test callback.
|
||||
*/
|
||||
private static function make_site_health_callback( $test, $cxn_tests ) {
|
||||
return function () use ( $test, $cxn_tests ) {
|
||||
$results = $cxn_tests->run_test( $test['name'] );
|
||||
if ( is_wp_error( $results ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$label = $results['label'] ?
|
||||
$results['label'] :
|
||||
ucwords(
|
||||
str_replace(
|
||||
'_',
|
||||
' ',
|
||||
str_replace( 'test__', '', $test['name'] )
|
||||
)
|
||||
);
|
||||
|
||||
if ( $results['long_description'] ) {
|
||||
$description = $results['long_description'];
|
||||
} elseif ( $results['short_description'] ) {
|
||||
$description = sprintf(
|
||||
'<p>%s</p>',
|
||||
$results['short_description']
|
||||
);
|
||||
} else {
|
||||
$description = sprintf(
|
||||
'<p>%s</p>',
|
||||
__( 'This test successfully passed!', 'jetpack-connection' )
|
||||
);
|
||||
}
|
||||
|
||||
$badge_label = $cxn_tests->get_site_health_badge_label();
|
||||
|
||||
$return = array(
|
||||
'label' => $label,
|
||||
'status' => 'good',
|
||||
'badge' => array(
|
||||
'label' => $badge_label,
|
||||
'color' => 'green',
|
||||
),
|
||||
'description' => $description,
|
||||
'actions' => '',
|
||||
'test' => 'jetpack_' . $test['name'],
|
||||
);
|
||||
|
||||
if ( false === $results['pass'] ) {
|
||||
$return['status'] = $results['severity'];
|
||||
if ( ! empty( $results['action'] ) ) {
|
||||
$return['actions'] = sprintf(
|
||||
'<a href="%1$s" target="_blank" rel="noopener noreferrer">%2$s <span class="screen-reader-text">%3$s</span><span aria-hidden="true" class="dashicons dashicons-external"></span></a>',
|
||||
esc_url( $results['action'] ),
|
||||
$results['action_label'],
|
||||
/* translators: accessibility text */
|
||||
__( '(opens in a new tab)', 'jetpack-connection' )
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $return;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX handler for async Site Health tests.
|
||||
*
|
||||
* @since 8.5.0
|
||||
*/
|
||||
public static function ajax_local_testing_suite() {
|
||||
check_ajax_referer( 'health-check-site-status' );
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
// @phan-suppress-next-line PhanTypeMismatchArgumentProbablyReal
|
||||
wp_send_json_error( null, null, JSON_UNESCAPED_SLASHES );
|
||||
}
|
||||
$tests = new Connection_Health_Tests();
|
||||
// @phan-suppress-next-line PhanTypeMismatchArgumentProbablyReal
|
||||
wp_send_json_success( $tests->output_results_for_core_async_site_health(), null, JSON_UNESCAPED_SLASHES );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
/**
|
||||
* A Terms of Service class for Jetpack.
|
||||
*
|
||||
* @package automattic/jetpack-connection
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack;
|
||||
|
||||
/**
|
||||
* Class Terms_Of_Service
|
||||
*
|
||||
* Helper class that is responsible for the state of agreement of the terms of service.
|
||||
*/
|
||||
class Terms_Of_Service {
|
||||
/**
|
||||
* Jetpack option name where the terms of service state is stored.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const OPTION_NAME = 'tos_agreed';
|
||||
|
||||
/**
|
||||
* Allow the site to agree to the terms of service.
|
||||
*/
|
||||
public function agree() {
|
||||
$this->set_agree();
|
||||
/**
|
||||
* Acton fired when the master user has agreed to the terms of service.
|
||||
*
|
||||
* @since 1.0.4
|
||||
* @since-jetpack 7.9.0
|
||||
*/
|
||||
do_action( 'jetpack_agreed_to_terms_of_service' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow the site to reject to the terms of service.
|
||||
*/
|
||||
public function reject() {
|
||||
$this->set_reject();
|
||||
/**
|
||||
* Acton fired when the master user has revoked their agreement to the terms of service.
|
||||
*
|
||||
* @since 1.0.4
|
||||
* @since-jetpack 7.9.1
|
||||
*/
|
||||
do_action( 'jetpack_reject_terms_of_service' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the master user has agreed to the terms of service.
|
||||
*
|
||||
* The following conditions have to be met in order to agree to the terms of service.
|
||||
* 1. The master user has gone though the connect flow.
|
||||
* 2. The site is not in dev mode.
|
||||
* 3. The master user of the site is still connected (deprecated @since 1.4.0).
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function has_agreed() {
|
||||
if ( $this->is_offline_mode() ) {
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* Before 1.4.0 we used to also check if the master user of the site is connected
|
||||
* by calling the Connection related `is_active` method.
|
||||
* As of 1.4.0 we have removed this check in order to resolve the
|
||||
* circular dependencies it was introducing to composer packages.
|
||||
*
|
||||
* @since 1.4.0
|
||||
*/
|
||||
return $this->get_raw_has_agreed();
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstracted for testing purposes.
|
||||
* Tells us if the site is in dev mode.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function is_offline_mode() {
|
||||
return ( new Status() )->is_offline_mode();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets just the Jetpack Option that contains the terms of service state.
|
||||
* Abstracted for testing purposes.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function get_raw_has_agreed() {
|
||||
return \Jetpack_Options::get_option( self::OPTION_NAME, false );
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the correct Jetpack Option to mark the that the site has agreed to the terms of service.
|
||||
* Abstracted for testing purposes.
|
||||
*/
|
||||
protected function set_agree() {
|
||||
\Jetpack_Options::update_option( self::OPTION_NAME, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the correct Jetpack Option to mark that the site has rejected the terms of service.
|
||||
* Abstracted for testing purposes.
|
||||
*/
|
||||
protected function set_reject() {
|
||||
\Jetpack_Options::update_option( self::OPTION_NAME, false );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
/**
|
||||
* The Jetpack Connection Tokens Locks class file.
|
||||
*
|
||||
* @package automattic/jetpack-connection
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Connection;
|
||||
|
||||
/**
|
||||
*
|
||||
* Jetpack Connection tokens cleanup during migration.
|
||||
* This class encapsulates plugin or tool specific code that activates token lock upon migration.
|
||||
*
|
||||
* The connection tokens are locked to the current domain.
|
||||
* If the database is imported on another site (domain name doesn't match), the tokens get removed.
|
||||
*
|
||||
* @see https://github.com/Automattic/jetpack/pull/23597
|
||||
* @see \Automattic\Jetpack\Connection\Tokens::is_locked()
|
||||
*
|
||||
* @phan-constructor-used-for-side-effects
|
||||
*/
|
||||
class Tokens_Locks {
|
||||
|
||||
/**
|
||||
* Whether the class has been initialized.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private static $is_initialized = false;
|
||||
|
||||
/**
|
||||
* Run the initializers if they haven't been run already.
|
||||
*/
|
||||
public function __construct() {
|
||||
if ( static::$is_initialized ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->init_aiowpm();
|
||||
|
||||
static::$is_initialized = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the token lock for AIOWPM plugin export.
|
||||
*
|
||||
* @param array $params The filter parameters.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function aiowpm_set_lock( $params ) {
|
||||
( new Tokens() )->set_lock();
|
||||
return $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the token lock for AIOWPM plugin export.
|
||||
*
|
||||
* @param array $params The filter parameters.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function aiowpm_remove_lock( $params ) {
|
||||
( new Tokens() )->remove_lock();
|
||||
return $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the All-in-One-WP-Migration plugin hooks.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function init_aiowpm() {
|
||||
add_filter( 'ai1wm_export', array( $this, 'aiowpm_set_lock' ), 180 );
|
||||
add_filter( 'ai1wm_export', array( $this, 'aiowpm_remove_lock' ), 250 );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,683 @@
|
||||
<?php
|
||||
/**
|
||||
* The Jetpack Connection Tokens class file.
|
||||
*
|
||||
* @package automattic/jetpack-connection
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Connection;
|
||||
|
||||
use Automattic\Jetpack\Constants;
|
||||
use Automattic\Jetpack\Roles;
|
||||
use DateInterval;
|
||||
use DateTime;
|
||||
use Exception;
|
||||
use Jetpack_Options;
|
||||
use WP_Error;
|
||||
|
||||
/**
|
||||
* The Jetpack Connection Tokens class that manages tokens.
|
||||
*/
|
||||
class Tokens {
|
||||
|
||||
const MAGIC_NORMAL_TOKEN_KEY = ';normal;';
|
||||
|
||||
/**
|
||||
* Datetime format.
|
||||
*/
|
||||
const DATE_FORMAT_ATOM = 'Y-m-d\TH:i:sP';
|
||||
|
||||
/**
|
||||
* Deletes all connection tokens and transients from the local Jetpack site.
|
||||
*/
|
||||
public function delete_all() {
|
||||
Jetpack_Options::delete_option(
|
||||
array(
|
||||
'blog_token',
|
||||
'user_token',
|
||||
'user_tokens',
|
||||
)
|
||||
);
|
||||
|
||||
$this->remove_lock();
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the API request to validate the blog and user tokens.
|
||||
*
|
||||
* @param int|null $user_id ID of the user we need to validate token for. Current user's ID by default.
|
||||
*
|
||||
* @return array|false|WP_Error The API response: `array( 'blog_token_is_healthy' => true|false, 'user_token_is_healthy' => true|false )`.
|
||||
*/
|
||||
public function validate( $user_id = null ) {
|
||||
$blog_id = Jetpack_Options::get_option( 'id' );
|
||||
if ( ! $blog_id ) {
|
||||
return new WP_Error( 'site_not_registered', 'Site not registered.' );
|
||||
}
|
||||
$url = sprintf(
|
||||
'%s/%s/v%s/%s',
|
||||
Constants::get_constant( 'JETPACK__WPCOM_JSON_API_BASE' ),
|
||||
'wpcom',
|
||||
'2',
|
||||
'sites/' . $blog_id . '/jetpack-token-health'
|
||||
);
|
||||
|
||||
$user_token = $this->get_access_token( $user_id ? $user_id : get_current_user_id() );
|
||||
$blog_token = $this->get_access_token();
|
||||
|
||||
// Cannot validate non-existent tokens.
|
||||
if ( false === $user_token || false === $blog_token ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$method = 'POST';
|
||||
$body = array(
|
||||
'user_token' => $this->get_signed_token( $user_token ),
|
||||
'blog_token' => $this->get_signed_token( $blog_token ),
|
||||
);
|
||||
$response = Client::_wp_remote_request( $url, compact( 'body', 'method' ) );
|
||||
|
||||
if ( is_wp_error( $response ) || ! wp_remote_retrieve_body( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$body = json_decode( wp_remote_retrieve_body( $response ), true );
|
||||
|
||||
return $body ? $body : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the API request to validate only the blog.
|
||||
*
|
||||
* @return bool|WP_Error Boolean with the test result. WP_Error if test cannot be performed.
|
||||
*/
|
||||
public function validate_blog_token() {
|
||||
$blog_id = Jetpack_Options::get_option( 'id' );
|
||||
if ( ! $blog_id ) {
|
||||
return new WP_Error( 'site_not_registered', 'Site not registered.' );
|
||||
}
|
||||
$url = sprintf(
|
||||
'%s/%s/v%s/%s',
|
||||
Constants::get_constant( 'JETPACK__WPCOM_JSON_API_BASE' ),
|
||||
'wpcom',
|
||||
'2',
|
||||
'sites/' . $blog_id . '/jetpack-token-health/blog'
|
||||
);
|
||||
|
||||
$method = 'GET';
|
||||
$response = Client::remote_request( compact( 'url', 'method' ) );
|
||||
|
||||
if ( is_wp_error( $response ) || ! wp_remote_retrieve_body( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$body = json_decode( wp_remote_retrieve_body( $response ), true );
|
||||
|
||||
return is_array( $body ) && isset( $body['is_healthy'] ) && true === $body['is_healthy'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains the auth token.
|
||||
*
|
||||
* @param array $data The request data.
|
||||
* @param string $token_api_url The URL of the Jetpack "token" API.
|
||||
* @return object|WP_Error Returns the auth token on success.
|
||||
* Returns a WP_Error on failure.
|
||||
*/
|
||||
public function get( $data, $token_api_url ) {
|
||||
$roles = new Roles();
|
||||
$role = $roles->translate_current_user_to_role();
|
||||
|
||||
if ( ! $role ) {
|
||||
return new WP_Error( 'role', __( 'An administrator for this blog must set up the Jetpack connection.', 'jetpack-connection' ) );
|
||||
}
|
||||
|
||||
$client_secret = $this->get_access_token();
|
||||
if ( ! $client_secret ) {
|
||||
return new WP_Error( 'client_secret', __( 'You need to register your Jetpack before connecting it.', 'jetpack-connection' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the URL of the first time the user gets redirected back to your site for connection
|
||||
* data processing.
|
||||
*
|
||||
* @since 1.7.0
|
||||
* @since-jetpack 8.0.0
|
||||
*
|
||||
* @param string $redirect_url Defaults to the site admin URL.
|
||||
*/
|
||||
$processing_url = apply_filters( 'jetpack_token_processing_url', admin_url( 'admin.php' ) );
|
||||
|
||||
$redirect = isset( $data['redirect'] ) ? esc_url_raw( (string) $data['redirect'] ) : '';
|
||||
|
||||
/**
|
||||
* Filter the URL to redirect the user back to when the authentication process
|
||||
* is complete.
|
||||
*
|
||||
* @since 1.7.0
|
||||
* @since-jetpack 8.0.0
|
||||
*
|
||||
* @param string $redirect_url Defaults to the site URL.
|
||||
*/
|
||||
$redirect = apply_filters( 'jetpack_token_redirect_url', $redirect );
|
||||
|
||||
$redirect_uri = ( 'calypso' === $data['auth_type'] )
|
||||
? $data['redirect_uri']
|
||||
: add_query_arg(
|
||||
array(
|
||||
'handler' => 'jetpack-connection-webhooks',
|
||||
'action' => 'authorize',
|
||||
'_wpnonce' => wp_create_nonce( "jetpack-authorize_{$role}_{$redirect}" ),
|
||||
'redirect' => $redirect ? rawurlencode( $redirect ) : false,
|
||||
),
|
||||
esc_url( $processing_url )
|
||||
);
|
||||
|
||||
/**
|
||||
* Filters the token request data.
|
||||
*
|
||||
* @since 1.7.0
|
||||
* @since-jetpack 8.0.0
|
||||
*
|
||||
* @param array $request_data request data.
|
||||
*/
|
||||
$body = apply_filters(
|
||||
'jetpack_token_request_body',
|
||||
array(
|
||||
'client_id' => Jetpack_Options::get_option( 'id' ),
|
||||
'client_secret' => $client_secret->secret,
|
||||
'grant_type' => 'authorization_code',
|
||||
'code' => $data['code'],
|
||||
'redirect_uri' => $redirect_uri,
|
||||
)
|
||||
);
|
||||
|
||||
$args = array(
|
||||
'method' => 'POST',
|
||||
'body' => $body,
|
||||
'headers' => array(
|
||||
'Accept' => 'application/json',
|
||||
),
|
||||
);
|
||||
add_filter( 'http_request_timeout', array( $this, 'return_30' ), PHP_INT_MAX - 1 );
|
||||
$response = Client::_wp_remote_request( $token_api_url, $args );
|
||||
remove_filter( 'http_request_timeout', array( $this, 'return_30' ), PHP_INT_MAX - 1 );
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return new WP_Error( 'token_http_request_failed', $response->get_error_message() );
|
||||
}
|
||||
|
||||
$code = wp_remote_retrieve_response_code( $response );
|
||||
$entity = wp_remote_retrieve_body( $response );
|
||||
|
||||
if ( $entity ) {
|
||||
$json = json_decode( $entity );
|
||||
} else {
|
||||
$json = false;
|
||||
}
|
||||
|
||||
if ( 200 !== $code || ! empty( $json->error ) ) {
|
||||
if ( empty( $json->error ) ) {
|
||||
return new WP_Error( 'unknown', '', $code );
|
||||
}
|
||||
|
||||
/* translators: Error description string. */
|
||||
$error_description = isset( $json->error_description ) ? sprintf( __( 'Error Details: %s', 'jetpack-connection' ), (string) $json->error_description ) : '';
|
||||
|
||||
return new WP_Error( (string) $json->error, $error_description, $code );
|
||||
}
|
||||
|
||||
if ( empty( $json->access_token ) || ! is_scalar( $json->access_token ) ) {
|
||||
return new WP_Error( 'access_token', '', $code );
|
||||
}
|
||||
|
||||
if ( empty( $json->token_type ) || 'X_JETPACK' !== strtoupper( $json->token_type ) ) {
|
||||
return new WP_Error( 'token_type', '', $code );
|
||||
}
|
||||
|
||||
if ( empty( $json->scope ) ) {
|
||||
return new WP_Error( 'scope', 'No Scope', $code );
|
||||
}
|
||||
|
||||
// TODO: get rid of the error silencer.
|
||||
// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
|
||||
@list( $role, $hmac ) = explode( ':', $json->scope );
|
||||
if ( empty( $role ) || empty( $hmac ) ) {
|
||||
return new WP_Error( 'scope', 'Malformed Scope', $code );
|
||||
}
|
||||
|
||||
if ( $this->sign_role( $role ) !== $json->scope ) {
|
||||
return new WP_Error( 'scope', 'Invalid Scope', $code );
|
||||
}
|
||||
|
||||
$cap = $roles->translate_role_to_cap( $role );
|
||||
if ( ! $cap ) {
|
||||
return new WP_Error( 'scope', 'No Cap', $code );
|
||||
}
|
||||
|
||||
if ( ! current_user_can( $cap ) ) {
|
||||
return new WP_Error( 'scope', 'current_user_cannot', $code );
|
||||
}
|
||||
|
||||
return (string) $json->access_token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enters a user token into the user_tokens option
|
||||
*
|
||||
* @param int $user_id The user id.
|
||||
* @param string $token The user token.
|
||||
* @param bool $is_master_user Whether the user is the master user.
|
||||
* @return bool
|
||||
*/
|
||||
public function update_user_token( $user_id, $token, $is_master_user ) {
|
||||
// Not designed for concurrent updates.
|
||||
$user_tokens = $this->get_user_tokens();
|
||||
if ( ! is_array( $user_tokens ) ) {
|
||||
$user_tokens = array();
|
||||
}
|
||||
$user_tokens[ $user_id ] = $token;
|
||||
if ( $is_master_user ) {
|
||||
$master_user = $user_id;
|
||||
$options = compact( 'user_tokens', 'master_user' );
|
||||
} else {
|
||||
$options = compact( 'user_tokens' );
|
||||
}
|
||||
return Jetpack_Options::update_options( $options );
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign a user role with the master access token.
|
||||
* If not specified, will default to the current user.
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @param string $role User role.
|
||||
* @param int $user_id ID of the user.
|
||||
* @return string Signed user role.
|
||||
*/
|
||||
public function sign_role( $role, $user_id = null ) {
|
||||
if ( empty( $user_id ) ) {
|
||||
$user_id = (int) get_current_user_id();
|
||||
}
|
||||
|
||||
if ( ! $user_id ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$token = $this->get_access_token();
|
||||
if ( ! $token || is_wp_error( $token ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $role . ':' . hash_hmac( 'md5', "{$role}|{$user_id}", $token->secret );
|
||||
}
|
||||
|
||||
/**
|
||||
* Increases the request timeout value to 30 seconds.
|
||||
*
|
||||
* @return int Returns 30.
|
||||
*/
|
||||
public function return_30() {
|
||||
return 30;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the requested token.
|
||||
*
|
||||
* Tokens are one of two types:
|
||||
* 1. Blog Tokens: These are the "main" tokens. Each site typically has one Blog Token,
|
||||
* though some sites can have multiple "Special" Blog Tokens (see below). These tokens
|
||||
* are not associated with a user account. They represent the site's connection with
|
||||
* the Jetpack servers.
|
||||
* 2. User Tokens: These are "sub-"tokens. Each connected user account has one User Token.
|
||||
*
|
||||
* All tokens look like "{$token_key}.{$private}". $token_key is a public ID for the
|
||||
* token, and $private is a secret that should never be displayed anywhere or sent
|
||||
* over the network; it's used only for signing things.
|
||||
*
|
||||
* Blog Tokens can be "Normal" or "Special".
|
||||
* * Normal: The result of a normal connection flow. They look like
|
||||
* "{$random_string_1}.{$random_string_2}"
|
||||
* That is, $token_key and $private are both random strings.
|
||||
* Sites only have one Normal Blog Token. Normal Tokens are found in either
|
||||
* Jetpack_Options::get_option( 'blog_token' ) (usual) or the JETPACK_BLOG_TOKEN
|
||||
* constant (rare).
|
||||
* * Special: A connection token for sites that have gone through an alternative
|
||||
* connection flow. They look like:
|
||||
* ";{$special_id}{$special_version};{$wpcom_blog_id};.{$random_string}"
|
||||
* That is, $private is a random string and $token_key has a special structure with
|
||||
* lots of semicolons.
|
||||
* Most sites have zero Special Blog Tokens. Special tokens are only found in the
|
||||
* JETPACK_BLOG_TOKEN constant.
|
||||
*
|
||||
* In particular, note that Normal Blog Tokens never start with ";" and that
|
||||
* Special Blog Tokens always do.
|
||||
*
|
||||
* When searching for a matching Blog Tokens, Blog Tokens are examined in the following
|
||||
* order:
|
||||
* 1. Defined Special Blog Tokens (via the JETPACK_BLOG_TOKEN constant)
|
||||
* 2. Stored Normal Tokens (via Jetpack_Options::get_option( 'blog_token' ))
|
||||
* 3. Defined Normal Tokens (via the JETPACK_BLOG_TOKEN constant)
|
||||
*
|
||||
* @param int|false $user_id false: Return the Blog Token. int: Return that user's User Token.
|
||||
* @param string|false $token_key If provided, check that the token matches the provided input.
|
||||
* @param bool|true $suppress_errors If true, return a falsy value when the token isn't found; When false, return a descriptive WP_Error when the token isn't found.
|
||||
*
|
||||
* @return object|false|WP_Error
|
||||
*/
|
||||
public function get_access_token( $user_id = false, $token_key = false, $suppress_errors = true ) {
|
||||
if ( $this->is_locked() ) {
|
||||
$this->delete_all();
|
||||
return false;
|
||||
}
|
||||
|
||||
$possible_special_tokens = array();
|
||||
$possible_normal_tokens = array();
|
||||
$user_tokens = $this->get_user_tokens();
|
||||
|
||||
if ( $user_id ) {
|
||||
if ( ! $user_tokens ) {
|
||||
return $suppress_errors ? false : new WP_Error( 'no_user_tokens', __( 'No user tokens found', 'jetpack-connection' ) );
|
||||
}
|
||||
if ( true === $user_id ) { // connection owner.
|
||||
$user_id = Jetpack_Options::get_option( 'master_user' );
|
||||
if ( ! $user_id ) {
|
||||
return $suppress_errors ? false : new WP_Error( 'empty_master_user_option', __( 'No primary user defined', 'jetpack-connection' ) );
|
||||
}
|
||||
}
|
||||
if ( ! isset( $user_tokens[ $user_id ] ) || ! $user_tokens[ $user_id ] ) {
|
||||
// translators: %s is the user ID.
|
||||
return $suppress_errors ? false : new WP_Error( 'no_token_for_user', sprintf( __( 'No token for user %d', 'jetpack-connection' ), $user_id ) );
|
||||
}
|
||||
$user_token_chunks = explode( '.', $user_tokens[ $user_id ] );
|
||||
if ( empty( $user_token_chunks[1] ) || empty( $user_token_chunks[2] ) ) {
|
||||
// translators: %s is the user ID.
|
||||
return $suppress_errors ? false : new WP_Error( 'token_malformed', sprintf( __( 'Token for user %d is malformed', 'jetpack-connection' ), $user_id ) );
|
||||
}
|
||||
if ( $user_token_chunks[2] !== (string) $user_id ) {
|
||||
// translators: %1$d is the ID of the requested user. %2$d is the user ID found in the token.
|
||||
return $suppress_errors ? false : new WP_Error( 'user_id_mismatch', sprintf( __( 'Requesting user_id %1$d does not match token user_id %2$d', 'jetpack-connection' ), $user_id, $user_token_chunks[2] ) );
|
||||
}
|
||||
$possible_normal_tokens[] = "{$user_token_chunks[0]}.{$user_token_chunks[1]}";
|
||||
} else {
|
||||
$stored_blog_token = Jetpack_Options::get_option( 'blog_token' );
|
||||
if ( $stored_blog_token ) {
|
||||
$possible_normal_tokens[] = $stored_blog_token;
|
||||
}
|
||||
|
||||
$defined_tokens_string = Constants::get_constant( 'JETPACK_BLOG_TOKEN' );
|
||||
|
||||
if ( $defined_tokens_string ) {
|
||||
$defined_tokens = explode( ',', $defined_tokens_string );
|
||||
foreach ( $defined_tokens as $defined_token ) {
|
||||
if ( ';' === $defined_token[0] ) {
|
||||
$possible_special_tokens[] = $defined_token;
|
||||
} else {
|
||||
$possible_normal_tokens[] = $defined_token;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( self::MAGIC_NORMAL_TOKEN_KEY === $token_key ) {
|
||||
$possible_tokens = $possible_normal_tokens;
|
||||
} else {
|
||||
$possible_tokens = array_merge( $possible_special_tokens, $possible_normal_tokens );
|
||||
}
|
||||
|
||||
if ( ! $possible_tokens ) {
|
||||
// If no user tokens were found, it would have failed earlier, so this is about blog token.
|
||||
return $suppress_errors ? false : new WP_Error( 'no_possible_tokens', __( 'No blog token found', 'jetpack-connection' ) );
|
||||
}
|
||||
|
||||
$valid_token = false;
|
||||
|
||||
if ( false === $token_key ) {
|
||||
// Use first token.
|
||||
$valid_token = $possible_tokens[0];
|
||||
} elseif ( self::MAGIC_NORMAL_TOKEN_KEY === $token_key ) {
|
||||
// Use first normal token.
|
||||
$valid_token = $possible_tokens[0]; // $possible_tokens only contains normal tokens because of earlier check.
|
||||
} else {
|
||||
// Use the token matching $token_key or false if none.
|
||||
// Ensure we check the full key.
|
||||
$token_check = rtrim( $token_key, '.' ) . '.';
|
||||
|
||||
foreach ( $possible_tokens as $possible_token ) {
|
||||
if ( hash_equals( substr( $possible_token, 0, strlen( $token_check ) ), $token_check ) ) {
|
||||
$valid_token = $possible_token;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! $valid_token ) {
|
||||
if ( $user_id ) {
|
||||
// translators: %d is the user ID.
|
||||
return $suppress_errors ? false : new WP_Error( 'no_valid_user_token', sprintf( __( 'Invalid token for user %d', 'jetpack-connection' ), $user_id ) );
|
||||
} else {
|
||||
return $suppress_errors ? false : new WP_Error( 'no_valid_blog_token', __( 'Invalid blog token', 'jetpack-connection' ) );
|
||||
}
|
||||
}
|
||||
|
||||
return (object) array(
|
||||
'secret' => $valid_token,
|
||||
'external_user_id' => (int) $user_id,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the blog token to a new value.
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @param string $token the new blog token value.
|
||||
* @return Boolean Whether updating the blog token was successful.
|
||||
*/
|
||||
public function update_blog_token( $token ) {
|
||||
return Jetpack_Options::update_option( 'blog_token', $token );
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlinks the current user from the linked WordPress.com user.
|
||||
*
|
||||
* @access public
|
||||
* @static
|
||||
*
|
||||
* @todo Refactor to properly load the XMLRPC client independently.
|
||||
*
|
||||
* @param int $user_id The user identifier.
|
||||
*
|
||||
* @return bool Whether the disconnection of the user was successful.
|
||||
*/
|
||||
public function disconnect_user( $user_id ) {
|
||||
$tokens = $this->get_user_tokens();
|
||||
if ( ! $tokens ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ! isset( $tokens[ $user_id ] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
unset( $tokens[ $user_id ] );
|
||||
|
||||
$this->update_user_tokens( $tokens );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of user_id's that have user tokens for communicating with wpcom.
|
||||
* Able to select by specific capability.
|
||||
*
|
||||
* @deprecated 1.30.0
|
||||
* @see Manager::get_connected_users
|
||||
*
|
||||
* @param string $capability The capability of the user.
|
||||
* @param int|null $limit How many connected users to get before returning.
|
||||
* @return array Array of WP_User objects if found.
|
||||
*/
|
||||
public function get_connected_users( $capability = 'any', $limit = null ) {
|
||||
_deprecated_function( __METHOD__, '1.30.0' );
|
||||
return ( new Manager( 'jetpack' ) )->get_connected_users( $capability, $limit );
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a signed token.
|
||||
*
|
||||
* @param object $token the token.
|
||||
* @return WP_Error|string a signed token
|
||||
*/
|
||||
public function get_signed_token( $token ) {
|
||||
if ( ! isset( $token->secret ) || empty( $token->secret ) ) {
|
||||
return new WP_Error( 'invalid_token' );
|
||||
}
|
||||
|
||||
list( $token_key, $token_secret ) = explode( '.', $token->secret );
|
||||
|
||||
$token_key = sprintf(
|
||||
'%s:%d:%d',
|
||||
$token_key,
|
||||
Constants::get_constant( 'JETPACK__API_VERSION' ),
|
||||
$token->external_user_id
|
||||
);
|
||||
|
||||
$timestamp = time();
|
||||
|
||||
if ( function_exists( 'wp_generate_password' ) ) {
|
||||
$nonce = wp_generate_password( 10, false );
|
||||
} else {
|
||||
$nonce = substr( sha1( (string) wp_rand( 0, 1000000 ) ), 0, 10 );
|
||||
}
|
||||
|
||||
$normalized_request_string = implode(
|
||||
"\n",
|
||||
array(
|
||||
$token_key,
|
||||
$timestamp,
|
||||
$nonce,
|
||||
)
|
||||
) . "\n";
|
||||
|
||||
// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
|
||||
$signature = base64_encode( hash_hmac( 'sha1', $normalized_request_string, $token_secret, true ) );
|
||||
|
||||
$auth = array(
|
||||
'token' => $token_key,
|
||||
'timestamp' => $timestamp,
|
||||
'nonce' => $nonce,
|
||||
'signature' => $signature,
|
||||
);
|
||||
|
||||
$header_pieces = array();
|
||||
foreach ( $auth as $key => $value ) {
|
||||
$header_pieces[] = sprintf( '%s="%s"', $key, $value );
|
||||
}
|
||||
|
||||
return implode( ' ', $header_pieces );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the list of user tokens
|
||||
*
|
||||
* @since 1.30.0
|
||||
*
|
||||
* @return bool|array An array of user tokens where keys are user IDs and values are the tokens. False if no user token is found.
|
||||
*/
|
||||
public function get_user_tokens() {
|
||||
return Jetpack_Options::get_option( 'user_tokens' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the option that stores the user tokens
|
||||
*
|
||||
* @since 1.30.0
|
||||
*
|
||||
* @param array $tokens An array of user tokens where keys are user IDs and values are the tokens.
|
||||
* @return bool Was the option successfully updated?
|
||||
*
|
||||
* @todo add validate the input.
|
||||
*/
|
||||
public function update_user_tokens( $tokens ) {
|
||||
return Jetpack_Options::update_option( 'user_tokens', $tokens );
|
||||
}
|
||||
|
||||
/**
|
||||
* Lock the tokens to the current site URL.
|
||||
*
|
||||
* @param int $timespan How long the tokens should be locked, in seconds.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function set_lock( $timespan = HOUR_IN_SECONDS ) {
|
||||
try {
|
||||
$expires = ( new DateTime() )->add( DateInterval::createFromDateString( (int) $timespan . ' seconds' ) );
|
||||
} catch ( Exception $e ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
|
||||
return Jetpack_Options::update_option( 'token_lock', $expires->format( static::DATE_FORMAT_ATOM ) . '|||' . base64_encode( Urls::site_url() ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the site lock from tokens.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function remove_lock() {
|
||||
Jetpack_Options::delete_option( 'token_lock' );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the domain is locked, remove the lock if needed.
|
||||
* Possible scenarios:
|
||||
* - lock expired, site URL matches the lock URL: remove the lock, return false.
|
||||
* - lock not expired, site URL matches the lock URL: return false.
|
||||
* - site URL does not match the lock URL (expiration date is ignored): return true, do not remove the lock.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_locked() {
|
||||
$the_lock = Jetpack_Options::get_option( 'token_lock' );
|
||||
if ( ! $the_lock ) {
|
||||
// Not locked.
|
||||
return false;
|
||||
}
|
||||
|
||||
$the_lock = explode( '|||', $the_lock, 2 );
|
||||
if ( count( $the_lock ) !== 2 ) {
|
||||
// Something's wrong with the lock.
|
||||
$this->remove_lock();
|
||||
return false;
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
|
||||
$locked_site_url = base64_decode( $the_lock[1] );
|
||||
$expires = $the_lock[0];
|
||||
|
||||
$expiration_date = DateTime::createFromFormat( static::DATE_FORMAT_ATOM, $expires );
|
||||
if ( false === $expiration_date || ! $locked_site_url ) {
|
||||
// Something's wrong with the lock.
|
||||
$this->remove_lock();
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( Urls::site_url() === $locked_site_url ) {
|
||||
if ( new DateTime() > $expiration_date ) {
|
||||
// Site lock expired.
|
||||
// Site URL matches, removing the lock.
|
||||
$this->remove_lock();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Site URL doesn't match.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,325 @@
|
||||
<?php
|
||||
/**
|
||||
* Nosara Tracks for Jetpack
|
||||
*
|
||||
* @package automattic/jetpack-connection
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack;
|
||||
|
||||
/**
|
||||
* The Tracking class, used to record events in wpcom
|
||||
*/
|
||||
class Tracking {
|
||||
/**
|
||||
* The assets version.
|
||||
*
|
||||
* @since 1.13.1
|
||||
* @deprecated since 1.40.1
|
||||
*
|
||||
* @var string Assets version.
|
||||
*/
|
||||
const ASSETS_VERSION = '1.0.0';
|
||||
|
||||
/**
|
||||
* Slug of the product that we are tracking.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $product_name;
|
||||
|
||||
/**
|
||||
* Connection manager object.
|
||||
*
|
||||
* @var Object
|
||||
*/
|
||||
private $connection;
|
||||
|
||||
/**
|
||||
* Creates the Tracking object.
|
||||
*
|
||||
* @param string $product_name the slug of the product that we are tracking.
|
||||
* @param \Automattic\Jetpack\Connection\Manager $connection the connection manager object.
|
||||
*/
|
||||
public function __construct( $product_name = 'jetpack', $connection = null ) {
|
||||
$this->product_name = $product_name;
|
||||
$this->connection = $connection;
|
||||
if ( $this->connection === null ) {
|
||||
// TODO We should always pass a Connection.
|
||||
$this->connection = new Connection\Manager();
|
||||
}
|
||||
|
||||
if ( ! did_action( 'jetpack_set_tracks_ajax_hook' ) ) {
|
||||
add_action( 'wp_ajax_jetpack_tracks', array( $this, 'ajax_tracks' ) );
|
||||
|
||||
/**
|
||||
* Fires when the Tracking::ajax_tracks() callback has been hooked to the
|
||||
* wp_ajax_jetpack_tracks action. This action is used to ensure that
|
||||
* the callback is hooked only once.
|
||||
*
|
||||
* @since 1.13.11
|
||||
*/
|
||||
do_action( 'jetpack_set_tracks_ajax_hook' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Universal method for for all tracking events triggered via the JavaScript client.
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function ajax_tracks() {
|
||||
// Check for nonce.
|
||||
if (
|
||||
empty( $_REQUEST['tracksNonce'] )
|
||||
|| ! wp_verify_nonce( $_REQUEST['tracksNonce'], 'jp-tracks-ajax-nonce' ) // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- WP core doesn't pre-sanitize nonces either.
|
||||
) {
|
||||
wp_send_json_error(
|
||||
__( 'You aren’t authorized to do that.', 'jetpack-connection' ),
|
||||
403,
|
||||
JSON_UNESCAPED_SLASHES
|
||||
);
|
||||
}
|
||||
|
||||
if ( ! isset( $_REQUEST['tracksEventName'] ) || ! isset( $_REQUEST['tracksEventType'] ) ) {
|
||||
wp_send_json_error(
|
||||
__( 'No valid event name or type.', 'jetpack-connection' ),
|
||||
403,
|
||||
JSON_UNESCAPED_SLASHES
|
||||
);
|
||||
}
|
||||
|
||||
$tracks_data = array();
|
||||
if ( 'click' === $_REQUEST['tracksEventType'] && isset( $_REQUEST['tracksEventProp'] ) ) {
|
||||
if ( is_array( $_REQUEST['tracksEventProp'] ) ) {
|
||||
$tracks_data = array_map( 'filter_var', wp_unslash( $_REQUEST['tracksEventProp'] ) );
|
||||
} else {
|
||||
$tracks_data = array( 'clicked' => filter_var( wp_unslash( $_REQUEST['tracksEventProp'] ) ) );
|
||||
}
|
||||
}
|
||||
|
||||
$this->record_user_event( filter_var( wp_unslash( $_REQUEST['tracksEventName'] ) ), $tracks_data, null, false );
|
||||
|
||||
wp_send_json_success( null, null, JSON_UNESCAPED_SLASHES );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register script necessary for tracking.
|
||||
*
|
||||
* @param boolean $enqueue Also enqueue? defaults to false.
|
||||
*/
|
||||
public static function register_tracks_functions_scripts( $enqueue = false ) {
|
||||
|
||||
// Register jp-tracks as it is a dependency.
|
||||
wp_register_script(
|
||||
'jp-tracks',
|
||||
'//stats.wp.com/w.js',
|
||||
array(),
|
||||
gmdate( 'YW' ),
|
||||
true
|
||||
);
|
||||
|
||||
Assets::register_script(
|
||||
'jp-tracks-functions',
|
||||
'../dist/tracks-callables.js',
|
||||
__FILE__,
|
||||
array(
|
||||
'dependencies' => array( 'jp-tracks' ),
|
||||
'enqueue' => $enqueue,
|
||||
'in_footer' => true,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue script necessary for tracking.
|
||||
*/
|
||||
public function enqueue_tracks_scripts() {
|
||||
Assets::register_script(
|
||||
'jptracks',
|
||||
'../dist/tracks-ajax.js',
|
||||
__FILE__,
|
||||
array(
|
||||
'dependencies' => array( 'jquery' ),
|
||||
'enqueue' => true,
|
||||
'in_footer' => true,
|
||||
)
|
||||
);
|
||||
|
||||
wp_localize_script(
|
||||
'jptracks',
|
||||
'jpTracksAJAX',
|
||||
array(
|
||||
'ajaxurl' => admin_url( 'admin-ajax.php' ),
|
||||
'jpTracksAJAX_nonce' => wp_create_nonce( 'jp-tracks-ajax-nonce' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an event in Tracks.
|
||||
*
|
||||
* @param string $event_type Type of the event.
|
||||
* @param array $data Data to send with the event.
|
||||
* @param mixed $user Username, user_id, or WP_User object.
|
||||
* @param bool $use_product_prefix Whether to use the object's product name as a prefix to the event type. If
|
||||
* set to false, the prefix will be 'jetpack_'.
|
||||
*/
|
||||
public function record_user_event( $event_type, $data = array(), $user = null, $use_product_prefix = true ) {
|
||||
if ( ! $user ) {
|
||||
$user = wp_get_current_user();
|
||||
}
|
||||
$site_url = get_option( 'siteurl' );
|
||||
|
||||
$data['_via_ua'] = isset( $_SERVER['HTTP_USER_AGENT'] ) ? filter_var( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : '';
|
||||
$data['_via_ip'] = isset( $_SERVER['REMOTE_ADDR'] ) ? filter_var( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : '';
|
||||
$data['_lg'] = isset( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ? filter_var( wp_unslash( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ) : '';
|
||||
$data['blog_url'] = $site_url;
|
||||
$data['blog_id'] = \Jetpack_Options::get_option( 'id' );
|
||||
|
||||
// Top level events should not be namespaced.
|
||||
if ( '_aliasUser' !== $event_type ) {
|
||||
$prefix = $use_product_prefix ? $this->product_name : 'jetpack';
|
||||
$event_type = $prefix . '_' . $event_type;
|
||||
}
|
||||
|
||||
$data['jetpack_version'] = defined( 'JETPACK__VERSION' ) ? JETPACK__VERSION : '0';
|
||||
|
||||
return $this->tracks_record_event( $user, $event_type, $data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Record an event in Tracks - this is the preferred way to record events from PHP.
|
||||
*
|
||||
* @param mixed $user username, user_id, or WP_User object.
|
||||
* @param string $event_name The name of the event.
|
||||
* @param array $properties Custom properties to send with the event.
|
||||
* @param int $event_timestamp_millis The time in millis since 1970-01-01 00:00:00 when the event occurred.
|
||||
*
|
||||
* @return bool true for success | \WP_Error if the event pixel could not be fired
|
||||
*/
|
||||
public function tracks_record_event( $user, $event_name, $properties = array(), $event_timestamp_millis = false ) {
|
||||
|
||||
// We don't want to track user events during unit tests/CI runs.
|
||||
if ( $user instanceof \WP_User && 'wptests_capabilities' === $user->cap_key ) {
|
||||
return false;
|
||||
}
|
||||
$terms_of_service = new Terms_Of_Service();
|
||||
$status = new Status();
|
||||
// Don't track users who have not agreed to our TOS.
|
||||
if ( ! $this->should_enable_tracking( $terms_of_service, $status ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$event_obj = $this->tracks_build_event_obj( $user, $event_name, $properties, $event_timestamp_millis );
|
||||
|
||||
if ( is_wp_error( $event_obj->error ) ) {
|
||||
return $event_obj->error;
|
||||
}
|
||||
|
||||
return $event_obj->record();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether tracking should be enabled.
|
||||
*
|
||||
* @param \Automattic\Jetpack\Terms_Of_Service $terms_of_service A Terms_Of_Service object.
|
||||
* @param \Automattic\Jetpack\Status $status A Status object.
|
||||
*
|
||||
* @return boolean True if tracking should be enabled, else false.
|
||||
*/
|
||||
public function should_enable_tracking( $terms_of_service, $status ) {
|
||||
if ( $status->is_offline_mode() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $terms_of_service->has_agreed() || $this->connection->is_user_connected();
|
||||
}
|
||||
|
||||
/**
|
||||
* Procedurally build a Tracks Event Object.
|
||||
* NOTE: Use this only when the simpler Automattic\Jetpack\Tracking->jetpack_tracks_record_event() function won't work for you.
|
||||
*
|
||||
* @param \WP_User $user WP_User object.
|
||||
* @param string $event_name The name of the event.
|
||||
* @param array $properties Custom properties to send with the event.
|
||||
* @param int $event_timestamp_millis The time in millis since 1970-01-01 00:00:00 when the event occurred.
|
||||
*
|
||||
* @return \Jetpack_Tracks_Event|\WP_Error
|
||||
*/
|
||||
private function tracks_build_event_obj( $user, $event_name, $properties = array(), $event_timestamp_millis = false ) {
|
||||
$identity = $this->tracks_get_identity( $user->ID );
|
||||
|
||||
$properties['user_lang'] = $user->get( 'WPLANG' );
|
||||
|
||||
$blog_details = array(
|
||||
'blog_lang' => $properties['blog_lang'] ?? get_bloginfo( 'language' ),
|
||||
'blog_id' => \Jetpack_Options::get_option( 'id' ),
|
||||
);
|
||||
|
||||
$timestamp = ( false !== $event_timestamp_millis ) ? $event_timestamp_millis : round( microtime( true ) * 1000 );
|
||||
$timestamp_string = is_string( $timestamp ) ? $timestamp : number_format( $timestamp, 0, '', '' );
|
||||
|
||||
return new \Jetpack_Tracks_Event(
|
||||
array_merge(
|
||||
$blog_details,
|
||||
(array) $properties,
|
||||
$identity,
|
||||
array(
|
||||
'_en' => $event_name,
|
||||
'_ts' => $timestamp_string,
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the identity to send to tracks.
|
||||
*
|
||||
* @param int $user_id The user id of the local user.
|
||||
*
|
||||
* @return array $identity
|
||||
*/
|
||||
public function tracks_get_identity( $user_id ) {
|
||||
|
||||
// Meta is set, and user is still connected. Use WPCOM ID.
|
||||
$wpcom_id = get_user_meta( $user_id, 'jetpack_tracks_wpcom_id', true );
|
||||
if ( $wpcom_id && is_string( $wpcom_id ) && $this->connection->is_user_connected( $user_id ) ) {
|
||||
return array(
|
||||
'_ut' => 'wpcom:user_id',
|
||||
'_ui' => $wpcom_id,
|
||||
);
|
||||
}
|
||||
|
||||
// User is connected, but no meta is set yet. Use WPCOM ID and set meta.
|
||||
if ( $this->connection->is_user_connected( $user_id ) ) {
|
||||
$wpcom_user_data = $this->connection->get_connected_user_data( $user_id );
|
||||
$wpcom_id = $wpcom_user_data['ID'] ?? null;
|
||||
|
||||
if ( is_string( $wpcom_id ) ) {
|
||||
update_user_meta( $user_id, 'jetpack_tracks_wpcom_id', $wpcom_id );
|
||||
|
||||
return array(
|
||||
'_ut' => 'wpcom:user_id',
|
||||
'_ui' => $wpcom_id,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// User isn't linked at all. Fall back to anonymous ID.
|
||||
$anon_id = get_user_meta( $user_id, 'jetpack_tracks_anon_id', true );
|
||||
if ( ! $anon_id ) {
|
||||
$anon_id = \Jetpack_Tracks_Client::get_anon_id();
|
||||
add_user_meta( $user_id, 'jetpack_tracks_anon_id', $anon_id, false );
|
||||
}
|
||||
|
||||
if ( ! isset( $_COOKIE['tk_ai'] ) && ! headers_sent() ) {
|
||||
setcookie( 'tk_ai', $anon_id, 0, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), false ); // phpcs:ignore Jetpack.Functions.SetCookie -- This is a random string and should be fine.
|
||||
}
|
||||
|
||||
return array(
|
||||
'_ut' => 'anon',
|
||||
'_ui' => $anon_id,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
<?php
|
||||
/**
|
||||
* The Jetpack Connection package Urls class file.
|
||||
*
|
||||
* @package automattic/jetpack-connection
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Connection;
|
||||
|
||||
use Automattic\Jetpack\Constants;
|
||||
|
||||
/**
|
||||
* Provides Url methods for the Connection package.
|
||||
*/
|
||||
class Urls {
|
||||
|
||||
const HTTPS_CHECK_OPTION_PREFIX = 'jetpack_sync_https_history_';
|
||||
const HTTPS_CHECK_HISTORY = 5;
|
||||
|
||||
/**
|
||||
* Return URL from option or PHP constant.
|
||||
*
|
||||
* @param string $option_name (e.g. 'home').
|
||||
*
|
||||
* @return mixed|null URL.
|
||||
*/
|
||||
public static function get_raw_url( $option_name ) {
|
||||
$value = null;
|
||||
$constant = ( 'home' === $option_name )
|
||||
? 'WP_HOME'
|
||||
: 'WP_SITEURL';
|
||||
|
||||
// Since we disregard the constant for multisites in ms-default-filters.php,
|
||||
// let's also use the db value if this is a multisite.
|
||||
if ( ! is_multisite() && Constants::is_defined( $constant ) ) {
|
||||
$value = Constants::get_constant( $constant );
|
||||
} else {
|
||||
// Let's get the option from the database so that we can bypass filters. This will help
|
||||
// ensure that we get more uniform values.
|
||||
$value = \Jetpack_Options::get_raw_option( $option_name );
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize domains by removing www unless declared in the site's option.
|
||||
*
|
||||
* @param string $option Option value from the site.
|
||||
* @param callable $url_function Function retrieving the URL to normalize.
|
||||
* @return mixed|string URL.
|
||||
*/
|
||||
public static function normalize_www_in_url( $option, $url_function ) {
|
||||
$url = wp_parse_url( call_user_func( $url_function ) );
|
||||
$option_url = wp_parse_url( get_option( $option ) );
|
||||
|
||||
if ( ! $option_url || ! $url ) {
|
||||
return $url;
|
||||
}
|
||||
|
||||
if ( "www.{$option_url[ 'host' ]}" === $url['host'] ) {
|
||||
// remove www if not present in option URL.
|
||||
$url['host'] = $option_url['host'];
|
||||
}
|
||||
if ( "www.{$url[ 'host' ]}" === $option_url['host'] ) {
|
||||
// add www if present in option URL.
|
||||
$url['host'] = $option_url['host'];
|
||||
}
|
||||
|
||||
$normalized_url = "{$url['scheme']}://{$url['host']}";
|
||||
if ( isset( $url['path'] ) ) {
|
||||
$normalized_url .= "{$url['path']}";
|
||||
}
|
||||
|
||||
if ( isset( $url['query'] ) ) {
|
||||
$normalized_url .= "?{$url['query']}";
|
||||
}
|
||||
|
||||
return $normalized_url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return URL with a normalized protocol.
|
||||
*
|
||||
* @param string $callable Function name that was used to retrieve URL option.
|
||||
* @param string $new_value URL Protocol to set URLs to.
|
||||
* @return string Normalized URL.
|
||||
*/
|
||||
public static function get_protocol_normalized_url( $callable, $new_value ) {
|
||||
$option_key = self::HTTPS_CHECK_OPTION_PREFIX . $callable;
|
||||
|
||||
$parsed_url = wp_parse_url( $new_value );
|
||||
|
||||
if ( ! $parsed_url ) {
|
||||
return $new_value;
|
||||
}
|
||||
if ( array_key_exists( 'scheme', $parsed_url ) ) {
|
||||
$scheme = $parsed_url['scheme'];
|
||||
} else {
|
||||
$scheme = '';
|
||||
}
|
||||
$scheme_history = get_option( $option_key, array() );
|
||||
|
||||
if ( ! is_array( $scheme_history ) ) {
|
||||
$scheme_history = array();
|
||||
}
|
||||
|
||||
$scheme_history[] = $scheme;
|
||||
|
||||
// Limit length to self::HTTPS_CHECK_HISTORY.
|
||||
$scheme_history = array_slice( $scheme_history, ( self::HTTPS_CHECK_HISTORY * -1 ) );
|
||||
|
||||
update_option( $option_key, $scheme_history );
|
||||
|
||||
$forced_scheme = in_array( 'https', $scheme_history, true ) ? 'https' : 'http';
|
||||
|
||||
return set_url_scheme( $new_value, $forced_scheme );
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function that is used when getting home or siteurl values. Decides
|
||||
* whether to get the raw or filtered value.
|
||||
*
|
||||
* @param string $url_type URL to get, home or siteurl.
|
||||
* @return string
|
||||
*/
|
||||
public static function get_raw_or_filtered_url( $url_type ) {
|
||||
$url_function = ( 'home' === $url_type )
|
||||
? 'home_url'
|
||||
: 'site_url';
|
||||
|
||||
if (
|
||||
! Constants::is_defined( 'JETPACK_SYNC_USE_RAW_URL' ) ||
|
||||
Constants::get_constant( 'JETPACK_SYNC_USE_RAW_URL' )
|
||||
) {
|
||||
$scheme = is_ssl() ? 'https' : 'http';
|
||||
$url = (string) self::get_raw_url( $url_type );
|
||||
$url = set_url_scheme( $url, $scheme );
|
||||
} else {
|
||||
$url = self::normalize_www_in_url( $url_type, $url_function );
|
||||
}
|
||||
|
||||
return self::get_protocol_normalized_url( $url_function, $url );
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the escaped home_url.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function home_url() {
|
||||
$url = self::get_raw_or_filtered_url( 'home' );
|
||||
|
||||
/**
|
||||
* Allows overriding of the home_url value that is synced back to WordPress.com.
|
||||
*
|
||||
* @since 1.7.0
|
||||
* @since-jetpack 5.2.0
|
||||
*
|
||||
* @param string $home_url
|
||||
*/
|
||||
return esc_url_raw( apply_filters( 'jetpack_sync_home_url', $url ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the escaped siteurl.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function site_url() {
|
||||
$url = self::get_raw_or_filtered_url( 'siteurl' );
|
||||
|
||||
/**
|
||||
* Allows overriding of the site_url value that is synced back to WordPress.com.
|
||||
*
|
||||
* @since 1.7.0
|
||||
* @since-jetpack 5.2.0
|
||||
*
|
||||
* @param string $site_url
|
||||
*/
|
||||
return esc_url_raw( apply_filters( 'jetpack_sync_site_url', $url ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Return main site URL with a normalized protocol.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function main_network_site_url() {
|
||||
return self::get_protocol_normalized_url( 'main_network_site_url', network_site_url() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Maybe add the origin_site_id query parameter to a URL.
|
||||
*
|
||||
* The parameter is only added for users who are members of the current blog.
|
||||
*
|
||||
* @since 7.1.0
|
||||
*
|
||||
* @param string $url The URL to add the query param to.
|
||||
* @return string The URL with the origin_site_id query parameter maybe added.
|
||||
*/
|
||||
public static function maybe_add_origin_site_id( $url ) {
|
||||
$site_id = Manager::get_site_id();
|
||||
if ( is_wp_error( $site_id ) ) {
|
||||
return $url;
|
||||
}
|
||||
|
||||
// Add query param to URL only for users who can access wp-admin.
|
||||
if ( ! is_user_member_of_blog() ) {
|
||||
return $url;
|
||||
}
|
||||
|
||||
return add_query_arg( 'origin_site_id', $site_id, $url );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
<?php
|
||||
/**
|
||||
* Class to check for account errors in the Jetpack Connection.
|
||||
*
|
||||
* @package automattic/jetpack-connection
|
||||
* @since 6.11.0
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Connection;
|
||||
|
||||
/**
|
||||
* Class User_Account_Status
|
||||
*/
|
||||
class User_Account_Status {
|
||||
/**
|
||||
* Check for possible account errors between the local user and WPCOM account.
|
||||
*
|
||||
* @since 6.11.0
|
||||
*
|
||||
* @param string $current_user_email The email of the current WordPress user.
|
||||
* @param string $wpcom_user_email The email of the connected WordPress.com account.
|
||||
*
|
||||
* @return array An array of possible account errors, empty if no errors.
|
||||
*/
|
||||
public function check_account_errors( $current_user_email, $wpcom_user_email ) {
|
||||
$errors = array();
|
||||
|
||||
// Check for email mismatch error.
|
||||
$has_mismatch = $this->possible_account_mismatch( $current_user_email, $wpcom_user_email );
|
||||
if ( $has_mismatch ) {
|
||||
$errors['mismatch'] = array(
|
||||
'type' => 'mismatch',
|
||||
'message' => __( 'Your WordPress.com email is also used by another user account. This won’t affect functionality but may cause confusion about which user account is connected.', 'jetpack-connection' ),
|
||||
'details' => array(
|
||||
'site_email' => $current_user_email,
|
||||
'wpcom_email' => $wpcom_user_email,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the account errors.
|
||||
*
|
||||
* @since 6.11.0
|
||||
*
|
||||
* @param array $errors The array of account errors.
|
||||
* @param string $current_user_email The email of the current WordPress user.
|
||||
* @param string $wpcom_user_email The email of the connected WordPress.com account.
|
||||
*/
|
||||
return apply_filters( 'jetpack_connection_account_errors', $errors, $current_user_email, $wpcom_user_email );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there is a possible account mismatch between the local user and WPCOM account.
|
||||
*
|
||||
* @since 6.11.0
|
||||
*
|
||||
* @param string $current_user_email The email of the current WordPress user.
|
||||
* @param string $wpcom_user_email The email of the connected WordPress.com account.
|
||||
*
|
||||
* @return bool Whether there is a possible account mismatch.
|
||||
*/
|
||||
public function possible_account_mismatch( $current_user_email, $wpcom_user_email ) {
|
||||
// If emails are the same or there's no WPCOM email, there's no mismatch.
|
||||
if ( $current_user_email === $wpcom_user_email || ! $wpcom_user_email ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Generate transient key with both wpcom email and user ID if available.
|
||||
$transient_key = 'jetpack_account_mismatch_';
|
||||
$transient_key .= md5( $wpcom_user_email );
|
||||
|
||||
$cached_result = get_transient( $transient_key );
|
||||
|
||||
if ( false !== $cached_result ) {
|
||||
return (bool) $cached_result;
|
||||
}
|
||||
|
||||
// Check if there's a WordPress user with the WPCOM email .
|
||||
$wpcom_email_user = get_user_by( 'email', $wpcom_user_email );
|
||||
$mismatch_exists = false !== $wpcom_email_user;
|
||||
|
||||
// Store the result in a transient for 24 hours.
|
||||
set_transient( $transient_key, $mismatch_exists, DAY_IN_SECONDS );
|
||||
|
||||
return $mismatch_exists;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears account mismatch transients for a user when they update their email or are deleted.
|
||||
*
|
||||
* @since 6.11.0
|
||||
*
|
||||
* @param string|int $user_id_or_email User ID or email address.
|
||||
* @return void
|
||||
*/
|
||||
public function clean_account_mismatch_transients( $user_id_or_email ) {
|
||||
$email = null;
|
||||
|
||||
if ( is_numeric( $user_id_or_email ) ) {
|
||||
$user = get_userdata( $user_id_or_email );
|
||||
if ( $user && isset( $user->user_email ) ) {
|
||||
$email = $user->user_email;
|
||||
}
|
||||
} else {
|
||||
$email = $user_id_or_email;
|
||||
}
|
||||
|
||||
if ( ! $email || ! is_email( $email ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$transient_key = 'jetpack_account_mismatch_' . md5( $email );
|
||||
delete_transient( $transient_key );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
<?php
|
||||
/**
|
||||
* Handles the WordPress.com account column in the users list table.
|
||||
*
|
||||
* @package automattic/jetpack-connection
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Connection;
|
||||
|
||||
use Automattic\Jetpack\Assets;
|
||||
use Automattic\Jetpack\Status\Host;
|
||||
|
||||
/**
|
||||
* Class Users_Connection_Admin
|
||||
*/
|
||||
class Users_Connection_Admin {
|
||||
/**
|
||||
* The column ID used for the WordPress.com account column.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const COLUMN_ID = 'user_jetpack';
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
// Only set up hooks if we're in the admin area and user has proper permissions
|
||||
add_action( 'init', array( $this, 'init' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the admin functionality if conditions are met.
|
||||
*/
|
||||
public function init() {
|
||||
if ( ! is_admin() || ! current_user_can( 'manage_options' ) || ( new Host() )->is_wpcom_simple() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
add_filter( 'manage_users_columns', array( $this, 'add_connection_column' ) );
|
||||
add_filter( 'manage_users_custom_column', array( $this, 'render_connection_column' ), 9, 3 ); // Priority 9 to run before SSO
|
||||
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
|
||||
add_action( 'admin_print_styles-users.php', array( $this, 'add_connection_column_styles' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the connection column to the users list table.
|
||||
*
|
||||
* @param array $columns The current columns.
|
||||
* @return array Modified columns.
|
||||
*/
|
||||
public function add_connection_column( $columns ) {
|
||||
$columns[ self::COLUMN_ID ] = sprintf(
|
||||
'<span class="jetpack-connection-tooltip-icon" role="tooltip" tabindex="0" aria-label="%2$s: %1$s">
|
||||
%1$s
|
||||
<span class="jetpack-connection-tooltip"></span>
|
||||
</span>',
|
||||
esc_html__( 'WordPress.com account', 'jetpack-connection' ),
|
||||
esc_attr__( 'Tooltip', 'jetpack-connection' )
|
||||
);
|
||||
return $columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the connection column content.
|
||||
*
|
||||
* @param string $output Custom column output.
|
||||
* @param string $column_name Column name.
|
||||
* @param int $user_id ID of the currently-listed user.
|
||||
* @return string
|
||||
*/
|
||||
public function render_connection_column( $output, $column_name, $user_id ) {
|
||||
if ( self::COLUMN_ID !== $column_name ) {
|
||||
return $output;
|
||||
}
|
||||
|
||||
if ( ( new Manager() )->is_user_connected( $user_id ) ) {
|
||||
$logo_url = Jetpack_Connector::get_inline_connector_logo_url();
|
||||
|
||||
return sprintf(
|
||||
'<span title="%1$s" class="jetpack-connection-status"><img src="%2$s" alt="" class="jetpack-connection-status__logo" height="18" decoding="async" loading="lazy" />%3$s</span>',
|
||||
esc_attr__( 'This user has connected their WordPress.com account.', 'jetpack-connection' ),
|
||||
esc_url( $logo_url ),
|
||||
esc_html__( 'Connected', 'jetpack-connection' )
|
||||
);
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue scripts and styles.
|
||||
*
|
||||
* @param string $hook The current admin page.
|
||||
*/
|
||||
public function enqueue_scripts( $hook ) {
|
||||
if ( 'users.php' !== $hook ) {
|
||||
return;
|
||||
}
|
||||
|
||||
Assets::register_script(
|
||||
'jetpack-users-connection',
|
||||
'../dist/jetpack-users-connection.js',
|
||||
__FILE__,
|
||||
array(
|
||||
'strategy' => 'defer',
|
||||
'in_footer' => true,
|
||||
'enqueue' => true,
|
||||
'version' => Package_Version::PACKAGE_VERSION,
|
||||
'deps' => array( 'wp-i18n' ),
|
||||
|
||||
)
|
||||
);
|
||||
|
||||
wp_localize_script(
|
||||
'jetpack-users-connection',
|
||||
'jetpackConnectionTooltips',
|
||||
array(
|
||||
'columnTooltip' => esc_html( self::get_column_tooltip_text() ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add styles for the connection column.
|
||||
*/
|
||||
public function add_connection_column_styles() {
|
||||
?>
|
||||
<style>
|
||||
.jetpack-connection-tooltip-icon {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
/* Add [?] icon using pseudo-element, only in column header */
|
||||
th.manage-column .jetpack-connection-tooltip-icon::after {
|
||||
content: '[?]';
|
||||
color: #3c434a;
|
||||
font-size: 1em;
|
||||
margin-left: 4px;
|
||||
}
|
||||
.jetpack-connection-tooltip {
|
||||
position: absolute;
|
||||
background: #f6f7f7;
|
||||
top: -85px;
|
||||
width: 250px;
|
||||
padding: 7px;
|
||||
color: #3c434a;
|
||||
font-size: .75rem;
|
||||
line-height: 17px;
|
||||
text-align: left;
|
||||
margin: 0;
|
||||
display: none;
|
||||
border-radius: 4px;
|
||||
font-family: sans-serif;
|
||||
box-shadow: 5px 10px 10px rgba(0, 0, 0, 0.1);
|
||||
left: -170px;
|
||||
}
|
||||
.column-user_jetpack {
|
||||
width: 190px;
|
||||
}
|
||||
@media screen and (max-width: 1100px) {
|
||||
.column-user_jetpack {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
td.column-user_jetpack {
|
||||
vertical-align: middle;
|
||||
}
|
||||
.jetpack-connection-status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
column-gap: 6px;
|
||||
}
|
||||
.jetpack-connection-status__logo {
|
||||
display: block;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
/* Show tooltip on hover and focus */
|
||||
.jetpack-connection-tooltip-icon:hover .jetpack-connection-tooltip,
|
||||
.jetpack-connection-tooltip-icon:focus-within .jetpack-connection-tooltip {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the column header tooltip text based on which plugin families use the connection.
|
||||
*
|
||||
* @return string Tooltip text.
|
||||
*/
|
||||
private static function get_column_tooltip_text() {
|
||||
$families = Jetpack_Connector::get_connected_plugin_families();
|
||||
|
||||
if ( $families['has_woo'] && $families['has_a4a'] ) {
|
||||
return __( 'Connecting a WordPress.com account unlocks features for Jetpack, WooCommerce, and Automattic for Agencies including secure logins.', 'jetpack-connection' );
|
||||
}
|
||||
|
||||
if ( $families['has_woo'] ) {
|
||||
return __( 'Connecting a WordPress.com account unlocks features for Jetpack and WooCommerce including secure logins.', 'jetpack-connection' );
|
||||
}
|
||||
|
||||
if ( $families['has_a4a'] ) {
|
||||
return __( 'Connecting a WordPress.com account unlocks features for Jetpack and Automattic for Agencies including secure logins.', 'jetpack-connection' );
|
||||
}
|
||||
|
||||
return __( 'Connecting a WordPress.com account unlocks Jetpack features including secure logins.', 'jetpack-connection' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the column ID. Allows other classes to reference the same column.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function get_column_id() {
|
||||
return self::COLUMN_ID;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
/**
|
||||
* The Jetpack Connection package Utils class file.
|
||||
*
|
||||
* @package automattic/jetpack-connection
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Connection;
|
||||
|
||||
use Automattic\Jetpack\Tracking;
|
||||
|
||||
/**
|
||||
* Provides utility methods for the Connection package.
|
||||
*/
|
||||
class Utils {
|
||||
|
||||
const DEFAULT_JETPACK__API_VERSION = 1;
|
||||
const DEFAULT_JETPACK__API_BASE = 'https://jetpack.wordpress.com/jetpack.';
|
||||
const DEFAULT_JETPACK__WPCOM_JSON_API_BASE = 'https://public-api.wordpress.com';
|
||||
|
||||
/**
|
||||
* Enters a user token into the user_tokens option
|
||||
*
|
||||
* @deprecated 1.24.0 Use Automattic\Jetpack\Connection\Tokens->update_user_token() instead.
|
||||
*
|
||||
* @param int $user_id The user id.
|
||||
* @param string $token The user token.
|
||||
* @param bool $is_master_user Whether the user is the master user.
|
||||
* @return bool
|
||||
*/
|
||||
public static function update_user_token( $user_id, $token, $is_master_user ) {
|
||||
_deprecated_function( __METHOD__, '1.24.0', 'Automattic\\Jetpack\\Connection\\Tokens->update_user_token' );
|
||||
return ( new Tokens() )->update_user_token( $user_id, $token, $is_master_user );
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the value of the api constant.
|
||||
*
|
||||
* @param String $constant_value The constant value.
|
||||
* @param String $constant_name The constant name.
|
||||
* @return mixed | null
|
||||
*/
|
||||
public static function jetpack_api_constant_filter( $constant_value, $constant_name ) {
|
||||
if ( $constant_value !== null ) {
|
||||
// If the constant value was already set elsewhere, use that value.
|
||||
return $constant_value;
|
||||
}
|
||||
|
||||
if ( defined( "self::DEFAULT_$constant_name" ) ) {
|
||||
return constant( "self::DEFAULT_$constant_name" );
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a filter to initialize default values of the constants.
|
||||
*/
|
||||
public static function init_default_constants() {
|
||||
add_filter(
|
||||
'jetpack_constant_default_value',
|
||||
array( __CLASS__, 'jetpack_api_constant_filter' ),
|
||||
10,
|
||||
2
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the registration request body to include tracking properties.
|
||||
*
|
||||
* @param array $properties Already prepared tracking properties.
|
||||
* @return array amended properties.
|
||||
*/
|
||||
public static function filter_register_request_body( $properties ) {
|
||||
$tracking = new Tracking();
|
||||
$tracks_identity = $tracking->tracks_get_identity( get_current_user_id() );
|
||||
|
||||
return array_merge(
|
||||
$properties,
|
||||
array(
|
||||
'_ui' => $tracks_identity['_ui'],
|
||||
'_ut' => $tracks_identity['_ut'],
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a new user from a SSO attempt.
|
||||
*
|
||||
* @param object $user_data WordPress.com user information.
|
||||
*/
|
||||
public static function generate_user( $user_data ) {
|
||||
$username = $user_data->login;
|
||||
/**
|
||||
* Determines how many times the SSO module can attempt to randomly generate a user.
|
||||
*
|
||||
* @module sso
|
||||
*
|
||||
* @since jetpack-4.3.2
|
||||
*
|
||||
* @param int 5 By default, SSO will attempt to random generate a user up to 5 times.
|
||||
*/
|
||||
$num_tries = (int) apply_filters( 'jetpack_sso_allowed_username_generate_retries', 5 );
|
||||
|
||||
$exists = username_exists( $username );
|
||||
$tries = 0;
|
||||
while ( $exists && $tries++ < $num_tries ) {
|
||||
$username = $user_data->login . '_' . $user_data->ID . '_' . wp_rand();
|
||||
$exists = username_exists( $username );
|
||||
}
|
||||
|
||||
if ( $exists ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$user = (object) array();
|
||||
$user->user_pass = wp_generate_password( 20 );
|
||||
$user->user_login = wp_slash( $username );
|
||||
$user->user_email = wp_slash( $user_data->email );
|
||||
$user->display_name = $user_data->display_name;
|
||||
$user->first_name = $user_data->first_name;
|
||||
$user->last_name = $user_data->last_name;
|
||||
$user->url = $user_data->url;
|
||||
$user->description = $user_data->description;
|
||||
|
||||
if ( isset( $user_data->role ) && $user_data->role ) {
|
||||
$user->role = $user_data->role;
|
||||
}
|
||||
|
||||
$created_user_id = wp_insert_user( $user );
|
||||
|
||||
update_user_meta( $created_user_id, 'wpcom_user_id', $user_data->ID );
|
||||
return get_userdata( $created_user_id );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
<?php
|
||||
/**
|
||||
* Connection Webhooks class.
|
||||
*
|
||||
* @package automattic/jetpack-connection
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Connection;
|
||||
|
||||
use Automattic\Jetpack\CookieState;
|
||||
use Automattic\Jetpack\Roles;
|
||||
use Automattic\Jetpack\Status\Host;
|
||||
use Automattic\Jetpack\Tracking;
|
||||
use Jetpack_Options;
|
||||
|
||||
/**
|
||||
* Connection Webhooks class.
|
||||
*/
|
||||
class Webhooks {
|
||||
|
||||
/**
|
||||
* The Connection Manager object.
|
||||
*
|
||||
* @var Manager
|
||||
*/
|
||||
private $connection;
|
||||
|
||||
/**
|
||||
* Webhooks constructor.
|
||||
*
|
||||
* @param Manager $connection The Connection Manager object.
|
||||
*/
|
||||
public function __construct( $connection ) {
|
||||
$this->connection = $connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the webhooks.
|
||||
*
|
||||
* @param Manager $connection The Connection Manager object.
|
||||
*/
|
||||
public static function init( $connection ) {
|
||||
$webhooks = new static( $connection );
|
||||
|
||||
add_action( 'init', array( $webhooks, 'controller' ) );
|
||||
add_action( 'load-toplevel_page_jetpack', array( $webhooks, 'fallback_jetpack_controller' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Jetpack plugin used to trigger this webhooks in Jetpack::admin_page_load()
|
||||
*
|
||||
* The Jetpack toplevel menu is still accessible for stand-alone plugins, and while there's no content for that page, there are still
|
||||
* actions from Calypso and WPCOM that reach that route regardless of the site having the Jetpack plugin or not. That's why we are still handling it here.
|
||||
*/
|
||||
public function fallback_jetpack_controller() {
|
||||
$this->controller( true );
|
||||
}
|
||||
|
||||
/**
|
||||
* The "controller" decides which handler we need to run.
|
||||
*
|
||||
* @param bool $force Do not check if it's a webhook request and just run the controller.
|
||||
*/
|
||||
public function controller( $force = false ) {
|
||||
if ( ! $force ) {
|
||||
// The nonce is verified in specific handlers.
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
if ( empty( $_GET['handler'] ) || 'jetpack-connection-webhooks' !== $_GET['handler'] ) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
if ( isset( $_GET['connect_url_redirect'] ) ) {
|
||||
$this->handle_connect_url_redirect();
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
if ( empty( $_GET['action'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The nonce is verified in specific handlers.
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
switch ( $_GET['action'] ) {
|
||||
case 'authorize':
|
||||
$this->handle_authorize();
|
||||
$this->do_exit();
|
||||
break; // @phan-suppress-current-line PhanPluginUnreachableCode -- Safer to include it even though do_exit never returns.
|
||||
case 'authorize_redirect':
|
||||
$this->handle_authorize_redirect();
|
||||
$this->do_exit();
|
||||
break; // @phan-suppress-current-line PhanPluginUnreachableCode -- Safer to include it even though do_exit never returns.
|
||||
// Class Jetpack::admin_page_load() still handles other cases.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the authorization action.
|
||||
*/
|
||||
public function handle_authorize() {
|
||||
if ( $this->connection->is_connected() && $this->connection->is_user_connected() ) {
|
||||
$redirect_url = apply_filters( 'jetpack_client_authorize_already_authorized_url', admin_url() );
|
||||
|
||||
if ( ! empty( $_GET['redirect'] ) ) {
|
||||
$explicit = esc_url_raw( wp_unslash( $_GET['redirect'] ) );
|
||||
if ( wp_validate_redirect( $explicit ) ) {
|
||||
$redirect_url = $explicit;
|
||||
}
|
||||
}
|
||||
|
||||
wp_safe_redirect( $redirect_url );
|
||||
|
||||
return;
|
||||
}
|
||||
do_action( 'jetpack_client_authorize_processing' );
|
||||
|
||||
$data = stripslashes_deep( $_GET ); // We need all request data under the context of an authorization request.
|
||||
$data['auth_type'] = 'client';
|
||||
$roles = new Roles();
|
||||
$role = $roles->translate_current_user_to_role();
|
||||
$redirect = isset( $data['redirect'] ) ? esc_url_raw( (string) $data['redirect'] ) : '';
|
||||
|
||||
check_admin_referer( "jetpack-authorize_{$role}_{$redirect}" );
|
||||
|
||||
$tracking = new Tracking();
|
||||
|
||||
$result = $this->connection->authorize( $data );
|
||||
|
||||
if ( is_wp_error( $result ) ) {
|
||||
do_action( 'jetpack_client_authorize_error', $result );
|
||||
|
||||
$tracking->record_user_event(
|
||||
'jpc_client_authorize_fail',
|
||||
array(
|
||||
'error_code' => $result->get_error_code(),
|
||||
'error_message' => $result->get_error_message(),
|
||||
)
|
||||
);
|
||||
} else {
|
||||
/**
|
||||
* Fires after the Jetpack client is authorized to communicate with WordPress.com.
|
||||
*
|
||||
* @param int Jetpack Blog ID.
|
||||
*
|
||||
* @since 1.7.0
|
||||
* @since-jetpack 4.2.0
|
||||
*/
|
||||
do_action( 'jetpack_client_authorized', Jetpack_Options::get_option( 'id' ) );
|
||||
|
||||
$tracking->record_user_event( 'jpc_client_authorize_success' );
|
||||
}
|
||||
|
||||
$fallback_redirect = apply_filters( 'jetpack_client_authorize_fallback_url', admin_url() );
|
||||
$redirect = wp_validate_redirect( $redirect ) ? $redirect : $fallback_redirect;
|
||||
|
||||
wp_safe_redirect( $redirect );
|
||||
}
|
||||
|
||||
/**
|
||||
* The authorhize_redirect webhook handler
|
||||
*/
|
||||
public function handle_authorize_redirect() {
|
||||
$authorize_redirect_handler = new Webhooks\Authorize_Redirect( $this->connection );
|
||||
$authorize_redirect_handler->handle();
|
||||
}
|
||||
|
||||
/**
|
||||
* The `exit` is wrapped into a method so we could mock it.
|
||||
*
|
||||
* @return never
|
||||
*/
|
||||
protected function do_exit() {
|
||||
exit( 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the `connect_url_redirect` action,
|
||||
* which is usually called to repeat an attempt for user to authorize the connection.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle_connect_url_redirect() {
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- no site changes.
|
||||
$from = ! empty( $_GET['from'] ) ? sanitize_text_field( wp_unslash( $_GET['from'] ) ) : 'iframe';
|
||||
|
||||
$skip_pricing = filter_input( INPUT_GET, 'skip_pricing', FILTER_VALIDATE_BOOLEAN );
|
||||
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- no site changes, sanitization happens in get_authorization_url()
|
||||
$redirect = ! empty( $_GET['redirect_after_auth'] ) ? wp_unslash( $_GET['redirect_after_auth'] ) : false;
|
||||
|
||||
add_filter( 'allowed_redirect_hosts', array( Host::class, 'allow_wpcom_environments' ) );
|
||||
|
||||
if ( ! $this->connection->is_user_connected() ) {
|
||||
if ( ! $this->connection->is_connected() ) {
|
||||
$this->connection->register();
|
||||
}
|
||||
|
||||
$connect_url = add_query_arg( 'from', $from, $this->connection->get_authorization_url( null, $redirect ) );
|
||||
|
||||
if ( $skip_pricing ) {
|
||||
$connect_url = add_query_arg( 'skip_pricing', '1', $connect_url );
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- no site changes.
|
||||
if ( isset( $_GET['notes_iframe'] ) ) {
|
||||
$connect_url .= '¬es_iframe';
|
||||
}
|
||||
wp_safe_redirect( $connect_url );
|
||||
$this->do_exit();
|
||||
} elseif ( ! isset( $_GET['calypso_env'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- no site changes.
|
||||
( new CookieState() )->state( 'message', 'already_authorized' );
|
||||
wp_safe_redirect( $redirect );
|
||||
$this->do_exit();
|
||||
} else {
|
||||
if ( 'connect-after-checkout' === $from && $redirect ) {
|
||||
wp_safe_redirect( $redirect );
|
||||
$this->do_exit();
|
||||
}
|
||||
$connect_url = add_query_arg(
|
||||
array(
|
||||
'from' => $from,
|
||||
'already_authorized' => true,
|
||||
),
|
||||
$this->connection->get_authorization_url()
|
||||
);
|
||||
wp_safe_redirect( $connect_url );
|
||||
$this->do_exit();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
/**
|
||||
* XMLRPC Async Call class.
|
||||
*
|
||||
* @package automattic/jetpack-connection
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Connection;
|
||||
|
||||
use Jetpack_IXR_ClientMulticall;
|
||||
|
||||
/**
|
||||
* Make XMLRPC async calls to WordPress.com
|
||||
*
|
||||
* This class allows you to enqueue XMLRPC calls that will be grouped and sent
|
||||
* at once in a multi-call request at shutdown.
|
||||
*
|
||||
* Usage:
|
||||
*
|
||||
* XMLRPC_Async_Call::add_call( 'methodName', get_current_user_id(), $arg1, $arg2, etc... )
|
||||
*
|
||||
* See XMLRPC_Async_Call::add_call for details
|
||||
*/
|
||||
class XMLRPC_Async_Call {
|
||||
|
||||
/**
|
||||
* Hold the IXR Clients that will be dispatched at shutdown
|
||||
*
|
||||
* Clients are stored in the following schema:
|
||||
* [
|
||||
* $blog_id => [
|
||||
* $user_id => [
|
||||
* arrat of Jetpack_IXR_ClientMulticall
|
||||
* ]
|
||||
* ]
|
||||
* ]
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $clients = array();
|
||||
|
||||
/**
|
||||
* Adds a new XMLRPC call to the queue to be processed on shutdown
|
||||
*
|
||||
* @param string $method The XML-RPC method.
|
||||
* @param integer $user_id The user ID used to make the request (will use this user's token); Use 0 for the blog token.
|
||||
* @param mixed ...$args This function accepts any number of additional arguments, that will be passed to the call.
|
||||
* @return void
|
||||
*/
|
||||
public static function add_call( $method, $user_id = 0, ...$args ) {
|
||||
global $blog_id;
|
||||
|
||||
$client_blog_id = is_multisite() ? $blog_id : 0;
|
||||
|
||||
if ( ! isset( self::$clients[ $client_blog_id ] ) ) {
|
||||
self::$clients[ $client_blog_id ] = array();
|
||||
}
|
||||
|
||||
if ( ! isset( self::$clients[ $client_blog_id ][ $user_id ] ) ) {
|
||||
self::$clients[ $client_blog_id ][ $user_id ] = new Jetpack_IXR_ClientMulticall( array( 'user_id' => $user_id ) );
|
||||
}
|
||||
|
||||
// https://plugins.trac.wordpress.org/ticket/2041
|
||||
if ( function_exists( 'ignore_user_abort' ) ) {
|
||||
ignore_user_abort( true );
|
||||
}
|
||||
|
||||
array_unshift( $args, $method );
|
||||
|
||||
call_user_func_array( array( self::$clients[ $client_blog_id ][ $user_id ], 'addCall' ), $args );
|
||||
|
||||
if ( false === has_action( 'shutdown', array( 'Automattic\Jetpack\Connection\XMLRPC_Async_Call', 'do_calls' ) ) ) {
|
||||
add_action( 'shutdown', array( 'Automattic\Jetpack\Connection\XMLRPC_Async_Call', 'do_calls' ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger the calls at shutdown
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function do_calls() {
|
||||
foreach ( self::$clients as $client_blog_id => $blog_clients ) {
|
||||
if ( $client_blog_id > 0 ) {
|
||||
$switch_success = switch_to_blog( $client_blog_id );
|
||||
|
||||
if ( ! $switch_success ) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ( $blog_clients as $client ) {
|
||||
if ( empty( $client->calls ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
flush();
|
||||
$client->query();
|
||||
}
|
||||
|
||||
if ( $client_blog_id > 0 ) {
|
||||
restore_current_blog();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
/**
|
||||
* Sets up the Connection XML-RPC methods.
|
||||
*
|
||||
* @package automattic/jetpack-connection
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Connection;
|
||||
|
||||
use IXR_Error;
|
||||
|
||||
/**
|
||||
* Registers the XML-RPC methods for Connections.
|
||||
*
|
||||
* @phan-constructor-used-for-side-effects
|
||||
*/
|
||||
class XMLRPC_Connector {
|
||||
/**
|
||||
* The Connection Manager.
|
||||
*
|
||||
* @var Manager
|
||||
*/
|
||||
private $connection;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param Manager $connection The Connection Manager.
|
||||
*/
|
||||
public function __construct( Manager $connection ) {
|
||||
$this->connection = $connection;
|
||||
|
||||
// Adding the filter late to avoid being overwritten by Jetpack's XMLRPC server.
|
||||
add_filter( 'xmlrpc_methods', array( $this, 'xmlrpc_methods' ), 20 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Attached to the `xmlrpc_methods` filter.
|
||||
*
|
||||
* @param array $methods The already registered XML-RPC methods.
|
||||
* @return array
|
||||
*/
|
||||
public function xmlrpc_methods( $methods ) {
|
||||
return array_merge(
|
||||
$methods,
|
||||
array(
|
||||
'jetpack.verifyRegistration' => array( $this, 'verify_registration' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles verification that a site is registered.
|
||||
*
|
||||
* @param array $registration_data The data sent by the XML-RPC client:
|
||||
* [ $secret_1, $user_id ].
|
||||
*
|
||||
* @return string|IXR_Error
|
||||
*/
|
||||
public function verify_registration( $registration_data ) {
|
||||
return $this->output( $this->connection->handle_registration( $registration_data ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes output for XML-RPC.
|
||||
*
|
||||
* @param mixed $data The data to output.
|
||||
*/
|
||||
private function output( $data ) {
|
||||
if ( is_wp_error( $data ) ) {
|
||||
$code = $data->get_error_data();
|
||||
if ( ! $code ) {
|
||||
$code = -10520;
|
||||
}
|
||||
|
||||
if ( ! class_exists( IXR_Error::class ) ) {
|
||||
require_once ABSPATH . WPINC . '/class-IXR.php';
|
||||
}
|
||||
return new IXR_Error(
|
||||
$code,
|
||||
sprintf( 'Jetpack: [%s] %s', $data->get_error_code(), $data->get_error_message() )
|
||||
);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,600 @@
|
||||
<?php
|
||||
/**
|
||||
* Jetpack connector card for the WP core Connectors screen.
|
||||
*
|
||||
* Registers a connector in the WP 7.0+ Connectors registry and enqueues
|
||||
* a script module that provides a custom render function with connection
|
||||
* details (owner, connected plugins, disconnect).
|
||||
*
|
||||
* @package automattic/jetpack-connection
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Connection;
|
||||
|
||||
use Automattic\Jetpack\Identity_Crisis;
|
||||
use Automattic\Jetpack\Modules;
|
||||
use Automattic\Jetpack\Status;
|
||||
use Automattic\Jetpack\Status\Host;
|
||||
|
||||
/**
|
||||
* Jetpack connector card handler.
|
||||
*
|
||||
* @since 8.2.0
|
||||
*/
|
||||
class Jetpack_Connector {
|
||||
|
||||
/**
|
||||
* Whether the connector has been initialized.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private static $initialized = false;
|
||||
|
||||
/**
|
||||
* Script module identifier.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const MODULE_ID = '@automattic/jetpack-connection-connectors';
|
||||
|
||||
/**
|
||||
* Screen ID assigned by WordPress to the Gutenberg plugin's connectors submenu page.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const GUTENBERG_CONNECTORS_SCREEN_ID = 'settings_page_options-connectors-wp-admin';
|
||||
|
||||
/**
|
||||
* Page slug registered by the Gutenberg plugin for the connectors submenu page.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const GUTENBERG_CONNECTORS_PAGE_SLUG = 'options-connectors-wp-admin';
|
||||
|
||||
/**
|
||||
* Initialize the connector.
|
||||
*/
|
||||
public static function init() {
|
||||
if ( static::$initialized ) {
|
||||
return;
|
||||
}
|
||||
static::$initialized = true;
|
||||
|
||||
add_action( 'wp_connectors_init', array( static::class, 'register_connector' ), 20 );
|
||||
add_action( 'admin_enqueue_scripts', array( static::class, 'enqueue_script_module' ) );
|
||||
add_action( 'jetpack_client_authorize_error', array( static::class, 'store_auth_error' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register Jetpack as a connector in the WP core Connectors screen.
|
||||
*
|
||||
* The wp_connectors_init action is available in WordPress 7.0+.
|
||||
* On older versions this action never fires, so the hook is safely a no-op.
|
||||
*
|
||||
* @since 8.2.0
|
||||
*
|
||||
* @param \WP_Connector_Registry $registry Connector registry instance.
|
||||
*/
|
||||
public static function register_connector( $registry ) {
|
||||
// @phan-suppress-previous-line PhanUndeclaredTypeParameter -- WP 7.0+ class.
|
||||
$registry->register( // @phan-suppress-current-line PhanUndeclaredClassMethod -- WP 7.0+ class.
|
||||
'wordpress_com',
|
||||
array(
|
||||
'name' => 'Jetpack Connection',
|
||||
'description' => __( 'Enhanced functionality for Jetpack and WooCommerce with WordPress.com.', 'jetpack-connection' ),
|
||||
'type' => 'cloud_service',
|
||||
'logo_url' => static::get_connector_logo_url(),
|
||||
'authentication' => array(
|
||||
'method' => 'none',
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue the connectors card script module on the Settings > Connectors page.
|
||||
*
|
||||
* @since 8.2.0
|
||||
*/
|
||||
public static function enqueue_script_module() {
|
||||
$screen = get_current_screen();
|
||||
|
||||
if ( ! $screen || ! static::is_connectors_screen( $screen ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! class_exists( 'WP_Connector_Registry' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$css_path = __DIR__ . '/css/connectors-card.css';
|
||||
wp_enqueue_style(
|
||||
'jetpack-connector-card',
|
||||
plugins_url( 'css/connectors-card.css', __FILE__ ),
|
||||
array(),
|
||||
(string) @filemtime( $css_path ) // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- fallback to empty string if file is missing.
|
||||
);
|
||||
|
||||
$js_path = __DIR__ . '/js/connectors-card.js';
|
||||
wp_register_script_module(
|
||||
static::MODULE_ID,
|
||||
plugins_url( 'js/connectors-card.js', __FILE__ ),
|
||||
array(
|
||||
array(
|
||||
'id' => '@wordpress/connectors',
|
||||
'import' => 'static',
|
||||
),
|
||||
),
|
||||
(string) @filemtime( $js_path ) // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- fallback to empty string if file is missing.
|
||||
);
|
||||
wp_enqueue_script_module( static::MODULE_ID );
|
||||
|
||||
add_filter(
|
||||
'script_module_data_' . static::MODULE_ID,
|
||||
array( static::class, 'get_connector_data' )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the data passed to the script module via the script_module_data_ filter.
|
||||
*
|
||||
* @since 8.2.0
|
||||
*
|
||||
* @param array $data Existing script module data.
|
||||
* @return array Filtered script module data.
|
||||
*/
|
||||
public static function get_connector_data( $data ) {
|
||||
$manager = new Manager();
|
||||
$is_registered = $manager->is_connected();
|
||||
$is_connected = $is_registered && $manager->has_connected_owner();
|
||||
|
||||
$data['isConnected'] = $is_connected;
|
||||
$data['isRegistered'] = $is_registered;
|
||||
$data['isOfflineMode'] = ( new Status() )->is_offline_mode();
|
||||
$data['isFirstConnection'] = ! $is_registered && ! (bool) \Jetpack_Options::get_option( 'id' );
|
||||
$data['apiRoot'] = esc_url_raw( rest_url() );
|
||||
$data['apiNonce'] = wp_create_nonce( 'wp_rest' );
|
||||
$data['redirectUri'] = static::get_connectors_page_path();
|
||||
$data['connectorName'] = 'Jetpack Connection';
|
||||
$data['connectorDescription'] = __( 'Enhanced functionality for Jetpack and WooCommerce with WordPress.com.', 'jetpack-connection' );
|
||||
$data['connectorLogoUrl'] = static::get_connector_logo_url();
|
||||
|
||||
$data['connectedPlugins'] = static::get_connected_plugins_data( $manager );
|
||||
|
||||
if ( $is_registered ) {
|
||||
$data['siteDetails'] = array(
|
||||
'blogId' => (int) \Jetpack_Options::get_option( 'id' ),
|
||||
'siteUrl' => site_url(),
|
||||
'homeUrl' => home_url(),
|
||||
);
|
||||
|
||||
if ( in_array( 'jetpack', array_column( $data['connectedPlugins'], 'slug' ), true ) ) {
|
||||
$data['ssoStatus'] = ( new Modules() )->is_active( 'sso', false );
|
||||
}
|
||||
|
||||
static::add_identity_crisis_data( $data );
|
||||
}
|
||||
|
||||
if ( $is_connected ) {
|
||||
$data['currentUser'] = static::get_current_user_data( $manager );
|
||||
$data['connectionOwner'] = static::get_connection_owner_data( $manager );
|
||||
}
|
||||
|
||||
$host = new Host();
|
||||
$data['isWoaSite'] = $host->is_woa_site();
|
||||
$data['isVipSite'] = $host->is_vip_site();
|
||||
|
||||
$auth_error = static::consume_auth_error();
|
||||
if ( $auth_error ) {
|
||||
$data['authError'] = $auth_error;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add Jetpack Identity Crisis (Safe Mode) data to the script module data.
|
||||
*
|
||||
* The connector card uses this to swap the status badge to "Safe Mode" and
|
||||
* to render IDC resolution options (migrate / start fresh / stay in safe
|
||||
* mode) in the expanded details. Mirrors the data assembled by
|
||||
* \Automattic\Jetpack\IdentityCrisis\UI::get_initial_state_data().
|
||||
*
|
||||
* @since 8.7.0
|
||||
*
|
||||
* @param array $data Script module data passed by reference.
|
||||
*/
|
||||
private static function add_identity_crisis_data( &$data ) {
|
||||
if ( ! class_exists( Identity_Crisis::class ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$in_safe_mode = ( new Status() )->in_safe_mode();
|
||||
|
||||
$data['isInSafeMode'] = $in_safe_mode;
|
||||
|
||||
if ( ! $in_safe_mode ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$idc_urls = Identity_Crisis::get_mismatched_urls();
|
||||
|
||||
$data['isSafeModeConfirmed'] = (bool) Identity_Crisis::$is_safe_mode_confirmed;
|
||||
$data['idc'] = array(
|
||||
'currentUrl' => ( is_array( $idc_urls ) && array_key_exists( 'current_url', $idc_urls ) ) ? $idc_urls['current_url'] : home_url(),
|
||||
'wpcomHomeUrl' => ( is_array( $idc_urls ) && array_key_exists( 'wpcom_url', $idc_urls ) ) ? $idc_urls['wpcom_url'] : '',
|
||||
'isDevelopmentSite' => (bool) Status::is_development_site(),
|
||||
'possibleDynamicSiteUrlDetected' => (bool) Identity_Crisis::detect_possible_dynamic_site_url(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current (logged-in) user's connection details.
|
||||
*
|
||||
* @param Manager $manager Connection manager instance.
|
||||
* @return array|null Current user data or null if not connected.
|
||||
*/
|
||||
private static function get_current_user_data( $manager ) {
|
||||
$user_id = get_current_user_id();
|
||||
|
||||
if ( ! $user_id || ! $manager->is_user_connected( $user_id ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$user = get_userdata( $user_id );
|
||||
$user_info = static::resolve_user_fields( $user, $manager->get_connected_user_data( $user_id ) );
|
||||
$is_owner = $manager->is_connection_owner( $user_id );
|
||||
|
||||
$has_other_connected_users = false;
|
||||
if ( $is_owner ) {
|
||||
$connected_users = $manager->get_connected_users( 'any', 2 );
|
||||
$has_other_connected_users = count( $connected_users ) > 1;
|
||||
}
|
||||
|
||||
return array_merge(
|
||||
$user_info,
|
||||
array(
|
||||
'isOwner' => $is_owner,
|
||||
'hasOtherConnectedUsers' => $has_other_connected_users,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the connection owner details for the script module.
|
||||
*
|
||||
* @param Manager $manager Connection manager instance.
|
||||
* @return array|null Owner data or null if unavailable.
|
||||
*/
|
||||
private static function get_connection_owner_data( $manager ) {
|
||||
$owner = $manager->get_connection_owner();
|
||||
|
||||
if ( false === $owner ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$fields = static::resolve_user_fields( $owner, $manager->get_connected_user_data( $owner->ID ) );
|
||||
|
||||
$fields['localLogin'] = $owner->user_login;
|
||||
|
||||
return $fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge local WP user fields with WordPress.com user data.
|
||||
*
|
||||
* WPCOM values take precedence when available. Returns the common
|
||||
* user shape used by both currentUser and connectionOwner.
|
||||
*
|
||||
* @param \WP_User|false $wp_user Local WordPress user object (false if unavailable).
|
||||
* @param array|false $wpcom_user_data WPCOM user data from the connection manager.
|
||||
* @return array User data with displayName, login, email, and avatar.
|
||||
*/
|
||||
private static function resolve_user_fields( $wp_user, $wpcom_user_data ) {
|
||||
$display_name = $wp_user ? $wp_user->display_name : '';
|
||||
$login = $wp_user ? $wp_user->user_login : '';
|
||||
$email = $wp_user ? $wp_user->user_email : '';
|
||||
|
||||
if ( is_array( $wpcom_user_data ) ) {
|
||||
if ( ! empty( $wpcom_user_data['display_name'] ) ) {
|
||||
$display_name = $wpcom_user_data['display_name'];
|
||||
}
|
||||
if ( ! empty( $wpcom_user_data['login'] ) ) {
|
||||
$login = $wpcom_user_data['login'];
|
||||
}
|
||||
if ( ! empty( $wpcom_user_data['email'] ) ) {
|
||||
$email = $wpcom_user_data['email'];
|
||||
}
|
||||
}
|
||||
|
||||
$user_id = $wp_user ? $wp_user->ID : 0;
|
||||
|
||||
return array(
|
||||
'displayName' => $display_name,
|
||||
'login' => $login,
|
||||
'email' => $email,
|
||||
'avatar' => $user_id
|
||||
? get_avatar_url(
|
||||
$user_id,
|
||||
array(
|
||||
'size' => 48,
|
||||
'default' => 'mysteryman',
|
||||
)
|
||||
)
|
||||
: '',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the given screen is the Connectors settings page.
|
||||
*
|
||||
* Handles both WP 7.0 core (`options-connectors`) and the Gutenberg
|
||||
* plugin (`settings_page_options-connectors-wp-admin`).
|
||||
*
|
||||
* @param \WP_Screen $screen Current admin screen.
|
||||
* @return bool
|
||||
*/
|
||||
private static function is_connectors_screen( $screen ) {
|
||||
return 'options-connectors' === $screen->id
|
||||
|| static::GUTENBERG_CONNECTORS_SCREEN_ID === $screen->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the admin-relative path for the Connectors page.
|
||||
*
|
||||
* WP 7.0 core uses the standalone `options-connectors.php` file while
|
||||
* the Gutenberg plugin registers a submenu page under options-general.php
|
||||
* with slug `options-connectors-wp-admin`. Both set parent_file to
|
||||
* `options-general.php` for menu highlighting, so we distinguish them by
|
||||
* checking the actual script filename being served.
|
||||
*
|
||||
* Note: for the Gutenberg case we use the registered page slug directly,
|
||||
* not `$screen->id`. WordPress auto-prefixes screen IDs for submenu pages
|
||||
* (e.g. `settings_page_options-connectors-wp-admin`), so using `$screen->id`
|
||||
* as the `page=` parameter produces an invalid URL.
|
||||
*
|
||||
* The result is suitable for the `redirect_uri` parameter accepted by the
|
||||
* `jetpack/v4/connection/register` REST endpoint (which wraps it in `admin_url()`).
|
||||
*
|
||||
* @return string Admin-relative path, e.g. 'options-connectors.php'.
|
||||
*/
|
||||
private static function get_connectors_page_path() {
|
||||
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- only compared against a hardcoded string.
|
||||
$script = isset( $_SERVER['SCRIPT_NAME'] ) ? wp_basename( wp_unslash( $_SERVER['SCRIPT_NAME'] ) ) : '';
|
||||
|
||||
if ( 'options-connectors.php' === $script ) {
|
||||
return 'options-connectors.php';
|
||||
}
|
||||
|
||||
// Gutenberg plugin registers the page under options-general.php.
|
||||
$screen = get_current_screen();
|
||||
if ( $screen && static::GUTENBERG_CONNECTORS_SCREEN_ID === $screen->id ) {
|
||||
return 'options-general.php?page=' . static::GUTENBERG_CONNECTORS_PAGE_SLUG;
|
||||
}
|
||||
|
||||
return 'options-connectors.php';
|
||||
}
|
||||
|
||||
/**
|
||||
* Store an authorization error in a short-lived transient.
|
||||
*
|
||||
* Hooked to `jetpack_client_authorize_error` which fires when
|
||||
* the auth webhook fails. The transient is read on the next
|
||||
* Connectors page load so the JS card can display the error.
|
||||
*
|
||||
* @since 8.2.0
|
||||
*
|
||||
* @param \WP_Error $error Authorization error.
|
||||
*/
|
||||
public static function store_auth_error( $error ) {
|
||||
if ( is_wp_error( $error ) ) {
|
||||
$user_id = get_current_user_id();
|
||||
if ( $user_id ) {
|
||||
set_transient(
|
||||
'jetpack_connector_auth_error_' . $user_id,
|
||||
$error->get_error_message(),
|
||||
60
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read and delete a stored authorization error for the current user.
|
||||
*
|
||||
* @return string|false Error message or false if none.
|
||||
*/
|
||||
private static function consume_auth_error() {
|
||||
$user_id = get_current_user_id();
|
||||
if ( ! $user_id ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$key = 'jetpack_connector_auth_error_' . $user_id;
|
||||
$error = get_transient( $key );
|
||||
if ( false !== $error ) {
|
||||
delete_transient( $key );
|
||||
}
|
||||
|
||||
return $error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get connected plugins data for the script module.
|
||||
*
|
||||
* @param Manager $manager Connection manager instance.
|
||||
* @return array List of connected plugin data.
|
||||
*/
|
||||
private static function get_connected_plugins_data( $manager ) {
|
||||
$plugins = $manager->get_connected_plugins();
|
||||
|
||||
if ( is_wp_error( $plugins ) || ! is_array( $plugins ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$result = array();
|
||||
|
||||
foreach ( $plugins as $slug => $plugin_data ) {
|
||||
$name = $plugin_data['name'] ?? $slug;
|
||||
|
||||
$entry = array(
|
||||
'name' => $name,
|
||||
'slug' => $slug,
|
||||
);
|
||||
|
||||
$logo_url = static::get_plugin_logo_url( $slug );
|
||||
if ( $logo_url ) {
|
||||
$entry['logoUrl'] = $logo_url;
|
||||
}
|
||||
|
||||
$result[] = $entry;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect which plugin families are using the connection.
|
||||
*
|
||||
* @since 8.5.0
|
||||
*
|
||||
* @return array{has_woo: bool, has_a4a: bool}
|
||||
*/
|
||||
public static function get_connected_plugin_families() {
|
||||
$plugins = Plugin_Storage::get_all();
|
||||
|
||||
$has_woo = false;
|
||||
$has_a4a = false;
|
||||
|
||||
if ( is_array( $plugins ) ) {
|
||||
foreach ( array_keys( $plugins ) as $slug ) {
|
||||
if ( str_starts_with( $slug, 'woocommerce' ) ) {
|
||||
$has_woo = true;
|
||||
}
|
||||
if ( str_starts_with( $slug, 'automattic' ) ) {
|
||||
$has_a4a = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return array(
|
||||
'has_woo' => $has_woo,
|
||||
'has_a4a' => $has_a4a,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the connector card logo based on which plugin families are connected.
|
||||
*
|
||||
* Priority:
|
||||
* 1. Both Woo-family and A4A plugins → jetpack-connect-all.svg
|
||||
* 2. Woo-family only → jetpack-connect-woo.svg
|
||||
* 3. A4A only → jetpack-connect-a8c.svg
|
||||
* 4. Default (Jetpack only or other) → jetpack-connect.svg
|
||||
*
|
||||
* @since 8.3.2
|
||||
*
|
||||
* @return string Logo URL.
|
||||
*/
|
||||
public static function get_connector_logo_url() {
|
||||
$families = self::get_connected_plugin_families();
|
||||
|
||||
if ( $families['has_woo'] && $families['has_a4a'] ) {
|
||||
return plugins_url( 'images/jetpack-connect-all.svg', __FILE__ );
|
||||
}
|
||||
|
||||
if ( $families['has_woo'] ) {
|
||||
return plugins_url( 'images/jetpack-connect-woo.svg', __FILE__ );
|
||||
}
|
||||
|
||||
if ( $families['has_a4a'] ) {
|
||||
return plugins_url( 'images/jetpack-connect-a8c.svg', __FILE__ );
|
||||
}
|
||||
|
||||
return plugins_url( 'images/jetpack-connect.svg', __FILE__ );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the inline (single-row) connector logo for use in compact contexts like table cells.
|
||||
*
|
||||
* All circles are arranged horizontally in a single row, unlike the card
|
||||
* logos which stack circles vertically for 3+ plugins.
|
||||
*
|
||||
* @since 8.5.0
|
||||
*
|
||||
* @return string Logo URL.
|
||||
*/
|
||||
public static function get_inline_connector_logo_url() {
|
||||
$families = self::get_connected_plugin_families();
|
||||
|
||||
if ( $families['has_woo'] && $families['has_a4a'] ) {
|
||||
return plugins_url( 'images/jetpack-connect-all-inline.svg', __FILE__ );
|
||||
}
|
||||
|
||||
if ( $families['has_woo'] ) {
|
||||
return plugins_url( 'images/jetpack-connect-woo-inline.svg', __FILE__ );
|
||||
}
|
||||
|
||||
if ( $families['has_a4a'] ) {
|
||||
return plugins_url( 'images/jetpack-connect-a8c-inline.svg', __FILE__ );
|
||||
}
|
||||
|
||||
return plugins_url( 'images/jetpack-connect.svg', __FILE__ );
|
||||
}
|
||||
|
||||
/**
|
||||
* Map a plugin slug to a brand logo URL.
|
||||
*
|
||||
* Jetpack-family plugins get the Jetpack mark, WooCommerce-family
|
||||
* plugins get the Woo mark, and Automattic for Agencies gets the
|
||||
* Automattic mark. Unknown slugs fall through to the
|
||||
* `jetpack_connection_plugin_logo_url` filter so that third-party
|
||||
* plugins can register their own logo. Only SVG URLs are accepted
|
||||
* to keep the icons sharp at every display density.
|
||||
*
|
||||
* @since 8.3.2
|
||||
*
|
||||
* @param string $slug Plugin slug.
|
||||
* @return string|null Logo URL or null.
|
||||
*/
|
||||
private static function get_plugin_logo_url( $slug ) {
|
||||
if ( str_starts_with( $slug, 'jetpack' ) ) {
|
||||
return plugins_url( 'images/jetpack-icon.svg', __FILE__ ); // str_starts_with() is polyfilled by WP since 5.9; this code only runs on WP 7.0+.
|
||||
}
|
||||
|
||||
if ( str_starts_with( $slug, 'woocommerce' ) ) {
|
||||
return plugins_url( 'images/woo-icon.svg', __FILE__ );
|
||||
}
|
||||
|
||||
if ( str_starts_with( $slug, 'automattic' ) ) {
|
||||
return plugins_url( 'images/automattic-icon.svg', __FILE__ );
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters a map of plugin slugs to custom logo URLs for the
|
||||
* Settings → Connectors card.
|
||||
*
|
||||
* Add entries as `$slug => $url` pairs. URLs must point to an
|
||||
* SVG file (`.svg` extension required); non-SVG values are
|
||||
* silently ignored and the generic fallback icon is shown.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* add_filter( 'jetpack_connection_plugin_logos', function ( $logos ) {
|
||||
* $logos['my-plugin'] = plugins_url( 'assets/logo.svg', __FILE__ );
|
||||
* return $logos;
|
||||
* } );
|
||||
*
|
||||
* @since 8.3.2
|
||||
*
|
||||
* @param array<string,string> $logos Map of plugin slug to SVG URL.
|
||||
*/
|
||||
$logos = apply_filters( 'jetpack_connection_plugin_logos', array() );
|
||||
|
||||
if ( isset( $logos[ $slug ] ) && is_string( $logos[ $slug ] ) && str_ends_with( strtolower( $logos[ $slug ] ), '.svg' ) ) {
|
||||
return esc_url( $logos[ $slug ] );
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
/**
|
||||
* Styles for the Jetpack connector card on the
|
||||
* WP core Settings > Connectors page (WP 7.0+).
|
||||
*
|
||||
* Enqueued by Jetpack_Connector::enqueue_script_module().
|
||||
* Uses CSS logical properties for RTL support.
|
||||
*
|
||||
* ConnectorItem renders inside .components-item which has
|
||||
* 20px padding (12px on <=480px screens). The divider uses
|
||||
* negative inline margins to span the full card width.
|
||||
*/
|
||||
|
||||
.jetpack-connector__description-padded {
|
||||
display: block;
|
||||
padding-inline-end: 40px;
|
||||
}
|
||||
|
||||
.jetpack-connector__expanded {
|
||||
margin-inline: -20px;
|
||||
margin-block-start: 4px;
|
||||
border-block-start: 1px solid #ddd;
|
||||
padding-block-start: 20px;
|
||||
padding-inline: 20px;
|
||||
}
|
||||
|
||||
.jetpack-connector__idc-options {
|
||||
flex-wrap: wrap;
|
||||
margin-block-end: 16px;
|
||||
}
|
||||
|
||||
/*
|
||||
* Stretch the option cards to equal height so their buttons stay aligned at
|
||||
* the bottom regardless of copy length (paired with margin-block-start: auto
|
||||
* on the button). On mobile each card wraps to its own row, so the stretch
|
||||
* has no effect and the stacked layout is unaffected.
|
||||
*
|
||||
* The selector is doubled to outrank the HStack's emotion-generated
|
||||
* `align-items: center/flex-start` rule, which has the same single-class
|
||||
* specificity but is injected later in the document.
|
||||
*/
|
||||
.jetpack-connector__idc-options.jetpack-connector__idc-options {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.jetpack-connector__idc-footer {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.jetpack-connector__idc-option {
|
||||
flex: 1 1 200px;
|
||||
min-inline-size: 200px;
|
||||
}
|
||||
|
||||
/*
|
||||
* Size the action to its label instead of stretching full-width (the option
|
||||
* card is a VStack, which stretches its children on the cross axis).
|
||||
* margin-block-start: auto keeps it pinned to the bottom so the two cards'
|
||||
* buttons stay aligned regardless of copy length.
|
||||
*/
|
||||
.jetpack-connector__idc-option .jetpack-connector__inline-action {
|
||||
align-self: flex-start;
|
||||
margin-inline-start: 0;
|
||||
margin-block-start: auto;
|
||||
}
|
||||
|
||||
.jetpack-connector__idc-safe-mode-link {
|
||||
margin-inline-start: 0;
|
||||
}
|
||||
|
||||
.jetpack-connector__idc-safe-mode-group {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.jetpack-connector__idc-choose {
|
||||
margin-block-start: 16px;
|
||||
}
|
||||
|
||||
.jetpack-connector__idc-why-toggle {
|
||||
align-self: flex-start;
|
||||
padding-inline: 0;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.jetpack-connector__idc-why-list {
|
||||
margin: 0;
|
||||
padding-inline-start: 20px;
|
||||
list-style: disc;
|
||||
font-size: 12px;
|
||||
color: #757575;
|
||||
}
|
||||
|
||||
.jetpack-connector__idc-why-list li {
|
||||
margin-block-end: 4px;
|
||||
}
|
||||
|
||||
.jetpack-connector__status-badge {
|
||||
padding-block: 4px;
|
||||
padding-inline: 12px;
|
||||
border-radius: 2px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.jetpack-connector__status-badge--connected {
|
||||
color: #345b37;
|
||||
background-color: #eff8f0;
|
||||
}
|
||||
|
||||
.jetpack-connector__status-badge--site-registered {
|
||||
color: #1e4c5e;
|
||||
background-color: #e9f0f5;
|
||||
}
|
||||
|
||||
.jetpack-connector__status-badge--safe-mode {
|
||||
color: #4f3500;
|
||||
background-color: #fcf0e5;
|
||||
}
|
||||
|
||||
.jetpack-connector__section {
|
||||
padding-block-end: 20px;
|
||||
}
|
||||
|
||||
.jetpack-connector__owner-avatar {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.jetpack-connector__user-link {
|
||||
text-decoration: underline;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.jetpack-connector__plugin-icon {
|
||||
inline-size: 20px;
|
||||
block-size: 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.jetpack-connector__plugin-icon--fallback {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.jetpack-connector__divider {
|
||||
border: none;
|
||||
border-block-start: 1px solid #ddd;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.jetpack-connector__details-link:focus:not(:focus-visible) {
|
||||
box-shadow: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.jetpack-connector__disconnect-site {
|
||||
margin-inline-start: auto;
|
||||
}
|
||||
|
||||
.jetpack-connector__modal .components-button:focus:not(:focus-visible),
|
||||
.jetpack-connector__confirm-modal .components-button:focus:not(:focus-visible) {
|
||||
box-shadow: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.jetpack-connector__modal.components-modal__frame,
|
||||
.jetpack-connector__modal {
|
||||
max-inline-size: 480px;
|
||||
}
|
||||
|
||||
.jetpack-connector__details-modal {
|
||||
display: grid;
|
||||
grid-template-columns: max-content 1fr;
|
||||
column-gap: 16px;
|
||||
row-gap: 10px;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.jetpack-connector__details-label {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.jetpack-connector__details-value {
|
||||
overflow-wrap: break-word;
|
||||
min-inline-size: 0;
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
.jetpack-connector__connect-prompt-text {
|
||||
flex: 1;
|
||||
min-inline-size: 0;
|
||||
}
|
||||
|
||||
.jetpack-connector__inline-action {
|
||||
margin-inline-start: auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
|
||||
.jetpack-connector__expanded {
|
||||
margin-inline: -12px;
|
||||
padding-block-start: 12px;
|
||||
padding-inline: 12px;
|
||||
}
|
||||
|
||||
.jetpack-connector__section.components-h-stack,
|
||||
.jetpack-connector__section > .components-h-stack {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.jetpack-connector__connect-prompt-text {
|
||||
min-inline-size: 100%;
|
||||
}
|
||||
|
||||
.jetpack-connector__inline-action {
|
||||
margin-inline-start: 0;
|
||||
}
|
||||
|
||||
.jetpack-connector__details-modal {
|
||||
grid-template-columns: 1fr;
|
||||
row-gap: 4px;
|
||||
}
|
||||
|
||||
.jetpack-connector__details-label:not(:first-child) {
|
||||
margin-block-start: 8px;
|
||||
}
|
||||
|
||||
.jetpack-connector__details-value {
|
||||
text-align: start;
|
||||
}
|
||||
}
|
||||
|
||||
.jetpack-connector__notice.components-notice {
|
||||
margin-block: 8px 0;
|
||||
margin-inline: 0;
|
||||
}
|
||||
|
||||
.jetpack-connector__tos-notice {
|
||||
margin-block-start: 8px;
|
||||
}
|
||||
|
||||
.jetpack-connector__tos-notice a {
|
||||
color: inherit;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" clip-rule="evenodd" fill="none" fill-rule="evenodd" viewBox="0 0 47 43">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M39.3164 20.9589C39.3164 13.2129 33.656 6.30469 23.4214 6.30469C13.1867 6.30469 7.58624 13.2129 7.58624 20.9589V21.9206C7.58624 29.6677 13.1867 36.6942 23.4214 36.6942C33.656 36.6942 39.3164 29.6677 39.3164 21.9206V20.9589ZM23.4214 43C9.21187 43 0 32.7913 0 22.1604V20.8407C0 10.0285 9.21187 0 23.4214 0C37.6919 0 46.9038 10.0285 46.9038 20.8407V22.1604C46.9038 32.7913 37.6919 43 23.4214 43Z" fill="#00A3E0"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M28.4548 14.1897C29.752 15.0401 30.1158 16.8077 29.2739 18.1343L22.712 28.4682C21.8691 29.7959 20.1348 30.1799 18.8397 29.3295C17.5446 28.477 17.1765 26.7137 18.0205 25.386L24.5824 15.0522C25.4253 13.7256 27.1597 13.3404 28.4548 14.1897Z" fill="#000000"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 887 B |
@@ -0,0 +1 @@
|
||||
<svg width="87" height="34" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M30 17c0-8.265-6.735-15-15-15C6.72 2 0 8.735 0 17c0 8.28 6.72 15 15 15 8.265 0 15-6.72 15-15Zm-18.33 8.055L6.555 11.33c.825-.03 1.755-.12 1.755-.12.75-.09.66-1.695-.09-1.665 0 0-2.175.165-3.555.165-.27 0-.555 0-.87-.015 2.385-3.66 6.51-6.03 11.205-6.03 3.495 0 6.675 1.305 9.075 3.51-1.02-.165-2.475.585-2.475 2.37 0 1.11.675 2.04 1.35 3.15.525.915.825 2.04.825 3.69 0 2.235-2.1 7.5-2.1 7.5L17.13 11.33c.81-.03 1.23-.255 1.23-.255.75-.075.66-1.875-.09-1.83 0 0-2.16.18-3.57.18-1.305 0-3.495-.18-3.495-.18-.75-.045-.84 1.8-.09 1.83l1.38.12 1.89 5.115-2.715 8.745ZM26.115 17c.36-.96 1.11-2.805.645-6.375 1.05 1.935 1.575 4.065 1.575 6.375 0 4.935-2.595 9.36-6.6 11.67 1.455-3.885 2.91-7.8 4.38-11.67ZM9.15 29.135C4.68 26.975 1.665 22.295 1.665 17c0-1.95.345-3.72 1.08-5.385 2.13 5.835 4.26 11.685 6.405 17.52Zm6.045-9.945 3.87 10.47c-1.29.435-2.64.675-4.065.675-1.185 0-2.355-.165-3.435-.495 1.215-3.57 2.43-7.11 3.63-10.65Z" fill="#3F5BE0"/><rect x="26" y="2" width="30" height="30" rx="15" fill="#fff"/><path fill-rule="evenodd" clip-rule="evenodd" d="M57.637 17c0 9.188-7.448 16.636-16.636 16.636-9.188 0-16.637-7.448-16.637-16.636 0-9.188 7.449-16.636 16.637-16.636S57.637 7.812 57.637 17ZM41.001 32c8.284 0 15-6.716 15-15 0-8.284-6.716-15-15-15-8.285 0-15 6.716-15 15 0 8.284 6.715 15 15 15Z" fill="#fff"/><path d="M41 2c-8.264 0-15 6.718-15 15s6.718 15 15 15 15-6.718 15-15S49.282 2 41 2Zm-.773 17.485h-7.472l7.472-14.54v14.54Zm1.528 9.533v-14.54h7.454l-7.454 14.54Z" fill="#069E08"/><rect x="52" y="2" width="33" height="30" rx="15" fill="#fff"/><g transform="translate(52, 2) scale(0.69767)"><path fill-rule="evenodd" clip-rule="evenodd" d="M39.3164 20.9589C39.3164 13.2129 33.656 6.30469 23.4214 6.30469C13.1867 6.30469 7.58624 13.2129 7.58624 20.9589V21.9206C7.58624 29.6677 13.1867 36.6942 23.4214 36.6942C33.656 36.6942 39.3164 29.6677 39.3164 21.9206V20.9589ZM23.4214 43C9.21187 43 0 32.7913 0 22.1604V20.8407C0 10.0285 9.21187 0 23.4214 0C37.6919 0 46.9038 10.0285 46.9038 20.8407V22.1604C46.9038 32.7913 37.6919 43 23.4214 43Z" fill="#00A3E0"/><path fill-rule="evenodd" clip-rule="evenodd" d="M28.4548 14.1897C29.752 15.0401 30.1158 16.8077 29.2739 18.1343L22.712 28.4682C21.8691 29.7959 20.1348 30.1799 18.8397 29.3295C17.5446 28.477 17.1765 26.7137 18.0205 25.386L24.5824 15.0522C25.4253 13.7256 27.1597 13.3404 28.4548 14.1897Z" fill="#000"/></g></svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
@@ -0,0 +1 @@
|
||||
<svg width="58" height="58" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M30 17c0-8.265-6.735-15-15-15C6.72 2 0 8.735 0 17c0 8.28 6.72 15 15 15 8.265 0 15-6.72 15-15Zm-18.33 8.055L6.555 11.33c.825-.03 1.755-.12 1.755-.12.75-.09.66-1.695-.09-1.665 0 0-2.175.165-3.555.165-.27 0-.555 0-.87-.015 2.385-3.66 6.51-6.03 11.205-6.03 3.495 0 6.675 1.305 9.075 3.51-1.02-.165-2.475.585-2.475 2.37 0 1.11.675 2.04 1.35 3.15.525.915.825 2.04.825 3.69 0 2.235-2.1 7.5-2.1 7.5L17.13 11.33c.81-.03 1.23-.255 1.23-.255.75-.075.66-1.875-.09-1.83 0 0-2.16.18-3.57.18-1.305 0-3.495-.18-3.495-.18-.75-.045-.84 1.8-.09 1.83l1.38.12 1.89 5.115-2.715 8.745ZM26.115 17c.36-.96 1.11-2.805.645-6.375 1.05 1.935 1.575 4.065 1.575 6.375 0 4.935-2.595 9.36-6.6 11.67 1.455-3.885 2.91-7.8 4.38-11.67ZM9.15 29.135C4.68 26.975 1.665 22.295 1.665 17c0-1.95.345-3.72 1.08-5.385 2.13 5.835 4.26 11.685 6.405 17.52Zm6.045-9.945 3.87 10.47c-1.29.435-2.64.675-4.065.675-1.185 0-2.355-.165-3.435-.495 1.215-3.57 2.43-7.11 3.63-10.65Z" fill="#3F5BE0"/><rect x="26" y="2" width="30" height="30" rx="15" fill="#fff"/><path fill-rule="evenodd" clip-rule="evenodd" d="M57.637 17c0 9.188-7.448 16.636-16.636 16.636-9.188 0-16.637-7.448-16.637-16.636 0-9.188 7.449-16.636 16.637-16.636S57.637 7.812 57.637 17ZM41.001 32c8.284 0 15-6.716 15-15 0-8.284-6.716-15-15-15-8.285 0-15 6.716-15 15 0 8.284 6.715 15 15 15Z" fill="#fff"/><path d="M41 2c-8.264 0-15 6.718-15 15s6.718 15 15 15 15-6.718 15-15S49.282 2 41 2Zm-.773 17.485h-7.472l7.472-14.54v14.54Zm1.528 9.533v-14.54h7.454l-7.454 14.54Z" fill="#069E08"/><circle cx="28" cy="43" r="16" fill="#fff"/><g transform="translate(13,29.3) scale(0.6383)"><path fill-rule="evenodd" clip-rule="evenodd" d="M39.3164 20.9589C39.3164 13.2129 33.656 6.30469 23.4214 6.30469C13.1867 6.30469 7.58624 13.2129 7.58624 20.9589V21.9206C7.58624 29.6677 13.1867 36.6942 23.4214 36.6942C33.656 36.6942 39.3164 29.6677 39.3164 21.9206V20.9589ZM23.4214 43C9.21187 43 0 32.7913 0 22.1604V20.8407C0 10.0285 9.21187 0 23.4214 0C37.6919 0 46.9038 10.0285 46.9038 20.8407V22.1604C46.9038 32.7913 37.6919 43 23.4214 43Z" fill="#00A3E0"/><path fill-rule="evenodd" clip-rule="evenodd" d="M28.4548 14.1897C29.752 15.0401 30.1158 16.8077 29.2739 18.1343L22.712 28.4682C21.8691 29.7959 20.1348 30.1799 18.8397 29.3295C17.5446 28.477 17.1765 26.7137 18.0205 25.386L24.5824 15.0522C25.4253 13.7256 27.1597 13.3404 28.4548 14.1897Z" fill="#000"/></g></svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
@@ -0,0 +1 @@
|
||||
<svg width="113" height="34" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M30 17c0-8.265-6.735-15-15-15C6.72 2 0 8.735 0 17c0 8.28 6.72 15 15 15 8.265 0 15-6.72 15-15Zm-18.33 8.055L6.555 11.33c.825-.03 1.755-.12 1.755-.12.75-.09.66-1.695-.09-1.665 0 0-2.175.165-3.555.165-.27 0-.555 0-.87-.015 2.385-3.66 6.51-6.03 11.205-6.03 3.495 0 6.675 1.305 9.075 3.51-1.02-.165-2.475.585-2.475 2.37 0 1.11.675 2.04 1.35 3.15.525.915.825 2.04.825 3.69 0 2.235-2.1 7.5-2.1 7.5L17.13 11.33c.81-.03 1.23-.255 1.23-.255.75-.075.66-1.875-.09-1.83 0 0-2.16.18-3.57.18-1.305 0-3.495-.18-3.495-.18-.75-.045-.84 1.8-.09 1.83l1.38.12 1.89 5.115-2.715 8.745ZM26.115 17c.36-.96 1.11-2.805.645-6.375 1.05 1.935 1.575 4.065 1.575 6.375 0 4.935-2.595 9.36-6.6 11.67 1.455-3.885 2.91-7.8 4.38-11.67ZM9.15 29.135C4.68 26.975 1.665 22.295 1.665 17c0-1.95.345-3.72 1.08-5.385 2.13 5.835 4.26 11.685 6.405 17.52Zm6.045-9.945 3.87 10.47c-1.29.435-2.64.675-4.065.675-1.185 0-2.355-.165-3.435-.495 1.215-3.57 2.43-7.11 3.63-10.65Z" fill="#3F5BE0"/><rect x="26" y="2" width="30" height="30" rx="15" fill="#fff"/><path fill-rule="evenodd" clip-rule="evenodd" d="M57.637 17c0 9.188-7.448 16.636-16.636 16.636-9.188 0-16.637-7.448-16.637-16.636 0-9.188 7.449-16.636 16.637-16.636S57.637 7.812 57.637 17ZM41.001 32c8.284 0 15-6.716 15-15 0-8.284-6.716-15-15-15-8.285 0-15 6.716-15 15 0 8.284 6.715 15 15 15Z" fill="#fff"/><path d="M41 2c-8.264 0-15 6.718-15 15s6.718 15 15 15 15-6.718 15-15S49.282 2 41 2Zm-.773 17.485h-7.472l7.472-14.54v14.54Zm1.528 9.533v-14.54h7.454l-7.454 14.54Z" fill="#069E08"/><rect x="52" y="2" width="30" height="30" rx="15" fill="#fff"/><g transform="translate(52, 2) scale(1.5)"><path fill="#873EFF" d="M10 0C15.5228 0 20 4.47715 20 10C20 15.5228 15.5228 20 10 20C4.47715 20 0 15.5228 0 10C0 4.47715 4.47715 0 10 0ZM3.96289 6.48242C3.18274 6.48255 2.78715 6.84526 2.78711 7.51562C2.78716 8.186 3.20472 8.5711 3.96289 8.5713H4.80957V12.583C4.80963 13.7152 5.5685 14.3857 6.67871 14.3857C7.56882 14.3856 8.2837 13.9465 8.8223 12.9355L10.0205 10.6924V12.5947C10.0207 13.7156 10.7461 14.3857 11.8672 14.3857C12.7463 14.3856 13.3951 14.0014 14.0215 12.9355L16.7803 8.2744C17.3848 7.25229 16.9565 6.4827 15.627 6.48242C14.9124 6.48242 14.4499 6.71367 14.0322 7.49414L12.1309 11.0664V7.88965C12.1309 6.94438 11.68 6.48252 10.8447 6.48242C10.1852 6.48242 9.6577 6.76825 9.251 7.55957L7.45898 11.0664V7.92285C7.45898 6.91172 7.04118 6.48258 6.03027 6.48242H3.96289Z"/></g><rect x="78" y="2" width="33" height="30" rx="15" fill="#fff"/><g transform="translate(78, 2) scale(0.69767)"><path fill-rule="evenodd" clip-rule="evenodd" d="M39.3164 20.9589C39.3164 13.2129 33.656 6.30469 23.4214 6.30469C13.1867 6.30469 7.58624 13.2129 7.58624 20.9589V21.9206C7.58624 29.6677 13.1867 36.6942 23.4214 36.6942C33.656 36.6942 39.3164 29.6677 39.3164 21.9206V20.9589ZM23.4214 43C9.21187 43 0 32.7913 0 22.1604V20.8407C0 10.0285 9.21187 0 23.4214 0C37.6919 0 46.9038 10.0285 46.9038 20.8407V22.1604C46.9038 32.7913 37.6919 43 23.4214 43Z" fill="#00A3E0"/><path fill-rule="evenodd" clip-rule="evenodd" d="M28.4548 14.1897C29.752 15.0401 30.1158 16.8077 29.2739 18.1343L22.712 28.4682C21.8691 29.7959 20.1348 30.1799 18.8397 29.3295C17.5446 28.477 17.1765 26.7137 18.0205 25.386L24.5824 15.0522C25.4253 13.7256 27.1597 13.3404 28.4548 14.1897Z" fill="#000"/></g></svg>
|
||||
|
After Width: | Height: | Size: 3.3 KiB |
@@ -0,0 +1 @@
|
||||
<svg width="58" height="58" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M30 17c0-8.265-6.735-15-15-15C6.72 2 0 8.735 0 17c0 8.28 6.72 15 15 15 8.265 0 15-6.72 15-15Zm-18.33 8.055L6.555 11.33c.825-.03 1.755-.12 1.755-.12.75-.09.66-1.695-.09-1.665 0 0-2.175.165-3.555.165-.27 0-.555 0-.87-.015 2.385-3.66 6.51-6.03 11.205-6.03 3.495 0 6.675 1.305 9.075 3.51-1.02-.165-2.475.585-2.475 2.37 0 1.11.675 2.04 1.35 3.15.525.915.825 2.04.825 3.69 0 2.235-2.1 7.5-2.1 7.5L17.13 11.33c.81-.03 1.23-.255 1.23-.255.75-.075.66-1.875-.09-1.83 0 0-2.16.18-3.57.18-1.305 0-3.495-.18-3.495-.18-.75-.045-.84 1.8-.09 1.83l1.38.12 1.89 5.115-2.715 8.745ZM26.115 17c.36-.96 1.11-2.805.645-6.375 1.05 1.935 1.575 4.065 1.575 6.375 0 4.935-2.595 9.36-6.6 11.67 1.455-3.885 2.91-7.8 4.38-11.67ZM9.15 29.135C4.68 26.975 1.665 22.295 1.665 17c0-1.95.345-3.72 1.08-5.385 2.13 5.835 4.26 11.685 6.405 17.52Zm6.045-9.945 3.87 10.47c-1.29.435-2.64.675-4.065.675-1.185 0-2.355-.165-3.435-.495 1.215-3.57 2.43-7.11 3.63-10.65Z" fill="#3F5BE0"/><rect x="26" y="2" width="30" height="30" rx="15" fill="#fff"/><path fill-rule="evenodd" clip-rule="evenodd" d="M57.637 17c0 9.188-7.448 16.636-16.636 16.636-9.188 0-16.637-7.448-16.637-16.636 0-9.188 7.449-16.636 16.637-16.636S57.637 7.812 57.637 17ZM41.001 32c8.284 0 15-6.716 15-15 0-8.284-6.716-15-15-15-8.285 0-15 6.716-15 15 0 8.284 6.715 15 15 15Z" fill="#fff"/><path d="M41 2c-8.264 0-15 6.718-15 15s6.718 15 15 15 15-6.718 15-15S49.282 2 41 2Zm-.773 17.485h-7.472l7.472-14.54v14.54Zm1.528 9.533v-14.54h7.454l-7.454 14.54Z" fill="#069E08"/><circle cx="15" cy="43" r="16" fill="#fff"/><g transform="translate(0,28) scale(1.5)"><path fill="#873EFF" d="M10 0C15.5228 0 20 4.47715 20 10C20 15.5228 15.5228 20 10 20C4.47715 20 0 15.5228 0 10C0 4.47715 4.47715 0 10 0ZM3.96289 6.48242C3.18274 6.48255 2.78715 6.84526 2.78711 7.51562C2.78716 8.186 3.20472 8.5711 3.96289 8.5713H4.80957V12.583C4.80963 13.7152 5.5685 14.3857 6.67871 14.3857C7.56882 14.3856 8.2837 13.9465 8.8223 12.9355L10.0205 10.6924V12.5947C10.0207 13.7156 10.7461 14.3857 11.8672 14.3857C12.7463 14.3856 13.3951 14.0014 14.0215 12.9355L16.7803 8.2744C17.3848 7.25229 16.9565 6.4827 15.627 6.48242C14.9124 6.48242 14.4499 6.71367 14.0322 7.49414L12.1309 11.0664V7.88965C12.1309 6.94438 11.68 6.48252 10.8447 6.48242C10.1852 6.48242 9.6577 6.76825 9.251 7.55957L7.45898 11.0664V7.92285C7.45898 6.91172 7.04118 6.48258 6.03027 6.48242H3.96289Z"/></g><circle cx="41" cy="43" r="16" fill="#fff"/><g transform="translate(26,29.3) scale(0.6383)"><path fill-rule="evenodd" clip-rule="evenodd" d="M39.3164 20.9589C39.3164 13.2129 33.656 6.30469 23.4214 6.30469C13.1867 6.30469 7.58624 13.2129 7.58624 20.9589V21.9206C7.58624 29.6677 13.1867 36.6942 23.4214 36.6942C33.656 36.6942 39.3164 29.6677 39.3164 21.9206V20.9589ZM23.4214 43C9.21187 43 0 32.7913 0 22.1604V20.8407C0 10.0285 9.21187 0 23.4214 0C37.6919 0 46.9038 10.0285 46.9038 20.8407V22.1604C46.9038 32.7913 37.6919 43 23.4214 43Z" fill="#00A3E0"/><path fill-rule="evenodd" clip-rule="evenodd" d="M28.4548 14.1897C29.752 15.0401 30.1158 16.8077 29.2739 18.1343L22.712 28.4682C21.8691 29.7959 20.1348 30.1799 18.8397 29.3295C17.5446 28.477 17.1765 26.7137 18.0205 25.386L24.5824 15.0522C25.4253 13.7256 27.1597 13.3404 28.4548 14.1897Z" fill="#000"/></g></svg>
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
@@ -0,0 +1 @@
|
||||
<svg width="84" height="34" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M30 17c0-8.265-6.735-15-15-15C6.72 2 0 8.735 0 17c0 8.28 6.72 15 15 15 8.265 0 15-6.72 15-15Zm-18.33 8.055L6.555 11.33c.825-.03 1.755-.12 1.755-.12.75-.09.66-1.695-.09-1.665 0 0-2.175.165-3.555.165-.27 0-.555 0-.87-.015 2.385-3.66 6.51-6.03 11.205-6.03 3.495 0 6.675 1.305 9.075 3.51-1.02-.165-2.475.585-2.475 2.37 0 1.11.675 2.04 1.35 3.15.525.915.825 2.04.825 3.69 0 2.235-2.1 7.5-2.1 7.5L17.13 11.33c.81-.03 1.23-.255 1.23-.255.75-.075.66-1.875-.09-1.83 0 0-2.16.18-3.57.18-1.305 0-3.495-.18-3.495-.18-.75-.045-.84 1.8-.09 1.83l1.38.12 1.89 5.115-2.715 8.745ZM26.115 17c.36-.96 1.11-2.805.645-6.375 1.05 1.935 1.575 4.065 1.575 6.375 0 4.935-2.595 9.36-6.6 11.67 1.455-3.885 2.91-7.8 4.38-11.67ZM9.15 29.135C4.68 26.975 1.665 22.295 1.665 17c0-1.95.345-3.72 1.08-5.385 2.13 5.835 4.26 11.685 6.405 17.52Zm6.045-9.945 3.87 10.47c-1.29.435-2.64.675-4.065.675-1.185 0-2.355-.165-3.435-.495 1.215-3.57 2.43-7.11 3.63-10.65Z" fill="#3F5BE0"/><rect x="26" y="2" width="30" height="30" rx="15" fill="#fff"/><path fill-rule="evenodd" clip-rule="evenodd" d="M57.637 17c0 9.188-7.448 16.636-16.636 16.636-9.188 0-16.637-7.448-16.637-16.636 0-9.188 7.449-16.636 16.637-16.636S57.637 7.812 57.637 17ZM41.001 32c8.284 0 15-6.716 15-15 0-8.284-6.716-15-15-15-8.285 0-15 6.716-15 15 0 8.284 6.715 15 15 15Z" fill="#fff"/><path d="M41 2c-8.264 0-15 6.718-15 15s6.718 15 15 15 15-6.718 15-15S49.282 2 41 2Zm-.773 17.485h-7.472l7.472-14.54v14.54Zm1.528 9.533v-14.54h7.454l-7.454 14.54Z" fill="#069E08"/><rect x="52" y="2" width="30" height="30" rx="15" fill="#fff"/><g transform="translate(52, 2) scale(1.5)"><path fill="#873EFF" d="M10 0C15.5228 0 20 4.47715 20 10C20 15.5228 15.5228 20 10 20C4.47715 20 0 15.5228 0 10C0 4.47715 4.47715 0 10 0ZM3.96289 6.48242C3.18274 6.48255 2.78715 6.84526 2.78711 7.51562C2.78716 8.186 3.20472 8.5711 3.96289 8.5713H4.80957V12.583C4.80963 13.7152 5.5685 14.3857 6.67871 14.3857C7.56882 14.3856 8.2837 13.9465 8.8223 12.9355L10.0205 10.6924V12.5947C10.0207 13.7156 10.7461 14.3857 11.8672 14.3857C12.7463 14.3856 13.3951 14.0014 14.0215 12.9355L16.7803 8.2744C17.3848 7.25229 16.9565 6.4827 15.627 6.48242C14.9124 6.48242 14.4499 6.71367 14.0322 7.49414L12.1309 11.0664V7.88965C12.1309 6.94438 11.68 6.48252 10.8447 6.48242C10.1852 6.48242 9.6577 6.76825 9.251 7.55957L7.45898 11.0664V7.92285C7.45898 6.91172 7.04118 6.48258 6.03027 6.48242H3.96289Z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
@@ -0,0 +1 @@
|
||||
<svg width="58" height="58" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M30 17c0-8.265-6.735-15-15-15C6.72 2 0 8.735 0 17c0 8.28 6.72 15 15 15 8.265 0 15-6.72 15-15Zm-18.33 8.055L6.555 11.33c.825-.03 1.755-.12 1.755-.12.75-.09.66-1.695-.09-1.665 0 0-2.175.165-3.555.165-.27 0-.555 0-.87-.015 2.385-3.66 6.51-6.03 11.205-6.03 3.495 0 6.675 1.305 9.075 3.51-1.02-.165-2.475.585-2.475 2.37 0 1.11.675 2.04 1.35 3.15.525.915.825 2.04.825 3.69 0 2.235-2.1 7.5-2.1 7.5L17.13 11.33c.81-.03 1.23-.255 1.23-.255.75-.075.66-1.875-.09-1.83 0 0-2.16.18-3.57.18-1.305 0-3.495-.18-3.495-.18-.75-.045-.84 1.8-.09 1.83l1.38.12 1.89 5.115-2.715 8.745ZM26.115 17c.36-.96 1.11-2.805.645-6.375 1.05 1.935 1.575 4.065 1.575 6.375 0 4.935-2.595 9.36-6.6 11.67 1.455-3.885 2.91-7.8 4.38-11.67ZM9.15 29.135C4.68 26.975 1.665 22.295 1.665 17c0-1.95.345-3.72 1.08-5.385 2.13 5.835 4.26 11.685 6.405 17.52Zm6.045-9.945 3.87 10.47c-1.29.435-2.64.675-4.065.675-1.185 0-2.355-.165-3.435-.495 1.215-3.57 2.43-7.11 3.63-10.65Z" fill="#3F5BE0"/><rect x="26" y="2" width="30" height="30" rx="15" fill="#fff"/><path fill-rule="evenodd" clip-rule="evenodd" d="M57.637 17c0 9.188-7.448 16.636-16.636 16.636-9.188 0-16.637-7.448-16.637-16.636 0-9.188 7.449-16.636 16.637-16.636S57.637 7.812 57.637 17ZM41.001 32c8.284 0 15-6.716 15-15 0-8.284-6.716-15-15-15-8.285 0-15 6.716-15 15 0 8.284 6.715 15 15 15Z" fill="#fff"/><path d="M41 2c-8.264 0-15 6.718-15 15s6.718 15 15 15 15-6.718 15-15S49.282 2 41 2Zm-.773 17.485h-7.472l7.472-14.54v14.54Zm1.528 9.533v-14.54h7.454l-7.454 14.54Z" fill="#069E08"/><circle cx="28" cy="43" r="16" fill="#fff"/><g transform="translate(13,28) scale(1.5)"><path fill="#873EFF" d="M10 0C15.5228 0 20 4.47715 20 10C20 15.5228 15.5228 20 10 20C4.47715 20 0 15.5228 0 10C0 4.47715 4.47715 0 10 0ZM3.96289 6.48242C3.18274 6.48255 2.78715 6.84526 2.78711 7.51562C2.78716 8.186 3.20472 8.5711 3.96289 8.5713H4.80957V12.583C4.80963 13.7152 5.5685 14.3857 6.67871 14.3857C7.56882 14.3856 8.2837 13.9465 8.8223 12.9355L10.0205 10.6924V12.5947C10.0207 13.7156 10.7461 14.3857 11.8672 14.3857C12.7463 14.3856 13.3951 14.0014 14.0215 12.9355L16.7803 8.2744C17.3848 7.25229 16.9565 6.4827 15.627 6.48242C14.9124 6.48242 14.4499 6.71367 14.0322 7.49414L12.1309 11.0664V7.88965C12.1309 6.94438 11.68 6.48252 10.8447 6.48242C10.1852 6.48242 9.6577 6.76825 9.251 7.55957L7.45898 11.0664V7.92285C7.45898 6.91172 7.04118 6.48258 6.03027 6.48242H3.96289Z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
@@ -0,0 +1 @@
|
||||
<svg width="58" height="34" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M30 17c0-8.265-6.735-15-15-15C6.72 2 0 8.735 0 17c0 8.28 6.72 15 15 15 8.265 0 15-6.72 15-15Zm-18.33 8.055L6.555 11.33c.825-.03 1.755-.12 1.755-.12.75-.09.66-1.695-.09-1.665 0 0-2.175.165-3.555.165-.27 0-.555 0-.87-.015 2.385-3.66 6.51-6.03 11.205-6.03 3.495 0 6.675 1.305 9.075 3.51-1.02-.165-2.475.585-2.475 2.37 0 1.11.675 2.04 1.35 3.15.525.915.825 2.04.825 3.69 0 2.235-2.1 7.5-2.1 7.5L17.13 11.33c.81-.03 1.23-.255 1.23-.255.75-.075.66-1.875-.09-1.83 0 0-2.16.18-3.57.18-1.305 0-3.495-.18-3.495-.18-.75-.045-.84 1.8-.09 1.83l1.38.12 1.89 5.115-2.715 8.745ZM26.115 17c.36-.96 1.11-2.805.645-6.375 1.05 1.935 1.575 4.065 1.575 6.375 0 4.935-2.595 9.36-6.6 11.67 1.455-3.885 2.91-7.8 4.38-11.67ZM9.15 29.135C4.68 26.975 1.665 22.295 1.665 17c0-1.95.345-3.72 1.08-5.385 2.13 5.835 4.26 11.685 6.405 17.52Zm6.045-9.945 3.87 10.47c-1.29.435-2.64.675-4.065.675-1.185 0-2.355-.165-3.435-.495 1.215-3.57 2.43-7.11 3.63-10.65Z" fill="#3F5BE0"/><rect x="26" y="2" width="30" height="30" rx="15" fill="#fff"/><path fill-rule="evenodd" clip-rule="evenodd" d="M57.637 17c0 9.188-7.448 16.636-16.636 16.636-9.188 0-16.637-7.448-16.637-16.636 0-9.188 7.449-16.636 16.637-16.636S57.637 7.812 57.637 17ZM41.001 32c8.284 0 15-6.716 15-15 0-8.284-6.716-15-15-15-8.285 0-15 6.716-15 15 0 8.284 6.715 15 15 15Z" fill="#fff"/><path d="M41 2c-8.264 0-15 6.718-15 15s6.718 15 15 15 15-6.718 15-15S49.282 2 41 2Zm-.773 17.485h-7.472l7.472-14.54v14.54Zm1.528 9.533v-14.54h7.454l-7.454 14.54Z" fill="#069E08"/></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<circle cx="16" cy="16" r="16" fill="#069E08"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.3295 3.17557V18.6565H7.36005L15.3295 3.17557ZM16.9478 28.8244V13.313H24.9478L16.9478 28.8244Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 279 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||
<path fill="#873EFF" d="M10 0C15.5228 0 20 4.47715 20 10C20 15.5228 15.5228 20 10 20C4.47715 20 0 15.5228 0 10C0 4.47715 4.47715 0 10 0ZM3.96289 6.48242C3.18274 6.48255 2.78715 6.84526 2.78711 7.51562C2.78716 8.186 3.20472 8.5711 3.96289 8.5713H4.80957V12.583C4.80963 13.7152 5.5685 14.3857 6.67871 14.3857C7.56882 14.3856 8.2837 13.9465 8.8223 12.9355L10.0205 10.6924V12.5947C10.0207 13.7156 10.7461 14.3857 11.8672 14.3857C12.7463 14.3856 13.3951 14.0014 14.0215 12.9355L16.7803 8.2744C17.3848 7.25229 16.9565 6.4827 15.627 6.48242C14.9124 6.48242 14.4499 6.71367 14.0322 7.49414L12.1309 11.0664V7.88965C12.1309 6.94438 11.68 6.48252 10.8447 6.48242C10.1852 6.48242 9.6577 6.76825 9.251 7.55957L7.45898 11.0664V7.92285C7.45898 6.91172 7.04118 6.48258 6.03027 6.48242H3.96289Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 849 B |
@@ -0,0 +1,3 @@
|
||||
<svg viewBox="0 0 72 72" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="#3858E9" d="M36,0C16.1,0,0,16.1,0,36c0,19.9,16.1,36,36,36c19.9,0,36-16.2,36-36C72,16.1,55.8,0,36,0z M3.6,36 c0-4.7,1-9.1,2.8-13.2l15.4,42.3C11.1,59.9,3.6,48.8,3.6,36z M36,68.4c-3.2,0-6.2-0.5-9.1-1.3l9.7-28.2l9.9,27.3 c0.1,0.2,0.1,0.3,0.2,0.4C43.4,67.7,39.8,68.4,36,68.4z M40.5,20.8c1.9-0.1,3.7-0.3,3.7-0.3c1.7-0.2,1.5-2.8-0.2-2.7 c0,0-5.2,0.4-8.6,0.4c-3.2,0-8.5-0.4-8.5-0.4c-1.7-0.1-2,2.6-0.2,2.7c0,0,1.7,0.2,3.4,0.3l5,13.8L28,55.9L16.2,20.8 c2-0.1,3.7-0.3,3.7-0.3c1.7-0.2,1.5-2.8-0.2-2.7c0,0-5.2,0.4-8.6,0.4c-0.6,0-1.3,0-2.1,0C14.7,9.4,24.7,3.6,36,3.6 c8.4,0,16.1,3.2,21.9,8.5c-0.1,0-0.3,0-0.4,0c-3.2,0-5.4,2.8-5.4,5.7c0,2.7,1.5,4.9,3.2,7.6c1.2,2.2,2.7,4.9,2.7,8.9 c0,2.8-0.8,6.3-2.5,10.5l-3.2,10.8L40.5,20.8z M52.3,64l9.9-28.6c1.8-4.6,2.5-8.3,2.5-11.6c0-1.2-0.1-2.3-0.2-3.3 c2.5,4.6,4,9.9,4,15.5C68.4,47.9,61.9,58.4,52.3,64z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 910 B |
@@ -0,0 +1,724 @@
|
||||
<?php
|
||||
/**
|
||||
* Base class for Jetpack Connection health tests.
|
||||
*
|
||||
* @package automattic/jetpack-connection
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Connection;
|
||||
|
||||
use Automattic\Jetpack\Redirect;
|
||||
use Automattic\Jetpack\Status;
|
||||
use WP_Error;
|
||||
|
||||
/**
|
||||
* Base framework for connection health tests.
|
||||
*
|
||||
* Provides test registration, execution, result formatting, and output
|
||||
* for WordPress Site Health integration.
|
||||
*
|
||||
* Individual tests should be added in the Connection_Health_Tests class.
|
||||
*
|
||||
* @since 8.5.0
|
||||
*/
|
||||
class Connection_Health_Test_Base {
|
||||
|
||||
/**
|
||||
* Tests to run on the Jetpack connection.
|
||||
*
|
||||
* @var array $tests
|
||||
*/
|
||||
protected $tests = array();
|
||||
|
||||
/**
|
||||
* Results of the Jetpack connection tests.
|
||||
*
|
||||
* @var array $results
|
||||
*/
|
||||
protected $results = array();
|
||||
|
||||
/**
|
||||
* Status of the testing suite.
|
||||
*
|
||||
* Used internally to determine if a test should be skipped since the tests are already failing. Assume passing.
|
||||
*
|
||||
* @var bool $pass
|
||||
*/
|
||||
protected $pass = true;
|
||||
|
||||
/**
|
||||
* Connection_Health_Test_Base constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->tests = array();
|
||||
$this->results = array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new test to the connection testing suite.
|
||||
*
|
||||
* @since 8.5.0
|
||||
*
|
||||
* @param callable $callable Test to add to queue.
|
||||
* @param string $name Unique name for the test.
|
||||
* @param string $type Optional. Core Site Health type: 'direct' or 'async'. Default 'direct'.
|
||||
* @param array $groups Optional. Testing groups to add test to. Default array( 'default' ).
|
||||
*
|
||||
* @return true|WP_Error True if successfully added. WP_Error on failure.
|
||||
*/
|
||||
public function add_test( $callable, $name, $type = 'direct', $groups = array( 'default' ) ) {
|
||||
if ( is_array( $name ) ) {
|
||||
// Pre-7.3.0 method passed the $groups parameter here.
|
||||
return new WP_Error( 'invalid_arguments', __( 'add_test arguments changed in 7.3.0. Please reference inline documentation.', 'jetpack-connection' ) );
|
||||
}
|
||||
if ( array_key_exists( $name, $this->tests ) ) {
|
||||
return new WP_Error( 'duplicate_test', __( 'Test names must be unique.', 'jetpack-connection' ) );
|
||||
}
|
||||
if ( ! is_callable( $callable ) ) {
|
||||
return new WP_Error( 'invalid_callable', __( 'Tests must be valid PHP callables.', 'jetpack-connection' ) );
|
||||
}
|
||||
|
||||
$this->tests[ $name ] = array(
|
||||
'name' => $name,
|
||||
'test' => $callable,
|
||||
'group' => $groups,
|
||||
'type' => $type,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists all tests to run.
|
||||
*
|
||||
* @since 8.5.0
|
||||
*
|
||||
* @param string $type Optional. Core Site Health type: 'direct' or 'async'. 'all' by default.
|
||||
* @param string $group Optional. A specific testing group. 'all' by default.
|
||||
*
|
||||
* @return array Array of tests with test information.
|
||||
*/
|
||||
public function list_tests( $type = 'all', $group = 'all' ) {
|
||||
if ( ! ( 'all' === $type || 'direct' === $type || 'async' === $type ) ) {
|
||||
_doing_it_wrong( __METHOD__, 'Type must be all, direct, or async.', '' );
|
||||
}
|
||||
|
||||
$tests = array();
|
||||
foreach ( $this->tests as $name => $value ) {
|
||||
$value_groups = isset( $value['group'] ) ? (array) $value['group'] : array();
|
||||
if ( ( 'all' === $group || in_array( $group, $value_groups, true ) )
|
||||
&& ( 'all' === $type || $type === $value['type'] )
|
||||
) {
|
||||
$tests[ $name ] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $tests;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a specific test.
|
||||
*
|
||||
* @since 8.5.0
|
||||
*
|
||||
* @param string $name Name of test.
|
||||
*
|
||||
* @return array|WP_Error Test result array or WP_Error if invalid name.
|
||||
*/
|
||||
public function run_test( $name ) {
|
||||
if ( array_key_exists( $name, $this->tests ) ) {
|
||||
return call_user_func( $this->tests[ $name ]['test'] );
|
||||
}
|
||||
return new WP_Error( 'unknown_test', __( 'There is no test by that name: ', 'jetpack-connection' ) . $name );
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the connection testing suite.
|
||||
*
|
||||
* @since 8.5.0
|
||||
*/
|
||||
public function run_tests() {
|
||||
foreach ( $this->tests as $test ) {
|
||||
$result = call_user_func( $test['test'] );
|
||||
$result['group'] = $test['group'];
|
||||
$result['type'] = $test['type'];
|
||||
$this->results[] = $result;
|
||||
if ( false === $result['pass'] ) {
|
||||
$this->pass = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the full results array.
|
||||
*
|
||||
* @since 8.5.0
|
||||
*
|
||||
* @param string $type Test type, async or direct. Default 'all'.
|
||||
* @param string $group Testing group whose results we want. Default 'all'.
|
||||
*
|
||||
* @return array Array of test results.
|
||||
*/
|
||||
public function raw_results( $type = 'all', $group = 'all' ) {
|
||||
if ( ! $this->results ) {
|
||||
$this->run_tests();
|
||||
}
|
||||
|
||||
$results = $this->results;
|
||||
|
||||
if ( 'all' !== $group ) {
|
||||
foreach ( $results as $test => $result ) {
|
||||
if ( ! in_array( $group, $result['group'], true ) ) {
|
||||
unset( $results[ $test ] );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( 'all' !== $type ) {
|
||||
foreach ( $results as $test => $result ) {
|
||||
if ( $type !== $result['type'] ) {
|
||||
unset( $results[ $test ] );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the status of the connection suite.
|
||||
*
|
||||
* @since 8.5.0
|
||||
*
|
||||
* @param string $type Test type, async or direct. Default 'all'.
|
||||
* @param string $group Testing group to check status of. Default 'all'.
|
||||
*
|
||||
* @return true|false True if all tests pass. False if any test fails.
|
||||
*/
|
||||
public function pass( $type = 'all', $group = 'all' ) {
|
||||
$results = $this->raw_results( $type, $group );
|
||||
|
||||
foreach ( $results as $result ) {
|
||||
// 'pass' could be true, false, or 'skipped'. We only want false.
|
||||
if ( isset( $result['pass'] ) && false === $result['pass'] ) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return array of failed test messages.
|
||||
*
|
||||
* @since 8.5.0
|
||||
*
|
||||
* @param string $type Test type, direct or async. Default 'all'.
|
||||
* @param string $group Testing group whose failures we want. Default 'all'.
|
||||
*
|
||||
* @return array Array of failed tests (may be empty).
|
||||
*/
|
||||
public function list_fails( $type = 'all', $group = 'all' ) {
|
||||
$results = $this->raw_results( $type, $group );
|
||||
|
||||
foreach ( $results as $test => $result ) {
|
||||
if ( ! isset( $result['pass'] ) || false !== $result['pass'] || ! isset( $result['short_description'] ) ) {
|
||||
unset( $results[ $test ] );
|
||||
}
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to return consistent responses for a passing test.
|
||||
*
|
||||
* @since 8.5.0
|
||||
*
|
||||
* @param array $args Arguments to override defaults.
|
||||
*
|
||||
* @return array Test results.
|
||||
*/
|
||||
public static function passing_test( $args ) {
|
||||
$defaults = self::test_result_defaults();
|
||||
$defaults['short_description'] = __( 'Test passed!', 'jetpack-connection' );
|
||||
|
||||
$args = wp_parse_args( $args, $defaults );
|
||||
|
||||
$args['pass'] = true;
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to return consistent responses for a skipped test.
|
||||
*
|
||||
* @since 8.5.0
|
||||
*
|
||||
* @param array $args Arguments to override defaults.
|
||||
*
|
||||
* @return array Test results.
|
||||
*/
|
||||
public static function skipped_test( $args = array() ) {
|
||||
$args = wp_parse_args(
|
||||
$args,
|
||||
self::test_result_defaults()
|
||||
);
|
||||
|
||||
$args['pass'] = 'skipped';
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to return consistent responses for an informational test.
|
||||
*
|
||||
* @since 8.5.0
|
||||
*
|
||||
* @param array $args Arguments to override defaults.
|
||||
*
|
||||
* @return array Test results.
|
||||
*/
|
||||
public static function informational_test( $args = array() ) {
|
||||
$args = wp_parse_args(
|
||||
$args,
|
||||
self::test_result_defaults()
|
||||
);
|
||||
|
||||
$args['pass'] = 'informational';
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to return consistent responses for a failing test.
|
||||
*
|
||||
* @since 8.5.0
|
||||
*
|
||||
* @param array $args Arguments to override defaults.
|
||||
*
|
||||
* @return array Test results.
|
||||
*/
|
||||
public static function failing_test( $args ) {
|
||||
$defaults = self::test_result_defaults();
|
||||
$defaults['short_description'] = __( 'Test failed!', 'jetpack-connection' );
|
||||
$defaults['severity'] = 'critical';
|
||||
|
||||
$args = wp_parse_args( $args, $defaults );
|
||||
|
||||
$args['pass'] = false;
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides defaults for test arguments.
|
||||
*
|
||||
* @since 8.5.0
|
||||
*
|
||||
* @return array Result defaults.
|
||||
*/
|
||||
private static function test_result_defaults() {
|
||||
return array(
|
||||
'name' => 'unnamed_test',
|
||||
'label' => false,
|
||||
'short_description' => false,
|
||||
'long_description' => false,
|
||||
'severity' => false,
|
||||
'action' => false,
|
||||
'action_label' => false,
|
||||
'show_in_site_health' => true,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to check if the site is connected and not in offline mode.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function helper_is_connected() {
|
||||
return ( new Manager() )->is_connected() && ! ( new Status() )->is_offline_mode();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to look up the connection owner and return the local WP_User.
|
||||
*
|
||||
* @return \WP_User The connection owner user.
|
||||
*/
|
||||
protected function helper_retrieve_connection_owner() {
|
||||
$owner_id = ( new Manager() )->get_connection_owner_id();
|
||||
return new \WP_User( $owner_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the blog token if it exists.
|
||||
*
|
||||
* @return object|false
|
||||
*/
|
||||
protected function helper_get_blog_token() {
|
||||
return ( new Tokens() )->get_access_token();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the URL to reconnect.
|
||||
*
|
||||
* @return string The reconnect URL.
|
||||
*/
|
||||
protected static function helper_get_reconnect_url() {
|
||||
/**
|
||||
* Filters the URL used to reconnect the Jetpack connection.
|
||||
*
|
||||
* @since 8.5.0
|
||||
*
|
||||
* @param string $url The reconnect URL.
|
||||
*/
|
||||
return apply_filters( 'jetpack_connection_reconnect_url', '' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a support URL.
|
||||
*
|
||||
* @return string The support URL.
|
||||
*/
|
||||
protected function helper_get_support_url() {
|
||||
/**
|
||||
* Filters the Jetpack support URL used in connection health tests.
|
||||
*
|
||||
* @since 8.5.0
|
||||
*
|
||||
* @param string $url The support URL.
|
||||
*/
|
||||
return apply_filters(
|
||||
'jetpack_connection_support_url',
|
||||
Redirect::get_url( 'jetpack-contact-support' )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets translated support text.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function helper_get_support_text() {
|
||||
return __( 'Please contact Jetpack support.', 'jetpack-connection' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the translated text to reconnect.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected static function helper_get_reconnect_text() {
|
||||
return __( 'Reconnect now', 'jetpack-connection' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the translated text for failing tests due to timeouts.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected static function helper_get_timeout_text() {
|
||||
return __( 'The test timed out which may sometimes indicate a failure or may be a false failure. Please relaunch tests.', 'jetpack-connection' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets translated reconnect long description.
|
||||
*
|
||||
* @param string $connection_error The connection specific error.
|
||||
* @param string $recommendation The recommendation for resolving the connection error.
|
||||
*
|
||||
* @return string The translated long description.
|
||||
*/
|
||||
protected static function helper_get_reconnect_long_description( $connection_error, $recommendation ) {
|
||||
return sprintf(
|
||||
'<p>%1$s</p>' .
|
||||
'<p><span class="dashicons fail"><span class="screen-reader-text">%2$s</span></span> %3$s</p><p><strong>%4$s</strong></p>',
|
||||
__( 'A healthy Jetpack Connection allows connected plugins (such as Jetpack and WooCommerce) to provide features like Stats, Site Security, and Payments.', 'jetpack-connection' ),
|
||||
/* translators: screen reader text indicating a test failed */
|
||||
__( 'Error', 'jetpack-connection' ),
|
||||
$connection_error,
|
||||
$recommendation
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to return consistent responses for a connection failing test.
|
||||
*
|
||||
* @param string $name The test method name.
|
||||
* @param string $connection_error The connection specific error.
|
||||
* @param string $recommendation The recommendation for resolving the connection error.
|
||||
*
|
||||
* @return array Test results.
|
||||
*/
|
||||
public static function connection_failing_test( $name, $connection_error = '', $recommendation = '' ) {
|
||||
$connection_error = empty( $connection_error ) ? __( 'Your site is not connected to WordPress.com.', 'jetpack-connection' ) : $connection_error;
|
||||
$recommendation = empty( $recommendation ) ? __( 'We recommend reconnecting your site to WordPress.com.', 'jetpack-connection' ) : $recommendation;
|
||||
|
||||
$reconnect_url = self::helper_get_reconnect_url();
|
||||
|
||||
$args = array(
|
||||
'name' => $name,
|
||||
'short_description' => $connection_error,
|
||||
'long_description' => self::helper_get_reconnect_long_description( $connection_error, $recommendation ),
|
||||
);
|
||||
|
||||
if ( ! empty( $reconnect_url ) ) {
|
||||
$args['action'] = $reconnect_url;
|
||||
$args['action_label'] = self::helper_get_reconnect_text();
|
||||
}
|
||||
|
||||
return self::failing_test( $args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets translated text to enable outbound requests.
|
||||
*
|
||||
* @param string $protocol Either 'HTTP' or 'HTTPS'.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function helper_enable_outbound_requests( $protocol ) {
|
||||
return sprintf(
|
||||
/* translators: %1$s - request protocol, either http or https */
|
||||
__(
|
||||
'Your server did not successfully connect to WordPress.com using %1$s.
|
||||
Please ask your hosting provider to confirm your server can make outbound requests to WordPress.com.',
|
||||
'jetpack-connection'
|
||||
),
|
||||
$protocol
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns 30 for use with a filter to increase HTTP request timeout.
|
||||
*
|
||||
* @return int 30
|
||||
*/
|
||||
public static function increase_timeout() {
|
||||
return 30;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a human-readable explanation of why the site is in offline mode.
|
||||
*
|
||||
* @since 8.5.0
|
||||
*
|
||||
* @return string The offline mode trigger explanation.
|
||||
*/
|
||||
public static function offline_mode_trigger_text() {
|
||||
$status = new Status();
|
||||
|
||||
if ( ! $status->is_offline_mode() ) {
|
||||
return __( 'The site is not in Offline Mode.', 'jetpack-connection' );
|
||||
}
|
||||
|
||||
if ( defined( 'JETPACK_DEV_DEBUG' ) && JETPACK_DEV_DEBUG ) {
|
||||
$notice = __( 'The JETPACK_DEV_DEBUG constant is defined in wp-config.php or elsewhere.', 'jetpack-connection' );
|
||||
} elseif ( defined( 'WP_LOCAL_DEV' ) && WP_LOCAL_DEV ) {
|
||||
$notice = __( 'The WP_LOCAL_DEV constant is defined in wp-config.php or elsewhere.', 'jetpack-connection' );
|
||||
} elseif ( $status->is_local_site() ) {
|
||||
$notice = __( 'The site URL is a known local development environment URL (e.g. http://localhost).', 'jetpack-connection' );
|
||||
} elseif ( get_option( 'jetpack_offline_mode' ) ) {
|
||||
$notice = __( 'The jetpack_offline_mode option is set to true.', 'jetpack-connection' );
|
||||
} else {
|
||||
$notice = __( 'The jetpack_offline_mode filter is set to true.', 'jetpack-connection' );
|
||||
}
|
||||
|
||||
return $notice;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide WP_CLI friendly testing results.
|
||||
*
|
||||
* @since 8.5.0
|
||||
*
|
||||
* @param string $group Testing group whose results we are outputting. Default 'all'.
|
||||
*/
|
||||
public function output_results_for_cli( $group = 'all' ) {
|
||||
if ( ! ( defined( 'WP_CLI' ) && WP_CLI ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ( new Status() )->is_offline_mode() ) {
|
||||
\WP_CLI::line( __( 'Your site is in Offline Mode:', 'jetpack-connection' ) );
|
||||
\WP_CLI::line( self::offline_mode_trigger_text() );
|
||||
}
|
||||
\WP_CLI::line( __( 'TEST RESULTS:', 'jetpack-connection' ) );
|
||||
foreach ( $this->raw_results( 'all', $group ) as $test ) {
|
||||
if ( true === $test['pass'] ) {
|
||||
\WP_CLI::log( \WP_CLI::colorize( '%gPassed:%n ' . $test['name'] ) );
|
||||
} elseif ( 'skipped' === $test['pass'] ) {
|
||||
\WP_CLI::log( \WP_CLI::colorize( '%ySkipped:%n ' . $test['name'] ) );
|
||||
if ( $test['short_description'] ) {
|
||||
\WP_CLI::log( ' ' . $test['short_description'] );
|
||||
}
|
||||
} elseif ( 'informational' === $test['pass'] ) {
|
||||
\WP_CLI::log( \WP_CLI::colorize( '%yInfo:%n ' . $test['name'] ) );
|
||||
if ( $test['short_description'] ) {
|
||||
\WP_CLI::log( ' ' . $test['short_description'] );
|
||||
}
|
||||
} else {
|
||||
\WP_CLI::log( \WP_CLI::colorize( '%rFailed:%n ' . $test['name'] ) );
|
||||
\WP_CLI::log( ' ' . $test['short_description'] );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Output results of failures in format expected by Core's Site Health tool for async tests.
|
||||
*
|
||||
* @since 8.5.0
|
||||
*
|
||||
* @return array Array of test results.
|
||||
*/
|
||||
public function output_results_for_core_async_site_health() {
|
||||
$badge_label = $this->get_site_health_badge_label();
|
||||
|
||||
$result = array(
|
||||
'label' => __( 'Jetpack Connection passed all async tests.', 'jetpack-connection' ),
|
||||
'status' => 'good',
|
||||
'badge' => array(
|
||||
'label' => $badge_label,
|
||||
'color' => 'green',
|
||||
),
|
||||
'description' => sprintf(
|
||||
'<p>%s</p>',
|
||||
__( "The Jetpack Connection's async local testing suite passed all tests!", 'jetpack-connection' )
|
||||
),
|
||||
'actions' => '',
|
||||
'test' => 'jetpack_connection_local_testing_suite',
|
||||
);
|
||||
|
||||
if ( $this->pass() ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$fails = $this->list_fails( 'async' );
|
||||
$error = false;
|
||||
foreach ( $fails as $fail ) {
|
||||
if ( ! $error ) {
|
||||
$error = true;
|
||||
$result['label'] = $fail['short_description'];
|
||||
$result['status'] = $fail['severity'];
|
||||
$result['description'] = sprintf(
|
||||
'<p>%s</p>',
|
||||
$fail['long_description'] ? $fail['long_description'] : $fail['short_description']
|
||||
);
|
||||
if ( ! empty( $fail['action'] ) ) {
|
||||
$result['actions'] = sprintf(
|
||||
'<a class="button button-primary" href="%1$s" target="_blank" rel="noopener noreferrer">%2$s <span class="screen-reader-text">%3$s</span><span aria-hidden="true" class="dashicons dashicons-external"></span></a>',
|
||||
esc_url( $fail['action'] ),
|
||||
$fail['action_label'] ? $fail['action_label'] : __( 'Resolve', 'jetpack-connection' ),
|
||||
/* translators: accessibility text */
|
||||
__( '(opens in a new tab)', 'jetpack-connection' )
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$result['description'] .= sprintf(
|
||||
'<p>%s</p>',
|
||||
__( 'There was another problem:', 'jetpack-connection' )
|
||||
) . ' ' . $fail['short_description'];
|
||||
if ( 'critical' === $fail['severity'] ) {
|
||||
$result['status'] = 'critical';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide single WP_Error instance of all failures.
|
||||
*
|
||||
* @since 8.5.0
|
||||
*
|
||||
* @param string $type Test type, direct or async. Default 'all'.
|
||||
* @param string $group Testing group whose failures we want converted. Default 'all'.
|
||||
*
|
||||
* @return WP_Error|false WP_Error with all failed tests or false if no failures.
|
||||
*/
|
||||
public function output_fails_as_wp_error( $type = 'all', $group = 'all' ) {
|
||||
if ( $this->pass( $type, $group ) ) {
|
||||
return false;
|
||||
}
|
||||
$fails = $this->list_fails( $type, $group );
|
||||
$error = false;
|
||||
|
||||
foreach ( $fails as $result ) {
|
||||
$code = 'failed_' . $result['name'];
|
||||
$message = $result['short_description'];
|
||||
$data = array(
|
||||
'resolution' => $result['action'] ?
|
||||
$result['action_label'] . ' :' . $result['action'] :
|
||||
'',
|
||||
);
|
||||
if ( ! $error ) {
|
||||
$error = new WP_Error( $code, $message, $data );
|
||||
} else {
|
||||
$error->add( $code, $message, $data );
|
||||
}
|
||||
}
|
||||
|
||||
return $error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt data for sending to WordPress.com.
|
||||
*
|
||||
* @since 8.5.0
|
||||
*
|
||||
* @param string $data Data to encrypt with the WP.com Public Key.
|
||||
*
|
||||
* @return false|array False if functionality not available. Array of encrypted data, encryption key.
|
||||
*/
|
||||
public function encrypt_string_for_wpcom( $data ) {
|
||||
$return = false;
|
||||
if ( ! function_exists( 'openssl_get_publickey' ) || ! function_exists( 'openssl_seal' ) ) {
|
||||
return $return;
|
||||
}
|
||||
|
||||
$public_key = openssl_get_publickey( REST_Connector::JETPACK__DEBUGGER_PUBLIC_KEY );
|
||||
|
||||
// Select the first allowed cipher method.
|
||||
$allowed_methods = array( 'aes-256-ctr', 'aes-256-cbc' );
|
||||
$methods = array_intersect( $allowed_methods, openssl_get_cipher_methods() );
|
||||
$method = array_shift( $methods );
|
||||
|
||||
$iv = '';
|
||||
// @phan-suppress-next-line PhanTypeMismatchArgumentInternal -- $env_key is populated by reference by openssl_seal().
|
||||
if ( $public_key && $method && openssl_seal( $data, $encrypted_data, $env_key, array( $public_key ), $method, $iv ) ) {
|
||||
// We are returning base64-encoded values to ensure they're characters we can use in JSON responses without issue.
|
||||
$return = array(
|
||||
'data' => base64_encode( $encrypted_data ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
|
||||
'key' => base64_encode( $env_key[0] ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- @phan-suppress-current-line PhanTypeArraySuspiciousNullable
|
||||
'iv' => base64_encode( $iv ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
|
||||
'cipher' => strtoupper( $method ),
|
||||
);
|
||||
}
|
||||
|
||||
// openssl_free_key was deprecated as no longer needed in PHP 8.0+. Can remove when PHP 8.0 is our minimum. (lol).
|
||||
if ( PHP_VERSION_ID < 80000 ) {
|
||||
// @phan-suppress-next-line PhanDeprecatedFunctionInternal
|
||||
openssl_free_key( $public_key ); // phpcs:ignore PHPCompatibility.FunctionUse.RemovedFunctions.openssl_free_keyDeprecated, Generic.PHP.DeprecatedFunctions.Deprecated
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the badge label for Site Health integration.
|
||||
*
|
||||
* @since 8.5.0
|
||||
*
|
||||
* @return string The badge label.
|
||||
*/
|
||||
public function get_site_health_badge_label() {
|
||||
/**
|
||||
* Filters the badge label shown in WordPress Site Health for connection tests.
|
||||
*
|
||||
* @since 8.5.0
|
||||
*
|
||||
* @param string $label The badge label. Default 'Jetpack Connection'.
|
||||
*/
|
||||
return apply_filters( 'jetpack_connection_site_health_badge_label', __( 'Jetpack Connection', 'jetpack-connection' ) );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,684 @@
|
||||
<?php
|
||||
/**
|
||||
* Collection of health tests for the Jetpack Connection.
|
||||
*
|
||||
* @package automattic/jetpack-connection
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Connection;
|
||||
|
||||
use Automattic\Jetpack\Constants;
|
||||
use Automattic\Jetpack\Identity_Crisis;
|
||||
use Automattic\Jetpack\Redirect;
|
||||
use Automattic\Jetpack\Status;
|
||||
use Jetpack_Options;
|
||||
|
||||
/**
|
||||
* Class Connection_Health_Tests contains all connection-specific health tests.
|
||||
*
|
||||
* @since 8.5.0
|
||||
*/
|
||||
class Connection_Health_Tests extends Connection_Health_Test_Base {
|
||||
|
||||
/**
|
||||
* Connection_Health_Tests constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
parent::__construct();
|
||||
|
||||
$methods = get_class_methods( static::class );
|
||||
|
||||
foreach ( $methods as $method ) {
|
||||
if ( ! str_contains( $method, 'test__' ) ) {
|
||||
continue;
|
||||
}
|
||||
$this->add_test( array( $this, $method ), $method, 'direct' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Fires after loading default connection health tests.
|
||||
*
|
||||
* Allows other packages or plugins to register additional tests.
|
||||
*
|
||||
* @since 7.1.0
|
||||
* @since 8.3.0 Passes the test suite instance.
|
||||
* @since 8.5.0 Moved from Jetpack_Cxn_Tests to Connection_Health_Tests.
|
||||
*
|
||||
* @param Connection_Health_Tests $this The Connection_Health_Tests instance.
|
||||
*/
|
||||
do_action( 'jetpack_connection_tests_loaded', $this );
|
||||
|
||||
/**
|
||||
* Determines if the WP.com testing suite should be included.
|
||||
*
|
||||
* @since 7.1.0
|
||||
* @since 8.1.0 Default false.
|
||||
*
|
||||
* @param bool $run_test To run the WP.com testing suite. Default false.
|
||||
*/
|
||||
if ( apply_filters( 'jetpack_debugger_run_self_test', false ) ) {
|
||||
$this->add_test( array( $this, 'last__wpcom_self_test' ), 'test__wpcom_self_test', 'direct' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The test verifies the blog token exists.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function test__blog_token_if_exists() {
|
||||
$name = 'test__blog_token_if_exists';
|
||||
|
||||
if ( ! $this->helper_is_connected() ) {
|
||||
return self::skipped_test(
|
||||
array(
|
||||
'name' => $name,
|
||||
'short_description' => __( 'Your site is not connected to WordPress.com. No site token to check.', 'jetpack-connection' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
$blog_token = $this->helper_get_blog_token();
|
||||
|
||||
if ( $blog_token ) {
|
||||
return self::passing_test( array( 'name' => $name ) );
|
||||
}
|
||||
|
||||
return self::connection_failing_test( $name, __( 'The site token used to authenticate with WordPress.com is missing.', 'jetpack-connection' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if Jetpack is connected.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function test__check_if_connected() {
|
||||
$name = 'test__check_if_connected';
|
||||
|
||||
if ( ! $this->helper_get_blog_token() ) {
|
||||
return self::skipped_test(
|
||||
array(
|
||||
'name' => $name,
|
||||
'short_description' => __( 'The site token used to authenticate with WordPress.com is missing.', 'jetpack-connection' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if ( $this->helper_is_connected() ) {
|
||||
return self::passing_test(
|
||||
array(
|
||||
'name' => $name,
|
||||
'label' => __( 'Your site is connected to WordPress.com', 'jetpack-connection' ),
|
||||
'long_description' => sprintf(
|
||||
'<p>%1$s</p>' .
|
||||
'<p><span class="dashicons pass"><span class="screen-reader-text">%2$s</span></span> %3$s</p>',
|
||||
__( 'A healthy Jetpack Connection allows connected plugins (such as Jetpack and WooCommerce) to provide features like Stats, Site Security, and Payments.', 'jetpack-connection' ),
|
||||
/* translators: Screen reader text indicating a test has passed */
|
||||
__( 'Passed', 'jetpack-connection' ),
|
||||
__( 'Your site is connected to WordPress.com.', 'jetpack-connection' )
|
||||
),
|
||||
)
|
||||
);
|
||||
} elseif ( ( new Status() )->is_offline_mode() ) {
|
||||
return self::skipped_test(
|
||||
array(
|
||||
'name' => $name,
|
||||
'short_description' => __( 'Your site is in Offline Mode.', 'jetpack-connection' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return self::connection_failing_test( $name, __( 'Your site is not connected to WordPress.com', 'jetpack-connection' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that the connection owner still exists on this site.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function test__master_user_exists_on_site() {
|
||||
$name = 'test__master_user_exists_on_site';
|
||||
|
||||
if ( ! $this->helper_is_connected() ) {
|
||||
return self::skipped_test(
|
||||
array(
|
||||
'name' => $name,
|
||||
'short_description' => __( 'Your site is not connected to WordPress.com. No connection owner to check.', 'jetpack-connection' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
if ( ! ( new Manager() )->get_connection_owner_id() ) {
|
||||
return self::skipped_test(
|
||||
array(
|
||||
'name' => $name,
|
||||
'short_description' => __( 'The site is connected to WordPress.com without a user. No connection owner to check.', 'jetpack-connection' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
$local_user = $this->helper_retrieve_connection_owner();
|
||||
|
||||
if ( $local_user->exists() ) {
|
||||
return self::passing_test( array( 'name' => $name ) );
|
||||
}
|
||||
|
||||
return self::connection_failing_test(
|
||||
$name,
|
||||
__( 'The user who set up the Jetpack Connection no longer exists on this site.', 'jetpack-connection' )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that the connection owner has the manage options capability (e.g. is an admin).
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function test__master_user_can_manage_options() {
|
||||
$name = 'test__master_user_can_manage_options';
|
||||
|
||||
if ( ! $this->helper_is_connected() ) {
|
||||
return self::skipped_test(
|
||||
array(
|
||||
'name' => $name,
|
||||
'short_description' => __( 'Your site is not connected to WordPress.com.', 'jetpack-connection' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
if ( ! ( new Manager() )->get_connection_owner_id() ) {
|
||||
return self::skipped_test(
|
||||
array(
|
||||
'name' => $name,
|
||||
'short_description' => __( 'The site is connected to WordPress.com without a user. No connection owner to check.', 'jetpack-connection' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
$owner_user = $this->helper_retrieve_connection_owner();
|
||||
|
||||
if ( user_can( $owner_user, 'manage_options' ) ) {
|
||||
return self::passing_test( array( 'name' => $name ) );
|
||||
}
|
||||
|
||||
/* translators: a WordPress username */
|
||||
$connection_error = sprintf( __( 'The user (%s) who set up the Jetpack Connection is not an administrator.', 'jetpack-connection' ), $owner_user->user_login );
|
||||
/* translators: a WordPress username */
|
||||
$recommendation = sprintf( __( 'We recommend either upgrading the user (%s) or reconnecting your site to WordPress.com.', 'jetpack-connection' ), $owner_user->user_login );
|
||||
|
||||
return self::connection_failing_test( $name, $connection_error, $recommendation );
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that the server is able to send an outbound HTTP communication.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function test__outbound_http() {
|
||||
$name = 'test__outbound_http';
|
||||
$api_base = Constants::get_constant( 'JETPACK__API_BASE' );
|
||||
if ( ! $api_base ) {
|
||||
$api_base = Utils::DEFAULT_JETPACK__API_BASE;
|
||||
}
|
||||
$request = wp_remote_get( preg_replace( '/^https:/', 'http:', $api_base ) . 'test/1/' );
|
||||
$code = wp_remote_retrieve_response_code( $request );
|
||||
|
||||
if ( 200 === (int) $code ) {
|
||||
return self::passing_test( array( 'name' => $name ) );
|
||||
}
|
||||
|
||||
return self::failing_test(
|
||||
array(
|
||||
'name' => $name,
|
||||
'short_description' => $this->helper_enable_outbound_requests( 'HTTP' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that the server is able to send an outbound HTTPS communication.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function test__outbound_https() {
|
||||
$name = 'test__outbound_https';
|
||||
$api_base = Constants::get_constant( 'JETPACK__API_BASE' );
|
||||
if ( ! $api_base ) {
|
||||
$api_base = Utils::DEFAULT_JETPACK__API_BASE;
|
||||
}
|
||||
$request = wp_remote_get( preg_replace( '/^http:/', 'https:', $api_base ) . 'test/1/' );
|
||||
$code = wp_remote_retrieve_response_code( $request );
|
||||
|
||||
if ( 200 === (int) $code ) {
|
||||
return self::passing_test( array( 'name' => $name ) );
|
||||
}
|
||||
|
||||
return self::failing_test(
|
||||
array(
|
||||
'name' => $name,
|
||||
'short_description' => $this->helper_enable_outbound_requests( 'HTTPS' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for an Identity Crisis.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function test__identity_crisis() {
|
||||
$name = 'test__identity_crisis';
|
||||
|
||||
if ( ! $this->helper_is_connected() ) {
|
||||
return self::skipped_test(
|
||||
array(
|
||||
'name' => $name,
|
||||
'short_description' => __( 'Your site is not connected to WordPress.com.', 'jetpack-connection' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$identity_crisis = $this->check_identity_crisis();
|
||||
|
||||
if ( ! $identity_crisis ) {
|
||||
return self::passing_test( array( 'name' => $name ) );
|
||||
}
|
||||
|
||||
$messages = array();
|
||||
|
||||
if ( isset( $identity_crisis['home'] ) && isset( $identity_crisis['wpcom_home'] ) && $identity_crisis['home'] !== $identity_crisis['wpcom_home'] ) {
|
||||
$messages[] = sprintf(
|
||||
/* translators: Two URLs. The first is the locally-recorded value, the second is the value as recorded on WP.com. */
|
||||
__( 'Your home URL is set as `%1$s`, but your Jetpack Connection lists it as `%2$s`.', 'jetpack-connection' ),
|
||||
$identity_crisis['home'],
|
||||
$identity_crisis['wpcom_home']
|
||||
);
|
||||
}
|
||||
|
||||
if ( isset( $identity_crisis['siteurl'] ) && isset( $identity_crisis['wpcom_siteurl'] ) && $identity_crisis['siteurl'] !== $identity_crisis['wpcom_siteurl'] ) {
|
||||
$messages[] = sprintf(
|
||||
/* translators: Two URLs. The first is the locally-recorded value, the second is the value as recorded on WP.com. */
|
||||
__( 'Your site URL is set as `%1$s`, but your Jetpack Connection lists it as `%2$s`.', 'jetpack-connection' ),
|
||||
$identity_crisis['siteurl'],
|
||||
$identity_crisis['wpcom_siteurl']
|
||||
);
|
||||
}
|
||||
|
||||
if ( empty( $messages ) ) {
|
||||
$messages[] = __( 'A URL mismatch was detected between your site and WordPress.com.', 'jetpack-connection' );
|
||||
}
|
||||
|
||||
return self::failing_test(
|
||||
array(
|
||||
'name' => $name,
|
||||
'short_description' => implode( ' ', $messages ),
|
||||
'action_label' => $this->helper_get_support_text(),
|
||||
'action' => $this->helper_get_support_url(),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for Identity Crisis using connection package classes.
|
||||
*
|
||||
* @return array|false False if no IDC, array with crisis details otherwise.
|
||||
*/
|
||||
protected function check_identity_crisis() {
|
||||
if ( ! ( new Manager() )->is_connected() || ( new Status() )->is_offline_mode() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ! class_exists( 'Automattic\Jetpack\Identity_Crisis' ) || ! Identity_Crisis::validate_sync_error_idc_option() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Jetpack_Options::get_option( 'sync_error_idc' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the health of the connection tokens.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function test__connection_token_health() {
|
||||
$name = 'test__connection_token_health';
|
||||
$m = new Manager();
|
||||
$user_id = get_current_user_id();
|
||||
|
||||
// Check if there's a connected logged in user.
|
||||
if ( $user_id && ! $m->is_user_connected( $user_id ) ) {
|
||||
$user_id = false;
|
||||
}
|
||||
|
||||
// If no logged in user to check, let's see if there's a connection owner set.
|
||||
if ( ! $user_id ) {
|
||||
$user_id = Jetpack_Options::get_option( 'master_user' );
|
||||
if ( $user_id && ! $m->is_user_connected( $user_id ) ) {
|
||||
return self::connection_failing_test( $name, __( 'Missing token for the connection owner.', 'jetpack-connection' ) );
|
||||
}
|
||||
}
|
||||
|
||||
if ( $user_id ) {
|
||||
return $this->check_tokens_health( $user_id );
|
||||
}
|
||||
|
||||
return $this->check_blog_token_health();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests blog token against WP.com's check-token-health endpoint.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function check_blog_token_health() {
|
||||
$name = 'test__connection_token_health';
|
||||
$valid = ( new Tokens() )->validate_blog_token();
|
||||
|
||||
if ( ! $valid ) {
|
||||
return self::connection_failing_test( $name, __( 'The site token used to authenticate with WordPress.com could not be validated.', 'jetpack-connection' ) );
|
||||
}
|
||||
|
||||
return self::passing_test( array( 'name' => $name ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests blog and user tokens against WP.com's check-token-health endpoint.
|
||||
*
|
||||
* @param int $user_id The user ID to check the tokens for.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function check_tokens_health( $user_id ) {
|
||||
$name = 'test__connection_token_health';
|
||||
$validated_tokens = ( new Tokens() )->validate( $user_id );
|
||||
|
||||
if ( ! is_array( $validated_tokens ) || count( array_diff_key( array_flip( array( 'blog_token', 'user_token' ) ), $validated_tokens ) ) ) {
|
||||
return self::skipped_test(
|
||||
array(
|
||||
'name' => $name,
|
||||
'short_description' => __( 'Token health check failed to validate tokens.', 'jetpack-connection' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$invalid_tokens_exist = false;
|
||||
foreach ( $validated_tokens as $validated_token ) {
|
||||
if ( ! $validated_token['is_healthy'] ) {
|
||||
$invalid_tokens_exist = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! $invalid_tokens_exist ) {
|
||||
return self::passing_test( array( 'name' => $name ) );
|
||||
}
|
||||
|
||||
return self::connection_failing_test( $name, __( 'Invalid Jetpack Connection tokens.', 'jetpack-connection' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests connection status against WP.com's test-connection endpoint.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function test__wpcom_connection_test() {
|
||||
$name = 'test__wpcom_connection_test';
|
||||
|
||||
$status = new Status();
|
||||
if ( ! ( new Manager() )->is_connected() || $status->is_offline_mode() || $status->in_safe_mode() || ! $this->pass ) {
|
||||
return self::skipped_test( array( 'name' => $name ) );
|
||||
}
|
||||
|
||||
add_filter( 'http_request_timeout', array( static::class, 'increase_timeout' ) );
|
||||
$response = Client::wpcom_json_api_request_as_blog(
|
||||
sprintf( '/jetpack-blogs/%d/test-connection', Jetpack_Options::get_option( 'id' ) ),
|
||||
Client::WPCOM_JSON_API_VERSION
|
||||
);
|
||||
remove_filter( 'http_request_timeout', array( static::class, 'increase_timeout' ) );
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
if ( str_contains( $response->get_error_message(), 'cURL error 28' ) ) {
|
||||
return self::skipped_test(
|
||||
array(
|
||||
'name' => $name,
|
||||
'short_description' => self::helper_get_timeout_text(),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/* translators: %1$s is the error code, %2$s is the error message */
|
||||
$message = sprintf( __( 'Connection test failed (#%1$s: %2$s)', 'jetpack-connection' ), $response->get_error_code(), $response->get_error_message() );
|
||||
return self::connection_failing_test( $name, $message );
|
||||
}
|
||||
|
||||
$body = wp_remote_retrieve_body( $response );
|
||||
if ( ! $body ) {
|
||||
return self::failing_test(
|
||||
array(
|
||||
'name' => $name,
|
||||
'short_description' => __( 'Connection test failed (empty response body)', 'jetpack-connection' ) . wp_remote_retrieve_response_code( $response ),
|
||||
'action_label' => $this->helper_get_support_text(),
|
||||
'action' => $this->helper_get_support_url(),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if ( 404 === wp_remote_retrieve_response_code( $response ) ) {
|
||||
return self::skipped_test(
|
||||
array(
|
||||
'name' => $name,
|
||||
'short_description' => __( 'The WordPress.com API returned a 404 error.', 'jetpack-connection' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return $this->evaluate_wpcom_connection_result( $name, json_decode( $body ), wp_remote_retrieve_response_code( $response ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns a decoded WP.com test-connection response into a test result.
|
||||
*
|
||||
* Split out from test__wpcom_connection_test() so the decision logic can be
|
||||
* exercised without performing a signed remote request.
|
||||
*
|
||||
* @param string $name The test name.
|
||||
* @param object $result The JSON-decoded response body.
|
||||
* @param int|string $status_code The HTTP status code of the WP.com response.
|
||||
*
|
||||
* @return array Test results.
|
||||
*/
|
||||
public function evaluate_wpcom_connection_result( $name, $result, $status_code ) {
|
||||
if ( ! empty( $result->connected ) ) {
|
||||
return self::passing_test( array( 'name' => $name ) );
|
||||
}
|
||||
|
||||
// The site itself rejected WordPress.com's request (firewall, WAF, security
|
||||
// plugin, or server rule). The connection token could be valid, but reconnecting would
|
||||
// be rejected the same way - surface the real cause and don't offer a reconnect.
|
||||
if ( isset( $result->error_code ) && 'xmlrpc_request_blocked' === $result->error_code ) {
|
||||
return $this->blocked_request_failing_test(
|
||||
$name,
|
||||
(int) ( $result->site_http_status ?? 0 )
|
||||
);
|
||||
}
|
||||
|
||||
$message = isset( $result->message ) && '' !== $result->message
|
||||
? $result->message
|
||||
: __( 'Connection test failed.', 'jetpack-connection' );
|
||||
|
||||
$message .= ' ' . sprintf(
|
||||
/* translators: %s is the HTTP status code returned by WordPress.com. */
|
||||
__( '(status code: %s)', 'jetpack-connection' ),
|
||||
$status_code
|
||||
);
|
||||
|
||||
return self::connection_failing_test( $name, $message );
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a failing result for the case where the site is blocking WordPress.com's
|
||||
* connection test (e.g. firewall/WAF/security plugin).
|
||||
*
|
||||
* No reconnect action is offered because the connection token could be valid but
|
||||
* reconnecting would be rejected the same way.
|
||||
*
|
||||
* @param string $name The test name.
|
||||
* @param int $site_http_status The HTTP status the site returned, or 0 if unknown.
|
||||
*
|
||||
* @return array Test results.
|
||||
*/
|
||||
protected function blocked_request_failing_test( $name, $site_http_status = 0 ) {
|
||||
$connection_error = $site_http_status
|
||||
? sprintf(
|
||||
/* translators: %d is the HTTP status code (e.g. 403) the site returned. */
|
||||
__( 'WordPress.com reached your site but the request was blocked (HTTP %d). This is usually caused by a firewall, security plugin, or server rule rejecting requests from WordPress.com.', 'jetpack-connection' ),
|
||||
$site_http_status
|
||||
)
|
||||
: __( 'WordPress.com reached your site but the request was blocked. This is usually caused by a firewall, security plugin, or server rule rejecting requests from WordPress.com.', 'jetpack-connection' );
|
||||
|
||||
$recommendation = __( 'Ask your host or security provider to allow requests from WordPress.com to your site\'s xmlrpc.php file. Reconnecting will not resolve this. If you need further help, contact Jetpack support.', 'jetpack-connection' );
|
||||
|
||||
return self::failing_test(
|
||||
array(
|
||||
'name' => $name,
|
||||
'short_description' => $connection_error,
|
||||
'long_description' => self::helper_get_reconnect_long_description( $connection_error, $recommendation ),
|
||||
'action_label' => $this->helper_get_support_text(),
|
||||
'action' => $this->helper_get_support_url(),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the port number to ensure it is an expected value.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function test__server_port_value() {
|
||||
$name = 'test__server_port_value';
|
||||
|
||||
if ( ! isset( $_SERVER['HTTP_X_FORWARDED_PORT'] ) && ! isset( $_SERVER['SERVER_PORT'] ) ) {
|
||||
return self::skipped_test(
|
||||
array(
|
||||
'name' => $name,
|
||||
'short_description' => __( 'The server port values are not defined. This is most common when running PHP via a CLI.', 'jetpack-connection' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
$site_port = wp_parse_url( home_url(), PHP_URL_PORT );
|
||||
$server_port = isset( $_SERVER['HTTP_X_FORWARDED_PORT'] ) ? (int) $_SERVER['HTTP_X_FORWARDED_PORT'] : (int) $_SERVER['SERVER_PORT'];
|
||||
$http_ports = array( 80 );
|
||||
$https_ports = array( 80, 443 );
|
||||
|
||||
if ( defined( 'JETPACK_SIGNATURE__HTTP_PORT' ) ) {
|
||||
$http_ports[] = JETPACK_SIGNATURE__HTTP_PORT;
|
||||
}
|
||||
|
||||
if ( defined( 'JETPACK_SIGNATURE__HTTPS_PORT' ) ) {
|
||||
$https_ports[] = JETPACK_SIGNATURE__HTTPS_PORT;
|
||||
}
|
||||
|
||||
if ( $site_port ) {
|
||||
return self::skipped_test( array( 'name' => $name ) );
|
||||
}
|
||||
|
||||
if ( is_ssl() && in_array( $server_port, $https_ports, true ) ) {
|
||||
return self::passing_test( array( 'name' => $name ) );
|
||||
} elseif ( in_array( $server_port, $http_ports, true ) ) {
|
||||
return self::passing_test( array( 'name' => $name ) );
|
||||
}
|
||||
|
||||
if ( is_ssl() ) {
|
||||
$needed_constant = 'JETPACK_SIGNATURE__HTTPS_PORT';
|
||||
} else {
|
||||
$needed_constant = 'JETPACK_SIGNATURE__HTTP_PORT';
|
||||
}
|
||||
return self::failing_test(
|
||||
array(
|
||||
'name' => $name,
|
||||
'short_description' => sprintf(
|
||||
/* translators: %1$s - a PHP code snippet */
|
||||
__(
|
||||
'The server port value is unexpected.
|
||||
Try adding the following to your wp-config.php file: %1$s',
|
||||
'jetpack-connection'
|
||||
),
|
||||
"define( '$needed_constant', $server_port )"
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that PHP's XML library is installed.
|
||||
*
|
||||
* @return array Test results.
|
||||
*/
|
||||
protected function test__xml_parser_available() {
|
||||
$name = 'test__xml_parser_available';
|
||||
if ( function_exists( 'xml_parser_create' ) ) {
|
||||
return self::passing_test( array( 'name' => $name ) );
|
||||
}
|
||||
|
||||
return self::failing_test(
|
||||
array(
|
||||
'name' => $name,
|
||||
'label' => __( 'PHP XML manipulation libraries are not available.', 'jetpack-connection' ),
|
||||
'short_description' => __( 'Please ask your hosting provider to refer to our server requirements and enable PHP\'s XML module.', 'jetpack-connection' ),
|
||||
'action_label' => __( 'View our server requirements', 'jetpack-connection' ),
|
||||
'action' => Redirect::get_url( 'jetpack-support-server-requirements' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls to WP.com to run the connection diagnostic testing suite.
|
||||
*
|
||||
* Intentionally added last as it will be skipped if any local failed conditions exist.
|
||||
*
|
||||
* @since 7.1.0
|
||||
*
|
||||
* @return array Test results.
|
||||
*/
|
||||
protected function last__wpcom_self_test() {
|
||||
$name = 'test__wpcom_self_test';
|
||||
|
||||
$status = new Status();
|
||||
if ( ! ( new Manager() )->is_connected() || $status->is_offline_mode() || $status->in_safe_mode() || ! $this->pass ) {
|
||||
return self::skipped_test( array( 'name' => $name ) );
|
||||
}
|
||||
|
||||
$self_xml_rpc_url = site_url( 'xmlrpc.php' );
|
||||
|
||||
$api_base = Constants::get_constant( 'JETPACK__API_BASE' );
|
||||
if ( ! $api_base ) {
|
||||
$api_base = Utils::DEFAULT_JETPACK__API_BASE;
|
||||
}
|
||||
$testsite_url = $api_base . 'testsite/1/?url=';
|
||||
|
||||
add_filter( 'http_request_timeout', array( static::class, 'increase_timeout' ), PHP_INT_MAX - 1 );
|
||||
|
||||
$response = wp_remote_get( $testsite_url . $self_xml_rpc_url );
|
||||
|
||||
remove_filter( 'http_request_timeout', array( static::class, 'increase_timeout' ), PHP_INT_MAX - 1 );
|
||||
|
||||
if ( 200 === wp_remote_retrieve_response_code( $response ) ) {
|
||||
return self::passing_test( array( 'name' => $name ) );
|
||||
} elseif ( is_wp_error( $response ) && str_contains( $response->get_error_message(), 'cURL error 28' ) ) {
|
||||
return self::skipped_test(
|
||||
array(
|
||||
'name' => $name,
|
||||
'short_description' => self::helper_get_timeout_text(),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return self::failing_test(
|
||||
array(
|
||||
'name' => $name,
|
||||
'short_description' => sprintf(
|
||||
/* translators: %1$s - A debugging url */
|
||||
__( 'Jetpack.com detected an error on the WP.com Self Test. Visit the Jetpack Debug page for more info: %1$s, or contact support.', 'jetpack-connection' ),
|
||||
Redirect::get_url( 'jetpack-support-debug', array( 'query' => 'url=' . rawurlencode( site_url() ) ) )
|
||||
),
|
||||
'action_label' => $this->helper_get_support_text(),
|
||||
'action' => $this->helper_get_support_url(),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
/**
|
||||
* Exception class for the Identity Crisis component.
|
||||
*
|
||||
* @package automattic/jetpack-connection
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\IdentityCrisis;
|
||||
|
||||
/**
|
||||
* Exception class for the Identity Crisis component.
|
||||
*/
|
||||
class Exception extends \Exception {}
|
||||
@@ -0,0 +1,376 @@
|
||||
<?php
|
||||
/**
|
||||
* Identity_Crisis REST endpoints of the Connection package.
|
||||
*
|
||||
* @package automattic/jetpack-connection
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\IdentityCrisis;
|
||||
|
||||
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
|
||||
use Automattic\Jetpack\Connection\Rest_Authentication;
|
||||
use Jetpack_Options;
|
||||
use Jetpack_XMLRPC_Server;
|
||||
use WP_Error;
|
||||
use WP_REST_Server;
|
||||
|
||||
/**
|
||||
* This class will handle Identity Crisis Endpoints
|
||||
*
|
||||
* @since automattic/jetpack-identity-crisis:0.2.0
|
||||
* @since 2.9.0
|
||||
*/
|
||||
class REST_Endpoints {
|
||||
|
||||
/**
|
||||
* Initialize REST routes.
|
||||
*/
|
||||
public static function initialize_rest_api() {
|
||||
|
||||
// Confirm that a site in identity crisis should be in staging mode.
|
||||
register_rest_route(
|
||||
'jetpack/v4',
|
||||
'/identity-crisis/confirm-safe-mode',
|
||||
array(
|
||||
'methods' => WP_REST_Server::EDITABLE,
|
||||
'callback' => __CLASS__ . '::confirm_safe_mode',
|
||||
'permission_callback' => __CLASS__ . '::identity_crisis_mitigation_permission_check',
|
||||
)
|
||||
);
|
||||
|
||||
// Handles the request to migrate stats and subscribers during an identity crisis.
|
||||
register_rest_route(
|
||||
'jetpack/v4',
|
||||
'identity-crisis/migrate',
|
||||
array(
|
||||
'methods' => WP_REST_Server::EDITABLE,
|
||||
'callback' => __CLASS__ . '::migrate_stats_and_subscribers',
|
||||
'permission_callback' => __CLASS__ . '::identity_crisis_mitigation_permission_check',
|
||||
)
|
||||
);
|
||||
|
||||
// IDC resolve: create an entirely new shadow site for this URL.
|
||||
register_rest_route(
|
||||
'jetpack/v4',
|
||||
'/identity-crisis/start-fresh',
|
||||
array(
|
||||
'methods' => WP_REST_Server::EDITABLE,
|
||||
'callback' => __CLASS__ . '::start_fresh_connection',
|
||||
'permission_callback' => __CLASS__ . '::identity_crisis_mitigation_permission_check',
|
||||
'args' => array(
|
||||
'redirect_uri' => array(
|
||||
'description' => __( 'URI of the admin page where the user should be redirected after connection flow', 'jetpack-connection' ),
|
||||
'type' => 'string',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
// Fetch URL and secret for IDC check.
|
||||
register_rest_route(
|
||||
'jetpack/v4',
|
||||
'/identity-crisis/idc-url-validation',
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( static::class, 'validate_urls_and_set_secret' ),
|
||||
'permission_callback' => array( static::class, 'url_secret_permission_check' ),
|
||||
)
|
||||
);
|
||||
|
||||
// Fetch URL verification secret.
|
||||
register_rest_route(
|
||||
'jetpack/v4',
|
||||
'/identity-crisis/url-secret',
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( static::class, 'fetch_url_secret' ),
|
||||
'permission_callback' => array( static::class, 'url_secret_permission_check' ),
|
||||
)
|
||||
);
|
||||
|
||||
// Fetch URL verification secret.
|
||||
register_rest_route(
|
||||
'jetpack/v4',
|
||||
'/identity-crisis/compare-url-secret',
|
||||
array(
|
||||
'methods' => WP_REST_Server::EDITABLE,
|
||||
'callback' => array( static::class, 'compare_url_secret' ),
|
||||
'permission_callback' => array( static::class, 'compare_url_secret_permission_check' ),
|
||||
'args' => array(
|
||||
'secret' => array(
|
||||
'description' => __( 'URL secret to compare to the ones stored in the database.', 'jetpack-connection' ),
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles identity crisis mitigation, confirming safe mode for this site.
|
||||
*
|
||||
* @since 0.2.0
|
||||
* @since-jetpack 4.4.0
|
||||
*
|
||||
* @return bool | WP_Error True if option is properly set.
|
||||
*/
|
||||
public static function confirm_safe_mode() {
|
||||
// Read the option's true DB state, not a stale cached copy: on sites with
|
||||
// a persistent object cache, a prior un-confirm (the admin-bar "clear
|
||||
// confirmation" action deletes jetpack_safe_mode_confirmed) can leave a
|
||||
// lingering cached value that would make update_option() below see the new
|
||||
// value as unchanged and report a false failure.
|
||||
self::flush_jetpack_option_cache( 'jetpack_safe_mode_confirmed' );
|
||||
|
||||
// update_option() returns false both when the write fails and when the
|
||||
// value is already set, so treat an already-confirmed option as success.
|
||||
if (
|
||||
Jetpack_Options::get_option( 'safe_mode_confirmed' )
|
||||
|| Jetpack_Options::update_option( 'safe_mode_confirmed', true )
|
||||
) {
|
||||
return rest_ensure_response(
|
||||
array(
|
||||
'code' => 'success',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return new WP_Error(
|
||||
'error_setting_jetpack_safe_mode',
|
||||
esc_html__( 'Could not confirm safe mode.', 'jetpack-connection' ),
|
||||
array( 'status' => 500 )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Flush any cached copy of a Jetpack option so a subsequent read reflects the
|
||||
* option's true state in the database.
|
||||
*
|
||||
* Jetpack options are stored as autoloaded `jetpack_*` WordPress options, so
|
||||
* bust both the individual key and the autoloaded-options cache to keep
|
||||
* persistent object caches from returning a stale value across requests.
|
||||
*
|
||||
* @since 8.7.0
|
||||
*
|
||||
* @param string $option The prefixed option name (e.g. `jetpack_safe_mode_confirmed`).
|
||||
*/
|
||||
private static function flush_jetpack_option_cache( $option ) {
|
||||
wp_cache_delete( $option, 'options' );
|
||||
wp_cache_delete( 'alloptions', 'options' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Flush any cached copy of the sync_error_idc option so a subsequent read
|
||||
* reflects the option's true state in the database.
|
||||
*
|
||||
* @since 8.7.0
|
||||
*/
|
||||
private static function flush_sync_error_idc_cache() {
|
||||
self::flush_jetpack_option_cache( 'jetpack_sync_error_idc' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles identity crisis mitigation, migrating stats and subscribers from old url to this, new url.
|
||||
*
|
||||
* @since 0.2.0
|
||||
* @since-jetpack 4.4.0
|
||||
*
|
||||
* @return bool | WP_Error True if option is properly set.
|
||||
*/
|
||||
public static function migrate_stats_and_subscribers() {
|
||||
// Read the option's true DB state, not a stale cached copy: on sites with
|
||||
// a persistent object cache, a prior successful migrate (or an IDC
|
||||
// revalidation) may have already removed the option while a cached value
|
||||
// lingers, which would otherwise make the delete below report a false
|
||||
// failure.
|
||||
self::flush_sync_error_idc_cache();
|
||||
|
||||
if ( Jetpack_Options::get_option( 'sync_error_idc' ) ) {
|
||||
Jetpack_Options::delete_option( 'sync_error_idc' );
|
||||
|
||||
// delete_option() returns false both when the write fails and when
|
||||
// there is nothing to delete, so re-read (cache-busted) and only treat
|
||||
// a still-present option as a real failure.
|
||||
self::flush_sync_error_idc_cache();
|
||||
|
||||
if ( Jetpack_Options::get_option( 'sync_error_idc' ) ) {
|
||||
return new WP_Error(
|
||||
'error_deleting_sync_error_idc',
|
||||
esc_html__( 'Could not delete sync error option.', 'jetpack-connection' ),
|
||||
array( 'status' => 500 )
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ( Jetpack_Options::get_option( 'migrate_for_idc' ) || Jetpack_Options::update_option( 'migrate_for_idc', true ) ) {
|
||||
return rest_ensure_response(
|
||||
array(
|
||||
'code' => 'success',
|
||||
)
|
||||
);
|
||||
}
|
||||
return new WP_Error(
|
||||
'error_setting_jetpack_migrate',
|
||||
esc_html__( 'Could not confirm migration.', 'jetpack-connection' ),
|
||||
array( 'status' => 500 )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* This IDC resolution will disconnect the site and re-connect to a completely new
|
||||
* and separate shadow site than the original.
|
||||
*
|
||||
* It will first will disconnect the site without phoning home as to not disturb the production site.
|
||||
* It then builds a fresh connection URL and sends it back along with the response.
|
||||
*
|
||||
* @since 0.2.0
|
||||
* @since-jetpack 4.4.0
|
||||
*
|
||||
* @param \WP_REST_Request $request The request sent to the WP REST API.
|
||||
*
|
||||
* @return \WP_REST_Response|WP_Error
|
||||
*/
|
||||
public static function start_fresh_connection( $request ) {
|
||||
/**
|
||||
* Fires when Users have requested through Identity Crisis for the connection to be reset.
|
||||
* Should be used to disconnect any connections and reset options.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*/
|
||||
do_action( 'jetpack_idc_disconnect' );
|
||||
|
||||
$connection = new Connection_Manager();
|
||||
$result = $connection->try_registration( true );
|
||||
|
||||
// early return if site registration fails.
|
||||
if ( ! $result || is_wp_error( $result ) ) {
|
||||
return rest_ensure_response( $result );
|
||||
}
|
||||
|
||||
$redirect_uri = $request->get_param( 'redirect_uri' ) ? admin_url( $request->get_param( 'redirect_uri' ) ) : null;
|
||||
|
||||
/**
|
||||
* Filters the connection url that users should be redirected to for re-establishing their connection.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @param \WP_REST_Response|WP_Error $connection_url Connection URL user should be redirected to.
|
||||
*/
|
||||
return apply_filters( 'jetpack_idc_authorization_url', rest_ensure_response( $connection->get_authorization_url( null, $redirect_uri ) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that user can mitigate an identity crisis.
|
||||
*
|
||||
* @since 0.2.0
|
||||
* @since-jetpack 4.4.0
|
||||
*
|
||||
* @return true|WP_Error True if the user has capability 'jetpack_disconnect', an error object otherwise.
|
||||
*/
|
||||
public static function identity_crisis_mitigation_permission_check() {
|
||||
if ( current_user_can( 'jetpack_disconnect' ) ) {
|
||||
return true;
|
||||
}
|
||||
$error_msg = esc_html__(
|
||||
'You do not have the correct user permissions to perform this action.
|
||||
Please contact your site admin if you think this is a mistake.',
|
||||
'jetpack-connection'
|
||||
);
|
||||
|
||||
return new WP_Error( 'invalid_user_permission_identity_crisis', $error_msg, array( 'status' => rest_authorization_required_code() ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Endpoint for URL validation and creating a secret.
|
||||
*
|
||||
* @since 0.18.0
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function validate_urls_and_set_secret() {
|
||||
$xmlrpc_server = new Jetpack_XMLRPC_Server();
|
||||
$result = $xmlrpc_server->validate_urls_for_idc_mitigation();
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Endpoint for fetching the existing secret.
|
||||
*
|
||||
* @return WP_Error|\WP_REST_Response
|
||||
*/
|
||||
public static function fetch_url_secret() {
|
||||
$secret = new URL_Secret();
|
||||
|
||||
if ( ! $secret->exists() ) {
|
||||
return new WP_Error( 'missing_url_secret', esc_html__( 'URL secret does not exist.', 'jetpack-connection' ) );
|
||||
}
|
||||
|
||||
return rest_ensure_response(
|
||||
array(
|
||||
'code' => 'success',
|
||||
'data' => array(
|
||||
'secret' => $secret->get_secret(),
|
||||
'expires_at' => $secret->get_expires_at(),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Endpoint for comparing the existing secret.
|
||||
*
|
||||
* @param \WP_REST_Request $request The request sent to the WP REST API.
|
||||
*
|
||||
* @return WP_Error|\WP_REST_Response
|
||||
*/
|
||||
public static function compare_url_secret( $request ) {
|
||||
$match = false;
|
||||
|
||||
$storage = new URL_Secret();
|
||||
|
||||
if ( $storage->exists() ) {
|
||||
$remote_secret = $request->get_param( 'secret' );
|
||||
$match = $remote_secret && hash_equals( $storage->get_secret(), $remote_secret );
|
||||
}
|
||||
|
||||
return rest_ensure_response(
|
||||
array(
|
||||
'code' => 'success',
|
||||
'match' => $match,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify url_secret create/fetch permissions (valid blog token authentication).
|
||||
*
|
||||
* @return true|WP_Error
|
||||
*/
|
||||
public static function url_secret_permission_check() {
|
||||
return Rest_Authentication::is_signed_with_blog_token()
|
||||
? true
|
||||
: new WP_Error(
|
||||
'invalid_user_permission_identity_crisis',
|
||||
esc_html__( 'You do not have the correct user permissions to perform this action.', 'jetpack-connection' ),
|
||||
array( 'status' => rest_authorization_required_code() )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The endpoint is only available on non-connected sites.
|
||||
* use `/identity-crisis/url-secret` for connected sites.
|
||||
*
|
||||
* @return true|WP_Error
|
||||
*/
|
||||
public static function compare_url_secret_permission_check() {
|
||||
return ( new Connection_Manager() )->is_connected()
|
||||
? new WP_Error(
|
||||
'invalid_connection_status',
|
||||
esc_html__( 'The endpoint is not available on connected sites.', 'jetpack-connection' ),
|
||||
array( 'status' => 403 )
|
||||
)
|
||||
: true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
<?php
|
||||
/**
|
||||
* Identity_Crisis UI class of the Connection package.
|
||||
*
|
||||
* @package automattic/jetpack-connection
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\IdentityCrisis;
|
||||
|
||||
use Automattic\Jetpack\Assets;
|
||||
use Automattic\Jetpack\Identity_Crisis;
|
||||
use Automattic\Jetpack\Status;
|
||||
use Automattic\Jetpack\Status\Host;
|
||||
use Automattic\Jetpack\Tracking;
|
||||
use Jetpack_Options;
|
||||
use Jetpack_Tracks_Client;
|
||||
|
||||
/**
|
||||
* The Identity Crisis UI handling.
|
||||
*/
|
||||
class UI {
|
||||
|
||||
/**
|
||||
* Temporary storage for consumer data.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static $consumers;
|
||||
|
||||
/**
|
||||
* Initialization.
|
||||
*/
|
||||
public static function init() {
|
||||
if ( did_action( 'jetpack_identity_crisis_ui_init' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Action called after initializing Identity Crisis UI.
|
||||
*
|
||||
* @since 0.6.0
|
||||
*/
|
||||
do_action( 'jetpack_identity_crisis_ui_init' );
|
||||
|
||||
$idc_data = Identity_Crisis::check_identity_crisis();
|
||||
|
||||
if ( false === $idc_data ) {
|
||||
return;
|
||||
}
|
||||
|
||||
add_action( 'admin_enqueue_scripts', array( static::class, 'enqueue_scripts' ) );
|
||||
|
||||
Tracking::register_tracks_functions_scripts( true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue scripts!
|
||||
*/
|
||||
public static function enqueue_scripts() {
|
||||
if ( is_admin() ) {
|
||||
Assets::register_script(
|
||||
'jp_identity_crisis_banner',
|
||||
'../../dist/identity-crisis.js',
|
||||
__FILE__,
|
||||
array(
|
||||
'in_footer' => true,
|
||||
'textdomain' => 'jetpack-connection',
|
||||
)
|
||||
);
|
||||
Assets::enqueue_script( 'jp_identity_crisis_banner' );
|
||||
wp_add_inline_script( 'jp_identity_crisis_banner', static::get_initial_state(), 'before' );
|
||||
|
||||
add_action( 'admin_notices', array( static::class, 'render_container' ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the container element for the IDC banner.
|
||||
*/
|
||||
public static function render_container() {
|
||||
?>
|
||||
<div id="jp-identity-crisis-container" class="notice"></div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the rendered initial state JavaScript code.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private static function get_initial_state() {
|
||||
return 'var JP_IDENTITY_CRISIS__INITIAL_STATE=' . wp_json_encode( static::get_initial_state_data(), JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ) . ';';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the initial state data.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private static function get_initial_state_data() {
|
||||
$idc_urls = Identity_Crisis::get_mismatched_urls();
|
||||
$current_screen = get_current_screen();
|
||||
$is_admin = current_user_can( 'jetpack_disconnect' );
|
||||
$possible_dynamic_site_url_detected = (bool) Identity_Crisis::detect_possible_dynamic_site_url();
|
||||
$is_development_site = (bool) Status::is_development_site();
|
||||
|
||||
return array(
|
||||
'WP_API_root' => esc_url_raw( rest_url() ),
|
||||
'WP_API_nonce' => wp_create_nonce( 'wp_rest' ),
|
||||
'wpcomHomeUrl' => ( is_array( $idc_urls ) && array_key_exists( 'wpcom_url', $idc_urls ) ) ? $idc_urls['wpcom_url'] : null,
|
||||
'currentUrl' => ( is_array( $idc_urls ) && array_key_exists( 'current_url', $idc_urls ) ) ? $idc_urls['current_url'] : null,
|
||||
'redirectUri' => isset( $_SERVER['REQUEST_URI'] ) ? str_replace( '/wp-admin/', '/', filter_var( wp_unslash( $_SERVER['REQUEST_URI'] ) ) ) : '',
|
||||
'tracksUserData' => Jetpack_Tracks_Client::get_connected_user_tracks_identity(),
|
||||
'tracksEventData' => array(
|
||||
'isAdmin' => $is_admin,
|
||||
'currentScreen' => $current_screen ? $current_screen->id : false,
|
||||
'blogID' => Jetpack_Options::get_option( 'id' ),
|
||||
'platform' => static::get_platform(),
|
||||
),
|
||||
'isSafeModeConfirmed' => Identity_Crisis::$is_safe_mode_confirmed,
|
||||
'consumerData' => static::get_consumer_data(),
|
||||
'isAdmin' => $is_admin,
|
||||
'possibleDynamicSiteUrlDetected' => $possible_dynamic_site_url_detected,
|
||||
'isDevelopmentSite' => $is_development_site,
|
||||
|
||||
/**
|
||||
* Use the filter to provide custom HTML elecontainer ID.
|
||||
*
|
||||
* @since 0.10.0
|
||||
*
|
||||
* @param string|null $containerID The container ID.
|
||||
*/
|
||||
'containerID' => apply_filters( 'identity_crisis_container_id', null ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the package consumer data.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_consumer_data() {
|
||||
if ( null !== static::$consumers ) {
|
||||
return static::$consumers;
|
||||
}
|
||||
|
||||
$consumers = apply_filters( 'jetpack_idc_consumers', array() );
|
||||
|
||||
if ( ! $consumers ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
usort(
|
||||
$consumers,
|
||||
function ( $c1, $c2 ) {
|
||||
$priority1 = ( array_key_exists( 'priority', $c1 ) && (int) $c1['priority'] ) ? (int) $c1['priority'] : 10;
|
||||
$priority2 = ( array_key_exists( 'priority', $c2 ) && (int) $c2['priority'] ) ? (int) $c2['priority'] : 10;
|
||||
|
||||
return $priority1 <=> $priority2;
|
||||
}
|
||||
);
|
||||
|
||||
$consumer_chosen = null;
|
||||
$consumer_url_length = 0;
|
||||
foreach ( $consumers as &$consumer ) {
|
||||
if ( empty( $consumer['admin_page'] ) || ! is_string( $consumer['admin_page'] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( isset( $consumer['customContent'] ) && is_callable( $consumer['customContent'] ) ) {
|
||||
$consumer['customContent'] = call_user_func( $consumer['customContent'] );
|
||||
}
|
||||
|
||||
if ( isset( $_SERVER['REQUEST_URI'] ) && str_starts_with( filter_var( wp_unslash( $_SERVER['REQUEST_URI'] ) ), $consumer['admin_page'] ) && strlen( $consumer['admin_page'] ) > $consumer_url_length ) {
|
||||
$consumer_chosen = $consumer;
|
||||
$consumer_url_length = strlen( $consumer['admin_page'] );
|
||||
}
|
||||
}
|
||||
unset( $consumer );
|
||||
|
||||
static::$consumers = $consumer_chosen ? $consumer_chosen : array_shift( $consumers );
|
||||
|
||||
return static::$consumers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the site platform.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private static function get_platform() {
|
||||
$host = new Host();
|
||||
|
||||
if ( $host->is_woa_site() ) {
|
||||
return 'woa';
|
||||
}
|
||||
|
||||
if ( $host->is_vip_site() ) {
|
||||
return 'vip';
|
||||
}
|
||||
|
||||
if ( $host->is_newspack_site() ) {
|
||||
return 'newspack';
|
||||
}
|
||||
|
||||
return 'self-hosted';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
<?php
|
||||
/**
|
||||
* IDC URL secret functionality.
|
||||
*
|
||||
* @package automattic/jetpack-connection
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\IdentityCrisis;
|
||||
|
||||
use Automattic\Jetpack\Connection\Urls;
|
||||
use Automattic\Jetpack\Tracking;
|
||||
use Jetpack_Options;
|
||||
|
||||
/**
|
||||
* IDC URL secret functionality.
|
||||
* A short-lived secret used to verify whether an IDC is coming from the same vs a different Jetpack site.
|
||||
*/
|
||||
class URL_Secret {
|
||||
|
||||
/**
|
||||
* The options key used to store the secret.
|
||||
*/
|
||||
const OPTION_KEY = 'identity_crisis_url_secret';
|
||||
|
||||
/**
|
||||
* Secret lifespan (5 minutes)
|
||||
*/
|
||||
const LIFESPAN = 300;
|
||||
|
||||
/**
|
||||
* The URL secret string.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
private $secret = null;
|
||||
|
||||
/**
|
||||
* The URL secret expiration date in unix timestamp.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
private $expires_at = null;
|
||||
|
||||
/**
|
||||
* Initialize the class.
|
||||
*/
|
||||
public function __construct() {
|
||||
$secret_data = $this->fetch();
|
||||
|
||||
if ( $secret_data !== null ) {
|
||||
$this->secret = $secret_data['secret'];
|
||||
$this->expires_at = $secret_data['expires_at'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the URL secret from the database.
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
private function fetch() {
|
||||
$data = Jetpack_Options::get_option( static::OPTION_KEY );
|
||||
|
||||
if ( $data === false || empty( $data['secret'] ) || empty( $data['expires_at'] ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ( time() > $data['expires_at'] ) {
|
||||
Jetpack_Options::delete_option( static::OPTION_KEY );
|
||||
return null;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new secret and save it in the options.
|
||||
*
|
||||
* @throws Exception Thrown if unable to save the new secret.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function create() {
|
||||
$secret_data = array(
|
||||
'secret' => $this->generate_secret(),
|
||||
'expires_at' => strval( time() + static::LIFESPAN ),
|
||||
);
|
||||
|
||||
$result = Jetpack_Options::update_option( static::OPTION_KEY, $secret_data );
|
||||
|
||||
if ( ! $result ) {
|
||||
throw new Exception( esc_html__( 'Unable to save new URL secret', 'jetpack-connection' ) );
|
||||
}
|
||||
|
||||
$this->secret = $secret_data['secret'];
|
||||
$this->expires_at = $secret_data['expires_at'];
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URL secret.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function get_secret() {
|
||||
return $this->secret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URL secret expiration date.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function get_expires_at() {
|
||||
return $this->expires_at;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the secret exists.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function exists() {
|
||||
return $this->secret && $this->expires_at;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the secret string.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function generate_secret() {
|
||||
return wp_generate_password( 12, false );
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate secret for response.
|
||||
*
|
||||
* @param string $flow used to tell which flow generated the exception.
|
||||
* @return string|null
|
||||
*/
|
||||
public static function create_secret( $flow = 'generating_secret_failed' ) {
|
||||
$secret_value = null;
|
||||
try {
|
||||
|
||||
$secret = new self();
|
||||
$secret->create();
|
||||
|
||||
if ( $secret->exists() ) {
|
||||
$secret_value = $secret->get_secret();
|
||||
}
|
||||
} catch ( Exception $e ) {
|
||||
// Track the error and proceed.
|
||||
( new Tracking() )->record_user_event( $flow, array( 'current_url' => Urls::site_url() ) );
|
||||
}
|
||||
return $secret_value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
/**
|
||||
* The Jetpack Connection Interface file.
|
||||
* No longer used.
|
||||
*
|
||||
* @package automattic/jetpack-connection
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Connection;
|
||||
|
||||
/**
|
||||
* This interface is no longer used and is now deprecated.
|
||||
*
|
||||
* @deprecated since jetpack 7.8
|
||||
*/
|
||||
interface Manager_Interface {
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
/**
|
||||
* Storage Provider Interface for External Storage.
|
||||
*
|
||||
* Defines the contract that all external storage providers must implement
|
||||
* to be compatible with the Jetpack Connection External Storage system.
|
||||
*
|
||||
* @package automattic/jetpack-connection
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Connection;
|
||||
|
||||
/**
|
||||
* Interface for external storage providers.
|
||||
*
|
||||
* All storage providers must implement this interface to ensure
|
||||
* compatibility with the External_Storage system.
|
||||
*
|
||||
* ## Required Methods
|
||||
*
|
||||
* - `is_available()` - Check if storage backend is accessible
|
||||
* - `should_handle( $option_name )` - Determine if provider handles the option
|
||||
* - `get( $option_name )` - Retrieve value from external storage
|
||||
* - `get_environment_id()` - Return environment identifier for logging
|
||||
*
|
||||
* ## Optional Methods
|
||||
*
|
||||
* Providers may implement these additional methods for enhanced functionality:
|
||||
*
|
||||
* ### handle_error_event( string $event_type, string $key, string $details, string $environment )
|
||||
*
|
||||
* Called when External_Storage detects an error or empty state. Implement this
|
||||
* to report errors to your host's monitoring system.
|
||||
*
|
||||
* - `$event_type` - 'error' or 'empty'
|
||||
* - `$key` - The option key that triggered the event
|
||||
* - `$details` - Additional error details (error message, etc.)
|
||||
* - `$environment` - Environment identifier from get_environment_id()
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* public function handle_error_event( $event_type, $key, $details, $environment ) {
|
||||
* // Report to your monitoring system
|
||||
* wp_remote_post(
|
||||
* 'https://your-api/errors',
|
||||
* array(
|
||||
* 'body' => array(
|
||||
* 'event_type' => $event_type,
|
||||
* 'key' => $key,
|
||||
* 'details' => $details,
|
||||
* 'environment' => $environment,
|
||||
* ),
|
||||
* )
|
||||
* );
|
||||
* }
|
||||
*
|
||||
* ### get_empty_state_delay_threshold()
|
||||
*
|
||||
* Customize the delay before reporting empty states. Returns delay in seconds.
|
||||
* Default (when not implemented) is 5 minutes (300 seconds). Use this if your
|
||||
* storage system has different sync times.
|
||||
*
|
||||
* - Return `0` if external storage is the source of truth (written first)
|
||||
* - Return higher values for slower sync systems
|
||||
* - Maximum allowed value is 15 minutes (900 seconds); values above this are ignored
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* public function get_empty_state_delay_threshold() {
|
||||
* return 90; // 90 seconds for fast-syncing storage
|
||||
* // return 0; // No delay - external storage is written first
|
||||
* }
|
||||
*
|
||||
* @since 6.18.0
|
||||
*/
|
||||
interface Storage_Provider_Interface {
|
||||
|
||||
/**
|
||||
* Check if the storage provider is available in the current environment.
|
||||
*
|
||||
* This method should return true if the storage backend is accessible
|
||||
* and ready to handle requests, false otherwise.
|
||||
*
|
||||
* @since 6.18.0
|
||||
*
|
||||
* @return bool True if storage is available, false otherwise.
|
||||
*/
|
||||
public function is_available();
|
||||
|
||||
/**
|
||||
* Determine if this provider should handle the given option.
|
||||
*
|
||||
* This method allows providers to selectively handle certain options
|
||||
* based on their configuration, environment, or other criteria.
|
||||
*
|
||||
* @since 6.18.0
|
||||
*
|
||||
* @param string $option_name The name of the option to check.
|
||||
* @return bool True if this provider should handle the option, false otherwise.
|
||||
*/
|
||||
public function should_handle( $option_name );
|
||||
|
||||
/**
|
||||
* Retrieve a value from external storage.
|
||||
*
|
||||
* This method should return the value from external storage, or null
|
||||
* if the value is not found or cannot be retrieved.
|
||||
*
|
||||
* @since 6.18.0
|
||||
*
|
||||
* @param string $option_name The name of the option to retrieve.
|
||||
* @return mixed The option value, or null if not found/available.
|
||||
* @throws \Exception If there's an error retrieving the value.
|
||||
*/
|
||||
public function get( $option_name );
|
||||
|
||||
/**
|
||||
* Get the environment identifier for this provider.
|
||||
*
|
||||
* This method should return a unique identifier for the environment
|
||||
* or storage type (e.g., 'atomic', 'vip', 'kubernetes', etc.).
|
||||
* Used for logging and debugging purposes.
|
||||
*
|
||||
* @since 6.18.0
|
||||
*
|
||||
* @return string The environment identifier.
|
||||
*/
|
||||
public function get_environment_id();
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
<?php
|
||||
/**
|
||||
* Force Jetpack 2FA Functionality
|
||||
*
|
||||
* Ported from original repo at https://github.com/automattic/jetpack-force-2fa
|
||||
*
|
||||
* @package automattic/jetpack-connection
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Connection\SSO;
|
||||
|
||||
use Automattic\Jetpack\Connection\SSO;
|
||||
use Automattic\Jetpack\Modules;
|
||||
use WP_Error;
|
||||
|
||||
/**
|
||||
* Force users to use two-factor authentication.
|
||||
*
|
||||
* @phan-constructor-used-for-side-effects
|
||||
*/
|
||||
class Force_2FA {
|
||||
/**
|
||||
* The role to force 2FA for.
|
||||
*
|
||||
* Defaults to manage_options via the plugins_loaded function.
|
||||
* Can be modified with the jetpack_force_2fa_cap filter.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $role;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
add_action( 'after_setup_theme', array( $this, 'plugins_loaded' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the plugin via the plugins_loaded hook.
|
||||
*/
|
||||
public function plugins_loaded() {
|
||||
/**
|
||||
* Filter the role to force 2FA for.
|
||||
* Defaults to manage_options.
|
||||
*
|
||||
* @param string $role The role to force 2FA for.
|
||||
* @return string
|
||||
* @since jetpack-12.7
|
||||
* @module SSO
|
||||
*/
|
||||
$this->role = apply_filters( 'jetpack_force_2fa_cap', 'manage_options' );
|
||||
|
||||
// Bail if Jetpack SSO is not active
|
||||
if ( ! ( new Modules() )->is_active( 'sso' ) ) {
|
||||
add_action( 'admin_notices', array( $this, 'admin_notice' ) );
|
||||
return;
|
||||
}
|
||||
|
||||
$this->force_2fa();
|
||||
}
|
||||
|
||||
/**
|
||||
* Display an admin notice if Jetpack SSO is not active.
|
||||
*/
|
||||
public function admin_notice() {
|
||||
/**
|
||||
* Filter if an admin notice is deplayed when Force 2FA is required, but SSO is not enabled.
|
||||
* Defaults to true.
|
||||
*
|
||||
* @param bool $display_notice Whether to display the notice.
|
||||
* @return bool
|
||||
* @since jetpack-12.7
|
||||
* @module SSO
|
||||
*/
|
||||
if ( apply_filters( 'jetpack_force_2fa_dependency_notice', true ) && current_user_can( $this->role ) ) {
|
||||
wp_admin_notice(
|
||||
esc_html__( 'Jetpack Force 2FA requires Jetpack’s SSO feature.', 'jetpack-connection' ),
|
||||
array(
|
||||
'type' => 'warning',
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Force 2FA when using Jetpack SSO and force Jetpack SSO.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function force_2fa() {
|
||||
// Allows WP.com login to a local account if it matches the local account.
|
||||
add_filter( 'jetpack_sso_match_by_email', '__return_true', 9999 );
|
||||
|
||||
// multisite
|
||||
if ( is_multisite() ) {
|
||||
|
||||
// Hide the login form
|
||||
add_filter( 'jetpack_remove_login_form', '__return_true', 9999 );
|
||||
add_filter( 'jetpack_sso_bypass_login_forward_wpcom', '__return_true', 9999 );
|
||||
add_filter( 'jetpack_sso_display_disclaimer', '__return_false', 9999 );
|
||||
|
||||
add_filter(
|
||||
'wp_authenticate_user',
|
||||
function () {
|
||||
return new WP_Error( 'wpcom-required', $this->get_login_error_message() ); },
|
||||
9999
|
||||
);
|
||||
|
||||
add_filter( 'jetpack_sso_require_two_step', '__return_true' );
|
||||
|
||||
add_filter( 'allow_password_reset', '__return_false' );
|
||||
} else {
|
||||
// Not multisite.
|
||||
|
||||
// Completely disable the standard login form for admins.
|
||||
add_filter(
|
||||
'wp_authenticate_user',
|
||||
function ( $user ) {
|
||||
if ( is_wp_error( $user ) ) {
|
||||
return $user;
|
||||
}
|
||||
if ( $user->has_cap( $this->role ) ) {
|
||||
return new WP_Error( 'wpcom-required', $this->get_login_error_message(), $user->user_login );
|
||||
}
|
||||
return $user;
|
||||
},
|
||||
9999
|
||||
);
|
||||
|
||||
add_filter(
|
||||
'allow_password_reset',
|
||||
function ( $allow, $user_id ) {
|
||||
if ( user_can( $user_id, $this->role ) ) {
|
||||
return false;
|
||||
}
|
||||
return $allow; },
|
||||
9999,
|
||||
2
|
||||
);
|
||||
|
||||
add_action( 'jetpack_sso_pre_handle_login', array( $this, 'jetpack_set_two_step' ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifically set the two step filter for Jetpack SSO.
|
||||
*
|
||||
* @param Object $user_data The user data from WordPress.com.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function jetpack_set_two_step( $user_data ) {
|
||||
$user = SSO::get_user_by_wpcom_id( $user_data->ID );
|
||||
|
||||
// Borrowed from Jetpack. Ignores the match_by_email setting.
|
||||
if ( empty( $user ) ) {
|
||||
$user = get_user_by( 'email', $user_data->email );
|
||||
}
|
||||
|
||||
if ( $user && $user->has_cap( $this->role ) ) {
|
||||
add_filter( 'jetpack_sso_require_two_step', '__return_true' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the login error message.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function get_login_error_message() {
|
||||
/**
|
||||
* Filter the login error message.
|
||||
* Defaults to a message that explains the user must use a WordPress.com account with 2FA enabled.
|
||||
*
|
||||
* @param string $message The login error message.
|
||||
* @return string
|
||||
* @since jetpack-12.7
|
||||
* @module SSO
|
||||
*/
|
||||
return apply_filters(
|
||||
'jetpack_force_2fa_login_error_message',
|
||||
sprintf( 'For added security, please log in using your WordPress.com account.<br /><br />Note: Your account must have <a href="%1$s" target="_blank">Two Step Authentication</a> enabled, which can be configured from <a href="%2$s" target="_blank">Security Settings</a>.', 'https://support.wordpress.com/security/two-step-authentication/', 'https://wordpress.com/me/security/two-step' )
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,398 @@
|
||||
<?php
|
||||
/**
|
||||
* A collection of helper functions used in the SSO module.
|
||||
*
|
||||
* @package automattic/jetpack-connection
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Connection\SSO;
|
||||
|
||||
use Automattic\Jetpack\Connection\SSO;
|
||||
use Automattic\Jetpack\Constants;
|
||||
use Jetpack_IXR_Client;
|
||||
|
||||
/**
|
||||
* A collection of helper functions used in the SSO module.
|
||||
*
|
||||
* @since jetpack-4.1.0
|
||||
*/
|
||||
class Helpers {
|
||||
/**
|
||||
* Determine if the login form should be hidden or not
|
||||
*
|
||||
* @return bool
|
||||
**/
|
||||
public static function should_hide_login_form() {
|
||||
/**
|
||||
* Remove the default log in form, only leave the WordPress.com log in button.
|
||||
*
|
||||
* @module sso
|
||||
*
|
||||
* @since jetpack-3.1.0
|
||||
*
|
||||
* @param bool get_option( 'jetpack_sso_remove_login_form', false ) Should the default log in form be removed. Default to false.
|
||||
*/
|
||||
return (bool) apply_filters( 'jetpack_remove_login_form', get_option( 'jetpack_sso_remove_login_form', false ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a boolean value for whether logging in by matching the WordPress.com user email to a
|
||||
* Jetpack site user's email is allowed.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function match_by_email() {
|
||||
$match_by_email = defined( 'WPCC_MATCH_BY_EMAIL' ) ? \WPCC_MATCH_BY_EMAIL : (bool) get_option( 'jetpack_sso_match_by_email', true );
|
||||
|
||||
/**
|
||||
* Link the local account to an account on WordPress.com using the same email address.
|
||||
*
|
||||
* @module sso
|
||||
*
|
||||
* @since jetpack-2.6.0
|
||||
*
|
||||
* @param bool $match_by_email Should we link the local account to an account on WordPress.com using the same email address. Default to false.
|
||||
*/
|
||||
return (bool) apply_filters( 'jetpack_sso_match_by_email', $match_by_email );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a boolean for whether users are allowed to register on the Jetpack site with SSO,
|
||||
* even though the site disallows normal registrations.
|
||||
*
|
||||
* @param object|null $user_data WordPress.com user information.
|
||||
* @return bool|string
|
||||
*/
|
||||
public static function new_user_override( $user_data = null ) {
|
||||
$new_user_override = defined( 'WPCC_NEW_USER_OVERRIDE' ) ? \WPCC_NEW_USER_OVERRIDE : false;
|
||||
|
||||
/**
|
||||
* Allow users to register on your site with a WordPress.com account, even though you disallow normal registrations.
|
||||
* If you return a string that corresponds to a user role, the user will be given that role.
|
||||
*
|
||||
* @module sso
|
||||
*
|
||||
* @since jetpack-2.6.0
|
||||
* @since jetpack-4.6 $user_data object is now passed to the jetpack_sso_new_user_override filter
|
||||
*
|
||||
* @param bool|string $new_user_override Allow users to register on your site with a WordPress.com account. Default to false.
|
||||
* @param object|null $user_data An object containing the user data returned from WordPress.com.
|
||||
*/
|
||||
$role = apply_filters( 'jetpack_sso_new_user_override', $new_user_override, $user_data );
|
||||
|
||||
if ( $role ) {
|
||||
if ( is_string( $role ) && get_role( $role ) ) {
|
||||
return $role;
|
||||
} else {
|
||||
return get_option( 'default_role' );
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a boolean value for whether two-step authentication is required for SSO.
|
||||
*
|
||||
* @since jetpack-4.1.0
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_two_step_required() {
|
||||
/**
|
||||
* Is it required to have 2-step authentication enabled on WordPress.com to use SSO?
|
||||
*
|
||||
* @module sso
|
||||
*
|
||||
* @since jetpack-2.8.0
|
||||
*
|
||||
* @param bool get_option( 'jetpack_sso_require_two_step' ) Does SSO require 2-step authentication?
|
||||
*/
|
||||
return (bool) apply_filters( 'jetpack_sso_require_two_step', get_option( 'jetpack_sso_require_two_step', false ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a boolean for whether a user that is attempting to log in will be automatically
|
||||
* redirected to WordPress.com to begin the SSO flow.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function bypass_login_forward_wpcom() {
|
||||
/**
|
||||
* Redirect the site's log in form to WordPress.com's log in form.
|
||||
*
|
||||
* @module sso
|
||||
*
|
||||
* @since jetpack-3.1.0
|
||||
*
|
||||
* @param bool false Should the site's log in form be automatically forwarded to WordPress.com's log in form.
|
||||
*/
|
||||
return (bool) apply_filters( 'jetpack_sso_bypass_login_forward_wpcom', false );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a boolean for whether the SSO login form should be displayed as the default
|
||||
* when both the default and SSO login form allowed.
|
||||
*
|
||||
* @since jetpack-4.1.0
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function show_sso_login() {
|
||||
if ( self::should_hide_login_form() ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the SSO login form as the default when both the default and SSO login forms are enabled.
|
||||
*
|
||||
* @module sso
|
||||
*
|
||||
* @since jetpack-4.1.0
|
||||
*
|
||||
* @param bool true Should the SSO login form be displayed by default when the default login form is also enabled?
|
||||
*/
|
||||
return (bool) apply_filters( 'jetpack_sso_default_to_sso_login', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a boolean for whether the two step required checkbox, displayed on the Jetpack admin page, should be disabled.
|
||||
*
|
||||
* @since jetpack-4.1.0
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_require_two_step_checkbox_disabled() {
|
||||
return (bool) has_filter( 'jetpack_sso_require_two_step' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a boolean for whether the match by email checkbox, displayed on the Jetpack admin page, should be disabled.
|
||||
*
|
||||
* @since jetpack-4.1.0
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_match_by_email_checkbox_disabled() {
|
||||
return defined( 'WPCC_MATCH_BY_EMAIL' ) || has_filter( 'jetpack_sso_match_by_email' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of hosts that SSO will redirect to.
|
||||
*
|
||||
* Instead of accessing JETPACK__API_BASE within the method directly, we set it as the
|
||||
* default for $api_base due to restrictions with testing constants in our tests.
|
||||
*
|
||||
* @since jetpack-4.3.0
|
||||
* @since jetpack-4.6.0 Added public-api.wordpress.com as an allowed redirect
|
||||
*
|
||||
* @param array $hosts Allowed redirect hosts.
|
||||
* @param string $api_base Base API URL.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function allowed_redirect_hosts( $hosts, $api_base = '' ) {
|
||||
if ( empty( $api_base ) ) {
|
||||
$api_base = Constants::get_constant( 'JETPACK__API_BASE' );
|
||||
}
|
||||
|
||||
if ( empty( $hosts ) ) {
|
||||
$hosts = array();
|
||||
}
|
||||
|
||||
$hosts[] = 'wordpress.com';
|
||||
$hosts[] = 'jetpack.wordpress.com';
|
||||
$hosts[] = 'public-api.wordpress.com';
|
||||
$hosts[] = 'jetpack.com';
|
||||
|
||||
if ( ! str_contains( $api_base, 'jetpack.wordpress.com/jetpack' ) ) {
|
||||
$base_url_parts = wp_parse_url( esc_url_raw( $api_base ) );
|
||||
if ( $base_url_parts && ! empty( $base_url_parts['host'] ) ) {
|
||||
$hosts[] = $base_url_parts['host'];
|
||||
}
|
||||
}
|
||||
|
||||
foreach ( array( SSO::get_broker_url(), SSO::get_broker_auth_url() ) as $broker_url ) {
|
||||
if ( $broker_url ) {
|
||||
$broker_parts = wp_parse_url( $broker_url );
|
||||
if ( $broker_parts && ! empty( $broker_parts['host'] ) ) {
|
||||
$hosts[] = $broker_parts['host'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return array_unique( $hosts );
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines how long the auth cookie is valid for when a user logs in with SSO.
|
||||
*
|
||||
* @return int result of the jetpack_sso_auth_cookie_expiration filter.
|
||||
*/
|
||||
public static function extend_auth_cookie_expiration_for_sso() {
|
||||
/**
|
||||
* Determines how long the auth cookie is valid for when a user logs in with SSO.
|
||||
*
|
||||
* @module sso
|
||||
*
|
||||
* @since jetpack-4.4.0
|
||||
* @since jetpack-6.1.0 Fixed a typo. Filter was previously jetpack_sso_auth_cookie_expirtation.
|
||||
*
|
||||
* @param int YEAR_IN_SECONDS
|
||||
*/
|
||||
return (int) apply_filters( 'jetpack_sso_auth_cookie_expiration', YEAR_IN_SECONDS );
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the SSO form should be displayed for the current action.
|
||||
*
|
||||
* @since jetpack-4.6.0
|
||||
*
|
||||
* @param string $action SSO action being performed.
|
||||
*
|
||||
* @return bool Is SSO allowed for the current action?
|
||||
*/
|
||||
public static function display_sso_form_for_action( $action ) {
|
||||
/**
|
||||
* Allows plugins the ability to overwrite actions where the SSO form is allowed to be used.
|
||||
*
|
||||
* @module sso
|
||||
*
|
||||
* @since jetpack-4.6.0
|
||||
*
|
||||
* @param array $allowed_actions_for_sso
|
||||
*/
|
||||
$allowed_actions_for_sso = (array) apply_filters(
|
||||
'jetpack_sso_allowed_actions',
|
||||
array(
|
||||
'login',
|
||||
'jetpack-sso',
|
||||
'jetpack_json_api_authorization',
|
||||
'entered_recovery_mode',
|
||||
)
|
||||
);
|
||||
return in_array( $action, $allowed_actions_for_sso, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* This method returns an environment array that is meant to simulate `$_REQUEST` when the initial
|
||||
* JSON API auth request was made.
|
||||
*
|
||||
* @since jetpack-4.6.0
|
||||
*
|
||||
* @return array|bool
|
||||
*/
|
||||
public static function get_json_api_auth_environment() {
|
||||
if ( empty( $_COOKIE['jetpack_sso_original_request'] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$original_request = esc_url_raw( wp_unslash( $_COOKIE['jetpack_sso_original_request'] ) );
|
||||
|
||||
$parsed_url = wp_parse_url( $original_request );
|
||||
if ( empty( $parsed_url ) || empty( $parsed_url['query'] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$args = array();
|
||||
wp_parse_str( $parsed_url['query'], $args );
|
||||
|
||||
if ( empty( $args ) || empty( $args['action'] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( 'jetpack_json_api_authorization' !== $args['action'] ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return array_merge(
|
||||
$args,
|
||||
array( 'jetpack_json_api_original_query' => $original_request )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the site has a custom login page URL, and return it.
|
||||
* If default login page URL is used (`wp-login.php`), `null` will be returned.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public static function get_custom_login_url() {
|
||||
$login_url = wp_login_url();
|
||||
|
||||
if ( str_ends_with( $login_url, 'wp-login.php' ) ) {
|
||||
// No custom URL found.
|
||||
return null;
|
||||
}
|
||||
|
||||
$site_url = trailingslashit( site_url() );
|
||||
|
||||
if ( ! str_starts_with( $login_url, $site_url ) ) {
|
||||
// Something went wrong, we can't properly extract the custom URL.
|
||||
return null;
|
||||
}
|
||||
|
||||
// Extracting the "path" part of the URL, because we don't need the `site_url` part.
|
||||
return str_ireplace( $site_url, '', $login_url );
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the cookies that store the profile information for the last
|
||||
* WPCOM user to connect.
|
||||
*/
|
||||
public static function clear_wpcom_profile_cookies() {
|
||||
if ( isset( $_COOKIE[ 'jetpack_sso_wpcom_name_' . COOKIEHASH ] ) ) {
|
||||
setcookie(
|
||||
'jetpack_sso_wpcom_name_' . COOKIEHASH,
|
||||
' ',
|
||||
time() - YEAR_IN_SECONDS,
|
||||
COOKIEPATH,
|
||||
COOKIE_DOMAIN,
|
||||
is_ssl(),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
if ( isset( $_COOKIE[ 'jetpack_sso_wpcom_gravatar_' . COOKIEHASH ] ) ) {
|
||||
setcookie(
|
||||
'jetpack_sso_wpcom_gravatar_' . COOKIEHASH,
|
||||
' ',
|
||||
time() - YEAR_IN_SECONDS,
|
||||
COOKIEPATH,
|
||||
COOKIE_DOMAIN,
|
||||
is_ssl(),
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an SSO connection for a user.
|
||||
*
|
||||
* @param int $user_id The local user id.
|
||||
*/
|
||||
public static function delete_connection_for_user( $user_id ) {
|
||||
$wpcom_user_id = get_user_meta( $user_id, 'wpcom_user_id', true );
|
||||
if ( ! $wpcom_user_id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$xml = new Jetpack_IXR_Client(
|
||||
array(
|
||||
'wpcom_user_id' => $user_id,
|
||||
)
|
||||
);
|
||||
$xml->query( 'jetpack.sso.removeUser', $wpcom_user_id );
|
||||
|
||||
if ( $xml->isError() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Clean up local data stored for SSO.
|
||||
delete_user_meta( $user_id, 'wpcom_user_id' );
|
||||
delete_user_meta( $user_id, 'wpcom_user_data' );
|
||||
self::clear_wpcom_profile_cookies();
|
||||
|
||||
return $xml->getResponse();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
<?php
|
||||
/**
|
||||
* A collection of helper functions used in the SSO module.
|
||||
*
|
||||
* @package automattic/jetpack-connection
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Connection\SSO;
|
||||
|
||||
use Automattic\Jetpack\Redirect;
|
||||
use WP_Error;
|
||||
use WP_User;
|
||||
|
||||
/**
|
||||
* A collection of helper functions used in the SSO module.
|
||||
*
|
||||
* @since jetpack-4.4.0
|
||||
*/
|
||||
class Notices {
|
||||
/**
|
||||
* Error message displayed on the login form when two step is required and
|
||||
* the user's account on WordPress.com does not have two step enabled.
|
||||
*
|
||||
* @since jetpack-2.7
|
||||
* @param string $message Error message.
|
||||
* @return string
|
||||
**/
|
||||
public static function error_msg_enable_two_step( $message ) {
|
||||
$error = sprintf(
|
||||
wp_kses(
|
||||
/* translators: URL to settings page */
|
||||
__(
|
||||
'Two-Step Authentication is required to access this site. Please visit your <a href="%1$s" rel="noopener noreferrer" target="_blank">Security Settings</a> to configure <a href="%2$s" rel="noopener noreferrer" target="_blank">Two-step Authentication</a> for your account.',
|
||||
'jetpack-connection'
|
||||
),
|
||||
array( 'a' => array( 'href' => array() ) )
|
||||
),
|
||||
Redirect::get_url( 'calypso-me-security-two-step' ),
|
||||
Redirect::get_url( 'wpcom-support-security-two-step-authentication' )
|
||||
);
|
||||
|
||||
$message .= sprintf( '<p class="message" id="login_error">%s</p>', $error );
|
||||
|
||||
return $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Error message displayed when the user tries to SSO, but match by email
|
||||
* is off and they already have an account with their email address on
|
||||
* this site.
|
||||
*
|
||||
* @param string $message Error message.
|
||||
* @return string
|
||||
*/
|
||||
public static function error_msg_email_already_exists( $message ) {
|
||||
$error = sprintf(
|
||||
wp_kses(
|
||||
/* translators: login URL */
|
||||
__(
|
||||
'You already have an account on this site. Please <a href="%1$s">sign in</a> with your username and password and then connect to WordPress.com.',
|
||||
'jetpack-connection'
|
||||
),
|
||||
array( 'a' => array( 'href' => array() ) )
|
||||
),
|
||||
esc_url_raw( add_query_arg( 'jetpack-sso-show-default-form', '1', wp_login_url() ) )
|
||||
);
|
||||
|
||||
$message .= sprintf( '<p class="message" id="login_error">%s</p>', $error );
|
||||
|
||||
return $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Error message that is displayed when the current site is in an identity crisis and SSO cannot be used.
|
||||
*
|
||||
* @since jetpack-4.3.2
|
||||
*
|
||||
* @param string $message Error Message.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function error_msg_identity_crisis( $message ) {
|
||||
$error = esc_html__( 'Logging in with WordPress.com is not currently available because this site is experiencing connection problems.', 'jetpack-connection' );
|
||||
$message .= sprintf( '<p class="message" id="login_error">%s</p>', $error );
|
||||
return $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Error message that is displayed when we are not able to verify the SSO nonce due to an XML error or
|
||||
* failed validation. In either case, we prompt the user to try again or log in with username and password.
|
||||
*
|
||||
* @since jetpack-4.3.2
|
||||
*
|
||||
* @param string $message Error message.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function error_invalid_response_data( $message ) {
|
||||
$error = esc_html__(
|
||||
'There was an error logging you in via WordPress.com, please try again or try logging in with your username and password.',
|
||||
'jetpack-connection'
|
||||
);
|
||||
$message .= sprintf( '<p class="message" id="login_error">%s</p>', $error );
|
||||
return $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Error message that is displayed when we were not able to automatically create an account for a user
|
||||
* after a user has logged in via SSO. By default, this message is triggered after trying to create an account 5 times.
|
||||
*
|
||||
* @since jetpack-4.3.2
|
||||
*
|
||||
* @param string $message Error message.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function error_unable_to_create_user( $message ) {
|
||||
$error = esc_html__(
|
||||
'There was an error creating a user for you. Please contact the administrator of your site.',
|
||||
'jetpack-connection'
|
||||
);
|
||||
$message .= sprintf( '<p class="message" id="login_error">%s</p>', $error );
|
||||
return $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* When the default login form is hidden, this method is called on the 'authenticate' filter with a priority of 30.
|
||||
* This method disables the ability to submit the default login form.
|
||||
*
|
||||
* @param WP_User|WP_Error $user Either the user attempting to login or an existing authentication failure.
|
||||
*
|
||||
* @return WP_Error
|
||||
*/
|
||||
public static function disable_default_login_form( $user ) {
|
||||
if ( is_wp_error( $user ) ) {
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Since we're returning an error that will be shown as a red notice, let's remove the
|
||||
* informational "blue" notice.
|
||||
*/
|
||||
remove_filter( 'login_message', array( static::class, 'msg_login_by_jetpack' ) );
|
||||
return new WP_Error( 'jetpack_sso_required', self::get_sso_required_message() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Message displayed when the site admin has disabled the default WordPress
|
||||
* login form in Settings > General > Secure Sign On
|
||||
*
|
||||
* @since jetpack-2.7
|
||||
* @param string $message Error message.
|
||||
*
|
||||
* @return string
|
||||
**/
|
||||
public static function msg_login_by_jetpack( $message ) {
|
||||
$message .= sprintf( '<p class="message">%s</p>', self::get_sso_required_message() );
|
||||
return $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message for SSO required.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function get_sso_required_message() {
|
||||
$msg = esc_html__(
|
||||
'A WordPress.com account is required to access this site. Click the button below to sign in or create a free WordPress.com account.',
|
||||
'jetpack-connection'
|
||||
);
|
||||
|
||||
/**
|
||||
* Filter the message displayed when the default WordPress login form is disabled.
|
||||
*
|
||||
* @module sso
|
||||
*
|
||||
* @since jetpack-2.8.0
|
||||
*
|
||||
* @param string $msg Disclaimer when default WordPress login form is disabled.
|
||||
*/
|
||||
return apply_filters( 'jetpack_sso_disclaimer_message', $msg );
|
||||
}
|
||||
|
||||
/**
|
||||
* Message displayed when the user cannot be found after approving the SSO process on WordPress.com
|
||||
*
|
||||
* @param string $message Error message.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function cant_find_user( $message ) {
|
||||
$error = __(
|
||||
"We couldn't find your account. If you already have an account, make sure you have connected to WordPress.com.",
|
||||
'jetpack-connection'
|
||||
);
|
||||
|
||||
/**
|
||||
* Filters the "couldn't find your account" notice after an attempted SSO.
|
||||
*
|
||||
* @module sso
|
||||
*
|
||||
* @since jetpack-10.5.0
|
||||
*
|
||||
* @param string $error Error text.
|
||||
*/
|
||||
$error = apply_filters( 'jetpack_sso_unknown_user_notice', $error );
|
||||
|
||||
$message .= sprintf( '<p class="message" id="login_error">%s</p>', esc_html( $error ) );
|
||||
|
||||
return $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Error message that is displayed when the current site is in an identity crisis and SSO cannot be used.
|
||||
*
|
||||
* @since 2.10.0
|
||||
*
|
||||
* @param string $message Error message.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function sso_not_allowed_in_safe_mode( $message ) {
|
||||
$error = __(
|
||||
'Logging in with WordPress.com is disabled for sites that are in safe mode.',
|
||||
'jetpack-connection'
|
||||
);
|
||||
|
||||
/**
|
||||
* Filters the disallowed notice for sites in safe mode attempting SSO.
|
||||
*
|
||||
* @module sso
|
||||
*
|
||||
* @since 2.10.0
|
||||
*
|
||||
* @param string $error Error text.
|
||||
*/
|
||||
$error = apply_filters( 'jetpack_sso_disallowed_safe_mode_notice', $error );
|
||||
$message .= sprintf( '<p class="message">%s</p>', esc_html( $error ) );
|
||||
return $message;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
.jetpack-sso-admin-create-user-invite-message {
|
||||
width: 550px;
|
||||
}
|
||||
|
||||
.jetpack-sso-admin-create-user-invite-message-link-sso {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#createuser .form-field textarea {
|
||||
width: 25em;
|
||||
}
|
||||
|
||||
#createuser .form-field [type="checkbox"] {
|
||||
width: 1rem;
|
||||
}
|
||||
|
||||
#custom_email_message_description {
|
||||
max-width: 25rem;
|
||||
color: #646970;
|
||||
font-size: 12px;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
document.addEventListener( 'DOMContentLoaded', function () {
|
||||
const sendUserNotificationCheckbox = document.getElementById( 'send_user_notification' );
|
||||
const userExternalContractorCheckbox = document.getElementById( 'user_external_contractor' );
|
||||
const inviteUserWpcomCheckbox = document.getElementById( 'invite_user_wpcom' );
|
||||
const customEmailMessageBlock = document.getElementById( 'custom_email_message_block' );
|
||||
|
||||
if ( inviteUserWpcomCheckbox && sendUserNotificationCheckbox && customEmailMessageBlock ) {
|
||||
// Toggle Send User Notification checkbox enabled/disabled based on Invite User checkbox
|
||||
// Enable External Contractor checkbox if Invite User checkbox is checked
|
||||
// Show/hide the external email message field.
|
||||
inviteUserWpcomCheckbox.addEventListener( 'change', function () {
|
||||
sendUserNotificationCheckbox.disabled = inviteUserWpcomCheckbox.checked;
|
||||
if ( inviteUserWpcomCheckbox.checked ) {
|
||||
sendUserNotificationCheckbox.checked = false;
|
||||
if ( userExternalContractorCheckbox ) {
|
||||
userExternalContractorCheckbox.disabled = false;
|
||||
}
|
||||
customEmailMessageBlock.style.display = 'table';
|
||||
} else {
|
||||
if ( userExternalContractorCheckbox ) {
|
||||
userExternalContractorCheckbox.disabled = true;
|
||||
userExternalContractorCheckbox.checked = false;
|
||||
}
|
||||
customEmailMessageBlock.style.display = 'none';
|
||||
}
|
||||
} );
|
||||
|
||||
// On load, disable Send User Notification checkbox
|
||||
// and show the custom email message if Invite User checkbox is checked
|
||||
if ( inviteUserWpcomCheckbox.checked ) {
|
||||
sendUserNotificationCheckbox.disabled = true;
|
||||
sendUserNotificationCheckbox.checked = false;
|
||||
customEmailMessageBlock.style.display = 'table';
|
||||
}
|
||||
|
||||
// On load, disable External Contractor checkbox
|
||||
// and hide the custom email message if Invite User checkbox is unchecked
|
||||
if ( ! inviteUserWpcomCheckbox.checked ) {
|
||||
if ( userExternalContractorCheckbox ) {
|
||||
userExternalContractorCheckbox.disabled = true;
|
||||
}
|
||||
customEmailMessageBlock.style.display = 'none';
|
||||
}
|
||||
}
|
||||
} );
|
||||
@@ -0,0 +1,165 @@
|
||||
#loginform {
|
||||
|
||||
/* We set !important because sometimes static is added inline */
|
||||
position: relative !important;
|
||||
padding-bottom: 92px;
|
||||
}
|
||||
|
||||
.jetpack-sso-repositioned #loginform {
|
||||
padding-bottom: 26px;
|
||||
}
|
||||
|
||||
#loginform #jetpack-sso-wrap,
|
||||
#loginform #jetpack-sso-wrap * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
|
||||
#jetpack-sso-wrap__action,
|
||||
#jetpack-sso-wrap__user {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.jetpack-sso-form-display #jetpack-sso-wrap__action,
|
||||
.jetpack-sso-form-display #jetpack-sso-wrap__user {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#jetpack-sso-wrap {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
padding: 0 24px;
|
||||
margin-left: -24px;
|
||||
margin-right: -24px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.jetpack-sso-repositioned #jetpack-sso-wrap {
|
||||
position: relative;
|
||||
bottom: auto;
|
||||
padding: 0;
|
||||
margin-top: 16px;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.jetpack-sso-form-display #jetpack-sso-wrap {
|
||||
position: relative;
|
||||
bottom: auto;
|
||||
padding: 0;
|
||||
margin-top: 0;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
#loginform #jetpack-sso-wrap p {
|
||||
color: #777;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
#jetpack-sso-wrap a {
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#jetpack-sso-wrap .jetpack-sso-toggle.wpcom {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.jetpack-sso-form-display #jetpack-sso-wrap .jetpack-sso-toggle.wpcom {
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
.jetpack-sso-form-display #jetpack-sso-wrap .jetpack-sso-toggle.default {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
.jetpack-sso-form-display #loginform > p,
|
||||
.jetpack-sso-form-display #loginform > div {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.jetpack-sso-form-display #loginform #jetpack-sso-wrap {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.jetpack-sso-form-display #loginform {
|
||||
padding: 26px 24px;
|
||||
}
|
||||
|
||||
.jetpack-sso-or {
|
||||
margin-bottom: 16px;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.jetpack-sso-or::before {
|
||||
background: #dcdcde;
|
||||
content: "";
|
||||
height: 1px;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.jetpack-sso-or span {
|
||||
background: #fff;
|
||||
color: #777;
|
||||
position: relative;
|
||||
padding: 0 8px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
#jetpack-sso-wrap .button {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 36px;
|
||||
margin-bottom: 16px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#jetpack-sso-wrap .button .genericon-wordpress {
|
||||
font-size: 24px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
#jetpack-sso-wrap__user img {
|
||||
border-radius: 50%;
|
||||
display: block;
|
||||
margin: 0 auto 16px;
|
||||
}
|
||||
|
||||
#jetpack-sso-wrap__user h2 {
|
||||
font-size: 21px;
|
||||
font-weight: 300;
|
||||
margin-bottom: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#jetpack-sso-wrap__user h2 span {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.jetpack-sso-wrap__reauth {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.jetpack-sso-form-display #nav {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.jetpack-sso-form-display #backtoblog {
|
||||
margin: 24px 0 0;
|
||||
}
|
||||
|
||||
.jetpack-sso-clear::after {
|
||||
content: "";
|
||||
display: table;
|
||||
clear: both;
|
||||
}
|
||||