if (!array_key_exists($instance_id, $deletion_errors)) {
$deletion_errors[$instance_id] = array('error_code' => $deleted, 'service' => $service);
$itext = $index ? (string) $index : '';
unset($backups[$timestamp][$key]);
if ('db' == strtolower(substr($key, 0, 2))) unset($backups[$timestamp][$key][$index.'-size']);
unset($backups[$timestamp][$key][$index]);
unset($backups[$timestamp][$key.$itext.'-size']);
if (empty($backups[$timestamp][$key])) unset($backups[$timestamp][$key]);
if (isset($backups[$timestamp]['checksums']) && is_array($backups[$timestamp]['checksums'])) {
foreach (array_keys($backups[$timestamp]['checksums']) as $algo) {
unset($backups[$timestamp]['checksums'][$algo][$key.$index]);
// If we don't save the array back, then the above section will fire again for the same files - and the remote storage will be requested to delete already-deleted files, which then means no time is actually saved by the browser-backend loop method.
UpdraftPlus_Backup_History::save_history($backups);
unset($backups[$timestamp]);
if ('' != $deleted_timestamps) $deleted_timestamps .= ',';
$deleted_timestamps .= $timestamp;
UpdraftPlus_Backup_History::save_history($backups);
$timestamps_list = implode(',', $timestamps);
return $this->remove_backup_set_cleanup(true, $backups, $local_deleted, $remote_deleted, $sets_removed, $timestamps_list, $deleted_timestamps, $deletion_errors);
* This function sorts the array of instance ids currently saved so that any instance id that is in both the saved settings and the backup history move to the top of the array, as these are likely to work. Then values that don't appear in the backup history move to the bottom.
* @param String $a - the first instance id
* @param String $b - the second instance id
* @return Integer - returns an integer to indicate what position the $b value should be moved in
public function instance_ids_sort($a, $b) {
if (in_array($a, $this->backups_instance_ids)) {
if (in_array($b, $this->backups_instance_ids)) return 0;
return in_array($b, $this->backups_instance_ids) ? 1 : 0;
* Called by self::delete_set() to finish up before returning (whether the complete deletion is finished or not)
* @param Boolean $delete_complete - whether the whole set is now gone (i.e. last round)
* @param Array $backups - the backup history
* @param Integer $local_deleted - how many backup archives were deleted from local storage
* @param Integer $remote_deleted - how many backup archives were deleted from remote storage
* @param Integer $sets_removed - how many complete sets were removed
* @param String $timestamps - a csv of remaining timestamps
* @param String $deleted_timestamps - a csv of deleted timestamps
* @param Array $deletion_errors - an array of abstracted deletion errors, consisting of [error_code, service, instance]. For user notification purposes only, main error logging occurs at service.
* @return Array - information on the status, suitable for returning to the UI
public function remove_backup_set_cleanup($delete_complete, $backups, $local_deleted, $remote_deleted, $sets_removed, $timestamps, $deleted_timestamps, $deletion_errors = array()) {// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- $deletion_errors was used below but the code has been commented out. Can both be removed?
$updraftplus->register_wp_http_option_hooks(false);
UpdraftPlus_Backup_History::save_history($backups);
$updraftplus->log("Local files deleted: $local_deleted. Remote files deleted: $remote_deleted");
Disable until next release
$error_messages = array();
$storage_details = array();
foreach ($deletion_errors as $instance => $entry) {
$service = $entry['service'];
if (!array_key_exists($service, $storage_details)) {
// As errors from multiple instances of a service can be present, store the service storage object for possible use later
$new_service = UpdraftPlus_Storage_Methods_Interface::get_storage_objects_and_ids(array($service));
$storage_details = array_merge($storage_details, $new_service);
$intance_label = !empty($storage_details[$service]['instance_settings'][$instance]['instance_label']) ? $storage_details[$service]['instance_settings'][$instance]['instance_label'] : $service;
switch ($entry['error_code']) {
case 'authentication_fail':
$error_messages[] = sprintf(__("The authentication failed for '%s'.", 'updraftplus').' '.__('Please check your credentials.', 'updraftplus'), $intance_label);
case 'service_unavailable':
$error_messages[] = sprintf(__("We were unable to access '%s'.", 'updraftplus').' '.__('Service unavailable.', 'updraftplus'), $intance_label);
case 'container_access_error':
$error_messages[] = sprintf(__("We were unable to access the folder/container for '%s'.", 'updraftplus').' '.__('Please check your permissions.', 'updraftplus'), $intance_label);
case 'file_access_error':
$error_messages[] = sprintf(__("We were unable to access a file on '%s'.", 'updraftplus').' '.__('Please check your permissions.', 'updraftplus'), $intance_label);
case 'file_delete_error':
$error_messages[] = sprintf(__("We were unable to delete a file on '%s'.", 'updraftplus').' '.__('The file may no longer exist or you may not have permission to delete.', 'updraftplus'), $intance_label);
$error_messages[] = sprintf(__("An error occurred while attempting to delete from '%s'.", 'updraftplus'), $intance_label);
// $error_message_string = implode("\n", $error_messages);
$error_message_string = '';
$set_message = __('Backup sets removed:', 'updraftplus');
$local_message = __('Local files deleted:', 'updraftplus');
$remote_message = __('Remote files deleted:', 'updraftplus');
if (UpdraftPlus_Options::get_updraft_option('updraft_debug_mode')) {
return array('result' => 'success', 'set_message' => $set_message, 'local_message' => $local_message, 'remote_message' => $remote_message, 'backup_sets' => $sets_removed, 'backup_local' => $local_deleted, 'backup_remote' => $remote_deleted, 'error_messages' => $error_message_string);
return array('result' => 'continue', 'backup_local' => $local_deleted, 'backup_remote' => $remote_deleted, 'backup_sets' => $sets_removed, 'timestamps' => $timestamps, 'deleted_timestamps' => $deleted_timestamps, 'error_messages' => $error_message_string);
* Get the history status HTML and other information
* @param Boolean $rescan - whether to rescan local storage
* @param Boolean $remotescan - whether to also rescan remote storage
* @param Boolean $debug - whether to return debugging information also
* @param Integer $backup_count - a count of the total backups we want to display on the front end for use by UpdraftPlus_Backup_History::existing_backup_table()
* @return Array - the information requested
public function get_history_status($rescan, $remotescan, $debug = false, $backup_count = 0) {
if ($rescan) $messages = UpdraftPlus_Backup_History::rebuild($remotescan, false, $debug);
$backup_history = UpdraftPlus_Backup_History::get_history();
$output = UpdraftPlus_Backup_History::existing_backup_table($backup_history, $backup_count);
if (!empty($messages) && is_array($messages)) {
foreach ($messages as $msg) {
if (empty($msg['code']) || 'file-listing' != $msg['code']) {
$noutput .= '<li>'.(empty($msg['desc']) ? '' : $msg['desc'].': ').'<em>'.$msg['message'].'</em></li>';
if (!empty($msg['data'])) {
$key = $msg['method'].'-'.$msg['service_instance_id'];
$data[$key] = $msg['data'];
$output = '<div style="margin-left: 100px; margin-top: 10px;"><ul style="list-style: disc inside;">'.$noutput.'</ul></div>'.$output;
$logs_exist = (false !== strpos($output, 'downloadlog'));
list($mod_time, $log_file, $nonce) = $updraftplus->last_modified_log();// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
if ($mod_time) $logs_exist = true;
return apply_filters('updraftplus_get_history_status_result', array(
'n' => __('Existing backups', 'updraftplus').' <span class="updraft_existing_backups_count">'.count($backup_history).'</span>',
'logs_exist' => $logs_exist,
'web_server_disk_space' => UpdraftPlus_Filesystem_Functions::web_server_disk_space(true),
* Stop an active backup job
* @param String $job_id - job ID of the job to stop
* @return Array - information on the outcome of the attempt
public function activejobs_delete($job_id) {
if (preg_match("/^[0-9a-f]{12}$/", $job_id)) {
$cron = get_option('cron', array());
$jobdata = $updraftplus->jobdata_getarray($job_id);
if (!empty($jobdata['clone_job']) && !empty($jobdata['clone_id']) && !empty($jobdata['secret_token'])) {
$clone_id = $jobdata['clone_id'];
$secret_token = $jobdata['secret_token'];
$updraftplus->get_updraftplus_clone()->clone_failed_delete(array('clone_id' => $clone_id, 'secret_token' => $secret_token));
$updraft_dir = $updraftplus->backups_dir_location();
if (file_exists($updraft_dir.'/log.'.$job_id.'.txt')) touch($updraft_dir.'/deleteflag-'.$job_id.'.txt');
foreach ($cron as $time => $job) {
if (!isset($job['updraft_backup_resume'])) continue;
foreach ($job['updraft_backup_resume'] as $hook => $info) {
if (isset($info['args'][1]) && $info['args'][1] == $job_id) {
$args = $cron[$time]['updraft_backup_resume'][$hook]['args'];
wp_unschedule_event($time, 'updraft_backup_resume', $args);
if (!$found_it) return array('ok' => 'Y', 'c' => 'deleted', 'm' => __('Job deleted', 'updraftplus'));
if (!$found_it) return array('ok' => 'N', 'c' => 'not_found', 'm' => __('Could not find that job - perhaps it has already finished?', 'updraftplus'));
* Input: an array of items
* Each item is in the format: <base>,<timestamp>,<type>(,<findex>)
* The 'base' is not for us: we just pass it straight back
* @param array $downloaders Array of Items to download
public function get_download_statuses($downloaders) {
$download_status = array();
foreach ($downloaders as $downloader) {
// prefix, timestamp, entity, index
if (preg_match('/^([^,]+),(\d+),([-a-z]+|db[0-9]+),(\d+)$/', $downloader, $matches)) {
$findex = (empty($matches[4])) ? '0' : $matches[4];
$updraftplus->nonce = dechex($matches[2]).$findex.substr(md5($matches[3]), 0, 3);
$updraftplus->jobdata_reset();
$status = $this->download_status($matches[2], $matches[3], $matches[4]);
$status['base'] = $matches[1];
$status['timestamp'] = $matches[2];
$status['what'] = $matches[3];
$status['findex'] = $findex;
$download_status[] = $status;
* Get, as HTML output, a list of active jobs
* @param Array $request - details on the request being made (e.g. extra info to include)
public function get_activejobs_list($request) {
$download_status = empty($request['downloaders']) ? array() : $this->get_download_statuses(explode(':', $request['downloaders']));
if (!empty($request['oneshot'])) {
$job_id = get_site_option('updraft_oneshotnonce', false);
// print_active_job() for one-shot jobs that aren't in cron
$active_jobs = (false === $job_id) ? '' : $this->print_active_job($job_id, true);
} elseif (!empty($request['thisjobonly'])) {
// print_active_jobs() is for resumable jobs where we want the cron info to be included in the output
$active_jobs = $this->print_active_jobs($request['thisjobonly']);
$active_jobs = $this->print_active_jobs();
$logupdate_array = array();
if (!empty($request['log_fetch'])) {
if (isset($request['log_nonce'])) {
$log_nonce = $request['log_nonce'];
$log_pointer = isset($request['log_pointer']) ? absint($request['log_pointer']) : 0;
$logupdate_array = $this->fetch_log($log_nonce, $log_pointer);
// We allow the front-end to decide what to do if there's nothing logged - we used to (up to 1.11.29) send a pre-defined message
'l' => htmlspecialchars(UpdraftPlus_Options::get_updraft_lastmessage()),
'ds' => $download_status,
'automatic_updates' => $updraftplus->is_automatic_updating_enabled()
$res['hosting_restriction'] = $updraftplus->is_hosting_backup_limit_reached();
* @param Boolean|Callable $close_connection_callable
public function request_backupnow($request, $close_connection_callable = false) {
$abort_before_booting = false;
$backupnow_nocloud = !empty($request['backupnow_nocloud']);
$request['incremental'] = !empty($request['incremental']);
$entities = !empty($request['onlythisfileentity']) ? explode(',', $request['onlythisfileentity']) : array();
$remote_storage_instances = array();
// if only_these_cloud_services is not an array then all connected remote storage locations are being backed up to and we don't need to do this
if (!empty($request['only_these_cloud_services']) && is_array($request['only_these_cloud_services'])) {
$remote_storage_locations = $request['only_these_cloud_services'];
foreach ($remote_storage_locations as $key => $value) {
This name key inside the value array is the remote storage method name prefixed by 31 characters (updraft_include_remote_service_) so we need to remove them to get the actual name, then the value key inside the value array has the instance id.
$remote_storage_instances[substr($value['name'], 31)][$key] = $value['value'];
$incremental = $request['incremental'] ? apply_filters('updraftplus_prepare_incremental_run', false, $entities) : false;
// The call to backup_time_nonce() allows us to know the nonce in advance, and return it
$nonce = $updraftplus->backup_time_nonce();
'm' => apply_filters('updraftplus_backupnow_start_message', '<strong>'.__('Start backup', 'updraftplus').':</strong> '.htmlspecialchars(__('OK. You should soon see activity in the "Last log message" field below.', 'updraftplus')), $nonce)
if (!empty($request['backup_nonce']) && 'current' != $request['backup_nonce']) $msg['nonce'] = $request['backup_nonce'];
if (!empty($request['incremental']) && !$incremental) {
'error' => __('No suitable backup set (that already contains a full backup of all the requested file component types) was found, to add increments to. Aborting this backup.', 'updraftplus')
$abort_before_booting = true;
if ($close_connection_callable && is_callable($close_connection_callable)) {
call_user_func($close_connection_callable, $msg);
$updraftplus->close_browser_connection(json_encode($msg));
if ($abort_before_booting) die;
$options = array('nocloud' => $backupnow_nocloud, 'use_nonce' => $nonce);
if (!empty($request['onlythisfileentity']) && is_string($request['onlythisfileentity'])) {
// Something to see in the 'last log' field when it first appears, before the backup actually starts
$updraftplus->log(__('Start backup', 'updraftplus'));
$options['restrict_files_to_override'] = explode(',', $request['onlythisfileentity']);
if ($request['incremental'] && !$incremental) {
$updraftplus->log('An incremental backup was requested but no suitable backup found to add increments to; will proceed with a new backup');
$request['incremental'] = false;
if (!empty($request['extradata'])) $options['extradata'] = $request['extradata'];
if (!empty($remote_storage_instances)) $options['remote_storage_instances'] = $remote_storage_instances;
$options['always_keep'] = !empty($request['always_keep']);
$event = empty($request['backupnow_nofiles']) ? (empty($request['backupnow_nodb']) ? 'updraft_backupnow_backup_all' : 'updraft_backupnow_backup') : 'updraft_backupnow_backup_database';
do_action($event, apply_filters('updraft_backupnow_options', $options, $request));
* Get the contents of a log file
* @param String $backup_nonce - the backup id; or empty, for the most recently modified
* @param Integer $log_pointer - the byte count to fetch from
* @param String $output_format - the format to return in; allowed as 'html' (which will escape HTML entities in what is returned) and 'raw'
public function fetch_log($backup_nonce = '', $log_pointer = 0, $output_format = 'html') {
if (empty($backup_nonce)) {
list($mod_time, $log_file, $nonce) = $updraftplus->last_modified_log();// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
if (!preg_match('/^[0-9a-f]+$/', $nonce)) die('Security check');
$new_pointer = $log_pointer;
$updraft_dir = $updraftplus->backups_dir_location();
$potential_log_file = $updraft_dir."/log.".$nonce.".txt";
if (is_readable($potential_log_file)) {
$templog_array = array();
$log_file = fopen($potential_log_file, "r");
if ($log_pointer > 0) fseek($log_file, $log_pointer);
while (($buffer = fgets($log_file, 4096)) !== false) {
$templog_array[] = $buffer;
$templog_array[] = __('Error: unexpected file read fail', 'updraftplus');
$new_pointer = ftell($log_file);
$log_content = implode("", $templog_array);
$log_content .= __('The log file could not be read.', 'updraftplus');
$log_content .= __('The log file could not be read.', 'updraftplus');
if ('html' == $output_format) $log_content = htmlspecialchars($log_content);
'pointer' => $new_pointer
* Get a count for the number of overdue cron jobs
* @return Integer - how many cron jobs are overdue
public function howmany_overdue_crons() {
if (function_exists('_get_cron_array') || (is_file(ABSPATH.WPINC.'/cron.php') && include_once(ABSPATH.WPINC.'/cron.php') && function_exists('_get_cron_array'))) {
$crons = _get_cron_array();
foreach ($crons as $jt => $job) {
if ($jt < $timenow) $how_many_overdue++;
return $how_many_overdue;
public function get_php_errors($errno, $errstr, $errfile, $errline) {
if (0 == error_reporting()) return true;
$logline = $updraftplus->php_error_to_logline($errno, $errstr, $errfile, $errline);
if (false !== $logline) $this->logged[] = $logline;
// Don't pass it up the chain (since it's going to be output to the user always)
private function download_status($timestamp, $type, $findex) {
$response = array('m' => $updraftplus->jobdata_get('dlmessage_'.$timestamp.'_'.$type.'_'.$findex).'<br>');
if ($file = $updraftplus->jobdata_get('dlfile_'.$timestamp.'_'.$type.'_'.$findex)) {
$response['e'] = __('Download failed', 'updraftplus').'<br>';
$response['failed'] = true;
$errs = $updraftplus->jobdata_get('dlerrors_'.$timestamp.'_'.$type.'_'.$findex);
if (is_array($errs) && !empty($errs)) {
$response['e'] .= '<ul class="disc">';
foreach ($errs as $err) {
$response['e'] .= '<li>'.htmlspecialchars($err['message']).'</li>';
$response['e'] .= '<li>'.htmlspecialchars($err).'</li>';
$response['e'] .= '</ul>';