Folder::copy()   F
last analyzed

Complexity

Conditions 25
Paths 139

Size

Total Lines 91

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 25
nc 139
nop 1
dl 0
loc 91
rs 3.8416
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
4
 * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
5
 *
6
 * Licensed under The MIT License
7
 * For full copyright and license information, please see the LICENSE.txt
8
 * Redistributions of files must retain the above copyright notice.
9
 *
10
 * @copyright     Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
11
 * @link          https://cakephp.org CakePHP(tm) Project
12
 * @since         0.2.9
13
 * @license       https://opensource.org/licenses/mit-license.php MIT License
14
 */
15
namespace Cake\Filesystem;
16
17
use DirectoryIterator;
18
use Exception;
19
use InvalidArgumentException;
20
use RecursiveDirectoryIterator;
21
use RecursiveIteratorIterator;
22
23
/**
24
 * Folder structure browser, lists folders and files.
25
 * Provides an Object interface for Common directory related tasks.
26
 *
27
 * @link https://book.cakephp.org/3/en/core-libraries/file-folder.html#folder-api
28
 */
29
class Folder
30
{
31
    /**
32
     * Default scheme for Folder::copy
33
     * Recursively merges subfolders with the same name
34
     *
35
     * @var string
36
     */
37
    const MERGE = 'merge';
38
39
    /**
40
     * Overwrite scheme for Folder::copy
41
     * subfolders with the same name will be replaced
42
     *
43
     * @var string
44
     */
45
    const OVERWRITE = 'overwrite';
46
47
    /**
48
     * Skip scheme for Folder::copy
49
     * if a subfolder with the same name exists it will be skipped
50
     *
51
     * @var string
52
     */
53
    const SKIP = 'skip';
54
55
    /**
56
     * Sort mode by name
57
     *
58
     * @var string
59
     */
60
    const SORT_NAME = 'name';
61
62
    /**
63
     * Sort mode by time
64
     *
65
     * @var string
66
     */
67
    const SORT_TIME = 'time';
68
69
    /**
70
     * Path to Folder.
71
     *
72
     * @var string
73
     */
74
    public $path;
75
76
    /**
77
     * Sortedness. Whether or not list results
78
     * should be sorted by name.
79
     *
80
     * @var bool
81
     */
82
    public $sort = false;
83
84
    /**
85
     * Mode to be used on create. Does nothing on windows platforms.
86
     *
87
     * @var int
88
     * https://book.cakephp.org/3/en/core-libraries/file-folder.html#Cake\Filesystem\Folder::$mode
89
     */
90
    public $mode = 0755;
91
92
    /**
93
     * Functions array to be called depending on the sort type chosen.
94
     */
95
    protected $_fsorts = [
96
        self::SORT_NAME => 'getPathname',
97
        self::SORT_TIME => 'getCTime',
98
    ];
99
100
    /**
101
     * Holds messages from last method.
102
     *
103
     * @var array
104
     */
105
    protected $_messages = [];
106
107
    /**
108
     * Holds errors from last method.
109
     *
110
     * @var array
111
     */
112
    protected $_errors = [];
113
114
    /**
115
     * Holds array of complete directory paths.
116
     *
117
     * @var array
118
     */
119
    protected $_directories;
120
121
    /**
122
     * Holds array of complete file paths.
123
     *
124
     * @var array
125
     */
126
    protected $_files;
127
128
    /**
129
     * Constructor.
130
     *
131
     * @param string|null $path Path to folder
132
     * @param bool $create Create folder if not found
133
     * @param int|false $mode Mode (CHMOD) to apply to created folder, false to ignore
134
     */
135
    public function __construct($path = null, $create = false, $mode = false)
136
    {
137
        if (empty($path)) {
138
            $path = TMP;
139
        }
140
        if ($mode) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $mode of type false|integer is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
141
            $this->mode = $mode;
142
        }
143
144
        if (!file_exists($path) && $create === true) {
145
            $this->create($path, $this->mode);
146
        }
147
        if (!Folder::isAbsolute($path)) {
148
            $path = realpath($path);
149
        }
150
        if (!empty($path)) {
151
            $this->cd($path);
152
        }
153
    }
154
155
    /**
156
     * Return current path.
157
     *
158
     * @return string Current path
159
     */
160
    public function pwd()
161
    {
162
        return $this->path;
163
    }
164
165
    /**
166
     * Change directory to $path.
167
     *
168
     * @param string $path Path to the directory to change to
169
     * @return string|bool The new path. Returns false on failure
170
     */
171
    public function cd($path)
172
    {
173
        $path = $this->realpath($path);
174
        if ($path !== false && is_dir($path)) {
175
            return $this->path = $path;
176
        }
177
178
        return false;
179
    }
180
181
    /**
182
     * Returns an array of the contents of the current directory.
183
     * The returned array holds two arrays: One of directories and one of files.
184
     *
185
     * @param string|bool $sort Whether you want the results sorted, set this and the sort property
186
     *   to false to get unsorted results.
187
     * @param array|bool $exceptions Either an array or boolean true will not grab dot files
188
     * @param bool $fullPath True returns the full path
189
     * @return array Contents of current directory as an array, an empty array on failure
190
     */
191
    public function read($sort = self::SORT_NAME, $exceptions = false, $fullPath = false)
192
    {
193
        $dirs = $files = [];
194
195
        if (!$this->pwd()) {
196
            return [$dirs, $files];
197
        }
198
        if (is_array($exceptions)) {
199
            $exceptions = array_flip($exceptions);
200
        }
201
        $skipHidden = isset($exceptions['.']) || $exceptions === true;
202
203
        try {
204
            $iterator = new DirectoryIterator($this->path);
205
        } catch (Exception $e) {
206
            return [$dirs, $files];
207
        }
208
209
        if (!is_bool($sort) && isset($this->_fsorts[$sort])) {
210
            $methodName = $this->_fsorts[$sort];
211
        } else {
212
            $methodName = $this->_fsorts[self::SORT_NAME];
213
        }
214
215
        foreach ($iterator as $item) {
216
            if ($item->isDot()) {
217
                continue;
218
            }
219
            $name = $item->getFilename();
220
            if ($skipHidden && $name[0] === '.' || isset($exceptions[$name])) {
221
                continue;
222
            }
223
            if ($fullPath) {
224
                $name = $item->getPathname();
225
            }
226
227
            if ($item->isDir()) {
228
                $dirs[$item->{$methodName}()][] = $name;
229
            } else {
230
                $files[$item->{$methodName}()][] = $name;
231
            }
232
        }
233
234
        if ($sort || $this->sort) {
235
            ksort($dirs);
236
            ksort($files);
237
        }
238
239
        if ($dirs) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $dirs of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
240
            $dirs = array_merge(...array_values($dirs));
241
        }
242
243
        if ($files) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $files of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
244
            $files = array_merge(...array_values($files));
245
        }
246
247
        return [$dirs, $files];
248
    }
249
250
    /**
251
     * Returns an array of all matching files in current directory.
252
     *
253
     * @param string $regexpPattern Preg_match pattern (Defaults to: .*)
254
     * @param bool $sort Whether results should be sorted.
255
     * @return array Files that match given pattern
256
     */
257
    public function find($regexpPattern = '.*', $sort = false)
258
    {
259
        list(, $files) = $this->read($sort);
260
261
        return array_values(preg_grep('/^' . $regexpPattern . '$/i', $files));
262
    }
263
264
    /**
265
     * Returns an array of all matching files in and below current directory.
266
     *
267
     * @param string $pattern Preg_match pattern (Defaults to: .*)
268
     * @param bool $sort Whether results should be sorted.
269
     * @return array Files matching $pattern
270
     */
271
    public function findRecursive($pattern = '.*', $sort = false)
272
    {
273
        if (!$this->pwd()) {
274
            return [];
275
        }
276
        $startsOn = $this->path;
277
        $out = $this->_findRecursive($pattern, $sort);
278
        $this->cd($startsOn);
279
280
        return $out;
281
    }
282
283
    /**
284
     * Private helper function for findRecursive.
285
     *
286
     * @param string $pattern Pattern to match against
287
     * @param bool $sort Whether results should be sorted.
288
     * @return array Files matching pattern
289
     */
290
    protected function _findRecursive($pattern, $sort = false)
291
    {
292
        list($dirs, $files) = $this->read($sort);
293
        $found = [];
294
295
        foreach ($files as $file) {
296
            if (preg_match('/^' . $pattern . '$/i', $file)) {
297
                $found[] = Folder::addPathElement($this->path, $file);
298
            }
299
        }
300
        $start = $this->path;
301
302
        foreach ($dirs as $dir) {
303
            $this->cd(Folder::addPathElement($start, $dir));
304
            $found = array_merge($found, $this->findRecursive($pattern, $sort));
305
        }
306
307
        return $found;
308
    }
309
310
    /**
311
     * Returns true if given $path is a Windows path.
312
     *
313
     * @param string $path Path to check
314
     * @return bool true if windows path, false otherwise
315
     */
316
    public static function isWindowsPath($path)
317
    {
318
        return (preg_match('/^[A-Z]:\\\\/i', $path) || substr($path, 0, 2) === '\\\\');
319
    }
320
321
    /**
322
     * Returns true if given $path is an absolute path.
323
     *
324
     * @param string $path Path to check
325
     * @return bool true if path is absolute.
326
     */
327
    public static function isAbsolute($path)
328
    {
329
        if (empty($path)) {
330
            return false;
331
        }
332
333
        return $path[0] === '/' ||
334
            preg_match('/^[A-Z]:\\\\/i', $path) ||
335
            substr($path, 0, 2) === '\\\\' ||
336
            self::isRegisteredStreamWrapper($path);
337
    }
338
339
    /**
340
     * Returns true if given $path is a registered stream wrapper.
341
     *
342
     * @param string $path Path to check
343
     * @return bool True if path is registered stream wrapper.
344
     */
345
    public static function isRegisteredStreamWrapper($path)
346
    {
347
        return preg_match('/^[^:\/\/]+?(?=:\/\/)/', $path, $matches) &&
348
            in_array($matches[0], stream_get_wrappers());
349
    }
350
351
    /**
352
     * Returns a correct set of slashes for given $path. (\\ for Windows paths and / for other paths.)
353
     *
354
     * @param string $path Path to check
355
     * @return string Set of slashes ("\\" or "/")
356
     *
357
     * @deprecated 3.7.0 This method will be removed in 4.0.0. Use correctSlashFor() instead.
358
     */
359
    public static function normalizePath($path)
360
    {
361
        deprecationWarning('Folder::normalizePath() is deprecated. Use Folder::correctSlashFor() instead.');
362
363
        return Folder::correctSlashFor($path);
364
    }
365
366
    /**
367
     * Returns a correct set of slashes for given $path. (\\ for Windows paths and / for other paths.)
368
     *
369
     * @param string $path Path to transform
370
     * @return string Path with the correct set of slashes ("\\" or "/")
371
     */
372
    public static function normalizeFullPath($path)
373
    {
374
        $to = Folder::correctSlashFor($path);
375
        $from = ($to == '/' ? '\\' : '/');
376
377
        return str_replace($from, $to, $path);
378
    }
379
380
    /**
381
     * Returns a correct set of slashes for given $path. (\\ for Windows paths and / for other paths.)
382
     *
383
     * @param string $path Path to check
384
     * @return string Set of slashes ("\\" or "/")
385
     */
386
    public static function correctSlashFor($path)
387
    {
388
        return Folder::isWindowsPath($path) ? '\\' : '/';
389
    }
390
391
    /**
392
     * Returns $path with added terminating slash (corrected for Windows or other OS).
393
     *
394
     * @param string $path Path to check
395
     * @return string Path with ending slash
396
     */
397
    public static function slashTerm($path)
398
    {
399
        if (Folder::isSlashTerm($path)) {
400
            return $path;
401
        }
402
403
        return $path . Folder::correctSlashFor($path);
404
    }
405
406
    /**
407
     * Returns $path with $element added, with correct slash in-between.
408
     *
409
     * @param string $path Path
410
     * @param string|array $element Element to add at end of path
411
     * @return string Combined path
412
     */
413
    public static function addPathElement($path, $element)
414
    {
415
        $element = (array)$element;
416
        array_unshift($element, rtrim($path, DIRECTORY_SEPARATOR));
417
418
        return implode(DIRECTORY_SEPARATOR, $element);
419
    }
420
421
    /**
422
     * Returns true if the Folder is in the given Cake path.
423
     *
424
     * @param string $path The path to check.
425
     * @return bool
426
     * @deprecated 3.2.12 This method will be removed in 4.0.0. Use inPath() instead.
427
     */
428
    public function inCakePath($path = '')
429
    {
430
        deprecationWarning('Folder::inCakePath() is deprecated. Use Folder::inPath() instead.');
431
        $dir = substr(Folder::slashTerm(ROOT), 0, -1);
432
        $newdir = $dir . $path;
433
434
        return $this->inPath($newdir);
435
    }
436
437
    /**
438
     * Returns true if the Folder is in the given path.
439
     *
440
     * @param string $path The absolute path to check that the current `pwd()` resides within.
441
     * @param bool $reverse Reverse the search, check if the given `$path` resides within the current `pwd()`.
442
     * @return bool
443
     * @throws \InvalidArgumentException When the given `$path` argument is not an absolute path.
444
     */
445
    public function inPath($path, $reverse = false)
446
    {
447
        if (!Folder::isAbsolute($path)) {
448
            throw new InvalidArgumentException('The $path argument is expected to be an absolute path.');
449
        }
450
451
        $dir = Folder::slashTerm($path);
452
        $current = Folder::slashTerm($this->pwd());
453
454
        if (!$reverse) {
455
            $return = preg_match('/^' . preg_quote($dir, '/') . '(.*)/', $current);
456
        } else {
457
            $return = preg_match('/^' . preg_quote($current, '/') . '(.*)/', $dir);
458
        }
459
460
        return (bool)$return;
461
    }
462
463
    /**
464
     * Change the mode on a directory structure recursively. This includes changing the mode on files as well.
465
     *
466
     * @param string $path The path to chmod.
467
     * @param int|bool $mode Octal value, e.g. 0755.
468
     * @param bool $recursive Chmod recursively, set to false to only change the current directory.
469
     * @param array $exceptions Array of files, directories to skip.
470
     * @return bool Success.
471
     */
472
    public function chmod($path, $mode = false, $recursive = true, array $exceptions = [])
473
    {
474
        if (!$mode) {
475
            $mode = $this->mode;
476
        }
477
478
        if ($recursive === false && is_dir($path)) {
479
            //@codingStandardsIgnoreStart
480
            if (@chmod($path, intval($mode, 8))) {
481
                //@codingStandardsIgnoreEnd
482
                $this->_messages[] = sprintf('%s changed to %s', $path, $mode);
483
484
                return true;
485
            }
486
487
            $this->_errors[] = sprintf('%s NOT changed to %s', $path, $mode);
488
489
            return false;
490
        }
491
492
        if (is_dir($path)) {
493
            $paths = $this->tree($path);
494
495
            foreach ($paths as $type) {
496
                foreach ($type as $fullpath) {
497
                    $check = explode(DIRECTORY_SEPARATOR, $fullpath);
498
                    $count = count($check);
499
500
                    if (in_array($check[$count - 1], $exceptions)) {
501
                        continue;
502
                    }
503
504
                    //@codingStandardsIgnoreStart
505
                    if (@chmod($fullpath, intval($mode, 8))) {
506
                        //@codingStandardsIgnoreEnd
507
                        $this->_messages[] = sprintf('%s changed to %s', $fullpath, $mode);
508
                    } else {
509
                        $this->_errors[] = sprintf('%s NOT changed to %s', $fullpath, $mode);
510
                    }
511
                }
512
            }
513
514
            if (empty($this->_errors)) {
515
                return true;
516
            }
517
        }
518
519
        return false;
520
    }
521
522
    /**
523
     * Returns an array of subdirectories for the provided or current path.
524
     *
525
     * @param string|null $path The directory path to get subdirectories for.
526
     * @param bool $fullPath Whether to return the full path or only the directory name.
527
     * @return array Array of subdirectories for the provided or current path.
528
     */
529
    public function subdirectories($path = null, $fullPath = true)
530
    {
531
        if (!$path) {
532
            $path = $this->path;
533
        }
534
        $subdirectories = [];
535
536
        try {
537
            $iterator = new DirectoryIterator($path);
538
        } catch (Exception $e) {
539
            return [];
540
        }
541
542
        foreach ($iterator as $item) {
543
            if (!$item->isDir() || $item->isDot()) {
544
                continue;
545
            }
546
            $subdirectories[] = $fullPath ? $item->getRealPath() : $item->getFilename();
547
        }
548
549
        return $subdirectories;
550
    }
551
552
    /**
553
     * Returns an array of nested directories and files in each directory
554
     *
555
     * @param string|null $path the directory path to build the tree from
556
     * @param array|bool $exceptions Either an array of files/folder to exclude
557
     *   or boolean true to not grab dot files/folders
558
     * @param string|null $type either 'file' or 'dir'. Null returns both files and directories
559
     * @return array Array of nested directories and files in each directory
560
     */
561
    public function tree($path = null, $exceptions = false, $type = null)
562
    {
563
        if (!$path) {
564
            $path = $this->path;
565
        }
566
        $files = [];
567
        $directories = [$path];
568
569
        if (is_array($exceptions)) {
570
            $exceptions = array_flip($exceptions);
571
        }
572
        $skipHidden = false;
573
        if ($exceptions === true) {
574
            $skipHidden = true;
575
        } elseif (isset($exceptions['.'])) {
576
            $skipHidden = true;
577
            unset($exceptions['.']);
578
        }
579
580
        try {
581
            $directory = new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::KEY_AS_PATHNAME | RecursiveDirectoryIterator::CURRENT_AS_SELF);
582
            $iterator = new RecursiveIteratorIterator($directory, RecursiveIteratorIterator::SELF_FIRST);
583
        } catch (Exception $e) {
584
            if ($type === null) {
585
                return [[], []];
586
            }
587
588
            return [];
589
        }
590
591
        /**
592
         * @var string $itemPath
593
         * @var \RecursiveDirectoryIterator $fsIterator
594
         */
595
        foreach ($iterator as $itemPath => $fsIterator) {
596
            if ($skipHidden) {
597
                $subPathName = $fsIterator->getSubPathname();
598
                if ($subPathName[0] === '.' || strpos($subPathName, DIRECTORY_SEPARATOR . '.') !== false) {
599
                    continue;
600
                }
601
            }
602
            /** @var \FilesystemIterator $item */
603
            $item = $fsIterator->current();
604
            if (!empty($exceptions) && isset($exceptions[$item->getFilename()])) {
605
                continue;
606
            }
607
608
            if ($item->isFile()) {
609
                $files[] = $itemPath;
610
            } elseif ($item->isDir() && !$item->isDot()) {
611
                $directories[] = $itemPath;
612
            }
613
        }
614
        if ($type === null) {
615
            return [$directories, $files];
616
        }
617
        if ($type === 'dir') {
618
            return $directories;
619
        }
620
621
        return $files;
622
    }
623
624
    /**
625
     * Create a directory structure recursively.
626
     *
627
     * Can be used to create deep path structures like `/foo/bar/baz/shoe/horn`
628
     *
629
     * @param string $pathname The directory structure to create. Either an absolute or relative
630
     *   path. If the path is relative and exists in the process' cwd it will not be created.
631
     *   Otherwise relative paths will be prefixed with the current pwd().
632
     * @param int|bool $mode octal value 0755
633
     * @return bool Returns TRUE on success, FALSE on failure
634
     */
635
    public function create($pathname, $mode = false)
636
    {
637
        if (is_dir($pathname) || empty($pathname)) {
638
            return true;
639
        }
640
641
        if (!self::isAbsolute($pathname)) {
642
            $pathname = self::addPathElement($this->pwd(), $pathname);
643
        }
644
645
        if (!$mode) {
646
            $mode = $this->mode;
647
        }
648
649
        if (is_file($pathname)) {
650
            $this->_errors[] = sprintf('%s is a file', $pathname);
651
652
            return false;
653
        }
654
        $pathname = rtrim($pathname, DIRECTORY_SEPARATOR);
655
        $nextPathname = substr($pathname, 0, strrpos($pathname, DIRECTORY_SEPARATOR));
656
657
        if ($this->create($nextPathname, $mode)) {
658
            if (!file_exists($pathname)) {
659
                $old = umask(0);
660
                if (mkdir($pathname, $mode, true)) {
661
                    umask($old);
662
                    $this->_messages[] = sprintf('%s created', $pathname);
663
664
                    return true;
665
                }
666
                umask($old);
667
                $this->_errors[] = sprintf('%s NOT created', $pathname);
668
669
                return false;
670
            }
671
        }
672
673
        return false;
674
    }
675
676
    /**
677
     * Returns the size in bytes of this Folder and its contents.
678
     *
679
     * @return int size in bytes of current folder
680
     */
681
    public function dirsize()
682
    {
683
        $size = 0;
684
        $directory = Folder::slashTerm($this->path);
685
        $stack = [$directory];
686
        $count = count($stack);
687
        for ($i = 0, $j = $count; $i < $j; $i++) {
688
            if (is_file($stack[$i])) {
689
                $size += filesize($stack[$i]);
690
            } elseif (is_dir($stack[$i])) {
691
                $dir = dir($stack[$i]);
692
                if ($dir) {
693
                    while (($entry = $dir->read()) !== false) {
694
                        if ($entry === '.' || $entry === '..') {
695
                            continue;
696
                        }
697
                        $add = $stack[$i] . $entry;
698
699
                        if (is_dir($stack[$i] . $entry)) {
700
                            $add = Folder::slashTerm($add);
701
                        }
702
                        $stack[] = $add;
703
                    }
704
                    $dir->close();
705
                }
706
            }
707
            $j = count($stack);
708
        }
709
710
        return $size;
711
    }
712
713
    /**
714
     * Recursively Remove directories if the system allows.
715
     *
716
     * @param string|null $path Path of directory to delete
717
     * @return bool Success
718
     */
719
    public function delete($path = null)
720
    {
721
        if (!$path) {
722
            $path = $this->pwd();
723
        }
724
        if (!$path) {
725
            return false;
726
        }
727
        $path = Folder::slashTerm($path);
728
        if (is_dir($path)) {
729
            try {
730
                $directory = new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::CURRENT_AS_SELF);
731
                $iterator = new RecursiveIteratorIterator($directory, RecursiveIteratorIterator::CHILD_FIRST);
732
            } catch (Exception $e) {
733
                return false;
734
            }
735
736
            foreach ($iterator as $item) {
737
                $filePath = $item->getPathname();
738
                if ($item->isFile() || $item->isLink()) {
739
                    //@codingStandardsIgnoreStart
740
                    if (@unlink($filePath)) {
741
                        //@codingStandardsIgnoreEnd
742
                        $this->_messages[] = sprintf('%s removed', $filePath);
743
                    } else {
744
                        $this->_errors[] = sprintf('%s NOT removed', $filePath);
745
                    }
746
                } elseif ($item->isDir() && !$item->isDot()) {
747
                    //@codingStandardsIgnoreStart
748 View Code Duplication
                    if (@rmdir($filePath)) {
749
                        //@codingStandardsIgnoreEnd
750
                        $this->_messages[] = sprintf('%s removed', $filePath);
751
                    } else {
752
                        $this->_errors[] = sprintf('%s NOT removed', $filePath);
753
754
                        return false;
755
                    }
756
                }
757
            }
758
759
            $path = rtrim($path, DIRECTORY_SEPARATOR);
760
            //@codingStandardsIgnoreStart
761 View Code Duplication
            if (@rmdir($path)) {
762
                //@codingStandardsIgnoreEnd
763
                $this->_messages[] = sprintf('%s removed', $path);
764
            } else {
765
                $this->_errors[] = sprintf('%s NOT removed', $path);
766
767
                return false;
768
            }
769
        }
770
771
        return true;
772
    }
773
774
    /**
775
     * Recursive directory copy.
776
     *
777
     * ### Options
778
     *
779
     * - `to` The directory to copy to.
780
     * - `from` The directory to copy from, this will cause a cd() to occur, changing the results of pwd().
781
     * - `mode` The mode to copy the files/directories with as integer, e.g. 0775.
782
     * - `skip` Files/directories to skip.
783
     * - `scheme` Folder::MERGE, Folder::OVERWRITE, Folder::SKIP
784
     * - `recursive` Whether to copy recursively or not (default: true - recursive)
785
     *
786
     * @param array|string $options Either an array of options (see above) or a string of the destination directory.
787
     * @return bool Success.
788
     */
789
    public function copy($options)
790
    {
791
        if (!$this->pwd()) {
792
            return false;
793
        }
794
        $to = null;
795
        if (is_string($options)) {
796
            $to = $options;
797
            $options = [];
798
        }
799
        $options += [
800
            'to' => $to,
801
            'from' => $this->path,
802
            'mode' => $this->mode,
803
            'skip' => [],
804
            'scheme' => Folder::MERGE,
805
            'recursive' => true,
806
        ];
807
808
        $fromDir = $options['from'];
809
        $toDir = $options['to'];
810
        $mode = $options['mode'];
811
812
        if (!$this->cd($fromDir)) {
813
            $this->_errors[] = sprintf('%s not found', $fromDir);
814
815
            return false;
816
        }
817
818
        if (!is_dir($toDir)) {
819
            $this->create($toDir, $mode);
820
        }
821
822
        if (!is_writable($toDir)) {
823
            $this->_errors[] = sprintf('%s not writable', $toDir);
824
825
            return false;
826
        }
827
828
        $exceptions = array_merge(['.', '..', '.svn'], $options['skip']);
829
        //@codingStandardsIgnoreStart
830
        if ($handle = @opendir($fromDir)) {
831
            //@codingStandardsIgnoreEnd
832
            while (($item = readdir($handle)) !== false) {
833
                $to = Folder::addPathElement($toDir, $item);
834
                if (($options['scheme'] != Folder::SKIP || !is_dir($to)) && !in_array($item, $exceptions)) {
835
                    $from = Folder::addPathElement($fromDir, $item);
836
                    if (is_file($from) && (!is_file($to) || $options['scheme'] != Folder::SKIP)) {
837
                        if (copy($from, $to)) {
838
                            chmod($to, intval($mode, 8));
839
                            touch($to, filemtime($from));
840
                            $this->_messages[] = sprintf('%s copied to %s', $from, $to);
841
                        } else {
842
                            $this->_errors[] = sprintf('%s NOT copied to %s', $from, $to);
843
                        }
844
                    }
845
846
                    if (is_dir($from) && file_exists($to) && $options['scheme'] === Folder::OVERWRITE) {
847
                        $this->delete($to);
848
                    }
849
850
                    if (is_dir($from) && $options['recursive'] === false) {
851
                        continue;
852
                    }
853
854
                    if (is_dir($from) && !file_exists($to)) {
855
                        $old = umask(0);
856
                        if (mkdir($to, $mode, true)) {
857
                            umask($old);
858
                            $old = umask(0);
859
                            chmod($to, $mode);
860
                            umask($old);
861
                            $this->_messages[] = sprintf('%s created', $to);
862
                            $options = ['to' => $to, 'from' => $from] + $options;
863
                            $this->copy($options);
864
                        } else {
865
                            $this->_errors[] = sprintf('%s not created', $to);
866
                        }
867
                    } elseif (is_dir($from) && $options['scheme'] === Folder::MERGE) {
868
                        $options = ['to' => $to, 'from' => $from] + $options;
869
                        $this->copy($options);
870
                    }
871
                }
872
            }
873
            closedir($handle);
874
        } else {
875
            return false;
876
        }
877
878
        return empty($this->_errors);
879
    }
880
881
    /**
882
     * Recursive directory move.
883
     *
884
     * ### Options
885
     *
886
     * - `to` The directory to copy to.
887
     * - `from` The directory to copy from, this will cause a cd() to occur, changing the results of pwd().
888
     * - `chmod` The mode to copy the files/directories with.
889
     * - `skip` Files/directories to skip.
890
     * - `scheme` Folder::MERGE, Folder::OVERWRITE, Folder::SKIP
891
     * - `recursive` Whether to copy recursively or not (default: true - recursive)
892
     *
893
     * @param array|string $options (to, from, chmod, skip, scheme)
894
     * @return bool Success
895
     */
896
    public function move($options)
897
    {
898
        $to = null;
899
        if (is_string($options)) {
900
            $to = $options;
901
            $options = (array)$options;
902
        }
903
        $options += ['to' => $to, 'from' => $this->path, 'mode' => $this->mode, 'skip' => [], 'recursive' => true];
904
905
        if ($this->copy($options) && $this->delete($options['from'])) {
906
            return (bool)$this->cd($options['to']);
907
        }
908
909
        return false;
910
    }
911
912
    /**
913
     * get messages from latest method
914
     *
915
     * @param bool $reset Reset message stack after reading
916
     * @return array
917
     */
918
    public function messages($reset = true)
919
    {
920
        $messages = $this->_messages;
921
        if ($reset) {
922
            $this->_messages = [];
923
        }
924
925
        return $messages;
926
    }
927
928
    /**
929
     * get error from latest method
930
     *
931
     * @param bool $reset Reset error stack after reading
932
     * @return array
933
     */
934
    public function errors($reset = true)
935
    {
936
        $errors = $this->_errors;
937
        if ($reset) {
938
            $this->_errors = [];
939
        }
940
941
        return $errors;
942
    }
943
944
    /**
945
     * Get the real path (taking ".." and such into account)
946
     *
947
     * @param string $path Path to resolve
948
     * @return string|false The resolved path
949
     */
950
    public function realpath($path)
951
    {
952
        if (strpos($path, '..') === false) {
953
            if (!Folder::isAbsolute($path)) {
954
                $path = Folder::addPathElement($this->path, $path);
955
            }
956
957
            return $path;
958
        }
959
        $path = str_replace('/', DIRECTORY_SEPARATOR, trim($path));
960
        $parts = explode(DIRECTORY_SEPARATOR, $path);
961
        $newparts = [];
962
        $newpath = '';
963
        if ($path[0] === DIRECTORY_SEPARATOR) {
964
            $newpath = DIRECTORY_SEPARATOR;
965
        }
966
967
        while (($part = array_shift($parts)) !== null) {
968
            if ($part === '.' || $part === '') {
969
                continue;
970
            }
971
            if ($part === '..') {
972
                if (!empty($newparts)) {
973
                    array_pop($newparts);
974
                    continue;
975
                }
976
977
                return false;
978
            }
979
            $newparts[] = $part;
980
        }
981
        $newpath .= implode(DIRECTORY_SEPARATOR, $newparts);
982
983
        return Folder::slashTerm($newpath);
984
    }
985
986
    /**
987
     * Returns true if given $path ends in a slash (i.e. is slash-terminated).
988
     *
989
     * @param string $path Path to check
990
     * @return bool true if path ends with slash, false otherwise
991
     */
992
    public static function isSlashTerm($path)
993
    {
994
        $lastChar = $path[strlen($path) - 1];
995
996
        return $lastChar === '/' || $lastChar === '\\';
997
    }
998
}
999