PluginProbe
Media Cloud Sync / 1.4.1
Media Cloud Sync v1.4.1
1.4.1 1.4.0 1.3.12 1.3.11 1.3.10 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.1.0 1.1.1 1.2.0 1.2.10 1.2.11 1.2.12 1.2.13 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.2.7 1.2.8 1.2.9 All 35 releases
← All changes | includes/sdk/google/google/cloud-storage/src/SigningHelper.php +111 -75 1.2.01.4.1 View file →
@@ -14,16 +14,18 @@
14 14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 15 * See the License for the specific language governing permissions and
16 16 * limitations under the License.
17 17 */
18 -namespace Dudlewebs\WPMCS\Google\Cloud\Storage;
18 +namespace Dudlewebs\WPMCS\GCP\Google\Cloud\Storage;
19 19
20 -use Dudlewebs\WPMCS\Google\Auth\CredentialsLoader;
21 -use Dudlewebs\WPMCS\Google\Auth\SignBlobInterface;
22 -use Dudlewebs\WPMCS\Google\Cloud\Core\ArrayTrait;
23 -use Dudlewebs\WPMCS\Google\Cloud\Core\JsonTrait;
24 -use Dudlewebs\WPMCS\Google\Cloud\Core\Timestamp;
25 -use Dudlewebs\WPMCS\Google\Cloud\Storage\Connection\ConnectionInterface;
20 +use Dudlewebs\WPMCS\GCP\Google\Auth\CredentialsLoader;
21 +use Dudlewebs\WPMCS\GCP\Google\Auth\SignBlobInterface;
22 +use Dudlewebs\WPMCS\GCP\Google\Cloud\Core\ArrayTrait;
23 +use Dudlewebs\WPMCS\GCP\Google\Cloud\Core\Exception\ServiceException;
24 +use Dudlewebs\WPMCS\GCP\Google\Cloud\Core\JsonTrait;
25 +use Dudlewebs\WPMCS\GCP\Google\Cloud\Core\Timestamp;
26 +use Dudlewebs\WPMCS\GCP\Google\Cloud\Storage\Connection\ConnectionInterface;
27 +use Dudlewebs\WPMCS\GCP\Google\Cloud\Storage\Connection\RetryTrait;
26 28 /**
27 29 * Provides common methods for signing storage URLs.
28 30 *
29 31 * @internal
@@ -31,13 +33,15 @@
31 33 class SigningHelper
32 34 {
33 35 use ArrayTrait;
34 36 use JsonTrait;
37 + use RetryTrait;
35 38 const DEFAULT_URL_SIGNING_VERSION = 'v2';
36 39 const DEFAULT_DOWNLOAD_HOST = 'storage.googleapis.com';
37 40 const V4_ALGO_NAME = 'GOOG4-RSA-SHA256';
38 - const V4_TIMESTAMP_FORMAT = 'Dudlewebs\WPMCS\Ymd\THis\Z';
41 + const V4_TIMESTAMP_FORMAT = 'Dudlewebs\\WPMCS\\GCP\\Ymd\\THis\\Z';
39 42 const V4_DATESTAMP_FORMAT = 'Ymd';
43 + const MAX_RETRIES = 5;
40 44 /**
41 45 * Create or fetch a SigningHelper instance.
42 46 *
43 47 * @return SigningHelper
@@ -74,9 +78,9 @@
74 78 public function sign(ConnectionInterface $connection, $expires, $resource, $generation, array $options)
75 79 {
76 80 $version = $options['version'] ?? self::DEFAULT_URL_SIGNING_VERSION;
77 81 unset($options['version']);
78 - switch (strtolower($version)) {
82 + switch (\strtolower($version)) {
79 83 case 'v2':
80 84 $method = 'v2Sign';
81 85 break;
82 86 case 'v4':
@@ -84,9 +88,9 @@
84 88 break;
85 89 default:
86 90 throw new \InvalidArgumentException('Invalid signing version.');
87 91 }
88 - return call_user_func_array([$this, $method], [$connection, $expires, $resource, $generation, $options]);
92 + return \call_user_func_array([$this, $method], [$connection, $expires, $resource, $generation, $options]);
89 93 }
90 94 /**
91 95 * Sign a URL using Google Signed URLs v2.
92 96 *
@@ -117,17 +121,17 @@
117 121 list($resource, $bucket) = $this->normalizeResource($resource);
118 122 $options = $this->normalizeOptions($options);
119 123 $headers = $this->normalizeHeaders($options['headers']);
120 124 if ($options['virtualHostedStyle']) {
121 - $options['bucketBoundHostname'] = sprintf('%s.storage.googleapis.com', $bucket);
125 + $options['bucketBoundHostname'] = \sprintf('%s.storage.googleapis.com', $bucket);
122 126 }
123 127 // Make sure disallowed headers are not included.
124 128 $illegalHeaders = ['x-goog-encryption-key', 'x-goog-encryption-key-sha256'];
125 - if ($illegal = array_intersect_key(array_flip($illegalHeaders), $headers)) {
126 - throw new \InvalidArgumentException(sprintf('%s %s not allowed in Signed URL headers.', implode(' and ', array_keys($illegal)), count($illegal) === 1 ? 'is' : 'are'));
129 + if ($illegal = \array_intersect_key(\array_flip($illegalHeaders), $headers)) {
130 + throw new \InvalidArgumentException(\sprintf('%s %s not allowed in Signed URL headers.', \implode(' and ', \array_keys($illegal)), \count($illegal) === 1 ? 'is' : 'are'));
127 131 }
128 132 // Sort headers by name.
129 - ksort($headers);
133 + \ksort($headers);
130 134 $toSign = [$options['method'], $options['contentMd5'], $options['contentType'], $expires];
131 135 $signedHeaders = [];
132 136 foreach ($headers as $name => $value) {
133 137 $signedHeaders[] = $name . ':' . $value;
@@ -133,13 +137,14 @@
133 137 $signedHeaders[] = $name . ':' . $value;
134 138 }
135 139 // Push the headers onto the end of the signing string.
136 140 if ($signedHeaders) {
137 - $toSign = array_merge($toSign, $signedHeaders);
141 + $toSign = \array_merge($toSign, $signedHeaders);
138 142 }
139 143 $toSign[] = $resource;
140 144 $stringToSign = $this->createV2CanonicalRequest($toSign);
141 - $signature = $credentials->signBlob($stringToSign, ['forceOpenssl' => $options['forceOpenssl']]);
145 + // Use exponential backOff
146 + $signature = $this->retrySignBlob(fn() => $credentials->signBlob($stringToSign, ['forceOpenssl' => $options['forceOpenssl']]));
142 147 // Start with user-provided query params and add required parameters.
143 148 $params = $options['queryParams'];
144 149 $params['GoogleAccessId'] = $credentials->getClientName();
145 150 $params['Expires'] = $expires;
@@ -145,9 +150,9 @@
145 150 $params['Expires'] = $expires;
146 151 $params['Signature'] = $signature;
147 152 // urlencode parameter values
148 153 foreach ($params as &$value) {
149 - $value = rawurlencode($value);
154 + $value = \rawurlencode($value ?? '');
150 155 }
151 156 $params = $this->addCommonParams($generation, $params, $options);
152 157 $queryString = $this->buildQueryString($params);
153 158 $resource = $this->normalizeUriPath($options['bucketBoundHostname'], $resource);
@@ -188,12 +193,12 @@
188 193 if ($expires > $expireLimit) {
189 194 throw new \InvalidArgumentException('V4 Signed URLs may not have an expiration greater than seven days in the future.');
190 195 }
191 196 $clientEmail = $credentials->getClientName();
192 - $credentialScope = sprintf('%s/auto/storage/goog4_request', $requestDatestamp);
193 - $credential = sprintf('%s/%s', $clientEmail, $credentialScope);
197 + $credentialScope = \sprintf('%s/auto/storage/goog4_request', $requestDatestamp);
198 + $credential = \sprintf('%s/%s', $clientEmail, $credentialScope);
194 199 if ($options['virtualHostedStyle']) {
195 - $options['bucketBoundHostname'] = sprintf('%s.storage.googleapis.com', $bucket);
200 + $options['bucketBoundHostname'] = \sprintf('%s.storage.googleapis.com', $bucket);
196 201 }
197 202 // Add headers and query params based on provided options.
198 203 $params = $options['queryParams'];
199 204 $headers = $options['headers'] + ['host' => $options['bucketBoundHostname']];
@@ -205,9 +210,9 @@
205 210 }
206 211 $params = $this->addCommonParams($generation, $params, $options);
207 212 $headers = $this->normalizeHeaders($headers);
208 213 // sort headers by name
209 - ksort($headers, \SORT_NATURAL | \SORT_FLAG_CASE);
214 + \ksort($headers, \SORT_NATURAL | \SORT_FLAG_CASE);
210 215 // Canonical headers are a list, newline separated, of keys and values,
211 216 // comma separated.
212 217 // Signed headers are a list of keys, separated by a semicolon.
213 218 $canonicalHeaders = [];
@@ -212,13 +217,13 @@
212 217 // Signed headers are a list of keys, separated by a semicolon.
213 218 $canonicalHeaders = [];
214 219 $signedHeaders = [];
215 220 foreach ($headers as $key => $val) {
216 - $canonicalHeaders[] = sprintf('%s:%s', $key, $val);
221 + $canonicalHeaders[] = \sprintf('%s:%s', $key, $val);
217 222 $signedHeaders[] = $key;
218 223 }
219 - $canonicalHeaders = implode("\n", $canonicalHeaders) . "\n";
220 - $signedHeaders = implode(';', $signedHeaders);
224 + $canonicalHeaders = \implode("\n", $canonicalHeaders) . "\n";
225 + $signedHeaders = \implode(';', $signedHeaders);
221 226 // Add required query parameters.
222 227 $params = ['X-Goog-Algorithm' => self::V4_ALGO_NAME, 'X-Goog-Credential' => $credential, 'X-Goog-Date' => $requestTimestamp, 'X-Goog-Expires' => $expires - $timeSeconds, 'X-Goog-SignedHeaders' => $signedHeaders] + $params;
223 228 $paramNames = [];
224 229 foreach ($params as $key => $val) {
@@ -223,12 +228,12 @@
223 228 $paramNames = [];
224 229 foreach ($params as $key => $val) {
225 230 $paramNames[] = $key;
226 231 }
227 - sort($paramNames, \SORT_REGULAR);
232 + \sort($paramNames, \SORT_REGULAR);
228 233 $sortedParams = [];
229 234 foreach ($paramNames as $name) {
230 - $sortedParams[rawurlencode($name)] = rawurlencode($params[$name]);
235 + $sortedParams[\rawurlencode($name)] = \rawurlencode($params[$name]);
231 236 }
232 237 $canonicalQueryString = $this->buildQueryString($sortedParams);
233 238 $canonicalResource = $this->normalizeCanonicalRequestResource($resource, $options['bucketBoundHostname'], $options['virtualHostedStyle']);
234 239 $canonicalRequest = [$options['method'], $canonicalResource, $canonicalQueryString, $canonicalHeaders, $signedHeaders, $this->getPayloadHash($headers)];
@@ -233,15 +238,15 @@
233 238 $canonicalResource = $this->normalizeCanonicalRequestResource($resource, $options['bucketBoundHostname'], $options['virtualHostedStyle']);
234 239 $canonicalRequest = [$options['method'], $canonicalResource, $canonicalQueryString, $canonicalHeaders, $signedHeaders, $this->getPayloadHash($headers)];
235 240 $requestHash = $this->createV4CanonicalRequest($canonicalRequest);
236 241 // Construct the string to sign.
237 - $stringToSign = implode("\n", [self::V4_ALGO_NAME, $requestTimestamp, $credentialScope, $requestHash]);
238 - $signature = bin2hex(base64_decode($credentials->signBlob($stringToSign, ['forceOpenssl' => $options['forceOpenssl']])));
242 + $stringToSign = \implode("\n", [self::V4_ALGO_NAME, $requestTimestamp, $credentialScope, $requestHash]);
243 + $signature = \bin2hex(\base64_decode($this->retrySignBlob(fn() => $credentials->signBlob($stringToSign, ['forceOpenssl' => $options['forceOpenssl']])) ?? ''));
239 244 // Construct the modified resource name. If a custom hostname is provided,
240 245 // this will remove the bucket name from the resource.
241 246 $resource = $this->normalizeUriPath($options['bucketBoundHostname'], $resource);
242 247 $scheme = $this->chooseScheme($options['scheme'], $options['bucketBoundHostname'], $options['virtualHostedStyle']);
243 - return sprintf('%s://%s%s?%s&X-Goog-Signature=%s', $scheme, $options['bucketBoundHostname'], $resource, $canonicalQueryString, $signature);
248 + return \sprintf('%s://%s%s?%s&X-Goog-Signature=%s', $scheme, $options['bucketBoundHostname'], $resource, $canonicalQueryString, $signature);
244 249 }
245 250 /**
246 251 * Create an HTTP POST policy using v4 signing.
247 252 *
@@ -261,22 +266,22 @@
261 266 {
262 267 list($credentials, $options) = $this->getSigningCredentials($connection, $options);
263 268 $expires = $this->normalizeExpiration($expires);
264 269 list($resource, $bucket, $object) = $this->normalizeResource($resource, \false);
265 - $object = trim($object, '/');
270 + $object = \trim($object, '/');
266 271 $options = $this->normalizeOptions($options) + ['fields' => [], 'conditions' => [], 'successActionRedirect' => null, 'successActionStatus' => null];
267 272 $time = $options['timestamp'];
268 273 $requestTimestamp = $time->format(self::V4_TIMESTAMP_FORMAT);
269 274 $requestDatestamp = $time->format(self::V4_DATESTAMP_FORMAT);
270 275 $expiration = \DateTimeImmutable::createFromFormat('U', (string) $expires);
271 - $expirationTimestamp = str_replace('+00:00', 'Z', $expiration->format(\DateTime::RFC3339));
276 + $expirationTimestamp = \str_replace('+00:00', 'Z', $expiration->format(\DateTime::RFC3339));
272 277 $clientEmail = $credentials->getClientName();
273 - $credentialScope = sprintf('%s/auto/storage/goog4_request', $requestDatestamp);
274 - $credential = sprintf('%s/%s', $clientEmail, $credentialScope);
278 + $credentialScope = \sprintf('%s/auto/storage/goog4_request', $requestDatestamp);
279 + $credential = \sprintf('%s/%s', $clientEmail, $credentialScope);
275 280 if ($options['virtualHostedStyle']) {
276 - $options['bucketBoundHostname'] = sprintf('%s.storage.googleapis.com', $bucket);
281 + $options['bucketBoundHostname'] = \sprintf('%s.storage.googleapis.com', $bucket);
277 282 }
278 - $fields = array_merge($options['fields'], ['key' => $object, 'x-goog-algorithm' => self::V4_ALGO_NAME, 'x-goog-credential' => $credential, 'x-goog-date' => $requestTimestamp]);
283 + $fields = \array_merge($options['fields'], ['key' => $object, 'x-goog-algorithm' => self::V4_ALGO_NAME, 'x-goog-credential' => $credential, 'x-goog-date' => $requestTimestamp]);
279 284 $conditions = $options['conditions'];
280 285 foreach ($options['fields'] as $key => $value) {
281 286 $conditions[] = [$key => $value];
282 287 }
@@ -284,13 +289,13 @@
284 289 $key = $key;
285 290 $value = $value;
286 291 $conditions[$key] = $value;
287 292 }
288 - $conditions = array_merge($conditions, [['bucket' => $bucket], ['key' => $object], ['x-goog-date' => $requestTimestamp], ['x-goog-credential' => $credential], ['x-goog-algorithm' => self::V4_ALGO_NAME]]);
293 + $conditions = \array_merge($conditions, [['bucket' => $bucket], ['key' => $object], ['x-goog-date' => $requestTimestamp], ['x-goog-credential' => $credential], ['x-goog-algorithm' => self::V4_ALGO_NAME]]);
289 294 $policy = ['conditions' => $conditions, 'expiration' => $expirationTimestamp];
290 - $json = str_replace('\\\\u', '\u', json_encode($policy, \JSON_UNESCAPED_SLASHES));
291 - $stringToSign = base64_encode($json);
292 - $signature = bin2hex(base64_decode($credentials->signBlob($stringToSign, ['forceOpenssl' => $options['forceOpenssl']])));
295 + $json = \str_replace('\\\\u', '\\u', \json_encode($policy, \JSON_UNESCAPED_SLASHES));
296 + $stringToSign = \base64_encode($json);
297 + $signature = \bin2hex(\base64_decode($credentials->signBlob($stringToSign, ['forceOpenssl' => $options['forceOpenssl']])));
293 298 $fields['x-goog-signature'] = $signature;
294 299 $fields['policy'] = $stringToSign;
295 300 // Construct the modified resource name. If a custom hostname is provided,
296 301 // this will remove the bucket name from the resource.
@@ -295,9 +300,9 @@
295 300 // Construct the modified resource name. If a custom hostname is provided,
296 301 // this will remove the bucket name from the resource.
297 302 $resource = $this->normalizeUriPath($options['bucketBoundHostname'], '/' . $bucket, \true);
298 303 $scheme = $this->chooseScheme($options['scheme'], $options['bucketBoundHostname'], $options['virtualHostedStyle']);
299 - return ['url' => sprintf('%s://%s%s', $scheme, $options['bucketBoundHostname'], $resource), 'fields' => $fields];
304 + return ['url' => \sprintf('%s://%s%s', $scheme, $options['bucketBoundHostname'], $resource), 'fields' => $fields];
300 305 }
301 306 /**
302 307 * Creates a canonical request hash for a V4 Signed URL.
303 308 *
@@ -309,10 +314,10 @@
309 314 * @return string
310 315 */
311 316 private function createV4CanonicalRequest(array $canonicalRequest)
312 317 {
313 - $canonicalRequestString = implode("\n", $canonicalRequest);
314 - return bin2hex(hash('sha256', $canonicalRequestString, \true));
318 + $canonicalRequestString = \implode("\n", $canonicalRequest);
319 + return \bin2hex(\hash('sha256', $canonicalRequestString, \true));
315 320 }
316 321 /**
317 322 * Creates a canonical request for a V2 Signed URL.
318 323 *
@@ -324,9 +329,9 @@
324 329 * @return string
325 330 */
326 331 private function createV2CanonicalRequest(array $canonicalRequest)
327 332 {
328 - return implode("\n", $canonicalRequest);
333 + return \implode("\n", $canonicalRequest);
329 334 }
330 335 /**
331 336 * Choose the correct URL scheme.
332 337 *
@@ -374,9 +379,9 @@
374 379 if ($expires instanceof Timestamp) {
375 380 $seconds = $expires->get()->format('U');
376 381 } elseif ($expires instanceof \DateTimeInterface) {
377 382 $seconds = $expires->format('U');
378 - } elseif (is_numeric($expires)) {
383 + } elseif (\is_numeric($expires)) {
379 384 $seconds = (int) $expires;
380 385 } else {
381 386 throw new \InvalidArgumentException('Invalid expiration.');
382 387 }
@@ -392,18 +397,18 @@
392 397 * name, and index 2 is the object name, relative to the bucket.
393 398 */
394 399 private function normalizeResource($resource, $urlencode = \true)
395 400 {
396 - $pieces = explode('/', trim($resource, '/'));
401 + $pieces = \explode('/', \trim($resource, '/'));
397 402 if ($urlencode) {
398 - array_walk($pieces, function (&$piece) {
399 - $piece = rawurlencode($piece);
403 + \array_walk($pieces, function (&$piece) {
404 + $piece = \rawurlencode($piece);
400 405 });
401 406 }
402 407 $bucket = $pieces[0];
403 408 $relative = $pieces;
404 - array_shift($relative);
405 - return ['/' . implode('/', $pieces), $bucket, '/' . implode('/', $relative)];
409 + \array_shift($relative);
410 + return ['/' . \implode('/', $pieces), $bucket, '/' . \implode('/', $relative)];
406 411 }
407 412 /**
408 413 * Fixes the user input options, filters and validates data.
409 414 *
@@ -423,8 +428,9 @@
423 428 'forceOpenssl' => \false,
424 429 'headers' => [],
425 430 'keyFile' => null,
426 431 'keyFilePath' => null,
432 + 'credentialsFetcher' => null,
427 433 'method' => 'GET',
428 434 'queryParams' => [],
429 435 'responseDisposition' => null,
430 436 'responseType' => null,
@@ -434,10 +440,10 @@
434 440 'timestamp' => null,
435 441 'virtualHostedStyle' => \false,
436 442 ];
437 443 $allowedMethods = ['GET', 'PUT', 'POST', 'DELETE'];
438 - $options['method'] = strtoupper($options['method']);
439 - if (!in_array($options['method'], $allowedMethods)) {
444 + $options['method'] = \strtoupper($options['method']);
445 + if (!\in_array($options['method'], $allowedMethods)) {
440 446 throw new \InvalidArgumentException('$options.method must be one of `GET`, `PUT` or `DELETE`.');
441 447 }
442 448 if ($options['method'] === 'POST' && !$options['allowPost']) {
443 449 throw new \InvalidArgumentException('Invalid method. To create an upload URI, use StorageObject::signedUploadUrl().');
@@ -446,19 +452,19 @@
446 452 if ($options['cname'] && $options['bucketBoundHostname'] === self::DEFAULT_DOWNLOAD_HOST) {
447 453 $options['bucketBoundHostname'] = $options['cname'];
448 454 }
449 455 // strip protocol from hostname.
450 - $hostnameParts = explode('//', $options['bucketBoundHostname']);
451 - if (count($hostnameParts) > 1) {
456 + $hostnameParts = \explode('//', $options['bucketBoundHostname']);
457 + if (\count($hostnameParts) > 1) {
452 458 $options['bucketBoundHostname'] = $hostnameParts[1];
453 459 }
454 - $options['bucketBoundHostname'] = trim($options['bucketBoundHostname'], '/');
460 + $options['bucketBoundHostname'] = \trim($options['bucketBoundHostname'], '/');
455 461 // If a timestamp is provided, use it in place of `now` for v4 URLs only..
456 462 // This option exists for testing purposes, and should not generally be provided by users.
457 463 if ($options['timestamp']) {
458 464 if (!$options['timestamp'] instanceof \DateTimeInterface) {
459 - if (!is_string($options['timestamp'])) {
460 - throw new \InvalidArgumentException('User-provided timestamps must be a string or instance of `\DateTimeInterface`.');
465 + if (!\is_string($options['timestamp'])) {
466 + throw new \InvalidArgumentException('User-provided timestamps must be a string or instance of `\\DateTimeInterface`.');
461 467 }
462 468 $options['timestamp'] = \DateTimeImmutable::createFromFormat(\DateTime::RFC3339, $options['timestamp'], new \DateTimeZone('UTC'));
463 469 if (!$options['timestamp']) {
464 470 throw new \InvalidArgumentException('Given timestamp string is in an invalid format. Provide timestamp formatted as follows: `' . \DateTime::RFC3339 . '`. Note that timestamps MUST be in UTC.');
@@ -483,22 +489,22 @@
483 489 private function normalizeHeaders(array $headers)
484 490 {
485 491 $out = [];
486 492 foreach ($headers as $name => $value) {
487 - $name = strtolower(trim($name));
493 + $name = \strtolower(\trim($name));
488 494 // collapse arrays of values into a comma-separated list.
489 - if (!is_array($value)) {
495 + if (!\is_array($value)) {
490 496 $value = [$value];
491 497 }
492 498 foreach ($value as &$headerValue) {
493 499 // strip trailing and leading spaces.
494 - $headerValue = trim($headerValue);
500 + $headerValue = \trim($headerValue);
495 501 // replace newlines with empty strings.
496 - $headerValue = str_replace(\PHP_EOL, '', $headerValue);
502 + $headerValue = \str_replace(\PHP_EOL, '', $headerValue);
497 503 // collapse multiple whitespace chars to a single space.
498 - $headerValue = preg_replace('/[\s]+/', ' ', $headerValue);
504 + $headerValue = \preg_replace('/[\\s]+/', ' ', $headerValue);
499 505 }
500 - $out[$name] = implode(', ', $value);
506 + $out[$name] = \implode(', ', $value);
501 507 }
502 508 return $out;
503 509 }
504 510 /**
@@ -513,18 +519,18 @@
513 519 */
514 520 private function normalizeUriPath($bucketBoundHostname, $resource, $withTrailingSlash = \false)
515 521 {
516 522 if ($bucketBoundHostname !== self::DEFAULT_DOWNLOAD_HOST) {
517 - $resourceParts = explode('/', trim($resource, '/'));
518 - array_shift($resourceParts);
523 + $resourceParts = \explode('/', \trim($resource, '/'));
524 + \array_shift($resourceParts);
519 525 // Resource is a Bucket.
520 526 if (empty($resourceParts)) {
521 527 $resource = '/';
522 528 } else {
523 - $resource = '/' . implode('/', $resourceParts);
529 + $resource = '/' . \implode('/', $resourceParts);
524 530 }
525 531 }
526 - $resource = rtrim($resource, '/');
532 + $resource = \rtrim($resource, '/');
527 533 return $withTrailingSlash ? $resource . '/' : $resource;
528 534 }
529 535 /**
530 536 * Normalize the resource provided to the canonical request string.
@@ -538,11 +544,11 @@
538 544 {
539 545 if ($bucketBoundHostname === self::DEFAULT_DOWNLOAD_HOST && !$virtualHostedStyle) {
540 546 return $resource;
541 547 }
542 - $pieces = explode('/', trim($resource, '/'));
543 - array_shift($pieces);
544 - return '/' . implode('/', $pieces);
548 + $pieces = \explode('/', \trim($resource, '/'));
549 + \array_shift($pieces);
550 + return '/' . \implode('/', $pieces);
545 551 }
546 552 /**
547 553 * Get the credentials for use with signing.
548 554 *
@@ -558,12 +564,12 @@
558 564 private function getSigningCredentials(ConnectionInterface $connection, array $options)
559 565 {
560 566 $keyFilePath = $options['keyFilePath'] ?? null;
561 567 if ($keyFilePath) {
562 - if (!file_exists($keyFilePath)) {
563 - throw new \InvalidArgumentException(sprintf('Keyfile path %s does not exist.', $keyFilePath));
568 + if (!\file_exists($keyFilePath)) {
569 + throw new \InvalidArgumentException(\sprintf('Keyfile path %s does not exist.', $keyFilePath));
564 570 }
565 - $options['keyFile'] = self::jsonDecode(file_get_contents($keyFilePath), \true);
571 + $options['keyFile'] = self::jsonDecode(\file_get_contents($keyFilePath), \true);
566 572 }
567 573 $rw = $connection->requestWrapper();
568 574 $keyFile = $options['keyFile'] ?? null;
569 575 if ($keyFile) {
@@ -568,17 +574,19 @@
568 574 $keyFile = $options['keyFile'] ?? null;
569 575 if ($keyFile) {
570 576 $scopes = $options['scopes'] ?? $rw->scopes();
571 577 $credentials = CredentialsLoader::makeCredentials($scopes, $keyFile);
578 + } elseif (isset($options['credentialsFetcher'])) {
579 + $credentials = $options['credentialsFetcher'];
572 580 } else {
573 581 $credentials = $rw->getCredentialsFetcher();
574 582 }
575 583 //@codeCoverageIgnoreStart
576 584 if (!$credentials instanceof SignBlobInterface) {
577 - throw new \RuntimeException(sprintf('Credentials object is of type `%s` and is not valid for signing.', get_class($credentials)));
585 + throw new \RuntimeException(\sprintf('Credentials object is of type `%s` and is not valid for signing.', \get_class($credentials)));
578 586 }
579 587 //@codeCoverageIgnoreEnd
580 - unset($options['keyFilePath'], $options['keyFile'], $options['scopes']);
588 + unset($options['keyFilePath'], $options['keyFile'], $options['credentialsFetcher'], $options['scopes']);
581 589 return [$credentials, $options];
582 590 }
583 591 /**
584 592 * Add parameters common to all signed URL versions.
@@ -616,7 +624,35 @@
616 624 $q = [];
617 625 foreach ($input as $key => $val) {
618 626 $q[] = $key . '=' . $val;
619 627 }
620 - return implode('&', $q);
628 + return \implode('&', $q);
629 + }
630 + /**
631 + * Retry logic for signBlob
632 + *
633 + * @param callable $signBlobFn A callable that perform the actual signBlob operation.
634 + * @param string $resourceName The resource name for logging or retry strategy determination.
635 + * @param array $args Arguments for the operations, include preconditions
636 + * @return string The signature genarated by signBlob.
637 + * @throws ServiceException If non-retryable error occur.
638 + * @throws \RuntimeException If retries are exhausted.
639 + */
640 + private function retrySignBlob(callable $signBlobFn, string $resourceName = 'signBlob', array $args = [])
641 + {
642 + $attempt = 0;
643 + // Generate a retry decider function using the RetryTrait logic.
644 + $retryDecider = $this->getRestRetryFunction($resourceName, 'execute', $args);
645 + while (\true) {
646 + ++$attempt;
647 + try {
648 + // Attempt the operation
649 + return $signBlobFn();
650 + } catch (\Exception $exception) {
651 + if (!$retryDecider($exception, $attempt, self::MAX_RETRIES)) {
652 + // Non-retryable error
653 + throw $exception;
654 + }
655 + }
656 + }
621 657 }
622 658 }