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