PluginProbe ʕ •ᴥ•ʔ
WP STAGING – WordPress Backups, Restore, Migration & Clone / 4.11.0
WP STAGING – WordPress Backups, Restore, Migration & Clone v4.11.0
4.11.0 4.10.0 4.9.5 4.9.4 4.9.3 4.9.2 4.9.1 4.9.0 4.8.1 trunk 3.0.0 3.0.1 3.0.2 3.0.3 3.0.4 3.0.5 3.0.6 3.1.0 3.1.1 3.1.2 3.1.3 3.1.4 3.10.0 3.2.0 3.3.1 3.3.2 3.3.3 3.4.1 3.4.3 3.5.0 3.6.0 3.7.1 3.8.0 3.8.1 3.8.2 3.8.3 3.8.4 3.8.5 3.8.6 3.8.7 3.9.0 3.9.1 3.9.2 3.9.3 3.9.4 4.0.0 4.1.0 4.1.1 4.1.2 4.1.3 4.1.4 4.2.0 4.2.1 4.3.0 4.3.1 4.3.2 4.4.0 4.5.0 4.6.0 4.7.0 4.7.1 4.7.2 4.7.3 4.8.0
wp-staging / Backend / Modules / Jobs / Files.php
wp-staging / Backend / Modules / Jobs Last commit date
Cleaners 1 day ago Exceptions 1 day ago Cancel.php 1 day ago CancelUpdate.php 1 day ago Cloning.php 1 day ago CloningProcess.php 1 day ago Data.php 1 day ago Database.php 1 day ago Delete.php 1 day ago Directories.php 1 day ago Files.php 1 day ago Finish.php 1 day ago Job.php 1 day ago JobExecutable.php 1 day ago Logs.php 1 day ago PreserveDataFirstStep.php 1 day ago PreserveDataSecondStep.php 1 day ago ProcessLock.php 1 day ago Scan.php 1 day ago SearchReplace.php 1 day ago TotalStepsAreNumberOfTables.php 1 day ago Updating.php 1 day ago
Files.php
741 lines
1 <?php
2
3 namespace WPStaging\Backend\Modules\Jobs;
4
5 use Exception;
6 use RuntimeException;
7 use WPStaging\Backend\Modules\Jobs\Cleaners\WpContentCleaner;
8 use WPStaging\Framework\Job\Exception\DiskNotWritableException;
9 use WPStaging\Core\Utils\Logger;
10 use WPStaging\Core\WPStaging;
11 use WPStaging\Framework\Adapter\Directory;
12 use WPStaging\Framework\Filesystem\FileObject;
13 use WPStaging\Framework\Filesystem\Filesystem;
14 use WPStaging\Framework\Filesystem\FilesystemExceptions;
15 use WPStaging\Framework\Filesystem\PathChecker;
16 use WPStaging\Framework\Filesystem\PathIdentifier;
17 use WPStaging\Framework\Filesystem\Permissions;
18 use WPStaging\Framework\Filesystem\WpUploadsFolderSymlinker;
19 use WPStaging\Framework\SiteInfo;
20 use WPStaging\Framework\Utils\Cache\Cache;
21 use WPStaging\Framework\Utils\Strings;
22 use WPStaging\Framework\Traits\EndOfLinePlaceholderTrait;
23
24
25
26
27
28
29
30
31
32
33 class Files extends JobExecutable
34 {
35 use EndOfLinePlaceholderTrait;
36
37
38 protected $strUtil;
39
40
41
42
43 protected $destination;
44
45
46
47
48 private $file;
49
50
51
52
53 private $maxFilesPerRun;
54
55
56
57
58 private $permissions;
59
60
61
62
63 private $filesystem;
64
65
66 private $pathAdapter;
67
68
69 private $pathChecker;
70
71
72 private $directory;
73
74
75
76
77 private $rootPath;
78
79
80
81
82 private $contentPath;
83
84
85
86
87 private $siteInfo;
88
89
90
91
92
93 public function initialize()
94 {
95 $this->permissions = new Permissions();
96
97
98 $this->filesystem = WPStaging::make(Filesystem::class);
99
100 $this->directory = WPStaging::make(Directory::class);
101 $this->pathAdapter = WPStaging::make(PathIdentifier::class);
102 $this->pathChecker = WPStaging::make(PathChecker::class);
103 $this->siteInfo = WPStaging::make(SiteInfo::class);
104 $this->strUtil = WPStaging::make(Strings::class);
105 $this->rootPath = rtrim($this->directory->getAbsPath(), '/');
106 $this->contentPath = rtrim($this->directory->getWpContentDirectory(), '/');
107 $this->destination = $this->filesystem->normalizePath($this->options->destinationDir);
108
109 $filePath = $this->getFilesIndexCacheFilePath();
110
111 if (is_file($filePath)) {
112 $this->file = new FileObject($filePath, 'r');
113 } elseif ($this->options->totalFiles !== 0) {
114 $this->returnException(sprintf('Fatal Error: Files - File: %s is missing! Either the file was deleted after directory scanning or there is a permission issue with the file system.', $filePath));
115 }
116
117 $logStep = 0;
118 if ($this->isUpdateOrResetJob()) {
119 $logStep = 1;
120 }
121
122
123 if ($this->options->currentStep === $logStep) {
124 $this->log("Copying files...");
125 }
126
127 $this->settings->batchSize = $this->settings->batchSize * 1000000;
128 $this->maxFilesPerRun = $this->settings->fileLimit;
129 }
130
131
132
133
134
135 protected function calculateTotalSteps()
136 {
137 $this->options->totalSteps = ceil($this->options->totalFiles / $this->maxFilesPerRun);
138
139
140 if ($this->isUpdateOrResetJob()) {
141 $this->options->totalSteps++;
142 }
143
144
145
146 if ($this->options->totalSteps == 0) {
147 $this->options->totalSteps = 1;
148 }
149 }
150
151
152
153
154
155
156
157 protected function execute()
158 {
159
160 if ($this->isFinished()) {
161 $this->symlinkUploadFolder();
162 $this->log("Copying files finished");
163 $this->prepareResponse(true, false);
164 return false;
165 }
166
167
168 if (!$this->cleanStagingDirectory()) {
169 $this->prepareResponse(false, false);
170 return false;
171 }
172
173
174 if (!$this->cleanWpContent()) {
175 $this->prepareResponse(false, false);
176 return false;
177 }
178
179
180 if (!$this->getFilesAndCopy()) {
181 $this->prepareResponse(false, false);
182 return false;
183 }
184
185
186 $this->prepareResponse();
187
188
189 return true;
190 }
191
192
193
194
195
196
197
198 private function cleanStagingDirectory()
199 {
200 if ($this->options->mainJob !== Job::RESET) {
201 return true;
202 }
203
204 if ($this->options->currentStep !== 0) {
205 return true;
206 }
207
208 if (rtrim($this->destination, '/') === rtrim(get_home_path(), '/')) {
209 $this->returnException('Can not delete directory: ' . $this->destination . '. This seems to be the root directory. Exclude this directory from deleting and try again.');
210 throw new \Exception('Can not delete directory: ' . $this->destination . ' This seems to be the root directory. Exclude this directory from deleting and try again.');
211 }
212
213
214 if (empty($this->destination) || !is_dir($this->destination)) {
215 $this->log(sprintf(__('Fail! Destination is not a directory! %s', 'wp-staging'), $this->destination));
216 return true;
217 }
218
219 if (!isset($this->options->filesResettingStatus)) {
220 $this->options->filesResettingStatus = 'pending';
221 $this->saveOptions();
222 }
223
224 if ($this->options->filesResettingStatus === 'finished') {
225 return true;
226 }
227
228 if ($this->options->filesResettingStatus === 'pending') {
229 $this->log(sprintf(__('Files: Resetting staging site: %s.', 'wp-staging'), $this->destination));
230 $this->options->filesResettingStatus = 'processing';
231 $this->saveOptions();
232 }
233
234 $fs = new Filesystem();
235 $fs->setShouldStop([$this, 'isOverThreshold'])
236 ->shouldPermissionExceptionsBypass(true)
237 ->setRecursive(true);
238 try {
239 if (!$fs->delete($this->destination, false)) {
240 foreach ($fs->getLogs() as $log) {
241 $this->log($log, Logger::TYPE_WARNING);
242 }
243
244 return false;
245 }
246 } catch (RuntimeException $ex) {
247 }
248
249 foreach ($fs->getLogs() as $log) {
250 $this->log($log, Logger::TYPE_WARNING);
251 }
252
253 $this->options->filesResettingStatus = 'finished';
254 $this->saveOptions();
255
256 $this->prepareResponse();
257 return true;
258 }
259
260
261
262
263
264
265 private function cleanWpContent()
266 {
267 if ($this->options->mainJob !== Job::UPDATE) {
268 return true;
269 }
270
271 if ($this->options->currentStep !== 0) {
272 return true;
273 }
274
275
276 $contentCleaner = new WpContentCleaner($this);
277
278 $result = $contentCleaner->tryCleanWpContent($this->destination);
279 foreach ($contentCleaner->getLogs() as $log) {
280 if ($log['type'] === Logger::TYPE_ERROR) {
281 $this->log($log['msg'], $log['type']);
282 $this->returnException($log['msg']);
283 } else {
284 $this->debugLog($log['msg'], $log['type']);
285 }
286 }
287
288 if (!$result) {
289 return false;
290 }
291
292 return true;
293 }
294
295
296
297
298
299
300 private function getFilesAndCopy()
301 {
302 if ($this->options->currentStep === 0 && ($this->isUpdateOrResetJob())) {
303 return true;
304 }
305
306
307 if ($this->isOverThreshold()) {
308
309 $this->prepareResponse(false, false);
310 $this->saveOptions();
311 return false;
312 }
313
314
315 if (isset($this->options->copiedFiles) && $this->options->copiedFiles != 0) {
316 $this->file->seek($this->options->copiedFiles - 1);
317 }
318
319 $this->file->setFlags(FileObject::DROP_NEW_LINE);
320
321 for ($i = 0; $i < $this->maxFilesPerRun; $i++) {
322
323
324 $this->options->copiedFiles++;
325
326
327 if ($this->file->eof()) {
328 break;
329 }
330
331 $file = trim($this->file->readAndMoveNext());
332
333
334 if ($file === trim(Cache::PHP_HEADER)) {
335 continue;
336 }
337
338 $file = $this->replacePlaceholdersWithEOLs($file);
339
340 if (empty($file)) {
341 continue;
342 }
343
344 $this->copyFile($file);
345 }
346
347 $totalFiles = $this->options->copiedFiles;
348
349 if ($this->options->copiedFiles % 50 == 0) {
350 $this->log("Total {$totalFiles} files processed");
351 }
352
353 return true;
354 }
355
356
357
358
359
360 private function symlinkUploadFolder()
361 {
362
363 if ($this->options->mainJob === Job::UPDATE) {
364 return true;
365 }
366
367 if (!$this->options->uploadsSymlinked) {
368 $this->log(__("Skipped symlinking WP Uploads Folder", 'wp-staging'));
369 return true;
370 }
371
372 $symlinker = WPStaging::make(WpUploadsFolderSymlinker::class);
373 $symlinker->setStagingPath($this->options->destinationDir);
374 if ($symlinker->trySymlink()) {
375 $this->log(__("Uploads Folder symlinked with the production site", 'wp-staging'));
376 return true;
377 }
378
379 $this->returnException($symlinker->getError());
380 return false;
381 }
382
383
384
385
386
387 private function isFinished()
388 {
389 return
390 !$this->isRunning() ||
391 $this->options->currentStep >= $this->options->totalSteps ||
392 $this->options->copiedFiles >= $this->options->totalFiles;
393 }
394
395
396
397
398
399 private function copyFile($file)
400 {
401 $basePath = $this->rootPath;
402 $isContent = false;
403 if ($this->pathAdapter->getIdentifierFromPath($file) === PathIdentifier::IDENTIFIER_WP_CONTENT) {
404 $basePath = $this->contentPath;
405 $isContent = true;
406 }
407
408 $filePath = $this->pathAdapter->transformIdentifiableToPath($file);
409 $file = $this->filesystem->maybeNormalizePath($filePath);
410 $directory = dirname($file);
411
412
413 if ($this->isDirectoryExcluded($directory)) {
414 $this->debugLog("Skipping directory by rule: {$file}", Logger::TYPE_INFO);
415 return false;
416 }
417
418
419 if ($this->isFileExcluded($file)) {
420 $this->debugLog("Skipping file by rule: {$file}", Logger::TYPE_INFO);
421 return false;
422 }
423
424
425 if ($this->isFileExcludedFullPath($file)) {
426 $this->options->tmpExcludedFilesFullPath[] = $file;
427 $this->debugLog("Skipping file by rule: {$file}", Logger::TYPE_INFO);
428 return false;
429 }
430
431
432 if (!is_file($file) && !is_dir($file)) {
433 $this->log("File doesn't exist {$file}", Logger::TYPE_WARNING);
434 return true;
435 }
436
437
438 if (!$this->filesystem->isReadableFile($file)) {
439 $this->log("Can't read file {$file}", Logger::TYPE_WARNING);
440 return true;
441 }
442
443
444 $fileSize = filesize($file);
445
446
447 if ($fileSize >= $this->settings->maxFileSize * 1000000) {
448 $this->log("Skipping big file: {$file}", Logger::TYPE_INFO);
449 return false;
450 }
451
452
453 if (($destination = $this->getDestination($file, $basePath, $isContent)) === false) {
454 $this->log("Can't get the destination of {$file}", Logger::TYPE_WARNING);
455 return false;
456 }
457
458 if ($file === $destination) {
459 $this->log("Skipping file copying: Destination same as source: {$destination}", Logger::TYPE_INFO);
460 return false;
461 }
462
463
464 if ($fileSize >= $this->settings->batchSize) {
465 return $this->copyBig($file, $destination, $this->settings->batchSize);
466 }
467
468
469 try {
470 if (is_dir($file)) {
471 $this->filesystem->copy($file, $destination);
472
473 @chmod($destination, $this->permissions->getDirectoryOctal());
474 } else {
475 $this->filesystem->copyFile($file, $destination);
476
477 @chmod($destination, $this->permissions->getFilePermission($destination));
478 }
479 } catch (RuntimeException $ex) {
480 $this->log('Files: ' . $ex->getMessage(), Logger::TYPE_ERROR);
481 return false;
482 }
483
484 $this->setDirPermissions($destination);
485
486 return true;
487 }
488
489
490
491
492
493
494
495
496
497 private function setDirPermissions($file)
498 {
499 $dir = dirname($file);
500 if (is_dir($dir)) {
501 @chmod($dir, $this->permissions->getDirectoryOctal());
502 }
503
504 return false;
505 }
506
507
508
509
510
511
512
513
514
515 protected function getDestination($file, $basePath, $isContent = false)
516 {
517 $file = $this->filesystem->normalizePath($file);
518 $relativePath = $this->strUtil->replaceStartWith($basePath, '', $file);
519 $destinationPath = $this->destination . $relativePath;
520
521 if ($isContent && $this->shouldUseDefaultWpContentPath()) {
522 $destinationPath = $this->destination . 'wp-content/' . $relativePath;
523 } elseif ($isContent) {
524 $absPath = $this->filesystem->normalizePath(ABSPATH);
525 $destinationPath = $this->strUtil->replaceStartWith($absPath, $this->destination, $file);
526 }
527
528 $destinationDirectory = dirname($destinationPath);
529
530 $isDirectoryNotCreated = !is_dir($destinationDirectory) && !$this->filesystem->mkdir($destinationDirectory) && !is_dir($destinationDirectory);
531 if ($isDirectoryNotCreated) {
532 $this->log("Files: Can not create directory {$destinationDirectory}." . $this->filesystem->getLogs()[0], Logger::TYPE_ERROR);
533 return false;
534 }
535
536 return $this->filesystem->normalizePath($destinationPath);
537 }
538
539
540
541
542 protected function shouldUseDefaultWpContentPath(): bool
543 {
544
545 return $this->siteInfo->isWpContentOutsideAbspath();
546 }
547
548
549
550
551
552
553
554
555 private function copyBig($src, $dst, $bufferSize)
556 {
557 $src = fopen($src, 'rb');
558 $dest = fopen($dst, 'wb');
559
560 if (!$src || !$dest) {
561 return false;
562 }
563
564
565 while (!feof($src)) {
566 if (fwrite($dest, fread($src, $bufferSize)) === false) {
567 $error = true;
568 }
569 }
570
571 if (isset($error) && ($error === true)) {
572 while (!feof($src)) {
573 if (stream_copy_to_stream($src, $dest, 1024) === false) {
574 $this->log("Can not copy file; {$src} -> {$dest}");
575 fclose($src);
576 fclose($dest);
577 return false;
578 }
579 }
580 }
581
582
583 fclose($src);
584 fclose($dest);
585 return true;
586 }
587
588
589
590
591
592
593
594 private function isFileExcluded($file)
595 {
596 $excludedFiles = (array)$this->options->excludedFiles;
597
598
599
600 if ($this->isIdenticalHostname() === false && $this->options->mainJob !== Job::UPDATE) {
601 $excludedFiles = \array_diff(
602 $excludedFiles,
603 ["web.config", ".htaccess"]
604 );
605 }
606
607 $isExcluded = $this->filesystem->isFilenameExcluded($file, $excludedFiles, true);
608 if ($isExcluded !== false) {
609 $this->options->tmpExcludedFilesFullPath[] = $isExcluded;
610 return true;
611 }
612
613
614
615 if (
616 isset($this->options->mainJob) && $this->options->mainJob === Job::UPDATE
617 && stripos(strrev($file), strrev("wp-config.php")) === 0
618 ) {
619 return true;
620 }
621
622 return false;
623 }
624
625
626
627
628
629
630 private function isIdenticalHostname()
631 {
632
633 $siteurl = get_site_url();
634 $url = parse_url($siteurl);
635 $productionHostname = $url['host'];
636
637
638 $cloneUrl = empty($this->options->cloneHostname) ? $url : parse_url($this->options->cloneHostname);
639 $targetHostname = $cloneUrl['host'];
640
641
642
643 if (wpstg_starts_with($productionHostname, $targetHostname)) {
644 return true;
645 }
646
647 return false;
648 }
649
650
651
652
653
654
655
656 private function isFileExcludedFullPath($file)
657 {
658
659 foreach ($this->options->excludedFilesFullPath as $excludedFile) {
660
661 try {
662 $excludedFileFullPath = $this->pathAdapter->transformIdentifiableToPath($excludedFile);
663 } catch (Exception $ex) {
664 $excludedFileFullPath = $excludedFile;
665 }
666
667 if ($file === $excludedFileFullPath) {
668 return true;
669 }
670 }
671
672 return false;
673 }
674
675
676
677
678
679
680
681
682
683
684 private function sanitizeDirectorySeparator($path)
685 {
686 return preg_replace('/[\\\\]+/', '/', $path);
687 }
688
689
690
691
692
693
694
695
696 private function isDirectoryExcluded($directory)
697 {
698 $abspath = $this->sanitizeDirectorySeparator(ABSPATH);
699 $directory = $this->sanitizeDirectorySeparator($directory);
700
701 if ($abspath === $directory . '/') {
702 return false;
703 }
704
705
706 if (strpos($directory, 'wp-staging') !== false || strpos($directory, 'wp-staging-pro') !== false) {
707 return false;
708 }
709
710 if ($this->isExtraDirectory($directory)) {
711 return false;
712 }
713
714 return $this->pathChecker->isPathInPathsList($directory, $this->options->excludedDirectories, false);
715 }
716
717
718
719
720
721
722 private function isExtraDirectory($directory)
723 {
724 $directory = $this->sanitizeDirectorySeparator($directory);
725
726 foreach ($this->options->extraDirectories as $extraDirectory) {
727 $extraDirectory = trim($extraDirectory);
728
729 if (empty($extraDirectory)) {
730 continue;
731 }
732
733 if (strpos($directory, $this->sanitizeDirectorySeparator($extraDirectory)) === 0) {
734 return true;
735 }
736 }
737
738 return false;
739 }
740 }
741