PluginProbe
InfiniteWP Client / trunk
InfiniteWP Client vtrunk
1.13.10 1.13.7 trunk 0.1.4 0.1.5 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.1.0 1.1.1 1.1.10 1.1.2 1.1.3 1.1.4 1.1.5 1.1.6 1.1.7 1.1.8 1.1.9 1.11.0 1.11.1 1.12.1 1.12.3 All 92 releases
iwp-client / backup / backup.php

backup.php in InfiniteWP Client trunk, at backup/backup.php

2,988 lines 146.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 if ( ! defined('ABSPATH') )
4 die();
5
6 if (!class_exists('IWP_MMB_PclZip')) {
7 require_once($GLOBALS['iwp_mmb_plugin_dir']."/backup/backup.zip.class.php");
8 require_once($GLOBALS['iwp_mmb_plugin_dir']."/backup/functions.php");
9
10 }
11
12 // This file contains functions that are only needed/loaded when a backup is running (reduces memory usage on other pages)
13
14 class IWP_MMB_Backup {
15
16 public $index = 0;
17
18 private $zipfiles_added;
19 private $zipfiles_added_thisrun = 0;
20 public $zipfiles_dirbatched;
21 public $zipfiles_batched;
22 public $zipfiles_skipped_notaltered;
23 private $zip_split_every = 419430400; # 400MB
24 private $zip_last_ratio = 1;
25 private $whichone;
26 private $zip_basename = '';
27 private $backup_basename = '';
28 private $zipfiles_lastwritetime;
29 // 0 = unknown; false = failed
30 public $binzip = 0;
31
32 private $dbhandle;
33 private $dbhandle_isgz;
34
35 # Array of entities => times
36 private $altered_since = -1;
37 # Time for the current entity
38 private $makezip_if_altered_since = -1;
39
40 private $excluded_extensions = array();
41
42 private $use_zip_object = 'IWP_MMB_ZipArchive';
43 public $debug = false;
44
45 public $iwp_backup_dir;
46 public $iwp_backup_dir_realpath;
47 public $zip_microtime_start;
48 public $first_linked_index;
49 public $last_service;
50 public $current_service;
51 public $whichdb;
52 public $table_prefix_raw;
53 public $account_space_oodles;
54 public $try_split;
55 public $dbinfo;
56 public $table_prefix;
57 public $whichdb_suffix;
58 public $duplicate_tables_exist;
59 public $excluded_prefixes;
60 public $makezip_recursive_batchedbytes;
61 public $existing_files;
62 public $make_zipfile_source;
63 public $existing_files_rawsize;
64 public $existing_zipfiles_size;
65 public $source;
66 private $blog_name;
67 private $wpdb_obj;
68 private $job_file_entities = array();
69
70 private $first_run = 0;
71
72 // Record of zip files created
73 private $backup_files_array = array();
74
75 // Used for reporting
76 private $remotestorage_extrainfo = array();
77
78 // Used when deciding to use the 'store' or 'deflate' zip storage method
79 private $extensions_to_not_compress = array();
80
81 // Append to this any skipped tables
82 private $skipped_tables;
83
84 public function __construct($backup_files, $altered_since = -1) {
85
86 global $iwp_backup_core;
87
88 // Get the blog name and rip out known-problematic characters. Remember that we may need to be able to upload this to any FTP server or cloud storage, where filename support may be unknown
89 $blog_name = str_replace('__', '_', preg_replace('/[^A-Za-z0-9_]/','', str_replace(' ','_', substr(get_bloginfo(), 0, 32))));
90 if (!$blog_name || preg_match('#^_+$#', $blog_name)) {
91 // Try again...
92 $parsed_url = parse_url(home_url(), PHP_URL_HOST);
93 $parsed_subdir = untrailingslashit(parse_url(home_url(), PHP_URL_PATH));
94 if ($parsed_subdir && '/' != $parsed_subdir) $parsed_url .= str_replace(array('/', '\\'), '_', $parsed_subdir);
95 $blog_name = str_replace('__', '_', preg_replace('/[^A-Za-z0-9_]/','', str_replace(' ','_', substr($parsed_url, 0, 32))));
96 if (!$blog_name || preg_match('#^_+$#', $blog_name)) $blog_name = 'WordPress_Backup';
97 }
98
99 $this->debug = IWP_MMB_Backup_Options::get_iwp_backup_option('IWP_debug_mode');
100 $this->iwp_backup_dir = $iwp_backup_core->backups_dir_location();
101 $this->iwp_backup_dir_realpath = realpath($this->iwp_backup_dir);
102 $iwp_backup_core->blog_name = $blog_name;
103 if ('no' === $backup_files) {
104 $this->use_zip_object = 'IWP_MMB_PclZip';
105 return;
106 }
107
108 $this->extensions_to_not_compress = array_unique(array_map('strtolower', array_map('trim', explode(',', IWP_ZIP_NOCOMPRESS))));
109
110 $this->altered_since = $altered_since;
111
112 // false means 'tried + failed'; whereas 0 means 'not yet tried'
113 // Disallow binzip on OpenVZ when we're not sure there's plenty of memory
114 if ($this->binzip === 0 && (!defined('IWP_PREFERPCLZIP') || IWP_PREFERPCLZIP != true) && (!defined('IWP_NO_BINZIP') || !IWP_NO_BINZIP) && $iwp_backup_core->current_resumption <9) {
115
116 if (@file_exists('/proc/user_beancounters') && @file_exists('/proc/meminfo') && @is_readable('/proc/meminfo')) {
117 $meminfo = @file_get_contents('/proc/meminfo', false, null, 0, 200);
118 if (is_string($meminfo) && preg_match('/MemTotal:\s+(\d+) kB/', $meminfo, $matches)) {
119 $memory_mb = $matches[1]/1024;
120 # If the report is of a large amount, then we're probably getting the total memory on the hypervisor (this has been observed), and don't really know the VPS's memory
121 $vz_log = "OpenVZ; reported memory: ".round($memory_mb, 1)." MB";
122 if ($memory_mb < 1024 || $memory_mb > 8192) {
123 $openvz_lowmem = true;
124 $vz_log .= " (will not use BinZip)";
125 }
126 $iwp_backup_core->log($vz_log);
127 }
128 }
129 if (empty($openvz_lowmem)) {
130 $iwp_backup_core->log('Checking if we have a zip executable available');
131 $binzip = $iwp_backup_core->find_working_bin_zip();
132 if (is_string($binzip)) {
133 $iwp_backup_core->log("Zip engine: found/will use a binary zip: $binzip");
134 $this->binzip = $binzip;
135 $this->use_zip_object = 'IWP_MMB_BinZip';
136 }
137 }
138 }
139
140 # In tests, PclZip was found to be 25% slower than ZipArchive
141 if ($this->use_zip_object != 'IWP_MMB_PclZip' && empty($this->binzip) && ((defined('IWP_PREFERPCLZIP') && IWP_PREFERPCLZIP == true) || !class_exists('ZipArchive') || !class_exists('IWP_MMB_ZipArchive') || (!extension_loaded('zip') && !method_exists('ZipArchive', 'AddFile')))) {
142 global $iwp_backup_core;
143 $iwp_backup_core->log("Zip engine: ZipArchive is not available or is disabled (will use PclZip if needed)");
144 $this->use_zip_object = 'IWP_MMB_PclZip';
145 }
146
147 }
148
149 public function report_remotestorage_extrainfo($service, $info_html, $info_plain) {
150 $this->remotestorage_extrainfo[$service] = array('pretty' => $info_html, 'plain' => $info_plain);
151 }
152
153 // Public, because called from the 'More Files' add-on
154 public function create_zip($create_from_dir, $whichone, $backup_file_basename, $index, $first_linked_index = false) {
155 // Note: $create_from_dir can be an array or a string
156 @set_time_limit(IWP_SET_TIME_LIMIT);
157 $original_index = $index;
158 $this->index = $index;
159 $this->first_linked_index = (false === $first_linked_index) ? 0 : $first_linked_index;
160
161 $this->whichone = $whichone;
162
163 global $iwp_backup_core;
164
165 $this->zip_split_every = max((int)$iwp_backup_core->jobdata_get('split_every'), IWP_SPLIT_MIN)*1048576;
166
167 if ('others' != $whichone) $iwp_backup_core->log("Beginning creation of dump of $whichone (split every: ".round($this->zip_split_every/1048576,1)." MB)");
168
169 if (is_string($create_from_dir) && !file_exists($create_from_dir)) {
170 $flag_error = true;
171 $iwp_backup_core->log("Does not exist: $create_from_dir");
172 if ('mu-plugins' == $whichone) {
173 if (!function_exists('get_mu_plugins')) require_once(ABSPATH.'wp-admin/includes/plugin.php');
174 $mu_plugins = get_mu_plugins();
175 if (count($mu_plugins) == 0) {
176 $iwp_backup_core->log("There appear to be no mu-plugins to back up. Will not raise an error.");
177 $flag_error = false;
178 }
179 }
180 if ($flag_error) $iwp_backup_core->log(sprintf(__("%s - could not back this entity up; the corresponding directory does not exist (%s)", 'iwp_backup_core'), $whichone, $create_from_dir), 'error');
181 return false;
182 }
183
184 $itext = (empty($index)) ? '' : ($index+1);
185 $base_path = $backup_file_basename.'-'.$whichone.$itext.'.zip';
186 $full_path = $this->iwp_backup_dir.'/'.$base_path;
187 $time_now = time();
188
189 # This is compatible with filenames which indicate increments, as it is looking only for the current increment
190 if (file_exists($full_path)) {
191 # Gather any further files that may also exist
192 $files_existing = array();
193 while (file_exists($full_path)) {
194 $files_existing[] = $base_path;
195 $time_mod = (int)@filemtime($full_path);
196 $iwp_backup_core->log($base_path.": this file has already been created (age: ".round($time_now-$time_mod,1)." s)");
197 if ($time_mod>100 && ($time_now-$time_mod)<30) {
198 $iwp_backup_core->terminate_due_to_activity($base_path, $time_now, $time_mod);
199 }
200 $index++;
201 # This is compatible with filenames which indicate increments, as it is looking only for the current increment
202 $base_path = $backup_file_basename.'-'.$whichone.($index+1).'.zip';
203 $full_path = $this->iwp_backup_dir.'/'.$base_path;
204 }
205 }
206
207 // Temporary file, to be able to detect actual completion (upon which, it is renamed)
208
209 // New (Jun-13) - be more aggressive in removing temporary files from earlier attempts - anything >=600 seconds old of this kind
210 $iwp_backup_core->clean_temporary_files('_'.$iwp_backup_core->nonce."-$whichone", 600);
211
212 // 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
213 $zip_name = $full_path.'.tmp';
214 if (file_exists($zip_name)){
215 $time_mod = (int)@filemtime($zip_name);
216 }else{
217 $time_mod = 0;
218 }
219 if (file_exists($zip_name) && $time_mod>100 && ($time_now-$time_mod)<30) {
220 $iwp_backup_core->terminate_due_to_activity($zip_name, $time_now, $time_mod);
221 }
222 if (file_exists($zip_name)) {
223 $iwp_backup_core->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).")");
224 }
225
226 // 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)
227 // Note: this doesn't catch PclZip temporary files
228 $d = dir($this->iwp_backup_dir);
229 $match = '_'.$iwp_backup_core->nonce."-".$whichone;
230 while (false !== ($e = $d->read())) {
231 if ('.' == $e || '..' == $e || !is_file($this->iwp_backup_dir.'/'.$e)) continue;
232 $ziparchive_match = preg_match("/$match([0-9]+)?\.zip\.tmp\.([A-Za-z0-9]){6}?$/i", $e);
233 $binzip_match = preg_match("/^zi([A-Za-z0-9]){6}$/", $e);
234 $pclzip_match = preg_match("/^pclzip-[a-z0-9]+.tmp$/", $e);
235 if ($time_now-filemtime($this->iwp_backup_dir.'/'.$e) < 30 && ($ziparchive_match || (0 != $iwp_backup_core->current_resumption && ($binzip_match || $pclzip_match)))) {
236 $iwp_backup_core->terminate_due_to_activity($this->iwp_backup_dir.'/'.$e, $time_now, filemtime($this->iwp_backup_dir.'/'.$e));
237 }
238 }
239 @$d->close();
240 clearstatcache();
241
242 if (isset($files_existing)) {
243 # 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.
244 # 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).
245 return $files_existing;
246 }
247
248 $this->log_account_space();
249
250 $this->zip_microtime_start = microtime(true);
251
252 # The paths in the zip should then begin with '$whichone', having removed WP_CONTENT_DIR from the front
253 $zipcode = $this->make_zipfile($create_from_dir, $backup_file_basename, $whichone);
254 if ($zipcode !== true) {
255 $iwp_backup_core->log("ERROR: Zip failure: Could not create $whichone zip (".$this->index." / $index)");
256 $iwp_backup_core->log(sprintf(__("Could not create %s zip. Consult the log file for more information.",'iwp_backup_core'),$whichone), 'error');
257 # The caller is required to update $index from $this->index
258 return false;
259 } else {
260 $itext = (empty($this->index)) ? '' : ($this->index+1);
261 $full_path = $this->iwp_backup_dir.'/'.$backup_file_basename.'-'.$whichone.$itext.'.zip';
262 if (file_exists($full_path.'.tmp')) {
263 if (@filesize($full_path.'.tmp') === 0) {
264 $iwp_backup_core->log("Did not create $whichone zip (".$this->index.") - not needed");
265 @unlink($full_path.'.tmp');
266 } else {
267
268 $checksum_description = '';
269
270 $checksums = $iwp_backup_core->which_checksums();
271
272 foreach ($checksums as $checksum) {
273
274 $cksum = hash_file($checksum, $full_path.'.tmp');
275 $iwp_backup_core->jobdata_set($checksum.'-'.$whichone.$this->index, $cksum);
276 if ($checksum_description) $checksum_description .= ', ';
277 $checksum_description .= "$checksum: $cksum";
278
279 }
280
281 @rename($full_path.'.tmp', $full_path);
282 $timetaken = max(microtime(true)-$this->zip_microtime_start, 0.000001);
283 $kbsize = filesize($full_path)/1024;
284 $rate = round($kbsize/$timetaken, 1);
285 $iwp_backup_core->log("Created $whichone zip (".$this->index.") - ".round($kbsize,1)." KB in ".round($timetaken,1)." s ($rate KB/s) ($checksum_description)");
286 // We can now remove any left-over temporary files from this job
287 }
288 } elseif ($this->index > $original_index) {
289 $iwp_backup_core->log("Did not create $whichone zip (".$this->index.") - not needed (2)");
290 # Added 12-Feb-2014 (to help multiple morefiles)
291 $this->index--;
292 } else {
293 $iwp_backup_core->log("Looked-for $whichone zip (".$this->index.") was not found (".basename($full_path).".tmp)", 'warning');
294 }
295 $iwp_backup_core->clean_temporary_files('_'.$iwp_backup_core->nonce."-$whichone", 0);
296 }
297
298 // Remove cache list files as well, if there are any
299 $iwp_backup_core->clean_temporary_files('_'.$iwp_backup_core->nonce."-$whichone", 0, true);
300
301 # Create the results array to send back (just the new ones, not any prior ones)
302 $files_existing = array();
303 $res_index = 0;
304 for ($i = $original_index; $i<= $this->index; $i++) {
305 $itext = (empty($i)) ? '' : ($i+1);
306 $full_path = $this->iwp_backup_dir.'/'.$backup_file_basename.'-'.$whichone.$itext.'.zip';
307 if (file_exists($full_path)) {
308 $files_existing[$res_index] = $backup_file_basename.'-'.$whichone.$itext.'.zip';
309 }
310 $res_index++;
311 }
312 return $files_existing;
313 }
314
315 // This method is for calling outside of a cloud_backup() context. It constructs a list of services for which prune operations should be attempted, and then calls prune_retained_backups() if necessary upon them.
316 public function do_prune_standalone() {
317 global $iwp_backup_core;
318
319 $services = $iwp_backup_core->just_one($iwp_backup_core->jobdata_get('service'));
320 if (!is_array($services)) $services = array($services);
321
322 $prune_services = array();
323
324 foreach ($services as $ind => $service) {
325 if ($service == "none" || '' == $service) continue;
326
327 $objname = "IWP_MMB_UploadModule_".$service;
328 if (!class_exists($objname) && file_exists($GLOBALS['iwp_mmb_plugin_dir'].'/backup/'.$service.'.php')) {
329 require_once($GLOBALS['iwp_mmb_plugin_dir'].'/backup/'.$service.'.php');
330 }
331 if (class_exists($objname)) {
332 $remote_obj = new $objname;
333 $pass_to_prune = null;
334 $prune_services[$service] = array($remote_obj, null);
335 } else {
336 $iwp_backup_core->log("Could not prune from service $service: remote method not found");
337 }
338
339 }
340
341 if (!empty($prune_services)) $this->prune_retained_backups($prune_services);
342 }
343
344 // Dispatch to the relevant function
345 public function cloud_backup($backup_array) {
346
347 global $iwp_backup_core;
348
349 $services = $iwp_backup_core->just_one($iwp_backup_core->jobdata_get('service'));
350 if (!is_array($services)) $services = array($services);
351
352 // We need to make sure that the loop below actually runs
353 if (empty($services)) $services = array('none');
354
355 $iwp_backup_core->jobdata_set('jobstatus', 'clouduploading');
356
357 add_action('http_request_args', array($iwp_backup_core, 'modify_http_options'));
358
359 $upload_status = $iwp_backup_core->jobdata_get('uploading_substatus');
360 if (!is_array($upload_status) || !isset($upload_status['t'])) {
361 $upload_status = array('i' => 0, 'p' => 0, 't' => max(1, count($services))*count($backup_array));
362 $iwp_backup_core->jobdata_set('uploading_substatus', $upload_status);
363 }
364
365 $do_prune = array();
366
367 # 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
368 if (count($services) >1 && !empty($iwp_backup_core->no_checkin_last_time)) {
369 $iwp_backup_core->log('No check-in last time: will try a different remote service first');
370 array_push($services, array_shift($services));
371 // Make sure that the 'no worthwhile activity' detector isn't flumoxed by the starting of a new upload at 0%
372 if ($iwp_backup_core->current_resumption > 9) $iwp_backup_core->jobdata_set('uploaded_lastreset', $iwp_backup_core->current_resumption);
373 if (1 == ($iwp_backup_core->current_resumption % 2) && count($services)>2) array_push($services, array_shift($services));
374 }
375
376 $errors_before_uploads = $iwp_backup_core->error_count();
377
378 foreach ($services as $ind => $service) {
379 # Used for logging by record_upload_chunk()
380 $this->current_service = $service;
381 # Used when deciding whether to delete the local file
382 $this->last_service = ($ind+1 >= count($services) && $errors_before_uploads == $iwp_backup_core->error_count()) ? true : false;
383
384 $log_extra = ($this->last_service) ? ' (last)' : '';
385 $iwp_backup_core->log("Cloud backup selection (".($ind+1)."/".count($services)."): ".$service.$log_extra);
386 @set_time_limit(IWP_SET_TIME_LIMIT);
387
388 $method_include = $GLOBALS['iwp_mmb_plugin_dir'].'/backup/'.$service.'.php';
389 if (file_exists($method_include)) require_once($method_include);
390
391 if ($service == "none" || '' == $service) {
392 $iwp_backup_core->log("No remote despatch: user chose no remote backup service");
393 # Still want to mark as "uploaded", to signal that nothing more needs doing. (Important on incremental runs with no cloud storage).
394 foreach ($backup_array as $bind => $file) {
395 if ($iwp_backup_core->is_uploaded($file)) {
396 $iwp_backup_core->log("Already uploaded: $file");
397 } else {
398 $iwp_backup_core->uploaded_file($file, true);
399 }
400 }
401 $this->prune_retained_backups(array("none" => array(null, null)));
402 } else {
403 $iwp_backup_core->log("Beginning dispatch of backup to remote ($service)");
404 $sarray = array();
405 foreach ($backup_array as $bind => $file) {
406 if ($iwp_backup_core->is_uploaded($file, $service)) {
407 $iwp_backup_core->log("Already uploaded to $service: $file");
408 } else {
409 $sarray[$bind] = $file;
410 }
411 }
412 $objname = "IWP_MMB_UploadModule_$service";
413 if (class_exists($objname)) {
414 $remote_obj = new $objname;
415 if (count($sarray)>0) {
416 $pass_to_prune = $remote_obj->backup($sarray);
417 $do_prune[$service] = array($remote_obj, $pass_to_prune);
418 } else {
419 // We still need to make sure that prune is run on this remote storage method, even if all entities were previously uploaded
420 $do_prune[$service] = array($remote_obj, null);
421 }
422 } else {
423 $iwp_backup_core->log("Unexpected error: no class '$objname' was found ($method_include)");
424 $iwp_backup_core->log(sprintf(__("Unexpected error: no class '%s' was found (your iwp_backup_core installation seems broken - try re-installing)", 'iwp_backup_core'), $objname), 'error');
425 }
426 }
427 }
428
429 if (!empty($do_prune)) $this->prune_retained_backups($do_prune);
430
431 remove_action('http_request_args', array($iwp_backup_core, 'modify_http_options'));
432
433 }
434
435 private function group_backups($backup_history) {
436 return array(array('sets' => $backup_history, 'process_order' => 'keep_newest'));
437 }
438
439 // $services *must* be an array
440 public function prune_retained_backups($services) {
441
442 global $iwp_backup_core, $wpdb;
443
444 if ($iwp_backup_core->jobdata_get('remotesend_info') != '') {
445 $iwp_backup_core->log("Prune old backups from local store: skipping, as this was a remote send operation");
446 return;
447 }
448
449 if (method_exists($wpdb, 'check_connection') && (!defined('IWP_SUPPRESS_CONNECTION_CHECKS') || !IWP_SUPPRESS_CONNECTION_CHECKS)) {
450 if (!$wpdb->check_connection(false)) {
451 $iwp_backup_core->reschedule(60);
452 $iwp_backup_core->log("It seems the database went away; scheduling a resumption and terminating for now");
453 $iwp_backup_core->record_still_alive();
454 die;
455 }
456 }
457
458 // If they turned off deletion on local backups, then there is nothing to do
459 if (0 == IWP_MMB_Backup_Options::get_iwp_backup_option('IWP_delete_local') && 1 == count($services) && in_array('none', $services)) {
460 $iwp_backup_core->log("Prune old backups from local store: nothing to do, since the user disabled local deletion and we are using local backups");
461 return;
462 }
463
464 // $iwp_backup_core->jobdata_set('jobstatus', 'pruning');
465 // $iwp_backup_core->jobdata_set('prune', 'begun');
466 call_user_func_array(array($iwp_backup_core, 'jobdata_set_multi'), array('jobstatus', 'pruning', 'prune', 'begun'));
467
468 // Number of backups to retain - files
469 $label = $iwp_backup_core->jobdata_get('label');
470 $IWP_retain = IWP_MMB_Backup_Options::get_iwp_backup_option('IWP_retain', 2);
471 $IWP_retain = is_numeric($IWP_retain) ? $IWP_retain : 1;
472
473 // Number of backups to retain - db
474 $IWP_retain_db = IWP_MMB_Backup_Options::get_iwp_backup_option('IWP_retain_db', $IWP_retain);
475 $IWP_retain_db = is_numeric($IWP_retain_db) ? $IWP_retain_db : 1;
476 $other_method_backups = iwp_mmb_get_backup_ID_by_taskname('multicall', $label);
477 $current_backups = $iwp_backup_core->get_timestamp_by_label($label);
478 $all_backups = array();
479 $delete_backup = array();
480 if (!empty($other_method_backups)) {
481 $all_backups = array_merge($all_backups, $other_method_backups);
482 if (!empty($current_backups)) {
483 $all_backups = array_merge($all_backups, $current_backups);
484 ksort($all_backups);
485 ksort($current_backups);
486 foreach ($other_method_backups as $lastUpdateTime => $historyID) {
487 foreach ($current_backups as $time => $value) {
488 if ($time > $lastUpdateTime) {
489 $delete_backup[$lastUpdateTime] = $historyID;
490 }
491 break;
492 }
493 }
494 }
495 }
496 if (!empty($delete_backup)) {
497 $total_backups = count($all_backups);
498 if ($total_backups > $IWP_retain) {
499 require_once($GLOBALS['iwp_mmb_plugin_dir']."/backup.class.singlecall.php");
500 $backup_instance = new IWP_MMB_Backup_Singlecall();
501 foreach ($delete_backup as $lastUpdateTime => $historyID) {
502 $total_backups--;
503 $backup_instance->delete_backup(array('result_id' => $historyID));
504 if ($total_backups<= $IWP_retain) {
505 return;
506 }
507 }
508 }
509 }
510 // $IWP_retain_db-=1;
511 $iwp_backup_core->log("Retain: beginning examination of existing backup sets; user setting: retain_files=$IWP_retain, retain_db=$IWP_retain_db");
512
513 // Returns an array, most recent first, of backup sets
514
515
516 $ignored_because_imported = array();
517 ksort($current_backups);
518 $delete_backup_count = count($current_backups);
519 if (!empty($current_backups) && $delete_backup_count>$IWP_retain) {
520 foreach ($current_backups as $timestamp => $value) {
521 $delete_backup_count--;
522 $iwp_backup_core->delete_backup(array('result_id'=>$timestamp));
523 if ($delete_backup_count==$IWP_retain) {
524 break;
525 }
526 }
527 }
528
529
530 // // Remove non-native (imported) backups, which are neither counted nor pruned. It's neater to do these in advance, and log only one line.
531 // $functional_backup_history = $backup_history;
532 // foreach ($functional_backup_history as $backup_time => $backup_to_examine) {
533 // if (isset($backup_to_examine['native']) && false == $backup_to_examine['native']) {
534 // $ignored_because_imported[] = $backup_time;
535 // unset($functional_backup_history[$backup_time]);
536 // }
537 // }
538 // if (!empty($ignored_because_imported)) {
539 // $iwp_backup_core->log("These backup set(s) were imported from a remote location, so will not be counted or pruned. Skipping: ".implode(', ', $ignored_because_imported));
540 // }
541
542 // $backupable_entities = $iwp_backup_core->get_backupable_file_entities(true);
543 // $backupable_entities = array_merge($backupable_entities, array('backup_file_basename'=>0, 'db' =>0));
544 // $database_backups_found = array();
545
546 // $file_entities_backups_found = array();
547 // foreach ($backupable_entities as $entity => $info) {
548 // $file_entities_backups_found[$entity] = 0;
549 // }
550 // $file_entities_backups_found['db'] = 0;
551 // if (false === ($backup_db_groups = apply_filters('IWP_group_backups_for_pruning', false, $functional_backup_history, 'db'))) {
552 // $backup_db_groups = $this->group_backups($functional_backup_history);
553 // }
554 // $iwp_backup_core->log("Number of backup sets in history: ".count($backup_history)."; groups (db): ".count($backup_db_groups));
555 // foreach ($backup_db_groups as $group_id => $group) {
556
557 // // The array returned by iwp_backup_core::get_backup_history() is already sorted, with most-recent first
558 // // foreach ($backup_history as $backup_datestamp => $backup_to_examine) {
559
560 // if (empty($group['sets']) || !is_array($group['sets'])) continue;
561 // $sets = $group['sets'];
562
563 // // Sort the groups into the desired "keep this first" order
564 // $process_order = (!empty($group['process_order']) && 'keep_oldest' == $group['process_order']) ? 'keep_oldest' : 'keep_newest';
565 // if ('keep_oldest' == $process_order) ksort($sets);
566
567 // $rule = !empty($group['rule']) ? $group['rule'] : array('after-howmany' => 0, 'after-period' => 0, 'every-period' => 1, 'every-howmany' => 1);
568
569 // foreach ($sets as $backup_datestamp => $backup_to_examine) {
570 // if ($backup_to_examine['label'] != $label) {
571 // continue;
572 // }
573
574 // $files_to_prune = array();
575 // $nonce = empty($backup_to_examine['nonce']) ? '???' : $backup_to_examine['nonce'];
576
577 // // $backup_to_examine is an array of file names, keyed on db/plugins/themes/uploads
578 // // The new backup_history array is saved afterwards, so remember to unset the ones that are to be deleted
579 // $iwp_backup_core->log(sprintf("Examining (for databases) backup set with group_id=$group_id, nonce=%s, datestamp=%s (%s)", $nonce, $backup_datestamp, gmdate('M d Y H:i:s', $backup_datestamp)));
580
581 // // Auto-backups are only counted or deleted once we have reached the retain limit - before that, they are skipped
582 // $is_autobackup = !empty($backup_to_examine['autobackup']);
583
584 // $remote_sent = (!empty($backup_to_examine['service']) && ((is_array($backup_to_examine['service']) && in_array('remotesend', $backup_to_examine['service'])) || 'remotesend' === $backup_to_examine['service'])) ? true : false;
585
586 // $any_deleted_via_filter_yet = false;
587
588 // // Databases
589 // foreach ($backup_to_examine as $key => $data) {
590 // if ('db' != strtolower(substr($key, 0, 2)) || '-size' == substr($key, -5, 5)) continue;
591
592 // if (empty($database_backups_found[$key])) $database_backups_found[$key] = 0;
593
594 // if ($nonce == $iwp_backup_core->nonce) {
595 // $iwp_backup_core->log("This backup set is the backup set just made, so will not be deleted.");
596 // $database_backups_found[$key]++;
597 // continue;
598 // }
599
600 // if ($is_autobackup) {
601 // if ($any_deleted_via_filter_yet) {
602 // $iwp_backup_core->log("This backup set ($backup_datestamp) was an automatic backup, but we have previously deleted a backup due to a limit, so it will be pruned (but not counted towards numerical limits).");
603 // $prune_it = true;
604 // } elseif ($database_backups_found[$key] < $IWP_retain_db) {
605 // $iwp_backup_core->log("This backup set ($backup_datestamp) was an automatic backup, and we have not yet reached any retain limits, so it will not be counted or pruned. Skipping.");
606 // continue;
607 // } else {
608 // $iwp_backup_core->log("This backup set ($backup_datestamp) was an automatic backup, and we have already reached retain limits, so it will be pruned.");
609 // $prune_it = true;
610 // }
611 // } else {
612 // $prune_it = false;
613 // }
614
615 // if ($remote_sent) {
616 // $prune_it = true;
617 // $iwp_backup_core->log("$backup_datestamp: $key: was sent to remote site; will remove from local record (only)");
618 // }
619
620 // // All non-auto backups must be run through this filter (in date order) regardless of the current state of $prune_it - so that filters are able to track state.
621 // $prune_it_before_filter = $prune_it;
622
623 // if (!$is_autobackup) $prune_it = apply_filters('IWP_prune_or_not', $prune_it, 'db', $backup_datestamp, $key, $database_backups_found[$key], $rule, $group_id);
624
625 // // Apply the final retention limit list (do not increase the 'retained' counter before seeing if the backup is being pruned for some other reason)
626 // if (!$prune_it && !$is_autobackup) {
627
628 // if ($database_backups_found[$key] + 1 > $IWP_retain_db) {
629 // $prune_it = true;
630
631 // $fname = (is_string($data)) ? $data : $data[0];
632 // $iwp_backup_core->log("$backup_datestamp: $key: this set includes a database (".$fname."); db count is now ".$database_backups_found[$key]);
633
634 // $iwp_backup_core->log("$backup_datestamp: $key: over retain limit ($IWP_retain_db); will delete this database");
635 // }
636
637 // }
638
639 // if ($prune_it) {
640 // if (!$prune_it_before_filter) $any_deleted_via_filter_yet = true;
641
642 // if (!empty($data)) {
643 // $size_key = $key.'-size';
644 // $size = isset($backup_to_examine[$size_key]) ? $backup_to_examine[$size_key] : null;
645 // foreach ($services as $service => $sd) {
646 // $this->prune_file($service, $data, $sd[0], $sd[1], array($size));
647 // }
648 // }
649 // unset($backup_to_examine[$key]);
650 // $iwp_backup_core->record_still_alive();
651 // } elseif (!$is_autobackup) {
652 // $database_backups_found[$key]++;
653 // }else{
654 // continue;
655 // }
656
657 // $backup_to_examine = $this->remove_backup_set_if_empty($backup_to_examine, $backup_datestamp, $backupable_entities, $backup_history);
658 // if (empty($backup_to_examine)) {
659 // unset($functional_backup_history[$backup_datestamp]);
660 // unset($backup_history[$backup_datestamp]);
661 // $this->maybe_save_backup_history_and_reschedule($backup_history);
662 // } else {
663 // $functional_backup_history[$backup_datestamp] = $backup_to_examine;
664 // $backup_history[$backup_datestamp] = $backup_to_examine;
665 // }
666 // }
667 // }
668 // }
669
670 // if (false === ($backup_files_groups = apply_filters('IWP_group_backups_for_pruning', false, $functional_backup_history, 'files'))) {
671 // $backup_files_groups = $this->group_backups($functional_backup_history);
672 // }
673
674 // $iwp_backup_core->log("Number of backup sets in history: ".count($backup_history)."; groups (files): ".count($backup_files_groups));
675
676 // // Now again - this time for the files
677 // foreach ($backup_files_groups as $group_id => $group) {
678
679 // // The array returned by iwp_backup_core::get_backup_history() is already sorted, with most-recent first
680 // // foreach ($backup_history as $backup_datestamp => $backup_to_examine) {
681
682 // if (empty($group['sets']) || !is_array($group['sets'])) continue;
683 // $sets = $group['sets'];
684
685 // // Sort the groups into the desired "keep this first" order
686 // $process_order = (!empty($group['process_order']) && 'keep_oldest' == $group['process_order']) ? 'keep_oldest' : 'keep_newest';
687 // // Youngest - i.e. smallest epoch - first
688 // if ('keep_oldest' == $process_order) ksort($sets);
689
690 // $rule = !empty($group['rule']) ? $group['rule'] : array('after-howmany' => 0, 'after-period' => 0, 'every-period' => 1, 'every-howmany' => 1);
691
692 // foreach ($sets as $backup_datestamp => $backup_to_examine) {
693 // if ($backup_to_examine['label'] != $label) {
694 // continue;
695 // }
696 // $files_to_prune = array();
697 // $nonce = empty($backup_to_examine['nonce']) ? '???' : $backup_to_examine['nonce'];
698
699 // // $backup_to_examine is an array of file names, keyed on db/plugins/themes/uploads
700 // // The new backup_history array is saved afterwards, so remember to unset the ones that are to be deleted
701 // $iwp_backup_core->log(sprintf("Examining (for files) backup set with nonce=%s, datestamp=%s (%s)", $nonce, $backup_datestamp, gmdate('M d Y H:i:s', $backup_datestamp)));
702
703 // // This was already done earlier
704 // // if (isset($backup_to_examine['native']) && false == $backup_to_examine['native']) {
705 // // $iwp_backup_core->log("This backup set was imported from a remote location, so will not be counted or pruned. Skipping.");
706 // // continue;
707 // // }
708
709 // // Auto-backups are only counted or deleted once we have reached the retain limit - before that, they are skipped
710 // $is_autobackup = !empty($backup_to_examine['autobackup']);
711
712 // $remote_sent = (!empty($backup_to_examine['service']) && ((is_array($backup_to_examine['service']) && in_array('remotesend', $backup_to_examine['service'])) || 'remotesend' === $backup_to_examine['service'])) ? true : false;
713
714 // $any_deleted_via_filter_yet = false;
715
716 // $file_sizes = array();
717
718 // // Files
719 // foreach ($backupable_entities as $entity => $info) {
720 // if (!empty($backup_to_examine[$entity])) {
721
722 // // This should only be able to happen if you import backups with a future timestamp
723 // if ($nonce == $iwp_backup_core->nonce) {
724 // $iwp_backup_core->log("This backup set is the backup set just made, so will not be deleted.");
725 // $file_entities_backups_found[$entity]++;
726 // continue;
727 // }
728
729 // if ($is_autobackup) {
730 // if ($any_deleted_via_filter_yet) {
731 // $iwp_backup_core->log("This backup set was an automatic backup, but we have previously deleted a backup due to a limit, so it will be pruned (but not counted towards numerical limits).");
732 // $prune_it = true;
733 // } elseif ($file_entities_backups_found[$entity] < $IWP_retain) {
734 // $iwp_backup_core->log("This backup set ($backup_datestamp) was an automatic backup, and we have not yet reached any retain limits, so it will not be counted or pruned. Skipping.");
735 // continue;
736 // } else {
737 // $iwp_backup_core->log("This backup set ($backup_datestamp) was an automatic backup, and we have already reached retain limits, so it will be pruned.");
738 // $prune_it = true;
739 // }
740 // } else {
741 // $prune_it = false;
742 // }
743
744 // if ($remote_sent) {
745 // $prune_it = true;
746 // }
747
748 // // All non-auto backups must be run through this filter (in date order) regardless of the current state of $prune_it - so that filters are able to track state.
749 // $prune_it_before_filter = $prune_it;
750 // if (!$is_autobackup) $prune_it = apply_filters('IWP_backup_core_prune_or_not', $prune_it, 'files', $backup_datestamp, $entity, $file_entities_backups_found[$entity], $rule, $group_id);
751
752 // // The "more than maximum to keep?" counter should not be increased until we actually know that the set is being kept. Before verison 1.11.22, we checked this before running the filter, which resulted in the counter being increased for sets that got pruned via the filter (i.e. not kept) - and too many backups were thus deleted
753 // if (!$prune_it && !$is_autobackup) {
754 // if ($file_entities_backups_found[$entity] >= $IWP_retain) {
755 // $iwp_backup_core->log("$entity: over retain limit ($IWP_retain); will delete this file entity");
756 // $prune_it = true;
757 // }
758 // }else{
759 // continue;
760 // }
761
762 // if ($prune_it) {
763 // if (!$prune_it_before_filter) $any_deleted_via_filter_yet = true;
764 // $prune_this = $backup_to_examine[$entity];
765 // if (is_string($prune_this)) $prune_this = array($prune_this);
766 // foreach ($prune_this as $k => $prune_file) {
767 // if ($remote_sent) {
768 // $iwp_backup_core->log("$entity: $backup_datestamp: was sent to remote site; will remove from local record (only)");
769 // }
770 // $size_key = (0 == $k) ? $entity.'-size' : $entity.$k.'-size';
771 // $size = (isset($backup_to_examine[$size_key])) ? $backup_to_examine[$size_key] : null;
772 // $files_to_prune[] = $prune_file;
773 // $file_sizes[] = $size;
774 // }
775 // unset($backup_to_examine[$entity]);
776
777 // } elseif (!$is_autobackup) {
778 // $file_entities_backups_found[$entity]++;
779 // }
780 // }
781 // }
782
783 // // Sending an empty array is not itself a problem - except that the remote storage method may not check that before setting up a connection, which can waste time: especially if this is done every time around the loop.
784 // if (!empty($files_to_prune)) {
785 // // Actually delete the files
786 // foreach ($services as $service => $sd) {
787 // $this->prune_file($service, $files_to_prune, $sd[0], $sd[1], $file_sizes);
788 // $iwp_backup_core->record_still_alive();
789 // }
790 // }
791
792 // $backup_to_examine = $this->remove_backup_set_if_empty($backup_to_examine, $backup_datestamp, $backupable_entities, $backup_history);
793 // if (empty($backup_to_examine)) {
794 // // unset($functional_backup_history[$backup_datestamp]);
795 // unset($backup_history[$backup_datestamp]);
796 // $this->maybe_save_backup_history_and_reschedule($backup_history);
797 // } else {
798 // // $functional_backup_history[$backup_datestamp] = $backup_to_examine;
799 // $backup_history[$backup_datestamp] = $backup_to_examine;
800 // }
801
802 // // Loop over backup sets
803 // }
804
805 // // Look over backup groups
806 // }
807
808 // $iwp_backup_core->log("Retain: saving new backup history (sets now: ".count($backup_history).") and finishing retain operation");
809
810 // IWP_MMB_Backup_Options::update_iwp_backup_option('IWP_backup_history', $backup_history, false);
811
812 do_action('IWP_prune_retained_backups_finished');
813
814 $iwp_backup_core->jobdata_set('prune', 'finished');
815
816 }
817
818 // The purpose of this is to save the backup history periodically - for the benefit of setups where the pruning takes longer than the total allow run time (e.g. if the network communications to the remote storage have delays in, and there are a lot of sets to scan)
819 private function maybe_save_backup_history_and_reschedule($backup_history) {
820 static $last_saved_at = 0;
821 if (!$last_saved_at) $last_saved_at = time();
822 if (time() - $last_saved_at >= 10) {
823 global $iwp_backup_core;
824 $iwp_backup_core->log("Retain: saving new backup history, because at least 10 seconds have passed since the last save (sets now: ".count($backup_history).")");
825 IWP_MMB_Backup_Options::update_iwp_backup_option('IWP_backup_history', $backup_history, false);
826 $iwp_backup_core->something_useful_happened();
827 $last_saved_at = time();
828 }
829 }
830
831 private function remove_backup_set_if_empty($backup_to_examine, $backup_datestamp, $backupable_entities, $backup_history) {
832
833 global $iwp_backup_core;
834
835 // Get new result, post-deletion; anything left in this set?
836 $contains_files = 0;
837 foreach ($backupable_entities as $entity => $info) {
838 if (isset($backup_to_examine[$entity])) {
839 $contains_files = 1;
840 break;
841 }
842 }
843
844 $contains_db = 0;
845 foreach ($backup_to_examine as $key => $data) {
846 if ('db' == strtolower(substr($key, 0, 2)) && '-size' != substr($key, -5, 5)) {
847 $contains_db = 1;
848 break;
849 }
850 }
851
852 // Delete backup set completely if empty, o/w just remove DB
853 // We search on the four keys which represent data, allowing other keys to be used to track other things
854 if (!$contains_files && !$contains_db) {
855 $iwp_backup_core->log("This backup set is now empty; will remove from history");
856 if (isset($backup_to_examine['nonce'])) {
857 $fullpath = $this->iwp_backup_dir."/log.".$backup_to_examine['nonce'].".txt";
858 if (is_file($fullpath)) {
859 $iwp_backup_core->log("Deleting log file (log.".$backup_to_examine['nonce'].".txt)");
860 @unlink($fullpath);
861 } else {
862 $iwp_backup_core->log("Corresponding log file (log.".$backup_to_examine['nonce'].".txt) not found - must have already been deleted");
863 }
864 } else {
865 $iwp_backup_core->log("No nonce record found in the backup set, so cannot delete any remaining log file");
866 }
867 // unset($backup_history[$backup_datestamp]);
868 return false;
869 } else {
870 $iwp_backup_core->log("This backup set remains non-empty (f=$contains_files/d=$contains_db); will retain in history");
871 return $backup_to_examine;
872 }
873
874 }
875
876 # $dofiles: An array of files (or a single string for one file)
877 private function prune_file($service, $dofiles, $method_object = null, $object_passback = null, $file_sizes = array()) {
878 global $iwp_backup_core;
879 if (!is_array($dofiles)) $dofiles=array($dofiles);
880
881 if (!apply_filters('IWP_prune_file', true, $dofiles, $service, $method_object, $object_passback, $file_sizes)) {
882 $iwp_backup_core->log("Prune: service=$service: skipped via filter");
883 }
884 foreach ($dofiles as $i => $dofile) {
885 if (empty($dofile)) continue;
886 $iwp_backup_core->log("Delete file: $dofile, service=$service");
887 $fullpath = $this->iwp_backup_dir.'/'.$dofile;
888 // delete it if it's locally available
889 if (file_exists($fullpath)) {
890 $iwp_backup_core->log("Deleting local copy ($dofile)");
891 @unlink($fullpath);
892 }
893 }
894 // Despatch to the particular method's deletion routine
895 if (!is_null($method_object)) $method_object->delete($dofiles, $object_passback, $file_sizes);
896 }
897
898 // The purpose of this function is to make sure that the options table is put in the database first, then the users table, then the site + blogs tables (if present - multisite), then the usermeta table; and after that the core WP tables - so that when restoring we restore the core tables first
899 private function backup_db_sorttables($a_arr, $b_arr) {
900
901 $a = $a_arr['name'];
902 $a_table_type = $a_arr['type'];
903 $b = $b_arr['name'];
904 $b_table_type = $b_arr['type'];
905
906 // Views must always go after tables (since they can depend upon them)
907 if ('VIEW' == $a_table_type && 'VIEW' != $b_table_type) return 1;
908 if ('VIEW' == $b_table_type && 'VIEW' != $a_table_type) return -1;
909
910 if ('wp' != $this->whichdb) return strcmp($a, $b);
911
912 global $iwp_backup_core;
913 if ($a == $b) return 0;
914 $our_table_prefix = $this->table_prefix_raw;
915 if ($a == $our_table_prefix.'options') return -1;
916 if ($b == $our_table_prefix.'options') return 1;
917 if ($a == $our_table_prefix.'site') return -1;
918 if ($b == $our_table_prefix.'site') return 1;
919 if ($a == $our_table_prefix.'blogs') return -1;
920 if ($b == $our_table_prefix.'blogs') return 1;
921 if ($a == $our_table_prefix.'users') return -1;
922 if ($b == $our_table_prefix.'users') return 1;
923 if ($a == $our_table_prefix.'usermeta') return -1;
924 if ($b == $our_table_prefix.'usermeta') return 1;
925
926 if (empty($our_table_prefix)) return strcmp($a, $b);
927
928 try {
929 $core_tables = array_merge($this->wpdb_obj->tables, $this->wpdb_obj->global_tables, $this->wpdb_obj->ms_global_tables);
930 } catch (Exception $e) {
931 $iwp_backup_core->log($e->getMessage());
932 }
933
934 if (empty($core_tables)) $core_tables = array('terms', 'term_taxonomy', 'termmeta', 'term_relationships', 'commentmeta', 'comments', 'links', 'postmeta', 'posts', 'site', 'sitemeta', 'blogs', 'blogversions');
935
936 global $iwp_backup_core;
937 $na = $iwp_backup_core->str_replace_once($our_table_prefix, '', $a);
938 $nb = $iwp_backup_core->str_replace_once($our_table_prefix, '', $b);
939 if (in_array($na, $core_tables) && !in_array($nb, $core_tables)) return -1;
940 if (!in_array($na, $core_tables) && in_array($nb, $core_tables)) return 1;
941 return strcmp($a, $b);
942 }
943
944 private function log_account_space() {
945 # Don't waste time if space is huge
946 if (!empty($this->account_space_oodles)) return;
947 global $iwp_backup_core;
948 $hosting_bytes_free = $iwp_backup_core->get_hosting_disk_quota_free();
949 if (is_array($hosting_bytes_free)) {
950 $perc = round(100*$hosting_bytes_free[1]/(max($hosting_bytes_free[2], 1)), 1);
951 $iwp_backup_core->log(sprintf('Free disk space in account: %s (%s used)', round($hosting_bytes_free[3]/1048576, 1)." MB", "$perc %"));
952 }
953 }
954
955 // Returns the basename up to and including the nonce (but not the entity)
956 private function get_backup_file_basename_from_time($use_time) {
957 global $iwp_backup_core;
958 $site_name = str_replace(array(
959 "_",
960 "/",
961 "~"
962 ), array(
963 "",
964 "-",
965 "-"
966 ), rtrim(remove_http(get_bloginfo('url')), "/"));
967 return $site_name.'_backup_'.get_date_from_gmt(gmdate('Y-m-d H:i:s', $use_time), 'Y-m-d-Hi').'_'.$this->blog_name.'_'.$iwp_backup_core->nonce.'_'.$iwp_backup_core->get_wordpress_version();
968 }
969
970 private function find_existing_zips($dir, $match_nonce) {
971 $zips = array();
972 if ($handle = opendir($dir)) {
973 while (false !== ($entry = readdir($handle))) {
974 if ($entry != "." && $entry != "..") {
975 if (preg_match('/^backup_(\d{4})-(\d{2})-(\d{2})-(\d{2})(\d{2})_.*_([0-9a-f]{12})-([\-a-z]+)([0-9]+)?\.zip$/i', $entry, $matches)) {
976 if ($matches[6] !== $match_nonce) continue;
977 $timestamp = mktime($matches[4], $matches[5], 0, $matches[2], $matches[3], $matches[1]);
978 $entity = $matches[7];
979 $index = empty($matches[8]) ? '0': $matches[8];
980 $zips[$entity][$index] = array($timestamp, $entry);
981 }
982 }
983 }
984 }
985 return $zips;
986 }
987
988 // $files should be an array as returned by find_existing_zips()
989 private function file_exists($files, $entity, $index = 0) {
990 if (isset($files[$entity]) && isset($files[$entity][$index])) {
991 $file = $files[$entity][$index];
992 // Return the filename
993 return $file[1];
994 } else {
995 return false;
996 }
997 }
998
999 // This function is resumable
1000 private function backup_dirs($job_status) {
1001
1002 global $iwp_backup_core;
1003
1004 if (!$iwp_backup_core->backup_time) $iwp_backup_core->backup_time_nonce();
1005
1006 $use_time = apply_filters('IWP_backup_time_thisrun', $iwp_backup_core->backup_time);
1007 $backup_file_basename = $this->get_backup_file_basename_from_time($use_time);
1008
1009 $backup_array = array();
1010
1011 $possible_backups = $iwp_backup_core->get_backupable_file_entities(true);
1012
1013 // Was there a check-in last time? If not, then reduce the amount of data attempted
1014 if ($job_status != 'finished' && $iwp_backup_core->current_resumption >= 2) {
1015 if (!empty($iwp_backup_core->no_checkin_last_time) || !$iwp_backup_core->newresumption_scheduled) {
1016 if ($iwp_backup_core->current_resumption - $iwp_backup_core->last_successful_resumption > 2 || !$iwp_backup_core->newresumption_scheduled) {
1017 $this->try_split = true;
1018 } elseif ($iwp_backup_core->current_resumption<=10) {
1019 $maxzipbatch = $iwp_backup_core->jobdata_get('maxzipbatch', 26214400);
1020 if ((int)$maxzipbatch < 1) $maxzipbatch = 26214400;
1021
1022 $new_maxzipbatch = max(floor($maxzipbatch * 0.75), 20971520);
1023 if ($new_maxzipbatch < $maxzipbatch) {
1024 $iwp_backup_core->log("No check-in was detected on the previous run - as a result, we are reducing the batch amount (old=$maxzipbatch, new=$new_maxzipbatch)");
1025 $iwp_backup_core->jobdata_set('maxzipbatch', $new_maxzipbatch);
1026 $iwp_backup_core->jobdata_set('maxzipbatch_ceiling', $new_maxzipbatch);
1027 }
1028 }
1029 }
1030 }
1031
1032 if($job_status != 'finished' && !$iwp_backup_core->really_is_writable($this->iwp_backup_dir)) {
1033 $iwp_backup_core->log("Backup directory (".$this->iwp_backup_dir.") is not writable, or does not exist");
1034 $iwp_backup_core->log(sprintf(__("Backup directory (%s) is not writable, or does not exist.", 'InfiniteWP'), $this->iwp_backup_dir), 'error');
1035 return array();
1036 }
1037
1038 $this->job_file_entities = $iwp_backup_core->jobdata_get('job_file_entities');
1039 # This is just used for the visual feedback (via the 'substatus' key)
1040 $which_entity = 0;
1041 # e.g. plugins, themes, uploads, others
1042 # $whichdir might be an array (if $youwhat is 'more')
1043
1044 # Returns an array (keyed off the entity) of ($timestamp, $filename) arrays
1045 $existing_zips = $this->find_existing_zips($this->iwp_backup_dir, $iwp_backup_core->nonce);
1046
1047 foreach ($possible_backups as $youwhat => $whichdir) {
1048
1049 if (isset($this->job_file_entities[$youwhat])) {
1050
1051 $index = (int)$this->job_file_entities[$youwhat]['index'];
1052 if (empty($index)) $index=0;
1053 $indextext = (0 == $index) ? '' : (1+$index);
1054
1055 $zip_file = $this->iwp_backup_dir.'/'.$backup_file_basename.'-'.$youwhat.$indextext.'.zip';
1056
1057 # Split needed?
1058 $split_every = max((int)$iwp_backup_core->jobdata_get('split_every'), 250);
1059 //if (file_exists($zip_file) && filesize($zip_file) > $split_every*1048576) {
1060 if (false != ($existing_file = $this->file_exists($existing_zips, $youwhat, $index)) && filesize($this->iwp_backup_dir.'/'.$existing_file) > $split_every*1048576) {
1061 $index++;
1062 $this->job_file_entities[$youwhat]['index'] = $index;
1063 $iwp_backup_core->jobdata_set('job_file_entities', $this->job_file_entities);
1064 }
1065
1066 // Populate prior parts of array, if we're on a subsequent zip file
1067 if ($index > 0) {
1068 for ($i=0; $i<$index; $i++) {
1069 $itext = (0 == $i) ? '' : ($i+1);
1070 // Get the previously-stored filename if possible (which should be always); failing that, base it on the current run
1071
1072 $zip_file = (isset($this->backup_files_array[$youwhat]) && isset($this->backup_files_array[$youwhat][$i])) ? $this->backup_files_array[$youwhat][$i] : $backup_file_basename.'-'.$youwhat.$itext.'.zip';
1073
1074 $backup_array[$youwhat][$i] = $zip_file;
1075 $z = $this->iwp_backup_dir.'/'.$zip_file;
1076 $itext = (0 == $i) ? '' : $i;
1077
1078 $fs_key = $youwhat.$itext.'-size';
1079 if (file_exists($z)) {
1080 $backup_array[$fs_key] = filesize($z);
1081 } elseif (isset($this->backup_files_array[$fs_key])) {
1082 $backup_array[$fs_key] = $this->backup_files_array[$fs_key];
1083 }
1084 }
1085 }
1086
1087 // I am not certain that all the conditions in here are possible. But there's no harm.
1088 if ('finished' == $job_status) {
1089 // Add the final part of the array
1090 if ($index > 0) {
1091 $zip_file = (isset($this->backup_files_array[$youwhat]) && isset($this->backup_files_array[$youwhat][$index])) ? $this->backup_files_array[$youwhat][$index] : $backup_file_basename.'-'.$youwhat.($index+1).'.zip';
1092
1093 //$fbase = $backup_file_basename.'-'.$youwhat.($index+1).'.zip';
1094 $fbase = '';
1095 $z = $this->iwp_backup_dir.'/'.$zip_file;
1096 $fs_key = $youwhat.$index.'-size';
1097 if (file_exists($z)) {
1098 $backup_array[$youwhat][$index] = $fbase;
1099 $backup_array[$fs_key] = filesize($z);
1100 } elseif (isset($this->backup_files_array[$fs_key])) {
1101 $backup_array[$youwhat][$index] = $fbase;
1102 $backup_array[$fs_key] = $this->backup_files_array[$fs_key];
1103 }
1104 } else {
1105 $zip_file = (isset($this->backup_files_array[$youwhat]) && isset($this->backup_files_array[$youwhat][0])) ? $this->backup_files_array[$youwhat][0] : $backup_file_basename.'-'.$youwhat.'.zip';
1106
1107 $backup_array[$youwhat] = $zip_file;
1108 $fs_key=$youwhat.'-size';
1109
1110 if (file_exists($zip_file)) {
1111 $backup_array[$fs_key] = filesize($zip_file);
1112 } elseif (isset($this->backup_files_array[$fs_key])) {
1113 $backup_array[$fs_key] = $this->backup_files_array[$fs_key];
1114 }
1115 }
1116 } else {
1117
1118 $which_entity++;
1119 $iwp_backup_core->jobdata_set('filecreating_substatus', array('e' => $youwhat, 'i' => $which_entity, 't' => count($this->job_file_entities)));
1120
1121 if ('others' == $youwhat) $iwp_backup_core->log("Beginning backup of other directories found in the content directory (index: $index)");
1122
1123 # Apply a filter to allow add-ons to provide their own method for creating a zip of the entity
1124 $created = apply_filters('IWP_backup_makezip_'.$youwhat, $whichdir, $backup_file_basename, $index);
1125 # If the filter did not lead to something being created, then use the default method
1126 if ($created === $whichdir) {
1127
1128 // http://www.phpconcept.net/pclzip/user-guide/53
1129 /* First parameter to create is:
1130 An array of filenames or dirnames,
1131 or
1132 A string containing the filename or a dirname,
1133 or
1134 A string containing a list of filename or dirname separated by a comma.
1135 */
1136
1137 if ('others' == $youwhat) {
1138 $dirlist = $iwp_backup_core->backup_others_dirlist(true);
1139 } elseif ('uploads' == $youwhat) {
1140 $dirlist = $iwp_backup_core->backup_uploads_dirlist(true);
1141 } elseif ('more' == $youwhat) {
1142 $dirlist = $iwp_backup_core->backup_more_dirlist($whichdir);
1143 }else {
1144 $dirlist = $whichdir;
1145 if (is_array($dirlist)) $dirlist=array_shift($dirlist);
1146 }
1147
1148 if (!empty($dirlist)) {
1149 $created = $this->create_zip($dirlist, $youwhat, $backup_file_basename, $index);
1150 # Now, store the results
1151 if (!is_string($created) && !is_array($created)) $iwp_backup_core->log("$youwhat: create_zip returned an error");
1152 } else {
1153 $iwp_backup_core->log("No backup of $youwhat: there was nothing found to back up");
1154 }
1155 }
1156
1157 if ($created != $whichdir && (is_string($created) || is_array($created))) {
1158 if (is_string($created)) $created=array($created);
1159 foreach ($created as $findex => $fname) {
1160 $backup_array[$youwhat][$index] = $fname;
1161 $itext = ($index == 0) ? '' : $index;
1162 $index++;
1163 $backup_array[$youwhat.$itext.'-size'] = filesize($this->iwp_backup_dir.'/'.$fname);
1164 }
1165 }
1166
1167 $this->job_file_entities[$youwhat]['index'] = $this->index;
1168 $iwp_backup_core->jobdata_set('job_file_entities', $this->job_file_entities);
1169
1170 }
1171 } else {
1172 $iwp_backup_core->log("No backup of $youwhat: excluded by user's options");
1173 }
1174 }
1175
1176 return $backup_array;
1177 }
1178
1179 // 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.
1180 public function resumable_backup_of_files($resumption_no) {
1181 global $iwp_backup_core;
1182 // Backup directories and return a numerically indexed array of file paths to the backup files
1183 $bfiles_status = $iwp_backup_core->jobdata_get('backup_files');
1184 $this->backup_files_array = $iwp_backup_core->jobdata_get('backup_files_array');;
1185 if (!is_array($this->backup_files_array)) $this->backup_files_array = array();
1186 if ('finished' == $bfiles_status) {
1187 $iwp_backup_core->log("Creation of backups of directories: already finished");
1188 # Check for recent activity
1189 foreach ($this->backup_files_array as $files) {
1190 if (!is_array($files)) $files=array($files);
1191 foreach ($files as $file) $iwp_backup_core->check_recent_modification($this->iwp_backup_dir.'/'.$file);
1192 }
1193 } elseif ('begun' == $bfiles_status) {
1194 $this->first_run = apply_filters('IWP_filerun_firstrun', 0);
1195 if ($resumption_no > $this->first_run) {
1196 $iwp_backup_core->log("Creation of backups of directories: had begun; will resume");
1197 } else {
1198 $iwp_backup_core->log("Creation of backups of directories: beginning");
1199 }
1200 $iwp_backup_core->jobdata_set('jobstatus', 'filescreating');
1201 $this->backup_files_array = $this->backup_dirs($bfiles_status);
1202 $iwp_backup_core->jobdata_set('backup_files_array', $this->backup_files_array);
1203 $iwp_backup_core->jobdata_set('backup_files', 'finished');
1204 $iwp_backup_core->jobdata_set('jobstatus', 'filescreated');
1205 } else {
1206 # This is not necessarily a backup run which is meant to contain files at all
1207 $iwp_backup_core->log('This backup run is not intended for files - skipping');
1208 return array();
1209 }
1210 return $this->backup_files_array;
1211 }
1212
1213 /* This function is resumable, using the following method:
1214 - Each table is written out to ($final_filename).table.tmp
1215 - When the writing finishes, it is renamed to ($final_filename).table
1216 - When all tables are finished, they are concatenated into the final file
1217 */
1218 // dbinfo is only used when whichdb != 'wp'; and the keys should be: user, pass, name, host, prefix
1219 public function backup_db($already_done = 'begun', $whichdb = 'wp', $dbinfo = array()) {
1220
1221 global $iwp_backup_core, $wpdb;
1222
1223 $this->whichdb = $whichdb;
1224 $this->whichdb_suffix = ('wp' == $whichdb) ? '' : $whichdb;
1225
1226 if (!$iwp_backup_core->backup_time) $iwp_backup_core->backup_time_nonce();
1227 if (!$iwp_backup_core->opened_log_time) $iwp_backup_core->logfile_open($iwp_backup_core->nonce);
1228
1229 if ('wp' == $this->whichdb) {
1230 $this->wpdb_obj = $wpdb;
1231 # The table prefix after being filtered - i.e. what filters what we'll actually back up
1232 $this->table_prefix = $iwp_backup_core->get_table_prefix(true);
1233 # The unfiltered table prefix - i.e. the real prefix that things are relative to
1234 $this->table_prefix_raw = $iwp_backup_core->get_table_prefix(false);
1235 $dbinfo['host'] = DB_HOST;
1236 $dbinfo['name'] = DB_NAME;
1237 $dbinfo['user'] = DB_USER;
1238 $dbinfo['pass'] = DB_PASSWORD;
1239 } else {
1240 if (!is_array($dbinfo) || empty($dbinfo['host'])) return false;
1241 # The methods that we may use: check_connection (WP>=3.9), get_results, get_row, query
1242 $this->wpdb_obj = new IWP_MMB_WPDB_OtherDB($dbinfo['user'], $dbinfo['pass'], $dbinfo['name'], $dbinfo['host']);
1243 if (!empty($this->wpdb_obj->error)) {
1244 $iwp_backup_core->log($dbinfo['user'].'@'.$dbinfo['host'].'/'.$dbinfo['name'].' : database connection attempt failed');
1245 $iwp_backup_core->log($dbinfo['user'].'@'.$dbinfo['host'].'/'.$dbinfo['name'].' : '.__('database connection attempt failed.', 'iwp_backup_core').' '.__('Connection failed: check your access details, that the database server is up, and that the network connection is not firewalled.', 'iwp_backup_core'), 'error');
1246 return $iwp_backup_core->log_wp_error($this->wpdb_obj->error);
1247 }
1248 $this->table_prefix = $dbinfo['prefix'];
1249 $this->table_prefix_raw = $dbinfo['prefix'];
1250 }
1251
1252 $this->dbinfo = $dbinfo;
1253
1254 $errors = 0;
1255
1256 $use_time = apply_filters('IWP_backup_time_thisrun', $iwp_backup_core->backup_time);
1257 $file_base = $this->get_backup_file_basename_from_time($use_time);
1258 $backup_file_base = $this->iwp_backup_dir.'/'.$file_base;
1259
1260 if ('finished' == $already_done) return basename($backup_file_base).'-db'.(('wp' == $whichdb) ? '' : $whichdb).'.gz';
1261 if ('encrypted' == $already_done) return basename($backup_file_base).'-db'.(('wp' == $whichdb) ? '' : $whichdb).'.gz.crypt';
1262
1263 $iwp_backup_core->jobdata_set('jobstatus', 'dbcreating'.$this->whichdb_suffix);
1264
1265 $binsqldump = $iwp_backup_core->find_working_sqldump();
1266
1267 $total_tables = 0;
1268
1269 # 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
1270 if ('wp' == $whichdb && method_exists($this->wpdb_obj, 'check_connection') && (!defined('IWP_SUPPRESS_CONNECTION_CHECKS') || !IWP_SUPPRESS_CONNECTION_CHECKS)) {
1271 if (!$this->wpdb_obj->check_connection(false)) {
1272 $iwp_backup_core->reschedule(60);
1273 $iwp_backup_core->log("It seems the database went away; scheduling a resumption and terminating for now");
1274 $iwp_backup_core->record_still_alive();
1275 die;
1276 }
1277 }
1278
1279 // SHOW FULL - so that we get to know whether it's a BASE TABLE or a VIEW
1280 $all_tables = $this->wpdb_obj->get_results("SHOW FULL TABLES", ARRAY_N);
1281
1282 if (empty($all_tables) && !empty($this->wpdb_obj->last_error)) {
1283 $all_tables = $this->wpdb_obj->get_results("SHOW TABLES", ARRAY_N);
1284 $all_tables = array_map(array($this, 'cb_get_name_base_type'), $all_tables);
1285 } else {
1286 $all_tables = array_map(array($this, 'cb_get_name_type'), $all_tables);
1287 }
1288
1289 # If this is not the WP database, then we do not consider it a fatal error if there are no tables
1290 if ('wp' == $whichdb && 0 == count($all_tables)) {
1291 $extra = ($iwp_backup_core->newresumption_scheduled) ? ' - '.__('please wait for the rescheduled attempt', 'InfiniteWP') : '';
1292 $iwp_backup_core->log("Error: No WordPress database tables found (SHOW TABLES returned nothing)".$extra);
1293 $iwp_backup_core->log(__("No database tables found", 'InfiniteWP').$extra, 'error');
1294 die;
1295 }
1296
1297 // Put the options table first
1298 usort($all_tables, array($this, 'backup_db_sorttables'));
1299
1300 $all_table_names = array_map(array($this, 'cb_get_name'), $all_tables);
1301
1302 if (!$iwp_backup_core->really_is_writable($this->iwp_backup_dir)) {
1303 $iwp_backup_core->log("The backup directory (".$this->iwp_backup_dir.") could not be written to (could be account/disk space full, or wrong permissions).");
1304 $iwp_backup_core->log($this->iwp_backup_dir.": ".__('The backup directory is not writable (or disk space is full) - the database backup is expected to shortly fail.','InfiniteWP'), 'warning');
1305 # Why not just fail now? We saw a bizarre case when the results of really_is_writable() changed during the run.
1306 }
1307
1308 # 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
1309 $this->duplicate_tables_exist = false;
1310 foreach ($all_table_names as $table) {
1311 if (strtolower($table) != $table && in_array(strtolower($table), $all_table_names)) {
1312 $this->duplicate_tables_exist = true;
1313 $iwp_backup_core->log("Tables with names differing only based on case-sensitivity exist in the MySQL database: $table / ".strtolower($table));
1314 }
1315 }
1316 $how_many_tables = count($all_tables);
1317
1318 $stitch_files = array();
1319 $found_options_table = false;
1320 $is_multisite = is_multisite();
1321
1322 foreach ($all_tables as $ti) {
1323
1324 $table = $ti['name'];
1325 $table_type = $ti['type'];
1326
1327 $manyrows_warning = false;
1328 $total_tables++;
1329
1330 // Increase script execution time-limit to 15 min for every table.
1331 @set_time_limit(IWP_SET_TIME_LIMIT);
1332 // The table file may already exist if we have produced it on a previous run
1333 $table_file_prefix = $file_base.'-db'.$this->whichdb_suffix.'-table-'.$table.'.table';
1334
1335 if ('wp' == $whichdb && (strtolower($this->table_prefix_raw.'options') == strtolower($table) || ($is_multisite && (strtolower($this->table_prefix_raw.'sitemeta') == strtolower($table) || strtolower($this->table_prefix_raw.'1_options') == strtolower($table))))) $found_options_table = true;
1336
1337 if (file_exists($this->iwp_backup_dir.'/'.$table_file_prefix.'.gz')) {
1338 $stitched = count($stitch_files);
1339 $skip_dblog = (($stitched > 10 && 0 != $stitched % 20) || ($stitched > 100 && 0 != $stitched % 100));
1340 $iwp_backup_core->log("Table $table: corresponding file already exists; moving on", 'notice', false, $skip_dblog);
1341 $stitch_files[] = $table_file_prefix;
1342 } else {
1343 # === is needed, otherwise 'false' matches (i.e. prefix does not match)
1344 if (empty($this->table_prefix) || ($this->duplicate_tables_exist == false && stripos($table, $this->table_prefix) === 0 ) || ($this->duplicate_tables_exist == true && strpos($table, $this->table_prefix) === 0 ) ) {
1345
1346 if (!apply_filters('IWP_backup_table', true, $table, $this->table_prefix, $whichdb, $dbinfo)) {
1347 $iwp_backup_core->log("Skipping table (filtered): $table");
1348 if (empty($this->skipped_tables)) $this->skipped_tables = array();
1349
1350 // whichdb could be an int in which case to get the name of the database and the array key use the name from dbinfo
1351 if ('wp' !== $whichdb) {
1352 $key = $dbinfo['name'];
1353 } else {
1354 $key = $whichdb;
1355 }
1356
1357 if (empty($this->skipped_tables[$key])) $this->skipped_tables[$key] = '';
1358 if ('' != $this->skipped_tables[$key]) $this->skipped_tables[$key] .= ',';
1359 $this->skipped_tables[$key] .= $table;
1360
1361 $total_tables--;
1362 } else {
1363
1364 $db_temp_file = $this->iwp_backup_dir.'/'.$table_file_prefix.'.tmp.gz';
1365 $iwp_backup_core->check_recent_modification($db_temp_file);
1366
1367 // Open file, store the handle
1368 $opened = $this->backup_db_open($db_temp_file, true);
1369 if (false === $opened) return false;
1370
1371 // Create the SQL statements
1372 $this->stow("# " . sprintf('Table: %s' ,$iwp_backup_core->backquote($table)) . "\n");
1373 $iwp_backup_core->jobdata_set('dbcreating_substatus', array('t' => $table, 'i' => $total_tables, 'a' => $how_many_tables));
1374
1375 $table_status = $this->wpdb_obj->get_row("SHOW TABLE STATUS WHERE Name='$table'");
1376 if (isset($table_status->Rows)) {
1377 $rows = $table_status->Rows;
1378 $iwp_backup_core->log("Table $table: Total expected rows (approximate): ".$rows);
1379 $this->stow("# Approximate rows expected in table: $rows\n");
1380 if ($rows > IWP_WARN_DB_ROWS) {
1381 $manyrows_warning = true;
1382 $iwp_backup_core->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", 'InfiniteWP'), $table, $rows).' '.__('If not, you will need to either remove data from this table, or contact your hosting company to request more resources.', 'InfiniteWP'), 'warning', 'manyrows_'.$this->whichdb_suffix.$table);
1383 }
1384 }
1385
1386 # Don't include the job data for any backups - so that when the database is restored, it doesn't continue an apparently incomplete backup
1387 if ('wp' == $this->whichdb && (!empty($this->table_prefix) && strtolower($this->table_prefix.'sitemeta') == strtolower($table))) {
1388 $where = 'meta_key NOT LIKE "IWP_jobdata_%"';
1389 } elseif ('wp' == $this->whichdb && (!empty($this->table_prefix) && strtolower($this->table_prefix.'options') == strtolower($table))) {
1390 if (strtolower(substr(PHP_OS, 0, 3)) == 'win') {
1391 $IWP_jobdata = "'IWP_jobdata_%'";
1392 $site_transient_update = "'_site_transient_update_%'";
1393 } else {
1394 $IWP_jobdata = '"IWP_jobdata_%"';
1395 $site_transient_update = '"_site_transient_update_%"';
1396 }
1397
1398 $where = 'option_name NOT LIKE '.$IWP_jobdata.' AND option_name NOT LIKE '.$site_transient_update.'';
1399 } else {
1400 $where = '';
1401 }
1402
1403 # 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??)
1404
1405 # 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
1406 $bindump_threshold = (!$iwp_backup_core->something_useful_happened && !empty($iwp_backup_core->current_resumption) && ($iwp_backup_core->current_resumption - $iwp_backup_core->last_successful_resumption == 2 )) ? 1000 : 8000;
1407
1408 $bindump = (isset($rows) && ($rows>$bindump_threshold || (defined('IWP_ALWAYS_TRY_MYSQLDUMP') && IWP_ALWAYS_TRY_MYSQLDUMP)) && is_string($binsqldump)) ? $this->backup_table_bindump($binsqldump, $table, $where) : false;
1409 if (true !== $bindump) $this->backup_table($table, $where, 'none', $table_type);
1410
1411 if (!empty($manyrows_warning)) $iwp_backup_core->log_removewarning('manyrows_'.$this->whichdb_suffix.$table);
1412
1413 $this->close();
1414
1415 $iwp_backup_core->log("Table $table: finishing file (".$table_file_prefix.".gz - ".round(filesize($this->iwp_backup_dir.'/'.$table_file_prefix.'.tmp.gz')/1024,1)." KB)");
1416
1417 rename($db_temp_file, $this->iwp_backup_dir.'/'.$table_file_prefix.'.gz');
1418 $iwp_backup_core->something_useful_happened();
1419 $stitch_files[] = $table_file_prefix;
1420 }
1421 } else {
1422 $total_tables--;
1423 $iwp_backup_core->log("Skipping table (lacks our prefix (".$this->table_prefix.")): $table");
1424 }
1425
1426 }
1427 }
1428
1429 if ('wp' == $whichdb) {
1430 if (!$found_options_table) {
1431 if ($is_multisite) {
1432 $iwp_backup_core->log(__('The database backup appears to have failed', 'InfiniteWP').' - '.__('no options or sitemeta table was found', 'InfiniteWP'), 'warning', 'optstablenotfound');
1433 } else {
1434 $iwp_backup_core->log(__('The database backup appears to have failed', 'InfiniteWP').' - '.__('the options table was not found', 'InfiniteWP'), 'warning', 'optstablenotfound');
1435 }
1436 $time_this_run = time()-$iwp_backup_core->opened_log_time;
1437 if ($time_this_run > 2000) {
1438 # 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.
1439 # If we have been running that long, then the resumption may be far off; bring it closer
1440 $iwp_backup_core->reschedule(60);
1441 $iwp_backup_core->log("Have been running very long, and it seems the database went away; scheduling a resumption and terminating for now");
1442 $iwp_backup_core->record_still_alive();
1443 die;
1444 }
1445 } else {
1446 $iwp_backup_core->log_removewarning('optstablenotfound');
1447 }
1448 }
1449
1450 // Race detection - with zip files now being resumable, these can more easily occur, with two running side-by-side
1451 $backup_final_file_name = $backup_file_base.'-db'.$this->whichdb_suffix.'.gz';
1452 $time_now = time();
1453 $time_mod = (int)@filemtime($backup_final_file_name);
1454 if (file_exists($backup_final_file_name) && $time_mod>100 && ($time_now-$time_mod)<30) {
1455 $iwp_backup_core->terminate_due_to_activity($backup_final_file_name, $time_now, $time_mod);
1456 }
1457 if (file_exists($backup_final_file_name)) {
1458 $iwp_backup_core->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 Backup terminated; thus we will continue.");
1459 }
1460
1461 // Finally, stitch the files together
1462 if (!function_exists('gzopen')) {
1463 if (function_exists('gzopen64')) {
1464 $iwp_backup_core->log("PHP function is disabled; abort expected: gzopen - buggy Ubuntu PHP version; try this plugin to help: https://wordpress.org/plugins/wp-ubuntu-gzopen-fix/");
1465 } else {
1466 $iwp_backup_core->log("PHP function is disabled; abort expected: gzopen");
1467 }
1468 }
1469
1470 if (false === $this->backup_db_open($backup_final_file_name, true)) return false;
1471
1472 $this->backup_db_header();
1473
1474 // 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
1475 $unlink_files = array();
1476
1477 $sind = 1;
1478 foreach ($stitch_files as $table_file) {
1479 $iwp_backup_core->log("{$table_file}.gz ($sind/$how_many_tables): adding to final database dump");
1480 if (!$handle = gzopen($this->iwp_backup_dir.'/'.$table_file.'.gz', "r")) {
1481 $iwp_backup_core->log("Error: Failed to open database file for reading: ".$table_file.".gz");
1482 $iwp_backup_core->log(__("Failed to open database file for reading:", 'InfiniteWP').' '.$table_file.'.gz', 'error');
1483 $errors++;
1484 } else {
1485 while ($line = gzgets($handle, 65536)) { $this->stow($line); }
1486 gzclose($handle);
1487 $unlink_files[] = $this->iwp_backup_dir.'/'.$table_file.'.gz';
1488 }
1489 $sind++;
1490 // Came across a database with 7600 tables... adding them all took over 500 seconds; and so when the resumption started up, no activity was detected
1491 if ($sind % 100 == 0) $iwp_backup_core->something_useful_happened();
1492 }
1493
1494 if (defined('DB_CHARSET') && DB_CHARSET) {
1495 $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");
1496 }
1497
1498 $iwp_backup_core->log($file_base.'-db'.$this->whichdb_suffix.'.gz: finished writing out complete database file ('.round(filesize($backup_final_file_name)/1024,1).' KB)');
1499 if (!$this->close()) {
1500 $iwp_backup_core->log('An error occurred whilst closing the final database file');
1501 $iwp_backup_core->log(__('An error occurred whilst closing the final database file', 'InfiniteWP'), 'error');
1502 $errors++;
1503 }
1504
1505 foreach ($unlink_files as $unlink_file) @unlink($unlink_file);
1506
1507 if ($errors > 0) {
1508 return false;
1509 } else {
1510 # We no longer encrypt here - because the operation can take long, we made it resumable and moved it to the upload loop
1511 $iwp_backup_core->jobdata_set('jobstatus', 'dbcreated'.$this->whichdb_suffix);
1512
1513 $checksums = $iwp_backup_core->which_checksums();
1514
1515 $checksum_description = '';
1516
1517 foreach ($checksums as $checksum) {
1518
1519 $cksum = hash_file($checksum, $backup_final_file_name);
1520 $iwp_backup_core->jobdata_set($checksum.'-db'.(('wp' == $whichdb) ? '0' : $whichdb.'0'), $cksum);
1521 if ($checksum_description) $checksum_description .= ', ';
1522 $checksum_description .= "$checksum: $cksum";
1523
1524 }
1525
1526 $iwp_backup_core->log("Total database tables backed up: $total_tables (".basename($backup_final_file_name).", size: ".filesize($backup_final_file_name).", $checksum)");
1527
1528 return basename($backup_final_file_name);
1529 }
1530
1531 }
1532
1533 private function backup_table_bindump($potsql, $table_name, $where) {
1534
1535 $microtime = microtime(true);
1536
1537 global $iwp_backup_core;
1538 $table_sans_prefix = substr($table_name, strlen($this->table_prefix_raw));
1539 $default_exclude_tables = explode(',', IWP_MMB_Backup_Options::get_iwp_backup_option('IWP_default_exclude_tables', IWP_DATA_OPTIONAL_TABLES));
1540 if (in_array($table_sans_prefix, $default_exclude_tables)) {
1541 $iwp_backup_core->log("Table $table_name: Data skipped (table is marked as non-essential)");
1542 return true;
1543 }
1544
1545 // Deal with Windows/old MySQL setups with erroneous table prefixes differing in case
1546 // Can't get binary mysqldump to make this transformation
1547 // $dump_as_table = ($this->duplicate_tables_exist == false && stripos($table, $this->table_prefix) === 0 && strpos($table, $this->table_prefix) !== 0) ? $this->table_prefix.substr($table, strlen($this->table_prefix)) : $table;
1548
1549 $pfile = md5(time().rand()).'.tmp';
1550 file_put_contents($this->iwp_backup_dir.'/'.$pfile, "[mysqldump]\npassword=".$this->dbinfo['pass']."\n");
1551
1552 // Note: escapeshellarg() adds quotes around the string
1553 if ($where) $where="--where=".escapeshellarg($where);
1554
1555 if (strtolower(substr(PHP_OS, 0, 3)) == 'win') {
1556 $exec = "cd ".escapeshellarg(str_replace('/', '\\', $this->iwp_backup_dir))." & ";
1557 } else {
1558 $exec = "cd ".escapeshellarg($this->iwp_backup_dir)."; ";
1559 }
1560
1561 $exec .= "$potsql --defaults-file=$pfile $where --max_allowed_packet=1M --quote-names --add-drop-table --skip-comments --skip-set-charset --allow-keywords --dump-date --extended-insert --user=".escapeshellarg($this->dbinfo['user'])." --host=".escapeshellarg($this->dbinfo['host'])." ".$this->dbinfo['name']." ".escapeshellarg($table_name);
1562
1563 $ret = false;
1564 $any_output = false;
1565 $writes = 0;
1566 $handle = popen($exec, "r");
1567 if ($handle) {
1568 while (!feof($handle)) {
1569 $w = fgets($handle);
1570 if ($w) {
1571 $this->stow($w);
1572 $writes++;
1573 $any_output = true;
1574 }
1575 }
1576 $ret = pclose($handle);
1577 if ($ret != 0) {
1578 $iwp_backup_core->log("Binary mysqldump: error (code: $ret)");
1579 // Keep counter of failures? Change value of binsqldump?
1580 } else {
1581 if ($any_output) {
1582 $iwp_backup_core->log("Table $table_name: binary mysqldump finished (writes: $writes) in ".sprintf("%.02f",max(microtime(true)-$microtime,0.00001))." seconds");
1583 $ret = true;
1584 }
1585 }
1586 } else {
1587 $iwp_backup_core->log("Binary mysqldump error: bindump popen failed");
1588 }
1589
1590 # Clean temporary files
1591 @unlink($this->iwp_backup_dir.'/'.$pfile);
1592
1593 return $ret;
1594
1595 }
1596
1597 /**
1598 * Taken partially from phpMyAdmin and partially from
1599 * Alain Wolf, Zurich - Switzerland
1600 * Website: http://restkultur.ch/personal/wolf/scripts/db_backup/
1601 * Modified by Scott Merrill (http://www.skippy.net/)
1602 * to use the WordPress $wpdb object
1603 * @param string $table
1604 * @param string $segment
1605 * @return void
1606 */
1607 private function backup_table($table, $where = '', $segment = 'none', $table_type = 'BASE TABLE') {
1608 global $iwp_backup_core;
1609
1610 $microtime = microtime(true);
1611 $total_rows = 0;
1612
1613 // Deal with Windows/old MySQL setups with erroneous table prefixes differing in case
1614 $dump_as_table = ($this->duplicate_tables_exist == false && stripos($table, $this->table_prefix) === 0 && strpos($table, $this->table_prefix) !== 0) ? $this->table_prefix.substr($table, strlen($this->table_prefix)) : $table;
1615
1616 $table_structure = $this->wpdb_obj->get_results("DESCRIBE ".$iwp_backup_core->backquote($table));
1617 if (! $table_structure) {
1618 return false;
1619 }
1620
1621 if($segment == 'none' || $segment == 0) {
1622 // Add SQL statement to drop existing table
1623 $this->stow("\n# Delete any existing table ".$iwp_backup_core->backquote($table)."\n\n");
1624 $this->stow("DROP TABLE IF EXISTS " . $iwp_backup_core->backquote($dump_as_table) . ";\n");
1625
1626 if ('VIEW' == $table_type) {
1627 $this->stow("DROP VIEW IF EXISTS " . $iwp_backup_core->backquote($dump_as_table) . ";\n");
1628 }
1629
1630 // Table structure
1631 // Comment in SQL-file
1632
1633 $description = ('VIEW' == $table_type) ? 'view' : 'table';
1634
1635 $this->stow("\n# Table structure of $description ".$iwp_backup_core->backquote($table)."\n\n");
1636
1637 $create_table = $this->wpdb_obj->get_results("SHOW CREATE TABLE ".$iwp_backup_core->backquote($table), ARRAY_N);
1638 if (false === $create_table) {
1639 $err_msg ='Error with SHOW CREATE TABLE for '.$table;
1640 //$iwp_backup_core->log($err_msg, 'error');
1641 $this->stow("#\n# $err_msg\n#\n");
1642 }
1643 $create_line = $iwp_backup_core->str_lreplace('TYPE=', 'ENGINE=', $create_table[0][1]);
1644
1645 # Remove PAGE_CHECKSUM parameter from MyISAM - was internal, undocumented, later removed (so causes errors on import)
1646 if (preg_match('/ENGINE=([^\s;]+)/', $create_line, $eng_match)) {
1647 $engine = $eng_match[1];
1648 if ('myisam' == strtolower($engine)) {
1649 $create_line = preg_replace('/PAGE_CHECKSUM=\d\s?/', '', $create_line, 1);
1650 }
1651 }
1652
1653 if ($dump_as_table !== $table) $create_line = $iwp_backup_core->str_replace_once($table, $dump_as_table, $create_line);
1654
1655 $this->stow($create_line.' ;');
1656
1657 if (false === $table_structure) {
1658 $err_msg = sprintf("Error getting $description structure of %s", $table);
1659 $this->stow("#\n# $err_msg\n#\n");
1660 }
1661
1662 // Comment in SQL-file
1663 $this->stow("\n\n# " . sprintf("Data contents of $description %s", $iwp_backup_core->backquote($table)) . "\n\n");
1664
1665 }
1666
1667 # Some tables have optional data, and should be skipped if they do not work
1668 $table_sans_prefix = substr($table, strlen($this->table_prefix_raw));
1669 $data_optional_tables = ('wp' == $this->whichdb) ? explode(',', IWP_MMB_Backup_Options::get_iwp_backup_option('IWP_default_exclude_tables', IWP_DATA_OPTIONAL_TABLES)) : array();
1670 if (in_array($table_sans_prefix, $data_optional_tables)) {
1671 //if (!$iwp_backup_core->something_useful_happened && !empty($iwp_backup_core->current_resumption) && ($iwp_backup_core->current_resumption - $iwp_backup_core->last_successful_resumption > 2)) {
1672 // $iwp_backup_core->log("Table $table: Data skipped (previous attempts failed, and table is marked as non-essential)");
1673 $iwp_backup_core->log("Table $table: Data skipped (table is marked as non-essential)");
1674 return true;
1675 //}
1676 }
1677
1678 if('VIEW' != $table_type && ($segment == 'none' || $segment >= 0)) {
1679 $defs = array();
1680 $integer_fields = array();
1681 // $table_structure was from "DESCRIBE $table"
1682 foreach ($table_structure as $struct) {
1683 if ( (0 === strpos($struct->Type, 'tinyint')) || (0 === strpos(strtolower($struct->Type), 'smallint')) ||
1684 (0 === strpos(strtolower($struct->Type), 'mediumint')) || (0 === strpos(strtolower($struct->Type), 'int')) || (0 === strpos(strtolower($struct->Type), 'bigint')) ) {
1685 $defs[strtolower($struct->Field)] = ( null === $struct->Default ) ? 'NULL' : $struct->Default;
1686 $integer_fields[strtolower($struct->Field)] = "1";
1687 }
1688 }
1689
1690 // 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%)
1691
1692 $increment = 1000;
1693 if (!$iwp_backup_core->something_useful_happened && !empty($iwp_backup_core->current_resumption) && ($iwp_backup_core->current_resumption - $iwp_backup_core->last_successful_resumption > 1)) {
1694 # 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.
1695 $increment = ( $iwp_backup_core->current_resumption - $iwp_backup_core->last_successful_resumption > 2 ) ? 350 : 500;
1696 }
1697
1698 if($segment == 'none') {
1699 $row_start = 0;
1700 $row_inc = $increment;
1701 } else {
1702 $row_start = $segment * $increment;
1703 $row_inc = $increment;
1704 }
1705
1706 $search = array("\x00", "\x0a", "\x0d", "\x1a");
1707 $replace = array('\0', '\n', '\r', '\Z');
1708
1709 if ($where) $where = "WHERE $where";
1710
1711 do {
1712 @set_time_limit(IWP_SET_TIME_LIMIT);
1713
1714 $table_data = $this->wpdb_obj->get_results("SELECT * FROM ".$iwp_backup_core->backquote($table)." $where LIMIT {$row_start}, {$row_inc}", ARRAY_A);
1715 $entries = 'INSERT INTO ' . $iwp_backup_core->backquote($dump_as_table) . ' VALUES ';
1716 // \x08\\x09, not required
1717 if($table_data) {
1718 $thisentry = "";
1719 foreach ($table_data as $row) {
1720 $total_rows++;
1721 $values = array();
1722 foreach ($row as $key => $value) {
1723 if (isset($integer_fields[strtolower($key)])) {
1724 // make sure there are no blank spots in the insert syntax,
1725 // yet try to avoid quotation marks around integers
1726 $value = ( null === $value || '' === $value) ? $defs[strtolower($key)] : $value;
1727 $values[] = ( '' === $value ) ? "''" : $value;
1728 } else {
1729 $values[] = (null === $value) ? 'NULL' : "'" . str_replace($search, $replace, str_replace('\'', '\\\'', str_replace('\\', '\\\\', $value))) . "'";
1730 }
1731 }
1732 if ($thisentry) $thisentry .= ", ";
1733 $thisentry .= '('.implode(', ', $values).')';
1734 // Flush every 512KB
1735 if (strlen($thisentry) > 524288) {
1736 $this->stow(" \n".$entries.$thisentry.';');
1737 $thisentry = "";
1738 }
1739
1740 }
1741 if ($thisentry) $this->stow(" \n".$entries.$thisentry.';');
1742 $row_start += $row_inc;
1743 }
1744 } while(count($table_data) > 0 && 'none' == $segment);
1745 }
1746
1747 if(($segment == 'none') || ($segment < 0)) {
1748 // Create footer/closing comment in SQL-file
1749 $this->stow("\n");
1750 $this->stow("# End of data contents of table ".$iwp_backup_core->backquote($table) . "\n");
1751 $this->stow("\n");
1752 }
1753 $iwp_backup_core->log("Table $table: Total rows added: $total_rows in ".sprintf("%.02f",max(microtime(true)-$microtime,0.00001))." seconds");
1754
1755 } // end backup_table()
1756
1757
1758 /*END OF WP-DB-BACKUP BLOCK */
1759
1760 // Encrypts the file if the option is set; returns the basename of the file (according to whether it was encrypted or nto)
1761 public function encrypt_file($file) {
1762 global $iwp_backup_core;
1763 $encryption = IWP_MMB_Backup_Options::get_iwp_backup_option('IWP_encryptionphrase');
1764 if (strlen($encryption) > 0) {
1765 $iwp_backup_core->log("Attempting to encrypt backup file");
1766 try {
1767 $result = apply_filters('IWP_encrypt_file', null, $file, $encryption, $this->whichdb, $this->whichdb_suffix);
1768 } catch (Exception $e) {
1769 $log_message = 'Exception ('.get_class($e).') occurred during encryption: '.$e->getMessage().' (Code: '.$e->getCode().', line '.$e->getLine().' in '.$e->getFile().')';
1770 error_log($log_message);
1771 // @codingStandardsIgnoreLine
1772 if (function_exists('wp_debug_backtrace_summary')) $log_message .= ' Backtrace: '.wp_debug_backtrace_summary();
1773 $iwp_backup_core->log($log_message);
1774 $iwp_backup_core->log(sprintf(__('A PHP exception (%s) has occurred: %s', 'InfiniteWP'), get_class($e), $e->getMessage()), 'error');
1775 die();
1776 // @codingStandardsIgnoreLine
1777 } catch (Error $e) {
1778 $log_message = 'PHP Fatal error ('.get_class($e).') has occurred during encryption. Error Message: '.$e->getMessage().' (Code: '.$e->getCode().', line '.$e->getLine().' in '.$e->getFile().')';
1779 error_log($log_message);
1780 // @codingStandardsIgnoreLine
1781 if (function_exists('wp_debug_backtrace_summary')) $log_message .= ' Backtrace: '.wp_debug_backtrace_summary();
1782 $iwp_backup_core->log($log_message);
1783 $iwp_backup_core->log(sprintf(__('A PHP fatal error (%s) has occurred: %s', 'InfiniteWP'), get_class($e), $e->getMessage()), 'error');
1784 die();
1785 }
1786 if (null === $result) {
1787 return basename($file);
1788 }
1789 return $result;
1790 } else {
1791 return basename($file);
1792 }
1793 }
1794
1795 public function close() {
1796 return ($this->dbhandle_isgz) ? gzclose($this->dbhandle) : fclose($this->dbhandle);
1797 }
1798
1799 // Open a file, store its filehandle
1800 public function backup_db_open($file, $allow_gz = true) {
1801 if (function_exists('gzopen') && $allow_gz == true) {
1802 $this->dbhandle = @gzopen($file, 'w');
1803 $this->dbhandle_isgz = true;
1804 } else {
1805 $this->dbhandle = @fopen($file, 'w');
1806 $this->dbhandle_isgz = false;
1807 }
1808 if(false === $this->dbhandle) {
1809 global $iwp_backup_core;
1810 $iwp_backup_core->log("ERROR: $file: Could not open the backup file for writing");
1811 $iwp_backup_core->log($file.": ".__("Could not open the backup file for writing",'InfiniteWP'), 'error');
1812 }
1813 return $this->dbhandle;
1814 }
1815
1816 public function stow($query_line) {
1817 if ($this->dbhandle_isgz) {
1818 if(false == ($ret = @gzwrite($this->dbhandle, $query_line))) {
1819 //$iwp_backup_core->log(__('There was an error writing a line to the backup script:','wp-db-backup') . ' ' . $query_line . ' ' . $php_errormsg, 'error');
1820 }
1821 } else {
1822 if(false == ($ret = @fwrite($this->dbhandle, $query_line))) {
1823 //$iwp_backup_core->log(__('There was an error writing a line to the backup script:','wp-db-backup') . ' ' . $query_line . ' ' . $php_errormsg, 'error');
1824 }
1825 }
1826 return $ret;
1827 }
1828
1829 private function backup_db_header() {
1830
1831 @include(ABSPATH.WPINC.'/version.php');
1832 global $wp_version, $iwp_backup_core;
1833
1834 $mysql_version = $this->wpdb_obj->db_version();
1835 # (function_exists('mysql_get_server_info')) ? @mysql_get_server_info() : '?';
1836
1837 if ('wp' == $this->whichdb) {
1838 $wp_upload_dir = wp_upload_dir();
1839 $this->stow("# WordPress MySQL database backup\n");
1840 $this->stow("# Created by InfiniteWP version ".$iwp_backup_core->version."\n");
1841 $this->stow("# WordPress Version: $wp_version, running on PHP ".phpversion()." (".$_SERVER["SERVER_SOFTWARE"]."), MySQL $mysql_version\n");
1842 $this->stow("# Backup of: ".untrailingslashit(site_url())."\n");
1843 $this->stow("# Home URL: ".untrailingslashit(home_url())."\n");
1844 $this->stow("# Content URL: ".untrailingslashit(content_url())."\n");
1845 $this->stow("# Uploads URL: ".untrailingslashit($wp_upload_dir['baseurl'])."\n");
1846 $this->stow("# Table prefix: ".$this->table_prefix_raw."\n");
1847 $this->stow("# Filtered table prefix: ".$this->table_prefix."\n");
1848 $this->stow("# Site info: multisite=".(is_multisite() ? '1' : '0')."\n");
1849 $this->stow("# Site info: end\n");
1850 } else {
1851 $this->stow("# MySQL database backup (supplementary database ".$this->whichdb.")\n");
1852 $this->stow("# Created by InfiniteWP version ".$iwp_backup_core->version."\n");
1853 $this->stow("# WordPress Version: $wp_version, running on PHP ".phpversion()." (".$_SERVER["SERVER_SOFTWARE"]."), MySQL $mysql_version\n");
1854 $this->stow("# ".sprintf('External database: (%s)', $this->dbinfo['user'].'@'.$this->dbinfo['host'].'/'.$this->dbinfo['name'])."\n");
1855 $this->stow("# Backup created by: ".untrailingslashit(site_url())."\n");
1856 $this->stow("# Table prefix: ".$this->table_prefix_raw."\n");
1857 $this->stow("# Filtered table prefix: ".$this->table_prefix."\n");
1858 }
1859
1860 $label = $iwp_backup_core->jobdata_get('label');
1861 if (!empty($label)) $this->stow("# Label: $label\n");
1862
1863 $this->stow("#\n");
1864 $this->stow("# Generated: ".date("l j. F Y H:i T")."\n");
1865 $this->stow("# Hostname: ".$this->dbinfo['host']."\n");
1866 $this->stow("# Database: ".$iwp_backup_core->backquote($this->dbinfo['name'])."\n");
1867
1868 if (!empty($this->skipped_tables)) {
1869 if ('wp' == $this->whichdb) {
1870 $this->stow("# Skipped tables: " . $this->skipped_tables[$this->whichdb]."\n");
1871 } elseif (isset($this->skipped_tables[$this->dbinfo['name']])) {
1872 $this->stow("# Skipped tables: " . $this->skipped_tables[$this->dbinfo['name']]."\n");
1873 }
1874 }
1875
1876 $this->stow("# --------------------------------------------------------\n");
1877
1878 if (defined("DB_CHARSET") && DB_CHARSET) {
1879 $this->stow("/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;\n");
1880 $this->stow("/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;\n");
1881 $this->stow("/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;\n");
1882 $this->stow("/*!40101 SET NAMES " . DB_CHARSET . " */;\n");
1883 }
1884 $this->stow("/*!40101 SET foreign_key_checks = 0 */;\n\n");
1885 }
1886
1887 private function makezip_recursive_add($fullpath, $use_path_when_storing, $original_fullpath, $startlevels, &$exclude) {
1888
1889 // $zipfile = $this->zip_basename.(($this->index == 0) ? '' : ($this->index+1)).'.zip.tmp';
1890
1891 global $iwp_backup_core;
1892
1893 // Only BinZip supports symlinks. This means that as a consistent outcome, the only think that can be done with directory symlinks is either a) potentially duplicate the data or b) skip it. Whilst with internal WP entities (e.g. plugins) we definitely want the data, in the case of user-selected directories, we assume the user knew what they were doing when they chose the directory - i.e. we can skip symlink-accessed data that's outside.
1894 if (is_link($fullpath) && is_dir($fullpath) && 'more' == $this->whichone) {
1895 $iwp_backup_core->log("Directory symlink encounted in more files backup: $use_path_when_storing -> ".readlink($fullpath).": skipping");
1896 return true;
1897 }
1898
1899 // 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
1900 $fullpath = realpath($fullpath);
1901 $original_fullpath = realpath($original_fullpath);
1902
1903 // Is the place we've ended up above the original base? That leads to infinite recursion
1904 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)))) {
1905 $iwp_backup_core->log("Infinite recursion: symlink led us to $fullpath, which is within $original_fullpath");
1906 $iwp_backup_core->log(__("Infinite recursion: consult your log for more information",'InfiniteWP'), 'error');
1907 return false;
1908 }
1909
1910 # This is sufficient for the ones we have exclude options for - uploads, others, wpcore
1911 $stripped_storage_path = (1 == $startlevels) ? $use_path_when_storing : substr($use_path_when_storing, strpos($use_path_when_storing, '/') + 1);
1912 if (false !== ($fkey = array_search($stripped_storage_path, $exclude))) {
1913 $iwp_backup_core->log("Entity excluded by configuration option: $stripped_storage_path");
1914 unset($exclude[$fkey]);
1915 return true;
1916 }
1917
1918 $if_altered_since = $this->makezip_if_altered_since;
1919
1920 if (is_file($fullpath)) {
1921 if (!empty($this->excluded_extensions) && $this->is_entity_excluded_by_extension($fullpath)) {
1922 $iwp_backup_core->log("Entity excluded by configuration option (extension): ".basename($fullpath));
1923 } elseif (!empty($this->excluded_prefixes) && $this->is_entity_excluded_by_prefix($fullpath)) {
1924 $iwp_backup_core->log("Entity excluded by configuration option (prefix): ".basename($fullpath));
1925 } elseif (apply_filters('IWP_exclude_file', false, $fullpath)) {
1926 $iwp_backup_core->log("Entity excluded by filter: ".basename($fullpath));
1927 } elseif (is_readable($fullpath)) {
1928 $mtime = filemtime($fullpath);
1929 $key = ($fullpath == $original_fullpath) ? ((2 == $startlevels) ? $use_path_when_storing : $this->basename($fullpath)) : $use_path_when_storing.'/'.$this->basename($fullpath);
1930 if ($mtime > 0 && $mtime > $if_altered_since) {
1931 $this->zipfiles_batched[$fullpath] = $key;
1932 $this->makezip_recursive_batchedbytes += @filesize($fullpath);
1933 #@touch($zipfile);
1934 } else {
1935 $this->zipfiles_skipped_notaltered[$fullpath] = $key;
1936 }
1937 } else {
1938 $iwp_backup_core->log("$fullpath: unreadable file");
1939 $iwp_backup_core->log(sprintf(__("%s: unreadable file - could not be backed up (check the file permissions and ownership)", 'iwp_backup_core'), $fullpath), 'warning');
1940 }
1941 } elseif (is_dir($fullpath)) {
1942 if ($fullpath == $this->iwp_backup_dir_realpath) {
1943 $iwp_backup_core->log("Skip directory (InfiniteWP backup directory): $use_path_when_storing");
1944 return true;
1945 }
1946
1947 if (apply_filters('IWP_exclude_directory', false, $fullpath)) {
1948 $iwp_backup_core->log("Skip filtered directory: $use_path_when_storing");
1949 return true;
1950 }
1951
1952 if (file_exists($fullpath.'/.donotbackup')) {
1953 $iwp_backup_core->log("Skip directory (.donotbackup file found): $use_path_when_storing");
1954 return true;
1955 }
1956
1957 if (!isset($this->existing_files[$use_path_when_storing])) $this->zipfiles_dirbatched[] = $use_path_when_storing;
1958
1959 if (!$dir_handle = @opendir($fullpath)) {
1960 $iwp_backup_core->log("Failed to open directory: $fullpath");
1961 $iwp_backup_core->log(sprintf(__("Failed to open directory (check the file permissions and ownership): %s",'iwp_backup_dir'), $fullpath), 'error');
1962 return false;
1963 }
1964
1965 while (false !== ($e = readdir($dir_handle))) {
1966 if ('.' == $e || '..' == $e) continue;
1967 // if ($this->whichone == 'more') {
1968 // if (!in_array(str_replace(ABSPATH, '', $e), explode(',',IWP_DEFAULT_INCLUDES))) {
1969 // continue;
1970 // }
1971 // }
1972
1973 if (is_link($fullpath.'/'.$e)) {
1974 $deref = realpath($fullpath.'/'.$e);
1975 if (is_file($deref)) {
1976 if (is_readable($deref)) {
1977 $use_stripped = $stripped_storage_path.'/'.$e;
1978 if (false !== ($fkey = array_search($use_stripped, $exclude))) {
1979 $iwp_backup_core->log("Entity excluded by configuration option: $use_stripped");
1980 unset($exclude[$fkey]);
1981 } elseif (!empty($this->excluded_extensions) && $this->is_entity_excluded_by_extension($e)) {
1982 $iwp_backup_core->log("Entity excluded by configuration option (extension): $use_stripped");
1983 } elseif (!empty($this->excluded_prefixes) && $this->is_entity_excluded_by_prefix($e)) {
1984 $iwp_backup_core->log("Entity excluded by configuration option (prefix): $use_stripped");
1985 } elseif (apply_filters('IWP_exclude_file', false, $deref)) {
1986 $iwp_backup_core->log("Entity excluded by filter: $use_stripped");
1987 } else {
1988 $mtime = filemtime($deref);
1989 if ($mtime > 0 && $mtime > $if_altered_since) {
1990 $this->zipfiles_batched[$deref] = $use_path_when_storing.'/'.$e;
1991 $this->makezip_recursive_batchedbytes += @filesize($deref);
1992 #@touch($zipfile);
1993 } else {
1994 $this->zipfiles_skipped_notaltered[$deref] = $use_path_when_storing.'/'.$e;
1995 }
1996 }
1997 } else {
1998 $iwp_backup_core->log("$deref: unreadable file");
1999 $iwp_backup_core->log(sprintf(__("%s: unreadable file - could not be backed up"), $deref), 'warning');
2000 }
2001 } elseif (is_dir($deref)) {
2002
2003 // $link_target = readlink($deref);
2004
2005 $this->makezip_recursive_add($deref, $use_path_when_storing.'/'.$e, $original_fullpath, $startlevels, $exclude);
2006 }
2007 } elseif (is_file($fullpath.'/'.$e)) {
2008 if (is_readable($fullpath.'/'.$e)) {
2009 $use_stripped = $stripped_storage_path.'/'.$e;
2010 if (false !== ($fkey = array_search($use_stripped, $exclude))) {
2011 $iwp_backup_core->log("Entity excluded by configuration option: $use_stripped");
2012 unset($exclude[$fkey]);
2013 } elseif (!empty($this->excluded_extensions) && $this->is_entity_excluded_by_extension($e)) {
2014 $iwp_backup_core->log("Entity excluded by configuration option (extension): $use_stripped");
2015 } elseif (!empty($this->excluded_prefixes) && $this->is_entity_excluded_by_prefix($e)) {
2016 $iwp_backup_core->log("Entity excluded by configuration option (prefix): $use_stripped");
2017 } elseif (apply_filters('IWP_exclude_file', false, $fullpath.'/'.$e)) {
2018 $iwp_backup_core->log("Entity excluded by filter: $use_stripped");
2019 } else {
2020 $mtime = filemtime($fullpath.'/'.$e);
2021 if ($mtime > 0 && $mtime > $if_altered_since) {
2022 $this->zipfiles_batched[$fullpath.'/'.$e] = $use_path_when_storing.'/'.$e;
2023 $this->makezip_recursive_batchedbytes += @filesize($fullpath.'/'.$e);
2024 } else {
2025 $this->zipfiles_skipped_notaltered[$fullpath.'/'.$e] = $use_path_when_storing.'/'.$e;
2026 }
2027 }
2028 } else {
2029 $iwp_backup_core->log("$fullpath/$e: unreadable file");
2030 $iwp_backup_core->log(sprintf(__("%s: unreadable file - could not be backed up", 'iwp_backup_dir'), $use_path_when_storing.'/'.$e), 'warning', "unrfile-$e");
2031 }
2032 } elseif (is_dir($fullpath.'/'.$e)) {
2033 if ('wpcore' == $this->whichone && 'updraft' == $e && basename($use_path_when_storing) == 'wp-content' && (!defined('IWP_WPCORE_INCLUDE_IWP_DIRS') || !IWP_WPCORE_INCLUDE_IWP_DIRS)) {
2034 // This test, of course, won't catch everything - it just aims to make things better by default
2035 $iwp_backup_core->log("Directory excluded for looking like a sub-site's internal IWP directory (enable by defining IWP_WPCORE_INCLUDE_IWP_DIRS): ".$use_path_when_storing.'/'.$e);
2036 } else {
2037 // no need to addEmptyDir here, as it gets done when we recurse
2038 $this->makezip_recursive_add($fullpath.'/'.$e, $use_path_when_storing.'/'.$e, $original_fullpath, $startlevels, $exclude);
2039 }
2040 }
2041 }
2042 closedir($dir_handle);
2043 } else {
2044 $iwp_backup_core->log("Unexpected: path ($use_path_when_storing) fails both is_file() and is_dir()");
2045 }
2046
2047 return true;
2048
2049 }
2050
2051 private function get_excluded_extensions($exclude) {
2052 if (!is_array($exclude)) $exclude = array();
2053 $exclude_extensions = array();
2054 foreach ($exclude as $ex) {
2055 if (preg_match('/^ext:(.+)$/i', $ex, $matches)) {
2056 $exclude_extensions[] = strtolower($matches[1]);
2057 }
2058 }
2059
2060 if (defined('IWP_EXCLUDE_EXTENSIONS')) {
2061 $exclude_from_define = explode(',', IWP_EXCLUDE_EXTENSIONS);
2062 foreach ($exclude_from_define as $ex) {
2063 $exclude_extensions[] = strtolower(trim($ex));
2064 }
2065 }
2066
2067 return $exclude_extensions;
2068 }
2069
2070 private function get_excluded_prefixes($exclude) {
2071 if (!is_array($exclude)) $exclude = array();
2072 $exclude_prefixes = array();
2073 foreach ($exclude as $pref) {
2074 if (preg_match('/^prefix:(.+)$/i', $pref, $matches)) {
2075 $exclude_prefixes[] = strtolower($matches[1]);
2076 }
2077 }
2078
2079 return $exclude_prefixes;
2080 }
2081
2082 private function is_entity_excluded_by_extension($entity) {
2083 foreach ($this->excluded_extensions as $ext) {
2084 if (!$ext) continue;
2085 $eln = strlen($ext);
2086 if (strtolower(substr($entity, -$eln, $eln)) == $ext) return true;
2087 }
2088 return false;
2089 }
2090
2091 private function is_entity_excluded_by_prefix($entity) {
2092 $entity = basename($entity);
2093 foreach ($this->excluded_prefixes as $pref) {
2094 if (!$pref) continue;
2095 $eln = strlen($pref);
2096 if (strtolower(substr($entity, 0, $eln)) == $pref) return true;
2097 }
2098 return false;
2099 }
2100
2101 private function unserialize_gz_cache_file($file) {
2102 if (!$whandle = gzopen($file, 'r')) return false;
2103 global $iwp_backup_core;
2104 $emptimes = 0;
2105 $var = '';
2106 while (!gzeof($whandle)) {
2107 $bytes = @gzread($whandle, 1048576);
2108 if (empty($bytes)) {
2109 $emptimes++;
2110 $iwp_backup_core->log("Got empty gzread ($emptimes times)");
2111 if ($emptimes>2) return false;
2112 } else {
2113 $var .= $bytes;
2114 }
2115 }
2116 gzclose($whandle);
2117 return unserialize($var);
2118 }
2119
2120 // Caution: $source is allowed to be an array, not just a filename
2121 // $destination is the temporary file (ending in .tmp)
2122 private function make_zipfile($source, $backup_file_basename, $whichone, $retry_on_error = true) {
2123
2124 global $iwp_backup_core;
2125
2126 $original_index = $this->index;
2127
2128 $itext = (empty($this->index)) ? '' : ($this->index+1);
2129 $destination_base = $backup_file_basename.'-'.$whichone.$itext.'.zip.tmp';
2130 $destination = $this->iwp_backup_dir.'/'.$destination_base;
2131
2132 // Legacy/redundant
2133 //if (empty($whichone) && is_string($whichone)) $whichone = basename($source);
2134
2135 // When to prefer PCL:
2136 // - We were asked to
2137 // - No zip extension present and no relevant method present
2138 // The zip extension check is not redundant, because method_exists segfaults some PHP installs, leading to support requests
2139
2140 // We need meta-info about $whichone
2141 $backupable_entities = $iwp_backup_core->get_backupable_file_entities(true, false);
2142 # This is only used by one corner-case in BinZip
2143 #$this->make_zipfile_source = (isset($backupable_entities[$whichone])) ? $backupable_entities[$whichone] : $source;
2144 $this->make_zipfile_source = (is_array($source) && isset($backupable_entities[$whichone])) ? (('uploads' == $whichone) ? dirname($backupable_entities[$whichone]) : $backupable_entities[$whichone]) : dirname($source);
2145
2146 $this->existing_files = array();
2147 # Used for tracking compression ratios
2148 $this->existing_files_rawsize = 0;
2149 $this->existing_zipfiles_size = 0;
2150
2151 // Enumerate existing files
2152 // Usually first_linked_index is zero; the exception being with more files, where previous zips' contents are irrelevant
2153 for ($j=$this->first_linked_index; $j<=$this->index; $j++) {
2154 $jtext = ($j == 0) ? '' : ($j+1);
2155 # This is, in a non-obvious way, compatible with filenames which indicate increments
2156 # $j does not need to start at zero; it should start at the index which the current entity split at. However, this is not directly known, and can only be deduced from examining the filenames. And, for other indexes from before the current increment, the searched-for filename won't exist (even if there is no cloud storage). So, this indirectly results in the desired outcome when we start from $j=0.
2157 $examine_zip = $this->iwp_backup_dir.'/'.$backup_file_basename.'-'.$whichone.$jtext.'.zip'.(($j == $this->index) ? '.tmp' : '');
2158
2159
2160 if ($j != $this->index && !file_exists($examine_zip)) {
2161 $alt_examine_zip = $this->iwp_backup_dir.'/'.$backup_file_basename.'-'.$whichone.$jtext.'.zip'.(($j == $this->index - 1) ? '.tmp' : '');
2162 if ($alt_examine_zip != $examine_zip && file_exists($alt_examine_zip) && is_readable($alt_examine_zip) && filesize($alt_examine_zip)>0) {
2163 $iwp_backup_core->log("Looked-for zip file not found; but non-zero .tmp zip was, despite not being current index ($j != ".$this->index." - renaming zip (assume previous resumption's IO was lost before kill)");
2164 if (rename($alt_examine_zip, $examine_zip)) {
2165 clearstatcache();
2166 } else {
2167 $iwp_backup_core->log("Rename failed - backup zips likely to not have sequential numbers (does not affect backup integrity, but can cause user confusion)");
2168 }
2169 }
2170 }
2171
2172 // If the file exists, then we should grab its index of files inside, and sizes
2173 // Then, when we come to write a file, we should check if it's already there, and only add if it is not
2174 if (file_exists($examine_zip) && is_readable($examine_zip) && filesize($examine_zip)>0) {
2175 $this->existing_zipfiles_size += filesize($examine_zip);
2176 $zip = new $this->use_zip_object;
2177 if (true !== $zip->open($examine_zip)) {
2178 $iwp_backup_core->log("Could not open zip file to examine (".$zip->last_error."); will remove: ".basename($examine_zip));
2179 @unlink($examine_zip);
2180 } else {
2181
2182 # 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
2183 $numfiles = $zip->numFiles;
2184 for ($i=0; $i < $numfiles; $i++) {
2185 $si = $zip->statIndex($i);
2186 $name = $si['name'];
2187 $this->existing_files[$name] = $si['size'];
2188 $this->existing_files_rawsize += $si['size'];
2189 }
2190
2191 @$zip->close();
2192 }
2193
2194 $iwp_backup_core->log(basename($examine_zip).": Zip file already exists, with ".count($this->existing_files)." files");
2195
2196 # try_split is set if there have been no check-ins recently - or if it needs to be split anyway
2197 if ($j == $this->index) {
2198 if (isset($this->try_split)) {
2199 if (filesize($examine_zip) > 50*1048576) {
2200 # We could, as a future enhancement, save this back to the job data, if we see a case that needs it
2201 $this->zip_split_every = max(
2202 (int)$this->zip_split_every/2,
2203 IWP_SPLIT_MIN*1048576,
2204 min(filesize($examine_zip)-1048576, $this->zip_split_every)
2205 );
2206 $iwp_backup_core->jobdata_set('split_every', (int)($this->zip_split_every/1048576));
2207 $iwp_backup_core->log("No check-in on last two runs; bumping index and reducing zip split to: ".round($this->zip_split_every/1048576, 1)." MB");
2208 $do_bump_index = true;
2209 }
2210 unset($this->try_split);
2211 } elseif (filesize($examine_zip) > $this->zip_split_every) {
2212 $iwp_backup_core->log(sprintf("Zip size is at/near split limit (%s MB / %s MB) - bumping index (from: %d)", filesize($examine_zip), round($this->zip_split_every/1048576, 1), $this->index));
2213 $do_bump_index = true;
2214 }
2215 }
2216
2217 } elseif (file_exists($examine_zip)) {
2218 $iwp_backup_core->log("Zip file already exists, but is not readable or was zero-sized; will remove: ".basename($examine_zip));
2219 @unlink($examine_zip);
2220 }
2221 }
2222
2223 $this->zip_last_ratio = ($this->existing_files_rawsize > 0) ? ($this->existing_zipfiles_size/$this->existing_files_rawsize) : 1;
2224
2225 $this->zipfiles_added = 0;
2226 $this->zipfiles_added_thisrun = 0;
2227 $this->zipfiles_dirbatched = array();
2228 $this->zipfiles_batched = array();
2229 $this->zipfiles_skipped_notaltered = array();
2230 $this->zipfiles_lastwritetime = time();
2231 $this->zip_basename = $this->iwp_backup_dir.'/'.$backup_file_basename.'-'.$whichone;
2232
2233 if (!empty($do_bump_index)) $this->bump_index();
2234
2235 $error_occurred = false;
2236
2237 # Store this in its original form
2238 $this->source = $source;
2239
2240 # Reset. This counter is used only with PcLZip, to decide if it's better to do it all-in-one
2241 $this->makezip_recursive_batchedbytes = 0;
2242 if (!is_array($source)) $source=array($source);
2243
2244 $exclude = $iwp_backup_core->get_exclude($whichone);
2245
2246 $files_enumerated_at = $iwp_backup_core->jobdata_get('files_enumerated_at');
2247 if (!is_array($files_enumerated_at)) $files_enumerated_at = array();
2248 $files_enumerated_at[$whichone] = time();
2249 $iwp_backup_core->jobdata_set('files_enumerated_at', $files_enumerated_at);
2250
2251 $this->makezip_if_altered_since = (is_array($this->altered_since)) ? (isset($this->altered_since[$whichone]) ? $this->altered_since[$whichone] : -1) : -1;
2252
2253 // Reset
2254 $got_uploads_from_cache = false;
2255
2256 // Uploads: can/should we get it back from the cache?
2257 // || 'others' == $whichone
2258 if (('uploads' == $whichone || 'others' == $whichone) && function_exists('gzopen') && function_exists('gzread')) {
2259 $use_cache_files = false;
2260 $cache_file_base = $this->zip_basename.'-cachelist-'.$this->makezip_if_altered_since;
2261 // Cache file suffixes: -zfd.gz.tmp, -zfb.gz.tmp, -info.tmp, (possible)-zfs.gz.tmp
2262 if (file_exists($cache_file_base.'-zfd.gz.tmp') && file_exists($cache_file_base.'-zfb.gz.tmp') && file_exists($cache_file_base.'-info.tmp')) {
2263 // Cache files exist; shall we use them?
2264 $mtime = filemtime($cache_file_base.'-zfd.gz.tmp');
2265 // Require < 30 minutes old
2266 if (time() - $mtime < 1800) { $use_cache_files = true; }
2267 $any_failures = false;
2268 if ($use_cache_files) {
2269 $var = $this->unserialize_gz_cache_file($cache_file_base.'-zfd.gz.tmp');
2270 if (is_array($var)) {
2271 $this->zipfiles_dirbatched = $var;
2272 $var = $this->unserialize_gz_cache_file($cache_file_base.'-zfb.gz.tmp');
2273 if (is_array($var)) {
2274 $this->zipfiles_batched = $var;
2275 if (file_exists($cache_file_base.'-info.tmp')) {
2276 $var = maybe_unserialize(file_get_contents($cache_file_base.'-info.tmp'));
2277 if (is_array($var) && isset($var['makezip_recursive_batchedbytes'])) {
2278 $this->makezip_recursive_batchedbytes = $var['makezip_recursive_batchedbytes'];
2279 if (file_exists($cache_file_base.'-zfs.gz.tmp')) {~
2280 $var = $this->unserialize_gz_cache_file($cache_file_base.'-zfs.gz.tmp');
2281 if (is_array($var)) {
2282 $this->zipfiles_skipped_notaltered = $var;
2283 } else {
2284 $any_failures = true;
2285 }
2286 } else {
2287 $this->zipfiles_skipped_notaltered = array();
2288 }
2289 } else {
2290 $any_failures = true;
2291 }
2292 }
2293 } else {
2294 $any_failures = true;
2295 }
2296 } else {
2297 $any_failures = true;
2298 }
2299 if ($any_failures) {
2300 $iwp_backup_core->log("Failed to recover file lists from existing cache files");
2301 // Reset it all
2302 $this->zipfiles_skipped_notaltered = array();
2303 $this->makezip_recursive_batchedbytes = 0;
2304 $this->zipfiles_batched = array();
2305 $this->zipfiles_dirbatched = array();
2306 } else {
2307 $iwp_backup_core->log("File lists recovered from cache files; sizes: ".count($this->zipfiles_batched).", ".count($this->zipfiles_batched).", ".count($this->zipfiles_skipped_notaltered).")");
2308 $got_uploads_from_cache = true;
2309 }
2310 }
2311 }
2312 }
2313
2314 $time_counting_began = time();
2315
2316 $this->excluded_extensions = $this->get_excluded_extensions($exclude);
2317 $this->excluded_prefixes = $this->get_excluded_prefixes($exclude);
2318
2319 foreach ($source as $element) {
2320 #makezip_recursive_add($fullpath, $use_path_when_storing, $original_fullpath, $startlevels = 1, $exclude_array)
2321 if ('uploads' == $whichone) {
2322 if (empty($got_uploads_from_cache)) {
2323 $dirname = dirname($element);
2324 $basename = $this->basename($element);
2325 $add_them = $this->makezip_recursive_add($element, basename($dirname).'/'.$basename, $element, 2, $exclude);
2326 } else {
2327 $add_them = true;
2328 }
2329 } else {
2330 if (empty($got_uploads_from_cache)) {
2331 $add_them = $this->makezip_recursive_add($element, $this->basename($element), $element, 1, $exclude);
2332 } else {
2333 $add_them = true;
2334 }
2335 }
2336 if (is_wp_error($add_them) || false === $add_them) $error_occurred = true;
2337 }
2338
2339 $time_counting_ended = time();
2340
2341 // Cache the file scan, if it looks like it'll be useful
2342 // We use gzip to reduce the size as on hosts which limit disk I/O, the cacheing may make things worse
2343 // || 'others' == $whichone
2344 if (('uploads' == $whichone|| 'others' == $whichone) && !$error_occurred && function_exists('gzopen') && function_exists('gzwrite')) {
2345 $cache_file_base = $this->zip_basename.'-cachelist-'.$this->makezip_if_altered_since;
2346
2347 // Just approximate - we're trying to avoid an otherwise-unpredictable PHP fatal error. Cacheing only happens if file enumeration took a long time - so presumably there are very many.
2348 $memory_needed_estimate = 0;
2349 foreach ($this->zipfiles_batched as $k => $v) { $memory_needed_estimate += strlen($k)+strlen($v)+12; }
2350
2351 // We haven't bothered to check if we just fetched the files from cache, as that shouldn't take a long time and so shouldn't trigger this
2352 // Let us suppose we need 15% overhead for gzipping
2353
2354 $memory_limit = ini_get('memory_limit');
2355 $memory_usage = round(@memory_get_usage(false)/1048576, 1);
2356 $memory_usage2 = round(@memory_get_usage(true)/1048576, 1);
2357
2358 if ($time_counting_ended-$time_counting_began > 20 && $iwp_backup_core->verify_free_memory($memory_needed_estimate*0.15) && $whandle = gzopen($cache_file_base.'-zfb.gz.tmp', 'w')) {
2359 $iwp_backup_core->log("File counting took a long time (".($time_counting_ended - $time_counting_began)."s); will attempt to cache results (memory_limit: $memory_limit (used: ".$memory_usage."M | ".$memory_usage2."M), estimated uncompressed bytes: ".round($memory_needed_estimate/1024, 1)." Kb)");
2360
2361 $buf = 'a:'.count($this->zipfiles_batched).':{';
2362 foreach ($this->zipfiles_batched as $file => $add_as) {
2363 $k = addslashes($file);
2364 $v = addslashes($add_as);
2365 $buf .= 's:'.strlen($k).':"'.$k.'";s:'.strlen($v).':"'.$v.'";';
2366 if (strlen($buf) > 1048576) {
2367 gzwrite($whandle, $buf, strlen($buf));
2368 $buf = '';
2369 }
2370 }
2371 $buf .= '}';
2372 $final = gzwrite($whandle, $buf);
2373 unset($buf);
2374
2375
2376 if (!$final) {
2377 @unlink($cache_file_base.'-zfb.gz.tmp');
2378 @gzclose($whandle);
2379 } else {
2380 gzclose($whandle);
2381 if (!empty($this->zipfiles_skipped_notaltered)) {
2382 if ($shandle = gzopen($cache_file_base.'-zfs.gz.tmp', 'w')) {
2383 if (!gzwrite($shandle, serialize($this->zipfiles_skipped_notaltered))) {
2384 $aborted_on_skipped = true;
2385 }
2386 gzclose($shandle);
2387 } else {
2388 $aborted_on_skipped = true;
2389 }
2390 }
2391 if (!empty($aborted_on_skipped)) {
2392 @unlink($cache_file_base.'-zfs.gz.tmp');
2393 @unlink($cache_file_base.'-zfb.gz.tmp');
2394 } else {
2395 $info_array = array('makezip_recursive_batchedbytes' => $this->makezip_recursive_batchedbytes);
2396 if (!file_put_contents($cache_file_base.'-info.tmp', serialize($info_array))) {
2397 @unlink($cache_file_base.'-zfs.gz.tmp');
2398 @unlink($cache_file_base.'-zfb.gz.tmp');
2399 }
2400 if ($dhandle = gzopen($cache_file_base.'-zfd.gz.tmp', 'w')) {
2401 if (!gzwrite($dhandle, serialize($this->zipfiles_dirbatched))) {
2402 $aborted_on_dirbatched = true;
2403 }
2404 gzclose($dhandle);
2405 } else {
2406 $aborted_on_dirbatched = true;
2407 }
2408 if (!empty($aborted_on_dirbatched)) {
2409 @unlink($cache_file_base.'-zfs.gz.tmp');
2410 @unlink($cache_file_base.'-zfd.gz.tmp');
2411 @unlink($cache_file_base.'-zfb.gz.tmp');
2412 @unlink($cache_file_base.'-info.tmp');
2413 } else {
2414 // Success.
2415 }
2416 }
2417 }
2418 }
2419
2420 /*
2421 Class variables that get altered:
2422 zipfiles_batched
2423 makezip_recursive_batchedbytes
2424 zipfiles_skipped_notaltered
2425 zipfiles_dirbatched
2426
2427 Class variables that the result depends upon (other than the state of the filesystem):
2428 makezip_if_altered_since
2429 existing_files
2430 */
2431
2432 }
2433
2434 // 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.
2435 // 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.
2436 // $retry_on_error is here being used as a proxy for 'not the second time around, when there might be the remains of the file on the first time around'
2437 if ($retry_on_error) $iwp_backup_core->check_recent_modification($destination);
2438 // 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')
2439 if (empty($do_bump_index)) @touch($destination);
2440
2441 if (count($this->zipfiles_dirbatched) > 0 || count($this->zipfiles_batched) > 0) {
2442
2443 $iwp_backup_core->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)));
2444
2445 // No need to warn if we're going to retry anyway. (And if we get killed, the zip will be rescanned for its contents upon resumption).
2446 $warn_on_failures = ($retry_on_error) ? false : true;
2447 $add_them = $this->makezip_addfiles($warn_on_failures);
2448
2449 if (is_wp_error($add_them)) {
2450 foreach ($add_them->get_error_messages() as $msg) {
2451 $iwp_backup_core->log("Error returned from makezip_addfiles: ".$msg);
2452 }
2453 $error_occurred = true;
2454 } elseif (false === $add_them) {
2455 $iwp_backup_core->log("Error: makezip_addfiles returned false");
2456 $error_occurred = true;
2457 }
2458
2459 }
2460
2461 // Reset these variables because the index may have changed since we began
2462
2463 $itext = (empty($this->index)) ? '' : ($this->index+1);
2464 $destination_base = $backup_file_basename.'-'.$whichone.$itext.'.zip.tmp';
2465 $destination = $this->iwp_backup_dir.'/'.$destination_base;
2466
2467 // ZipArchive::addFile sometimes fails - there's nothing when we expected something.
2468 // Did not used to have || $error_occured here. But it is better to retry, than to simply warn the user to check his logs.
2469 if (((file_exists($destination) || $this->index == $original_index) && @filesize($destination) < 90 && 'IWP_MMB_ZipArchive' == $this->use_zip_object) || ($error_occurred && $retry_on_error)) {
2470 // This can be made more sophisticated if feedback justifies it. Currently we just switch to PclZip. But, it may have been a BinZip failure, so we could then try ZipArchive if that is available. If doing that, make sure that an infinite recursion isn't made possible.
2471 $iwp_backup_core->log("makezip_addfiles(".$this->use_zip_object.") apparently failed (file=".basename($destination).", type=$whichone, size=".filesize($destination).") - retrying with PclZip");
2472 $saved_zip_object = $this->use_zip_object;
2473 $this->use_zip_object = 'IWP_MMB_PclZip';
2474 $ret = $this->make_zipfile($source, $backup_file_basename, $whichone, false);
2475 $this->use_zip_object = $saved_zip_object;
2476 return $ret;
2477 }
2478
2479 // zipfiles_added > 0 means that $zip->close() has been called. i.e. An attempt was made to add something: something _should_ be there.
2480 // Why return true even if $error_occurred may be set? 1) Because in that case, a warning has already been logged. 2) Because returning false causes an error to be logged, which means it'll all be retried again. Also 3) this has been the pattern of the code for a long time, and the algorithm has been proven in the real-world: don't change what's not broken.
2481 // (file_exists($destination) || $this->index == $original_index) might be an alternative to $this->zipfiles_added > 0 - ? But, don't change what's not broken.
2482 if ($error_occurred == false || $this->zipfiles_added > 0) {
2483 return true;
2484 } else {
2485 $iwp_backup_core->log("makezip failure: zipfiles_added=".$this->zipfiles_added.", error_occurred=".$error_occurred." (method=".$this->use_zip_object.")");
2486 return false;
2487 }
2488
2489 }
2490
2491 private function basename($element) {
2492 # 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.
2493 $dirname = dirname($element);
2494 $basename_manual = preg_replace('#^[\\/]+#', '', substr($element, strlen($dirname)));
2495 $basename = basename($element);
2496 if ($basename_manual != $basename) {
2497 $locale = setlocale(LC_CTYPE, "0");
2498 if ('C' == $locale) {
2499 setlocale(LC_CTYPE, 'en_US.UTF8');
2500 $basename_new = basename($element);
2501 if ($basename_new == $basename_manual) $basename = $basename_new;
2502 setlocale(LC_CTYPE, $locale);
2503 }
2504 }
2505 return $basename;
2506 }
2507
2508 private function file_should_be_stored_without_compression($file) {
2509 if (!is_array($this->extensions_to_not_compress)) return false;
2510 foreach ($this->extensions_to_not_compress as $ext) {
2511 $ext_len = strlen($ext);
2512 if (strtolower(substr($file, -$ext_len, $ext_len)) == $ext) return true;
2513 }
2514 return false;
2515 }
2516
2517 // Q. Why don't we only open and close the zip file just once?
2518 // 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)
2519
2520 // We batch up the files, rather than do them one at a time. So we are more efficient than open,one-write,close.
2521 // To call into here, the array $this->zipfiles_batched must be populated (keys=paths, values=add-to-zip-as values). It gets reset upon exit from here.
2522 private function makezip_addfiles($warn_on_failures) {
2523
2524 global $iwp_backup_core;
2525
2526 # Used to detect requests to bump the size
2527 $bump_index = false;
2528 $ret = true;
2529
2530 $zipfile = $this->zip_basename.(($this->index == 0) ? '' : ($this->index+1)).'.zip.tmp';
2531
2532 $maxzipbatch = $iwp_backup_core->jobdata_get('maxzipbatch', 26214400);
2533 if ((int)$maxzipbatch < 1024) $maxzipbatch = 26214400;
2534
2535 // Short-circuit the null case, because we want to detect later if something useful happenned
2536 if (count($this->zipfiles_dirbatched) == 0 && count($this->zipfiles_batched) == 0) return true;
2537
2538 # 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)
2539 # This assumes that makezip_addfiles() is only called once so that we know about all needed files (the new style)
2540 # This is rather conservative - because it assumes zero compression. But we can't know that in advance.
2541 $force_allinone = false;
2542 if (0 == $this->index && $this->makezip_recursive_batchedbytes < $this->zip_split_every) {
2543 # So far, we only have a processor for this for PclZip; but that check can be removed - need to address the below items
2544 # 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.
2545 # TODO: Test this new method for PclZip - are we still getting the performance gains? Test for ZipArchive too.
2546 if ('IWP_MMB_PclZip' == $this->use_zip_object && ($this->makezip_recursive_batchedbytes < 512*1048576 || (defined('IWP_PCLZIP_FORCEALLINONE') && IWP_PCLZIP_FORCEALLINONE == true && 'IWP_MMB_PclZip' == $this->use_zip_object))) {
2547 $iwp_backup_core->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)");
2548 $force_allinone = true;
2549 }
2550 }
2551
2552 // 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!
2553
2554 $data_added_since_reopen = 0;
2555 # 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)
2556 $files_zipadded_since_open = array();
2557
2558 $zip = new $this->use_zip_object;
2559 if (file_exists($zipfile)) {
2560 $opencode = $zip->open($zipfile);
2561 $original_size = filesize($zipfile);
2562 clearstatcache();
2563 } else {
2564 $create_code = (version_compare(PHP_VERSION, '5.2.12', '>') && defined('ZIPARCHIVE::CREATE')) ? ZIPARCHIVE::CREATE : 1;
2565 $opencode = $zip->open($zipfile, $create_code);
2566 $original_size = 0;
2567 }
2568
2569 if ($opencode !== true) return new WP_Error('no_open', sprintf(__('Failed to open the zip file (%s) - %s', 'InfiniteWP'),$zipfile, $zip->last_error));
2570 # TODO: This action isn't being called for the all-in-one case - should be, I think
2571 do_action("IWP_makezip_addfiles_prepack", $this, $this->whichone);
2572
2573 // Make sure all directories are created before we start creating files
2574 while ($dir = array_pop($this->zipfiles_dirbatched)) $zip->addEmptyDir($dir);
2575 $zipfiles_added_thisbatch = 0;
2576
2577 // Go through all those batched files
2578 foreach ($this->zipfiles_batched as $file => $add_as) {
2579
2580 if (!file_exists($file)) {
2581 $iwp_backup_core->log("File has vanished from underneath us; dropping: ".$add_as);
2582 continue;
2583 }
2584
2585 $fsize = filesize($file);
2586
2587 if (defined('IWP_SKIP_FILE_OVER_SIZE') && $fsize > IWP_SKIP_FILE_OVER_SIZE) {
2588 $iwp_backup_core->log("File is larger than the user-configured (IWP_SKIP_FILE_OVER_SIZE) maximum (is: ".round($fsize/1024, 1)." KB); will skip: ".$add_as);
2589 continue;
2590 } elseif ($fsize > IWP_WARN_FILE_SIZE) {
2591 $iwp_backup_core->log(sprintf(__('A very large file was encountered: %s (size: %s Mb)', 'InfiniteWP'), $add_as, round($fsize/1048576, 1)), 'warning', 'vlargefile_'.md5($this->whichone.'#'.$add_as));
2592 }
2593
2594 // Skips files that are already added
2595 if (!isset($this->existing_files[$add_as]) || $this->existing_files[$add_as] != $fsize) {
2596
2597 @touch($zipfile);
2598 $zip->addFile($file, $add_as);
2599 $zipfiles_added_thisbatch++;
2600
2601 if (method_exists($zip, 'setCompressionName') && $this->file_should_be_stored_without_compression($add_as)) {
2602 if (false == ($set_compress = $zip->setCompressionName($add_as, ZipArchive::CM_STORE))) {
2603 $iwp_backup_core->log("Zip: setCompressionName failed on: $add_as");
2604 }
2605 }
2606
2607 // N.B., Since makezip_addfiles() can get called more than once if there were errors detected, potentially $zipfiles_added_thisrun can exceed the total number of batched files (if they get processed twice).
2608 $this->zipfiles_added_thisrun++;
2609 $files_zipadded_since_open[] = array('file' => $file, 'addas' => $add_as);
2610
2611 $data_added_since_reopen += $fsize;
2612 /* Conditions for forcing a write-out and re-open:
2613 - more than $maxzipbatch bytes have been batched
2614 - more than 2.0 seconds have passed since the last time we wrote
2615 - 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)
2616 - more than 500 files batched (should perhaps intelligently lower this as the zip file gets bigger - not yet needed)
2617 */
2618
2619 # 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)
2620 # 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
2621 $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;
2622
2623 if (!$force_allinone && ($zipfiles_added_thisbatch > IWP_MAXBATCHFILES || $reaching_split_limit || $data_added_since_reopen > $maxzipbatch || (time() - $this->zipfiles_lastwritetime) > 2)) {
2624
2625 @set_time_limit(IWP_SET_TIME_LIMIT);
2626 $something_useful_sizetest = false;
2627
2628 if ($data_added_since_reopen > $maxzipbatch) {
2629 $something_useful_sizetest = true;
2630 $iwp_backup_core->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)');
2631 } elseif ($zipfiles_added_thisbatch > IWP_MAXBATCHFILES) {
2632 $iwp_backup_core->log("Adding batch to zip file (".$this->use_zip_object."): over ".IWP_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)');
2633 } elseif (!$reaching_split_limit) {
2634 $iwp_backup_core->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)');
2635 } else {
2636 $iwp_backup_core->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)');
2637 }
2638
2639 if (!$zip->close()) {
2640 // Though we will continue processing the files we've got, the final error code will be false, to allow a second attempt on the failed ones. This also keeps us consistent with a negative result for $zip->close() further down. We don't just retry here, because we have seen cases (with BinZip) where upon failure, the existing zip had actually been deleted. So, to be safe we need to re-scan the existing zips.
2641 $ret = false;
2642 $this->record_zip_error($files_zipadded_since_open, $zip->last_error, $warn_on_failures);
2643 }
2644
2645 $zipfiles_added_thisbatch = 0;
2646
2647 # This triggers a re-open, later
2648 unset($zip);
2649 $files_zipadded_since_open = array();
2650 // Call here, in case we've got so many big files that we don't complete the whole routine
2651 if (filesize($zipfile) > $original_size) {
2652
2653 # 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
2654 $this->zip_last_ratio = ($data_added_since_reopen > 0) ? min((filesize($zipfile) - $original_size)/$data_added_since_reopen, 1) : 1;
2655
2656 # We need a rolling update of this
2657 $original_size = filesize($zipfile);
2658
2659 # Move on to next zip?
2660 if ($reaching_split_limit || filesize($zipfile) > $this->zip_split_every) {
2661 $bump_index = true;
2662 # Take the filesize now because later we wanted to know we did clearstatcache()
2663 $bumped_at = round(filesize($zipfile)/1048576, 1);
2664 }
2665
2666 # Need to make sure that something_useful_happened() is always called
2667
2668 # 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.
2669 if (!$something_useful_sizetest) {
2670 $iwp_backup_core->something_useful_happened();
2671 } else {
2672
2673 // Do this as early as possible
2674 $iwp_backup_core->something_useful_happened();
2675
2676 $time_since_began = max(microtime(true)- $this->zipfiles_lastwritetime, 0.000001);
2677 $normalised_time_since_began = $time_since_began*($maxzipbatch/$data_added_since_reopen);
2678
2679 // Don't measure speed until after ZipArchive::close()
2680 $rate = round($data_added_since_reopen/$time_since_began, 1);
2681
2682 $iwp_backup_core->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)));
2683
2684 // 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.
2685
2686 /* "Could have done more" - detect as:
2687 - A batch operation would still leave a "good chunk" of time in a run
2688 - "Good chunk" means that the time we took to add the batch is less than 50% of a run time
2689 - We can do that on any run after the first (when at least one ceiling on the maximum time is known)
2690 - 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.
2691 */
2692
2693 // 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
2694
2695 // Gather the data. We try not to do this unless necessary (may be time-sensitive)
2696 if ($iwp_backup_core->current_resumption >= 1) {
2697 $time_passed = $iwp_backup_core->jobdata_get('run_times');
2698 if (!is_array($time_passed)) $time_passed = array();
2699 list($max_time, $timings_string, $run_times_known) = $iwp_backup_core->max_time_passed($time_passed, $iwp_backup_core->current_resumption-1, $this->first_run);
2700 } else {
2701 $run_times_known = 0;
2702 $max_time = -1;
2703 }
2704
2705 if ($normalised_time_since_began<6 || ($iwp_backup_core->current_resumption >=1 && $run_times_known >=1 && $time_since_began < 0.6*$max_time )) {
2706
2707 // How much can we increase it by?
2708 if ($normalised_time_since_began <6) {
2709 if ($run_times_known > 0 && $max_time >0) {
2710 $new_maxzipbatch = min(floor(max(
2711 $maxzipbatch*6/$normalised_time_since_began, $maxzipbatch*((0.6*$max_time)/$normalised_time_since_began))),
2712 200*1024*1024
2713 );
2714 } else {
2715 # Maximum of 200MB in a batch
2716 $new_maxzipbatch = min( floor($maxzipbatch*6/$normalised_time_since_began),
2717 200*1024*1024
2718 );
2719 }
2720 } else {
2721 // Use up to 60% of available time
2722 $new_maxzipbatch = min(
2723 floor($maxzipbatch*((0.6*$max_time)/$normalised_time_since_began)),
2724 200*1024*1024
2725 );
2726 }
2727
2728 # Throttle increases - don't increase by more than 2x in one go - ???
2729 # $new_maxzipbatch = floor(min(2*$maxzipbatch, $new_maxzipbatch));
2730 # 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
2731 # $new_maxzipbatch = floor(min(18*$rate ,$new_maxzipbatch));
2732
2733 # Don't go above the split amount (though we expect that to be higher anyway, unless sending via email)
2734 $new_maxzipbatch = min($new_maxzipbatch, $this->zip_split_every);
2735
2736 # Don't raise it above a level that failed on a previous run
2737 $maxzipbatch_ceiling = $iwp_backup_core->jobdata_get('maxzipbatch_ceiling');
2738 if (is_numeric($maxzipbatch_ceiling) && $maxzipbatch_ceiling > 20*1024*1024 && $new_maxzipbatch > $maxzipbatch_ceiling) {
2739 $iwp_backup_core->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");
2740 $new_maxzipbatch = $maxzipbatch_ceiling;
2741 }
2742
2743 // Final sanity check
2744 if ($new_maxzipbatch > 1024*1024) $iwp_backup_core->jobdata_set("maxzipbatch", $new_maxzipbatch);
2745
2746 if ($new_maxzipbatch <= 1024*1024) {
2747 $iwp_backup_core->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)");
2748 } elseif ($new_maxzipbatch > $maxzipbatch) {
2749 $iwp_backup_core->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)");
2750 } elseif ($new_maxzipbatch < $maxzipbatch) {
2751 // Ironically, we thought we were speedy...
2752 $iwp_backup_core->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)");
2753 } else {
2754 $iwp_backup_core->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)");
2755 }
2756
2757 if ($new_maxzipbatch > 1024*1024) $maxzipbatch = $new_maxzipbatch;
2758 }
2759
2760 // Detect excessive slowness
2761 // 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)
2762
2763 // 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).
2764
2765 if (!$iwp_backup_core->something_useful_happened && $iwp_backup_core->current_resumption >= 7) {
2766
2767 $iwp_backup_core->something_useful_happened();
2768
2769 if ($run_times_known >= 5 && ($time_since_began > 0.8 * $max_time || $time_since_began + 7 > $max_time)) {
2770
2771 $new_maxzipbatch = max(floor($maxzipbatch*0.8), 20971520);
2772 if ($new_maxzipbatch < $maxzipbatch) {
2773 $maxzipbatch = $new_maxzipbatch;
2774 $iwp_backup_core->jobdata_set("maxzipbatch", $new_maxzipbatch);
2775 $iwp_backup_core->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)");
2776 } else {
2777 $iwp_backup_core->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)");
2778 }
2779 }
2780
2781 } else {
2782 $iwp_backup_core->something_useful_happened();
2783 }
2784 }
2785 $data_added_since_reopen = 0;
2786 } else {
2787 # ZipArchive::close() can take a very long time, which we want to know about
2788 $iwp_backup_core->record_still_alive();
2789 }
2790
2791 clearstatcache();
2792 $this->zipfiles_lastwritetime = time();
2793 }
2794 } elseif (0 == $this->zipfiles_added_thisrun) {
2795 // 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.
2796 // Testing shows that calling time() 1000 times takes negligible time
2797 $this->zipfiles_lastwritetime=time();
2798 }
2799
2800 $this->zipfiles_added++;
2801
2802 // Don't call something_useful_happened() here - nothing necessarily happens until close() is called
2803 if ($this->zipfiles_added % 100 == 0) $iwp_backup_core->log("Zip: ".basename($zipfile).": ".$this->zipfiles_added." files added (on-disk size: ".round(@filesize($zipfile)/1024,1)." KB)");
2804
2805 if ($bump_index) {
2806 $iwp_backup_core->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));
2807 $bump_index = false;
2808 $this->bump_index();
2809 $zipfile = $this->zip_basename.($this->index+1).'.zip.tmp';
2810 }
2811
2812 if (empty($zip)) {
2813 $zip = new $this->use_zip_object;
2814
2815 if (file_exists($zipfile)) {
2816 $opencode = $zip->open($zipfile);
2817 $original_size = filesize($zipfile);
2818 clearstatcache();
2819 } else {
2820 $create_code = defined('ZIPARCHIVE::CREATE') ? ZIPARCHIVE::CREATE : 1;
2821 $opencode = $zip->open($zipfile, $create_code);
2822 $original_size = 0;
2823 }
2824
2825 if ($opencode !== true) return new WP_Error('no_open', sprintf(__('Failed to open the zip file (%s) - %s', 'InfiniteWP'), $zipfile, $zip->last_error));
2826 }
2827
2828 }
2829
2830 # Reset array
2831 $this->zipfiles_batched = array();
2832 $this->zipfiles_skipped_notaltered = array();
2833
2834 if (false == ($nret = $zip->close())) $this->record_zip_error($files_zipadded_since_open, $zip->last_error, $warn_on_failures);
2835
2836 do_action("IWP_makezip_addfiles_finished", $this, $this->whichone);
2837
2838 $this->zipfiles_lastwritetime = time();
2839 # May not exist if the last thing we did was bump
2840 if (file_exists($zipfile) && filesize($zipfile) > $original_size) $iwp_backup_core->something_useful_happened();
2841
2842 # Move on to next archive?
2843 if (file_exists($zipfile) && filesize($zipfile) > $this->zip_split_every) {
2844 $iwp_backup_core->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));
2845 $this->bump_index();
2846 }
2847
2848 clearstatcache();
2849
2850 return ($ret == false) ? false : $nret;
2851 }
2852
2853 private function record_zip_error($files_zipadded_since_open, $msg, $warn = true) {
2854 global $iwp_backup_core;
2855
2856 if (!empty($iwp_backup_core->cpanel_quota_readable)) {
2857 $hosting_bytes_free = $iwp_backup_core->get_hosting_disk_quota_free();
2858 if (is_array($hosting_bytes_free)) {
2859 $perc = round(100*$hosting_bytes_free[1]/(max($hosting_bytes_free[2], 1)), 1);
2860 $quota_free_msg = sprintf('Free disk space in account: %s (%s used)', round($hosting_bytes_free[3]/1048576, 1)." MB", "$perc %");
2861 $iwp_backup_core->log($quota_free_msg);
2862 if ($hosting_bytes_free[3] < 1048576*50) {
2863 $quota_low = true;
2864 $quota_free_mb = round($hosting_bytes_free[3]/1048576, 1);
2865 $iwp_backup_core->log(sprintf(__('Your free space in your hosting account is very low - only %s Mb remain', 'InfiniteWP'), $quota_free_mb), 'warning', 'lowaccountspace'.$quota_free_mb);
2866 }
2867 }
2868 }
2869
2870 // Always warn of this
2871 if (strpos($msg, 'File Size Limit Exceeded') !== false && 'IWP_MMB_BinZip' == $this->use_zip_object) {
2872 $iwp_backup_core->log(sprintf(__('The zip engine returned the message: %s.', 'InfiniteWP'), 'File Size Limit Exceeded'). __('Go here for more information.','InfiniteWP').' Please define this constant on your wp-config file define("IWP_NO_BINZIP", true);', 'warning', 'zipcloseerror-filesizelimit');
2873 } elseif ($warn) {
2874 $warn_msg = __('A zip error occurred', 'InfiniteWP').' - ';
2875 if (!empty($quota_low)) {
2876 $warn_msg = sprintf(__('your web hosting account appears to be full; please see: %s', 'InfiniteWP'), 'Increase your server space to backup. Make sure your web server space should be twice the site size');
2877 } else {
2878 $warn_msg .= __('check your log for more details.', 'InfiniteWP');
2879 }
2880 $iwp_backup_core->log($warn_msg, 'warning', 'zipcloseerror-'.$this->whichone);
2881 }
2882
2883 $iwp_backup_core->log("The attempt to close the zip file returned an error ($msg). List of files we were trying to add follows (check their permissions).");
2884
2885 foreach ($files_zipadded_since_open as $ffile) {
2886 $iwp_backup_core->log("File: ".$ffile['addas']." (exists: ".(int)@file_exists($ffile['file']).", is_readable: ".(int)@is_readable($ffile['file'])." size: ".@filesize($ffile['file']).')', 'notice', false, true);
2887 }
2888 }
2889
2890 private function bump_index() {
2891 global $iwp_backup_core;
2892 $youwhat = $this->whichone;
2893
2894 $timetaken = max(microtime(true)-$this->zip_microtime_start, 0.000001);
2895
2896 $itext = ($this->index == 0) ? '' : ($this->index+1);
2897 $full_path = $this->zip_basename.$itext.'.zip';
2898
2899 $checksums = $iwp_backup_core->which_checksums();
2900
2901 $checksum_description = '';
2902
2903 foreach ($checksums as $checksum) {
2904
2905 $cksum = hash_file($checksum, $full_path.'.tmp');
2906 $iwp_backup_core->jobdata_set($checksum.'-'.$youwhat.$this->index, $cksum);
2907 if ($checksum_description) $checksum_description .= ', ';
2908 $checksum_description .= "$checksum: $cksum";
2909
2910 }
2911
2912 $next_full_path = $this->zip_basename.($this->index+2).'.zip';
2913 # We touch the next zip before renaming the temporary file; this indicates that the backup for the entity is not *necessarily* finished
2914 touch($next_full_path.'.tmp');
2915
2916 if (file_exists($full_path.'.tmp') && filesize($full_path.'.tmp') > 0) {
2917 if (!rename($full_path.'.tmp', $full_path)) {
2918 $iwp_backup_core->log("Rename failed for $full_path.tmp");
2919 } else {
2920 $iwp_backup_core->something_useful_happened();
2921 }
2922 }
2923
2924 $kbsize = filesize($full_path)/1024;
2925 $rate = round($kbsize/$timetaken, 1);
2926 $iwp_backup_core->log("Created ".$this->whichone." zip (".$this->index.") - ".round($kbsize,1)." KB in ".round($timetaken,1)." s ($rate KB/s) (checksums: $checksum_description)");
2927 $this->zip_microtime_start = microtime(true);
2928
2929 # No need to add $itext here - we can just delete any temporary files for this zip
2930 $iwp_backup_core->clean_temporary_files('_'.$iwp_backup_core->nonce."-".$youwhat, 600);
2931
2932 $this->index++;
2933 $this->job_file_entities[$youwhat]['index'] = $this->index;
2934 $iwp_backup_core->jobdata_set('job_file_entities', $this->job_file_entities);
2935 }
2936
2937 /**
2938 * Returns the member of the array with key (int)0, as a new array. This function is used as a callback for array_map().
2939 *
2940 * @param Array $a - the array
2941 *
2942 * @return Array - with keys 'name' and 'type'
2943 */
2944 private function cb_get_name_base_type($a) {
2945 return array('name' => $a[0], 'type' => 'BASE TABLE');
2946 }
2947
2948 /**
2949 * Returns the members of the array with keys (int)0 and (int)1, as part of a new array.
2950 *
2951 * @param Array $a - the array
2952 *
2953 * @return Array - keys are 'name' and 'type'
2954 */
2955 private function cb_get_name_type($a) {
2956 return array('name' => $a[0], 'type' => $a[1]);
2957 }
2958
2959 /**
2960 * Returns the member of the array with key (string)'name'. This function is used as a callback for array_map().
2961 *
2962 * @param Array $a - the array
2963 *
2964 * @return Mixed - the value with key (string)'name'
2965 */
2966 private function cb_get_name($a) {
2967 return $a['name'];
2968 }
2969
2970 }
2971
2972 class IWP_MMB_WPDB_OtherDB extends wpdb {
2973 public $error;
2974 // This adjusted bail() does two things: 1) Never dies and 2) logs in the UD log
2975 public function bail( $message, $error_code = '500' ) {
2976 global $iwp_backup_core;
2977 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.';
2978 $iwp_backup_core->log("WPDB_OtherDB error: $message ($error_code)");
2979 # Now do the things that would have been done anyway
2980 if ( class_exists( 'WP_Error' ) )
2981 $this->error = new WP_Error($error_code, $message);
2982 else
2983 $this->error = $message;
2984 return false;
2985 }
2986 }
2987
2988