Файловый менеджер - Редактировать - /home/tuudkjt/globeasy/wp-includes/ID3/automattic.tar
Назад
jetpack-admin-ui/LICENSE.txt 0000777 00000043760 15251741406 0011537 0 ustar 00 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. jetpack-admin-ui/composer.json 0000777 00000002613 15251741406 0012426 0 ustar 00 { "name": "automattic/jetpack-admin-ui", "description": "Generic Jetpack wp-admin UI elements", "type": "jetpack-library", "license": "GPL-2.0-or-later", "require": { "php": ">=7.2" }, "require-dev": { "yoast/phpunit-polyfills": "^4.0.0", "automattic/jetpack-changelogger": "^6.0.5", "automattic/jetpack-logo": "^3.0.5", "automattic/jetpack-test-environment": "@dev", "automattic/phpunit-select-config": "^1.0.3" }, "suggest": { "automattic/jetpack-autoloader": "Allow for better interoperability with other plugins that use this package." }, "autoload": { "classmap": [ "src/" ] }, "scripts": { "phpunit": [ "phpunit-select-config phpunit.#.xml.dist --colors=always" ], "test-coverage": [ "php -dpcov.directory=. ./vendor/bin/phpunit-select-config phpunit.#.xml.dist --coverage-php \"$COVERAGE_DIR/php.cov\"" ], "test-php": [ "@composer phpunit" ] }, "minimum-stability": "dev", "prefer-stable": true, "extra": { "autotagger": true, "mirror-repo": "Automattic/jetpack-admin-ui", "textdomain": "jetpack-admin-ui", "changelogger": { "link-template": "https://github.com/Automattic/jetpack-admin-ui/compare/${old}...${new}" }, "branch-alias": { "dev-trunk": "0.5.x-dev" }, "version-constants": { "::PACKAGE_VERSION": "src/class-admin-menu.php" } }, "config": { "allow-plugins": { "roots/wordpress-core-installer": true } } } jetpack-admin-ui/src/class-admin-menu.php 0000777 00000016227 15251741406 0014347 0 ustar 00 <?php /** * Admin Menu Registration * * @package automattic/jetpack-admin-ui */ namespace Automattic\Jetpack\Admin_UI; /** * This class offers a wrapper to add_submenu_page and makes sure stand-alone plugin's menu items are always added under the Jetpack top level menu. * If the Jetpack top level was not previously registered by other plugin, it will be registered here. */ class Admin_Menu { const PACKAGE_VERSION = '0.5.11'; /** * Whether this class has been initialized * * @var boolean */ private static $initialized = false; /** * List of menu items enqueued to be added * * @var array */ private static $menu_items = array(); /** * Initialize the class and set up the main hook * * @return void */ public static function init() { if ( ! self::$initialized ) { 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. } } /** * Handles the Akismet menu item when used alongside other stand-alone plugins * * When Jetpack plugin is present, Akismet menu item is moved under the Jetpack top level menu, but if Akismet is active alongside other stand-alone plugins, * we use this method to move the menu item. */ private static function handle_akismet_menu() { if ( class_exists( 'Akismet_Admin' ) ) { 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' ), 6 ); }, 4 ); } } /** * Callback to the admin_menu and network_admin_menu hooks that will register the enqueued menu items * * @return void */ public static function admin_menu_hook_callback() { $can_see_toplevel_menu = true; $jetpack_plugin_present = class_exists( 'Jetpack_React_Page' ); $icon = method_exists( '\Automattic\Jetpack\Assets\Logo', 'get_base64_logo' ) ? ( new \Automattic\Jetpack\Assets\Logo() )->get_base64_logo() : 'dashicons-admin-plugins'; if ( ! $jetpack_plugin_present ) { add_menu_page( 'Jetpack', 'Jetpack', 'edit_posts', 'jetpack', '__return_null', $icon, 3 ); // If Jetpack plugin is not present, user will only be able to see this menu if they have enough capability to at least one of the sub menus being added. $can_see_toplevel_menu = false; } /** * The add_sub_menu function has a bug and will not keep the right order of menu items. * * @see https://core.trac.wordpress.org/ticket/52035 * Let's order the items before registering them. * Since this all happens after the Jetpack plugin menu items were added, all items will be added after Jetpack plugin items - unless position is very low number (smaller than the number of menu items present in Jetpack plugin). */ usort( self::$menu_items, function ( $a, $b ) { $position_a = empty( $a['position'] ) ? 0 : $a['position']; $position_b = empty( $b['position'] ) ? 0 : $b['position']; $result = $position_a <=> $position_b; if ( 0 === $result ) { $result = strcmp( $a['menu_title'], $b['menu_title'] ); } return $result; } ); foreach ( self::$menu_items as $menu_item ) { if ( ! current_user_can( $menu_item['capability'] ) ) { continue; } $can_see_toplevel_menu = true; add_submenu_page( 'jetpack', $menu_item['page_title'], $menu_item['menu_title'], $menu_item['capability'], $menu_item['menu_slug'], $menu_item['function'], $menu_item['position'] ); } if ( ! $jetpack_plugin_present ) { remove_submenu_page( 'jetpack', 'jetpack' ); } if ( ! $can_see_toplevel_menu ) { remove_menu_page( 'jetpack' ); } } /** * Adds a new submenu to the Jetpack Top level menu * * The parameters this method accepts are the same as @see add_submenu_page. This class will * aggreagate all menu items registered by stand-alone plugins and make sure they all go under the same * Jetpack top level menu. It will also handle the top level menu registration in case the Jetpack plugin is not present. * * @param string $page_title The text to be displayed in the title tags of the page when the menu * is selected. * @param string $menu_title The text to be used for the menu. * @param string $capability The capability required for this menu to be displayed to the user. * @param string $menu_slug The slug name to refer to this menu by. Should be unique for this menu * and only include lowercase alphanumeric, dashes, and underscores characters * to be compatible with sanitize_key(). * @param callable|null $function The function to be called to output the content for this page. * @param int $position The position in the menu order this item should appear. Leave empty typically. * * @return string The resulting page's hook_suffix */ public static function add_menu( $page_title, $menu_title, $capability, $menu_slug, $function, $position = null ) { self::init(); self::$menu_items[] = compact( 'page_title', 'menu_title', 'capability', 'menu_slug', 'function', 'position' ); /** * Let's return the page hook so consumers can use. * We know all pages will be under Jetpack top level menu page, so we can hardcode the first part of the string. * Using get_plugin_page_hookname here won't work because the top level page is not registered yet. */ return 'jetpack_page_' . $menu_slug; } /** * Removes an already added submenu * * @param string $menu_slug The slug of the submenu to remove. * * @return array|false The removed submenu on success, false if not found. */ public static function remove_menu( $menu_slug ) { foreach ( self::$menu_items as $index => $menu_item ) { if ( $menu_item['menu_slug'] === $menu_slug ) { unset( self::$menu_items[ $index ] ); return $menu_item; } } return false; } /** * Gets the slug for the first item under the Jetpack top level menu * * @return string|null */ public static function get_top_level_menu_item_slug() { global $submenu; if ( ! empty( $submenu['jetpack'] ) ) { $item = reset( $submenu['jetpack'] ); if ( isset( $item[2] ) ) { return $item[2]; } } } /** * Gets the URL for the first item under the Jetpack top level menu * * @param string $fallback If Jetpack menu is not there or no children is found, return this fallback instead. Default to admin_url(). * @return string */ public static function get_top_level_menu_item_url( $fallback = false ) { $slug = self::get_top_level_menu_item_slug(); if ( $slug ) { $url = menu_page_url( $slug, false ); return $url; } $url = $fallback ? $fallback : admin_url(); return $url; } } block-delimiter/composer.json 0000777 00000002450 15251741406 0012351 0 ustar 00 { "name": "automattic/block-delimiter", "description": "Efficiently work with block structure", "type": "jetpack-library", "license": "GPL-2.0-or-later", "require": { "php": ">=7.2" }, "require-dev": { "automattic/jetpack-test-environment": "@dev", "automattic/jetpack-changelogger": "^6.0.6", "automattic/phpunit-select-config": "^1.0.3" }, "autoload": { "classmap": [ "src/" ] }, "autoload-dev": { "classmap": [ "tests/php/lib/" ], "psr-4": { "Automattic\\Block_Delimiter\\Tests\\": "tests/php/" } }, "scripts": { "phpunit": [ "phpunit-select-config phpunit.#.xml.dist --colors=always" ], "test-coverage": [ "php -dpcov.directory=. ./vendor/bin/phpunit-select-config phpunit.#.xml.dist --coverage-php \"$COVERAGE_DIR/php.cov\"" ], "test-php": [ "@composer phpunit" ] }, "minimum-stability": "dev", "prefer-stable": true, "extra": { "autotagger": true, "branch-alias": { "dev-trunk": "0.3.x-dev" }, "changelogger": { "link-template": "https://github.com/Automattic/block-delimiter/compare/v${old}...v${new}" }, "mirror-repo": "Automattic/block-delimiter", "textdomain": "jetpack-block-delimiter" }, "suggest": { "automattic/jetpack-autoloader": "Allow for better interoperability with other plugins that use this package." } } block-delimiter/LICENSE.txt 0000777 00000043760 15251741406 0011463 0 ustar 00 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. block-delimiter/src/class-block-delimiter.php 0000777 00000072601 15251741406 0015305 0 ustar 00 <?php /** * Efficiently working with block structure. * * @package automattic/block-delimiter */ declare( strict_types = 1 ); namespace Automattic; use Exception; /** * Class for efficiently working with block structure. * * This class follows design values of the HTML API: * - minimize allocations and strive for zero memory overhead * - make costs explicit; pay only for what you need * - follow a streaming, re-entrant design for pausing and aborting * * For usage, jump straight to {@see static::next_delimiter}. */ class Block_Delimiter { /** * Indicates if the last operation failed, otherwise * will be `null` for success. * * @var string|null */ private static $last_error = self::UNINITIALIZED; /** * Indicates failures from decoding JSON attributes. * * @var int */ private $last_json_error = JSON_ERROR_NONE; /** * Holds a reference to the original source text from which to * extract the parsed spans of the delimiter. * * @var string */ private $source_text; /** * Byte offset into source text where entire delimiter begins. * * @var int */ private $delimiter_at; /** * Byte length of full span of delimiter. * * @var int */ private $delimiter_length; /** * Byte offset where namespace span begins. * * @var int */ private $namespace_at; /** * Byte length of namespace span, or `0` if implicitly in the "core" namespace. * * @var int */ private $namespace_length; /** * Byte offset where block name span begins. * * @var int */ private $name_at; /** * Byte length of block name span. * * @var int */ private $name_length; /** * Whether the delimiter contains the block self-closing flag. * * This may be erroneous if present within a block closer, * therefore the {@see self::has_void_flag} can be used by * calling code to perform appropriate error-handling. * * @var bool */ private $has_void_flag = false; /** * Byte offset where JSON attributes span begins. * * @var int */ private $json_at; /** * Byte length of JSON attributes span, or `0` if none are present. * * @var int */ private $json_length; /** * Indicates what kind of block comment delimiter this represents. * * One of: * * - `static::OPENER` If the delimiter is opening a block. * - `static::CLOSER` If the delimiter is closing an open block. * - `static::VOID` If the delimiter represents a void block with no inner content. * * If a parsed comment delimiter contains both the closing and the void * flags then it will be interpreted as a void block to match the behavior * of the official block parser, however, this is a mistake and probably * the block ought to close an open block of the same name, if one is open. * * @var string */ private $type; /** * Finds the next block delimiter in a text document and returns a parsed * block delimiter info record if it parses, otherwise returns `null`. * * Block comment delimiters must be valid HTML comments and may contain JSON. * This search does not determine, however, if the JSON is valid. * * Example delimiters: * * `<!-- wp:paragraph {"dropCap": true} -->` * `<!-- wp:separator /-->` * `<!-- /wp:paragraph -->` * * In the case that a block comment delimiter contains both the void indicator and * also the closing indicator, it will be treated as a void block. * * Example: * * // Find all image block opening delimiters. * $at = 0; * $end = strlen( $html ); * $images = array(); * while ( $at < $end ) { * $delimiter = Block_Delimiter::next_delimiter( $html, $at, $next_at, $next_length ); * if ( ! isset( $delimiter ) ) { * break; * } * * if ( * Block_Delimiter::OPENER === $delimiter->get_delimiter_type() && * $delimiter->is_block_type( 'core/image' ) * ) { * $images[] = $delimiter; * } * * $at = $next_at + $next_length; * } * * @param string $text Input document possibly containing block comment delimiters. * @param int $starting_byte_offset Where in the input document to begin searching. * @param int|null $match_byte_offset Optional. When provided, will be set to the byte offset in * the input document where the delimiter was found, if one * is found, otherwise not set. * @param int|null $match_byte_length Optional. When provided, will be set to the byte length of * the matched delimiter if one is found, otherwise not set. * @return Block_Delimiter|null Parsed block delimiter info record if found, otherwise `null`. */ public static function next_delimiter( string $text, int $starting_byte_offset, ?int &$match_byte_offset = null, ?int &$match_byte_length = null ): ?Block_Delimiter { $end = strlen( $text ); $at = $starting_byte_offset; $delimiter = null; static::$last_error = null; while ( $at < $end ) { /* * Find the next possible opening. * * This follows the behavior in the official block parser, which treats a post * as a list of blocks with nested HTML. If HTML comment syntax appears within * an HTML attribute value, SCRIPT or STYLE element, or in other select places, * which it can do inside of HTML, then the block parsing may break. * * For a more robust parse scan through the document with the HTML API. In * practice, this has not been a problem in the entire history of blocks. */ $comment_opening_at = strpos( $text, '<!--', $at ); if ( false === $comment_opening_at ) { return null; } $opening_whitespace_at = $comment_opening_at + 4; $opening_whitespace_length = strspn( $text, " \t\f\r\n", $opening_whitespace_at ); if ( 0 === $opening_whitespace_length ) { $at = self::find_html_comment_end( $text, $comment_opening_at, $end ); continue; } $wp_prefix_at = $opening_whitespace_at + $opening_whitespace_length; if ( $wp_prefix_at >= $end ) { static::$last_error = self::INCOMPLETE_INPUT; return null; } $has_closer = false; if ( '/' === $text[ $wp_prefix_at ] ) { $has_closer = true; ++$wp_prefix_at; } if ( 0 !== substr_compare( $text, 'wp:', $wp_prefix_at, 3 ) ) { $at = self::find_html_comment_end( $text, $comment_opening_at, $end ); continue; } $namespace_at = $wp_prefix_at + 3; if ( $namespace_at >= $end ) { static::$last_error = self::INCOMPLETE_INPUT; return null; } $start_of_namespace = $text[ $namespace_at ]; // The namespace must start with a-z. if ( 'a' > $start_of_namespace || 'z' < $start_of_namespace ) { $at = self::find_html_comment_end( $text, $comment_opening_at, $end ); continue; } $namespace_length = 1 + strspn( $text, 'abcdefghijklmnopqrstuvwxyz0123456789-_', $namespace_at + 1 ); $separator_at = $namespace_at + $namespace_length; if ( $separator_at >= $end ) { static::$last_error = self::INCOMPLETE_INPUT; return null; } $has_separator = '/' === $text[ $separator_at ]; if ( $has_separator ) { $name_at = $separator_at + 1; $start_of_name = $text[ $name_at ]; if ( 'a' > $start_of_name || 'z' < $start_of_name ) { $at = self::find_html_comment_end( $text, $comment_opening_at, $end ); continue; } $name_length = 1 + strspn( $text, 'abcdefghijklmnopqrstuvwxyz0123456789-_', $name_at + 1 ); } else { $name_at = $namespace_at; $name_length = $namespace_length; $namespace_length = 0; } $after_name_whitespace_at = $name_at + $name_length; $after_name_whitespace_length = strspn( $text, " \t\f\r\n", $after_name_whitespace_at ); if ( 0 === $after_name_whitespace_length ) { $at = self::find_html_comment_end( $text, $comment_opening_at, $end ); continue; } $json_at = $after_name_whitespace_at + $after_name_whitespace_length; if ( $json_at >= $end ) { static::$last_error = self::INCOMPLETE_INPUT; return null; } $has_json = '{' === $text[ $json_at ]; $json_length = 0; /* * For the final span of the delimiter it's most efficient to find the end * of the HTML comment and work backwards. This prevents complicated parsing * inside the JSON span, which cannot contain the HTML comment terminator. * * This also matches the behavior in the official block parser, though it * allows for matching invalid JSON content. */ $comment_closing_at = strpos( $text, '-->', $json_at ); if ( false === $comment_closing_at ) { static::$last_error = self::INCOMPLETE_INPUT; return null; } /* * It looks like this logic leaves an error in here, when the position * overlaps the JSON or block name. However, for neither of those is it * possible to parse a valid block if that last overlapping character * is the void flag. This, therefore, will be valid regardless of how * the rest of the comment delimiter is written. */ if ( '/' === $text[ $comment_closing_at - 1 ] ) { $has_void_flag = true; $void_flag_length = 1; } else { $has_void_flag = false; $void_flag_length = 0; } /* * If there's no JSON, then the span of text after the name * until the comment closing must be completely whitespace. */ if ( ! $has_json ) { $max_whitespace_length = $comment_closing_at - $json_at - $void_flag_length; // This shouldn't be possible, but it can't be allowed regardless. if ( $max_whitespace_length < 0 ) { $at = self::find_html_comment_end( $text, $comment_opening_at, $end ); continue; } $closing_whitespace_length = strspn( $text, " \t\f\r\n", $json_at, $comment_closing_at - $json_at - $void_flag_length ); if ( 0 === $after_name_whitespace_length + $closing_whitespace_length ) { $at = self::find_html_comment_end( $text, $comment_opening_at, $end ); continue; } // This must be a block delimiter! $delimiter = new static(); break; } // There's no JSON, so attempt to find its boundary. $after_json_whitespace_length = 0; for ( $char_at = $comment_closing_at - $void_flag_length - 1; $char_at > $json_at; $char_at-- ) { $char = $text[ $char_at ]; switch ( $char ) { case ' ': case "\t": case "\f": case "\r": case "\n": ++$after_json_whitespace_length; continue 2; case '}': $json_length = $char_at - $json_at + 1; break 2; default: ++$at; continue 3; } } if ( 0 === $json_length || 0 === $after_json_whitespace_length ) { $at = self::find_html_comment_end( $text, $comment_opening_at, $end ); continue; } // This must be a block delimiter! $delimiter = new static(); break; } if ( null === $delimiter ) { return null; } $delimiter->source_text = $text; $delimiter->delimiter_at = $comment_opening_at; $delimiter->delimiter_length = $comment_closing_at + 3 - $comment_opening_at; $delimiter->namespace_at = $namespace_at; $delimiter->namespace_length = $namespace_length; $delimiter->name_at = $name_at; $delimiter->name_length = $name_length; $delimiter->json_at = $json_at; $delimiter->json_length = $json_length; $delimiter->type = $has_closer ? static::CLOSER : ( $has_void_flag ? static::VOID : static::OPENER ); $delimiter->has_void_flag = $has_void_flag; $match_byte_offset = $delimiter->delimiter_at; $match_byte_length = $delimiter->delimiter_length; return $delimiter; } /** * Generator function to traverse block delimiters in a text and also * yield the position information for the delimiter as it scans. * * Example: * * foreach ( Block_Delimiter::scan_delimiters( $post_content ) as $where => $delimiter ) { * echo "Found a {$delimiter->allocate_and_return_block_type()} at {$where[0]} (length is {$where[1]} bytes)\n"; * } * * @param string $text Document potentially containing blocks. * @param ?string $freeform_blocks Optional. 'visit' to visit virtual block delimiters for freeform content. * @return \Generator Visits each block delimiter and provides [ at, length ] => delimiter. */ public static function scan_delimiters( string $text, ?string $freeform_blocks = 'skip' ): \Generator { $at = 0; $depth = 0; /* * Although `phpcs` confidently asserts that `$match_at` and `$match_length` * are undefined, it is not aware enough to realize that they are set by the * call to `next_delimiter` and so it’s necessary to alter the code so it * doesn’t get confused and reject valid code. */ $match_at = 0; $match_length = 0; while ( null !== ( $delimiter = self::next_delimiter( $text, $at, $match_at, $match_length ) ) ) { // Handle top-level text as freeform blocks if ( 0 === $depth && $match_at > $at && 'visit' === $freeform_blocks ) { list( $text_opener, $text_closer ) = static::freeform_pair( $text, $at, $match_at - $at ); ++$depth; yield [ $at, 0 ] => $text_opener; --$depth; yield [ $match_at, 0 ] => $text_closer; } $delimiter_type = $delimiter->get_delimiter_type(); switch ( $delimiter_type ) { case static::OPENER: case static::VOID: ++$depth; break; case static::CLOSER: --$depth; break; } yield [ $match_at, $match_length ] => $delimiter; if ( static::VOID === $delimiter_type ) { --$depth; } $at = $match_at + $match_length; } $end = strlen( $text ); if ( 'visit' === $freeform_blocks && $at < $end ) { list( $text_opener, $text_closer ) = static::freeform_pair( $text, $at, $end - $at ); ++$depth; yield [ $at, 0 ] => $text_opener; --$depth; yield [ $end, 0 ] => $text_closer; } } /** * Constructor function. */ private function __construct() { // This is not to be called from the outside. } /** * Returns the byte-offset after the ending character of an HTML comment, * assuming the proper starting byte offset. * * @since 0.2.1 * * @param string $text Document in which to search for HTML comment end. * @param int $comment_starting_at Where the HTML comment started, the leading `<`. * @param int $search_end Last offset in which to search, for limiting search span. * @return int Offset after the current HTML comment ends, or `$end` if no end was found. */ private static function find_html_comment_end( string $text, int $comment_starting_at, int $search_end ): int { // Find span-of-dashes comments which look like `<!----->`. $span_of_dashes = strspn( $text, '-', $comment_starting_at + 2 ); if ( $comment_starting_at + 2 + $span_of_dashes < $search_end && '>' === $text[ $comment_starting_at + 2 + $span_of_dashes ] ) { return $comment_starting_at + $span_of_dashes + 1; } // Otherwise, there are other characters inside the comment, find the first `-->` or `--!>`. $now_at = $comment_starting_at + 4; while ( $now_at < $search_end ) { $dashes_at = strpos( $text, '--', $now_at ); if ( false === $dashes_at ) { static::$last_error = self::INCOMPLETE_INPUT; return $search_end; } $closer_must_be_at = $dashes_at + 2 + strspn( $text, '-', $dashes_at + 2 ); if ( $closer_must_be_at < $search_end && '!' === $text[ $closer_must_be_at ] ) { $closer_must_be_at++; } if ( $closer_must_be_at < $search_end && '>' === $text[ $closer_must_be_at ] ) { return $closer_must_be_at + 1; } $now_at++; } return $search_end; } /** * Creates a pair of delimiters for freeform text content * since there are no delimiters in a document for them. * * @param string $text Source document. * @param int $at Where the text region starts (byte offset). * @param int $length How long the text region spans (byte count). * @return static[] Opening and closing block delimiters for the text region. */ private static function freeform_pair( string $text, int $at, int $length ): array { $opener = new static(); $opener->source_text = $text; $opener->delimiter_at = $at; $opener->delimiter_length = 0; $opener->namespace_at = $at; $opener->namespace_length = 0; $opener->name_at = $at; $opener->name_length = 0; $opener->json_at = $at; $opener->json_length = 0; $opener->type = static::OPENER; $opener->has_void_flag = false; $closer = clone $opener; $end_at = $at + $length; $closer->delimiter_at = $end_at; $closer->namespace_at = $end_at; $closer->name_at = $end_at; $closer->json_at = $end_at; $closer->type = static::CLOSER; return [ $opener, $closer ]; } /** * Indicates if the last attempt to parse a block comment delimiter * failed, if set, otherwise `null` if the last attempt succeeded. * * @return string|null */ public static function get_last_error() { return static::$last_error; } /** * Indicates if the last attempt to parse a block’s JSON attributes failed. * * @see JSON_ERROR_NONE, JSON_ERROR_DEPTH, etc… * * @return int JSON_ERROR_ code from last attempt to parse block JSON attributes. */ public function get_last_json_error(): int { return $this->last_json_error; } /** * Allocates a substring from the source text containing the delimiter * and releases the reference to the source text. * * Use this function when the delimiter is holding on to the source * text and preventing it from being freed by PHP. This function incurs * a string allocation; if the source text will be retained anyway then * there's no need to detach as that memory cannot be freed. * * This is a low-level function available for controlling the performance * of sensitive hot-paths. You probably don't need this. * * Example: * * function first_block( $html ) { * return Block_Delimiter::next_delimiter( $really_long_html, 0 ); * } * * $delimiter = first_block( $really_long_html_document ); * // `$really_long_html_document` is still retained inside `$delimiter`, which could lead to a memory leak. * * $delimiter->allocate_and_detach_from_source_text(); * // `$really_long_html_document` is no longer referenced, and its memory may be freed or used for something else. * * @return void */ public function allocate_and_detach_from_source_text(): void { $this->source_text = substr( $this->source_text, $this->delimiter_at, $this->delimiter_length ); $byte_delta = $this->delimiter_at; $this->delimiter_at -= $byte_delta; $this->namespace_at -= $byte_delta; $this->name_at -= $byte_delta; $this->json_at -= $byte_delta; } /** * Returns the type of the block comment delimiter. * * One of: * * - `static::OPENER` * - `static::CLOSER` * - `static::VOID` * * @return string type of the block comment delimiter. */ public function get_delimiter_type(): string { return $this->type; } /** * Returns whether the delimiter contains the void flag. * * This should be avoided except in cases of handling errors with * block closers containing the void flag. For normative use, * {@see self::get_delimiter_type}. * * @return bool */ public function has_void_flag(): bool { return $this->has_void_flag; } /** * Indicates if the block delimiter represents a block of the given type. * * Since the "core" namespace may be implicit, it's allowable to pass * either the fully-qualified block type with namespace and block name * as well as the shorthand version only containing the block name, if * the desired block is in the "core" namespace. * * Example: * * $is_core_paragraph = $delimiter->is_block_type( 'paragraph' ); * $is_core_paragraph = $delimiter->is_block_type( 'core/paragraph' ); * $is_formula = $delimiter->is_block_type( 'math-block/formula' ); * * @param string $block_type Block type name for the desired block. * E.g. "paragraph", "core/paragraph", "math-blocks/formula". * @return bool Whether this delimiter represents a block of the given type. */ public function is_block_type( string $block_type ): bool { // This is a core/freeform text block, it’s special. if ( 0 === $this->name_length ) { return 'core/freeform' === $block_type || 'freeform' === $block_type; } $slash_at = strpos( $block_type, '/' ); if ( false === $slash_at ) { $namespace = 'core'; $block_name = $block_type; } else { $namespace = substr( $block_type, 0, $slash_at ); $block_name = substr( $block_type, $slash_at + 1 ); } // Only the 'core' namespace is allowed to be omitted. if ( 0 === $this->namespace_length && 'core' !== $namespace ) { return false; } // If given an explicit namespace, they must match. if ( 0 !== $this->namespace_length && ( strlen( $namespace ) !== $this->namespace_length || 0 !== substr_compare( $this->source_text, $namespace, $this->namespace_at, $this->namespace_length ) ) ) { return false; } // The block name must match. return ( strlen( $block_name ) === $this->name_length && 0 === substr_compare( $this->source_text, $block_name, $this->name_at, $this->name_length ) ); } /** * Allocates a substring for the block type and returns the * fully-qualified name, including the namespace. * * This function allocates a substring for the given block type. This * allocation will be small and likely fine in most cases, but it's * preferable to call {@link static::is_block_type} if only needing * to know whether the delimiter is for a given block type, as that * function is more efficient for this purpose and avoids the allocation. * * Example: * * 'core/paragraph' = $delimiter->allocate_and_return_block_type(); * * @return string Fully-qualified block namespace and type, e.g. "core/paragraph". */ public function allocate_and_return_block_type(): string { // This is a core/freeform text block, it’s special. if ( 0 === $this->name_length ) { return 'core/freeform'; } // This is implicitly in the "core" namespace. if ( 0 === $this->namespace_length ) { $block_name = substr( $this->source_text, $this->name_at, $this->name_length ); return "core/{$block_name}"; } return substr( $this->source_text, $this->namespace_at, $this->namespace_length + $this->name_length + 1 ); } /** * Returns a lazy wrapper around the block attributes, which can be used * for efficiently interacting with the JSON attributes. * * @throws Exception This function is not yet implemented. * * @todo Create a lazy JSON wrapper so specific attributes can be * efficiently queried without parsing everything and loading * the entire object into memory. * @todo After realistic benchmarking, see if JsonStreamingParser\Parser * could be used — it would need to be fast enough for the reduction * in memory use to be worth it, compared to {@see \json_decode}. * * @see \JsonStreamingParser\Parser * * @return never */ public function get_attributes(): void { throw new Exception( 'Lazy attribute parsing not yet supported' ); } /** * Attempts to parse and return the entire JSON attributes from the delimiter, * allocating memory and processing the JSON span in the process. * * This does not return any parsed attributes for a closing block delimiter * even if there is a span of JSON content; this JSON is a parsing error. * * Consider calling {@link static::get_attributes} instead if it's not * necessary to read all the attributes at the same time, as that provides * a more efficient mechanism for typical use cases. * * Since the JSON span inside the comment delimiter may not be valid JSON, * this function will return `null` if it cannot parse the span and set the * {@see static::get_last_json_error} to the appropriate JSON_ERROR_ constant. * * If the delimiter contains no JSON span, it will also return `null`, * but the last error will be set to {@see JSON_ERROR_NONE}. * * Example: * * $delimiter = Block_Delimiter::next_delimiter( '<!-- wp:image {"url": "https://wordpress.org/favicon.ico"} -->', 0 ); * $memory_hungry_and_slow_attributes = $delimiter->allocate_and_return_parsed_attributes(); * $memory_hungry_and_slow_attributes === array( 'url' => 'https://wordpress.org/favicon.ico' ); * * $delimiter = Block_Delimiter::next_delimiter( '<!-- /wp:image {"url": "https://wordpress.org/favicon.ico"} -->', 0 ); * null = $delimiter->allocate_and_return_parsed_attributes(); * JSON_ERROR_NONE = $delimiter->get_last_json_error(); * * $delimiter = Block_Delimiter::next_delimiter( '<!-- wp:separator {} /-->', 0 ); * array() === $delimiter->allocate_and_return_parsed_attributes(); * * $delimiter = Block_Delimiter::next_delimiter( '<!-- wp:separator /-->', 0 ); * null = $delimiter->allocate_and_return_parsed_attributes(); * * $delimiter = Block_Delimiter::next_delimiter( '<!-- wp:image {"url} -->', 0 ); * null = $delimiter->allocate_and_return_parsed_attributes(); * JSON_ERROR_CTRL_CHAR = $delimiter->get_last_json_error(); * * @return array|null Parsed JSON attributes, if present and valid, otherwise `null`. */ public function allocate_and_return_parsed_attributes(): ?array { $this->last_json_error = JSON_ERROR_NONE; if ( static::CLOSER === $this->type ) { return null; } if ( 0 === $this->json_length ) { return null; } $json_span = substr( $this->source_text, $this->json_at, $this->json_length ); $parsed = json_decode( $json_span, null, 512, JSON_OBJECT_AS_ARRAY | JSON_INVALID_UTF8_SUBSTITUTE ); $last_error = json_last_error(); $this->last_json_error = $last_error; return ( JSON_ERROR_NONE === $last_error && is_array( $parsed ) ) ? $parsed : null; } // Debugging methods not meant for production use. /** * Prints a debugging message showing the structure of the parsed delimiter. * * This is not meant to be used in production! * * @access private */ public function debug_print_structure(): void { $c = ( ! defined( 'STDOUT' ) || posix_isatty( STDOUT ) ) ? function ( $color = null ) { return $color; } // phpcs:ignore : function ( $color ) { return ''; }; // phpcs:ignore if ( $this->is_block_type( 'core/freeform' ) ) { $closer = static::CLOSER === $this->get_delimiter_type() ? '/' : ''; echo "{$c( "\e[90m" )}<!-- "; // phpcs:ignore echo "{$c( "\e[0;31m" )}{$closer}"; // phpcs:ignore echo "{$c("\e[90m" )}wp:"; // phpcs:ignore echo "{$c( "\e[0;34m" )}freeform"; // phpcs:ignore echo "{$c( "\e[0;36m" )} {$c("\e[90m")}-->\n"; // phpcs:ignore return; } $namespace = substr( $this->source_text, $this->namespace_at, $this->namespace_length ); $slash = 0 === $this->namespace_length ? '' : '/'; $block_name = substr( $this->source_text, $this->name_at, $this->name_length ); $closer = static::CLOSER === $this->type ? '/' : ''; $json = substr( $this->source_text, $this->json_at, $this->json_length ); $opener_whitespace_at = $this->delimiter_at + 4; $opener_whitespace_length = $this->namespace_at - 3 - $opener_whitespace_at - ( static::CLOSER === $this->type ? 1 : 0 ); $after_name_whitespace_at = $this->name_at + $this->name_length; $after_name_whitespace_length = $this->json_at - $after_name_whitespace_at; $closing_whitespace_at = $this->json_at + $this->json_length; $closing_whitespace_length = $this->delimiter_at + $this->delimiter_length - 3 - $closing_whitespace_at; if ( '/' === $this->source_text[ $this->delimiter_at + $this->delimiter_length - 4 ] ) { $void_flag = '/'; --$closing_whitespace_length; } else { $void_flag = ''; } $w = function ( $whitespace ) use ( $c ) { return $c( "\e[2;90m" ) . str_replace( array( ' ', "\t", "\f", "\r", "\n" ), array( '␣', '␉', '␌', '␍', '' ), $whitespace ); }; echo "{$c( "\e[90m" )}<!--"; // phpcs:ignore echo $w( substr( $this->source_text, $opener_whitespace_at, $opener_whitespace_length ) ); // phpcs:ignore echo "{$c( "\e[0;31m" )}{$closer}"; // phpcs:ignore echo "{$c("\e[90m" )}wp:{$c( "\e[2;34m" )}{$namespace}"; // phpcs:ignore echo "{$c( "\e[2;90m" )}{$slash}"; // phpcs:ignore echo "{$c( "\e[0;34m" )}{$block_name}"; // phpcs:ignore echo $w( substr( $this->source_text, $after_name_whitespace_at, $after_name_whitespace_length ) ); // phpcs:ignore echo "{$c("\e[0;2;32m" )}{$json}"; // phpcs:ignore echo $w( substr( $this->source_text, $closing_whitespace_at, $closing_whitespace_length ) ); // phpcs:ignore echo "{$c( "\e[0;36m" )}{$void_flag}{$c("\e[90m")}-->\n"; // phpcs:ignore } // Constant declarations that would otherwise pollute the top of the class. /** * Indicates that the block comment delimiter closes an open block. */ const CLOSER = 'closer'; /** * Indicates that the parser started parsing a block comment delimiter, but * the input document ended before it could finish. The document was likely truncated. */ const INCOMPLETE_INPUT = 'incomplete-input'; /** * Indicates that the block comment delimiter opens a block. */ const OPENER = 'opener'; /** * Indicates that the parser has not yet attempted to parse a block comment delimiter. */ const UNINITIALIZED = 'uninitialized'; /** * Indicates that the block comment delimiter represents a void block * with no inner content of any kind. */ const VOID = 'void'; } block-delimiter/src/class-block-scanner.php 0000777 00000073345 15251741406 0014766 0 ustar 00 <?php /** * Efficiently scan through block structure in document without parsing * the entire block tree and all of its JSON attributes into memory. * * @package automattic/block-delimiter */ declare( strict_types = 1 ); namespace Automattic; use Exception; use WP_HTML_Span; /** * Class for efficiently scanning through block structure in a document * without parsing the entire block tree and JSON attributes into memory. * * This class follows design values of the HTML API: * - minimize allocations and strive for zero memory overhead * - make costs explicit; pay only for what you need * - follow a streaming, re-entrant design for pausing and aborting * * For usage, jump straight to {@see self::next_delimiter}. */ class Block_Scanner { /** * Indicates if the last operation failed, otherwise * will be `null` for success. * * @var string|null */ private $last_error = null; /** * Indicates failures from decoding JSON attributes. * * @var int */ private $last_json_error = JSON_ERROR_NONE; /** * Holds a reference to the original source text from which to * extract the parsed spans of the delimiter. * * @var string */ private $source_text; /** * Byte offset into source text where entire delimiter begins. * * @var int */ private $delimiter_at = 0; /** * Byte length of full span of delimiter. * * @var int */ private $delimiter_length = 0; /** * Byte offset where namespace span begins. * * @var int */ private $namespace_at = 0; /** * Byte length of namespace span, or `0` if implicitly in the "core" namespace. * * @var int */ private $namespace_length = 0; /** * Byte offset where block name span begins. * * @var int */ private $name_at = 0; /** * Byte length of block name span. * * @var int */ private $name_length = 0; /** * Whether the delimiter contains the block self-closing flag. * * This may be erroneous if present within a block closer, * therefore the {@see self::has_void_flag} can be used by * calling code to perform appropriate error-handling. * * @var bool */ private $has_void_flag = false; /** * Byte offset where JSON attributes span begins. * * @var int */ private $json_at; /** * Byte length of JSON attributes span, or `0` if none are present. * * @var int */ private $json_length; /** * Indicates what kind of block comment delimiter this represents. * * One of: * * - `static::OPENER` If the delimiter is opening a block. * - `static::CLOSER` If the delimiter is closing an open block. * - `static::VOID` If the delimiter represents a void block with no inner content. * * If a parsed comment delimiter contains both the closing and the void * flags then it will be interpreted as a void block to match the behavior * of the official block parser, however, this is a mistake and probably * the block ought to close an open block of the same name, if one is open. * * @var string */ private $type; /** * Creates a new block scanner. * * Example: * * $scanner = Block_Scanner::create( $html ); * while ( $scanner->next_delimiter() ) { * if ( $scanner->opens_block( 'core/image' ) ) { * echo "Found an image!\n"; * } * } * * This function is currently a stub so that future improvements can add configuration * options and reject creation, which cannot occur directly inside class constructors. * * @see self::next_delimiter * * @param string $source_text Input document potentially containing block content. * @return ?self Created block scanner, if successfully created. */ public static function create( string $source_text ): ?self { return new self( $source_text ); } /** * Scan to the next block delimiter in a document, indicating if one was found. * * Block comment delimiters must be valid HTML comments and may contain JSON. * This search does not determine, however, if the JSON is valid. * * Example delimiters: * * `<!-- wp:paragraph {"dropCap": true} -->` * `<!-- wp:separator /-->` * `<!-- /wp:paragraph -->` * * In the case that a block comment delimiter contains both the void indicator and * also the closing indicator, it will be treated as a void block. * * Example: * * // Find all image block opening delimiters. * $images = array(); * $scanner = Block_Scanner::create( $html ); * while ( $scanner->next_delimiter() ) { * if ( $scanner->opens_block( 'core/image' ) ) { * $images[] = $scanner->get_span(); * } * } * * Not all blocks have explicit delimiters. Non-block content at the top-level of * a document (so-called “HTML soup”) forms implicit blocks containing neither a * block name nor block attributes. Because this content often comprises only * HTML whitespace and adds undo performance burden, it is skipped by default. * To scan the implicit freeform blocks, pass the `$freeform_blocks` argument. * * Example: * * $html = '<!-- wp:void /-->\n<!-- wp:void /-->'; * $blocks = [ * [ 'blockName' => 'core/void' ], * [ 'blockName' => null ], * [ 'blockName' => 'core/void' ], * ]; * $scanner = Block_Scanner::create( $html ); * while ( $scanner->next_delimiter( freeform_blocks: 'visit' ) { * ... * } * * In some cases it may be useful to conditionally visit the implicit freeform * blocks, such as when determining if a post contains freeform content that * isn’t purely whitespace. * * Example: * * $seen_block_types = []; * $freeform_blocks = 'visit'; * $scanner = Block_Scanner::create( $html ); * while ( $scanner->next_delimiter( freeform_blocks: $freeform_blocks ) { * if ( ! $scanner->opens_block() ) { * continue; * } * * // Stop wasting time visiting freeform blocks after one has been found. * if ('visit' === $freeform_blocks ) { * if ( $scanner->is_non_whitespace_freeform() ) { * $freeform_blocks = 'skip'; * $seen_block_types['core/freeform'] = true; * } * continue; * } * * $seen_block_types[ $scanner->get_block_type() ] = true; * } * * @param string $freeform_blocks Optional. Pass `visit` to match freeform HTML content * not surrounded by block delimiters. Defaults to `skip`. * @return bool Whether a block delimiter was matched. */ public function next_delimiter( string $freeform_blocks = 'skip' ): bool { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable if ( $this->last_error ) { return false; } $text = $this->source_text; $end = strlen( $text ); $at = $this->delimiter_at + $this->delimiter_length; $found_one = false; while ( $at < $end ) { /* * Find the next possible opening. * * This follows the behavior in the official block parser, which treats a post * as a list of blocks with nested HTML. If HTML comment syntax appears within * an HTML attribute value, SCRIPT or STYLE element, or in other select places, * which it can do inside of HTML, then the block parsing may break. * * For a more robust parse scan through the document with the HTML API. In * practice, this has not been a problem in the entire history of blocks. */ $comment_opening_at = strpos( $text, '<!--', $at ); if ( false === $comment_opening_at ) { return false; } $opening_whitespace_at = $comment_opening_at + 4; $opening_whitespace_length = strspn( $text, " \t\f\r\n", $opening_whitespace_at ); if ( 0 === $opening_whitespace_length ) { $at = $this->find_html_comment_end( $comment_opening_at, $end ); continue; } $wp_prefix_at = $opening_whitespace_at + $opening_whitespace_length; if ( $wp_prefix_at >= $end ) { $this->last_error = self::INCOMPLETE_INPUT; return false; } $has_closer = false; if ( '/' === $text[ $wp_prefix_at ] ) { $has_closer = true; ++$wp_prefix_at; } if ( 0 !== substr_compare( $text, 'wp:', $wp_prefix_at, 3 ) ) { $at = $this->find_html_comment_end( $comment_opening_at, $end ); continue; } $namespace_at = $wp_prefix_at + 3; if ( $namespace_at >= $end ) { $this->last_error = self::INCOMPLETE_INPUT; return false; } $start_of_namespace = $text[ $namespace_at ]; // The namespace must start with a-z. if ( 'a' > $start_of_namespace || 'z' < $start_of_namespace ) { $at = $this->find_html_comment_end( $comment_opening_at, $end ); continue; } $namespace_length = 1 + strspn( $text, 'abcdefghijklmnopqrstuvwxyz0123456789-_', $namespace_at + 1 ); $separator_at = $namespace_at + $namespace_length; if ( $separator_at >= $end ) { $this->last_error = self::INCOMPLETE_INPUT; return false; } $has_separator = '/' === $text[ $separator_at ]; if ( $has_separator ) { $name_at = $separator_at + 1; $start_of_name = $text[ $name_at ]; if ( 'a' > $start_of_name || 'z' < $start_of_name ) { $at = $this->find_html_comment_end( $comment_opening_at, $end ); continue; } $name_length = 1 + strspn( $text, 'abcdefghijklmnopqrstuvwxyz0123456789-_', $name_at + 1 ); } else { $name_at = $namespace_at; $name_length = $namespace_length; $namespace_length = 0; } $after_name_whitespace_at = $name_at + $name_length; $after_name_whitespace_length = strspn( $text, " \t\f\r\n", $after_name_whitespace_at ); if ( 0 === $after_name_whitespace_length ) { $at = $this->find_html_comment_end( $comment_opening_at, $end ); continue; } $json_at = $after_name_whitespace_at + $after_name_whitespace_length; if ( $json_at >= $end ) { $this->last_error = self::INCOMPLETE_INPUT; return false; } $has_json = '{' === $text[ $json_at ]; $json_length = 0; /* * For the final span of the delimiter it's most efficient to find the end * of the HTML comment and work backwards. This prevents complicated parsing * inside the JSON span, which cannot contain the HTML comment terminator. * * This also matches the behavior in the official block parser, though it * allows for matching invalid JSON content. */ $comment_closing_at = strpos( $text, '-->', $json_at ); if ( false === $comment_closing_at ) { $this->last_error = self::INCOMPLETE_INPUT; return false; } /* * It looks like this logic leaves an error in here, when the position * overlaps the JSON or block name. However, for neither of those is it * possible to parse a valid block if that last overlapping character * is the void flag. This, therefore, will be valid regardless of how * the rest of the comment delimiter is written. */ if ( '/' === $text[ $comment_closing_at - 1 ] ) { $has_void_flag = true; $void_flag_length = 1; } else { $has_void_flag = false; $void_flag_length = 0; } /* * If there's no JSON, then the span of text after the name * until the comment closing must be completely whitespace. */ if ( ! $has_json ) { $max_whitespace_length = $comment_closing_at - $json_at - $void_flag_length; // This shouldn't be possible, but it can't be allowed regardless. if ( $max_whitespace_length < 0 ) { $at = $this->find_html_comment_end( $comment_opening_at, $end ); continue; } $closing_whitespace_length = strspn( $text, " \t\f\r\n", $json_at, $comment_closing_at - $json_at - $void_flag_length ); if ( 0 === $after_name_whitespace_length + $closing_whitespace_length ) { $at = $this->find_html_comment_end( $comment_opening_at, $end ); continue; } // This must be a block delimiter! $found_one = true; break; } // There's no JSON, so attempt to find its boundary. $after_json_whitespace_length = 0; for ( $char_at = $comment_closing_at - $void_flag_length - 1; $char_at > $json_at; $char_at-- ) { $char = $text[ $char_at ]; switch ( $char ) { case ' ': case "\t": case "\f": case "\r": case "\n": ++$after_json_whitespace_length; continue 2; case '}': $json_length = $char_at - $json_at + 1; break 2; default: ++$at; continue 3; } } if ( 0 === $json_length || 0 === $after_json_whitespace_length ) { $at = $this->find_html_comment_end( $comment_opening_at, $end ); continue; } // This must be a block delimiter! $found_one = true; break; } if ( ! $found_one ) { return false; } $this->delimiter_at = $comment_opening_at; $this->delimiter_length = $comment_closing_at + 3 - $comment_opening_at; $this->namespace_at = $namespace_at; $this->namespace_length = $namespace_length; $this->name_at = $name_at; $this->name_length = $name_length; $this->json_at = $json_at; $this->json_length = $json_length; $this->type = $has_closer ? static::CLOSER : ( $has_void_flag ? static::VOID : static::OPENER ); $this->has_void_flag = $has_void_flag; return true; } /** * Constructor function. * * @param string $source_text Input document potentially containing block content. */ private function __construct( string $source_text ) { $this->source_text = $source_text; } /** * Returns the byte-offset after the ending character of an HTML comment, * assuming the proper starting byte offset. * * @param int $comment_starting_at Where the HTML comment started, the leading `<`. * @param int $search_end Last offset in which to search, for limiting search span. * @return int Offset after the current HTML comment ends, or `$end` if no end was found. */ private function find_html_comment_end( int $comment_starting_at, int $search_end ): int { $text = $this->source_text; // Find span-of-dashes comments which look like `<!----->`. $span_of_dashes = strspn( $text, '-', $comment_starting_at + 2 ); if ( $comment_starting_at + 2 + $span_of_dashes < $search_end && '>' === $text[ $comment_starting_at + 2 + $span_of_dashes ] ) { return $comment_starting_at + $span_of_dashes + 1; } // Otherwise, there are other characters inside the comment, find the first `-->` or `--!>`. $now_at = $comment_starting_at + 4; while ( $now_at < $search_end ) { $dashes_at = strpos( $text, '--', $now_at ); if ( false === $dashes_at ) { $this->last_error = self::INCOMPLETE_INPUT; return $search_end; } $closer_must_be_at = $dashes_at + 2 + strspn( $text, '-', $dashes_at + 2 ); if ( $closer_must_be_at < $search_end && '!' === $text[ $closer_must_be_at ] ) { $closer_must_be_at++; } if ( $closer_must_be_at < $search_end && '>' === $text[ $closer_must_be_at ] ) { return $closer_must_be_at + 1; } $now_at++; } return $search_end; } /** * Indicates if the last attempt to parse a block comment delimiter * failed, if set, otherwise `null` if the last attempt succeeded. * * @return string|null */ public function get_last_error() { return $this->last_error; } /** * Indicates if the last attempt to parse a block’s JSON attributes failed. * * @see JSON_ERROR_NONE, JSON_ERROR_DEPTH, etc… * * @return int JSON_ERROR_ code from last attempt to parse block JSON attributes. */ public function get_last_json_error(): int { return $this->last_json_error; } /** * Returns the type of the block comment delimiter. * * One of: * * - `static::OPENER` * - `static::CLOSER` * - `static::VOID` * * @return string type of the block comment delimiter. */ public function get_delimiter_type(): string { return $this->type; } /** * Returns whether the delimiter contains the void flag. * * This should be avoided except in cases of handling errors with * block closers containing the void flag. For normative use, * {@see self::get_delimiter_type}. * * @return bool */ public function has_void_flag(): bool { return $this->has_void_flag; } /** * Indicates if the block delimiter represents a block of the given type. * * Since the "core" namespace may be implicit, it's allowable to pass * either the fully-qualified block type with namespace and block name * as well as the shorthand version only containing the block name, if * the desired block is in the "core" namespace. * * Example: * * $is_core_paragraph = $scanner->is_block_type( 'paragraph' ); * $is_core_paragraph = $scanner->is_block_type( 'core/paragraph' ); * $is_formula = $scanner->is_block_type( 'math-block/formula' ); * * @param string $block_type Block type name for the desired block. * E.g. "paragraph", "core/paragraph", "math-blocks/formula". * @return bool Whether this delimiter represents a block of the given type. */ public function is_block_type( string $block_type ): bool { // This is a core/freeform text block, it’s special. if ( 0 === $this->name_length ) { return 'core/freeform' === $block_type || 'freeform' === $block_type; } $slash_at = strpos( $block_type, '/' ); if ( false === $slash_at ) { $namespace = 'core'; $block_name = $block_type; } else { // @todo Get lengths but avoid the allocation, use substr_compare below. $namespace = substr( $block_type, 0, $slash_at ); $block_name = substr( $block_type, $slash_at + 1 ); } // Only the 'core' namespace is allowed to be omitted. if ( 0 === $this->namespace_length && 'core' !== $namespace ) { return false; } // If given an explicit namespace, they must match. if ( 0 !== $this->namespace_length && ( strlen( $namespace ) !== $this->namespace_length || 0 !== substr_compare( $this->source_text, $namespace, $this->namespace_at, $this->namespace_length ) ) ) { return false; } // The block name must match. return ( strlen( $block_name ) === $this->name_length && 0 === substr_compare( $this->source_text, $block_name, $this->name_at, $this->name_length ) ); } /** * Indicates if the matched delimiter is an opening or void delimiter * (i.e. it opens the block) of the given type, if a type is provided. * * This is a helper method to ease handling of code inspecting where * blocks start, and of checking if the blocks are of a given type. * The function is variadic to allow for checking if the delimiter * opens one of many possible block types. * * Example: * * $scanner = Block_Scanner::create( $html ); * while ( $scanner->next_delimiter() ) { * if ( $scanner->opens_block( 'core/code', 'syntaxhighlighter/code' ) ) { * echo "Found code!"; * continue; * } * * if ( $scanner->opens_block( 'core/image' ) ) { * echo "Found an image!"; * continue; * } * * if ( $scanner->opens_block() ) { * echo "Found a new block!"; * } * } * * @see self::is_block_type * * @param string|null ...$block_type Optional. Is the matched block type one of these? * If none are provided, will not test block type. * @return bool Whether the matched block delimiter opens a block, and whether it * opens a block of one of the given block types, if provided. */ public function opens_block( ...$block_type ): bool { if ( static::CLOSER === $this->type ) { return false; } if ( count( $block_type ) === 0 ) { return true; } foreach ( $block_type as $block ) { if ( $this->is_block_type( $block ) ) { return true; } } return false; } /** * Indicates if the matched delimiter is implied due to top-level * non-block content in the post. * * @see self::is_non_whitespace_freeform * * @return bool Whether or not the matched delimiter is implied as `core/freeform`. */ public function is_freeform(): bool { return 0 === $this->name_length; } /** * Indicates if the matched delimiter is implicit and surrounding * top-level non-block content that contains non-whitespace text. * * Many block serializers introduce newlines between block delimiters, * so the presence of top-level non-block content does not imply that * there are “real” freeform HTML blocks. Checking if there is content * beyond whitespace is a more certain check, such as for determining * whether to load CSS for the freeform or fallback block type. * * @see self::is_freeform * * @return bool */ public function is_non_whitespace_freeform(): bool { if ( 0 !== $this->name_length ) { return false; } // For now, return false as this method is not yet fully implemented. // @todo Implement logic to check if freeform content contains non-whitespace text. return false; } /** * Allocates a substring for the block type and returns the * fully-qualified name, including the namespace. * * This function allocates a substring for the given block type. This * allocation will be small and likely fine in most cases, but it's * preferable to call {@link self::is_block_type} if only needing * to know whether the delimiter is for a given block type, as that * function is more efficient for this purpose and avoids the allocation. * * Example: * * // Avoid. * 'core/paragraph' = $scanner->get_block_type(); * * // Prefer. * $scanner->is_block_type( 'core/paragraph' ); * $scanner->is_block_type( 'paragraph' ); * * @return string Fully-qualified block namespace and type, e.g. "core/paragraph". */ public function get_block_type(): string { // This is a core/freeform text block, it’s special. if ( 0 === $this->name_length ) { return 'core/freeform'; } // This is implicitly in the "core" namespace. if ( 0 === $this->namespace_length ) { $block_name = substr( $this->source_text, $this->name_at, $this->name_length ); return "core/{$block_name}"; } return substr( $this->source_text, $this->namespace_at, $this->namespace_length + $this->name_length + 1 ); } /** * Returns a lazy wrapper around the block attributes, which can be used * for efficiently interacting with the JSON attributes. * * @throws Exception This function is not yet implemented. * * @todo Create a lazy JSON wrapper so specific attributes can be * efficiently queried without parsing everything and loading * the entire object into memory. * @todo After realistic benchmarking, see if JsonStreamingParser\Parser * could be used — it would need to be fast enough for the reduction * in memory use to be worth it, compared to {@see \json_decode}. * * @see \JsonStreamingParser\Parser * * @return never */ public function get_attributes(): void { throw new Exception( 'Lazy attribute parsing not yet supported' ); } /** * Attempts to parse and return the entire JSON attributes from the delimiter, * allocating memory and processing the JSON span in the process. * * This does not return any parsed attributes for a closing block delimiter * even if there is a span of JSON content; this JSON is a parsing error. * * Consider calling {@link self::get_attributes} instead if it's not * necessary to read all the attributes at the same time, as that provides * a more efficient mechanism for typical use cases. * * Since the JSON span inside the comment delimiter may not be valid JSON, * this function will return `null` if it cannot parse the span and set the * {@see self::get_last_json_error} to the appropriate JSON_ERROR_ constant. * * If the delimiter contains no JSON span, it will also return `null`, * but the last error will be set to {@see JSON_ERROR_NONE}. * * Example: * * $scanner = Block_Scanner::create( '<!-- wp:image {"url": "https://wordpress.org/favicon.ico"} -->' ); * $scanner->next_delimiter(); * $memory_hungry_and_slow_attributes = $scanner->allocate_and_return_parsed_attributes(); * $memory_hungry_and_slow_attributes === array( 'url' => 'https://wordpress.org/favicon.ico' ); * * $scanner = Block_Scanner::create( '<!-- /wp:image {"url": "https://wordpress.org/favicon.ico"} -->' ); * $scanner->next_delimiter(); * null = $scanner->allocate_and_return_parsed_attributes(); * JSON_ERROR_NONE = $scanner->get_last_json_error(); * * $scanner = Block_Scanner::create( '<!-- wp:separator {} /-->' ); * $scanner->next_delimiter(); * array() === $scanner->allocate_and_return_parsed_attributes(); * * $scanner = Block_Scanner::create( '<!-- wp:separator /-->' ); * $scanner->next_delimiter(); * null = $scanner->allocate_and_return_parsed_attributes(); * * $scanner = Block_Scanner::create( '<!-- wp:image {"url} -->' ); * $scanner->next_delimiter(); * null = $scanner->allocate_and_return_parsed_attributes(); * JSON_ERROR_CTRL_CHAR = $scanner->get_last_json_error(); * * @return array|null Parsed JSON attributes, if present and valid, otherwise `null`. */ public function allocate_and_return_parsed_attributes(): ?array { $this->last_json_error = JSON_ERROR_NONE; if ( static::CLOSER === $this->type ) { return null; } if ( 0 === $this->json_length ) { return null; } $json_span = substr( $this->source_text, $this->json_at, $this->json_length ); $parsed = json_decode( $json_span, null, 512, JSON_OBJECT_AS_ARRAY | JSON_INVALID_UTF8_SUBSTITUTE ); $last_error = json_last_error(); $this->last_json_error = $last_error; return ( JSON_ERROR_NONE === $last_error && is_array( $parsed ) ) ? $parsed : null; } /** * Returns the span representing the currently-matched delimiter, * if matched, otherwise `null`. * * Note that for freeform blocks this will return a span of length * zero, since there is no explicit block delimiter. * * Example: * * $scanner = Block_Scanner::create( '<!-- wp:void /-->' ); * null === $scanner->get_span(); * * $scanner->next_delimiter(); * WP_HTML_Span( 0, 17 ) === $scanner->get_span(); * * @return WP_HTML_Span|null Span of text in source text spanning matched delimiter. */ public function get_span(): ?WP_HTML_Span { return new WP_HTML_Span( $this->delimiter_at, $this->delimiter_length ); } // Debugging methods not meant for production use. /** * Prints a debugging message showing the structure of the parsed delimiter. * * This is not meant to be used in production! * * @access private */ public function debug_print_structure(): void { $c = ( ! defined( 'STDOUT' ) || posix_isatty( STDOUT ) ) ? function ( $color = null ) { return $color; } // phpcs:ignore : function ( $color ) { return ''; }; // phpcs:ignore if ( $this->is_block_type( 'core/freeform' ) ) { $closer = static::CLOSER === $this->get_delimiter_type() ? '/' : ''; echo "{$c( "\e[90m" )}<!-- "; // phpcs:ignore echo "{$c( "\e[0;31m" )}{$closer}"; // phpcs:ignore echo "{$c("\e[90m" )}wp:"; // phpcs:ignore echo "{$c( "\e[0;34m" )}freeform"; // phpcs:ignore echo "{$c( "\e[0;36m" )} {$c("\e[90m")}-->\n"; // phpcs:ignore return; } $namespace = substr( $this->source_text, $this->namespace_at, $this->namespace_length ); $slash = 0 === $this->namespace_length ? '' : '/'; $block_name = substr( $this->source_text, $this->name_at, $this->name_length ); $closer = static::CLOSER === $this->type ? '/' : ''; $json = substr( $this->source_text, $this->json_at, $this->json_length ); $opener_whitespace_at = $this->delimiter_at + 4; $opener_whitespace_length = $this->namespace_at - 3 - $opener_whitespace_at - ( static::CLOSER === $this->type ? 1 : 0 ); $after_name_whitespace_at = $this->name_at + $this->name_length; $after_name_whitespace_length = $this->json_at - $after_name_whitespace_at; $closing_whitespace_at = $this->json_at + $this->json_length; $closing_whitespace_length = $this->delimiter_at + $this->delimiter_length - 3 - $closing_whitespace_at; if ( '/' === $this->source_text[ $this->delimiter_at + $this->delimiter_length - 4 ] ) { $void_flag = '/'; --$closing_whitespace_length; } else { $void_flag = ''; } $w = function ( $whitespace ) use ( $c ) { return $c( "\e[2;90m" ) . str_replace( array( ' ', "\t", "\f", "\r", "\n" ), array( '␣', '␉', '␌', '␍', '' ), $whitespace ); }; echo "{$c( "\e[90m" )}<!--"; // phpcs:ignore echo $w( substr( $this->source_text, $opener_whitespace_at, $opener_whitespace_length ) ); // phpcs:ignore echo "{$c( "\e[0;31m" )}{$closer}"; // phpcs:ignore echo "{$c("\e[90m" )}wp:{$c( "\e[2;34m" )}{$namespace}"; // phpcs:ignore echo "{$c( "\e[2;90m" )}{$slash}"; // phpcs:ignore echo "{$c( "\e[0;34m" )}{$block_name}"; // phpcs:ignore echo $w( substr( $this->source_text, $after_name_whitespace_at, $after_name_whitespace_length ) ); // phpcs:ignore echo "{$c("\e[0;2;32m" )}{$json}"; // phpcs:ignore echo $w( substr( $this->source_text, $closing_whitespace_at, $closing_whitespace_length ) ); // phpcs:ignore echo "{$c( "\e[0;36m" )}{$void_flag}{$c("\e[90m")}-->\n"; // phpcs:ignore } // Constant declarations that would otherwise pollute the top of the class. /** * Indicates that the block comment delimiter closes an open block. */ const CLOSER = 'closer'; /** * Indicates that the parser started parsing a block comment delimiter, but * the input document ended before it could finish. The document was likely truncated. */ const INCOMPLETE_INPUT = 'incomplete-input'; /** * Indicates that the block comment delimiter opens a block. */ const OPENER = 'opener'; /** * Indicates that the parser has not yet attempted to parse a block comment delimiter. */ const UNINITIALIZED = 'uninitialized'; /** * Indicates that the block comment delimiter represents a void block * with no inner content of any kind. */ const VOID = 'void'; } jetpack-status/LICENSE.txt 0000777 00000043760 15251741406 0011357 0 ustar 00 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. jetpack-status/src/class-status.php 0000777 00000031125 15251741406 0013452 0 ustar 00 <?php /** * A status class for Jetpack. * * @package automattic/jetpack-status */ namespace Automattic\Jetpack; use Automattic\Jetpack\Status\Cache; use Automattic\Jetpack\Status\Host; use WPCOM_Masterbar; /** * Class Automattic\Jetpack\Status * * Used to retrieve information about the current status of Jetpack and the site overall. */ class Status { /** * Is Jetpack in offline mode? * * This was formerly called "Development Mode", but sites "in development" aren't always offline/localhost. * * @since 1.3.0 * * @return bool Whether Jetpack's offline mode is active. */ public function is_offline_mode() { $cached = Cache::get( 'is_offline_mode' ); if ( null !== $cached ) { return $cached; } $offline_mode = false; if ( defined( '\\JETPACK_DEV_DEBUG' ) ) { $offline_mode = constant( '\\JETPACK_DEV_DEBUG' ); } elseif ( defined( '\\WP_LOCAL_DEV' ) ) { $offline_mode = constant( '\\WP_LOCAL_DEV' ); } elseif ( $this->is_local_site() ) { $offline_mode = true; } /** * Filters Jetpack's offline mode. * * @see https://jetpack.com/support/offline-mode/ * * @since 1.3.0 * * @param bool $offline_mode Is Jetpack's offline mode active. */ $offline_mode = (bool) apply_filters( 'jetpack_offline_mode', $offline_mode ); if ( ! $offline_mode ) { $offline_mode = (bool) get_option( 'jetpack_offline_mode' ); } Cache::set( 'is_offline_mode', $offline_mode ); return $offline_mode; } /** * Whether this is a system with a multiple networks. * Implemented since there is no core is_multi_network function. * Right now there is no way to tell which network is the dominant network on the system. * * @return boolean */ public function is_multi_network() { global $wpdb; $cached = Cache::get( 'is_multi_network' ); if ( null !== $cached ) { return $cached; } // If we don't have a multi site setup no need to do any more. if ( ! is_multisite() ) { Cache::set( 'is_multi_network', false ); return false; } $num_sites = $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->site}" ); if ( $num_sites > 1 ) { Cache::set( 'is_multi_network', true ); return true; } Cache::set( 'is_multi_network', false ); return false; } /** * Whether the current site is single user site. * * @return bool */ public function is_single_user_site() { global $wpdb; $ret = Cache::get( 'is_single_user_site' ); if ( null === $ret ) { $some_users = get_transient( 'jetpack_is_single_user' ); if ( false === $some_users ) { $some_users = $wpdb->get_var( "SELECT COUNT(*) FROM (SELECT user_id FROM $wpdb->usermeta WHERE meta_key = '{$wpdb->prefix}capabilities' LIMIT 2) AS someusers" ); set_transient( 'jetpack_is_single_user', (int) $some_users, 12 * HOUR_IN_SECONDS ); } $ret = 1 === (int) $some_users; Cache::set( 'is_single_user_site', $ret ); } return $ret; } /** * If the site is a local site. * * @since 1.3.0 * * @return bool */ public function is_local_site() { $cached = Cache::get( 'is_local_site' ); if ( null !== $cached ) { return $cached; } $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. if ( 'local' === wp_get_environment_type() ) { $is_local = true; } // Then check for usual usual domains used by local dev tools. $known_local = array( '#\.local$#i', '#\.localhost$#i', '#\.test$#i', '#\.docksal$#i', // Docksal. '#\.docksal\.site$#i', // Docksal. '#\.dev\.cc$#i', // ServerPress. '#\.lndo\.site$#i', // Lando. '#\.ddev\.site$#i', // DDEV. '#^https?://127\.0\.0\.1$#', ); if ( ! $is_local ) { foreach ( $known_local as $url ) { if ( preg_match( $url, $site_url ) ) { $is_local = true; break; } } } /** * Filters is_local_site check. * * @since 1.3.0 * * @param bool $is_local If the current site is a local site. */ $is_local = apply_filters( 'jetpack_is_local_site', $is_local ); Cache::set( 'is_local_site', $is_local ); return $is_local; } /** * If is a staging site. * * @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; } /* * Core's wp_get_environment_type allows for a few specific options. * We should default to bowing out gracefully for anything other than production or local. */ $is_staging = ! in_array( wp_get_environment_type(), array( 'production', 'local' ), true ); $known_staging = array( 'urls' => array( '#\.staging\.wpengine\.com$#i', // WP Engine. This is their legacy staging URL structure. Their new platform does not have a common URL. https://github.com/Automattic/jetpack/issues/21504 '#\.staging\.kinsta\.com$#i', // Kinsta.com. '#\.kinsta\.cloud$#i', // Kinsta.com. '#\.stage\.site$#i', // DreamPress. '#\.newspackstaging\.com$#i', // Newspack. '#^(?!live-)([a-zA-Z0-9-]+)\.pantheonsite\.io$#i', // Pantheon. '#\.flywheelsites\.com$#i', // Flywheel. '#\.flywheelstaging\.com$#i', // Flywheel. '#\.cloudwaysapps\.com$#i', // Cloudways. '#\.azurewebsites\.net$#i', // Azure. '#\.wpserveur\.net$#i', // WPServeur. '#\-liquidwebsites\.com$#i', // Liquidweb. ), 'constants' => array( 'IS_WPE_SNAPSHOT', // WP Engine. This is used on their legacy staging environment. Their new platform does not have a constant. https://github.com/Automattic/jetpack/issues/21504 'KINSTA_DEV_ENV', // Kinsta.com. 'WPSTAGECOACH_STAGING', // WP Stagecoach. 'JETPACK_STAGING_MODE', // Generic. 'WP_LOCAL_DEV', // Generic. ), ); /** * Filters the flags of known staging sites. * * @since 1.1.1 * @since-jetpack 3.9.0 * * @param array $known_staging { * An array of arrays that each are used to check if the current site is staging. * @type array $urls URLs of staging sites in regex to check against site_url. * @type array $constants PHP constants of known staging/developement environments. * } */ $known_staging = apply_filters( 'jetpack_known_staging', $known_staging ); if ( isset( $known_staging['urls'] ) ) { $site_url = site_url(); foreach ( $known_staging['urls'] as $url ) { if ( preg_match( $url, wp_parse_url( $site_url, PHP_URL_HOST ) ) ) { $is_staging = true; break; } } } if ( isset( $known_staging['constants'] ) ) { foreach ( $known_staging['constants'] as $constant ) { if ( defined( $constant ) && constant( $constant ) ) { $is_staging = true; } } } // Last, let's check if sync is erroring due to an IDC. If so, set the site to staging mode. if ( ! $is_staging && method_exists( 'Automattic\\Jetpack\\Identity_Crisis', 'validate_sync_error_idc_option' ) && \Automattic\Jetpack\Identity_Crisis::validate_sync_error_idc_option() ) { $is_staging = true; } /** * Filters is_staging_site check. * * @since 1.1.1 * @since-jetpack 3.9.0 * * @param bool $is_staging If the current site is a staging site. */ $is_staging = apply_filters( 'jetpack_is_staging_site', $is_staging ); Cache::set( 'is_staging_site', $is_staging ); 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. * * @since-jetpack 5.8 * * @deprecated since 4.0.0 * * @access public * @static * * @return bool True if the site is currently onboarding, false otherwise */ public function is_onboarding() { return \Jetpack_Options::get_option( 'onboarding' ) !== false; } /** * Whether the site is currently private or not. * On WordPress.com and WoA, sites can be marked as private * * @since 1.16.0 * * @return bool True if the site is private. */ public function is_private_site() { $ret = Cache::get( 'is_private_site' ); if ( null === $ret ) { $is_private_site = '-1' === get_option( 'blog_public' ); /** * Filters the is_private_site check. * * @since 1.16.1 * * @param bool $is_private_site True if the site is private. */ $is_private_site = apply_filters( 'jetpack_is_private_site', $is_private_site ); Cache::set( 'is_private_site', $is_private_site ); return $is_private_site; } return $ret; } /** * Whether the site is currently unlaunched or not. * On WordPress.com and WoA, sites can be marked as "coming soon", aka unlaunched * * @since 1.16.0 * * @return bool True if the site is not launched. */ public function is_coming_soon() { $ret = Cache::get( 'is_coming_soon' ); if ( null === $ret ) { $is_coming_soon = ( function_exists( 'site_is_coming_soon' ) && \site_is_coming_soon() ) || get_option( 'wpcom_public_coming_soon' ); /** * Filters the is_coming_soon check. * * @since 1.16.1 * * @param bool $is_coming_soon True if the site is coming soon (i.e. unlaunched). */ $is_coming_soon = apply_filters( 'jetpack_is_coming_soon', $is_coming_soon ); Cache::set( 'is_coming_soon', $is_coming_soon ); return $is_coming_soon; } return $ret; } /** * Returns the site slug suffix to be used as part of Calypso URLs. * * Strips http:// or https:// from a url, replaces forward slash with ::. * * @since 1.6.0 * * @param string $url Optional. URL to build the site suffix from. Default: Home URL. * * @return string */ public function get_site_suffix( $url = '' ) { // On WordPress.com, site suffixes are a bit different. if ( method_exists( 'WPCOM_Masterbar', 'get_calypso_site_slug' ) ) { return WPCOM_Masterbar::get_calypso_site_slug( get_current_blog_id() ); } // Grab the 'site_url' option for WoA sites to avoid plugins to interfere with the site // identifier (e.g. i18n plugins may change the main url to '<DOMAIN>/<LOCALE>', but we // want to exclude the locale since it's not part of the site suffix). if ( ( new Host() )->is_woa_site() ) { $url = \site_url(); } if ( empty( $url ) ) { // WordPress can be installed in subdirectories (e.g. make.wordpress.org/plugins) // where the 'site_url' option points to the root domain (e.g. make.wordpress.org) // which could collide with another site in the same domain but with WordPress // installed in a different subdirectory (e.g. make.wordpress.org/core). To avoid // such collision, we identify the site with the 'home_url' option. $url = \home_url(); } $url = preg_replace( '#^.*?://#', '', $url ); $url = str_replace( '/', '::', $url ); return rtrim( $url, ':' ); } } jetpack-status/src/class-cookiestate.php 0000777 00000005572 15251741406 0014450 0 ustar 00 <?php /** * Pass state to subsequent requests via cookies. * * @package automattic/jetpack-status */ namespace Automattic\Jetpack; /** * Class Automattic\Jetpack\Status * * Used to retrieve information about the current status of Jetpack and the site overall. */ class CookieState { /** * State is passed via cookies from one request to the next, but never to subsequent requests. * SET: state( $key, $value ); * GET: $value = state( $key ); * * @param string $key State key. * @param string $value Value. * @param bool $restate Reset the cookie (private). */ public function state( $key = null, $value = null, $restate = false ) { static $state = array(); static $path, $domain; if ( ! isset( $path ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; $admin_url = ( new Paths() )->admin_url(); $bits = wp_parse_url( $admin_url ); if ( is_array( $bits ) ) { $path = ( isset( $bits['path'] ) ) ? dirname( $bits['path'] ) : null; $domain = ( isset( $bits['host'] ) ) ? $bits['host'] : null; } else { $path = null; $domain = null; } } // Extract state from cookies and delete cookies. if ( isset( $_COOKIE['jetpackState'] ) && is_array( $_COOKIE['jetpackState'] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- User should sanitize if necessary. $yum = wp_unslash( $_COOKIE['jetpackState'] ); unset( $_COOKIE['jetpackState'] ); foreach ( $yum as $k => $v ) { if ( strlen( $v ) ) { $state[ $k ] = $v; } // @phan-suppress-next-line PhanTypeMismatchArgumentInternal -- Setting cookie to false is valid and documented for deletion. setcookie( "jetpackState[$k]", false, 0, $path, $domain, is_ssl(), true ); } } if ( $restate ) { foreach ( $state as $k => $v ) { setcookie( "jetpackState[$k]", $v, 0, $path, $domain, is_ssl(), true ); } return; } // Get a state variable. if ( isset( $key ) && ! isset( $value ) ) { if ( array_key_exists( $key, $state ) ) { return $state[ $key ]; } return null; } // Set a state variable. if ( isset( $key ) && isset( $value ) ) { if ( is_array( $value ) && isset( $value[0] ) ) { $value = $value[0]; } $state[ $key ] = $value; if ( ! headers_sent() ) { if ( $this->should_set_cookie( $key ) ) { setcookie( "jetpackState[$key]", $value, 0, $path, $domain, is_ssl(), true ); } } } } /** * Determines whether the jetpackState[$key] value should be added to the * cookie. * * @param string $key The state key. * * @return boolean Whether the value should be added to the cookie. */ public function should_set_cookie( $key ) { global $current_screen; $page = isset( $current_screen->base ) ? $current_screen->base : null; if ( 'toplevel_page_jetpack' === $page && 'display_update_modal' === $key ) { return false; } return true; } } jetpack-status/src/class-host.php 0000777 00000012523 15251741406 0013105 0 ustar 00 <?php /** * A hosting provide class for Jetpack. * * @package automattic/jetpack-status */ namespace Automattic\Jetpack\Status; use Automattic\Jetpack\Constants; /** * Hosting provider class. */ class Host { /** * Determine if this site is an WordPress.com on Atomic site or not by looking for presence of the wpcomsh plugin. * * @since 1.9.0 * * @return bool */ public function is_woa_site() { $ret = Cache::get( 'is_woa_site' ); if ( null === $ret ) { $ret = $this->is_atomic_platform() && Constants::is_true( 'WPCOMSH__PLUGIN_FILE' ); Cache::set( 'is_woa_site', $ret ); } return $ret; } /** * Determine if the site is hosted on the Atomic hosting platform. * * @since 1.9.0 * * @return bool */ public function is_atomic_platform() { return Constants::is_true( 'ATOMIC_SITE_ID' ) && Constants::is_true( 'ATOMIC_CLIENT_ID' ); } /** * Determine if this is a Newspack site. * * @return bool */ public function is_newspack_site() { return Constants::is_defined( 'NEWSPACK_PLUGIN_FILE' ); } /** * Determine if this is a VIP-hosted site. * * @return bool */ public function is_vip_site() { return Constants::is_defined( 'WPCOM_IS_VIP_ENV' ) && true === Constants::get_constant( 'WPCOM_IS_VIP_ENV' ); } /** * Determine if this is a Simple platform site. * * @return bool */ public function is_wpcom_simple() { return Constants::is_defined( 'IS_WPCOM' ) && true === Constants::get_constant( 'IS_WPCOM' ); } /** * Determine if this is a WordPress.com site. * * Includes both Simple and WoA platforms. * * @return bool */ public function is_wpcom_platform() { return $this->is_wpcom_simple() || $this->is_woa_site(); } /** * Determine if this is a P2 site. * This covers both P2 and P2020 themes. * * @return bool */ public function is_p2_site() { $site_id = $this->get_wpcom_site_id(); if ( ! $site_id ) { return false; } return str_contains( get_stylesheet(), 'pub/p2' ) || ( function_exists( '\WPForTeams\is_wpforteams_site' ) && \WPForTeams\is_wpforteams_site( $site_id ) ); } /** * Get the current site's WordPress.com ID. * * @return mixed The site's WordPress.com ID. */ public function get_wpcom_site_id() { if ( $this->is_wpcom_simple() ) { return get_current_blog_id(); } elseif ( class_exists( 'Jetpack' ) && \Jetpack::is_connection_ready() ) { return \Jetpack_Options::get_option( 'id' ); } return false; } /** * Add all wordpress.com environments to the safe redirect allowed list. * * To be used with a filter of allowed domains for a redirect. * * @param array $domains Allowed WP.com Environments. */ public static function allow_wpcom_environments( $domains ) { $domains[] = 'wordpress.com'; $domains[] = 'jetpack.wordpress.com'; $domains[] = 'wpcalypso.wordpress.com'; $domains[] = 'horizon.wordpress.com'; $domains[] = 'calypso.localhost'; return $domains; } /** * Return Calypso environment value; used for developing Jetpack and pairing * it with different Calypso environments, such as localhost. * * @since 1.18.0 * * @return string Calypso environment */ public function get_calypso_env() { // phpcs:disable WordPress.Security.NonceVerification.Recommended -- Nonce is not required; only used for changing environments. if ( isset( $_GET['calypso_env'] ) ) { return sanitize_key( $_GET['calypso_env'] ); } // phpcs:enable WordPress.Security.NonceVerification.Recommended if ( getenv( 'CALYPSO_ENV' ) ) { return sanitize_key( getenv( 'CALYPSO_ENV' ) ); } if ( defined( 'CALYPSO_ENV' ) && CALYPSO_ENV ) { return sanitize_key( CALYPSO_ENV ); } return ''; } /** * Return source query param value from the URL if exists in the allowed sources list. * * @return string "source" query param value */ public function get_source_query() { // phpcs:disable WordPress.Security.NonceVerification.Recommended $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 a guess of the hosting provider for the current site based on various checks. * * @since 5.0.4 Added $guess parameter. * @since 6.0.0 Removed $guess parameter. * * @return string */ public function get_known_host_guess() { // 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; } 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; } } jetpack-status/src/class-errors.php 0000777 00000002725 15251741406 0013447 0 ustar 00 <?php /** * An errors utility class for Jetpack. * * @package automattic/jetpack-status */ // phpcs:disable WordPress.PHP.IniSet.display_errors_Disallowed // phpcs:disable WordPress.PHP.NoSilencedErrors.Discouraged // phpcs:disable WordPress.PHP.DevelopmentFunctions.prevent_path_disclosure_error_reporting // phpcs:disable WordPress.PHP.DiscouragedPHPFunctions.runtime_configuration_error_reporting 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 ); if ( $error_reporting !== null ) { @error_reporting( $error_reporting ); } if ( class_exists( 'Jetpack' ) ) { remove_action( 'shutdown', array( 'Jetpack', 'catch_errors_on_shutdown' ), 0 ); } } } } jetpack-status/src/class-visitor.php 0000777 00000003061 15251741406 0013624 0 ustar 00 <?php /** * Status and information regarding the site visitor. * * @package automattic/jetpack-status */ namespace Automattic\Jetpack\Status; /** * Visitor class. */ class Visitor { /** * Gets current user IP address. * * @param bool $check_all_headers Check all headers? Default is `false`. * * @return string Current user IP address. */ public function get_ip( $check_all_headers = false ) { if ( $check_all_headers ) { foreach ( array( 'HTTP_CF_CONNECTING_IP', 'HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED', 'HTTP_X_CLUSTER_CLIENT_IP', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED', 'HTTP_VIA', ) as $key ) { if ( ! empty( $_SERVER[ $key ] ) ) { // @todo Some of these might actually be lists of IPs (e.g. HTTP_X_FORWARDED_FOR) or something else entirely (HTTP_VIA). return filter_var( wp_unslash( $_SERVER[ $key ] ) ); } } } 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 ); } } jetpack-status/src/class-request.php 0000777 00000006214 15251741406 0013620 0 ustar 00 <?php /** * Get information about the current request. * * @package automattic/jetpack-status */ namespace Automattic\Jetpack\Status; use Automattic\Jetpack\Constants; /** * Get information about the current request. */ class Request { /** * Determine whether the current request is for accessing the frontend. * Also update Vary headers to indicate that the response may vary by Accept header. * * @since 6.0.3 Added $send_vary_headers argument. * * @param bool $send_vary_headers Whether to send Vary headers. * * @return bool True if it's a frontend request, false otherwise. */ public static function is_frontend( $send_vary_headers = true ) { $is_frontend = true; $is_varying_request = true; if ( is_admin() || wp_doing_ajax() || wp_is_jsonp_request() || is_feed() || Constants::is_true( 'REST_REQUEST' ) || Constants::is_true( 'REST_API_REQUEST' ) || Constants::is_true( 'WP_CLI' ) || Constants::is_true( 'WPCOM_CLI_SCRIPT' ) // Special case for CLI scripts on WP.com that aren't using WP CLI. ) { $is_frontend = false; $is_varying_request = false; } elseif ( wp_is_json_request() || wp_is_xml_request() ) { $is_frontend = false; } /* * Check existing headers for the request. * If there is no existing Vary Accept header, add one. */ if ( $send_vary_headers && $is_varying_request && ! headers_sent() ) { $headers = headers_list(); $vary_header_parts = self::get_vary_headers( $headers ); header( 'Vary: ' . implode( ', ', $vary_header_parts ) ); } /** * Filter whether the current request is for accessing the frontend. * * @since jetpack-9.0.0 * @since 6.0.3 Added $send_vary_headers argument. * * @param bool $is_frontend Whether the current request is for accessing the frontend. * @param bool $send_vary_headers Whether to send Vary headers. */ return (bool) apply_filters( 'jetpack_is_frontend', $is_frontend, $send_vary_headers ); } /** * Go through headers and get a list of Vary headers to add, * including Vary on Accept and Content-Type if necessary. * * @since jetpack-12.2 * * @param array $headers The headers to be sent. * * @return array $vary_header_parts Vary Headers to be sent. */ public static function get_vary_headers( $headers = array() ) { $vary_header_parts = array( 'accept', 'content-type' ); foreach ( $headers as $header ) { // Check for a Vary header. if ( ! str_starts_with( strtolower( $header ), 'vary:' ) ) { continue; } // If the header is a wildcard, we'll return that. if ( str_contains( $header, '*' ) ) { $vary_header_parts = array( '*' ); break; } // Remove the Vary: part of the header. $header = preg_replace( '/^vary\:\s?/i', '', $header ); // Remove spaces from the header. $header = str_replace( ' ', '', $header ); // Break the header into parts. $header_parts = explode( ',', strtolower( $header ) ); // Build an array with the Accept header and what was already there. $vary_header_parts = array_values( array_unique( array_merge( $vary_header_parts, $header_parts ) ) ); } return $vary_header_parts; } } jetpack-status/src/class-modules.php 0000777 00000047213 15251741406 0013604 0 ustar 00 <?php /** * A modules class for Jetpack. * * @package automattic/jetpack-status */ namespace Automattic\Jetpack; use Automattic\Jetpack\Current_Plan as Jetpack_Plan; use Automattic\Jetpack\IP\Utils as IP_Utils; use Automattic\Jetpack\Status\Host; /** * Class Automattic\Jetpack\Modules * * Used to retrieve information about the current status of Jetpack modules. */ 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, $available_only = true ) { if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) { return true; } return in_array( $module, self::get_active( $available_only ), true ); } /** * Load module data from module file. Headers differ from WordPress * plugin headers to avoid them being identified as standalone * plugins on the WordPress plugins page. * * @param string $module The module slug. */ public function get( $module ) { static $modules_details; // This method relies heavy on auto-generated file found in Jetpack only: module-headings.php // If it doesn't exist, it's safe to assume none of this will be helpful. if ( ! function_exists( 'jetpack_has_no_module_info' ) ) { return false; } if ( jetpack_has_no_module_info( $module ) ) { return false; } $file = $this->get_path( $this->get_slug( $module ) ); if ( isset( $modules_details[ $module ] ) ) { $mod = $modules_details[ $module ]; } else { $mod = jetpack_get_module_info( $module ); if ( null === $mod ) { // Try to get the module info from the file as a fallback. $mod = $this->get_file_data( $file, jetpack_get_all_module_header_names() ); if ( empty( $mod['name'] ) ) { // No info for this module. return false; } } $mod['sort'] = empty( $mod['sort'] ) ? 10 : (int) $mod['sort']; $mod['recommendation_order'] = empty( $mod['recommendation_order'] ) ? 20 : (int) $mod['recommendation_order']; $mod['deactivate'] = empty( $mod['deactivate'] ); $mod['free'] = empty( $mod['free'] ); $mod['requires_connection'] = ( ! empty( $mod['requires_connection'] ) && 'No' === $mod['requires_connection'] ) ? false : true; $mod['requires_user_connection'] = ( empty( $mod['requires_user_connection'] ) || 'No' === $mod['requires_user_connection'] ) ? false : true; if ( empty( $mod['auto_activate'] ) || ! in_array( strtolower( $mod['auto_activate'] ), array( 'yes', 'no', 'public' ), true ) ) { $mod['auto_activate'] = 'No'; } else { $mod['auto_activate'] = (string) $mod['auto_activate']; } if ( $mod['module_tags'] ) { $mod['module_tags'] = explode( ',', $mod['module_tags'] ); $mod['module_tags'] = array_map( 'trim', $mod['module_tags'] ); } else { $mod['module_tags'] = array( 'Other' ); } if ( $mod['plan_classes'] ) { $mod['plan_classes'] = explode( ',', $mod['plan_classes'] ); $mod['plan_classes'] = array_map( 'strtolower', array_map( 'trim', $mod['plan_classes'] ) ); } else { $mod['plan_classes'] = array( 'free' ); } if ( $mod['feature'] ) { $mod['feature'] = explode( ',', $mod['feature'] ); $mod['feature'] = array_map( 'trim', $mod['feature'] ); } else { $mod['feature'] = array( 'Other' ); } $modules_details[ $module ] = $mod; } /** * Filters the feature array on a module. * * This filter allows you to control where each module is filtered: Recommended, * and the default "Other" listing. * * @since-jetpack 3.5.0 * * @param array $mod['feature'] The areas to feature this module: * 'Recommended' shows on the main Jetpack admin screen. * 'Other' should be the default if no other value is in the array. * @param string $module The slug of the module, e.g. sharedaddy. * @param array $mod All the currently assembled module data. */ $mod['feature'] = apply_filters( 'jetpack_module_feature', $mod['feature'], $module, $mod ); /** * Filter the returned data about a module. * * This filter allows overriding any info about Jetpack modules. It is dangerous, * so please be careful. * * @since-jetpack 3.6.0 * * @param array $mod The details of the requested module. * @param string $module The slug of the module, e.g. sharedaddy * @param string $file The path to the module source file. */ return apply_filters( 'jetpack_get_module', $mod, $module, $file ); } /** * Like core's get_file_data implementation, but caches the result. * * @param string $file Absolute path to the file. * @param array $headers List of headers, in the format array( 'HeaderKey' => 'Header Name' ). */ public function get_file_data( $file, $headers ) { // Get just the filename from $file (i.e. exclude full path) so that a consistent hash is generated. $file_name = basename( $file ); if ( ! Constants::is_defined( 'JETPACK__VERSION' ) ) { return get_file_data( $file, $headers ); } $cache_key = 'jetpack_file_data_' . JETPACK__VERSION; $file_data_option = get_transient( $cache_key ); if ( ! is_array( $file_data_option ) ) { delete_transient( $cache_key ); $file_data_option = false; } if ( false === $file_data_option ) { $file_data_option = array(); } $key = md5( $file_name . maybe_serialize( $headers ) ); $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 ] ) ) { return $file_data_option[ $key ]; } $data = get_file_data( $file, $headers ); $file_data_option[ $key ] = $data; set_transient( $cache_key, $file_data_option, 29 * DAY_IN_SECONDS ); return $data; } /** * 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( $available_only = true ) { $active = \Jetpack_Options::get_option( 'active_modules' ); if ( ! is_array( $active ) ) { $active = array(); } if ( class_exists( 'VaultPress' ) || function_exists( 'vaultpress_contact_service' ) ) { $active[] = 'vaultpress'; } else { $active = array_diff( $active, array( 'vaultpress' ) ); } // If protect is active on the main site of a multisite, it should be active on all sites. Doesn't apply to WP.com. if ( ! in_array( 'protect', $active, true ) && ! ( new Host() )->is_wpcom_simple() && is_multisite() && get_site_option( 'jetpack_protect_active' ) ) { $active[] = 'protect'; } 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. * * Gives theme and plugin developers the power to alter the modules that * are activated on the fly. * * @since-jetpack 5.8.0 * * @param array $active Array of active module slugs. */ $active = apply_filters( 'jetpack_active_modules', $active ); return array_unique( $active ); } /** * Extract a module's slug from its full path. * * @param string $file Full path to a file. * * @return string Module slug. */ public function get_slug( $file ) { return str_replace( '.php', '', basename( $file ) ); } /** * List available Jetpack modules. Simply lists .php files in /modules/. * Make sure to tuck away module "library" files in a sub-directory. * * @param bool|string $min_version Only return modules introduced in this version or later. Default is false, do not filter. * @param bool|string $max_version Only return modules introduced before this version. Default is false, do not filter. * @param bool|null $requires_connection Pass a boolean value to only return modules that require (or do not require) a connection. * @param bool|null $requires_user_connection Pass a boolean value to only return modules that require (or do not require) a user connection. * * @return array $modules Array of module slugs */ public function get_available( $min_version = false, $max_version = false, $requires_connection = null, $requires_user_connection = null ) { static $modules = null; if ( ! class_exists( 'Jetpack' ) || ! Constants::is_defined( 'JETPACK__VERSION' ) || ! Constants::is_defined( 'JETPACK__PLUGIN_DIR' ) ) { return array_unique( /** * Stand alone plugins need to use this filter to register the modules they interact with. * This will allow them to activate and deactivate these modules even when Jetpack is not present. * Note: Standalone plugins can only interact with modules that also exist in the Jetpack plugin, otherwise they'll lose the ability to control it if Jetpack is activated. * * @since 1.13.6 * * @param array $modules The list of available modules as an array of slugs. * @param bool $requires_connection Whether to list only modules that require a connection to work. * @param bool $requires_user_connection Whether to list only modules that require a user connection to work. */ apply_filters( 'jetpack_get_available_standalone_modules', array(), $requires_connection, $requires_user_connection ) ); } if ( ! isset( $modules ) ) { $available_modules_option = \Jetpack_Options::get_option( 'available_modules', array() ); // Use the cache if we're on the front-end and it's available... if ( ! is_admin() && ! empty( $available_modules_option[ JETPACK__VERSION ] ) ) { $modules = $available_modules_option[ JETPACK__VERSION ]; } else { $files = ( new Files() )->glob_php( JETPACK__PLUGIN_DIR . 'modules' ); $modules = array(); foreach ( $files as $file ) { $slug = $this->get_slug( $file ); $headers = $this->get( $slug ); if ( ! $headers ) { continue; } $modules[ $slug ] = $headers['introduced']; } \Jetpack_Options::update_option( 'available_modules', array( JETPACK__VERSION => $modules, ) ); } } /** * Filters the array of modules available to be activated. * * @since 2.4.0 * * @param array $modules Array of available modules. * @param string $min_version Minimum version number required to use modules. * @param string $max_version Maximum version number required to use modules. * @param bool|null $requires_connection Value of the Requires Connection filter. * @param bool|null $requires_user_connection Value of the Requires User Connection filter. */ $mods = apply_filters( 'jetpack_get_available_modules', $modules, $min_version, $max_version, $requires_connection, $requires_user_connection ); if ( ! $min_version && ! $max_version && $requires_connection === null && $requires_user_connection === null ) { return array_keys( $mods ); } $r = array(); foreach ( $mods as $slug => $introduced ) { if ( $min_version && version_compare( $min_version, $introduced, '>=' ) ) { continue; } if ( $max_version && version_compare( $max_version, $introduced, '<' ) ) { continue; } $mod_details = $this->get( $slug ); if ( null !== $requires_connection && (bool) $requires_connection !== $mod_details['requires_connection'] ) { continue; } if ( null !== $requires_user_connection && (bool) $requires_user_connection !== $mod_details['requires_user_connection'] ) { continue; } $r[] = $slug; } return $r; } /** * Is slug a valid module. * * @param string $module Module slug. * * @return bool */ public function is_module( $module ) { return ! empty( $module ) && ! validate_file( $module, $this->get_available() ); } /** * Update module status. * * @param string $module - module slug. * @param boolean $active - true to activate, false to deactivate. * @param bool $exit Should exit be called after deactivation. * @param bool $redirect Should there be a redirection after activation. */ public function update_status( $module, $active, $exit = true, $redirect = true ) { return $active ? $this->activate( $module, $exit, $redirect ) : $this->deactivate( $module ); } /** * Activate a module. * * @param string $module Module slug. * @param bool $exit Should exit be called after deactivation. * @param bool $redirect Should there be a redirection after activation. * * @return bool|void */ public function activate( $module, $exit = true, $redirect = true ) { /** * Fires before a module is activated. * * @since 2.6.0 * * @param string $module Module slug. * @param bool $exit Should we exit after the module has been activated. Default to true. * @param bool $redirect Should the user be redirected after module activation? Default to true. */ do_action( 'jetpack_pre_activate_module', $module, $exit, $redirect ); if ( ! strlen( $module ) ) { return false; } // If it's already active, then don't do it again. $active = $this->get_active(); foreach ( $active as $act ) { if ( $act === $module ) { return true; } } if ( ! $this->is_module( $module ) ) { return false; } // Jetpack plugin only if ( class_exists( 'Jetpack' ) ) { $module_data = $this->get( $module ); $status = new Status(); $state = new CookieState(); if ( ! \Jetpack::is_connection_ready() ) { if ( ! $status->is_offline_mode() ) { return false; } // If we're not connected but in offline mode, make sure the module doesn't require a connection. if ( $status->is_offline_mode() && $module_data['requires_connection'] ) { return false; } } if ( class_exists( 'Jetpack_Client_Server' ) ) { $jetpack = \Jetpack::init(); // Check and see if the old plugin is active. if ( isset( $jetpack->plugins_to_deactivate[ $module ] ) ) { // Deactivate the old plugins. $deactivated = array(); foreach ( $jetpack->plugins_to_deactivate[ $module ] as $idx => $deactivate_me ) { if ( \Jetpack_Client_Server::deactivate_plugin( $deactivate_me[0], $deactivate_me[1] ) ) { // If we deactivated the old plugin, remembere that with ::state() and redirect back to this page to activate the module // We can't activate the module on this page load since the newly deactivated old plugin is still loaded on this page load. $deactivated[] = "$module:$idx"; } } if ( $deactivated ) { $state->state( 'deactivated_plugins', implode( ',', $deactivated ) ); wp_safe_redirect( add_query_arg( 'jetpack_restate', 1 ) ); exit( 0 ); } } } // Protect won't work with mis-configured IPs. if ( 'protect' === $module ) { if ( ! IP_Utils::get_ip() ) { $state->state( 'message', 'protect_misconfigured_ip' ); return false; } } if ( ! Jetpack_Plan::supports( $module ) ) { return false; } // Check the file for fatal errors, a la wp-admin/plugins.php::activate. $state->state( 'module', $module ); $state->state( 'error', 'module_activation_failed' ); // we'll override this later if the plugin can be included without fatal error. ob_start(); $module_path = $this->get_path( $module ); if ( file_exists( $module_path ) ) { require_once $this->get_path( $module ); // phpcs:ignore WordPressVIPMinimum.Files.IncludingFile.NotAbsolutePath } $active[] = $module; $this->update_active( $active ); $state->state( 'error', false ); // the override. ob_end_clean(); } else { // Not a Jetpack plugin. $active[] = $module; $this->update_active( $active ); } if ( $redirect ) { wp_safe_redirect( ( new Paths() )->admin_url( 'page=jetpack' ) ); } if ( $exit ) { exit( 0 ); } return true; } /** * Deactivate module. * * @param string $module Module slug. * * @return bool */ public function deactivate( $module ) { /** * Fires when a module is deactivated. * * @since 1.9.0 * * @param string $module Module slug. */ do_action( 'jetpack_pre_deactivate_module', $module ); $active = $this->get_active(); $new = array_filter( array_diff( $active, (array) $module ) ); return $this->update_active( $new ); } /** * Generate a module's path from its slug. * * @param string $slug Module slug. */ public function get_path( $slug ) { if ( ! Constants::is_defined( 'JETPACK__PLUGIN_DIR' ) ) { return ''; } /** * Filters the path of a modules. * * @since 7.4.0 * * @param array $return The absolute path to a module's root php file * @param string $slug The module slug */ return apply_filters( 'jetpack_get_module_path', JETPACK__PLUGIN_DIR . "modules/$slug.php", $slug ); } /** * Saves all the currently active modules to options. * Also fires Action hooks for each newly activated and deactivated module. * * @param array $modules Array of active modules to be saved in options. * * @return bool $success true for success, false for failure. */ public function update_active( $modules ) { $current_modules = \Jetpack_Options::get_option( 'active_modules', array() ); $active_modules = $this->get_active(); $new_active_modules = array_diff( $modules, $current_modules ); $new_inactive_modules = array_diff( $active_modules, $modules ); $new_current_modules = array_diff( array_merge( $current_modules, $new_active_modules ), $new_inactive_modules ); $reindexed_modules = array_values( $new_current_modules ); $success = \Jetpack_Options::update_option( 'active_modules', array_unique( $reindexed_modules ) ); // Let's take `pre_update_option_jetpack_active_modules` filter into account // and actually decide for which modules we need to fire hooks by comparing // the 'active_modules' option before and after the update. $current_modules_post_update = \Jetpack_Options::get_option( 'active_modules', array() ); $new_inactive_modules = array_diff( $current_modules, $current_modules_post_update ); $new_inactive_modules = array_unique( $new_inactive_modules ); $new_inactive_modules = array_values( $new_inactive_modules ); $new_active_modules = array_diff( $current_modules_post_update, $current_modules ); $new_active_modules = array_unique( $new_active_modules ); $new_active_modules = array_values( $new_active_modules ); foreach ( $new_active_modules as $module ) { /** * Fires when a specific module is activated. * * @since 1.9.0 * * @param string $module Module slug. * @param boolean $success whether the module was activated. @since 4.2 */ do_action( 'jetpack_activate_module', $module, $success ); /** * Fires when a module is activated. * The dynamic part of the filter, $module, is the module slug. * * @since 1.9.0 * * @param string $module Module slug. */ do_action( "jetpack_activate_module_$module", $module ); } foreach ( $new_inactive_modules as $module ) { /** * Fired after a module has been deactivated. * * @since 4.2.0 * * @param string $module Module slug. * @param boolean $success whether the module was deactivated. */ do_action( 'jetpack_deactivate_module', $module, $success ); /** * Fires when a module is deactivated. * The dynamic part of the filter, $module, is the module slug. * * @since 1.9.0 * * @param string $module Module slug. */ do_action( "jetpack_deactivate_module_$module", $module ); } return $success; } } jetpack-status/src/class-files.php 0000777 00000002332 15251741406 0013227 0 ustar 00 <?php /** * A modules class for Jetpack. * * @package automattic/jetpack-status */ namespace Automattic\Jetpack; /** * Class Automattic\Jetpack\Files * * Used to retrieve information about files. */ class Files { /** * Returns an array of all PHP files in the specified absolute path. * Equivalent to glob( "$absolute_path/*.php" ). * * @param string $absolute_path The absolute path of the directory to search. * @return array Array of absolute paths to the PHP files. */ public function glob_php( $absolute_path ) { if ( function_exists( 'glob' ) ) { return glob( "$absolute_path/*.php" ); } $absolute_path = untrailingslashit( $absolute_path ); $files = array(); $dir = @opendir( $absolute_path ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged if ( ! $dir ) { return $files; } // phpcs:ignore Generic.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition while ( false !== $file = readdir( $dir ) ) { if ( str_starts_with( $file, '.' ) || ! str_ends_with( $file, '.php' ) ) { continue; } $file = "$absolute_path/$file"; if ( ! is_file( $file ) ) { continue; } $files[] = $file; } closedir( $dir ); return $files; } } jetpack-status/src/class-paths.php 0000777 00000004443 15251741406 0013251 0 ustar 00 <?php /** * A Path & URL utility class for Jetpack. * * @package automattic/jetpack-status */ namespace Automattic\Jetpack; /** * Class Automattic\Jetpack\Paths * * Used to retrieve information about files. */ class Paths { /** * Jetpack Admin URL. * * @param array $args Query string args. * * @return string Jetpack admin URL. */ public function admin_url( $args = null ) { $args = wp_parse_args( $args, array( 'page' => 'jetpack' ) ); $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; } } jetpack-status/src/class-cache.php 0000777 00000002237 15251741406 0013174 0 ustar 00 <?php /** * A static in-process cache for blog data. * * @package automattic/jetpack-status */ namespace Automattic\Jetpack\Status; /** * A static in-process cache for blog data. * * For internal use only. Do not use this externally. */ class Cache { /** * Cached data; * * @var array[] */ private static $cache = array(); /** * Get a value from the cache. * * @param string $key Key to fetch. * @param mixed $default Default value to return if the key is not set. * @return mixed Data. */ public static function get( $key, $default = null ) { $blog_id = get_current_blog_id(); return isset( self::$cache[ $blog_id ] ) && array_key_exists( $key, self::$cache[ $blog_id ] ) ? self::$cache[ $blog_id ][ $key ] : $default; } /** * Set a value in the cache. * * @param string $key Key to set. * @param mixed $value Value to store. */ public static function set( $key, $value ) { $blog_id = get_current_blog_id(); self::$cache[ $blog_id ][ $key ] = $value; } /** * Clear the cache. * * This is intended for use in unit tests. */ public static function clear() { self::$cache = array(); } } jetpack-status/composer.json 0000777 00000002667 15251741406 0012257 0 ustar 00 { "name": "automattic/jetpack-status", "description": "Used to retrieve information about the current status of Jetpack and the site overall.", "type": "jetpack-library", "license": "GPL-2.0-or-later", "require": { "php": ">=7.2", "automattic/jetpack-constants": "^3.0.8" }, "require-dev": { "brain/monkey": "^2.6.2", "yoast/phpunit-polyfills": "^4.0.0", "automattic/jetpack-changelogger": "^6.0.7", "automattic/jetpack-connection": "@dev", "automattic/jetpack-plans": "@dev", "automattic/jetpack-ip": "^0.4.10", "automattic/phpunit-select-config": "^1.0.3" }, "suggest": { "automattic/jetpack-autoloader": "Allow for better interoperability with other plugins that use this package." }, "autoload": { "classmap": [ "src/" ] }, "scripts": { "phpunit": [ "phpunit-select-config phpunit.#.xml.dist --colors=always" ], "test-coverage": [ "php -dpcov.directory=. ./vendor/bin/phpunit-select-config phpunit.#.xml.dist --coverage-php \"$COVERAGE_DIR/php.cov\"" ], "test-php": [ "@composer phpunit" ] }, "minimum-stability": "dev", "prefer-stable": true, "extra": { "autotagger": true, "mirror-repo": "Automattic/jetpack-status", "changelogger": { "link-template": "https://github.com/Automattic/jetpack-status/compare/v${old}...v${new}" }, "branch-alias": { "dev-trunk": "6.1.x-dev" }, "dependencies": { "test-only": [ "packages/connection", "packages/plans" ] } } } jetpack-a8c-mc-stats/LICENSE.txt 0000777 00000043760 15251741406 0012240 0 ustar 00 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. jetpack-a8c-mc-stats/src/class-a8c-mc-stats.php 0000777 00000010673 15251741406 0015221 0 ustar 00 <?php /** * Jetpack MC Stats package. * * @package automattic/jetpack-mc-stats */ namespace Automattic\Jetpack; /** * Class MC Stats, used to record stats using https://pixel.wp.com/g.gif */ class A8c_Mc_Stats { /** * Holds the stats to be processed * * @var array */ private $stats = array(); /** * Indicates whether to use the transparent pixel (b.gif) instead of the regular smiley (g.gif) * * @var boolean */ public $use_transparent_pixel = true; /** * Class Constructor * * @param boolean $use_transparent_pixel Use the transparent pixel instead of the smiley. */ public function __construct( $use_transparent_pixel = true ) { $this->use_transparent_pixel = $use_transparent_pixel; } /** * Store a stat for later output. * * @param string $group The stat group. * @param string $name The stat name to bump. * * @return boolean true if stat successfully added */ public function add( $group, $name ) { if ( ! \is_string( $group ) || ! \is_string( $name ) ) { return false; } if ( ! isset( $this->stats[ $group ] ) ) { $this->stats[ $group ] = array(); } if ( \in_array( $name, $this->stats[ $group ], true ) ) { return false; } $this->stats[ $group ][] = $name; return true; } /** * Gets current stats stored to be processed * * @return array $stats */ public function get_current_stats() { return $this->stats; } /** * Return the stats from a group in an array ready to be added as parameters in a query string * * @param string $group_name The name of the group to retrieve. * @return array Array with one item, where the key is the prefixed group and the value are all stats concatenated with a comma. If group not found, an empty array will be returned */ public function get_group_query_args( $group_name ) { $stats = $this->get_current_stats(); if ( isset( $stats[ $group_name ] ) && ! empty( $stats[ $group_name ] ) ) { return array( "x_jetpack-{$group_name}" => implode( ',', $stats[ $group_name ] ) ); } return array(); } /** * Gets a list of trac URLs for every stored URL * * @return array An array of URLs */ public function get_stats_urls() { $urls = array(); foreach ( $this->get_current_stats() as $group => $stat ) { $group_query_string = $this->get_group_query_args( $group ); $urls[] = $this->build_stats_url( $group_query_string ); } return $urls; } /** * Outputs the tracking pixels for the current stats and empty the stored stats from the object * * @return void */ public function do_stats() { $urls = $this->get_stats_urls(); foreach ( $urls as $url ) { echo '<img src="' . esc_url( $url ) . '" width="1" height="1" style="display:none;" />'; } $this->stats = array(); } /** * Pings the stats server for the current stats and empty the stored stats from the object * * @return void */ public function do_server_side_stats() { $urls = $this->get_stats_urls(); foreach ( $urls as $url ) { $this->do_server_side_stat( $url ); } $this->stats = array(); } /** * Runs stats code for a one-off, server-side. * * @param string $url string The URL to be pinged. Should include `x_jetpack-{$group}={$stats}` or whatever we want to store. * * @return bool If it worked. */ public function do_server_side_stat( $url ) { $response = wp_remote_get( esc_url_raw( $url ) ); if ( is_wp_error( $response ) ) { return false; } if ( 200 !== wp_remote_retrieve_response_code( $response ) ) { return false; } return true; } /** * Builds the stats url. * * @param array $args array|string The arguments to append to the URL. * * @return string The URL to be pinged. */ public function build_stats_url( $args ) { $defaults = array( 'v' => 'wpcom2', // phpcs:ignore WordPress.WP.AlternativeFunctions.rand_rand -- There can be a case where pluggables are not yet loaded. 'rand' => md5( ( function_exists( 'wp_rand' ) ? wp_rand( 0, 999 ) : rand( 0, 999 ) ) . time() ), ); $args = wp_parse_args( $args, $defaults ); $gifname = true === $this->use_transparent_pixel ? 'b.gif' : 'g.gif'; /** * Filter the URL used as the Stats tracking pixel. * * @since-jetpack 2.3.2 * @since 1.0.0 * * @param string $url Base URL used as the Stats tracking pixel. */ $base_url = apply_filters( 'jetpack_stats_base_url', 'https://pixel.wp.com/' . $gifname ); $url = add_query_arg( $args, $base_url ); return $url; } } jetpack-a8c-mc-stats/composer.json 0000777 00000002243 15251741406 0013126 0 ustar 00 { "name": "automattic/jetpack-a8c-mc-stats", "description": "Used to record internal usage stats for Automattic. Not visible to site owners.", "type": "jetpack-library", "license": "GPL-2.0-or-later", "require": { "php": ">=7.2" }, "require-dev": { "yoast/phpunit-polyfills": "^4.0.0", "automattic/jetpack-changelogger": "^6.0.5", "automattic/phpunit-select-config": "^1.0.3" }, "suggest": { "automattic/jetpack-autoloader": "Allow for better interoperability with other plugins that use this package." }, "autoload": { "classmap": [ "src/" ] }, "scripts": { "phpunit": [ "phpunit-select-config phpunit.#.xml.dist --colors=always" ], "test-coverage": [ "php -dpcov.directory=. ./vendor/bin/phpunit-select-config phpunit.#.xml.dist --coverage-php \"$COVERAGE_DIR/php.cov\"" ], "test-php": [ "@composer phpunit" ] }, "minimum-stability": "dev", "prefer-stable": true, "extra": { "autotagger": true, "mirror-repo": "Automattic/jetpack-a8c-mc-stats", "changelogger": { "link-template": "https://github.com/Automattic/jetpack-a8c-mc-stats/compare/v${old}...v${new}" }, "branch-alias": { "dev-trunk": "3.0.x-dev" } } } jetpack-assets/build/jetpack-script-data.asset.php 0000777 00000000124 15251741406 0016256 0 ustar 00 <?php return array('dependencies' => array(), 'version' => 'd93b770c3a1796c5e279'); jetpack-assets/build/i18n-loader.asset.php 0000777 00000000135 15251741406 0014451 0 ustar 00 <?php return array('dependencies' => array('wp-i18n'), 'version' => '517685b2423141b3a0a3'); jetpack-assets/build/i18n-loader.js 0000777 00000013627 15251741406 0013172 0 ustar 00 (()=>{var e={305:(e,r,n)=>{const t=n(723),{default:o}=n(477),a={plugin:"plugins/",theme:"themes/",core:""},i=(e,r)=>Object.prototype.hasOwnProperty.call(e,r);e.exports={state:{baseUrl:null,locale:null,domainMap:{},domainPaths:{}},async downloadI18n(e,r,n){const f=this.state;if(!f||"string"!=typeof f.baseUrl)throw new Error("wp.jpI18nLoader.state is not set");if("en_US"===f.locale)return;if("undefined"==typeof fetch)throw new Error("Fetch API is not available.");const u=i(f.domainPaths,r)?f.domainPaths[r]:"";let c,h;const l=e.indexOf("?");l>=0?(c=o.hash(u+e.substring(0,l)),h=e.substring(l)):(c=o.hash(u+e),h="");const s=i(f.domainMap,r)?f.domainMap[r]:a[n]+r,g=await fetch(`${f.baseUrl}${s}-${f.locale}-${c}.json${h}`);if(!g.ok)throw new Error(`HTTP request failed: ${g.status} ${g.statusText}`);const d=await g.json(),p=i(d.locale_data,r)?d.locale_data[r]:d.locale_data.messages;p[""].domain=r,t.setLocaleData(p,r)}}},477:function(e){e.exports=function(e){function r(t){if(n[t])return n[t].exports;var o=n[t]={i:t,l:!1,exports:{}};return e[t].call(o.exports,o,o.exports,r),o.l=!0,o.exports}var n={};return r.m=e,r.c=n,r.i=function(e){return e},r.d=function(e,n,t){r.o(e,n)||Object.defineProperty(e,n,{configurable:!1,enumerable:!0,get:t})},r.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(n,"a",n),n},r.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},r.p="",r(r.s=1)}([function(e,r,n){"use strict";function t(e,r){if(!(e instanceof r))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(r,"__esModule",{value:!0});var o=function(){function e(e,r){for(var n=0;n<r.length;n++){var t=r[n];t.enumerable=t.enumerable||!1,t.configurable=!0,"value"in t&&(t.writable=!0),Object.defineProperty(e,t.key,t)}}return function(r,n,t){return n&&e(r.prototype,n),t&&e(r,t),r}}(),a=function(){function e(){t(this,e)}return o(e,null,[{key:"hash",value:function(r){return e.hex(e.md51(r))}},{key:"md5cycle",value:function(r,n){var t=r[0],o=r[1],a=r[2],i=r[3];t=e.ff(t,o,a,i,n[0],7,-680876936),i=e.ff(i,t,o,a,n[1],12,-389564586),a=e.ff(a,i,t,o,n[2],17,606105819),o=e.ff(o,a,i,t,n[3],22,-1044525330),t=e.ff(t,o,a,i,n[4],7,-176418897),i=e.ff(i,t,o,a,n[5],12,1200080426),a=e.ff(a,i,t,o,n[6],17,-1473231341),o=e.ff(o,a,i,t,n[7],22,-45705983),t=e.ff(t,o,a,i,n[8],7,1770035416),i=e.ff(i,t,o,a,n[9],12,-1958414417),a=e.ff(a,i,t,o,n[10],17,-42063),o=e.ff(o,a,i,t,n[11],22,-1990404162),t=e.ff(t,o,a,i,n[12],7,1804603682),i=e.ff(i,t,o,a,n[13],12,-40341101),a=e.ff(a,i,t,o,n[14],17,-1502002290),o=e.ff(o,a,i,t,n[15],22,1236535329),t=e.gg(t,o,a,i,n[1],5,-165796510),i=e.gg(i,t,o,a,n[6],9,-1069501632),a=e.gg(a,i,t,o,n[11],14,643717713),o=e.gg(o,a,i,t,n[0],20,-373897302),t=e.gg(t,o,a,i,n[5],5,-701558691),i=e.gg(i,t,o,a,n[10],9,38016083),a=e.gg(a,i,t,o,n[15],14,-660478335),o=e.gg(o,a,i,t,n[4],20,-405537848),t=e.gg(t,o,a,i,n[9],5,568446438),i=e.gg(i,t,o,a,n[14],9,-1019803690),a=e.gg(a,i,t,o,n[3],14,-187363961),o=e.gg(o,a,i,t,n[8],20,1163531501),t=e.gg(t,o,a,i,n[13],5,-1444681467),i=e.gg(i,t,o,a,n[2],9,-51403784),a=e.gg(a,i,t,o,n[7],14,1735328473),o=e.gg(o,a,i,t,n[12],20,-1926607734),t=e.hh(t,o,a,i,n[5],4,-378558),i=e.hh(i,t,o,a,n[8],11,-2022574463),a=e.hh(a,i,t,o,n[11],16,1839030562),o=e.hh(o,a,i,t,n[14],23,-35309556),t=e.hh(t,o,a,i,n[1],4,-1530992060),i=e.hh(i,t,o,a,n[4],11,1272893353),a=e.hh(a,i,t,o,n[7],16,-155497632),o=e.hh(o,a,i,t,n[10],23,-1094730640),t=e.hh(t,o,a,i,n[13],4,681279174),i=e.hh(i,t,o,a,n[0],11,-358537222),a=e.hh(a,i,t,o,n[3],16,-722521979),o=e.hh(o,a,i,t,n[6],23,76029189),t=e.hh(t,o,a,i,n[9],4,-640364487),i=e.hh(i,t,o,a,n[12],11,-421815835),a=e.hh(a,i,t,o,n[15],16,530742520),o=e.hh(o,a,i,t,n[2],23,-995338651),t=e.ii(t,o,a,i,n[0],6,-198630844),i=e.ii(i,t,o,a,n[7],10,1126891415),a=e.ii(a,i,t,o,n[14],15,-1416354905),o=e.ii(o,a,i,t,n[5],21,-57434055),t=e.ii(t,o,a,i,n[12],6,1700485571),i=e.ii(i,t,o,a,n[3],10,-1894986606),a=e.ii(a,i,t,o,n[10],15,-1051523),o=e.ii(o,a,i,t,n[1],21,-2054922799),t=e.ii(t,o,a,i,n[8],6,1873313359),i=e.ii(i,t,o,a,n[15],10,-30611744),a=e.ii(a,i,t,o,n[6],15,-1560198380),o=e.ii(o,a,i,t,n[13],21,1309151649),t=e.ii(t,o,a,i,n[4],6,-145523070),i=e.ii(i,t,o,a,n[11],10,-1120210379),a=e.ii(a,i,t,o,n[2],15,718787259),o=e.ii(o,a,i,t,n[9],21,-343485551),r[0]=t+r[0]&4294967295,r[1]=o+r[1]&4294967295,r[2]=a+r[2]&4294967295,r[3]=i+r[3]&4294967295}},{key:"cmn",value:function(e,r,n,t,o,a){return((r=(r+e&4294967295)+(t+a&4294967295)&4294967295)<<o|r>>>32-o)+n&4294967295}},{key:"ff",value:function(r,n,t,o,a,i,f){return e.cmn(n&t|~n&o,r,n,a,i,f)}},{key:"gg",value:function(r,n,t,o,a,i,f){return e.cmn(n&o|t&~o,r,n,a,i,f)}},{key:"hh",value:function(r,n,t,o,a,i,f){return e.cmn(n^t^o,r,n,a,i,f)}},{key:"ii",value:function(r,n,t,o,a,i,f){return e.cmn(t^(n|~o),r,n,a,i,f)}},{key:"md51",value:function(r){for(var n=r.length,t=[1732584193,-271733879,-1732584194,271733878],o=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],a=0,i=64;i<=n;i+=64)e.md5cycle(t,e.md5blk(r.substring(i-64,i)));for(r=r.substring(i-64),i=0,a=r.length;i<a;i++)o[i>>2]|=r.charCodeAt(i)<<(i%4<<3);if(o[i>>2]|=128<<(i%4<<3),i>55)for(e.md5cycle(t,o),i=0;i<16;i++)o[i]=0;return o[14]=8*n,e.md5cycle(t,o),t}},{key:"md5blk",value:function(e){for(var r=[],n=0;n<64;n+=4)r[n>>2]=e.charCodeAt(n)+(e.charCodeAt(n+1)<<8)+(e.charCodeAt(n+2)<<16)+(e.charCodeAt(n+3)<<24);return r}},{key:"rhex",value:function(r){var n="";return n+=e.hexArray[r>>4&15]+e.hexArray[15&r],n+=e.hexArray[r>>12&15]+e.hexArray[r>>8&15],(n+=e.hexArray[r>>20&15]+e.hexArray[r>>16&15])+(e.hexArray[r>>28&15]+e.hexArray[r>>24&15])}},{key:"hex",value:function(r){for(var n=r.length,t=0;t<n;t++)r[t]=e.rhex(r[t]);return r.join("")}}]),e}();a.hexArray=["0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f"],r.default=a},function(e,r,n){e.exports=n(0)}])},723:e=>{"use strict";e.exports=window.wp.i18n}},r={};var n=function n(t){var o=r[t];if(void 0!==o)return o.exports;var a=r[t]={exports:{}};return e[t].call(a.exports,a,a.exports,n),a.exports}(305);(window.wp=window.wp||{}).jpI18nLoader=n})(); jetpack-assets/build/jetpack-script-data.js 0000777 00000003645 15251741406 0015000 0 ustar 00 !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,()=>(()=>{"use strict";var e={336:(e,t,r)=>{function n(){return window.JetpackScriptData}function o(){return n()?.site}function i(e=""){return`${n()?.site.admin_url}${e}`}function a(e=""){return i(`admin.php?page=jetpack${e}`)}function u(e=""){return i(`admin.php?page=my-jetpack${e}`)}function c(){return n()?.site.plan?.features?.active??[]}function p(e){return c().includes(e)}function s(){return"wpcom"===n()?.site?.host}function f(){return"woa"===n()?.site?.host}function d(){return n()?.site?.is_wpcom_platform}function l(){return"unknown"===n()?.site?.host}function m(e){return n()?.user.current_user.capabilities[e]}r.d(t,{$8:()=>d,IT:()=>p,L2:()=>l,Sy:()=>s,au:()=>n,d_:()=>m,e5:()=>u,hT:()=>i,lI:()=>f,mH:()=>c,oQ:()=>a,sV:()=>o})},729:(e,t,r)=>{r.d(t,{$8:()=>n.$8,IT:()=>n.IT,L2:()=>n.L2,Sy:()=>n.Sy,au:()=>n.au,d_:()=>n.d_,e5:()=>n.e5,hT:()=>n.hT,lI:()=>n.lI,mH:()=>n.mH,oQ:()=>n.oQ,sV:()=>n.sV});var n=r(336)}},t={};function r(n){var o=t[n];if(void 0!==o)return o.exports;var i=t[n]={exports:{}};return e[n](i,i.exports,r),i.exports}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={};r.r(n),r.d(n,{currentUserCan:()=>o.d_,getActiveFeatures:()=>o.mH,getAdminUrl:()=>o.hT,getJetpackAdminPageUrl:()=>o.oQ,getMyJetpackUrl:()=>o.e5,getScriptData:()=>o.au,getSiteData:()=>o.sV,isJetpackSelfHostedSite:()=>o.L2,isSimpleSite:()=>o.Sy,isWoASite:()=>o.lI,isWpcomPlatformSite:()=>o.$8,siteHasFeature:()=>o.IT});var o=r(729);return n})()); jetpack-assets/src/class-semver.php 0000777 00000007157 15251741406 0013417 0 ustar 00 <?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; } } jetpack-assets/src/class-script-data.php 0000777 00000014423 15251741406 0014323 0 ustar 00 <?php /** * Jetpack script data. * * @package automattic/jetpack-assets */ namespace Automattic\Jetpack\Assets; use Automattic\Jetpack\Assets; use Automattic\Jetpack\Status; use Automattic\Jetpack\Status\Host; /** * Class script data */ class Script_Data { const SCRIPT_HANDLE = 'jetpack-script-data'; /** * Whether the script data has been rendered. * * @var bool */ private static $did_render_script_data = false; /** * 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 ); add_action( 'enqueue_block_editor_assets', 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() { // In case of 'enqueue_block_editor_assets' action, this can be called multiple times. if ( self::$did_render_script_data ) { return; } self::$did_render_script_data = true; $script_data = is_admin() || self::is_authenticated_rest_request() ? self::get_admin_script_data() : self::get_public_script_data(); if ( ! empty( $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' ); Assets::enqueue_script( self::SCRIPT_HANDLE ); } } /** * Whether the current request is an authenticated REST API request. * * @return bool */ protected static function is_authenticated_rest_request() { return wp_is_serving_rest_request() && current_user_can( 'read' ); } /** * Get the admin script data. * * @return array */ protected static function get_admin_script_data() { global $wp_version; $data = 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(), 'host' => ( new Host() )->get_known_host_guess(), 'is_wpcom_platform' => ( new Host() )->is_wpcom_platform(), '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() ), 'suffix' => ( new Status() )->get_site_suffix(), '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 $data The script data. */ return apply_filters( 'jetpack_admin_js_script_data', $data ); } /** * Get the public script data. * * @return array */ protected static function get_public_script_data() { $data = array(); /** * Filter the public script data. * * See the docs for `jetpack_admin_js_script_data` filter for more information. * * @since 2.3.0 * * @param array $data The script data. */ return apply_filters( 'jetpack_public_js_script_data', $data ); } /** * 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, 'capabilities' => array( 'manage_options' => current_user_can( 'manage_options' ), 'manage_modules' => current_user_can( 'jetpack_manage_modules' ), ), ); } } jetpack-assets/src/js/script-data.js 0000777 00000000061 15251741406 0013452 0 ustar 00 export * from '@automattic/jetpack-script-data'; jetpack-assets/src/js/i18n-loader.js 0000777 00000004356 15251741406 0013275 0 ustar 00 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'. * @return {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 ); }, }; jetpack-assets/src/class-assets.php 0000777 00000070212 15251741406 0013410 0 ustar 00 <?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 ); $handle = 'wp-jp-i18n-loader'; $wp_scripts->add( $handle, $url, $asset['dependencies'], $asset['version'] ); // Ensure the script is loaded in the footer and deferred. $wp_scripts->add_data( $handle, 'group', 1 ); 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( $handle, 'console.warn( "I18n state deleted by jetpack_i18n_state hook" );' ); } elseif ( ! $data['baseUrl'] ) { $wp_scripts->add_inline_script( $handle, '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( $handle, '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', $handle ) ); $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;' ); } // 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 // . jetpack-assets/LICENSE.txt 0000777 00000043760 15251741406 0011336 0 ustar 00 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. jetpack-assets/actions.php 0000777 00000001730 15251741406 0011653 0 ustar 00 <?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\Assets\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\Assets\Script_Data::class, 'configure' ), ); } jetpack-assets/composer.json 0000777 00000003063 15251741406 0012225 0 ustar 00 { "name": "automattic/jetpack-assets", "description": "Asset management utilities for Jetpack ecosystem packages", "type": "jetpack-library", "license": "GPL-2.0-or-later", "require": { "php": ">=7.2", "automattic/jetpack-constants": "^3.0.8", "automattic/jetpack-status": "^6.1.0" }, "require-dev": { "brain/monkey": "^2.6.2", "yoast/phpunit-polyfills": "^4.0.0", "automattic/jetpack-changelogger": "^6.0.7", "wikimedia/testing-access-wrapper": "^1.0 || ^2.0 || ^3.0", "automattic/phpunit-select-config": "^1.0.3" }, "suggest": { "automattic/jetpack-autoloader": "Allow for better interoperability with other plugins that use this package." }, "autoload": { "files": [ "actions.php" ], "classmap": [ "src/" ] }, "scripts": { "build-development": [ "pnpm run build" ], "build-production": [ "pnpm run build-production" ], "phpunit": [ "phpunit-select-config phpunit.#.xml.dist --colors=always" ], "test-coverage": "pnpm concurrently --names php,js 'php -dpcov.directory=. ./vendor/bin/phpunit-select-config phpunit.#.xml.dist --coverage-php \"$COVERAGE_DIR/php.cov\"' 'pnpm:test-coverage'", "test-js": [ "pnpm run test" ], "test-php": [ "@composer phpunit" ] }, "minimum-stability": "dev", "prefer-stable": true, "extra": { "autotagger": true, "mirror-repo": "Automattic/jetpack-assets", "textdomain": "jetpack-assets", "changelogger": { "link-template": "https://github.com/Automattic/jetpack-assets/compare/v${old}...v${new}" }, "branch-alias": { "dev-trunk": "4.3.x-dev" } } } jetpack-constants/src/class-constants.php 0000777 00000006611 15251741406 0014636 0 ustar 00 <?php /** * A constants manager for Jetpack. * * @package automattic/jetpack-constants */ namespace Automattic\Jetpack; /** * Class Automattic\Jetpack\Constants * * Testing constants is hard. Once you define a constant, it's defined. Constants Manager is an * abstraction layer so that unit tests can set "constants" for tests. * * To test your code, you'll need to swap out `defined( 'CONSTANT' )` with `Automattic\Jetpack\Constants::is_defined( 'CONSTANT' )` * and replace `CONSTANT` with `Automattic\Jetpack\Constants::get_constant( 'CONSTANT' )`. Then in the unit test, you can set the * constant with `Automattic\Jetpack\Constants::set_constant( 'CONSTANT', $value )` and then clean up after each test with something like * this: * * function tearDown() { * Automattic\Jetpack\Constants::clear_constants(); * } */ class Constants { /** * A container for all defined constants. * * @access public * @static * * @var array */ public static $set_constants = array(); /** * Checks if a "constant" has been set in constants Manager * and has a truthy value (e.g. not null, not false, not 0, any string). * * @param string $name The name of the constant. * * @return bool */ public static function is_true( $name ) { return self::is_defined( $name ) && self::get_constant( $name ); } /** * Checks if a "constant" has been set in constants Manager, and if not, * checks if the constant was defined with define( 'name', 'value ). * * @param string $name The name of the constant. * * @return bool */ public static function is_defined( $name ) { return array_key_exists( $name, self::$set_constants ) ? true : defined( $name ); } /** * Attempts to retrieve the "constant" from constants Manager, and if it hasn't been set, * then attempts to get the constant with the constant() function. If that also hasn't * been set, attempts to get a value from filters. * * @param string $name The name of the constant. * * @return mixed null if the constant does not exist or the value of the constant. */ public static function get_constant( $name ) { if ( array_key_exists( $name, self::$set_constants ) ) { return self::$set_constants[ $name ]; } if ( defined( $name ) ) { return constant( $name ); } /** * Filters the value of the constant. * * @since 1.2.0 * * @param null The constant value to be filtered. The default is null. * @param String $name The constant name. */ return apply_filters( 'jetpack_constant_default_value', null, $name ); } /** * Sets the value of the "constant" within constants Manager. * * @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; } /** * Will unset a "constant" from constants Manager if the constant exists. * * @param string $name The name of the constant. * * @return bool Whether the constant was removed. */ public static function clear_single_constant( $name ) { if ( ! array_key_exists( $name, self::$set_constants ) ) { return false; } unset( self::$set_constants[ $name ] ); return true; } /** * Resets all of the constants within constants Manager. */ public static function clear_constants() { self::$set_constants = array(); } } jetpack-constants/composer.json 0000777 00000002237 15251741406 0012741 0 ustar 00 { "name": "automattic/jetpack-constants", "description": "A wrapper for defining constants in a more testable way.", "type": "jetpack-library", "license": "GPL-2.0-or-later", "require": { "php": ">=7.2" }, "require-dev": { "brain/monkey": "^2.6.2", "yoast/phpunit-polyfills": "^4.0.0", "automattic/jetpack-changelogger": "^6.0.5", "automattic/phpunit-select-config": "^1.0.3" }, "suggest": { "automattic/jetpack-autoloader": "Allow for better interoperability with other plugins that use this package." }, "autoload": { "classmap": [ "src/" ] }, "scripts": { "phpunit": [ "phpunit-select-config phpunit.#.xml.dist --colors=always" ], "test-coverage": [ "php -dpcov.directory=. ./vendor/bin/phpunit-select-config phpunit.#.xml.dist --coverage-php \"$COVERAGE_DIR/php.cov\"" ], "test-php": [ "@composer phpunit" ] }, "minimum-stability": "dev", "prefer-stable": true, "extra": { "autotagger": true, "mirror-repo": "Automattic/jetpack-constants", "changelogger": { "link-template": "https://github.com/Automattic/jetpack-constants/compare/v${old}...v${new}" }, "branch-alias": { "dev-trunk": "3.0.x-dev" } } } jetpack-constants/LICENSE.txt 0000777 00000043760 15251741406 0012050 0 ustar 00 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. jetpack-roles/LICENSE.txt 0000777 00000043760 15251741406 0011160 0 ustar 00 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. jetpack-roles/src/class-roles.php 0000777 00000003507 15251741406 0013057 0 ustar 00 <?php /** * A user roles class for Jetpack. * * @package automattic/jetpack-roles */ namespace Automattic\Jetpack; /** * Class Automattic\Jetpack\Roles * * Contains utilities for translating user roles to capabilities and vice versa. */ class Roles { /** * Map of roles we care about, and their corresponding minimum capabilities. * * @access protected * * @var array */ protected $capability_translations = array( 'administrator' => 'manage_options', 'editor' => 'edit_others_posts', 'author' => 'publish_posts', 'contributor' => 'edit_posts', 'subscriber' => 'read', ); /** * Get the role of the current user. * * @access public * * @return string|boolean Current user's role, false if not enough capabilities for any of the roles. */ public function translate_current_user_to_role() { foreach ( $this->capability_translations as $role => $cap ) { if ( current_user_can( $role ) || current_user_can( $cap ) ) { return $role; } } return false; } /** * Get the role of a particular user. * * @access public * * @param \WP_User $user User object. * @return string|boolean User's role, false if not enough capabilities for any of the roles. */ public function translate_user_to_role( $user ) { foreach ( $this->capability_translations as $role => $cap ) { if ( user_can( $user, $role ) || user_can( $user, $cap ) ) { return $role; } } return false; } /** * Get the minimum capability for a role. * * @access public * * @param string $role Role name. * @return string|boolean Capability, false if role isn't mapped to any capabilities. */ public function translate_role_to_cap( $role ) { if ( ! isset( $this->capability_translations[ $role ] ) ) { return false; } return $this->capability_translations[ $role ]; } } jetpack-roles/composer.json 0000777 00000002217 15251741406 0012047 0 ustar 00 { "name": "automattic/jetpack-roles", "description": "Utilities, related with user roles and capabilities.", "type": "jetpack-library", "license": "GPL-2.0-or-later", "require": { "php": ">=7.2" }, "require-dev": { "brain/monkey": "^2.6.2", "yoast/phpunit-polyfills": "^4.0.0", "automattic/jetpack-changelogger": "^6.0.5", "automattic/phpunit-select-config": "^1.0.3" }, "suggest": { "automattic/jetpack-autoloader": "Allow for better interoperability with other plugins that use this package." }, "autoload": { "classmap": [ "src/" ] }, "scripts": { "phpunit": [ "phpunit-select-config phpunit.#.xml.dist --colors=always" ], "test-coverage": [ "php -dpcov.directory=. ./vendor/bin/phpunit-select-config phpunit.#.xml.dist --coverage-php \"$COVERAGE_DIR/php.cov\"" ], "test-php": [ "@composer phpunit" ] }, "minimum-stability": "dev", "prefer-stable": true, "extra": { "autotagger": true, "mirror-repo": "Automattic/jetpack-roles", "changelogger": { "link-template": "https://github.com/Automattic/jetpack-roles/compare/v${old}...v${new}" }, "branch-alias": { "dev-trunk": "3.0.x-dev" } } } jetpack-config/composer.json 0000777 00000003531 15251741406 0012170 0 ustar 00 { "name": "automattic/jetpack-config", "description": "Jetpack configuration package that initializes other packages and configures Jetpack's functionality. Can be used as a base for all variants of Jetpack package usage.", "type": "jetpack-library", "license": "GPL-2.0-or-later", "require": { "php": ">=7.2" }, "require-dev": { "automattic/jetpack-changelogger": "^5.0.0", "automattic/jetpack-connection": "@dev", "automattic/jetpack-import": "@dev", "automattic/jetpack-jitm": "@dev", "automattic/jetpack-post-list": "@dev", "automattic/jetpack-publicize": "@dev", "automattic/jetpack-search": "@dev", "automattic/jetpack-stats": "@dev", "automattic/jetpack-stats-admin": "@dev", "automattic/jetpack-sync": "@dev", "automattic/jetpack-videopress": "@dev", "automattic/jetpack-waf": "@dev", "automattic/jetpack-wordads": "@dev", "automattic/jetpack-yoast-promo": "@dev" }, "suggest": { "automattic/jetpack-autoloader": "Allow for better interoperability with other plugins that use this package." }, "autoload": { "classmap": [ "src/" ] }, "minimum-stability": "dev", "prefer-stable": true, "extra": { "autotagger": true, "mirror-repo": "Automattic/jetpack-config", "textdomain": "jetpack-config", "changelogger": { "link-template": "https://github.com/Automattic/jetpack-config/compare/v${old}...v${new}" }, "branch-alias": { "dev-trunk": "3.0.x-dev" }, "dependencies": { "test-only": [ "packages/connection", "packages/import", "packages/jitm", "packages/post-list", "packages/publicize", "packages/search", "packages/stats", "packages/stats-admin", "packages/sync", "packages/videopress", "packages/waf", "packages/wordads", "packages/yoast-promo" ] } }, "config": { "allow-plugins": { "automattic/jetpack-autoloader": true } } } jetpack-config/src/class-config.php 0000777 00000026232 15251741406 0013321 0 ustar 00 <?php /** * The base Jetpack configuration class file. * * @package automattic/jetpack-config */ namespace Automattic\Jetpack; /* * The Config package does not require the composer packages that * contain the package classes shown below. The consumer plugin * must require the corresponding packages to use these features. */ use Automattic\Jetpack\Connection\Manager; use Automattic\Jetpack\Connection\Plugin; use Automattic\Jetpack\Import\Main as Import_Main; use Automattic\Jetpack\JITMS\JITM as JITMS_JITM; use Automattic\Jetpack\Post_List\Post_List; use Automattic\Jetpack\Publicize\Publicize_Setup; use Automattic\Jetpack\Search\Initializer as Jetpack_Search_Main; use Automattic\Jetpack\Stats\Main as Stats_Main; use Automattic\Jetpack\Stats_Admin\Main as Stats_Admin_Main; use Automattic\Jetpack\Sync\Main as Sync_Main; use Automattic\Jetpack\VideoPress\Initializer as VideoPress_Pkg_Initializer; use Automattic\Jetpack\Waf\Waf_Initializer as Jetpack_Waf_Main; use Automattic\Jetpack\WordAds\Initializer as Jetpack_WordAds_Main; /** * The configuration class. */ class Config { const FEATURE_ENSURED = 1; const FEATURE_NOT_AVAILABLE = 0; const FEATURE_ALREADY_ENSURED = -1; /** * The initial setting values. * * @var Array */ protected $config = array( 'jitm' => false, 'connection' => false, 'sync' => false, 'post_list' => false, 'identity_crisis' => false, 'search' => false, 'publicize' => false, 'wordads' => false, 'waf' => false, 'videopress' => false, 'stats' => false, 'stats_admin' => false, 'yoast_promo' => false, 'import' => false, ); /** * Initialization options stored here. * * @var array */ protected $feature_options = array(); /** * Creates the configuration class instance. */ public function __construct() { /** * Adding the config handler to run on priority 2 because the class itself is * being constructed on priority 1. */ add_action( 'plugins_loaded', array( $this, 'on_plugins_loaded' ), 2 ); } /** * Require a feature to be initialized. It's up to the package consumer to actually add * the package to their composer project. Declaring a requirement using this method * instructs the class to initialize it. * * Run the options method (if exists) every time the method is called. * * @param String $feature the feature slug. * @param array $options Additional options, optional. */ public function ensure( $feature, array $options = array() ) { $this->config[ $feature ] = true; $this->set_feature_options( $feature, $options ); $method_options = 'ensure_options_' . $feature; if ( method_exists( $this, $method_options ) ) { $this->{ $method_options }(); } } /** * Runs on plugins_loaded hook priority with priority 2. * * @action plugins_loaded */ public function on_plugins_loaded() { if ( $this->config['connection'] ) { $this->ensure_class( 'Automattic\Jetpack\Connection\Manager' ) && $this->ensure_feature( 'connection' ); } if ( $this->config['sync'] ) { $this->ensure_class( 'Automattic\Jetpack\Sync\Main' ) && $this->ensure_feature( 'sync' ); } if ( $this->config['jitm'] ) { // Check for the JITM class in both namespaces. The namespace was changed in jetpack-jitm v1.6. ( $this->ensure_class( 'Automattic\Jetpack\JITMS\JITM', false ) || $this->ensure_class( 'Automattic\Jetpack\JITM' ) ) && $this->ensure_feature( 'jitm' ); } if ( $this->config['post_list'] ) { $this->ensure_class( 'Automattic\Jetpack\Post_List\Post_List' ) && $this->ensure_feature( 'post_list' ); } if ( $this->config['identity_crisis'] ) { $this->ensure_class( 'Automattic\Jetpack\Identity_Crisis' ) && $this->ensure_feature( 'identity_crisis' ); } if ( $this->config['search'] ) { $this->ensure_class( 'Automattic\Jetpack\Search\Initializer' ) && $this->ensure_feature( 'search' ); } if ( $this->config['publicize'] ) { $this->ensure_class( 'Automattic\Jetpack\Publicize\Publicize_UI' ) && $this->ensure_class( 'Automattic\Jetpack\Publicize\Publicize' ) && $this->ensure_feature( 'publicize' ); } if ( $this->config['wordads'] ) { $this->ensure_class( 'Automattic\Jetpack\WordAds\Initializer' ) && $this->ensure_feature( 'wordads' ); } if ( $this->config['waf'] ) { $this->ensure_class( 'Automattic\Jetpack\Waf\Waf_Initializer' ) && $this->ensure_feature( 'waf' ); } if ( $this->config['videopress'] ) { $this->ensure_class( 'Automattic\Jetpack\VideoPress\Initializer' ) && $this->ensure_feature( 'videopress' ); } if ( $this->config['stats'] ) { $this->ensure_class( 'Automattic\Jetpack\Stats\Main' ) && $this->ensure_feature( 'stats' ); } if ( $this->config['stats_admin'] ) { $this->ensure_class( 'Automattic\Jetpack\Stats_Admin\Main' ) && $this->ensure_feature( 'stats_admin' ); } if ( $this->config['yoast_promo'] ) { $this->ensure_class( 'Automattic\Jetpack\Yoast_Promo' ) && $this->ensure_feature( 'yoast_promo' ); } if ( $this->config['import'] ) { $this->ensure_class( 'Automattic\Jetpack\Import\Main' ) && $this->ensure_feature( 'import' ); } } /** * Returns true if the required class is available and alerts the user if it's not available * in case the site is in debug mode. * * @param String $classname a fully qualified class name. * @param Boolean $log_notice whether the E_USER_NOTICE should be generated if the class is not found. * * @return Boolean whether the class is available. */ protected function ensure_class( $classname, $log_notice = true ) { $available = class_exists( $classname ); if ( $log_notice && ! $available && defined( 'WP_DEBUG' ) && WP_DEBUG ) { trigger_error( // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error sprintf( /* translators: %1$s is a PHP class name. */ esc_html__( 'Unable to load class %1$s. Please add the package that contains it using composer and make sure you are requiring the Jetpack autoloader', 'jetpack-config' ), esc_html( $classname ) ), E_USER_NOTICE ); } return $available; } /** * Ensures a feature is enabled, sets it up if it hasn't already been set up. * * @param String $feature slug of the feature. * @return Integer either FEATURE_ENSURED, FEATURE_ALREADY_ENSURED or FEATURE_NOT_AVAILABLE constants. */ protected function ensure_feature( $feature ) { $method = 'enable_' . $feature; if ( ! method_exists( $this, $method ) ) { return self::FEATURE_NOT_AVAILABLE; } if ( did_action( 'jetpack_feature_' . $feature . '_enabled' ) ) { return self::FEATURE_ALREADY_ENSURED; } $this->{ $method }(); /** * Fires when a specific Jetpack package feature is initalized using the Config package. * * @since 1.1.0 */ do_action( 'jetpack_feature_' . $feature . '_enabled' ); return self::FEATURE_ENSURED; } /** * Enables the JITM feature. */ protected function enable_jitm() { if ( class_exists( 'Automattic\Jetpack\JITMS\JITM' ) ) { JITMS_JITM::configure(); } else { // Provides compatibility with jetpack-jitm <v1.6. // @phan-suppress-next-line PhanUndeclaredClassMethod JITM::configure(); } return true; } /** * Enables the Post_List feature. */ protected function enable_post_list() { Post_List::configure(); return true; } /** * Enables the Sync feature. */ protected function enable_sync() { Sync_Main::configure(); return true; } /** * Enables the Connection feature. */ protected function enable_connection() { Manager::configure(); return true; } /** * Enables the identity-crisis feature. */ protected function enable_identity_crisis() { Identity_Crisis::init(); } /** * Enables the search feature. */ protected function enable_search() { Jetpack_Search_Main::init(); } /** * Enables the Publicize feature. */ protected function enable_publicize() { Publicize_Setup::configure(); return true; } /** * Handles Publicize options. */ protected function ensure_options_publicize() { $options = $this->get_feature_options( 'publicize' ); if ( ! empty( $options['force_refresh'] ) ) { Publicize_Setup::$refresh_plan_info = true; } } /** * Enables WordAds. */ protected function enable_wordads() { Jetpack_WordAds_Main::init(); } /** * Enables Waf. */ protected function enable_waf() { Jetpack_Waf_Main::init(); return true; } /** * Enables VideoPress. */ protected function enable_videopress() { VideoPress_Pkg_Initializer::init(); return true; } /** * Enables Stats. */ protected function enable_stats() { Stats_Main::init(); return true; } /** * Enables Stats Admin. */ protected function enable_stats_admin() { Stats_Admin_Main::init(); return true; } /** * Handles VideoPress options */ protected function ensure_options_videopress() { $options = $this->get_feature_options( 'videopress' ); if ( ! empty( $options ) ) { VideoPress_Pkg_Initializer::update_init_options( $options ); } return true; } /** * Enables Yoast Promo. */ protected function enable_yoast_promo() { Yoast_Promo::init(); return true; } /** * Enables the Import feature. */ protected function enable_import() { Import_Main::configure(); return true; } /** * Setup the Connection options. */ protected function ensure_options_connection() { $options = $this->get_feature_options( 'connection' ); if ( ! empty( $options['slug'] ) ) { // The `slug` and `name` are removed from the options because they need to be passed as arguments. $slug = $options['slug']; unset( $options['slug'] ); $name = $slug; if ( ! empty( $options['name'] ) ) { $name = $options['name']; unset( $options['name'] ); } ( new Plugin( $slug ) )->add( $name, $options ); } return true; } /** * Setup the Identity Crisis options. * * @return bool */ protected function ensure_options_identity_crisis() { $options = $this->get_feature_options( 'identity_crisis' ); if ( is_array( $options ) && count( $options ) ) { add_filter( 'jetpack_idc_consumers', function ( $consumers ) use ( $options ) { $consumers[] = $options; return $consumers; } ); } return true; } /** * Setup the Sync options. */ protected function ensure_options_sync() { $options = $this->get_feature_options( 'sync' ); if ( method_exists( 'Automattic\Jetpack\Sync\Main', 'set_sync_data_options' ) ) { Sync_Main::set_sync_data_options( $options ); } return true; } /** * Temporary save initialization options for a feature. * * @param string $feature The feature slug. * @param array $options The options. * * @return bool */ protected function set_feature_options( $feature, array $options ) { if ( $options ) { $this->feature_options[ $feature ] = $options; } return true; } /** * Get initialization options for a feature from the temporary storage. * * @param string $feature The feature slug. * * @return array */ protected function get_feature_options( $feature ) { return empty( $this->feature_options[ $feature ] ) ? array() : $this->feature_options[ $feature ]; } } jetpack-config/LICENSE.txt 0000777 00000043760 15251741406 0011301 0 ustar 00 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. jetpack-autoloader/LICENSE.txt 0000777 00000043760 15251741406 0012173 0 ustar 00 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. jetpack-autoloader/composer.json 0000777 00000002603 15251741406 0013061 0 ustar 00 { "name": "automattic/jetpack-autoloader", "description": "Creates a custom autoloader for a plugin or theme.", "type": "composer-plugin", "license": "GPL-2.0-or-later", "keywords": [ "autoload", "autoloader", "composer", "plugin", "jetpack", "wordpress" ], "require": { "php": ">=7.2", "composer-plugin-api": "^2.2" }, "require-dev": { "composer/composer": "^2.2", "yoast/phpunit-polyfills": "^1.1.1", "automattic/jetpack-changelogger": "^5.1.0" }, "autoload": { "classmap": [ "src/AutoloadGenerator.php" ], "psr-4": { "Automattic\\Jetpack\\Autoloader\\": "src" } }, "scripts": { "phpunit": [ "./vendor/phpunit/phpunit/phpunit --colors=always" ], "test-coverage": [ "php -dpcov.directory=. ./vendor/bin/phpunit --coverage-php \"./tests/php/tmp/coverage-report.php\"", "php ./tests/php/bin/test-coverage.php \"$COVERAGE_DIR/php.cov\"" ], "test-php": [ "@composer phpunit" ] }, "minimum-stability": "dev", "prefer-stable": true, "extra": { "autotagger": true, "class": "Automattic\\Jetpack\\Autoloader\\CustomAutoloaderPlugin", "mirror-repo": "Automattic/jetpack-autoloader", "changelogger": { "link-template": "https://github.com/Automattic/jetpack-autoloader/compare/v${old}...v${new}" }, "version-constants": { "::VERSION": "src/AutoloadGenerator.php" }, "branch-alias": { "dev-trunk": "5.0.x-dev" } } } jetpack-autoloader/src/class-plugin-locator.php 0000777 00000010512 15251741406 0015677 0 ustar 00 <?php /* HEADER */ // phpcs:ignore /** * This class scans the WordPress installation to find active plugins. */ class Plugin_Locator { /** * The path processor for finding plugin paths. * * @var Path_Processor */ private $path_processor; /** * The constructor. * * @param Path_Processor $path_processor The Path_Processor instance. */ public function __construct( $path_processor ) { $this->path_processor = $path_processor; } /** * Finds the path to the current plugin. * * @return string $path The path to the current plugin. * * @throws \RuntimeException If the current plugin does not have an autoloader. */ public function find_current_plugin() { // Escape from `vendor/__DIR__` to root plugin directory. $plugin_directory = dirname( __DIR__, 2 ); // Use the path processor to ensure that this is an autoloader we're referencing. $path = $this->path_processor->find_directory_with_autoloader( $plugin_directory, array() ); if ( false === $path ) { throw new \RuntimeException( 'Failed to locate plugin ' . $plugin_directory ); } return $path; } /** * Checks a given option for plugin paths. * * @param string $option_name The option that we want to check for plugin information. * @param bool $site_option Indicates whether or not we want to check the site option. * * @return array $plugin_paths The list of absolute paths we've found. */ public function find_using_option( $option_name, $site_option = false ) { $raw = $site_option ? get_site_option( $option_name ) : get_option( $option_name ); if ( false === $raw ) { return array(); } return $this->convert_plugins_to_paths( $raw ); } /** * Checks for plugins in the `action` request parameter. * * @param string[] $allowed_actions The actions that we're allowed to return plugins for. * * @return array $plugin_paths The list of absolute paths we've found. */ public function find_using_request_action( $allowed_actions ) { /** * Note: we're not actually checking the nonce here because it's too early * in the execution. The pluggable functions are not yet loaded to give * plugins a chance to plug their versions. Therefore we're doing the bare * minimum: checking whether the nonce exists and it's in the right place. * The request will fail later if the nonce doesn't pass the check. */ if ( empty( $_REQUEST['_wpnonce'] ) ) { return array(); } // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Validated just below. $action = isset( $_REQUEST['action'] ) ? wp_unslash( $_REQUEST['action'] ) : false; if ( ! in_array( $action, $allowed_actions, true ) ) { return array(); } $plugin_slugs = array(); switch ( $action ) { case 'activate': case 'deactivate': if ( empty( $_REQUEST['plugin'] ) ) { break; } // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Validated by convert_plugins_to_paths. $plugin_slugs[] = wp_unslash( $_REQUEST['plugin'] ); break; case 'activate-selected': case 'deactivate-selected': if ( empty( $_REQUEST['checked'] ) ) { break; } // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Validated by convert_plugins_to_paths. $plugin_slugs = wp_unslash( $_REQUEST['checked'] ); break; } return $this->convert_plugins_to_paths( $plugin_slugs ); } /** * Given an array of plugin slugs or paths, this will convert them to absolute paths and filter * out the plugins that are not directory plugins. Note that array keys will also be included * if they are plugin paths! * * @param string[] $plugins Plugin paths or slugs to filter. * * @return string[] */ private function convert_plugins_to_paths( $plugins ) { if ( ! is_array( $plugins ) || empty( $plugins ) ) { return array(); } // We're going to look for plugins in the standard directories. $path_constants = array( WP_PLUGIN_DIR, WPMU_PLUGIN_DIR ); $plugin_paths = array(); foreach ( $plugins as $key => $value ) { $path = $this->path_processor->find_directory_with_autoloader( $key, $path_constants ); if ( $path ) { $plugin_paths[] = $path; } $path = $this->path_processor->find_directory_with_autoloader( $value, $path_constants ); if ( $path ) { $plugin_paths[] = $path; } } return $plugin_paths; } } jetpack-autoloader/src/class-plugins-handler.php 0000777 00000013071 15251741406 0016037 0 ustar 00 <?php /* HEADER */ // phpcs:ignore /** * This class handles locating and caching all of the active plugins. */ class Plugins_Handler { /** * The transient key for plugin paths. */ const TRANSIENT_KEY = 'jetpack_autoloader_plugin_paths'; /** * The locator for finding plugins in different locations. * * @var Plugin_Locator */ private $plugin_locator; /** * The processor for transforming cached paths. * * @var Path_Processor */ private $path_processor; /** * The constructor. * * @param Plugin_Locator $plugin_locator The locator for finding active plugins. * @param Path_Processor $path_processor The processor for transforming cached paths. */ public function __construct( $plugin_locator, $path_processor ) { $this->plugin_locator = $plugin_locator; $this->path_processor = $path_processor; } /** * Gets all of the active plugins we can find. * * @param bool $include_deactivating When true, plugins deactivating this request will be considered active. * @param bool $record_unknown When true, the current plugin will be marked as active and recorded when unknown. * * @return string[] */ public function get_active_plugins( $include_deactivating, $record_unknown ) { global $jetpack_autoloader_activating_plugins_paths; // We're going to build a unique list of plugins from a few different sources // to find all of our "active" plugins. While we need to return an integer // array, we're going to use an associative array internally to reduce // the amount of time that we're going to spend checking uniqueness // and merging different arrays together to form the output. $active_plugins = array(); // Make sure that plugins which have activated this request are considered as "active" even though // they probably won't be present in any option. if ( is_array( $jetpack_autoloader_activating_plugins_paths ) ) { foreach ( $jetpack_autoloader_activating_plugins_paths as $path ) { $active_plugins[ $path ] = $path; } } // This option contains all of the plugins that have been activated. $plugins = $this->plugin_locator->find_using_option( 'active_plugins' ); foreach ( $plugins as $path ) { $active_plugins[ $path ] = $path; } // This option contains all of the multisite plugins that have been activated. if ( is_multisite() ) { $plugins = $this->plugin_locator->find_using_option( 'active_sitewide_plugins', true ); foreach ( $plugins as $path ) { $active_plugins[ $path ] = $path; } } // These actions contain plugins that are being activated/deactivated during this request. $plugins = $this->plugin_locator->find_using_request_action( array( 'activate', 'activate-selected', 'deactivate', 'deactivate-selected' ) ); foreach ( $plugins as $path ) { $active_plugins[ $path ] = $path; } // When the current plugin isn't considered "active" there's a problem. // Since we're here, the plugin is active and currently being loaded. // We can support this case (mu-plugins and non-standard activation) // by adding the current plugin to the active list and marking it // as an unknown (activating) plugin. This also has the benefit // of causing a reset because the active plugins list has // been changed since it was saved in the global. $current_plugin = $this->plugin_locator->find_current_plugin(); if ( $record_unknown && ! in_array( $current_plugin, $active_plugins, true ) ) { $active_plugins[ $current_plugin ] = $current_plugin; $jetpack_autoloader_activating_plugins_paths[] = $current_plugin; } // When deactivating plugins aren't desired we should entirely remove them from the active list. if ( ! $include_deactivating ) { // These actions contain plugins that are being deactivated during this request. $plugins = $this->plugin_locator->find_using_request_action( array( 'deactivate', 'deactivate-selected' ) ); foreach ( $plugins as $path ) { unset( $active_plugins[ $path ] ); } } // Transform the array so that we don't have to worry about the keys interacting with other array types later. return array_values( $active_plugins ); } /** * Gets all of the cached plugins if there are any. * * @return string[] */ public function get_cached_plugins() { $cached = get_transient( self::TRANSIENT_KEY ); if ( ! is_array( $cached ) || empty( $cached ) ) { return array(); } // We need to expand the tokens to an absolute path for this webserver. return array_map( array( $this->path_processor, 'untokenize_path_constants' ), $cached ); } /** * Saves the plugin list to the cache. * * @param array $plugins The plugin list to save to the cache. */ public function cache_plugins( $plugins ) { // We store the paths in a tokenized form so that that webservers with different absolute paths don't break. $plugins = array_map( array( $this->path_processor, 'tokenize_path_constants' ), $plugins ); set_transient( self::TRANSIENT_KEY, $plugins ); } /** * Checks to see whether or not the plugin list given has changed when compared to the * shared `$jetpack_autoloader_cached_plugin_paths` global. This allows us to deal * with cases where the active list may change due to filtering.. * * @param string[] $plugins The plugins list to check against the global cache. * * @return bool True if the plugins have changed, otherwise false. */ public function have_plugins_changed( $plugins ) { global $jetpack_autoloader_cached_plugin_paths; if ( $jetpack_autoloader_cached_plugin_paths !== $plugins ) { $jetpack_autoloader_cached_plugin_paths = $plugins; return true; } return false; } } jetpack-autoloader/src/AutoloadFileWriter.php 0000777 00000005017 15251741406 0015406 0 ustar 00 <?php /** * Autoloader file writer. * * @package automattic/jetpack-autoloader */ namespace Automattic\Jetpack\Autoloader; use Composer\IO\IOInterface; /** * Class AutoloadFileWriter. */ class AutoloadFileWriter { /** * The file comment to use. */ const COMMENT = <<<AUTOLOADER_COMMENT /** * This file was automatically generated by automattic/jetpack-autoloader. * * @package automattic/jetpack-autoloader */ AUTOLOADER_COMMENT; /** * Copies autoloader files and replaces any placeholders in them. * * @param IOInterface|null $io An IO for writing to. * @param string $outDir The directory to place the autoloader files in. * @param string $suffix The suffix to use in the autoloader's namespace. */ public static function copyAutoloaderFiles( $io, $outDir, $suffix ) { $renameList = array( 'autoload.php' => '../autoload_packages.php', ); $ignoreList = array( 'AutoloadGenerator.php', 'AutoloadProcessor.php', 'CustomAutoloaderPlugin.php', 'ManifestGenerator.php', 'AutoloadFileWriter.php', ); // Copy all of the autoloader files. $files = scandir( __DIR__ ); foreach ( $files as $file ) { // Only PHP files will be copied. if ( substr( $file, -4 ) !== '.php' ) { continue; } if ( in_array( $file, $ignoreList, true ) ) { continue; } $newFile = $renameList[ $file ] ?? $file; $content = self::prepareAutoloaderFile( $file, $suffix ); $written = file_put_contents( $outDir . '/' . $newFile, $content ); if ( $io ) { if ( $written ) { $io->writeError( " <info>Generated: $newFile</info>" ); } else { $io->writeError( " <error>Error: $newFile</error>" ); } } } } /** * Prepares an autoloader file to be written to the destination. * * @param String $filename a file to prepare. * @param String $suffix Unique suffix used in the namespace. * * @return string */ private static function prepareAutoloaderFile( $filename, $suffix ) { $header = self::COMMENT; $header .= PHP_EOL; if ( $suffix === 'Current' ) { // Unit testing. $header .= 'namespace Automattic\Jetpack\Autoloader\jpCurrent;'; } else { $header .= 'namespace Automattic\Jetpack\Autoloader\jp' . $suffix . '\al' . preg_replace( '/[^0-9a-zA-Z]/', '_', AutoloadGenerator::VERSION ) . ';'; } $header .= PHP_EOL . PHP_EOL; $sourceLoader = fopen( __DIR__ . '/' . $filename, 'r' ); $file_contents = stream_get_contents( $sourceLoader ); return str_replace( '/* HEADER */', $header, $file_contents ); } } jetpack-autoloader/src/class-autoloader-handler.php 0000777 00000010465 15251741406 0016521 0 ustar 00 <?php /* HEADER */ // phpcs:ignore use Automattic\Jetpack\Autoloader\AutoloadGenerator; /** * This class selects the package version for the autoloader. */ class Autoloader_Handler { /** * The PHP_Autoloader instance. * * @var PHP_Autoloader */ private $php_autoloader; /** * The Hook_Manager instance. * * @var Hook_Manager */ private $hook_manager; /** * The Manifest_Reader instance. * * @var Manifest_Reader */ private $manifest_reader; /** * The Version_Selector instance. * * @var Version_Selector */ private $version_selector; /** * The constructor. * * @param PHP_Autoloader $php_autoloader The PHP_Autoloader instance. * @param Hook_Manager $hook_manager The Hook_Manager instance. * @param Manifest_Reader $manifest_reader The Manifest_Reader instance. * @param Version_Selector $version_selector The Version_Selector instance. */ public function __construct( $php_autoloader, $hook_manager, $manifest_reader, $version_selector ) { $this->php_autoloader = $php_autoloader; $this->hook_manager = $hook_manager; $this->manifest_reader = $manifest_reader; $this->version_selector = $version_selector; } /** * Checks to see whether or not an autoloader is currently in the process of initializing. * * @return bool */ public function is_initializing() { // If no version has been set it means that no autoloader has started initializing yet. global $jetpack_autoloader_latest_version; if ( ! isset( $jetpack_autoloader_latest_version ) ) { return false; } // When the version is set but the classmap is not it ALWAYS means that this is the // latest autoloader and is being included by an older one. global $jetpack_packages_classmap; if ( empty( $jetpack_packages_classmap ) ) { return true; } // Version 2.4.0 added a new global and altered the reset semantics. We need to check // the other global as well since it may also point at initialization. // Note: We don't need to check for the class first because every autoloader that // will set the latest version global requires this class in the classmap. $replacing_version = $jetpack_packages_classmap[ AutoloadGenerator::class ]['version']; if ( $this->version_selector->is_dev_version( $replacing_version ) || version_compare( $replacing_version, '2.4.0.0', '>=' ) ) { global $jetpack_autoloader_loader; if ( ! isset( $jetpack_autoloader_loader ) ) { return true; } } return false; } /** * Activates an autoloader using the given plugins and activates it. * * @param string[] $plugins The plugins to initialize the autoloader for. */ public function activate_autoloader( $plugins ) { global $jetpack_packages_psr4; $jetpack_packages_psr4 = array(); $this->manifest_reader->read_manifests( $plugins, 'vendor/composer/jetpack_autoload_psr4.php', $jetpack_packages_psr4 ); global $jetpack_packages_classmap; $jetpack_packages_classmap = array(); $this->manifest_reader->read_manifests( $plugins, 'vendor/composer/jetpack_autoload_classmap.php', $jetpack_packages_classmap ); global $jetpack_packages_filemap; $jetpack_packages_filemap = array(); $this->manifest_reader->read_manifests( $plugins, 'vendor/composer/jetpack_autoload_filemap.php', $jetpack_packages_filemap ); $loader = new Version_Loader( $this->version_selector, $jetpack_packages_classmap, $jetpack_packages_psr4, $jetpack_packages_filemap ); $this->php_autoloader->register_autoloader( $loader ); // Now that the autoloader is active we can load the filemap. $loader->load_filemap(); } /** * Resets the active autoloader and all related global state. */ public function reset_autoloader() { $this->php_autoloader->unregister_autoloader(); $this->hook_manager->reset(); // Clear all of the autoloader globals so that older autoloaders don't do anything strange. global $jetpack_autoloader_latest_version; $jetpack_autoloader_latest_version = null; global $jetpack_packages_classmap; $jetpack_packages_classmap = array(); // Must be array to avoid exceptions in old autoloaders! global $jetpack_packages_psr4; $jetpack_packages_psr4 = array(); // Must be array to avoid exceptions in old autoloaders! global $jetpack_packages_filemap; $jetpack_packages_filemap = array(); // Must be array to avoid exceptions in old autoloaders! } } jetpack-autoloader/src/ManifestGenerator.php 0000777 00000006101 15251741406 0015251 0 ustar 00 <?php /** * Manifest Generator. * * @package automattic/jetpack-autoloader */ // phpcs:disable WordPress.PHP.DevelopmentFunctions.error_log_var_export namespace Automattic\Jetpack\Autoloader; /** * Class ManifestGenerator. */ class ManifestGenerator { /** * Builds a manifest file for the given autoloader type. * * @param string $autoloaderType The type of autoloader to build a manifest for. * @param string $fileName The filename of the manifest. * @param array $content The manifest content to generate using. * * @return string|null $manifestFile * @throws \InvalidArgumentException When an invalid autoloader type is given. */ public static function buildManifest( $autoloaderType, $fileName, $content ) { if ( empty( $content ) ) { return null; } switch ( $autoloaderType ) { case 'classmap': case 'files': return self::buildStandardManifest( $fileName, $content ); case 'psr-4': return self::buildPsr4Manifest( $fileName, $content ); } throw new \InvalidArgumentException( 'An invalid manifest type of ' . $autoloaderType . ' was passed!' ); } /** * Builds the contents for the standard manifest file. * * @param string $fileName The filename we are building. * @param array $manifestData The formatted data for the manifest. * * @return string|null $manifestFile */ private static function buildStandardManifest( $fileName, $manifestData ) { $fileContent = PHP_EOL; foreach ( $manifestData as $key => $data ) { $key = var_export( $key, true ); $versionCode = var_export( $data['version'], true ); $fileContent .= <<<MANIFEST_CODE $key => array( 'version' => $versionCode, 'path' => {$data['path']} ), MANIFEST_CODE; $fileContent .= PHP_EOL; } return self::buildFile( $fileName, $fileContent ); } /** * Builds the contents for the PSR-4 manifest file. * * @param string $fileName The filename we are building. * @param array $namespaces The formatted PSR-4 data for the manifest. * * @return string|null $manifestFile */ private static function buildPsr4Manifest( $fileName, $namespaces ) { $fileContent = PHP_EOL; foreach ( $namespaces as $namespace => $data ) { $namespaceCode = var_export( $namespace, true ); $versionCode = var_export( $data['version'], true ); $pathCode = 'array( ' . implode( ', ', $data['path'] ) . ' )'; $fileContent .= <<<MANIFEST_CODE $namespaceCode => array( 'version' => $versionCode, 'path' => $pathCode ), MANIFEST_CODE; $fileContent .= PHP_EOL; } return self::buildFile( $fileName, $fileContent ); } /** * Generate the PHP that will be used in the file. * * @param string $fileName The filename we are building. * @param string $content The content to be written into the file. * * @return string $fileContent */ private static function buildFile( $fileName, $content ) { return <<<INCLUDE_FILE <?php // This file `$fileName` was auto generated by automattic/jetpack-autoloader. \$vendorDir = dirname(__DIR__); \$baseDir = dirname(\$vendorDir); return array($content); INCLUDE_FILE; } } jetpack-autoloader/src/class-path-processor.php 0000777 00000012601 15251741406 0015712 0 ustar 00 <?php /* HEADER */ // phpcs:ignore /** * This class handles dealing with paths for the autoloader. */ class Path_Processor { /** * Given a path this will replace any of the path constants with a token to represent it. * * @param string $path The path we want to process. * * @return string The tokenized path. */ public function tokenize_path_constants( $path ) { $path = wp_normalize_path( $path ); $constants = $this->get_normalized_constants(); foreach ( $constants as $constant => $constant_path ) { $len = strlen( $constant_path ); if ( substr( $path, 0, $len ) !== $constant_path ) { continue; } return substr_replace( $path, '{{' . $constant . '}}', 0, $len ); } return $path; } /** * Given a path this will replace any of the path constant tokens with the expanded path. * * @param string $tokenized_path The path we want to process. * * @return string The expanded path. */ public function untokenize_path_constants( $tokenized_path ) { $tokenized_path = wp_normalize_path( $tokenized_path ); $constants = $this->get_normalized_constants(); foreach ( $constants as $constant => $constant_path ) { $constant = '{{' . $constant . '}}'; $len = strlen( $constant ); if ( substr( $tokenized_path, 0, $len ) !== $constant ) { continue; } return $this->get_real_path( substr_replace( $tokenized_path, $constant_path, 0, $len ) ); } return $tokenized_path; } /** * Given a file and an array of places it might be, this will find the absolute path and return it. * * @param string $file The plugin or theme file to resolve. * @param array $directories_to_check The directories we should check for the file if it isn't an absolute path. * * @return string|false Returns the absolute path to the directory, otherwise false. */ public function find_directory_with_autoloader( $file, $directories_to_check ) { $file = wp_normalize_path( $file ); if ( ! $this->is_absolute_path( $file ) ) { $file = $this->find_absolute_plugin_path( $file, $directories_to_check ); if ( ! isset( $file ) ) { return false; } } // We need the real path for consistency with __DIR__ paths. $file = $this->get_real_path( $file ); // phpcs:disable WordPress.PHP.NoSilencedErrors.Discouraged $directory = @is_file( $file ) ? dirname( $file ) : $file; if ( ! @is_file( $directory . '/vendor/composer/jetpack_autoload_classmap.php' ) ) { return false; } // phpcs:enable WordPress.PHP.NoSilencedErrors.Discouraged return $directory; } /** * Fetches an array of normalized paths keyed by the constant they came from. * * @return string[] The normalized paths keyed by the constant. */ private function get_normalized_constants() { $raw_constants = array( // Order the constants from most-specific to least-specific. 'WP_PLUGIN_DIR', 'WPMU_PLUGIN_DIR', 'WP_CONTENT_DIR', 'ABSPATH', ); $constants = array(); foreach ( $raw_constants as $raw ) { if ( ! defined( $raw ) ) { continue; } $path = wp_normalize_path( constant( $raw ) ); if ( isset( $path ) ) { $constants[ $raw ] = $path; } } return $constants; } /** * Indicates whether or not a path is absolute. * * @param string $path The path to check. * * @return bool True if the path is absolute, otherwise false. */ private function is_absolute_path( $path ) { if ( empty( $path ) || 0 === strlen( $path ) || '.' === $path[0] ) { return false; } // Absolute paths on Windows may begin with a drive letter. if ( preg_match( '/^[a-zA-Z]:[\/\\\\]/', $path ) ) { return true; } // A path starting with / or \ is absolute; anything else is relative. return ( '/' === $path[0] || '\\' === $path[0] ); } /** * Given a file and a list of directories to check, this method will try to figure out * the absolute path to the file in question. * * @param string $normalized_path The normalized path to the plugin or theme file to resolve. * @param array $directories_to_check The directories we should check for the file if it isn't an absolute path. * * @return string|null The absolute path to the plugin file, otherwise null. */ private function find_absolute_plugin_path( $normalized_path, $directories_to_check ) { // We're only able to find the absolute path for plugin/theme PHP files. if ( ! is_string( $normalized_path ) || '.php' !== substr( $normalized_path, -4 ) ) { return null; } foreach ( $directories_to_check as $directory ) { $normalized_check = wp_normalize_path( trailingslashit( $directory ) ) . $normalized_path; // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged if ( @is_file( $normalized_check ) ) { return $normalized_check; } } return null; } /** * Given a path this will figure out the real path that we should be using. * * @param string $path The path to resolve. * * @return string The resolved path. */ private function get_real_path( $path ) { // We want to resolve symbolic links for consistency with __DIR__ paths. // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged $real_path = @realpath( $path ); if ( false === $real_path ) { // Let the autoloader deal with paths that don't exist. $real_path = $path; } // Using realpath will make it platform-specific so we must normalize it after. if ( $path !== $real_path ) { $real_path = wp_normalize_path( $real_path ); } return $real_path; } } jetpack-autoloader/src/class-autoloader-locator.php 0000777 00000003664 15251741406 0016552 0 ustar 00 <?php /* HEADER */ // phpcs:ignore use Automattic\Jetpack\Autoloader\AutoloadGenerator; /** * This class locates autoloaders. */ class Autoloader_Locator { /** * The object for comparing autoloader versions. * * @var Version_Selector */ private $version_selector; /** * The constructor. * * @param Version_Selector $version_selector The version selector object. */ public function __construct( $version_selector ) { $this->version_selector = $version_selector; } /** * Finds the path to the plugin with the latest autoloader. * * @param array $plugin_paths An array of plugin paths. * @param string $latest_version The latest version reference. @phan-output-reference. * * @return string|null */ public function find_latest_autoloader( $plugin_paths, &$latest_version ) { $latest_plugin = null; foreach ( $plugin_paths as $plugin_path ) { $version = $this->get_autoloader_version( $plugin_path ); if ( ! $version || ! $this->version_selector->is_version_update_required( $latest_version, $version ) ) { continue; } $latest_version = $version; $latest_plugin = $plugin_path; } return $latest_plugin; } /** * Gets the path to the autoloader. * * @param string $plugin_path The path to the plugin. * * @return string */ public function get_autoloader_path( $plugin_path ) { return trailingslashit( $plugin_path ) . 'vendor/autoload_packages.php'; } /** * Gets the version for the autoloader. * * @param string $plugin_path The path to the plugin. * * @return string|null */ public function get_autoloader_version( $plugin_path ) { $classmap = trailingslashit( $plugin_path ) . 'vendor/composer/jetpack_autoload_classmap.php'; if ( ! file_exists( $classmap ) ) { return null; } $classmap = require $classmap; if ( isset( $classmap[ AutoloadGenerator::class ] ) ) { return $classmap[ AutoloadGenerator::class ]['version']; } return null; } } jetpack-autoloader/src/class-version-loader.php 0000777 00000010300 15251741406 0015664 0 ustar 00 <?php /* HEADER */ // phpcs:ignore /** * This class loads other classes based on given parameters. */ class Version_Loader { /** * The Version_Selector object. * * @var Version_Selector */ private $version_selector; /** * A map of available classes and their version and file path. * * @var array */ private $classmap; /** * A map of PSR-4 namespaces and their version and directory path. * * @var array */ private $psr4_map; /** * A map of all the files that we should load. * * @var array */ private $filemap; /** * The constructor. * * @param Version_Selector $version_selector The Version_Selector object. * @param array $classmap The verioned classmap to load using. * @param array $psr4_map The versioned PSR-4 map to load using. * @param array $filemap The versioned filemap to load. */ public function __construct( $version_selector, $classmap, $psr4_map, $filemap ) { $this->version_selector = $version_selector; $this->classmap = $classmap; $this->psr4_map = $psr4_map; $this->filemap = $filemap; } /** * Fetch the classmap. * * @since 3.1.0 * @return array<string, array> */ public function get_class_map() { return $this->classmap; } /** * Fetch the psr-4 mappings. * * @since 3.1.0 * @return array<string, array> */ public function get_psr4_map() { return $this->psr4_map; } /** * Finds the file path for the given class. * * @param string $class_name The class to find. * * @return string|null $file_path The path to the file if found, null if no class was found. */ public function find_class_file( $class_name ) { $data = $this->select_newest_file( $this->classmap[ $class_name ] ?? null, $this->find_psr4_file( $class_name ) ); if ( ! isset( $data ) ) { return null; } return $data['path']; } /** * Load all of the files in the filemap. */ public function load_filemap() { if ( empty( $this->filemap ) ) { return; } foreach ( $this->filemap as $file_identifier => $file_data ) { if ( empty( $GLOBALS['__composer_autoload_files'][ $file_identifier ] ) ) { require_once $file_data['path']; $GLOBALS['__composer_autoload_files'][ $file_identifier ] = true; } } } /** * Compares different class sources and returns the newest. * * @param array|null $classmap_data The classmap class data. * @param array|null $psr4_data The PSR-4 class data. * * @return array|null $data */ private function select_newest_file( $classmap_data, $psr4_data ) { if ( ! isset( $classmap_data ) ) { return $psr4_data; } elseif ( ! isset( $psr4_data ) ) { return $classmap_data; } if ( $this->version_selector->is_version_update_required( $classmap_data['version'], $psr4_data['version'] ) ) { return $psr4_data; } return $classmap_data; } /** * Finds the file for a given class in a PSR-4 namespace. * * @param string $class_name The class to find. * * @return array|null $data The version and path path to the file if found, null otherwise. */ private function find_psr4_file( $class_name ) { if ( empty( $this->psr4_map ) ) { return null; } // Don't bother with classes that have no namespace. $class_index = strrpos( $class_name, '\\' ); if ( ! $class_index ) { return null; } $class_for_path = str_replace( '\\', '/', $class_name ); // Search for the namespace by iteratively cutting off the last segment until // we find a match. This allows us to check the most-specific namespaces // first as well as minimize the amount of time spent looking. for ( $class_namespace = substr( $class_name, 0, $class_index ); ! empty( $class_namespace ); $class_namespace = substr( $class_namespace, 0, strrpos( $class_namespace, '\\' ) ) ) { $namespace = $class_namespace . '\\'; if ( ! isset( $this->psr4_map[ $namespace ] ) ) { continue; } $data = $this->psr4_map[ $namespace ]; foreach ( $data['path'] as $path ) { $path .= '/' . substr( $class_for_path, strlen( $namespace ) ) . '.php'; if ( file_exists( $path ) ) { return array( 'version' => $data['version'], 'path' => $path, ); } } } return null; } } jetpack-autoloader/src/autoload.php 0000777 00000000173 15251741406 0013447 0 ustar 00 <?php /* HEADER */ // phpcs:ignore require_once __DIR__ . '/jetpack-autoloader/class-autoloader.php'; Autoloader::init(); jetpack-autoloader/src/class-version-selector.php 0000777 00000003163 15251741406 0016247 0 ustar 00 <?php /* HEADER */ // phpcs:ignore /** * Used to select package versions. */ class Version_Selector { /** * Checks whether the selected package version should be updated. Composer development * package versions ('9999999-dev' or versions that start with 'dev-') are favored * when the JETPACK_AUTOLOAD_DEV constant is set to true. * * @param String $selected_version The currently selected package version. * @param String $compare_version The package version that is being evaluated to * determine if the version needs to be updated. * * @return bool Returns true if the selected package version should be updated, * else false. */ public function is_version_update_required( $selected_version, $compare_version ) { $use_dev_versions = defined( 'JETPACK_AUTOLOAD_DEV' ) && JETPACK_AUTOLOAD_DEV; if ( $selected_version === null ) { return true; } if ( $use_dev_versions && $this->is_dev_version( $selected_version ) ) { return false; } if ( $this->is_dev_version( $compare_version ) ) { if ( $use_dev_versions ) { return true; } else { return false; } } if ( version_compare( $selected_version, $compare_version, '<' ) ) { return true; } return false; } /** * Checks whether the given package version is a development version. * * @param String $version The package version. * * @return bool True if the version is a dev version, else false. */ public function is_dev_version( $version ) { if ( 'dev-' === substr( $version, 0, 4 ) || '9999999-dev' === $version ) { return true; } return false; } } jetpack-autoloader/src/class-hook-manager.php 0000777 00000003643 15251741406 0015317 0 ustar 00 <?php /* HEADER */ // phpcs:ignore /** * Allows the latest autoloader to register hooks that can be removed when the autoloader is reset. */ class Hook_Manager { /** * An array containing all of the hooks that we've registered. * * @var array */ private $registered_hooks; /** * The constructor. */ public function __construct() { $this->registered_hooks = array(); } /** * Adds an action to WordPress and registers it internally. * * @param string $tag The name of the action which is hooked. * @param callable $callable The function to call. * @param int $priority Used to specify the priority of the action. * @param int $accepted_args Used to specify the number of arguments the callable accepts. */ public function add_action( $tag, $callable, $priority = 10, $accepted_args = 1 ) { $this->registered_hooks[ $tag ][] = array( 'priority' => $priority, 'callable' => $callable, ); add_action( $tag, $callable, $priority, $accepted_args ); } /** * Adds a filter to WordPress and registers it internally. * * @param string $tag The name of the filter which is hooked. * @param callable $callable The function to call. * @param int $priority Used to specify the priority of the filter. * @param int $accepted_args Used to specify the number of arguments the callable accepts. */ public function add_filter( $tag, $callable, $priority = 10, $accepted_args = 1 ) { $this->registered_hooks[ $tag ][] = array( 'priority' => $priority, 'callable' => $callable, ); add_filter( $tag, $callable, $priority, $accepted_args ); } /** * Removes all of the registered hooks. */ public function reset() { foreach ( $this->registered_hooks as $tag => $hooks ) { foreach ( $hooks as $hook ) { remove_filter( $tag, $hook['callable'], $hook['priority'] ); } } $this->registered_hooks = array(); } } jetpack-autoloader/src/class-manifest-reader.php 0000777 00000004673 15251741406 0016021 0 ustar 00 <?php /* HEADER */ // phpcs:ignore /** * This class reads autoloader manifest files. */ class Manifest_Reader { /** * The Version_Selector object. * * @var Version_Selector */ private $version_selector; /** * The constructor. * * @param Version_Selector $version_selector The Version_Selector object. */ public function __construct( $version_selector ) { $this->version_selector = $version_selector; } /** * Reads all of the manifests in the given plugin paths. * * @param array $plugin_paths The paths to the plugins we're loading the manifest in. * @param string $manifest_path The path that we're loading the manifest from in each plugin. * @param array $path_map The path map to add the contents of the manifests to. * * @return array $path_map The path map we've built using the manifests in each plugin. */ public function read_manifests( $plugin_paths, $manifest_path, &$path_map ) { $file_paths = array_map( function ( $path ) use ( $manifest_path ) { return trailingslashit( $path ) . $manifest_path; }, $plugin_paths ); foreach ( $file_paths as $path ) { $this->register_manifest( $path, $path_map ); } return $path_map; } /** * Registers a plugin's manifest file with the path map. * * @param string $manifest_path The absolute path to the manifest that we're loading. * @param array $path_map The path map to add the contents of the manifest to. */ protected function register_manifest( $manifest_path, &$path_map ) { if ( ! is_readable( $manifest_path ) ) { return; } $manifest = require $manifest_path; if ( ! is_array( $manifest ) ) { return; } foreach ( $manifest as $key => $data ) { $this->register_record( $key, $data, $path_map ); } } /** * Registers an entry from the manifest in the path map. * * @param string $key The identifier for the entry we're registering. * @param array $data The data for the entry we're registering. * @param array $path_map The path map to add the contents of the manifest to. */ protected function register_record( $key, $data, &$path_map ) { if ( isset( $path_map[ $key ]['version'] ) ) { $selected_version = $path_map[ $key ]['version']; } else { $selected_version = null; } if ( $this->version_selector->is_version_update_required( $selected_version, $data['version'] ) ) { $path_map[ $key ] = array( 'version' => $data['version'], 'path' => $data['path'], ); } } } jetpack-autoloader/src/AutoloadProcessor.php 0000777 00000011671 15251741406 0015314 0 ustar 00 <?php /** * Autoload Processor. * * @package automattic/jetpack-autoloader */ namespace Automattic\Jetpack\Autoloader; /** * Class AutoloadProcessor. */ class AutoloadProcessor { /** * A callable for scanning a directory for all of its classes. * * @var callable */ private $classmapScanner; /** * A callable for transforming a path into one to be used in code. * * @var callable */ private $pathCodeTransformer; /** * The constructor. * * @param callable $classmapScanner A callable for scanning a directory for all of its classes. * @param callable $pathCodeTransformer A callable for transforming a path into one to be used in code. */ public function __construct( $classmapScanner, $pathCodeTransformer ) { $this->classmapScanner = $classmapScanner; $this->pathCodeTransformer = $pathCodeTransformer; } /** * Processes the classmap autoloads into a relative path format including the version for each file. * * @param array $autoloads The autoloads we are processing. * @param bool $scanPsrPackages Whether or not PSR packages should be converted to a classmap. * * @return array|null $processed * @phan-param array{classmap:?array{path:string,version:string}[],psr-4:?array<string,array{path:string,version:string}[]>,psr-0:?array<string,array{path:string,version:string}[]>} $autoloads */ public function processClassmap( $autoloads, $scanPsrPackages ) { // We can't scan PSR packages if we don't actually have any. if ( empty( $autoloads['psr-4'] ) ) { $scanPsrPackages = false; } if ( empty( $autoloads['classmap'] ) && ! $scanPsrPackages ) { return null; } $excludedClasses = null; if ( ! empty( $autoloads['exclude-from-classmap'] ) ) { $excludedClasses = '{(' . implode( '|', $autoloads['exclude-from-classmap'] ) . ')}'; } $processed = array(); if ( $scanPsrPackages ) { foreach ( $autoloads['psr-4'] as $namespace => $sources ) { $namespace = empty( $namespace ) ? null : $namespace; foreach ( $sources as $source ) { $classmap = call_user_func( $this->classmapScanner, $source['path'], $excludedClasses, $namespace ); foreach ( $classmap as $class => $path ) { $processed[ $class ] = array( 'version' => $source['version'], 'path' => call_user_func( $this->pathCodeTransformer, $path ), ); } } } } /* * PSR-0 namespaces are converted to classmaps for both optimized and unoptimized autoloaders because any new * development should use classmap or PSR-4 autoloading. */ if ( ! empty( $autoloads['psr-0'] ) ) { foreach ( $autoloads['psr-0'] as $namespace => $sources ) { $namespace = empty( $namespace ) ? null : $namespace; foreach ( $sources as $source ) { $classmap = call_user_func( $this->classmapScanner, $source['path'], $excludedClasses, $namespace ); foreach ( $classmap as $class => $path ) { $processed[ $class ] = array( 'version' => $source['version'], 'path' => call_user_func( $this->pathCodeTransformer, $path ), ); } } } } if ( ! empty( $autoloads['classmap'] ) ) { foreach ( $autoloads['classmap'] as $package ) { $classmap = call_user_func( $this->classmapScanner, $package['path'], $excludedClasses, null ); foreach ( $classmap as $class => $path ) { $processed[ $class ] = array( 'version' => $package['version'], 'path' => call_user_func( $this->pathCodeTransformer, $path ), ); } } } ksort( $processed ); return $processed; } /** * Processes the PSR-4 autoloads into a relative path format including the version for each file. * * @param array $autoloads The autoloads we are processing. * @param bool $scanPsrPackages Whether or not PSR packages should be converted to a classmap. * * @return array|null $processed */ public function processPsr4Packages( $autoloads, $scanPsrPackages ) { if ( $scanPsrPackages || empty( $autoloads['psr-4'] ) ) { return null; } $processed = array(); foreach ( $autoloads['psr-4'] as $namespace => $packages ) { $namespace = empty( $namespace ) ? null : $namespace; $paths = array(); foreach ( $packages as $package ) { $paths[] = call_user_func( $this->pathCodeTransformer, $package['path'] ); } $processed[ $namespace ] = array( 'version' => $package['version'], 'path' => $paths, ); } return $processed; } /** * Processes the file autoloads into a relative format including the version for each file. * * @param array $autoloads The autoloads we are processing. * * @return array|null $processed */ public function processFiles( $autoloads ) { if ( empty( $autoloads['files'] ) ) { return null; } $processed = array(); foreach ( $autoloads['files'] as $file_id => $package ) { $processed[ $file_id ] = array( 'version' => $package['version'], 'path' => call_user_func( $this->pathCodeTransformer, $package['path'] ), ); } return $processed; } } jetpack-autoloader/src/class-latest-autoloader-guard.php 0000777 00000013222 15251741406 0017472 0 ustar 00 <?php /* HEADER */ // phpcs:ignore /** * This class ensures that we're only executing the latest autoloader. */ class Latest_Autoloader_Guard { /** * The Plugins_Handler instance. * * @var Plugins_Handler */ private $plugins_handler; /** * The Autoloader_Handler instance. * * @var Autoloader_Handler */ private $autoloader_handler; /** * The Autoloader_locator instance. * * @var Autoloader_Locator */ private $autoloader_locator; /** * The constructor. * * @param Plugins_Handler $plugins_handler The Plugins_Handler instance. * @param Autoloader_Handler $autoloader_handler The Autoloader_Handler instance. * @param Autoloader_Locator $autoloader_locator The Autoloader_Locator instance. */ public function __construct( $plugins_handler, $autoloader_handler, $autoloader_locator ) { $this->plugins_handler = $plugins_handler; $this->autoloader_handler = $autoloader_handler; $this->autoloader_locator = $autoloader_locator; } /** * Indicates whether or not the autoloader should be initialized. Note that this function * has the side-effect of actually loading the latest autoloader in the event that this * is not it. * * @param string $current_plugin The current plugin we're checking. * @param string[] $plugins The active plugins to check for autoloaders in. * @param bool $was_included_by_autoloader Indicates whether or not this autoloader was included by another. * * @return bool True if we should stop initialization, otherwise false. */ public function should_stop_init( $current_plugin, $plugins, $was_included_by_autoloader ) { global $jetpack_autoloader_latest_version; // We need to reset the autoloader when the plugins change because // that means the autoloader was generated with a different list. if ( $this->plugins_handler->have_plugins_changed( $plugins ) ) { $this->autoloader_handler->reset_autoloader(); } // When the latest autoloader has already been found we don't need to search for it again. // We should take care however because this will also trigger if the autoloader has been // included by an older one. if ( isset( $jetpack_autoloader_latest_version ) && ! $was_included_by_autoloader ) { return true; } $latest_plugin = $this->autoloader_locator->find_latest_autoloader( $plugins, $jetpack_autoloader_latest_version ); if ( isset( $latest_plugin ) && $latest_plugin !== $current_plugin ) { require $this->autoloader_locator->get_autoloader_path( $latest_plugin ); return true; } return false; } /** * Check for conflicting autoloaders. * * A common source of strange and confusing problems is when another plugin * registers a Composer autoloader at a higher priority that us. If enabled, * check for this problem and warn about it. * * Called from the plugins_loaded hook. * * @since 3.1.0 * @return void */ public function check_for_conflicting_autoloaders() { if ( ! defined( 'JETPACK_AUTOLOAD_DEBUG_CONFLICTING_LOADERS' ) || ! JETPACK_AUTOLOAD_DEBUG_CONFLICTING_LOADERS ) { return; } global $jetpack_autoloader_loader; if ( ! isset( $jetpack_autoloader_loader ) ) { return; } $prefixes = array(); foreach ( ( $jetpack_autoloader_loader->get_class_map() ?? array() ) as $classname => $data ) { $parts = explode( '\\', trim( $classname, '\\' ) ); array_pop( $parts ); while ( $parts ) { $prefixes[ implode( '\\', $parts ) . '\\' ] = true; array_pop( $parts ); } } foreach ( ( $jetpack_autoloader_loader->get_psr4_map() ?? array() ) as $prefix => $data ) { $parts = explode( '\\', trim( $prefix, '\\' ) ); while ( $parts ) { $prefixes[ implode( '\\', $parts ) . '\\' ] = true; array_pop( $parts ); } } $autoload_chain = spl_autoload_functions(); if ( ! $autoload_chain ) { return; } foreach ( $autoload_chain as $autoloader ) { // No need to check anything after us. if ( is_array( $autoloader ) && is_string( $autoloader[0] ) && substr( $autoloader[0], 0, strlen( __NAMESPACE__ ) + 1 ) === __NAMESPACE__ . '\\' ) { break; } // We can check Composer autoloaders easily enough. if ( is_array( $autoloader ) && $autoloader[0] instanceof \Composer\Autoload\ClassLoader && is_callable( array( $autoloader[0], 'getPrefixesPsr4' ) ) ) { $composer_autoloader = $autoloader[0]; foreach ( $composer_autoloader->getClassMap() as $classname => $path ) { if ( $jetpack_autoloader_loader->find_class_file( $classname ) ) { $msg = "A Composer autoloader is registered with a higher priority than the Jetpack Autoloader and would also handle some of the classes we handle (e.g. $classname => $path). This may cause strange and confusing problems."; wp_trigger_error( '', $msg ); continue 2; } } foreach ( $composer_autoloader->getPrefixesPsr4() as $prefix => $paths ) { if ( isset( $prefixes[ $prefix ] ) ) { $path = array_pop( $paths ); $msg = "A Composer autoloader is registered with a higher priority than the Jetpack Autoloader and would also handle some of the namespaces we handle (e.g. $prefix => $path). This may cause strange and confusing problems."; wp_trigger_error( '', $msg ); continue 2; } } foreach ( $composer_autoloader->getPrefixes() as $prefix => $paths ) { if ( isset( $prefixes[ $prefix ] ) ) { $path = array_pop( $paths ); $msg = "A Composer autoloader is registered with a higher priority than the Jetpack Autoloader and would also handle some of the namespaces we handle (e.g. $prefix => $path). This may cause strange and confusing problems."; wp_trigger_error( '', $msg ); continue 2; } } } } } } jetpack-autoloader/src/AutoloadGenerator.php 0000777 00000032521 15251741406 0015260 0 ustar 00 <?php /** * Autoloader Generator. * * @package automattic/jetpack-autoloader */ namespace Automattic\Jetpack\Autoloader; use Composer\Composer; use Composer\Config; use Composer\Installer\InstallationManager; use Composer\IO\IOInterface; use Composer\Package\PackageInterface; use Composer\Repository\InstalledRepositoryInterface; use Composer\Util\Filesystem; use Composer\Util\PackageSorter; /** * Class AutoloadGenerator. */ class AutoloadGenerator { const VERSION = '5.0.0'; /** * IO object. * * @var IOInterface IO object. */ private $io; /** * The filesystem utility. * * @var Filesystem */ private $filesystem; /** * Instantiate an AutoloadGenerator object. * * @param IOInterface $io IO object. */ public function __construct( IOInterface $io ) { $this->io = $io; $this->filesystem = new Filesystem(); } /** * Dump the Jetpack autoloader files. * * @param Composer $composer The Composer object. * @param Config $config Config object. * @param InstalledRepositoryInterface $localRepo Installed Repository object. * @param PackageInterface $mainPackage Main Package object. * @param InstallationManager $installationManager Manager for installing packages. * @param string $targetDir Path to the current target directory. * @param bool $scanPsrPackages Whether or not PSR packages should be converted to a classmap. * @param string $suffix The autoloader suffix. */ public function dump( Composer $composer, Config $config, InstalledRepositoryInterface $localRepo, PackageInterface $mainPackage, InstallationManager $installationManager, $targetDir, $scanPsrPackages = false, $suffix = null ) { $this->filesystem->ensureDirectoryExists( $config->get( 'vendor-dir' ) ); $packageMap = $composer->getAutoloadGenerator()->buildPackageMap( $installationManager, $mainPackage, $localRepo->getCanonicalPackages() ); $autoloads = $this->parseAutoloads( $packageMap, $mainPackage ); // Convert the autoloads into a format that the manifest generator can consume more easily. $basePath = $this->filesystem->normalizePath( realpath( getcwd() ) ); $vendorPath = $this->filesystem->normalizePath( realpath( $config->get( 'vendor-dir' ) ) ); $processedAutoloads = $this->processAutoloads( $autoloads, $scanPsrPackages, $vendorPath, $basePath ); unset( $packageMap, $autoloads ); // Make sure none of the legacy files remain that can lead to problems with the autoloader. $this->removeLegacyFiles( $vendorPath ); // Write all of the files now that we're done. $this->writeAutoloaderFiles( $vendorPath . '/jetpack-autoloader/', $suffix ); $this->writeManifests( $vendorPath . '/' . $targetDir, $processedAutoloads ); if ( ! $scanPsrPackages ) { $this->io->writeError( '<warning>You are generating an unoptimized autoloader. If this is a production build, consider using the -o option.</warning>' ); } } /** * Compiles an ordered list of namespace => path mappings * * @param array $packageMap Array of array(package, installDir-relative-to-composer.json). * @param PackageInterface $mainPackage Main package instance. * * @return array The list of path mappings. */ public function parseAutoloads( array $packageMap, PackageInterface $mainPackage ) { $rootPackageMap = array_shift( $packageMap ); $sortedPackageMap = $this->sortPackageMap( $packageMap ); $sortedPackageMap[] = $rootPackageMap; array_unshift( $packageMap, $rootPackageMap ); $psr0 = $this->parseAutoloadsType( $packageMap, 'psr-0', $mainPackage ); $psr4 = $this->parseAutoloadsType( $packageMap, 'psr-4', $mainPackage ); $classmap = $this->parseAutoloadsType( array_reverse( $sortedPackageMap ), 'classmap', $mainPackage ); $files = $this->parseAutoloadsType( $sortedPackageMap, 'files', $mainPackage ); krsort( $psr0 ); krsort( $psr4 ); return array( 'psr-0' => $psr0, 'psr-4' => $psr4, 'classmap' => $classmap, 'files' => $files, ); } /** * Sorts packages by dependency weight * * Packages of equal weight retain the original order * * @param array $packageMap The package map. * * @return array */ protected function sortPackageMap( array $packageMap ) { $packages = array(); $paths = array(); foreach ( $packageMap as $item ) { list( $package, $path ) = $item; $name = $package->getName(); $packages[ $name ] = $package; $paths[ $name ] = $path; } $sortedPackages = PackageSorter::sortPackages( $packages ); $sortedPackageMap = array(); foreach ( $sortedPackages as $package ) { $name = $package->getName(); $sortedPackageMap[] = array( $packages[ $name ], $paths[ $name ] ); } return $sortedPackageMap; } /** * Returns the file identifier. * * @param PackageInterface $package The package instance. * @param string $path The path. */ protected function getFileIdentifier( PackageInterface $package, $path ) { return md5( $package->getName() . ':' . $path ); } /** * Returns the path code for the given path. * * @param Filesystem $filesystem The filesystem instance. * @param string $basePath The base path. * @param string $vendorPath The vendor path. * @param string $path The path. * * @return string The path code. */ protected function getPathCode( Filesystem $filesystem, $basePath, $vendorPath, $path ) { if ( ! $filesystem->isAbsolutePath( $path ) ) { $path = $basePath . '/' . $path; } $path = $filesystem->normalizePath( $path ); $baseDir = ''; if ( 0 === strpos( $path . '/', $vendorPath . '/' ) ) { $path = substr( $path, strlen( $vendorPath ) ); $baseDir = '$vendorDir'; if ( false !== $path ) { $baseDir .= ' . '; } } else { $path = $filesystem->normalizePath( $filesystem->findShortestPath( $basePath, $path, true ) ); if ( ! $filesystem->isAbsolutePath( $path ) ) { $baseDir = '$baseDir . '; $path = '/' . $path; } } if ( strpos( $path, '.phar' ) !== false ) { $baseDir = "'phar://' . " . $baseDir; } // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_var_export return $baseDir . ( ( false !== $path ) ? var_export( $path, true ) : '' ); } /** * This function differs from the composer parseAutoloadsType in that beside returning the path. * It also return the path and the version of a package. * * Supports PSR-4, PSR-0, and classmap parsing. * * @param array $packageMap Map of all the packages. * @param string $type Type of autoloader to use. * @param PackageInterface $mainPackage Instance of the Package Object. * * @return array */ protected function parseAutoloadsType( array $packageMap, $type, PackageInterface $mainPackage ) { $autoloads = array(); foreach ( $packageMap as $item ) { list($package, $installPath) = $item; $autoload = $package->getAutoload(); $version = $package->getVersion(); // Version of the class comes from the package - should we try to parse it? // Store our own actual package version, not "dev-trunk" or whatever. if ( $package->getName() === 'automattic/jetpack-autoloader' ) { $version = self::VERSION; } if ( $package === $mainPackage ) { $autoload = array_merge_recursive( $autoload, $package->getDevAutoload() ); } if ( null !== $package->getTargetDir() && $package !== $mainPackage ) { $installPath = substr( $installPath, 0, -strlen( '/' . $package->getTargetDir() ) ); } if ( in_array( $type, array( 'psr-4', 'psr-0' ), true ) && isset( $autoload[ $type ] ) && is_array( $autoload[ $type ] ) ) { foreach ( $autoload[ $type ] as $namespace => $paths ) { $paths = is_array( $paths ) ? $paths : array( $paths ); foreach ( $paths as $path ) { $relativePath = empty( $installPath ) ? ( empty( $path ) ? '.' : $path ) : $installPath . '/' . $path; $autoloads[ $namespace ][] = array( 'path' => $relativePath, 'version' => $version, ); } } } if ( 'classmap' === $type && isset( $autoload['classmap'] ) && is_array( $autoload['classmap'] ) ) { foreach ( $autoload['classmap'] as $paths ) { $paths = is_array( $paths ) ? $paths : array( $paths ); foreach ( $paths as $path ) { $relativePath = empty( $installPath ) ? ( empty( $path ) ? '.' : $path ) : $installPath . '/' . $path; $autoloads[] = array( 'path' => $relativePath, 'version' => $version, ); } } } if ( 'files' === $type && isset( $autoload['files'] ) && is_array( $autoload['files'] ) ) { foreach ( $autoload['files'] as $paths ) { $paths = is_array( $paths ) ? $paths : array( $paths ); foreach ( $paths as $path ) { $relativePath = empty( $installPath ) ? ( empty( $path ) ? '.' : $path ) : $installPath . '/' . $path; $autoloads[ $this->getFileIdentifier( $package, $path ) ] = array( 'path' => $relativePath, 'version' => $version, ); } } } } return $autoloads; } /** * Given Composer's autoloads this will convert them to a version that we can use to generate the manifests. * * When the $scanPsrPackages argument is true, PSR-4 namespaces are converted to classmaps. When $scanPsrPackages * is false, PSR-4 namespaces are not converted to classmaps. * * PSR-0 namespaces are always converted to classmaps. * * @param array $autoloads The autoloads we want to process. * @param bool $scanPsrPackages Whether or not PSR-4 packages should be converted to a classmap. * @param string $vendorPath The path to the vendor directory. * @param string $basePath The path to the current directory. * * @return array $processedAutoloads */ private function processAutoloads( $autoloads, $scanPsrPackages, $vendorPath, $basePath ) { $processor = new AutoloadProcessor( function ( $path, $excludedClasses, $namespace ) use ( $basePath ) { $dir = $this->filesystem->normalizePath( $this->filesystem->isAbsolutePath( $path ) ? $path : $basePath . '/' . $path ); // Composer 2.4 changed the name of the class. if ( class_exists( \Composer\ClassMapGenerator\ClassMapGenerator::class ) ) { if ( ! is_dir( $dir ) && ! is_file( $dir ) ) { return array(); } $generator = new \Composer\ClassMapGenerator\ClassMapGenerator(); $generator->scanPaths( $dir, $excludedClasses, 'classmap', empty( $namespace ) ? null : $namespace ); return $generator->getClassMap()->getMap(); } return \Composer\Autoload\ClassMapGenerator::createMap( $dir, $excludedClasses, null, // Don't pass the IOInterface since the normal autoload generation will have reported already. empty( $namespace ) ? null : $namespace ); }, function ( $path ) use ( $basePath, $vendorPath ) { return $this->getPathCode( $this->filesystem, $basePath, $vendorPath, $path ); } ); return array( 'psr-4' => $processor->processPsr4Packages( $autoloads, $scanPsrPackages ), 'classmap' => $processor->processClassmap( $autoloads, $scanPsrPackages ), 'files' => $processor->processFiles( $autoloads ), ); } /** * Removes all of the legacy autoloader files so they don't cause any problems. * * @param string $outDir The directory legacy files are written to. */ private function removeLegacyFiles( $outDir ) { $files = array( 'autoload_functions.php', 'class-autoloader-handler.php', 'class-classes-handler.php', 'class-files-handler.php', 'class-plugins-handler.php', 'class-version-selector.php', ); foreach ( $files as $file ) { $this->filesystem->remove( $outDir . '/' . $file ); } } /** * Writes all of the autoloader files to disk. * * @param string $outDir The directory to write to. * @param string $suffix The unique autoloader suffix. */ private function writeAutoloaderFiles( $outDir, $suffix ) { $this->io->writeError( "<info>Generating jetpack autoloader ($outDir)</info>" ); // We will remove all autoloader files to generate this again. $this->filesystem->emptyDirectory( $outDir ); // Write the autoloader files. AutoloadFileWriter::copyAutoloaderFiles( $this->io, $outDir, $suffix ); } /** * Writes all of the manifest files to disk. * * @param string $outDir The directory to write to. * @param array $processedAutoloads The processed autoloads. */ private function writeManifests( $outDir, $processedAutoloads ) { $this->io->writeError( "<info>Generating jetpack autoloader manifests ($outDir)</info>" ); $manifestFiles = array( 'classmap' => 'jetpack_autoload_classmap.php', 'psr-4' => 'jetpack_autoload_psr4.php', 'files' => 'jetpack_autoload_filemap.php', ); foreach ( $manifestFiles as $key => $file ) { // Make sure the file doesn't exist so it isn't there if we don't write it. $this->filesystem->remove( $outDir . '/' . $file ); if ( empty( $processedAutoloads[ $key ] ) ) { continue; } $content = ManifestGenerator::buildManifest( $key, $file, $processedAutoloads[ $key ] ); if ( empty( $content ) ) { continue; } if ( file_put_contents( $outDir . '/' . $file, $content ) ) { $this->io->writeError( " <info>Generated: $file</info>" ); } else { $this->io->writeError( " <error>Error: $file</error>" ); } } } } jetpack-autoloader/src/class-container.php 0000777 00000011157 15251741406 0014730 0 ustar 00 <?php /* HEADER */ // phpcs:ignore /** * This class manages the files and dependencies of the autoloader. */ class Container { /** * Since each autoloader's class files exist within their own namespace we need a map to * convert between the local class and a shared key. Note that no version checking is * performed on these dependencies and the first autoloader to register will be the * one that is utilized. */ const SHARED_DEPENDENCY_KEYS = array( Hook_Manager::class => 'Hook_Manager', ); /** * A map of all the dependencies we've registered with the container and created. * * @var array */ protected $dependencies; /** * The constructor. */ public function __construct() { $this->dependencies = array(); $this->register_shared_dependencies(); $this->register_dependencies(); $this->initialize_globals(); } /** * Gets a dependency out of the container. * * @param string $class The class to fetch. * * @return mixed * @throws \InvalidArgumentException When a class that isn't registered with the container is fetched. */ public function get( $class ) { if ( ! isset( $this->dependencies[ $class ] ) ) { throw new \InvalidArgumentException( "Class '$class' is not registered with the container." ); } return $this->dependencies[ $class ]; } /** * Registers all of the dependencies that are shared between all instances of the autoloader. */ private function register_shared_dependencies() { global $jetpack_autoloader_container_shared; if ( ! isset( $jetpack_autoloader_container_shared ) ) { $jetpack_autoloader_container_shared = array(); } $key = self::SHARED_DEPENDENCY_KEYS[ Hook_Manager::class ]; if ( ! isset( $jetpack_autoloader_container_shared[ $key ] ) ) { require_once __DIR__ . '/class-hook-manager.php'; $jetpack_autoloader_container_shared[ $key ] = new Hook_Manager(); } $this->dependencies[ Hook_Manager::class ] = &$jetpack_autoloader_container_shared[ $key ]; } /** * Registers all of the dependencies with the container. */ private function register_dependencies() { require_once __DIR__ . '/class-path-processor.php'; $this->dependencies[ Path_Processor::class ] = new Path_Processor(); require_once __DIR__ . '/class-plugin-locator.php'; $this->dependencies[ Plugin_Locator::class ] = new Plugin_Locator( $this->get( Path_Processor::class ) ); require_once __DIR__ . '/class-version-selector.php'; $this->dependencies[ Version_Selector::class ] = new Version_Selector(); require_once __DIR__ . '/class-autoloader-locator.php'; $this->dependencies[ Autoloader_Locator::class ] = new Autoloader_Locator( $this->get( Version_Selector::class ) ); require_once __DIR__ . '/class-php-autoloader.php'; $this->dependencies[ PHP_Autoloader::class ] = new PHP_Autoloader(); require_once __DIR__ . '/class-manifest-reader.php'; $this->dependencies[ Manifest_Reader::class ] = new Manifest_Reader( $this->get( Version_Selector::class ) ); require_once __DIR__ . '/class-plugins-handler.php'; $this->dependencies[ Plugins_Handler::class ] = new Plugins_Handler( $this->get( Plugin_Locator::class ), $this->get( Path_Processor::class ) ); require_once __DIR__ . '/class-autoloader-handler.php'; $this->dependencies[ Autoloader_Handler::class ] = new Autoloader_Handler( $this->get( PHP_Autoloader::class ), $this->get( Hook_Manager::class ), $this->get( Manifest_Reader::class ), $this->get( Version_Selector::class ) ); require_once __DIR__ . '/class-latest-autoloader-guard.php'; $this->dependencies[ Latest_Autoloader_Guard::class ] = new Latest_Autoloader_Guard( $this->get( Plugins_Handler::class ), $this->get( Autoloader_Handler::class ), $this->get( Autoloader_Locator::class ) ); // Register any classes that we will use elsewhere. require_once __DIR__ . '/class-version-loader.php'; require_once __DIR__ . '/class-shutdown-handler.php'; } /** * Initializes any of the globals needed by the autoloader. */ private function initialize_globals() { /* * This global was retired in version 2.9. The value is set to 'false' to maintain * compatibility with older versions of the autoloader. */ global $jetpack_autoloader_including_latest; $jetpack_autoloader_including_latest = false; // Not all plugins can be found using the locator. In cases where a plugin loads the autoloader // but was not discoverable, we will record them in this array to track them as "active". global $jetpack_autoloader_activating_plugins_paths; if ( ! isset( $jetpack_autoloader_activating_plugins_paths ) ) { $jetpack_autoloader_activating_plugins_paths = array(); } } } jetpack-autoloader/src/class-autoloader.php 0000777 00000010063 15251741406 0015100 0 ustar 00 <?php /* HEADER */ // phpcs:ignore /** * This class handles management of the actual PHP autoloader. */ class Autoloader { /** * Checks to see whether or not the autoloader should be initialized and then initializes it if so. * * @param Container|null $container The container we want to use for autoloader initialization. If none is given * then a container will be created automatically. */ public static function init( $container = null ) { // The container holds and manages the lifecycle of our dependencies // to make them easier to work with and increase flexibility. if ( ! isset( $container ) ) { require_once __DIR__ . '/class-container.php'; $container = new Container(); } // phpcs:disable Generic.Commenting.DocComment.MissingShort /** @var Autoloader_Handler $autoloader_handler */ $autoloader_handler = $container->get( Autoloader_Handler::class ); // If the autoloader is already initializing it means that it has included us as the latest. $was_included_by_autoloader = $autoloader_handler->is_initializing(); /** @var Plugin_Locator $plugin_locator */ $plugin_locator = $container->get( Plugin_Locator::class ); /** @var Plugins_Handler $plugins_handler */ $plugins_handler = $container->get( Plugins_Handler::class ); // The current plugin is the one that we are attempting to initialize here. $current_plugin = $plugin_locator->find_current_plugin(); // The active plugins are those that we were able to discover on the site. This list will not // include mu-plugins, those activated by code, or those who are hidden by filtering. We also // want to take care to not consider the current plugin unknown if it was included by an // autoloader. This avoids the case where a plugin will be marked "active" while deactivated // due to it having the latest autoloader. $active_plugins = $plugins_handler->get_active_plugins( true, ! $was_included_by_autoloader ); // The cached plugins are all of those that were active or discovered by the autoloader during a previous request. // Note that it's possible this list will include plugins that have since been deactivated, but after a request // the cache should be updated and the deactivated plugins will be removed. $cached_plugins = $plugins_handler->get_cached_plugins(); // We combine the active list and cached list to preemptively load classes for plugins that are // presently unknown but will be loaded during the request. While this may result in us considering packages in // deactivated plugins there shouldn't be any problems as a result and the eventual consistency is sufficient. $all_plugins = array_merge( $active_plugins, $cached_plugins ); // In particular we also include the current plugin to address the case where it is the latest autoloader // but also unknown (and not cached). We don't want it in the active list because we don't know that it // is active but we need it in the all plugins list so that it is considered by the autoloader. $all_plugins[] = $current_plugin; // We require uniqueness in the array to avoid processing the same plugin more than once. $all_plugins = array_values( array_unique( $all_plugins ) ); /** @var Latest_Autoloader_Guard $guard */ $guard = $container->get( Latest_Autoloader_Guard::class ); if ( $guard->should_stop_init( $current_plugin, $all_plugins, $was_included_by_autoloader ) ) { return; } // Initialize the autoloader using the handler now that we're ready. $autoloader_handler->activate_autoloader( $all_plugins ); /** @var Hook_Manager $hook_manager */ $hook_manager = $container->get( Hook_Manager::class ); // Register a shutdown handler to clean up the autoloader. $hook_manager->add_action( 'shutdown', new Shutdown_Handler( $plugins_handler, $cached_plugins, $was_included_by_autoloader ) ); // Register a plugins_loaded handler to check for conflicting autoloaders. $hook_manager->add_action( 'plugins_loaded', array( $guard, 'check_for_conflicting_autoloaders' ), 1 ); // phpcs:enable Generic.Commenting.DocComment.MissingShort } } jetpack-autoloader/src/class-php-autoloader.php 0000777 00000006633 15251741406 0015675 0 ustar 00 <?php /* HEADER */ // phpcs:ignore /** * This class handles management of the actual PHP autoloader. */ class PHP_Autoloader { /** * Registers the autoloader with PHP so that it can begin autoloading classes. * * @param Version_Loader $version_loader The class loader to use in the autoloader. */ public function register_autoloader( $version_loader ) { // Make sure no other autoloaders are registered. $this->unregister_autoloader(); // Set the global so that it can be used to load classes. global $jetpack_autoloader_loader; $jetpack_autoloader_loader = $version_loader; // Ensure that the autoloader is first to avoid contention with others. spl_autoload_register( array( self::class, 'load_class' ), true, true ); } /** * Unregisters the active autoloader so that it will no longer autoload classes. */ public function unregister_autoloader() { // Remove any v2 autoloader that we've already registered. $autoload_chain = spl_autoload_functions(); if ( ! $autoload_chain ) { return; } foreach ( $autoload_chain as $autoloader ) { // We can identify a v2 autoloader using the namespace. $namespace_check = null; // Functions are recorded as strings. if ( is_string( $autoloader ) ) { $namespace_check = $autoloader; } elseif ( is_array( $autoloader ) && is_string( $autoloader[0] ) ) { // Static method calls have the class as the first array element. $namespace_check = $autoloader[0]; } else { // Since the autoloader has only ever been a function or a static method we don't currently need to check anything else. continue; } // Check for the namespace without the generated suffix. if ( 'Automattic\\Jetpack\\Autoloader\\jp' === substr( $namespace_check, 0, 32 ) ) { spl_autoload_unregister( $autoloader ); } } // Clear the global now that the autoloader has been unregistered. global $jetpack_autoloader_loader; $jetpack_autoloader_loader = null; } /** * Loads a class file if one could be found. * * Note: This function is static so that the autoloader can be easily unregistered. If * it was a class method we would have to unwrap the object to check the namespace. * * @param string $class_name The name of the class to autoload. * * @return bool Indicates whether or not a class file was loaded. */ public static function load_class( $class_name ) { global $jetpack_autoloader_loader; if ( ! isset( $jetpack_autoloader_loader ) ) { return false; } $file = $jetpack_autoloader_loader->find_class_file( $class_name ); if ( ! isset( $file ) ) { return false; } // A common source of strange and confusing problems is when a vendor // file is autoloaded before all plugins have had a chance to register // with the autoloader. Detect that, if a development constant is set. if ( defined( 'JETPACK_AUTOLOAD_DEBUG_EARLY_LOADS' ) && JETPACK_AUTOLOAD_DEBUG_EARLY_LOADS && ( strpos( $file, '/vendor/' ) !== false || strpos( $file, '/jetpack_vendor/' ) !== false ) && is_callable( 'did_action' ) && ! did_action( 'plugins_loaded' ) ) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_wp_debug_backtrace_summary -- This is a debug log message. $msg = "Jetpack Autoloader: Autoloading `$class_name` before the plugins_loaded hook may cause strange and confusing problems. " . wp_debug_backtrace_summary( '', 1 ); wp_trigger_error( '', $msg ); } require $file; return true; } } jetpack-autoloader/src/CustomAutoloaderPlugin.php 0000777 00000013132 15251741406 0016307 0 ustar 00 <?php /** * Custom Autoloader Composer Plugin, hooks into composer events to generate the custom autoloader. * * @package automattic/jetpack-autoloader */ namespace Automattic\Jetpack\Autoloader; use Composer\Composer; use Composer\EventDispatcher\EventSubscriberInterface; use Composer\IO\IOInterface; use Composer\Plugin\PluginInterface; use Composer\Script\Event; use Composer\Script\ScriptEvents; /** * Class CustomAutoloaderPlugin. * * @package automattic/jetpack-autoloader */ class CustomAutoloaderPlugin implements PluginInterface, EventSubscriberInterface { /** * IO object. * * @var IOInterface IO object. */ private $io; /** * Composer object. * * @var Composer Composer object. */ private $composer; /** * Do nothing. * * @param Composer $composer Composer object. * @param IOInterface $io IO object. */ public function activate( Composer $composer, IOInterface $io ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable $this->composer = $composer; $this->io = $io; } /** * Do nothing. * phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable * * @param Composer $composer Composer object. * @param IOInterface $io IO object. */ public function deactivate( Composer $composer, IOInterface $io ) { /* * Intentionally left empty. This is a PluginInterface method. * phpcs:enable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable */ } /** * Do nothing. * phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable * * @param Composer $composer Composer object. * @param IOInterface $io IO object. */ public function uninstall( Composer $composer, IOInterface $io ) { /* * Intentionally left empty. This is a PluginInterface method. * phpcs:enable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable */ } /** * Tell composer to listen for events and do something with them. * * @return array List of subscribed events. */ public static function getSubscribedEvents() { return array( ScriptEvents::POST_AUTOLOAD_DUMP => 'postAutoloadDump', ); } /** * Generate the custom autolaoder. * * @param Event $event Script event object. */ public function postAutoloadDump( Event $event ) { // When the autoloader is not required by the root package we don't want to execute it. // This prevents unwanted transitive execution that generates unused autoloaders or // at worst throws fatal executions. if ( ! $this->isRequiredByRoot() ) { return; } $config = $this->composer->getConfig(); if ( 'vendor' !== $config->raw()['config']['vendor-dir'] ) { $this->io->writeError( "\n<error>An error occurred while generating the autoloader files:", true ); $this->io->writeError( 'The project\'s composer.json or composer environment set a non-default vendor directory.', true ); $this->io->writeError( 'The default composer vendor directory must be used.</error>', true ); exit(); } $installationManager = $this->composer->getInstallationManager(); $repoManager = $this->composer->getRepositoryManager(); $localRepo = $repoManager->getLocalRepository(); $package = $this->composer->getPackage(); $optimize = $event->getFlags()['optimize']; $suffix = $this->determineSuffix(); $generator = new AutoloadGenerator( $this->io ); $generator->dump( $this->composer, $config, $localRepo, $package, $installationManager, 'composer', $optimize, $suffix ); } /** * Determine the suffix for the autoloader class. * * Reuses an existing suffix from vendor/autoload_packages.php or vendor/autoload.php if possible. * * @return string Suffix. */ private function determineSuffix() { $config = $this->composer->getConfig(); $vendorPath = $config->get( 'vendor-dir' ); // Command line. $suffix = $config->get( 'autoloader-suffix' ); if ( $suffix ) { return $suffix; } // Reuse our own suffix, if any. if ( is_readable( $vendorPath . '/autoload_packages.php' ) ) { $content = file_get_contents( $vendorPath . '/autoload_packages.php' ); if ( preg_match( '/^namespace Automattic\\\\Jetpack\\\\Autoloader\\\\jp([^;\s]+?)(?:\\\\al[^;\s]+)?;/m', $content, $match ) ) { return $match[1]; } } // Reuse Composer's suffix, if any. if ( is_readable( $vendorPath . '/autoload.php' ) ) { $content = file_get_contents( $vendorPath . '/autoload.php' ); if ( preg_match( '{ComposerAutoloaderInit([^:\s]+)::}', $content, $match ) ) { return $match[1]; } } // Generate a random suffix. return md5( uniqid( '', true ) ); } /** * Checks to see whether or not the root package is the one that required the autoloader. * * @return bool */ private function isRequiredByRoot() { $package = $this->composer->getPackage(); $requires = $package->getRequires(); if ( ! is_array( $requires ) ) { // @phan-suppress-current-line PhanRedundantCondition -- Earlier Composer versions may not have guaranteed this. $requires = array(); } $devRequires = $package->getDevRequires(); if ( ! is_array( $devRequires ) ) { // @phan-suppress-current-line PhanRedundantCondition -- Earlier Composer versions may not have guaranteed this. $devRequires = array(); } $requires = array_merge( $requires, $devRequires ); if ( empty( $requires ) ) { $this->io->writeError( "\n<error>The package is not required and this should never happen?</error>", true ); exit(); } foreach ( $requires as $require ) { if ( 'automattic/jetpack-autoloader' === $require->getTarget() ) { return true; } } return false; } } jetpack-autoloader/src/class-shutdown-handler.php 0000777 00000005203 15251741406 0016227 0 ustar 00 <?php /* HEADER */ // phpcs:ignore /** * This class handles the shutdown of the autoloader. */ class Shutdown_Handler { /** * The Plugins_Handler instance. * * @var Plugins_Handler */ private $plugins_handler; /** * The plugins cached by this autoloader. * * @var string[] */ private $cached_plugins; /** * Indicates whether or not this autoloader was included by another. * * @var bool */ private $was_included_by_autoloader; /** * Constructor. * * @param Plugins_Handler $plugins_handler The Plugins_Handler instance to use. * @param string[] $cached_plugins The plugins cached by the autoloaer. * @param bool $was_included_by_autoloader Indicates whether or not the autoloader was included by another. */ public function __construct( $plugins_handler, $cached_plugins, $was_included_by_autoloader ) { $this->plugins_handler = $plugins_handler; $this->cached_plugins = $cached_plugins; $this->was_included_by_autoloader = $was_included_by_autoloader; } /** * Handles the shutdown of the autoloader. */ public function __invoke() { // Don't save a broken cache if an error happens during some plugin's initialization. if ( ! did_action( 'plugins_loaded' ) ) { // Ensure that the cache is emptied to prevent consecutive failures if the cache is to blame. if ( ! empty( $this->cached_plugins ) ) { $this->plugins_handler->cache_plugins( array() ); } return; } // Load the active plugins fresh since the list we pulled earlier might not contain // plugins that were activated but did not reset the autoloader. This happens // when a plugin is in the cache but not "active" when the autoloader loads. // We also want to make sure that plugins which are deactivating are not // considered "active" so that they will be removed from the cache now. try { $active_plugins = $this->plugins_handler->get_active_plugins( false, ! $this->was_included_by_autoloader ); } catch ( \Exception $ex ) { // When the package is deleted before shutdown it will throw an exception. // In the event this happens we should erase the cache. if ( ! empty( $this->cached_plugins ) ) { $this->plugins_handler->cache_plugins( array() ); } return; } // The paths should be sorted for easy comparisons with those loaded from the cache. // Note we don't need to sort the cached entries because they're already sorted. sort( $active_plugins ); // We don't want to waste time saving a cache that hasn't changed. if ( $this->cached_plugins === $active_plugins ) { return; } $this->plugins_handler->cache_plugins( $active_plugins ); } } jetpack-connection/composer.json 0000777 00000003774 15251741406 0013073 0 ustar 00 { "name": "automattic/jetpack-connection", "description": "Everything needed to connect to the Jetpack infrastructure", "type": "jetpack-library", "license": "GPL-2.0-or-later", "require": { "php": ">=7.2", "automattic/jetpack-a8c-mc-stats": "^3.0.5", "automattic/jetpack-admin-ui": "^0.5.11", "automattic/jetpack-assets": "^4.3.11", "automattic/jetpack-constants": "^3.0.8", "automattic/jetpack-roles": "^3.0.8", "automattic/jetpack-status": "^6.1.0", "automattic/jetpack-redirect": "^3.0.9" }, "require-dev": { "automattic/jetpack-test-environment": "@dev", "yoast/phpunit-polyfills": "^4.0.0", "brain/monkey": "^2.6.2", "automattic/jetpack-changelogger": "^6.0.7", "automattic/phpunit-select-config": "^1.0.3" }, "suggest": { "automattic/jetpack-autoloader": "Allow for better interoperability with other plugins that use this package." }, "autoload": { "files": [ "actions.php" ], "classmap": [ "legacy", "src/", "src/webhooks", "src/identity-crisis" ] }, "scripts": { "build-production": [ "pnpm run build-production" ], "build-development": [ "pnpm run build" ], "phpunit": [ "phpunit-select-config phpunit.#.xml.dist --colors=always" ], "test-coverage": [ "php -dpcov.directory=. ./vendor/bin/phpunit-select-config phpunit.#.xml.dist --coverage-php \"$COVERAGE_DIR/php.cov\"" ], "test-php": [ "@composer phpunit" ] }, "minimum-stability": "dev", "prefer-stable": true, "extra": { "autotagger": true, "mirror-repo": "Automattic/jetpack-connection", "textdomain": "jetpack-connection", "version-constants": { "::PACKAGE_VERSION": "src/class-package-version.php" }, "changelogger": { "link-template": "https://github.com/Automattic/jetpack-connection/compare/v${old}...v${new}" }, "branch-alias": { "dev-trunk": "6.19.x-dev" }, "dependencies": { "test-only": [ "packages/licensing", "packages/sync" ] } }, "config": { "allow-plugins": { "roots/wordpress-core-installer": true } } } jetpack-connection/LICENSE.txt 0000777 00000043760 15251741406 0012173 0 ustar 00 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. jetpack-connection/actions.php 0000777 00000001247 15251741406 0012513 0 ustar 00 <?php /** * Action Hooks for Jetpack connection assets. * * @package automattic/jetpack-connection */ // If WordPress's plugin API is available already, use it. If not, // drop data into `$wp_filter` for `WP_Hook::build_preinitialized_hooks()`. if ( function_exists( 'add_action' ) ) { add_action( 'plugins_loaded', array( Automattic\Jetpack\Connection\Connection_Assets::class, 'configure' ), 1 ); } else { global $wp_filter; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited $wp_filter['plugins_loaded'][1][] = array( 'accepted_args' => 0, 'function' => array( Automattic\Jetpack\Connection\Connection_Assets::class, 'configure' ), ); } jetpack-connection/legacy/class-jetpack-ixr-client.php 0000777 00000011663 15251741406 0017122 0 ustar 00 <?php /** * IXR_Client * * @package automattic/jetpack-connection * * @since 1.7.0 * @since-jetpack 1.5 * @since-jetpack 7.7 Moved to the jetpack-connection package. */ use Automattic\Jetpack\Connection\Client; use Automattic\Jetpack\Connection\Manager; /** * Disable direct access. */ if ( ! defined( 'ABSPATH' ) ) { exit( 0 ); } if ( ! class_exists( IXR_Client::class ) ) { require_once ABSPATH . WPINC . '/class-IXR.php'; } /** * A Jetpack implementation of the WordPress core IXR client. */ class Jetpack_IXR_Client extends IXR_Client { /** * Jetpack args, used for the remote requests. * * @var array */ public $jetpack_args = null; /** * Remote Response Headers. * * @var array */ public $response_headers = null; /** * Holds the raw remote response from the latest call to query(). * * @var null|array|WP_Error */ public $last_response = null; /** * Constructor. * Initialize a new Jetpack IXR client instance. * * @param array $args Jetpack args, used for the remote requests. * @param string|bool $path Path to perform the reuqest to. * @param int $port Port number. * @param int $timeout The connection timeout, in seconds. */ public function __construct( $args = array(), $path = false, $port = 80, $timeout = 15 ) { $connection = new Manager(); $defaults = array( 'url' => $connection->xmlrpc_api_url(), 'user_id' => 0, 'headers' => array(), ); $args = wp_parse_args( $args, $defaults ); $args['headers'] = array_merge( array( 'Content-Type' => 'text/xml' ), (array) $args['headers'] ); $this->jetpack_args = $args; $this->IXR_Client( $args['url'], $path, $port, $timeout ); } /** * Perform the IXR request. * * @param mixed ...$args IXR method and args. * * @return bool True if request succeeded, false otherwise. */ public function query( ...$args ) { $method = array_shift( $args ); $request = new IXR_Request( $method, $args ); $xml = trim( $request->getXml() ); $response = Client::remote_request( $this->jetpack_args, $xml ); // Store response headers. $this->response_headers = wp_remote_retrieve_headers( $response ); $this->last_response = $response; if ( is_array( $this->last_response ) && isset( $this->last_response['http_response'] ) ) { // If the expected array response is received, format the data as plain arrays. $this->last_response = $this->last_response['http_response']->to_array(); $this->last_response['headers'] = $this->last_response['headers']->getAll(); } if ( is_wp_error( $response ) ) { $this->error = new IXR_Error( -10520, sprintf( 'Jetpack: [%s] %s', $response->get_error_code(), $response->get_error_message() ) ); return false; } if ( ! $response ) { $this->error = new IXR_Error( -10520, 'Jetpack: Unknown Error' ); return false; } if ( 200 !== wp_remote_retrieve_response_code( $response ) ) { $this->error = new IXR_Error( -32300, 'transport error - HTTP status code was not 200' ); return false; } $content = wp_remote_retrieve_body( $response ); // Now parse what we've got back. $this->message = new IXR_Message( $content ); if ( ! $this->message->parse() ) { // XML error. $this->error = new IXR_Error( -32700, 'parse error. not well formed' ); return false; } // Is the message a fault? if ( 'fault' === $this->message->messageType ) { $this->error = new IXR_Error( $this->message->faultCode, $this->message->faultString ); return false; } // Message must be OK. return true; } /** * Retrieve the Jetpack error from the result of the last request. * * @param int $fault_code Fault code. * @param string $fault_string Fault string. * @return WP_Error Error object. */ public function get_jetpack_error( $fault_code = null, $fault_string = null ) { if ( $fault_code === null ) { $fault_code = $this->error->code; } if ( $fault_string === null ) { $fault_string = $this->error->message; } if ( preg_match( '#jetpack:\s+\[(\w+)\]\s*(.*)?$#i', $fault_string, $match ) ) { $code = $match[1]; $message = $match[2]; $status = $fault_code; return new WP_Error( $code, $message, $status ); } return new WP_Error( "IXR_{$fault_code}", $fault_string ); } /** * Retrieve a response header if set. * * @param string $name header name. * @return string|bool Header value if set, false if not set. */ public function get_response_header( $name ) { if ( isset( $this->response_headers[ $name ] ) ) { return $this->response_headers[ $name ]; } // case-insensitive header names: http://www.ietf.org/rfc/rfc2616.txt. if ( isset( $this->response_headers[ strtolower( $name ) ] ) ) { return $this->response_headers[ strtolower( $name ) ]; } return false; } /** * Retrieve the raw response for the last query() call. * * @return null|array|WP_Error */ public function get_last_response() { return $this->last_response; } } jetpack-connection/legacy/class-jetpack-xmlrpc-server.php 0000777 00000061502 15251741406 0017652 0 ustar 00 <?php /** * Jetpack XMLRPC Server. * * @package automattic/jetpack-connection */ use Automattic\Jetpack\Connection\Client; use Automattic\Jetpack\Connection\Manager as Connection_Manager; use Automattic\Jetpack\Connection\Secrets; use Automattic\Jetpack\Connection\Tokens; use Automattic\Jetpack\Connection\Urls; use Automattic\Jetpack\Constants; use Automattic\Jetpack\Roles; /** * Just a sack of functions. Not actually an IXR_Server */ class Jetpack_XMLRPC_Server { /** * The current error object * * @var \WP_Error */ public $error = null; /** * The current user * * @var \WP_User */ public $user = null; /** * The connection manager object. * * @var Automattic\Jetpack\Connection\Manager */ private $connection; /** * Creates a new XMLRPC server object. */ public function __construct() { $this->connection = new Connection_Manager(); } /** * Whitelist of the XML-RPC methods available to the Jetpack Server. If the * user is not authenticated (->login()) then the methods are never added, * so they will get a "does not exist" error. * * @param array $core_methods Core XMLRPC methods. */ public function xmlrpc_methods( $core_methods ) { $jetpack_methods = array( 'jetpack.verifyAction' => array( $this, 'verify_action' ), 'jetpack.idcUrlValidation' => array( $this, 'validate_urls_for_idc_mitigation' ), 'jetpack.unlinkUser' => array( $this, 'unlink_user' ), 'jetpack.testConnection' => array( $this, 'test_connection' ), ); $jetpack_methods = array_merge( $jetpack_methods, $this->provision_xmlrpc_methods() ); $this->user = $this->login(); if ( $this->user ) { $jetpack_methods = array_merge( $jetpack_methods, array( 'jetpack.testAPIUserCode' => array( $this, 'test_api_user_code' ), ) ); if ( isset( $core_methods['metaWeblog.editPost'] ) ) { $jetpack_methods['metaWeblog.newMediaObject'] = $core_methods['metaWeblog.newMediaObject']; $jetpack_methods['jetpack.updateAttachmentParent'] = array( $this, 'update_attachment_parent' ); } /** * Filters the XML-RPC methods available to Jetpack for authenticated users. * * @since 1.7.0 * @since-jetpack 1.1.0 * * @param array $jetpack_methods XML-RPC methods available to the Jetpack Server. * @param array $core_methods Available core XML-RPC methods. * @param \WP_User $user Information about the user authenticated in the request. */ $jetpack_methods = apply_filters( 'jetpack_xmlrpc_methods', $jetpack_methods, $core_methods, $this->user ); } /** * Filters the XML-RPC methods available to Jetpack for requests signed both with a blog token or a user token. * * @since 1.7.0 * @since 1.9.5 Introduced the $user parameter. * @since-jetpack 3.0.0 * * @param array $jetpack_methods XML-RPC methods available to the Jetpack Server. * @param array $core_methods Available core XML-RPC methods. * @param \WP_User|bool $user Information about the user authenticated in the request. False if authenticated with blog token. */ return apply_filters( 'jetpack_xmlrpc_unauthenticated_methods', $jetpack_methods, $core_methods, $this->user ); } /** * Whitelist of the bootstrap XML-RPC methods */ public function bootstrap_xmlrpc_methods() { return array( 'jetpack.remoteAuthorize' => array( $this, 'remote_authorize' ), 'jetpack.remoteRegister' => array( $this, 'remote_register' ), ); } /** * Additional method needed for authorization calls. */ public function authorize_xmlrpc_methods() { return array( 'jetpack.remoteAuthorize' => array( $this, 'remote_authorize' ), 'jetpack.remoteRegister' => array( $this, 'remote_already_registered' ), ); } /** * Remote provisioning methods. */ public function provision_xmlrpc_methods() { return array( 'jetpack.remoteRegister' => array( $this, 'remote_register' ), 'jetpack.remoteProvision' => array( $this, 'remote_provision' ), 'jetpack.remoteConnect' => array( $this, 'remote_connect' ), 'jetpack.getUser' => array( $this, 'get_user' ), ); } /** * Used to verify whether a local user exists and what role they have. * * @param int|string|array $request One of: * int|string The local User's ID, username, or email address. * array A request array containing: * 0: int|string The local User's ID, username, or email address. * * @return array|\IXR_Error Information about the user, or error if no such user found: * roles: string[] The user's rols. * login: string The user's username. * email_hash string[] The MD5 hash of the user's normalized email address. * caps string[] The user's capabilities. * allcaps string[] The user's granular capabilities, merged from role capabilities. * token_key string The Token Key of the user's Jetpack token. Empty string if none. */ public function get_user( $request ) { $user_id = is_array( $request ) ? $request[0] : $request; if ( ! $user_id ) { return $this->error( new \WP_Error( 'invalid_user', __( 'Invalid user identifier.', 'jetpack-connection' ), 400 ), 'get_user' ); } $user = $this->get_user_by_anything( $user_id ); if ( ! $user ) { return $this->error( new \WP_Error( 'user_unknown', __( 'User not found.', 'jetpack-connection' ), 404 ), 'get_user' ); } $user_token = ( new Tokens() )->get_access_token( $user->ID ); if ( $user_token ) { list( $user_token_key ) = explode( '.', $user_token->secret ); if ( $user_token_key === $user_token->secret ) { $user_token_key = ''; } } else { $user_token_key = ''; } return array( 'id' => $user->ID, 'login' => $user->user_login, 'email_hash' => md5( strtolower( trim( $user->user_email ) ) ), 'roles' => $user->roles, 'caps' => $user->caps, 'allcaps' => $user->allcaps, 'token_key' => $user_token_key, ); } /** * Remote authorization XMLRPC method handler. * * @param array $request the request. */ public function remote_authorize( $request ) { $user = get_user_by( 'id', $request['state'] ); /** * Happens on various request handling events in the Jetpack XMLRPC server. * The action combines several types of events: * - remote_authorize * - remote_provision * - get_user. * * @since 1.7.0 * @since-jetpack 8.0.0 * * @param String $action the action name, i.e., 'remote_authorize'. * @param String $stage the execution stage, can be 'begin', 'success', 'error', etc. * @param array $parameters extra parameters from the event. * @param WP_User $user the acting user. */ do_action( 'jetpack_xmlrpc_server_event', 'remote_authorize', 'begin', array(), $user ); foreach ( array( 'secret', 'state', 'redirect_uri', 'code' ) as $required ) { if ( ! isset( $request[ $required ] ) || empty( $request[ $required ] ) ) { return $this->error( new \WP_Error( 'missing_parameter', 'One or more parameters is missing from the request.', 400 ), 'remote_authorize' ); } } if ( ! $user ) { return $this->error( new \WP_Error( 'user_unknown', 'User not found.', 404 ), 'remote_authorize' ); } if ( $this->connection->has_connected_owner() && $this->connection->is_user_connected( $request['state'] ) ) { return $this->error( new \WP_Error( 'already_connected', 'User already connected.', 400 ), 'remote_authorize' ); } $verified = $this->verify_action( array( 'authorize', $request['secret'], $request['state'] ) ); if ( is_a( $verified, 'IXR_Error' ) ) { return $this->error( $verified, 'remote_authorize' ); } wp_set_current_user( $request['state'] ); $result = $this->connection->authorize( $request ); if ( is_wp_error( $result ) ) { return $this->error( $result, 'remote_authorize' ); } // This action is documented in class.jetpack-xmlrpc-server.php. do_action( 'jetpack_xmlrpc_server_event', 'remote_authorize', 'success' ); return array( 'result' => $result, ); } /** * This XML-RPC method is called from the /jpphp/provision endpoint on WPCOM in order to * register this site so that a plan can be provisioned. * * @param array|ArrayAccess $request An array containing at minimum nonce and local_user keys. * * @return \WP_Error|array */ public function remote_register( $request ) { // This action is documented in class.jetpack-xmlrpc-server.php. do_action( 'jetpack_xmlrpc_server_event', 'remote_register', 'begin', array() ); $user = $this->fetch_and_verify_local_user( $request ); if ( ! $user ) { return $this->error( new WP_Error( 'input_error', __( 'Valid user is required', 'jetpack-connection' ), 400 ), 'remote_register' ); } if ( is_wp_error( $user ) || is_a( $user, 'IXR_Error' ) ) { return $this->error( $user, 'remote_register' ); } if ( empty( $request['nonce'] ) ) { return $this->error( new \WP_Error( 'nonce_missing', __( 'The required "nonce" parameter is missing.', 'jetpack-connection' ), 400 ), 'remote_register' ); } $nonce = sanitize_text_field( $request['nonce'] ); unset( $request['nonce'] ); $api_url = $this->connection->api_url( 'partner_provision_nonce_check' ); // @phan-suppress-next-line PhanAccessMethodInternal -- Phan is correct, but the usage is intentional. $response = Client::_wp_remote_request( esc_url_raw( add_query_arg( 'nonce', $nonce, $api_url ) ), array( 'method' => 'GET' ), true ); if ( 200 !== wp_remote_retrieve_response_code( $response ) || 'OK' !== trim( wp_remote_retrieve_body( $response ) ) ) { return $this->error( new \WP_Error( 'invalid_nonce', __( 'There was an issue validating this request.', 'jetpack-connection' ), 400 ), 'remote_register' ); } if ( ! Jetpack_Options::get_option( 'id' ) || ! ( new Tokens() )->get_access_token() || ! empty( $request['force'] ) ) { wp_set_current_user( $user->ID ); // This code mostly copied from Jetpack::admin_page_load. if ( isset( $request['from'] ) ) { $this->connection->add_register_request_param( 'from', (string) $request['from'] ); } $registered = $this->connection->try_registration(); if ( is_wp_error( $registered ) ) { return $this->error( $registered, 'remote_register' ); } elseif ( ! $registered ) { return $this->error( new \WP_Error( 'registration_error', __( 'There was an unspecified error registering the site', 'jetpack-connection' ), 400 ), 'remote_register' ); } } // This action is documented in class.jetpack-xmlrpc-server.php. do_action( 'jetpack_xmlrpc_server_event', 'remote_register', 'success' ); return array( 'client_id' => Jetpack_Options::get_option( 'id' ), ); } /** * This is a substitute for remote_register() when the blog is already registered which returns an error code * signifying that state. * This is an unauthorized call and we should not be responding with any data other than the error code. * * @return \IXR_Error */ public function remote_already_registered() { return $this->error( new \WP_Error( 'already_registered', __( 'Blog is already registered', 'jetpack-connection' ), 400 ), 'remote_register' ); } /** * This XML-RPC method is called from the /jpphp/provision endpoint on WPCOM in order to * register this site so that a plan can be provisioned. * * @param array|ArrayAccess $request An array containing at minimum a nonce key and a local_username key. * * @return \WP_Error|array */ public function remote_provision( $request ) { $user = $this->fetch_and_verify_local_user( $request ); if ( ! $user ) { return $this->error( new WP_Error( 'input_error', __( 'Valid user is required', 'jetpack-connection' ), 400 ), 'remote_provision' ); } if ( is_wp_error( $user ) || is_a( $user, 'IXR_Error' ) ) { return $this->error( $user, 'remote_provision' ); } $site_icon = get_site_icon_url(); /** * Filters the Redirect URI returned by the remote_register XMLRPC method * * @param string $redirect_uri The Redirect URI * * @since 1.9.7 */ $redirect_uri = apply_filters( 'jetpack_xmlrpc_remote_register_redirect_uri', admin_url() ); // Generate secrets. $roles = new Roles(); $role = $roles->translate_user_to_role( $user ); $secrets = ( new Secrets() )->generate( 'authorize', $user->ID ); $response = array( 'jp_version' => Constants::get_constant( 'JETPACK__VERSION' ), 'redirect_uri' => $redirect_uri, 'user_id' => $user->ID, 'user_email' => $user->user_email, 'user_login' => $user->user_login, 'scope' => $this->connection->sign_role( $role, $user->ID ), 'secret' => $secrets['secret_1'], 'is_active' => $this->connection->has_connected_owner(), ); if ( $site_icon ) { $response['site_icon'] = $site_icon; } /** * Filters the response of the remote_provision XMLRPC method * * @param array $response The response. * @param array $request An array containing at minimum a nonce key and a local_username key. * @param \WP_User $user The local authenticated user. * * @since 1.9.7 */ $response = apply_filters( 'jetpack_remote_xmlrpc_provision_response', $response, $request, $user ); return $response; } /** * Given an array containing a local user identifier and a nonce, will attempt to fetch and set * an access token for the given user. * * @param array|ArrayAccess $request An array containing local_user and nonce keys at minimum. * @param \IXR_Client $ixr_client The client object, optional. * @return mixed */ public function remote_connect( $request, $ixr_client = false ) { if ( $this->connection->has_connected_owner() ) { return $this->error( new WP_Error( 'already_connected', __( 'Jetpack is already connected.', 'jetpack-connection' ), 400 ), 'remote_connect' ); } $user = $this->fetch_and_verify_local_user( $request ); if ( ! $user || is_wp_error( $user ) || is_a( $user, 'IXR_Error' ) ) { return $this->error( new WP_Error( 'input_error', __( 'Valid user is required.', 'jetpack-connection' ), 400 ), 'remote_connect' ); } if ( empty( $request['nonce'] ) ) { return $this->error( new WP_Error( 'input_error', __( 'A non-empty nonce must be supplied.', 'jetpack-connection' ), 400 ), 'remote_connect' ); } if ( ! $ixr_client ) { $ixr_client = new Jetpack_IXR_Client(); } // TODO: move this query into the Tokens class? $ixr_client->query( 'jetpack.getUserAccessToken', array( 'nonce' => sanitize_text_field( $request['nonce'] ), 'external_user_id' => $user->ID, ) ); $token = $ixr_client->isError() ? false : $ixr_client->getResponse(); if ( empty( $token ) ) { return $this->error( new WP_Error( 'token_fetch_failed', __( 'Failed to fetch user token from WordPress.com.', 'jetpack-connection' ), 400 ), 'remote_connect' ); } $token = sanitize_text_field( $token ); ( new Tokens() )->update_user_token( $user->ID, sprintf( '%s.%d', $token, $user->ID ), true ); /** * Hook fired at the end of the jetpack.remoteConnect XML-RPC callback * * @since 1.9.7 */ do_action( 'jetpack_remote_connect_end' ); return $this->connection->has_connected_owner(); } /** * Getter for the local user to act as. * * @param array $request the current request data. * @return WP_User|IXR_Error|false IXR_Error if the request is missing a local_user field, WP_User object on success, or false on failure to find a user. */ private function fetch_and_verify_local_user( $request ) { if ( empty( $request['local_user'] ) ) { return $this->error( new \WP_Error( 'local_user_missing', __( 'The required "local_user" parameter is missing.', 'jetpack-connection' ), 400 ), 'remote_provision' ); } // Local user is used to look up by login, email or ID. $local_user_info = $request['local_user']; return $this->get_user_by_anything( $local_user_info ); } /** * Gets the user object by its data. * * @param string $user_id can be any identifying user data. * @return WP_User|false WP_User object on success, false on failure. */ private function get_user_by_anything( $user_id ) { $user = get_user_by( 'login', $user_id ); if ( ! $user ) { $user = get_user_by( 'email', $user_id ); } if ( ! $user ) { $user = get_user_by( 'ID', $user_id ); } return $user; } /** * Possible error_codes: * * - verify_secret_1_missing * - verify_secret_1_malformed * - verify_secrets_missing: verification secrets are not found in database * - verify_secrets_incomplete: verification secrets are only partially found in database * - verify_secrets_expired: verification secrets have expired * - verify_secrets_mismatch: stored secret_1 does not match secret_1 sent by Jetpack.WordPress.com * - state_missing: required parameter of state not found * - state_malformed: state is not a digit * - invalid_state: state in request does not match the stored state * * The 'authorize' and 'register' actions have additional error codes * * state_missing: a state ( user id ) was not supplied * state_malformed: state is not the correct data type * invalid_state: supplied state does not match the stored state * * @param array $params action An array of 3 parameters: * [0]: string action. Possible values are `authorize`, `publicize` and `register`. * [1]: string secret_1. * [2]: int state. * @return \IXR_Error|string IXR_Error on failure, secret_2 on success. */ public function verify_action( $params ) { $action = isset( $params[0] ) ? $params[0] : ''; $verify_secret = isset( $params[1] ) ? $params[1] : ''; $state = isset( $params[2] ) ? $params[2] : ''; $result = ( new Secrets() )->verify( $action, $verify_secret, $state ); if ( is_wp_error( $result ) ) { return $this->error( $result ); } return $result; } /** * Wrapper for wp_authenticate( $username, $password ); * * @return \WP_User|bool */ public function login() { $this->connection->require_jetpack_authentication(); $user = wp_authenticate( 'username', 'password' ); if ( is_wp_error( $user ) ) { if ( 'authentication_failed' === $user->get_error_code() ) { // Generic error could mean most anything. $this->error = new \WP_Error( 'invalid_request', 'Invalid Request', 403 ); } else { $this->error = $user; } return false; } elseif ( ! $user ) { // Shouldn't happen. $this->error = new \WP_Error( 'invalid_request', 'Invalid Request', 403 ); return false; } wp_set_current_user( $user->ID ); return $user; } /** * Returns the current error as an \IXR_Error * * @param \WP_Error|\IXR_Error $error The error object, optional. * @param string $event_name The event name. * @param \WP_User $user The user object. * @return bool|\IXR_Error */ public function error( $error = null, $event_name = null, $user = null ) { if ( null !== $event_name ) { // This action is documented in class.jetpack-xmlrpc-server.php. do_action( 'jetpack_xmlrpc_server_event', $event_name, 'fail', $error, $user ); } if ( $error !== null ) { $this->error = $error; } if ( is_wp_error( $this->error ) ) { $code = $this->error->get_error_data(); if ( ! $code ) { $code = -10520; } $message = sprintf( 'Jetpack: [%s] %s', $this->error->get_error_code(), $this->error->get_error_message() ); if ( ! class_exists( \IXR_Error::class ) ) { require_once ABSPATH . WPINC . '/class-IXR.php'; } return new \IXR_Error( $code, $message ); } elseif ( is_a( $this->error, 'IXR_Error' ) ) { return $this->error; } return false; } /* API Methods */ /** * Just authenticates with the given Jetpack credentials. * * @return string A success string. The Jetpack plugin filters it and make it return the Jetpack plugin version. */ public function test_connection() { /** * Filters the successful response of the XMLRPC test_connection method * * @param string $response The response string. */ return apply_filters( 'jetpack_xmlrpc_test_connection_response', 'success' ); } /** * Test the API user code. * * @param array $args arguments identifying the test site. */ public function test_api_user_code( $args ) { $client_id = (int) $args[0]; $user_id = (int) $args[1]; $nonce = (string) $args[2]; $verify = (string) $args[3]; if ( ! $client_id || ! $user_id || ! strlen( $nonce ) || 32 !== strlen( $verify ) ) { return false; } $user = get_user_by( 'id', $user_id ); if ( ! $user || is_wp_error( $user ) ) { return false; } /* phpcs:ignore debugging error_log( "CLIENT: $client_id" ); error_log( "USER: $user_id" ); error_log( "NONCE: $nonce" ); error_log( "VERIFY: $verify" ); */ $jetpack_token = ( new Tokens() )->get_access_token( $user_id ); $api_user_code = get_user_meta( $user_id, "jetpack_json_api_$client_id", true ); if ( ! $api_user_code ) { return false; } $hmac = hash_hmac( 'md5', json_encode( // phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode (object) array( 'client_id' => $client_id, 'user_id' => $user_id, 'nonce' => $nonce, 'code' => (string) $api_user_code, ) ), $jetpack_token->secret ); if ( ! hash_equals( $hmac, $verify ) ) { return false; } return $user_id; } /** * Unlink a user from WordPress.com * * When the request is done without any parameter, this XMLRPC callback gets an empty array as input. * * If $user_id is not provided, it will try to disconnect the current logged in user. This will fail if called by the Master User. * * If $user_id is is provided, it will try to disconnect the informed user, even if it's the Master User. * * @param mixed $user_id The user ID to disconnect from this site. */ public function unlink_user( $user_id = array() ) { $user_id = (int) $user_id; if ( $user_id < 1 ) { $user_id = null; } /** * Fired when we want to log an event to the Jetpack event log. * * @since 1.7.0 * @since-jetpack 7.7.0 * * @param string $code Unique name for the event. * @param string $data Optional data about the event. */ do_action( 'jetpack_event_log', 'unlink' ); return $this->connection->disconnect_user( $user_id, (bool) $user_id ); } /** * Returns the home URL, site URL, and URL secret for the current site which can be used on the WPCOM side for * IDC mitigation to decide whether sync should be allowed if the home and siteurl values differ between WPCOM * and the remote Jetpack site. * * @since 1.56.0 Additional data may be added via filter `jetpack_connection_validate_urls_for_idc_mitigation_response`. * * @return array */ public function validate_urls_for_idc_mitigation() { $response = array( 'home' => Urls::home_url(), 'siteurl' => Urls::site_url(), ); /** * Allows modifying the response. * * @since 1.56.0 * * @param array $response */ return apply_filters( 'jetpack_connection_validate_urls_for_idc_mitigation_response', $response ); } /** * Updates the attachment parent object. * * @param array $args attachment and parent identifiers. */ public function update_attachment_parent( $args ) { $attachment_id = (int) $args[0]; $parent_id = (int) $args[1]; return wp_update_post( array( 'ID' => $attachment_id, 'post_parent' => $parent_id, ) ); } /** * Deprecated: This method is no longer part of the Connection package and now lives on the Jetpack plugin. * * Disconnect this blog from the connected wordpress.com account * * @deprecated since 1.25.0 * @see Jetpack_XMLRPC_Methods::disconnect_blog() in the Jetpack plugin * * @return boolean */ public function disconnect_blog() { _deprecated_function( __METHOD__, '1.25.0', 'Jetpack_XMLRPC_Methods::disconnect_blog()' ); if ( class_exists( 'Jetpack_XMLRPC_Methods' ) ) { return Jetpack_XMLRPC_Methods::disconnect_blog(); } return false; } /** * Deprecated: This method is no longer part of the Connection package and now lives on the Jetpack plugin. * * Serve a JSON API request. * * @deprecated since 1.25.0 * @see Jetpack_XMLRPC_Methods::json_api() in the Jetpack plugin * * @param array $args request arguments. */ public function json_api( $args = array() ) { _deprecated_function( __METHOD__, '1.25.0', 'Jetpack_XMLRPC_Methods::json_api()' ); if ( class_exists( 'Jetpack_XMLRPC_Methods' ) ) { return Jetpack_XMLRPC_Methods::json_api( $args ); } return array(); } } jetpack-connection/legacy/class-jetpack-ixr-clientmulticall.php 0000777 00000003464 15251741406 0021031 0 ustar 00 <?php /** * IXR_ClientMulticall * * @package automattic/jetpack-connection * * @since 1.7.0 * @since-jetpack 1.5 * @since-jetpack 7.7 Moved to the jetpack-connection package. */ if ( ! defined( 'ABSPATH' ) ) { exit( 0 ); } /** * A Jetpack implementation of the WordPress core IXR client, capable of multiple calls in a single request. */ class Jetpack_IXR_ClientMulticall extends Jetpack_IXR_Client { /** * Storage for the IXR calls. * * @var array */ public $calls = array(); /** * Add a IXR call to the client. * First argument is the method name. * The rest of the arguments are the params specified to the method. * * @param string[] ...$args IXR args. */ public function addCall( ...$args ) { $method_name = array_shift( $args ); $struct = array( 'methodName' => $method_name, 'params' => $args, ); $this->calls[] = $struct; } /** * Perform the IXR multicall request. * * @param string[] ...$args IXR args. * * @return bool True if request succeeded, false otherwise. */ public function query( ...$args ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable $this->calls = $this->sort_calls( $this->calls ); // Prepare multicall, then call the parent::query() method. return parent::query( 'system.multicall', $this->calls ); } /** * Sort the IXR calls. * Make sure syncs are always done first preserving relative order. * * @param array $calls Calls to sort. * @return array Sorted calls. */ public function sort_calls( $calls ) { $sync_calls = array(); $other_calls = array(); foreach ( $calls as $call ) { if ( 'jetpack.syncContent' === $call['methodName'] ) { $sync_calls[] = $call; } else { $other_calls[] = $call; } } return array_merge( $sync_calls, $other_calls ); } } jetpack-connection/legacy/class-jetpack-signature.php 0000777 00000035412 15251741406 0017043 0 ustar 00 <?php /** * The Jetpack Connection signature class file. * * @package automattic/jetpack-connection */ use Automattic\Jetpack\Connection\Manager as Connection_Manager; /** * The Jetpack Connection signature class that is used to sign requests. */ class Jetpack_Signature { /** * Token part of the access token. * * @access public * @var string */ public $token; /** * Access token secret. * * @access public * @var string */ public $secret; /** * Timezone difference (in seconds). * * @access public * @var int */ public $time_diff; /** * The current request URL. * * @access public * @var string */ public $current_request_url; /** * Constructor. * * @param string $access_token Access token. * @param int $time_diff Timezone difference (in seconds). */ public function __construct( $access_token, $time_diff = 0 ) { $secret = explode( '.', $access_token ); if ( 2 !== count( $secret ) ) { return; } $this->token = $secret[0]; $this->secret = $secret[1]; $this->time_diff = $time_diff; } /** * Sign the current request. * * @todo Implement a proper nonce verification. * * @param array $override Optional arguments to override the ones from the current request. * @return string|WP_Error Request signature, or a WP_Error on failure. */ public function sign_current_request( $override = array() ) { if ( isset( $override['scheme'] ) ) { $scheme = $override['scheme']; if ( ! in_array( $scheme, array( 'http', 'https' ), true ) ) { return new WP_Error( 'invalid_scheme', 'Invalid URL scheme' ); } } elseif ( is_ssl() ) { $scheme = 'https'; } else { $scheme = 'http'; } $port = $this->get_current_request_port(); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidatedNotSanitized -- Sniff misses the esc_url_raw wrapper. $this->current_request_url = esc_url_raw( wp_unslash( "{$scheme}://{$_SERVER['HTTP_HOST']}:{$port}" . ( isset( $_SERVER['REQUEST_URI'] ) ? $_SERVER['REQUEST_URI'] : '' ) ) ); if ( array_key_exists( 'body', $override ) && ! empty( $override['body'] ) ) { $body = $override['body']; } elseif ( isset( $_SERVER['REQUEST_METHOD'] ) && 'POST' === strtoupper( $_SERVER['REQUEST_METHOD'] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- This is validating. $body = isset( $GLOBALS['HTTP_RAW_POST_DATA'] ) ? $GLOBALS['HTTP_RAW_POST_DATA'] : null; // Convert the $_POST to the body, if the body was empty. This is how arrays are hashed // and encoded on the Jetpack side. if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Used to generate a cryptographic signature of the post data. Not actually using any of it here. if ( empty( $body ) && is_array( $_POST ) && $_POST !== array() ) { $body = $_POST; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- We need all of $_POST in order to generate a cryptographic signature of the post data. } } } elseif ( isset( $_SERVER['REQUEST_METHOD'] ) && 'PUT' === strtoupper( $_SERVER['REQUEST_METHOD'] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- This is validating. // This is a little strange-looking, but there doesn't seem to be another way to get the PUT body. $raw_put_data = file_get_contents( 'php://input' ); parse_str( $raw_put_data, $body ); if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) { $put_data = json_decode( $raw_put_data, true ); if ( is_array( $put_data ) && $put_data !== array() ) { $body = $put_data; } } } else { $body = null; } if ( empty( $body ) ) { $body = null; } $a = array(); foreach ( array( 'token', 'timestamp', 'nonce', 'body-hash' ) as $parameter ) { if ( isset( $override[ $parameter ] ) ) { $a[ $parameter ] = $override[ $parameter ]; } else { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $a[ $parameter ] = isset( $_GET[ $parameter ] ) ? filter_var( wp_unslash( $_GET[ $parameter ] ) ) : ''; } } $method = isset( $override['method'] ) ? $override['method'] : ( isset( $_SERVER['REQUEST_METHOD'] ) ? filter_var( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) : null ); return $this->sign_request( $a['token'], $a['timestamp'], $a['nonce'], $a['body-hash'], $method, $this->current_request_url, $body, true ); } /** * Sign a specified request. * * @todo Having body_hash v. body-hash is annoying. Refactor to accept an array? * @todo Use wp_json_encode() instead of json_encode()? * * @param string $token Request token. * @param int $timestamp Timestamp of the request. * @param string $nonce Request nonce. * @param string $body_hash Request body hash. * @param string $method Request method. * @param string $url Request URL. * @param mixed $body Request body. * @param bool $verify_body_hash Whether to verify the body hash against the body. * @return string|WP_Error Request signature, or a WP_Error on failure. */ public function sign_request( $token = '', $timestamp = 0, $nonce = '', $body_hash = '', $method = '', $url = '', $body = null, $verify_body_hash = true ) { if ( ! $this->secret ) { return new WP_Error( 'invalid_secret', 'Invalid secret' ); } if ( ! $this->token ) { return new WP_Error( 'invalid_token', 'Invalid token' ); } list( $token ) = explode( '.', $token ); $signature_details = compact( 'token', 'timestamp', 'nonce', 'body_hash', 'method', 'url' ); if ( ! str_starts_with( $token, "$this->token:" ) ) { return new WP_Error( 'token_mismatch', 'Incorrect token', compact( 'signature_details' ) ); } // If we got an array at this point, let's encode it, so we can see what it looks like as a string. if ( is_array( $body ) ) { if ( $body !== array() ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode $body = json_encode( $body ); } else { $body = ''; } } $required_parameters = array( 'token', 'timestamp', 'nonce', 'method', 'url' ); if ( $body !== null ) { $required_parameters[] = 'body_hash'; if ( ! is_string( $body ) ) { return new WP_Error( 'invalid_body', 'Body is malformed.', compact( 'signature_details' ) ); } } foreach ( $required_parameters as $required ) { if ( ! is_scalar( $$required ) ) { return new WP_Error( 'invalid_signature', sprintf( 'The required "%s" parameter is malformed.', str_replace( '_', '-', $required ) ), compact( 'signature_details' ) ); } if ( ! strlen( $$required ) ) { return new WP_Error( 'invalid_signature', sprintf( 'The required "%s" parameter is missing.', str_replace( '_', '-', $required ) ), compact( 'signature_details' ) ); } } if ( empty( $body ) ) { if ( $body_hash ) { return new WP_Error( 'invalid_body_hash', 'Invalid body hash for empty body.', compact( 'signature_details' ) ); } } else { $connection = new Connection_Manager(); if ( $verify_body_hash && $connection->sha1_base64( $body ) !== $body_hash ) { return new WP_Error( 'invalid_body_hash', 'The body hash does not match.', compact( 'signature_details' ) ); } } $parsed = wp_parse_url( $url ); if ( ! isset( $parsed['host'] ) ) { return new WP_Error( 'invalid_signature', sprintf( 'The required "%s" parameter is malformed.', 'url' ), compact( 'signature_details' ) ); } if ( ! empty( $parsed['port'] ) ) { $port = $parsed['port']; } elseif ( 'http' === $parsed['scheme'] ) { $port = 80; } elseif ( 'https' === $parsed['scheme'] ) { $port = 443; } else { return new WP_Error( 'unknown_scheme_port', "The scheme's port is unknown", compact( 'signature_details' ) ); } if ( ! ctype_digit( "$timestamp" ) || 10 < strlen( (string) $timestamp ) ) { // If Jetpack is around in 275 years, you can blame mdawaffe for the bug. return new WP_Error( 'invalid_signature', sprintf( 'The required "%s" parameter is malformed.', 'timestamp' ), compact( 'signature_details' ) ); } $local_time = $timestamp - $this->time_diff; if ( $local_time < time() - 600 || $local_time > time() + 300 ) { return new WP_Error( 'invalid_signature', 'The timestamp is too old.', compact( 'signature_details' ) ); } if ( 12 < strlen( $nonce ) || preg_match( '/[^a-zA-Z0-9]/', $nonce ) ) { return new WP_Error( 'invalid_signature', sprintf( 'The required "%s" parameter is malformed.', 'nonce' ), compact( 'signature_details' ) ); } $normalized_request_pieces = array( $token, $timestamp, $nonce, $body_hash, strtoupper( $method ), strtolower( $parsed['host'] ), $port, empty( $parsed['path'] ) ? '' : $parsed['path'], // Normalized Query String. ); $normalized_request_pieces = array_merge( $normalized_request_pieces, $this->normalized_query_parameters( isset( $parsed['query'] ) ? $parsed['query'] : '' ) ); $flat_normalized_request_pieces = array(); foreach ( $normalized_request_pieces as $piece ) { if ( is_array( $piece ) ) { foreach ( $piece as $subpiece ) { $flat_normalized_request_pieces[] = $subpiece; } } else { $flat_normalized_request_pieces[] = $piece; } } $normalized_request_pieces = $flat_normalized_request_pieces; $normalized_request_string = implode( "\n", $normalized_request_pieces ) . "\n"; // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode return base64_encode( hash_hmac( 'sha1', $normalized_request_string, $this->secret, true ) ); } /** * Retrieve and normalize the parameters from a query string. * * @param string $query_string Query string. * @return array Normalized query string parameters. */ public function normalized_query_parameters( $query_string ) { parse_str( $query_string, $array ); unset( $array['signature'] ); $names = array_keys( $array ); $values = array_values( $array ); $names = array_map( array( $this, 'encode_3986' ), $names ); $values = array_map( array( $this, 'encode_3986' ), $values ); $pairs = array_map( array( $this, 'join_with_equal_sign' ), $names, $values ); sort( $pairs ); return $pairs; } /** * Encodes a string or array of strings according to RFC 3986. * * @param string|array $string_or_array String or array to encode. * @return string|array URL-encoded string or array. */ public function encode_3986( $string_or_array ) { if ( is_array( $string_or_array ) ) { return array_map( array( $this, 'encode_3986' ), $string_or_array ); } return rawurlencode( $string_or_array ); } /** * Concatenates a parameter name and a parameter value with an equals sign between them. * * @param string $name Parameter name. * @param string|array $value Parameter value. * @return string|array A string pair (e.g. `name=value`) or an array of string pairs. */ public function join_with_equal_sign( $name, $value ) { if ( is_array( $value ) ) { return $this->join_array_with_equal_sign( $name, $value ); } return "{$name}={$value}"; } /** * Helper function for join_with_equal_sign for handling arrayed values. * Explicitly supports nested arrays. * * @param string $name Parameter name. * @param array $value Parameter value. * @return array An array of string pairs (e.g. `[ name[example]=value ]`). */ private function join_array_with_equal_sign( $name, $value ) { $result = array(); foreach ( $value as $value_key => $value_value ) { $joined_value = $this->join_with_equal_sign( $name . '[' . $value_key . ']', $value_value ); if ( is_array( $joined_value ) ) { foreach ( array_values( $joined_value ) as $individual_joined_value ) { $result[] = $individual_joined_value; } } elseif ( is_string( $joined_value ) ) { $result[] = $joined_value; } } sort( $result ); return $result; } /** * Gets the port that should be considered to sign the current request. * * It will analyze the current request, as well as some Jetpack constants, to return the string * to be concatenated in the URL representing the port of the current request. * * @since 1.8.4 * * @return string The port to be used in the signature */ public function get_current_request_port() { $host_port = isset( $_SERVER['HTTP_X_FORWARDED_PORT'] ) ? $this->sanitize_host_post( $_SERVER['HTTP_X_FORWARDED_PORT'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- This is validating. if ( '' === $host_port && isset( $_SERVER['SERVER_PORT'] ) ) { $host_port = $this->sanitize_host_post( $_SERVER['SERVER_PORT'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- This is validating. } /** * Note: This port logic is tested in the Jetpack_Cxn_Tests->test__server_port_value() test. * Please update the test if any changes are made in this logic. */ if ( is_ssl() ) { // 443: Standard Port // 80: Assume we're behind a proxy without X-Forwarded-Port. Hardcoding "80" here means most sites // with SSL termination proxies (self-served, Cloudflare, etc.) don't need to fiddle with // the JETPACK_SIGNATURE__HTTPS_PORT constant. The code also implies we can't talk to a // site at https://example.com:80/ (which would be a strange configuration). // JETPACK_SIGNATURE__HTTPS_PORT: Set this constant in wp-config.php to the back end webserver's port // if the site is behind a proxy running on port 443 without // X-Forwarded-Port and the back end's port is *not* 80. It's better, // though, to configure the proxy to send X-Forwarded-Port. $https_port = defined( 'JETPACK_SIGNATURE__HTTPS_PORT' ) ? $this->sanitize_host_post( JETPACK_SIGNATURE__HTTPS_PORT ) : '443'; $port = in_array( $host_port, array( '443', '80', $https_port ), true ) ? '' : $host_port; } else { // 80: Standard Port // JETPACK_SIGNATURE__HTTPS_PORT: Set this constant in wp-config.php to the back end webserver's port // if the site is behind a proxy running on port 80 without // X-Forwarded-Port. It's better, though, to configure the proxy to // send X-Forwarded-Port. $http_port = defined( 'JETPACK_SIGNATURE__HTTP_PORT' ) ? $this->sanitize_host_post( JETPACK_SIGNATURE__HTTP_PORT ) : '80'; $port = in_array( $host_port, array( '80', $http_port ), true ) ? '' : $host_port; } return (string) $port; } /** * Sanitizes a variable checking if it's a valid port number, which can be an integer or a numeric string * * @since 1.8.4 * * @param mixed $port_number Variable representing a port number. * @return string Always a string with a valid port number, or an empty string if input is invalid */ public function sanitize_host_post( $port_number ) { if ( ! is_int( $port_number ) && ! is_string( $port_number ) ) { return ''; } if ( is_string( $port_number ) && ! ctype_digit( $port_number ) ) { return ''; } if ( 0 >= (int) $port_number || 65535 < $port_number ) { return ''; } return (string) $port_number; } } jetpack-connection/legacy/class-jetpack-tracks-client.php 0000777 00000014666 15251741406 0017615 0 ustar 00 <?php /** * Legacy Jetpack Tracks Client * * @package automattic/jetpack-tracking */ use Automattic\Jetpack\Connection\Manager; /** * Jetpack_Tracks_Client * * Send Tracks events on behalf of a user * * Example Usage: ```php require( dirname(__FILE__).'path/to/tracks/class-jetpack-tracks-client.php' ); $result = Jetpack_Tracks_Client::record_event( array( '_en' => $event_name, // required '_ui' => $user_id, // required unless _ul is provided '_ul' => $user_login, // required unless _ui is provided // Optional, but recommended '_ts' => $ts_in_ms, // Default: now '_via_ip' => $client_ip, // we use it for geo, etc. // Possibly useful to set some context for the event '_via_ua' => $client_user_agent, '_via_url' => $client_url, '_via_ref' => $client_referrer, // For user-targeted tests 'abtest_name' => $abtest_name, 'abtest_variation' => $abtest_variation, // Your application-specific properties 'custom_property' => $some_value, ) ); if ( is_wp_error( $result ) ) { // Handle the error in your app } ``` */ class Jetpack_Tracks_Client { const PIXEL = 'https://pixel.wp.com/t.gif'; const BROWSER_TYPE = 'php-agent'; const USER_AGENT_SLUG = 'tracks-client'; const VERSION = '0.3'; /** * Stores the Terms of Service Object Reference. * * @var null */ private static $terms_of_service = null; /** * Record an event. * * @param mixed $event Event object to send to Tracks. An array will be cast to object. Required. * Properties are included directly in the pixel query string after light validation. * @return mixed True on success, WP_Error on failure */ public static function record_event( $event ) { if ( ! self::$terms_of_service ) { self::$terms_of_service = new \Automattic\Jetpack\Terms_Of_Service(); } // Don't track users who have opted out or not agreed to our TOS, or are not running an active Jetpack. if ( ! self::$terms_of_service->has_agreed() || ! empty( $_COOKIE['tk_opt-out'] ) ) { return false; } if ( ! $event instanceof Jetpack_Tracks_Event ) { $event = new Jetpack_Tracks_Event( $event ); } if ( is_wp_error( $event ) ) { return $event; } $pixel = $event->build_pixel_url(); if ( ! $pixel ) { return new WP_Error( 'invalid_pixel', 'cannot generate tracks pixel for given input', 400 ); } return self::record_pixel( $pixel ); } /** * Synchronously request the pixel. * * @param string $pixel The wp.com tracking pixel. * @return array|bool|WP_Error True if successful. wp_remote_get response or WP_Error if not. */ public static function record_pixel( $pixel ) { // Add the Request Timestamp and URL terminator just before the HTTP request. $pixel .= '&_rt=' . self::build_timestamp() . '&_=_'; $response = wp_remote_get( $pixel, array( 'blocking' => true, // The default, but being explicit here :). 'timeout' => 1, 'redirection' => 2, 'httpversion' => '1.1', 'user-agent' => self::get_user_agent(), ) ); if ( is_wp_error( $response ) ) { return $response; } $code = isset( $response['response']['code'] ) ? $response['response']['code'] : 0; if ( 200 !== $code ) { return new WP_Error( 'request_failed', 'Tracks pixel request failed', $code ); } return true; } /** * Get the user agent. * * @return string The user agent. */ public static function get_user_agent() { return self::USER_AGENT_SLUG . '-v' . self::VERSION; } /** * Build an event and return its tracking URL * * @deprecated Call the `build_pixel_url` method on a Jetpack_Tracks_Event object instead. * @param array $event Event keys and values. * @return string URL of a tracking pixel. */ public static function build_pixel_url( $event ) { $_event = new Jetpack_Tracks_Event( $event ); return $_event->build_pixel_url(); } /** * Validate input for a tracks event. * * @deprecated Instantiate a Jetpack_Tracks_Event object instead * @param array $event Event keys and values. * @return mixed Validated keys and values or WP_Error on failure */ private static function validate_and_sanitize( $event ) { $_event = new Jetpack_Tracks_Event( $event ); if ( is_wp_error( $_event ) ) { return $_event; } return get_object_vars( $_event ); } /** * Builds a timestamp. * * Milliseconds since 1970-01-01. * * @return string */ public static function build_timestamp() { $ts = round( microtime( true ) * 1000 ); return number_format( $ts, 0, '', '' ); } /** * Grabs the user's anon id from cookies, or generates and sets a new one * * @return string An anon id for the user */ public static function get_anon_id() { static $anon_id = null; if ( ! isset( $anon_id ) ) { // Did the browser send us a cookie? if ( isset( $_COOKIE['tk_ai'] ) && preg_match( '#^[a-z]+:[A-Za-z0-9+/=]{24}$#', $_COOKIE['tk_ai'] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- This is validating. $anon_id = $_COOKIE['tk_ai']; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- This is validating. } else { $binary = ''; // Generate a new anonId and try to save it in the browser's cookies. // Note that base64-encoding an 18 character string generates a 24-character anon id. for ( $i = 0; $i < 18; ++$i ) { $binary .= chr( wp_rand( 0, 255 ) ); } $anon_id = 'jetpack:' . base64_encode( $binary ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode if ( ! headers_sent() && ! ( defined( 'REST_REQUEST' ) && REST_REQUEST ) && ! ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST ) ) { setcookie( 'tk_ai', $anon_id, 0, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), false ); // phpcs:ignore Jetpack.Functions.SetCookie -- This is a random value and should be fine. } } } return $anon_id; } /** * Gets the WordPress.com user's Tracks identity, if connected. * * @return array|bool */ public static function get_connected_user_tracks_identity() { $user_data = ( new Manager() )->get_connected_user_data(); if ( ! $user_data ) { return false; } return array( 'blogid' => Jetpack_Options::get_option( 'id', 0 ), 'email' => $user_data['email'], 'userid' => $user_data['ID'], 'username' => $user_data['login'], 'user_locale' => $user_data['user_locale'], ); } } jetpack-connection/legacy/class-jetpack-tracks-event.php 0000777 00000010530 15251741406 0017442 0 ustar 00 <?php /** * Class Jetpack_Tracks_Event. Legacy. * * @package automattic/jetpack-sync */ /* * Example Usage: ```php require_once( dirname(__FILE__) . 'path/to/tracks/class-jetpack-tracks-event.php' ); $event = new Jetpack_Tracks_Event( array( '_en' => $event_name, // required '_ui' => $user_id, // required unless _ul is provided '_ul' => $user_login, // required unless _ui is provided // Optional, but recommended '_via_ip' => $client_ip, // for geo, etc. // Possibly useful to set some context for the event '_via_ua' => $client_user_agent, '_via_url' => $client_url, '_via_ref' => $client_referrer, // For user-targeted tests 'abtest_name' => $abtest_name, 'abtest_variation' => $abtest_variation, // Your application-specific properties 'custom_property' => $some_value, ) ); if ( is_wp_error( $event->error ) ) { // Handle the error in your app } $bump_and_redirect_pixel = $event->build_signed_pixel_url(); ``` */ /** * Class Jetpack_Tracks_Event */ #[AllowDynamicProperties] class Jetpack_Tracks_Event { const EVENT_NAME_REGEX = '/^(([a-z0-9]+)_){2}([a-z0-9_]+)$/'; const PROP_NAME_REGEX = '/^[a-z_][a-z0-9_]*$/'; /** * Tracks Event Error. * * @var mixed Error. */ public $error; /** * Jetpack_Tracks_Event constructor. * * @param object $event Tracks event. */ public function __construct( $event ) { $_event = self::validate_and_sanitize( $event ); if ( is_wp_error( $_event ) ) { $this->error = $_event; return; } foreach ( $_event as $key => $value ) { $this->{$key} = $value; } } /** * Record a track event. */ public function record() { return Jetpack_Tracks_Client::record_event( $this ); } /** * Annotate the event with all relevant info. * * @param mixed $event Object or (flat) array. * @return mixed The transformed event array or WP_Error on failure. */ public static function validate_and_sanitize( $event ) { $event = (object) $event; // Required. if ( ! $event->_en ) { return new WP_Error( 'invalid_event', 'A valid event must be specified via `_en`', 400 ); } // delete non-routable addresses otherwise geoip will discard the record entirely. if ( property_exists( $event, '_via_ip' ) && preg_match( '/^192\.168|^10\./', $event->_via_ip ) ) { unset( $event->_via_ip ); } $validated = array( 'browser_type' => Jetpack_Tracks_Client::BROWSER_TYPE, '_aua' => Jetpack_Tracks_Client::get_user_agent(), ); $_event = (object) array_merge( (array) $event, $validated ); // If you want to block property names, do it here. // Make sure we have an event timestamp. if ( ! isset( $_event->_ts ) ) { $_event->_ts = Jetpack_Tracks_Client::build_timestamp(); } return $_event; } /** * Build a pixel URL that will send a Tracks event when fired. * On error, returns an empty string (''). * * @return string A pixel URL or empty string ('') if there were invalid args. */ public function build_pixel_url() { if ( $this->error ) { return ''; } $args = get_object_vars( $this ); // Request Timestamp and URL Terminator must be added just before the HTTP request or not at all. unset( $args['_rt'] ); unset( $args['_'] ); $validated = self::validate_and_sanitize( $args ); if ( is_wp_error( $validated ) ) { return ''; } return Jetpack_Tracks_Client::PIXEL . '?' . http_build_query( $validated ); } /** * Validate the event name. * * @param string $name Event name. * @return false|int */ public static function event_name_is_valid( $name ) { return preg_match( self::EVENT_NAME_REGEX, $name ); } /** * Validates prop name * * @param string $name Property name. * * @return false|int Truthy value. */ public static function prop_name_is_valid( $name ) { return preg_match( self::PROP_NAME_REGEX, $name ); } /** * Scrutinize event name. * * @param object $event Tracks event. */ public static function scrutinize_event_names( $event ) { if ( ! self::event_name_is_valid( $event->_en ) ) { return; } $whitelisted_key_names = array( 'anonId', 'Browser_Type', ); foreach ( array_keys( (array) $event ) as $key ) { if ( in_array( $key, $whitelisted_key_names, true ) ) { continue; } if ( ! self::prop_name_is_valid( $key ) ) { return; } } } } jetpack-connection/legacy/class-jetpack-options.php 0000777 00000066616 15251741406 0016547 0 ustar 00 <?php /** * Legacy Jetpack_Options class. * * @package automattic/jetpack-connection */ use Automattic\Jetpack\Constants; /** * Class Jetpack_Options */ class Jetpack_Options { /** * An array that maps a grouped option type to an option name. * * @var array */ private static $grouped_options = array( 'compact' => 'jetpack_options', 'private' => 'jetpack_private_options', ); /** * Returns an array of option names for a given type. * * @param string $type The type of option to return. Defaults to 'compact'. * * @return array */ public static function get_option_names( $type = 'compact' ) { switch ( $type ) { case 'non-compact': case 'non_compact': return array( 'activated', 'active_modules', 'active_modules_initialized', // (bool) used to determine that all the default modules were activated, so we know how to act on a reconnection. 'allowed_xsite_search_ids', // (array) Array of WP.com blog ids that are allowed to search the content of this site 'available_modules', 'do_activate', 'log', 'slideshow_background_color', 'widget_twitter', 'wpcc_options', 'relatedposts', 'file_data', 'autoupdate_plugins', // (array) An array of plugin ids ( eg. jetpack/jetpack ) that should be autoupdated 'autoupdate_plugins_translations', // (array) An array of plugin ids ( eg. jetpack/jetpack ) that should be autoupdated translation files. 'autoupdate_themes', // (array) An array of theme ids ( eg. twentyfourteen ) that should be autoupdated 'autoupdate_themes_translations', // (array) An array of theme ids ( eg. twentyfourteen ) that should autoupdated translation files. 'autoupdate_core', // (bool) Whether or not to autoupdate core 'autoupdate_translations', // (bool) Whether or not to autoupdate all translations 'json_api_full_management', // (bool) Allow full management (eg. Activate, Upgrade plugins) of the site via the JSON API. 'sync_non_public_post_stati', // (bool) Allow synchronisation of posts and pages with non-public status. 'site_icon_url', // (string) url to the full site icon 'site_icon_id', // (int) Attachment id of the site icon file 'dismissed_manage_banner', // (bool) Dismiss Jetpack manage banner allows the user to dismiss the banner permanently 'unique_connection', // (array) A flag to determine a unique connection to wordpress.com two values "connected" and "disconnected" with values for how many times each has occurred 'unique_registrations', // (integer) A counter of how many times the site was registered 'protect_whitelist', // (array) IP Address for the Protect module to ignore 'sync_error_idc', // (bool|array) false or array containing the site's home and siteurl at time of IDC error 'sync_health_status', // (bool|array) An array of data relating to Jetpack's sync health. 'safe_mode_confirmed', // (bool) True if someone confirms that this site was correctly put into safe mode automatically after an identity crisis is discovered. 'migrate_for_idc', // (bool) True if someone confirms that this site should migrate stats and subscribers from its previous URL 'ab_connect_banner_green_bar', // (int) Version displayed of the A/B test for the green bar at the top of the connect banner. 'tos_agreed', // (bool) Whether or not the TOS for connection has been agreed upon. 'static_asset_cdn_files', // (array) An nested array of files that we can swap out for cdn versions. 'mapbox_api_key', // (string) Mapbox API Key, for use with Map block. 'mailchimp', // (string) Mailchimp keyring data, for mailchimp block. 'xmlrpc_errors', // (array) Keys are XML-RPC signature error codes. Values are truthy. 'dismissed_wizard_banner', // (int) (DEPRECATED) True if the Wizard banner has been dismissed. ); case 'private': return array( 'blog_token', // (string) The Client Secret/Blog Token of this site. 'user_token', // (string) The User Token of this site. (deprecated) 'user_tokens', // (array) User Tokens for each user of this site who has connected to jetpack.wordpress.com. 'purchase_token', // (string) Token for logged out user purchases. 'token_lock', // (string) Token lock in format `expiration_date|||site_url`. ); case 'network': return array( 'file_data', // (array) List of absolute paths to all Jetpack modules ); } return array( 'id', // (int) The Client ID/WP.com Blog ID of this site. 'publicize_connections', // (array) An array of Publicize connections from WordPress.com. 'master_user', // (int) The local User ID of the user who connected this site to jetpack.wordpress.com. 'version', // (string) Used during upgrade procedure to auto-activate new modules. version:time. 'old_version', // (string) Used to determine which modules are the most recently added. previous_version:time. 'fallback_no_verify_ssl_certs', // (int) Flag for determining if this host must skip SSL Certificate verification due to misconfigured SSL. 'time_diff', // (int) Offset between Jetpack server's clocks and this server's clocks. Jetpack Server Time = time() + (int) Jetpack_Options::get_option( 'time_diff' ) 'public', // (int|bool) If we think this site is public or not (1, 0), false if we haven't yet tried to figure it out. 'videopress', // (array) VideoPress options array. 'is_network_site', // (int|bool) If we think this site is a network or a single blog (1, 0), false if we haven't yet tried to figue it out. 'social_links', // (array) The specified links for each social networking site. 'identity_crisis_whitelist', // (array) An array of options, each having an array of the values whitelisted for it. 'gplus_authors', // (array) The Google+ authorship information for connected users. 'last_heartbeat', // (int) The timestamp of the last heartbeat that fired. 'hide_jitm', // (array) A list of just in time messages that we should not show because they have been dismissed by the user. 'custom_css_4.7_migration', // (bool) Whether Custom CSS has scanned for and migrated any legacy CSS CPT entries to the new Core format. 'image_widget_migration', // (bool) Whether any legacy Image Widgets have been converted to the new Core widget. 'gallery_widget_migration', // (bool) Whether any legacy Gallery Widgets have been converted to the new Core widget. 'sso_first_login', // (bool) Is this the first time the user logins via SSO. 'dismissed_hints', // (array) Part of Plugin Search Hints. List of cards that have been dismissed. 'first_admin_view', // (bool) Set to true the first time the user views the admin. Usually after the initial connection. 'setup_wizard_questionnaire', // (array) (DEPRECATED) List of user choices from the setup wizard. 'setup_wizard_status', // (string) (DEPRECATED) Status of the setup wizard. 'licensing_error', // (string) Last error message occurred while attaching licenses that is yet to be surfaced to the user. 'recommendations_data', // (array) The user choice and other data for the recommendations. 'recommendations_step', // (string) The current step of the recommendations. 'recommendations_conditional', // (array) An array of action-based recommendations. 'licensing_activation_notice_dismiss', // (array) The `last_detached_count` and the `last_dismissed_time` for the user-license activation notice. 'has_seen_wc_connection_modal', // (bool) Whether the site has displayed the WooCommerce Connection modal 'partner_coupon', // (string) A Jetpack partner issued coupon to promote a sale together with Jetpack. 'partner_coupon_added', // (string) A date for when `partner_coupon` was added, so we can auto-purge after a certain time interval. 'dismissed_backup_review_restore', // (bool) Determines if the component review request is dismissed for successful restore requests. 'dismissed_backup_review_backups', // (bool) Determines if the component review request is dismissed for successful backup requests. 'identity_crisis_url_secret', // (array) The IDC URL secret and its expiration date. 'identity_crisis_ip_requester', // (array) The IDC IP address and its expiration date. 'dismissed_welcome_banner', // (bool) Determines if the welcome banner has been dismissed or not. 'recommendations_evaluation', // (object) Catalog of recommended modules with corresponding score following successful site evaluation in Welcome Banner. 'dismissed_recommendations', // (bool) Determines if the recommendations have been dismissed or not. 'recommendations_first_run', // (bool) Determines if the current recommendations are the initial default auto-loaded ones (without user input). 'historically_active_modules', // (array) List of installed plugins/enabled modules that have at one point in time been active and working ); } /** * Is the option name valid? * * @param string $name The name of the option. * @param string|null $group The name of the group that the option is in. Default to null, which will search non_compact. * * @return bool Is the option name valid? */ public static function is_valid( $name, $group = null ) { if ( is_array( $name ) ) { $compact_names = array(); foreach ( array_keys( self::$grouped_options ) as $_group ) { $compact_names = array_merge( $compact_names, self::get_option_names( $_group ) ); } $result = array_diff( $name, self::get_option_names( 'non_compact' ), $compact_names ); return empty( $result ); } if ( $group === null || 'non_compact' === $group ) { if ( in_array( $name, self::get_option_names( $group ), true ) ) { return true; } } foreach ( array_keys( self::$grouped_options ) as $_group ) { if ( $group === null || $group === $_group ) { if ( in_array( $name, self::get_option_names( $_group ), true ) ) { return true; } } } return false; } /** * Checks if an option must be saved for the whole network in WP Multisite * * @param string $option_name Option name. It must come _without_ `jetpack_%` prefix. The method will prefix the option name. * * @return bool */ public static function is_network_option( $option_name ) { if ( ! is_multisite() ) { return false; } return in_array( $option_name, self::get_option_names( 'network' ), true ); } /** * Filters the requested option. * This is a wrapper around `get_option_from_database` so that we can filter the option. * * @param string $name Option name. It must come _without_ `jetpack_%` prefix. The method will prefix the option name. * @param mixed $default (optional). * * @return mixed */ public static function get_option( $name, $default = false ) { // Check if external storage should be used for this option if ( self::should_use_external_storage( $name ) ) { // Try external storage if ( class_exists( 'Automattic\Jetpack\Connection\External_Storage' ) ) { $external_value = \Automattic\Jetpack\Connection\External_Storage::get_value( $name ); if ( null !== $external_value ) { return $external_value; } } } /** * Filter Jetpack Options. * Can be useful in environments when Jetpack is running with a different setup * * @since 1.7.0 * * @param string $value The value from the database. * @param string $name Option name, _without_ `jetpack_%` prefix. * @return string $value, unless the filters modify it. */ return apply_filters( 'jetpack_options', self::get_option_from_database( $name, $default ), $name ); } /** * Returns the requested option. Looks in jetpack_options or jetpack_$name as appropriate. * * @param string $name Option name. It must come _without_ `jetpack_%` prefix. The method will prefix the option name. * @param mixed $default (optional). * * @return mixed */ private static function get_option_from_database( $name, $default = false ) { if ( self::is_valid( $name, 'non_compact' ) ) { if ( self::is_network_option( $name ) ) { return get_site_option( "jetpack_$name", $default ); } return get_option( "jetpack_$name", $default ); } foreach ( array_keys( self::$grouped_options ) as $group ) { if ( self::is_valid( $name, $group ) ) { return self::get_grouped_option( $group, $name, $default ); } } return $default; } /** * Options that can be stored in external storage. * * @since 6.18.0 * * @var array */ private static $external_storage_allowlist = array( 'blog_token', 'id', 'master_user', 'user_tokens' ); /** * Determines if external storage should be used for a given option. * Simple allowlist check with global killswitch. * * @since 6.17.0 * * @param string $name Option name, _without_ `jetpack_%` prefix. * @return bool True if external storage should be checked for this option. */ private static function should_use_external_storage( $name ) { // Check allowlist and global killswitch if ( ! in_array( $name, self::$external_storage_allowlist, true ) || ( defined( 'JETPACK_EXTERNAL_STORAGE_DISABLED' ) && constant( 'JETPACK_EXTERNAL_STORAGE_DISABLED' ) ) ) { return false; } return true; } /** * Returns the requested option, and ensures it's autoloaded in the future. * This does _not_ adjust the prefix in any way (does not prefix jetpack_%) * * @param string $name Option name. * @param mixed $default (optional). * * @return mixed */ public static function get_option_and_ensure_autoload( $name, $default ) { // In this function the name is not adjusted by prefixing jetpack_ // so if it has already prefixed, we'll replace it and then // check if the option name is a network option or not. $jetpack_name = preg_replace( '/^jetpack_/', '', $name, 1 ); $is_network_option = self::is_network_option( $jetpack_name ); $value = $is_network_option ? get_site_option( $name ) : get_option( $name ); if ( false === $value && false !== $default ) { if ( $is_network_option ) { add_site_option( $name, $default ); } else { add_option( $name, $default ); } $value = $default; } return $value; } /** * Update grouped option * * @param string $group Options group. * @param string $name Options name. * @param mixed $value Options value. * * @return bool Success or failure. */ private static function update_grouped_option( $group, $name, $value ) { $options = get_option( self::$grouped_options[ $group ] ); if ( ! is_array( $options ) ) { $options = array(); } $options[ $name ] = $value; return update_option( self::$grouped_options[ $group ], $options ); } /** * Updates the single given option. Updates jetpack_options or jetpack_$name as appropriate. * * @param string $name Option name. It must come _without_ `jetpack_%` prefix. The method will prefix the option name. * @param mixed $value Option value. * @param bool|null $autoload If not compact option, allows specifying whether to autoload or not. * * @return bool Was the option successfully updated? */ public static function update_option( $name, $value, $autoload = null ) { /** * Fires before Jetpack updates a specific option. * * @since 1.1.2 * @since-jetpack 3.0.0 * * @param string $name The name of the option being updated. * @param mixed $value The new value of the option. */ do_action( 'pre_update_jetpack_option_' . $name, $name, $value ); if ( self::is_valid( $name, 'non_compact' ) ) { if ( self::is_network_option( $name ) ) { return update_site_option( "jetpack_$name", $value ); } return update_option( "jetpack_$name", $value, $autoload ); } foreach ( array_keys( self::$grouped_options ) as $group ) { if ( self::is_valid( $name, $group ) ) { return self::update_grouped_option( $group, $name, $value ); } } trigger_error( sprintf( 'Invalid Jetpack option name: %s', esc_html( $name ) ), E_USER_WARNING ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error -- Don't want to change legacy behavior. return false; } /** * Updates the multiple given options. Updates jetpack_options and/or jetpack_$name as appropriate. * * @param array $array array( option name => option value, ... ). */ public static function update_options( $array ) { $names = array_keys( $array ); foreach ( array_diff( $names, self::get_option_names(), self::get_option_names( 'non_compact' ), self::get_option_names( 'private' ) ) as $unknown_name ) { trigger_error( sprintf( 'Invalid Jetpack option name: %s', esc_html( $unknown_name ) ), E_USER_WARNING ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error -- Don't change legacy behavior. unset( $array[ $unknown_name ] ); } foreach ( $names as $name ) { self::update_option( $name, $array[ $name ] ); } } /** * Deletes the given option. May be passed multiple option names as an array. * Updates jetpack_options and/or deletes jetpack_$name as appropriate. * * @param string|array $names Option names. They must come _without_ `jetpack_%` prefix. The method will prefix the option names. * * @return bool Was the option successfully deleted? */ public static function delete_option( $names ) { $result = true; $names = (array) $names; if ( ! self::is_valid( $names ) ) { // phpcs:disable -- This line triggers a handful of errors; ignoring to avoid changing legacy behavior. trigger_error( sprintf( 'Invalid Jetpack option names: %s', print_r( $names, 1 ) ), E_USER_WARNING ); // phpcs:enable return false; } foreach ( array_intersect( $names, self::get_option_names( 'non_compact' ) ) as $name ) { if ( self::is_network_option( $name ) ) { $result = delete_site_option( "jetpack_$name" ); } else { $result = delete_option( "jetpack_$name" ); } } foreach ( array_keys( self::$grouped_options ) as $group ) { if ( ! self::delete_grouped_option( $group, $names ) ) { $result = false; } } return $result; } /** * Get group option. * * @param string $group Option group name. * @param string $name Option name. * @param mixed $default Default option value. * * @return mixed Option. */ private static function get_grouped_option( $group, $name, $default ) { $options = get_option( self::$grouped_options[ $group ] ); if ( is_array( $options ) && isset( $options[ $name ] ) ) { return $options[ $name ]; } return $default; } /** * Delete grouped option. * * @param string $group Option group name. * @param array $names Option names. * * @return bool Success or failure. */ private static function delete_grouped_option( $group, $names ) { $options = get_option( self::$grouped_options[ $group ], array() ); $to_delete = array_intersect( $names, self::get_option_names( $group ), array_keys( $options ) ); if ( $to_delete ) { foreach ( $to_delete as $name ) { unset( $options[ $name ] ); } return update_option( self::$grouped_options[ $group ], $options ); } return true; } /* * Raw option methods allow Jetpack to get / update / delete options via direct DB queries, including options * that are not created by the Jetpack plugin. This is helpful only in rare cases when we need to bypass * cache and filters. */ /** * Deletes an option via $wpdb query. * * @param string $name Option name. * * @return bool Is the option deleted? */ public static function delete_raw_option( $name ) { if ( self::bypass_raw_option( $name ) ) { return delete_option( $name ); } global $wpdb; $result = $wpdb->query( $wpdb->prepare( "DELETE FROM $wpdb->options WHERE option_name = %s", $name ) ); return $result; } /** * Updates an option via $wpdb query. * * @param string $name Option name. * @param mixed $value Option value. * @param bool $autoload Specifying whether to autoload or not. * * @return bool Is the option updated? */ public static function update_raw_option( $name, $value, $autoload = false ) { if ( self::bypass_raw_option( $name ) ) { return update_option( $name, $value, $autoload ); } global $wpdb; $autoload_value = $autoload ? 'yes' : 'no'; $old_value = $wpdb->get_var( $wpdb->prepare( "SELECT option_value FROM $wpdb->options WHERE option_name = %s LIMIT 1", $name ) ); if ( $old_value === $value ) { return false; } $serialized_value = maybe_serialize( $value ); // below we used "insert ignore" to at least suppress the resulting error. $updated_num = $wpdb->query( $wpdb->prepare( "UPDATE $wpdb->options SET option_value = %s WHERE option_name = %s", $serialized_value, $name ) ); // Try inserting the option if the value doesn't exits. if ( ! $updated_num ) { $updated_num = $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO $wpdb->options ( option_name, option_value, autoload ) VALUES ( %s, %s, %s )", $name, $serialized_value, $autoload_value ) ); } return (bool) $updated_num; } /** * Gets an option via $wpdb query. * * @since 1.1.2 * @since-jetpack 5.4.0 * * @param string $name Option name. * @param mixed $default Default option value if option is not found. * * @return mixed Option value, or null if option is not found and default is not specified. */ public static function get_raw_option( $name, $default = null ) { if ( self::bypass_raw_option( $name ) ) { return get_option( $name, $default ); } global $wpdb; $value = $wpdb->get_var( $wpdb->prepare( "SELECT option_value FROM $wpdb->options WHERE option_name = %s LIMIT 1", $name ) ); $value = maybe_unserialize( $value ); if ( null === $value && null !== $default ) { return $default; } return $value; } /** * This function checks for a constant that, if present, will disable direct DB queries Jetpack uses to manage certain options and force Jetpack to always use Options API instead. * Options can be selectively managed via a blocklist by filtering option names via the jetpack_disabled_raw_option filter. * * @param string $name Option name. * * @return bool */ public static function bypass_raw_option( $name ) { if ( Constants::get_constant( 'JETPACK_DISABLE_RAW_OPTIONS' ) ) { return true; } /** * Allows to disable particular raw options. * * @since 1.1.2 * @since-jetpack 5.5.0 * * @param array $disabled_raw_options An array of option names that you can selectively blocklist from being managed via direct database queries. */ $disabled_raw_options = apply_filters( 'jetpack_disabled_raw_options', array() ); return isset( $disabled_raw_options[ $name ] ); } /** * Gets all known options that are used by Jetpack and managed by Jetpack_Options. * * @since 1.1.2 * @since-jetpack 5.4.0 * * @param boolean $strip_unsafe_options If true, and by default, will strip out options necessary for the connection to WordPress.com. * @return array An array of all options managed via the Jetpack_Options class. */ public static function get_all_jetpack_options( $strip_unsafe_options = true ) { $jetpack_options = self::get_option_names(); $jetpack_options_non_compat = self::get_option_names( 'non_compact' ); $jetpack_options_private = self::get_option_names( 'private' ); $all_jp_options = array_merge( $jetpack_options, $jetpack_options_non_compat, $jetpack_options_private ); if ( $strip_unsafe_options ) { // Flag some Jetpack options as unsafe. $unsafe_options = array( 'id', // (int) The Client ID/WP.com Blog ID of this site. 'master_user', // (int) The local User ID of the user who connected this site to jetpack.wordpress.com. 'version', // (string) Used during upgrade procedure to auto-activate new modules. version:time // non_compact. 'activated', // private. 'register', 'blog_token', // (string) The Client Secret/Blog Token of this site. 'user_token', // (string) The User Token of this site. (deprecated) 'user_tokens', ); // Remove the unsafe Jetpack options. foreach ( $unsafe_options as $unsafe_option ) { $key = array_search( $unsafe_option, $all_jp_options, true ); if ( false !== $key ) { unset( $all_jp_options[ $key ] ); } } } return $all_jp_options; } /** * Get all options that are not managed by the Jetpack_Options class that are used by Jetpack. * * @since 1.1.2 * @since-jetpack 5.4.0 * * @return array */ public static function get_all_wp_options() { // A manual build of the wp options. return array( 'sharing-options', 'disabled_likes', 'disabled_reblogs', 'jetpack_comments_likes_enabled', 'stats_options', 'stats_dashboard_widget', 'safecss_preview_rev', 'safecss_rev', 'safecss_revision_migrated', 'nova_menu_order', 'jetpack_portfolio', 'jetpack_portfolio_posts_per_page', 'jetpack_testimonial', 'jetpack_testimonial_posts_per_page', 'sharedaddy_disable_resources', 'sharing-options', 'sharing-services', 'site_icon_temp_data', 'featured-content', 'site_logo', 'jetpack_dismissed_notices', 'jetpack-twitter-cards-site-tag', 'jetpack-sitemap-state', 'jetpack_sitemap_post_types', 'jetpack_sitemap_location', 'jetpack_protect_key', 'jetpack_protect_blocked_attempts', 'jetpack_protect_activating', 'jetpack_active_plan', 'jetpack_activation_source', 'jetpack_site_products', 'jetpack_sso_match_by_email', 'jetpack_sso_require_two_step', 'jetpack_sso_remove_login_form', 'jetpack_last_connect_url_check', 'jetpack_excluded_extensions', ); } /** * Gets all options that can be safely reset by CLI. * * @since 1.1.2 * @since-jetpack 5.4.0 * * @return array array Associative array containing jp_options which are managed by the Jetpack_Options class and wp_options which are not. */ public static function get_options_for_reset() { $all_jp_options = self::get_all_jetpack_options(); $wp_options = self::get_all_wp_options(); $options = array( 'jp_options' => $all_jp_options, 'wp_options' => $wp_options, ); return $options; } /** * Delete all known options * * @since 1.1.2 * @since-jetpack 5.4.0 * * @return void */ public static function delete_all_known_options() { // Delete all compact options. foreach ( (array) self::$grouped_options as $option_name ) { delete_option( $option_name ); } // Delete all non-compact Jetpack options. foreach ( (array) self::get_option_names( 'non-compact' ) as $option_name ) { self::delete_option( $option_name ); } // Delete all options that can be reset via CLI, that aren't Jetpack options. foreach ( (array) self::get_all_wp_options() as $option_name ) { delete_option( $option_name ); } } } jetpack-connection/src/class-partner.php 0000777 00000011720 15251741406 0014415 0 ustar 00 <?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; } } jetpack-connection/src/class-package-version-tracker.php 0000777 00000013066 15251741406 0017456 0 ustar 00 <?php /** * The Package_Version_Tracker class. * * @package automattic/jetpack-connection */ namespace Automattic\Jetpack\Connection; use Jetpack_Options; /** * The Package_Version_Tracker class. */ class Package_Version_Tracker { const PACKAGE_VERSION_OPTION = 'jetpack_package_versions'; /** * The cache key for storing a failed request to update remote package versions. * The caching logic is that when a failed request occurs, we cache it temporarily * with a set expiration time. * Only after the key has expired, we'll be able to repeat a remote request. * This also implies that the cached value is redundant, however we chose the datetime * of the failed request to avoid using booleans. */ const CACHED_FAILED_REQUEST_KEY = 'jetpack_failed_update_remote_package_versions'; /** * The min time difference in seconds for attempting to * update remote tracked package versions after a failed remote request. */ const CACHED_FAILED_REQUEST_EXPIRATION = 1 * HOUR_IN_SECONDS; /** * Transient key for rate limiting the package version requests; */ const RATE_LIMITER_KEY = 'jetpack_update_remote_package_last_query'; /** * Only allow one versions check (and request) per minute. */ const RATE_LIMITER_TIMEOUT = MINUTE_IN_SECONDS; /** * Uses the jetpack_package_versions filter to obtain the package versions from packages that need * version tracking. If the package versions have changed, updates the option and notifies WPCOM. */ public function maybe_update_package_versions() { // Do not run too early or all the modules may not be loaded. if ( ! did_action( 'init' ) ) { return; } // Only attempt to update the option on POST requests. // This will prevent the option from being updated multiple times due to concurrent requests. if ( ! ( isset( $_SERVER['REQUEST_METHOD'] ) && 'POST' === $_SERVER['REQUEST_METHOD'] ) ) { return; } // The version check is being rate limited. if ( $this->is_rate_limiting() ) { return; } /** * Obtains the package versions. * * @since 1.30.2 * * @param array An associative array of Jetpack package slugs and their corresponding versions as key/value pairs. */ $filter_versions = apply_filters( 'jetpack_package_versions', array() ); if ( ! is_array( $filter_versions ) ) { return; } $option_versions = get_option( self::PACKAGE_VERSION_OPTION, array() ); foreach ( $filter_versions as $package => $version ) { if ( ! is_string( $package ) || ! is_string( $version ) ) { unset( $filter_versions[ $package ] ); } } if ( ! is_array( $option_versions ) || count( array_diff_assoc( $filter_versions, $option_versions ) ) || count( array_diff_assoc( $option_versions, $filter_versions ) ) ) { $this->update_package_versions_option( $filter_versions ); } } /** * Updates the package versions option. * * @param array $package_versions The package versions. */ protected function update_package_versions_option( $package_versions ) { if ( ! $this->is_sync_enabled() ) { $this->update_package_versions_via_remote_request( $package_versions ); // Remove the checksum for package versions, so it gets recalculated when sync gets activated. $jetpack_callables_sync_checksum = Jetpack_Options::get_raw_option( 'jetpack_callables_sync_checksum' ); if ( isset( $jetpack_callables_sync_checksum['jetpack_package_versions'] ) ) { unset( $jetpack_callables_sync_checksum['jetpack_package_versions'] ); Jetpack_Options::update_raw_option( 'jetpack_callables_sync_checksum', $jetpack_callables_sync_checksum ); } return; } update_option( self::PACKAGE_VERSION_OPTION, $package_versions ); } /** * Whether Jetpack Sync is enabled. * * @return boolean true if Sync is present and enabled, false otherwise */ protected function is_sync_enabled() { if ( class_exists( 'Automattic\Jetpack\Sync\Settings' ) && \Automattic\Jetpack\Sync\Settings::is_sync_enabled() ) { return true; } return false; } /** * Fallback for updating the package versions via a remote request when Sync is not present. * * Updates the package versions as follows: * - Sends the updated package versions to wpcom. * - Updates the 'jetpack_package_versions' option. * * @param array $package_versions The package versions. */ protected function update_package_versions_via_remote_request( $package_versions ) { $connection = new Manager(); if ( ! $connection->is_connected() ) { return; } $site_id = \Jetpack_Options::get_option( 'id' ); $last_failed_attempt_within_hour = get_transient( self::CACHED_FAILED_REQUEST_KEY ); if ( $last_failed_attempt_within_hour ) { return; } $body = wp_json_encode( array( 'package_versions' => $package_versions, ) ); $response = Client::wpcom_json_api_request_as_blog( sprintf( '/sites/%d/jetpack-package-versions', $site_id ), '2', array( 'headers' => array( 'content-type' => 'application/json' ), 'method' => 'POST', ), $body, 'wpcom' ); if ( 200 === wp_remote_retrieve_response_code( $response ) ) { update_option( self::PACKAGE_VERSION_OPTION, $package_versions ); } else { set_transient( self::CACHED_FAILED_REQUEST_KEY, time(), self::CACHED_FAILED_REQUEST_EXPIRATION ); } } /** * Check if version check is being rate limited, and update the rate limiting transient if needed. * * @return bool */ private function is_rate_limiting() { if ( get_transient( static::RATE_LIMITER_KEY ) ) { return true; } set_transient( static::RATE_LIMITER_KEY, time(), static::RATE_LIMITER_TIMEOUT ); return false; } } jetpack-connection/src/webhooks/class-authorize-redirect.php 0000777 00000015025 15251741406 0020376 0 ustar 00 <?php /** * Authorize_Redirect Webhook handler class. * * @package automattic/jetpack-connection */ 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; /** * 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. */ public function __construct( $connection ) { $this->connection = $connection; } /** * 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() { add_filter( 'allowed_redirect_hosts', function ( $domains ) { $domains[] = 'jetpack.com'; $domains[] = 'jetpack.wordpress.com'; $domains[] = 'wordpress.com'; // Calypso envs. $domains[] = 'calypso.localhost'; $domains[] = 'wpcalypso.wordpress.com'; $domains[] = 'horizon.wordpress.com'; return array_unique( $domains ); } ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended $dest_url = empty( $_GET['dest_url'] ) ? null : esc_url_raw( wp_unslash( $_GET['dest_url'] ) ); if ( ! $dest_url || ( 0 === stripos( $dest_url, 'https://jetpack.com/' ) && 0 === stripos( $dest_url, 'https://wordpress.com/' ) ) ) { // The destination URL is missing or invalid, nothing to do here. exit( 0 ); } // The user is either already connected, or finished the connection process. if ( $this->connection->is_connected() && $this->connection->is_user_connected() ) { if ( class_exists( '\Automattic\Jetpack\Licensing' ) && method_exists( '\Automattic\Jetpack\Licensing', 'handle_user_connected_redirect' ) ) { Licensing::instance()->handle_user_connected_redirect( $dest_url ); } wp_safe_redirect( $dest_url ); exit( 0 ); } elseif ( ! empty( $_GET['done'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended // The user decided not to proceed with setting up the connection. wp_safe_redirect( Admin_Menu::get_top_level_menu_item_url() ); exit( 0 ); } $redirect_args = array( 'page' => 'jetpack', 'action' => 'authorize_redirect', 'dest_url' => rawurlencode( $dest_url ), 'done' => '1', ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( ! empty( $_GET['from'] ) && 'jetpack_site_only_checkout' === $_GET['from'] ) { $redirect_args['from'] = 'jetpack_site_only_checkout'; } wp_safe_redirect( $this->build_authorize_url( add_query_arg( $redirect_args, admin_url( 'admin.php' ) ) ) ); exit( 0 ); } /** * Create the Jetpack authorization URL. * * @since 2.7.6 Added optional $from and $raw parameters. * @since 6.8.0 Added optional $provider and $provider_args 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, $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, $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' ) ); /** * 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, $raw ); } /** * Filters the redirection URL that is used for connect requests. The redirect * 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. */ public static function filter_connect_redirect_url( $redirect ) { $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; if ( class_exists( 'Jetpack_Network' ) && isset( $_REQUEST['is_multisite'] ) // phpcs:ignore WordPress.Security.NonceVerification.Recommended ) { $redirect = Jetpack_Network::init()->get_url( 'network_admin_page' ); } return $redirect; } /** * Filters the connection URL parameter array. * * @param array $args default URL parameters used by the package. * @return array the modified URL arguments array. */ public static function filter_connect_request_body( $args ) { if ( Constants::is_defined( 'JETPACK__GLOTPRESS_LOCALES_PATH' ) && include_once Constants::get_constant( 'JETPACK__GLOTPRESS_LOCALES_PATH' ) ) { $gp_locale = GP_Locales::by_field( 'wp_locale', get_locale() ); $args['locale'] = isset( $gp_locale ) && isset( $gp_locale->slug ) ? $gp_locale->slug : ''; } $tracking = new Tracking(); $tracks_identity = $tracking->tracks_get_identity( $args['state'] ); $args = array_merge( $args, array( '_ui' => $tracks_identity['_ui'], '_ut' => $tracks_identity['_ut'], ) ); $calypso_env = ( new Host() )->get_calypso_env(); if ( ! empty( $calypso_env ) ) { $args['calypso_env'] = $calypso_env; } return $args; } /** * Return Calypso environment value; used for developing Jetpack and pairing * 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() { _deprecated_function( __METHOD__, '2.7.6', 'Automattic\\Jetpack\\Status\\Host::get_calypso_env' ); return ( new Host() )->get_calypso_env(); } } jetpack-connection/src/class-tokens.php 0000777 00000051476 15251741406 0014261 0 ustar 00 <?php /** * The Jetpack Connection Tokens class file. * * @package automattic/jetpack-connection */ namespace Automattic\Jetpack\Connection; use Automattic\Jetpack\Constants; use Automattic\Jetpack\Roles; use DateInterval; use DateTime; use Exception; use Jetpack_Options; use WP_Error; /** * The Jetpack Connection Tokens class that manages tokens. */ class Tokens { const MAGIC_NORMAL_TOKEN_KEY = ';normal;'; /** * Datetime format. */ const DATE_FORMAT_ATOM = 'Y-m-d\TH:i:sP'; /** * Deletes all connection tokens and transients from the local Jetpack site. */ public function delete_all() { Jetpack_Options::delete_option( array( 'blog_token', 'user_token', 'user_tokens', ) ); $this->remove_lock(); } /** * Perform the API request to validate the blog and user tokens. * * @param int|null $user_id ID of the user we need to validate token for. Current user's ID by default. * * @return array|false|WP_Error The API response: `array( 'blog_token_is_healthy' => true|false, 'user_token_is_healthy' => true|false )`. */ public function validate( $user_id = null ) { $blog_id = Jetpack_Options::get_option( 'id' ); if ( ! $blog_id ) { return new WP_Error( 'site_not_registered', 'Site not registered.' ); } $url = sprintf( '%s/%s/v%s/%s', Constants::get_constant( 'JETPACK__WPCOM_JSON_API_BASE' ), 'wpcom', '2', 'sites/' . $blog_id . '/jetpack-token-health' ); $user_token = $this->get_access_token( $user_id ? $user_id : get_current_user_id() ); $blog_token = $this->get_access_token(); // Cannot validate non-existent tokens. if ( false === $user_token || false === $blog_token ) { return false; } $method = 'POST'; $body = array( 'user_token' => $this->get_signed_token( $user_token ), 'blog_token' => $this->get_signed_token( $blog_token ), ); $response = Client::_wp_remote_request( $url, compact( 'body', 'method' ) ); if ( is_wp_error( $response ) || ! wp_remote_retrieve_body( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) { return false; } $body = json_decode( wp_remote_retrieve_body( $response ), true ); return $body ? $body : false; } /** * Perform the API request to validate only the blog. * * @return bool|WP_Error Boolean with the test result. WP_Error if test cannot be performed. */ public function validate_blog_token() { $blog_id = Jetpack_Options::get_option( 'id' ); if ( ! $blog_id ) { return new WP_Error( 'site_not_registered', 'Site not registered.' ); } $url = sprintf( '%s/%s/v%s/%s', Constants::get_constant( 'JETPACK__WPCOM_JSON_API_BASE' ), 'wpcom', '2', 'sites/' . $blog_id . '/jetpack-token-health/blog' ); $method = 'GET'; $response = Client::remote_request( compact( 'url', 'method' ) ); if ( is_wp_error( $response ) || ! wp_remote_retrieve_body( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) { return false; } $body = json_decode( wp_remote_retrieve_body( $response ), true ); return is_array( $body ) && isset( $body['is_healthy'] ) && true === $body['is_healthy']; } /** * Obtains the auth token. * * @param array $data The request data. * @param string $token_api_url The URL of the Jetpack "token" API. * @return object|WP_Error Returns the auth token on success. * Returns a WP_Error on failure. */ public function get( $data, $token_api_url ) { $roles = new Roles(); $role = $roles->translate_current_user_to_role(); if ( ! $role ) { return new WP_Error( 'role', __( 'An administrator for this blog must set up the Jetpack connection.', 'jetpack-connection' ) ); } $client_secret = $this->get_access_token(); if ( ! $client_secret ) { return new WP_Error( 'client_secret', __( 'You need to register your Jetpack before connecting it.', 'jetpack-connection' ) ); } /** * Filter the URL of the first time the user gets redirected back to your site for connection * data processing. * * @since 1.7.0 * @since-jetpack 8.0.0 * * @param string $redirect_url Defaults to the site admin URL. */ $processing_url = apply_filters( 'jetpack_token_processing_url', admin_url( 'admin.php' ) ); $redirect = isset( $data['redirect'] ) ? esc_url_raw( (string) $data['redirect'] ) : ''; /** * Filter the URL to redirect the user back to when the authentication process * is complete. * * @since 1.7.0 * @since-jetpack 8.0.0 * * @param string $redirect_url Defaults to the site URL. */ $redirect = apply_filters( 'jetpack_token_redirect_url', $redirect ); $redirect_uri = ( 'calypso' === $data['auth_type'] ) ? $data['redirect_uri'] : add_query_arg( array( 'handler' => 'jetpack-connection-webhooks', 'action' => 'authorize', '_wpnonce' => wp_create_nonce( "jetpack-authorize_{$role}_{$redirect}" ), 'redirect' => $redirect ? rawurlencode( $redirect ) : false, ), esc_url( $processing_url ) ); /** * Filters the token request data. * * @since 1.7.0 * @since-jetpack 8.0.0 * * @param array $request_data request data. */ $body = apply_filters( 'jetpack_token_request_body', array( 'client_id' => Jetpack_Options::get_option( 'id' ), 'client_secret' => $client_secret->secret, 'grant_type' => 'authorization_code', 'code' => $data['code'], 'redirect_uri' => $redirect_uri, ) ); $args = array( 'method' => 'POST', 'body' => $body, 'headers' => array( 'Accept' => 'application/json', ), ); add_filter( 'http_request_timeout', array( $this, 'return_30' ), PHP_INT_MAX - 1 ); $response = Client::_wp_remote_request( $token_api_url, $args ); remove_filter( 'http_request_timeout', array( $this, 'return_30' ), PHP_INT_MAX - 1 ); if ( is_wp_error( $response ) ) { return new WP_Error( 'token_http_request_failed', $response->get_error_message() ); } $code = wp_remote_retrieve_response_code( $response ); $entity = wp_remote_retrieve_body( $response ); if ( $entity ) { $json = json_decode( $entity ); } else { $json = false; } if ( 200 !== $code || ! empty( $json->error ) ) { if ( empty( $json->error ) ) { return new WP_Error( 'unknown', '', $code ); } /* translators: Error description string. */ $error_description = isset( $json->error_description ) ? sprintf( __( 'Error Details: %s', 'jetpack-connection' ), (string) $json->error_description ) : ''; return new WP_Error( (string) $json->error, $error_description, $code ); } if ( empty( $json->access_token ) || ! is_scalar( $json->access_token ) ) { return new WP_Error( 'access_token', '', $code ); } if ( empty( $json->token_type ) || 'X_JETPACK' !== strtoupper( $json->token_type ) ) { return new WP_Error( 'token_type', '', $code ); } if ( empty( $json->scope ) ) { return new WP_Error( 'scope', 'No Scope', $code ); } // TODO: get rid of the error silencer. // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged @list( $role, $hmac ) = explode( ':', $json->scope ); if ( empty( $role ) || empty( $hmac ) ) { return new WP_Error( 'scope', 'Malformed Scope', $code ); } if ( $this->sign_role( $role ) !== $json->scope ) { return new WP_Error( 'scope', 'Invalid Scope', $code ); } $cap = $roles->translate_role_to_cap( $role ); if ( ! $cap ) { return new WP_Error( 'scope', 'No Cap', $code ); } if ( ! current_user_can( $cap ) ) { return new WP_Error( 'scope', 'current_user_cannot', $code ); } return (string) $json->access_token; } /** * Enters a user token into the user_tokens option * * @param int $user_id The user id. * @param string $token The user token. * @param bool $is_master_user Whether the user is the master user. * @return bool */ public function update_user_token( $user_id, $token, $is_master_user ) { // Not designed for concurrent updates. $user_tokens = $this->get_user_tokens(); if ( ! is_array( $user_tokens ) ) { $user_tokens = array(); } $user_tokens[ $user_id ] = $token; if ( $is_master_user ) { $master_user = $user_id; $options = compact( 'user_tokens', 'master_user' ); } else { $options = compact( 'user_tokens' ); } return Jetpack_Options::update_options( $options ); } /** * Sign a user role with the master access token. * If not specified, will default to the current user. * * @access public * * @param string $role User role. * @param int $user_id ID of the user. * @return string Signed user role. */ public function sign_role( $role, $user_id = null ) { if ( empty( $user_id ) ) { $user_id = (int) get_current_user_id(); } if ( ! $user_id ) { return false; } $token = $this->get_access_token(); if ( ! $token || is_wp_error( $token ) ) { return false; } return $role . ':' . hash_hmac( 'md5', "{$role}|{$user_id}", $token->secret ); } /** * Increases the request timeout value to 30 seconds. * * @return int Returns 30. */ public function return_30() { return 30; } /** * Gets the requested token. * * Tokens are one of two types: * 1. Blog Tokens: These are the "main" tokens. Each site typically has one Blog Token, * though some sites can have multiple "Special" Blog Tokens (see below). These tokens * are not associated with a user account. They represent the site's connection with * the Jetpack servers. * 2. User Tokens: These are "sub-"tokens. Each connected user account has one User Token. * * All tokens look like "{$token_key}.{$private}". $token_key is a public ID for the * token, and $private is a secret that should never be displayed anywhere or sent * over the network; it's used only for signing things. * * Blog Tokens can be "Normal" or "Special". * * Normal: The result of a normal connection flow. They look like * "{$random_string_1}.{$random_string_2}" * That is, $token_key and $private are both random strings. * Sites only have one Normal Blog Token. Normal Tokens are found in either * Jetpack_Options::get_option( 'blog_token' ) (usual) or the JETPACK_BLOG_TOKEN * constant (rare). * * Special: A connection token for sites that have gone through an alternative * connection flow. They look like: * ";{$special_id}{$special_version};{$wpcom_blog_id};.{$random_string}" * That is, $private is a random string and $token_key has a special structure with * lots of semicolons. * Most sites have zero Special Blog Tokens. Special tokens are only found in the * JETPACK_BLOG_TOKEN constant. * * In particular, note that Normal Blog Tokens never start with ";" and that * Special Blog Tokens always do. * * When searching for a matching Blog Tokens, Blog Tokens are examined in the following * order: * 1. Defined Special Blog Tokens (via the JETPACK_BLOG_TOKEN constant) * 2. Stored Normal Tokens (via Jetpack_Options::get_option( 'blog_token' )) * 3. Defined Normal Tokens (via the JETPACK_BLOG_TOKEN constant) * * @param int|false $user_id false: Return the Blog Token. int: Return that user's User Token. * @param string|false $token_key If provided, check that the token matches the provided input. * @param bool|true $suppress_errors If true, return a falsy value when the token isn't found; When false, return a descriptive WP_Error when the token isn't found. * * @return object|false|WP_Error */ public function get_access_token( $user_id = false, $token_key = false, $suppress_errors = true ) { if ( $this->is_locked() ) { $this->delete_all(); return false; } $possible_special_tokens = array(); $possible_normal_tokens = array(); $user_tokens = $this->get_user_tokens(); if ( $user_id ) { if ( ! $user_tokens ) { return $suppress_errors ? false : new WP_Error( 'no_user_tokens', __( 'No user tokens found', 'jetpack-connection' ) ); } if ( true === $user_id ) { // connection owner. $user_id = Jetpack_Options::get_option( 'master_user' ); if ( ! $user_id ) { return $suppress_errors ? false : new WP_Error( 'empty_master_user_option', __( 'No primary user defined', 'jetpack-connection' ) ); } } if ( ! isset( $user_tokens[ $user_id ] ) || ! $user_tokens[ $user_id ] ) { // translators: %s is the user ID. return $suppress_errors ? false : new WP_Error( 'no_token_for_user', sprintf( __( 'No token for user %d', 'jetpack-connection' ), $user_id ) ); } $user_token_chunks = explode( '.', $user_tokens[ $user_id ] ); if ( empty( $user_token_chunks[1] ) || empty( $user_token_chunks[2] ) ) { // translators: %s is the user ID. return $suppress_errors ? false : new WP_Error( 'token_malformed', sprintf( __( 'Token for user %d is malformed', 'jetpack-connection' ), $user_id ) ); } if ( $user_token_chunks[2] !== (string) $user_id ) { // translators: %1$d is the ID of the requested user. %2$d is the user ID found in the token. return $suppress_errors ? false : new WP_Error( 'user_id_mismatch', sprintf( __( 'Requesting user_id %1$d does not match token user_id %2$d', 'jetpack-connection' ), $user_id, $user_token_chunks[2] ) ); } $possible_normal_tokens[] = "{$user_token_chunks[0]}.{$user_token_chunks[1]}"; } else { $stored_blog_token = Jetpack_Options::get_option( 'blog_token' ); if ( $stored_blog_token ) { $possible_normal_tokens[] = $stored_blog_token; } $defined_tokens_string = Constants::get_constant( 'JETPACK_BLOG_TOKEN' ); if ( $defined_tokens_string ) { $defined_tokens = explode( ',', $defined_tokens_string ); foreach ( $defined_tokens as $defined_token ) { if ( ';' === $defined_token[0] ) { $possible_special_tokens[] = $defined_token; } else { $possible_normal_tokens[] = $defined_token; } } } } if ( self::MAGIC_NORMAL_TOKEN_KEY === $token_key ) { $possible_tokens = $possible_normal_tokens; } else { $possible_tokens = array_merge( $possible_special_tokens, $possible_normal_tokens ); } if ( ! $possible_tokens ) { // If no user tokens were found, it would have failed earlier, so this is about blog token. return $suppress_errors ? false : new WP_Error( 'no_possible_tokens', __( 'No blog token found', 'jetpack-connection' ) ); } $valid_token = false; if ( false === $token_key ) { // Use first token. $valid_token = $possible_tokens[0]; } elseif ( self::MAGIC_NORMAL_TOKEN_KEY === $token_key ) { // Use first normal token. $valid_token = $possible_tokens[0]; // $possible_tokens only contains normal tokens because of earlier check. } else { // Use the token matching $token_key or false if none. // Ensure we check the full key. $token_check = rtrim( $token_key, '.' ) . '.'; foreach ( $possible_tokens as $possible_token ) { if ( hash_equals( substr( $possible_token, 0, strlen( $token_check ) ), $token_check ) ) { $valid_token = $possible_token; break; } } } if ( ! $valid_token ) { if ( $user_id ) { // translators: %d is the user ID. return $suppress_errors ? false : new WP_Error( 'no_valid_user_token', sprintf( __( 'Invalid token for user %d', 'jetpack-connection' ), $user_id ) ); } else { return $suppress_errors ? false : new WP_Error( 'no_valid_blog_token', __( 'Invalid blog token', 'jetpack-connection' ) ); } } return (object) array( 'secret' => $valid_token, 'external_user_id' => (int) $user_id, ); } /** * Updates the blog token to a new value. * * @access public * * @param string $token the new blog token value. * @return Boolean Whether updating the blog token was successful. */ public function update_blog_token( $token ) { return Jetpack_Options::update_option( 'blog_token', $token ); } /** * Unlinks the current user from the linked WordPress.com user. * * @access public * @static * * @todo Refactor to properly load the XMLRPC client independently. * * @param int $user_id The user identifier. * * @return bool Whether the disconnection of the user was successful. */ public function disconnect_user( $user_id ) { $tokens = $this->get_user_tokens(); if ( ! $tokens ) { return false; } if ( ! isset( $tokens[ $user_id ] ) ) { return false; } unset( $tokens[ $user_id ] ); $this->update_user_tokens( $tokens ); return true; } /** * Returns an array of user_id's that have user tokens for communicating with wpcom. * Able to select by specific capability. * * @deprecated 1.30.0 * @see Manager::get_connected_users * * @param string $capability The capability of the user. * @param int|null $limit How many connected users to get before returning. * @return array Array of WP_User objects if found. */ public function get_connected_users( $capability = 'any', $limit = null ) { _deprecated_function( __METHOD__, '1.30.0' ); return ( new Manager( 'jetpack' ) )->get_connected_users( $capability, $limit ); } /** * Fetches a signed token. * * @param object $token the token. * @return WP_Error|string a signed token */ public function get_signed_token( $token ) { if ( ! isset( $token->secret ) || empty( $token->secret ) ) { return new WP_Error( 'invalid_token' ); } list( $token_key, $token_secret ) = explode( '.', $token->secret ); $token_key = sprintf( '%s:%d:%d', $token_key, Constants::get_constant( 'JETPACK__API_VERSION' ), $token->external_user_id ); $timestamp = time(); if ( function_exists( 'wp_generate_password' ) ) { $nonce = wp_generate_password( 10, false ); } else { $nonce = substr( sha1( (string) wp_rand( 0, 1000000 ) ), 0, 10 ); } $normalized_request_string = implode( "\n", array( $token_key, $timestamp, $nonce, ) ) . "\n"; // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode $signature = base64_encode( hash_hmac( 'sha1', $normalized_request_string, $token_secret, true ) ); $auth = array( 'token' => $token_key, 'timestamp' => $timestamp, 'nonce' => $nonce, 'signature' => $signature, ); $header_pieces = array(); foreach ( $auth as $key => $value ) { $header_pieces[] = sprintf( '%s="%s"', $key, $value ); } return implode( ' ', $header_pieces ); } /** * Gets the list of user tokens * * @since 1.30.0 * * @return bool|array An array of user tokens where keys are user IDs and values are the tokens. False if no user token is found. */ public function get_user_tokens() { return Jetpack_Options::get_option( 'user_tokens' ); } /** * Updates the option that stores the user tokens * * @since 1.30.0 * * @param array $tokens An array of user tokens where keys are user IDs and values are the tokens. * @return bool Was the option successfully updated? * * @todo add validate the input. */ public function update_user_tokens( $tokens ) { return Jetpack_Options::update_option( 'user_tokens', $tokens ); } /** * Lock the tokens to the current site URL. * * @param int $timespan How long the tokens should be locked, in seconds. * * @return bool */ public function set_lock( $timespan = HOUR_IN_SECONDS ) { try { $expires = ( new DateTime() )->add( DateInterval::createFromDateString( (int) $timespan . ' seconds' ) ); } catch ( Exception $e ) { return false; } // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode return Jetpack_Options::update_option( 'token_lock', $expires->format( static::DATE_FORMAT_ATOM ) . '|||' . base64_encode( Urls::site_url() ) ); } /** * Remove the site lock from tokens. * * @return bool */ public function remove_lock() { Jetpack_Options::delete_option( 'token_lock' ); return true; } /** * Check if the domain is locked, remove the lock if needed. * Possible scenarios: * - lock expired, site URL matches the lock URL: remove the lock, return false. * - lock not expired, site URL matches the lock URL: return false. * - site URL does not match the lock URL (expiration date is ignored): return true, do not remove the lock. * * @return bool */ public function is_locked() { $the_lock = Jetpack_Options::get_option( 'token_lock' ); if ( ! $the_lock ) { // Not locked. return false; } $the_lock = explode( '|||', $the_lock, 2 ); if ( count( $the_lock ) !== 2 ) { // Something's wrong with the lock. $this->remove_lock(); return false; } // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode $locked_site_url = base64_decode( $the_lock[1] ); $expires = $the_lock[0]; $expiration_date = DateTime::createFromFormat( static::DATE_FORMAT_ATOM, $expires ); if ( false === $expiration_date || ! $locked_site_url ) { // Something's wrong with the lock. $this->remove_lock(); return false; } if ( Urls::site_url() === $locked_site_url ) { if ( new DateTime() > $expiration_date ) { // Site lock expired. // Site URL matches, removing the lock. $this->remove_lock(); } return false; } // Site URL doesn't match. return true; } } jetpack-connection/src/class-xmlrpc-async-call.php 0000777 00000005175 15251741406 0016302 0 ustar 00 <?php /** * XMLRPC Async Call class. * * @package automattic/jetpack-connection */ namespace Automattic\Jetpack\Connection; use Jetpack_IXR_ClientMulticall; /** * Make XMLRPC async calls to WordPress.com * * This class allows you to enqueue XMLRPC calls that will be grouped and sent * at once in a multi-call request at shutdown. * * Usage: * * XMLRPC_Async_Call::add_call( 'methodName', get_current_user_id(), $arg1, $arg2, etc... ) * * See XMLRPC_Async_Call::add_call for details */ class XMLRPC_Async_Call { /** * Hold the IXR Clients that will be dispatched at shutdown * * Clients are stored in the following schema: * [ * $blog_id => [ * $user_id => [ * arrat of Jetpack_IXR_ClientMulticall * ] * ] * ] * * @var array */ public static $clients = array(); /** * Adds a new XMLRPC call to the queue to be processed on shutdown * * @param string $method The XML-RPC method. * @param integer $user_id The user ID used to make the request (will use this user's token); Use 0 for the blog token. * @param mixed ...$args This function accepts any number of additional arguments, that will be passed to the call. * @return void */ public static function add_call( $method, $user_id = 0, ...$args ) { global $blog_id; $client_blog_id = is_multisite() ? $blog_id : 0; if ( ! isset( self::$clients[ $client_blog_id ] ) ) { self::$clients[ $client_blog_id ] = array(); } if ( ! isset( self::$clients[ $client_blog_id ][ $user_id ] ) ) { self::$clients[ $client_blog_id ][ $user_id ] = new Jetpack_IXR_ClientMulticall( array( 'user_id' => $user_id ) ); } // https://plugins.trac.wordpress.org/ticket/2041 if ( function_exists( 'ignore_user_abort' ) ) { ignore_user_abort( true ); } array_unshift( $args, $method ); call_user_func_array( array( self::$clients[ $client_blog_id ][ $user_id ], 'addCall' ), $args ); if ( false === has_action( 'shutdown', array( 'Automattic\Jetpack\Connection\XMLRPC_Async_Call', 'do_calls' ) ) ) { add_action( 'shutdown', array( 'Automattic\Jetpack\Connection\XMLRPC_Async_Call', 'do_calls' ) ); } } /** * Trigger the calls at shutdown * * @return void */ public static function do_calls() { foreach ( self::$clients as $client_blog_id => $blog_clients ) { if ( $client_blog_id > 0 ) { $switch_success = switch_to_blog( $client_blog_id ); if ( ! $switch_success ) { continue; } } foreach ( $blog_clients as $client ) { if ( empty( $client->calls ) ) { continue; } flush(); $client->query(); } if ( $client_blog_id > 0 ) { restore_current_blog(); } } } } jetpack-connection/src/class-utils.php 0000777 00000007522 15251741406 0014107 0 ustar 00 <?php /** * The Jetpack Connection package Utils class file. * * @package automattic/jetpack-connection */ namespace Automattic\Jetpack\Connection; use Automattic\Jetpack\Tracking; /** * Provides utility methods for the Connection package. */ class Utils { const DEFAULT_JETPACK__API_VERSION = 1; const DEFAULT_JETPACK__API_BASE = 'https://jetpack.wordpress.com/jetpack.'; const DEFAULT_JETPACK__WPCOM_JSON_API_BASE = 'https://public-api.wordpress.com'; /** * Enters a user token into the user_tokens option * * @deprecated 1.24.0 Use Automattic\Jetpack\Connection\Tokens->update_user_token() instead. * * @param int $user_id The user id. * @param string $token The user token. * @param bool $is_master_user Whether the user is the master user. * @return bool */ public static function update_user_token( $user_id, $token, $is_master_user ) { _deprecated_function( __METHOD__, '1.24.0', 'Automattic\\Jetpack\\Connection\\Tokens->update_user_token' ); return ( new Tokens() )->update_user_token( $user_id, $token, $is_master_user ); } /** * Filters the value of the api constant. * * @param String $constant_value The constant value. * @param String $constant_name The constant name. * @return mixed | null */ public static function jetpack_api_constant_filter( $constant_value, $constant_name ) { if ( $constant_value !== null ) { // If the constant value was already set elsewhere, use that value. return $constant_value; } if ( defined( "self::DEFAULT_$constant_name" ) ) { return constant( "self::DEFAULT_$constant_name" ); } return null; } /** * Add a filter to initialize default values of the constants. */ public static function init_default_constants() { add_filter( 'jetpack_constant_default_value', array( __CLASS__, 'jetpack_api_constant_filter' ), 10, 2 ); } /** * Filters the registration request body to include tracking properties. * * @param array $properties Already prepared tracking properties. * @return array amended properties. */ public static function filter_register_request_body( $properties ) { $tracking = new Tracking(); $tracks_identity = $tracking->tracks_get_identity( get_current_user_id() ); return array_merge( $properties, array( '_ui' => $tracks_identity['_ui'], '_ut' => $tracks_identity['_ut'], ) ); } /** * Generate a new user from a SSO attempt. * * @param object $user_data WordPress.com user information. */ public static function generate_user( $user_data ) { $username = $user_data->login; /** * Determines how many times the SSO module can attempt to randomly generate a user. * * @module sso * * @since jetpack-4.3.2 * * @param int 5 By default, SSO will attempt to random generate a user up to 5 times. */ $num_tries = (int) apply_filters( 'jetpack_sso_allowed_username_generate_retries', 5 ); $exists = username_exists( $username ); $tries = 0; while ( $exists && $tries++ < $num_tries ) { $username = $user_data->login . '_' . $user_data->ID . '_' . wp_rand(); $exists = username_exists( $username ); } if ( $exists ) { return false; } $user = (object) array(); $user->user_pass = wp_generate_password( 20 ); $user->user_login = wp_slash( $username ); $user->user_email = wp_slash( $user_data->email ); $user->display_name = $user_data->display_name; $user->first_name = $user_data->first_name; $user->last_name = $user_data->last_name; $user->url = $user_data->url; $user->description = $user_data->description; if ( isset( $user_data->role ) && $user_data->role ) { $user->role = $user_data->role; } $created_user_id = wp_insert_user( $user ); update_user_meta( $created_user_id, 'wpcom_user_id', $user_data->ID ); return get_userdata( $created_user_id ); } } jetpack-connection/src/class-external-storage.php 0000777 00000022251 15251741406 0016227 0 ustar 00 <?php /** * External Storage utilities for Jetpack Connection. * * Provides centralized logic for external storage implementations * across different environments (WoA, VIP, other). * * Usage Example: * * // 1. Create a storage provider class implementing the interface: * class My_Storage_Provider implements Storage_Provider_Interface { * public function is_available() { return true; } * public function should_handle( $option_name ) { * return in_array( $option_name, array( 'blog_token', 'id' ), true ); * } * public function get( $option_name ) { * // Return value from your external storage or null * } * public function get_environment_id() { return 'my_env'; } * } * * // 2. Register the provider: * if ( class_exists( 'Automattic\Jetpack\Connection\External_Storage' ) ) { * \Automattic\Jetpack\Connection\External_Storage::register_provider( new My_Storage_Provider() ); * } * * // 3. External storage is now automatically used by Jetpack_Options::get_option() * * @package automattic/jetpack-connection */ namespace Automattic\Jetpack\Connection; require_once __DIR__ . '/interface-storage-provider.php'; /** * External Storage utilities class. * * @since 6.18.0 */ class External_Storage { /** * Registered storage provider. * * @since 6.18.0 * * @var Storage_Provider_Interface|null */ private static $provider = null; /** * Register a storage provider for external storage. * * @since 6.18.0 * * @param Storage_Provider_Interface $provider Storage provider implementing the interface. * @return bool True if provider was registered successfully, false otherwise. */ public static function register_provider( Storage_Provider_Interface $provider ) { self::$provider = $provider; return true; } /** * Get value from external storage provider. * * Returns null if no provider is registered or if the provider can't provide the value (triggers database fallback). * * @since 6.18.0 * * @param string $key The key to retrieve. * @return mixed The value from external storage, or null for database fallback. */ public static function get_value( $key ) { $provider = self::$provider; // Check if we have a registered provider if ( null === $provider ) { return null; // No provider registered, use database } // Get environment ID from provider $environment = method_exists( $provider, 'get_environment_id' ) ? $provider->get_environment_id() : 'unknown'; // Check if provider is available in current environment if ( ! $provider->is_available() ) { self::log_event( 'unavailable', $key, 'External storage not available', $environment ); return null; } // Check if provider should handle this option if ( ! $provider->should_handle( $key ) ) { return null; } // Try to get value from the provider try { $value = $provider->get( $key ); // Check if we got a valid value if ( null !== $value && false !== $value && '' !== $value && 0 !== $value ) { return $value; } // Empty value - log it self::log_event( 'empty', $key, '', $environment ); } catch ( \Exception $e ) { // Provider threw an exception self::log_event( 'error', $key, $e->getMessage(), $environment ); } // Provider couldn't provide value, return null for database fallback return null; } /** * Log events if WP_DEBUG is enabled. * Report external storage events through Jetpack Connection Error_Handler. * Includes rate limiting to prevent log spam from noisy events. * * @since 6.18.0 * * @param string $event_type The event type (error, empty, unavailable). * @param string $key The key that triggered the event. * @param string $details Additional details about the event. * @param string $environment The environment identifier (atomic, vip, etc.). */ public static function log_event( $event_type, $key, $details = '', $environment = 'unknown' ) { // Apply rate limiting to prevent log spam if ( ! self::should_log_event( $key ) ) { return; } // Local debug logging (only when WP_DEBUG is enabled) if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { error_log( // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log sprintf( 'Jetpack External Storage %s: %s in %s%s', $event_type, $key, $environment, $details ? ' - ' . $details : '' ) ); } // Only report 'error' and 'empty' events to WordPress.com if ( 'error' !== $event_type && 'empty' !== $event_type ) { return; } $should_report_remote = false; if ( 'error' === $event_type ) { // Report external storage errors for supported environments $should_report_remote = self::should_report_for_environment( $key ); } elseif ( 'empty' === $event_type ) { // Use delay mechanism to distinguish disconnection from a delay $should_report_remote = self::should_report_for_environment( $key ) && self::should_report_empty_state( $key ); } if ( ! $should_report_remote || ! class_exists( 'Automattic\Jetpack\Connection\Error_Handler' ) ) { return; } // Create and report error $error_code = 'external_storage_' . $event_type; $error_message = sprintf( 'External storage %s for key "%s"%s', str_replace( '_', ' ', $event_type ), $key, $details ? ': ' . $details : '' ); $error_data = array( 'key' => $key, 'event_type' => $event_type, 'details' => $details, 'environment' => $environment, 'timestamp' => time(), 'site_url' => home_url(), ); $error = new \WP_Error( $error_code, $error_message, $error_data ); Error_Handler::get_instance()->report_error( $error, false, true ); } /** * Determine if the current environment should report external storage errors to WordPress.com. * Allows providers to control remote error reporting per-option via optional should_report_errors_for() method. * * @since 6.18.0 * * @param string $key The option key being accessed. * @return bool True if this environment should report external storage errors to WordPress.com. */ private static function should_report_for_environment( $key = '' ) { $provider = self::$provider; // Check if provider implements per-option reporting (optional method not defined in interface). // Providers can optionally implement: public function should_report_errors_for( $option_name ) if ( null !== $provider && method_exists( $provider, 'should_report_errors_for' ) && ! empty( $key ) ) { // @phan-suppress-next-line PhanUndeclaredMethodInCallable - Optional method, checked via method_exists() return call_user_func( array( $provider, 'should_report_errors_for' ), $key ); } // Deprecated: JETPACK_EXTERNAL_STORAGE_REPORTING_ENABLED constant // @deprecated 6.18.13 Use should_report_errors_for() method in your Storage Provider instead. if ( defined( 'JETPACK_EXTERNAL_STORAGE_REPORTING_ENABLED' ) ) { if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error trigger_error( 'JETPACK_EXTERNAL_STORAGE_REPORTING_ENABLED constant is deprecated. Implement should_report_errors_for() method in your Storage Provider instead.', E_USER_DEPRECATED ); } return (bool) constant( 'JETPACK_EXTERNAL_STORAGE_REPORTING_ENABLED' ); } return false; } /** * Determine if we should report an empty state based on delay mechanism. * We need this due to delays in writing in external storage vs writing into the database. * On first encounter of empty state, sets a transient. On subsequent encounters * after 10 minutes, allows reporting (indicating likely disconnection, not sync delay). * * @since 6.18.0 * * @param string $key The key that was empty. * @return bool True if we should report this empty state, false otherwise. */ private static function should_report_empty_state( $key ) { $delay_key = 'jetpack_external_storage_empty_delay_' . $key; $first_empty_time = get_transient( $delay_key ); if ( false === $first_empty_time ) { // First time encountering empty state - set delay transient and don't report yet set_transient( $delay_key, time(), 15 * MINUTE_IN_SECONDS ); // Keep for 15 minutes return false; } // Check if 10 minutes have passed since first empty encounter $delay_threshold = 10 * MINUTE_IN_SECONDS; if ( ( time() - $first_empty_time ) >= $delay_threshold ) { // 10+ minutes of empty state - likely disconnection, report it delete_transient( $delay_key ); return true; } return false; } /** * Determine if an event should be logged based on rate limiting rules. * * This prevents log spam from noisy events by applying a simple one-hour * rate limit per key, regardless of event type. * * @since 6.18.0 * * @param string $key The key that triggered the event. * @return bool True if the event should be logged, false if rate limited. */ private static function should_log_event( $key ) { $rate_limit_key = 'jetpack_ext_storage_rate_limit_' . $key; // Check if we're still within the rate limit period if ( get_transient( $rate_limit_key ) ) { return false; } set_transient( $rate_limit_key, true, HOUR_IN_SECONDS ); return true; } } jetpack-connection/src/interface-manager.php 0000777 00000000452 15251741406 0015207 0 ustar 00 <?php /** * The Jetpack Connection Interface file. * No longer used. * * @package automattic/jetpack-connection */ namespace Automattic\Jetpack\Connection; /** * This interface is no longer used and is now deprecated. * * @deprecated since jetpack 7.8 */ interface Manager_Interface { } jetpack-connection/src/class-nonce-handler.php 0000777 00000013264 15251741406 0015464 0 ustar 00 <?php /** * The nonce handler. * * @package automattic/jetpack-connection */ namespace Automattic\Jetpack\Connection; /** * The nonce handler. */ class Nonce_Handler { /** * How long the scheduled cleanup can run (in seconds). * Can be modified using the filter `jetpack_connection_nonce_scheduled_cleanup_limit`. */ const SCHEDULED_CLEANUP_TIME_LIMIT = 5; /** * How many nonces should be removed per batch during the `clean_all()` run. */ const CLEAN_ALL_LIMIT_PER_BATCH = 1000; /** * Nonce lifetime in seconds. */ const LIFETIME = HOUR_IN_SECONDS; /** * The nonces used during the request are stored here to keep them valid. * The property is static to keep the nonces accessible between the `Nonce_Handler` instances. * * @var array */ private static $nonces_used_this_request = array(); /** * The database object. * * @var \wpdb */ private $db; /** * Initializing the object. */ public function __construct() { global $wpdb; $this->db = $wpdb; } /** * Scheduling the WP-cron cleanup event. */ public function init_schedule() { add_action( 'jetpack_clean_nonces', array( __CLASS__, 'clean_scheduled' ) ); if ( ! wp_next_scheduled( 'jetpack_clean_nonces' ) ) { wp_schedule_event( time(), 'hourly', 'jetpack_clean_nonces' ); } } /** * Reschedule the WP-cron cleanup event to make it start sooner. */ public function reschedule() { wp_clear_scheduled_hook( 'jetpack_clean_nonces' ); wp_schedule_event( time(), 'hourly', 'jetpack_clean_nonces' ); } /** * Adds a used nonce to a list of known nonces. * * @param int $timestamp the current request timestamp. * @param string $nonce the nonce value. * * @return bool whether the nonce is unique or not. */ public function add( $timestamp, $nonce ) { if ( isset( static::$nonces_used_this_request[ "$timestamp:$nonce" ] ) ) { return static::$nonces_used_this_request[ "$timestamp:$nonce" ]; } // This should always have gone through Jetpack_Signature::sign_request() first to check $timestamp and $nonce. $timestamp = (int) $timestamp; $nonce = esc_sql( $nonce ); // Raw query so we can avoid races: add_option will also update. $show_errors = $this->db->hide_errors(); // Running `try...finally` to make sure that we re-enable errors in case of an exception. try { $old_nonce = $this->db->get_row( $this->db->prepare( "SELECT 1 FROM `{$this->db->options}` WHERE option_name = %s", "jetpack_nonce_{$timestamp}_{$nonce}" ) ); if ( $old_nonce === null ) { $return = (bool) $this->db->query( $this->db->prepare( "INSERT INTO `{$this->db->options}` (`option_name`, `option_value`, `autoload`) VALUES (%s, %s, %s)", "jetpack_nonce_{$timestamp}_{$nonce}", time(), 'no' ) ); } else { $return = false; } } finally { $this->db->show_errors( $show_errors ); } static::$nonces_used_this_request[ "$timestamp:$nonce" ] = $return; return $return; } /** * Removing all existing nonces, or at least as many as possible. * Capped at 20 seconds to avoid breaking the site. * * @param int $cutoff_timestamp All nonces added before this timestamp will be removed. * @param int $time_limit How long the cleanup can run (in seconds). * * @return true */ public function clean_all( $cutoff_timestamp = PHP_INT_MAX, $time_limit = 20 ) { // phpcs:ignore Generic.CodeAnalysis.ForLoopWithTestFunctionCall.NotAllowed for ( $end_time = time() + $time_limit; time() < $end_time; ) { $result = $this->delete( static::CLEAN_ALL_LIMIT_PER_BATCH, $cutoff_timestamp ); if ( ! $result ) { break; } } return true; } /** * Scheduled clean up of the expired nonces. */ public static function clean_scheduled() { /** * Adjust the time limit for the scheduled cleanup. * * @since 9.5.0 * * @param int $time_limit How long the cleanup can run (in seconds). */ $time_limit = apply_filters( 'jetpack_connection_nonce_cleanup_runtime_limit', static::SCHEDULED_CLEANUP_TIME_LIMIT ); ( new static() )->clean_all( time() - static::LIFETIME, $time_limit ); } /** * Delete the nonces. * * @param int $limit How many nonces to delete. * @param null|int $cutoff_timestamp All nonces added before this timestamp will be removed. * * @return int|false Number of removed nonces, or `false` if nothing to remove (or in case of a database error). */ public function delete( $limit = 10, $cutoff_timestamp = null ) { global $wpdb; $ids = $wpdb->get_col( $wpdb->prepare( "SELECT option_id FROM `{$wpdb->options}`" . " WHERE `option_name` >= 'jetpack_nonce_' AND `option_name` < %s" . ' LIMIT %d', 'jetpack_nonce_' . $cutoff_timestamp, $limit ) ); if ( ! is_array( $ids ) ) { // There's an error and we can't proceed. return false; } // Removing zeroes in case AUTO_INCREMENT of the options table is broken, and all ID's are zeroes. $ids = array_filter( $ids ); if ( array() === $ids ) { // There's nothing to remove. return false; } $ids_fill = implode( ', ', array_fill( 0, count( $ids ), '%d' ) ); $args = $ids; $args[] = 'jetpack_nonce_%'; // The Code Sniffer is unable to understand what's going on... // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber return $wpdb->query( $wpdb->prepare( "DELETE FROM `{$wpdb->options}` WHERE `option_id` IN ( {$ids_fill} ) AND option_name LIKE %s", $args ) ); } /** * Clean the cached nonces valid during the current request, therefore making them invalid. * * @return bool */ public static function invalidate_request_nonces() { static::$nonces_used_this_request = array(); return true; } } jetpack-connection/src/class-tokens-locks.php 0000777 00000003321 15251741406 0015354 0 ustar 00 <?php /** * The Jetpack Connection Tokens Locks class file. * * @package automattic/jetpack-connection */ namespace Automattic\Jetpack\Connection; /** * * Jetpack Connection tokens cleanup during migration. * This class encapsulates plugin or tool specific code that activates token lock upon migration. * * The connection tokens are locked to the current domain. * If the database is imported on another site (domain name doesn't match), the tokens get removed. * * @see https://github.com/Automattic/jetpack/pull/23597 * @see \Automattic\Jetpack\Connection\Tokens::is_locked() * * @phan-constructor-used-for-side-effects */ class Tokens_Locks { /** * Whether the class has been initialized. * * @var bool */ private static $is_initialized = false; /** * Run the initializers if they haven't been run already. */ public function __construct() { if ( static::$is_initialized ) { return; } $this->init_aiowpm(); static::$is_initialized = true; } /** * Set the token lock for AIOWPM plugin export. * * @param array $params The filter parameters. * * @return array */ public function aiowpm_set_lock( $params ) { ( new Tokens() )->set_lock(); return $params; } /** * Remove the token lock for AIOWPM plugin export. * * @param array $params The filter parameters. * * @return array */ public function aiowpm_remove_lock( $params ) { ( new Tokens() )->remove_lock(); return $params; } /** * Initialize the All-in-One-WP-Migration plugin hooks. * * @return void */ private function init_aiowpm() { add_filter( 'ai1wm_export', array( $this, 'aiowpm_set_lock' ), 180 ); add_filter( 'ai1wm_export', array( $this, 'aiowpm_remove_lock' ), 250 ); } } jetpack-connection/src/class-error-handler.php 0000777 00000102000 15251741406 0015476 0 ustar 00 <?php /** * The Jetpack Connection error class file. * * @package automattic/jetpack-connection */ namespace Automattic\Jetpack\Connection; /** * The Jetpack Connection Errors that handles errors * * This class handles the following workflow for incoming XML-RPC and REST API requests: * * 1. An incoming XML-RPC or REST API request with an invalid signature triggers an error * 2. Applies a gate to only process each error code once an hour to avoid overflow * 3. It stores the error on the database, but we don't know yet if this is a valid error, because * we can't confirm it came from WP.com. * 4. It encrypts the error details and sends it to the wp.com server * 5. wp.com checks it and, if valid, sends a new request back to this site using the verify_xml_rpc_error REST endpoint * 6. This endpoint adds this error to the Verified errors in the database * 7. Triggers a workflow depending on the error (display user an error message, do some self healing, etc.) * * Note: This class only handles authentication/signature errors from incoming requests to this site. * Outgoing request signing issues (when this site makes requests to WP.com) are not handled here. * * Errors are stored in the database as options in the following format: * * [ * $error_code => [ * $user_id => [ * $error_details * ] * ] * ] * * For each error code we store a maximum of 5 errors for 5 different user ids. * * A user ID can be: * * 0 for blog tokens * * positive integer for user tokens * * 'invalid' for malformed tokens * * Example error structure: * [ * 'invalid_token' => [ * '123' => [ * 'error_code' => 'invalid_token', * 'user_id' => '123', * 'error_message' => 'The token is invalid', * 'error_data' => ['action' => 'reconnect'], * 'timestamp' => 1234567890, * 'nonce' => 'abc123def', * 'error_type' => 'xmlrpc' * ] * ] * ] * * @since 1.14.2 */ class Error_Handler { /** * The name of the option that stores the errors * * @since 1.14.2 * * @var string */ const STORED_ERRORS_OPTION = 'jetpack_connection_xmlrpc_errors'; /** * The name of the option that stores the errors * * @since 1.14.2 * * @var string */ const STORED_VERIFIED_ERRORS_OPTION = 'jetpack_connection_xmlrpc_verified_errors'; /** * The prefix of the transient that controls the gate for each error code * * @since 1.14.2 * * @var string */ const ERROR_REPORTING_GATE = 'jetpack_connection_error_reporting_gate_'; /** * Time in seconds a test should live in the database before being discarded * * @since 1.14.2 */ const ERROR_LIFE_TIME = DAY_IN_SECONDS; /** * List of known errors. Only error codes in this list will be handled * * @since 1.14.2 * * @var array */ public $known_errors = array( 'malformed_token', 'malformed_user_id', 'unknown_user', 'no_user_tokens', 'empty_master_user_option', 'no_token_for_user', 'token_malformed', 'user_id_mismatch', 'no_possible_tokens', 'no_valid_user_token', 'no_valid_blog_token', 'unknown_token', 'could_not_sign', 'invalid_scheme', 'invalid_secret', 'invalid_token', 'token_mismatch', 'invalid_body', 'invalid_signature', 'invalid_body_hash', 'invalid_nonce', 'signature_mismatch', 'invalid_connection_owner', 'external_storage_empty', 'external_storage_error', ); /** * Holds the instance of this singleton class * * @since 1.14.2 * * @var Error_Handler $instance */ public static $instance = null; /** * Cached displayable errors to avoid duplicate processing * * @since 6.13.10 * * @var array|null */ private $cached_displayable_errors = null; /** * Initialize instance, hooks and load verified errors handlers * * @since 1.14.2 */ private function __construct() { defined( 'JETPACK__ERRORS_PUBLIC_KEY' ) || define( 'JETPACK__ERRORS_PUBLIC_KEY', 'KdZY80axKX+nWzfrOcizf0jqiFHnrWCl9X8yuaClKgM=' ); add_action( 'rest_api_init', array( $this, 'register_verify_error_endpoint' ) ); // Handle verified errors on admin pages. add_action( 'admin_init', array( $this, 'handle_verified_errors' ) ); // If the site gets reconnected, clear errors. add_action( 'jetpack_site_registered', array( $this, 'delete_all_errors' ) ); add_action( 'jetpack_get_site_data_success', array( $this, 'delete_all_api_errors' ) ); add_filter( 'jetpack_connection_disconnect_site_wpcom', array( $this, 'delete_all_errors_and_return_unfiltered_value' ) ); add_filter( 'jetpack_connection_delete_all_tokens', array( $this, 'delete_all_errors_and_return_unfiltered_value' ) ); add_action( 'jetpack_unlinked_user', array( $this, 'delete_all_errors' ) ); add_action( 'jetpack_updated_user_token', array( $this, 'delete_all_errors' ) ); } /** * Gets displayable errors with predefined structure and optional filtering. * * This method returns a hierarchical array of errors (error_code => user_id => error_details) * that can be safely displayed in My Jetpack and other UI components. It includes * predefined error messages and actions, with optional filtering for specific sites. * Only processes a limited set of error codes that are meant to be displayed to users. * * @since 6.13.10 * * @return array Array of displayable errors with hierarchical structure. * Example: * [ * 'invalid_token' => [ * '123' => [ * 'error_code' => 'invalid_token', * 'user_id' => '123', * 'error_message' => 'Your connection with WordPress.com seems to be broken...', * 'error_data' => ['action' => 'reconnect'], * 'timestamp' => 1234567890, * 'nonce' => 'abc123def', * 'error_type' => 'xmlrpc' * ] * ] * ] */ public function get_displayable_errors() { // Check if we have cached result AND no filters are applied if ( $this->cached_displayable_errors !== null && ! $this->has_external_filters() ) { return $this->cached_displayable_errors; } $verified_errors = $this->get_verified_errors(); $displayable_errors = array(); // Only process error codes that are meant to be displayed to users $displayable_error_codes = array( 'malformed_token', 'token_malformed', 'no_possible_tokens', 'no_valid_user_token', 'no_valid_blog_token', 'unknown_token', 'could_not_sign', 'invalid_token', 'token_mismatch', 'invalid_signature', 'signature_mismatch', 'no_user_tokens', 'no_token_for_user', 'invalid_connection_owner', ); foreach ( $verified_errors as $error_code => $users ) { // Skip error codes that are not meant to be displayed if ( ! in_array( $error_code, $displayable_error_codes, true ) ) { continue; } $displayable_errors[ $error_code ] = array(); foreach ( $users as $user_id => $error ) { // Override other error messages with default display message $displayable_errors[ $error_code ][ $user_id ] = array_merge( $error, array( 'error_message' => __( "Your connection with WordPress.com seems to be broken. If you're experiencing issues, please try reconnecting.", 'jetpack-connection' ), ) ); } } /** * Filter displayable connection errors to allow customization of error messages and actions. * * This filter allows sites to customize how connection errors are displayed, * including modifying error messages, actions, and data. Access to this filter * is controlled by should_allow_error_filtering(). * * @since 6.12.0 * * @param array $displayable_errors Array of displayable errors with hierarchical structure. * @param array $verified_errors Array of raw verified errors from the database. */ if ( $this->should_allow_error_filtering() ) { $displayable_errors = apply_filters( 'jetpack_connection_get_verified_errors', $displayable_errors, $verified_errors ); } // Only cache if no external filters are applied if ( ! $this->has_external_filters() ) { $this->cached_displayable_errors = $displayable_errors; } return $displayable_errors; } /** * Sets up hooks for displaying verified errors on admin pages. * * This method is hooked into 'admin_init'. It retrieves displayable errors * and, if any exist, sets up the necessary action and filter hooks to display * them in admin notices and the React dashboard. * * @since 1.14.2 */ public function handle_verified_errors() { $displayable_errors = $this->get_displayable_errors(); // If there are any displayable errors, set up the hooks for displaying them in React dashboard and admin notices. if ( ! empty( $displayable_errors ) ) { add_action( 'admin_notices', array( $this, 'generic_admin_notice_error' ) ); add_filter( 'react_connection_errors_initial_state', array( $this, 'jetpack_react_dashboard_error' ), 10, 1 ); } } /** * Determines whether error filtering should be allowed. * * This method controls access to the jetpack_connection_displayable_errors filter. * Currently, only WoA sites are allowed to use this filter. * * @since 6.13.10 * * @return bool True if error filtering should be allowed, false otherwise. */ protected function should_allow_error_filtering() { $host = new \Automattic\Jetpack\Status\Host(); if ( $host->is_woa_site() || $host->is_vip_site() || $host->is_newspack_site() ) { return true; } return false; } /** * Provides displayable connection errors for the React dashboard in a flat array format. * * This method transforms the hierarchical displayable_errors structure into the flat format * expected by the React dashboard. It's used as a filter for 'react_connection_errors_initial_state'. * Returns only the first error to avoid overwhelming the user with multiple error messages. * * @since 8.9.0 * * @param array $errors Existing errors from other filters (unused but required for filter signature). * @return array Array containing only the first displayable error for the React dashboard. * Example: * [ * [ * 'code' => 'connection_error', * 'message' => 'Your connection with WordPress.com seems to be broken...', * 'action' => 'reconnect', * 'data' => [ * 'api_error_code' => 'invalid_token', * 'action' => 'reconnect' * ] * ] * ] */ public function jetpack_react_dashboard_error( $errors ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable $displayable_errors = $this->get_displayable_errors(); // Get the first error only $first_error_code = array_key_first( $displayable_errors ); if ( ! $first_error_code ) { return array(); // No errors } $first_user_errors = $displayable_errors[ $first_error_code ]; if ( ! is_array( $first_user_errors ) || empty( $first_user_errors ) ) { return array(); // Invalid error structure } $first_error = reset( $first_user_errors ); // Validate error structure if ( ! is_array( $first_error ) || ! isset( $first_error['error_message'] ) ) { return array(); // Invalid error structure } // Determine the action - use the one from error_data if available, otherwise default to 'reconnect' $action = 'reconnect'; // Default action for connection errors if ( isset( $first_error['error_data']['action'] ) && is_string( $first_error['error_data']['action'] ) ) { $action = $first_error['error_data']['action']; } // Safely merge error data, ensuring we don't overwrite critical fields $error_data = isset( $first_error['error_data'] ) && is_array( $first_error['error_data'] ) ? $first_error['error_data'] : array(); // Build the data array with safe merging $dashboard_data = array( 'api_error_code' => $first_error_code ); // Add error_data fields, but be careful not to overwrite api_error_code foreach ( $error_data as $key => $value ) { if ( 'api_error_code' !== $key ) { $dashboard_data[ $key ] = $value; } } $dashboard_error = array( array( 'code' => 'connection_error', 'message' => $first_error['error_message'], 'action' => $action, 'data' => $dashboard_data, ), ); return $dashboard_error; } /** * Gets the instance of this singleton class * * @since 1.14.2 * * @return Error_Handler $instance */ public static function get_instance() { if ( self::$instance === null ) { self::$instance = new self(); } return self::$instance; } /** * Keep track of a connection error that was encountered * * @param \WP_Error $error The error object. * @param boolean $force Force the report, even if should_report_error is false. * @param boolean $skip_wpcom_verification Set to 'true' to verify the error locally and skip the WP.com verification. * * @return void * @since 1.14.2 */ public function report_error( \WP_Error $error, $force = false, $skip_wpcom_verification = false ) { if ( in_array( $error->get_error_code(), $this->known_errors, true ) && ( $this->should_report_error( $error ) || $force ) ) { $stored_error = $this->store_error( $error ); if ( $stored_error ) { $skip_wpcom_verification ? $this->verify_error( $stored_error ) : $this->send_error_to_wpcom( $stored_error ); } } } /** * Checks the status of the gate * * This protects the site (and WPCOM) against over loads. * * @since 1.14.2 * * @param \WP_Error $error the error object. * @return boolean $should_report True if gate is open and the error should be reported. */ public function should_report_error( \WP_Error $error ) { if ( defined( '\\JETPACK_DEV_DEBUG' ) && constant( '\\JETPACK_DEV_DEBUG' ) ) { return true; } /** * Whether to bypass the gate for the error handling * * By default, we only process errors once an hour for each error code. * This is done to avoid overflows. If you need to disable this gate, you can set this variable to true. * * This filter is useful for unit testing * * @since 1.14.2 * * @param boolean $bypass_gate whether to bypass the gate. Default is false, do not bypass. */ $bypass_gate = apply_filters( 'jetpack_connection_bypass_error_reporting_gate', false ); if ( true === $bypass_gate ) { return true; } $transient = self::ERROR_REPORTING_GATE . $error->get_error_code(); if ( get_transient( $transient ) ) { return false; } set_transient( $transient, true, HOUR_IN_SECONDS ); return true; } /** * Stores the error in the database so we know there is an issue and can inform the user * * @since 1.14.2 * * @param \WP_Error $error the error object. * @return boolean|array False if stored errors were not updated and the error array if it was successfully stored. */ public function store_error( \WP_Error $error ) { $stored_errors = $this->get_stored_errors(); $error_array = $this->wp_error_to_array( $error ); $error_code = $error->get_error_code(); $user_id = $error_array['user_id']; if ( ! isset( $stored_errors[ $error_code ] ) || ! is_array( $stored_errors[ $error_code ] ) ) { $stored_errors[ $error_code ] = array(); } $stored_errors[ $error_code ][ $user_id ] = $error_array; // Let's store a maximum of 5 different user ids for each error code. $error_code_count = is_countable( $stored_errors[ $error_code ] ) ? count( $stored_errors[ $error_code ] ) : 0; if ( $error_code_count > 5 ) { // array_shift will destroy keys here because they are numeric, so manually remove first item. $keys = array_keys( $stored_errors[ $error_code ] ); unset( $stored_errors[ $error_code ][ $keys[0] ] ); } if ( update_option( self::STORED_ERRORS_OPTION, $stored_errors ) ) { return $error_array; } return false; } /** * Builds action error data for generic JavaScript components. * * This helper method creates standardized error_data arrays that work with the generic * JavaScript error handling components. External plugins (like wpcomsh) can use this * to ensure their error structures are compatible. * * @since 6.16.0 * * @param array $args Action configuration arguments - only non-empty values will be included. * @return array Standardized error_data array for JavaScript components. */ public function build_action_error_data( array $args = array() ) { // Set default values for variants $args = wp_parse_args( $args, array( 'action_variant' => 'primary', 'secondary_action_variant' => 'secondary', ) ); // Start with core data $error_data = array( 'blog_id' => \Jetpack_Options::get_option( 'id' ), ); // Validate variant values $valid_variants = array( 'primary', 'secondary' ); if ( ! in_array( $args['action_variant'], $valid_variants, true ) ) { $args['action_variant'] = 'primary'; } if ( ! in_array( $args['secondary_action_variant'], $valid_variants, true ) ) { $args['secondary_action_variant'] = 'secondary'; } // Merge extra_data first, then regular args (so args take precedence) if ( ! empty( $args['extra_data'] ) && is_array( $args['extra_data'] ) ) { $error_data = array_merge( $error_data, $args['extra_data'] ); unset( $args['extra_data'] ); // Remove from args to avoid duplication } // Filter out empty values and merge with error_data $filtered_args = array_filter( $args, function ( $value ) { return ! empty( $value ); } ); return array_merge( $error_data, $filtered_args ); } /** * Builds a standardized error array for the connection error system. * * This method creates a consistent error array structure that can be used * by both internal error handling and external plugins/customizations. * * @since 1.14.2 * * @param string $error_code The error code identifier. * @param string $error_message The human-readable error message. * @param array $error_data Additional error data (optional). * @param string $user_id The user ID associated with the error (optional). * @param string $error_type The type of error (optional). * @return array|false The standardized error array or false on failure. * Example successful return: * [ * 'error_code' => 'invalid_token', * 'user_id' => '123', * 'error_message' => 'The token is invalid', * 'error_data' => ['action' => 'reconnect'], * 'timestamp' => 1234567890, * 'nonce' => 'abc123def', * 'error_type' => 'xmlrpc' * ] */ public function build_error_array( string $error_code, string $error_message, array $error_data = array(), $user_id = '0', string $error_type = '' ) { // Validate required parameters if ( empty( $error_code ) || empty( $error_message ) ) { return false; } // Validate user_id is a string or integer if ( ! is_string( $user_id ) && ! is_int( $user_id ) ) { return false; } return array( 'error_code' => $error_code, 'user_id' => $user_id, 'error_message' => $error_message, 'error_data' => $error_data, 'timestamp' => time(), 'nonce' => wp_generate_password( 10, false ), 'error_type' => $error_type, ); } /** * Converts a WP_Error object in the array representation we store in the database * * @since 1.14.2 * * @param \WP_Error $error the error object. * @return boolean|array False if error is invalid or the error array */ public function wp_error_to_array( \WP_Error $error ) { $data = $error->get_error_data(); if ( ! isset( $data['signature_details'] ) || ! is_array( $data['signature_details'] ) ) { return false; } $signature_details = $data['signature_details']; if ( ! isset( $signature_details['token'] ) ) { return false; } $user_id = $this->get_user_id_from_token( $signature_details['token'] ); return $this->build_error_array( $error->get_error_code(), $error->get_error_message(), $signature_details, $user_id, empty( $data['error_type'] ) ? '' : $data['error_type'] ); } /** * Sends the error to WP.com to be verified * * @since 1.14.2 * * @param array $error_array The array representation of the error as it is stored in the database. * @return bool */ public function send_error_to_wpcom( $error_array ) { $blog_id = \Jetpack_Options::get_option( 'id' ); $encrypted_data = $this->encrypt_data_to_wpcom( $error_array ); if ( false === $encrypted_data ) { return false; } $args = array( 'body' => array( 'error_data' => $encrypted_data, ), ); // send encrypted data to WP.com Public-API v2. wp_remote_post( "https://public-api.wordpress.com/wpcom/v2/sites/{$blog_id}/jetpack-report-error/", $args ); return true; } /** * Encrypt data to be sent over to WP.com * * @since 1.14.2 * * @param array|string $data the data to be encoded. * @return boolean|string The encoded string on success, false on failure */ public function encrypt_data_to_wpcom( $data ) { try { // phpcs:disable WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode // phpcs:disable WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode $encrypted_data = base64_encode( sodium_crypto_box_seal( wp_json_encode( $data ), base64_decode( JETPACK__ERRORS_PUBLIC_KEY ) ) ); // phpcs:enable WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode // phpcs:enable WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode } catch ( \SodiumException $e ) { // error encrypting data. return false; } return $encrypted_data; } /** * Extracts the user ID from a token * * @since 1.14.2 * * @param string $token the token used to make the request. * @return string $the user id or `invalid` if user id not present. */ public function get_user_id_from_token( $token ) { $user_id = 'invalid'; if ( $token ) { $parsed_token = explode( ':', wp_unslash( $token ) ); if ( isset( $parsed_token[2] ) && ctype_digit( $parsed_token[2] ) ) { $user_id = $parsed_token[2]; } } return $user_id; } /** * Gets the reported errors stored in the database * * @since 1.14.2 * * @return array $errors */ public function get_stored_errors() { $stored_errors = get_option( self::STORED_ERRORS_OPTION ); if ( ! is_array( $stored_errors ) ) { $stored_errors = array(); } $stored_errors = $this->garbage_collector( $stored_errors ); return $stored_errors; } /** * Gets the verified errors stored in the database. * * This method retrieves only the errors that are actually stored in the database, * without applying any filters that might inject additional errors. This is used * internally by methods that need to modify and store the verified errors back * to the database to prevent accidentally persisting filtered/injected errors. * * @since 1.14.2 * * @return array $errors */ public function get_verified_errors() { $verified_errors = get_option( self::STORED_VERIFIED_ERRORS_OPTION ); if ( ! is_array( $verified_errors ) ) { $verified_errors = array(); } $verified_errors = $this->garbage_collector( $verified_errors ); return $verified_errors; } /** * Removes expired errors from the array * * This method is called by get_stored_errors and get_verified errors and filters their result * Whenever a new error is stored to the database or verified, this will be triggered and the * expired error will be permanently removed from the database * * @since 1.14.2 * * @param array $errors array of errors as stored in the database. * @return array */ private function garbage_collector( $errors ) { foreach ( $errors as $error_code => $users ) { foreach ( $users as $user_id => $error ) { if ( empty( $error['timestamp'] ) || self::ERROR_LIFE_TIME < time() - (int) $error['timestamp'] ) { unset( $errors[ $error_code ][ $user_id ] ); } } } // Clear empty error codes. $errors = array_filter( $errors, function ( $user_errors ) { return ! empty( $user_errors ); } ); return $errors; } /** * Delete all stored and verified errors from the database * * @since 1.14.2 * * @return void */ public function delete_all_errors() { $this->delete_stored_errors(); $this->delete_verified_errors(); // Invalidate cache since we deleted all errors $this->invalidate_displayable_errors_cache(); } /** * Delete all stored and verified API errors from the database, leave the non-API errors intact. * * @since 1.54.0 * * @return void */ public function delete_all_api_errors() { $type_filter = function ( $errors ) { if ( is_array( $errors ) ) { foreach ( $errors as $key => $error ) { if ( ! empty( $error['error_type'] ) && in_array( $error['error_type'], array( 'xmlrpc', 'rest' ), true ) ) { unset( $errors[ $key ] ); } } } return count( $errors ) ? $errors : null; }; $stored_errors = $this->get_stored_errors(); if ( is_array( $stored_errors ) && count( $stored_errors ) ) { $stored_errors = array_filter( array_map( $type_filter, $stored_errors ) ); if ( count( $stored_errors ) ) { update_option( static::STORED_ERRORS_OPTION, $stored_errors ); } else { delete_option( static::STORED_ERRORS_OPTION ); } } $verified_errors = $this->get_verified_errors(); if ( is_array( $verified_errors ) && count( $verified_errors ) ) { $verified_errors = array_filter( array_map( $type_filter, $verified_errors ) ); if ( count( $verified_errors ) ) { update_option( static::STORED_VERIFIED_ERRORS_OPTION, $verified_errors ); } else { delete_option( static::STORED_VERIFIED_ERRORS_OPTION ); } } // Invalidate cache since we may have deleted verified errors $this->invalidate_displayable_errors_cache(); } /** * Delete all stored and verified errors from the database and returns unfiltered value * * This is used to hook into a couple of filters that expect true to not short circuit the disconnection flow * * @since 8.9.0 * * @param mixed $check The input sent by the filter. * @return boolean */ public function delete_all_errors_and_return_unfiltered_value( $check ) { $this->delete_all_errors(); return $check; } /** * Delete the reported errors stored in the database * * @since 1.14.2 * * @return boolean True, if option is successfully deleted. False on failure. */ public function delete_stored_errors() { return delete_option( self::STORED_ERRORS_OPTION ); } /** * Delete the verified errors stored in the database * * @since 1.14.2 * * @return boolean True, if option is successfully deleted. False on failure. */ public function delete_verified_errors() { return delete_option( self::STORED_VERIFIED_ERRORS_OPTION ); } /** * Gets an error based on the nonce * * Receives a nonce and finds the related error. * * @since 1.14.2 * * @param string $nonce The nonce created for the error we want to get. * @return null|array Returns the error array representation or null if error not found. */ public function get_error_by_nonce( $nonce ) { $errors = $this->get_stored_errors(); foreach ( $errors as $user_group ) { foreach ( $user_group as $error ) { if ( $error['nonce'] === $nonce ) { return $error; } } } return null; } /** * Adds an error to the verified error list * * @since 1.14.2 * * @param array $error The error array, as it was saved in the unverified errors list. * @return void */ public function verify_error( $error ) { $verified_errors = $this->get_verified_errors(); $error_code = $error['error_code']; $user_id = $error['user_id']; if ( ! isset( $verified_errors[ $error_code ] ) ) { $verified_errors[ $error_code ] = array(); } $verified_errors[ $error_code ][ $user_id ] = $error; update_option( self::STORED_VERIFIED_ERRORS_OPTION, $verified_errors ); // Invalidate cache since we added a new verified error $this->invalidate_displayable_errors_cache(); } /** * Register REST API end point for error handling. * * @since 1.14.2 * * @return void */ public function register_verify_error_endpoint() { register_rest_route( 'jetpack/v4', '/verify_xmlrpc_error', array( 'methods' => \WP_REST_Server::CREATABLE, 'callback' => array( $this, 'verify_xml_rpc_error' ), 'permission_callback' => '__return_true', 'args' => array( 'nonce' => array( 'required' => true, 'type' => 'string', ), ), ) ); } /** * Handles verification that a xml rpc error is legit and came from WordPres.com * * @since 1.14.2 * * @param \WP_REST_Request $request The request sent to the WP REST API. * * @return boolean */ public function verify_xml_rpc_error( \WP_REST_Request $request ) { $error = $this->get_error_by_nonce( $request['nonce'] ); if ( $error ) { $this->verify_error( $error ); return new \WP_REST_Response( true, 200 ); } return new \WP_REST_Response( false, 200 ); } /** * Prints a generic error notice for all connection errors * * @since 8.9.0 * * @return void */ public function generic_admin_notice_error() { // do not add admin notice to the jetpack dashboard. global $pagenow; if ( 'admin.php' === $pagenow || isset( $_GET['page'] ) && 'jetpack' === $_GET['page'] ) { // phpcs:ignore return; } if ( ! current_user_can( 'jetpack_connect' ) ) { return; } /** * Filters the message to be displayed in the admin notices area when there's a connection error. * * By default we don't display any errors. * * Return an empty value to disable the message. * * @since 8.9.0 * * @param string $message The error message. * @param array $errors The array of errors. See Automattic\Jetpack\Connection\Error_Handler for details on the array structure. */ $message = apply_filters( 'jetpack_connection_error_notice_message', '', $this->get_displayable_errors() ); /** * Fires inside the admin_notices hook just before displaying the error message for a broken connection. * * If you want to disable the default message from being displayed, return an empty value in the jetpack_connection_error_notice_message filter. * * @since 8.9.0 * * @param array $errors The array of errors. See Automattic\Jetpack\Connection\Error_Handler for details on the array structure. */ do_action( 'jetpack_connection_error_notice', $this->get_displayable_errors() ); if ( empty( $message ) ) { return; } wp_admin_notice( esc_html( $message ), array( 'type' => 'error', 'dismissible' => true, 'additional_classes' => array( 'jetpack-message', 'jp-connect' ), 'attributes' => array( 'style' => 'display:block !important;' ), ) ); } /** * Check REST API response for errors, and report them to WP.com if needed. * * @see wp_remote_request() For more information on the $http_response array format. * @param array|\WP_Error $http_response The response or WP_Error on failure. * @param array $auth_data Auth data, allowed keys: `token`, `timestamp`, `nonce`, `body-hash`. * @param string $url Request URL. * @param string $method Request method. * @param string $error_type The source of an error: 'xmlrpc' or 'rest'. * * @return void */ public function check_api_response_for_errors( $http_response, $auth_data, $url, $method, $error_type ) { if ( 200 === wp_remote_retrieve_response_code( $http_response ) || ! is_array( $auth_data ) || ! $url || ! $method ) { return; } $body_raw = wp_remote_retrieve_body( $http_response ); if ( ! $body_raw ) { return; } $body = json_decode( $body_raw, true ); if ( empty( $body['error'] ) || ( ! is_string( $body['error'] ) && ! is_int( $body['error'] ) ) ) { return; } $error = new \WP_Error( $body['error'], empty( $body['message'] ) ? '' : $body['message'], array( 'signature_details' => array( 'token' => empty( $auth_data['token'] ) ? '' : $auth_data['token'], 'timestamp' => empty( $auth_data['timestamp'] ) ? '' : $auth_data['timestamp'], 'nonce' => empty( $auth_data['nonce'] ) ? '' : $auth_data['nonce'], 'body_hash' => empty( $auth_data['body_hash'] ) ? '' : $auth_data['body_hash'], 'method' => $method, 'url' => $url, ), 'error_type' => in_array( $error_type, array( 'xmlrpc', 'rest' ), true ) ? $error_type : '', ) ); $this->report_error( $error, false, true ); } /** * Determines whether external filters are applied to the get_displayable_errors method. * * @since 6.13.10 * * @return bool True if external filters are applied, false otherwise. */ private function has_external_filters() { return has_filter( 'jetpack_connection_get_verified_errors' ) && $this->should_allow_error_filtering(); } /** * Invalidates the cached displayable errors * * @since 6.13.10 * * @return void */ private function invalidate_displayable_errors_cache() { $this->cached_displayable_errors = null; } } jetpack-connection/src/class-rest-connector.php 0000777 00000103737 15251741406 0015721 0 ustar 00 <?php /** * Sets up the Connection REST API endpoints. * * @package automattic/jetpack-connection */ namespace Automattic\Jetpack\Connection; use Automattic\Jetpack\Connection\Webhooks\Authorize_Redirect; use Automattic\Jetpack\Constants; use Automattic\Jetpack\Redirect; use Automattic\Jetpack\Status; use Jetpack_XMLRPC_Server; use WP_Error; use WP_REST_Request; use WP_REST_Response; use WP_REST_Server; /** * Registers the REST routes for Connections. * * @phan-constructor-used-for-side-effects */ class REST_Connector { /** * The Connection Manager. * * @var Manager */ private $connection; /** * This property stores the localized "Insufficient Permissions" error message. * * @var string Generic error message when user is not allowed to perform an action. */ private static $user_permissions_error_msg; const JETPACK__DEBUGGER_PUBLIC_KEY = "\r\n" . '-----BEGIN PUBLIC KEY-----' . "\r\n" . 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAm+uLLVoxGCY71LS6KFc6' . "\r\n" . '1UnF6QGBAsi5XF8ty9kR3/voqfOkpW+gRerM2Kyjy6DPCOmzhZj7BFGtxSV2ZoMX' . "\r\n" . '9ZwWxzXhl/Q/6k8jg8BoY1QL6L2K76icXJu80b+RDIqvOfJruaAeBg1Q9NyeYqLY' . "\r\n" . 'lEVzN2vIwcFYl+MrP/g6Bc2co7Jcbli+tpNIxg4Z+Hnhbs7OJ3STQLmEryLpAxQO' . "\r\n" . 'q8cbhQkMx+FyQhxzSwtXYI/ClCUmTnzcKk7SgGvEjoKGAmngILiVuEJ4bm7Q1yok' . "\r\n" . 'xl9+wcfW6JAituNhml9dlHCWnn9D3+j8pxStHihKy2gVMwiFRjLEeD8K/7JVGkb/' . "\r\n" . 'EwIDAQAB' . "\r\n" . '-----END PUBLIC KEY-----' . "\r\n"; /** * Constructor. * * @param Manager $connection The Connection Manager. */ public function __construct( Manager $connection ) { $this->connection = $connection; self::$user_permissions_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' ); $jp_version = Constants::get_constant( 'JETPACK__VERSION' ); if ( ! $this->connection->has_connected_owner() ) { // Register a site. register_rest_route( 'jetpack/v4', '/verify_registration', array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'verify_registration' ), 'permission_callback' => '__return_true', ) ); } // Authorize a remote user. register_rest_route( 'jetpack/v4', '/remote_authorize', array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => __CLASS__ . '::remote_authorize', 'permission_callback' => '__return_true', ) ); // 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', '/connection', array( 'methods' => WP_REST_Server::READABLE, 'callback' => __CLASS__ . '::connection_status', 'permission_callback' => '__return_true', ) ); // Disconnect site. register_rest_route( 'jetpack/v4', '/connection', array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => __CLASS__ . '::disconnect_site', 'permission_callback' => __CLASS__ . '::disconnect_site_permission_check', 'args' => array( 'isActive' => array( 'description' => __( 'Set to false will trigger the site to disconnect.', 'jetpack-connection' ), 'validate_callback' => function ( $value ) { if ( false !== $value ) { return new WP_Error( 'rest_invalid_param', __( 'The isActive argument should be set to false.', 'jetpack-connection' ), array( 'status' => 400 ) ); } return true; }, 'required' => true, ), ), ) ); // Disconnect/unlink user from WordPress.com servers. // this endpoint is set to override the older endpoint that was previously in the Jetpack plugin // Override is here in case an older version of the Jetpack plugin is installed alongside an updated standalone. register_rest_route( 'jetpack/v4', '/connection/user', array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => __CLASS__ . '::unlink_user', 'permission_callback' => __CLASS__ . '::unlink_user_permission_callback', ), true // override other implementations. ); // We are only registering this route if Jetpack-the-plugin is not active or it's version is ge 10.0-alpha. // The reason for doing so is to avoid conflicts between the Connection package and // older versions of Jetpack, registering the same route twice. if ( empty( $jp_version ) || version_compare( $jp_version, '10.0-alpha', '>=' ) ) { // Get current user connection data. register_rest_route( 'jetpack/v4', '/connection/data', array( 'methods' => WP_REST_Server::READABLE, 'callback' => __CLASS__ . '::get_user_connection_data', 'permission_callback' => __CLASS__ . '::user_connection_data_permission_check', ) ); } // Get list of plugins that use the Jetpack connection. register_rest_route( 'jetpack/v4', '/connection/plugins', array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( __CLASS__, 'get_connection_plugins' ), 'permission_callback' => __CLASS__ . '::connection_plugins_permission_check', ) ); // Full or partial reconnect in case of connection issues. register_rest_route( 'jetpack/v4', '/connection/reconnect', array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'connection_reconnect' ), 'permission_callback' => __CLASS__ . '::jetpack_reconnect_permission_check', ) ); // Register the site (get `blog_token`). register_rest_route( 'jetpack/v4', '/connection/register', array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'connection_register' ), 'permission_callback' => __CLASS__ . '::jetpack_register_permission_check', 'args' => array( 'from' => array( 'description' => __( 'Indicates where the registration action was triggered for tracking/segmentation purposes', 'jetpack-connection' ), 'type' => 'string', ), 'redirect_uri' => array( 'description' => __( 'URI of the admin page where the user should be redirected after connection flow', 'jetpack-connection' ), 'type' => 'string', ), 'plugin_slug' => array( 'description' => __( 'Indicates from what plugin the request is coming from', 'jetpack-connection' ), 'type' => 'string', ), ), ) ); // Get authorization URL. register_rest_route( 'jetpack/v4', '/connection/authorize_url', array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'connection_authorize_url' ), 'permission_callback' => __CLASS__ . '::user_connection_data_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', ), ), ) ); register_rest_route( 'jetpack/v4', '/user-token', array( array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( static::class, 'update_user_token' ), 'permission_callback' => array( static::class, 'update_user_token_permission_check' ), 'args' => array( 'user_token' => array( 'description' => __( 'New user token', 'jetpack-connection' ), 'type' => 'string', 'required' => true, ), 'is_connection_owner' => array( 'description' => __( 'Is connection owner', 'jetpack-connection' ), 'type' => 'boolean', ), ), ), ) ); // Set the connection owner. register_rest_route( 'jetpack/v4', '/connection/owner', array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( static::class, 'set_connection_owner' ), 'permission_callback' => array( static::class, 'set_connection_owner_permission_check' ), 'args' => array( 'owner' => array( 'description' => __( 'New owner', 'jetpack-connection' ), 'type' => 'integer', 'required' => true, ), ), ) ); } /** * Handles verification that a site is registered. * * @since 1.7.0 * @since-jetpack 5.4.0 * * @param WP_REST_Request $request The request sent to the WP REST API. * * @return string|WP_Error */ public function verify_registration( WP_REST_Request $request ) { $registration_data = array( $request['secret_1'], $request['state'] ); return $this->connection->handle_registration( $registration_data ); } /** * Handles verification that a site is registered * * @since 1.7.0 * @since-jetpack 5.4.0 * * @param WP_REST_Request $request The request sent to the WP REST API. * * @return array|WP_Error */ public static function remote_authorize( $request ) { $xmlrpc_server = new Jetpack_XMLRPC_Server(); $result = $xmlrpc_server->remote_authorize( $request ); if ( is_a( $result, 'IXR_Error' ) ) { $result = new WP_Error( $result->code, $result->message ); } 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 function remote_provision( WP_REST_Request $request ) { $request_data = $request->get_params(); if ( current_user_can( 'jetpack_connect_user' ) ) { $request_data['local_user'] = get_current_user_id(); } $xmlrpc_server = new Jetpack_XMLRPC_Server(); $result = $xmlrpc_server->remote_provision( $request_data ); 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. * * @param WP_REST_Request $request The request object. * * @return true|WP_Error */ public function remote_provision_permission_check( WP_REST_Request $request ) { if ( empty( $request['local_user'] ) && current_user_can( 'jetpack_connect_user' ) ) { return true; } 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. * * @since 1.7.0 * @since-jetpack 4.3.0 * * @param bool $rest_response Should we return a rest response or a simple array. Default to rest response. * * @return WP_REST_Response|array Connection information. */ public static function connection_status( $rest_response = true ) { $status = new Status(); $connection = new Manager(); $connection_status = array( 'isActive' => $connection->has_connected_owner(), // TODO deprecate this. 'isStaging' => $status->in_safe_mode(), // TODO deprecate this. 'isRegistered' => $connection->is_connected(), 'isUserConnected' => $connection->is_user_connected(), 'hasConnectedOwner' => $connection->has_connected_owner(), 'offlineMode' => array( 'isActive' => $status->is_offline_mode(), 'constant' => defined( 'JETPACK_DEV_DEBUG' ) && JETPACK_DEV_DEBUG, 'url' => $status->is_local_site(), /** This filter is documented in packages/status/src/class-status.php */ 'filter' => ( apply_filters( 'jetpack_development_mode', false ) || apply_filters( 'jetpack_offline_mode', false ) ), // jetpack_development_mode is deprecated. 'wpLocalConstant' => defined( 'WP_LOCAL_DEV' ) && WP_LOCAL_DEV, 'option' => (bool) get_option( 'jetpack_offline_mode' ), ), 'isPublic' => '1' == get_option( 'blog_public' ), // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual ); /** * Filters the connection status data. * * @since 1.25.0 * * @param array An array containing the connection status data. */ $connection_status = apply_filters( 'jetpack_connection_status', $connection_status ); if ( $rest_response ) { return rest_ensure_response( $connection_status ); } else { return $connection_status; } } /** * Get plugins connected to the Jetpack. * * @param bool $rest_response Should we return a rest response or a simple array. Default to rest response. * * @since 1.13.1 * @since 1.38.0 Added $rest_response param. * * @return WP_REST_Response|WP_Error Response or error object, depending on the request result. */ public static function get_connection_plugins( $rest_response = true ) { $plugins = ( new Manager() )->get_connected_plugins(); if ( is_wp_error( $plugins ) ) { return $plugins; } array_walk( $plugins, function ( &$data, $slug ) { $data['slug'] = $slug; } ); if ( $rest_response ) { return rest_ensure_response( array_values( $plugins ) ); } return array_values( $plugins ); } /** * Verify that user can view Jetpack admin page and can activate plugins. * * @since 1.15.0 * * @return bool|WP_Error Whether user has the capability 'activate_plugins'. */ public static function activate_plugins_permission_check() { if ( current_user_can( 'activate_plugins' ) ) { return true; } return new WP_Error( 'invalid_user_permission_activate_plugins', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) ); } /** * Permission check for the connection_plugins endpoint * * @return bool|WP_Error */ public static function connection_plugins_permission_check() { if ( true === static::activate_plugins_permission_check() ) { return true; } if ( true === static::is_request_signed_by_jetpack_debugger() ) { return true; } return new WP_Error( 'invalid_user_permission_activate_plugins', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) ); } /** * Permission check for the disconnect site endpoint. * * @since 1.30.1 * * @since 5.1.0 Modified the permission check to accept requests signed with blog tokens. * * @return bool|WP_Error True if user is able to disconnect the site or the request is signed with a blog token (aka a direct request from WPCOM). */ public static function disconnect_site_permission_check() { if ( current_user_can( 'jetpack_disconnect' ) ) { return true; } return Rest_Authentication::is_signed_with_blog_token() ? true : new WP_Error( 'invalid_user_permission_jetpack_disconnect', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) ); } /** * Verify that a user can use the /connection/user endpoint. Has to be a registered user and be currently linked. * * @since 6.3.3 * * @return bool|WP_Error True if user is able to unlink. */ public static function unlink_user_permission_callback() { // This is a mapped capability // phpcs:ignore WordPress.WP.Capabilities.Unknown if ( current_user_can( 'jetpack_unlink_user' ) && ( new Manager() )->is_user_connected( get_current_user_id() ) ) { return true; } return new WP_Error( 'invalid_user_permission_unlink_user', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) ); } /** * Get miscellaneous user data related to the connection. Similar data available in old "My Jetpack". * Information about the master/primary user. * Information about the current user. * * @param bool $rest_response Should we return a rest response or a simple array. Default to rest response. * * @since 1.30.1 * * @return \WP_REST_Response|array */ public static function get_user_connection_data( $rest_response = true ) { $blog_id = \Jetpack_Options::get_option( 'id' ); $connection = new Manager(); $current_user = wp_get_current_user(); $connection_owner = $connection->get_connection_owner(); $owner_display_name = false === $connection_owner ? null : $connection_owner->display_name; $is_user_connected = $connection->is_user_connected(); $is_master_user = false === $connection_owner ? false : ( $current_user->ID === $connection_owner->ID ); $wpcom_user_data = $connection->get_connected_user_data(); // Add connected user gravatar to the returned wpcom_user_data. // Probably we shouldn't do this when $wpcom_user_data is false, but we have been since 2016 so // clients probably expect that by now. if ( false === $wpcom_user_data ) { $wpcom_user_data = array(); } $wpcom_user_data['avatar'] = ( ! empty( $wpcom_user_data['email'] ) ? get_avatar_url( $wpcom_user_data['email'], array( 'size' => 64, 'default' => 'mysteryman', ) ) : false ); // Check for possible account errors between the local user and WPCOM account. $possible_errors = array(); if ( $is_user_connected && ! empty( $wpcom_user_data['email'] ) ) { $user_account_status = new \Automattic\Jetpack\Connection\User_Account_Status(); $possible_errors = $user_account_status->check_account_errors( $current_user->user_email, $wpcom_user_data['email'] ); } $current_user_connection_data = array( 'isConnected' => $is_user_connected, 'isMaster' => $is_master_user, 'username' => $current_user->user_login, 'id' => $current_user->ID, 'blogId' => $blog_id, 'wpcomUser' => $wpcom_user_data, 'gravatar' => get_avatar_url( $current_user->ID ), 'permissions' => array( 'connect' => current_user_can( 'jetpack_connect' ), 'connect_user' => current_user_can( 'jetpack_connect_user' ), // This is a mapped capability // phpcs:ignore WordPress.WP.Capabilities.Unknown 'unlink_user' => current_user_can( 'jetpack_unlink_user' ), 'disconnect' => current_user_can( 'jetpack_disconnect' ), 'manage_options' => current_user_can( 'manage_options' ), ), 'possibleAccountErrors' => $possible_errors, ); /** * Filters the current user connection data. * * @since 1.30.1 * * @param array An array containing the current user connection data. */ $current_user_connection_data = apply_filters( 'jetpack_current_user_connection_data', $current_user_connection_data ); $response = array( 'currentUser' => $current_user_connection_data, 'connectionOwner' => $owner_display_name, 'isRegistered' => $connection->is_connected(), ); if ( $rest_response ) { return rest_ensure_response( $response ); } return $response; } /** * Verify that user is allowed to disconnect Jetpack. * * @since 1.15.0 * * @return bool|WP_Error Whether user has the capability 'jetpack_disconnect'. */ public static function jetpack_reconnect_permission_check() { if ( current_user_can( 'jetpack_reconnect' ) ) { return true; } return new WP_Error( 'invalid_user_permission_jetpack_disconnect', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) ); } /** * Returns generic error message when user is not allowed to perform an action. * * @return string The error message. */ public static function get_user_permissions_error_msg() { return self::$user_permissions_error_msg; } /** * The endpoint tried to partially or fully reconnect the website to WP.com. * * @since 1.15.0 * * @return \WP_REST_Response|WP_Error */ public function connection_reconnect() { $response = array(); $next = null; $result = $this->connection->restore(); if ( is_wp_error( $result ) ) { $response = $result; } elseif ( is_string( $result ) ) { $next = $result; } else { $next = true === $result ? 'completed' : 'failed'; } switch ( $next ) { case 'authorize': $response['status'] = 'in_progress'; $response['authorizeUrl'] = $this->connection->get_authorization_url(); break; case 'completed': $response['status'] = 'completed'; /** * Action fired when reconnection has completed successfully. * * @since 1.18.1 */ do_action( 'jetpack_reconnection_completed' ); break; case 'failed': $response = new WP_Error( 'Reconnect failed' ); break; } return rest_ensure_response( $response ); } /** * Verify that user is allowed to connect Jetpack. * * @since 1.26.0 * * @return bool|WP_Error Whether user has the capability 'jetpack_connect'. */ public static function jetpack_register_permission_check() { if ( current_user_can( 'jetpack_connect' ) ) { return true; } return new WP_Error( 'invalid_user_permission_jetpack_connect', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) ); } /** * The endpoint tried to connect Jetpack site to WPCOM. * * @since 1.7.0 * @since 6.7.0 No longer needs `registration_nonce`. * @since-jetpack 7.7.0 * * @param \WP_REST_Request $request The request sent to the WP REST API. * * @return \WP_REST_Response|WP_Error */ public function connection_register( $request ) { if ( isset( $request['from'] ) ) { $this->connection->add_register_request_param( 'from', (string) $request['from'] ); } if ( ! empty( $request['plugin_slug'] ) ) { // If `plugin_slug` matches a plugin using the connection, let's inform the plugin that is establishing the connection. $connected_plugin = Plugin_Storage::get_one( (string) $request['plugin_slug'] ); if ( ! is_wp_error( $connected_plugin ) && ! empty( $connected_plugin ) ) { $this->connection->set_plugin_instance( new Plugin( (string) $request['plugin_slug'] ) ); } } $result = $this->connection->try_registration(); if ( is_wp_error( $result ) ) { return $result; } $redirect_uri = $request->get_param( 'redirect_uri' ) ? admin_url( $request->get_param( 'redirect_uri' ) ) : null; $authorize_url = ( new Authorize_Redirect( $this->connection ) )->build_authorize_url( $redirect_uri ); /** * Filters the response of jetpack/v4/connection/register endpoint * * @param array $response Array response * @since 1.27.0 */ $response_body = apply_filters( 'jetpack_register_site_rest_response', array() ); // We manipulate the alternate URLs after the filter is applied, so they cannot be overwritten. $response_body['authorizeUrl'] = $authorize_url; if ( ! empty( $response_body['alternateAuthorizeUrl'] ) ) { $response_body['alternateAuthorizeUrl'] = Redirect::get_url( $response_body['alternateAuthorizeUrl'] ); } return rest_ensure_response( $response_body ); } /** * Get the authorization URL. * * @since 1.27.0 * * @param \WP_REST_Request $request The request sent to the WP REST API. * * @return \WP_REST_Response|WP_Error */ public function connection_authorize_url( $request ) { $redirect_uri = $request->get_param( 'redirect_uri' ) ? admin_url( $request->get_param( 'redirect_uri' ) ) : null; $authorize_url = $this->connection->get_authorization_url( null, $redirect_uri ); return rest_ensure_response( array( 'authorizeUrl' => $authorize_url, ) ); } /** * The endpoint tried to partially or fully reconnect the website to WP.com. * * @since 1.29.0 * * @param \WP_REST_Request $request The request sent to the WP REST API. * * @return \WP_REST_Response|WP_Error */ public static function update_user_token( $request ) { $token_parts = explode( '.', $request['user_token'] ); if ( count( $token_parts ) !== 3 || ! (int) $token_parts[2] || ! ctype_digit( $token_parts[2] ) ) { return new WP_Error( 'invalid_argument_user_token', esc_html__( 'Invalid user token is provided', 'jetpack-connection' ) ); } $user_id = (int) $token_parts[2]; if ( false === get_userdata( $user_id ) ) { return new WP_Error( 'invalid_argument_user_id', esc_html__( 'Invalid user id is provided', 'jetpack-connection' ) ); } $connection = new Manager(); if ( ! $connection->is_connected() ) { return new WP_Error( 'site_not_connected', esc_html__( 'Site is not connected', 'jetpack-connection' ) ); } $is_connection_owner = isset( $request['is_connection_owner'] ) ? (bool) $request['is_connection_owner'] : ( new Manager() )->get_connection_owner_id() === $user_id; ( new Tokens() )->update_user_token( $user_id, $request['user_token'], $is_connection_owner ); /** * Fires when the user token gets successfully replaced. * * @since 1.29.0 * * @param int $user_id User ID. * @param string $token New user token. */ do_action( 'jetpack_updated_user_token', $user_id, $request['user_token'] ); return rest_ensure_response( array( 'success' => true, ) ); } /** * Disconnects Jetpack from the WordPress.com Servers * * @since 1.30.1 * * @return bool|WP_Error True if Jetpack successfully disconnected. */ public static function disconnect_site() { $connection = new Manager(); if ( $connection->is_connected() ) { $connection->disconnect_site(); return rest_ensure_response( array( 'code' => 'success' ) ); } return new WP_Error( 'disconnect_failed', esc_html__( 'Failed to disconnect the site as it appears already disconnected.', 'jetpack-connection' ), array( 'status' => 400 ) ); } /** * Unlinks current user from the WordPress.com Servers. * * @since 6.3.3 * * @param WP_REST_Request $request The request sent to the WP REST API. * * @return bool|WP_Error True if user successfully unlinked. */ public static function unlink_user( $request ) { if ( ! isset( $request['linked'] ) || false !== $request['linked'] ) { return new WP_Error( 'invalid_param', esc_html__( 'Invalid Parameter', 'jetpack-connection' ), array( 'status' => 404 ) ); } // If the user is also connection owner, we need to disconnect all users. Since disconnecting all users is a destructive action, we need to pass a parameter to confirm the action. $disconnect_all_users = false; if ( ( new Manager() )->get_connection_owner_id() === get_current_user_id() ) { if ( isset( $request['disconnect-all-users'] ) && false !== $request['disconnect-all-users'] ) { $disconnect_all_users = true; } else { return new WP_Error( 'unlink_user_failed', esc_html__( 'Unable to unlink the connection owner.', 'jetpack-connection' ), array( 'status' => 400 ) ); } } // Allow admins to force a disconnect by passing the "force" parameter // This allows an admin to disconnect themselves if ( isset( $request['force'] ) && false !== $request['force'] && current_user_can( 'manage_options' ) && ( new Manager( 'jetpack' ) )->disconnect_user_force( get_current_user_id(), $disconnect_all_users ) ) { return rest_ensure_response( array( 'code' => 'success', ) ); } elseif ( ( new Manager( 'jetpack' ) )->disconnect_user() ) { return rest_ensure_response( array( 'code' => 'success', ) ); } return new WP_Error( 'unlink_user_failed', esc_html__( 'Was not able to unlink the user. Please try again.', 'jetpack-connection' ), array( 'status' => 400 ) ); } /** * Verify that the API client is allowed to replace user token. * * @since 1.29.0 * * @return bool|WP_Error */ public static function update_user_token_permission_check() { return Rest_Authentication::is_signed_with_blog_token() ? true : new WP_Error( 'invalid_permission_update_user_token', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) ); } /** * Change the connection owner. * * @since 1.29.0 * * @param WP_REST_Request $request The request sent to the WP REST API. * * @return \WP_REST_Response|WP_Error */ public static function set_connection_owner( $request ) { $new_owner_id = $request['owner']; $owner_set = ( new Manager() )->update_connection_owner( $new_owner_id ); if ( is_wp_error( $owner_set ) ) { return $owner_set; } return rest_ensure_response( array( 'code' => 'success', ) ); } /** * Check that user has permission to change the master user. * * @since 1.7.0 * @since-jetpack 6.2.0 * @since-jetpack 7.7.0 Update so that any user with jetpack_disconnect privs can set owner. * * @return bool|WP_Error True if user is able to change master user. */ public static function set_connection_owner_permission_check() { if ( current_user_can( 'jetpack_disconnect' ) ) { return true; } 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() ) ); } /** * Permission check for the connection/data endpoint * * @return bool|WP_Error */ public static function user_connection_data_permission_check() { if ( current_user_can( 'jetpack_connect_user' ) ) { return true; } return new WP_Error( 'invalid_user_permission_user_connection_data', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) ); } /** * Verifies if the request was signed with the Jetpack Debugger key * * @param string|null $pub_key The public key used to verify the signature. Default is the Jetpack Debugger key. This is used for testing purposes. * * @return bool */ public static function is_request_signed_by_jetpack_debugger( $pub_key = null ) { // phpcs:disable WordPress.Security.NonceVerification.Recommended if ( ! isset( $_GET['signature'] ) || ! isset( $_GET['timestamp'] ) || ! isset( $_GET['url'] ) || ! isset( $_GET['rest_route'] ) ) { return false; } // signature timestamp must be within 5min of current time. if ( abs( time() - (int) $_GET['timestamp'] ) > 300 ) { return false; } $signature = base64_decode( filter_var( wp_unslash( $_GET['signature'] ) ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode $signature_data = wp_json_encode( array( 'rest_route' => filter_var( wp_unslash( $_GET['rest_route'] ) ), 'timestamp' => (int) $_GET['timestamp'], 'url' => filter_var( wp_unslash( $_GET['url'] ) ), ) ); if ( ! function_exists( 'openssl_verify' ) || 1 !== openssl_verify( $signature_data, $signature, $pub_key ? $pub_key : static::JETPACK__DEBUGGER_PUBLIC_KEY ) ) { return false; } // phpcs:enable WordPress.Security.NonceVerification.Recommended return true; } } jetpack-connection/src/class-tracking.php 0000777 00000023564 15251741406 0014555 0 ustar 00 <?php /** * Nosara Tracks for Jetpack * * @package automattic/jetpack-connection */ namespace Automattic\Jetpack; /** * The Tracking class, used to record events in wpcom */ class Tracking { /** * The assets version. * * @since 1.13.1 * @deprecated since 1.40.1 * * @var string Assets version. */ const ASSETS_VERSION = '1.0.0'; /** * Slug of the product that we are tracking. * * @var string */ private $product_name; /** * Connection manager object. * * @var Object */ private $connection; /** * Creates the Tracking object. * * @param string $product_name the slug of the product that we are tracking. * @param \Automattic\Jetpack\Connection\Manager $connection the connection manager object. */ public function __construct( $product_name = 'jetpack', $connection = null ) { $this->product_name = $product_name; $this->connection = $connection; if ( $this->connection === null ) { // TODO We should always pass a Connection. $this->connection = new Connection\Manager(); } if ( ! did_action( 'jetpack_set_tracks_ajax_hook' ) ) { add_action( 'wp_ajax_jetpack_tracks', array( $this, 'ajax_tracks' ) ); /** * Fires when the Tracking::ajax_tracks() callback has been hooked to the * wp_ajax_jetpack_tracks action. This action is used to ensure that * the callback is hooked only once. * * @since 1.13.11 */ do_action( 'jetpack_set_tracks_ajax_hook' ); } } /** * Universal method for for all tracking events triggered via the JavaScript client. * * @access public */ public function ajax_tracks() { // Check for nonce. if ( empty( $_REQUEST['tracksNonce'] ) || ! wp_verify_nonce( $_REQUEST['tracksNonce'], 'jp-tracks-ajax-nonce' ) // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- WP core doesn't pre-sanitize nonces either. ) { wp_send_json_error( __( 'You aren’t authorized to do that.', 'jetpack-connection' ), 403 ); } if ( ! isset( $_REQUEST['tracksEventName'] ) || ! isset( $_REQUEST['tracksEventType'] ) ) { wp_send_json_error( __( 'No valid event name or type.', 'jetpack-connection' ), 403 ); } $tracks_data = array(); if ( 'click' === $_REQUEST['tracksEventType'] && isset( $_REQUEST['tracksEventProp'] ) ) { if ( is_array( $_REQUEST['tracksEventProp'] ) ) { $tracks_data = array_map( 'filter_var', wp_unslash( $_REQUEST['tracksEventProp'] ) ); } else { $tracks_data = array( 'clicked' => filter_var( wp_unslash( $_REQUEST['tracksEventProp'] ) ) ); } } $this->record_user_event( filter_var( wp_unslash( $_REQUEST['tracksEventName'] ) ), $tracks_data, null, false ); wp_send_json_success(); } /** * Register script necessary for tracking. * * @param boolean $enqueue Also enqueue? defaults to false. */ public static function register_tracks_functions_scripts( $enqueue = false ) { // Register jp-tracks as it is a dependency. wp_register_script( 'jp-tracks', '//stats.wp.com/w.js', array(), gmdate( 'YW' ), true ); Assets::register_script( 'jp-tracks-functions', '../dist/tracks-callables.js', __FILE__, array( 'dependencies' => array( 'jp-tracks' ), 'enqueue' => $enqueue, 'in_footer' => true, ) ); } /** * Enqueue script necessary for tracking. */ public function enqueue_tracks_scripts() { Assets::register_script( 'jptracks', '../dist/tracks-ajax.js', __FILE__, array( 'dependencies' => array( 'jquery' ), 'enqueue' => true, 'in_footer' => true, ) ); wp_localize_script( 'jptracks', 'jpTracksAJAX', array( 'ajaxurl' => admin_url( 'admin-ajax.php' ), 'jpTracksAJAX_nonce' => wp_create_nonce( 'jp-tracks-ajax-nonce' ), ) ); } /** * Send an event in Tracks. * * @param string $event_type Type of the event. * @param array $data Data to send with the event. * @param mixed $user Username, user_id, or WP_User object. * @param bool $use_product_prefix Whether to use the object's product name as a prefix to the event type. If * set to false, the prefix will be 'jetpack_'. */ public function record_user_event( $event_type, $data = array(), $user = null, $use_product_prefix = true ) { if ( ! $user ) { $user = wp_get_current_user(); } $site_url = get_option( 'siteurl' ); $data['_via_ua'] = isset( $_SERVER['HTTP_USER_AGENT'] ) ? filter_var( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : ''; $data['_via_ip'] = isset( $_SERVER['REMOTE_ADDR'] ) ? filter_var( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : ''; $data['_lg'] = isset( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ? filter_var( wp_unslash( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ) : ''; $data['blog_url'] = $site_url; $data['blog_id'] = \Jetpack_Options::get_option( 'id' ); // Top level events should not be namespaced. if ( '_aliasUser' !== $event_type ) { $prefix = $use_product_prefix ? $this->product_name : 'jetpack'; $event_type = $prefix . '_' . $event_type; } $data['jetpack_version'] = defined( 'JETPACK__VERSION' ) ? JETPACK__VERSION : '0'; return $this->tracks_record_event( $user, $event_type, $data ); } /** * Record an event in Tracks - this is the preferred way to record events from PHP. * * @param mixed $user username, user_id, or WP_User object. * @param string $event_name The name of the event. * @param array $properties Custom properties to send with the event. * @param int $event_timestamp_millis The time in millis since 1970-01-01 00:00:00 when the event occurred. * * @return bool true for success | \WP_Error if the event pixel could not be fired */ public function tracks_record_event( $user, $event_name, $properties = array(), $event_timestamp_millis = false ) { // We don't want to track user events during unit tests/CI runs. if ( $user instanceof \WP_User && 'wptests_capabilities' === $user->cap_key ) { return false; } $terms_of_service = new Terms_Of_Service(); $status = new Status(); // Don't track users who have not agreed to our TOS. if ( ! $this->should_enable_tracking( $terms_of_service, $status ) ) { return false; } $event_obj = $this->tracks_build_event_obj( $user, $event_name, $properties, $event_timestamp_millis ); if ( is_wp_error( $event_obj->error ) ) { return $event_obj->error; } return $event_obj->record(); } /** * Determines whether tracking should be enabled. * * @param \Automattic\Jetpack\Terms_Of_Service $terms_of_service A Terms_Of_Service object. * @param \Automattic\Jetpack\Status $status A Status object. * * @return boolean True if tracking should be enabled, else false. */ public function should_enable_tracking( $terms_of_service, $status ) { if ( $status->is_offline_mode() ) { return false; } return $terms_of_service->has_agreed() || $this->connection->is_user_connected(); } /** * Procedurally build a Tracks Event Object. * NOTE: Use this only when the simpler Automattic\Jetpack\Tracking->jetpack_tracks_record_event() function won't work for you. * * @param \WP_User $user WP_User object. * @param string $event_name The name of the event. * @param array $properties Custom properties to send with the event. * @param int $event_timestamp_millis The time in millis since 1970-01-01 00:00:00 when the event occurred. * * @return \Jetpack_Tracks_Event|\WP_Error */ private function tracks_build_event_obj( $user, $event_name, $properties = array(), $event_timestamp_millis = false ) { $identity = $this->tracks_get_identity( $user->ID ); $properties['user_lang'] = $user->get( 'WPLANG' ); $blog_details = array( 'blog_lang' => 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 ); $timestamp_string = is_string( $timestamp ) ? $timestamp : number_format( $timestamp, 0, '', '' ); return new \Jetpack_Tracks_Event( array_merge( $blog_details, (array) $properties, $identity, array( '_en' => $event_name, '_ts' => $timestamp_string, ) ) ); } /** * Get the identity to send to tracks. * * @param int $user_id The user id of the local user. * * @return array $identity */ public function tracks_get_identity( $user_id ) { // Meta is set, and user is still connected. Use WPCOM ID. $wpcom_id = get_user_meta( $user_id, 'jetpack_tracks_wpcom_id', true ); if ( $wpcom_id && is_string( $wpcom_id ) && $this->connection->is_user_connected( $user_id ) ) { return array( '_ut' => 'wpcom:user_id', '_ui' => $wpcom_id, ); } // User is connected, but no meta is set yet. Use WPCOM ID and set meta. if ( $this->connection->is_user_connected( $user_id ) ) { $wpcom_user_data = $this->connection->get_connected_user_data( $user_id ); $wpcom_id = $wpcom_user_data['ID'] ?? null; if ( is_string( $wpcom_id ) ) { update_user_meta( $user_id, 'jetpack_tracks_wpcom_id', $wpcom_id ); return array( '_ut' => 'wpcom:user_id', '_ui' => $wpcom_id, ); } } // User isn't linked at all. Fall back to anonymous ID. $anon_id = get_user_meta( $user_id, 'jetpack_tracks_anon_id', true ); if ( ! $anon_id ) { $anon_id = \Jetpack_Tracks_Client::get_anon_id(); add_user_meta( $user_id, 'jetpack_tracks_anon_id', $anon_id, false ); } if ( ! isset( $_COOKIE['tk_ai'] ) && ! headers_sent() ) { setcookie( 'tk_ai', $anon_id, 0, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), false ); // phpcs:ignore Jetpack.Functions.SetCookie -- This is a random string and should be fine. } return array( '_ut' => 'anon', '_ui' => $anon_id, ); } } jetpack-connection/src/class-secrets.php 0000777 00000020640 15251741406 0014413 0 ustar 00 <?php /** * The Jetpack Connection Secrets class file. * * @package automattic/jetpack-connection */ namespace Automattic\Jetpack\Connection; use Jetpack_Options; use WP_Error; /** * The Jetpack Connection Secrets class that is used to manage secrets. */ class Secrets { const SECRETS_MISSING = 'secrets_missing'; const SECRETS_EXPIRED = 'secrets_expired'; const LEGACY_SECRETS_OPTION_NAME = 'jetpack_secrets'; /** * Deletes all connection secrets from the local Jetpack site. */ public function delete_all() { Jetpack_Options::delete_raw_option( 'jetpack_secrets' ); } /** * Runs the wp_generate_password function with the required parameters. This is the * default implementation of the secret callable, can be overridden using the * jetpack_connection_secret_generator filter. * * @return String $secret value. */ private function secret_callable_method() { $secret = wp_generate_password( 32, false ); // Some sites may hook into the random_password filter and make the password shorter, let's make sure our secret has the required length. $attempts = 1; $secret_length = strlen( $secret ); while ( $secret_length < 32 && $attempts < 32 ) { ++$attempts; $secret .= wp_generate_password( 32, false ); $secret_length = strlen( $secret ); } return (string) substr( $secret, 0, 32 ); } /** * Generates two secret tokens and the end of life timestamp for them. * * @param String $action The action name. * @param Integer|bool $user_id The user identifier. Defaults to `false`. * @param Integer $exp Expiration time in seconds. */ public function generate( $action, $user_id = false, $exp = 600 ) { if ( false === $user_id ) { $user_id = get_current_user_id(); } $callable = apply_filters( 'jetpack_connection_secret_generator', array( static::class, 'secret_callable_method' ) ); $secrets = Jetpack_Options::get_raw_option( self::LEGACY_SECRETS_OPTION_NAME, array() ); $secret_name = 'jetpack_' . $action . '_' . $user_id; if ( isset( $secrets[ $secret_name ] ) && $secrets[ $secret_name ]['exp'] > time() ) { return $secrets[ $secret_name ]; } $secret_value = array( 'secret_1' => call_user_func( $callable ), 'secret_2' => call_user_func( $callable ), 'exp' => time() + $exp, ); $secrets[ $secret_name ] = $secret_value; $res = Jetpack_Options::update_raw_option( self::LEGACY_SECRETS_OPTION_NAME, $secrets ); return $res ? $secrets[ $secret_name ] : false; } /** * Returns two secret tokens and the end of life timestamp for them. * * @param String $action The action name. * @param Integer $user_id The user identifier. * @return string|array an array of secrets or an error string. * @phan-return string|array{secret_1:string,secret_2:string,exp:int} */ public function get( $action, $user_id ) { $secret_name = 'jetpack_' . $action . '_' . $user_id; $secrets = Jetpack_Options::get_raw_option( self::LEGACY_SECRETS_OPTION_NAME, array() ); if ( ! isset( $secrets[ $secret_name ] ) ) { return self::SECRETS_MISSING; } if ( $secrets[ $secret_name ]['exp'] < time() ) { $this->delete( $action, $user_id ); return self::SECRETS_EXPIRED; } return $secrets[ $secret_name ]; } /** * Deletes secret tokens in case they, for example, have expired. * * @param String $action The action name. * @param Integer $user_id The user identifier. */ public function delete( $action, $user_id ) { $secret_name = 'jetpack_' . $action . '_' . $user_id; $secrets = Jetpack_Options::get_raw_option( self::LEGACY_SECRETS_OPTION_NAME, array() ); if ( isset( $secrets[ $secret_name ] ) ) { unset( $secrets[ $secret_name ] ); Jetpack_Options::update_raw_option( self::LEGACY_SECRETS_OPTION_NAME, $secrets ); } } /** * Verify a Previously Generated Secret. * * @param string $action The type of secret to verify. * @param string $secret_1 The secret string to compare to what is stored. * @param int $user_id The user ID of the owner of the secret. * @return WP_Error|string WP_Error on failure, secret_2 on success. */ public function verify( $action, $secret_1, $user_id ) { $allowed_actions = array( 'register', 'authorize', 'publicize' ); if ( ! in_array( $action, $allowed_actions, true ) ) { return new WP_Error( 'unknown_verification_action', 'Unknown Verification Action', 400 ); } $user = get_user_by( 'id', $user_id ); /** * We've begun verifying the previously generated secret. * * @since 1.7.0 * @since-jetpack 7.5.0 * * @param string $action The type of secret to verify. * @param \WP_User $user The user object. */ do_action( 'jetpack_verify_secrets_begin', $action, $user ); /** Closure to run the 'fail' action and return an error. */ $return_error = function ( WP_Error $error ) use ( $action, $user ) { /** * Verifying of the previously generated secret has failed. * * @since 1.7.0 * @since-jetpack 7.5.0 * * @param string $action The type of secret to verify. * @param \WP_User $user The user object. * @param WP_Error $error The error object. */ do_action( 'jetpack_verify_secrets_fail', $action, $user, $error ); return $error; }; $stored_secrets = $this->get( $action, $user_id ); $this->delete( $action, $user_id ); $error = null; if ( empty( $secret_1 ) ) { $error = $return_error( new WP_Error( 'verify_secret_1_missing', /* translators: "%s" is the name of a paramter. It can be either "secret_1" or "state". */ sprintf( __( 'The required "%s" parameter is missing.', 'jetpack-connection' ), 'secret_1' ), 400 ) ); } elseif ( ! is_string( $secret_1 ) ) { $error = $return_error( new WP_Error( 'verify_secret_1_malformed', /* translators: "%s" is the name of a paramter. It can be either "secret_1" or "state". */ sprintf( __( 'The required "%s" parameter is malformed.', 'jetpack-connection' ), 'secret_1' ), 400 ) ); } elseif ( empty( $user_id ) ) { // $user_id is passed around during registration as "state". $error = $return_error( new WP_Error( 'state_missing', /* translators: "%s" is the name of a paramter. It can be either "secret_1" or "state". */ sprintf( __( 'The required "%s" parameter is missing.', 'jetpack-connection' ), 'state' ), 400 ) ); } elseif ( ! ctype_digit( (string) $user_id ) ) { $error = $return_error( new WP_Error( 'state_malformed', /* translators: "%s" is the name of a paramter. It can be either "secret_1" or "state". */ sprintf( __( 'The required "%s" parameter is malformed.', 'jetpack-connection' ), 'state' ), 400 ) ); } elseif ( self::SECRETS_MISSING === $stored_secrets ) { $error = $return_error( new WP_Error( 'verify_secrets_missing', __( 'Verification secrets not found', 'jetpack-connection' ), 400 ) ); } elseif ( self::SECRETS_EXPIRED === $stored_secrets ) { $error = $return_error( new WP_Error( 'verify_secrets_expired', __( 'Verification took too long', 'jetpack-connection' ), 400 ) ); } elseif ( ! $stored_secrets ) { $error = $return_error( new WP_Error( 'verify_secrets_empty', __( 'Verification secrets are empty', 'jetpack-connection' ), 400 ) ); } elseif ( is_wp_error( $stored_secrets ) ) { $stored_secrets->add_data( 400 ); $error = $return_error( $stored_secrets ); } elseif ( empty( $stored_secrets['secret_1'] ) || empty( $stored_secrets['secret_2'] ) || empty( $stored_secrets['exp'] ) ) { $error = $return_error( new WP_Error( 'verify_secrets_incomplete', __( 'Verification secrets are incomplete', 'jetpack-connection' ), 400 ) ); } elseif ( ! hash_equals( $secret_1, $stored_secrets['secret_1'] ) ) { $error = $return_error( new WP_Error( 'verify_secrets_mismatch', __( 'Secret mismatch', 'jetpack-connection' ), 400 ) ); } // Something went wrong during the checks, returning the error. if ( ! empty( $error ) ) { return $error; } /** * We've succeeded at verifying the previously generated secret. * * @since 1.7.0 * @since-jetpack 7.5.0 * * @param string $action The type of secret to verify. * @param \WP_User $user The user object. */ do_action( 'jetpack_verify_secrets_success', $action, $user ); return $stored_secrets['secret_2']; } } jetpack-connection/src/class-server-sandbox.php 0000777 00000017267 15251741406 0015720 0 ustar 00 <?php /** * The Server_Sandbox class. * * This feature is only useful for Automattic developers. * It configures Jetpack to talk to staging/sandbox servers * on WordPress.com instead of production servers. * * @package automattic/jetpack-sandbox */ namespace Automattic\Jetpack\Connection; use Automattic\Jetpack\Constants; /** * The Server_Sandbox class. */ class Server_Sandbox { /** * Sets up the action hooks for the server sandbox. */ public function init() { if ( did_action( 'jetpack_server_sandbox_init' ) ) { return; } add_action( 'requests-requests.before_request', array( $this, 'server_sandbox' ), 10, 4 ); add_action( 'admin_bar_menu', array( $this, 'admin_bar_add_sandbox_item' ), 999 ); /** * Fires when the server sandbox is initialized. This action is used to ensure that * the server sandbox action hooks are set up only once. * * @since 1.30.7 */ do_action( 'jetpack_server_sandbox_init' ); } /** * Returns the new url and host values. * * @param string $sandbox Sandbox domain. * @param string $url URL of request about to be made. * @param array $headers Headers of request about to be made. * @param string $data The body of request about to be made. * @param string $method The method of request about to be made. * * @return array [ 'url' => new URL, 'host' => new Host, 'new_signature => New signature if url was changed ] */ public function server_sandbox_request_parameters( $sandbox, $url, $headers, $data = null, $method = 'GET' ) { $host = ''; $new_signature = ''; if ( ! is_string( $sandbox ) || ! is_string( $url ) ) { return array( 'url' => $url, 'host' => $host, 'new_signature' => $new_signature, ); } $url_host = wp_parse_url( $url, PHP_URL_HOST ); switch ( $url_host ) { case 'public-api.wordpress.com': case 'jetpack.wordpress.com': case 'jetpack.com': case 'dashboard.wordpress.com': $host = isset( $headers['Host'] ) ? $headers['Host'] : $url_host; $original_url = $url; $url = preg_replace( '@^(https?://)' . preg_quote( $url_host, '@' ) . '(?=[/?#].*|$)@', '${1}' . $sandbox, $url, 1 ); /** * Whether to add the X Debug query parameter to the request made to the Sandbox * * @since 1.36.0 * * @param bool $add_parameter Whether to add the parameter to the request or not. Default is to false. * @param string $url The URL of the request being made. * @param string $host The host of the request being made. */ if ( apply_filters( 'jetpack_sandbox_add_profile_parameter', false, $url, $host ) ) { $url = add_query_arg( 'XDEBUG_PROFILE', 1, $url ); // URL has been modified since the signature was created. We'll need a new one. $original_url = add_query_arg( 'XDEBUG_PROFILE', 1, $original_url ); $new_signature = $this->get_new_signature( $original_url, $headers, $data, $method ); } } return compact( 'url', 'host', 'new_signature' ); } /** * Gets a new signature for the request * * @param string $url The new URL to be signed. * @param array $headers The headers of the request about to be made. * @param string $data The body of request about to be made. * @param string $method The method of the request about to be made. * @return string|null */ private function get_new_signature( $url, $headers, $data, $method ) { if ( ! empty( $headers['Authorization'] ) ) { $a_headers = $this->extract_authorization_headers( $headers ); if ( ! empty( $a_headers ) ) { $token_details = explode( ':', $a_headers['token'] ); if ( count( $token_details ) === 3 ) { $user_id = $token_details[2]; $token = ( new Tokens() )->get_access_token( $user_id ); $time_diff = (int) \Jetpack_Options::get_option( 'time_diff' ); $jetpack_signature = new \Jetpack_Signature( $token->secret, $time_diff ); $signature = $jetpack_signature->sign_request( $a_headers['token'], $a_headers['timestamp'], $a_headers['nonce'], $a_headers['body-hash'], $method, $url, $data, false ); if ( $signature && ! is_wp_error( $signature ) ) { return $signature; } elseif ( is_wp_error( $signature ) ) { $this->log_new_signature_error( $signature->get_error_message() ); } } else { $this->log_new_signature_error( 'Malformed token on Authorization Header' ); } } else { $this->log_new_signature_error( 'Error extracting Authorization Header' ); } } else { $this->log_new_signature_error( 'Empty Authorization Header' ); } } /** * Logs error if the attempt to create a new signature fails * * @param string $message The error message. * @return void */ private function log_new_signature_error( $message ) { if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { error_log( sprintf( "SANDBOXING: Error re-signing the request. '%s'", $message ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log } } /** * Extract the values in the Authorization header into an array * * @param array $headers The headers of the request about to be made. * @return array|null */ public function extract_authorization_headers( $headers ) { if ( ! empty( $headers['Authorization'] ) && is_string( $headers['Authorization'] ) ) { $header = str_replace( 'X_JETPACK ', '', $headers['Authorization'] ); $vars = explode( ' ', $header ); $result = array(); foreach ( $vars as $var ) { $elements = explode( '"', $var ); if ( count( $elements ) === 3 ) { $result[ substr( $elements[0], 0, -1 ) ] = $elements[1]; } } return $result; } } /** * Modifies parameters of request in order to send the request to the * server specified by `JETPACK__SANDBOX_DOMAIN`. * * Attached to the `requests-requests.before_request` filter. * * @param string $url URL of request about to be made. * @param array $headers Headers of request about to be made. * @param array|string $data Data of request about to be made. * @param string $type Type of request about to be made. * @return void */ public function server_sandbox( &$url, &$headers, &$data = null, &$type = null ) { if ( ! Constants::get_constant( 'JETPACK__SANDBOX_DOMAIN' ) ) { return; } $original_url = $url; $request_parameters = $this->server_sandbox_request_parameters( Constants::get_constant( 'JETPACK__SANDBOX_DOMAIN' ), $url, $headers, $data, $type ); $url = $request_parameters['url']; if ( $request_parameters['host'] ) { $headers['Host'] = $request_parameters['host']; if ( $request_parameters['new_signature'] ) { $headers['Authorization'] = preg_replace( '/signature=\"[^\"]+\"/', 'signature="' . $request_parameters['new_signature'] . '"', $headers['Authorization'] ); } if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { error_log( sprintf( "SANDBOXING via '%s': '%s'", Constants::get_constant( 'JETPACK__SANDBOX_DOMAIN' ), $original_url ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log } } } /** * Adds a "Jetpack API Sandboxed" item to the admin bar if the JETPACK__SANDBOX_DOMAIN * constant is set. * * Attached to the `admin_bar_menu` action. * * @param \WP_Admin_Bar $wp_admin_bar The WP_Admin_Bar instance. */ public function admin_bar_add_sandbox_item( $wp_admin_bar ) { if ( ! Constants::get_constant( 'JETPACK__SANDBOX_DOMAIN' ) ) { return; } $node = array( 'id' => 'jetpack-connection-api-sandbox', 'title' => 'Jetpack API Sandboxed', 'meta' => array( 'title' => 'Sandboxing via ' . Constants::get_constant( 'JETPACK__SANDBOX_DOMAIN' ), ), ); $wp_admin_bar->add_menu( $node ); } } jetpack-connection/src/identity-crisis/class-exception.php 0000777 00000000405 15251741406 0020061 0 ustar 00 <?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 {} jetpack-connection/src/identity-crisis/_inc/admin.jsx 0000777 00000002763 15251741406 0017006 0 ustar 00 import { IDCScreen } from '@automattic/jetpack-idc'; import * as WPElement from '@wordpress/element'; import './admin-bar.scss'; import './style.scss'; /** * The initial renderer function. */ function render() { if ( ! Object.hasOwn( window, '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={ Object.hasOwn( consumerData, 'customContent' ) ? consumerData.customContent : {} } isAdmin={ isAdmin } logo={ Object.hasOwn( consumerData, 'logo' ) ? consumerData.logo : undefined } possibleDynamicSiteUrlDetected={ possibleDynamicSiteUrlDetected } isDevelopmentSite={ isDevelopmentSite } /> ); WPElement.createRoot( container ).render( component ); } } window.addEventListener( 'load', () => render() ); jetpack-connection/src/identity-crisis/class-url-secret.php 0000777 00000006200 15251741406 0020147 0 ustar 00 <?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; } } jetpack-connection/src/identity-crisis/class-rest-endpoints.php 0000777 00000022445 15251741406 0021051 0 ustar 00 <?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; } } jetpack-connection/src/identity-crisis/class-ui.php 0000777 00000013541 15251741406 0016505 0 ustar 00 <?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( $consumer['customContent'] ) && is_callable( $consumer['customContent'] ) ) { $consumer['customContent'] = call_user_func( $consumer['customContent'] ); } if ( isset( $_SERVER['REQUEST_URI'] ) && str_starts_with( filter_var( wp_unslash( $_SERVER['REQUEST_URI'] ) ), $consumer['admin_page'] ) && strlen( $consumer['admin_page'] ) > $consumer_url_length ) { $consumer_chosen = $consumer; $consumer_url_length = strlen( $consumer['admin_page'] ); } } unset( $consumer ); static::$consumers = $consumer_chosen ? $consumer_chosen : array_shift( $consumers ); return static::$consumers; } /** * Get the site platform. * * @return string */ private static function get_platform() { $host = new Host(); if ( $host->is_woa_site() ) { return 'woa'; } if ( $host->is_vip_site() ) { return 'vip'; } if ( $host->is_newspack_site() ) { return 'newspack'; } return 'self-hosted'; } } jetpack-connection/src/identity-crisis/class-identity-crisis.php 0000777 00000060673 15251741406 0021223 0 ustar 00 <?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 ) { // Only checking the database option to limit the effect. if ( get_option( 'jetpack_offline_mode' ) ) { $response['offline_mode'] = '1'; return $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; } } jetpack-connection/src/class-connection-notice.php 0000777 00000017631 15251741406 0016367 0 ustar 00 <?php /** * Admin connection notices. * * @package automattic/jetpack-admin-ui */ namespace Automattic\Jetpack\Connection; use Automattic\Jetpack\Redirect; use Automattic\Jetpack\Tracking; /** * Admin connection notices. * * @phan-constructor-used-for-side-effects */ class Connection_Notice { /** * Whether the class has been initialized. * * @var bool */ private static $is_initialized = false; /** * The constructor. */ public function __construct() { if ( ! static::$is_initialized ) { add_action( 'current_screen', array( $this, 'initialize_notices' ) ); static::$is_initialized = true; } } /** * Initialize the notices if needed. * * @param \WP_Screen $screen WP Core's screen object. * * @return void */ public function initialize_notices( $screen ) { if ( in_array( $screen->id, array( 'jetpack_page_akismet-key-config', 'admin_page_jetpack_modules', ), true ) ) { return; } /* * phpcs:disable WordPress.Security.NonceVerification.Recommended * * This function is firing within wp-admin and checks (below) if it is in the midst of a deletion on the users * page. Nonce will be already checked by WordPress, so we do not need to check ourselves. */ if ( isset( $screen->base ) && 'users' === $screen->base && isset( $_REQUEST['action'] ) && 'delete' === $_REQUEST['action'] ) { add_action( 'admin_notices', array( $this, 'delete_user_update_connection_owner_notice' ) ); } } /** * This is an entire admin notice dedicated to messaging and handling of the case where a user is trying to delete * the connection owner. */ public function delete_user_update_connection_owner_notice() { // Get connection owner or bail. $connection_manager = new Manager(); $connection_owner_id = $connection_manager->get_connection_owner_id(); if ( ! $connection_owner_id ) { return; } $connection_owner_userdata = get_userdata( $connection_owner_id ); // Bail if we're not trying to delete connection owner. $user_ids_to_delete = array(); if ( isset( $_REQUEST['users'] ) ) { $user_ids_to_delete = array_map( 'sanitize_text_field', wp_unslash( $_REQUEST['users'] ) ); } elseif ( isset( $_REQUEST['user'] ) ) { $user_ids_to_delete[] = sanitize_text_field( wp_unslash( $_REQUEST['user'] ) ); } // phpcs:enable $user_ids_to_delete = array_map( 'absint', $user_ids_to_delete ); $deleting_connection_owner = in_array( $connection_owner_id, (array) $user_ids_to_delete, true ); if ( ! $deleting_connection_owner ) { return; } // Bail if they're trying to delete themselves to avoid confusion. if ( get_current_user_id() === $connection_owner_id ) { return; } $tracking = new Tracking(); // Track it! if ( method_exists( $tracking, 'record_user_event' ) ) { $tracking->record_user_event( 'delete_connection_owner_notice_view' ); } $connected_admins = $connection_manager->get_connected_users( 'jetpack_disconnect' ); $user = is_a( $connection_owner_userdata, 'WP_User' ) ? esc_html( $connection_owner_userdata->data->user_login ) : ''; echo "<div class='notice notice-warning' id='jetpack-notice-switch-connection-owner'>"; echo '<h2>' . esc_html__( 'Important notice about your Jetpack connection:', 'jetpack-connection' ) . '</h2>'; echo '<p>' . sprintf( /* translators: WordPress User, if available. */ esc_html__( 'Warning! You are about to delete the Jetpack connection owner (%s) for this site, which may cause some of your Jetpack features to stop working.', 'jetpack-connection' ), esc_html( $user ) ) . '</p>'; if ( ! empty( $connected_admins ) && count( $connected_admins ) > 1 ) { echo '<form id="jp-switch-connection-owner" action="" method="post">'; echo "<label for='owner'>" . esc_html__( 'You can choose to transfer connection ownership to one of these already-connected admins:', 'jetpack-connection' ) . ' </label>'; $connected_admin_ids = array_map( function ( $connected_admin ) { return $connected_admin->ID; }, $connected_admins ); wp_dropdown_users( array( 'name' => 'owner', 'include' => array_diff( $connected_admin_ids, array( $connection_owner_id ) ), 'show' => 'display_name_with_login', ) ); echo '<p>'; submit_button( esc_html__( 'Set new connection owner', 'jetpack-connection' ), 'primary', 'jp-switch-connection-owner-submit', false ); echo '</p>'; echo "<div id='jp-switch-user-results'></div>"; echo '</form>'; ?> <script type="text/javascript"> ( function() { const switchOwnerButton = document.getElementById('jp-switch-connection-owner'); if ( ! switchOwnerButton ) { return; } switchOwnerButton.addEventListener( 'submit', function ( e ) { e.preventDefault(); const submitBtn = document.getElementById('jp-switch-connection-owner-submit'); submitBtn.disabled = true; const results = document.getElementById('jp-switch-user-results'); results.innerHTML = ''; results.classList.remove( 'error-message' ); const handleAPIError = ( message ) => { submitBtn.disabled = false; results.classList.add( 'error-message' ); results.innerHTML = message || "<?php esc_html_e( 'Something went wrong. Please try again.', 'jetpack-connection' ); ?>"; } fetch( <?php echo wp_json_encode( esc_url_raw( get_rest_url() . 'jetpack/v4/connection/owner' ), JSON_HEX_TAG | JSON_HEX_AMP ); ?>, { method: 'POST', headers: { 'X-WP-Nonce': <?php echo wp_json_encode( wp_create_nonce( 'wp_rest' ), JSON_HEX_TAG | JSON_HEX_AMP ); ?>, }, body: new URLSearchParams( new FormData( this ) ), } ) .then( response => response.json() ) .then( data => { if ( data.hasOwnProperty( 'code' ) && data.code === 'success' ) { // Owner successfully changed. results.innerHTML = <?php echo wp_json_encode( esc_html__( 'Success!', 'jetpack-connection' ), JSON_HEX_TAG | JSON_HEX_AMP ); ?>; setTimeout(function () { document.getElementById( 'jetpack-notice-switch-connection-owner' ).style.display = 'none'; }, 1000); return; } handleAPIError( data?.message ); } ) .catch( () => handleAPIError() ); }); } )(); </script> <?php } else { echo '<p>' . esc_html__( 'Every Jetpack site needs at least one connected admin for the features to work properly. Please connect to your WordPress.com account via the button below. Once you connect, you may refresh this page to see an option to change the connection owner.', 'jetpack-connection' ) . '</p>'; $connect_url = $connection_manager->get_authorization_url(); $connect_url = add_query_arg( 'from', 'delete_connection_owner_notice', $connect_url ); echo "<a href='" . esc_url( $connect_url ) . "' target='_blank' rel='noopener noreferrer' class='button-primary'>" . esc_html__( 'Connect to WordPress.com', 'jetpack-connection' ) . '</a>'; } echo '<p>'; printf( wp_kses( /* translators: URL to Jetpack support doc regarding the primary user. */ __( "<a href='%s' target='_blank' rel='noopener noreferrer'>Learn more</a> about the connection owner and what will break if you do not have one.", 'jetpack-connection' ), array( 'a' => array( 'href' => true, 'target' => true, 'rel' => true, ), ) ), esc_url( Redirect::get_url( 'jetpack-support-primary-user' ) ) ); echo '</p>'; echo '<p>'; printf( wp_kses( /* translators: URL to contact Jetpack support. */ __( 'As always, feel free to <a href="%s" target="_blank" rel="noopener noreferrer">contact our support team</a> if you have any questions.', 'jetpack-connection' ), array( 'a' => array( 'href' => true, 'target' => true, 'rel' => true, ), ) ), esc_url( Redirect::get_url( 'jetpack-contact-support' ) ) ); echo '</p>'; echo '</div>'; } } jetpack-connection/src/interface-storage-provider.php 0000777 00000004106 15251741406 0017071 0 ustar 00 <?php /** * Storage Provider Interface for External Storage. * * Defines the contract that all external storage providers must implement * to be compatible with the Jetpack Connection External Storage system. * * @package automattic/jetpack-connection */ namespace Automattic\Jetpack\Connection; /** * Interface for external storage providers. * * All storage providers must implement this interface to ensure * compatibility with the External_Storage system. * * @since 6.18.0 */ interface Storage_Provider_Interface { /** * Check if the storage provider is available in the current environment. * * This method should return true if the storage backend is accessible * and ready to handle requests, false otherwise. * * @since 6.18.0 * * @return bool True if storage is available, false otherwise. */ public function is_available(); /** * Determine if this provider should handle the given option. * * This method allows providers to selectively handle certain options * based on their configuration, environment, or other criteria. * * @since 6.18.0 * * @param string $option_name The name of the option to check. * @return bool True if this provider should handle the option, false otherwise. */ public function should_handle( $option_name ); /** * Retrieve a value from external storage. * * This method should return the value from external storage, or null * if the value is not found or cannot be retrieved. * * @since 6.18.0 * * @param string $option_name The name of the option to retrieve. * @return mixed The option value, or null if not found/available. * @throws \Exception If there's an error retrieving the value. */ public function get( $option_name ); /** * Get the environment identifier for this provider. * * This method should return a unique identifier for the environment * or storage type (e.g., 'atomic', 'vip', 'kubernetes', etc.). * Used for logging and debugging purposes. * * @since 6.18.0 * * @return string The environment identifier. */ public function get_environment_id(); } jetpack-connection/src/class-manager.php 0000777 00000256135 15251741406 0014367 0 ustar 00 <?php /** * The Jetpack Connection manager class file. * * @package automattic/jetpack-connection */ 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; /** * The Jetpack Connection Manager class that is used as a single gateway between WordPress.com * and Jetpack. */ class Manager { /** * A copy of the raw POST data for signature verification purposes. * * @var string */ protected $raw_post_data; /** * Verification data needs to be stored to properly verify everything. * * @var Object */ private $xmlrpc_verification = null; /** * Plugin management object. * * @var Plugin */ private $plugin = null; /** * Error handler object. * * @var Error_Handler */ public $error_handler = null; /** * Jetpack_XMLRPC_Server object * * @var Jetpack_XMLRPC_Server */ public $xmlrpc_server = null; /** * Holds extra parameters that will be sent along in the register request body. * * Use Manager::add_register_request_param to add values to this array. * * @since 1.26.0 * @var array */ 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(); /** * Cached connection status. * * @var bool|null True if the site is connected, false if not, null if not determined yet. */ private static $is_connected = null; /** * Memoized user ID of the connection owner. * If undefined or invalid, set to 0. * * @var null|int */ private static $connection_owner_id = null; /** * Tracks whether connection status invalidation hooks have been added. * * @var bool */ private static $connection_invalidators_added = false; /** * Initialize the object. * Make sure to call the "Configure" first. * * @param string $plugin_slug Slug of the plugin using the connection (optional, but encouraged). * * @see \Automattic\Jetpack\Config */ public function __construct( $plugin_slug = null ) { if ( $plugin_slug && is_string( $plugin_slug ) ) { $this->set_plugin_instance( new Plugin( $plugin_slug ) ); } } /** * Initializes required listeners. This is done separately from the constructors * because some objects sometimes need to instantiate separate objects of this class. * * @todo Implement a proper nonce verification. */ public static function configure() { $manager = new self(); add_filter( 'jetpack_constant_default_value', __NAMESPACE__ . '\Utils::jetpack_api_constant_filter', 10, 2 ); $manager->setup_xmlrpc_handlers( null, $manager->has_connected_owner(), $manager->verify_xml_rpc_signature() ); $manager->error_handler = Error_Handler::get_instance(); if ( $manager->is_connected() ) { add_filter( 'xmlrpc_methods', array( $manager, 'public_xmlrpc_methods' ) ); add_filter( 'shutdown', array( new Package_Version_Tracker(), 'maybe_update_package_versions' ) ); } // This runs on priority 11 - at least one api method in the connection package is set to override a previously // existing method from the Jetpack plugin. Running later than Jetpack's api init ensures the override is successful. add_action( 'rest_api_init', array( $manager, 'initialize_rest_api_registration_connector' ), 11 ); ( new Nonce_Handler() )->init_schedule(); add_action( 'plugins_loaded', __NAMESPACE__ . '\Plugin_Storage::configure', 100 ); add_filter( 'map_meta_cap', array( $manager, 'jetpack_connection_custom_caps' ), 1, 4 ); Heartbeat::init(); add_filter( 'jetpack_heartbeat_stats_array', array( $manager, 'add_stats_to_heartbeat' ) ); 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 ); // Add hooks for cleaning up account mismatch transients $user_account_status = new User_Account_Status(); add_action( 'delete_user', array( $user_account_status, 'clean_account_mismatch_transients' ), 9, 1 ); add_action( 'remove_user_from_blog', array( $user_account_status, 'clean_account_mismatch_transients' ), 9, 1 ); add_action( 'user_register', array( $user_account_status, 'clean_account_mismatch_transients' ), 9, 1 ); add_action( 'profile_update', array( $user_account_status, 'clean_account_mismatch_transients' ), 9, 1 ); $manager->add_connection_status_invalidation_hooks(); // Set up package version hook. add_filter( 'jetpack_package_versions', __NAMESPACE__ . '\Package_Version::send_package_version_to_tracker' ); if ( defined( 'JETPACK__SANDBOX_DOMAIN' ) && JETPACK__SANDBOX_DOMAIN ) { ( new Server_Sandbox() )->init(); } // Initialize connection notices. new Connection_Notice(); // Initialize token locks. new Tokens_Locks(); // Initial Partner management. Partner::init(); } /** * Adds hooks to invalidate the memoized connection status. */ private function add_connection_status_invalidation_hooks() { if ( self::$connection_invalidators_added ) { return; } // Force is_connected() to recompute after important actions. add_action( 'jetpack_site_registered', array( $this, 'reset_connection_status' ) ); add_action( 'jetpack_site_disconnected', array( $this, 'reset_connection_status' ) ); add_action( 'jetpack_sync_register_user', array( $this, 'reset_connection_status' ) ); add_action( 'pre_update_jetpack_option_id', array( $this, 'reset_connection_status' ) ); add_action( 'pre_update_jetpack_option_blog_token', array( $this, 'reset_connection_status' ) ); add_action( 'pre_update_jetpack_option_user_token', array( $this, 'reset_connection_status' ) ); add_action( 'pre_update_jetpack_option_user_tokens', array( $this, 'reset_connection_status' ) ); add_action( 'pre_update_jetpack_option_master_user', array( $this, 'reset_connection_status' ) ); // phpcs:ignore WPCUT.SwitchBlog.SwitchBlog -- wpcom flags **every** use of switch_blog, apparently expecting valid instances to ignore or suppress the sniff. add_action( 'switch_blog', array( $this, 'reset_connection_status' ) ); self::$connection_invalidators_added = true; } /** * Sets up the XMLRPC request handlers. * * @since 1.25.0 Deprecate $is_active param. * @since 2.8.4 Deprecate $request_params param. * * @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( $deprecated, $has_connected_owner, $is_signed, ?Jetpack_XMLRPC_Server $xmlrpc_server = null ) { add_filter( 'xmlrpc_blog_options', array( $this, 'xmlrpc_options' ), 1000, 2 ); 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. // 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 ); } add_action( 'template_redirect', array( $this, 'alternate_xmlrpc' ) ); add_filter( 'xmlrpc_methods', array( $this, 'remove_non_jetpack_xmlrpc_methods' ), 1000 ); } 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->require_jetpack_authentication(); if ( $is_signed ) { // If the site is connected either at a site or user level and the request is signed, expose the methods. // The callback is responsible to determine whether the request is signed with blog or user token and act accordingly. // The actual API methods. $callback = array( $this->xmlrpc_server, 'xmlrpc_methods' ); // Hack to preserve $HTTP_RAW_POST_DATA. add_filter( 'xmlrpc_methods', array( $this, 'xmlrpc_methods' ) ); } elseif ( $has_connected_owner ) { // The jetpack.authorize method should be available for unauthenticated users on a site with an // active Jetpack connection, so that additional users can link their account. $callback = array( $this->xmlrpc_server, 'authorize_xmlrpc_methods' ); } else { // Any other unsigned request should expose the bootstrap methods. $callback = array( $this->xmlrpc_server, 'bootstrap_xmlrpc_methods' ); new XMLRPC_Connector( $this ); } add_filter( 'xmlrpc_methods', $callback ); // Now that no one can authenticate, and we're whitelisting all XML-RPC methods, force enable_xmlrpc on. add_filter( 'pre_option_enable_xmlrpc', '__return_true' ); return true; } /** * Initializes the REST API connector on the init hook. */ public function initialize_rest_api_registration_connector() { new REST_Connector( $this ); } /** * Since a lot of hosts use a hammer approach to "protecting" WordPress sites, * and just blanket block all requests to /xmlrpc.php, or apply other overly-sensitive * security/firewall policies, we provide our own alternate XML RPC API endpoint * which is accessible via a different URI. Most of the below is copied directly * 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. $_COOKIE = array(); include_once ABSPATH . 'wp-admin/includes/admin.php'; include_once ABSPATH . WPINC . '/class-IXR.php'; include_once ABSPATH . WPINC . '/class-wp-xmlrpc-server.php'; /** * Filters the class used for handling XML-RPC requests. * * @since 1.7.0 * @since-jetpack 3.1.0 * * @param string $class The name of the XML-RPC server class. */ $wp_xmlrpc_server_class = apply_filters( 'wp_xmlrpc_server_class', 'wp_xmlrpc_server' ); $wp_xmlrpc_server = new $wp_xmlrpc_server_class(); // Fire off the request. nocache_headers(); $wp_xmlrpc_server->serve_request(); exit( 0 ); } /** * Removes all XML-RPC methods that are not `jetpack.*`. * Only used in our alternate XML-RPC endpoint, where we want to * ensure that Core and other plugins' methods are not exposed. * * @param array $methods a list of registered WordPress XMLRPC methods. * @return array filtered $methods */ public function remove_non_jetpack_xmlrpc_methods( $methods ) { $jetpack_methods = array(); foreach ( $methods as $method => $callback ) { if ( str_starts_with( $method, 'jetpack.' ) ) { $jetpack_methods[ $method ] = $callback; } } return $jetpack_methods; } /** * Removes all other authentication methods not to allow other * methods to validate unauthenticated requests. */ public function require_jetpack_authentication() { // Don't let anyone authenticate. $_COOKIE = array(); remove_all_filters( 'authenticate' ); remove_all_actions( 'wp_login_failed' ); if ( $this->is_connected() ) { // Allow Jetpack authentication. add_filter( 'authenticate', array( $this, 'authenticate_jetpack' ), 10, 3 ); } } /** * Authenticates XML-RPC and other requests from the Jetpack Server * * @param WP_User|mixed $user user object if authenticated. * @param string $username username. * @param string $password password string. * @return WP_User|mixed authenticated user or error. */ public function authenticate_jetpack( $user, $username, $password ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable if ( is_a( $user, '\\WP_User' ) ) { return $user; } $token_details = $this->verify_xml_rpc_signature(); if ( ! $token_details ) { return $user; } if ( 'user' !== $token_details['type'] ) { return $user; } if ( ! $token_details['user_id'] ) { return $user; } nocache_headers(); return new \WP_User( $token_details['user_id'] ); } /** * Verifies the signature of the current request. * * @return false|array */ public function verify_xml_rpc_signature() { if ( $this->xmlrpc_verification === null ) { $this->xmlrpc_verification = $this->internal_verify_xml_rpc_signature(); if ( is_wp_error( $this->xmlrpc_verification ) ) { /** * Action for logging XMLRPC signature verification errors. This data is sensitive. * * @since 1.7.0 * @since-jetpack 7.5.0 * * @param WP_Error $signature_verification_error The verification error */ do_action( 'jetpack_verify_signature_error', $this->xmlrpc_verification ); Error_Handler::get_instance()->report_error( $this->xmlrpc_verification ); } } return is_wp_error( $this->xmlrpc_verification ) ? false : $this->xmlrpc_verification; } /** * Verifies the signature of the current request. * * This function has side effects and should not be used. Instead, * use the memoized version `->verify_xml_rpc_signature()`. * * @internal * @todo Refactor to use proper nonce verification. */ private function internal_verify_xml_rpc_signature() { // phpcs:disable WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized // It's not for us. if ( ! isset( $_GET['token'] ) || empty( $_GET['signature'] ) ) { return false; } $signature_details = array( 'token' => isset( $_GET['token'] ) ? wp_unslash( $_GET['token'] ) : '', 'timestamp' => isset( $_GET['timestamp'] ) ? wp_unslash( $_GET['timestamp'] ) : '', 'nonce' => isset( $_GET['nonce'] ) ? wp_unslash( $_GET['nonce'] ) : '', 'body_hash' => isset( $_GET['body-hash'] ) ? wp_unslash( $_GET['body-hash'] ) : '', 'method' => isset( $_SERVER['REQUEST_METHOD'] ) ? wp_unslash( $_SERVER['REQUEST_METHOD'] ) : null, 'url' => wp_unslash( ( isset( $_SERVER['HTTP_HOST'] ) ? $_SERVER['HTTP_HOST'] : null ) . ( isset( $_SERVER['REQUEST_URI'] ) ? $_SERVER['REQUEST_URI'] : null ) ), // Temp - will get real signature URL later. 'signature' => isset( $_GET['signature'] ) ? wp_unslash( $_GET['signature'] ) : '', ); $error_type = 'xmlrpc'; // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged @list( $token_key, $version, $user_id ) = explode( ':', wp_unslash( $_GET['token'] ) ); // phpcs:enable WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $jetpack_api_version = Constants::get_constant( 'JETPACK__API_VERSION' ); if ( empty( $token_key ) || empty( $version ) || (string) $jetpack_api_version !== $version ) { return new \WP_Error( 'malformed_token', 'Malformed token in request', compact( 'signature_details', 'error_type' ) ); } if ( '0' === $user_id ) { $token_type = 'blog'; $user_id = 0; } else { $token_type = 'user'; if ( empty( $user_id ) || ! ctype_digit( $user_id ) ) { return new \WP_Error( 'malformed_user_id', 'Malformed user_id in request', compact( 'signature_details', 'error_type' ) ); } $user_id = (int) $user_id; $user = new \WP_User( $user_id ); if ( ! $user->exists() ) { return new \WP_Error( 'unknown_user', sprintf( 'User %d does not exist', $user_id ), compact( 'signature_details', 'error_type' ) ); } } $token = $this->get_tokens()->get_access_token( $user_id, $token_key, false ); if ( is_wp_error( $token ) ) { $token->add_data( compact( 'signature_details', 'error_type' ) ); return $token; } elseif ( ! $token ) { return new \WP_Error( 'unknown_token', sprintf( 'Token %s:%s:%d does not exist', $token_key, $version, $user_id ), compact( 'signature_details', 'error_type' ) ); } $jetpack_signature = new \Jetpack_Signature( $token->secret, (int) \Jetpack_Options::get_option( 'time_diff' ) ); // 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; // 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 ( ! str_starts_with( $post_data_key, '_jetpack_file_hmac_' ) ) { continue; } $post_data_key = substr( $post_data_key, strlen( '_jetpack_file_hmac_' ) ); $file_hashes[ $post_data_key ] = $post_data_value; } foreach ( $file_hashes as $post_data_key => $post_data_value ) { unset( $post_data[ "_jetpack_file_hmac_{$post_data_key}" ] ); $post_data[ $post_data_key ] = $post_data_value; } ksort( $post_data ); $body = http_build_query( stripslashes_deep( $post_data ) ); } elseif ( $this->raw_post_data === null ) { $body = file_get_contents( 'php://input' ); } else { $body = null; } // phpcs:enable $signature = $jetpack_signature->sign_current_request( array( 'body' => $body === null ? $this->raw_post_data : $body ) ); $signature_details['url'] = $jetpack_signature->current_request_url; if ( ! $signature ) { return new \WP_Error( 'could_not_sign', 'Unknown signature error', compact( 'signature_details', 'error_type' ) ); } elseif ( is_wp_error( $signature ) ) { return $signature; } // phpcs:disable WordPress.Security.NonceVerification.Recommended $timestamp = (int) $_GET['timestamp']; $nonce = wp_unslash( (string) $_GET['nonce'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- WP Core doesn't sanitize nonces either. // phpcs:enable WordPress.Security.NonceVerification.Recommended // Use up the nonce regardless of whether the signature matches. if ( ! ( new Nonce_Handler() )->add( $timestamp, $nonce ) ) { return new \WP_Error( 'invalid_nonce', 'Could not add nonce', compact( 'signature_details', 'error_type' ) ); } // Be careful about what you do with this debugging data. // If a malicious requester has access to the expected signature, // bad things might be possible. $signature_details['expected'] = $signature; // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( ! hash_equals( $signature, wp_unslash( $_GET['signature'] ) ) ) { return new \WP_Error( 'signature_mismatch', 'Signature mismatch', compact( 'signature_details', 'error_type' ) ); } /** * Action for additional token checking. * * @since 1.7.0 * @since-jetpack 7.7.0 * * @param array $post_data request data. * @param array $token_data token data. */ return apply_filters( 'jetpack_signature_check_token', array( 'type' => $token_type, 'token_key' => $token_key, 'user_id' => $token->external_user_id, ), $token, $this->raw_post_data ); } /** * Returns true if the current site is connected to WordPress.com and has the minimum requirements to enable Jetpack UI. * * This method is deprecated since version 1.25.0 of this package. Please use has_connected_owner instead. * * Since this method has a wide spread use, we decided not to throw any deprecation warnings for now. * * @deprecated 1.25.0 * @see Manager::has_connected_owner * @return bool is the site connected? */ public function is_active() { return (bool) $this->get_tokens()->get_access_token( true ); } /** * Obtains an instance of the Tokens class. * * @return Tokens the Tokens object */ public function get_tokens() { return new Tokens(); } /** * Returns true if the site has both a token and a blog id, which indicates a site has been registered. * * @access public * @deprecated 1.12.1 Use is_connected instead * @see Manager::is_connected * * @return bool */ public function is_registered() { _deprecated_function( __METHOD__, '1.12.1' ); return $this->is_connected(); } /** * Returns true if the site has both a token and a blog id, which indicates a site has been connected. * * @access public * @since 1.21.1 * * @return bool */ public function is_connected() { if ( self::$is_connected === null ) { if ( ! self::$connection_invalidators_added ) { $this->add_connection_status_invalidation_hooks(); } $has_blog_id = (bool) \Jetpack_Options::get_option( 'id' ); if ( $has_blog_id ) { self::$is_connected = (bool) $this->get_tokens()->get_access_token(); } else { // Short-circuit, no need to check for tokens if there's no blog ID. self::$is_connected = false; } } return self::$is_connected; } /** * Resets the memoized connection status. * This will force the connection status to be recomputed on the next check. * * @since 5.0.0 */ public function reset_connection_status() { self::$is_connected = null; self::$connection_owner_id = null; } /** * Returns true if the site has at least one connected administrator. * * @access public * @since 1.21.1 * * @return bool */ public function has_connected_admin() { return (bool) count( $this->get_connected_users( 'manage_options' ) ); } /** * Returns true if the site has any connected user. * * @access public * @since 1.21.1 * * @return bool */ public function has_connected_user() { return (bool) count( $this->get_connected_users( 'any', 1 ) ); } /** * Returns an array of users that have user tokens for communicating with wpcom. * Able to select by specific capability. * * @since 9.9.1 Added $limit parameter. * * @param string $capability The capability of the user. * @param int|null $limit How many connected users to get before returning. * @return WP_User[] Array of WP_User objects if found. */ public function get_connected_users( $capability = 'any', $limit = null ) { $connected_users = array(); $user_tokens = $this->get_tokens()->get_user_tokens(); if ( ! is_array( $user_tokens ) || empty( $user_tokens ) ) { return $connected_users; } $connected_user_ids = array_keys( $user_tokens ); if ( ! empty( $connected_user_ids ) ) { foreach ( $connected_user_ids as $id ) { // Check for capability. if ( 'any' !== $capability && ! user_can( $id, $capability ) ) { continue; } $user_data = get_userdata( $id ); if ( $user_data instanceof \WP_User ) { $connected_users[] = $user_data; if ( $limit && count( $connected_users ) >= $limit ) { return $connected_users; } } } } return $connected_users; } /** * Returns true if the site has a connected Blog owner (master_user). * * @access public * @since 1.21.1 * * @return bool */ public function has_connected_owner() { return (bool) $this->get_connection_owner_id(); } /** * Returns true if the site is connected only at a site level. * * Note that we are explicitly checking for the existence of the master_user option in order to account for cases where we don't have any user tokens (user-level connection) but the master_user option is set, which could be the result of a problematic user connection. * * @access public * @since 1.25.0 * @deprecated 1.27.0 * * @return bool */ public function is_userless() { _deprecated_function( __METHOD__, '1.27.0', 'Automattic\\Jetpack\\Connection\\Manager::is_site_connection' ); return $this->is_site_connection(); } /** * Returns true if the site is connected only at a site level. * * Note that we are explicitly checking for the existence of the master_user option in order to account for cases where we don't have any user tokens (user-level connection) but the master_user option is set, which could be the result of a problematic user connection. * * @access public * @since 1.27.0 * * @return bool */ public function is_site_connection() { return $this->is_connected() && ! $this->has_connected_user() && ! \Jetpack_Options::get_option( 'master_user' ); } /** * Checks to see if the connection owner of the site is missing. * * @return bool */ public function is_missing_connection_owner() { $connection_owner = $this->get_connection_owner_id(); if ( ! get_user_by( 'id', $connection_owner ) ) { return true; } return false; } /** * Returns true if the user with the specified identifier is connected to * WordPress.com. * * @param int $user_id the user identifier. Default is the current user. * @return bool Boolean is the user connected? */ public function is_user_connected( $user_id = false ) { $user_id = false === $user_id ? get_current_user_id() : absint( $user_id ); if ( ! $user_id ) { return false; } return (bool) $this->get_tokens()->get_access_token( $user_id ); } /** * Returns the local user ID of the connection owner. * * @return bool|int Returns the ID of the connection owner or False if no connection owner found. */ public function get_connection_owner_id() { // Check if the memoized value is available. if ( null === self::$connection_owner_id ) { $owner = $this->get_connection_owner(); self::$connection_owner_id = $owner instanceof \WP_User ? $owner->ID : 0; } // If the ID is set to 0, there's no valid connection owner. return self::$connection_owner_id > 0 ? self::$connection_owner_id : false; } /** * Get the wpcom user data of the current|specified connected user. * * @todo Refactor to properly load the XMLRPC client independently. * * @param int|null $user_id the user identifier. * @return bool|array An array with the WPCOM user data on success, false otherwise. */ public function get_connected_user_data( $user_id = null ) { if ( ! $user_id ) { $user_id = get_current_user_id(); } // Check if the user is connected and return false otherwise. if ( ! $this->is_user_connected( $user_id ) ) { return false; } $transient_key = "jetpack_connected_user_data_$user_id"; $cached_user_data = get_transient( $transient_key ); if ( $cached_user_data ) { return $cached_user_data; } $xml = new Jetpack_IXR_Client( array( 'user_id' => $user_id, ) ); $xml->query( 'wpcom.getUser' ); if ( ! $xml->isError() ) { $user_data = $xml->getResponse(); set_transient( $transient_key, $xml->getResponse(), DAY_IN_SECONDS ); return $user_data; } return false; } /** * Returns a user object of the connection owner. * * @return WP_User|false False if no connection owner found. */ public function get_connection_owner() { $user_id = \Jetpack_Options::get_option( 'master_user' ); if ( ! $user_id ) { return false; } // Make sure user is connected. $user_token = $this->get_tokens()->get_access_token( $user_id ); $connection_owner = false; if ( $user_token && is_object( $user_token ) && isset( $user_token->external_user_id ) ) { $connection_owner = get_userdata( $user_token->external_user_id ); } if ( $connection_owner === false ) { Error_Handler::get_instance()->report_error( new WP_Error( 'invalid_connection_owner', 'Invalid connection owner', array( 'user_id' => $user_id, 'has_user_token' => (bool) $user_token, 'error_type' => 'connection', 'signature_details' => array( 'token' => '', ), ) ), false, true ); } return $connection_owner; } /** * Returns true if the provided user is the Jetpack connection owner. * If user ID is not specified, the current user will be used. * * @param int|bool $user_id the user identifier. False for current user. * @return bool True the user the connection owner, false otherwise. */ public function is_connection_owner( $user_id = false ) { if ( ! $user_id ) { $user_id = get_current_user_id(); } return ( (int) $user_id ) === $this->get_connection_owner_id(); } /** * Connects the user with a specified ID to a WordPress.com user using the * remote login flow. * * @access public * * @param int|null $user_id (optional) the user identifier, defaults to current user. * @param string|null $redirect_url the URL to redirect the user to for processing, defaults to * admin_url(). * @return WP_Error only in case of a failed user lookup. */ public function connect_user( $user_id = null, $redirect_url = null ) { $user = null; if ( null === $user_id ) { $user = wp_get_current_user(); } else { $user = get_user_by( 'ID', $user_id ); } if ( empty( $user ) ) { return new \WP_Error( 'user_not_found', 'Attempting to connect a non-existent user.' ); } if ( null === $redirect_url ) { $redirect_url = admin_url(); } // Using wp_redirect intentionally because we're redirecting outside. wp_redirect( $this->get_authorization_url( $user, $redirect_url ) ); // phpcs:ignore WordPress.Security.SafeRedirect exit( 0 ); } /** * Force user disconnect. * * @param int $user_id Local (external) user ID. * @param bool $disconnect_all_users Whether to disconnect all users before disconnecting the primary user. * * @return bool */ public function disconnect_user_force( $user_id, $disconnect_all_users = false ) { if ( ! (int) $user_id ) { // Missing user ID. return false; } // If we are disconnecting the primary user we may need to disconnect all other users first if ( $user_id === $this->get_connection_owner_id() && $disconnect_all_users && ! $this->disconnect_all_users_except_primary() ) { return false; } return $this->disconnect_user( $user_id, true, true ); } /** * Disconnects all users except the primary user. * * @return bool */ public function disconnect_all_users_except_primary() { $all_connected_users = $this->get_connected_users(); foreach ( $all_connected_users as $user ) { // Skip the primary. if ( $user->ID === $this->get_connection_owner_id() ) { continue; } $disconnected = $this->disconnect_user( $user->ID, false, true ); // If we fail to disconnect any user, we should not proceed with disconnecting the primary user. if ( ! $disconnected ) { return false; } } return true; } /** * Unlinks the current user from the linked WordPress.com user. * * @access public * @static * * @todo Refactor to properly load the XMLRPC client independently. * * @param int|null $user_id the user identifier. * @param bool $can_overwrite_primary_user Allow for the primary user to be disconnected. * @param bool $force_disconnect_locally Disconnect user locally even if we were unable to disconnect them from WP.com. * @return bool Whether the disconnection of the user was successful. */ public function disconnect_user( $user_id = null, $can_overwrite_primary_user = false, $force_disconnect_locally = false ) { $user_id = empty( $user_id ) ? get_current_user_id() : (int) $user_id; $is_primary_user = Jetpack_Options::get_option( 'master_user' ) === $user_id; if ( $is_primary_user && ! $can_overwrite_primary_user ) { 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 ); $is_disconnected_locally = false; if ( $is_disconnected_from_wpcom || $force_disconnect_locally ) { // Get the WordPress.com email before disconnecting the user $wpcom_user_data = $this->get_connected_user_data( $user_id ); $wpcom_email = isset( $wpcom_user_data['email'] ) ? $wpcom_user_data['email'] : null; // Disconnect the user locally. $is_disconnected_locally = $this->get_tokens()->disconnect_user( $user_id ); if ( $is_disconnected_locally ) { // Delete cached connected user data. $transient_key = "jetpack_connected_user_data_$user_id"; delete_transient( $transient_key ); // Clean up account mismatch transients for this user if ( $wpcom_email ) { $user_account_status = new User_Account_Status(); $user_account_status->clean_account_mismatch_transients( $wpcom_email ); } /** * Fires after the current user has been unlinked from WordPress.com. * * @since 1.7.0 * @since-jetpack 4.1.0 * * @param int $user_id The current user's ID. */ do_action( 'jetpack_unlinked_user', $user_id ); if ( $is_primary_user ) { Jetpack_Options::delete_option( 'master_user' ); // Clear the memoized connection owner ID since it changed self::$connection_owner_id = null; } } } self::$disconnected_users[] = $user_id; return $is_disconnected_from_wpcom && $is_disconnected_locally; } /** * Request to wpcom for a user to be unlinked from their WordPress.com account * * @param int $user_id The user identifier. * * @return bool Whether the disconnection of the user was successful. */ public function unlink_user_from_wpcom( $user_id ) { // Attempt to disconnect the user from WordPress.com. $xml = new Jetpack_IXR_Client(); $xml->query( 'jetpack.unlink_user', $user_id ); if ( $xml->isError() ) { return false; } return (bool) $xml->getResponse(); } /** * Update the connection owner. * * @since 1.29.0 * * @param int $new_owner_id The ID of the user to become the connection owner. * * @return true|WP_Error True if owner successfully changed, WP_Error otherwise. */ public function update_connection_owner( $new_owner_id ) { $roles = new Roles(); if ( ! user_can( $new_owner_id, $roles->translate_role_to_cap( 'administrator' ) ) ) { return new WP_Error( 'new_owner_not_admin', __( 'New owner is not admin', 'jetpack-connection' ), array( 'status' => 400 ) ); } $old_owner_id = $this->get_connection_owner_id(); if ( $old_owner_id === $new_owner_id ) { return new WP_Error( 'new_owner_is_existing_owner', __( 'New owner is same as existing owner', 'jetpack-connection' ), array( 'status' => 400 ) ); } if ( ! $this->is_user_connected( $new_owner_id ) ) { return new WP_Error( 'new_owner_not_connected', __( 'New owner is not connected', 'jetpack-connection' ), array( 'status' => 400 ) ); } // Notify WPCOM about the connection owner change. $owner_updated_wpcom = $this->update_connection_owner_wpcom( $new_owner_id ); if ( $owner_updated_wpcom ) { // Update the connection owner in Jetpack only if they were successfully updated on WPCOM. // This will ensure consistency with WPCOM. \Jetpack_Options::update_option( 'master_user', $new_owner_id ); // Clear the memoized connection owner ID since it changed self::$connection_owner_id = null; // Track it. ( new Tracking() )->record_user_event( 'set_connection_owner_success' ); return true; } return new WP_Error( 'error_setting_new_owner', __( 'Could not confirm new owner.', 'jetpack-connection' ), array( 'status' => 500 ) ); } /** * Request to WPCOM to update the connection owner. * * @since 1.29.0 * * @param int $new_owner_id The ID of the user to become the connection owner. * * @return bool Whether the ownership transfer was successful. */ public function update_connection_owner_wpcom( $new_owner_id ) { // Notify WPCOM about the connection owner change. $xml = new Jetpack_IXR_Client( array( 'user_id' => get_current_user_id(), ) ); $xml->query( 'jetpack.switchBlogOwner', array( 'new_blog_owner' => $new_owner_id, ) ); if ( $xml->isError() ) { return false; } return (bool) $xml->getResponse(); } /** * Returns the requested Jetpack API URL. * * @param string $relative_url the relative API path. * @return string API URL. */ public function api_url( $relative_url ) { $api_base = Constants::get_constant( 'JETPACK__API_BASE' ); $api_version = '/' . Constants::get_constant( 'JETPACK__API_VERSION' ) . '/'; /** * Filters the API URL that Jetpack uses for server communication. * * @since 1.7.0 * @since-jetpack 8.0.0 * * @param string $url the generated URL. * @param string $relative_url the relative URL that was passed as an argument. * @param string $api_base the API base string that is being used. * @param string $api_version the API version string that is being used. */ return apply_filters( 'jetpack_api_url', rtrim( $api_base . $relative_url, '/\\' ) . $api_version, $relative_url, $api_base, $api_version ); } /** * Returns the Jetpack XMLRPC WordPress.com API endpoint URL. * * @return string XMLRPC API URL. */ public function xmlrpc_api_url() { $base = preg_replace( '#(https?://[^?/]+)(/?.*)?$#', '\\1', Constants::get_constant( 'JETPACK__API_BASE' ) ); return untrailingslashit( $base ) . '/xmlrpc.php'; } /** * Attempts Jetpack registration which sets up the site for connection. Should * remain public because the call to action comes from the current site, not from * WordPress.com. * * @param string $api_endpoint (optional) an API endpoint to use, defaults to 'register'. * @return true|WP_Error The error object. */ public function register( $api_endpoint = 'register' ) { // Clean-up leftover tokens just in-case. // This fixes an edge case that was preventing users to register when the blog token was missing but // there were still leftover user tokens present. $this->delete_all_connection_tokens( true ); add_action( 'pre_update_jetpack_option_register', array( '\\Jetpack_Options', 'delete_option' ) ); $secrets = ( new Secrets() )->generate( 'register', get_current_user_id(), 600 ); if ( false === $secrets ) { return new WP_Error( 'cannot_save_secrets', __( 'Jetpack experienced an issue trying to save options (cannot_save_secrets). We suggest that you contact your hosting provider, and ask them for help checking that the options table is writable on your site.', 'jetpack-connection' ) ); } if ( empty( $secrets['secret_1'] ) || empty( $secrets['secret_2'] ) || empty( $secrets['exp'] ) ) { return new \WP_Error( 'missing_secrets' ); } // Better to try (and fail) to set a higher timeout than this system // supports than to have register fail for more users than it should. $timeout = $this->set_min_time_limit( 60 ) / 2; $gmt_offset = get_option( 'gmt_offset' ); if ( ! $gmt_offset ) { $gmt_offset = 0; } $stats_options = get_option( 'stats_options' ); $stats_id = isset( $stats_options['blog_id'] ) ? $stats_options['blog_id'] : null; /* This action is documented in src/class-package-version-tracker.php */ $package_versions = apply_filters( 'jetpack_package_versions', array() ); $active_plugins_using_connection = Plugin_Storage::get_all(); /** * Filters the request body for additional property addition. * * @since 1.7.0 * @since-jetpack 7.7.0 * * @param array $post_data request data. * @param Array $token_data token data. */ $body = apply_filters( 'jetpack_register_request_body', array_merge( array( 'siteurl' => Urls::site_url(), 'home' => Urls::home_url(), 'gmt_offset' => $gmt_offset, 'timezone_string' => (string) get_option( 'timezone_string' ), 'site_name' => (string) get_option( 'blogname' ), 'secret_1' => $secrets['secret_1'], 'secret_2' => $secrets['secret_2'], 'site_lang' => get_locale(), 'timeout' => $timeout, 'stats_id' => $stats_id, 'state' => get_current_user_id(), 'site_created' => $this->get_assumed_site_creation_date(), 'jetpack_version' => Constants::get_constant( 'JETPACK__VERSION' ), 'ABSPATH' => Constants::get_constant( 'ABSPATH' ), 'current_user_email' => wp_get_current_user()->user_email, 'connect_plugin' => $this->get_plugin() ? $this->get_plugin()->get_slug() : null, 'package_versions' => $package_versions, 'active_connected_plugins' => $active_plugins_using_connection, ), self::$extra_register_params ) ); $args = array( 'method' => 'POST', 'body' => $body, 'headers' => array( 'Accept' => 'application/json', ), 'timeout' => $timeout, ); $args['body'] = static::apply_activation_source_to_args( $args['body'] ); // TODO: fix URLs for bad hosts. $response = Client::_wp_remote_request( $this->api_url( $api_endpoint ), $args, true ); // Make sure the response is valid and does not contain any Jetpack errors. $registration_details = $this->validate_remote_register_response( $response ); if ( is_wp_error( $registration_details ) ) { return $registration_details; } elseif ( ! $registration_details ) { return new \WP_Error( 'unknown_error', 'Unknown error registering your Jetpack site.', wp_remote_retrieve_response_code( $response ) ); } if ( empty( $registration_details->jetpack_secret ) || ! is_string( $registration_details->jetpack_secret ) ) { return new \WP_Error( 'jetpack_secret', 'Unable to validate registration of your Jetpack site.', wp_remote_retrieve_response_code( $response ) ); } if ( isset( $registration_details->jetpack_public ) ) { $jetpack_public = (int) $registration_details->jetpack_public; } else { $jetpack_public = false; } Jetpack_Options::update_options( array( 'id' => (int) $registration_details->jetpack_id, 'public' => $jetpack_public, ) ); update_option( Package_Version_Tracker::PACKAGE_VERSION_OPTION, $package_versions ); $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( 'jetpack_register_site_rest_response', function ( $response ) use ( $alternate_authorization_url ) { $response['alternateAuthorizeUrl'] = $alternate_authorization_url; return $response; } ); /** * Fires when a site is registered on WordPress.com. * * @since 1.7.0 * @since-jetpack 3.7.0 * * @param int $json->jetpack_id Jetpack Blog ID. * @param string $json->jetpack_secret Jetpack Blog Token. * @param int|bool $jetpack_public Is the site public. */ do_action( 'jetpack_site_registered', $registration_details->jetpack_id, $registration_details->jetpack_secret, $jetpack_public ); if ( isset( $registration_details->token ) ) { /** * Fires when a user token is sent along with the registration data. * * @since 1.7.0 * @since-jetpack 7.6.0 * * @param object $token the administrator token for the newly registered site. */ do_action( 'jetpack_site_registered_user_token', $registration_details->token ); } return true; } /** * Attempts Jetpack registration. * * @param bool $tos_agree Whether the user agreed to TOS. * * @return bool|WP_Error */ public function try_registration( $tos_agree = true ) { if ( $tos_agree ) { $terms_of_service = new Terms_Of_Service(); $terms_of_service->agree(); } /** * Action fired when the user attempts the registration. * * @since 1.26.0 */ $pre_register = apply_filters( 'jetpack_pre_register', null ); if ( is_wp_error( $pre_register ) ) { return $pre_register; } $tracking_data = array(); if ( null !== $this->get_plugin() ) { $tracking_data['plugin_slug'] = $this->get_plugin()->get_slug(); } $tracking = new Tracking(); $tracking->record_user_event( 'jpc_register_begin', $tracking_data ); add_filter( 'jetpack_register_request_body', array( Utils::class, 'filter_register_request_body' ) ); $result = $this->register(); remove_filter( 'jetpack_register_request_body', array( Utils::class, 'filter_register_request_body' ) ); // If there was an error with registration and the site was not registered, record this so we can show a message. if ( ! $result || is_wp_error( $result ) ) { return $result; } return true; } /** * Adds a parameter to the register request body * * @since 1.26.0 * * @param string $name The name of the parameter to be added. * @param string $value The value of the parameter to be added. * * @throws \InvalidArgumentException If supplied arguments are not strings. * @return void */ public function add_register_request_param( $name, $value ) { if ( ! is_string( $name ) || ! is_string( $value ) ) { throw new \InvalidArgumentException( 'name and value must be strings' ); } self::$extra_register_params[ $name ] = $value; } /** * Takes the response from the Jetpack register new site endpoint and * verifies it worked properly. * * @since 1.7.0 * @since-jetpack 2.6.0 * * @param mixed $response the response object, or the error object. * @return string|WP_Error A JSON object on success or WP_Error on failures **/ protected function validate_remote_register_response( $response ) { if ( is_wp_error( $response ) ) { return new \WP_Error( 'register_http_request_failed', $response->get_error_message() ); } $code = wp_remote_retrieve_response_code( $response ); $entity = wp_remote_retrieve_body( $response ); if ( $entity ) { $registration_response = json_decode( $entity ); } else { $registration_response = false; } $code_type = (int) ( $code / 100 ); if ( 5 === $code_type ) { return new \WP_Error( 'wpcom_5??', $code ); } elseif ( 408 === $code ) { return new \WP_Error( 'wpcom_408', $code ); } elseif ( ! empty( $registration_response->error ) ) { if ( 'xml_rpc-32700' === $registration_response->error && ! function_exists( 'xml_parser_create' ) ) { $error_description = __( "PHP's XML extension is not available. Jetpack requires the XML extension to communicate with WordPress.com. Please contact your hosting provider to enable PHP's XML extension.", 'jetpack-connection' ); } else { $error_description = isset( $registration_response->error_description ) ? (string) $registration_response->error_description : ''; } return new \WP_Error( (string) $registration_response->error, $error_description, $code ); } elseif ( 200 !== $code ) { return new \WP_Error( 'wpcom_bad_response', $code ); } // Jetpack ID error block. if ( empty( $registration_response->jetpack_id ) ) { return new \WP_Error( 'jetpack_id', /* translators: %s is an error message string */ sprintf( __( 'Error Details: Jetpack ID is empty. Do not publicly post this error message! %s', 'jetpack-connection' ), $entity ), $entity ); } elseif ( ! is_scalar( $registration_response->jetpack_id ) ) { return new \WP_Error( 'jetpack_id', /* translators: %s is an error message string */ sprintf( __( 'Error Details: Jetpack ID is not a scalar. Do not publicly post this error message! %s', 'jetpack-connection' ), $entity ), $entity ); } elseif ( preg_match( '/[^0-9]/', $registration_response->jetpack_id ) ) { return new \WP_Error( 'jetpack_id', /* translators: %s is an error message string */ sprintf( __( 'Error Details: Jetpack ID begins with a numeral. Do not publicly post this error message! %s', 'jetpack-connection' ), $entity ), $entity ); } return $registration_response; } /** * Adds a used nonce to a list of known nonces. * * @param int $timestamp the current request timestamp. * @param string $nonce the nonce value. * @return bool whether the nonce is unique or not. * * @deprecated since 1.24.0 * @see Nonce_Handler::add() */ public function add_nonce( $timestamp, $nonce ) { _deprecated_function( __METHOD__, '1.24.0', 'Automattic\\Jetpack\\Connection\\Nonce_Handler::add' ); return ( new Nonce_Handler() )->add( $timestamp, $nonce ); } /** * Cleans nonces that were saved when calling ::add_nonce. * * @todo Properly prepare the query before executing it. * * @param bool $all whether to clean even non-expired nonces. * * @deprecated since 1.24.0 * @see Nonce_Handler::clean_all() */ public function clean_nonces( $all = false ) { _deprecated_function( __METHOD__, '1.24.0', 'Automattic\\Jetpack\\Connection\\Nonce_Handler::clean_all' ); ( new Nonce_Handler() )->clean_all( $all ? PHP_INT_MAX : ( time() - Nonce_Handler::LIFETIME ) ); } /** * Sets the Connection custom capabilities. * * @param string[] $caps Array of the user's capabilities. * @param string $cap Capability name. * @param int $user_id The user ID. * @param array $args Adds the context to the cap. Typically the object ID. */ public function jetpack_connection_custom_caps( $caps, $cap, $user_id, $args ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable switch ( $cap ) { case 'jetpack_connect': case 'jetpack_reconnect': $is_offline_mode = ( new Status() )->is_offline_mode(); if ( $is_offline_mode ) { $caps = array( 'do_not_allow' ); break; } // Pass through. If it's not offline mode, these should match disconnect. // Let users disconnect if it's offline mode, just in case things glitch. case 'jetpack_disconnect': /** * Filters the jetpack_disconnect capability. * * @since 1.14.2 * * @param array An array containing the capability name. */ $caps = apply_filters( 'jetpack_disconnect_cap', array( 'manage_options' ) ); break; case 'jetpack_connect_user': $is_offline_mode = ( new Status() )->is_offline_mode(); if ( $is_offline_mode ) { $caps = array( 'do_not_allow' ); break; } // With site connections in mind, non-admin users can connect their account only if a connection owner exists. $caps = $this->has_connected_owner() ? array( 'read' ) : array( 'manage_options' ); break; case 'jetpack_unlink_user': $is_offline_mode = ( new Status() )->is_offline_mode(); if ( $is_offline_mode ) { $caps = array( 'do_not_allow' ); break; } // Non-admins can always disconnect $caps = array( 'read' ); break; } return $caps; } /** * Builds the timeout limit for queries talking with the wpcom servers. * * Based on local php max_execution_time in php.ini * * @since 1.7.0 * @since-jetpack 5.4.0 * @return int **/ public function get_max_execution_time() { $timeout = (int) ini_get( 'max_execution_time' ); // Ensure exec time set in php.ini. if ( ! $timeout ) { $timeout = 30; } return $timeout; } /** * Sets a minimum request timeout, and returns the current timeout * * @since 1.7.0 * @since-jetpack 5.4.0 * @param int $min_timeout the minimum timeout value. **/ public function set_min_time_limit( $min_timeout ) { $timeout = $this->get_max_execution_time(); if ( $timeout < $min_timeout ) { $timeout = $min_timeout; set_time_limit( $timeout ); } return $timeout; } /** * Get our assumed site creation date. * Calculated based on the earlier date of either: * - Earliest admin user registration date. * - Earliest date of post of any post type. * * @since 1.7.0 * @since-jetpack 7.2.0 * * @return string Assumed site creation date and time. */ public function get_assumed_site_creation_date() { $cached_date = get_transient( 'jetpack_assumed_site_creation_date' ); if ( ! empty( $cached_date ) ) { return $cached_date; } /** * We don't use the 'ID' field, but need it to overcome a WP caching bug: https://core.trac.wordpress.org/ticket/62003 * * @todo Remote the 'ID' field from users fetching when the issue is fixed and Jetpack-supported WP versions move beyond it. */ $earliest_registered_users = get_users( array( 'role' => 'administrator', 'orderby' => 'user_registered', 'order' => 'ASC', 'fields' => array( 'ID', 'user_registered' ), 'number' => 1, ) ); $earliest_registration_date = $earliest_registered_users[0]->user_registered; $earliest_posts = get_posts( array( 'posts_per_page' => 1, 'post_type' => 'any', 'post_status' => 'any', 'orderby' => 'date', 'order' => 'ASC', ) ); // If there are no posts at all, we'll count only on user registration date. if ( $earliest_posts ) { $earliest_post_date = $earliest_posts[0]->post_date; } else { $earliest_post_date = PHP_INT_MAX; } $assumed_date = min( $earliest_registration_date, $earliest_post_date ); set_transient( 'jetpack_assumed_site_creation_date', $assumed_date ); return $assumed_date; } /** * Adds the activation source string as a parameter to passed arguments. * * @todo Refactor to use rawurlencode() instead of urlencode(). * * @param array $args arguments that need to have the source added. * @return array $amended arguments. */ public static function apply_activation_source_to_args( $args ) { list( $activation_source_name, $activation_source_keyword ) = get_option( 'jetpack_activation_source' ); if ( $activation_source_name ) { // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.urlencode_urlencode $args['_as'] = urlencode( $activation_source_name ); } if ( $activation_source_keyword ) { // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.urlencode_urlencode $args['_ak'] = urlencode( $activation_source_keyword ); } return $args; } /** * Generates two secret tokens and the end of life timestamp for them. * * @param string $action The action name. * @param int|bool $user_id The user identifier. * @param int $exp Expiration time in seconds. */ public function generate_secrets( $action, $user_id = false, $exp = 600 ) { return ( new Secrets() )->generate( $action, $user_id, $exp ); } /** * Returns two secret tokens and the end of life timestamp for them. * * @deprecated 1.24.0 Use Automattic\Jetpack\Connection\Secrets->get() instead. * * @param string $action The action name. * @param int $user_id The user identifier. * @return string|array an array of secrets or an error string. */ public function get_secrets( $action, $user_id ) { _deprecated_function( __METHOD__, '1.24.0', 'Automattic\\Jetpack\\Connection\\Secrets->get' ); return ( new Secrets() )->get( $action, $user_id ); } /** * Deletes secret tokens in case they, for example, have expired. * * @deprecated 1.24.0 Use Automattic\Jetpack\Connection\Secrets->delete() instead. * * @param string $action The action name. * @param int $user_id The user identifier. */ public function delete_secrets( $action, $user_id ) { _deprecated_function( __METHOD__, '1.24.0', 'Automattic\\Jetpack\\Connection\\Secrets->delete' ); ( new Secrets() )->delete( $action, $user_id ); } /** * Deletes all connection tokens and transients from the local Jetpack site. * If the plugin object has been provided in the constructor, the function first checks * whether it's the only active connection. * If there are any other connections, the function will do nothing and return `false` * (unless `$ignore_connected_plugins` is set to `true`). * * @param bool $ignore_connected_plugins Delete the tokens even if there are other connected plugins. * * @return bool True if disconnected successfully, false otherwise. */ public function delete_all_connection_tokens( $ignore_connected_plugins = false ) { // refuse to delete if we're not the last Jetpack plugin installed. if ( ! $ignore_connected_plugins && null !== $this->plugin && ! $this->plugin->is_only() ) { return false; } /** * Fires upon the disconnect attempt. * Return `false` to prevent the disconnect. * * @since 1.14.2 */ if ( ! apply_filters( 'jetpack_connection_delete_all_tokens', true ) ) { return false; } \Jetpack_Options::delete_option( array( 'master_user', 'time_diff', 'fallback_no_verify_ssl_certs', ) ); // Clear the memoized connection owner ID since it changed self::$connection_owner_id = null; ( new Secrets() )->delete_all(); $this->get_tokens()->delete_all(); // Delete cached connected user data. $transient_key = 'jetpack_connected_user_data_' . get_current_user_id(); delete_transient( $transient_key ); // Delete all XML-RPC errors. Error_Handler::get_instance()->delete_all_errors(); return true; } /** * Tells WordPress.com to disconnect the site and clear all tokens from cached site. * If the plugin object has been provided in the constructor, the function first check * whether it's the only active connection. * If there are any other connections, the function will do nothing and return `false` * (unless `$ignore_connected_plugins` is set to `true`). * * @param bool $ignore_connected_plugins Delete the tokens even if there are other connected plugins. * * @return bool True if disconnected successfully, false otherwise. */ public function disconnect_site_wpcom( $ignore_connected_plugins = false ) { if ( ! $ignore_connected_plugins && null !== $this->plugin && ! $this->plugin->is_only() ) { return false; } if ( ( new Status() )->is_offline_mode() && ! apply_filters( 'jetpack_connection_disconnect_site_wpcom_offline_mode', false ) ) { // Prevent potential disconnect of the live site by removing WPCOM tokens. return false; } /** * Fires upon the disconnect attempt. * Return `false` to prevent the disconnect. * * @since 1.14.2 */ if ( ! apply_filters( 'jetpack_connection_disconnect_site_wpcom', true, $this ) ) { return false; } $xml = new Jetpack_IXR_Client(); $xml->query( 'jetpack.deregister', get_current_user_id() ); return true; } /** * Disconnect the plugin and remove the tokens. * This function will automatically perform "soft" or "hard" disconnect depending on whether other plugins are using the connection. * This is a proxy method to simplify the Connection package API. * * @see Manager::disconnect_site() * * @param boolean $disconnect_wpcom Should disconnect_site_wpcom be called. * @param bool $ignore_connected_plugins Delete the tokens even if there are other connected plugins. * @return bool */ public function remove_connection( $disconnect_wpcom = true, $ignore_connected_plugins = false ) { $this->disconnect_site( $disconnect_wpcom, $ignore_connected_plugins ); return true; } /** * Completely clearing up the connection, and initiating reconnect. * * @return true|WP_Error True if reconnected successfully, a `WP_Error` object otherwise. */ public function reconnect() { ( new Tracking() )->record_user_event( 'restore_connection_reconnect' ); $this->disconnect_site_wpcom( true ); return $this->register(); } /** * Validate the tokens, and refresh the invalid ones. * * @return string|bool|WP_Error True if connection restored or string indicating what's to be done next. A `WP_Error` object or false otherwise. */ public function restore() { // If this is a site connection we need to trigger a full reconnection as our only secure means of // communication with WPCOM, aka the blog token, is compromised. if ( $this->is_site_connection() ) { return $this->reconnect(); } $validate_tokens_response = $this->get_tokens()->validate(); // If token validation failed, trigger a full reconnection. if ( is_array( $validate_tokens_response ) && isset( $validate_tokens_response['blog_token']['is_healthy'] ) && isset( $validate_tokens_response['user_token']['is_healthy'] ) ) { $blog_token_healthy = $validate_tokens_response['blog_token']['is_healthy']; $user_token_healthy = $validate_tokens_response['user_token']['is_healthy']; } else { $blog_token_healthy = false; $user_token_healthy = false; } // Tokens are both valid, or both invalid. We can't fix the problem we don't see, so the full reconnection is needed. if ( $blog_token_healthy === $user_token_healthy ) { $result = $this->reconnect(); return ( true === $result ) ? 'authorize' : $result; } if ( ! $blog_token_healthy ) { return $this->refresh_blog_token(); } if ( ! $user_token_healthy ) { return ( true === $this->refresh_user_token() ) ? 'authorize' : false; } return false; } /** * Responds to a WordPress.com call to register the current site. * Should be changed to protected. * * @param array $registration_data Array of [ secret_1, user_id ]. */ public function handle_registration( array $registration_data ) { list( $registration_secret_1, $registration_user_id ) = $registration_data; if ( empty( $registration_user_id ) ) { return new \WP_Error( 'registration_state_invalid', __( 'Invalid Registration State', 'jetpack-connection' ), 400 ); } return ( new Secrets() )->verify( 'register', $registration_secret_1, (int) $registration_user_id ); } /** * Perform the API request to validate the blog and user tokens. * * @deprecated 1.24.0 Use Automattic\Jetpack\Connection\Tokens->validate_tokens() instead. * * @param int|null $user_id ID of the user we need to validate token for. Current user's ID by default. * * @return array|false|WP_Error The API response: `array( 'blog_token_is_healthy' => true|false, 'user_token_is_healthy' => true|false )`. */ public function validate_tokens( $user_id = null ) { _deprecated_function( __METHOD__, '1.24.0', 'Automattic\\Jetpack\\Connection\\Tokens->validate' ); return $this->get_tokens()->validate( $user_id ); } /** * Verify a Previously Generated Secret. * * @deprecated 1.24.0 Use Automattic\Jetpack\Connection\Secrets->verify() instead. * * @param string $action The type of secret to verify. * @param string $secret_1 The secret string to compare to what is stored. * @param int $user_id The user ID of the owner of the secret. * @return \WP_Error|string WP_Error on failure, secret_2 on success. */ public function verify_secrets( $action, $secret_1, $user_id ) { _deprecated_function( __METHOD__, '1.24.0', 'Automattic\\Jetpack\\Connection\\Secrets->verify' ); return ( new Secrets() )->verify( $action, $secret_1, $user_id ); } /** * Responds to a WordPress.com call to authorize the current user. * Should be changed to protected. */ public function handle_authorization() { } /** * Obtains the auth token. * * @param array $data The request data. * @return object|\WP_Error Returns the auth token on success. * Returns a \WP_Error on failure. */ public function get_token( $data ) { return $this->get_tokens()->get( $data, $this->api_url( 'token' ) ); } /** * Builds a URL to the Jetpack connection auth page. * * @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, $from = false, $raw = false ) { if ( empty( $user ) ) { $user = wp_get_current_user(); } $roles = new Roles(); $role = $roles->translate_user_to_role( $user ); $signed_role = $this->get_tokens()->sign_role( $role ); /** * Filter the URL of the first time the user gets redirected back to your site for connection * data processing. * * @since 1.7.0 * @since-jetpack 8.0.0 * * @param string $redirect_url Defaults to the site admin URL. */ $processing_url = apply_filters( 'jetpack_connect_processing_url', admin_url( 'admin.php' ) ); /** * Filter the URL to redirect the user back to when the authorization process * is complete. * * @since 1.7.0 * @since-jetpack 8.0.0 * * @param string $redirect_url Defaults to the site URL. */ $redirect = apply_filters( 'jetpack_connect_redirect_url', $redirect ); $secrets = ( new Secrets() )->generate( 'authorize', $user->ID, 2 * HOUR_IN_SECONDS ); /** * Filter the type of authorization. * 'calypso' completes authorization on wordpress.com/jetpack/connect * while 'jetpack' ( or any other value ) completes the authorization at jetpack.wordpress.com. * * @since 1.7.0 * @since-jetpack 4.3.3 * * @param string $auth_type Defaults to 'calypso', can also be 'jetpack'. */ $auth_type = apply_filters( 'jetpack_auth_type', 'calypso' ); /** * Filters the user connection request data for additional property addition. * * @since 1.7.0 * @since-jetpack 8.0.0 * * @param array $request_data request data. */ $body = apply_filters( 'jetpack_connect_request_body', array( 'response_type' => 'code', 'client_id' => \Jetpack_Options::get_option( 'id' ), 'redirect_uri' => add_query_arg( array( 'handler' => 'jetpack-connection-webhooks', 'action' => 'authorize', '_wpnonce' => wp_create_nonce( "jetpack-authorize_{$role}_{$redirect}" ), 'redirect' => $redirect ? rawurlencode( $redirect ) : false, ), esc_url( $processing_url ) ), 'state' => $user->ID, 'scope' => $signed_role, 'user_email' => $user->user_email, 'user_login' => $user->user_login, 'is_active' => $this->has_connected_owner(), // TODO Deprecate this. 'jp_version' => (string) Constants::get_constant( 'JETPACK__VERSION' ), 'auth_type' => $auth_type, 'secret' => $secrets['secret_1'], 'blogname' => get_option( 'blogname' ), 'site_url' => Urls::site_url(), 'home_url' => Urls::home_url(), 'site_icon' => get_site_icon_url(), 'site_lang' => get_locale(), 'site_created' => $this->get_assumed_site_creation_date(), 'allow_site_connection' => ! $this->has_connected_owner(), 'calypso_env' => ( new Host() )->get_calypso_env(), 'source' => ( new Host() )->get_source_query(), ) ); $body = static::apply_activation_source_to_args( urlencode_deep( $body ) ); $api_url = $this->api_url( 'authorize' ); $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 ); } /** * Authorizes the user by obtaining and storing the user token. * * @param array $data The request data. * @return string|\WP_Error Returns a string on success. * Returns a \WP_Error on failure. */ public function authorize( $data = array() ) { /** * Action fired when user authorization starts. * * @since 1.7.0 * @since-jetpack 8.0.0 */ do_action( 'jetpack_authorize_starting' ); $roles = new Roles(); $role = $roles->translate_current_user_to_role(); if ( ! $role ) { return new \WP_Error( 'no_role', 'Invalid request.', 400 ); } $cap = $roles->translate_role_to_cap( $role ); if ( ! $cap ) { return new \WP_Error( 'no_cap', 'Invalid request.', 400 ); } if ( ! empty( $data['error'] ) ) { return new \WP_Error( $data['error'], 'Error included in the request.', 400 ); } if ( ! isset( $data['state'] ) ) { return new \WP_Error( 'no_state', 'Request must include state.', 400 ); } if ( ! ctype_digit( $data['state'] ) ) { return new \WP_Error( $data['error'], 'State must be an integer.', 400 ); } $current_user_id = get_current_user_id(); if ( $current_user_id !== (int) $data['state'] ) { return new \WP_Error( 'wrong_state', 'State does not match current user.', 400 ); } if ( empty( $data['code'] ) ) { return new \WP_Error( 'no_code', 'Request must include an authorization code.', 400 ); } $token = $this->get_tokens()->get( $data, $this->api_url( 'token' ) ); if ( is_wp_error( $token ) ) { $code = $token->get_error_code(); if ( empty( $code ) ) { $code = 'invalid_token'; } return new \WP_Error( $code, $token->get_error_message(), 400 ); } if ( ! $token ) { return new \WP_Error( 'no_token', 'Error generating token.', 400 ); } $is_connection_owner = ! $this->has_connected_owner(); $this->get_tokens()->update_user_token( $current_user_id, sprintf( '%s.%d', $token, $current_user_id ), $is_connection_owner ); /** * Fires after user has successfully received an auth token. * * @since 1.7.0 * @since-jetpack 3.9.0 */ do_action( 'jetpack_user_authorized' ); if ( ! $is_connection_owner ) { /** * Action fired when a secondary user has been authorized. * * @since 1.7.0 * @since-jetpack 8.0.0 */ do_action( 'jetpack_authorize_ending_linked' ); return 'linked'; } /** * Action fired when the master user has been authorized. * * @since 1.7.0 * @since-jetpack 8.0.0 * * @param array $data The request data. */ do_action( 'jetpack_authorize_ending_authorized', $data ); \Jetpack_Options::delete_raw_option( 'jetpack_last_connect_url_check' ); ( new Nonce_Handler() )->reschedule(); return 'authorized'; } /** * Disconnects from the Jetpack servers. * Forgets all connection details and tells the Jetpack servers to do the same. * * @param boolean $disconnect_wpcom Should disconnect_site_wpcom be called. * @param bool $ignore_connected_plugins Delete the tokens even if there are other connected plugins. */ public function disconnect_site( $disconnect_wpcom = true, $ignore_connected_plugins = true ) { if ( ! $ignore_connected_plugins && null !== $this->plugin && ! $this->plugin->is_only() ) { return false; } wp_clear_scheduled_hook( 'jetpack_clean_nonces' ); ( new Nonce_Handler() )->clean_all(); Heartbeat::init()->deactivate(); /** * Fires before a site is disconnected. * * @since 1.36.3 */ do_action( 'jetpack_site_before_disconnected' ); // If the site is in an IDC because sync is not allowed, // let's make sure to not disconnect the production site. if ( $disconnect_wpcom ) { $tracking = new Tracking(); $tracking->record_user_event( 'disconnect_site', array() ); $this->disconnect_site_wpcom( $ignore_connected_plugins ); } $this->delete_all_connection_tokens( $ignore_connected_plugins ); // Remove tracked package versions, since they depend on the Jetpack Connection. delete_option( Package_Version_Tracker::PACKAGE_VERSION_OPTION ); $jetpack_unique_connection = \Jetpack_Options::get_option( 'unique_connection' ); if ( $jetpack_unique_connection ) { // Check then record unique disconnection if site has never been disconnected previously. if ( - 1 === $jetpack_unique_connection['disconnected'] ) { $jetpack_unique_connection['disconnected'] = 1; } else { if ( 0 === $jetpack_unique_connection['disconnected'] ) { $a8c_mc_stats_instance = new A8c_Mc_Stats(); $a8c_mc_stats_instance->add( 'connections', 'unique-disconnect' ); $a8c_mc_stats_instance->do_server_side_stats(); } // increment number of times disconnected. $jetpack_unique_connection['disconnected'] += 1; } \Jetpack_Options::update_option( 'unique_connection', $jetpack_unique_connection ); } /** * Fires when a site is disconnected. * * @since 1.30.1 */ do_action( 'jetpack_site_disconnected' ); } /** * The Base64 Encoding of the SHA1 Hash of the Input. * * @param string $text The string to hash. * @return string */ public function sha1_base64( $text ) { return base64_encode( sha1( $text, true ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode } /** * This function mirrors Jetpack_Data::is_usable_domain() in the WPCOM codebase. * * @param string $domain The domain to check. * * @return bool|WP_Error */ public function is_usable_domain( $domain ) { // If it's empty, just fail out. if ( ! $domain ) { return new \WP_Error( 'fail_domain_empty', /* translators: %1$s is a domain name. */ sprintf( __( 'Domain `%1$s` just failed is_usable_domain check as it is empty.', 'jetpack-connection' ), $domain ) ); } /** * Skips the usuable domain check when connecting a site. * * Allows site administrators with domains that fail gethostname-based checks to pass the request to WP.com * * @since 1.7.0 * @since-jetpack 4.1.0 * * @param bool If the check should be skipped. Default false. */ if ( apply_filters( 'jetpack_skip_usuable_domain_check', false ) ) { return true; } // None of the explicit localhosts. $forbidden_domains = array( 'wordpress.com', 'localhost', 'localhost.localdomain', 'local.wordpress.test', // VVV pattern. 'local.wordpress-trunk.test', // VVV pattern. 'src.wordpress-develop.test', // VVV pattern. 'build.wordpress-develop.test', // VVV pattern. ); if ( in_array( $domain, $forbidden_domains, true ) ) { return new \WP_Error( 'fail_domain_forbidden', sprintf( /* translators: %1$s is a domain name. */ __( 'Domain `%1$s` just failed is_usable_domain check as it is in the forbidden array.', 'jetpack-connection' ), $domain ) ); } // No .test or .local domains. if ( preg_match( '#\.(test|local)$#i', $domain ) ) { return new \WP_Error( 'fail_domain_tld', sprintf( /* translators: %1$s is a domain name. */ __( 'Domain `%1$s` just failed is_usable_domain check as it uses an invalid top level domain.', 'jetpack-connection' ), $domain ) ); } // No WPCOM subdomains. if ( preg_match( '#\.WordPress\.com$#i', $domain ) ) { return new \WP_Error( 'fail_subdomain_wpcom', sprintf( /* translators: %1$s is a domain name. */ __( 'Domain `%1$s` just failed is_usable_domain check as it is a subdomain of WordPress.com.', 'jetpack-connection' ), $domain ) ); } // If PHP was compiled without support for the Filter module (very edge case). if ( ! function_exists( 'filter_var' ) ) { // Just pass back true for now, and let wpcom sort it out. return true; } $domain = preg_replace( '#^https?://#', '', untrailingslashit( $domain ) ); if ( filter_var( $domain, FILTER_VALIDATE_IP ) && ! filter_var( $domain, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) ) { return new \WP_Error( 'fail_ip_forbidden', sprintf( /* translators: %1$s is a domain name. */ __( 'IP address `%1$s` just failed is_usable_domain check as it is in the private network.', 'jetpack-connection' ), $domain ) ); } return true; } /** * Gets the requested token. * * @deprecated 1.24.0 Use Automattic\Jetpack\Connection\Tokens->get_access_token() instead. * * @param int|false $user_id false: Return the Blog Token. int: Return that user's User Token. * @param string|false $token_key If provided, check that the token matches the provided input. * @param bool|true $suppress_errors If true, return a falsy value when the token isn't found; When false, return a descriptive WP_Error when the token isn't found. * * @return object|false * * @see $this->get_tokens()->get_access_token() */ public function get_access_token( $user_id = false, $token_key = false, $suppress_errors = true ) { _deprecated_function( __METHOD__, '1.24.0', 'Automattic\\Jetpack\\Connection\\Tokens->get_access_token' ); return $this->get_tokens()->get_access_token( $user_id, $token_key, $suppress_errors ); } /** * In some setups, $HTTP_RAW_POST_DATA can be emptied during some IXR_Server paths * since it is passed by reference to various methods. * Capture it here so we can verify the signature later. * * @param array $methods an array of available XMLRPC methods. * @return array the same array, since this method doesn't add or remove anything. */ public function xmlrpc_methods( $methods ) { $this->raw_post_data = isset( $GLOBALS['HTTP_RAW_POST_DATA'] ) ? $GLOBALS['HTTP_RAW_POST_DATA'] : null; return $methods; } /** * Resets the raw post data parameter for testing purposes. */ public function reset_raw_post_data() { $this->raw_post_data = null; } /** * Registering an additional method. * * @param array $methods an array of available XMLRPC methods. * @return array the amended array in case the method is added. */ public function public_xmlrpc_methods( $methods ) { if ( array_key_exists( 'wp.getOptions', $methods ) ) { $methods['wp.getOptions'] = array( $this, 'jetpack_get_options' ); } return $methods; } /** * Handles a getOptions XMLRPC method call. * * @param array $args method call arguments. * @return array|IXR_Error An amended XMLRPC server options array. */ public function jetpack_get_options( $args ) { global $wp_xmlrpc_server; $wp_xmlrpc_server->escape( $args ); $username = $args[1]; $password = $args[2]; $user = $wp_xmlrpc_server->login( $username, $password ); if ( ! $user ) { return $wp_xmlrpc_server->error; } $options = array(); $user_data = $this->get_connected_user_data(); if ( is_array( $user_data ) ) { $options['jetpack_user_id'] = array( 'desc' => __( 'The WP.com user ID of the connected user', 'jetpack-connection' ), 'readonly' => true, 'value' => $user_data['ID'], ); $options['jetpack_user_login'] = array( 'desc' => __( 'The WP.com username of the connected user', 'jetpack-connection' ), 'readonly' => true, 'value' => $user_data['login'], ); $options['jetpack_user_email'] = array( 'desc' => __( 'The WP.com user email of the connected user', 'jetpack-connection' ), 'readonly' => true, 'value' => $user_data['email'], ); $options['jetpack_user_site_count'] = array( 'desc' => __( 'The number of sites of the connected WP.com user', 'jetpack-connection' ), 'readonly' => true, 'value' => $user_data['site_count'], ); } $wp_xmlrpc_server->blog_options = array_merge( $wp_xmlrpc_server->blog_options, $options ); $args = stripslashes_deep( $args ); return $wp_xmlrpc_server->wp_getOptions( $args ); } /** * Adds Jetpack-specific options to the output of the XMLRPC options method. * * @param array $options standard Core options. * @return array amended options. */ public function xmlrpc_options( $options ) { $jetpack_client_id = false; if ( $this->is_connected() ) { $jetpack_client_id = \Jetpack_Options::get_option( 'id' ); } $options['jetpack_version'] = array( 'desc' => __( 'Jetpack Plugin Version', 'jetpack-connection' ), 'readonly' => true, 'value' => Constants::get_constant( 'JETPACK__VERSION' ), ); $options['jetpack_client_id'] = array( 'desc' => __( 'The Client ID/WP.com Blog ID of this site', 'jetpack-connection' ), 'readonly' => true, 'value' => $jetpack_client_id, ); return $options; } /** * Resets the saved authentication state in between testing requests. */ public function reset_saved_auth_state() { $this->xmlrpc_verification = null; } /** * Sign a user role with the master access token. * If not specified, will default to the current user. * * @access public * * @param string $role User role. * @param int $user_id ID of the user. * @return string Signed user role. */ public function sign_role( $role, $user_id = null ) { return $this->get_tokens()->sign_role( $role, $user_id ); } /** * Set the plugin instance. * * @param Plugin $plugin_instance The plugin instance. * * @return $this */ public function set_plugin_instance( Plugin $plugin_instance ) { $this->plugin = $plugin_instance; return $this; } /** * Retrieve the plugin management object. * * @return Plugin|null */ public function get_plugin() { return $this->plugin; } /** * Get all connected plugins information, excluding those disconnected by user. * WARNING: the method cannot be called until Plugin_Storage::configure is called, which happens on plugins_loaded * Even if you don't use Jetpack Config, it may be introduced later by other plugins, * so please make sure not to run the method too early in the code. * * @return array|WP_Error */ public function get_connected_plugins() { $maybe_plugins = Plugin_Storage::get_all(); if ( $maybe_plugins instanceof WP_Error ) { return $maybe_plugins; } return $maybe_plugins; } /** * Force plugin disconnect. After its called, the plugin will not be allowed to use the connection. * Note: this method does not remove any access tokens. * * @deprecated since 1.39.0 * @return bool */ public function disable_plugin() { return null; } /** * Force plugin reconnect after user-initiated disconnect. * After its called, the plugin will be allowed to use the connection again. * Note: this method does not initialize access tokens. * * @deprecated since 1.39.0. * @return bool */ public function enable_plugin() { return null; } /** * Whether the plugin is allowed to use the connection, or it's been disconnected by user. * If no plugin slug was passed into the constructor, always returns true. * * @deprecated 1.42.0 This method no longer has a purpose after the removal of the soft disconnect feature. * * @return bool */ public function is_plugin_enabled() { return true; } /** * Perform the API request to refresh the blog token. * Note that we are making this request on behalf of the Jetpack master user, * given they were (most probably) the ones that registered the site at the first place. * * @return WP_Error|bool The result of updating the blog_token option. */ public function refresh_blog_token() { ( new Tracking() )->record_user_event( 'restore_connection_refresh_blog_token' ); $blog_id = \Jetpack_Options::get_option( 'id' ); if ( ! $blog_id ) { return new WP_Error( 'site_not_registered', 'Site not registered.' ); } $url = sprintf( '%s/%s/v%s/%s', Constants::get_constant( 'JETPACK__WPCOM_JSON_API_BASE' ), 'wpcom', '2', 'sites/' . $blog_id . '/jetpack-refresh-blog-token' ); $method = 'POST'; $user_id = get_current_user_id(); $response = Client::remote_request( compact( 'url', 'method', 'user_id' ) ); if ( is_wp_error( $response ) ) { return new WP_Error( 'refresh_blog_token_http_request_failed', $response->get_error_message() ); } $code = wp_remote_retrieve_response_code( $response ); $entity = wp_remote_retrieve_body( $response ); if ( $entity ) { $json = json_decode( $entity ); } else { $json = false; } if ( 200 !== $code ) { if ( empty( $json->code ) ) { return new WP_Error( 'unknown', '', $code ); } /* translators: Error description string. */ $error_description = isset( $json->message ) ? sprintf( __( 'Error Details: %s', 'jetpack-connection' ), (string) $json->message ) : ''; return new WP_Error( (string) $json->code, $error_description, $code ); } if ( empty( $json->jetpack_secret ) || ! is_scalar( $json->jetpack_secret ) ) { return new WP_Error( 'jetpack_secret', '', $code ); } Error_Handler::get_instance()->delete_all_errors(); return $this->get_tokens()->update_blog_token( (string) $json->jetpack_secret ); } /** * Disconnect the user from WP.com, and initiate the reconnect process. * * @return bool */ public function refresh_user_token() { ( new Tracking() )->record_user_event( 'restore_connection_refresh_user_token' ); $this->disconnect_user( null, true, true ); return true; } /** * Fetches a signed token. * * @deprecated 1.24.0 Use Automattic\Jetpack\Connection\Tokens->get_signed_token() instead. * * @param object $token the token. * @return WP_Error|string a signed token */ public function get_signed_token( $token ) { _deprecated_function( __METHOD__, '1.24.0', 'Automattic\\Jetpack\\Connection\\Tokens->get_signed_token' ); return $this->get_tokens()->get_signed_token( $token ); } /** * If the site-level connection is active, add the list of plugins using connection to the heartbeat (except Jetpack itself) * * @since 6.11.0 Add the list of Jetpack package versions to the heartbeat. * * @param array $stats The Heartbeat stats array. * @return array $stats */ public function add_stats_to_heartbeat( $stats ) { if ( ! $this->is_connected() ) { return $stats; } $active_plugins_using_connection = Plugin_Storage::get_all(); foreach ( array_keys( $active_plugins_using_connection ) as $plugin_slug ) { if ( 'jetpack' !== $plugin_slug ) { $stats_group = isset( $active_plugins_using_connection['jetpack'] ) ? 'combined-connection' : 'standalone-connection'; $stats[ $stats_group ][] = $plugin_slug; } } $stats['jetpack_package_versions'] = apply_filters( 'jetpack_package_versions', array() ); return $stats; } /** * Get the WPCOM or self-hosted site ID. * * @param bool $quiet Return null instead of an error. * * @return int|WP_Error|null */ 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 $quiet ? null : new \WP_Error( 'unavailable_site_id', __( 'Sorry, something is wrong with your Jetpack connection.', 'jetpack-connection' ), 403 ); } return (int) $site_id; } /** * Check if Jetpack is ready for uninstall cleanup. * * @param string $current_plugin_slug The current plugin's slug. * * @return bool */ public static function is_ready_for_cleanup( $current_plugin_slug ) { $active_plugins = get_option( Plugin_Storage::ACTIVE_PLUGINS_OPTION_NAME ); return empty( $active_plugins ) || ! is_array( $active_plugins ) || ( count( $active_plugins ) === 1 && array_key_exists( $current_plugin_slug, $active_plugins ) ); } } jetpack-connection/src/sso/jetpack-sso-login.js 0000777 00000001745 15251741406 0015627 0 ustar 00 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; } } ); } ); jetpack-connection/src/sso/class-force-2fa.php 0000777 00000011510 15251741406 0015307 0 ustar 00 <?php /** * Force Jetpack 2FA Functionality * * Ported from original repo at https://github.com/automattic/jetpack-force-2fa * * @package automattic/jetpack-connection */ namespace Automattic\Jetpack\Connection\SSO; use Automattic\Jetpack\Connection\SSO; use Automattic\Jetpack\Modules; use WP_Error; /** * Force users to use two-factor authentication. * * @phan-constructor-used-for-side-effects */ class Force_2FA { /** * The role to force 2FA for. * * Defaults to manage_options via the plugins_loaded function. * Can be modified with the jetpack_force_2fa_cap filter. * * @var string */ private $role; /** * Constructor. */ public function __construct() { add_action( 'after_setup_theme', array( $this, 'plugins_loaded' ) ); } /** * Load the plugin via the plugins_loaded hook. */ public function plugins_loaded() { /** * Filter the role to force 2FA for. * Defaults to manage_options. * * @param string $role The role to force 2FA for. * @return string * @since jetpack-12.7 * @module SSO */ $this->role = apply_filters( 'jetpack_force_2fa_cap', 'manage_options' ); // Bail if Jetpack SSO is not active if ( ! ( new Modules() )->is_active( 'sso' ) ) { add_action( 'admin_notices', array( $this, 'admin_notice' ) ); return; } $this->force_2fa(); } /** * Display an admin notice if Jetpack SSO is not active. */ public function admin_notice() { /** * Filter if an admin notice is deplayed when Force 2FA is required, but SSO is not enabled. * Defaults to true. * * @param bool $display_notice Whether to display the notice. * @return bool * @since jetpack-12.7 * @module SSO */ if ( apply_filters( 'jetpack_force_2fa_dependency_notice', true ) && current_user_can( $this->role ) ) { wp_admin_notice( esc_html__( 'Jetpack Force 2FA requires Jetpack’s SSO feature.', 'jetpack-connection' ), array( 'type' => 'warning', ) ); } } /** * Force 2FA when using Jetpack SSO and force Jetpack SSO. * * @return void */ private function force_2fa() { // Allows WP.com login to a local account if it matches the local account. add_filter( 'jetpack_sso_match_by_email', '__return_true', 9999 ); // multisite if ( is_multisite() ) { // Hide the login form add_filter( 'jetpack_remove_login_form', '__return_true', 9999 ); add_filter( 'jetpack_sso_bypass_login_forward_wpcom', '__return_true', 9999 ); add_filter( 'jetpack_sso_display_disclaimer', '__return_false', 9999 ); add_filter( 'wp_authenticate_user', function () { return new WP_Error( 'wpcom-required', $this->get_login_error_message() ); }, 9999 ); add_filter( 'jetpack_sso_require_two_step', '__return_true' ); add_filter( 'allow_password_reset', '__return_false' ); } else { // Not multisite. // Completely disable the standard login form for admins. add_filter( 'wp_authenticate_user', function ( $user ) { if ( is_wp_error( $user ) ) { return $user; } if ( $user->has_cap( $this->role ) ) { return new WP_Error( 'wpcom-required', $this->get_login_error_message(), $user->user_login ); } return $user; }, 9999 ); add_filter( 'allow_password_reset', function ( $allow, $user_id ) { if ( user_can( $user_id, $this->role ) ) { return false; } return $allow; }, 9999, 2 ); add_action( 'jetpack_sso_pre_handle_login', array( $this, 'jetpack_set_two_step' ) ); } } /** * Specifically set the two step filter for Jetpack SSO. * * @param Object $user_data The user data from WordPress.com. * * @return void */ public function jetpack_set_two_step( $user_data ) { $user = SSO::get_user_by_wpcom_id( $user_data->ID ); // Borrowed from Jetpack. Ignores the match_by_email setting. if ( empty( $user ) ) { $user = get_user_by( 'email', $user_data->email ); } if ( $user && $user->has_cap( $this->role ) ) { add_filter( 'jetpack_sso_require_two_step', '__return_true' ); } } /** * Get the login error message. * * @return string */ private function get_login_error_message() { /** * Filter the login error message. * Defaults to a message that explains the user must use a WordPress.com account with 2FA enabled. * * @param string $message The login error message. * @return string * @since jetpack-12.7 * @module SSO */ return apply_filters( 'jetpack_force_2fa_login_error_message', sprintf( 'For added security, please log in using your WordPress.com account.<br /><br />Note: Your account must have <a href="%1$s" target="_blank">Two Step Authentication</a> enabled, which can be configured from <a href="%2$s" target="_blank">Security Settings</a>.', 'https://support.wordpress.com/security/two-step-authentication/', 'https://wordpress.com/me/security/two-step' ) ); } } jetpack-connection/src/sso/jetpack-sso-admin-create-user.js 0000777 00000003741 15251741406 0020022 0 ustar 00 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'; } } } ); jetpack-connection/src/sso/class-notices.php 0000777 00000020113 15251741406 0015206 0 ustar 00 <?php /** * A collection of helper functions used in the SSO module. * * @package automattic/jetpack-connection */ namespace Automattic\Jetpack\Connection\SSO; use Automattic\Jetpack\Redirect; use WP_Error; use WP_User; /** * A collection of helper functions used in the SSO module. * * @since jetpack-4.4.0 */ class Notices { /** * Error message displayed on the login form when two step is required and * the user's account on WordPress.com does not have two step enabled. * * @since jetpack-2.7 * @param string $message Error message. * @return string **/ public static function error_msg_enable_two_step( $message ) { $error = sprintf( wp_kses( /* translators: URL to settings page */ __( 'Two-Step Authentication is required to access this site. Please visit your <a href="%1$s" rel="noopener noreferrer" target="_blank">Security Settings</a> to configure <a href="%2$s" rel="noopener noreferrer" target="_blank">Two-step Authentication</a> for your account.', 'jetpack-connection' ), array( 'a' => array( 'href' => array() ) ) ), Redirect::get_url( 'calypso-me-security-two-step' ), Redirect::get_url( 'wpcom-support-security-two-step-authentication' ) ); $message .= sprintf( '<p class="message" id="login_error">%s</p>', $error ); return $message; } /** * Error message displayed when the user tries to SSO, but match by email * is off and they already have an account with their email address on * this site. * * @param string $message Error message. * @return string */ public static function error_msg_email_already_exists( $message ) { $error = sprintf( wp_kses( /* translators: login URL */ __( 'You already have an account on this site. Please <a href="%1$s">sign in</a> with your username and password and then connect to WordPress.com.', 'jetpack-connection' ), array( 'a' => array( 'href' => array() ) ) ), esc_url_raw( add_query_arg( 'jetpack-sso-show-default-form', '1', wp_login_url() ) ) ); $message .= sprintf( '<p class="message" id="login_error">%s</p>', $error ); return $message; } /** * Error message that is displayed when the current site is in an identity crisis and SSO cannot be used. * * @since jetpack-4.3.2 * * @param string $message Error Message. * * @return string */ public static function error_msg_identity_crisis( $message ) { $error = esc_html__( 'Logging in with WordPress.com is not currently available because this site is experiencing connection problems.', 'jetpack-connection' ); $message .= sprintf( '<p class="message" id="login_error">%s</p>', $error ); return $message; } /** * Error message that is displayed when we are not able to verify the SSO nonce due to an XML error or * failed validation. In either case, we prompt the user to try again or log in with username and password. * * @since jetpack-4.3.2 * * @param string $message Error message. * * @return string */ public static function error_invalid_response_data( $message ) { $error = esc_html__( 'There was an error logging you in via WordPress.com, please try again or try logging in with your username and password.', 'jetpack-connection' ); $message .= sprintf( '<p class="message" id="login_error">%s</p>', $error ); return $message; } /** * Error message that is displayed when we were not able to automatically create an account for a user * after a user has logged in via SSO. By default, this message is triggered after trying to create an account 5 times. * * @since jetpack-4.3.2 * * @param string $message Error message. * * @return string */ public static function error_unable_to_create_user( $message ) { $error = esc_html__( 'There was an error creating a user for you. Please contact the administrator of your site.', 'jetpack-connection' ); $message .= sprintf( '<p class="message" id="login_error">%s</p>', $error ); return $message; } /** * When the default login form is hidden, this method is called on the 'authenticate' filter with a priority of 30. * This method disables the ability to submit the default login form. * * @param WP_User|WP_Error $user Either the user attempting to login or an existing authentication failure. * * @return WP_Error */ public static function disable_default_login_form( $user ) { if ( is_wp_error( $user ) ) { return $user; } /** * Since we're returning an error that will be shown as a red notice, let's remove the * informational "blue" notice. */ remove_filter( 'login_message', array( static::class, 'msg_login_by_jetpack' ) ); return new WP_Error( 'jetpack_sso_required', self::get_sso_required_message() ); } /** * Message displayed when the site admin has disabled the default WordPress * login form in Settings > General > Secure Sign On * * @since jetpack-2.7 * @param string $message Error message. * * @return string **/ public static function msg_login_by_jetpack( $message ) { $message .= sprintf( '<p class="message">%s</p>', self::get_sso_required_message() ); return $message; } /** * Get the message for SSO required. * * @return string */ public static function get_sso_required_message() { $msg = esc_html__( 'A WordPress.com account is required to access this site. Click the button below to sign in or create a free WordPress.com account.', 'jetpack-connection' ); /** * Filter the message displayed when the default WordPress login form is disabled. * * @module sso * * @since jetpack-2.8.0 * * @param string $msg Disclaimer when default WordPress login form is disabled. */ return apply_filters( 'jetpack_sso_disclaimer_message', $msg ); } /** * Message displayed when the user cannot be found after approving the SSO process on WordPress.com * * @param string $message Error message. * * @return string */ public static function cant_find_user( $message ) { $error = __( "We couldn't find your account. If you already have an account, make sure you have connected to WordPress.com.", 'jetpack-connection' ); /** * Filters the "couldn't find your account" notice after an attempted SSO. * * @module sso * * @since jetpack-10.5.0 * * @param string $error Error text. */ $error = apply_filters( 'jetpack_sso_unknown_user_notice', $error ); $message .= sprintf( '<p class="message" id="login_error">%s</p>', esc_html( $error ) ); return $message; } /** * Error message that is displayed when the current site is in an identity crisis and SSO cannot be used. * * @since 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 cannot be used. * * @since 2.10.0 * * @param string $message Error message. * * @return string */ public static function sso_not_allowed_in_safe_mode( $message ) { $error = __( 'Logging in with WordPress.com is disabled for sites that are in safe mode.', 'jetpack-connection' ); /** * Filters the disallowed notice for sites in safe mode attempting SSO. * * @module sso * * @since 2.10.0 * * @param string $error Error text. */ $error = apply_filters( 'jetpack_sso_disallowed_safe_mode_notice', $error ); $message .= sprintf( '<p class="message">%s</p>', esc_html( $error ) ); return $message; } } jetpack-connection/src/sso/class-helpers.php 0000777 00000025077 15251741406 0015222 0 ustar 00 <?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(); } } jetpack-connection/src/sso/class-user-admin.php 0000777 00000117466 15251741406 0015630 0 ustar 00 <?php /** * Extra UI elements added to the User Menu for the SSO feature. * * @package automattic/jetpack-connection */ namespace Automattic\Jetpack\Connection\SSO; use Automattic\Jetpack\Assets; use Automattic\Jetpack\Connection\Client; use Automattic\Jetpack\Connection\Manager; use Automattic\Jetpack\Connection\Package_Version; use Automattic\Jetpack\Connection\Users_Connection_Admin as Base_Admin; use Automattic\Jetpack\Roles; use Automattic\Jetpack\Status\Host; use Automattic\Jetpack\Tracking; use WP_Error; use WP_User; use WP_User_Query; if ( ! defined( 'ABSPATH' ) ) { exit( 0 ); } /** * Jetpack sso user admin class. * * @phan-constructor-used-for-side-effects */ class User_Admin extends Base_Admin { /** * Instance of WP_User_Query. * * @var $user_search */ private static $user_search = null; /** * Array of cached invites. * * @var $cached_invites */ private static $cached_invites = null; /** * Instance of Jetpack Tracking. * * @var $instance */ private static $tracking = null; /** * Constructor function. */ public function __construct() { add_action( 'delete_user', array( Helpers::class, 'delete_connection_for_user' ) ); // If the user has no errors on creation, send an invite to WordPress.com. add_filter( 'user_profile_update_errors', array( $this, 'send_wpcom_mail_user_invite' ), 10, 3 ); add_filter( 'wp_send_new_user_notification_to_user', array( $this, 'should_send_wp_mail_new_user' ) ); add_action( 'user_new_form', array( $this, 'render_invitation_email_message' ) ); add_action( 'user_new_form', array( $this, 'render_wpcom_invite_checkbox' ), 1 ); add_action( 'user_new_form', array( $this, 'render_wpcom_external_user_checkbox' ), 1 ); add_action( 'user_new_form', array( $this, 'render_custom_email_message_form_field' ), 1 ); add_action( 'delete_user_form', array( $this, 'render_invitations_notices_for_deleted_users' ) ); add_action( 'delete_user', array( $this, 'revoke_user_invite' ) ); add_filter( 'manage_users_custom_column', array( $this, 'jetpack_show_connection_status' ), 10, 3 ); add_action( 'user_row_actions', array( $this, 'jetpack_user_table_row_actions' ), 10, 2 ); if ( isset( $_GET['jetpack-sso-invite-user'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended add_action( 'admin_notices', array( $this, 'handle_invitation_results' ) ); } add_action( 'admin_post_jetpack_invite_user_to_wpcom', array( $this, 'invite_user_to_wpcom' ) ); add_action( 'admin_post_jetpack_revoke_invite_user_to_wpcom', array( $this, 'handle_request_revoke_invite' ) ); add_action( 'admin_post_jetpack_resend_invite_user_to_wpcom', array( $this, 'handle_request_resend_invite' ) ); add_action( 'admin_print_styles-users.php', array( $this, 'jetpack_user_table_styles' ) ); add_filter( 'users_list_table_query_args', array( $this, 'set_user_query' ), 100, 1 ); add_action( 'admin_print_styles-user-new.php', array( $this, 'jetpack_new_users_styles' ) ); add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_scripts' ) ); self::$tracking = new Tracking(); } /** * Enqueue assets for user-new.php. */ public function jetpack_new_users_styles() { Assets::register_script( 'jetpack-sso-admin-create-user', '../../dist/jetpack-sso-admin-create-user.js', __FILE__, array( 'strategy' => 'defer', 'in_footer' => true, 'enqueue' => true, ) ); } /** * Intercept the arguments for building the table, and create WP_User_Query instance * * @param array $args The search arguments. * * @return array */ public function set_user_query( $args ) { self::$user_search = new WP_User_Query( $args ); return $args; } /** * Revokes WordPress.com invitation. * * @param int $user_id The user ID. * @return mixed Response from the API call or false on failure. */ public function revoke_user_invite( $user_id ) { try { $has_pending_invite = self::has_pending_wpcom_invite( $user_id ); if ( $has_pending_invite ) { $response = self::send_revoke_wpcom_invite( $has_pending_invite ); $event = 'sso_user_invite_revoked'; if ( 200 !== wp_remote_retrieve_response_code( $response ) ) { $body = json_decode( wp_remote_retrieve_body( $response ) ); $tracking_event_data = array( 'success' => 'false', 'error_code' => 'invalid-revoke-api-error', ); if ( ! empty( $body ) && ! empty( $body->message ) ) { $tracking_event_data['error_message'] = $body->message; } self::$tracking->record_user_event( $event, $tracking_event_data ); return $response; } $body = json_decode( $response['body'] ); if ( ! $body->deleted ) { self::$tracking->record_user_event( $event, array( 'success' => 'false', 'error_message' => 'invalid-invite-revoke', ) ); } else { self::$tracking->record_user_event( $event, array( 'success' => 'true' ) ); } return $response; } else { // Delete external contributor if it exists. $wpcom_user_data = ( new Manager() )->get_connected_user_data( $user_id ); if ( isset( $wpcom_user_data['ID'] ) ) { return self::delete_external_contributor( $wpcom_user_data['ID'] ); } } } catch ( \Exception $e ) { return false; } } /** * Renders invitations errors/success messages in users.php. */ public function handle_invitation_results() { $valid_nonce = isset( $_GET['_wpnonce'] ) ? wp_verify_nonce( sanitize_key( $_GET['_wpnonce'] ), 'jetpack-sso-invite-user' ) : false; if ( ! $valid_nonce || ! isset( $_GET['jetpack-sso-invite-user'] ) ) { return; } if ( $_GET['jetpack-sso-invite-user'] === 'success' ) { return wp_admin_notice( __( 'User was invited successfully!', 'jetpack-connection' ), array( 'type' => 'success' ) ); } if ( $_GET['jetpack-sso-invite-user'] === 'reinvited-success' ) { return wp_admin_notice( __( 'User was re-invited successfully!', 'jetpack-connection' ), array( 'type' => 'success' ) ); } if ( $_GET['jetpack-sso-invite-user'] === 'successful-revoke' ) { return wp_admin_notice( __( 'User invite revoked successfully.', 'jetpack-connection' ), array( 'type' => 'success' ) ); } if ( $_GET['jetpack-sso-invite-user'] === 'failed' && isset( $_GET['jetpack-sso-api-error-message'] ) ) { return wp_admin_notice( wp_kses( wp_unslash( $_GET['jetpack-sso-api-error-message'] ), array() ), array( 'type' => 'error' ) ); } if ( $_GET['jetpack-sso-invite-user'] === 'failed' && isset( $_GET['jetpack-sso-invite-error'] ) ) { switch ( $_GET['jetpack-sso-invite-error'] ) { case 'invalid-user': return wp_admin_notice( __( 'Tried to invite a user that doesn’t exist.', 'jetpack-connection' ), array( 'type' => 'error' ) ); case 'invalid-email': return wp_admin_notice( __( 'Tried to invite a user that doesn’t have an email address.', 'jetpack-connection' ), array( 'type' => 'error' ) ); case 'invalid-user-permissions': return wp_admin_notice( __( 'You don’t have permission to invite users.', 'jetpack-connection' ), array( 'type' => 'error' ) ); case 'invalid-user-revoke': return wp_admin_notice( __( 'Tried to revoke an invite for a user that doesn’t exist.', 'jetpack-connection' ), array( 'type' => 'error' ) ); case 'invalid-invite-revoke': return wp_admin_notice( __( 'Tried to revoke an invite that doesn’t exist.', 'jetpack-connection' ), array( 'type' => 'error' ) ); case 'invalid-revoke-permissions': return wp_admin_notice( __( 'You don’t have permission to revoke invites.', 'jetpack-connection' ), array( 'type' => 'error' ) ); case 'empty-invite': return wp_admin_notice( __( 'There is no previous invite for this user', 'jetpack-connection' ), array( 'type' => 'error' ) ); case 'invalid-invite': return wp_admin_notice( __( 'Attempted to send a new invitation to a user using an invite that doesn’t exist.', 'jetpack-connection' ), array( 'type' => 'error' ) ); case 'error-revoke': return wp_admin_notice( __( 'An error has occurred when revoking the invite for the user.', 'jetpack-connection' ), array( 'type' => 'error' ) ); case 'invalid-revoke-api-error': return wp_admin_notice( __( 'An error has occurred when revoking the user invite.', 'jetpack-connection' ), array( 'type' => 'error' ) ); default: return wp_admin_notice( __( 'An error has occurred when inviting the user to the site.', 'jetpack-connection' ), array( 'type' => 'error' ) ); } } } /** * Invites a user to connect to WordPress.com to allow them to log in via SSO. */ public function invite_user_to_wpcom() { check_admin_referer( 'jetpack-sso-invite-user', 'invite_nonce' ); $nonce = wp_create_nonce( 'jetpack-sso-invite-user' ); $event = 'sso_user_invite_sent'; if ( ! current_user_can( 'create_users' ) ) { $error = 'invalid-user-permissions'; $query_params = array( 'jetpack-sso-invite-user' => 'failed', 'jetpack-sso-invite-error' => $error, '_wpnonce' => $nonce, ); return self::create_error_notice_and_redirect( $query_params ); } elseif ( isset( $_GET['user_id'] ) ) { $user_id = intval( wp_unslash( $_GET['user_id'] ) ); $user = get_user_by( 'id', $user_id ); $user_email = $user->user_email; if ( ! $user || ! $user_email ) { $reason = ! $user ? 'invalid-user' : 'invalid-email'; $query_params = array( 'jetpack-sso-invite-user' => 'failed', 'jetpack-sso-invite-error' => $reason, '_wpnonce' => $nonce, ); self::$tracking->record_user_event( $event, array( 'success' => 'false', 'error_message' => $reason, ) ); return self::create_error_notice_and_redirect( $query_params ); } $blog_id = Manager::get_site_id( true ); $roles = new Roles(); $user_role = $roles->translate_user_to_role( $user ); $url = '/sites/' . $blog_id . '/invites/new'; $response = Client::wpcom_json_api_request_as_user( $url, 'v2', array( 'method' => 'POST', ), array( 'invitees' => array( array( 'email_or_username' => $user_email, 'role' => $user_role, ), ), ), 'wpcom' ); if ( 200 !== wp_remote_retrieve_response_code( $response ) ) { $error_code = 'invalid-invite-api-error'; $query_params = array( 'jetpack-sso-invite-user' => 'failed', 'jetpack-sso-invite-error' => $error_code, '_wpnonce' => $nonce, ); $tracking_event_data = array( 'success' => 'false', 'error_code' => $error_code, ); $body = json_decode( wp_remote_retrieve_body( $response ) ); if ( ! empty( $body ) && ! empty( $body->message ) ) { $query_params['jetpack-sso-api-error-message'] = $body->message; $tracking_event_data['error_message'] = $body->message; } self::$tracking->record_user_event( $event, $tracking_event_data ); return self::create_error_notice_and_redirect( $query_params ); } $body = json_decode( wp_remote_retrieve_body( $response ) ); // access the first item since we're inviting one user. if ( is_array( $body ) && ! empty( $body ) ) { $body = $body[0]; } $query_params = array( 'jetpack-sso-invite-user' => $body->success ? 'success' : 'failed', '_wpnonce' => $nonce, ); if ( ! $body->success && $body->errors ) { $response_error = array_keys( (array) $body->errors ); $query_params['jetpack-sso-invite-error'] = $response_error[0]; self::$tracking->record_user_event( $event, array( 'success' => 'false', 'error_message' => $response_error[0], ) ); } else { self::$tracking->record_user_event( $event, array( 'success' => 'true' ) ); } return self::create_error_notice_and_redirect( $query_params ); } else { $error = 'invalid-user'; $query_params = array( 'jetpack-sso-invite-user' => 'failed', 'jetpack-sso-invite-error' => $error, '_wpnonce' => $nonce, ); self::$tracking->record_user_event( $event, array( 'success' => 'false', 'error_message' => $error, ) ); return self::create_error_notice_and_redirect( $query_params ); } wp_die(); } /** * Revokes a user's invitation to connect to WordPress.com. * * @param string $invite_id The ID of the invite to revoke. */ public function send_revoke_wpcom_invite( $invite_id ) { $blog_id = Manager::get_site_id( true ); $url = '/sites/' . $blog_id . '/invites/delete'; return Client::wpcom_json_api_request_as_user( $url, 'v2', array( 'method' => 'POST', ), array( 'invite_ids' => array( $invite_id ), ), 'wpcom' ); } /** * Handles logic to revoke user invite. */ public function handle_request_revoke_invite() { check_admin_referer( 'jetpack-sso-revoke-user-invite', 'revoke_invite_nonce' ); $nonce = wp_create_nonce( 'jetpack-sso-invite-user' ); $event = 'sso_user_invite_revoked'; if ( ! current_user_can( 'promote_users' ) ) { $error = 'invalid-revoke-permissions'; $query_params = array( 'jetpack-sso-invite-user' => 'failed', 'jetpack-sso-invite-error' => $error, '_wpnonce' => $nonce, ); return self::create_error_notice_and_redirect( $query_params ); } elseif ( isset( $_GET['user_id'] ) ) { $user_id = intval( wp_unslash( $_GET['user_id'] ) ); $user = get_user_by( 'id', $user_id ); if ( ! $user ) { $error = 'invalid-user-revoke'; $query_params = array( 'jetpack-sso-invite-user' => 'failed', 'jetpack-sso-invite-error' => $error, '_wpnonce' => $nonce, ); self::$tracking->record_user_event( $event, array( 'success' => 'false', 'error_message' => $error, ) ); return self::create_error_notice_and_redirect( $query_params ); } if ( ! isset( $_GET['invite_id'] ) ) { $error = 'invalid-invite-revoke'; $query_params = array( 'jetpack-sso-invite-user' => 'failed', 'jetpack-sso-invite-error' => $error, '_wpnonce' => $nonce, ); self::$tracking->record_user_event( $event, array( 'success' => 'false', 'error_message' => $error, ) ); return self::create_error_notice_and_redirect( $query_params ); } $invite_id = sanitize_text_field( wp_unslash( $_GET['invite_id'] ) ); $response = self::send_revoke_wpcom_invite( $invite_id ); if ( 200 !== wp_remote_retrieve_response_code( $response ) ) { $error = 'invalid-revoke-api-error'; $query_params = array( 'jetpack-sso-invite-user' => 'failed', 'jetpack-sso-invite-error' => $error, // general error message '_wpnonce' => $nonce, ); $tracking_event_data = array( 'success' => 'false', 'error_code' => $error, ); $body = json_decode( wp_remote_retrieve_body( $response ) ); if ( ! empty( $body ) && ! empty( $body->message ) ) { $query_params['jetpack-sso-api-error-message'] = $body->message; $tracking_event_data['error_message'] = $body->message; } self::$tracking->record_user_event( $event, $tracking_event_data ); return self::create_error_notice_and_redirect( $query_params ); } $body = json_decode( $response['body'] ); $query_params = array( 'jetpack-sso-invite-user' => $body->deleted ? 'successful-revoke' : 'failed', '_wpnonce' => $nonce, ); if ( ! $body->deleted ) { // no invite was deleted, probably it does not exist $error = 'invalid-invite-revoke'; $query_params['jetpack-sso-invite-error'] = $error; self::$tracking->record_user_event( $event, array( 'success' => 'false', 'error_message' => $error, ) ); } else { self::$tracking->record_user_event( $event, array( 'success' => 'true' ) ); } return self::create_error_notice_and_redirect( $query_params ); } else { $error = 'invalid-user-revoke'; $query_params = array( 'jetpack-sso-invite-user' => 'failed', 'jetpack-sso-invite-error' => $error, '_wpnonce' => $nonce, ); self::$tracking->record_user_event( $event, array( 'success' => 'false', 'error_message' => $error, ) ); return self::create_error_notice_and_redirect( $query_params ); } wp_die(); } /** * Handles resend user invite. */ public function handle_request_resend_invite() { check_admin_referer( 'jetpack-sso-resend-user-invite', 'resend_invite_nonce' ); $nonce = wp_create_nonce( 'jetpack-sso-invite-user' ); $event = 'sso_user_invite_resend'; if ( ! current_user_can( 'create_users' ) ) { $query_params = array( 'jetpack-sso-invite-user' => 'failed', 'jetpack-sso-invite-error' => 'invalid-user-permissions', '_wpnonce' => $nonce, ); return self::create_error_notice_and_redirect( $query_params ); } elseif ( isset( $_GET['invite_id'] ) ) { $invite_slug = sanitize_text_field( wp_unslash( $_GET['invite_id'] ) ); $blog_id = Manager::get_site_id( true ); $url = '/sites/' . $blog_id . '/invites/resend'; $response = Client::wpcom_json_api_request_as_user( $url, 'v2', array( 'method' => 'POST', ), array( 'invite_slug' => $invite_slug, ), 'wpcom' ); $status_code = wp_remote_retrieve_response_code( $response ); if ( 200 !== $status_code ) { $message_type = $status_code === 404 ? 'invalid-invite' : ''; // empty is the general error message $query_params = array( 'jetpack-sso-invite-user' => 'failed', 'jetpack-sso-invite-error' => $message_type, '_wpnonce' => $nonce, ); self::$tracking->record_user_event( $event, array( 'success' => 'false', 'error_message' => $message_type, ) ); return self::create_error_notice_and_redirect( $query_params ); } $body = json_decode( $response['body'] ); $invite_response_message = $body->success ? 'reinvited-success' : 'failed'; $query_params = array( 'jetpack-sso-invite-user' => $invite_response_message, '_wpnonce' => $nonce, ); if ( ! $body->success ) { self::$tracking->record_user_event( $event, array( 'success' => 'false', 'error_message' => $invite_response_message, ) ); } else { self::$tracking->record_user_event( $event, array( 'success' => 'true' ) ); } return self::create_error_notice_and_redirect( $query_params ); } else { $error = 'empty-invite'; $query_params = array( 'jetpack-sso-invite-user' => 'failed', 'jetpack-sso-invite-error' => 'empty-invite', '_wpnonce' => $nonce, ); self::$tracking->record_user_event( $event, array( 'success' => 'false', 'error_message' => $error, ) ); return self::create_error_notice_and_redirect( $query_params ); } } /** * Adds 'Revoke invite' and 'Resend invite' link to user table row actions. * Removes 'Reset password' link. * * @param array $actions - User row actions. * @param WP_User $user_object - User object. */ public function jetpack_user_table_row_actions( $actions, $user_object ) { $user_id = $user_object->ID; $has_pending_invite = self::has_pending_wpcom_invite( $user_id ); if ( current_user_can( 'promote_users' ) && $has_pending_invite ) { $nonce = wp_create_nonce( 'jetpack-sso-revoke-user-invite' ); $actions['sso_revoke_invite'] = sprintf( '<a class="jetpack-sso-revoke-invite-action" href="%s">%s</a>', add_query_arg( array( 'action' => 'jetpack_revoke_invite_user_to_wpcom', 'user_id' => $user_id, 'revoke_invite_nonce' => $nonce, 'invite_id' => $has_pending_invite, ), admin_url( 'admin-post.php' ) ), esc_html__( 'Revoke invite', 'jetpack-connection' ) ); } if ( current_user_can( 'promote_users' ) && $has_pending_invite ) { $nonce = wp_create_nonce( 'jetpack-sso-resend-user-invite' ); $actions['sso_resend_invite'] = sprintf( '<a class="jetpack-sso-resend-invite-action" href="%s">%s</a>', add_query_arg( array( 'action' => 'jetpack_resend_invite_user_to_wpcom', 'user_id' => $user_id, 'resend_invite_nonce' => $nonce, 'invite_id' => $has_pending_invite, ), admin_url( 'admin-post.php' ) ), esc_html__( 'Resend invite', 'jetpack-connection' ) ); } if ( current_user_can( 'promote_users' ) && ( $has_pending_invite || ( new Manager() )->is_user_connected( $user_id ) ) ) { unset( $actions['resetpassword'] ); } return $actions; } /** * Render the invitation email message. */ public function render_invitation_email_message() { $message = wp_kses( __( 'We highly recommend inviting users to join WordPress.com and log in securely using <a class="jetpack-sso-admin-create-user-invite-message-link-sso" rel="noopener noreferrer" target="_blank" href="https://jetpack.com/support/sso/">Secure Sign On</a> to ensure maximum security and efficiency.', 'jetpack-connection' ), array( 'a' => array( 'class' => array(), 'href' => array(), 'rel' => array(), 'target' => array(), ), ) ); wp_admin_notice( $message, array( 'id' => 'invitation_message', 'type' => 'info', 'dismissible' => false, 'additional_classes' => array( 'jetpack-sso-admin-create-user-invite-message' ), ) ); } /** * Render a note that wp.com invites will be automatically revoked. */ public function render_invitations_notices_for_deleted_users() { check_admin_referer( 'bulk-users' ); // When one user is deleted, the param is `user`, when multiple users are deleted, the param is `users`. // We start with `users` and fallback to `user`. $user_id = isset( $_GET['user'] ) ? intval( wp_unslash( $_GET['user'] ) ) : null; $user_ids = isset( $_GET['users'] ) ? array_map( 'intval', wp_unslash( $_GET['users'] ) ) : array( $user_id ); $users_with_invites = array_filter( $user_ids, function ( $user_id ) { return $user_id !== null && self::has_pending_wpcom_invite( $user_id ); } ); $users_with_invites = array_map( function ( $user_id ) { $user = get_user_by( 'id', $user_id ); return $user->user_login; }, $users_with_invites ); $invites_count = count( $users_with_invites ); if ( $invites_count > 0 ) { $users_with_invites = implode( ', ', $users_with_invites ); $message = wp_kses( sprintf( /* translators: %s is a comma-separated list of user logins. */ _n( 'WordPress.com invitation will be automatically revoked for user: <strong>%s</strong>.', 'WordPress.com invitations will be automatically revoked for users: <strong>%s</strong>.', $invites_count, 'jetpack-connection' ), $users_with_invites ), array( 'strong' => true ) ); wp_admin_notice( $message, array( 'id' => 'invitation_message', 'type' => 'info', 'dismissible' => false, 'additional_classes' => array( 'jetpack-sso-admin-create-user-invite-message' ), ) ); } } /** * Render WordPress.com invite checkbox for new user registration. * * @param string $type The type of new user form the hook follows. */ public function render_wpcom_invite_checkbox( $type ) { /* * Only check this box by default on WordPress.com sites * that do not use the WooCommerce plugin. */ $is_checked = ( new Host() )->is_wpcom_platform() && ! class_exists( 'WooCommerce' ); if ( $type === 'add-new-user' ) { ?> <table class="form-table"> <tr class="form-field"> <th scope="row"> <label for="invite_user_wpcom"><?php esc_html_e( 'Invite user', 'jetpack-connection' ); ?></label> </th> <td> <fieldset> <legend class="screen-reader-text"> <span><?php esc_html_e( 'Invite user', 'jetpack-connection' ); ?></span> </legend> <label for="invite_user_wpcom"> <input name="invite_user_wpcom" type="checkbox" id="invite_user_wpcom" <?php checked( $is_checked ); ?> > <?php esc_html_e( 'Invite user to WordPress.com', 'jetpack-connection' ); ?> </label> </fieldset> </td> </tr> </table> <?php } } /** * Render a checkbox to differentiate if a user is external. * * @param string $type The type of new user form the hook follows. */ public function render_wpcom_external_user_checkbox( $type ) { // Only enable this feature on WordPress.com sites. if ( ! ( new Host() )->is_wpcom_platform() ) { return; } if ( $type === 'add-new-user' ) { ?> <table class="form-table"> <tr class="form-field"> <th scope="row"> <label for="user_external_contractor"><?php esc_html_e( 'External User', 'jetpack-connection' ); ?></label> </th> <td> <fieldset> <legend class="screen-reader-text"> <span><?php esc_html_e( 'Invite user', 'jetpack-connection' ); ?></span> </legend> <label for="user_external_contractor"> <input name="user_external_contractor" type="checkbox" id="user_external_contractor" > <?php esc_html_e( 'This user is a contractor, freelancer, consultant, or agency.', 'jetpack-connection' ); ?> </label> </fieldset> </td> </tr> </table> <?php } } /** * Render the custom email message form field for new user registration. * * @param string $type The type of new user form the hook follows. */ public function render_custom_email_message_form_field( $type ) { if ( $type === 'add-new-user' ) { $valid_nonce = isset( $_POST['_wpnonce_create-user'] ) ? wp_verify_nonce( sanitize_key( $_POST['_wpnonce_create-user'] ), 'create-user' ) : false; $custom_email_message = ( $valid_nonce && isset( $_POST['custom_email_message'] ) ) ? sanitize_text_field( wp_unslash( $_POST['custom_email_message'] ) ) : ''; ?> <table class="form-table" id="custom_email_message_block"> <tr class="form-field"> <th scope="row"> <label for="custom_email_message"><?php esc_html_e( 'Custom Message', 'jetpack-connection' ); ?></label> </th> <td> <label for="custom_email_message"> <textarea aria-describedby="custom_email_message_description" rows="3" maxlength="500" id="custom_email_message" name="custom_email_message"><?php echo esc_html( $custom_email_message ); ?></textarea> <p id="custom_email_message_description"> <?php esc_html_e( 'This user will be invited to WordPress.com. You can include a personalized welcome message with the invitation.', 'jetpack-connection' ); ?> </label> </td> </tr> </table> <?php } } /** * Conditionally disable the core invitation email. * It should be sent when SSO is disabled or when admins opt-out of WordPress.com invites intentionally. * If the "Send User Notification" checkbox is checked, the core invitation email should be sent. * * @param boolean $send_wp_email Whether the core invitation email should be sent. * * @return boolean Indicating if the core invitation main should be sent. */ public function should_send_wp_mail_new_user( $send_wp_email ) { if ( ! isset( $_POST['invite_user_wpcom'] ) && isset( $_POST['send_user_notification'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Hooked to 'wp_send_new_user_notification_to_user' to conditionally disable the core invitation email. At this point nonces should be checked already. return $send_wp_email; } return false; } /** * Send user invitation to WordPress.com if user has no errors. * * @param WP_Error $errors The WP_Error object. * @param bool $update Whether the user is being updated or not. * @param \stdClass $user The User object about to be created. * @return WP_Error The modified or not WP_Error object. */ public function send_wpcom_mail_user_invite( $errors, $update, $user ) { // Only admins should be able to invite new users. if ( ! current_user_can( 'create_users' ) ) { return $errors; } if ( $update ) { return $errors; } // check for a valid nonce. if ( ! isset( $_POST['_wpnonce_create-user'] ) || ! wp_verify_nonce( sanitize_key( $_POST['_wpnonce_create-user'] ), 'create-user' ) ) { return $errors; } // Check if the user is being invited to WordPress.com. if ( ! isset( $_POST['invite_user_wpcom'] ) ) { return $errors; } // check if the custom email message is too long. if ( ! empty( $_POST['custom_email_message'] ) && strlen( sanitize_text_field( wp_unslash( $_POST['custom_email_message'] ) ) ) > 500 ) { $errors->add( 'custom_email_message', wp_kses( __( '<strong>Error</strong>: The custom message is too long. Please keep it under 500 characters.', 'jetpack-connection' ), array( 'strong' => array(), ) ) ); } $site_id = Manager::get_site_id( true ); if ( ! $site_id ) { $errors->add( 'invalid_site_id', wp_kses( __( '<strong>Error</strong>: Invalid site ID.', 'jetpack-connection' ), array( 'strong' => array(), ) ) ); } // Bail if there are any errors. if ( $errors->has_errors() ) { return $errors; } $new_user_request = array( 'email_or_username' => sanitize_email( $user->user_email ), 'role' => sanitize_key( $user->role ), ); if ( isset( $_POST['custom_email_message'] ) && strlen( sanitize_text_field( wp_unslash( $_POST['custom_email_message'] ) ) ) > 0 ) { $new_user_request['message'] = sanitize_text_field( wp_unslash( $_POST['custom_email_message'] ) ); } if ( isset( $_POST['user_external_contractor'] ) ) { $new_user_request['is_external'] = true; } $response = Client::wpcom_json_api_request_as_user( sprintf( '/sites/%d/invites/new', (int) $site_id ), '2', // Api version array( 'method' => 'POST', ), array( 'invitees' => array( $new_user_request ), ) ); $event_name = 'sso_new_user_invite_sent'; $custom_message_sent = isset( $new_user_request['message'] ) ? 'true' : 'false'; if ( 200 !== wp_remote_retrieve_response_code( $response ) ) { $errors->add( 'invitation_not_sent', wp_kses( __( '<strong>Error</strong>: The user invitation email could not be sent, the user account was not created.', 'jetpack-connection' ), array( 'strong' => array(), ) ) ); self::$tracking->record_user_event( $event_name, array( 'success' => 'false', 'error' => wp_remote_retrieve_body( $response ), // Get as much information as possible. ) ); } else { self::$tracking->record_user_event( $event_name, array( 'success' => 'true', 'custom_message_sent' => $custom_message_sent, ) ); } return $errors; } /** * Deprecated method. Adds a column in the user admin table to display user connection status and actions. * * @param array $columns User list table columns. * @return array * @deprecated 6.5.0 */ public function jetpack_user_connected_th( $columns ) { _deprecated_function( __METHOD__, 'package-6.5.0' ); return $columns; } /** * Executed when our WP_User_Query instance is set, and we don't have cached invites. * This function uses the user emails and the 'are-users-invited' endpoint to build the cache. * * @return void */ private static function rebuild_invite_cache() { $blog_id = Manager::get_site_id( true ); if ( self::$cached_invites === null && self::$user_search !== null ) { self::$cached_invites = array(); $results = self::$user_search->get_results(); $user_emails = array_reduce( $results, function ( $current, $item ) { if ( ! ( new Manager() )->is_user_connected( $item->ID ) ) { $current[] = rawurlencode( $item->user_email ); } else { self::$cached_invites[] = array( 'email_or_username' => $item->user_email, 'invited' => false, 'invite_code' => '', ); } return $current; }, array() ); if ( ! empty( $user_emails ) ) { $url = '/sites/' . $blog_id . '/invites/are-users-invited'; $response = Client::wpcom_json_api_request_as_user( $url, 'v2', array( 'method' => 'POST', ), array( 'users' => $user_emails ), 'wpcom' ); if ( 200 === wp_remote_retrieve_response_code( $response ) ) { $body = json_decode( $response['body'], true ); // ensure array_merge happens with the right parameters if ( empty( $body ) ) { $body = array(); } self::$cached_invites = array_merge( self::$cached_invites, $body ); } } } } /** * Check if there is cached invite for a user email. * * @access private * @static * * @param string $email The user email. * * @return array|void Returns the cached invite if found. */ public static function get_pending_cached_wpcom_invite( $email ) { if ( self::$cached_invites === null ) { self::rebuild_invite_cache(); } if ( ! empty( self::$cached_invites ) && is_array( self::$cached_invites ) ) { $index = array_search( $email, array_column( self::$cached_invites, 'email_or_username' ), true ); if ( $index !== false ) { return self::$cached_invites[ $index ]; } } } /** * Check if a given user is invited to the site. * * @access private * @static * @param int $user_id The user ID. * * @return false|string returns the user invite code if the user is invited, false otherwise. */ private static function has_pending_wpcom_invite( $user_id ) { $user = get_user_by( 'id', $user_id ); if ( ! $user instanceof \WP_User ) { return false; } $blog_id = Manager::get_site_id( true ); $cached_invite = self::get_pending_cached_wpcom_invite( $user->user_email ); if ( $cached_invite ) { return $cached_invite['invite_code']; } $url = '/sites/' . $blog_id . '/invites/is-invited'; $url = add_query_arg( array( 'email_or_username' => rawurlencode( $user->user_email ), ), $url ); $response = Client::wpcom_json_api_request_as_user( $url, 'v2', array(), null, 'wpcom' ); if ( 200 !== wp_remote_retrieve_response_code( $response ) ) { return false; } $body_response = wp_remote_retrieve_body( $response ); if ( empty( $body_response ) ) { return false; } $body = json_decode( $body_response, true ); if ( ! empty( $body['invite_code'] ) ) { return $body['invite_code']; } return false; } /** * Delete an external contributor from the site. * * @access private * @static * @param int $user_id The user ID. * * @return bool Returns true if the user was successfully deleted, false otherwise. */ private static function delete_external_contributor( $user_id ) { $blog_id = Manager::get_site_id( true ); $url = '/sites/' . $blog_id . '/external-contributors/remove'; $response = Client::wpcom_json_api_request_as_user( $url, 'v2', array( 'method' => 'POST', ), array( 'user_id' => $user_id, ), 'wpcom' ); if ( 200 !== wp_remote_retrieve_response_code( $response ) ) { return false; } return true; } /** * Show Jetpack SSO user connection status. * * @param string $val HTML for the column. * @param string $col User list table column. * @param int $user_id User ID. * @return string Modified column content. */ public function jetpack_show_connection_status( $val, $col, $user_id ) { if ( 'user_jetpack' !== $col ) { return $val; } // Get base connection status from parent $connection_status = parent::render_connection_column( '', $col, $user_id ); // If user is not connected, check for pending invite if ( ! $connection_status ) { $has_pending_invite = self::has_pending_wpcom_invite( $user_id ); if ( $has_pending_invite ) { return sprintf( '<span title="%1$s" class="jetpack-sso-invitation sso-pending-invite">%2$s</span>', esc_attr__( 'This user didn’t accept the invitation to join this site yet.', 'jetpack-connection' ), esc_html__( 'Pending invite', 'jetpack-connection' ) ); } // Show invite button for non-connected users $nonce = wp_create_nonce( 'jetpack-sso-invite-user' ); return sprintf( '<span tabindex="0" role="tooltip" aria-label="%4$s: %3$s" class="jetpack-sso-invitation-tooltip-icon sso-disconnected-user"> <a href="%1$s" class="jetpack-sso-invitation sso-disconnected-user">%2$s</a> <span class="sso-disconnected-user-icon dashicons dashicons-warning"> <span class="jetpack-sso-invitation-tooltip jetpack-sso-td-tooltip">%3$s</span> </span> </span>', add_query_arg( array( 'user_id' => $user_id, 'invite_nonce' => $nonce, 'action' => 'jetpack_invite_user_to_wpcom', ), admin_url( 'admin-post.php' ) ), esc_html__( 'Send invite', 'jetpack-connection' ), esc_attr__( 'This user doesn’t have a Jetpack SSO connection to WordPress.com. Invite them to the site to increase security and improve their experience.', 'jetpack-connection' ), esc_attr__( 'Tooltip', 'jetpack-connection' ) ); } return $connection_status; } /** * Creates error notices and redirects the user to the previous page. * * @param array $query_params - query parameters added to redirection URL. * @phan-suppress PhanPluginNeverReturnMethod */ public function create_error_notice_and_redirect( $query_params ) { $ref = wp_get_referer(); if ( empty( $ref ) ) { $ref = network_admin_url( 'users.php' ); } $url = add_query_arg( $query_params, $ref ); wp_safe_redirect( $url ); exit; } /** * Style the Jetpack user rows and columns. */ public function jetpack_user_table_styles() { ?> <style> #the-list tr:has(.sso-disconnected-user) { background: #F5F1E1; } #the-list tr:has(.sso-pending-invite) { background: #E9F0F5; } .jetpack-sso-invitation { background: none; border: none; color: #50575e; padding: 0; text-align: unset; } .jetpack-sso-invitation.sso-disconnected-user { color: #0073aa; cursor: pointer; text-decoration: underline; } .jetpack-sso-invitation.sso-disconnected-user:hover, .jetpack-sso-invitation.sso-disconnected-user:focus, .jetpack-sso-invitation.sso-disconnected-user:active { color: #0096dd; } .sso-disconnected-user-icon { margin-left: 4px; cursor: pointer; background: gray; border-radius: 10px; } .sso-disconnected-user-icon.dashicons { font-size: 1rem; height: 1rem; width: 1rem; background-color: #9D6E00; color: #F5F1E1; } .jetpack-sso-invitation-tooltip-icon{ position: relative; cursor: pointer; } .jetpack-sso-td-tooltip { left: -256px; } .jetpack-sso-invitation-tooltip { position: absolute; background: #f6f7f7; top: -85px; width: 250px; padding: 7px; color: #3c434a; font-size: .75rem; line-height: 17px; text-align: left; margin: 0; display: none; border-radius: 4px; font-family: sans-serif; box-shadow: 5px 10px 10px rgba(0, 0, 0, 0.1); } </style> <?php } /** * Enqueue SSO-specific scripts. * * @param string $hook The current admin page. */ public function enqueue_scripts( $hook ) { if ( 'users.php' !== $hook ) { return; } parent::enqueue_scripts( $hook ); // Enqueue the SSO users script. Assets::register_script( 'jetpack-sso-users', '../../dist/jetpack-sso-users.js', __FILE__, array( 'strategy' => 'defer', 'in_footer' => true, 'enqueue' => true, 'version' => Package_Version::PACKAGE_VERSION, ) ); } } jetpack-connection/src/sso/jetpack-sso-users.js 0000777 00000004057 15251741406 0015657 0 ustar 00 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' ); 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 ( tooltip.ownerDocument.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 ( event.target.ownerDocument.activeElement === event.target ) { return; } this.querySelector( '.jetpack-sso-invitation-tooltip' ).style.display = 'none'; } } ); jetpack-connection/src/sso/jetpack-sso-login.css 0000777 00000005172 15251741406 0016001 0 ustar 00 #loginform { /* We set !important because sometimes static is added inline */ position: relative !important; padding-bottom: 92px; } .jetpack-sso-repositioned #loginform { padding-bottom: 26px; } #loginform #jetpack-sso-wrap, #loginform #jetpack-sso-wrap * { box-sizing: border-box; } #jetpack-sso-wrap__action, #jetpack-sso-wrap__user { display: none; } .jetpack-sso-form-display #jetpack-sso-wrap__action, .jetpack-sso-form-display #jetpack-sso-wrap__user { display: block; } #jetpack-sso-wrap { position: absolute; bottom: 20px; padding: 0 24px; margin-left: -24px; margin-right: -24px; width: 100%; } .jetpack-sso-repositioned #jetpack-sso-wrap { position: relative; bottom: auto; padding: 0; margin-top: 16px; margin-left: 0; margin-right: 0; } .jetpack-sso-form-display #jetpack-sso-wrap { position: relative; bottom: auto; padding: 0; margin-top: 0; margin-left: 0; margin-right: 0; } #loginform #jetpack-sso-wrap p { color: #777; margin-bottom: 16px; } #jetpack-sso-wrap a { display: block; width: 100%; text-align: center; text-decoration: none; } #jetpack-sso-wrap .jetpack-sso-toggle.wpcom { display: none; } .jetpack-sso-form-display #jetpack-sso-wrap .jetpack-sso-toggle.wpcom { display: block; } .jetpack-sso-form-display #jetpack-sso-wrap .jetpack-sso-toggle.default { display: none; } .jetpack-sso-form-display #loginform > p, .jetpack-sso-form-display #loginform > div { display: none; } .jetpack-sso-form-display #loginform #jetpack-sso-wrap { display: block; } .jetpack-sso-form-display #loginform { padding: 26px 24px; } .jetpack-sso-or { margin-bottom: 16px; position: relative; text-align: center; } .jetpack-sso-or::before { background: #dcdcde; content: ""; height: 1px; position: absolute; left: 0; top: 50%; width: 100%; } .jetpack-sso-or span { background: #fff; color: #777; position: relative; padding: 0 8px; text-transform: uppercase; } #jetpack-sso-wrap .button { display: flex; justify-content: center; align-items: center; height: 36px; margin-bottom: 16px; width: 100%; } #jetpack-sso-wrap .button .genericon-wordpress { font-size: 24px; margin-right: 4px; } #jetpack-sso-wrap__user img { border-radius: 50%; display: block; margin: 0 auto 16px; } #jetpack-sso-wrap__user h2 { font-size: 21px; font-weight: 300; margin-bottom: 16px; text-align: center; } #jetpack-sso-wrap__user h2 span { font-weight: 700; } .jetpack-sso-wrap__reauth { margin-bottom: 16px; } .jetpack-sso-form-display #nav { display: none; } .jetpack-sso-form-display #backtoblog { margin: 24px 0 0; } .jetpack-sso-clear::after { content: ""; display: table; clear: both; } jetpack-connection/src/sso/jetpack-sso-admin-create-user.css 0000777 00000000543 15251741406 0020173 0 ustar 00 .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; } jetpack-connection/src/sso/class-sso.php 0000777 00000116371 15251741406 0014362 0 ustar 00 <?php /** * SSO feature. Entry point. * * @package automattic/jetpack-connection */ namespace Automattic\Jetpack\Connection; use Automattic\Jetpack\Assets; use Automattic\Jetpack\Connection\SSO\Force_2FA; use Automattic\Jetpack\Connection\SSO\Helpers; use Automattic\Jetpack\Connection\SSO\Notices; use Automattic\Jetpack\Connection\SSO\User_Admin; use Automattic\Jetpack\Connection\Webhooks\Authorize_Redirect; use Automattic\Jetpack\Roles; use Automattic\Jetpack\Status; use Automattic\Jetpack\Status\Host; use Automattic\Jetpack\Tracking; use Jetpack_IXR_Client; use WP_Error; use WP_User; use WP_User_Query; /** * SSO feature main class. */ class SSO { /** * WordPress.com User information. * * @var false|object */ private $user_data; /** * Automattic\Jetpack\Connection\SSO instance. * * @var \Automattic\Jetpack\Connection\SSO */ public static $instance = null; /** * Automattic\Jetpack\Connection\SSO constructor. */ private function __construct() { self::$instance = $this; add_action( 'admin_init', array( $this, 'maybe_authorize_user_after_sso' ), 1 ); add_action( 'admin_init', array( $this, 'register_settings' ) ); add_action( 'login_init', array( $this, 'login_init' ) ); add_filter( 'jetpack_xmlrpc_methods', array( $this, 'xmlrpc_methods' ) ); add_action( 'init', array( $this, 'maybe_logout_user' ), 5 ); add_action( 'login_form_logout', array( $this, 'store_wpcom_profile_cookies_on_logout' ) ); add_action( 'jetpack_unlinked_user', array( Helpers::class, 'delete_connection_for_user' ) ); add_action( 'jetpack_site_before_disconnected', array( static::class, 'disconnect' ) ); add_action( 'wp_login', array( static::class, 'clear_cookies_after_login' ) ); // Adding this action so that on login_init, the action won't be sanitized out of the $action global. add_action( 'login_form_jetpack-sso', '__return_true' ); add_filter( 'wp_login_errors', array( $this, 'sso_reminder_logout_wpcom' ) ); // Synchronize SSO options with WordPress.com. add_filter( 'jetpack_sync_callable_whitelist', array( $this, 'sync_sso_callables' ), 10, 1 ); /** * Filter to include Force 2FA feature. * * By default, `manage_options` users are forced when enable. The capability can be modified * with the `jetpack_force_2fa_cap` filter. * * To enable the feature, add the following code: * add_filter( 'jetpack_force_2fa', '__return_true' ); * * @param bool $force_2fa Whether to force 2FA or not. * * @todo Provide a UI to enable/disable the feature. * * @since jetpack-12.7 * @module SSO * @return bool */ if ( ! class_exists( 'Automattic\Jetpack\Connection\SSO\Force_2FA', false ) && apply_filters( 'jetpack_force_2fa', false ) ) { new Force_2FA(); } /* * Allow admins to invite new users to create a WordPress.com account * as they are added to the site. * * This is a feature that is only available when the admin is connected to WordPress.com. */ if ( ( new Manager() )->is_user_connected() && ! is_multisite() && /** * Toggle the ability to invite new users to create a WordPress.com account. * * @module sso * * @since 2.7.2 * * @param bool true Whether to allow admins to invite new users to create a WordPress.com account. */ apply_filters( 'jetpack_sso_invite_new_users_wpcom', true ) ) { new User_Admin(); } } /** * Returns the single instance of the Automattic\Jetpack\Connection\SSO object * * @since jetpack-2.8 * @return \Automattic\Jetpack\Connection\SSO */ public static function get_instance() { if ( self::$instance !== null ) { return self::$instance; } self::$instance = new SSO(); return self::$instance; } /** * Add SSO callables to the sync whitelist. * * @since 2.8.1 * * @param array $callables list of callables. * * @return array list of callables. */ public function sync_sso_callables( $callables ) { $sso_callables = array( 'sso_is_two_step_required' => array( Helpers::class, 'is_two_step_required' ), 'sso_should_hide_login_form' => array( Helpers::class, 'should_hide_login_form' ), 'sso_match_by_email' => array( Helpers::class, 'match_by_email' ), 'sso_new_user_override' => array( Helpers::class, 'new_user_override' ), 'sso_bypass_default_login_form' => array( Helpers::class, 'bypass_login_forward_wpcom' ), ); return array_merge( $callables, $sso_callables ); } /** * Safety heads-up added to the logout messages when SSO is enabled. * Some folks on a shared computer don't know that they need to log out of WordPress.com as well. * * @param WP_Error $errors WP_Error object. */ public function sso_reminder_logout_wpcom( $errors ) { if ( ( new Host() )->is_wpcom_platform() ) { return $errors; } if ( ! empty( $errors->errors['loggedout'] ) ) { $logout_message = wp_kses( sprintf( /* translators: %1$s is a link to the WordPress.com account settings page. */ __( 'If you are on a shared computer, remember to also <a href="%1$s">log out of WordPress.com</a>.', 'jetpack-connection' ), 'https://wordpress.com/me' ), array( 'a' => array( 'href' => array(), ), ) ); $errors->add( 'jetpack-sso-show-logout', $logout_message, 'message' ); } return $errors; } /** * If jetpack_force_logout == 1 in current user meta the user will be forced * to logout and reauthenticate with the site. **/ public function maybe_logout_user() { global $current_user; if ( 1 === (int) $current_user->jetpack_force_logout ) { delete_user_meta( $current_user->ID, 'jetpack_force_logout' ); Helpers::delete_connection_for_user( $current_user->ID ); wp_logout(); wp_safe_redirect( wp_login_url() ); exit( 0 ); } } /** * Adds additional methods the WordPress xmlrpc API for handling SSO specific features * * @param array $methods API methods. * @return array **/ public function xmlrpc_methods( $methods ) { $methods['jetpack.userDisconnect'] = array( $this, 'xmlrpc_user_disconnect' ); return $methods; } /** * Marks a user's profile for disconnect from WordPress.com and forces a logout * the next time the user visits the site. * * @param int $user_id User to disconnect from the site. **/ public function xmlrpc_user_disconnect( $user_id ) { $user_query = new WP_User_Query( array( 'meta_key' => 'wpcom_user_id', 'meta_value' => $user_id, ) ); $user = $user_query->get_results(); $user = $user[0]; if ( $user instanceof WP_User ) { $user = wp_set_current_user( $user->ID ); update_user_meta( $user->ID, 'jetpack_force_logout', '1' ); Helpers::delete_connection_for_user( $user->ID ); return true; } return false; } /** * Enqueues scripts and styles necessary for SSO login. */ public function login_enqueue_scripts() { global $action; if ( ! Helpers::display_sso_form_for_action( $action ) ) { return; } Assets::register_script( 'jetpack-sso-login', '../../dist/jetpack-sso-login.js', __FILE__, array( 'enqueue' => true, 'version' => Package_Version::PACKAGE_VERSION, ) ); } /** * Adds Jetpack SSO classes to login body * * @param array $classes Array of classes to add to body tag. * @return array Array of classes to add to body tag. */ public function login_body_class( $classes ) { global $action; if ( ! Helpers::display_sso_form_for_action( $action ) ) { return $classes; } // Always add the jetpack-sso class so that we can add SSO specific styling even when the SSO form isn't being displayed. $classes[] = 'jetpack-sso'; if ( ! ( new Status() )->in_safe_mode() ) { /** * Should we show the SSO login form? * * $_GET['jetpack-sso-default-form'] is used to provide a fallback in case JavaScript is not enabled. * * The default_to_sso_login() method allows us to dynamically decide whether we show the SSO login form or not. * The SSO module uses the method to display the default login form if we cannot find a user to log in via SSO. * But, the method could be filtered by a site admin to always show the default login form if that is preferred. */ if ( empty( $_GET['jetpack-sso-show-default-form'] ) && Helpers::show_sso_login() ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $classes[] = 'jetpack-sso-form-display'; } } return $classes; } /** * Inlined admin styles for SSO. */ public function print_inline_admin_css() { ?> <style> .jetpack-sso .message { margin-top: 20px; } .jetpack-sso #login .message:first-child, .jetpack-sso #login h1 + .message { margin-top: 0; } </style> <?php } /** * Adds settings fields to Settings > General > Secure Sign On that allows users to * turn off the login form on wp-login.php * * @since jetpack-2.7 **/ public function register_settings() { add_settings_section( 'jetpack_sso_settings', __( 'Secure Sign On', 'jetpack-connection' ), '__return_false', 'jetpack-sso' ); /* * Settings > General > Secure Sign On * Require two step authentication */ register_setting( 'jetpack-sso', 'jetpack_sso_require_two_step', array( $this, 'validate_jetpack_sso_require_two_step' ) ); add_settings_field( 'jetpack_sso_require_two_step', '', // Output done in render $callback: __( 'Require Two-Step Authentication' , 'jetpack-connection' ). array( $this, 'render_require_two_step' ), 'jetpack-sso', 'jetpack_sso_settings' ); /* * Settings > General > Secure Sign On */ register_setting( 'jetpack-sso', 'jetpack_sso_match_by_email', array( $this, 'validate_jetpack_sso_match_by_email' ) ); add_settings_field( 'jetpack_sso_match_by_email', '', // Output done in render $callback: __( 'Match by Email' , 'jetpack-connection' ). array( $this, 'render_match_by_email' ), 'jetpack-sso', 'jetpack_sso_settings' ); } /** * Builds the display for the checkbox allowing user to require two step * auth be enabled on WordPress.com accounts before login. Displays in Settings > General * * @since jetpack-2.7 **/ public function render_require_two_step() { ?> <label> <input type="checkbox" name="jetpack_sso_require_two_step" <?php checked( Helpers::is_two_step_required() ); ?> <?php disabled( Helpers::is_require_two_step_checkbox_disabled() ); ?> > <?php esc_html_e( 'Require Two-Step Authentication', 'jetpack-connection' ); ?> </label> <?php } /** * Validate the require two step checkbox in Settings > General. * * @param bool $input The jetpack_sso_require_two_step option setting. * * @since jetpack-2.7 * @return int **/ public function validate_jetpack_sso_require_two_step( $input ) { return ( ! empty( $input ) ) ? 1 : 0; } /** * Builds the display for the checkbox allowing the user to allow matching logins by email * Displays in Settings > General * * @since jetpack-2.9 **/ public function render_match_by_email() { ?> <label> <input type="checkbox" name="jetpack_sso_match_by_email" <?php checked( Helpers::match_by_email() ); ?> <?php disabled( Helpers::is_match_by_email_checkbox_disabled() ); ?> > <?php esc_html_e( 'Match by Email', 'jetpack-connection' ); ?> </label> <?php } /** * Validate the match by email check in Settings > General. * * @param bool $input The jetpack_sso_match_by_email option setting. * * @since jetpack-2.9 * @return int **/ public function validate_jetpack_sso_match_by_email( $input ) { return ( ! empty( $input ) ) ? 1 : 0; } /** * Checks to determine if the user wants to login on wp-login * * This function mostly exists to cover the exceptions to login * that may exist as other parameters to $_GET[action] as $_GET[action] * does not have to exist. By default WordPress assumes login if an action * is not set, however this may not be true, as in the case of logout * where $_GET[loggedout] is instead set * * @return boolean **/ private function wants_to_login() { $wants_to_login = false; // Cover default WordPress behavior. $action = isset( $_REQUEST['action'] ) ? filter_var( wp_unslash( $_REQUEST['action'] ) ) : 'login'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended // And now the exceptions. $action = isset( $_GET['loggedout'] ) ? 'loggedout' : $action; // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( Helpers::display_sso_form_for_action( $action ) ) { $wants_to_login = true; } return $wants_to_login; } /** * Initialization for a SSO request. */ public function login_init() { global $action; $tracking = new Tracking(); if ( Helpers::should_hide_login_form() ) { /** * Since the default authenticate filters fire at priority 20 for checking username and password, * let's fire at priority 30. wp_authenticate_spam_check is fired at priority 99, but since we return a * WP_Error in disable_default_login_form, then we won't trigger spam processing logic. */ add_filter( 'authenticate', array( Notices::class, 'disable_default_login_form' ), 30 ); /** * Filter the display of the disclaimer message appearing when default WordPress login form is disabled. * * @module sso * * @since jetpack-2.8.0 * * @param bool true Should the disclaimer be displayed. Default to true. */ $display_sso_disclaimer = apply_filters( 'jetpack_sso_display_disclaimer', true ); if ( $display_sso_disclaimer ) { add_filter( 'login_message', array( Notices::class, 'msg_login_by_jetpack' ) ); } } if ( 'jetpack-sso' === $action ) { if ( isset( $_GET['result'] ) && isset( $_GET['user_id'] ) && isset( $_GET['sso_nonce'] ) && 'success' === $_GET['result'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $this->handle_login(); $this->display_sso_login_form(); } elseif ( ( new Status() )->in_safe_mode() ) { add_filter( 'login_message', array( Notices::class, 'sso_not_allowed_in_safe_mode' ) ); } else { // Is it wiser to just use wp_redirect than do this runaround to wp_safe_redirect? add_filter( 'allowed_redirect_hosts', array( Helpers::class, 'allowed_redirect_hosts' ) ); $reauth = ! empty( $_GET['force_reauth'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended $sso_url = $this->get_sso_url_or_die( $reauth ); $tracking->record_user_event( 'sso_login_redirect_success' ); wp_safe_redirect( $sso_url ); exit( 0 ); } } elseif ( Helpers::display_sso_form_for_action( $action ) ) { // Save cookies so we can handle redirects after SSO. static::save_cookies(); /** * Check to see if the site admin wants to automagically forward the user * to the WordPress.com login page AND that the request to wp-login.php * is not something other than login (Like logout!) */ if ( Helpers::bypass_login_forward_wpcom() && $this->wants_to_login() ) { add_filter( 'allowed_redirect_hosts', array( Helpers::class, 'allowed_redirect_hosts' ) ); $reauth = ! empty( $_GET['force_reauth'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended $sso_url = $this->get_sso_url_or_die( $reauth ); $tracking->record_user_event( 'sso_login_redirect_bypass_success' ); wp_safe_redirect( $sso_url ); exit( 0 ); } $this->display_sso_login_form(); } } /** * Ensures that we can get a nonce from WordPress.com via XML-RPC before setting * up the hooks required to display the SSO form. */ public function display_sso_login_form() { add_filter( 'login_body_class', array( $this, 'login_body_class' ) ); add_action( 'login_head', array( $this, 'print_inline_admin_css' ) ); if ( ( new Status() )->in_safe_mode() ) { add_filter( 'login_message', array( Notices::class, 'sso_not_allowed_in_safe_mode' ) ); return; } $sso_nonce = self::request_initial_nonce(); if ( is_wp_error( $sso_nonce ) ) { return; } add_action( 'login_form', array( $this, 'login_form' ) ); add_action( 'login_enqueue_scripts', array( $this, 'login_enqueue_scripts' ) ); } /** * Conditionally save the redirect_to url as a cookie. * * @since jetpack-4.6.0 Renamed to save_cookies from maybe_save_redirect_cookies */ public static function save_cookies() { if ( headers_sent() ) { return new WP_Error( 'headers_sent', __( 'Cannot deal with cookie redirects, as headers are already sent.', 'jetpack-connection' ) ); } setcookie( 'jetpack_sso_original_request', // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sniff misses the wrapping esc_url_raw(). esc_url_raw( set_url_scheme( ( isset( $_SERVER['HTTP_HOST'] ) ? wp_unslash( $_SERVER['HTTP_HOST'] ) : '' ) . ( isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : '' ) ) ), time() + HOUR_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), true ); if ( ! empty( $_GET['redirect_to'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended // If we have something to redirect to. $url = esc_url_raw( wp_unslash( $_GET['redirect_to'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended setcookie( 'jetpack_sso_redirect_to', $url, time() + HOUR_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), true ); } elseif ( ! empty( $_COOKIE['jetpack_sso_redirect_to'] ) ) { // Otherwise, if it's already set, purge it. setcookie( 'jetpack_sso_redirect_to', ' ', time() - YEAR_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), true ); } } /** * Outputs the Jetpack SSO button and description as well as the toggle link * for switching between Jetpack SSO and default login. */ public function login_form() { $site_name = get_bloginfo( 'name' ); if ( ! $site_name ) { $site_name = get_bloginfo( 'url' ); } $display_name = ! empty( $_COOKIE[ 'jetpack_sso_wpcom_name_' . COOKIEHASH ] ) ? sanitize_text_field( wp_unslash( $_COOKIE[ 'jetpack_sso_wpcom_name_' . COOKIEHASH ] ) ) : false; $gravatar = ! empty( $_COOKIE[ 'jetpack_sso_wpcom_gravatar_' . COOKIEHASH ] ) ? esc_url_raw( wp_unslash( $_COOKIE[ 'jetpack_sso_wpcom_gravatar_' . COOKIEHASH ] ) ) : false; ?> <div id="jetpack-sso-wrap"> <?php /** * Allow extension above Jetpack's SSO form. * * @module sso * * @since jetpack-8.6.0 */ do_action( 'jetpack_sso_login_form_above_wpcom' ); if ( $display_name && $gravatar ) : ?> <div id="jetpack-sso-wrap__user"> <img width="72" height="72" src="<?php echo esc_html( $gravatar ); ?>" /> <h2> <?php echo wp_kses( /* translators: %s a user display name. */ sprintf( __( 'Log in as <span>%s</span>', 'jetpack-connection' ), esc_html( $display_name ) ), array( 'span' => true ) ); ?> </h2> </div> <?php endif; ?> <div id="jetpack-sso-wrap__action"> <?php echo $this->build_sso_button( array(), true ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaping done in build_sso_button() ?> <?php if ( $display_name && $gravatar ) : ?> <a rel="nofollow" class="jetpack-sso-wrap__reauth" href="<?php echo esc_url( $this->build_sso_button_url( array( 'force_reauth' => '1' ) ) ); ?>"> <?php esc_html_e( 'Log in with another WordPress.com account', 'jetpack-connection' ); ?> </a> <?php else : ?> <p> <?php /** * Filter the messeage displayed below the SSO button. * * @module sso * * @since jetpack-10.3.0 * * @param string $sso_explanation Message displayed below the SSO button. */ $sso_explanation = apply_filters( 'jetpack_sso_login_form_explanation_text', sprintf( /* Translators: %s is the name of the site. */ __( 'You can now save time spent logging in by connecting your WordPress.com account to %s.', 'jetpack-connection' ), esc_html( $site_name ) ) ); echo esc_html( $sso_explanation ); ?> </p> <?php endif; ?> </div> <?php /** * Allow extension below Jetpack's SSO form. * * @module sso * * @since jetpack-8.6.0 */ do_action( 'jetpack_sso_login_form_below_wpcom' ); if ( ! Helpers::should_hide_login_form() ) : ?> <div class="jetpack-sso-or"> <span><?php esc_html_e( 'Or', 'jetpack-connection' ); ?></span> </div> <a href="<?php echo esc_url( add_query_arg( 'jetpack-sso-show-default-form', '1' ) ); ?>" class="jetpack-sso-toggle wpcom"> <?php esc_html_e( 'Log in with username and password', 'jetpack-connection' ) ?> </a> <a href="<?php echo esc_url( add_query_arg( 'jetpack-sso-show-default-form', '0' ) ); ?>" class="jetpack-sso-toggle default"> <?php esc_html_e( 'Log in with WordPress.com', 'jetpack-connection' ) ?> </a> <?php endif; ?> </div> <?php } /** * Clear cookies that are no longer needed once the user has logged in. * * @since jetpack-4.8.0 */ public static function clear_cookies_after_login() { Helpers::clear_wpcom_profile_cookies(); if ( isset( $_COOKIE['jetpack_sso_nonce'] ) ) { setcookie( 'jetpack_sso_nonce', ' ', time() - YEAR_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), true ); } if ( isset( $_COOKIE['jetpack_sso_original_request'] ) ) { setcookie( 'jetpack_sso_original_request', ' ', time() - YEAR_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), true ); } if ( isset( $_COOKIE['jetpack_sso_redirect_to'] ) ) { setcookie( 'jetpack_sso_redirect_to', ' ', time() - YEAR_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), true ); } } /** * Clean up after Jetpack gets disconnected. * * @since jetpack-10.7 */ public static function disconnect() { if ( ( new Manager() )->is_user_connected() ) { Helpers::delete_connection_for_user( get_current_user_id() ); } } /** * Retrieves nonce used for SSO form. * * @return string|WP_Error */ public static function request_initial_nonce() { $nonce = ! empty( $_COOKIE['jetpack_sso_nonce'] ) ? sanitize_key( wp_unslash( $_COOKIE['jetpack_sso_nonce'] ) ) : false; if ( ! $nonce ) { $xml = new Jetpack_IXR_Client(); $xml->query( 'jetpack.sso.requestNonce' ); if ( $xml->isError() ) { return new WP_Error( $xml->getErrorCode(), $xml->getErrorMessage() ); } $nonce = sanitize_key( $xml->getResponse() ); setcookie( 'jetpack_sso_nonce', $nonce, time() + ( 10 * MINUTE_IN_SECONDS ), COOKIEPATH, COOKIE_DOMAIN, is_ssl(), true ); } return $nonce; } /** * The function that actually handles the login! */ public function handle_login() { $wpcom_nonce = isset( $_GET['sso_nonce'] ) ? sanitize_key( $_GET['sso_nonce'] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended $wpcom_user_id = isset( $_GET['user_id'] ) ? (int) $_GET['user_id'] : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended $xml = new Jetpack_IXR_Client(); $xml->query( 'jetpack.sso.validateResult', $wpcom_nonce, $wpcom_user_id ); $user_data = $xml->isError() ? false : $xml->getResponse(); if ( empty( $user_data ) ) { add_filter( 'jetpack_sso_default_to_sso_login', '__return_false' ); add_filter( 'login_message', array( Notices::class, 'error_invalid_response_data' ) ); return; } $user_data = (object) $user_data; $user = null; /** * Fires before Jetpack's SSO modifies the log in form. * * @module sso * * @since jetpack-2.6.0 * * @param object $user_data WordPress.com User information. */ do_action( 'jetpack_sso_pre_handle_login', $user_data ); $tracking = new Tracking(); if ( Helpers::is_two_step_required() && 0 === (int) $user_data->two_step_enabled ) { $this->user_data = $user_data; $tracking->record_user_event( 'sso_login_failed', array( 'error_message' => 'error_msg_enable_two_step', ) ); $error = new WP_Error( 'two_step_required', __( 'You must have Two-Step Authentication enabled on your WordPress.com account.', 'jetpack-connection' ) ); /** This filter is documented in core/src/wp-includes/pluggable.php */ do_action( 'wp_login_failed', $user_data->login, $error ); add_filter( 'login_message', array( Notices::class, 'error_msg_enable_two_step' ) ); return; } $user_found_with = ''; if ( isset( $user_data->external_user_id ) ) { $user_found_with = 'external_user_id'; $user = get_user_by( 'id', (int) $user_data->external_user_id ); if ( $user ) { $expected_id = get_user_meta( $user->ID, 'wpcom_user_id', true ); if ( $expected_id && $expected_id != $user_data->ID ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison, Universal.Operators.StrictComparisons.LooseNotEqual $error = new WP_Error( 'expected_wpcom_user', __( 'Something got a little mixed up and an unexpected WordPress.com user logged in.', 'jetpack-connection' ) ); $tracking->record_user_event( 'sso_login_failed', array( 'error_message' => 'error_unexpected_wpcom_user', ) ); /** This filter is documented in core/src/wp-includes/pluggable.php */ do_action( 'wp_login_failed', $user_data->login, $error ); add_filter( 'login_message', array( Notices::class, 'error_invalid_response_data' ) ); // @todo Need to have a better notice. This is only for the sake of testing the validation. return; } update_user_meta( $user->ID, 'wpcom_user_id', $user_data->ID ); } } // If we don't have one by wpcom_user_id, try by the email? if ( empty( $user ) && Helpers::match_by_email() ) { $user_found_with = 'match_by_email'; $user = get_user_by( 'email', $user_data->email ); if ( $user ) { update_user_meta( $user->ID, 'wpcom_user_id', $user_data->ID ); } } // If we've still got nothing, create the user. $new_user_override_role = Helpers::new_user_override( $user_data ); if ( empty( $user ) && ( get_option( 'users_can_register' ) || $new_user_override_role ) ) { /** * If not matching by email we still need to verify the email does not exist * or this blows up * * If match_by_email is true, we know the email doesn't exist, as it would have * been found in the first pass. If get_user_by( 'email' ) doesn't find the * user, then we know that email is unused, so it's safe to add. */ if ( Helpers::match_by_email() || ! get_user_by( 'email', $user_data->email ) ) { if ( $new_user_override_role ) { $user_data->role = $new_user_override_role; } $user = Utils::generate_user( $user_data ); if ( ! $user ) { $tracking->record_user_event( 'sso_login_failed', array( 'error_message' => 'could_not_create_username', ) ); add_filter( 'login_message', array( Notices::class, 'error_unable_to_create_user' ) ); return; } $user_found_with = $new_user_override_role ? 'user_created_new_user_override' : 'user_created_users_can_register'; } else { $tracking->record_user_event( 'sso_login_failed', array( 'error_message' => 'error_msg_email_already_exists', ) ); $this->user_data = $user_data; add_action( 'login_message', array( Notices::class, 'error_msg_email_already_exists' ) ); return; } } /** * Fires after we got login information from WordPress.com. * * @module sso * * @since jetpack-2.6.0 * * @param WP_User|false|null $user Local User information. * @param object $user_data WordPress.com User Login information. */ do_action( 'jetpack_sso_handle_login', $user, $user_data ); if ( $user ) { // Cache the user's details, so we can present it back to them on their user screen. update_user_meta( $user->ID, 'wpcom_user_data', $user_data ); add_filter( 'auth_cookie_expiration', array( Helpers::class, 'extend_auth_cookie_expiration_for_sso' ) ); wp_set_auth_cookie( $user->ID, true ); remove_filter( 'auth_cookie_expiration', array( Helpers::class, 'extend_auth_cookie_expiration_for_sso' ) ); /** This filter is documented in core/src/wp-includes/user.php */ do_action( 'wp_login', $user->user_login, $user ); wp_set_current_user( $user->ID ); $_request_redirect_to = isset( $_REQUEST['redirect_to'] ) ? esc_url_raw( wp_unslash( $_REQUEST['redirect_to'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended $redirect_to = user_can( $user, 'edit_posts' ) ? admin_url() : self::profile_page_url(); // If we have a saved redirect to request in a cookie. if ( ! empty( $_COOKIE['jetpack_sso_redirect_to'] ) ) { // Set that as the requested redirect to. $redirect_to = esc_url_raw( wp_unslash( $_COOKIE['jetpack_sso_redirect_to'] ) ); $_request_redirect_to = $redirect_to; } $json_api_auth_environment = Helpers::get_json_api_auth_environment(); $is_json_api_auth = ! empty( $json_api_auth_environment ); $is_user_connected = ( new Manager() )->is_user_connected( $user->ID ); $roles = new Roles(); $tracking->record_user_event( 'sso_user_logged_in', array( 'user_found_with' => $user_found_with, 'user_connected' => (bool) $is_user_connected, 'user_role' => $roles->translate_current_user_to_role(), 'is_json_api_auth' => $is_json_api_auth, ) ); if ( $is_json_api_auth ) { $authorize_json_api = new Authorize_Json_Api(); $authorize_json_api->verify_json_api_authorization_request( $json_api_auth_environment ); $authorize_json_api->store_json_api_authorization_token( $user->user_login, $user ); } elseif ( ! $is_user_connected ) { wp_safe_redirect( add_query_arg( array( 'redirect_to' => $redirect_to, 'request_redirect_to' => $_request_redirect_to, 'calypso_env' => ( new Host() )->get_calypso_env(), 'jetpack-sso-auth-redirect' => '1', ), admin_url() ) ); exit( 0 ); } add_filter( 'allowed_redirect_hosts', array( Helpers::class, 'allowed_redirect_hosts' ) ); wp_safe_redirect( /** This filter is documented in core/src/wp-login.php */ apply_filters( 'login_redirect', $redirect_to, $_request_redirect_to, $user ) ); exit( 0 ); } add_filter( 'jetpack_sso_default_to_sso_login', '__return_false' ); $tracking->record_user_event( 'sso_login_failed', array( 'error_message' => 'cant_find_user', ) ); $this->user_data = $user_data; $error = new WP_Error( 'account_not_found', __( 'Account not found. If you already have an account, make sure you have connected to WordPress.com.', 'jetpack-connection' ) ); /** This filter is documented in core/src/wp-includes/pluggable.php */ do_action( 'wp_login_failed', $user_data->login, $error ); add_filter( 'login_message', array( Notices::class, 'cant_find_user' ) ); } /** * Retrieve the admin profile page URL. */ public static function profile_page_url() { return admin_url( 'profile.php' ); } /** * Builds the "Login to WordPress.com" button that is displayed on the login page as well as user profile page. * * @param array $args An array of arguments to add to the SSO URL. * @param boolean $is_primary If the button have the `button-primary` class. * @return string Returns the HTML markup for the button. */ public function build_sso_button( $args = array(), $is_primary = false ) { $url = $this->build_sso_button_url( $args ); $classes = $is_primary ? 'jetpack-sso button button-primary' : 'jetpack-sso button'; return sprintf( '<a rel="nofollow" href="%1$s" class="%2$s">%3$s %4$s</a>', esc_url( $url ), $classes, '<span class="genericon genericon-wordpress"></span>', esc_html__( 'Log in with WordPress.com', 'jetpack-connection' ) ); } /** * Builds a URL with `jetpack-sso` action and option args which is used to setup SSO. * * @param array $args An array of arguments to add to the SSO URL. * @return string The URL used for SSO. */ public function build_sso_button_url( $args = array() ) { $defaults = array( 'action' => 'jetpack-sso', ); $args = wp_parse_args( $args, $defaults ); if ( ! empty( $_GET['redirect_to'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $args['redirect_to'] = rawurlencode( esc_url_raw( wp_unslash( $_GET['redirect_to'] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended } return add_query_arg( $args, wp_login_url() ); } /** * Retrieves a WordPress.com SSO URL with appropriate query parameters or dies. * * @param boolean $reauth If the user be forced to reauthenticate on WordPress.com. * @param array $args Optional query parameters. * @return string The WordPress.com SSO URL. */ public function get_sso_url_or_die( $reauth = false, $args = array() ) { $custom_login_url = Helpers::get_custom_login_url(); if ( $custom_login_url ) { $args['login_url'] = rawurlencode( $custom_login_url ); } if ( empty( $reauth ) ) { $sso_redirect = $this->build_sso_url( $args ); } else { Helpers::clear_wpcom_profile_cookies(); $sso_redirect = $this->build_reauth_and_sso_url( $args ); } // If there was an error retrieving the SSO URL, then error. if ( is_wp_error( $sso_redirect ) ) { $error_message = sanitize_text_field( sprintf( '%s: %s', $sso_redirect->get_error_code(), $sso_redirect->get_error_message() ) ); $tracking = new Tracking(); $tracking->record_user_event( 'sso_login_redirect_failed', array( 'error_message' => $error_message, ) ); wp_die( esc_html( $error_message ) ); } return $sso_redirect; } /** * Build WordPress.com SSO URL with appropriate query parameters. * * @param array $args Optional query parameters. * @return string|WP_Error WordPress.com SSO URL */ public function build_sso_url( $args = array() ) { $sso_nonce = ! empty( $args['sso_nonce'] ) ? $args['sso_nonce'] : self::request_initial_nonce(); $defaults = array( 'action' => 'jetpack-sso', 'site_id' => Manager::get_site_id( true ), 'sso_nonce' => $sso_nonce, 'calypso_auth' => '1', ); $args = wp_parse_args( $args, $defaults ); if ( is_wp_error( $sso_nonce ) ) { return $sso_nonce; } return add_query_arg( $args, 'https://wordpress.com/wp-login.php' ); } /** * Build WordPress.com SSO URL with appropriate query parameters, * including the parameters necessary to force the user to reauthenticate * on WordPress.com. * * @param array $args Optional query parameters. * @return string|WP_Error WordPress.com SSO URL */ public function build_reauth_and_sso_url( $args = array() ) { $sso_nonce = ! empty( $args['sso_nonce'] ) ? $args['sso_nonce'] : self::request_initial_nonce(); $redirect = $this->build_sso_url( array( 'force_auth' => '1', 'sso_nonce' => $sso_nonce, ) ); if ( is_wp_error( $redirect ) ) { return $redirect; } $defaults = array( 'action' => 'jetpack-sso', 'site_id' => Manager::get_site_id( true ), 'sso_nonce' => $sso_nonce, 'reauth' => '1', 'redirect_to' => rawurlencode( $redirect ), 'calypso_auth' => '1', ); $args = wp_parse_args( $args, $defaults ); if ( is_wp_error( $args['sso_nonce'] ) ) { return $args['sso_nonce']; } return add_query_arg( $args, 'https://wordpress.com/wp-login.php' ); } /** * Determines local user associated with a given WordPress.com user ID. * * @since jetpack-2.6.0 * * @param int $wpcom_user_id User ID from WordPress.com. * @return null|object Local user object if found, null if not. */ public static function get_user_by_wpcom_id( $wpcom_user_id ) { $user_query = new WP_User_Query( array( 'meta_key' => 'wpcom_user_id', 'meta_value' => (int) $wpcom_user_id, 'number' => 1, ) ); $users = $user_query->get_results(); return $users ? array_shift( $users ) : null; } /** * When jetpack-sso-auth-redirect query parameter is set, will redirect user to * WordPress.com authorization flow. * * We redirect here instead of in handle_login() because Jetpack::init()->build_connect_url * calls menu_page_url() which doesn't work properly until admin menus are registered. */ public function maybe_authorize_user_after_sso() { if ( empty( $_GET['jetpack-sso-auth-redirect'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended return; } $redirect_to = ! empty( $_GET['redirect_to'] ) ? esc_url_raw( wp_unslash( $_GET['redirect_to'] ) ) : admin_url(); // phpcs:ignore WordPress.Security.NonceVerification.Recommended $request_redirect_to = ! empty( $_GET['request_redirect_to'] ) ? esc_url_raw( wp_unslash( $_GET['request_redirect_to'] ) ) : $redirect_to; // phpcs:ignore WordPress.Security.NonceVerification.Recommended /** This filter is documented in core/src/wp-login.php */ $redirect_after_auth = apply_filters( 'login_redirect', $redirect_to, $request_redirect_to, wp_get_current_user() ); /** * Since we are passing this redirect to WordPress.com and therefore cannot use wp_safe_redirect(), * let's sanitize it here to make sure it's safe. If the redirect is not safe, then use admin_url(). */ $redirect_after_auth = wp_sanitize_redirect( $redirect_after_auth ); $redirect_after_auth = wp_validate_redirect( $redirect_after_auth, admin_url() ); /** * Return the raw connect URL with our redirect and attribute connection to SSO. * We remove any other filters that may be turning on the in-place connection * since we will be redirecting the user as opposed to iFraming. */ remove_all_filters( 'jetpack_use_iframe_authorization_flow' ); add_filter( 'jetpack_use_iframe_authorization_flow', '__return_false' ); $connection = new Manager( 'jetpack-connection' ); $connect_url = ( new Authorize_Redirect( $connection ) )->build_authorize_url( $redirect_after_auth, 'sso', true ); add_filter( 'allowed_redirect_hosts', array( Helpers::class, 'allowed_redirect_hosts' ) ); wp_safe_redirect( $connect_url ); exit( 0 ); } /** * Cache user's display name and Gravatar so it can be displayed on the login screen. These cookies are * stored when the user logs out, and then deleted when the user logs in. */ public function store_wpcom_profile_cookies_on_logout() { $user_id = get_current_user_id(); if ( ! ( new Manager() )->is_user_connected( $user_id ) ) { return; } $user_data = $this->get_user_data( $user_id ); if ( ! $user_data ) { return; } setcookie( 'jetpack_sso_wpcom_name_' . COOKIEHASH, $user_data->display_name, time() + WEEK_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), true ); setcookie( 'jetpack_sso_wpcom_gravatar_' . COOKIEHASH, get_avatar_url( $user_data->email, array( 'size' => 144, 'default' => 'mystery', ) ), time() + WEEK_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), true ); } /** * Determines if a local user is connected to WordPress.com * * @since jetpack-2.8 * @param integer $user_id - Local user id. * @return boolean **/ public function is_user_connected( $user_id ) { return $this->get_user_data( $user_id ); } /** * Retrieves a user's WordPress.com data * * @since jetpack-2.8 * @param integer $user_id - Local user id. * @return mixed null or stdClass **/ public function get_user_data( $user_id ) { return get_user_meta( $user_id, 'wpcom_user_data', true ); } } jetpack-connection/src/traits/trait-wpcom-rest-api-proxy-request.php 0000777 00000011762 15251741406 0022010 0 ustar 00 <?php /** * Trait WPCOM_REST_API_Proxy_Request * * Used to proxy requests to wpcom servers. * * @package automattic/jetpack-connection */ namespace Automattic\Jetpack\Connection\Traits; use Automattic\Jetpack\Connection\Client; use Automattic\Jetpack\Connection\Manager; use Automattic\Jetpack\Status\Visitor; use WP_Error; use WP_REST_Request; trait WPCOM_REST_API_Proxy_Request { /** * Base path for the API. * * @var string */ protected $base_api_path; /** * Version of the API. * * @var string */ protected $version; /** * The base of the controller's route. * * @var string */ protected $rest_base; /** * Proxy request to wpcom servers on behalf of a user or using the Site-level Connection (blog token). * * @param WP_REST_Request $request Request to proxy. * @param string $path Path to append to the rest base. * @param string $context Whether the request should be proxied on behalf of the current user or using the Site-level Connection, aka 'blog' token. Can be Either 'user' or 'blog'. Defaults to 'user'. * @param bool $allow_fallback_to_blog If the $context is 'user', whether we should fallback to using the Site-level Connection in case the current user is not connected. * @param array $request_options Request options to pass to wp_remote_request. * * @return mixed|WP_Error Response from wpcom servers or an error. */ public function proxy_request_to_wpcom( $request, $path = '', $context = 'user', $allow_fallback_to_blog = false, $request_options = array() ) { $blog_id = \Jetpack_Options::get_option( 'id' ); $path = '/sites/' . rawurldecode( $blog_id ) . '/' . rawurldecode( ltrim( $this->rest_base, '/' ) ) . ( $path ? '/' . rawurldecode( ltrim( $path, '/' ) ) : '' ); $query_params = $request->get_query_params(); $manager = new Manager(); /* * A rest_route parameter can be added when using plain permalinks. * It is not necessary to pass them to WordPress.com, * and may even cause issues with some endpoints. * Let's remove it. */ if ( isset( $query_params['rest_route'] ) ) { unset( $query_params['rest_route'] ); } $api_url = add_query_arg( $query_params, $path ); $request_options = array_replace_recursive( array( 'headers' => array( 'Content-Type' => 'application/json', 'X-Forwarded-For' => ( new Visitor() )->get_ip( true ), ), 'method' => $request->get_method(), ), $request_options ); // If no body is present, passing it as $request->get_body() will cause an error. $body = $request->get_body() ? $request->get_body() : null; $response = new WP_Error( 'rest_unauthorized', __( 'Please connect your user account to WordPress.com', 'jetpack-connection' ), array( 'status' => rest_authorization_required_code() ) ); if ( 'user' === $context ) { if ( ! $manager->is_user_connected() ) { if ( false === $allow_fallback_to_blog ) { return $response; } $context = 'blog'; } else { $response = Client::wpcom_json_api_request_as_user( $api_url, $this->version, $request_options, $body, $this->base_api_path ); } } if ( 'blog' === $context ) { if ( ! $manager->is_connected() ) { return $response; } $response = Client::wpcom_json_api_request_as_blog( $api_url, $this->version, $request_options, $body, $this->base_api_path ); } if ( is_wp_error( $response ) ) { return $response; } $response_status = wp_remote_retrieve_response_code( $response ); $response_body = json_decode( wp_remote_retrieve_body( $response ), true ); if ( $response_status >= 400 ) { $code = $response_body['code'] ?? 'unknown_error'; $message = $response_body['message'] ?? __( 'An unknown error occurred.', 'jetpack-connection' ); return new WP_Error( $code, $message, array( 'status' => $response_status ) ); } return $response_body; } /** * Proxy request to wpcom servers on behalf of a user. * * @param WP_REST_Request $request Request to proxy. * @param string $path Path to append to the rest base. * @param array $request_options Request options to pass to wp_remote_request. * * @return mixed|WP_Error Response from wpcom servers or an error. */ public function proxy_request_to_wpcom_as_user( $request, $path = '', $request_options = array() ) { return $this->proxy_request_to_wpcom( $request, $path, 'user', false, $request_options ); } /** * Proxy request to wpcom servers using the Site-level Connection (blog token). * * @param WP_REST_Request $request Request to proxy. * @param string $path Path to append to the rest base. * @param array $request_options Request options to pass to wp_remote_request. * * @return mixed|WP_Error Response from wpcom servers or an error. */ public function proxy_request_to_wpcom_as_blog( $request, $path = '', $request_options = array() ) { return $this->proxy_request_to_wpcom( $request, $path, 'blog', false, $request_options ); } } jetpack-connection/src/class-urls.php 0000777 00000011743 15251741406 0013734 0 ustar 00 <?php /** * The Jetpack Connection package Urls class file. * * @package automattic/jetpack-connection */ namespace Automattic\Jetpack\Connection; use Automattic\Jetpack\Constants; /** * Provides Url methods for the Connection package. */ class Urls { const HTTPS_CHECK_OPTION_PREFIX = 'jetpack_sync_https_history_'; const HTTPS_CHECK_HISTORY = 5; /** * Return URL from option or PHP constant. * * @param string $option_name (e.g. 'home'). * * @return mixed|null URL. */ public static function get_raw_url( $option_name ) { $value = null; $constant = ( 'home' === $option_name ) ? 'WP_HOME' : 'WP_SITEURL'; // Since we disregard the constant for multisites in ms-default-filters.php, // let's also use the db value if this is a multisite. if ( ! is_multisite() && Constants::is_defined( $constant ) ) { $value = Constants::get_constant( $constant ); } else { // Let's get the option from the database so that we can bypass filters. This will help // ensure that we get more uniform values. $value = \Jetpack_Options::get_raw_option( $option_name ); } return $value; } /** * Normalize domains by removing www unless declared in the site's option. * * @param string $option Option value from the site. * @param callable $url_function Function retrieving the URL to normalize. * @return mixed|string URL. */ public static function normalize_www_in_url( $option, $url_function ) { $url = wp_parse_url( call_user_func( $url_function ) ); $option_url = wp_parse_url( get_option( $option ) ); if ( ! $option_url || ! $url ) { return $url; } if ( "www.{$option_url[ 'host' ]}" === $url['host'] ) { // remove www if not present in option URL. $url['host'] = $option_url['host']; } if ( "www.{$url[ 'host' ]}" === $option_url['host'] ) { // add www if present in option URL. $url['host'] = $option_url['host']; } $normalized_url = "{$url['scheme']}://{$url['host']}"; if ( isset( $url['path'] ) ) { $normalized_url .= "{$url['path']}"; } if ( isset( $url['query'] ) ) { $normalized_url .= "?{$url['query']}"; } return $normalized_url; } /** * Return URL with a normalized protocol. * * @param string $callable Function name that was used to retrieve URL option. * @param string $new_value URL Protocol to set URLs to. * @return string Normalized URL. */ public static function get_protocol_normalized_url( $callable, $new_value ) { $option_key = self::HTTPS_CHECK_OPTION_PREFIX . $callable; $parsed_url = wp_parse_url( $new_value ); if ( ! $parsed_url ) { return $new_value; } if ( array_key_exists( 'scheme', $parsed_url ) ) { $scheme = $parsed_url['scheme']; } else { $scheme = ''; } $scheme_history = get_option( $option_key, array() ); if ( ! is_array( $scheme_history ) ) { $scheme_history = array(); } $scheme_history[] = $scheme; // Limit length to self::HTTPS_CHECK_HISTORY. $scheme_history = array_slice( $scheme_history, ( self::HTTPS_CHECK_HISTORY * -1 ) ); update_option( $option_key, $scheme_history ); $forced_scheme = in_array( 'https', $scheme_history, true ) ? 'https' : 'http'; return set_url_scheme( $new_value, $forced_scheme ); } /** * Helper function that is used when getting home or siteurl values. Decides * whether to get the raw or filtered value. * * @param string $url_type URL to get, home or siteurl. * @return string */ public static function get_raw_or_filtered_url( $url_type ) { $url_function = ( 'home' === $url_type ) ? 'home_url' : 'site_url'; if ( ! Constants::is_defined( 'JETPACK_SYNC_USE_RAW_URL' ) || Constants::get_constant( 'JETPACK_SYNC_USE_RAW_URL' ) ) { $scheme = is_ssl() ? 'https' : 'http'; $url = (string) self::get_raw_url( $url_type ); $url = set_url_scheme( $url, $scheme ); } else { $url = self::normalize_www_in_url( $url_type, $url_function ); } return self::get_protocol_normalized_url( $url_function, $url ); } /** * Return the escaped home_url. * * @return string */ public static function home_url() { $url = self::get_raw_or_filtered_url( 'home' ); /** * Allows overriding of the home_url value that is synced back to WordPress.com. * * @since 1.7.0 * @since-jetpack 5.2.0 * * @param string $home_url */ return esc_url_raw( apply_filters( 'jetpack_sync_home_url', $url ) ); } /** * Return the escaped siteurl. * * @return string */ public static function site_url() { $url = self::get_raw_or_filtered_url( 'siteurl' ); /** * Allows overriding of the site_url value that is synced back to WordPress.com. * * @since 1.7.0 * @since-jetpack 5.2.0 * * @param string $site_url */ return esc_url_raw( apply_filters( 'jetpack_sync_site_url', $url ) ); } /** * Return main site URL with a normalized protocol. * * @return string */ public static function main_network_site_url() { return self::get_protocol_normalized_url( 'main_network_site_url', network_site_url() ); } } jetpack-connection/src/class-terms-of-service.php 0000777 00000005356 15251741406 0016144 0 ustar 00 <?php /** * A Terms of Service class for Jetpack. * * @package automattic/jetpack-connection */ namespace Automattic\Jetpack; /** * Class Terms_Of_Service * * Helper class that is responsible for the state of agreement of the terms of service. */ class Terms_Of_Service { /** * Jetpack option name where the terms of service state is stored. * * @var string */ const OPTION_NAME = 'tos_agreed'; /** * Allow the site to agree to the terms of service. */ public function agree() { $this->set_agree(); /** * Acton fired when the master user has agreed to the terms of service. * * @since 1.0.4 * @since-jetpack 7.9.0 */ do_action( 'jetpack_agreed_to_terms_of_service' ); } /** * Allow the site to reject to the terms of service. */ public function reject() { $this->set_reject(); /** * Acton fired when the master user has revoked their agreement to the terms of service. * * @since 1.0.4 * @since-jetpack 7.9.1 */ do_action( 'jetpack_reject_terms_of_service' ); } /** * Returns whether the master user has agreed to the terms of service. * * The following conditions have to be met in order to agree to the terms of service. * 1. The master user has gone though the connect flow. * 2. The site is not in dev mode. * 3. The master user of the site is still connected (deprecated @since 1.4.0). * * @return bool */ public function has_agreed() { if ( $this->is_offline_mode() ) { return false; } /** * Before 1.4.0 we used to also check if the master user of the site is connected * by calling the Connection related `is_active` method. * As of 1.4.0 we have removed this check in order to resolve the * circular dependencies it was introducing to composer packages. * * @since 1.4.0 */ return $this->get_raw_has_agreed(); } /** * Abstracted for testing purposes. * Tells us if the site is in dev mode. * * @return bool */ protected function is_offline_mode() { return ( new Status() )->is_offline_mode(); } /** * Gets just the Jetpack Option that contains the terms of service state. * Abstracted for testing purposes. * * @return bool */ protected function get_raw_has_agreed() { return \Jetpack_Options::get_option( self::OPTION_NAME, false ); } /** * Sets the correct Jetpack Option to mark the that the site has agreed to the terms of service. * Abstracted for testing purposes. */ protected function set_agree() { \Jetpack_Options::update_option( self::OPTION_NAME, true ); } /** * Sets the correct Jetpack Option to mark that the site has rejected the terms of service. * Abstracted for testing purposes. */ protected function set_reject() { \Jetpack_Options::update_option( self::OPTION_NAME, false ); } } jetpack-connection/src/class-user-account-status.php 0000777 00000007063 15251741406 0016700 0 ustar 00 <?php /** * Class to check for account errors in the Jetpack Connection. * * @package automattic/jetpack-connection * @since 6.11.0 */ namespace Automattic\Jetpack\Connection; /** * Class User_Account_Status */ class User_Account_Status { /** * Check for possible account errors between the local user and WPCOM account. * * @since 6.11.0 * * @param string $current_user_email The email of the current WordPress user. * @param string $wpcom_user_email The email of the connected WordPress.com account. * * @return array An array of possible account errors, empty if no errors. */ public function check_account_errors( $current_user_email, $wpcom_user_email ) { $errors = array(); // Check for email mismatch error. $has_mismatch = $this->possible_account_mismatch( $current_user_email, $wpcom_user_email ); if ( $has_mismatch ) { $errors['mismatch'] = array( 'type' => 'mismatch', 'message' => __( 'Your WordPress.com email is also used by another user account. This won’t affect functionality but may cause confusion about which user account is connected.', 'jetpack-connection' ), 'details' => array( 'site_email' => $current_user_email, 'wpcom_email' => $wpcom_user_email, ), ); } /** * Filters the account errors. * * @since 6.11.0 * * @param array $errors The array of account errors. * @param string $current_user_email The email of the current WordPress user. * @param string $wpcom_user_email The email of the connected WordPress.com account. */ return apply_filters( 'jetpack_connection_account_errors', $errors, $current_user_email, $wpcom_user_email ); } /** * Check if there is a possible account mismatch between the local user and WPCOM account. * * @since 6.11.0 * * @param string $current_user_email The email of the current WordPress user. * @param string $wpcom_user_email The email of the connected WordPress.com account. * * @return bool Whether there is a possible account mismatch. */ public function possible_account_mismatch( $current_user_email, $wpcom_user_email ) { // If emails are the same or there's no WPCOM email, there's no mismatch. if ( $current_user_email === $wpcom_user_email || ! $wpcom_user_email ) { return false; } // Generate transient key with both wpcom email and user ID if available. $transient_key = 'jetpack_account_mismatch_'; $transient_key .= md5( $wpcom_user_email ); $cached_result = get_transient( $transient_key ); if ( false !== $cached_result ) { return (bool) $cached_result; } // Check if there's a WordPress user with the WPCOM email . $wpcom_email_user = get_user_by( 'email', $wpcom_user_email ); $mismatch_exists = false !== $wpcom_email_user; // Store the result in a transient for 24 hours. set_transient( $transient_key, $mismatch_exists, DAY_IN_SECONDS ); return $mismatch_exists; } /** * Clears account mismatch transients for a user when they update their email or are deleted. * * @since 6.11.0 * * @param string|int $user_id_or_email User ID or email address. * @return void */ public function clean_account_mismatch_transients( $user_id_or_email ) { $email = null; if ( is_numeric( $user_id_or_email ) ) { $user = get_userdata( $user_id_or_email ); if ( $user && isset( $user->user_email ) ) { $email = $user->user_email; } } else { $email = $user_id_or_email; } if ( ! $email || ! is_email( $email ) ) { return; } $transient_key = 'jetpack_account_mismatch_' . md5( $email ); delete_transient( $transient_key ); } } jetpack-connection/src/class-xmlrpc-connector.php 0000777 00000003576 15251741406 0016251 0 ustar 00 <?php /** * Sets up the Connection XML-RPC methods. * * @package automattic/jetpack-connection */ namespace Automattic\Jetpack\Connection; use IXR_Error; /** * Registers the XML-RPC methods for Connections. * * @phan-constructor-used-for-side-effects */ class XMLRPC_Connector { /** * The Connection Manager. * * @var Manager */ private $connection; /** * Constructor. * * @param Manager $connection The Connection Manager. */ public function __construct( Manager $connection ) { $this->connection = $connection; // Adding the filter late to avoid being overwritten by Jetpack's XMLRPC server. add_filter( 'xmlrpc_methods', array( $this, 'xmlrpc_methods' ), 20 ); } /** * Attached to the `xmlrpc_methods` filter. * * @param array $methods The already registered XML-RPC methods. * @return array */ public function xmlrpc_methods( $methods ) { return array_merge( $methods, array( 'jetpack.verifyRegistration' => array( $this, 'verify_registration' ), ) ); } /** * Handles verification that a site is registered. * * @param array $registration_data The data sent by the XML-RPC client: * [ $secret_1, $user_id ]. * * @return string|IXR_Error */ public function verify_registration( $registration_data ) { return $this->output( $this->connection->handle_registration( $registration_data ) ); } /** * Normalizes output for XML-RPC. * * @param mixed $data The data to output. */ private function output( $data ) { if ( is_wp_error( $data ) ) { $code = $data->get_error_data(); if ( ! $code ) { $code = -10520; } if ( ! class_exists( IXR_Error::class ) ) { require_once ABSPATH . WPINC . '/class-IXR.php'; } return new IXR_Error( $code, sprintf( 'Jetpack: [%s] %s', $data->get_error_code(), $data->get_error_message() ) ); } return $data; } } jetpack-connection/src/class-plugin.php 0000777 00000004633 15251741406 0014245 0 ustar 00 <?php /** * Plugin connection management class. * * @package automattic/jetpack-connection */ namespace Automattic\Jetpack\Connection; /** * Plugin connection management class. * The class represents a single plugin that uses Jetpack connection. * Its functionality has been pretty simplistic so far: add to the storage (`Plugin_Storage`), remove it from there, * and determine whether it's the last active connection. As the component grows, there'll be more functionality added. */ class Plugin { /** * List of the keys allowed as arguments * * @var array */ private $arguments_whitelist = array( 'url_info', ); /** * Plugin slug. * * @var string */ private $slug; /** * Users Connection Admin instance. * * @var Users_Connection_Admin */ private $users_connection_admin; /** * Initialize the plugin manager. * * @param string $slug Plugin slug. */ public function __construct( $slug ) { $this->slug = $slug; // Initialize Users_Connection_Admin $this->users_connection_admin = new Users_Connection_Admin(); } /** * Get the plugin slug. * * @return string */ public function get_slug() { return $this->slug; } /** * Add the plugin connection info into Jetpack. * * @param string $name Plugin name, required. * @param array $args Plugin arguments, optional. * * @return $this * @see $this->arguments_whitelist */ public function add( $name, array $args = array() ) { $args = compact( 'name' ) + array_intersect_key( $args, array_flip( $this->arguments_whitelist ) ); Plugin_Storage::upsert( $this->slug, $args ); return $this; } /** * Remove the plugin connection info from Jetpack. * * @return $this */ public function remove() { Plugin_Storage::delete( $this->slug ); return $this; } /** * Determine if this plugin connection is the only one active at the moment, if any. * * @return bool */ public function is_only() { $plugins = Plugin_Storage::get_all(); if ( is_wp_error( $plugins ) ) { if ( 'too_early' === $plugins->get_error_code() ) { _doing_it_wrong( __METHOD__, esc_html( $plugins->get_error_code() . ': ' . $plugins->get_error_message() ), '6.16.1' ); } else { wp_trigger_error( __METHOD__, $plugins->get_error_code() . ': ' . $plugins->get_error_message() ); } return false; } return ! $plugins || ( array_key_exists( $this->slug, $plugins ) && 1 === count( $plugins ) ); } } jetpack-connection/src/class-heartbeat.php 0000777 00000017506 15251741406 0014711 0 ustar 00 <?php /** * Jetpack Heartbeat package. * * @package automattic/jetpack-connection */ namespace Automattic\Jetpack; use Automattic\Jetpack\Connection\Rest_Authentication; use Automattic\Jetpack\Connection\REST_Connector; use Jetpack_Options; use WP_CLI; use WP_Error; use WP_REST_Request; use WP_REST_Server; /** * Heartbeat sends a batch of stats to wp.com once a day */ class Heartbeat { /** * Holds the singleton instance of this class * * @since 1.0.0 * @since-jetpack 2.3.3 * @var Heartbeat */ private static $instance = false; /** * Cronjob identifier * * @var string */ private $cron_name = 'jetpack_v2_heartbeat'; /** * Singleton * * @since 1.0.0 * @since-jetpack 2.3.3 * @static * @return Heartbeat */ public static function init() { if ( ! self::$instance ) { self::$instance = new Heartbeat(); } return self::$instance; } /** * Constructor for singleton * * @since 1.0.0 * @since-jetpack 2.3.3 */ private function __construct() { // Schedule the task. add_action( $this->cron_name, array( $this, 'cron_exec' ) ); if ( ! wp_next_scheduled( $this->cron_name ) ) { // Deal with the old pre-3.0 weekly one. $timestamp = wp_next_scheduled( 'jetpack_heartbeat' ); if ( $timestamp ) { wp_unschedule_event( $timestamp, 'jetpack_heartbeat' ); } wp_schedule_event( time(), 'daily', $this->cron_name ); } add_filter( 'jetpack_xmlrpc_unauthenticated_methods', array( __CLASS__, 'jetpack_xmlrpc_methods' ) ); if ( defined( 'WP_CLI' ) && WP_CLI ) { WP_CLI::add_command( 'jetpack-heartbeat', array( $this, 'cli_callback' ) ); } add_action( 'rest_api_init', array( $this, 'initialize_rest_api' ) ); } /** * Method that gets executed on the wp-cron call * * @since 1.0.0 * @since-jetpack 2.3.3 * @global string $wp_version */ public function cron_exec() { $a8c_mc_stats = new A8c_Mc_Stats(); /* * This should run daily. Figuring in for variances in * WP_CRON, don't let it run more than every 23 hours at most. * * i.e. if it ran less than 23 hours ago, fail out. */ $last = (int) Jetpack_Options::get_option( 'last_heartbeat' ); if ( $last && ( $last + DAY_IN_SECONDS - HOUR_IN_SECONDS > time() ) ) { return; } /* * Check for an identity crisis * * If one exists: * - Bump stat for ID crisis * - Email site admin about potential ID crisis */ // Coming Soon! foreach ( self::generate_stats_array( 'v2-' ) as $key => $value ) { if ( is_array( $value ) ) { foreach ( $value as $v ) { $a8c_mc_stats->add( $key, (string) $v ); } } else { $a8c_mc_stats->add( $key, (string) $value ); } } Jetpack_Options::update_option( 'last_heartbeat', time() ); $a8c_mc_stats->do_server_side_stats(); /** * Fires when we synchronize all registered options on heartbeat. * * @since 3.3.0 */ do_action( 'jetpack_heartbeat' ); } /** * Generates heartbeat stats data. * * @param string $prefix Prefix to add before stats identifier. * * @return array The stats array. */ public static function generate_stats_array( $prefix = '' ) { /** * This filter is used to build the array of stats that are bumped once a day by Jetpack Heartbeat. * * Filter the array and add key => value pairs where * * key is the stat group name * * value is the stat name. * * Example: * add_filter( 'jetpack_heartbeat_stats_array', function( $stats ) { * $stats['is-https'] = is_ssl() ? 'https' : 'http'; * }); * * This will bump the stats for the 'is-https/https' or 'is-https/http' stat. * * @param array $stats The stats to be filtered. * @param string $prefix The prefix that will automatically be added at the begining at each stat group name. */ $stats = apply_filters( 'jetpack_heartbeat_stats_array', array(), $prefix ); $return = array(); // Apply prefix to stats. foreach ( $stats as $stat => $value ) { $return[ "$prefix$stat" ] = $value; } return $return; } /** * Registers jetpack.getHeartbeatData xmlrpc method * * @param array $methods The list of methods to be filtered. * @return array $methods */ public static function jetpack_xmlrpc_methods( $methods ) { $methods['jetpack.getHeartbeatData'] = array( __CLASS__, 'xmlrpc_data_response' ); return $methods; } /** * Handles the response for the jetpack.getHeartbeatData xmlrpc method * * @param array $params The parameters received in the request. * @return array $params all the stats that heartbeat handles. */ public static function xmlrpc_data_response( $params = array() ) { // The WordPress XML-RPC server sets a default param of array() // if no argument is passed on the request and the method handlers get this array in $params. // generate_stats_array() needs a string as first argument. $params = empty( $params ) ? '' : $params; return self::generate_stats_array( $params ); } /** * Clear scheduled events * * @return void */ public function deactivate() { // Deal with the old pre-3.0 weekly one. $timestamp = wp_next_scheduled( 'jetpack_heartbeat' ); if ( $timestamp ) { wp_unschedule_event( $timestamp, 'jetpack_heartbeat' ); } $timestamp = wp_next_scheduled( $this->cron_name ); wp_unschedule_event( $timestamp, $this->cron_name ); } /** * Interact with the Heartbeat * * ## OPTIONS * * inspect (default): Gets the list of data that is going to be sent in the heartbeat and the date/time of the last heartbeat * * @param array $args Arguments passed via CLI. * * @return void */ public function cli_callback( $args ) { $allowed_args = array( 'inspect', ); if ( isset( $args[0] ) && ! in_array( $args[0], $allowed_args, true ) ) { /* translators: %s is a command like "prompt" */ WP_CLI::error( sprintf( __( '%s is not a valid command.', 'jetpack-connection' ), $args[0] ) ); } $stats = self::generate_stats_array(); $formatted_stats = array(); foreach ( $stats as $stat_name => $bin ) { $formatted_stats[] = array( 'Stat name' => $stat_name, 'Bin' => $bin, ); } WP_CLI\Utils\format_items( 'table', $formatted_stats, array( 'Stat name', 'Bin' ) ); $last_heartbeat = Jetpack_Options::get_option( 'last_heartbeat' ); if ( $last_heartbeat ) { $last_date = gmdate( 'Y-m-d H:i:s', $last_heartbeat ); /* translators: %s is the full datetime of the last heart beat e.g. 2020-01-01 12:21:23 */ WP_CLI::line( sprintf( __( 'Last heartbeat sent at: %s', 'jetpack-connection' ), $last_date ) ); } } /** * Initialize the heartbeat REST API. * * @return void */ public function initialize_rest_api() { register_rest_route( 'jetpack/v4', '/heartbeat/data', array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'rest_heartbeat_data' ), 'permission_callback' => array( $this, 'rest_heartbeat_data_permission_check' ), 'args' => array( 'prefix' => array( 'description' => __( 'Prefix to add before the stats identifiers.', 'jetpack-connection' ), 'type' => 'string', ), ), ) ); } /** * Endpoint to retrieve the heartbeat data. * * @param WP_REST_Request $request The request data. * * @since 2.7.0 * * @return array */ public function rest_heartbeat_data( WP_REST_Request $request ) { return static::generate_stats_array( $request->get_param( 'prefix' ) ); } /** * Check permissions for the `get_heartbeat_data` endpoint. * * @return true|WP_Error */ public function rest_heartbeat_data_permission_check() { if ( current_user_can( 'jetpack_connect' ) ) { return true; } return Rest_Authentication::is_signed_with_blog_token() ? true : new WP_Error( 'invalid_permission_heartbeat_data', REST_Connector::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) ); } } jetpack-connection/src/class-users-connection-admin.php 0000777 00000010705 15251741406 0017330 0 ustar 00 <?php /** * Handles the WordPress.com account column in the users list table. * * @package automattic/jetpack-connection */ namespace Automattic\Jetpack\Connection; use Automattic\Jetpack\Assets; use Automattic\Jetpack\Status\Host; /** * Class Users_Connection_Admin */ class Users_Connection_Admin { /** * The column ID used for the WordPress.com account column. * * @var string */ const COLUMN_ID = 'user_jetpack'; /** * Constructor. */ public function __construct() { // Only set up hooks if we're in the admin area and user has proper permissions add_action( 'init', array( $this, 'init' ) ); } /** * Initialize the admin functionality if conditions are met. */ public function init() { if ( ! is_admin() || ! current_user_can( 'manage_options' ) || ( new Host() )->is_wpcom_simple() ) { return; } add_filter( 'manage_users_columns', array( $this, 'add_connection_column' ) ); add_filter( 'manage_users_custom_column', array( $this, 'render_connection_column' ), 9, 3 ); // Priority 9 to run before SSO add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_scripts' ) ); add_action( 'admin_print_styles-users.php', array( $this, 'add_connection_column_styles' ) ); } /** * Add the connection column to the users list table. * * @param array $columns The current columns. * @return array Modified columns. */ public function add_connection_column( $columns ) { $columns[ self::COLUMN_ID ] = sprintf( '<span class="jetpack-connection-tooltip-icon" role="tooltip" tabindex="0" aria-label="%2$s: %1$s"> %1$s <span class="jetpack-connection-tooltip"></span> </span>', esc_html__( 'WordPress.com account', 'jetpack-connection' ), esc_attr__( 'Tooltip', 'jetpack-connection' ) ); return $columns; } /** * Render the connection column content. * * @param string $output Custom column output. * @param string $column_name Column name. * @param int $user_id ID of the currently-listed user. * @return string */ public function render_connection_column( $output, $column_name, $user_id ) { if ( self::COLUMN_ID !== $column_name ) { return $output; } if ( ( new Manager() )->is_user_connected( $user_id ) ) { return sprintf( '<span title="%1$s" class="jetpack-connection-status">%2$s</span>', esc_attr__( 'This user has connected their WordPress.com account.', 'jetpack-connection' ), esc_html__( 'Connected', 'jetpack-connection' ) ); } return $output; } /** * Enqueue scripts and styles. * * @param string $hook The current admin page. */ public function enqueue_scripts( $hook ) { if ( 'users.php' !== $hook ) { return; } Assets::register_script( 'jetpack-users-connection', '../dist/jetpack-users-connection.js', __FILE__, array( 'strategy' => 'defer', 'in_footer' => true, 'enqueue' => true, 'version' => Package_Version::PACKAGE_VERSION, 'deps' => array( 'wp-i18n' ), ) ); wp_localize_script( 'jetpack-users-connection', 'jetpackConnectionTooltips', array( 'columnTooltip' => esc_html__( 'Connecting a WordPress.com account unlocks Jetpack’s full suite of features including secure logins.', 'jetpack-connection' ), ) ); } /** * Add styles for the connection column. */ public function add_connection_column_styles() { ?> <style> .jetpack-connection-tooltip-icon { position: relative; cursor: pointer; } /* Add [?] icon using pseudo-element, only in column header */ th.manage-column .jetpack-connection-tooltip-icon::after { content: '[?]'; color: #3c434a; font-size: 1em; margin-left: 4px; } .jetpack-connection-tooltip { position: absolute; background: #f6f7f7; top: -85px; width: 250px; padding: 7px; color: #3c434a; font-size: .75rem; line-height: 17px; text-align: left; margin: 0; display: none; border-radius: 4px; font-family: sans-serif; box-shadow: 5px 10px 10px rgba(0, 0, 0, 0.1); left: -170px; } .column-user_jetpack { width: 140px; } /* Show tooltip on hover and focus */ .jetpack-connection-tooltip-icon:hover .jetpack-connection-tooltip, .jetpack-connection-tooltip-icon:focus-within .jetpack-connection-tooltip { display: block; } </style> <?php } /** * Get the column ID. Allows other classes to reference the same column. * * @return string */ public static function get_column_id() { return self::COLUMN_ID; } } jetpack-connection/src/class-webhooks.php 0000777 00000015632 15251741406 0014571 0 ustar 00 <?php /** * Connection Webhooks class. * * @package automattic/jetpack-connection */ namespace Automattic\Jetpack\Connection; use Automattic\Jetpack\CookieState; use Automattic\Jetpack\Roles; use Automattic\Jetpack\Status\Host; use Automattic\Jetpack\Tracking; use Jetpack_Options; /** * Connection Webhooks class. */ class Webhooks { /** * The Connection Manager object. * * @var Manager */ private $connection; /** * Webhooks constructor. * * @param Manager $connection The Connection Manager object. */ public function __construct( $connection ) { $this->connection = $connection; } /** * Initialize the webhooks. * * @param Manager $connection The Connection Manager object. */ public static function init( $connection ) { $webhooks = new static( $connection ); add_action( 'init', array( $webhooks, 'controller' ) ); add_action( 'load-toplevel_page_jetpack', array( $webhooks, 'fallback_jetpack_controller' ) ); } /** * Jetpack plugin used to trigger this webhooks in Jetpack::admin_page_load() * * The Jetpack toplevel menu is still accessible for stand-alone plugins, and while there's no content for that page, there are still * actions from Calypso and WPCOM that reach that route regardless of the site having the Jetpack plugin or not. That's why we are still handling it here. */ public function fallback_jetpack_controller() { $this->controller( true ); } /** * The "controller" decides which handler we need to run. * * @param bool $force Do not check if it's a webhook request and just run the controller. */ public function controller( $force = false ) { if ( ! $force ) { // The nonce is verified in specific handlers. // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( empty( $_GET['handler'] ) || 'jetpack-connection-webhooks' !== $_GET['handler'] ) { return; } } // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( isset( $_GET['connect_url_redirect'] ) ) { $this->handle_connect_url_redirect(); } // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( empty( $_GET['action'] ) ) { return; } // The nonce is verified in specific handlers. // phpcs:ignore WordPress.Security.NonceVerification.Recommended switch ( $_GET['action'] ) { case 'authorize': $this->handle_authorize(); $this->do_exit(); break; // @phan-suppress-current-line PhanPluginUnreachableCode -- Safer to include it even though do_exit never returns. case 'authorize_redirect': $this->handle_authorize_redirect(); $this->do_exit(); break; // @phan-suppress-current-line PhanPluginUnreachableCode -- Safer to include it even though do_exit never returns. // Class Jetpack::admin_page_load() still handles other cases. } } /** * Perform the authorization action. */ public function handle_authorize() { if ( $this->connection->is_connected() && $this->connection->is_user_connected() ) { $redirect_url = apply_filters( 'jetpack_client_authorize_already_authorized_url', admin_url() ); wp_safe_redirect( $redirect_url ); return; } do_action( 'jetpack_client_authorize_processing' ); $data = stripslashes_deep( $_GET ); // We need all request data under the context of an authorization request. $data['auth_type'] = 'client'; $roles = new Roles(); $role = $roles->translate_current_user_to_role(); $redirect = isset( $data['redirect'] ) ? esc_url_raw( (string) $data['redirect'] ) : ''; check_admin_referer( "jetpack-authorize_{$role}_{$redirect}" ); $tracking = new Tracking(); $result = $this->connection->authorize( $data ); if ( is_wp_error( $result ) ) { do_action( 'jetpack_client_authorize_error', $result ); $tracking->record_user_event( 'jpc_client_authorize_fail', array( 'error_code' => $result->get_error_code(), 'error_message' => $result->get_error_message(), ) ); } else { /** * Fires after the Jetpack client is authorized to communicate with WordPress.com. * * @param int Jetpack Blog ID. * * @since 1.7.0 * @since-jetpack 4.2.0 */ do_action( 'jetpack_client_authorized', Jetpack_Options::get_option( 'id' ) ); $tracking->record_user_event( 'jpc_client_authorize_success' ); } $fallback_redirect = apply_filters( 'jetpack_client_authorize_fallback_url', admin_url() ); $redirect = wp_validate_redirect( $redirect ) ? $redirect : $fallback_redirect; wp_safe_redirect( $redirect ); } /** * The authorhize_redirect webhook handler */ public function handle_authorize_redirect() { $authorize_redirect_handler = new Webhooks\Authorize_Redirect( $this->connection ); $authorize_redirect_handler->handle(); } /** * The `exit` is wrapped into a method so we could mock it. * * @return never */ protected function do_exit() { exit( 0 ); } /** * Handle the `connect_url_redirect` action, * which is usually called to repeat an attempt for user to authorize the connection. * * @return void */ public function handle_connect_url_redirect() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- no site changes. $from = ! empty( $_GET['from'] ) ? sanitize_text_field( wp_unslash( $_GET['from'] ) ) : 'iframe'; $skip_pricing = filter_input( INPUT_GET, 'skip_pricing', FILTER_VALIDATE_BOOLEAN ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- no site changes, sanitization happens in get_authorization_url() $redirect = ! empty( $_GET['redirect_after_auth'] ) ? wp_unslash( $_GET['redirect_after_auth'] ) : false; add_filter( 'allowed_redirect_hosts', array( Host::class, 'allow_wpcom_environments' ) ); if ( ! $this->connection->is_user_connected() ) { if ( ! $this->connection->is_connected() ) { $this->connection->register(); } $connect_url = add_query_arg( 'from', $from, $this->connection->get_authorization_url( null, $redirect ) ); if ( $skip_pricing ) { $connect_url = add_query_arg( 'skip_pricing', '1', $connect_url ); } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- no site changes. if ( isset( $_GET['notes_iframe'] ) ) { $connect_url .= '¬es_iframe'; } wp_safe_redirect( $connect_url ); $this->do_exit(); } elseif ( ! isset( $_GET['calypso_env'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- no site changes. ( new CookieState() )->state( 'message', 'already_authorized' ); wp_safe_redirect( $redirect ); $this->do_exit(); } else { if ( 'connect-after-checkout' === $from && $redirect ) { wp_safe_redirect( $redirect ); $this->do_exit(); } $connect_url = add_query_arg( array( 'from' => $from, 'already_authorized' => true, ), $this->connection->get_authorization_url() ); wp_safe_redirect( $connect_url ); $this->do_exit(); } } } jetpack-connection/src/class-partner-coupon.php 0000777 00000025702 15251741406 0015723 0 ustar 00 <?php /** * Class for the Jetpack partner coupon logic. * * @package automattic/jetpack-connection */ namespace Automattic\Jetpack; use Automattic\Jetpack\Connection\Client as Connection_Client; use Automattic\Jetpack\Connection\Manager as Connection_Manager; use Jetpack_Options; /** * Disable direct access. */ if ( ! defined( 'ABSPATH' ) ) { exit( 0 ); } /** * Class Jetpack_Partner_Coupon * * @since partner-1.6.0 * @since 2.0.0 */ class Partner_Coupon { /** * Name of the Jetpack_Option coupon option. * * @var string */ public static $coupon_option = 'partner_coupon'; /** * Name of the Jetpack_Option added option. * * @var string */ public static $added_option = 'partner_coupon_added'; /** * Name of "last availability check" transient. * * @var string */ public static $last_check_transient = 'jetpack_partner_coupon_last_check'; /** * Callable that executes a blog-authenticated request. * * @var callable */ protected $request_as_blog; /** * Jetpack_Partner_Coupon * * @var Partner_Coupon|null **/ private static $instance = null; /** * A list of supported partners. * * @var array */ private static $supported_partners = array( 'IONOS' => array( 'name' => 'IONOS', 'logo' => array( 'src' => '/images/ionos-logo.jpg', 'width' => 119, 'height' => 32, ), ), ); /** * A list of supported presets. * * @var array */ private static $supported_presets = array( 'IONA' => 'jetpack_backup_daily', ); /** * Get singleton instance of class. * * @return Partner_Coupon */ public static function get_instance() { if ( self::$instance === null ) { self::$instance = new Partner_Coupon( array( Connection_Client::class, 'wpcom_json_api_request_as_blog' ) ); } return self::$instance; } /** * Constructor. * * @param callable $request_as_blog Callable that executes a blog-authenticated request. */ public function __construct( $request_as_blog ) { $this->request_as_blog = $request_as_blog; } /** * Register hooks to catch and purge coupon. * * @param string $plugin_slug The plugin slug to differentiate between Jetpack connections. * @param string $redirect_location The location we should redirect to after catching the coupon. */ public static function register_coupon_admin_hooks( $plugin_slug, $redirect_location ) { $instance = self::get_instance(); // We have to use an anonymous function, so we can pass along relevant information // and not have to hardcode values for a single plugin. // This open up the opportunity for e.g. the "all-in-one" and backup plugins // to both implement partner coupon logic. add_action( 'admin_init', function () use ( $plugin_slug, $redirect_location, $instance ) { $instance->catch_coupon( $plugin_slug, $redirect_location ); $instance->maybe_purge_coupon( $plugin_slug ); } ); } /** * Catch partner coupon and redirect to claim component. * * @param string $plugin_slug The plugin slug to differentiate between Jetpack connections. * @param string $redirect_location The location we should redirect to after catching the coupon. */ public function catch_coupon( $plugin_slug, $redirect_location ) { // Accept and store a partner coupon if present, and redirect to Jetpack connection screen. $partner_coupon = isset( $_GET['jetpack-partner-coupon'] ) ? sanitize_text_field( wp_unslash( $_GET['jetpack-partner-coupon'] ) ) : false; // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( $partner_coupon ) { Jetpack_Options::update_options( array( self::$coupon_option => $partner_coupon, self::$added_option => time(), ) ); $connection = new Connection_Manager( $plugin_slug ); if ( $connection->is_connected() ) { $redirect_location = add_query_arg( array( 'showCouponRedemption' => 1 ), $redirect_location ); wp_safe_redirect( $redirect_location ); } else { wp_safe_redirect( $redirect_location ); } } } /** * Purge partner coupon. * * We try to remotely check if a coupon looks valid. We also automatically purge * partner coupons after a certain amount of time to prevent unnecessary look-ups * and/or promoting a product for months or years in the future due to unknown * errors. * * @param string $plugin_slug The plugin slug to differentiate between Jetpack connections. */ public function maybe_purge_coupon( $plugin_slug ) { // Only run coupon checks on Jetpack admin pages. // The "admin-ui" package is responsible for registering the Jetpack admin // page for all Jetpack plugins and has hardcoded the settings page to be // "jetpack", so we shouldn't need to allow for dynamic/custom values. // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( ! isset( $_GET['page'] ) || 'jetpack' !== $_GET['page'] ) { return; } if ( ( new Status() )->is_offline_mode() ) { return; } $connection = new Connection_Manager( $plugin_slug ); if ( ! $connection->is_connected() ) { return; } if ( $this->maybe_purge_coupon_by_added_date() ) { return; } // Limit checks to happen once a minute at most. if ( get_transient( self::$last_check_transient ) ) { return; } set_transient( self::$last_check_transient, true, MINUTE_IN_SECONDS ); $this->maybe_purge_coupon_by_availability_check(); } /** * Purge coupon based on local added date. * * We automatically remove the coupon after a month to "self-heal" if * something in the claim process has broken with the site. * * @return bool Return whether we should skip further purge checks. */ protected function maybe_purge_coupon_by_added_date() { $date = Jetpack_Options::get_option( self::$added_option, '' ); if ( empty( $date ) ) { return true; } $expire_date = strtotime( '+30 days', $date ); $today = time(); if ( $today >= $expire_date ) { $this->delete_coupon_data(); return true; } return false; } /** * Purge coupon based on availability check. * * @return bool Return whether we deleted coupon data. */ protected function maybe_purge_coupon_by_availability_check() { $blog_id = Jetpack_Options::get_option( 'id', false ); if ( ! $blog_id ) { return false; } $coupon = self::get_coupon(); if ( ! $coupon ) { return false; } $response = call_user_func_array( $this->request_as_blog, array( add_query_arg( array( 'coupon_code' => $coupon['coupon_code'] ), sprintf( '/sites/%d/jetpack-partner/coupon/v1/site/coupon', $blog_id ) ), 2, array( 'method' => 'GET' ), null, 'wpcom', ) ); $body = json_decode( wp_remote_retrieve_body( $response ), true ); if ( 200 === wp_remote_retrieve_response_code( $response ) && is_array( $body ) && isset( $body['available'] ) && false === $body['available'] ) { $this->delete_coupon_data(); return true; } return false; } /** * Delete all coupon data. */ protected function delete_coupon_data() { Jetpack_Options::delete_option( array( self::$coupon_option, self::$added_option, ) ); } /** * Get partner coupon data. * * @return array|bool */ public static function get_coupon() { $coupon_code = Jetpack_Options::get_option( self::$coupon_option, '' ); if ( ! is_string( $coupon_code ) || empty( $coupon_code ) ) { return false; } $instance = self::get_instance(); $partner = $instance->get_coupon_partner( $coupon_code ); if ( ! $partner ) { return false; } $preset = $instance->get_coupon_preset( $coupon_code ); if ( ! $preset ) { return false; } $product = $instance->get_coupon_product( $preset ); if ( ! $product ) { return false; } return array( 'coupon_code' => $coupon_code, 'partner' => $partner, 'preset' => $preset, 'product' => $product, ); } /** * Get coupon partner. * * @param string $coupon_code Coupon code to go through. * @return array|bool */ private function get_coupon_partner( $coupon_code ) { if ( ! is_string( $coupon_code ) || false === strpos( $coupon_code, '_' ) ) { return false; } $prefix = strtok( $coupon_code, '_' ); $supported_partners = $this->get_supported_partners(); if ( ! isset( $supported_partners[ $prefix ] ) ) { return false; } return array( 'name' => $supported_partners[ $prefix ]['name'], 'prefix' => $prefix, 'logo' => 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 ); } } jetpack-connection/src/class-authorize-json-api.php 0000777 00000021304 15251741406 0016471 0 ustar 00 <?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 site’s data. Log in to authorize that access.', 'jetpack-connection' ), '<strong>' . esc_html( $this->json_api_authorization_request['client_title'] ) . '</strong>' ) . '<img src="' . esc_url( $this->json_api_authorization_request['client_image'] ) . '" /></p>'; } } jetpack-connection/src/class-connection-assets.php 0000777 00000001604 15251741406 0016401 0 ustar 00 <?php /** * Connection_Assets. * * @package automattic/jetpack-connection */ namespace Automattic\Jetpack\Connection; use Automattic\Jetpack\Assets; /** * Connection_Assets class. */ class Connection_Assets { /** * Initialize the class. */ public static function configure() { add_action( 'wp_loaded', array( __CLASS__, 'register_assets' ) ); add_filter( 'jetpack_admin_js_script_data', array( Initial_State::class, 'set_connection_script_data' ), 10, 1 ); } /** * Register assets. * * NOTICE: Please think twice before including Connection scripts in the frontend. * Those scripts are intended to be used in WP admin area. */ public static function register_assets() { Assets::register_script( 'jetpack-connection', '../dist/jetpack-connection.js', __FILE__, array( 'in_footer' => true, 'textdomain' => 'jetpack-connection', ) ); } } jetpack-connection/src/class-client.php 0000777 00000037517 15251741406 0014234 0 ustar 00 <?php /** * The Connection Client class file. * * @package automattic/jetpack-connection */ namespace Automattic\Jetpack\Connection; use Automattic\Jetpack\Constants; use WP_Error; // `wp_remote_request` returns an array with a particular format. '@phan-type _WP_Remote_Response_Array = array{headers:\WpOrg\Requests\Utility\CaseInsensitiveDictionary,body:string,response:array{code:int,message:string},cookies:\WP_HTTP_Cookie[],filename:?string,http_response:WP_HTTP_Requests_Response}'; /** * The Client class that is used to connect to WordPress.com Jetpack API. */ class Client { const WPCOM_JSON_API_VERSION = '1.1'; /** * Makes an authorized remote request using Jetpack_Signature * * @param array $args the arguments for the remote request. * @param array|string|null $body the request body. * @return array|WP_Error WP HTTP response on success * @phan-return _WP_Remote_Response_Array|WP_Error */ public static function remote_request( $args, $body = null ) { if ( isset( $args['url'] ) ) { /** * Filters the remote request url. * * @since 1.30.12 * * @param string The remote request url. */ $args['url'] = apply_filters( 'jetpack_remote_request_url', $args['url'] ); } $result = self::build_signed_request( $args, $body ); if ( is_wp_error( $result ) ) { return $result; } $response = self::_wp_remote_request( $result['url'], $result['request'] ); Error_Handler::get_instance()->check_api_response_for_errors( $response, $result['auth'], empty( $args['url'] ) ? '' : $args['url'], empty( $args['method'] ) ? 'POST' : $args['method'], 'rest' ); /** * Fired when the remote request response has been received. * * @since 1.30.8 * * @param array|WP_Error The HTTP response. */ do_action( 'jetpack_received_remote_request_response', $response ); return $response; } /** * Adds authorization signature to a remote request using Jetpack_Signature * * @param array $args the arguments for the remote request. * @param array|string|null $body the request body. * @return WP_Error|array{url:string,request:array,auth:array} { * An array containing URL and request items. * * @type string $url The request URL. * @type array $request Request arguments. * @type array $auth Authorization data. * } */ public static function build_signed_request( $args, $body = null ) { add_filter( 'jetpack_constant_default_value', __NAMESPACE__ . '\Utils::jetpack_api_constant_filter', 10, 2 ); $defaults = array( 'url' => '', 'user_id' => 0, 'blog_id' => 0, 'auth_location' => Constants::get_constant( 'JETPACK_CLIENT__AUTH_LOCATION' ), 'method' => 'POST', 'format' => 'json', 'timeout' => 10, 'redirection' => 0, 'headers' => array(), 'stream' => false, 'filename' => null, 'sslverify' => true, ); $args = wp_parse_args( $args, $defaults ); $args['blog_id'] = (int) $args['blog_id']; if ( 'header' !== $args['auth_location'] ) { $args['auth_location'] = 'query_string'; } $token = ( new Tokens() )->get_access_token( $args['user_id'] ); if ( ! $token ) { return new WP_Error( 'missing_token' ); } $method = strtoupper( $args['method'] ); $timeout = (int) $args['timeout']; $redirection = $args['redirection']; $stream = $args['stream']; $filename = $args['filename']; $sslverify = $args['sslverify']; $request = compact( 'method', 'body', 'timeout', 'redirection', 'stream', 'filename', 'sslverify' ); @list( $token_key, $secret ) = explode( '.', $token->secret ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged if ( ! $secret ) { return new WP_Error( 'malformed_token' ); } $token_key = sprintf( '%s:%d:%d', $token_key, Constants::get_constant( 'JETPACK__API_VERSION' ), $token->external_user_id ); $time_diff = (int) \Jetpack_Options::get_option( 'time_diff' ); $jetpack_signature = new \Jetpack_Signature( $token->secret, $time_diff ); $timestamp = time() + $time_diff; if ( function_exists( 'wp_generate_password' ) ) { $nonce = wp_generate_password( 10, false ); } else { $nonce = substr( sha1( (string) wp_rand( 0, 1000000 ) ), 0, 10 ); } // Kind of annoying. Maybe refactor Jetpack_Signature to handle body-hashing. if ( $body === null ) { $body_hash = ''; } else { // Allow arrays to be used in passing data. $body_to_hash = $body; if ( $args['format'] === 'jsonl' ) { parse_str( $body, $body_to_hash ); } if ( is_array( $body_to_hash ) ) { // We cast this to a new variable, because the array form of $body needs to be // maintained so it can be passed into the request later on in the code. if ( array() !== $body_to_hash ) { $body_to_hash = wp_json_encode( self::_stringify_data( $body_to_hash ) ); } else { $body_to_hash = ''; } } if ( ! is_string( $body_to_hash ) ) { return new WP_Error( 'invalid_body', 'Body is malformed.' ); } $body_hash = base64_encode( sha1( $body_to_hash, true ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode } $auth = array( 'token' => $token_key, 'timestamp' => $timestamp, 'nonce' => $nonce, 'body-hash' => $body_hash, ); if ( false !== strpos( $args['url'], 'xmlrpc.php' ) ) { $url_args = array( 'for' => 'jetpack', 'wpcom_blog_id' => \Jetpack_Options::get_option( 'id' ), ); } else { $url_args = array(); } if ( 'header' !== $args['auth_location'] ) { $url_args += $auth; } $url = add_query_arg( urlencode_deep( $url_args ), $args['url'] ); $signature = $jetpack_signature->sign_request( $token_key, $timestamp, $nonce, $body_hash, $method, $url, $body, false ); if ( is_wp_error( $signature ) ) { return $signature; } // Send an Authorization header so various caches/proxies do the right thing. $auth['signature'] = $signature; $auth['version'] = Constants::get_constant( 'JETPACK__VERSION' ); $header_pieces = array(); foreach ( $auth as $key => $value ) { $header_pieces[] = sprintf( '%s="%s"', $key, $value ); } $request['headers'] = array_merge( $args['headers'], array( 'Authorization' => 'X_JETPACK ' . implode( ' ', $header_pieces ), ) ); if ( 'header' !== $args['auth_location'] ) { $url = add_query_arg( 'signature', rawurlencode( $signature ), $url ); } return compact( 'url', 'request', 'auth' ); } /** * Wrapper for wp_remote_request(). Turns off SSL verification for certain SSL errors. * This is lame, but many, many, many hosts have misconfigured SSL. * * When Jetpack is registered, the jetpack_fallback_no_verify_ssl_certs option is set to the current time if: * 1. a certificate error is found AND * 2. not verifying the certificate works around the problem. * * The option is checked on each request. * * @internal * * @param string $url the request URL. * @param array $args request arguments. * @param boolean $set_fallback whether to allow flagging this request to use a fallback certficate override. * @return array|WP_Error WP HTTP response on success * @phan-return _WP_Remote_Response_Array|WP_Error */ public static function _wp_remote_request( $url, $args, $set_fallback = false ) { // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore $fallback = \Jetpack_Options::get_option( 'fallback_no_verify_ssl_certs' ); if ( false === $fallback ) { \Jetpack_Options::update_option( 'fallback_no_verify_ssl_certs', 0 ); } /** * SSL verification (`sslverify`) for the JetpackClient remote request * defaults to off, use this filter to force it on. * * Return `true` to ENABLE SSL verification, return `false` * to DISABLE SSL verification. * * @since 1.7.0 * @since-jetpack 3.6.0 * * @param bool Whether to force `sslverify` or not. */ if ( apply_filters( 'jetpack_client_verify_ssl_certs', false ) ) { return wp_remote_request( $url, $args ); } if ( (int) $fallback ) { // We're flagged to fallback. $args['sslverify'] = false; } $response = wp_remote_request( $url, $args ); if ( ! $set_fallback // We're not allowed to set the flag on this request, so whatever happens happens. || isset( $args['sslverify'] ) && ! $args['sslverify'] // No verification - no point in doing it again. || ! is_wp_error( $response ) // Let it ride. ) { self::set_time_diff( $response, $set_fallback ); return $response; } // At this point, we're not flagged to fallback and we are allowed to set the flag on this request. $message = $response->get_error_message(); // Is it an SSL Certificate verification error? if ( false === strpos( $message, '14090086' ) // OpenSSL SSL3 certificate error. && false === strpos( $message, '1407E086' ) // OpenSSL SSL2 certificate error. && false === strpos( $message, 'error setting certificate verify locations' ) // cURL CA bundle not found. && false === strpos( $message, 'Peer certificate cannot be authenticated with' ) // cURL CURLE_SSL_CACERT: CA bundle found, but not helpful // Different versions of curl have different error messages // this string should catch them all. && false === strpos( $message, 'Problem with the SSL CA cert' ) // cURL CURLE_SSL_CACERT_BADFILE: probably access rights. ) { // No, it is not. return $response; } // Redo the request without SSL certificate verification. $args['sslverify'] = false; $response = wp_remote_request( $url, $args ); if ( ! is_wp_error( $response ) ) { // The request went through this time, flag for future fallbacks. \Jetpack_Options::update_option( 'fallback_no_verify_ssl_certs', time() ); self::set_time_diff( $response, $set_fallback ); } return $response; } /** * Sets the time difference for correct signature computation. * * @param array|WP_Error $response Response array from `wp_remote_request`, or WP_Error on error. * @param bool $force_set whether to force setting the time difference. * @phan-param _WP_Remote_Response_Array|WP_Error $response */ public static function set_time_diff( &$response, $force_set = false ) { $code = wp_remote_retrieve_response_code( $response ); // Only trust the Date header on some responses. if ( 200 != $code && 304 != $code && 400 != $code && 401 != $code ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseNotEqual return; } $date = wp_remote_retrieve_header( $response, 'date' ); if ( ! $date ) { return; } $time = (int) strtotime( $date ); if ( 0 >= $time ) { return; } $time_diff = $time - time(); if ( $force_set ) { // During register. \Jetpack_Options::update_option( 'time_diff', $time_diff ); } else { // Otherwise. $old_diff = \Jetpack_Options::get_option( 'time_diff' ); if ( false === $old_diff || abs( $time_diff - (int) $old_diff ) > 10 ) { \Jetpack_Options::update_option( 'time_diff', $time_diff ); } } } /** * Validate and build arguments for a WordPress.com REST API request. * * @param string $path REST API path. * @param string $version REST API version. Default is `2`. * @param array $args Arguments to {@see WP_Http}. Default is `array()`. * @param string $base_api_path REST API root. Default is `wpcom`. * * @return array Validated arguments. */ public static function validate_args_for_wpcom_json_api_request( $path, $version = '2', $args = array(), $base_api_path = 'wpcom' ) { $base_api_path = trim( $base_api_path, '/' ); $version = ltrim( $version, 'v' ); $path = ltrim( $path, '/' ); $filtered_args = array_intersect_key( $args, array( 'headers' => 'array', 'method' => 'string', 'format' => 'string', 'timeout' => 'int', 'redirection' => 'int', 'stream' => 'boolean', 'filename' => 'string', 'sslverify' => 'boolean', ) ); // Use GET by default whereas `remote_request` uses POST. $request_method = isset( $filtered_args['method'] ) ? strtoupper( $filtered_args['method'] ) : 'GET'; $url = sprintf( '%s/%s/v%s/%s', Constants::get_constant( 'JETPACK__WPCOM_JSON_API_BASE' ), $base_api_path, $version, $path ); $validated_args = array_merge( $filtered_args, array( 'url' => $url, 'method' => $request_method, ) ); return $validated_args; } /** * Queries the WordPress.com REST API with a user token. * * @param string $path REST API path. * @param string $version REST API version. Default is `2`. * @param array $args Arguments to {@see WP_Http}. Default is `array()`. * @param null|string|array $body Body passed to {@see WP_Http}. Default is `null`. * @param string $base_api_path REST API root. Default is `wpcom`. * * @return array|WP_Error $response Response data, else {@see WP_Error} on failure. * @phan-return _WP_Remote_Response_Array|WP_Error */ public static function wpcom_json_api_request_as_user( $path, $version = '2', $args = array(), $body = null, $base_api_path = 'wpcom' ) { $args = self::validate_args_for_wpcom_json_api_request( $path, $version, $args, $base_api_path ); $args['user_id'] = get_current_user_id(); if ( isset( $body ) && ! isset( $args['headers'] ) && in_array( $args['method'], array( 'POST', 'PUT', 'PATCH' ), true ) ) { $args['headers'] = array( 'Content-Type' => 'application/json' ); } if ( isset( $body ) && ! is_string( $body ) ) { $body = wp_json_encode( $body ); } return self::remote_request( $args, $body ); } /** * Query the WordPress.com REST API using the blog token * * @param string $path The API endpoint relative path. * @param string $version The API version. * @param array $args Request arguments. * @param array|string|null $body Request body. * @param string $base_api_path (optional) the API base path override, defaults to 'rest'. * @return array|WP_Error $response Data. * @phan-return _WP_Remote_Response_Array|WP_Error */ public static function wpcom_json_api_request_as_blog( $path, $version = self::WPCOM_JSON_API_VERSION, $args = array(), $body = null, $base_api_path = 'rest' ) { $validated_args = self::validate_args_for_wpcom_json_api_request( $path, $version, $args, $base_api_path ); $validated_args['blog_id'] = (int) \Jetpack_Options::get_option( 'id' ); // For Simple sites get the response directly without any HTTP requests. if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) { add_filter( 'is_jetpack_authorized_for_site', '__return_true' ); require_lib( 'wpcom-api-direct' ); return \WPCOM_API_Direct::do_request( $validated_args, $body ); } return self::remote_request( $validated_args, $body ); } /** * Takes an array or similar structure and recursively turns all values into strings. This is used to * make sure that body hashes are made ith the string version, which is what will be seen after a * server pulls up the data in the $_POST array. * * @param mixed $data the data that needs to be stringified. * * @return array|string */ public static function _stringify_data( $data ) { // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore // Booleans are special, lets just makes them and explicit 1/0 instead of the 0 being an empty string. if ( is_bool( $data ) ) { return $data ? '1' : '0'; } // Cast objects into arrays. if ( is_object( $data ) ) { $data = (array) $data; } // Non arrays at this point should be just converted to strings. if ( ! is_array( $data ) ) { return (string) $data; } foreach ( $data as &$value ) { $value = self::_stringify_data( $value ); } return $data; } } jetpack-connection/src/class-plugin-storage.php 0000777 00000017075 15251741406 0015713 0 ustar 00 <?php /** * Storage for plugin connection information. * * @package automattic/jetpack-connection */ namespace Automattic\Jetpack\Connection; use Jetpack_Options; use WP_Error; /** * The class serves a single purpose - to store the data which plugins use the connection, along with some auxiliary information. */ class Plugin_Storage { const ACTIVE_PLUGINS_OPTION_NAME = 'jetpack_connection_active_plugins'; /** * Transient name used as flag to indicate that the active connected plugins list needs refreshing. */ const ACTIVE_PLUGINS_REFRESH_FLAG = 'jetpack_connection_active_plugins_refresh'; /** * Whether this class was configured for the first time or not. * * @var boolean */ private static $configured = false; /** * Connected plugins. * * @var array */ private static $plugins = array(); /** * The blog ID the storage is setup for. * The data will be refreshed if the blog ID changes. * Used for the multisite networks. * * @var int */ private static $current_blog_id = null; /** * Add or update the plugin information in the storage. * * @param string $slug Plugin slug. * @param array $args Plugin arguments, optional. * * @return bool */ public static function upsert( $slug, array $args = array() ) { self::$plugins[ $slug ] = $args; return true; } /** * Retrieve the plugin information by slug. * WARNING: the method cannot be called until Plugin_Storage::configure is called, which happens on plugins_loaded * Even if you don't use Jetpack Config, it may be introduced later by other plugins, * so please make sure not to run the method too early in the code. * * @param string $slug The plugin slug. * * @return array|null|WP_Error */ public static function get_one( $slug ) { $plugins = self::get_all(); if ( $plugins instanceof WP_Error ) { return $plugins; } return empty( $plugins[ $slug ] ) ? null : $plugins[ $slug ]; } /** * Retrieve info for all plugins that use the connection. * WARNING: the method cannot be called until Plugin_Storage::configure is called, which happens on plugins_loaded * Even if you don't use Jetpack Config, it may be introduced later by other plugins, * so please make sure not to run the method too early in the code. * * @return array|WP_Error */ public static function get_all() { $maybe_error = self::ensure_configured(); if ( $maybe_error instanceof WP_Error ) { return $maybe_error; } return self::$plugins; } /** * Remove the plugin connection info from Jetpack. * WARNING: the method cannot be called until Plugin_Storage::configure is called, which happens on plugins_loaded * Even if you don't use Jetpack Config, it may be introduced later by other plugins, * so please make sure not to run the method too early in the code. * * @param string $slug The plugin slug. * * @return bool|WP_Error */ public static function delete( $slug ) { $maybe_error = self::ensure_configured(); if ( $maybe_error instanceof WP_Error ) { return $maybe_error; } if ( array_key_exists( $slug, self::$plugins ) ) { unset( self::$plugins[ $slug ] ); } return true; } /** * The method makes sure that `Jetpack\Config` has finished, and it's now safe to retrieve the list of plugins. * * @return bool|WP_Error */ private static function ensure_configured() { if ( ! self::$configured ) { return new WP_Error( 'too_early', __( 'You cannot call this method until Jetpack Config is configured', 'jetpack-connection' ) ); } if ( is_multisite() && get_current_blog_id() !== self::$current_blog_id ) { if ( self::$current_blog_id ) { // If blog ID got changed, pull the list of active plugins for that blog from the database. self::$plugins = (array) get_option( self::ACTIVE_PLUGINS_OPTION_NAME, array() ); } self::$current_blog_id = get_current_blog_id(); } return true; } /** * Called once to configure this class after plugins_loaded. * * @return void */ public static function configure() { if ( self::$configured ) { return; } self::$configured = true; add_action( 'update_option_active_plugins', array( __CLASS__, 'set_flag_to_refresh_active_connected_plugins' ) ); self::maybe_update_active_connected_plugins(); } /** * Set a flag to indicate that the active connected plugins list needs to be updated. * This will happen when the `active_plugins` option is updated. * * @see configure */ public static function set_flag_to_refresh_active_connected_plugins() { set_transient( self::ACTIVE_PLUGINS_REFRESH_FLAG, time() ); } /** * Determine if we need to update the active connected plugins list. */ public static function maybe_update_active_connected_plugins() { $maybe_error = self::ensure_configured(); if ( $maybe_error instanceof WP_Error ) { return; } // Only attempt to update the option if the corresponding flag is set. if ( ! get_transient( self::ACTIVE_PLUGINS_REFRESH_FLAG ) ) { return; } // Only attempt to update the option on POST requests. // This will prevent the option from being updated multiple times due to concurrent requests. if ( ! ( isset( $_SERVER['REQUEST_METHOD'] ) && 'POST' === $_SERVER['REQUEST_METHOD'] ) ) { return; } delete_transient( self::ACTIVE_PLUGINS_REFRESH_FLAG ); if ( is_multisite() ) { self::$current_blog_id = get_current_blog_id(); } // If a plugin was activated or deactivated. // self::$plugins is populated in Config::ensure_options_connection(). $configured_plugin_keys = array_keys( self::$plugins ); $stored_plugin_keys = array_keys( (array) get_option( self::ACTIVE_PLUGINS_OPTION_NAME, array() ) ); sort( $configured_plugin_keys ); sort( $stored_plugin_keys ); if ( $configured_plugin_keys !== $stored_plugin_keys ) { self::update_active_plugins_option(); } } /** * Updates the active plugins option with current list of active plugins. * * @return void */ public static function update_active_plugins_option() { // Note: Since this option is synced to wpcom, if you change its structure, you have to update the sanitizer at wpcom side. update_option( self::ACTIVE_PLUGINS_OPTION_NAME, self::$plugins ); if ( ! class_exists( 'Automattic\Jetpack\Sync\Settings' ) || ! \Automattic\Jetpack\Sync\Settings::is_sync_enabled() ) { self::update_active_plugins_wpcom_no_sync_fallback(); // Remove the checksum for active plugins, so it gets recalculated when sync gets activated. $jetpack_callables_sync_checksum = Jetpack_Options::get_raw_option( 'jetpack_callables_sync_checksum' ); if ( isset( $jetpack_callables_sync_checksum['jetpack_connection_active_plugins'] ) ) { unset( $jetpack_callables_sync_checksum['jetpack_connection_active_plugins'] ); Jetpack_Options::update_raw_option( 'jetpack_callables_sync_checksum', $jetpack_callables_sync_checksum ); } } } /** * Update active plugins option with current list of active plugins on WPCOM. * This is a fallback to ensure this option is always up to date on WPCOM in case * Sync is not present or disabled. * * @since 1.34.0 */ private static function update_active_plugins_wpcom_no_sync_fallback() { $connection = new Manager(); if ( ! $connection->is_connected() ) { return; } $site_id = \Jetpack_Options::get_option( 'id' ); $body = wp_json_encode( array( 'active_connected_plugins' => self::$plugins, ) ); Client::wpcom_json_api_request_as_blog( sprintf( '/sites/%d/jetpack-active-connected-plugins', $site_id ), '2', array( 'headers' => array( 'content-type' => 'application/json' ), 'method' => 'POST', ), $body, 'wpcom' ); } } jetpack-connection/src/class-rest-authentication.php 0000777 00000014236 15251741406 0016741 0 ustar 00 <?php /** * The Jetpack Connection Rest Authentication file. * * @package automattic/jetpack-connection */ namespace Automattic\Jetpack\Connection; use WP_Error; /** * The Jetpack Connection Rest Authentication class. */ class Rest_Authentication { /** * The rest authentication status. * * @since 1.17.0 * @var boolean */ private $rest_authentication_status = null; /** * The rest authentication type. * Can be either 'user' or 'blog' depending on whether the request * is signed with a user or a blog token. * * @since 1.29.0 * @var string */ private $rest_authentication_type = null; /** * The Manager object. * * @since 1.17.0 * @var Object */ private $connection_manager = null; /** * Holds the singleton instance of this class * * @since 1.17.0 * @var Object */ private static $instance = false; /** * Flag used to avoid determine_current_user filter to enter an infinite loop * * @since 1.26.0 * @var boolean */ private $doing_determine_current_user_filter = false; /** * The constructor. */ private function __construct() { $this->connection_manager = new Manager(); } /** * Controls the single instance of this class. * * @static */ public static function init() { if ( ! self::$instance ) { self::$instance = new self(); add_filter( 'determine_current_user', array( self::$instance, 'wp_rest_authenticate' ) ); add_filter( 'rest_authentication_errors', array( self::$instance, 'wp_rest_authentication_errors' ) ); } return self::$instance; } /** * Authenticates requests from Jetpack server to WP REST API endpoints. * Uses the existing XMLRPC request signing implementation. * * @param int|bool $user User ID if one has been determined, false otherwise. * * @return int|null The user id or null if the request was authenticated via blog token, or not authenticated at all. */ public function wp_rest_authenticate( $user ) { if ( $this->doing_determine_current_user_filter ) { return $user; } $this->doing_determine_current_user_filter = true; try { if ( ! empty( $user ) ) { // Another authentication method is in effect. return $user; } add_filter( 'jetpack_constant_default_value', __NAMESPACE__ . '\Utils::jetpack_api_constant_filter', 10, 2 ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( ! isset( $_GET['_for'] ) || 'jetpack' !== $_GET['_for'] ) { // Nothing to do for this authentication method. return null; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( ! isset( $_GET['token'] ) && ! isset( $_GET['signature'] ) ) { // Nothing to do for this authentication method. return null; } if ( ! isset( $_SERVER['REQUEST_METHOD'] ) ) { $this->rest_authentication_status = new WP_Error( 'rest_invalid_request', __( 'The request method is missing.', 'jetpack-connection' ), array( 'status' => 400 ) ); return null; } // Only support specific request parameters that have been tested and // are known to work with signature verification. A different method // can be passed to the WP REST API via the '?_method=' parameter if // needed. if ( 'GET' !== $_SERVER['REQUEST_METHOD'] && 'POST' !== $_SERVER['REQUEST_METHOD'] ) { $this->rest_authentication_status = new WP_Error( 'rest_invalid_request', __( 'This request method is not supported.', 'jetpack-connection' ), array( 'status' => 400 ) ); return null; } if ( 'POST' !== $_SERVER['REQUEST_METHOD'] && ! empty( file_get_contents( 'php://input' ) ) ) { $this->rest_authentication_status = new WP_Error( 'rest_invalid_request', __( 'This request method does not support body parameters.', 'jetpack-connection' ), array( 'status' => 400 ) ); return null; } $verified = $this->connection_manager->verify_xml_rpc_signature(); if ( $verified && isset( $verified['type'] ) && 'blog' === $verified['type'] ) { // Site-level authentication successful. $this->rest_authentication_status = true; $this->rest_authentication_type = 'blog'; return null; } if ( $verified && isset( $verified['type'] ) && 'user' === $verified['type'] && ! empty( $verified['user_id'] ) ) { // User-level authentication successful. $this->rest_authentication_status = true; $this->rest_authentication_type = 'user'; return $verified['user_id']; } // Something else went wrong. Probably a signature error. $this->rest_authentication_status = new WP_Error( 'rest_invalid_signature', __( 'The request is not signed correctly.', 'jetpack-connection' ), array( 'status' => 400 ) ); return null; } finally { $this->doing_determine_current_user_filter = false; } } /** * Report authentication status to the WP REST API. * * @param WP_Error|mixed $value Error from another authentication handler, null if we should handle it, or another value if not. * @return WP_Error|boolean|null {@see WP_JSON_Server::check_authentication} */ public function wp_rest_authentication_errors( $value ) { if ( null !== $value ) { return $value; } return $this->rest_authentication_status; } /** * Resets the saved authentication state in between testing requests. */ public function reset_saved_auth_state() { $this->rest_authentication_status = null; $this->connection_manager->reset_saved_auth_state(); } /** * Whether the request was signed with a blog token. * * @since 1.29.0 * * @return bool True if the request was signed with a valid blog token, false otherwise. */ public static function is_signed_with_blog_token() { $instance = self::init(); return true === $instance->rest_authentication_status && 'blog' === $instance->rest_authentication_type; } /** * Whether the request was signed with a user token. * * @since 6.7.0 * * @return bool True if the request was signed with a valid user token, false otherwise. */ public static function is_signed_with_user_token() { $instance = self::init(); return true === $instance->rest_authentication_status && 'user' === $instance->rest_authentication_type; } } jetpack-connection/src/class-initial-state.php 0000777 00000003617 15251741406 0015517 0 ustar 00 <?php /** * The React initial state. * * @package automattic/jetpack-connection */ namespace Automattic\Jetpack\Connection; use Automattic\Jetpack\Status; /** * The React initial state. */ class Initial_State { /** * Get the initial state data. * * @return array */ private static function get_data() { global $wp_version; $status = new Status(); return array( 'apiRoot' => esc_url_raw( rest_url() ), 'apiNonce' => wp_create_nonce( 'wp_rest' ), 'registrationNonce' => wp_create_nonce( 'jetpack-registration-nonce' ), 'connectionStatus' => REST_Connector::connection_status( false ), 'userConnectionData' => REST_Connector::get_user_connection_data( false ), 'connectedPlugins' => REST_Connector::get_connection_plugins( false ), 'wpVersion' => $wp_version, 'siteSuffix' => $status->get_site_suffix(), 'connectionErrors' => Error_Handler::get_instance()->get_displayable_errors(), 'isOfflineMode' => $status->is_offline_mode(), 'calypsoEnv' => ( new Status\Host() )->get_calypso_env(), ); } /** * Set the connection script data. * * @param array $data The script data. */ public static function set_connection_script_data( $data ) { $data['connection'] = self::get_data(); return $data; } /** * Render the initial state into a JavaScript variable. * * @return string */ public static function render() { return 'var JP_CONNECTION_INITIAL_STATE; typeof JP_CONNECTION_INITIAL_STATE === "object" || (JP_CONNECTION_INITIAL_STATE = JSON.parse(decodeURIComponent("' . rawurlencode( wp_json_encode( self::get_data() ) ) . '")));'; } /** * Render the initial state using an inline script. * * @param string $handle The JS script handle. * * @return void */ public static function render_script( $handle ) { wp_add_inline_script( $handle, static::render(), 'before' ); } } jetpack-connection/src/class-package-version.php 0000777 00000001210 15251741406 0016011 0 ustar 00 <?php /** * The Package_Version class. * * @package automattic/jetpack-connection */ namespace Automattic\Jetpack\Connection; /** * The Package_Version class. */ class Package_Version { const PACKAGE_VERSION = '6.19.2'; const PACKAGE_SLUG = 'connection'; /** * Adds the package slug and version to the package version tracker's data. * * @param array $package_versions The package version array. * * @return array The packge version array. */ public static function send_package_version_to_tracker( $package_versions ) { $package_versions[ self::PACKAGE_SLUG ] = self::PACKAGE_VERSION; return $package_versions; } } jetpack-connection/dist/tracks-ajax.asset.php 0000777 00000000124 15251741406 0015335 0 ustar 00 <?php return array('dependencies' => array(), 'version' => 'a25b171bd940e52c2b96'); jetpack-connection/dist/jetpack-connection.js 0000777 00000311502 15251741406 0015417 0 ustar 00 !function(e,n){"object"==typeof exports&&"object"==typeof module?module.exports=n():"function"==typeof define&&define.amd?define([],n):"object"==typeof exports?exports.JetpackConnection=n():e.JetpackConnection=n()}(globalThis,()=>(()=>{var e={110:(e,n,t)=>{"use strict";t.d(n,{Q:()=>o,Z:()=>s});const o={"headline-medium":"h1","headline-small":"h2","headline-small-regular":"h2","title-medium":"h3","title-medium-semi-bold":"h3","title-small":"h4",body:"p","body-small":"p","body-extra-small":"p","body-extra-small-bold":"p",label:"p"},s=["mt","mr","mb","ml","mx","my","m","pt","pr","pb","pl","px","py","p"]},177:()=>{},372:(e,n,t)=>{"use strict";t.d(n,{A:()=>a});var o=t(4804);const s=t.n(o)()("dops:analytics");let c,i;window._tkq=window._tkq||[],window.ga=window.ga||function(){(window.ga.q=window.ga.q||[]).push(arguments)},window.ga.l=+new Date;const r={initialize:function(e,n,t){r.setUser(e,n),r.setSuperProps(t),r.identifyUser()},setGoogleAnalyticsEnabled:function(e,n=null){this.googleAnalyticsEnabled=e,this.googleAnalyticsKey=n},setMcAnalyticsEnabled:function(e){this.mcAnalyticsEnabled=e},setUser:function(e,n){i={ID:e,username:n}},setSuperProps:function(e){c=e},assignSuperProps:function(e){c=Object.assign(c||{},e)},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]);s("Bumping stats %o",e)}else t="&x_"+encodeURIComponent(e)+"="+encodeURIComponent(n),s('Bumping stat "%s" in group "%s"',n,e);return t}(e,n);r.mcAnalyticsEnabled&&((new Image).src=document.location.protocol+"//pixel.wp.com/g.gif?v=wpcom-no-pv"+t+"&t="+Math.random())},bumpStatWithPageView:function(e,n){const t=function(e,n){let t="";if("object"==typeof e){for(const n in e)t+="&"+encodeURIComponent(n)+"="+encodeURIComponent(e[n]);s("Built stats %o",e)}else t="&"+encodeURIComponent(e)+"="+encodeURIComponent(n),s('Built stat "%s" in group "%s"',n,e);return t}(e,n);r.mcAnalyticsEnabled&&((new Image).src=document.location.protocol+"//pixel.wp.com/g.gif?v=wpcom"+t+"&t="+Math.random())}},pageView:{record:function(e,n){r.tracks.recordPageView(e),r.ga.recordPageView(e,n)}},purchase:{record:function(e,n,t,o,s,c,i){r.ga.recordPurchase(e,n,t,o,s,c,i)}},tracks:{recordEvent:function(e,n){n=n||{},0===e.indexOf("akismet_")||0===e.indexOf("jetpack_")?(c&&(s("- Super Props: %o",c),n=Object.assign(n,c)),s('Record event "%s" called with props %s',e,JSON.stringify(n)),window._tkq.push(["recordEvent",e,n])):s('- Event name must be prefixed by "akismet_" or "jetpack_"')},recordJetpackClick:function(e){const n="object"==typeof e?e:{target:e};r.tracks.recordEvent("jetpack_wpa_click",n)},recordPageView:function(e){r.tracks.recordEvent("akismet_page_view",{path:e})},setOptOut:function(e){s("Pushing setOptOut: %o",e),window._tkq.push(["setOptOut",e])}},ga:{initialized:!1,initialize:function(){let e={};r.ga.initialized||(i&&(e={userId:"u-"+i.ID}),window.ga("create",this.googleAnalyticsKey,"auto",e),r.ga.initialized=!0)},recordPageView:function(e,n){r.ga.initialize(),s("Recording Page View ~ [URL: "+e+"] [Title: "+n+"]"),this.googleAnalyticsEnabled&&(window.ga("set","page",e),window.ga("send",{hitType:"pageview",page:e,title:n}))},recordEvent:function(e,n,t,o){r.ga.initialize();let c="Recording Event ~ [Category: "+e+"] [Action: "+n+"]";void 0!==t&&(c+=" [Option Label: "+t+"]"),void 0!==o&&(c+=" [Option Value: "+o+"]"),s(c),this.googleAnalyticsEnabled&&window.ga("send","event",e,n,t,o)},recordPurchase:function(e,n,t,o,s,c,i){window.ga("require","ecommerce"),window.ga("ecommerce:addTransaction",{id:e,revenue:o,currency:i}),window.ga("ecommerce:addItem",{id:e,name:n,sku:t,price:s,quantity:c}),window.ga("ecommerce:send")}},identifyUser:function(){i&&window._tkq.push(["identifyUser",i.ID,i.username])},setProperties:function(e){window._tkq.push(["setProperties",e])},clearedIdentity:function(){window._tkq.push(["clearIdentity"])}},a=r},401:(e,n,t)=>{"use strict";t.d(n,{A:()=>y});var o=t(3924),s=t(8089),c=t(9957),i=t(5879),r=t(6087),a=t(7723),l=t(2231),d=t(4804),p=t.n(d),u=t(3619),m=t.n(u),g=t(2668),h=(t(1590),t(790));const __=a.__,f=p()("jetpack:connection:ConnectScreenRequiredPlanVisual"),_=e=>{const{title:n,buttonLabel:t,children:a,priceBefore:d,priceAfter:p,pricingIcon:u,pricingTitle:m,pricingCurrencyCode:_="USD",isLoading:y=!1,handleButtonClick:b=()=>{},displayButtonError:j=!1,buttonIsLoading:k=!1,logo:C,isOfflineMode:v,rna:w=!1}=e;f("props are %o",e);const x=(0,r.createInterpolateElement)(__("Already have a subscription? <connectButton/>","jetpack-connection"),{connectButton:(0,h.jsx)(s.A,{label:__("Log in to get started","jetpack-connection"),onClick:b,isLoading:k})}),A=v?(0,r.createInterpolateElement)(__("Unavailable in <a>Offline Mode</a>","jetpack-connection"),{a:(0,h.jsx)("a",{href:(0,o.A)("jetpack-support-development-mode"),target:"_blank",rel:"noopener noreferrer"})}):void 0;return(0,h.jsx)(g.A,{title:n,className:(0,l.A)("jp-connection__connect-screen-required-plan",y?"jp-connection__connect-screen-required-plan__loading":"",w?"rna":""),logo:C,rna:w,children:(0,h.jsxs)("div",{className:"jp-connection__connect-screen-required-plan__content",children:[a,(0,h.jsx)("div",{className:"jp-connection__connect-screen-required-plan__pricing-card",children:(0,h.jsxs)(c.A,{title:m,icon:u,priceBefore:d,currencyCode:_,priceAfter:p,children:[(0,h.jsx)(i.A,{agreeButtonLabel:t}),(0,h.jsx)(s.A,{label:t,onClick:b,displayError:j||v,errorMessage:A,isLoading:k,isDisabled:v})]})}),!v&&(0,h.jsx)("div",{className:"jp-connection__connect-screen-required-plan__with-subscription",children:x})]})})};_.propTypes={pricingTitle:m().string.isRequired,priceBefore:m().number.isRequired,priceAfter:m().number.isRequired,pricingCurrencyCode:m().string,title:m().string,buttonLabel:m().string,pricingIcon:m().oneOfType([m().string,m().element]),isLoading:m().bool,handleButtonClick:m().func,displayButtonError:m().bool,buttonIsLoading:m().bool,logo:m().element,isOfflineMode:m().bool};const y=_},412:(e,n,t)=>{"use strict";t.d(n,{A:()=>l});var o=t(7723),s=t(3619),c=t.n(s),i=(t(7556),t(2951)),r=t(790);const __=o.__,a=e=>{const{onExit:n,onFeedBackProvided:t,isSubmittingFeedback:o}=e;return(0,r.jsxs)("div",{className:"jp-connection__disconnect-dialog__content",children:[(0,r.jsx)("h1",{children:__("Before you go, help us improve Jetpack","jetpack-connection")}),(0,r.jsx)("p",{className:"jp-connection__disconnect-dialog__large-text",children:__("Let us know what didn‘t work for you","jetpack-connection")}),(0,r.jsx)(i.A,{onSubmit:t,isSubmittingFeedback:o}),(0,r.jsx)("a",{className:"jp-connection__disconnect-dialog__link jp-connection__disconnect-dialog__link--bold",href:"#",onClick:n,children:__("Skip for now","jetpack-connection")})]})};a.PropTypes={onExit:c().func,onFeedBackProvided:c().func,isSubmittingFeedback:c().bool};const l=a},648:(e,n,t)=>{"use strict";t.d(n,{A:()=>d});var o=t(7723),s=t(3619),c=t.n(s),i=t(1609),r=t(7499),a=t(790);const __=o.__,l=e=>{const{connectedPlugins:n,disconnectingPlugin:t}=e,o=(0,i.useMemo)(()=>{if(n){return Object.keys(n).map(e=>Object.assign({slug:e},n[e])).filter(e=>t!==e.slug)}return[]},[n,t]);return n&&o.length>0?(0,a.jsxs)(i.Fragment,{children:[(0,a.jsx)("div",{className:"jp-connection__disconnect-dialog__step-copy",children:(0,a.jsx)("p",{className:"jp-connection__disconnect-dialog__large-text",children:__("Jetpack is powering other plugins on your site. If you disconnect, these plugins will no longer work.","jetpack-connection")})}),(0,a.jsx)("div",{className:"jp-connection__disconnect-card__group",children:o.map(e=>(0,a.jsx)(r.A,{title:e.name},e.slug))})]}):null};l.propTypes={connectedPlugins:c().array,disconnectingPlugin:c().string};const d=l},790:e=>{"use strict";e.exports=window.ReactJSXRuntime},1112:(e,n,t)=>{"use strict";t.d(n,{A:()=>u});var o=t(6427),s=t(7723),c=t(7750),i=t(8391),r=t(2231),a=t(1609),l=t(4659),d=t(790);const __=s.__,p=(0,a.forwardRef)((e,n)=>{const{children:t,variant:s="primary",size:a="normal",weight:p="bold",icon:u,iconSize:m,disabled:g,isDestructive:h,isLoading:f,isExternalLink:_,className:y,text:b,fullWidth:j,...k}=e,C=(0,r.A)(l.A.button,y,{[l.A.normal]:"normal"===a,[l.A.small]:"small"===a,[l.A.icon]:Boolean(u),[l.A.loading]:f,[l.A.regular]:"regular"===p,[l.A["full-width"]]:j,[l.A["is-icon-button"]]:Boolean(u)&&!t});k.ref=n;const v="normal"===a?20:16,w=_&&(0,d.jsxs)(d.Fragment,{children:[(0,d.jsx)(c.A,{size:v,icon:i.A,className:l.A["external-icon"]}),(0,d.jsx)(o.VisuallyHidden,{as:"span",children:/* translators: accessibility text */ __("(opens in a new tab)","jetpack-connection")})]}),x=_?"_blank":void 0,A=t?.[0]&&null!==t[0]&&"components-tooltip"!==t?.[0]?.props?.className;return(0,d.jsxs)(o.Button,{target:x,variant:s,className:(0,r.A)(C,{"has-text":!!u&&A}),icon:_?void 0:u,iconSize:m,disabled:g,"aria-disabled":g,isDestructive:h,text:b,...k,children:[f&&(0,d.jsx)(o.Spinner,{}),(0,d.jsx)("span",{children:t}),w]})});p.displayName="Button";const u=p},1133:()=>{},1167:(e,n,t)=>{"use strict";t.d(n,{c:()=>c,j:()=>s});var o=t(3328);const s=({browserSafeLocale:e,decimals:n=0,forceLatin:t=!0,numberFormatOptions:s={}})=>{const c=`${e}${t?"-u-nu-latn":""}`,i={minimumFractionDigits:n,maximumFractionDigits:n,...s};return(0,o.J)({locale:c,options:i})},c=({numberFormatOptions:e={},...n})=>s({...n,numberFormatOptions:{notation:"compact",maximumFractionDigits:1,...e}})},1386:(e,n,t)=>{"use strict";t.d(n,{A:()=>c});var o=t(5573),s=t(790);const c=(0,s.jsx)(o.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,s.jsx)(o.Path,{d:"M10.6 6L9.4 7l4.6 5-4.6 5 1.2 1 5.4-6z"})})},1452:(e,n,t)=>{"use strict";t.d(n,{A:()=>r});var o=t(8443),s=t(3673),c=t(9980),i=t(1167);const r=function(){let e,n;const t=()=>{const{l10n:{locale:n}}=(0,o.getSettings)();return(e??(n||window?.window?.navigator?.language)??s.M).split("_")[0]};return{setLocale:n=>{e=n},setGeoLocation:e=>{n=e},formatNumber:(e,{decimals:n=0,forceLatin:o=!0,numberFormatOptions:s={}}={})=>{try{return(0,i.j)({browserSafeLocale:t(),decimals:n,forceLatin:o,numberFormatOptions:s}).format(e)}catch{return String(e)}},formatNumberCompact:(e,{decimals:n=0,forceLatin:o=!0,numberFormatOptions:s={}}={})=>{try{return(0,i.c)({browserSafeLocale:t(),decimals:n,forceLatin:o,numberFormatOptions:s}).format(e)}catch{return String(e)}},formatCurrency:(e,o,{stripZeros:s=!1,isSmallestUnit:i=!1,signForPositive:r=!1,forceLatin:a=!0}={})=>(0,c.u)({number:e,currency:o,browserSafeLocale:t(),stripZeros:s,isSmallestUnit:i,signForPositive:r,geoLocation:n,forceLatin:a}),getCurrencyObject:(e,o,{stripZeros:s=!1,isSmallestUnit:i=!1,signForPositive:r=!1,forceLatin:a=!0}={})=>(0,c.v)({number:e,currency:o,browserSafeLocale:t(),stripZeros:s,isSmallestUnit:i,signForPositive:r,geoLocation:n,forceLatin:a})}}},1583:(e,n,t)=>{"use strict";var o=t(1752);function s(){}function c(){}c.resetWarningCache=s,e.exports=function(){function e(e,n,t,s,c,i){if(i!==o){var r=new Error("Calling PropTypes validators directly is not supported by the `prop-types` package. Use PropTypes.checkPropTypes() to call them. Read more at http://fb.me/use-check-prop-types");throw r.name="Invariant Violation",r}}function n(){return e}e.isRequired=e;var t={array:e,bigint:e,bool:e,func:e,number:e,object:e,string:e,symbol:e,any:e,arrayOf:n,element:e,elementType:e,instanceOf:n,node:e,objectOf:n,oneOf:n,oneOfType:n,shape:n,exact:n,checkPropTypes:c,resetWarningCache:s};return t.PropTypes=t,t}},1590:()=>{},1609:e=>{"use strict";e.exports=window.React},1713:(e,n,t)=>{"use strict";t.d(n,{A:()=>p});var o=t(5932),s=t(7999),c=t(7143),i=t(1609),r=t(8343),a=t(4293);const{apiRoot:l,apiNonce:d}=window?.JP_CONNECTION_INITIAL_STATE||(0,s.getScriptData)()?.connection||{};function p(){const[e,n]=(0,i.useState)(!1),[t,s]=(0,i.useState)(null),{disconnectUserSuccess:p,setConnectionErrors:u}=(0,c.useDispatch)(a.a),m=(0,r.z)();return(0,i.useEffect)(()=>{o.Ay.setApiRoot(l),o.Ay.setApiNonce(d)},[]),{restoreConnection:(e=!0)=>(n(!0),s(null),o.Ay.reconnect().then(n=>("in_progress"===n.status?(p(),u({}),e&&(window.location.href=m)):window.location.reload(),n)).catch(e=>{throw s(e),n(!1),e})),isRestoringConnection:e,restoreConnectionError:t}}},1752:e=>{"use strict";e.exports="SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED"},1876:(e,n,t)=>{"use strict";t.d(n,{A:()=>i});var o=t(2231),s=t(7773),c=t(790);const i=({children:e=null,width:n=null,height:t=null,className:i=""})=>(0,c.jsx)("div",{className:(0,o.A)(s.A.placeholder,i),style:{width:n,height:t},children:e})},2144:()=>{},2231:(e,n,t)=>{"use strict";function o(e){var n,t,s="";if("string"==typeof e||"number"==typeof e)s+=e;else if("object"==typeof e)if(Array.isArray(e)){var c=e.length;for(n=0;n<c;n++)e[n]&&(t=o(e[n]))&&(s&&(s+=" "),s+=t)}else for(t in e)e[t]&&(s&&(s+=" "),s+=t);return s}t.d(n,{A:()=>s});const s=function(){for(var e,n,t=0,s="",c=arguments.length;t<c;t++)(e=arguments[t])&&(n=o(e))&&(s&&(s+=" "),s+=n);return s}},2279:(e,n,t)=>{"use strict";t.d(n,{A:()=>o});const o="jetpack-connection"},2365:(e,n,t)=>{"use strict";e.exports=t.p+"images/disconnect-confirm-dc9fe8f5c68cfd1320e0.jpg"},2423:(e,n,t)=>{n.formatArgs=function(n){if(n[0]=(this.useColors?"%c":"")+this.namespace+(this.useColors?" %c":" ")+n[0]+(this.useColors?"%c ":" ")+"+"+e.exports.humanize(this.diff),!this.useColors)return;const t="color: "+this.color;n.splice(1,0,t,"color: inherit");let o=0,s=0;n[0].replace(/%[a-zA-Z%]/g,e=>{"%%"!==e&&(o++,"%c"===e&&(s=o))}),n.splice(s,0,t)},n.save=function(e){try{e?n.storage.setItem("debug",e):n.storage.removeItem("debug")}catch(e){}},n.load=function(){let e;try{e=n.storage.getItem("debug")||n.storage.getItem("DEBUG")}catch(e){}!e&&"undefined"!=typeof process&&"env"in process&&(e=process.env.DEBUG);return e},n.useColors=function(){if("undefined"!=typeof window&&window.process&&("renderer"===window.process.type||window.process.__nwjs))return!0;if("undefined"!=typeof navigator&&navigator.userAgent&&navigator.userAgent.toLowerCase().match(/(edge|trident)\/(\d+)/))return!1;let e;return"undefined"!=typeof document&&document.documentElement&&document.documentElement.style&&document.documentElement.style.WebkitAppearance||"undefined"!=typeof window&&window.console&&(window.console.firebug||window.console.exception&&window.console.table)||"undefined"!=typeof navigator&&navigator.userAgent&&(e=navigator.userAgent.toLowerCase().match(/firefox\/(\d+)/))&&parseInt(e[1],10)>=31||"undefined"!=typeof navigator&&navigator.userAgent&&navigator.userAgent.toLowerCase().match(/applewebkit\/(\d+)/)},n.storage=function(){try{return localStorage}catch(e){}}(),n.destroy=(()=>{let e=!1;return()=>{e||(e=!0,console.warn("Instance method `debug.destroy()` is deprecated and no longer does anything. It will be removed in the next major version of `debug`."))}})(),n.colors=["#0000CC","#0000FF","#0033CC","#0033FF","#0066CC","#0066FF","#0099CC","#0099FF","#00CC00","#00CC33","#00CC66","#00CC99","#00CCCC","#00CCFF","#3300CC","#3300FF","#3333CC","#3333FF","#3366CC","#3366FF","#3399CC","#3399FF","#33CC00","#33CC33","#33CC66","#33CC99","#33CCCC","#33CCFF","#6600CC","#6600FF","#6633CC","#6633FF","#66CC00","#66CC33","#9900CC","#9900FF","#9933CC","#9933FF","#99CC00","#99CC33","#CC0000","#CC0033","#CC0066","#CC0099","#CC00CC","#CC00FF","#CC3300","#CC3333","#CC3366","#CC3399","#CC33CC","#CC33FF","#CC6600","#CC6633","#CC9900","#CC9933","#CCCC00","#CCCC33","#FF0000","#FF0033","#FF0066","#FF0099","#FF00CC","#FF00FF","#FF3300","#FF3333","#FF3366","#FF3399","#FF33CC","#FF33FF","#FF6600","#FF6633","#FF9900","#FF9933","#FFCC00","#FFCC33"],n.log=console.debug||console.log||(()=>{}),e.exports=t(7448)(n);const{formatters:o}=e.exports;o.j=function(e){try{return JSON.stringify(e)}catch(e){return"[UnexpectedJSONParseError]: "+e.message}}},2494:(e,n,t)=>{"use strict";t.d(n,{A:()=>r});var o=t(5932),s=t(7143),c=t(7938),i=t(2279);const r={FETCH_AUTHORIZATION_URL:({redirectUri:e})=>o.Ay.fetchAuthorizationUrl(e),REGISTER_SITE:({redirectUri:e,from:n})=>o.Ay.registerSite(null,e,n),CONNECT_USER:(0,s.createRegistryControl)(({resolveSelect:e})=>({from:n,redirectFunc:t,redirectUri:o,skipPricingPage:s}={})=>new Promise((r,a)=>{e(i.A).getAuthorizationUrl(o).then(e=>{const o=t||(e=>(0,c.d)(e)),i=new URL(e);s&&i.searchParams.set("skip_pricing","true"),n&&i.searchParams.set("from",encodeURIComponent(n));const a=i.toString();o(a),r(a)}).catch(e=>{a(e)})}))}},2558:(e,n,t)=>{"use strict";t.d(n,{A:()=>y});var o=t(5932),s=t(7999),c=t(7143),i=t(4804),r=t.n(i),a=t(1609),l=t(3765),d=t(9660),p=t(4293);const u=r()("jetpack:connection:useProductCheckoutWorkflow"),{registrationNonce:m,apiRoot:g,apiNonce:h,siteSuffix:f}=window?.JP_CONNECTION_INITIAL_STATE||(0,s.getScriptData)()?.connection||{},_=()=>"undefined"!=typeof window?window?.myJetpackInitialState?.adminUrl:null;function y({productSlug:e,redirectUrl:n,siteSuffix:t=f,adminUrl:s=_(),connectAfterCheckout:i=!1,siteProductAvailabilityHandler:r=null,quantity:y=null,from:b,useBlogIdSuffix:j=!1}={}){u("productSlug is %s",e),u("redirectUrl is %s",n),u("siteSuffix is %s",t),u("from is %s",b);const[k,C]=(0,a.useState)(!1),{registerSite:v}=(0,c.useDispatch)(p.a),w=(0,c.useSelect)(e=>e(p.a).getBlogId(),[]);u("blogID is %s",w??"undefined"),j=j&&!!w;const{isUserConnected:x,isRegistered:A,handleConnectUser:N}=(0,d.A)({redirectUri:n,from:b}),S=(0,a.useMemo)(()=>{const o=(0,l.A)(),c=(!A||!x)&&i,r=c?"checkout/jetpack/":`checkout/${j?w.toString():t}/`,a=new URL(`${o}${r}${e}${null!=y?`:-q-${y}`:""}`);return c?(a.searchParams.set("connect_after_checkout",!0),a.searchParams.set("admin_url",s),a.searchParams.set("from_site_slug",t)):a.searchParams.set("site",t),a.searchParams.set("source",b),a.searchParams.set("redirect_to",n),x||a.searchParams.set("unlinked","1"),a},[A,x,i,t,y,e,b,n,s,j,w]);u("isRegistered is %s",A),u("isUserConnected is %s",x),u("connectAfterCheckout is %s",i),u("checkoutUrl is %s",S);const E=(e=null)=>Promise.resolve(r&&r()).then(n=>{if(e&&S.searchParams.set("redirect_to",e),n)return u("handleAfterRegistration: Site has a product associated"),N();u("handleAfterRegistration: Site does not have a product associated. Redirecting to checkout %s",S),window.location.href=S});return(0,a.useEffect)(()=>{o.Ay.setApiRoot(g),o.Ay.setApiNonce(h)},[]),{run:(e,t=null)=>(e&&e.preventDefault(),C(!0),i?((e=null)=>{e&&S.searchParams.set("redirect_to",e),u("Redirecting to connectAfterCheckout flow: %s",S),window.location.href=S})(t):A?E(t):void v({registrationNonce:m,redirectUri:n}).then(()=>E(t))),isRegistered:A,hasCheckoutStarted:k}}},2668:(e,n,t)=>{"use strict";t.d(n,{A:()=>r});var o=t(7142),s=t(2231),c=t(5745),i=(t(1133),t(790));const r=({title:e,children:n,className:t,assetBaseUrl:r,images:a,logo:l,rna:d=!1})=>{const p=a?.length;return(0,i.jsxs)("div",{className:(0,s.A)("jp-connection__connect-screen-layout",p?"jp-connection__connect-screen-layout__two-columns":"",t?" "+t:""),children:[d&&(0,i.jsxs)("div",{className:"jp-connection__connect-screen-layout__color-blobs",children:[(0,i.jsx)("div",{className:"jp-connection__connect-screen-layout__color-blobs__green"}),(0,i.jsx)("div",{className:"jp-connection__connect-screen-layout__color-blobs__yellow"}),(0,i.jsx)("div",{className:"jp-connection__connect-screen-layout__color-blobs__blue"})]}),(0,i.jsxs)("div",{className:"jp-connection__connect-screen-layout__left",children:[l||(0,i.jsx)(o.A,{}),(0,i.jsx)("h2",{children:e}),n]}),p?(0,i.jsx)("div",{className:"jp-connection__connect-screen-layout__right",children:(0,i.jsx)(c.A,{images:a,assetBaseUrl:r})}):null]})}},2676:(e,n,t)=>{"use strict";t.d(n,{A:()=>o});const o={...{getConnectionStatus:e=>e.connectionStatus||{},getConnectionStatusIsFetching:()=>!1,getSiteIsRegistering:e=>e.siteIsRegistering||!1,getUserIsConnecting:e=>e.userIsConnecting||!1,getRegistrationError:e=>e.registrationError||!1,getAuthorizationUrl:e=>e.authorizationUrl||!1,getUserConnectionData:e=>e.userConnectionData||!1,getConnectedPlugins:e=>e.connectedPlugins||[],getConnectionErrors:e=>e.connectionErrors||[],getIsOfflineMode:e=>e.isOfflineMode||!1,getWpcomUser:e=>e?.userConnectionData?.currentUser?.wpcomUser,getBlogId:e=>e?.userConnectionData?.currentUser?.blogId}}},2951:(e,n,t)=>{"use strict";t.d(n,{A:()=>p});var o=t(6427),s=t(7723),c=t(3619),i=t.n(c),r=t(1609),a=t(8233),l=t(790);const __=s.__,d=e=>{const{onSubmit:n,isSubmittingFeedback:t}=e,[s,c]=(0,r.useState)(),[i,d]=(0,r.useState)(),p=[{id:"troubleshooting",answerText:__("Troubleshooting - I'll be reconnecting afterwards.","jetpack-connection")},{id:"not-working",answerText:__("I can't get it to work.","jetpack-connection")},{id:"slowed-down-site",answerText:__("It slowed down my site.","jetpack-connection")},{id:"buggy",answerText:__("It's buggy.","jetpack-connection")},{id:"what-does-it-do",answerText:__("I don't know what it does.","jetpack-connection")}],u="another-reason",m=(0,r.useCallback)(()=>{n(s,s===u?i:"")},[n,u,i,s]),g=(0,r.useCallback)(e=>{const n=e.target.value;e.stopPropagation(),d(n)},[d]),h=e=>e===s?"jp-connect__disconnect-survey-card--selected":"",f=(0,r.useCallback)((e,n)=>{switch(n.key){case"Enter":case"Space":case"Spacebar":case" ":c(e)}},[c]);return(0,l.jsxs)(r.Fragment,{children:[(0,l.jsxs)("div",{className:"jp-connection__disconnect-dialog__survey",children:[p.map(e=>(0,l.jsx)(a.A,{id:e.id,onClick:c,onKeyDown:f,className:"card jp-connect__disconnect-survey-card "+h(e.id),children:(0,l.jsx)("p",{className:"jp-connect__disconnect-survey-card__answer",children:e.answerText})},e.id)),(0,l.jsx)(a.A,{id:u,onClick:c,onKeyDown:f,className:"card jp-connect__disconnect-survey-card "+h(u),children:(0,l.jsxs)("p",{className:"jp-connect__disconnect-survey-card__answer",children:[__("Other:","jetpack-connection")," ",(0,l.jsx)("input",{placeholder:__("share your experience","jetpack-connection"),className:"jp-connect__disconnect-survey-card__input",type:"text",value:i,onChange:g,maxLength:1e3})]})},u)]}),(0,l.jsx)("p",{children:(0,l.jsx)(o.Button,{disabled:!s||t,variant:"primary",onClick:m,className:"jp-connection__disconnect-dialog__btn-back-to-wp",children:t?__("Submitting…","jetpack-connection"):__("Submit Feedback","jetpack-connection",0)})})]})};d.PropTypes={onSubmit:i().func,isSubmittingFeedback:i().bool};const p=d},2990:()=>{},3269:(e,n,t)=>{"use strict";t.d(n,{A:()=>_});var o=t(372),s=t(5932),c=t(6439),i=t(6427),r=t(7723),a=t(3619),l=t.n(a),d=t(1609),p=(t(3524),t(4472)),u=t(8503),m=t(412),g=t(8090),h=t(790);const __=r.__,f=e=>{const[n,t]=(0,d.useState)(!1),[r,a]=(0,d.useState)(!1),[l,f]=(0,d.useState)(!1),[_,y]=(0,d.useState)(!1),[b,j]=(0,d.useState)(!1),[k,C]=(0,d.useState)(!1),{apiRoot:v,apiNonce:w,connectedPlugins:x,title:A=__("Are you sure you want to disconnect?","jetpack-connection"),pluginScreenDisconnectCallback:N,onDisconnected:S,onError:E,disconnectStepComponent:F,context:T="jetpack-dashboard",connectedUser:O={},connectedSiteId:L,isOpen:I,onClose:R}=e;let P="";(0,c.jetpackConfigHas)("consumer_slug")&&(P=(0,c.jetpackConfigGet)("consumer_slug"));const D=(0,d.useMemo)(()=>({context:T,plugin:P}),[T,P]);(0,d.useEffect)(()=>{s.Ay.setApiRoot(v),s.Ay.setApiNonce(w)},[v,w]),(0,d.useEffect)(()=>{O&&O.ID&&O.login&&o.A.initialize(O.ID,O.login)},[O,O.ID,O.login]),(0,d.useEffect)(()=>{I&&o.A.tracks.recordEvent("jetpack_disconnect_dialog_open",D)},[I,D]),(0,d.useEffect)(()=>{I&&(r?!r||_||b?_&&!b?o.A.tracks.recordEvent("jetpack_disconnect_dialog_step",Object.assign({},{step:"survey"},D)):b&&o.A.tracks.recordEvent("jetpack_disconnect_dialog_step",Object.assign({},{step:"thank_you"},D)):o.A.tracks.recordEvent("jetpack_disconnect_dialog_step",Object.assign({},{step:"disconnect_confirm"},D)):o.A.tracks.recordEvent("jetpack_disconnect_dialog_step",Object.assign({},{step:"disconnect"},D)))},[I,r,_,b,D]);const U=(0,d.useCallback)(()=>{s.Ay.disconnectSite().then(()=>{t(!1),a(!0)}).catch(e=>{t(!1),f(e),E&&E(e)})},[t,a,f,E]),B=(0,d.useCallback)((e,n)=>{C(!0),fetch("https://public-api.wordpress.com/wpcom/v2/marketing/feedback-survey",{method:"POST",headers:{"Content-Type":"application/json",Accept:"application/json"},body:JSON.stringify(e)}).then(e=>e.json()).then(e=>{if(!0!==e.success)throw new Error("Survey endpoint returned error code "+e.code);o.A.tracks.recordEvent("jetpack_disconnect_survey_submit",n),j(!0),C(!1)}).catch(e=>{o.A.tracks.recordEvent("jetpack_disconnect_survey_error",Object.assign({},{error:e.message},n)),j(!0),C(!1)})},[C,j]),M=(0,d.useCallback)(e=>{e&&e.preventDefault(),f(!1),t(!0),"plugins"!==T?U():N&&N(e)},[f,t,N,T,U]),$=(0,d.useCallback)(e=>o.A.tracks.recordEvent(e,D),[D]),J=(0,d.useCallback)(()=>!(!O.ID||!L),[O,L]),z=(0,d.useCallback)((e,n,t)=>{if(t&&t.preventDefault(),!J())return void j(!0);const o={site_id:L,user_id:O.ID,survey_id:"jetpack-plugin-disconnect",survey_responses:{"why-cancel":{response:e,text:n||null}}},s=Object.assign({},D,{disconnect_reason:e});B(o,s)},[B,j,J,L,O,D]),q=(0,d.useCallback)(e=>{e&&e.preventDefault(),S&&S(),R()},[S,R]),G=(0,d.useCallback)(e=>{e&&e.preventDefault(),y(!0)},[y]);return(0,h.jsx)(h.Fragment,{children:I&&(0,h.jsx)(i.Modal,{title:"",contentLabel:A,aria:{labelledby:"jp-connection__disconnect-dialog__heading"},onRequestClose:R,shouldCloseOnClickOutside:!1,shouldCloseOnEsc:!1,isDismissible:!1,className:"jp-connection__disconnect-dialog"+(r?" jp-connection__disconnect-dialog__success":""),children:r?!r||_||b?_&&!b?(0,h.jsx)(m.A,{isSubmittingFeedback:k,onFeedBackProvided:z,onExit:q}):b?(0,h.jsx)(g.A,{onExit:q}):void 0:(0,h.jsx)(u.A,{canProvideFeedback:J(),onProvideFeedback:G,onExit:q}):(0,h.jsx)(p.A,{title:A,connectedPlugins:x,disconnectStepComponent:F,isDisconnecting:n,closeModal:R,onDisconnect:M,disconnectError:l,context:T,disconnectingPlugin:P,trackModalClick:$})})})};f.propTypes={apiRoot:l().string.isRequired,apiNonce:l().string.isRequired,title:l().string,onDisconnected:l().func,onError:l().func,context:l().string,connectedPlugins:l().oneOfType([l().array,l().object]),pluginScreenDisconnectCallback:l().func,disconnectStepComponent:l().element,connectedUser:l().object,connectedSiteId:l().number,isOpen:l().bool,onClose:l().func};const _=f},3328:(e,n,t)=>{"use strict";t.d(n,{J:()=>a});var o=t(2423),s=t.n(o),c=t(3673);const i=s()("number-formatters:get-cached-formatter"),r=new Map;function a({locale:e,fallbackLocale:n=c.M,options:t,retries:o=1}){const s=JSON.stringify([e,t]);try{return r.get(s)??r.set(s,new Intl.NumberFormat(e,t)).get(s)}catch(s){if(i(`Intl.NumberFormat was called with a non-existent locale "${e}"; falling back to ${n}`),o)return a({locale:n,options:t,retries:o-1});throw s}}},3370:(e,n,t)=>{"use strict";t.d(n,{A:()=>o});const o={reset:"WQVtrU6q0L1Igcj7wCrQ","headline-medium":"UujoBFTnQNY2cWU2SIsH","headline-small":"TeGO5V_thHw5lDAm1_2M","headline-small-regular":"WolQzb2MsSgiNmLtc7_j","title-medium":"hUB0JT8p1T2Hw28N6qC8","title-medium-semi-bold":"gKZWDv5chz3_O3Syp74H","title-small":"zY2No8Ga4b8shbOQGhnv",body:"tIj0D1t8Cc892ikmgFPZ","body-small":"KdcN0BnOaVeVhyLRKqhS","body-extra-small":"dso3Rh3tl3Xv1GumBktz","body-extra-small-bold":"mQ1UlbN9u4Mg9byO8m7v",label:"PItlW5vRExLnTj4a8eLE","m-0":"TwRpPlktzxhmFVeua7P5","mx-0":"zVfqx7gyb3o9mxfGynn1","my-0":"iSHVzNiB9iVleGljaQxy","mt-0":"xqDIp6cNVr_E6RXaiPyD","mr-0":"S8EwaXk1kyPizt6x4WH2","mb-0":"ODX5Vr1TARoLFkDDFooD","ml-0":"cphJ8dCpfimnky7P2FHg","m-1":"PFgIhNxIyiSuNvQjAIYj","mx-1":"M2jKmUzDxvJjjVEPU3zn","my-1":"io15gAh8tMTNbSEfwJKk","mt-1":"rcTN5uw9xIEeMEGL3Xi_","mr-1":"CQSkybjq2TcRM1Xo9COV","mb-1":"hfqOWgq6_MEGdFE82eOY","ml-1":"I8MxZQYTbuu595yfesWA","m-2":"kQkc6rmdpvLKPkyoJtVQ","mx-2":"j6vFPxWuu4Jan2ldoxpp","my-2":"hqr39dC4H_AbactPAkCG","mt-2":"c3dQnMi16C6J6Ecy4283","mr-2":"YNZmHOuRo6hU7zzKfPdP","mb-2":"Db8lbak1_wunpPk8NwKU","ml-2":"ftsYE5J9hLzquQ0tA5dY","m-3":"Det4MHzLUW7EeDnafPzq","mx-3":"h_8EEAztC29Vve1datb5","my-3":"YXIXJ0h1k47u6hzK8KcM","mt-3":"soADBBkcIKCBXzCTuV9_","mr-3":"zSX59ziEaEWGjnpZa4uV","mb-3":"yrVTnq_WBMbejg89c2ZQ","ml-3":"UKtHPJnI2cXBWtPDm5hM","m-4":"guexok_Tqd5Tf52hRlbT","mx-4":"oS1E2KfTBZkJ3F0tN7T6","my-4":"DN1OhhXi6AoBgEdDSbGd","mt-4":"ot2kkMcYHv53hLZ4LSn0","mr-4":"A1krOZZhlQ6Sp8Cy4bly","mb-4":"pkDbXXXL32237M0hokEh","ml-4":"XXv4kDTGvEnQeuGKOPU3","m-5":"yGqHk1a57gaISwkXwXe6","mx-5":"X8cghM358X3DkXLc9aNK","my-5":"GdfSmGwHlFnN2S6xBn1f","mt-5":"yqeuzwyGQ7zG0avrGqi_","mr-5":"g9emeCkuHvYhveiJbfXO","mb-5":"Lvk3dqcyHbZ07QCRlrUQ","ml-5":"r3yQECDQ9qX0XZzXlVAg","m-6":"aQhlPwht2Cz1X_63Miw0","mx-6":"JyHb0vK3wJgpblL9s5j8","my-6":"cY2gULL1lAv6WPNIRuf3","mt-6":"NBWQ9Lwhh_fnry3lg_p7","mr-6":"yIOniNe5E40C8fWvBm5V","mb-6":"t30usboNSyqfQWIwHvT3","ml-6":"Nm_TyFkYCMhOoghoToKJ","m-7":"C4qJKoBXpgKtpmrqtEKB","mx-7":"S93Srbu6NQ_PBr7DmTiD","my-7":"fJj8k6gGJDks3crUZxOS","mt-7":"cW6D6djs7Ppm7fD7TeoV","mr-7":"DuCnqNfcxcP3Z__Yo5Ro","mb-7":"im8407m2fw5vOg7O2zsw","ml-7":"G0fbeBgvz2sh3uTP9gNl","m-8":"kvW3sBCxRxUqz1jrVMJl","mx-8":"tOjEqjLONQdkiYx_XRnw","my-8":"op5hFSx318zgxsoZZNLN","mt-8":"c9WfNHP6TFKWIfLxv52J","mr-8":"sBA75QqcqRwwYSHJh2wc","mb-8":"GpL6idrXmSOM6jB8Ohsf","ml-8":"HbtWJoQwpgGycz8dGzeT","p-0":"uxX3khU88VQ_Ah49Ejsa","px-0":"KX0FhpBKwKzs9fOUdbNz","py-0":"PfK8vKDyN32dnimlzYjz","pt-0":"emxLHRjQuJsImnPbQIzE","pr-0":"kJ8WzlpTVgdViXt8ukP9","pb-0":"tg_UIUI11VBzrTAn2AzJ","pl-0":"uczvl8kaz84oPQJ2DB2R","p-1":"o7UHPcdVK3lt7q3lqV4o","px-1":"IDqEOxvDoYrFYxELPmtX","py-1":"DdywPW2qSYlu2pt8tpO2","pt-1":"npy3hw4A5QSkDicb2CJJ","pr-1":"LgbptTApNY5NwLQvEFAt","pb-1":"WZQy2SZuZso59bUsXXyl","pl-1":"o331apInxNunbYB3SfPE","p-2":"fMPIyD9Vqki1Lrc_yJnG","px-2":"i2pMcTcdrr10IQoiSm_L","py-2":"eA702gn32kwptiI1obXH","pt-2":"o9bGieUKcYc8o0Ij9oZX","pr-2":"SwZcFez1RDqWsOFjB5iG","pb-2":"eHpLc_idmuEqeqCTvqkN","pl-2":"vU39i2B4P1fUTMB2l6Vo","p-3":"JHWNzBnE29awhdu5BEh1","px-3":"X72lGbb56L3KFzC2xQ9N","py-3":"BzfNhRG8wXdCEB5ocQ6e","pt-3":"srV0KSDC83a2fiimSMMQ","pr-3":"lUWfkmbQjCskhcNwkyCm","pb-3":"Ts0dIlc3aTSL7V4cIHis","pl-3":"CzlqQXXhX6MvorArFZ8B","p-4":"TqMPkQtR_DdZuKb5vBoV","px-4":"a7UrjhI69Vetlcj9ZVzz","py-4":"StEhBzGs2Gi5dDEkjhAv","pt-4":"FGneZfZyvYrt1dG0zcnm","pr-4":"APEH216rpdlJWgD2fHc8","pb-4":"oGwXC3ohCic9XnAj6x69","pl-4":"U6gnT9y42ViPNOcNzBwb","p-5":"IpdRLBwnHqbqFrixgbYC","px-5":"HgNeXvkBa9o3bQ5fvFZm","py-5":"tJtFZM3XfPG9v9TSDfN1","pt-5":"PdifHW45QeXYfK568uD8","pr-5":"mbLkWTTZ0Za_BBbFZ5b2","pb-5":"vVWpZpLlWrkTt0hMk8XU","pl-5":"RxfaJj5a1Nt6IavEo5Zl","p-6":"SppJULDGdnOGcjZNCYBy","px-6":"palY2nLwdoyooPUm9Hhk","py-6":"WYw1JvZC0ppLdvSAPhr_","pt-6":"YEEJ9b90ueQaPfiU8aeN","pr-6":"QE0ssnsKvWJMqlhPbY5u","pb-6":"n8yA3jHlMRyLd5UIfoND","pl-6":"tXHmxYnHzbwtfxEaG51n","p-7":"kBTsPKkO_3g_tLkj77Um","px-7":"RyhrFx6Y1FGDrGAAyaxm","py-7":"CBwRpB0bDN3iEdQPPMJO","pt-7":"vQVSq6SvWKbOMu6r4H6b","pr-7":"oBy5__aEADMsH46mrgFX","pb-7":"KVEXoJqf1s92j0JMdNmN","pl-7":"ZMXGNrNaKW3k_3TLz0Fq","p-8":"tuiR9PhkHXhGyEgzRZRI","px-8":"U7454qyWkQNa2iaSJziu","py-8":"VLYIv2GVocjuN93e8HC8","pt-8":"X1rm9DQ1zLGLfogja5Gn","pr-8":"JS7G6kAuqJo5GIuF8S5t","pb-8":"Y8F9ga1TDCMbM1lj4gUz","pl-8":"AJuyNGrI63BOWql719H8"}},3524:()=>{},3594:e=>{var n=1e3,t=60*n,o=60*t,s=24*o,c=7*s,i=365.25*s;function r(e,n,t,o){var s=n>=1.5*t;return Math.round(e/t)+" "+o+(s?"s":"")}e.exports=function(e,a){a=a||{};var l=typeof e;if("string"===l&&e.length>0)return function(e){if((e=String(e)).length>100)return;var r=/^(-?(?:\d+)?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)?$/i.exec(e);if(!r)return;var a=parseFloat(r[1]);switch((r[2]||"ms").toLowerCase()){case"years":case"year":case"yrs":case"yr":case"y":return a*i;case"weeks":case"week":case"w":return a*c;case"days":case"day":case"d":return a*s;case"hours":case"hour":case"hrs":case"hr":case"h":return a*o;case"minutes":case"minute":case"mins":case"min":case"m":return a*t;case"seconds":case"second":case"secs":case"sec":case"s":return a*n;case"milliseconds":case"millisecond":case"msecs":case"msec":case"ms":return a;default:return}}(e);if("number"===l&&isFinite(e))return a.long?function(e){var c=Math.abs(e);if(c>=s)return r(e,c,s,"day");if(c>=o)return r(e,c,o,"hour");if(c>=t)return r(e,c,t,"minute");if(c>=n)return r(e,c,n,"second");return e+" ms"}(e):function(e){var c=Math.abs(e);if(c>=s)return Math.round(e/s)+"d";if(c>=o)return Math.round(e/o)+"h";if(c>=t)return Math.round(e/t)+"m";if(c>=n)return Math.round(e/n)+"s";return e+"ms"}(e);throw new Error("val is not a non-empty string or a valid number. val="+JSON.stringify(e))}},3619:(e,n,t)=>{e.exports=t(1583)()},3673:(e,n,t)=>{"use strict";t.d(n,{$:()=>s,M:()=>o});const o="en",s="USD"},3735:(e,n,t)=>{"use strict";t.d(n,{A:()=>b});var o=t(372),s=t(5932),c=t(3924),i=t(6427),r=t(6087),a=t(7723),l=t(7750),d=t(1386),p=t(8391),u=t(2231),m=t(3619),g=t.n(m),h=t(1609),f=(t(7202),t(790));const __=a.__,_=({isOpen:e,onClose:n,apiRoot:t,apiNonce:a,onDisconnected:l,onUnlinked:d})=>{const[p,u]=(0,h.useState)(!1),[m,g]=(0,h.useState)(""),_=__("Disconnecting…","jetpack-connection"),b=__("Disconnect","jetpack-connection");(0,h.useEffect)(()=>{s.Ay.setApiRoot(t),s.Ay.setApiNonce(a)},[t,a]);const j=(0,h.useCallback)(()=>{n()},[n]),k=(0,h.useCallback)(()=>{o.A.tracks.recordEvent("jetpack_manage_connection_dialog_owner_disconnect_click"),u(!0),g(""),s.Ay.unlinkUser(!0,{disconnectAllUsers:!0}).then(()=>{o.A.tracks.recordEvent("jetpack_manage_connection_dialog_owner_disconnect_success"),l&&l(),d&&d()}).catch(()=>{o.A.tracks.recordEvent("jetpack_manage_connection_dialog_owner_disconnect_error"),g(__("There was a problem disconnecting your account. Please try again.","jetpack-connection")),u(!1)})},[l,d]);return e&&(0,f.jsxs)(i.Modal,{title:"",contentLabel:__("Disconnect Owner Account","jetpack-connection"),aria:{labelledby:"jp-connection__disconnect-dialog__heading"},onRequestClose:j,className:"jp-connection__disconnect-dialog",children:[(0,f.jsxs)("div",{className:"jp-connection__disconnect-dialog__content",children:[(0,f.jsx)("h1",{id:"jp-connection__disconnect-dialog__heading",children:__("Disconnect Owner Account","jetpack-connection")}),(0,f.jsx)("p",{className:"jp-connection__disconnect-dialog__large-text",children:__("Disconnecting the owner account will remove the Jetpack connection for all users on this site. The site will remain connected.","jetpack-connection")}),(0,f.jsx)(y,{title:__("Transfer ownership to another admin","jetpack-connection"),link:(0,c.A)("calypso-settings-manage-connection",{site:window?.myJetpackInitialState?.siteSuffix}),isExternal:!0,action:"transfer"}),(0,f.jsx)(y,{title:__("View other connected accounts","jetpack-connection"),link:"users.php",action:"check-users"})]}),(0,f.jsxs)("div",{className:"jp-connection__disconnect-dialog__actions",children:[(0,f.jsxs)("div",{className:"jp-row",children:[(0,f.jsx)("div",{className:"lg-col-span-8 md-col-span-9 sm-col-span-4",children:(0,f.jsx)("p",{children:(0,r.createInterpolateElement)(__("<strong>Need help?</strong> Learn more about the <connectionInfoLink>Jetpack connection</connectionInfoLink> or <supportLink>contact Jetpack support</supportLink>","jetpack-connection"),{strong:(0,f.jsx)("strong",{}),connectionInfoLink:(0,f.jsx)(i.ExternalLink,{href:(0,c.A)("why-the-wordpress-com-connection-is-important-for-jetpack"),className:"jp-connection__disconnect-dialog__link"}),supportLink:(0,f.jsx)(i.ExternalLink,{href:(0,c.A)("jetpack-support"),className:"jp-connection__disconnect-dialog__link"})})})}),(0,f.jsxs)("div",{className:"jp-connection__disconnect-dialog__button-wrap lg-col-span-4 md-col-span-7 sm-col-span-4",children:[(0,f.jsx)(i.Button,{variant:"primary",onClick:j,className:"jp-connection__disconnect-dialog__btn-dismiss",children:__("Stay Connected","jetpack-connection")}),(0,f.jsx)(i.Button,{variant:"primary",onClick:k,className:"jp-connection__disconnect-dialog__btn-disconnect",isDestructive:!0,disabled:p,children:p?_:b})]})]}),m&&(0,f.jsx)("p",{className:"jp-connection__disconnect-dialog__error",children:m})]})]})};_.propTypes={isOpen:g().bool,onClose:g().func,apiRoot:g().string.isRequired,apiNonce:g().string.isRequired,onDisconnected:g().func,onUnlinked:g().func};const y=({title:e,onClick:n=()=>null,isExternal:t=!1,link:o="#",action:s,disabled:c=!1})=>{const i=(0,h.useCallback)(e=>e.preventDefault(),[]);return(0,f.jsx)("div",{className:"jp-connection__manage-dialog__action-card card"+(c?" disabled":""),children:(0,f.jsx)("div",{className:"jp-connection__manage-dialog__action-card__card-content",children:(0,f.jsxs)("a",{href:o,className:(0,u.A)("jp-connection__manage-dialog__action-card__card-headline",s),onClick:c?i:n,target:t?"_blank":"_self",rel:"noopener noreferrer",children:[e,(0,f.jsx)(l.A,{icon:t?p.A:d.A,className:"jp-connection__manage-dialog__action-card__icon"})]})})})},b=_},3765:(e,n,t)=>{"use strict";t.d(n,{A:()=>s});var o=t(7999);function s(){const e=("undefined"!=typeof window&&window?.JP_CONNECTION_INITIAL_STATE||(0,o.getScriptData)()?.connection)?.calypsoEnv;switch(e){case"development":return"http://calypso.localhost:3000/";case"wpcalypso":return"https://wpcalypso.wordpress.com/";case"horizon":return"https://horizon.wordpress.com/";default:return"https://wordpress.com/"}}},3832:e=>{"use strict";e.exports=window.wp.url},3924:(e,n,t)=>{"use strict";function o(e,n={}){const t={};let o;if("undefined"!=typeof window&&(o=window?.JP_CONNECTION_INITIAL_STATE?.calypsoEnv),0===e.search("https://")){const n=new URL(e);e=`https://${n.host}${n.pathname}`,t.url=encodeURIComponent(e)}else t.source=encodeURIComponent(e);for(const e in n)t[e]=encodeURIComponent(n[e]);!Object.keys(t).includes("site")&&"undefined"!=typeof jetpack_redirects&&Object.hasOwn(jetpack_redirects,"currentSiteRawUrl")&&(t.site=jetpack_redirects.currentBlogID??jetpack_redirects.currentSiteRawUrl),o&&(t.calypso_env=o);return"https://jetpack.com/redirect/?"+Object.keys(t).map(e=>e+"="+t[e]).join("&")}t.d(n,{A:()=>o})},3935:(e,n,t)=>{"use strict";t.d(n,{A1:()=>a,Ay:()=>C,DO:()=>r,Ij:()=>i,Kl:()=>m,LW:()=>l,MU:()=>g,XY:()=>d,ZO:()=>c,dz:()=>p,gH:()=>u,v_:()=>s});var o=t(5932);const s="SET_CONNECTION_STATUS",c="SET_CONNECTION_STATUS_IS_FETCHING",i="SET_SITE_IS_REGISTERING",r="SET_USER_IS_CONNECTING",a="SET_REGISTRATION_ERROR",l="CLEAR_REGISTRATION_ERROR",d="SET_AUTHORIZATION_URL",p="DISCONNECT_USER_SUCCESS",u="SET_CONNECTED_PLUGINS",m="SET_CONNECTION_ERRORS",g="SET_IS_OFFLINE_MODE",h=e=>({type:s,connectionStatus:e}),f=e=>({type:i,isRegistering:e}),_=e=>({type:r,isConnecting:e}),y=e=>({type:a,registrationError:e}),b=()=>({type:l}),j=e=>({type:d,authorizationUrl:e}),k=e=>({type:u,connectedPlugins:e});const C={setConnectionStatus:h,setConnectionStatusIsFetching:e=>({type:c,isFetching:e}),fetchConnectionStatus:()=>({type:"FETCH_CONNECTION_STATUS"}),fetchAuthorizationUrl:e=>({type:"FETCH_AUTHORIZATION_URL",redirectUri:e}),setSiteIsRegistering:f,setUserIsConnecting:_,setRegistrationError:y,clearRegistrationError:b,setAuthorizationUrl:j,registerSite:function*({registrationNonce:e,redirectUri:n,from:t=""}){yield b(),yield f(!0);try{const o=yield{type:"REGISTER_SITE",registrationNonce:e,redirectUri:n,from:t};return yield h({isRegistered:!0}),yield j(o.authorizeUrl),yield f(!1),Promise.resolve(o)}catch(e){return yield y(e),yield f(!1),Promise.reject(e)}},connectUser:function*({from:e,redirectFunc:n,redirectUri:t,skipPricingPage:o}={}){yield _(!0),yield{type:"CONNECT_USER",from:e,redirectFunc:n,redirectUri:t,skipPricingPage:o}},disconnectUserSuccess:()=>({type:p}),setConnectedPlugins:k,refreshConnectedPlugins:()=>async({dispatch:e})=>await new Promise(n=>o.Ay.fetchConnectedPlugins().then(t=>{e(k(t)),n(t)})),setConnectionErrors:e=>({type:m,connectionErrors:e}),setIsOfflineMode:e=>({type:g,isOfflineMode:e})}},4046:()=>{},4099:()=>{},4268:(e,n,t)=>{"use strict";t.d(n,{vA:()=>l});const o=(0,t(1452).A)(),{setLocale:s,setGeoLocation:c,formatNumber:i,formatNumberCompact:r,formatCurrency:a,getCurrencyObject:l}=o},4293:(e,n,t)=>{"use strict";t.d(n,{a:()=>d.A});var o=t(7999),s=t(3935),c=t(2494),i=t(5051),r=t(8019),a=t(2676),l=t(8734),d=t(2279);const p=window.JP_CONNECTION_INITIAL_STATE||(0,o.getScriptData)()?.connection;p||console.error("Jetpack Connection package: Initial state is missing. Check documentation to see how to use the Connection composer package to set up the initial state."),l.A.mayBeInit(d.A,{__experimentalUseThunks:!0,reducer:i.A,actions:s.Ay,selectors:a.A,resolvers:r.A,controls:c.A,initialState:p||{}})},4317:()=>{},4472:(e,n,t)=>{"use strict";t.d(n,{A:()=>m});var o=t(3924),s=t(6427),c=t(6087),i=t(7723),r=t(3619),a=t.n(r),l=t(1609),d=t(648),p=t(790);const __=i.__,u=e=>{const{title:n,isDisconnecting:t,onDisconnect:i,disconnectError:r,disconnectStepComponent:a,connectedPlugins:u,disconnectingPlugin:m,closeModal:g,context:h,trackModalClick:f}=e,_=(0,l.useCallback)(()=>f("jetpack_disconnect_dialog_click_learn_about"),[f]),y=(0,l.useCallback)(()=>f("jetpack_disconnect_dialog_click_support"),[f]),b=(0,l.useCallback)(()=>{f("jetpack_disconnect_dialog_click_stay_connected"),g()},[f,g]),j=(0,l.useCallback)(e=>{f("jetpack_disconnect_dialog_click_disconnect"),i(e)},[f,i]),k=(0,l.useCallback)(e=>{"Escape"!==e.key||t||b()},[b,t]);(0,l.useEffect)(()=>(document.addEventListener("keydown",k,!1),()=>{document.removeEventListener("keydown",k,!1)}),[]);return(0,p.jsxs)(l.Fragment,{children:[(0,p.jsxs)("div",{className:"jp-connection__disconnect-dialog__content",children:[(0,p.jsx)("h1",{id:"jp-connection__disconnect-dialog__heading",children:n}),(0,p.jsx)(d.A,{connectedPlugins:u,disconnectingPlugin:m}),a,(()=>{if(!(u&&Object.keys(u).filter(e=>e!==m).length)&&!a)return(0,p.jsx)("div",{className:"jp-connection__disconnect-dialog__step-copy",children:(0,p.jsxs)("p",{className:"jp-connection__disconnect-dialog__large-text",children:[__("Jetpack is currently powering multiple products on your site.","jetpack-connection"),(0,p.jsx)("br",{}),__("Once you disconnect Jetpack, these will no longer work.","jetpack-connection")]})})})()]}),(0,p.jsxs)("div",{className:"jp-connection__disconnect-dialog__actions",children:[(0,p.jsxs)("div",{className:"jp-row",children:[(0,p.jsx)("div",{className:"lg-col-span-8 md-col-span-9 sm-col-span-4",children:(0,p.jsx)("p",{children:(0,c.createInterpolateElement)(__("<strong>Need help?</strong> Learn more about the <jpConnectionInfoLink>Jetpack connection</jpConnectionInfoLink> or <jpSupportLink>contact Jetpack support</jpSupportLink>.","jetpack-connection"),{strong:(0,p.jsx)("strong",{}),jpConnectionInfoLink:(0,p.jsx)(s.ExternalLink,{href:(0,o.A)("why-the-wordpress-com-connection-is-important-for-jetpack"),className:"jp-connection__disconnect-dialog__link",onClick:_}),jpSupportLink:(0,p.jsx)(s.ExternalLink,{href:(0,o.A)("jetpack-support"),className:"jp-connection__disconnect-dialog__link",onClick:y})})})}),(0,p.jsxs)("div",{className:"jp-connection__disconnect-dialog__button-wrap lg-col-span-4 md-col-span-7 sm-col-span-4",children:[(0,p.jsx)(s.Button,{variant:"primary",disabled:t,onClick:b,className:"jp-connection__disconnect-dialog__btn-dismiss",children:"plugins"===h?__("Cancel","jetpack-connection"):__("Stay connected","jetpack-connection",0)}),(()=>{let e=__("Disconnect","jetpack-connection");return t?e=__("Disconnecting…","jetpack-connection"):"plugins"===h&&(e=__("Deactivate","jetpack-connection")),(0,p.jsx)(s.Button,{variant:"primary",disabled:t,onClick:j,className:"jp-connection__disconnect-dialog__btn-disconnect",children:e})})()]})]}),r&&(0,p.jsx)("p",{className:"jp-connection__disconnect-dialog__error",children:r})]})]})};u.propTypes={title:a().string,isDisconnecting:a().bool,onDisconnect:a().func,disconnectError:a().bool,disconnectStepComponent:a().element,connectedPlugins:a().array,disconnectingPlugin:a().string,closeModal:a().func,context:a().string,trackModalClick:a().func};const m=u},4617:(e,n,t)=>{"use strict";t.d(n,{A:()=>o});const o=e=>{window.location.replace(e)}},4659:(e,n,t)=>{"use strict";t.d(n,{A:()=>o});const o={button:"zI5tJ_qhWE6Oe6Lk75GY","is-icon-button":"tuBt2DLqimiImoqVzPqo",small:"Na39I683LAaSA99REg14",normal:"ipS7tKy9GntCS4R3vekF",icon:"paGLQwtPEaJmtArCcmyK",regular:"lZAo6_oGfclXOO9CC6Rd","full-width":"xJDOiJxTt0R_wSl8Ipz_",loading:"q_tVWqMjl39RcY6WtQA6","external-icon":"CDuBjJp_8jxzx5j6Nept"}},4804:(e,n,t)=>{n.formatArgs=function(n){if(n[0]=(this.useColors?"%c":"")+this.namespace+(this.useColors?" %c":" ")+n[0]+(this.useColors?"%c ":" ")+"+"+e.exports.humanize(this.diff),!this.useColors)return;const t="color: "+this.color;n.splice(1,0,t,"color: inherit");let o=0,s=0;n[0].replace(/%[a-zA-Z%]/g,e=>{"%%"!==e&&(o++,"%c"===e&&(s=o))}),n.splice(s,0,t)},n.save=function(e){try{e?n.storage.setItem("debug",e):n.storage.removeItem("debug")}catch(e){}},n.load=function(){let e;try{e=n.storage.getItem("debug")||n.storage.getItem("DEBUG")}catch(e){}!e&&"undefined"!=typeof process&&"env"in process&&(e=process.env.DEBUG);return e},n.useColors=function(){if("undefined"!=typeof window&&window.process&&("renderer"===window.process.type||window.process.__nwjs))return!0;if("undefined"!=typeof navigator&&navigator.userAgent&&navigator.userAgent.toLowerCase().match(/(edge|trident)\/(\d+)/))return!1;let e;return"undefined"!=typeof document&&document.documentElement&&document.documentElement.style&&document.documentElement.style.WebkitAppearance||"undefined"!=typeof window&&window.console&&(window.console.firebug||window.console.exception&&window.console.table)||"undefined"!=typeof navigator&&navigator.userAgent&&(e=navigator.userAgent.toLowerCase().match(/firefox\/(\d+)/))&&parseInt(e[1],10)>=31||"undefined"!=typeof navigator&&navigator.userAgent&&navigator.userAgent.toLowerCase().match(/applewebkit\/(\d+)/)},n.storage=function(){try{return localStorage}catch(e){}}(),n.destroy=(()=>{let e=!1;return()=>{e||(e=!0,console.warn("Instance method `debug.destroy()` is deprecated and no longer does anything. It will be removed in the next major version of `debug`."))}})(),n.colors=["#0000CC","#0000FF","#0033CC","#0033FF","#0066CC","#0066FF","#0099CC","#0099FF","#00CC00","#00CC33","#00CC66","#00CC99","#00CCCC","#00CCFF","#3300CC","#3300FF","#3333CC","#3333FF","#3366CC","#3366FF","#3399CC","#3399FF","#33CC00","#33CC33","#33CC66","#33CC99","#33CCCC","#33CCFF","#6600CC","#6600FF","#6633CC","#6633FF","#66CC00","#66CC33","#9900CC","#9900FF","#9933CC","#9933FF","#99CC00","#99CC33","#CC0000","#CC0033","#CC0066","#CC0099","#CC00CC","#CC00FF","#CC3300","#CC3333","#CC3366","#CC3399","#CC33CC","#CC33FF","#CC6600","#CC6633","#CC9900","#CC9933","#CCCC00","#CCCC33","#FF0000","#FF0033","#FF0066","#FF0099","#FF00CC","#FF00FF","#FF3300","#FF3333","#FF3366","#FF3399","#FF33CC","#FF33FF","#FF6600","#FF6633","#FF9900","#FF9933","#FFCC00","#FFCC33"],n.log=console.debug||console.log||(()=>{}),e.exports=t(5067)(n);const{formatters:o}=e.exports;o.j=function(e){try{return JSON.stringify(e)}catch(e){return"[UnexpectedJSONParseError]: "+e.message}}},4981:(e,n,t)=>{"use strict";t.d(n,{A:()=>A});var o=t(372),s=t(5932),c=t(3924),i=t(7425),r=t(1112),a=t(7999),l=t(6427),d=t(6087),p=t(7723),u=t(7750),m=t(1386),g=t(8391),h=t(2231),f=t(3619),_=t.n(f),y=t(1609),b=t(7088),j=t(3269),k=t(3735),C=(t(4046),t(790));const __=p.__,v=e=>{const{title:n=__("Manage your Jetpack connection","jetpack-connection"),apiRoot:t,apiNonce:r,connectedPlugins:d,onDisconnected:p,onUnlinked:u,context:m="jetpack-dashboard",connectedUser:g={},connectedSiteId:h,isOpen:f=!1,onClose:_}=e,[v,A]=(0,y.useState)(!1),[N,S]=(0,y.useState)(!1),[E,F]=(0,y.useState)(""),[T,O]=(0,y.useState)(!1);(0,y.useEffect)(()=>{s.Ay.setApiRoot(t),s.Ay.setApiNonce(r)},[t,r]);const L=(0,y.useCallback)(e=>{e&&e.preventDefault(),A(!0)},[A]),I=(0,y.useCallback)(e=>{e&&e.preventDefault(),A(!1)},[A]),R=(0,y.useMemo)(()=>!!g.currentUser?.permissions?.manage_options,[g.currentUser]),P=(0,y.useCallback)(()=>{g.currentUser?.isConnected&&(S(!0),F(""),s.Ay.unlinkUser(R).then(()=>{S(!1),_(),u()}).catch(()=>{let e=__("There was some trouble disconnecting your user account, your Jetpack plugin(s) may be outdated. Please visit your plugins page and make sure all Jetpack plugins are updated.","jetpack-connection");R||(e=__("There was some trouble disconnecting your user account, your Jetpack plugin(s) may be outdated. Please ask a site admin to update Jetpack","jetpack-connection")),F(e),S(!1)}))},[S,F,R,u,_,g]),D=(0,y.useCallback)(e=>{e&&e.preventDefault(),g.currentUser?.isMaster?O(!0):(o.A.tracks.recordEvent("jetpack_manage_connection_dialog_disconnect_user_click",{context:m}),P())},[P,m,g]),U=(0,y.useMemo)(()=>N,[N]),B=__("Disconnecting…","jetpack-connection"),M=(0,y.useCallback)(()=>{O(!1)},[O]);return(0,C.jsx)(C.Fragment,{children:f&&(0,C.jsxs)(C.Fragment,{children:[(0,C.jsxs)(l.Modal,{title:"",contentLabel:n,aria:{labelledby:"jp-connection__manage-dialog__heading"},shouldCloseOnClickOutside:!1,shouldCloseOnEsc:!1,isDismissible:!1,className:"jp-connection__manage-dialog",children:[(0,C.jsxs)("div",{className:"jp-connection__manage-dialog__content",children:[(0,C.jsx)("h1",{id:"jp-connection__manage-dialog__heading",children:n}),(0,C.jsx)(i.Ay,{className:"jp-connection__manage-dialog__large-text",children:__("At least one user must be connected for your Jetpack products to work properly.","jetpack-connection")}),R&&g.currentUser?.isConnected&&g.currentUser?.isMaster&&(0,C.jsx)(w,{title:__("Transfer ownership to another admin","jetpack-connection"),link:(0,c.A)("calypso-settings-manage-connection",{site:window?.myJetpackInitialState?.siteSuffix}),isExternal:!0,action:"transfer",disabled:U},"transfer"),g.currentUser?.isConnected&&(0,C.jsxs)(C.Fragment,{children:[""!==E&&(0,C.jsx)(b.A,{message:E}),(0,C.jsx)(w,{title:N?B:__("Disconnect my user account","jetpack-connection"),onClick:D,action:"unlink",disabled:U},"unlink")]}),R&&!(0,a.isWoASite)()&&(0,C.jsx)(w,{title:__("Disconnect Jetpack","jetpack-connection"),onClick:L,action:"disconnect",disabled:U},"disconnect")]}),(0,C.jsx)(x,{onClose:_,disabled:U})]}),(0,C.jsx)(j.A,{apiRoot:t,apiNonce:r,onDisconnected:p,connectedPlugins:d,connectedSiteId:h,connectedUser:g,isOpen:v,onClose:I,context:m}),(0,C.jsx)(k.A,{isOpen:T,onClose:M,apiRoot:t,apiNonce:r,onDisconnected:p,onUnlinked:u})]})})},w=({title:e,onClick:n=()=>null,isExternal:t=!1,link:o="#",action:s,disabled:c=!1})=>{const i=(0,y.useCallback)(e=>e.preventDefault(),[]);return(0,C.jsx)("div",{className:"jp-connection__manage-dialog__action-card card"+(c?" disabled":""),children:(0,C.jsx)("div",{className:"jp-connection__manage-dialog__action-card__card-content",children:(0,C.jsxs)("a",{href:o,className:(0,h.A)("jp-connection__manage-dialog__action-card__card-headline",s),onClick:c?i:n,target:t?"_blank":"_self",rel:"noopener noreferrer",children:[e,(0,C.jsx)(u.A,{icon:t?g.A:m.A,className:"jp-connection__manage-dialog__action-card__icon"})]})})})},x=({onClose:e,disabled:n})=>(0,C.jsxs)("div",{className:"jp-row jp-connection__manage-dialog__actions",children:[(0,C.jsx)("div",{className:"jp-connection__manage-dialog__text-wrap lg-col-span-9 md-col-span-7 sm-col-span-3",children:(0,C.jsx)(i.Ay,{children:(0,d.createInterpolateElement)(__("<strong>Need help?</strong> Learn more about the <connectionInfoLink>Jetpack connection</connectionInfoLink> or <supportLink>contact Jetpack support</supportLink>","jetpack-connection"),{strong:(0,C.jsx)("strong",{}),connectionInfoLink:(0,C.jsx)(l.ExternalLink,{href:(0,c.A)("why-the-wordpress-com-connection-is-important-for-jetpack"),className:"jp-connection__manage-dialog__link"}),supportLink:(0,C.jsx)(l.ExternalLink,{href:(0,c.A)("jetpack-support"),className:"jp-connection__manage-dialog__link"})})})}),(0,C.jsx)("div",{className:"jp-connection__manage-dialog__button-wrap lg-col-span-3 md-col-span-1 sm-col-span-1",children:(0,C.jsx)(r.A,{weight:"regular",variant:"secondary",onClick:e,className:"jp-connection__manage-dialog__btn-dismiss",disabled:n,children:__("Cancel","jetpack-connection")})})]});v.propTypes={title:_().string,apiRoot:_().string.isRequired,apiNonce:_().string.isRequired,connectedPlugins:_().oneOfType([_().array,_().object]),onDisconnected:_().func,onUnlinked:_().func,context:_().string,connectedUser:_().object,connectedSiteId:_().number,isOpen:_().bool,onClose:_().func};const A=v},5051:(e,n,t)=>{"use strict";t.d(n,{A:()=>c});var o=t(7143),s=t(3935);const c=(0,o.combineReducers)({connectionStatus:(e={},n)=>{switch(n.type){case s.v_:return{...e,...n.connectionStatus};case s.dz:return{...e,isUserConnected:!1}}return e},connectionStatusIsFetching:(e=!1,n)=>n.type===s.ZO?n.isFetching:e,siteIsRegistering:(e=!1,n)=>n.type===s.Ij?n.isRegistering:e,userIsConnecting:(e=!1,n)=>n.type===s.DO?n.isConnecting:e,registrationError:(e,n)=>{switch(n.type){case s.LW:return!1;case s.A1:return n.registrationError;default:return e}},authorizationUrl:(e,n)=>n.type===s.XY?n.authorizationUrl:e,userConnectionData:(e,n)=>(n.type,e),connectedPlugins:(e={},n)=>n.type===s.gH?n.connectedPlugins:e,connectionErrors:(e={},n)=>n.type===s.Kl?n.connectionErrors:e,isOfflineMode:(e=!1,n)=>n.type===s.MU?n.isConnecting:e})},5067:(e,n,t)=>{e.exports=function(e){function n(e){let t,s,c,i=null;function r(...e){if(!r.enabled)return;const o=r,s=Number(new Date),c=s-(t||s);o.diff=c,o.prev=t,o.curr=s,t=s,e[0]=n.coerce(e[0]),"string"!=typeof e[0]&&e.unshift("%O");let i=0;e[0]=e[0].replace(/%([a-zA-Z%])/g,(t,s)=>{if("%%"===t)return"%";i++;const c=n.formatters[s];if("function"==typeof c){const n=e[i];t=c.call(o,n),e.splice(i,1),i--}return t}),n.formatArgs.call(o,e);(o.log||n.log).apply(o,e)}return r.namespace=e,r.useColors=n.useColors(),r.color=n.selectColor(e),r.extend=o,r.destroy=n.destroy,Object.defineProperty(r,"enabled",{enumerable:!0,configurable:!1,get:()=>null!==i?i:(s!==n.namespaces&&(s=n.namespaces,c=n.enabled(e)),c),set:e=>{i=e}}),"function"==typeof n.init&&n.init(r),r}function o(e,t){const o=n(this.namespace+(void 0===t?":":t)+e);return o.log=this.log,o}function s(e,n){let t=0,o=0,s=-1,c=0;for(;t<e.length;)if(o<n.length&&(n[o]===e[t]||"*"===n[o]))"*"===n[o]?(s=o,c=t,o++):(t++,o++);else{if(-1===s)return!1;o=s+1,c++,t=c}for(;o<n.length&&"*"===n[o];)o++;return o===n.length}return n.debug=n,n.default=n,n.coerce=function(e){if(e instanceof Error)return e.stack||e.message;return e},n.disable=function(){const e=[...n.names,...n.skips.map(e=>"-"+e)].join(",");return n.enable(""),e},n.enable=function(e){n.save(e),n.namespaces=e,n.names=[],n.skips=[];const t=("string"==typeof e?e:"").trim().replace(/\s+/g,",").split(",").filter(Boolean);for(const e of t)"-"===e[0]?n.skips.push(e.slice(1)):n.names.push(e)},n.enabled=function(e){for(const t of n.skips)if(s(e,t))return!1;for(const t of n.names)if(s(e,t))return!0;return!1},n.humanize=t(3594),n.destroy=function(){console.warn("Instance method `debug.destroy()` is deprecated and no longer does anything. It will be removed in the next major version of `debug`.")},Object.keys(e).forEach(t=>{n[t]=e[t]}),n.names=[],n.skips=[],n.formatters={},n.selectColor=function(e){let t=0;for(let n=0;n<e.length;n++)t=(t<<5)-t+e.charCodeAt(n),t|=0;return n.colors[Math.abs(t)%n.colors.length]},n.enable(n.load()),n}},5404:()=>{},5573:e=>{"use strict";e.exports=window.wp.primitives},5582:(e,n,t)=>{"use strict";t.d(n,{A:()=>p});var o=t(3924),s=t(8089),c=t(5879),i=t(6087),r=t(7723),a=t(2668),l=(t(7627),t(790));const __=r.__,d=(e,n)=>{switch(e){case"fail_domain_forbidden":case"fail_ip_forbidden":case"fail_domain_tld":case"fail_subdomain_wpcom":case"siteurl_private_ip":return __("Your site host is on a private network. Jetpack can only connect to public sites.","jetpack-connection");case"connection_disabled":return __("This site has been suspended.","jetpack-connection")}if(n)return(0,i.createInterpolateElement)(__("Unavailable in <a>Offline Mode</a>","jetpack-connection"),{a:(0,l.jsx)("a",{href:(0,o.A)("jetpack-support-development-mode"),target:"_blank",rel:"noopener noreferrer"})})},p=({title:e,images:n,children:t,assetBaseUrl:o,isLoading:i,buttonLabel:r,handleButtonClick:p,displayButtonError:u,errorCode:m,buttonIsLoading:g,loadingLabel:h,footer:f,isOfflineMode:_,logo:y})=>(0,l.jsx)(a.A,{title:e,assetBaseUrl:o,images:n,className:"jp-connection__connect-screen"+(i?" jp-connection__connect-screen__loading":""),logo:y,children:(0,l.jsxs)("div",{className:"jp-connection__connect-screen__content",children:[t,(0,l.jsx)("div",{className:"jp-connection__connect-screen__tos",children:(0,l.jsx)(c.A,{agreeButtonLabel:r})}),(0,l.jsx)(s.A,{label:r,onClick:p,displayError:u||_,errorMessage:d(m,_),isLoading:g,isDisabled:_}),(0,l.jsx)("span",{className:"jp-connection__connect-screen__loading-message",role:"status",children:g?h||__("Loading","jetpack-connection"):""}),f&&(0,l.jsx)("div",{className:"jp-connection__connect-screen__footer",children:f})]})})},5745:(e,n,t)=>{"use strict";t.d(n,{A:()=>c});var o=t(1609),s=t(790);const c=({images:e,assetBaseUrl:n=""})=>{if(!e?.length)return null;const t=e.map((e,t)=>(0,s.jsx)(o.Fragment,{children:(0,s.jsx)("img",{src:n+e,alt:""})},t));return(0,s.jsx)("div",{className:"jp-connection__connect-screen__image-slider",children:t})}},5879:(e,n,t)=>{"use strict";t.d(n,{A:()=>g});var o=t(6427),s=t(6087),c=t(7723),i=t(2231),r=t(3924),a=t(7425),l=(t(4099),t(790));const __=c.__,d=({multipleButtonsLabels:e})=>Array.isArray(e)&&e.length>1?(0,s.createInterpolateElement)((0,c.sprintf)(/* translators: %1$s is button label 1 and %2$s is button label 2 */ __("By clicking <strong>%1$s</strong> or <strong>%2$s</strong>, you agree to our <tosLink>Terms of Service</tosLink> and to <shareDetailsLink>sync your site‘s data</shareDetailsLink> with us.","jetpack-connection"),e[0],e[1]),{strong:(0,l.jsx)("strong",{}),tosLink:(0,l.jsx)(m,{slug:"wpcom-tos"}),shareDetailsLink:(0,l.jsx)(m,{slug:"jetpack-support-what-data-does-jetpack-sync"})}):(0,s.createInterpolateElement)(__("By clicking the buttons above, you agree to our <tosLink>Terms of Service</tosLink> and to <shareDetailsLink>sync your site‘s data</shareDetailsLink> with us.","jetpack-connection"),{tosLink:(0,l.jsx)(m,{slug:"wpcom-tos"}),shareDetailsLink:(0,l.jsx)(m,{slug:"jetpack-support-what-data-does-jetpack-sync"})}),p=({agreeButtonLabel:e})=>(0,s.createInterpolateElement)((0,c.sprintf)(/* translators: %s is a button label */ __("By clicking <strong>%s</strong>, you agree to our <tosLink>Terms of Service</tosLink> and to <shareDetailsLink>sync your site‘s data</shareDetailsLink> with us.","jetpack-connection"),e),{strong:(0,l.jsx)("strong",{}),tosLink:(0,l.jsx)(m,{slug:"wpcom-tos"}),shareDetailsLink:(0,l.jsx)(m,{slug:"jetpack-support-what-data-does-jetpack-sync"})}),u=()=>(0,s.createInterpolateElement)(__("By continuing you agree to our <tosLink>Terms of Service</tosLink> and to <shareDetailsLink>sync your site’s data</shareDetailsLink> with us. We’ll check if that email is linked to an existing WordPress.com account or create a new one instantly.","jetpack-connection"),{tosLink:(0,l.jsx)(m,{slug:"wpcom-tos"}),shareDetailsLink:(0,l.jsx)(m,{slug:"jetpack-support-what-data-does-jetpack-sync"})}),m=({slug:e,children:n})=>(0,l.jsx)(o.ExternalLink,{className:"terms-of-service__link",href:(0,r.A)(e),children:n}),g=({className:e,multipleButtons:n,agreeButtonLabel:t,isTextOnly:o,...s})=>(0,l.jsx)(a.Ay,{className:(0,i.A)(e,"terms-of-service"),...s,children:o?(0,l.jsx)(u,{}):n?(0,l.jsx)(d,{multipleButtonsLabels:n}):(0,l.jsx)(p,{agreeButtonLabel:t})})},5932:(e,n,t)=>{"use strict";t.d(n,{Ay:()=>p});var o=t(6439),s=t(3832);function c(e){class n extends Error{constructor(...n){super(...n),this.name=e}}return n}const i=c("JsonParseError"),r=c("JsonParseAfterRedirectError"),a=c("Api404Error"),l=c("Api404AfterRedirectError"),d=c("FetchNetworkError");const p=new function(e,n){let t=e,c=e,i={"X-WP-Nonce":n},r={credentials:"same-origin",headers:i},a={method:"post",credentials:"same-origin",headers:Object.assign({},i,{"Content-type":"application/json"})},l=function(e){const n=e.split("?"),t=n.length>1?n[1]:"",o=t.length?t.split("&"):[];return o.push("_cacheBuster="+(new Date).getTime()),n[0]+"?"+o.join("&")};const d={setApiRoot(e){t=e},setWpcomOriginApiUrl(e){c=e},setApiNonce(e){i={"X-WP-Nonce":e},r={credentials:"same-origin",headers:i},a={method:"post",credentials:"same-origin",headers:Object.assign({},i,{"Content-type":"application/json"})}},setCacheBusterCallback:e=>{l=e},registerSite:(e,n,s)=>{const c={};return(0,o.jetpackConfigHas)("consumer_slug")&&(c.plugin_slug=(0,o.jetpackConfigGet)("consumer_slug")),null!==n&&(c.redirect_uri=n),s&&(c.from=s),g(`${t}jetpack/v4/connection/register`,a,{body:JSON.stringify(c)}).then(u).then(m)},fetchAuthorizationUrl:e=>p((0,s.addQueryArgs)(`${t}jetpack/v4/connection/authorize_url`,{no_iframe:"1",redirect_uri:e}),r).then(u).then(m),fetchSiteConnectionData:()=>p(`${t}jetpack/v4/connection/data`,r).then(m),fetchSiteConnectionStatus:()=>p(`${t}jetpack/v4/connection`,r).then(m),fetchSiteConnectionTest:()=>p(`${t}jetpack/v4/connection/test`,r).then(u).then(m),fetchUserConnectionData:()=>p(`${t}jetpack/v4/connection/data`,r).then(m),fetchUserTrackingSettings:()=>p(`${t}jetpack/v4/tracking/settings`,r).then(u).then(m),updateUserTrackingSettings:e=>g(`${t}jetpack/v4/tracking/settings`,a,{body:JSON.stringify(e)}).then(u).then(m),disconnectSite:()=>g(`${t}jetpack/v4/connection`,a,{body:JSON.stringify({isActive:!1})}).then(u).then(m),fetchConnectUrl:()=>p(`${t}jetpack/v4/connection/url`,r).then(u).then(m),unlinkUser:(e=!1,n={})=>{const o={linked:!1,force:!!e};return n.disconnectAllUsers&&(o["disconnect-all-users"]=!0),g(`${t}jetpack/v4/connection/user`,a,{body:JSON.stringify(o)}).then(u).then(m)},reconnect:()=>g(`${t}jetpack/v4/connection/reconnect`,a).then(u).then(m),fetchConnectedPlugins:()=>p(`${t}jetpack/v4/connection/plugins`,r).then(u).then(m),setHasSeenWCConnectionModal:()=>g(`${t}jetpack/v4/seen-wc-connection-modal`,a).then(u).then(m),fetchModules:()=>p(`${t}jetpack/v4/module/all`,r).then(u).then(m),fetchModule:e=>p(`${t}jetpack/v4/module/${e}`,r).then(u).then(m),activateModule:e=>g(`${t}jetpack/v4/module/${e}/active`,a,{body:JSON.stringify({active:!0})}).then(u).then(m),deactivateModule:e=>g(`${t}jetpack/v4/module/${e}/active`,a,{body:JSON.stringify({active:!1})}),updateModuleOptions:(e,n)=>g(`${t}jetpack/v4/module/${e}`,a,{body:JSON.stringify(n)}).then(u).then(m),updateSettings:e=>g(`${t}jetpack/v4/settings`,a,{body:JSON.stringify(e)}).then(u).then(m),getProtectCount:()=>p(`${t}jetpack/v4/module/protect/data`,r).then(u).then(m),resetOptions:e=>g(`${t}jetpack/v4/options/${e}`,a,{body:JSON.stringify({reset:!0})}).then(u).then(m),activateVaultPress:()=>g(`${t}jetpack/v4/plugins`,a,{body:JSON.stringify({slug:"vaultpress",status:"active"})}).then(u).then(m),getVaultPressData:()=>p(`${t}jetpack/v4/module/vaultpress/data`,r).then(u).then(m),installPlugin:(e,n)=>{const o={slug:e,status:"active"};return n&&(o.source=n),g(`${t}jetpack/v4/plugins`,a,{body:JSON.stringify(o)}).then(u).then(m)},activateAkismet:()=>g(`${t}jetpack/v4/plugins`,a,{body:JSON.stringify({slug:"akismet",status:"active"})}).then(u).then(m),getAkismetData:()=>p(`${t}jetpack/v4/module/akismet/data`,r).then(u).then(m),checkAkismetKey:()=>p(`${t}jetpack/v4/module/akismet/key/check`,r).then(u).then(m),checkAkismetKeyTyped:e=>g(`${t}jetpack/v4/module/akismet/key/check`,a,{body:JSON.stringify({api_key:e})}).then(u).then(m),getFeatureTypeStatus:e=>p(`${t}jetpack/v4/feature/${e}`,r).then(u).then(m),fetchStatsData:e=>p(function(e){let n=`${t}jetpack/v4/module/stats/data`;-1!==n.indexOf("?")?n+=`&range=${encodeURIComponent(e)}`:n+=`?range=${encodeURIComponent(e)}`;return n}(e),r).then(u).then(m).then(f),getPluginUpdates:()=>p(`${t}jetpack/v4/updates/plugins`,r).then(u).then(m),getPlans:()=>p(`${t}jetpack/v4/plans`,r).then(u).then(m),fetchSettings:()=>p(`${t}jetpack/v4/settings`,r).then(u).then(m),updateSetting:e=>g(`${t}jetpack/v4/settings`,a,{body:JSON.stringify(e)}).then(u).then(m),fetchSiteData:()=>p(`${t}jetpack/v4/site`,r).then(u).then(m).then(e=>JSON.parse(e.data)),fetchSiteFeatures:()=>p(`${t}jetpack/v4/site/features`,r).then(u).then(m).then(e=>JSON.parse(e.data)),fetchSiteProducts:()=>p(`${t}jetpack/v4/site/products`,r).then(u).then(m),fetchSitePurchases:()=>p(`${t}jetpack/v4/site/purchases`,r).then(u).then(m).then(e=>JSON.parse(e.data)),fetchSiteBenefits:()=>p(`${t}jetpack/v4/site/benefits`,r).then(u).then(m).then(e=>JSON.parse(e.data)),fetchSiteDiscount:()=>p(`${t}jetpack/v4/site/discount`,r).then(u).then(m).then(e=>e.data),fetchSetupQuestionnaire:()=>p(`${t}jetpack/v4/setup/questionnaire`,r).then(u).then(m),fetchRecommendationsData:()=>p(`${t}jetpack/v4/recommendations/data`,r).then(u).then(m),fetchRecommendationsProductSuggestions:()=>p(`${t}jetpack/v4/recommendations/product-suggestions`,r).then(u).then(m),fetchRecommendationsUpsell:()=>p(`${t}jetpack/v4/recommendations/upsell`,r).then(u).then(m),fetchRecommendationsConditional:()=>p(`${t}jetpack/v4/recommendations/conditional`,r).then(u).then(m),saveRecommendationsData:e=>g(`${t}jetpack/v4/recommendations/data`,a,{body:JSON.stringify({data:e})}).then(u),fetchProducts:()=>p(`${t}jetpack/v4/products`,r).then(u).then(m),fetchRewindStatus:()=>p(`${t}jetpack/v4/rewind`,r).then(u).then(m).then(e=>JSON.parse(e.data)),fetchScanStatus:()=>p(`${t}jetpack/v4/scan`,r).then(u).then(m).then(e=>JSON.parse(e.data)),dismissJetpackNotice:e=>g(`${t}jetpack/v4/notice/${e}`,a,{body:JSON.stringify({dismissed:!0})}).then(u).then(m),fetchPluginsData:()=>p(`${t}jetpack/v4/plugins`,r).then(u).then(m),fetchIntroOffers:()=>p(`${t}jetpack/v4/intro-offers`,r).then(u).then(m),fetchVerifySiteGoogleStatus:e=>p(null!==e?`${t}jetpack/v4/verify-site/google/${e}`:`${t}jetpack/v4/verify-site/google`,r).then(u).then(m),verifySiteGoogle:e=>g(`${t}jetpack/v4/verify-site/google`,a,{body:JSON.stringify({keyring_id:e})}).then(u).then(m),submitSurvey:e=>g(`${t}jetpack/v4/marketing/survey`,a,{body:JSON.stringify(e)}).then(u).then(m),saveSetupQuestionnaire:e=>g(`${t}jetpack/v4/setup/questionnaire`,a,{body:JSON.stringify(e)}).then(u).then(m),updateLicensingError:e=>g(`${t}jetpack/v4/licensing/error`,a,{body:JSON.stringify(e)}).then(u).then(m),updateLicenseKey:e=>g(`${t}jetpack/v4/licensing/set-license`,a,{body:JSON.stringify({license:e})}).then(u).then(m),getUserLicensesCounts:()=>p(`${t}jetpack/v4/licensing/user/counts`,r).then(u).then(m),getUserLicenses:()=>p(`${t}jetpack/v4/licensing/user/licenses`,r).then(u).then(m),updateLicensingActivationNoticeDismiss:e=>g(`${t}jetpack/v4/licensing/user/activation-notice-dismiss`,a,{body:JSON.stringify({last_detached_count:e})}).then(u).then(m),updateRecommendationsStep:e=>g(`${t}jetpack/v4/recommendations/step`,a,{body:JSON.stringify({step:e})}).then(u),confirmIDCSafeMode:()=>g(`${t}jetpack/v4/identity-crisis/confirm-safe-mode`,a).then(u),startIDCFresh:e=>g(`${t}jetpack/v4/identity-crisis/start-fresh`,a,{body:JSON.stringify({redirect_uri:e})}).then(u).then(m),migrateIDC:()=>g(`${t}jetpack/v4/identity-crisis/migrate`,a).then(u),attachLicenses:e=>g(`${t}jetpack/v4/licensing/attach-licenses`,a,{body:JSON.stringify({licenses:e})}).then(u).then(m),fetchSearchPlanInfo:()=>p(`${c}jetpack/v4/search/plan`,r).then(u).then(m),fetchSearchSettings:()=>p(`${c}jetpack/v4/search/settings`,r).then(u).then(m),updateSearchSettings:e=>g(`${c}jetpack/v4/search/settings`,a,{body:JSON.stringify(e)}).then(u).then(m),fetchSearchStats:()=>p(`${c}jetpack/v4/search/stats`,r).then(u).then(m),fetchWafSettings:()=>p(`${t}jetpack/v4/waf`,r).then(u).then(m),updateWafSettings:e=>g(`${t}jetpack/v4/waf`,a,{body:JSON.stringify(e)}).then(u).then(m),fetchWordAdsSettings:()=>p(`${t}jetpack/v4/wordads/settings`,r).then(u).then(m),updateWordAdsSettings:e=>g(`${t}jetpack/v4/wordads/settings`,a,{body:JSON.stringify(e)}),fetchSearchPricing:()=>p(`${c}jetpack/v4/search/pricing`,r).then(u).then(m),fetchMigrationStatus:()=>p(`${t}jetpack/v4/migration/status`,r).then(u).then(m),fetchBackupUndoEvent:()=>p(`${t}jetpack/v4/site/backup/undo-event`,r).then(u).then(m),fetchBackupPreflightStatus:()=>p(`${t}jetpack/v4/site/backup/preflight`,r).then(u).then(m)};function p(e,n){return fetch(l(e),n)}function g(e,n,t){return fetch(e,Object.assign({},n,t)).catch(h)}function f(e){return e.general&&void 0===e.general.response||e.week&&void 0===e.week.response||e.month&&void 0===e.month.response?e:{}}Object.assign(this,d)};function u(e){return e.status>=200&&e.status<300?e:404===e.status?new Promise(()=>{throw e.redirected?new l(e.redirected):new a}):e.json().catch(e=>g(e)).then(n=>{const t=new Error(`${n.message} (Status ${e.status})`);throw t.response=n,t.name="ApiError",t})}function m(e){return e.json().catch(n=>g(n,e.redirected,e.url))}function g(e,n,t){throw n?new r(t):new i}function h(){throw new d}},6087:e=>{"use strict";e.exports=window.wp.element},6212:(e,n,t)=>{"use strict";t.d(n,{A:()=>r});var o=t(7723),s=t(9660),c=t(5582),i=t(790);const __=o.__,r=({title:e,buttonLabel:n,loadingLabel:t,apiRoot:o,apiNonce:r,registrationNonce:a,from:l,redirectUri:d,images:p,children:u,assetBaseUrl:m,autoTrigger:g,footer:h,skipUserConnection:f,skipPricingPage:_,logo:y})=>{const{handleRegisterSite:b,siteIsRegistering:j,userIsConnecting:k,registrationError:C,isOfflineMode:v}=(0,s.A)({registrationNonce:a,redirectUri:d,apiRoot:o,apiNonce:r,autoTrigger:g,from:l,skipUserConnection:f,skipPricingPage:_}),w=Boolean(C),x=j||k,A=C?.response?.code;return(0,i.jsx)(c.A,{title:e||__("Over 5 million WordPress sites are faster and more secure","jetpack-connection"),images:p||[],assetBaseUrl:m,buttonLabel:n||__("Set up Jetpack","jetpack-connection"),loadingLabel:t,handleButtonClick:b,displayButtonError:w,errorCode:A,buttonIsLoading:x,footer:h,isOfflineMode:v,logo:y,children:u})}},6427:e=>{"use strict";e.exports=window.wp.components},6439:(e,n,t)=>{let o={};try{o=t(9074)}catch{console.error("jetpackConfig is missing in your webpack config file. See @automattic/jetpack-config"),o={missingConfig:!0}}const s=e=>Object.hasOwn(o,e);e.exports={jetpackConfigHas:s,jetpackConfigGet:e=>{if(!s(e))throw'This app requires the "'+e+'" Jetpack Config to be defined in your webpack configuration file. See details in @automattic/jetpack-config package docs.';return o[e]}}},6461:(e,n,t)=>{"use strict";t.d(n,{A:()=>r});var o=t(3619),s=t.n(o),c=(t(2144),t(790));const i=({color:e="#FFFFFF",className:n="",size:t=20})=>{const o=n+" jp-components-spinner",s={width:t,height:t,fontSize:t,borderTopColor:e},i={borderTopColor:e,borderRightColor:e};return(0,c.jsx)("div",{className:o,children:(0,c.jsx)("div",{className:"jp-components-spinner__outer",style:s,children:(0,c.jsx)("div",{className:"jp-components-spinner__inner",style:i})})})};i.propTypes={color:s().string,className:s().string,size:s().number};const r=i},6673:(e,n,t)=>{"use strict";t.d(n,{a:()=>o});const o={AED:{symbol:"د.إ."},AFN:{symbol:"؋"},ALL:{symbol:"Lek"},AMD:{symbol:"֏"},ANG:{symbol:"ƒ"},AOA:{symbol:"Kz"},ARS:{symbol:"$"},AUD:{symbol:"A$"},AWG:{symbol:"ƒ"},AZN:{symbol:"₼"},BAM:{symbol:"КМ"},BBD:{symbol:"Bds$"},BDT:{symbol:"৳"},BGN:{symbol:"лв."},BHD:{symbol:"د.ب."},BIF:{symbol:"FBu"},BMD:{symbol:"$"},BND:{symbol:"$"},BOB:{symbol:"Bs"},BRL:{symbol:"R$"},BSD:{symbol:"$"},BTC:{symbol:"Ƀ"},BTN:{symbol:"Nu."},BWP:{symbol:"P"},BYR:{symbol:"р."},BZD:{symbol:"BZ$"},CAD:{symbol:"C$"},CDF:{symbol:"FC"},CHF:{symbol:"CHF"},CLP:{symbol:"$"},CNY:{symbol:"¥"},COP:{symbol:"$"},CRC:{symbol:"₡"},CUC:{symbol:"CUC"},CUP:{symbol:"$MN"},CVE:{symbol:"$"},CZK:{symbol:"Kč"},DJF:{symbol:"Fdj"},DKK:{symbol:"kr."},DOP:{symbol:"RD$"},DZD:{symbol:"د.ج."},EGP:{symbol:"ج.م."},ERN:{symbol:"Nfk"},ETB:{symbol:"ETB"},EUR:{symbol:"€"},FJD:{symbol:"FJ$"},FKP:{symbol:"£"},GBP:{symbol:"£"},GEL:{symbol:"Lari"},GHS:{symbol:"₵"},GIP:{symbol:"£"},GMD:{symbol:"D"},GNF:{symbol:"FG"},GTQ:{symbol:"Q"},GYD:{symbol:"G$"},HKD:{symbol:"HK$"},HNL:{symbol:"L."},HRK:{symbol:"kn"},HTG:{symbol:"G"},HUF:{symbol:"Ft"},IDR:{symbol:"Rp"},ILS:{symbol:"₪"},INR:{symbol:"₹"},IQD:{symbol:"د.ع."},IRR:{symbol:"﷼"},ISK:{symbol:"kr."},JMD:{symbol:"J$"},JOD:{symbol:"د.ا."},JPY:{symbol:"¥"},KES:{symbol:"S"},KGS:{symbol:"сом"},KHR:{symbol:"៛"},KMF:{symbol:"CF"},KPW:{symbol:"₩"},KRW:{symbol:"₩"},KWD:{symbol:"د.ك."},KYD:{symbol:"$"},KZT:{symbol:"₸"},LAK:{symbol:"₭"},LBP:{symbol:"ل.ل."},LKR:{symbol:"₨"},LRD:{symbol:"L$"},LSL:{symbol:"M"},LYD:{symbol:"د.ل."},MAD:{symbol:"د.م."},MDL:{symbol:"lei"},MGA:{symbol:"Ar"},MKD:{symbol:"ден."},MMK:{symbol:"K"},MNT:{symbol:"₮"},MOP:{symbol:"MOP$"},MRO:{symbol:"UM"},MTL:{symbol:"₤"},MUR:{symbol:"₨"},MVR:{symbol:"MVR"},MWK:{symbol:"MK"},MXN:{symbol:"MX$"},MYR:{symbol:"RM"},MZN:{symbol:"MT"},NAD:{symbol:"N$"},NGN:{symbol:"₦"},NIO:{symbol:"C$"},NOK:{symbol:"kr"},NPR:{symbol:"₨"},NZD:{symbol:"NZ$"},OMR:{symbol:"﷼"},PAB:{symbol:"B/."},PEN:{symbol:"S/."},PGK:{symbol:"K"},PHP:{symbol:"₱"},PKR:{symbol:"₨"},PLN:{symbol:"zł"},PYG:{symbol:"₲"},QAR:{symbol:"﷼"},RON:{symbol:"lei"},RSD:{symbol:"Дин."},RUB:{symbol:"₽"},RWF:{symbol:"RWF"},SAR:{symbol:"﷼"},SBD:{symbol:"S$"},SCR:{symbol:"₨"},SDD:{symbol:"LSd"},SDG:{symbol:"£"},SEK:{symbol:"kr"},SGD:{symbol:"S$"},SHP:{symbol:"£"},SLL:{symbol:"Le"},SOS:{symbol:"S"},SRD:{symbol:"$"},STD:{symbol:"Db"},SVC:{symbol:"₡"},SYP:{symbol:"£"},SZL:{symbol:"E"},THB:{symbol:"฿"},TJS:{symbol:"TJS"},TMT:{symbol:"m"},TND:{symbol:"د.ت."},TOP:{symbol:"T$"},TRY:{symbol:"TL"},TTD:{symbol:"TT$"},TVD:{symbol:"$T"},TWD:{symbol:"NT$"},TZS:{symbol:"TSh"},UAH:{symbol:"₴"},UGX:{symbol:"USh"},USD:{},UYU:{symbol:"$U"},UZS:{symbol:"сўм"},VEB:{symbol:"Bs."},VEF:{symbol:"Bs. F."},VND:{symbol:"₫"},VUV:{symbol:"VT"},WST:{symbol:"WS$"},XAF:{symbol:"F"},XCD:{symbol:"$"},XOF:{symbol:"F"},XPF:{symbol:"F"},YER:{symbol:"﷼"},ZAR:{symbol:"R"},ZMW:{symbol:"ZK"},WON:{symbol:"₩"}}},6854:(e,n,t)=>{"use strict";t.d(n,{A:()=>o});const o={error:"TcCZnGE6mad8Dvz9pCZi",button:"_mn6o2Dtm5pfFWc8_A1K"}},7018:(e,n,t)=>{"use strict";t.d(n,{A:()=>l});var o=t(7723),s=t(3619),c=t.n(s),i=t(1609),r=(t(4317),t(790));const __=o.__,a=e=>{const{title:n,isLoading:t=!1,width:o="100%",displayTOS:s,scrollToIframe:c=!1,connectUrl:a,onComplete:l,onThirdPartyCookiesBlocked:d,location:p}=e;let{height:u="300"}=e;const m=(0,i.useRef)(void 0),g=(0,i.useRef)(void 0),h=e=>{if(g.current&&e.source===g.current.contentWindow)switch(e.data){case"close":window.removeEventListener("message",h),l&&l();break;case"wpcom_nocookie":d&&d()}};(0,i.useEffect)(()=>{c&&window.scrollTo(0,m.current.offsetTop-10),window.addEventListener("message",h)});let f=a.replace("authorize/","authorize_iframe/");return f.includes("?")||(f+="?"),s&&(f+="&display-tos",u=(parseInt(u)+50).toString()),f+="&iframe_height="+parseInt(u),p&&(f+="&iframe_source="+p),(0,r.jsxs)("div",{className:"dops-card fade-in jp-iframe-wrap",ref:m,children:[(0,r.jsx)("h1",{children:n}),t?(0,r.jsx)("p",{children:__("Loading…","jetpack-connection")}):(0,r.jsx)("iframe",{title:n,width:o,height:u,src:f,ref:g})]})};a.propTypes={title:c().string.isRequired,isLoading:c().bool,width:c().string,height:c().string,connectUrl:c().string.isRequired,displayTOS:c().bool.isRequired,scrollToIframe:c().bool,onComplete:c().func,onThirdPartyCookiesBlocked:c().func,location:c().string};const l=a},7088:(e,n,t)=>{"use strict";t.d(n,{A:()=>p});var o=t(6461),s=t(6427),c=t(7723),i=t(3619),r=t.n(i),a=t(9910),l=t(790);const __=c.__,d=e=>{const{message:n,isRestoringConnection:t,restoreConnectionCallback:i,restoreConnectionError:r,actions:d=[]}=e,p=a.A.notice,u=(0,l.jsx)(s.Icon,{icon:(0,l.jsxs)(s.SVG,{width:"24",height:"24",viewBox:"0 0 24 24",fill:"none",xmlns:"http://www.w3.org/2000/svg",children:[(0,l.jsx)(s.Path,{d:"M11.7815 4.93772C11.8767 4.76626 12.1233 4.76626 12.2185 4.93772L20.519 19.8786C20.6116 20.0452 20.4911 20.25 20.3005 20.25H3.69951C3.50889 20.25 3.3884 20.0452 3.48098 19.8786L11.7815 4.93772Z",stroke:"#D63638",strokeWidth:"1.5"}),(0,l.jsx)(s.Path,{d:"M13 10H11V15H13V10Z",fill:"#D63638"}),(0,l.jsx)(s.Path,{d:"M13 16H11V18H13V16Z",fill:"#D63638"})]})});if(!n)return null;if(t)return(0,l.jsx)(s.Notice,{status:"error",isDismissible:!1,className:p,children:(0,l.jsxs)("div",{className:a.A.message,children:[(0,l.jsx)(o.A,{color:"#B32D2E",size:24}),__("Reconnecting Jetpack","jetpack-connection")]})});const m=r?(0,l.jsx)(s.Notice,{status:"error",isDismissible:!1,className:p+" "+a.A.error,children:(0,l.jsxs)("div",{className:a.A.message,children:[u,(0,c.sprintf)(/* translators: %s: the error. */ __("There was an error reconnecting Jetpack. Error: %s","jetpack-connection"),r)]})}):null;let g=[];return d.length>0?g=d.map((e,n)=>{let t=a.A.button;return"primary"===e.variant?t+=" "+a.A.primary:"secondary"===e.variant&&(t+=" "+a.A.secondary),(0,l.jsx)("button",{type:"button",onClick:e.onClick,onKeyDown:e.onClick,className:t,disabled:e.isLoading,children:e.isLoading?e.loadingText||__("Loading…","jetpack-connection"):e.label},n)}):i&&(g=[(0,l.jsx)("button",{type:"button",onClick:i,onKeyDown:i,className:a.A.button,children:__("Restore Connection","jetpack-connection")},"restore")]),(0,l.jsxs)(l.Fragment,{children:[m,(0,l.jsxs)(s.Notice,{status:"error",isDismissible:!1,className:p,children:[(0,l.jsxs)("div",{className:a.A.message,children:[u,n]}),g.length>0&&(0,l.jsx)("div",{className:a.A.actions,children:g})]})]})};d.propTypes={message:r().oneOfType([r().string,r().element]).isRequired,restoreConnectionCallback:r().func,isRestoringConnection:r().bool,restoreConnectionError:r().string,actions:r().arrayOf(r().shape({label:r().string.isRequired,onClick:r().func.isRequired,isLoading:r().bool,loadingText:r().string,variant:r().oneOf(["primary","secondary"])}))};const p=d},7142:(e,n,t)=>{"use strict";t.d(n,{A:()=>i});var o=t(7723),s=t(2231),c=t(790);const __=o.__,i=({logoColor:e="#069e08",showText:n=!0,className:t,height:o=32,...i})=>{const r=n?"0 0 118 32":"0 0 32 32";return(0,c.jsxs)("svg",{xmlns:"http://www.w3.org/2000/svg",x:"0px",y:"0px",viewBox:r,className:(0,s.A)("jetpack-logo",t),"aria-labelledby":"jetpack-logo-title",height:o,...i,role:"img",children:[(0,c.jsx)("title",{id:"jetpack-logo-title",children:__("Jetpack Logo","jetpack-connection")}),(0,c.jsx)("path",{fill:e,d:"M16,0C7.2,0,0,7.2,0,16s7.2,16,16,16s16-7.2,16-16S24.8,0,16,0z M15,19H7l8-16V19z M17,29V13h8L17,29z"}),n&&(0,c.jsxs)(c.Fragment,{children:[(0,c.jsx)("path",{d:"M41.3,26.6c-0.5-0.7-0.9-1.4-1.3-2.1c2.3-1.4,3-2.5,3-4.6V8h-3V6h6v13.4C46,22.8,45,24.8,41.3,26.6z"}),(0,c.jsx)("path",{d:"M65,18.4c0,1.1,0.8,1.3,1.4,1.3c0.5,0,2-0.2,2.6-0.4v2.1c-0.9,0.3-2.5,0.5-3.7,0.5c-1.5,0-3.2-0.5-3.2-3.1V12H60v-2h2.1V7.1 H65V10h4v2h-4V18.4z"}),(0,c.jsx)("path",{d:"M71,10h3v1.3c1.1-0.8,1.9-1.3,3.3-1.3c2.5,0,4.5,1.8,4.5,5.6s-2.2,6.3-5.8,6.3c-0.9,0-1.3-0.1-2-0.3V28h-3V10z M76.5,12.3 c-0.8,0-1.6,0.4-2.5,1.2v5.9c0.6,0.1,0.9,0.2,1.8,0.2c2,0,3.2-1.3,3.2-3.9C79,13.4,78.1,12.3,76.5,12.3z"}),(0,c.jsx)("path",{d:"M93,22h-3v-1.5c-0.9,0.7-1.9,1.5-3.5,1.5c-1.5,0-3.1-1.1-3.1-3.2c0-2.9,2.5-3.4,4.2-3.7l2.4-0.3v-0.3c0-1.5-0.5-2.3-2-2.3 c-0.7,0-2.3,0.5-3.7,1.1L84,11c1.2-0.4,3-1,4.4-1c2.7,0,4.6,1.4,4.6,4.7L93,22z M90,16.4l-2.2,0.4c-0.7,0.1-1.4,0.5-1.4,1.6 c0,0.9,0.5,1.4,1.3,1.4s1.5-0.5,2.3-1V16.4z"}),(0,c.jsx)("path",{d:"M104.5,21.3c-1.1,0.4-2.2,0.6-3.5,0.6c-4.2,0-5.9-2.4-5.9-5.9c0-3.7,2.3-6,6.1-6c1.4,0,2.3,0.2,3.2,0.5V13 c-0.8-0.3-2-0.6-3.2-0.6c-1.7,0-3.2,0.9-3.2,3.6c0,2.9,1.5,3.8,3.3,3.8c0.9,0,1.9-0.2,3.2-0.7V21.3z"}),(0,c.jsx)("path",{d:"M110,15.2c0.2-0.3,0.2-0.8,3.8-5.2h3.7l-4.6,5.7l5,6.3h-3.7l-4.2-5.8V22h-3V6h3V15.2z"}),(0,c.jsx)("path",{d:"M58.5,21.3c-1.5,0.5-2.7,0.6-4.2,0.6c-3.6,0-5.8-1.8-5.8-6c0-3.1,1.9-5.9,5.5-5.9s4.9,2.5,4.9,4.9c0,0.8,0,1.5-0.1,2h-7.3 c0.1,2.5,1.5,2.8,3.6,2.8c1.1,0,2.2-0.3,3.4-0.7C58.5,19,58.5,21.3,58.5,21.3z M56,15c0-1.4-0.5-2.9-2-2.9c-1.4,0-2.3,1.3-2.4,2.9 C51.6,15,56,15,56,15z"})]})]})}},7143:e=>{"use strict";e.exports=window.wp.data},7202:()=>{},7425:(e,n,t)=>{"use strict";t.d(n,{Ay:()=>l});var o=t(2231),s=t(1609),c=t(110),i=t(3370),r=t(790);const a=(0,s.forwardRef)(({variant:e="body",children:n,component:t,className:a,...l},d)=>{const p=t||c.Q[e]||"span",u=(0,s.useMemo)(()=>c.Z.reduce((e,n)=>(void 0!==l[n]&&(e+=i.A[`${n}-${l[n]}`]+" ",delete l[n]),e),""),[l]);return(0,r.jsx)(p,{className:(0,o.A)(i.A.reset,i.A[e],a,u),...l,ref:d,children:n})});a.displayName="Text";const l=a},7448:(e,n,t)=>{e.exports=function(e){function n(e){let t,s,c,i=null;function r(...e){if(!r.enabled)return;const o=r,s=Number(new Date),c=s-(t||s);o.diff=c,o.prev=t,o.curr=s,t=s,e[0]=n.coerce(e[0]),"string"!=typeof e[0]&&e.unshift("%O");let i=0;e[0]=e[0].replace(/%([a-zA-Z%])/g,(t,s)=>{if("%%"===t)return"%";i++;const c=n.formatters[s];if("function"==typeof c){const n=e[i];t=c.call(o,n),e.splice(i,1),i--}return t}),n.formatArgs.call(o,e);(o.log||n.log).apply(o,e)}return r.namespace=e,r.useColors=n.useColors(),r.color=n.selectColor(e),r.extend=o,r.destroy=n.destroy,Object.defineProperty(r,"enabled",{enumerable:!0,configurable:!1,get:()=>null!==i?i:(s!==n.namespaces&&(s=n.namespaces,c=n.enabled(e)),c),set:e=>{i=e}}),"function"==typeof n.init&&n.init(r),r}function o(e,t){const o=n(this.namespace+(void 0===t?":":t)+e);return o.log=this.log,o}function s(e,n){let t=0,o=0,s=-1,c=0;for(;t<e.length;)if(o<n.length&&(n[o]===e[t]||"*"===n[o]))"*"===n[o]?(s=o,c=t,o++):(t++,o++);else{if(-1===s)return!1;o=s+1,c++,t=c}for(;o<n.length&&"*"===n[o];)o++;return o===n.length}return n.debug=n,n.default=n,n.coerce=function(e){if(e instanceof Error)return e.stack||e.message;return e},n.disable=function(){const e=[...n.names,...n.skips.map(e=>"-"+e)].join(",");return n.enable(""),e},n.enable=function(e){n.save(e),n.namespaces=e,n.names=[],n.skips=[];const t=("string"==typeof e?e:"").trim().replace(/\s+/g,",").split(",").filter(Boolean);for(const e of t)"-"===e[0]?n.skips.push(e.slice(1)):n.names.push(e)},n.enabled=function(e){for(const t of n.skips)if(s(e,t))return!1;for(const t of n.names)if(s(e,t))return!0;return!1},n.humanize=t(3594),n.destroy=function(){console.warn("Instance method `debug.destroy()` is deprecated and no longer does anything. It will be removed in the next major version of `debug`.")},Object.keys(e).forEach(t=>{n[t]=e[t]}),n.names=[],n.skips=[],n.formatters={},n.selectColor=function(e){let t=0;for(let n=0;n<e.length;n++)t=(t<<5)-t+e.charCodeAt(n),t|=0;return n.colors[Math.abs(t)%n.colors.length]},n.enable(n.load()),n}},7499:(e,n,t)=>{"use strict";t.d(n,{A:()=>r});var o=t(3619),s=t.n(o),c=(t(177),t(790));const i=e=>{const{title:n,value:t,description:o}=e;return(0,c.jsx)("div",{className:"jp-connection__disconnect-card card",children:(0,c.jsxs)("div",{className:"jp-connection__disconnect-card__card-content",children:[(0,c.jsx)("p",{className:"jp-connection__disconnect-card__card-headline",children:n}),(t||o)&&(0,c.jsxs)("div",{className:"jp-connection__disconnect-card__card-stat-block",children:[(0,c.jsx)("span",{className:"jp-connection__disconnect-card__card-stat",children:t}),(0,c.jsx)("div",{className:"jp-connection__disconnect-card__card-description",children:o})]})]})})};i.propTypes={title:s().string,value:s().oneOfType([s().string,s().number]),description:s().string};const r=i},7556:()=>{},7627:()=>{},7723:e=>{"use strict";e.exports=window.wp.i18n},7750:(e,n,t)=>{"use strict";t.d(n,{A:()=>s});var o=t(6087);const s=(0,o.forwardRef)(({icon:e,size:n=24,...t},s)=>(0,o.cloneElement)(e,{width:n,height:n,...t,ref:s}))},7773:(e,n,t)=>{"use strict";t.d(n,{A:()=>o});const o={placeholder:"NisihrgiIKl_knpYJtfg",pulse:"R2i0K45dEF157drbVRPI"}},7840:(e,n,t)=>{"use strict";t.d(n,{A:()=>a});var o=t(5932),s=t(3619),c=t.n(s),i=t(1609);const r=e=>{const{redirectFunc:n=e=>window.location.assign(e),connectUrl:t,redirectUri:s=null,from:c}=e,[r,a]=(0,i.useState)(null);return t&&t!==r&&a(t),(0,i.useEffect)(()=>{r||o.Ay.fetchAuthorizationUrl(s).then(e=>a(e.authorizeUrl)).catch(e=>{throw e})},[]),r?(n(r+(c?(r.includes("?")?"&":"?")+"from="+encodeURIComponent(c):"")),null):null};r.propTypes={connectUrl:c().string,redirectUri:c().string.isRequired,from:c().string,redirectFunc:c().func};const a=r},7938:(e,n,t)=>{"use strict";function o(e){return window.location.assign(e)}t.d(n,{d:()=>o})},7945:(e,n,t)=>{"use strict";t.d(n,{A:()=>p});var o=t(7723),s=t(3619),c=t.n(s),i=t(2558),r=t(9660),a=t(401),l=t(790);const __=o.__,d=e=>{const{title:n=__("Over 5 million WordPress sites are faster and more secure","jetpack-connection"),autoTrigger:t=!1,buttonLabel:o=__("Set up Jetpack","jetpack-connection"),apiRoot:s,apiNonce:c,registrationNonce:d,from:p,redirectUri:u,children:m,priceBefore:g,priceAfter:h,pricingIcon:f,pricingTitle:_,pricingCurrencyCode:y="USD",wpcomProductSlug:b,siteProductAvailabilityHandler:j,logo:k,rna:C=!1}=e,{handleRegisterSite:v,siteIsRegistering:w,userIsConnecting:x,registrationError:A,isOfflineMode:N}=(0,r.A)({registrationNonce:d,redirectUri:u,apiRoot:s,apiNonce:c,autoTrigger:t,from:p}),S=b||"",{run:E,hasCheckoutStarted:F}=(0,i.A)({productSlug:S,redirectUrl:u,siteProductAvailabilityHandler:j,from:p}),T=Boolean(A),O=w||x||F,L=S?E:v;return(0,l.jsx)(a.A,{title:n,buttonLabel:o,priceBefore:g,priceAfter:h,pricingIcon:f,pricingTitle:_,pricingCurrencyCode:y,handleButtonClick:L,displayButtonError:T,buttonIsLoading:O,logo:k,isOfflineMode:N,rna:C,children:m})};d.propTypes={title:c().string,buttonLabel:c().string,apiRoot:c().string.isRequired,apiNonce:c().string.isRequired,registrationNonce:c().string.isRequired,from:c().string,redirectUri:c().string.isRequired,autoTrigger:c().bool,pricingTitle:c().string.isRequired,pricingIcon:c().oneOfType([c().string,c().element]),priceBefore:c().number.isRequired,priceAfter:c().number.isRequired,pricingCurrencyCode:c().string,wpcomProductSlug:c().string,checkSiteHasWpcomProduct:c().func,logo:c().element};const p=d},7999:e=>{"use strict";e.exports=window.JetpackScriptDataModule},8019:(e,n,t)=>{"use strict";t.d(n,{A:()=>i});var o=t(7143),s=t(3935),c=t(2279);const i={...{getAuthorizationUrl:{isFulfilled:(e,...n)=>{const t=Boolean(e.authorizationUrl),s=(0,o.select)(c.A).hasFinishedResolution("getAuthorizationUrl",n);return t&&!s&&(0,o.dispatch)(c.A).finishResolution("getAuthorizationUrl",n),t},*fulfill(e){const n=yield s.Ay.fetchAuthorizationUrl(e);yield s.Ay.setAuthorizationUrl(n.authorizeUrl)}}}}},8089:(e,n,t)=>{"use strict";t.d(n,{A:()=>u});var o=t(6427),s=t(7723),c=t(2231),i=t(3619),r=t.n(i),a=t(1112),l=t(6854),d=t(790);const __=s.__,p=e=>{const{label:n,onClick:t,isLoading:s=!1,loadingText:i,isDisabled:r,displayError:p=!1,errorMessage:u=__("An error occurred. Please try again.","jetpack-connection"),variant:m="primary",isExternalLink:g=!1,customClass:h}=e,f=i||(0,d.jsx)(o.Spinner,{});return(0,d.jsxs)(d.Fragment,{children:[(0,d.jsx)(a.A,{className:(0,c.A)(l.A.button,"jp-action-button--button",h),label:n,onClick:t,variant:g?"link":m,isExternalLink:g,disabled:s||r,children:s?f:n}),p&&(0,d.jsx)("p",{className:(0,c.A)(l.A.error,"jp-action-button__error"),children:u})]})};p.propTypes={label:r().string.isRequired,onClick:r().func,isLoading:r().bool,isDisabled:r().bool,displayError:r().bool,errorMessage:r().oneOfType([r().string,r().element]),variant:r().oneOf(["primary","secondary","link"]),isExternalLink:r().bool,customClass:r().string,loadingText:r().oneOfType([r().string,r().element])};const u=p},8090:(e,n,t)=>{"use strict";t.d(n,{A:()=>u});var o=t(9121),s=t(6427),c=t(6087),i=t(7723),r=t(3619),a=t.n(r),l=t(9362),d=t(790);const __=i.__,p=e=>{const{onExit:n}=e;return(0,d.jsxs)("div",{className:"jp-connection__disconnect-dialog__content",children:[(0,d.jsx)(o.A,{format:"vertical",imageUrl:l}),(0,d.jsxs)("div",{className:"jp-connection__disconnect-dialog__copy",children:[(0,d.jsx)("h1",{children:__("Thank you!","jetpack-connection")}),(0,d.jsx)("p",{className:"jp-connection__disconnect-dialog__large-text",children:(0,c.createInterpolateElement)(__("Your answer has been submitted. <br/>Thanks for your input on how we can improve Jetpack.","jetpack-connection"),{br:(0,d.jsx)("br",{})})}),(0,d.jsx)(s.Button,{variant:"primary",onClick:n,className:"jp-connection__disconnect-dialog__btn-back-to-wp",children:__("Back to my website","jetpack-connection")})]})]})};p.PropTypes={onExit:a().func,assetBaseUrl:a().string};const u=p},8233:(e,n,t)=>{"use strict";t.d(n,{A:()=>c});var o=t(1609),s=(t(7556),t(790));const c=e=>{const{id:n,onClick:t,onKeyDown:c,children:i,className:r}=e,a=(0,o.useCallback)(()=>{t(n)},[n,t]),l=(0,o.useCallback)(e=>{c(n,e)},[n,c]);return(0,s.jsx)("div",{tabIndex:"0",role:"button",onClick:a,onKeyDown:l,className:"card jp-connect__disconnect-survey-card "+r,children:i})}},8343:(e,n,t)=>{"use strict";t.d(n,{z:()=>c});var o=t(7999),s=t(3832);function c(e={}){const{redirect_url:n,from:t,skip_pricing:c=!0}=e;return(0,s.addQueryArgs)((0,o.getJetpackAdminPageUrl)(),{connect_url_redirect:1,redirect_after_auth:n??(0,o.getMyJetpackUrl)(),from:t??"my-jetpack",skip_pricing:c})}},8391:(e,n,t)=>{"use strict";t.d(n,{A:()=>c});var o=t(5573),s=t(790);const c=(0,s.jsx)(o.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,s.jsx)(o.Path,{d:"M19.5 4.5h-7V6h4.44l-5.97 5.97 1.06 1.06L18 7.06v4.44h1.5v-7Zm-13 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-3H17v3a.5.5 0 0 1-.5.5h-10a.5.5 0 0 1-.5-.5v-10a.5.5 0 0 1 .5-.5h3V5.5h-3Z"})})},8421:(e,n,t)=>{"use strict";t.d(n,{A:()=>d});var o=t(8089),s=t(7723),c=t(3619),i=t.n(c),r=t(9660),a=t(790);const __=s.__,l=e=>{const{apiRoot:n,apiNonce:t,connectLabel:s=__("Connect","jetpack-connection"),registrationNonce:c,redirectUri:i=null,from:l,autoTrigger:d=!1}=e,{handleRegisterSite:p,isRegistered:u,isUserConnected:m,siteIsRegistering:g,userIsConnecting:h,registrationError:f}=(0,r.A)({registrationNonce:c,redirectUri:i,apiRoot:n,apiNonce:t,autoTrigger:d,from:l});return(0,a.jsx)(a.Fragment,{children:(!u||!m)&&(0,a.jsx)(o.A,{label:s,onClick:p,displayError:!!f,isLoading:g||h})})};l.propTypes={connectLabel:i().string,apiRoot:i().string.isRequired,apiNonce:i().string.isRequired,from:i().string,redirectUri:i().string.isRequired,registrationNonce:i().string.isRequired,autoTrigger:i().bool};const d=l},8443:e=>{"use strict";e.exports=window.wp.date},8503:(e,n,t)=>{"use strict";t.d(n,{A:()=>u});var o=t(9121),s=t(6427),c=t(6087),i=t(7723),r=t(3619),a=t.n(r),l=t(2365),d=t(790);const __=i.__,p=e=>{const{onExit:n,canProvideFeedback:t,onProvideFeedback:i}=e;return(0,d.jsxs)("div",{className:"jp-connection__disconnect-dialog__content",children:[(0,d.jsx)(o.A,{icon:"unlink",imageUrl:l}),(0,d.jsxs)("div",{className:"jp-connection__disconnect-dialog__step-copy jp-connection__disconnect-dialog__step-copy--narrow",children:[(0,d.jsx)("h1",{children:(0,c.createInterpolateElement)(__("Jetpack has been <br/>successfully disconnected.","jetpack-connection"),{br:(0,d.jsx)("br",{})})}),t&&(0,d.jsxs)(d.Fragment,{children:[(0,d.jsx)("p",{children:__("We’re sorry to see you go. Here at Jetpack, we’re always striving to provide the best experience for our customers. Please take our short survey (2 minutes, promise).","jetpack-connection")}),(0,d.jsx)("p",{children:(0,d.jsx)(s.Button,{variant:"primary",onClick:i,className:"jp-connection__disconnect-dialog__btn-back-to-wp",children:__("Help us improve","jetpack-connection")})}),(0,d.jsx)("a",{className:"jp-connection__disconnect-dialog__link jp-connection__disconnect-dialog__link--bold",href:"#",onClick:n,children:__("No thank you","jetpack-connection")})]}),!t&&(0,d.jsx)(d.Fragment,{children:(0,d.jsx)("p",{children:(0,d.jsx)(s.Button,{variant:"primary",onClick:n,className:"jp-connection__disconnect-dialog__btn-back-to-wp",children:__("Back to my website","jetpack-connection")})})})]})]})};p.propTypes={onExit:a().func,onProvideFeedback:a().func,canProvideFeedback:a().bool};const u=p},8734:(e,n,t)=>{"use strict";t.d(n,{A:()=>c});var o=t(7143);class s{static store=null;static mayBeInit(e,n){null===s.store&&(s.store=(0,o.createReduxStore)(e,n),(0,o.register)(s.store))}}const c=s},8980:(e,n,t)=>{"use strict";t.d(n,{AY:()=>g.A,F0:()=>o.A,Hx:()=>y.a,JC:()=>l.A,Jl:()=>s.A,Ni:()=>u.A,Ob:()=>j.A,Rc:()=>d.R,Sx:()=>d.A,ag:()=>f.A,bo:()=>p.A,cS:()=>b.A,d1:()=>h.A,mX:()=>a.A,nM:()=>c.A,pK:()=>i.A,w5:()=>m.A,xW:()=>r.A,zL:()=>_.z});var o=t(6212),s=t(2668),c=t(7945),i=t(8421),r=t(7018),a=t(7840),l=t(7088),d=t(9628),p=t(3269),u=t(7499),m=t(9660),g=t(4981),h=t(4617),f=t(3765),_=t(8343),y=t(4293),b=t(2558),j=t(1713)},9074:e=>{"use strict";e.exports={consumer_slug:"connection_package"}},9121:(e,n,t)=>{"use strict";t.d(n,{A:()=>s});t(2990);var o=t(790);const s=({format:e="horizontal",icon:n,imageUrl:t})=>(0,o.jsxs)("div",{className:"jp-components__decorative-card "+(e?"jp-components__decorative-card--"+e:""),children:[(0,o.jsx)("div",{className:"jp-components__decorative-card__image",style:{backgroundImage:t?`url( ${t} )`:""}}),(0,o.jsx)("div",{className:"jp-components__decorative-card__content",children:(0,o.jsx)("div",{className:"jp-components__decorative-card__lines"})}),n?(0,o.jsx)("div",{className:"jp-components__decorative-card__icon-container",children:(0,o.jsx)("span",{className:"jp-components__decorative-card__icon jp-components__decorative-card__icon--"+n})}):null]})},9362:(e,n,t)=>{"use strict";e.exports=t.p+"images/disconnect-thanks-5873bfac56a9bd7322cd.jpg"},9628:(e,n,t)=>{"use strict";t.d(n,{A:()=>a,R:()=>l});var o=t(7723),s=t(7088),c=t(9660),i=t(1713),r=t(790);const __=o.__;function a(){const{connectionErrors:e}=(0,c.A)({}),n=Object.values(e).shift(),t=n&&Object.values(n).length&&Object.values(n).shift(),o=t&&t.error_message;return{hasConnectionError:Boolean(o),connectionErrorMessage:o,connectionError:t,connectionErrors:e}}const l=({actionHandlers:e={},trackingCallback:n=null,customActions:t=null}={})=>{const{hasConnectionError:o,connectionErrorMessage:c,connectionError:l}=a(),{restoreConnection:d,isRestoringConnection:p,restoreConnectionError:u}=(0,i.A)();if(!o)return null;let m=[];if(t)try{m=t(l,{restoreConnection:d,isRestoringConnection:p})}catch{m=[]}else{const t=l?.error_data||{},o=t.action,s=e[o];if(o&&s){const e=t.action_label||__("Take Action","jetpack-connection"),o=t.action_variant||"primary",c=t.tracking_event;m=[{label:e,onClick:()=>{try{n&&c&&n(c,{}),s(l)}catch{}},variant:o}]}else if(t.action_url&&t.action_label){const e=t.action_label,o=t.action_variant||"primary",s=t.tracking_event;m=[{label:e,onClick:()=>{try{n&&s&&n(s,{}),window.location.href=t.action_url}catch{}},variant:o}]}else m=[{label:__("Restore Connection","jetpack-connection"),onClick:()=>{try{n&&n("jetpack_connection_error_notice_reconnect_cta_click",{}),d()}catch{}},isLoading:p,loadingText:__("Reconnecting Jetpack…","jetpack-connection")}];if(m.length>0&&(o||t.action_url)){const o=t.secondary_action,s=e[o],c=t.secondary_action_url,i=t.secondary_action_label;if(o&&s&&i){const e=t.secondary_action_variant||"secondary",o=t.secondary_tracking_event;m.push({label:i,onClick:()=>{try{n&&o&&n(o,{}),s(l)}catch{}},variant:e})}else if(c&&i){const e=t.secondary_action_variant||"secondary",o=t.secondary_tracking_event;m.push({label:i,onClick:()=>{try{n&&o&&n(o,{}),window.location.href=c}catch{}},variant:e})}}}return 0!==m.length||t?(0,r.jsx)(s.A,{isRestoringConnection:p,restoreConnectionError:u,restoreConnectionCallback:0===m.length?d:null,message:c,actions:m}):null}},9660:(e,n,t)=>{"use strict";t.d(n,{A:()=>l});var o=t(5932),s=t(7999),c=t(7143),i=t(1609),r=t(4293);const a=window?.JP_CONNECTION_INITIAL_STATE||(0,s.getScriptData)()?.connection||{};function l({registrationNonce:e=a.registrationNonce,apiRoot:n=a.apiRoot,apiNonce:t=a.apiNonce,redirectUri:s,autoTrigger:l,from:d,skipUserConnection:p,skipPricingPage:u}={}){const{registerSite:m,connectUser:g,refreshConnectedPlugins:h}=(0,c.useDispatch)(r.a),f=(0,c.useSelect)(e=>e(r.a).getRegistrationError()),{siteIsRegistering:_,userIsConnecting:y,userConnectionData:b,connectedPlugins:j,connectionErrors:k,isRegistered:C,isUserConnected:v,hasConnectedOwner:w,isOfflineMode:x}=(0,c.useSelect)(e=>({siteIsRegistering:e(r.a).getSiteIsRegistering(),userIsConnecting:e(r.a).getUserIsConnecting(),userConnectionData:e(r.a).getUserConnectionData(),connectedPlugins:e(r.a).getConnectedPlugins(),connectionErrors:e(r.a).getConnectionErrors(),isOfflineMode:e(r.a).getIsOfflineMode(),...e(r.a).getConnectionStatus()})),A=()=>p?s?(window.location=s,Promise.resolve(s)):Promise.resolve():g({from:d,redirectUri:s,skipPricingPage:u}),N=n=>(n&&n.preventDefault(),C?A():m({registrationNonce:e,redirectUri:s,from:d}).then(()=>A()));return(0,i.useEffect)(()=>{o.Ay.setApiRoot(n),o.Ay.setApiNonce(t)},[n,t]),(0,i.useEffect)(()=>{!l||_||y||N()},[]),{handleRegisterSite:N,handleConnectUser:A,refreshConnectedPlugins:h,isRegistered:C,isUserConnected:v,siteIsRegistering:_,userIsConnecting:y,registrationError:f,userConnectionData:b,hasConnectedOwner:w,connectedPlugins:j,connectionErrors:k,isOfflineMode:x}}},9910:(e,n,t)=>{"use strict";t.d(n,{A:()=>o});const o={heading:"urouayitSUT8zW0V3p_0",notice:"iXXJlk08gFDeCvsTTlNQ",button:"MWqRqr7q6fgvLxitcWYk",primary:"qExIAscWqEKaVy2cSTqb",secondary:"fh6gO3o64bHDWoktxV1A",actions:"ix1awakl4n7EjEZdShcd",error:"e6hHy8BZ7ZKPSXbIC0UG",message:"jXz8LnXNzMDdtHqkG0sZ"}},9957:(e,n,t)=>{"use strict";t.d(n,{A:()=>d});var o=t(4268),s=t(6427),c=t(7723),i=t(1876),r=t(5879),a=(t(5404),t(790));const __=c.__,l=e=>-1===e.fraction.indexOf("00"),d=({currencyCode:e="USD",priceDetails:n=__("/month, paid yearly","jetpack-connection"),...t})=>{const d=(0,o.vA)(t.priceBefore,e),p=(0,o.vA)(t.priceAfter,e);return(0,a.jsxs)("div",{className:"jp-components__pricing-card",children:[t.icon&&(0,a.jsx)("div",{className:"jp-components__pricing-card__icon",children:"string"==typeof t.icon?(0,a.jsx)("img",{src:t.icon,alt:(0,c.sprintf)(/* translators: %s: the product name */ __("Icon for the product %s","jetpack-connection"),t.title)}):t.icon}),(0,a.jsx)("h1",{className:"jp-components__pricing-card__title",children:t.title}),(0,a.jsxs)("div",{className:"jp-components__pricing-card__pricing",children:[0===t.priceAfter&&(0,a.jsx)(i.A,{width:"100%",height:48}),t.priceBefore!==t.priceAfter&&t.priceAfter>0&&(0,a.jsxs)("div",{className:"jp-components__pricing-card__price-before",children:[(0,a.jsx)("span",{className:"jp-components__pricing-card__currency",children:d.symbol}),(0,a.jsx)("span",{className:"jp-components__pricing-card__price",children:d.integer}),l(d)&&(0,a.jsxs)("span",{className:"jp-components__pricing-card__price-decimal",children:[" ",d.fraction]}),(0,a.jsx)("div",{className:"jp-components__pricing-card__price-strikethrough"})]}),t.priceAfter>0&&(0,a.jsxs)(a.Fragment,{children:[(0,a.jsxs)("div",{className:"jp-components__pricing-card__price-after",children:[(0,a.jsx)("span",{className:"jp-components__pricing-card__currency",children:p.symbol}),(0,a.jsx)("span",{className:"jp-components__pricing-card__price",children:p.integer}),l(p)&&(0,a.jsx)("span",{className:"jp-components__pricing-card__price-decimal",children:p.fraction})]}),(0,a.jsx)("span",{className:"jp-components__pricing-card__price-details",children:n})]})]}),t.children&&(0,a.jsx)("div",{className:"jp-components__pricing-card__extra-content-wrapper",children:t.children}),t.tosText&&(0,a.jsx)("div",{className:"jp-components__pricing-card__tos",children:t.tosText}),t.ctaText&&(0,a.jsxs)(a.Fragment,{children:[!t.tosText&&(0,a.jsx)("div",{className:"jp-components__pricing-card__tos",children:(0,a.jsx)(r.A,{agreeButtonLabel:t.ctaText})}),(0,a.jsx)("div",{className:"jp-components__pricing-card__cta",children:(0,a.jsx)(s.Button,{className:"jp-components__pricing-card__button",label:t.ctaText,onClick:t.onCtaClick,children:t.ctaText})})]}),t.infoText&&(0,a.jsx)("div",{className:"jp-components__pricing-card__info",children:t.infoText})]})}},9980:(e,n,t)=>{"use strict";t.d(n,{u:()=>h,v:()=>f});var o=t(2423),s=t.n(o),c=t(3673),i=t(3328),r=t(6673);const a=s()("number-formatters:number-format-currency");function l(e,n){return"USD"===e&&n&&""!==n&&"US"!==n?{symbol:"US$"}:r.a[e]}function d(e,n){return l(e,n)?e:(a(`getValidCurrency was called with a non-existent currency "${e}"; falling back to ${c.$}`),c.$)}function p({number:e,currency:n,browserSafeLocale:t,forceLatin:o=!0,stripZeros:s,signForPositive:c}){const r=`${t}${o?"-u-nu-latn":""}`,a={style:"currency",currency:n,...s&&Number.isInteger(e)&&{maximumFractionDigits:0,minimumFractionDigits:0},...c&&{signDisplay:"exceptZero"}};return(0,i.J)({locale:r,options:a})}function u(e,n,t){return p({number:0,currency:n,browserSafeLocale:e,forceLatin:t}).resolvedOptions().maximumFractionDigits}function m(e,n){const t=Math.pow(10,n);return Math.round(e*t)/t}function g(e,n,t){if(isNaN(e))return a("formatCurrency was called with NaN"),0;if(t){Number.isInteger(e)||a("formatCurrency was called with isSmallestUnit and a float which will be rounded",e);const t=10**n;return m(Math.round(e)/t,n)}return m(e,n)}const h=({number:e,browserSafeLocale:n,currency:t,stripZeros:o,isSmallestUnit:s,signForPositive:c,geoLocation:i,forceLatin:r})=>{const a=d(t,i),m=l(a,i),h=u(n,a,r);if(s&&void 0===h)throw new Error(`Could not determine currency precision for ${a} in ${n}`);const f=g(e,h??0,s);return p({number:f,currency:a,browserSafeLocale:n,forceLatin:r,stripZeros:o,signForPositive:c}).formatToParts(f).reduce((e,n)=>"currency"===n.type&&m?.symbol?e+m.symbol:e+n.value,"")},f=({number:e,browserSafeLocale:n,currency:t,stripZeros:o,isSmallestUnit:s,signForPositive:c,geoLocation:i,forceLatin:r})=>{const a=d(t,i),m=l(a,i),h=g(e,u(n,a,r)??0,s),f=p({number:h,currency:a,browserSafeLocale:n,forceLatin:r,stripZeros:o,signForPositive:c}).formatToParts(h);let _="",y="$",b="before",j=!1,k=!1,C="",v="";f.forEach(e=>{switch(e.type){case"currency":return y=m?.symbol??e.value,void(j&&(b="after"));case"group":case"integer":return C+=e.value,void(j=!0);case"decimal":case"fraction":return v+=e.value,j=!0,void(k=!0);case"minusSign":return void(_="-");case"plusSign":_="+"}});const w=!Number.isInteger(h)&&k;return{sign:_,symbol:y,symbolPosition:b,integer:C,fraction:v,hasNonZeroFraction:w}}}},n={};function t(o){var s=n[o];if(void 0!==s)return s.exports;var c=n[o]={exports:{}};return e[o](c,c.exports,t),c.exports}t.n=e=>{var n=e&&e.__esModule?()=>e.default:()=>e;return t.d(n,{a:n}),n},t.d=(e,n)=>{for(var o in n)t.o(n,o)&&!t.o(e,o)&&Object.defineProperty(e,o,{enumerable:!0,get:n[o]})},t.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),t.o=(e,n)=>Object.prototype.hasOwnProperty.call(e,n),t.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},(()=>{var e;t.g.importScripts&&(e=t.g.location+"");var n=t.g.document;if(!e&&n&&(n.currentScript&&"SCRIPT"===n.currentScript.tagName.toUpperCase()&&(e=n.currentScript.src),!e)){var o=n.getElementsByTagName("script");if(o.length)for(var s=o.length-1;s>-1&&(!e||!/^http(s?):/.test(e));)e=o[s--].src}if(!e)throw new Error("Automatic publicPath is not supported in this browser");e=e.replace(/^blob:/,"").replace(/#.*$/,"").replace(/\?.*$/,"").replace(/\/[^\/]+$/,"/"),t.p=e})();var o={};return(()=>{"use strict";t.r(o),t.d(o,{CONNECTION_STORE_ID:()=>e.Hx,ConnectButton:()=>e.pK,ConnectScreen:()=>e.F0,ConnectScreenLayout:()=>e.Jl,ConnectScreenRequiredPlan:()=>e.nM,ConnectUser:()=>e.mX,ConnectionError:()=>e.Rc,ConnectionErrorNotice:()=>e.JC,DisconnectCard:()=>e.Ni,DisconnectDialog:()=>e.bo,InPlaceConnection:()=>e.xW,ManageConnectionDialog:()=>e.AY,getCalypsoOrigin:()=>e.ag,getUserConnectionUrl:()=>e.zL,thirdPartyCookiesFallbackHelper:()=>e.d1,useConnection:()=>e.w5,useConnectionErrorNotice:()=>e.Sx,useProductCheckoutWorkflow:()=>e.cS,useRestoreConnection:()=>e.Ob});var e=t(8980)})(),o})()); jetpack-connection/dist/jetpack-sso-users.asset.php 0000777 00000000141 15251741406 0016506 0 ustar 00 <?php return array('dependencies' => array('wp-polyfill'), 'version' => '9b04da34a7196789f44c'); jetpack-connection/dist/jetpack-users-connection.asset.php 0000777 00000000141 15251741406 0020041 0 ustar 00 <?php return array('dependencies' => array('wp-polyfill'), 'version' => '2a9e9767fedce3c596e7'); jetpack-connection/dist/jetpack-sso-admin-create-user.js 0000777 00000001116 15251741406 0017364 0 ustar 00 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"))}); jetpack-connection/dist/jetpack-sso-login.css 0000777 00000004266 15251741406 0015354 0 ustar 00 #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} jetpack-connection/dist/jetpack-connection.asset.php 0000777 00000000361 15251741406 0016706 0 ustar 00 <?php return array('dependencies' => array('jetpack-script-data', 'react', 'react-jsx-runtime', 'wp-components', 'wp-data', 'wp-date', 'wp-element', 'wp-i18n', 'wp-polyfill', 'wp-primitives', 'wp-url'), 'version' => '9ae5525b23be7fff1159'); jetpack-connection/dist/jetpack-sso-admin-create-user.rtl.css 0000777 00000000464 15251741406 0020345 0 ustar 00 .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} jetpack-connection/dist/identity-crisis.js 0000777 00000140364 15251741406 0014772 0 ustar 00 (()=>{var e={372:(e,t,n)=>{"use strict";n.d(t,{A:()=>c});var s=n(4804);const r=n.n(s)()("dops:analytics");let o,i;window._tkq=window._tkq||[],window.ga=window.ga||function(){(window.ga.q=window.ga.q||[]).push(arguments)},window.ga.l=+new Date;const a={initialize:function(e,t,n){a.setUser(e,t),a.setSuperProps(n),a.identifyUser()},setGoogleAnalyticsEnabled:function(e,t=null){this.googleAnalyticsEnabled=e,this.googleAnalyticsKey=t},setMcAnalyticsEnabled:function(e){this.mcAnalyticsEnabled=e},setUser:function(e,t){i={ID:e,username:t}},setSuperProps:function(e){o=e},assignSuperProps:function(e){o=Object.assign(o||{},e)},mc:{bumpStat:function(e,t){const n=function(e,t){let n="";if("object"==typeof e){for(const t in e)n+="&x_"+encodeURIComponent(t)+"="+encodeURIComponent(e[t]);r("Bumping stats %o",e)}else n="&x_"+encodeURIComponent(e)+"="+encodeURIComponent(t),r('Bumping stat "%s" in group "%s"',t,e);return n}(e,t);a.mcAnalyticsEnabled&&((new Image).src=document.location.protocol+"//pixel.wp.com/g.gif?v=wpcom-no-pv"+n+"&t="+Math.random())},bumpStatWithPageView:function(e,t){const n=function(e,t){let n="";if("object"==typeof e){for(const t in e)n+="&"+encodeURIComponent(t)+"="+encodeURIComponent(e[t]);r("Built stats %o",e)}else n="&"+encodeURIComponent(e)+"="+encodeURIComponent(t),r('Built stat "%s" in group "%s"',t,e);return n}(e,t);a.mcAnalyticsEnabled&&((new Image).src=document.location.protocol+"//pixel.wp.com/g.gif?v=wpcom"+n+"&t="+Math.random())}},pageView:{record:function(e,t){a.tracks.recordPageView(e),a.ga.recordPageView(e,t)}},purchase:{record:function(e,t,n,s,r,o,i){a.ga.recordPurchase(e,t,n,s,r,o,i)}},tracks:{recordEvent:function(e,t){t=t||{},0===e.indexOf("akismet_")||0===e.indexOf("jetpack_")?(o&&(r("- Super Props: %o",o),t=Object.assign(t,o)),r('Record event "%s" called with props %s',e,JSON.stringify(t)),window._tkq.push(["recordEvent",e,t])):r('- Event name must be prefixed by "akismet_" or "jetpack_"')},recordJetpackClick:function(e){const t="object"==typeof e?e:{target:e};a.tracks.recordEvent("jetpack_wpa_click",t)},recordPageView:function(e){a.tracks.recordEvent("akismet_page_view",{path:e})},setOptOut:function(e){r("Pushing setOptOut: %o",e),window._tkq.push(["setOptOut",e])}},ga:{initialized:!1,initialize:function(){let e={};a.ga.initialized||(i&&(e={userId:"u-"+i.ID}),window.ga("create",this.googleAnalyticsKey,"auto",e),a.ga.initialized=!0)},recordPageView:function(e,t){a.ga.initialize(),r("Recording Page View ~ [URL: "+e+"] [Title: "+t+"]"),this.googleAnalyticsEnabled&&(window.ga("set","page",e),window.ga("send",{hitType:"pageview",page:e,title:t}))},recordEvent:function(e,t,n,s){a.ga.initialize();let o="Recording Event ~ [Category: "+e+"] [Action: "+t+"]";void 0!==n&&(o+=" [Option Label: "+n+"]"),void 0!==s&&(o+=" [Option Value: "+s+"]"),r(o),this.googleAnalyticsEnabled&&window.ga("send","event",e,t,n,s)},recordPurchase:function(e,t,n,s,r,o,i){window.ga("require","ecommerce"),window.ga("ecommerce:addTransaction",{id:e,revenue:s,currency:i}),window.ga("ecommerce:addItem",{id:e,name:t,sku:n,price:r,quantity:o}),window.ga("ecommerce:send")}},identifyUser:function(){i&&window._tkq.push(["identifyUser",i.ID,i.username])},setProperties:function(e){window._tkq.push(["setProperties",e])},clearedIdentity:function(){window._tkq.push(["clearIdentity"])}},c=a},790:e=>{"use strict";e.exports=window.ReactJSXRuntime},1057:(e,t,n)=>{"use strict";n.d(t,{A:()=>o});var s=n(3619),r=n.n(s);const o={headerText:r().string,logoAlt:r().string,mainTitle:r().string,mainBodyText:r().string,mainBodyTextDev:r().string,migratedTitle:r().string,migratedBodyText:r().string,migrateCardTitle:r().string,migrateButtonLabel:r().string,migrateCardBodyText:r().string,startFreshCardTitle:r().string,startFreshCardBodyText:r().string,safeModeCardBodyText:r().string,startFreshCardBodyTextDev:r().string,startFreshButtonLabel:r().string,nonAdminTitle:r().string,nonAdminBodyText:r().string,supportURL:r().string,stayInSafeModeButtonLabel:r().string}},1133:(e,t,n)=>{"use strict";n.d(t,{A:()=>C});var s=n(5932),r=n(3924),o=n(6461),i=n(6427),a=n(9491),c=n(7143),d=n(6087),l=n(7723),p=n(3832),h=n(3619),u=n.n(h),m=n(1609),g=n(3207),f=n(1057),j=n(2879),k=n(6576),y=(n(9626),n(790));const __=l.__,v=e=>(0,y.jsx)(k.A,{children:(0,d.createInterpolateElement)(__("Could not stay in safe mode. Retry or find out more <a>here</a>.","jetpack-connection"),{a:(0,y.jsx)("a",{href:e||(0,r.A)("jetpack-support-safe-mode"),rel:"noopener noreferrer",target:"_blank"})})}),_=e=>{const{isActionInProgress:t,setIsActionInProgress:n,setErrorType:a,clearErrorType:c,hasError:l=!1,customContent:h,isDevelopmentSite:u}=e,[g,f]=(0,m.useState)(!1),k=h.stayInSafeModeButtonLabel||__("Stay in Safe mode","jetpack-connection"),_=(0,m.useCallback)(()=>{t||(f(!0),n(!0),c(),(0,j.A)("confirm_safe_mode"),s.Ay.confirmIDCSafeMode().then(()=>{window.location.href=(0,p.removeQueryArgs)(window.location.href,"jetpack_idc_clear_confirmation","_wpnonce")}).catch(e=>{throw n(!1),f(!1),a("safe-mode"),e}))},[t,n,a,c]);return(0,y.jsx)(m.Fragment,{children:u?(0,y.jsxs)("div",{className:"jp-idc__idc-screen__card-action-base"+(l?" jp-idc__idc-screen__card-action-error":""),children:[(0,y.jsxs)("div",{className:"jp-idc__idc-screen__card-action-top",children:[(0,y.jsx)("h4",{children:h.safeModeTitle?(0,d.createInterpolateElement)(h.safeModeTitle,{em:(0,y.jsx)("em",{})}):__("Stay in Safe Mode","jetpack-connection")}),(0,y.jsx)("div",{children:(0,d.createInterpolateElement)(h.safeModeCardBodyText||/* translators: %1$s: The current site domain name. %2$s: The original site domain name. */ __("<p><strong>Recommended for</strong></p><list><item>short-lived test sites</item><item>sites that will be cloned back to production after testing</item></list><p><strong>Please note</strong> that staying in Safe mode will disable some Jetpack features, including security features such as SSO, firewall, and site monitor. <safeModeLink>Learn more</safeModeLink>.</p>","jetpack-connection"),{p:(0,y.jsx)("p",{}),hostname:(0,y.jsx)("strong",{}),em:(0,y.jsx)("em",{}),strong:(0,y.jsx)("strong",{}),list:(0,y.jsx)("ul",{}),item:(0,y.jsx)("li",{}),safeModeLink:(0,y.jsx)("a",{href:h.supportURL||(0,r.A)("jetpack-support-safe-mode"),rel:"noopener noreferrer",target:"_blank"})})})]}),(0,y.jsxs)("div",{className:"jp-idc__idc-screen__card-action-bottom",children:[(0,y.jsx)(i.Button,{className:"jp-idc__idc-screen__card-action-button-secondary",label:k,onClick:_,disabled:t,children:g?(0,y.jsx)(o.A,{color:"black"}):k}),l&&v(h.supportURL)]})]}):(0,y.jsxs)("div",{className:"jp-idc__safe-mode",children:[g?(0,y.jsxs)("div",{className:"jp-idc__safe-mode__staying-safe",children:[(0,y.jsx)(o.A,{color:"black"}),(0,y.jsx)("span",{children:__("Finishing setting up Safe mode…","jetpack-connection")})]}):(C=_,b=t,(0,d.createInterpolateElement)(__("Or decide later and stay in <button>Safe mode</button>","jetpack-connection"),{button:(0,y.jsx)(i.Button,{label:__("Safe mode","jetpack-connection"),variant:"link",onClick:C,disabled:b})})),l&&v(h.supportURL)]})});var C,b};_.propTypes={isActionInProgress:u().bool,setIsActionInProgress:u().func.isRequired,setErrorType:u().func.isRequired,clearErrorType:u().func.isRequired,hasError:u().bool,customContent:u().shape(f.A),isDevelopmentSite:u().bool};const C=(0,a.compose)([(0,c.withSelect)(e=>({isActionInProgress:e(g.a).getIsActionInProgress()})),(0,c.withDispatch)(e=>({setIsActionInProgress:e(g.a).setIsActionInProgress,setErrorType:e(g.a).setErrorType,clearErrorType:e(g.a).clearErrorType}))])(_)},1217:(e,t,n)=>{"use strict";n.d(t,{A:()=>f});var s=n(3924),r=n(6087),o=n(7723),i=n(3619),a=n.n(i),c=n(1609),d=n(1057),l=n(6043),p=n(3685),h=n(6930),u=n(1133),m=n(790);const __=o.__,g=e=>{const{wpcomHomeUrl:t,currentUrl:n,isMigrating:i=!1,migrateCallback:a,isStartingFresh:d=!1,startFreshCallback:g,customContent:f={},hasMigrateError:j=!1,hasFreshError:k=!1,hasStaySafeError:y=!1,possibleDynamicSiteUrlDetected:v=!1,isDevelopmentSite:_}=e,C=(0,l.A)(e.wpcomHomeUrl),b=(0,l.A)(e.currentUrl);return(0,m.jsxs)(c.Fragment,{children:[(0,m.jsx)("h2",{children:f.mainTitle?(0,r.createInterpolateElement)(f.mainTitle,{em:(0,m.jsx)("em",{})}):__("Safe Mode has been activated","jetpack-connection")}),(0,m.jsx)("p",{children:_?(0,r.createInterpolateElement)(f.mainBodyTextDev||(0,o.sprintf)(/* translators: %1$s: The current site domain name. %2$s: The original site domain name. */ __("<span>Your site is in Safe Mode because <hostname>%1$s</hostname> appears to be a staging or development copy of <hostname>%2$s</hostname>.</span>Two sites that are telling Jetpack they’re the same site. <safeModeLink>Learn more or troubleshoot common Safe mode issues</safeModeLink>.","jetpack-connection"),b,C),{span:(0,m.jsx)("span",{style:{display:"block"}}),hostname:(0,m.jsx)("strong",{}),em:(0,m.jsx)("em",{}),strong:(0,m.jsx)("strong",{}),safeModeLink:(0,m.jsx)("a",{href:f.supportURL||(0,s.A)("jetpack-support-safe-mode"),rel:"noopener noreferrer",target:"_blank"})}):(0,r.createInterpolateElement)(f.mainBodyText||__("Your site is in Safe Mode because you have 2 Jetpack-powered sites that appear to be duplicates. Two sites that are telling Jetpack they’re the same site. <safeModeLink>Learn more about safe mode.</safeModeLink>","jetpack-connection"),{safeModeLink:(0,m.jsx)("a",{href:f.supportURL||(0,s.A)("jetpack-support-safe-mode"),rel:"noopener noreferrer",target:"_blank"}),em:(0,m.jsx)("em",{}),strong:(0,m.jsx)("strong",{})})}),v&&(0,m.jsx)("p",{children:(0,r.createInterpolateElement)(f.dynamicSiteUrlText||__("<strong>Notice:</strong> It appears that your 'wp-config.php' file might be using dynamic site URL values. Dynamic site URLs could cause Jetpack to enter Safe Mode. <dynamicSiteUrlSupportLink>Learn how to set a static site URL.</dynamicSiteUrlSupportLink>","jetpack-connection"),{dynamicSiteUrlSupportLink:(0,m.jsx)("a",{href:f.dynamicSiteUrlSupportLink||(0,s.A)("jetpack-idcscreen-dynamic-site-urls"),rel:"noopener noreferrer",target:"_blank"}),em:(0,m.jsx)("em",{}),strong:(0,m.jsx)("strong",{})})}),(0,m.jsx)("h3",{children:__("Please select an option","jetpack-connection")}),(0,m.jsx)("div",{className:"jp-idc__idc-screen__cards"+(j||k?" jp-idc__idc-screen__cards-error":""),children:_?(0,m.jsxs)(c.Fragment,{children:[(0,m.jsx)(p.A,{wpcomHomeUrl:t,currentUrl:n,isStartingFresh:d,startFreshCallback:g,customContent:f,hasError:k,isDevelopmentSite:_}),(0,m.jsx)("div",{className:"jp-idc__idc-screen__cards-separator",children:"or"}),(0,m.jsx)(u.A,{hasError:y,customContent:f,isDevelopmentSite:_})]}):(0,m.jsxs)(c.Fragment,{children:[(0,m.jsx)(h.A,{wpcomHomeUrl:t,currentUrl:n,isMigrating:i,migrateCallback:a,customContent:f,hasError:j}),(0,m.jsx)("div",{className:"jp-idc__idc-screen__cards-separator",children:"or"}),(0,m.jsx)(p.A,{wpcomHomeUrl:t,currentUrl:n,isStartingFresh:d,startFreshCallback:g,customContent:f,hasError:k,isDevelopmentSite:_})]})}),_?null:(0,m.jsx)(u.A,{hasError:y,customContent:f})]})};g.propTypes={wpcomHomeUrl:a().string.isRequired,currentUrl:a().string.isRequired,isMigrating:a().bool,migrateCallback:a().func,isStartingFresh:a().bool,startFreshCallback:a().func,customContent:a().shape(d.A),hasMigrateError:a().bool,hasFreshError:a().bool,hasStaySafeError:a().bool,possibleDynamicSiteUrlDetected:a().bool,isDevelopmentSite:a().bool};const f=g},1583:(e,t,n)=>{"use strict";var s=n(1752);function r(){}function o(){}o.resetWarningCache=r,e.exports=function(){function e(e,t,n,r,o,i){if(i!==s){var a=new Error("Calling PropTypes validators directly is not supported by the `prop-types` package. Use PropTypes.checkPropTypes() to call them. Read more at http://fb.me/use-check-prop-types");throw a.name="Invariant Violation",a}}function t(){return e}e.isRequired=e;var n={array:e,bigint:e,bool:e,func:e,number:e,object:e,string:e,symbol:e,any:e,arrayOf:t,element:e,elementType:e,instanceOf:t,node:e,objectOf:t,oneOf:t,oneOfType:t,shape:t,exact:t,checkPropTypes:o,resetWarningCache:r};return n.PropTypes=n,n}},1609:e=>{"use strict";e.exports=window.React},1752:e=>{"use strict";e.exports="SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED"},1908:(e,t,n)=>{"use strict";n.d(t,{A:()=>o});var s=n(7143);class r{static store=null;static mayBeInit(e,t){null===r.store&&(r.store=(0,s.createReduxStore)(e,t),(0,s.register)(r.store))}}const o=r},2093:(e,t,n)=>{"use strict";n.d(t,{A:()=>o});var s=n(7143),r=n(8269);const o=(0,s.combineReducers)({isActionInProgress:(e=!1,t)=>t.type===r.Xs?t.isInProgress:e,errorType:(e=null,t)=>{switch(t.type){case r.xj:return t.errorType;case r.sL:return null}return e}})},2144:()=>{},2145:()=>{},2231:(e,t,n)=>{"use strict";function s(e){var t,n,r="";if("string"==typeof e||"number"==typeof e)r+=e;else if("object"==typeof e)if(Array.isArray(e)){var o=e.length;for(t=0;t<o;t++)e[t]&&(n=s(e[t]))&&(r&&(r+=" "),r+=n)}else for(n in e)e[n]&&(r&&(r+=" "),r+=n);return r}n.d(t,{A:()=>r});const r=function(){for(var e,t,n=0,r="",o=arguments.length;n<o;n++)(e=arguments[n])&&(t=s(e))&&(r&&(r+=" "),r+=t);return r}},2879:(e,t,n)=>{"use strict";n.d(t,{A:()=>o,f:()=>r});var s=n(372);function r(e,t){t&&Object.hasOwn(t,"userid")&&Object.hasOwn(t,"username")&&s.A.initialize(t.userid,t.username),e&&(Object.hasOwn(e,"blogID")&&s.A.assignSuperProps({blog_id:e.blogID}),Object.hasOwn(e,"platform")&&s.A.assignSuperProps({platform:e.platform})),s.A.setMcAnalyticsEnabled(!0)}function o(e,t={}){void 0!==t&&"object"==typeof t||(t={}),e&&e.length&&void 0!==s.A&&s.A.tracks&&s.A.mc&&(e=0!==(e=e.replace(/-/g,"_")).indexOf("jetpack_idc_")?"jetpack_idc_"+e:e,s.A.tracks.recordEvent(e,t),e=(e=e.replace("jetpack_idc_","")).replace(/_/g,"-"),s.A.mc.bumpStat("jetpack-idc",e))}},3207:(e,t,n)=>{"use strict";n.d(t,{a:()=>a});var s=n(8269),r=n(2093),o=n(8918),i=n(1908);const a="jetpack-idc";i.A.mayBeInit(a,{reducer:r.A,actions:s.Ay,selectors:o.A})},3594:e=>{var t=1e3,n=60*t,s=60*n,r=24*s,o=7*r,i=365.25*r;function a(e,t,n,s){var r=t>=1.5*n;return Math.round(e/n)+" "+s+(r?"s":"")}e.exports=function(e,c){c=c||{};var d=typeof e;if("string"===d&&e.length>0)return function(e){if((e=String(e)).length>100)return;var a=/^(-?(?:\d+)?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)?$/i.exec(e);if(!a)return;var c=parseFloat(a[1]);switch((a[2]||"ms").toLowerCase()){case"years":case"year":case"yrs":case"yr":case"y":return c*i;case"weeks":case"week":case"w":return c*o;case"days":case"day":case"d":return c*r;case"hours":case"hour":case"hrs":case"hr":case"h":return c*s;case"minutes":case"minute":case"mins":case"min":case"m":return c*n;case"seconds":case"second":case"secs":case"sec":case"s":return c*t;case"milliseconds":case"millisecond":case"msecs":case"msec":case"ms":return c;default:return}}(e);if("number"===d&&isFinite(e))return c.long?function(e){var o=Math.abs(e);if(o>=r)return a(e,o,r,"day");if(o>=s)return a(e,o,s,"hour");if(o>=n)return a(e,o,n,"minute");if(o>=t)return a(e,o,t,"second");return e+" ms"}(e):function(e){var o=Math.abs(e);if(o>=r)return Math.round(e/r)+"d";if(o>=s)return Math.round(e/s)+"h";if(o>=n)return Math.round(e/n)+"m";if(o>=t)return Math.round(e/t)+"s";return e+"ms"}(e);throw new Error("val is not a non-empty string or a valid number. val="+JSON.stringify(e))}},3619:(e,t,n)=>{e.exports=n(1583)()},3685:(e,t,n)=>{"use strict";n.d(t,{A:()=>k});var s=n(3924),r=n(6461),o=n(6427),i=n(7143),a=n(6087),c=n(7723),d=n(3619),l=n.n(d),p=n(3207),h=n(1057),u=n(6043),m=n(6576),g=n(790);const __=c.__,f=(e,t)=>(0,g.jsxs)("div",{children:[(0,g.jsx)("div",{className:"jp-idc__idc-screen__card-action-sitename",children:e}),(0,g.jsx)(o.Dashicon,{icon:"minus",className:"jp-idc__idc-screen__card-action-separator"}),(0,g.jsx)("div",{className:"jp-idc__idc-screen__card-action-sitename",children:t})]}),j=e=>{const{isStartingFresh:t=!1,startFreshCallback:n=()=>{},customContent:d={},hasError:l=!1,isDevelopmentSite:h}=e,j=(0,u.A)(e.wpcomHomeUrl),k=(0,u.A)(e.currentUrl),y=(0,i.useSelect)(e=>e(p.a).getIsActionInProgress(),[]),v=d.startFreshButtonLabel||__("Create a fresh connection","jetpack-connection");return(0,g.jsxs)("div",{className:"jp-idc__idc-screen__card-action-base"+(l?" jp-idc__idc-screen__card-action-error":""),children:[(0,g.jsxs)("div",{className:"jp-idc__idc-screen__card-action-top",children:[(0,g.jsx)("h4",{children:d.startFreshCardTitle?(0,a.createInterpolateElement)(d.startFreshCardTitle,{em:(0,g.jsx)("em",{})}):__("Treat each site as independent sites","jetpack-connection")}),h?(0,g.jsx)("div",{className:"jp-idc__dev-mode-content",children:(0,a.createInterpolateElement)(d.startFreshCardBodyTextDev||(0,c.sprintf)(/* translators: %1$s: The current site domain name. %2$s: The original site domain name. */ __("<p><strong>Recommended for</strong></p><list><item>development sites</item><item>sites that need access to all Jetpack features</item></list><p><strong>Please note</strong> that creating a fresh connection for <hostname>%1$s</hostname> would require restoring the connection on <hostname>%2$s</hostname> if that site is cloned back to production. <safeModeLink>Learn more</safeModeLink>.</p>","jetpack-connection"),k,j),{p:(0,g.jsx)("p",{}),hostname:(0,g.jsx)("strong",{}),em:(0,g.jsx)("em",{}),strong:(0,g.jsx)("strong",{}),list:(0,g.jsx)("ul",{}),item:(0,g.jsx)("li",{}),safeModeLink:(0,g.jsx)("a",{href:d.supportURL||(0,s.A)("jetpack-support-safe-mode"),rel:"noopener noreferrer",target:"_blank"})})}):(0,g.jsx)("p",{children:(0,a.createInterpolateElement)(d.startFreshCardBodyText||(0,c.sprintf)(/* translators: %1$s: The current site domain name. %2$s: The original site domain name. */ __("<hostname>%1$s</hostname> settings, stats, and subscribers will start fresh. <hostname>%2$s</hostname> will keep its data as is.","jetpack-connection"),k,j),{hostname:(0,g.jsx)("strong",{}),em:(0,g.jsx)("em",{}),strong:(0,g.jsx)("strong",{})})})]}),(0,g.jsxs)("div",{className:"jp-idc__idc-screen__card-action-bottom",children:[(0,g.jsx)("div",{children:h?null:f(j,k)}),(0,g.jsx)(o.Button,{className:"jp-idc__idc-screen__card-action-button",label:v,onClick:n,disabled:y,children:t?(0,g.jsx)(r.A,{}):v}),l&&(_=d.supportURL,(0,g.jsx)(m.A,{children:(0,a.createInterpolateElement)(__("Could not create the connection. Retry or find out more <a>here</a>.","jetpack-connection"),{a:(0,g.jsx)("a",{href:_||(0,s.A)("jetpack-support-safe-mode"),rel:"noopener noreferrer",target:"_blank"})})}))]})]});var _};j.propTypes={wpcomHomeUrl:l().string.isRequired,currentUrl:l().string.isRequired,isStartingFresh:l().bool,startFreshCallback:l().func,customContent:l().shape(h.A),hasError:l().bool,isDevelopmentSite:l().bool};const k=j},3832:e=>{"use strict";e.exports=window.wp.url},3924:(e,t,n)=>{"use strict";function s(e,t={}){const n={};let s;if("undefined"!=typeof window&&(s=window?.JP_CONNECTION_INITIAL_STATE?.calypsoEnv),0===e.search("https://")){const t=new URL(e);e=`https://${t.host}${t.pathname}`,n.url=encodeURIComponent(e)}else n.source=encodeURIComponent(e);for(const e in t)n[e]=encodeURIComponent(t[e]);!Object.keys(n).includes("site")&&"undefined"!=typeof jetpack_redirects&&Object.hasOwn(jetpack_redirects,"currentSiteRawUrl")&&(n.site=jetpack_redirects.currentBlogID??jetpack_redirects.currentSiteRawUrl),s&&(n.calypso_env=s);return"https://jetpack.com/redirect/?"+Object.keys(n).map(e=>e+"="+n[e]).join("&")}n.d(t,{A:()=>s})},4295:(e,t,n)=>{"use strict";n.d(t,{A:()=>m});var s=n(6461),r=n(6427),o=n(6087),i=n(7723),a=n(3619),c=n.n(a),d=n(1609),l=n(1057),p=n(6043),h=n(790);const __=i.__,u=e=>{const{finishCallback:t=()=>{},isFinishing:n=!1,customContent:a={}}=e,c=(0,p.A)(e.wpcomHomeUrl),l=(0,p.A)(e.currentUrl),u=__("Got it, thanks","jetpack-connection");return(0,h.jsxs)(d.Fragment,{children:[(0,h.jsx)("h2",{children:a.migratedTitle?(0,o.createInterpolateElement)(a.migratedTitle,{em:(0,h.jsx)("em",{})}):__("Your Jetpack settings have migrated successfully","jetpack-connection")}),(0,h.jsx)("p",{children:(0,o.createInterpolateElement)(a.migratedBodyText||(0,i.sprintf)(/* translators: %1$s: The current site domain name. */ __("Safe Mode has been switched off for <hostname>%1$s</hostname> website and Jetpack is fully functional.","jetpack-connection"),l),{hostname:(0,h.jsx)("strong",{}),em:(0,h.jsx)("em",{}),strong:(0,h.jsx)("strong",{})})}),(0,h.jsxs)("div",{className:"jp-idc__idc-screen__card-migrated",children:[(0,h.jsx)("div",{className:"jp-idc__idc-screen__card-migrated-hostname",children:c}),(0,h.jsx)(r.Dashicon,{icon:"arrow-down-alt",className:"jp-idc__idc-screen__card-migrated-separator"}),(0,h.jsx)(r.Dashicon,{icon:"arrow-right-alt",className:"jp-idc__idc-screen__card-migrated-separator-wide"}),(0,h.jsx)("div",{className:"jp-idc__idc-screen__card-migrated-hostname",children:l})]}),(0,h.jsx)(r.Button,{className:"jp-idc__idc-screen__card-action-button jp-idc__idc-screen__card-action-button-migrated",onClick:t,label:u,children:n?(0,h.jsx)(s.A,{}):u})]})};u.propTypes={wpcomHomeUrl:c().string.isRequired,currentUrl:c().string.isRequired,finishCallback:c().func,isFinishing:c().bool,customContent:c().shape(l.A)};const m=u},4804:(e,t,n)=>{t.formatArgs=function(t){if(t[0]=(this.useColors?"%c":"")+this.namespace+(this.useColors?" %c":" ")+t[0]+(this.useColors?"%c ":" ")+"+"+e.exports.humanize(this.diff),!this.useColors)return;const n="color: "+this.color;t.splice(1,0,n,"color: inherit");let s=0,r=0;t[0].replace(/%[a-zA-Z%]/g,e=>{"%%"!==e&&(s++,"%c"===e&&(r=s))}),t.splice(r,0,n)},t.save=function(e){try{e?t.storage.setItem("debug",e):t.storage.removeItem("debug")}catch(e){}},t.load=function(){let e;try{e=t.storage.getItem("debug")||t.storage.getItem("DEBUG")}catch(e){}!e&&"undefined"!=typeof process&&"env"in process&&(e=process.env.DEBUG);return e},t.useColors=function(){if("undefined"!=typeof window&&window.process&&("renderer"===window.process.type||window.process.__nwjs))return!0;if("undefined"!=typeof navigator&&navigator.userAgent&&navigator.userAgent.toLowerCase().match(/(edge|trident)\/(\d+)/))return!1;let e;return"undefined"!=typeof document&&document.documentElement&&document.documentElement.style&&document.documentElement.style.WebkitAppearance||"undefined"!=typeof window&&window.console&&(window.console.firebug||window.console.exception&&window.console.table)||"undefined"!=typeof navigator&&navigator.userAgent&&(e=navigator.userAgent.toLowerCase().match(/firefox\/(\d+)/))&&parseInt(e[1],10)>=31||"undefined"!=typeof navigator&&navigator.userAgent&&navigator.userAgent.toLowerCase().match(/applewebkit\/(\d+)/)},t.storage=function(){try{return localStorage}catch(e){}}(),t.destroy=(()=>{let e=!1;return()=>{e||(e=!0,console.warn("Instance method `debug.destroy()` is deprecated and no longer does anything. It will be removed in the next major version of `debug`."))}})(),t.colors=["#0000CC","#0000FF","#0033CC","#0033FF","#0066CC","#0066FF","#0099CC","#0099FF","#00CC00","#00CC33","#00CC66","#00CC99","#00CCCC","#00CCFF","#3300CC","#3300FF","#3333CC","#3333FF","#3366CC","#3366FF","#3399CC","#3399FF","#33CC00","#33CC33","#33CC66","#33CC99","#33CCCC","#33CCFF","#6600CC","#6600FF","#6633CC","#6633FF","#66CC00","#66CC33","#9900CC","#9900FF","#9933CC","#9933FF","#99CC00","#99CC33","#CC0000","#CC0033","#CC0066","#CC0099","#CC00CC","#CC00FF","#CC3300","#CC3333","#CC3366","#CC3399","#CC33CC","#CC33FF","#CC6600","#CC6633","#CC9900","#CC9933","#CCCC00","#CCCC33","#FF0000","#FF0033","#FF0066","#FF0099","#FF00CC","#FF00FF","#FF3300","#FF3333","#FF3366","#FF3399","#FF33CC","#FF33FF","#FF6600","#FF6633","#FF9900","#FF9933","#FFCC00","#FFCC33"],t.log=console.debug||console.log||(()=>{}),e.exports=n(5067)(t);const{formatters:s}=e.exports;s.j=function(e){try{return JSON.stringify(e)}catch(e){return"[UnexpectedJSONParseError]: "+e.message}}},5067:(e,t,n)=>{e.exports=function(e){function t(e){let n,r,o,i=null;function a(...e){if(!a.enabled)return;const s=a,r=Number(new Date),o=r-(n||r);s.diff=o,s.prev=n,s.curr=r,n=r,e[0]=t.coerce(e[0]),"string"!=typeof e[0]&&e.unshift("%O");let i=0;e[0]=e[0].replace(/%([a-zA-Z%])/g,(n,r)=>{if("%%"===n)return"%";i++;const o=t.formatters[r];if("function"==typeof o){const t=e[i];n=o.call(s,t),e.splice(i,1),i--}return n}),t.formatArgs.call(s,e);(s.log||t.log).apply(s,e)}return a.namespace=e,a.useColors=t.useColors(),a.color=t.selectColor(e),a.extend=s,a.destroy=t.destroy,Object.defineProperty(a,"enabled",{enumerable:!0,configurable:!1,get:()=>null!==i?i:(r!==t.namespaces&&(r=t.namespaces,o=t.enabled(e)),o),set:e=>{i=e}}),"function"==typeof t.init&&t.init(a),a}function s(e,n){const s=t(this.namespace+(void 0===n?":":n)+e);return s.log=this.log,s}function r(e,t){let n=0,s=0,r=-1,o=0;for(;n<e.length;)if(s<t.length&&(t[s]===e[n]||"*"===t[s]))"*"===t[s]?(r=s,o=n,s++):(n++,s++);else{if(-1===r)return!1;s=r+1,o++,n=o}for(;s<t.length&&"*"===t[s];)s++;return s===t.length}return t.debug=t,t.default=t,t.coerce=function(e){if(e instanceof Error)return e.stack||e.message;return e},t.disable=function(){const e=[...t.names,...t.skips.map(e=>"-"+e)].join(",");return t.enable(""),e},t.enable=function(e){t.save(e),t.namespaces=e,t.names=[],t.skips=[];const n=("string"==typeof e?e:"").trim().replace(/\s+/g,",").split(",").filter(Boolean);for(const e of n)"-"===e[0]?t.skips.push(e.slice(1)):t.names.push(e)},t.enabled=function(e){for(const n of t.skips)if(r(e,n))return!1;for(const n of t.names)if(r(e,n))return!0;return!1},t.humanize=n(3594),t.destroy=function(){console.warn("Instance method `debug.destroy()` is deprecated and no longer does anything. It will be removed in the next major version of `debug`.")},Object.keys(e).forEach(n=>{t[n]=e[n]}),t.names=[],t.skips=[],t.formatters={},t.selectColor=function(e){let n=0;for(let t=0;t<e.length;t++)n=(n<<5)-n+e.charCodeAt(t),n|=0;return t.colors[Math.abs(n)%t.colors.length]},t.enable(t.load()),t}},5574:(e,t,n)=>{"use strict";n.d(t,{A:()=>r});var s=n(1609);const r=()=>{const[e,t]=(0,s.useState)(!1),n=(0,s.useCallback)(()=>{e||(t(!0),window.location.reload())},[e,t]);return{isFinishingMigration:e,finishMigrationCallback:n}}},5932:(e,t,n)=>{"use strict";n.d(t,{Ay:()=>p});var s=n(6439),r=n(3832);function o(e){class t extends Error{constructor(...t){super(...t),this.name=e}}return t}const i=o("JsonParseError"),a=o("JsonParseAfterRedirectError"),c=o("Api404Error"),d=o("Api404AfterRedirectError"),l=o("FetchNetworkError");const p=new function(e,t){let n=e,o=e,i={"X-WP-Nonce":t},a={credentials:"same-origin",headers:i},c={method:"post",credentials:"same-origin",headers:Object.assign({},i,{"Content-type":"application/json"})},d=function(e){const t=e.split("?"),n=t.length>1?t[1]:"",s=n.length?n.split("&"):[];return s.push("_cacheBuster="+(new Date).getTime()),t[0]+"?"+s.join("&")};const l={setApiRoot(e){n=e},setWpcomOriginApiUrl(e){o=e},setApiNonce(e){i={"X-WP-Nonce":e},a={credentials:"same-origin",headers:i},c={method:"post",credentials:"same-origin",headers:Object.assign({},i,{"Content-type":"application/json"})}},setCacheBusterCallback:e=>{d=e},registerSite:(e,t,r)=>{const o={};return(0,s.jetpackConfigHas)("consumer_slug")&&(o.plugin_slug=(0,s.jetpackConfigGet)("consumer_slug")),null!==t&&(o.redirect_uri=t),r&&(o.from=r),m(`${n}jetpack/v4/connection/register`,c,{body:JSON.stringify(o)}).then(h).then(u)},fetchAuthorizationUrl:e=>p((0,r.addQueryArgs)(`${n}jetpack/v4/connection/authorize_url`,{no_iframe:"1",redirect_uri:e}),a).then(h).then(u),fetchSiteConnectionData:()=>p(`${n}jetpack/v4/connection/data`,a).then(u),fetchSiteConnectionStatus:()=>p(`${n}jetpack/v4/connection`,a).then(u),fetchSiteConnectionTest:()=>p(`${n}jetpack/v4/connection/test`,a).then(h).then(u),fetchUserConnectionData:()=>p(`${n}jetpack/v4/connection/data`,a).then(u),fetchUserTrackingSettings:()=>p(`${n}jetpack/v4/tracking/settings`,a).then(h).then(u),updateUserTrackingSettings:e=>m(`${n}jetpack/v4/tracking/settings`,c,{body:JSON.stringify(e)}).then(h).then(u),disconnectSite:()=>m(`${n}jetpack/v4/connection`,c,{body:JSON.stringify({isActive:!1})}).then(h).then(u),fetchConnectUrl:()=>p(`${n}jetpack/v4/connection/url`,a).then(h).then(u),unlinkUser:(e=!1,t={})=>{const s={linked:!1,force:!!e};return t.disconnectAllUsers&&(s["disconnect-all-users"]=!0),m(`${n}jetpack/v4/connection/user`,c,{body:JSON.stringify(s)}).then(h).then(u)},reconnect:()=>m(`${n}jetpack/v4/connection/reconnect`,c).then(h).then(u),fetchConnectedPlugins:()=>p(`${n}jetpack/v4/connection/plugins`,a).then(h).then(u),setHasSeenWCConnectionModal:()=>m(`${n}jetpack/v4/seen-wc-connection-modal`,c).then(h).then(u),fetchModules:()=>p(`${n}jetpack/v4/module/all`,a).then(h).then(u),fetchModule:e=>p(`${n}jetpack/v4/module/${e}`,a).then(h).then(u),activateModule:e=>m(`${n}jetpack/v4/module/${e}/active`,c,{body:JSON.stringify({active:!0})}).then(h).then(u),deactivateModule:e=>m(`${n}jetpack/v4/module/${e}/active`,c,{body:JSON.stringify({active:!1})}),updateModuleOptions:(e,t)=>m(`${n}jetpack/v4/module/${e}`,c,{body:JSON.stringify(t)}).then(h).then(u),updateSettings:e=>m(`${n}jetpack/v4/settings`,c,{body:JSON.stringify(e)}).then(h).then(u),getProtectCount:()=>p(`${n}jetpack/v4/module/protect/data`,a).then(h).then(u),resetOptions:e=>m(`${n}jetpack/v4/options/${e}`,c,{body:JSON.stringify({reset:!0})}).then(h).then(u),activateVaultPress:()=>m(`${n}jetpack/v4/plugins`,c,{body:JSON.stringify({slug:"vaultpress",status:"active"})}).then(h).then(u),getVaultPressData:()=>p(`${n}jetpack/v4/module/vaultpress/data`,a).then(h).then(u),installPlugin:(e,t)=>{const s={slug:e,status:"active"};return t&&(s.source=t),m(`${n}jetpack/v4/plugins`,c,{body:JSON.stringify(s)}).then(h).then(u)},activateAkismet:()=>m(`${n}jetpack/v4/plugins`,c,{body:JSON.stringify({slug:"akismet",status:"active"})}).then(h).then(u),getAkismetData:()=>p(`${n}jetpack/v4/module/akismet/data`,a).then(h).then(u),checkAkismetKey:()=>p(`${n}jetpack/v4/module/akismet/key/check`,a).then(h).then(u),checkAkismetKeyTyped:e=>m(`${n}jetpack/v4/module/akismet/key/check`,c,{body:JSON.stringify({api_key:e})}).then(h).then(u),getFeatureTypeStatus:e=>p(`${n}jetpack/v4/feature/${e}`,a).then(h).then(u),fetchStatsData:e=>p(function(e){let t=`${n}jetpack/v4/module/stats/data`;-1!==t.indexOf("?")?t+=`&range=${encodeURIComponent(e)}`:t+=`?range=${encodeURIComponent(e)}`;return t}(e),a).then(h).then(u).then(f),getPluginUpdates:()=>p(`${n}jetpack/v4/updates/plugins`,a).then(h).then(u),getPlans:()=>p(`${n}jetpack/v4/plans`,a).then(h).then(u),fetchSettings:()=>p(`${n}jetpack/v4/settings`,a).then(h).then(u),updateSetting:e=>m(`${n}jetpack/v4/settings`,c,{body:JSON.stringify(e)}).then(h).then(u),fetchSiteData:()=>p(`${n}jetpack/v4/site`,a).then(h).then(u).then(e=>JSON.parse(e.data)),fetchSiteFeatures:()=>p(`${n}jetpack/v4/site/features`,a).then(h).then(u).then(e=>JSON.parse(e.data)),fetchSiteProducts:()=>p(`${n}jetpack/v4/site/products`,a).then(h).then(u),fetchSitePurchases:()=>p(`${n}jetpack/v4/site/purchases`,a).then(h).then(u).then(e=>JSON.parse(e.data)),fetchSiteBenefits:()=>p(`${n}jetpack/v4/site/benefits`,a).then(h).then(u).then(e=>JSON.parse(e.data)),fetchSiteDiscount:()=>p(`${n}jetpack/v4/site/discount`,a).then(h).then(u).then(e=>e.data),fetchSetupQuestionnaire:()=>p(`${n}jetpack/v4/setup/questionnaire`,a).then(h).then(u),fetchRecommendationsData:()=>p(`${n}jetpack/v4/recommendations/data`,a).then(h).then(u),fetchRecommendationsProductSuggestions:()=>p(`${n}jetpack/v4/recommendations/product-suggestions`,a).then(h).then(u),fetchRecommendationsUpsell:()=>p(`${n}jetpack/v4/recommendations/upsell`,a).then(h).then(u),fetchRecommendationsConditional:()=>p(`${n}jetpack/v4/recommendations/conditional`,a).then(h).then(u),saveRecommendationsData:e=>m(`${n}jetpack/v4/recommendations/data`,c,{body:JSON.stringify({data:e})}).then(h),fetchProducts:()=>p(`${n}jetpack/v4/products`,a).then(h).then(u),fetchRewindStatus:()=>p(`${n}jetpack/v4/rewind`,a).then(h).then(u).then(e=>JSON.parse(e.data)),fetchScanStatus:()=>p(`${n}jetpack/v4/scan`,a).then(h).then(u).then(e=>JSON.parse(e.data)),dismissJetpackNotice:e=>m(`${n}jetpack/v4/notice/${e}`,c,{body:JSON.stringify({dismissed:!0})}).then(h).then(u),fetchPluginsData:()=>p(`${n}jetpack/v4/plugins`,a).then(h).then(u),fetchIntroOffers:()=>p(`${n}jetpack/v4/intro-offers`,a).then(h).then(u),fetchVerifySiteGoogleStatus:e=>p(null!==e?`${n}jetpack/v4/verify-site/google/${e}`:`${n}jetpack/v4/verify-site/google`,a).then(h).then(u),verifySiteGoogle:e=>m(`${n}jetpack/v4/verify-site/google`,c,{body:JSON.stringify({keyring_id:e})}).then(h).then(u),submitSurvey:e=>m(`${n}jetpack/v4/marketing/survey`,c,{body:JSON.stringify(e)}).then(h).then(u),saveSetupQuestionnaire:e=>m(`${n}jetpack/v4/setup/questionnaire`,c,{body:JSON.stringify(e)}).then(h).then(u),updateLicensingError:e=>m(`${n}jetpack/v4/licensing/error`,c,{body:JSON.stringify(e)}).then(h).then(u),updateLicenseKey:e=>m(`${n}jetpack/v4/licensing/set-license`,c,{body:JSON.stringify({license:e})}).then(h).then(u),getUserLicensesCounts:()=>p(`${n}jetpack/v4/licensing/user/counts`,a).then(h).then(u),getUserLicenses:()=>p(`${n}jetpack/v4/licensing/user/licenses`,a).then(h).then(u),updateLicensingActivationNoticeDismiss:e=>m(`${n}jetpack/v4/licensing/user/activation-notice-dismiss`,c,{body:JSON.stringify({last_detached_count:e})}).then(h).then(u),updateRecommendationsStep:e=>m(`${n}jetpack/v4/recommendations/step`,c,{body:JSON.stringify({step:e})}).then(h),confirmIDCSafeMode:()=>m(`${n}jetpack/v4/identity-crisis/confirm-safe-mode`,c).then(h),startIDCFresh:e=>m(`${n}jetpack/v4/identity-crisis/start-fresh`,c,{body:JSON.stringify({redirect_uri:e})}).then(h).then(u),migrateIDC:()=>m(`${n}jetpack/v4/identity-crisis/migrate`,c).then(h),attachLicenses:e=>m(`${n}jetpack/v4/licensing/attach-licenses`,c,{body:JSON.stringify({licenses:e})}).then(h).then(u),fetchSearchPlanInfo:()=>p(`${o}jetpack/v4/search/plan`,a).then(h).then(u),fetchSearchSettings:()=>p(`${o}jetpack/v4/search/settings`,a).then(h).then(u),updateSearchSettings:e=>m(`${o}jetpack/v4/search/settings`,c,{body:JSON.stringify(e)}).then(h).then(u),fetchSearchStats:()=>p(`${o}jetpack/v4/search/stats`,a).then(h).then(u),fetchWafSettings:()=>p(`${n}jetpack/v4/waf`,a).then(h).then(u),updateWafSettings:e=>m(`${n}jetpack/v4/waf`,c,{body:JSON.stringify(e)}).then(h).then(u),fetchWordAdsSettings:()=>p(`${n}jetpack/v4/wordads/settings`,a).then(h).then(u),updateWordAdsSettings:e=>m(`${n}jetpack/v4/wordads/settings`,c,{body:JSON.stringify(e)}),fetchSearchPricing:()=>p(`${o}jetpack/v4/search/pricing`,a).then(h).then(u),fetchMigrationStatus:()=>p(`${n}jetpack/v4/migration/status`,a).then(h).then(u),fetchBackupUndoEvent:()=>p(`${n}jetpack/v4/site/backup/undo-event`,a).then(h).then(u),fetchBackupPreflightStatus:()=>p(`${n}jetpack/v4/site/backup/preflight`,a).then(h).then(u)};function p(e,t){return fetch(d(e),t)}function m(e,t,n){return fetch(e,Object.assign({},t,n)).catch(g)}function f(e){return e.general&&void 0===e.general.response||e.week&&void 0===e.week.response||e.month&&void 0===e.month.response?e:{}}Object.assign(this,l)};function h(e){return e.status>=200&&e.status<300?e:404===e.status?new Promise(()=>{throw e.redirected?new d(e.redirected):new c}):e.json().catch(e=>m(e)).then(t=>{const n=new Error(`${t.message} (Status ${e.status})`);throw n.response=t,n.name="ApiError",n})}function u(e){return e.json().catch(t=>m(t,e.redirected,e.url))}function m(e,t,n){throw t?new a(n):new i}function g(){throw new l}},6043:(e,t,n)=>{"use strict";n.d(t,{A:()=>s});const s=e=>/^https?:\/\//.test(e)?new URL(e).hostname:e.replace(/\/$/,"")},6087:e=>{"use strict";e.exports=window.wp.element},6427:e=>{"use strict";e.exports=window.wp.components},6439:(e,t,n)=>{let s={};try{s=n(9074)}catch{console.error("jetpackConfig is missing in your webpack config file. See @automattic/jetpack-config"),s={missingConfig:!0}}const r=e=>Object.hasOwn(s,e);e.exports={jetpackConfigHas:r,jetpackConfigGet:e=>{if(!r(e))throw'This app requires the "'+e+'" Jetpack Config to be defined in your webpack configuration file. See details in @automattic/jetpack-config package docs.';return s[e]}}},6461:(e,t,n)=>{"use strict";n.d(t,{A:()=>a});var s=n(3619),r=n.n(s),o=(n(2144),n(790));const i=({color:e="#FFFFFF",className:t="",size:n=20})=>{const s=t+" jp-components-spinner",r={width:n,height:n,fontSize:n,borderTopColor:e},i={borderTopColor:e,borderRightColor:e};return(0,o.jsx)("div",{className:s,children:(0,o.jsx)("div",{className:"jp-components-spinner__outer",style:r,children:(0,o.jsx)("div",{className:"jp-components-spinner__inner",style:i})})})};i.propTypes={color:r().string,className:r().string,size:r().number};const a=i},6576:(e,t,n)=>{"use strict";n.d(t,{A:()=>o});var s=n(9882),r=(n(2145),n(790));const o=e=>{const{children:t}=e;return(0,r.jsxs)("div",{className:"jp-idc__error-message",children:[(0,r.jsx)(s.A,{}),(0,r.jsx)("span",{children:t})]})}},6930:(e,t,n)=>{"use strict";n.d(t,{A:()=>j});var s=n(3924),r=n(6461),o=n(6427),i=n(7143),a=n(6087),c=n(7723),d=n(3619),l=n.n(d),p=n(3207),h=n(1057),u=n(6043),m=n(6576),g=n(790);const __=c.__,f=e=>{const t=(0,u.A)(e.wpcomHomeUrl),n=(0,u.A)(e.currentUrl),d=(0,i.useSelect)(e=>e(p.a).getIsActionInProgress(),[]),{isMigrating:l=!1,migrateCallback:h=()=>{},customContent:f={},hasError:j=!1}=e,k=f.migrateButtonLabel||__("Move your settings","jetpack-connection");return(0,g.jsxs)("div",{className:"jp-idc__idc-screen__card-action-base"+(j?" jp-idc__idc-screen__card-action-error":""),children:[(0,g.jsxs)("div",{className:"jp-idc__idc-screen__card-action-top",children:[(0,g.jsx)("h4",{children:f.migrateCardTitle?(0,a.createInterpolateElement)(f.migrateCardTitle,{em:(0,g.jsx)("em",{})}):__("Move Jetpack data","jetpack-connection")}),(0,g.jsx)("p",{children:(0,a.createInterpolateElement)(f.migrateCardBodyText||(0,c.sprintf)(/* translators: %1$s: The current site domain name. %2$s: The original site domain name. */ __("Move all your settings, stats and subscribers to your other URL, <hostname>%1$s</hostname>. <hostname>%2$s</hostname> will be disconnected from Jetpack.","jetpack-connection"),n,t),{hostname:(0,g.jsx)("strong",{}),em:(0,g.jsx)("em",{}),strong:(0,g.jsx)("strong",{})})})]}),(0,g.jsxs)("div",{className:"jp-idc__idc-screen__card-action-bottom",children:[(0,g.jsx)("div",{className:"jp-idc__idc-screen__card-action-sitename",children:t}),(0,g.jsx)(o.Dashicon,{icon:"arrow-down-alt",className:"jp-idc__idc-screen__card-action-separator"}),(0,g.jsx)("div",{className:"jp-idc__idc-screen__card-action-sitename",children:n}),(0,g.jsx)(o.Button,{className:"jp-idc__idc-screen__card-action-button",label:k,onClick:h,disabled:d,children:l?(0,g.jsx)(r.A,{}):k}),j&&(y=f.supportURL,(0,g.jsx)(m.A,{children:(0,a.createInterpolateElement)(__("Could not move your settings. Retry or find out more <a>here</a>.","jetpack-connection"),{a:(0,g.jsx)("a",{href:y||(0,s.A)("jetpack-support-safe-mode"),rel:"noopener noreferrer",target:"_blank"})})}))]})]});var y};f.propTypes={wpcomHomeUrl:l().string.isRequired,currentUrl:l().string.isRequired,isMigrating:l().bool,migrateCallback:l().func,customContent:l().shape(h.A),hasError:l().bool};const j=f},7142:(e,t,n)=>{"use strict";n.d(t,{A:()=>i});var s=n(7723),r=n(2231),o=n(790);const __=s.__,i=({logoColor:e="#069e08",showText:t=!0,className:n,height:s=32,...i})=>{const a=t?"0 0 118 32":"0 0 32 32";return(0,o.jsxs)("svg",{xmlns:"http://www.w3.org/2000/svg",x:"0px",y:"0px",viewBox:a,className:(0,r.A)("jetpack-logo",n),"aria-labelledby":"jetpack-logo-title",height:s,...i,role:"img",children:[(0,o.jsx)("title",{id:"jetpack-logo-title",children:__("Jetpack Logo","jetpack-connection")}),(0,o.jsx)("path",{fill:e,d:"M16,0C7.2,0,0,7.2,0,16s7.2,16,16,16s16-7.2,16-16S24.8,0,16,0z M15,19H7l8-16V19z M17,29V13h8L17,29z"}),t&&(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)("path",{d:"M41.3,26.6c-0.5-0.7-0.9-1.4-1.3-2.1c2.3-1.4,3-2.5,3-4.6V8h-3V6h6v13.4C46,22.8,45,24.8,41.3,26.6z"}),(0,o.jsx)("path",{d:"M65,18.4c0,1.1,0.8,1.3,1.4,1.3c0.5,0,2-0.2,2.6-0.4v2.1c-0.9,0.3-2.5,0.5-3.7,0.5c-1.5,0-3.2-0.5-3.2-3.1V12H60v-2h2.1V7.1 H65V10h4v2h-4V18.4z"}),(0,o.jsx)("path",{d:"M71,10h3v1.3c1.1-0.8,1.9-1.3,3.3-1.3c2.5,0,4.5,1.8,4.5,5.6s-2.2,6.3-5.8,6.3c-0.9,0-1.3-0.1-2-0.3V28h-3V10z M76.5,12.3 c-0.8,0-1.6,0.4-2.5,1.2v5.9c0.6,0.1,0.9,0.2,1.8,0.2c2,0,3.2-1.3,3.2-3.9C79,13.4,78.1,12.3,76.5,12.3z"}),(0,o.jsx)("path",{d:"M93,22h-3v-1.5c-0.9,0.7-1.9,1.5-3.5,1.5c-1.5,0-3.1-1.1-3.1-3.2c0-2.9,2.5-3.4,4.2-3.7l2.4-0.3v-0.3c0-1.5-0.5-2.3-2-2.3 c-0.7,0-2.3,0.5-3.7,1.1L84,11c1.2-0.4,3-1,4.4-1c2.7,0,4.6,1.4,4.6,4.7L93,22z M90,16.4l-2.2,0.4c-0.7,0.1-1.4,0.5-1.4,1.6 c0,0.9,0.5,1.4,1.3,1.4s1.5-0.5,2.3-1V16.4z"}),(0,o.jsx)("path",{d:"M104.5,21.3c-1.1,0.4-2.2,0.6-3.5,0.6c-4.2,0-5.9-2.4-5.9-5.9c0-3.7,2.3-6,6.1-6c1.4,0,2.3,0.2,3.2,0.5V13 c-0.8-0.3-2-0.6-3.2-0.6c-1.7,0-3.2,0.9-3.2,3.6c0,2.9,1.5,3.8,3.3,3.8c0.9,0,1.9-0.2,3.2-0.7V21.3z"}),(0,o.jsx)("path",{d:"M110,15.2c0.2-0.3,0.2-0.8,3.8-5.2h3.7l-4.6,5.7l5,6.3h-3.7l-4.2-5.8V22h-3V6h3V15.2z"}),(0,o.jsx)("path",{d:"M58.5,21.3c-1.5,0.5-2.7,0.6-4.2,0.6c-3.6,0-5.8-1.8-5.8-6c0-3.1,1.9-5.9,5.5-5.9s4.9,2.5,4.9,4.9c0,0.8,0,1.5-0.1,2h-7.3 c0.1,2.5,1.5,2.8,3.6,2.8c1.1,0,2.2-0.3,3.4-0.7C58.5,19,58.5,21.3,58.5,21.3z M56,15c0-1.4-0.5-2.9-2-2.9c-1.4,0-2.3,1.3-2.4,2.9 C51.6,15,56,15,56,15z"})]})]})}},7143:e=>{"use strict";e.exports=window.wp.data},7459:(e,t,n)=>{"use strict";n.d(t,{A:()=>j});var s=n(5932),r=n(7143),o=n(3619),i=n.n(o),a=n(1609),c=n(8101),d=n(5574),l=n(8502),p=n(3207),h=n(1057),u=n(2879),m=n(8979),g=n(790);const f=e=>{const{logo:t,customContent:n={},wpcomHomeUrl:o,currentUrl:i,apiNonce:h,apiRoot:f,redirectUri:j,tracksUserData:k,tracksEventData:y,isAdmin:v,possibleDynamicSiteUrlDetected:_,isDevelopmentSite:C}=e,[b,w]=(0,a.useState)(!1),S=(0,r.useSelect)(e=>e(p.a).getErrorType(),[]),{isMigrating:x,migrateCallback:A}=(0,c.A)((0,a.useCallback)(()=>{w(!0)},[w])),{isStartingFresh:F,startFreshCallback:I}=(0,l.A)(j),{isFinishingMigration:E,finishMigrationCallback:U}=(0,d.A)();return(0,a.useEffect)(()=>{s.Ay.setApiRoot(f),s.Ay.setApiNonce(h),(0,u.f)(y,k),y&&(Object.hasOwn(y,"isAdmin")&&y.isAdmin?(0,u.A)("notice_view"):(0,u.A)("non_admin_notice_view",{page:!!Object.hasOwn(y,"currentScreen")&&y.currentScreen}))},[f,h,k,y]),(0,g.jsx)(m.A,{logo:t,customContent:n,wpcomHomeUrl:o,currentUrl:i,redirectUri:j,isMigrating:x,migrateCallback:A,isMigrated:b,finishMigrationCallback:U,isFinishingMigration:E,isStartingFresh:F,startFreshCallback:I,isAdmin:v,hasStaySafeError:"safe-mode"===S,hasFreshError:"start-fresh"===S,hasMigrateError:"migrate"===S,possibleDynamicSiteUrlDetected:_,isDevelopmentSite:C})};f.propTypes={logo:i().oneOfType([i().string,i().object]),customContent:i().shape(h.A),wpcomHomeUrl:i().string.isRequired,currentUrl:i().string.isRequired,redirectUri:i().string.isRequired,apiRoot:i().string.isRequired,apiNonce:i().string.isRequired,tracksUserData:i().object,tracksEventData:i().object,isAdmin:i().bool.isRequired,possibleDynamicSiteUrlDetected:i().bool,isDevelopmentSite:i().bool};const j=f},7723:e=>{"use strict";e.exports=window.wp.i18n},8101:(e,t,n)=>{"use strict";n.d(t,{A:()=>c});var s=n(5932),r=n(7143),o=n(1609),i=n(3207),a=n(2879);const c=e=>{const[t,n]=(0,o.useState)(!1),c=(0,r.useSelect)(e=>e(i.a).getIsActionInProgress(),[]),{setIsActionInProgress:d,setErrorType:l,clearErrorType:p}=(0,r.useDispatch)(i.a);return{isMigrating:t,migrateCallback:(0,o.useCallback)(()=>{c||((0,a.A)("migrate"),d(!0),n(!0),p(),s.Ay.migrateIDC().then(()=>{n(!1),e&&"[object Function]"==={}.toString.call(e)&&e()}).catch(e=>{throw d(!1),n(!1),l("migrate"),e}))},[n,e,c,d,l,p])}}},8269:(e,t,n)=>{"use strict";n.d(t,{Ay:()=>i,Xs:()=>s,sL:()=>o,xj:()=>r});const s="SET_IS_ACTION_IN_PROGRESS",r="SET_ERROR_TYPE",o="CLEAR_ERROR_TYPE",i={setIsActionInProgress:e=>({type:s,isInProgress:e}),setErrorType:e=>({type:r,errorType:e}),clearErrorType:()=>({type:o})}},8502:(e,t,n)=>{"use strict";n.d(t,{A:()=>c});var s=n(5932),r=n(7143),o=n(1609),i=n(3207),a=n(2879);const c=e=>{const[t,n]=(0,o.useState)(!1),c=(0,r.useSelect)(e=>e(i.a).getIsActionInProgress(),[]),{setIsActionInProgress:d,setErrorType:l,clearErrorType:p}=(0,r.useDispatch)(i.a);return{isStartingFresh:t,startFreshCallback:(0,o.useCallback)(()=>{c||((0,a.A)("start_fresh"),d(!0),n(!0),p(),s.Ay.startIDCFresh(e).then(e=>{window.location.href=e+"&from=idc-notice"}).catch(e=>{throw d(!1),n(!1),l("start-fresh"),e}))},[n,c,d,e,l,p])}}},8918:(e,t,n)=>{"use strict";n.d(t,{A:()=>s});const s={getIsActionInProgress:e=>e.isActionInProgress||!1,getErrorType:e=>e.errorType||null}},8979:(e,t,n)=>{"use strict";n.d(t,{A:()=>g});var s=n(7142),r=n(6087),o=n(7723),i=n(3619),a=n.n(i),c=n(1057),d=n(1217),l=n(4295),p=n(9291),h=(n(9958),n(790));const __=o.__,u=(e,t)=>"string"==typeof e||e instanceof String?(0,h.jsx)("img",{src:e,alt:t,className:"jp-idc__idc-screen__logo-image"}):e,m=e=>{const{logo:t=(0,h.jsx)(s.A,{height:24}),customContent:n={},wpcomHomeUrl:o,currentUrl:i,redirectUri:a,isMigrating:c=!1,migrateCallback:m,isMigrated:g=!1,finishMigrationCallback:f,isFinishingMigration:j=!1,isStartingFresh:k=!1,startFreshCallback:y,isAdmin:v,hasMigrateError:_=!1,hasFreshError:C=!1,hasStaySafeError:b=!1,possibleDynamicSiteUrlDetected:w=!1,isDevelopmentSite:S}=e,x=v?"":(0,h.jsx)(p.A,{customContent:n});let A="";return v&&(A=g?(0,h.jsx)(l.A,{wpcomHomeUrl:o,currentUrl:i,finishCallback:f,isFinishing:j,customContent:n}):(0,h.jsx)(d.A,{wpcomHomeUrl:o,currentUrl:i,redirectUri:a,customContent:n,isMigrating:c,migrateCallback:m,isStartingFresh:k,startFreshCallback:y,hasMigrateError:_,hasFreshError:C,hasStaySafeError:b,possibleDynamicSiteUrlDetected:w,isDevelopmentSite:S})),(0,h.jsxs)("div",{className:"jp-idc__idc-screen"+(g?" jp-idc__idc-screen__success":""),children:[(0,h.jsxs)("div",{className:"jp-idc__idc-screen__header",children:[(0,h.jsx)("div",{className:"jp-idc__idc-screen__logo",children:u(t,n.logoAlt||"")}),(0,h.jsx)("div",{className:"jp-idc__idc-screen__logo-label",children:n.headerText?(0,r.createInterpolateElement)(n.headerText,{em:(0,h.jsx)("em",{}),strong:(0,h.jsx)("strong",{})}):__("Safe Mode","jetpack-connection")})]}),x,A]})};m.propTypes={logo:a().oneOfType([a().string,a().object]),customContent:a().shape(c.A),wpcomHomeUrl:a().string.isRequired,currentUrl:a().string.isRequired,redirectUri:a().string.isRequired,isMigrating:a().bool,migrateCallback:a().func,isMigrated:a().bool,finishMigrationCallback:a().func,isFinishingMigration:a().bool,isStartingFresh:a().bool,startFreshCallback:a().func,isAdmin:a().bool.isRequired,hasMigrateError:a().bool,hasFreshError:a().bool,hasStaySafeError:a().bool,possibleDynamicSiteUrlDetected:a().bool,isDevelopmentSite:a().bool};const g=m},9074:e=>{"use strict";e.exports={consumer_slug:"connection_package"}},9291:(e,t,n)=>{"use strict";n.d(t,{A:()=>h});var s=n(3924),r=n(6087),o=n(7723),i=n(3619),a=n.n(i),c=n(1609),d=n(1057),l=n(790);const __=o.__,p=e=>{const{customContent:t={}}=e;return(0,l.jsxs)(c.Fragment,{children:[(0,l.jsx)("h2",{children:t.nonAdminTitle?(0,r.createInterpolateElement)(t.nonAdminTitle,{em:(0,l.jsx)("em",{})}):__("Safe Mode has been activated","jetpack-connection")}),(0,l.jsx)("p",{children:(0,r.createInterpolateElement)(t.nonAdminBodyText||__("This site is in Safe Mode because there are 2 Jetpack-powered sites that appear to be duplicates. 2 sites that are telling Jetpack they’re the same site. <safeModeLink>Learn more about safe mode.</safeModeLink>","jetpack-connection"),{safeModeLink:(0,l.jsx)("a",{href:t.supportURL||(0,s.A)("jetpack-support-safe-mode"),rel:"noopener noreferrer",target:"_blank"}),em:(0,l.jsx)("em",{}),strong:(0,l.jsx)("strong",{})})}),t.nonAdminBodyText?"":(0,l.jsx)("p",{children:__("An administrator of this site can take Jetpack out of Safe Mode.","jetpack-connection")})]})};p.propTypes={customContent:a().shape(d.A)};const h=p},9491:e=>{"use strict";e.exports=window.wp.compose},9626:()=>{},9882:(e,t,n)=>{"use strict";n.d(t,{A:()=>r});var s=n(790);const r=()=>(0,s.jsxs)("svg",{className:"error-gridicon",xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",height:24,children:[(0,s.jsx)("rect",{x:"0",fill:"none",width:"24",height:"24"}),(0,s.jsx)("g",{children:(0,s.jsx)("path",{d:"M12 4c4.411 0 8 3.589 8 8s-3.589 8-8 8-8-3.589-8-8 3.589-8 8-8m0-2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2zm1 13h-2v2h2v-2zm-2-2h2l.5-6h-3l.5 6z"})})]})},9958:()=>{}},t={};function n(s){var r=t[s];if(void 0!==r)return r.exports;var o=t[s]={exports:{}};return e[s](o,o.exports,n),o.exports}n.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return n.d(t,{a:t}),t},n.d=(e,t)=>{for(var s in t)n.o(t,s)&&!n.o(e,s)&&Object.defineProperty(e,s,{enumerable:!0,get:t[s]})},n.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{"use strict";var e=n(7459),t=n(6087),s=n(790);window.addEventListener("load",()=>function(){if(!Object.hasOwn(window,"JP_IDENTITY_CRISIS__INITIAL_STATE"))return;const n=document.getElementById(window.JP_IDENTITY_CRISIS__INITIAL_STATE.containerID||"jp-identity-crisis-container");if(null===n)return;const{WP_API_root:r,WP_API_nonce:o,wpcomHomeUrl:i,currentUrl:a,redirectUri:c,tracksUserData:d,tracksEventData:l,isSafeModeConfirmed:p,consumerData:h,isAdmin:u,possibleDynamicSiteUrlDetected:m,isDevelopmentSite:g}=window.JP_IDENTITY_CRISIS__INITIAL_STATE;if(!p){const p=(0,s.jsx)(e.A,{wpcomHomeUrl:i,currentUrl:a,apiRoot:r,apiNonce:o,redirectUri:c,tracksUserData:d||{},tracksEventData:l,customContent:Object.hasOwn(h,"customContent")?h.customContent:{},isAdmin:u,logo:Object.hasOwn(h,"logo")?h.logo:void 0,possibleDynamicSiteUrlDetected:m,isDevelopmentSite:g});t.createRoot(n).render(p)}}())})()})(); jetpack-connection/dist/jetpack-sso-admin-create-user.css 0000777 00000000464 15251741406 0017545 0 ustar 00 .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} jetpack-connection/dist/jetpack-connection.css 0000777 00000131310 15251741406 0015570 0 ustar 00 .zI5tJ_qhWE6Oe6Lk75GY{--wp-admin-theme-color:var(--jp-black);--wp-admin-theme-color-darker-10:var(--jp-black-80);--wp-admin-theme-color-darker-20:var(--jp-black-80);--wp-admin-border-width-focus:1.51px;border-radius:var(--jp-border-radius);font-weight:600;justify-content:center}.zI5tJ_qhWE6Oe6Lk75GY.tuBt2DLqimiImoqVzPqo{height:calc(var(--spacing-base)*5);padding:var(--spacing-base);width:calc(var(--spacing-base)*5)}.zI5tJ_qhWE6Oe6Lk75GY.tuBt2DLqimiImoqVzPqo>svg:first-child{margin:0;padding:0}.zI5tJ_qhWE6Oe6Lk75GY.tuBt2DLqimiImoqVzPqo.Na39I683LAaSA99REg14{height:calc(var(--spacing-base)*4);min-width:calc(var(--spacing-base)*4);padding:calc(var(--spacing-base)/2);width:calc(var(--spacing-base)*4)}.zI5tJ_qhWE6Oe6Lk75GY.ipS7tKy9GntCS4R3vekF:not(.tuBt2DLqimiImoqVzPqo){font-size:var(--font-body);height:auto;line-height:24px;padding:var(--spacing-base) calc(var(--spacing-base)*3)}.zI5tJ_qhWE6Oe6Lk75GY.ipS7tKy9GntCS4R3vekF:not(.tuBt2DLqimiImoqVzPqo).paGLQwtPEaJmtArCcmyK{padding:var(--spacing-base) calc(var(--spacing-base)*2)}.zI5tJ_qhWE6Oe6Lk75GY.Na39I683LAaSA99REg14:not(.tuBt2DLqimiImoqVzPqo){font-size:var(--font-body-extra-small);height:auto;line-height:20px;padding:calc(var(--spacing-base)/2) var(--spacing-base)}.zI5tJ_qhWE6Oe6Lk75GY.Na39I683LAaSA99REg14:not(.tuBt2DLqimiImoqVzPqo).paGLQwtPEaJmtArCcmyK>svg:first-child{margin-right:calc(var(--spacing-base)/2)}.zI5tJ_qhWE6Oe6Lk75GY.Na39I683LAaSA99REg14:not(.tuBt2DLqimiImoqVzPqo)>.components-spinner{height:20px}.zI5tJ_qhWE6Oe6Lk75GY.lZAo6_oGfclXOO9CC6Rd{font-weight:400}.zI5tJ_qhWE6Oe6Lk75GY.xJDOiJxTt0R_wSl8Ipz_{min-width:100%}.zI5tJ_qhWE6Oe6Lk75GY.is-primary:disabled,.zI5tJ_qhWE6Oe6Lk75GY.is-secondary:disabled{background:var(--jp-gray);color:var(--jp-gray-20)}.zI5tJ_qhWE6Oe6Lk75GY.is-secondary{background:var(--jp-white);box-shadow:inset 0 0 0 1.51px var(--jp-black)}.zI5tJ_qhWE6Oe6Lk75GY.is-secondary:active:not(:disabled),.zI5tJ_qhWE6Oe6Lk75GY.is-secondary:hover:not(:disabled){background:var(--jp-gray-0)}.zI5tJ_qhWE6Oe6Lk75GY.is-link.Na39I683LAaSA99REg14,.zI5tJ_qhWE6Oe6Lk75GY.is-link.ipS7tKy9GntCS4R3vekF{padding:0}.zI5tJ_qhWE6Oe6Lk75GY.is-link:hover:not(:disabled){text-decoration-thickness:3px}.zI5tJ_qhWE6Oe6Lk75GY.is-link:focus:not(:disabled){text-decoration-line:none}.zI5tJ_qhWE6Oe6Lk75GY.is-destructive.is-primary{box-shadow:none}.zI5tJ_qhWE6Oe6Lk75GY.is-destructive.is-primary:not(:disabled){background:var(--jp-red-50);box-shadow:inset 0 0 0 1px var(--jp-red-50);color:var(--jp-white)}.zI5tJ_qhWE6Oe6Lk75GY.is-destructive.is-primary:hover:not(:disabled){background:var(--jp-red-60);box-shadow:inset 0 0 0 1px var(--jp-red-60)}.zI5tJ_qhWE6Oe6Lk75GY.is-destructive.is-primary:focus:not(:disabled){background:var(--jp-red-70);box-shadow:inset 0 0 0 1px var(--jp-white),0 0 0 var(--wp-admin-border-width-focus) var(--jp-red-70);color:var(--jp-white)}.zI5tJ_qhWE6Oe6Lk75GY.is-destructive.is-primary:active:not(:disabled){background:var(--jp-red-50)}.zI5tJ_qhWE6Oe6Lk75GY.is-destructive.is-secondary{box-shadow:none}.zI5tJ_qhWE6Oe6Lk75GY.is-destructive.is-secondary:not(:disabled){background:var(--jp-white);box-shadow:inset 0 0 0 1.5px var(--jp-red-50);color:var(--jp-red-50)}.zI5tJ_qhWE6Oe6Lk75GY.is-destructive.is-secondary:hover:not(:disabled){background:var(--jp-red-0);box-shadow:inset 0 0 0 1.5px var(--jp-red-60);color:var(--jp-red-60)}.zI5tJ_qhWE6Oe6Lk75GY.is-destructive.is-secondary:focus:not(:disabled){box-shadow:inset 0 0 0 1px var(--jp-white),0 0 0 var(--wp-admin-border-width-focus) var(--jp-red-70);color:var(--jp-red-70)}.zI5tJ_qhWE6Oe6Lk75GY.is-destructive.is-secondary:active:not(:disabled){background:var(--jp-gray-0)}.zI5tJ_qhWE6Oe6Lk75GY.is-destructive.is-link:not(:disabled){color:var(--jp-red-50)}.zI5tJ_qhWE6Oe6Lk75GY.is-destructive.is-link:hover:not(:disabled){box-shadow:none;color:var(--jp-red-60)}.zI5tJ_qhWE6Oe6Lk75GY.is-destructive.is-link:focus:not(:disabled){box-shadow:inset 0 0 0 1px var(--jp-white),0 0 0 var(--wp-admin-border-width-focus) var(--jp-red-70);color:var(--jp-red-70)}.zI5tJ_qhWE6Oe6Lk75GY.q_tVWqMjl39RcY6WtQA6{position:relative}.zI5tJ_qhWE6Oe6Lk75GY.q_tVWqMjl39RcY6WtQA6.has-icon{justify-content:center}.zI5tJ_qhWE6Oe6Lk75GY.q_tVWqMjl39RcY6WtQA6>:not(.components-spinner){visibility:hidden}.zI5tJ_qhWE6Oe6Lk75GY.q_tVWqMjl39RcY6WtQA6>.components-spinner{margin:0;position:absolute}.CDuBjJp_8jxzx5j6Nept{margin-left:calc(var(--spacing-base)/2)}.TcCZnGE6mad8Dvz9pCZi{background:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgZmlsbD0ibm9uZSIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBzdHJva2U9IiNkNjM2MzkiIHN0cm9rZS13aWR0aD0iMS41IiBkPSJNMTIgMjBhOCA4IDAgMSAwIDAtMTYgOCA4IDAgMCAwIDAgMTZaIi8+PHBhdGggZmlsbD0iI2Q2MzYzOSIgZD0iTTEzIDdoLTJ2Nmgyek0xMyAxNWgtMnYyaDJ6Ii8+PC9zdmc+) no-repeat 0 0;color:var(--jp-red);line-height:25px;padding-left:25px}._mn6o2Dtm5pfFWc8_A1K{--spacing-base:8px;min-width:264px}.WQVtrU6q0L1Igcj7wCrQ{margin:0;padding:0}.UujoBFTnQNY2cWU2SIsH{font-size:var(--font-headline-medium);font-weight:700;line-height:52px}.TeGO5V_thHw5lDAm1_2M{font-weight:700}.TeGO5V_thHw5lDAm1_2M,.WolQzb2MsSgiNmLtc7_j{font-size:var(--font-headline-small);line-height:40px}.WolQzb2MsSgiNmLtc7_j{font-weight:400}.hUB0JT8p1T2Hw28N6qC8{font-weight:500}.gKZWDv5chz3_O3Syp74H,.hUB0JT8p1T2Hw28N6qC8{font-size:var(--font-title-medium);line-height:32px}.gKZWDv5chz3_O3Syp74H{font-weight:600}.zY2No8Ga4b8shbOQGhnv{font-size:var(--font-title-small);font-weight:500;line-height:30px}.tIj0D1t8Cc892ikmgFPZ{font-size:var(--font-body);font-weight:400;line-height:24px}.KdcN0BnOaVeVhyLRKqhS{font-size:var(--font-body-small);font-weight:400;line-height:24px}.dso3Rh3tl3Xv1GumBktz{font-weight:400}.dso3Rh3tl3Xv1GumBktz,.mQ1UlbN9u4Mg9byO8m7v{font-size:var(--font-body-extra-small);line-height:20px}.mQ1UlbN9u4Mg9byO8m7v{font-weight:700}.PItlW5vRExLnTj4a8eLE{font-size:var(--font-body-extra-small);font-weight:600;line-height:16px}.TwRpPlktzxhmFVeua7P5{margin:calc(var(--spacing-base)*0)}.zVfqx7gyb3o9mxfGynn1{margin-left:calc(var(--spacing-base)*0);margin-right:calc(var(--spacing-base)*0)}.iSHVzNiB9iVleGljaQxy{margin-bottom:calc(var(--spacing-base)*0)}.iSHVzNiB9iVleGljaQxy,.xqDIp6cNVr_E6RXaiPyD{margin-top:calc(var(--spacing-base)*0)}.S8EwaXk1kyPizt6x4WH2{margin-right:calc(var(--spacing-base)*0)}.ODX5Vr1TARoLFkDDFooD{margin-bottom:calc(var(--spacing-base)*0)}.cphJ8dCpfimnky7P2FHg{margin-left:calc(var(--spacing-base)*0)}.PFgIhNxIyiSuNvQjAIYj{margin:calc(var(--spacing-base)*1)}.M2jKmUzDxvJjjVEPU3zn{margin-left:calc(var(--spacing-base)*1);margin-right:calc(var(--spacing-base)*1)}.io15gAh8tMTNbSEfwJKk{margin-bottom:calc(var(--spacing-base)*1)}.io15gAh8tMTNbSEfwJKk,.rcTN5uw9xIEeMEGL3Xi_{margin-top:calc(var(--spacing-base)*1)}.CQSkybjq2TcRM1Xo9COV{margin-right:calc(var(--spacing-base)*1)}.hfqOWgq6_MEGdFE82eOY{margin-bottom:calc(var(--spacing-base)*1)}.I8MxZQYTbuu595yfesWA{margin-left:calc(var(--spacing-base)*1)}.kQkc6rmdpvLKPkyoJtVQ{margin:calc(var(--spacing-base)*2)}.j6vFPxWuu4Jan2ldoxpp{margin-left:calc(var(--spacing-base)*2);margin-right:calc(var(--spacing-base)*2)}.hqr39dC4H_AbactPAkCG{margin-bottom:calc(var(--spacing-base)*2)}.c3dQnMi16C6J6Ecy4283,.hqr39dC4H_AbactPAkCG{margin-top:calc(var(--spacing-base)*2)}.YNZmHOuRo6hU7zzKfPdP{margin-right:calc(var(--spacing-base)*2)}.Db8lbak1_wunpPk8NwKU{margin-bottom:calc(var(--spacing-base)*2)}.ftsYE5J9hLzquQ0tA5dY{margin-left:calc(var(--spacing-base)*2)}.Det4MHzLUW7EeDnafPzq{margin:calc(var(--spacing-base)*3)}.h_8EEAztC29Vve1datb5{margin-left:calc(var(--spacing-base)*3);margin-right:calc(var(--spacing-base)*3)}.YXIXJ0h1k47u6hzK8KcM{margin-bottom:calc(var(--spacing-base)*3)}.YXIXJ0h1k47u6hzK8KcM,.soADBBkcIKCBXzCTuV9_{margin-top:calc(var(--spacing-base)*3)}.zSX59ziEaEWGjnpZa4uV{margin-right:calc(var(--spacing-base)*3)}.yrVTnq_WBMbejg89c2ZQ{margin-bottom:calc(var(--spacing-base)*3)}.UKtHPJnI2cXBWtPDm5hM{margin-left:calc(var(--spacing-base)*3)}.guexok_Tqd5Tf52hRlbT{margin:calc(var(--spacing-base)*4)}.oS1E2KfTBZkJ3F0tN7T6{margin-left:calc(var(--spacing-base)*4);margin-right:calc(var(--spacing-base)*4)}.DN1OhhXi6AoBgEdDSbGd{margin-bottom:calc(var(--spacing-base)*4)}.DN1OhhXi6AoBgEdDSbGd,.ot2kkMcYHv53hLZ4LSn0{margin-top:calc(var(--spacing-base)*4)}.A1krOZZhlQ6Sp8Cy4bly{margin-right:calc(var(--spacing-base)*4)}.pkDbXXXL32237M0hokEh{margin-bottom:calc(var(--spacing-base)*4)}.XXv4kDTGvEnQeuGKOPU3{margin-left:calc(var(--spacing-base)*4)}.yGqHk1a57gaISwkXwXe6{margin:calc(var(--spacing-base)*5)}.X8cghM358X3DkXLc9aNK{margin-left:calc(var(--spacing-base)*5);margin-right:calc(var(--spacing-base)*5)}.GdfSmGwHlFnN2S6xBn1f{margin-bottom:calc(var(--spacing-base)*5)}.GdfSmGwHlFnN2S6xBn1f,.yqeuzwyGQ7zG0avrGqi_{margin-top:calc(var(--spacing-base)*5)}.g9emeCkuHvYhveiJbfXO{margin-right:calc(var(--spacing-base)*5)}.Lvk3dqcyHbZ07QCRlrUQ{margin-bottom:calc(var(--spacing-base)*5)}.r3yQECDQ9qX0XZzXlVAg{margin-left:calc(var(--spacing-base)*5)}.aQhlPwht2Cz1X_63Miw0{margin:calc(var(--spacing-base)*6)}.JyHb0vK3wJgpblL9s5j8{margin-left:calc(var(--spacing-base)*6);margin-right:calc(var(--spacing-base)*6)}.cY2gULL1lAv6WPNIRuf3{margin-bottom:calc(var(--spacing-base)*6)}.NBWQ9Lwhh_fnry3lg_p7,.cY2gULL1lAv6WPNIRuf3{margin-top:calc(var(--spacing-base)*6)}.yIOniNe5E40C8fWvBm5V{margin-right:calc(var(--spacing-base)*6)}.t30usboNSyqfQWIwHvT3{margin-bottom:calc(var(--spacing-base)*6)}.Nm_TyFkYCMhOoghoToKJ{margin-left:calc(var(--spacing-base)*6)}.C4qJKoBXpgKtpmrqtEKB{margin:calc(var(--spacing-base)*7)}.S93Srbu6NQ_PBr7DmTiD{margin-left:calc(var(--spacing-base)*7);margin-right:calc(var(--spacing-base)*7)}.fJj8k6gGJDks3crUZxOS{margin-bottom:calc(var(--spacing-base)*7)}.cW6D6djs7Ppm7fD7TeoV,.fJj8k6gGJDks3crUZxOS{margin-top:calc(var(--spacing-base)*7)}.DuCnqNfcxcP3Z__Yo5Ro{margin-right:calc(var(--spacing-base)*7)}.im8407m2fw5vOg7O2zsw{margin-bottom:calc(var(--spacing-base)*7)}.G0fbeBgvz2sh3uTP9gNl{margin-left:calc(var(--spacing-base)*7)}.kvW3sBCxRxUqz1jrVMJl{margin:calc(var(--spacing-base)*8)}.tOjEqjLONQdkiYx_XRnw{margin-left:calc(var(--spacing-base)*8);margin-right:calc(var(--spacing-base)*8)}.op5hFSx318zgxsoZZNLN{margin-bottom:calc(var(--spacing-base)*8)}.c9WfNHP6TFKWIfLxv52J,.op5hFSx318zgxsoZZNLN{margin-top:calc(var(--spacing-base)*8)}.sBA75QqcqRwwYSHJh2wc{margin-right:calc(var(--spacing-base)*8)}.GpL6idrXmSOM6jB8Ohsf{margin-bottom:calc(var(--spacing-base)*8)}.HbtWJoQwpgGycz8dGzeT{margin-left:calc(var(--spacing-base)*8)}.uxX3khU88VQ_Ah49Ejsa{padding:calc(var(--spacing-base)*0)}.KX0FhpBKwKzs9fOUdbNz{padding-left:calc(var(--spacing-base)*0);padding-right:calc(var(--spacing-base)*0)}.PfK8vKDyN32dnimlzYjz{padding-bottom:calc(var(--spacing-base)*0)}.PfK8vKDyN32dnimlzYjz,.emxLHRjQuJsImnPbQIzE{padding-top:calc(var(--spacing-base)*0)}.kJ8WzlpTVgdViXt8ukP9{padding-right:calc(var(--spacing-base)*0)}.tg_UIUI11VBzrTAn2AzJ{padding-bottom:calc(var(--spacing-base)*0)}.uczvl8kaz84oPQJ2DB2R{padding-left:calc(var(--spacing-base)*0)}.o7UHPcdVK3lt7q3lqV4o{padding:calc(var(--spacing-base)*1)}.IDqEOxvDoYrFYxELPmtX{padding-left:calc(var(--spacing-base)*1);padding-right:calc(var(--spacing-base)*1)}.DdywPW2qSYlu2pt8tpO2{padding-bottom:calc(var(--spacing-base)*1)}.DdywPW2qSYlu2pt8tpO2,.npy3hw4A5QSkDicb2CJJ{padding-top:calc(var(--spacing-base)*1)}.LgbptTApNY5NwLQvEFAt{padding-right:calc(var(--spacing-base)*1)}.WZQy2SZuZso59bUsXXyl{padding-bottom:calc(var(--spacing-base)*1)}.o331apInxNunbYB3SfPE{padding-left:calc(var(--spacing-base)*1)}.fMPIyD9Vqki1Lrc_yJnG{padding:calc(var(--spacing-base)*2)}.i2pMcTcdrr10IQoiSm_L{padding-left:calc(var(--spacing-base)*2);padding-right:calc(var(--spacing-base)*2)}.eA702gn32kwptiI1obXH{padding-bottom:calc(var(--spacing-base)*2)}.eA702gn32kwptiI1obXH,.o9bGieUKcYc8o0Ij9oZX{padding-top:calc(var(--spacing-base)*2)}.SwZcFez1RDqWsOFjB5iG{padding-right:calc(var(--spacing-base)*2)}.eHpLc_idmuEqeqCTvqkN{padding-bottom:calc(var(--spacing-base)*2)}.vU39i2B4P1fUTMB2l6Vo{padding-left:calc(var(--spacing-base)*2)}.JHWNzBnE29awhdu5BEh1{padding:calc(var(--spacing-base)*3)}.X72lGbb56L3KFzC2xQ9N{padding-left:calc(var(--spacing-base)*3);padding-right:calc(var(--spacing-base)*3)}.BzfNhRG8wXdCEB5ocQ6e{padding-bottom:calc(var(--spacing-base)*3)}.BzfNhRG8wXdCEB5ocQ6e,.srV0KSDC83a2fiimSMMQ{padding-top:calc(var(--spacing-base)*3)}.lUWfkmbQjCskhcNwkyCm{padding-right:calc(var(--spacing-base)*3)}.Ts0dIlc3aTSL7V4cIHis{padding-bottom:calc(var(--spacing-base)*3)}.CzlqQXXhX6MvorArFZ8B{padding-left:calc(var(--spacing-base)*3)}.TqMPkQtR_DdZuKb5vBoV{padding:calc(var(--spacing-base)*4)}.a7UrjhI69Vetlcj9ZVzz{padding-left:calc(var(--spacing-base)*4);padding-right:calc(var(--spacing-base)*4)}.StEhBzGs2Gi5dDEkjhAv{padding-bottom:calc(var(--spacing-base)*4)}.FGneZfZyvYrt1dG0zcnm,.StEhBzGs2Gi5dDEkjhAv{padding-top:calc(var(--spacing-base)*4)}.APEH216rpdlJWgD2fHc8{padding-right:calc(var(--spacing-base)*4)}.oGwXC3ohCic9XnAj6x69{padding-bottom:calc(var(--spacing-base)*4)}.U6gnT9y42ViPNOcNzBwb{padding-left:calc(var(--spacing-base)*4)}.IpdRLBwnHqbqFrixgbYC{padding:calc(var(--spacing-base)*5)}.HgNeXvkBa9o3bQ5fvFZm{padding-left:calc(var(--spacing-base)*5);padding-right:calc(var(--spacing-base)*5)}.tJtFZM3XfPG9v9TSDfN1{padding-bottom:calc(var(--spacing-base)*5)}.PdifHW45QeXYfK568uD8,.tJtFZM3XfPG9v9TSDfN1{padding-top:calc(var(--spacing-base)*5)}.mbLkWTTZ0Za_BBbFZ5b2{padding-right:calc(var(--spacing-base)*5)}.vVWpZpLlWrkTt0hMk8XU{padding-bottom:calc(var(--spacing-base)*5)}.RxfaJj5a1Nt6IavEo5Zl{padding-left:calc(var(--spacing-base)*5)}.SppJULDGdnOGcjZNCYBy{padding:calc(var(--spacing-base)*6)}.palY2nLwdoyooPUm9Hhk{padding-left:calc(var(--spacing-base)*6);padding-right:calc(var(--spacing-base)*6)}.WYw1JvZC0ppLdvSAPhr_{padding-bottom:calc(var(--spacing-base)*6)}.WYw1JvZC0ppLdvSAPhr_,.YEEJ9b90ueQaPfiU8aeN{padding-top:calc(var(--spacing-base)*6)}.QE0ssnsKvWJMqlhPbY5u{padding-right:calc(var(--spacing-base)*6)}.n8yA3jHlMRyLd5UIfoND{padding-bottom:calc(var(--spacing-base)*6)}.tXHmxYnHzbwtfxEaG51n{padding-left:calc(var(--spacing-base)*6)}.kBTsPKkO_3g_tLkj77Um{padding:calc(var(--spacing-base)*7)}.RyhrFx6Y1FGDrGAAyaxm{padding-left:calc(var(--spacing-base)*7);padding-right:calc(var(--spacing-base)*7)}.CBwRpB0bDN3iEdQPPMJO{padding-bottom:calc(var(--spacing-base)*7)}.CBwRpB0bDN3iEdQPPMJO,.vQVSq6SvWKbOMu6r4H6b{padding-top:calc(var(--spacing-base)*7)}.oBy5__aEADMsH46mrgFX{padding-right:calc(var(--spacing-base)*7)}.KVEXoJqf1s92j0JMdNmN{padding-bottom:calc(var(--spacing-base)*7)}.ZMXGNrNaKW3k_3TLz0Fq{padding-left:calc(var(--spacing-base)*7)}.tuiR9PhkHXhGyEgzRZRI{padding:calc(var(--spacing-base)*8)}.U7454qyWkQNa2iaSJziu{padding-left:calc(var(--spacing-base)*8);padding-right:calc(var(--spacing-base)*8)}.VLYIv2GVocjuN93e8HC8{padding-bottom:calc(var(--spacing-base)*8)}.VLYIv2GVocjuN93e8HC8,.X1rm9DQ1zLGLfogja5Gn{padding-top:calc(var(--spacing-base)*8)}.JS7G6kAuqJo5GIuF8S5t{padding-right:calc(var(--spacing-base)*8)}.Y8F9ga1TDCMbM1lj4gUz{padding-bottom:calc(var(--spacing-base)*8)}.AJuyNGrI63BOWql719H8{padding-left:calc(var(--spacing-base)*8)}.terms-of-service{color:var(--jp-black);font-size:var(--font-body)}.terms-of-service .terms-of-service__link{color:var(--jp-green-50);white-space:nowrap}.jp-connection__connect-screen-layout{background:var(--jp-white);border-radius:4px;box-shadow:0 0 40px #00000014}.jp-connection__connect-screen-layout__loading{display:none}.jp-connection__connect-screen-layout__left,.jp-connection__connect-screen-layout__right{box-sizing:border-box}.jp-connection__connect-screen-layout__left{padding:calc(var(--spacing-base)*3)}@media(min-width:600px){.jp-connection__connect-screen-layout__left{padding:64px 96px}}.jp-connection__connect-screen-layout__left .jetpack-logo{margin-bottom:24px}.jp-connection__connect-screen-layout__left h2{color:var(--jp-black);font-size:36px;font-style:normal;font-weight:700;line-height:40px;margin-bottom:0;margin-top:32px}.jp-connection__connect-screen-layout__left h3{color:var(--jp-black);font-size:24px;font-style:normal;font-weight:500;line-height:32px;margin-bottom:0;margin-top:32px}.jp-connection__connect-screen-layout__left li,.jp-connection__connect-screen-layout__left p{font-size:16px;font-style:normal;font-weight:400;line-height:24px}.jp-connection__connect-screen-layout__left p{color:#101517;margin:16px 0}.jp-connection__connect-screen-layout__left a{color:var(--jp-black);font:inherit;height:auto;padding:0;text-decoration:underline}.jp-connection__connect-screen-layout__left a:hover{color:var(--jp-black);text-decoration-thickness:var(--jp-underline-thickness)}.jp-connection__connect-screen-layout__left a:focus{box-shadow:none!important;color:var(--jp-black)}.jp-connection__connect-screen-layout__left ul{list-style-type:none;padding:0}.jp-connection__connect-screen-layout__left ul li{background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAMAAABg3Am1AAAANlBMVEVHcEwFnwUInggGnggGnggHnAcAnwUFnQcAnwcGnwkFnQgGnQgFnwcGnQYFnQcFnAcGnQkDnwdhiL0pAAAAEnRSTlMAMF//f2Aw7yBQ3+9gcIBgcED+HDbkAAAAZklEQVR4Ae3LNwICARDDQC0+cv7/Y8mwV9odSfWIcf/+VegnGkIvDaGXKvTTn/Gz+Uf5xTL0K1XotS7fs5H6GHvvaO8d7c3j7rdgHne/A/PYt/cO+R42oYdN6OEQetiFHo4A//6dAXqtBEkmtWutAAAAAElFTkSuQmCC) no-repeat;background-size:24px;color:var(--jp-black);margin-bottom:9px;padding-left:30px}.jp-connection__connect-screen-layout__right{padding:64px 0}.jp-connection__connect-screen-layout__right img{max-width:100%}.jp-connection__connect-screen-layout__two-columns{display:flex;flex-wrap:wrap}.jp-connection__connect-screen-layout__two-columns .jp-connection__connect-screen-layout__left{flex-basis:100%;flex-grow:1}@media(min-width:1080px){.jp-connection__connect-screen-layout__two-columns .jp-connection__connect-screen-layout__left{flex-basis:52%}}.jp-connection__connect-screen-layout__two-columns .jp-connection__connect-screen-layout__right{background:#f9f9f6;display:none;flex-basis:47%;flex-grow:1}@media(min-width:1080px){.jp-connection__connect-screen-layout__two-columns .jp-connection__connect-screen-layout__right{display:block}}.rna{overflow:hidden}.rna .jp-connection__connect-screen-layout__left{position:relative;z-index:2}.rna .jp-connection__connect-screen-layout__left h2{font-weight:700;margin-top:0}.rna .jp-connection__connect-screen-layout__left h3{margin-top:24px}@media(min-width:600px){.rna .jp-connection__connect-screen-layout__left{padding:4rem 6rem 4rem 4rem}}@media(min-width:1080px){.rna .jp-connection__connect-screen-required-plan__pricing-card{position:absolute;right:calc(var(--spacing-base)*-45);top:calc(var(--spacing-base)*9.25)}}.rna .jp-connection__connect-screen-required-plan__pricing-card .jp-components__pricing-card{border-radius:var(--jp-border-radius-rna);max-width:100%;width:425px}.rna .jp-connection__connect-screen-required-plan__pricing-card .jp-components__pricing-card__title{margin-top:.625rem}@media(min-width:960px){.rna .jp-connection__connect-screen-required-plan__pricing-card .jp-components__pricing-card{padding:3rem}}.rna .jp-connection__connect-screen-required-plan__pricing-card .components-button{margin-bottom:0}.rna .jp-backup-dashboard-promotion ul.jp-product-promote li{margin-bottom:.75rem}.rna .jp-connection__connect-screen-layout__color-blobs{clip-path:polygon(100% 0,100% 100%,0 0,0 0);display:none;height:677px;position:absolute;right:0;top:0;width:363px;z-index:1}.rna .jp-connection__connect-screen-layout__color-blobs__blue,.rna .jp-connection__connect-screen-layout__color-blobs__green,.rna .jp-connection__connect-screen-layout__color-blobs__yellow{border-radius:50%;filter:blur(50px);position:absolute}.rna .jp-connection__connect-screen-layout__color-blobs__blue{background-color:var(--jp-blue-5);height:400px;right:-100px;top:-275px;width:400px;z-index:3}.rna .jp-connection__connect-screen-layout__color-blobs__yellow{background-color:var(--jp-yellow-5);height:250px;right:-25px;top:10px;width:250px;z-index:2}.rna .jp-connection__connect-screen-layout__color-blobs__green{background-color:var(--jp-green-5);height:300px;right:0;top:175px;width:300px;z-index:1}@media(min-width:1080px){.rna .jp-connection__connect-screen-layout__color-blobs{display:initial}}.jp-connection__connect-screen{--spacing-base:8px}.jp-connection__connect-screen__loading{display:none}.jp-connection__connect-screen .terms-of-service{margin-bottom:calc(var(--spacing-base)*3);margin-top:calc(var(--spacing-base)*4);max-width:360px}.jp-connection__connect-screen .terms-of-service a{text-decoration:underline}.jp-connection__connect-screen .jp-action-button{margin-top:40px}.jp-connection__connect-screen .jp-action-button--button{border-radius:4px;font-weight:600}.jp-connection__connect-screen .jp-action-button button{max-width:100%}.jp-connection__connect-screen .jp-action-button button:disabled{color:#fff6}@media(max-width:782px){.jp-connection__connect-screen .jp-action-button button{max-width:none;width:100%}}.jp-connection__connect-screen__loading-message{position:absolute;clip:rect(1px,1px,1px,1px);border:0;height:1px;overflow:hidden;padding:0;white-space:nowrap;width:1px}.jp-connection__connect-screen__loading-message:empty{display:none}.jp-connection__connect-screen__footer{margin-top:32px}@keyframes R2i0K45dEF157drbVRPI{0%{opacity:.6}50%{opacity:1}to{opacity:.6}}.NisihrgiIKl_knpYJtfg{animation:R2i0K45dEF157drbVRPI 1.5s infinite;background-color:var(--jp-gray);height:100%;width:100%}.jp-components__pricing-card{background:var(--jp-white);border-radius:var(--jp-border-radius);box-shadow:0 10px 40px #00000014;max-width:384px;padding:24px 24px 32px;width:-moz-fit-content;width:fit-content}@media screen and (min-width:600px){.jp-components__pricing-card{padding:32px 32px 44px}}.jp-components__pricing-card__icon img{height:32px;width:32px}.jp-components__pricing-card__title{color:#101517;font-size:32px;line-height:38px;margin:16px 0 24px}.jp-components__pricing-card__pricing{display:flex;flex-wrap:wrap}.jp-components__pricing-card__price-after,.jp-components__pricing-card__price-before{display:inline-block;font-size:54px;font-weight:700;line-height:40px;margin-bottom:8px;padding:0 2px}.jp-components__pricing-card__price-before{color:var(--jp-gray-20);margin-right:16px;position:relative}.jp-components__pricing-card__price-strikethrough{background:var(--jp-pink);border-radius:1.5px;height:3px;left:0;position:absolute;top:20px;width:100%}.jp-components__pricing-card__price-after{color:var(--jp-black)}.jp-components__pricing-card__currency{font-size:var(--font-title-small);font-weight:400;line-height:20px;vertical-align:super}.jp-components__pricing-card__price-details{align-self:flex-end;color:var(--jp-gray-50);font-size:14px;font-weight:400;letter-spacing:-.02em;line-height:17px;margin-bottom:8px}.jp-components__pricing-card__price-decimal{font-size:var(--font-label);line-height:14px;vertical-align:top}.jp-components__pricing-card__button{align-items:center;background:var(--jp-black);border-radius:var(--jp-border-radius);color:var(--jp-white)!important;font-size:18px;height:auto;justify-content:center;margin:24px 0 32px;padding:14px 24px;width:100%}.jp-components__pricing-card__info,.jp-components__pricing-card__tos{color:var(--jp-gray-60);font-size:var(--font-label);letter-spacing:-.02em;line-height:20px}.jp-components__pricing-card__tos{margin-top:24px}@media(min-width:1080px){.jp-connection__connect-screen-layout__left{width:calc(100% - 384px - var(--spacing-base)*4)}.jp-connection__connect-screen-required-plan{background:linear-gradient(90deg,#fff 70%,#f9f9f6 0);position:relative}}.jp-connection__connect-screen-required-plan__loading{display:none}.jp-connection__connect-screen-required-plan ul.jp-product-promote{margin-block-end:calc(var(--spacing-base)*4);margin-block-start:calc(var(--spacing-base)*3)}@media(min-width:1080px){.jp-connection__connect-screen-required-plan__pricing-card{position:absolute;right:calc(var(--spacing-base)*12);top:calc(var(--spacing-base)*8)}}.jp-connection__connect-screen-required-plan__pricing-card .jp-action-button--button.components-button{align-items:center;background:var(--jp-black)!important;border-radius:var(--jp-border-radius);color:var(--jp-white)!important;font-size:18px;font-weight:500;height:auto;justify-content:center;margin:24px 0 32px;padding:14px 24px;width:100%}.jp-connection__connect-screen-required-plan__pricing-card .jp-action-button--button.components-button:disabled{background:var(--jp-gray)!important;color:var(--jp-gray-20)!important}.jp-connection__connect-screen-required-plan__pricing-card .terms-of-service{margin-bottom:var(--spacing-base);margin-top:calc(var(--spacing-base)*4)}.jp-connection__connect-screen-required-plan__with-subscription{display:flex;flex-wrap:wrap;gap:1ch;justify-content:flex-start;line-height:1;margin-top:calc(var(--spacing-base)*4)}.jp-connection__connect-screen-required-plan__with-subscription .jp-action-button--button.components-button.is-primary{background:inherit!important;color:var(--jp-black)!important;display:inline;font:inherit;height:auto;min-width:0;padding:0;text-decoration:underline;width:auto}.jp-connection__connect-screen-required-plan__with-subscription .jp-action-button--button.components-button.is-primary:hover{background:inherit;text-decoration-thickness:var(--jp-underline-thickness)}.jp-connection__connect-screen-required-plan__with-subscription .jp-action-button--button.components-button.is-primary:focus{background:inherit;box-shadow:none!important}.jp-connection__connect-screen-required-plan__with-subscription .jp-components-spinner__inner,.jp-connection__connect-screen-required-plan__with-subscription .jp-components-spinner__outer{border-right-color:var(--jp-black);border-top-color:var(--jp-black)}.jp-iframe-wrap{text-align:center}.fade-in{animation:fadein 1.5s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}@keyframes rotate-spinner{to{transform:rotate(1turn)}}.jp-components-spinner{align-items:center;display:flex}.jp-components-spinner__inner,.jp-components-spinner__outer{animation:3s linear infinite;animation-name:rotate-spinner;border:.1em solid #0000;border-radius:50%;box-sizing:border-box;margin:auto}.jp-components-spinner__outer{border-top-color:#fff}.jp-components-spinner__inner{border-right-color:#fff;border-top-color:#fff;height:100%;opacity:.4;width:100%}.urouayitSUT8zW0V3p_0{margin-bottom:0}.iXXJlk08gFDeCvsTTlNQ{border:1px solid var(--jp-gray);border-left:6px solid var(--jp-red-50);border-radius:var(--jp-border-radius);box-shadow:0 4px 8px #00000008,0 1px 2px #0000000f;box-sizing:border-box;color:var(--jp-gray-80);font-size:16px;line-height:22px;margin:0;padding:calc(var(--spacing-base)*2) calc(var(--spacing-base)*3) calc(var(--spacing-base)*2) calc(var(--spacing-base)*3)}.iXXJlk08gFDeCvsTTlNQ.is-error{background-color:var(--jp-white)}.iXXJlk08gFDeCvsTTlNQ .components-notice__content{align-items:center;display:flex;flex-direction:column;margin:0;padding:12px 4px}.iXXJlk08gFDeCvsTTlNQ .is-link{color:var(--jp-black);font-size:16px;font-weight:600}.iXXJlk08gFDeCvsTTlNQ .components-notice__dismiss{align-self:center}.iXXJlk08gFDeCvsTTlNQ .MWqRqr7q6fgvLxitcWYk,.iXXJlk08gFDeCvsTTlNQ .MWqRqr7q6fgvLxitcWYk:active,.iXXJlk08gFDeCvsTTlNQ .MWqRqr7q6fgvLxitcWYk:hover,.iXXJlk08gFDeCvsTTlNQ .MWqRqr7q6fgvLxitcWYk:visited{align-items:center;border-radius:var(--jp-border-radius);cursor:pointer;display:inline-block;font-size:16px;font-weight:600;justify-content:center;letter-spacing:-.01em;line-height:24px;margin-left:calc(var(--spacing-base)*2 + 24px);margin-top:24px;padding:8px 24px;text-decoration:none}.iXXJlk08gFDeCvsTTlNQ .qExIAscWqEKaVy2cSTqb,.iXXJlk08gFDeCvsTTlNQ .qExIAscWqEKaVy2cSTqb:active,.iXXJlk08gFDeCvsTTlNQ .qExIAscWqEKaVy2cSTqb:hover,.iXXJlk08gFDeCvsTTlNQ .qExIAscWqEKaVy2cSTqb:visited{background:var(--jp-black);border:1px solid #0000;color:var(--jp-white)}.iXXJlk08gFDeCvsTTlNQ .fh6gO3o64bHDWoktxV1A,.iXXJlk08gFDeCvsTTlNQ .fh6gO3o64bHDWoktxV1A:active,.iXXJlk08gFDeCvsTTlNQ .fh6gO3o64bHDWoktxV1A:hover,.iXXJlk08gFDeCvsTTlNQ .fh6gO3o64bHDWoktxV1A:visited{background:#0000;border:1px solid var(--jp-gray-30);color:var(--jp-gray-80)}.iXXJlk08gFDeCvsTTlNQ .fh6gO3o64bHDWoktxV1A:active:hover,.iXXJlk08gFDeCvsTTlNQ .fh6gO3o64bHDWoktxV1A:hover,.iXXJlk08gFDeCvsTTlNQ .fh6gO3o64bHDWoktxV1A:hover:hover,.iXXJlk08gFDeCvsTTlNQ .fh6gO3o64bHDWoktxV1A:visited:hover{border-color:var(--jp-gray-50);color:var(--jp-gray-100)}.iXXJlk08gFDeCvsTTlNQ .ix1awakl4n7EjEZdShcd{display:flex;flex-direction:column;gap:calc(var(--spacing-base)*2);margin-top:calc(var(--spacing-base)*2);text-align:center}.iXXJlk08gFDeCvsTTlNQ .ix1awakl4n7EjEZdShcd .MWqRqr7q6fgvLxitcWYk,.iXXJlk08gFDeCvsTTlNQ .ix1awakl4n7EjEZdShcd .MWqRqr7q6fgvLxitcWYk:active,.iXXJlk08gFDeCvsTTlNQ .ix1awakl4n7EjEZdShcd .MWqRqr7q6fgvLxitcWYk:hover,.iXXJlk08gFDeCvsTTlNQ .ix1awakl4n7EjEZdShcd .MWqRqr7q6fgvLxitcWYk:visited{margin:0;max-width:300px;width:100%}@media(min-width:1100px){.iXXJlk08gFDeCvsTTlNQ .ix1awakl4n7EjEZdShcd{align-items:center;flex-direction:row;margin-left:0;margin-top:0}.iXXJlk08gFDeCvsTTlNQ .ix1awakl4n7EjEZdShcd .MWqRqr7q6fgvLxitcWYk,.iXXJlk08gFDeCvsTTlNQ .ix1awakl4n7EjEZdShcd .MWqRqr7q6fgvLxitcWYk:active,.iXXJlk08gFDeCvsTTlNQ .ix1awakl4n7EjEZdShcd .MWqRqr7q6fgvLxitcWYk:hover,.iXXJlk08gFDeCvsTTlNQ .ix1awakl4n7EjEZdShcd .MWqRqr7q6fgvLxitcWYk:visited{max-width:none;width:auto}.iXXJlk08gFDeCvsTTlNQ .MWqRqr7q6fgvLxitcWYk,.iXXJlk08gFDeCvsTTlNQ .MWqRqr7q6fgvLxitcWYk:active,.iXXJlk08gFDeCvsTTlNQ .MWqRqr7q6fgvLxitcWYk:hover,.iXXJlk08gFDeCvsTTlNQ .MWqRqr7q6fgvLxitcWYk:visited{margin-left:0;margin-top:0;white-space:nowrap}.iXXJlk08gFDeCvsTTlNQ .components-notice__content{align-items:center;flex-direction:row}}.e6hHy8BZ7ZKPSXbIC0UG{margin-bottom:25px}.jXz8LnXNzMDdtHqkG0sZ{display:flex;flex-grow:1;margin-right:var(--spacing-base)}.jXz8LnXNzMDdtHqkG0sZ>svg{align-self:flex-start;flex-shrink:0}.jXz8LnXNzMDdtHqkG0sZ .jp-components-spinner,.jXz8LnXNzMDdtHqkG0sZ>svg{margin-right:calc(var(--spacing-base)*2)}.jp-connection__disconnect-dialog h1{font-size:var(--font-title-small);font-weight:600;line-height:1.2;margin-top:0}.jp-connection__disconnect-dialog h2{font-size:var(--font-title-small);font-weight:400;line-height:1.2;margin:0}.jp-connection__disconnect-dialog p{font-size:var(--font-body);margin-top:0}.jp-connection__disconnect-dialog p.jp-connection__disconnect-dialog__large-text,.jp-connection__disconnect-dialog__large-text{font-size:1.25rem}.jp-connection__disconnect-dialog .jp-connection__disconnect-dialog__link,.jp-connection__disconnect-dialog__link{color:var(--jp-black);font:inherit;height:auto;padding:0;text-decoration:underline}.jp-connection__disconnect-dialog .jp-connection__disconnect-dialog__link:hover,.jp-connection__disconnect-dialog__link:hover{color:var(--jp-black);text-decoration-thickness:var(--jp-underline-thickness)}.jp-connection__disconnect-dialog .jp-connection__disconnect-dialog__link:focus,.jp-connection__disconnect-dialog__link:focus{box-shadow:none!important;color:var(--jp-black)}.jp-connection__disconnect-dialog .jp-connection__disconnect-dialog__link--bold,.jp-connection__disconnect-dialog__link--bold{font-weight:700}.jp-connection__disconnect-dialog .components-button{border-radius:4px;font-size:var(--font-body-small);height:40px}.jp-connection__disconnect-dialog .components-modal__content{display:flex;flex-direction:column;flex-grow:1;margin:0;padding:0}.jp-connection__disconnect-dialog .components-modal__content:before,.jp-connection__disconnect-dialog .components-modal__header{display:none}.jp-connection__disconnect-dialog .jp-row{align-items:center;width:calc(100% - 48px)}.jp-connection__disconnect-dialog__actions{background:var(--jp-white);border-top:1px solid var(--jp-gray);bottom:0;padding:2rem 0;position:sticky}.jp-connection__disconnect-dialog__actions p{margin-bottom:0}.jp-connection__disconnect-dialog__actions:before{background:linear-gradient(to bottom,#0000,var(--jp-white-off));bottom:calc(100% + 1px);content:"";display:block;height:80px;left:0;position:absolute;width:100%}.jp-connection__disconnect-dialog__btn-dismiss,.jp-connection__disconnect-dialog__btn-dismiss.components-button{background:var(--jp-black)!important;margin-right:10px}.jp-connection__disconnect-dialog__btn-disconnect{background:var(--jp-red)!important}.jp-connection__disconnect-dialog__btn-back-to-wp{background:var(--jp-black)!important}.jp-connection__disconnect-dialog__button-wrap{text-align:left}@media(min-width:960px){.jp-connection__disconnect-dialog__button-wrap{text-align:center}}.jp-connection__disconnect-dialog__error{color:var(--jp-red)}.jp-connection__disconnect-dialog__survey{margin-bottom:1.5rem;max-width:100%}.jp-connection__disconnect-dialog__step-copy{margin:0 auto;max-width:800px}.jp-connection__disconnect-dialog__step-copy--narrow{max-width:600px}.jp-connection__disconnect-dialog__content{align-items:center;background:var(--jp-white-off);border-radius:4px;display:flex;flex-direction:column;flex-grow:1;justify-content:center;margin:0;padding:2rem 1rem;text-align:center}@media(max-height:900px){.jp-connection__disconnect-dialog__content .jp-components__decorative-card{display:none}}@media(min-width:600px){.jp-connection__disconnect-dialog,.jp-connection__disconnect-dialog.components-modal__frame{max-width:calc(100% - 32px);width:100%}.jp-connection__disconnect-dialog__actions,.jp-connection__disconnect-dialog__content{padding:2rem}}@media(min-width:960px){.jp-connection__disconnect-dialog,.jp-connection__disconnect-dialog.components-modal__frame{display:flex;flex-direction:column;height:900px;width:1200px}.jp-connection__disconnect-dialog h1{font-size:var(--font-title-large)}.jp-connection__disconnect-dialog p.jp-connection__disconnect-dialog__large-text,.jp-connection__disconnect-dialog__large-text{font-size:1.5rem}.jp-connection__disconnect-dialog__content{padding:80px}.jp-connection__disconnect-dialog__actions{padding:2rem 3rem}.jp-row{margin-left:0}}.jp-connection__disconnect-card{background-color:var(--jp-white);border:none;border-radius:3px;box-shadow:0 0 15px var(--jp-gray-off);margin:0 auto 1rem;max-width:100%;padding:1rem 2rem;text-align:left;width:800px}.jp-connection__disconnect-card__group{margin-bottom:1rem;max-width:100%}.jp-connection__disconnect-card__card-content{display:block;font-size:.875rem}@media only screen and (min-width:782px){.jp-connection__disconnect-card__card-content{align-items:center;display:flex;justify-content:space-between}}.jp-connection__disconnect-card .jp-connection__disconnect-card__card-headline,.jp-connection__disconnect-card__card-headline{flex-shrink:0;font-size:1.25rem;font-weight:600;margin-bottom:0;margin-top:0}@media only screen and (min-width:782px){.jp-connection__disconnect-card .jp-connection__disconnect-card__card-headline,.jp-connection__disconnect-card__card-headline{font-size:1.5rem;margin-right:1.5rem}}@media only screen and (max-width:782px){.jp-connection__disconnect-card .jp-connection__disconnect-card__card-headline+.jp-disconnect-card__card-stat-block,.jp-connection__disconnect-card__card-headline+.jp-disconnect-card__card-stat-block{margin-top:.5rem}}.jp-connection__disconnect-card__card-stat-block{align-items:baseline;display:flex;flex-grow:1}@media only screen and (min-width:782px){.jp-connection__disconnect-card__card-stat-block{flex-direction:row-reverse}}.jp-connection__disconnect-card__card-description{flex-grow:1}@media only screen and (min-width:782px){.jp-connection__disconnect-card__card-description{text-align:right}}.jp-connection__disconnect-card__card-stat{font-size:1rem;font-weight:600;margin-right:.5rem}@media only screen and (min-width:782px){.jp-connection__disconnect-card__card-stat{font-size:1.5rem;margin-left:1rem;margin-right:0}}.jp-components__decorative-card{border-radius:8px;box-shadow:0 0 15px var(--jp-gray);display:flex;height:280px;margin:0 auto 3rem;max-width:100%;overflow:hidden;position:relative;width:360px}.jp-components__decorative-card__content,.jp-components__decorative-card__image{width:50%}.jp-components__decorative-card__image{background:var(--jp-gray);background-size:cover;position:relative}.jp-components__decorative-card__image:before{background-image:url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" width="38" height="8" fill="none" viewBox="0 0 38 8"><path stroke="%23fff" stroke-linejoin="round" stroke-width="1.5" d="M1 7s1.37-6 5.898-6 7.473 6 12 6 7.88-6 12.407-6C35.912 1 37 7 37 7"/></svg>');content:"";display:block;height:8px;left:24px;position:absolute;top:24px;width:38px}.jp-components__decorative-card__content{background:#fff;padding:2rem}.jp-components__decorative-card__icon-container{background:var(--jp-red);border-radius:50px;height:80px;left:50%;position:absolute;top:50%;transform:translate(-50%,-50%);width:80px}.jp-components__decorative-card__icon{background-position:50%,50%;background-repeat:no-repeat;height:40px;left:50%;position:absolute;top:50%;transform:translate(-50%,-50%);width:40px}.jp-components__decorative-card__icon--unlink{background-image:url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" width="34" height="37" fill="none" viewBox="0 0 34 37"><path stroke="%23fff" stroke-linecap="square" stroke-width="1.5" d="M22.334 10.001H25a8 8 0 0 1 8 8v1.778a8 8 0 0 1-8 8h-2.666M11.668 27.778H9a8 8 0 0 1-8-8v-1.777a8 8 0 0 1 8-8h2.667"/><path stroke="%23fff" stroke-width="1.5" d="M11 19.167h6"/><path stroke="%23fff" d="m9 35.998 16-35"/></svg>')}.jp-components__decorative-card__lines,.jp-components__decorative-card__lines:after,.jp-components__decorative-card__lines:before{background:#e9eff5;border-radius:6px;display:block;height:12px;position:relative;width:100%}.jp-components__decorative-card__lines:after,.jp-components__decorative-card__lines:before{content:"";top:calc(100% + 16px)}.jp-components__decorative-card__lines:after{top:calc(100% + 32px);width:75%}.jp-components__decorative-card--vertical{flex-direction:column}.jp-components__decorative-card--vertical .jp-components__decorative-card__content,.jp-components__decorative-card--vertical .jp-components__decorative-card__image{height:50%;width:100%}.jp-components__decorative-card--vertical .jp-components__decorative-card__lines{margin-left:auto;margin-right:auto;max-width:135px}.jp-components__decorative-card--vertical .jp-components__decorative-card__lines:after,.jp-components__decorative-card--vertical .jp-components__decorative-card__lines:before{margin-left:auto;margin-right:auto}.jp-connect__disconnect-survey-card{border:2px solid #0000;border-radius:4px;box-shadow:0 0 15px var(--jp-gray-off);margin-left:auto;margin-right:auto;max-width:100%;padding:1rem;position:relative;text-align:left;width:800px}.jp-connect__disconnect-survey-card--selected{background:var(--jp-gray-off);border-color:var(--jp-black)}.jp-connect__disconnect-survey-card:after{border-right:2px solid var(--jp-black);border-top:2px solid var(--jp-black);content:"";display:block;height:5px;position:absolute;right:1.5rem;top:50%;transform:translateY(-50%) rotate(45deg);width:5px}.jp-connect__disconnect-survey-card:hover{cursor:pointer}.jp-connect__disconnect-survey-card:focus:not(.jp-disconnect-survey-card--selected),.jp-connect__disconnect-survey-card:hover:not(.jp-disconnect-survey-card--selected){border-color:var(--jp-black-80)}.jp-connect__disconnect-survey-card__answer{align-items:center;display:flex;font-weight:700;margin:0}input.jp-connect__disconnect-survey-card__input{-webkit-appearance:none;background-color:#0000;border:none;color:var(--jp-black-80);flex-grow:1;max-width:calc(100% - 40px);padding-right:40px}.jp-connection__disconnect-dialog .components-button.jp-connection__disconnect-dialog__btn-dismiss{background:var(--jp-black)!important}.jp-connection__disconnect-dialog .jp-connection__disconnect-dialog__content{--spacing-base:8px}.jp-connection__disconnect-dialog .components-modal__content>div:not(.components-modal__header){display:flex;flex-direction:column;height:100%}:root{--font-title-large:36px;--font-title-small:24px;--font-body:16px;--font-label:12px;--jp-black:#000;--jp-black-80:#2c3338;--jp-white:#fff;--jp-white-off:#f9f9f6;--jp-gray:#dcdcde;--jp-gray-0:#f6f7f7;--jp-gray-5:#dcdcde;--jp-gray-10:#c3c4c7;--jp-gray-20:#a7aaad;--jp-gray-30:#8c8f94;--jp-gray-40:#787c82;--jp-gray-50:#646970;--jp-gray-60:#50575e;--jp-gray-70:#3c434a;--jp-gray-80:#2c3338;--jp-gray-90:#1d2327;--jp-gray-100:#101517;--jp-gray-off:#e2e2df;--jp-yellow-5:#f5e6b3;--jp-yellow-10:#f2cf75;--jp-yellow-40:#c08c00;--jp-orange-20:#faa754;--jp-blue-5:#ced9f2;--jp-red-0:#f7ebec;--jp-red-50:#d63638;--jp-red-60:#b32d2e;--jp-red-80:#8a2424;--jp-red:#d63639;--jp-pink:#c9356e;--jp-green-0:#f0f2eb;--jp-green-5:#d0e6b8;--jp-green-10:#9dd977;--jp-green-20:#64ca43;--jp-green-30:#2fb41f;--jp-green-40:#069e08;--jp-green-50:#008710;--jp-green-60:#007117;--jp-green-70:#005b18;--jp-green-80:#004515;--jp-green-90:#003010;--jp-green-100:#001c09;--jp-green:#069e08;--jp-green-mint:#d3f6d5;--jp-green-primary:var(--jp-green-40);--jp-green-secondary:var(--jp-green-30);--jp-border-radius:4px;--jp-border-radius-rna:8px;--jp-menu-border-height:1px;--jp-underline-thickness:2px;--jp-modal-padding-large:32px;--jp-modal-padding:24px;--jp-modal-padding-small:16px;--jp-modal-radius:8px;--jp-button-padding:8px;--jp-button-radius:4px;--jp-gap:16px;--jp-highlight:#3858e9}:where(body){font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif;margin:0;min-height:100%;padding:0}.jp-wrap{align-items:center;display:flex;flex-wrap:wrap;margin:0 auto;max-width:1128px}.jp-row{display:grid;grid-gap:24px;grid-template-columns:repeat(4,1fr);margin:0 16px;width:100%}@media(min-width:600px){.jp-row{grid-template-columns:repeat(8,1fr);margin:0 18px}}@media(min-width:960px){.jp-row{grid-template-columns:repeat(12,1fr);margin:0 24px;max-width:1128px}}.sm-col-span-1{grid-column-end:span 1}.sm-col-span-2{grid-column-end:span 2}.sm-col-span-3{grid-column-end:span 3}.sm-col-span-4{grid-column-end:span 4}@media(min-width:600px){.md-col-span-1{grid-column-end:span 1}.md-col-span-2{grid-column-end:span 2}.md-col-span-3{grid-column-end:span 3}.md-col-span-4{grid-column-end:span 4}.md-col-span-5{grid-column-end:span 5}.md-col-span-6{grid-column-end:span 6}.md-col-span-7{grid-column-end:span 7}.md-col-span-8{grid-column-end:span 8}}@media(min-width:960px){.lg-col-span-1{grid-column-end:span 1}.lg-col-span-2{grid-column-end:span 2}.lg-col-span-3{grid-column-end:span 3}.lg-col-span-4{grid-column-end:span 4}.lg-col-span-5{grid-column-end:span 5}.lg-col-span-6{grid-column-end:span 6}.lg-col-span-7{grid-column-end:span 7}.lg-col-span-8{grid-column-end:span 8}.lg-col-span-9{grid-column-end:span 9}.lg-col-span-10{grid-column-end:span 10}.lg-col-span-11{grid-column-end:span 11}.lg-col-span-12{grid-column-end:span 12}}@media(max-width:960px){.md-col-span-0{display:none}}@media(max-width:600px){.sm-col-span-0{display:none}}.jp-cut{border:2px solid var(--jp-green-primary);border-radius:var(--jp-border-radius);margin:32px 0;padding:16px 64px 16px 24px;position:relative;text-decoration:none}.jp-cut,.jp-cut span{display:block}.jp-cut span:last-of-type{font-weight:600}.jp-cut:focus span:last-of-type,.jp-cut:hover span:last-of-type{text-decoration:underline;text-decoration-thickness:var(--jp-underline-thickness)}.jp-cut:focus:after,.jp-cut:hover:after{transform:translateY(-50%) translateX(8px)}.jp-cut:after{color:var(--jp-green-primary);content:"→";font-size:24px;font-weight:600;position:absolute;right:24px;top:50%;transform:translateY(-50%);transition:transform .15s ease-out}.jp-connection__manage-dialog{--spacing-base:8px;border-radius:3px;margin:auto;width:1200px}.jp-connection__manage-dialog__content{align-items:center;background:var(--jp-white-off);display:flex;flex-direction:column;justify-content:center;padding:80px;text-align:center}.jp-connection__manage-dialog__content h1{font-size:var(--font-title-large);font-weight:700;line-height:1.2;margin:0}.jp-connection__manage-dialog .jp-connection__manage-dialog__large-text{font-size:1.25rem;font-weight:600;margin-bottom:calc(var(--spacing-base)*4);margin-top:calc(var(--spacing-base)*3);max-width:60%}.jp-connection__manage-dialog__actions{align-items:center;background:var(--jp-white);border-top:1px solid var(--jp-gray);bottom:0;box-sizing:border-box;margin:0!important;max-width:1200px!important;padding:calc(var(--spacing-base)*4) calc(var(--spacing-base)*5);position:sticky}.jp-connection__manage-dialog__link{color:var(--jp-black)}.jp-connection__manage-dialog__link:hover{color:var(--jp-black);text-decoration-thickness:var(--jp-underline-thickness)}.jp-connection__manage-dialog__link:focus{color:var(--jp-black)}.jp-connection__manage-dialog__button-wrap button{float:right}.jp-connection__manage-dialog__action-card{background-color:var(--jp-white);border:none;border-radius:3px;box-shadow:0 0 15px var(--jp-gray-off);margin:var(--spacing-base) auto;max-width:100%;padding:1rem 2rem;position:relative;text-align:left;width:750px}.jp-connection__manage-dialog__action-card__card-headline{font-size:var(--font-body);font-weight:600;line-height:calc(var(--spacing-base)*3);text-decoration:none}.jp-connection__manage-dialog__action-card__icon{float:right}.jp-connection__manage-dialog__action-card.disabled:before{background:var(--jp-black);border-radius:3px;content:"";display:block;height:100%;left:0;opacity:25%;position:absolute;top:0;width:100%}.jp-connection__manage-dialog__action-card .transfer,.jp-connection__manage-dialog__action-card .unlink{color:var(--jp-black);fill:var(--jp-black)}.jp-connection__manage-dialog__action-card .disconnect{color:var(--jp-red);fill:var(--jp-red)}.jp-connection__manage-dialog__action-card .check-users{color:var(--jp-black);fill:var(--jp-black)}.jp-connection__manage-dialog .components-notice{margin:var(--spacing-base) auto;max-width:100%;text-align:left;width:750px}.jp-connection__manage-dialog .components-modal__header{display:none}.jp-connection__manage-dialog .components-modal__content{margin:0;padding:0} jetpack-connection/dist/identity-crisis.asset.php 0000777 00000000314 15251741406 0016251 0 ustar 00 <?php return array('dependencies' => array('react', 'react-jsx-runtime', 'wp-components', 'wp-compose', 'wp-data', 'wp-element', 'wp-i18n', 'wp-polyfill', 'wp-url'), 'version' => 'a9e104da356324846150'); jetpack-connection/dist/identity-crisis.rtl.css 0000777 00000025447 15251741406 0015752 0 ustar 00 @keyframes rotate-spinner{to{transform:rotate(-1turn)}}.jp-components-spinner{align-items:center;display:flex}.jp-components-spinner__inner,.jp-components-spinner__outer{animation:3s linear infinite;animation-name:rotate-spinner;border:.1em solid #0000;border-radius:50%;box-sizing:border-box;margin:auto}.jp-components-spinner__outer{border-top-color:#fff}.jp-components-spinner__inner{border-left-color:#fff;border-top-color:#fff;height:100%;opacity:.4;width:100%}:root{--font-title-large:36px;--font-title-small:24px;--font-body:16px;--font-label:12px;--jp-black:#000;--jp-black-80:#2c3338;--jp-white:#fff;--jp-white-off:#f9f9f6;--jp-gray:#dcdcde;--jp-gray-0:#f6f7f7;--jp-gray-5:#dcdcde;--jp-gray-10:#c3c4c7;--jp-gray-20:#a7aaad;--jp-gray-30:#8c8f94;--jp-gray-40:#787c82;--jp-gray-50:#646970;--jp-gray-60:#50575e;--jp-gray-70:#3c434a;--jp-gray-80:#2c3338;--jp-gray-90:#1d2327;--jp-gray-100:#101517;--jp-gray-off:#e2e2df;--jp-yellow-5:#f5e6b3;--jp-yellow-10:#f2cf75;--jp-yellow-40:#c08c00;--jp-orange-20:#faa754;--jp-blue-5:#ced9f2;--jp-red-0:#f7ebec;--jp-red-50:#d63638;--jp-red-60:#b32d2e;--jp-red-80:#8a2424;--jp-red:#d63639;--jp-pink:#c9356e;--jp-green-0:#f0f2eb;--jp-green-5:#d0e6b8;--jp-green-10:#9dd977;--jp-green-20:#64ca43;--jp-green-30:#2fb41f;--jp-green-40:#069e08;--jp-green-50:#008710;--jp-green-60:#007117;--jp-green-70:#005b18;--jp-green-80:#004515;--jp-green-90:#003010;--jp-green-100:#001c09;--jp-green:#069e08;--jp-green-mint:#d3f6d5;--jp-green-primary:var(--jp-green-40);--jp-green-secondary:var(--jp-green-30);--jp-border-radius:4px;--jp-border-radius-rna:8px;--jp-menu-border-height:1px;--jp-underline-thickness:2px;--jp-modal-padding-large:32px;--jp-modal-padding:24px;--jp-modal-padding-small:16px;--jp-modal-radius:8px;--jp-button-padding:8px;--jp-button-radius:4px;--jp-gap:16px;--jp-highlight:#3858e9}:where(body){font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif;margin:0;min-height:100%;padding:0}.jp-wrap{align-items:center;display:flex;flex-wrap:wrap;margin:0 auto;max-width:1128px}.jp-row{display:grid;grid-gap:24px;grid-template-columns:repeat(4,1fr);margin:0 16px;width:100%}@media(min-width:600px){.jp-row{grid-template-columns:repeat(8,1fr);margin:0 18px}}@media(min-width:960px){.jp-row{grid-template-columns:repeat(12,1fr);margin:0 24px;max-width:1128px}}.sm-col-span-1{grid-column-end:span 1}.sm-col-span-2{grid-column-end:span 2}.sm-col-span-3{grid-column-end:span 3}.sm-col-span-4{grid-column-end:span 4}@media(min-width:600px){.md-col-span-1{grid-column-end:span 1}.md-col-span-2{grid-column-end:span 2}.md-col-span-3{grid-column-end:span 3}.md-col-span-4{grid-column-end:span 4}.md-col-span-5{grid-column-end:span 5}.md-col-span-6{grid-column-end:span 6}.md-col-span-7{grid-column-end:span 7}.md-col-span-8{grid-column-end:span 8}}@media(min-width:960px){.lg-col-span-1{grid-column-end:span 1}.lg-col-span-2{grid-column-end:span 2}.lg-col-span-3{grid-column-end:span 3}.lg-col-span-4{grid-column-end:span 4}.lg-col-span-5{grid-column-end:span 5}.lg-col-span-6{grid-column-end:span 6}.lg-col-span-7{grid-column-end:span 7}.lg-col-span-8{grid-column-end:span 8}.lg-col-span-9{grid-column-end:span 9}.lg-col-span-10{grid-column-end:span 10}.lg-col-span-11{grid-column-end:span 11}.lg-col-span-12{grid-column-end:span 12}}@media(max-width:960px){.md-col-span-0{display:none}}@media(max-width:600px){.sm-col-span-0{display:none}}.jp-cut{border:2px solid var(--jp-green-primary);border-radius:var(--jp-border-radius);margin:32px 0;padding:16px 24px 16px 64px;position:relative;text-decoration:none}.jp-cut,.jp-cut span{display:block}.jp-cut span:last-of-type{font-weight:600}.jp-cut:focus span:last-of-type,.jp-cut:hover span:last-of-type{text-decoration:underline;text-decoration-thickness:var(--jp-underline-thickness)}.jp-cut:focus:after,.jp-cut:hover:after{transform:translateY(-50%) translateX(-8px)}.jp-cut:after{color:var(--jp-green-primary);content:"→";font-size:24px;font-weight:600;left:24px;position:absolute;top:50%;transform:translateY(-50%);transition:transform .15s ease-out}.jp-idc__idc-screen .jp-idc__error-message{align-items:center;color:var(--jp-red);display:flex;flex-direction:row;justify-content:center;margin:15px 0}.jp-idc__idc-screen .jp-idc__error-message .error-gridicon{fill:var(--jp-red);margin-left:8px}.jp-idc__idc-screen .jp-idc__error-message a,.jp-idc__idc-screen .jp-idc__error-message span{color:var(--jp-red);font-size:var(--font-body)}.jp-idc__idc-screen .jp-idc__safe-mode .jp-idc__error-message{margin-top:5px}.jp-idc__idc-screen .jp-idc__idc-screen__cards.jp-idc__idc-screen__cards-error .jp-idc__idc-screen__card-action-base{padding-bottom:75px}.jp-idc__idc-screen .jp-idc__idc-screen__cards.jp-idc__idc-screen__cards-error .jp-idc__idc-screen__card-action-base.jp-idc__idc-screen__card-action-error{padding-bottom:5px}.jp-idc__idc-screen .jp-idc__idc-screen__cards.jp-idc__idc-screen__cards-error .jp-idc__idc-screen__card-action-base .jp-idc__error-message{height:40px}.jp-idc__idc-screen .jp-idc__safe-mode{text-align:center}.jp-idc__idc-screen .jp-idc__safe-mode .jp-idc__safe-mode__staying-safe{display:flex;justify-content:center;padding:6px}.jp-idc__idc-screen .jp-idc__safe-mode .jp-idc__safe-mode__staying-safe .jp-components-spinner{margin:0 10px}.jp-idc__idc-screen .jp-idc__safe-mode,.jp-idc__idc-screen .jp-idc__safe-mode button{color:#2c3338;font-size:16px;line-height:24px}.jp-idc__idc-screen .jp-idc__safe-mode button{padding:0;text-decoration:underline}.jp-idc__idc-screen{background:#fff;border-radius:4px;border-right:4px solid #e68b28;box-shadow:0 0 40px #0000000a;box-sizing:border-box;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif;margin:0 auto;max-width:1128px;padding:10px}.jp-idc__idc-screen.jp-idc__idc-screen__success{border-color:#069e08}@media(min-width:600px){.jp-idc__idc-screen{padding:48px}}.jp-idc__idc-screen .jp-idc__idc-screen__header{align-items:center;display:flex}.jp-idc__idc-screen .jp-idc__idc-screen__header .jp-idc__idc-screen__logo-image{max-height:100px;max-width:100px}.jp-idc__idc-screen .jp-idc__idc-screen__header .jp-idc__idc-screen__logo-label{font-size:14px;line-height:22px;margin:-7px 8px 0 0}.jp-idc__idc-screen h2{font-size:24px;font-weight:600;line-height:28px;margin:32px 0 0}.jp-idc__idc-screen h3{font-size:20px;font-weight:600;line-height:28px;margin:24px 0 0}.jp-idc__idc-screen p{color:#2c3338;font-size:16px;line-height:24px;margin:16px 0 0;max-width:710px}.jp-idc__idc-screen a{color:#2c3338;text-decoration-line:underline!important}.jp-idc__idc-screen ul{font-size:16px;list-style-type:disc;padding-right:20px}.jp-idc__idc-screen .jp-idc__idc-screen__cards{align-items:center;display:flex;flex-direction:column;flex-wrap:wrap}@media only screen and (min-width:1403px){.jp-idc__idc-screen .jp-idc__idc-screen__cards{align-items:normal;flex-direction:row}}.jp-idc__idc-screen .jp-idc__idc-screen__cards .jp-idc__idc-screen__cards-separator{align-self:center;color:#23282d;font-size:20px;font-weight:600;line-height:28px;margin:0 24px}.jp-idc__idc-screen .jp-idc__idc-screen__cards .jp-idc__idc-screen__card-action-base{border:1px solid #c3c4c7;border-radius:4px;box-sizing:border-box;display:flex;flex-direction:column;justify-content:space-between;margin:24px 0;max-width:100%;padding:10px;width:480px}@media(min-width:600px){.jp-idc__idc-screen .jp-idc__idc-screen__cards .jp-idc__idc-screen__card-action-base{padding:24px}}.jp-idc__idc-screen .jp-idc__idc-screen__cards .jp-idc__idc-screen__card-action-base h4{font-size:20px;font-weight:400;line-height:28px;margin:0 0 8px}.jp-idc__idc-screen .jp-idc__idc-screen__cards .jp-idc__idc-screen__card-action-base p{margin:0 0 24px}.jp-idc__idc-screen .jp-idc__idc-screen__cards .jp-idc__idc-screen__card-action-base .jp-idc__idc-screen__card-action-sitename{background:#f9f9f6;border-radius:33px;box-sizing:border-box;color:#2c3338;font-size:16px;font-weight:700;line-height:24px;overflow-wrap:anywhere;padding:16px;text-align:center;width:100%}.jp-idc__idc-screen .jp-idc__idc-screen__cards .jp-idc__idc-screen__card-action-base .jp-idc__idc-screen__card-action-separator{display:block;margin:12px auto}.jp-idc__idc-screen .jp-idc__idc-screen__cards .jp-idc__idc-screen__card-action-base .jp-idc__idc-screen__card-action-button,.jp-idc__idc-screen .jp-idc__idc-screen__cards .jp-idc__idc-screen__card-action-base .jp-idc__idc-screen__card-action-button-secondary{padding:8px 24px;width:100%}.jp-idc__idc-screen .jp-idc__idc-screen__card-action-button,.jp-idc__idc-screen .jp-idc__idc-screen__card-action-button-secondary{background:#000;border-radius:4px;color:#fff;font-size:16px;font-weight:600;height:auto;justify-content:center;line-height:24px;margin-top:24px;min-height:40px;padding:8px}.jp-idc__idc-screen .jp-idc__idc-screen__card-action-button-secondary{background:var(--jp-white);box-shadow:inset 0 0 0 1.51px var(--jp-black);color:var(--jp-black)}.jp-idc__idc-screen .jp-idc__idc-screen__card-action-button-migrated{margin-top:64px;width:141px}.jp-idc__idc-screen .jp-idc__idc-screen__card-migrated{align-items:center;display:flex;flex-direction:column;flex-wrap:wrap;margin-top:24px;width:100%}.jp-idc__idc-screen .jp-idc__idc-screen__card-migrated .jp-idc__idc-screen__card-migrated-hostname{border:1px solid #c3c4c7;border-radius:4px;color:#2c3338;flex-grow:1;font-size:16px;font-weight:700;line-height:24px;padding:24px;width:100%}.jp-idc__idc-screen .jp-idc__idc-screen__card-migrated .jp-idc__idc-screen__card-migrated-separator-wide{display:none}.jp-idc__idc-screen .jp-idc__idc-screen__card-migrated .jp-idc__idc-screen__card-migrated-separator{display:block}.jp-idc__idc-screen .jp-idc__idc-screen__card-migrated .jp-idc__idc-screen__card-migrated-separator,.jp-idc__idc-screen .jp-idc__idc-screen__card-migrated .jp-idc__idc-screen__card-migrated-separator-wide{margin:28px}@media only screen and (min-width:1400px){.jp-idc__idc-screen .jp-idc__idc-screen__card-migrated{flex-direction:row;width:auto}.jp-idc__idc-screen .jp-idc__idc-screen__card-migrated .jp-idc__idc-screen__card-migrated-separator{display:none}.jp-idc__idc-screen .jp-idc__idc-screen__card-migrated .jp-idc__idc-screen__card-migrated-separator-wide{display:block}.jp-idc__idc-screen .jp-idc__idc-screen__card-migrated .jp-idc__idc-screen__card-migrated-hostname{width:auto}}#wpadminbar #wp-admin-bar-jetpack-idc{margin-left:5px}#wpadminbar #wp-admin-bar-jetpack-idc .jp-idc-admin-bar{border-radius:2px;color:#efeff0;font-size:14px;font-weight:500;line-height:20px;padding:6px 8px}#wpadminbar #wp-admin-bar-jetpack-idc.hide{display:none}#wpadminbar #wp-admin-bar-jetpack-idc .dashicons{font-family:dashicons;margin-top:-6px}#wpadminbar #wp-admin-bar-jetpack-idc .dashicons:before{font-size:18px}#wpadminbar #wp-admin-bar-jetpack-idc .ab-item{background:#e68b28;padding:0}#jp-identity-crisis-container .jp-idc__idc-screen{margin-bottom:40px;margin-top:40px}#jp-identity-crisis-container.notice{background:none;border:none} jetpack-connection/dist/images/disconnect-thanks-5873bfac56a9bd7322cd.jpg 0000777 00000154464 15251741406 0021665 0 ustar 00 ��� JFIF �� � %%2 %%2�� >�"