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

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