PluginProbe
UpdraftPlus: WP Backup & Migration Plugin / 1.16.5
UpdraftPlus: WP Backup & Migration Plugin v1.16.5
1.26.7 1.26.6 1.26.5 1.26.4 1.26.3 1.9.19 1.9.25 1.9.26 1.9.30 1.9.31 1.9.32 1.9.4 1.9.40 1.9.41 1.9.42 1.9.43 1.9.44 1.9.45 1.9.46 1.9.5 1.9.50 1.9.51 1.9.60 1.9.62 1.9.63 All 371 releases
updraftplus / backup.php

backup.php in UpdraftPlus: WP Backup & Migration Plugin 1.16.5, at backup.php

3,594 lines 170.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 if (!defined('UPDRAFTPLUS_DIR')) die('No direct access allowed');
4 if (!class_exists('UpdraftPlus_PclZip')) require_once(UPDRAFTPLUS_DIR.'/includes/class-zip.php');
5
6 /**
7 * This file contains code that is only needed/loaded when a backup is running
8 */
9 class UpdraftPlus_Backup {
10
11 private $index = 0;
12
13 private $manifest_path;
14
15 private $zipfiles_added;
16
17 private $zipfiles_added_thisrun = 0;
18
19 public $zipfiles_dirbatched;
20
21 public $zipfiles_batched;
22
23 public $zipfiles_skipped_notaltered;
24
25 private $zip_split_every = 419430400; // 400MB
26
27 private $zip_last_ratio = 1;
28
29 private $whichone;
30
31 private $zip_basename = '';
32
33 private $backup_basename = '';
34
35 private $zipfiles_lastwritetime;
36
37 // 0 = unknown; false = failed
38 public $binzip = 0;
39
40 private $dbhandle;
41
42 private $dbhandle_isgz;
43
44 // Array of entities => times
45 private $altered_since = -1;
46
47 // Time for the current entity
48 private $makezip_if_altered_since = -1;
49
50 private $excluded_extensions = false;
51
52 private $use_zip_object = 'UpdraftPlus_ZipArchive';
53
54 public $debug = false;
55
56 public $updraft_dir;
57
58 private $site_name;
59
60 private $wpdb_obj;
61
62 private $job_file_entities = array();
63
64 private $first_run = 0;
65
66 // Record of zip files created
67 private $backup_files_array = array();
68
69 // Used for reporting
70 private $remotestorage_extrainfo = array();
71
72 // Used when deciding to use the 'store' or 'deflate' zip storage method
73 private $extensions_to_not_compress = array();
74
75 // Append to this any skipped tables
76 private $skipped_tables;
77
78 // When initialised, a boolean
79 public $last_storage_instance;
80
81 // The absolute upper limit that will be considered for a zip batch (in bytes)
82 private $zip_batch_ceiling;
83
84 /**
85 * Class constructor
86 *
87 * @param Array|String $backup_files - files to backup, or (string)'no'
88 * @param Integer $altered_since - only backup files altered since this time (UNIX epoch time)
89 */
90 public function __construct($backup_files, $altered_since = -1) {
91
92 global $updraftplus;
93
94 $this->site_name = $this->get_site_name();
95
96 // Decide which zip engine to begin with
97 $this->debug = UpdraftPlus_Options::get_updraft_option('updraft_debug_mode');
98 $this->updraft_dir = $updraftplus->backups_dir_location();
99 $this->updraft_dir_realpath = realpath($this->updraft_dir);
100
101 add_action('updraft_report_remotestorage_extrainfo', array($this, 'report_remotestorage_extrainfo'), 10, 3);
102
103 if ('no' === $backup_files) {
104 $this->use_zip_object = 'UpdraftPlus_PclZip';
105 return;
106 }
107
108 $this->extensions_to_not_compress = array_unique(array_map('strtolower', array_map('trim', explode(',', UPDRAFTPLUS_ZIP_NOCOMPRESS))));
109
110 $this->altered_since = $altered_since;
111
112 // false means 'tried + failed'; whereas 0 means 'not yet tried'
113 // Disallow binzip on OpenVZ when we're not sure there's plenty of memory
114 if (0 === $this->binzip && (!defined('UPDRAFTPLUS_PREFERPCLZIP') || UPDRAFTPLUS_PREFERPCLZIP != true) && (!defined('UPDRAFTPLUS_NO_BINZIP') || !UPDRAFTPLUS_NO_BINZIP) && $updraftplus->current_resumption <9) {
115
116 if (@file_exists('/proc/user_beancounters') && @file_exists('/proc/meminfo') && @is_readable('/proc/meminfo')) {
117 $meminfo = @file_get_contents('/proc/meminfo', false, null, 0, 200);
118 if (is_string($meminfo) && preg_match('/MemTotal:\s+(\d+) kB/', $meminfo, $matches)) {
119 $memory_mb = $matches[1]/1024;
120 // If the report is of a large amount, then we're probably getting the total memory on the hypervisor (this has been observed), and don't really know the VPS's memory
121 $vz_log = "OpenVZ; reported memory: ".round($memory_mb, 1)." MB";
122 if ($memory_mb < 1024 || $memory_mb > 8192) {
123 $openvz_lowmem = true;
124 $vz_log .= " (will not use BinZip)";
125 }
126 $updraftplus->log($vz_log);
127 }
128 }
129 if (empty($openvz_lowmem)) {
130 $updraftplus->log('Checking if we have a zip executable available');
131 $binzip = $updraftplus->find_working_bin_zip();
132 if (is_string($binzip)) {
133 $updraftplus->log("Zip engine: found/will use a binary zip: $binzip");
134 $this->binzip = $binzip;
135 $this->use_zip_object = 'UpdraftPlus_BinZip';
136 }
137 }
138 }
139
140 // In tests, PclZip was found to be 25% slower than ZipArchive
141 if ('UpdraftPlus_PclZip' != $this->use_zip_object && empty($this->binzip) && ((defined('UPDRAFTPLUS_PREFERPCLZIP') && UPDRAFTPLUS_PREFERPCLZIP == true) || !class_exists('ZipArchive') || !class_exists('UpdraftPlus_ZipArchive') || (!extension_loaded('zip') && !method_exists('ZipArchive', 'AddFile')))) {
142 global $updraftplus;
143 $updraftplus->log("Zip engine: ZipArchive (a.k.a. php-zip) is not available or is disabled (will use PclZip (much slower) if needed)");
144 $this->use_zip_object = 'UpdraftPlus_PclZip';
145 }
146
147 $this->zip_batch_ceiling = (defined('UPDRAFTPLUS_ZIP_BATCH_CEILING') && UPDRAFTPLUS_ZIP_BATCH_CEILING > 104857600) ? UPDRAFTPLUS_ZIP_BATCH_CEILING : 200 * 1048576;
148
149 }
150
151 /**
152 * Get a site name suitable for use in the backup filename
153 *
154 * @return String
155 */
156 private function get_site_name() {
157 // Get the blog name and rip out known-problematic characters. Remember that we may need to be able to upload this to any FTP server or cloud storage, where filename support may be unknown
158 $site_name = str_replace('__', '_', preg_replace('/[^A-Za-z0-9_]/', '', str_replace(' ', '_', substr(get_bloginfo(), 0, 32))));
159 if (!$site_name || preg_match('#^_+$#', $site_name)) {
160 // Try again...
161 $parsed_url = parse_url(home_url(), PHP_URL_HOST);
162 $parsed_subdir = untrailingslashit(parse_url(home_url(), PHP_URL_PATH));
163 if ($parsed_subdir && '/' != $parsed_subdir) $parsed_url .= str_replace(array('/', '\\'), '_', $parsed_subdir);
164 $site_name = str_replace('__', '_', preg_replace('/[^A-Za-z0-9_]/', '', str_replace(' ', '_', substr($parsed_url, 0, 32))));
165 if (!$site_name || preg_match('#^_+$#', $site_name)) $site_name = 'WordPress_Backup';
166 }
167
168 // Allow an over-ride. Careful about introducing characters not supported by your filesystem or cloud storage.
169 return apply_filters('updraftplus_blog_name', $site_name);
170 }
171
172 /**
173 * Called by the WP action updraft_report_remotestorage_extrainfo
174 *
175 * @param String $service
176 * @param String $info_html
177 * @param String $info_plain
178 */
179 public function report_remotestorage_extrainfo($service, $info_html, $info_plain) {
180 $this->remotestorage_extrainfo[$service] = array('pretty' => $info_html, 'plain' => $info_plain);
181 }
182
183 /**
184 * Public, because called from the 'More Files' add-on
185 *
186 * @param String $create_from_dir Directory to create the zip
187 * @param String $whichone Entity being backed up (e.g. 'plugins', 'uploads')
188 * @param String $backup_file_basename Name of backup file
189 * @param Integer $index Index of zip in the sequence
190 * @param Integer|Boolean $first_linked_index First linked index in the sequence, or false
191 *
192 * @return Boolean
193 */
194 public function create_zip($create_from_dir, $whichone, $backup_file_basename, $index, $first_linked_index = false) {
195 // Note: $create_from_dir can be an array or a string
196 @set_time_limit(UPDRAFTPLUS_SET_TIME_LIMIT);
197 $original_index = $index;
198 $this->index = $index;
199 $this->first_linked_index = (false === $first_linked_index) ? 0 : $first_linked_index;
200
201 $this->whichone = $whichone;
202
203 global $updraftplus;
204
205 $this->zip_split_every = max((int) $updraftplus->jobdata_get('split_every'), UPDRAFTPLUS_SPLIT_MIN)*1048576;
206
207 if ('others' != $whichone) $updraftplus->log("Beginning creation of dump of $whichone (split every: ".round($this->zip_split_every/1048576, 1)." MB)");
208
209 if (is_string($create_from_dir) && !file_exists($create_from_dir)) {
210 $flag_error = true;
211 $updraftplus->log("Does not exist: $create_from_dir");
212 if ('mu-plugins' == $whichone) {
213 if (!function_exists('get_mu_plugins')) include_once(ABSPATH.'wp-admin/includes/plugin.php');
214 $mu_plugins = get_mu_plugins();
215 if (count($mu_plugins) == 0) {
216 $updraftplus->log("There appear to be no mu-plugins to backup. Will not raise an error.");
217 $flag_error = false;
218 }
219 }
220 if ($flag_error) $updraftplus->log(sprintf(__("%s - could not back this entity up; the corresponding directory does not exist (%s)", 'updraftplus'), $whichone, $create_from_dir), 'error');
221 return false;
222 }
223
224 $itext = (empty($index)) ? '' : ($index+1);
225 $base_path = $backup_file_basename.'-'.$whichone.$itext.'.zip';
226 $full_path = $this->updraft_dir.'/'.$base_path;
227 $time_now = time();
228
229 // This is compatible with filenames which indicate increments, as it is looking only for the current increment
230 if (file_exists($full_path)) {
231 // Gather any further files that may also exist
232 $files_existing = array();
233 while (file_exists($full_path)) {
234 $files_existing[] = $base_path;
235 $time_mod = (int) @filemtime($full_path);
236 $updraftplus->log($base_path.": this file has already been created (age: ".round($time_now-$time_mod, 1)." s)");
237 if ($time_mod>100 && ($time_now-$time_mod)<30) {
238 UpdraftPlus_Job_Scheduler::terminate_due_to_activity($base_path, $time_now, $time_mod);
239 }
240 $index++;
241 // This is compatible with filenames which indicate increments, as it is looking only for the current increment
242 $base_path = $backup_file_basename.'-'.$whichone.($index+1).'.zip';
243 $full_path = $this->updraft_dir.'/'.$base_path;
244 }
245 }
246
247 // Temporary file, to be able to detect actual completion (upon which, it is renamed)
248
249 // New (Jun-13) - be more aggressive in removing temporary files from earlier attempts - anything >=600 seconds old of this kind
250 UpdraftPlus_Filesystem_Functions::clean_temporary_files('_'.$updraftplus->file_nonce."-$whichone", 600);
251
252 // Firstly, make sure that the temporary file is not already being written to - which can happen if a resumption takes place whilst an old run is still active
253 $zip_name = $full_path.'.tmp';
254 $time_mod = (int) @filemtime($zip_name);
255 if (file_exists($zip_name) && $time_mod>100 && ($time_now-$time_mod)<30) {
256 UpdraftPlus_Job_Scheduler::terminate_due_to_activity($zip_name, $time_now, $time_mod);
257 }
258 if (file_exists($zip_name)) {
259 $updraftplus->log("File exists ($zip_name), but was apparently not modified within the last 30 seconds, so we assume that any previous run has now terminated (time_mod=$time_mod, time_now=$time_now, diff=".($time_now-$time_mod).")");
260 }
261
262 // Now, check for other forms of temporary file, which would indicate that some activity is going on (even if it hasn't made it into the main zip file yet)
263 // Note: this doesn't catch PclZip temporary files
264 $d = dir($this->updraft_dir);
265 $match = '_'.$updraftplus->file_nonce."-".$whichone;
266 while (false !== ($e = $d->read())) {
267 if ('.' == $e || '..' == $e || !is_file($this->updraft_dir.'/'.$e)) continue;
268 $ziparchive_match = preg_match("/$match([0-9]+)?\.zip\.tmp\.([A-Za-z0-9]){6}?$/i", $e);
269 $binzip_match = preg_match("/^zi([A-Za-z0-9]){6}$/", $e);
270 $pclzip_match = preg_match("/^pclzip-[a-z0-9]+.tmp$/", $e);
271 if ($time_now-filemtime($this->updraft_dir.'/'.$e) < 30 && ($ziparchive_match || (0 != $updraftplus->current_resumption && ($binzip_match || $pclzip_match)))) {
272 UpdraftPlus_Job_Scheduler::terminate_due_to_activity($this->updraft_dir.'/'.$e, $time_now, filemtime($this->updraft_dir.'/'.$e));
273 }
274 }
275 @$d->close();
276 clearstatcache();
277
278 if (isset($files_existing)) {
279 // Because of zip-splitting, the mere fact that files exist is not enough to indicate that the entity is finished. For that, we need to also see that no subsequent file has been started.
280 // Q. What if the previous runner died in between zips, and it is our job to start the next one? A. The next temporary file is created before finishing the former zip, so we are safe (and we are also safe-guarded by the updated value of the index being stored in the database).
281 return $files_existing;
282 }
283
284 $this->log_account_space();
285
286 $this->zip_microtime_start = microtime(true);
287
288 // The paths in the zip should then begin with '$whichone', having removed WP_CONTENT_DIR from the front
289 $zipcode = $this->make_zipfile($create_from_dir, $backup_file_basename, $whichone);
290 if (true !== $zipcode) {
291 $updraftplus->log("ERROR: Zip failure: Could not create $whichone zip (".$this->index." / $index)");
292 $updraftplus->log(sprintf(__("Could not create %s zip. Consult the log file for more information.", 'updraftplus'), $whichone), 'error');
293 // The caller is required to update $index from $this->index
294 return false;
295 } else {
296 $itext = (empty($this->index)) ? '' : ($this->index+1);
297 $full_path = $this->updraft_dir.'/'.$backup_file_basename.'-'.$whichone.$itext.'.zip';
298 if (file_exists($full_path.'.tmp')) {
299 if (@filesize($full_path.'.tmp') === 0) {
300 $updraftplus->log("Did not create $whichone zip (".$this->index.") - not needed");
301 @unlink($full_path.'.tmp');
302 } else {
303
304 $checksum_description = '';
305
306 $checksums = $updraftplus->which_checksums();
307
308 foreach ($checksums as $checksum) {
309
310 $cksum = hash_file($checksum, $full_path.'.tmp');
311 $updraftplus->jobdata_set($checksum.'-'.$whichone.$this->index, $cksum);
312 if ($checksum_description) $checksum_description .= ', ';
313 $checksum_description .= "$checksum: $cksum";
314
315 }
316
317 @rename($full_path.'.tmp', $full_path);
318 $timetaken = max(microtime(true)-$this->zip_microtime_start, 0.000001);
319 $kbsize = filesize($full_path)/1024;
320 $rate = round($kbsize/$timetaken, 1);
321 $updraftplus->log("Created $whichone zip (".$this->index.") - ".round($kbsize, 1)." KB in ".round($timetaken, 1)." s ($rate KB/s) ($checksum_description)");
322 // We can now remove any left-over temporary files from this job
323 }
324 } elseif ($this->index > $original_index) {
325 $updraftplus->log("Did not create $whichone zip (".$this->index.") - not needed (2)");
326 // Added 12-Feb-2014 (to help multiple morefiles)
327 $this->index--;
328 } else {
329 $updraftplus->log("Looked-for $whichone zip (".$this->index.") was not found (".basename($full_path).".tmp)", 'warning');
330 }
331 UpdraftPlus_Filesystem_Functions::clean_temporary_files('_'.$updraftplus->file_nonce."-$whichone", 0);
332 }
333
334 // Remove cache list files as well, if there are any
335 UpdraftPlus_Filesystem_Functions::clean_temporary_files('_'.$updraftplus->file_nonce."-$whichone", 0, true);
336
337 // Create the results array to send back (just the new ones, not any prior ones)
338 $files_existing = array();
339 $res_index = $original_index;
340 for ($i = $original_index; $i<= $this->index; $i++) {
341 $itext = empty($i) ? '' : ($i+1);
342 $full_path = $this->updraft_dir.'/'.$backup_file_basename.'-'.$whichone.$itext.'.zip';
343 if (file_exists($full_path)) {
344 $files_existing[$res_index] = $backup_file_basename.'-'.$whichone.$itext.'.zip';
345 }
346 $res_index++;
347 }
348 return $files_existing;
349 }
350
351 /**
352 * This method is for calling outside of a cloud_backup() context. It constructs a list of services for which prune operations should be attempted, and then calls prune_retained_backups() if necessary upon them.
353 */
354 public function do_prune_standalone() {
355 global $updraftplus;
356
357 $services = $updraftplus->just_one($updraftplus->jobdata_get('service'));
358 if (!is_array($services)) $services = array($services);
359
360 $prune_services = array();
361
362 foreach ($services as $ind => $service) {
363 if ("none" == $service || '' == $service) continue;
364
365 $objname = "UpdraftPlus_BackupModule_${service}";
366 if (!class_exists($objname) && file_exists(UPDRAFTPLUS_DIR.'/methods/'.$service.'.php')) {
367 include_once(UPDRAFTPLUS_DIR.'/methods/'.$service.'.php');
368 }
369 if (class_exists($objname)) {
370 $remote_obj = new $objname;
371 $pass_to_prune = null;
372 $prune_services[$service]['all'] = array($remote_obj, null);
373 } else {
374 $updraftplus->log("Could not prune from service $service: remote method not found");
375 }
376
377 }
378
379 if (!empty($prune_services)) $this->prune_retained_backups($prune_services);
380 }
381
382 /**
383 * Dispatch to the relevant function
384 *
385 * @param Array $backup_array List of archives for the backup
386 */
387 public function cloud_backup($backup_array) {
388
389 global $updraftplus;
390
391 $services = $updraftplus->just_one($updraftplus->jobdata_get('service'));
392 if (!is_array($services)) $services = array($services);
393
394 // We need to make sure that the loop below actually runs
395 if (empty($services)) $services = array('none');
396
397 $storage_objects_and_ids = UpdraftPlus_Storage_Methods_Interface::get_enabled_storage_objects_and_ids($services);
398
399 $total_instances_count = 0;
400
401 foreach ($storage_objects_and_ids as $service) {
402 if ($service['object']->supports_feature('multi_options')) $total_instances_count += count($service['instance_settings']);
403 }
404
405 $updraftplus->jobdata_set('jobstatus', 'clouduploading');
406
407 $updraftplus->register_wp_http_option_hooks();
408
409 $upload_status = $updraftplus->jobdata_get('uploading_substatus');
410 if (!is_array($upload_status) || !isset($upload_status['t'])) {
411 $upload_status = array('i' => 0, 'p' => 0, 't' => max(1, $total_instances_count)*count($backup_array));
412 $updraftplus->jobdata_set('uploading_substatus', $upload_status);
413 }
414
415 $do_prune = array();
416
417 // If there was no check-in last time, then attempt a different service first - in case a time-out on the attempted service leads to no activity and everything stopping
418 if (count($services) >1 && !empty($updraftplus->no_checkin_last_time)) {
419 $updraftplus->log('No check-in last time: will try a different remote service first');
420 array_push($services, array_shift($services));
421 // Make sure that the 'no worthwhile activity' detector isn't flumoxed by the starting of a new upload at 0%
422 if ($updraftplus->current_resumption > 9) $updraftplus->jobdata_set('uploaded_lastreset', $updraftplus->current_resumption);
423 if (1 == ($updraftplus->current_resumption % 2) && count($services)>2) array_push($services, array_shift($services));
424 }
425
426 $errors_before_uploads = $updraftplus->error_count();
427
428 foreach ($services as $ind => $service) {
429 try {
430 $instance_id_count = 0;
431 $total_instance_ids = ('none' !== $service && '' !== $service && $storage_objects_and_ids[$service]['object']->supports_feature('multi_options')) ? count($storage_objects_and_ids[$service]['instance_settings']) : 1;
432
433 // Used for logging by record_upload_chunk()
434 $this->current_service = $service;
435
436 // Used when deciding whether to delete the local file
437 $this->last_storage_instance = ($ind+1 >= count($services) && $instance_id_count+1 >= $total_instance_ids && $errors_before_uploads == $updraftplus->error_count()) ? true : false;
438 $log_extra = $this->last_storage_instance ? ' (last)' : '';
439 $updraftplus->log("Cloud backup selection (".($ind+1)."/".count($services)."): ".$service." with instance (".($instance_id_count+1)."/".$total_instance_ids.")".$log_extra);
440 @set_time_limit(UPDRAFTPLUS_SET_TIME_LIMIT);
441
442 if ('none' == $service || '' == $service) {
443 $updraftplus->log('No remote despatch: user chose no remote backup service');
444 // Still want to mark as "uploaded", to signal that nothing more needs doing. (Important on incremental runs with no cloud storage).
445 foreach ($backup_array as $bind => $file) {
446 if ($updraftplus->is_uploaded($file)) {
447 $updraftplus->log("Already uploaded: $file");
448 } else {
449 $updraftplus->uploaded_file($file, true);
450 }
451 }
452 $this->prune_retained_backups(array('none' => array('all' => array(null, null))));
453 } elseif (!empty($storage_objects_and_ids[$service]['object']) && !$storage_objects_and_ids[$service]['object']->supports_feature('multi_options')) {
454 $remote_obj = $storage_objects_and_ids[$service]['object'];
455
456 $do_prune = array_merge_recursive($do_prune, $this->upload_cloud($remote_obj, $service, $backup_array, ''));
457 } elseif (!empty($storage_objects_and_ids[$service]['instance_settings'])) {
458 foreach ($storage_objects_and_ids[$service]['instance_settings'] as $instance_id => $options) {
459
460 if ($instance_id_count > 0) {
461 $this->last_storage_instance = ($ind+1 >= count($services) && $instance_id_count+1 >= $total_instance_ids && $errors_before_uploads == $updraftplus->error_count()) ? true : false;
462 $log_extra = $this->last_storage_instance ? ' (last)' : '';
463 $updraftplus->log("Cloud backup selection (".($ind+1)."/".count($services)."): ".$service." with instance (".($instance_id_count+1)."/".$total_instance_ids.")".$log_extra);
464 }
465
466 // Used for logging by record_upload_chunk()
467 $this->current_instance = $instance_id;
468
469 if (!isset($options['instance_enabled'])) $options['instance_enabled'] = 1;
470
471 if (1 == $options['instance_enabled']) {
472 $remote_obj = $storage_objects_and_ids[$service]['object'];
473 $remote_obj->set_options($options, true, $instance_id);
474 $do_prune = array_merge_recursive($do_prune, $this->upload_cloud($remote_obj, $service, $backup_array, $instance_id));
475 } else {
476 $updraftplus->log("This instance id ($instance_id) is set as inactive.");
477 }
478
479 $instance_id_count++;
480 }
481 }
482 } catch (Exception $e) {
483 $log_message = 'Exception ('.get_class($e).') occurred during backup uploads to the '.$service.'. Exception Message: '.$e->getMessage().' (Code: '.$e->getCode().', line '.$e->getLine().' in '.$e->getFile().')';
484 $updraftplus->log($log_message);
485 error_log($log_message);
486 $updraftplus->log(sprintf(__('A PHP exception (%s) has occurred: %s', 'updraftplus'), get_class($e), $e->getMessage()), 'error');
487 // @codingStandardsIgnoreLine
488 } catch (Error $e) {
489 $log_message = 'PHP Fatal error ('.get_class($e).') has occurred during backup uploads to the '.$service.'. Error Message: '.$e->getMessage().' (Code: '.$e->getCode().', line '.$e->getLine().' in '.$e->getFile().')';
490 $updraftplus->log($log_message);
491 error_log($log_message);
492 $updraftplus->log(sprintf(__('A PHP fatal error (%s) has occurred: %s', 'updraftplus'), get_class($e), $e->getMessage()), 'error');
493 }
494 }
495
496 if (!empty($do_prune)) $this->prune_retained_backups($do_prune);
497
498 $updraftplus->register_wp_http_option_hooks(false);
499
500 }
501
502 /**
503 * This method will start the upload of the backups to the chosen remote storage method and return an array of files to be pruned and their location.
504 *
505 * @param Object $remote_obj - the remote storage object
506 * @param String $service - the name of the service we are uploading to
507 * @param Array $backup_array - an array that contains the backup files we want to upload
508 * @param String $instance_id - the instance id we are using
509 * @return Array - an array with information about what files to prune and where they are located
510 */
511 private function upload_cloud($remote_obj, $service, $backup_array, $instance_id) {
512
513 global $updraftplus;
514
515 $do_prune = array();
516
517 if ('' == $instance_id) {
518 $updraftplus->log("Beginning dispatch of backup to remote ($service)");
519 } else {
520 $updraftplus->log("Beginning dispatch of backup to remote ($service) (instance identifier $instance_id)");
521 }
522
523 $sarray = array();
524 foreach ($backup_array as $bind => $file) {
525 if ($updraftplus->is_uploaded($file, $service, $instance_id)) {
526 if ('' == $instance_id) {
527 $updraftplus->log("Already uploaded to $service: $file", 'notice', false, true);
528 } else {
529 $updraftplus->log("Already uploaded to $service / $instance_id: $file", 'notice', false, true);
530 }
531 } else {
532 $sarray[$bind] = $file;
533 }
534 }
535
536 if (count($sarray) > 0) {
537 $pass_to_prune = $remote_obj->backup($sarray);
538 if ('remotesend' != $service) {
539 $do_prune[$service][$instance_id] = array($remote_obj, $pass_to_prune);
540 } else {
541 $do_prune[$service]['default'] = array($remote_obj, $pass_to_prune);
542 }
543 } else {
544 // We still need to make sure that prune is run on this remote storage method, even if all entities were previously uploaded
545 $do_prune[$service]['all'] = array($remote_obj, null);
546 }
547
548 return $do_prune;
549 }
550
551 /**
552 * Group the backup history into sets for retention processing and indicate the retention rule to apply to each group. This is a 'default' function which just puts them all in together.
553 *
554 * @param Array $backup_history
555 *
556 * @return Array
557 */
558 private function group_backups($backup_history) {
559 return array(array('sets' => $backup_history, 'process_order' => 'keep_newest'));
560 }
561
562 /**
563 * Logs a message; with the message being logged to the database also only if that has not been done in the last 3 seconds. Useful for better overall performance on slow database servers with rapid logging.
564 *
565 * @uses UpdraftPlus::log()
566 *
567 * @param String $message - the message to log
568 * @param String $level - the log level
569 */
570 private function log_with_db_occasionally($message, $level = 'notice') {
571 global $updraftplus;
572 static $last_db = false;
573
574 if (time() > $last_db + 3) {
575 $last_db = time();
576 $skip_dblog = false;
577 } else {
578 $skip_dblog = true;
579 }
580
581 return $updraftplus->log($message, $level, false, $skip_dblog);
582 }
583
584 /**
585 * Prunes historical backups, according to the user's settings
586 *
587 * @param Array $services - An associative array with list of services as key and remote object and boolean flag as values to prune on. This must be an array (i.e. it is not flexible like some other places)
588 *
589 * @return void
590 */
591 public function prune_retained_backups($services) {
592
593 global $updraftplus, $wpdb;
594
595 if ($updraftplus->jobdata_get('remotesend_info') != '') {
596 $updraftplus->log("Prune old backups from local store: skipping, as this was a remote send operation");
597 return;
598 }
599
600 if (method_exists($wpdb, 'check_connection') && (!defined('UPDRAFTPLUS_SUPPRESS_CONNECTION_CHECKS') || !UPDRAFTPLUS_SUPPRESS_CONNECTION_CHECKS)) {
601 if (!$wpdb->check_connection(false)) {
602 UpdraftPlus_Job_Scheduler::reschedule(60);
603 $updraftplus->log('It seems the database went away; scheduling a resumption and terminating for now');
604 UpdraftPlus_Job_Scheduler::record_still_alive();
605 die;
606 }
607 }
608
609 // If they turned off deletion on local backups, then there is nothing to do
610 if (!UpdraftPlus_Options::get_updraft_option('updraft_delete_local', 1) && 1 == count($services) && array_key_exists('none', $services)) {
611 $updraftplus->log("Prune old backups from local store: nothing to do, since the user disabled local deletion and we are using local backups");
612 return;
613 }
614
615 $updraftplus->jobdata_set_multi(array('jobstatus' => 'pruning', 'prune' => 'begun'));
616
617 // Number of backups to retain - files
618 $updraft_retain = UpdraftPlus_Options::get_updraft_option('updraft_retain', 2);
619 $updraft_retain = is_numeric($updraft_retain) ? $updraft_retain : 1;
620
621 // Number of backups to retain - db
622 $updraft_retain_db = UpdraftPlus_Options::get_updraft_option('updraft_retain_db', $updraft_retain);
623 $updraft_retain_db = is_numeric($updraft_retain_db) ? $updraft_retain_db : 1;
624
625 $updraftplus->log("Retain: beginning examination of existing backup sets; user setting: retain_files=$updraft_retain, retain_db=$updraft_retain_db");
626
627 // Returns an array, most recent first, of backup sets
628 $backup_history = UpdraftPlus_Backup_History::get_history();
629 $db_backups_found = 0;
630 $file_backups_found = 0;
631
632 $ignored_because_imported = array();
633
634 // Remove non-native (imported) backups, which are neither counted nor pruned. It's neater to do these in advance, and log only one line.
635 $functional_backup_history = $backup_history;
636 foreach ($functional_backup_history as $backup_time => $backup_to_examine) {
637 if (isset($backup_to_examine['native']) && false == $backup_to_examine['native']) {
638 $ignored_because_imported[] = $backup_time;
639 unset($functional_backup_history[$backup_time]);
640 }
641 }
642 if (!empty($ignored_because_imported)) {
643 $updraftplus->log("These backup set(s) were imported from a remote location, so will not be counted or pruned. Skipping: ".implode(', ', $ignored_because_imported));
644 }
645
646 $backupable_entities = $updraftplus->get_backupable_file_entities(true);
647
648 $database_backups_found = array();
649
650 $file_entities_backups_found = array();
651 foreach ($backupable_entities as $entity => $info) {
652 $file_entities_backups_found[$entity] = 0;
653 }
654
655 if (false === ($backup_db_groups = apply_filters('updraftplus_group_backups_for_pruning', false, $functional_backup_history, 'db'))) {
656 $backup_db_groups = $this->group_backups($functional_backup_history);
657 }
658 $updraftplus->log("Number of backup sets in history: ".count($backup_history)."; groups (db): ".count($backup_db_groups));
659
660 $started_main_prune_loop_at = time();
661
662 foreach ($backup_db_groups as $group_id => $group) {
663
664 // N.B. The array returned by UpdraftPlus_Backup_History::get_history() is already sorted, with most-recent first
665
666 if (empty($group['sets']) || !is_array($group['sets'])) continue;
667 $sets = $group['sets'];
668
669 // Sort the groups into the desired "keep this first" order
670 $process_order = (!empty($group['process_order']) && 'keep_oldest' == $group['process_order']) ? 'keep_oldest' : 'keep_newest';
671 if ('keep_oldest' == $process_order) ksort($sets);
672
673 $rule = !empty($group['rule']) ? $group['rule'] : array('after-howmany' => 0, 'after-period' => 0, 'every-period' => 1, 'every-howmany' => 1);
674
675 foreach ($sets as $backup_datestamp => $backup_to_examine) {
676
677 $files_to_prune = array();
678 $nonce = empty($backup_to_examine['nonce']) ? '???' : $backup_to_examine['nonce'];
679
680 // $backup_to_examine is an array of file names, keyed on db/plugins/themes/uploads
681 // The new backup_history array is saved afterwards, so remember to unset the ones that are to be deleted
682 $this->log_with_db_occasionally(sprintf("Examining (for databases) backup set with group_id=$group_id, nonce=%s, datestamp=%s (%s)", $nonce, $backup_datestamp, gmdate('M d Y H:i:s', $backup_datestamp)));
683
684 // "Always Keep" Backups should be counted in the count of how many have been retained for purposes of the "how many to retain" count... but if that count is already matched, it's not a problem
685 $is_always_keep = !empty($backup_to_examine['always_keep']);
686
687 // Auto-backups are only counted or deleted once we have reached the retain limit - before that, they are skipped
688 $is_autobackup = !empty($backup_to_examine['autobackup']);
689
690 $remote_sent = (!empty($backup_to_examine['service']) && ((is_array($backup_to_examine['service']) && in_array('remotesend', $backup_to_examine['service'])) || 'remotesend' === $backup_to_examine['service'])) ? true : false;
691
692 $any_deleted_via_filter_yet = false;
693
694 // Databases
695 foreach ($backup_to_examine as $key => $data) {
696 if ('db' != strtolower(substr($key, 0, 2)) || '-size' == substr($key, -5, 5)) continue;
697
698 if (empty($database_backups_found[$key])) $database_backups_found[$key] = 0;
699
700 if ($nonce == $updraftplus->nonce || $nonce == $updraftplus->file_nonce) {
701 $this->log_with_db_occasionally("This backup set is the backup set just made, so will not be deleted.");
702 $database_backups_found[$key]++;
703 continue;
704 }
705
706 if ($is_always_keep) {
707 if ($database_backups_found[$key] < $updraft_retain) {
708 $this->log_with_db_occasionally("This backup set ($backup_datestamp) was an 'Always Keep' backup, and we have not yet reached any retain limits, so it should be counted in the count of how many have been retained for purposes of the 'how many to retain' count. It will not be pruned. Skipping.");
709 $database_backups_found[$key]++;
710 } else {
711 $this->log_with_db_occasionally("This backup set ($backup_datestamp) was an 'Always Keep' backup, so it will not be pruned. Skipping.");
712 }
713 continue;
714 }
715
716 if ($is_autobackup) {
717 if ($any_deleted_via_filter_yet) {
718 $this->log_with_db_occasionally("This backup set ($backup_datestamp) was an automatic backup, but we have previously deleted a backup due to a limit, so it will be pruned (but not counted towards numerical limits).");
719 $prune_it = true;
720 } elseif ($database_backups_found[$key] < $updraft_retain_db) {
721 $this->log_with_db_occasionally("This backup set ($backup_datestamp) was an automatic backup, and we have not yet reached any retain limits, so it will not be counted or pruned. Skipping.");
722 continue;
723 } else {
724 $this->log_with_db_occasionally("This backup set ($backup_datestamp) was an automatic backup, and we have already reached retain limits, so it will be pruned.");
725 $prune_it = true;
726 }
727 } else {
728 $prune_it = false;
729 }
730
731 if ($remote_sent) {
732 $prune_it = true;
733 $this->log_with_db_occasionally("$backup_datestamp: $key: was sent to remote site; will remove from local record (only)");
734 }
735
736 // All non-auto backups must be run through this filter (in date order) regardless of the current state of $prune_it - so that filters are able to track state.
737 $prune_it_before_filter = $prune_it;
738
739 if (!$is_autobackup) $prune_it = apply_filters('updraftplus_prune_or_not', $prune_it, 'db', $backup_datestamp, $key, $database_backups_found[$key], $rule, $group_id);
740
741 // Apply the final retention limit list (do not increase the 'retained' counter before seeing if the backup is being pruned for some other reason)
742 if (!$prune_it && !$is_autobackup) {
743
744 if ($database_backups_found[$key] + 1 > $updraft_retain_db) {
745 $prune_it = true;
746
747 $fname = is_string($data) ? $data : $data[0];
748 $this->log_with_db_occasionally("$backup_datestamp: $key: this set includes a database (".$fname."); db count is now ".$database_backups_found[$key]);
749
750 $this->log_with_db_occasionally("$backup_datestamp: $key: over retain limit ($updraft_retain_db); will delete this database");
751 }
752
753 }
754
755 if ($prune_it) {
756 if (!$prune_it_before_filter) $any_deleted_via_filter_yet = true;
757
758 if (!empty($data)) {
759 $size_key = $key.'-size';
760 $size = isset($backup_to_examine[$size_key]) ? $backup_to_examine[$size_key] : null;
761 foreach ($services as $service => $instance_ids_to_prune) {
762 foreach ($instance_ids_to_prune as $instance_id_to_prune => $sd) {
763 if ('none' != $service && '' != $service && $sd[0]->supports_feature('multi_options')) {
764 $storage_objects_and_ids = UpdraftPlus_Storage_Methods_Interface::get_storage_objects_and_ids(array($service));
765 if ('all' == $instance_id_to_prune) {
766 foreach ($storage_objects_and_ids[$service]['instance_settings'] as $saved_instance_id => $options) {
767 $sd[0]->set_options($options, false, $saved_instance_id);
768 $this->prune_file($service, $data, $sd[0], $sd[1], array($size));
769 }
770 } else {
771 $opts = $storage_objects_and_ids[$service]['instance_settings'][$instance_id_to_prune];
772 $sd[0]->set_options($opts, false, $instance_id_to_prune);
773 $this->prune_file($service, $data, $sd[0], $sd[1], array($size));
774 }
775 } else {
776 $this->prune_file($service, $data, $sd[0], $sd[1], array($size));
777 }
778 }
779 }
780 }
781 unset($backup_to_examine[$key]);
782 UpdraftPlus_Job_Scheduler::record_still_alive();
783 } elseif (!$is_autobackup) {
784 $database_backups_found[$key]++;
785 }
786
787 $backup_to_examine = $this->remove_backup_set_if_empty($backup_to_examine, $backupable_entities);
788 if (empty($backup_to_examine)) {
789 unset($functional_backup_history[$backup_datestamp]);
790 unset($backup_history[$backup_datestamp]);
791 $this->maybe_save_backup_history_and_reschedule($backup_history);
792 } else {
793 $functional_backup_history[$backup_datestamp] = $backup_to_examine;
794 $backup_history[$backup_datestamp] = $backup_to_examine;
795 }
796 }
797 }
798 }
799
800 if (false === ($backup_files_groups = apply_filters('updraftplus_group_backups_for_pruning', false, $functional_backup_history, 'files'))) {
801 $backup_files_groups = $this->group_backups($functional_backup_history);
802 }
803
804 $updraftplus->log("Number of backup sets in history: ".count($backup_history)."; groups (files): ".count($backup_files_groups));
805
806 // Now again - this time for the files
807 foreach ($backup_files_groups as $group_id => $group) {
808
809 // N.B. The array returned by UpdraftPlus_Backup_History::get_history() is already sorted, with most-recent first
810
811 if (empty($group['sets']) || !is_array($group['sets'])) continue;
812 $sets = $group['sets'];
813
814 // Sort the groups into the desired "keep this first" order
815 $process_order = (!empty($group['process_order']) && 'keep_oldest' == $group['process_order']) ? 'keep_oldest' : 'keep_newest';
816 // Youngest - i.e. smallest epoch - first
817 if ('keep_oldest' == $process_order) ksort($sets);
818
819 $rule = !empty($group['rule']) ? $group['rule'] : array('after-howmany' => 0, 'after-period' => 0, 'every-period' => 1, 'every-howmany' => 1);
820
821 foreach ($sets as $backup_datestamp => $backup_to_examine) {
822
823 $files_to_prune = array();
824 $nonce = empty($backup_to_examine['nonce']) ? '???' : $backup_to_examine['nonce'];
825
826 // $backup_to_examine is an array of file names, keyed on db/plugins/themes/uploads
827 // The new backup_history array is saved afterwards, so remember to unset the ones that are to be deleted
828 $this->log_with_db_occasionally(sprintf("Examining (for files) backup set with nonce=%s, datestamp=%s (%s)", $nonce, $backup_datestamp, gmdate('M d Y H:i:s', $backup_datestamp)));
829
830 // "Always Keep" Backups should be counted in the count of how many have been retained for purposes of the "how many to retain" count... but if that count is already matched, it's not a problem
831 $is_always_keep = !empty($backup_to_examine['always_keep']);
832
833 // Auto-backups are only counted or deleted once we have reached the retain limit - before that, they are skipped
834 $is_autobackup = !empty($backup_to_examine['autobackup']);
835
836 $remote_sent = (!empty($backup_to_examine['service']) && ((is_array($backup_to_examine['service']) && in_array('remotesend', $backup_to_examine['service'])) || 'remotesend' === $backup_to_examine['service'])) ? true : false;
837
838 $any_deleted_via_filter_yet = false;
839
840 $file_sizes = array();
841
842 // Files
843 foreach ($backupable_entities as $entity => $info) {
844 if (!empty($backup_to_examine[$entity])) {
845
846 // This should only be able to happen if you import backups with a future timestamp
847 if ($nonce == $updraftplus->nonce || $nonce == $updraftplus->file_nonce) {
848 $updraftplus->log("This backup set is the backup set just made, so will not be deleted.");
849 $file_entities_backups_found[$entity]++;
850 continue;
851 }
852
853 if ($is_always_keep) {
854 if ($file_entities_backups_found[$entity] < $updraft_retain) {
855 $this->log_with_db_occasionally("This backup set ($backup_datestamp) was an 'Always Keep' backup, and we have not yet reached any retain limits, so it should be counted in the count of how many have been retained for purposes of the 'how many to retain' count. It will not be pruned. Skipping.");
856 $file_entities_backups_found[$entity]++;
857 } else {
858 $this->log_with_db_occasionally("This backup set ($backup_datestamp) was an 'Always Keep' backup, so it will not be pruned. Skipping.");
859 }
860 continue;
861 }
862
863 if ($is_autobackup) {
864 if ($any_deleted_via_filter_yet) {
865 $this->log_with_db_occasionally("This backup set was an automatic backup, but we have previously deleted a backup due to a limit, so it will be pruned (but not counted towards numerical limits).");
866 $prune_it = true;
867 } elseif ($file_entities_backups_found[$entity] < $updraft_retain) {
868 $this->log_with_db_occasionally("This backup set ($backup_datestamp) was an automatic backup, and we have not yet reached any retain limits, so it will not be counted or pruned. Skipping.");
869 continue;
870 } else {
871 $this->log_with_db_occasionally("This backup set ($backup_datestamp) was an automatic backup, and we have already reached retain limits, so it will be pruned.");
872 $prune_it = true;
873 }
874 } else {
875 $prune_it = false;
876 }
877
878 if ($remote_sent) {
879 $prune_it = true;
880 }
881
882 // All non-auto backups must be run through this filter (in date order) regardless of the current state of $prune_it - so that filters are able to track state.
883 $prune_it_before_filter = $prune_it;
884 if (!$is_autobackup) $prune_it = apply_filters('updraftplus_prune_or_not', $prune_it, 'files', $backup_datestamp, $entity, $file_entities_backups_found[$entity], $rule, $group_id);
885
886 // The "more than maximum to keep?" counter should not be increased until we actually know that the set is being kept. Before verison 1.11.22, we checked this before running the filter, which resulted in the counter being increased for sets that got pruned via the filter (i.e. not kept) - and too many backups were thus deleted
887 if (!$prune_it && !$is_autobackup) {
888 if ($file_entities_backups_found[$entity] >= $updraft_retain) {
889 $this->log_with_db_occasionally("$entity: over retain limit ($updraft_retain); will delete this file entity");
890 $prune_it = true;
891 }
892 }
893
894 if ($prune_it) {
895 if (!$prune_it_before_filter) $any_deleted_via_filter_yet = true;
896 $prune_this = $backup_to_examine[$entity];
897 if (is_string($prune_this)) $prune_this = array($prune_this);
898
899 foreach ($prune_this as $k => $prune_file) {
900 if ($remote_sent) {
901 $updraftplus->log("$entity: $backup_datestamp: was sent to remote site; will remove from local record (only)");
902 }
903 $size_key = (0 == $k) ? $entity.'-size' : $entity.$k.'-size';
904 $size = (isset($backup_to_examine[$size_key])) ? $backup_to_examine[$size_key] : null;
905 $files_to_prune[] = $prune_file;
906 $file_sizes[] = $size;
907 }
908 unset($backup_to_examine[$entity]);
909
910 } elseif (!$is_autobackup) {
911 $file_entities_backups_found[$entity]++;
912 }
913 }
914 }
915
916 // Sending an empty array is not itself a problem - except that the remote storage method may not check that before setting up a connection, which can waste time: especially if this is done every time around the loop.
917 if (!empty($files_to_prune)) {
918 // Actually delete the files
919 foreach ($services as $service => $instance_ids_to_prune) {
920 foreach ($instance_ids_to_prune as $instance_id_to_prune => $sd) {
921 if ("none" != $service && '' != $service && $sd[0]->supports_feature('multi_options')) {
922 $storage_objects_and_ids = UpdraftPlus_Storage_Methods_Interface::get_storage_objects_and_ids(array($service));
923 if ('all' == $instance_id_to_prune) {
924 foreach ($storage_objects_and_ids[$service]['instance_settings'] as $saved_instance_id => $options) {
925 $sd[0]->set_options($options, false, $saved_instance_id);
926 $this->prune_file($service, $files_to_prune, $sd[0], $sd[1], array($size));
927 }
928 } else {
929 $opts = $storage_objects_and_ids[$service]['instance_settings'][$instance_id_to_prune];
930 $sd[0]->set_options($opts, false, $instance_id_to_prune);
931 $this->prune_file($service, $files_to_prune, $sd[0], $sd[1], array($size));
932 }
933 } else {
934 $this->prune_file($service, $files_to_prune, $sd[0], $sd[1], array($size));
935 }
936 UpdraftPlus_Job_Scheduler::record_still_alive();
937 }
938 }
939 }
940
941 $backup_to_examine = $this->remove_backup_set_if_empty($backup_to_examine, $backupable_entities);
942 if (empty($backup_to_examine)) {
943 // unset($functional_backup_history[$backup_datestamp]);
944 unset($backup_history[$backup_datestamp]);
945 $this->maybe_save_backup_history_and_reschedule($backup_history);
946 } else {
947 // $functional_backup_history[$backup_datestamp] = $backup_to_examine;
948 $backup_history[$backup_datestamp] = $backup_to_examine;
949 }
950
951 // Loop over backup sets
952 }
953
954 // Look over backup groups
955 }
956
957 $updraftplus->log("Retain: saving new backup history (sets now: ".count($backup_history).") and finishing retain operation");
958 UpdraftPlus_Backup_History::save_history($backup_history, false);
959
960 do_action('updraftplus_prune_retained_backups_finished');
961
962 $updraftplus->jobdata_set('prune', 'finished');
963
964 }
965
966 /**
967 * The purpose of this is to save the backup history periodically - for the benefit of setups where the pruning takes longer than the total allow run time (e.g. if the network communications to the remote storage have delays in, and there are a lot of sets to scan)
968 *
969 * @param Array $backup_history - the backup history to possible save
970 */
971 private function maybe_save_backup_history_and_reschedule($backup_history) {
972 static $last_saved_at = 0;
973 if (!$last_saved_at) $last_saved_at = time();
974 if (time() - $last_saved_at >= 10) {
975 global $updraftplus;
976 $updraftplus->log("Retain: saving new backup history, because at least 10 seconds have passed since the last save (sets now: ".count($backup_history).")");
977 UpdraftPlus_Backup_History::save_history($backup_history, false);
978 UpdraftPlus_Job_Scheduler::something_useful_happened();
979 $last_saved_at = time();
980 }
981 }
982
983 private function remove_backup_set_if_empty($backup_to_examine, $backupable_entities) {
984
985 global $updraftplus;
986
987 // Get new result, post-deletion; anything left in this set?
988 $contains_files = 0;
989 foreach ($backupable_entities as $entity => $info) {
990 if (isset($backup_to_examine[$entity])) {
991 $contains_files = 1;
992 break;
993 }
994 }
995
996 $contains_db = 0;
997 foreach ($backup_to_examine as $key => $data) {
998 if ('db' == strtolower(substr($key, 0, 2)) && '-size' != substr($key, -5, 5)) {
999 $contains_db = 1;
1000 break;
1001 }
1002 }
1003
1004 // Delete backup set completely if empty, o/w just remove DB
1005 // We search on the four keys which represent data, allowing other keys to be used to track other things
1006 if (!$contains_files && !$contains_db) {
1007 $updraftplus->log("This backup set is now empty; will remove from history");
1008 if (isset($backup_to_examine['nonce'])) {
1009 $fullpath = $this->updraft_dir."/log.".$backup_to_examine['nonce'].".txt";
1010 if (is_file($fullpath)) {
1011 $updraftplus->log("Deleting log file (log.".$backup_to_examine['nonce'].".txt)");
1012 @unlink($fullpath);
1013 } else {
1014 $updraftplus->log("Corresponding log file (log.".$backup_to_examine['nonce'].".txt) not found - must have already been deleted");
1015 }
1016 } else {
1017 $updraftplus->log("No nonce record found in the backup set, so cannot delete any remaining log file");
1018 }
1019 return false;
1020 } else {
1021 $updraftplus->log("This backup set remains non-empty (f=$contains_files/d=$contains_db); will retain in history");
1022 return $backup_to_examine;
1023 }
1024
1025 }
1026
1027 /**
1028 * Prune files from local or remote storage
1029 *
1030 * @param String $service Service to prune
1031 * @param Array $dofiles An array of files (or a single string for one file)
1032 * @param Array $method_object specific method object
1033 * @param Array $object_passback specific passback object
1034 * @param Array $file_sizes size of files
1035 */
1036 private function prune_file($service, $dofiles, $method_object = null, $object_passback = null, $file_sizes = array()) {
1037 global $updraftplus;
1038 if (!is_array($dofiles)) $dofiles =array($dofiles);
1039
1040 if (!apply_filters('updraftplus_prune_file', true, $dofiles, $service, $method_object, $object_passback, $file_sizes)) {
1041 $updraftplus->log("Prune: service=$service: skipped via filter");
1042 }
1043
1044 foreach ($dofiles as $i => $dofile) {
1045 if (empty($dofile)) continue;
1046 $updraftplus->log("Delete file: $dofile, service=$service");
1047 $fullpath = $this->updraft_dir.'/'.$dofile;
1048 // delete it if it's locally available
1049 if (file_exists($fullpath)) {
1050 $updraftplus->log("Deleting local copy ($dofile)");
1051 @unlink($fullpath);
1052 }
1053 }
1054 // Despatch to the particular method's deletion routine
1055 if (!is_null($method_object)) $method_object->delete($dofiles, $object_passback, $file_sizes);
1056 }
1057
1058 /**
1059 * The jobdata is passed in instead of fetched, because the live jobdata may now differ from that which should be reported on (e.g. an incremental run was subsequently scheduled)
1060 *
1061 * @param String $final_message The final message to be sent
1062 * @param Array $jobdata Full job data
1063 */
1064 public function send_results_email($final_message, $jobdata) {
1065
1066 global $updraftplus;
1067
1068 $debug_mode = UpdraftPlus_Options::get_updraft_option('updraft_debug_mode');
1069
1070 $sendmail_to = $updraftplus->just_one_email(UpdraftPlus_Options::get_updraft_option('updraft_email'));
1071 if (is_string($sendmail_to)) $sendmail_to = array($sendmail_to);
1072
1073 $backup_files =$jobdata['backup_files'];
1074 $backup_db = $jobdata['backup_database'];
1075
1076 if (is_array($backup_db)) $backup_db = $backup_db['wp'];
1077 if (is_array($backup_db)) $backup_db = $backup_db['status'];
1078
1079 $backup_type = ('backup' == $jobdata['job_type']) ? __('Full backup', 'updraftplus') : __('Incremental', 'updraftplus');
1080
1081 $was_aborted = !empty($jobdata['aborted']);
1082
1083 if ($was_aborted) {
1084 $backup_contains = __('The backup was aborted by the user', 'updraftplus');
1085 } elseif ('finished' == $backup_files && ('finished' == $backup_db || 'encrypted' == $backup_db)) {
1086 $backup_contains = __("Files and database", 'updraftplus')." ($backup_type)";
1087 } elseif ('finished' == $backup_files) {
1088 $backup_contains = ('begun' == $backup_db) ? __("Files (database backup has not completed)", 'updraftplus') : __("Files only (database was not part of this particular schedule)", 'updraftplus');
1089 $backup_contains .= " ($backup_type)";
1090 } elseif ('finished' == $backup_db || 'encrypted' == $backup_db) {
1091 $backup_contains = ('begun' == $backup_files) ? __("Database (files backup has not completed)", 'updraftplus') : __("Database only (files were not part of this particular schedule)", 'updraftplus');
1092 } else {
1093 $updraftplus->log('Unknown/unexpected status: '.serialize($backup_files).'/'.serialize($backup_db));
1094 $backup_contains = __("Unknown/unexpected error - please raise a support request", 'updraftplus');
1095 }
1096
1097 $append_log = '';
1098 $attachments = array();
1099
1100 $error_count = 0;
1101
1102 if ($updraftplus->error_count() > 0) {
1103 $append_log .= __('Errors encountered:', 'updraftplus')."\r\n";
1104 $attachments[0] = $updraftplus->logfile_name;
1105 foreach ($updraftplus->errors as $err) {
1106 if (is_wp_error($err)) {
1107 foreach ($err->get_error_messages() as $msg) {
1108 $append_log .= "* ".rtrim($msg)."\r\n";
1109 }
1110 } elseif (is_array($err) && 'error' == $err['level']) {
1111 $append_log .= "* ".rtrim($err['message'])."\r\n";
1112 } elseif (is_string($err)) {
1113 $append_log .= "* ".rtrim($err)."\r\n";
1114 }
1115 $error_count++;
1116 }
1117 $append_log .="\r\n";
1118 }
1119 $warnings = (isset($jobdata['warnings'])) ? $jobdata['warnings'] : array();
1120 if (is_array($warnings) && count($warnings) >0) {
1121 $append_log .= __('Warnings encountered:', 'updraftplus')."\r\n";
1122 $attachments[0] = $updraftplus->logfile_name;
1123 foreach ($warnings as $err) {
1124 $append_log .= "* ".rtrim($err)."\r\n";
1125 }
1126 $append_log .="\r\n";
1127 }
1128
1129 if ($debug_mode && '' != $updraftplus->logfile_name && !in_array($updraftplus->logfile_name, $attachments)) {
1130 $append_log .= "\r\n".__('The log file has been attached to this email.', 'updraftplus');
1131 $attachments[0] = $updraftplus->logfile_name;
1132 }
1133
1134 // We have to use the action in order to set the MIME type on the attachment - by default, WordPress just puts application/octet-stream
1135
1136 $subject = apply_filters('updraft_report_subject', sprintf(__('Backed up: %s', 'updraftplus'), get_bloginfo('name')).' (UpdraftPlus '.$updraftplus->version.') '.get_date_from_gmt(gmdate('Y-m-d H:i:s', time()), 'Y-m-d H:i'), $error_count, count($warnings));
1137
1138 // The class_exists() check here is a micro-optimization to prevent a possible HTTP call whose results may be disregarded by the filter
1139 $feed = '';
1140 if (!class_exists('UpdraftPlus_Addon_Reporting') && !defined('UPDRAFTPLUS_NOADS_B') && !defined('UPDRAFTPLUS_NONEWSFEED')) {
1141 $updraftplus->log('Fetching RSS news feed');
1142 $rss = $updraftplus->get_updraftplus_rssfeed();
1143 $updraftplus->log('Fetched RSS news feed; result is a: '.get_class($rss));
1144 if (is_a($rss, 'SimplePie')) {
1145 $feed .= __('Email reports created by UpdraftPlus (free edition) bring you the latest UpdraftPlus.com news', 'updraftplus')." - ".sprintf(__('read more at %s', 'updraftplus'), 'https://updraftplus.com/news/')."\r\n\r\n";
1146 foreach ($rss->get_items(0, 6) as $item) {
1147 $feed .= '* ';
1148 $feed .= $item->get_title();
1149 $feed .= " (".$item->get_date('j F Y').")";
1150 // $feed .= ' - '.$item->get_permalink();
1151 $feed .= "\r\n";
1152 }
1153 }
1154 $feed .= "\r\n\r\n";
1155 }
1156
1157 $extra_messages = apply_filters('updraftplus_report_extramessages', array());
1158 $extra_msg = '';
1159 if (is_array($extra_messages)) {
1160 foreach ($extra_messages as $msg) {
1161 $extra_msg .= '<strong>'.$msg['key'].'</strong>: '.$msg['val']."\r\n";
1162 }
1163 }
1164
1165 foreach ($this->remotestorage_extrainfo as $service => $message) {
1166 if (!empty($updraftplus->backup_methods[$service])) $extra_msg .= $updraftplus->backup_methods[$service].': '.$message['plain']."\r\n";
1167 }
1168
1169 // Make it available to the filter
1170 $jobdata['remotestorage_extrainfo'] = $this->remotestorage_extrainfo;
1171
1172 if (!class_exists('UpdraftPlus_Notices')) include_once(UPDRAFTPLUS_DIR.'/includes/updraftplus-notices.php');
1173 global $updraftplus_notices;
1174 $ws_advert = $updraftplus_notices->do_notice(false, 'report-plain', true);
1175
1176 $body = apply_filters('updraft_report_body',
1177 __('Backup of:', 'updraftplus').' '.site_url()."\r\n".
1178 "UpdraftPlus ".__('WordPress backup is complete', 'updraftplus').".\r\n".
1179 __('Backup contains:', 'updraftplus')." $backup_contains\r\n".
1180 __('Latest status:', 'updraftplus').' '.$final_message."\r\n".
1181 $extra_msg.
1182 "\r\n".
1183 $feed.
1184 $ws_advert."\r\n".
1185 $append_log,
1186 $final_message,
1187 $backup_contains,
1188 $updraftplus->errors,
1189 $warnings,
1190 $jobdata);
1191
1192 $this->attachments = apply_filters('updraft_report_attachments', $attachments);
1193
1194 if (count($this->attachments)>0) add_action('phpmailer_init', array($this, 'phpmailer_init'));
1195
1196 $attach_size = 0;
1197 $unlink_files = array();
1198
1199 foreach ($this->attachments as $ind => $attach) {
1200 if ($attach == $updraftplus->logfile_name && filesize($attach) > 6*1048576) {
1201
1202 $updraftplus->log("Log file is large (".round(filesize($attach)/1024, 1)." KB): will compress before e-mailing");
1203
1204 if (!$handle = fopen($attach, "r")) {
1205 $updraftplus->log("Error: Failed to open log file for reading: ".$attach);
1206 } else {
1207 if (!$whandle = gzopen($attach.'.gz', 'w')) {
1208 $updraftplus->log("Error: Failed to open log file for reading: ".$attach.".gz");
1209 } else {
1210 while (false !== ($line = @stream_get_line($handle, 131072, "\n"))) {
1211 @gzwrite($whandle, $line."\n");
1212 }
1213 fclose($handle);
1214 gzclose($whandle);
1215 $this->attachments[$ind] = $attach.'.gz';
1216 $unlink_files[] = $attach.'.gz';
1217 }
1218 }
1219 }
1220 $attach_size += filesize($this->attachments[$ind]);
1221 }
1222
1223 foreach ($sendmail_to as $ind => $mailto) {
1224
1225 if (false === apply_filters('updraft_report_sendto', true, $mailto, $error_count, count($warnings), $ind)) continue;
1226
1227 foreach (explode(',', $mailto) as $sendmail_addr) {
1228 $updraftplus->log("Sending email ('$backup_contains') report (attachments: ".count($attachments).", size: ".round($attach_size/1024, 1)." KB) to: ".substr($sendmail_addr, 0, 5)."...");
1229 try {
1230 wp_mail(trim($sendmail_addr), $subject, $body, array("X-UpdraftPlus-Backup-ID: ".$updraftplus->nonce));
1231 } catch (Exception $e) {
1232 $updraftplus->log("Exception occurred when sending mail (".get_class($e)."): ".$e->getMessage());
1233 }
1234 }
1235 }
1236
1237 foreach ($unlink_files as $file) @unlink($file);
1238
1239 do_action('updraft_report_finished');
1240 if (count($this->attachments)>0) remove_action('phpmailer_init', array($this, 'phpmailer_init'));
1241
1242 }
1243
1244 /**
1245 * The purpose of this function is to make sure that the options table is put in the database first, then the users table, then the site + blogs tables (if present - multisite), then the usermeta table; and after that the core WP tables - so that when restoring we restore the core tables first
1246 *
1247 * @param Array $a_arr First array to be compared
1248 * @param Array $b_arr Second array to be compared
1249 * @return Integer - according to the rules of usort()
1250 */
1251 private function backup_db_sorttables($a_arr, $b_arr) {
1252
1253 $a = $a_arr['name'];
1254 $a_table_type = $a_arr['type'];
1255 $b = $b_arr['name'];
1256 $b_table_type = $b_arr['type'];
1257
1258 // Views must always go after tables (since they can depend upon them)
1259 if ('VIEW' == $a_table_type && 'VIEW' != $b_table_type) return 1;
1260 if ('VIEW' == $b_table_type && 'VIEW' != $a_table_type) return -1;
1261
1262 if ('wp' != $this->whichdb) return strcmp($a, $b);
1263
1264 global $updraftplus;
1265 if ($a == $b) return 0;
1266 $our_table_prefix = $this->table_prefix_raw;
1267 if ($a == $our_table_prefix.'options') return -1;
1268 if ($b == $our_table_prefix.'options') return 1;
1269 if ($a == $our_table_prefix.'site') return -1;
1270 if ($b == $our_table_prefix.'site') return 1;
1271 if ($a == $our_table_prefix.'blogs') return -1;
1272 if ($b == $our_table_prefix.'blogs') return 1;
1273 if ($a == $our_table_prefix.'users') return -1;
1274 if ($b == $our_table_prefix.'users') return 1;
1275 if ($a == $our_table_prefix.'usermeta') return -1;
1276 if ($b == $our_table_prefix.'usermeta') return 1;
1277
1278 if (empty($our_table_prefix)) return strcmp($a, $b);
1279
1280 try {
1281 $core_tables = array_merge($this->wpdb_obj->tables, $this->wpdb_obj->global_tables, $this->wpdb_obj->ms_global_tables);
1282 } catch (Exception $e) {
1283 $updraftplus->log($e->getMessage());
1284 }
1285
1286 if (empty($core_tables)) $core_tables = array('terms', 'term_taxonomy', 'termmeta', 'term_relationships', 'commentmeta', 'comments', 'links', 'postmeta', 'posts', 'site', 'sitemeta', 'blogs', 'blogversions', 'blogmeta');
1287
1288 global $updraftplus;
1289 $na = UpdraftPlus_Manipulation_Functions::str_replace_once($our_table_prefix, '', $a);
1290 $nb = UpdraftPlus_Manipulation_Functions::str_replace_once($our_table_prefix, '', $b);
1291 if (in_array($na, $core_tables) && !in_array($nb, $core_tables)) return -1;
1292 if (!in_array($na, $core_tables) && in_array($nb, $core_tables)) return 1;
1293 return strcmp($a, $b);
1294 }
1295
1296 private function log_account_space() {
1297 // Don't waste time if space is huge
1298 if (!empty($this->account_space_oodles)) return;
1299 global $updraftplus;
1300 $hosting_bytes_free = $updraftplus->get_hosting_disk_quota_free();
1301 if (is_array($hosting_bytes_free)) {
1302 $perc = round(100*$hosting_bytes_free[1]/(max($hosting_bytes_free[2], 1)), 1);
1303 $updraftplus->log(sprintf('Free disk space in account: %s (%s used)', round($hosting_bytes_free[3]/1048576, 1)." MB", "$perc %"));
1304 }
1305 }
1306
1307 /**
1308 * Returns the basename up to and including the nonce (but not the entity)
1309 *
1310 * @param Integer $use_time epoch time to use
1311 * @return String
1312 */
1313 private function get_backup_file_basename_from_time($use_time) {
1314 global $updraftplus;
1315 return apply_filters('updraftplus_get_backup_file_basename_from_time', 'backup_'.get_date_from_gmt(gmdate('Y-m-d H:i:s', $use_time), 'Y-m-d-Hi').'_'.$this->site_name.'_'.$updraftplus->file_nonce, $use_time, $this->site_name);
1316 }
1317
1318 private function find_existing_zips($dir, $match_nonce) {
1319 $zips = array();
1320 if ($handle = opendir($dir)) {
1321 while (false !== ($entry = readdir($handle))) {
1322 if ("." != $entry && ".." != $entry) {
1323 if (preg_match('/^backup_(\d{4})-(\d{2})-(\d{2})-(\d{2})(\d{2})_.*_([0-9a-f]{12})-([\-a-z]+)([0-9]+)?\.zip$/i', $entry, $matches)) {
1324 if ($matches[6] !== $match_nonce) continue;
1325 $timestamp = mktime($matches[4], $matches[5], 0, $matches[2], $matches[3], $matches[1]);
1326 $entity = $matches[7];
1327 $index = empty($matches[8]) ? '0' : $matches[8];
1328 $zips[$entity][$index] = array($timestamp, $entry);
1329 }
1330 }
1331 }
1332 }
1333 return $zips;
1334 }
1335
1336 /**
1337 * Get information on whether a particular file exists in a set
1338 *
1339 * @param Array $files should be an array as returned by find_existing_zips()]
1340 * @param String $entity entty of the file (e.g. 'plugins')
1341 * @param Integer $index Index within the files array
1342 * @return String|Boolean - false if the file does not exist; otherwise, the basename
1343 */
1344 private function file_exists($files, $entity, $index = 0) {
1345 if (isset($files[$entity]) && isset($files[$entity][$index])) {
1346 $file = $files[$entity][$index];
1347 // Return the filename
1348 return $file[1];
1349 } else {
1350 return false;
1351 }
1352 }
1353
1354 /**
1355 * This function is resumable
1356 *
1357 * @param String $job_status Current status
1358 * @return Array - array of backed-up files
1359 */
1360 private function backup_dirs($job_status) {
1361
1362 global $updraftplus;
1363
1364 if (!$updraftplus->backup_time) $updraftplus->backup_time_nonce();
1365
1366 $use_time = $updraftplus->backup_time;
1367 $backup_file_basename = $this->get_backup_file_basename_from_time($use_time);
1368
1369 $backup_array = array();
1370
1371 $possible_backups = $updraftplus->get_backupable_file_entities(true);
1372
1373 // Was there a check-in last time? If not, then reduce the amount of data attempted
1374 if ('finished' != $job_status && $updraftplus->current_resumption >= 2) {
1375
1376 // NOTYET: Possible amendment to original algorithm; not just no check-in, but if the check in was very early (can happen if we get a very early checkin for some trivial operation, then attempt something too big)
1377
1378
1379 // 03-Sep-2015 - came across a case (HS#2052) where there apparently was a check-in 'last time', but no resumption was scheduled because the 'useful_checkin' jobdata was *not* last time - which must indicate dying at a very unfortunate/unlikely point in the code. As a result, the split was not auto-reduced. Consequently, we've added !$updraftplus->newresumption_scheduled as a condition on the first check here (it was already on the second), as if no resumption is scheduled then whatever checkin there was last time was only partial. This was on GoDaddy, for which a number of curious I/O event combinations have been seen in recent months - their platform appears to have some odd behaviour when PHP is killed off.
1380 // 04-Sep-2015 - move the '$updraftplus->current_resumption<=10' check to the inner loop (instead of applying to this whole section), as I see no reason for that restriction (case seen in HS#2064 where it was required on resumption 15)
1381 if (!empty($updraftplus->no_checkin_last_time) || !$updraftplus->newresumption_scheduled) {
1382 // Apr 2015: !$updraftplus->newresumption_scheduled added after seeing a log where there was no activity on resumption 9, and extra resumption 10 then tried the same operation.
1383 if ($updraftplus->current_resumption - $updraftplus->last_successful_resumption > 2 || !$updraftplus->newresumption_scheduled) {
1384 $this->try_split = true;
1385 } elseif ($updraftplus->current_resumption<=10) {
1386 $maxzipbatch = $updraftplus->jobdata_get('maxzipbatch', 26214400);
1387 if ((int) $maxzipbatch < 1) $maxzipbatch = 26214400;
1388
1389 $new_maxzipbatch = max(floor($maxzipbatch * 0.75), 20971520);
1390 if ($new_maxzipbatch < $maxzipbatch) {
1391 $updraftplus->log("No check-in was detected on the previous run - as a result, we are reducing the batch amount (old=$maxzipbatch, new=$new_maxzipbatch)");
1392 $updraftplus->jobdata_set('maxzipbatch', $new_maxzipbatch);
1393 $updraftplus->jobdata_set('maxzipbatch_ceiling', $new_maxzipbatch);
1394 }
1395 }
1396 }
1397 }
1398
1399 if ('finished' != $job_status && !UpdraftPlus_Filesystem_Functions::really_is_writable($this->updraft_dir)) {
1400 $updraftplus->log("Backup directory (".$this->updraft_dir.") is not writable, or does not exist");
1401 $updraftplus->log(sprintf(__("Backup directory (%s) is not writable, or does not exist.", 'updraftplus'), $this->updraft_dir), 'error');
1402 return array();
1403 }
1404
1405 $this->job_file_entities = $updraftplus->jobdata_get('job_file_entities');
1406 // This is just used for the visual feedback (via the 'substatus' key)
1407 $which_entity = 0;
1408 // e.g. plugins, themes, uploads, others
1409 // $whichdir might be an array (if $youwhat is 'more')
1410
1411 // Returns an array (keyed off the entity) of ($timestamp, $filename) arrays
1412 $existing_zips = $this->find_existing_zips($this->updraft_dir, $updraftplus->file_nonce);
1413
1414 foreach ($possible_backups as $youwhat => $whichdir) {
1415
1416 if (isset($this->job_file_entities[$youwhat])) {
1417
1418 $index = (int) $this->job_file_entities[$youwhat]['index'];
1419 if (empty($index)) $index=0;
1420 $indextext = (0 == $index) ? '' : (1+$index);
1421
1422 $zip_file = $this->updraft_dir.'/'.$backup_file_basename.'-'.$youwhat.$indextext.'.zip';
1423
1424 // Split needed?
1425 $split_every = max((int) $updraftplus->jobdata_get('split_every'), 250);
1426 // if (file_exists($zip_file) && filesize($zip_file) > $split_every*1048576) {
1427 if (false != ($existing_file = $this->file_exists($existing_zips, $youwhat, $index)) && filesize($this->updraft_dir.'/'.$existing_file) > $split_every*1048576) {
1428 $index++;
1429 $this->job_file_entities[$youwhat]['index'] = $index;
1430 $updraftplus->jobdata_set('job_file_entities', $this->job_file_entities);
1431 }
1432
1433 // Populate prior parts of array, if we're on a subsequent zip file
1434 if ($index > 0) {
1435 for ($i=0; $i<$index; $i++) {
1436 $itext = (0 == $i) ? '' : ($i+1);
1437 // Get the previously-stored filename if possible (which should be always); failing that, base it on the current run
1438
1439 $zip_file = (isset($this->backup_files_array[$youwhat]) && isset($this->backup_files_array[$youwhat][$i])) ? $this->backup_files_array[$youwhat][$i] : $backup_file_basename.'-'.$youwhat.$itext.'.zip';
1440
1441 $backup_array[$youwhat][$i] = $zip_file;
1442 $z = $this->updraft_dir.'/'.$zip_file;
1443 $itext = (0 == $i) ? '' : $i;
1444
1445 $fs_key = $youwhat.$itext.'-size';
1446 if (file_exists($z)) {
1447 $backup_array[$fs_key] = filesize($z);
1448 } elseif (isset($this->backup_files_array[$fs_key])) {
1449 $backup_array[$fs_key] = $this->backup_files_array[$fs_key];
1450 }
1451 }
1452 }
1453
1454 // I am not certain that all the conditions in here are possible. But there's no harm.
1455 if ('finished' == $job_status) {
1456 // Add the final part of the array
1457 if ($index > 0) {
1458 $zip_file = (isset($this->backup_files_array[$youwhat]) && isset($this->backup_files_array[$youwhat][$index])) ? $this->backup_files_array[$youwhat][$index] : $backup_file_basename.'-'.$youwhat.($index+1).'.zip';
1459
1460 // $fbase = $backup_file_basename.'-'.$youwhat.($index+1).'.zip';
1461 $z = $this->updraft_dir.'/'.$zip_file;
1462 $fs_key = $youwhat.$index.'-size';
1463 if (file_exists($z)) {
1464 $backup_array[$youwhat][$index] = $fbase;
1465 $backup_array[$fs_key] = filesize($z);
1466 } elseif (isset($this->backup_files_array[$fs_key])) {
1467 $backup_array[$youwhat][$index] = $fbase;
1468 $backup_array[$fs_key] = $this->backup_files_array[$fskey];
1469 }
1470 } else {
1471 $zip_file = (isset($this->backup_files_array[$youwhat]) && isset($this->backup_files_array[$youwhat][0])) ? $this->backup_files_array[$youwhat][0] : $backup_file_basename.'-'.$youwhat.'.zip';
1472
1473 $backup_array[$youwhat] = $zip_file;
1474 $fs_key=$youwhat.'-size';
1475
1476 if (file_exists($zip_file)) {
1477 $backup_array[$fs_key] = filesize($zip_file);
1478 } elseif (isset($this->backup_files_array[$fs_key])) {
1479 $backup_array[$fs_key] = $this->backup_files_array[$fs_key];
1480 }
1481 }
1482 } else {
1483
1484 $which_entity++;
1485 $updraftplus->jobdata_set('filecreating_substatus', array('e' => $youwhat, 'i' => $which_entity, 't' => count($this->job_file_entities)));
1486
1487 if ('others' == $youwhat) $updraftplus->log("Beginning backup of other directories found in the content directory (index: $index)");
1488
1489 // Apply a filter to allow add-ons to provide their own method for creating a zip of the entity
1490 $created = apply_filters('updraftplus_backup_makezip_'.$youwhat, $whichdir, $backup_file_basename, $index);
1491 // If the filter did not lead to something being created, then use the default method
1492 if ($created === $whichdir) {
1493
1494 // http://www.phpconcept.net/pclzip/user-guide/53
1495 /* First parameter to create is:
1496 An array of filenames or dirnames,
1497 or
1498 A string containing the filename or a dirname,
1499 or
1500 A string containing a list of filename or dirname separated by a comma.
1501 */
1502
1503 if ('others' == $youwhat) {
1504 $dirlist = $updraftplus->backup_others_dirlist(true);
1505 } elseif ('uploads' == $youwhat) {
1506 $dirlist = $updraftplus->backup_uploads_dirlist(true);
1507 } else {
1508 $dirlist = $whichdir;
1509 if (is_array($dirlist)) $dirlist =array_shift($dirlist);
1510 }
1511
1512 if (!empty($dirlist)) {
1513 $created = $this->create_zip($dirlist, $youwhat, $backup_file_basename, $index);
1514 // Now, store the results
1515 if (!is_string($created) && !is_array($created)) $updraftplus->log("$youwhat: create_zip returned an error");
1516 } else {
1517 $updraftplus->log("No backup of $youwhat: there was nothing found to backup");
1518 }
1519 }
1520
1521 if ($created != $whichdir && (is_string($created) || is_array($created))) {
1522 if (is_string($created)) $created =array($created);
1523 foreach ($created as $fname) {
1524 $backup_array[$youwhat][$index] = $fname;
1525 $itext = (0 == $index) ? '' : $index;
1526 $index++;
1527 $backup_array[$youwhat.$itext.'-size'] = filesize($this->updraft_dir.'/'.$fname);
1528 }
1529 }
1530
1531 $this->job_file_entities[$youwhat]['index'] = $this->index;
1532 $updraftplus->jobdata_set('job_file_entities', $this->job_file_entities);
1533
1534 }
1535 } else {
1536 $updraftplus->log("No backup of $youwhat: excluded by user's options");
1537 }
1538 }
1539
1540 return $backup_array;
1541 }
1542
1543 /**
1544 * This uses a saved status indicator; its only purpose is to indicate *total* completion; there is no actual danger, just wasted time, in resuming when it was not needed. So the saved status indicator just helps save resources.
1545 *
1546 * @param integer $resumption_no Check for first run
1547 * @return array
1548 */
1549 public function resumable_backup_of_files($resumption_no) {
1550 global $updraftplus;
1551 // Backup directories and return a numerically indexed array of file paths to the backup files
1552 $bfiles_status = $updraftplus->jobdata_get('backup_files');
1553 $this->backup_files_array = $updraftplus->jobdata_get('backup_files_array');
1554
1555 if (!is_array($this->backup_files_array)) $this->backup_files_array = array();
1556 if ('finished' == $bfiles_status) {
1557 $updraftplus->log("Creation of backups of directories: already finished");
1558 // Check for recent activity
1559 foreach ($this->backup_files_array as $files) {
1560 if (!is_array($files)) $files =array($files);
1561 foreach ($files as $file) $updraftplus->check_recent_modification($this->updraft_dir.'/'.$file);
1562 }
1563 } elseif ('begun' == $bfiles_status) {
1564 $this->first_run = apply_filters('updraftplus_filerun_firstrun', 0);
1565 if ($resumption_no > $this->first_run) {
1566 $updraftplus->log("Creation of backups of directories: had begun; will resume");
1567 } else {
1568 $updraftplus->log("Creation of backups of directories: beginning");
1569 }
1570 $updraftplus->jobdata_set('jobstatus', 'filescreating');
1571 $this->backup_files_array = $this->backup_dirs($bfiles_status);
1572 $updraftplus->jobdata_set('backup_files_array', $this->backup_files_array);
1573 $updraftplus->jobdata_set('backup_files', 'finished');
1574 $updraftplus->jobdata_set('jobstatus', 'filescreated');
1575 } else {
1576 // This is not necessarily a backup run which is meant to contain files at all
1577 $updraftplus->log('This backup run is not intended for files - skipping');
1578 return array();
1579 }
1580
1581 /*
1582 // DOES NOT WORK: there is no crash-safe way to do this here - have to be renamed at cloud-upload time instead
1583 $new_backup_array = array();
1584 foreach ($backup_array as $entity => $files) {
1585 if (!is_array($files)) $files=array($files);
1586 $outof = count($files);
1587 foreach ($files as $ind => $file) {
1588 $nval = $file;
1589 if (preg_match('/^(backup_[\-0-9]{15}_.*_[0-9a-f]{12}-[\-a-z]+)([0-9]+)?\.zip$/i', $file, $matches)) {
1590 $num = max((int)$matches[2],1);
1591 $new = $matches[1].$num.'of'.$outof.'.zip';
1592 if (file_exists($this->updraft_dir.'/'.$file)) {
1593 if (@rename($this->updraft_dir.'/'.$file, $this->updraft_dir.'/'.$new)) {
1594 $updraftplus->log(sprintf("Renaming: %s to %s", $file, $new));
1595 $nval = $new;
1596 }
1597 } elseif (file_exists($this->updraft_dir.'/'.$new)) {
1598 $nval = $new;
1599 }
1600 }
1601 $new_backup_array[$entity][$ind] = $nval;
1602 }
1603 }
1604 */
1605 return $this->backup_files_array;
1606 }
1607
1608 /**
1609 * This function is resumable, using the following method:
1610 * Each table is written out to ($final_filename).table.tmp
1611 * When the writing finishes, it is renamed to ($final_filename).table
1612 * When all tables are finished, they are concatenated into the final file
1613 *
1614 * @param String $already_done Status of backup
1615 * @param String $whichdb Indicated which database is being backed up
1616 * @param Array $dbinfo is only used when whichdb != 'wp'; and the keys should be: user, pass, name, host, prefix
1617 * @return Boolean|String - the basename of the database backup, or false for failure
1618 */
1619 public function backup_db($already_done = 'begun', $whichdb = 'wp', $dbinfo = array()) {
1620
1621 global $updraftplus, $wpdb;
1622
1623 $this->whichdb = $whichdb;
1624 $this->whichdb_suffix = ('wp' == $whichdb) ? '' : $whichdb;
1625
1626 if (!$updraftplus->backup_time) $updraftplus->backup_time_nonce();
1627 if (!$updraftplus->opened_log_time) $updraftplus->logfile_open($updraftplus->nonce);
1628
1629 if ('wp' == $this->whichdb) {
1630 $this->wpdb_obj = $wpdb;
1631 // The table prefix after being filtered - i.e. what filters what we'll actually backup
1632 $this->table_prefix = $updraftplus->get_table_prefix(true);
1633 // The unfiltered table prefix - i.e. the real prefix that things are relative to
1634 $this->table_prefix_raw = $updraftplus->get_table_prefix(false);
1635 $dbinfo['host'] = DB_HOST;
1636 $dbinfo['name'] = DB_NAME;
1637 $dbinfo['user'] = DB_USER;
1638 $dbinfo['pass'] = DB_PASSWORD;
1639 } else {
1640 if (!is_array($dbinfo) || empty($dbinfo['host'])) return false;
1641 // The methods that we may use: check_connection (WP>=3.9), get_results, get_row, query
1642 $this->wpdb_obj = new UpdraftPlus_WPDB_OtherDB($dbinfo['user'], $dbinfo['pass'], $dbinfo['name'], $dbinfo['host']);
1643 if (!empty($this->wpdb_obj->error)) {
1644 $updraftplus->log($dbinfo['user'].'@'.$dbinfo['host'].'/'.$dbinfo['name'].' : database connection attempt failed');
1645 $updraftplus->log($dbinfo['user'].'@'.$dbinfo['host'].'/'.$dbinfo['name'].' : '.__('database connection attempt failed.', 'updraftplus').' '.__('Connection failed: check your access details, that the database server is up, and that the network connection is not firewalled.', 'updraftplus'), 'error');
1646 return $updraftplus->log_wp_error($this->wpdb_obj->error);
1647 }
1648 $this->table_prefix = $dbinfo['prefix'];
1649 $this->table_prefix_raw = $dbinfo['prefix'];
1650 }
1651
1652 $this->dbinfo = $dbinfo;
1653
1654 $errors = 0;
1655
1656 $use_time = apply_filters('updraftplus_base_backup_timestamp', $updraftplus->backup_time);
1657 $file_base = $this->get_backup_file_basename_from_time($use_time);
1658 $backup_file_base = $this->updraft_dir.'/'.$file_base;
1659
1660 if ('finished' == $already_done) return basename($backup_file_base).'-db'.(('wp' == $whichdb) ? '' : $whichdb).'.gz';
1661 if ('encrypted' == $already_done) return basename($backup_file_base).'-db'.(('wp' == $whichdb) ? '' : $whichdb).'.gz.crypt';
1662
1663 $updraftplus->jobdata_set('jobstatus', 'dbcreating'.$this->whichdb_suffix);
1664
1665 $binsqldump = $updraftplus->find_working_sqldump();
1666
1667 $total_tables = 0;
1668
1669 // WP 3.9 onwards - https://core.trac.wordpress.org/browser/trunk/src/wp-includes/wp-db.php?rev=27925 - check_connection() allows us to get the database connection back if it had dropped
1670 if ('wp' == $whichdb && method_exists($this->wpdb_obj, 'check_connection') && (!defined('UPDRAFTPLUS_SUPPRESS_CONNECTION_CHECKS') || !UPDRAFTPLUS_SUPPRESS_CONNECTION_CHECKS)) {
1671 if (!$this->wpdb_obj->check_connection(false)) {
1672 UpdraftPlus_Job_Scheduler::reschedule(60);
1673 $updraftplus->log("It seems the database went away; scheduling a resumption and terminating for now");
1674 UpdraftPlus_Job_Scheduler::record_still_alive();
1675 die;
1676 }
1677 }
1678
1679 // SHOW FULL - so that we get to know whether it's a BASE TABLE or a VIEW
1680 $all_tables = $this->wpdb_obj->get_results("SHOW FULL TABLES", ARRAY_N);
1681
1682 if (empty($all_tables) && !empty($this->wpdb_obj->last_error)) {
1683 $all_tables = $this->wpdb_obj->get_results("SHOW TABLES", ARRAY_N);
1684 $all_tables = array_map(array($this, 'cb_get_name_base_type'), $all_tables);
1685 } else {
1686 $all_tables = array_map(array($this, 'cb_get_name_type'), $all_tables);
1687 }
1688
1689 // If this is not the WP database, then we do not consider it a fatal error if there are no tables
1690 if ('wp' == $whichdb && 0 == count($all_tables)) {
1691 $extra = ($updraftplus->newresumption_scheduled) ? ' - '.__('please wait for the rescheduled attempt', 'updraftplus') : '';
1692 $updraftplus->log("Error: No WordPress database tables found (SHOW TABLES returned nothing)".$extra);
1693 $updraftplus->log(__("No database tables found", 'updraftplus').$extra, 'error');
1694 die;
1695 }
1696
1697 // Put the options table first
1698 usort($all_tables, array($this, 'backup_db_sorttables'));
1699
1700 $all_table_names = array_map(array($this, 'cb_get_name'), $all_tables);
1701
1702 if (!UpdraftPlus_Filesystem_Functions::really_is_writable($this->updraft_dir)) {
1703 $updraftplus->log("The backup directory (".$this->updraft_dir.") could not be written to (could be account/disk space full, or wrong permissions).");
1704 $updraftplus->log($this->updraft_dir.": ".__('The backup directory is not writable (or disk space is full) - the database backup is expected to shortly fail.', 'updraftplus'), 'warning');
1705 // Why not just fail now? We saw a bizarre case when the results of really_is_writable() changed during the run.
1706 }
1707
1708 // This check doesn't strictly get all possible duplicates; it's only designed for the case that can happen when moving between deprecated Windows setups and Linux
1709 $this->duplicate_tables_exist = false;
1710 foreach ($all_table_names as $table) {
1711 if (strtolower($table) != $table && in_array(strtolower($table), $all_table_names)) {
1712 $this->duplicate_tables_exist = true;
1713 $updraftplus->log("Tables with names differing only based on case-sensitivity exist in the MySQL database: $table / ".strtolower($table));
1714 }
1715 }
1716 $how_many_tables = count($all_tables);
1717
1718 $stitch_files = array();
1719 $found_options_table = false;
1720 $is_multisite = is_multisite();
1721
1722 foreach ($all_tables as $ti) {
1723
1724 $table = $ti['name'];
1725 $table_type = $ti['type'];
1726
1727 $manyrows_warning = false;
1728 $total_tables++;
1729
1730 // Increase script execution time-limit to 15 min for every table.
1731 @set_time_limit(UPDRAFTPLUS_SET_TIME_LIMIT);
1732 // The table file may already exist if we have produced it on a previous run
1733 $table_file_prefix = $file_base.'-db'.$this->whichdb_suffix.'-table-'.$table.'.table';
1734
1735 if ('wp' == $whichdb && (strtolower($this->table_prefix_raw.'options') == strtolower($table) || ($is_multisite && (strtolower($this->table_prefix_raw.'sitemeta') == strtolower($table) || strtolower($this->table_prefix_raw.'1_options') == strtolower($table))))) $found_options_table = true;
1736
1737 if (file_exists($this->updraft_dir.'/'.$table_file_prefix.'.gz')) {
1738 $stitched = count($stitch_files);
1739 $skip_dblog = (($stitched > 10 && 0 != $stitched % 20) || ($stitched > 100 && 0 != $stitched % 100));
1740 $updraftplus->log("Table $table: corresponding file already exists; moving on", 'notice', false, $skip_dblog);
1741 $stitch_files[] = $table_file_prefix;
1742 } else {
1743 // === is needed, otherwise 'false' matches (i.e. prefix does not match)
1744 if (empty($this->table_prefix) || (false == $this->duplicate_tables_exist && stripos($table, $this->table_prefix) === 0 ) || (true == $this->duplicate_tables_exist && strpos($table, $this->table_prefix) === 0)) {
1745
1746 if (!apply_filters('updraftplus_backup_table', true, $table, $this->table_prefix, $whichdb, $dbinfo)) {
1747 $updraftplus->log("Skipping table (filtered): $table");
1748 if (empty($this->skipped_tables)) $this->skipped_tables = array();
1749
1750 // whichdb could be an int in which case to get the name of the database and the array key use the name from dbinfo
1751 if ('wp' !== $whichdb) {
1752 $key = $dbinfo['name'];
1753 } else {
1754 $key = $whichdb;
1755 }
1756
1757 if (empty($this->skipped_tables[$key])) $this->skipped_tables[$key] = '';
1758 if ('' != $this->skipped_tables[$key]) $this->skipped_tables[$key] .= ',';
1759 $this->skipped_tables[$key] .= $table;
1760
1761 $total_tables--;
1762 } else {
1763
1764 $db_temp_file = $this->updraft_dir.'/'.$table_file_prefix.'.tmp.gz';
1765 $updraftplus->check_recent_modification($db_temp_file);
1766
1767 // Open file, store the handle
1768 $opened = $this->backup_db_open($db_temp_file, true);
1769 if (false === $opened) return false;
1770
1771 // Create the SQL statements
1772 $this->stow("# " . sprintf('Table: %s', UpdraftPlus_Manipulation_Functions::backquote($table)) . "\n");
1773 $updraftplus->jobdata_set('dbcreating_substatus', array('t' => $table, 'i' => $total_tables, 'a' => $how_many_tables));
1774
1775 $table_status = $this->wpdb_obj->get_row("SHOW TABLE STATUS WHERE Name='$table'");
1776 if (isset($table_status->Rows)) {
1777 $rows = $table_status->Rows;
1778 $updraftplus->log("Table $table: Total expected rows (approximate): ".$rows);
1779 $this->stow("# Approximate rows expected in table: $rows\n");
1780 if ($rows > UPDRAFTPLUS_WARN_DB_ROWS) {
1781 $manyrows_warning = true;
1782 $updraftplus->log(sprintf(__("Table %s has very many rows (%s) - we hope your web hosting company gives you enough resources to dump out that table in the backup", 'updraftplus'), $table, $rows).' '.__('If not, you will need to either remove data from this table, or contact your hosting company to request more resources.', 'updraftplus'), 'warning', 'manyrows_'.$this->whichdb_suffix.$table);
1783 }
1784 }
1785
1786 // Don't include the job data for any backups - so that when the database is restored, it doesn't continue an apparently incomplete backup
1787 if ('wp' == $this->whichdb && (!empty($this->table_prefix) && strtolower($this->table_prefix.'sitemeta') == strtolower($table))) {
1788 $where = 'meta_key NOT LIKE "updraft_jobdata_%"';
1789 } elseif ('wp' == $this->whichdb && (!empty($this->table_prefix) && strtolower($this->table_prefix.'options') == strtolower($table))) {
1790 if (strtolower(substr(PHP_OS, 0, 3)) == 'win') {
1791 $updraft_jobdata = "'updraft_jobdata_%'";
1792 $site_transient_update = "'_site_transient_update_%'";
1793 } else {
1794 $updraft_jobdata = '"updraft_jobdata_%"';
1795 $site_transient_update = '"_site_transient_update_%"';
1796 }
1797
1798 $where = 'option_name NOT LIKE '.$updraft_jobdata.' AND option_name NOT LIKE '.$site_transient_update.'';
1799 } else {
1800 $where = '';
1801 }
1802
1803 // If no check-in last time, then we could in future try the other method (but - any point in retrying slow method on large tables??)
1804
1805 // New Jul 2014: This attempt to use bindump instead at a lower threshold is quite conservative - only if the last successful run was exactly two resumptions ago - may be useful to expand
1806 $bindump_threshold = (!$updraftplus->something_useful_happened && !empty($updraftplus->current_resumption) && (2 == $updraftplus->current_resumption - $updraftplus->last_successful_resumption)) ? 1000 : 8000;
1807
1808 $bindump = (isset($rows) && ($rows>$bindump_threshold || (defined('UPDRAFTPLUS_ALWAYS_TRY_MYSQLDUMP') && UPDRAFTPLUS_ALWAYS_TRY_MYSQLDUMP)) && is_string($binsqldump)) ? $this->backup_table_bindump($binsqldump, $table, $where) : false;
1809 if (true !== $bindump) $this->backup_table($table, $where, 'none', $table_type);
1810
1811 if (!empty($manyrows_warning)) $updraftplus->log_remove_warning('manyrows_'.$this->whichdb_suffix.$table);
1812
1813 $this->close();
1814
1815 $updraftplus->log("Table $table: finishing file (${table_file_prefix}.gz - ".round(filesize($this->updraft_dir.'/'.$table_file_prefix.'.tmp.gz')/1024, 1)." KB)", 'notice', false, false);
1816
1817 rename($db_temp_file, $this->updraft_dir.'/'.$table_file_prefix.'.gz');
1818 UpdraftPlus_Job_Scheduler::something_useful_happened();
1819 $stitch_files[] = $table_file_prefix;
1820 }
1821 } else {
1822 $total_tables--;
1823 $updraftplus->log("Skipping table (lacks our prefix (".$this->table_prefix.")): $table");
1824 }
1825
1826 }
1827 }
1828
1829 if ('wp' == $whichdb) {
1830 if (!$found_options_table) {
1831 if ($is_multisite) {
1832 $updraftplus->log(__('The database backup appears to have failed', 'updraftplus').' - '.__('no options or sitemeta table was found', 'updraftplus'), 'warning', 'optstablenotfound');
1833 } else {
1834 $updraftplus->log(__('The database backup appears to have failed', 'updraftplus').' - '.__('the options table was not found', 'updraftplus'), 'warning', 'optstablenotfound');
1835 }
1836 $time_this_run = time()-$updraftplus->opened_log_time;
1837 if ($time_this_run > 2000) {
1838 // Have seen this happen; not sure how, but it was apparently deterministic; if the current process had been running for a long time, then apparently all database commands silently failed.
1839 // If we have been running that long, then the resumption may be far off; bring it closer
1840 UpdraftPlus_Job_Scheduler::reschedule(60);
1841 $updraftplus->log("Have been running very long, and it seems the database went away; scheduling a resumption and terminating for now");
1842 UpdraftPlus_Job_Scheduler::record_still_alive();
1843 die;
1844 }
1845 } else {
1846 $updraftplus->log_remove_warning('optstablenotfound');
1847 }
1848 }
1849
1850 // Race detection - with zip files now being resumable, these can more easily occur, with two running side-by-side
1851 $backup_final_file_name = $backup_file_base.'-db'.$this->whichdb_suffix.'.gz';
1852 $time_now = time();
1853 $time_mod = (int) @filemtime($backup_final_file_name);
1854 if (file_exists($backup_final_file_name) && $time_mod>100 && ($time_now-$time_mod)<30) {
1855 UpdraftPlus_Job_Scheduler::terminate_due_to_activity($backup_final_file_name, $time_now, $time_mod);
1856 }
1857 if (file_exists($backup_final_file_name)) {
1858 $updraftplus->log("The final database file ($backup_final_file_name) exists, but was apparently not modified within the last 30 seconds (time_mod=$time_mod, time_now=$time_now, diff=".($time_now-$time_mod)."). Thus we assume that another UpdraftPlus terminated; thus we will continue.");
1859 }
1860
1861 // Finally, stitch the files together
1862 if (!function_exists('gzopen')) {
1863 if (function_exists('gzopen64')) {
1864 $updraftplus->log("PHP function is disabled; abort expected: gzopen - buggy Ubuntu PHP version; try this plugin to help: https://wordpress.org/plugins/wp-ubuntu-gzopen-fix/");
1865 } else {
1866 $updraftplus->log("PHP function is disabled; abort expected: gzopen");
1867 }
1868 }
1869
1870 if (false === $this->backup_db_open($backup_final_file_name, true)) return false;
1871
1872 $this->backup_db_header();
1873
1874 // We delay the unlinking because if two runs go concurrently and fail to detect each other (should not happen, but there's no harm in assuming the detection failed) then that leads to files missing from the db dump
1875 $unlink_files = array();
1876
1877 $sind = 1;
1878 foreach ($stitch_files as $table_file) {
1879 $updraftplus->log("{$table_file}.gz ($sind/$how_many_tables): adding to final database dump");
1880 if (!$handle = gzopen($this->updraft_dir.'/'.$table_file.'.gz', "r")) {
1881 $updraftplus->log("Error: Failed to open database file for reading: ${table_file}.gz");
1882 $updraftplus->log(__("Failed to open database file for reading:", 'updraftplus').' '.$table_file.'.gz', 'error');
1883 $errors++;
1884 } else {
1885 while ($line = gzgets($handle, 65536)) {
1886 $this->stow($line);
1887 }
1888 gzclose($handle);
1889 $unlink_files[] = $this->updraft_dir.'/'.$table_file.'.gz';
1890 }
1891 $sind++;
1892 // Came across a database with 7600 tables... adding them all took over 500 seconds; and so when the resumption started up, no activity was detected
1893 if (0 == $sind % 100) UpdraftPlus_Job_Scheduler::something_useful_happened();
1894 }
1895
1896 $this->stow("/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;\n/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;\n/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;\n");
1897
1898 $updraftplus->log($file_base.'-db'.$this->whichdb_suffix.'.gz: finished writing out complete database file ('.round(filesize($backup_final_file_name)/1024, 1).' KB)');
1899 if (!$this->close()) {
1900 $updraftplus->log('An error occurred whilst closing the final database file');
1901 $updraftplus->log(__('An error occurred whilst closing the final database file', 'updraftplus'), 'error');
1902 $errors++;
1903 }
1904
1905 foreach ($unlink_files as $unlink_file) @unlink($unlink_file);
1906
1907 if ($errors > 0) {
1908 return false;
1909 } else {
1910 // We no longer encrypt here - because the operation can take long, we made it resumable and moved it to the upload loop
1911 $updraftplus->jobdata_set('jobstatus', 'dbcreated'.$this->whichdb_suffix);
1912
1913 $checksums = $updraftplus->which_checksums();
1914
1915 $checksum_description = '';
1916
1917 foreach ($checksums as $checksum) {
1918
1919 $cksum = hash_file($checksum, $backup_final_file_name);
1920 $updraftplus->jobdata_set($checksum.'-db'.(('wp' == $whichdb) ? '0' : $whichdb.'0'), $cksum);
1921 if ($checksum_description) $checksum_description .= ', ';
1922 $checksum_description .= "$checksum: $cksum";
1923
1924 }
1925
1926 $updraftplus->log("Total database tables backed up: $total_tables (".basename($backup_final_file_name).", size: ".filesize($backup_final_file_name).", $checksum)");
1927
1928 return basename($backup_final_file_name);
1929 }
1930
1931 }
1932
1933 private function backup_table_bindump($potsql, $table_name, $where) {
1934
1935 $microtime = microtime(true);
1936
1937 global $updraftplus;
1938
1939 // Deal with Windows/old MySQL setups with erroneous table prefixes differing in case
1940 // Can't get binary mysqldump to make this transformation
1941 // $dump_as_table = ($this->duplicate_tables_exist == false && stripos($table, $this->table_prefix) === 0 && strpos($table, $this->table_prefix) !== 0) ? $this->table_prefix.substr($table, strlen($this->table_prefix)) : $table;
1942
1943 $pfile = md5(time().rand()).'.tmp';
1944 file_put_contents($this->updraft_dir.'/'.$pfile, "[mysqldump]\npassword=".$this->dbinfo['pass']."\n");
1945
1946 // Note: escapeshellarg() adds quotes around the string
1947 if ($where) $where = "--where=".escapeshellarg($where);
1948
1949 if (strtolower(substr(PHP_OS, 0, 3)) == 'win') {
1950 $exec = "cd ".escapeshellarg(str_replace('/', '\\', $this->updraft_dir))." & ";
1951 } else {
1952 $exec = "cd ".escapeshellarg($this->updraft_dir)."; ";
1953 }
1954
1955 $exec .= "$potsql --defaults-file=$pfile $where --max_allowed_packet=1M --quote-names --add-drop-table --skip-comments --skip-set-charset --allow-keywords --dump-date --extended-insert --user=".escapeshellarg($this->dbinfo['user'])." --host=".escapeshellarg($this->dbinfo['host'])." ".$this->dbinfo['name']." ".escapeshellarg($table_name);
1956
1957 $ret = false;
1958 $any_output = false;
1959 $writes = 0;
1960 $handle = popen($exec, "r");
1961 if ($handle) {
1962 while (!feof($handle)) {
1963 $w = fgets($handle);
1964 if ($w) {
1965 $this->stow($w);
1966 $writes++;
1967 $any_output = true;
1968 }
1969 }
1970 $ret = pclose($handle);
1971 if (0 != $ret) {
1972 $updraftplus->log("Binary mysqldump: error (code: $ret)");
1973 // Keep counter of failures? Change value of binsqldump?
1974 } else {
1975 if ($any_output) {
1976 $updraftplus->log("Table $table_name: binary mysqldump finished (writes: $writes) in ".sprintf("%.02f", max(microtime(true)-$microtime, 0.00001))." seconds");
1977 $ret = true;
1978 }
1979 }
1980 } else {
1981 $updraftplus->log("Binary mysqldump error: bindump popen failed");
1982 }
1983
1984 // Clean temporary files
1985 @unlink($this->updraft_dir.'/'.$pfile);
1986
1987 return $ret;
1988
1989 }
1990
1991 /**
1992 * Taken partially from phpMyAdmin and partially from Alain Wolf, Zurich - Switzerland to use the WordPress $wpdb object
1993 * Website: http://restkultur.ch/personal/wolf/scripts/db_backup/
1994 * Modified by Scott Merrill (http://www.skippy.net/)
1995 *
1996 * @param String $table Table to backup
1997 * @param String $where If there is a where clause to use
1998 * @param String $segment Specify a segment (not used in UD)
1999 * @param String $table_type Table type
2000 * @return Boolean
2001 */
2002 private function backup_table($table, $where = '', $segment = 'none', $table_type = 'BASE TABLE') {
2003 global $updraftplus;
2004
2005 $microtime = microtime(true);
2006 $total_rows = 0;
2007
2008 // Deal with Windows/old MySQL setups with erroneous table prefixes differing in case
2009 $dump_as_table = (false == $this->duplicate_tables_exist && stripos($table, $this->table_prefix) === 0 && strpos($table, $this->table_prefix) !== 0) ? $this->table_prefix.substr($table, strlen($this->table_prefix)) : $table;
2010
2011 $table_structure = $this->wpdb_obj->get_results("DESCRIBE ".UpdraftPlus_Manipulation_Functions::backquote($table));
2012 if (!$table_structure) {
2013 // $updraftplus->log(__('Error getting table details','wp-db-backup') . ": $table", 'error');
2014 return false;
2015 }
2016
2017 if ('none' == $segment || 0 == $segment) {
2018 // Add SQL statement to drop existing table
2019 $this->stow("\n# Delete any existing table ".UpdraftPlus_Manipulation_Functions::backquote($table)."\n\n");
2020 $this->stow("DROP TABLE IF EXISTS " . UpdraftPlus_Manipulation_Functions::backquote($dump_as_table) . ";\n");
2021
2022 if ('VIEW' == $table_type) {
2023 $this->stow("DROP VIEW IF EXISTS " . UpdraftPlus_Manipulation_Functions::backquote($dump_as_table) . ";\n");
2024 }
2025
2026 // Table structure
2027 // Comment in SQL-file
2028
2029 $description = ('VIEW' == $table_type) ? 'view' : 'table';
2030
2031 $this->stow("\n# Table structure of $description ".UpdraftPlus_Manipulation_Functions::backquote($table)."\n\n");
2032
2033 $create_table = $this->wpdb_obj->get_results("SHOW CREATE TABLE ".UpdraftPlus_Manipulation_Functions::backquote($table), ARRAY_N);
2034 if (false === $create_table) {
2035 $err_msg ='Error with SHOW CREATE TABLE for '.$table;
2036 // $updraftplus->log($err_msg, 'error');
2037 $this->stow("#\n# $err_msg\n#\n");
2038 }
2039 $create_line = UpdraftPlus_Manipulation_Functions::str_lreplace('TYPE=', 'ENGINE=', $create_table[0][1]);
2040
2041 // Remove PAGE_CHECKSUM parameter from MyISAM - was internal, undocumented, later removed (so causes errors on import)
2042 if (preg_match('/ENGINE=([^\s;]+)/', $create_line, $eng_match)) {
2043 $engine = $eng_match[1];
2044 if ('myisam' == strtolower($engine)) {
2045 $create_line = preg_replace('/PAGE_CHECKSUM=\d\s?/', '', $create_line, 1);
2046 }
2047 }
2048
2049 if ($dump_as_table !== $table) $create_line = UpdraftPlus_Manipulation_Functions::str_replace_once($table, $dump_as_table, $create_line);
2050
2051 $this->stow($create_line.' ;');
2052
2053 if (false === $table_structure) {
2054 $err_msg = sprintf("Error getting $description structure of %s", $table);
2055 $this->stow("#\n# $err_msg\n#\n");
2056 }
2057
2058 // Comment in SQL-file
2059 $this->stow("\n\n# ".sprintf("Data contents of $description %s", UpdraftPlus_Manipulation_Functions::backquote($table))."\n\n");
2060
2061 }
2062
2063 // Some tables have optional data, and should be skipped if they do not work
2064 $table_sans_prefix = substr($table, strlen($this->table_prefix_raw));
2065 $data_optional_tables = ('wp' == $this->whichdb) ? apply_filters('updraftplus_data_optional_tables', explode(',', UPDRAFTPLUS_DATA_OPTIONAL_TABLES)) : array();
2066 if (in_array($table_sans_prefix, $data_optional_tables)) {
2067 if (!$updraftplus->something_useful_happened && !empty($updraftplus->current_resumption) && ($updraftplus->current_resumption - $updraftplus->last_successful_resumption > 2)) {
2068 $updraftplus->log("Table $table: Data skipped (previous attempts failed, and table is marked as non-essential)");
2069 return true;
2070 }
2071 }
2072
2073 // In UpdraftPlus, segment is always 'none'
2074 if ('VIEW' != $table_type && ('none' == $segment || 0 <= $segment)) {
2075 $defs = array();
2076 $integer_fields = array();
2077 // $table_structure was from "DESCRIBE $table"
2078 foreach ($table_structure as $struct) {
2079 if ((0 === strpos($struct->Type, 'tinyint')) || (0 === strpos(strtolower($struct->Type), 'smallint'))
2080 || (0 === strpos(strtolower($struct->Type), 'mediumint')) || (0 === strpos(strtolower($struct->Type), 'int')) || (0 === strpos(strtolower($struct->Type), 'bigint'))
2081 ) {
2082 $defs[strtolower($struct->Field)] = (null === $struct->Default ) ? 'NULL' : $struct->Default;
2083 $integer_fields[strtolower($struct->Field)] = "1";
2084 }
2085 }
2086
2087 // Experimentation here shows that on large tables (we tested with 180,000 rows) on MyISAM, 1000 makes the table dump out 3x faster than the previous value of 100. After that, the benefit diminishes (increasing to 4000 only saved another 12%)
2088
2089 $increment = 1000;
2090 if (!$updraftplus->something_useful_happened && !empty($updraftplus->current_resumption) && ($updraftplus->current_resumption - $updraftplus->last_successful_resumption > 1)) {
2091 // This used to be fixed at 500; but we (after a long time) saw a case that looked like an out-of-memory even at this level. We must be careful about going too low, though - otherwise we increase the risks of timeouts.
2092 $increment = ($updraftplus->current_resumption - $updraftplus->last_successful_resumption > 2) ? 350 : 500;
2093 }
2094
2095 if ('none' == $segment) {
2096 $row_start = 0;
2097 $row_inc = $increment;
2098 } else {
2099 $row_start = $segment * $increment;
2100 $row_inc = $increment;
2101 }
2102
2103 $search = array("\x00", "\x0a", "\x0d", "\x1a");
2104 $replace = array('\0', '\n', '\r', '\Z');
2105
2106 if ($where) $where = "WHERE $where";
2107
2108 do {
2109 @set_time_limit(UPDRAFTPLUS_SET_TIME_LIMIT);
2110
2111 $table_data = $this->wpdb_obj->get_results("SELECT * FROM ".UpdraftPlus_Manipulation_Functions::backquote($table)." $where LIMIT {$row_start}, {$row_inc}", ARRAY_A);
2112 $entries = 'INSERT INTO '.UpdraftPlus_Manipulation_Functions::backquote($dump_as_table).' VALUES ';
2113 // \x08\\x09, not required
2114 if ($table_data) {
2115 $thisentry = "";
2116 foreach ($table_data as $row) {
2117 $total_rows++;
2118 $values = array();
2119 foreach ($row as $key => $value) {
2120 if (isset($integer_fields[strtolower($key)])) {
2121 // make sure there are no blank spots in the insert syntax,
2122 // yet try to avoid quotation marks around integers
2123 $value = (null === $value || '' === $value) ? $defs[strtolower($key)] : $value;
2124 $values[] = ('' === $value) ? "''" : $value;
2125 } else {
2126 $values[] = (null === $value) ? 'NULL' : "'" . str_replace($search, $replace, str_replace('\'', '\\\'', str_replace('\\', '\\\\', $value))) . "'";
2127 }
2128 }
2129 if ($thisentry) $thisentry .= ",\n ";
2130 $thisentry .= '('.implode(', ', $values).')';
2131 // Flush every 512KB
2132 if (strlen($thisentry) > 524288) {
2133 $this->stow(" \n".$entries.$thisentry.';');
2134 $thisentry = "";
2135 }
2136
2137 }
2138 if ($thisentry) $this->stow(" \n".$entries.$thisentry.';');
2139 $row_start += $row_inc;
2140 }
2141 } while (count($table_data) > 0 && 'none' == $segment);
2142 }
2143
2144 if ('none' == $segment || $segment < 0) {
2145 // Create footer/closing comment in SQL-file
2146 $this->stow("\n# End of data contents of table ".UpdraftPlus_Manipulation_Functions::backquote($table)."\n\n");
2147 }
2148 $updraftplus->log("Table $table: Total rows added: $total_rows in ".sprintf("%.02f", max(microtime(true)-$microtime, 0.00001))." seconds");
2149
2150 }
2151
2152 /*END OF WP-DB-BACKUP BLOCK */
2153
2154 /**
2155 * Encrypts the file if the option is set; returns the basename of the file (according to whether it was encrypted or nto)
2156 *
2157 * @param string $file file to encrypt
2158 * @return array
2159 */
2160 public function encrypt_file($file) {
2161 global $updraftplus;
2162 $encryption = $updraftplus->get_job_option('updraft_encryptionphrase');
2163 if (strlen($encryption) > 0) {
2164 $updraftplus->log("Attempting to encrypt backup file");
2165 try {
2166 $result = apply_filters('updraft_encrypt_file', null, $file, $encryption, $this->whichdb, $this->whichdb_suffix);
2167 } catch (Exception $e) {
2168 $log_message = 'Exception ('.get_class($e).') occurred during encryption: '.$e->getMessage().' (Code: '.$e->getCode().', line '.$e->getLine().' in '.$e->getFile().')';
2169 error_log($log_message);
2170 // @codingStandardsIgnoreLine
2171 if (function_exists('wp_debug_backtrace_summary')) $log_message .= ' Backtrace: '.wp_debug_backtrace_summary();
2172 $updraftplus->log($log_message);
2173 $updraftplus->log(sprintf(__('A PHP exception (%s) has occurred: %s', 'updraftplus'), get_class($e), $e->getMessage()), 'error');
2174 die();
2175 // @codingStandardsIgnoreLine
2176 } catch (Error $e) {
2177 $log_message = 'PHP Fatal error ('.get_class($e).') has occurred during encryption. Error Message: '.$e->getMessage().' (Code: '.$e->getCode().', line '.$e->getLine().' in '.$e->getFile().')';
2178 error_log($log_message);
2179 // @codingStandardsIgnoreLine
2180 if (function_exists('wp_debug_backtrace_summary')) $log_message .= ' Backtrace: '.wp_debug_backtrace_summary();
2181 $updraftplus->log($log_message);
2182 $updraftplus->log(sprintf(__('A PHP fatal error (%s) has occurred: %s', 'updraftplus'), get_class($e), $e->getMessage()), 'error');
2183 die();
2184 }
2185 if (null === $result) return basename($file);
2186 return $result;
2187 } else {
2188 return basename($file);
2189 }
2190 }
2191
2192 public function close() {
2193 return $this->dbhandle_isgz ? gzclose($this->dbhandle) : fclose($this->dbhandle);
2194 }
2195
2196 /**
2197 * Open a file, store its filehandle
2198 *
2199 * @param String $file Full path to the file to open
2200 * @param Boolean $allow_gz Use gzopen() if available, instead of fopen()
2201 * @return Resource - the opened file handle
2202 */
2203 public function backup_db_open($file, $allow_gz = true) {
2204 if (function_exists('gzopen') && true == $allow_gz) {
2205 $this->dbhandle = @gzopen($file, 'w');
2206 $this->dbhandle_isgz = true;
2207 } else {
2208 $this->dbhandle = @fopen($file, 'w');
2209 $this->dbhandle_isgz = false;
2210 }
2211 if (false === $this->dbhandle) {
2212 global $updraftplus;
2213 $updraftplus->log("ERROR: $file: Could not open the backup file for writing");
2214 $updraftplus->log($file.": ".__("Could not open the backup file for writing", 'updraftplus'), 'error');
2215 }
2216 return $this->dbhandle;
2217 }
2218
2219 /**
2220 * Adds a line to the database backup
2221 *
2222 * @param String $query_line - the line to log
2223 *
2224 * @return Integer|Boolean - the number of octets written, or false for a failure (as returned by gzwrite() / fwrite)
2225 */
2226 public function stow($query_line) {
2227 if ($this->dbhandle_isgz) {
2228 if (false == ($ret = @gzwrite($this->dbhandle, $query_line))) {
2229 // $updraftplus->log(__('There was an error writing a line to the backup script:','wp-db-backup').' '.$query_line.' '.$php_errormsg, 'error');
2230 }
2231 } else {
2232 if (false == ($ret = @fwrite($this->dbhandle, $query_line))) {
2233 // $updraftplus->log(__('There was an error writing a line to the backup script:','wp-db-backup').' '.$query_line.' '.$php_errormsg, 'error');
2234 }
2235 }
2236 return $ret;
2237 }
2238
2239 /**
2240 * Stow the database backup header
2241 */
2242 private function backup_db_header() {
2243
2244 global $updraftplus;
2245 $wp_version = $updraftplus->get_wordpress_version();
2246 $mysql_version = $this->wpdb_obj->get_var('SELECT VERSION()');
2247 if ('' == $mysql_version) $mysql_version = $this->wpdb_obj->db_version();
2248
2249 if ('wp' == $this->whichdb) {
2250 $wp_upload_dir = wp_upload_dir();
2251 $this->stow("# WordPress MySQL database backup\n");
2252 $this->stow("# Created by UpdraftPlus version ".$updraftplus->version." (https://updraftplus.com)\n");
2253 $this->stow("# WordPress Version: $wp_version, running on PHP ".phpversion()." (".$_SERVER["SERVER_SOFTWARE"]."), MySQL $mysql_version\n");
2254 $this->stow("# Backup of: ".untrailingslashit(site_url())."\n");
2255 $this->stow("# Home URL: ".untrailingslashit(home_url())."\n");
2256 $this->stow("# Content URL: ".untrailingslashit(content_url())."\n");
2257 $this->stow("# Uploads URL: ".untrailingslashit($wp_upload_dir['baseurl'])."\n");
2258 $this->stow("# Table prefix: ".$this->table_prefix_raw."\n");
2259 $this->stow("# Filtered table prefix: ".$this->table_prefix."\n");
2260 $this->stow("# Site info: multisite=".(is_multisite() ? '1' : '0')."\n");
2261 $this->stow("# Site info: end\n");
2262 } else {
2263 $this->stow("# MySQL database backup (supplementary database ".$this->whichdb.")\n");
2264 $this->stow("# Created by UpdraftPlus version ".$updraftplus->version." (https://updraftplus.com)\n");
2265 $this->stow("# WordPress Version: $wp_version, running on PHP ".phpversion()." (".$_SERVER["SERVER_SOFTWARE"]."), MySQL $mysql_version\n");
2266 $this->stow("# ".sprintf('External database: (%s)', $this->dbinfo['user'].'@'.$this->dbinfo['host'].'/'.$this->dbinfo['name'])."\n");
2267 $this->stow("# Backup created by: ".untrailingslashit(site_url())."\n");
2268 $this->stow("# Table prefix: ".$this->table_prefix_raw."\n");
2269 $this->stow("# Filtered table prefix: ".$this->table_prefix."\n");
2270 }
2271
2272 $label = $updraftplus->jobdata_get('label');
2273 if (!empty($label)) $this->stow("# Label: $label\n");
2274
2275 $this->stow("\n# Generated: ".date("l j. F Y H:i T")."\n");
2276 $this->stow("# Hostname: ".$this->dbinfo['host']."\n");
2277 $this->stow("# Database: ".UpdraftPlus_Manipulation_Functions::backquote($this->dbinfo['name'])."\n");
2278
2279 if (!empty($this->skipped_tables)) {
2280 if ('wp' == $this->whichdb) {
2281 $this->stow("# Skipped tables: " . $this->skipped_tables[$this->whichdb]."\n");
2282 } elseif (isset($this->skipped_tables[$this->dbinfo['name']])) {
2283 $this->stow("# Skipped tables: " . $this->skipped_tables[$this->dbinfo['name']]."\n");
2284 }
2285 }
2286
2287 $this->stow("# --------------------------------------------------------\n");
2288
2289 $this->stow("/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;\n");
2290 $this->stow("/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;\n");
2291 $this->stow("/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;\n");
2292 $this->stow("/*!40101 SET NAMES ".$updraftplus->get_connection_charset($this->wpdb_obj)." */;\n");
2293 $this->stow("/*!40101 SET foreign_key_checks = 0 */;\n\n");
2294
2295 }
2296
2297
2298 public function phpmailer_init($phpmailer) {
2299 global $updraftplus;
2300 if (empty($this->attachments) || !is_array($this->attachments)) return;
2301 foreach ($this->attachments as $attach) {
2302 $mime_type = (preg_match('/\.gz$/', $attach)) ? 'application/x-gzip' : 'text/plain';
2303 try {
2304 $phpmailer->AddAttachment($attach, '', 'base64', $mime_type);
2305 } catch (Exception $e) {
2306 $updraftplus->log("Exception occurred when adding attachment (".get_class($e)."): ".$e->getMessage());
2307 }
2308 }
2309 }
2310
2311 /**
2312 * This function recursively packs the zip, dereferencing symlinks but packing into a single-parent tree for universal unpacking
2313 *
2314 * @param String $fullpath Full path
2315 * @param String $use_path_when_storing Controls the path to use when storing in the zip file
2316 * @param String $original_fullpath Original path
2317 * @param Integer $startlevels How deep within the directory structure the recursive operation has gone
2318 * @param Array $exclude passed by reference so that we can remove elements as they are matched - saves time checking against already-dealt-with objects]
2319 * @return Boolean
2320 */
2321 private function makezip_recursive_add($fullpath, $use_path_when_storing, $original_fullpath, $startlevels = 1, &$exclude) {
2322
2323 // $zipfile = $this->zip_basename.(($this->index == 0) ? '' : ($this->index+1)).'.zip.tmp';
2324
2325 global $updraftplus;
2326
2327 // Only BinZip supports symlinks. This means that as a consistent outcome, the only think that can be done with directory symlinks is either a) potentially duplicate the data or b) skip it. Whilst with internal WP entities (e.g. plugins) we definitely want the data, in the case of user-selected directories, we assume the user knew what they were doing when they chose the directory - i.e. we can skip symlink-accessed data that's outside.
2328 if (is_link($fullpath) && is_dir($fullpath) && 'more' == $this->whichone) {
2329 $updraftplus->log("Directory symlink encounted in more files backup: $use_path_when_storing -> ".readlink($fullpath).": skipping");
2330 return true;
2331 }
2332
2333 // De-reference. Important to do to both, because on Windows only doing it to one can make them non-equal, where they were previously equal - something which we later rely upon
2334 $fullpath = realpath($fullpath);
2335 $original_fullpath = realpath($original_fullpath);
2336
2337 // Is the place we've ended up above the original base? That leads to infinite recursion
2338 if (($fullpath !== $original_fullpath && strpos($original_fullpath, $fullpath) === 0) || ($original_fullpath == $fullpath && ((1== $startlevels && strpos($use_path_when_storing, '/') !== false) || (2 == $startlevels && substr_count($use_path_when_storing, '/') >1)))) {
2339 $updraftplus->log("Infinite recursion: symlink led us to $fullpath, which is within $original_fullpath");
2340 $updraftplus->log(__("Infinite recursion: consult your log for more information", 'updraftplus'), 'error');
2341 return false;
2342 }
2343
2344 // This is sufficient for the ones we have exclude options for - uploads, others, wpcore
2345 $stripped_storage_path = (1 == $startlevels) ? $use_path_when_storing : substr($use_path_when_storing, strpos($use_path_when_storing, '/') + 1);
2346 if (false !== ($fkey = array_search($stripped_storage_path, $exclude))) {
2347 $updraftplus->log("Entity excluded by configuration option: $stripped_storage_path");
2348 unset($exclude[$fkey]);
2349 return true;
2350 }
2351
2352 $if_altered_since = $this->makezip_if_altered_since;
2353
2354 if (is_file($fullpath)) {
2355 if (!empty($this->excluded_extensions) && $this->is_entity_excluded_by_extension($fullpath)) {
2356 $updraftplus->log("Entity excluded by configuration option (extension): ".basename($fullpath));
2357 } elseif (!empty($this->excluded_prefixes) && $this->is_entity_excluded_by_prefix($fullpath)) {
2358 $updraftplus->log("Entity excluded by configuration option (prefix): ".basename($fullpath));
2359 } elseif (apply_filters('updraftplus_exclude_file', false, $fullpath)) {
2360 $updraftplus->log("Entity excluded by filter: ".basename($fullpath));
2361 } elseif (is_readable($fullpath)) {
2362 $mtime = filemtime($fullpath);
2363 $key = ($fullpath == $original_fullpath) ? ((2 == $startlevels) ? $use_path_when_storing : $this->basename($fullpath)) : $use_path_when_storing.'/'.$this->basename($fullpath);
2364 if ($mtime > 0 && $mtime > $if_altered_since) {
2365 $this->zipfiles_batched[$fullpath] = $key;
2366 $this->makezip_recursive_batchedbytes += @filesize($fullpath);
2367 // @touch($zipfile);
2368 } else {
2369 $this->zipfiles_skipped_notaltered[$fullpath] = $key;
2370 }
2371 } else {
2372 $updraftplus->log("$fullpath: unreadable file");
2373 $updraftplus->log(sprintf(__("%s: unreadable file - could not be backed up (check the file permissions and ownership)", 'updraftplus'), $fullpath), 'warning');
2374 }
2375 } elseif (is_dir($fullpath)) {
2376 if ($fullpath == $this->updraft_dir_realpath) {
2377 $updraftplus->log("Skip directory (UpdraftPlus backup directory): $use_path_when_storing");
2378 return true;
2379 }
2380
2381 if (apply_filters('updraftplus_exclude_directory', false, $fullpath, $use_path_when_storing)) {
2382 $updraftplus->log("Skip filtered directory: $use_path_when_storing");
2383 return true;
2384 }
2385
2386 if (file_exists($fullpath.'/.donotbackup')) {
2387 $updraftplus->log("Skip directory (.donotbackup file found): $use_path_when_storing");
2388 return true;
2389 }
2390
2391 if (!isset($this->existing_files[$use_path_when_storing])) $this->zipfiles_dirbatched[] = $use_path_when_storing;
2392
2393 if (!$dir_handle = @opendir($fullpath)) {
2394 $updraftplus->log("Failed to open directory: $fullpath");
2395 $updraftplus->log(sprintf(__("Failed to open directory (check the file permissions and ownership): %s", 'updraftplus'), $fullpath), 'error');
2396 return false;
2397 }
2398
2399 while (false !== ($e = readdir($dir_handle))) {
2400 if ('.' == $e || '..' == $e) continue;
2401
2402 if (is_link($fullpath.'/'.$e)) {
2403 $deref = realpath($fullpath.'/'.$e);
2404 if (is_file($deref)) {
2405 if (is_readable($deref)) {
2406 $use_stripped = $stripped_storage_path.'/'.$e;
2407 if (false !== ($fkey = array_search($use_stripped, $exclude))) {
2408 $updraftplus->log("Entity excluded by configuration option: $use_stripped");
2409 unset($exclude[$fkey]);
2410 } elseif (!empty($this->excluded_extensions) && $this->is_entity_excluded_by_extension($e)) {
2411 $updraftplus->log("Entity excluded by configuration option (extension): $use_stripped");
2412 } elseif (!empty($this->excluded_prefixes) && $this->is_entity_excluded_by_prefix($e)) {
2413 $updraftplus->log("Entity excluded by configuration option (prefix): $use_stripped");
2414 } elseif (apply_filters('updraftplus_exclude_file', false, $deref, $use_stripped)) {
2415 $updraftplus->log("Entity excluded by filter: $use_stripped");
2416 } else {
2417 $mtime = filemtime($deref);
2418 if ($mtime > 0 && $mtime > $if_altered_since) {
2419 $this->zipfiles_batched[$deref] = $use_path_when_storing.'/'.$e;
2420 $this->makezip_recursive_batchedbytes += @filesize($deref);
2421 // @touch($zipfile);
2422 } else {
2423 $this->zipfiles_skipped_notaltered[$deref] = $use_path_when_storing.'/'.$e;
2424 }
2425 }
2426 } else {
2427 $updraftplus->log("$deref: unreadable file");
2428 $updraftplus->log(sprintf(__("%s: unreadable file - could not be backed up"), $deref), 'warning');
2429 }
2430 } elseif (is_dir($deref)) {
2431
2432 // $link_target = readlink($deref);
2433 // $updraftplus->log("Symbolic link $use_path_when_storing/$e -> $link_target");
2434
2435 $this->makezip_recursive_add($deref, $use_path_when_storing.'/'.$e, $original_fullpath, $startlevels, $exclude);
2436 }
2437 } elseif (is_file($fullpath.'/'.$e)) {
2438 if (is_readable($fullpath.'/'.$e)) {
2439 $use_stripped = $stripped_storage_path.'/'.$e;
2440 if (false !== ($fkey = array_search($use_stripped, $exclude))) {
2441 $updraftplus->log("Entity excluded by configuration option: $use_stripped");
2442 unset($exclude[$fkey]);
2443 } elseif (!empty($this->excluded_extensions) && $this->is_entity_excluded_by_extension($e)) {
2444 $updraftplus->log("Entity excluded by configuration option (extension): $use_stripped");
2445 } elseif (!empty($this->excluded_prefixes) && $this->is_entity_excluded_by_prefix($e)) {
2446 $updraftplus->log("Entity excluded by configuration option (prefix): $use_stripped");
2447 } elseif (apply_filters('updraftplus_exclude_file', false, $fullpath.'/'.$e)) {
2448 $updraftplus->log("Entity excluded by filter: $use_stripped");
2449 } else {
2450 $mtime = filemtime($fullpath.'/'.$e);
2451 if ($mtime > 0 && $mtime > $if_altered_since) {
2452 $this->zipfiles_batched[$fullpath.'/'.$e] = $use_path_when_storing.'/'.$e;
2453 $this->makezip_recursive_batchedbytes += @filesize($fullpath.'/'.$e);
2454 } else {
2455 $this->zipfiles_skipped_notaltered[$fullpath.'/'.$e] = $use_path_when_storing.'/'.$e;
2456 }
2457 }
2458 } else {
2459 $updraftplus->log("$fullpath/$e: unreadable file");
2460 $updraftplus->log(sprintf(__("%s: unreadable file - could not be backed up", 'updraftplus'), $use_path_when_storing.'/'.$e), 'warning', "unrfile-$e");
2461 }
2462 } elseif (is_dir($fullpath.'/'.$e)) {
2463 if ('wpcore' == $this->whichone && 'updraft' == $e && basename($use_path_when_storing) == 'wp-content' && (!defined('UPDRAFTPLUS_WPCORE_INCLUDE_UPDRAFT_DIRS') || !UPDRAFTPLUS_WPCORE_INCLUDE_UPDRAFT_DIRS)) {
2464 // This test, of course, won't catch everything - it just aims to make things better by default
2465 $updraftplus->log("Directory excluded for looking like a sub-site's internal UpdraftPlus directory (enable by defining UPDRAFTPLUS_WPCORE_INCLUDE_UPDRAFT_DIRS): ".$use_path_when_storing.'/'.$e);
2466 } else {
2467 // no need to add_empty_dir here, as it gets done when we recurse
2468 $this->makezip_recursive_add($fullpath.'/'.$e, $use_path_when_storing.'/'.$e, $original_fullpath, $startlevels, $exclude);
2469 }
2470 }
2471 }
2472 closedir($dir_handle);
2473 } else {
2474 $updraftplus->log("Unexpected: path ($use_path_when_storing) fails both is_file() and is_dir()");
2475 }
2476
2477 return true;
2478
2479 }
2480
2481 private function get_excluded_extensions($exclude) {
2482 if (!is_array($exclude)) $exclude = array();
2483 $exclude_extensions = array();
2484 foreach ($exclude as $ex) {
2485 if (preg_match('/^ext:(.+)$/i', $ex, $matches)) {
2486 $exclude_extensions[] = strtolower($matches[1]);
2487 }
2488 }
2489
2490 if (defined('UPDRAFTPLUS_EXCLUDE_EXTENSIONS')) {
2491 $exclude_from_define = explode(',', UPDRAFTPLUS_EXCLUDE_EXTENSIONS);
2492 foreach ($exclude_from_define as $ex) {
2493 $exclude_extensions[] = strtolower(trim($ex));
2494 }
2495 }
2496
2497 return $exclude_extensions;
2498 }
2499
2500 private function get_excluded_prefixes($exclude) {
2501 if (!is_array($exclude)) $exclude = array();
2502 $exclude_prefixes = array();
2503 foreach ($exclude as $pref) {
2504 if (preg_match('/^prefix:(.+)$/i', $pref, $matches)) {
2505 $exclude_prefixes[] = strtolower($matches[1]);
2506 }
2507 }
2508
2509 return $exclude_prefixes;
2510 }
2511
2512 private function is_entity_excluded_by_extension($entity) {
2513 foreach ($this->excluded_extensions as $ext) {
2514 if (!$ext) continue;
2515 $eln = strlen($ext);
2516 if (strtolower(substr($entity, -$eln, $eln)) == $ext) return true;
2517 }
2518 return false;
2519 }
2520
2521 private function is_entity_excluded_by_prefix($entity) {
2522 $entity = basename($entity);
2523 foreach ($this->excluded_prefixes as $pref) {
2524 if (!$pref) continue;
2525 $eln = strlen($pref);
2526 if (strtolower(substr($entity, 0, $eln)) == $pref) return true;
2527 }
2528 return false;
2529 }
2530
2531 private function unserialize_gz_cache_file($file) {
2532 if (!$whandle = gzopen($file, 'r')) return false;
2533 global $updraftplus;
2534 $emptimes = 0;
2535 $var = '';
2536 while (!gzeof($whandle)) {
2537 $bytes = @gzread($whandle, 1048576);
2538 if (empty($bytes)) {
2539 $emptimes++;
2540 $updraftplus->log("Got empty gzread ($emptimes times)");
2541 if ($emptimes>2) return false;
2542 } else {
2543 $var .= $bytes;
2544 }
2545 }
2546 gzclose($whandle);
2547 return unserialize($var);
2548 }
2549
2550
2551
2552 /**
2553 * Make Zip File. $destination is the temporary file (ending in .tmp)
2554 *
2555 * @param Array|String $source Caution: $source is allowed to be an array, not just a filename
2556 * @param String $backup_file_basename Name of backup file
2557 * @param String $whichone Backup entity type (e.g. 'plugins')
2558 * @param Boolean $retry_on_error Set to retry upon error
2559 * @return Boolean
2560 */
2561 private function make_zipfile($source, $backup_file_basename, $whichone, $retry_on_error = true) {
2562
2563 global $updraftplus;
2564
2565 $original_index = $this->index;
2566
2567 $itext = (empty($this->index)) ? '' : ($this->index+1);
2568 $destination_base = $backup_file_basename.'-'.$whichone.$itext.'.zip.tmp';
2569 $destination = $this->updraft_dir.'/'.$destination_base;
2570
2571 // Legacy/redundant
2572 // if (empty($whichone) && is_string($whichone)) $whichone = basename($source);
2573
2574 // When to prefer PCL:
2575 // - We were asked to
2576 // - No zip extension present and no relevant method present
2577 // The zip extension check is not redundant, because method_exists segfaults some PHP installs, leading to support requests
2578
2579 // We need meta-info about $whichone
2580 $backupable_entities = $updraftplus->get_backupable_file_entities(true, false);
2581 // This is only used by one corner-case in BinZip
2582 // $this->make_zipfile_source = (isset($backupable_entities[$whichone])) ? $backupable_entities[$whichone] : $source;
2583 $this->make_zipfile_source = (is_array($source) && isset($backupable_entities[$whichone])) ? (('uploads' == $whichone) ? dirname($backupable_entities[$whichone]) : $backupable_entities[$whichone]) : dirname($source);
2584
2585 $this->existing_files = array();
2586 // Used for tracking compression ratios
2587 $this->existing_files_rawsize = 0;
2588 $this->existing_zipfiles_size = 0;
2589
2590 // Enumerate existing files
2591 // Usually first_linked_index is zero; the exception being with more files, where previous zips' contents are irrelevant
2592 for ($j=$this->first_linked_index; $j<=$this->index; $j++) {
2593 $jtext = (0 == $j) ? '' : ($j+1);
2594 // This is, in a non-obvious way, compatible with filenames which indicate increments
2595 // $j does not need to start at zero; it should start at the index which the current entity split at. However, this is not directly known, and can only be deduced from examining the filenames. And, for other indexes from before the current increment, the searched-for filename won't exist (even if there is no cloud storage). So, this indirectly results in the desired outcome when we start from $j=0.
2596 $examine_zip = $this->updraft_dir.'/'.$backup_file_basename.'-'.$whichone.$jtext.'.zip'.(($j == $this->index) ? '.tmp' : '');
2597
2598 // This comes from https://wordpress.org/support/topic/updraftplus-not-moving-all-files-to-remote-server - where it appears that the jobdata's record of the split was done (i.e. database write), but the *earlier* rename of the .tmp file was not done (i.e. I/O lost). i.e. In theory, this should be impossible; but, the sychnronicity apparently cannot be fully relied upon in some setups. The check for the index being one behind is being conservative - there's no inherent reason why it couldn't be done for other indexes.
2599 // Note that in this 'impossible' case, no backup data was being lost - the design still ensures that the on-disk backup is fine. The problem was a gap in the sequence numbering of the zip files, leading to user confusion.
2600 // Other examples of this appear to be in HS#1001 and #1047
2601 if ($j != $this->index && !file_exists($examine_zip)) {
2602 $alt_examine_zip = $this->updraft_dir.'/'.$backup_file_basename.'-'.$whichone.$jtext.'.zip'.(($j == $this->index - 1) ? '.tmp' : '');
2603 if ($alt_examine_zip != $examine_zip && file_exists($alt_examine_zip) && is_readable($alt_examine_zip) && filesize($alt_examine_zip)>0) {
2604 $updraftplus->log("Looked-for zip file not found; but non-zero .tmp zip was, despite not being current index ($j != ".$this->index." - renaming zip (assume previous resumption's IO was lost before kill)");
2605 if (rename($alt_examine_zip, $examine_zip)) {
2606 clearstatcache();
2607 } else {
2608 $updraftplus->log("Rename failed - backup zips likely to not have sequential numbers (does not affect backup integrity, but can cause user confusion)");
2609 }
2610 }
2611 }
2612
2613 // If the file exists, then we should grab its index of files inside, and sizes
2614 // Then, when we come to write a file, we should check if it's already there, and only add if it is not
2615 if (file_exists($examine_zip) && is_readable($examine_zip) && filesize($examine_zip)>0) {
2616 $this->existing_zipfiles_size += filesize($examine_zip);
2617 $zip = new $this->use_zip_object;
2618 if (true !== $zip->open($examine_zip)) {
2619 $updraftplus->log("Could not open zip file to examine (".$zip->last_error."); will remove: ".basename($examine_zip));
2620 @unlink($examine_zip);
2621 } else {
2622
2623 // Don't put this in the for loop, or the magic __get() method gets repeatedly called every time the loop goes round
2624 $numfiles = $zip->numFiles;
2625
2626 for ($i=0; $i < $numfiles; $i++) {
2627 $si = $zip->statIndex($i);
2628 $name = $si['name'];
2629 // Exclude folders
2630 if ('/' == substr($name, -1)) continue;
2631 $this->existing_files[$name] = $si['size'];
2632 $this->existing_files_rawsize += $si['size'];
2633 }
2634
2635 @$zip->close();
2636 }
2637
2638 $updraftplus->log(basename($examine_zip).": Zip file already exists, with ".count($this->existing_files)." files");
2639
2640 // try_split is set if there have been no check-ins recently - or if it needs to be split anyway
2641 if ($j == $this->index) {
2642 if (isset($this->try_split)) {
2643 if (filesize($examine_zip) > 50*1048576) {
2644 // We could, as a future enhancement, save this back to the job data, if we see a case that needs it
2645 $this->zip_split_every = max(
2646 (int) $this->zip_split_every/2,
2647 UPDRAFTPLUS_SPLIT_MIN*1048576,
2648 min(filesize($examine_zip)-1048576, $this->zip_split_every)
2649 );
2650 $updraftplus->jobdata_set('split_every', (int) ($this->zip_split_every/1048576));
2651 $updraftplus->log("No check-in on last two runs; bumping index and reducing zip split to: ".round($this->zip_split_every/1048576, 1)." MB");
2652 $do_bump_index = true;
2653 }
2654 unset($this->try_split);
2655 } elseif (filesize($examine_zip) > $this->zip_split_every) {
2656 $updraftplus->log(sprintf("Zip size is at/near split limit (%s MB / %s MB) - bumping index (from: %d)", filesize($examine_zip), round($this->zip_split_every/1048576, 1), $this->index));
2657 $do_bump_index = true;
2658 }
2659 }
2660
2661 } elseif (file_exists($examine_zip)) {
2662 $updraftplus->log("Zip file already exists, but is not readable or was zero-sized; will remove: ".basename($examine_zip));
2663 @unlink($examine_zip);
2664 }
2665 }
2666
2667 $this->zip_last_ratio = ($this->existing_files_rawsize > 0) ? ($this->existing_zipfiles_size/$this->existing_files_rawsize) : 1;
2668
2669 $this->zipfiles_added = 0;
2670 $this->zipfiles_added_thisrun = 0;
2671 $this->zipfiles_dirbatched = array();
2672 $this->zipfiles_batched = array();
2673 $this->zipfiles_skipped_notaltered = array();
2674 $this->zipfiles_lastwritetime = time();
2675 $this->zip_basename = $this->updraft_dir.'/'.$backup_file_basename.'-'.$whichone;
2676
2677 if (!empty($do_bump_index)) $this->bump_index();
2678
2679 $error_occurred = false;
2680
2681 // Store this in its original form
2682 $this->source = $source;
2683
2684 // Reset. This counter is used only with PcLZip, to decide if it's better to do it all-in-one
2685 $this->makezip_recursive_batchedbytes = 0;
2686 if (!is_array($source)) $source=array($source);
2687
2688 $exclude = $updraftplus->get_exclude($whichone);
2689
2690 $files_enumerated_at = $updraftplus->jobdata_get('files_enumerated_at');
2691 if (!is_array($files_enumerated_at)) $files_enumerated_at = array();
2692 $files_enumerated_at[$whichone] = time();
2693 $updraftplus->jobdata_set('files_enumerated_at', $files_enumerated_at);
2694
2695 $this->makezip_if_altered_since = (is_array($this->altered_since)) ? (isset($this->altered_since[$whichone]) ? $this->altered_since[$whichone] : -1) : -1;
2696
2697 // Reset
2698 $got_uploads_from_cache = false;
2699
2700 // Uploads: can/should we get it back from the cache?
2701 // || 'others' == $whichone
2702 if (('uploads' == $whichone || 'others' == $whichone) && function_exists('gzopen') && function_exists('gzread')) {
2703 $use_cache_files = false;
2704 $cache_file_base = $this->zip_basename.'-cachelist-'.$this->makezip_if_altered_since;
2705 // Cache file suffixes: -zfd.gz.tmp, -zfb.gz.tmp, -info.tmp, (possible)-zfs.gz.tmp
2706 if (file_exists($cache_file_base.'-zfd.gz.tmp') && file_exists($cache_file_base.'-zfb.gz.tmp') && file_exists($cache_file_base.'-info.tmp')) {
2707 // Cache files exist; shall we use them?
2708 $mtime = filemtime($cache_file_base.'-zfd.gz.tmp');
2709 // Require < 30 minutes old
2710 if (time() - $mtime < 1800) {
2711 $use_cache_files = true;
2712 }
2713 $any_failures = false;
2714 if ($use_cache_files) {
2715 $var = $this->unserialize_gz_cache_file($cache_file_base.'-zfd.gz.tmp');
2716 if (is_array($var)) {
2717 $this->zipfiles_dirbatched = $var;
2718 $var = $this->unserialize_gz_cache_file($cache_file_base.'-zfb.gz.tmp');
2719 if (is_array($var)) {
2720 $this->zipfiles_batched = $var;
2721 if (file_exists($cache_file_base.'-info.tmp')) {
2722 $var = maybe_unserialize(file_get_contents($cache_file_base.'-info.tmp'));
2723 if (is_array($var) && isset($var['makezip_recursive_batchedbytes'])) {
2724 $this->makezip_recursive_batchedbytes = $var['makezip_recursive_batchedbytes'];
2725 if (file_exists($cache_file_base.'-zfs.gz.tmp')) {
2726 $var = $this->unserialize_gz_cache_file($cache_file_base.'-zfs.gz.tmp');
2727 if (is_array($var)) {
2728 $this->zipfiles_skipped_notaltered = $var;
2729 } else {
2730 $any_failures = true;
2731 }
2732 } else {
2733 $this->zipfiles_skipped_notaltered = array();
2734 }
2735 } else {
2736 $any_failures = true;
2737 }
2738 }
2739 } else {
2740 $any_failures = true;
2741 }
2742 } else {
2743 $any_failures = true;
2744 }
2745 if ($any_failures) {
2746 $updraftplus->log("Failed to recover file lists from existing cache files");
2747 // Reset it all
2748 $this->zipfiles_skipped_notaltered = array();
2749 $this->makezip_recursive_batchedbytes = 0;
2750 $this->zipfiles_batched = array();
2751 $this->zipfiles_dirbatched = array();
2752 } else {
2753 $updraftplus->log("File lists recovered from cache files; sizes: ".count($this->zipfiles_batched).", ".count($this->zipfiles_batched).", ".count($this->zipfiles_skipped_notaltered).")");
2754 $got_uploads_from_cache = true;
2755 }
2756 }
2757 }
2758 }
2759
2760 $time_counting_began = time();
2761
2762 $this->excluded_extensions = $this->get_excluded_extensions($exclude);
2763 $this->excluded_prefixes = $this->get_excluded_prefixes($exclude);
2764
2765 foreach ($source as $element) {
2766 // makezip_recursive_add($fullpath, $use_path_when_storing, $original_fullpath, $startlevels = 1, $exclude_array)
2767 if ('uploads' == $whichone) {
2768 if (empty($got_uploads_from_cache)) {
2769 $dirname = dirname($element);
2770 $basename = $this->basename($element);
2771 $add_them = $this->makezip_recursive_add($element, basename($dirname).'/'.$basename, $element, 2, $exclude);
2772 } else {
2773 $add_them = true;
2774 }
2775 } else {
2776 if (empty($got_uploads_from_cache)) {
2777 $add_them = $this->makezip_recursive_add($element, $this->basename($element), $element, 1, $exclude);
2778 } else {
2779 $add_them = true;
2780 }
2781 }
2782 if (is_wp_error($add_them) || false === $add_them) $error_occurred = true;
2783 }
2784
2785 $time_counting_ended = time();
2786
2787 // Cache the file scan, if it looks like it'll be useful
2788 // We use gzip to reduce the size as on hosts which limit disk I/O, the cacheing may make things worse
2789 // || 'others' == $whichone
2790 if (('uploads' == $whichone || 'others' == $whichone) && !$error_occurred && function_exists('gzopen') && function_exists('gzwrite')) {
2791 $cache_file_base = $this->zip_basename.'-cachelist-'.$this->makezip_if_altered_since;
2792
2793 // Just approximate - we're trying to avoid an otherwise-unpredictable PHP fatal error. Cacheing only happens if file enumeration took a long time - so presumably there are very many.
2794 $memory_needed_estimate = 0;
2795 foreach ($this->zipfiles_batched as $k => $v) {
2796 $memory_needed_estimate += strlen($k)+strlen($v)+12;
2797 }
2798
2799 // We haven't bothered to check if we just fetched the files from cache, as that shouldn't take a long time and so shouldn't trigger this
2800 // Let us suppose we need 15% overhead for gzipping
2801
2802 $memory_limit = ini_get('memory_limit');
2803 $memory_usage = round(@memory_get_usage(false)/1048576, 1);
2804 $memory_usage2 = round(@memory_get_usage(true)/1048576, 1);
2805
2806 if ($time_counting_ended-$time_counting_began > 20 && $updraftplus->verify_free_memory($memory_needed_estimate*0.15) && $whandle = gzopen($cache_file_base.'-zfb.gz.tmp', 'w')) {
2807 $updraftplus->log("File counting took a long time (".($time_counting_ended - $time_counting_began)."s); will attempt to cache results (memory_limit: $memory_limit (used: ${memory_usage}M | ${memory_usage2}M), estimated uncompressed bytes: ".round($memory_needed_estimate/1024, 1)." Kb)");
2808
2809 $buf = 'a:'.count($this->zipfiles_batched).':{';
2810 foreach ($this->zipfiles_batched as $file => $add_as) {
2811 $k = addslashes($file);
2812 $v = addslashes($add_as);
2813 $buf .= 's:'.strlen($k).':"'.$k.'";s:'.strlen($v).':"'.$v.'";';
2814 if (strlen($buf) > 1048576) {
2815 gzwrite($whandle, $buf, strlen($buf));
2816 $buf = '';
2817 }
2818 }
2819 $buf .= '}';
2820 $final = gzwrite($whandle, $buf);
2821 unset($buf);
2822
2823 // $serialised = serialize($this->zipfiles_batched);
2824 // $updraftplus->log("Actual uncompressed bytes: ".round(strlen($serialised)/1024, 1)." Kb");
2825 // if (!gzwrite($whandle, $serialised)) {
2826 if (!$final) {
2827 @unlink($cache_file_base.'-zfb.gz.tmp');
2828 @gzclose($whandle);
2829 } else {
2830 gzclose($whandle);
2831 if (!empty($this->zipfiles_skipped_notaltered)) {
2832 if ($shandle = gzopen($cache_file_base.'-zfs.gz.tmp', 'w')) {
2833 if (!gzwrite($shandle, serialize($this->zipfiles_skipped_notaltered))) {
2834 $aborted_on_skipped = true;
2835 }
2836 gzclose($shandle);
2837 } else {
2838 $aborted_on_skipped = true;
2839 }
2840 }
2841 if (!empty($aborted_on_skipped)) {
2842 @unlink($cache_file_base.'-zfs.gz.tmp');
2843 @unlink($cache_file_base.'-zfb.gz.tmp');
2844 } else {
2845 $info_array = array('makezip_recursive_batchedbytes' => $this->makezip_recursive_batchedbytes);
2846 if (!file_put_contents($cache_file_base.'-info.tmp', serialize($info_array))) {
2847 @unlink($cache_file_base.'-zfs.gz.tmp');
2848 @unlink($cache_file_base.'-zfb.gz.tmp');
2849 }
2850 if ($dhandle = gzopen($cache_file_base.'-zfd.gz.tmp', 'w')) {
2851 if (!gzwrite($dhandle, serialize($this->zipfiles_dirbatched))) {
2852 $aborted_on_dirbatched = true;
2853 }
2854 gzclose($dhandle);
2855 } else {
2856 $aborted_on_dirbatched = true;
2857 }
2858 if (!empty($aborted_on_dirbatched)) {
2859 @unlink($cache_file_base.'-zfs.gz.tmp');
2860 @unlink($cache_file_base.'-zfd.gz.tmp');
2861 @unlink($cache_file_base.'-zfb.gz.tmp');
2862 @unlink($cache_file_base.'-info.tmp');
2863 // @codingStandardsIgnoreLine
2864 } else {
2865 // Success.
2866 }
2867 }
2868 }
2869 }
2870
2871 /*
2872 Class variables that get altered:
2873 zipfiles_batched
2874 makezip_recursive_batchedbytes
2875 zipfiles_skipped_notaltered
2876 zipfiles_dirbatched
2877 Class variables that the result depends upon (other than the state of the filesystem):
2878 makezip_if_altered_since
2879 existing_files
2880 */
2881
2882 }
2883
2884 // Any not yet dispatched? Under our present scheme, at this point nothing has yet been despatched. And since the enumerating of all files can take a while, we can at this point do a further modification check to reduce the chance of overlaps.
2885 // This relies on us *not* touch()ing the zip file to indicate to any resumption 'behind us' that we're already here. Rather, we're relying on the combined facts that a) if it takes us a while to search the directory tree, then it should do for the one behind us too (though they'll have the benefit of cache, so could catch very fast) and b) we touch *immediately* after finishing the enumeration of the files to add.
2886 // $retry_on_error is here being used as a proxy for 'not the second time around, when there might be the remains of the file on the first time around'
2887 if ($retry_on_error) $updraftplus->check_recent_modification($destination);
2888 // Here we're relying on the fact that both PclZip and ZipArchive will happily operate on an empty file. Note that BinZip *won't* (for that, may need a new strategy - e.g. add the very first file on its own, in order to 'lay down a marker')
2889 if (empty($do_bump_index)) @touch($destination);
2890
2891 if (count($this->zipfiles_dirbatched) > 0 || count($this->zipfiles_batched) > 0) {
2892
2893 $updraftplus->log(sprintf("Total entities for the zip file: %d directories, %d files (%d skipped as non-modified), %s MB", count($this->zipfiles_dirbatched), count($this->zipfiles_batched), count($this->zipfiles_skipped_notaltered), round($this->makezip_recursive_batchedbytes/1048576, 1)));
2894
2895 // No need to warn if we're going to retry anyway. (And if we get killed, the zip will be rescanned for its contents upon resumption).
2896 $warn_on_failures = ($retry_on_error) ? false : true;
2897 $add_them = $this->makezip_addfiles($warn_on_failures);
2898
2899 if (is_wp_error($add_them)) {
2900 foreach ($add_them->get_error_messages() as $msg) {
2901 $updraftplus->log("Error returned from makezip_addfiles: ".$msg);
2902 }
2903 $error_occurred = true;
2904 } elseif (false === $add_them) {
2905 $updraftplus->log("Error: makezip_addfiles returned false");
2906 $error_occurred = true;
2907 }
2908
2909 }
2910
2911 // Reset these variables because the index may have changed since we began
2912
2913 $itext = (empty($this->index)) ? '' : ($this->index+1);
2914 $destination_base = $backup_file_basename.'-'.$whichone.$itext.'.zip.tmp';
2915 $destination = $this->updraft_dir.'/'.$destination_base;
2916
2917 // ZipArchive::addFile sometimes fails - there's nothing when we expected something.
2918 // Did not used to have || $error_occured here. But it is better to retry, than to simply warn the user to check his logs.
2919 if (((file_exists($destination) || $this->index == $original_index) && @filesize($destination) < 90 && 'UpdraftPlus_ZipArchive' == $this->use_zip_object) || ($error_occurred && $retry_on_error)) {
2920 // This can be made more sophisticated if feedback justifies it. Currently we just switch to PclZip. But, it may have been a BinZip failure, so we could then try ZipArchive if that is available. If doing that, make sure that an infinite recursion isn't made possible.
2921 $updraftplus->log("makezip_addfiles(".$this->use_zip_object.") apparently failed (file=".basename($destination).", type=$whichone, size=".filesize($destination).") - retrying with PclZip");
2922 $saved_zip_object = $this->use_zip_object;
2923 $this->use_zip_object = 'UpdraftPlus_PclZip';
2924 $ret = $this->make_zipfile($source, $backup_file_basename, $whichone, false);
2925 $this->use_zip_object = $saved_zip_object;
2926 return $ret;
2927 }
2928
2929 // zipfiles_added > 0 means that $zip->close() has been called. i.e. An attempt was made to add something: something _should_ be there.
2930 // Why return true even if $error_occurred may be set? 1) Because in that case, a warning has already been logged. 2) Because returning false causes an error to be logged, which means it'll all be retried again. Also 3) this has been the pattern of the code for a long time, and the algorithm has been proven in the real-world: don't change what's not broken.
2931 // (file_exists($destination) || $this->index == $original_index) might be an alternative to $this->zipfiles_added > 0 - ? But, don't change what's not broken.
2932 if (false == $error_occurred || $this->zipfiles_added > 0) {
2933 return true;
2934 } else {
2935 $updraftplus->log("makezip failure: zipfiles_added=".$this->zipfiles_added.", error_occurred=".$error_occurred." (method=".$this->use_zip_object.")");
2936 return false;
2937 }
2938
2939 }
2940
2941 private function basename($element) {
2942 // This function is an ugly, conservative workaround for https://bugs.php.net/bug.php?id=62119. It does not aim to always work-around, but to ensure that nothing is made worse.
2943 $dirname = dirname($element);
2944 $basename_manual = preg_replace('#^[\\/]+#', '', substr($element, strlen($dirname)));
2945 $basename = basename($element);
2946 if ($basename_manual != $basename) {
2947 $locale = setlocale(LC_CTYPE, "0");
2948 if ('C' == $locale) {
2949 setlocale(LC_CTYPE, 'en_US.UTF8');
2950 $basename_new = basename($element);
2951 if ($basename_new == $basename_manual) $basename = $basename_new;
2952 setlocale(LC_CTYPE, $locale);
2953 }
2954 }
2955 return $basename;
2956 }
2957
2958 private function file_should_be_stored_without_compression($file) {
2959 if (!is_array($this->extensions_to_not_compress)) return false;
2960 foreach ($this->extensions_to_not_compress as $ext) {
2961 $ext_len = strlen($ext);
2962 if (strtolower(substr($file, -$ext_len, $ext_len)) == $ext) return true;
2963 }
2964 return false;
2965 }
2966
2967 /**
2968 * This method will add a manifest file to the backup zip
2969 *
2970 * @param String $whichone - the type of backup (e.g. 'plugins', 'themes')
2971 *
2972 * @return Boolean - success/failure status
2973 */
2974 private function updraftplus_include_manifest($whichone) {
2975 global $updraftplus;
2976
2977 $manifest_name = "updraftplus-manifest.json";
2978 $manifest = trailingslashit($this->updraft_dir).$manifest_name;
2979
2980 $updraftplus->log(sprintf("Creating file manifest ($manifest_name) for incremental backup (included: %d, skipped: %d)", count($this->zipfiles_batched), count($this->zipfiles_skipped_notaltered)));
2981
2982 if (false === ($handle = fopen($manifest, 'w+'))) return $updraftplus->log("Failed to open manifest file ($manifest_name)");
2983
2984 $this->manifest_path = $manifest;
2985
2986 $version = 1;
2987
2988 $go_to_levels = array(
2989 'plugins' => 2,
2990 'themes' => 2,
2991 'uploads' => 3,
2992 'others' => 3
2993 );
2994
2995 $go_to_levels = apply_filters('updraftplus_manifest_go_to_level', $go_to_levels, $whichone);
2996
2997 $go_to_level = isset($go_to_levels[$whichone]) ? $go_to_levels[$whichone] : 'all';
2998
2999 $directory = '';
3000
3001 if ('more' == $whichone) {
3002 foreach ($this->zipfiles_batched as $index => $dir) {
3003 $directory = '"directory":"' . dirname($index) . '",';
3004 }
3005 }
3006
3007 if (false === fwrite($handle, '{"version":'.$version.',"type":"'.$whichone.'",'.$directory.'"listed_levels":"'.$go_to_level.'","contents":{"directories":[')) $updraftplus->log("First write to manifest file failed ($manifest_name)");
3008
3009 // First loop: find out which is the last entry, so that we don't write the comma after it
3010 $last_dir_index = false;
3011 foreach ($this->zipfiles_dirbatched as $index => $dir) {
3012 if ('all' !== $go_to_level && substr_count($dir, '/') > $go_to_level - 1) continue;
3013 $last_dir_index = $index;
3014 }
3015
3016 // Second loop: write out the entry
3017 foreach ($this->zipfiles_dirbatched as $index => $dir) {
3018 if ('all' !== $go_to_level && substr_count($dir, '/') > $go_to_level - 1) continue;
3019 fwrite($handle, json_encode($dir).(($index != $last_dir_index) ? ',' : ''));
3020 }
3021
3022 // Now do the same for files
3023 fwrite($handle, '],"files":[');
3024
3025 $last_file_index = false;
3026 foreach ($this->zipfiles_batched as $source => $store_as) {
3027 if ('all' !== $go_to_level && substr_count($store_as, '/') > $go_to_level - 1) continue;
3028 $last_file_index = $store_as;
3029 }
3030 foreach ($this->zipfiles_skipped_notaltered as $source => $store_as) {
3031 if ('all' !== $go_to_level && substr_count($store_as, '/') > $go_to_level - 1) continue;
3032 $last_file_index = $store_as;
3033 }
3034
3035 foreach ($this->zipfiles_batched as $source => $store_as) {
3036 if ('all' !== $go_to_level && substr_count($store_as, '/') > $go_to_level - 1) continue;
3037 fwrite($handle, json_encode($store_as).(($store_as != $last_file_index) ? ',' : ''));
3038 }
3039
3040 foreach ($this->zipfiles_skipped_notaltered as $source => $store_as) {
3041 if ('all' !== $go_to_level && substr_count($store_as, '/') > $go_to_level - 1) continue;
3042 fwrite($handle, json_encode($store_as).(($store_as != $last_file_index) ? ',' : ''));
3043 }
3044
3045 fwrite($handle, ']}}');
3046 fclose($handle);
3047
3048 $this->zipfiles_batched[$manifest] = $manifest_name;
3049
3050 $updraftplus->log("Successfully created file manifest (size: ".filesize($manifest).")");
3051
3052 return true;
3053 }
3054
3055 // Q. Why don't we only open and close the zip file just once?
3056 // A. Because apparently PHP doesn't write out until the final close, and it will return an error if anything file has vanished in the meantime. So going directory-by-directory reduces our chances of hitting an error if the filesystem is changing underneath us (which is very possible if dealing with e.g. 1GB of files)
3057
3058 /**
3059 * We batch up the files, rather than do them one at a time. So we are more efficient than open,one-write,close.
3060 * To call into here, the array $this->zipfiles_batched must be populated (keys=paths, values=add-to-zip-as values). It gets reset upon exit from here.
3061 *
3062 * @param Boolean $warn_on_failures See if it warns on faliures or not
3063 * @return Boolean|WP_Error
3064 */
3065 private function makezip_addfiles($warn_on_failures) {
3066
3067 global $updraftplus;
3068
3069 // Used to detect requests to bump the size
3070 $bump_index = false;
3071 $ret = true;
3072
3073 $zipfile = $this->zip_basename.((0 == $this->index) ? '' : ($this->index+1)).'.zip.tmp';
3074
3075 $maxzipbatch = $updraftplus->jobdata_get('maxzipbatch', 26214400);
3076 if ((int) $maxzipbatch < 1024) $maxzipbatch = 26214400;
3077
3078 // Short-circuit the null case, because we want to detect later if something useful happenned
3079 if (count($this->zipfiles_dirbatched) == 0 && count($this->zipfiles_batched) == 0) return true;
3080
3081 // If on PclZip, then if possible short-circuit to a quicker method (makes a huge time difference - on a folder of 1500 small files, 2.6s instead of 76.6)
3082 // This assumes that makezip_addfiles() is only called once so that we know about all needed files (the new style)
3083 // This is rather conservative - because it assumes zero compression. But we can't know that in advance.
3084 $force_allinone = false;
3085 if (0 == $this->index && $this->makezip_recursive_batchedbytes < $this->zip_split_every) {
3086 // So far, we only have a processor for this for PclZip; but that check can be removed - need to address the below items
3087 // TODO: Is this really what we want? Always go all-in-one for < 500MB???? Should be more conservative? Or, is it always faster to go all-in-one? What about situations where we might want to auto-split because of slowness - check that that is still working.
3088 // TODO: Test this new method for PclZip - are we still getting the performance gains? Test for ZipArchive too.
3089 if ('UpdraftPlus_PclZip' == $this->use_zip_object && ($this->makezip_recursive_batchedbytes < 512*1048576 || (defined('UPDRAFTPLUS_PCLZIP_FORCEALLINONE') && UPDRAFTPLUS_PCLZIP_FORCEALLINONE == true && 'UpdraftPlus_PclZip' == $this->use_zip_object))) {
3090 $updraftplus->log("Only one archive required (".$this->use_zip_object.") - will attempt to do in single operation (data: ".round($this->makezip_recursive_batchedbytes/1024, 1)." KB, split: ".round($this->zip_split_every/1024, 1)." KB)");
3091 // $updraftplus->log("PclZip, and only one archive required - will attempt to do in single operation (data: ".round($this->makezip_recursive_batchedbytes/1024, 1)." KB, split: ".round($this->zip_split_every/1024, 1)." KB)");
3092 $force_allinone = true;
3093 // if(!class_exists('PclZip')) require_once(ABSPATH.'/wp-admin/includes/class-pclzip.php');
3094 // $zip = new PclZip($zipfile);
3095 // $remove_path = ($this->whichone == 'wpcore') ? untrailingslashit(ABSPATH) : WP_CONTENT_DIR;
3096 // $add_path = false;
3097 // Remove prefixes
3098 // $backupable_entities = $updraftplus->get_backupable_file_entities(true);
3099 // if (isset($backupable_entities[$this->whichone])) {
3100 // if ('plugins' == $this->whichone || 'themes' == $this->whichone || 'uploads' == $this->whichone) {
3101 // $remove_path = dirname($backupable_entities[$this->whichone]);
3102 // To normalise instead of removing (which binzip doesn't support, so we don't do it), you'd remove the dirname() in the above line, and uncomment the below one.
3103 // #$add_path = $this->whichone;
3104 // } else {
3105 // $remove_path = $backupable_entities[$this->whichone];
3106 // }
3107 // }
3108 // if ($add_path) {
3109 // $zipcode = $zip->create($this->source, PCLZIP_OPT_REMOVE_PATH, $remove_path, PCLZIP_OPT_ADD_PATH, $add_path);
3110 // } else {
3111 // $zipcode = $zip->create($this->source, PCLZIP_OPT_REMOVE_PATH, $remove_path);
3112 // }
3113 // if ($zipcode == 0) {
3114 // $updraftplus->log("PclZip Error: ".$zip->errorInfo(true), 'warning');
3115 // return $zip->errorCode();
3116 // } else {
3117 // UpdraftPlus_Job_Scheduler::something_useful_happened();
3118 // return true;
3119 // }
3120 }
3121 }
3122
3123 // 05-Mar-2013 - added a new check on the total data added; it appears that things fall over if too much data is contained in the cumulative total of files that were addFile'd without a close-open cycle; presumably data is being stored in memory. In the case in question, it was a batch of MP3 files of around 100MB each - 25 of those equals 2.5GB!
3124
3125 $data_added_since_reopen = 0;
3126 // The following array is used only for error reporting if ZipArchive::close fails (since that method itself reports no error messages - we have to track manually what we were attempting to add)
3127 $files_zipadded_since_open = array();
3128
3129 $zip = new $this->use_zip_object;
3130 if (file_exists($zipfile)) {
3131 $opencode = $zip->open($zipfile);
3132 $original_size = filesize($zipfile);
3133 clearstatcache();
3134 } else {
3135 $create_code = (version_compare(PHP_VERSION, '5.2.12', '>') && defined('ZIPARCHIVE::CREATE')) ? ZIPARCHIVE::CREATE : 1;
3136 $opencode = $zip->open($zipfile, $create_code);
3137 $original_size = 0;
3138 }
3139
3140 if (true !== $opencode) return new WP_Error('no_open', sprintf(__('Failed to open the zip file (%s) - %s', 'updraftplus'), $zipfile, $zip->last_error));
3141
3142 if (apply_filters('updraftplus_include_manifest', false, $this->whichone, $this)) {
3143 $this->updraftplus_include_manifest($this->whichone);
3144 }
3145
3146 // Make sure all directories are created before we start creating files
3147 while ($dir = array_pop($this->zipfiles_dirbatched)) {
3148 $zip->addEmptyDir($dir);
3149 }
3150 $zipfiles_added_thisbatch = 0;
3151
3152 // Go through all those batched files
3153 foreach ($this->zipfiles_batched as $file => $add_as) {
3154
3155 if (!file_exists($file)) {
3156 $updraftplus->log("File has vanished from underneath us; dropping: ".$add_as);
3157 continue;
3158 }
3159
3160 $fsize = filesize($file);
3161
3162 if (@constant('UPDRAFTPLUS_SKIP_FILE_OVER_SIZE') && $fsize > UPDRAFTPLUS_SKIP_FILE_OVER_SIZE) {
3163 $updraftplus->log("File is larger than the user-configured (UPDRAFTPLUS_SKIP_FILE_OVER_SIZE) maximum (is: ".round($fsize/1024, 1)." KB); will skip: ".$add_as);
3164 continue;
3165 } elseif ($fsize > UPDRAFTPLUS_WARN_FILE_SIZE) {
3166 $updraftplus->log(sprintf(__('A very large file was encountered: %s (size: %s Mb)', 'updraftplus'), $add_as, round($fsize/1048576, 1)), 'warning', 'vlargefile_'.md5($this->whichone.'#'.$add_as));
3167 }
3168
3169 // Skips files that are already added
3170 if (!isset($this->existing_files[$add_as]) || $this->existing_files[$add_as] != $fsize) {
3171
3172 @touch($zipfile);
3173 $zip->addFile($file, $add_as);
3174 $zipfiles_added_thisbatch++;
3175
3176 if (method_exists($zip, 'setCompressionName') && $this->file_should_be_stored_without_compression($add_as)) {
3177 if (false == ($set_compress = $zip->setCompressionName($add_as, ZipArchive::CM_STORE))) {
3178 $updraftplus->log("Zip: setCompressionName failed on: $add_as");
3179 }
3180 }
3181
3182 // N.B., Since makezip_addfiles() can get called more than once if there were errors detected, potentially $zipfiles_added_thisrun can exceed the total number of batched files (if they get processed twice).
3183 $this->zipfiles_added_thisrun++;
3184 $files_zipadded_since_open[] = array('file' => $file, 'addas' => $add_as);
3185
3186 $data_added_since_reopen += $fsize;
3187 /* Conditions for forcing a write-out and re-open:
3188 - more than $maxzipbatch bytes have been batched
3189 - more than 2.0 seconds have passed since the last time we wrote
3190 - that adding this batch of data is likely already enough to take us over the split limit (and if that happens, then do actually split - to prevent a scenario of progressively tinier writes as we approach but don't actually reach the limit)
3191 - more than 500 files batched (should perhaps intelligently lower this as the zip file gets bigger - not yet needed)
3192 */
3193
3194 // Add 10% margin. It only really matters when the OS has a file size limit, exceeding which causes failure (e.g. 2GB on 32-bit)
3195 // Since we don't test before the file has been created (so that zip_last_ratio has meaningful data), we rely on max_zip_batch being less than zip_split_every - which should always be the case
3196 $reaching_split_limit = ($this->zip_last_ratio > 0 && $original_size>0 && ($original_size + 1.1*$data_added_since_reopen*$this->zip_last_ratio) > $this->zip_split_every) ? true : false;
3197
3198 if (!$force_allinone && ($zipfiles_added_thisbatch > UPDRAFTPLUS_MAXBATCHFILES || $reaching_split_limit || $data_added_since_reopen > $maxzipbatch || (time() - $this->zipfiles_lastwritetime) > 2)) {
3199
3200 // We are coming towards a limit and about to close the zip, check if this is a more file backup and the manifest file has made it into this zip if not add it
3201 if (apply_filters('updraftplus_include_manifest', false, $this->whichone, $this)) {
3202
3203 $manifest = false;
3204
3205 foreach ($files_zipadded_since_open as $info) {
3206 if ('updraftplus-manifest.json' == $info['file']) $manifest = true;
3207 }
3208
3209 if (!$manifest) {
3210 @touch($zipfile);
3211 $path = array_search('updraftplus-manifest.json', $this->zipfiles_batched);
3212 $zip->addFile($path, 'updraftplus-manifest.json');
3213 $zipfiles_added_thisbatch++;
3214
3215 if (method_exists($zip, 'setCompressionName') && $this->file_should_be_stored_without_compression($this->zipfiles_batched[$path])) {
3216 if (false == ($set_compress = $zip->setCompressionName($this->zipfiles_batched[$path], ZipArchive::CM_STORE))) {
3217 $updraftplus->log("Zip: setCompressionName failed on: $this->zipfiles_batched[$path]");
3218 }
3219 }
3220
3221 // N.B., Since makezip_addfiles() can get called more than once if there were errors detected, potentially $zipfiles_added_thisrun can exceed the total number of batched files (if they get processed twice).
3222 $this->zipfiles_added_thisrun++;
3223 $files_zipadded_since_open[] = array('file' => $path, 'addas' => 'updraftplus-manifest.json');
3224 $data_added_since_reopen += filesize($path);
3225 }
3226 }
3227
3228 @set_time_limit(UPDRAFTPLUS_SET_TIME_LIMIT);
3229 $something_useful_sizetest = false;
3230
3231 if ($data_added_since_reopen > $maxzipbatch) {
3232 $something_useful_sizetest = true;
3233 $updraftplus->log("Adding batch to zip file (".$this->use_zip_object."): over ".round($maxzipbatch/1048576, 1)." MB added on this batch (".round($data_added_since_reopen/1048576, 1)." MB, ".count($this->zipfiles_batched)." files batched, $zipfiles_added_thisbatch (".$this->zipfiles_added_thisrun.") added so far); re-opening (prior size: ".round($original_size/1024, 1).' KB)');
3234 } elseif ($zipfiles_added_thisbatch > UPDRAFTPLUS_MAXBATCHFILES) {
3235 $updraftplus->log("Adding batch to zip file (".$this->use_zip_object."): over ".UPDRAFTPLUS_MAXBATCHFILES." files added on this batch (".round($data_added_since_reopen/1048576, 1)." MB, ".count($this->zipfiles_batched)." files batched, $zipfiles_added_thisbatch (".$this->zipfiles_added_thisrun.") added so far); re-opening (prior size: ".round($original_size/1024, 1).' KB)');
3236 } elseif (!$reaching_split_limit) {
3237 $updraftplus->log("Adding batch to zip file (".$this->use_zip_object."): over 2.0 seconds have passed since the last write (".round($data_added_since_reopen/1048576, 1)." MB, $zipfiles_added_thisbatch (".$this->zipfiles_added_thisrun.") files added so far); re-opening (prior size: ".round($original_size/1024, 1).' KB)');
3238 } else {
3239 $updraftplus->log("Adding batch to zip file (".$this->use_zip_object."): possibly approaching split limit (".round($data_added_since_reopen/1048576, 1)." MB, $zipfiles_added_thisbatch (".$this->zipfiles_added_thisrun.") files added so far); last ratio: ".round($this->zip_last_ratio, 4)."; re-opening (prior size: ".round($original_size/1024, 1).' KB)');
3240 }
3241
3242 if (!$zip->close()) {
3243 // Though we will continue processing the files we've got, the final error code will be false, to allow a second attempt on the failed ones. This also keeps us consistent with a negative result for $zip->close() further down. We don't just retry here, because we have seen cases (with BinZip) where upon failure, the existing zip had actually been deleted. So, to be safe we need to re-scan the existing zips.
3244 $ret = false;
3245 $this->record_zip_error($files_zipadded_since_open, $zip->last_error, $warn_on_failures);
3246 }
3247
3248 $zipfiles_added_thisbatch = 0;
3249
3250 // This triggers a re-open, later
3251 unset($zip);
3252 $files_zipadded_since_open = array();
3253 // Call here, in case we've got so many big files that we don't complete the whole routine
3254 if (filesize($zipfile) > $original_size) {
3255
3256 // It is essential that this does not go above 1, even though in reality (and this can happen at the start, if just 1 file is added (e.g. due to >2.0s detection) the 'compressed' zip file may be *bigger* than the files stored in it. When that happens, if the ratio is big enough, it can then fire the "approaching split limit" detection (very) prematurely
3257 $this->zip_last_ratio = ($data_added_since_reopen > 0) ? min((filesize($zipfile) - $original_size)/$data_added_since_reopen, 1) : 1;
3258
3259 // We need a rolling update of this
3260 $original_size = filesize($zipfile);
3261
3262 // Move on to next zip?
3263 if ($reaching_split_limit || filesize($zipfile) > $this->zip_split_every) {
3264 $bump_index = true;
3265 // Take the filesize now because later we wanted to know we did clearstatcache()
3266 $bumped_at = round(filesize($zipfile)/1048576, 1);
3267 }
3268
3269 // Need to make sure that something_useful_happened() is always called
3270
3271 // How long since the current run began? If it's taken long (and we're in danger of not making it at all), or if that is forseeable in future because of general slowness, then we should reduce the parameters.
3272 if (!$something_useful_sizetest) {
3273 UpdraftPlus_Job_Scheduler::something_useful_happened();
3274 } else {
3275
3276 // Do this as early as possible
3277 UpdraftPlus_Job_Scheduler::something_useful_happened();
3278
3279 $time_since_began = max(microtime(true)- $this->zipfiles_lastwritetime, 0.000001);
3280 $normalised_time_since_began = $time_since_began*($maxzipbatch/$data_added_since_reopen);
3281
3282 // Don't measure speed until after ZipArchive::close()
3283 $rate = round($data_added_since_reopen/$time_since_began, 1);
3284
3285 $updraftplus->log(sprintf("A useful amount of data was added after this amount of zip processing: %s s (normalised: %s s, rate: %s KB/s)", round($time_since_began, 1), round($normalised_time_since_began, 1), round($rate/1024, 1)));
3286
3287 // We want to detect not only that we need to reduce the size of batches, but also the capability to increase them. This is particularly important because of ZipArchive()'s (understandable, given the tendency of PHP processes being terminated without notice) practice of first creating a temporary zip file via copying before acting on that zip file (so the information is atomic). Unfortunately, once the size of the zip file gets over 100MB, the copy operation beguns to be significant. By the time you've hit 500MB on many web hosts the copy is the majority of the time taken. So we want to do more in between these copies if possible.
3288
3289 /* "Could have done more" - detect as:
3290 - A batch operation would still leave a "good chunk" of time in a run
3291 - "Good chunk" means that the time we took to add the batch is less than 50% of a run time
3292 - We can do that on any run after the first (when at least one ceiling on the maximum time is known)
3293 - But in the case where a max_execution_time is long (so that resumptions are never needed), and we're always on run 0, we will automatically increase chunk size if the batch took less than 6 seconds.
3294 */
3295
3296 // At one stage we had a strategy of not allowing check-ins to have more than 20s between them. However, once the zip file got to a certain size, PHP's habit of copying the entire zip file first meant that it *always* went over 18s, and thence a drop in the max size was inevitable - which was bad, because with the copy time being something that only grew, the outcome was less data being copied every time
3297
3298 // Gather the data. We try not to do this unless necessary (may be time-sensitive)
3299 if ($updraftplus->current_resumption >= 1) {
3300 $time_passed = $updraftplus->jobdata_get('run_times');
3301 if (!is_array($time_passed)) $time_passed = array();
3302 list($max_time, $timings_string, $run_times_known) = UpdraftPlus_Manipulation_Functions::max_time_passed($time_passed, $updraftplus->current_resumption-1, $this->first_run);
3303 } else {
3304 $run_times_known = 0;
3305 $max_time = -1;
3306 }
3307
3308 if ($normalised_time_since_began < 6 || ($updraftplus->current_resumption >= 1 && $run_times_known >= 1 && $time_since_began < 0.6*$max_time)) {
3309
3310 // How much can we increase it by?
3311 if ($normalised_time_since_began < 6) {
3312 if ($run_times_known > 0 && $max_time >0) {
3313 $new_maxzipbatch = min(floor(max($maxzipbatch*6/$normalised_time_since_began, $maxzipbatch*((0.6*$max_time)/$normalised_time_since_began))), $this->zip_batch_ceiling);
3314 } else {
3315 // Maximum of 200MB in a batch
3316 $new_maxzipbatch = min(floor($maxzipbatch*6/$normalised_time_since_began), $this->zip_batch_ceiling);
3317 }
3318 } else {
3319 // Use up to 60% of available time
3320 $new_maxzipbatch = min(floor($maxzipbatch*((0.6*$max_time)/$normalised_time_since_began)), $this->zip_batch_ceiling);
3321 }
3322
3323 // Throttle increases - don't increase by more than 2x in one go - ???
3324 // $new_maxzipbatch = floor(min(2*$maxzipbatch, $new_maxzipbatch));
3325 // Also don't allow anything that is going to be more than 18 seconds - actually, that's harmful because of the basically fixed time taken to copy the file
3326 // $new_maxzipbatch = floor(min(18*$rate ,$new_maxzipbatch));
3327
3328 // Don't go above the split amount (though we expect that to be higher anyway, unless sending via email)
3329 $new_maxzipbatch = min($new_maxzipbatch, $this->zip_split_every);
3330
3331 // Don't raise it above a level that failed on a previous run
3332 $maxzipbatch_ceiling = $updraftplus->jobdata_get('maxzipbatch_ceiling');
3333 if (is_numeric($maxzipbatch_ceiling) && $maxzipbatch_ceiling > 20*1024*1024 && $new_maxzipbatch > $maxzipbatch_ceiling) {
3334 $updraftplus->log("Was going to raise maxzipbytes to $new_maxzipbatch, but this is too high: a previous failure led to the ceiling being set at $maxzipbatch_ceiling, which we will use instead");
3335 $new_maxzipbatch = $maxzipbatch_ceiling;
3336 }
3337
3338 // Final sanity check
3339 if ($new_maxzipbatch > 1024*1024) $updraftplus->jobdata_set("maxzipbatch", $new_maxzipbatch);
3340
3341 if ($new_maxzipbatch <= 1024*1024) {
3342 $updraftplus->log("Unexpected new_maxzipbatch value obtained (time=$time_since_began, normalised_time=$normalised_time_since_began, max_time=$max_time, data points known=$run_times_known, old_max_bytes=$maxzipbatch, new_max_bytes=$new_maxzipbatch)");
3343 } elseif ($new_maxzipbatch > $maxzipbatch) {
3344 $updraftplus->log("Performance is good - will increase the amount of data we attempt to batch (time=$time_since_began, normalised_time=$normalised_time_since_began, max_time=$max_time, data points known=$run_times_known, old_max_bytes=$maxzipbatch, new_max_bytes=$new_maxzipbatch)");
3345 } elseif ($new_maxzipbatch < $maxzipbatch) {
3346 // Ironically, we thought we were speedy...
3347 $updraftplus->log("Adjust: Reducing maximum amount of batched data (time=$time_since_began, normalised_time=$normalised_time_since_began, max_time=$max_time, data points known=$run_times_known, new_max_bytes=$new_maxzipbatch, old_max_bytes=$maxzipbatch)");
3348 } else {
3349 $updraftplus->log("Performance is good - but we will not increase the amount of data we batch, as we are already at the present limit (time=$time_since_began, normalised_time=$normalised_time_since_began, max_time=$max_time, data points known=$run_times_known, max_bytes=$maxzipbatch)");
3350 }
3351
3352 if ($new_maxzipbatch > 1024*1024) $maxzipbatch = $new_maxzipbatch;
3353 }
3354
3355 // Detect excessive slowness
3356 // Don't do this until we're on at least resumption 7, as we want to allow some time for things to settle down and the maxiumum time to be accurately known (since reducing the batch size unnecessarily can itself cause extra slowness, due to PHP's usage of temporary zip files)
3357
3358 // We use a percentage-based system as much as possible, to avoid the various criteria being in conflict with each other (i.e. a run being both 'slow' and 'fast' at the same time, which is increasingly likely as max_time gets smaller).
3359
3360 if (!$updraftplus->something_useful_happened && $updraftplus->current_resumption >= 7) {
3361
3362 UpdraftPlus_Job_Scheduler::something_useful_happened();
3363
3364 if ($run_times_known >= 5 && ($time_since_began > 0.8 * $max_time || $time_since_began + 7 > $max_time)) {
3365
3366 $new_maxzipbatch = max(floor($maxzipbatch*0.8), 20971520);
3367 if ($new_maxzipbatch < $maxzipbatch) {
3368 $maxzipbatch = $new_maxzipbatch;
3369 $updraftplus->jobdata_set("maxzipbatch", $new_maxzipbatch);
3370 $updraftplus->log("We are within a small amount of the expected maximum amount of time available; the zip-writing thresholds will be reduced (time_passed=$time_since_began, normalised_time_passed=$normalised_time_since_began, max_time=$max_time, data points known=$run_times_known, old_max_bytes=$maxzipbatch, new_max_bytes=$new_maxzipbatch)");
3371 } else {
3372 $updraftplus->log("We are within a small amount of the expected maximum amount of time available, but the zip-writing threshold is already at its lower limit (20MB), so will not be further reduced (max_time=$max_time, data points known=$run_times_known, max_bytes=$maxzipbatch)");
3373 }
3374 }
3375
3376 } else {
3377 UpdraftPlus_Job_Scheduler::something_useful_happened();
3378 }
3379 }
3380 $data_added_since_reopen = 0;
3381 } else {
3382 // ZipArchive::close() can take a very long time, which we want to know about
3383 UpdraftPlus_Job_Scheduler::record_still_alive();
3384 }
3385
3386 clearstatcache();
3387 $this->zipfiles_lastwritetime = time();
3388 }
3389 } elseif (0 == $this->zipfiles_added_thisrun) {
3390 // Update lastwritetime, because otherwise the 2.0-second-activity detection can fire prematurely (e.g. if it takes >2.0 seconds to process the previously-written files, then the detector fires after 1 file. This then can have the knock-on effect of having something_useful_happened() called, but then a subsequent attempt to write out a lot of meaningful data fails, and the maximum batch is not then reduced.
3391 // Testing shows that calling time() 1000 times takes negligible time
3392 $this->zipfiles_lastwritetime = time();
3393 }
3394
3395 $this->zipfiles_added++;
3396
3397 // Don't call something_useful_happened() here - nothing necessarily happens until close() is called
3398 if (0 == $this->zipfiles_added % 100) {
3399 $skip_dblog = ($this->zipfiles_added_thisrun > 0 || 0 == $this->zipfiles_added % 1000) ? false : true;
3400 $updraftplus->log("Zip: ".basename($zipfile).": ".$this->zipfiles_added." files added (on-disk size: ".round(@filesize($zipfile)/1024, 1)." KB)", 'notice', false, $skip_dblog);
3401 }
3402
3403 if ($bump_index) {
3404 $updraftplus->log(sprintf("Zip size is at/near split limit (%s MB / %s MB) - bumping index (from: %d)", $bumped_at, round($this->zip_split_every/1048576, 1), $this->index));
3405 $bump_index = false;
3406 $this->bump_index();
3407 $zipfile = $this->zip_basename.($this->index+1).'.zip.tmp';
3408 }
3409
3410 if (empty($zip)) {
3411 $zip = new $this->use_zip_object;
3412
3413 if (file_exists($zipfile)) {
3414 $opencode = $zip->open($zipfile);
3415 $original_size = filesize($zipfile);
3416 clearstatcache();
3417 } else {
3418 $create_code = defined('ZIPARCHIVE::CREATE') ? ZIPARCHIVE::CREATE : 1;
3419 $opencode = $zip->open($zipfile, $create_code);
3420 $original_size = 0;
3421 }
3422
3423 if (true !== $opencode) return new WP_Error('no_open', sprintf(__('Failed to open the zip file (%s) - %s', 'updraftplus'), $zipfile, $zip->last_error));
3424 }
3425
3426 }
3427
3428 // Reset array
3429 $this->zipfiles_batched = array();
3430 $this->zipfiles_skipped_notaltered = array();
3431
3432 if (false == ($nret = $zip->close())) $this->record_zip_error($files_zipadded_since_open, $zip->last_error, $warn_on_failures);
3433
3434 if (apply_filters('updraftplus_include_manifest', false, $this->whichone, $this)) {
3435 if (!empty($this->manifest_path) && file_exists($this->manifest_path)) {
3436 $updraftplus->log('Removing manifest file: '.basename($this->manifest_path).': '.(@unlink($this->manifest_path) ? 'OK' : 'failed'));
3437 }
3438 }
3439
3440 $this->zipfiles_lastwritetime = time();
3441 // May not exist if the last thing we did was bump
3442 if (file_exists($zipfile) && filesize($zipfile) > $original_size) UpdraftPlus_Job_Scheduler::something_useful_happened();
3443
3444 // Move on to next archive?
3445 if (file_exists($zipfile) && filesize($zipfile) > $this->zip_split_every) {
3446 $updraftplus->log(sprintf("Zip size has gone over split limit (%s, %s) - bumping index (%d)", round(filesize($zipfile)/1048576, 1), round($this->zip_split_every/1048576, 1), $this->index));
3447 $this->bump_index();
3448 }
3449
3450 clearstatcache();
3451
3452 return (false == $ret) ? false : $nret;
3453 }
3454
3455 private function record_zip_error($files_zipadded_since_open, $msg, $warn = true) {
3456 global $updraftplus;
3457
3458 if (!empty($updraftplus->cpanel_quota_readable)) {
3459 $hosting_bytes_free = $updraftplus->get_hosting_disk_quota_free();
3460 if (is_array($hosting_bytes_free)) {
3461 $perc = round(100*$hosting_bytes_free[1]/(max($hosting_bytes_free[2], 1)), 1);
3462 $quota_free_msg = sprintf('Free disk space in account: %s (%s used)', round($hosting_bytes_free[3]/1048576, 1)." MB", "$perc %");
3463 $updraftplus->log($quota_free_msg);
3464 if ($hosting_bytes_free[3] < 1048576*50) {
3465 $quota_low = true;
3466 $quota_free_mb = round($hosting_bytes_free[3]/1048576, 1);
3467 $updraftplus->log(sprintf(__('Your free space in your hosting account is very low - only %s Mb remain', 'updraftplus'), $quota_free_mb), 'warning', 'lowaccountspace'.$quota_free_mb);
3468 }
3469 }
3470 }
3471
3472 // Always warn of this
3473 if (strpos($msg, 'File Size Limit Exceeded') !== false && 'UpdraftPlus_BinZip' == $this->use_zip_object) {
3474 $updraftplus->log(sprintf(__('The zip engine returned the message: %s.', 'updraftplus'), 'File Size Limit Exceeded'). __('Go here for more information.', 'updraftplus').' https://updraftplus.com/what-should-i-do-if-i-see-the-message-file-size-limit-exceeded/', 'warning', 'zipcloseerror-filesizelimit');
3475 } elseif ($warn) {
3476 $warn_msg = __('A zip error occurred', 'updraftplus').' - ';
3477 if (!empty($quota_low)) {
3478 $warn_msg = sprintf(__('your web hosting account appears to be full; please see: %s', 'updraftplus'), 'https://updraftplus.com/faqs/how-much-free-disk-space-do-i-need-to-create-a-backup/');
3479 } else {
3480 $warn_msg .= __('check your log for more details.', 'updraftplus');
3481 }
3482 $updraftplus->log($warn_msg, 'warning', 'zipcloseerror-'.$this->whichone);
3483 }
3484
3485 $updraftplus->log("The attempt to close the zip file returned an error ($msg). List of files we were trying to add follows (check their permissions).");
3486
3487 foreach ($files_zipadded_since_open as $ffile) {
3488 $updraftplus->log("File: ".$ffile['addas']." (exists: ".(int) @file_exists($ffile['file']).", is_readable: ".(int) @is_readable($ffile['file'])." size: ".@filesize($ffile['file']).')', 'notice', false, true);
3489 }
3490 }
3491
3492 private function bump_index() {
3493 global $updraftplus;
3494 $youwhat = $this->whichone;
3495
3496 $timetaken = max(microtime(true)-$this->zip_microtime_start, 0.000001);
3497
3498 $itext = (0 == $this->index) ? '' : ($this->index+1);
3499 $full_path = $this->zip_basename.$itext.'.zip';
3500
3501 $checksums = $updraftplus->which_checksums();
3502
3503 $checksum_description = '';
3504
3505 foreach ($checksums as $checksum) {
3506
3507 $cksum = hash_file($checksum, $full_path.'.tmp');
3508 $updraftplus->jobdata_set($checksum.'-'.$youwhat.$this->index, $cksum);
3509 if ($checksum_description) $checksum_description .= ', ';
3510 $checksum_description .= "$checksum: $cksum";
3511
3512 }
3513
3514 $next_full_path = $this->zip_basename.($this->index+2).'.zip';
3515 // We touch the next zip before renaming the temporary file; this indicates that the backup for the entity is not *necessarily* finished
3516 touch($next_full_path.'.tmp');
3517
3518 if (file_exists($full_path.'.tmp') && filesize($full_path.'.tmp') > 0) {
3519 if (!rename($full_path.'.tmp', $full_path)) {
3520 $updraftplus->log("Rename failed for $full_path.tmp");
3521 } else {
3522 UpdraftPlus_Job_Scheduler::something_useful_happened();
3523 }
3524 }
3525
3526 $kbsize = filesize($full_path)/1024;
3527 $rate = round($kbsize/$timetaken, 1);
3528 $updraftplus->log("Created ".$this->whichone." zip (".$this->index.") - ".round($kbsize, 1)." KB in ".round($timetaken, 1)." s ($rate KB/s) (checksums: $checksum_description)");
3529 $this->zip_microtime_start = microtime(true);
3530
3531 // No need to add $itext here - we can just delete any temporary files for this zip
3532 UpdraftPlus_Filesystem_Functions::clean_temporary_files('_'.$updraftplus->file_nonce."-".$youwhat, 600);
3533
3534 $this->index++;
3535 $this->job_file_entities[$youwhat]['index'] = $this->index;
3536 $updraftplus->jobdata_set('job_file_entities', $this->job_file_entities);
3537 }
3538
3539 /**
3540 * Returns the member of the array with key (int)0, as a new array. This function is used as a callback for array_map().
3541 *
3542 * @param Array $a - the array
3543 *
3544 * @return Array - with keys 'name' and 'type'
3545 */
3546 private function cb_get_name_base_type($a) {
3547 return array('name' => $a[0], 'type' => 'BASE TABLE');
3548 }
3549
3550 /**
3551 * Returns the members of the array with keys (int)0 and (int)1, as part of a new array.
3552 *
3553 * @param Array $a - the array
3554 *
3555 * @return Array - keys are 'name' and 'type'
3556 */
3557 private function cb_get_name_type($a) {
3558 return array('name' => $a[0], 'type' => $a[1]);
3559 }
3560
3561 /**
3562 * Returns the member of the array with key (string)'name'. This function is used as a callback for array_map().
3563 *
3564 * @param Array $a - the array
3565 *
3566 * @return Mixed - the value with key (string)'name'
3567 */
3568 private function cb_get_name($a) {
3569 return $a['name'];
3570 }
3571 }
3572
3573 class UpdraftPlus_WPDB_OtherDB extends wpdb {
3574 /**
3575 * This adjusted bail() does two things: 1) Never dies and 2) logs in the UD log
3576 *
3577 * @param string $message Error message
3578 * @param string $error_code Error Code
3579 * @return boolean
3580 */
3581 public function bail($message, $error_code = '500') {
3582 global $updraftplus;
3583 if ('db_connect_fail' == $error_code) $message = 'Connection failed: check your access details, that the database server is up, and that the network connection is not firewalled.';
3584 $updraftplus->log("WPDB_OtherDB error: $message ($error_code)");
3585 // Now do the things that would have been done anyway
3586 if (class_exists('WP_Error')) {
3587 $this->error = new WP_Error($error_code, $message);
3588 } else {
3589 $this->error = $message;
3590 }
3591 return false;
3592 }
3593 }
3594