if (!defined('UPDRAFTPLUS_DIR')) die('No direct access allowed');
public $plugin_title = 'UpdraftPlus Backup/Restore';
// Choices will be shown in the admin menu in the order used here
public $backup_methods = array(
'updraftvault' => 'UpdraftPlus Vault',
'cloudfiles' => 'Rackspace Cloud Files',
'googledrive' => 'Google Drive',
'onedrive' => 'Microsoft OneDrive',
'azure' => 'Microsoft Azure',
'googlecloud' => 'Google Cloud',
'backblaze' => 'Backblaze',
's3generic' => 'S3-Compatible (Generic)',
'openstack' => 'OpenStack (Swift)',
'dreamobjects' => 'DreamObjects',
public $errors = array();
public $logfile_name = "";
public $logfile_handle = false;
public $something_useful_happened = false;
public $have_addons = false;
// Used to schedule resumption attempts beyond the tenth, if needed
public $current_resumption;
public $newresumption_scheduled = false;
public $resumption_scheduled_for_cleanup = false;
public $cpanel_quota_readable = false;
public $error_reporting_stop_when_logged = false;
private $combine_jobs_around;
private $remotestorage_extrainfo = array();
public $no_checkin_last_time;
public function __construct() {
// Initialisation actions - takes place on plugin load
if ($fp = fopen(UPDRAFTPLUS_DIR.'/updraftplus.php', 'r')) {
$file_data = fread($fp, 1024);
if (preg_match("/Version: ([\d\.]+)(\r|\n)/", $file_data, $matches)) {
$this->version = $matches[1];
'UpdraftPlus_Backup_History' => 'includes/class-backup-history.php',
'UpdraftPlus_Encryption' => 'includes/class-updraftplus-encryption.php',
'UpdraftPlus_Manipulation_Functions' => 'includes/class-manipulation-functions.php',
'UpdraftPlus_Filesystem_Functions' => 'includes/class-filesystem-functions.php',
'UpdraftPlus_Storage_Methods_Interface' => 'includes/class-storage-methods-interface.php',
'UpdraftPlus_Job_Scheduler' => 'includes/class-job-scheduler.php',
foreach ($load_classes as $class => $relative_path) {
if (!class_exists($class)) include_once(UPDRAFTPLUS_DIR.'/'.$relative_path);
add_action('init', array($this, 'handle_url_actions'));
add_action('init', array($this, 'updraftplus_single_site_maintenance_init'));
// Run earlier than default - hence earlier than other components
// admin_menu runs earlier, and we need it because options.php wants to use $updraftplus_admin before admin_init happens
add_action(apply_filters('updraft_admin_menu_hook', 'admin_menu'), array($this, 'admin_menu'), 9);
// Not a mistake: admin-ajax.php calls only admin_init and not admin_menu
add_action('admin_init', array($this, 'admin_menu'), 9);
add_action('admin_init', array($this, 'wordpress_55_updates_potential_migration'));
// The two actions which we schedule upon
add_action('updraft_backup', array($this, 'backup_files'));
add_action('updraft_backup_database', array($this, 'backup_database'));
// The three actions that can be called from "Backup Now"
add_action('updraft_backupnow_backup', array($this, 'backupnow_files'));
add_action('updraft_backupnow_backup_database', array($this, 'backupnow_database'));
add_action('updraft_backupnow_backup_all', array($this, 'backup_all'));
// backup_all as an action is legacy (Oct 2013) - there may be some people who wrote cron scripts to use it
add_action('updraft_backup_all', array($this, 'backup_all'));
// This is our runs-after-backup event, whose purpose is to see if it succeeded or failed, and resume/mom-up etc.
add_action('updraft_backup_resume', array($this, 'backup_resume'), 10, 3);
// If files + db are on different schedules but are scheduled for the same time, then combine them
add_filter('schedule_event', array($this, 'schedule_event'));
add_action('plugins_loaded', array($this, 'plugins_loaded'));
// Since the WordPress version 5.5, we are no longer forcing an auto update by hooking the auto_update_plugin filter because WordPress does something different to its auto-update interface
if (version_compare($this->get_wordpress_version(), '5.5', '<')) {
add_filter('auto_update_plugin', array($this, 'maybe_auto_update_plugin'), 20, 2);
// Prevent iThemes Security from telling people that they have no backups (and advertising them another product on that basis!)
add_filter('itsec_has_external_backup', '__return_true', 999);
add_filter('itsec_external_backup_link', array($this, 'itsec_external_backup_link'), 999);
add_filter('itsec_scheduled_external_backup', array($this, 'itsec_scheduled_external_backup'), 999);
add_action('updraft_report_remotestorage_extrainfo', array($this, 'report_remotestorage_extrainfo'), 10, 3);
// Prevent people using WP < 5.5 upgrading from being baffled by WP's obscure error message. See: https://core.trac.wordpress.org/ticket/27196
if (version_compare($this->get_wordpress_version(), '5.4.99999999', '<')) {
add_filter('upgrader_source_selection', array($this, 'upgrader_source_selection'), 10, 4);
// register_deactivation_hook(__FILE__, array($this, 'deactivation'));
if (!empty($_POST) && !empty($_GET['udm_action']) && 'vault_disconnect' == $_GET['udm_action'] && !empty($_POST['udrpc_message']) && !empty($_POST['reset_hash'])) {
add_action('wp_loaded', array($this, 'wp_loaded_vault_disconnect'), 1);
// Remove the notice on the Updates page that confuses users who already have backups installed
if ('update-core.php' == $pagenow) {
// added filter here instead of admin.php because the jetpack_just_in_time_msgs filter applied in init hook
add_filter('jetpack_just_in_time_msgs', '__return_false', 20);
* Enables automatic updates for the plugin.
* @internal uses auto_update_plugin filter
* @param Bool $update Whether the item has automatic updates enabled
* @param Object $item Object holding the asset to be updated
* @return bool True of automatic updates enabled, false if not
public function maybe_auto_update_plugin($update, $item) {
if (!isset($item->plugin) || basename(UPDRAFTPLUS_DIR).'/updraftplus.php' !== $item->plugin) return $update;
$option_auto_update_settings = (array) get_site_option('auto_update_plugins', array());
return in_array($item->plugin, $option_auto_update_settings, true);
* Called by the WP action updraft_report_remotestorage_extrainfo
* @param String $info_html - the HTML version of the extra info
* @param String $info_plain - the plain text version of the extra info
public function report_remotestorage_extrainfo($service, $info_html, $info_plain) {
$this->remotestorage_extrainfo[$service] = array('pretty' => $info_html, 'plain' => $info_plain);
* WP filter upgrader_source_selection. We use it to tweak the error message shown when an install of a new version is prevented by the existence of an existing version (i.e. us!), to give the user some actual useful information instead of WP's default.
* @param String $source File source location.
* @param String $remote_source Remote file source location.
* @param WP_Upgrader $upgrader_object WP_Upgrader instance.
* @param Array $hook_extra Extra arguments passed to hooked filters.
* @return String - filtered value
public function upgrader_source_selection($source, $remote_source, $upgrader_object, $hook_extra = array()) {// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found -- Filter use
static $been_here_already = false;
if ($been_here_already || !is_array($hook_extra) || empty($hook_extra['type']) || 'plugin' !== $hook_extra['type'] || empty($hook_extra['action']) || 'install' !== $hook_extra['action'] || empty($source) || 'updraftplus' !== basename(untrailingslashit($source)) || !class_exists('ReflectionObject')) return $source;
$been_here_already = true;
$reflect = new ReflectionObject($upgrader_object);
$properties = $reflect->getProperty('strings');
if (!$properties->isPublic() || !is_array($upgrader_object->strings) || empty($upgrader_object->strings['folder_exists'])) return $source;
$upgrader_object->strings['folder_exists'] .= ' '.__('A version of UpdraftPlus is already installed. WordPress will only allow you to install your new version after first de-installing the existing one. That is safe - all your settings and backups will be retained. So, go to the "Plugins" page, de-activate and de-install UpdraftPlus, and then try again.', 'updraftplus');
* WordPress filter itsec_scheduled_external_backup - from iThemes Security
* @return Boolean - filtered value
public function itsec_scheduled_external_backup() {
return wp_next_scheduled('updraft_backup') ? true : false;
* WordPress filter itsec_external_backup_link - from iThemes security
* @return String - filtered value
public function itsec_external_backup_link() {
return UpdraftPlus_Options::admin_page_url().'?page=updraftplus';
* This method will disconnect UpdraftVault accounts.
* @return Array - returns the saved options if an error is encountered.
public function wp_loaded_vault_disconnect() {
$opts = UpdraftPlus_Storage_Methods_Interface::update_remote_storage_options_format('updraftvault');
if (is_wp_error($opts)) {
if ('recursion' !== $opts->get_error_code()) {
$msg = "UpdraftVault (".$opts->get_error_code()."): ".$opts->get_error_message();
error_log("UpdraftPlus: $msg");
// The saved options had a problem; so, return the new ones
} elseif (!empty($opts['settings'])) {
foreach ($opts['settings'] as $storage_options) {
if (!empty($storage_options['token']) && $storage_options['token']) {
$site_id = $this->siteid();
$hash = hash('sha256', $site_id.':::'.$storage_options['token']);
if ($hash == $_POST['reset_hash']) {
$this->log('This site has been remotely disconnected from UpdraftPlus Vault');
include_once(UPDRAFTPLUS_DIR.'/methods/updraftvault.php');
$vault = new UpdraftPlus_BackupModule_updraftvault();
$vault->ajax_vault_disconnect();
// Die, as the vault method has already sent output
$this->log('An invalid request was received to disconnect this site from UpdraftPlus Vault');
echo json_encode(array('disconnected' => 0));
* Gets an RPC object, and sets some defaults on it that we always want
* @param string $indicator_name indicator name
public function get_udrpc($indicator_name = 'migrator.updraftplus.com') {
if (!class_exists('UpdraftPlus_Remote_Communications')) include_once(apply_filters('updraftplus_class_udrpc_path', UPDRAFTPLUS_DIR.'/includes/class-udrpc.php', $this->version));
$ud_rpc = new UpdraftPlus_Remote_Communications($indicator_name);
$ud_rpc->set_can_generate(true);
* Ensure that the indicated phpseclib classes are available
* @param String|Array $classes - a class, or list of classes. There used to be a second parameter with paths to include; but this is now inferred from $classes; and there's no backwards compatibility problem because sending more parameters than are used is acceptable in PHP.
* @return Boolean|WP_Error
public function ensure_phpseclib($classes = array()) {
$classes = (array) $classes;
$this->no_deprecation_warnings_on_php7();
foreach ($classes as $cl) {
if (!class_exists($cl)) $any_missing = true;
if (!$any_missing) return true;
// From phpseclib/phpseclib/phpseclib/bootstrap.php - we nullify it there, but log here instead
if (extension_loaded('mbstring')) {
// 2 - MB_OVERLOAD_STRING
// @codingStandardsIgnoreLine
if (ini_get('mbstring.func_overload') & 2) {
// We go on to try anyway, in case the caller wasn't using an affected part of phpseclib
// @codingStandardsIgnoreLine
$ret = new WP_Error('mbstring_func_overload', 'Overloading of string functions using mbstring.func_overload is not supported by phpseclib.');
$phpseclib_dir = UPDRAFTPLUS_DIR.'/vendor/phpseclib/phpseclib/phpseclib';
if (false === strpos(get_include_path(), $phpseclib_dir)) set_include_path(get_include_path().PATH_SEPARATOR.$phpseclib_dir);
foreach ($classes as $cl) {
$path = str_replace('_', '/', $cl);
if (!class_exists($cl)) include_once($phpseclib_dir.'/'.$path.'.php');
* Ugly, but necessary to prevent debug output breaking the conversation when the user has debug turned on
private function no_deprecation_warnings_on_php7() {
// PHP_MAJOR_VERSION is defined in PHP 5.2.7+
// We don't test for PHP > 7 because the specific deprecated element will be removed in PHP 8 - and so no warning should come anyway (and we shouldn't suppress other stuff until we know we need to).
// @codingStandardsIgnoreLine
if (defined('PHP_MAJOR_VERSION') && PHP_MAJOR_VERSION == 7) {
$old_level = error_reporting();
// @codingStandardsIgnoreLine
$new_level = $old_level & ~E_DEPRECATED;
if ($old_level != $new_level) error_reporting($new_level);
$this->no_deprecation_warnings = true;
* Attempt to close the connection to the browser, optionally with some output sent first, whilst continuing execution
* @param String $txt - output to send
public function close_browser_connection($txt = '') {
// Close browser connection so that it can resume AJAX polling
header('Content-Length: '.(empty($txt) ? '0' : 4+strlen($txt)));
header('Connection: close');
header('Content-Encoding: none');
if (function_exists('session_id') && session_id()) session_write_close();
// These two added - 19-Feb-15 - started being required on local dev machine, for unknown reason (probably some plugin that started an output buffer).
$ob_level = ob_get_level();
if (function_exists('fastcgi_finish_request')) fastcgi_finish_request();
* Returns the number of bytes free, if it can be detected; otherwise, false
* Presently, we only detect CPanel. If you know of others, then feel free to contribute!
public function get_hosting_disk_quota_free() {
if (!@is_dir('/usr/local/cpanel') || $this->detect_safe_mode() || !function_exists('popen') || (!@is_executable('/usr/local/bin/perl') && !@is_executable('/usr/local/cpanel/3rdparty/bin/perl')) || (defined('UPDRAFTPLUS_SKIP_CPANEL_QUOTA_CHECK') && UPDRAFTPLUS_SKIP_CPANEL_QUOTA_CHECK)) return false;// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
$perl = (@is_executable('/usr/local/cpanel/3rdparty/bin/perl')) ? '/usr/local/cpanel/3rdparty/bin/perl' : '/usr/local/bin/perl';// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
$exec = "UPDRAFTPLUSKEY=updraftplus $perl ".UPDRAFTPLUS_DIR."/includes/get-cpanel-quota-usage.pl";
$handle = function_exists('popen') ? @popen($exec, 'r') : false; // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
if (!is_resource($handle)) return false;
while (false === $found && !feof($handle) && $lines<100) {
if (preg_match('/RESULT: (\d+) (\d+) (\d+) /', $w, $matches)) {
// The manual page for pclose() claims that only -1 indicates an error, but this is untrue
if (false === $found || 0 != $ret) return false;
if ((int) $matches[2]<100 || ($matches[1] + $matches[3] != $matches[2])) return false;
$this->cpanel_quota_readable = true;
* Fetch information about the most recently modified log file
* @return Array - lists the modification time, the full path to the log file, and the log's nonce (ID)
public function last_modified_log() {
$updraft_dir = $this->backups_dir_location();
if ($handle = @opendir($updraft_dir)) {// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
while (false !== ($entry = readdir($handle))) {
// The latter match is for files created internally by zipArchive::addFile
if (preg_match('/^log\.([a-z0-9]+)\.txt$/i', $entry, $matches)) {
$mtime = filemtime($updraft_dir.'/'.$entry);
if ($mtime > $mod_time) {
$log_file = $updraft_dir.'/'.$entry;
@closedir($handle);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
return array($mod_time, $log_file, $nonce);
* This function may get called multiple times, so write accordingly
public function admin_menu() {
// We are in the admin area: now load all that code
global $updraftplus_admin;
if (empty($updraftplus_admin)) include_once(UPDRAFTPLUS_DIR.'/admin.php');
if (isset($_GET['wpnonce']) && isset($_GET['page']) && isset($_GET['action']) && 'updraftplus' == $_GET['page'] && 'downloadlatestmodlog' == $_GET['action'] && wp_verify_nonce($_GET['wpnonce'], 'updraftplus_download')) {
list($mod_time, $log_file, $nonce) = $this->last_modified_log();// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
if (is_readable($log_file)) {
header('Content-type: text/plain');
add_action('all_admin_notices', array($this, 'show_admin_warning_unreadablelog'));
add_action('all_admin_notices', array($this, 'show_admin_warning_nolog'));
* WP action http_api_curl
* @param Resource $handle A curl handle returned by curl_init()
* @return the handle (having potentially had some options set upon it)
public function http_api_curl($handle) {
if (defined('UPDRAFTPLUS_IPV4_ONLY') && UPDRAFTPLUS_IPV4_ONLY) {
curl_setopt($handle, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
* Used as a central location (to avoid repetition) to register or de-register hooks into the WP HTTP API
* @param Boolean $register - true to register, false to de-register
public function register_wp_http_option_hooks($register = true) {
add_filter('http_request_args', array($this, 'modify_http_options'));
add_action('http_api_curl', array($this, 'http_api_curl'));
remove_filter('http_request_args', array($this, 'modify_http_options'));
remove_action('http_api_curl', array($this, 'http_api_curl'));
* Used as a WordPress options filter (http_request_args)
* @param Array $opts - existing options