Passed
Push — main ( 44baea...f4593e )
by Sugeng
03:04
created

ZipArchiveAdapter::publicUrl()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 4
c 0
b 0
f 0
dl 0
loc 9
rs 10
cc 1
nc 1
nop 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace diecoding\flysystem\adapter;
6
7
use DateTimeInterface;
8
use diecoding\flysystem\AbstractComponent;
9
use diecoding\flysystem\traits\ChecksumAdapterTrait;
10
use diecoding\flysystem\traits\UrlGeneratorComponentTrait;
11
use Generator;
12
use League\Flysystem\ChecksumProvider;
13
use League\Flysystem\Config;
14
use League\Flysystem\DirectoryAttributes;
15
use League\Flysystem\FileAttributes;
16
use League\Flysystem\FilesystemAdapter;
17
use League\Flysystem\PathPrefixer;
18
use League\Flysystem\UnableToCopyFile;
19
use League\Flysystem\UnableToCreateDirectory;
20
use League\Flysystem\UnableToDeleteDirectory;
21
use League\Flysystem\UnableToDeleteFile;
22
use League\Flysystem\UnableToMoveFile;
23
use League\Flysystem\UnableToReadFile;
24
use League\Flysystem\UnableToRetrieveMetadata;
25
use League\Flysystem\UnableToSetVisibility;
26
use League\Flysystem\UnableToWriteFile;
27
use League\Flysystem\UnixVisibility\PortableVisibilityConverter;
28
use League\Flysystem\UnixVisibility\VisibilityConverter;
29
use League\Flysystem\UrlGeneration\PublicUrlGenerator;
30
use League\Flysystem\UrlGeneration\TemporaryUrlGenerator;
31
use League\Flysystem\ZipArchive\ZipArchiveProvider;
32
use League\MimeTypeDetection\FinfoMimeTypeDetector;
33
use League\MimeTypeDetection\MimeTypeDetector;
34
use Throwable;
35
use yii\helpers\Json;
36
use yii\helpers\Url;
37
use ZipArchive;
38
39
use function fclose;
40
use function fopen;
41
use function rewind;
42
use function stream_copy_to_stream;
43
44
/**
45
 * This class override \League\Flysystem\ZipArchive\ZipArchiveAdapter
46
 */
47
final class ZipArchiveAdapter implements FilesystemAdapter, ChecksumProvider, PublicUrlGenerator, TemporaryUrlGenerator
48
{
49
    use ChecksumAdapterTrait;
50
51
    private PathPrefixer $pathPrefixer;
52
    private MimeTypeDetector $mimeTypeDetector;
53
    private VisibilityConverter $visibility;
54
55
    public function __construct(
56
        private ZipArchiveProvider $zipArchiveProvider,
57
        string $root = '',
58
        ?MimeTypeDetector $mimeTypeDetector = null,
59
        ?VisibilityConverter $visibility = null,
60
        private bool $detectMimeTypeUsingPath = false,
61
    ) {
62
        $this->pathPrefixer = new PathPrefixer(ltrim($root, '/'));
63
        $this->mimeTypeDetector = $mimeTypeDetector ?? new FinfoMimeTypeDetector();
64
        $this->visibility = $visibility ?? new PortableVisibilityConverter();
65
    }
66
67
    public function fileExists(string $path): bool
68
    {
69
        $archive = $this->zipArchiveProvider->createZipArchive();
70
        $fileExists = $archive->locateName($this->pathPrefixer->prefixPath($path)) !== false;
71
        $archive->close();
72
73
        return $fileExists;
74
    }
75
76
    public function write(string $path, string $contents, Config $config): void
77
    {
78
        try {
79
            $this->ensureParentDirectoryExists($path, $config);
80
        } catch (Throwable $exception) {
81
            throw UnableToWriteFile::atLocation($path, 'creating parent directory failed', $exception);
82
        }
83
84
        $archive = $this->zipArchiveProvider->createZipArchive();
85
        $prefixedPath = $this->pathPrefixer->prefixPath($path);
86
87
        if (!$archive->addFromString($prefixedPath, $contents)) {
88
            throw UnableToWriteFile::atLocation($path, 'writing the file failed');
89
        }
90
91
        $archive->close();
92
        $archive = $this->zipArchiveProvider->createZipArchive();
93
94
        $visibility = $config->get(Config::OPTION_VISIBILITY);
95
        $visibilityResult = $visibility === null
96
            || $this->setVisibilityAttribute($prefixedPath, $visibility, $archive);
97
        $archive->close();
98
99
        if ($visibilityResult === false) {
100
            throw UnableToWriteFile::atLocation($path, 'setting visibility failed');
101
        }
102
    }
103
104
    public function writeStream(string $path, $contents, Config $config): void
105
    {
106
        $contents = stream_get_contents($contents);
107
108
        if ($contents === false) {
109
            throw UnableToWriteFile::atLocation($path, 'Could not get contents of given resource.');
110
        }
111
112
        $this->write($path, $contents, $config);
113
    }
114
115
    public function read(string $path): string
116
    {
117
        $archive = $this->zipArchiveProvider->createZipArchive();
118
        $contents = $archive->getFromName($this->pathPrefixer->prefixPath($path));
119
        $statusString = $archive->getStatusString();
120
        $archive->close();
121
122
        if ($contents === false) {
123
            throw UnableToReadFile::fromLocation($path, $statusString);
124
        }
125
126
        return $contents;
127
    }
128
129
    public function readStream(string $path)
130
    {
131
        $archive = $this->zipArchiveProvider->createZipArchive();
132
        $resource = $archive->getStream($this->pathPrefixer->prefixPath($path));
133
134
        if ($resource === false) {
135
            $status = $archive->getStatusString();
136
            $archive->close();
137
            throw UnableToReadFile::fromLocation($path, $status);
138
        }
139
140
        $stream = fopen('php://temp', 'w+b');
141
        stream_copy_to_stream($resource, $stream);
142
        rewind($stream);
143
        fclose($resource);
144
145
        return $stream;
146
    }
147
148
    public function delete(string $path): void
149
    {
150
        $prefixedPath = $this->pathPrefixer->prefixPath($path);
151
        $zipArchive = $this->zipArchiveProvider->createZipArchive();
152
        $success = $zipArchive->locateName($prefixedPath) === false || $zipArchive->deleteName($prefixedPath);
153
        $statusString = $zipArchive->getStatusString();
154
        $zipArchive->close();
155
156
        if (!$success) {
157
            throw UnableToDeleteFile::atLocation($path, $statusString);
158
        }
159
    }
160
161
    public function deleteDirectory(string $path): void
162
    {
163
        $archive = $this->zipArchiveProvider->createZipArchive();
164
        $prefixedPath = $this->pathPrefixer->prefixDirectoryPath($path);
165
166
        for ($i = $archive->numFiles; $i > 0; $i--) {
167
            if (($stats = $archive->statIndex($i)) === false) {
168
                continue;
169
            }
170
171
            $itemPath = $stats['name'];
172
173
            if (strpos($itemPath, $prefixedPath) !== 0) {
174
                continue;
175
            }
176
177
            if (!$archive->deleteIndex($i)) {
178
                $statusString = $archive->getStatusString();
179
                $archive->close();
180
                throw UnableToDeleteDirectory::atLocation($path, $statusString);
181
            }
182
        }
183
184
        $archive->deleteName($prefixedPath);
185
186
        $archive->close();
187
    }
188
189
    public function createDirectory(string $path, Config $config): void
190
    {
191
        try {
192
            $this->ensureDirectoryExists($path, $config);
193
        } catch (Throwable $exception) {
194
            throw UnableToCreateDirectory::dueToFailure($path, $exception);
195
        }
196
    }
197
198
    public function directoryExists(string $path): bool
199
    {
200
        $archive = $this->zipArchiveProvider->createZipArchive();
201
        $location = $this->pathPrefixer->prefixDirectoryPath($path);
202
203
        return $archive->statName($location) !== false;
204
    }
205
206
    public function setVisibility(string $path, string $visibility): void
207
    {
208
        $archive = $this->zipArchiveProvider->createZipArchive();
209
        $location = $this->pathPrefixer->prefixPath($path);
210
        $stats = $archive->statName($location) ?: $archive->statName($location . '/');
211
212
        if ($stats === false) {
0 ignored issues
show
introduced by
The condition $stats === false is always false.
Loading history...
213
            $statusString = $archive->getStatusString();
214
            $archive->close();
215
            throw UnableToSetVisibility::atLocation($path, $statusString);
216
        }
217
218
        if (!$this->setVisibilityAttribute($stats['name'], $visibility, $archive)) {
219
            $statusString1 = $archive->getStatusString();
220
            $archive->close();
221
            throw UnableToSetVisibility::atLocation($path, $statusString1);
222
        }
223
224
        $archive->close();
225
    }
226
227
    public function visibility(string $path): FileAttributes
228
    {
229
        /** @var int|null $opsys */
230
        $opsys = null;
231
        /** @var int|null $attr */
232
        $attr = null;
233
        $archive = $this->zipArchiveProvider->createZipArchive();
234
        $archive->getExternalAttributesName(
235
            $this->pathPrefixer->prefixPath($path),
236
            $opsys,
237
            $attr
238
        );
239
        $archive->close();
240
241
        if ($opsys !== ZipArchive::OPSYS_UNIX || $attr === null) {
242
            throw UnableToRetrieveMetadata::visibility($path);
243
        }
244
245
        return new FileAttributes(
246
            $path,
247
            null,
248
            $this->visibility->inverseForFile($attr >> 16)
249
        );
250
    }
251
252
    public function mimeType(string $path): FileAttributes
253
    {
254
        try {
255
            $mimetype = $this->detectMimeTypeUsingPath
256
                ? $this->mimeTypeDetector->detectMimeTypeFromPath($path)
257
                : $this->mimeTypeDetector->detectMimeType($path, $this->read($path));
258
        } catch (Throwable $exception) {
259
            throw UnableToRetrieveMetadata::mimeType($path, $exception->getMessage(), $exception);
260
        }
261
262
        if ($mimetype === null) {
263
            throw UnableToRetrieveMetadata::mimeType($path, 'Unknown.');
264
        }
265
266
        return new FileAttributes($path, null, null, null, $mimetype);
267
    }
268
269
    public function lastModified(string $path): FileAttributes
270
    {
271
        $zipArchive = $this->zipArchiveProvider->createZipArchive();
272
        $stats = $zipArchive->statName($this->pathPrefixer->prefixPath($path));
273
        $statusString = $zipArchive->getStatusString();
274
        $zipArchive->close();
275
276
        if ($stats === false) {
277
            throw UnableToRetrieveMetadata::lastModified($path, $statusString);
278
        }
279
280
        return new FileAttributes($path, null, null, $stats['mtime']);
281
    }
282
283
    public function fileSize(string $path): FileAttributes
284
    {
285
        $archive = $this->zipArchiveProvider->createZipArchive();
286
        $stats = $archive->statName($this->pathPrefixer->prefixPath($path));
287
        $statusString = $archive->getStatusString();
288
        $archive->close();
289
290
        if ($stats === false) {
291
            throw UnableToRetrieveMetadata::fileSize($path, $statusString);
292
        }
293
294
        if ($this->isDirectoryPath($stats['name'])) {
295
            throw UnableToRetrieveMetadata::fileSize($path, 'It\'s a directory.');
296
        }
297
298
        return new FileAttributes($path, $stats['size'], null, null);
299
    }
300
301
    public function listContents(string $path, bool $deep): iterable
302
    {
303
        $archive = $this->zipArchiveProvider->createZipArchive();
304
        $location = $this->pathPrefixer->prefixDirectoryPath($path);
305
        $items = [];
306
307
        for ($i = 0; $i < $archive->numFiles; $i++) {
308
            $stats = $archive->statIndex($i);
309
            if ($stats === false) {
310
                continue;
311
            }
312
313
            $itemPath = $stats['name'];
314
315
            if (
316
                $location === $itemPath
317
                || ($deep && $location !== '' && strpos($itemPath, $location) !== 0)
318
                || ($deep === false && !$this->isAtRootDirectory($location, $itemPath))
319
            ) {
320
                continue;
321
            }
322
323
            $items[] = $this->isDirectoryPath($itemPath)
324
                ? new DirectoryAttributes(
325
                    $this->pathPrefixer->stripDirectoryPrefix($itemPath),
326
                    null,
327
                    $stats['mtime']
328
                )
329
                : new FileAttributes(
330
                    $this->pathPrefixer->stripPrefix($itemPath),
331
                    $stats['size'],
332
                    null,
333
                    $stats['mtime']
334
                );
335
        }
336
337
        $archive->close();
338
339
        return $this->yieldItemsFrom($items);
340
    }
341
342
    private function yieldItemsFrom(array $items): Generator
343
    {
344
        yield from $items;
345
    }
346
347
    public function move(string $source, string $destination, Config $config): void
348
    {
349
        try {
350
            $this->ensureParentDirectoryExists($destination, $config);
351
        } catch (Throwable $exception) {
352
            throw UnableToMoveFile::fromLocationTo($source, $destination, $exception);
353
        }
354
355
        $archive = $this->zipArchiveProvider->createZipArchive();
356
357
        if ($archive->locateName($this->pathPrefixer->prefixPath($destination)) !== false) {
358
            $this->delete($destination);
359
            $this->copy($source, $destination, $config);
360
            $this->delete($source);
361
            return;
362
        }
363
364
        $renamed = $archive->renameName(
365
            $this->pathPrefixer->prefixPath($source),
366
            $this->pathPrefixer->prefixPath($destination)
367
        );
368
        if ($renamed === false) {
369
            throw UnableToMoveFile::fromLocationTo($source, $destination);
370
        }
371
    }
372
373
    public function copy(string $source, string $destination, Config $config): void
374
    {
375
        try {
376
            $readStream = $this->readStream($source);
377
            $this->writeStream($destination, $readStream, $config);
378
        } catch (Throwable $exception) {
379
            if (isset($readStream)) {
380
                @fclose($readStream);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for fclose(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

380
                /** @scrutinizer ignore-unhandled */ @fclose($readStream);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
381
            }
382
383
            throw UnableToCopyFile::fromLocationTo($source, $destination, $exception);
384
        }
385
    }
386
387
    private function ensureParentDirectoryExists(string $path, Config $config): void
388
    {
389
        $dirname = dirname($path);
390
391
        if ($dirname === '' || $dirname === '.') {
392
            return;
393
        }
394
395
        $this->ensureDirectoryExists($dirname, $config);
396
    }
397
398
    private function ensureDirectoryExists(string $dirname, Config $config): void
399
    {
400
        $visibility = $config->get(Config::OPTION_DIRECTORY_VISIBILITY);
401
        $archive = $this->zipArchiveProvider->createZipArchive();
402
        $prefixedDirname = $this->pathPrefixer->prefixDirectoryPath($dirname);
403
        $parts = array_filter(explode('/', trim($prefixedDirname, '/')));
404
        $dirPath = '';
405
406
        foreach ($parts as $part) {
407
            $dirPath .= $part . '/';
408
            $info = $archive->statName($dirPath);
409
410
            if ($info === false && $archive->addEmptyDir($dirPath) === false) {
411
                throw UnableToCreateDirectory::atLocation($dirname);
412
            }
413
414
            if ($visibility === null) {
415
                continue;
416
            }
417
418
            if (!$this->setVisibilityAttribute($dirPath, $visibility, $archive)) {
419
                $archive->close();
420
                throw UnableToCreateDirectory::atLocation($dirname, 'Unable to set visibility.');
421
            }
422
        }
423
424
        $archive->close();
425
    }
426
427
    private function isDirectoryPath(string $path): bool
428
    {
429
        return substr($path, -1) === '/';
430
    }
431
432
    private function isAtRootDirectory(string $directoryRoot, string $path): bool
433
    {
434
        $dirname = dirname($path);
435
436
        if ('' === $directoryRoot && '.' === $dirname) {
437
            return true;
438
        }
439
440
        return $directoryRoot === (rtrim($dirname, '/') . '/');
441
    }
442
443
    private function setVisibilityAttribute(string $statsName, string $visibility, ZipArchive $archive): bool
444
    {
445
        $visibility = $this->isDirectoryPath($statsName)
446
            ? $this->visibility->forDirectory($visibility)
447
            : $this->visibility->forFile($visibility);
448
449
        return $archive->setExternalAttributesName($statsName, ZipArchive::OPSYS_UNIX, $visibility << 16);
450
    }
451
452
    // =================================================
453
454
    /**
455
     * @var UrlGeneratorComponentTrait|AbstractComponent
456
     */
457
    public $component;
458
459
    public function publicUrl(string $path, Config $config): string
460
    {
461
        // TODO: Use absolute path and don't encrypt
462
        $params = [
463
            'path' => $path,
464
            'expires' => 0,
465
        ];
466
467
        return Url::toRoute([$this->component->action, 'data' => $this->component->encrypt(Json::encode($params))], true);
0 ignored issues
show
Bug Best Practice introduced by
The property action does not exist on diecoding\flysystem\AbstractComponent. Since you implemented __get, consider adding a @property annotation.
Loading history...
Bug introduced by
The method encrypt() does not exist on diecoding\flysystem\AbstractComponent. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

467
        return Url::toRoute([$this->component->action, 'data' => $this->component->/** @scrutinizer ignore-call */ encrypt(Json::encode($params))], true);
Loading history...
468
    }
469
470
    public function temporaryUrl(string $path, DateTimeInterface $expiresAt, Config $config): string
471
    {
472
        // TODO: Use absolute path and don't encrypt
473
        $params = [
474
            'path' => $path,
475
            'expires' => (int) $expiresAt->getTimestamp(),
476
        ];
477
478
        return Url::toRoute([$this->component->action, 'data' => $this->component->encrypt(Json::encode($params))], true);
0 ignored issues
show
Bug Best Practice introduced by
The property action does not exist on diecoding\flysystem\AbstractComponent. Since you implemented __get, consider adding a @property annotation.
Loading history...
479
    }
480
}