1: <?php
2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14:
15: namespace Cake\Filesystem;
16:
17: use DirectoryIterator;
18: use Exception;
19: use InvalidArgumentException;
20: use RecursiveDirectoryIterator;
21: use RecursiveIteratorIterator;
22:
23: 24: 25: 26: 27: 28:
29: class Folder
30: {
31:
32: 33: 34: 35: 36: 37:
38: const MERGE = 'merge';
39:
40: 41: 42: 43: 44: 45:
46: const OVERWRITE = 'overwrite';
47:
48: 49: 50: 51: 52: 53:
54: const SKIP = 'skip';
55:
56: 57: 58: 59: 60:
61: const SORT_NAME = 'name';
62:
63: 64: 65: 66: 67:
68: const SORT_TIME = 'time';
69:
70: 71: 72: 73: 74:
75: public $path;
76:
77: 78: 79: 80: 81: 82:
83: public $sort = false;
84:
85: 86: 87: 88: 89: 90:
91: public $mode = 0755;
92:
93: 94: 95:
96: protected $_fsorts = [
97: self::SORT_NAME => 'getPathname',
98: self::SORT_TIME => 'getCTime'
99: ];
100:
101: 102: 103: 104: 105:
106: protected $_messages = [];
107:
108: 109: 110: 111: 112:
113: protected $_errors = [];
114:
115: 116: 117: 118: 119:
120: protected $_directories;
121:
122: 123: 124: 125: 126:
127: protected $_files;
128:
129: 130: 131: 132: 133: 134: 135:
136: public function __construct($path = null, $create = false, $mode = false)
137: {
138: if (empty($path)) {
139: $path = TMP;
140: }
141: if ($mode) {
142: $this->mode = $mode;
143: }
144:
145: if (!file_exists($path) && $create === true) {
146: $this->create($path, $this->mode);
147: }
148: if (!Folder::isAbsolute($path)) {
149: $path = realpath($path);
150: }
151: if (!empty($path)) {
152: $this->cd($path);
153: }
154: }
155:
156: 157: 158: 159: 160:
161: public function pwd()
162: {
163: return $this->path;
164: }
165:
166: 167: 168: 169: 170: 171:
172: public function cd($path)
173: {
174: $path = $this->realpath($path);
175: if ($path !== false && is_dir($path)) {
176: return $this->path = $path;
177: }
178:
179: return false;
180: }
181:
182: 183: 184: 185: 186: 187: 188: 189: 190: 191:
192: public function read($sort = self::SORT_NAME, $exceptions = false, $fullPath = false)
193: {
194: $dirs = $files = [];
195:
196: if (!$this->pwd()) {
197: return [$dirs, $files];
198: }
199: if (is_array($exceptions)) {
200: $exceptions = array_flip($exceptions);
201: }
202: $skipHidden = isset($exceptions['.']) || $exceptions === true;
203:
204: try {
205: $iterator = new DirectoryIterator($this->path);
206: } catch (Exception $e) {
207: return [$dirs, $files];
208: }
209:
210: if (!is_bool($sort) && isset($this->_fsorts[$sort])) {
211: $methodName = $this->_fsorts[$sort];
212: } else {
213: $methodName = $this->_fsorts[self::SORT_NAME];
214: }
215:
216: foreach ($iterator as $item) {
217: if ($item->isDot()) {
218: continue;
219: }
220: $name = $item->getFilename();
221: if ($skipHidden && $name[0] === '.' || isset($exceptions[$name])) {
222: continue;
223: }
224: if ($fullPath) {
225: $name = $item->getPathname();
226: }
227:
228: if ($item->isDir()) {
229: $dirs[$item->{$methodName}()][] = $name;
230: } else {
231: $files[$item->{$methodName}()][] = $name;
232: }
233: }
234:
235: if ($sort || $this->sort) {
236: ksort($dirs);
237: ksort($files);
238: }
239:
240: if ($dirs) {
241: $dirs = array_merge(...array_values($dirs));
242: }
243:
244: if ($files) {
245: $files = array_merge(...array_values($files));
246: }
247:
248: return [$dirs, $files];
249: }
250:
251: 252: 253: 254: 255: 256: 257:
258: public function find($regexpPattern = '.*', $sort = false)
259: {
260: list(, $files) = $this->read($sort);
261:
262: return array_values(preg_grep('/^' . $regexpPattern . '$/i', $files));
263: }
264:
265: 266: 267: 268: 269: 270: 271:
272: public function findRecursive($pattern = '.*', $sort = false)
273: {
274: if (!$this->pwd()) {
275: return [];
276: }
277: $startsOn = $this->path;
278: $out = $this->_findRecursive($pattern, $sort);
279: $this->cd($startsOn);
280:
281: return $out;
282: }
283:
284: 285: 286: 287: 288: 289: 290:
291: protected function _findRecursive($pattern, $sort = false)
292: {
293: list($dirs, $files) = $this->read($sort);
294: $found = [];
295:
296: foreach ($files as $file) {
297: if (preg_match('/^' . $pattern . '$/i', $file)) {
298: $found[] = Folder::addPathElement($this->path, $file);
299: }
300: }
301: $start = $this->path;
302:
303: foreach ($dirs as $dir) {
304: $this->cd(Folder::addPathElement($start, $dir));
305: $found = array_merge($found, $this->findRecursive($pattern, $sort));
306: }
307:
308: return $found;
309: }
310:
311: 312: 313: 314: 315: 316:
317: public static function isWindowsPath($path)
318: {
319: return (preg_match('/^[A-Z]:\\\\/i', $path) || substr($path, 0, 2) === '\\\\');
320: }
321:
322: 323: 324: 325: 326: 327:
328: public static function isAbsolute($path)
329: {
330: if (empty($path)) {
331: return false;
332: }
333:
334: return $path[0] === '/' ||
335: preg_match('/^[A-Z]:\\\\/i', $path) ||
336: substr($path, 0, 2) === '\\\\' ||
337: self::isRegisteredStreamWrapper($path);
338: }
339:
340: 341: 342: 343: 344: 345:
346: public static function isRegisteredStreamWrapper($path)
347: {
348: return preg_match('/^[^:\/\/]+?(?=:\/\/)/', $path, $matches) &&
349: in_array($matches[0], stream_get_wrappers());
350: }
351:
352: 353: 354: 355: 356: 357: 358: 359:
360: public static function normalizePath($path)
361: {
362: deprecationWarning('Folder::normalizePath() is deprecated. Use Folder::correctSlashFor() instead.');
363:
364: return Folder::correctSlashFor($path);
365: }
366:
367: 368: 369: 370: 371: 372:
373: public static function normalizeFullPath($path)
374: {
375: $to = Folder::correctSlashFor($path);
376: $from = ($to == '/' ? '\\' : '/');
377:
378: return str_replace($from, $to, $path);
379: }
380:
381: 382: 383: 384: 385: 386:
387: public static function correctSlashFor($path)
388: {
389: return Folder::isWindowsPath($path) ? '\\' : '/';
390: }
391:
392: 393: 394: 395: 396: 397:
398: public static function slashTerm($path)
399: {
400: if (Folder::isSlashTerm($path)) {
401: return $path;
402: }
403:
404: return $path . Folder::correctSlashFor($path);
405: }
406:
407: 408: 409: 410: 411: 412: 413:
414: public static function addPathElement($path, $element)
415: {
416: $element = (array)$element;
417: array_unshift($element, rtrim($path, DIRECTORY_SEPARATOR));
418:
419: return implode(DIRECTORY_SEPARATOR, $element);
420: }
421:
422: 423: 424: 425: 426: 427: 428:
429: public function inCakePath($path = '')
430: {
431: deprecationWarning('Folder::inCakePath() is deprecated. Use Folder::inPath() instead.');
432: $dir = substr(Folder::slashTerm(ROOT), 0, -1);
433: $newdir = $dir . $path;
434:
435: return $this->inPath($newdir);
436: }
437:
438: 439: 440: 441: 442: 443: 444: 445:
446: public function inPath($path, $reverse = false)
447: {
448: if (!Folder::isAbsolute($path)) {
449: throw new InvalidArgumentException('The $path argument is expected to be an absolute path.');
450: }
451:
452: $dir = Folder::slashTerm($path);
453: $current = Folder::slashTerm($this->pwd());
454:
455: if (!$reverse) {
456: $return = preg_match('/^' . preg_quote($dir, '/') . '(.*)/', $current);
457: } else {
458: $return = preg_match('/^' . preg_quote($current, '/') . '(.*)/', $dir);
459: }
460:
461: return (bool)$return;
462: }
463:
464: 465: 466: 467: 468: 469: 470: 471: 472:
473: public function chmod($path, $mode = false, $recursive = true, array $exceptions = [])
474: {
475: if (!$mode) {
476: $mode = $this->mode;
477: }
478:
479: if ($recursive === false && is_dir($path)) {
480:
481: if (@chmod($path, intval($mode, 8))) {
482:
483: $this->_messages[] = sprintf('%s changed to %s', $path, $mode);
484:
485: return true;
486: }
487:
488: $this->_errors[] = sprintf('%s NOT changed to %s', $path, $mode);
489:
490: return false;
491: }
492:
493: if (is_dir($path)) {
494: $paths = $this->tree($path);
495:
496: foreach ($paths as $type) {
497: foreach ($type as $fullpath) {
498: $check = explode(DIRECTORY_SEPARATOR, $fullpath);
499: $count = count($check);
500:
501: if (in_array($check[$count - 1], $exceptions)) {
502: continue;
503: }
504:
505:
506: if (@chmod($fullpath, intval($mode, 8))) {
507:
508: $this->_messages[] = sprintf('%s changed to %s', $fullpath, $mode);
509: } else {
510: $this->_errors[] = sprintf('%s NOT changed to %s', $fullpath, $mode);
511: }
512: }
513: }
514:
515: if (empty($this->_errors)) {
516: return true;
517: }
518: }
519:
520: return false;
521: }
522:
523: 524: 525: 526: 527: 528: 529:
530: public function subdirectories($path = null, $fullPath = true)
531: {
532: if (!$path) {
533: $path = $this->path;
534: }
535: $subdirectories = [];
536:
537: try {
538: $iterator = new DirectoryIterator($path);
539: } catch (Exception $e) {
540: return [];
541: }
542:
543: foreach ($iterator as $item) {
544: if (!$item->isDir() || $item->isDot()) {
545: continue;
546: }
547: $subdirectories[] = $fullPath ? $item->getRealPath() : $item->getFilename();
548: }
549:
550: return $subdirectories;
551: }
552:
553: 554: 555: 556: 557: 558: 559: 560: 561:
562: public function tree($path = null, $exceptions = false, $type = null)
563: {
564: if (!$path) {
565: $path = $this->path;
566: }
567: $files = [];
568: $directories = [$path];
569:
570: if (is_array($exceptions)) {
571: $exceptions = array_flip($exceptions);
572: }
573: $skipHidden = false;
574: if ($exceptions === true) {
575: $skipHidden = true;
576: } elseif (isset($exceptions['.'])) {
577: $skipHidden = true;
578: unset($exceptions['.']);
579: }
580:
581: try {
582: $directory = new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::KEY_AS_PATHNAME | RecursiveDirectoryIterator::CURRENT_AS_SELF);
583: $iterator = new RecursiveIteratorIterator($directory, RecursiveIteratorIterator::SELF_FIRST);
584: } catch (Exception $e) {
585: if ($type === null) {
586: return [[], []];
587: }
588:
589: return [];
590: }
591:
592: 593: 594: 595:
596: foreach ($iterator as $itemPath => $fsIterator) {
597: if ($skipHidden) {
598: $subPathName = $fsIterator->getSubPathname();
599: if ($subPathName{0} === '.' || strpos($subPathName, DIRECTORY_SEPARATOR . '.') !== false) {
600: continue;
601: }
602: }
603:
604: $item = $fsIterator->current();
605: if (!empty($exceptions) && isset($exceptions[$item->getFilename()])) {
606: continue;
607: }
608:
609: if ($item->isFile()) {
610: $files[] = $itemPath;
611: } elseif ($item->isDir() && !$item->isDot()) {
612: $directories[] = $itemPath;
613: }
614: }
615: if ($type === null) {
616: return [$directories, $files];
617: }
618: if ($type === 'dir') {
619: return $directories;
620: }
621:
622: return $files;
623: }
624:
625: 626: 627: 628: 629: 630: 631: 632: 633: 634: 635:
636: public function create($pathname, $mode = false)
637: {
638: if (is_dir($pathname) || empty($pathname)) {
639: return true;
640: }
641:
642: if (!self::isAbsolute($pathname)) {
643: $pathname = self::addPathElement($this->pwd(), $pathname);
644: }
645:
646: if (!$mode) {
647: $mode = $this->mode;
648: }
649:
650: if (is_file($pathname)) {
651: $this->_errors[] = sprintf('%s is a file', $pathname);
652:
653: return false;
654: }
655: $pathname = rtrim($pathname, DIRECTORY_SEPARATOR);
656: $nextPathname = substr($pathname, 0, strrpos($pathname, DIRECTORY_SEPARATOR));
657:
658: if ($this->create($nextPathname, $mode)) {
659: if (!file_exists($pathname)) {
660: $old = umask(0);
661: if (mkdir($pathname, $mode, true)) {
662: umask($old);
663: $this->_messages[] = sprintf('%s created', $pathname);
664:
665: return true;
666: }
667: umask($old);
668: $this->_errors[] = sprintf('%s NOT created', $pathname);
669:
670: return false;
671: }
672: }
673:
674: return false;
675: }
676:
677: 678: 679: 680: 681:
682: public function dirsize()
683: {
684: $size = 0;
685: $directory = Folder::slashTerm($this->path);
686: $stack = [$directory];
687: $count = count($stack);
688: for ($i = 0, $j = $count; $i < $j; $i++) {
689: if (is_file($stack[$i])) {
690: $size += filesize($stack[$i]);
691: } elseif (is_dir($stack[$i])) {
692: $dir = dir($stack[$i]);
693: if ($dir) {
694: while (($entry = $dir->read()) !== false) {
695: if ($entry === '.' || $entry === '..') {
696: continue;
697: }
698: $add = $stack[$i] . $entry;
699:
700: if (is_dir($stack[$i] . $entry)) {
701: $add = Folder::slashTerm($add);
702: }
703: $stack[] = $add;
704: }
705: $dir->close();
706: }
707: }
708: $j = count($stack);
709: }
710:
711: return $size;
712: }
713:
714: 715: 716: 717: 718: 719:
720: public function delete($path = null)
721: {
722: if (!$path) {
723: $path = $this->pwd();
724: }
725: if (!$path) {
726: return false;
727: }
728: $path = Folder::slashTerm($path);
729: if (is_dir($path)) {
730: try {
731: $directory = new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::CURRENT_AS_SELF);
732: $iterator = new RecursiveIteratorIterator($directory, RecursiveIteratorIterator::CHILD_FIRST);
733: } catch (Exception $e) {
734: return false;
735: }
736:
737: foreach ($iterator as $item) {
738: $filePath = $item->getPathname();
739: if ($item->isFile() || $item->isLink()) {
740:
741: if (@unlink($filePath)) {
742:
743: $this->_messages[] = sprintf('%s removed', $filePath);
744: } else {
745: $this->_errors[] = sprintf('%s NOT removed', $filePath);
746: }
747: } elseif ($item->isDir() && !$item->isDot()) {
748:
749: if (@rmdir($filePath)) {
750:
751: $this->_messages[] = sprintf('%s removed', $filePath);
752: } else {
753: $this->_errors[] = sprintf('%s NOT removed', $filePath);
754:
755: return false;
756: }
757: }
758: }
759:
760: $path = rtrim($path, DIRECTORY_SEPARATOR);
761:
762: if (@rmdir($path)) {
763:
764: $this->_messages[] = sprintf('%s removed', $path);
765: } else {
766: $this->_errors[] = sprintf('%s NOT removed', $path);
767:
768: return false;
769: }
770: }
771:
772: return true;
773: }
774:
775: 776: 777: 778: 779: 780: 781: 782: 783: 784: 785: 786: 787: 788: 789:
790: public function copy($options)
791: {
792: if (!$this->pwd()) {
793: return false;
794: }
795: $to = null;
796: if (is_string($options)) {
797: $to = $options;
798: $options = [];
799: }
800: $options += [
801: 'to' => $to,
802: 'from' => $this->path,
803: 'mode' => $this->mode,
804: 'skip' => [],
805: 'scheme' => Folder::MERGE,
806: 'recursive' => true
807: ];
808:
809: $fromDir = $options['from'];
810: $toDir = $options['to'];
811: $mode = $options['mode'];
812:
813: if (!$this->cd($fromDir)) {
814: $this->_errors[] = sprintf('%s not found', $fromDir);
815:
816: return false;
817: }
818:
819: if (!is_dir($toDir)) {
820: $this->create($toDir, $mode);
821: }
822:
823: if (!is_writable($toDir)) {
824: $this->_errors[] = sprintf('%s not writable', $toDir);
825:
826: return false;
827: }
828:
829: $exceptions = array_merge(['.', '..', '.svn'], $options['skip']);
830:
831: if ($handle = @opendir($fromDir)) {
832:
833: while (($item = readdir($handle)) !== false) {
834: $to = Folder::addPathElement($toDir, $item);
835: if (($options['scheme'] != Folder::SKIP || !is_dir($to)) && !in_array($item, $exceptions)) {
836: $from = Folder::addPathElement($fromDir, $item);
837: if (is_file($from) && (!is_file($to) || $options['scheme'] != Folder::SKIP)) {
838: if (copy($from, $to)) {
839: chmod($to, intval($mode, 8));
840: touch($to, filemtime($from));
841: $this->_messages[] = sprintf('%s copied to %s', $from, $to);
842: } else {
843: $this->_errors[] = sprintf('%s NOT copied to %s', $from, $to);
844: }
845: }
846:
847: if (is_dir($from) && file_exists($to) && $options['scheme'] === Folder::OVERWRITE) {
848: $this->delete($to);
849: }
850:
851: if (is_dir($from) && $options['recursive'] === false) {
852: continue;
853: }
854:
855: if (is_dir($from) && !file_exists($to)) {
856: $old = umask(0);
857: if (mkdir($to, $mode, true)) {
858: umask($old);
859: $old = umask(0);
860: chmod($to, $mode);
861: umask($old);
862: $this->_messages[] = sprintf('%s created', $to);
863: $options = ['to' => $to, 'from' => $from] + $options;
864: $this->copy($options);
865: } else {
866: $this->_errors[] = sprintf('%s not created', $to);
867: }
868: } elseif (is_dir($from) && $options['scheme'] === Folder::MERGE) {
869: $options = ['to' => $to, 'from' => $from] + $options;
870: $this->copy($options);
871: }
872: }
873: }
874: closedir($handle);
875: } else {
876: return false;
877: }
878:
879: return empty($this->_errors);
880: }
881:
882: 883: 884: 885: 886: 887: 888: 889: 890: 891: 892: 893: 894: 895: 896:
897: public function move($options)
898: {
899: $to = null;
900: if (is_string($options)) {
901: $to = $options;
902: $options = (array)$options;
903: }
904: $options += ['to' => $to, 'from' => $this->path, 'mode' => $this->mode, 'skip' => [], 'recursive' => true];
905:
906: if ($this->copy($options) && $this->delete($options['from'])) {
907: return (bool)$this->cd($options['to']);
908: }
909:
910: return false;
911: }
912:
913: 914: 915: 916: 917: 918:
919: public function messages($reset = true)
920: {
921: $messages = $this->_messages;
922: if ($reset) {
923: $this->_messages = [];
924: }
925:
926: return $messages;
927: }
928:
929: 930: 931: 932: 933: 934:
935: public function errors($reset = true)
936: {
937: $errors = $this->_errors;
938: if ($reset) {
939: $this->_errors = [];
940: }
941:
942: return $errors;
943: }
944:
945: 946: 947: 948: 949: 950:
951: public function realpath($path)
952: {
953: if (strpos($path, '..') === false) {
954: if (!Folder::isAbsolute($path)) {
955: $path = Folder::addPathElement($this->path, $path);
956: }
957:
958: return $path;
959: }
960: $path = str_replace('/', DIRECTORY_SEPARATOR, trim($path));
961: $parts = explode(DIRECTORY_SEPARATOR, $path);
962: $newparts = [];
963: $newpath = '';
964: if ($path[0] === DIRECTORY_SEPARATOR) {
965: $newpath = DIRECTORY_SEPARATOR;
966: }
967:
968: while (($part = array_shift($parts)) !== null) {
969: if ($part === '.' || $part === '') {
970: continue;
971: }
972: if ($part === '..') {
973: if (!empty($newparts)) {
974: array_pop($newparts);
975: continue;
976: }
977:
978: return false;
979: }
980: $newparts[] = $part;
981: }
982: $newpath .= implode(DIRECTORY_SEPARATOR, $newparts);
983:
984: return Folder::slashTerm($newpath);
985: }
986:
987: 988: 989: 990: 991: 992:
993: public static function isSlashTerm($path)
994: {
995: $lastChar = $path[strlen($path) - 1];
996:
997: return $lastChar === '/' || $lastChar === '\\';
998: }
999: }
1000: