PluginProbe
WP-Stateless – Google Cloud Storage / 3.0.3
WP-Stateless – Google Cloud Storage v3.0.3
4.4.3 2.1.7 2.1.8 2.1.9 2.2.0 2.2.1 2.2.2 2.2.3 2.2.4 2.2.5 2.2.6 2.2.7 2.3.0 2.3.1 2.3.2 3.0 3.0.1 3.0.2 3.0.3 3.0.4 3.1.0 3.1.1 3.2.0 3.2.1 3.2.2 All 62 releases
wp-stateless / lib / Google / vendor / google / cloud-storage / src / SigningHelper.php

SigningHelper.php in WP-Stateless – Google Cloud Storage 3.0.3, at lib/Google/vendor/google/cloud-storage/src/SigningHelper.php

887 lines 29.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Copyright 2019 Google LLC
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18 namespace Google\Cloud\Storage;
19
20 use Google\Auth\CredentialsLoader;
21 use Google\Auth\SignBlobInterface;
22 use Google\Cloud\Core\ArrayTrait;
23 use Google\Cloud\Core\JsonTrait;
24 use Google\Cloud\Core\Timestamp;
25 use Google\Cloud\Storage\Connection\ConnectionInterface;
26
27 /**
28 * Provides common methods for signing storage URLs.
29 *
30 * @internal
31 */
32 class SigningHelper
33 {
34 use ArrayTrait;
35 use JsonTrait;
36
37 const DEFAULT_URL_SIGNING_VERSION = 'v2';
38 const DEFAULT_DOWNLOAD_HOST = 'storage.googleapis.com';
39
40 const V4_ALGO_NAME = 'GOOG4-RSA-SHA256';
41 const V4_TIMESTAMP_FORMAT = 'Ymd\THis\Z';
42 const V4_DATESTAMP_FORMAT = 'Ymd';
43
44 /**
45 * Create or fetch a SigningHelper instance.
46 *
47 * @return SigningHelper
48 */
49 public static function getHelper()
50 {
51 static $helper;
52 if (!$helper) {
53 $helper = new static;
54 }
55
56 return $helper;
57 }
58
59 /**
60 * Sign using the version inferred from `$options.version`.
61 *
62 * @param ConnectionInterface $connection A connection to the Cloud Storage
63 * API.
64 * @param Timestamp|\DateTimeInterface|int $expires The signed URL
65 * expiration.
66 * @param string $resource The URI to the storage resource, preceded by a
67 * leading slash.
68 * @param int|null $generation The resource generation.
69 * @param array $options Configuration options. See
70 * {@see Google\Cloud\Storage\StorageObject::signedUrl()} for
71 * details.
72 * @return string
73 * @throws \InvalidArgumentException
74 * @throws \RuntimeException If required data could not be gathered from
75 * credentials.
76 * @throws \RuntimeException If OpenSSL signing is required by user input
77 * and OpenSSL is not available.
78 */
79 public function sign(ConnectionInterface $connection, $expires, $resource, $generation, array $options)
80 {
81 $version = isset($options['version'])
82 ? $options['version']
83 : self::DEFAULT_URL_SIGNING_VERSION;
84
85 unset($options['version']);
86
87 switch (strtolower($version)) {
88 case 'v2':
89 $method = 'v2Sign';
90 break;
91
92 case 'v4':
93 $method = 'v4Sign';
94 break;
95
96 default:
97 throw new \InvalidArgumentException('Invalid signing version.');
98 }
99
100 return call_user_func_array([$this, $method], [
101 $connection,
102 $expires,
103 $resource,
104 $generation,
105 $options
106 ]);
107 }
108
109 /**
110 * Sign a URL using Google Signed URLs v2.
111 *
112 * This method will be deprecated in the future.
113 *
114 * @param ConnectionInterface $connection A connection to the Cloud Storage
115 * API.
116 * @param Timestamp|\DateTimeInterface|int $expires The signed URL
117 * expiration.
118 * @param string $resource The URI to the storage resource, preceded by a
119 * leading slash.
120 * @param int|null $generation The resource generation.
121 * @param array $options Configuration options. See
122 * {@see Google\Cloud\Storage\StorageObject::signedUrl()} for
123 * details.
124 * @return string
125 * @throws \InvalidArgumentException
126 * @throws \RuntimeException If required data could not be gathered from
127 * credentials.
128 * @throws \RuntimeException If OpenSSL signing is required by user input
129 * and OpenSSL is not available.
130 */
131 public function v2Sign(ConnectionInterface $connection, $expires, $resource, $generation, array $options)
132 {
133 list($credentials, $options) = $this->getSigningCredentials($connection, $options);
134
135 $expires = $this->normalizeExpiration($expires);
136 list($resource, $bucket) = $this->normalizeResource($resource);
137 $options = $this->normalizeOptions($options);
138 $headers = $this->normalizeHeaders($options['headers']);
139
140 if ($options['virtualHostedStyle']) {
141 $options['bucketBoundHostname'] = sprintf(
142 '%s.storage.googleapis.com',
143 $bucket
144 );
145 }
146
147 // Make sure disallowed headers are not included.
148 $illegalHeaders = [
149 'x-goog-encryption-key',
150 'x-goog-encryption-key-sha256'
151 ];
152 if ($illegal = array_intersect_key(array_flip($illegalHeaders), $headers)) {
153 throw new \InvalidArgumentException(sprintf(
154 '%s %s not allowed in Signed URL headers.',
155 implode(' and ', array_keys($illegal)),
156 count($illegal) === 1 ? 'is' : 'are'
157 ));
158 }
159
160 // Sort headers by name.
161 ksort($headers);
162
163 $toSign = [
164 $options['method'],
165 $options['contentMd5'],
166 $options['contentType'],
167 $expires,
168 ];
169
170 $signedHeaders = [];
171 foreach ($headers as $name => $value) {
172 $signedHeaders[] = $name .':'. $value;
173 }
174
175 // Push the headers onto the end of the signing string.
176 if ($signedHeaders) {
177 $toSign = array_merge($toSign, $signedHeaders);
178 }
179
180 $toSign[] = $resource;
181
182 $stringToSign = $this->createV2CanonicalRequest($toSign);
183
184 $signature = $credentials->signBlob($stringToSign, [
185 'forceOpenssl' => $options['forceOpenssl']
186 ]);
187
188 // Start with user-provided query params and add required parameters.
189 $params = $options['queryParams'];
190 $params['GoogleAccessId'] = $credentials->getClientName();
191 $params['Expires'] = $expires;
192 $params['Signature'] = $signature;
193
194 // urlencode parameter values
195 foreach ($params as &$value) {
196 $value = rawurlencode($value);
197 }
198
199 $params = $this->addCommonParams($generation, $params, $options);
200
201 $queryString = $this->buildQueryString($params);
202
203 $resource = $this->normalizeUriPath($options['bucketBoundHostname'], $resource);
204 return 'https://' . $options['bucketBoundHostname'] . $resource . '?' . $queryString;
205 }
206
207 /**
208 * Sign a storage URL using Google Signed URLs v4.
209 *
210 * @param ConnectionInterface $connection A connection to the Cloud Storage
211 * API.
212 * @param Timestamp|\DateTimeInterface|int $expires The signed URL
213 * expiration.
214 * @param string $resource The URI to the storage resource, preceded by a
215 * leading slash.
216 * @param int|null $generation The resource generation.
217 * @param array $options Configuration options. See
218 * {@see Google\Cloud\Storage\StorageObject::signedUrl()} for
219 * details.
220 * @return string
221 * @throws \InvalidArgumentException
222 * @throws \RuntimeException If required data could not be gathered from
223 * credentials.
224 * @throws \RuntimeException If OpenSSL signing is required by user input
225 * and OpenSSL is not available.
226 */
227 public function v4Sign(ConnectionInterface $connection, $expires, $resource, $generation, array $options)
228 {
229 list($credentials, $options) = $this->getSigningCredentials($connection, $options);
230
231 $expires = $this->normalizeExpiration($expires);
232 list($resource, $bucket) = $this->normalizeResource($resource);
233 $options = $this->normalizeOptions($options);
234
235 $time = $options['timestamp'];
236 $requestTimestamp = $time->format(self::V4_TIMESTAMP_FORMAT);
237 $requestDatestamp = $time->format(self::V4_DATESTAMP_FORMAT);
238 $timeSeconds = $time->format('U');
239
240 $expireLimit = $timeSeconds + 604800;
241 if ($expires > $expireLimit) {
242 throw new \InvalidArgumentException(
243 'V4 Signed URLs may not have an expiration greater than seven days in the future.'
244 );
245 }
246
247 $clientEmail = $credentials->getClientName();
248 $credentialScope = sprintf('%s/auto/storage/goog4_request', $requestDatestamp);
249 $credential = sprintf('%s/%s', $clientEmail, $credentialScope);
250
251 if ($options['virtualHostedStyle']) {
252 $options['bucketBoundHostname'] = sprintf(
253 '%s.storage.googleapis.com',
254 $bucket
255 );
256 }
257
258 // Add headers and query params based on provided options.
259 $params = $options['queryParams'];
260 $headers = $options['headers'] + [
261 'host' => $options['bucketBoundHostname']
262 ];
263
264 if ($options['contentType']) {
265 $headers['content-type'] = $options['contentType'];
266 }
267
268 if ($options['contentMd5']) {
269 $headers['content-md5'] = $options['contentMd5'];
270 }
271
272 $params = $this->addCommonParams($generation, $params, $options);
273
274 $headers = $this->normalizeHeaders($headers);
275
276 // sort headers by name
277 ksort($headers, SORT_NATURAL | SORT_FLAG_CASE);
278
279 // Canonical headers are a list, newline separated, of keys and values,
280 // comma separated.
281 // Signed headers are a list of keys, separated by a semicolon.
282 $canonicalHeaders = [];
283 $signedHeaders = [];
284 foreach ($headers as $key => $val) {
285 $canonicalHeaders[] = sprintf('%s:%s', $key, $val);
286 $signedHeaders[] = $key;
287 }
288 $canonicalHeaders = implode("\n", $canonicalHeaders) . "\n";
289
290 $signedHeaders = implode(';', $signedHeaders);
291
292 // Add required query parameters.
293 $params = [
294 'X-Goog-Algorithm' => self::V4_ALGO_NAME,
295 'X-Goog-Credential' => $credential,
296 'X-Goog-Date' => $requestTimestamp,
297 'X-Goog-Expires' => $expires - $timeSeconds,
298 'X-Goog-SignedHeaders' => $signedHeaders,
299 ] + $params;
300
301 $paramNames = [];
302 foreach ($params as $key => $val) {
303 $paramNames[] = $key;
304 }
305
306 sort($paramNames, SORT_REGULAR);
307
308 $sortedParams = [];
309 foreach ($paramNames as $name) {
310 $sortedParams[rawurlencode($name)] = rawurlencode($params[$name]);
311 }
312
313 $canonicalQueryString = $this->buildQueryString($sortedParams);
314 $canonicalResource = $this->normalizeCanonicalRequestResource(
315 $resource,
316 $options['bucketBoundHostname'],
317 $options['virtualHostedStyle']
318 );
319
320 $canonicalRequest = [
321 $options['method'],
322 $canonicalResource,
323 $canonicalQueryString,
324 $canonicalHeaders,
325 $signedHeaders,
326 $this->getPayloadHash($headers)
327 ];
328
329 $requestHash = $this->createV4CanonicalRequest($canonicalRequest);
330
331 // Construct the string to sign.
332 $stringToSign = implode("\n", [
333 self::V4_ALGO_NAME,
334 $requestTimestamp,
335 $credentialScope,
336 $requestHash
337 ]);
338
339 $signature = bin2hex(base64_decode($credentials->signBlob($stringToSign, [
340 'forceOpenssl' => $options['forceOpenssl']
341 ])));
342
343 // Construct the modified resource name. If a custom hostname is provided,
344 // this will remove the bucket name from the resource.
345 $resource = $this->normalizeUriPath($options['bucketBoundHostname'], $resource);
346
347 $scheme = $this->chooseScheme(
348 $options['scheme'],
349 $options['bucketBoundHostname'],
350 $options['virtualHostedStyle']
351 );
352
353 return sprintf(
354 '%s://%s%s?%s&X-Goog-Signature=%s',
355 $scheme,
356 $options['bucketBoundHostname'],
357 $resource,
358 $canonicalQueryString,
359 $signature
360 );
361 }
362
363 /**
364 * Create an HTTP POST policy using v4 signing.
365 *
366 * @param ConnectionInterface $connection A Connection to Google Cloud Storage.
367 * @param Timestamp|\DateTimeInterface|int $expires The signed URL
368 * expiration.
369 * @param string $resource The URI to the storage resource, preceded by a
370 * leading slash.
371 * @param array $options Configuration options. See
372 * {@see Google\Cloud\Storage\Bucket::generateSignedPostPolicyV4()} for details.
373 * @return array An associative array, containing (string) `uri` and
374 * (array) `fields` keys.
375 */
376 public function v4PostPolicy(
377 ConnectionInterface $connection,
378 $expires,
379 $resource,
380 array $options = []
381 ) {
382 list($credentials, $options) = $this->getSigningCredentials($connection, $options);
383
384 $expires = $this->normalizeExpiration($expires);
385 list($resource, $bucket, $object) = $this->normalizeResource($resource, false);
386 $object = trim($object, '/');
387
388 $options = $this->normalizeOptions($options) + [
389 'fields' => [],
390 'conditions' => [],
391 'successActionRedirect' => null,
392 'successActionStatus' => null
393 ];
394
395 $time = $options['timestamp'];
396 $requestTimestamp = $time->format(self::V4_TIMESTAMP_FORMAT);
397 $requestDatestamp = $time->format(self::V4_DATESTAMP_FORMAT);
398 $expiration = \DateTimeImmutable::createFromFormat('U', (string) $expires);
399 $expirationTimestamp = str_replace(
400 '+00:00',
401 'Z',
402 $expiration->format(\DateTime::RFC3339)
403 );
404
405 $clientEmail = $credentials->getClientName();
406 $credentialScope = sprintf('%s/auto/storage/goog4_request', $requestDatestamp);
407 $credential = sprintf('%s/%s', $clientEmail, $credentialScope);
408
409 if ($options['virtualHostedStyle']) {
410 $options['bucketBoundHostname'] = sprintf(
411 '%s.storage.googleapis.com',
412 $bucket
413 );
414 }
415
416 $fields = array_merge($options['fields'], [
417 'key' => $object,
418 'x-goog-algorithm' => self::V4_ALGO_NAME,
419 'x-goog-credential' => $credential,
420 'x-goog-date' => $requestTimestamp
421 ]);
422
423 $conditions = $options['conditions'];
424 foreach ($options['fields'] as $key => $value) {
425 $conditions[] = [$key => $value];
426 }
427
428 foreach ($conditions as $key => $value) {
429 $key = $key;
430 $value = $value;
431 $conditions[$key] = $value;
432 }
433
434 $conditions = array_merge($conditions, [
435 ['bucket' => $bucket],
436 ['key' => $object],
437 ['x-goog-date' => $requestTimestamp],
438 ['x-goog-credential' => $credential],
439 ['x-goog-algorithm' => self::V4_ALGO_NAME],
440 ]);
441
442 $policy = [
443 'conditions' => $conditions,
444 'expiration' => $expirationTimestamp
445 ];
446
447 $json = str_replace('\\\u', '\\u', json_encode($policy, JSON_UNESCAPED_SLASHES));
448 $stringToSign = base64_encode($json);
449
450 $signature = bin2hex(base64_decode($credentials->signBlob($stringToSign, [
451 'forceOpenssl' => $options['forceOpenssl']
452 ])));
453
454 $fields['x-goog-signature'] = $signature;
455 $fields['policy'] = $stringToSign;
456
457 // Construct the modified resource name. If a custom hostname is provided,
458 // this will remove the bucket name from the resource.
459 $resource = $this->normalizeUriPath($options['bucketBoundHostname'], '/' . $bucket, true);
460
461 $scheme = $this->chooseScheme(
462 $options['scheme'],
463 $options['bucketBoundHostname'],
464 $options['virtualHostedStyle']
465 );
466
467 return [
468 'url' => sprintf(
469 '%s://%s%s',
470 $scheme,
471 $options['bucketBoundHostname'],
472 $resource
473 ),
474 'fields' => $fields
475 ];
476 }
477
478 /**
479 * Creates a canonical request hash for a V4 Signed URL.
480 *
481 * NOTE: While in most cases `PHP_EOL` is preferable to a system-specific
482 * character, in this case `\n` is required.
483 *
484 * @param array $canonicalRequest The canonical request, with each element
485 * representing a line in the request.
486 * @return string
487 */
488 private function createV4CanonicalRequest(array $canonicalRequest)
489 {
490 $canonicalRequestString = implode("\n", $canonicalRequest);
491 return bin2hex(hash('sha256', $canonicalRequestString, true));
492 }
493
494 /**
495 * Creates a canonical request for a V2 Signed URL.
496 *
497 * NOTE: While in most cases `PHP_EOL` is preferable to a system-specific
498 * character, in this case `\n` is required.
499 *
500 * @param array $canonicalRequest The canonical request, with each element
501 * representing a line in the request.
502 * @return string
503 */
504 private function createV2CanonicalRequest(array $canonicalRequest)
505 {
506 return implode("\n", $canonicalRequest);
507 }
508
509 /**
510 * Choose the correct URL scheme.
511 *
512 * @param string $scheme The scheme provided by the user or defaults.
513 * @param string $bucketBoundHostname The bucketBoundHostname provided by the user or defaults.
514 * @param bool $virtualHostedStyle Whether virtual host style is enabled.
515 * @return string
516 */
517 private function chooseScheme($scheme, $bucketBoundHostname, $virtualHostedStyle = false)
518 {
519 // bucketBoundHostname not used -- always https.
520 if ($bucketBoundHostname === self::DEFAULT_DOWNLOAD_HOST) {
521 return 'https';
522 }
523
524 // virtualHostedStyle enabled -- always https.
525 if ($virtualHostedStyle) {
526 return 'https';
527 }
528
529 // not virtual hosted style, and custom hostname -- use default (http) or user choice.
530 return $scheme;
531 }
532
533 /**
534 * If `X-Goog-Content-SHA256` header is provided, use that as the payload.
535 * Otherwise, `UNSIGNED-PAYLOAD`.
536 *
537 * @param array $headers
538 * @return string
539 */
540 private function getPayloadHash(array $headers)
541 {
542 if (!isset($headers['x-goog-content-sha256'])) {
543 return 'UNSIGNED-PAYLOAD';
544 }
545
546 return $headers['x-goog-content-sha256'];
547 }
548
549 /**
550 * Normalizes and validates an expiration.
551 *
552 * @param Timestamp|\DateTimeInterface|int $expires The expiration
553 * @return int
554 * @throws \InvalidArgumentException If an invalid value is given.
555 */
556 private function normalizeExpiration($expires)
557 {
558 if ($expires instanceof Timestamp) {
559 $seconds = $expires->get()->format('U');
560 } elseif ($expires instanceof \DateTimeInterface) {
561 $seconds = $expires->format('U');
562 } elseif (is_numeric($expires)) {
563 $seconds = (int) $expires;
564 } else {
565 throw new \InvalidArgumentException('Invalid expiration.');
566 }
567
568 return $seconds;
569 }
570
571 /**
572 * Normalizes and encodes the resource identifier.
573 *
574 * @param string $resource The resource identifier. In form
575 * `[/]$bucket/$object`.
576 * @return array A list, where index 0 is the resource path, with pieces
577 * encoded and prefixed with a forward slash, index 1 is the bucket
578 * name, and index 2 is the object name, relative to the bucket.
579 */
580 private function normalizeResource($resource, $urlencode = true)
581 {
582 $pieces = explode('/', trim($resource, '/'));
583
584 if ($urlencode) {
585 array_walk($pieces, function (&$piece) {
586 $piece = rawurlencode($piece);
587 });
588 }
589
590 $bucket = $pieces[0];
591
592 $relative = $pieces;
593 array_shift($relative);
594
595 return [
596 '/' . implode('/', $pieces),
597 $bucket,
598 '/' . implode('/', $relative),
599 ];
600 }
601
602 /**
603 * Fixes the user input options, filters and validates data.
604 *
605 * @param array $options Signed URL configuration options.
606 * @return array
607 * @throws \InvalidArgumentException
608 */
609 private function normalizeOptions(array $options)
610 {
611 $options += [
612 'allowPost' => false,
613 'cname' => null, //@deprecated
614 'bucketBoundHostname' => self::DEFAULT_DOWNLOAD_HOST,
615 'contentMd5' => null,
616 'contentType' => null,
617 'forceOpenssl' => false,
618 'headers' => [],
619 'keyFile' => null,
620 'keyFilePath' => null,
621 'method' => 'GET',
622 'queryParams' => [],
623 'responseDisposition' => null,
624 'responseType' => null,
625 'saveAsName' => null,
626
627 // note that in almost every case this default will be overridden.
628 'scheme' => 'http',
629 'timestamp' => null,
630 'virtualHostedStyle' => false,
631 ];
632
633 $allowedMethods = ['GET', 'PUT', 'POST', 'DELETE'];
634 $options['method'] = strtoupper($options['method']);
635 if (!in_array($options['method'], $allowedMethods)) {
636 throw new \InvalidArgumentException('$options.method must be one of `GET`, `PUT` or `DELETE`.');
637 }
638
639 if ($options['method'] === 'POST' && !$options['allowPost']) {
640 throw new \InvalidArgumentException(
641 'Invalid method. To create an upload URI, use StorageObject::signedUploadUrl().'
642 );
643 }
644
645 // Rewrite deprecated `cname` to new `bucketBoundHostname`.
646 if ($options['cname'] && $options['bucketBoundHostname'] === self::DEFAULT_DOWNLOAD_HOST) {
647 $options['bucketBoundHostname'] = $options['cname'];
648 }
649
650 // strip protocol from hostname.
651 $hostnameParts = explode('//', $options['bucketBoundHostname']);
652 if (count($hostnameParts) > 1) {
653 $options['bucketBoundHostname'] = $hostnameParts[1];
654 }
655
656 $options['bucketBoundHostname'] = trim($options['bucketBoundHostname'], '/');
657
658 // If a timestamp is provided, use it in place of `now` for v4 URLs only..
659 // This option exists for testing purposes, and should not generally be provided by users.
660 if ($options['timestamp']) {
661 if (!($options['timestamp'] instanceof \DateTimeInterface)) {
662 if (!is_string($options['timestamp'])) {
663 throw new \InvalidArgumentException(
664 'User-provided timestamps must be a string or instance of `\DateTimeInterface`.'
665 );
666 }
667
668 $options['timestamp'] = \DateTimeImmutable::createFromFormat(
669 \DateTime::RFC3339,
670 $options['timestamp'],
671 new \DateTimeZone('UTC')
672 );
673
674 if (!$options['timestamp']) {
675 throw new \InvalidArgumentException(
676 'Given timestamp string is in an invalid format. Provide timestamp formatted as follows: `' .
677 \DateTime::RFC3339 .
678 '`. Note that timestamps MUST be in UTC.'
679 );
680 }
681 }
682 } else {
683 $options['timestamp'] = new \DateTimeImmutable('now', new \DateTimeZone('UTC'));
684 }
685
686 unset(
687 $options['cname'],
688 $options['allowPost']
689 );
690
691 return $options;
692 }
693
694 /**
695 * Cleans and normalizes header values.
696 *
697 * Arrays of values are collapsed into a comma-separated list, trailing and
698 * leading spaces are removed, newlines are replaced by empty strings, and
699 * multiple whitespace chars are replaced by a single space.
700 *
701 * @param array $headers Input headers
702 * @return array
703 */
704 private function normalizeHeaders(array $headers)
705 {
706 $out = [];
707 foreach ($headers as $name => $value) {
708 $name = strtolower(trim($name));
709 // collapse arrays of values into a comma-separated list.
710 if (!is_array($value)) {
711 $value = [$value];
712 }
713
714 foreach ($value as &$headerValue) {
715 // strip trailing and leading spaces.
716 $headerValue = trim($headerValue);
717
718 // replace newlines with empty strings.
719 $headerValue = str_replace(PHP_EOL, '', $headerValue);
720
721 // collapse multiple whitespace chars to a single space.
722 $headerValue = preg_replace('/[\s]+/', ' ', $headerValue);
723 }
724
725 $out[$name] = implode(', ', $value);
726 }
727
728 return $out;
729 }
730
731 /**
732 * Returns a resource formatted for use in a URI.
733 *
734 * If the bucketBoundHostname is other than the default, will omit the bucket name.
735 *
736 * @param string $bucketBoundHostname The bucketBoundHostname provided by the user, or the default
737 * value.
738 * @param string $resource The GCS resource path (i.e. /bucket/object).
739 * @return string
740 */
741 private function normalizeUriPath($bucketBoundHostname, $resource, $withTrailingSlash = false)
742 {
743 if ($bucketBoundHostname !== self::DEFAULT_DOWNLOAD_HOST) {
744 $resourceParts = explode('/', trim($resource, '/'));
745 array_shift($resourceParts);
746
747 // Resource is a Bucket.
748 if (empty($resourceParts)) {
749 $resource = '/';
750 } else {
751 $resource = '/' . implode('/', $resourceParts);
752 }
753 }
754
755 $resource = rtrim($resource, '/');
756
757 return $withTrailingSlash
758 ? $resource . '/'
759 : $resource;
760 }
761
762 /**
763 * Normalize the resource provided to the canonical request string.
764 *
765 * @param string $resource
766 * @param string $bucketBoundHostname
767 * @param boolean $virtualHostedStyle
768 * @return string
769 */
770 private function normalizeCanonicalRequestResource($resource, $bucketBoundHostname, $virtualHostedStyle = false)
771 {
772 if ($bucketBoundHostname === self::DEFAULT_DOWNLOAD_HOST && !$virtualHostedStyle) {
773 return $resource;
774 }
775
776 $pieces = explode('/', trim($resource, '/'));
777 array_shift($pieces);
778 return '/' . implode('/', $pieces);
779 }
780
781 /**
782 * Get the credentials for use with signing.
783 *
784 * @param ConnectionInterface $connection A Storage connection object.
785 * @param array $options Configuration options.
786 * @return array A list containing a credentials object at index 0 and the
787 * modified options at index 1.
788 * @throws \RuntimeException If the credentials type is not valid for signing.
789 * @throws \InvalidArgumentException If a keyfile is given and is not valid.
790 */
791 private function getSigningCredentials(ConnectionInterface $connection, array $options)
792 {
793 $keyFilePath = isset($options['keyFilePath'])
794 ? $options['keyFilePath']
795 : null;
796
797 if ($keyFilePath) {
798 if (!file_exists($keyFilePath)) {
799 throw new \InvalidArgumentException(sprintf(
800 'Keyfile path %s does not exist.',
801 $keyFilePath
802 ));
803 }
804
805 $options['keyFile'] = self::jsonDecode(file_get_contents($keyFilePath), true);
806 }
807
808 $rw = $connection->requestWrapper();
809
810 $keyFile = isset($options['keyFile'])
811 ? $options['keyFile']
812 : null;
813 if ($keyFile) {
814 $scopes = isset($options['scopes'])
815 ? $options['scopes']
816 : $rw->scopes();
817
818 $credentials = CredentialsLoader::makeCredentials($scopes, $keyFile);
819 } else {
820 $credentials = $rw->getCredentialsFetcher();
821 }
822
823 //@codeCoverageIgnoreStart
824 if (!($credentials instanceof SignBlobInterface)) {
825 throw new \RuntimeException(sprintf(
826 'Credentials object is of type `%s` and is not valid for signing.',
827 get_class($credentials)
828 ));
829 }
830 //@codeCoverageIgnoreEnd
831
832 unset(
833 $options['keyFilePath'],
834 $options['keyFile'],
835 $options['scopes']
836 );
837
838 return [$credentials, $options];
839 }
840
841 /**
842 * Add parameters common to all signed URL versions.
843 *
844 * @param int|null $generation
845 * @param array $params
846 * @param array $options
847 * @return array
848 */
849 private function addCommonParams($generation, array $params, array $options)
850 {
851 if ($options['responseType']) {
852 $params['response-content-type'] = $options['responseType'];
853 }
854
855 if ($options['responseDisposition']) {
856 $params['response-content-disposition'] = $options['responseDisposition'];
857 } elseif ($options['saveAsName']) {
858 $params['response-content-disposition'] = 'attachment; filename='
859 . '"' . $options['saveAsName'] . '"';
860 }
861
862 if ($generation) {
863 $params['generation'] = $generation;
864 }
865
866 return $params;
867 }
868
869 /**
870 * Create a query string from an array.
871 *
872 * Note that this method does NOT urlencode keys or values.
873 *
874 * @param array $input
875 * @return string
876 */
877 private function buildQueryString(array $input)
878 {
879 $q = [];
880 foreach ($input as $key => $val) {
881 $q[] = $key . '=' . $val;
882 }
883
884 return implode('&', $q);
885 }
886 }
887