Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
12.38% covered (danger)
12.38%
13 / 105
9.09% covered (danger)
9.09%
2 / 22
CRAP
0.00% covered (danger)
0.00%
0 / 1
FileSystem
12.38% covered (danger)
12.38%
13 / 105
9.09% covered (danger)
9.09%
2 / 22
720.80
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 findAllFilesAbsolutePaths
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
12
 getAttributes
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
3.03
 fileExists
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 read
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 readStream
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 listContents
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 lastModified
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 fileSize
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 mimeType
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 visibility
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 write
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 writeStream
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 setVisibility
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 delete
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 deleteDirectory
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 createDirectory
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 move
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 copy
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 getRelativePath
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
42
 isSymlinkedFile
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 isSubDirOf
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * This class extends Flysystem's Filesystem class to add some additional functionality, particularly around
4 * symlinks which are not supported by Flysystem.
5 *
6 * TODO: Delete and modify operations on files in symlinked directories should fail with a warning.
7 *
8 * @see https://github.com/thephpleague/flysystem/issues/599
9 */
10
11namespace BrianHenryIE\Strauss\Helpers;
12
13use BrianHenryIE\Strauss\Files\FileBase;
14use Elazar\Flystream\StripProtocolPathNormalizer;
15use League\Flysystem\DirectoryListing;
16use League\Flysystem\FileAttributes;
17use League\Flysystem\FilesystemOperator;
18use League\Flysystem\FilesystemReader;
19use League\Flysystem\PathNormalizer;
20use League\Flysystem\StorageAttributes;
21
22class FileSystem implements FilesystemOperator, FlysystemBackCompatInterface
23{
24    use FlysystemBackCompatTrait;
25
26    protected FilesystemOperator $flysystem;
27    protected PathNormalizer $normalizer;
28
29    protected string $workingDir;
30
31    /**
32     * TODO: maybe restrict the constructor to only accept a LocalFilesystemAdapter.
33     *
34     * TODO: Check are any of these methods unused
35     */
36    public function __construct(FilesystemOperator $flysystem, string $workingDir)
37    {
38        $this->flysystem = $flysystem;
39        $this->normalizer = new StripProtocolPathNormalizer('mem');
40
41        $this->workingDir = $workingDir;
42    }
43
44    /**
45     * @param string[] $fileAndDirPaths
46     *
47     * @return string[]
48     */
49    public function findAllFilesAbsolutePaths(array $fileAndDirPaths): array
50    {
51        $files = [];
52
53        foreach ($fileAndDirPaths as $path) {
54            if (!$this->directoryExists($path)) {
55                $files[] = $path;
56                continue;
57            }
58
59            $directoryListing = $this->listContents(
60                $path,
61                FilesystemReader::LIST_DEEP
62            );
63
64            /** @var FileAttributes[] $files */
65            $fileAttributesArray = $directoryListing->toArray();
66
67            $f = array_map(fn($file) => '/'.$file->path(), $fileAttributesArray);
68
69            $files = array_merge($files, $f);
70        }
71
72        return $files;
73    }
74
75    public function getAttributes(string $absolutePath): ?StorageAttributes
76    {
77        $fileDirectory = realpath(dirname($absolutePath));
78
79        $absolutePath = $this->normalizer->normalizePath($absolutePath);
80
81        // Unsupported symbolic link encountered at location //home
82        // \League\Flysystem\SymbolicLinkEncountered
83        $dirList = $this->listContents($fileDirectory)->toArray();
84        foreach ($dirList as $file) { // TODO: use the generator.
85            if ($file->path() === $absolutePath) {
86                return $file;
87            }
88        }
89
90        return null;
91    }
92
93    public function fileExists(string $location): bool
94    {
95        return $this->flysystem->fileExists(
96            $this->normalizer->normalizePath($location)
97        );
98    }
99
100    public function read(string $location): string
101    {
102        return $this->flysystem->read(
103            $this->normalizer->normalizePath($location)
104        );
105    }
106
107    public function readStream(string $location)
108    {
109        return $this->flysystem->readStream(
110            $this->normalizer->normalizePath($location)
111        );
112    }
113
114    public function listContents(string $location, bool $deep = self::LIST_SHALLOW): DirectoryListing
115    {
116        return $this->flysystem->listContents(
117            $this->normalizer->normalizePath($location),
118            $deep
119        );
120    }
121
122    public function lastModified(string $path): int
123    {
124        return $this->flysystem->lastModified(
125            $this->normalizer->normalizePath($path)
126        );
127    }
128
129    public function fileSize(string $path): int
130    {
131        return $this->flysystem->fileSize(
132            $this->normalizer->normalizePath($path)
133        );
134    }
135
136    public function mimeType(string $path): string
137    {
138        return $this->flysystem->mimeType(
139            $this->normalizer->normalizePath($path)
140        );
141    }
142
143    public function visibility(string $path): string
144    {
145        return $this->flysystem->visibility(
146            $this->normalizer->normalizePath($path)
147        );
148    }
149
150    public function write(string $location, string $contents, array $config = []): void
151    {
152        $this->flysystem->write(
153            $this->normalizer->normalizePath($location),
154            $contents,
155            $config
156        );
157    }
158
159    public function writeStream(string $location, $contents, array $config = []): void
160    {
161        $this->flysystem->writeStream(
162            $this->normalizer->normalizePath($location),
163            $contents,
164            $config
165        );
166    }
167
168    public function setVisibility(string $path, string $visibility): void
169    {
170        $this->flysystem->setVisibility(
171            $this->normalizer->normalizePath($path),
172            $visibility
173        );
174    }
175
176    public function delete(string $location): void
177    {
178        $this->flysystem->delete(
179            $this->normalizer->normalizePath($location)
180        );
181    }
182
183    public function deleteDirectory(string $location): void
184    {
185        $this->flysystem->deleteDirectory(
186            $this->normalizer->normalizePath($location)
187        );
188    }
189
190    public function createDirectory(string $location, array $config = []): void
191    {
192        $this->flysystem->createDirectory(
193            $this->normalizer->normalizePath($location),
194            $config
195        );
196    }
197
198    public function move(string $source, string $destination, array $config = []): void
199    {
200        $this->flysystem->move(
201            $this->normalizer->normalizePath($source),
202            $this->normalizer->normalizePath($destination),
203            $config
204        );
205    }
206
207    public function copy(string $source, string $destination, array $config = []): void
208    {
209        $this->flysystem->copy(
210            $this->normalizer->normalizePath($source),
211            $this->normalizer->normalizePath($destination),
212            $config
213        );
214    }
215
216    /**
217     *
218     * /path/to/this/dir, /path/to/file.php => ../../file.php
219     * /path/to/here, /path/to/here/dir/file.php => dir/file.php
220     *
221     * @param string $fromAbsoluteDirectory
222     * @param string $toAbsolutePath
223     * @return string
224     */
225    public function getRelativePath(string $fromAbsoluteDirectory, string $toAbsolutePath): string
226    {
227        $fromAbsoluteDirectory = $this->normalizer->normalizePath($fromAbsoluteDirectory);
228        $toAbsolutePath = $this->normalizer->normalizePath($toAbsolutePath);
229
230        $fromDirectoryParts = array_filter(explode('/', $fromAbsoluteDirectory));
231        $toPathParts = array_filter(explode('/', $toAbsolutePath));
232        foreach ($fromDirectoryParts as $key => $part) {
233            if ($part === $toPathParts[$key]) {
234                unset($toPathParts[$key]);
235                unset($fromDirectoryParts[$key]);
236            } else {
237                break;
238            }
239            if (count($fromDirectoryParts) === 0 || count($toPathParts) === 0) {
240                break;
241            }
242        }
243
244        $relativePath =
245            str_repeat('../', count($fromDirectoryParts))
246            . implode('/', $toPathParts);
247
248        if ($this->directoryExists($toAbsolutePath)) {
249            $relativePath .= '/';
250        }
251
252        return $relativePath;
253    }
254
255
256    /**
257     * Check does the filepath point to a file outside the working directory.
258     * If `realpath()` fails to resolve the path, assume it's a symlink.
259     */
260    public function isSymlinkedFile(FileBase $file): bool
261    {
262        $realpath = realpath($file->getSourcePath());
263
264        return ! $realpath || ! str_starts_with($realpath, $this->workingDir);
265    }
266
267    /**
268     * Does the subdir path start with the dir path?
269     */
270    public function isSubDirOf(string $dir, string $subdir): bool
271    {
272        return str_starts_with(
273            $this->normalizer->normalizePath($subdir),
274            $this->normalizer->normalizePath($dir)
275        );
276    }
277}