1<?php
2
3namespace League\Flysystem\Adapter;
4
5use DirectoryIterator;
6use FilesystemIterator;
7use finfo as Finfo;
8use League\Flysystem\Config;
9use League\Flysystem\Exception;
10use League\Flysystem\NotSupportedException;
11use League\Flysystem\UnreadableFileException;
12use League\Flysystem\Util;
13use LogicException;
14use RecursiveDirectoryIterator;
15use RecursiveIteratorIterator;
16use SplFileInfo;
17
18class Local extends AbstractAdapter
19{
20    /**
21     * @var int
22     */
23    const SKIP_LINKS = 0001;
24
25    /**
26     * @var int
27     */
28    const DISALLOW_LINKS = 0002;
29
30    /**
31     * @var array
32     */
33    protected static $permissions = [
34        'file' => [
35            'public' => 0644,
36            'private' => 0600,
37        ],
38        'dir' => [
39            'public' => 0755,
40            'private' => 0700,
41        ],
42    ];
43
44    /**
45     * @var string
46     */
47    protected $pathSeparator = DIRECTORY_SEPARATOR;
48
49    /**
50     * @var array
51     */
52    protected $permissionMap;
53
54    /**
55     * @var int
56     */
57    protected $writeFlags;
58
59    /**
60     * @var int
61     */
62    private $linkHandling;
63
64    /**
65     * Constructor.
66     *
67     * @param string $root
68     * @param int    $writeFlags
69     * @param int    $linkHandling
70     * @param array  $permissions
71     *
72     * @throws LogicException
73     */
74    public function __construct($root, $writeFlags = LOCK_EX, $linkHandling = self::DISALLOW_LINKS, array $permissions = [])
75    {
76        $root = is_link($root) ? realpath($root) : $root;
77        $this->permissionMap = array_replace_recursive(static::$permissions, $permissions);
78        $this->ensureDirectory($root);
79
80        if ( ! is_dir($root) || ! is_readable($root)) {
81            throw new LogicException('The root path ' . $root . ' is not readable.');
82        }
83
84        $this->setPathPrefix($root);
85        $this->writeFlags = $writeFlags;
86        $this->linkHandling = $linkHandling;
87    }
88
89    /**
90     * Ensure the root directory exists.
91     *
92     * @param string $root root directory path
93     *
94     * @return void
95     *
96     * @throws Exception in case the root directory can not be created
97     */
98    protected function ensureDirectory($root)
99    {
100        if ( ! is_dir($root)) {
101            $umask = umask(0);
102
103            if ( ! @mkdir($root, $this->permissionMap['dir']['public'], true)) {
104                $mkdirError = error_get_last();
105            }
106
107            umask($umask);
108            clearstatcache(false, $root);
109
110            if ( ! is_dir($root)) {
111                $errorMessage = isset($mkdirError['message']) ? $mkdirError['message'] : '';
112                throw new Exception(sprintf('Impossible to create the root directory "%s". %s', $root, $errorMessage));
113            }
114        }
115    }
116
117    /**
118     * @inheritdoc
119     */
120    public function has($path)
121    {
122        $location = $this->applyPathPrefix($path);
123
124        return file_exists($location);
125    }
126
127    /**
128     * @inheritdoc
129     */
130    public function write($path, $contents, Config $config)
131    {
132        $location = $this->applyPathPrefix($path);
133        $this->ensureDirectory(dirname($location));
134
135        if (($size = file_put_contents($location, $contents, $this->writeFlags)) === false) {
136            return false;
137        }
138
139        $type = 'file';
140        $result = compact('contents', 'type', 'size', 'path');
141
142        if ($visibility = $config->get('visibility')) {
143            $result['visibility'] = $visibility;
144            $this->setVisibility($path, $visibility);
145        }
146
147        return $result;
148    }
149
150    /**
151     * @inheritdoc
152     */
153    public function writeStream($path, $resource, Config $config)
154    {
155        $location = $this->applyPathPrefix($path);
156        $this->ensureDirectory(dirname($location));
157        $stream = fopen($location, 'w+b');
158
159        if ( ! $stream || stream_copy_to_stream($resource, $stream) === false || ! fclose($stream)) {
160            return false;
161        }
162
163        $type = 'file';
164        $result = compact('type', 'path');
165
166        if ($visibility = $config->get('visibility')) {
167            $this->setVisibility($path, $visibility);
168            $result['visibility'] = $visibility;
169        }
170
171        return $result;
172    }
173
174    /**
175     * @inheritdoc
176     */
177    public function readStream($path)
178    {
179        $location = $this->applyPathPrefix($path);
180        $stream = fopen($location, 'rb');
181
182        return ['type' => 'file', 'path' => $path, 'stream' => $stream];
183    }
184
185    /**
186     * @inheritdoc
187     */
188    public function updateStream($path, $resource, Config $config)
189    {
190        return $this->writeStream($path, $resource, $config);
191    }
192
193    /**
194     * @inheritdoc
195     */
196    public function update($path, $contents, Config $config)
197    {
198        $location = $this->applyPathPrefix($path);
199        $size = file_put_contents($location, $contents, $this->writeFlags);
200
201        if ($size === false) {
202            return false;
203        }
204
205        $type = 'file';
206
207        $result = compact('type', 'path', 'size', 'contents');
208
209        if ($visibility = $config->get('visibility')) {
210            $this->setVisibility($path, $visibility);
211            $result['visibility'] = $visibility;
212        }
213
214        return $result;
215    }
216
217    /**
218     * @inheritdoc
219     */
220    public function read($path)
221    {
222        $location = $this->applyPathPrefix($path);
223        $contents = @file_get_contents($location);
224
225        if ($contents === false) {
226            return false;
227        }
228
229        return ['type' => 'file', 'path' => $path, 'contents' => $contents];
230    }
231
232    /**
233     * @inheritdoc
234     */
235    public function rename($path, $newpath)
236    {
237        $location = $this->applyPathPrefix($path);
238        $destination = $this->applyPathPrefix($newpath);
239        $parentDirectory = $this->applyPathPrefix(Util::dirname($newpath));
240        $this->ensureDirectory($parentDirectory);
241
242        return rename($location, $destination);
243    }
244
245    /**
246     * @inheritdoc
247     */
248    public function copy($path, $newpath)
249    {
250        $location = $this->applyPathPrefix($path);
251        $destination = $this->applyPathPrefix($newpath);
252        $this->ensureDirectory(dirname($destination));
253
254        return copy($location, $destination);
255    }
256
257    /**
258     * @inheritdoc
259     */
260    public function delete($path)
261    {
262        $location = $this->applyPathPrefix($path);
263
264        return @unlink($location);
265    }
266
267    /**
268     * @inheritdoc
269     */
270    public function listContents($directory = '', $recursive = false)
271    {
272        $result = [];
273        $location = $this->applyPathPrefix($directory);
274
275        if ( ! is_dir($location)) {
276            return [];
277        }
278
279        $iterator = $recursive ? $this->getRecursiveDirectoryIterator($location) : $this->getDirectoryIterator($location);
280
281        foreach ($iterator as $file) {
282            $path = $this->getFilePath($file);
283
284            if (preg_match('#(^|/|\\\\)\.{1,2}$#', $path)) {
285                continue;
286            }
287
288            $result[] = $this->normalizeFileInfo($file);
289        }
290
291        unset($iterator);
292
293        return array_filter($result);
294    }
295
296    /**
297     * @inheritdoc
298     */
299    public function getMetadata($path)
300    {
301        $location = $this->applyPathPrefix($path);
302        clearstatcache(false, $location);
303        $info = new SplFileInfo($location);
304
305        return $this->normalizeFileInfo($info);
306    }
307
308    /**
309     * @inheritdoc
310     */
311    public function getSize($path)
312    {
313        return $this->getMetadata($path);
314    }
315
316    /**
317     * @inheritdoc
318     */
319    public function getMimetype($path)
320    {
321        $location = $this->applyPathPrefix($path);
322        $finfo = new Finfo(FILEINFO_MIME_TYPE);
323        $mimetype = $finfo->file($location);
324
325        if (in_array($mimetype, ['application/octet-stream', 'inode/x-empty', 'application/x-empty'])) {
326            $mimetype = Util\MimeType::detectByFilename($location);
327        }
328
329        return ['path' => $path, 'type' => 'file', 'mimetype' => $mimetype];
330    }
331
332    /**
333     * @inheritdoc
334     */
335    public function getTimestamp($path)
336    {
337        return $this->getMetadata($path);
338    }
339
340    /**
341     * @inheritdoc
342     */
343    public function getVisibility($path)
344    {
345        $location = $this->applyPathPrefix($path);
346        clearstatcache(false, $location);
347        $permissions = octdec(substr(sprintf('%o', fileperms($location)), -4));
348        $type = is_dir($location) ? 'dir' : 'file';
349
350        foreach ($this->permissionMap[$type] as $visibility => $visibilityPermissions) {
351            if ($visibilityPermissions == $permissions) {
352                return compact('path', 'visibility');
353            }
354        }
355
356        $visibility = substr(sprintf('%o', fileperms($location)), -4);
357
358        return compact('path', 'visibility');
359    }
360
361    /**
362     * @inheritdoc
363     */
364    public function setVisibility($path, $visibility)
365    {
366        $location = $this->applyPathPrefix($path);
367        $type = is_dir($location) ? 'dir' : 'file';
368        $success = chmod($location, $this->permissionMap[$type][$visibility]);
369
370        if ($success === false) {
371            return false;
372        }
373
374        return compact('path', 'visibility');
375    }
376
377    /**
378     * @inheritdoc
379     */
380    public function createDir($dirname, Config $config)
381    {
382        $location = $this->applyPathPrefix($dirname);
383        $umask = umask(0);
384        $visibility = $config->get('visibility', 'public');
385        $return = ['path' => $dirname, 'type' => 'dir'];
386
387        if ( ! is_dir($location)) {
388            if (false === @mkdir($location, $this->permissionMap['dir'][$visibility], true)
389                || false === is_dir($location)) {
390                $return = false;
391            }
392        }
393
394        umask($umask);
395
396        return $return;
397    }
398
399    /**
400     * @inheritdoc
401     */
402    public function deleteDir($dirname)
403    {
404        $location = $this->applyPathPrefix($dirname);
405
406        if ( ! is_dir($location)) {
407            return false;
408        }
409
410        $contents = $this->getRecursiveDirectoryIterator($location, RecursiveIteratorIterator::CHILD_FIRST);
411
412        /** @var SplFileInfo $file */
413        foreach ($contents as $file) {
414            $this->guardAgainstUnreadableFileInfo($file);
415            $this->deleteFileInfoObject($file);
416        }
417
418        unset($contents);
419
420        return rmdir($location);
421    }
422
423    /**
424     * @param SplFileInfo $file
425     */
426    protected function deleteFileInfoObject(SplFileInfo $file)
427    {
428        switch ($file->getType()) {
429            case 'dir':
430                rmdir($file->getRealPath());
431                break;
432            case 'link':
433                unlink($file->getPathname());
434                break;
435            default:
436                unlink($file->getRealPath());
437        }
438    }
439
440    /**
441     * Normalize the file info.
442     *
443     * @param SplFileInfo $file
444     *
445     * @return array|void
446     *
447     * @throws NotSupportedException
448     */
449    protected function normalizeFileInfo(SplFileInfo $file)
450    {
451        if ( ! $file->isLink()) {
452            return $this->mapFileInfo($file);
453        }
454
455        if ($this->linkHandling & self::DISALLOW_LINKS) {
456            throw NotSupportedException::forLink($file);
457        }
458    }
459
460    /**
461     * Get the normalized path from a SplFileInfo object.
462     *
463     * @param SplFileInfo $file
464     *
465     * @return string
466     */
467    protected function getFilePath(SplFileInfo $file)
468    {
469        $location = $file->getPathname();
470        $path = $this->removePathPrefix($location);
471
472        return trim(str_replace('\\', '/', $path), '/');
473    }
474
475    /**
476     * @param string $path
477     * @param int    $mode
478     *
479     * @return RecursiveIteratorIterator
480     */
481    protected function getRecursiveDirectoryIterator($path, $mode = RecursiveIteratorIterator::SELF_FIRST)
482    {
483        return new RecursiveIteratorIterator(
484            new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS),
485            $mode
486        );
487    }
488
489    /**
490     * @param string $path
491     *
492     * @return DirectoryIterator
493     */
494    protected function getDirectoryIterator($path)
495    {
496        $iterator = new DirectoryIterator($path);
497
498        return $iterator;
499    }
500
501    /**
502     * @param SplFileInfo $file
503     *
504     * @return array
505     */
506    protected function mapFileInfo(SplFileInfo $file)
507    {
508        $normalized = [
509            'type' => $file->getType(),
510            'path' => $this->getFilePath($file),
511        ];
512
513        $normalized['timestamp'] = $file->getMTime();
514
515        if ($normalized['type'] === 'file') {
516            $normalized['size'] = $file->getSize();
517        }
518
519        return $normalized;
520    }
521
522    /**
523     * @param SplFileInfo $file
524     *
525     * @throws UnreadableFileException
526     */
527    protected function guardAgainstUnreadableFileInfo(SplFileInfo $file)
528    {
529        if ( ! $file->isReadable()) {
530            throw UnreadableFileException::forFileInfo($file);
531        }
532    }
533}
534