initial
This commit is contained in:
+119
@@ -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
|
||||
+620
@@ -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 );
|
||||
}
|
||||
}
|
||||
+89
@@ -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();
|
||||
}
|
||||
}
|
||||
+498
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user