This commit is contained in:
2026-07-02 15:54:39 -06:00
commit 9883323161
17470 changed files with 4470592 additions and 0 deletions
@@ -0,0 +1,119 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.3.11] - 2026-06-08
### Changed
- Internal updates.
## [0.3.10] - 2026-05-19
### Changed
- Internal updates.
## [0.3.9] - 2026-05-04
### Changed
- Internal: No longer require automattic/jetpack-changelogger as a per-project dev dependency. [#48225]
## [0.3.8] - 2025-09-08
### Changed
- Internal updates.
## [0.3.7] - 2025-04-28
### Changed
- Internal updates.
## [0.3.6] - 2025-03-24
### Changed
- Internal updates.
## [0.3.5] - 2025-03-12
### Changed
- Internal updates.
## [0.3.4] - 2025-03-10
### Changed
- Internal updates.
## [0.3.3] - 2025-02-24
### Changed
- Update dependencies.
## [0.3.2] - 2025-02-03
### Changed
- Internal updates.
## [0.3.1] - 2024-11-25
### Changed
- Update dependencies. [#40286]
## [0.3.0] - 2024-11-14
### Changed
- Backup: Add next daily backup schedule time on admin page. [#39914]
### Removed
- General: Update minimum PHP version to 7.2. [#40147]
## [0.2.8] - 2024-11-04
### Added
- Enable test coverage. [#39961]
## [0.2.7] - 2024-08-26
### Changed
- Updated package dependencies. [#39004]
## [0.2.6] - 2024-04-08
### Changed
- Internal updates.
## [0.2.5] - 2024-03-25
### Fixed
- Backup: change some error messages to not trigger security scanners [#36496]
## [0.2.4] - 2024-03-18
### Changed
- Internal updates.
## [0.2.3] - 2024-03-14
### Changed
- Internal updates.
## [0.2.2] - 2024-02-27
### Added
- Increasing backup version for new endpoint [#35649]
## [0.2.1] - 2024-02-08
### Fixed
- Write helper script to ABSPATH by default, just like we did before [#35508]
## [0.2.0] - 2024-01-04
### Fixed
- Backup: Add namespace versioning to Helper_Script_Manager and other classes. [#34739]
## 0.1.0 - 2023-12-13
### Fixed
- Initial release (improved helper script installer logging). [#34297]
[0.3.11]: https://github.com/Automattic/jetpack-backup-helper-script-manager/compare/v0.3.10...v0.3.11
[0.3.10]: https://github.com/Automattic/jetpack-backup-helper-script-manager/compare/v0.3.9...v0.3.10
[0.3.9]: https://github.com/Automattic/jetpack-backup-helper-script-manager/compare/v0.3.8...v0.3.9
[0.3.8]: https://github.com/Automattic/jetpack-backup-helper-script-manager/compare/v0.3.7...v0.3.8
[0.3.7]: https://github.com/Automattic/jetpack-backup-helper-script-manager/compare/v0.3.6...v0.3.7
[0.3.6]: https://github.com/Automattic/jetpack-backup-helper-script-manager/compare/v0.3.5...v0.3.6
[0.3.5]: https://github.com/Automattic/jetpack-backup-helper-script-manager/compare/v0.3.4...v0.3.5
[0.3.4]: https://github.com/Automattic/jetpack-backup-helper-script-manager/compare/v0.3.3...v0.3.4
[0.3.3]: https://github.com/Automattic/jetpack-backup-helper-script-manager/compare/v0.3.2...v0.3.3
[0.3.2]: https://github.com/Automattic/jetpack-backup-helper-script-manager/compare/v0.3.1...v0.3.2
[0.3.1]: https://github.com/Automattic/jetpack-backup-helper-script-manager/compare/v0.3.0...v0.3.1
[0.3.0]: https://github.com/Automattic/jetpack-backup-helper-script-manager/compare/v0.2.8...v0.3.0
[0.2.8]: https://github.com/Automattic/jetpack-backup-helper-script-manager/compare/v0.2.7...v0.2.8
[0.2.7]: https://github.com/Automattic/jetpack-backup-helper-script-manager/compare/v0.2.6...v0.2.7
[0.2.6]: https://github.com/Automattic/jetpack-backup-helper-script-manager/compare/v0.2.5...v0.2.6
[0.2.5]: https://github.com/Automattic/jetpack-backup-helper-script-manager/compare/v0.2.4...v0.2.5
[0.2.4]: https://github.com/Automattic/jetpack-backup-helper-script-manager/compare/v0.2.3...v0.2.4
[0.2.3]: https://github.com/Automattic/jetpack-backup-helper-script-manager/compare/v0.2.2...v0.2.3
[0.2.2]: https://github.com/Automattic/jetpack-backup-helper-script-manager/compare/v0.2.1...v0.2.2
[0.2.1]: https://github.com/Automattic/jetpack-backup-helper-script-manager/compare/v0.2.0...v0.2.1
[0.2.0]: https://github.com/Automattic/jetpack-backup-helper-script-manager/compare/v0.1.0...v0.2.0
@@ -0,0 +1,620 @@
<?php
/**
* The Jetpack Backup Helper Script Manager class (implementation).
*
* @package automattic/jetpack-backup
*/
// After changing this file, consider increasing the version number ("VXXX") in all the files using this namespace, in
// order to ensure that the specific version of this file always get loaded. Otherwise, Jetpack autoloader might decide
// to load an older/newer version of the class (if, for example, both the standalone and bundled versions of the plugin
// are installed, or in some other cases).
namespace Automattic\Jetpack\Backup\V0005;
use Exception;
use WP_Error;
use function content_url;
use function get_site_url;
use function is_wp_error;
use function set_url_scheme;
use function trailingslashit;
use function wp_generate_password;
use function wp_http_validate_url;
use function wp_schedule_single_event;
use function wp_upload_dir;
use const ABSPATH;
use const WP_CONTENT_DIR;
/**
* Manage installation, deletion and cleanup of Helper Scripts to assist with backing up Jetpack Sites.
*
* Does *not* use WP_Filesystem, because if there are permissions issues between the webserver's user and the FTP/SSH
* user, then we'll just install the helper script and do a backup/restore using FTP/SSH credentials (that we collect
* ourselves), without using WP_Filesystem in any way.
*
* Also, if we can't write that helper script somewhere (due to writes being inaccessible to the webserver's user, or
* for other reasons), we want to know about it (in the form of an error response), instead of having that helper
* script silently uploaded via FTP/SFTP, so that we could fall back to a backup/restore using credentials.
*
* Lastly, PHP provides us with better error reporting than WP_Filesystem.
*/
class Helper_Script_Manager_Impl {
/**
* Name of a directory that will be created for storing the helper script.
*/
const TEMP_DIRECTORY = 'jetpack-temp';
/**
* How long until the helper script will "expire" and refuse taking requests, in seconds.
*/
const EXPIRY_TIME = 60 * 60 * 8;
/**
* Maximum size of the helper script, in bytes.
*/
const MAX_FILESIZE = 1024 * 1024;
/**
* Associative array of possible places to install a jetpack-temp directory, along with the URL to access each.
*
* Keys specify the full path of install locations, and values point to the equivalent URL.
*
* If null, then install locations will be determined dynamically at the point of an install.
*
* @var array|null
*/
protected $custom_install_locations;
/**
* Filenames to ignore in scandir()'s return value.
*
* @var string[]
*/
protected $scandir_ignored_names = array( '.', '..' );
/**
* Header that the helper script is expected to start with.
*/
const HELPER_HEADER = "<?php /* Jetpack Backup Helper Script */\n";
/**
* Lines that will be written to README in the helper directory.
*/
const README_LINES = array(
'These files have been put on your server by Jetpack to assist with backups, restores, and scans of your ' .
'site content. They are cleaned up automatically when we no longer need them.',
'If you no longer have Jetpack connected to your site, you can delete them manually.',
'If you have questions or need assistance, please contact Jetpack Support at https://jetpack.com/support/',
'If you like to build amazing things with WordPress, you should visit automattic.com/jobs and apply to join ' .
'the fun mention this file when you apply!',
);
/**
* Data that will be written to index.php in the helper directory.
*/
const INDEX_FILE = '<?php // Silence is golden';
/**
* Create Helper Script Manager.
*
* @param array|null $custom_install_locations Associative array of possible places to install a jetpack-temp
* directory, along with the URL to access each.
*/
public function __construct( $custom_install_locations = null ) {
$this->custom_install_locations = $custom_install_locations;
}
/**
* Get either the default install locations, or the ones configured in the constructor.
*
* Has to be done late, i.e. can't be done in constructor, because in __construct() not all constants / functions
* might be available.
*
* @return array<string, string|WP_Error> Array with keys specifying the full path of install locations, and values
* either pointing to the equivalent URL, or being WP_Error if a specific path is not accessible.
*/
public function install_locations() {
if ( $this->custom_install_locations !== null ) {
return $this->custom_install_locations;
}
$abspath_url = get_site_url();
$locations = array();
// Prioritize ABSPATH first, because even though ABSPATH constant's value might be weird sometimes, it's the
// path where the PHP scripts will be most likely be able to get executed.
try {
if ( Throw_On_Errors::t_is_dir( ABSPATH ) ) {
$abspath_dir = Throw_On_Errors::t_realpath( ABSPATH );
$locations[ $abspath_dir ] = $abspath_url;
}
} catch ( Exception $exception ) {
$locations[ ABSPATH ] = new WP_Error(
'abspath_missing',
'Unable to access WordPress root "' . ABSPATH . '": ' . $exception->getMessage(),
array( 'status' => 500 )
);
}
try {
if ( Throw_On_Errors::t_is_dir( WP_CONTENT_DIR ) ) {
$wp_content_dir = Throw_On_Errors::t_realpath( WP_CONTENT_DIR );
// Using content_url() instead of WP_CONTENT_URL as it tests for whether we're using SSL.
$wp_content_url = content_url();
// I think we mess up the order in which we load things somewhere in a test, so "wp-content" and
// "wp-content/uploads/" URLs don't actually have the scheme+host part in them.
if ( ! wp_http_validate_url( $wp_content_url ) ) {
$wp_content_url = $abspath_url . $wp_content_url;
}
$locations[ $wp_content_dir ] = $wp_content_url;
}
} catch ( Exception $exception ) {
$locations[ WP_CONTENT_DIR ] = new WP_Error(
'content_path_missing',
'Unable to access content path "' . WP_CONTENT_DIR . '"' . $exception->getMessage(),
array( 'status' => 500 )
);
}
$upload_dir_info = wp_upload_dir();
$wp_uploads_dir = $upload_dir_info['basedir'];
try {
if ( Throw_On_Errors::t_is_dir( $wp_uploads_dir ) ) {
$wp_uploads_dir = Throw_On_Errors::t_realpath( $wp_uploads_dir );
$wp_uploads_url = $upload_dir_info['baseurl'];
// wp_upload_dir() doesn't check for whether we're using SSL:
//
// https://core.trac.wordpress.org/ticket/25449
//
// so set the scheme manually.
$wp_uploads_url = set_url_scheme( $wp_uploads_url );
if ( ! wp_http_validate_url( $wp_uploads_url ) ) {
$wp_uploads_url = $abspath_url . $wp_uploads_url;
}
$locations[ $wp_uploads_dir ] = $wp_uploads_url;
}
} catch ( Exception $exception ) {
$locations[ $wp_uploads_dir ] = new WP_Error(
'uploads_path_missing',
'Unable to access uploads path "' . $wp_uploads_dir . '"' . $exception->getMessage(),
array( 'status' => 500 )
);
}
return $locations;
}
/**
* Installs a Helper Script, and returns its filesystem path and access url.
*
* @param string $script_body Helper Script file contents.
*
* @return array|WP_Error Either an array containing the filesystem path ("path"), the URL ("url") of the helper
* script, and the WordPress root ("abspath"), or an instance of WP_Error.
*/
public function install_helper_script( $script_body ) {
// Check that the script body contains the correct header.
$actual_header = static::string_starts_with_substring( $script_body, static::HELPER_HEADER );
if ( true !== $actual_header ) {
return new WP_Error(
'bad_header',
'Bad helper script header: 0x' . bin2hex( $actual_header ),
array( 'status' => 400 )
);
}
// Refuse to install a Helper Script that is too large.
$helper_script_size = strlen( $script_body );
if ( $helper_script_size > static::MAX_FILESIZE ) {
return new WP_Error(
'too_big',
"Helper script is bigger ($helper_script_size bytes) " .
'than the max. size (' . static::MAX_FILESIZE . ' bytes)',
array( 'status' => 413 )
);
}
// Replace '[wp_path]' in the Helper Script with the WordPress installation location. Allows the Helper Script
// to find WordPress.
$wp_path_marker = '[wp_path]';
try {
$normalized_abspath = addslashes( Throw_On_Errors::t_realpath( ABSPATH ) );
} catch ( Exception $exception ) {
return new WP_Error(
'abspath_missing',
'Error while resolving ABSPATH "' . ABSPATH . '": ' . $exception->getMessage(),
array( 'status' => 500 )
);
}
$script_body = str_replace(
$wp_path_marker,
$normalized_abspath,
$script_body,
$wp_path_marker_replacement_count
);
if ( 0 === $wp_path_marker_replacement_count ) {
return new WP_Error(
'no_wp_path_marker',
"Helper script does not have the '$wp_path_marker' marker",
array( 'status' => 400 )
);
}
$failure_paths_and_reasons = array();
foreach ( $this->install_locations() as $directory => $url ) {
if ( is_wp_error( $url ) ) {
$failure_paths_and_reasons[] = "directory '$directory': " . $url->get_error_message();
continue;
}
try {
$installed = $this->install_to_location_or_throw( $script_body, $directory, $url );
// Always schedule a cleanup run shortly after EXPIRY_TIME.
wp_schedule_single_event(
time() + static::EXPIRY_TIME + 60,
'jetpack_backup_cleanup_helper_scripts'
);
return array(
'path' => $installed['path'],
'url' => $installed['url'],
'abspath' => Throw_On_Errors::t_realpath( ABSPATH ),
);
} catch ( Exception $exception ) {
$failure_paths_and_reasons[] = "directory '$directory' (URL '$url'): " . $exception->getMessage();
}
}
return new WP_Error(
'all_locations_failed',
'Unable to write the helper script to any install locations; ' .
'tried: ' . implode( ';', $failure_paths_and_reasons ),
array( 'status' => 500 )
);
}
/**
* Install helper script to a directory, or throw an exception.
*
* @param string $script_body Helper script's body.
* @param string $directory Candidate directory to create "jetpack-temp" in and write the helper script.
* @param string $url Base URL that the files in a directory are expected to be available at.
*
* @return string[] Array with "path" (location to the installed helper script) and "url"
* (URL of the installed helper script) keys.
* @throws Exception On I/O errors.
*/
protected function install_to_location_or_throw( $script_body, $directory, $url ) {
if ( ! Throw_On_Errors::t_is_writable( $directory ) ) {
throw new Exception( "Directory '$directory' is not writable" );
}
$temp_dir = trailingslashit( $directory ) . static::TEMP_DIRECTORY;
if ( ! Throw_On_Errors::t_is_dir( $temp_dir ) ) {
Throw_On_Errors::t_mkdir( $temp_dir );
}
$readme_path = trailingslashit( $temp_dir ) . 'README';
Throw_On_Errors::t_file_put_contents( $readme_path, implode( "\n\n", static::README_LINES ) );
$index_path = trailingslashit( $temp_dir ) . 'index.php';
Throw_On_Errors::t_file_put_contents( $index_path, static::INDEX_FILE );
$file_key = wp_generate_password( 10, false );
$file_name = 'jp-helper-' . $file_key . '.php';
$file_path = trailingslashit( $temp_dir ) . $file_name;
// Very unlikely, but check nonetheless.
if ( Throw_On_Errors::t_file_exists( $file_path ) ) {
throw new Exception( "Helper script at '$file_path' already exists" );
}
Throw_On_Errors::t_file_put_contents( $file_path, $script_body );
return array(
'path' => $file_path,
'url' => trailingslashit( $url ) . trailingslashit( static::TEMP_DIRECTORY ) . $file_name,
);
}
/**
* Ensure that the helper script is gone (by deleting it, if needed).
*
* @param string $path Path to the helper script to delete.
*
* @return true|WP_Error True if the file helper script is gone (either it got deleted, or it was never there), or
* WP_Error instance on deletion failures.
*/
public function delete_helper_script( $path ) {
try {
$this->delete_helper_script_or_throw( $path );
} catch ( Exception $exception ) {
return new WP_Error(
'deletion_failure',
"Unable to delete helper script at '$path': " . $exception->getMessage(),
array( 'status' => 500 )
);
}
return true;
}
/**
* Ensure that the helper script is gone (by deleting it, if needed), throw an exception on errors.
*
* @param string $path Path to the helper script to delete.
*
* @return void
* @throws Exception On deletion failures.
*/
protected function delete_helper_script_or_throw( $path ) {
if ( ! Throw_On_Errors::t_file_exists( $path ) ) {
return;
}
if ( ! Throw_On_Errors::t_is_readable( $path ) ) {
throw new Exception( "File '$path' is not readable" );
}
if ( ! Throw_On_Errors::t_is_writable( $path ) ) {
throw new Exception( "File '$path' is not writable" );
}
$helper_script_size = Throw_On_Errors::t_filesize( $path );
// Check this file looks like a JPR helper script.
$helper_header_size = strlen( static::HELPER_HEADER );
if ( $helper_script_size < $helper_header_size ) {
throw new Exception(
"Helper script is smaller ($helper_script_size bytes) " .
"than the expected header ($helper_header_size bytes)"
);
}
if ( $helper_script_size > static::MAX_FILESIZE ) {
throw new Exception(
"Helper script is bigger ($helper_script_size bytes) " .
'than the max. size (' . static::MAX_FILESIZE . ' bytes)'
);
}
$actual_header = static::verify_file_header( $path, static::HELPER_HEADER );
if ( true !== $actual_header ) {
throw new Exception( 'Bad helper script header: 0x' . bin2hex( $actual_header ) );
}
Throw_On_Errors::t_unlink( $path );
$this->delete_helper_directory_if_empty( dirname( $path ) );
}
/**
* Search for Helper Scripts that are suspiciously old, and clean them out.
*
* @return true|WP_Error True if all expired helper scripts got cleaned up successfully, or an instance of
* WP_Error if one or more expired helper scripts didn't manage to get cleaned up.
*/
public function cleanup_expired_helper_scripts() {
try {
$this->cleanup_helper_scripts( time() - static::EXPIRY_TIME );
} catch ( Exception $exception ) {
return new WP_Error(
'cleanup_failed',
'Unable to clean up expired helper scripts: ' . $exception->getMessage(),
array( 'status' => 500 )
);
}
return true;
}
/**
* Search for and delete all Helper Scripts. Used during uninstallation.
*
* @return true|WP_Error True if all helper scripts got deleted successfully, or an instance of WP_Error if one or
* more helper scripts didn't manage to get deleted.
*/
public function delete_all_helper_scripts() {
try {
$this->cleanup_helper_scripts();
} catch ( Exception $exception ) {
return new WP_Error(
'cleanup_failed',
'Unable to clean up all helper scripts: ' . $exception->getMessage(),
array( 'status' => 500 )
);
}
return true;
}
/**
* Search for and delete Helper Scripts. If an $expiry_time is specified, only delete Helper Scripts
* with a mtime older than $expiry_time. Otherwise, delete them all.
*
* @param int|null $expiry_time If specified, only delete scripts older than this UNIX timestamp.
*
* @return void
* @throws Exception If one or more helper scripts doesn't manage to get cleaned up.
*/
protected function cleanup_helper_scripts( $expiry_time = null ) {
$error_messages = array();
foreach ( $this->install_locations() as $directory => $url ) {
if ( is_wp_error( $url ) ) {
$error_messages[] = $url->get_error_message();
continue;
}
$temp_dir = trailingslashit( trailingslashit( $directory ) . static::TEMP_DIRECTORY );
if ( Throw_On_Errors::t_is_dir( $temp_dir ) ) {
// Find expired helper scripts and delete them.
$temp_dir_contents = Throw_On_Errors::t_scandir( $temp_dir );
foreach ( $temp_dir_contents as $name ) {
if ( in_array( $name, $this->scandir_ignored_names, true ) ) {
continue;
}
$full_path = $temp_dir . $name;
$last_modified = Throw_On_Errors::t_filemtime( $full_path );
if ( preg_match( '/^jp-helper-.*\.php$/', $name ) ) {
if ( null === $expiry_time || $last_modified < $expiry_time ) {
try {
$this->delete_helper_script_or_throw( $full_path );
} catch ( Exception $exception ) {
$error_messages[] = $exception->getMessage();
}
}
}
}
// Delete the directory if it's empty now.
$this->delete_helper_directory_if_empty( $temp_dir );
}
}
if ( count( $error_messages ) > 0 ) {
throw new Exception(
'Unable to clean up one or more helper scripts: ' . implode( ';', $error_messages )
);
}
}
/**
* Delete a helper script directory if it's empty.
*
* @param string $dir Path to the helper script directory.
*
* @return bool True if the directory is missing, or was empty and got deleted; false if directory still contains
* something and wasn't deleted.
* @throws Exception On I/O errors.
*/
protected function delete_helper_directory_if_empty( $dir ) {
if ( ! Throw_On_Errors::t_is_dir( $dir ) ) {
return true;
}
// Check that the only remaining files are a README and index.php generated by this system.
$allowed_files_and_headers = array(
'README' => static::README_LINES[0],
'index.php' => static::INDEX_FILE,
);
$dir_contents = Throw_On_Errors::t_scandir( $dir );
if ( count( $dir_contents ) > count( $allowed_files_and_headers ) + count( $this->scandir_ignored_names ) ) {
return false;
}
foreach ( $dir_contents as $name ) {
if ( in_array( $name, $this->scandir_ignored_names, true ) ) {
continue;
}
$full_path = trailingslashit( $dir ) . $name;
if ( ! isset( $allowed_files_and_headers[ $name ] ) ) {
return false;
}
// Verify the file starts with the expected contents.
$actual_header = static::verify_file_header( $full_path, $allowed_files_and_headers[ $name ] );
if ( true !== $actual_header ) {
throw new Exception( "Bad header for file '$full_path': 0x" . bin2hex( $actual_header ) );
}
Throw_On_Errors::t_unlink( $full_path );
}
// If the directory is now empty, delete it.
$dir_contents_after_cleanup = Throw_On_Errors::t_scandir( $dir );
if ( count( $dir_contents_after_cleanup ) <= count( $this->scandir_ignored_names ) ) {
Throw_On_Errors::t_rmdir( $dir );
}
return true;
}
/**
* Test if string starts with a substring, and if it doesn't, return the actual prefix.
*
* @param string $string String to search in.
* @param string $expected_prefix Expected prefix.
*
* @return bool|string True if string starts with a substring, or the actual prefix that was found instead of the
* expected prefix.
*/
protected static function string_starts_with_substring( $string, $expected_prefix ) {
$actual_prefix = substr( $string, 0, strlen( $expected_prefix ) );
if ( $actual_prefix !== $expected_prefix ) {
return $actual_prefix;
}
return true;
}
/**
* Verify that a file exists, is readable, and has the expected header.
*
* @param string $path File to verify.
* @param string $expected_header Header that the file should have.
*
* @return bool|string True if header matches, or an actual header if it doesn't match.
* @throws Exception If the file doesn't exist, isn't readable, or is of the wrong size.
*/
protected static function verify_file_header( $path, $expected_header ) {
if ( ! Throw_On_Errors::t_file_exists( $path ) ) {
throw new Exception( "File '$path' does not exist" );
}
if ( ! Throw_On_Errors::t_is_readable( $path ) ) {
throw new Exception( "File '$path' is not readable" );
}
$file_size = Throw_On_Errors::t_filesize( $path );
// Check this file looks like a JPR helper script.
$expected_header_size = strlen( $expected_header );
if ( $file_size < $expected_header_size ) {
throw new Exception(
"File is smaller ($file_size bytes) " .
"than the expected header ($expected_header_size bytes)"
);
}
if ( $file_size > static::MAX_FILESIZE ) {
throw new Exception(
"File is bigger ($file_size bytes) " .
'than the max. size (' . static::MAX_FILESIZE . ' bytes)'
);
}
$file_contents = Throw_On_Errors::t_file_get_contents( $path );
return static::string_starts_with_substring( $file_contents, $expected_header );
}
}
@@ -0,0 +1,89 @@
<?php
/**
* Jetpack Backup Helper Script Manager class (static wrapper).
*
* @package automattic/jetpack-backup
*/
// After changing this file, consider increasing the version number ("VXXX") in all the files using this namespace, in
// order to ensure that the specific version of this file always get loaded. Otherwise, Jetpack autoloader might decide
// to load an older/newer version of the class (if, for example, both the standalone and bundled versions of the plugin
// are installed, or in some other cases).
namespace Automattic\Jetpack\Backup\V0005;
/**
* Manage installation, deletion and cleanup of Helper Scripts to assist with backing up Jetpack Sites.
*
* A static wrapper around an "implementation" class so that it gets autoloaded late, and we always get the latest
* version of the class instead of a random version of it:
*
* https://github.com/Automattic/jetpack/pull/34297#discussion_r1424227489
*/
class Helper_Script_Manager {
/**
* Instance of helper script manager implementation, or null if not initialized yet.
*
* @var Helper_Script_Manager_Impl|null
*/
protected static $impl = null;
/**
* Initialize an instance of helper script manager implementation (if needed).
*
* @return void
*/
protected static function initialize_impl_if_needed() {
if ( null === static::$impl ) {
static::$impl = new Helper_Script_Manager_Impl();
}
}
/**
* Install a Helper Script, and returns its filesystem path and access url.
*
* @param string $script_body Helper Script file contents.
*
* @return array|\WP_Error Either an array containing the filesystem path ("path"), the URL ("url") of the helper
* script, and the WordPress root ("abspath"), or an instance of WP_Error.
*/
public static function install_helper_script( $script_body ) {
static::initialize_impl_if_needed();
return static::$impl->install_helper_script( $script_body );
}
/**
* Ensure that the helper script is gone (by deleting it, if needed).
*
* @param string $path Path to the helper script to delete.
*
* @return true|\WP_Error True if the file helper script is gone (either it got deleted, or it was never there), or
* WP_Error instance on deletion failures.
*/
public static function delete_helper_script( $path ) {
static::initialize_impl_if_needed();
return static::$impl->delete_helper_script( $path );
}
/**
* Search for Helper Scripts that are suspiciously old, and clean them out.
*
* @return true|\WP_Error True if all expired helper scripts got cleaned up successfully, or an instance of
* WP_Error if one or more expired helper scripts didn't manage to get cleaned up.
*/
public static function cleanup_expired_helper_scripts() {
static::initialize_impl_if_needed();
return static::$impl->cleanup_expired_helper_scripts();
}
/**
* Search for and delete all Helper Scripts. Used during uninstallation.
*
* @return true|\WP_Error True if all helper scripts got deleted successfully, or an instance of WP_Error if one or
* more helper scripts didn't manage to get deleted.
*/
public static function delete_all_helper_scripts() {
static::initialize_impl_if_needed();
return static::$impl->delete_all_helper_scripts();
}
}
@@ -0,0 +1,498 @@
<?php // phpcs:disable Squiz.Commenting.FileComment.Missing
// phpcs:disable WordPress.PHP.DevelopmentFunctions.prevent_path_disclosure_error_reporting
// phpcs:disable WordPress.PHP.DiscouragedPHPFunctions.runtime_configuration_error_reporting
// phpcs:disable WordPress.PHP.IniSet.display_errors_Disallowed
// After changing this file, consider increasing the version number ("VXXX") in all the files using this namespace, in
// order to ensure that the specific version of this file always get loaded. Otherwise, Jetpack autoloader might decide
// to load an older/newer version of the class (if, for example, both the standalone and bundled versions of the plugin
// are installed, or in some other cases).
namespace Automattic\Jetpack\Backup\V0005;
use Exception;
use Throwable;
/**
* Wrappers for functions which throw an exception on errors and warnings instead of silently continuing operation.
*
* PHP is pretty lax with error reporting when doing I/O operations, e.g. a typical I/O helper function returns false,
* null, and/or emits a warning, but the whole PHP application continues operation. It's for the caller of the I/O
* helper function to test the returned result of the function, and notice + act upon I/O errors.
*
* We really want to know about each and every I/O error. Therefore, this class provides wrappers for some common
* (mostly) I/O operations that throw an exception on errors instead of just returning false, null, or something else.
* This wrapper class treats warnings as errors too.
*
* Given that static method names are similar to the ones used by PHP, they're prefixed with "t_" to not erroneously
* trigger various security scanners.
*/
class Throw_On_Errors {
/**
* Execute a callable, throw an exception (together with a descriptive label) on PHP warnings / errors.
*
* @param callable $callable Callable to execute.
* @param string $label Label to add to the thrown exception to clarify what was attempted.
*
* @return mixed Callable's return value, if any.
* @throws Exception On warnings thrown by the callable.
* @noinspection PhpUnusedParameterInspection
*/
private static function throw_on_warnings( $callable, $label ) {
$old_error_reporting = error_reporting( - 1 );
$old_display_errors = ini_set( 'display_errors', 'stderr' );
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_set_error_handler
set_error_handler(
/**
* Temporary error handler.
*
* @see https://php-legacy-docs.zend.com/manual/php5/en/function.set-error-handler
* @see https://www.php.net/manual/en/function.set-error-handler.php
*
* @param int $errno Level of the error raised.
* @param string $errstr Error message.
* @param string|null $errfile Filename that the error was raised in.
* @param int|null $errline Line number where the error was raised.
* @param array|null $errcontext Deprecated, unused.
*
* @return never
* @throws Exception
*/
// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
function ( $errno, $errstr, $errfile = null, $errline = null, $errcontext = null ) {
throw new Exception( "$errstr (file: $errfile; line: $errline)" );
}
);
$result = null;
$error_message = null;
try {
$result = $callable();
} catch ( Throwable $throwable ) {
$error_message = $throwable->getMessage();
}
restore_error_handler();
ini_set( 'display_errors', $old_display_errors );
error_reporting( $old_error_reporting );
if ( $error_message !== null ) {
throw new Exception( "$label failed: $error_message" );
}
return $result;
}
/**
* Return canonicalized absolute pathname, throw on warnings / errors.
*
* @see https://php-legacy-docs.zend.com/manual/php5/en/function.realpath
* @see https://www.php.net/manual/en/function.realpath.php
*
* @param string $path Path being checked.
*
* @return string Canonicalized absolute pathname
* @throws Exception On invalid parameters, or if realpath() has returned false or thrown warnings.
*/
public static function t_realpath( $path ) {
// PHP 5.x won't complain about parameter being unset, so let's do it ourselves.
if ( ! $path ) {
throw new Exception( 'Filename for realpath() is unset' );
}
$label = "realpath( '$path' )";
$realpath_result = static::throw_on_warnings(
function () use ( $path ) {
return realpath( $path );
},
$label
);
if ( false === $realpath_result ) {
throw new Exception( "Unable to $label" );
}
return $realpath_result;
}
/**
* Check whether a file or directory exists, throw on warnings / errors.
*
* @see https://php-legacy-docs.zend.com/manual/php5/en/function.file-exists
* @see https://www.php.net/manual/en/function.file-exists.php
*
* @param string $filename Path to the file or directory.
*
* @return bool True if the file or directory specified by filename exists; false otherwise.
* @throws Exception On invalid parameters, or if file_exists() has thrown warnings.
*/
public static function t_file_exists( $filename ) {
// PHP 5.x won't complain about parameter being unset, so let's do it ourselves.
if ( ! $filename ) {
throw new Exception( 'Filename for file_exists() is unset' );
}
return static::throw_on_warnings(
function () use ( $filename ) {
return file_exists( $filename );
},
"file_exists( '$filename' )"
);
}
/**
* Tell whether the filename (or a directory) is readable, throw on warnings / errors.
*
* @see https://php-legacy-docs.zend.com/manual/php5/en/function.is-readable
* @see https://www.php.net/manual/en/function.is-readable.php
*
* @param string $filename Filename (or directory) to check.
*
* @return bool True if the filename (or a directory) exists and is readable, false otherwise.
* @throws Exception On invalid parameters, or if is_readable() has thrown warnings.
*/
public static function t_is_readable( $filename ) {
// PHP 5.x won't complain about parameter being unset, so let's do it ourselves.
if ( ! $filename ) {
throw new Exception( 'Filename for is_readable() is unset' );
}
return static::throw_on_warnings(
function () use ( $filename ) {
return is_readable( $filename );
},
"is_readable( '$filename' )"
);
}
/**
* Tell whether the filename (or a directory) is writable, throw on warnings / errors.
*
* @see https://php-legacy-docs.zend.com/manual/php5/en/function.is-writable
* @see https://www.php.net/manual/en/function.is-writable.php
*
* @param string $filename Filename (or directory) to check.
*
* @return bool True if the filename (or a directory) exists and is writable, false otherwise.
* @throws Exception On invalid parameters, or if is_writable() has thrown warnings.
*/
public static function t_is_writable( $filename ) {
// PHP 5.x won't complain about parameter being unset, so let's do it ourselves.
if ( ! $filename ) {
throw new Exception( 'Filename for is_writable() is unset' );
}
return static::throw_on_warnings(
function () use ( $filename ) {
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_is_writable
return is_writable( $filename );
},
"is_writable( '$filename' )"
);
}
/**
* Get file size, throw on warnings / errors.
*
* @see https://php-legacy-docs.zend.com/manual/php5/en/function.filesize
* @see https://www.php.net/manual/en/function.filesize.php
*
* @param string $filename Path to the file.
*
* @return int Size of the file in bytes
* @throws Exception On invalid parameters, or if filesize() has thrown warnings.
*/
public static function t_filesize( $filename ) {
// PHP 5.x won't complain about parameter being unset, so let's do it ourselves.
if ( ! $filename ) {
throw new Exception( 'Filename for filesize() is unset' );
}
$label = "filesize( '$filename' )";
$filesize_result = static::throw_on_warnings(
function () use ( $filename ) {
return filesize( $filename );
},
$label
);
if ( false === $filesize_result ) {
throw new Exception( "Unable to $label" );
}
return $filesize_result;
}
/**
* Get file modification time, throw on warnings / errors.
*
* @see https://php-legacy-docs.zend.com/manual/php5/en/function.filemtime
* @see https://www.php.net/manual/en/function.filemtime.php
*
* @param string $filename Path to the file.
*
* @return int The time the file was last modified
* @throws Exception On invalid parameters, or if filemtime() has thrown warnings.
*/
public static function t_filemtime( $filename ) {
// PHP 5.x won't complain about parameter being unset, so let's do it ourselves.
if ( ! $filename ) {
throw new Exception( 'Filename for filemtime() is unset' );
}
$label = "filemtime( '$filename' )";
$filemtime_result = static::throw_on_warnings(
function () use ( $filename ) {
return filemtime( $filename );
},
$label
);
if ( false === $filemtime_result ) {
throw new Exception( "Unable to $label" );
}
return $filemtime_result;
}
/**
* Tell whether the filename is a directory (follow symlinks), throw on warnings / errors.
*
* @see https://php-legacy-docs.zend.com/manual/php5/en/function.is-dir
* @see https://www.php.net/manual/en/function.is-dir.php
*
* @param string $filename Path to the file.
*
* @return bool True if the filename (or the symlink's target) exists and is a directory, false otherwise.
* @throws Exception On invalid parameters, if is_dir() has thrown warnings, or has failed.
*/
public static function t_is_dir( $filename ) {
// PHP 5.x won't complain about parameter being unset, so let's do it ourselves.
if ( ! $filename ) {
throw new Exception( 'Filename for is_dir() is unset' );
}
return static::throw_on_warnings(
function () use ( $filename ) {
return is_dir( $filename );
},
"is_dir( '$filename' )"
);
}
/**
* Make a directory, throw on warnings / errors.
*
* @see https://php-legacy-docs.zend.com/manual/php5/en/function.mkdir
* @see https://www.php.net/manual/en/function.mkdir.php
*
* @param string $directory Directory path.
* @param int $permissions Permissions of the newly created directory.
* @param bool $recursive If true, then any parent directories to the directory specified will also be created,
* with the same permissions.
*
* @return void
* @throws Exception On invalid parameters, if mkdir() has thrown warnings, or has failed.
*/
public static function t_mkdir( $directory, $permissions = 0777, $recursive = false ) {
// PHP 5.x won't complain about permissions being null, so let's do it ourselves.
if ( $permissions === null ) {
throw new Exception( 'Permissions for mkdir() are unset' );
}
$label = "mkdir( '$directory', 0" . decoct( $permissions ) . ', ' . ( $recursive ? 'true' : 'false' ) . ' )';
$mkdir_result = static::throw_on_warnings(
function () use ( $directory, $permissions, $recursive ) {
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_mkdir
return mkdir( $directory, $permissions, $recursive );
},
$label
);
if ( false === $mkdir_result ) {
throw new Exception( "Unable to $label" );
}
}
/**
* List files and directories inside the specified path, throw on warnings / errors.
*
* @see https://php-legacy-docs.zend.com/manual/php5/en/function.scandir
* @see https://www.php.net/manual/en/function.scandir.php
*
* @param string $directory Directory that will be scanned.
*
* @return string[] An array of filenames.
* @throws Exception If scandir() has thrown warnings, or has failed.
*/
public static function t_scandir( $directory ) {
// PHP 5.x won't complain about parameter being unset, so let's do it ourselves.
if ( ! $directory ) {
throw new Exception( 'Directory for scandir() is unset' );
}
$label = "scandir( '$directory' )";
$scandir_result = static::throw_on_warnings(
function () use ( $directory ) {
return scandir( $directory );
},
$label
);
if ( false === $scandir_result ) {
throw new Exception( "Unable to $label" );
}
return $scandir_result;
}
/**
* Remove a directory, throw on warnings / errors.
*
* @see https://php-legacy-docs.zend.com/manual/php5/en/function.rmdir
* @see https://www.php.net/manual/en/function.rmdir.php
*
* @param string $directory Directory path.
*
* @return void
* @throws Exception On invalid parameters, if rmdir() has thrown warnings, or has failed.
*/
public static function t_rmdir( $directory ) {
// PHP 5.x won't complain about parameter being unset, so let's do it ourselves.
if ( ! $directory ) {
throw new Exception( 'Directory for mkdir() is unset' );
}
$label = "rmdir( '$directory' )";
$rmdir_result = static::throw_on_warnings(
function () use ( $directory ) {
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_rmdir
return rmdir( $directory );
},
$label
);
if ( false === $rmdir_result ) {
throw new Exception( "Unable to $label" );
}
}
/**
* Delete a file, throw on warnings / errors.
*
* @see https://php-legacy-docs.zend.com/manual/php5/en/function.unlink
* @see https://www.php.net/manual/en/function.unlink.php
*
* @param string $filename Path to the file.
*
* @return void
* @throws Exception If unlink() has thrown warnings, or has failed.
*/
public static function t_unlink( $filename ) {
$label = "unlink( '$filename' )";
$unlink_result = static::throw_on_warnings(
function () use ( $filename ) {
// phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink
return unlink( $filename );
},
$label
);
if ( false === $unlink_result ) {
throw new Exception( "Unable to $label" );
}
}
/**
* Write data to a file, throw on warnings / errors.
*
* @see https://php-legacy-docs.zend.com/manual/php5/en/function.file-put-contents
* @see https://www.php.net/manual/en/function.file-put-contents.php
*
* @param string $filename Path to the file where to write the data.
* @param string $data The data to write.
*
* @return void
* @throws Exception If file_put_contents() has thrown warnings, has failed, or if it didn't write all the bytes.
*/
public static function t_file_put_contents( $filename, $data ) {
// PHP 5.x won't complain about parameter being unset, so let's do it ourselves.
if ( ! $filename ) {
throw new Exception( 'Filename for f_p_c() is unset' );
}
if ( $data === null ) {
throw new Exception( 'Data to write is null' );
}
$data_length = strlen( $data );
// Weird label is intentional, otherwise security scanners find this label suspicious.
$label = "f_p_c( '$filename', $data_length bytes of data )";
$number_of_bytes_written = static::throw_on_warnings(
function () use ( $filename, $data ) {
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
return file_put_contents( $filename, $data );
},
$label
);
if ( false === $number_of_bytes_written ) {
throw new Exception( "Unable to $label" );
}
if ( $number_of_bytes_written !== $data_length ) {
throw new Exception(
"$label was expected to write $data_length bytes, but wrote $number_of_bytes_written bytes"
);
}
}
/**
* Read entire file into a string, throw on warnings / errors.
*
* @see https://php-legacy-docs.zend.com/manual/php5/en/function.file-get-contents
* @see https://www.php.net/manual/en/function.file-get-contents.php
*
* @param string $filename Name of the file to read.
*
* @return string The read data.
* @throws Exception If file_get_contents() has thrown warnings, or has failed.
*/
public static function t_file_get_contents( $filename ) {
// PHP 5.x won't complain about parameter being unset, so let's do it ourselves.
if ( ! $filename ) {
throw new Exception( 'Filename for f_g_c() is unset' );
}
// Weird label is intentional, otherwise security scanners find this label suspicious.
$label = "f_g_c( '$filename' )";
$fgc_result = static::throw_on_warnings(
function () use ( $filename ) {
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
return file_get_contents( $filename );
},
$label
);
if ( false === $fgc_result ) {
throw new Exception( "Unable to $label" );
}
return $fgc_result;
}
}