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 / Framework / Filesystem / Filesystem.php
wp-staging / Framework / Filesystem Last commit date
Filters 1 day ago Scanning 1 day ago AbstractFileObject.php 1 day ago AbstractFilesystemScanner.php 1 day ago DebugLogReader.php 1 day ago DirectoryListing.php 1 day ago DirectorySize.php 1 day ago DiskWriteCheck.php 1 day ago FileObject.php 1 day ago Filesystem.php 1 day ago FilesystemExceptions.php 5 years ago FilesystemScanner.php 1 day ago FilesystemScannerDto.php 1 day ago FilterableDirectoryIterator.php 1 day ago LegacyFileRulesTrait.php 1 day ago LogCleanup.php 1 day ago LogFiles.php 1 day ago MissingFileException.php 3 years ago OPcache.php 1 day ago PartIdentifier.php 1 day ago PathChecker.php 1 day ago PathIdentifier.php 1 day ago Permissions.php 1 day ago WpUploadsFolderSymlinker.php 1 day ago
Filesystem.php
1160 lines
1 <?php
2
3 namespace WPStaging\Framework\Filesystem;
4
5 use Exception;
6 use SplFileInfo;
7 use WPStaging\Framework\Notices\Notices;
8 use WPStaging\Core\WPStaging;
9 use WPStaging\Vendor\Psr\Log\LoggerInterface;
10 use RuntimeException;
11 use WPStaging\Backend\Pro\Modules\Jobs\Copiers\Copier;
12 use WPStaging\Framework\Adapter\PhpAdapter;
13
14 use function WPStaging\functions\debug_log;
15
16
17
18
19
20
21
22 class Filesystem extends FilterableDirectoryIterator
23 {
24
25 const BACKUP_FILE_EXTENSION = ['.wpstg', '.wpstg.sql'];
26
27
28 private $path;
29
30
31 private $shouldStop;
32
33
34 private $depth;
35
36
37 private $fileNames;
38
39
40 private $logger;
41
42
43 private $bypassPermissionExceptions;
44
45
46 private $logs = [];
47
48
49 private $phpAdapter;
50
51
52 private $processed;
53
54
55
56
57
58 private $useCopyFunction = true;
59
60
61
62
63 public function __construct()
64 {
65 parent::__construct();
66 $this->phpAdapter = new PhpAdapter();
67 }
68
69
70
71
72 public function getLogs()
73 {
74 return $this->logs;
75 }
76
77
78
79
80
81
82 public function safePath(string $fullPath)
83 {
84 $safePath = realpath(dirname($fullPath));
85 if (!$safePath) {
86 return null;
87 }
88
89 $safePath = ABSPATH . str_replace(ABSPATH, '', $safePath);
90 $safePath .= DIRECTORY_SEPARATOR . basename($fullPath);
91 return $safePath;
92 }
93
94
95
96
97
98
99
100
101
102 public function move(string $source, string $target): bool
103 {
104
105 if (is_link($source) || is_file($source)) {
106 return $this->renameDirect($source, $target);
107 }
108
109
110 if ($this->isEmptyDir($source)) {
111 return wp_mkdir_p($target) && @rmdir($source);
112 }
113
114 $this->setDirectory($source);
115 $iterator = null;
116 try {
117
118 $iterator = $this->setIteratorMode(\RecursiveIteratorIterator::CHILD_FIRST)->get();
119 } catch (FilesystemExceptions $e) {
120 $this->log('Permission Error: Can not create recursive iterator for ' . $source);
121 return false;
122 }
123
124 $basePath = trailingslashit($target);
125 foreach ($iterator as $item) {
126 if ($item->isDir() && !$this->isEmptyDir($item->getPathname())) {
127 continue;
128 }
129
130 $relativeFilePath = $iterator->getFilename();
131 if ($this->isIteratorRecursive()) {
132 $relativeFilePath = $iterator->getSubPathName();
133 }
134
135 $destination = $basePath . $relativeFilePath;
136 if (file_exists($destination)) {
137 continue;
138 }
139
140 $result = false;
141
142 if ($item->isDir()) {
143 $result = wp_mkdir_p($destination) && @rmdir($item->getPathname());
144 } else {
145 $result = $this->renameDirect($item->getPathname(), $destination);
146 }
147
148 if (!$result || !$this->phpAdapter->isCallable($this->shouldStop)) {
149 continue;
150 }
151
152 if (call_user_func($this->shouldStop)) {
153 return false;
154 }
155 }
156
157 $deleteSelf = true;
158 if (count($this->getExcludePaths()) > 0 || !$this->isIteratorRecursive()) {
159 $deleteSelf = false;
160 }
161
162 return $this->delete($source, $deleteSelf);
163 }
164
165
166
167
168
169
170
171 public function renameDirect(string $source, string $target): bool
172 {
173 $dir = dirname($target);
174 if (!file_exists($dir)) {
175 $this->mkdir($dir);
176 }
177
178 $renamed = @rename($source, $target);
179
180 if (!$renamed) {
181 $this->log(sprintf('Failed to move %s to %s', $source, $target));
182 }
183
184 return $renamed;
185 }
186
187
188
189
190
191
192
193
194
195
196
197 public function moveFileOrDir(string $source, string $dest): bool
198 {
199 if (is_dir($source)) {
200 return $this->moveDirRecursively($source, $dest);
201 }
202
203 try {
204 if (!@copy($source, $dest)) {
205 return false;
206 }
207
208 @unlink($source);
209 return true;
210 } catch (\Throwable $th) {
211 debug_log("Failed to copy $source in moveFileOrDir. Error message: " . $th->getMessage());
212 return false;
213 }
214 }
215
216
217
218
219
220
221
222 private function moveDirRecursively(string $source, string $dest): bool
223 {
224 if (!is_dir($source)) {
225 debug_log("moveDirRecursively() - Is no dir: $source.");
226 return false;
227 }
228
229 if (!$this->mkdir($dest)) {
230 return false;
231 }
232
233
234 $iterator = new \RecursiveIteratorIterator(
235 new \RecursiveDirectoryIterator($source, \RecursiveDirectoryIterator::SKIP_DOTS),
236 \RecursiveIteratorIterator::SELF_FIRST
237 );
238
239 $copySucceed = true;
240 foreach ($iterator as $item) {
241
242 if ($item->isLink()) {
243 continue;
244 }
245
246 if ($item->isDir() && !$this->mkdir(trailingslashit($dest) . $iterator->getSubPathname())) {
247 $copySucceed = false;
248 continue;
249 }
250
251 if (!$item->isFile()) {
252 continue;
253 }
254
255 if (!$this->moveFileOrDir($item->getPathname(), trailingslashit($dest) . $iterator->getSubPathname())) {
256 $copySucceed = false;
257 }
258 }
259
260 if ($copySucceed && !$this->delete($source, true)) {
261 debug_log("moveDirRecursively() - Failed to delete $source.");
262 }
263
264 return true;
265 }
266
267
268
269
270
271
272
273 public function mkdir($path, bool $detectDirectoryListing = false): string
274 {
275 $path = $this->findPath($path);
276
277
278
279
280
281
282
283
284 if (strpos($path, '//') === 0) {
285 $path = '\\\\' . substr($path, 2);
286 }
287
288 set_error_handler([$this, 'handleMkdirError']);
289 $result = $this->recursiveCreateDirectory($path);
290 restore_error_handler();
291 if (!$result) {
292 \WPStaging\functions\debug_log("Failed to create directory $path");
293
294 return '';
295 }
296
297 if (!$detectDirectoryListing) {
298 return trailingslashit($path);
299 }
300
301
302 $directoryListing = WPStaging::getInstance()->getContainer()->get(DirectoryListing::class);
303 try {
304 $directoryListing->preventDirectoryListing($path);
305 } catch (\Exception $e) {
306
307
308
309
310
311 WPStaging::getInstance()->getContainer()->pushToArray(Notices::$directoryListingErrors, $e->getMessage());
312 }
313
314 return trailingslashit($path);
315 }
316
317
318
319
320
321
322
323
324 public function copy(string $source, string $target): bool
325 {
326
327 if (is_link($source) || is_file($source)) {
328 $this->mkdir(dirname($target));
329 return copy($source, $target);
330 }
331
332
333 if ($this->isEmptyDir($source)) {
334 return wp_mkdir_p($target);
335 }
336
337 $this->setDirectory($source);
338 $iterator = null;
339 try {
340
341 $iterator = $this->setIteratorMode(\RecursiveIteratorIterator::CHILD_FIRST)->get();
342 } catch (FilesystemExceptions $e) {
343 $this->log('Permission Error: Can not create recursive iterator for ' . $source);
344 return false;
345 }
346
347 $basePath = trailingslashit($target);
348 foreach ($iterator as $item) {
349 if ($item->isDir() && !$this->isEmptyDir($item->getPathname())) {
350 continue;
351 }
352
353 $relativeFilePath = $iterator->getFilename();
354 if ($this->isIteratorRecursive()) {
355 $relativeFilePath = $iterator->getSubPathName();
356 }
357
358 $destination = $basePath . $relativeFilePath;
359 if (file_exists($destination)) {
360 continue;
361 }
362
363 $result = false;
364
365 if ($item->isDir()) {
366 $result = wp_mkdir_p($destination);
367 } else {
368 $this->mkdir(dirname($destination));
369 $result = copy($item->getPathname(), $destination);
370 }
371
372 if (!$result || !$this->phpAdapter->isCallable($this->shouldStop)) {
373 continue;
374 }
375
376 if (call_user_func($this->shouldStop)) {
377 return false;
378 }
379 }
380
381 return true;
382 }
383
384
385
386
387
388
389 public function isEmptyDir(string $dir): bool
390 {
391 if (is_dir($dir)) {
392 $iterator = new \FilesystemIterator($dir);
393 return !$iterator->valid();
394 }
395
396 return true;
397 }
398
399
400
401
402
403
404
405
406
407
408
409
410
411 public function delete($path = null, bool $deleteSelf = true, bool $throw = false): bool
412 {
413 $path = $this->findPath($path);
414
415 if ($path === ABSPATH) {
416 $this->log('You can not delete WP Root directory');
417 throw new RuntimeException('You can not delete WP Root directory');
418 }
419
420 clearstatcache();
421
422
423 if (is_link($path) || is_file($path)) {
424 if (!@unlink($path)) {
425 $this->log('Permission Error: Can not delete file ' . $path);
426 return false;
427 }
428
429 $this->processed++;
430 return true;
431 }
432
433
434 if (!is_dir($path)) {
435 return true;
436 }
437
438
439 if (is_dir($path) && $this->isEmptyDir($path) && $deleteSelf) {
440 if (!@rmdir($path)) {
441 $this->log('Permission Error: Can not delete directory ' . $path);
442 return false;
443 }
444
445 $this->processed++;
446 return true;
447 }
448
449
450 if (is_dir($path) && $this->isEmptyDir($path) && !$deleteSelf) {
451 return true;
452 }
453
454 $this->setDirectory($path);
455 $originalIsRecursive = (bool)$this->isIteratorRecursive();
456 try {
457
458
459
460
461
462
463 if ($this->isIteratorRecursive() === null) {
464 $this->setRecursive();
465 }
466
467 $iterator = $this->setIteratorMode(\RecursiveIteratorIterator::CHILD_FIRST)->get();
468 } catch (FilesystemExceptions $e) {
469 $this->log('Permission Error: Can not create recursive iterator for ' . $path);
470 if ($throw) {
471 $this->setRecursive($originalIsRecursive);
472
473 throw $e;
474 } else {
475 $this->setRecursive($originalIsRecursive);
476 return false;
477 }
478 }
479
480 foreach ($iterator as $item) {
481 $result = false;
482
483 try {
484 $result = $this->deleteItem($item);
485 $this->processed++;
486 } catch (RuntimeException $e) {
487 if ($this->arePermissionExceptionsBypassed() !== true) {
488 $this->setRecursive($originalIsRecursive);
489
490 throw $e;
491 }
492 }
493
494 if (!$result || !$this->phpAdapter->isCallable($this->shouldStop)) {
495 continue;
496 }
497
498 if (call_user_func($this->shouldStop)) {
499 $this->setRecursive($originalIsRecursive);
500 return false;
501 }
502 }
503
504
505 if (!$deleteSelf || !$this->isEmptyDir($path)) {
506 $this->setRecursive($originalIsRecursive);
507 return true;
508 }
509
510
511 if (is_dir($path)) {
512 if (!@rmdir($path)) {
513 $this->log('Permission Error: Can not delete directory ' . $path);
514 }
515 }
516
517 $this->setRecursive($originalIsRecursive);
518 $this->processed++;
519 return true;
520 }
521
522
523
524
525
526
527
528
529 public function isFilenameExcluded(string $file, array $excludedFiles, bool $returnPattern = false)
530 {
531 $filename = basename($file);
532
533
534 if (in_array($filename, $excludedFiles, true)) {
535 if ($returnPattern) {
536 return $filename;
537 }
538
539 return true;
540 }
541
542
543 foreach ($excludedFiles as $pattern) {
544 if ($this->fnmatch($pattern, $filename)) {
545 if ($returnPattern) {
546 return $pattern;
547 }
548
549 return true;
550 }
551 }
552
553 return false;
554 }
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569 protected function fnmatch(string $pattern, string $string, array $options = []): bool
570 {
571 if ($pattern === '*' && empty($options['filePath'])) {
572 return true;
573 }
574
575 $replacements = [
576 '\\\\\\\\' => '\\\\',
577 '\\\\\\*' => '[*]',
578 '\\\\\\?' => '[?]',
579 '\*' => '.*',
580 '\?' => '.',
581 '\[\!' => '[^',
582 '\[' => '[',
583 '\]' => ']',
584 '\-' => '-',
585 ];
586
587 if (isset($options['escape']) && !$options['escape']) {
588 unset($replacements['\\\\\\\\'], $replacements['\\\\\\*'], $replacements['\\\\\\?']);
589 }
590
591 if (!empty($options['filePath'])) {
592 $replacements['\*'] = '[^/\\\\]*';
593 $replacements['\?'] = '[^/\\\\]';
594 }
595
596 $pattern = strtr(preg_quote($pattern, '#'), $replacements);
597 $pattern = '#^' . $pattern . '$#us';
598 if (isset($options['caseSensitive']) && !$options['caseSensitive']) {
599 $pattern .= 'i';
600 }
601
602 return preg_match($pattern, $string) === 1;
603 }
604
605
606
607
608
609 public function deletePaths(array $paths): bool
610 {
611 foreach ($paths as $path) {
612
613
614 if (is_dir($path) && $this->isEmptyDir($path)) {
615 if (!@rmdir($path)) {
616 $this->log('Permission Error: Can not delete directory ' . $path);
617 throw new RuntimeException('Permission Error: Can not delete directory ' . $path);
618 }
619
620 continue;
621 }
622
623
624 if (!$this->delete($path, false)) {
625 return false;
626 }
627 }
628
629 return true;
630 }
631
632
633
634
635
636 public function findPath($path)
637 {
638 return $path ?: $this->path;
639 }
640
641
642
643
644 public function arePermissionExceptionsBypassed()
645 {
646 return $this->bypassPermissionExceptions;
647 }
648
649
650
651
652
653 public function shouldPermissionExceptionsBypass($flag)
654 {
655 $this->bypassPermissionExceptions = $flag;
656 return $this;
657 }
658
659
660
661
662 public function getPath()
663 {
664 return $this->path;
665 }
666
667
668
669
670
671 public function setPath($path)
672 {
673 $this->path = $path;
674 return $this;
675 }
676
677
678
679
680 public function getShouldStop()
681 {
682 return $this->shouldStop;
683 }
684
685
686
687
688
689 public function setShouldStop($shouldStop = null)
690 {
691 $this->shouldStop = $shouldStop;
692 return $this;
693 }
694
695
696
697
698 public function getDepth()
699 {
700 return $this->depth;
701 }
702
703
704
705
706
707 public function setDepth($depth)
708 {
709 $this->depth = $depth;
710 return $this;
711 }
712
713
714
715
716 public function getFileNames(): array
717 {
718 return $this->fileNames ?: [];
719 }
720
721
722
723
724
725 public function setFileNames(array $fileNames)
726 {
727 $this->fileNames = $fileNames;
728 return $this;
729 }
730
731
732
733
734
735 public function addFileName(string $fileName)
736 {
737 $this->fileNames[] = $fileName;
738 return $this;
739 }
740
741
742
743
744
745 public function setLogger($logger)
746 {
747 $this->logger = $logger;
748 return $this;
749 }
750
751
752
753
754
755
756 protected function deleteItem(SplFileInfo $item): bool
757 {
758 $path = $item->getPathname();
759
760 if ($item->isLink()) {
761 if (!$this->removeSymlink($path)) {
762 $this->log('Permission Error: Can not delete link ' . $path);
763 throw new RuntimeException('Permission Error: Can not delete link ' . $path);
764 }
765 }
766
767
768 if (!file_exists($path)) {
769 return true;
770 }
771
772 if ($item->isDir()) {
773 if (!$this->isEmptyDir($path)) {
774 return false;
775 }
776
777 if (!@rmdir($path)) {
778 $this->log('Permission Error: Can not delete folder ' . $path);
779 throw new RuntimeException('Permission Error: Can not delete folder ' . $path);
780 }
781
782 return true;
783 }
784
785 if (!$item->isFile()) {
786 return false;
787 }
788
789 if (!@unlink($path)) {
790 $this->log('Permission Error: Can not delete file ' . $path);
791 throw new RuntimeException('Permission Error: Can not delete file ' . $path);
792 }
793
794 return true;
795 }
796
797
798
799
800
801
802 protected function removeSymlink(string $path): bool
803 {
804
805 if (PHP_SHLIB_SUFFIX === 'dll') {
806 return @rmdir($path);
807 }
808
809 return @unlink($path);
810 }
811
812
813
814
815 protected function log(string $string)
816 {
817 if ($this->logger instanceof LoggerInterface) {
818 $this->logger->warning($string);
819 return;
820 }
821
822 $this->logs[] = $string;
823 }
824
825
826
827
828
829
830
831
832
833 public function create(string $path, string $content, string $mode = 'wb'): bool
834 {
835 if (!@file_exists($path)) {
836 if (!@is_writable(dirname($path))) {
837 return false;
838 }
839
840 if (!@touch($path)) {
841 return false;
842 }
843 } elseif (!@is_writable($path)) {
844 return false;
845 }
846
847 $written = false;
848 if (( $handle = @fopen($path, $mode) ) !== false) {
849 if (@fwrite($handle, $content) !== false) {
850 $written = true;
851 }
852
853 @fclose($handle);
854 }
855
856 return $written;
857 }
858
859
860
861
862
863
864
865
866
867 public function createWithMarkers(string $path, string $marker, $content): bool
868 {
869 return @insert_with_markers($path, $marker, $content);
870 }
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887 public function maybeNormalizePath(string $path, bool $addTrailingslash = false): string
888 {
889 if ($this->isWindowsOs() || !strpos($path, '\\')) {
890 return $this->normalizePath($path, $addTrailingslash);
891 }
892
893 return $addTrailingslash ? $this->trailingSlashit($path) : $path;
894 }
895
896
897
898
899
900
901
902
903
904
905
906
907
908 public function normalizePath(string $path, bool $addTrailingslash = false): string
909 {
910
911
912
913
914
915
916
917
918 if (strpos($path, '\\') === 0 && strpos($path, '\\\\') !== 0) {
919 $path = '\\' . $path;
920 }
921
922 if ($addTrailingslash) {
923 $path = trim($path);
924 $path = wp_normalize_path($path);
925 $path = trailingslashit($path);
926
927 return $path;
928 }
929
930 return wp_normalize_path($path);
931 }
932
933
934
935
936
937
938
939
940 public function handleMkdirError(int $errno, string $errstr, string $errfile = '', int $errline = 0): bool
941 {
942 $this->logs[] = "Unable to create directory. Reason: " . $errstr;
943 return true;
944 }
945
946
947
948
949
950
951
952
953 public function tmpDestinationPath(string $fullPath): string
954 {
955 return preg_replace(
956 '#wp-content/(plugins|themes)/([A-Za-z0-9-_]+)#',
957 'wp-content/' . Copier::PREFIX_TEMP . '$1/$2',
958 $fullPath
959 );
960 }
961
962
963
964
965
966
967
968
969
970 public function isReadableFile(string $filePath): bool
971 {
972 if (is_readable($filePath)) {
973 return true;
974 }
975
976 if (!file_exists($filePath) || !is_file($filePath)) {
977 return false;
978 }
979
980
981
982
983 try {
984 $fileHandle = fopen($filePath, 'rb');
985 if (!is_resource($fileHandle)) {
986 return false;
987 }
988
989 if (fclose($fileHandle)) {
990 return true;
991 }
992 } catch (Exception $ex) {
993 debug_log($ex->getMessage());
994 }
995
996 return false;
997 }
998
999
1000
1001
1002
1003 public function setProcessedCount(int $processed = 0)
1004 {
1005 $this->processed = $processed;
1006 }
1007
1008
1009
1010
1011 public function getProcessedCount(): int
1012 {
1013 return $this->processed;
1014 }
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026 public function findFilesInDir(string $path): array
1027 {
1028 $path = $this->normalizePath($path);
1029
1030 $it = @new \RecursiveDirectoryIterator($path, \RecursiveDirectoryIterator::SKIP_DOTS);
1031 $it = new \RecursiveIteratorIterator($it);
1032
1033 $files = [];
1034
1035
1036 foreach ($it as $item) {
1037
1038 if (!$item->isFile() || $item->isLink()) {
1039 continue;
1040 }
1041
1042 $pathName = $this->normalizePath($item->getPathname());
1043
1044 $relativePath = str_replace($path, '', $pathName);
1045
1046 $files[$relativePath] = $pathName;
1047 }
1048
1049 return $files;
1050 }
1051
1052
1053
1054
1055
1056
1057
1058
1059 public function trailingSlashit(string $path): string
1060 {
1061 if ($this->isWindowsOs()) {
1062 return trailingslashit($path);
1063 }
1064
1065 if ($path[strlen($path) - 1] === '\\') {
1066 return $path . '/';
1067 }
1068
1069 return trailingslashit($path);
1070 }
1071
1072
1073
1074
1075
1076
1077 protected function isWindowsOs(): bool
1078 {
1079 return WPStaging::isWindowsOs();
1080 }
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091 protected function recursiveCreateDirectory(string $directory): bool
1092 {
1093 if (is_dir($directory)) {
1094 return true;
1095 }
1096
1097 return mkdir($directory, 0775, true);
1098 }
1099
1100
1101
1102
1103
1104
1105
1106 public function isWpstgBackupFile(string $backupFile): bool
1107 {
1108 if (empty($backupFile)) {
1109 return false;
1110 }
1111
1112 $backupFile = basename($backupFile);
1113 $backupFile = strtolower($backupFile);
1114
1115 foreach (self::BACKUP_FILE_EXTENSION as $extension) {
1116 if ($extension === substr($backupFile, -strlen($extension))) {
1117 return true;
1118 }
1119 }
1120
1121 return false;
1122 }
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132 public function copyFile(string $source, string $destination): bool
1133 {
1134 if ($this->useCopyFunction && @copy($source, $destination)) {
1135 return true;
1136 }
1137
1138
1139
1140 if ($this->useCopyFunction) {
1141 $errorObject = error_get_last();
1142 $errorMessage = $errorObject['message'] ?? '';
1143 }
1144
1145 $result = file_put_contents($destination, file_get_contents($source));
1146
1147 if ($result === false) {
1148 throw new RuntimeException("Failed to copy file to destination: {$source} -> {$destination}. Error: {$errorMessage}");
1149 }
1150
1151
1152 if ($this->useCopyFunction) {
1153 $this->useCopyFunction = false;
1154 debug_log("Copy function failed with error: {$errorMessage}. Using file_get_contents and file_put_contents instead.");
1155 }
1156
1157 return true;
1158 }
1159 }
1160