PluginProbe
UpdraftPlus: WP Backup & Migration Plugin / 1.8.13
UpdraftPlus: WP Backup & Migration Plugin v1.8.13
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.8.13, at backup.php

1,949 lines 93.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(UPDRAFTPLUS_DIR.'/class-zip.php');
5
6 // This file contains functions that are only needed/loaded when a backup is running (reduces memory usage on other site pages)
7
8 class UpdraftPlus_Backup {
9
10 public $index = 0;
11
12 private $zipfiles_added;
13 private $zipfiles_added_thisrun = 0;
14 private $zipfiles_dirbatched;
15 private $zipfiles_batched;
16 private $zip_split_every = 838860800; # 800Mb
17 private $zip_last_ratio = 1;
18 private $whichone;
19 private $zip_basename = '';
20 private $zipfiles_lastwritetime;
21 // 0 = unknown; false = failed
22 public $binzip = 0;
23
24 private $dbhandle;
25 private $dbhandle_isgz;
26
27 private $use_zip_object = 'UpdraftPlus_ZipArchive';
28 public $debug = false;
29
30 private $updraft_dir;
31 private $job_file_entities = array();
32
33 public function __construct($backup_files) {
34
35 global $updraftplus;
36
37 # Decide which zip engine to begin with
38
39 $this->debug = UpdraftPlus_Options::get_updraft_option('updraft_debug_mode');
40 $this->updraft_dir = $updraftplus->backups_dir_location();
41
42 if ('no' === $backup_files) {
43 $this->use_zip_object = 'UpdraftPlus_PclZip';
44 return;
45 }
46
47 // false means 'tried + failed'; whereas 0 means 'not yet tried'
48 // Disallow binzip on OpenVZ when we're not sure there's plenty of memory
49 if ($this->binzip === 0 && (!defined('UPDRAFTPLUS_PREFERPCLZIP') || UPDRAFTPLUS_PREFERPCLZIP != true) && (!defined('UPDRAFTPLUS_NO_BINZIP') || !UPDRAFTPLUS_NO_BINZIP) && $updraftplus->current_resumption <9) {
50
51 if (@file_exists('/proc/user_beancounters') && @file_exists('/proc/meminfo') && @is_readable('/proc/meminfo')) {
52 $meminfo = @file_get_contents('/proc/meminfo', false, null, -1, 200);
53 if (is_string($meminfo) && preg_match('/MemTotal:\s+(\d+) kB/', $meminfo, $matches)) {
54 $memory_mb = $matches[1]/1024;
55 # 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
56 $vz_log = "OpenVZ; reported memory: ".round($memory_mb, 1)." Mb";
57 if ($memory_mb < 1024 || $memory_mb > 8192) {
58 $openvz_lowmem = true;
59 $vz_log .= " (will not use BinZip)";
60 }
61 $updraftplus->log($vz_log);
62 }
63 }
64 if (empty($openvz_lowmem)) {
65 $updraftplus->log('Checking if we have a zip executable available');
66 $binzip = $updraftplus->find_working_bin_zip();
67 if (is_string($binzip)) {
68 $updraftplus->log("Zip engine: found/will use a binary zip: $binzip");
69 $this->binzip = $binzip;
70 $this->use_zip_object = 'UpdraftPlus_BinZip';
71 }
72 }
73 }
74
75 # In tests, PclZip was found to be 25% slower than ZipArchive
76 if ($this->use_zip_object != 'UpdraftPlus_PclZip' && empty($this->binzip) && ((defined('UPDRAFTPLUS_PREFERPCLZIP') && UPDRAFTPLUS_PREFERPCLZIP == true) || !class_exists('ZipArchive') || !class_exists('UpdraftPlus_ZipArchive') || (!extension_loaded('zip') && !method_exists('ZipArchive', 'AddFile')))) {
77 global $updraftplus;
78 $updraftplus->log("Zip engine: ZipArchive is not available or is disabled (will use PclZip if needed)");
79 $this->use_zip_object = 'UpdraftPlus_PclZip';
80 }
81
82 }
83
84 public function create_zip($create_from_dir, $whichone, $backup_file_basename, $index) {
85 // Note: $create_from_dir can be an array or a string
86 @set_time_limit(900);
87
88 $original_index = $index;
89 $this->index = $index;
90 $this->whichone = $whichone;
91
92 global $updraftplus;
93
94 $this->zip_split_every = max((int)$updraftplus->jobdata_get('split_every'), UPDRAFTPLUS_SPLIT_MIN)*1048576;
95
96 if ('others' != $whichone) $updraftplus->log("Beginning creation of dump of $whichone (split every: ".round($this->zip_split_every/1048576,1)." Mb)");
97
98 if (is_string($create_from_dir) && !file_exists($create_from_dir)) {
99 $flag_error = true;
100 $updraftplus->log("Does not exist: $create_from_dir");
101 if ('mu-plugins' == $whichone) {
102 if (!function_exists('get_mu_plugins')) require_once(ABSPATH.'wp-admin/includes/plugin.php');
103 $mu_plugins = get_mu_plugins();
104 if (count($mu_plugins) == 0) {
105 $updraftplus->log("There appear to be no mu-plugins to back up. Will not raise an error.");
106 $flag_error = false;
107 }
108 }
109 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');
110 return false;
111 }
112
113 $itext = (empty($index)) ? '' : ($index+1);
114 $base_path = $backup_file_basename.'-'.$whichone.$itext.'.zip';
115 $full_path = $this->updraft_dir.'/'.$base_path;
116 $time_now = time();
117
118 if (file_exists($full_path)) {
119 # Gather any further files that may also exist
120 $files_existing = array();
121 while (file_exists($full_path)) {
122 $files_existing[] = $base_path;
123 $time_mod = (int)@filemtime($full_path);
124 $updraftplus->log($base_path.": this file has already been created (age: ".round($time_now-$time_mod,1)." s)");
125 if ($time_mod>100 && ($time_now-$time_mod)<30) {
126 $updraftplus->terminate_due_to_activity($base_path, $time_now, $time_mod);
127 }
128 $index++;
129 $base_path = $backup_file_basename.'-'.$whichone.$index.'.zip';
130 $full_path = $this->updraft_dir.'/'.$base_path;
131 }
132 }
133
134 // Temporary file, to be able to detect actual completion (upon which, it is renamed)
135
136 // New (Jun-13) - be more aggressive in removing temporary files from earlier attempts - anything >=600 seconds old of this kind
137 $updraftplus->clean_temporary_files('_'.$updraftplus->nonce."-$whichone", 600);
138
139 // 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
140 $zip_name = $full_path.'.tmp';
141 $time_mod = (int)@filemtime($zip_name);
142 if (file_exists($zip_name) && $time_mod>100 && ($time_now-$time_mod)<30) {
143 $updraftplus->terminate_due_to_activity($zip_name, $time_now, $time_mod);
144 } elseif (file_exists($zip_name)) {
145 $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).")");
146 }
147
148 // 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)
149 // Note: this doesn't catch PclZip temporary files
150 $d = dir($this->updraft_dir);
151 $match = '_'.$updraftplus->nonce."-".$whichone;
152 while (false !== ($e = $d->read())) {
153 if ('.' == $e || '..' == $e || !is_file($this->updraft_dir.'/'.$e)) continue;
154 $ziparchive_match = preg_match("/$match([0-9]+)?\.zip\.tmp\.([A-Za-z0-9]){6}?$/i", $e);
155 $binzip_match = preg_match("/^zi([A-Za-z0-9]){6}$/", $e);
156 if ($time_now-filemtime($this->updraft_dir.'/'.$e) < 30 && ($ziparchive_match || (0 != $updraftplus->current_resumption && $binzip_match))) {
157 $updraftplus->terminate_due_to_activity($this->updraft_dir.'/'.$e, $time_now, filemtime($this->updraft_dir.'/'.$e));
158 }
159 }
160 @$d->close();
161 clearstatcache();
162
163 if (isset($files_existing)) {
164 # 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.
165 # 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).
166 return $files_existing;
167 }
168
169 $this->log_account_space();
170
171 $this->zip_microtime_start = microtime(true);
172 # The paths in the zip should then begin with '$whichone', having removed WP_CONTENT_DIR from the front
173 $zipcode = $this->make_zipfile($create_from_dir, $backup_file_basename, $whichone);
174 if ($zipcode !== true) {
175 $updraftplus->log("ERROR: Zip failure: Could not create $whichone zip (".$this->index." / $index)");
176 $updraftplus->log(sprintf(__("Could not create %s zip. Consult the log file for more information.",'updraftplus'),$whichone), 'error');
177 # The caller is required to update $index from $this->index
178 return false;
179 } else {
180 $itext = (empty($this->index)) ? '' : ($this->index+1);
181 $full_path = $this->updraft_dir.'/'.$backup_file_basename.'-'.$whichone.$itext.'.zip';
182 if (file_exists($full_path.'.tmp')) {
183 if (@filesize($full_path.'.tmp') === 0) {
184 $updraftplus->log("Did not create $whichone zip (".$this->index.") - not needed");
185 @unlink($full_path.'.tmp');
186 } else {
187 $sha = sha1_file($full_path.'.tmp');
188 $updraftplus->jobdata_set('sha1-'.$whichone.$this->index, $sha);
189 @rename($full_path.'.tmp', $full_path);
190 $timetaken = max(microtime(true)-$this->zip_microtime_start, 0.000001);
191 $kbsize = filesize($full_path)/1024;
192 $rate = round($kbsize/$timetaken, 1);
193 $updraftplus->log("Created $whichone zip (".$this->index.") - ".round($kbsize,1)." Kb in ".round($timetaken,1)." s ($rate Kb/s) (SHA1 checksum: $sha)");
194 // We can now remove any left-over temporary files from this job
195 }
196 } elseif ($this->index > $original_index) {
197 $updraftplus->log("Did not create $whichone zip (".$this->index.") - not needed");
198 # Added 12-Feb-2014 (to help multiple morefiles)
199 $this->index--;
200 } else {
201 $updraftplus->log("Looked-for $whichone zip (".$this->index.") was not found (".basename($full_path).".tmp)", 'warning');
202 }
203 $updraftplus->clean_temporary_files('_'.$updraftplus->nonce."-$whichone", 0);
204 }
205
206 # Create the results array to send back (just the new ones, not any prior ones)
207 $files_existing = array();
208 $res_index = 0;
209 for ($i = $original_index; $i<= $this->index; $i++) {
210 $itext = (empty($i)) ? '' : ($i+1);
211 $full_path = $this->updraft_dir.'/'.$backup_file_basename.'-'.$whichone.$itext.'.zip';
212 if (file_exists($full_path)) {
213 $files_existing[$res_index] = $backup_file_basename.'-'.$whichone.$itext.'.zip';
214 }
215 $res_index++;
216 }
217 return $files_existing;
218 }
219
220 // Dispatch to the relevant function
221 public function cloud_backup($backup_array) {
222
223 global $updraftplus;
224
225 $services = $updraftplus->just_one($updraftplus->jobdata_get('service'));
226 if (!is_array($services)) $services = array($services);
227
228 $updraftplus->jobdata_set('jobstatus', 'clouduploading');
229
230 add_action('http_api_curl', array($updraftplus, 'add_curl_capath'));
231
232 $upload_status = $updraftplus->jobdata_get('uploading_substatus');
233 if (!is_array($upload_status) || !isset($upload_status['t'])) {
234 $upload_status = array('i' => 0, 'p' => 0, 't' => max(1, count($services))*count($backup_array));
235 $updraftplus->jobdata_set('uploading_substatus', $upload_status);
236 }
237
238 $do_prune = array();
239
240 # 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
241 if (count($services) >1 && !empty($updraftplus->no_checkin_last_time)) {
242 $updraftplus->log('No check-in last time: will try a different remote service first');
243 array_push($services, array_shift($services));
244 if (1 == ($updraftplus->current_resumption % 2) && count($services)>2) array_push($services, array_shift($services));
245 }
246
247 $errors_before_uploads = $updraftplus->error_count();
248
249 foreach ($services as $ind => $service) {
250
251 # Used for logging by record_upload_chunk()
252 $this->current_service = $service;
253 # Used when deciding whether to delete the local file
254 $this->last_service = ($ind+1 >= count($services) && $errors_before_uploads == $updraftplus->error_count()) ? true : false;
255
256 $updraftplus->log("Cloud backup selection: ".$service);
257 @set_time_limit(900);
258
259 $method_include = UPDRAFTPLUS_DIR.'/methods/'.$service.'.php';
260 if (file_exists($method_include)) require_once($method_include);
261
262 if ($service == "none" || $service == "") {
263 $updraftplus->log("No remote despatch: user chose no remote backup service");
264 $this->prune_retained_backups(array("none" => array(null, null)));
265 } else {
266 $updraftplus->log("Beginning dispatch of backup to remote ($service)");
267 $sarray = array();
268 foreach ($backup_array as $bind => $file) {
269 if ($updraftplus->is_uploaded($file, $service)) {
270 $updraftplus->log("Already uploaded to $service: $file");
271 } else {
272 $sarray[$bind] = $file;
273 }
274 }
275 if (count($sarray)>0) {
276 $objname = "UpdraftPlus_BackupModule_${service}";
277 if (class_exists($objname)) {
278 $remote_obj = new $objname;
279 $pass_to_prune = $remote_obj->backup($backup_array);
280 $do_prune[$service] = array($remote_obj, $pass_to_prune);
281 } else {
282 $updraftplus->log("Unexpected error: no class '$objname' was found ($method_include)");
283 $updraftplus->log(__("Unexpected error: no class '$objname' was found (your UpdraftPlus installation seems broken - try re-installing)",'updraftplus'), 'error');
284 }
285 }
286 }
287 }
288
289 if (!empty($do_prune)) $this->prune_retained_backups($do_prune);
290
291 remove_action('http_api_curl', array($updraftplus, 'add_curl_capath'));
292
293 }
294
295 // Carries out retain behaviour. Pass in a valid S3 or FTP object and path if relevant.
296 // Services *must* be an array
297 public function prune_retained_backups($services) {
298
299 global $updraftplus;
300
301 // If they turned off deletion on local backups, then there is nothing to do
302 if (UpdraftPlus_Options::get_updraft_option('updraft_delete_local') == 0 && count($services) == 1 && in_array('none', $services)) {
303 $updraftplus->log("Prune old backups from local store: nothing to do, since the user disabled local deletion and we are using local backups");
304 return;
305 }
306
307 $updraftplus->jobdata_set('jobstatus', 'pruning');
308
309 // Number of backups to retain - files
310 $updraft_retain = UpdraftPlus_Options::get_updraft_option('updraft_retain', 2);
311 $updraft_retain = (is_numeric($updraft_retain)) ? $updraft_retain : 1;
312
313 // Number of backups to retain - db
314 $updraft_retain_db = UpdraftPlus_Options::get_updraft_option('updraft_retain_db', $updraft_retain);
315 $updraft_retain_db = (is_numeric($updraft_retain_db)) ? $updraft_retain_db : 1;
316
317 $updraftplus->log("Retain: beginning examination of existing backup sets; user setting: retain_files=$updraft_retain, retain_db=$updraft_retain_db");
318
319 // Returns an array, most recent first, of backup sets
320 $backup_history = $updraftplus->get_backup_history();
321 $db_backups_found = 0;
322 $file_backups_found = 0;
323 $updraftplus->log("Number of backup sets in history: ".count($backup_history));
324
325 $backupable_entities = $updraftplus->get_backupable_file_entities(true);
326
327 $database_backups_found = array();
328
329 $file_entities_backups_found = array();
330 foreach ($backupable_entities as $entity => $info) {
331 $file_entities_backups_found[$entity] = 0;
332 }
333
334 foreach ($backup_history as $backup_datestamp => $backup_to_examine) {
335
336 $files_to_prune = array();
337
338 // $backup_to_examine is an array of file names, keyed on db/plugins/themes/uploads
339 // The new backup_history array is saved afterwards, so remember to unset the ones that are to be deleted
340 $updraftplus->log(sprintf("Examining backup set with datestamp: %s (%s)", $backup_datestamp, gmdate('M d Y H:i:s', $backup_datestamp)));
341
342 # Databases
343 foreach ($backup_to_examine as $key => $data) {
344 if ('db' != strtolower(substr($key, 0, 2)) || '-size' == substr($key, -5, 5)) continue;
345
346 $database_backups_found[$key] = (empty($database_backups_found[$key])) ? 1 : $database_backups_found[$key] + 1;
347
348 $fname = (is_string($data)) ? $data : $data[0];
349 $updraftplus->log("$backup_datestamp: $key: this set includes a database (".$fname."); db count is now ".$database_backups_found[$key]);
350 if ($database_backups_found[$key] > $updraft_retain_db) {
351 $updraftplus->log("$backup_datestamp: $key: over retain limit ($updraft_retain_db); will delete this database");
352 if (!empty($data)) {
353 foreach ($services as $service => $sd) $this->prune_file($service, $data, $sd[0], $sd[1]);
354 }
355 unset($backup_to_examine[$key]);
356 $updraftplus->record_still_alive();
357 }
358 }
359
360 foreach ($backupable_entities as $entity => $info) {
361 if (!empty($backup_to_examine[$entity])) {
362 $file_entities_backups_found[$entity]++;
363 if ($file_entities_backups_found[$entity] > $updraft_retain) {
364 $prune_this = $backup_to_examine[$entity];
365 if (is_string($prune_this)) $prune_this = array($prune_this);
366 foreach ($prune_this as $prune_file) {
367 $updraftplus->log("$entity: $backup_datestamp: over retain limit ($updraft_retain); will delete this file ($prune_file)");
368 $files_to_prune[] = $prune_file;
369 }
370 unset($backup_to_examine[$entity]);
371 }
372 }
373 }
374
375 # Actually delete the files
376 foreach ($services as $service => $sd) {
377 $this->prune_file($service, $files_to_prune, $sd[0], $sd[1]);
378 $updraftplus->record_still_alive();
379 }
380
381 // Get new result, post-deletion; anything left in this set?
382 $contains_files = 0;
383 foreach ($backupable_entities as $entity => $info) {
384 if (isset($backup_to_examine[$entity])) {
385 $contains_files = 1;
386 break;
387 }
388 }
389
390 $contains_db = 0;
391 foreach ($backup_to_examine as $key => $data) {
392 if ('db' == strtolower(substr($key, 0, 2)) && '-size' != substr($key, -5, 5)) {
393 $contains_db = 1;
394 break;
395 }
396 }
397
398 // Delete backup set completely if empty, o/w just remove DB
399 // We search on the four keys which represent data, allowing other keys to be used to track other things
400 if (!$contains_files && !$contains_db) {
401 $updraftplus->log("$backup_datestamp: this backup set is now empty; will remove from history");
402 unset($backup_history[$backup_datestamp]);
403 if (isset($backup_to_examine['nonce'])) {
404 $fullpath = $this->updraft_dir.'/log.'.$backup_to_examine['nonce'].'.txt';
405 if (is_file($fullpath)) {
406 $updraftplus->log("$backup_datestamp: deleting log file (log.".$backup_to_examine['nonce'].".txt)");
407 @unlink($fullpath);
408 } else {
409 $updraftplus->log("$backup_datestamp: corresponding log file not found - must have already been deleted");
410 }
411 } else {
412 $updraftplus->log("$backup_datestamp: no nonce record found in the backup set, so cannot delete any remaining log file");
413 }
414 } else {
415 $updraftplus->log("$backup_datestamp: this backup set remains non-empty ($contains_files/$contains_db); will retain in history");
416 $backup_history[$backup_datestamp] = $backup_to_examine;
417 }
418 # Loop over backup sets
419 }
420 $updraftplus->log("Retain: saving new backup history (sets now: ".count($backup_history).") and finishing retain operation");
421 UpdraftPlus_Options::update_updraft_option('updraft_backup_history', $backup_history, false);
422 }
423
424 # $dofiles: An array of files (or a single string for one file)
425 private function prune_file($service, $dofiles, $method_object = null, $object_passback = null) {
426 global $updraftplus;
427 if (!is_array($dofiles)) $dofiles=array($dofiles);
428 foreach ($dofiles as $dofile) {
429 if (empty($dofile)) continue;
430 $updraftplus->log("Delete file: $dofile, service=$service");
431 $fullpath = $this->updraft_dir.'/'.$dofile;
432 // delete it if it's locally available
433 if (file_exists($fullpath)) {
434 $updraftplus->log("Deleting local copy ($dofile)");
435 @unlink($fullpath);
436 }
437 }
438 // Despatch to the particular method's deletion routine
439 if (!is_null($method_object)) $method_object->delete($dofiles, $object_passback);
440 }
441
442 public function send_results_email($final_message) {
443
444 global $updraftplus;
445
446 $debug_mode = UpdraftPlus_Options::get_updraft_option('updraft_debug_mode');
447
448 $sendmail_to = $updraftplus->just_one_email(UpdraftPlus_Options::get_updraft_option('updraft_email'));
449 if (is_string($sendmail_to)) $sendmail_to = array($sendmail_to);
450
451 $backup_files = $updraftplus->jobdata_get('backup_files');
452 $backup_db = $updraftplus->jobdata_get('backup_database');
453
454 if ('finished' == $backup_files && ('finished' == $backup_db || 'encrypted' == $backup_db)) {
455 $backup_contains = __("Files and database", 'updraftplus');
456 } elseif ('finished' == $backup_files) {
457 $backup_contains = ($backup_db == "begun") ? __("Files (database backup has not completed)", 'updraftplus') : __("Files only (database was not part of this particular schedule)", 'updraftplus');
458 } elseif ($backup_db == 'finished' || $backup_db == 'encrypted') {
459 $backup_contains = ($backup_files == "begun") ? __("Database (files backup has not completed)", 'updraftplus') : __("Database only (files were not part of this particular schedule)", 'updraftplus');
460 } else {
461 $backup_contains = __("Unknown/unexpected error - please raise a support request", 'updraftplus');
462 }
463
464 $append_log = '';
465 $attachments = array();
466
467 $error_count = 0;
468
469 if ($updraftplus->error_count() > 0) {
470 $append_log .= __('Errors encountered:', 'updraftplus')."\r\n";
471 $attachments[0] = $updraftplus->logfile_name;
472 foreach ($updraftplus->errors as $err) {
473 if (is_wp_error($err)) {
474 foreach ($err->get_error_messages() as $msg) {
475 $append_log .= "* ".rtrim($msg)."\r\n";
476 }
477 } elseif (is_array($err) && 'error' == $err['level']) {
478 $append_log .= "* ".rtrim($err['message'])."\r\n";
479 } elseif (is_string($err)) {
480 $append_log .= "* ".rtrim($err)."\r\n";
481 }
482 $error_count++;
483 }
484 $append_log.="\r\n";
485 }
486 $warnings = $updraftplus->jobdata_get('warnings');
487 if (is_array($warnings) && count($warnings) >0) {
488 $append_log .= __('Warnings encountered:', 'updraftplus')."\r\n";
489 $attachments[0] = $updraftplus->logfile_name;
490 foreach ($warnings as $err) {
491 $append_log .= "* ".rtrim($err)."\r\n";
492 }
493 $append_log.="\r\n";
494 }
495
496 if ($debug_mode && '' != $updraftplus->logfile_name && !in_array($updraftplus->logfile_name, $attachments)) {
497 $append_log .= "\r\n".__('The log file has been attached to this email.', 'updraftplus');
498 $attachments[0] = $updraftplus->logfile_name;
499 }
500
501 // We have to use the action in order to set the MIME type on the attachment - by default, WordPress just puts application/octet-stream
502
503 $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));
504
505 $body = apply_filters('updraft_report_body', __('Backup of:').' '.site_url()."\r\nUpdraftPlus ".__('WordPress backup is complete','updraftplus').".\r\n".__('Backup contains:','updraftplus').' '.$backup_contains."\r\n".__('Latest status:', 'updraftplus').' '.$final_message."\r\n\r\n".$updraftplus->wordshell_random_advert(0)."\r\n".$append_log, $final_message, $backup_contains, $updraftplus->errors, $warnings);
506
507 $this->attachments = apply_filters('updraft_report_attachments', $attachments);
508
509 if (count($this->attachments)>0) add_action('phpmailer_init', array($this, 'phpmailer_init'));
510
511 $attach_size = 0;
512 $unlink_files = array();
513
514 foreach ($this->attachments as $ind => $attach) {
515 if ($attach == $updraftplus->logfile_name && filesize($attach) > 6*1048576) {
516
517 $updraftplus->log("Log file is large (".round(filesize($attach)/1024, 1)." Kb): will compress before e-mailing");
518
519 if (!$handle = fopen($attach, "r")) {
520 $updraftplus->log("Error: Failed to open log file for reading: ".$attach);
521 } else {
522 if (!$whandle = gzopen($attach.'.gz', 'w')) {
523 $updraftplus->log("Error: Failed to open log file for reading: ".$attach.".gz");
524 } else {
525 while (false !== ($line = @stream_get_line($handle, 131072, "\n"))) {
526 @gzwrite($whandle, $line."\n");
527 }
528 fclose($handle);
529 gzclose($whandle);
530 $this->attachments[$ind] = $attach.'.gz';
531 $unlink_files[] = $attach.'.gz';
532 }
533 }
534 }
535 $attach_size += filesize($this->attachments[$ind]);
536 }
537
538 foreach ($sendmail_to as $ind => $mailto) {
539
540 if (false === apply_filters('updraft_report_sendto', true, $mailto, $error_count, count($warnings), $ind)) continue;
541
542 foreach (explode(',', $mailto) as $sendmail_addr) {
543 $updraftplus->log("Sending email ('$backup_contains') report (attachments: ".count($attachments).", size: ".round($attach_size/1024, 1)." Kb) to: ".substr($sendmail_addr, 0, 5)."...");
544 wp_mail(trim($sendmail_addr), $subject, $body);
545 }
546 }
547
548 foreach ($unlink_files as $file) @unlink($file);
549
550 do_action('updraft_report_finished');
551 if (count($this->attachments)>0) remove_action('phpmailer_init', array($this, 'phpmailer_init'));
552
553 }
554
555 // 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 usermeta table; and after that the core WP tables - so that when restoring we restore the core tables first
556 private function backup_db_sorttables($a, $b) {
557 global $updraftplus, $wpdb;
558 if ($a == $b) return 0;
559 $our_table_prefix = $this->table_prefix;
560 if ($a == $our_table_prefix.'options') return -1;
561 if ($b == $our_table_prefix.'options') return 1;
562 if ($a == $our_table_prefix.'users') return -1;
563 if ($b == $our_table_prefix.'users') return 1;
564 if ($a == $our_table_prefix.'usermeta') return -1;
565 if ($b == $our_table_prefix.'usermeta') return 1;
566
567 if (empty($our_table_prefix)) return strcmp($a, $b);
568
569 try {
570 $core_tables = array_merge($wpdb->tables, $wpdb->global_tables, $wpdb->ms_global_tables);
571 } catch (Exception $e) {
572 }
573 if (empty($core_tables)) $core_tables = array('terms', 'term_taxonomy', 'term_relationships', 'commentmeta', 'comments', 'links', 'postmeta', 'posts', 'site', 'sitemeta', 'blogs', 'blogversions');
574
575 global $updraftplus;
576 $na = $updraftplus->str_replace_once($our_table_prefix, '', $a);
577 $nb = $updraftplus->str_replace_once($our_table_prefix, '', $b);
578 if (in_array($na, $core_tables) && !in_array($nb, $core_tables)) return -1;
579 if (!in_array($na, $core_tables) && in_array($nb, $core_tables)) return 1;
580 return strcmp($a, $b);
581 }
582
583 private function log_account_space() {
584 # Don't waste time if space is huge
585 if (!empty($this->account_space_oodles)) return;
586 global $updraftplus;
587 $hosting_bytes_free = $updraftplus->get_hosting_disk_quota_free();
588 if (is_array($hosting_bytes_free)) {
589 $perc = round(100*$hosting_bytes_free[1]/(max($hosting_bytes_free[2], 1)), 1);
590 $updraftplus->log(sprintf('Free disk space in account: %s (%s used)', round($hosting_bytes_free[3]/1048576, 1)." Mb", "$perc %"));
591 }
592 }
593
594 // This function is resumable
595 public function backup_dirs($job_status) {
596
597 global $updraftplus;
598
599 if(!$updraftplus->backup_time) $updraftplus->backup_time_nonce();
600
601 //get the blog name and rip out all non-alphanumeric chars other than _
602 $blog_name = preg_replace('/[^A-Za-z0-9_]/','', str_replace(' ','_', substr(get_bloginfo(), 0, 32)));
603 if (!$blog_name) $blog_name = 'non_alpha_name';
604 $blog_name = apply_filters('updraftplus_blog_name', $blog_name);
605
606 $backup_file_basename = 'backup_'.get_date_from_gmt(gmdate('Y-m-d H:i:s', $updraftplus->backup_time), 'Y-m-d-Hi').'_'.$blog_name.'_'.$updraftplus->nonce;
607
608 $backup_array = array();
609
610 $possible_backups = $updraftplus->get_backupable_file_entities(true);
611
612 // Was there a check-in last time? If not, then reduce the amount of data attempted
613 if ($job_status != 'finished' && $updraftplus->current_resumption >= 2 && $updraftplus->current_resumption<=10) {
614 $maxzipbatch = $updraftplus->jobdata_get('maxzipbatch', 26214400);
615 if ((int)$maxzipbatch < 1) $maxzipbatch = 26214400;
616
617 # 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)
618
619 if (!empty($updraftplus->no_checkin_last_time)) {
620 if ($updraftplus->current_resumption - $updraftplus->last_successful_resumption > 2) {
621 $this->try_split = true;
622 } else {
623 $new_maxzipbatch = max(floor($maxzipbatch * 0.75), 20971520);
624 if ($new_maxzipbatch < $maxzipbatch) {
625 $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)");
626 $updraftplus->jobdata_set('maxzipbatch', $new_maxzipbatch);
627 $updraftplus->jobdata_set('maxzipbatch_ceiling', $new_maxzipbatch);
628 }
629 }
630 }
631 }
632
633 if($job_status != 'finished' && !$updraftplus->really_is_writable($this->updraft_dir)) {
634 $updraftplus->log("Backup directory (".$this->updraft_dir.") is not writable, or does not exist");
635 $updraftplus->log(sprintf(__("Backup directory (%s) is not writable, or does not exist.", 'updraftplus'), $this->updraft_dir), 'error');
636 return array();
637 }
638
639 $this->job_file_entities = $updraftplus->jobdata_get('job_file_entities');
640 # This is just used for the visual feedback (via the 'substatus' key)
641 $which_entity = 0;
642 # e.g. plugins, themes, uploads, others
643 # $whichdir might be an array (if $youwhat is 'more')
644 foreach ($possible_backups as $youwhat => $whichdir) {
645
646 if (isset($this->job_file_entities[$youwhat])) {
647
648 $index = (int)$this->job_file_entities[$youwhat]['index'];
649 if (empty($index)) $index=0;
650 $indextext = (0 == $index) ? '' : (1+$index);
651 $zip_file = $this->updraft_dir.'/'.$backup_file_basename.'-'.$youwhat.$indextext.'.zip';
652
653 # Split needed?
654 $split_every=max((int)$updraftplus->jobdata_get('split_every'), 250);
655 if (file_exists($zip_file) && filesize($zip_file) > $split_every*1048576) {
656 $index++;
657 $this->job_file_entities[$youwhat]['index'] = $index;
658 $updraftplus->jobdata_set('job_file_entities', $this->job_file_entities);
659 }
660
661 // Populate prior parts of array, if we're on a subsequent zip file
662 if ($index >0) {
663 for ($i=0; $i<$index; $i++) {
664 $itext = (0 == $i) ? '' : ($i+1);
665 $backup_array[$youwhat][$i] = $backup_file_basename.'-'.$youwhat.$itext.'.zip';
666 $z = $this->updraft_dir.'/'.$backup_file_basename.'-'.$youwhat.$itext.'.zip';
667 $itext = (0 == $i) ? '' : $i;
668 if (file_exists($z)) $backup_array[$youwhat.$itext.'-size'] = filesize($z);
669 }
670 }
671
672 if ('finished' == $job_status) {
673 // Add the final part of the array
674 if ($index >0) {
675 $fbase = $backup_file_basename.'-'.$youwhat.($index+1).'.zip';
676 $z = $this->updraft_dir.'/'.$fbase;
677 if (file_exists($z)) {
678 $backup_array[$youwhat][$index] = $fbase;
679 $backup_array[$youwhat.$index.'-size'] = filesize($z);
680 }
681 } else {
682 $backup_array[$youwhat] = $backup_file_basename.'-'.$youwhat.'.zip';
683 if (file_exists($zip_file)) $backup_array[$youwhat.'-size'] = filesize($zip_file);
684 }
685 } else {
686
687 $which_entity++;
688 $updraftplus->jobdata_set('filecreating_substatus', array('e' => $youwhat, 'i' => $which_entity, 't' => count($this->job_file_entities)));
689
690 if ('others' == $youwhat) $updraftplus->log("Beginning backup of other directories found in the content directory (index: $index)");
691
692 # Apply a filter to allow add-ons to provide their own method for creating a zip of the entity
693 $created = apply_filters('updraftplus_backup_makezip_'.$youwhat, $whichdir, $backup_file_basename, $index);
694 # If the filter did not lead to something being created, then use the default method
695 if ($created === $whichdir) {
696
697 // http://www.phpconcept.net/pclzip/user-guide/53
698 /* First parameter to create is:
699 An array of filenames or dirnames,
700 or
701 A string containing the filename or a dirname,
702 or
703 A string containing a list of filename or dirname separated by a comma.
704 */
705
706 if ('others' == $youwhat) {
707 $dirlist = $updraftplus->backup_others_dirlist(true);
708 } elseif ('uploads' == $youwhat) {
709 $dirlist = $updraftplus->backup_uploads_dirlist(true);
710 } else {
711 $dirlist = $whichdir;
712 if (is_array($dirlist)) $dirlist=array_shift($dirlist);
713 }
714
715 if (count($dirlist)>0) {
716 $created = $this->create_zip($dirlist, $youwhat, $backup_file_basename, $index);
717 # Now, store the results
718 if (!is_string($created) && !is_array($created)) $updraftplus->log("$youwhat: create_zip returned an error");
719 } else {
720 $updraftplus->log("No backup of $youwhat: there was nothing found to back up");
721 }
722 }
723
724 if ($created != $whichdir && (is_string($created) || is_array($created))) {
725 if (is_string($created)) $created=array($created);
726 foreach ($created as $findex => $fname) {
727 $backup_array[$youwhat][$index] = $fname;
728 $itext = ($index == 0) ? '' : $index;
729 $index++;
730 $backup_array[$youwhat.$itext.'-size'] = filesize($this->updraft_dir.'/'.$fname);
731 }
732 }
733
734 $this->job_file_entities[$youwhat]['index'] = $this->index;
735 $updraftplus->jobdata_set('job_file_entities', $this->job_file_entities);
736
737 }
738 } else {
739 $updraftplus->log("No backup of $youwhat: excluded by user's options");
740 }
741 }
742
743 return $backup_array;
744 }
745
746 // 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.
747 public function resumable_backup_of_files($resumption_no) {
748 global $updraftplus;
749 //backup directories and return a numerically indexed array of file paths to the backup files
750 $bfiles_status = $updraftplus->jobdata_get('backup_files');
751 if ('finished' == $bfiles_status) {
752 $updraftplus->log("Creation of backups of directories: already finished");
753 $backup_array = $updraftplus->jobdata_get('backup_files_array');
754 if (!is_array($backup_array)) $backup_array = array();
755
756 # Check for recent activity
757 foreach ($backup_array as $files) {
758 if (!is_array($files)) $files=array($files);
759 foreach ($files as $file) $updraftplus->check_recent_modification($this->updraft_dir.'/'.$file);
760 }
761 } elseif ('begun' == $bfiles_status) {
762 if ($resumption_no>0) {
763 $updraftplus->log("Creation of backups of directories: had begun; will resume");
764 } else {
765 $updraftplus->log("Creation of backups of directories: beginning");
766 }
767 $updraftplus->jobdata_set('jobstatus', 'filescreating');
768 $backup_array = $this->backup_dirs($bfiles_status);
769 $updraftplus->jobdata_set('backup_files_array', $backup_array);
770 $updraftplus->jobdata_set('backup_files', 'finished');
771 $updraftplus->jobdata_set('jobstatus', 'filescreated');
772 } else {
773 # This is not necessarily a backup run which is meant to contain files at all
774 $updraftplus->log('This backup run is not intended for files - skipping');
775 return array();
776 }
777
778 /*
779 // DOES NOT WORK: there is no crash-safe way to do this here - have to be renamed at cloud-upload time instead
780 $new_backup_array = array();
781 foreach ($backup_array as $entity => $files) {
782 if (!is_array($files)) $files=array($files);
783 $outof = count($files);
784 foreach ($files as $ind => $file) {
785 $nval = $file;
786 if (preg_match('/^(backup_[\-0-9]{15}_.*_[0-9a-f]{12}-[\-a-z]+)([0-9]+)?\.zip$/i', $file, $matches)) {
787 $num = max((int)$matches[2],1);
788 $new = $matches[1].$num.'of'.$outof.'.zip';
789 if (file_exists($this->updraft_dir.'/'.$file)) {
790 if (@rename($this->updraft_dir.'/'.$file, $this->updraft_dir.'/'.$new)) {
791 $updraftplus->log(sprintf("Renaming: %s to %s", $file, $new));
792 $nval = $new;
793 }
794 } elseif (file_exists($this->updraft_dir.'/'.$new)) {
795 $nval = $new;
796 }
797 }
798 $new_backup_array[$entity][$ind] = $nval;
799 }
800 }
801 */
802 return $backup_array;
803 }
804
805 /* This function is resumable, using the following method:
806 - Each table is written out to ($final_filename).table.tmp
807 - When the writing finishes, it is renamed to ($final_filename).table
808 - When all tables are finished, they are concatenated into the final file
809 */
810 public function backup_db($already_done = 'begun') {
811
812 global $updraftplus, $wpdb;
813
814 $this->table_prefix = $updraftplus->get_table_prefix(true);
815 $this->table_prefix_raw = $updraftplus->get_table_prefix(false);
816
817 $errors = 0;
818
819 if (!$updraftplus->backup_time) $updraftplus->backup_time_nonce();
820 if (!$updraftplus->opened_log_time) $updraftplus->logfile_open($updraftplus->nonce);
821
822 // Get the blog name and rip out all non-alphanumeric chars other than _
823 $blog_name = preg_replace('/[^A-Za-z0-9_]/','', str_replace(' ','_', substr(get_bloginfo(), 0, 32)));
824 if (!$blog_name) $blog_name = 'non_alpha_name';
825 $blog_name = apply_filters('updraftplus_blog_name', $blog_name);
826
827 $file_base = 'backup_'.get_date_from_gmt(gmdate('Y-m-d H:i:s', $updraftplus->backup_time), 'Y-m-d-Hi').'_'.$blog_name.'_'.$updraftplus->nonce;
828 $backup_file_base = $this->updraft_dir.'/'.$file_base;
829
830 if ('finished' == $already_done) return basename($backup_file_base.'-db.gz');
831 if ('encrypted' == $already_done) return basename($backup_file_base.'-db.gz.crypt');
832
833 $updraftplus->jobdata_set('jobstatus', 'dbcreating');
834
835 $binsqldump = $updraftplus->find_working_sqldump();
836
837 $total_tables = 0;
838
839 $all_tables = $wpdb->get_results("SHOW TABLES", ARRAY_N);
840 $all_tables = array_map(create_function('$a', 'return $a[0];'), $all_tables);
841
842 if (0 == count($all_tables)) {
843 $extra = ($updraftplus->newresumption_scheduled) ? ' - '.__('please wait for the rescheduled attempt', 'updraftplus') : '';
844 $updraftplus->log("Error: No database tables found (SHOW TABLES returned nothing)".$extra);
845 $updraftplus->log(__("No database tables found", 'updraftplus').$extra, 'error');
846 die;
847 }
848
849 // Put the options table first
850 usort($all_tables, array($this, 'backup_db_sorttables'));
851
852 if (!$updraftplus->really_is_writable($this->updraft_dir)) {
853 $updraftplus->log("The backup directory (".$this->updraft_dir.") is not writable.");
854 $updraftplus->log($this->updraft_dir.": ".__('The backup directory is not writable - the database backup is expected to shortly fail.','updraftplus'), 'warning');
855 # Why not just fail now? We saw a bizarre case when the results of really_is_writable() changed during the run.
856 }
857
858 $stitch_files = array();
859
860 $how_many_tables = count($all_tables);
861
862 $found_options_table = false;
863
864 foreach ($all_tables as $table) {
865
866 $manyrows_warning = false;
867 $total_tables++;
868
869 // Increase script execution time-limit to 15 min for every table.
870 @set_time_limit(900);
871 // The table file may already exist if we have produced it on a previous run
872 $table_file_prefix = $file_base.'-db-table-'.$table.'.table';
873
874 if ($this->table_prefix_raw.'options' == $table) $found_options_table = true;
875
876 if (file_exists($this->updraft_dir.'/'.$table_file_prefix.'.gz')) {
877 $updraftplus->log("Table $table: corresponding file already exists; moving on");
878 $stitch_files[] = $table_file_prefix;
879 } else {
880 # === is needed, otherwise 'false' matches (i.e. prefix does not match)
881 if (empty($this->table_prefix) || strpos($table, $this->table_prefix) === 0 ) {
882
883 // Open file, store the handle
884 $opened = $this->backup_db_open($this->updraft_dir.'/'.$table_file_prefix.'.tmp.gz', true);
885 if (false === $opened) return false;
886
887 // Create the SQL statements
888 $this->stow("# " . sprintf('Table: %s' ,$updraftplus->backquote($table)) . "\n");
889 $updraftplus->jobdata_set('dbcreating_substatus', array('t' => $table, 'i' => $total_tables, 'a' => $how_many_tables));
890
891 $table_status = $wpdb->get_row("SHOW TABLE STATUS WHERE Name='$table'");
892 if (isset($table_status->Rows)) {
893 $rows = $table_status->Rows;
894 $updraftplus->log("Table $table: Total expected rows (approximate): ".$rows);
895 $this->stow("# Approximate rows expected in table: $rows\n");
896 if ($rows > UPDRAFTPLUS_WARN_DB_ROWS) {
897 $manyrows_warning = true;
898 $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), 'warning', 'manyrows_'.$table);
899 }
900 }
901
902 # Don't include the job data for any backups - so that when the database is restored, it doesn't continue an apparently incomplete backup
903 if (!empty($this->table_prefix) && $this->table_prefix.'sitemeta' == $table) {
904 $where = 'meta_key NOT LIKE "updraft_jobdata_%"';
905 } elseif (!empty($this->table_prefix) && $this->table_prefix.'options' == $table) {
906 $where = 'option_name NOT LIKE "updraft_jobdata_%"';
907 } else {
908 $where = '';
909 }
910
911 # TODO: If no check-in last time, then try the other method (but - any point in retrying slow method on large tables??)
912
913 # TODO: Lower this from 10,000 if the feedback is good
914 $bindump = (isset($rows) && $rows>10000 && is_string($binsqldump)) ? $this->backup_table_bindump($binsqldump, $table, $where) : false;
915 if (true !== $bindump) $this->backup_table($table, $where);
916
917 if (!empty($manyrows_warning)) $updraftplus->log_removewarning('manyrows_'.$table);
918
919 // Close file
920 $updraftplus->log("Table $table: finishing file (${table_file_prefix}.gz - ".round(filesize($this->updraft_dir.'/'.$table_file_prefix.'.tmp.gz')/1024,1)." Kb)");
921 $this->close($this->dbhandle);
922 rename($this->updraft_dir.'/'.$table_file_prefix.'.tmp.gz', $this->updraft_dir.'/'.$table_file_prefix.'.gz');
923 $updraftplus->something_useful_happened();
924 $stitch_files[] = $table_file_prefix;
925
926 } else {
927 $total_tables--;
928 $updraftplus->log("Skipping table (lacks our prefix (".$this->table_prefix.")): $table");
929 }
930
931 }
932 }
933
934 if (!$found_options_table) {
935 $updraftplus->log(__('The database backup appears to have failed - the options table was not found', 'updraftplus'), 'warning', 'optstablenotfound');
936 $time_this_run = time()-$updraftplus->opened_log_time;
937 if ($time_this_run > 2000) {
938 # 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.
939 # If we have been running that long, then the resumption may be far off; bring it closer
940 $updraftplus->reschedule(60);
941 $updraftplus->log("Have been running very long, and it seems the database went away; terminating");
942 $updraftplus->record_still_alive();
943 die;
944 }
945 } else {
946 $updraftplus->log_removewarning('optstablenotfound');
947 }
948
949 // Race detection - with zip files now being resumable, these can more easily occur, with two running side-by-side
950 $backup_final_file_name = $backup_file_base.'-db.gz';
951 $time_now = time();
952 $time_mod = (int)@filemtime($backup_final_file_name);
953 if (file_exists($backup_final_file_name) && $time_mod>100 && ($time_now-$time_mod)<30) {
954 $updraftplus->terminate_due_to_activity($backup_final_file_name, $time_now, $time_mod);
955 } elseif (file_exists($backup_final_file_name)) {
956 $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.");
957 }
958
959 // Finally, stitch the files together
960 $opendb = $this->backup_db_open($backup_final_file_name, true);
961 if (false === $opendb) return false;
962 $this->backup_db_header();
963
964 // 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
965 $unlink_files = array();
966
967 $sind = 1;
968 foreach ($stitch_files as $table_file) {
969 $updraftplus->log("{$table_file}.gz ($sind/$how_many_tables): adding to final database dump");
970 if (!$handle = gzopen($this->updraft_dir.'/'.$table_file.'.gz', "r")) {
971 $updraftplus->log("Error: Failed to open database file for reading: ${table_file}.gz");
972 $updraftplus->log(__("Failed to open database file for reading:", 'updraftplus').' '.$table_file.'.gz', 'error');
973 $errors++;
974 } else {
975 while ($line = gzgets($handle, 2048)) { $this->stow($line); }
976 gzclose($handle);
977 $unlink_files[] = $this->updraft_dir.'/'.$table_file.'.gz';
978 }
979 $sind++;
980 }
981
982 if (defined("DB_CHARSET")) {
983 $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");
984 }
985
986 $updraftplus->log($file_base.'-db.gz: finished writing out complete database file ('.round(filesize($backup_final_file_name)/1024,1).' Kb)');
987 if (!$this->close($this->dbhandle)) {
988 $updraftplus->log('An error occurred whilst closing the final database file');
989 $updraftplus->log(__('An error occurred whilst closing the final database file', 'updraftplus'), 'error');
990 $errors++;
991 }
992
993 foreach ($unlink_files as $unlink_file) @unlink($unlink_file);
994
995 if ($errors > 0) {
996 return false;
997 } else {
998 # We no longer encrypt here - because the operation can take long, we made it resumable and moved it to the upload loop
999 $updraftplus->jobdata_set('jobstatus', 'dbcreated');
1000 $sha = sha1_file($backup_final_file_name);
1001 $updraftplus->jobdata_set('sha1-db0', $sha);
1002 $updraftplus->log("Total database tables backed up: $total_tables (".basename($backup_final_file_name).": checksum (SHA1): $sha)");
1003 return basename($backup_file_base.'-db.gz');
1004 }
1005
1006 } //wp_db_backup
1007
1008 private function backup_table_bindump($potsql, $table_name, $where) {
1009
1010 $microtime = microtime(true);
1011
1012 global $updraftplus;
1013
1014 $pfile = md5(time().rand()).'.tmp';
1015 file_put_contents($this->updraft_dir.'/'.$pfile, "[mysqldump]\npassword=".DB_PASSWORD."\n");
1016
1017 if ($where) $where="--where='".escapeshellarg($where)."'";
1018
1019 $exec = "cd ".escapeshellarg($this->updraft_dir)."; $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(DB_USER)." --host=".escapeshellarg(DB_HOST)." ".DB_NAME." ".escapeshellarg($table_name);
1020
1021 $ret = false;
1022 $any_output = false;
1023 $writes = 0;
1024 $handle = popen($exec, "r");
1025 if ($handle) {
1026 while (!feof($handle)) {
1027 $w = fgets($handle);
1028 if ($w) {
1029 $this->stow($w);
1030 $writes++;
1031 $any_output = true;
1032 }
1033 }
1034 $ret = pclose($handle);
1035 if ($ret != 0) {
1036 $updraftplus->log("Binary mysqldump: error (code: $ret)");
1037 // Keep counter of failures? Change value of binsqldump?
1038 } else {
1039 if ($any_output) {
1040 $updraftplus->log("Table $table_name: binary mysqldump finished (writes: $writes) in ".sprintf("%.02f",max(microtime(true)-$microtime,0.00001))." seconds");
1041 $ret = true;
1042 }
1043 }
1044 } else {
1045 $updraftplus->log("Binary mysqldump error: bindump popen failed");
1046 }
1047
1048 # Clean temporary files
1049 @unlink($this->updraft_dir.'/'.$pfile);
1050
1051 return $ret;
1052
1053 }
1054
1055 /**
1056 * Taken partially from phpMyAdmin and partially from
1057 * Alain Wolf, Zurich - Switzerland
1058 * Website: http://restkultur.ch/personal/wolf/scripts/db_backup/
1059 * Modified by Scott Merrill (http://www.skippy.net/)
1060 * to use the WordPress $wpdb object
1061 * @param string $table
1062 * @param string $segment
1063 * @return void
1064 */
1065 private function backup_table($table, $where = '', $segment = 'none') {
1066 global $wpdb, $updraftplus;
1067
1068 $microtime = microtime(true);
1069 $total_rows = 0;
1070
1071 $table_structure = $wpdb->get_results("DESCRIBE $table");
1072 if (! $table_structure) {
1073 //$updraftplus->log(__('Error getting table details','wp-db-backup') . ": $table", 'error');
1074 return false;
1075 }
1076
1077 if($segment == 'none' || $segment == 0) {
1078 // Add SQL statement to drop existing table
1079 $this->stow("\n# " . sprintf(__('Delete any existing table %s','wp-db-backup'),$updraftplus->backquote($table)) . "\n\n");
1080 $this->stow("DROP TABLE IF EXISTS " . $updraftplus->backquote($table) . ";\n");
1081
1082 // Table structure
1083 // Comment in SQL-file
1084 $this->stow("\n# " . sprintf(__('Table structure of table %s','wp-db-backup'),$updraftplus->backquote($table)) . "\n\n");
1085
1086 $create_table = $wpdb->get_results("SHOW CREATE TABLE `$table`", ARRAY_N);
1087 if (false === $create_table) {
1088 $err_msg = sprintf(__('Error with SHOW CREATE TABLE for %s.','wp-db-backup'), $table);
1089 //$updraftplus->log($err_msg, 'error');
1090 $this->stow("#\n# $err_msg\n#\n");
1091 }
1092 $create_line = $updraftplus->str_lreplace('TYPE=', 'ENGINE=', $create_table[0][1]);
1093
1094 # Remove PAGE_CHECKSUM parameter from MyISAM - was internal, undocumented, later removed (so causes errors on import)
1095 if (preg_match('/ENGINE=([^\s;]+)/', $create_line, $eng_match)) {
1096 $engine = $eng_match[1];
1097 if ('myisam' == strtolower($engine)) {
1098 $create_line = preg_replace('/PAGE_CHECKSUM=\d\s?/', '', $create_line, 1);
1099 }
1100 }
1101
1102 $this->stow($create_line.' ;');
1103
1104 if (false === $table_structure) {
1105 $err_msg = sprintf('Error getting table structure of %s', $table);
1106 $this->stow("#\n# $err_msg\n#\n");
1107 }
1108
1109 // Comment in SQL-file
1110 $this->stow("\n\n# " . sprintf('Data contents of table %s',$updraftplus->backquote($table)) . "\n\n");
1111
1112 }
1113
1114 # Some tables have optional data, and should be skipped if they do not work
1115 $table_sans_prefix = substr($table, strlen($this->table_prefix_raw));
1116 $data_optional_tables = apply_filters('updraftplus_data_optional_tables', explode(',', UPDRAFTPLUS_DATA_OPTIONAL_TABLES));
1117 if (in_array($table_sans_prefix, $data_optional_tables)) {
1118 if (!$updraftplus->something_useful_happened && !empty($updraftplus->current_resumption) && ($updraftplus->current_resumption - $updraftplus->last_successful_resumption > 2)) {
1119 $updraftplus->log("Table $table: Data skipped (previous attempts failed, and table is marked as non-essential)");
1120 return true;
1121 }
1122 }
1123
1124 // In UpdraftPlus, segment is always 'none'
1125 if($segment == 'none' || $segment >= 0) {
1126 $defs = array();
1127 $integer_fields = array();
1128 // $table_structure was from "DESCRIBE $table"
1129 foreach ($table_structure as $struct) {
1130 if ( (0 === strpos($struct->Type, 'tinyint')) || (0 === strpos(strtolower($struct->Type), 'smallint')) ||
1131 (0 === strpos(strtolower($struct->Type), 'mediumint')) || (0 === strpos(strtolower($struct->Type), 'int')) || (0 === strpos(strtolower($struct->Type), 'bigint')) ) {
1132 $defs[strtolower($struct->Field)] = ( null === $struct->Default ) ? 'NULL' : $struct->Default;
1133 $integer_fields[strtolower($struct->Field)] = "1";
1134 }
1135 }
1136
1137 // 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%)
1138 if($segment == 'none') {
1139 $row_start = 0;
1140 $row_inc = 1000;
1141 } else {
1142 $row_start = $segment * 1000;
1143 $row_inc = 1000;
1144 }
1145
1146 $search = array("\x00", "\x0a", "\x0d", "\x1a");
1147 $replace = array('\0', '\n', '\r', '\Z');
1148
1149 if ($where) $where = "WHERE $where";
1150
1151 do {
1152 @set_time_limit(900);
1153
1154 $table_data = $wpdb->get_results("SELECT * FROM $table $where LIMIT {$row_start}, {$row_inc}", ARRAY_A);
1155 $entries = 'INSERT INTO ' . $updraftplus->backquote($table) . ' VALUES ';
1156 // \x08\\x09, not required
1157 if($table_data) {
1158 $thisentry = "";
1159 foreach ($table_data as $row) {
1160 $total_rows++;
1161 $values = array();
1162 foreach ($row as $key => $value) {
1163 if (isset($integer_fields[strtolower($key)])) {
1164 // make sure there are no blank spots in the insert syntax,
1165 // yet try to avoid quotation marks around integers
1166 $value = ( null === $value || '' === $value) ? $defs[strtolower($key)] : $value;
1167 $values[] = ( '' === $value ) ? "''" : $value;
1168 } else {
1169 $values[] = (null === $value) ? 'NULL' : "'" . str_replace($search, $replace, str_replace('\'', '\\\'', str_replace('\\', '\\\\', $value))) . "'";
1170 }
1171 }
1172 if ($thisentry) $thisentry .= ",\n ";
1173 $thisentry .= '('.implode(', ', $values).')';
1174 // Flush every 512Kb
1175 if (strlen($thisentry) > 524288) {
1176 $this->stow(" \n".$entries.$thisentry.';');
1177 $thisentry = "";
1178 }
1179
1180 }
1181 if ($thisentry) $this->stow(" \n".$entries.$thisentry.';');
1182 $row_start += $row_inc;
1183 }
1184 } while(count($table_data) > 0 && 'none' == $segment);
1185 }
1186
1187 if(($segment == 'none') || ($segment < 0)) {
1188 // Create footer/closing comment in SQL-file
1189 $this->stow("\n");
1190 $this->stow("# " . sprintf(__('End of data contents of table %s','wp-db-backup'),$updraftplus->backquote($table)) . "\n");
1191 $this->stow("\n");
1192 }
1193 $updraftplus->log("Table $table: Total rows added: $total_rows in ".sprintf("%.02f",max(microtime(true)-$microtime,0.00001))." seconds");
1194
1195 } // end backup_table()
1196
1197
1198 /*END OF WP-DB-BACKUP BLOCK */
1199
1200 // Encrypts the file if the option is set; returns the basename of the file (according to whether it was encrypted or nto)
1201 public function encrypt_file($file) {
1202 global $updraftplus;
1203 $encryption = UpdraftPlus_Options::get_updraft_option('updraft_encryptionphrase');
1204 if (strlen($encryption) > 0) {
1205 $updraftplus->log("$file: applying encryption");
1206 $updraftplus->jobdata_set('jobstatus', 'dbencrypting');
1207 $encryption_error = 0;
1208 $microstart = microtime(true);
1209 $file_size = @filesize($this->updraft_dir.'/'.$file)/1024;
1210
1211 if (false === file_put_contents($this->updraft_dir.'/'.$file.'.crypt' , $updraftplus->encrypt($this->updraft_dir.'/'.$file, $encryption))) $encryption_error = 1;
1212 if (0 == $encryption_error) {
1213 $time_taken = max(0.000001, microtime(true)-$microstart);
1214
1215 $sha = sha1_file($this->updraft_dir.'/'.$file.'.crypt');
1216 $updraftplus->jobdata_set('sha1-db0.crypt', $sha);
1217
1218 $updraftplus->log("$file: encryption successful: ".round($file_size,1)."Kb in ".round($time_taken,2)."s (".round($file_size/$time_taken, 1)."Kb/s) (SHA1 checksum: $sha)");
1219 # Delete unencrypted file
1220 @unlink($this->updraft_dir.'/'.$file);
1221 $updraftplus->jobdata_set('jobstatus', 'dbencrypted');
1222 return basename($file.'.crypt');
1223 } else {
1224 $updraftplus->log("Encryption error occurred when encrypting database. Encryption aborted.");
1225 $updraftplus->log(__("Encryption error occurred when encrypting database. Encryption aborted.",'updraftplus'), 'error');
1226 return basename($file);
1227 }
1228 } else {
1229 return basename($file);
1230 }
1231 }
1232
1233 private function close($handle) {
1234 if ($this->dbhandle_isgz) {
1235 return gzclose($handle);
1236 } else {
1237 return fclose($handle);
1238 }
1239 }
1240
1241 // Open a file, store its filehandle
1242 private function backup_db_open($file, $allow_gz = true) {
1243 if (function_exists('gzopen') && $allow_gz == true) {
1244 $this->dbhandle = @gzopen($file, 'w');
1245 $this->dbhandle_isgz = true;
1246 } else {
1247 $this->dbhandle = @fopen($file, 'w');
1248 $this->dbhandle_isgz = false;
1249 }
1250 if(false === $this->dbhandle) {
1251 global $updraftplus;
1252 $updraftplus->log("ERROR: $file: Could not open the backup file for writing");
1253 $updraftplus->log($file.": ".__("Could not open the backup file for writing",'updraftplus'), 'error');
1254 }
1255 return $this->dbhandle;
1256 }
1257
1258 private function stow($query_line) {
1259 if ($this->dbhandle_isgz) {
1260 if(! @gzwrite($this->dbhandle, $query_line)) {
1261 //$updraftplus->log(__('There was an error writing a line to the backup script:','wp-db-backup') . ' ' . $query_line . ' ' . $php_errormsg, 'error');
1262 }
1263 } else {
1264 if(false === @fwrite($this->dbhandle, $query_line)) {
1265 //$updraftplus->log(__('There was an error writing a line to the backup script:','wp-db-backup') . ' ' . $query_line . ' ' . $php_errormsg, 'error');
1266 }
1267 }
1268 }
1269
1270 private function backup_db_header() {
1271
1272 @include(ABSPATH.'wp-includes/version.php');
1273 global $wp_version, $updraftplus;
1274
1275 // Will need updating when WP stops being just plain MySQL
1276 $mysql_version = (function_exists('mysql_get_server_info')) ? @mysql_get_server_info() : '?';
1277
1278 $this->stow("# WordPress MySQL database backup\n");
1279 $this->stow("# Created by UpdraftPlus version ".$updraftplus->version." (http://updraftplus.com)\n");
1280 $this->stow("# WordPress Version: $wp_version, running on PHP ".phpversion()." (".$_SERVER["SERVER_SOFTWARE"]."), MySQL $mysql_version\n");
1281 $this->stow("# Backup of: ".untrailingslashit(site_url())."\n");
1282 $this->stow("# Home URL: ".untrailingslashit(home_url())."\n");
1283 $this->stow("# Content URL: ".untrailingslashit(content_url())."\n");
1284 $this->stow("# Table prefix: ".$this->table_prefix_raw."\n");
1285 $this->stow("# Filtered table prefix: ".$this->table_prefix."\n");
1286 $this->stow("# Site info: multisite=".(is_multisite() ? '1' : '0')."\n");
1287 $this->stow("# Site info: end\n");
1288
1289 $this->stow("#\n");
1290 $this->stow("# " . sprintf(__('Generated: %s','wp-db-backup'),date("l j. F Y H:i T")) . "\n");
1291 $this->stow("# " . sprintf(__('Hostname: %s','wp-db-backup'),DB_HOST) . "\n");
1292 $this->stow("# " . sprintf(__('Database: %s','wp-db-backup'),$updraftplus->backquote(DB_NAME)) . "\n");
1293 $this->stow("# --------------------------------------------------------\n");
1294
1295 if (defined("DB_CHARSET")) {
1296 $this->stow("/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;\n");
1297 $this->stow("/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;\n");
1298 $this->stow("/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;\n");
1299 $this->stow("/*!40101 SET NAMES " . DB_CHARSET . " */;\n");
1300 }
1301 $this->stow("/*!40101 SET foreign_key_checks = 0 */;\n\n");
1302 }
1303
1304 public function phpmailer_init($phpmailer) {
1305 global $updraftplus;
1306 if (empty($this->attachments) || !is_array($this->attachments)) return;
1307 foreach ($this->attachments as $attach) {
1308 $mime_type = (preg_match('/\.gz$/', $attach)) ? 'application/x-gzip' : 'text/plain';
1309 $phpmailer->AddAttachment($attach, '', 'base64', $mime_type);
1310 }
1311 }
1312
1313 // This function recursively packs the zip, dereferencing symlinks but packing into a single-parent tree for universal unpacking
1314 // $exclude is passed by reference so that we can remove elements as they are matched - saves time checking against already-dealt-with objects
1315 private function makezip_recursive_add($fullpath, $use_path_when_storing, $original_fullpath, $startlevels = 1, &$exclude) {
1316
1317 $zipfile = $this->zip_basename.(($this->index == 0) ? '' : ($this->index+1)).'.zip.tmp';
1318
1319 global $updraftplus;
1320
1321 // 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
1322 $fullpath = realpath($fullpath);
1323 $original_fullpath = realpath($original_fullpath);
1324
1325 // Is the place we've ended up above the original base? That leads to infinite recursion
1326 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)))) {
1327 $updraftplus->log("Infinite recursion: symlink lead us to $fullpath, which is within $original_fullpath");
1328 $updraftplus->log(__("Infinite recursion: consult your log for more information",'updraftplus'), 'error');
1329 return false;
1330 }
1331
1332 # This is sufficient for the ones we have exclude options for - uploads, others, wpcore
1333 $stripped_storage_path = (1 == $startlevels) ? $use_path_when_storing : substr($use_path_when_storing, strpos($use_path_when_storing, '/') + 1);
1334 if (false !== ($fkey = array_search($stripped_storage_path, $exclude))) {
1335 $updraftplus->log("Entity excluded by configuration option: $stripped_storage_path");
1336 unset($exclude[$fkey]);
1337 return true;
1338 }
1339
1340 if(is_file($fullpath)) {
1341 if (is_readable($fullpath)) {
1342 $key = ($fullpath == $original_fullpath) ? ((2 == $startlevels) ? $use_path_when_storing : basename($fullpath)) : $use_path_when_storing.'/'.basename($fullpath);
1343 $this->zipfiles_batched[$fullpath] = $key;
1344 $this->makezip_recursive_batchedbytes += @filesize($fullpath);
1345 #@touch($zipfile);
1346 } else {
1347 $updraftplus->log("$fullpath: unreadable file");
1348 $updraftplus->log(sprintf(__("%s: unreadable file - could not be backed up (check the file permissions)", 'updraftplus'), $fullpath), 'warning');
1349 }
1350 } elseif (is_dir($fullpath)) {
1351 if (!isset($this->existing_files[$use_path_when_storing])) $this->zipfiles_dirbatched[] = $use_path_when_storing;
1352 if (!$dir_handle = @opendir($fullpath)) {
1353 $updraftplus->log("Failed to open directory: $fullpath");
1354 $updraftplus->log(sprintf(__("Failed to open directory (check the file permissions): %s",'updraftplus'), $fullpath), 'error');
1355 return false;
1356 }
1357 while (false !== ($e = readdir($dir_handle))) {
1358 if ($e != '.' && $e != '..') {
1359 if (is_link($fullpath.'/'.$e)) {
1360 $deref = realpath($fullpath.'/'.$e);
1361 if (is_file($deref)) {
1362 if (is_readable($deref)) {
1363 $use_stripped = $stripped_storage_path.'/'.$e;
1364 if (false !== ($fkey = array_search($use_stripped, $exclude))) {
1365 $updraftplus->log("Entity excluded by configuration option: $use_stripped");
1366 unset($exclude[$fkey]);
1367 } else {
1368 $this->zipfiles_batched[$deref] = $use_path_when_storing.'/'.$e;
1369 $this->makezip_recursive_batchedbytes += @filesize($deref);
1370 #@touch($zipfile);
1371 }
1372 } else {
1373 $updraftplus->log("$deref: unreadable file");
1374 $updraftplus->log(sprintf(__("%s: unreadable file - could not be backed up"), $deref), 'warning');
1375 }
1376 } elseif (is_dir($deref)) {
1377 $this->makezip_recursive_add($deref, $use_path_when_storing.'/'.$e, $original_fullpath, $startlevels, $exclude);
1378 }
1379 } elseif (is_file($fullpath.'/'.$e)) {
1380 if (is_readable($fullpath.'/'.$e)) {
1381 $use_stripped = $stripped_storage_path.'/'.$e;
1382 if (false !== ($fkey = array_search($use_stripped, $exclude))) {
1383 $updraftplus->log("Entity excluded by configuration option: $use_stripped");
1384 unset($exclude[$fkey]);
1385 } else {
1386 $this->zipfiles_batched[$fullpath.'/'.$e] = $use_path_when_storing.'/'.$e;
1387 $this->makezip_recursive_batchedbytes += @filesize($fullpath.'/'.$e);
1388 #@touch($zipfile);
1389 }
1390 } else {
1391 $updraftplus->log("$fullpath/$e: unreadable file");
1392 $updraftplus->log(sprintf(__("%s: unreadable file - could not be backed up", 'updraftplus'), $use_path_when_storing.'/'.$e), 'warning', "unrfile-$e");
1393 }
1394 } elseif (is_dir($fullpath.'/'.$e)) {
1395 // no need to addEmptyDir here, as it gets done when we recurse
1396 $this->makezip_recursive_add($fullpath.'/'.$e, $use_path_when_storing.'/'.$e, $original_fullpath, $startlevels, $exclude);
1397 }
1398 }
1399 }
1400 closedir($dir_handle);
1401 } else {
1402 $updraftplus->log("Unexpected: path ($use_path_when_storing) fails both is_file() and is_dir()");
1403 }
1404
1405 // We don't want to tweak the zip file on every single file, so we batch them up
1406 // We go every 25 files, because if you wait too much longer, the contents may have changed from under you. Note though that since this fires once-per-directory, the actual number by this stage may be much larger; the most we saw was over 3000; but in that case, makezip_addfiles() will split the write-out up into smaller chunks
1407 // And for some redundancy (redundant because of the touches going on anyway), we try to touch the file after 20 seconds, to help with the "recently modified" check on resumption (we saw a case where the file went for 155 seconds without being touched and so the other runner was not detected)
1408 if (count($this->zipfiles_batched) > 25 || (file_exists($zipfile) && ((time()-filemtime($zipfile)) > 20) )) {
1409 $ret = true;
1410 # In fact, this is entirely redundant, and slows things down - the logic in makezip_addfiles() now does this, much better
1411 # If adding this back in, then be careful - we now assume that makezip_recursive_add() does *not* touch the zipfile
1412 // $ret = $this->makezip_addfiles();
1413 } else {
1414 $ret = true;
1415 }
1416
1417 return $ret;
1418
1419 }
1420
1421 // Caution: $source is allowed to be an array, not just a filename
1422 // $destination is the temporary file (ending in .tmp)
1423 private function make_zipfile($source, $backup_file_basename, $whichone = '') {
1424
1425 global $updraftplus;
1426
1427 $original_index = $this->index;
1428
1429 $itext = (empty($this->index)) ? '' : ($this->index+1);
1430 $destination_base = $backup_file_basename.'-'.$whichone.$itext.'.zip.tmp';
1431 $destination = $this->updraft_dir.'/'.$destination_base;
1432
1433 // Legacy/redundant
1434 if (empty($whichone) && is_string($whichone)) $whichone = basename($source);
1435
1436 // When to prefer PCL:
1437 // - We were asked to
1438 // - No zip extension present and no relevant method present
1439 // The zip extension check is not redundant, because method_exists segfaults some PHP installs, leading to support requests
1440
1441 // We need meta-info about $whichone
1442 $backupable_entities = $updraftplus->get_backupable_file_entities(true, false);
1443 # This is only used by one corner-case in BinZip
1444 #$this->make_zipfile_source = (isset($backupable_entities[$whichone])) ? $backupable_entities[$whichone] : $source;
1445 $this->make_zipfile_source = (is_array($source) && isset($backupable_entities[$whichone])) ? (('uploads' == $whichone) ? dirname($backupable_entities[$whichone]) : $backupable_entities[$whichone]) : dirname($source);
1446
1447 $this->existing_files = array();
1448 # Used for tracking compression ratios
1449 $this->existing_files_rawsize = 0;
1450 $this->existing_zipfiles_size = 0;
1451
1452 // Enumerate existing files
1453 for ($j=0; $j<=$this->index; $j++) {
1454 $jtext = ($j == 0) ? '' : ($j+1);
1455 $examine_zip = $this->updraft_dir.'/'.$backup_file_basename.'-'.$whichone.$jtext.'.zip'.(($j == $this->index) ? '.tmp' : '');
1456
1457 // If the file exists, then we should grab its index of files inside, and sizes
1458 // Then, when we come to write a file, we should check if it's already there, and only add if it is not
1459 if (file_exists($examine_zip) && is_readable($examine_zip) && filesize($examine_zip)>0) {
1460
1461 $this->existing_zipfiles_size += filesize($examine_zip);
1462 $zip = new $this->use_zip_object;
1463 if (!$zip->open($examine_zip)) {
1464 $updraftplus->log("Could not open zip file to examine (".$zip->last_error."); will remove: ".basename($examine_zip));
1465 @unlink($examine_zip);
1466 } else {
1467
1468 # Don't put this in the for loop, or the magic __get() method gets called and opens the zip file every time the loop goes round
1469 $numfiles = $zip->numFiles;
1470
1471 for ($i=0; $i < $numfiles; $i++) {
1472 $si = $zip->statIndex($i);
1473 $name = $si['name'];
1474 $this->existing_files[$name] = $si['size'];
1475 $this->existing_files_rawsize += $si['size'];
1476 }
1477
1478 @$zip->close();
1479 }
1480
1481 $updraftplus->log(basename($examine_zip).": Zip file already exists, with ".count($this->existing_files)." files");
1482
1483 # try_split is set if there have been no check-ins recently
1484 if ($j == $this->index && isset($this->try_split)) {
1485 if (filesize($examine_zip) > 50*1048576) {
1486 # We could, as a future enhancement, save this back to the job data, if we see a case that needs it
1487 $this->zip_split_every = max((int)$this->zip_split_every/2, UPDRAFTPLUS_SPLIT_MIN*1048576, filesize($examine_zip));
1488 $updraftplus->log("No check-in on last two runs; bumping index and reducing zip split for this job to: ".round($this->zip_split_every/1048576, 1)." Mb");
1489 $do_bump_index = true;
1490 }
1491 unset($this->try_split);
1492 }
1493
1494 } elseif (file_exists($examine_zip)) {
1495 $updraftplus->log("Zip file already exists, but is not readable or was zero-sized; will remove: ".basename($examine_zip));
1496 @unlink($examine_zip);
1497 }
1498 }
1499
1500 $this->zip_last_ratio = ($this->existing_files_rawsize > 0) ? ($this->existing_zipfiles_size/$this->existing_files_rawsize) : 1;
1501
1502 $this->zipfiles_added = 0;
1503 $this->zipfiles_added_thisrun = 0;
1504 $this->zipfiles_dirbatched = array();
1505 $this->zipfiles_batched = array();
1506 $this->zipfiles_lastwritetime = time();
1507
1508 $this->zip_basename = $this->updraft_dir.'/'.$backup_file_basename.'-'.$whichone;
1509
1510 if (!empty($do_bump_index)) {
1511 $this->bump_index();
1512 }
1513
1514 $error_occurred = false;
1515
1516 # Store this in its original form
1517 $this->source = $source;
1518
1519 # Reset. This counter is used only with PcLZip, to decide if it's better to do it all-in-one
1520 $this->makezip_recursive_batchedbytes = 0;
1521 if (!is_array($source)) $source=array($source);
1522
1523 $exclude = $updraftplus->get_exclude($whichone);
1524 foreach ($source as $element) {
1525 #makezip_recursive_add($fullpath, $use_path_when_storing, $original_fullpath, $startlevels = 1, $exclude_array)
1526 if ('uploads' == $whichone) {
1527 $dirname = dirname($element);
1528 $add_them = $this->makezip_recursive_add($element, basename($dirname).'/'.basename($element), $element, 2, $exclude);
1529 } else {
1530 $add_them = $this->makezip_recursive_add($element, basename($element), $element, 1, $exclude);
1531 }
1532 if (is_wp_error($add_them) || false === $add_them) $error_occurred = true;
1533 }
1534
1535 // 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.
1536 // 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.
1537 $updraftplus->check_recent_modification($destination);
1538 // 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')
1539 if (empty($do_bump_index)) @touch($destination);
1540
1541 if (count($this->zipfiles_dirbatched)>0 || count($this->zipfiles_batched)>0) {
1542 $updraftplus->log(sprintf("Total entities for the zip file: %d directories, %d files, %s Mb", count($this->zipfiles_dirbatched), count($this->zipfiles_batched), round($this->makezip_recursive_batchedbytes/1048576,1)));
1543 $add_them = $this->makezip_addfiles();
1544 if (is_wp_error($add_them)) {
1545 foreach ($add_them->get_error_messages() as $msg) {
1546 $updraftplus->log("Error returned from makezip_addfiles: ".$msg);
1547 }
1548 $error_occurred = true;
1549 } elseif (false === $add_them) {
1550 $updraftplus->log("Error: makezip_addfiles returned false");
1551 $error_occurred = true;
1552 }
1553 }
1554
1555 # Reset these variables because the index may have changed since we began
1556
1557 $itext = (empty($this->index)) ? '' : ($this->index+1);
1558 $destination_base = $backup_file_basename.'-'.$whichone.$itext.'.zip.tmp';
1559 $destination = $this->updraft_dir.'/'.$destination_base;
1560
1561 if ($this->zipfiles_added > 0 || $error_occurred == false) {
1562 // ZipArchive::addFile sometimes fails
1563 if ((file_exists($destination) || $this->index == $original_index) && @filesize($destination) < 90 && 'UpdraftPlus_ZipArchive' == $this->use_zip_object) {
1564 $updraftplus->log("makezip_addfiles(ZipArchive) apparently failed (file=".basename($destination).", type=$whichone, size=".filesize($destination).") - retrying with PclZip");
1565 $this->use_zip_object = 'UpdraftPlus_PclZip';
1566 return $this->make_zipfile($source, $backup_file_basename, $whichone);
1567 }
1568 return true;
1569 } else {
1570 # If ZipArchive, and if an error occurred, and if apparently ZipArchive did nothing, then immediately retry with PclZip. Q. Why this specific criteria? A. Because we've seen it in the wild, and it's quicker to try PcLZip now than waiting until resumption 9 when the automatic switchover happens.
1571 if ($error_occurred != false && (file_exists($destination) || $this->index == $original_index) && @filesize($destination) < 90 && 'UpdraftPlus_ZipArchive' == $this->use_zip_object) {
1572 $updraftplus->log("makezip_addfiles(ZipArchive) apparently failed (file=".basename($destination).", type=$whichone, size=".filesize($destination).") - retrying with PclZip");
1573 $this->use_zip_object = 'UpdraftPlus_PclZip';
1574 return $this->make_zipfile($source, $backup_file_basename, $whichone);
1575 }
1576 $updraftplus->log("makezip failure: zipfiles_added=".$this->zipfiles_added.", error_occurred=".$error_occurred." (method=".$this->use_zip_object.")");
1577 return false;
1578 }
1579
1580 }
1581
1582 // Q. Why don't we only open and close the zip file just once?
1583 // 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)
1584
1585 // We batch up the files, rather than do them one at a time. So we are more efficient than open,one-write,close.
1586 private function makezip_addfiles() {
1587
1588 global $updraftplus;
1589
1590 # Used to detect requests to bump the size
1591 $bump_index = false;
1592
1593 $zipfile = $this->zip_basename.(($this->index == 0) ? '' : ($this->index+1)).'.zip.tmp';
1594
1595 $maxzipbatch = $updraftplus->jobdata_get('maxzipbatch', 26214400);
1596 if ((int)$maxzipbatch < 1) $maxzipbatch = 26214400;
1597
1598 // Short-circuit the null case, because we want to detect later if something useful happenned
1599 if (count($this->zipfiles_dirbatched) == 0 && count($this->zipfiles_batched) == 0) return true;
1600
1601 # 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)
1602 # This assumes that makezip_addfiles() is only called once so that we know about all needed files (the new style)
1603 # This is rather conservative - because it assumes zero compression. But we can't know that in advance.
1604 if (0 == $this->index && 'UpdraftPlus_PclZip' == $this->use_zip_object && $this->makezip_recursive_batchedbytes < $this->zip_split_every && ($this->makezip_recursive_batchedbytes < 512*1024*1024 || (defined('UPDRAFTPLUS_PCLZIP_FORCEALLINONE') && UPDRAFTPLUS_PCLZIP_FORCEALLINONE == true))) {
1605 $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)");
1606 if(!class_exists('PclZip')) require_once(ABSPATH.'/wp-admin/includes/class-pclzip.php');
1607 $zip = new PclZip($zipfile);
1608 $remove_path = ($this->whichone == 'wpcore') ? untrailingslashit(ABSPATH) : WP_CONTENT_DIR;
1609 $add_path = false;
1610 // Remove prefixes
1611 $backupable_entities = $updraftplus->get_backupable_file_entities(true);
1612 if (isset($backupable_entities[$this->whichone])) {
1613 if ('plugins' == $this->whichone || 'themes' == $this->whichone || 'uploads' == $this->whichone) {
1614 $remove_path = dirname($backupable_entities[$this->whichone]);
1615 # 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.
1616 #$add_path = $this->whichone;
1617 } else {
1618 $remove_path = $backupable_entities[$this->whichone];
1619 }
1620 }
1621 if ($add_path) {
1622 $zipcode = $zip->create($this->source, PCLZIP_OPT_REMOVE_PATH, $remove_path, PCLZIP_OPT_ADD_PATH, $add_path);
1623 } else {
1624 $zipcode = $zip->create($this->source, PCLZIP_OPT_REMOVE_PATH, $remove_path);
1625 }
1626 if ($zipcode == 0 ) {
1627 $updraftplus->log("PclZip Error: ".$zip->errorInfo(true), 'warning');
1628 return $zip->errorCode();
1629 } else {
1630 return true;
1631 }
1632 }
1633
1634 // 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!
1635
1636 $data_added_since_reopen = 0;
1637 # 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)
1638 $files_zipadded_since_open = array();
1639
1640 $zip = new $this->use_zip_object;
1641 if (file_exists($zipfile)) {
1642 $opencode = $zip->open($zipfile);
1643 $original_size = filesize($zipfile);
1644 clearstatcache();
1645 } else {
1646 $create_code = (defined('ZIPARCHIVE::CREATE')) ? ZIPARCHIVE::CREATE : 1;
1647 $opencode = $zip->open($zipfile, $create_code);
1648 $original_size = 0;
1649 }
1650
1651 if ($opencode !== true) return new WP_Error('no_open', sprintf(__('Failed to open the zip file (%s) - %s', 'updraftplus'),$zipfile, $zip->last_error));
1652 // Make sure all directories are created before we start creating files
1653 while ($dir = array_pop($this->zipfiles_dirbatched)) {
1654 $zip->addEmptyDir($dir);
1655 }
1656
1657 $zipfiles_added_thisbatch = 0;
1658
1659 // Go through all those batched files
1660 foreach ($this->zipfiles_batched as $file => $add_as) {
1661
1662 $fsize = filesize($file);
1663
1664 if ($fsize > UPDRAFTPLUS_WARN_FILE_SIZE) {
1665 $updraftplus->log(sprintf(__('A very large file was encountered: %s (size: %s Mb)', 'updraftplus'), $add_as, round($fsize/1048576, 1)), 'warning');
1666 }
1667
1668 // Skips files that are already added
1669 if (!isset($this->existing_files[$add_as]) || $this->existing_files[$add_as] != $fsize) {
1670
1671 @touch($zipfile);
1672 $zip->addFile($file, $add_as);
1673 $zipfiles_added_thisbatch++;
1674 $this->zipfiles_added_thisrun++;
1675 $files_zipadded_since_open[] = array('file' => $file, 'addas' => $add_as);
1676
1677 $data_added_since_reopen += $fsize;
1678 /* Conditions for forcing a write-out and re-open:
1679 - more than $maxzipbatch bytes have been batched
1680 - more than 1.5 seconds have passed since the last time we wrote
1681 - 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)
1682 - more than 500 files batched (should perhaps intelligently lower this as the zip file gets bigger - not yet needed)
1683 */
1684
1685 # 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)
1686 # 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
1687 $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;
1688
1689 if ($zipfiles_added_thisbatch > 500 || $reaching_split_limit || $data_added_since_reopen > $maxzipbatch || (time() - $this->zipfiles_lastwritetime) > 1.5) {
1690
1691 $something_useful_sizetest = false;
1692
1693 if ($data_added_since_reopen > $maxzipbatch) {
1694 $something_useful_sizetest = true;
1695 $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)');
1696 } elseif ($zipfiles_added_thisbatch >500) {
1697 $updraftplus->log("Adding batch to zip file (".$this->use_zip_object."): over 500 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)');
1698 } elseif (!$reaching_split_limit) {
1699 $updraftplus->log("Adding batch to zip file (".$this->use_zip_object."): over 1.5 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)');
1700 } else {
1701 $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)');
1702 }
1703 if (!$zip->close()) {
1704 $updraftplus->log(__('A zip error occurred - check your log for more details.', 'updraftplus'), 'warning', 'zipcloseerror');
1705 $updraftplus->log("The attempt to close the zip file returned an error (".$zip->last_error."). List of files we were trying to add follows (check their permissions).");
1706 foreach ($files_zipadded_since_open as $ffile) {
1707 $updraftplus->log("File: ".$ffile['addas']." (exists: ".(int)@file_exists($ffile['file']).", is_readable: ".(int)@is_readable($ffile['file'])." size: ".@filesize($ffile['file']).')');
1708 }
1709 }
1710 $zipfiles_added_thisbatch = 0;
1711
1712 # This triggers a re-open, later
1713 unset($zip);
1714 $files_zipadded_since_open = array();
1715 // Call here, in case we've got so many big files that we don't complete the whole routine
1716 if (filesize($zipfile) > $original_size) {
1717
1718 # 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 >1.5s 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
1719 $this->zip_last_ratio = ($data_added_since_reopen > 0) ? min((filesize($zipfile) - $original_size)/$data_added_since_reopen, 1) : 1;
1720
1721 # We need a rolling update of this
1722 $original_size = filesize($zipfile);
1723
1724 # Move on to next zip?
1725 if ($reaching_split_limit || filesize($zipfile) > $this->zip_split_every) {
1726 $bump_index = true;
1727 # Take the filesize now because later we wanted to know we did clearstatcache()
1728 $bumped_at = round(filesize($zipfile)/1048576, 1);
1729 }
1730
1731 # Need to make sure that something_useful_happened() is always called
1732
1733 # 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.
1734 if (!$something_useful_sizetest) {
1735 $updraftplus->something_useful_happened();
1736 } else {
1737
1738 // Do this as early as possible
1739 $updraftplus->something_useful_happened();
1740
1741 $time_since_began = max(microtime(true)- $this->zipfiles_lastwritetime, 0.000001);
1742 $normalised_time_since_began = $time_since_began*($maxzipbatch/$data_added_since_reopen);
1743
1744 // Don't measure speed until after ZipArchive::close()
1745 $rate = round($data_added_since_reopen/$time_since_began, 1);
1746
1747 $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)));
1748
1749 // 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.
1750
1751 /* "Could have done more" - detect as:
1752 - A batch operation would still leave a "good chunk" of time in a run
1753 - "Good chunk" means that the time we took to add the batch is less than 50% of a run time
1754 - We can do that on any run after the first (when at least one ceiling on the maximum time is known)
1755 - 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.
1756 */
1757
1758 // 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
1759
1760 // Gather the data. We try not to do this unless necessary (may be time-sensitive)
1761 if ($updraftplus->current_resumption >= 1) {
1762 $time_passed = $updraftplus->jobdata_get('run_times');
1763 if (!is_array($time_passed)) $time_passed = array();
1764 list($max_time, $timings_string, $run_times_known) = $updraftplus->max_time_passed($time_passed, $updraftplus->current_resumption-1);
1765 } else {
1766 $run_times_known = 0;
1767 $max_time = -1;
1768 }
1769
1770 if ($normalised_time_since_began<6 || ($updraftplus->current_resumption >=1 && $run_times_known >=1 && $time_since_began < 0.6*$max_time )) {
1771
1772 // How much can we increase it by?
1773 if ($normalised_time_since_began <6) {
1774 if ($run_times_known > 0 && $max_time >0) {
1775 $new_maxzipbatch = min(floor(max(
1776 $maxzipbatch*6/$normalised_time_since_began, $maxzipbatch*((0.6*$max_time)/$normalised_time_since_began))),
1777 200*1024*1024
1778 );
1779 } else {
1780 # Maximum of 200Mb in a batch
1781 $new_maxzipbatch = min( floor($maxzipbatch*6/$normalised_time_since_began),
1782 200*1024*1024
1783 );
1784 }
1785 } else {
1786 // Use up to 60% of available time
1787 $new_maxzipbatch = min(
1788 floor($maxzipbatch*((0.6*$max_time)/$normalised_time_since_began)),
1789 200*1024*1024
1790 );
1791 }
1792
1793 # Throttle increases - don't increase by more than 2x in one go - ???
1794 # $new_maxzipbatch = floor(min(2*$maxzipbatch, $new_maxzipbatch));
1795 # 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
1796 # $new_maxzipbatch = floor(min(18*$rate ,$new_maxzipbatch));
1797
1798 # Don't go above the split amount (though we expect that to be higher anyway, unless sending via email)
1799 $new_maxzipbatch = min($new_maxzipbatch, $this->zip_split_every);
1800
1801 # Don't raise it above a level that failed on a previous run
1802 $maxzipbatch_ceiling = $updraftplus->jobdata_get('maxzipbatch_ceiling');
1803 if (is_numeric($maxzipbatch_ceiling) && $maxzipbatch_ceiling > 20*1024*1024 && $new_maxzipbatch > $maxzipbatch_ceiling) {
1804 $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");
1805 $new_maxzipbatch = $maxzipbatch_ceiling;
1806 }
1807
1808 // Final sanity check
1809 if ($new_maxzipbatch > 1024*1024) $updraftplus->jobdata_set("maxzipbatch", $new_maxzipbatch);
1810
1811 if ($new_maxzipbatch <= 1024*1024) {
1812 $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)");
1813 } elseif ($new_maxzipbatch > $maxzipbatch) {
1814 $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)");
1815 } elseif ($new_maxzipbatch < $maxzipbatch) {
1816 // Ironically, we thought we were speedy...
1817 $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)");
1818 } else {
1819 $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)");
1820 }
1821
1822 if ($new_maxzipbatch > 1024*1024) $maxzipbatch = $new_maxzipbatch;
1823 }
1824
1825 // Detect excessive slowness
1826 // 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)
1827
1828 // 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).
1829
1830 if (!$updraftplus->something_useful_happened && $updraftplus->current_resumption >= 7) {
1831
1832 $updraftplus->something_useful_happened();
1833
1834 if ($run_times_known >= 5 && ($time_since_began > 0.8 * $max_time || $time_since_began + 7 > $max_time)) {
1835
1836 $new_maxzipbatch = max(floor($maxzipbatch*0.8), 20971520);
1837 if ($new_maxzipbatch < $maxzipbatch) {
1838 $maxzipbatch = $new_maxzipbatch;
1839 $updraftplus->jobdata_set("maxzipbatch", $new_maxzipbatch);
1840 $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)");
1841 } else {
1842 $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)");
1843 }
1844 }
1845
1846 } else {
1847 $updraftplus->something_useful_happened();
1848 }
1849 }
1850 $data_added_since_reopen = 0;
1851 } else {
1852 # ZipArchive::close() can take a very long time, which we want to know about
1853 $updraftplus->record_still_alive();
1854 }
1855
1856 clearstatcache();
1857 $this->zipfiles_lastwritetime = time();
1858 }
1859 } elseif (0 == $this->zipfiles_added_thisrun) {
1860 // Update lastwritetime, because otherwise the 1.5-second-activity detection can fire prematurely (e.g. if it takes >1.5 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.
1861 // Testing shows that calling time() 1000 times takes negligible time
1862 $this->zipfiles_lastwritetime=time();
1863 }
1864 $this->zipfiles_added++;
1865 // Don't call something_useful_happened() here - nothing necessarily happens until close() is called
1866 if ($this->zipfiles_added % 100 == 0) $updraftplus->log("Zip: ".basename($zipfile).": ".$this->zipfiles_added." files added (on-disk size: ".round(@filesize($zipfile)/1024,1)." Kb)");
1867
1868 if ($bump_index) {
1869 $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));
1870 $bump_index = false;
1871 $this->bump_index();
1872 $zipfile = $this->zip_basename.($this->index+1).'.zip.tmp';
1873 }
1874 if (empty($zip)) {
1875 $zip = new $this->use_zip_object;
1876
1877 if (file_exists($zipfile)) {
1878 $opencode = $zip->open($zipfile);
1879 $original_size = filesize($zipfile);
1880 clearstatcache();
1881 } else {
1882 $create_code = (defined('ZIPARCHIVE::CREATE')) ? ZIPARCHIVE::CREATE : 1;
1883 $opencode = $zip->open($zipfile, $create_code);
1884 $original_size = 0;
1885 }
1886
1887 if ($opencode !== true) return new WP_Error('no_open', sprintf(__('Failed to open the zip file (%s) - %s', 'updraftplus'),$zipfile, $zip->last_error));
1888 }
1889
1890 }
1891
1892 # Reset array
1893 $this->zipfiles_batched = array();
1894
1895 $ret = $zip->close();
1896 if (!$ret) {
1897 $updraftplus->log(__('A zip error occurred - check your log for more details.', 'updraftplus'), 'warning', 'zipcloseerror');
1898 $updraftplus->log("Closing the zip file returned an error (".$zip->last_error."). List of files we were trying to add follows (check their permissions).");
1899 foreach ($files_zipadded_since_open as $ffile) {
1900 $updraftplus->log("File: ".$ffile['addas']." (exists: ".(int)@file_exists($ffile['file']).", size: ".@filesize($ffile['file']).')');
1901 }
1902 }
1903
1904 $this->zipfiles_lastwritetime = time();
1905 # May not exist if the last thing we did was bump
1906 if (file_exists($zipfile) && filesize($zipfile) > $original_size) $updraftplus->something_useful_happened();
1907
1908 # Move on to next archive?
1909 if (file_exists($zipfile) && filesize($zipfile) > $this->zip_split_every) {
1910 $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));
1911 $this->bump_index();
1912 }
1913
1914 clearstatcache();
1915
1916 return $ret;
1917 }
1918
1919 private function bump_index() {
1920 global $updraftplus;
1921 $youwhat = $this->whichone;
1922
1923 $timetaken = max(microtime(true)-$this->zip_microtime_start, 0.000001);
1924
1925 $itext = ($this->index == 0) ? '' : ($this->index+1);
1926 $full_path = $this->zip_basename.$itext.'.zip';
1927 $sha = sha1_file($full_path.'.tmp');
1928 $updraftplus->jobdata_set('sha1-'.$youwhat.$this->index, $sha);
1929
1930 $next_full_path = $this->zip_basename.($this->index+2).'.zip';
1931 # We touch the next zip before renaming the temporary file; this indicates that the backup for the entity is not *necessarily* finished
1932 touch($next_full_path.'.tmp');
1933
1934 @rename($full_path.'.tmp', $full_path);
1935 $kbsize = filesize($full_path)/1024;
1936 $rate = round($kbsize/$timetaken, 1);
1937 $updraftplus->log("Created ".$this->whichone." zip (".$this->index.") - ".round($kbsize,1)." Kb in ".round($timetaken,1)." s ($rate Kb/s) (SHA1 checksum: ".$sha.")");
1938 $this->zip_microtime_start = microtime(true);
1939
1940 # No need to add $itext here - we can just delete any temporary files for this zip
1941 $updraftplus->clean_temporary_files('_'.$updraftplus->nonce."-".$youwhat, 600);
1942
1943 $this->index++;
1944 $this->job_file_entities[$youwhat]['index'] = $this->index;
1945 $updraftplus->jobdata_set('job_file_entities', $this->job_file_entities);
1946 }
1947
1948 }
1949