plugin updates

This commit is contained in:
Tony Volpe
2024-09-17 10:43:54 -04:00
parent 44b413346f
commit b7c8882c8c
1359 changed files with 58219 additions and 11364 deletions

View File

@@ -13,7 +13,7 @@ namespace Automattic\Jetpack\Admin_UI;
*/
class Admin_Menu {
const PACKAGE_VERSION = '0.2.25';
const PACKAGE_VERSION = '0.4.2';
/**
* Whether this class has been initialized
@@ -39,6 +39,7 @@ class Admin_Menu {
self::$initialized = true;
self::handle_akismet_menu();
add_action( 'admin_menu', array( __CLASS__, 'admin_menu_hook_callback' ), 1000 ); // Jetpack uses 998.
add_action( 'network_admin_menu', array( __CLASS__, 'admin_menu_hook_callback' ), 1000 ); // Jetpack uses 998.
}
}
@@ -50,23 +51,23 @@ class Admin_Menu {
*/
private static function handle_akismet_menu() {
if ( class_exists( 'Akismet_Admin' ) ) {
// Prevent Akismet from adding a menu item.
add_action(
'admin_menu',
function () {
// Prevent Akismet from adding a menu item.
remove_action( 'admin_menu', array( 'Akismet_Admin', 'admin_menu' ), 5 );
// Add an Anti-spam menu item for Jetpack.
self::add_menu( __( 'Akismet Anti-spam', 'jetpack-admin-ui' ), __( 'Akismet Anti-spam', 'jetpack-admin-ui' ), 'manage_options', 'akismet-key-config', array( 'Akismet_Admin', 'display_page' ) );
},
4
);
// Add an Anti-spam menu item for Jetpack.
self::add_menu( __( 'Akismet Anti-spam', 'jetpack-admin-ui' ), __( 'Akismet Anti-spam', 'jetpack-admin-ui' ), 'manage_options', 'akismet-key-config', array( 'Akismet_Admin', 'display_page' ) );
}
}
/**
* Callback to the admin_menu hook that will register the enqueued menu items
* Callback to the admin_menu and network_admin_menu hooks that will register the enqueued menu items
*
* @return void
*/
@@ -104,7 +105,7 @@ class Admin_Menu {
function ( $a, $b ) {
$position_a = empty( $a['position'] ) ? 0 : $a['position'];
$position_b = empty( $b['position'] ) ? 0 : $b['position'];
$result = $position_a - $position_b;
$result = $position_a <=> $position_b;
if ( 0 === $result ) {
$result = strcmp( $a['menu_title'], $b['menu_title'] );

View File

@@ -0,0 +1,357 @@
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
===================================
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.

View File

@@ -0,0 +1,25 @@
<?php
/**
* Action Hooks for Jetpack Assets module.
*
* @package automattic/jetpack-assets
*/
// 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( 'wp_default_scripts', array( Automattic\Jetpack\Assets::class, 'wp_default_scripts_hook' ) );
add_action( 'plugins_loaded', array( Automattic\Jetpack\Script_Data::class, 'configure' ), 1 );
} else {
global $wp_filter;
// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
$wp_filter['wp_default_scripts'][10][] = array(
'accepted_args' => 1,
'function' => array( Automattic\Jetpack\Assets::class, 'wp_default_scripts_hook' ),
);
// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
$wp_filter['plugins_loaded'][1][] = array(
'accepted_args' => 0,
'function' => array( Automattic\Jetpack\Script_Data::class, 'configure' ),
);
}

View File

@@ -0,0 +1 @@
<?php return array('dependencies' => array('wp-i18n'), 'version' => 'b5d2a25bb8ad1698db1c');

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
<?php return array('dependencies' => array(), 'version' => '3efc8c9f2b724b4a0bcb');

View File

@@ -0,0 +1 @@
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.JetpackScriptDataModule=t():e.JetpackScriptDataModule=t()}(globalThis,(()=>(()=>{var e={537:(e,t,r)=>{"use strict";r.r(t),r.d(t,{getActiveFeatures:()=>a.$l,getAdminUrl:()=>a.A7,getJetpackAdminPageUrl:()=>a.Z$,getMyJetpackUrl:()=>a.n3,getScriptData:()=>a.I4,getSiteData:()=>a.zf});var n=r(352),o={};for(const e in n)"default"!==e&&(o[e]=()=>n[e]);r.d(t,o);var a=r(618)},352:()=>{},618:(e,t,r)=>{"use strict";function n(){return window.JetpackScriptData}function o(){return n().site}function a(e=""){return`${n().site.admin_url}${e}`}function i(e=""){return a(`admin.php?page=jetpack${e}`)}function u(e=""){return a(`admin.php?page=my-jetpack${e}`)}function p(){return n().site.plan?.features?.active??[]}r.d(t,{$l:()=>p,A7:()=>a,I4:()=>n,Z$:()=>i,n3:()=>u,zf:()=>o})}},t={};function r(n){var o=t[n];if(void 0!==o)return o.exports;var a=t[n]={exports:{}};return e[n](a,a.exports,r),a.exports}r.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return r.d(t,{a:t}),t},r.d=(e,t)=>{for(var n in t)r.o(t,n)&&!r.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:t[n]})},r.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),r.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})};var n={};return(()=>{"use strict";r.r(n);var e=r(537),t={};for(const r in e)"default"!==r&&(t[r]=()=>e[r]);r.d(n,t)})(),n})()));

View File

@@ -0,0 +1,2 @@
/*! For license information please see react-jsx-runtime.js.LICENSE.txt */
(()=>{"use strict";var r={574:(r,e,t)=>{var o=t(196),n=Symbol.for("react.element"),s=Symbol.for("react.fragment"),a=Object.prototype.hasOwnProperty,f=o.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,p={key:!0,ref:!0,__self:!0,__source:!0};function _(r,e,t){var o,s={},_=null,i=null;for(o in void 0!==t&&(_=""+t),void 0!==e.key&&(_=""+e.key),void 0!==e.ref&&(i=e.ref),e)a.call(e,o)&&!p.hasOwnProperty(o)&&(s[o]=e[o]);if(r&&r.defaultProps)for(o in e=r.defaultProps)void 0===s[o]&&(s[o]=e[o]);return{$$typeof:n,type:r,key:_,ref:i,props:s,_owner:f.current}}e.Fragment=s,e.jsx=_,e.jsxs=_},93:(r,e,t)=>{r.exports=t(574)},196:r=>{r.exports=window.React}},e={},t=function t(o){var n=e[o];if(void 0!==n)return n.exports;var s=e[o]={exports:{}};return r[o](s,s.exports,t),s.exports}(93);window.ReactJSXRuntime=t})();

View File

@@ -0,0 +1,9 @@
/**
* @license React
* react-jsx-runtime.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

View File

@@ -0,0 +1,761 @@
<?php
/**
* Jetpack Assets package.
*
* @package automattic/jetpack-assets
*/
namespace Automattic\Jetpack;
use Automattic\Jetpack\Assets\Semver;
use Automattic\Jetpack\Constants as Jetpack_Constants;
use InvalidArgumentException;
/**
* Class Assets
*/
class Assets {
/**
* Holds all the scripts handles that should be loaded in a deferred fashion.
*
* @var array
*/
private $defer_script_handles = array();
/**
* The singleton instance of this class.
*
* @var Assets
*/
protected static $instance;
/**
* The registered textdomain mappings.
*
* @var array `array( mapped_domain => array( string target_domain, string target_type, string semver, string path_prefix ) )`.
*/
private static $domain_map = array();
/**
* Constructor.
*
* Static-only class, so nothing here.
*/
private function __construct() {}
// ////////////////////
// region Async script loading
/**
* Get the singleton instance of the class.
*
* @return Assets
*/
public static function instance() {
if ( ! isset( self::$instance ) ) {
self::$instance = new Assets();
}
return self::$instance;
}
/**
* A public method for adding the async script.
*
* @deprecated Since 2.1.0, the `strategy` feature should be used instead, with the "defer" setting.
*
* @param string $script_handle Script handle.
*/
public static function add_async_script( $script_handle ) {
_deprecated_function( __METHOD__, '2.1.0' );
wp_script_add_data( $script_handle, 'strategy', 'defer' );
}
/**
* Add an async attribute to scripts that can be loaded deferred.
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script
*
* @deprecated Since 2.1.0, the `strategy` feature should be used instead.
*
* @param string $tag The <script> tag for the enqueued script.
* @param string $handle The script's registered handle.
*/
public function script_add_async( $tag, $handle ) {
_deprecated_function( __METHOD__, '2.1.0' );
if ( empty( $this->defer_script_handles ) ) {
return $tag;
}
if ( in_array( $handle, $this->defer_script_handles, true ) ) {
// phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript
return preg_replace( '/<script( [^>]*)? src=/i', '<script defer$1 src=', $tag );
}
return $tag;
}
/**
* A helper function that lets you enqueue scripts in an async fashion.
*
* @deprecated Since 2.1.0 - use the strategy feature instead.
*
* @param string $handle Name of the script. Should be unique.
* @param string $min_path Minimized script path.
* @param string $non_min_path Full Script path.
* @param array $deps Array of script dependencies.
* @param bool $ver The script version.
* @param bool $in_footer Should the script be included in the footer.
*/
public static function enqueue_async_script( $handle, $min_path, $non_min_path, $deps = array(), $ver = false, $in_footer = true ) {
_deprecated_function( __METHOD__, '2.1.0' );
wp_enqueue_script( $handle, self::get_file_url_for_environment( $min_path, $non_min_path ), $deps, $ver, $in_footer );
wp_script_add_data( $handle, 'strategy', 'defer' );
}
// endregion .
// ////////////////////
// region Utils
/**
* Given a minified path, and a non-minified path, will return
* a minified or non-minified file URL based on whether SCRIPT_DEBUG is set and truthy.
*
* If $package_path is provided, then the minified or non-minified file URL will be generated
* relative to the root package directory.
*
* Both `$min_base` and `$non_min_base` can be either full URLs, or are expected to be relative to the
* root Jetpack directory.
*
* @param string $min_path minified path.
* @param string $non_min_path non-minified path.
* @param string $package_path Optional. A full path to a file inside a package directory
* The URL will be relative to its directory. Default empty.
* Typically this is done by passing __FILE__ as the argument.
*
* @return string The URL to the file
* @since 1.0.3
* @since-jetpack 5.6.0
*/
public static function get_file_url_for_environment( $min_path, $non_min_path, $package_path = '' ) {
$path = ( Jetpack_Constants::is_defined( 'SCRIPT_DEBUG' ) && Jetpack_Constants::get_constant( 'SCRIPT_DEBUG' ) )
? $non_min_path
: $min_path;
/*
* If the path is actually a full URL, keep that.
* We look for a host value, since enqueues are sometimes without a scheme.
*/
$file_parts = wp_parse_url( $path );
if ( ! empty( $file_parts['host'] ) ) {
$url = $path;
} else {
$plugin_path = empty( $package_path ) ? Jetpack_Constants::get_constant( 'JETPACK__PLUGIN_FILE' ) : $package_path;
$url = plugins_url( $path, $plugin_path );
}
/**
* Filters the URL for a file passed through the get_file_url_for_environment function.
*
* @since 1.0.3
*
* @package assets
*
* @param string $url The URL to the file.
* @param string $min_path The minified path.
* @param string $non_min_path The non-minified path.
*/
return apply_filters( 'jetpack_get_file_for_environment', $url, $min_path, $non_min_path );
}
/**
* Passes an array of URLs to wp_resource_hints.
*
* @since 1.5.0
*
* @param string|array $urls URLs to hint.
* @param string $type One of the supported resource types: dns-prefetch (default), preconnect, prefetch, or prerender.
*/
public static function add_resource_hint( $urls, $type = 'dns-prefetch' ) {
add_filter(
'wp_resource_hints',
function ( $hints, $resource_type ) use ( $urls, $type ) {
if ( $resource_type === $type ) {
// Type casting to array required since the function accepts a single string.
foreach ( (array) $urls as $url ) {
$hints[] = $url;
}
}
return $hints;
},
10,
2
);
}
/**
* Serve a WordPress.com static resource via a randomized wp.com subdomain.
*
* @since 1.9.0
*
* @param string $url WordPress.com static resource URL.
*
* @return string $url
*/
public static function staticize_subdomain( $url ) {
// Extract hostname from URL.
$host = wp_parse_url( $url, PHP_URL_HOST );
// Explode hostname on '.'.
$exploded_host = explode( '.', $host );
// Retrieve the name and TLD.
if ( count( $exploded_host ) > 1 ) {
$name = $exploded_host[ count( $exploded_host ) - 2 ];
$tld = $exploded_host[ count( $exploded_host ) - 1 ];
// Rebuild domain excluding subdomains.
$domain = $name . '.' . $tld;
} else {
$domain = $host;
}
// Array of Automattic domains.
$domains_allowed = array( 'wordpress.com', 'wp.com' );
// Return $url if not an Automattic domain.
if ( ! in_array( $domain, $domains_allowed, true ) ) {
return $url;
}
if ( \is_ssl() ) {
return preg_replace( '|https?://[^/]++/|', 'https://s-ssl.wordpress.com/', $url );
}
/*
* Generate a random subdomain id by taking the modulus of the crc32 value of the URL.
* Valid values are 0, 1, and 2.
*/
$static_counter = abs( crc32( basename( $url ) ) % 3 );
return preg_replace( '|://[^/]+?/|', "://s$static_counter.wp.com/", $url );
}
/**
* Resolve '.' and '..' components in a path or URL.
*
* @since 1.12.0
* @param string $path Path or URL.
* @return string Normalized path or URL.
*/
public static function normalize_path( $path ) {
$parts = wp_parse_url( $path );
if ( ! isset( $parts['path'] ) ) {
return $path;
}
$ret = '';
$ret .= isset( $parts['scheme'] ) ? $parts['scheme'] . '://' : '';
if ( isset( $parts['user'] ) || isset( $parts['pass'] ) ) {
$ret .= $parts['user'] ?? '';
$ret .= isset( $parts['pass'] ) ? ':' . $parts['pass'] : '';
$ret .= '@';
}
$ret .= $parts['host'] ?? '';
$ret .= isset( $parts['port'] ) ? ':' . $parts['port'] : '';
$pp = explode( '/', $parts['path'] );
if ( '' === $pp[0] ) {
$ret .= '/';
array_shift( $pp );
}
$i = 0;
while ( $i < count( $pp ) ) { // phpcs:ignore Squiz.PHP.DisallowSizeFunctionsInLoops.Found
if ( '' === $pp[ $i ] || '.' === $pp[ $i ] || 0 === $i && '..' === $pp[ $i ] ) {
array_splice( $pp, $i, 1 );
} elseif ( '..' === $pp[ $i ] ) {
array_splice( $pp, --$i, 2 );
} else {
++$i;
}
}
$ret .= implode( '/', $pp );
$ret .= isset( $parts['query'] ) ? '?' . $parts['query'] : '';
$ret .= isset( $parts['fragment'] ) ? '#' . $parts['fragment'] : '';
return $ret;
}
// endregion .
// ////////////////////
// region Webpack-built script registration
/**
* Register a Webpack-built script.
*
* Our Webpack-built scripts tend to need a bunch of boilerplate:
* - A call to `Assets::get_file_url_for_environment()` for possible debugging.
* - A call to `wp_register_style()` for extracted CSS, possibly with detection of RTL.
* - Loading of dependencies and version provided by `@wordpress/dependency-extraction-webpack-plugin`.
* - Avoiding WPCom's broken minifier.
*
* This wrapper handles all of that.
*
* @since 1.12.0
* @since 2.1.0 Add a new `strategy` option to leverage WP >= 6.3 script strategy feature. The `async` option is deprecated.
* @param string $handle Name of the script. Should be unique across both scripts and styles.
* @param string $path Minimized script path.
* @param string $relative_to File that `$path` is relative to. Pass `__FILE__`.
* @param array $options Additional options:
* - `asset_path`: (string|null) `.asset.php` to load. Default is to base it on `$path`.
* - `async`: (bool) Set true to register the script as deferred, like `Assets::enqueue_async_script()`. Deprecated in favor of `strategy`.
* - `css_dependencies`: (string[]) Additional style dependencies to queue.
* - `css_path`: (string|null) `.css` to load. Default is to base it on `$path`.
* - `dependencies`: (string[]) Additional script dependencies to queue.
* - `enqueue`: (bool) Set true to enqueue the script immediately.
* - `in_footer`: (bool) Set true to register script for the footer.
* - `media`: (string) Media for the css file. Default 'all'.
* - `minify`: (bool|null) Set true to pass `minify=true` in the query string, or `null` to suppress the normal `minify=false`.
* - `nonmin_path`: (string) Non-minified script path.
* - `strategy`: (string) Specify a script strategy to use, eg. `defer` or `async`. Default is `""`.
* - `textdomain`: (string) Text domain for the script. Required if the script depends on wp-i18n.
* - `version`: (string) Override the version from the `asset_path` file.
* @phan-param array{asset_path?:?string,async?:bool,css_dependencies?:string[],css_path?:?string,dependencies?:string[],enqueue?:bool,in_footer?:bool,media?:string,minify?:?bool,nonmin_path?:string,strategy?:string,textdomain?:string,version?:string} $options
* @throws \InvalidArgumentException If arguments are invalid.
*/
public static function register_script( $handle, $path, $relative_to, array $options = array() ) {
if ( substr( $path, -3 ) !== '.js' ) {
throw new \InvalidArgumentException( '$path must end in ".js"' );
}
if ( isset( $options['async'] ) ) {
_deprecated_argument( __METHOD__, '2.1.0', 'The `async` option is deprecated in favor of `strategy`' );
}
$dir = dirname( $relative_to );
$base = substr( $path, 0, -3 );
$options += array(
'asset_path' => "$base.asset.php",
'async' => false,
'css_dependencies' => array(),
'css_path' => "$base.css",
'dependencies' => array(),
'enqueue' => false,
'in_footer' => false,
'media' => 'all',
'minify' => false,
'strategy' => '',
'textdomain' => null,
);
'@phan-var array{asset_path:?string,async:bool,css_dependencies:string[],css_path:?string,dependencies:string[],enqueue:bool,in_footer:bool,media:string,minify:?bool,nonmin_path?:string,strategy:string,textdomain:string,version?:string} $options'; // Phan gets confused by the array addition.
if ( is_string( $options['css_path'] ) && $options['css_path'] !== '' && substr( $options['css_path'], -4 ) !== '.css' ) {
throw new \InvalidArgumentException( '$options[\'css_path\'] must end in ".css"' );
}
if ( isset( $options['nonmin_path'] ) ) {
$url = self::get_file_url_for_environment( $path, $options['nonmin_path'], $relative_to );
} else {
$url = plugins_url( $path, $relative_to );
}
$url = self::normalize_path( $url );
if ( null !== $options['minify'] ) {
$url = add_query_arg( 'minify', $options['minify'] ? 'true' : 'false', $url );
}
if ( $options['asset_path'] && file_exists( "$dir/{$options['asset_path']}" ) ) {
$asset = require "$dir/{$options['asset_path']}"; // phpcs:ignore WordPressVIPMinimum.Files.IncludingFile.NotAbsolutePath
$options['dependencies'] = array_merge( $asset['dependencies'], $options['dependencies'] );
$options['css_dependencies'] = array_merge(
array_filter(
$asset['dependencies'],
function ( $d ) {
return wp_style_is( $d, 'registered' );
}
),
$options['css_dependencies']
);
$ver = $options['version'] ?? $asset['version'];
} else {
// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
$ver = $options['version'] ?? @filemtime( "$dir/$path" );
}
if ( $options['async'] && '' === $options['strategy'] ) { // Handle the deprecated `async` option
$options['strategy'] = 'defer';
}
wp_register_script(
$handle,
$url,
$options['dependencies'],
$ver,
array(
'in_footer' => $options['in_footer'],
'strategy' => $options['strategy'],
)
);
if ( $options['textdomain'] ) {
// phpcs:ignore Jetpack.Functions.I18n.DomainNotLiteral
wp_set_script_translations( $handle, $options['textdomain'] );
} elseif ( in_array( 'wp-i18n', $options['dependencies'], true ) ) {
_doing_it_wrong(
__METHOD__,
/* translators: %s is the script handle. */
esc_html( sprintf( __( 'Script "%s" depends on wp-i18n but does not specify "textdomain"', 'jetpack-assets' ), $handle ) ),
''
);
}
if ( is_string( $options['css_path'] ) && $options['css_path'] !== '' && file_exists( "$dir/{$options['css_path']}" ) ) {
$csspath = $options['css_path'];
if ( is_rtl() ) {
$rtlcsspath = substr( $csspath, 0, -4 ) . '.rtl.css';
if ( file_exists( "$dir/$rtlcsspath" ) ) {
$csspath = $rtlcsspath;
}
}
$url = self::normalize_path( plugins_url( $csspath, $relative_to ) );
if ( null !== $options['minify'] ) {
$url = add_query_arg( 'minify', $options['minify'] ? 'true' : 'false', $url );
}
wp_register_style( $handle, $url, $options['css_dependencies'], $ver, $options['media'] );
wp_script_add_data( $handle, 'Jetpack::Assets::hascss', true );
} else {
wp_script_add_data( $handle, 'Jetpack::Assets::hascss', false );
}
if ( $options['enqueue'] ) {
self::enqueue_script( $handle );
}
}
/**
* Enqueue a script registered with `Assets::register_script`.
*
* @since 1.12.0
* @param string $handle Name of the script. Should be unique across both scripts and styles.
*/
public static function enqueue_script( $handle ) {
wp_enqueue_script( $handle );
if ( wp_scripts()->get_data( $handle, 'Jetpack::Assets::hascss' ) ) {
wp_enqueue_style( $handle );
}
}
/**
* 'wp_default_scripts' action handler.
*
* This registers the `wp-jp-i18n-loader` script for use by Webpack bundles built with
* `@automattic/i18n-loader-webpack-plugin`.
*
* @since 1.14.0
* @param \WP_Scripts $wp_scripts WP_Scripts instance.
*/
public static function wp_default_scripts_hook( $wp_scripts ) {
$data = array(
'baseUrl' => false,
'locale' => determine_locale(),
'domainMap' => array(),
'domainPaths' => array(),
);
$lang_dir = Jetpack_Constants::get_constant( 'WP_LANG_DIR' );
$content_dir = Jetpack_Constants::get_constant( 'WP_CONTENT_DIR' );
$abspath = Jetpack_Constants::get_constant( 'ABSPATH' );
// Note: str_starts_with() is not used here, as wp-includes/compat.php may not be loaded at this point.
if ( strpos( $lang_dir, $content_dir ) === 0 ) {
$data['baseUrl'] = content_url( substr( trailingslashit( $lang_dir ), strlen( trailingslashit( $content_dir ) ) ) );
} elseif ( strpos( $lang_dir, $abspath ) === 0 ) {
$data['baseUrl'] = site_url( substr( trailingslashit( $lang_dir ), strlen( untrailingslashit( $abspath ) ) ) );
}
foreach ( self::$domain_map as $from => list( $to, $type, , $path ) ) {
$data['domainMap'][ $from ] = ( 'core' === $type ? '' : "{$type}/" ) . $to;
if ( '' !== $path ) {
$data['domainPaths'][ $from ] = trailingslashit( $path );
}
}
/**
* Filters the i18n state data for use by Webpack bundles built with
* `@automattic/i18n-loader-webpack-plugin`.
*
* @since 1.14.0
* @package assets
* @param array $data The state data to generate. Expected fields are:
* - `baseUrl`: (string|false) The URL to the languages directory. False if no URL could be determined.
* - `locale`: (string) The locale for the page.
* - `domainMap`: (string[]) A mapping from Composer package textdomains to the corresponding
* `plugins/textdomain` or `themes/textdomain` (or core `textdomain`, but that's unlikely).
* - `domainPaths`: (string[]) A mapping from Composer package textdomains to the corresponding package
* paths.
*/
$data = apply_filters( 'jetpack_i18n_state', $data );
// Can't use self::register_script(), this action is called too early.
if ( file_exists( __DIR__ . '/../build/i18n-loader.asset.php' ) ) {
$path = '../build/i18n-loader.js';
$asset = require __DIR__ . '/../build/i18n-loader.asset.php';
} else {
$path = 'js/i18n-loader.js';
$asset = array(
'dependencies' => array( 'wp-i18n' ),
'version' => filemtime( __DIR__ . "/$path" ),
);
}
$url = self::normalize_path( plugins_url( $path, __FILE__ ) );
$url = add_query_arg( 'minify', 'true', $url );
$wp_scripts->add( 'wp-jp-i18n-loader', $url, $asset['dependencies'], $asset['version'] );
if ( ! is_array( $data ) ||
! isset( $data['baseUrl'] ) || ! ( is_string( $data['baseUrl'] ) || false === $data['baseUrl'] ) ||
! isset( $data['locale'] ) || ! is_string( $data['locale'] ) ||
! isset( $data['domainMap'] ) || ! is_array( $data['domainMap'] ) ||
! isset( $data['domainPaths'] ) || ! is_array( $data['domainPaths'] )
) {
$wp_scripts->add_inline_script( 'wp-jp-i18n-loader', 'console.warn( "I18n state deleted by jetpack_i18n_state hook" );' );
} elseif ( ! $data['baseUrl'] ) {
$wp_scripts->add_inline_script( 'wp-jp-i18n-loader', 'console.warn( "Failed to determine languages base URL. Is WP_LANG_DIR in the WordPress root?" );' );
} else {
$data['domainMap'] = (object) $data['domainMap']; // Ensure it becomes a json object.
$data['domainPaths'] = (object) $data['domainPaths']; // Ensure it becomes a json object.
$wp_scripts->add_inline_script( 'wp-jp-i18n-loader', 'wp.jpI18nLoader.state = ' . wp_json_encode( $data, JSON_UNESCAPED_SLASHES ) . ';' );
}
// Deprecated state module: Depend on wp-i18n to ensure global `wp` exists and because anything needing this will need that too.
$wp_scripts->add( 'wp-jp-i18n-state', false, array( 'wp-deprecated', 'wp-jp-i18n-loader' ) );
$wp_scripts->add_inline_script( 'wp-jp-i18n-state', 'wp.deprecated( "wp-jp-i18n-state", { alternative: "wp-jp-i18n-loader" } );' );
$wp_scripts->add_inline_script( 'wp-jp-i18n-state', 'wp.jpI18nState = wp.jpI18nLoader.state;' );
// Register the React JSX runtime script - used as a polyfill until we can update JSX transforms. See https://github.com/Automattic/jetpack/issues/38424.
// @todo Remove this when we drop support for WordPress 6.5, as well as the script inclusion in test_wp_default_scripts_hook.
$jsx_url = self::normalize_path( plugins_url( '../build/react-jsx-runtime.js', __FILE__ ) );
$wp_scripts->add( 'react-jsx-runtime', $jsx_url, array( 'react' ), '18.3.1', true );
}
// endregion .
// ////////////////////
// region Textdomain aliasing
/**
* Register a textdomain alias.
*
* Composer packages included in plugins will likely not use the textdomain of the plugin, while
* WordPress's i18n infrastructure will include the translations in the plugin's domain. This
* allows for mapping the package's domain to the plugin's.
*
* Since multiple plugins may use the same package, we include the package's version here so
* as to choose the most recent translations (which are most likely to match the package
* selected by jetpack-autoloader).
*
* @since 1.15.0
* @param string $from Domain to alias.
* @param string $to Domain to alias it to.
* @param string $totype What is the target of the alias: 'plugins', 'themes', or 'core'.
* @param string $ver Version of the `$from` domain.
* @param string $path Path to prepend when lazy-loading from JavaScript.
* @throws InvalidArgumentException If arguments are invalid.
*/
public static function alias_textdomain( $from, $to, $totype, $ver, $path = '' ) {
if ( ! in_array( $totype, array( 'plugins', 'themes', 'core' ), true ) ) {
throw new InvalidArgumentException( 'Type must be "plugins", "themes", or "core"' );
}
if (
did_action( 'wp_default_scripts' ) &&
// Don't complain during plugin activation.
! defined( 'WP_SANDBOX_SCRAPING' )
) {
_doing_it_wrong(
__METHOD__,
sprintf(
/* translators: 1: wp_default_scripts. 2: Name of the domain being aliased. */
esc_html__( 'Textdomain aliases should be registered before the %1$s hook. This notice was triggered by the %2$s domain.', 'jetpack-assets' ),
'<code>wp_default_scripts</code>',
'<code>' . esc_html( $from ) . '</code>'
),
''
);
}
if ( empty( self::$domain_map[ $from ] ) ) {
self::init_domain_map_hooks( $from, array() === self::$domain_map );
self::$domain_map[ $from ] = array( $to, $totype, $ver, $path );
} elseif ( Semver::compare( $ver, self::$domain_map[ $from ][2] ) > 0 ) {
self::$domain_map[ $from ] = array( $to, $totype, $ver, $path );
}
}
/**
* Register textdomain aliases from a mapping file.
*
* The mapping file is simply a PHP file that returns an array
* with the following properties:
* - 'domain': String, `$to`
* - 'type': String, `$totype`
* - 'packages': Array, mapping `$from` to `array( 'path' => $path, 'ver' => $ver )` (or to the string `$ver` for back compat).
*
* @since 1.15.0
* @param string $file Mapping file.
*/
public static function alias_textdomains_from_file( $file ) {
$data = require $file;
foreach ( $data['packages'] as $from => $fromdata ) {
if ( ! is_array( $fromdata ) ) {
$fromdata = array(
'path' => '',
'ver' => $fromdata,
);
}
self::alias_textdomain( $from, $data['domain'], $data['type'], $fromdata['ver'], $fromdata['path'] );
}
}
/**
* Register the hooks for textdomain aliasing.
*
* @param string $domain Domain to alias.
* @param bool $firstcall If this is the first call.
*/
private static function init_domain_map_hooks( $domain, $firstcall ) {
// 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_filter' ) ) {
$add_filter = 'add_filter';
} else {
$add_filter = function ( $hook_name, $callback, $priority = 10, $accepted_args = 1 ) {
global $wp_filter;
// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
$wp_filter[ $hook_name ][ $priority ][] = array(
'accepted_args' => $accepted_args,
'function' => $callback,
);
};
}
$add_filter( "gettext_{$domain}", array( self::class, 'filter_gettext' ), 10, 3 );
$add_filter( "ngettext_{$domain}", array( self::class, 'filter_ngettext' ), 10, 5 );
$add_filter( "gettext_with_context_{$domain}", array( self::class, 'filter_gettext_with_context' ), 10, 4 );
$add_filter( "ngettext_with_context_{$domain}", array( self::class, 'filter_ngettext_with_context' ), 10, 6 );
if ( $firstcall ) {
$add_filter( 'load_script_translation_file', array( self::class, 'filter_load_script_translation_file' ), 10, 3 );
}
}
/**
* Filter for `gettext`.
*
* @since 1.15.0
* @param string $translation Translated text.
* @param string $text Text to translate.
* @param string $domain Text domain.
* @return string Translated text.
*/
public static function filter_gettext( $translation, $text, $domain ) {
if ( $translation === $text ) {
// phpcs:ignore WordPress.WP.I18n -- This is a filter hook to map the text domains from our Composer packages to the domain for a containing plugin. See https://wp.me/p2gHKz-oRh#problem-6-text-domains-in-composer-packages
$newtext = __( $text, self::$domain_map[ $domain ][0] );
if ( $newtext !== $text ) {
return $newtext;
}
}
return $translation;
}
/**
* Filter for `ngettext`.
*
* @since 1.15.0
* @param string $translation Translated text.
* @param string $single The text to be used if the number is singular.
* @param string $plural The text to be used if the number is plural.
* @param int $number The number to compare against to use either the singular or plural form.
* @param string $domain Text domain.
* @return string Translated text.
*/
public static function filter_ngettext( $translation, $single, $plural, $number, $domain ) {
if ( $translation === $single || $translation === $plural ) {
// phpcs:ignore WordPress.WP.I18n -- This is a filter hook to map the text domains from our Composer packages to the domain for a containing plugin. See https://wp.me/p2gHKz-oRh#problem-6-text-domains-in-composer-packages
$translation = _n( $single, $plural, $number, self::$domain_map[ $domain ][0] );
}
return $translation;
}
/**
* Filter for `gettext_with_context`.
*
* @since 1.15.0
* @param string $translation Translated text.
* @param string $text Text to translate.
* @param string $context Context information for the translators.
* @param string $domain Text domain.
* @return string Translated text.
*/
public static function filter_gettext_with_context( $translation, $text, $context, $domain ) {
if ( $translation === $text ) {
// phpcs:ignore WordPress.WP.I18n -- This is a filter hook to map the text domains from our Composer packages to the domain for a containing plugin. See https://wp.me/p2gHKz-oRh#problem-6-text-domains-in-composer-packages
$translation = _x( $text, $context, self::$domain_map[ $domain ][0] );
}
return $translation;
}
/**
* Filter for `ngettext_with_context`.
*
* @since 1.15.0
* @param string $translation Translated text.
* @param string $single The text to be used if the number is singular.
* @param string $plural The text to be used if the number is plural.
* @param int $number The number to compare against to use either the singular or plural form.
* @param string $context Context information for the translators.
* @param string $domain Text domain.
* @return string Translated text.
*/
public static function filter_ngettext_with_context( $translation, $single, $plural, $number, $context, $domain ) {
if ( $translation === $single || $translation === $plural ) {
// phpcs:ignore WordPress.WP.I18n -- This is a filter hook to map the text domains from our Composer packages to the domain for a containing plugin. See https://wp.me/p2gHKz-oRh#problem-6-text-domains-in-composer-packages
$translation = _nx( $single, $plural, $number, $context, self::$domain_map[ $domain ][0] );
}
return $translation;
}
/**
* Filter for `load_script_translation_file`.
*
* @since 1.15.0
* @param string|false $file Path to the translation file to load. False if there isn't one.
* @param string $handle Name of the script to register a translation domain to.
* @param string $domain The text domain.
*/
public static function filter_load_script_translation_file( $file, $handle, $domain ) {
if ( false !== $file && isset( self::$domain_map[ $domain ] ) && ! is_readable( $file ) ) {
// Determine the part of the filename after the domain.
$suffix = basename( $file );
$l = strlen( $domain );
if ( substr( $suffix, 0, $l ) !== $domain || '-' !== $suffix[ $l ] ) {
return $file;
}
$suffix = substr( $suffix, $l );
$lang_dir = Jetpack_Constants::get_constant( 'WP_LANG_DIR' );
// Look for replacement files.
list( $newdomain, $type ) = self::$domain_map[ $domain ];
$newfile = $lang_dir . ( 'core' === $type ? '/' : "/{$type}/" ) . $newdomain . $suffix;
if ( is_readable( $newfile ) ) {
return $newfile;
}
}
return $file;
}
// endregion .
}
// Enable section folding in vim:
// vim: foldmarker=//\ region,//\ endregion foldmethod=marker
// .

View File

@@ -0,0 +1,204 @@
<?php
/**
* Jetpack script data.
*
* @package automattic/jetpack-assets
*/
namespace Automattic\Jetpack;
/**
* Class script data
*/
class Script_Data {
const SCRIPT_HANDLE = 'jetpack-script-data';
/**
* Configure.
*/
public static function configure() {
/**
* Ensure that assets are registered on wp_loaded,
* which is fired before *_enqueue_scripts actions.
* It means that when the dependent scripts are registered,
* the scripts here are already registered.
*/
add_action( 'wp_loaded', array( self::class, 'register_assets' ) );
/**
* Notes:
* 1. wp_print_scripts action is fired on both admin and public pages.
* On admin pages, it's fired before admin_enqueue_scripts action,
* which can be a problem if the consumer package uses admin_enqueue_scripts
* to hook into the script data. Thus, we prefer to use admin_print_scripts on admin pages.
* 2. We want to render the script data on print, instead of init or enqueue actions,
* so that the hook callbacks have enough time and information
* to decide whether to update the script data or not.
*/
$hook = is_admin() ? 'admin_print_scripts' : 'wp_print_scripts';
add_action( $hook, array( self::class, 'render_script_data' ), 1 );
}
/**
* Register assets.
*
* @access private
*/
public static function register_assets() {
Assets::register_script(
self::SCRIPT_HANDLE,
'../build/jetpack-script-data.js',
__FILE__,
array(
'in_footer' => true,
'textdomain' => 'jetpack-assets',
)
);
}
/**
* Render the script data using an inline script.
*
* @access private
*
* @return void
*/
public static function render_script_data() {
$script_data = is_admin() ? self::get_admin_script_data() : self::get_public_script_data();
$script_data = wp_json_encode(
$script_data,
JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP | JSON_UNESCAPED_UNICODE
);
wp_add_inline_script(
self::SCRIPT_HANDLE,
sprintf( 'window.JetpackScriptData = %s;', $script_data ),
'before'
);
}
/**
* Get the admin script data.
*
* @return array
*/
protected static function get_admin_script_data() {
global $wp_version;
$state = array(
'site' => array(
'admin_url' => esc_url_raw( admin_url() ),
'date_format' => get_option( 'date_format' ),
'icon' => self::get_site_icon(),
'is_multisite' => is_multisite(),
'plan' => array(
// The properties here should be updated by the consumer package/plugin.
// It includes properties like 'product_slug', 'features', etc.
'product_slug' => '',
),
'rest_nonce' => wp_create_nonce( 'wp_rest' ),
'rest_root' => esc_url_raw( rest_url() ),
'title' => self::get_site_title(),
'wp_version' => $wp_version,
'wpcom' => array(
// This should contain the connected site details like blog_id, is_atomic etc.
'blog_id' => 0,
),
),
'user' => array(
'current_user' => self::get_current_user_data(),
),
);
/**
* Filter the admin script data.
*
* When using this filter, ensure that the data is added only if it is used by some script.
* This filter may be called on almost every admin page load. So, one should check if the data is needed/used on that page.
* For example, the social (publicize) data is used only on Social admin page, Jetpack settings page and the post editor.
* So, the social data should be added only on those pages.
*
* @since 2.3.0
*
* @param array $state The script data.
*/
return apply_filters( 'jetpack_admin_js_script_data', $state );
}
/**
* Get the admin script data.
*
* @return array
*/
protected static function get_public_script_data() {
$state = array(
'site' => array(
'icon' => self::get_site_icon(),
'title' => self::get_site_title(),
),
);
/**
* Filter the public script data.
*
* See the docs for `jetpack_admin_js_script_data` filter for more information.
*
* @since 2.3.0
*
* @param array $state The script data.
*/
return apply_filters( 'jetpack_public_js_script_data', $state );
}
/**
* Get the site title.
*
* @return string
*/
protected static function get_site_title() {
$title = get_bloginfo( 'name' );
return $title ? $title : esc_url_raw( ( get_site_url() ) );
}
/**
* Get the site icon.
*
* @return string
*/
protected static function get_site_icon() {
if ( ! has_site_icon() ) {
return '';
}
/**
* Filters the site icon using Photon.
*
* @see https://developer.wordpress.com/docs/photon/
*
* @param string $url The URL of the site icon.
* @param array|string $args An array of arguments, e.g. array( 'w' => '300', 'resize' => array( 123, 456 ) ), or in string form (w=123&h=456).
*/
return apply_filters( 'jetpack_photon_url', get_site_icon_url(), array( 'w' => 64 ) );
}
/**
* Get the current user data.
*
* @return array
*/
protected static function get_current_user_data() {
$current_user = wp_get_current_user();
return array(
'display_name' => $current_user->display_name,
'id' => $current_user->ID,
);
}
}

View File

@@ -0,0 +1,121 @@
<?php
/**
* Simple semver version handling.
*
* We use this instead of something like `composer/semver` to avoid
* plugins needing to include yet-another dependency package. The
* amount of code we need here is pretty small.
*
* We use this instead of PHP's `version_compare()` because that doesn't
* handle prerelease versions in the way anyone other than PHP devs would
* expect, and silently breaks on various unexpected input.
*
* @package automattic/jetpack-assets
*/
namespace Automattic\Jetpack\Assets;
use InvalidArgumentException;
/**
* Simple semver version handling.
*/
class Semver {
/**
* Parse a semver version.
*
* @param string $version Version.
* @return array With components:
* - major: (int) Major version.
* - minor: (int) Minor version.
* - patch: (int) Patch version.
* - version: (string) Major.minor.patch.
* - prerelease: (string|null) Pre-release string.
* - buildinfo: (string|null) Build metadata string.
* @throws InvalidArgumentException If the version number is not in a recognized format.
*/
public static function parse( $version ) {
// This is slightly looser than the official version from semver.org, in that leading zeros are allowed.
if ( ! preg_match( '/^(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<prerelease>(?:[0-9a-zA-Z-]+)(?:\.(?:[0-9a-zA-Z-]+))*))?(?:\+(?P<buildinfo>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/', $version, $m ) ) {
throw new InvalidArgumentException( "Version number \"$version\" is not in a recognized format." );
}
$info = array(
'major' => (int) $m['major'],
'minor' => (int) $m['minor'],
'patch' => (int) $m['patch'],
'version' => sprintf( '%d.%d.%d', $m['major'], $m['minor'], $m['patch'] ),
'prerelease' => isset( $m['prerelease'] ) && '' !== $m['prerelease'] ? $m['prerelease'] : null,
'buildinfo' => isset( $m['buildinfo'] ) && '' !== $m['buildinfo'] ? $m['buildinfo'] : null,
);
if ( null !== $info['prerelease'] ) {
$sep = '';
$prerelease = '';
foreach ( explode( '.', $info['prerelease'] ) as $part ) {
if ( ctype_digit( $part ) ) {
$part = (int) $part;
}
$prerelease .= $sep . $part;
$sep = '.';
}
$info['prerelease'] = $prerelease;
}
return $info;
}
/**
* Compare two version numbers.
*
* @param string $a First version.
* @param string $b Second version.
* @return int Less than, equal to, or greater than 0 depending on whether `$a` is less than, equal to, or greater than `$b`.
* @throws InvalidArgumentException If the version numbers are not in a recognized format.
*/
public static function compare( $a, $b ) {
$aa = self::parse( $a );
$bb = self::parse( $b );
if ( $aa['major'] !== $bb['major'] ) {
return $aa['major'] - $bb['major'];
}
if ( $aa['minor'] !== $bb['minor'] ) {
return $aa['minor'] - $bb['minor'];
}
if ( $aa['patch'] !== $bb['patch'] ) {
return $aa['patch'] - $bb['patch'];
}
if ( null === $aa['prerelease'] ) {
return null === $bb['prerelease'] ? 0 : 1;
}
if ( null === $bb['prerelease'] ) {
return -1;
}
$aaa = explode( '.', $aa['prerelease'] );
$bbb = explode( '.', $bb['prerelease'] );
$al = count( $aaa );
$bl = count( $bbb );
for ( $i = 0; $i < $al && $i < $bl; $i++ ) {
$a = $aaa[ $i ];
$b = $bbb[ $i ];
if ( ctype_digit( $a ) ) {
if ( ctype_digit( $b ) ) {
if ( (int) $a !== (int) $b ) {
return (int) $a - (int) $b;
}
} else {
return -1;
}
} elseif ( ctype_digit( $b ) ) {
return 1;
} else {
$tmp = strcmp( $a, $b );
if ( 0 !== $tmp ) {
return $tmp;
}
}
}
return $al - $bl;
}
}

View File

@@ -0,0 +1,76 @@
const i18n = require( '@wordpress/i18n' );
const { default: md5 } = require( 'md5-es' );
const locationMap = {
plugin: 'plugins/',
theme: 'themes/',
core: '',
};
const hasOwn = ( obj, prop ) => Object.prototype.hasOwnProperty.call( obj, prop );
module.exports = {
state: {
baseUrl: null,
locale: null,
domainMap: {},
domainPaths: {},
},
/**
* Download and register translations for a bundle.
*
* @param {string} path - Bundle path being fetched. May have a query part.
* @param {string} domain - Text domain to register into.
* @param {string} location - Location for the translation: 'plugin', 'theme', or 'core'.
* @returns {Promise} Resolved when the translations are registered, or rejected with an `Error`.
*/
async downloadI18n( path, domain, location ) {
const state = this.state;
if ( ! state || typeof state.baseUrl !== 'string' ) {
throw new Error( 'wp.jpI18nLoader.state is not set' );
}
// "en_US" is the default, no translations are needed.
if ( state.locale === 'en_US' ) {
return;
}
// Check that fetch is available.
if ( typeof fetch === 'undefined' ) {
throw new Error( 'Fetch API is not available.' );
}
// Extract any query part and hash the script name like WordPress does.
const pathPrefix = hasOwn( state.domainPaths, domain ) ? state.domainPaths[ domain ] : '';
let hash, query;
const i = path.indexOf( '?' );
if ( i >= 0 ) {
hash = md5.hash( pathPrefix + path.substring( 0, i ) );
query = path.substring( i );
} else {
hash = md5.hash( pathPrefix + path );
query = '';
}
// Download.
const locationAndDomain = hasOwn( state.domainMap, domain )
? state.domainMap[ domain ]
: locationMap[ location ] + domain;
const res = await fetch(
// prettier-ignore
`${ state.baseUrl }${ locationAndDomain }-${ state.locale }-${ hash }.json${ query }`
);
if ( ! res.ok ) {
throw new Error( `HTTP request failed: ${ res.status } ${ res.statusText }` );
}
const data = await res.json();
// Extract the messages from the file and register them.
const localeData = hasOwn( data.locale_data, domain )
? data.locale_data[ domain ]
: data.locale_data.messages;
localeData[ '' ].domain = domain;
i18n.setLocaleData( localeData, domain );
},
};

View File

@@ -0,0 +1 @@
export * from '@automattic/jetpack-script-data';

View File

@@ -0,0 +1 @@
<?php return array('dependencies' => array('react', 'wp-components', 'wp-compose', 'wp-data', 'wp-element', 'wp-i18n', 'wp-url'), 'version' => '490b076190173a65d8da');

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
<?php return array('dependencies' => array(), 'version' => 'd0b88193b5b4008c3108');

View File

@@ -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}

View File

@@ -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"))}));

View File

@@ -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}

View File

@@ -0,0 +1 @@
<?php return array('dependencies' => array(), 'version' => '049775f3b0a647a127f9');

View File

@@ -0,0 +1 @@
#loginform{padding-bottom:92px;position:relative!important}.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{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>div,.jetpack-sso-form-display #loginform>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}

View File

@@ -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)}))}));

View File

@@ -0,0 +1 @@
#loginform{padding-bottom:92px;position:relative!important}.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{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>div,.jetpack-sso-form-display #loginform>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}

View File

@@ -0,0 +1 @@
<?php return array('dependencies' => array(), 'version' => '04d208524c748ec232f3');

View File

@@ -0,0 +1 @@
document.addEventListener("DOMContentLoaded",(function(){function t(){this.querySelector(".jetpack-sso-invitation-tooltip").style.display="block"}function e(t){document.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","jetpack-sso-th-tooltip");const n=window.Jetpack_SSOTooltip.tooltipString;function o(){t.appendChild(e),e.style.display="block"}function i(){document.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)}))}));

View File

@@ -1 +1 @@
<?php return array('dependencies' => array(), 'version' => 'd9dbf909a3d10fb26f39');
<?php return array('dependencies' => array(), 'version' => '37afc9296c403dfe5f38');

View File

@@ -1 +1 @@
(()=>{var e={775: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}(775);window.analytics=t})();
(()=>{var e={7775: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}(7775);window.analytics=t})();

View File

@@ -12,6 +12,13 @@
use Automattic\Jetpack\Connection\Client;
use Automattic\Jetpack\Connection\Manager;
/**
* Disable direct access.
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( ! class_exists( IXR_Client::class ) ) {
require_once ABSPATH . WPINC . '/class-IXR.php';
}
@@ -70,7 +77,7 @@ class Jetpack_IXR_Client extends IXR_Client {
/**
* Perform the IXR request.
*
* @param string[] ...$args IXR args.
* @param mixed ...$args IXR method and args.
*
* @return bool True if request succeeded, false otherwise.
*/
@@ -146,10 +153,10 @@ class Jetpack_IXR_Client extends IXR_Client {
$code = $match[1];
$message = $match[2];
$status = $fault_code;
return new \WP_Error( $code, $message, $status );
return new WP_Error( $code, $message, $status );
}
return new \WP_Error( "IXR_{$fault_code}", $fault_string );
return new WP_Error( "IXR_{$fault_code}", $fault_string );
}
/**

View File

@@ -65,7 +65,6 @@ class Jetpack_Options {
'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
'dismissed_connection_banner', // (bool) True if the connection banner has been dismissed
'ab_connect_banner_green_bar', // (int) Version displayed of the A/B test for the green bar at the top of the connect banner.
'onboarding', // (string) Auth token to be used in the onboarding connection flow
'tos_agreed', // (bool) Whether or not the TOS for connection has been agreed upon.
@@ -117,8 +116,6 @@ class Jetpack_Options {
'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_banner_dismissed', // (bool) Determines if the recommendations dashboard banner is dismissed or not.
'recommendations_banner_enabled', // (bool) Whether the recommendations are enabled or not.
'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.
@@ -129,6 +126,11 @@ class Jetpack_Options {
'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.
'historically_active_modules', // (array) List of installed plugins/enabled modules that have at one point in time been active and working
);
}
@@ -630,7 +632,6 @@ class Jetpack_Options {
'jetpack_protect_key',
'jetpack_protect_blocked_attempts',
'jetpack_protect_activating',
'jetpack_connection_banner_ab',
'jetpack_active_plan',
'jetpack_activation_source',
'jetpack_site_products',

View File

@@ -93,9 +93,9 @@ class Jetpack_Signature {
// 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
// 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
$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.
@@ -160,7 +160,7 @@ class Jetpack_Signature {
$signature_details = compact( 'token', 'timestamp', 'nonce', 'body_hash', 'method', 'url' );
if ( 0 !== strpos( $token, "$this->token:" ) ) {
if ( ! str_starts_with( $token, "$this->token:" ) ) {
return new WP_Error( 'token_mismatch', 'Incorrect token', compact( 'signature_details' ) );
}

View File

@@ -269,7 +269,7 @@ class Jetpack_XMLRPC_Server {
* 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 $request An array containing at minimum nonce and local_user keys.
* @param array|ArrayAccess $request An array containing at minimum nonce and local_user keys.
*
* @return \WP_Error|array
*/
@@ -373,7 +373,7 @@ class Jetpack_XMLRPC_Server {
* 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 $request An array containing at minimum a nonce key and a local_username key.
* @param array|ArrayAccess $request An array containing at minimum a nonce key and a local_username key.
*
* @return \WP_Error|array
*/
@@ -440,8 +440,8 @@ class Jetpack_XMLRPC_Server {
* 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 $request An array containing local_user and nonce keys at minimum.
* @param \IXR_Client $ixr_client The client object, optional.
* @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 ) {
@@ -521,6 +521,7 @@ class Jetpack_XMLRPC_Server {
* 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'] ) ) {
@@ -544,6 +545,7 @@ class Jetpack_XMLRPC_Server {
* 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 );

View File

@@ -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 ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
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 sites 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>';
}
}

View File

@@ -8,6 +8,10 @@
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.
@@ -18,9 +22,10 @@ class Client {
/**
* Makes an authorized remote request using Jetpack_Signature
*
* @param array $args the arguments for the remote request.
* @param array|String $body the request body.
* @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'] ) ) {
@@ -35,7 +40,7 @@ class Client {
}
$result = self::build_signed_request( $args, $body );
if ( ! $result || is_wp_error( $result ) ) {
if ( is_wp_error( $result ) ) {
return $result;
}
@@ -64,12 +69,12 @@ class Client {
/**
* Adds authorization signature to a remote request using Jetpack_Signature
*
* @param array $args the arguments for the remote request.
* @param array|String $body the request body.
* @return WP_Error|array {
* @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 string $url The request URL.
* @type array $request Request arguments.
* @type array $auth Authorization data.
* }
@@ -88,6 +93,7 @@ class Client {
'blog_id' => 0,
'auth_location' => Constants::get_constant( 'JETPACK_CLIENT__AUTH_LOCATION' ),
'method' => 'POST',
'format' => 'json',
'timeout' => 10,
'redirection' => 0,
'headers' => array(),
@@ -106,7 +112,7 @@ class Client {
$token = ( new Tokens() )->get_access_token( $args['user_id'] );
if ( ! $token ) {
return new \WP_Error( 'missing_token' );
return new WP_Error( 'missing_token' );
}
$method = strtoupper( $args['method'] );
@@ -121,8 +127,8 @@ class Client {
$request = compact( 'method', 'body', 'timeout', 'redirection', 'stream', 'filename', 'sslverify' );
@list( $token_key, $secret ) = explode( '.', $token->secret ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
if ( empty( $token ) || empty( $secret ) ) {
return new \WP_Error( 'malformed_token' );
if ( ! $secret ) {
return new WP_Error( 'malformed_token' );
}
$token_key = sprintf(
@@ -140,7 +146,7 @@ class Client {
if ( function_exists( 'wp_generate_password' ) ) {
$nonce = wp_generate_password( 10, false );
} else {
$nonce = substr( sha1( wp_rand( 0, 1000000 ) ), 0, 10 );
$nonce = substr( sha1( (string) wp_rand( 0, 1000000 ) ), 0, 10 );
}
// Kind of annoying. Maybe refactor Jetpack_Signature to handle body-hashing.
@@ -151,20 +157,22 @@ class Client {
// Allow arrays to be used in passing data.
$body_to_hash = $body;
if ( is_array( $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 ) {
$body_to_hash = wp_json_encode( self::_stringify_data( $body ) );
if ( array() !== $body_to_hash ) {
$body_to_hash = wp_json_encode( self::_stringify_data( $body_to_hash ) );
} else {
$body_to_hash = '';
}
}
if ( ! is_string( $body_to_hash ) ) {
return new \WP_Error( 'invalid_body', 'Body is malformed.' );
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
}
@@ -192,7 +200,7 @@ class Client {
$signature = $jetpack_signature->sign_request( $token_key, $timestamp, $nonce, $body_hash, $method, $url, $body, false );
if ( ! $signature || is_wp_error( $signature ) ) {
if ( is_wp_error( $signature ) ) {
return $signature;
}
@@ -229,10 +237,11 @@ class Client {
*
* @internal
*
* @param String $url the request URL.
* @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.
* @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' );
@@ -312,8 +321,9 @@ class Client {
/**
* Sets the time difference for correct signature computation.
*
* @param HTTP_Response $response the response object.
* @param Boolean $force_set whether to force setting the time difference.
* @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 );
@@ -353,7 +363,7 @@ class Client {
* @param array $args Arguments to {@see WP_Http}. Default is `array()`.
* @param string $base_api_path REST API root. Default is `wpcom`.
*
* @return array|WP_Error $response Response data, else {@see WP_Error} on failure.
* @return array Validated arguments.
*/
public static function validate_args_for_wpcom_json_api_request(
$path,
@@ -370,6 +380,7 @@ class Client {
array(
'headers' => 'array',
'method' => 'string',
'format' => 'string',
'timeout' => 'int',
'redirection' => 'int',
'stream' => 'boolean',
@@ -403,13 +414,14 @@ class Client {
/**
* 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 string $body Body passed to {@see WP_Http}. Default is `null`.
* @param string $base_api_path REST API root. Default is `wpcom`.
* @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,
@@ -435,12 +447,13 @@ class Client {
/**
* 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 String $body Request body.
* @param String $base_api_path (optional) the API base path override, defaults to 'rest'.
* @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,
@@ -467,7 +480,7 @@ class Client {
* 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 array|Mixed $data the data that needs to be stringified.
* @param mixed $data the data that needs to be stringified.
*
* @return array|string
*/

View File

@@ -702,11 +702,15 @@ class Error_Handler {
return;
}
?>
<div class="notice notice-error is-dismissible jetpack-message jp-connect" style="display:block !important;">
<p><?php echo esc_html( $message ); ?></p>
</div>
<?php
wp_admin_notice(
esc_html( $message ),
array(
'type' => 'error',
'dismissible' => true,
'additional_classes' => array( 'jetpack-message', 'jp-connect' ),
'attributes' => array( 'style' => 'display:block !important;' ),
)
);
}
/**

View File

@@ -7,8 +7,13 @@
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
@@ -73,6 +78,8 @@ class Heartbeat {
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' ) );
}
/**
@@ -249,4 +256,55 @@ class Heartbeat {
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() ) );
}
}

View File

@@ -10,13 +10,16 @@ namespace Automattic\Jetpack\Connection;
use Automattic\Jetpack\A8c_Mc_Stats;
use Automattic\Jetpack\Constants;
use Automattic\Jetpack\Heartbeat;
use Automattic\Jetpack\Partner;
use Automattic\Jetpack\Roles;
use Automattic\Jetpack\Status;
use Automattic\Jetpack\Status\Host;
use Automattic\Jetpack\Terms_Of_Service;
use Automattic\Jetpack\Tracking;
use IXR_Error;
use Jetpack_IXR_Client;
use Jetpack_Options;
use Jetpack_XMLRPC_Server;
use WP_Error;
use WP_User;
@@ -70,6 +73,13 @@ class Manager {
*/
private static $extra_register_params = array();
/**
* We store ID's of users already disconnected to prevent multiple disconnect requests.
*
* @var array
*/
private static $disconnected_users = array();
/**
* Initialize the object.
* Make sure to call the "Configure" first.
@@ -101,7 +111,7 @@ class Manager {
);
$manager->setup_xmlrpc_handlers(
$_GET, // phpcs:ignore WordPress.Security.NonceVerification.Recommended
null,
$manager->has_connected_owner(),
$manager->verify_xml_rpc_signature()
);
@@ -126,6 +136,10 @@ class Manager {
Webhooks::init( $manager );
// Unlink user before deleting the user from WP.com.
add_action( 'deleted_user', array( $manager, 'disconnect_user_force' ), 9, 1 );
add_action( 'remove_user_from_blog', array( $manager, 'disconnect_user_force' ), 9, 1 );
// Set up package version hook.
add_filter( 'jetpack_package_versions', __NAMESPACE__ . '\Package_Version::send_package_version_to_tracker' );
@@ -138,38 +152,40 @@ class Manager {
// Initialize token locks.
new Tokens_Locks();
// Initial Partner management.
Partner::init();
}
/**
* Sets up the XMLRPC request handlers.
*
* @since 1.25.0 Deprecate $is_active param.
* @since 2.8.4 Deprecate $request_params param.
*
* @param array $request_params incoming request parameters.
* @param bool $has_connected_owner Whether the site has a connected owner.
* @param bool $is_signed whether the signature check has been successful.
* @param \Jetpack_XMLRPC_Server $xmlrpc_server (optional) an instance of the server to use instead of instantiating a new one.
* @param array|null $deprecated Deprecated. Not used.
* @param bool $has_connected_owner Whether the site has a connected owner.
* @param bool $is_signed whether the signature check has been successful.
* @param Jetpack_XMLRPC_Server $xmlrpc_server (optional) an instance of the server to use instead of instantiating a new one.
*/
public function setup_xmlrpc_handlers(
$request_params,
$deprecated,
$has_connected_owner,
$is_signed,
\Jetpack_XMLRPC_Server $xmlrpc_server = null
Jetpack_XMLRPC_Server $xmlrpc_server = null
) {
add_filter( 'xmlrpc_blog_options', array( $this, 'xmlrpc_options' ), 1000, 2 );
if (
! isset( $request_params['for'] )
|| 'jetpack' !== $request_params['for']
) {
if ( $deprecated !== null ) {
_deprecated_argument( __METHOD__, '2.8.4' );
}
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- We are using the 'for' request param to early return unless it's 'jetpack'.
if ( ! isset( $_GET['for'] ) || 'jetpack' !== $_GET['for'] ) {
return false;
}
// Alternate XML-RPC, via ?for=jetpack&jetpack=comms.
if (
isset( $request_params['jetpack'] )
&& 'comms' === $request_params['jetpack']
) {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This just determines whether to handle the request as an XML-RPC request. The actual XML-RPC endpoints do the appropriate nonce checking where applicable. Plus we make sure to clear all cookies via require_jetpack_authentication called later in method.
if ( isset( $_GET['jetpack'] ) && 'comms' === $_GET['jetpack'] ) {
if ( ! Constants::is_defined( 'XMLRPC_REQUEST' ) ) {
// Use the real constant here for WordPress' sake.
define( 'XMLRPC_REQUEST', true );
@@ -183,13 +199,16 @@ class Manager {
if ( ! Constants::get_constant( 'XMLRPC_REQUEST' ) ) {
return false;
}
// Display errors can cause the XML to be not well formed.
// This only affects Jetpack XML-RPC endpoints received from WordPress.com servers.
// All other XML-RPC requests are unaffected.
@ini_set( 'display_errors', false ); // phpcs:ignore
if ( $xmlrpc_server ) {
$this->xmlrpc_server = $xmlrpc_server;
} else {
$this->xmlrpc_server = new \Jetpack_XMLRPC_Server();
$this->xmlrpc_server = new Jetpack_XMLRPC_Server();
}
$this->require_jetpack_authentication();
@@ -235,6 +254,8 @@ class Manager {
* from /xmlrpc.php so that we're replicating it as closely as possible.
*
* @todo Tighten $wp_xmlrpc_server_class a bit to make sure it doesn't do bad things.
*
* @return never
*/
public function alternate_xmlrpc() {
// Some browser-embedded clients send cookies. We don't want them.
@@ -274,7 +295,7 @@ class Manager {
$jetpack_methods = array();
foreach ( $methods as $method => $callback ) {
if ( 0 === strpos( $method, 'jetpack.' ) ) {
if ( str_starts_with( $method, 'jetpack.' ) ) {
$jetpack_methods[ $method ] = $callback;
}
}
@@ -436,12 +457,12 @@ class Manager {
}
$jetpack_signature = new \Jetpack_Signature( $token->secret, (int) \Jetpack_Options::get_option( 'time_diff' ) );
// phpcs:disable WordPress.Security.NonceVerification.Missing
// phpcs:disable WordPress.Security.NonceVerification.Missing -- Used to verify a cryptographic signature of the post data. Also a nonce is verified later in the function.
if ( isset( $_POST['_jetpack_is_multipart'] ) ) {
$post_data = $_POST;
$post_data = $_POST; // We need all of $_POST in order to verify a cryptographic signature of the post data.
$file_hashes = array();
foreach ( $post_data as $post_data_key => $post_data_value ) {
if ( 0 !== strpos( $post_data_key, '_jetpack_file_hmac_' ) ) {
if ( ! str_starts_with( $post_data_key, '_jetpack_file_hmac_' ) ) {
continue;
}
$post_data_key = substr( $post_data_key, strlen( '_jetpack_file_hmac_' ) );
@@ -857,6 +878,22 @@ class Manager {
exit();
}
/**
* Force user disconnect.
*
* @param int $user_id Local (external) user ID.
*
* @return bool
*/
public function disconnect_user_force( $user_id ) {
if ( ! (int) $user_id ) {
// Missing user ID.
return false;
}
return $this->disconnect_user( $user_id, true, true );
}
/**
* Unlinks the current user from the linked WordPress.com user.
*
@@ -878,6 +915,11 @@ class Manager {
return false;
}
if ( in_array( $user_id, self::$disconnected_users, true ) ) {
// The user is already disconnected.
return false;
}
// Attempt to disconnect the user from WordPress.com.
$is_disconnected_from_wpcom = $this->unlink_user_from_wpcom( $user_id );
@@ -907,6 +949,8 @@ class Manager {
}
}
self::$disconnected_users[] = $user_id;
return $is_disconnected_from_wpcom && $is_disconnected_locally;
}
@@ -1188,7 +1232,7 @@ class Manager {
$jetpack_public = false;
}
\Jetpack_Options::update_options(
Jetpack_Options::update_options(
array(
'id' => (int) $registration_details->jetpack_id,
'public' => $jetpack_public,
@@ -1199,6 +1243,13 @@ class Manager {
$this->get_tokens()->update_blog_token( (string) $registration_details->jetpack_secret );
if ( ! Jetpack_Options::get_option( 'id' ) || ! $this->get_tokens()->get_access_token() ) {
return new WP_Error(
'connection_data_save_failed',
'Failed to save connection data in the database'
);
}
$alternate_authorization_url = isset( $registration_details->alternate_authorization_url ) ? $registration_details->alternate_authorization_url : '';
add_filter(
@@ -1829,11 +1880,16 @@ class Manager {
/**
* Builds a URL to the Jetpack connection auth page.
*
* @param WP_User $user (optional) defaults to the current logged in user.
* @param String $redirect (optional) a redirect URL to use instead of the default.
* @since 2.7.6 Added optional $from and $raw parameters.
*
* @param WP_User|null $user (optional) defaults to the current logged in user.
* @param string|null $redirect (optional) a redirect URL to use instead of the default.
* @param bool|string $from If not false, adds 'from=$from' param to the connect URL.
* @param bool $raw If true, URL will not be escaped.
*
* @return string Connect URL.
*/
public function get_authorization_url( $user = null, $redirect = null ) {
public function get_authorization_url( $user = null, $redirect = null, $from = false, $raw = false ) {
if ( empty( $user ) ) {
$user = wp_get_current_user();
}
@@ -1924,7 +1980,30 @@ class Manager {
$api_url = $this->api_url( 'authorize' );
return add_query_arg( $body, $api_url );
$url = add_query_arg( $body, $api_url );
if ( is_network_admin() ) {
$url = add_query_arg( 'is_multisite', network_admin_url( 'admin.php?page=jetpack-settings' ), $url );
}
if ( $from ) {
$url = add_query_arg( 'from', $from, $url );
}
if ( $raw ) {
$url = esc_url_raw( $url );
}
/**
* Filter the URL used when connecting a user to a WordPress.com account.
*
* @since 2.0.0
* @since 2.7.6 Added $raw parameter.
*
* @param string $url Connection URL.
* @param bool $raw If true, URL will not be escaped.
*/
return apply_filters( 'jetpack_build_authorize_url', $url, $raw );
}
/**
@@ -2047,7 +2126,7 @@ class Manager {
( new Nonce_Handler() )->clean_all();
/**
* Fires when a site is disconnected.
* Fires before a site is disconnected.
*
* @since 1.36.3
*/
@@ -2271,7 +2350,7 @@ class Manager {
* Handles a getOptions XMLRPC method call.
*
* @param array $args method call arguments.
* @return an amended XMLRPC server options array.
* @return array|IXR_Error An amended XMLRPC server options array.
*/
public function jetpack_get_options( $args ) {
global $wp_xmlrpc_server;
@@ -2545,17 +2624,21 @@ class Manager {
/**
* Get the WPCOM or self-hosted site ID.
*
* @return int|WP_Error
* @param bool $quiet Return null instead of an error.
*
* @return int|WP_Error|null
*/
public static function get_site_id() {
public static function get_site_id( $quiet = false ) {
$is_wpcom = ( defined( 'IS_WPCOM' ) && IS_WPCOM );
$site_id = $is_wpcom ? get_current_blog_id() : \Jetpack_Options::get_option( 'id' );
if ( ! $site_id ) {
return new \WP_Error(
'unavailable_site_id',
__( 'Sorry, something is wrong with your Jetpack connection.', 'jetpack-connection' ),
403
);
return $quiet
? null
: new \WP_Error(
'unavailable_site_id',
__( 'Sorry, something is wrong with your Jetpack connection.', 'jetpack-connection' ),
403
);
}
return (int) $site_id;
}

View File

@@ -30,11 +30,31 @@ class Package_Version_Tracker {
*/
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;
/**
* 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;
}
// The version check is being rate limited.
if ( $this->is_rate_limiting() ) {
return;
}
/**
* Obtains the package versions.
*
@@ -65,13 +85,43 @@ class Package_Version_Tracker {
}
/**
* Updates the package 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 );
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_option( $package_versions ) {
protected function update_package_versions_via_remote_request( $package_versions ) {
$connection = new Manager();
if ( ! $connection->is_connected() ) {
return;
@@ -108,4 +158,19 @@ class Package_Version_Tracker {
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;
}
}

View File

@@ -12,7 +12,7 @@ namespace Automattic\Jetpack\Connection;
*/
class Package_Version {
const PACKAGE_VERSION = '1.60.1';
const PACKAGE_VERSION = '2.11.3';
const PACKAGE_SLUG = 'connection';

View File

@@ -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;
}
/**
* 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' => isset( $supported_partners[ $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 );
}
}

View File

@@ -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;
}
}

View File

@@ -24,6 +24,11 @@ class Plugin_Storage {
*/
const PLUGINS_DISABLED_OPTION_NAME = 'jetpack_connection_disabled_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.
*
@@ -31,13 +36,6 @@ class Plugin_Storage {
*/
private static $configured = false;
/**
* Refresh list of connected plugins upon intialization.
*
* @var boolean
*/
private static $refresh_connected_plugins = false;
/**
* Connected plugins.
*
@@ -65,11 +63,6 @@ class Plugin_Storage {
public static function upsert( $slug, array $args = array() ) {
self::$plugins[ $slug ] = $args;
// if plugin is not in the list of active plugins, refresh the list.
if ( ! array_key_exists( $slug, (array) get_option( self::ACTIVE_PLUGINS_OPTION_NAME, array() ) ) ) {
self::$refresh_connected_plugins = true;
}
return true;
}
@@ -167,19 +160,58 @@ class Plugin_Storage {
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().
$number_of_plugins_differ = count( self::$plugins ) !== count( (array) get_option( self::ACTIVE_PLUGINS_OPTION_NAME, array() ) );
$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 ( $number_of_plugins_differ || true === self::$refresh_connected_plugins ) {
if ( $configured_plugin_keys !== $stored_plugin_keys ) {
self::update_active_plugins_option();
}
self::$configured = true;
}
/**
@@ -188,7 +220,7 @@ class Plugin_Storage {
* @return void
*/
public static function update_active_plugins_option() {
// Note: Since this options is synced to wpcom, if you change its structure, you have to update the sanitizer at wpcom side.
// 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() ) {

View File

@@ -7,6 +7,8 @@
namespace Automattic\Jetpack\Connection;
use WP_Error;
/**
* The Jetpack Connection Rest Authentication class.
*/
@@ -118,7 +120,7 @@ class Rest_Authentication {
}
if ( ! isset( $_SERVER['REQUEST_METHOD'] ) ) {
$this->rest_authentication_status = new \WP_Error(
$this->rest_authentication_status = new WP_Error(
'rest_invalid_request',
__( 'The request method is missing.', 'jetpack-connection' ),
array( 'status' => 400 )
@@ -131,7 +133,7 @@ class Rest_Authentication {
// 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(
$this->rest_authentication_status = new WP_Error(
'rest_invalid_request',
__( 'This request method is not supported.', 'jetpack-connection' ),
array( 'status' => 400 )
@@ -139,7 +141,7 @@ class Rest_Authentication {
return null;
}
if ( 'POST' !== $_SERVER['REQUEST_METHOD'] && ! empty( file_get_contents( 'php://input' ) ) ) {
$this->rest_authentication_status = new \WP_Error(
$this->rest_authentication_status = new WP_Error(
'rest_invalid_request',
__( 'This request method does not support body parameters.', 'jetpack-connection' ),
array( 'status' => 400 )
@@ -173,7 +175,7 @@ class Rest_Authentication {
}
// Something else went wrong. Probably a signature error.
$this->rest_authentication_status = new \WP_Error(
$this->rest_authentication_status = new WP_Error(
'rest_invalid_signature',
__( 'The request is not signed correctly.', 'jetpack-connection' ),
array( 'status' => 400 )

View File

@@ -7,6 +7,7 @@
namespace Automattic\Jetpack\Connection;
use Automattic\Jetpack\Connection\Webhooks\Authorize_Redirect;
use Automattic\Jetpack\Constants;
use Automattic\Jetpack\Redirect;
use Automattic\Jetpack\Status;
@@ -84,6 +85,49 @@ class REST_Connector {
)
);
// Authorize a remote user.
register_rest_route(
'jetpack/v4',
'/remote_provision',
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'remote_provision' ),
'permission_callback' => array( $this, 'remote_provision_permission_check' ),
)
);
register_rest_route(
'jetpack/v4',
'/remote_register',
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'remote_register' ),
'permission_callback' => array( $this, 'remote_register_permission_check' ),
)
);
// Connect a remote user.
register_rest_route(
'jetpack/v4',
'/remote_connect',
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'remote_connect' ),
'permission_callback' => array( $this, 'remote_connect_permission_check' ),
)
);
// The endpoint verifies blog connection and blog token validity.
register_rest_route(
'jetpack/v4',
'/connection/check',
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'connection_check' ),
'permission_callback' => array( $this, 'connection_check_permission_check' ),
)
);
// Get current connection status of Jetpack.
register_rest_route(
'jetpack/v4',
@@ -274,7 +318,7 @@ class REST_Connector {
*
* @param WP_REST_Request $request The request sent to the WP REST API.
*
* @return array|wp-error
* @return array|WP_Error
*/
public static function remote_authorize( $request ) {
$xmlrpc_server = new Jetpack_XMLRPC_Server();
@@ -287,6 +331,103 @@ class REST_Connector {
return $result;
}
/**
* Initiate the site provisioning process.
*
* @since 2.5.0
*
* @param WP_REST_Request $request The request sent to the WP REST API.
*
* @return WP_Error|array
*/
public static function remote_provision( WP_REST_Request $request ) {
$xmlrpc_server = new Jetpack_XMLRPC_Server();
$result = $xmlrpc_server->remote_provision( $request );
if ( is_a( $result, 'IXR_Error' ) ) {
$result = new WP_Error( $result->code, $result->message );
}
return $result;
}
/**
* Connect a remote user.
*
* @since 2.6.0
*
* @param WP_REST_Request $request The request sent to the WP REST API.
*
* @return WP_Error|array
*/
public static function remote_connect( WP_REST_Request $request ) {
$xmlrpc_server = new Jetpack_XMLRPC_Server();
$result = $xmlrpc_server->remote_connect( $request );
if ( is_a( $result, 'IXR_Error' ) ) {
$result = new WP_Error( $result->code, $result->message );
}
return $result;
}
/**
* Register the site so that a plan can be provisioned.
*
* @since 2.5.0
*
* @param WP_REST_Request $request The request object.
*
* @return WP_Error|array
*/
public function remote_register( WP_REST_Request $request ) {
$xmlrpc_server = new Jetpack_XMLRPC_Server();
$result = $xmlrpc_server->remote_register( $request );
if ( is_a( $result, 'IXR_Error' ) ) {
$result = new WP_Error( $result->code, $result->message );
}
return $result;
}
/**
* Remote provision endpoint permission check.
*
* @return true|WP_Error
*/
public function remote_provision_permission_check() {
return Rest_Authentication::is_signed_with_blog_token()
? true
: new WP_Error( 'invalid_permission_remote_provision', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) );
}
/**
* Remote connect endpoint permission check.
*
* @return true|WP_Error
*/
public function remote_connect_permission_check() {
return Rest_Authentication::is_signed_with_blog_token()
? true
: new WP_Error( 'invalid_permission_remote_connect', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) );
}
/**
* Remote register endpoint permission check.
*
* @return true|WP_Error
*/
public function remote_register_permission_check() {
if ( $this->connection->has_connected_owner() ) {
return Rest_Authentication::is_signed_with_blog_token()
? true
: new WP_Error( 'already_registered', __( 'Blog is already registered', 'jetpack-connection' ), 400 );
}
return true;
}
/**
* Get connection status for this Jetpack site.
*
@@ -303,7 +444,7 @@ class REST_Connector {
$connection_status = array(
'isActive' => $connection->has_connected_owner(), // TODO deprecate this.
'isStaging' => $status->is_staging_site(),
'isStaging' => $status->in_safe_mode(), // TODO deprecate this.
'isRegistered' => $connection->is_connected(),
'isUserConnected' => $connection->is_user_connected(),
'hasConnectedOwner' => $connection->has_connected_owner(),
@@ -674,11 +815,7 @@ class REST_Connector {
$redirect_uri = $request->get_param( 'redirect_uri' ) ? admin_url( $request->get_param( 'redirect_uri' ) ) : null;
if ( class_exists( 'Jetpack' ) ) {
$authorize_url = \Jetpack::build_authorize_url( $redirect_uri );
} else {
$authorize_url = $this->connection->get_authorization_url( null, $redirect_uri );
}
$authorize_url = ( new Authorize_Redirect( $this->connection ) )->build_authorize_url( $redirect_uri );
/**
* Filters the response of jetpack/v4/connection/register endpoint
@@ -798,7 +935,7 @@ class REST_Connector {
*
* @since 1.29.0
*
* @return bool|WP_Error.
* @return bool|WP_Error
*/
public static function update_user_token_permission_check() {
return Rest_Authentication::is_signed_with_blog_token()
@@ -847,4 +984,41 @@ class REST_Connector {
return new WP_Error( 'invalid_user_permission_set_connection_owner', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) );
}
/**
* The endpoint verifies blog connection and blog token validity.
*
* @since 2.7.0
*
* @return mixed|null
*/
public function connection_check() {
/**
* Filters the successful response of the REST API test_connection method
*
* @param string $response The response string.
*/
$status = apply_filters( 'jetpack_rest_connection_check_response', 'success' );
return rest_ensure_response(
array(
'status' => $status,
)
);
}
/**
* Remote connect endpoint permission check.
*
* @return true|WP_Error
*/
public function connection_check_permission_check() {
if ( current_user_can( 'jetpack_connect' ) ) {
return true;
}
return Rest_Authentication::is_signed_with_blog_token()
? true
: new WP_Error( 'invalid_permission_connection_check', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) );
}
}

View File

@@ -158,6 +158,7 @@ class Secrets {
*/
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.

View File

@@ -223,7 +223,7 @@ class Server_Sandbox {
*
* Attached to the `admin_bar_menu` action.
*
* @param WP_Admin_Bar $wp_admin_bar The WP_Admin_Bar instance.
* @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' ) ) {

View File

@@ -38,8 +38,8 @@ class Tracking {
/**
* 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.
* @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;
@@ -159,7 +159,7 @@ class Tracking {
*
* @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 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_'.
*/
@@ -189,7 +189,7 @@ class Tracking {
/**
* 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 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.
@@ -221,8 +221,8 @@ class Tracking {
/**
* 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.
* @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.
*/
@@ -238,10 +238,10 @@ class Tracking {
* 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.
* @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
*/
@@ -252,6 +252,7 @@ class Tracking {
$blog_details = array(
'blog_lang' => isset( $properties['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 );

View File

@@ -82,8 +82,8 @@ class Urls {
/**
* Return URL with a normalized protocol.
*
* @param callable $callable Function to retrieve URL option.
* @param string $new_value URL Protocol to set URLs to.
* @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 ) {

View File

@@ -83,4 +83,53 @@ class Utils {
)
);
}
/**
* 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 );
}
}

View File

@@ -86,11 +86,11 @@ class Webhooks {
case 'authorize':
$this->handle_authorize();
$this->do_exit();
break;
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;
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.
}
}
@@ -107,7 +107,7 @@ class Webhooks {
}
do_action( 'jetpack_client_authorize_processing' );
$data = stripslashes_deep( $_GET );
$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();
@@ -159,6 +159,8 @@ class Webhooks {
/**
* The `exit` is wrapped into a method so we could mock it.
*
* @return never
*/
protected function do_exit() {
exit;

View File

@@ -60,6 +60,7 @@ class XMLRPC_Async_Call {
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 );
}

View File

@@ -7,6 +7,8 @@
namespace Automattic\Jetpack\Connection;
use IXR_Error;
/**
* Registers the XML-RPC methods for Connections.
*/
@@ -69,10 +71,10 @@ class XMLRPC_Connector {
$code = -10520;
}
if ( ! class_exists( \IXR_Error::class ) ) {
if ( ! class_exists( IXR_Error::class ) ) {
require_once ABSPATH . WPINC . '/class-IXR.php';
}
return new \IXR_Error(
return new IXR_Error(
$code,
sprintf( 'Jetpack: [%s] %s', $data->get_error_code(), $data->get_error_message() )
);

View File

@@ -0,0 +1,30 @@
#wpadminbar #wp-admin-bar-jetpack-idc {
margin-right: 5px;
.jp-idc-admin-bar {
border-radius: 2px;
font-weight: 500;
font-size: 14px;
line-height: 20px;
color: #EFEFF0;
padding: 6px 8px;
}
&.hide {
display: none;
}
.dashicons {
font-family: 'dashicons';
margin-top: -6px;
&:before {
font-size: 18px;
}
}
.ab-item {
padding: 0;
background: #E68B28;
}
}

View File

@@ -0,0 +1,62 @@
import { IDCScreen } from '@automattic/jetpack-idc';
import * as WPElement from '@wordpress/element';
import React from 'react';
import './admin-bar.scss';
import './style.scss';
/**
* The initial renderer function.
*/
function render() {
if ( ! window.hasOwnProperty( 'JP_IDENTITY_CRISIS__INITIAL_STATE' ) ) {
return;
}
const container = document.getElementById(
window.JP_IDENTITY_CRISIS__INITIAL_STATE.containerID || 'jp-identity-crisis-container'
);
if ( null === container ) {
return;
}
const {
WP_API_root,
WP_API_nonce,
wpcomHomeUrl,
currentUrl,
redirectUri,
tracksUserData,
tracksEventData,
isSafeModeConfirmed,
consumerData,
isAdmin,
possibleDynamicSiteUrlDetected,
isDevelopmentSite,
} = window.JP_IDENTITY_CRISIS__INITIAL_STATE;
if ( ! isSafeModeConfirmed ) {
const component = (
<IDCScreen
wpcomHomeUrl={ wpcomHomeUrl }
currentUrl={ currentUrl }
apiRoot={ WP_API_root }
apiNonce={ WP_API_nonce }
redirectUri={ redirectUri }
tracksUserData={ tracksUserData || {} }
tracksEventData={ tracksEventData }
customContent={
consumerData.hasOwnProperty( 'customContent' ) ? consumerData.customContent : {}
}
isAdmin={ isAdmin }
logo={ consumerData.hasOwnProperty( 'logo' ) ? consumerData.logo : undefined }
possibleDynamicSiteUrlDetected={ possibleDynamicSiteUrlDetected }
isDevelopmentSite={ isDevelopmentSite }
/>
);
WPElement.createRoot( container ).render( component );
}
}
window.addEventListener( 'load', () => render() );

View File

@@ -0,0 +1,9 @@
#jp-identity-crisis-container .jp-idc__idc-screen {
margin-top: 40px;
margin-bottom: 40px;
}
#jp-identity-crisis-container.notice {
background: none;
border: none;
}

View File

@@ -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 {}

View File

@@ -0,0 +1,833 @@
<?php
/**
* Identity_Crisis class of the Connection package.
*
* @package automattic/jetpack-connection
*/
namespace Automattic\Jetpack;
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
use Automattic\Jetpack\Connection\Urls;
use Automattic\Jetpack\IdentityCrisis\Exception;
use Automattic\Jetpack\IdentityCrisis\UI;
use Automattic\Jetpack\IdentityCrisis\URL_Secret;
use Jetpack_Options;
use WP_Error;
/**
* This class will handle everything involved with fixing an Identity Crisis.
*
* @since automattic/jetpack-identity-crisis:0.2.0
* @since-jetpack 4.4.0
* @since 2.9.0
*/
class Identity_Crisis {
/**
* Persistent WPCOM blog ID that stays in the options after disconnect.
*/
const PERSISTENT_BLOG_ID_OPTION_NAME = 'jetpack_persistent_blog_id';
/**
* Instance of the object.
*
* @var Identity_Crisis
**/
private static $instance = null;
/**
* The wpcom value of the home URL.
*
* @var string
*/
public static $wpcom_home_url;
/**
* Has safe mode been confirmed?
* Beware, it never contains `true` for non-admins, so doesn't always reflect the actual value.
*
* @var bool
*/
public static $is_safe_mode_confirmed;
/**
* The current screen, which is set if the current user is a non-admin and this is an admin page.
*
* @var \WP_Screen
*/
public static $current_screen;
/**
* Initializer.
*
* @return object
*/
public static function init() {
if ( self::$instance === null ) {
self::$instance = new Identity_Crisis();
}
return self::$instance;
}
/**
* Class constructor.
*
* @return void
*/
private function __construct() {
add_action( 'jetpack_sync_processed_actions', array( $this, 'maybe_clear_migrate_option' ) );
add_action( 'rest_api_init', array( 'Automattic\\Jetpack\\IdentityCrisis\\REST_Endpoints', 'initialize_rest_api' ) );
add_action( 'jetpack_idc_disconnect', array( __CLASS__, 'do_jetpack_idc_disconnect' ) );
add_action( 'jetpack_received_remote_request_response', array( $this, 'check_http_response_for_idc_detected' ) );
add_filter( 'jetpack_connection_disconnect_site_wpcom', array( __CLASS__, 'jetpack_connection_disconnect_site_wpcom_filter' ) );
add_filter( 'jetpack_remote_request_url', array( $this, 'add_idc_query_args_to_url' ) );
add_filter( 'jetpack_connection_validate_urls_for_idc_mitigation_response', array( static::class, 'add_secret_to_url_validation_response' ) );
add_filter( 'jetpack_connection_validate_urls_for_idc_mitigation_response', array( static::class, 'add_ip_requester_to_url_validation_response' ) );
add_filter( 'jetpack_options', array( static::class, 'reverse_wpcom_urls_for_idc' ) );
add_filter( 'jetpack_register_request_body', array( static::class, 'register_request_body' ) );
add_action( 'jetpack_site_registered', array( static::class, 'site_registered' ) );
$urls_in_crisis = self::check_identity_crisis();
if ( false === $urls_in_crisis ) {
return;
}
self::$wpcom_home_url = $urls_in_crisis['wpcom_home'];
add_action( 'init', array( $this, 'wordpress_init' ) );
}
/**
* Disconnect current connection and clear IDC options.
*/
public static function do_jetpack_idc_disconnect() {
$connection = new Connection_Manager();
// If the site is in an IDC because sync is not allowed,
// let's make sure to not disconnect the production site.
if ( ! self::validate_sync_error_idc_option() ) {
$connection->disconnect_site( true );
} else {
$connection->disconnect_site( false );
}
delete_option( static::PERSISTENT_BLOG_ID_OPTION_NAME );
// Clear IDC options.
self::clear_all_idc_options();
}
/**
* Filter to prevent site from disconnecting from WPCOM if it's in an IDC.
*
* @see jetpack_connection_disconnect_site_wpcom filter.
*
* @return bool False if the site is in IDC, true otherwise.
*/
public static function jetpack_connection_disconnect_site_wpcom_filter() {
return ! self::validate_sync_error_idc_option();
}
/**
* This method loops through the array of processed items from sync and checks if one of the items was the
* home_url or site_url callable. If so, then we delete the jetpack_migrate_for_idc option.
*
* @param array $processed_items Array of processed items that were synced to WordPress.com.
*/
public function maybe_clear_migrate_option( $processed_items ) {
foreach ( (array) $processed_items as $item ) {
// First, is this item a jetpack_sync_callable action? If so, then proceed.
$callable_args = ( is_array( $item ) && isset( $item[0] ) && isset( $item[1] ) && 'jetpack_sync_callable' === $item[0] )
? $item[1]
: null;
// Second, if $callable_args is set, check if the callable was home_url or site_url. If so,
// clear the migrate option.
if (
isset( $callable_args[0] )
&& ( 'home_url' === $callable_args[0] || 'site_url' === $callable_args[1] )
) {
Jetpack_Options::delete_option( 'migrate_for_idc' );
break;
}
}
}
/**
* WordPress init.
*
* @return void
*/
public function wordpress_init() {
if ( current_user_can( 'jetpack_disconnect' ) ) {
if (
isset( $_GET['jetpack_idc_clear_confirmation'] ) && isset( $_GET['_wpnonce'] ) &&
wp_verify_nonce( $_GET['_wpnonce'], 'jetpack_idc_clear_confirmation' ) // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- WordPress core doesn't unslash or verify nonces either.
) {
Jetpack_Options::delete_option( 'safe_mode_confirmed' );
self::$is_safe_mode_confirmed = false;
} else {
self::$is_safe_mode_confirmed = (bool) Jetpack_Options::get_option( 'safe_mode_confirmed' );
}
}
// 121 Priority so that it's the most inner Jetpack item in the admin bar.
add_action( 'admin_bar_menu', array( $this, 'display_admin_bar_button' ), 121 );
UI::init();
}
/**
* Add the idc query arguments to the url.
*
* @param string $url The remote request url.
*/
public function add_idc_query_args_to_url( $url ) {
$status = new Status();
if ( ! is_string( $url )
|| $status->is_offline_mode()
|| self::validate_sync_error_idc_option() ) {
return $url;
}
$home_url = Urls::home_url();
$site_url = Urls::site_url();
$hostname = wp_parse_url( $site_url, PHP_URL_HOST );
// If request is from an IP, make sure ip_requester option is set
if ( self::url_is_ip( $hostname ) ) {
self::maybe_update_ip_requester( $hostname );
}
$query_args = array(
'home' => $home_url,
'siteurl' => $site_url,
);
if ( self::should_handle_idc() ) {
$query_args['idc'] = true;
}
if ( \Jetpack_Options::get_option( 'migrate_for_idc', false ) ) {
$query_args['migrate_for_idc'] = true;
}
if ( is_multisite() ) {
$query_args['multisite'] = true;
}
return add_query_arg( $query_args, $url );
}
/**
* Renders the admin bar button.
*
* @return void
*/
public function display_admin_bar_button() {
global $wp_admin_bar;
$href = is_admin()
? add_query_arg( 'jetpack_idc_clear_confirmation', '1' )
: add_query_arg( 'jetpack_idc_clear_confirmation', '1', admin_url() );
$href = wp_nonce_url( $href, 'jetpack_idc_clear_confirmation' );
$consumer_data = UI::get_consumer_data();
$label = isset( $consumer_data['customContent']['adminBarSafeModeLabel'] )
? esc_html( $consumer_data['customContent']['adminBarSafeModeLabel'] )
: esc_html__( 'Jetpack Safe Mode', 'jetpack-connection' );
$title = sprintf(
'<span class="jp-idc-admin-bar">%s %s</span>',
'<span class="dashicons dashicons-info-outline"></span>',
$label
);
$menu = array(
'id' => 'jetpack-idc',
'title' => $title,
'href' => esc_url( $href ),
'parent' => 'top-secondary',
);
if ( ! self::$is_safe_mode_confirmed ) {
$menu['meta'] = array(
'class' => 'hide',
);
}
$wp_admin_bar->add_node( $menu );
}
/**
* Checks if the site is currently in an identity crisis.
*
* @return array|bool Array of options that are in a crisis, or false if everything is OK.
*/
public static function check_identity_crisis() {
$connection = new Connection_Manager( 'jetpack' );
if ( ! $connection->is_connected() || ( new Status() )->is_offline_mode() || ! self::validate_sync_error_idc_option() ) {
return false;
}
return Jetpack_Options::get_option( 'sync_error_idc' );
}
/**
* Checks the HTTP response body for the 'idc_detected' key. If the key exists,
* checks the idc_detected value for a valid idc error.
*
* @param array|WP_Error $http_response The HTTP response.
*
* @return bool Whether the site is in an identity crisis.
*/
public function check_http_response_for_idc_detected( $http_response ) {
if ( ! is_array( $http_response ) ) {
return false;
}
$response_body = json_decode( wp_remote_retrieve_body( $http_response ), true );
if ( isset( $response_body['idc_detected'] ) ) {
return $this->check_response_for_idc( $response_body['idc_detected'] );
}
if ( isset( $response_body['migrated_for_idc'] ) ) {
Jetpack_Options::delete_option( 'migrate_for_idc' );
}
return false;
}
/**
* Checks the WPCOM response to determine if the site is in an identity crisis. Updates the
* sync_error_idc option if it is.
*
* @param array $response The response data.
*
* @return bool Whether the site is in an identity crisis.
*/
public function check_response_for_idc( $response ) {
if ( is_array( $response ) && isset( $response['error_code'] ) ) {
$error_code = $response['error_code'];
$allowed_idc_error_codes = array(
'jetpack_url_mismatch',
'jetpack_home_url_mismatch',
'jetpack_site_url_mismatch',
);
if ( in_array( $error_code, $allowed_idc_error_codes, true ) ) {
Jetpack_Options::update_option(
'sync_error_idc',
self::get_sync_error_idc_option( $response )
);
}
return true;
}
return false;
}
/**
* Clears all IDC specific options. This method is used on disconnect and reconnect.
*
* @return void
*/
public static function clear_all_idc_options() {
// If the site is currently in IDC, let's also clear the VaultPress connection options.
// We have to check if the site is in IDC, otherwise we'd be clearing the VaultPress
// connection any time the Jetpack connection is cycled.
if ( self::validate_sync_error_idc_option() ) {
delete_option( 'vaultpress' );
delete_option( 'vaultpress_auto_register' );
}
Jetpack_Options::delete_option(
array(
'sync_error_idc',
'safe_mode_confirmed',
'migrate_for_idc',
)
);
delete_transient( 'jetpack_idc_possible_dynamic_site_url_detected' );
}
/**
* Checks whether the sync_error_idc option is valid or not, and if not, will do cleanup.
*
* @return bool
* @since-jetpack 5.4.0 Do not call get_sync_error_idc_option() unless site is in IDC
*
* @since 0.2.0
* @since-jetpack 4.4.0
*/
public static function validate_sync_error_idc_option() {
$is_valid = false;
// Is the site opted in and does the stored sync_error_idc option match what we now generate?
$sync_error = Jetpack_Options::get_option( 'sync_error_idc' );
if ( $sync_error && self::should_handle_idc() ) {
$local_options = self::get_sync_error_idc_option();
// Ensure all values are set.
if ( isset( $sync_error['home'] ) && isset( $local_options['home'] ) && isset( $sync_error['siteurl'] ) && isset( $local_options['siteurl'] ) ) {
// If the WP.com expected home and siteurl match local home and siteurl it is not valid IDC.
if (
isset( $sync_error['wpcom_home'] ) &&
isset( $sync_error['wpcom_siteurl'] ) &&
$sync_error['wpcom_home'] === $local_options['home'] &&
$sync_error['wpcom_siteurl'] === $local_options['siteurl']
) {
// Enable migrate_for_idc so that sync actions are accepted.
Jetpack_Options::update_option( 'migrate_for_idc', true );
} elseif ( $sync_error['home'] === $local_options['home'] && $sync_error['siteurl'] === $local_options['siteurl'] ) {
$is_valid = true;
}
}
}
/**
* Filters whether the sync_error_idc option is valid.
*
* @param bool $is_valid If the sync_error_idc is valid or not.
*
* @since 0.2.0
* @since-jetpack 4.4.0
*/
$is_valid = (bool) apply_filters( 'jetpack_sync_error_idc_validation', $is_valid );
if ( ! $is_valid && $sync_error ) {
// Since the option exists, and did not validate, delete it.
Jetpack_Options::delete_option( 'sync_error_idc' );
}
return $is_valid;
}
/**
* Reverses WP.com URLs stored in sync_error_idc option.
*
* @param array $sync_error error option containing reversed URLs.
* @return array
*/
public static function reverse_wpcom_urls_for_idc( $sync_error ) {
if ( isset( $sync_error['reversed_url'] ) ) {
if ( array_key_exists( 'wpcom_siteurl', $sync_error ) ) {
$sync_error['wpcom_siteurl'] = strrev( $sync_error['wpcom_siteurl'] );
}
if ( array_key_exists( 'wpcom_home', $sync_error ) ) {
$sync_error['wpcom_home'] = strrev( $sync_error['wpcom_home'] );
}
}
return $sync_error;
}
/**
* Normalizes a url by doing three things:
* - Strips protocol
* - Strips www
* - Adds a trailing slash
*
* @param string $url URL to parse.
*
* @return WP_Error|string
* @since 0.2.0
* @since-jetpack 4.4.0
*/
public static function normalize_url_protocol_agnostic( $url ) {
$parsed_url = wp_parse_url( trailingslashit( esc_url_raw( $url ) ) );
if ( ! $parsed_url || empty( $parsed_url['host'] ) || empty( $parsed_url['path'] ) ) {
return new WP_Error(
'cannot_parse_url',
sprintf(
/* translators: %s: URL to parse. */
esc_html__( 'Cannot parse URL %s', 'jetpack-connection' ),
$url
)
);
}
// Strip www and protocols.
$url = preg_replace( '/^www\./i', '', $parsed_url['host'] . $parsed_url['path'] );
return $url;
}
/**
* Gets the value that is to be saved in the jetpack_sync_error_idc option.
*
* @param array $response HTTP response.
*
* @return array Array of the local urls, wpcom urls, and error code.
* @since 0.2.0
* @since-jetpack 4.4.0
* @since-jetpack 5.4.0 Add transient since home/siteurl retrieved directly from DB.
*/
public static function get_sync_error_idc_option( $response = array() ) {
// Since the local options will hit the database directly, store the values
// in a transient to allow for autoloading and caching on subsequent views.
$local_options = get_transient( 'jetpack_idc_local' );
if ( false === $local_options ) {
$local_options = array(
'home' => Urls::home_url(),
'siteurl' => Urls::site_url(),
);
set_transient( 'jetpack_idc_local', $local_options, MINUTE_IN_SECONDS );
}
$options = array_merge( $local_options, $response );
$returned_values = array();
foreach ( $options as $key => $option ) {
if ( 'error_code' === $key ) {
$returned_values[ $key ] = $option;
continue;
}
$normalized_url = self::normalize_url_protocol_agnostic( $option );
if ( is_wp_error( $normalized_url ) ) {
continue;
}
$returned_values[ $key ] = $normalized_url;
}
// We need to protect WPCOM URLs from search & replace by reversing them. See https://wp.me/pf5801-3R
// Add 'reversed_url' key for backward compatibility
if ( array_key_exists( 'wpcom_home', $returned_values ) && array_key_exists( 'wpcom_siteurl', $returned_values ) ) {
$returned_values['reversed_url'] = true;
$returned_values = self::reverse_wpcom_urls_for_idc( $returned_values );
}
return $returned_values;
}
/**
* Returns the value of the jetpack_should_handle_idc filter or constant.
* If set to true, the site will be put into staging mode.
*
* This method uses both the current jetpack_should_handle_idc filter
* and constant to determine whether an IDC should be handled.
*
* @return bool
* @since 0.2.6
*/
public static function should_handle_idc() {
if ( Constants::is_defined( 'JETPACK_SHOULD_HANDLE_IDC' ) ) {
$default = Constants::get_constant( 'JETPACK_SHOULD_HANDLE_IDC' );
} else {
$default = ! Constants::is_defined( 'SUNRISE' ) && ! is_multisite();
}
/**
* Allows sites to opt in for IDC mitigation which blocks the site from syncing to WordPress.com when the home
* URL or site URL do not match what WordPress.com expects. The default value is either true, or the value of
* JETPACK_SHOULD_HANDLE_IDC constant if set.
*
* @param bool $default Whether the site is opted in to IDC mitigation.
*
* @since 0.2.6
*/
return (bool) apply_filters( 'jetpack_should_handle_idc', $default );
}
/**
* Whether the site is undergoing identity crisis.
*
* @return bool
*/
public static function has_identity_crisis() {
return false !== static::check_identity_crisis() && ! static::$is_safe_mode_confirmed;
}
/**
* Whether an admin has confirmed safe mode.
* Unlike `static::$is_safe_mode_confirmed` this function always returns the actual flag value.
*
* @return bool
*/
public static function safe_mode_is_confirmed() {
return Jetpack_Options::get_option( 'safe_mode_confirmed' );
}
/**
* Returns the mismatched URLs.
*
* @return array|bool The mismatched urls, or false if the site is not connected, offline, in safe mode, or the IDC error is not valid.
*/
public static function get_mismatched_urls() {
if ( ! static::has_identity_crisis() ) {
return false;
}
$data = static::check_identity_crisis();
if ( ! $data ||
! isset( $data['error_code'] ) ||
! isset( $data['wpcom_home'] ) ||
! isset( $data['home'] ) ||
! isset( $data['wpcom_siteurl'] ) ||
! isset( $data['siteurl'] )
) {
// The jetpack_sync_error_idc option is missing a key.
return false;
}
if ( 'jetpack_site_url_mismatch' === $data['error_code'] ) {
return array(
'wpcom_url' => $data['wpcom_siteurl'],
'current_url' => $data['siteurl'],
);
}
return array(
'wpcom_url' => $data['wpcom_home'],
'current_url' => $data['home'],
);
}
/**
* Try to detect $_SERVER['HTTP_HOST'] being used within WP_SITEURL or WP_HOME definitions inside of wp-config.
*
* If `HTTP_HOST` usage is found, it's possbile (though not certain) that site URLs are dynamic.
*
* When a site URL is dynamic, it can lead to a Jetpack IDC. If potentially dynamic usage is detected,
* helpful support info will be shown on the IDC UI about setting a static site/home URL.
*
* @return bool True if potentially dynamic site urls were detected in wp-config, false otherwise.
*/
public static function detect_possible_dynamic_site_url() {
$transient_key = 'jetpack_idc_possible_dynamic_site_url_detected';
$transient_val = get_transient( $transient_key );
if ( false !== $transient_val ) {
return (bool) $transient_val;
}
$path = self::locate_wp_config();
$wp_config = $path ? file_get_contents( $path ) : false; // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
if ( $wp_config ) {
$matched = preg_match(
'/define ?\( ?[\'"](?:WP_SITEURL|WP_HOME).+(?:HTTP_HOST).+\);/',
$wp_config
);
if ( $matched ) {
set_transient( $transient_key, 1, HOUR_IN_SECONDS );
return true;
}
}
set_transient( $transient_key, 0, HOUR_IN_SECONDS );
return false;
}
/**
* Gets path to WordPress configuration.
* Source: https://github.com/wp-cli/wp-cli/blob/master/php/utils.php
*
* @return string
*/
public static function locate_wp_config() {
static $path;
if ( null === $path ) {
$path = false;
if ( getenv( 'WP_CONFIG_PATH' ) && file_exists( getenv( 'WP_CONFIG_PATH' ) ) ) {
$path = getenv( 'WP_CONFIG_PATH' );
} elseif ( file_exists( ABSPATH . 'wp-config.php' ) ) {
$path = ABSPATH . 'wp-config.php';
} elseif ( file_exists( dirname( ABSPATH ) . '/wp-config.php' ) && ! file_exists( dirname( ABSPATH ) . '/wp-settings.php' ) ) {
$path = dirname( ABSPATH ) . '/wp-config.php';
}
if ( $path ) {
$path = realpath( $path );
}
}
return $path;
}
/**
* Adds `url_secret` to the `jetpack.idcUrlValidation` URL validation endpoint.
* Adds `url_secret_error` in case of an error.
*
* @param array $response The endpoint response that we're modifying.
*
* @return array
*
* phpcs:ignore Squiz.Commenting.FunctionCommentThrowTag -- The exception is being caught, false positive.
*/
public static function add_secret_to_url_validation_response( array $response ) {
try {
$secret = new URL_Secret();
$secret->create();
if ( $secret->exists() ) {
$response['url_secret'] = $secret->get_secret();
}
} catch ( Exception $e ) {
$response['url_secret_error'] = new WP_Error( 'unable_to_create_url_secret', $e->getMessage() );
}
return $response;
}
/**
* Check if URL is an IP.
*
* @param string $hostname The hostname to check.
* @return bool
*/
public static function url_is_ip( $hostname = null ) {
if ( ! $hostname ) {
$hostname = wp_parse_url( Urls::site_url(), PHP_URL_HOST );
}
$is_ip = filter_var( $hostname, FILTER_VALIDATE_IP ) !== false ? $hostname : false;
return $is_ip;
}
/**
* Add IDC-related data to the registration query.
*
* @param array $params The existing query params.
*
* @return array
*/
public static function register_request_body( array $params ) {
$persistent_blog_id = get_option( static::PERSISTENT_BLOG_ID_OPTION_NAME );
if ( $persistent_blog_id ) {
$params['persistent_blog_id'] = $persistent_blog_id;
$params['url_secret'] = URL_Secret::create_secret( 'registration_request_url_secret_failed' );
}
return $params;
}
/**
* Set the necessary options when site gets registered.
*
* @param int $blog_id The blog ID.
*
* @return void
*/
public static function site_registered( $blog_id ) {
update_option( static::PERSISTENT_BLOG_ID_OPTION_NAME, (int) $blog_id, false );
}
/**
* Check if we need to update the ip_requester option.
*
* @param string $hostname The hostname to check.
*
* @return void
*/
public static function maybe_update_ip_requester( $hostname ) {
// Check if transient exists
$transient_key = ip2long( $hostname );
if ( $transient_key && ! get_transient( 'jetpack_idc_ip_requester_' . $transient_key ) ) {
self::set_ip_requester_for_idc( $hostname, $transient_key );
}
}
/**
* If URL is an IP, add the IP value to the ip_requester option with its expiry value.
*
* @param string $hostname The hostname to check.
* @param int $transient_key The transient key.
*/
public static function set_ip_requester_for_idc( $hostname, $transient_key ) {
// Check if option exists
$data = Jetpack_Options::get_option( 'identity_crisis_ip_requester' );
$ip_requester = array(
'ip' => $hostname,
'expires_at' => time() + 360,
);
// If not set, initialize it
if ( empty( $data ) ) {
$data = array( $ip_requester );
} else {
$updated_data = array();
$updated_value = false;
// Remove expired values and update existing IP
foreach ( $data as $item ) {
if ( time() > $item['expires_at'] ) {
continue; // Skip expired IP
}
if ( $item['ip'] === $hostname ) {
$item['expires_at'] = time() + 360;
$updated_value = true;
}
$updated_data[] = $item;
}
if ( ! $updated_value || empty( $updated_data ) ) {
$updated_data[] = $ip_requester;
}
$data = $updated_data;
}
self::update_ip_requester( $data, $transient_key );
}
/**
* Update the ip_requester option and set a transient to expire in 5 minutes.
*
* @param array $data The data to be updated.
* @param int $transient_key The transient key.
*
* @return void
*/
public static function update_ip_requester( $data, $transient_key ) {
// Update the option
$updated = Jetpack_Options::update_option( 'identity_crisis_ip_requester', $data );
// Set a transient to expire in 5 minutes
if ( $updated ) {
$transient_name = 'jetpack_idc_ip_requester_' . $transient_key;
set_transient( $transient_name, $data, 300 );
}
}
/**
* Adds `ip_requester` to the `jetpack.idcUrlValidation` URL validation endpoint.
*
* @param array $response The enpoint response that we're modifying.
*
* @return array
*/
public static function add_ip_requester_to_url_validation_response( array $response ) {
$requesters = Jetpack_Options::get_option( 'identity_crisis_ip_requester' );
if ( $requesters ) {
// Loop through the requesters and add the IP to the response if it's not expired
$i = 0;
foreach ( $requesters as $ip ) {
if ( $ip['expires_at'] > time() ) {
$response['ip_requester'][] = $ip['ip'];
}
// Limit the response to five IPs
$i = ++$i;
if ( $i === 5 ) {
break;
}
}
}
return $response;
}
}

View File

@@ -0,0 +1,322 @@
<?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() {
$updated = Jetpack_Options::update_option( 'safe_mode_confirmed', true );
if ( $updated ) {
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 )
);
}
/**
* 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() {
if ( Jetpack_Options::get_option( 'sync_error_idc' ) && ! Jetpack_Options::delete_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;
}
}

View File

@@ -0,0 +1,204 @@
<?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=JSON.parse(decodeURIComponent("' . rawurlencode( wp_json_encode( static::get_initial_state_data() ) ) . '"));';
}
/**
* 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( $_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'] );
}
}
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';
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,184 @@
<?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.
*/
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 Jetpacks 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' )
);
}
}

View File

@@ -0,0 +1,387 @@
<?php
/**
* A collection of helper functions used in the SSO module.
*
* @package automattic/jetpack-connection
*/
namespace 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'];
}
}
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',
)
);
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();
}
}

View File

@@ -0,0 +1,272 @@
<?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 can not 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 can not 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 can not be used.
*
* @since jetpack-4.4.0
* @deprecated since 2.10.0
*
* @param string $message Error message.
*
* @return string
*/
public static function sso_not_allowed_in_staging( $message ) {
_deprecated_function( __FUNCTION__, '2.10.0', 'sso_not_allowed_in_safe_mode' );
$error = __(
'Logging in with WordPress.com is disabled for sites that are in staging mode.',
'jetpack-connection'
);
/**
* Filters the disallowed notice for staging sites attempting SSO.
*
* @module sso
*
* @since jetpack-10.5.0
*
* @param string $error Error text.
*/
$error = apply_filters_deprecated( 'jetpack_sso_disallowed_staging_notice', array( $error ), '2.9.1', 'jetpack_sso_disallowed_safe_mode_notice' );
$message .= sprintf( '<p class="message">%s</p>', esc_html( $error ) );
return $message;
}
/**
* Error message that is displayed when the current site is in an identity crisis and SSO can not 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;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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;
}

View File

@@ -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';
}
}
} );

View File

@@ -0,0 +1,164 @@
#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: #777777;
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: bold;
}
.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;
}

View File

@@ -0,0 +1,27 @@
document.addEventListener( 'DOMContentLoaded', () => {
const body = document.querySelector( 'body' ),
toggleSSO = document.querySelector( '.jetpack-sso-toggle' ),
userLogin = document.getElementById( 'user_login' ),
userPassword = document.getElementById( 'user_pass' ),
ssoWrap = document.getElementById( 'jetpack-sso-wrap' ),
loginForm = document.getElementById( 'loginform' ),
overflow = document.createElement( 'div' );
overflow.className = 'jetpack-sso-clear';
loginForm.appendChild( overflow );
overflow.appendChild( document.querySelector( 'p.forgetmenot' ) );
overflow.appendChild( document.querySelector( 'p.submit' ) );
loginForm.appendChild( ssoWrap );
body.classList.add( 'jetpack-sso-repositioned' );
toggleSSO.addEventListener( 'click', e => {
e.preventDefault();
body.classList.toggle( 'jetpack-sso-form-display' );
if ( ! body.classList.contains( 'jetpack-sso-form-display' ) ) {
userLogin.focus();
userPassword.disabled = false;
}
} );
} );

View File

@@ -0,0 +1,64 @@
document.addEventListener( 'DOMContentLoaded', function () {
document
.querySelectorAll( '.jetpack-sso-invitation-tooltip-icon:not(.sso-disconnected-user)' )
.forEach( function ( tooltip ) {
tooltip.innerHTML += ' [?]';
const tooltipTextbox = document.createElement( 'span' );
tooltipTextbox.classList.add( 'jetpack-sso-invitation-tooltip', 'jetpack-sso-th-tooltip' );
const tooltipString = window.Jetpack_SSOTooltip.tooltipString;
tooltipTextbox.innerHTML += tooltipString;
tooltip.addEventListener( 'mouseenter', appendTooltip );
tooltip.addEventListener( 'focus', appendTooltip );
tooltip.addEventListener( 'mouseleave', removeTooltip );
tooltip.addEventListener( 'blur', removeTooltip );
/**
* Display the tooltip textbox.
*/
function appendTooltip() {
tooltip.appendChild( tooltipTextbox );
tooltipTextbox.style.display = 'block';
}
/**
* Remove the tooltip textbox.
*/
function removeTooltip() {
// Only remove tooltip if the element isn't currently active.
if ( document.activeElement === tooltip ) {
return;
}
tooltip.removeChild( tooltipTextbox );
}
} );
document
.querySelectorAll( '.jetpack-sso-invitation-tooltip-icon:not(.jetpack-sso-status-column)' )
.forEach( function ( tooltip ) {
tooltip.addEventListener( 'mouseenter', appendSSOInvitationTooltip );
tooltip.addEventListener( 'focus', appendSSOInvitationTooltip );
tooltip.addEventListener( 'mouseleave', removeSSOInvitationTooltip );
tooltip.addEventListener( 'blur', removeSSOInvitationTooltip );
} );
/**
* Display the SSO invitation tooltip textbox.
*/
function appendSSOInvitationTooltip() {
this.querySelector( '.jetpack-sso-invitation-tooltip' ).style.display = 'block';
}
/**
* Remove the SSO invitation tooltip textbox.
*
* @param {Event} event - Triggering event.
*/
function removeSSOInvitationTooltip( event ) {
if ( document.activeElement === event.target ) {
return;
}
this.querySelector( '.jetpack-sso-invitation-tooltip' ).style.display = 'none';
}
} );

View File

@@ -10,6 +10,7 @@ namespace Automattic\Jetpack\Connection\Webhooks;
use Automattic\Jetpack\Admin_UI\Admin_Menu;
use Automattic\Jetpack\Constants;
use Automattic\Jetpack\Licensing;
use Automattic\Jetpack\Status\Host;
use Automattic\Jetpack\Tracking;
use GP_Locales;
use Jetpack_Network;
@@ -18,11 +19,17 @@ use Jetpack_Network;
* Authorize_Redirect Webhook handler class.
*/
class Authorize_Redirect {
/**
* The Connection Manager object.
*
* @var \Automattic\Jetpack\Connection\Manager
*/
private $connection;
/**
* Constructs the object
*
* @param Automattic\Jetpack\Connection\Manager $connection The Connection Manager object.
* @param \Automattic\Jetpack\Connection\Manager $connection The Connection Manager object.
*/
public function __construct( $connection ) {
$this->connection = $connection;
@@ -32,6 +39,8 @@ class Authorize_Redirect {
* Handle the webhook
*
* This method implements what's in Jetpack::admin_page_load when the Jetpack plugin is not present
*
* @return never
*/
public function handle() {
@@ -89,46 +98,57 @@ class Authorize_Redirect {
}
/**
* Create the Jetpack authorization URL. Copied from Jetpack class.
* Create the Jetpack authorization URL.
*
* @since 2.7.6 Added optional $from and $raw parameters.
*
* @param bool|string $redirect URL to redirect to.
* @param bool|string $from If not false, adds 'from=$from' param to the connect URL.
* @param bool $raw If true, URL will not be escaped.
*
* @todo Update default value for redirect since the called function expects a string.
*
* @return mixed|void
*/
public function build_authorize_url( $redirect = false ) {
public function build_authorize_url( $redirect = false, $from = false, $raw = false ) {
add_filter( 'jetpack_connect_request_body', array( __CLASS__, 'filter_connect_request_body' ) );
add_filter( 'jetpack_connect_redirect_url', array( __CLASS__, 'filter_connect_redirect_url' ) );
$url = $this->connection->get_authorization_url( wp_get_current_user(), $redirect );
$url = $this->connection->get_authorization_url( wp_get_current_user(), $redirect, $from, $raw );
remove_filter( 'jetpack_connect_request_body', array( __CLASS__, 'filter_connect_request_body' ) );
remove_filter( 'jetpack_connect_redirect_url', array( __CLASS__, 'filter_connect_redirect_url' ) );
/**
* This filter is documented in plugins/jetpack/class-jetpack.php
* Filter the URL used when authorizing a user to a WordPress.com account.
*
* @since jetpack-8.9.0
* @since 2.7.6 Added $raw parameter.
*
* @param string $url Connection URL.
* @param bool $raw If true, URL will not be escaped.
*/
return apply_filters( 'jetpack_build_authorize_url', $url );
return apply_filters( 'jetpack_build_authorize_url', $url, $raw );
}
/**
* Filters the redirection URL that is used for connect requests. The redirect
* URL should return the user back to the Jetpack console.
* Copied from Jetpack class.
* URL should return the user back to the My Jetpack page.
*
* @param String $redirect the default redirect URL used by the package.
* @return String the modified URL.
* @param string $redirect the default redirect URL used by the package.
* @return string the modified URL.
*/
public static function filter_connect_redirect_url( $redirect ) {
$jetpack_admin_page = esc_url_raw( admin_url( 'admin.php?page=jetpack' ) );
$jetpack_admin_page = esc_url_raw( admin_url( 'admin.php?page=my-jetpack' ) );
$redirect = $redirect
? wp_validate_redirect( esc_url_raw( $redirect ), $jetpack_admin_page )
: $jetpack_admin_page;
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( isset( $_REQUEST['is_multisite'] ) ) {
if (
class_exists( 'Jetpack_Network' )
&& isset( $_REQUEST['is_multisite'] ) // phpcs:ignore WordPress.Security.NonceVerification.Recommended
) {
$redirect = Jetpack_Network::init()->get_url( 'network_admin_page' );
}
@@ -137,7 +157,6 @@ class Authorize_Redirect {
/**
* Filters the connection URL parameter array.
* Copied from Jetpack class.
*
* @param array $args default URL parameters used by the package.
* @return array the modified URL arguments array.
@@ -164,7 +183,7 @@ class Authorize_Redirect {
)
);
$calypso_env = self::get_calypso_env();
$calypso_env = ( new Host() )->get_calypso_env();
if ( ! empty( $calypso_env ) ) {
$args['calypso_env'] = $calypso_env;
@@ -178,25 +197,15 @@ class Authorize_Redirect {
* it with different Calypso enrionments, such as localhost.
* Copied from Jetpack class.
*
* @deprecated 2.7.6
*
* @since 1.37.1
*
* @return string Calypso environment
*/
public static function get_calypso_env() {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( isset( $_GET['calypso_env'] ) ) {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
return sanitize_key( $_GET['calypso_env'] );
}
_deprecated_function( __METHOD__, '2.7.6', 'Automattic\\Jetpack\\Status\\Host::get_calypso_env' );
if ( getenv( 'CALYPSO_ENV' ) ) {
return sanitize_key( getenv( 'CALYPSO_ENV' ) );
}
if ( defined( 'CALYPSO_ENV' ) && CALYPSO_ENV ) {
return sanitize_key( CALYPSO_ENV );
}
return '';
return ( new Host() )->get_calypso_env();
}
}

View File

@@ -29,7 +29,7 @@ class Constants {
* @access public
* @static
*
* @var array.
* @var array
*/
public static $set_constants = array();
@@ -91,8 +91,8 @@ class Constants {
/**
* Sets the value of the "constant" within constants Manager.
*
* @param string $name The name of the constant.
* @param string $value The value of the constant.
* @param string $name The name of the constant.
* @param int|float|string|bool|array|null $value The value of the constant.
*/
public static function set_constant( $name, $value ) {
self::$set_constants[ $name ] = $value;

View File

@@ -48,7 +48,7 @@ class Redirect {
$source_key = 'source';
if ( 0 === strpos( $source, 'https://' ) ) {
if ( \str_starts_with( $source, 'https://' ) ) {
$source_key = 'url';
$source_url = \wp_parse_url( $source );

View File

@@ -25,7 +25,7 @@ class Cache {
*
* @param string $key Key to fetch.
* @param mixed $default Default value to return if the key is not set.
* @returns mixed Data.
* @return mixed Data.
*/
public static function get( $key, $default = null ) {
$blog_id = get_current_blog_id();

View File

@@ -14,25 +14,31 @@ namespace Automattic\Jetpack;
/**
* Erros class.
*
* @deprecated since 3.2.0
*/
class Errors {
/**
* Catches PHP errors. Must be used in conjunction with output buffering.
*
* @deprecated since 3.2.0
* @param bool $catch True to start catching, False to stop.
*
* @static
*/
public function catch_errors( $catch ) {
_deprecated_function( __METHOD__, '3.2.0' );
static $display_errors, $error_reporting;
if ( $catch ) {
// Force error reporting and output, store original values.
$display_errors = @ini_set( 'display_errors', 1 );
$error_reporting = @error_reporting( E_ALL );
if ( class_exists( 'Jetpack' ) ) {
add_action( 'shutdown', array( 'Jetpack', 'catch_errors_on_shutdown' ), 0 );
}
} else {
// Restore the original values for error reporting and output.
@ini_set( 'display_errors', $display_errors );
@error_reporting( $error_reporting );
if ( class_exists( 'Jetpack' ) ) {

View File

@@ -34,7 +34,7 @@ class Files {
// phpcs:ignore Generic.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition
while ( false !== $file = readdir( $dir ) ) {
if ( '.' === substr( $file, 0, 1 ) || '.php' !== substr( $file, -4 ) ) {
if ( str_starts_with( $file, '.' ) || ! str_ends_with( $file, '.php' ) ) {
continue;
}

View File

@@ -34,7 +34,7 @@ class Host {
*
* @since 1.9.0
*
* @return bool;
* @return bool
*/
public function is_atomic_platform() {
return Constants::is_true( 'ATOMIC_SITE_ID' ) && Constants::is_true( 'ATOMIC_CLIENT_ID' );
@@ -127,11 +127,169 @@ class Host {
*/
public function get_source_query() {
// phpcs:disable WordPress.Security.NonceVerification.Recommended
$allowed_sources = array( 'jetpack-manage' );
$allowed_sources = array( 'jetpack-manage', 'a8c-for-agencies' );
if ( isset( $_GET['source'] ) && in_array( $_GET['source'], $allowed_sources, true ) ) {
return sanitize_key( $_GET['source'] );
}
return '';
}
/**
* Returns an array of nameservers for the current site.
*
* @param string $domain The domain of the site to check.
* @return array
*/
public function get_nameserver_dns_records( $domain ) {
if ( ! function_exists( 'dns_get_record' ) ) {
return array();
}
$dns_records = dns_get_record( $domain, DNS_NS ); // Fetches the DNS records of type NS (Name Server)
$nameservers = array();
foreach ( $dns_records as $record ) {
if ( isset( $record['target'] ) ) {
$nameservers[] = $record['target']; // Adds the nameserver to the array
}
}
return $nameservers; // Returns an array of nameserver names
}
/**
* Given a DNS entry, will return a hosting provider if one can be determined. Otherwise, will return 'unknown'.
* Sourced from: fbhepr%2Skers%2Sjcpbz%2Sjc%2Qpbagrag%2Syvo%2Subfgvat%2Qcebivqre%2Sanzrfreiref.cuc-og
*
* @param string $domain The domain of the site to check.
* @return string The hosting provider of 'unknown'.
*/
public function get_hosting_provider_by_nameserver( $domain ) {
$known_nameservers = array(
'bluehost' => array(
'.bluehost.com',
),
'dreamhost' => array(
'.dreamhost.com',
),
'mediatemple' => array(
'.mediatemple.net',
),
'xserver' => array(
'.xserver.jp',
),
'namecheap' => array(
'.namecheaphosting.com',
),
'hostmonster' => array(
'.hostmonster.com',
),
'justhost' => array(
'.justhost.com',
),
'digitalocean' => array(
'.digitalocean.com',
),
'one' => array(
'.one.com',
),
'hostpapa' => array(
'.hostpapa.com',
),
'siteground' => array(
'.sgcloud.net',
'.sgedu.site',
'.sgsrv1.com',
'.sgvps.net',
'.siteground.biz',
'.siteground.net',
'.siteground.eu',
),
'inmotion' => array(
'.inmotionhosting.com',
),
'ionos' => array(
'.ui-dns.org',
'.ui-dns.de',
'.ui-dns.biz',
'.ui-dns.com',
),
);
$dns_records = $this->get_nameserver_dns_records( $domain );
$dns_records = array_map( 'strtolower', $dns_records );
foreach ( $known_nameservers as $host => $ns_patterns ) {
foreach ( $ns_patterns as $ns_pattern ) {
foreach ( $dns_records as $record ) {
if ( false !== strpos( $record, $ns_pattern ) ) {
return $host;
}
}
}
}
return 'unknown';
}
/**
* Returns a guess of the hosting provider for the current site based on various checks.
*
* @return string
*/
public function get_known_host_guess() {
$host = Cache::get( 'host_guess' );
if ( null !== $host ) {
return $host;
}
// First, let's check if we can recognize provider manually:
switch ( true ) {
case $this->is_woa_site():
$provider = 'woa';
break;
case $this->is_atomic_platform():
$provider = 'atomic';
break;
case $this->is_newspack_site():
$provider = 'newspack';
break;
case $this->is_vip_site():
$provider = 'vip';
break;
case $this->is_wpcom_simple():
case $this->is_wpcom_platform():
$provider = 'wpcom';
break;
default:
$provider = 'unknown';
break;
}
// Second, let's check if we can recognize provider by nameservers:
$domain = isset( $_SERVER['SERVER_NAME'] ) ? sanitize_text_field( wp_unslash( $_SERVER['SERVER_NAME'] ) ) : '';
if ( $provider === 'unknown' && ! empty( $domain ) ) {
$provider = $this->get_hosting_provider_by_nameserver( $domain );
}
Cache::set( 'host_guess', $provider );
return $provider;
}
/**
* Add public-api.wordpress.com to the safe redirect allowed list - only added when someone allows API access.
*
* @since 3.0.2 Ported from Jetpack to the Status package.
*
* To be used with a filter of allowed domains for a redirect.
*
* @param array $domains Allowed WP.com Environments.
*
* @return array
*/
public static function allow_wpcom_public_api_domain( $domains ) {
$domains[] = 'public-api.wordpress.com';
return $domains;
}
}

View File

@@ -22,10 +22,16 @@ class Modules {
* Check whether or not a Jetpack module is active.
*
* @param string $module The slug of a Jetpack module.
* @param bool $available_only Whether to only check among available modules.
*
* @return bool
*/
public function is_active( $module ) {
return in_array( $module, self::get_active(), true );
public function is_active( $module, $available_only = true ) {
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
return true;
}
return in_array( $module, self::get_active( $available_only ), true );
}
/**
@@ -162,7 +168,7 @@ class Modules {
}
$key = md5( $file_name . maybe_serialize( $headers ) );
$refresh_cache = is_admin() && isset( $_GET['page'] ) && 'jetpack' === substr( $_GET['page'], 0, 7 ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput
$refresh_cache = is_admin() && isset( $_GET['page'] ) && is_string( $_GET['page'] ) && str_starts_with( $_GET['page'], 'jetpack' ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput
// If we don't need to refresh the cache, and already have the value, short-circuit!
if ( ! $refresh_cache && isset( $file_data_option[ $key ] ) ) {
@@ -180,8 +186,12 @@ class Modules {
/**
* Get a list of activated modules as an array of module slugs.
*
* @param bool $available_only Filter out the unavailable (deleted) modules.
*
* @return array
*/
public function get_active() {
public function get_active( $available_only = true ) {
$active = \Jetpack_Options::get_option( 'active_modules' );
if ( ! is_array( $active ) ) {
@@ -202,9 +212,11 @@ class Modules {
$active[] = 'protect';
}
// If it's not available, it shouldn't be active.
// We don't delete it from the options though, as it will be active again when a plugin gets reactivated.
$active = array_intersect( $active, $this->get_available() );
if ( $available_only ) {
// If it's not available, it shouldn't be active.
// We don't delete it from the options though, as it will be active again when a plugin gets reactivated.
$active = array_intersect( $active, $this->get_available() );
}
/**
* Allow filtering of the active modules.
@@ -450,10 +462,8 @@ class Modules {
}
// Check the file for fatal errors, a la wp-admin/plugins.php::activate.
$errors = new Errors();
$state->state( 'module', $module );
$state->state( 'error', 'module_activation_failed' ); // we'll override this later if the plugin can be included without fatal error.
$errors->catch_errors( true );
ob_start();
$module_path = $this->get_path( $module );
@@ -466,7 +476,6 @@ class Modules {
$state->state( 'error', false ); // the override.
ob_end_clean();
$errors->catch_errors( false );
} else { // Not a Jetpack plugin.
$active[] = $module;
$this->update_active( $active );
@@ -530,7 +539,7 @@ class Modules {
*
* @param array $modules Array of active modules to be saved in options.
*
* @return $success bool true for success, false for failure.
* @return bool $success true for success, false for failure.
*/
public function update_active( $modules ) {
$current_modules = \Jetpack_Options::get_option( 'active_modules', array() );

View File

@@ -25,4 +25,57 @@ class Paths {
$url = add_query_arg( $args, admin_url( 'admin.php' ) );
return $url;
}
/**
* Determine if the current request is activating a plugin from the plugins page.
*
* @param string $plugin Plugin file path to check.
* @return bool
*/
public function is_current_request_activating_plugin_from_plugins_screen( $plugin ) {
// Filter out common async request contexts
if (
wp_doing_ajax() ||
( defined( 'REST_REQUEST' ) && REST_REQUEST ) ||
( defined( 'REST_API_REQUEST' ) && REST_API_REQUEST ) ||
( defined( 'WP_CLI' ) && WP_CLI )
) {
return false;
}
if ( isset( $_SERVER['SCRIPT_NAME'] ) ) {
$request_file = esc_url_raw( wp_unslash( $_SERVER['SCRIPT_NAME'] ) );
} elseif ( isset( $_SERVER['REQUEST_URI'] ) ) {
list( $request_file ) = explode( '?', esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) );
} else {
return false;
}
// Not the plugins page
if ( strpos( $request_file, 'wp-admin/plugins.php' ) === false ) {
return false;
}
// Same method to get the action as used by plugins.php
$wp_list_table = _get_list_table( 'WP_Plugins_List_Table' );
$action = $wp_list_table->current_action();
// Not a singular activation
// This also means that if the plugin is activated as part of a group ( bulk activation ), this function will return false here.
if ( 'activate' !== $action ) {
return false;
}
// Check the nonce associated with the plugin activation
// We are not changing any data here, so this is not super necessary, it's just a best practice before using the form data from $_REQUEST.
check_admin_referer( 'activate-plugin_' . $plugin );
// Not the right plugin
$requested_plugin = isset( $_REQUEST['plugin'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['plugin'] ) ) : null;
if ( $requested_plugin !== $plugin ) {
return false;
}
return true;
}
}

View File

@@ -17,18 +17,6 @@ use WPCOM_Masterbar;
* Used to retrieve information about the current status of Jetpack and the site overall.
*/
class Status {
/**
* Is Jetpack in development (offline) mode?
*
* @deprecated 1.3.0 Use Status->is_offline_mode().
*
* @return bool Whether Jetpack's offline mode is active.
*/
public function is_development_mode() {
_deprecated_function( __FUNCTION__, '1.3.0', 'Automattic\Jetpack\Status->is_offline_mode' );
return $this->is_offline_mode();
}
/**
* Is Jetpack in offline mode?
*
@@ -54,20 +42,6 @@ class Status {
$offline_mode = true;
}
/**
* Filters Jetpack's offline mode.
*
* @see https://jetpack.com/support/development-mode/
* @todo Update documentation ^^.
*
* @since 1.1.1
* @since-jetpack 2.2.1
* @deprecated 1.3.0
*
* @param bool $offline_mode Is Jetpack's offline mode active.
*/
$offline_mode = (bool) apply_filters_deprecated( 'jetpack_development_mode', array( $offline_mode ), '1.3.0', 'jetpack_offline_mode' );
/**
* Filters Jetpack's offline mode.
*
@@ -84,21 +58,6 @@ class Status {
return $offline_mode;
}
/**
* Is Jetpack in "No User test mode"?
*
* This will make Jetpack act as if there were no connected users, but only a site connection (aka blog token)
*
* @since 1.6.0
* @deprecated 1.7.5 Since this version, Jetpack connection is considered active after registration, making no_user_testing_mode obsolete.
*
* @return bool Whether Jetpack's No User Testing Mode is active.
*/
public function is_no_user_testing_mode() {
_deprecated_function( __METHOD__, '1.7.5' );
return true;
}
/**
* Whether this is a system with a multiple networks.
* Implemented since there is no core is_multi_network function.
@@ -167,6 +126,7 @@ class Status {
$site_url = site_url();
// Check for localhost and sites using an IP only first.
// Note: str_contains() is not used here, as wp-includes/compat.php is not loaded in this file.
$is_local = $site_url && false === strpos( $site_url, '.' );
// Use Core's environment check, if available.
@@ -211,11 +171,12 @@ class Status {
/**
* If is a staging site.
*
* @todo Add IDC detection to a package.
* @deprecated since 3.3.0
*
* @return bool
*/
public function is_staging_site() {
_deprecated_function( __FUNCTION__, '3.3.0', 'in_safe_mode' );
$cached = Cache::get( 'is_staging_site' );
if ( null !== $cached ) {
return $cached;
@@ -301,6 +262,64 @@ class Status {
return $is_staging;
}
/**
* If the site is in safe mode.
*
* @since 3.3.0
*
* @return bool
*/
public function in_safe_mode() {
$cached = Cache::get( 'in_safe_mode' );
if ( null !== $cached ) {
return $cached;
}
$in_safe_mode = false;
if ( method_exists( 'Automattic\\Jetpack\\Identity_Crisis', 'validate_sync_error_idc_option' ) && \Automattic\Jetpack\Identity_Crisis::validate_sync_error_idc_option() ) {
$in_safe_mode = true;
}
/**
* Filters in_safe_mode check.
*
* @since 3.3.0
*
* @param bool $in_safe_mode If the current site is in safe mode.
*/
$in_safe_mode = apply_filters( 'jetpack_is_in_safe_mode', $in_safe_mode );
Cache::set( 'in_safe_mode', $in_safe_mode );
return $in_safe_mode;
}
/**
* If the site is a development/staging site.
* This is a new version of is_staging_site added to separate safe mode from the legacy staging mode.
* This method checks for core WP_ENVIRONMENT_TYPE setting
* Using the jetpack_is_development_site filter.
*
* @since 3.3.0
*
* @return bool
*/
public static function is_development_site() {
$cached = Cache::get( 'is_development_site' );
if ( null !== $cached ) {
return $cached;
}
$is_dev_site = ! in_array( wp_get_environment_type(), array( 'production', 'local' ), true );
/**
* Filters is_development_site check.
*
* @since 3.3.0
*
* @param bool $is_dev_site If the current site is a staging or dev site.
*/
$is_dev_site = apply_filters( 'jetpack_is_development_site', $is_dev_site );
Cache::set( 'is_development_site', $is_dev_site );
return $is_dev_site;
}
/**
* Whether the site is currently onboarding or not.
* A site is considered as being onboarded if it currently has an onboarding token.

View File

@@ -40,4 +40,17 @@ class Visitor {
return ! empty( $_SERVER['REMOTE_ADDR'] ) ? filter_var( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : '';
}
/**
* Simple gate check for a11n feature testing purposes using AT_PROXIED_REQUEST constant.
* IMPORTANT: Only use it for internal feature test purposes, not authorization.
*
* The goal of this function is to help us gate features by using a similar function name
* we find on simple sites: is_automattician().
*
* @return bool True if the current request is PROXIED, false otherwise.
*/
public function is_automattician_feature_flags_only() {
return ( defined( 'AT_PROXIED_REQUEST' ) && AT_PROXIED_REQUEST );
}
}