Completed
Pull Request — master (#208)
by
unknown
12:19
created

AwsS3Adapter::getCloudFrontUrl()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 4

Importance

Changes 0
Metric Value
dl 0
loc 21
ccs 8
cts 8
cp 1
rs 9.584
c 0
b 0
f 0
cc 4
nc 2
nop 3
crap 4
1
<?php
2
3
namespace League\Flysystem\AwsS3v3;
4
5
use Aws\Result;
6
use Aws\S3\Exception\DeleteMultipleObjectsException;
7
use Aws\S3\Exception\S3Exception;
8
use Aws\S3\Exception\S3MultipartUploadException;
9
use Aws\S3\S3Client;
10
use Aws\S3\S3ClientInterface;
11
use Aws\CloudFront\UrlSigner;
12
13
use League\Flysystem\AdapterInterface;
14
use League\Flysystem\Adapter\AbstractAdapter;
15
use League\Flysystem\Adapter\CanOverwriteFiles;
16
use League\Flysystem\Config;
17
use League\Flysystem\Util;
18
19
class AwsS3Adapter extends AbstractAdapter implements CanOverwriteFiles
20
{
21
    const PUBLIC_GRANT_URI = 'http://acs.amazonaws.com/groups/global/AllUsers';
22
23
    /**
24
     * @var array
25
     */
26
    protected static $resultMap = [
27
        'Body'          => 'contents',
28
        'ContentLength' => 'size',
29
        'ContentType'   => 'mimetype',
30
        'Size'          => 'size',
31
        'Metadata'      => 'metadata',
32
        'StorageClass'  => 'storageclass',
33
        'ETag'          => 'etag',
34
        'VersionId'     => 'versionid'
35
    ];
36
37
    /**
38
     * @var array
39
     */
40
    protected static $metaOptions = [
41
        'ACL',
42
        'CacheControl',
43
        'ContentDisposition',
44
        'ContentEncoding',
45
        'ContentLength',
46
        'ContentType',
47
        'Expires',
48
        'GrantFullControl',
49
        'GrantRead',
50
        'GrantReadACP',
51
        'GrantWriteACP',
52
        'Metadata',
53
        'RequestPayer',
54
        'SSECustomerAlgorithm',
55
        'SSECustomerKey',
56
        'SSECustomerKeyMD5',
57
        'SSEKMSKeyId',
58
        'ServerSideEncryption',
59
        'StorageClass',
60
        'Tagging',
61
        'WebsiteRedirectLocation',
62
    ];
63
64
    /**
65
     * @var S3ClientInterface
66
     */
67
    protected $s3Client;
68
69
    /**
70
     * @var string
71
     */
72
    protected $bucket;
73
74
    /**
75
     * @var array
76
     */
77
    protected $options = [];
78
79
    /**
80
     * Constructor.
81
     *
82
     * @param S3ClientInterface $client
83
     * @param string   $bucket
84
     * @param string   $prefix
85 76
     * @param array    $options
86
     */
87 76
    public function __construct(S3ClientInterface $client, $bucket, $prefix = '', array $options = [])
88 76
    {
89 76
        $this->s3Client = $client;
90 76
        $this->bucket = $bucket;
91 76
        $this->setPathPrefix($prefix);
92
        $this->options = $options;
93
    }
94
95
    /**
96
     * Get the S3Client bucket.
97
     *
98 4
     * @return string
99
     */
100 4
    public function getBucket()
101
    {
102
        return $this->bucket;
103
    }
104
105
    /**
106
     * Set the S3Client bucket.
107
     *
108 2
     * @return string
109
     */
110 2
    public function setBucket($bucket)
111 2
    {
112
        $this->bucket = $bucket;
113
    }
114
115
    /**
116
     * Get the S3Client instance.
117
     *
118 2
     * @return S3ClientInterface
119
     */
120 2
    public function getClient()
121
    {
122
        return $this->s3Client;
123
    }
124
125
    /**
126
     * Write a new file.
127
     *
128
     * @param string $path
129
     * @param string $contents
130
     * @param Config $config Config object
131
     *
132 6
     * @return false|array false on failure file meta data on success
133
     */
134 6
    public function write($path, $contents, Config $config)
135
    {
136
        return $this->upload($path, $contents, $config);
137
    }
138
139
    /**
140
     * Update a file.
141
     *
142
     * @param string $path
143
     * @param string $contents
144
     * @param Config $config Config object
145
     *
146 4
     * @return false|array false on failure file meta data on success
147
     */
148 4
    public function update($path, $contents, Config $config)
149
    {
150
        return $this->upload($path, $contents, $config);
151
    }
152
153
    /**
154
     * Rename a file.
155
     *
156
     * @param string $path
157
     * @param string $newpath
158
     *
159 4
     * @return bool
160
     */
161 4
    public function rename($path, $newpath)
162 2
    {
163
        if ( ! $this->copy($path, $newpath)) {
164
            return false;
165 2
        }
166
167
        return $this->delete($path);
168
    }
169
170
    /**
171
     * Delete a file.
172
     *
173
     * @param string $path
174
     *
175 4
     * @return bool
176
     */
177 4
    public function delete($path)
178
    {
179 4
        $location = $this->applyPathPrefix($path);
180 4
181
        $command = $this->s3Client->getCommand(
182 4
            'deleteObject',
183 4
            [
184
                'Bucket' => $this->bucket,
185 2
                'Key'    => $location,
186
            ]
187 4
        );
188
189 4
        $this->s3Client->execute($command);
190
191
        return ! $this->has($path);
192
    }
193
194
    /**
195
     * Delete a directory.
196
     *
197
     * @param string $dirname
198
     *
199 4
     * @return bool
200
     */
201
    public function deleteDir($dirname)
202 4
    {
203 4
        try {
204 3
            $prefix = $this->applyPathPrefix($dirname) . '/';
205 2
            $this->s3Client->deleteMatchingObjects($this->bucket, $prefix);
206
        } catch (DeleteMultipleObjectsException $exception) {
207
            return false;
208 2
        }
209
210
        return true;
211
    }
212
213
    /**
214
     * Create a directory.
215
     *
216
     * @param string $dirname directory name
217
     * @param Config $config
218
     *
219 4
     * @return bool|array
220
     */
221 4
    public function createDir($dirname, Config $config)
222
    {
223
        return $this->upload($dirname . '/', '', $config);
224
    }
225
226
    /**
227
     * Check whether a file exists.
228
     *
229
     * @param string $path
230
     *
231 12
     * @return bool
232
     */
233 12
    public function has($path)
234
    {
235 12
        $location = $this->applyPathPrefix($path);
236 2
237
        if ($this->s3Client->doesObjectExist($this->bucket, $location, $this->options)) {
238
            return true;
239 10
        }
240
241
        return $this->doesDirectoryExist($location);
242
    }
243
244
    /**
245
     * Read a file.
246
     *
247
     * @param string $path
248
     *
249 6
     * @return false|array
250
     */
251 6
    public function read($path)
252
    {
253 6
        $response = $this->readObject($path);
254 4
255 2
        if ($response !== false) {
256
            $response['contents'] = $response['contents']->getContents();
257 6
        }
258
259
        return $response;
260
    }
261
262
    /**
263
     * List contents of a directory.
264
     *
265
     * @param string $directory
266
     * @param bool   $recursive
267
     *
268 4
     * @return array
269
     */
270 4
    public function listContents($directory = '', $recursive = false)
271 4
    {
272
        $prefix = $this->applyPathPrefix(rtrim($directory, '/') . '/');
273 4
        $options = ['Bucket' => $this->bucket, 'Prefix' => ltrim($prefix, '/')];
274 4
275 2
        if ($recursive === false) {
276
            $options['Delimiter'] = '/';
277 4
        }
278 4
279 4
        $listing = $this->retrievePaginatedListing($options);
280
        $normalizer = [$this, 'normalizeResponse'];
281 4
        $normalized = array_map($normalizer, $listing);
282
283
        return Util::emulateDirectories($normalized);
284
    }
285
286
    /**
287
     * @param array $options
288
     *
289 4
     * @return array
290
     */
291 4
    protected function retrievePaginatedListing(array $options)
292 4
    {
293
        $resultPaginator = $this->s3Client->getPaginator('ListObjects', $options);
294 4
        $listing = [];
295 4
296 2
        foreach ($resultPaginator as $result) {
297
            $listing = array_merge($listing, $result->get('Contents') ?: [], $result->get('CommonPrefixes') ?: []);
298 4
        }
299
300
        return $listing;
301
    }
302
303
    /**
304
     * Get all the meta data of a file or directory.
305
     *
306
     * @param string $path
307
     *
308 14
     * @return false|array
309
     */
310 14
    public function getMetadata($path)
311 14
    {
312
        $command = $this->s3Client->getCommand(
313 14
            'headObject',
314 14
            [
315 14
                'Bucket' => $this->bucket,
316 7
                'Key'    => $this->applyPathPrefix($path),
317
            ] + $this->options
318
        );
319
320 14
        /* @var Result $result */
321 9
        try {
322 4
            $result = $this->s3Client->execute($command);
323 2
        } catch (S3Exception $exception) {
324
            if ($this->is404Exception($exception)) {
325
                return false;
326 2
            }
327
328
            throw $exception;
329 10
        }
330
331
        return $this->normalizeResponse($result->toArray(), $path);
332
    }
333
334
    /**
335 4
     * @return bool
336
     */
337 4
    private function is404Exception(S3Exception $exception)
338
    {
339 4
        $response = $exception->getResponse();
340 2
341
        if ($response !== null && $response->getStatusCode() === 404) {
342
            return true;
343 2
        }
344
345
        return false;
346
    }
347
348
    /**
349
     * Get all the meta data of a file or directory.
350
     *
351
     * @param string $path
352
     *
353 2
     * @return false|array
354
     */
355 2
    public function getSize($path)
356
    {
357
        return $this->getMetadata($path);
358
    }
359
360
    /**
361
     * Get the mimetype of a file.
362
     *
363
     * @param string $path
364
     *
365 2
     * @return false|array
366
     */
367 2
    public function getMimetype($path)
368
    {
369
        return $this->getMetadata($path);
370
    }
371
372
    /**
373
     * Get the timestamp of a file.
374
     *
375
     * @param string $path
376
     *
377 2
     * @return false|array
378
     */
379 2
    public function getTimestamp($path)
380
    {
381
        return $this->getMetadata($path);
382
    }
383
384
    /**
385
     * Write a new file using a stream.
386
     *
387
     * @param string   $path
388
     * @param resource $resource
389
     * @param Config   $config Config object
390
     *
391 4
     * @return array|false false on failure file meta data on success
392
     */
393 4
    public function writeStream($path, $resource, Config $config)
394
    {
395
        return $this->upload($path, $resource, $config);
396
    }
397
398
    /**
399
     * Update a file using a stream.
400
     *
401
     * @param string   $path
402
     * @param resource $resource
403
     * @param Config   $config Config object
404
     *
405 4
     * @return array|false false on failure file meta data on success
406
     */
407 4
    public function updateStream($path, $resource, Config $config)
408
    {
409
        return $this->upload($path, $resource, $config);
410
    }
411
412
    /**
413
     * Copy a file.
414
     *
415
     * @param string $path
416
     * @param string $newpath
417
     *
418 8
     * @return bool
419
     */
420 8
    public function copy($path, $newpath)
421 8
    {
422
        $command = $this->s3Client->getCommand(
423 8
            'copyObject',
424 8
            [
425 8
                'Bucket'     => $this->bucket,
426 8
                'Key'        => $this->applyPathPrefix($newpath),
427 8
                'CopySource' => S3Client::encodeKey($this->bucket . '/' . $this->applyPathPrefix($path)),
428 8
                'ACL'        => $this->getRawVisibility($path) === AdapterInterface::VISIBILITY_PUBLIC
429 4
                    ? 'public-read' : 'private',
430
            ] + $this->options
431
        );
432 8
433 6
        try {
434 4
            $this->s3Client->execute($command);
435
        } catch (S3Exception $e) {
436
            return false;
437 4
        }
438
439
        return true;
440
    }
441
442
    /**
443
     * Get a CloudFront URL for the file at the given path.
444
     *
445
     * @param  string $path
446
     * @param  \DateTimeInterface $expiration
447 6
     * @param  array $options
448
     * @return string
449 6
     */
450
    public function getCloudFrontUrl($path, $expiration, array $options = [])
451 6
    {
452 6
        $cloudfront_options = [
453 6
            'endpoint', 'key_pair_id', 'private_key'
454 3
        ];
455
456 6
        $options = array_merge($options, $this->options);
457
458
        if (count(array_intersect_key(array_flip($cloudfront_options), $options)) === count($cloudfront_options) && count(array_filter($options)) === count($cloudfront_options)) {
459
            $urlSigner = new UrlSigner(
460
                $options['key_pair_id'],
461
                $options['private_key']
462
            );
463
464
            return $urlSigner->getSignedUrl(
465
                rtrim($options['endpoint'], '/').'/'.ltrim($path, '/'),
466 10
                $expiration->getTimestamp(),
467
                isset($options['policy']) ? $options['policy'] : null
468
            );
469 10
        }
470 10
    }
471 5
472
    /**
473 10
     * Get a temporary URL for the file at the given path.
474 2
     *
475 1
     * @param  string $path
476
     * @param  \DateTimeInterface $expiration
477 10
     * @param  array $options
478
     * @return string
479
     */
480
    public function getTemporaryUrl($path, $expiration, array $options = [])
481 10
    {
482 6
        $cloudfront_url = $this->getCloudFrontUrl($path, $expiration, $options);
483 2
484
        if ($cloudfront_url) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $cloudfront_url of type string|null is loosely compared to true; this is ambiguous if the string can be empty. 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 string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
485
            return $cloudfront_url;
486 8
        }
487
488
        $command = $this->s3Client->getCommand('GetObject', array_merge([
489
            'Bucket' => $this->getBucket(),
490
            'Key' => $this->getPathPrefix().$path,
491
        ], $options));
492
493
        return (string) $this->s3Client->createPresignedRequest(
494
            $command, $expiration
0 ignored issues
show
Documentation introduced by
$expiration is of type object<DateTimeInterface>, but the function expects a integer|string|object<DateTime>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
495
        )->getUri();
496
    }
497 6
498
    /**
499 6
     * Read a file as a stream.
500 6
     *
501
     * @param string $path
502 6
     *
503 6
     * @return array|false
504 6
     */
505
    public function readStream($path)
506 3
    {
507
        $response = $this->readObject($path);
508
509 6
        if ($response !== false) {
510 4
            $response['stream'] = $response['contents']->detach();
511 2
            unset($response['contents']);
512
        }
513
514 4
        return $response;
515
    }
516
517
    /**
518
     * Read an object and normalize the response.
519
     *
520
     * @param string $path
521
     *
522
     * @return array|bool
523
     */
524 4
    protected function readObject($path)
525
    {
526 4
        $options = [
527
            'Bucket' => $this->bucket,
528
            'Key'    => $this->applyPathPrefix($path),
529
        ];
530
531
        if (isset($this->options['@http'])) {
532 68
            $options['@http'] = $this->options['@http'];
533
        }
534 68
535
        $command = $this->s3Client->getCommand('getObject', $options + $this->options);
536
537
        try {
538
            /** @var Result $response */
539
            $response = $this->s3Client->execute($command);
540 76
        } catch (S3Exception $e) {
541
            return false;
542 76
        }
543
544 76
        return $this->normalizeResponse($response->toArray(), $path);
545
    }
546
547
    /**
548
     * Set the visibility for a file.
549
     *
550
     * @param string $path
551
     * @param string $visibility
552
     *
553
     * @return array|false file meta data
554 12
     */
555
    public function setVisibility($path, $visibility)
556 12
    {
557 12
        $command = $this->s3Client->getCommand(
558
            'putObjectAcl',
559 12
            [
560 12
                'Bucket' => $this->bucket,
561
                'Key'    => $this->applyPathPrefix($path),
562 6
                'ACL'    => $visibility === AdapterInterface::VISIBILITY_PUBLIC ? 'public-read' : 'private',
563
            ]
564 12
        );
565 12
566
        try {
567 12
            $this->s3Client->execute($command);
568
        } catch (S3Exception $exception) {
569 2
            return false;
570 2
        }
571 2
572 1
        return compact('path', 'visibility');
573 2
    }
574 2
575
    /**
576 6
     * Get the visibility of a file.
577
     *
578 12
     * @param string $path
579
     *
580
     * @return array|false
581
     */
582
    public function getVisibility($path)
583
    {
584
        return ['visibility' => $this->getRawVisibility($path)];
585
    }
586
587
    /**
588
     * {@inheritdoc}
589
     */
590 14
    public function applyPathPrefix($path)
591
    {
592 14
        return ltrim(parent::applyPathPrefix($path), '/');
593 14
    }
594 14
595
    /**
596 14
     * {@inheritdoc}
597 12
     */
598 2
    public function setPathPrefix($prefix)
599 1
    {
600
        $prefix = ltrim($prefix, '/');
601 12
602 12
        return parent::setPathPrefix($prefix);
603 6
    }
604
605 12
    /**
606
     * Get the object acl presented as a visibility.
607
     *
608 6
     * @param string $path
609
     *
610
     * @return string
611 14
     */
612 8
    protected function getRawVisibility($path)
613 2
    {
614
        $command = $this->s3Client->getCommand(
615
            'getObjectAcl',
616 12
            [
617
                'Bucket' => $this->bucket,
618
                'Key'    => $this->applyPathPrefix($path),
619
            ]
620
        );
621
622
        $result = $this->s3Client->execute($command);
623
        $visibility = AdapterInterface::VISIBILITY_PRIVATE;
624
625
        foreach ($result->get('Grants') as $grant) {
626 30
            if (
627
                isset($grant['Grantee']['URI'])
628 30
                && $grant['Grantee']['URI'] === self::PUBLIC_GRANT_URI
629
                && $grant['Permission'] === 'READ'
630
            ) {
631
                $visibility = AdapterInterface::VISIBILITY_PUBLIC;
632
                break;
633
            }
634
        }
635
636
        return $visibility;
637
    }
638 14
639
    /**
640 14
     * Upload an object.
641
     *
642 14
     * @param string          $path
643
     * @param string|resource $body
644 10
     * @param Config          $config
645
     *
646 10
     * @return array|bool
647 5
     */
648
    protected function upload($path, $body, Config $config)
649 14
    {
650
        $key = $this->applyPathPrefix($path);
651 10
        $options = $this->getOptionsFromConfig($config);
652
        $acl = array_key_exists('ACL', $options) ? $options['ACL'] : 'private';
653 10
654 5
        if (!$this->isOnlyDir($path)) {
655
            if ( ! isset($options['ContentType'])) {
656 14
                $options['ContentType'] = Util::guessMimeType($path, $body);
657 14
            }
658 14
659
            if ( ! isset($options['ContentLength'])) {
660 10
                $options['ContentLength'] = is_resource($body) ? Util::getStreamSize($body) : Util::contentSize($body);
0 ignored issues
show
Bug introduced by
It seems like $body defined by parameter $body on line 648 can also be of type resource; however, League\Flysystem\Util::contentSize() does only seem to accept string, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
661 7
            }
662
663 14
            if ($options['ContentLength'] === null) {
664
                unset($options['ContentLength']);
665
            }
666
        }
667
668
        try {
669
            $this->s3Client->upload($this->bucket, $key, $body, $acl, ['params' => $options]);
670
        } catch (S3MultipartUploadException $multipartUploadException) {
671
            return false;
672
        }
673
674 28
        return $this->normalizeResponse($options, $path);
675
    }
676
677 28
    /**
678 16
     * Check if the path contains only directories
679 14
     *
680 14
     * @param string $path
681 28
     *
682
     * @return bool
683 28
     */
684 16
    private function isOnlyDir($path)
685 8
    {
686
        return substr($path, -1) === '/';
687 28
    }
688 6
689 6
    /**
690
     * Get options from the config.
691 6
     *
692
     * @param Config $config
693
     *
694 26
     * @return array
695
     */
696
    protected function getOptionsFromConfig(Config $config)
697
    {
698
        $options = $this->options;
699
700
        if ($visibility = $config->get('visibility')) {
701
            // For local reference
702 10
            $options['visibility'] = $visibility;
703
            // For external reference
704
            $options['ACL'] = $visibility === AdapterInterface::VISIBILITY_PUBLIC ? 'public-read' : 'private';
705
        }
706 10
707 10
        if ($mimetype = $config->get('mimetype')) {
708
            // For local reference
709 10
            $options['mimetype'] = $mimetype;
710 10
            // For external reference
711 10
            $options['ContentType'] = $mimetype;
712
        }
713 5
714
        foreach (static::$metaOptions as $option) {
715
            if ( ! $config->has($option)) {
716 10
                continue;
717
            }
718 6
            $options[$option] = $config->get($option);
719 4
        }
720 4
721 2
        return $options;
722
    }
723
724 2
    /**
725
     * Normalize the object result array.
726
     *
727
     * @param array  $response
728
     * @param string $path
729
     *
730
     * @return array
731
     */
732
    protected function normalizeResponse(array $response, $path = null)
733
    {
734
        $result = [
735
            'path' => $path ?: $this->removePathPrefix(
736
                isset($response['Key']) ? $response['Key'] : $response['Prefix']
737
            ),
738
        ];
739
        $result = array_merge($result, Util::pathinfo($result['path']));
740
741
        if (isset($response['LastModified'])) {
742
            $result['timestamp'] = strtotime($response['LastModified']);
743
        }
744
745
        if ($this->isOnlyDir($result['path'])) {
746
            $result['type'] = 'dir';
747
            $result['path'] = rtrim($result['path'], '/');
748
749
            return $result;
750
        }
751
752
        return array_merge($result, Util::map($response, static::$resultMap), ['type' => 'file']);
753
    }
754
755
    /**
756
     * @param string $location
757
     *
758
     * @return bool
759
     */
760
    protected function doesDirectoryExist($location)
761
    {
762
        // Maybe this isn't an actual key, but a prefix.
763
        // Do a prefix listing of objects to determine.
764
        $command = $this->s3Client->getCommand(
765
            'listObjects',
766
            [
767
                'Bucket'  => $this->bucket,
768
                'Prefix'  => rtrim($location, '/') . '/',
769
                'MaxKeys' => 1,
770
            ]
771
        );
772
773
        try {
774
            $result = $this->s3Client->execute($command);
775
776
            return $result['Contents'] || $result['CommonPrefixes'];
777
        } catch (S3Exception $e) {
778
            if (in_array($e->getStatusCode(), [403, 404], true)) {
779
                return false;
780
            }
781
782
            throw $e;
783
        }
784
    }
785
}
786