From 10ef81bf85ad0a4bad0d204838e14c99ca2526f7 Mon Sep 17 00:00:00 2001 From: "Anthony G. Basile" Date: Mon, 6 Jan 2020 14:32:30 -0500 Subject: Update jetpack 8.0 Signed-off-by: Anthony G. Basile --- .../automattic/jetpack-abtest/src/class-abtest.php | 102 + .../automattic/jetpack-assets/src/class-assets.php | 42 + .../vendor/automattic/jetpack-backup/actions.php | 13 + .../src/class-helper-script-manager.php | 347 ++++ .../vendor/automattic/jetpack-compat/functions.php | 25 + .../jetpack-compat/legacy/class-jetpack-client.php | 90 + .../legacy/class-jetpack-sync-actions.php | 359 ++++ .../legacy/class-jetpack-sync-modules.php | 28 + .../legacy/class-jetpack-sync-settings.php | 230 +++ .../legacy/class-jetpacktracking.php | 47 + .../jetpack-compat/lib/tracks/client.php | 41 + .../legacy/class-jetpack-ixr-client.php | 122 ++ .../legacy/class-jetpack-ixr-clientmulticall.php | 68 + .../legacy/class-jetpack-signature.php | 344 ++++ .../legacy/class-jetpack-xmlrpc-server.php | 943 +++++++++ .../jetpack-connection/legacy/load-ixr.php | 13 + .../jetpack-connection/src/class-client.php | 455 ++++ .../jetpack-connection/src/class-manager.php | 2169 ++++++++++++++++++++ .../src/class-rest-connector.php | 54 + .../jetpack-connection/src/class-utils.php | 62 + .../src/class-xmlrpc-connector.php | 80 + .../jetpack-connection/src/interface-manager.php | 17 + .../jetpack-constants/src/class-constants.php | 111 + .../automattic/jetpack-error/src/class-error.php | 15 + .../jetpack-jitm/assets/jetpack-admin-jitm-rtl.css | 490 +++++ .../assets/jetpack-admin-jitm-rtl.min.css | 5 + .../jetpack-jitm/assets/jetpack-admin-jitm.css | 489 +++++ .../jetpack-jitm/assets/jetpack-admin-jitm.min.css | 3 + .../automattic/jetpack-jitm/src/class-jitm.php | 637 ++++++ .../automattic/jetpack-logo/src/class-logo.php | 71 + .../legacy/class-jetpack-options.php | 646 ++++++ .../automattic/jetpack-roles/src/class-roles.php | 81 + .../automattic/jetpack-status/src/class-status.php | 83 + .../automattic/jetpack-sync/src/class-actions.php | 767 +++++++ .../automattic/jetpack-sync/src/class-defaults.php | 1179 +++++++++++ .../jetpack-sync/src/class-functions.php | 544 +++++ .../src/class-json-deflate-array-codec.php | 136 ++ .../automattic/jetpack-sync/src/class-listener.php | 442 ++++ .../automattic/jetpack-sync/src/class-lock.php | 65 + .../automattic/jetpack-sync/src/class-main.php | 34 + .../automattic/jetpack-sync/src/class-modules.php | 207 ++ .../jetpack-sync/src/class-queue-buffer.php | 78 + .../automattic/jetpack-sync/src/class-queue.php | 706 +++++++ .../jetpack-sync/src/class-replicastore.php | 1489 ++++++++++++++ .../automattic/jetpack-sync/src/class-sender.php | 795 +++++++ .../automattic/jetpack-sync/src/class-server.php | 190 ++ .../automattic/jetpack-sync/src/class-settings.php | 440 ++++ .../jetpack-sync/src/class-simple-codec.php | 63 + .../automattic/jetpack-sync/src/class-users.php | 157 ++ .../automattic/jetpack-sync/src/class-utils.php | 65 + .../jetpack-sync/src/interface-codec.php | 44 + .../jetpack-sync/src/interface-replicastore.php | 566 +++++ .../jetpack-sync/src/modules/class-attachments.php | 95 + .../jetpack-sync/src/modules/class-callables.php | 491 +++++ .../jetpack-sync/src/modules/class-comments.php | 411 ++++ .../jetpack-sync/src/modules/class-constants.php | 248 +++ .../jetpack-sync/src/modules/class-full-sync.php | 673 ++++++ .../jetpack-sync/src/modules/class-import.php | 218 ++ .../jetpack-sync/src/modules/class-menus.php | 143 ++ .../jetpack-sync/src/modules/class-meta.php | 81 + .../jetpack-sync/src/modules/class-module.php | 463 +++++ .../src/modules/class-network-options.php | 236 +++ .../jetpack-sync/src/modules/class-options.php | 344 ++++ .../jetpack-sync/src/modules/class-plugins.php | 413 ++++ .../jetpack-sync/src/modules/class-posts.php | 671 ++++++ .../jetpack-sync/src/modules/class-protect.php | 53 + .../jetpack-sync/src/modules/class-stats.php | 66 + .../src/modules/class-term-relationships.php | 204 ++ .../jetpack-sync/src/modules/class-terms.php | 322 +++ .../jetpack-sync/src/modules/class-themes.php | 825 ++++++++ .../jetpack-sync/src/modules/class-updates.php | 496 +++++ .../jetpack-sync/src/modules/class-users.php | 854 ++++++++ .../jetpack-sync/src/modules/class-woocommerce.php | 546 +++++ .../src/modules/class-wp-super-cache.php | 156 ++ .../src/class-terms-of-service.php | 116 ++ .../legacy/class-jetpack-tracks-client.php | 228 ++ .../legacy/class-jetpack-tracks-event.php | 189 ++ .../jetpack-tracking/src/class-tracking.php | 202 ++ 78 files changed, 24993 insertions(+) create mode 100644 plugins/jetpack/vendor/automattic/jetpack-abtest/src/class-abtest.php create mode 100644 plugins/jetpack/vendor/automattic/jetpack-assets/src/class-assets.php create mode 100644 plugins/jetpack/vendor/automattic/jetpack-backup/actions.php create mode 100644 plugins/jetpack/vendor/automattic/jetpack-backup/src/class-helper-script-manager.php create mode 100644 plugins/jetpack/vendor/automattic/jetpack-compat/functions.php create mode 100644 plugins/jetpack/vendor/automattic/jetpack-compat/legacy/class-jetpack-client.php create mode 100644 plugins/jetpack/vendor/automattic/jetpack-compat/legacy/class-jetpack-sync-actions.php create mode 100644 plugins/jetpack/vendor/automattic/jetpack-compat/legacy/class-jetpack-sync-modules.php create mode 100644 plugins/jetpack/vendor/automattic/jetpack-compat/legacy/class-jetpack-sync-settings.php create mode 100644 plugins/jetpack/vendor/automattic/jetpack-compat/legacy/class-jetpacktracking.php create mode 100644 plugins/jetpack/vendor/automattic/jetpack-compat/lib/tracks/client.php create mode 100644 plugins/jetpack/vendor/automattic/jetpack-connection/legacy/class-jetpack-ixr-client.php create mode 100644 plugins/jetpack/vendor/automattic/jetpack-connection/legacy/class-jetpack-ixr-clientmulticall.php create mode 100644 plugins/jetpack/vendor/automattic/jetpack-connection/legacy/class-jetpack-signature.php create mode 100644 plugins/jetpack/vendor/automattic/jetpack-connection/legacy/class-jetpack-xmlrpc-server.php create mode 100644 plugins/jetpack/vendor/automattic/jetpack-connection/legacy/load-ixr.php create mode 100644 plugins/jetpack/vendor/automattic/jetpack-connection/src/class-client.php create mode 100644 plugins/jetpack/vendor/automattic/jetpack-connection/src/class-manager.php create mode 100644 plugins/jetpack/vendor/automattic/jetpack-connection/src/class-rest-connector.php create mode 100644 plugins/jetpack/vendor/automattic/jetpack-connection/src/class-utils.php create mode 100644 plugins/jetpack/vendor/automattic/jetpack-connection/src/class-xmlrpc-connector.php create mode 100644 plugins/jetpack/vendor/automattic/jetpack-connection/src/interface-manager.php create mode 100644 plugins/jetpack/vendor/automattic/jetpack-constants/src/class-constants.php create mode 100644 plugins/jetpack/vendor/automattic/jetpack-error/src/class-error.php create mode 100644 plugins/jetpack/vendor/automattic/jetpack-jitm/assets/jetpack-admin-jitm-rtl.css create mode 100644 plugins/jetpack/vendor/automattic/jetpack-jitm/assets/jetpack-admin-jitm-rtl.min.css create mode 100644 plugins/jetpack/vendor/automattic/jetpack-jitm/assets/jetpack-admin-jitm.css create mode 100644 plugins/jetpack/vendor/automattic/jetpack-jitm/assets/jetpack-admin-jitm.min.css create mode 100644 plugins/jetpack/vendor/automattic/jetpack-jitm/src/class-jitm.php create mode 100644 plugins/jetpack/vendor/automattic/jetpack-logo/src/class-logo.php create mode 100644 plugins/jetpack/vendor/automattic/jetpack-options/legacy/class-jetpack-options.php create mode 100644 plugins/jetpack/vendor/automattic/jetpack-roles/src/class-roles.php create mode 100644 plugins/jetpack/vendor/automattic/jetpack-status/src/class-status.php create mode 100644 plugins/jetpack/vendor/automattic/jetpack-sync/src/class-actions.php create mode 100644 plugins/jetpack/vendor/automattic/jetpack-sync/src/class-defaults.php create mode 100644 plugins/jetpack/vendor/automattic/jetpack-sync/src/class-functions.php create mode 100644 plugins/jetpack/vendor/automattic/jetpack-sync/src/class-json-deflate-array-codec.php create mode 100644 plugins/jetpack/vendor/automattic/jetpack-sync/src/class-listener.php create mode 100644 plugins/jetpack/vendor/automattic/jetpack-sync/src/class-lock.php create mode 100644 plugins/jetpack/vendor/automattic/jetpack-sync/src/class-main.php create mode 100644 plugins/jetpack/vendor/automattic/jetpack-sync/src/class-modules.php create mode 100644 plugins/jetpack/vendor/automattic/jetpack-sync/src/class-queue-buffer.php create mode 100644 plugins/jetpack/vendor/automattic/jetpack-sync/src/class-queue.php create mode 100644 plugins/jetpack/vendor/automattic/jetpack-sync/src/class-replicastore.php create mode 100644 plugins/jetpack/vendor/automattic/jetpack-sync/src/class-sender.php create mode 100644 plugins/jetpack/vendor/automattic/jetpack-sync/src/class-server.php create mode 100644 plugins/jetpack/vendor/automattic/jetpack-sync/src/class-settings.php create mode 100644 plugins/jetpack/vendor/automattic/jetpack-sync/src/class-simple-codec.php create mode 100644 plugins/jetpack/vendor/automattic/jetpack-sync/src/class-users.php create mode 100644 plugins/jetpack/vendor/automattic/jetpack-sync/src/class-utils.php create mode 100644 plugins/jetpack/vendor/automattic/jetpack-sync/src/interface-codec.php create mode 100644 plugins/jetpack/vendor/automattic/jetpack-sync/src/interface-replicastore.php create mode 100644 plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-attachments.php create mode 100644 plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-callables.php create mode 100644 plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-comments.php create mode 100644 plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-constants.php create mode 100644 plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-full-sync.php create mode 100644 plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-import.php create mode 100644 plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-menus.php create mode 100644 plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-meta.php create mode 100644 plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-module.php create mode 100644 plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-network-options.php create mode 100644 plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-options.php create mode 100644 plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-plugins.php create mode 100644 plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-posts.php create mode 100644 plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-protect.php create mode 100644 plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-stats.php create mode 100644 plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-term-relationships.php create mode 100644 plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-terms.php create mode 100644 plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-themes.php create mode 100644 plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-updates.php create mode 100644 plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-users.php create mode 100644 plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-woocommerce.php create mode 100644 plugins/jetpack/vendor/automattic/jetpack-sync/src/modules/class-wp-super-cache.php create mode 100644 plugins/jetpack/vendor/automattic/jetpack-terms-of-service/src/class-terms-of-service.php create mode 100644 plugins/jetpack/vendor/automattic/jetpack-tracking/legacy/class-jetpack-tracks-client.php create mode 100644 plugins/jetpack/vendor/automattic/jetpack-tracking/legacy/class-jetpack-tracks-event.php create mode 100644 plugins/jetpack/vendor/automattic/jetpack-tracking/src/class-tracking.php (limited to 'plugins/jetpack/vendor/automattic') 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 @@ +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 @@ + 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 @@ +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 @@ +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 @@ + $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 @@ + $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 @@ +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 @@ +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 @@ + '', + '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 @@ +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 '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 @@ +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 @@ +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 @@ +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 "
"; + echo '

' . esc_html__( 'Important notice about your Jetpack connection:', 'jetpack' ) . '

'; + echo '

' . 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 ) + ) . '

'; + + if ( ! empty( $connected_admins ) && count( $connected_admins ) > 1 ) { + echo '
'; + echo "'; + + $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 '

'; + submit_button( esc_html__( 'Set new connection owner', 'jetpack' ), 'primary', 'jp-switch-connection-owner-submit', false ); + echo '

'; + + echo "
"; + echo '
'; + ?> + + ' . 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' ) . '

'; + $connect_url = \Jetpack::init()->build_connect_url( false, false, 'delete_connection_owner_notice' ); + echo "" . esc_html__( 'Connect to WordPress.com', 'jetpack' ) . ''; + } + + echo '

'; + printf( + wp_kses( + /* translators: URL to Jetpack support doc regarding the primary user. */ + __( "Learn more 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 '

'; + echo '

'; + printf( + wp_kses( + /* translators: URL to contact Jetpack support. */ + __( 'As always, feel free to contact our support team if you have any questions.', 'jetpack' ), + array( + 'a' => array( + 'href' => true, + 'target' => true, + 'rel' => true, + ), + ) + ), + 'https://jetpack.com/contact-support' + ); + echo '

'; + echo '
'; + } + + /** + * 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'] ); + ?> +
+ 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 = ''; + break; + case 'woocommerce': + $envelope->content->icon = ''; + 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 @@ + 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' + +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 = ''; + $text = ' + + + + + + + + '; + return sprintf( + '%2$s', + ( 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 @@ + '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 @@ + '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 @@ +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 @@ +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 @@ + '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 @@ +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 @@ +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 @@ +:( + */ + 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 @@ + '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 @@ +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 @@ +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 @@ +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 = <<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 @@ +:( + * + * @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 @@ +:( + * + * @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 @@ + 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 @@ +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 @@ +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 @@ +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 @@ +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 @@ + '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 @@ +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 @@ +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 @@ +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 @@ + '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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ + 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 @@ +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 @@ + $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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ + $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 @@ + $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 @@ +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, + ); + + } +} -- cgit v1.2.3-65-gdbad