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