diff options
author | Anthony G. Basile <blueness@gentoo.org> | 2020-01-06 14:32:30 -0500 |
---|---|---|
committer | Anthony G. Basile <blueness@gentoo.org> | 2020-01-06 14:32:30 -0500 |
commit | 10ef81bf85ad0a4bad0d204838e14c99ca2526f7 (patch) | |
tree | b4bb36a326d41de12d1a6181d2a2baf34696ac24 /plugins/jetpack/vendor/automattic | |
parent | Updating script for Update (diff) | |
download | blogs-gentoo-10ef81bf85ad0a4bad0d204838e14c99ca2526f7.tar.gz blogs-gentoo-10ef81bf85ad0a4bad0d204838e14c99ca2526f7.tar.bz2 blogs-gentoo-10ef81bf85ad0a4bad0d204838e14c99ca2526f7.zip |
Update jetpack 8.0
Signed-off-by: Anthony G. Basile <blueness@gentoo.org>
Diffstat (limited to 'plugins/jetpack/vendor/automattic')
78 files changed, 24993 insertions, 0 deletions
diff --git a/plugins/jetpack/vendor/automattic/jetpack-abtest/src/class-abtest.php b/plugins/jetpack/vendor/automattic/jetpack-abtest/src/class-abtest.php new file mode 100644 index 00000000..8bf6b34e --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-abtest/src/class-abtest.php @@ -0,0 +1,102 @@ +<?php +/** + * A class that interacts with WP.com A/B tests. + * + * @package automattic/jetpack-abtest + */ + +namespace Automattic\Jetpack; + +use Automattic\Jetpack\Connection\Client; +use Automattic\Jetpack\Error; + +/** + * This class provides an interface to the WP.com A/B tests. + */ +class Abtest { + /** + * A variable to hold the tests we fetched, and their variations for the current user. + * + * @access private + * + * @var array + */ + private $tests = array(); + + /** + * Retrieve the test variation for a provided A/B test. + * + * @access public + * + * @param string $test_name Name of the A/B test. + * @return mixed|null A/B test variation, or null on failure. + */ + public function get_variation( $test_name ) { + $variation = $this->fetch_variation( $test_name ); + + // If there was an error retrieving a variation, conceal the error for the consumer. + if ( is_wp_error( $variation ) ) { + return null; + } + + return $variation; + } + + /** + * Fetch and cache the test variation for a provided A/B test from WP.com. + * + * @access protected + * + * @param string $test_name Name of the A/B test. + * @return mixed|Automattic\Jetpack\Error A/B test variation, or Automattic\Jetpack\Error on failure. + */ + protected function fetch_variation( $test_name ) { + // Make sure test name exists. + if ( ! $test_name ) { + return new Error( 'test_name_not_provided', 'A/B test name has not been provided.' ); + } + + // Make sure test name is a valid one. + if ( ! preg_match( '/^[A-Za-z0-9_]+$/', $test_name ) ) { + return new Error( 'invalid_test_name', 'Invalid A/B test name.' ); + } + + // Return cached test variations. + if ( isset( $this->tests[ $test_name ] ) ) { + return $this->tests[ $test_name ]; + } + + // Make the request to the WP.com API. + $response = $this->request_variation( $test_name ); + + // Bail if there was an error or malformed response. + if ( is_wp_error( $response ) || ! is_array( $response ) || ! isset( $response['body'] ) ) { + return new Error( 'failed_to_fetch_data', 'Unable to fetch the requested data.' ); + } + + // Decode the results. + $results = json_decode( $response['body'], true ); + + // Bail if there were no results or there is no test variation returned. + if ( ! is_array( $results ) || empty( $results['variation'] ) ) { + return new Error( 'unexpected_data_format', 'Data was not returned in the expected format.' ); + } + + // Store the variation in our internal cache. + $this->tests[ $test_name ] = $results['variation']; + + return $results['variation']; + } + + /** + * Perform the request for a variation of a provided A/B test from WP.com. + * + * @access protected + * + * @param string $test_name Name of the A/B test. + * @return mixed|Automattic\Jetpack\Error A/B test variation, or Automattic\Jetpack\Error on failure. + */ + protected function request_variation( $test_name ) { + return Client::wpcom_json_api_request_as_blog( sprintf( '/abtest/%s', $test_name ), '2', array(), null, 'wpcom' ); + } +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-assets/src/class-assets.php b/plugins/jetpack/vendor/automattic/jetpack-assets/src/class-assets.php new file mode 100644 index 00000000..7713aadf --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-assets/src/class-assets.php @@ -0,0 +1,42 @@ +<?php +/** + * Jetpack Assets package. + * + * @package automattic/jetpack-assets + */ + +namespace Automattic\Jetpack; + +use Automattic\Jetpack\Constants as Jetpack_Constants; + +/** + * Class Assets + */ +class Assets { + /** + * Constructor. + * + * Static-only class, so nothing here. + */ + private function __construct() {} + + /** + * 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. + * + * Both `$min_base` and `$non_min_base` are expected to be relative to the + * root Jetpack directory. + * + * @since 5.6.0 + * + * @param string $min_path minified path. + * @param string $non_min_path non-minified path. + * @return string The URL to the file + */ + public static function get_file_url_for_environment( $min_path, $non_min_path ) { + $path = ( Jetpack_Constants::is_defined( 'SCRIPT_DEBUG' ) && Jetpack_Constants::get_constant( 'SCRIPT_DEBUG' ) ) + ? $non_min_path + : $min_path; + return plugins_url( $path, Jetpack_Constants::get_constant( 'JETPACK__PLUGIN_FILE' ) ); + } +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-backup/actions.php b/plugins/jetpack/vendor/automattic/jetpack-backup/actions.php new file mode 100644 index 00000000..6a0a55a3 --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-backup/actions.php @@ -0,0 +1,13 @@ +<?php +/** + * Action Hooks for Jetpack Backup module. + * + * @package automattic/jetpack-backup + */ + +if ( ! defined( 'ABSPATH' ) ) { + return; +} + +// Clean up expired Helper Scripts from a scheduled event. +add_action( 'jetpack_backup_cleanup_helper_scripts', array( 'Automattic\\Jetpack\\Backup\\Helper_Script_Manager', 'cleanup_expired_helper_scripts' ) ); diff --git a/plugins/jetpack/vendor/automattic/jetpack-backup/src/class-helper-script-manager.php b/plugins/jetpack/vendor/automattic/jetpack-backup/src/class-helper-script-manager.php new file mode 100644 index 00000000..58c54eb4 --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-backup/src/class-helper-script-manager.php @@ -0,0 +1,347 @@ +<?php +/** + * The Jetpack Backup Helper Script Manager class. + * + * @package automattic/jetpack-backup + */ + +namespace Automattic\Jetpack\Backup; + +/** + * Helper_Script_Manager manages installation, deletion and cleanup of Helper Scripts + * to assist with backing up Jetpack Sites. + */ +class Helper_Script_Manager { + + const TEMP_DIRECTORY = 'jetpack-temp'; + const HELPER_HEADER = "<?php /* Jetpack Backup Helper Script */\n"; + const EXPIRY_TIME = 8 * 3600; // 8 hours + const MAX_FILESIZE = 1024 * 1024; // 1 MiB + + const README_LINES = array( + 'These files have been put on your server by Jetpack to assist with backups and restores of your site content. They are cleaned up automatically when we no longer need them.', + 'If you no longer have Jetpack connected to your site, you can delete them manually.', + 'If you have questions or need assistance, please contact Jetpack Support at https://jetpack.com/support/', + 'If you like to build amazing things with WordPress, you should visit automattic.com/jobs and apply to join the fun – mention this file when you apply!;', + ); + + const INDEX_FILE = '<?php // Silence is golden'; + + /** + * Installs a Helper Script, and returns its filesystem path and access url. + * + * @access public + * @static + * + * @param string $script_body Helper Script file contents. + * @return array|WP_Error Either an array containing the path and url of the helper script, or an error. + */ + public static function install_helper_script( $script_body ) { + // Check that the script body contains the correct header. + if ( strncmp( $script_body, self::HELPER_HEADER, strlen( self::HELPER_HEADER ) ) !== 0 ) { + return new \WP_Error( 'invalid_helper', 'Invalid Helper Script header' ); + } + + // Refuse to install a Helper Script that is too large. + if ( strlen( $script_body ) > self::MAX_FILESIZE ) { + return new \WP_Error( 'invalid_helper', 'Invalid Helper Script size' ); + } + + // Replace '[wp_path]' in the Helper Script with the WordPress installation location. Allows the Helper Script to find WordPress. + $script_body = str_replace( '[wp_path]', addslashes( ABSPATH ), $script_body ); + + // Create a jetpack-temp directory for the Helper Script. + $temp_directory = self::create_temp_directory(); + if ( \is_wp_error( $temp_directory ) ) { + return $temp_directory; + } + + // Generate a random filename, avoid clashes. + $max_attempts = 5; + for ( $attempt = 0; $attempt < $max_attempts; $attempt++ ) { + $file_key = wp_generate_password( 10, false ); + $file_name = 'jp-helper-' . $file_key . '.php'; + $file_path = trailingslashit( $temp_directory['path'] ) . $file_name; + + if ( ! file_exists( $file_path ) ) { + // Attempt to write helper script. + if ( ! self::put_contents( $file_path, $script_body ) ) { + if ( file_exists( $file_path ) ) { + unlink( $file_path ); + } + + continue; + } + + // Always schedule a cleanup run shortly after EXPIRY_TIME. + \wp_schedule_single_event( time() + self::EXPIRY_TIME + 60, 'jetpack_backup_cleanup_helper_scripts' ); + + // Success! Figure out the URL and return the path and URL. + return array( + 'path' => $file_path, + 'url' => trailingslashit( $temp_directory['url'] ) . $file_name, + ); + } + } + + return new \WP_Error( 'install_faied', 'Failed to install Helper Script' ); + } + + /** + * Given a path, verify it looks like a helper script and then delete it if so. + * + * @access public + * @static + * + * @param string $path Path to Helper Script to delete. + * @return boolean True if the file is deleted (or does not exist). + */ + public static function delete_helper_script( $path ) { + if ( ! file_exists( $path ) ) { + return true; + } + + // Check this file looks like a JPR helper script. + if ( ! self::verify_file_header( $path, self::HELPER_HEADER ) ) { + return false; + } + + return unlink( $path ); + } + + /** + * Search for Helper Scripts that are suspiciously old, and clean them out. + * + * @access public + * @static + */ + public static function cleanup_expired_helper_scripts() { + self::cleanup_helper_scripts( time() - self::EXPIRY_TIME ); + } + + /** + * Search for and delete all Helper Scripts. Used during uninstallation. + * + * @access public + * @static + */ + public static function delete_all_helper_scripts() { + self::cleanup_helper_scripts( null ); + } + + /** + * Search for and delete Helper Scripts. If an $expiry_time is specified, only delete Helper Scripts + * with an mtime older than $expiry_time. Otherwise, delete them all. + * + * @access public + * @static + * + * @param int|null $expiry_time If specified, only delete scripts older than $expiry_time. + */ + public static function cleanup_helper_scripts( $expiry_time = null ) { + foreach ( self::get_install_locations() as $directory => $url ) { + $temp_dir = trailingslashit( $directory ) . self::TEMP_DIRECTORY; + + if ( is_dir( $temp_dir ) ) { + // Find expired helper scripts and delete them. + $helper_scripts = glob( trailingslashit( $temp_dir ) . 'jp-helper-*.php' ); + if ( is_array( $helper_scripts ) ) { + foreach ( $helper_scripts as $filename ) { + if ( null === $expiry_time || filemtime( $filename ) < $expiry_time ) { + self::delete_helper_script( $filename ); + } + } + } + + // Delete the directory if it's empty now. + self::delete_empty_helper_directory( $temp_dir ); + } + } + } + + /** + * Delete a helper script directory if it's empty + * + * @access public + * @static + * + * @param string $dir Path to Helper Script directory. + * @return boolean True if the directory is deleted + */ + private static function delete_empty_helper_directory( $dir ) { + if ( ! is_dir( $dir ) ) { + return false; + } + + // Tally the files in the target directory, and reject if there are too many. + $glob_path = trailingslashit( $dir ) . '*'; + $dir_contents = glob( $glob_path ); + if ( count( $dir_contents ) > 2 ) { + return false; + } + + // Check that the only remaining files are a README and index.php generated by this system. + $allowed_files = array( + 'README' => self::README_LINES[0], + 'index.php' => self::INDEX_FILE, + ); + + foreach ( $dir_contents as $path ) { + $basename = basename( $path ); + if ( ! isset( $allowed_files[ $basename ] ) ) { + return false; + } + + // Verify the file starts with the expected contents. + if ( ! self::verify_file_header( $path, $allowed_files[ $basename ] ) ) { + return false; + } + + if ( ! unlink( $path ) ) { + return false; + } + } + + // If the directory is now empty, delete it. + if ( count( glob( $glob_path ) ) === 0 ) { + return rmdir( $dir ); + } + + return false; + } + + /** + * Find an appropriate location for a jetpack-temp folder, and create one + * + * @access public + * @static + * + * @return WP_Error|array Array containing the url and path of the temp directory if successful, WP_Error if not. + */ + private static function create_temp_directory() { + foreach ( self::get_install_locations() as $directory => $url ) { + // Check if the install location is writeable. + if ( ! is_writeable( $directory ) ) { + continue; + } + + // Create if one doesn't already exist. + $temp_dir = trailingslashit( $directory ) . self::TEMP_DIRECTORY; + if ( ! is_dir( $temp_dir ) ) { + if ( ! mkdir( $temp_dir ) ) { + continue; + } + + // Temp directory created. Drop a README and index.php file in there. + self::write_supplementary_temp_files( $temp_dir ); + } + + return array( + 'path' => trailingslashit( $directory ) . self::TEMP_DIRECTORY, + 'url' => trailingslashit( $url ) . self::TEMP_DIRECTORY, + ); + } + + return new \WP_Error( 'temp_directory', 'Failed to create jetpack-temp directory' ); + } + + /** + * Write out an index.php file and a README file for a new jetpack-temp directory. + * + * @access public + * @static + * + * @param string $dir Path to Helper Script directory. + */ + private static function write_supplementary_temp_files( $dir ) { + $readme_path = trailingslashit( $dir ) . 'README'; + self::put_contents( $readme_path, implode( "\n\n", self::README_LINES ) ); + + $index_path = trailingslashit( $dir ) . 'index.php'; + self::put_contents( $index_path, self::INDEX_FILE ); + } + + /** + * Write a file to the specified location with the specified contents. + * + * @access private + * @static + * + * @param string $file_path Path to write to. + * @param string $contents File contents to write. + * @return boolean True if successfully written. + */ + private static function put_contents( $file_path, $contents ) { + global $wp_filesystem; + + if ( ! function_exists( '\\WP_Filesystem' ) ) { + require_once ABSPATH . 'wp-admin/includes/file.php'; + } + + if ( ! \WP_Filesystem() ) { + return false; + } + + return $wp_filesystem->put_contents( $file_path, $contents ); + } + + /** + * Checks that a file exists, is readable, and has the expected header. + * + * @access private + * @static + * + * @param string $file_path File to verify. + * @param string $expected_header Header that the file should have. + * @return boolean True if the file exists, is readable, and the header matches. + */ + private static function verify_file_header( $file_path, $expected_header ) { + global $wp_filesystem; + + if ( ! function_exists( '\\WP_Filesystem' ) ) { + require_once ABSPATH . 'wp-admin/includes/file.php'; + } + + if ( ! \WP_Filesystem() ) { + return false; + } + + // Verify the file exists and is readable. + if ( ! $wp_filesystem->exists( $file_path ) || ! $wp_filesystem->is_readable( $file_path ) ) { + return false; + } + + // Verify that the file isn't too big or small. + $file_size = $wp_filesystem->size( $file_path ); + if ( $file_size < strlen( $expected_header ) || $file_size > self::MAX_FILESIZE ) { + return false; + } + + // Read the file and verify its header. + $contents = $wp_filesystem->get_contents( $file_path ); + return ( strncmp( $contents, $expected_header, strlen( $expected_header ) ) === 0 ); + } + + /** + * Gets an associative array of possible places to install a jetpack-temp directory, along with the URL to access each. + * + * @access private + * @static + * + * @return array Array, with keys specifying the full path of install locations, and values with the equivalent URL. + */ + public static function get_install_locations() { + // Include WordPress root and wp-content. + $install_locations = array( + \ABSPATH => \get_site_url(), + \WP_CONTENT_DIR => \WP_CONTENT_URL, + ); + + // Include uploads folder. + $upload_dir_info = \wp_upload_dir(); + $install_locations[ $upload_dir_info['basedir'] ] = $upload_dir_info['baseurl']; + + return $install_locations; + } + +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-compat/functions.php b/plugins/jetpack/vendor/automattic/jetpack-compat/functions.php new file mode 100644 index 00000000..f326a46c --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-compat/functions.php @@ -0,0 +1,25 @@ +<?php +/** + * Legacy global scope functions. + * + * @package automattic/jetpack-compat + */ + +if ( ! defined( 'ABSPATH' ) ) { + return; +} + +// Add here, after the condition above, any code that should only run when WordPress is running. +// Autoload will load everything even when PHPCS is running and we don't want to run these +// in such case because they will fatal, for example, due to 'add_action' being undefined. + +/** + * Load necessary functions. + */ +function jetpack_compat_require_defined_functions() { + jetpack_require_lib( 'tracks/client' ); +} + +add_action( 'plugins_loaded', 'jetpack_compat_require_defined_functions' ); + + diff --git a/plugins/jetpack/vendor/automattic/jetpack-compat/legacy/class-jetpack-client.php b/plugins/jetpack/vendor/automattic/jetpack-compat/legacy/class-jetpack-client.php new file mode 100644 index 00000000..1b71cdd7 --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-compat/legacy/class-jetpack-client.php @@ -0,0 +1,90 @@ +<?php +/** + * Jetpack Client + * + * Deprecated methods for Jetpack to act as client with wpcom, provided for back-compatibility. + * + * @category Connection + * @package automattic/jetpack-compat + */ + +use Automattic\Jetpack\Connection\Client; + +/** + * Class Jetpack_Client + * + * @deprecated Use Automattic\Jetpack\Connection\Client + */ +class Jetpack_Client { + + /** + * Jetpack API version. + * + * @deprecated use Automattic\Jetpack\Connection\Client::WPCOM_JSON_API_VERSION + */ + const WPCOM_JSON_API_VERSION = '1.1'; + + /** + * Perform remote request. + * + * @deprecated use Automattic\Jetpack\Connection\Client::remote_request + * + * @param array $args Arguments. + * @param null $body Request body. + * + * @return array|WP_Error + */ + public static function remote_request( $args, $body = null ) { + _deprecated_function( __METHOD__, 'jetpack-7.5', 'Automattic\Jetpack\Connection\Client' ); + return Client::remote_request( $args, $body ); + } + + /** + * Request to wpcom using the blog id. + * + * @deprecated use Automattic\Jetpack\Connection\Client::wpcom_json_api_request_as_blog + * + * @param string $path Endpoint path. + * @param string $version Endpoint version. + * @param array $args Arguments. + * @param null $body Request body. + * @param string $base_api_path Endpoint base prefix. + * + * @return 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' + ) { + _deprecated_function( __METHOD__, 'jetpack-7.5', 'Automattic\Jetpack\Connection\Client' ); + return Client::wpcom_json_api_request_as_blog( $path, $version, $args, $body, $base_api_path ); + } + + /** + * Wrapper for wp_remote_request(). Turns off SSL verification for certain SSL errors. + * This is suboptimal, but many, many, many hosts have misconfigured SSL. + * + * @deprecated use Automattic\Jetpack\Connection\Client::_wp_remote_request + * + * 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 + * @see Utils::fix_url_for_bad_hosts() + * + * @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 + */ + public static function _wp_remote_request( $url, $args, $set_fallback = false ) { // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore + _deprecated_function( __METHOD__, 'jetpack-7.5', 'Automattic\Jetpack\Connection\Client' ); + return Client::_wp_remote_request( $url, $args, $set_fallback ); + } +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-compat/legacy/class-jetpack-sync-actions.php b/plugins/jetpack/vendor/automattic/jetpack-compat/legacy/class-jetpack-sync-actions.php new file mode 100644 index 00000000..5e8c325a --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-compat/legacy/class-jetpack-sync-actions.php @@ -0,0 +1,359 @@ +<?php +/** + * A compatibility shim for the sync actions class. + * + * @package automattic/jetpack-compat + */ + +use Automattic\Jetpack\Sync\Actions; + +/** + * Class Jetpack_Sync_Actions + * + * @deprecated Use Automattic\Jetpack\Sync\Actions + */ +class Jetpack_Sync_Actions extends Automattic\Jetpack\Sync\Actions { + + /** + * Initializes the class. + * + * @deprecated Automattic\Jetpack\Sync\Actions::init + */ + public static function init() { + _deprecated_function( __METHOD__, 'jetpack-7.5', 'Automattic\Jetpack\Sync\Actions' ); + + return Actions::init(); + } + + /** + * Adds a shutdown sender callback. + * + * @deprecated Automattic\Jetpack\Sync\Actions::add_sender_shutdown + */ + public static function add_sender_shutdown() { + _deprecated_function( __METHOD__, 'jetpack-7.5', 'Automattic\Jetpack\Sync\Actions' ); + + return Actions::add_sender_shutdown(); + } + + /** + * Returns false or true based on whether this class should initialize the sender + * in current circumstances. + * + * @deprecated Automattic\Jetpack\Sync\Actions::should_initialize_sender + * + * @return Boolean should the object initialize sender? + */ + public static function should_initialize_sender() { + _deprecated_function( __METHOD__, 'jetpack-7.5', 'Automattic\Jetpack\Sync\Actions' ); + + return Actions::should_initialize_sender(); + } + + /** + * Returns false or true based on whether sync is allowed. + * + * @deprecated Automattic\Jetpack\Sync\Actions::sync_allowed + * + * @return Boolean is sync allowed? + */ + public static function sync_allowed() { + _deprecated_function( __METHOD__, 'jetpack-7.5', 'Automattic\Jetpack\Sync\Actions' ); + + return Actions::sync_allowed(); + } + + /** + * Returns false or true based on whether sync via cron is allowed. + * + * @deprecated Automattic\Jetpack\Sync\Actions::sync_via_cron_allowed + * + * @return Boolean is sync via cron allowed? + */ + public static function sync_via_cron_allowed() { + _deprecated_function( __METHOD__, 'jetpack-7.5', 'Automattic\Jetpack\Sync\Actions' ); + + return Actions::sync_via_cron_allowed(); + } + + /** + * Filters a boolean value that determines whether blacklisted posts should be prevented + * from being publicized. + * + * @deprecated Automattic\Jetpack\Sync\Actions::prevent_publicize_blacklisted_posts + * + * @param Boolean $should_publicize initial setting value. + * @param WP_Post $post the post object. + * @return Boolean whether to prevent publicizing. + */ + public static function prevent_publicize_blacklisted_posts( $should_publicize, $post ) { + _deprecated_function( __METHOD__, 'jetpack-7.5', 'Automattic\Jetpack\Sync\Actions' ); + + return Actions::prevent_publicize_blacklisted_posts( $should_publicize, $post ); + } + + /** + * Set the importing flag to true. + * + * @deprecated Automattic\Jetpack\Sync\Actions::set_is_importing_true + */ + public static function set_is_importing_true() { + _deprecated_function( __METHOD__, 'jetpack-7.5', 'Automattic\Jetpack\Sync\Actions' ); + + return Actions::set_is_importing_true(); + } + + /** + * Send the sync data. + * + * @deprecated Automattic\Jetpack\Sync\Actions::send_data + * + * @param Mixed $data the sync data. + * @param String $codec_name the codec slug. + * @param Integer $sent_timestamp the current server timestamp. + * @param Integer $queue_id the queue identifier. + * @param Integer $checkout_duration time spent retrieving items. + * @param Integer $preprocess_duration Time spent converting items into data. + * @return WP_Response the response object. + */ + public static function send_data( $data, $codec_name, $sent_timestamp, $queue_id, $checkout_duration, $preprocess_duration ) { + _deprecated_function( __METHOD__, 'jetpack-7.5', 'Automattic\Jetpack\Sync\Actions' ); + + return Actions::send_data( $data, $codec_name, $sent_timestamp, $queue_id, $checkout_duration, $preprocess_duration ); + } + + /** + * Commence initial sync. + * + * @deprecated Automattic\Jetpack\Sync\Actions::do_initial_sync + */ + public static function do_initial_sync() { + _deprecated_function( __METHOD__, 'jetpack-7.5', 'Automattic\Jetpack\Sync\Actions' ); + + return Actions::do_initial_sync(); + } + + /** + * Commence full sync. + * + * @deprecated Automattic\Jetpack\Sync\Actions::do_full_sync + * + * @param Array $modules the modules list. + * @return Boolean whether the sync was initialized. + */ + public static function do_full_sync( $modules = null ) { + _deprecated_function( __METHOD__, 'jetpack-7.5', 'Automattic\Jetpack\Sync\Actions' ); + + return Actions::do_full_sync( $modules ); + } + + /** + * Schedule cron sessions. + * + * @deprecated Automattic\Jetpack\Sync\Actions::jetpack_cron_schedule + * + * @param Array $schedules the schedules to add. + */ + public static function jetpack_cron_schedule( $schedules ) { + _deprecated_function( __METHOD__, 'jetpack-7.5', 'Automattic\Jetpack\Sync\Actions' ); + + return Actions::jetpack_cron_schedule( $schedules ); + } + + /** + * Commence cron sync. + * + * @deprecated Automattic\Jetpack\Sync\Actions::do_cron_sync + */ + public static function do_cron_sync() { + _deprecated_function( __METHOD__, 'jetpack-7.5', 'Automattic\Jetpack\Sync\Actions' ); + + return Actions::do_cron_sync(); + } + + /** + * Commence cron full sync. + * + * @deprecated Automattic\Jetpack\Sync\Actions::do_cron_full_sync + */ + public static function do_cron_full_sync() { + _deprecated_function( __METHOD__, 'jetpack-7.5', 'Automattic\Jetpack\Sync\Actions' ); + + return Actions::do_cron_full_sync(); + } + + /** + * Commence cron sync of a specific type of object. + * + * @deprecated Automattic\Jetpack\Sync\Actions::do_cron_sync_by_type + * + * @param Array $type the type of object to sync. + */ + public static function do_cron_sync_by_type( $type ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable + _deprecated_function( __METHOD__, 'jetpack-7.5', 'Automattic\Jetpack\Sync\Actions' ); + + return Actions::do_cron_sync_by_type(); + } + + /** + * Initalize the listener of the object. + * + * @deprecated Automattic\Jetpack\Sync\Actions::initialize_listener + */ + public static function initialize_listener() { + _deprecated_function( __METHOD__, 'jetpack-7.5', 'Automattic\Jetpack\Sync\Actions' ); + + return Actions::initialize_listener(); + } + + /** + * Initalize the sender of the object. + * + * @deprecated Automattic\Jetpack\Sync\Actions::initialize_sender + */ + public static function initialize_sender() { + _deprecated_function( __METHOD__, 'jetpack-7.5', 'Automattic\Jetpack\Sync\Actions' ); + + return Actions::initialize_sender(); + } + + /** + * Initalize the woocommerce listeners. + * + * @deprecated Automattic\Jetpack\Sync\Actions::initialize_woocommerce + */ + public static function initialize_woocommerce() { + _deprecated_function( __METHOD__, 'jetpack-7.5', 'Automattic\Jetpack\Sync\Actions' ); + + return Actions::initialize_woocommerce(); + } + + /** + * Add the woocommerce sync module. + * + * @deprecated Automattic\Jetpack\Sync\Actions::add_woocommerce_sync_module + * + * @param Array $sync_modules an array of modules. + */ + public static function add_woocommerce_sync_module( $sync_modules ) { + _deprecated_function( __METHOD__, 'jetpack-7.5', 'Automattic\Jetpack\Sync\Actions' ); + + return Actions::add_woocommerce_sync_module( $sync_modules ); + } + + /** + * Initalize the WP Super Cache listener. + * + * @deprecated Automattic\Jetpack\Sync\Actions::initialize_wp_super_cache + */ + public static function initialize_wp_super_cache() { + _deprecated_function( __METHOD__, 'jetpack-7.5', 'Automattic\Jetpack\Sync\Actions' ); + + return Actions::initialize_wp_super_cache(); + } + + /** + * Add the WP Super Cache sync module. + * + * @deprecated Automattic\Jetpack\Sync\Actions::add_wp_super_cache_sync_module + * + * @param Array $sync_modules the list to be amended. + */ + public static function add_wp_super_cache_sync_module( $sync_modules ) { + _deprecated_function( __METHOD__, 'jetpack-7.5', 'Automattic\Jetpack\Sync\Actions' ); + + return Actions::add_wp_super_cache_sync_module( $sync_modules ); + } + + /** + * Sanitizes the filtered sync cron schedule. + * + * @deprecated Automattic\Jetpack\Sync\Actions::sanitize_filtered_sync_cron_schedule + * + * @param String $schedule the cron schedule to sanitize. + * @return String sanitized cron schedule. + */ + public static function sanitize_filtered_sync_cron_schedule( $schedule ) { + _deprecated_function( __METHOD__, 'jetpack-7.5', 'Automattic\Jetpack\Sync\Actions' ); + + return Actions::sanitize_filtered_sync_cron_schedule( $schedule ); + } + + /** + * Returns the time offset for a the start schedule. + * + * @deprecated Automattic\Jetpack\Sync\Actions::get_start_time_offset + * + * @param String $schedule the schedule string. + * @param String $hook hook slug. + * @return Integer start time offset. + */ + public static function get_start_time_offset( $schedule = '', $hook = '' ) { + _deprecated_function( __METHOD__, 'jetpack-7.5', 'Automattic\Jetpack\Sync\Actions' ); + + return Actions::get_start_time_offset( $schedule, $hook ); + } + + /** + * If needed, schedule a cron sync. + * + * @deprecated Automattic\Jetpack\Sync\Actions::maybe_schedule_sync_cron + * + * @param String $schedule the schedule string. + * @param String $hook hook slug. + */ + public static function maybe_schedule_sync_cron( $schedule, $hook ) { + _deprecated_function( __METHOD__, 'jetpack-7.5', 'Automattic\Jetpack\Sync\Actions' ); + + return Actions::maybe_schedule_sync_cron( $schedule, $hook ); + } + + /** + * Clears cron jobs scheduled for sync. + * + * @deprecated Automattic\Jetpack\Sync\Actions::clear_sync_cron_jobs + */ + public static function clear_sync_cron_jobs() { + _deprecated_function( __METHOD__, 'jetpack-7.5', 'Automattic\Jetpack\Sync\Actions' ); + + return Actions::clear_sync_cron_jobs(); + } + + /** + * Initialize cron jobs for sync. + * + * @deprecated Automattic\Jetpack\Sync\Actions::init_sync_cron_jobs + */ + public static function init_sync_cron_jobs() { + _deprecated_function( __METHOD__, 'jetpack-7.5', 'Automattic\Jetpack\Sync\Actions' ); + + return Actions::init_sync_cron_jobs(); + } + + /** + * Cleans up schedules on plugin upgrade. + * + * @deprecated Automattic\Jetpack\Sync\Actions::cleanup_on_upgrade + * + * @param String $new_version the new version. + * @param String $old_version the old version. + */ + public static function cleanup_on_upgrade( $new_version = null, $old_version = null ) { + _deprecated_function( __METHOD__, 'jetpack-7.5', 'Automattic\Jetpack\Sync\Actions' ); + + return Actions::cleanup_on_upgrade( $new_version, $old_version ); + } + + /** + * Clears cron jobs scheduled for sync. + * + * @deprecated Automattic\Jetpack\Sync\Actions::get_sync_status + * + * @param Array $fields sync fields to get status of. + */ + public static function get_sync_status( $fields = null ) { + _deprecated_function( __METHOD__, 'jetpack-7.5', 'Automattic\Jetpack\Sync\Actions' ); + + return Actions::get_sync_status( $fields ); + } +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-compat/legacy/class-jetpack-sync-modules.php b/plugins/jetpack/vendor/automattic/jetpack-compat/legacy/class-jetpack-sync-modules.php new file mode 100644 index 00000000..69bc8af5 --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-compat/legacy/class-jetpack-sync-modules.php @@ -0,0 +1,28 @@ +<?php +/** + * A compatibility shim for the sync modules class. + * + * @package automattic/jetpack-compat + */ + +use Automattic\Jetpack\Sync\Modules; + +/** + * Class Jetpack_Sync_Modules + * + * @deprecated Use Automattic\Jetpack\Sync\Modules + */ +class Jetpack_Sync_Modules { + + /** + * Returns the sync module object. + * + * @param String $module_name the module name. + * @return Automattic\Jetpack\Sync\Modules\Module the module object. + */ + public static function get_module( $module_name ) { + _deprecated_function( __METHOD__, 'jetpack-7.5', 'Automattic\Jetpack\Sync\Modules' ); + + return Modules::get_module( $module_name ); + } +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-compat/legacy/class-jetpack-sync-settings.php b/plugins/jetpack/vendor/automattic/jetpack-compat/legacy/class-jetpack-sync-settings.php new file mode 100644 index 00000000..f365257d --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-compat/legacy/class-jetpack-sync-settings.php @@ -0,0 +1,230 @@ +<?php +/** + * Legacy/deprecated Sync Setting getter and setter. + * + * @package automattic/jetpack-sync + */ + +use Automattic\Jetpack\Sync\Settings; + +/** + * Class Jetpack_Sync_Settings + * + * @deprecated Use Automattic\Jetpack\Sync\Settings + */ +class Jetpack_Sync_Settings { + + /** + * Return all settings + * + * @deprecated See Automattic/Jetpack/Sync/Settings + * + * @return array All Sync Settings. + */ + public static function get_settings() { + _deprecated_function( __METHOD__, 'jetpack-7.5', 'Automattic\Jetpack\Sync\Settings' ); + return Settings::get_settings(); + } + + /** + * Return a single setting. + * + * @deprecated See Automattic\Jetpack\Sync\Settings + * + * @param string $setting Setting to return. + * + * @return mixed Value of setting. + */ + public static function get_setting( $setting ) { + _deprecated_function( __METHOD__, 'jetpack-7.5', 'Automattic\Jetpack\Sync\Settings' ); + return Settings::get_setting( $setting ); + } + + /** + * Update a sync setting + * + * @deprecated See Automattic\Jetpack\Sync\Settings + * + * @param mixed $new_settings New setting to set. + */ + public static function update_settings( $new_settings ) { + _deprecated_function( __METHOD__, 'jetpack-7.5', 'Automattic\Jetpack\Sync\Settings' ); + Settings::update_settings( $new_settings ); + } + + /** + * Return is_network_setting result. + * + * @deprecated See Automattic\Jetpack\Sync\Settings + * + * @param string $setting Setting to check. + * + * @return bool + */ + public static function is_network_setting( $setting ) { + _deprecated_function( __METHOD__, 'jetpack-7.5', 'Automattic\Jetpack\Sync\Settings' ); + return Settings::is_network_setting( $setting ); + } + + + /** + * Return blacklisted post types SQL. + * + * @deprecated See Automattic\Jetpack\Sync\Settings + */ + public static function get_blacklisted_post_types_sql() { + _deprecated_function( __METHOD__, 'jetpack-7.5', 'Automattic\Jetpack\Sync\Settings' ); + return Settings::get_blacklisted_post_types_sql(); + } + + /** + * Return whitelisted post meta SQL. + * + * @deprecated See Automattic\Jetpack\Sync\Settings + * + * @return string + */ + public static function get_whitelisted_post_meta_sql() { + _deprecated_function( __METHOD__, 'jetpack-7.5', 'Automattic\Jetpack\Sync\Settings' ); + return Settings::get_whitelisted_post_meta_sql(); + } + + /** + * Return whitelsited comment meta SQL + * + * @deprecated See Automattic\Jetpack\Sync\Settings + */ + public static function get_whitelisted_comment_meta_sql() { + _deprecated_function( __METHOD__, 'jetpack-7.5', 'Automattic\Jetpack\Sync\Settings' ); + return Settings::get_whitelisted_comment_meta_sql(); + } + + /** + * Return get_comments_filter_sql + * + * @deprecated See Automattic\Jetpack\Sync\Settings + */ + public static function get_comments_filter_sql() { + _deprecated_function( __METHOD__, 'jetpack-7.5', 'Automattic\Jetpack\Sync\Settings' ); + return Settings::get_comments_filter_sql(); + } + + /** + * Result data. + * + * @deprecated See Automattic\Jetpack\Sync\Settings + */ + public static function reset_data() { + _deprecated_function( __METHOD__, 'jetpack-7.5', 'Automattic\Jetpack\Sync\Settings' ); + Settings::reset_data(); + } + + /** + * Set importing status. + * + * @deprecated See Automattic\Jetpack\Sync\Settings + * + * @param mixed $is_importing Value to set. + */ + public static function set_importing( $is_importing ) { + _deprecated_function( __METHOD__, 'jetpack-7.5', 'Automattic\Jetpack\Sync\Settings' ); + Settings::set_importing( $is_importing ); + } + + /** + * Return is_importing status. + * + * @deprecated See Automattic\Jetpack\Sync\Settings + * + * @return bool + */ + public static function is_importing() { + _deprecated_function( __METHOD__, 'jetpack-7.5', 'Automattic\Jetpack\Sync\Settings' ); + return Settings::is_importing(); + } + + /** + * Return is_sync_enabled status. + * + * @deprecated See Automattic\Jetpack\Sync\Settings + * + * @return bool + */ + public static function is_sync_enabled() { + _deprecated_function( __METHOD__, 'jetpack-7.5', 'Automattic\Jetpack\Sync\Settings' ); + return Settings::is_sync_enabled(); + } + + /** + * Set cron status. + * + * @deprecated See Automattic\Jetpack\Sync\Settings + * + * @param mixed $is_doing_cron Value to set. + */ + public static function set_doing_cron( $is_doing_cron ) { + _deprecated_function( __METHOD__, 'jetpack-7.5', 'Automattic\Jetpack\Sync\Settings' ); + Settings::set_doing_cron( $is_doing_cron ); + } + + /** + * Return is_doing_cron status. + * + * @deprecated See Automattic\Jetpack\Sync\Settings + * + * @return bool + */ + public static function is_doing_cron() { + _deprecated_function( __METHOD__, 'jetpack-7.5', 'Automattic\Jetpack\Sync\Settings' ); + return Settings::is_doing_cron(); + } + + /** + * Return is_syncing status. + * + * @deprecated See Automattic\Jetpack\Sync\Settings + * + * @return bool + */ + public static function is_syncing() { + _deprecated_function( __METHOD__, 'jetpack-7.5', 'Automattic\Jetpack\Sync\Settings' ); + return Settings::is_syncing(); + } + + /** + * Set "is syncing" status. + * + * @deprecated See Automattic\Jetpack\Sync\Settings + * + * @param mixed $is_syncing Is syncing value. + */ + public static function set_is_syncing( $is_syncing ) { + _deprecated_function( __METHOD__, 'jetpack-7.5', 'Automattic\Jetpack\Sync\Settings' ); + Settings::set_is_syncing( $is_syncing ); + } + + /** + * Return is_sending status. + * + * @deprecated See Automattic\Jetpack\Sync\Settings + * + * @return bool + */ + public static function is_sending() { + _deprecated_function( __METHOD__, 'jetpack-7.5', 'Automattic\Jetpack\Sync\Settings' ); + return Settings::is_sending(); + } + + /** + * Set "is sending" status. + * + * @deprecated See Automattic\Jetpack\Sync\Settings + * + * @param mixed $is_sending Is sending value. + */ + public static function set_is_sending( $is_sending ) { + _deprecated_function( __METHOD__, 'jetpack-7.5', 'Automattic\Jetpack\Sync\Settings' ); + Settings::set_is_sending( $is_sending ); + } + +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-compat/legacy/class-jetpacktracking.php b/plugins/jetpack/vendor/automattic/jetpack-compat/legacy/class-jetpacktracking.php new file mode 100644 index 00000000..abfe4f62 --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-compat/legacy/class-jetpacktracking.php @@ -0,0 +1,47 @@ +<?php +/** + * Legacy and deprecated Jetpack Tracking class. + * + * @package automattic/jetpack-compat + */ + +use Automattic\Jetpack\Tracking; + +/** + * Legacy class JetpackTracking + * + * @deprecated See Automattic\Jetpack\Tracking + */ +class JetpackTracking { + + /** + * Enqueue tracks scripts. + * + * @deprecated See Automattic\Jetpack\Tracking + */ + public static function enqueue_tracks_scripts() { + _deprecated_function( __METHOD__, 'jetpack-7.5', 'Automattic\Jetpack\Tracking' ); + + $tracking = new Tracking(); + return $tracking->enqueue_tracks_scripts(); + } + + /** + * Record user event. + * + * @deprecated See Automattic\Jetpack\Tracking + * + * @param mixed $event_type Event type. + * @param array $data Event data. + * @param mixed $user User who did the event. + * + * @return bool + */ + public static function record_user_event( $event_type, $data = array(), $user = null ) { + _deprecated_function( __METHOD__, 'jetpack-7.5', 'Automattic\Jetpack\Tracking' ); + + $tracking = new Tracking(); + return $tracking->record_user_event( $event_type, $data, $user ); + } + +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-compat/lib/tracks/client.php b/plugins/jetpack/vendor/automattic/jetpack-compat/lib/tracks/client.php new file mode 100644 index 00000000..41056897 --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-compat/lib/tracks/client.php @@ -0,0 +1,41 @@ +<?php +/** + * Deprecated Tracks client. + * + * @package automattic/jetpack-compat + */ + +/** + * Get tracks identity for an user. + * + * @deprecated 7.5.0 use Automattic\Jetpack\Tracking->tracks_get_identity instead + * + * @param int $user_id User id. + * + * @return mixed tracks identity. + */ +function jetpack_tracks_get_identity( $user_id ) { + _deprecated_function( __METHOD__, 'jetpack-7.5', 'Automattic\Jetpack\Tracking->tracks_get_identity' ); + + $tracking = new Automattic\Jetpack\Tracking( 'jetpack', Jetpack::connection() ); + return $tracking->tracks_get_identity( $user_id ); +} + +/** + * Record Jetpack Tracks Event + * + * @deprecated 7.5.0 use Automattic\Jetpack\Tracking->tracks_record_event instead + * + * @param object $user User acting. + * @param string $event_name Event name. + * @param array $properties Properties. + * @param string|bool $event_timestamp_millis Timestamp. + * + * @return bool + */ +function jetpack_tracks_record_event( $user, $event_name, $properties = array(), $event_timestamp_millis = false ) { + _deprecated_function( __METHOD__, 'jetpack-7.5', 'Automattic\Jetpack\Tracking->tracks_record_event' ); + + $tracking = new Automattic\Jetpack\Tracking( 'jetpack', Jetpack::connection() ); + return $tracking->tracks_record_event( $user, $event_name, $properties, $event_timestamp_millis ); +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-connection/legacy/class-jetpack-ixr-client.php b/plugins/jetpack/vendor/automattic/jetpack-connection/legacy/class-jetpack-ixr-client.php new file mode 100644 index 00000000..084cc8e6 --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-connection/legacy/class-jetpack-ixr-client.php @@ -0,0 +1,122 @@ +<?php +/** + * IXR_Client + * + * @package automattic/jetpack-connection + * + * @since 1.5 + * @since 7.7 Moved to the jetpack-connection package. + */ + +use Automattic\Jetpack\Connection\Client; +use Automattic\Jetpack\Connection\Manager; + +/** + * 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; + + /** + * 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, + ); + + $args = wp_parse_args( $args, $defaults ); + + $this->jetpack_args = $args; + + $this->IXR_Client( $args['url'], $path, $port, $timeout ); + } + + /** + * Perform the IXR request. + * + * @return bool True if request succeeded, false otherwise. + */ + public function query() { + $args = func_get_args(); + $method = array_shift( $args ); + $request = new IXR_Request( $method, $args ); + $xml = trim( $request->getXml() ); + + $response = Client::remote_request( $this->jetpack_args, $xml ); + + 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 ( is_null( $fault_code ) ) { + $fault_code = $this->error->code; + } + + if ( is_null( $fault_string ) ) { + $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 ); + } +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-connection/legacy/class-jetpack-ixr-clientmulticall.php b/plugins/jetpack/vendor/automattic/jetpack-connection/legacy/class-jetpack-ixr-clientmulticall.php new file mode 100644 index 00000000..da71873f --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-connection/legacy/class-jetpack-ixr-clientmulticall.php @@ -0,0 +1,68 @@ +<?php +/** + * IXR_ClientMulticall + * + * @package automattic/jetpack-connection + * + * @since 1.5 + * @since 7.7 Moved to the jetpack-connection package. + */ + +/** + * 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. + */ + public function addCall() { + $args = func_get_args(); + $method_name = array_shift( $args ); + $struct = array( + 'methodName' => $method_name, + 'params' => $args, + ); + $this->calls[] = $struct; + } + + /** + * Perform the IXR multicall request. + * + * @return bool True if request succeeded, false otherwise. + */ + public function query() { + usort( $this->calls, array( $this, 'sort_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. + * + * @param array $a First call in the sorting iteration. + * @param array $b Second call in the sorting iteration. + * @return int Result of the sorting iteration. + */ + public function sort_calls( $a, $b ) { + if ( 'jetpack.syncContent' === $a['methodName'] ) { + return -1; + } + + if ( 'jetpack.syncContent' === $b['methodName'] ) { + return 1; + } + + return 0; + } +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-connection/legacy/class-jetpack-signature.php b/plugins/jetpack/vendor/automattic/jetpack-connection/legacy/class-jetpack-signature.php new file mode 100644 index 00000000..2d6b7529 --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-connection/legacy/class-jetpack-signature.php @@ -0,0 +1,344 @@ +<?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; + + /** + * The current request URL. + * + * @access public + * @var string + */ + public $current_request_url; + + /** + * Constructor. + * + * @param array $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' ); + } + } else { + if ( is_ssl() ) { + $scheme = 'https'; + } else { + $scheme = 'http'; + } + } + + $host_port = isset( $_SERVER['HTTP_X_FORWARDED_PORT'] ) ? $_SERVER['HTTP_X_FORWARDED_PORT'] : $_SERVER['SERVER_PORT']; + $host_port = intval( $host_port ); + + /** + * 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' ) ? JETPACK_SIGNATURE__HTTPS_PORT : 443; + $port = in_array( $host_port, array( 443, 80, $https_port ), false ) ? '' : $host_port; // phpcs:ignore WordPress.PHP.StrictInArray.FoundNonStrictFalse + } 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' ) ? JETPACK_SIGNATURE__HTTP_PORT : 80; + $port = in_array( $host_port, array( 80, $http_port ), false ) ? '' : $host_port; // phpcs:ignore WordPress.PHP.StrictInArray.FoundNonStrictFalse + } + + $this->current_request_url = "{$scheme}://{$_SERVER['HTTP_HOST']}:{$port}" . stripslashes( $_SERVER['REQUEST_URI'] ); + + if ( array_key_exists( 'body', $override ) && ! empty( $override['body'] ) ) { + $body = $override['body']; + } elseif ( 'POST' === strtoupper( $_SERVER['REQUEST_METHOD'] ) ) { + $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 + if ( empty( $body ) && is_array( $_POST ) && count( $_POST ) > 0 ) { + $body = $_POST; // phpcs:ignore WordPress.Security.NonceVerification.Missing + } + } + } elseif ( 'PUT' === strtoupper( $_SERVER['REQUEST_METHOD'] ) ) { + // 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 ) && count( $put_data ) > 0 ) { + $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 ] ) ? stripslashes( $_GET[ $parameter ] ) : ''; + } + } + + $method = isset( $override['method'] ) ? $override['method'] : $_SERVER['REQUEST_METHOD']; + 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 ( 0 !== strpos( $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 ( count( $body ) > 0 ) { + // phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode + $body = json_encode( $body ); + + } else { + $body = ''; + } + } + + $required_parameters = array( 'token', 'timestamp', 'nonce', 'method', 'url' ); + if ( ! is_null( $body ) ) { + $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']; + } else { + if ( '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( $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, + $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 = join( "\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. + * Supports one-dimensional arrays as `$value`. + * + * @param string $name Parameter name. + * @param mixed $value Parameter value. + * @return string A pair with parameter name and value (e.g. `name=value`). + */ + public function join_with_equal_sign( $name, $value ) { + if ( is_array( $value ) ) { + $result = array(); + foreach ( $value as $array_key => $array_value ) { + $result[] = $name . '[' . $array_key . ']=' . $array_value; + } + return $result; + } + return "{$name}={$value}"; + } +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-connection/legacy/class-jetpack-xmlrpc-server.php b/plugins/jetpack/vendor/automattic/jetpack-connection/legacy/class-jetpack-xmlrpc-server.php new file mode 100644 index 00000000..db897a0d --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-connection/legacy/class-jetpack-xmlrpc-server.php @@ -0,0 +1,943 @@ +<?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\Utils as Connection_Utils; +use Automattic\Jetpack\Roles; +use Automattic\Jetpack\Sync\Modules; +use Automattic\Jetpack\Sync\Functions; +use Automattic\Jetpack\Sync\Sender; + +/** + * 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. + * + * @param Automattic\Jetpack\Connection\Manager $manager the connection manager object. + */ + public function __construct( $manager = null ) { + $this->connection = is_null( $manager ) ? new Connection_Manager() : $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.jsonAPI' => array( $this, 'json_api' ), + 'jetpack.verifyAction' => array( $this, 'verify_action' ), + 'jetpack.getUser' => array( $this, 'get_user' ), + 'jetpack.remoteRegister' => array( $this, 'remote_register' ), + 'jetpack.remoteProvision' => array( $this, 'remote_provision' ), + ); + + $this->user = $this->login(); + + if ( $this->user ) { + $jetpack_methods = array_merge( + $jetpack_methods, + array( + 'jetpack.testConnection' => array( $this, 'test_connection' ), + 'jetpack.testAPIUserCode' => array( $this, 'test_api_user_code' ), + 'jetpack.featuresAvailable' => array( $this, 'features_available' ), + 'jetpack.featuresEnabled' => array( $this, 'features_enabled' ), + 'jetpack.disconnectBlog' => array( $this, 'disconnect_blog' ), + 'jetpack.unlinkUser' => array( $this, 'unlink_user' ), + 'jetpack.idcUrlValidation' => array( $this, 'validate_urls_for_idc_mitigation' ), + ) + ); + + 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.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 a given WordPress user. + */ + $jetpack_methods = apply_filters( 'jetpack_xmlrpc_methods', $jetpack_methods, $core_methods, $this->user ); + } + + /** + * Filters the XML-RPC methods available to Jetpack for unauthenticated users. + * + * @since 3.0.0 + * + * @param array $jetpack_methods XML-RPC methods available to the Jetpack Server. + * @param array $core_methods Available core XML-RPC methods. + */ + return apply_filters( 'jetpack_xmlrpc_unauthenticated_methods', $jetpack_methods, $core_methods ); + } + + /** + * 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' ), + ); + } + + /** + * 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 Jetpack_Error( + 'invalid_user', + __( 'Invalid user identifier.', 'jetpack' ), + 400 + ), + 'get_user' + ); + } + + $user = $this->get_user_by_anything( $user_id ); + + if ( ! $user ) { + return $this->error( + new Jetpack_Error( + 'user_unknown', + __( 'User not found.', 'jetpack' ), + 404 + ), + 'get_user' + ); + } + + $user_token = $this->connection->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 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 Jetpack_Error( 'missing_parameter', 'One or more parameters is missing from the request.', 400 ), + 'remote_authorize' + ); + } + } + + if ( ! $user ) { + return $this->error( new Jetpack_Error( 'user_unknown', 'User not found.', 404 ), 'remote_authorize' ); + } + + if ( $this->connection->is_active() && $this->connection->is_user_connected( $request['state'] ) ) { + return $this->error( new Jetpack_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 $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' ), 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 Jetpack_Error( + 'nonce_missing', + __( 'The required "nonce" parameter is missing.', 'jetpack' ), + 400 + ), + 'remote_register' + ); + } + + $nonce = sanitize_text_field( $request['nonce'] ); + unset( $request['nonce'] ); + + $api_url = Connection_Utils::fix_url_for_bad_hosts( + $this->connection->api_url( 'partner_provision_nonce_check' ) + ); + $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 Jetpack_Error( + 'invalid_nonce', + __( 'There was an issue validating this request.', 'jetpack' ), + 400 + ), + 'remote_register' + ); + } + + if ( ! Jetpack_Options::get_option( 'id' ) || ! $this->connection->get_access_token() || ! empty( $request['force'] ) ) { + wp_set_current_user( $user->ID ); + + // This code mostly copied from Jetpack::admin_page_load. + Jetpack::maybe_set_version_option(); + $registered = Jetpack::try_registration(); + if ( is_wp_error( $registered ) ) { + return $this->error( $registered, 'remote_register' ); + } elseif ( ! $registered ) { + return $this->error( + new Jetpack_Error( + 'registration_error', + __( 'There was an unspecified error registering the site', 'jetpack' ), + 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 XML-RPC method is called from the /jpphp/provision endpoint on WPCOM in order to + * register this site so that a plan can be provisioned. + * + * @param array $request An array containing at minimum a nonce key and a local_username key. + * + * @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' ), 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(); + + $auto_enable_sso = ( ! $this->connection->is_active() || Jetpack::is_module_active( 'sso' ) ); + + /** This filter is documented in class.jetpack-cli.php */ + if ( apply_filters( 'jetpack_start_enable_sso', $auto_enable_sso ) ) { + $redirect_uri = add_query_arg( + array( + 'action' => 'jetpack-sso', + 'redirect_to' => rawurlencode( admin_url() ), + ), + wp_login_url() // TODO: come back to Jetpack dashboard? + ); + } else { + $redirect_uri = admin_url(); + } + + // Generate secrets. + $roles = new Roles(); + $role = $roles->translate_user_to_role( $user ); + $secrets = $this->connection->generate_secrets( 'authorize', $user->ID ); + + $response = array( + 'jp_version' => 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->is_active(), + ); + + if ( $site_icon ) { + $response['site_icon'] = $site_icon; + } + + if ( ! empty( $request['onboarding'] ) ) { + Jetpack::create_onboarding_token(); + $response['onboarding_token'] = Jetpack_Options::get_option( 'onboarding' ); + } + + 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 $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->is_active() ) { + return $this->error( + new WP_Error( + 'already_connected', + __( 'Jetpack is already connected.', 'jetpack' ), + 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' ), + 400 + ), + 'remote_connect' + ); + } + + if ( empty( $request['nonce'] ) ) { + return $this->error( + new WP_Error( + 'input_error', + __( 'A non-empty nonce must be supplied.', 'jetpack' ), + 400 + ), + 'remote_connect' + ); + } + + if ( ! $ixr_client ) { + $ixr_client = new Jetpack_IXR_Client(); + } + $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' ), + 400 + ), + 'remote_connect' + ); + } + $token = sanitize_text_field( $token ); + + Connection_Utils::update_user_token( $user->ID, sprintf( '%s.%d', $token, $user->ID ), true ); + + $this->do_post_authorization(); + + return $this->connection->is_active(); + } + + /** + * Getter for the local user to act as. + * + * @param array $request the current request data. + */ + private function fetch_and_verify_local_user( $request ) { + if ( empty( $request['local_user'] ) ) { + return $this->error( + new Jetpack_Error( + 'local_user_missing', + __( 'The required "local_user" parameter is missing.', 'jetpack' ), + 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. + */ + 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 = $this->connection->verify_secrets( $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 Jetpack_Error( 'invalid_request', 'Invalid Request', 403 ); + } else { + $this->error = $user; + } + return false; + } elseif ( ! $user ) { // Shouldn't happen. + $this->error = new Jetpack_Error( 'invalid_request', 'Invalid Request', 403 ); + return false; + } + + 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 ( ! is_null( $error ) ) { + $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() ); + 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 The current Jetpack version number + */ + public function test_connection() { + return JETPACK__VERSION; + } + + /** + * 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 = $this->connection->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' => (int) $client_id, + 'user_id' => (int) $user_id, + 'nonce' => (string) $nonce, + 'code' => (string) $api_user_code, + ) + ), + $jetpack_token->secret + ); + + if ( ! hash_equals( $hmac, $verify ) ) { + return false; + } + + return $user_id; + } + + /** + * Disconnect this blog from the connected wordpress.com account + * + * @return boolean + */ + public function disconnect_blog() { + + // For tracking. + if ( ! empty( $this->user->ID ) ) { + wp_set_current_user( $this->user->ID ); + } + + /** + * Fired when we want to log an event to the Jetpack event log. + * + * @since 7.7.0 + * + * @param string $code Unique name for the event. + * @param string $data Optional data about the event. + */ + do_action( 'jetpack_event_log', 'disconnect' ); + Jetpack::disconnect(); + + return true; + } + + /** + * Unlink a user from WordPress.com + * + * This will fail if called by the Master User. + */ + public function unlink_user() { + /** + * Fired when we want to log an event to the Jetpack event log. + * + * @since 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 Connection_Manager::disconnect_user(); + } + + /** + * Returns any object that is able to be synced. + * + * @deprecated since 7.8.0 + * @see Automattic\Jetpack\Sync\Sender::sync_object() + * + * @param array $args the synchronized object parameters. + * @return string Encoded sync object. + */ + public function sync_object( $args ) { + _deprecated_function( __METHOD__, 'jetpack-7.8', 'Automattic\\Jetpack\\Sync\\Sender::sync_object' ); + return Sender::get_instance()->sync_object( $args ); + } + + /** + * Returns the home URL and site URL 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. + * + * @return array + */ + public function validate_urls_for_idc_mitigation() { + return array( + 'home' => Functions::home_url(), + 'siteurl' => Functions::site_url(), + ); + } + + /** + * Returns what features are available. Uses the slug of the module files. + * + * @return array + */ + public function features_available() { + $raw_modules = Jetpack::get_available_modules(); + $modules = array(); + foreach ( $raw_modules as $module ) { + $modules[] = Jetpack::get_module_slug( $module ); + } + + return $modules; + } + + /** + * Returns what features are enabled. Uses the slug of the modules files. + * + * @return array + */ + public function features_enabled() { + $raw_modules = Jetpack::get_active_modules(); + $modules = array(); + foreach ( $raw_modules as $module ) { + $modules[] = Jetpack::get_module_slug( $module ); + } + + return $modules; + } + + /** + * 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, + ) + ); + } + + /** + * Serve a JSON API request. + * + * @param array $args request arguments. + */ + public function json_api( $args = array() ) { + $json_api_args = $args[0]; + $verify_api_user_args = $args[1]; + + $method = (string) $json_api_args[0]; + $url = (string) $json_api_args[1]; + $post_body = is_null( $json_api_args[2] ) ? null : (string) $json_api_args[2]; + $user_details = (array) $json_api_args[4]; + $locale = (string) $json_api_args[5]; + + if ( ! $verify_api_user_args ) { + $user_id = 0; + } elseif ( 'internal' === $verify_api_user_args[0] ) { + $user_id = (int) $verify_api_user_args[1]; + if ( $user_id ) { + $user = get_user_by( 'id', $user_id ); + if ( ! $user || is_wp_error( $user ) ) { + return false; + } + } + } else { + $user_id = call_user_func( array( $this, 'test_api_user_code' ), $verify_api_user_args ); + if ( ! $user_id ) { + return false; + } + } + + /* phpcs:ignore + debugging + error_log( "-- begin json api via jetpack debugging -- " ); + error_log( "METHOD: $method" ); + error_log( "URL: $url" ); + error_log( "POST BODY: $post_body" ); + error_log( "VERIFY_ARGS: " . print_r( $verify_api_user_args, 1 ) ); + error_log( "VERIFIED USER_ID: " . (int) $user_id ); + error_log( "-- end json api via jetpack debugging -- " ); + */ + + if ( 'en' !== $locale ) { + // .org mo files are named slightly different from .com, and all we have is this the locale -- try to guess them. + $new_locale = $locale; + if ( strpos( $locale, '-' ) !== false ) { + $locale_pieces = explode( '-', $locale ); + $new_locale = $locale_pieces[0]; + $new_locale .= ( ! empty( $locale_pieces[1] ) ) ? '_' . strtoupper( $locale_pieces[1] ) : ''; + } else { + // .com might pass 'fr' because thats what our language files are named as, where core seems + // to do fr_FR - so try that if we don't think we can load the file. + if ( ! file_exists( WP_LANG_DIR . '/' . $locale . '.mo' ) ) { + $new_locale = $locale . '_' . strtoupper( $locale ); + } + } + + if ( file_exists( WP_LANG_DIR . '/' . $new_locale . '.mo' ) ) { + unload_textdomain( 'default' ); + load_textdomain( 'default', WP_LANG_DIR . '/' . $new_locale . '.mo' ); + } + } + + $old_user = wp_get_current_user(); + wp_set_current_user( $user_id ); + + if ( $user_id ) { + $token_key = false; + } else { + $verified = $this->connection->verify_xml_rpc_signature(); + $token_key = $verified['token_key']; + } + + $token = $this->connection->get_access_token( $user_id, $token_key ); + if ( ! $token || is_wp_error( $token ) ) { + return false; + } + + define( 'REST_API_REQUEST', true ); + define( 'WPCOM_JSON_API__BASE', 'public-api.wordpress.com/rest/v1' ); + + // needed? + require_once ABSPATH . 'wp-admin/includes/admin.php'; + + require_once JETPACK__PLUGIN_DIR . 'class.json-api.php'; + $api = WPCOM_JSON_API::init( $method, $url, $post_body ); + $api->token_details['user'] = $user_details; + require_once JETPACK__PLUGIN_DIR . 'class.json-api-endpoints.php'; + + $display_errors = ini_set( 'display_errors', 0 ); // phpcs:ignore WordPress.PHP.IniSet + ob_start(); + $api->serve( false ); + $output = ob_get_clean(); + ini_set( 'display_errors', $display_errors ); // phpcs:ignore WordPress.PHP.IniSet + + $nonce = wp_generate_password( 10, false ); + $hmac = hash_hmac( 'md5', $nonce . $output, $token->secret ); + + wp_set_current_user( isset( $old_user->ID ) ? $old_user->ID : 0 ); + + return array( + (string) $output, + (string) $nonce, + (string) $hmac, + ); + } + + /** + * Handles authorization actions after connecting a site, such as enabling modules. + * + * This do_post_authorization() is used in this class, as opposed to calling + * Jetpack::handle_post_authorization_actions() directly so that we can mock this method as necessary. + * + * @return void + */ + public function do_post_authorization() { + /** This filter is documented in class.jetpack-cli.php */ + $enable_sso = apply_filters( 'jetpack_start_enable_sso', true ); + Jetpack::handle_post_authorization_actions( $enable_sso, false, false ); + } +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-connection/legacy/load-ixr.php b/plugins/jetpack/vendor/automattic/jetpack-connection/legacy/load-ixr.php new file mode 100644 index 00000000..de77dfc4 --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-connection/legacy/load-ixr.php @@ -0,0 +1,13 @@ +<?php +/** + * WordPress IXR classes aren't always loaded by default. + * + * Here we ensure that they are loaded before we declare our implementations. + * + * @package automattic/jetpack-connection + * @since 7.7 + */ + +if ( defined( 'ABSPATH' ) && defined( 'WPINC' ) ) { + require_once ABSPATH . WPINC . '/class-IXR.php'; +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-connection/src/class-client.php b/plugins/jetpack/vendor/automattic/jetpack-connection/src/class-client.php new file mode 100644 index 00000000..0070f294 --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-connection/src/class-client.php @@ -0,0 +1,455 @@ +<?php +/** + * The Connection Client class file. + * + * @package automattic/jetpack-connection + */ + +namespace Automattic\Jetpack\Connection; + +use Automattic\Jetpack\Constants; + +/** + * 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 $body the request body. + * @return array|WP_Error WP HTTP response on success + */ + public static function remote_request( $args, $body = null ) { + $defaults = array( + 'url' => '', + 'user_id' => 0, + 'blog_id' => 0, + 'auth_location' => Constants::get_constant( 'JETPACK_CLIENT__AUTH_LOCATION' ), + 'method' => 'POST', + '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'; + } + + $connection = new Manager(); + $token = $connection->get_access_token( $args['user_id'] ); + if ( ! $token ) { + return new \WP_Error( 'missing_token' ); + } + + $method = strtoupper( $args['method'] ); + + $timeout = intval( $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 ( empty( $token ) || empty( $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( wp_rand( 0, 1000000 ) ), 0, 10 ); + } + + // Kind of annoying. Maybe refactor Jetpack_Signature to handle body-hashing. + if ( is_null( $body ) ) { + $body_hash = ''; + + } else { + // Allow arrays to be used in passing data. + $body_to_hash = $body; + + if ( is_array( $body ) ) { + // 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 ( count( $body ) > 0 ) { + $body_to_hash = wp_json_encode( self::_stringify_data( $body ) ); + } 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'] ); + $url = Utils::fix_url_for_bad_hosts( $url ); + + $signature = $jetpack_signature->sign_request( $token_key, $timestamp, $nonce, $body_hash, $method, $url, $body, false ); + + if ( ! $signature || 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 ' . join( ' ', $header_pieces ), + ) + ); + + if ( 'header' !== $args['auth_location'] ) { + $url = add_query_arg( 'signature', rawurlencode( $signature ), $url ); + } + + return self::_wp_remote_request( $url, $request ); + } + + /** + * 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 + * @see Utils::fix_url_for_bad_hosts() + * + * @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 + */ + public static function _wp_remote_request( $url, $args, $set_fallback = false ) { // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore + /** + * 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 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 ); + } + + $fallback = \Jetpack_Options::get_option( 'fallback_no_verify_ssl_certs' ); + if ( false === $fallback ) { + \Jetpack_Options::update_option( 'fallback_no_verify_ssl_certs', 0 ); + } + + 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 HTTP_Response $response the response object. + * @param Boolean $force_set whether to force setting the time difference. + */ + 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 WordPress.PHP.StrictComparisons.LooseComparison + 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 ); + } + } + } + + /** + * Queries the WordPress.com REST API with a user token. + * + * @param string $path REST API path. + * @param string $version REST API version. Default is `2`. + * @param array $args Arguments to {@see WP_Http}. Default is `array()`. + * @param string $body Body passed to {@see WP_Http}. Default is `null`. + * @param string $base_api_path REST API root. Default is `wpcom`. + * + * @return array|WP_Error $response Response data, else {@see WP_Error} on failure. + */ + public static function wpcom_json_api_request_as_user( + $path, + $version = '2', + $args = array(), + $body = null, + $base_api_path = 'wpcom' + ) { + $base_api_path = trim( $base_api_path, '/' ); + $version = ltrim( $version, 'v' ); + $path = ltrim( $path, '/' ); + + $args = array_intersect_key( + $args, + array( + 'headers' => 'array', + 'method' => 'string', + 'timeout' => 'int', + 'redirection' => 'int', + 'stream' => 'boolean', + 'filename' => 'string', + 'sslverify' => 'boolean', + ) + ); + + $args['user_id'] = get_current_user_id(); + $args['method'] = isset( $args['method'] ) ? strtoupper( $args['method'] ) : 'GET'; + $args['url'] = sprintf( + '%s://%s/%s/v%s/%s', + self::protocol(), + Constants::get_constant( 'JETPACK__WPCOM_JSON_API_HOST' ), + $base_api_path, + $version, + $path + ); + + 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 String $body Request body. + * @param String $base_api_path (optional) the API base path override, defaults to 'rest'. + * @return Array|WP_Error $response Data. + */ + public static function wpcom_json_api_request_as_blog( + $path, + $version = self::WPCOM_JSON_API_VERSION, + $args = array(), + $body = null, + $base_api_path = 'rest' + ) { + $filtered_args = array_intersect_key( + $args, + array( + 'headers' => 'array', + 'method' => 'string', + 'timeout' => 'int', + 'redirection' => 'int', + 'stream' => 'boolean', + 'filename' => 'string', + 'sslverify' => 'boolean', + ) + ); + + // unprecedingslashit. + $_path = preg_replace( '/^\//', '', $path ); + + // Use GET by default whereas `remote_request` uses POST. + $request_method = ( isset( $filtered_args['method'] ) ) ? $filtered_args['method'] : 'GET'; + + $url = sprintf( + '%s://%s/%s/v%s/%s', + self::protocol(), + Constants::get_constant( 'JETPACK__WPCOM_JSON_API_HOST' ), + $base_api_path, + $version, + $_path + ); + + $validated_args = array_merge( + $filtered_args, + array( + 'url' => $url, + 'blog_id' => (int) \Jetpack_Options::get_option( 'id' ), + 'method' => $request_method, + ) + ); + + 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 Array|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 $key => &$value ) { + $value = self::_stringify_data( $value ); + } + + return $data; + } + + /** + * Gets protocol string. + * + * @return string `https` (if possible), else `http`. + */ + public static function protocol() { + /** + * Determines whether Jetpack can send outbound https requests to the WPCOM api. + * + * @since 3.6.0 + * + * @param bool $proto Defaults to true. + */ + $https = apply_filters( 'jetpack_can_make_outbound_https', true ); + + return $https ? 'https' : 'http'; + } +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-connection/src/class-manager.php b/plugins/jetpack/vendor/automattic/jetpack-connection/src/class-manager.php new file mode 100644 index 00000000..f37dbf88 --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-connection/src/class-manager.php @@ -0,0 +1,2169 @@ +<?php +/** + * The Jetpack Connection manager class file. + * + * @package automattic/jetpack-connection + */ + +namespace Automattic\Jetpack\Connection; + +use Automattic\Jetpack\Constants; +use Automattic\Jetpack\Roles; +use Automattic\Jetpack\Tracking; + +/** + * The Jetpack Connection Manager class that is used as a single gateway between WordPress.com + * and Jetpack. + */ +class Manager { + + const SECRETS_MISSING = 'secrets_missing'; + const SECRETS_EXPIRED = 'secrets_expired'; + const SECRETS_OPTION_NAME = 'jetpack_secrets'; + const MAGIC_NORMAL_TOKEN_KEY = ';normal;'; + const JETPACK_MASTER_USER = true; + + /** + * The procedure that should be run to generate secrets. + * + * @var Callable + */ + protected $secret_callable; + + /** + * 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; + + /** + * 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 function init() { + $this->setup_xmlrpc_handlers( + $_GET, // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $this->is_active(), + $this->verify_xml_rpc_signature() + ); + + if ( $this->is_active() ) { + add_filter( 'xmlrpc_methods', array( $this, 'public_xmlrpc_methods' ) ); + } else { + add_action( 'rest_api_init', array( $this, 'initialize_rest_api_registration_connector' ) ); + } + + add_action( 'jetpack_clean_nonces', array( $this, 'clean_nonces' ) ); + if ( ! wp_next_scheduled( 'jetpack_clean_nonces' ) ) { + wp_schedule_event( time(), 'hourly', 'jetpack_clean_nonces' ); + } + } + + /** + * Sets up the XMLRPC request handlers. + * + * @param Array $request_params incoming request parameters. + * @param Boolean $is_active whether the connection is currently active. + * @param Boolean $is_signed whether the signature check has been successful. + * @param \Jetpack_XMLRPC_Server $xmlrpc_server (optional) an instance of the server to use instead of instantiating a new one. + */ + public function setup_xmlrpc_handlers( + $request_params, + $is_active, + $is_signed, + \Jetpack_XMLRPC_Server $xmlrpc_server = null + ) { + add_filter( 'xmlrpc_blog_options', array( $this, 'xmlrpc_options' ), 1000, 2 ); + + if ( + ! isset( $request_params['for'] ) + || 'jetpack' !== $request_params['for'] + ) { + return false; + } + + // Alternate XML-RPC, via ?for=jetpack&jetpack=comms. + if ( + isset( $request_params['jetpack'] ) + && 'comms' === $request_params['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. + @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_active ) { + // Hack to preserve $HTTP_RAW_POST_DATA. + add_filter( 'xmlrpc_methods', array( $this, 'xmlrpc_methods' ) ); + + if ( $is_signed ) { + // The actual API methods. + add_filter( 'xmlrpc_methods', array( $this->xmlrpc_server, 'xmlrpc_methods' ) ); + } else { + // 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. + add_filter( 'xmlrpc_methods', array( $this->xmlrpc_server, 'authorize_xmlrpc_methods' ) ); + } + } else { + // The bootstrap API methods. + add_filter( 'xmlrpc_methods', array( $this->xmlrpc_server, 'bootstrap_xmlrpc_methods' ) ); + + if ( $is_signed ) { + // The jetpack Provision method is available for blog-token-signed requests. + add_filter( 'xmlrpc_methods', array( $this->xmlrpc_server, 'provision_xmlrpc_methods' ) ); + } else { + new XMLRPC_Connector( $this ); + } + } + + // 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. + */ + public function alternate_xmlrpc() { + // phpcs:disable PHPCompatibility.Variables.RemovedPredefinedGlobalVariables.http_raw_post_dataDeprecatedRemoved + // phpcs:disable WordPress.WP.GlobalVariablesOverride.Prohibited + global $HTTP_RAW_POST_DATA; + + // Some browser-embedded clients send cookies. We don't want them. + $_COOKIE = array(); + + // A fix for mozBlog and other cases where '<?xml' isn't on the very first line. + if ( isset( $HTTP_RAW_POST_DATA ) ) { + $HTTP_RAW_POST_DATA = trim( $HTTP_RAW_POST_DATA ); + } + + // phpcs:enable + + 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 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; + } + + /** + * 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 ( 0 === strpos( $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_active() ) { + // 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 ) { + 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 ( is_null( $this->xmlrpc_verification ) ) { + $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. + * + * Error codes: + * - malformed_token + * - malformed_user_id + * - unknown_token + * - could_not_sign + * - invalid_nonce + * - signature_mismatch + * + * @since 7.5.0 + * + * @param WP_Error $signature_verification_error The verification error + */ + do_action( 'jetpack_verify_signature_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 + // 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' => wp_unslash( $_SERVER['REQUEST_METHOD'] ), + 'url' => wp_unslash( $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] ), // Temp - will get real signature URL later. + 'signature' => isset( $_GET['signature'] ) ? wp_unslash( $_GET['signature'] ) : '', + ); + + // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged + @list( $token_key, $version, $user_id ) = explode( ':', wp_unslash( $_GET['token'] ) ); + // phpcs:enable WordPress.Security.NonceVerification.Recommended + + if ( + empty( $token_key ) + || + empty( $version ) || strval( JETPACK__API_VERSION ) !== $version + ) { + return new \WP_Error( 'malformed_token', 'Malformed token in request', compact( 'signature_details' ) ); + } + + 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' ) + ); + } + $user_id = (int) $user_id; + + $user = new \WP_User( $user_id ); + if ( ! $user || ! $user->exists() ) { + return new \WP_Error( + 'unknown_user', + sprintf( 'User %d does not exist', $user_id ), + compact( 'signature_details' ) + ); + } + } + + $token = $this->get_access_token( $user_id, $token_key, false ); + if ( is_wp_error( $token ) ) { + $token->add_data( compact( 'signature_details' ) ); + 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' ) + ); + } + + $jetpack_signature = new \Jetpack_Signature( $token->secret, (int) \Jetpack_Options::get_option( 'time_diff' ) ); + // phpcs:disable WordPress.Security.NonceVerification.Missing + if ( isset( $_POST['_jetpack_is_multipart'] ) ) { + $post_data = $_POST; + $file_hashes = array(); + foreach ( $post_data as $post_data_key => $post_data_value ) { + if ( 0 !== strpos( $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 ( is_null( $this->raw_post_data ) ) { + $body = file_get_contents( 'php://input' ); + } else { + $body = null; + } + // phpcs:enable + + $signature = $jetpack_signature->sign_current_request( + array( 'body' => is_null( $body ) ? $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' ) + ); + } elseif ( is_wp_error( $signature ) ) { + return $signature; + } + + // phpcs:disable WordPress.Security.NonceVerification.Recommended + $timestamp = (int) $_GET['timestamp']; + $nonce = stripslashes( (string) $_GET['nonce'] ); + // phpcs:enable WordPress.Security.NonceVerification.Recommended + + // Use up the nonce regardless of whether the signature matches. + if ( ! $this->add_nonce( $timestamp, $nonce ) ) { + return new \WP_Error( + 'invalid_nonce', + 'Could not add nonce', + compact( 'signature_details' ) + ); + } + + // 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, $_GET['signature'] ) ) { + return new \WP_Error( + 'signature_mismatch', + 'Signature mismatch', + compact( 'signature_details' ) + ); + } + + /** + * Action for additional token checking. + * + * @since 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. + * + * @return Boolean is the site connected? + */ + public function is_active() { + return (bool) $this->get_access_token( self::JETPACK_MASTER_USER ); + } + + /** + * Returns true if the site has both a token and a blog id, which indicates a site has been registered. + * + * @access public + * + * @return bool + */ + public function is_registered() { + $blog_id = \Jetpack_Options::get_option( 'id' ); + $has_token = $this->is_active(); + return $blog_id && $has_token; + } + + /** + * 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 Integer|Boolean $user_id the user identifier. + * @return 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_access_token( $user_id ); + } + + /** + * Returns the local user ID of the connection owner. + * + * @return string|int Returns the ID of the connection owner or False if no connection owner found. + */ + public function get_connection_owner_id() { + $user_token = $this->get_access_token( JETPACK_MASTER_USER ); + $connection_owner = false; + if ( $user_token && is_object( $user_token ) && isset( $user_token->external_user_id ) ) { + $connection_owner = $user_token->external_user_id; + } + + return $connection_owner; + } + + /** + * Returns an array of user_id's that have user tokens for communicating with wpcom. + * Able to select by specific capability. + * + * @param string $capability The capability of the user. + * @return array Array of WP_User objects if found. + */ + public function get_connected_users( $capability = 'any' ) { + $connected_users = array(); + $connected_user_ids = array_keys( \Jetpack_Options::get_option( 'user_tokens' ) ); + + if ( ! empty( $connected_user_ids ) ) { + foreach ( $connected_user_ids as $id ) { + // Check for capability. + if ( 'any' !== $capability && ! user_can( $id, $capability ) ) { + continue; + } + + $connected_users[] = get_userdata( $id ); + } + } + + return $connected_users; + } + + /** + * Get the wpcom user data of the current|specified connected user. + * + * @todo Refactor to properly load the XMLRPC client independently. + * + * @param Integer $user_id the user identifier. + * @return Object the user object. + */ + public function get_connected_user_data( $user_id = null ) { + if ( ! $user_id ) { + $user_id = get_current_user_id(); + } + + $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 object|false False if no connection owner found. + */ + public function get_connection_owner() { + $user_token = $this->get_access_token( JETPACK_MASTER_USER ); + + $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 ); + } + + 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 Integer|Boolean $user_id the user identifier. False for current user. + * @return Boolean 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(); + } + + $user_token = $this->get_access_token( JETPACK_MASTER_USER ); + + return $user_token && is_object( $user_token ) && isset( $user_token->external_user_id ) && $user_id === $user_token->external_user_id; + } + + /** + * Connects the user with a specified ID to a WordPress.com user using the + * remote login flow. + * + * @access public + * + * @param Integer $user_id (optional) the user identifier, defaults to current user. + * @param String $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 ) ); // phpcs:ignore WordPress.Security.SafeRedirect + exit(); + } + + /** + * Unlinks the current user from the linked WordPress.com user. + * + * @access public + * @static + * + * @todo Refactor to properly load the XMLRPC client independently. + * + * @param Integer $user_id the user identifier. + * @return Boolean Whether the disconnection of the user was successful. + */ + public static function disconnect_user( $user_id = null ) { + $tokens = \Jetpack_Options::get_option( 'user_tokens' ); + if ( ! $tokens ) { + return false; + } + + $user_id = empty( $user_id ) ? get_current_user_id() : intval( $user_id ); + + if ( \Jetpack_Options::get_option( 'master_user' ) === $user_id ) { + return false; + } + + if ( ! isset( $tokens[ $user_id ] ) ) { + return false; + } + + $xml = new \Jetpack_IXR_Client( compact( 'user_id' ) ); + $xml->query( 'jetpack.unlink_user', $user_id ); + + unset( $tokens[ $user_id ] ); + + \Jetpack_Options::update_option( 'user_tokens', $tokens ); + + /** + * Fires after the current user has been unlinked from WordPress.com. + * + * @since 4.1.0 + * + * @param int $user_id The current user's ID. + */ + do_action( 'jetpack_unlinked_user', $user_id ); + + return true; + } + + /** + * 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' ); + $version = Constants::get_constant( 'JETPACK__API_VERSION' ); + + $api_base = $api_base ? $api_base : 'https://jetpack.wordpress.com/jetpack.'; + $version = $version ? '/' . $version . '/' : '/1/'; + + /** + * Filters the API URL that Jetpack uses for server communication. + * + * @since 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 $version the version string that is being used. + */ + return apply_filters( + 'jetpack_api_url', + rtrim( $api_base . $relative_url, '/\\' ) . $version, + $relative_url, + $api_base, + $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 Integer zero on success, or a bitmask on failure. + */ + public function register( $api_endpoint = 'register' ) { + add_action( 'pre_update_jetpack_option_register', array( '\\Jetpack_Options', 'delete_option' ) ); + $secrets = $this->generate_secrets( 'register', get_current_user_id(), 600 ); + + 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; + + /** + * Filters the request body for additional property addition. + * + * @since 7.7.0 + * + * @param Array $post_data request data. + * @param Array $token_data token data. + */ + $body = apply_filters( + 'jetpack_register_request_body', + array( + 'siteurl' => site_url(), + 'home' => 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' ), + ) + ); + + $args = array( + 'method' => 'POST', + 'body' => $body, + 'headers' => array( + 'Accept' => 'application/json', + ), + 'timeout' => $timeout, + ); + + $args['body'] = $this->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, + 'blog_token' => (string) $registration_details->jetpack_secret, + 'public' => $jetpack_public, + ) + ); + + /** + * Fires when a site is registered on WordPress.com. + * + * @since 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 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; + } + + /** + * Takes the response from the Jetpack register new site endpoint and + * verifies it worked properly. + * + * @since 2.6 + * + * @param Mixed $response the response object, or the error object. + * @return string|WP_Error A JSON object on success or Jetpack_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 = intval( $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' ); + } 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' ), $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' ), $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' ), $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. + */ + public function add_nonce( $timestamp, $nonce ) { + global $wpdb; + static $nonces_used_this_request = array(); + + if ( isset( $nonces_used_this_request[ "$timestamp:$nonce" ] ) ) { + return $nonces_used_this_request[ "$timestamp:$nonce" ]; + } + + // This should always have gone through Jetpack_Signature::sign_request() first to check $timestamp an $nonce. + $timestamp = (int) $timestamp; + $nonce = esc_sql( $nonce ); + + // Raw query so we can avoid races: add_option will also update. + $show_errors = $wpdb->show_errors( false ); + + $old_nonce = $wpdb->get_row( + $wpdb->prepare( "SELECT * FROM `$wpdb->options` WHERE option_name = %s", "jetpack_nonce_{$timestamp}_{$nonce}" ) + ); + + if ( is_null( $old_nonce ) ) { + $return = $wpdb->query( + $wpdb->prepare( + "INSERT INTO `$wpdb->options` (`option_name`, `option_value`, `autoload`) VALUES (%s, %s, %s)", + "jetpack_nonce_{$timestamp}_{$nonce}", + time(), + 'no' + ) + ); + } else { + $return = false; + } + + $wpdb->show_errors( $show_errors ); + + $nonces_used_this_request[ "$timestamp:$nonce" ] = $return; + + return $return; + } + + /** + * 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. + */ + public function clean_nonces( $all = false ) { + global $wpdb; + + $sql = "DELETE FROM `$wpdb->options` WHERE `option_name` LIKE %s"; + $sql_args = array( $wpdb->esc_like( 'jetpack_nonce_' ) . '%' ); + + if ( true !== $all ) { + $sql .= ' AND CAST( `option_value` AS UNSIGNED ) < %d'; + $sql_args[] = time() - 3600; + } + + $sql .= ' ORDER BY `option_id` LIMIT 100'; + + $sql = $wpdb->prepare( $sql, $sql_args ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + + for ( $i = 0; $i < 1000; $i++ ) { + if ( ! $wpdb->query( $sql ) ) { // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + break; + } + } + } + + /** + * Builds the timeout limit for queries talking with the wpcom servers. + * + * Based on local php max_execution_time in php.ini + * + * @since 5.4 + * @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 5.4 + * @param Integer $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 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; + } + + $earliest_registered_users = get_users( + array( + 'role' => 'administrator', + 'orderby' => 'user_registered', + 'order' => 'ASC', + 'fields' => array( '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; + } + + /** + * Returns the callable that would be used to generate secrets. + * + * @return Callable a function that returns a secure string to be used as a secret. + */ + protected function get_secret_callable() { + if ( ! isset( $this->secret_callable ) ) { + /** + * Allows modification of the callable that is used to generate connection secrets. + * + * @param Callable a function or method that returns a secret string. + */ + $this->secret_callable = apply_filters( 'jetpack_connection_secret_generator', 'wp_generate_password' ); + } + + return $this->secret_callable; + } + + /** + * Generates two secret tokens and the end of life timestamp for them. + * + * @param String $action The action name. + * @param Integer $user_id The user identifier. + * @param Integer $exp Expiration time in seconds. + */ + public function generate_secrets( $action, $user_id = false, $exp = 600 ) { + if ( false === $user_id ) { + $user_id = get_current_user_id(); + } + + $callable = $this->get_secret_callable(); + + $secrets = \Jetpack_Options::get_raw_option( + self::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; + + \Jetpack_Options::update_raw_option( self::SECRETS_OPTION_NAME, $secrets ); + return $secrets[ $secret_name ]; + } + + /** + * 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. + */ + public function get_secrets( $action, $user_id ) { + $secret_name = 'jetpack_' . $action . '_' . $user_id; + $secrets = \Jetpack_Options::get_raw_option( + self::SECRETS_OPTION_NAME, + array() + ); + + if ( ! isset( $secrets[ $secret_name ] ) ) { + return self::SECRETS_MISSING; + } + + if ( $secrets[ $secret_name ]['exp'] < time() ) { + $this->delete_secrets( $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_secrets( $action, $user_id ) { + $secret_name = 'jetpack_' . $action . '_' . $user_id; + $secrets = \Jetpack_Options::get_raw_option( + self::SECRETS_OPTION_NAME, + array() + ); + if ( isset( $secrets[ $secret_name ] ) ) { + unset( $secrets[ $secret_name ] ); + \Jetpack_Options::update_raw_option( self::SECRETS_OPTION_NAME, $secrets ); + } + } + + /** + * Deletes all connection tokens and transients from the local Jetpack site. + */ + public function delete_all_connection_tokens() { + \Jetpack_Options::delete_option( + array( + 'blog_token', + 'user_token', + 'user_tokens', + 'master_user', + 'time_diff', + 'fallback_no_verify_ssl_certs', + ) + ); + + \Jetpack_Options::delete_raw_option( 'jetpack_secrets' ); + + // Delete cached connected user data. + $transient_key = 'jetpack_connected_user_data_' . get_current_user_id(); + delete_transient( $transient_key ); + } + + /** + * Tells WordPress.com to disconnect the site and clear all tokens from cached site. + */ + public function disconnect_site_wpcom() { + $xml = new \Jetpack_IXR_Client(); + $xml->query( 'jetpack.deregister', get_current_user_id() ); + } + + /** + * 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' ), 400 ); + } + + return $this->verify_secrets( 'register', $registration_secret_1, (int) $registration_user_id ); + } + + /** + * 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_secrets( $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 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 ); + + $return_error = function( \WP_Error $error ) use ( $action, $user ) { + /** + * Verifying of the previously generated secret has failed. + * + * @since 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_secrets( $action, $user_id ); + $this->delete_secrets( $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' ), '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' ), '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' ), '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' ), 'state' ), + 400 + ) + ); + } elseif ( self::SECRETS_MISSING === $stored_secrets ) { + $error = $return_error( + new \WP_Error( + 'verify_secrets_missing', + __( 'Verification secrets not found', 'jetpack' ), + 400 + ) + ); + } elseif ( self::SECRETS_EXPIRED === $stored_secrets ) { + $error = $return_error( + new \WP_Error( + 'verify_secrets_expired', + __( 'Verification took too long', 'jetpack' ), + 400 + ) + ); + } elseif ( ! $stored_secrets ) { + $error = $return_error( + new \WP_Error( + 'verify_secrets_empty', + __( 'Verification secrets are empty', 'jetpack' ), + 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' ), + 400 + ) + ); + } elseif ( ! hash_equals( $secret_1, $stored_secrets['secret_1'] ) ) { + $error = $return_error( + new \WP_Error( + 'verify_secrets_mismatch', + __( 'Secret mismatch', 'jetpack' ), + 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 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']; + } + + /** + * 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 ) { + $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' ) ); + } + + $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' ) ); + } + + /** + * Filter the URL of the first time the user gets redirected back to your site for connection + * data processing. + * + * @since 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 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( + '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 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', + ), + ); + + $response = Client::_wp_remote_request( Utils::fix_url_for_bad_hosts( $this->api_url( 'token' ) ), $args ); + + 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 ); + } + + $error_description = isset( $json->error_description ) ? sprintf( __( 'Error Details: %s', 'jetpack' ), (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 ); + } + + @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 ); + } + + /** + * Fires after user has successfully received an auth token. + * + * @since 3.9.0 + */ + do_action( 'jetpack_user_authorized' ); + + return (string) $json->access_token; + } + + /** + * Builds a URL to the Jetpack connection auth page. + * + * @param WP_User $user (optional) defaults to the current logged in user. + * @param String $redirect (optional) a redirect URL to use instead of the default. + * @return string Connect URL. + */ + public function get_authorization_url( $user = null, $redirect = null ) { + + if ( empty( $user ) ) { + $user = wp_get_current_user(); + } + + $roles = new Roles(); + $role = $roles->translate_user_to_role( $user ); + $signed_role = $this->sign_role( $role ); + + /** + * Filter the URL of the first time the user gets redirected back to your site for connection + * data processing. + * + * @since 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 8.0.0 + * + * @param string $redirect_url Defaults to the site URL. + */ + $redirect = apply_filters( 'jetpack_connect_redirect_url', $redirect ); + + $secrets = $this->generate_secrets( '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 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 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( + 'action' => 'authorize', + '_wpnonce' => wp_create_nonce( "jetpack-authorize_{$role}_{$redirect}" ), + 'redirect' => rawurlencode( $redirect ), + ), + esc_url( $processing_url ) + ), + 'state' => $user->ID, + 'scope' => $signed_role, + 'user_email' => $user->user_email, + 'user_login' => $user->user_login, + 'is_active' => $this->is_active(), + 'jp_version' => Constants::get_constant( 'JETPACK__VERSION' ), + 'auth_type' => $auth_type, + 'secret' => $secrets['secret_1'], + 'blogname' => get_option( 'blogname' ), + 'site_url' => site_url(), + 'home_url' => home_url(), + 'site_icon' => get_site_icon_url(), + 'site_lang' => get_locale(), + 'site_created' => $this->get_assumed_site_creation_date(), + ) + ); + + $body = $this->apply_activation_source_to_args( urlencode_deep( $body ) ); + + $api_url = $this->api_url( 'authorize' ); + + return add_query_arg( $body, $api_url ); + } + + /** + * 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 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_token( $data ); + + 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_master_user = ! $this->is_active(); + + Utils::update_user_token( $current_user_id, sprintf( '%s.%d', $token, $current_user_id ), $is_master_user ); + + if ( ! $is_master_user ) { + /** + * Action fired when a secondary user has been authorized. + * + * @since 8.0.0 + */ + do_action( 'jetpack_authorize_ending_linked' ); + return 'linked'; + } + + /** + * Action fired when the master user has been authorized. + * + * @since 8.0.0 + * + * @param array $data The request data. + */ + do_action( 'jetpack_authorize_ending_authorized', $data ); + + return 'authorized'; + } + + /** + * Disconnects from the Jetpack servers. + * Forgets all connection details and tells the Jetpack servers to do the same. + */ + public function disconnect_site() { + + } + + /** + * 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' ), $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 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', + '127.0.0.1', + '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' + ), + $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' + ), + $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' + ), + $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; + } + + return true; + } + + /** + * 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 + */ + public function get_access_token( $user_id = false, $token_key = false, $suppress_errors = true ) { + $possible_special_tokens = array(); + $possible_normal_tokens = array(); + $user_tokens = \Jetpack_Options::get_option( 'user_tokens' ); + + if ( $user_id ) { + if ( ! $user_tokens ) { + return $suppress_errors ? false : new \WP_Error( 'no_user_tokens' ); + } + if ( self::JETPACK_MASTER_USER === $user_id ) { + $user_id = \Jetpack_Options::get_option( 'master_user' ); + if ( ! $user_id ) { + return $suppress_errors ? false : new \WP_Error( 'empty_master_user_option' ); + } + } + if ( ! isset( $user_tokens[ $user_id ] ) || ! $user_tokens[ $user_id ] ) { + return $suppress_errors ? false : new \WP_Error( 'no_token_for_user', sprintf( 'No token for user %d', $user_id ) ); + } + $user_token_chunks = explode( '.', $user_tokens[ $user_id ] ); + if ( empty( $user_token_chunks[1] ) || empty( $user_token_chunks[2] ) ) { + return $suppress_errors ? false : new \WP_Error( 'token_malformed', sprintf( 'Token for user %d is malformed', $user_id ) ); + } + if ( $user_token_chunks[2] !== (string) $user_id ) { + return $suppress_errors ? false : new \WP_Error( 'user_id_mismatch', sprintf( 'Requesting user_id %d does not match token user_id %d', $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 ) { + return $suppress_errors ? false : new \WP_Error( 'no_possible_tokens' ); + } + + $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 ) { + return $suppress_errors ? false : new \WP_Error( 'no_valid_token' ); + } + + return (object) array( + 'secret' => $valid_token, + 'external_user_id' => (int) $user_id, + ); + } + + /** + * 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 = $GLOBALS['HTTP_RAW_POST_DATA']; + 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 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' ), + 'readonly' => true, + 'value' => $user_data['ID'], + ); + $options['jetpack_user_login'] = array( + 'desc' => __( 'The WP.com username of the connected user', 'jetpack' ), + 'readonly' => true, + 'value' => $user_data['login'], + ); + $options['jetpack_user_email'] = array( + 'desc' => __( 'The WP.com user email of the connected user', 'jetpack' ), + 'readonly' => true, + 'value' => $user_data['email'], + ); + $options['jetpack_user_site_count'] = array( + 'desc' => __( 'The number of sites of the connected WP.com user', 'jetpack' ), + '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_active() ) { + $jetpack_client_id = \Jetpack_Options::get_option( 'id' ); + } + $options['jetpack_version'] = array( + 'desc' => __( 'Jetpack Plugin Version', 'jetpack' ), + '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' ), + '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 ) { + 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 ); + } +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-connection/src/class-rest-connector.php b/plugins/jetpack/vendor/automattic/jetpack-connection/src/class-rest-connector.php new file mode 100644 index 00000000..2231193b --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-connection/src/class-rest-connector.php @@ -0,0 +1,54 @@ +<?php +/** + * Sets up the Connection REST API endpoints. + * + * @package automattic/jetpack-connection + */ + +namespace Automattic\Jetpack\Connection; + +/** + * Registers the REST routes for Connections. + */ +class REST_Connector { + /** + * The Connection Manager. + * + * @var Manager + */ + private $connection; + + /** + * Constructor. + * + * @param Manager $connection The Connection Manager. + */ + public function __construct( Manager $connection ) { + $this->connection = $connection; + + // Register a site. + register_rest_route( + 'jetpack/v4', + '/verify_registration', + array( + 'methods' => \WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'verify_registration' ), + ) + ); + } + + /** + * Handles verification that a site is registered. + * + * @since 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 ); + } +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-connection/src/class-utils.php b/plugins/jetpack/vendor/automattic/jetpack-connection/src/class-utils.php new file mode 100644 index 00000000..1c280262 --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-connection/src/class-utils.php @@ -0,0 +1,62 @@ +<?php +/** + * The Jetpack Connection package Utils class file. + * + * @package automattic/jetpack-connection + */ + +namespace Automattic\Jetpack\Connection; + +use Automattic\Jetpack\Constants; + +/** + * Provides utility methods for the Connection package. + */ +class Utils { + + /** + * Some hosts disable the OpenSSL extension and so cannot make outgoing HTTPS requests. + * This method sets the URL scheme to HTTP when HTTPS requests can't be made. + * + * @param string $url The url. + * @return string The url with the required URL scheme. + */ + public static function fix_url_for_bad_hosts( $url ) { + // If we receive an http url, return it. + if ( 'http' === wp_parse_url( $url, PHP_URL_SCHEME ) ) { + return $url; + } + + // If the url should never be https, ensure it isn't https. + if ( 'NEVER' === Constants::get_constant( 'JETPACK_CLIENT__HTTPS' ) ) { + return set_url_scheme( $url, 'http' ); + } + + // Otherwise, return the https url. + return $url; + } + + /** + * 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 static function update_user_token( $user_id, $token, $is_master_user ) { + // Not designed for concurrent updates. + $user_tokens = \Jetpack_Options::get_option( '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 ); + } +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-connection/src/class-xmlrpc-connector.php b/plugins/jetpack/vendor/automattic/jetpack-connection/src/class-xmlrpc-connector.php new file mode 100644 index 00000000..813f5e95 --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-connection/src/class-xmlrpc-connector.php @@ -0,0 +1,80 @@ +<?php +/** + * Sets up the Connection XML-RPC methods. + * + * @package automattic/jetpack-connection + */ + +namespace Automattic\Jetpack\Connection; + +/** + * Registers the XML-RPC methods for Connections. + */ +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; + } + + return new \IXR_Error( + $code, + sprintf( 'Jetpack: [%s] %s', $data->get_error_code(), $data->get_error_message() ) + ); + } + + return $data; + } +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-connection/src/interface-manager.php b/plugins/jetpack/vendor/automattic/jetpack-connection/src/interface-manager.php new file mode 100644 index 00000000..176c8523 --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-connection/src/interface-manager.php @@ -0,0 +1,17 @@ +<?php +/** + * The Jetpack Connection Interface file. + * No longer used. + * + * @package automattic/jetpack-connection + */ + +namespace Automattic\Jetpack\Connection; + +/** + * This interface is no longer used and is now deprecated. + * + * @deprecated since 7.8 + */ +interface Manager_Interface { +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-constants/src/class-constants.php b/plugins/jetpack/vendor/automattic/jetpack-constants/src/class-constants.php new file mode 100644 index 00000000..cc2f0221 --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-constants/src/class-constants.php @@ -0,0 +1,111 @@ +<?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 the value of true + * + * @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. + * + * @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 ]; + } + + return defined( $name ) ? constant( $name ) : null; + } + + /** + * Sets the value of the "constant" within constants Manager. + * + * @param string $name The name of the constant. + * @param string $value The value of the constant. + */ + 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(); + } +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-error/src/class-error.php b/plugins/jetpack/vendor/automattic/jetpack-error/src/class-error.php new file mode 100644 index 00000000..579b851f --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-error/src/class-error.php @@ -0,0 +1,15 @@ +<?php +/** + * Jetpack Error - a wrapper around WP_Error. + * + * @see https://codex.wordpress.org/Class_Reference/WP_Error + * + * @package automattic/jetpack-error + */ + +namespace Automattic\Jetpack; + +/** + * Class Automattic\Jetpack\Error + */ +class Error extends \WP_Error {} diff --git a/plugins/jetpack/vendor/automattic/jetpack-jitm/assets/jetpack-admin-jitm-rtl.css b/plugins/jetpack/vendor/automattic/jetpack-jitm/assets/jetpack-admin-jitm-rtl.css new file mode 100644 index 00000000..ca0300d5 --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-jitm/assets/jetpack-admin-jitm-rtl.css @@ -0,0 +1,490 @@ +/*! +* Do not modify this file directly. It is automatically generated. +*/ +/*! +* Do not modify this file directly. It is compiled SASS code. +*/ +@charset "UTF-8"; +/* +The MIT License (MIT) + +Copyright © 2011–2015 thoughtbot, inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +documentation files (the “Software”), to deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit +persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +https://github.com/thoughtbot/bourbon +*/ +.jitm-button { + background: #f3f5f6; + border-color: #0071a1; + border-style: solid; + border-width: 1px; + color: #0071a1; + cursor: pointer; + display: inline-block; + margin: 0; + outline: 0; + overflow: hidden; + font-size: 14px; + text-overflow: ellipsis; + text-decoration: none; + vertical-align: top; + box-sizing: border-box; + font-size: 14px; + border-radius: 4px; + padding: 7px 14px 9px; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; +} + +.jitm-button:hover { + background: #f1f1f1; + border-color: #016087; + color: #016087; +} + +.jitm-button[disabled], .jitm-button:disabled { + color: #eeeeee; + background: white; + border-color: #eeeeee; + cursor: default; +} + +.jitm-button:focus { + background: white; + border-color: #0071a1; + box-shadow: 0 0 0 1px #0071a1; +} + +.jitm-button.is-compact { + padding: 7px; + font-size: 11px; + line-height: 1; + text-transform: uppercase; +} + +.jitm-button.is-compact:disabled { + color: #eeeeee; +} + +.jitm-button.is-compact .gridicon { + top: 4px; + margin-top: -8px; +} + +.jitm-button.is-compact .gridicons-plus-small { + margin-right: -4px; +} + +.jitm-button.is-compact .gridicons-plus-small:last-of-type { + margin-right: 0; +} + +.jitm-button.is-compact .gridicons-plus-small + .gridicon { + margin-right: -4px; +} + +.jitm-button.hidden { + display: none; +} + +.jitm-button.is-primary { + background: #007cba; + border-color: #007cba; + color: white; +} + +.jitm-button.is-primary:hover, .jitm-button.is-primary:focus { + border-color: #0071a1; + background: #0071a1; + color: white; +} + +.jitm-button.is-primary:focus { + box-shadow: 0 0 0 1px white, 0 0 0 3px #0071a1; +} + +.jitm-button.is-primary[disabled], .jitm-button.is-primary:disabled { + background: #bceefd; + border-color: #8cc9e2; + color: white; +} + +.jitm-button.is-primary.is-compact { + color: white; + white-space: nowrap; +} + +.jitm-card { + display: block; + clear: both; + position: relative; + margin: 3rem auto 0 1.25rem; + padding: 1rem; + box-sizing: border-box; + background: white; + box-shadow: 0 0 0 1px #ccd0d4, 0 1px 1px 1px rgba(0, 0, 0, 0.04); +} + +.jitm-card:after { + content: "."; + display: block; + height: 0; + clear: both; + visibility: hidden; +} + +@media (min-width: 481px) { + .jitm-card { + margin-bottom: 1rem; + padding: 1.5rem; + } +} + +.jitm-card.is-compact { + margin-bottom: 0.0625rem; +} + +@media (min-width: 481px) { + .jitm-card.is-compact { + margin-bottom: 1px; + padding: 1rem 1.5rem; + } +} + +.jitm-card.is-card-link { + padding-left: 3rem; +} + +#screen-meta-links + .jitm-card { + margin: 2.5rem auto 0 1.5385em; +} + +#dolly + .jitm-card { + margin: 3rem auto 0 1rem; +} + +.post-php .jitm-card { + margin-left: 0; +} + +.jp-lower .jitm-card { + margin: 0 0 1.5rem; +} + +.jitm-banner.jitm-card { + border-right: 4px solid; + display: flex; + padding: 0.75rem 0.75rem 0.75rem 0.375rem; + position: relative; + z-index: 2; + border-right-color: #4ab866; +} + +@media (max-width: 480px) { + .jitm-banner.jitm-card { + display: block; + } +} + +.jitm-banner.jitm-card.is-card-link { + padding: 0.75rem 1rem 0.75rem 3rem; +} + +.jitm-banner.jitm-card.is-dismissible { + padding-left: 3rem; +} + +.jitm-banner.jitm-card .jitm-banner__icon { + color: #4ab866; +} + +.jitm-banner.jitm-card .jitm-banner__icon-circle { + background-color: #4ab866; +} + +.jitm-banner.jitm-card.is-upgrade-personal { + border-right-color: #f0b849; +} + +.jitm-banner.jitm-card.is-upgrade-personal .jitm-banner__icon { + color: #f0b849; +} + +.jitm-banner.jitm-card.is-upgrade-personal .jitm-banner__icon-circle { + background-color: #f0b849; +} + +.jitm-banner.jitm-card.is-upgrade-premium { + border-right-color: #4ab866; +} + +.jitm-banner.jitm-card.is-upgrade-premium .jitm-banner__icon { + color: #4ab866; +} + +.jitm-banner.jitm-card.is-upgrade-premium .jitm-banner__icon-circle { + background-color: #4ab866; +} + +.jitm-banner.jitm-card.is-upgrade-business, .jitm-banner.jitm-card.woo-jitm { + border-right-color: #855DA6; +} + +.jitm-banner.jitm-card.is-upgrade-business .jitm-banner__icon, .jitm-banner.jitm-card.woo-jitm .jitm-banner__icon { + color: #855DA6; +} + +.jitm-banner.jitm-card.is-upgrade-business .jitm-banner__icon-circle, .jitm-banner.jitm-card.woo-jitm .jitm-banner__icon-circle { + background-color: #855DA6; +} + +.jitm-banner.jitm-card .jitm-card__link-indicator { + align-items: center; + color: #0087be; + display: flex; +} + +.jitm-banner.jitm-card:hover { + transition: all 100ms ease-in-out; +} + +.jitm-banner.jitm-card:hover.is-card-link { + box-shadow: 0 0 0 1px #a2a2a2, 0 2px 4px #d5d5d5; +} + +.jitm-banner.jitm-card:hover .jitm-card__link-indicator { + color: #005082; +} + +@media (min-width: 481px) { + .jitm-banner.jitm-card { + padding: 0.75rem 1rem; + } + .jitm-banner.jitm-card.is-dismissible { + padding-left: 1rem; + } +} + +.jitm-banner__icons { + display: flex; +} + +.jitm-banner__icons .jitm-banner__icon, +.jitm-banner__icons .jitm-banner__icon-circle { + border-radius: 50%; + flex-shrink: 0; + height: 1.5rem; + width: 1.5rem; + margin-left: 1rem; + margin-top: -0.125rem; + text-align: center; + top: 0.25rem; +} + +.jitm-banner__icons .jitm-banner__icon { + align-self: center; + color: white; + display: block; +} + +.jitm-banner__icons .jitm-banner__icon-circle { + color: white; + display: none; + padding: 0.1875rem 0.1875rem 0.25rem 0.25rem; +} + +@media (min-width: 481px) { + .jitm-banner__icons { + align-items: center; + } + .jitm-banner__icons .jitm-banner__icon { + display: none; + } + .jitm-banner__icons .jitm-banner__icon-circle { + display: block; + } +} + +.jitm-banner__icon-plan { + display: flex; + margin-left: 1rem; +} + +.jitm-banner__icon-plan .dops-plan-icon { + height: 2rem; + width: 2rem; +} + +.jitm-banner__icon-plan .jp-emblem { + position: relative; + top: 0.125rem; +} + +@media (max-width: 480px) { + .jitm-banner__icon-plan .jp-emblem { + margin-bottom: 0.75rem; + } +} + +.jitm-banner__icon-plan .jp-emblem svg { + height: 2rem; + width: 2rem; + fill: #00BE28; +} + +@media (min-width: 481px) { + .jitm-banner__icon-plan { + align-items: center; + } +} + +.jitm-banner__content { + align-items: center; + display: flex; + flex-grow: 1; + flex-wrap: wrap; +} + +@media (min-width: 481px) { + .jitm-banner__content { + flex-wrap: nowrap; + } +} + +.jitm-banner__info { + flex-grow: 1; + line-height: 1.4; +} + +@media (min-width: 481px) { + .jitm-banner__info { + flex-basis: 50%; + } +} + +@media (min-width: 961px) { + .jitm-banner__info { + flex-basis: 70%; + } +} + +.jitm-banner__info .jitm-banner__title, +.jitm-banner__info .jitm-banner__description { + color: #414141; +} + +.jitm-banner__info .jitm-banner__title { + font-size: 14px; + font-weight: 500; +} + +.jitm-banner__info .jitm-banner__description { + font-size: 0.75rem; + line-height: 1.5; + margin-top: 0.375rem; +} + +.jitm-banner__info .banner__list { + font-size: 12px; + list-style: none; + margin: 10px 0; +} + +.jitm-banner__info .banner__list li { + margin: 6px 0; +} + +.jitm-banner__info .banner__list li .gridicon { + fill: #a2a2a2; + display: inline; + margin-left: 12px; + vertical-align: bottom; +} + +.jitm-banner__action { + align-self: center; + font-size: 0.75rem; + margin: 0.5rem 0 0; + text-align: right; + width: 100%; +} + +.jitm-banner__action .jitm-banner__prices { + display: flex; + justify-content: flex-start; +} + +.jitm-banner__action .jitm-banner__prices .dops-plan-price { + margin-bottom: 0; +} + +.jitm-banner__action .jitm-banner__prices .dops-plan-price.is-discounted, +.jitm-banner__action .jitm-banner__prices .dops-plan-price.is-discounted .dops-plan-price__currency-symbol { + color: #414141; +} + +.has-call-to-action .jitm-banner__action .jitm-banner__prices .dops-plan-price { + margin-bottom: 0.5rem; +} + +@media (min-width: 481px) { + .jitm-banner__action { + margin: 0 0.5rem 0 0.25rem; + text-align: center; + width: auto; + } + .jitm-banner__action .is-dismissible { + margin-top: 2.5rem; + } + .jitm-banner__action .jitm-banner__prices { + justify-content: flex-end; + text-align: left; + } +} + +.jitm-banner__dismiss { + display: block; + text-decoration: none; + line-height: .5; +} + +.jitm-banner__dismiss:before { + color: #6f6f6f; + font: 400 16px/1 dashicons; + content: '\f158'; +} + +@media (min-width: 661px) { + .jitm-banner__dismiss { + margin-left: -0.5rem; + } +} + +@media (max-width: 480px) { + .jitm-banner__dismiss { + position: absolute; + top: 0.875rem; + left: 0.875rem; + } +} + +.jitm-banner__action + .jitm-banner__dismiss { + margin-right: 0.625rem; +} + +#dolly + .jitm-card { + margin: 3rem auto 0 1rem; +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-jitm/assets/jetpack-admin-jitm-rtl.min.css b/plugins/jetpack/vendor/automattic/jetpack-jitm/assets/jetpack-admin-jitm-rtl.min.css new file mode 100644 index 00000000..a54f3e97 --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-jitm/assets/jetpack-admin-jitm-rtl.min.css @@ -0,0 +1,5 @@ +@charset "UTF-8";/*! +* Do not modify this file directly. It is automatically generated. +*//*! +* Do not modify this file directly. It is compiled SASS code. +*/.jitm-button{background:#f3f5f6;border-color:#0071a1;border-style:solid;border-width:1px;color:#0071a1;cursor:pointer;display:inline-block;margin:0;outline:0;overflow:hidden;font-size:14px;font-weight:500;text-overflow:ellipsis;text-decoration:none;vertical-align:top;box-sizing:border-box;font-size:14px;border-radius:4px;padding:7px 14px 9px;-webkit-appearance:none;-moz-appearance:none;appearance:none}.jitm-button:hover{background:#f1f1f1;border-color:#016087;color:#016087}.jitm-button:disabled,.jitm-button[disabled]{color:#eee;background:#fff;border-color:#eee;cursor:default}.jitm-button:focus{background:#fff;border-color:#0071a1;box-shadow:0 0 0 1px #0071a1}.jitm-button.is-compact{padding:7px;font-size:11px;line-height:1;text-transform:uppercase}.jitm-button.is-compact:disabled{color:#eee}.jitm-button.is-compact .gridicon{top:4px;margin-top:-8px}.jitm-button.is-compact .gridicons-plus-small{margin-right:-4px}.jitm-button.is-compact .gridicons-plus-small:last-of-type{margin-right:0}.jitm-button.is-compact .gridicons-plus-small+.gridicon{margin-right:-4px}.jitm-button.hidden{display:none}.jitm-button.is-primary{background:#007cba;border-color:#007cba;color:#fff}.jitm-button.is-primary:focus,.jitm-button.is-primary:hover{border-color:#0071a1;background:#0071a1;color:#fff}.jitm-button.is-primary:focus{box-shadow:0 0 0 1px #fff,0 0 0 3px #0071a1}.jitm-button.is-primary:disabled,.jitm-button.is-primary[disabled]{background:#bceefd;border-color:#8cc9e2;color:#fff}.jitm-button.is-primary.is-compact{color:#fff;white-space:nowrap}.jitm-card{display:block;clear:both;position:relative;margin:3rem auto 0 1.25rem;padding:1rem;box-sizing:border-box;background:#fff;box-shadow:0 0 0 1px #ccd0d4,0 1px 1px 1px rgba(0,0,0,.04)}.jitm-card:after{content:".";display:block;height:0;clear:both;visibility:hidden}@media (min-width:481px){.jitm-card{margin-bottom:1rem;padding:1.5rem}}.jitm-card.is-compact{margin-bottom:.0625rem}@media (min-width:481px){.jitm-card.is-compact{margin-bottom:1px;padding:1rem 1.5rem}}.jitm-card.is-card-link{padding-left:3rem}#screen-meta-links+.jitm-card{margin:2.5rem auto 0 1.5385em}#dolly+.jitm-card{margin:3rem auto 0 1rem}.post-php .jitm-card{margin-left:0}.jp-lower .jitm-card{margin:0 0 1.5rem}.jitm-banner.jitm-card{border-right:4px solid;display:flex;padding:.75rem .75rem .75rem .375rem;position:relative;z-index:2;border-right-color:#4ab866}@media (max-width:480px){.jitm-banner.jitm-card{display:block}}.jitm-banner.jitm-card.is-card-link{padding:.75rem 1rem .75rem 3rem}.jitm-banner.jitm-card.is-dismissible{padding-left:3rem}.jitm-banner.jitm-card .jitm-banner__icon{color:#4ab866}.jitm-banner.jitm-card .jitm-banner__icon-circle{background-color:#4ab866}.jitm-banner.jitm-card.is-upgrade-personal{border-right-color:#f0b849}.jitm-banner.jitm-card.is-upgrade-personal .jitm-banner__icon{color:#f0b849}.jitm-banner.jitm-card.is-upgrade-personal .jitm-banner__icon-circle{background-color:#f0b849}.jitm-banner.jitm-card.is-upgrade-premium{border-right-color:#4ab866}.jitm-banner.jitm-card.is-upgrade-premium .jitm-banner__icon{color:#4ab866}.jitm-banner.jitm-card.is-upgrade-premium .jitm-banner__icon-circle{background-color:#4ab866}.jitm-banner.jitm-card.is-upgrade-business,.jitm-banner.jitm-card.woo-jitm{border-right-color:#855da6}.jitm-banner.jitm-card.is-upgrade-business .jitm-banner__icon,.jitm-banner.jitm-card.woo-jitm .jitm-banner__icon{color:#855da6}.jitm-banner.jitm-card.is-upgrade-business .jitm-banner__icon-circle,.jitm-banner.jitm-card.woo-jitm .jitm-banner__icon-circle{background-color:#855da6}.jitm-banner.jitm-card .jitm-card__link-indicator{align-items:center;color:#0087be;display:flex}.jitm-banner.jitm-card:hover{transition:all .1s ease-in-out}.jitm-banner.jitm-card:hover.is-card-link{box-shadow:0 0 0 1px #a2a2a2,0 2px 4px #d5d5d5}.jitm-banner.jitm-card:hover .jitm-card__link-indicator{color:#005082}@media (min-width:481px){.jitm-banner.jitm-card{padding:.75rem 1rem}.jitm-banner.jitm-card.is-dismissible{padding-left:1rem}}.jitm-banner__icons{display:flex}.jitm-banner__icons .jitm-banner__icon,.jitm-banner__icons .jitm-banner__icon-circle{border-radius:50%;flex-shrink:0;height:1.5rem;width:1.5rem;margin-left:1rem;margin-top:-.125rem;text-align:center;top:.25rem}.jitm-banner__icons .jitm-banner__icon{align-self:center;color:#fff;display:block}.jitm-banner__icons .jitm-banner__icon-circle{color:#fff;display:none;padding:.1875rem .1875rem .25rem .25rem}@media (min-width:481px){.jitm-banner__icons{align-items:center}.jitm-banner__icons .jitm-banner__icon{display:none}.jitm-banner__icons .jitm-banner__icon-circle{display:block}}.jitm-banner__icon-plan{display:flex;margin-left:1rem}.jitm-banner__icon-plan .dops-plan-icon{height:2rem;width:2rem}.jitm-banner__icon-plan .jp-emblem{position:relative;top:.125rem}@media (max-width:480px){.jitm-banner__icon-plan .jp-emblem{margin-bottom:.75rem}}.jitm-banner__icon-plan .jp-emblem svg{height:2rem;width:2rem;fill:#00be28}@media (min-width:481px){.jitm-banner__icon-plan{align-items:center}}.jitm-banner__content{align-items:center;display:flex;flex-grow:1;flex-wrap:wrap}@media (min-width:481px){.jitm-banner__content{flex-wrap:nowrap}}.jitm-banner__info{flex-grow:1;line-height:1.4}@media (min-width:481px){.jitm-banner__info{flex-basis:50%}}@media (min-width:961px){.jitm-banner__info{flex-basis:70%}}.jitm-banner__info .jitm-banner__description,.jitm-banner__info .jitm-banner__title{color:#414141}.jitm-banner__info .jitm-banner__title{font-size:14px;font-weight:500}.jitm-banner__info .jitm-banner__description{font-size:.75rem;line-height:1.5;margin-top:.375rem}.jitm-banner__info .banner__list{font-size:12px;list-style:none;margin:10px 0}.jitm-banner__info .banner__list li{margin:6px 0}.jitm-banner__info .banner__list li .gridicon{fill:#a2a2a2;display:inline;margin-left:12px;vertical-align:bottom}.jitm-banner__action{align-self:center;font-size:.75rem;margin:.5rem 0 0;text-align:right;width:100%}.jitm-banner__action .jitm-banner__prices{display:flex;justify-content:flex-start}.jitm-banner__action .jitm-banner__prices .dops-plan-price{margin-bottom:0}.jitm-banner__action .jitm-banner__prices .dops-plan-price.is-discounted,.jitm-banner__action .jitm-banner__prices .dops-plan-price.is-discounted .dops-plan-price__currency-symbol{color:#414141}.has-call-to-action .jitm-banner__action .jitm-banner__prices .dops-plan-price{margin-bottom:.5rem}@media (min-width:481px){.jitm-banner__action{margin:0 .5rem 0 .25rem;text-align:center;width:auto}.jitm-banner__action .is-dismissible{margin-top:2.5rem}.jitm-banner__action .jitm-banner__prices{justify-content:flex-end;text-align:left}}.jitm-banner__dismiss{display:block;text-decoration:none;line-height:.5}.jitm-banner__dismiss:before{color:#6f6f6f;font:400 16px/1 dashicons;content:'\f158'}@media (min-width:661px){.jitm-banner__dismiss{margin-left:-.5rem}}@media (max-width:480px){.jitm-banner__dismiss{position:absolute;top:.875rem;left:.875rem}}.jitm-banner__action+.jitm-banner__dismiss{margin-right:.625rem}#dolly+.jitm-card{margin:3rem auto 0 1rem}
\ No newline at end of file diff --git a/plugins/jetpack/vendor/automattic/jetpack-jitm/assets/jetpack-admin-jitm.css b/plugins/jetpack/vendor/automattic/jetpack-jitm/assets/jetpack-admin-jitm.css new file mode 100644 index 00000000..a2717042 --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-jitm/assets/jetpack-admin-jitm.css @@ -0,0 +1,489 @@ +/*! +* Do not modify this file directly. It is compiled SASS code. +*/ +@charset "UTF-8"; +/* +The MIT License (MIT) + +Copyright © 2011–2015 thoughtbot, inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +documentation files (the “Software”), to deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit +persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +https://github.com/thoughtbot/bourbon +*/ +.jitm-button { + background: #f3f5f6; + border-color: #0071a1; + border-style: solid; + border-width: 1px; + color: #0071a1; + cursor: pointer; + display: inline-block; + margin: 0; + outline: 0; + overflow: hidden; + font-size: 14px; + text-overflow: ellipsis; + text-decoration: none; + vertical-align: top; + box-sizing: border-box; + font-size: 14px; + border-radius: 4px; + padding: 7px 14px 9px; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; +} + +.jitm-button:hover { + background: #f1f1f1; + border-color: #016087; + color: #016087; +} + +.jitm-button[disabled], .jitm-button:disabled { + color: #eeeeee; + background: white; + border-color: #eeeeee; + cursor: default; +} + +.jitm-button:focus { + background: white; + border-color: #0071a1; + box-shadow: 0 0 0 1px #0071a1; +} + +.jitm-button.is-compact { + padding: 7px; + font-size: 11px; + line-height: 1; + text-transform: uppercase; +} + +.jitm-button.is-compact:disabled { + color: #eeeeee; +} + +.jitm-button.is-compact .gridicon { + top: 4px; + margin-top: -8px; +} + +.jitm-button.is-compact .gridicons-plus-small { + margin-left: -4px; +} + +.jitm-button.is-compact .gridicons-plus-small:last-of-type { + margin-left: 0; +} + +.jitm-button.is-compact .gridicons-plus-small + .gridicon { + margin-left: -4px; +} + +.jitm-button.hidden { + display: none; +} + +.jitm-button.is-primary { + background: #007cba; + border-color: #007cba; + color: white; +} + +.jitm-button.is-primary:hover, .jitm-button.is-primary:focus { + border-color: #0071a1; + background: #0071a1; + color: white; +} + +.jitm-button.is-primary:focus { + box-shadow: 0 0 0 1px white, 0 0 0 3px #0071a1; +} + +.jitm-button.is-primary[disabled], .jitm-button.is-primary:disabled { + background: #bceefd; + border-color: #8cc9e2; + color: white; +} + +.jitm-button.is-primary.is-compact { + color: white; + white-space: nowrap; +} + +.jitm-card { + display: block; + clear: both; + position: relative; + margin: 3rem 1.25rem 0 auto; + padding: 1rem; + box-sizing: border-box; + background: white; + box-shadow: 0 0 0 1px #ccd0d4, 0 1px 1px 1px rgba(0, 0, 0, 0.04); +} + +.jitm-card:after { + content: "."; + display: block; + height: 0; + clear: both; + visibility: hidden; +} + +@media (min-width: 481px) { + .jitm-card { + margin-bottom: 1rem; + padding: 1.5rem; + } +} + +.jitm-card.is-compact { + margin-bottom: 0.0625rem; +} + +@media (min-width: 481px) { + .jitm-card.is-compact { + margin-bottom: 1px; + padding: 1rem 1.5rem; + } +} + +.jitm-card.is-card-link { + padding-right: 3rem; +} + +#screen-meta-links + .jitm-card { + margin: 2.5rem 1.5385em 0 auto; +} + +#dolly + .jitm-card { + margin: 3rem 1rem 0 auto; +} + +.post-php .jitm-card { + margin-right: 0; +} + +.jp-lower .jitm-card { + margin: 0 0 1.5rem; +} + +.jitm-banner.jitm-card { + border-left: 4px solid; + display: flex; + padding: 0.75rem 0.375rem 0.75rem 0.75rem; + position: relative; + z-index: 2; + border-left-color: #4ab866; +} + +@media (max-width: 480px) { + .jitm-banner.jitm-card { + display: block; + } +} + +.jitm-banner.jitm-card.is-card-link { + padding: 0.75rem 3rem 0.75rem 1rem; +} + +.jitm-banner.jitm-card.is-dismissible { + padding-right: 3rem; +} + +.jitm-banner.jitm-card .jitm-banner__icon { + color: #4ab866; +} + +.jitm-banner.jitm-card .jitm-banner__icon-circle { + background-color: #4ab866; +} + +.jitm-banner.jitm-card.is-upgrade-personal { + border-left-color: #f0b849; +} + +.jitm-banner.jitm-card.is-upgrade-personal .jitm-banner__icon { + color: #f0b849; +} + +.jitm-banner.jitm-card.is-upgrade-personal .jitm-banner__icon-circle { + background-color: #f0b849; +} + +.jitm-banner.jitm-card.is-upgrade-premium { + border-left-color: #4ab866; +} + +.jitm-banner.jitm-card.is-upgrade-premium .jitm-banner__icon { + color: #4ab866; +} + +.jitm-banner.jitm-card.is-upgrade-premium .jitm-banner__icon-circle { + background-color: #4ab866; +} + +.jitm-banner.jitm-card.is-upgrade-business, .jitm-banner.jitm-card.woo-jitm { + border-left-color: #855DA6; +} + +.jitm-banner.jitm-card.is-upgrade-business .jitm-banner__icon, .jitm-banner.jitm-card.woo-jitm .jitm-banner__icon { + color: #855DA6; +} + +.jitm-banner.jitm-card.is-upgrade-business .jitm-banner__icon-circle, .jitm-banner.jitm-card.woo-jitm .jitm-banner__icon-circle { + background-color: #855DA6; +} + +.jitm-banner.jitm-card .jitm-card__link-indicator { + align-items: center; + color: #0087be; + display: flex; +} + +.jitm-banner.jitm-card:hover { + transition: all 100ms ease-in-out; +} + +.jitm-banner.jitm-card:hover.is-card-link { + box-shadow: 0 0 0 1px #a2a2a2, 0 2px 4px #d5d5d5; +} + +.jitm-banner.jitm-card:hover .jitm-card__link-indicator { + color: #005082; +} + +@media (min-width: 481px) { + .jitm-banner.jitm-card { + padding: 0.75rem 1rem; + } + .jitm-banner.jitm-card.is-dismissible { + padding-right: 1rem; + } +} + +.jitm-banner__icons { + display: flex; +} + +.jitm-banner__icons .jitm-banner__icon, +.jitm-banner__icons .jitm-banner__icon-circle { + border-radius: 50%; + flex-shrink: 0; + height: 1.5rem; + width: 1.5rem; + margin-right: 1rem; + margin-top: -0.125rem; + text-align: center; + top: 0.25rem; +} + +.jitm-banner__icons .jitm-banner__icon { + align-self: center; + color: white; + display: block; +} + +.jitm-banner__icons .jitm-banner__icon-circle { + color: white; + display: none; + padding: 0.1875rem 0.25rem 0.25rem 0.1875rem; +} + +@media (min-width: 481px) { + .jitm-banner__icons { + align-items: center; + } + .jitm-banner__icons .jitm-banner__icon { + display: none; + } + .jitm-banner__icons .jitm-banner__icon-circle { + display: block; + } +} + +.jitm-banner__icon-plan { + display: flex; + margin-right: 1rem; +} + +.jitm-banner__icon-plan .dops-plan-icon { + height: 2rem; + width: 2rem; +} + +.jitm-banner__icon-plan .jp-emblem { + position: relative; + top: 0.125rem; +} + +@media (max-width: 480px) { + .jitm-banner__icon-plan .jp-emblem { + margin-bottom: 0.75rem; + } +} + +.jitm-banner__icon-plan .jp-emblem svg { + height: 2rem; + width: 2rem; + fill: #00BE28; +} + +@media (min-width: 481px) { + .jitm-banner__icon-plan { + align-items: center; + } +} + +.jitm-banner__content { + align-items: center; + display: flex; + flex-grow: 1; + flex-wrap: wrap; +} + +@media (min-width: 481px) { + .jitm-banner__content { + flex-wrap: nowrap; + } +} + +.jitm-banner__info { + flex-grow: 1; + line-height: 1.4; +} + +@media (min-width: 481px) { + .jitm-banner__info { + flex-basis: 50%; + } +} + +@media (min-width: 961px) { + .jitm-banner__info { + flex-basis: 70%; + } +} + +.jitm-banner__info .jitm-banner__title, +.jitm-banner__info .jitm-banner__description { + color: #414141; +} + +.jitm-banner__info .jitm-banner__title { + font-size: 14px; + font-weight: 500; +} + +.jitm-banner__info .jitm-banner__description { + font-size: 0.75rem; + line-height: 1.5; + margin-top: 0.375rem; +} + +.jitm-banner__info .banner__list { + font-size: 12px; + list-style: none; + margin: 10px 0; +} + +.jitm-banner__info .banner__list li { + margin: 6px 0; +} + +.jitm-banner__info .banner__list li .gridicon { + fill: #a2a2a2; + display: inline; + margin-right: 12px; + vertical-align: bottom; +} + +.jitm-banner__action { + align-self: center; + font-size: 0.75rem; + margin: 0.5rem 0 0; + text-align: left; + width: 100%; +} + +.jitm-banner__action .jitm-banner__prices { + display: flex; + justify-content: flex-start; +} + +.jitm-banner__action .jitm-banner__prices .dops-plan-price { + margin-bottom: 0; +} + +.jitm-banner__action .jitm-banner__prices .dops-plan-price.is-discounted, +.jitm-banner__action .jitm-banner__prices .dops-plan-price.is-discounted .dops-plan-price__currency-symbol { + color: #414141; +} + +.has-call-to-action .jitm-banner__action .jitm-banner__prices .dops-plan-price { + margin-bottom: 0.5rem; +} + +@media (min-width: 481px) { + .jitm-banner__action { + margin: 0 0.25rem 0 0.5rem; + text-align: center; + width: auto; + } + .jitm-banner__action .is-dismissible { + margin-top: 2.5rem; + } + .jitm-banner__action .jitm-banner__prices { + justify-content: flex-end; + text-align: right; + } +} + +.jitm-banner__dismiss { + display: block; + text-decoration: none; + line-height: .5; +} + +.jitm-banner__dismiss:before { + color: #6f6f6f; + font: 400 16px/1 dashicons; + content: '\f158'; +} + +@media (min-width: 661px) { + .jitm-banner__dismiss { + margin-right: -0.5rem; + } +} + +@media (max-width: 480px) { + .jitm-banner__dismiss { + position: absolute; + top: 0.875rem; + right: 0.875rem; + } +} + +.jitm-banner__action + .jitm-banner__dismiss { + margin-left: 0.625rem; +} + +#dolly + .jitm-card { + margin: 3rem 1rem 0 auto; +} + +/*# sourceMappingURL=data:application/json;charset=utf8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiIiwic291cmNlcyI6WyJwYWNrYWdlcy9qaXRtL2Fzc2V0cy9qZXRwYWNrLWFkbWluLWppdG0uY3NzIl0sInNvdXJjZXNDb250ZW50IjpbIi8qIVxuKiBEbyBub3QgbW9kaWZ5IHRoaXMgZmlsZSBkaXJlY3RseS4gIEl0IGlzIGNvbXBpbGVkIFNBU1MgY29kZS5cbiovXG5AY2hhcnNldCBcIlVURi04XCI7XG4vKlxuVGhlIE1JVCBMaWNlbnNlIChNSVQpXG5cbkNvcHlyaWdodCDCqSAyMDEx4oCTMjAxNSB0aG91Z2h0Ym90LCBpbmMuXG5cblBlcm1pc3Npb24gaXMgaGVyZWJ5IGdyYW50ZWQsIGZyZWUgb2YgY2hhcmdlLCB0byBhbnkgcGVyc29uIG9idGFpbmluZyBhIGNvcHkgb2YgdGhpcyBzb2Z0d2FyZSBhbmQgYXNzb2NpYXRlZFxuZG9jdW1lbnRhdGlvbiBmaWxlcyAodGhlIOKAnFNvZnR3YXJl4oCdKSwgdG8gZGVhbCBpbiB0aGUgU29mdHdhcmUgd2l0aG91dCByZXN0cmljdGlvbiwgaW5jbHVkaW5nIHdpdGhvdXQgbGltaXRhdGlvbiB0aGVcbnJpZ2h0cyB0byB1c2UsIGNvcHksIG1vZGlmeSwgbWVyZ2UsIHB1Ymxpc2gsIGRpc3RyaWJ1dGUsIHN1YmxpY2Vuc2UsIGFuZC9vciBzZWxsIGNvcGllcyBvZiB0aGUgU29mdHdhcmUsIGFuZCB0byBwZXJtaXRcbnBlcnNvbnMgdG8gd2hvbSB0aGUgU29mdHdhcmUgaXMgZnVybmlzaGVkIHRvIGRvIHNvLCBzdWJqZWN0IHRvIHRoZSBmb2xsb3dpbmcgY29uZGl0aW9uczpcblxuVGhlIGFib3ZlIGNvcHlyaWdodCBub3RpY2UgYW5kIHRoaXMgcGVybWlzc2lvbiBub3RpY2Ugc2hhbGwgYmUgaW5jbHVkZWQgaW4gYWxsIGNvcGllcyBvciBzdWJzdGFudGlhbFxucG9ydGlvbnMgb2YgdGhlIFNvZnR3YXJlLlxuXG5USEUgU09GVFdBUkUgSVMgUFJPVklERUQg4oCcQVMgSVPigJ0sIFdJVEhPVVQgV0FSUkFOVFkgT0YgQU5ZIEtJTkQsIEVYUFJFU1MgT1IgSU1QTElFRCwgSU5DTFVESU5HIEJVVCBOT1QgTElNSVRFRCBUTyBUSEVcbldBUlJBTlRJRVMgT0YgTUVSQ0hBTlRBQklMSVRZLCBGSVRORVNTIEZPUiBBIFBBUlRJQ1VMQVIgUFVSUE9TRSBBTkQgTk9OSU5GUklOR0VNRU5ULiBJTiBOTyBFVkVOVCBTSEFMTCBUSEUgQVVUSE9SUyBPUlxuQ09QWVJJR0hUIEhPTERFUlMgQkUgTElBQkxFIEZPUiBBTlkgQ0xBSU0sIERBTUFHRVMgT1IgT1RIRVIgTElBQklMSVRZLCBXSEVUSEVSIElOIEFOIEFDVElPTiBPRiBDT05UUkFDVCwgVE9SVCBPUlxuT1RIRVJXSVNFLCBBUklTSU5HIEZST00sIE9VVCBPRiBPUiBJTiBDT05ORUNUSU9OIFdJVEggVEhFIFNPRlRXQVJFIE9SIFRIRSBVU0UgT1IgT1RIRVIgREVBTElOR1MgSU4gVEhFIFNPRlRXQVJFLlxuXG5odHRwczovL2dpdGh1Yi5jb20vdGhvdWdodGJvdC9ib3VyYm9uXG4qL1xuLmppdG0tYnV0dG9uIHtcbiAgYmFja2dyb3VuZDogI2YzZjVmNjtcbiAgYm9yZGVyLWNvbG9yOiAjMDA3MWExO1xuICBib3JkZXItc3R5bGU6IHNvbGlkO1xuICBib3JkZXItd2lkdGg6IDFweDtcbiAgY29sb3I6ICMwMDcxYTE7XG4gIGN1cnNvcjogcG9pbnRlcjtcbiAgZGlzcGxheTogaW5saW5lLWJsb2NrO1xuICBtYXJnaW46IDA7XG4gIG91dGxpbmU6IDA7XG4gIG92ZXJmbG93OiBoaWRkZW47XG4gIGZvbnQtc2l6ZTogMTRweDtcbiAgdGV4dC1vdmVyZmxvdzogZWxsaXBzaXM7XG4gIHRleHQtZGVjb3JhdGlvbjogbm9uZTtcbiAgdmVydGljYWwtYWxpZ246IHRvcDtcbiAgYm94LXNpemluZzogYm9yZGVyLWJveDtcbiAgZm9udC1zaXplOiAxNHB4O1xuICBib3JkZXItcmFkaXVzOiA0cHg7XG4gIHBhZGRpbmc6IDdweCAxNHB4IDlweDtcbiAgLXdlYmtpdC1hcHBlYXJhbmNlOiBub25lO1xuICAtbW96LWFwcGVhcmFuY2U6IG5vbmU7XG4gICAgICAgYXBwZWFyYW5jZTogbm9uZTtcbn1cblxuLmppdG0tYnV0dG9uOmhvdmVyIHtcbiAgYmFja2dyb3VuZDogI2YxZjFmMTtcbiAgYm9yZGVyLWNvbG9yOiAjMDE2MDg3O1xuICBjb2xvcjogIzAxNjA4Nztcbn1cblxuLmppdG0tYnV0dG9uW2Rpc2FibGVkXSwgLmppdG0tYnV0dG9uOmRpc2FibGVkIHtcbiAgY29sb3I6ICNlZWVlZWU7XG4gIGJhY2tncm91bmQ6IHdoaXRlO1xuICBib3JkZXItY29sb3I6ICNlZWVlZWU7XG4gIGN1cnNvcjogZGVmYXVsdDtcbn1cblxuLmppdG0tYnV0dG9uOmZvY3VzIHtcbiAgYmFja2dyb3VuZDogd2hpdGU7XG4gIGJvcmRlci1jb2xvcjogIzAwNzFhMTtcbiAgYm94LXNoYWRvdzogMCAwIDAgMXB4ICMwMDcxYTE7XG59XG5cbi5qaXRtLWJ1dHRvbi5pcy1jb21wYWN0IHtcbiAgcGFkZGluZzogN3B4O1xuICBmb250LXNpemU6IDExcHg7XG4gIGxpbmUtaGVpZ2h0OiAxO1xuICB0ZXh0LXRyYW5zZm9ybTogdXBwZXJjYXNlO1xufVxuXG4uaml0bS1idXR0b24uaXMtY29tcGFjdDpkaXNhYmxlZCB7XG4gIGNvbG9yOiAjZWVlZWVlO1xufVxuXG4uaml0bS1idXR0b24uaXMtY29tcGFjdCAuZ3JpZGljb24ge1xuICB0b3A6IDRweDtcbiAgbWFyZ2luLXRvcDogLThweDtcbn1cblxuLmppdG0tYnV0dG9uLmlzLWNvbXBhY3QgLmdyaWRpY29ucy1wbHVzLXNtYWxsIHtcbiAgbWFyZ2luLWxlZnQ6IC00cHg7XG59XG5cbi5qaXRtLWJ1dHRvbi5pcy1jb21wYWN0IC5ncmlkaWNvbnMtcGx1cy1zbWFsbDpsYXN0LW9mLXR5cGUge1xuICBtYXJnaW4tbGVmdDogMDtcbn1cblxuLmppdG0tYnV0dG9uLmlzLWNvbXBhY3QgLmdyaWRpY29ucy1wbHVzLXNtYWxsICsgLmdyaWRpY29uIHtcbiAgbWFyZ2luLWxlZnQ6IC00cHg7XG59XG5cbi5qaXRtLWJ1dHRvbi5oaWRkZW4ge1xuICBkaXNwbGF5OiBub25lO1xufVxuXG4uaml0bS1idXR0b24uaXMtcHJpbWFyeSB7XG4gIGJhY2tncm91bmQ6ICMwMDdjYmE7XG4gIGJvcmRlci1jb2xvcjogIzAwN2NiYTtcbiAgY29sb3I6IHdoaXRlO1xufVxuXG4uaml0bS1idXR0b24uaXMtcHJpbWFyeTpob3ZlciwgLmppdG0tYnV0dG9uLmlzLXByaW1hcnk6Zm9jdXMge1xuICBib3JkZXItY29sb3I6ICMwMDcxYTE7XG4gIGJhY2tncm91bmQ6ICMwMDcxYTE7XG4gIGNvbG9yOiB3aGl0ZTtcbn1cblxuLmppdG0tYnV0dG9uLmlzLXByaW1hcnk6Zm9jdXMge1xuICBib3gtc2hhZG93OiAwIDAgMCAxcHggd2hpdGUsIDAgMCAwIDNweCAjMDA3MWExO1xufVxuXG4uaml0bS1idXR0b24uaXMtcHJpbWFyeVtkaXNhYmxlZF0sIC5qaXRtLWJ1dHRvbi5pcy1wcmltYXJ5OmRpc2FibGVkIHtcbiAgYmFja2dyb3VuZDogI2JjZWVmZDtcbiAgYm9yZGVyLWNvbG9yOiAjOGNjOWUyO1xuICBjb2xvcjogd2hpdGU7XG59XG5cbi5qaXRtLWJ1dHRvbi5pcy1wcmltYXJ5LmlzLWNvbXBhY3Qge1xuICBjb2xvcjogd2hpdGU7XG4gIHdoaXRlLXNwYWNlOiBub3dyYXA7XG59XG5cbi5qaXRtLWNhcmQge1xuICBkaXNwbGF5OiBibG9jaztcbiAgY2xlYXI6IGJvdGg7XG4gIHBvc2l0aW9uOiByZWxhdGl2ZTtcbiAgbWFyZ2luOiAzcmVtIDEuMjVyZW0gMCBhdXRvO1xuICBwYWRkaW5nOiAxcmVtO1xuICBib3gtc2l6aW5nOiBib3JkZXItYm94O1xuICBiYWNrZ3JvdW5kOiB3aGl0ZTtcbiAgYm94LXNoYWRvdzogMCAwIDAgMXB4ICNjY2QwZDQsIDAgMXB4IDFweCAxcHggcmdiYSgwLCAwLCAwLCAwLjA0KTtcbn1cblxuLmppdG0tY2FyZDphZnRlciB7XG4gIGNvbnRlbnQ6IFwiLlwiO1xuICBkaXNwbGF5OiBibG9jaztcbiAgaGVpZ2h0OiAwO1xuICBjbGVhcjogYm90aDtcbiAgdmlzaWJpbGl0eTogaGlkZGVuO1xufVxuXG5AbWVkaWEgKG1pbi13aWR0aDogNDgxcHgpIHtcbiAgLmppdG0tY2FyZCB7XG4gICAgbWFyZ2luLWJvdHRvbTogMXJlbTtcbiAgICBwYWRkaW5nOiAxLjVyZW07XG4gIH1cbn1cblxuLmppdG0tY2FyZC5pcy1jb21wYWN0IHtcbiAgbWFyZ2luLWJvdHRvbTogMC4wNjI1cmVtO1xufVxuXG5AbWVkaWEgKG1pbi13aWR0aDogNDgxcHgpIHtcbiAgLmppdG0tY2FyZC5pcy1jb21wYWN0IHtcbiAgICBtYXJnaW4tYm90dG9tOiAxcHg7XG4gICAgcGFkZGluZzogMXJlbSAxLjVyZW07XG4gIH1cbn1cblxuLmppdG0tY2FyZC5pcy1jYXJkLWxpbmsge1xuICBwYWRkaW5nLXJpZ2h0OiAzcmVtO1xufVxuXG4jc2NyZWVuLW1ldGEtbGlua3MgKyAuaml0bS1jYXJkIHtcbiAgbWFyZ2luOiAyLjVyZW0gMS41Mzg1ZW0gMCBhdXRvO1xufVxuXG4jZG9sbHkgKyAuaml0bS1jYXJkIHtcbiAgbWFyZ2luOiAzcmVtIDFyZW0gMCBhdXRvO1xufVxuXG4ucG9zdC1waHAgLmppdG0tY2FyZCB7XG4gIG1hcmdpbi1yaWdodDogMDtcbn1cblxuLmpwLWxvd2VyIC5qaXRtLWNhcmQge1xuICBtYXJnaW46IDAgMCAxLjVyZW07XG59XG5cbi5qaXRtLWJhbm5lci5qaXRtLWNhcmQge1xuICBib3JkZXItbGVmdDogNHB4IHNvbGlkO1xuICBkaXNwbGF5OiBmbGV4O1xuICBwYWRkaW5nOiAwLjc1cmVtIDAuMzc1cmVtIDAuNzVyZW0gMC43NXJlbTtcbiAgcG9zaXRpb246IHJlbGF0aXZlO1xuICB6LWluZGV4OiAyO1xuICBib3JkZXItbGVmdC1jb2xvcjogIzRhYjg2Njtcbn1cblxuQG1lZGlhIChtYXgtd2lkdGg6IDQ4MHB4KSB7XG4gIC5qaXRtLWJhbm5lci5qaXRtLWNhcmQge1xuICAgIGRpc3BsYXk6IGJsb2NrO1xuICB9XG59XG5cbi5qaXRtLWJhbm5lci5qaXRtLWNhcmQuaXMtY2FyZC1saW5rIHtcbiAgcGFkZGluZzogMC43NXJlbSAzcmVtIDAuNzVyZW0gMXJlbTtcbn1cblxuLmppdG0tYmFubmVyLmppdG0tY2FyZC5pcy1kaXNtaXNzaWJsZSB7XG4gIHBhZGRpbmctcmlnaHQ6IDNyZW07XG59XG5cbi5qaXRtLWJhbm5lci5qaXRtLWNhcmQgLmppdG0tYmFubmVyX19pY29uIHtcbiAgY29sb3I6ICM0YWI4NjY7XG59XG5cbi5qaXRtLWJhbm5lci5qaXRtLWNhcmQgLmppdG0tYmFubmVyX19pY29uLWNpcmNsZSB7XG4gIGJhY2tncm91bmQtY29sb3I6ICM0YWI4NjY7XG59XG5cbi5qaXRtLWJhbm5lci5qaXRtLWNhcmQuaXMtdXBncmFkZS1wZXJzb25hbCB7XG4gIGJvcmRlci1sZWZ0LWNvbG9yOiAjZjBiODQ5O1xufVxuXG4uaml0bS1iYW5uZXIuaml0bS1jYXJkLmlzLXVwZ3JhZGUtcGVyc29uYWwgLmppdG0tYmFubmVyX19pY29uIHtcbiAgY29sb3I6ICNmMGI4NDk7XG59XG5cbi5qaXRtLWJhbm5lci5qaXRtLWNhcmQuaXMtdXBncmFkZS1wZXJzb25hbCAuaml0bS1iYW5uZXJfX2ljb24tY2lyY2xlIHtcbiAgYmFja2dyb3VuZC1jb2xvcjogI2YwYjg0OTtcbn1cblxuLmppdG0tYmFubmVyLmppdG0tY2FyZC5pcy11cGdyYWRlLXByZW1pdW0ge1xuICBib3JkZXItbGVmdC1jb2xvcjogIzRhYjg2Njtcbn1cblxuLmppdG0tYmFubmVyLmppdG0tY2FyZC5pcy11cGdyYWRlLXByZW1pdW0gLmppdG0tYmFubmVyX19pY29uIHtcbiAgY29sb3I6ICM0YWI4NjY7XG59XG5cbi5qaXRtLWJhbm5lci5qaXRtLWNhcmQuaXMtdXBncmFkZS1wcmVtaXVtIC5qaXRtLWJhbm5lcl9faWNvbi1jaXJjbGUge1xuICBiYWNrZ3JvdW5kLWNvbG9yOiAjNGFiODY2O1xufVxuXG4uaml0bS1iYW5uZXIuaml0bS1jYXJkLmlzLXVwZ3JhZGUtYnVzaW5lc3MsIC5qaXRtLWJhbm5lci5qaXRtLWNhcmQud29vLWppdG0ge1xuICBib3JkZXItbGVmdC1jb2xvcjogIzg1NURBNjtcbn1cblxuLmppdG0tYmFubmVyLmppdG0tY2FyZC5pcy11cGdyYWRlLWJ1c2luZXNzIC5qaXRtLWJhbm5lcl9faWNvbiwgLmppdG0tYmFubmVyLmppdG0tY2FyZC53b28taml0bSAuaml0bS1iYW5uZXJfX2ljb24ge1xuICBjb2xvcjogIzg1NURBNjtcbn1cblxuLmppdG0tYmFubmVyLmppdG0tY2FyZC5pcy11cGdyYWRlLWJ1c2luZXNzIC5qaXRtLWJhbm5lcl9faWNvbi1jaXJjbGUsIC5qaXRtLWJhbm5lci5qaXRtLWNhcmQud29vLWppdG0gLmppdG0tYmFubmVyX19pY29uLWNpcmNsZSB7XG4gIGJhY2tncm91bmQtY29sb3I6ICM4NTVEQTY7XG59XG5cbi5qaXRtLWJhbm5lci5qaXRtLWNhcmQgLmppdG0tY2FyZF9fbGluay1pbmRpY2F0b3Ige1xuICBhbGlnbi1pdGVtczogY2VudGVyO1xuICBjb2xvcjogIzAwODdiZTtcbiAgZGlzcGxheTogZmxleDtcbn1cblxuLmppdG0tYmFubmVyLmppdG0tY2FyZDpob3ZlciB7XG4gIHRyYW5zaXRpb246IGFsbCAxMDBtcyBlYXNlLWluLW91dDtcbn1cblxuLmppdG0tYmFubmVyLmppdG0tY2FyZDpob3Zlci5pcy1jYXJkLWxpbmsge1xuICBib3gtc2hhZG93OiAwIDAgMCAxcHggI2EyYTJhMiwgMCAycHggNHB4ICNkNWQ1ZDU7XG59XG5cbi5qaXRtLWJhbm5lci5qaXRtLWNhcmQ6aG92ZXIgLmppdG0tY2FyZF9fbGluay1pbmRpY2F0b3Ige1xuICBjb2xvcjogIzAwNTA4Mjtcbn1cblxuQG1lZGlhIChtaW4td2lkdGg6IDQ4MXB4KSB7XG4gIC5qaXRtLWJhbm5lci5qaXRtLWNhcmQge1xuICAgIHBhZGRpbmc6IDAuNzVyZW0gMXJlbTtcbiAgfVxuICAuaml0bS1iYW5uZXIuaml0bS1jYXJkLmlzLWRpc21pc3NpYmxlIHtcbiAgICBwYWRkaW5nLXJpZ2h0OiAxcmVtO1xuICB9XG59XG5cbi5qaXRtLWJhbm5lcl9faWNvbnMge1xuICBkaXNwbGF5OiBmbGV4O1xufVxuXG4uaml0bS1iYW5uZXJfX2ljb25zIC5qaXRtLWJhbm5lcl9faWNvbixcbi5qaXRtLWJhbm5lcl9faWNvbnMgLmppdG0tYmFubmVyX19pY29uLWNpcmNsZSB7XG4gIGJvcmRlci1yYWRpdXM6IDUwJTtcbiAgZmxleC1zaHJpbms6IDA7XG4gIGhlaWdodDogMS41cmVtO1xuICB3aWR0aDogMS41cmVtO1xuICBtYXJnaW4tcmlnaHQ6IDFyZW07XG4gIG1hcmdpbi10b3A6IC0wLjEyNXJlbTtcbiAgdGV4dC1hbGlnbjogY2VudGVyO1xuICB0b3A6IDAuMjVyZW07XG59XG5cbi5qaXRtLWJhbm5lcl9faWNvbnMgLmppdG0tYmFubmVyX19pY29uIHtcbiAgYWxpZ24tc2VsZjogY2VudGVyO1xuICBjb2xvcjogd2hpdGU7XG4gIGRpc3BsYXk6IGJsb2NrO1xufVxuXG4uaml0bS1iYW5uZXJfX2ljb25zIC5qaXRtLWJhbm5lcl9faWNvbi1jaXJjbGUge1xuICBjb2xvcjogd2hpdGU7XG4gIGRpc3BsYXk6IG5vbmU7XG4gIHBhZGRpbmc6IDAuMTg3NXJlbSAwLjI1cmVtIDAuMjVyZW0gMC4xODc1cmVtO1xufVxuXG5AbWVkaWEgKG1pbi13aWR0aDogNDgxcHgpIHtcbiAgLmppdG0tYmFubmVyX19pY29ucyB7XG4gICAgYWxpZ24taXRlbXM6IGNlbnRlcjtcbiAgfVxuICAuaml0bS1iYW5uZXJfX2ljb25zIC5qaXRtLWJhbm5lcl9faWNvbiB7XG4gICAgZGlzcGxheTogbm9uZTtcbiAgfVxuICAuaml0bS1iYW5uZXJfX2ljb25zIC5qaXRtLWJhbm5lcl9faWNvbi1jaXJjbGUge1xuICAgIGRpc3BsYXk6IGJsb2NrO1xuICB9XG59XG5cbi5qaXRtLWJhbm5lcl9faWNvbi1wbGFuIHtcbiAgZGlzcGxheTogZmxleDtcbiAgbWFyZ2luLXJpZ2h0OiAxcmVtO1xufVxuXG4uaml0bS1iYW5uZXJfX2ljb24tcGxhbiAuZG9wcy1wbGFuLWljb24ge1xuICBoZWlnaHQ6IDJyZW07XG4gIHdpZHRoOiAycmVtO1xufVxuXG4uaml0bS1iYW5uZXJfX2ljb24tcGxhbiAuanAtZW1ibGVtIHtcbiAgcG9zaXRpb246IHJlbGF0aXZlO1xuICB0b3A6IDAuMTI1cmVtO1xufVxuXG5AbWVkaWEgKG1heC13aWR0aDogNDgwcHgpIHtcbiAgLmppdG0tYmFubmVyX19pY29uLXBsYW4gLmpwLWVtYmxlbSB7XG4gICAgbWFyZ2luLWJvdHRvbTogMC43NXJlbTtcbiAgfVxufVxuXG4uaml0bS1iYW5uZXJfX2ljb24tcGxhbiAuanAtZW1ibGVtIHN2ZyB7XG4gIGhlaWdodDogMnJlbTtcbiAgd2lkdGg6IDJyZW07XG4gIGZpbGw6ICMwMEJFMjg7XG59XG5cbkBtZWRpYSAobWluLXdpZHRoOiA0ODFweCkge1xuICAuaml0bS1iYW5uZXJfX2ljb24tcGxhbiB7XG4gICAgYWxpZ24taXRlbXM6IGNlbnRlcjtcbiAgfVxufVxuXG4uaml0bS1iYW5uZXJfX2NvbnRlbnQge1xuICBhbGlnbi1pdGVtczogY2VudGVyO1xuICBkaXNwbGF5OiBmbGV4O1xuICBmbGV4LWdyb3c6IDE7XG4gIGZsZXgtd3JhcDogd3JhcDtcbn1cblxuQG1lZGlhIChtaW4td2lkdGg6IDQ4MXB4KSB7XG4gIC5qaXRtLWJhbm5lcl9fY29udGVudCB7XG4gICAgZmxleC13cmFwOiBub3dyYXA7XG4gIH1cbn1cblxuLmppdG0tYmFubmVyX19pbmZvIHtcbiAgZmxleC1ncm93OiAxO1xuICBsaW5lLWhlaWdodDogMS40O1xufVxuXG5AbWVkaWEgKG1pbi13aWR0aDogNDgxcHgpIHtcbiAgLmppdG0tYmFubmVyX19pbmZvIHtcbiAgICBmbGV4LWJhc2lzOiA1MCU7XG4gIH1cbn1cblxuQG1lZGlhIChtaW4td2lkdGg6IDk2MXB4KSB7XG4gIC5qaXRtLWJhbm5lcl9faW5mbyB7XG4gICAgZmxleC1iYXNpczogNzAlO1xuICB9XG59XG5cbi5qaXRtLWJhbm5lcl9faW5mbyAuaml0bS1iYW5uZXJfX3RpdGxlLFxuLmppdG0tYmFubmVyX19pbmZvIC5qaXRtLWJhbm5lcl9fZGVzY3JpcHRpb24ge1xuICBjb2xvcjogIzQxNDE0MTtcbn1cblxuLmppdG0tYmFubmVyX19pbmZvIC5qaXRtLWJhbm5lcl9fdGl0bGUge1xuICBmb250LXNpemU6IDE0cHg7XG4gIGZvbnQtd2VpZ2h0OiA1MDA7XG59XG5cbi5qaXRtLWJhbm5lcl9faW5mbyAuaml0bS1iYW5uZXJfX2Rlc2NyaXB0aW9uIHtcbiAgZm9udC1zaXplOiAwLjc1cmVtO1xuICBsaW5lLWhlaWdodDogMS41O1xuICBtYXJnaW4tdG9wOiAwLjM3NXJlbTtcbn1cblxuLmppdG0tYmFubmVyX19pbmZvIC5iYW5uZXJfX2xpc3Qge1xuICBmb250LXNpemU6IDEycHg7XG4gIGxpc3Qtc3R5bGU6IG5vbmU7XG4gIG1hcmdpbjogMTBweCAwO1xufVxuXG4uaml0bS1iYW5uZXJfX2luZm8gLmJhbm5lcl9fbGlzdCBsaSB7XG4gIG1hcmdpbjogNnB4IDA7XG59XG5cbi5qaXRtLWJhbm5lcl9faW5mbyAuYmFubmVyX19saXN0IGxpIC5ncmlkaWNvbiB7XG4gIGZpbGw6ICNhMmEyYTI7XG4gIGRpc3BsYXk6IGlubGluZTtcbiAgbWFyZ2luLXJpZ2h0OiAxMnB4O1xuICB2ZXJ0aWNhbC1hbGlnbjogYm90dG9tO1xufVxuXG4uaml0bS1iYW5uZXJfX2FjdGlvbiB7XG4gIGFsaWduLXNlbGY6IGNlbnRlcjtcbiAgZm9udC1zaXplOiAwLjc1cmVtO1xuICBtYXJnaW46IDAuNXJlbSAwIDA7XG4gIHRleHQtYWxpZ246IGxlZnQ7XG4gIHdpZHRoOiAxMDAlO1xufVxuXG4uaml0bS1iYW5uZXJfX2FjdGlvbiAuaml0bS1iYW5uZXJfX3ByaWNlcyB7XG4gIGRpc3BsYXk6IGZsZXg7XG4gIGp1c3RpZnktY29udGVudDogZmxleC1zdGFydDtcbn1cblxuLmppdG0tYmFubmVyX19hY3Rpb24gLmppdG0tYmFubmVyX19wcmljZXMgLmRvcHMtcGxhbi1wcmljZSB7XG4gIG1hcmdpbi1ib3R0b206IDA7XG59XG5cbi5qaXRtLWJhbm5lcl9fYWN0aW9uIC5qaXRtLWJhbm5lcl9fcHJpY2VzIC5kb3BzLXBsYW4tcHJpY2UuaXMtZGlzY291bnRlZCxcbi5qaXRtLWJhbm5lcl9fYWN0aW9uIC5qaXRtLWJhbm5lcl9fcHJpY2VzIC5kb3BzLXBsYW4tcHJpY2UuaXMtZGlzY291bnRlZCAuZG9wcy1wbGFuLXByaWNlX19jdXJyZW5jeS1zeW1ib2wge1xuICBjb2xvcjogIzQxNDE0MTtcbn1cblxuLmhhcy1jYWxsLXRvLWFjdGlvbiAuaml0bS1iYW5uZXJfX2FjdGlvbiAuaml0bS1iYW5uZXJfX3ByaWNlcyAuZG9wcy1wbGFuLXByaWNlIHtcbiAgbWFyZ2luLWJvdHRvbTogMC41cmVtO1xufVxuXG5AbWVkaWEgKG1pbi13aWR0aDogNDgxcHgpIHtcbiAgLmppdG0tYmFubmVyX19hY3Rpb24ge1xuICAgIG1hcmdpbjogMCAwLjI1cmVtIDAgMC41cmVtO1xuICAgIHRleHQtYWxpZ246IGNlbnRlcjtcbiAgICB3aWR0aDogYXV0bztcbiAgfVxuICAuaml0bS1iYW5uZXJfX2FjdGlvbiAuaXMtZGlzbWlzc2libGUge1xuICAgIG1hcmdpbi10b3A6IDIuNXJlbTtcbiAgfVxuICAuaml0bS1iYW5uZXJfX2FjdGlvbiAuaml0bS1iYW5uZXJfX3ByaWNlcyB7XG4gICAganVzdGlmeS1jb250ZW50OiBmbGV4LWVuZDtcbiAgICB0ZXh0LWFsaWduOiByaWdodDtcbiAgfVxufVxuXG4uaml0bS1iYW5uZXJfX2Rpc21pc3Mge1xuICBkaXNwbGF5OiBibG9jaztcbiAgdGV4dC1kZWNvcmF0aW9uOiBub25lO1xuICBsaW5lLWhlaWdodDogLjU7XG59XG5cbi5qaXRtLWJhbm5lcl9fZGlzbWlzczpiZWZvcmUge1xuICBjb2xvcjogIzZmNmY2ZjtcbiAgZm9udDogNDAwIDE2cHgvMSBkYXNoaWNvbnM7XG4gIGNvbnRlbnQ6ICdcXGYxNTgnO1xufVxuXG5AbWVkaWEgKG1pbi13aWR0aDogNjYxcHgpIHtcbiAgLmppdG0tYmFubmVyX19kaXNtaXNzIHtcbiAgICBtYXJnaW4tcmlnaHQ6IC0wLjVyZW07XG4gIH1cbn1cblxuQG1lZGlhIChtYXgtd2lkdGg6IDQ4MHB4KSB7XG4gIC5qaXRtLWJhbm5lcl9fZGlzbWlzcyB7XG4gICAgcG9zaXRpb246IGFic29sdXRlO1xuICAgIHRvcDogMC44NzVyZW07XG4gICAgcmlnaHQ6IDAuODc1cmVtO1xuICB9XG59XG5cbi5qaXRtLWJhbm5lcl9fYWN0aW9uICsgLmppdG0tYmFubmVyX19kaXNtaXNzIHtcbiAgbWFyZ2luLWxlZnQ6IDAuNjI1cmVtO1xufVxuXG4jZG9sbHkgKyAuaml0bS1jYXJkIHtcbiAgbWFyZ2luOiAzcmVtIDFyZW0gMCBhdXRvO1xufVxuIl0sImZpbGUiOiJwYWNrYWdlcy9qaXRtL2Fzc2V0cy9qZXRwYWNrLWFkbWluLWppdG0uY3NzIn0= */ diff --git a/plugins/jetpack/vendor/automattic/jetpack-jitm/assets/jetpack-admin-jitm.min.css b/plugins/jetpack/vendor/automattic/jetpack-jitm/assets/jetpack-admin-jitm.min.css new file mode 100644 index 00000000..109c1c49 --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-jitm/assets/jetpack-admin-jitm.min.css @@ -0,0 +1,3 @@ +@charset "UTF-8";/*! +* Do not modify this file directly. It is compiled SASS code. +*/.jitm-button{background:#f3f5f6;border-color:#0071a1;border-style:solid;border-width:1px;color:#0071a1;cursor:pointer;display:inline-block;margin:0;outline:0;overflow:hidden;font-size:14px;text-overflow:ellipsis;text-decoration:none;vertical-align:top;box-sizing:border-box;font-size:14px;border-radius:4px;padding:7px 14px 9px;-webkit-appearance:none;-moz-appearance:none;appearance:none}.jitm-button:hover{background:#f1f1f1;border-color:#016087;color:#016087}.jitm-button:disabled,.jitm-button[disabled]{color:#eee;background:#fff;border-color:#eee;cursor:default}.jitm-button:focus{background:#fff;border-color:#0071a1;box-shadow:0 0 0 1px #0071a1}.jitm-button.is-compact{padding:7px;font-size:11px;line-height:1;text-transform:uppercase}.jitm-button.is-compact:disabled{color:#eee}.jitm-button.is-compact .gridicon{top:4px;margin-top:-8px}.jitm-button.is-compact .gridicons-plus-small{margin-left:-4px}.jitm-button.is-compact .gridicons-plus-small:last-of-type{margin-left:0}.jitm-button.is-compact .gridicons-plus-small+.gridicon{margin-left:-4px}.jitm-button.hidden{display:none}.jitm-button.is-primary{background:#007cba;border-color:#007cba;color:#fff}.jitm-button.is-primary:focus,.jitm-button.is-primary:hover{border-color:#0071a1;background:#0071a1;color:#fff}.jitm-button.is-primary:focus{box-shadow:0 0 0 1px #fff,0 0 0 3px #0071a1}.jitm-button.is-primary:disabled,.jitm-button.is-primary[disabled]{background:#bceefd;border-color:#8cc9e2;color:#fff}.jitm-button.is-primary.is-compact{color:#fff;white-space:nowrap}.jitm-card{display:block;clear:both;position:relative;margin:3rem 1.25rem 0 auto;padding:1rem;box-sizing:border-box;background:#fff;box-shadow:0 0 0 1px #ccd0d4,0 1px 1px 1px rgba(0,0,0,.04)}.jitm-card:after{content:".";display:block;height:0;clear:both;visibility:hidden}@media (min-width:481px){.jitm-card{margin-bottom:1rem;padding:1.5rem}}.jitm-card.is-compact{margin-bottom:.0625rem}@media (min-width:481px){.jitm-card.is-compact{margin-bottom:1px;padding:1rem 1.5rem}}.jitm-card.is-card-link{padding-right:3rem}#screen-meta-links+.jitm-card{margin:2.5rem 1.5385em 0 auto}#dolly+.jitm-card{margin:3rem 1rem 0 auto}.post-php .jitm-card{margin-right:0}.jp-lower .jitm-card{margin:0 0 1.5rem}.jitm-banner.jitm-card{border-left:4px solid;display:flex;padding:.75rem .375rem .75rem .75rem;position:relative;z-index:2;border-left-color:#4ab866}@media (max-width:480px){.jitm-banner.jitm-card{display:block}}.jitm-banner.jitm-card.is-card-link{padding:.75rem 3rem .75rem 1rem}.jitm-banner.jitm-card.is-dismissible{padding-right:3rem}.jitm-banner.jitm-card .jitm-banner__icon{color:#4ab866}.jitm-banner.jitm-card .jitm-banner__icon-circle{background-color:#4ab866}.jitm-banner.jitm-card.is-upgrade-personal{border-left-color:#f0b849}.jitm-banner.jitm-card.is-upgrade-personal .jitm-banner__icon{color:#f0b849}.jitm-banner.jitm-card.is-upgrade-personal .jitm-banner__icon-circle{background-color:#f0b849}.jitm-banner.jitm-card.is-upgrade-premium{border-left-color:#4ab866}.jitm-banner.jitm-card.is-upgrade-premium .jitm-banner__icon{color:#4ab866}.jitm-banner.jitm-card.is-upgrade-premium .jitm-banner__icon-circle{background-color:#4ab866}.jitm-banner.jitm-card.is-upgrade-business,.jitm-banner.jitm-card.woo-jitm{border-left-color:#855da6}.jitm-banner.jitm-card.is-upgrade-business .jitm-banner__icon,.jitm-banner.jitm-card.woo-jitm .jitm-banner__icon{color:#855da6}.jitm-banner.jitm-card.is-upgrade-business .jitm-banner__icon-circle,.jitm-banner.jitm-card.woo-jitm .jitm-banner__icon-circle{background-color:#855da6}.jitm-banner.jitm-card .jitm-card__link-indicator{align-items:center;color:#0087be;display:flex}.jitm-banner.jitm-card:hover{transition:all .1s ease-in-out}.jitm-banner.jitm-card:hover.is-card-link{box-shadow:0 0 0 1px #a2a2a2,0 2px 4px #d5d5d5}.jitm-banner.jitm-card:hover .jitm-card__link-indicator{color:#005082}@media (min-width:481px){.jitm-banner.jitm-card{padding:.75rem 1rem}.jitm-banner.jitm-card.is-dismissible{padding-right:1rem}}.jitm-banner__icons{display:flex}.jitm-banner__icons .jitm-banner__icon,.jitm-banner__icons .jitm-banner__icon-circle{border-radius:50%;flex-shrink:0;height:1.5rem;width:1.5rem;margin-right:1rem;margin-top:-.125rem;text-align:center;top:.25rem}.jitm-banner__icons .jitm-banner__icon{align-self:center;color:#fff;display:block}.jitm-banner__icons .jitm-banner__icon-circle{color:#fff;display:none;padding:.1875rem .25rem .25rem .1875rem}@media (min-width:481px){.jitm-banner__icons{align-items:center}.jitm-banner__icons .jitm-banner__icon{display:none}.jitm-banner__icons .jitm-banner__icon-circle{display:block}}.jitm-banner__icon-plan{display:flex;margin-right:1rem}.jitm-banner__icon-plan .dops-plan-icon{height:2rem;width:2rem}.jitm-banner__icon-plan .jp-emblem{position:relative;top:.125rem}@media (max-width:480px){.jitm-banner__icon-plan .jp-emblem{margin-bottom:.75rem}}.jitm-banner__icon-plan .jp-emblem svg{height:2rem;width:2rem;fill:#00be28}@media (min-width:481px){.jitm-banner__icon-plan{align-items:center}}.jitm-banner__content{align-items:center;display:flex;flex-grow:1;flex-wrap:wrap}@media (min-width:481px){.jitm-banner__content{flex-wrap:nowrap}}.jitm-banner__info{flex-grow:1;line-height:1.4}@media (min-width:481px){.jitm-banner__info{flex-basis:50%}}@media (min-width:961px){.jitm-banner__info{flex-basis:70%}}.jitm-banner__info .jitm-banner__description,.jitm-banner__info .jitm-banner__title{color:#414141}.jitm-banner__info .jitm-banner__title{font-size:14px;font-weight:500}.jitm-banner__info .jitm-banner__description{font-size:.75rem;line-height:1.5;margin-top:.375rem}.jitm-banner__info .banner__list{font-size:12px;list-style:none;margin:10px 0}.jitm-banner__info .banner__list li{margin:6px 0}.jitm-banner__info .banner__list li .gridicon{fill:#a2a2a2;display:inline;margin-right:12px;vertical-align:bottom}.jitm-banner__action{align-self:center;font-size:.75rem;margin:.5rem 0 0;text-align:left;width:100%}.jitm-banner__action .jitm-banner__prices{display:flex;justify-content:flex-start}.jitm-banner__action .jitm-banner__prices .dops-plan-price{margin-bottom:0}.jitm-banner__action .jitm-banner__prices .dops-plan-price.is-discounted,.jitm-banner__action .jitm-banner__prices .dops-plan-price.is-discounted .dops-plan-price__currency-symbol{color:#414141}.has-call-to-action .jitm-banner__action .jitm-banner__prices .dops-plan-price{margin-bottom:.5rem}@media (min-width:481px){.jitm-banner__action{margin:0 .25rem 0 .5rem;text-align:center;width:auto}.jitm-banner__action .is-dismissible{margin-top:2.5rem}.jitm-banner__action .jitm-banner__prices{justify-content:flex-end;text-align:right}}.jitm-banner__dismiss{display:block;text-decoration:none;line-height:.5}.jitm-banner__dismiss:before{color:#6f6f6f;font:400 16px/1 dashicons;content:'\f158'}@media (min-width:661px){.jitm-banner__dismiss{margin-right:-.5rem}}@media (max-width:480px){.jitm-banner__dismiss{position:absolute;top:.875rem;right:.875rem}}.jitm-banner__action+.jitm-banner__dismiss{margin-left:.625rem}#dolly+.jitm-card{margin:3rem 1rem 0 auto}
\ No newline at end of file diff --git a/plugins/jetpack/vendor/automattic/jetpack-jitm/src/class-jitm.php b/plugins/jetpack/vendor/automattic/jetpack-jitm/src/class-jitm.php new file mode 100644 index 00000000..ccdd1d72 --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-jitm/src/class-jitm.php @@ -0,0 +1,637 @@ +<?php +/** + * Jetpack's JITM class. + * + * @package automattic/jetpack-jitm + */ + +namespace Automattic\Jetpack; + +use Automattic\Jetpack\Assets; +use Automattic\Jetpack\Connection\Manager as Jetpack_Connection; +use Automattic\Jetpack\Connection\Client; +use Automattic\Jetpack\Assets\Logo as Jetpack_Logo; +use Automattic\Jetpack\Tracking; +use Automattic\Jetpack\Connection\Manager; + +/** + * Jetpack just in time messaging through out the admin + * + * @since 5.6.0 + */ +class JITM { + + const PACKAGE_VERSION = '1.0'; // TODO: Keep in sync with version specified in composer.json. + + /** + * Tracking object. + * + * @var Automattic\Jetpack\Tracking + * + * @access private + */ + private $tracking; + + /** + * JITM constructor. + */ + public function __construct() { + $this->tracking = new Tracking(); + } + + /** + * Determines if JITMs are enabled. + * + * @return bool Enable JITMs. + */ + public function register() { + /** + * Filter to turn off all just in time messages + * + * @since 3.7.0 + * @since 5.4.0 Correct docblock to reflect default arg value + * + * @param bool false Whether to show just in time messages. + */ + if ( ! apply_filters( 'jetpack_just_in_time_msgs', false ) ) { + return false; + } + add_action( 'current_screen', array( $this, 'prepare_jitms' ) ); + return true; + } + + /** + * Prepare actions according to screen and post type. + * + * @since 3.8.2 + * + * @uses Jetpack_Autoupdate::get_possible_failures() + * + * @param \WP_Screen $screen WP Core's screen object. + */ + public function prepare_jitms( $screen ) { + if ( ! in_array( + $screen->id, + array( + 'jetpack_page_stats', + 'jetpack_page_akismet-key-config', + 'admin_page_jetpack_modules', + ), + true + ) ) { + add_action( 'admin_enqueue_scripts', array( $this, 'jitm_enqueue_files' ) ); + add_action( 'admin_notices', array( $this, 'ajax_message' ) ); + add_action( 'edit_form_top', array( $this, 'ajax_message' ) ); + + // Not really a JITM. Don't know where else to put this :) . + add_action( 'admin_notices', array( $this, 'delete_user_update_connection_owner_notice' ) ); + } + } + + /** + * A special filter for WooCommerce, to set a message based on local state. + * + * @param string $content The current message. + * + * @return array The new message. + */ + public static function jitm_woocommerce_services_msg( $content ) { + if ( ! function_exists( 'wc_get_base_location' ) ) { + return $content; + } + + $base_location = wc_get_base_location(); + + switch ( $base_location['country'] ) { + case 'US': + $content->message = esc_html__( 'New free service: Show USPS shipping rates on your store! Added bonus: print shipping labels without leaving WooCommerce.', 'jetpack' ); + break; + case 'CA': + $content->message = esc_html__( 'New free service: Show Canada Post shipping rates on your store!', 'jetpack' ); + break; + default: + $content->message = ''; + } + + return $content; + } + + /** + * A special filter for WooCommerce Call To Action button + * + * @return string The new CTA + */ + public static function jitm_jetpack_woo_services_install() { + return wp_nonce_url( + add_query_arg( + array( + 'wc-services-action' => 'install', + ), + admin_url( 'admin.php?page=wc-settings' ) + ), + 'wc-services-install' + ); + } + + /** + * A special filter for WooCommerce Call To Action button. + * + * @return string The new CTA + */ + public static function jitm_jetpack_woo_services_activate() { + return wp_nonce_url( + add_query_arg( + array( + 'wc-services-action' => 'activate', + ), + admin_url( 'admin.php?page=wc-settings' ) + ), + 'wc-services-install' + ); + } + + /** + * 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() { + global $current_screen; + + /* + * 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( $current_screen->base ) || 'users' !== $current_screen->base ) { + return; + } + + if ( ! isset( $_REQUEST['action'] ) || 'delete' !== $_REQUEST['action'] ) { + return; + } + + // 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; + } + + // Track it! + if ( method_exists( $this->tracking, 'record_user_event' ) ) { + $this->tracking->record_user_event( 'delete_connection_owner_notice_view' ); + } + + $connection_manager = new Manager(); + $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' ) . '</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' ), + 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' ) . ' </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' ), 'primary', 'jp-switch-connection-owner-submit', false ); + echo '</p>'; + + echo "<div id='jp-switch-user-results'></div>"; + echo '</form>'; + ?> + <script type="text/javascript"> + jQuery( document ).ready( function( $ ) { + $( '#jp-switch-connection-owner' ).on( 'submit', function( e ) { + var formData = $( this ).serialize(); + var submitBtn = document.getElementById( 'jp-switch-connection-owner-submit' ); + var results = document.getElementById( 'jp-switch-user-results' ); + + submitBtn.disabled = true; + + $.ajax( { + type : "POST", + url : "<?php echo esc_url( get_rest_url() . 'jetpack/v4/connection/owner' ); ?>", + data : formData, + headers : { + 'X-WP-Nonce': "<?php echo esc_js( wp_create_nonce( 'wp_rest' ) ); ?>", + }, + success: function() { + results.innerHTML = "<?php esc_html_e( 'Success!', 'jetpack' ); ?>"; + setTimeout( function() { + $( '#jetpack-notice-switch-connection-owner' ).hide( 'slow' ); + }, 1000 ); + } + } ).done( function() { + submitBtn.disabled = false; + } ); + + e.preventDefault(); + return false; + } ); + } ); + </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' ) . '</p>'; + $connect_url = \Jetpack::init()->build_connect_url( false, false, 'delete_connection_owner_notice' ); + echo "<a href='" . esc_url( $connect_url ) . "' target='_blank' rel='noopener noreferrer' class='button-primary'>" . esc_html__( 'Connect to WordPress.com', 'jetpack' ) . '</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' ), + array( + 'a' => array( + 'href' => true, + 'target' => true, + 'rel' => true, + ), + ) + ), + 'https://jetpack.com/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' ), + array( + 'a' => array( + 'href' => true, + 'target' => true, + 'rel' => true, + ), + ) + ), + 'https://jetpack.com/contact-support' + ); + echo '</p>'; + echo '</div>'; + } + + /** + * Injects the dom to show a JITM inside of wp-admin. + */ + public function ajax_message() { + if ( ! is_admin() ) { + return; + } + + // do not display on Gutenberg pages. + if ( $this->is_gutenberg_page() ) { + return; + } + + $message_path = $this->get_message_path(); + $query_string = _http_build_query( $_GET, '', ',' ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $current_screen = wp_unslash( $_SERVER['REQUEST_URI'] ); + ?> + <div class="jetpack-jitm-message" + data-nonce="<?php echo esc_attr( wp_create_nonce( 'wp_rest' ) ); ?>" + data-message-path="<?php echo esc_attr( $message_path ); ?>" + data-query="<?php echo urlencode_deep( $query_string ); ?>" + data-redirect="<?php echo urlencode_deep( $current_screen ); ?>" + ></div> + <?php + } + + /** + * Get's the current message path for display of a JITM + * + * @return string The message path + */ + public function get_message_path() { + $screen = get_current_screen(); + + return 'wp:' . $screen->id . ':' . current_filter(); + } + + /** + * Function to enqueue jitm css and js + */ + public function jitm_enqueue_files() { + if ( $this->is_gutenberg_page() ) { + return; + } + $min = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min'; + wp_register_style( + 'jetpack-jitm-css', + plugins_url( "assets/jetpack-admin-jitm{$min}.css", __DIR__ ), + false, + self::PACKAGE_VERSION . + '-201243242' + ); + wp_style_add_data( 'jetpack-jitm-css', 'rtl', 'replace' ); + wp_style_add_data( 'jetpack-jitm-css', 'suffix', $min ); + wp_enqueue_style( 'jetpack-jitm-css' ); + + wp_enqueue_script( + 'jetpack-jitm-new', + Assets::get_file_url_for_environment( '_inc/build/jetpack-jitm.min.js', '_inc/jetpack-jitm.js' ), + array( 'jquery' ), + self::PACKAGE_VERSION, + true + ); + wp_localize_script( + 'jetpack-jitm-new', + 'jitm_config', + array( + 'api_root' => esc_url_raw( rest_url() ), + 'activate_module_text' => esc_html__( 'Activate', 'jetpack' ), + 'activated_module_text' => esc_html__( 'Activated', 'jetpack' ), + 'activating_module_text' => esc_html__( 'Activating', 'jetpack' ), + ) + ); + } + + /** + * Dismisses a JITM feature class so that it will no longer be shown. + * + * @param string $id The id of the JITM that was dismissed. + * @param string $feature_class The feature class of the JITM that was dismissed. + * + * @return bool Always true. + */ + public function dismiss( $id, $feature_class ) { + $this->tracking->record_user_event( + 'jitm_dismiss_client', + array( + 'jitm_id' => $id, + 'feature_class' => $feature_class, + ) + ); + + $hide_jitm = \Jetpack_Options::get_option( 'hide_jitm' ); + if ( ! is_array( $hide_jitm ) ) { + $hide_jitm = array(); + } + + if ( isset( $hide_jitm[ $feature_class ] ) ) { + if ( ! is_array( $hide_jitm[ $feature_class ] ) ) { + $hide_jitm[ $feature_class ] = array( + 'last_dismissal' => 0, + 'number' => 0, + ); + } + } else { + $hide_jitm[ $feature_class ] = array( + 'last_dismissal' => 0, + 'number' => 0, + ); + } + + $number = $hide_jitm[ $feature_class ]['number']; + + $hide_jitm[ $feature_class ] = array( + 'last_dismissal' => time(), + 'number' => $number + 1, + ); + + \Jetpack_Options::update_option( 'hide_jitm', $hide_jitm ); + + return true; + } + + /** + * Asks the wpcom API for the current message to display keyed on query string and message path + * + * @param string $message_path The message path to ask for. + * @param string $query The query string originally from the front end. + * + * @return array The JITM's to show, or an empty array if there is nothing to show + */ + public function get_messages( $message_path, $query ) { + // Custom filters go here. + add_filter( 'jitm_woocommerce_services_msg', array( $this, 'jitm_woocommerce_services_msg' ) ); + add_filter( 'jitm_jetpack_woo_services_install', array( $this, 'jitm_jetpack_woo_services_install' ) ); + add_filter( 'jitm_jetpack_woo_services_activate', array( $this, 'jitm_jetpack_woo_services_activate' ) ); + + $user = wp_get_current_user(); + + // Unauthenticated or invalid requests just bail. + if ( ! $user ) { + return array(); + } + + $user_roles = implode( ',', $user->roles ); + $site_id = \Jetpack_Options::get_option( 'id' ); + + // Build our jitm request. + $path = add_query_arg( + array( + 'external_user_id' => urlencode_deep( $user->ID ), + 'user_roles' => urlencode_deep( $user_roles ), + 'query_string' => urlencode_deep( $query ), + 'mobile_browser' => jetpack_is_mobile( 'smart' ) ? 1 : 0, + '_locale' => get_user_locale(), + ), + sprintf( '/sites/%d/jitm/%s', $site_id, $message_path ) + ); + + // Attempt to get from cache. + $envelopes = get_transient( 'jetpack_jitm_' . substr( md5( $path ), 0, 31 ) ); + + // If something is in the cache and it was put in the cache after the last sync we care about, use it. + $use_cache = false; + + /** This filter is documented in class.jetpack.php */ + if ( apply_filters( 'jetpack_just_in_time_msg_cache', false ) ) { + $use_cache = true; + } + + if ( $use_cache ) { + $last_sync = (int) get_transient( 'jetpack_last_plugin_sync' ); + $from_cache = $envelopes && $last_sync > 0 && $last_sync < $envelopes['last_response_time']; + } else { + $from_cache = false; + } + + // Otherwise, ask again. + if ( ! $from_cache ) { + $wpcom_response = Client::wpcom_json_api_request_as_blog( + $path, + '2', + array( + 'user_id' => $user->ID, + 'user_roles' => implode( ',', $user->roles ), + ), + null, + 'wpcom' + ); + + // silently fail...might be helpful to track it? + if ( is_wp_error( $wpcom_response ) ) { + return array(); + } + + $envelopes = json_decode( $wpcom_response['body'] ); + + if ( ! is_array( $envelopes ) ) { + return array(); + } + + $expiration = isset( $envelopes[0] ) ? $envelopes[0]->ttl : 300; + + // Do not cache if expiration is 0 or we're not using the cache. + if ( 0 !== $expiration && $use_cache ) { + $envelopes['last_response_time'] = time(); + + set_transient( 'jetpack_jitm_' . substr( md5( $path ), 0, 31 ), $envelopes, $expiration ); + } + } + + $hidden_jitms = \Jetpack_Options::get_option( 'hide_jitm' ); + unset( $envelopes['last_response_time'] ); + + /** + * Allow adding your own custom JITMs after a set of JITMs has been received. + * + * @since 6.9.0 + * + * @param array $envelopes array of existing JITMs. + */ + $envelopes = apply_filters( 'jetpack_jitm_received_envelopes', $envelopes ); + + foreach ( $envelopes as $idx => &$envelope ) { + + $dismissed_feature = isset( $hidden_jitms[ $envelope->feature_class ] ) && is_array( $hidden_jitms[ $envelope->feature_class ] ) ? $hidden_jitms[ $envelope->feature_class ] : null; + + // If the this feature class has been dismissed and the request has not passed the ttl, skip it as it's been dismissed. + if ( is_array( $dismissed_feature ) && ( time() - $dismissed_feature['last_dismissal'] < $envelope->expires || $dismissed_feature['number'] >= $envelope->max_dismissal ) ) { + unset( $envelopes[ $idx ] ); + continue; + } + + $this->tracking->record_user_event( + 'jitm_view_client', + array( + 'jitm_id' => $envelope->id, + ) + ); + + $normalized_site_url = \Jetpack::build_raw_urls( get_home_url() ); + + $url_params = array( + 'source' => "jitm-$envelope->id", + 'site' => $normalized_site_url, + 'u' => $user->ID, + ); + + if ( ! class_exists( 'Jetpack_Affiliate' ) ) { + require_once JETPACK__PLUGIN_DIR . 'class.jetpack-affiliate.php'; + } + // Get affiliate code and add it to the array of URL parameters. + $aff = \Jetpack_Affiliate::init()->get_affiliate_code(); + if ( '' !== $aff ) { + $url_params['aff'] = $aff; + } + + $envelope->url = add_query_arg( $url_params, 'https://jetpack.com/redirect/' ); + + $envelope->jitm_stats_url = \Jetpack::build_stats_url( array( 'x_jetpack-jitm' => $envelope->id ) ); + + // phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + // $CTA is not valid per PHPCS, but it is part of the return from WordPress.com, so allowing. + if ( $envelope->CTA->hook ) { + $envelope->url = apply_filters( 'jitm_' . $envelope->CTA->hook, $envelope->url ); + unset( $envelope->CTA->hook ); + } + // phpcs:enable + + if ( isset( $envelope->content->hook ) ) { + $envelope->content = apply_filters( 'jitm_' . $envelope->content->hook, $envelope->content ); + unset( $envelope->content->hook ); + } + + // No point in showing an empty message. + if ( empty( $envelope->content->message ) ) { + unset( $envelopes[ $idx ] ); + continue; + } + + switch ( $envelope->content->icon ) { + case 'jetpack': + $jetpack_logo = new Jetpack_Logo(); + $envelope->content->icon = '<div class="jp-emblem">' . $jetpack_logo->get_jp_emblem() . '</div>'; + break; + case 'woocommerce': + $envelope->content->icon = '<div class="jp-emblem"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Layer_1" x="0px" y="0px" viewBox="0 0 168 100" xml:space="preserve" enable-background="new 0 0 168 100" width="50" height="30"><style type="text/css"> + .st0{clip-path:url(#SVGID_2_);enable-background:new ;} + .st1{clip-path:url(#SVGID_4_);} + .st2{clip-path:url(#SVGID_6_);} + .st3{clip-path:url(#SVGID_8_);fill:#8F567F;} + .st4{clip-path:url(#SVGID_10_);fill:#FFFFFE;} + .st5{clip-path:url(#SVGID_12_);fill:#FFFFFE;} + .st6{clip-path:url(#SVGID_14_);fill:#FFFFFE;} + </style><g><defs><polygon id="SVGID_1_" points="83.8 100 0 100 0 0.3 83.8 0.3 167.6 0.3 167.6 100 "/></defs><clipPath id="SVGID_2_"><use xlink:href="#SVGID_1_" overflow="visible"/></clipPath><g class="st0"><g><defs><rect id="SVGID_3_" width="168" height="100"/></defs><clipPath id="SVGID_4_"><use xlink:href="#SVGID_3_" overflow="visible"/></clipPath><g class="st1"><defs><path id="SVGID_5_" d="M15.6 0.3H152c8.6 0 15.6 7 15.6 15.6v52c0 8.6-7 15.6-15.6 15.6h-48.9l6.7 16.4L80.2 83.6H15.6C7 83.6 0 76.6 0 67.9v-52C0 7.3 7 0.3 15.6 0.3"/></defs><clipPath id="SVGID_6_"><use xlink:href="#SVGID_5_" overflow="visible"/></clipPath><g class="st2"><defs><rect id="SVGID_7_" width="168" height="100"/></defs><clipPath id="SVGID_8_"><use xlink:href="#SVGID_7_" overflow="visible"/></clipPath><rect x="-10" y="-9.7" class="st3" width="187.6" height="119.7"/></g></g></g></g></g><g><defs><path id="SVGID_9_" d="M8.4 14.5c1-1.3 2.4-2 4.3-2.1 3.5-0.2 5.5 1.4 6 4.9 2.1 14.3 4.4 26.4 6.9 36.4l15-28.6c1.4-2.6 3.1-3.9 5.2-4.1 3-0.2 4.9 1.7 5.6 5.7 1.7 9.1 3.9 16.9 6.5 23.4 1.8-17.4 4.8-30 9-37.7 1-1.9 2.5-2.9 4.5-3 1.6-0.1 3 0.3 4.3 1.4 1.3 1 2 2.3 2.1 3.9 0.1 1.2-0.1 2.3-0.7 3.3 -2.7 5-4.9 13.2-6.6 24.7 -1.7 11.1-2.3 19.8-1.9 26.1 0.1 1.7-0.1 3.2-0.8 4.5 -0.8 1.5-2 2.4-3.7 2.5 -1.8 0.1-3.6-0.7-5.4-2.5C52.4 66.7 47.4 57 43.7 44.1c-4.4 8.8-7.7 15.3-9.9 19.7 -4 7.7-7.5 11.7-10.3 11.9 -1.9 0.1-3.5-1.4-4.8-4.7 -3.5-9-7.3-26.3-11.3-52C7.1 17.3 7.5 15.8 8.4 14.5"/></defs><clipPath id="SVGID_10_"><use xlink:href="#SVGID_9_" overflow="visible"/></clipPath><rect x="-2.7" y="-0.6" class="st4" width="90.6" height="86.4"/></g><g><defs><path id="SVGID_11_" d="M155.6 25.2c-2.5-4.3-6.1-6.9-11-7.9 -1.3-0.3-2.5-0.4-3.7-0.4 -6.6 0-11.9 3.4-16.1 10.2 -3.6 5.8-5.3 12.3-5.3 19.3 0 5.3 1.1 9.8 3.3 13.6 2.5 4.3 6.1 6.9 11 7.9 1.3 0.3 2.5 0.4 3.7 0.4 6.6 0 12-3.4 16.1-10.2 3.6-5.9 5.3-12.4 5.3-19.4C159 33.4 157.9 28.9 155.6 25.2zM147 44.2c-0.9 4.5-2.7 7.9-5.2 10.1 -2 1.8-3.9 2.5-5.5 2.2 -1.7-0.3-3-1.8-4-4.4 -0.8-2.1-1.2-4.2-1.2-6.2 0-1.7 0.2-3.4 0.5-5 0.6-2.8 1.8-5.5 3.6-8.1 2.3-3.3 4.7-4.8 7.1-4.2 1.7 0.3 3 1.8 4 4.4 0.8 2.1 1.2 4.2 1.2 6.2C147.5 40.9 147.3 42.6 147 44.2z"/></defs><clipPath id="SVGID_12_"><use xlink:href="#SVGID_11_" overflow="visible"/></clipPath><rect x="109.6" y="6.9" class="st5" width="59.4" height="71.4"/></g><g><defs><path id="SVGID_13_" d="M112.7 25.2c-2.5-4.3-6.1-6.9-11-7.9 -1.3-0.3-2.5-0.4-3.7-0.4 -6.6 0-11.9 3.4-16.1 10.2 -3.5 5.8-5.3 12.3-5.3 19.3 0 5.3 1.1 9.8 3.3 13.6 2.5 4.3 6.1 6.9 11 7.9 1.3 0.3 2.5 0.4 3.7 0.4 6.6 0 12-3.4 16.1-10.2 3.5-5.9 5.3-12.4 5.3-19.4C116 33.4 114.9 28.9 112.7 25.2zM104.1 44.2c-0.9 4.5-2.7 7.9-5.2 10.1 -2 1.8-3.9 2.5-5.5 2.2 -1.7-0.3-3-1.8-4-4.4 -0.8-2.1-1.2-4.2-1.2-6.2 0-1.7 0.2-3.4 0.5-5 0.6-2.8 1.8-5.5 3.6-8.1 2.3-3.3 4.7-4.8 7.1-4.2 1.7 0.3 3 1.8 4 4.4 0.8 2.1 1.2 4.2 1.2 6.2C104.6 40.9 104.4 42.6 104.1 44.2z"/></defs><clipPath id="SVGID_14_"><use xlink:href="#SVGID_13_" overflow="visible"/></clipPath><rect x="66.7" y="6.9" class="st6" width="59.4" height="71.4"/></g></svg></div>'; + break; + default: + $envelope->content->icon = ''; + break; + } + + $jetpack = \Jetpack::init(); + $jetpack->stat( 'jitm', $envelope->id . '-viewed-' . JETPACK__VERSION ); + $jetpack->do_stats( 'server_side' ); + } + + return $envelopes; + } + + /** + * Is the current page a block editor page? + * + * @since 8.0.0 + */ + private function is_gutenberg_page() { + $current_screen = get_current_screen(); + return ( method_exists( $current_screen, 'is_block_editor' ) && $current_screen->is_block_editor() ); + } +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-logo/src/class-logo.php b/plugins/jetpack/vendor/automattic/jetpack-logo/src/class-logo.php new file mode 100644 index 00000000..ded465dd --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-logo/src/class-logo.php @@ -0,0 +1,71 @@ +<?php +/** + * A logo for Jetpack. + * + * @package automattic/jetpack-logo + */ + +namespace Automattic\Jetpack\Assets; + +/** + * Jetpack logo as SVG shapes. + * + * Initializes the logo property with a string describing the Jetpack logo. + * The Jetpack logo SVG string includes CSS classes to stylize it: + * - jetpack-logo: the wrapper <svg> tag. + * - jetpack-logo__icon-circle: the circle of the Jetpack mark. + * - jetpack-logo__icon-triangle: two shapes that correspond to each triangle in the Jetpack mark. + * - jetpack-logo__icon-text: the Jetpack lettering. + * + * @var string + */ +const JETPACK_LOGO_SVG = <<<'EOSVG' +<svg xmlns="http://www.w3.org/2000/svg" height="32" class="jetpack-logo" viewBox="0 0 118 32"> + <path class="jetpack-logo__icon-circle" d="M16,0C7.2,0,0,7.2,0,16s7.2,16,16,16s16-7.2,16-16S24.8,0,16,0z" fill="#00be28" /> + <polygon class="jetpack-logo__icon-triangle" points="15,19 7,19 15,3" fill="#fff" /> + <polygon class="jetpack-logo__icon-triangle" points="17,29 17,13 25,13" fill="#fff" /> + <path class="jetpack-logo__text" d="M41.3 26.6c-.5-.7-.9-1.4-1.3-2.1 2.3-1.4 3-2.5 3-4.6V8h-3V6h6v13.4C46 22.8 45 24.8 41.3 26.6zM58.5 21.3c-1.5.5-2.7.6-4.2.6-3.6 0-5.8-1.8-5.8-6 0-3.1 1.9-5.9 5.5-5.9s4.9 2.5 4.9 4.9c0 .8 0 1.5-.1 2h-7.3c.1 2.5 1.5 2.8 3.6 2.8 1.1 0 2.2-.3 3.4-.7C58.5 19 58.5 21.3 58.5 21.3zM56 15c0-1.4-.5-2.9-2-2.9-1.4 0-2.3 1.3-2.4 2.9C51.6 15 56 15 56 15zM65 18.4c0 1.1.8 1.3 1.4 1.3.5 0 2-.2 2.6-.4v2.1c-.9.3-2.5.5-3.7.5-1.5 0-3.2-.5-3.2-3.1V12H60v-2h2.1V7.1H65V10h4v2h-4V18.4zM71 10h3v1.3c1.1-.8 1.9-1.3 3.3-1.3 2.5 0 4.5 1.8 4.5 5.6s-2.2 6.3-5.8 6.3c-.9 0-1.3-.1-2-.3V28h-3V10zM76.5 12.3c-.8 0-1.6.4-2.5 1.2v5.9c.6.1.9.2 1.8.2 2 0 3.2-1.3 3.2-3.9C79 13.4 78.1 12.3 76.5 12.3zM93 22h-3v-1.5c-.9.7-1.9 1.5-3.5 1.5-1.5 0-3.1-1.1-3.1-3.2 0-2.9 2.5-3.4 4.2-3.7l2.4-.3v-.3c0-1.5-.5-2.3-2-2.3-.7 0-2.3.5-3.7 1.1L84 11c1.2-.4 3-1 4.4-1 2.7 0 4.6 1.4 4.6 4.7L93 22zM90 16.4l-2.2.4c-.7.1-1.4.5-1.4 1.6 0 .9.5 1.4 1.3 1.4s1.5-.5 2.3-1V16.4zM104.5 21.3c-1.1.4-2.2.6-3.5.6-4.2 0-5.9-2.4-5.9-5.9 0-3.7 2.3-6 6.1-6 1.4 0 2.3.2 3.2.5V13c-.8-.3-2-.6-3.2-.6-1.7 0-3.2.9-3.2 3.6 0 2.9 1.5 3.8 3.3 3.8.9 0 1.9-.2 3.2-.7V21.3zM110 15.2c.2-.3.2-.8 3.8-5.2h3.7l-4.6 5.7 5 6.3h-3.7l-4.2-5.8V22h-3V6h3V15.2z" /> +</svg> +EOSVG; + +/** + * Create and render a Jetpack logo. + */ +class Logo { + + /** + * Return the Jetpack logo. + * + * @return string The Jetpack logo. + */ + public function render() { + return JETPACK_LOGO_SVG; + } + + /** + * Return string containing the Jetpack logo. + * + * @since 7.5.0 + * + * @param bool $logotype Should we use the full logotype (logo + text). Default to false. + * + * @return string + */ + public function get_jp_emblem( $logotype = false ) { + $logo = '<path fill="#00BE28" d="M16,0C7.2,0,0,7.2,0,16s7.2,16,16,16c8.8,0,16-7.2,16-16S24.8,0,16,0z M15.2,18.7h-8l8-15.5V18.7z M16.8,28.8 V13.3h8L16.8,28.8z"/>'; + $text = ' +<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" /> +<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" /> +<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" /> +<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" /> +<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" /> +<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" /> +<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" /> + '; + return sprintf( + '<svg id="jetpack-logo__icon" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" viewBox="0 0 %1$s 32">%2$s</svg>', + ( true === $logotype ? '118' : '32' ), + ( true === $logotype ? $logo . $text : $logo ) + ); + } +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-options/legacy/class-jetpack-options.php b/plugins/jetpack/vendor/automattic/jetpack-options/legacy/class-jetpack-options.php new file mode 100644 index 00000000..5a0ea022 --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-options/legacy/class-jetpack-options.php @@ -0,0 +1,646 @@ +<?php +/** + * Legacy Jetpack_Options class. + * + * @package automattic/jetpack-options + */ + +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', + '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', + 'edit_links_calypso_redirect', // (bool) Whether post/page edit links on front end should point to Calypso. + '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 occured + '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 + 'safe_mode_confirmed', // (bool) True if someone confirms that this site was correctly put into safe mode automatically after an identity crisis is discovered. + 'migrate_for_idc', // (bool) True if someone confirms that this site should migrate stats and subscribers from its previous URL + 'dismissed_connection_banner', // (bool) True if the connection banner has been dismissed + 'ab_connect_banner_green_bar', // (int) Version displayed of the A/B test for the green bar at the top of the connect banner. + 'onboarding', // (string) Auth token to be used in the onboarding connection flow + 'tos_agreed', // (bool) Whether or not the TOS for connection has been agreed upon. + '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. + ); + + 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. + ); + + case 'network': + return array( + 'onboarding', // (string) Auth token to be used in the onboarding connection flow + '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. + ); + } + + /** + * 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 ( is_null( $group ) || 'non_compact' === $group ) { + if ( in_array( $name, self::get_option_names( $group ), true ) ) { + return true; + } + } + + foreach ( array_keys( self::$grouped_options ) as $_group ) { + if ( is_null( $group ) || $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 ); + } + + /** + * 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 + */ + public static function get_option( $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 ); + } + } + + 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 wish to change legacy behavior. + + return $default; + } + + /** + * 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 string $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 3.0.0 + * + * @param str $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 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 blacklist 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 5.5.0 + * + * @param array $disabled_raw_options An array of option names that you can selectively blacklist 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 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 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', + 'wp_mobile_excerpt', + 'wp_mobile_featured_images', + 'wp_mobile_app_promos', + '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', + 'wp_mobile_custom_css', + '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_connection_banner_ab', + 'jetpack_active_plan', + 'jetpack_activation_source', + 'jetpack_sso_match_by_email', + 'jetpack_sso_require_two_step', + 'jetpack_sso_remove_login_form', + 'jetpack_last_connect_url_check', + 'jpo_business_address', + 'jpo_site_type', + 'jpo_homepage_format', + 'jpo_contact_page', + 'jetpack_excluded_extensions', + ); + } + + /** + * Gets all options that can be safely reset by CLI. + * + * @since 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 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 ); + } + } +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-roles/src/class-roles.php b/plugins/jetpack/vendor/automattic/jetpack-roles/src/class-roles.php new file mode 100644 index 00000000..7bce3462 --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-roles/src/class-roles.php @@ -0,0 +1,81 @@ +<?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 ]; + } +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-status/src/class-status.php b/plugins/jetpack/vendor/automattic/jetpack-status/src/class-status.php new file mode 100644 index 00000000..f87ca9af --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-status/src/class-status.php @@ -0,0 +1,83 @@ +<?php +/** + * A status class for Jetpack. + * + * @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 Status { + /** + * Is Jetpack in development (offline) mode? + * + * @return bool Whether Jetpack's development mode is active. + */ + public function is_development_mode() { + $development_mode = false; + $site_url = site_url(); + + if ( defined( '\\JETPACK_DEV_DEBUG' ) ) { + $development_mode = constant( '\\JETPACK_DEV_DEBUG' ); + } elseif ( $site_url ) { + $development_mode = false === strpos( $site_url, '.' ); + } + + /** + * Filters Jetpack's development mode. + * + * @see https://jetpack.com/support/development-mode/ + * + * @since 2.2.1 + * + * @param bool $development_mode Is Jetpack's development mode active. + */ + $development_mode = (bool) apply_filters( 'jetpack_development_mode', $development_mode ); + + return $development_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; + + // If we don't have a multi site setup no need to do any more. + if ( ! is_multisite() ) { + return false; + } + + $num_sites = $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->site}" ); + if ( $num_sites > 1 ) { + return true; + } + + return false; + } + + /** + * Whether the current site is single user site. + * + * @return bool + */ + public function is_single_user_site() { + global $wpdb; + + $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 ); + } + return 1 === (int) $some_users; + } +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-sync/src/class-actions.php b/plugins/jetpack/vendor/automattic/jetpack-sync/src/class-actions.php new file mode 100644 index 00000000..2c31b914 --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-sync/src/class-actions.php @@ -0,0 +1,767 @@ +<?php +/** + * A class that defines syncable actions for Jetpack. + * + * @package automattic/jetpack-sync + */ + +namespace Automattic\Jetpack\Sync; + +use Automattic\Jetpack\Connection\Manager as Jetpack_Connection; +use Automattic\Jetpack\Constants; +use Automattic\Jetpack\Status; +use Automattic\Jetpack\Sync\Modules; + +/** + * The role of this class is to hook the Sync subsystem into WordPress - when to listen for actions, + * when to send, when to perform a full sync, etc. + * + * It also binds the action to send data to WPCOM to Jetpack's XMLRPC client object. + */ +class Actions { + /** + * A variable to hold a sync sender object. + * + * @access public + * @static + * + * @var Automattic\Jetpack\Sync\Sender + */ + public static $sender = null; + + /** + * A variable to hold a sync listener object. + * + * @access public + * @static + * + * @var Automattic\Jetpack\Sync\Listener + */ + public static $listener = null; + + /** + * Name of the sync cron schedule. + * + * @access public + * + * @var string + */ + const DEFAULT_SYNC_CRON_INTERVAL_NAME = 'jetpack_sync_interval'; + + /** + * Interval between the last and the next sync cron action. + * + * @access public + * + * @var int + */ + const DEFAULT_SYNC_CRON_INTERVAL_VALUE = 300; // 5 * MINUTE_IN_SECONDS; + + /** + * Initialize Sync for cron jobs, set up listeners for WordPress Actions, + * and set up a shut-down action for sending actions to WordPress.com + * + * @access public + * @static + */ + public static function init() { + // Everything below this point should only happen if we're a valid sync site. + if ( ! self::sync_allowed() ) { + return; + } + + if ( self::sync_via_cron_allowed() ) { + self::init_sync_cron_jobs(); + } elseif ( wp_next_scheduled( 'jetpack_sync_cron' ) ) { + self::clear_sync_cron_jobs(); + } + // When importing via cron, do not sync. + add_action( 'wp_cron_importer_hook', array( __CLASS__, 'set_is_importing_true' ), 1 ); + + // Sync connected user role changes to WordPress.com. + Users::init(); + + // Publicize filter to prevent publicizing blacklisted post types. + add_filter( 'publicize_should_publicize_published_post', array( __CLASS__, 'prevent_publicize_blacklisted_posts' ), 10, 2 ); + + /** + * Fires on every request before default loading sync listener code. + * Return false to not load sync listener code that monitors common + * WP actions to be serialized. + * + * By default this returns true for cron jobs, non-GET-requests, or requests where the + * user is logged-in. + * + * @since 4.2.0 + * + * @param bool should we load sync listener code for this request + */ + if ( apply_filters( 'jetpack_sync_listener_should_load', true ) ) { + self::initialize_listener(); + } + + add_action( 'init', array( __CLASS__, 'add_sender_shutdown' ), 90 ); + } + + /** + * Prepares sync to send actions on shutdown for the current request. + * + * @access public + * @static + */ + public static function add_sender_shutdown() { + /** + * Fires on every request before default loading sync sender code. + * Return false to not load sync sender code that serializes pending + * data and sends it to WPCOM for processing. + * + * By default this returns true for cron jobs, POST requests, admin requests, or requests + * by users who can manage_options. + * + * @since 4.2.0 + * + * @param bool should we load sync sender code for this request + */ + if ( apply_filters( + 'jetpack_sync_sender_should_load', + self::should_initialize_sender() + ) ) { + self::initialize_sender(); + add_action( 'shutdown', array( self::$sender, 'do_sync' ) ); + add_action( 'shutdown', array( self::$sender, 'do_full_sync' ) ); + } + } + + /** + * Decides if the sender should run on shutdown for this request. + * + * @access public + * @static + * + * @return bool + */ + public static function should_initialize_sender() { + if ( Constants::is_true( 'DOING_CRON' ) ) { + return self::sync_via_cron_allowed(); + } + + if ( isset( $_SERVER['REQUEST_METHOD'] ) && 'POST' === $_SERVER['REQUEST_METHOD'] ) { + return true; + } + + if ( current_user_can( 'manage_options' ) ) { + return true; + } + + if ( is_admin() ) { + return true; + } + + if ( defined( 'PHPUNIT_JETPACK_TESTSUITE' ) ) { + return true; + } + + if ( Constants::get_constant( 'WP_CLI' ) ) { + return true; + } + + return false; + } + + /** + * Decides if sync should run at all during this request. + * + * @access public + * @static + * + * @return bool + */ + public static function sync_allowed() { + if ( defined( 'PHPUNIT_JETPACK_TESTSUITE' ) ) { + return true; + } + + if ( ! Settings::is_sync_enabled() ) { + return false; + } + + if ( ( new Status() )->is_development_mode() ) { + return false; + } + + if ( \Jetpack::is_staging_site() ) { + return false; + } + + $connection = new Jetpack_Connection(); + if ( ! $connection->is_active() ) { + if ( ! doing_action( 'jetpack_user_authorized' ) ) { + return false; + } + } + + return true; + } + + /** + * Determines if syncing during a cron job is allowed. + * + * @access public + * @static + * + * @return bool|int + */ + public static function sync_via_cron_allowed() { + return ( Settings::get_setting( 'sync_via_cron' ) ); + } + + /** + * Decides if the given post should be Publicized based on its type. + * + * @access public + * @static + * + * @param bool $should_publicize Publicize status prior to this filter running. + * @param \WP_Post $post The post to test for Publicizability. + * @return bool + */ + public static function prevent_publicize_blacklisted_posts( $should_publicize, $post ) { + if ( in_array( $post->post_type, Settings::get_setting( 'post_types_blacklist' ), true ) ) { + return false; + } + + return $should_publicize; + } + + /** + * Set an importing flag to `true` in sync settings. + * + * @access public + * @static + */ + public static function set_is_importing_true() { + Settings::set_importing( true ); + } + + /** + * Sends data to WordPress.com via an XMLRPC request. + * + * @access public + * @static + * + * @param object $data Data relating to a sync action. + * @param string $codec_name The name of the codec that encodes the data. + * @param float $sent_timestamp Current server time so we can compensate for clock differences. + * @param string $queue_id The queue the action belongs to, sync or full_sync. + * @param float $checkout_duration Time spent retrieving queue items from the DB. + * @param float $preprocess_duration Time spent converting queue items into data to send. + * @return Jetpack_Error|mixed|WP_Error The result of the sending request. + */ + public static function send_data( $data, $codec_name, $sent_timestamp, $queue_id, $checkout_duration, $preprocess_duration ) { + $query_args = array( + 'sync' => '1', // Add an extra parameter to the URL so we can tell it's a sync action. + 'codec' => $codec_name, + 'timestamp' => $sent_timestamp, + 'queue' => $queue_id, + 'home' => Functions::home_url(), // Send home url option to check for Identity Crisis server-side. + 'siteurl' => Functions::site_url(), // Send siteurl option to check for Identity Crisis server-side. + 'cd' => sprintf( '%.4f', $checkout_duration ), + 'pd' => sprintf( '%.4f', $preprocess_duration ), + ); + + // Has the site opted in to IDC mitigation? + if ( \Jetpack::sync_idc_optin() ) { + $query_args['idc'] = true; + } + + if ( \Jetpack_Options::get_option( 'migrate_for_idc', false ) ) { + $query_args['migrate_for_idc'] = true; + } + + $query_args['timeout'] = Settings::is_doing_cron() ? 30 : 15; + + /** + * Filters query parameters appended to the Sync request URL sent to WordPress.com. + * + * @since 4.7.0 + * + * @param array $query_args associative array of query parameters. + */ + $query_args = apply_filters( 'jetpack_sync_send_data_query_args', $query_args ); + + $connection = new Jetpack_Connection(); + $url = add_query_arg( $query_args, $connection->xmlrpc_api_url() ); + + // If we're currently updating to Jetpack 7.7, the IXR client may be missing briefly + // because since 7.7 it's being autoloaded with Composer. + if ( ! class_exists( '\\Jetpack_IXR_Client' ) ) { + return new \WP_Error( + 'ixr_client_missing', + esc_html__( 'Sync has been aborted because the IXR client is missing.', 'jetpack' ) + ); + } + + $rpc = new \Jetpack_IXR_Client( + array( + 'url' => $url, + 'user_id' => JETPACK_MASTER_USER, + 'timeout' => $query_args['timeout'], + ) + ); + + $result = $rpc->query( 'jetpack.syncActions', $data ); + + if ( ! $result ) { + return $rpc->get_jetpack_error(); + } + + $response = $rpc->getResponse(); + + // Check if WordPress.com IDC mitigation blocked the sync request. + 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', + \Jetpack::get_sync_error_idc_option( $response ) + ); + } + + return new \WP_Error( + 'sync_error_idc', + esc_html__( 'Sync has been blocked from WordPress.com because it would cause an identity crisis', 'jetpack' ) + ); + } + + return $response; + } + + /** + * Kicks off the initial sync. + * + * @access public + * @static + * + * @return bool|null False if sync is not allowed. + */ + public static function do_initial_sync() { + // Lets not sync if we are not suppose to. + if ( ! self::sync_allowed() ) { + return false; + } + + // Don't start new sync if a full sync is in process. + $full_sync_module = Modules::get_module( 'full-sync' ); + if ( $full_sync_module && $full_sync_module->is_started() && ! $full_sync_module->is_finished() ) { + return false; + } + + $initial_sync_config = array( + 'options' => true, + 'functions' => true, + 'constants' => true, + 'users' => array( get_current_user_id() ), + ); + + if ( is_multisite() ) { + $initial_sync_config['network_options'] = true; + } + + self::do_full_sync( $initial_sync_config ); + } + + /** + * Kicks off a full sync. + * + * @access public + * @static + * + * @param array $modules The sync modules should be included in this full sync. All will be included if null. + * @return bool True if full sync was successfully started. + */ + public static function do_full_sync( $modules = null ) { + if ( ! self::sync_allowed() ) { + return false; + } + + $full_sync_module = Modules::get_module( 'full-sync' ); + + if ( ! $full_sync_module ) { + return false; + } + + self::initialize_listener(); + + $full_sync_module->start( $modules ); + + return true; + } + + /** + * Adds a cron schedule for regular syncing via cron, unless the schedule already exists. + * + * @access public + * @static + * + * @param array $schedules The list of WordPress cron schedules prior to this filter. + * @return array A list of WordPress cron schedules with the Jetpack sync interval added. + */ + public static function jetpack_cron_schedule( $schedules ) { + if ( ! isset( $schedules[ self::DEFAULT_SYNC_CRON_INTERVAL_NAME ] ) ) { + $minutes = intval( self::DEFAULT_SYNC_CRON_INTERVAL_VALUE / 60 ); + $display = ( 1 === $minutes ) ? + __( 'Every minute', 'jetpack' ) : + /* translators: %d is an integer indicating the number of minutes. */ + sprintf( __( 'Every %d minutes', 'jetpack' ), $minutes ); + $schedules[ self::DEFAULT_SYNC_CRON_INTERVAL_NAME ] = array( + 'interval' => self::DEFAULT_SYNC_CRON_INTERVAL_VALUE, + 'display' => $display, + ); + } + return $schedules; + } + + /** + * Starts an incremental sync via cron. + * + * @access public + * @static + */ + public static function do_cron_sync() { + self::do_cron_sync_by_type( 'sync' ); + } + + /** + * Starts a full sync via cron. + * + * @access public + * @static + */ + public static function do_cron_full_sync() { + self::do_cron_sync_by_type( 'full_sync' ); + } + + /** + * Try to send actions until we run out of things to send, + * or have to wait more than 15s before sending again, + * or we hit a lock or some other sending issue + * + * @access public + * @static + * + * @param string $type Sync type. Can be `sync` or `full_sync`. + */ + public static function do_cron_sync_by_type( $type ) { + if ( ! self::sync_allowed() || ( 'sync' !== $type && 'full_sync' !== $type ) ) { + return; + } + + self::initialize_sender(); + + $time_limit = Settings::get_setting( 'cron_sync_time_limit' ); + $start_time = time(); + + do { + $next_sync_time = self::$sender->get_next_sync_time( $type ); + + if ( $next_sync_time ) { + $delay = $next_sync_time - time() + 1; + if ( $delay > 15 ) { + break; + } elseif ( $delay > 0 ) { + sleep( $delay ); + } + } + + $result = 'full_sync' === $type ? self::$sender->do_full_sync() : self::$sender->do_sync(); + } while ( $result && ! is_wp_error( $result ) && ( $start_time + $time_limit ) > time() ); + } + + /** + * Initialize the sync listener. + * + * @access public + * @static + */ + public static function initialize_listener() { + self::$listener = Listener::get_instance(); + } + + /** + * Initializes the sync sender. + * + * @access public + * @static + */ + public static function initialize_sender() { + self::$sender = Sender::get_instance(); + add_filter( 'jetpack_sync_send_data', array( __CLASS__, 'send_data' ), 10, 6 ); + } + + /** + * Initializes sync for WooCommerce. + * + * @access public + * @static + */ + public static function initialize_woocommerce() { + if ( false === class_exists( 'WooCommerce' ) ) { + return; + } + add_filter( 'jetpack_sync_modules', array( __CLASS__, 'add_woocommerce_sync_module' ) ); + } + + /** + * Adds Woo's sync modules to existing modules for sending. + * + * @access public + * @static + * + * @param array $sync_modules The list of sync modules declared prior to this filter. + * @return array A list of sync modules that now includes Woo's modules. + */ + public static function add_woocommerce_sync_module( $sync_modules ) { + $sync_modules[] = 'Automattic\\Jetpack\\Sync\\Modules\\WooCommerce'; + return $sync_modules; + } + + /** + * Initializes sync for WP Super Cache. + * + * @access public + * @static + */ + public static function initialize_wp_super_cache() { + if ( false === function_exists( 'wp_cache_is_enabled' ) ) { + return; + } + add_filter( 'jetpack_sync_modules', array( __CLASS__, 'add_wp_super_cache_sync_module' ) ); + } + + /** + * Adds WP Super Cache's sync modules to existing modules for sending. + * + * @access public + * @static + * + * @param array $sync_modules The list of sync modules declared prior to this filer. + * @return array A list of sync modules that now includes WP Super Cache's modules. + */ + public static function add_wp_super_cache_sync_module( $sync_modules ) { + $sync_modules[] = 'Automattic\\Jetpack\\Sync\\Modules\\WP_Super_Cache'; + return $sync_modules; + } + + /** + * Sanitizes the name of sync's cron schedule. + * + * @access public + * @static + * + * @param string $schedule The name of a WordPress cron schedule. + * @return string The sanitized name of sync's cron schedule. + */ + public static function sanitize_filtered_sync_cron_schedule( $schedule ) { + $schedule = sanitize_key( $schedule ); + $schedules = wp_get_schedules(); + + // Make sure that the schedule has actually been registered using the `cron_intervals` filter. + if ( isset( $schedules[ $schedule ] ) ) { + return $schedule; + } + + return self::DEFAULT_SYNC_CRON_INTERVAL_NAME; + } + + /** + * Allows offsetting of start times for sync cron jobs. + * + * @access public + * @static + * + * @param string $schedule The name of a cron schedule. + * @param string $hook The hook that this method is responding to. + * @return int The offset for the sync cron schedule. + */ + public static function get_start_time_offset( $schedule = '', $hook = '' ) { + $start_time_offset = is_multisite() + ? wp_rand( 0, ( 2 * self::DEFAULT_SYNC_CRON_INTERVAL_VALUE ) ) + : 0; + + /** + * Allows overriding the offset that the sync cron jobs will first run. This can be useful when scheduling + * cron jobs across multiple sites in a network. + * + * @since 4.5.0 + * + * @param int $start_time_offset + * @param string $hook + * @param string $schedule + */ + return intval( + apply_filters( + 'jetpack_sync_cron_start_time_offset', + $start_time_offset, + $hook, + $schedule + ) + ); + } + + /** + * Decides if a sync cron should be scheduled. + * + * @access public + * @static + * + * @param string $schedule The name of a cron schedule. + * @param string $hook The hook that this method is responding to. + */ + public static function maybe_schedule_sync_cron( $schedule, $hook ) { + if ( ! $hook ) { + return; + } + $schedule = self::sanitize_filtered_sync_cron_schedule( $schedule ); + + $start_time = time() + self::get_start_time_offset( $schedule, $hook ); + if ( ! wp_next_scheduled( $hook ) ) { + // Schedule a job to send pending queue items once a minute. + wp_schedule_event( $start_time, $schedule, $hook ); + } elseif ( wp_get_schedule( $hook ) !== $schedule ) { + // If the schedule has changed, update the schedule. + wp_clear_scheduled_hook( $hook ); + wp_schedule_event( $start_time, $schedule, $hook ); + } + } + + /** + * Clears Jetpack sync cron jobs. + * + * @access public + * @static + */ + public static function clear_sync_cron_jobs() { + wp_clear_scheduled_hook( 'jetpack_sync_cron' ); + wp_clear_scheduled_hook( 'jetpack_sync_full_cron' ); + } + + /** + * Initializes Jetpack sync cron jobs. + * + * @access public + * @static + */ + public static function init_sync_cron_jobs() { + add_filter( 'cron_schedules', array( __CLASS__, 'jetpack_cron_schedule' ) ); // phpcs:ignore WordPress.WP.CronInterval.ChangeDetected + + add_action( 'jetpack_sync_cron', array( __CLASS__, 'do_cron_sync' ) ); + add_action( 'jetpack_sync_full_cron', array( __CLASS__, 'do_cron_full_sync' ) ); + + /** + * Allows overriding of the default incremental sync cron schedule which defaults to once every 5 minutes. + * + * @since 4.3.2 + * + * @param string self::DEFAULT_SYNC_CRON_INTERVAL_NAME + */ + $incremental_sync_cron_schedule = apply_filters( 'jetpack_sync_incremental_sync_interval', self::DEFAULT_SYNC_CRON_INTERVAL_NAME ); + self::maybe_schedule_sync_cron( $incremental_sync_cron_schedule, 'jetpack_sync_cron' ); + + /** + * Allows overriding of the full sync cron schedule which defaults to once every 5 minutes. + * + * @since 4.3.2 + * + * @param string self::DEFAULT_SYNC_CRON_INTERVAL_NAME + */ + $full_sync_cron_schedule = apply_filters( 'jetpack_sync_full_sync_interval', self::DEFAULT_SYNC_CRON_INTERVAL_NAME ); + self::maybe_schedule_sync_cron( $full_sync_cron_schedule, 'jetpack_sync_full_cron' ); + } + + /** + * Perform maintenance when a plugin upgrade occurs. + * + * @access public + * @static + * + * @param string $new_version New version of the plugin. + * @param string $old_version Old version of the plugin. + */ + public static function cleanup_on_upgrade( $new_version = null, $old_version = null ) { + if ( wp_next_scheduled( 'jetpack_sync_send_db_checksum' ) ) { + wp_clear_scheduled_hook( 'jetpack_sync_send_db_checksum' ); + } + + $is_new_sync_upgrade = version_compare( $old_version, '4.2', '>=' ); + if ( ! empty( $old_version ) && $is_new_sync_upgrade && version_compare( $old_version, '4.5', '<' ) ) { + self::clear_sync_cron_jobs(); + Settings::update_settings( + array( + 'render_filtered_content' => Defaults::$default_render_filtered_content, + ) + ); + } + } + + /** + * Get syncing status for the given fields. + * + * @access public + * @static + * + * @param string|null $fields A comma-separated string of the fields to include in the array from the JSON response. + * @return array An associative array with the status report. + */ + public static function get_sync_status( $fields = null ) { + self::initialize_sender(); + + $sync_module = Modules::get_module( 'full-sync' ); + $queue = self::$sender->get_sync_queue(); + $full_queue = self::$sender->get_full_sync_queue(); + $cron_timestamps = array_keys( _get_cron_array() ); + $next_cron = $cron_timestamps[0] - time(); + + $checksums = array(); + + if ( ! empty( $fields ) ) { + $store = new Replicastore(); + $fields_params = array_map( 'trim', explode( ',', $fields ) ); + + if ( in_array( 'posts_checksum', $fields_params, true ) ) { + $checksums['posts_checksum'] = $store->posts_checksum(); + } + if ( in_array( 'comments_checksum', $fields_params, true ) ) { + $checksums['comments_checksum'] = $store->comments_checksum(); + } + if ( in_array( 'post_meta_checksum', $fields_params, true ) ) { + $checksums['post_meta_checksum'] = $store->post_meta_checksum(); + } + if ( in_array( 'comment_meta_checksum', $fields_params, true ) ) { + $checksums['comment_meta_checksum'] = $store->comment_meta_checksum(); + } + } + + $full_sync_status = ( $sync_module ) ? $sync_module->get_status() : array(); + + return array_merge( + $full_sync_status, + $checksums, + array( + 'cron_size' => count( $cron_timestamps ), + 'next_cron' => $next_cron, + 'queue_size' => $queue->size(), + 'queue_lag' => $queue->lag(), + 'queue_next_sync' => ( self::$sender->get_next_sync_time( 'sync' ) - microtime( true ) ), + 'full_queue_size' => $full_queue->size(), + 'full_queue_lag' => $full_queue->lag(), + 'full_queue_next_sync' => ( self::$sender->get_next_sync_time( 'full_sync' ) - microtime( true ) ), + ) + ); + } +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-sync/src/class-defaults.php b/plugins/jetpack/vendor/automattic/jetpack-sync/src/class-defaults.php new file mode 100644 index 00000000..69b7c7a8 --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-sync/src/class-defaults.php @@ -0,0 +1,1179 @@ +<?php +/** + * Jetpack Sync Defaults + * + * @package automattic/jetpack-sync + */ + +namespace Automattic\Jetpack\Sync; + +require_once JETPACK__PLUGIN_DIR . 'modules/sso/class.jetpack-sso-helpers.php'; + +use Automattic\Jetpack\Status; +use Automattic\Jetpack\Sync\Functions; + +/** + * Just some defaults that we share with the server. + */ +class Defaults { + + /** + * Default Options. + * + * @var array + */ + public static $default_options_whitelist = array( + 'stylesheet', + 'blogname', + 'blogdescription', + 'blog_charset', + 'permalink_structure', + 'category_base', + 'tag_base', + 'sidebars_widgets', + 'comment_moderation', + 'default_comment_status', + 'page_on_front', + 'rss_use_excerpt', + 'subscription_options', + 'stb_enabled', + 'stc_enabled', + 'comment_registration', + 'show_avatars', + 'avatar_default', + 'avatar_rating', + 'highlander_comment_form_prompt', + 'jetpack_comment_form_color_scheme', + 'stats_options', + 'gmt_offset', + 'timezone_string', + 'jetpack_sync_non_public_post_stati', + 'jetpack_options', + 'site_icon', // (int) - ID of core's Site Icon attachment ID + 'default_post_format', + 'default_category', + 'large_size_w', + 'large_size_h', + 'thumbnail_size_w', + 'thumbnail_size_h', + 'medium_size_w', + 'medium_size_h', + 'thumbnail_crop', + 'image_default_link_type', + 'site_logo', + 'sharing-options', + 'sharing-services', + 'post_count', + 'default_ping_status', + 'sticky_posts', + 'blog_public', + 'default_pingback_flag', + 'require_name_email', + 'close_comments_for_old_posts', + 'close_comments_days_old', + 'thread_comments', + 'thread_comments_depth', + 'page_comments', + 'comments_per_page', + 'default_comments_page', + 'comment_order', + 'comments_notify', + 'moderation_notify', + 'social_notifications_like', + 'social_notifications_reblog', + 'social_notifications_subscribe', + 'comment_whitelist', + 'comment_max_links', + 'moderation_keys', + 'jetpack_wga', + 'disabled_likes', + 'disabled_reblogs', + 'jetpack_comment_likes_enabled', + 'twitter_via', + 'jetpack-memberships-connected-account-id', + 'jetpack-twitter-cards-site-tag', + 'wpcom_publish_posts_with_markdown', + 'wpcom_publish_comments_with_markdown', + 'jetpack_activated', + 'jetpack_available_modules', + 'jetpack_allowed_xsite_search_ids', + 'jetpack_autoupdate_plugins', + 'jetpack_autoupdate_plugins_translations', + 'jetpack_autoupdate_themes', + 'jetpack_autoupdate_themes_translations', + 'jetpack_autoupdate_core', + 'jetpack_autoupdate_translations', + 'carousel_background_color', + 'carousel_display_exif', + 'jetpack_portfolio', + 'jetpack_portfolio_posts_per_page', + 'jetpack_testimonial', + 'jetpack_testimonial_posts_per_page', + 'tiled_galleries', + 'gravatar_disable_hovercards', + 'infinite_scroll', + 'infinite_scroll_google_analytics', + 'wp_mobile_excerpt', + 'wp_mobile_featured_images', + 'wp_mobile_app_promos', + 'monitor_receive_notifications', + 'post_by_email_address', + 'jetpack_mailchimp', + 'jetpack_protect_key', + 'jetpack_protect_global_whitelist', + 'jetpack_sso_require_two_step', + 'jetpack_sso_match_by_email', + 'jetpack_relatedposts', + 'verification_services_codes', + 'users_can_register', + 'active_plugins', + 'uninstall_plugins', + 'advanced_seo_front_page_description', // Jetpack_SEO_Utils::FRONT_PAGE_META_OPTION. + 'advanced_seo_title_formats', // Jetpack_SEO_Titles::TITLE_FORMATS_OPTION. + 'jetpack_api_cache_enabled', + 'start_of_week', + 'blacklist_keys', + 'posts_per_page', + 'posts_per_rss', + 'show_on_front', + 'ping_sites', + 'uploads_use_yearmonth_folders', + 'date_format', + 'time_format', + 'admin_email', + 'new_admin_email', + 'default_email_category', + 'default_role', + 'page_for_posts', + 'mailserver_url', + 'mailserver_login', // Not syncing contents, only the option name. + 'mailserver_pass', // Not syncing contents, only the option name. + 'mailserver_port', + 'wp_page_for_privacy_policy', + 'enable_header_ad', + 'wordads_second_belowpost', + 'wordads_display_front_page', + 'wordads_display_post', + 'wordads_display_page', + 'wordads_display_archive', + 'wordads_custom_adstxt', + 'site_segment', + 'site_user_type', + 'site_vertical', + 'jetpack_excluded_extensions', + ); + + /** + * Return options whitelist filtered. + * + * @return array Options whitelist. + */ + public static function get_options_whitelist() { + /** This filter is already documented in json-endpoints/jetpack/class.wpcom-json-api-get-option-endpoint.php */ + $options_whitelist = apply_filters( 'jetpack_options_whitelist', self::$default_options_whitelist ); + /** + * Filter the list of WordPress options that are manageable via the JSON API. + * + * @module sync + * + * @since 4.8.0 + * + * @param array The default list of options. + */ + return apply_filters( 'jetpack_sync_options_whitelist', $options_whitelist ); + } + + /** + * "Contentless" Options. + * + * Do not sync contents for these events, only the option name. Good for sensitive information that Sync does not need. + * + * @var array Options to sync name only. + */ + public static $default_options_contentless = array( + 'mailserver_login', + 'mailserver_pass', + ); + + /** + * Return contentless options. + * + * These are options that Sync only uses the option names, not the content of the option. + * + * @return array + */ + public static function get_options_contentless() { + /** + * Filter the list of WordPress options that should be synced without content + * + * @module sync + * + * @since 6.1.0 + * + * @param array The list of options synced without content. + */ + return apply_filters( 'jetpack_sync_options_contentless', self::$default_options_contentless ); + } + + /** + * Array of defaulted constants whitelisted. + * + * @var array Default constants whitelist + */ + public static $default_constants_whitelist = array( + 'EMPTY_TRASH_DAYS', + 'WP_POST_REVISIONS', + 'AUTOMATIC_UPDATER_DISABLED', + 'ABSPATH', + 'WP_CONTENT_DIR', + 'FS_METHOD', + 'DISALLOW_FILE_EDIT', + 'DISALLOW_FILE_MODS', + 'WP_AUTO_UPDATE_CORE', + 'WP_HTTP_BLOCK_EXTERNAL', + 'WP_ACCESSIBLE_HOSTS', + 'JETPACK__VERSION', + 'IS_PRESSABLE', + 'DISABLE_WP_CRON', + 'ALTERNATE_WP_CRON', + 'WP_CRON_LOCK_TIMEOUT', + 'PHP_VERSION', + 'WP_MEMORY_LIMIT', + 'WP_MAX_MEMORY_LIMIT', + 'WP_DEBUG', + ); + + /** + * Get constants whitelisted by Sync. + * + * @return array Constants accessible via sync. + */ + public static function get_constants_whitelist() { + /** + * Filter the list of PHP constants that are manageable via the JSON API. + * + * @module sync + * + * @since 4.8.0 + * + * @param array The default list of constants options. + */ + return apply_filters( 'jetpack_sync_constants_whitelist', self::$default_constants_whitelist ); + } + + /** + * Callables able to be managed via JSON API. + * + * @var array Default whitelist of callables. + */ + public static $default_callable_whitelist = array( + 'wp_max_upload_size' => 'wp_max_upload_size', + 'is_main_network' => array( __CLASS__, 'is_multi_network' ), + 'is_multi_site' => 'is_multisite', + 'main_network_site' => array( 'Automattic\\Jetpack\\Sync\\Functions', 'main_network_site_url' ), + 'site_url' => array( 'Automattic\\Jetpack\\Sync\\Functions', 'site_url' ), + 'home_url' => array( 'Automattic\\Jetpack\\Sync\\Functions', 'home_url' ), + 'single_user_site' => array( 'Jetpack', 'is_single_user_site' ), + 'updates' => array( 'Jetpack', 'get_updates' ), + 'has_file_system_write_access' => array( 'Automattic\\Jetpack\\Sync\\Functions', 'file_system_write_access' ), + 'is_version_controlled' => array( 'Automattic\\Jetpack\\Sync\\Functions', 'is_version_controlled' ), + 'taxonomies' => array( 'Automattic\\Jetpack\\Sync\\Functions', 'get_taxonomies' ), + 'post_types' => array( 'Automattic\\Jetpack\\Sync\\Functions', 'get_post_types' ), + 'post_type_features' => array( 'Automattic\\Jetpack\\Sync\\Functions', 'get_post_type_features' ), + 'shortcodes' => array( 'Automattic\\Jetpack\\Sync\\Functions', 'get_shortcodes' ), + 'rest_api_allowed_post_types' => array( 'Automattic\\Jetpack\\Sync\\Functions', 'rest_api_allowed_post_types' ), + 'rest_api_allowed_public_metadata' => array( 'Automattic\\Jetpack\\Sync\\Functions', 'rest_api_allowed_public_metadata' ), + 'sso_is_two_step_required' => array( 'Jetpack_SSO_Helpers', 'is_two_step_required' ), + 'sso_should_hide_login_form' => array( 'Jetpack_SSO_Helpers', 'should_hide_login_form' ), + 'sso_match_by_email' => array( 'Jetpack_SSO_Helpers', 'match_by_email' ), + 'sso_new_user_override' => array( 'Jetpack_SSO_Helpers', 'new_user_override' ), + 'sso_bypass_default_login_form' => array( 'Jetpack_SSO_Helpers', 'bypass_login_forward_wpcom' ), + 'wp_version' => array( 'Automattic\\Jetpack\\Sync\\Functions', 'wp_version' ), + 'get_plugins' => array( 'Automattic\\Jetpack\\Sync\\Functions', 'get_plugins' ), + 'get_plugins_action_links' => array( 'Automattic\\Jetpack\\Sync\\Functions', 'get_plugins_action_links' ), + 'active_modules' => array( 'Jetpack', 'get_active_modules' ), + 'hosting_provider' => array( 'Automattic\\Jetpack\\Sync\\Functions', 'get_hosting_provider' ), + 'locale' => 'get_locale', + 'site_icon_url' => array( 'Automattic\\Jetpack\\Sync\\Functions', 'site_icon_url' ), + 'roles' => array( 'Automattic\\Jetpack\\Sync\\Functions', 'roles' ), + 'timezone' => array( 'Automattic\\Jetpack\\Sync\\Functions', 'get_timezone' ), + 'available_jetpack_blocks' => array( 'Jetpack_Gutenberg', 'get_availability' ), // Includes both Gutenberg blocks *and* plugins. + 'paused_themes' => array( 'Automattic\\Jetpack\\Sync\\Functions', 'get_paused_themes' ), + 'paused_plugins' => array( 'Automattic\\Jetpack\\Sync\\Functions', 'get_paused_plugins' ), + ); + + + /** + * Array of post type attributes synced. + * + * @var array Default post type attributes. + */ + public static $default_post_type_attributes = array( + 'name' => '', + 'label' => '', + 'labels' => array(), + 'description' => '', + 'public' => false, + 'hierarchical' => false, + 'exclude_from_search' => true, + 'publicly_queryable' => null, + 'show_ui' => false, + 'show_in_menu' => null, + 'show_in_nav_menus' => null, + 'show_in_admin_bar' => false, + 'menu_position' => null, + 'menu_icon' => null, + 'supports' => array(), + 'capability_type' => 'post', + 'capabilities' => array(), + 'cap' => array(), + 'map_meta_cap' => true, + 'taxonomies' => array(), + 'has_archive' => false, + 'rewrite' => true, + 'query_var' => true, + 'can_export' => true, + 'delete_with_user' => null, + 'show_in_rest' => false, + 'rest_base' => false, + '_builtin' => false, + '_edit_link' => 'post.php?post=%d', + ); + + /** + * Get the whitelist of callables allowed to be managed via the JSON API. + * + * @return array Whitelist of callables allowed to be managed via the JSON API. + */ + public static function get_callable_whitelist() { + /** + * Filter the list of callables that are manageable via the JSON API. + * + * @module sync + * + * @since 4.8.0 + * + * @param array The default list of callables. + */ + return apply_filters( 'jetpack_sync_callable_whitelist', self::$default_callable_whitelist ); + } + + /** + * Post types that will not be synced. + * + * These are usually automated post types (sitemaps, logs, etc). + * + * @var array Blacklisted post types. + */ + public static $blacklisted_post_types = array( + 'ai1ec_event', + 'bwg_album', + 'bwg_gallery', + 'customize_changeset', // WP built-in post type for Customizer changesets. + 'dn_wp_yt_log', + 'http', + 'idx_page', + 'jetpack_migration', + 'jp_img_sitemap', + 'jp_img_sitemap_index', + 'jp_sitemap', + 'jp_sitemap_index', + 'jp_sitemap_master', + 'jp_vid_sitemap', + 'jp_vid_sitemap_index', + 'postman_sent_mail', + 'rssap-feed', + 'rssmi_feed_item', + 'scheduled-action', // Action Scheduler - Job Queue for WordPress https://github.com/woocommerce/woocommerce/tree/e7762627c37ec1f7590e6cac4218ba0c6a20024d/includes/libraries/action-scheduler . + 'secupress_log_action', + 'sg_optimizer_jobs', + 'snitch', + 'vip-legacy-redirect', + 'wp_automatic', + 'wpephpcompat_jobs', + 'wprss_feed_item', + ); + + /** + * Taxonomies that we're not syncing by default. + * + * The list is compiled by auditing the dynamic filters and actions that contain taxonomy slugs + * and could conflict with other existing filters/actions in WP core, Jetpack and WooCommerce. + * + * @var array + */ + public static $blacklisted_taxonomies = array( + 'ancestors', + 'archives_link', + 'attached_file', + 'attached_media', + 'attached_media_args', + 'attachment', + 'available_languages', + 'avatar', + 'avatar_comment_types', + 'avatar_data', + 'avatar_url', + 'bloginfo_rss', + 'blogs_of_user', + 'bookmark_link', + 'bookmarks', + 'calendar', + 'canonical_url', + 'categories_per_page', + 'categories_taxonomy', + 'category_form', + 'category_form_fields', + 'category_form_pre', + 'comment', + 'comment_author', + 'comment_author_email', + 'comment_author_IP', + 'comment_author_link', + 'comment_author_url', + 'comment_author_url_link', + 'comment_date', + 'comment_excerpt', + 'comment_ID', + 'comment_link', + 'comment_misc_actions', + 'comment_text', + 'comment_time', + 'comment_type', + 'comments_link', + 'comments_number', + 'comments_pagenum_link', + 'custom_logo', + 'date_sql', + 'default_comment_status', + 'delete_post_link', + 'edit_bookmark_link', + 'edit_comment_link', + 'edit_post_link', + 'edit_tag_link', + 'edit_term_link', + 'edit_user_link', + 'enclosed', + 'feed_build_date', + 'form_advanced', + 'form_after_editor', + 'form_after_title', + 'form_before_permalink', + 'form_top', + 'handle_product_cat', + 'header_image_tag', + 'header_video_url', + 'image_tag', + 'image_tag_class', + 'lastpostdate', + 'lastpostmodified', + 'link', + 'link_category_form', + 'link_category_form_fields', + 'link_category_form_pre', + 'main_network_id', + 'media', + 'media_item_args', + 'ms_user', + 'network', + 'object_terms', + 'option', + 'page', + 'page_form', + 'page_of_comment', + 'page_uri', + 'pagenum_link', + 'pages', + 'plugin', + 'post', + 'post_galleries', + 'post_gallery', + 'post_link', + 'post_modified_time', + 'post_status', + 'post_time', + 'postmeta', + 'posts_per_page', + 'product_cat', + 'product_search_form', + 'profile_url', + 'pung', + 'role_list', + 'sample_permalink', + 'sample_permalink_html', + 'schedule', + 'search_form', + 'search_query', + 'shortlink', + 'site', + 'site_email_content', + 'site_icon_url', + 'site_option', + 'space_allowed', + 'tag', + 'tag_form', + 'tag_form_fields', + 'tag_form_pre', + 'tag_link', + 'tags', + 'tags_per_page', + 'term', + 'term_link', + 'term_relationships', + 'term_taxonomies', + 'term_taxonomy', + 'terms', + 'terms_args', + 'terms_defaults', + 'terms_fields', + 'terms_orderby', + 'the_archive_description', + 'the_archive_title', + 'the_categories', + 'the_date', + 'the_excerpt', + 'the_guid', + 'the_modified_date', + 'the_modified_time', + 'the_post_type_description', + 'the_tags', + 'the_terms', + 'the_time', + 'theme_starter_content', + 'to_ping', + 'user', + 'user_created_user', + 'user_form', + 'user_profile', + 'user_profile_update', + 'usermeta', + 'usernumposts', + 'users_drafts', + 'webhook', + 'widget', + 'woocommerce_archive', + 'wp_title_rss', + ); + + /** + * Default array of post table columns. + * + * @var array Post table columns. + */ + public static $default_post_checksum_columns = array( + 'ID', + 'post_modified', + ); + + /** + * Default array of post meta table columns. + * + * @var array Post meta table columns. + */ + public static $default_post_meta_checksum_columns = array( + 'meta_id', + 'meta_value', + ); + + /** + * Default array of comment table columns. + * + * @var array Default comment table columns. + */ + public static $default_comment_checksum_columns = array( + 'comment_ID', + 'comment_content', + ); + + /** + * Default array of comment meta columns. + * + * @var array Comment meta table columns. + */ + public static $default_comment_meta_checksum_columns = array( + 'meta_id', + 'meta_value', + ); + + /** + * Default array of option table columns. + * + * @var array Default array of option columns. + */ + public static $default_option_checksum_columns = array( + 'option_name', + 'option_value', + ); + + /** + * Default array of term columns. + * + * @var array array of term columns. + */ + public static $default_term_checksum_columns = array( + 'term_id', + 'name', + 'slug', + ); + + /** + * Default array of term taxonomy columns. + * + * @var array Array of term taxonomy columns. + */ + public static $default_term_taxonomy_checksum_columns = array( + 'term_taxonomy_id', + 'term_id', + 'taxonomy', + 'parent', + 'count', + ); + + /** + * Default term relationship columns. + * + * @var array Array of term relationship columns. + */ + public static $default_term_relationships_checksum_columns = array( + 'object_id', + 'term_taxonomy_id', + 'term_order', + ); + + /** + * Default multisite callables able to be managed via JSON API. + * + * @var array multsite callables whitelisted + */ + public static $default_multisite_callable_whitelist = array( + 'network_name' => array( 'Jetpack', 'network_name' ), + 'network_allow_new_registrations' => array( 'Jetpack', 'network_allow_new_registrations' ), + 'network_add_new_users' => array( 'Jetpack', 'network_add_new_users' ), + 'network_site_upload_space' => array( 'Jetpack', 'network_site_upload_space' ), + 'network_upload_file_types' => array( 'Jetpack', 'network_upload_file_types' ), + 'network_enable_administration_menus' => array( 'Jetpack', 'network_enable_administration_menus' ), + ); + + /** + * Get array of multisite callables whitelisted. + * + * @return array Multisite callables managable via JSON API. + */ + public static function get_multisite_callable_whitelist() { + /** + * Filter the list of multisite callables that are manageable via the JSON API. + * + * @module sync + * + * @since 4.8.0 + * + * @param array The default list of multisite callables. + */ + return apply_filters( 'jetpack_sync_multisite_callable_whitelist', self::$default_multisite_callable_whitelist ); + } + + /** + * Array of post meta keys whitelisted. + * + * @var array Post meta whitelist. + */ + public static $post_meta_whitelist = array( + '_feedback_akismet_values', + '_feedback_email', + '_feedback_extra_fields', + '_g_feedback_shortcode', + '_jetpack_post_thumbnail', + '_menu_item_classes', + '_menu_item_menu_item_parent', + '_menu_item_object', + '_menu_item_object_id', + '_menu_item_orphaned', + '_menu_item_type', + '_menu_item_xfn', + '_publicize_facebook_user', + '_publicize_twitter_user', + '_thumbnail_id', + '_wp_attached_file', + '_wp_attachment_backup_sizes', + '_wp_attachment_context', + '_wp_attachment_image_alt', + '_wp_attachment_is_custom_background', + '_wp_attachment_is_custom_header', + '_wp_attachment_metadata', + '_wp_page_template', + '_wp_trash_meta_comments_status', + '_wpas_mess', + 'content_width', + 'custom_css_add', + 'custom_css_preprocessor', + 'enclosure', + 'imagedata', + 'nova_price', + 'publicize_results', + 'sharing_disabled', + 'switch_like_status', + 'videopress_guid', + 'vimeo_poster_image', + 'advanced_seo_description', // Jetpack_SEO_Posts::DESCRIPTION_META_KEY. + ); + + /** + * Get the post meta key whitelist. + * + * @return array Post meta whitelist. + */ + public static function get_post_meta_whitelist() { + /** + * Filter the list of post meta data that are manageable via the JSON API. + * + * @module sync + * + * @since 4.8.0 + * + * @param array The default list of meta data keys. + */ + return apply_filters( 'jetpack_sync_post_meta_whitelist', self::$post_meta_whitelist ); + } + + /** + * Comment meta whitelist. + * + * @var array Comment meta whitelist. + */ + public static $comment_meta_whitelist = array( + 'hc_avatar', + 'hc_post_as', + 'hc_wpcom_id_sig', + 'hc_foreign_user_id', + ); + + /** + * Get the comment meta whitelist. + * + * @return array + */ + public static function get_comment_meta_whitelist() { + /** + * Filter the list of comment meta data that are manageable via the JSON API. + * + * @module sync + * + * @since 5.7.0 + * + * @param array The default list of comment meta data keys. + */ + return apply_filters( 'jetpack_sync_comment_meta_whitelist', self::$comment_meta_whitelist ); + } + + /** + * Default theme support whitelist. + * + * @todo move this to server? - these are theme support values + * that should be synced as jetpack_current_theme_supports_foo option values + * + * @var array Default theme support whitelist. + */ + public static $default_theme_support_whitelist = array( + 'post-thumbnails', + 'post-formats', + 'custom-header', + 'custom-background', + 'custom-logo', + 'menus', + 'automatic-feed-links', + 'editor-style', + 'widgets', + 'html5', + 'title-tag', + 'jetpack-social-menu', + 'jetpack-responsive-videos', + 'infinite-scroll', + 'site-logo', + ); + + /** + * Is an option whitelisted? + * + * @param string $option Option name. + * @return bool If option is on the whitelist. + */ + public static function is_whitelisted_option( $option ) { + $whitelisted_options = self::get_options_whitelist(); + foreach ( $whitelisted_options as $whitelisted_option ) { + if ( '/' === $whitelisted_option[0] && preg_match( $whitelisted_option, $option ) ) { + return true; + } elseif ( $whitelisted_option === $option ) { + return true; + } + } + + return false; + } + + /** + * Default whitelist of capabilities to sync. + * + * @var array Array of WordPress capabilities. + */ + public static $default_capabilities_whitelist = array( + 'switch_themes', + 'edit_themes', + 'edit_theme_options', + 'install_themes', + 'activate_plugins', + 'edit_plugins', + 'install_plugins', + 'edit_users', + 'edit_files', + 'manage_options', + 'moderate_comments', + 'manage_categories', + 'manage_links', + 'upload_files', + 'import', + 'unfiltered_html', + 'edit_posts', + 'edit_others_posts', + 'edit_published_posts', + 'publish_posts', + 'edit_pages', + 'read', + 'publish_pages', + 'edit_others_pages', + 'edit_published_pages', + 'delete_pages', + 'delete_others_pages', + 'delete_published_pages', + 'delete_posts', + 'delete_others_posts', + 'delete_published_posts', + 'delete_private_posts', + 'edit_private_posts', + 'read_private_posts', + 'delete_private_pages', + 'edit_private_pages', + 'read_private_pages', + 'delete_users', + 'create_users', + 'unfiltered_upload', + 'edit_dashboard', + 'customize', + 'delete_site', + 'update_plugins', + 'delete_plugins', + 'update_themes', + 'update_core', + 'list_users', + 'remove_users', + 'add_users', + 'promote_users', + 'delete_themes', + 'export', + 'edit_comment', + 'upload_plugins', + 'upload_themes', + ); + + /** + * Get default capabilities whitelist. + * + * @return array + */ + public static function get_capabilities_whitelist() { + /** + * Filter the list of capabilities that we care about + * + * @module sync + * + * @since 5.5.0 + * + * @param array The default list of capabilities. + */ + return apply_filters( 'jetpack_sync_capabilities_whitelist', self::$default_capabilities_whitelist ); + } + + /** + * Get max execution sync time. + * + * @return float Number of seconds. + */ + public static function get_max_sync_execution_time() { + $max_exec_time = intval( ini_get( 'max_execution_time' ) ); + if ( 0 === $max_exec_time ) { + // 0 actually means "unlimited", but let's not treat it that way. + $max_exec_time = 60; + } + return floor( $max_exec_time / 3 ); + } + + /** + * Get default for a given setting. + * + * @param string $setting Setting to get. + * @return mixed Value will be a string, int, array, based on the particular setting requested. + */ + public static function get_default_setting( $setting ) { + $default_name = "default_$setting"; // e.g. default_dequeue_max_bytes. + return self::$$default_name; + } + + /** + * Default list of network options. + * + * @var array network options + */ + public static $default_network_options_whitelist = array( + 'site_name', + 'jetpack_protect_key', + 'jetpack_protect_global_whitelist', + 'active_sitewide_plugins', + ); + + /** + * A mapping of known importers to friendly names. + * + * Keys are the class name of the known importer. + * Values are the friendly name. + * + * @since 7.3.0 + * + * @var array + */ + public static $default_known_importers = array( + 'Blogger_Importer' => 'blogger', + 'LJ_API_Import' => 'livejournal', + 'MT_Import' => 'mt', + 'RSS_Import' => 'rss', + 'WC_Tax_Rate_Importer' => 'woo-tax-rate', + 'WP_Import' => 'wordpress', + ); + + /** + * Returns a list of known importers. + * + * @since 7.3.0 + * + * @return array Known importers with importer class names as keys and friendly names as values. + */ + public static function get_known_importers() { + /** + * Filter the list of known importers. + * + * @module sync + * + * @since 7.3.0 + * + * @param array The default list of known importers. + */ + return apply_filters( 'jetpack_sync_known_importers', self::$default_known_importers ); + } + + /** + * Whether this is a system with a multiple networks. + * We currently need this static wrapper because we statically define our default list of callables. + * + * @since 7.6.0 + * + * @uses Automattic\Jetpack\Status::is_multi_network + * + * @return boolean + */ + public static function is_multi_network() { + $status = new Status(); + return $status->is_multi_network(); + } + + /** + * Default bytes to dequeue. + * + * @var int Bytes. + */ + public static $default_dequeue_max_bytes = 500000; // very conservative value, 1/2 MB. + + /** + * Default upload bytes. + * + * This value is a little bigger than the upload limit to account for serialization. + * + * @var int Bytes. + */ + public static $default_upload_max_bytes = 600000; + + /** + * Default number of rows uploaded. + * + * @var int Number of rows. + */ + public static $default_upload_max_rows = 500; + + /** + * Default sync wait time. + * + * @var int Number of seconds. + */ + public static $default_sync_wait_time = 10; // seconds, between syncs. + + /** + * Only wait before next send if the current send took more than this number of seconds. + * + * @var int Number of seconds. + */ + public static $default_sync_wait_threshold = 5; + + /** + * Default wait between attempting to continue a full sync via requests. + * + * @var int Number of seconds. + */ + public static $default_enqueue_wait_time = 10; + + /** + * Maximum queue size. + * + * Each item is represented with a new row in the wp_options table. + * + * @var int Number of queue items. + */ + public static $default_max_queue_size = 1000; + + /** + * Default maximum lag allowed in the queue. + * + * @var int Number of seconds + */ + public static $default_max_queue_lag = 900; // 15 minutes. + + /** + * Default for default writes per sec. + * + * @var int Rows per second. + */ + public static $default_queue_max_writes_sec = 100; // 100 rows a second. + + /** + * Default for post types blacklist. + * + * @var array Empty array. + */ + public static $default_post_types_blacklist = array(); + + /** + * Default for taxonomies blacklist. + * + * @var array Empty array. + */ + public static $default_taxonomies_blacklist = array(); + + /** + * Default for taxonomies whitelist. + * + * @var array Empty array. + */ + public static $default_taxonomy_whitelist = array(); + + /** + * Default for post meta whitelist. + * + * @var array Empty array. + */ + public static $default_post_meta_whitelist = array(); + + /** + * Default for comment meta whitelist. + * + * @var array Empty array. + */ + public static $default_comment_meta_whitelist = array(); + + /** + * Default for disabling sync across the site. + * + * @var int Bool-ish. Default to 0. + */ + public static $default_disable = 0; // completely disable sending data to wpcom. + + /** + * Default for disabling sync across the entire network on multisite. + * + * @var int Bool-ish. Default 0. + */ + public static $default_network_disable = 0; + + /** + * Should Sync use cron? + * + * @var int Bool-ish value. Default 1. + */ + public static $default_sync_via_cron = 1; + + /** + * Default if Sync should render content. + * + * @var int Bool-ish value. Default is 0. + */ + public static $default_render_filtered_content = 0; + + /** + * Default number of items to enqueue at a time when running full sync. + * + * @var int Number of items. + */ + public static $default_max_enqueue_full_sync = 100; + + /** + * Default for maximum queue size during a full sync. + * + * Each item will represent a value in the wp_options table. + * + * @var int Number of items. + */ + public static $default_max_queue_size_full_sync = 1000; // max number of total items in the full sync queue. + + /** + * Defaul for time between syncing callables. + * + * @var int Number of seconds. + */ + public static $default_sync_callables_wait_time = MINUTE_IN_SECONDS; // seconds before sending callables again. + + /** + * Default for time between syncing constants. + * + * @var int Number of seconds. + */ + public static $default_sync_constants_wait_time = HOUR_IN_SECONDS; // seconds before sending constants again. + /** + * Default for sync queue lock timeout time. + * + * @var int Number of seconds. + */ + public static $default_sync_queue_lock_timeout = 120; // 2 minutes. + + /** + * Default for cron sync time limit. + * + * @var int Number of seconds. + */ + public static $default_cron_sync_time_limit = 30; // 30 seconds. + + /** + * Default for number of term relationship items sent in an full sync item. + * + * @var int Number of items. + */ + public static $default_term_relationships_full_sync_item_size = 100; + + /** + * Default for enabling incremental sync. + * + * @var int 1 for true. + */ + public static $default_sync_sender_enabled = 1; // Should send incremental sync items. + + /** + * Default for enabling Full Sync. + * + * @var int 1 for true. + */ + public static $default_full_sync_sender_enabled = 1; // Should send full sync items. +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-sync/src/class-functions.php b/plugins/jetpack/vendor/automattic/jetpack-sync/src/class-functions.php new file mode 100644 index 00000000..dc45c5c8 --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-sync/src/class-functions.php @@ -0,0 +1,544 @@ +<?php +/** + * Utility functions to generate data synced to wpcom + * + * @package automattic/jetpack-sync + */ + +namespace Automattic\Jetpack\Sync; + +use Automattic\Jetpack\Constants; + +/** + * Utility functions to generate data synced to wpcom + */ +class Functions { + const HTTPS_CHECK_OPTION_PREFIX = 'jetpack_sync_https_history_'; + const HTTPS_CHECK_HISTORY = 5; + + /** + * Return array of Jetpack modules. + * + * @return array + */ + public static function get_modules() { + require_once JETPACK__PLUGIN_DIR . 'class.jetpack-admin.php'; + + return \Jetpack_Admin::init()->get_modules(); + } + + /** + * Return array of taxonomies registered on the site. + * + * @return array + */ + public static function get_taxonomies() { + global $wp_taxonomies; + $wp_taxonomies_without_callbacks = array(); + foreach ( $wp_taxonomies as $taxonomy_name => $taxonomy ) { + $sanitized_taxonomy = self::sanitize_taxonomy( $taxonomy ); + if ( ! empty( $sanitized_taxonomy ) ) { + $wp_taxonomies_without_callbacks[ $taxonomy_name ] = $sanitized_taxonomy; + } + } + return $wp_taxonomies_without_callbacks; + } + + /** + * Return array of registered shortcodes. + * + * @return array + */ + public static function get_shortcodes() { + global $shortcode_tags; + return array_keys( $shortcode_tags ); + } + + /** + * Removes any callback data since we will not be able to process it on our side anyways. + * + * @param \WP_Taxonomy $taxonomy \WP_Taxonomy item. + * + * @return mixed|null + */ + public static function sanitize_taxonomy( $taxonomy ) { + + // Lets clone the taxonomy object instead of modifing the global one. + $cloned_taxonomy = json_decode( wp_json_encode( $taxonomy ) ); + + // recursive taxonomies are no fun. + if ( is_null( $cloned_taxonomy ) ) { + return null; + } + // Remove any meta_box_cb if they are not the default wp ones. + if ( isset( $cloned_taxonomy->meta_box_cb ) && + ! in_array( $cloned_taxonomy->meta_box_cb, array( 'post_tags_meta_box', 'post_categories_meta_box' ), true ) ) { + $cloned_taxonomy->meta_box_cb = null; + } + // Remove update call back. + if ( isset( $cloned_taxonomy->update_count_callback ) && + ! is_null( $cloned_taxonomy->update_count_callback ) ) { + $cloned_taxonomy->update_count_callback = null; + } + // Remove rest_controller_class if it something other then the default. + if ( isset( $cloned_taxonomy->rest_controller_class ) && + 'WP_REST_Terms_Controller' !== $cloned_taxonomy->rest_controller_class ) { + $cloned_taxonomy->rest_controller_class = null; + } + return $cloned_taxonomy; + } + + /** + * Return array of registered post types. + * + * @return array + */ + public static function get_post_types() { + global $wp_post_types; + + $post_types_without_callbacks = array(); + foreach ( $wp_post_types as $post_type_name => $post_type ) { + $sanitized_post_type = self::sanitize_post_type( $post_type ); + if ( ! empty( $sanitized_post_type ) ) { + $post_types_without_callbacks[ $post_type_name ] = $sanitized_post_type; + } + } + return $post_types_without_callbacks; + } + + /** + * Sanitizes by cloning post type object. + * + * @param object $post_type \WP_Post_Type. + * + * @return object + */ + public static function sanitize_post_type( $post_type ) { + // Lets clone the post type object instead of modifing the global one. + $sanitized_post_type = array(); + foreach ( Defaults::$default_post_type_attributes as $attribute_key => $default_value ) { + if ( isset( $post_type->{ $attribute_key } ) ) { + $sanitized_post_type[ $attribute_key ] = $post_type->{ $attribute_key }; + } + } + return (object) $sanitized_post_type; + } + + /** + * Return information about a synced post type. + * + * @param array $sanitized_post_type Array of args used in constructing \WP_Post_Type. + * @param string $post_type Post type name. + * + * @return object \WP_Post_Type + */ + public static function expand_synced_post_type( $sanitized_post_type, $post_type ) { + $post_type = sanitize_key( $post_type ); + $post_type_object = new \WP_Post_Type( $post_type, $sanitized_post_type ); + $post_type_object->add_supports(); + $post_type_object->add_rewrite_rules(); + $post_type_object->add_hooks(); + $post_type_object->register_taxonomies(); + return (object) $post_type_object; + } + + /** + * Returns site's post_type_features. + * + * @return array + */ + public static function get_post_type_features() { + global $_wp_post_type_features; + + return $_wp_post_type_features; + } + + /** + * Return hosting provider. + * + * Uses a set of known constants, classes, or functions to help determine the hosting platform. + * + * @return string Hosting provider. + */ + public static function get_hosting_provider() { + if ( defined( 'GD_SYSTEM_PLUGIN_DIR' ) || class_exists( '\\WPaaS\\Plugin' ) ) { + return 'gd-managed-wp'; + } + if ( defined( 'MM_BASE_DIR' ) ) { + return 'bh'; + } + if ( defined( 'IS_PRESSABLE' ) ) { + return 'pressable'; + } + if ( function_exists( 'is_wpe' ) || function_exists( 'is_wpe_snapshot' ) ) { + return 'wpe'; + } + if ( defined( 'VIP_GO_ENV' ) && false !== VIP_GO_ENV ) { + return 'vip-go'; + } + return 'unknown'; + } + + /** + * Return array of allowed REST API post types. + * + * @return array Array of allowed post types. + */ + public static function rest_api_allowed_post_types() { + /** This filter is already documented in class.json-api-endpoints.php */ + return apply_filters( 'rest_api_allowed_post_types', array( 'post', 'page', 'revision' ) ); + } + + /** + * Return array of allowed REST API public metadata. + * + * @return array Array of allowed metadata. + */ + public static function rest_api_allowed_public_metadata() { + /** This filter is documented in json-endpoints/class.wpcom-json-api-post-endpoint.php */ + return apply_filters( 'rest_api_allowed_public_metadata', array() ); + } + + /** + * Finds out if a site is using a version control system. + * + * @return bool + **/ + public static function is_version_controlled() { + + if ( ! class_exists( 'WP_Automatic_Updater' ) ) { + require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; + } + $updater = new \WP_Automatic_Updater(); + + return (bool) strval( $updater->is_vcs_checkout( ABSPATH ) ); + } + + /** + * Returns true if the site has file write access false otherwise. + * + * @return bool + **/ + public static function file_system_write_access() { + if ( ! function_exists( 'get_filesystem_method' ) ) { + require_once ABSPATH . 'wp-admin/includes/file.php'; + } + + require_once ABSPATH . 'wp-admin/includes/template.php'; + + $filesystem_method = get_filesystem_method(); + if ( 'direct' === $filesystem_method ) { + return true; + } + + ob_start(); + + if ( ! function_exists( 'request_filesystem_credentials' ) ) { + require_once ABSPATH . 'wp-admin/includes/file.php'; + } + + $filesystem_credentials_are_stored = request_filesystem_credentials( self_admin_url() ); + ob_end_clean(); + if ( $filesystem_credentials_are_stored ) { + return true; + } + + return false; + } + + /** + * 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 = 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 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 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() ); + } + + /** + * Return URL with a normalized protocol. + * + * @param callable $callable Function 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() ); + $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 ); + } + + /** + * 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 filtered value of get_plugins. + * + * @return mixed|void + */ + public static function get_plugins() { + if ( ! function_exists( 'get_plugins' ) ) { + require_once ABSPATH . 'wp-admin/includes/plugin.php'; + } + + /** This filter is documented in wp-admin/includes/class-wp-plugins-list-table.php */ + return apply_filters( 'all_plugins', get_plugins() ); + } + + /** + * Get custom action link tags that the plugin is using + * Ref: https://codex.wordpress.org/Plugin_API/Filter_Reference/plugin_action_links_(plugin_file_name) + * + * @param string $plugin_file_singular Particular plugin. + * @return array of plugin action links (key: link name value: url) + */ + public static function get_plugins_action_links( $plugin_file_singular = null ) { + // Some sites may have DOM disabled in PHP fail early. + if ( ! class_exists( 'DOMDocument' ) ) { + return array(); + } + $plugins_action_links = get_option( 'jetpack_plugin_api_action_links', array() ); + if ( ! empty( $plugins_action_links ) ) { + if ( is_null( $plugin_file_singular ) ) { + return $plugins_action_links; + } + return ( isset( $plugins_action_links[ $plugin_file_singular ] ) ? $plugins_action_links[ $plugin_file_singular ] : null ); + } + return array(); + } + + /** + * Return the WP version as defined in the $wp_version global. + * + * @return string + */ + public static function wp_version() { + global $wp_version; + return $wp_version; + } + + /** + * Return site icon url used on the site. + * + * @param int $size Size of requested icon in pixels. + * @return mixed|string|void + */ + public static function site_icon_url( $size = 512 ) { + $site_icon = get_site_icon_url( $size ); + return $site_icon ? $site_icon : get_option( 'jetpack_site_icon_url' ); + } + + /** + * Return roles registered on the site. + * + * @return array + */ + public static function roles() { + $wp_roles = wp_roles(); + return $wp_roles->roles; + } + + /** + * Determine time zone from WordPress' options "timezone_string" + * and "gmt_offset". + * + * 1. Check if `timezone_string` is set and return it. + * 2. Check if `gmt_offset` is set, formats UTC-offset from it and return it. + * 3. Default to "UTC+0" if nothing is set. + * + * Note: This function is specifically not using wp_timezone() to keep consistency with + * the existing formatting of the timezone string. + * + * @return string + */ + public static function get_timezone() { + $timezone_string = get_option( 'timezone_string' ); + + if ( ! empty( $timezone_string ) ) { + return str_replace( '_', ' ', $timezone_string ); + } + + $gmt_offset = get_option( 'gmt_offset', 0 ); + + $formatted_gmt_offset = sprintf( '%+g', floatval( $gmt_offset ) ); + + $formatted_gmt_offset = str_replace( + array( '.25', '.5', '.75' ), + array( ':15', ':30', ':45' ), + (string) $formatted_gmt_offset + ); + + /* translators: %s is UTC offset, e.g. "+1" */ + return sprintf( __( 'UTC%s', 'jetpack' ), $formatted_gmt_offset ); + } + + /** + * Return list of paused themes. + * + * @todo Remove function_exists check when WP 5.2 is the minimum. + * + * @return array|bool Array of paused themes or false if unsupported. + */ + public static function get_paused_themes() { + if ( function_exists( 'wp_paused_themes' ) ) { + $paused_themes = wp_paused_themes(); + return $paused_themes->get_all(); + } + return false; + } + + /** + * Return list of paused plugins. + * + * @todo Remove function_exists check when WP 5.2 is the minimum. + * + * @return array|bool Array of paused plugins or false if unsupported. + */ + public static function get_paused_plugins() { + if ( function_exists( 'wp_paused_plugins' ) ) { + $paused_plugins = wp_paused_plugins(); + return $paused_plugins->get_all(); + } + return false; + } +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-sync/src/class-json-deflate-array-codec.php b/plugins/jetpack/vendor/automattic/jetpack-sync/src/class-json-deflate-array-codec.php new file mode 100644 index 00000000..f13e5dcb --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-sync/src/class-json-deflate-array-codec.php @@ -0,0 +1,136 @@ +<?php +/** + * An implementation of Automattic\Jetpack\Sync\Codec_Interface that uses gzip's DEFLATE + * algorithm to compress objects serialized using json_encode. + * + * @package automattic/jetpack-sync + */ + +namespace Automattic\Jetpack\Sync; + +use Automattic\Jetpack\Sync\Codec_Interface; + +/** + * An implementation of Automattic\Jetpack\Sync\Codec_Interface that uses gzip's DEFLATE + * algorithm to compress objects serialized using json_encode + */ +class JSON_Deflate_Array_Codec implements Codec_Interface { + const CODEC_NAME = 'deflate-json-array'; + + /** + * Return the name of the codec. + * + * @return string + */ + public function name() { + return self::CODEC_NAME; + } + + /** + * Encodes an object. + * + * @param object $object Item to encode. + * @return string + */ + public function encode( $object ) { + return base64_encode( gzdeflate( $this->json_serialize( $object ) ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode + } + + /** + * Decode compressed serialized value. + * + * @param string $input Item to decode. + * @return array|mixed|object + */ + public function decode( $input ) { + return $this->json_unserialize( gzinflate( base64_decode( $input ) ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode + } + + /** + * Serialize JSON + * + * @see https://gist.github.com/muhqu/820694 + * + * @param string $any Value to serialize and wrap. + * + * @return false|string + */ + protected function json_serialize( $any ) { + if ( function_exists( 'jetpack_json_wrap' ) ) { + return wp_json_encode( jetpack_json_wrap( $any ) ); + } + // This prevents fatal error when updating pre 6.0 via the cli command. + return wp_json_encode( $this->json_wrap( $any ) ); + } + + /** + * Unserialize JSON + * + * @param string $str JSON string. + * @return array|object Unwrapped JSON. + */ + protected function json_unserialize( $str ) { + return $this->json_unwrap( json_decode( $str, true ) ); + } + + /** + * Wraps JSON + * + * @param object|array $any Wrapping value. + * @param array $seen_nodes Seen nodes. + * @return array + */ + private function json_wrap( &$any, $seen_nodes = array() ) { + if ( is_object( $any ) ) { + $input = get_object_vars( $any ); + $input['__o'] = 1; + } else { + $input = &$any; + } + + if ( is_array( $input ) ) { + $seen_nodes[] = &$any; + + $return = array(); + + foreach ( $input as $k => &$v ) { + if ( ( is_array( $v ) || is_object( $v ) ) ) { + if ( in_array( $v, $seen_nodes, true ) ) { + continue; + } + $return[ $k ] = $this->json_wrap( $v, $seen_nodes ); + } else { + $return[ $k ] = $v; + } + } + + return $return; + } + + return $any; + } + + /** + * Unwraps a json_decode return. + * + * @param array|object $any json_decode object. + * @return array|object + */ + private function json_unwrap( $any ) { + if ( is_array( $any ) ) { + foreach ( $any as $k => $v ) { + if ( '__o' === $k ) { + continue; + } + $any[ $k ] = $this->json_unwrap( $v ); + } + + if ( isset( $any['__o'] ) ) { + unset( $any['__o'] ); + $any = (object) $any; + } + } + + return $any; + } +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-sync/src/class-listener.php b/plugins/jetpack/vendor/automattic/jetpack-sync/src/class-listener.php new file mode 100644 index 00000000..8073e11b --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-sync/src/class-listener.php @@ -0,0 +1,442 @@ +<?php +/** + * Jetpack's Sync Listener + * + * @package automattic/jetpack-sync + */ + +namespace Automattic\Jetpack\Sync; + +use Automattic\Jetpack\Roles; + +/** + * This class monitors actions and logs them to the queue to be sent. + */ +class Listener { + const QUEUE_STATE_CHECK_TRANSIENT = 'jetpack_sync_last_checked_queue_state'; + const QUEUE_STATE_CHECK_TIMEOUT = 300; // 5 minutes. + + /** + * Sync queue. + * + * @var object + */ + private $sync_queue; + + /** + * Full sync queue. + * + * @var object + */ + private $full_sync_queue; + + /** + * Sync queue size limit. + * + * @var int size limit. + */ + private $sync_queue_size_limit; + + /** + * Sync queue lag limit. + * + * @var int Lag limit. + */ + private $sync_queue_lag_limit; + + /** + * Singleton implementation. + * + * @var Listener + */ + private static $instance; + + /** + * Get the Listener instance. + * + * @return Listener + */ + public static function get_instance() { + if ( null === self::$instance ) { + self::$instance = new self(); + } + + return self::$instance; + } + + /** + * Listener constructor. + * + * This is necessary because you can't use "new" when you declare instance properties >:( + */ + protected function __construct() { + Main::init(); + $this->set_defaults(); + $this->init(); + } + + /** + * Sync Listener init. + */ + private function init() { + $handler = array( $this, 'action_handler' ); + $full_sync_handler = array( $this, 'full_sync_action_handler' ); + + foreach ( Modules::get_modules() as $module ) { + $module->init_listeners( $handler ); + $module->init_full_sync_listeners( $full_sync_handler ); + } + + // Module Activation. + add_action( 'jetpack_activate_module', $handler ); + add_action( 'jetpack_deactivate_module', $handler ); + + // Jetpack Upgrade. + add_action( 'updating_jetpack_version', $handler, 10, 2 ); + + // Send periodic checksum. + add_action( 'jetpack_sync_checksum', $handler ); + } + + /** + * Get incremental sync queue. + */ + public function get_sync_queue() { + return $this->sync_queue; + } + + /** + * Gets the full sync queue. + */ + public function get_full_sync_queue() { + return $this->full_sync_queue; + } + + /** + * Sets queue size limit. + * + * @param int $limit Queue size limit. + */ + public function set_queue_size_limit( $limit ) { + $this->sync_queue_size_limit = $limit; + } + + /** + * Get queue size limit. + */ + public function get_queue_size_limit() { + return $this->sync_queue_size_limit; + } + + /** + * Sets the queue lag limit. + * + * @param int $age Queue lag limit. + */ + public function set_queue_lag_limit( $age ) { + $this->sync_queue_lag_limit = $age; + } + + /** + * Return value of queue lag limit. + */ + public function get_queue_lag_limit() { + return $this->sync_queue_lag_limit; + } + + /** + * Force a recheck of the queue limit. + */ + public function force_recheck_queue_limit() { + delete_transient( self::QUEUE_STATE_CHECK_TRANSIENT . '_' . $this->sync_queue->id ); + delete_transient( self::QUEUE_STATE_CHECK_TRANSIENT . '_' . $this->full_sync_queue->id ); + } + + /** + * Determine if an item can be added to the queue. + * + * Prevent adding items to the queue if it hasn't sent an item for 15 mins + * AND the queue is over 1000 items long (by default). + * + * @param object $queue Sync queue. + * @return bool + */ + public function can_add_to_queue( $queue ) { + if ( ! Settings::is_sync_enabled() ) { + return false; + } + + $state_transient_name = self::QUEUE_STATE_CHECK_TRANSIENT . '_' . $queue->id; + + $queue_state = get_transient( $state_transient_name ); + + if ( false === $queue_state ) { + $queue_state = array( $queue->size(), $queue->lag() ); + set_transient( $state_transient_name, $queue_state, self::QUEUE_STATE_CHECK_TIMEOUT ); + } + + list( $queue_size, $queue_age ) = $queue_state; + + return ( $queue_age < $this->sync_queue_lag_limit ) + || + ( ( $queue_size + 1 ) < $this->sync_queue_size_limit ); + } + + /** + * Full sync action handler. + * + * @param mixed ...$args Args passed to the action. + */ + public function full_sync_action_handler( ...$args ) { + $this->enqueue_action( current_filter(), $args, $this->full_sync_queue ); + } + + /** + * Action handler. + * + * @param mixed ...$args Args passed to the action. + */ + public function action_handler( ...$args ) { + $this->enqueue_action( current_filter(), $args, $this->sync_queue ); + } + + // add many actions to the queue directly, without invoking them. + + /** + * Bulk add action to the queue. + * + * @param string $action_name The name the full sync action. + * @param array $args_array Array of chunked arguments. + */ + public function bulk_enqueue_full_sync_actions( $action_name, $args_array ) { + $queue = $this->get_full_sync_queue(); + + /* + * If we add any items to the queue, we should try to ensure that our script + * can't be killed before they are sent. + */ + if ( function_exists( 'ignore_user_abort' ) ) { + ignore_user_abort( true ); + } + + $data_to_enqueue = array(); + $user_id = get_current_user_id(); + $currtime = microtime( true ); + $is_importing = Settings::is_importing(); + + foreach ( $args_array as $args ) { + $previous_end = isset( $args['previous_end'] ) ? $args['previous_end'] : null; + $args = isset( $args['ids'] ) ? $args['ids'] : $args; + + /** + * Modify or reject the data within an action before it is enqueued locally. + * + * @since 4.2.0 + * + * @module sync + * + * @param array The action parameters + */ + $args = apply_filters( "jetpack_sync_before_enqueue_$action_name", $args ); + $action_data = array( $args ); + if ( ! is_null( $previous_end ) ) { + $action_data[] = $previous_end; + } + // allow listeners to abort. + if ( false === $args ) { + continue; + } + + $data_to_enqueue[] = array( + $action_name, + $action_data, + $user_id, + $currtime, + $is_importing, + ); + } + + $queue->add_all( $data_to_enqueue ); + } + + /** + * Enqueue the action. + * + * @param string $current_filter Current WordPress filter. + * @param object $args Sync args. + * @param string $queue Sync queue. + */ + public function enqueue_action( $current_filter, $args, $queue ) { + // don't enqueue an action during the outbound http request - this prevents recursion. + if ( Settings::is_sending() ) { + return; + } + + /** + * Add an action hook to execute when anything on the whitelist gets sent to the queue to sync. + * + * @module sync + * + * @since 5.9.0 + */ + do_action( 'jetpack_sync_action_before_enqueue' ); + + /** + * Modify or reject the data within an action before it is enqueued locally. + * + * @since 4.2.0 + * + * @param array The action parameters + */ + $args = apply_filters( "jetpack_sync_before_enqueue_$current_filter", $args ); + + // allow listeners to abort. + if ( false === $args ) { + return; + } + + /* + * Periodically check the size of the queue, and disable adding to it if + * it exceeds some limit AND the oldest item exceeds the age limit (i.e. sending has stopped). + */ + if ( ! $this->can_add_to_queue( $queue ) ) { + return; + } + + /* + * If we add any items to the queue, we should try to ensure that our script + * can't be killed before they are sent. + */ + if ( function_exists( 'ignore_user_abort' ) ) { + ignore_user_abort( true ); + } + + if ( + 'sync' === $queue->id || + in_array( + $current_filter, + array( + 'jetpack_full_sync_start', + 'jetpack_full_sync_end', + 'jetpack_full_sync_cancel', + ), + true + ) + ) { + $queue->add( + array( + $current_filter, + $args, + get_current_user_id(), + microtime( true ), + Settings::is_importing(), + $this->get_actor( $current_filter, $args ), + ) + ); + } else { + $queue->add( + array( + $current_filter, + $args, + get_current_user_id(), + microtime( true ), + Settings::is_importing(), + ) + ); + } + + // since we've added some items, let's try to load the sender so we can send them as quickly as possible. + if ( ! Actions::$sender ) { + add_filter( 'jetpack_sync_sender_should_load', '__return_true' ); + if ( did_action( 'init' ) ) { + Actions::add_sender_shutdown(); + } + } + } + + /** + * Get the event's actor. + * + * @param string $current_filter Current wp-admin page. + * @param object $args Sync event. + * @return array Actor information. + */ + public function get_actor( $current_filter, $args ) { + if ( 'wp_login' === $current_filter ) { + $user = get_user_by( 'ID', $args[1]->data->ID ); + } else { + $user = wp_get_current_user(); + } + + $roles = new Roles(); + $translated_role = $roles->translate_user_to_role( $user ); + + $actor = array( + 'wpcom_user_id' => null, + 'external_user_id' => isset( $user->ID ) ? $user->ID : null, + 'display_name' => isset( $user->display_name ) ? $user->display_name : null, + 'user_email' => isset( $user->user_email ) ? $user->user_email : null, + 'user_roles' => isset( $user->roles ) ? $user->roles : null, + 'translated_role' => $translated_role ? $translated_role : null, + 'is_cron' => defined( 'DOING_CRON' ) ? DOING_CRON : false, + 'is_rest' => defined( 'REST_API_REQUEST' ) ? REST_API_REQUEST : false, + 'is_xmlrpc' => defined( 'XMLRPC_REQUEST' ) ? XMLRPC_REQUEST : false, + 'is_wp_rest' => defined( 'REST_REQUEST' ) ? REST_REQUEST : false, + 'is_ajax' => defined( 'DOING_AJAX' ) ? DOING_AJAX : false, + 'is_wp_admin' => is_admin(), + 'is_cli' => defined( 'WP_CLI' ) ? WP_CLI : false, + 'from_url' => $this->get_request_url(), + ); + + if ( $this->should_send_user_data_with_actor( $current_filter ) ) { + require_once JETPACK__PLUGIN_DIR . 'modules/protect/shared-functions.php'; + $actor['ip'] = jetpack_protect_get_ip(); + $actor['user_agent'] = isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : 'unknown'; + } + + return $actor; + } + + /** + * Should user data be sent as the actor? + * + * @param string $current_filter The current WordPress filter being executed. + * @return bool + */ + public function should_send_user_data_with_actor( $current_filter ) { + $should_send = in_array( $current_filter, array( 'jetpack_wp_login', 'wp_logout', 'jetpack_valid_failed_login_attempt' ), true ); + /** + * Allow or deny sending actor's user data ( IP and UA ) during a sync event + * + * @since 5.8.0 + * + * @module sync + * + * @param bool True if we should send user data + * @param string The current filter that is performing the sync action + */ + return apply_filters( 'jetpack_sync_actor_user_data', $should_send, $current_filter ); + } + + /** + * Sets Listener defaults. + */ + public function set_defaults() { + $this->sync_queue = new Queue( 'sync' ); + $this->full_sync_queue = new Queue( 'full_sync' ); + $this->set_queue_size_limit( Settings::get_setting( 'max_queue_size' ) ); + $this->set_queue_lag_limit( Settings::get_setting( 'max_queue_lag' ) ); + } + + /** + * Get the request URL. + * + * @return string Request URL, if known. Otherwise, wp-admin or home_url. + */ + public function get_request_url() { + if ( isset( $_SERVER['HTTP_HOST'], $_SERVER['REQUEST_URI'] ) ) { + return 'http' . ( isset( $_SERVER['HTTPS'] ) ? 's' : '' ) . '://' . "{$_SERVER['HTTP_HOST']}{$_SERVER['REQUEST_URI']}"; + } + return is_admin() ? get_admin_url( get_current_blog_id() ) : home_url(); + } +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-sync/src/class-lock.php b/plugins/jetpack/vendor/automattic/jetpack-sync/src/class-lock.php new file mode 100644 index 00000000..84d87bc8 --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-sync/src/class-lock.php @@ -0,0 +1,65 @@ +<?php +/** + * Lock class. + * + * @package automattic/jetpack-sync + */ + +namespace Automattic\Jetpack\Sync; + +/** + * Lock class + */ +class Lock { + /** + * Prefix of the blog lock transient. + * + * @access public + * + * @var string + */ + const LOCK_PREFIX = 'jp_sync_lock_'; + + /** + * Default Lifetime of the lock. + * + * @access public + * + * @var int + */ + const LOCK_TRANSIENT_EXPIRY = 15; // Seconds. + + /** + * Attempt to lock. + * + * @access public + * + * @param string $name lock name. + * @param int $expiry lock duration in seconds. + * + * @return boolean True if succeeded, false otherwise. + */ + public function attempt( $name, $expiry = self::LOCK_TRANSIENT_EXPIRY ) { + $name = self::LOCK_PREFIX . $name; + $locked_time = get_option( $name ); + if ( $locked_time ) { + if ( microtime( true ) < $locked_time ) { + return false; + } + } + update_option( $name, microtime( true ) + $expiry ); + + return true; + } + + /** + * Remove the lock. + * + * @access public + * + * @param string $name lock name. + */ + public function remove( $name ) { + delete_option( self::LOCK_PREFIX . $name ); + } +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-sync/src/class-main.php b/plugins/jetpack/vendor/automattic/jetpack-sync/src/class-main.php new file mode 100644 index 00000000..2e1c3cbd --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-sync/src/class-main.php @@ -0,0 +1,34 @@ +<?php +/** + * This class hooks the main sync actions. + * + * @package automattic/jetpack-sync + */ + +namespace Automattic\Jetpack\Sync; + +/** + * Jetpack Sync main class. + */ +class Main { + /** + * Initialize the main sync actions. + */ + public static function init() { + // Check for WooCommerce support. + add_action( 'plugins_loaded', array( 'Automattic\\Jetpack\\Sync\\Actions', 'initialize_woocommerce' ), 5 ); + + // Check for WP Super Cache. + add_action( 'plugins_loaded', array( 'Automattic\\Jetpack\\Sync\\Actions', 'initialize_wp_super_cache' ), 5 ); + + /* + * Init after plugins loaded and before the `init` action. This helps with issues where plugins init + * with a high priority or sites that use alternate cron. + */ + add_action( 'plugins_loaded', array( 'Automattic\\Jetpack\\Sync\\Actions', 'init' ), 90 ); + + // We need to define this here so that it's hooked before `updating_jetpack_version` is called. + add_action( 'updating_jetpack_version', array( 'Automattic\\Jetpack\\Sync\\Actions', 'cleanup_on_upgrade' ), 10, 2 ); + add_action( 'jetpack_user_authorized', array( 'Automattic\\Jetpack\\Sync\\Actions', 'do_initial_sync' ), 10, 0 ); + } +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-sync/src/class-modules.php b/plugins/jetpack/vendor/automattic/jetpack-sync/src/class-modules.php new file mode 100644 index 00000000..1aaddb9a --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-sync/src/class-modules.php @@ -0,0 +1,207 @@ +<?php +/** + * Simple wrapper that allows enumerating cached static instances + * of sync modules. + * + * @package automattic/jetpack-sync + */ + +namespace Automattic\Jetpack\Sync; + +/** + * A class to handle loading of sync modules. + */ +class Modules { + + /** + * Lists classnames of sync modules we load by default. + * + * @access public + * + * @var array + */ + const DEFAULT_SYNC_MODULES = array( + 'Jetpack_Sync_Modules_Constants', + 'Jetpack_Sync_Modules_Callables', + 'Jetpack_Sync_Modules_Network_Options', + 'Jetpack_Sync_Modules_Options', + 'Jetpack_Sync_Modules_Terms', + 'Jetpack_Sync_Modules_Menus', + 'Jetpack_Sync_Modules_Themes', + 'Jetpack_Sync_Modules_Users', + 'Jetpack_Sync_Modules_Import', + 'Jetpack_Sync_Modules_Posts', + 'Jetpack_Sync_Modules_Protect', + 'Jetpack_Sync_Modules_Comments', + 'Jetpack_Sync_Modules_Updates', + 'Jetpack_Sync_Modules_Attachments', + 'Jetpack_Sync_Modules_Meta', + 'Jetpack_Sync_Modules_Plugins', + 'Jetpack_Sync_Modules_Stats', + 'Jetpack_Sync_Modules_Full_Sync', + 'Automattic\\Jetpack\\Sync\\Modules\\Term_Relationships', + ); + + /** + * Maps classnames of sync modules before to v7.5 to classnames of sync modules after v7.5. + * + * @access public + * + * @var array + */ + const LEGACY_SYNC_MODULES_MAP = array( + 'Jetpack_Sync_Modules_Constants' => 'Automattic\\Jetpack\\Sync\\Modules\\Constants', + 'Jetpack_Sync_Modules_Callables' => 'Automattic\\Jetpack\\Sync\\Modules\\Callables', + 'Jetpack_Sync_Modules_Network_Options' => 'Automattic\\Jetpack\\Sync\\Modules\\Network_Options', + 'Jetpack_Sync_Modules_Options' => 'Automattic\\Jetpack\\Sync\\Modules\\Options', + 'Jetpack_Sync_Modules_Terms' => 'Automattic\\Jetpack\\Sync\\Modules\\Terms', + 'Jetpack_Sync_Modules_Menus' => 'Automattic\\Jetpack\\Sync\\Modules\\Menus', + 'Jetpack_Sync_Modules_Themes' => 'Automattic\\Jetpack\\Sync\\Modules\\Themes', + 'Jetpack_Sync_Modules_Users' => 'Automattic\\Jetpack\\Sync\\Modules\\Users', + 'Jetpack_Sync_Modules_Import' => 'Automattic\\Jetpack\\Sync\\Modules\\Import', + 'Jetpack_Sync_Modules_Posts' => 'Automattic\\Jetpack\\Sync\\Modules\\Posts', + 'Jetpack_Sync_Modules_Protect' => 'Automattic\\Jetpack\\Sync\\Modules\\Protect', + 'Jetpack_Sync_Modules_Comments' => 'Automattic\\Jetpack\\Sync\\Modules\\Comments', + 'Jetpack_Sync_Modules_Updates' => 'Automattic\\Jetpack\\Sync\\Modules\\Updates', + 'Jetpack_Sync_Modules_Attachments' => 'Automattic\\Jetpack\\Sync\\Modules\\Attachments', + 'Jetpack_Sync_Modules_Meta' => 'Automattic\\Jetpack\\Sync\\Modules\\Meta', + 'Jetpack_Sync_Modules_Plugins' => 'Automattic\\Jetpack\\Sync\\Modules\\Plugins', + 'Jetpack_Sync_Modules_Stats' => 'Automattic\\Jetpack\\Sync\\Modules\\Stats', + 'Jetpack_Sync_Modules_Full_Sync' => 'Automattic\\Jetpack\\Sync\\Modules\\Full_Sync', + ); + + /** + * Keeps track of initialized sync modules. + * + * @access private + * @static + * + * @var null|array + */ + private static $initialized_modules = null; + + /** + * Gets a list of initialized modules. + * + * @access public + * @static + * + * @return array|null + */ + public static function get_modules() { + if ( null === self::$initialized_modules ) { + self::$initialized_modules = self::initialize_modules(); + } + + return self::$initialized_modules; + } + + /** + * Sets defaults for all initialized modules. + * + * @access public + * @static + */ + public static function set_defaults() { + foreach ( self::get_modules() as $module ) { + $module->set_defaults(); + } + } + + /** + * Gets the name of an initialized module. Returns false if given module has not been initialized. + * + * @access public + * @static + * + * @param string $module_name A module name. + * + * @return bool|Automattic\Jetpack\Sync\Modules\Module + */ + public static function get_module( $module_name ) { + foreach ( self::get_modules() as $module ) { + if ( $module->name() === $module_name ) { + return $module; + } + } + + return false; + } + + /** + * Loads and sets defaults for all declared modules. + * + * @access public + * @static + * + * @return array + */ + public static function initialize_modules() { + /** + * Filters the list of class names of sync modules. + * If you add to this list, make sure any classes implement the + * Jetpack_Sync_Module interface. + * + * @since 4.2.0 + */ + $modules = apply_filters( 'jetpack_sync_modules', self::DEFAULT_SYNC_MODULES ); + + $modules = array_map( array( 'Automattic\\Jetpack\\Sync\\Modules', 'map_legacy_modules' ), $modules ); + + $modules = array_map( array( 'Automattic\\Jetpack\\Sync\\Modules', 'load_module' ), $modules ); + + return array_map( array( 'Automattic\\Jetpack\\Sync\\Modules', 'set_module_defaults' ), $modules ); + } + + /** + * Returns an instance of the given module class. + * + * @access public + * @static + * + * @param string $module_class The classname of a Jetpack sync module. + * + * @return Automattic\Jetpack\Sync\Modules\Module + */ + public static function load_module( $module_class ) { + return new $module_class(); + } + + /** + * For backwards compat, takes the classname of a given module pre Jetpack 7.5, + * and returns the new namespaced classname. + * + * @access public + * @static + * + * @param string $module_class The classname of a Jetpack sync module. + * + * @return string + */ + public static function map_legacy_modules( $module_class ) { + $legacy_map = self::LEGACY_SYNC_MODULES_MAP; + if ( isset( $legacy_map[ $module_class ] ) ) { + return $legacy_map[ $module_class ]; + } + return $module_class; + } + + /** + * Sets defaults for the given instance of a Jetpack sync module. + * + * @access public + * @static + * + * @param Automattic\Jetpack\Sync\Modules\Module $module Instance of a Jetpack sync module. + * + * @return Automattic\Jetpack\Sync\Modules\Module + */ + public static function set_module_defaults( $module ) { + $module->set_defaults(); + if ( method_exists( $module, 'set_late_default' ) ) { + add_action( 'init', array( $module, 'set_late_default' ), 90 ); + } + return $module; + } + +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-sync/src/class-queue-buffer.php b/plugins/jetpack/vendor/automattic/jetpack-sync/src/class-queue-buffer.php new file mode 100644 index 00000000..a9846150 --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-sync/src/class-queue-buffer.php @@ -0,0 +1,78 @@ +<?php +/** + * Sync queue buffer. + * + * @package automattic/jetpack-sync + */ + +namespace Automattic\Jetpack\Sync; + +/** + * A buffer of items from the queue that can be checked out. + */ +class Queue_Buffer { + /** + * Sync queue buffer ID. + * + * @access public + * + * @var int + */ + public $id; + + /** + * Sync items. + * + * @access public + * + * @var array + */ + public $items_with_ids; + + /** + * Constructor. + * Initializes the queue buffer. + * + * @access public + * + * @param int $id Sync queue buffer ID. + * @param array $items_with_ids Items for the buffer to work with. + */ + public function __construct( $id, $items_with_ids ) { + $this->id = $id; + $this->items_with_ids = $items_with_ids; + } + + /** + * Retrieve the sync items in the buffer, in an ID => value form. + * + * @access public + * + * @return array Sync items in the buffer. + */ + public function get_items() { + return array_combine( $this->get_item_ids(), $this->get_item_values() ); + } + + /** + * Retrieve the values of the sync items in the buffer. + * + * @access public + * + * @return array Sync items values. + */ + public function get_item_values() { + return Utils::get_item_values( $this->items_with_ids ); + } + + /** + * Retrieve the IDs of the sync items in the buffer. + * + * @access public + * + * @return array Sync items IDs. + */ + public function get_item_ids() { + return Utils::get_item_ids( $this->items_with_ids ); + } +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-sync/src/class-queue.php b/plugins/jetpack/vendor/automattic/jetpack-sync/src/class-queue.php new file mode 100644 index 00000000..1ee94a10 --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-sync/src/class-queue.php @@ -0,0 +1,706 @@ +<?php +/** + * The class that describes the Queue for the sync package. + * + * @package automattic/jetpack-sync + */ + +namespace Automattic\Jetpack\Sync; + +/** + * A persistent queue that can be flushed in increments of N items, + * and which blocks reads until checked-out buffers are checked in or + * closed. This uses raw SQL for two reasons: speed, and not triggering + * tons of added_option callbacks. + */ +class Queue { + /** + * The queue id. + * + * @var string + */ + public $id; + /** + * Keeps track of the rows. + * + * @var int + */ + private $row_iterator; + + /** + * Queue constructor. + * + * @param string $id Name of the queue. + */ + public function __construct( $id ) { + $this->id = str_replace( '-', '_', $id ); // Necessary to ensure we don't have ID collisions in the SQL. + $this->row_iterator = 0; + $this->random_int = wp_rand( 1, 1000000 ); + } + + /** + * Add a single item to the queue. + * + * @param object $item Event object to add to queue. + */ + public function add( $item ) { + global $wpdb; + $added = false; + // This basically tries to add the option until enough time has elapsed that + // it has a unique (microtime-based) option key. + while ( ! $added ) { + $rows_added = $wpdb->query( + $wpdb->prepare( + "INSERT INTO $wpdb->options (option_name, option_value, autoload) VALUES (%s, %s,%s)", + $this->get_next_data_row_option_name(), + serialize( $item ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize + 'no' + ) + ); + $added = ( 0 !== $rows_added ); + } + } + + /** + * Insert all the items in a single SQL query. May be subject to query size limits! + * + * @param array $items Array of events to add to the queue. + * + * @return bool|\WP_Error + */ + public function add_all( $items ) { + global $wpdb; + $base_option_name = $this->get_next_data_row_option_name(); + + $query = "INSERT INTO $wpdb->options (option_name, option_value, autoload) VALUES "; + + $rows = array(); + $count_items = count( $items ); + for ( $i = 0; $i < $count_items; ++$i ) { + $option_name = esc_sql( $base_option_name . '-' . $i ); + $option_value = esc_sql( serialize( $items[ $i ] ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize + $rows[] = "('$option_name', '$option_value', 'no')"; + } + + $rows_added = $wpdb->query( $query . join( ',', $rows ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + + if ( count( $items ) === $rows_added ) { + return new \WP_Error( 'row_count_mismatch', "The number of rows inserted didn't match the size of the input array" ); + } + return true; + } + + /** + * Get the front-most item on the queue without checking it out. + * + * @param int $count Number of items to return when looking at the items. + * + * @return array + */ + public function peek( $count = 1 ) { + $items = $this->fetch_items( $count ); + if ( $items ) { + return Utils::get_item_values( $items ); + } + + return array(); + } + + /** + * Gets items with particular IDs. + * + * @param array $item_ids Array of item IDs to retrieve. + * + * @return array + */ + public function peek_by_id( $item_ids ) { + $items = $this->fetch_items_by_id( $item_ids ); + if ( $items ) { + return Utils::get_item_values( $items ); + } + + return array(); + } + + /** + * Gets the queue lag. + * Lag is the difference in time between the age of the oldest item + * (aka first or frontmost item) and the current time. + * + * @param microtime $now The current time in microtime. + * + * @return float|int|mixed|null + */ + public function lag( $now = null ) { + global $wpdb; + + $first_item_name = $wpdb->get_var( + $wpdb->prepare( + "SELECT option_name FROM $wpdb->options WHERE option_name LIKE %s ORDER BY option_name ASC LIMIT 1", + "jpsq_{$this->id}-%" + ) + ); + + if ( ! $first_item_name ) { + return 0; + } + + if ( null === $now ) { + $now = microtime( true ); + } + + // Break apart the item name to get the timestamp. + $matches = null; + if ( preg_match( '/^jpsq_' . $this->id . '-(\d+\.\d+)-/', $first_item_name, $matches ) ) { + return $now - floatval( $matches[1] ); + } else { + return 0; + } + } + + /** + * Resets the queue. + */ + public function reset() { + global $wpdb; + $this->delete_checkout_id(); + $wpdb->query( + $wpdb->prepare( + "DELETE FROM $wpdb->options WHERE option_name LIKE %s", + "jpsq_{$this->id}-%" + ) + ); + } + + /** + * Return the size of the queue. + * + * @return int + */ + public function size() { + global $wpdb; + + return (int) $wpdb->get_var( + $wpdb->prepare( + "SELECT count(*) FROM $wpdb->options WHERE option_name LIKE %s", + "jpsq_{$this->id}-%" + ) + ); + } + + /** + * Lets you know if there is any items in the queue. + * + * We use this peculiar implementation because it's much faster than count(*). + * + * @return bool + */ + public function has_any_items() { + global $wpdb; + $value = $wpdb->get_var( + $wpdb->prepare( + "SELECT exists( SELECT option_name FROM $wpdb->options WHERE option_name LIKE %s )", + "jpsq_{$this->id}-%" + ) + ); + + return ( '1' === $value ); + } + + /** + * Used to checkout the queue. + * + * @param int $buffer_size Size of the buffer to checkout. + * + * @return Automattic\Jetpack\Sync\Queue_Buffer|bool|int|\WP_Error + */ + public function checkout( $buffer_size ) { + if ( $this->get_checkout_id() ) { + return new \WP_Error( 'unclosed_buffer', 'There is an unclosed buffer' ); + } + + $buffer_id = uniqid(); + + $result = $this->set_checkout_id( $buffer_id ); + + if ( ! $result || is_wp_error( $result ) ) { + return $result; + } + + $items = $this->fetch_items( $buffer_size ); + + if ( count( $items ) === 0 ) { + return false; + } + + $buffer = new Queue_Buffer( $buffer_id, array_slice( $items, 0, $buffer_size ) ); + + return $buffer; + } + + /** + * Given a list of items return the items ids. + * + * @param array $items List of item objects. + * + * @return array Ids of the items. + */ + public function get_ids( $items ) { + return array_map( + function( $item ) { + return $item->id; + }, + $items + ); + } + + /** + * Pop elements from the queue. + * + * @param int $limit Number of items to pop from the queue. + * + * @return array|object|null + */ + public function pop( $limit ) { + $items = $this->fetch_items( $limit ); + + $ids = $this->get_ids( $items ); + + $this->delete( $ids ); + + return $items; + } + + /** + * Get the items from the queue with a memory limit. + * + * This checks out rows until it either empties the queue or hits a certain memory limit + * it loads the sizes from the DB first so that it doesn't accidentally + * load more data into memory than it needs to. + * The only way it will load more items than $max_size is if a single queue item + * exceeds the memory limit, but in that case it will send that item by itself. + * + * @param int $max_memory (bytes) Maximum memory threshold. + * @param int $max_buffer_size Maximum buffer size (number of items). + * + * @return Automattic\Jetpack\Sync\Queue_Buffer|bool|int|\WP_Error + */ + public function checkout_with_memory_limit( $max_memory, $max_buffer_size = 500 ) { + if ( $this->get_checkout_id() ) { + return new \WP_Error( 'unclosed_buffer', 'There is an unclosed buffer' ); + } + + $buffer_id = uniqid(); + + $result = $this->set_checkout_id( $buffer_id ); + + if ( ! $result || is_wp_error( $result ) ) { + return $result; + } + + // Get the map of buffer_id -> memory_size. + global $wpdb; + + $items_with_size = $wpdb->get_results( + $wpdb->prepare( + "SELECT option_name AS id, LENGTH(option_value) AS value_size FROM $wpdb->options WHERE option_name LIKE %s ORDER BY option_name ASC LIMIT %d", + "jpsq_{$this->id}-%", + $max_buffer_size + ), + OBJECT + ); + + if ( count( $items_with_size ) === 0 ) { + return false; + } + + $total_memory = 0; + $max_item_id = $items_with_size[0]->id; + $min_item_id = $max_item_id; + + foreach ( $items_with_size as $id => $item_with_size ) { + $total_memory += $item_with_size->value_size; + + // If this is the first item and it exceeds memory, allow loop to continue + // we will exit on the next iteration instead. + if ( $total_memory > $max_memory && $id > 0 ) { + break; + } + + $max_item_id = $item_with_size->id; + } + + $query = $wpdb->prepare( + "SELECT option_name AS id, option_value AS value FROM $wpdb->options WHERE option_name >= %s and option_name <= %s ORDER BY option_name ASC", + $min_item_id, + $max_item_id + ); + + $items = $wpdb->get_results( $query, OBJECT ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + foreach ( $items as $item ) { + $item->value = maybe_unserialize( $item->value ); + } + + if ( count( $items ) === 0 ) { + $this->delete_checkout_id(); + + return false; + } + + $buffer = new Queue_Buffer( $buffer_id, $items ); + + return $buffer; + } + + /** + * Check in the queue. + * + * @param Automattic\Jetpack\Sync\Queue_Buffer $buffer Queue_Buffer object. + * + * @return bool|\WP_Error + */ + public function checkin( $buffer ) { + $is_valid = $this->validate_checkout( $buffer ); + + if ( is_wp_error( $is_valid ) ) { + return $is_valid; + } + + $this->delete_checkout_id(); + + return true; + } + + /** + * Close the buffer. + * + * @param Automattic\Jetpack\Sync\Queue_Buffer $buffer Queue_Buffer object. + * @param null|array $ids_to_remove Ids to remove from the queue. + * + * @return bool|\WP_Error + */ + public function close( $buffer, $ids_to_remove = null ) { + $is_valid = $this->validate_checkout( $buffer ); + + if ( is_wp_error( $is_valid ) ) { + return $is_valid; + } + + $this->delete_checkout_id(); + + // By default clear all items in the buffer. + if ( is_null( $ids_to_remove ) ) { + $ids_to_remove = $buffer->get_item_ids(); + } + + $this->delete( $ids_to_remove ); + + return true; + } + + /** + * Delete elements from the queue. + * + * @param array $ids Ids to delete. + * + * @return bool|int + */ + private function delete( $ids ) { + if ( 0 === count( $ids ) ) { + return 0; + } + global $wpdb; + $sql = "DELETE FROM $wpdb->options WHERE option_name IN (" . implode( ', ', array_fill( 0, count( $ids ), '%s' ) ) . ')'; + $query = call_user_func_array( array( $wpdb, 'prepare' ), array_merge( array( $sql ), $ids ) ); + + return $wpdb->query( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + } + + /** + * Flushes all items from the queue. + * + * @return array + */ + public function flush_all() { + $items = Utils::get_item_values( $this->fetch_items() ); + $this->reset(); + + return $items; + } + + /** + * Get all the items from the queue. + * + * @return array|object|null + */ + public function get_all() { + return $this->fetch_items(); + } + + /** + * Forces Checkin of the queue. + * Use with caution, this could allow multiple processes to delete + * and send from the queue at the same time + */ + public function force_checkin() { + $this->delete_checkout_id(); + } + + /** + * Locks checkouts from the queue + * tries to wait up to $timeout seconds for the queue to be empty. + * + * @param int $timeout The wait time in seconds for the queue to be empty. + * + * @return bool|int|\WP_Error + */ + public function lock( $timeout = 30 ) { + $tries = 0; + + while ( $this->has_any_items() && $tries < $timeout ) { + sleep( 1 ); + ++$tries; + } + + if ( 30 === $tries ) { + return new \WP_Error( 'lock_timeout', 'Timeout waiting for sync queue to empty' ); + } + + if ( $this->get_checkout_id() ) { + return new \WP_Error( 'unclosed_buffer', 'There is an unclosed buffer' ); + } + + // Hopefully this means we can acquire a checkout? + $result = $this->set_checkout_id( 'lock' ); + + if ( ! $result || is_wp_error( $result ) ) { + return $result; + } + + return true; + } + + /** + * Unlocks the queue. + * + * @return bool|int + */ + public function unlock() { + return $this->delete_checkout_id(); + } + + /** + * This option is specifically chosen to, as much as possible, preserve time order + * and minimise the possibility of collisions between multiple processes working + * at the same time. + * + * @return string + */ + protected function generate_option_name_timestamp() { + return sprintf( '%.6f', microtime( true ) ); + } + + /** + * Gets the checkout ID. + * + * @return bool|string + */ + private function get_checkout_id() { + global $wpdb; + $checkout_value = $wpdb->get_var( + $wpdb->prepare( + "SELECT option_value FROM $wpdb->options WHERE option_name = %s", + $this->get_lock_option_name() + ) + ); + + if ( $checkout_value ) { + list( $checkout_id, $timestamp ) = explode( ':', $checkout_value ); + if ( intval( $timestamp ) > time() ) { + return $checkout_id; + } + } + + return false; + } + + /** + * Sets the checkout id. + * + * @param string $checkout_id The ID of the checkout. + * + * @return bool|int + */ + private function set_checkout_id( $checkout_id ) { + global $wpdb; + + $expires = time() + Defaults::$default_sync_queue_lock_timeout; + $updated_num = $wpdb->query( + $wpdb->prepare( + "UPDATE $wpdb->options SET option_value = %s WHERE option_name = %s", + "$checkout_id:$expires", + $this->get_lock_option_name() + ) + ); + + if ( ! $updated_num ) { + $updated_num = $wpdb->query( + $wpdb->prepare( + "INSERT INTO $wpdb->options ( option_name, option_value, autoload ) VALUES ( %s, %s, 'no' )", + $this->get_lock_option_name(), + "$checkout_id:$expires" + ) + ); + } + + return $updated_num; + } + + /** + * Deletes the checkout ID. + * + * @return bool|int + */ + private function delete_checkout_id() { + global $wpdb; + // Rather than delete, which causes fragmentation, we update in place. + return $wpdb->query( + $wpdb->prepare( + "UPDATE $wpdb->options SET option_value = %s WHERE option_name = %s", + '0:0', + $this->get_lock_option_name() + ) + ); + + } + + /** + * Return the lock option name. + * + * @return string + */ + private function get_lock_option_name() { + return "jpsq_{$this->id}_checkout"; + } + + /** + * Return the next data row option name. + * + * @return string + */ + private function get_next_data_row_option_name() { + $timestamp = $this->generate_option_name_timestamp(); + + // Row iterator is used to avoid collisions where we're writing data waaay fast in a single process. + if ( PHP_INT_MAX === $this->row_iterator ) { + $this->row_iterator = 0; + } else { + $this->row_iterator += 1; + } + + return 'jpsq_' . $this->id . '-' . $timestamp . '-' . $this->random_int . '-' . $this->row_iterator; + } + + /** + * Return the items in the queue. + * + * @param null|int $limit Limit to the number of items we fetch at once. + * + * @return array|object|null + */ + private function fetch_items( $limit = null ) { + global $wpdb; + + if ( $limit ) { + $items = $wpdb->get_results( + $wpdb->prepare( + "SELECT option_name AS id, option_value AS value FROM $wpdb->options WHERE option_name LIKE %s ORDER BY option_name ASC LIMIT %d", + "jpsq_{$this->id}-%", + $limit + ), + OBJECT + ); + } else { + $items = $wpdb->get_results( + $wpdb->prepare( + "SELECT option_name AS id, option_value AS value FROM $wpdb->options WHERE option_name LIKE %s ORDER BY option_name ASC", + "jpsq_{$this->id}-%" + ), + OBJECT + ); + } + + return $this->unserialize_values( $items ); + + } + + /** + * Return items with specific ids. + * + * @param array $items_ids Array of event ids. + * + * @return array|object|null + */ + private function fetch_items_by_id( $items_ids ) { + global $wpdb; + + $ids_placeholders = implode( ', ', array_fill( 0, count( $items_ids ), '%s' ) ); + $query_with_placeholders = "SELECT option_name AS id, option_value AS value + FROM $wpdb->options + WHERE option_name IN ( $ids_placeholders )"; + $items = $wpdb->get_results( + $wpdb->prepare( + $query_with_placeholders, // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + $items_ids + ), + OBJECT + ); + + return $this->unserialize_values( $items ); + } + + /** + * Unserialize item values. + * + * @param array $items Events from the Queue to be serialized. + * + * @return mixed + */ + private function unserialize_values( $items ) { + array_walk( + $items, + function( $item ) { + $item->value = maybe_unserialize( $item->value ); + } + ); + + return $items; + + } + + /** + * Return true if the buffer is still valid or an Error other wise. + * + * @param Automattic\Jetpack\Sync\Queue_Buffer $buffer The Queue_Buffer. + * + * @return bool|\WP_Error + */ + private function validate_checkout( $buffer ) { + if ( ! $buffer instanceof Queue_Buffer ) { + return new \WP_Error( 'not_a_buffer', 'You must checkin an instance of Automattic\\Jetpack\\Sync\\Queue_Buffer' ); + } + + $checkout_id = $this->get_checkout_id(); + + if ( ! $checkout_id ) { + return new \WP_Error( 'buffer_not_checked_out', 'There are no checked out buffers' ); + } + + // TODO: change to strict comparison. + if ( $checkout_id != $buffer->id ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison + return new \WP_Error( 'buffer_mismatch', 'The buffer you checked in was not checked out' ); + } + + return true; + } +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-sync/src/class-replicastore.php b/plugins/jetpack/vendor/automattic/jetpack-sync/src/class-replicastore.php new file mode 100644 index 00000000..34a275da --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-sync/src/class-replicastore.php @@ -0,0 +1,1489 @@ +<?php +/** + * Sync replicastore. + * + * @package automattic/jetpack-sync + */ + +namespace Automattic\Jetpack\Sync; + +/** + * An implementation of Replicastore Interface which returns data stored in a WordPress.org DB. + * This is useful to compare values in the local WP DB to values in the synced replica store + */ +class Replicastore implements Replicastore_Interface { + /** + * Empty and reset the replicastore. + * + * @access public + */ + public function reset() { + global $wpdb; + + $wpdb->query( "DELETE FROM $wpdb->posts" ); + $wpdb->query( "DELETE FROM $wpdb->comments" ); + + // Also need to delete terms from cache. + $term_ids = $wpdb->get_col( "SELECT term_id FROM $wpdb->terms" ); + foreach ( $term_ids as $term_id ) { + wp_cache_delete( $term_id, 'terms' ); + } + + $wpdb->query( "DELETE FROM $wpdb->terms" ); + + $wpdb->query( "DELETE FROM $wpdb->term_taxonomy" ); + $wpdb->query( "DELETE FROM $wpdb->term_relationships" ); + + // Callables and constants. + $wpdb->query( "DELETE FROM $wpdb->options WHERE option_name LIKE 'jetpack_%'" ); + $wpdb->query( "DELETE FROM $wpdb->postmeta WHERE meta_key NOT LIKE '\_%'" ); + } + + /** + * Ran when full sync has just started. + * + * @access public + * + * @param array $config Full sync configuration for this sync module. + */ + public function full_sync_start( $config ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable + $this->reset(); + } + + /** + * Ran when full sync has just finished. + * + * @access public + * + * @param string $checksum Deprecated since 7.3.0. + */ + public function full_sync_end( $checksum ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable + // Noop right now. + } + + /** + * Retrieve the number of terms. + * + * @access public + * + * @return int Number of terms. + */ + public function term_count() { + global $wpdb; + return $wpdb->get_var( "SELECT COUNT(*) FROM $wpdb->terms" ); + } + + /** + * Retrieve the number of rows in the `term_taxonomy` table. + * + * @access public + * + * @return int Number of terms. + */ + public function term_taxonomy_count() { + global $wpdb; + return $wpdb->get_var( "SELECT COUNT(*) FROM $wpdb->term_taxonomy" ); + } + + /** + * Retrieve the number of term relationships. + * + * @access public + * + * @return int Number of rows in the term relationships table. + */ + public function term_relationship_count() { + global $wpdb; + return $wpdb->get_var( "SELECT COUNT(*) FROM $wpdb->term_relationships" ); + } + + /** + * Retrieve the number of posts with a particular post status within a certain range. + * + * @access public + * + * @todo Prepare the SQL query before executing it. + * + * @param string $status Post status. + * @param int $min_id Minimum post ID. + * @param int $max_id Maximum post ID. + * @return int Number of posts. + */ + public function post_count( $status = null, $min_id = null, $max_id = null ) { + global $wpdb; + + $where = ''; + + if ( $status ) { + $where = "post_status = '" . esc_sql( $status ) . "'"; + } else { + $where = '1=1'; + } + + if ( ! empty( $min_id ) ) { + $where .= ' AND ID >= ' . intval( $min_id ); + } + + if ( ! empty( $max_id ) ) { + $where .= ' AND ID <= ' . intval( $max_id ); + } + + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + return $wpdb->get_var( "SELECT COUNT(*) FROM $wpdb->posts WHERE $where" ); + } + + /** + * Retrieve the posts with a particular post status. + * + * @access public + * + * @todo Implement range and actually use max_id/min_id arguments. + * + * @param string $status Post status. + * @param int $min_id Minimum post ID. + * @param int $max_id Maximum post ID. + * @return array Array of posts. + */ + public function get_posts( $status = null, $min_id = null, $max_id = null ) { + $args = array( + 'orderby' => 'ID', + 'posts_per_page' => -1, + ); + + if ( $status ) { + $args['post_status'] = $status; + } else { + $args['post_status'] = 'any'; + } + + return get_posts( $args ); + } + + /** + * Retrieve a post object by the post ID. + * + * @access public + * + * @param int $id Post ID. + * @return \WP_Post Post object. + */ + public function get_post( $id ) { + return get_post( $id ); + } + + /** + * Update or insert a post. + * + * @access public + * + * @param \WP_Post $post Post object. + * @param bool $silent Whether to perform a silent action. Not used in this implementation. + */ + public function upsert_post( $post, $silent = false ) { + global $wpdb; + + // Reject the post if it's not a \WP_Post. + if ( ! $post instanceof \WP_Post ) { + return; + } + + $post = $post->to_array(); + + // Reject posts without an ID. + if ( ! isset( $post['ID'] ) ) { + return; + } + + $now = current_time( 'mysql' ); + $now_gmt = get_gmt_from_date( $now ); + + $defaults = array( + 'ID' => 0, + 'post_author' => '0', + 'post_content' => '', + 'post_content_filtered' => '', + 'post_title' => '', + 'post_name' => '', + 'post_excerpt' => '', + 'post_status' => 'draft', + 'post_type' => 'post', + 'comment_status' => 'closed', + 'comment_count' => '0', + 'ping_status' => '', + 'post_password' => '', + 'to_ping' => '', + 'pinged' => '', + 'post_parent' => 0, + 'menu_order' => 0, + 'guid' => '', + 'post_date' => $now, + 'post_date_gmt' => $now_gmt, + 'post_modified' => $now, + 'post_modified_gmt' => $now_gmt, + ); + + $post = array_intersect_key( $post, $defaults ); + + $post = sanitize_post( $post, 'db' ); + + unset( $post['filter'] ); + + $exists = $wpdb->get_var( $wpdb->prepare( "SELECT EXISTS( SELECT 1 FROM $wpdb->posts WHERE ID = %d )", $post['ID'] ) ); + + if ( $exists ) { + $wpdb->update( $wpdb->posts, $post, array( 'ID' => $post['ID'] ) ); + } else { + $wpdb->insert( $wpdb->posts, $post ); + } + + clean_post_cache( $post['ID'] ); + } + + /** + * Delete a post by the post ID. + * + * @access public + * + * @param int $post_id Post ID. + */ + public function delete_post( $post_id ) { + wp_delete_post( $post_id, true ); + } + + /** + * Retrieve the checksum for posts within a range. + * + * @access public + * + * @param int $min_id Minimum post ID. + * @param int $max_id Maximum post ID. + * @return int The checksum. + */ + public function posts_checksum( $min_id = null, $max_id = null ) { + global $wpdb; + return $this->table_checksum( $wpdb->posts, Defaults::$default_post_checksum_columns, 'ID', Settings::get_blacklisted_post_types_sql(), $min_id, $max_id ); + } + + /** + * Retrieve the checksum for post meta within a range. + * + * @access public + * + * @param int $min_id Minimum post meta ID. + * @param int $max_id Maximum post meta ID. + * @return int The checksum. + */ + public function post_meta_checksum( $min_id = null, $max_id = null ) { + global $wpdb; + return $this->table_checksum( $wpdb->postmeta, Defaults::$default_post_meta_checksum_columns, 'meta_id', Settings::get_whitelisted_post_meta_sql(), $min_id, $max_id ); + } + + /** + * Retrieve the number of comments with a particular comment status within a certain range. + * + * @access public + * + * @todo Prepare the SQL query before executing it. + * + * @param string $status Comment status. + * @param int $min_id Minimum comment ID. + * @param int $max_id Maximum comment ID. + * @return int Number of comments. + */ + public function comment_count( $status = null, $min_id = null, $max_id = null ) { + global $wpdb; + + $comment_approved = $this->comment_status_to_approval_value( $status ); + + if ( false !== $comment_approved ) { + $where = "comment_approved = '" . esc_sql( $comment_approved ) . "'"; + } else { + $where = '1=1'; + } + + if ( ! empty( $min_id ) ) { + $where .= ' AND comment_ID >= ' . intval( $min_id ); + } + + if ( ! empty( $max_id ) ) { + $where .= ' AND comment_ID <= ' . intval( $max_id ); + } + + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + return $wpdb->get_var( "SELECT COUNT(*) FROM $wpdb->comments WHERE $where" ); + } + + /** + * Translate a comment status to a value of the comment_approved field. + * + * @access private + * + * @param string $status Comment status. + * @return string|bool New comment_approved value, false if the status doesn't affect it. + */ + private function comment_status_to_approval_value( $status ) { + switch ( $status ) { + case 'approve': + return '1'; + case 'hold': + return '0'; + case 'spam': + return 'spam'; + case 'trash': + return 'trash'; + case 'any': + return false; + case 'all': + return false; + default: + return false; + } + } + + /** + * Retrieve the comments with a particular comment status. + * + * @access public + * + * @todo Implement range and actually use max_id/min_id arguments. + * + * @param string $status Comment status. + * @param int $min_id Minimum comment ID. + * @param int $max_id Maximum comment ID. + * @return array Array of comments. + */ + public function get_comments( $status = null, $min_id = null, $max_id = null ) { + $args = array( + 'orderby' => 'ID', + 'status' => 'all', + ); + + if ( $status ) { + $args['status'] = $status; + } + + return get_comments( $args ); + } + + /** + * Retrieve a comment object by the comment ID. + * + * @access public + * + * @param int $id Comment ID. + * @return \WP_Comment Comment object. + */ + public function get_comment( $id ) { + return \WP_Comment::get_instance( $id ); + } + + /** + * Update or insert a comment. + * + * @access public + * + * @param \WP_Comment $comment Comment object. + */ + public function upsert_comment( $comment ) { + global $wpdb; + + $comment = $comment->to_array(); + + // Filter by fields on comment table. + $comment_fields_whitelist = array( + 'comment_ID', + 'comment_post_ID', + 'comment_author', + 'comment_author_email', + 'comment_author_url', + 'comment_author_IP', + 'comment_date', + 'comment_date_gmt', + 'comment_content', + 'comment_karma', + 'comment_approved', + 'comment_agent', + 'comment_type', + 'comment_parent', + 'user_id', + ); + + foreach ( $comment as $key => $value ) { + if ( ! in_array( $key, $comment_fields_whitelist, true ) ) { + unset( $comment[ $key ] ); + } + } + + $exists = $wpdb->get_var( + $wpdb->prepare( + "SELECT EXISTS( SELECT 1 FROM $wpdb->comments WHERE comment_ID = %d )", + $comment['comment_ID'] + ) + ); + + if ( $exists ) { + $wpdb->update( $wpdb->comments, $comment, array( 'comment_ID' => $comment['comment_ID'] ) ); + } else { + $wpdb->insert( $wpdb->comments, $comment ); + } + + wp_update_comment_count( $comment['comment_post_ID'] ); + } + + /** + * Trash a comment by the comment ID. + * + * @access public + * + * @param int $comment_id Comment ID. + */ + public function trash_comment( $comment_id ) { + wp_delete_comment( $comment_id ); + } + + /** + * Delete a comment by the comment ID. + * + * @access public + * + * @param int $comment_id Comment ID. + */ + public function delete_comment( $comment_id ) { + wp_delete_comment( $comment_id, true ); + } + + /** + * Mark a comment by the comment ID as spam. + * + * @access public + * + * @param int $comment_id Comment ID. + */ + public function spam_comment( $comment_id ) { + wp_spam_comment( $comment_id ); + } + + /** + * Trash the comments of a post. + * + * @access public + * + * @param int $post_id Post ID. + * @param array $statuses Post statuses. Not used in this implementation. + */ + public function trashed_post_comments( $post_id, $statuses ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable + wp_trash_post_comments( $post_id ); + } + + /** + * Untrash the comments of a post. + * + * @access public + * + * @param int $post_id Post ID. + */ + public function untrashed_post_comments( $post_id ) { + wp_untrash_post_comments( $post_id ); + } + + /** + * Retrieve the checksum for comments within a range. + * + * @access public + * + * @param int $min_id Minimum comment ID. + * @param int $max_id Maximum comment ID. + * @return int The checksum. + */ + public function comments_checksum( $min_id = null, $max_id = null ) { + global $wpdb; + return $this->table_checksum( $wpdb->comments, Defaults::$default_comment_checksum_columns, 'comment_ID', Settings::get_comments_filter_sql(), $min_id, $max_id ); + } + + /** + * Retrieve the checksum for comment meta within a range. + * + * @access public + * + * @param int $min_id Minimum comment meta ID. + * @param int $max_id Maximum comment meta ID. + * @return int The checksum. + */ + public function comment_meta_checksum( $min_id = null, $max_id = null ) { + global $wpdb; + return $this->table_checksum( $wpdb->commentmeta, Defaults::$default_comment_meta_checksum_columns, 'meta_id', Settings::get_whitelisted_comment_meta_sql(), $min_id, $max_id ); + } + + /** + * Retrieve the checksum for all options. + * + * @access public + * + * @return int The checksum. + */ + public function options_checksum() { + global $wpdb; + $options_whitelist = "'" . implode( "', '", Defaults::$default_options_whitelist ) . "'"; + $where_sql = "option_name IN ( $options_whitelist )"; + + return $this->table_checksum( $wpdb->options, Defaults::$default_option_checksum_columns, null, $where_sql, null, null ); + } + + /** + * Update the value of an option. + * + * @access public + * + * @param string $option Option name. + * @param mixed $value Option value. + * @return bool False if value was not updated and true if value was updated. + */ + public function update_option( $option, $value ) { + return update_option( $option, $value ); + } + + /** + * Retrieve an option value based on an option name. + * + * @access public + * + * @param string $option Name of option to retrieve. + * @param mixed $default Optional. Default value to return if the option does not exist. + * @return mixed Value set for the option. + */ + public function get_option( $option, $default = false ) { + return get_option( $option, $default ); + } + + /** + * Remove an option by name. + * + * @access public + * + * @param string $option Name of option to remove. + * @return bool True, if option is successfully deleted. False on failure. + */ + public function delete_option( $option ) { + return delete_option( $option ); + } + + /** + * Change the features that the current theme supports. + * Intentionally not implemented in this replicastore. + * + * @access public + * + * @param array $theme_support Features that the theme supports. + */ + public function set_theme_support( $theme_support ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable + // Noop. + } + + /** + * Whether the current theme supports a certain feature. + * + * @access public + * + * @param string $feature Name of the feature. + */ + public function current_theme_supports( $feature ) { + return current_theme_supports( $feature ); + } + + /** + * Retrieve metadata for the specified object. + * + * @access public + * + * @param string $type Meta type. + * @param int $object_id ID of the object. + * @param string $meta_key Meta key. + * @param bool $single If true, return only the first value of the specified meta_key. + * + * @return mixed Single metadata value, or array of values. + */ + public function get_metadata( $type, $object_id, $meta_key = '', $single = false ) { + return get_metadata( $type, $object_id, $meta_key, $single ); + } + + /** + * Stores remote meta key/values alongside an ID mapping key. + * + * @access public + * + * @todo Refactor to not use interpolated values when preparing the SQL query. + * + * @param string $type Meta type. + * @param int $object_id ID of the object. + * @param string $meta_key Meta key. + * @param mixed $meta_value Meta value. + * @param int $meta_id ID of the meta. + * + * @return bool False if meta table does not exist, true otherwise. + */ + public function upsert_metadata( $type, $object_id, $meta_key, $meta_value, $meta_id ) { + $table = _get_meta_table( $type ); + if ( ! $table ) { + return false; + } + + global $wpdb; + + $exists = $wpdb->get_var( + $wpdb->prepare( + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + "SELECT EXISTS( SELECT 1 FROM $table WHERE meta_id = %d )", + $meta_id + ) + ); + + if ( $exists ) { + $wpdb->update( + $table, + array( + 'meta_key' => $meta_key, + 'meta_value' => maybe_serialize( $meta_value ), + ), + array( 'meta_id' => $meta_id ) + ); + } else { + $object_id_field = $type . '_id'; + $wpdb->insert( + $table, + array( + 'meta_id' => $meta_id, + $object_id_field => $object_id, + 'meta_key' => $meta_key, + 'meta_value' => maybe_serialize( $meta_value ), + ) + ); + } + + wp_cache_delete( $object_id, $type . '_meta' ); + + return true; + } + + /** + * Delete metadata for the specified object. + * + * @access public + * + * @todo Refactor to not use interpolated values when preparing the SQL query. + * + * @param string $type Meta type. + * @param int $object_id ID of the object. + * @param array $meta_ids IDs of the meta objects to delete. + */ + public function delete_metadata( $type, $object_id, $meta_ids ) { + global $wpdb; + + $table = _get_meta_table( $type ); + if ( ! $table ) { + return false; + } + + foreach ( $meta_ids as $meta_id ) { + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + $wpdb->query( $wpdb->prepare( "DELETE FROM $table WHERE meta_id = %d", $meta_id ) ); + } + + // If we don't have an object ID what do we do - invalidate ALL meta? + if ( $object_id ) { + wp_cache_delete( $object_id, $type . '_meta' ); + } + } + + /** + * Delete metadata with a certain key for the specified objects. + * + * @access public + * + * @todo Test this out to make sure it works as expected. + * @todo Refactor to not use interpolated values when preparing the SQL query. + * + * @param string $type Meta type. + * @param array $object_ids IDs of the objects. + * @param string $meta_key Meta key. + */ + public function delete_batch_metadata( $type, $object_ids, $meta_key ) { + global $wpdb; + + $table = _get_meta_table( $type ); + if ( ! $table ) { + return false; + } + $column = sanitize_key( $type . '_id' ); + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + $wpdb->query( $wpdb->prepare( "DELETE FROM $table WHERE $column IN (%s) && meta_key = %s", implode( ',', $object_ids ), $meta_key ) ); + + // If we don't have an object ID what do we do - invalidate ALL meta? + foreach ( $object_ids as $object_id ) { + wp_cache_delete( $object_id, $type . '_meta' ); + } + } + + /** + * Retrieve value of a constant based on the constant name. + * + * @access public + * + * @param string $constant Name of constant to retrieve. + * @return mixed Value set for the constant. + */ + public function get_constant( $constant ) { + $value = get_option( 'jetpack_constant_' . $constant ); + + if ( $value ) { + return $value; + } + + return null; + } + + /** + * Set the value of a constant. + * + * @access public + * + * @param string $constant Name of constant to retrieve. + * @param mixed $value Value set for the constant. + */ + public function set_constant( $constant, $value ) { + update_option( 'jetpack_constant_' . $constant, $value ); + } + + /** + * Retrieve the number of the available updates of a certain type. + * Type is one of: `plugins`, `themes`, `wordpress`, `translations`, `total`, `wp_update_version`. + * + * @access public + * + * @param string $type Type of updates to retrieve. + * @return int|null Number of updates available, `null` if type is invalid or missing. + */ + public function get_updates( $type ) { + $all_updates = get_option( 'jetpack_updates', array() ); + + if ( isset( $all_updates[ $type ] ) ) { + return $all_updates[ $type ]; + } else { + return null; + } + } + + /** + * Set the available updates of a certain type. + * Type is one of: `plugins`, `themes`, `wordpress`, `translations`, `total`, `wp_update_version`. + * + * @access public + * + * @param string $type Type of updates to set. + * @param int $updates Total number of updates. + */ + public function set_updates( $type, $updates ) { + $all_updates = get_option( 'jetpack_updates', array() ); + $all_updates[ $type ] = $updates; + update_option( 'jetpack_updates', $all_updates ); + } + + /** + * Retrieve a callable value based on its name. + * + * @access public + * + * @param string $name Name of the callable to retrieve. + * @return mixed Value of the callable. + */ + public function get_callable( $name ) { + $value = get_option( 'jetpack_' . $name ); + + if ( $value ) { + return $value; + } + + return null; + } + + /** + * Update the value of a callable. + * + * @access public + * + * @param string $name Callable name. + * @param mixed $value Callable value. + */ + public function set_callable( $name, $value ) { + update_option( 'jetpack_' . $name, $value ); + } + + /** + * Retrieve a network option value based on a network option name. + * + * @access public + * + * @param string $option Name of network option to retrieve. + * @return mixed Value set for the network option. + */ + public function get_site_option( $option ) { + return get_option( 'jetpack_network_' . $option ); + } + + /** + * Update the value of a network option. + * + * @access public + * + * @param string $option Network option name. + * @param mixed $value Network option value. + * @return bool False if value was not updated and true if value was updated. + */ + public function update_site_option( $option, $value ) { + return update_option( 'jetpack_network_' . $option, $value ); + } + + /** + * Remove a network option by name. + * + * @access public + * + * @param string $option Name of option to remove. + * @return bool True, if option is successfully deleted. False on failure. + */ + public function delete_site_option( $option ) { + return delete_option( 'jetpack_network_' . $option ); + } + + /** + * Retrieve the terms from a particular taxonomy. + * + * @access public + * + * @param string $taxonomy Taxonomy slug. + * @return array Array of terms. + */ + public function get_terms( $taxonomy ) { + return get_terms( $taxonomy ); + } + + /** + * Retrieve a particular term. + * + * @access public + * + * @param string $taxonomy Taxonomy slug. + * @param int $term_id ID of the term. + * @param bool $is_term_id Whether this is a `term_id` or a `term_taxonomy_id`. + * @return \WP_Term|\WP_Error Term object on success, \WP_Error object on failure. + */ + public function get_term( $taxonomy, $term_id, $is_term_id = true ) { + $t = $this->ensure_taxonomy( $taxonomy ); + if ( ! $t || is_wp_error( $t ) ) { + return $t; + } + + return get_term( $term_id, $taxonomy ); + } + + /** + * Verify a taxonomy is legitimate and register it if necessary. + * + * @access private + * + * @param string $taxonomy Taxonomy slug. + * @return bool|void|\WP_Error True if already exists; void if it was registered; \WP_Error on error. + */ + private function ensure_taxonomy( $taxonomy ) { + if ( ! taxonomy_exists( $taxonomy ) ) { + // Try re-registering synced taxonomies. + $taxonomies = $this->get_callable( 'taxonomies' ); + if ( ! isset( $taxonomies[ $taxonomy ] ) ) { + // Doesn't exist, or somehow hasn't been synced. + return new \WP_Error( 'invalid_taxonomy', "The taxonomy '$taxonomy' doesn't exist" ); + } + $t = $taxonomies[ $taxonomy ]; + + return register_taxonomy( + $taxonomy, + $t->object_type, + (array) $t + ); + } + + return true; + } + + /** + * Retrieve all terms from a taxonomy that are related to an object with a particular ID. + * + * @access public + * + * @param int $object_id Object ID. + * @param string $taxonomy Taxonomy slug. + * @return array|bool|\WP_Error Array of terms on success, `false` if no terms or post doesn't exist, \WP_Error on failure. + */ + public function get_the_terms( $object_id, $taxonomy ) { + return get_the_terms( $object_id, $taxonomy ); + } + + /** + * Insert or update a term. + * + * @access public + * + * @param \WP_Term $term_object Term object. + * @return array|bool|\WP_Error Array of term_id and term_taxonomy_id if updated, true if inserted, \WP_Error on failure. + */ + public function update_term( $term_object ) { + $taxonomy = $term_object->taxonomy; + global $wpdb; + $exists = $wpdb->get_var( + $wpdb->prepare( + "SELECT EXISTS( SELECT 1 FROM $wpdb->terms WHERE term_id = %d )", + $term_object->term_id + ) + ); + if ( ! $exists ) { + $term_object = sanitize_term( clone( $term_object ), $taxonomy, 'db' ); + $term = array( + 'term_id' => $term_object->term_id, + 'name' => $term_object->name, + 'slug' => $term_object->slug, + 'term_group' => $term_object->term_group, + ); + $term_taxonomy = array( + 'term_taxonomy_id' => $term_object->term_taxonomy_id, + 'term_id' => $term_object->term_id, + 'taxonomy' => $term_object->taxonomy, + 'description' => $term_object->description, + 'parent' => (int) $term_object->parent, + 'count' => (int) $term_object->count, + ); + $wpdb->insert( $wpdb->terms, $term ); + $wpdb->insert( $wpdb->term_taxonomy, $term_taxonomy ); + + return true; + } + + return wp_update_term( $term_object->term_id, $taxonomy, (array) $term_object ); + } + + /** + * Delete a term by the term ID and its corresponding taxonomy. + * + * @access public + * + * @param int $term_id Term ID. + * @param string $taxonomy Taxonomy slug. + * @return bool|int|\WP_Error True on success, false if term doesn't exist. Zero if trying with default category. \WP_Error on invalid taxonomy. + */ + public function delete_term( $term_id, $taxonomy ) { + return wp_delete_term( $term_id, $taxonomy ); + } + + /** + * Add/update terms of a particular taxonomy of an object with the specified ID. + * + * @access public + * + * @param int $object_id The object to relate to. + * @param string $taxonomy The context in which to relate the term to the object. + * @param string|int|array $terms A single term slug, single term id, or array of either term slugs or ids. + * @param bool $append Optional. If false will delete difference of terms. Default false. + */ + public function update_object_terms( $object_id, $taxonomy, $terms, $append ) { + wp_set_object_terms( $object_id, $terms, $taxonomy, $append ); + } + + /** + * Remove certain term relationships from the specified object. + * + * @access public + * + * @todo Refactor to not use interpolated values when preparing the SQL query. + * + * @param int $object_id ID of the object. + * @param array $tt_ids Term taxonomy IDs. + * @return bool True on success, false on failure. + */ + public function delete_object_terms( $object_id, $tt_ids ) { + global $wpdb; + + if ( is_array( $tt_ids ) && ! empty( $tt_ids ) ) { + // Escape. + $tt_ids_sanitized = array_map( 'intval', $tt_ids ); + + $taxonomies = array(); + foreach ( $tt_ids_sanitized as $tt_id ) { + $term = get_term_by( 'term_taxonomy_id', $tt_id ); + $taxonomies[ $term->taxonomy ][] = $tt_id; + } + $in_tt_ids = implode( ', ', $tt_ids_sanitized ); + + /** + * Fires immediately before an object-term relationship is deleted. + * + * @since 2.9.0 + * + * @param int $object_id Object ID. + * @param array $tt_ids An array of term taxonomy IDs. + */ + do_action( 'delete_term_relationships', $object_id, $tt_ids_sanitized ); + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + $deleted = $wpdb->query( $wpdb->prepare( "DELETE FROM $wpdb->term_relationships WHERE object_id = %d AND term_taxonomy_id IN ($in_tt_ids)", $object_id ) ); + foreach ( $taxonomies as $taxonomy => $taxonomy_tt_ids ) { + $this->ensure_taxonomy( $taxonomy ); + wp_cache_delete( $object_id, $taxonomy . '_relationships' ); + /** + * Fires immediately after an object-term relationship is deleted. + * + * @since 2.9.0 + * + * @param int $object_id Object ID. + * @param array $tt_ids An array of term taxonomy IDs. + */ + do_action( 'deleted_term_relationships', $object_id, $taxonomy_tt_ids ); + wp_update_term_count( $taxonomy_tt_ids, $taxonomy ); + } + + return (bool) $deleted; + } + + return false; + } + + /** + * Retrieve the number of users. + * Not supported in this replicastore. + * + * @access public + */ + public function user_count() { + // Noop. + } + + /** + * Retrieve a user object by the user ID. + * + * @access public + * + * @param int $user_id User ID. + * @return \WP_User User object. + */ + public function get_user( $user_id ) { + return \WP_User::get_instance( $user_id ); + } + + /** + * Insert or update a user. + * Not supported in this replicastore. + * + * @access public + * @throws \Exception If this method is invoked. + * + * @param \WP_User $user User object. + */ + public function upsert_user( $user ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable + $this->invalid_call(); + } + + /** + * Delete a user. + * Not supported in this replicastore. + * + * @access public + * @throws \Exception If this method is invoked. + * + * @param int $user_id User ID. + */ + public function delete_user( $user_id ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable + $this->invalid_call(); + } + + /** + * Update/insert user locale. + * Not supported in this replicastore. + * + * @access public + * @throws \Exception If this method is invoked. + * + * @param int $user_id User ID. + * @param string $local The user locale. + */ + public function upsert_user_locale( $user_id, $local ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable + $this->invalid_call(); + } + + /** + * Delete user locale. + * Not supported in this replicastore. + * + * @access public + * @throws \Exception If this method is invoked. + * + * @param int $user_id User ID. + */ + public function delete_user_locale( $user_id ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable + $this->invalid_call(); + } + + /** + * Retrieve the user locale. + * + * @access public + * + * @param int $user_id User ID. + * @return string The user locale. + */ + public function get_user_locale( $user_id ) { + return get_user_locale( $user_id ); + } + + /** + * Retrieve the allowed mime types for the user. + * Not supported in this replicastore. + * + * @access public + * + * @param int $user_id User ID. + */ + public function get_allowed_mime_types( $user_id ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable + // Noop. + } + + /** + * Retrieve all the checksums we are interested in. + * Currently that is posts, comments, post meta and comment meta. + * + * @access public + * + * @return array Checksums. + */ + public function checksum_all() { + $post_meta_checksum = $this->checksum_histogram( 'post_meta', 1 ); + $comment_meta_checksum = $this->checksum_histogram( 'comment_meta', 1 ); + + return array( + 'posts' => $this->posts_checksum(), + 'comments' => $this->comments_checksum(), + 'post_meta' => reset( $post_meta_checksum ), + 'comment_meta' => reset( $comment_meta_checksum ), + ); + } + + /** + * Retrieve the columns that are needed to calculate a checksum for an object type. + * + * @access public + * + * @todo Refactor to not use interpolated values and prepare the SQL query. + * + * @param string $object_type Object type. + * @return array|bool Columns, or false if invalid object type is specified. + */ + public function get_checksum_columns_for_object_type( $object_type ) { + switch ( $object_type ) { + case 'posts': + return Defaults::$default_post_checksum_columns; + case 'post_meta': + return Defaults::$default_post_meta_checksum_columns; + case 'comments': + return Defaults::$default_comment_checksum_columns; + case 'comment_meta': + return Defaults::$default_post_meta_checksum_columns; + case 'terms': + return Defaults::$default_term_checksum_columns; + case 'term_taxonomy': + return Defaults::$default_term_taxonomy_checksum_columns; + case 'term_relationships': + return Defaults::$default_term_relationships_checksum_columns; + default: + return false; + } + } + + /** + * Grabs the minimum and maximum object ids for the given parameters. + * + * @access public + * + * @param string $id_field The id column in the table to query. + * @param string $object_table The table to query. + * @param string $where A sql where clause without 'WHERE'. + * @param int $bucket_size The maximum amount of objects to include in the query. + * For `term_relationships` table, the bucket size will refer to the amount + * of distinct object ids. This will likely include more database rows than + * the bucket size implies. + * + * @return object An object with min_id and max_id properties. + */ + public function get_min_max_object_id( $id_field, $object_table, $where, $bucket_size ) { + global $wpdb; + + // The term relationship table's unique key is a combination of 2 columns. `DISTINCT` helps us get a more acurate query. + $distinct_sql = ( $wpdb->term_relationships === $object_table ) ? 'DISTINCT' : ''; + $where_sql = $where ? "WHERE $where" : ''; + + // Since MIN() and MAX() do not work with LIMIT, we'll need to adjust the dataset we query if a limit is present. + // With a limit present, we'll look at a dataset consisting of object_ids that meet the constructs of the $where clause. + // Without a limit, we can use the actual table as a dataset. + $from = $bucket_size ? + "( SELECT $distinct_sql $id_field FROM $object_table $where_sql ORDER BY $id_field ASC LIMIT $bucket_size ) as ids" : + "$object_table $where_sql ORDER BY $id_field ASC"; + + return $wpdb->get_row( + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + "SELECT MIN($id_field) as min, MAX($id_field) as max FROM $from" + ); + } + + /** + * Retrieve the checksum histogram for a specific object type. + * + * @access public + * + * @todo Refactor to not use interpolated values and properly prepare the SQL query. + * + * @param string $object_type Object type. + * @param int $buckets Number of buckets to split the objects to. + * @param int $start_id Minimum object ID. + * @param int $end_id Maximum object ID. + * @param array $columns Table columns to calculate the checksum from. + * @param bool $strip_non_ascii Whether to strip non-ASCII characters. + * @param string $salt Salt, used for $wpdb->prepare()'s args. + * @return array The checksum histogram. + */ + public function checksum_histogram( $object_type, $buckets, $start_id = null, $end_id = null, $columns = null, $strip_non_ascii = true, $salt = '' ) { + global $wpdb; + + $wpdb->queries = array(); + + if ( empty( $columns ) ) { + $columns = $this->get_checksum_columns_for_object_type( $object_type ); + } + + switch ( $object_type ) { + case 'posts': + $object_count = $this->post_count( null, $start_id, $end_id ); + $object_table = $wpdb->posts; + $id_field = 'ID'; + $where_sql = Settings::get_blacklisted_post_types_sql(); + break; + case 'post_meta': + $object_table = $wpdb->postmeta; + $where_sql = Settings::get_whitelisted_post_meta_sql(); + $object_count = $this->meta_count( $object_table, $where_sql, $start_id, $end_id ); + $id_field = 'meta_id'; + break; + case 'comments': + $object_count = $this->comment_count( null, $start_id, $end_id ); + $object_table = $wpdb->comments; + $id_field = 'comment_ID'; + $where_sql = Settings::get_comments_filter_sql(); + break; + case 'comment_meta': + $object_table = $wpdb->commentmeta; + $where_sql = Settings::get_whitelisted_comment_meta_sql(); + $object_count = $this->meta_count( $object_table, $where_sql, $start_id, $end_id ); + $id_field = 'meta_id'; + break; + case 'terms': + $object_table = $wpdb->terms; + $object_count = $this->term_count(); + $id_field = 'term_id'; + $where_sql = '1=1'; + break; + case 'term_taxonomy': + $object_table = $wpdb->term_taxonomy; + $object_count = $this->term_taxonomy_count(); + $id_field = 'term_taxonomy_id'; + $where_sql = '1=1'; + break; + case 'term_relationships': + $object_table = $wpdb->term_relationships; + $object_count = $this->term_relationship_count(); + $id_field = 'object_id'; + $where_sql = '1=1'; + break; + default: + return false; + } + + $bucket_size = intval( ceil( $object_count / $buckets ) ); + $previous_max_id = 0; + $histogram = array(); + + // This is used for the min / max query, while $where_sql is used for the checksum query. + $where = $where_sql; + + if ( $start_id ) { + $where .= " AND $id_field >= " . intval( $start_id ); + } + + if ( $end_id ) { + $where .= " AND $id_field <= " . intval( $end_id ); + } + + do { + $result = $this->get_min_max_object_id( + $id_field, + $object_table, + $where . " AND $id_field > $previous_max_id", + $bucket_size + ); + + if ( null === $result->min || null === $result->max ) { + // Nothing to checksum here... + break; + } + + // Get the checksum value. + $value = $this->table_checksum( $object_table, $columns, $id_field, $where_sql, $result->min, $result->max, $strip_non_ascii, $salt ); + + if ( is_wp_error( $value ) ) { + return $value; + } + + if ( null === $result->min || null === $result->max ) { + break; + } elseif ( $result->min === $result->max ) { + $histogram[ $result->min ] = $value; + } else { + $histogram[ "{$result->min}-{$result->max}" ] = $value; + } + + $previous_max_id = $result->max; + } while ( true ); + + return $histogram; + } + + /** + * Retrieve the checksum for a specific database table. + * + * @access private + * + * @todo Refactor to properly prepare the SQL query. + * + * @param string $table Table name. + * @param array $columns Table columns to calculate the checksum from. + * @param int $id_column Name of the unique ID column. + * @param string $where_sql Additional WHERE clause SQL. + * @param int $min_id Minimum object ID. + * @param int $max_id Maximum object ID. + * @param bool $strip_non_ascii Whether to strip non-ASCII characters. + * @param string $salt Salt, used for $wpdb->prepare()'s args. + * @return int|\WP_Error The table histogram, or \WP_Error on failure. + */ + private function table_checksum( $table, $columns, $id_column, $where_sql = '1=1', $min_id = null, $max_id = null, $strip_non_ascii = true, $salt = '' ) { + global $wpdb; + + // Sanitize to just valid MySQL column names. + $sanitized_columns = preg_grep( '/^[0-9,a-z,A-Z$_]+$/i', $columns ); + + if ( $strip_non_ascii ) { + $columns_sql = implode( ',', array_map( array( $this, 'strip_non_ascii_sql' ), $sanitized_columns ) ); + } else { + $columns_sql = implode( ',', $sanitized_columns ); + } + + if ( null !== $min_id && null !== $max_id ) { + if ( $min_id === $max_id ) { + $min_id = intval( $min_id ); + $where_sql .= " AND $id_column = $min_id LIMIT 1"; + } else { + $min_id = intval( $min_id ); + $max_id = intval( $max_id ); + $size = $max_id - $min_id; + $where_sql .= " AND $id_column >= $min_id AND $id_column <= $max_id LIMIT $size"; + } + } else { + if ( null !== $min_id ) { + $min_id = intval( $min_id ); + $where_sql .= " AND $id_column >= $min_id"; + } + + if ( null !== $max_id ) { + $max_id = intval( $max_id ); + $where_sql .= " AND $id_column <= $max_id"; + } + } + + $query = <<<ENDSQL + SELECT CAST( SUM( CRC32( CONCAT_WS( '#', '%s', {$columns_sql} ) ) ) AS UNSIGNED INT ) + FROM $table + WHERE $where_sql; +ENDSQL; + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + $result = $wpdb->get_var( $wpdb->prepare( $query, $salt ) ); + if ( $wpdb->last_error ) { + return new \WP_Error( 'database_error', $wpdb->last_error ); + } + + return $result; + } + + /** + * Retrieve the type of the checksum. + * + * @access public + * + * @return string Type of the checksum. + */ + public function get_checksum_type() { + return 'sum'; + } + + /** + * Count the meta values in a table, within a specified range. + * + * @access private + * + * @todo Refactor to not use interpolated values when preparing the SQL query. + * + * @param string $table Table name. + * @param string $where_sql Additional WHERE SQL. + * @param int $min_id Minimum meta ID. + * @param int $max_id Maximum meta ID. + * @return int Number of meta values. + */ + private function meta_count( $table, $where_sql, $min_id, $max_id ) { + global $wpdb; + + if ( ! empty( $min_id ) ) { + $where_sql .= ' AND meta_id >= ' . intval( $min_id ); + } + + if ( ! empty( $max_id ) ) { + $where_sql .= ' AND meta_id <= ' . intval( $max_id ); + } + + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + return $wpdb->get_var( "SELECT COUNT(*) FROM $table WHERE $where_sql" ); + } + + /** + * Wraps a column name in SQL which strips non-ASCII chars. + * This helps normalize data to avoid checksum differences caused by + * badly encoded data in the DB. + * + * @param string $column_name Name of the column. + * @return string Column name, without the non-ASCII chars. + */ + public function strip_non_ascii_sql( $column_name ) { + return "REPLACE( CONVERT( $column_name USING ascii ), '?', '' )"; + } + + /** + * Used in methods that are not implemented and shouldn't be invoked. + * + * @access private + * @throws \Exception If this method is invoked. + */ + private function invalid_call() { + // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace + $backtrace = debug_backtrace(); + $caller = $backtrace[1]['function']; + throw new \Exception( "This function $caller is not supported on the WP Replicastore" ); + } +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-sync/src/class-sender.php b/plugins/jetpack/vendor/automattic/jetpack-sync/src/class-sender.php new file mode 100644 index 00000000..4bed9181 --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-sync/src/class-sender.php @@ -0,0 +1,795 @@ +<?php +/** + * Sync sender. + * + * @package automattic/jetpack-sync + */ + +namespace Automattic\Jetpack\Sync; + +use Automattic\Jetpack\Constants; + +/** + * This class grabs pending actions from the queue and sends them + */ +class Sender { + /** + * Name of the option that stores the time of the next sync. + * + * @access public + * + * @var string + */ + const NEXT_SYNC_TIME_OPTION_NAME = 'jetpack_next_sync_time'; + + /** + * Sync timeout after a WPCOM error. + * + * @access public + * + * @var int + */ + const WPCOM_ERROR_SYNC_DELAY = 60; + + /** + * Sync timeout after a queue has been locked. + * + * @access public + * + * @var int + */ + const QUEUE_LOCKED_SYNC_DELAY = 10; + + /** + * Maximum bytes to checkout without exceeding the memory limit. + * + * @access private + * + * @var int + */ + private $dequeue_max_bytes; + + /** + * Maximum bytes in a single encoded item. + * + * @access private + * + * @var int + */ + private $upload_max_bytes; + + /** + * Maximum number of sync items in a single action. + * + * @access private + * + * @var int + */ + private $upload_max_rows; + + /** + * Maximum time for perfirming a checkout of items from the queue (in seconds). + * + * @access private + * + * @var int + */ + private $max_dequeue_time; + + /** + * How many seconds to wait after sending sync items after exceeding the sync wait threshold (in seconds). + * + * @access private + * + * @var int + */ + private $sync_wait_time; + + /** + * How much maximum time to wait for the checkout to finish (in seconds). + * + * @access private + * + * @var int + */ + private $sync_wait_threshold; + + /** + * How much maximum time to wait for the sync items to be queued for sending (in seconds). + * + * @access private + * + * @var int + */ + private $enqueue_wait_time; + + /** + * Incremental sync queue object. + * + * @access private + * + * @var Automattic\Jetpack\Sync\Queue + */ + private $sync_queue; + + /** + * Full sync queue object. + * + * @access private + * + * @var Automattic\Jetpack\Sync\Queue + */ + private $full_sync_queue; + + /** + * Codec object for encoding and decoding sync items. + * + * @access private + * + * @var Automattic\Jetpack\Sync\Codec_Interface + */ + private $codec; + + /** + * The current user before we change or clear it. + * + * @access private + * + * @var \WP_User + */ + private $old_user; + + /** + * Container for the singleton instance of this class. + * + * @access private + * @static + * + * @var Automattic\Jetpack\Sync\Sender + */ + private static $instance; + + /** + * Retrieve the singleton instance of this class. + * + * @access public + * @static + * + * @return Automattic\Jetpack\Sync\Sender + */ + public static function get_instance() { + if ( null === self::$instance ) { + self::$instance = new self(); + } + + return self::$instance; + } + + /** + * Constructor. + * This is necessary because you can't use "new" when you declare instance properties >:( + * + * @access protected + * @static + */ + protected function __construct() { + $this->set_defaults(); + $this->init(); + } + + /** + * Initialize the sender. + * Prepares the current user and initializes all sync modules. + * + * @access private + */ + private function init() { + add_action( 'jetpack_sync_before_send_queue_sync', array( $this, 'maybe_set_user_from_token' ), 1 ); + add_action( 'jetpack_sync_before_send_queue_sync', array( $this, 'maybe_clear_user_from_token' ), 20 ); + add_filter( 'jetpack_xmlrpc_methods', array( $this, 'register_jetpack_xmlrpc_methods' ) ); + foreach ( Modules::get_modules() as $module ) { + $module->init_before_send(); + } + } + + /** + * Detect if this is a XMLRPC request with a valid signature. + * If so, changes the user to the new one. + * + * @access public + */ + public function maybe_set_user_from_token() { + $verified_user = \Jetpack::connection()->verify_xml_rpc_signature(); + if ( Constants::is_true( 'XMLRPC_REQUEST' ) && + ! is_wp_error( $verified_user ) + && $verified_user + ) { + $old_user = wp_get_current_user(); + $this->old_user = isset( $old_user->ID ) ? $old_user->ID : 0; + wp_set_current_user( $verified_user['user_id'] ); + } + } + + /** + * If we used to have a previous current user, revert back to it. + * + * @access public + */ + public function maybe_clear_user_from_token() { + if ( isset( $this->old_user ) ) { + wp_set_current_user( $this->old_user ); + } + } + + /** + * Retrieve the next sync time. + * + * @access public + * + * @param string $queue_name Name of the queue. + * @return float Timestamp of the next sync. + */ + public function get_next_sync_time( $queue_name ) { + return (float) get_option( self::NEXT_SYNC_TIME_OPTION_NAME . '_' . $queue_name, 0 ); + } + + /** + * Set the next sync time. + * + * @access public + * + * @param int $time Timestamp of the next sync. + * @param string $queue_name Name of the queue. + * @return boolean True if update was successful, false otherwise. + */ + public function set_next_sync_time( $time, $queue_name ) { + return update_option( self::NEXT_SYNC_TIME_OPTION_NAME . '_' . $queue_name, $time, true ); + } + + /** + * Trigger a full sync. + * + * @access public + * + * @return boolean|\WP_Error True if this sync sending was successful, error object otherwise. + */ + public function do_full_sync() { + if ( ! Modules::get_module( 'full-sync' ) ) { + return; + } + $this->continue_full_sync_enqueue(); + return $this->do_sync_and_set_delays( $this->full_sync_queue ); + } + + /** + * Enqueue the next sync items for sending. + * Will not be done if the current request is a WP import one. + * Will be delayed until the next sync time comes. + * + * @access private + */ + private function continue_full_sync_enqueue() { + if ( defined( 'WP_IMPORTING' ) && WP_IMPORTING ) { + return false; + } + + if ( $this->get_next_sync_time( 'full-sync-enqueue' ) > microtime( true ) ) { + return false; + } + + Modules::get_module( 'full-sync' )->continue_enqueuing(); + + $this->set_next_sync_time( time() + $this->get_enqueue_wait_time(), 'full-sync-enqueue' ); + } + + /** + * Trigger incremental sync. + * + * @access public + * + * @return boolean|\WP_Error True if this sync sending was successful, error object otherwise. + */ + public function do_sync() { + return $this->do_sync_and_set_delays( $this->sync_queue ); + } + + /** + * Trigger sync for a certain sync queue. + * Responsible for setting next sync time. + * Will not be delayed if the current request is a WP import one. + * Will be delayed until the next sync time comes. + * + * @access public + * + * @param Automattic\Jetpack\Sync\Queue $queue Queue object. + * @return boolean|\WP_Error True if this sync sending was successful, error object otherwise. + */ + public function do_sync_and_set_delays( $queue ) { + // Don't sync if importing. + if ( defined( 'WP_IMPORTING' ) && WP_IMPORTING ) { + return new \WP_Error( 'is_importing' ); + } + + if ( ! Settings::is_sender_enabled( $queue->id ) ) { + return new \WP_Error( 'sender_disabled_for_queue_' . $queue->id ); + } + + // Don't sync if we are throttled. + if ( $this->get_next_sync_time( $queue->id ) > microtime( true ) ) { + return new \WP_Error( 'sync_throttled' ); + } + + $start_time = microtime( true ); + + Settings::set_is_syncing( true ); + + $sync_result = $this->do_sync_for_queue( $queue ); + + Settings::set_is_syncing( false ); + + $exceeded_sync_wait_threshold = ( microtime( true ) - $start_time ) > (float) $this->get_sync_wait_threshold(); + + if ( is_wp_error( $sync_result ) ) { + if ( 'unclosed_buffer' === $sync_result->get_error_code() ) { + $this->set_next_sync_time( time() + self::QUEUE_LOCKED_SYNC_DELAY, $queue->id ); + } + if ( 'wpcom_error' === $sync_result->get_error_code() ) { + $this->set_next_sync_time( time() + self::WPCOM_ERROR_SYNC_DELAY, $queue->id ); + } + } elseif ( $exceeded_sync_wait_threshold ) { + // If we actually sent data and it took a while, wait before sending again. + $this->set_next_sync_time( time() + $this->get_sync_wait_time(), $queue->id ); + } + + return $sync_result; + } + + /** + * Retrieve the next sync items to send. + * + * @access public + * + * @param Automattic\Jetpack\Sync\Queue_Buffer $buffer Queue buffer object. + * @param boolean $encode Whether to encode the items. + * @return array Sync items to send. + */ + public function get_items_to_send( $buffer, $encode = true ) { + // Track how long we've been processing so we can avoid request timeouts. + $start_time = microtime( true ); + $upload_size = 0; + $items_to_send = array(); + $items = $buffer->get_items(); + // Set up current screen to avoid errors rendering content. + require_once ABSPATH . 'wp-admin/includes/class-wp-screen.php'; + require_once ABSPATH . 'wp-admin/includes/screen.php'; + set_current_screen( 'sync' ); + $skipped_items_ids = array(); + /** + * We estimate the total encoded size as we go by encoding each item individually. + * This is expensive, but the only way to really know :/ + */ + foreach ( $items as $key => $item ) { + // Suspending cache addition help prevent overloading in memory cache of large sites. + wp_suspend_cache_addition( true ); + /** + * Modify the data within an action before it is serialized and sent to the server + * For example, during full sync this expands Post ID's into full Post objects, + * so that we don't have to serialize the whole object into the queue. + * + * @since 4.2.0 + * + * @param array The action parameters + * @param int The ID of the user who triggered the action + */ + $item[1] = apply_filters( 'jetpack_sync_before_send_' . $item[0], $item[1], $item[2] ); + wp_suspend_cache_addition( false ); + if ( false === $item[1] ) { + $skipped_items_ids[] = $key; + continue; + } + $encoded_item = $encode ? $this->codec->encode( $item ) : $item; + $upload_size += strlen( $encoded_item ); + if ( $upload_size > $this->upload_max_bytes && count( $items_to_send ) > 0 ) { + break; + } + $items_to_send[ $key ] = $encoded_item; + if ( microtime( true ) - $start_time > $this->max_dequeue_time ) { + break; + } + } + + return array( $items_to_send, $skipped_items_ids, $items, microtime( true ) - $start_time ); + } + + /** + * If supported, flush all response data to the client and finish the request. + * This allows for time consuming tasks to be performed without leaving the connection open. + * + * @access private + */ + private function fastcgi_finish_request() { + if ( function_exists( 'fastcgi_finish_request' ) && version_compare( phpversion(), '7.0.16', '>=' ) ) { + fastcgi_finish_request(); + } + } + + /** + * Perform sync for a certain sync queue. + * + * @access public + * + * @param Automattic\Jetpack\Sync\Queue $queue Queue object. + * @return boolean|\WP_Error True if this sync sending was successful, error object otherwise. + */ + public function do_sync_for_queue( $queue ) { + do_action( 'jetpack_sync_before_send_queue_' . $queue->id ); + if ( $queue->size() === 0 ) { + return new \WP_Error( 'empty_queue_' . $queue->id ); + } + /** + * Now that we're sure we are about to sync, try to ignore user abort + * so we can avoid getting into a bad state. + */ + if ( function_exists( 'ignore_user_abort' ) ) { + ignore_user_abort( true ); + } + + /* Don't make the request block till we finish, if possible. */ + if ( Constants::is_true( 'REST_REQUEST' ) || Constants::is_true( 'XMLRPC_REQUEST' ) ) { + $this->fastcgi_finish_request(); + } + + $checkout_start_time = microtime( true ); + + $buffer = $queue->checkout_with_memory_limit( $this->dequeue_max_bytes, $this->upload_max_rows ); + + if ( ! $buffer ) { + // Buffer has no items. + return new \WP_Error( 'empty_buffer' ); + } + + if ( is_wp_error( $buffer ) ) { + return $buffer; + } + + $checkout_duration = microtime( true ) - $checkout_start_time; + + list( $items_to_send, $skipped_items_ids, $items, $preprocess_duration ) = $this->get_items_to_send( $buffer, true ); + if ( ! empty( $items_to_send ) ) { + /** + * Fires when data is ready to send to the server. + * Return false or WP_Error to abort the sync (e.g. if there's an error) + * The items will be automatically re-sent later + * + * @since 4.2.0 + * + * @param array $data The action buffer + * @param string $codec The codec name used to encode the data + * @param double $time The current time + * @param string $queue The queue used to send ('sync' or 'full_sync') + */ + Settings::set_is_sending( true ); + $processed_item_ids = apply_filters( 'jetpack_sync_send_data', $items_to_send, $this->codec->name(), microtime( true ), $queue->id, $checkout_duration, $preprocess_duration ); + Settings::set_is_sending( false ); + } else { + $processed_item_ids = $skipped_items_ids; + $skipped_items_ids = array(); + } + + if ( ! $processed_item_ids || is_wp_error( $processed_item_ids ) ) { + $checked_in_item_ids = $queue->checkin( $buffer ); + if ( is_wp_error( $checked_in_item_ids ) ) { + // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log + error_log( 'Error checking in buffer: ' . $checked_in_item_ids->get_error_message() ); + $queue->force_checkin(); + } + if ( is_wp_error( $processed_item_ids ) ) { + return new \WP_Error( 'wpcom_error', $processed_item_ids->get_error_code() ); + } + // Returning a wpcom_error is a sign to the caller that we should wait a while before syncing again. + return new \WP_Error( 'wpcom_error', 'jetpack_sync_send_data_false' ); + } else { + // Detect if the last item ID was an error. + $had_wp_error = is_wp_error( end( $processed_item_ids ) ); + if ( $had_wp_error ) { + $wp_error = array_pop( $processed_item_ids ); + } + // Also checkin any items that were skipped. + if ( count( $skipped_items_ids ) > 0 ) { + $processed_item_ids = array_merge( $processed_item_ids, $skipped_items_ids ); + } + $processed_items = array_intersect_key( $items, array_flip( $processed_item_ids ) ); + /** + * Allows us to keep track of all the actions that have been sent. + * Allows us to calculate the progress of specific actions. + * + * @since 4.2.0 + * + * @param array $processed_actions The actions that we send successfully. + */ + do_action( 'jetpack_sync_processed_actions', $processed_items ); + $queue->close( $buffer, $processed_item_ids ); + // Returning a WP_Error is a sign to the caller that we should wait a while before syncing again. + if ( $had_wp_error ) { + return new \WP_Error( 'wpcom_error', $wp_error->get_error_code() ); + } + } + return true; + } + + /** + * Returns any object that is able to be synced. + * + * @access public + * + * @param array $args the synchronized object parameters. + * @return string Encoded sync object. + */ + public function sync_object( $args ) { + // For example: posts, post, 5. + list( $module_name, $object_type, $id ) = $args; + + $sync_module = Modules::get_module( $module_name ); + $codec = $this->get_codec(); + + return $codec->encode( $sync_module->get_object_by_id( $object_type, $id ) ); + } + + /** + * Register additional sync XML-RPC methods available to Jetpack for authenticated users. + * + * @access public + * @since 7.8 + * + * @param array $jetpack_methods XML-RPC methods available to the Jetpack Server. + * @return array Filtered XML-RPC methods. + */ + public function register_jetpack_xmlrpc_methods( $jetpack_methods ) { + $jetpack_methods['jetpack.syncObject'] = array( $this, 'sync_object' ); + return $jetpack_methods; + } + + /** + * Get the incremental sync queue object. + * + * @access public + * + * @return Automattic\Jetpack\Sync\Queue Queue object. + */ + public function get_sync_queue() { + return $this->sync_queue; + } + + /** + * Get the full sync queue object. + * + * @access public + * + * @return Automattic\Jetpack\Sync\Queue Queue object. + */ + public function get_full_sync_queue() { + return $this->full_sync_queue; + } + + /** + * Get the codec object. + * + * @access public + * + * @return Automattic\Jetpack\Sync\Codec_Interface Codec object. + */ + public function get_codec() { + return $this->codec; + } + + /** + * Determine the codec object. + * Use gzip deflate if supported. + * + * @access public + */ + public function set_codec() { + if ( function_exists( 'gzinflate' ) ) { + $this->codec = new JSON_Deflate_Array_Codec(); + } else { + $this->codec = new Simple_Codec(); + } + } + + /** + * Compute and send all the checksums. + * + * @access public + */ + public function send_checksum() { + $store = new Replicastore(); + do_action( 'jetpack_sync_checksum', $store->checksum_all() ); + } + + /** + * Reset the incremental sync queue. + * + * @access public + */ + public function reset_sync_queue() { + $this->sync_queue->reset(); + } + + /** + * Reset the full sync queue. + * + * @access public + */ + public function reset_full_sync_queue() { + $this->full_sync_queue->reset(); + } + + /** + * Set the maximum bytes to checkout without exceeding the memory limit. + * + * @access public + * + * @param int $size Maximum bytes to checkout. + */ + public function set_dequeue_max_bytes( $size ) { + $this->dequeue_max_bytes = $size; + } + + /** + * Set the maximum bytes in a single encoded item. + * + * @access public + * + * @param int $max_bytes Maximum bytes in a single encoded item. + */ + public function set_upload_max_bytes( $max_bytes ) { + $this->upload_max_bytes = $max_bytes; + } + + /** + * Set the maximum number of sync items in a single action. + * + * @access public + * + * @param int $max_rows Maximum number of sync items. + */ + public function set_upload_max_rows( $max_rows ) { + $this->upload_max_rows = $max_rows; + } + + /** + * Set the sync wait time (in seconds). + * + * @access public + * + * @param int $seconds Sync wait time. + */ + public function set_sync_wait_time( $seconds ) { + $this->sync_wait_time = $seconds; + } + + /** + * Get current sync wait time (in seconds). + * + * @access public + * + * @return int Sync wait time. + */ + public function get_sync_wait_time() { + return $this->sync_wait_time; + } + + /** + * Set the enqueue wait time (in seconds). + * + * @access public + * + * @param int $seconds Enqueue wait time. + */ + public function set_enqueue_wait_time( $seconds ) { + $this->enqueue_wait_time = $seconds; + } + + /** + * Get current enqueue wait time (in seconds). + * + * @access public + * + * @return int Enqueue wait time. + */ + public function get_enqueue_wait_time() { + return $this->enqueue_wait_time; + } + + /** + * Set the sync wait threshold (in seconds). + * + * @access public + * + * @param int $seconds Sync wait threshold. + */ + public function set_sync_wait_threshold( $seconds ) { + $this->sync_wait_threshold = $seconds; + } + + /** + * Get current sync wait threshold (in seconds). + * + * @access public + * + * @return int Sync wait threshold. + */ + public function get_sync_wait_threshold() { + return $this->sync_wait_threshold; + } + + /** + * Set the maximum time for perfirming a checkout of items from the queue (in seconds). + * + * @access public + * + * @param int $seconds Maximum dequeue time. + */ + public function set_max_dequeue_time( $seconds ) { + $this->max_dequeue_time = $seconds; + } + + /** + * Initialize the sync queues, codec and set the default settings. + * + * @access public + */ + public function set_defaults() { + $this->sync_queue = new Queue( 'sync' ); + $this->full_sync_queue = new Queue( 'full_sync' ); + $this->set_codec(); + + // Saved settings. + Settings::set_importing( null ); + $settings = Settings::get_settings(); + $this->set_dequeue_max_bytes( $settings['dequeue_max_bytes'] ); + $this->set_upload_max_bytes( $settings['upload_max_bytes'] ); + $this->set_upload_max_rows( $settings['upload_max_rows'] ); + $this->set_sync_wait_time( $settings['sync_wait_time'] ); + $this->set_enqueue_wait_time( $settings['enqueue_wait_time'] ); + $this->set_sync_wait_threshold( $settings['sync_wait_threshold'] ); + $this->set_max_dequeue_time( Defaults::get_max_sync_execution_time() ); + } + + /** + * Reset sync queues, modules and settings. + * + * @access public + */ + public function reset_data() { + $this->reset_sync_queue(); + $this->reset_full_sync_queue(); + + foreach ( Modules::get_modules() as $module ) { + $module->reset_data(); + } + + foreach ( array( 'sync', 'full_sync', 'full-sync-enqueue' ) as $queue_name ) { + delete_option( self::NEXT_SYNC_TIME_OPTION_NAME . '_' . $queue_name ); + } + + Settings::reset_data(); + } + + /** + * Perform cleanup at the event of plugin uninstallation. + * + * @access public + */ + public function uninstall() { + // Lets delete all the other fun stuff like transient and option and the sync queue. + $this->reset_data(); + + // Delete the full sync status. + delete_option( 'jetpack_full_sync_status' ); + + // Clear the sync cron. + wp_clear_scheduled_hook( 'jetpack_sync_cron' ); + wp_clear_scheduled_hook( 'jetpack_sync_full_cron' ); + } +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-sync/src/class-server.php b/plugins/jetpack/vendor/automattic/jetpack-sync/src/class-server.php new file mode 100644 index 00000000..2f97fd13 --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-sync/src/class-server.php @@ -0,0 +1,190 @@ +<?php +/** + * Sync server. + * + * @package automattic/jetpack-sync + */ + +namespace Automattic\Jetpack\Sync; + +/** + * Simple version of a Jetpack Sync Server - just receives arrays of events and + * issues them locally with the 'jetpack_sync_remote_action' action. + */ +class Server { + /** + * Codec used to decode sync events. + * + * @access private + * + * @var Automattic\Jetpack\Sync\Codec_Interface + */ + private $codec; + + /** + * Maximum time for processing sync actions. + * + * @access public + * + * @var int + */ + const MAX_TIME_PER_REQUEST_IN_SECONDS = 15; + + /** + * Prefix of the blog lock transient. + * + * @access public + * + * @var string + */ + const BLOG_LOCK_TRANSIENT_PREFIX = 'jp_sync_req_lock_'; + + /** + * Lifetime of the blog lock transient. + * + * @access public + * + * @var int + */ + const BLOG_LOCK_TRANSIENT_EXPIRY = 60; // Seconds. + + /** + * Constructor. + * + * This is necessary because you can't use "new" when you declare instance properties >:( + * + * @access public + */ + public function __construct() { + $this->codec = new JSON_Deflate_Array_Codec(); + } + + /** + * Set the codec instance. + * + * @access public + * + * @param Automattic\Jetpack\Sync\Codec_Interface $codec Codec instance. + */ + public function set_codec( Codec_Interface $codec ) { + $this->codec = $codec; + } + + /** + * Attempt to lock the request when the server receives concurrent requests from the same blog. + * + * @access public + * + * @param int $blog_id ID of the blog. + * @param int $expiry Blog lock transient lifetime. + * @return boolean True if succeeded, false otherwise. + */ + public function attempt_request_lock( $blog_id, $expiry = self::BLOG_LOCK_TRANSIENT_EXPIRY ) { + $transient_name = $this->get_concurrent_request_transient_name( $blog_id ); + $locked_time = get_site_transient( $transient_name ); + if ( $locked_time ) { + return false; + } + set_site_transient( $transient_name, microtime( true ), $expiry ); + + return true; + } + + /** + * Retrieve the blog lock transient name for a particular blog. + * + * @access public + * + * @param int $blog_id ID of the blog. + * @return string Name of the blog lock transient. + */ + private function get_concurrent_request_transient_name( $blog_id ) { + return self::BLOG_LOCK_TRANSIENT_PREFIX . $blog_id; + } + + /** + * Remove the request lock from a particular blog ID. + * + * @access public + * + * @param int $blog_id ID of the blog. + */ + public function remove_request_lock( $blog_id ) { + delete_site_transient( $this->get_concurrent_request_transient_name( $blog_id ) ); + } + + /** + * Receive and process sync events. + * + * @access public + * + * @param array $data Sync events. + * @param object $token The auth token used to invoke the API. + * @param int $sent_timestamp Timestamp (in seconds) when the actions were transmitted. + * @param string $queue_id ID of the queue from which the event was sent (`sync` or `full_sync`). + * @return array Processed sync events. + */ + public function receive( $data, $token = null, $sent_timestamp = null, $queue_id = null ) { + $start_time = microtime( true ); + if ( ! is_array( $data ) ) { + return new \WP_Error( 'action_decoder_error', 'Events must be an array' ); + } + + if ( $token && ! $this->attempt_request_lock( $token->blog_id ) ) { + /** + * Fires when the server receives two concurrent requests from the same blog + * + * @since 4.2.0 + * + * @param token The token object of the misbehaving site + */ + do_action( 'jetpack_sync_multi_request_fail', $token ); + + return new \WP_Error( 'concurrent_request_error', 'There is another request running for the same blog ID' ); + } + + $events = wp_unslash( array_map( array( $this->codec, 'decode' ), $data ) ); + $events_processed = array(); + + /** + * Fires when an array of actions are received from a remote Jetpack site + * + * @since 4.2.0 + * + * @param array Array of actions received from the remote site + */ + do_action( 'jetpack_sync_remote_actions', $events, $token ); + + foreach ( $events as $key => $event ) { + list( $action_name, $args, $user_id, $timestamp, $silent ) = $event; + + /** + * Fires when an action is received from a remote Jetpack site + * + * @since 4.2.0 + * + * @param string $action_name The name of the action executed on the remote site + * @param array $args The arguments passed to the action + * @param int $user_id The external_user_id who did the action + * @param bool $silent Whether the item was created via import + * @param double $timestamp Timestamp (in seconds) when the action occurred + * @param double $sent_timestamp Timestamp (in seconds) when the action was transmitted + * @param string $queue_id ID of the queue from which the event was sent (sync or full_sync) + * @param array $token The auth token used to invoke the API + */ + do_action( 'jetpack_sync_remote_action', $action_name, $args, $user_id, $silent, $timestamp, $sent_timestamp, $queue_id, $token ); + + $events_processed[] = $key; + + if ( microtime( true ) - $start_time > self::MAX_TIME_PER_REQUEST_IN_SECONDS ) { + break; + } + } + + if ( $token ) { + $this->remove_request_lock( $token->blog_id ); + } + + return $events_processed; + } +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-sync/src/class-settings.php b/plugins/jetpack/vendor/automattic/jetpack-sync/src/class-settings.php new file mode 100644 index 00000000..834d3670 --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-sync/src/class-settings.php @@ -0,0 +1,440 @@ +<?php +/** + * Sync settings. + * + * @package automattic/jetpack-sync + */ + +namespace Automattic\Jetpack\Sync; + +/** + * Class to manage the sync settings. + */ +class Settings { + /** + * Prefix, used for the sync settings option names. + * + * @access public + * + * @var string + */ + const SETTINGS_OPTION_PREFIX = 'jetpack_sync_settings_'; + + /** + * A whitelist of valid settings. + * + * @access public + * @static + * + * @var array + */ + public static $valid_settings = array( + 'dequeue_max_bytes' => true, + 'upload_max_bytes' => true, + 'upload_max_rows' => true, + 'sync_wait_time' => true, + 'sync_wait_threshold' => true, + 'enqueue_wait_time' => true, + 'max_queue_size' => true, + 'max_queue_lag' => true, + 'queue_max_writes_sec' => true, + 'post_types_blacklist' => true, + 'taxonomies_blacklist' => true, + 'disable' => true, + 'network_disable' => true, + 'render_filtered_content' => true, + 'post_meta_whitelist' => true, + 'comment_meta_whitelist' => true, + 'max_enqueue_full_sync' => true, + 'max_queue_size_full_sync' => true, + 'sync_via_cron' => true, + 'cron_sync_time_limit' => true, + 'known_importers' => true, + 'term_relationships_full_sync_item_size' => true, + 'sync_sender_enabled' => true, + 'full_sync_sender_enabled' => true, + ); + + /** + * Whether WordPress is currently running an import. + * + * @access public + * @static + * + * @var null|boolean + */ + public static $is_importing; + + /** + * Whether WordPress is currently running a WP cron request. + * + * @access public + * @static + * + * @var null|boolean + */ + public static $is_doing_cron; + + /** + * Whether we're currently syncing. + * + * @access public + * @static + * + * @var null|boolean + */ + public static $is_syncing; + + /** + * Whether we're currently sending sync items. + * + * @access public + * @static + * + * @var null|boolean + */ + public static $is_sending; + + /** + * Retrieve all settings with their current values. + * + * @access public + * @static + * + * @return array All current settings. + */ + public static function get_settings() { + $settings = array(); + foreach ( array_keys( self::$valid_settings ) as $setting ) { + $settings[ $setting ] = self::get_setting( $setting ); + } + + return $settings; + } + + /** + * Fetches the setting. It saves it if the setting doesn't exist, so that it gets + * autoloaded on page load rather than re-queried every time. + * + * @access public + * @static + * + * @param string $setting The setting name. + * @return mixed The setting value. + */ + public static function get_setting( $setting ) { + if ( ! isset( self::$valid_settings[ $setting ] ) ) { + return false; + } + + if ( self::is_network_setting( $setting ) ) { + if ( is_multisite() ) { + $value = get_site_option( self::SETTINGS_OPTION_PREFIX . $setting ); + } else { + // On single sites just return the default setting. + return Defaults::get_default_setting( $setting ); + } + } else { + $value = get_option( self::SETTINGS_OPTION_PREFIX . $setting ); + } + + if ( false === $value ) { // No default value is set. + $value = Defaults::get_default_setting( $setting ); + if ( self::is_network_setting( $setting ) ) { + update_site_option( self::SETTINGS_OPTION_PREFIX . $setting, $value ); + } else { + // We set one so that it gets autoloaded. + update_option( self::SETTINGS_OPTION_PREFIX . $setting, $value, true ); + } + } + + if ( is_numeric( $value ) ) { + $value = intval( $value ); + } + $default_array_value = null; + switch ( $setting ) { + case 'post_types_blacklist': + $default_array_value = Defaults::$blacklisted_post_types; + break; + case 'taxonomies_blacklist': + $default_array_value = Defaults::$blacklisted_taxonomies; + break; + case 'post_meta_whitelist': + $default_array_value = Defaults::get_post_meta_whitelist(); + break; + case 'comment_meta_whitelist': + $default_array_value = Defaults::get_comment_meta_whitelist(); + break; + case 'known_importers': + $default_array_value = Defaults::get_known_importers(); + break; + } + + if ( $default_array_value ) { + if ( is_array( $value ) ) { + $value = array_unique( array_merge( $value, $default_array_value ) ); + } else { + $value = $default_array_value; + } + } + + return $value; + } + + /** + * Change multiple settings in the same time. + * + * @access public + * @static + * + * @param array $new_settings The new settings. + */ + public static function update_settings( $new_settings ) { + $validated_settings = array_intersect_key( $new_settings, self::$valid_settings ); + foreach ( $validated_settings as $setting => $value ) { + + if ( self::is_network_setting( $setting ) ) { + if ( is_multisite() && is_main_site() ) { + update_site_option( self::SETTINGS_OPTION_PREFIX . $setting, $value ); + } + } else { + update_option( self::SETTINGS_OPTION_PREFIX . $setting, $value, true ); + } + + // If we set the disabled option to true, clear the queues. + if ( ( 'disable' === $setting || 'network_disable' === $setting ) && ! ! $value ) { + $listener = Listener::get_instance(); + $listener->get_sync_queue()->reset(); + $listener->get_full_sync_queue()->reset(); + } + } + } + + /** + * Whether the specified setting is a network setting. + * + * @access public + * @static + * + * @param string $setting Setting name. + * @return boolean Whether the setting is a network setting. + */ + public static function is_network_setting( $setting ) { + return strpos( $setting, 'network_' ) === 0; + } + + /** + * Returns escaped SQL for blacklisted post types. + * Can be injected directly into a WHERE clause. + * + * @access public + * @static + * + * @return string SQL WHERE clause. + */ + public static function get_blacklisted_post_types_sql() { + return 'post_type NOT IN (\'' . join( '\', \'', array_map( 'esc_sql', self::get_setting( 'post_types_blacklist' ) ) ) . '\')'; + } + + /** + * Returns escaped SQL for blacklisted taxonomies. + * Can be injected directly into a WHERE clause. + * + * @access public + * @static + * + * @return string SQL WHERE clause. + */ + public static function get_blacklisted_taxonomies_sql() { + return 'taxonomy NOT IN (\'' . join( '\', \'', array_map( 'esc_sql', self::get_setting( 'taxonomies_blacklist' ) ) ) . '\')'; + } + + /** + * Returns escaped SQL for blacklisted post meta. + * Can be injected directly into a WHERE clause. + * + * @access public + * @static + * + * @return string SQL WHERE clause. + */ + public static function get_whitelisted_post_meta_sql() { + return 'meta_key IN (\'' . join( '\', \'', array_map( 'esc_sql', self::get_setting( 'post_meta_whitelist' ) ) ) . '\')'; + } + + /** + * Returns escaped SQL for blacklisted comment meta. + * Can be injected directly into a WHERE clause. + * + * @access public + * @static + * + * @return string SQL WHERE clause. + */ + public static function get_whitelisted_comment_meta_sql() { + return 'meta_key IN (\'' . join( '\', \'', array_map( 'esc_sql', self::get_setting( 'comment_meta_whitelist' ) ) ) . '\')'; + } + + /** + * Returns escaped SQL for comments, excluding any spam comments. + * Can be injected directly into a WHERE clause. + * + * @access public + * @static + * + * @return string SQL WHERE clause. + */ + public static function get_comments_filter_sql() { + return "comment_approved <> 'spam'"; + } + + /** + * Delete any settings options and clean up the current settings state. + * + * @access public + * @static + */ + public static function reset_data() { + $valid_settings = self::$valid_settings; + foreach ( $valid_settings as $option => $value ) { + delete_option( self::SETTINGS_OPTION_PREFIX . $option ); + } + self::set_importing( null ); + self::set_doing_cron( null ); + self::set_is_syncing( null ); + self::set_is_sending( null ); + } + + /** + * Set the importing state. + * + * @access public + * @static + * + * @param boolean $is_importing Whether WordPress is currently importing. + */ + public static function set_importing( $is_importing ) { + // Set to NULL to revert to WP_IMPORTING, the standard behavior. + self::$is_importing = $is_importing; + } + + /** + * Whether WordPress is currently importing. + * + * @access public + * @static + * + * @return boolean Whether WordPress is currently importing. + */ + public static function is_importing() { + if ( ! is_null( self::$is_importing ) ) { + return self::$is_importing; + } + + return defined( 'WP_IMPORTING' ) && WP_IMPORTING; + } + + /** + * Whether sync is enabled. + * + * @access public + * @static + * + * @return boolean Whether sync is enabled. + */ + public static function is_sync_enabled() { + return ! ( self::get_setting( 'disable' ) || self::get_setting( 'network_disable' ) ); + } + + /** + * Set the WP cron state. + * + * @access public + * @static + * + * @param boolean $is_doing_cron Whether WordPress is currently doing WP cron. + */ + public static function set_doing_cron( $is_doing_cron ) { + // Set to NULL to revert to WP_IMPORTING, the standard behavior. + self::$is_doing_cron = $is_doing_cron; + } + + /** + * Whether WordPress is currently doing WP cron. + * + * @access public + * @static + * + * @return boolean Whether WordPress is currently doing WP cron. + */ + public static function is_doing_cron() { + if ( ! is_null( self::$is_doing_cron ) ) { + return self::$is_doing_cron; + } + + return defined( 'DOING_CRON' ) && DOING_CRON; + } + + /** + * Whether we are currently syncing. + * + * @access public + * @static + * + * @return boolean Whether we are currently syncing. + */ + public static function is_syncing() { + return (bool) self::$is_syncing || ( defined( 'REST_API_REQUEST' ) && REST_API_REQUEST ); + } + + /** + * Set the syncing state. + * + * @access public + * @static + * + * @param boolean $is_syncing Whether we are currently syncing. + */ + public static function set_is_syncing( $is_syncing ) { + self::$is_syncing = $is_syncing; + } + + /** + * Whether we are currently sending sync items. + * + * @access public + * @static + * + * @return boolean Whether we are currently sending sync items. + */ + public static function is_sending() { + return (bool) self::$is_sending; + } + + /** + * Set the sending state. + * + * @access public + * @static + * + * @param boolean $is_sending Whether we are currently sending sync items. + */ + public static function set_is_sending( $is_sending ) { + self::$is_sending = $is_sending; + } + + /** + * Whether should send from the queue + * + * @access public + * @static + * + * @param string $queue_id The queue identifier. + * + * @return boolean Whether sync is enabled. + */ + public static function is_sender_enabled( $queue_id ) { + return (bool) self::get_setting( $queue_id . '_sender_enabled' ); + } + +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-sync/src/class-simple-codec.php b/plugins/jetpack/vendor/automattic/jetpack-sync/src/class-simple-codec.php new file mode 100644 index 00000000..613323fd --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-sync/src/class-simple-codec.php @@ -0,0 +1,63 @@ +<?php +/** + * Simple codec for encoding and decoding sync objects. + * + * @package automattic/jetpack-sync + */ + +namespace Automattic\Jetpack\Sync; + +/** + * An implementation of Automattic\Jetpack\Sync\Codec_Interface that uses base64 + * algorithm to compress objects serialized using json_encode. + */ +class Simple_Codec extends JSON_Deflate_Array_Codec { + /** + * Name of the codec. + * + * @access public + * + * @var string + */ + const CODEC_NAME = 'simple'; + + /** + * Retrieve the name of the codec. + * + * @access public + * + * @return string Name of the codec. + */ + public function name() { + return self::CODEC_NAME; + } + + /** + * Encode a sync object. + * + * @access public + * + * @param mixed $object Sync object to encode. + * @return string Encoded sync object. + */ + public function encode( $object ) { + // This is intentionally using base64_encode(). + // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode + return base64_encode( $this->json_serialize( $object ) ); + } + + /** + * Encode a sync object. + * + * @access public + * + * @param string $input Encoded sync object to decode. + * @return mixed Decoded sync object. + */ + public function decode( $input ) { + // This is intentionally using base64_decode(). + // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode + return $this->json_unserialize( base64_decode( $input ) ); + } + +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-sync/src/class-users.php b/plugins/jetpack/vendor/automattic/jetpack-sync/src/class-users.php new file mode 100644 index 00000000..efb43a28 --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-sync/src/class-users.php @@ -0,0 +1,157 @@ +<?php +/** + * Sync for users. + * + * @package automattic/jetpack-sync + */ + +namespace Automattic\Jetpack\Sync; + +use Automattic\Jetpack\Connection\Manager as Jetpack_Connection; +use Automattic\Jetpack\Roles; + +/** + * Class Users. + * + * Responsible for syncing user data changes. + */ +class Users { + /** + * Roles of all users, indexed by user ID. + * + * @access public + * @static + * + * @var array + */ + public static $user_roles = array(); + + /** + * Jetpack connection manager instance. + * + * @access public + * @static + * + * @var null|Automattic\Jetpack\Connection\Manager + */ + public static $connection = null; + + /** + * Initialize sync for user data changes. + * + * @access public + * @static + * @todo Eventually, connection needs to be instantiated at the top level in the sync package. + */ + public static function init() { + self::$connection = new Jetpack_Connection(); + if ( self::$connection->is_active() ) { + // Kick off synchronization of user role when it changes. + add_action( 'set_user_role', array( __CLASS__, 'user_role_change' ) ); + } + } + + /** + * Synchronize connected user role changes. + * + * @access public + * @static + * + * @param int $user_id ID of the user. + */ + public static function user_role_change( $user_id ) { + if ( self::$connection->is_user_connected( $user_id ) ) { + self::update_role_on_com( $user_id ); + // Try to choose a new master if we're demoting the current one. + self::maybe_demote_master_user( $user_id ); + } + } + + /** + * Retrieve the role of a user by their ID. + * + * @access public + * @static + * + * @param int $user_id ID of the user. + * @return string Role of the user. + */ + public static function get_role( $user_id ) { + if ( isset( self::$user_roles[ $user_id ] ) ) { + return self::$user_roles[ $user_id ]; + } + + $current_user_id = get_current_user_id(); + wp_set_current_user( $user_id ); + $roles = new Roles(); + $role = $roles->translate_current_user_to_role(); + wp_set_current_user( $current_user_id ); + self::$user_roles[ $user_id ] = $role; + + return $role; + } + + /** + * Retrieve the signed role of a user by their ID. + * + * @access public + * @static + * + * @param int $user_id ID of the user. + * @return string Signed role of the user. + */ + public static function get_signed_role( $user_id ) { + return \Jetpack::connection()->sign_role( self::get_role( $user_id ), $user_id ); + } + + /** + * Retrieve the signed role and update it in WP.com for that user. + * + * @access public + * @static + * + * @param int $user_id ID of the user. + */ + public static function update_role_on_com( $user_id ) { + $signed_role = self::get_signed_role( $user_id ); + \Jetpack::xmlrpc_async_call( 'jetpack.updateRole', $user_id, $signed_role ); + } + + /** + * Choose a new master user if we're demoting the current one. + * + * @access public + * @static + * @todo Disconnect if there is no user with enough capabilities to be the master user. + * @uses \WP_User_Query + * + * @param int $user_id ID of the user. + */ + public static function maybe_demote_master_user( $user_id ) { + $master_user_id = (int) \Jetpack_Options::get_option( 'master_user' ); + $role = self::get_role( $user_id ); + if ( $user_id === $master_user_id && 'administrator' !== $role ) { + $query = new \WP_User_Query( + array( + 'fields' => array( 'id' ), + 'role' => 'administrator', + 'orderby' => 'id', + 'exclude' => array( $master_user_id ), + ) + ); + $new_master = false; + foreach ( $query->results as $result ) { + $found_user_id = absint( $result->id ); + if ( $found_user_id && self::$connection->is_user_connected( $found_user_id ) ) { + $new_master = $found_user_id; + break; + } + } + + if ( $new_master ) { + \Jetpack_Options::update_option( 'master_user', $new_master ); + } + // TODO: else disconnect..? + } + } +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-sync/src/class-utils.php b/plugins/jetpack/vendor/automattic/jetpack-sync/src/class-utils.php new file mode 100644 index 00000000..23f24e95 --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-sync/src/class-utils.php @@ -0,0 +1,65 @@ +<?php +/** + * Sync utils. + * + * @package automattic/jetpack-sync + */ + +namespace Automattic\Jetpack\Sync; + +/** + * Class for sync utilities. + */ +class Utils { + /** + * Retrieve the values of sync items. + * + * @access public + * @static + * + * @param array $items Array of sync items. + * @return array Array of sync item values. + */ + public static function get_item_values( $items ) { + return array_map( array( __CLASS__, 'get_item_value' ), $items ); + } + + /** + * Retrieve the IDs of sync items. + * + * @access public + * @static + * + * @param array $items Array of sync items. + * @return array Array of sync item IDs. + */ + public static function get_item_ids( $items ) { + return array_map( array( __CLASS__, 'get_item_id' ), $items ); + } + + /** + * Get the value of a sync item. + * + * @access private + * @static + * + * @param array $item Sync item. + * @return mixed Sync item value. + */ + private static function get_item_value( $item ) { + return $item->value; + } + + /** + * Get the ID of a sync item. + * + * @access private + * @static + * + * @param array $item Sync item. + * @return int Sync item ID. + */ + private static function get_item_id( $item ) { + return $item->id; + } +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-sync/src/interface-codec.php b/plugins/jetpack/vendor/automattic/jetpack-sync/src/interface-codec.php new file mode 100644 index 00000000..7653f26d --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-sync/src/interface-codec.php @@ -0,0 +1,44 @@ +<?php +/** + * Interface for encoding and decoding sync objects. + * + * @package automattic/jetpack-sync + */ + +namespace Automattic\Jetpack\Sync; + +/** + * Very simple interface for encoding and decoding input. + * This is used to provide compression and serialization to messages. + **/ +interface Codec_Interface { + /** + * Retrieve the name of the codec. + * We send this with the payload so we can select the appropriate decoder at the other end. + * + * @access public + * + * @return string Name of the codec. + */ + public function name(); + + /** + * Encode a sync object. + * + * @access public + * + * @param mixed $object Sync object to encode. + * @return string Encoded sync object. + */ + public function encode( $object ); + + /** + * Encode a sync object. + * + * @access public + * + * @param string $input Encoded sync object to decode. + * @return mixed Decoded sync object. + */ + public function decode( $input ); +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-sync/src/interface-replicastore.php b/plugins/jetpack/vendor/automattic/jetpack-sync/src/interface-replicastore.php new file mode 100644 index 00000000..90918803 --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-sync/src/interface-replicastore.php @@ -0,0 +1,566 @@ +<?php +/** + * Sync architecture prototype. + * + * To run tests: phpunit --testsuite sync --filter New_Sync + * + * @author Dan Walmsley + * @package automattic/jetpack-sync + */ + +namespace Automattic\Jetpack\Sync; + +/** + * A high-level interface for objects that store synced WordPress data. + * Useful for ensuring that different storage mechanisms implement the + * required semantics for storing all the data that we sync. + */ +interface Replicastore_Interface { + /** + * Empty and reset the replicastore. + * + * @access public + */ + public function reset(); + + /** + * Ran when full sync has just started. + * + * @access public + * + * @param array $config Full sync configuration for this sync module. + */ + public function full_sync_start( $config ); + + /** + * Ran when full sync has just finished. + * + * @access public + * + * @param string $checksum Deprecated since 7.3.0. + */ + public function full_sync_end( $checksum ); + + /** + * Retrieve the number of posts with a particular post status within a certain range. + * + * @access public + * + * @todo Prepare the SQL query before executing it. + * + * @param string $status Post status. + * @param int $min_id Minimum post ID. + * @param int $max_id Maximum post ID. + */ + public function post_count( $status = null, $min_id = null, $max_id = null ); + + /** + * Retrieve the posts with a particular post status. + * + * @access public + * + * @param string $status Post status. + * @param int $min_id Minimum post ID. + * @param int $max_id Maximum post ID. + */ + public function get_posts( $status = null, $min_id = null, $max_id = null ); + + /** + * Retrieve a post object by the post ID. + * + * @access public + * + * @param int $id Post ID. + */ + public function get_post( $id ); + + /** + * Update or insert a post. + * + * @access public + * + * @param \WP_Post $post Post object. + * @param bool $silent Whether to perform a silent action. + */ + public function upsert_post( $post, $silent = false ); + + /** + * Delete a post by the post ID. + * + * @access public + * + * @param int $post_id Post ID. + */ + public function delete_post( $post_id ); + + /** + * Retrieve the checksum for posts within a range. + * + * @access public + * + * @param int $min_id Minimum post ID. + * @param int $max_id Maximum post ID. + */ + public function posts_checksum( $min_id = null, $max_id = null ); + + /** + * Retrieve the checksum for post meta within a range. + * + * @access public + * + * @param int $min_id Minimum post meta ID. + * @param int $max_id Maximum post meta ID. + */ + public function post_meta_checksum( $min_id = null, $max_id = null ); + + /** + * Retrieve the number of comments with a particular comment status within a certain range. + * + * @access public + * + * @param string $status Comment status. + * @param int $min_id Minimum comment ID. + * @param int $max_id Maximum comment ID. + */ + public function comment_count( $status = null, $min_id = null, $max_id = null ); + + /** + * Retrieve the comments with a particular comment status. + * + * @access public + * + * @param string $status Comment status. + * @param int $min_id Minimum comment ID. + * @param int $max_id Maximum comment ID. + */ + public function get_comments( $status = null, $min_id = null, $max_id = null ); + + /** + * Retrieve a comment object by the comment ID. + * + * @access public + * + * @param int $id Comment ID. + */ + public function get_comment( $id ); + + /** + * Update or insert a comment. + * + * @access public + * + * @param \WP_Comment $comment Comment object. + */ + public function upsert_comment( $comment ); + + /** + * Trash a comment by the comment ID. + * + * @access public + * + * @param int $comment_id Comment ID. + */ + public function trash_comment( $comment_id ); + + /** + * Mark a comment by the comment ID as spam. + * + * @access public + * + * @param int $comment_id Comment ID. + */ + public function spam_comment( $comment_id ); + + /** + * Delete a comment by the comment ID. + * + * @access public + * + * @param int $comment_id Comment ID. + */ + public function delete_comment( $comment_id ); + + /** + * Trash the comments of a post. + * + * @access public + * + * @param int $post_id Post ID. + * @param array $statuses Post statuses. + */ + public function trashed_post_comments( $post_id, $statuses ); + + /** + * Untrash the comments of a post. + * + * @access public + * + * @param int $post_id Post ID. + */ + public function untrashed_post_comments( $post_id ); + + /** + * Retrieve the checksum for comments within a range. + * + * @access public + * + * @param int $min_id Minimum comment ID. + * @param int $max_id Maximum comment ID. + */ + public function comments_checksum( $min_id = null, $max_id = null ); + + /** + * Retrieve the checksum for comment meta within a range. + * + * @access public + * + * @param int $min_id Minimum comment meta ID. + * @param int $max_id Maximum comment meta ID. + */ + public function comment_meta_checksum( $min_id = null, $max_id = null ); + + /** + * Update the value of an option. + * + * @access public + * + * @param string $option Option name. + * @param mixed $value Option value. + */ + public function update_option( $option, $value ); + + /** + * Retrieve an option value based on an option name. + * + * @access public + * + * @param string $option Name of option to retrieve. + * @param mixed $default Optional. Default value to return if the option does not exist. + */ + public function get_option( $option, $default = false ); + + /** + * Remove an option by name. + * + * @access public + * + * @param string $option Name of option to remove. + */ + public function delete_option( $option ); + + /** + * Change the features that the current theme supports. + * + * @access public + * + * @param array $theme_support Features that the theme supports. + */ + public function set_theme_support( $theme_support ); + + /** + * Whether the current theme supports a certain feature. + * + * @access public + * + * @param string $feature Name of the feature. + */ + public function current_theme_supports( $feature ); + + /** + * Retrieve metadata for the specified object. + * + * @access public + * + * @param string $type Meta type. + * @param int $object_id ID of the object. + * @param string $meta_key Meta key. + * @param bool $single If true, return only the first value of the specified meta_key. + */ + public function get_metadata( $type, $object_id, $meta_key = '', $single = false ); + + /** + * Stores remote meta key/values alongside an ID mapping key. + * + * @access public + * + * @param string $type Meta type. + * @param int $object_id ID of the object. + * @param string $meta_key Meta key. + * @param mixed $meta_value Meta value. + * @param int $meta_id ID of the meta. + */ + public function upsert_metadata( $type, $object_id, $meta_key, $meta_value, $meta_id ); + + /** + * Delete metadata for the specified object. + * + * @access public + * + * @param string $type Meta type. + * @param int $object_id ID of the object. + * @param array $meta_ids IDs of the meta objects to delete. + */ + public function delete_metadata( $type, $object_id, $meta_ids ); + + /** + * Delete metadata with a certain key for the specified objects. + * + * @access public + * + * @param string $type Meta type. + * @param array $object_ids IDs of the objects. + * @param string $meta_key Meta key. + */ + public function delete_batch_metadata( $type, $object_ids, $meta_key ); + + /** + * Retrieve value of a constant based on the constant name. + * + * @access public + * + * @param string $constant Name of constant to retrieve. + */ + public function get_constant( $constant ); + + /** + * Set the value of a constant. + * + * @access public + * + * @param string $constant Name of constant to retrieve. + * @param mixed $value Value set for the constant. + */ + public function set_constant( $constant, $value ); + + /** + * Retrieve the number of the available updates of a certain type. + * Type is one of: `plugins`, `themes`, `wordpress`, `translations`, `total`, `wp_update_version`. + * + * @access public + * + * @param string $type Type of updates to retrieve. + */ + public function get_updates( $type ); + + /** + * Set the available updates of a certain type. + * Type is one of: `plugins`, `themes`, `wordpress`, `translations`, `total`, `wp_update_version`. + * + * @access public + * + * @param string $type Type of updates to set. + * @param int $updates Total number of updates. + */ + public function set_updates( $type, $updates ); + + /** + * Retrieve a callable value based on its name. + * + * @access public + * + * @param string $callable Name of the callable to retrieve. + */ + public function get_callable( $callable ); + + /** + * Update the value of a callable. + * + * @access public + * + * @param string $callable Callable name. + * @param mixed $value Callable value. + */ + public function set_callable( $callable, $value ); + + /** + * Retrieve a network option value based on a network option name. + * + * @access public + * + * @param string $option Name of network option to retrieve. + */ + public function get_site_option( $option ); + + /** + * Update the value of a network option. + * + * @access public + * + * @param string $option Network option name. + * @param mixed $value Network option value. + */ + public function update_site_option( $option, $value ); + + /** + * Remove a network option by name. + * + * @access public + * + * @param string $option Name of option to remove. + */ + public function delete_site_option( $option ); + + /** + * Retrieve the terms from a particular taxonomy. + * + * @access public + * + * @param string $taxonomy Taxonomy slug. + */ + public function get_terms( $taxonomy ); + + /** + * Retrieve a particular term. + * + * @access public + * + * @param string $taxonomy Taxonomy slug. + * @param int $term_id ID of the term. + * @param bool $is_term_id Whether this is a `term_id` or a `term_taxonomy_id`. + */ + public function get_term( $taxonomy, $term_id, $is_term_id = true ); + + /** + * Insert or update a term. + * + * @access public + * + * @param \WP_Term $term_object Term object. + */ + public function update_term( $term_object ); + + /** + * Delete a term by the term ID and its corresponding taxonomy. + * + * @access public + * + * @param int $term_id Term ID. + * @param string $taxonomy Taxonomy slug. + */ + public function delete_term( $term_id, $taxonomy ); + + /** + * Retrieve all terms from a taxonomy that are related to an object with a particular ID. + * + * @access public + * + * @param int $object_id Object ID. + * @param string $taxonomy Taxonomy slug. + */ + public function get_the_terms( $object_id, $taxonomy ); + + /** + * Add/update terms of a particular taxonomy of an object with the specified ID. + * + * @access public + * + * @param int $object_id The object to relate to. + * @param string $taxonomy The context in which to relate the term to the object. + * @param string|int|array $terms A single term slug, single term id, or array of either term slugs or ids. + * @param bool $append Optional. If false will delete difference of terms. Default false. + */ + public function update_object_terms( $object_id, $taxonomy, $terms, $append ); + + /** + * Remove certain term relationships from the specified object. + * + * @access public + * + * @todo Refactor to not use interpolated values when preparing the SQL query. + * + * @param int $object_id ID of the object. + * @param array $tt_ids Term taxonomy IDs. + */ + public function delete_object_terms( $object_id, $tt_ids ); + + /** + * Retrieve the number of users. + * + * @access public + */ + public function user_count(); + + /** + * Retrieve a user object by the user ID. + * + * @access public + * + * @param int $user_id User ID. + */ + public function get_user( $user_id ); + + /** + * Insert or update a user. + * + * @access public + * + * @param \WP_User $user User object. + */ + public function upsert_user( $user ); + + /** + * Delete a user. + * + * @access public + * + * @param int $user_id User ID. + */ + public function delete_user( $user_id ); + + /** + * Update/insert user locale. + * + * @access public + * + * @param int $user_id User ID. + * @param string $locale The user locale. + */ + public function upsert_user_locale( $user_id, $locale ); + + /** + * Delete user locale. + * + * @access public + * + * @param int $user_id User ID. + */ + public function delete_user_locale( $user_id ); + + /** + * Retrieve the user locale. + * + * @access public + * + * @param int $user_id User ID. + */ + public function get_user_locale( $user_id ); + + /** + * Retrieve the allowed mime types for the user. + * + * @access public + * + * @param int $user_id User ID. + */ + public function get_allowed_mime_types( $user_id ); + + /** + * Retrieve all the checksums we are interested in. + * Currently that is posts, comments, post meta and comment meta. + * + * @access public + */ + public function checksum_all(); + + /** + * Retrieve the checksum histogram for a specific object type. + * + * @access public + * + * @param string $object_type Object type. + * @param int $buckets Number of buckets to split the objects to. + * @param int $start_id Minimum object ID. + * @param int $end_id Maximum object ID. + */ + public function checksum_histogram( $object_type, $buckets, $start_id = null, $end_id = null ); +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-attachments.php b/plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-attachments.php new file mode 100644 index 00000000..bf716be3 --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-attachments.php @@ -0,0 +1,95 @@ +<?php +/** + * Attachments sync module. + * + * @package automattic/jetpack-sync + */ + +namespace Automattic\Jetpack\Sync\Modules; + +/** + * Class to handle sync for attachments. + */ +class Attachments extends Module { + /** + * Sync module name. + * + * @access public + * + * @return string + */ + public function name() { + return 'attachments'; + } + + /** + * Initialize attachment action listeners. + * + * @access public + * + * @param callable $callable Action handler callable. + */ + public function init_listeners( $callable ) { + add_action( 'add_attachment', array( $this, 'process_add' ) ); + add_action( 'attachment_updated', array( $this, 'process_update' ), 10, 3 ); + add_action( 'jetpack_sync_save_update_attachment', $callable, 10, 2 ); + add_action( 'jetpack_sync_save_add_attachment', $callable, 10, 2 ); + add_action( 'jetpack_sync_save_attach_attachment', $callable, 10, 2 ); + } + + /** + * Handle the creation of a new attachment. + * + * @access public + * + * @param int $attachment_id ID of the attachment. + */ + public function process_add( $attachment_id ) { + $attachment = get_post( $attachment_id ); + /** + * Fires when the client needs to sync an new attachment + * + * @since 4.2.0 + * + * @param int Attachment ID. + * @param \WP_Post Attachment post object. + */ + do_action( 'jetpack_sync_save_add_attachment', $attachment_id, $attachment ); + } + + /** + * Handle updating an existing attachment. + * + * @access public + * + * @param int $attachment_id Attachment ID. + * @param \WP_Post $attachment_after Attachment post object before the update. + * @param \WP_Post $attachment_before Attachment post object after the update. + */ + public function process_update( $attachment_id, $attachment_after, $attachment_before ) { + // Check whether attachment was added to a post for the first time. + if ( 0 === $attachment_before->post_parent && 0 !== $attachment_after->post_parent ) { + /** + * Fires when an existing attachment is added to a post for the first time + * + * @since 6.6.0 + * + * @param int $attachment_id Attachment ID. + * @param \WP_Post $attachment_after Attachment post object after the update. + */ + do_action( 'jetpack_sync_save_attach_attachment', $attachment_id, $attachment_after ); + } else { + /** + * Fires when the client needs to sync an updated attachment + * + * @since 4.9.0 + * + * @param int $attachment_id Attachment ID. + * @param \WP_Post $attachment_after Attachment post object after the update. + * + * Previously this action was synced using jetpack_sync_save_add_attachment action. + */ + do_action( 'jetpack_sync_save_update_attachment', $attachment_id, $attachment_after ); + } + } +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-callables.php b/plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-callables.php new file mode 100644 index 00000000..d8ac3e9e --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-callables.php @@ -0,0 +1,491 @@ +<?php +/** + * Callables sync module. + * + * @package automattic/jetpack-sync + */ + +namespace Automattic\Jetpack\Sync\Modules; + +use Automattic\Jetpack\Sync\Functions; +use Automattic\Jetpack\Sync\Defaults; +use Automattic\Jetpack\Sync\Settings; +use Automattic\Jetpack\Constants as Jetpack_Constants; + +/** + * Class to handle sync for callables. + */ +class Callables extends Module { + /** + * Name of the callables checksum option. + * + * @var string + */ + const CALLABLES_CHECKSUM_OPTION_NAME = 'jetpack_callables_sync_checksum'; + + /** + * Name of the transient for locking callables. + * + * @var string + */ + const CALLABLES_AWAIT_TRANSIENT_NAME = 'jetpack_sync_callables_await'; + + /** + * Whitelist for callables we want to sync. + * + * @access private + * + * @var array + */ + private $callable_whitelist; + + /** + * For some options, we should always send the change right away! + * + * @access public + * + * @var array + */ + const ALWAYS_SEND_UPDATES_TO_THESE_OPTIONS = array( + 'jetpack_active_modules', + 'home', // option is home, callable is home_url. + 'siteurl', + 'jetpack_sync_error_idc', + 'paused_plugins', + 'paused_themes', + ); + + /** + * For some options, the callable key differs from the option name/key + * + * @access public + * + * @var array + */ + const OPTION_NAMES_TO_CALLABLE_NAMES = array( + // @TODO: Audit the other option names for differences between the option names and callable names. + 'home' => 'home_url', + 'siteurl' => 'site_url', + ); + + /** + * Sync module name. + * + * @access public + * + * @return string + */ + public function name() { + return 'functions'; + } + + /** + * Set module defaults. + * Define the callable whitelist based on whether this is a single site or a multisite installation. + * + * @access public + */ + public function set_defaults() { + if ( is_multisite() ) { + $this->callable_whitelist = array_merge( Defaults::get_callable_whitelist(), Defaults::get_multisite_callable_whitelist() ); + } else { + $this->callable_whitelist = Defaults::get_callable_whitelist(); + } + } + + /** + * Initialize callables action listeners. + * + * @access public + * + * @param callable $callable Action handler callable. + */ + public function init_listeners( $callable ) { + add_action( 'jetpack_sync_callable', $callable, 10, 2 ); + add_action( 'current_screen', array( $this, 'set_plugin_action_links' ), 9999 ); // Should happen very late. + + foreach ( self::ALWAYS_SEND_UPDATES_TO_THESE_OPTIONS as $option ) { + add_action( "update_option_{$option}", array( $this, 'unlock_sync_callable' ) ); + add_action( "delete_option_{$option}", array( $this, 'unlock_sync_callable' ) ); + } + + // Provide a hook so that hosts can send changes to certain callables right away. + // Especially useful when a host uses constants to change home and siteurl. + add_action( 'jetpack_sync_unlock_sync_callable', array( $this, 'unlock_sync_callable' ) ); + + // get_plugins and wp_version + // gets fired when new code gets installed, updates etc. + add_action( 'upgrader_process_complete', array( $this, 'unlock_plugin_action_link_and_callables' ) ); + add_action( 'update_option_active_plugins', array( $this, 'unlock_plugin_action_link_and_callables' ) ); + } + + /** + * Initialize callables action listeners for full sync. + * + * @access public + * + * @param callable $callable Action handler callable. + */ + public function init_full_sync_listeners( $callable ) { + add_action( 'jetpack_full_sync_callables', $callable ); + } + + /** + * Initialize the module in the sender. + * + * @access public + */ + public function init_before_send() { + add_action( 'jetpack_sync_before_send_queue_sync', array( $this, 'maybe_sync_callables' ) ); + + // Full sync. + add_filter( 'jetpack_sync_before_send_jetpack_full_sync_callables', array( $this, 'expand_callables' ) ); + } + + /** + * Perform module cleanup. + * Deletes any transients and options that this module uses. + * Usually triggered when uninstalling the plugin. + * + * @access public + */ + public function reset_data() { + delete_option( self::CALLABLES_CHECKSUM_OPTION_NAME ); + delete_transient( self::CALLABLES_AWAIT_TRANSIENT_NAME ); + + $url_callables = array( 'home_url', 'site_url', 'main_network_site_url' ); + foreach ( $url_callables as $callable ) { + delete_option( Functions::HTTPS_CHECK_OPTION_PREFIX . $callable ); + } + } + + /** + * Set the callable whitelist. + * + * @access public + * + * @param array $callables The new callables whitelist. + */ + public function set_callable_whitelist( $callables ) { + $this->callable_whitelist = $callables; + } + + /** + * Get the callable whitelist. + * + * @access public + * + * @return array The callables whitelist. + */ + public function get_callable_whitelist() { + return $this->callable_whitelist; + } + + /** + * Retrieve all callables as per the current callables whitelist. + * + * @access public + * + * @return array All callables. + */ + public function get_all_callables() { + // get_all_callables should run as the master user always. + $current_user_id = get_current_user_id(); + wp_set_current_user( \Jetpack_Options::get_option( 'master_user' ) ); + $callables = array_combine( + array_keys( $this->get_callable_whitelist() ), + array_map( array( $this, 'get_callable' ), array_values( $this->get_callable_whitelist() ) ) + ); + wp_set_current_user( $current_user_id ); + return $callables; + } + + /** + * Invoke a particular callable. + * Used as a wrapper to standartize invocation. + * + * @access private + * + * @param callable $callable Callable to invoke. + * @return mixed Return value of the callable. + */ + private function get_callable( $callable ) { + return call_user_func( $callable ); + } + + /** + * Enqueue the callable actions for full sync. + * + * @access public + * + * @param array $config Full sync configuration for this sync module. + * @param int $max_items_to_enqueue Maximum number of items to enqueue. + * @param boolean $state True if full sync has finished enqueueing this module, false otherwise. + * @return array Number of actions enqueued, and next module state. + */ + public function enqueue_full_sync_actions( $config, $max_items_to_enqueue, $state ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable + /** + * Tells the client to sync all callables to the server + * + * @since 4.2.0 + * + * @param boolean Whether to expand callables (should always be true) + */ + do_action( 'jetpack_full_sync_callables', true ); + + // The number of actions enqueued, and next module state (true == done). + return array( 1, true ); + } + + /** + * Retrieve an estimated number of actions that will be enqueued. + * + * @access public + * + * @param array $config Full sync configuration for this sync module. + * @return array Number of items yet to be enqueued. + */ + public function estimate_full_sync_actions( $config ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable + return 1; + } + + /** + * Retrieve the actions that will be sent for this module during a full sync. + * + * @access public + * + * @return array Full sync actions of this module. + */ + public function get_full_sync_actions() { + return array( 'jetpack_full_sync_callables' ); + } + + /** + * Unlock callables so they would be available for syncing again. + * + * @access public + */ + public function unlock_sync_callable() { + delete_transient( self::CALLABLES_AWAIT_TRANSIENT_NAME ); + } + + /** + * Unlock callables and plugin action links. + * + * @access public + */ + public function unlock_plugin_action_link_and_callables() { + delete_transient( self::CALLABLES_AWAIT_TRANSIENT_NAME ); + delete_transient( 'jetpack_plugin_api_action_links_refresh' ); + add_filter( 'jetpack_check_and_send_callables', '__return_true' ); + } + + /** + * Parse and store the plugin action links if on the plugins page. + * + * @uses \DOMDocument + * @uses libxml_use_internal_errors + * @uses mb_convert_encoding + * + * @access public + */ + public function set_plugin_action_links() { + if ( + ! class_exists( '\DOMDocument' ) || + ! function_exists( 'libxml_use_internal_errors' ) || + ! function_exists( 'mb_convert_encoding' ) + ) { + return; + } + + $current_screeen = get_current_screen(); + + $plugins_action_links = array(); + // Is the transient lock in place? + $plugins_lock = get_transient( 'jetpack_plugin_api_action_links_refresh', false ); + if ( ! empty( $plugins_lock ) && ( isset( $current_screeen->id ) && 'plugins' !== $current_screeen->id ) ) { + return; + } + $plugins = array_keys( Functions::get_plugins() ); + foreach ( $plugins as $plugin_file ) { + /** + * Plugins often like to unset things but things break if they are not able to. + */ + $action_links = array( + 'deactivate' => '', + 'activate' => '', + 'details' => '', + 'delete' => '', + 'edit' => '', + ); + /** This filter is documented in src/wp-admin/includes/class-wp-plugins-list-table.php */ + $action_links = apply_filters( 'plugin_action_links', $action_links, $plugin_file, null, 'all' ); + /** This filter is documented in src/wp-admin/includes/class-wp-plugins-list-table.php */ + $action_links = apply_filters( "plugin_action_links_{$plugin_file}", $action_links, $plugin_file, null, 'all' ); + $action_links = array_filter( $action_links ); + $formatted_action_links = null; + if ( ! empty( $action_links ) && count( $action_links ) > 0 ) { + $dom_doc = new \DOMDocument(); + foreach ( $action_links as $action_link ) { + // The @ is not enough to suppress errors when dealing with libxml, + // we have to tell it directly how we want to handle errors. + libxml_use_internal_errors( true ); + $dom_doc->loadHTML( mb_convert_encoding( $action_link, 'HTML-ENTITIES', 'UTF-8' ) ); + libxml_use_internal_errors( false ); + + $link_elements = $dom_doc->getElementsByTagName( 'a' ); + if ( 0 === $link_elements->length ) { + continue; + } + + $link_element = $link_elements->item( 0 ); + // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + if ( $link_element->hasAttribute( 'href' ) && $link_element->nodeValue ) { + $link_url = trim( $link_element->getAttribute( 'href' ) ); + + // Add the full admin path to the url if the plugin did not provide it. + $link_url_scheme = wp_parse_url( $link_url, PHP_URL_SCHEME ); + if ( empty( $link_url_scheme ) ) { + $link_url = admin_url( $link_url ); + } + + // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + $formatted_action_links[ $link_element->nodeValue ] = $link_url; + } + } + } + if ( $formatted_action_links ) { + $plugins_action_links[ $plugin_file ] = $formatted_action_links; + } + } + // Cache things for a long time. + set_transient( 'jetpack_plugin_api_action_links_refresh', time(), DAY_IN_SECONDS ); + update_option( 'jetpack_plugin_api_action_links', $plugins_action_links ); + } + + /** + * Whether a certain callable should be sent. + * + * @access public + * + * @param array $callable_checksums Callable checksums. + * @param string $name Name of the callable. + * @param string $checksum A checksum of the callable. + * @return boolean Whether to send the callable. + */ + public function should_send_callable( $callable_checksums, $name, $checksum ) { + $idc_override_callables = array( + 'main_network_site', + 'home_url', + 'site_url', + ); + if ( in_array( $name, $idc_override_callables, true ) && \Jetpack_Options::get_option( 'migrate_for_idc' ) ) { + return true; + } + + return ! $this->still_valid_checksum( $callable_checksums, $name, $checksum ); + } + + /** + * Sync the callables if we're supposed to. + * + * @access public + */ + public function maybe_sync_callables() { + + $callables = $this->get_all_callables(); + if ( ! apply_filters( 'jetpack_check_and_send_callables', false ) ) { + if ( ! is_admin() ) { + // If we're not an admin and we're not doing cron and this isn't WP_CLI, don't sync anything. + if ( ! Settings::is_doing_cron() && ! Jetpack_Constants::get_constant( 'WP_CLI' ) ) { + return; + } + // If we're not an admin and we are doing cron, sync the Callables that are always supposed to sync ( See https://github.com/Automattic/jetpack/issues/12924 ). + $callables = $this->get_always_sent_callables(); + } + if ( get_transient( self::CALLABLES_AWAIT_TRANSIENT_NAME ) ) { + return; + } + } + + if ( empty( $callables ) ) { + return; + } + + set_transient( self::CALLABLES_AWAIT_TRANSIENT_NAME, microtime( true ), Defaults::$default_sync_callables_wait_time ); + + $callable_checksums = (array) \Jetpack_Options::get_raw_option( self::CALLABLES_CHECKSUM_OPTION_NAME, array() ); + $has_changed = false; + // Only send the callables that have changed. + foreach ( $callables as $name => $value ) { + $checksum = $this->get_check_sum( $value ); + // Explicitly not using Identical comparison as get_option returns a string. + if ( ! is_null( $value ) && $this->should_send_callable( $callable_checksums, $name, $checksum ) ) { + /** + * Tells the client to sync a callable (aka function) to the server + * + * @since 4.2.0 + * + * @param string The name of the callable + * @param mixed The value of the callable + */ + do_action( 'jetpack_sync_callable', $name, $value ); + $callable_checksums[ $name ] = $checksum; + $has_changed = true; + } else { + $callable_checksums[ $name ] = $checksum; + } + } + if ( $has_changed ) { + \Jetpack_Options::update_raw_option( self::CALLABLES_CHECKSUM_OPTION_NAME, $callable_checksums ); + } + + } + + /** + * Get the callables that should always be sent, e.g. on cron. + * + * @return array Callables that should always be sent + */ + protected function get_always_sent_callables() { + $callables = $this->get_all_callables(); + $cron_callables = array(); + foreach ( self::ALWAYS_SEND_UPDATES_TO_THESE_OPTIONS as $option_name ) { + if ( array_key_exists( $option_name, $callables ) ) { + $cron_callables[ $option_name ] = $callables[ $option_name ]; + continue; + } + + // Check for the Callable name/key for the option, if different from option name. + if ( array_key_exists( $option_name, self::OPTION_NAMES_TO_CALLABLE_NAMES ) ) { + $callable_name = self::OPTION_NAMES_TO_CALLABLE_NAMES[ $option_name ]; + if ( array_key_exists( $callable_name, $callables ) ) { + $cron_callables[ $callable_name ] = $callables[ $callable_name ]; + } + } + } + return $cron_callables; + } + + /** + * Expand the callables within a hook before they are serialized and sent to the server. + * + * @access public + * + * @param array $args The hook parameters. + * @return array $args The hook parameters. + */ + public function expand_callables( $args ) { + if ( $args[0] ) { + $callables = $this->get_all_callables(); + $callables_checksums = array(); + foreach ( $callables as $name => $value ) { + $callables_checksums[ $name ] = $this->get_check_sum( $value ); + } + \Jetpack_Options::update_raw_option( self::CALLABLES_CHECKSUM_OPTION_NAME, $callables_checksums ); + return $callables; + } + + return $args; + } +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-comments.php b/plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-comments.php new file mode 100644 index 00000000..e956748c --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-comments.php @@ -0,0 +1,411 @@ +<?php +/** + * Comments sync module. + * + * @package automattic/jetpack-sync + */ + +namespace Automattic\Jetpack\Sync\Modules; + +use Automattic\Jetpack\Sync\Settings; + +/** + * Class to handle sync for comments. + */ +class Comments extends Module { + /** + * Sync module name. + * + * @access public + * + * @return string + */ + public function name() { + return 'comments'; + } + + /** + * The id field in the database. + * + * @access public + * + * @return string + */ + public function id_field() { + return 'comment_ID'; + } + + /** + * The table in the database. + * + * @access public + * + * @return string + */ + public function table_name() { + return 'comments'; + } + + /** + * Retrieve a comment by its ID. + * + * @access public + * + * @param string $object_type Type of the sync object. + * @param int $id ID of the sync object. + * @return \WP_Comment|bool Filtered \WP_Comment object, or false if the object is not a comment. + */ + public function get_object_by_id( $object_type, $id ) { + $comment_id = intval( $id ); + if ( 'comment' === $object_type ) { + $comment = get_comment( $comment_id ); + if ( $comment ) { + return $this->filter_comment( $comment ); + } + } + + return false; + } + + /** + * Initialize comments action listeners. + * Also responsible for initializing comment meta listeners. + * + * @access public + * + * @param callable $callable Action handler callable. + */ + public function init_listeners( $callable ) { + add_action( 'wp_insert_comment', $callable, 10, 2 ); + add_action( 'deleted_comment', $callable ); + add_action( 'trashed_comment', $callable ); + add_action( 'spammed_comment', $callable ); + add_action( 'trashed_post_comments', $callable, 10, 2 ); + add_action( 'untrash_post_comments', $callable ); + add_action( 'comment_approved_to_unapproved', $callable ); + add_action( 'comment_unapproved_to_approved', $callable ); + add_action( 'jetpack_modified_comment_contents', $callable, 10, 2 ); + add_action( 'untrashed_comment', $callable, 10, 2 ); + add_action( 'unspammed_comment', $callable, 10, 2 ); + add_filter( 'wp_update_comment_data', array( $this, 'handle_comment_contents_modification' ), 10, 3 ); + add_filter( 'jetpack_sync_before_enqueue_wp_insert_comment', array( $this, 'only_allow_white_listed_comment_types' ) ); + + /** + * Even though it's messy, we implement these hooks because + * the edit_comment hook doesn't include the data + * so this saves us a DB read for every comment event. + */ + foreach ( $this->get_whitelisted_comment_types() as $comment_type ) { + foreach ( array( 'unapproved', 'approved' ) as $comment_status ) { + $comment_action_name = "comment_{$comment_status}_{$comment_type}"; + add_action( $comment_action_name, $callable, 10, 2 ); + } + } + + // Listen for meta changes. + $this->init_listeners_for_meta_type( 'comment', $callable ); + $this->init_meta_whitelist_handler( 'comment', array( $this, 'filter_meta' ) ); + } + + /** + * Handler for any comment content updates. + * + * @access public + * + * @param array $new_comment The new, processed comment data. + * @param array $old_comment The old, unslashed comment data. + * @param array $new_comment_with_slashes The new, raw comment data. + * @return array The new, processed comment data. + */ + public function handle_comment_contents_modification( $new_comment, $old_comment, $new_comment_with_slashes ) { + $changes = array(); + $content_fields = array( + 'comment_author', + 'comment_author_email', + 'comment_author_url', + 'comment_content', + ); + foreach ( $content_fields as $field ) { + if ( $new_comment_with_slashes[ $field ] !== $old_comment[ $field ] ) { + $changes[ $field ] = array( $new_comment[ $field ], $old_comment[ $field ] ); + } + } + + if ( ! empty( $changes ) ) { + /** + * Signals to the sync listener that this comment's contents were modified and a sync action + * reflecting the change(s) to the content should be sent + * + * @since 4.9.0 + * + * @param int $new_comment['comment_ID'] ID of comment whose content was modified + * @param mixed $changes Array of changed comment fields with before and after values + */ + do_action( 'jetpack_modified_comment_contents', $new_comment['comment_ID'], $changes ); + } + return $new_comment; + } + + /** + * Initialize comments action listeners for full sync. + * + * @access public + * + * @param callable $callable Action handler callable. + */ + public function init_full_sync_listeners( $callable ) { + add_action( 'jetpack_full_sync_comments', $callable ); // Also send comments meta. + } + + /** + * Gets a filtered list of comment types that sync can hook into. + * + * @access public + * + * @return array Defaults to [ '', 'trackback', 'pingback' ]. + */ + public function get_whitelisted_comment_types() { + /** + * Comment types present in this list will sync their status changes to WordPress.com. + * + * @since 7.6.0 + * + * @param array A list of comment types. + */ + return apply_filters( + 'jetpack_sync_whitelisted_comment_types', + array( '', 'trackback', 'pingback' ) + ); + } + + /** + * Prevents any comment types that are not in the whitelist from being enqueued and sent to WordPress.com. + * + * @param array $args Arguments passed to wp_insert_comment. + * + * @return bool or array $args Arguments passed to wp_insert_comment + */ + public function only_allow_white_listed_comment_types( $args ) { + $comment = $args[1]; + + if ( ! in_array( $comment->comment_type, $this->get_whitelisted_comment_types(), true ) ) { + return false; + } + + return $args; + } + + /** + * Initialize the module in the sender. + * + * @access public + */ + public function init_before_send() { + add_filter( 'jetpack_sync_before_send_wp_insert_comment', array( $this, 'expand_wp_insert_comment' ) ); + + foreach ( $this->get_whitelisted_comment_types() as $comment_type ) { + foreach ( array( 'unapproved', 'approved' ) as $comment_status ) { + $comment_action_name = "comment_{$comment_status}_{$comment_type}"; + add_filter( + 'jetpack_sync_before_send_' . $comment_action_name, + array( + $this, + 'expand_wp_insert_comment', + ) + ); + } + } + + // Full sync. + add_filter( 'jetpack_sync_before_send_jetpack_full_sync_comments', array( $this, 'expand_comment_ids' ) ); + } + + /** + * Enqueue the comments actions for full sync. + * + * @access public + * + * @param array $config Full sync configuration for this sync module. + * @param int $max_items_to_enqueue Maximum number of items to enqueue. + * @param boolean $state True if full sync has finished enqueueing this module, false otherwise. + * @return array Number of actions enqueued, and next module state. + */ + public function enqueue_full_sync_actions( $config, $max_items_to_enqueue, $state ) { + global $wpdb; + return $this->enqueue_all_ids_as_action( 'jetpack_full_sync_comments', $wpdb->comments, 'comment_ID', $this->get_where_sql( $config ), $max_items_to_enqueue, $state ); + } + + /** + * Retrieve an estimated number of actions that will be enqueued. + * + * @access public + * + * @param array $config Full sync configuration for this sync module. + * @return int Number of items yet to be enqueued. + */ + public function estimate_full_sync_actions( $config ) { + global $wpdb; + + $query = "SELECT count(*) FROM $wpdb->comments"; + + $where_sql = $this->get_where_sql( $config ); + if ( $where_sql ) { + $query .= ' WHERE ' . $where_sql; + } + + // TODO: Call $wpdb->prepare on the following query. + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + $count = $wpdb->get_var( $query ); + + return (int) ceil( $count / self::ARRAY_CHUNK_SIZE ); + } + + /** + * Retrieve the WHERE SQL clause based on the module config. + * + * @access public + * + * @param array $config Full sync configuration for this sync module. + * @return string WHERE SQL clause, or `null` if no comments are specified in the module config. + */ + public function get_where_sql( $config ) { + if ( is_array( $config ) ) { + return 'comment_ID IN (' . implode( ',', array_map( 'intval', $config ) ) . ')'; + } + + return null; + } + + /** + * Retrieve the actions that will be sent for this module during a full sync. + * + * @access public + * + * @return array Full sync actions of this module. + */ + public function get_full_sync_actions() { + return array( 'jetpack_full_sync_comments' ); + } + + /** + * Count all the actions that are going to be sent. + * + * @access public + * + * @param array $action_names Names of all the actions that will be sent. + * @return int Number of actions. + */ + public function count_full_sync_actions( $action_names ) { + return $this->count_actions( $action_names, array( 'jetpack_full_sync_comments' ) ); + } + + /** + * Expand the comment status change before the data is serialized and sent to the server. + * + * @access public + * @todo This is not used currently - let's implement it. + * + * @param array $args The hook parameters. + * @return array The expanded hook parameters. + */ + public function expand_wp_comment_status_change( $args ) { + return array( $args[0], $this->filter_comment( $args[1] ) ); + } + + /** + * Expand the comment creation before the data is serialized and sent to the server. + * + * @access public + * + * @param array $args The hook parameters. + * @return array The expanded hook parameters. + */ + public function expand_wp_insert_comment( $args ) { + return array( $args[0], $this->filter_comment( $args[1] ) ); + } + + /** + * Filter a comment object to the fields we need. + * + * @access public + * + * @param \WP_Comment $comment The unfiltered comment object. + * @return \WP_Comment Filtered comment object. + */ + public function filter_comment( $comment ) { + /** + * Filters whether to prevent sending comment data to .com + * + * Passing true to the filter will prevent the comment data from being sent + * to the WordPress.com. + * Instead we pass data that will still enable us to do a checksum against the + * Jetpacks data but will prevent us from displaying the data on in the API as well as + * other services. + * + * @since 4.2.0 + * + * @param boolean false prevent post data from bing synced to WordPress.com + * @param mixed $comment WP_COMMENT object + */ + if ( apply_filters( 'jetpack_sync_prevent_sending_comment_data', false, $comment ) ) { + $blocked_comment = new \stdClass(); + $blocked_comment->comment_ID = $comment->comment_ID; + $blocked_comment->comment_date = $comment->comment_date; + $blocked_comment->comment_date_gmt = $comment->comment_date_gmt; + $blocked_comment->comment_approved = 'jetpack_sync_blocked'; + return $blocked_comment; + } + + return $comment; + } + + /** + * Whether a certain comment meta key is whitelisted for sync. + * + * @access public + * + * @param string $meta_key Comment meta key. + * @return boolean Whether the meta key is whitelisted. + */ + public function is_whitelisted_comment_meta( $meta_key ) { + return in_array( $meta_key, Settings::get_setting( 'comment_meta_whitelist' ), true ); + } + + /** + * Handler for filtering out non-whitelisted comment meta. + * + * @access public + * + * @param array $args Hook args. + * @return array|boolean False if not whitelisted, the original hook args otherwise. + */ + public function filter_meta( $args ) { + return ( $this->is_whitelisted_comment_meta( $args[2] ) ? $args : false ); + } + + /** + * Expand the comment IDs to comment objects and meta before being serialized and sent to the server. + * + * @access public + * + * @param array $args The hook parameters. + * @return array The expanded hook parameters. + */ + public function expand_comment_ids( $args ) { + list( $comment_ids, $previous_interval_end ) = $args; + $comments = get_comments( + array( + 'include_unapproved' => true, + 'comment__in' => $comment_ids, + 'orderby' => 'comment_ID', + 'order' => 'DESC', + ) + ); + + return array( + $comments, + $this->get_metadata( $comment_ids, 'comment', Settings::get_setting( 'comment_meta_whitelist' ) ), + $previous_interval_end, + ); + } +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-constants.php b/plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-constants.php new file mode 100644 index 00000000..d4fecb3b --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-constants.php @@ -0,0 +1,248 @@ +<?php +/** + * Constants sync module. + * + * @package automattic/jetpack-sync + */ + +namespace Automattic\Jetpack\Sync\Modules; + +use Automattic\Jetpack\Sync\Defaults; + +/** + * Class to handle sync for constants. + */ +class Constants extends Module { + /** + * Name of the constants checksum option. + * + * @var string + */ + const CONSTANTS_CHECKSUM_OPTION_NAME = 'jetpack_constants_sync_checksum'; + + /** + * Name of the transient for locking constants. + * + * @var string + */ + const CONSTANTS_AWAIT_TRANSIENT_NAME = 'jetpack_sync_constants_await'; + + /** + * Sync module name. + * + * @access public + * + * @return string + */ + public function name() { + return 'constants'; + } + + /** + * Initialize constants action listeners. + * + * @access public + * + * @param callable $callable Action handler callable. + */ + public function init_listeners( $callable ) { + add_action( 'jetpack_sync_constant', $callable, 10, 2 ); + } + + /** + * Initialize constants action listeners for full sync. + * + * @access public + * + * @param callable $callable Action handler callable. + */ + public function init_full_sync_listeners( $callable ) { + add_action( 'jetpack_full_sync_constants', $callable ); + } + + /** + * Initialize the module in the sender. + * + * @access public + */ + public function init_before_send() { + add_action( 'jetpack_sync_before_send_queue_sync', array( $this, 'maybe_sync_constants' ) ); + + // Full sync. + add_filter( 'jetpack_sync_before_send_jetpack_full_sync_constants', array( $this, 'expand_constants' ) ); + } + + /** + * Perform module cleanup. + * Deletes any transients and options that this module uses. + * Usually triggered when uninstalling the plugin. + * + * @access public + */ + public function reset_data() { + delete_option( self::CONSTANTS_CHECKSUM_OPTION_NAME ); + delete_transient( self::CONSTANTS_AWAIT_TRANSIENT_NAME ); + } + + /** + * Set the constants whitelist. + * + * @access public + * @todo We don't seem to use this one. Should we remove it? + * + * @param array $constants The new constants whitelist. + */ + public function set_constants_whitelist( $constants ) { + $this->constants_whitelist = $constants; + } + + /** + * Get the constants whitelist. + * + * @access public + * + * @return array The constants whitelist. + */ + public function get_constants_whitelist() { + return Defaults::get_constants_whitelist(); + } + + /** + * Enqueue the constants actions for full sync. + * + * @access public + * + * @param array $config Full sync configuration for this sync module. + * @param int $max_items_to_enqueue Maximum number of items to enqueue. + * @param boolean $state True if full sync has finished enqueueing this module, false otherwise. + * @return array Number of actions enqueued, and next module state. + */ + public function enqueue_full_sync_actions( $config, $max_items_to_enqueue, $state ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable + /** + * Tells the client to sync all constants to the server + * + * @since 4.2.0 + * + * @param boolean Whether to expand constants (should always be true) + */ + do_action( 'jetpack_full_sync_constants', true ); + + // The number of actions enqueued, and next module state (true == done). + return array( 1, true ); + } + + /** + * Retrieve an estimated number of actions that will be enqueued. + * + * @access public + * + * @param array $config Full sync configuration for this sync module. + * @return array Number of items yet to be enqueued. + */ + public function estimate_full_sync_actions( $config ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable + return 1; + } + + /** + * Retrieve the actions that will be sent for this module during a full sync. + * + * @access public + * + * @return array Full sync actions of this module. + */ + public function get_full_sync_actions() { + return array( 'jetpack_full_sync_constants' ); + } + + /** + * Sync the constants if we're supposed to. + * + * @access public + */ + public function maybe_sync_constants() { + if ( get_transient( self::CONSTANTS_AWAIT_TRANSIENT_NAME ) ) { + return; + } + + set_transient( self::CONSTANTS_AWAIT_TRANSIENT_NAME, microtime( true ), Defaults::$default_sync_constants_wait_time ); + + $constants = $this->get_all_constants(); + if ( empty( $constants ) ) { + return; + } + + $constants_checksums = (array) get_option( self::CONSTANTS_CHECKSUM_OPTION_NAME, array() ); + + foreach ( $constants as $name => $value ) { + $checksum = $this->get_check_sum( $value ); + // Explicitly not using Identical comparison as get_option returns a string. + if ( ! $this->still_valid_checksum( $constants_checksums, $name, $checksum ) && ! is_null( $value ) ) { + /** + * Tells the client to sync a constant to the server + * + * @since 4.2.0 + * + * @param string The name of the constant + * @param mixed The value of the constant + */ + do_action( 'jetpack_sync_constant', $name, $value ); + $constants_checksums[ $name ] = $checksum; + } else { + $constants_checksums[ $name ] = $checksum; + } + } + update_option( self::CONSTANTS_CHECKSUM_OPTION_NAME, $constants_checksums ); + } + + /** + * Retrieve all constants as per the current constants whitelist. + * Public so that we don't have to store an option for each constant. + * + * @access public + * + * @return array All constants. + */ + public function get_all_constants() { + $constants_whitelist = $this->get_constants_whitelist(); + return array_combine( + $constants_whitelist, + array_map( array( $this, 'get_constant' ), $constants_whitelist ) + ); + } + + /** + * Retrieve the value of a constant. + * Used as a wrapper to standartize access to constants. + * + * @access private + * + * @param string $constant Constant name. + * @return mixed Return value of the constant. + */ + private function get_constant( $constant ) { + return ( defined( $constant ) ) ? + constant( $constant ) + : null; + } + + /** + * Expand the constants within a hook before they are serialized and sent to the server. + * + * @access public + * + * @param array $args The hook parameters. + * @return array $args The hook parameters. + */ + public function expand_constants( $args ) { + if ( $args[0] ) { + $constants = $this->get_all_constants(); + $constants_checksums = array(); + foreach ( $constants as $name => $value ) { + $constants_checksums[ $name ] = $this->get_check_sum( $value ); + } + update_option( self::CONSTANTS_CHECKSUM_OPTION_NAME, $constants_checksums ); + return $constants; + } + return $args; + } +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-full-sync.php b/plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-full-sync.php new file mode 100644 index 00000000..325b35f4 --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-full-sync.php @@ -0,0 +1,673 @@ +<?php +/** + * Full sync module. + * + * @package automattic/jetpack-sync + */ + +namespace Automattic\Jetpack\Sync\Modules; + +use Automattic\Jetpack\Sync\Listener; +use Automattic\Jetpack\Sync\Lock; +use Automattic\Jetpack\Sync\Modules; +use Automattic\Jetpack\Sync\Queue; +use Automattic\Jetpack\Sync\Settings; + +/** + * This class does a full resync of the database by + * enqueuing an outbound action for every single object + * that we care about. + * + * This class, and its related class Jetpack_Sync_Module, contain a few non-obvious optimisations that should be explained: + * - we fire an action called jetpack_full_sync_start so that WPCOM can erase the contents of the cached database + * - for each object type, we page through the object IDs and enqueue them by firing some monitored actions + * - we load the full objects for those IDs in chunks of Jetpack_Sync_Module::ARRAY_CHUNK_SIZE (to reduce the number of MySQL calls) + * - we fire a trigger for the entire array which the Automattic\Jetpack\Sync\Listener then serializes and queues. + */ +class Full_Sync extends Module { + /** + * Prefix of the full sync status option name. + * + * @var string + */ + const STATUS_OPTION_PREFIX = 'jetpack_sync_full_'; + + + /** + * Enqueue Lock name. + * + * @var string + */ + const ENQUEUE_LOCK_NAME = 'full_sync_enqueue'; + + /** + * Sync module name. + * + * @access public + * + * @return string + */ + public function name() { + return 'full-sync'; + } + + /** + * Initialize action listeners for full sync. + * + * @access public + * + * @param callable $callable Action handler callable. + */ + public function init_full_sync_listeners( $callable ) { + // Synthetic actions for full sync. + add_action( 'jetpack_full_sync_start', $callable, 10, 3 ); + add_action( 'jetpack_full_sync_end', $callable, 10, 2 ); + add_action( 'jetpack_full_sync_cancelled', $callable ); + } + + /** + * Initialize the module in the sender. + * + * @access public + */ + public function init_before_send() { + // This is triggered after actions have been processed on the server. + add_action( 'jetpack_sync_processed_actions', array( $this, 'update_sent_progress_action' ) ); + } + + /** + * Start a full sync. + * + * @access public + * + * @param array $module_configs Full sync configuration for all sync modules. + * @return bool Always returns true at success. + */ + public function start( $module_configs = null ) { + $was_already_running = $this->is_started() && ! $this->is_finished(); + + // Remove all evidence of previous full sync items and status. + $this->reset_data(); + + if ( $was_already_running ) { + /** + * Fires when a full sync is cancelled. + * + * @since 4.2.0 + */ + do_action( 'jetpack_full_sync_cancelled' ); + } + + $this->update_status_option( 'started', time() ); + $this->update_status_option( 'params', $module_configs ); + + $enqueue_status = array(); + $full_sync_config = array(); + $include_empty = false; + $empty = array(); + + // Default value is full sync. + if ( ! is_array( $module_configs ) ) { + $module_configs = array(); + $include_empty = true; + foreach ( Modules::get_modules() as $module ) { + $module_configs[ $module->name() ] = true; + } + } + + // Set default configuration, calculate totals, and save configuration if totals > 0. + foreach ( Modules::get_modules() as $module ) { + $module_name = $module->name(); + $module_config = isset( $module_configs[ $module_name ] ) ? $module_configs[ $module_name ] : false; + + if ( ! $module_config ) { + continue; + } + + if ( 'users' === $module_name && 'initial' === $module_config ) { + $module_config = $module->get_initial_sync_user_config(); + } + + $enqueue_status[ $module_name ] = false; + + $total_items = $module->estimate_full_sync_actions( $module_config ); + + // If there's information to process, configure this module. + if ( ! is_null( $total_items ) && $total_items > 0 ) { + $full_sync_config[ $module_name ] = $module_config; + $enqueue_status[ $module_name ] = array( + $total_items, // Total. + 0, // Queued. + false, // Current state. + ); + } elseif ( $include_empty && 0 === $total_items ) { + $empty[ $module_name ] = true; + } + } + + $this->set_config( $full_sync_config ); + $this->set_enqueue_status( $enqueue_status ); + + $range = $this->get_content_range( $full_sync_config ); + /** + * Fires when a full sync begins. This action is serialized + * and sent to the server so that it knows a full sync is coming. + * + * @since 4.2.0 + * @since 7.3.0 Added $range arg. + * @since 7.4.0 Added $empty arg. + * + * @param array $full_sync_config Sync configuration for all sync modules. + * @param array $range Range of the sync items, containing min and max IDs for some item types. + * @param array $empty The modules with no items to sync during a full sync. + */ + do_action( 'jetpack_full_sync_start', $full_sync_config, $range, $empty ); + + $this->continue_enqueuing( $full_sync_config ); + + return true; + } + + /** + * Enqueue the next items to sync. + * + * @access public + * + * @param array $configs Full sync configuration for all sync modules. + */ + public function continue_enqueuing( $configs = null ) { + if ( ! $this->is_started() || ! ( new Lock() )->attempt( self::ENQUEUE_LOCK_NAME ) || $this->get_status_option( 'queue_finished' ) ) { + return; + } + + $this->enqueue( $configs ); + + ( new Lock() )->remove( self::ENQUEUE_LOCK_NAME ); + } + + /** + * Get Modules that are configured to Full Sync and haven't finished enqueuing + * + * @param array $configs Full sync configuration for all sync modules. + * + * @return array + */ + public function get_remaining_modules_to_enqueue( $configs ) { + $enqueue_status = $this->get_enqueue_status(); + return array_filter( + Modules::get_modules(), + /** + * Select configured and not finished modules. + * + * @var $module Module + * @return bool + */ + function ( $module ) use ( $configs, $enqueue_status ) { + // Skip module if not configured for this sync or module is done. + if ( ! isset( $configs[ $module->name() ] ) ) { + return false; + } + if ( ! $configs[ $module->name() ] ) { + return false; + } + if ( isset( $enqueue_status[ $module->name() ][2] ) ) { + if ( true === $enqueue_status[ $module->name() ][2] ) { + return false; + } + } + + return true; + } + ); + } + + /** + * Enqueue the next items to sync. + * + * @access public + * + * @param array $configs Full sync configuration for all sync modules. + */ + public function enqueue( $configs = null ) { + if ( ! $configs ) { + $configs = $this->get_config(); + } + + $enqueue_status = $this->get_enqueue_status(); + $full_sync_queue = new Queue( 'full_sync' ); + $available_queue_slots = Settings::get_setting( 'max_queue_size_full_sync' ) - $full_sync_queue->size(); + + if ( $available_queue_slots <= 0 ) { + return; + } + + $remaining_items_to_enqueue = min( Settings::get_setting( 'max_enqueue_full_sync' ), $available_queue_slots ); + + /** + * If a module exits early (e.g. because it ran out of full sync queue slots, or we ran out of request time) + * then it should exit early + */ + foreach ( $this->get_remaining_modules_to_enqueue( $configs ) as $module ) { + list( $items_enqueued, $next_enqueue_state ) = $module->enqueue_full_sync_actions( $configs[ $module->name() ], $remaining_items_to_enqueue, $enqueue_status[ $module->name() ][2] ); + + $enqueue_status[ $module->name() ][2] = $next_enqueue_state; + + // If items were processed, subtract them from the limit. + if ( ! is_null( $items_enqueued ) && $items_enqueued > 0 ) { + $enqueue_status[ $module->name() ][1] += $items_enqueued; + $remaining_items_to_enqueue -= $items_enqueued; + } + + if ( 0 >= $remaining_items_to_enqueue || true !== $next_enqueue_state ) { + $this->set_enqueue_status( $enqueue_status ); + return; + } + } + + $this->queue_full_sync_end( $configs ); + $this->set_enqueue_status( $enqueue_status ); + } + + /** + * Enqueue 'jetpack_full_sync_end' and update 'queue_finished' status. + * + * @access public + * + * @param array $configs Full sync configuration for all sync modules. + */ + public function queue_full_sync_end( $configs ) { + $range = $this->get_content_range( $configs ); + + /** + * Fires when a full sync ends. This action is serialized + * and sent to the server. + * + * @since 4.2.0 + * @since 7.3.0 Added $range arg. + * + * @param string $checksum Deprecated since 7.3.0 - @see https://github.com/Automattic/jetpack/pull/11945/ + * @param array $range Range of the sync items, containing min and max IDs for some item types. + */ + do_action( 'jetpack_full_sync_end', '', $range ); + + // Setting autoload to true means that it's faster to check whether we should continue enqueuing. + $this->update_status_option( 'queue_finished', time(), true ); + } + + /** + * Get the range (min ID, max ID and total items) of items to sync. + * + * @access public + * + * @param string $type Type of sync item to get the range for. + * @return array Array of min ID, max ID and total items in the range. + */ + public function get_range( $type ) { + global $wpdb; + if ( ! in_array( $type, array( 'comments', 'posts' ), true ) ) { + return array(); + } + + switch ( $type ) { + case 'posts': + $table = $wpdb->posts; + $id = 'ID'; + $where_sql = Settings::get_blacklisted_post_types_sql(); + + break; + case 'comments': + $table = $wpdb->comments; + $id = 'comment_ID'; + $where_sql = Settings::get_comments_filter_sql(); + break; + } + + // TODO: Call $wpdb->prepare on the following query. + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + $results = $wpdb->get_results( "SELECT MAX({$id}) as max, MIN({$id}) as min, COUNT({$id}) as count FROM {$table} WHERE {$where_sql}" ); + if ( isset( $results[0] ) ) { + return $results[0]; + } + + return array(); + } + + /** + * Get the range for content (posts and comments) to sync. + * + * @access private + * + * @param array $config Full sync configuration for this all sync modules. + * @return array Array of range (min ID, max ID, total items) for all content types. + */ + private function get_content_range( $config ) { + $range = array(); + // Only when we are sending the whole range do we want to send also the range. + if ( true === isset( $config['posts'] ) && $config['posts'] ) { + $range['posts'] = $this->get_range( 'posts' ); + } + + if ( true === isset( $config['comments'] ) && $config['comments'] ) { + $range['comments'] = $this->get_range( 'comments' ); + } + return $range; + } + + /** + * Update the progress after sync modules actions have been processed on the server. + * + * @access public + * + * @param array $actions Actions that have been processed on the server. + */ + public function update_sent_progress_action( $actions ) { + // Quick way to map to first items with an array of arrays. + $actions_with_counts = array_count_values( array_filter( array_map( array( $this, 'get_action_name' ), $actions ) ) ); + + // Total item counts for each action. + $actions_with_total_counts = $this->get_actions_totals( $actions ); + + if ( ! $this->is_started() || $this->is_finished() ) { + return; + } + + if ( isset( $actions_with_counts['jetpack_full_sync_start'] ) ) { + $this->update_status_option( 'send_started', time() ); + } + + foreach ( Modules::get_modules() as $module ) { + $module_actions = $module->get_full_sync_actions(); + $status_option_name = "{$module->name()}_sent"; + $total_option_name = "{$status_option_name}_total"; + $items_sent = $this->get_status_option( $status_option_name, 0 ); + $items_sent_total = $this->get_status_option( $total_option_name, 0 ); + + foreach ( $module_actions as $module_action ) { + if ( isset( $actions_with_counts[ $module_action ] ) ) { + $items_sent += $actions_with_counts[ $module_action ]; + } + + if ( ! empty( $actions_with_total_counts[ $module_action ] ) ) { + $items_sent_total += $actions_with_total_counts[ $module_action ]; + } + } + + if ( $items_sent > 0 ) { + $this->update_status_option( $status_option_name, $items_sent ); + } + + if ( 0 !== $items_sent_total ) { + $this->update_status_option( $total_option_name, $items_sent_total ); + } + } + + if ( isset( $actions_with_counts['jetpack_full_sync_end'] ) ) { + $this->update_status_option( 'finished', time() ); + } + } + + /** + * Get the name of the action for an item in the sync queue. + * + * @access public + * + * @param array $queue_item Item of the sync queue. + * @return string|boolean Name of the action, false if queue item is invalid. + */ + public function get_action_name( $queue_item ) { + if ( is_array( $queue_item ) && isset( $queue_item[0] ) ) { + return $queue_item[0]; + } + return false; + } + + /** + * Retrieve the total number of items we're syncing in a particular queue item (action). + * `$queue_item[1]` is expected to contain chunks of items, and `$queue_item[1][0]` + * represents the first (and only) chunk of items to sync in that action. + * + * @access public + * + * @param array $queue_item Item of the sync queue that corresponds to a particular action. + * @return int Total number of items in the action. + */ + public function get_action_totals( $queue_item ) { + if ( is_array( $queue_item ) && isset( $queue_item[1][0] ) ) { + if ( is_array( $queue_item[1][0] ) ) { + // Let's count the items we sync in this action. + return count( $queue_item[1][0] ); + } + // -1 indicates that this action syncs all items by design. + return -1; + } + return 0; + } + + /** + * Retrieve the total number of items for a set of actions, grouped by action name. + * + * @access public + * + * @param array $actions An array of actions. + * @return array An array, representing the total number of items, grouped per action. + */ + public function get_actions_totals( $actions ) { + $totals = array(); + + foreach ( $actions as $action ) { + $name = $this->get_action_name( $action ); + $action_totals = $this->get_action_totals( $action ); + if ( ! isset( $totals[ $name ] ) ) { + $totals[ $name ] = 0; + } + $totals[ $name ] += $action_totals; + } + + return $totals; + } + + /** + * Whether full sync has started. + * + * @access public + * + * @return boolean + */ + public function is_started() { + return ! ! $this->get_status_option( 'started' ); + } + + /** + * Whether full sync has finished. + * + * @access public + * + * @return boolean + */ + public function is_finished() { + return ! ! $this->get_status_option( 'finished' ); + } + + /** + * Retrieve the status of the current full sync. + * + * @access public + * + * @return array Full sync status. + */ + public function get_status() { + $status = array( + 'started' => $this->get_status_option( 'started' ), + 'queue_finished' => $this->get_status_option( 'queue_finished' ), + 'send_started' => $this->get_status_option( 'send_started' ), + 'finished' => $this->get_status_option( 'finished' ), + 'sent' => array(), + 'sent_total' => array(), + 'queue' => array(), + 'config' => $this->get_status_option( 'params' ), + 'total' => array(), + ); + + $enqueue_status = $this->get_enqueue_status(); + + foreach ( Modules::get_modules() as $module ) { + $name = $module->name(); + + if ( ! isset( $enqueue_status[ $name ] ) ) { + continue; + } + + list( $total, $queued ) = $enqueue_status[ $name ]; + + if ( $total ) { + $status['total'][ $name ] = $total; + } + + if ( $queued ) { + $status['queue'][ $name ] = $queued; + } + + $sent = $this->get_status_option( "{$name}_sent" ); + if ( $sent ) { + $status['sent'][ $name ] = $sent; + } + + $sent_total = $this->get_status_option( "{$name}_sent_total" ); + if ( $sent_total ) { + $status['sent_total'][ $name ] = $sent_total; + } + } + + return $status; + } + + /** + * Clear all the full sync status options. + * + * @access public + */ + public function clear_status() { + $prefix = self::STATUS_OPTION_PREFIX; + \Jetpack_Options::delete_raw_option( "{$prefix}_started" ); + \Jetpack_Options::delete_raw_option( "{$prefix}_params" ); + \Jetpack_Options::delete_raw_option( "{$prefix}_queue_finished" ); + \Jetpack_Options::delete_raw_option( "{$prefix}_send_started" ); + \Jetpack_Options::delete_raw_option( "{$prefix}_finished" ); + + $this->delete_enqueue_status(); + + foreach ( Modules::get_modules() as $module ) { + \Jetpack_Options::delete_raw_option( "{$prefix}_{$module->name()}_sent" ); + \Jetpack_Options::delete_raw_option( "{$prefix}_{$module->name()}_sent_total" ); + } + } + + /** + * Clear all the full sync data. + * + * @access public + */ + public function reset_data() { + $this->clear_status(); + $this->delete_config(); + ( new Lock() )->remove( self::ENQUEUE_LOCK_NAME ); + + $listener = Listener::get_instance(); + $listener->get_full_sync_queue()->reset(); + } + + /** + * Get the value of a full sync status option. + * + * @access private + * + * @param string $name Name of the option. + * @param mixed $default Default value of the option. + * @return mixed Option value. + */ + private function get_status_option( $name, $default = null ) { + $value = \Jetpack_Options::get_raw_option( self::STATUS_OPTION_PREFIX . "_$name", $default ); + + return is_numeric( $value ) ? intval( $value ) : $value; + } + + /** + * Update the value of a full sync status option. + * + * @access private + * + * @param string $name Name of the option. + * @param mixed $value Value of the option. + * @param boolean $autoload Whether the option should be autoloaded at the beginning of the request. + */ + private function update_status_option( $name, $value, $autoload = false ) { + \Jetpack_Options::update_raw_option( self::STATUS_OPTION_PREFIX . "_$name", $value, $autoload ); + } + + /** + * Set the full sync enqueue status. + * + * @access private + * + * @param array $new_status The new full sync enqueue status. + */ + private function set_enqueue_status( $new_status ) { + \Jetpack_Options::update_raw_option( 'jetpack_sync_full_enqueue_status', $new_status ); + } + + /** + * Delete full sync enqueue status. + * + * @access private + * + * @return boolean Whether the status was deleted. + */ + private function delete_enqueue_status() { + return \Jetpack_Options::delete_raw_option( 'jetpack_sync_full_enqueue_status' ); + } + + /** + * Retrieve the current full sync enqueue status. + * + * @access private + * + * @return array Full sync enqueue status. + */ + public function get_enqueue_status() { + return \Jetpack_Options::get_raw_option( 'jetpack_sync_full_enqueue_status' ); + } + + /** + * Set the full sync enqueue configuration. + * + * @access private + * + * @param array $config The new full sync enqueue configuration. + */ + private function set_config( $config ) { + \Jetpack_Options::update_raw_option( 'jetpack_sync_full_config', $config ); + } + + /** + * Delete full sync configuration. + * + * @access private + * + * @return boolean Whether the configuration was deleted. + */ + private function delete_config() { + return \Jetpack_Options::delete_raw_option( 'jetpack_sync_full_config' ); + } + + /** + * Retrieve the current full sync enqueue config. + * + * @access private + * + * @return array Full sync enqueue config. + */ + private function get_config() { + return \Jetpack_Options::get_raw_option( 'jetpack_sync_full_config' ); + } + +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-import.php b/plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-import.php new file mode 100644 index 00000000..99afd74b --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-import.php @@ -0,0 +1,218 @@ +<?php +/** + * Import sync module. + * + * @package automattic/jetpack-sync + */ + +namespace Automattic\Jetpack\Sync\Modules; + +use Automattic\Jetpack\Sync\Settings; + +/** + * Class to handle sync for imports. + */ +class Import extends Module { + + /** + * Tracks which actions have already been synced for the import + * to prevent the same event from being triggered a second time. + * + * @var array + */ + private $synced_actions = array(); + + /** + * A mapping of action types to sync action name. + * Keys are the name of the import action. + * Values are the resulting sync action. + * + * Note: import_done and import_end both intentionally map to + * jetpack_sync_import_end, as they both track the same type of action, + * the successful completion of an import. Different import plugins use + * differently named actions, and this is an attempt to consolidate. + * + * @var array + */ + private static $import_sync_action_map = array( + 'import_start' => 'jetpack_sync_import_start', + 'import_done' => 'jetpack_sync_import_end', + 'import_end' => 'jetpack_sync_import_end', + ); + + /** + * Sync module name. + * + * @access public + * + * @return string + */ + public function name() { + return 'import'; + } + + /** + * Initialize imports action listeners. + * + * @access public + * + * @param callable $callable Action handler callable. + */ + public function init_listeners( $callable ) { + add_action( 'export_wp', $callable ); + add_action( 'jetpack_sync_import_start', $callable, 10, 2 ); + add_action( 'jetpack_sync_import_end', $callable, 10, 2 ); + + // WordPress. + add_action( 'import_start', array( $this, 'sync_import_action' ) ); + + // Movable type, RSS, Livejournal. + add_action( 'import_done', array( $this, 'sync_import_action' ) ); + + // WordPress, Blogger, Livejournal, woo tax rate. + add_action( 'import_end', array( $this, 'sync_import_action' ) ); + } + + /** + * Set module defaults. + * Define an empty list of synced actions for us to fill later. + * + * @access public + */ + public function set_defaults() { + $this->synced_actions = array(); + } + + /** + * Generic handler for import actions. + * + * @access public + * + * @param string $importer Either a string reported by the importer, the class name of the importer, or 'unknown'. + */ + public function sync_import_action( $importer ) { + $import_action = current_filter(); + // Map action to event name. + $sync_action = self::$import_sync_action_map[ $import_action ]; + + // Only sync each action once per import. + if ( array_key_exists( $sync_action, $this->synced_actions ) && $this->synced_actions[ $sync_action ] ) { + return; + } + + // Mark this action as synced. + $this->synced_actions[ $sync_action ] = true; + + // Prefer self-reported $importer value. + if ( ! $importer ) { + // Fall back to inferring by calling class name. + $importer = self::get_calling_importer_class(); + } + + // Get $importer from known_importers. + $known_importers = Settings::get_setting( 'known_importers' ); + if ( isset( $known_importers[ $importer ] ) ) { + $importer = $known_importers[ $importer ]; + } + + $importer_name = $this->get_importer_name( $importer ); + + switch ( $sync_action ) { + case 'jetpack_sync_import_start': + /** + * Used for syncing the start of an import + * + * @since 7.3.0 + * + * @module sync + * + * @param string $importer Either a string reported by the importer, the class name of the importer, or 'unknown'. + * @param string $importer_name The name reported by the importer, or 'Unknown Importer'. + */ + do_action( 'jetpack_sync_import_start', $importer, $importer_name ); + break; + + case 'jetpack_sync_import_end': + /** + * Used for syncing the end of an import + * + * @since 7.3.0 + * + * @module sync + * + * @param string $importer Either a string reported by the importer, the class name of the importer, or 'unknown'. + * @param string $importer_name The name reported by the importer, or 'Unknown Importer'. + */ + do_action( 'jetpack_sync_import_end', $importer, $importer_name ); + break; + } + } + + /** + * Retrieve the name of the importer. + * + * @access private + * + * @param string $importer Either a string reported by the importer, the class name of the importer, or 'unknown'. + * @return string Name of the importer, or "Unknown Importer" if importer is unknown. + */ + private function get_importer_name( $importer ) { + $importers = get_importers(); + return isset( $importers[ $importer ] ) ? $importers[ $importer ][0] : 'Unknown Importer'; + } + + /** + * Determine the class that extends `WP_Importer` which is responsible for + * the current action. Designed to be used within an action handler. + * + * @access private + * @static + * + * @return string The name of the calling class, or 'unknown'. + */ + private static function get_calling_importer_class() { + // If WP_Importer doesn't exist, neither will any importer that extends it. + if ( ! class_exists( 'WP_Importer', false ) ) { + return 'unknown'; + } + + $action = current_filter(); + $backtrace = debug_backtrace( false ); //phpcs:ignore PHPCompatibility.FunctionUse.NewFunctionParameters.debug_backtrace_optionsFound,WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace + + $do_action_pos = -1; + $backtrace_len = count( $backtrace ); + for ( $i = 0; $i < $backtrace_len; $i++ ) { + // Find the location in the stack of the calling action. + if ( 'do_action' === $backtrace[ $i ]['function'] && $action === $backtrace[ $i ]['args'][0] ) { + $do_action_pos = $i; + break; + } + } + + // If the action wasn't called, the calling class is unknown. + if ( -1 === $do_action_pos ) { + return 'unknown'; + } + + // Continue iterating the stack looking for a caller that extends WP_Importer. + for ( $i = $do_action_pos + 1; $i < $backtrace_len; $i++ ) { + // If there is no class on the trace, continue. + if ( ! isset( $backtrace[ $i ]['class'] ) ) { + continue; + } + + $class_name = $backtrace[ $i ]['class']; + + // Check if the class extends WP_Importer. + if ( class_exists( $class_name, false ) ) { + $parents = class_parents( $class_name, false ); + if ( $parents && in_array( 'WP_Importer', $parents, true ) ) { + return $class_name; + } + } + } + + // If we've exhausted the stack without a match, the calling class is unknown. + return 'unknown'; + } +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-menus.php b/plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-menus.php new file mode 100644 index 00000000..69faa9b5 --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-menus.php @@ -0,0 +1,143 @@ +<?php +/** + * Menus sync module. + * + * @package automattic/jetpack-sync + */ + +namespace Automattic\Jetpack\Sync\Modules; + +/** + * Class to handle sync for menus. + */ +class Menus extends Module { + /** + * Navigation menu items that were added but not synced yet. + * + * @access private + * + * @var array + */ + private $nav_items_just_added = array(); + + /** + * Sync module name. + * + * @access public + * + * @return string + */ + public function name() { + return 'menus'; + } + + /** + * Initialize menus action listeners. + * + * @access public + * + * @param callable $callable Action handler callable. + */ + public function init_listeners( $callable ) { + add_action( 'wp_create_nav_menu', $callable, 10, 2 ); + add_action( 'wp_update_nav_menu', array( $this, 'update_nav_menu' ), 10, 2 ); + add_action( 'wp_add_nav_menu_item', array( $this, 'update_nav_menu_add_item' ), 10, 3 ); + add_action( 'wp_update_nav_menu_item', array( $this, 'update_nav_menu_update_item' ), 10, 3 ); + add_action( 'post_updated', array( $this, 'remove_just_added_menu_item' ), 10, 2 ); + + add_action( 'jetpack_sync_updated_nav_menu', $callable, 10, 2 ); + add_action( 'jetpack_sync_updated_nav_menu_add_item', $callable, 10, 4 ); + add_action( 'jetpack_sync_updated_nav_menu_update_item', $callable, 10, 4 ); + add_action( 'delete_nav_menu', $callable, 10, 3 ); + } + + /** + * Nav menu update handler. + * + * @access public + * + * @param int $menu_id ID of the menu. + * @param array $menu_data An array of menu data. + */ + public function update_nav_menu( $menu_id, $menu_data = array() ) { + if ( empty( $menu_data ) ) { + return; + } + /** + * Helps sync log that a nav menu was updated. + * + * @since 5.0.0 + * + * @param int $menu_id ID of the menu. + * @param array $menu_data An array of menu data. + */ + do_action( 'jetpack_sync_updated_nav_menu', $menu_id, $menu_data ); + } + + /** + * Nav menu item addition handler. + * + * @access public + * + * @param int $menu_id ID of the menu. + * @param int $nav_item_id ID of the new menu item. + * @param array $nav_item_args Arguments used to add the menu item. + */ + public function update_nav_menu_add_item( $menu_id, $nav_item_id, $nav_item_args ) { + $menu_data = wp_get_nav_menu_object( $menu_id ); + $this->nav_items_just_added[] = $nav_item_id; + /** + * Helps sync log that a new menu item was added. + * + * @since 5.0.0 + * + * @param int $menu_id ID of the menu. + * @param array $menu_data An array of menu data. + * @param int $nav_item_id ID of the new menu item. + * @param array $nav_item_args Arguments used to add the menu item. + */ + do_action( 'jetpack_sync_updated_nav_menu_add_item', $menu_id, $menu_data, $nav_item_id, $nav_item_args ); + } + + /** + * Nav menu item update handler. + * + * @access public + * + * @param int $menu_id ID of the menu. + * @param int $nav_item_id ID of the new menu item. + * @param array $nav_item_args Arguments used to update the menu item. + */ + public function update_nav_menu_update_item( $menu_id, $nav_item_id, $nav_item_args ) { + if ( in_array( $nav_item_id, $this->nav_items_just_added, true ) ) { + return; + } + $menu_data = wp_get_nav_menu_object( $menu_id ); + /** + * Helps sync log that an update to the menu item happened. + * + * @since 5.0.0 + * + * @param int $menu_id ID of the menu. + * @param array $menu_data An array of menu data. + * @param int $nav_item_id ID of the new menu item. + * @param array $nav_item_args Arguments used to update the menu item. + */ + do_action( 'jetpack_sync_updated_nav_menu_update_item', $menu_id, $menu_data, $nav_item_id, $nav_item_args ); + } + + /** + * Remove menu items that have already been saved from the "just added" list. + * + * @access public + * + * @param int $nav_item_id ID of the new menu item. + * @param \WP_Post $post_after Nav menu item post object after the update. + */ + public function remove_just_added_menu_item( $nav_item_id, $post_after ) { + if ( 'nav_menu_item' !== $post_after->post_type ) { + return; + } + $this->nav_items_just_added = array_diff( $this->nav_items_just_added, array( $nav_item_id ) ); + } +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-meta.php b/plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-meta.php new file mode 100644 index 00000000..1d30c72e --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-meta.php @@ -0,0 +1,81 @@ +<?php +/** + * Meta sync module. + * + * @package automattic/jetpack-sync + */ + +namespace Automattic\Jetpack\Sync\Modules; + +/** + * Class to handle sync for meta. + */ +class Meta extends Module { + /** + * Sync module name. + * + * @access public + * + * @return string + */ + public function name() { + return 'meta'; + } + + /** + * This implementation of get_objects_by_id() is a bit hacky since we're not passing in an array of meta IDs, + * but instead an array of post or comment IDs for which to retrieve meta for. On top of that, + * we also pass in an associative array where we expect there to be 'meta_key' and 'ids' keys present. + * + * This seemed to be required since if we have missing meta on WP.com and need to fetch it, we don't know what + * the meta key is, but we do know that we have missing meta for a given post or comment. + * + * @todo Refactor the $wpdb->prepare call to use placeholders. + * + * @param string $object_type The type of object for which we retrieve meta. Either 'post' or 'comment'. + * @param array $config Must include 'meta_key' and 'ids' keys. + * + * @return array + */ + public function get_objects_by_id( $object_type, $config ) { + global $wpdb; + + $table = _get_meta_table( $object_type ); + + if ( ! $table ) { + return array(); + } + + if ( ! isset( $config['meta_key'] ) || ! isset( $config['ids'] ) || ! is_array( $config['ids'] ) ) { + return array(); + } + + $meta_key = $config['meta_key']; + $ids = $config['ids']; + $object_id_column = $object_type . '_id'; + + // Sanitize so that the array only has integer values. + $ids_string = implode( ', ', array_map( 'intval', $ids ) ); + $metas = $wpdb->get_results( + $wpdb->prepare( + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + "SELECT * FROM {$table} WHERE {$object_id_column} IN ( {$ids_string} ) AND meta_key = %s", + $meta_key + ) + ); + + $meta_objects = array(); + foreach ( (array) $metas as $meta_object ) { + $meta_object = (array) $meta_object; + $meta_objects[ $meta_object[ $object_id_column ] ] = array( + 'meta_type' => $object_type, + 'meta_id' => $meta_object['meta_id'], + 'meta_key' => $meta_key, + 'meta_value' => $meta_object['meta_value'], + 'object_id' => $meta_object[ $object_id_column ], + ); + } + + return $meta_objects; + } +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-module.php b/plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-module.php new file mode 100644 index 00000000..b8b57d87 --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-module.php @@ -0,0 +1,463 @@ +<?php +/** + * A base abstraction of a sync module. + * + * @package automattic/jetpack-sync + */ + +namespace Automattic\Jetpack\Sync\Modules; + +use Automattic\Jetpack\Sync\Listener; +use Automattic\Jetpack\Sync\Replicastore; + +/** + * Basic methods implemented by Jetpack Sync extensions. + * + * @abstract + */ +abstract class Module { + /** + * Number of items per chunk when grouping objects for performance reasons. + * + * @access public + * + * @var int + */ + const ARRAY_CHUNK_SIZE = 10; + + /** + * Sync module name. + * + * @access public + * + * @return string + */ + abstract public function name(); + + /** + * The id field in the database. + * + * @access public + * + * @return string + */ + public function id_field() { + return 'ID'; + } + + /** + * The table in the database. + * + * @access public + * + * @return string|bool + */ + public function table_name() { + return false; + } + + // phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable + + /** + * Retrieve a sync object by its ID. + * + * @access public + * + * @param string $object_type Type of the sync object. + * @param int $id ID of the sync object. + * @return mixed Object, or false if the object is invalid. + */ + public function get_object_by_id( $object_type, $id ) { + return false; + } + + /** + * Initialize callables action listeners. + * Override these to set up listeners and set/reset data/defaults. + * + * @access public + * + * @param callable $callable Action handler callable. + */ + public function init_listeners( $callable ) { + } + + /** + * Initialize module action listeners for full sync. + * + * @access public + * + * @param callable $callable Action handler callable. + */ + public function init_full_sync_listeners( $callable ) { + } + + /** + * Initialize the module in the sender. + * + * @access public + */ + public function init_before_send() { + } + + /** + * Set module defaults. + * + * @access public + */ + public function set_defaults() { + } + + /** + * Perform module cleanup. + * Usually triggered when uninstalling the plugin. + * + * @access public + */ + public function reset_data() { + } + + /** + * Enqueue the module actions for full sync. + * + * @access public + * + * @param array $config Full sync configuration for this sync module. + * @param int $max_items_to_enqueue Maximum number of items to enqueue. + * @param boolean $state True if full sync has finished enqueueing this module, false otherwise. + * @return array Number of actions enqueued, and next module state. + */ + public function enqueue_full_sync_actions( $config, $max_items_to_enqueue, $state ) { + // In subclasses, return the number of actions enqueued, and next module state (true == done). + return array( null, true ); + } + + /** + * Retrieve an estimated number of actions that will be enqueued. + * + * @access public + * + * @param array $config Full sync configuration for this sync module. + * @return array Number of items yet to be enqueued. + */ + public function estimate_full_sync_actions( $config ) { + // In subclasses, return the number of items yet to be enqueued. + return null; + } + + // phpcs:enable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable + + /** + * Retrieve the actions that will be sent for this module during a full sync. + * + * @access public + * + * @return array Full sync actions of this module. + */ + public function get_full_sync_actions() { + return array(); + } + + /** + * Get the number of actions that we care about. + * + * @access protected + * + * @param array $action_names Action names we're interested in. + * @param array $actions_to_count Unfiltered list of actions we want to count. + * @return array Number of actions that we're interested in. + */ + protected function count_actions( $action_names, $actions_to_count ) { + return count( array_intersect( $action_names, $actions_to_count ) ); + } + + /** + * Calculate the checksum of one or more values. + * + * @access protected + * + * @param mixed $values Values to calculate checksum for. + * @return int The checksum. + */ + protected function get_check_sum( $values ) { + return crc32( wp_json_encode( jetpack_json_wrap( $values ) ) ); + } + + /** + * Whether a particular checksum in a set of checksums is valid. + * + * @access protected + * + * @param array $sums_to_check Array of checksums. + * @param string $name Name of the checksum. + * @param int $new_sum Checksum to compare against. + * @return boolean Whether the checksum is valid. + */ + protected function still_valid_checksum( $sums_to_check, $name, $new_sum ) { + if ( isset( $sums_to_check[ $name ] ) && $sums_to_check[ $name ] === $new_sum ) { + return true; + } + + return false; + } + + /** + * Enqueue all items of a sync type as an action. + * + * @access protected + * + * @param string $action_name Name of the action. + * @param string $table_name Name of the database table. + * @param string $id_field Name of the ID field in the database. + * @param string $where_sql The SQL WHERE clause to filter to the desired items. + * @param int $max_items_to_enqueue Maximum number of items to enqueue in the same time. + * @param boolean $state Whether enqueueing has finished. + * @return array Array, containing the number of chunks and TRUE, indicating enqueueing has finished. + */ + protected function enqueue_all_ids_as_action( $action_name, $table_name, $id_field, $where_sql, $max_items_to_enqueue, $state ) { + global $wpdb; + + if ( ! $where_sql ) { + $where_sql = '1 = 1'; + } + + $items_per_page = 1000; + $page = 1; + $chunk_count = 0; + $previous_interval_end = $state ? $state : '~0'; + $listener = Listener::get_instance(); + + // Count down from max_id to min_id so we get newest posts/comments/etc first. + // phpcs:ignore WordPress.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition, WordPress.DB.PreparedSQL.InterpolatedNotPrepared + while ( $ids = $wpdb->get_col( "SELECT {$id_field} FROM {$table_name} WHERE {$where_sql} AND {$id_field} < {$previous_interval_end} ORDER BY {$id_field} DESC LIMIT {$items_per_page}" ) ) { + // Request posts in groups of N for efficiency. + $chunked_ids = array_chunk( $ids, self::ARRAY_CHUNK_SIZE ); + + // If we hit our row limit, process and return. + if ( $chunk_count + count( $chunked_ids ) >= $max_items_to_enqueue ) { + $remaining_items_count = $max_items_to_enqueue - $chunk_count; + $remaining_items = array_slice( $chunked_ids, 0, $remaining_items_count ); + $remaining_items_with_previous_interval_end = $this->get_chunks_with_preceding_end( $remaining_items, $previous_interval_end ); + $listener->bulk_enqueue_full_sync_actions( $action_name, $remaining_items_with_previous_interval_end ); + + $last_chunk = end( $remaining_items ); + return array( $remaining_items_count + $chunk_count, end( $last_chunk ) ); + } + $chunked_ids_with_previous_end = $this->get_chunks_with_preceding_end( $chunked_ids, $previous_interval_end ); + + $listener->bulk_enqueue_full_sync_actions( $action_name, $chunked_ids_with_previous_end ); + + $chunk_count += count( $chunked_ids ); + $page++; + // The $ids are ordered in descending order. + $previous_interval_end = end( $ids ); + } + + if ( $wpdb->last_error ) { + // return the values that were passed in so all these chunks get retried. + return array( $max_items_to_enqueue, $state ); + } + + return array( $chunk_count, true ); + } + + /** + * Retrieve chunk IDs with previous interval end. + * + * @access protected + * + * @param array $chunks All remaining items. + * @param int $previous_interval_end The last item from the previous interval. + * @return array Chunk IDs with the previous interval end. + */ + protected function get_chunks_with_preceding_end( $chunks, $previous_interval_end ) { + $chunks_with_ends = array(); + foreach ( $chunks as $chunk ) { + $chunks_with_ends[] = array( + 'ids' => $chunk, + 'previous_end' => $previous_interval_end, + ); + // Chunks are ordered in descending order. + $previous_interval_end = end( $chunk ); + } + return $chunks_with_ends; + } + + /** + * Get metadata of a particular object type within the designated meta key whitelist. + * + * @access protected + * + * @todo Refactor to use $wpdb->prepare() on the SQL query. + * + * @param array $ids Object IDs. + * @param string $meta_type Meta type. + * @param array $meta_key_whitelist Meta key whitelist. + * @return array Unserialized meta values. + */ + protected function get_metadata( $ids, $meta_type, $meta_key_whitelist ) { + global $wpdb; + $table = _get_meta_table( $meta_type ); + $id = $meta_type . '_id'; + if ( ! $table ) { + return array(); + } + + $private_meta_whitelist_sql = "'" . implode( "','", array_map( 'esc_sql', $meta_key_whitelist ) ) . "'"; + + return array_map( + array( $this, 'unserialize_meta' ), + $wpdb->get_results( + // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared + "SELECT $id, meta_key, meta_value, meta_id FROM $table WHERE $id IN ( " . implode( ',', wp_parse_id_list( $ids ) ) . ' )' . + " AND meta_key IN ( $private_meta_whitelist_sql ) ", + // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared + OBJECT + ) + ); + } + + /** + * Initialize listeners for the particular meta type. + * + * @access public + * + * @param string $meta_type Meta type. + * @param callable $callable Action handler callable. + */ + public function init_listeners_for_meta_type( $meta_type, $callable ) { + add_action( "added_{$meta_type}_meta", $callable, 10, 4 ); + add_action( "updated_{$meta_type}_meta", $callable, 10, 4 ); + add_action( "deleted_{$meta_type}_meta", $callable, 10, 4 ); + } + + /** + * Initialize meta whitelist handler for the particular meta type. + * + * @access public + * + * @param string $meta_type Meta type. + * @param callable $whitelist_handler Action handler callable. + */ + public function init_meta_whitelist_handler( $meta_type, $whitelist_handler ) { + add_filter( "jetpack_sync_before_enqueue_added_{$meta_type}_meta", $whitelist_handler ); + add_filter( "jetpack_sync_before_enqueue_updated_{$meta_type}_meta", $whitelist_handler ); + add_filter( "jetpack_sync_before_enqueue_deleted_{$meta_type}_meta", $whitelist_handler ); + } + + /** + * Retrieve the term relationships for the specified object IDs. + * + * @access protected + * + * @todo This feels too specific to be in the abstract sync Module class. Move it? + * + * @param array $ids Object IDs. + * @return array Term relationships - object ID and term taxonomy ID pairs. + */ + protected function get_term_relationships( $ids ) { + global $wpdb; + + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + return $wpdb->get_results( "SELECT object_id, term_taxonomy_id FROM $wpdb->term_relationships WHERE object_id IN ( " . implode( ',', wp_parse_id_list( $ids ) ) . ' )', OBJECT ); + } + + /** + * Unserialize the value of a meta object, if necessary. + * + * @access public + * + * @param object $meta Meta object. + * @return object Meta object with possibly unserialized value. + */ + public function unserialize_meta( $meta ) { + $meta->meta_value = maybe_unserialize( $meta->meta_value ); + return $meta; + } + + /** + * Retrieve a set of objects by their IDs. + * + * @access public + * + * @param string $object_type Object type. + * @param array $ids Object IDs. + * @return array Array of objects. + */ + public function get_objects_by_id( $object_type, $ids ) { + if ( empty( $ids ) || empty( $object_type ) ) { + return array(); + } + + $objects = array(); + foreach ( (array) $ids as $id ) { + $object = $this->get_object_by_id( $object_type, $id ); + + // Only add object if we have the object. + if ( $object ) { + $objects[ $id ] = $object; + } + } + + return $objects; + } + + /** + * Gets a list of minimum and maximum object ids for each batch based on the given batch size. + * + * @access public + * + * @param int $batch_size The batch size for objects. + * @param string|bool $where_sql The sql where clause minus 'WHERE', or false if no where clause is needed. + * + * @return array|bool An array of min and max ids for each batch. FALSE if no table can be found. + */ + public function get_min_max_object_ids_for_batches( $batch_size, $where_sql = false ) { + global $wpdb; + + if ( ! $this->table_name() ) { + return false; + } + + $results = array(); + $table = $wpdb->{$this->table_name()}; + $current_max = 0; + $current_min = 1; + $id_field = $this->id_field(); + $replicastore = new Replicastore(); + + $total = $replicastore->get_min_max_object_id( + $id_field, + $table, + $where_sql, + false + ); + + while ( $total->max > $current_max ) { + $where = $where_sql ? + $where_sql . " AND $id_field > $current_max" : + "$id_field > $current_max"; + $result = $replicastore->get_min_max_object_id( + $id_field, + $table, + $where, + $batch_size + ); + if ( empty( $result->min ) && empty( $result->max ) ) { + // Our query produced no min and max. We can assume the min from the previous query, + // and the total max we found in the initial query. + $current_max = (int) $total->max; + $result = (object) array( + 'min' => $current_min, + 'max' => $current_max, + ); + } else { + $current_min = (int) $result->min; + $current_max = (int) $result->max; + } + $results[] = $result; + } + + return $results; + } +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-network-options.php b/plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-network-options.php new file mode 100644 index 00000000..c30ae8c7 --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-network-options.php @@ -0,0 +1,236 @@ +<?php +/** + * Network Options sync module. + * + * @package automattic/jetpack-sync + */ + +namespace Automattic\Jetpack\Sync\Modules; + +use Automattic\Jetpack\Sync\Defaults; + +/** + * Class to handle sync for network options. + */ +class Network_Options extends Module { + /** + * Whitelist for network options we want to sync. + * + * @access private + * + * @var array + */ + private $network_options_whitelist; + + /** + * Sync module name. + * + * @access public + * + * @return string + */ + public function name() { + return 'network_options'; + } + + /** + * Initialize network options action listeners when on multisite. + * + * @access public + * + * @param callable $callable Action handler callable. + */ + public function init_listeners( $callable ) { + if ( ! is_multisite() ) { + return; + } + + // Multi site network options. + add_action( 'add_site_option', $callable, 10, 2 ); + add_action( 'update_site_option', $callable, 10, 3 ); + add_action( 'delete_site_option', $callable, 10, 1 ); + + $whitelist_network_option_handler = array( $this, 'whitelist_network_options' ); + add_filter( 'jetpack_sync_before_enqueue_delete_site_option', $whitelist_network_option_handler ); + add_filter( 'jetpack_sync_before_enqueue_add_site_option', $whitelist_network_option_handler ); + add_filter( 'jetpack_sync_before_enqueue_update_site_option', $whitelist_network_option_handler ); + } + + /** + * Initialize network options action listeners for full sync. + * + * @access public + * + * @param callable $callable Action handler callable. + */ + public function init_full_sync_listeners( $callable ) { + add_action( 'jetpack_full_sync_network_options', $callable ); + } + + /** + * Initialize the module in the sender. + * + * @access public + */ + public function init_before_send() { + if ( ! is_multisite() ) { + return; + } + + // Full sync. + add_filter( + 'jetpack_sync_before_send_jetpack_full_sync_network_options', + array( + $this, + 'expand_network_options', + ) + ); + } + + /** + * Set module defaults. + * Define the network options whitelist based on the default one. + * + * @access public + */ + public function set_defaults() { + $this->network_options_whitelist = Defaults::$default_network_options_whitelist; + } + + /** + * Enqueue the network options actions for full sync. + * + * @access public + * + * @param array $config Full sync configuration for this sync module. + * @param int $max_items_to_enqueue Maximum number of items to enqueue. + * @param boolean $state True if full sync has finished enqueueing this module, false otherwise. + * @return array Number of actions enqueued, and next module state. + */ + public function enqueue_full_sync_actions( $config, $max_items_to_enqueue, $state ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable + if ( ! is_multisite() ) { + return array( null, true ); + } + + /** + * Tells the client to sync all options to the server + * + * @since 4.2.0 + * + * @param boolean Whether to expand options (should always be true) + */ + do_action( 'jetpack_full_sync_network_options', true ); + + // The number of actions enqueued, and next module state (true == done). + return array( 1, true ); + } + + /** + * Retrieve an estimated number of actions that will be enqueued. + * + * @access public + * + * @param array $config Full sync configuration for this sync module. + * @return array Number of items yet to be enqueued. + */ + public function estimate_full_sync_actions( $config ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable + if ( ! is_multisite() ) { + return null; + } + + return 1; + } + + /** + * Retrieve the actions that will be sent for this module during a full sync. + * + * @access public + * + * @return array Full sync actions of this module. + */ + public function get_full_sync_actions() { + return array( 'jetpack_full_sync_network_options' ); + } + + /** + * Retrieve all network options as per the current network options whitelist. + * + * @access public + * + * @return array All network options. + */ + public function get_all_network_options() { + $options = array(); + foreach ( $this->network_options_whitelist as $option ) { + $options[ $option ] = get_site_option( $option ); + } + + return $options; + } + + /** + * Set the network options whitelist. + * + * @access public + * + * @param array $options The new network options whitelist. + */ + public function set_network_options_whitelist( $options ) { + $this->network_options_whitelist = $options; + } + + /** + * Get the network options whitelist. + * + * @access public + * + * @return array The network options whitelist. + */ + public function get_network_options_whitelist() { + return $this->network_options_whitelist; + } + + /** + * Reject non-whitelisted network options. + * + * @access public + * + * @param array $args The hook parameters. + * @return array|false $args The hook parameters, false if not a whitelisted network option. + */ + public function whitelist_network_options( $args ) { + if ( ! $this->is_whitelisted_network_option( $args[0] ) ) { + return false; + } + + return $args; + } + + /** + * Whether the option is a whitelisted network option in a multisite system. + * + * @access public + * + * @param string $option Option name. + * @return boolean True if this is a whitelisted network option. + */ + public function is_whitelisted_network_option( $option ) { + return is_multisite() && in_array( $option, $this->network_options_whitelist, true ); + } + + /** + * Expand the network options within a hook before they are serialized and sent to the server. + * + * @access public + * + * @param array $args The hook parameters. + * @return array $args The hook parameters. + */ + public function expand_network_options( $args ) { + if ( $args[0] ) { + return $this->get_all_network_options(); + } + + return $args; + } +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-options.php b/plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-options.php new file mode 100644 index 00000000..2c323a2b --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-options.php @@ -0,0 +1,344 @@ +<?php +/** + * Options sync module. + * + * @package automattic/jetpack-sync + */ + +namespace Automattic\Jetpack\Sync\Modules; + +use Automattic\Jetpack\Sync\Defaults; + +/** + * Class to handle sync for options. + */ +class Options extends Module { + /** + * Whitelist for options we want to sync. + * + * @access private + * + * @var array + */ + private $options_whitelist; + + /** + * Contentless options we want to sync. + * + * @access private + * + * @var array + */ + private $options_contentless; + + /** + * Sync module name. + * + * @access public + * + * @return string + */ + public function name() { + return 'options'; + } + + /** + * Initialize options action listeners. + * + * @access public + * + * @param callable $callable Action handler callable. + */ + public function init_listeners( $callable ) { + // Options. + add_action( 'added_option', $callable, 10, 2 ); + add_action( 'updated_option', $callable, 10, 3 ); + add_action( 'deleted_option', $callable, 10, 1 ); + + // Sync Core Icon: Detect changes in Core's Site Icon and make it syncable. + add_action( 'add_option_site_icon', array( $this, 'jetpack_sync_core_icon' ) ); + add_action( 'update_option_site_icon', array( $this, 'jetpack_sync_core_icon' ) ); + add_action( 'delete_option_site_icon', array( $this, 'jetpack_sync_core_icon' ) ); + + $whitelist_option_handler = array( $this, 'whitelist_options' ); + add_filter( 'jetpack_sync_before_enqueue_deleted_option', $whitelist_option_handler ); + add_filter( 'jetpack_sync_before_enqueue_added_option', $whitelist_option_handler ); + add_filter( 'jetpack_sync_before_enqueue_updated_option', $whitelist_option_handler ); + } + + /** + * Initialize options action listeners for full sync. + * + * @access public + * + * @param callable $callable Action handler callable. + */ + public function init_full_sync_listeners( $callable ) { + add_action( 'jetpack_full_sync_options', $callable ); + } + + /** + * Initialize the module in the sender. + * + * @access public + */ + public function init_before_send() { + // Full sync. + add_filter( 'jetpack_sync_before_send_jetpack_full_sync_options', array( $this, 'expand_options' ) ); + } + + /** + * Set module defaults. + * Define the options whitelist and contentless options. + * + * @access public + */ + public function set_defaults() { + $this->update_options_whitelist(); + $this->update_options_contentless(); + } + + /** + * Set module defaults at a later time. + * + * @access public + */ + public function set_late_default() { + /** This filter is already documented in json-endpoints/jetpack/class.wpcom-json-api-get-option-endpoint.php */ + $late_options = apply_filters( 'jetpack_options_whitelist', array() ); + if ( ! empty( $late_options ) && is_array( $late_options ) ) { + $this->options_whitelist = array_merge( $this->options_whitelist, $late_options ); + } + } + + /** + * Enqueue the options actions for full sync. + * + * @access public + * + * @param array $config Full sync configuration for this sync module. + * @param int $max_items_to_enqueue Maximum number of items to enqueue. + * @param boolean $state True if full sync has finished enqueueing this module, false otherwise. + * @return array Number of actions enqueued, and next module state. + */ + public function enqueue_full_sync_actions( $config, $max_items_to_enqueue, $state ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable + /** + * Tells the client to sync all options to the server + * + * @since 4.2.0 + * + * @param boolean Whether to expand options (should always be true) + */ + do_action( 'jetpack_full_sync_options', true ); + + // The number of actions enqueued, and next module state (true == done). + return array( 1, true ); + } + + /** + * Retrieve an estimated number of actions that will be enqueued. + * + * @access public + * + * @param array $config Full sync configuration for this sync module. + * @return int Number of items yet to be enqueued. + */ + public function estimate_full_sync_actions( $config ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable + return 1; + } + + /** + * Retrieve the actions that will be sent for this module during a full sync. + * + * @access public + * + * @return array Full sync actions of this module. + */ + public function get_full_sync_actions() { + return array( 'jetpack_full_sync_options' ); + } + + /** + * Retrieve all options as per the current options whitelist. + * Public so that we don't have to store so much data all the options twice. + * + * @access public + * + * @return array All options. + */ + public function get_all_options() { + $options = array(); + $random_string = wp_generate_password(); + foreach ( $this->options_whitelist as $option ) { + $option_value = get_option( $option, $random_string ); + if ( $option_value !== $random_string ) { + $options[ $option ] = $option_value; + } + } + + // Add theme mods. + $theme_mods_option = 'theme_mods_' . get_option( 'stylesheet' ); + $theme_mods_value = get_option( $theme_mods_option, $random_string ); + if ( $theme_mods_value === $random_string ) { + return $options; + } + $this->filter_theme_mods( $theme_mods_value ); + $options[ $theme_mods_option ] = $theme_mods_value; + return $options; + } + + /** + * Update the options whitelist to the default one. + * + * @access public + */ + public function update_options_whitelist() { + $this->options_whitelist = Defaults::get_options_whitelist(); + } + + /** + * Set the options whitelist. + * + * @access public + * + * @param array $options The new options whitelist. + */ + public function set_options_whitelist( $options ) { + $this->options_whitelist = $options; + } + + /** + * Get the options whitelist. + * + * @access public + * + * @return array The options whitelist. + */ + public function get_options_whitelist() { + return $this->options_whitelist; + } + + /** + * Update the contentless options to the defaults. + * + * @access public + */ + public function update_options_contentless() { + $this->options_contentless = Defaults::get_options_contentless(); + } + + /** + * Get the contentless options. + * + * @access public + * + * @return array Array of the contentless options. + */ + public function get_options_contentless() { + return $this->options_contentless; + } + + /** + * Reject any options that aren't whitelisted or contentless. + * + * @access public + * + * @param array $args The hook parameters. + * @return array $args The hook parameters. + */ + public function whitelist_options( $args ) { + // Reject non-whitelisted options. + if ( ! $this->is_whitelisted_option( $args[0] ) ) { + return false; + } + + // Filter our weird array( false ) value for theme_mods_*. + if ( 'theme_mods_' === substr( $args[0], 0, 11 ) ) { + $this->filter_theme_mods( $args[1] ); + if ( isset( $args[2] ) ) { + $this->filter_theme_mods( $args[2] ); + } + } + + // Set value(s) of contentless option to empty string(s). + if ( $this->is_contentless_option( $args[0] ) ) { + // Create a new array matching length of $args, containing empty strings. + $empty = array_fill( 0, count( $args ), '' ); + $empty[0] = $args[0]; + return $empty; + } + + return $args; + } + + /** + * Whether a certain option is whitelisted for sync. + * + * @access public + * + * @param string $option Option name. + * @return boolean Whether the option is whitelisted. + */ + public function is_whitelisted_option( $option ) { + return in_array( $option, $this->options_whitelist, true ) || 'theme_mods_' === substr( $option, 0, 11 ); + } + + /** + * Whether a certain option is a contentless one. + * + * @access private + * + * @param string $option Option name. + * @return boolean Whether the option is contentless. + */ + private function is_contentless_option( $option ) { + return in_array( $option, $this->options_contentless, true ); + } + + /** + * Filters out falsy values from theme mod options. + * + * @access private + * + * @param array $value Option value. + */ + private function filter_theme_mods( &$value ) { + if ( is_array( $value ) && isset( $value[0] ) ) { + unset( $value[0] ); + } + } + + /** + * Handle changes in the core site icon and sync them. + * + * @access public + */ + public function jetpack_sync_core_icon() { + $url = get_site_icon_url(); + + require_once JETPACK__PLUGIN_DIR . 'modules/site-icon/site-icon-functions.php'; + // If there's a core icon, maybe update the option. If not, fall back to Jetpack's. + if ( ! empty( $url ) && jetpack_site_icon_url() !== $url ) { + // This is the option that is synced with dotcom. + \Jetpack_Options::update_option( 'site_icon_url', $url ); + } elseif ( empty( $url ) ) { + \Jetpack_Options::delete_option( 'site_icon_url' ); + } + } + + /** + * Expand all options within a hook before they are serialized and sent to the server. + * + * @access public + * + * @param array $args The hook parameters. + * @return array $args The hook parameters. + */ + public function expand_options( $args ) { + if ( $args[0] ) { + return $this->get_all_options(); + } + + return $args; + } +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-plugins.php b/plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-plugins.php new file mode 100644 index 00000000..9f257557 --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-plugins.php @@ -0,0 +1,413 @@ +<?php +/** + * Plugins sync module. + * + * @package automattic/jetpack-sync + */ + +namespace Automattic\Jetpack\Sync\Modules; + +use Automattic\Jetpack\Constants as Jetpack_Constants; + +/** + * Class to handle sync for plugins. + */ +class Plugins extends Module { + /** + * Action handler callable. + * + * @access private + * + * @var callable + */ + private $action_handler; + + /** + * Information about plugins we store temporarily. + * + * @access private + * + * @var array + */ + private $plugin_info = array(); + + /** + * List of all plugins in the installation. + * + * @access private + * + * @var array + */ + private $plugins = array(); + + /** + * Sync module name. + * + * @access public + * + * @return string + */ + public function name() { + return 'plugins'; + } + + /** + * Initialize plugins action listeners. + * + * @access public + * + * @param callable $callable Action handler callable. + */ + public function init_listeners( $callable ) { + $this->action_handler = $callable; + + add_action( 'deleted_plugin', array( $this, 'deleted_plugin' ), 10, 2 ); + add_action( 'activated_plugin', $callable, 10, 2 ); + add_action( 'deactivated_plugin', $callable, 10, 2 ); + add_action( 'delete_plugin', array( $this, 'delete_plugin' ) ); + add_filter( 'upgrader_pre_install', array( $this, 'populate_plugins' ), 10, 1 ); + add_action( 'upgrader_process_complete', array( $this, 'on_upgrader_completion' ), 10, 2 ); + add_action( 'jetpack_plugin_installed', $callable, 10, 1 ); + add_action( 'jetpack_plugin_update_failed', $callable, 10, 4 ); + add_action( 'jetpack_plugins_updated', $callable, 10, 2 ); + add_action( 'admin_action_update', array( $this, 'check_plugin_edit' ) ); + add_action( 'jetpack_edited_plugin', $callable, 10, 2 ); + add_action( 'wp_ajax_edit-theme-plugin-file', array( $this, 'plugin_edit_ajax' ), 0 ); + } + + /** + * Initialize the module in the sender. + * + * @access public + */ + public function init_before_send() { + add_filter( 'jetpack_sync_before_send_activated_plugin', array( $this, 'expand_plugin_data' ) ); + add_filter( 'jetpack_sync_before_send_deactivated_plugin', array( $this, 'expand_plugin_data' ) ); + // Note that we don't simply 'expand_plugin_data' on the 'delete_plugin' action here because the plugin file is deleted when that action finishes. + } + + /** + * Fetch and populate all current plugins before upgrader installation. + * + * @access public + * + * @param bool|WP_Error $response Install response, true if successful, WP_Error if not. + */ + public function populate_plugins( $response ) { + $this->plugins = get_plugins(); + return $response; + } + + /** + * Handler for the upgrader success finishes. + * + * @access public + * + * @param \WP_Upgrader $upgrader Upgrader instance. + * @param array $details Array of bulk item update data. + */ + public function on_upgrader_completion( $upgrader, $details ) { + if ( ! isset( $details['type'] ) ) { + return; + } + if ( 'plugin' !== $details['type'] ) { + return; + } + + if ( ! isset( $details['action'] ) ) { + return; + } + + $plugins = ( isset( $details['plugins'] ) ? $details['plugins'] : null ); + if ( empty( $plugins ) ) { + $plugins = ( isset( $details['plugin'] ) ? array( $details['plugin'] ) : null ); + } + + // For plugin installer. + if ( empty( $plugins ) && method_exists( $upgrader, 'plugin_info' ) ) { + $plugins = array( $upgrader->plugin_info() ); + } + + if ( empty( $plugins ) ) { + return; // We shouldn't be here. + } + + switch ( $details['action'] ) { + case 'update': + $state = array( + 'is_autoupdate' => Jetpack_Constants::is_true( 'JETPACK_PLUGIN_AUTOUPDATE' ), + ); + $errors = $this->get_errors( $upgrader->skin ); + if ( $errors ) { + foreach ( $plugins as $slug ) { + /** + * Sync that a plugin update failed + * + * @since 5.8.0 + * + * @module sync + * + * @param string $plugin , Plugin slug + * @param string Error code + * @param string Error message + */ + do_action( 'jetpack_plugin_update_failed', $this->get_plugin_info( $slug ), $errors['code'], $errors['message'], $state ); + } + + return; + } + /** + * Sync that a plugin update + * + * @since 5.8.0 + * + * @module sync + * + * @param array () $plugin, Plugin Data + */ + do_action( 'jetpack_plugins_updated', array_map( array( $this, 'get_plugin_info' ), $plugins ), $state ); + break; + case 'install': + } + + if ( 'install' === $details['action'] ) { + /** + * Signals to the sync listener that a plugin was installed and a sync action + * reflecting the installation and the plugin info should be sent + * + * @since 5.8.0 + * + * @module sync + * + * @param array () $plugin, Plugin Data + */ + do_action( 'jetpack_plugin_installed', array_map( array( $this, 'get_plugin_info' ), $plugins ) ); + + return; + } + } + + /** + * Retrieve the plugin information by a plugin slug. + * + * @access private + * + * @param string $slug Plugin slug. + * @return array Plugin information. + */ + private function get_plugin_info( $slug ) { + $plugins = get_plugins(); // Get the most up to date info. + if ( isset( $plugins[ $slug ] ) ) { + return array_merge( array( 'slug' => $slug ), $plugins[ $slug ] ); + }; + // Try grabbing the info from before the update. + return isset( $this->plugins[ $slug ] ) ? array_merge( array( 'slug' => $slug ), $this->plugins[ $slug ] ) : array( 'slug' => $slug ); + } + + /** + * Retrieve upgrade errors. + * + * @access private + * + * @param \Automatic_Upgrader_Skin|\WP_Upgrader_Skin $skin The upgrader skin being used. + * @return array|boolean Error on error, false otherwise. + */ + private function get_errors( $skin ) { + $errors = method_exists( $skin, 'get_errors' ) ? $skin->get_errors() : null; + if ( is_wp_error( $errors ) ) { + $error_code = $errors->get_error_code(); + if ( ! empty( $error_code ) ) { + return array( + 'code' => $error_code, + 'message' => $errors->get_error_message(), + ); + } + } + + if ( isset( $skin->result ) ) { + $errors = $skin->result; + if ( is_wp_error( $errors ) ) { + return array( + 'code' => $errors->get_error_code(), + 'message' => $errors->get_error_message(), + ); + } + + if ( empty( $skin->result ) ) { + return array( + 'code' => 'unknown', + 'message' => __( 'Unknown Plugin Update Failure', 'jetpack' ), + ); + } + } + return false; + } + + /** + * Handle plugin edit in the administration. + * + * @access public + * + * @todo The `admin_action_update` hook is called only for logged in users, but maybe implement nonce verification? + */ + public function check_plugin_edit() { + $screen = get_current_screen(); + // phpcs:ignore WordPress.Security.NonceVerification.Missing + if ( 'plugin-editor' !== $screen->base || ! isset( $_POST['newcontent'] ) || ! isset( $_POST['plugin'] ) ) { + return; + } + + // phpcs:ignore WordPress.Security.NonceVerification.Missing + $plugin = $_POST['plugin']; + $plugins = get_plugins(); + if ( ! isset( $plugins[ $plugin ] ) ) { + return; + } + + /** + * Helps Sync log that a plugin was edited + * + * @since 4.9.0 + * + * @param string $plugin, Plugin slug + * @param mixed $plugins[ $plugin ], Array of plugin data + */ + do_action( 'jetpack_edited_plugin', $plugin, $plugins[ $plugin ] ); + } + + /** + * Handle plugin ajax edit in the administration. + * + * @access public + * + * @todo Update this method to use WP_Filesystem instead of fopen/fclose. + */ + public function plugin_edit_ajax() { + // This validation is based on wp_edit_theme_plugin_file(). + $args = wp_unslash( $_POST ); + if ( empty( $args['file'] ) ) { + return; + } + + $file = $args['file']; + if ( 0 !== validate_file( $file ) ) { + return; + } + + if ( ! isset( $args['newcontent'] ) ) { + return; + } + + if ( ! isset( $args['nonce'] ) ) { + return; + } + + if ( empty( $args['plugin'] ) ) { + return; + } + + $plugin = $args['plugin']; + if ( ! current_user_can( 'edit_plugins' ) ) { + return; + } + + if ( ! wp_verify_nonce( $args['nonce'], 'edit-plugin_' . $file ) ) { + return; + } + $plugins = get_plugins(); + if ( ! array_key_exists( $plugin, $plugins ) ) { + return; + } + + if ( 0 !== validate_file( $file, get_plugin_files( $plugin ) ) ) { + return; + } + + $real_file = WP_PLUGIN_DIR . '/' . $file; + + if ( ! is_writeable( $real_file ) ) { + return; + } + + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fopen + $file_pointer = fopen( $real_file, 'w+' ); + if ( false === $file_pointer ) { + return; + } + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fclose + fclose( $file_pointer ); + /** + * This action is documented already in this file + */ + do_action( 'jetpack_edited_plugin', $plugin, $plugins[ $plugin ] ); + } + + /** + * Handle plugin deletion. + * + * @access public + * + * @param string $plugin_path Path to the plugin main file. + */ + public function delete_plugin( $plugin_path ) { + $full_plugin_path = WP_PLUGIN_DIR . DIRECTORY_SEPARATOR . $plugin_path; + + // Checking for file existence because some sync plugin module tests simulate plugin installation and deletion without putting file on disk. + if ( file_exists( $full_plugin_path ) ) { + $all_plugin_data = get_plugin_data( $full_plugin_path ); + $data = array( + 'name' => $all_plugin_data['Name'], + 'version' => $all_plugin_data['Version'], + ); + } else { + $data = array( + 'name' => $plugin_path, + 'version' => 'unknown', + ); + } + + $this->plugin_info[ $plugin_path ] = $data; + } + + /** + * Invoked after plugin deletion. + * + * @access public + * + * @param string $plugin_path Path to the plugin main file. + * @param boolean $is_deleted Whether the plugin was deleted successfully. + */ + public function deleted_plugin( $plugin_path, $is_deleted ) { + call_user_func( $this->action_handler, $plugin_path, $is_deleted, $this->plugin_info[ $plugin_path ] ); + unset( $this->plugin_info[ $plugin_path ] ); + } + + /** + * Expand the plugins within a hook before they are serialized and sent to the server. + * + * @access public + * + * @param array $args The hook parameters. + * @return array $args The expanded hook parameters. + */ + public function expand_plugin_data( $args ) { + $plugin_path = $args[0]; + $plugin_data = array(); + + if ( ! function_exists( 'get_plugins' ) ) { + require_once ABSPATH . 'wp-admin/includes/plugin.php'; + } + $all_plugins = get_plugins(); + if ( isset( $all_plugins[ $plugin_path ] ) ) { + $all_plugin_data = $all_plugins[ $plugin_path ]; + $plugin_data['name'] = $all_plugin_data['Name']; + $plugin_data['version'] = $all_plugin_data['Version']; + } + + return array( + $args[0], + $args[1], + $plugin_data, + ); + } +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-posts.php b/plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-posts.php new file mode 100644 index 00000000..14d1c0b7 --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-posts.php @@ -0,0 +1,671 @@ +<?php +/** + * Posts sync module. + * + * @package automattic/jetpack-sync + */ + +namespace Automattic\Jetpack\Sync\Modules; + +use Automattic\Jetpack\Constants as Jetpack_Constants; +use Automattic\Jetpack\Roles; +use Automattic\Jetpack\Sync\Settings; + +/** + * Class to handle sync for posts. + */ +class Posts extends Module { + /** + * The post IDs of posts that were just published but not synced yet. + * + * @access private + * + * @var array + */ + private $just_published = array(); + + /** + * The previous status of posts that we use for calculating post status transitions. + * + * @access private + * + * @var array + */ + private $previous_status = array(); + + /** + * Action handler callable. + * + * @access private + * + * @var callable + */ + private $action_handler; + + /** + * Import end. + * + * @access private + * + * @todo This appears to be unused - let's remove it. + * + * @var boolean + */ + private $import_end = false; + + /** + * Default previous post state. + * Used for default previous post status. + * + * @access public + * + * @var string + */ + const DEFAULT_PREVIOUS_STATE = 'new'; + + /** + * Sync module name. + * + * @access public + * + * @return string + */ + public function name() { + return 'posts'; + } + + /** + * The table in the database. + * + * @access public + * + * @return string + */ + public function table_name() { + return 'posts'; + } + + /** + * Retrieve a post by its ID. + * + * @access public + * + * @param string $object_type Type of the sync object. + * @param int $id ID of the sync object. + * @return \WP_Post|bool Filtered \WP_Post object, or false if the object is not a post. + */ + public function get_object_by_id( $object_type, $id ) { + if ( 'post' === $object_type ) { + $post = get_post( intval( $id ) ); + if ( $post ) { + return $this->filter_post_content_and_add_links( $post ); + } + } + + return false; + } + + /** + * Initialize posts action listeners. + * + * @access public + * + * @param callable $callable Action handler callable. + */ + public function init_listeners( $callable ) { + $this->action_handler = $callable; + + add_action( 'wp_insert_post', array( $this, 'wp_insert_post' ), 11, 3 ); + add_action( 'jetpack_sync_save_post', $callable, 10, 4 ); + + add_action( 'deleted_post', $callable, 10 ); + add_action( 'jetpack_published_post', $callable, 10, 2 ); + + add_action( 'transition_post_status', array( $this, 'save_published' ), 10, 3 ); + add_filter( 'jetpack_sync_before_enqueue_jetpack_sync_save_post', array( $this, 'filter_blacklisted_post_types' ) ); + + // Listen for meta changes. + $this->init_listeners_for_meta_type( 'post', $callable ); + $this->init_meta_whitelist_handler( 'post', array( $this, 'filter_meta' ) ); + + add_action( 'jetpack_daily_akismet_meta_cleanup_before', array( $this, 'daily_akismet_meta_cleanup_before' ) ); + add_action( 'jetpack_daily_akismet_meta_cleanup_after', array( $this, 'daily_akismet_meta_cleanup_after' ) ); + add_action( 'jetpack_post_meta_batch_delete', $callable, 10, 2 ); + } + + /** + * Before Akismet's daily cleanup of spam detection metadata. + * + * @access public + * + * @param array $feedback_ids IDs of feedback posts. + */ + public function daily_akismet_meta_cleanup_before( $feedback_ids ) { + remove_action( 'deleted_post_meta', $this->action_handler ); + /** + * Used for syncing deletion of batch post meta + * + * @since 6.1.0 + * + * @module sync + * + * @param array $feedback_ids feedback post IDs + * @param string $meta_key to be deleted + */ + do_action( 'jetpack_post_meta_batch_delete', $feedback_ids, '_feedback_akismet_values' ); + } + + /** + * After Akismet's daily cleanup of spam detection metadata. + * + * @access public + * + * @param array $feedback_ids IDs of feedback posts. + */ + public function daily_akismet_meta_cleanup_after( $feedback_ids ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable + add_action( 'deleted_post_meta', $this->action_handler ); + } + + /** + * Initialize posts action listeners for full sync. + * + * @access public + * + * @param callable $callable Action handler callable. + */ + public function init_full_sync_listeners( $callable ) { + add_action( 'jetpack_full_sync_posts', $callable ); // Also sends post meta. + } + + /** + * Initialize the module in the sender. + * + * @access public + */ + public function init_before_send() { + add_filter( 'jetpack_sync_before_send_jetpack_sync_save_post', array( $this, 'expand_jetpack_sync_save_post' ) ); + + // Full sync. + add_filter( 'jetpack_sync_before_send_jetpack_full_sync_posts', array( $this, 'expand_post_ids' ) ); + } + + /** + * Enqueue the posts actions for full sync. + * + * @access public + * + * @param array $config Full sync configuration for this sync module. + * @param int $max_items_to_enqueue Maximum number of items to enqueue. + * @param boolean $state True if full sync has finished enqueueing this module, false otherwise. + * @return array Number of actions enqueued, and next module state. + */ + public function enqueue_full_sync_actions( $config, $max_items_to_enqueue, $state ) { + global $wpdb; + + return $this->enqueue_all_ids_as_action( 'jetpack_full_sync_posts', $wpdb->posts, 'ID', $this->get_where_sql( $config ), $max_items_to_enqueue, $state ); + } + + /** + * Retrieve an estimated number of actions that will be enqueued. + * + * @access public + * + * @todo Use $wpdb->prepare for the SQL query. + * + * @param array $config Full sync configuration for this sync module. + * @return array Number of items yet to be enqueued. + */ + public function estimate_full_sync_actions( $config ) { + global $wpdb; + + $query = "SELECT count(*) FROM $wpdb->posts WHERE " . $this->get_where_sql( $config ); + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + $count = $wpdb->get_var( $query ); + + return (int) ceil( $count / self::ARRAY_CHUNK_SIZE ); + } + + /** + * Retrieve the WHERE SQL clause based on the module config. + * + * @access public + * + * @param array $config Full sync configuration for this sync module. + * @return string WHERE SQL clause, or `null` if no comments are specified in the module config. + */ + public function get_where_sql( $config ) { + $where_sql = Settings::get_blacklisted_post_types_sql(); + + // Config is a list of post IDs to sync. + if ( is_array( $config ) ) { + $where_sql .= ' AND ID IN (' . implode( ',', array_map( 'intval', $config ) ) . ')'; + } + + return $where_sql; + } + + /** + * Retrieve the actions that will be sent for this module during a full sync. + * + * @access public + * + * @return array Full sync actions of this module. + */ + public function get_full_sync_actions() { + return array( 'jetpack_full_sync_posts' ); + } + + /** + * Process content before send. + * + * @param array $args Arguments of the `wp_insert_post` hook. + * + * @return array + */ + public function expand_jetpack_sync_save_post( $args ) { + list( $post_id, $post, $update, $previous_state ) = $args; + return array( $post_id, $this->filter_post_content_and_add_links( $post ), $update, $previous_state ); + } + + /** + * Filter all blacklisted post types. + * + * @param array $args Hook arguments. + * @return array|false Hook arguments, or false if the post type is a blacklisted one. + */ + public function filter_blacklisted_post_types( $args ) { + $post = $args[1]; + + if ( in_array( $post->post_type, Settings::get_setting( 'post_types_blacklist' ), true ) ) { + return false; + } + + return $args; + } + + /** + * Filter all meta that is not blacklisted, or is stored for a disallowed post type. + * + * @param array $args Hook arguments. + * @return array|false Hook arguments, or false if meta was filtered. + */ + public function filter_meta( $args ) { + if ( $this->is_post_type_allowed( $args[1] ) && $this->is_whitelisted_post_meta( $args[2] ) ) { + return $args; + } + + return false; + } + + /** + * Whether a post meta key is whitelisted. + * + * @param string $meta_key Meta key. + * @return boolean Whether the post meta key is whitelisted. + */ + public function is_whitelisted_post_meta( $meta_key ) { + // The _wpas_skip_ meta key is used by Publicize. + return in_array( $meta_key, Settings::get_setting( 'post_meta_whitelist' ), true ) || wp_startswith( $meta_key, '_wpas_skip_' ); + } + + /** + * Whether a post type is allowed. + * A post type will be disallowed if it's present in the post type blacklist. + * + * @param int $post_id ID of the post. + * @return boolean Whether the post type is allowed. + */ + public function is_post_type_allowed( $post_id ) { + $post = get_post( intval( $post_id ) ); + + if ( isset( $post->post_type ) ) { + return ! in_array( $post->post_type, Settings::get_setting( 'post_types_blacklist' ), true ); + } + return false; + } + + /** + * Remove the embed shortcode. + * + * @global $wp_embed + */ + public function remove_embed() { + global $wp_embed; + remove_filter( 'the_content', array( $wp_embed, 'run_shortcode' ), 8 ); + // remove the embed shortcode since we would do the part later. + remove_shortcode( 'embed' ); + // Attempts to embed all URLs in a post. + remove_filter( 'the_content', array( $wp_embed, 'autoembed' ), 8 ); + } + + /** + * Add the embed shortcode. + * + * @global $wp_embed + */ + public function add_embed() { + global $wp_embed; + add_filter( 'the_content', array( $wp_embed, 'run_shortcode' ), 8 ); + // Shortcode placeholder for strip_shortcodes(). + add_shortcode( 'embed', '__return_false' ); + // Attempts to embed all URLs in a post. + add_filter( 'the_content', array( $wp_embed, 'autoembed' ), 8 ); + } + + /** + * Expands wp_insert_post to include filtered content + * + * @param \WP_Post $post_object Post object. + */ + public function filter_post_content_and_add_links( $post_object ) { + global $post; + // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited + $post = $post_object; + + // Return non existant post. + $post_type = get_post_type_object( $post->post_type ); + if ( empty( $post_type ) || ! is_object( $post_type ) ) { + $non_existant_post = new \stdClass(); + $non_existant_post->ID = $post->ID; + $non_existant_post->post_modified = $post->post_modified; + $non_existant_post->post_modified_gmt = $post->post_modified_gmt; + $non_existant_post->post_status = 'jetpack_sync_non_registered_post_type'; + $non_existant_post->post_type = $post->post_type; + + return $non_existant_post; + } + /** + * Filters whether to prevent sending post data to .com + * + * Passing true to the filter will prevent the post data from being sent + * to the WordPress.com. + * Instead we pass data that will still enable us to do a checksum against the + * Jetpacks data but will prevent us from displaying the data on in the API as well as + * other services. + * + * @since 4.2.0 + * + * @param boolean false prevent post data from being synced to WordPress.com + * @param mixed $post \WP_Post object + */ + if ( apply_filters( 'jetpack_sync_prevent_sending_post_data', false, $post ) ) { + // We only send the bare necessary object to be able to create a checksum. + $blocked_post = new \stdClass(); + $blocked_post->ID = $post->ID; + $blocked_post->post_modified = $post->post_modified; + $blocked_post->post_modified_gmt = $post->post_modified_gmt; + $blocked_post->post_status = 'jetpack_sync_blocked'; + $blocked_post->post_type = $post->post_type; + + return $blocked_post; + } + + // lets not do oembed just yet. + $this->remove_embed(); + + if ( 0 < strlen( $post->post_password ) ) { + $post->post_password = 'auto-' . wp_generate_password( 10, false ); + } + + /** This filter is already documented in core. wp-includes/post-template.php */ + if ( Settings::get_setting( 'render_filtered_content' ) && $post_type->public ) { + global $shortcode_tags; + /** + * Filter prevents some shortcodes from expanding. + * + * Since we can can expand some type of shortcode better on the .com side and make the + * expansion more relevant to contexts. For example [galleries] and subscription emails + * + * @since 4.5.0 + * + * @param array of shortcode tags to remove. + */ + $shortcodes_to_remove = apply_filters( + 'jetpack_sync_do_not_expand_shortcodes', + array( + 'gallery', + 'slideshow', + ) + ); + $removed_shortcode_callbacks = array(); + foreach ( $shortcodes_to_remove as $shortcode ) { + if ( isset( $shortcode_tags[ $shortcode ] ) ) { + $removed_shortcode_callbacks[ $shortcode ] = $shortcode_tags[ $shortcode ]; + } + } + + array_map( 'remove_shortcode', array_keys( $removed_shortcode_callbacks ) ); + + $post->post_content_filtered = apply_filters( 'the_content', $post->post_content ); + $post->post_excerpt_filtered = apply_filters( 'the_excerpt', $post->post_excerpt ); + + foreach ( $removed_shortcode_callbacks as $shortcode => $callback ) { + add_shortcode( $shortcode, $callback ); + } + } + + $this->add_embed(); + + if ( has_post_thumbnail( $post->ID ) ) { + $image_attributes = wp_get_attachment_image_src( get_post_thumbnail_id( $post->ID ), 'full' ); + if ( is_array( $image_attributes ) && isset( $image_attributes[0] ) ) { + $post->featured_image = $image_attributes[0]; + } + } + + $post->permalink = get_permalink( $post->ID ); + $post->shortlink = wp_get_shortlink( $post->ID ); + + if ( function_exists( 'amp_get_permalink' ) ) { + $post->amp_permalink = amp_get_permalink( $post->ID ); + } + + return $post; + } + + /** + * Handle transition from another post status to a published one. + * + * @param string $new_status New post status. + * @param string $old_status Old post status. + * @param \WP_Post $post Post object. + */ + public function save_published( $new_status, $old_status, $post ) { + if ( 'publish' === $new_status && 'publish' !== $old_status ) { + $this->just_published[ $post->ID ] = true; + } + + $this->previous_status[ $post->ID ] = $old_status; + } + + /** + * When publishing or updating a post, the Gutenberg editor sends two requests: + * 1. sent to WP REST API endpoint `wp-json/wp/v2/posts/$id` + * 2. sent to wp-admin/post.php `?post=$id&action=edit&classic-editor=1&meta_box=1` + * + * The 2nd request is to update post meta, which is not supported on WP REST API. + * When syncing post data, we will include if this was a meta box update. + * + * @todo Implement nonce verification. + * + * @return boolean Whether this is a Gutenberg meta box update. + */ + public function is_gutenberg_meta_box_update() { + // phpcs:disable WordPress.Security.NonceVerification.Missing, WordPress.Security.NonceVerification.Recommended + return ( + isset( $_POST['action'], $_GET['classic-editor'], $_GET['meta_box'] ) && + 'editpost' === $_POST['action'] && + '1' === $_GET['classic-editor'] && + '1' === $_GET['meta_box'] + // phpcs:enable WordPress.Security.NonceVerification.Missing, WordPress.Security.NonceVerification.Recommended + ); + } + + /** + * Handler for the wp_insert_post hook. + * Called upon creation of a new post. + * + * @param int $post_ID Post ID. + * @param \WP_Post $post Post object. + * @param boolean $update Whether this is an existing post being updated or not. + */ + public function wp_insert_post( $post_ID, $post = null, $update = null ) { + if ( ! is_numeric( $post_ID ) || is_null( $post ) ) { + return; + } + + // Workaround for https://github.com/woocommerce/woocommerce/issues/18007. + if ( $post && 'shop_order' === $post->post_type ) { + $post = get_post( $post_ID ); + } + + $previous_status = isset( $this->previous_status[ $post_ID ] ) ? + $this->previous_status[ $post_ID ] : + self::DEFAULT_PREVIOUS_STATE; + + $just_published = isset( $this->just_published[ $post_ID ] ) ? + $this->just_published[ $post_ID ] : + false; + + $state = array( + 'is_auto_save' => (bool) Jetpack_Constants::get_constant( 'DOING_AUTOSAVE' ), + 'previous_status' => $previous_status, + 'just_published' => $just_published, + 'is_gutenberg_meta_box_update' => $this->is_gutenberg_meta_box_update(), + ); + /** + * Filter that is used to add to the post flags ( meta data ) when a post gets published + * + * @since 5.8.0 + * + * @param int $post_ID the post ID + * @param mixed $post \WP_Post object + * @param bool $update Whether this is an existing post being updated or not. + * @param mixed $state state + * + * @module sync + */ + do_action( 'jetpack_sync_save_post', $post_ID, $post, $update, $state ); + unset( $this->previous_status[ $post_ID ] ); + $this->send_published( $post_ID, $post ); + } + + /** + * Send a published post for sync. + * + * @param int $post_ID Post ID. + * @param \WP_Post $post Post object. + */ + public function send_published( $post_ID, $post ) { + if ( ! isset( $this->just_published[ $post_ID ] ) ) { + return; + } + + // Post revisions cause race conditions where this send_published add the action before the actual post gets synced. + if ( wp_is_post_autosave( $post ) || wp_is_post_revision( $post ) ) { + return; + } + + $post_flags = array( + 'post_type' => $post->post_type, + ); + + $author_user_object = get_user_by( 'id', $post->post_author ); + if ( $author_user_object ) { + $roles = new Roles(); + + $post_flags['author'] = array( + 'id' => $post->post_author, + 'wpcom_user_id' => get_user_meta( $post->post_author, 'wpcom_user_id', true ), + 'display_name' => $author_user_object->display_name, + 'email' => $author_user_object->user_email, + 'translated_role' => $roles->translate_user_to_role( $author_user_object ), + ); + } + + /** + * Filter that is used to add to the post flags ( meta data ) when a post gets published + * + * @since 4.4.0 + * + * @param mixed array post flags that are added to the post + * @param mixed $post \WP_Post object + */ + $flags = apply_filters( 'jetpack_published_post_flags', $post_flags, $post ); + + /** + * Action that gets synced when a post type gets published. + * + * @since 4.4.0 + * + * @param int $post_ID + * @param mixed array $flags post flags that are added to the post + */ + do_action( 'jetpack_published_post', $post_ID, $flags ); + unset( $this->just_published[ $post_ID ] ); + + /** + * Send additional sync action for Activity Log when post is a Customizer publish + */ + if ( 'customize_changeset' === $post->post_type ) { + $post_content = json_decode( $post->post_content, true ); + foreach ( $post_content as $key => $value ) { + // Skip if it isn't a widget. + if ( 'widget_' !== substr( $key, 0, strlen( 'widget_' ) ) ) { + continue; + } + // Change key from "widget_archives[2]" to "archives-2". + $key = str_replace( 'widget_', '', $key ); + $key = str_replace( '[', '-', $key ); + $key = str_replace( ']', '', $key ); + + global $wp_registered_widgets; + if ( isset( $wp_registered_widgets[ $key ] ) ) { + $widget_data = array( + 'name' => $wp_registered_widgets[ $key ]['name'], + 'id' => $key, + 'title' => $value['value']['title'], + ); + do_action( 'jetpack_widget_edited', $widget_data ); + } + } + } + } + + /** + * Expand post IDs to post objects within a hook before they are serialized and sent to the server. + * + * @access public + * + * @param array $args The hook parameters. + * @return array $args The expanded hook parameters. + */ + public function expand_post_ids( $args ) { + list( $post_ids, $previous_interval_end) = $args; + + $posts = array_filter( array_map( array( 'WP_Post', 'get_instance' ), $post_ids ) ); + $posts = array_map( array( $this, 'filter_post_content_and_add_links' ), $posts ); + $posts = array_values( $posts ); // Reindex in case posts were deleted. + + return array( + $posts, + $this->get_metadata( $post_ids, 'post', Settings::get_setting( 'post_meta_whitelist' ) ), + $this->get_term_relationships( $post_ids ), + $previous_interval_end, + ); + } + + /** + * Gets a list of minimum and maximum object ids for each batch based on the given batch size. + * + * @access public + * + * @param int $batch_size The batch size for objects. + * @param string|bool $where_sql The sql where clause minus 'WHERE', or false if no where clause is needed. + * + * @return array|bool An array of min and max ids for each batch. FALSE if no table can be found. + */ + public function get_min_max_object_ids_for_batches( $batch_size, $where_sql = false ) { + return parent::get_min_max_object_ids_for_batches( $batch_size, $this->get_where_sql( $where_sql ) ); + } +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-protect.php b/plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-protect.php new file mode 100644 index 00000000..ebd62ff8 --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-protect.php @@ -0,0 +1,53 @@ +<?php +/** + * Protect sync module. + * + * @package automattic/jetpack-sync + */ + +namespace Automattic\Jetpack\Sync\Modules; + +use Automattic\Jetpack\Constants as Jetpack_Constants; + +/** + * Class to handle sync for Protect. + * Logs BruteProtect failed logins via sync. + */ +class Protect extends Module { + /** + * Sync module name. + * + * @access public + * + * @return string + */ + public function name() { + return 'protect'; + } + + /** + * Initialize Protect action listeners. + * + * @access public + * + * @param callable $callback Action handler callable. + */ + public function init_listeners( $callback ) { + add_action( 'jpp_log_failed_attempt', array( $this, 'maybe_log_failed_login_attempt' ) ); + add_action( 'jetpack_valid_failed_login_attempt', $callback ); + } + + /** + * Maybe log a failed login attempt. + * + * @access public + * + * @param array $failed_attempt Failed attempt data. + */ + public function maybe_log_failed_login_attempt( $failed_attempt ) { + $protect = \Jetpack_Protect_Module::instance(); + if ( $protect->has_login_ability() && ! Jetpack_Constants::is_true( 'XMLRPC_REQUEST' ) ) { + do_action( 'jetpack_valid_failed_login_attempt', $failed_attempt ); + } + } +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-stats.php b/plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-stats.php new file mode 100644 index 00000000..bbd4cae6 --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-stats.php @@ -0,0 +1,66 @@ +<?php +/** + * Stats sync module. + * + * @package automattic/jetpack-sync + */ + +namespace Automattic\Jetpack\Sync\Modules; + +/** + * Class to handle sync for stats. + */ +class Stats extends Module { + /** + * Sync module name. + * + * @access public + * + * @return string + */ + public function name() { + return 'stats'; + } + + /** + * Initialize stats action listeners. + * + * @access public + * + * @param callable $callback Action handler callable. + */ + public function init_listeners( $callback ) { + add_action( 'jetpack_heartbeat', array( $this, 'sync_site_stats' ), 20 ); + add_action( 'jetpack_sync_heartbeat_stats', $callback ); + } + + /** + * This namespaces the action that we sync. + * So that we can differentiate it from future actions. + * + * @access public + */ + public function sync_site_stats() { + do_action( 'jetpack_sync_heartbeat_stats' ); + } + + /** + * Initialize the module in the sender. + * + * @access public + */ + public function init_before_send() { + add_filter( 'jetpack_sync_before_send_jetpack_sync_heartbeat_stats', array( $this, 'add_stats' ) ); + } + + /** + * Retrieve the stats data for the site. + * + * @access public + * + * @return array Stats data. + */ + public function add_stats() { + return array( \Jetpack::get_stat_data( false, false ) ); + } +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-term-relationships.php b/plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-term-relationships.php new file mode 100644 index 00000000..3cad885d --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-term-relationships.php @@ -0,0 +1,204 @@ +<?php +/** + * Term relationships sync module. + * + * @package automattic/jetpack-sync + */ + +namespace Automattic\Jetpack\Sync\Modules; + +use Automattic\Jetpack\Sync\Listener; +use Automattic\Jetpack\Sync\Settings; + +/** + * Class to handle sync for term relationships. + */ +class Term_Relationships extends Module { + + /** + * Max terms to return in one single query + * + * @access public + * + * @const int + */ + const QUERY_LIMIT = 1000; + + /** + * Max value for a signed INT in MySQL - https://dev.mysql.com/doc/refman/8.0/en/integer-types.html + * + * @access public + * + * @const int + */ + const MAX_INT = 2147483647; + + /** + * Sync module name. + * + * @access public + * + * @return string + */ + public function name() { + return 'term_relationships'; + } + + /** + * The id field in the database. + * + * @access public + * + * @return string + */ + public function id_field() { + return 'object_id'; + } + + /** + * The table in the database. + * + * @access public + * + * @return string + */ + public function table_name() { + return 'term_relationships'; + } + + /** + * Initialize term relationships action listeners for full sync. + * + * @access public + * + * @param callable $callable Action handler callable. + */ + public function init_full_sync_listeners( $callable ) { + add_action( 'jetpack_full_sync_term_relationships', $callable, 10, 2 ); + } + + /** + * Initialize the module in the sender. + * + * @access public + */ + public function init_before_send() { + // Full sync. + add_filter( 'jetpack_sync_before_send_jetpack_full_sync_term_relationships', array( $this, 'expand_term_relationships' ) ); + } + + /** + * Enqueue the term relationships actions for full sync. + * + * @access public + * + * @param array $config Full sync configuration for this sync module. + * @param int $max_items_to_enqueue Maximum number of items to enqueue. + * @param object $last_object_enqueued Last object enqueued. + * + * @return array Number of actions enqueued, and next module state. + * @todo This method has similarities with Automattic\Jetpack\Sync\Modules\Module::enqueue_all_ids_as_action. Refactor to keep DRY. + * @see Automattic\Jetpack\Sync\Modules\Module::enqueue_all_ids_as_action + */ + public function enqueue_full_sync_actions( $config, $max_items_to_enqueue, $last_object_enqueued ) { + global $wpdb; + $term_relationships_full_sync_item_size = Settings::get_setting( 'term_relationships_full_sync_item_size' ); + $limit = min( $max_items_to_enqueue * $term_relationships_full_sync_item_size, self::QUERY_LIMIT ); + $items_enqueued_count = 0; + $last_object_enqueued = $last_object_enqueued ? $last_object_enqueued : array( + 'object_id' => self::MAX_INT, + 'term_taxonomy_id' => self::MAX_INT, + ); + + while ( $limit > 0 ) { + /* + * SELECT object_id, term_taxonomy_id + * FROM $wpdb->term_relationships + * WHERE ( object_id = 11 AND term_taxonomy_id < 14 ) OR ( object_id < 11 ) + * ORDER BY object_id DESC, term_taxonomy_id DESC LIMIT 1000 + */ + $objects = $wpdb->get_results( $wpdb->prepare( "SELECT object_id, term_taxonomy_id FROM $wpdb->term_relationships WHERE ( object_id = %d AND term_taxonomy_id < %d ) OR ( object_id < %d ) ORDER BY object_id DESC, term_taxonomy_id DESC LIMIT %d", $last_object_enqueued['object_id'], $last_object_enqueued['term_taxonomy_id'], $last_object_enqueued['object_id'], $limit ), ARRAY_A ); + // Request term relationships in groups of N for efficiency. + $objects_count = count( $objects ); + if ( ! count( $objects ) ) { + return array( $items_enqueued_count, true ); + } + $items = array_chunk( $objects, $term_relationships_full_sync_item_size ); + $last_object_enqueued = $this->bulk_enqueue_full_sync_term_relationships( $items, $last_object_enqueued ); + $items_enqueued_count += count( $items ); + $limit = min( $limit - $objects_count, self::QUERY_LIMIT ); + } + + // We need to do this extra check in case $max_items_to_enqueue * $term_relationships_full_sync_item_size == relationships objects left. + $count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $wpdb->term_relationships WHERE ( object_id = %d AND term_taxonomy_id < %d ) OR ( object_id < %d ) ORDER BY object_id DESC, term_taxonomy_id DESC LIMIT %d", $last_object_enqueued['object_id'], $last_object_enqueued['term_taxonomy_id'], $last_object_enqueued['object_id'], 1 ) ); + if ( intval( $count ) === 0 ) { + return array( $items_enqueued_count, true ); + } + + return array( $items_enqueued_count, $last_object_enqueued ); + } + + /** + * + * Enqueue all $items within `jetpack_full_sync_term_relationships` actions. + * + * @param array $items Groups of objects to sync. + * @param array $previous_interval_end Last item enqueued. + * + * @return array Last enqueued object. + */ + public function bulk_enqueue_full_sync_term_relationships( $items, $previous_interval_end ) { + $listener = Listener::get_instance(); + $items_with_previous_interval_end = $this->get_chunks_with_preceding_end( $items, $previous_interval_end ); + $listener->bulk_enqueue_full_sync_actions( 'jetpack_full_sync_term_relationships', $items_with_previous_interval_end ); + $last_item = end( $items ); + return end( $last_item ); + } + + /** + * Retrieve an estimated number of actions that will be enqueued. + * + * @access public + * + * @param array $config Full sync configuration for this sync module. + * @return int Number of items yet to be enqueued. + */ + public function estimate_full_sync_actions( $config ) { + global $wpdb; + + $query = "SELECT COUNT(*) FROM $wpdb->term_relationships"; + + // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared + $count = $wpdb->get_var( $query ); + + return (int) ceil( $count / Settings::get_setting( 'term_relationships_full_sync_item_size' ) ); + } + + /** + * Retrieve the actions that will be sent for this module during a full sync. + * + * @access public + * + * @return array Full sync actions of this module. + */ + public function get_full_sync_actions() { + return array( 'jetpack_full_sync_term_relationships' ); + } + + /** + * Expand the term relationships within a hook before they are serialized and sent to the server. + * + * @access public + * + * @param array $args The hook parameters. + * @return array $args The expanded hook parameters. + */ + public function expand_term_relationships( $args ) { + list( $term_relationships, $previous_end ) = $args; + + return array( + 'term_relationships' => $term_relationships, + 'previous_end' => $previous_end, + ); + } +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-terms.php b/plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-terms.php new file mode 100644 index 00000000..36afc5d7 --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-terms.php @@ -0,0 +1,322 @@ +<?php +/** + * Terms sync module. + * + * @package automattic/jetpack-sync + */ + +namespace Automattic\Jetpack\Sync\Modules; + +use Automattic\Jetpack\Sync\Defaults; +use Automattic\Jetpack\Sync\Settings; + +/** + * Class to handle sync for terms. + */ +class Terms extends Module { + /** + * Whitelist for taxonomies we want to sync. + * + * @access private + * + * @var array + */ + private $taxonomy_whitelist; + + /** + * Sync module name. + * + * @access public + * + * @return string + */ + public function name() { + return 'terms'; + } + + /** + * The id field in the database. + * + * @access public + * + * @return string + */ + public function id_field() { + return 'term_id'; + } + + /** + * The table in the database. + * + * @access public + * + * @return string + */ + public function table_name() { + return 'terms'; + } + + /** + * Allows WordPress.com servers to retrieve term-related objects via the sync API. + * + * @param string $object_type The type of object. + * @param int $id The id of the object. + * + * @return bool|object A WP_Term object, or a row from term_taxonomy table depending on object type. + */ + public function get_object_by_id( $object_type, $id ) { + global $wpdb; + $object = false; + if ( 'term' === $object_type ) { + $object = get_term( intval( $id ) ); + + if ( is_wp_error( $object ) && $object->get_error_code() === 'invalid_taxonomy' ) { + // Fetch raw term. + $columns = implode( ', ', array_unique( array_merge( Defaults::$default_term_checksum_columns, array( 'term_group' ) ) ) ); + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + $object = $wpdb->get_row( $wpdb->prepare( "SELECT $columns FROM $wpdb->terms WHERE term_id = %d", $id ) ); + } + } + + if ( 'term_taxonomy' === $object_type ) { + $columns = implode( ', ', array_unique( array_merge( Defaults::$default_term_taxonomy_checksum_columns, array( 'description' ) ) ) ); + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + $object = $wpdb->get_row( $wpdb->prepare( "SELECT $columns FROM $wpdb->term_taxonomy WHERE term_taxonomy_id = %d", $id ) ); + } + + if ( 'term_relationships' === $object_type ) { + $columns = implode( ', ', Defaults::$default_term_relationships_checksum_columns ); + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + $objects = $wpdb->get_results( $wpdb->prepare( "SELECT $columns FROM $wpdb->term_relationships WHERE object_id = %d", $id ) ); + $object = (object) array( + 'object_id' => $id, + 'relationships' => array_map( array( $this, 'expand_terms_for_relationship' ), $objects ), + ); + } + + return $object ? $object : false; + } + + /** + * Initialize terms action listeners. + * + * @access public + * + * @param callable $callable Action handler callable. + */ + public function init_listeners( $callable ) { + add_action( 'created_term', array( $this, 'save_term_handler' ), 10, 3 ); + add_action( 'edited_term', array( $this, 'save_term_handler' ), 10, 3 ); + add_action( 'jetpack_sync_save_term', $callable ); + add_action( 'jetpack_sync_add_term', $callable ); + add_action( 'delete_term', $callable, 10, 4 ); + add_action( 'set_object_terms', $callable, 10, 6 ); + add_action( 'deleted_term_relationships', $callable, 10, 2 ); + add_filter( 'jetpack_sync_before_enqueue_jetpack_sync_save_term', array( $this, 'filter_blacklisted_taxonomies' ) ); + add_filter( 'jetpack_sync_before_enqueue_jetpack_sync_add_term', array( $this, 'filter_blacklisted_taxonomies' ) ); + } + + /** + * Initialize terms action listeners for full sync. + * + * @access public + * + * @param callable $callable Action handler callable. + */ + public function init_full_sync_listeners( $callable ) { + add_action( 'jetpack_full_sync_terms', $callable, 10, 2 ); + } + + /** + * Initialize the module in the sender. + * + * @access public + */ + public function init_before_send() { + // Full sync. + add_filter( 'jetpack_sync_before_send_jetpack_full_sync_terms', array( $this, 'expand_term_taxonomy_id' ) ); + } + + /** + * Enqueue the terms actions for full sync. + * + * @access public + * + * @param array $config Full sync configuration for this sync module. + * @param int $max_items_to_enqueue Maximum number of items to enqueue. + * @param boolean $state True if full sync has finished enqueueing this module, false otherwise. + * @return array Number of actions enqueued, and next module state. + */ + public function enqueue_full_sync_actions( $config, $max_items_to_enqueue, $state ) { + global $wpdb; + return $this->enqueue_all_ids_as_action( 'jetpack_full_sync_terms', $wpdb->term_taxonomy, 'term_taxonomy_id', $this->get_where_sql( $config ), $max_items_to_enqueue, $state ); + } + + /** + * Retrieve the WHERE SQL clause based on the module config. + * + * @access public + * + * @param array $config Full sync configuration for this sync module. + * @return string WHERE SQL clause, or `null` if no comments are specified in the module config. + */ + public function get_where_sql( $config ) { + $where_sql = Settings::get_blacklisted_taxonomies_sql(); + + if ( is_array( $config ) ) { + $where_sql .= ' AND term_taxonomy_id IN (' . implode( ',', array_map( 'intval', $config ) ) . ')'; + } + + return $where_sql; + } + + /** + * Retrieve an estimated number of actions that will be enqueued. + * + * @access public + * + * @param array $config Full sync configuration for this sync module. + * @return int Number of items yet to be enqueued. + */ + public function estimate_full_sync_actions( $config ) { + global $wpdb; + + $query = "SELECT count(*) FROM $wpdb->term_taxonomy"; + + $where_sql = $this->get_where_sql( $config ); + if ( $where_sql ) { + $query .= ' WHERE ' . $where_sql; + } + + // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared + $count = $wpdb->get_var( $query ); + + return (int) ceil( $count / self::ARRAY_CHUNK_SIZE ); + } + + /** + * Retrieve the actions that will be sent for this module during a full sync. + * + * @access public + * + * @return array Full sync actions of this module. + */ + public function get_full_sync_actions() { + return array( 'jetpack_full_sync_terms' ); + } + + /** + * Handler for creating and updating terms. + * + * @access public + * + * @param int $term_id Term ID. + * @param int $tt_id Term taxonomy ID. + * @param string $taxonomy Taxonomy slug. + */ + public function save_term_handler( $term_id, $tt_id, $taxonomy ) { + if ( class_exists( '\\WP_Term' ) ) { + $term_object = \WP_Term::get_instance( $term_id, $taxonomy ); + } else { + $term_object = get_term_by( 'id', $term_id, $taxonomy ); + } + + $current_filter = current_filter(); + + if ( 'created_term' === $current_filter ) { + /** + * Fires when the client needs to add a new term + * + * @since 5.0.0 + * + * @param object the Term object + */ + do_action( 'jetpack_sync_add_term', $term_object ); + return; + } + + /** + * Fires when the client needs to update a term + * + * @since 4.2.0 + * + * @param object the Term object + */ + do_action( 'jetpack_sync_save_term', $term_object ); + } + + /** + * Filter blacklisted taxonomies. + * + * @access public + * + * @param array $args Hook args. + * @return array|boolean False if not whitelisted, the original hook args otherwise. + */ + public function filter_blacklisted_taxonomies( $args ) { + $term = $args[0]; + + if ( in_array( $term->taxonomy, Settings::get_setting( 'taxonomies_blacklist' ), true ) ) { + return false; + } + + return $args; + } + + /** + * Set the taxonomy whitelist. + * + * @access public + * + * @param array $taxonomies The new taxonomyy whitelist. + */ + public function set_taxonomy_whitelist( $taxonomies ) { + $this->taxonomy_whitelist = $taxonomies; + } + + /** + * Set module defaults. + * Define the taxonomy whitelist to be the default one. + * + * @access public + */ + public function set_defaults() { + $this->taxonomy_whitelist = Defaults::$default_taxonomy_whitelist; + } + + /** + * Expand the term taxonomy IDs to terms within a hook before they are serialized and sent to the server. + * + * @access public + * + * @param array $args The hook parameters. + * @return array $args The expanded hook parameters. + */ + public function expand_term_taxonomy_id( $args ) { + list( $term_taxonomy_ids, $previous_end ) = $args; + + return array( + 'terms' => get_terms( + array( + 'hide_empty' => false, + 'term_taxonomy_id' => $term_taxonomy_ids, + 'orderby' => 'term_taxonomy_id', + 'order' => 'DESC', + ) + ), + 'previous_end' => $previous_end, + ); + } + + /** + * Gets a term object based on a given row from the term_relationships database table. + * + * @access public + * + * @param object $relationship A row object from the term_relationships table. + * @return object|bool A term object, or false if term taxonomy doesn't exist. + */ + public function expand_terms_for_relationship( $relationship ) { + return get_term_by( 'term_taxonomy_id', $relationship->term_taxonomy_id ); + } +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-themes.php b/plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-themes.php new file mode 100644 index 00000000..57535527 --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-themes.php @@ -0,0 +1,825 @@ +<?php +/** + * Themes sync module. + * + * @package automattic/jetpack-sync + */ + +namespace Automattic\Jetpack\Sync\Modules; + +use Automattic\Jetpack\Sync\Defaults; + +/** + * Class to handle sync for themes. + */ +class Themes extends Module { + /** + * Sync module name. + * + * @access public + * + * @return string + */ + public function name() { + return 'themes'; + } + + /** + * Initialize themes action listeners. + * + * @access public + * + * @param callable $callable Action handler callable. + */ + public function init_listeners( $callable ) { + add_action( 'switch_theme', array( $this, 'sync_theme_support' ), 10, 3 ); + add_action( 'jetpack_sync_current_theme_support', $callable, 10, 2 ); + add_action( 'upgrader_process_complete', array( $this, 'check_upgrader' ), 10, 2 ); + add_action( 'jetpack_installed_theme', $callable, 10, 2 ); + add_action( 'jetpack_updated_themes', $callable, 10, 2 ); + add_action( 'delete_site_transient_update_themes', array( $this, 'detect_theme_deletion' ) ); + add_action( 'jetpack_deleted_theme', $callable, 10, 2 ); + add_filter( 'wp_redirect', array( $this, 'detect_theme_edit' ) ); + add_action( 'jetpack_edited_theme', $callable, 10, 2 ); + add_action( 'wp_ajax_edit-theme-plugin-file', array( $this, 'theme_edit_ajax' ), 0 ); + add_action( 'update_site_option_allowedthemes', array( $this, 'sync_network_allowed_themes_change' ), 10, 4 ); + add_action( 'jetpack_network_disabled_themes', $callable, 10, 2 ); + add_action( 'jetpack_network_enabled_themes', $callable, 10, 2 ); + + // Sidebar updates. + add_action( 'update_option_sidebars_widgets', array( $this, 'sync_sidebar_widgets_actions' ), 10, 2 ); + + add_action( 'jetpack_widget_added', $callable, 10, 4 ); + add_action( 'jetpack_widget_removed', $callable, 10, 4 ); + add_action( 'jetpack_widget_moved_to_inactive', $callable, 10, 2 ); + add_action( 'jetpack_cleared_inactive_widgets', $callable ); + add_action( 'jetpack_widget_reordered', $callable, 10, 2 ); + add_filter( 'widget_update_callback', array( $this, 'sync_widget_edit' ), 10, 4 ); + add_action( 'jetpack_widget_edited', $callable ); + } + + /** + * Sync handler for a widget edit. + * + * @access public + * + * @todo Implement nonce verification + * + * @param array $instance The current widget instance's settings. + * @param array $new_instance Array of new widget settings. + * @param array $old_instance Array of old widget settings. + * @param \WP_Widget $widget_object The current widget instance. + * @return array The current widget instance's settings. + */ + public function sync_widget_edit( $instance, $new_instance, $old_instance, $widget_object ) { + if ( empty( $old_instance ) ) { + return $instance; + } + + // Don't trigger sync action if this is an ajax request, because Customizer makes them during preview before saving changes. + // phpcs:disable WordPress.Security.NonceVerification.Missing + if ( defined( 'DOING_AJAX' ) && DOING_AJAX && isset( $_POST['customized'] ) ) { + return $instance; + } + + $widget = array( + 'name' => $widget_object->name, + 'id' => $widget_object->id, + 'title' => isset( $new_instance['title'] ) ? $new_instance['title'] : '', + ); + /** + * Trigger action to alert $callable sync listener that a widget was edited. + * + * @since 5.0.0 + * + * @param string $widget_name , Name of edited widget + */ + do_action( 'jetpack_widget_edited', $widget ); + + return $instance; + } + + /** + * Sync handler for network allowed themes change. + * + * @access public + * + * @param string $option Name of the network option. + * @param mixed $value Current value of the network option. + * @param mixed $old_value Old value of the network option. + * @param int $network_id ID of the network. + */ + public function sync_network_allowed_themes_change( $option, $value, $old_value, $network_id ) { + $all_enabled_theme_slugs = array_keys( $value ); + + if ( count( $old_value ) > count( $value ) ) { + + // Suppress jetpack_network_disabled_themes sync action when theme is deleted. + $delete_theme_call = $this->get_delete_theme_call(); + if ( ! empty( $delete_theme_call ) ) { + return; + } + + $newly_disabled_theme_names = array_keys( array_diff_key( $old_value, $value ) ); + $newly_disabled_themes = $this->get_theme_details_for_slugs( $newly_disabled_theme_names ); + /** + * Trigger action to alert $callable sync listener that network themes were disabled. + * + * @since 5.0.0 + * + * @param mixed $newly_disabled_themes, Array of info about network disabled themes + * @param mixed $all_enabled_theme_slugs, Array of slugs of all enabled themes + */ + do_action( 'jetpack_network_disabled_themes', $newly_disabled_themes, $all_enabled_theme_slugs ); + return; + } + + $newly_enabled_theme_names = array_keys( array_diff_key( $value, $old_value ) ); + $newly_enabled_themes = $this->get_theme_details_for_slugs( $newly_enabled_theme_names ); + /** + * Trigger action to alert $callable sync listener that network themes were enabled + * + * @since 5.0.0 + * + * @param mixed $newly_enabled_themes , Array of info about network enabled themes + * @param mixed $all_enabled_theme_slugs, Array of slugs of all enabled themes + */ + do_action( 'jetpack_network_enabled_themes', $newly_enabled_themes, $all_enabled_theme_slugs ); + } + + /** + * Retrieve details for one or more themes by their slugs. + * + * @access private + * + * @param array $theme_slugs Theme slugs. + * @return array Details for the themes. + */ + private function get_theme_details_for_slugs( $theme_slugs ) { + $theme_data = array(); + foreach ( $theme_slugs as $slug ) { + $theme = wp_get_theme( $slug ); + $theme_data[ $slug ] = array( + 'name' => $theme->get( 'Name' ), + 'version' => $theme->get( 'Version' ), + 'uri' => $theme->get( 'ThemeURI' ), + 'slug' => $slug, + ); + } + return $theme_data; + } + + /** + * Detect a theme edit during a redirect. + * + * @access public + * + * @param string $redirect_url Redirect URL. + * @return string Redirect URL. + */ + public function detect_theme_edit( $redirect_url ) { + $url = wp_parse_url( admin_url( $redirect_url ) ); + $theme_editor_url = wp_parse_url( admin_url( 'theme-editor.php' ) ); + + if ( $theme_editor_url['path'] !== $url['path'] ) { + return $redirect_url; + } + + $query_params = array(); + wp_parse_str( $url['query'], $query_params ); + if ( + ! isset( $_POST['newcontent'] ) || + ! isset( $query_params['file'] ) || + ! isset( $query_params['theme'] ) || + ! isset( $query_params['updated'] ) + ) { + return $redirect_url; + } + $theme = wp_get_theme( $query_params['theme'] ); + $theme_data = array( + 'name' => $theme->get( 'Name' ), + 'version' => $theme->get( 'Version' ), + 'uri' => $theme->get( 'ThemeURI' ), + ); + + /** + * Trigger action to alert $callable sync listener that a theme was edited. + * + * @since 5.0.0 + * + * @param string $query_params['theme'], Slug of edited theme + * @param string $theme_data, Information about edited them + */ + do_action( 'jetpack_edited_theme', $query_params['theme'], $theme_data ); + + return $redirect_url; + } + + /** + * Handler for AJAX theme editing. + * + * @todo Refactor to use WP_Filesystem instead of fopen()/fclose(). + */ + public function theme_edit_ajax() { + $args = wp_unslash( $_POST ); + + if ( empty( $args['theme'] ) ) { + return; + } + + if ( empty( $args['file'] ) ) { + return; + } + $file = $args['file']; + if ( 0 !== validate_file( $file ) ) { + return; + } + + if ( ! isset( $args['newcontent'] ) ) { + return; + } + + if ( ! isset( $args['nonce'] ) ) { + return; + } + + $stylesheet = $args['theme']; + if ( 0 !== validate_file( $stylesheet ) ) { + return; + } + + if ( ! current_user_can( 'edit_themes' ) ) { + return; + } + + $theme = wp_get_theme( $stylesheet ); + if ( ! $theme->exists() ) { + return; + } + + $real_file = $theme->get_stylesheet_directory() . '/' . $file; + if ( ! wp_verify_nonce( $args['nonce'], 'edit-theme_' . $real_file . $stylesheet ) ) { + return; + } + + if ( $theme->errors() && 'theme_no_stylesheet' === $theme->errors()->get_error_code() ) { + return; + } + + $editable_extensions = wp_get_theme_file_editable_extensions( $theme ); + + $allowed_files = array(); + foreach ( $editable_extensions as $type ) { + switch ( $type ) { + case 'php': + $allowed_files = array_merge( $allowed_files, $theme->get_files( 'php', -1 ) ); + break; + case 'css': + $style_files = $theme->get_files( 'css', -1 ); + $allowed_files['style.css'] = $style_files['style.css']; + $allowed_files = array_merge( $allowed_files, $style_files ); + break; + default: + $allowed_files = array_merge( $allowed_files, $theme->get_files( $type, -1 ) ); + break; + } + } + + if ( 0 !== validate_file( $real_file, $allowed_files ) ) { + return; + } + + // Ensure file is real. + if ( ! is_file( $real_file ) ) { + return; + } + + // Ensure file extension is allowed. + $extension = null; + if ( preg_match( '/\.([^.]+)$/', $real_file, $matches ) ) { + $extension = strtolower( $matches[1] ); + if ( ! in_array( $extension, $editable_extensions, true ) ) { + return; + } + } + + if ( ! is_writeable( $real_file ) ) { + return; + } + + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fopen + $file_pointer = fopen( $real_file, 'w+' ); + if ( false === $file_pointer ) { + return; + } + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fclose + fclose( $file_pointer ); + + $theme_data = array( + 'name' => $theme->get( 'Name' ), + 'version' => $theme->get( 'Version' ), + 'uri' => $theme->get( 'ThemeURI' ), + ); + + /** + * This action is documented already in this file. + */ + do_action( 'jetpack_edited_theme', $stylesheet, $theme_data ); + } + + /** + * Detect a theme deletion. + * + * @access public + */ + public function detect_theme_deletion() { + $delete_theme_call = $this->get_delete_theme_call(); + if ( empty( $delete_theme_call ) ) { + return; + } + + $slug = $delete_theme_call['args'][0]; + $theme = wp_get_theme( $slug ); + $theme_data = array( + 'name' => $theme->get( 'Name' ), + 'version' => $theme->get( 'Version' ), + 'uri' => $theme->get( 'ThemeURI' ), + 'slug' => $slug, + ); + + /** + * Signals to the sync listener that a theme was deleted and a sync action + * reflecting the deletion and theme slug should be sent + * + * @since 5.0.0 + * + * @param string $slug Theme slug + * @param array $theme_data Theme info Since 5.3 + */ + do_action( 'jetpack_deleted_theme', $slug, $theme_data ); + } + + /** + * Handle an upgrader completion action. + * + * @access public + * + * @param \WP_Upgrader $upgrader The upgrader instance. + * @param array $details Array of bulk item update data. + */ + public function check_upgrader( $upgrader, $details ) { + if ( ! isset( $details['type'] ) || + 'theme' !== $details['type'] || + is_wp_error( $upgrader->skin->result ) || + ! method_exists( $upgrader, 'theme_info' ) + ) { + return; + } + + if ( 'install' === $details['action'] ) { + $theme = $upgrader->theme_info(); + if ( ! $theme instanceof \WP_Theme ) { + return; + } + $theme_info = array( + 'name' => $theme->get( 'Name' ), + 'version' => $theme->get( 'Version' ), + 'uri' => $theme->get( 'ThemeURI' ), + ); + + /** + * Signals to the sync listener that a theme was installed and a sync action + * reflecting the installation and the theme info should be sent + * + * @since 4.9.0 + * + * @param string $theme->theme_root Text domain of the theme + * @param mixed $theme_info Array of abbreviated theme info + */ + do_action( 'jetpack_installed_theme', $theme->stylesheet, $theme_info ); + } + + if ( 'update' === $details['action'] ) { + $themes = array(); + + if ( empty( $details['themes'] ) && isset( $details['theme'] ) ) { + $details['themes'] = array( $details['theme'] ); + } + + foreach ( $details['themes'] as $theme_slug ) { + $theme = wp_get_theme( $theme_slug ); + + if ( ! $theme instanceof \WP_Theme ) { + continue; + } + + $themes[ $theme_slug ] = array( + 'name' => $theme->get( 'Name' ), + 'version' => $theme->get( 'Version' ), + 'uri' => $theme->get( 'ThemeURI' ), + 'stylesheet' => $theme->stylesheet, + ); + } + + if ( empty( $themes ) ) { + return; + } + + /** + * Signals to the sync listener that one or more themes was updated and a sync action + * reflecting the update and the theme info should be sent + * + * @since 6.2.0 + * + * @param mixed $themes Array of abbreviated theme info + */ + do_action( 'jetpack_updated_themes', $themes ); + } + } + + /** + * Initialize themes action listeners for full sync. + * + * @access public + * + * @param callable $callable Action handler callable. + */ + public function init_full_sync_listeners( $callable ) { + add_action( 'jetpack_full_sync_theme_data', $callable ); + } + + /** + * Handle a theme switch. + * + * @access public + * + * @param string $new_name Name of the new theme. + * @param \WP_Theme $new_theme The new theme. + * @param \WP_Theme $old_theme The previous theme. + */ + public function sync_theme_support( $new_name, $new_theme = null, $old_theme = null ) { + $previous_theme = $this->get_theme_support_info( $old_theme ); + + /** + * Fires when the client needs to sync theme support info + * Only sends theme support attributes whitelisted in Defaults::$default_theme_support_whitelist + * + * @since 4.2.0 + * + * @param array the theme support array + * @param array the previous theme since Jetpack 6.5.0 + */ + do_action( 'jetpack_sync_current_theme_support', $this->get_theme_support_info(), $previous_theme ); + } + + /** + * Enqueue the themes actions for full sync. + * + * @access public + * + * @param array $config Full sync configuration for this sync module. + * @param int $max_items_to_enqueue Maximum number of items to enqueue. + * @param boolean $state True if full sync has finished enqueueing this module, false otherwise. + * @return array Number of actions enqueued, and next module state. + */ + public function enqueue_full_sync_actions( $config, $max_items_to_enqueue, $state ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable + /** + * Tells the client to sync all theme data to the server + * + * @since 4.2.0 + * + * @param boolean Whether to expand theme data (should always be true) + */ + do_action( 'jetpack_full_sync_theme_data', true ); + + // The number of actions enqueued, and next module state (true == done). + return array( 1, true ); + } + + /** + * Retrieve an estimated number of actions that will be enqueued. + * + * @access public + * + * @param array $config Full sync configuration for this sync module. + * @return array Number of items yet to be enqueued. + */ + public function estimate_full_sync_actions( $config ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable + return 1; + } + + /** + * Initialize the module in the sender. + * + * @access public + */ + public function init_before_send() { + add_filter( 'jetpack_sync_before_send_jetpack_full_sync_theme_data', array( $this, 'expand_theme_data' ) ); + } + + /** + * Retrieve the actions that will be sent for this module during a full sync. + * + * @access public + * + * @return array Full sync actions of this module. + */ + public function get_full_sync_actions() { + return array( 'jetpack_full_sync_theme_data' ); + } + + /** + * Expand the theme within a hook before it is serialized and sent to the server. + * + * @access public + * + * @return array Theme data. + */ + public function expand_theme_data() { + return array( $this->get_theme_support_info() ); + } + + /** + * Retrieve the name of the widget by the widget ID. + * + * @access public + * @global $wp_registered_widgets + * + * @param string $widget_id Widget ID. + * @return string Name of the widget, or null if not found. + */ + public function get_widget_name( $widget_id ) { + global $wp_registered_widgets; + return ( isset( $wp_registered_widgets[ $widget_id ] ) ? $wp_registered_widgets[ $widget_id ]['name'] : null ); + } + + /** + * Retrieve the name of the sidebar by the sidebar ID. + * + * @access public + * @global $wp_registered_sidebars + * + * @param string $sidebar_id Sidebar ID. + * @return string Name of the sidebar, or null if not found. + */ + public function get_sidebar_name( $sidebar_id ) { + global $wp_registered_sidebars; + return ( isset( $wp_registered_sidebars[ $sidebar_id ] ) ? $wp_registered_sidebars[ $sidebar_id ]['name'] : null ); + } + + /** + * Sync addition of widgets to a sidebar. + * + * @access public + * + * @param array $new_widgets New widgets. + * @param array $old_widgets Old widgets. + * @param string $sidebar Sidebar ID. + * @return array All widgets that have been moved to the sidebar. + */ + public function sync_add_widgets_to_sidebar( $new_widgets, $old_widgets, $sidebar ) { + $added_widgets = array_diff( $new_widgets, $old_widgets ); + if ( empty( $added_widgets ) ) { + return array(); + } + $moved_to_sidebar = array(); + $sidebar_name = $this->get_sidebar_name( $sidebar ); + + // Don't sync jetpack_widget_added if theme was switched. + if ( $this->is_theme_switch() ) { + return array(); + } + + foreach ( $added_widgets as $added_widget ) { + $moved_to_sidebar[] = $added_widget; + $added_widget_name = $this->get_widget_name( $added_widget ); + /** + * Helps Sync log that a widget got added + * + * @since 4.9.0 + * + * @param string $sidebar, Sidebar id got changed + * @param string $added_widget, Widget id got added + * @param string $sidebar_name, Sidebar id got changed Since 5.0.0 + * @param string $added_widget_name, Widget id got added Since 5.0.0 + */ + do_action( 'jetpack_widget_added', $sidebar, $added_widget, $sidebar_name, $added_widget_name ); + } + return $moved_to_sidebar; + } + + /** + * Sync removal of widgets from a sidebar. + * + * @access public + * + * @param array $new_widgets New widgets. + * @param array $old_widgets Old widgets. + * @param string $sidebar Sidebar ID. + * @param array $inactive_widgets Current inactive widgets. + * @return array All widgets that have been moved to inactive. + */ + public function sync_remove_widgets_from_sidebar( $new_widgets, $old_widgets, $sidebar, $inactive_widgets ) { + $removed_widgets = array_diff( $old_widgets, $new_widgets ); + + if ( empty( $removed_widgets ) ) { + return array(); + } + + $moved_to_inactive = array(); + $sidebar_name = $this->get_sidebar_name( $sidebar ); + + foreach ( $removed_widgets as $removed_widget ) { + // Lets check if we didn't move the widget to in_active_widgets. + if ( isset( $inactive_widgets ) && ! in_array( $removed_widget, $inactive_widgets, true ) ) { + $removed_widget_name = $this->get_widget_name( $removed_widget ); + /** + * Helps Sync log that a widgte got removed + * + * @since 4.9.0 + * + * @param string $sidebar, Sidebar id got changed + * @param string $removed_widget, Widget id got removed + * @param string $sidebar_name, Name of the sidebar that changed Since 5.0.0 + * @param string $removed_widget_name, Name of the widget that got removed Since 5.0.0 + */ + do_action( 'jetpack_widget_removed', $sidebar, $removed_widget, $sidebar_name, $removed_widget_name ); + } else { + $moved_to_inactive[] = $removed_widget; + } + } + return $moved_to_inactive; + + } + + /** + * Sync a reorder of widgets within a sidebar. + * + * @access public + * + * @todo Refactor serialize() to a json_encode(). + * + * @param array $new_widgets New widgets. + * @param array $old_widgets Old widgets. + * @param string $sidebar Sidebar ID. + */ + public function sync_widgets_reordered( $new_widgets, $old_widgets, $sidebar ) { + $added_widgets = array_diff( $new_widgets, $old_widgets ); + if ( ! empty( $added_widgets ) ) { + return; + } + $removed_widgets = array_diff( $old_widgets, $new_widgets ); + if ( ! empty( $removed_widgets ) ) { + return; + } + + // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize + if ( serialize( $old_widgets ) !== serialize( $new_widgets ) ) { + $sidebar_name = $this->get_sidebar_name( $sidebar ); + /** + * Helps Sync log that a sidebar id got reordered + * + * @since 4.9.0 + * + * @param string $sidebar, Sidebar id got changed + * @param string $sidebar_name, Name of the sidebar that changed Since 5.0.0 + */ + do_action( 'jetpack_widget_reordered', $sidebar, $sidebar_name ); + } + + } + + /** + * Handle the update of the sidebars and widgets mapping option. + * + * @access public + * + * @param mixed $old_value The old option value. + * @param mixed $new_value The new option value. + */ + public function sync_sidebar_widgets_actions( $old_value, $new_value ) { + // Don't really know how to deal with different array_values yet. + if ( + ( isset( $old_value['array_version'] ) && 3 !== $old_value['array_version'] ) || + ( isset( $new_value['array_version'] ) && 3 !== $new_value['array_version'] ) + ) { + return; + } + + $moved_to_inactive_ids = array(); + $moved_to_sidebar = array(); + + foreach ( $new_value as $sidebar => $new_widgets ) { + if ( in_array( $sidebar, array( 'array_version', 'wp_inactive_widgets' ), true ) ) { + continue; + } + $old_widgets = isset( $old_value[ $sidebar ] ) + ? $old_value[ $sidebar ] + : array(); + + if ( ! is_array( $new_widgets ) ) { + $new_widgets = array(); + } + + $moved_to_inactive_recently = $this->sync_remove_widgets_from_sidebar( $new_widgets, $old_widgets, $sidebar, $new_value['wp_inactive_widgets'] ); + $moved_to_inactive_ids = array_merge( $moved_to_inactive_ids, $moved_to_inactive_recently ); + + $moved_to_sidebar_recently = $this->sync_add_widgets_to_sidebar( $new_widgets, $old_widgets, $sidebar ); + $moved_to_sidebar = array_merge( $moved_to_sidebar, $moved_to_sidebar_recently ); + + $this->sync_widgets_reordered( $new_widgets, $old_widgets, $sidebar ); + + } + + // Don't sync either jetpack_widget_moved_to_inactive or jetpack_cleared_inactive_widgets if theme was switched. + if ( $this->is_theme_switch() ) { + return; + } + + // Treat inactive sidebar a bit differently. + if ( ! empty( $moved_to_inactive_ids ) ) { + $moved_to_inactive_name = array_map( array( $this, 'get_widget_name' ), $moved_to_inactive_ids ); + /** + * Helps Sync log that a widgets IDs got moved to in active + * + * @since 4.9.0 + * + * @param array $moved_to_inactive_ids, Array of widgets id that moved to inactive id got changed + * @param array $moved_to_inactive_names, Array of widgets names that moved to inactive id got changed Since 5.0.0 + */ + do_action( 'jetpack_widget_moved_to_inactive', $moved_to_inactive_ids, $moved_to_inactive_name ); + } elseif ( empty( $moved_to_sidebar ) && empty( $new_value['wp_inactive_widgets'] ) && ! empty( $old_value['wp_inactive_widgets'] ) ) { + /** + * Helps Sync log that a got cleared from inactive. + * + * @since 4.9.0 + */ + do_action( 'jetpack_cleared_inactive_widgets' ); + } + } + + /** + * Retrieve the theme data for the current or a specific theme. + * + * @access private + * + * @param \WP_Theme $theme Theme object. Optional, will default to the current theme. + * @return array Theme data. + */ + private function get_theme_support_info( $theme = null ) { + global $_wp_theme_features; + + $theme_support = array(); + + // We are trying to get the current theme info. + if ( null === $theme ) { + $theme = wp_get_theme(); + + foreach ( Defaults::$default_theme_support_whitelist as $theme_feature ) { + $has_support = current_theme_supports( $theme_feature ); + if ( $has_support ) { + $theme_support[ $theme_feature ] = $_wp_theme_features[ $theme_feature ]; + } + } + } + + $theme_support['name'] = $theme->get( 'Name' ); + $theme_support['version'] = $theme->get( 'Version' ); + $theme_support['slug'] = $theme->get_stylesheet(); + $theme_support['uri'] = $theme->get( 'ThemeURI' ); + + return $theme_support; + } + + /** + * Whether we've deleted a theme in the current request. + * + * @access private + * + * @return boolean True if this is a theme deletion request, false otherwise. + */ + private function get_delete_theme_call() { + // Intentional usage of `debug_backtrace()` for production needs. + // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace + $backtrace = debug_backtrace(); + $delete_theme_call = null; + foreach ( $backtrace as $call ) { + if ( isset( $call['function'] ) && 'delete_theme' === $call['function'] ) { + $delete_theme_call = $call; + break; + } + } + return $delete_theme_call; + } + + /** + * Whether we've switched to another theme in the current request. + * + * @access private + * + * @return boolean True if this is a theme switch request, false otherwise. + */ + private function is_theme_switch() { + return did_action( 'after_switch_theme' ); + } +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-updates.php b/plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-updates.php new file mode 100644 index 00000000..d99c9c57 --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-updates.php @@ -0,0 +1,496 @@ +<?php +/** + * Updates sync module. + * + * @package automattic/jetpack-sync + */ + +namespace Automattic\Jetpack\Sync\Modules; + +use Automattic\Jetpack\Constants as Jetpack_Constants; + +/** + * Class to handle sync for updates. + */ +class Updates extends Module { + /** + * Name of the updates checksum option. + * + * @var string + */ + const UPDATES_CHECKSUM_OPTION_NAME = 'jetpack_updates_sync_checksum'; + + /** + * WordPress Version. + * + * @access private + * + * @var string + */ + private $old_wp_version = null; + + /** + * The current updates. + * + * @access private + * + * @var array + */ + private $updates = array(); + + /** + * Set module defaults. + * + * @access public + */ + public function set_defaults() { + $this->updates = array(); + } + + /** + * Sync module name. + * + * @access public + * + * @return string + */ + public function name() { + return 'updates'; + } + + /** + * Initialize updates action listeners. + * + * @access public + * + * @param callable $callable Action handler callable. + */ + public function init_listeners( $callable ) { + global $wp_version; + $this->old_wp_version = $wp_version; + add_action( 'set_site_transient_update_plugins', array( $this, 'validate_update_change' ), 10, 3 ); + add_action( 'set_site_transient_update_themes', array( $this, 'validate_update_change' ), 10, 3 ); + add_action( 'set_site_transient_update_core', array( $this, 'validate_update_change' ), 10, 3 ); + + add_action( 'jetpack_update_plugins_change', $callable ); + add_action( 'jetpack_update_themes_change', $callable ); + add_action( 'jetpack_update_core_change', $callable ); + + add_filter( + 'jetpack_sync_before_enqueue_jetpack_update_plugins_change', + array( + $this, + 'filter_update_keys', + ), + 10, + 2 + ); + add_filter( + 'jetpack_sync_before_enqueue_upgrader_process_complete', + array( + $this, + 'filter_upgrader_process_complete', + ), + 10, + 2 + ); + + add_action( 'automatic_updates_complete', $callable ); + + if ( is_multisite() ) { + add_filter( 'pre_update_site_option_wpmu_upgrade_site', array( $this, 'update_core_network_event' ), 10, 2 ); + add_action( 'jetpack_sync_core_update_network', $callable, 10, 3 ); + } + + // Send data when update completes. + add_action( '_core_updated_successfully', array( $this, 'update_core' ) ); + add_action( 'jetpack_sync_core_reinstalled_successfully', $callable ); + add_action( 'jetpack_sync_core_autoupdated_successfully', $callable, 10, 2 ); + add_action( 'jetpack_sync_core_updated_successfully', $callable, 10, 2 ); + + } + + /** + * Initialize updates action listeners for full sync. + * + * @access public + * + * @param callable $callable Action handler callable. + */ + public function init_full_sync_listeners( $callable ) { + add_action( 'jetpack_full_sync_updates', $callable ); + } + + /** + * Initialize the module in the sender. + * + * @access public + */ + public function init_before_send() { + add_filter( 'jetpack_sync_before_send_jetpack_full_sync_updates', array( $this, 'expand_updates' ) ); + add_filter( 'jetpack_sync_before_send_jetpack_update_themes_change', array( $this, 'expand_themes' ) ); + } + + /** + * Handle a core network update. + * + * @access public + * + * @param int $wp_db_version Current version of the WordPress database. + * @param int $old_wp_db_version Old version of the WordPress database. + * @return int Current version of the WordPress database. + */ + public function update_core_network_event( $wp_db_version, $old_wp_db_version ) { + global $wp_version; + /** + * Sync event for when core wp network updates to a new db version + * + * @since 5.0.0 + * + * @param int $wp_db_version the latest wp_db_version + * @param int $old_wp_db_version previous wp_db_version + * @param string $wp_version the latest wp_version + */ + do_action( 'jetpack_sync_core_update_network', $wp_db_version, $old_wp_db_version, $wp_version ); + return $wp_db_version; + } + + /** + * Handle a core update. + * + * @access public + * + * @todo Implement nonce or refactor to use `admin_post_{$action}` hooks instead. + * + * @param string $new_wp_version The new WP core version. + */ + public function update_core( $new_wp_version ) { + global $pagenow; + + // // phpcs:ignore WordPress.Security.NonceVerification.Recommended + if ( isset( $_GET['action'] ) && 'do-core-reinstall' === $_GET['action'] ) { + /** + * Sync event that fires when core reinstall was successful + * + * @since 5.0.0 + * + * @param string $new_wp_version the updated WordPress version + */ + do_action( 'jetpack_sync_core_reinstalled_successfully', $new_wp_version ); + return; + } + + // Core was autoupdated. + if ( + 'update-core.php' !== $pagenow && + ! Jetpack_Constants::is_true( 'REST_API_REQUEST' ) // WP.com rest api calls should never be marked as a core autoupdate. + ) { + /** + * Sync event that fires when core autoupdate was successful + * + * @since 5.0.0 + * + * @param string $new_wp_version the updated WordPress version + * @param string $old_wp_version the previous WordPress version + */ + do_action( 'jetpack_sync_core_autoupdated_successfully', $new_wp_version, $this->old_wp_version ); + return; + } + /** + * Sync event that fires when core update was successful + * + * @since 5.0.0 + * + * @param string $new_wp_version the updated WordPress version + * @param string $old_wp_version the previous WordPress version + */ + do_action( 'jetpack_sync_core_updated_successfully', $new_wp_version, $this->old_wp_version ); + } + + /** + * Retrieve the checksum for an update. + * + * @access public + * + * @param object $update The update object. + * @param string $transient The transient we're retrieving a checksum for. + * @return int The checksum. + */ + public function get_update_checksum( $update, $transient ) { + $updates = array(); + $no_updated = array(); + switch ( $transient ) { + case 'update_plugins': + if ( ! empty( $update->response ) && is_array( $update->response ) ) { + foreach ( $update->response as $plugin_slug => $response ) { + if ( ! empty( $plugin_slug ) && isset( $response->new_version ) ) { + $updates[] = array( $plugin_slug => $response->new_version ); + } + } + } + if ( ! empty( $update->no_update ) ) { + $no_updated = array_keys( $update->no_update ); + } + + if ( ! isset( $no_updated['jetpack/jetpack.php'] ) && isset( $updates['jetpack/jetpack.php'] ) ) { + return false; + } + + break; + case 'update_themes': + if ( ! empty( $update->response ) && is_array( $update->response ) ) { + foreach ( $update->response as $theme_slug => $response ) { + if ( ! empty( $theme_slug ) && isset( $response['new_version'] ) ) { + $updates[] = array( $theme_slug => $response['new_version'] ); + } + } + } + + if ( ! empty( $update->checked ) ) { + $no_updated = $update->checked; + } + + break; + case 'update_core': + if ( ! empty( $update->updates ) && is_array( $update->updates ) ) { + foreach ( $update->updates as $response ) { + if ( ! empty( $response->response ) && 'latest' === $response->response ) { + continue; + } + if ( ! empty( $response->response ) && isset( $response->packages->full ) ) { + $updates[] = array( $response->response => $response->packages->full ); + } + } + } + + if ( ! empty( $update->version_checked ) ) { + $no_updated = $update->version_checked; + } + + if ( empty( $updates ) ) { + return false; + } + break; + + } + if ( empty( $updates ) && empty( $no_updated ) ) { + return false; + } + return $this->get_check_sum( array( $no_updated, $updates ) ); + } + + /** + * Validate a change coming from an update before sending for sync. + * + * @access public + * + * @param mixed $value Site transient value. + * @param int $expiration Time until transient expiration in seconds. + * @param string $transient Transient name. + */ + public function validate_update_change( $value, $expiration, $transient ) { + $new_checksum = $this->get_update_checksum( $value, $transient ); + + if ( false === $new_checksum ) { + return; + } + + $checksums = get_option( self::UPDATES_CHECKSUM_OPTION_NAME, array() ); + + if ( isset( $checksums[ $transient ] ) && $checksums[ $transient ] === $new_checksum ) { + return; + } + + $checksums[ $transient ] = $new_checksum; + + update_option( self::UPDATES_CHECKSUM_OPTION_NAME, $checksums ); + if ( 'update_core' === $transient ) { + /** + * Trigger a change to core update that we want to sync. + * + * @since 5.1.0 + * + * @param array $value Contains info that tells us what needs updating. + */ + do_action( 'jetpack_update_core_change', $value ); + return; + } + if ( empty( $this->updates ) ) { + // Lets add the shutdown method once and only when the updates move from empty to filled with something. + add_action( 'shutdown', array( $this, 'sync_last_event' ), 9 ); + } + if ( ! isset( $this->updates[ $transient ] ) ) { + $this->updates[ $transient ] = array(); + } + $this->updates[ $transient ][] = $value; + } + + /** + * Sync the last update only. + * + * @access public + */ + public function sync_last_event() { + foreach ( $this->updates as $transient => $values ) { + $value = end( $values ); // Only send over the last value. + /** + * Trigger a change to a specific update that we want to sync. + * Triggers one of the following actions: + * - jetpack_{$transient}_change + * - jetpack_update_plugins_change + * - jetpack_update_themes_change + * + * @since 5.1.0 + * + * @param array $value Contains info that tells us what needs updating. + */ + do_action( "jetpack_{$transient}_change", $value ); + } + + } + + /** + * Enqueue the updates actions for full sync. + * + * @access public + * + * @param array $config Full sync configuration for this sync module. + * @param int $max_items_to_enqueue Maximum number of items to enqueue. + * @param boolean $state True if full sync has finished enqueueing this module, false otherwise. + * @return array Number of actions enqueued, and next module state. + */ + public function enqueue_full_sync_actions( $config, $max_items_to_enqueue, $state ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable + /** + * Tells the client to sync all updates to the server + * + * @since 4.2.0 + * + * @param boolean Whether to expand updates (should always be true) + */ + do_action( 'jetpack_full_sync_updates', true ); + + // The number of actions enqueued, and next module state (true == done). + return array( 1, true ); + } + + /** + * Retrieve an estimated number of actions that will be enqueued. + * + * @access public + * + * @param array $config Full sync configuration for this sync module. + * @return array Number of items yet to be enqueued. + */ + public function estimate_full_sync_actions( $config ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable + return 1; + } + + /** + * Retrieve the actions that will be sent for this module during a full sync. + * + * @access public + * + * @return array Full sync actions of this module. + */ + public function get_full_sync_actions() { + return array( 'jetpack_full_sync_updates' ); + } + + /** + * Retrieve all updates that we're interested in. + * + * @access public + * + * @return array All updates. + */ + public function get_all_updates() { + return array( + 'core' => get_site_transient( 'update_core' ), + 'plugins' => get_site_transient( 'update_plugins' ), + 'themes' => get_site_transient( 'update_themes' ), + ); + } + + /** + * Remove unnecessary keys from synced updates data. + * + * @access public + * + * @param array $args Hook arguments. + * @return array $args Hook arguments. + */ + public function filter_update_keys( $args ) { + $updates = $args[0]; + + if ( isset( $updates->no_update ) ) { + unset( $updates->no_update ); + } + + return $args; + } + + /** + * Filter out upgrader object from the completed upgrader action args. + * + * @access public + * + * @param array $args Hook arguments. + * @return array $args Filtered hook arguments. + */ + public function filter_upgrader_process_complete( $args ) { + array_shift( $args ); + + return $args; + } + + /** + * Expand the updates within a hook before they are serialized and sent to the server. + * + * @access public + * + * @param array $args The hook parameters. + * @return array $args The hook parameters. + */ + public function expand_updates( $args ) { + if ( $args[0] ) { + return $this->get_all_updates(); + } + + return $args; + } + + /** + * Expand the themes within a hook before they are serialized and sent to the server. + * + * @access public + * + * @param array $args The hook parameters. + * @return array $args The hook parameters. + */ + public function expand_themes( $args ) { + if ( ! isset( $args[0], $args[0]->response ) ) { + return $args; + } + if ( ! is_array( $args[0]->response ) ) { + // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error + trigger_error( 'Warning: Not an Array as expected but -> ' . wp_json_encode( $args[0]->response ) . ' instead', E_USER_WARNING ); + return $args; + } + foreach ( $args[0]->response as $stylesheet => &$theme_data ) { + $theme = wp_get_theme( $stylesheet ); + $theme_data['name'] = $theme->name; + } + return $args; + } + + /** + * Perform module cleanup. + * Deletes any transients and options that this module uses. + * Usually triggered when uninstalling the plugin. + * + * @access public + */ + public function reset_data() { + delete_option( self::UPDATES_CHECKSUM_OPTION_NAME ); + } +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-users.php b/plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-users.php new file mode 100644 index 00000000..21974a5b --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-users.php @@ -0,0 +1,854 @@ +<?php +/** + * Users sync module. + * + * @package automattic/jetpack-sync + */ + +namespace Automattic\Jetpack\Sync\Modules; + +use Automattic\Jetpack\Constants as Jetpack_Constants; +use Automattic\Jetpack\Sync\Defaults; + +/** + * Class to handle sync for users. + */ +class Users extends Module { + /** + * Maximum number of users to sync initially. + * + * @var int + */ + const MAX_INITIAL_SYNC_USERS = 100; + + /** + * User flags we care about. + * + * @access protected + * + * @var array + */ + protected $flags = array(); + + /** + * Sync module name. + * + * @access public + * + * @return string + */ + public function name() { + return 'users'; + } + + /** + * The table in the database. + * + * @access public + * + * @return string + */ + public function table_name() { + return 'users'; + } + + /** + * Retrieve a user by its ID. + * This is here to support the backfill API. + * + * @access public + * + * @param string $object_type Type of the sync object. + * @param int $id ID of the sync object. + * @return \WP_User|bool Filtered \WP_User object, or false if the object is not a user. + */ + public function get_object_by_id( $object_type, $id ) { + if ( 'user' === $object_type ) { + $user = get_user_by( 'id', intval( $id ) ); + if ( $user ) { + return $this->sanitize_user_and_expand( $user ); + } + } + + return false; + } + + /** + * Initialize users action listeners. + * + * @access public + * + * @param callable $callable Action handler callable. + */ + public function init_listeners( $callable ) { + // Users. + add_action( 'user_register', array( $this, 'user_register_handler' ) ); + add_action( 'profile_update', array( $this, 'save_user_handler' ), 10, 2 ); + + add_action( 'add_user_to_blog', array( $this, 'add_user_to_blog_handler' ) ); + add_action( 'jetpack_sync_add_user', $callable, 10, 2 ); + + add_action( 'jetpack_sync_register_user', $callable, 10, 2 ); + add_action( 'jetpack_sync_save_user', $callable, 10, 2 ); + + add_action( 'jetpack_sync_user_locale', $callable, 10, 2 ); + add_action( 'jetpack_sync_user_locale_delete', $callable, 10, 1 ); + + add_action( 'deleted_user', array( $this, 'deleted_user_handler' ), 10, 2 ); + add_action( 'jetpack_deleted_user', $callable, 10, 3 ); + add_action( 'remove_user_from_blog', array( $this, 'remove_user_from_blog_handler' ), 10, 2 ); + add_action( 'jetpack_removed_user_from_blog', $callable, 10, 2 ); + + // User roles. + add_action( 'add_user_role', array( $this, 'save_user_role_handler' ), 10, 2 ); + add_action( 'set_user_role', array( $this, 'save_user_role_handler' ), 10, 3 ); + add_action( 'remove_user_role', array( $this, 'save_user_role_handler' ), 10, 2 ); + + // User capabilities. + add_action( 'added_user_meta', array( $this, 'maybe_save_user_meta' ), 10, 4 ); + add_action( 'updated_user_meta', array( $this, 'maybe_save_user_meta' ), 10, 4 ); + add_action( 'deleted_user_meta', array( $this, 'maybe_save_user_meta' ), 10, 4 ); + + // User authentication. + add_filter( 'authenticate', array( $this, 'authenticate_handler' ), 1000, 3 ); + add_action( 'wp_login', array( $this, 'wp_login_handler' ), 10, 2 ); + + add_action( 'jetpack_wp_login', $callable, 10, 3 ); + + add_action( 'wp_logout', $callable, 10, 0 ); + add_action( 'wp_masterbar_logout', $callable, 10, 0 ); + + // Add on init. + add_filter( 'jetpack_sync_before_enqueue_jetpack_sync_add_user', array( $this, 'expand_action' ) ); + add_filter( 'jetpack_sync_before_enqueue_jetpack_sync_register_user', array( $this, 'expand_action' ) ); + add_filter( 'jetpack_sync_before_enqueue_jetpack_sync_save_user', array( $this, 'expand_action' ) ); + } + + /** + * Initialize users action listeners for full sync. + * + * @access public + * + * @param callable $callable Action handler callable. + */ + public function init_full_sync_listeners( $callable ) { + add_action( 'jetpack_full_sync_users', $callable ); + } + + /** + * Initialize the module in the sender. + * + * @access public + */ + public function init_before_send() { + add_filter( 'jetpack_sync_before_send_jetpack_wp_login', array( $this, 'expand_login_username' ), 10, 1 ); + add_filter( 'jetpack_sync_before_send_wp_logout', array( $this, 'expand_logout_username' ), 10, 2 ); + + // Full sync. + add_filter( 'jetpack_sync_before_send_jetpack_full_sync_users', array( $this, 'expand_users' ) ); + } + + /** + * Retrieve a user by a user ID or object. + * + * @access private + * + * @param mixed $user User object or ID. + * @return \WP_User User object, or `null` if user invalid/not found. + */ + private function get_user( $user ) { + if ( is_numeric( $user ) ) { + $user = get_user_by( 'id', $user ); + } + if ( $user instanceof \WP_User ) { + return $user; + } + return null; + } + + /** + * Sanitize a user object. + * Removes the password from the user object because we don't want to sync it. + * + * @access public + * + * @todo Refactor `serialize`/`unserialize` to `wp_json_encode`/`wp_json_decode`. + * + * @param \WP_User $user User object. + * @return \WP_User Sanitized user object. + */ + public function sanitize_user( $user ) { + $user = $this->get_user( $user ); + // This creates a new user object and stops the passing of the object by reference. + // // phpcs:disable WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize, WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize + $user = unserialize( serialize( $user ) ); + + if ( is_object( $user ) && is_object( $user->data ) ) { + unset( $user->data->user_pass ); + } + return $user; + } + + /** + * Expand a particular user. + * + * @access public + * + * @param \WP_User $user User object. + * @return \WP_User Expanded user object. + */ + public function expand_user( $user ) { + if ( ! is_object( $user ) ) { + return null; + } + $user->allowed_mime_types = get_allowed_mime_types( $user ); + $user->allcaps = $this->get_real_user_capabilities( $user ); + + // Only set the user locale if it is different from the site locale. + if ( get_locale() !== get_user_locale( $user->ID ) ) { + $user->locale = get_user_locale( $user->ID ); + } + + return $user; + } + + /** + * Retrieve capabilities we care about for a particular user. + * + * @access public + * + * @param \WP_User $user User object. + * @return array User capabilities. + */ + public function get_real_user_capabilities( $user ) { + $user_capabilities = array(); + if ( is_wp_error( $user ) ) { + return $user_capabilities; + } + foreach ( Defaults::get_capabilities_whitelist() as $capability ) { + if ( user_can( $user, $capability ) ) { + $user_capabilities[ $capability ] = true; + } + } + return $user_capabilities; + } + + /** + * Retrieve, expand and sanitize a user. + * Can be directly used in the sync user action handlers. + * + * @access public + * + * @param mixed $user User ID or user object. + * @return \WP_User Expanded and sanitized user object. + */ + public function sanitize_user_and_expand( $user ) { + $user = $this->get_user( $user ); + $user = $this->expand_user( $user ); + return $this->sanitize_user( $user ); + } + + /** + * Expand the user within a hook before it is serialized and sent to the server. + * + * @access public + * + * @param array $args The hook arguments. + * @return array $args The hook arguments. + */ + public function expand_action( $args ) { + // The first argument is always the user. + list( $user ) = $args; + if ( $user ) { + $args[0] = $this->sanitize_user_and_expand( $user ); + return $args; + } + + return false; + } + + /** + * Expand the user username at login before being sent to the server. + * + * @access public + * + * @param array $args The hook arguments. + * @return array $args Expanded hook arguments. + */ + public function expand_login_username( $args ) { + list( $login, $user, $flags ) = $args; + $user = $this->sanitize_user( $user ); + + return array( $login, $user, $flags ); + } + + /** + * Expand the user username at logout before being sent to the server. + * + * @access public + * + * @param array $args The hook arguments. + * @param int $user_id ID of the user. + * @return array $args Expanded hook arguments. + */ + public function expand_logout_username( $args, $user_id ) { + $user = get_userdata( $user_id ); + $user = $this->sanitize_user( $user ); + + $login = ''; + if ( is_object( $user ) && is_object( $user->data ) ) { + $login = $user->data->user_login; + } + + // If we don't have a user here lets not send anything. + if ( empty( $login ) ) { + return false; + } + + return array( $login, $user ); + } + + /** + * Additional processing is needed for wp_login so we introduce this wrapper handler. + * + * @access public + * + * @param string $user_login The user login. + * @param \WP_User $user The user object. + */ + public function wp_login_handler( $user_login, $user ) { + /** + * Fires when a user is logged into a site. + * + * @since 7.2.0 + * + * @param int $user_id The user ID. + * @param \WP_User $user The User Object of the user that currently logged in. + * @param array $params Any Flags that have been added during login. + */ + do_action( 'jetpack_wp_login', $user->ID, $user, $this->get_flags( $user->ID ) ); + $this->clear_flags( $user->ID ); + } + + /** + * A hook for the authenticate event that checks the password strength. + * + * @access public + * + * @param \WP_Error|\WP_User $user The user object, or an error. + * @param string $username The username. + * @param string $password The password used to authenticate. + * @return \WP_Error|\WP_User the same object that was passed into the function. + */ + public function authenticate_handler( $user, $username, $password ) { + // In case of cookie authentication we don't do anything here. + if ( empty( $password ) ) { + return $user; + } + + // We are only interested in successful authentication events. + if ( is_wp_error( $user ) || ! ( $user instanceof \WP_User ) ) { + return $user; + } + + jetpack_require_lib( 'class.jetpack-password-checker' ); + $password_checker = new \Jetpack_Password_Checker( $user->ID ); + + $test_results = $password_checker->test( $password, true ); + + // If the password passes tests, we don't do anything. + if ( empty( $test_results['test_results']['failed'] ) ) { + return $user; + } + + $this->add_flags( + $user->ID, + array( + 'warning' => 'The password failed at least one strength test.', + 'failures' => $test_results['test_results']['failed'], + ) + ); + + return $user; + } + + /** + * Handler for after the user is deleted. + * + * @access public + * + * @param int $deleted_user_id ID of the deleted user. + * @param int $reassigned_user_id ID of the user the deleted user's posts are reassigned to (if any). + */ + public function deleted_user_handler( $deleted_user_id, $reassigned_user_id = '' ) { + $is_multisite = is_multisite(); + /** + * Fires when a user is deleted on a site + * + * @since 5.4.0 + * + * @param int $deleted_user_id - ID of the deleted user. + * @param int $reassigned_user_id - ID of the user the deleted user's posts are reassigned to (if any). + * @param bool $is_multisite - Whether this site is a multisite installation. + */ + do_action( 'jetpack_deleted_user', $deleted_user_id, $reassigned_user_id, $is_multisite ); + } + + /** + * Handler for user registration. + * + * @access public + * + * @param int $user_id ID of the deleted user. + */ + public function user_register_handler( $user_id ) { + // Ensure we only sync users who are members of the current blog. + if ( ! is_user_member_of_blog( $user_id, get_current_blog_id() ) ) { + return; + } + + if ( Jetpack_Constants::is_true( 'JETPACK_INVITE_ACCEPTED' ) ) { + $this->add_flags( $user_id, array( 'invitation_accepted' => true ) ); + } + /** + * Fires when a new user is registered on a site + * + * @since 4.9.0 + * + * @param object The WP_User object + */ + do_action( 'jetpack_sync_register_user', $user_id, $this->get_flags( $user_id ) ); + $this->clear_flags( $user_id ); + + } + + /** + * Handler for user addition to the current blog. + * + * @access public + * + * @param int $user_id ID of the user. + */ + public function add_user_to_blog_handler( $user_id ) { + // Ensure we only sync users who are members of the current blog. + if ( ! is_user_member_of_blog( $user_id, get_current_blog_id() ) ) { + return; + } + + if ( Jetpack_Constants::is_true( 'JETPACK_INVITE_ACCEPTED' ) ) { + $this->add_flags( $user_id, array( 'invitation_accepted' => true ) ); + } + + /** + * Fires when a user is added on a site + * + * @since 4.9.0 + * + * @param object The WP_User object + */ + do_action( 'jetpack_sync_add_user', $user_id, $this->get_flags( $user_id ) ); + $this->clear_flags( $user_id ); + } + + /** + * Handler for user save. + * + * @access public + * + * @param int $user_id ID of the user. + * @param \WP_User $old_user_data User object before the changes. + */ + public function save_user_handler( $user_id, $old_user_data = null ) { + // Ensure we only sync users who are members of the current blog. + if ( ! is_user_member_of_blog( $user_id, get_current_blog_id() ) ) { + return; + } + + $user = get_user_by( 'id', $user_id ); + + // Older versions of WP don't pass the old_user_data in ->data. + if ( isset( $old_user_data->data ) ) { + $old_user = $old_user_data->data; + } else { + $old_user = $old_user_data; + } + + if ( null !== $old_user && $user->user_pass !== $old_user->user_pass ) { + $this->flags[ $user_id ]['password_changed'] = true; + } + if ( null !== $old_user && $user->data->user_email !== $old_user->user_email ) { + /** + * The '_new_email' user meta is deleted right after the call to wp_update_user + * that got us to this point so if it's still set then this was a user confirming + * their new email address. + */ + if ( 1 === intval( get_user_meta( $user->ID, '_new_email', true ) ) ) { + $this->flags[ $user_id ]['email_changed'] = true; + } + } + + /** + * Fires when the client needs to sync an updated user. + * + * @since 4.2.0 + * + * @param \WP_User The WP_User object + * @param array State - New since 5.8.0 + */ + do_action( 'jetpack_sync_save_user', $user_id, $this->get_flags( $user_id ) ); + $this->clear_flags( $user_id ); + } + + /** + * Handler for user role change. + * + * @access public + * + * @param int $user_id ID of the user. + * @param string $role New user role. + * @param array $old_roles Previous user roles. + */ + public function save_user_role_handler( $user_id, $role, $old_roles = null ) { + $this->add_flags( + $user_id, + array( + 'role_changed' => true, + 'previous_role' => $old_roles, + ) + ); + + // The jetpack_sync_register_user payload is identical to jetpack_sync_save_user, don't send both. + if ( $this->is_create_user() || $this->is_add_user_to_blog() ) { + return; + } + /** + * This action is documented already in this file + */ + do_action( 'jetpack_sync_save_user', $user_id, $this->get_flags( $user_id ) ); + $this->clear_flags( $user_id ); + } + + /** + * Retrieve current flags for a particular user. + * + * @access public + * + * @param int $user_id ID of the user. + * @return array Current flags of the user. + */ + public function get_flags( $user_id ) { + if ( isset( $this->flags[ $user_id ] ) ) { + return $this->flags[ $user_id ]; + } + return array(); + } + + /** + * Clear the flags of a particular user. + * + * @access public + * + * @param int $user_id ID of the user. + */ + public function clear_flags( $user_id ) { + if ( isset( $this->flags[ $user_id ] ) ) { + unset( $this->flags[ $user_id ] ); + } + } + + /** + * Add flags to a particular user. + * + * @access public + * + * @param int $user_id ID of the user. + * @param array $flags New flags to add for the user. + */ + public function add_flags( $user_id, $flags ) { + $this->flags[ $user_id ] = wp_parse_args( $flags, $this->get_flags( $user_id ) ); + } + + /** + * Save the user meta, if we're interested in it. + * Also uses the time to add flags for the user. + * + * @access public + * + * @param int $meta_id ID of the meta object. + * @param int $user_id ID of the user. + * @param string $meta_key Meta key. + * @param mixed $value Meta value. + */ + public function maybe_save_user_meta( $meta_id, $user_id, $meta_key, $value ) { + if ( 'locale' === $meta_key ) { + $this->add_flags( $user_id, array( 'locale_changed' => true ) ); + } + + $user = get_user_by( 'id', $user_id ); + if ( isset( $user->cap_key ) && $meta_key === $user->cap_key ) { + $this->add_flags( $user_id, array( 'capabilities_changed' => true ) ); + } + + if ( $this->is_create_user() || $this->is_add_user_to_blog() || $this->is_delete_user() ) { + return; + } + + if ( isset( $this->flags[ $user_id ] ) ) { + /** + * This action is documented already in this file + */ + do_action( 'jetpack_sync_save_user', $user_id, $this->get_flags( $user_id ) ); + } + } + + /** + * Enqueue the users actions for full sync. + * + * @access public + * + * @param array $config Full sync configuration for this sync module. + * @param int $max_items_to_enqueue Maximum number of items to enqueue. + * @param boolean $state True if full sync has finished enqueueing this module, false otherwise. + * @return array Number of actions enqueued, and next module state. + */ + public function enqueue_full_sync_actions( $config, $max_items_to_enqueue, $state ) { + global $wpdb; + + return $this->enqueue_all_ids_as_action( 'jetpack_full_sync_users', $wpdb->usermeta, 'user_id', $this->get_where_sql( $config ), $max_items_to_enqueue, $state ); + } + + /** + * Retrieve an estimated number of actions that will be enqueued. + * + * @access public + * + * @todo Refactor to prepare the SQL query before executing it. + * + * @param array $config Full sync configuration for this sync module. + * @return array Number of items yet to be enqueued. + */ + public function estimate_full_sync_actions( $config ) { + global $wpdb; + + $query = "SELECT count(*) FROM $wpdb->usermeta"; + + $where_sql = $this->get_where_sql( $config ); + if ( $where_sql ) { + $query .= ' WHERE ' . $where_sql; + } + + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + $count = $wpdb->get_var( $query ); + + return (int) ceil( $count / self::ARRAY_CHUNK_SIZE ); + } + + /** + * Retrieve the WHERE SQL clause based on the module config. + * + * @access public + * + * @param array $config Full sync configuration for this sync module. + * @return string WHERE SQL clause, or `null` if no comments are specified in the module config. + */ + public function get_where_sql( $config ) { + global $wpdb; + + $query = "meta_key = '{$wpdb->prefix}capabilities'"; + + // The $config variable is a list of user IDs to sync. + if ( is_array( $config ) ) { + $query .= ' AND user_id IN (' . implode( ',', array_map( 'intval', $config ) ) . ')'; + } + + return $query; + } + + /** + * Retrieve the actions that will be sent for this module during a full sync. + * + * @access public + * + * @return array Full sync actions of this module. + */ + public function get_full_sync_actions() { + return array( 'jetpack_full_sync_users' ); + } + + /** + * Retrieve initial sync user config. + * + * @access public + * + * @todo Refactor the SQL query to call $wpdb->prepare() before execution. + * + * @return array|boolean IDs of users to initially sync, or false if tbe number of users exceed the maximum. + */ + public function get_initial_sync_user_config() { + global $wpdb; + + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + $user_ids = $wpdb->get_col( "SELECT user_id FROM $wpdb->usermeta WHERE meta_key = '{$wpdb->prefix}user_level' AND meta_value > 0 LIMIT " . ( self::MAX_INITIAL_SYNC_USERS + 1 ) ); + + if ( count( $user_ids ) <= self::MAX_INITIAL_SYNC_USERS ) { + return $user_ids; + } else { + return false; + } + } + + /** + * Expand the users within a hook before they are serialized and sent to the server. + * + * @access public + * + * @param array $args The hook arguments. + * @return array $args The hook arguments. + */ + public function expand_users( $args ) { + list( $user_ids, $previous_end ) = $args; + + return array( + 'users' => array_map( + array( $this, 'sanitize_user_and_expand' ), + get_users( + array( + 'include' => $user_ids, + 'orderby' => 'ID', + 'order' => 'DESC', + ) + ) + ), + 'previous_end' => $previous_end, + ); + } + + /** + * Handler for user removal from a particular blog. + * + * @access public + * + * @param int $user_id ID of the user. + * @param int $blog_id ID of the blog. + */ + public function remove_user_from_blog_handler( $user_id, $blog_id ) { + // User is removed on add, see https://github.com/WordPress/WordPress/blob/0401cee8b36df3def8e807dd766adc02b359dfaf/wp-includes/ms-functions.php#L2114. + if ( $this->is_add_new_user_to_blog() ) { + return; + } + + $reassigned_user_id = $this->get_reassigned_network_user_id(); + + // Note that we are in the context of the blog the user is removed from, see https://github.com/WordPress/WordPress/blob/473e1ba73bc5c18c72d7f288447503713d518790/wp-includes/ms-functions.php#L233. + /** + * Fires when a user is removed from a blog on a multisite installation + * + * @since 5.4.0 + * + * @param int $user_id - ID of the removed user + * @param int $reassigned_user_id - ID of the user the removed user's posts are reassigned to (if any). + */ + do_action( 'jetpack_removed_user_from_blog', $user_id, $reassigned_user_id ); + } + + /** + * Whether we're adding a new user to a blog in this request. + * + * @access protected + * + * @return boolean + */ + protected function is_add_new_user_to_blog() { + return $this->is_function_in_backtrace( 'add_new_user_to_blog' ); + } + + /** + * Whether we're adding an existing user to a blog in this request. + * + * @access protected + * + * @return boolean + */ + protected function is_add_user_to_blog() { + return $this->is_function_in_backtrace( 'add_user_to_blog' ); + } + + /** + * Whether we're removing a user from a blog in this request. + * + * @access protected + * + * @return boolean + */ + protected function is_delete_user() { + return $this->is_function_in_backtrace( array( 'wp_delete_user', 'remove_user_from_blog' ) ); + } + + /** + * Whether we're creating a user or adding a new user to a blog. + * + * @access protected + * + * @return boolean + */ + protected function is_create_user() { + $functions = array( + 'add_new_user_to_blog', // Used to suppress jetpack_sync_save_user in save_user_cap_handler when user registered on multi site. + 'wp_create_user', // Used to suppress jetpack_sync_save_user in save_user_role_handler when user registered on multi site. + 'wp_insert_user', // Used to suppress jetpack_sync_save_user in save_user_cap_handler and save_user_role_handler when user registered on single site. + ); + + return $this->is_function_in_backtrace( $functions ); + } + + /** + * Retrieve the ID of the user the removed user's posts are reassigned to (if any). + * + * @return int ID of the user that got reassigned as the author of the posts. + */ + protected function get_reassigned_network_user_id() { + $backtrace = debug_backtrace( false ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace + foreach ( $backtrace as $call ) { + if ( + 'remove_user_from_blog' === $call['function'] && + 3 === count( $call['args'] ) + ) { + return $call['args'][2]; + } + } + + return false; + } + + /** + * Checks if one or more function names is in debug_backtrace. + * + * @access protected + * + * @param array|string $names Mixed string name of function or array of string names of functions. + * @return bool + */ + protected function is_function_in_backtrace( $names ) { + $backtrace = debug_backtrace( false ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace + if ( ! is_array( $names ) ) { + $names = array( $names ); + } + $names_as_keys = array_flip( $names ); + + // Do check in constant O(1) time for PHP5.5+. + if ( function_exists( 'array_column' ) ) { + $backtrace_functions = array_column( $backtrace, 'function' ); // phpcs:ignore PHPCompatibility.FunctionUse.NewFunctions.array_columnFound + $backtrace_functions_as_keys = array_flip( $backtrace_functions ); + $intersection = array_intersect_key( $backtrace_functions_as_keys, $names_as_keys ); + return ! empty( $intersection ); + } + + // Do check in linear O(n) time for < PHP5.5 ( using isset at least prevents O(n^2) ). + foreach ( $backtrace as $call ) { + if ( isset( $names_as_keys[ $call['function'] ] ) ) { + return true; + } + } + return false; + } +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-woocommerce.php b/plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-woocommerce.php new file mode 100644 index 00000000..1c336342 --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-woocommerce.php @@ -0,0 +1,546 @@ +<?php +/** + * WooCommerce sync module. + * + * @package automattic/jetpack-sync + */ + +namespace Automattic\Jetpack\Sync\Modules; + +/** + * Class to handle sync for WooCommerce. + */ +class WooCommerce extends Module { + /** + * Whitelist for order item meta we are interested to sync. + * + * @access private + * + * @var array + */ + private $order_item_meta_whitelist = array( + // See https://github.com/woocommerce/woocommerce/blob/master/includes/data-stores/class-wc-order-item-product-store.php#L20 . + '_product_id', + '_variation_id', + '_qty', + // Tax ones also included in below class + // See https://github.com/woocommerce/woocommerce/blob/master/includes/data-stores/class-wc-order-item-fee-data-store.php#L20 . + '_tax_class', + '_tax_status', + '_line_subtotal', + '_line_subtotal_tax', + '_line_total', + '_line_tax', + '_line_tax_data', + // See https://github.com/woocommerce/woocommerce/blob/master/includes/data-stores/class-wc-order-item-shipping-data-store.php#L20 . + 'method_id', + 'cost', + 'total_tax', + 'taxes', + // See https://github.com/woocommerce/woocommerce/blob/master/includes/data-stores/class-wc-order-item-tax-data-store.php#L20 . + 'rate_id', + 'label', + 'compound', + 'tax_amount', + 'shipping_tax_amount', + // See https://github.com/woocommerce/woocommerce/blob/master/includes/data-stores/class-wc-order-item-coupon-data-store.php . + 'discount_amount', + 'discount_amount_tax', + ); + + /** + * Name of the order item database table. + * + * @access private + * + * @var string + */ + private $order_item_table_name; + + /** + * Constructor. + * + * @global $wpdb + * + * @todo Should we refactor this to use $this->set_defaults() instead? + */ + public function __construct() { + global $wpdb; + $this->order_item_table_name = $wpdb->prefix . 'woocommerce_order_items'; + + // Options, constants and post meta whitelists. + add_filter( 'jetpack_sync_options_whitelist', array( $this, 'add_woocommerce_options_whitelist' ), 10 ); + add_filter( 'jetpack_sync_constants_whitelist', array( $this, 'add_woocommerce_constants_whitelist' ), 10 ); + add_filter( 'jetpack_sync_post_meta_whitelist', array( $this, 'add_woocommerce_post_meta_whitelist' ), 10 ); + add_filter( 'jetpack_sync_comment_meta_whitelist', array( $this, 'add_woocommerce_comment_meta_whitelist' ), 10 ); + + add_filter( 'jetpack_sync_before_enqueue_woocommerce_new_order_item', array( $this, 'filter_order_item' ) ); + add_filter( 'jetpack_sync_before_enqueue_woocommerce_update_order_item', array( $this, 'filter_order_item' ) ); + add_filter( 'jetpack_sync_whitelisted_comment_types', array( $this, 'add_review_comment_types' ) ); + + // Blacklist Action Scheduler comment types. + add_filter( 'jetpack_sync_prevent_sending_comment_data', array( $this, 'filter_action_scheduler_comments' ), 10, 2 ); + } + + /** + * Sync module name. + * + * @access public + * + * @return string + */ + public function name() { + return 'woocommerce'; + } + + /** + * Initialize WooCommerce action listeners. + * + * @access public + * + * @param callable $callable Action handler callable. + */ + public function init_listeners( $callable ) { + // Attributes. + add_action( 'woocommerce_attribute_added', $callable, 10, 2 ); + add_action( 'woocommerce_attribute_updated', $callable, 10, 3 ); + add_action( 'woocommerce_attribute_deleted', $callable, 10, 3 ); + + // Orders. + add_action( 'woocommerce_new_order', $callable, 10, 1 ); + add_action( 'woocommerce_order_status_changed', $callable, 10, 3 ); + add_action( 'woocommerce_payment_complete', $callable, 10, 1 ); + + // Order items. + add_action( 'woocommerce_new_order_item', $callable, 10, 4 ); + add_action( 'woocommerce_update_order_item', $callable, 10, 4 ); + add_action( 'woocommerce_delete_order_item', $callable, 10, 1 ); + $this->init_listeners_for_meta_type( 'order_item', $callable ); + + // Payment tokens. + add_action( 'woocommerce_new_payment_token', $callable, 10, 1 ); + add_action( 'woocommerce_payment_token_deleted', $callable, 10, 2 ); + add_action( 'woocommerce_payment_token_updated', $callable, 10, 1 ); + $this->init_listeners_for_meta_type( 'payment_token', $callable ); + + // Product downloads. + add_action( 'woocommerce_downloadable_product_download_log_insert', $callable, 10, 1 ); + add_action( 'woocommerce_grant_product_download_access', $callable, 10, 1 ); + + // Tax rates. + add_action( 'woocommerce_tax_rate_added', $callable, 10, 2 ); + add_action( 'woocommerce_tax_rate_updated', $callable, 10, 2 ); + add_action( 'woocommerce_tax_rate_deleted', $callable, 10, 1 ); + + // Webhooks. + add_action( 'woocommerce_new_webhook', $callable, 10, 1 ); + add_action( 'woocommerce_webhook_deleted', $callable, 10, 2 ); + add_action( 'woocommerce_webhook_updated', $callable, 10, 1 ); + } + + /** + * Initialize WooCommerce action listeners for full sync. + * + * @access public + * + * @param callable $callable Action handler callable. + */ + public function init_full_sync_listeners( $callable ) { + add_action( 'jetpack_full_sync_woocommerce_order_items', $callable ); // Also sends post meta. + } + + /** + * Retrieve the actions that will be sent for this module during a full sync. + * + * @access public + * + * @return array Full sync actions of this module. + */ + public function get_full_sync_actions() { + return array( 'jetpack_full_sync_woocommerce_order_items' ); + } + + /** + * Initialize the module in the sender. + * + * @access public + */ + public function init_before_send() { + // Full sync. + add_filter( 'jetpack_sync_before_send_jetpack_full_sync_woocommerce_order_items', array( $this, 'expand_order_item_ids' ) ); + } + + /** + * Expand the order items properly. + * + * @access public + * + * @param array $args The hook arguments. + * @return array $args The hook arguments. + */ + public function filter_order_item( $args ) { + // Make sure we always have all the data - prior to WooCommerce 3.0 we only have the user supplied data in the second argument and not the full details. + $args[1] = $this->build_order_item( $args[0] ); + return $args; + } + + /** + * Expand order item IDs to order items and their meta. + * + * @access public + * + * @todo Refactor table name to use a $wpdb->prepare placeholder. + * + * @param array $args The hook arguments. + * @return array $args Expanded order items with meta. + */ + public function expand_order_item_ids( $args ) { + $order_item_ids = $args[0]; + + global $wpdb; + + $order_item_ids_sql = implode( ', ', array_map( 'intval', $order_item_ids ) ); + + $order_items = $wpdb->get_results( + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + "SELECT * FROM $this->order_item_table_name WHERE order_item_id IN ( $order_item_ids_sql )" + ); + + return array( + $order_items, + $this->get_metadata( $order_item_ids, 'order_item', $this->order_item_meta_whitelist ), + ); + } + + /** + * Extract the full order item from the database by its ID. + * + * @access public + * + * @todo Refactor table name to use a $wpdb->prepare placeholder. + * + * @param int $order_item_id Order item ID. + * @return object Order item. + */ + public function build_order_item( $order_item_id ) { + global $wpdb; + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + return $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $this->order_item_table_name WHERE order_item_id = %d", $order_item_id ) ); + } + + /** + * Enqueue the WooCommerce actions for full sync. + * + * @access public + * + * @param array $config Full sync configuration for this sync module. + * @param int $max_items_to_enqueue Maximum number of items to enqueue. + * @param boolean $state True if full sync has finished enqueueing this module, false otherwise. + * @return array Number of actions enqueued, and next module state. + */ + public function enqueue_full_sync_actions( $config, $max_items_to_enqueue, $state ) { + return $this->enqueue_all_ids_as_action( 'jetpack_full_sync_woocommerce_order_items', $this->order_item_table_name, 'order_item_id', $this->get_where_sql( $config ), $max_items_to_enqueue, $state ); + } + + /** + * Retrieve an estimated number of actions that will be enqueued. + * + * @access public + * + * @todo Refactor the SQL query to use $wpdb->prepare(). + * + * @param array $config Full sync configuration for this sync module. + * @return array Number of items yet to be enqueued. + */ + public function estimate_full_sync_actions( $config ) { + global $wpdb; + + $query = "SELECT count(*) FROM $this->order_item_table_name WHERE " . $this->get_where_sql( $config ); + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + $count = $wpdb->get_var( $query ); + + return (int) ceil( $count / self::ARRAY_CHUNK_SIZE ); + } + + /** + * Retrieve the WHERE SQL clause based on the module config. + * + * @access private + * + * @param array $config Full sync configuration for this sync module. + * @return string WHERE SQL clause. + */ + private function get_where_sql( $config ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable + return '1=1'; + } + + /** + * Add WooCommerce options to the options whitelist. + * + * @param array $list Existing options whitelist. + * @return array Updated options whitelist. + */ + public function add_woocommerce_options_whitelist( $list ) { + return array_merge( $list, self::$wc_options_whitelist ); + } + + /** + * Add WooCommerce constants to the constants whitelist. + * + * @param array $list Existing constants whitelist. + * @return array Updated constants whitelist. + */ + public function add_woocommerce_constants_whitelist( $list ) { + return array_merge( $list, self::$wc_constants_whitelist ); + } + + /** + * Add WooCommerce post meta to the post meta whitelist. + * + * @param array $list Existing post meta whitelist. + * @return array Updated post meta whitelist. + */ + public function add_woocommerce_post_meta_whitelist( $list ) { + return array_merge( $list, self::$wc_post_meta_whitelist ); + } + + /** + * Add WooCommerce comment meta to the comment meta whitelist. + * + * @param array $list Existing comment meta whitelist. + * @return array Updated comment meta whitelist. + */ + public function add_woocommerce_comment_meta_whitelist( $list ) { + return array_merge( $list, self::$wc_comment_meta_whitelist ); + } + + /** + * Adds 'revew' to the list of comment types so Sync will listen for status changes on 'reviews'. + * + * @access public + * + * @param array $comment_types The list of comment types prior to this filter. + * return array The list of comment types with 'review' added. + */ + public function add_review_comment_types( $comment_types ) { + if ( is_array( $comment_types ) ) { + $comment_types[] = 'review'; + } + return $comment_types; + } + + /** + * Stop comments from the Action Scheduler from being synced. + * https://github.com/woocommerce/woocommerce/tree/e7762627c37ec1f7590e6cac4218ba0c6a20024d/includes/libraries/action-scheduler + * + * @since 7.7.0 + * + * @param boolean $can_sync Should we prevent comment data from bing synced to WordPress.com. + * @param mixed $comment WP_COMMENT object. + * + * @return bool + */ + public function filter_action_scheduler_comments( $can_sync, $comment ) { + if ( isset( $comment->comment_agent ) && 'ActionScheduler' === $comment->comment_agent ) { + return true; + } + return $can_sync; + } + + /** + * Whitelist for options we are interested to sync. + * + * @access private + * @static + * + * @var array + */ + private static $wc_options_whitelist = array( + 'woocommerce_currency', + 'woocommerce_db_version', + 'woocommerce_weight_unit', + 'woocommerce_version', + 'woocommerce_unforce_ssl_checkout', + 'woocommerce_tax_total_display', + 'woocommerce_tax_round_at_subtotal', + 'woocommerce_tax_display_shop', + 'woocommerce_tax_display_cart', + 'woocommerce_prices_include_tax', + 'woocommerce_price_thousand_sep', + 'woocommerce_price_num_decimals', + 'woocommerce_price_decimal_sep', + 'woocommerce_notify_low_stock', + 'woocommerce_notify_low_stock_amount', + 'woocommerce_notify_no_stock', + 'woocommerce_notify_no_stock_amount', + 'woocommerce_manage_stock', + 'woocommerce_force_ssl_checkout', + 'woocommerce_hide_out_of_stock_items', + 'woocommerce_file_download_method', + 'woocommerce_enable_signup_and_login_from_checkout', + 'woocommerce_enable_shipping_calc', + 'woocommerce_enable_review_rating', + 'woocommerce_enable_guest_checkout', + 'woocommerce_enable_coupons', + 'woocommerce_enable_checkout_login_reminder', + 'woocommerce_enable_ajax_add_to_cart', + 'woocommerce_dimension_unit', + 'woocommerce_default_country', + 'woocommerce_default_customer_address', + 'woocommerce_currency_pos', + 'woocommerce_api_enabled', + 'woocommerce_allow_tracking', + ); + + /** + * Whitelist for constants we are interested to sync. + * + * @access private + * @static + * + * @var array + */ + private static $wc_constants_whitelist = array( + // WooCommerce constants. + 'WC_PLUGIN_FILE', + 'WC_ABSPATH', + 'WC_PLUGIN_BASENAME', + 'WC_VERSION', + 'WOOCOMMERCE_VERSION', + 'WC_ROUNDING_PRECISION', + 'WC_DISCOUNT_ROUNDING_MODE', + 'WC_TAX_ROUNDING_MODE', + 'WC_DELIMITER', + 'WC_LOG_DIR', + 'WC_SESSION_CACHE_GROUP', + 'WC_TEMPLATE_DEBUG_MODE', + ); + + /** + * Whitelist for post meta we are interested to sync. + * + * @access private + * @static + * + * @var array + */ + private static $wc_post_meta_whitelist = array( + // WooCommerce products. + // See https://github.com/woocommerce/woocommerce/blob/8ed6e7436ff87c2153ed30edd83c1ab8abbdd3e9/includes/data-stores/class-wc-product-data-store-cpt.php#L21 . + '_visibility', + '_sku', + '_price', + '_regular_price', + '_sale_price', + '_sale_price_dates_from', + '_sale_price_dates_to', + 'total_sales', + '_tax_status', + '_tax_class', + '_manage_stock', + '_backorders', + '_sold_individually', + '_weight', + '_length', + '_width', + '_height', + '_upsell_ids', + '_crosssell_ids', + '_purchase_note', + '_default_attributes', + '_product_attributes', + '_virtual', + '_downloadable', + '_download_limit', + '_download_expiry', + '_featured', + '_downloadable_files', + '_wc_rating_count', + '_wc_average_rating', + '_wc_review_count', + '_variation_description', + '_thumbnail_id', + '_file_paths', + '_product_image_gallery', + '_product_version', + '_wp_old_slug', + + // Woocommerce orders. + // See https://github.com/woocommerce/woocommerce/blob/8ed6e7436ff87c2153ed30edd83c1ab8abbdd3e9/includes/data-stores/class-wc-order-data-store-cpt.php#L27 . + '_order_key', + '_order_currency', + // '_billing_first_name', do not sync these as they contain personal data + // '_billing_last_name', + // '_billing_company', + // '_billing_address_1', + // '_billing_address_2', + '_billing_city', + '_billing_state', + '_billing_postcode', + '_billing_country', + // '_billing_email', do not sync these as they contain personal data. + // '_billing_phone', + // '_shipping_first_name', + // '_shipping_last_name', + // '_shipping_company', + // '_shipping_address_1', + // '_shipping_address_2', + '_shipping_city', + '_shipping_state', + '_shipping_postcode', + '_shipping_country', + '_completed_date', + '_paid_date', + '_cart_discount', + '_cart_discount_tax', + '_order_shipping', + '_order_shipping_tax', + '_order_tax', + '_order_total', + '_payment_method', + '_payment_method_title', + // '_transaction_id', do not sync these as they contain personal data. + // '_customer_ip_address', + // '_customer_user_agent', + '_created_via', + '_order_version', + '_prices_include_tax', + '_date_completed', + '_date_paid', + '_payment_tokens', + '_billing_address_index', + '_shipping_address_index', + '_recorded_sales', + '_recorded_coupon_usage_counts', + // See https://github.com/woocommerce/woocommerce/blob/8ed6e7436ff87c2153ed30edd83c1ab8abbdd3e9/includes/data-stores/class-wc-order-data-store-cpt.php#L539 . + '_download_permissions_granted', + // See https://github.com/woocommerce/woocommerce/blob/8ed6e7436ff87c2153ed30edd83c1ab8abbdd3e9/includes/data-stores/class-wc-order-data-store-cpt.php#L594 . + '_order_stock_reduced', + + // Woocommerce order refunds. + // See https://github.com/woocommerce/woocommerce/blob/b8a2815ae546c836467008739e7ff5150cb08e93/includes/data-stores/class-wc-order-refund-data-store-cpt.php#L20 . + '_order_currency', + '_refund_amount', + '_refunded_by', + '_refund_reason', + '_order_shipping', + '_order_shipping_tax', + '_order_tax', + '_order_total', + '_order_version', + '_prices_include_tax', + '_payment_tokens', + ); + + /** + * Whitelist for comment meta we are interested to sync. + * + * @access private + * @static + * + * @var array + */ + private static $wc_comment_meta_whitelist = array( + 'rating', + ); +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-wp-super-cache.php b/plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-wp-super-cache.php new file mode 100644 index 00000000..af4aec41 --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-wp-super-cache.php @@ -0,0 +1,156 @@ +<?php +/** + * WP_Super_Cache sync module. + * + * @package automattic/jetpack-sync + */ + +namespace Automattic\Jetpack\Sync\Modules; + +/** + * Class to handle sync for WP_Super_Cache. + */ +class WP_Super_Cache extends Module { + /** + * Constructor. + * + * @todo Should we refactor this to use $this->set_defaults() instead? + */ + public function __construct() { + add_filter( 'jetpack_sync_constants_whitelist', array( $this, 'add_wp_super_cache_constants_whitelist' ), 10 ); + add_filter( 'jetpack_sync_callable_whitelist', array( $this, 'add_wp_super_cache_callable_whitelist' ), 10 ); + } + + /** + * Whitelist for constants we are interested to sync. + * + * @access public + * @static + * + * @var array + */ + public static $wp_super_cache_constants = array( + 'WPLOCKDOWN', + 'WPSC_DISABLE_COMPRESSION', + 'WPSC_DISABLE_LOCKING', + 'WPSC_DISABLE_HTACCESS_UPDATE', + 'ADVANCEDCACHEPROBLEM', + ); + + /** + * Container for the whitelist for WP_Super_Cache callables we are interested to sync. + * + * @access public + * @static + * + * @var array + */ + public static $wp_super_cache_callables = array( + 'wp_super_cache_globals' => array( __CLASS__, 'get_wp_super_cache_globals' ), + ); + + /** + * Sync module name. + * + * @access public + * + * @return string + */ + public function name() { + return 'wp-super-cache'; + } + + /** + * Retrieve all WP_Super_Cache callables we are interested to sync. + * + * @access public + * + * @global $wp_cache_mod_rewrite; + * @global $cache_enabled; + * @global $super_cache_enabled; + * @global $ossdlcdn; + * @global $cache_rebuild_files; + * @global $wp_cache_mobile; + * @global $wp_super_cache_late_init; + * @global $wp_cache_anon_only; + * @global $wp_cache_not_logged_in; + * @global $wp_cache_clear_on_post_edit; + * @global $wp_cache_mobile_enabled; + * @global $wp_super_cache_debug; + * @global $cache_max_time; + * @global $wp_cache_refresh_single_only; + * @global $wp_cache_mfunc_enabled; + * @global $wp_supercache_304; + * @global $wp_cache_no_cache_for_get; + * @global $wp_cache_mutex_disabled; + * @global $cache_jetpack; + * @global $cache_domain_mapping; + * + * @return array All WP_Super_Cache callables. + */ + public static function get_wp_super_cache_globals() { + global $wp_cache_mod_rewrite; + global $cache_enabled; + global $super_cache_enabled; + global $ossdlcdn; + global $cache_rebuild_files; + global $wp_cache_mobile; + global $wp_super_cache_late_init; + global $wp_cache_anon_only; + global $wp_cache_not_logged_in; + global $wp_cache_clear_on_post_edit; + global $wp_cache_mobile_enabled; + global $wp_super_cache_debug; + global $cache_max_time; + global $wp_cache_refresh_single_only; + global $wp_cache_mfunc_enabled; + global $wp_supercache_304; + global $wp_cache_no_cache_for_get; + global $wp_cache_mutex_disabled; + global $cache_jetpack; + global $cache_domain_mapping; + + return array( + 'wp_cache_mod_rewrite' => $wp_cache_mod_rewrite, + 'cache_enabled' => $cache_enabled, + 'super_cache_enabled' => $super_cache_enabled, + 'ossdlcdn' => $ossdlcdn, + 'cache_rebuild_files' => $cache_rebuild_files, + 'wp_cache_mobile' => $wp_cache_mobile, + 'wp_super_cache_late_init' => $wp_super_cache_late_init, + 'wp_cache_anon_only' => $wp_cache_anon_only, + 'wp_cache_not_logged_in' => $wp_cache_not_logged_in, + 'wp_cache_clear_on_post_edit' => $wp_cache_clear_on_post_edit, + 'wp_cache_mobile_enabled' => $wp_cache_mobile_enabled, + 'wp_super_cache_debug' => $wp_super_cache_debug, + 'cache_max_time' => $cache_max_time, + 'wp_cache_refresh_single_only' => $wp_cache_refresh_single_only, + 'wp_cache_mfunc_enabled' => $wp_cache_mfunc_enabled, + 'wp_supercache_304' => $wp_supercache_304, + 'wp_cache_no_cache_for_get' => $wp_cache_no_cache_for_get, + 'wp_cache_mutex_disabled' => $wp_cache_mutex_disabled, + 'cache_jetpack' => $cache_jetpack, + 'cache_domain_mapping' => $cache_domain_mapping, + ); + } + + /** + * Add WP_Super_Cache constants to the constants whitelist. + * + * @param array $list Existing constants whitelist. + * @return array Updated constants whitelist. + */ + public function add_wp_super_cache_constants_whitelist( $list ) { + return array_merge( $list, self::$wp_super_cache_constants ); + } + + /** + * Add WP_Super_Cache callables to the callables whitelist. + * + * @param array $list Existing callables whitelist. + * @return array Updated callables whitelist. + */ + public function add_wp_super_cache_callable_whitelist( $list ) { + return array_merge( $list, self::$wp_super_cache_callables ); + } +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-terms-of-service/src/class-terms-of-service.php b/plugins/jetpack/vendor/automattic/jetpack-terms-of-service/src/class-terms-of-service.php new file mode 100644 index 00000000..8bd0f5b1 --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-terms-of-service/src/class-terms-of-service.php @@ -0,0 +1,116 @@ +<?php +/** + * A Terms of Service class for Jetpack. + * + * @package automattic/jetpack-terms-of-service + */ + +namespace Automattic\Jetpack; + +use Automattic\Jetpack\Connection\Manager; +use Automattic\Jetpack\Status; + +/** + * 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 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 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. + * + * @return bool + */ + public function has_agreed() { + if ( $this->is_development_mode() ) { + return false; + } + + return $this->get_raw_has_agreed() || $this->is_active(); + } + + /** + * Abstracted for testing purposes. + * Tells us if the site is in dev mode. + * + * @return bool + */ + protected function is_development_mode() { + return ( new Status() )->is_development_mode(); + } + + /** + * Tells us if the site is connected. + * Abstracted for testing purposes. + * + * @return bool + */ + protected function is_active() { + return ( new Manager() )->is_active(); + } + + /** + * 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 ); + } + +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-tracking/legacy/class-jetpack-tracks-client.php b/plugins/jetpack/vendor/automattic/jetpack-tracking/legacy/class-jetpack-tracks-client.php new file mode 100644 index 00000000..0ea25184 --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-tracking/legacy/class-jetpack-tracks-client.php @@ -0,0 +1,228 @@ +<?php +/** + * Legacy Jetpack Tracks Client + * + * @package Jetpack + */ + +/** + * Jetpack_Tracks_Client + * + * @autounit nosara 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( $event ); + + 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-Za-z0-9+/=]{24}$#', $_COOKIE['tk_ai'] ) ) { + $anon_id = $_COOKIE['tk_ai']; + } 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 ); + } + } + } + + 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 = Jetpack::get_connected_user_data(); + if ( ! $user_data ) { + return false; + } + + return array( + 'blogid' => Jetpack_Options::get_option( 'id', 0 ), + 'userid' => $user_data['ID'], + 'username' => $user_data['login'], + ); + } +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-tracking/legacy/class-jetpack-tracks-event.php b/plugins/jetpack/vendor/automattic/jetpack-tracking/legacy/class-jetpack-tracks-event.php new file mode 100644 index 00000000..1ccd871f --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-tracking/legacy/class-jetpack-tracks-event.php @@ -0,0 +1,189 @@ +<?php +/** + * Class Jetpack_Tracks_Event. Legacy. + * + * @package automattic/jetpack-sync + */ + +/* + * @autounit nosara tracks-client + * + * 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 + */ +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 blacklist 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; + } + } + } +} diff --git a/plugins/jetpack/vendor/automattic/jetpack-tracking/src/class-tracking.php b/plugins/jetpack/vendor/automattic/jetpack-tracking/src/class-tracking.php new file mode 100644 index 00000000..0f08744c --- /dev/null +++ b/plugins/jetpack/vendor/automattic/jetpack-tracking/src/class-tracking.php @@ -0,0 +1,202 @@ +<?php +/** + * Nosara Tracks for Jetpack + * + * @package automattic/jetpack-tracking + */ + +namespace Automattic\Jetpack; + +use Automattic\Jetpack\Terms_Of_Service; + +/** + * The Tracking class, used to record events in wpcom + */ +class Tracking { + /** + * 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 ( is_null( $this->connection ) ) { + // TODO We should always pass a Connection. + $this->connection = new Connection\Manager(); + } + } + + /** + * Enqueue script necessary for tracking. + */ + public function enqueue_tracks_scripts() { + wp_enqueue_script( 'jptracks', plugins_url( '_inc/lib/tracks/tracks-ajax.js', JETPACK__PLUGIN_FILE ), array(), JETPACK__VERSION, 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. + */ + public function record_user_event( $event_type, $data = array(), $user = null ) { + if ( ! $user ) { + $user = wp_get_current_user(); + } + $site_url = get_option( 'siteurl' ); + + $data['_via_ua'] = isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : ''; + $data['_via_ip'] = isset( $_SERVER['REMOTE_ADDR'] ) ? $_SERVER['REMOTE_ADDR'] : ''; + $data['_lg'] = isset( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ? $_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 ) { + $event_type = $this->product_name . '_' . $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(); + // Don't track users who have opted out or not agreed to our TOS, or are not running an active Jetpack. + if ( ! $terms_of_service->has_agreed() ) { + 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(); + } + + /** + * 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' ), + ); + + $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 && $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 ); + update_user_meta( $user_id, 'jetpack_tracks_wpcom_id', $wpcom_user_data['ID'] ); + + return array( + '_ut' => 'wpcom:user_id', + '_ui' => $wpcom_user_data['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 ); + } + + return array( + '_ut' => 'anon', + '_ui' => $anon_id, + ); + + } +} |