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

class-zip.php in UpdraftPlus: WP Backup & Migration Plugin 1.16.5, at includes/class-zip.php

447 lines 14.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 if (!defined('ABSPATH')) die('No direct access allowed');
4
5 if (class_exists('ZipArchive')) :
6 /**
7 * We just add a last_error variable for comaptibility with our UpdraftPlus_PclZip object
8 */
9 class UpdraftPlus_ZipArchive extends ZipArchive {
10
11 public $last_error = 'Unknown: ZipArchive does not return error messages';
12 }
13 endif;
14
15 /**
16 * A ZipArchive compatibility layer, with behaviour sufficient for our usage of ZipArchive
17 */
18 class UpdraftPlus_PclZip {
19
20 protected $pclzip;
21
22 protected $path;
23
24 protected $addfiles;
25
26 protected $adddirs;
27
28 private $statindex;
29
30 private $include_mtime = false;
31
32 public $last_error;
33
34 /**
35 * Constructor
36 */
37 public function __construct() {
38 $this->addfiles = array();
39 $this->adddirs = array();
40 // Put this in a non-backed-up, writeable location, to make sure that huge temporary files aren't created and then added to the backup - and that we have somewhere writable
41 global $updraftplus;
42 if (!defined('PCLZIP_TEMPORARY_DIR')) define('PCLZIP_TEMPORARY_DIR', trailingslashit($updraftplus->backups_dir_location()));
43 }
44
45 /**
46 * Used to include mtime in statindex (by default, not done - to save memory; probably a bit paranoid)
47 *
48 * @return null
49 */
50 public function ud_include_mtime() {
51 if (empty($this->include_mtime)) $this->statindex = null;
52 $this->include_mtime = true;
53 }
54
55 /**
56 * Magic function for getting an otherwise-undefined class variable
57 *
58 * @param String $name
59 *
60 * @return Boolean|Null|Integer - the value, or null if an unknown variable, or false if something goes wrong
61 */
62 public function __get($name) {
63
64 if ('numFiles' == $name) {
65
66 if (empty($this->pclzip)) return false;
67
68 if (!empty($this->statindex)) return count($this->statindex);
69
70 $statindex = $this->pclzip->listContent();
71
72 if (empty($statindex)) {
73 $this->statindex = array();
74 // We return a value that is == 0, but allowing a PclZip error to be detected (PclZip returns 0 in the case of an error).
75 if (0 === $statindex) $this->last_error = $this->pclzip->errorInfo(true);
76 return (0 === $statindex) ? false : 0;
77 }
78
79 // We used to exclude folders in the case of numFiles (and implemented a private alternative, numAll, that included them), because we had no use for them (we ran a loop over $statindex to build a result that excluded the folders); but that is no longer the case (Dec 2018)
80 $this->statindex = $statindex;
81
82 return count($this->statindex);
83 }
84
85 return null;
86
87 }
88
89 /**
90 * Get stat info for a file
91 *
92 * @param Integer $i The index of the file
93 *
94 * @return Array - the stat info
95 */
96 public function statIndex($i) {
97 if (empty($this->statindex[$i])) return array('name' => null, 'size' => 0);
98 $v = array('name' => $this->statindex[$i]['filename'], 'size' => $this->statindex[$i]['size']);
99 if ($this->include_mtime) $v['mtime'] = $this->statindex[$i]['mtime'];
100 return $v;
101 }
102
103 /**
104 * Returns the entry contents using its index
105 *
106 * @see https://php.net/manual/en/ziparchive.getfromindex.php
107 *
108 * @param Integer $index - Index of the entry
109 * @param Integer $length - The length to be read from the entry. If 0, then the entire entry is read.
110 * @param Integer $flags - The flags to use to open the archive.
111 *
112 * @return String|Boolean - Returns the contents of the entry on success or FALSE on failure.
113 */
114 public function getFromIndex($index, $length = 0, $flags = 0) {
115
116 $contents = $this->pclzip->extract(PCLZIP_OPT_BY_INDEX, array($index), PCLZIP_OPT_EXTRACT_AS_STRING);
117
118 if (0 === $contents) {
119 $this->last_error = $this->pclzip->errorInfo(true);
120 return false;
121 }
122
123 // This also prevents CI complaining about an unused parameter
124 if ($flags) {
125 error_log("A call to UpdraftPlus_PclZip::getFromIndex() set flags=$flags, but this is not implemented");
126 }
127
128 if (!is_array($contents)) {
129 $this->last_error = 'PclZip::extract() did not return the expected information (1)';
130 return false;
131 }
132
133 $content = array_pop($contents);
134
135 if (!isset($content['content'])) {
136 $this->last_error = 'PclZip::extract() did not return the expected information (2)';
137 return false;
138 }
139
140 $results = $content['content'];
141
142 return $length ? substr($results, 0, $length) : $results;
143
144 }
145
146 /**
147 * Open a zip file
148 *
149 * @param String $path - the filesystem path to the zip file
150 * @param Integer $flags - flags for the open operation (see ZipArchive::open() - N.B. may not all be implemented)
151 *
152 * @return Boolean - success or failure. Failure will set self::last_error
153 */
154 public function open($path, $flags = 0) {
155
156 if (!class_exists('PclZip')) include_once(ABSPATH.'/wp-admin/includes/class-pclzip.php');
157 if (!class_exists('PclZip')) {
158 $this->last_error = "No PclZip class was found";
159 return false;
160 }
161
162 // Route around PHP bug (exact version with the problem not known)
163 $ziparchive_create_match = (version_compare(PHP_VERSION, '5.2.12', '>') && defined('ZIPARCHIVE::CREATE')) ? ZIPARCHIVE::CREATE : 1;
164
165 if ($flags == $ziparchive_create_match && file_exists($path)) @unlink($path);
166
167 $this->pclzip = new PclZip($path);
168
169 if (empty($this->pclzip)) {
170 $this->last_error = 'Could not get a PclZip object';
171 return false;
172 }
173
174 // Make the empty directory we need to implement add_empty_dir()
175 global $updraftplus;
176 $updraft_dir = $updraftplus->backups_dir_location();
177 if (!is_dir($updraft_dir.'/emptydir') && !mkdir($updraft_dir.'/emptydir')) {
178 $this->last_error = "Could not create empty directory ($updraft_dir/emptydir)";
179 return false;
180 }
181
182 $this->path = $path;
183
184 return true;
185
186 }
187
188 /**
189 * Do the actual write-out - it is assumed that close() is where this is done. Needs to return true/false
190 *
191 * @return boolean
192 */
193 public function close() {
194
195 if (empty($this->pclzip)) {
196 $this->last_error = 'Zip file was not opened';
197 return false;
198 }
199
200 global $updraftplus;
201 $updraft_dir = $updraftplus->backups_dir_location();
202
203 $activity = false;
204
205 // Add the empty directories
206 foreach ($this->adddirs as $dir) {
207 if (false == $this->pclzip->add($updraft_dir.'/emptydir', PCLZIP_OPT_REMOVE_PATH, $updraft_dir.'/emptydir', PCLZIP_OPT_ADD_PATH, $dir)) {
208 $this->last_error = $this->pclzip->errorInfo(true);
209 return false;
210 }
211 $activity = true;
212 }
213
214 foreach ($this->addfiles as $rdirname => $adirnames) {
215 foreach ($adirnames as $adirname => $files) {
216 if (false == $this->pclzip->add($files, PCLZIP_OPT_REMOVE_PATH, $rdirname, PCLZIP_OPT_ADD_PATH, $adirname)) {
217 $this->last_error = $this->pclzip->errorInfo(true);
218 return false;
219 }
220 $activity = true;
221 }
222 unset($this->addfiles[$rdirname]);
223 }
224
225 $this->pclzip = false;
226 $this->addfiles = array();
227 $this->adddirs = array();
228
229 clearstatcache();
230
231 if ($activity && filesize($this->path) < 50) {
232 $this->last_error = "Write failed - unknown cause (check your file permissions)";
233 return false;
234 }
235
236 return true;
237 }
238
239 /**
240 * Note: basename($add_as) is irrelevant; that is, it is actually basename($file) that will be used. But these are always identical in our usage.
241 *
242 * @param string $file Specific file to add
243 * @param string $add_as This is the name of the file that it is added as but it is usually the same as $file
244 */
245 public function addFile($file, $add_as) {
246 // Add the files. PclZip appears to do the whole (copy zip to temporary file, add file, move file) cycle for each file - so batch them as much as possible. We have to batch by dirname(). On a test with 1000 files of 25KB each in the same directory, this reduced the time needed on that directory from 120s to 15s (or 5s with primed caches).
247 $rdirname = dirname($file);
248 $adirname = dirname($add_as);
249 $this->addfiles[$rdirname][$adirname][] = $file;
250 }
251
252 /**
253 * PclZip doesn't have a direct way to do this
254 *
255 * @param string $dir Specific Directory to empty
256 */
257 public function addEmptyDir($dir) {
258 $this->adddirs[] = $dir;
259 }
260
261 /**
262 * Extract a path
263 *
264 * @param String $path_to_extract
265 * @param String $path
266 *
267 * @see http://www.phpconcept.net/pclzip/user-guide/55
268 *
269 * @return Array|Integer - either an array with the extracted files or an error. N.B. "If one file extraction fail, the full extraction does not fail. The method does not return an error, but the file status is set with the error reason."
270 */
271 public function extract($path_to_extract, $path) {
272 return $this->pclzip->extract(PCLZIP_OPT_PATH, $path_to_extract, PCLZIP_OPT_BY_NAME, $path);
273 }
274 }
275
276 class UpdraftPlus_BinZip extends UpdraftPlus_PclZip {
277
278 private $binzip;
279
280 /**
281 * Class constructor
282 */
283 public function __construct() {
284 global $updraftplus_backup;
285 $this->binzip = $updraftplus_backup->binzip;
286 if (!is_string($this->binzip)) {
287 $this->last_error = "No binary zip was found";
288 return false;
289 }
290 return parent::__construct();
291 }
292
293 public function addFile($file, $add_as) {
294
295 global $updraftplus;
296 // Get the directory that $add_as is relative to
297 $base = UpdraftPlus_Manipulation_Functions::str_lreplace($add_as, '', $file);
298
299 if ($file == $base) {
300 // Shouldn't happen; but see: https://bugs.php.net/bug.php?id=62119
301 $updraftplus->log("File skipped due to unexpected name mismatch (locale: ".setlocale(LC_CTYPE, "0")."): file=$file add_as=$add_as", 'notice', false, true);
302 } else {
303 $rdirname = untrailingslashit($base);
304 // Note: $file equals $rdirname/$add_as
305 $this->addfiles[$rdirname][] = $add_as;
306 }
307
308 }
309
310 /**
311 * The standard zip binary cannot list; so we use PclZip for that
312 * Do the actual write-out - it is assumed that close() is where this is done. Needs to return true/false
313 *
314 * @return Boolean - success or failure state
315 */
316 public function close() {
317
318 if (empty($this->pclzip)) {
319 $this->last_error = 'Zip file was not opened';
320 return false;
321 }
322
323 global $updraftplus, $updraftplus_backup;
324 $updraft_dir = $updraftplus->backups_dir_location();
325
326 $activity = false;
327
328 // BinZip does not like zero-sized zip files
329 if (file_exists($this->path) && 0 == filesize($this->path)) @unlink($this->path);
330
331 $descriptorspec = array(
332 0 => array('pipe', 'r'),
333 1 => array('pipe', 'w'),
334 2 => array('pipe', 'w')
335 );
336 $exec = $this->binzip;
337 if (defined('UPDRAFTPLUS_BINZIP_OPTS') && UPDRAFTPLUS_BINZIP_OPTS) $exec .= ' '.UPDRAFTPLUS_BINZIP_OPTS;
338 $exec .= " -v -@ ".escapeshellarg($this->path);
339
340 $last_recorded_alive = time();
341 $something_useful_happened = $updraftplus->something_useful_happened;
342 $orig_size = file_exists($this->path) ? filesize($this->path) : 0;
343 $last_size = $orig_size;
344 clearstatcache();
345
346 $added_dirs_yet = false;
347
348 // If there are no files to add, but there are empty directories, then we need to make sure the directories actually get added
349 if (0 == count($this->addfiles) && 0 < count($this->adddirs)) {
350 $dir = realpath($updraftplus_backup->make_zipfile_source);
351 $this->addfiles[$dir] = '././.';
352 }
353 // Loop over each destination directory name
354 foreach ($this->addfiles as $rdirname => $files) {
355
356 $process = proc_open($exec, $descriptorspec, $pipes, $rdirname);
357
358 if (!is_resource($process)) {
359 $updraftplus->log('BinZip error: proc_open failed');
360 $this->last_error = 'BinZip error: proc_open failed';
361 return false;
362 }
363
364 if (!$added_dirs_yet) {
365 // Add the directories - (in fact, with binzip, non-empty directories automatically have their entries added; but it doesn't hurt to add them explicitly)
366 foreach ($this->adddirs as $dir) {
367 fwrite($pipes[0], $dir."/\n");
368 }
369 $added_dirs_yet = true;
370 }
371
372 $read = array($pipes[1], $pipes[2]);
373 $except = null;
374
375 if (!is_array($files) || 0 == count($files)) {
376 fclose($pipes[0]);
377 $write = array();
378 } else {
379 $write = array($pipes[0]);
380 }
381
382 while ((!feof($pipes[1]) || !feof($pipes[2]) || (is_array($files) && count($files)>0)) && false !== ($changes = @stream_select($read, $write, $except, 0, 200000))) {
383
384 if (is_array($write) && in_array($pipes[0], $write) && is_array($files) && count($files)>0) {
385 $file = array_pop($files);
386 // Send the list of files on stdin
387 fwrite($pipes[0], $file."\n");
388 if (0 == count($files)) fclose($pipes[0]);
389 }
390
391 if (is_array($read) && in_array($pipes[1], $read)) {
392 $w = fgets($pipes[1]);
393 // Logging all this really slows things down; use debug to mitigate
394 if ($w && $updraftplus_backup->debug) $updraftplus->log("Output from zip: ".trim($w), 'debug');
395 if (time() > $last_recorded_alive + 5) {
396 UpdraftPlus_Job_Scheduler::record_still_alive();
397 $last_recorded_alive = time();
398 }
399 if (file_exists($this->path)) {
400 $new_size = @filesize($this->path);
401 if (!$something_useful_happened && $new_size > $orig_size + 20) {
402 UpdraftPlus_Job_Scheduler::something_useful_happened();
403 $something_useful_happened = true;
404 }
405 clearstatcache();
406 // Log when 20% bigger or at least every 50MB
407 if ($new_size > $last_size*1.2 || $new_size > $last_size + 52428800) {
408 $updraftplus->log(basename($this->path).sprintf(": size is now: %.2f MB", round($new_size/1048576, 1)));
409 $last_size = $new_size;
410 }
411 }
412 }
413
414 if (is_array($read) && in_array($pipes[2], $read)) {
415 $last_error = fgets($pipes[2]);
416 if (!empty($last_error)) $this->last_error = rtrim($last_error);
417 }
418
419 // Re-set
420 $read = array($pipes[1], $pipes[2]);
421 $write = (is_array($files) && count($files) >0) ? array($pipes[0]) : array();
422 $except = null;
423
424 }
425
426 fclose($pipes[1]);
427 fclose($pipes[2]);
428
429 $ret = proc_close($process);
430
431 if (0 != $ret && 12 != $ret) {
432 if ($ret < 128) {
433 $updraftplus->log("Binary zip: error (code: $ret - look it up in the Diagnostics section of the zip manual at http://infozip.sourceforge.net/FAQ.html#error-codes for interpretation... and also check that your hosting account quota is not full)");
434 } else {
435 $updraftplus->log("Binary zip: error (code: $ret - a code above 127 normally means that the zip process was deliberately killed ... and also check that your hosting account quota is not full)");
436 }
437 if (!empty($w) && !$updraftplus_backup->debug) $updraftplus->log("Last output from zip: ".trim($w), 'debug');
438 return false;
439 }
440
441 unset($this->addfiles[$rdirname]);
442 }
443
444 return true;
445 }
446 }
447