PluginProbe
UpdraftPlus: WP Backup & Migration Plugin / 1.16.43
UpdraftPlus: WP Backup & Migration Plugin v1.16.43
1.26.7 1.26.6 1.26.5 1.26.4 1.26.3 1.9.19 1.9.25 1.9.26 1.9.30 1.9.31 1.9.32 1.9.4 1.9.40 1.9.41 1.9.42 1.9.43 1.9.44 1.9.45 1.9.46 1.9.5 1.9.50 1.9.51 1.9.60 1.9.62 1.9.63 All 371 releases
updraftplus / includes / S3.php

S3.php in UpdraftPlus: WP Backup & Migration Plugin 1.16.43, at includes/S3.php

2,428 lines 86.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * $Id$
4 *
5 * Copyright (c) 2011, Donovan Schönknecht. All rights reserved.
6 * Portions copyright (c) 2012-2018, David Anderson (https://david.dw-perspective.org.uk). All rights reserved.
7 *
8 * Redistribution and use in source and binary forms, with or without
9 * modification, are permitted provided that the following conditions are met:
10 *
11 * - Redistributions of source code must retain the above copyright notice,
12 * this list of conditions and the following disclaimer.
13 * - Redistributions in binary form must reproduce the above copyright
14 * notice, this list of conditions and the following disclaimer in the
15 * documentation and/or other materials provided with the distribution.
16 *
17 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
21 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
22 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
23 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
25 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
26 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27 * POSSIBILITY OF SUCH DAMAGE.
28 *
29 * Amazon S3 is a trademark of Amazon.com, Inc. or its affiliates.
30 */
31
32 /**
33 * Amazon S3 PHP class
34 *
35 * @link http://undesigned.org.za/2007/10/22/amazon-s3-php-class
36 * @version 0.5.0-dev
37 */
38 class UpdraftPlus_S3 {
39 // ACL flags
40 const ACL_PRIVATE = 'private';
41 const ACL_PUBLIC_READ = 'public-read';
42 const ACL_PUBLIC_READ_WRITE = 'public-read-write';
43 const ACL_AUTHENTICATED_READ = 'authenticated-read';
44
45 const STORAGE_CLASS_STANDARD = 'STANDARD';
46
47 private $__accessKey = null; // AWS Access key
48 private $__secretKey = null; // AWS Secret key
49 private $__sslKey = null;
50 private $__session_token = null; //For Vault temporary users
51 private $_serverSideEncryption = false;
52
53 public $endpoint = 's3.amazonaws.com';
54 public $region = 'us-east-1';
55 public $proxy = null;
56
57 // Added to cope with a particular situation where the user had no permission to check the bucket location, which necessitated using DNS-based endpoints.
58 public $use_dns_bucket_name = false;
59
60 public $useSSL = false;
61 public $useSSLValidation = true;
62 public $useExceptions = false;
63
64 // Added at request of a user using a non-default port.
65 public $port = false;
66
67 // SSL CURL SSL options - only needed if you are experiencing problems with your OpenSSL configuration
68 public $sslKey = null;
69 public $sslCert = null;
70 public $sslCACert = null;
71
72 private $__signingKeyPairId = null; // AWS Key Pair ID
73 private $__signingKeyResource = false; // Key resource, freeSigningKey() must be called to clear it from memory
74
75 public $signVer = 'v2';
76
77 /**
78 * Constructor - if you're not using the class statically
79 *
80 * @param string $accessKey Access key
81 * @param string $secretKey Secret key
82 * @param boolean $useSSL Enable SSL
83 * @param boolean $sslCACert SSL Certificate
84 * @param string|null $endpoint Endpoint
85 * @param string $session_token The session token returned by AWS for temporary credentials access
86 * @param string $region Region
87
88 * @throws Exception If cURL extension is not present
89 *
90 * @return self
91 */
92 public function __construct($accessKey = null, $secretKey = null, $useSSL = true, $sslCACert = true, $endpoint = null, $session_token = null, $region = 'us-east-1') {
93 if (null !== $accessKey && null !== $secretKey) {
94 $this->setAuth($accessKey, $secretKey, $session_token);
95 }
96
97 $this->setSSL($useSSL, !empty($sslCACert));
98
99 $this->sslCACert = $sslCACert;
100 if (!empty($endpoint)) {
101 $this->endpoint = $endpoint;
102 }
103
104 $this->region = $region;
105
106 if (!function_exists('curl_init')) {
107 global $updraftplus;
108 $updraftplus->log('The PHP cURL extension must be installed and enabled to use this remote storage method');
109 throw new Exception('The PHP cURL extension must be installed and enabled to use this remote storage method');
110 }
111 }
112
113 /**
114 * Set the service endpoint
115 *
116 * @param string $host Hostname
117 *
118 * @return void
119 */
120 public function setEndpoint($host) {
121 $this->endpoint = $host;
122 }
123
124 /**
125 * Set Server Side Encryption
126 * Example value: 'AES256'. See: https://docs.aws.amazon.com/AmazonS3/latest/dev/SSEUsingPHPSDK.html
127 *
128 * @param string|boolean $sse Server side encryption standard; or false for none
129 * @return void
130 */
131 public function setServerSideEncryption($value) {
132 $this->_serverSideEncryption = $value;
133 }
134
135 /**
136 * Set the service region
137 *
138 * @param string $region Region
139 * @return void
140 */
141 public function setRegion($region) {
142 $this->region = $region;
143 }
144
145 /**
146 * Get the service region
147 * Note: Region calculation will be done in methods/s3.php file
148 *
149 * @return string Region
150 */
151 public function getRegion() {
152 return $this->region;
153 }
154
155 /**
156 * Set the service port
157 *
158 * @param Integer $port Port number
159 *
160 * @return void
161 */
162 public function setPort($port) {
163 $this->port = $port;
164 }
165
166 /**
167 * Set AWS access key and secret key
168 *
169 * @param string $accessKey Access key
170 * @param string $secretKey Secret key
171 *
172 * @return void
173 */
174 public function setAuth($accessKey, $secretKey, $session_token = null) {
175 $this->__accessKey = $accessKey;
176 $this->__secretKey = $secretKey;
177 $this->__session_token = $session_token;
178 }
179
180 /**
181 * Check if AWS keys have been set
182 *
183 * @return boolean
184 */
185 public function hasAuth() {
186 return (null !== $this->__accessKey && null !== $this->__secretKey);
187 }
188
189
190 /**
191 * Set SSL on or off
192 *
193 * @param boolean $enabled SSL enabled
194 * @param boolean $validate SSL certificate validation
195 *
196 * @return void
197 */
198 public function setSSL($enabled, $validate = true) {
199 $this->useSSL = $enabled;
200 $this->useSSLValidation = $validate;
201 }
202
203 /**
204 * Get SSL value. Determines whether use it or not.
205 *
206 * @return bool
207 */
208 public function getuseSSL() {
209 return $this->useSSL;
210 }
211
212 /**
213 * Set SSL client certificates (experimental)
214 *
215 * @param string $sslCert SSL client certificate
216 * @param string $sslKey SSL client key
217 * @param string $sslCACert SSL CA cert (only required if you are having problems with your system CA cert)
218 *
219 * @return void
220 */
221 public function setSSLAuth($sslCert = null, $sslKey = null, $sslCACert = null) {
222 $this->sslCert = $sslCert;
223 $this->sslKey = $sslKey;
224 $this->sslCACert = $sslCACert;
225 }
226
227
228 /**
229 * Set proxy information
230 *
231 * @param string $host Proxy hostname and port (localhost:1234)
232 * @param string $user Proxy username
233 * @param string $pass Proxy password
234 * @param integer $type CURL proxy type
235 *
236 * @return void
237 */
238 public function setProxy($host, $user = null, $pass = null, $type = CURLPROXY_SOCKS5, $port = null) {
239 $this->proxy = array('host' => $host, 'type' => $type, 'user' => $user, 'pass' => $pass, 'port' => $port);
240 }
241
242
243 /**
244 * Set the error mode to exceptions
245 *
246 * @param boolean $enabled Enable exceptions
247 *
248 * @return void
249 */
250 public function setExceptions($enabled = true) {
251 $this->useExceptions = $enabled;
252 }
253
254
255 /**
256 * Set signing key
257 *
258 * @param string $keyPairId AWS Key Pair ID
259 * @param string $signingKey Private Key
260 * @param boolean $isFile Load private key from file, set to false to load string
261 *
262 * @return boolean
263 */
264 public function setSigningKey($keyPairId, $signingKey, $isFile = true) {
265 $this->__signingKeyPairId = $keyPairId;
266 if (($this->__signingKeyResource = openssl_pkey_get_private($isFile ?
267 file_get_contents($signingKey) : $signingKey)) !== false) return true;
268 $this->__triggerError('UpdraftPlus_S3::setSigningKey(): Unable to open load private key: '.$signingKey, __FILE__, __LINE__);
269 return false;
270 }
271
272
273 /**
274 * Free signing key from memory, MUST be called if you are using setSigningKey()
275 *
276 * @return void
277 */
278 public function freeSigningKey() {
279 if (false !== $this->__signingKeyResource) {
280 openssl_free_key($this->__signingKeyResource);
281 }
282 }
283
284 /**
285 * Set Signature Version
286 *
287 * @param string $version
288 * @return void
289 */
290 public function setSignatureVersion($version = 'v2') {
291 $this->signVer = $version;
292 }
293
294 /**
295 * Internal error handler
296 *
297 * @param string $message Error message
298 * @param string $file Filename
299 * @param integer $line Line number
300 * @param integer $code Error code
301 *
302 * @internal Internal error handler
303 * @throws UpdraftPlus_S3Exception
304 *
305 * @return void
306 */
307 private function __triggerError($message, $file, $line, $code = 0) {// phpcs:ignore PHPCompatibility.FunctionNameRestrictions.ReservedFunctionNames.MethodDoubleUnderscore -- Method name "UpdraftPlus_S3Request::__responseHeaderCallback" is discouraged; PHP has reserved all method names with a double underscore prefix for future use.
308 if ($this->useExceptions) {
309 throw new UpdraftPlus_S3Exception($message, $file, $line, $code);
310 } else {
311 trigger_error($message, E_USER_WARNING);
312 }
313 }
314
315
316 /**
317 * Get a list of buckets
318 *
319 * @param boolean $detailed Returns detailed bucket list when true
320 *
321 * @return array | false
322 */
323 public function listBuckets($detailed = false) {
324 $rest = new UpdraftPlus_S3Request('GET', '', '', $this->endpoint, $this->use_dns_bucket_name, $this);
325 $rest = $rest->getResponse();
326 if (false === $rest->error && 200 !== $rest->code) {
327 $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
328 }
329 if (false !== $rest->error) {
330 $this->__triggerError(sprintf("UpdraftPlus_S3::listBuckets(): [%s] %s", $rest->error['code'],
331 $rest->error['message']), __FILE__, __LINE__);
332 return false;
333 }
334 $results = array();
335 if (!isset($rest->body->Buckets)) return $results;
336
337 if ($detailed) {
338 if (isset($rest->body->Owner, $rest->body->Owner->ID, $rest->body->Owner->DisplayName))
339 $results['owner'] = array(
340 'id' => (string)$rest->body->Owner->ID, 'name' => (string)$rest->body->Owner->ID
341 );
342 $results['buckets'] = array();
343 foreach ($rest->body->Buckets->Bucket as $b) {
344 $results['buckets'][] = array(
345 'name' => (string)$b->Name, 'time' => strtotime((string)$b->CreationDate)
346 );
347 }
348 } else {
349 foreach ($rest->body->Buckets->Bucket as $b) $results[] = (string)$b->Name;
350 }
351
352 return $results;
353 }
354
355 public function useDNSBucketName($use = true, $bucket = '') {
356 $this->use_dns_bucket_name = $use;
357 return true;
358 }
359
360 /**
361 * Get contents for a bucket
362 *
363 * If maxKeys is null this method will loop through truncated result sets
364 *
365 * @param string $bucket Bucket name
366 * @param string $prefix Prefix
367 * @param string $marker Marker (last file listed)
368 * @param string $maxKeys Max keys (maximum number of keys to return)
369 * @param string $delimiter Delimiter
370 * @param boolean $returnCommonPrefixes Set to true to return CommonPrefixes
371 *
372 * @return array | false
373 */
374 public function getBucket($bucket, $prefix = null, $marker = null, $maxKeys = null, $delimiter = null, $returnCommonPrefixes = false) {
375 $rest = new UpdraftPlus_S3Request('GET', $bucket, '', $this->endpoint, $this->use_dns_bucket_name, $this);
376 if (0 == $maxKeys) $maxKeys = null;
377 if (!empty($prefix)) $rest->setParameter('prefix', $prefix);
378 if (!empty($marker)) $rest->setParameter('marker', $marker);
379 if (!empty($maxKeys)) $rest->setParameter('max-keys', $maxKeys);
380 if (!empty($delimiter)) $rest->setParameter('delimiter', $delimiter);
381 $response = $rest->getResponse();
382 if (false === $response->error && 200 !== $response->code) {
383 $response->error = array('code' => $response->code, 'message' => 'Unexpected HTTP status');
384 }
385 if (false !== $response->error) {
386 $this->__triggerError(sprintf("UpdraftPlus_S3::getBucket(): [%s] %s",
387 $response->error['code'], $response->error['message']), __FILE__, __LINE__);
388 return false;
389 }
390
391 $results = array();
392
393 $nextMarker = null;
394 if (isset($response->body, $response->body->Contents))
395 foreach ($response->body->Contents as $c) {
396 $results[(string)$c->Key] = array(
397 'name' => (string)$c->Key,
398 'time' => strtotime((string)$c->LastModified),
399 'size' => (int)$c->Size,
400 'hash' => substr((string)$c->ETag, 1, -1)
401 );
402 $nextMarker = (string)$c->Key;
403 }
404
405 if ($returnCommonPrefixes && isset($response->body, $response->body->CommonPrefixes))
406 foreach ($response->body->CommonPrefixes as $c)
407 $results[(string)$c->Prefix] = array('prefix' => (string)$c->Prefix);
408
409 if (isset($response->body, $response->body->IsTruncated) &&
410 (string)$response->body->IsTruncated == 'false') return $results;
411
412 if (isset($response->body, $response->body->NextMarker))
413 $nextMarker = (string)$response->body->NextMarker;
414
415 // Loop through truncated results if maxKeys isn't specified
416 if (null == $maxKeys && null !== $nextMarker && 'true' == (string)$response->body->IsTruncated)
417 do
418 {
419 $rest = new UpdraftPlus_S3Request('GET', $bucket, '', $this->endpoint, $this->use_dns_bucket_name, $this);
420 if (!empty($prefix)) $rest->setParameter('prefix', $prefix);
421 $rest->setParameter('marker', $nextMarker);
422 if (!empty($delimiter)) $rest->setParameter('delimiter', $delimiter);
423
424 if (false == ($response = $rest->getResponse()) || 200 !== $response->code) break;
425
426 if (isset($response->body, $response->body->Contents))
427 foreach ($response->body->Contents as $c)
428 {
429 $results[(string)$c->Key] = array(
430 'name' => (string)$c->Key,
431 'time' => strtotime((string)$c->LastModified),
432 'size' => (int)$c->Size,
433 'hash' => substr((string)$c->ETag, 1, -1)
434 );
435 $nextMarker = (string)$c->Key;
436 }
437
438 if ($returnCommonPrefixes && isset($response->body, $response->body->CommonPrefixes))
439 foreach ($response->body->CommonPrefixes as $c)
440 $results[(string)$c->Prefix] = array('prefix' => (string)$c->Prefix);
441
442 if (isset($response->body, $response->body->NextMarker))
443 $nextMarker = (string)$response->body->NextMarker;
444
445 } while (false !== $response && 'true' == (string)$response->body->IsTruncated);
446
447 return $results;
448 }
449
450
451 /**
452 * Put a bucket
453 *
454 * @param string $bucket Bucket name
455 * @param string ACL_PRIVATE ACL flag
456 * @param mixed $location Set as "EU" to create buckets hosted in Europe
457 * @return boolean
458 */
459 public function putBucket($bucket, $acl = self::ACL_PRIVATE, $location = false) {
460 $rest = new UpdraftPlus_S3Request('PUT', $bucket, '', $this->endpoint, $this->use_dns_bucket_name, $this);
461 $rest->setAmzHeader('x-amz-acl', $acl);
462
463 if (false === $location) $location = $this->getRegion();
464
465 if (false !== $location && 'us-east-1' !== $location) {
466 $dom = new DOMDocument;
467 $createBucketConfiguration = $dom->createElement('CreateBucketConfiguration');
468 $locationConstraint = $dom->createElement('LocationConstraint', $location);
469 $createBucketConfiguration->appendChild($locationConstraint);
470 $dom->appendChild($createBucketConfiguration);
471 $rest->data = $dom->saveXML();
472 $rest->size = strlen($rest->data);
473 $rest->setHeader('Content-Type', 'application/xml');
474 }
475 $rest = $rest->getResponse();
476
477 if (false === $rest->error && 200 !== $rest->code) {
478 $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
479 }
480
481 if (false !== $rest->error) {
482 $this->__triggerError(sprintf("UpdraftPlus_S3::putBucket({$bucket}, {$acl}, {$location}): [%s] %s",
483 $rest->error['code'], $rest->error['message']), __FILE__, __LINE__);
484 return false;
485 }
486 return true;
487 }
488
489
490 /**
491 * Delete an empty bucket
492 *
493 * @param string $bucket Bucket name
494 *
495 * @return boolean
496 */
497 public function deleteBucket($bucket) {
498 $rest = new UpdraftPlus_S3Request('DELETE', $bucket, '', $this->endpoint, $this->use_dns_bucket_name, $this);
499 $rest = $rest->getResponse();
500 if (false === $rest->error && 204 !== $rest->code) {
501 $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
502 }
503
504 if (false !== $rest->error) {
505 $this->__triggerError(sprintf("UpdraftPlus_S3::deleteBucket({$bucket}): [%s] %s",
506 $rest->error['code'], $rest->error['message']), __FILE__, __LINE__);
507 return false;
508 }
509 return true;
510 }
511
512
513 /**
514 * Create input info array for putObject()
515 *
516 * @param string $file Input file
517 * @param mixed $md5sum Use MD5 hash (supply a string if you want to use your own)
518 * @return array | false
519 */
520 public function inputFile($file, $md5sum = true) {
521 if (!file_exists($file) || !is_file($file) || !is_readable($file)) {
522 $this->__triggerError('UpdraftPlus_S3::inputFile(): Unable to open input file: '.$file, __FILE__, __LINE__);
523 return false;
524 }
525 return array('file' => $file, 'size' => filesize($file), 'md5sum' => $md5sum !== false ?
526 (is_string($md5sum) ? $md5sum : base64_encode(md5_file($file, true))) : '', 'sha256sum' => hash_file('sha256', $file));
527 }
528
529
530 /**
531 * Create input array info for putObject() with a resource
532 *
533 * @param string $resource Input resource to read from
534 * @param integer $bufferSize Input byte size
535 * @param string $md5sum MD5 hash to send (optional)
536 * @return array | false
537 */
538 public function inputResource(&$resource, $bufferSize, $md5sum = '') {
539 if (!is_resource($resource) || $bufferSize < 0) {
540 $this->__triggerError('UpdraftPlus_S3::inputResource(): Invalid resource or buffer size', __FILE__, __LINE__);
541 return false;
542 }
543 $input = array('size' => $bufferSize, 'md5sum' => $md5sum);
544 $input['fp'] =& $resource;
545 return $input;
546 }
547
548 /**
549 * Initiate a multi-part upload (http://docs.amazonwebservices.com/AmazonS3/latest/API/mpUploadInitiate.html)
550 *
551 * @param string $bucket Bucket name
552 * @param string $uri Object URI
553 * @param string $acl ACL constant
554 * @param array $metaHeaders Array of x-amz-meta-* headers
555 * @param array $requestHeaders Array of request headers or content type as a string
556 * @param string $storageClass Storage class constant
557 *
558 * @return string | false
559 */
560 public function initiateMultipartUpload ($bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $requestHeaders = array(), $storageClass = self::STORAGE_CLASS_STANDARD) {
561
562 $rest = new UpdraftPlus_S3Request('POST', $bucket, $uri, $this->endpoint, $this->use_dns_bucket_name, $this);
563 $rest->setParameter('uploads','');
564
565 // Custom request headers (Content-Type, Content-Disposition, Content-Encoding)
566 if (is_array($requestHeaders))
567 foreach ($requestHeaders as $h => $v) $rest->setHeader($h, $v);
568
569 // Set storage class
570 if ($storageClass !== self::STORAGE_CLASS_STANDARD) // Storage class
571 $rest->setAmzHeader('x-amz-storage-class', $storageClass);
572
573 // Set ACL headers
574 $rest->setAmzHeader('x-amz-acl', $acl);
575 foreach ($metaHeaders as $h => $v) $rest->setAmzHeader('x-amz-meta-'.$h, $v);
576
577 // Carry out the HTTP operation
578 $rest->getResponse();
579
580 if (false === $rest->response->error && 200 !== $rest->response->code) {
581 $rest->response->error = array('code' => $rest->response->code, 'message' => 'Unexpected HTTP status');
582 }
583
584 if (false !== $rest->response->error) {
585 $this->__triggerError(sprintf("UpdraftPlus_S3::initiateMultipartUpload(): [%s] %s",
586 $rest->response->error['code'], $rest->response->error['message']), __FILE__, __LINE__);
587 return false;
588 } elseif (isset($rest->response->body)) {
589 // DreamObjects already returns a SimpleXMLElement here. Not sure how that works.
590 if (is_a($rest->response->body, 'SimpleXMLElement')) {
591 $body = $rest->response->body;
592 } else {
593 $body = new SimpleXMLElement($rest->response->body);
594 }
595 return (string) $body->UploadId;
596 }
597
598 // It is a programming error if we reach this line
599 return false;
600
601 }
602
603 /**
604 * Upload a part of a multi-part set (http://docs.amazonwebservices.com/AmazonS3/latest/API/mpUploadUploadPart.html)
605 * The chunk is read into memory, so make sure that you have enough (or patch this function to work another way!)
606 *
607 * @param string $bucket Bucket name
608 * @param string $uri Object URI
609 * @param string $uploadId uploadId returned previously from initiateMultipartUpload
610 * @param integer $partNumber sequential part number to upload
611 * @param string $filePath file to upload content from
612 * @param integer $partSize number of bytes in each part (though final part may have fewer) - pass the same value each time (for this particular upload) - default 5Mb (which is Amazon's minimum)
613 * @return string (ETag) | false
614 */
615 public function uploadPart ($bucket, $uri, $uploadId, $filePath, $partNumber, $partSize = 5242880) {
616
617 $rest = new UpdraftPlus_S3Request('PUT', $bucket, $uri, $this->endpoint, $this->use_dns_bucket_name, $this);
618 $rest->setParameter('partNumber', $partNumber);
619 $rest->setParameter('uploadId', $uploadId);
620
621 // Where to begin
622 $fileOffset = ($partNumber - 1 ) * $partSize;
623
624 // Download the smallest of the remaining bytes and the part size
625 $fileBytes = min(filesize($filePath) - $fileOffset, $partSize);
626 if ($fileBytes < 0) $fileBytes = 0;
627
628 $rest->setHeader('Content-Type', 'application/octet-stream');
629 $rest->data = "";
630
631 if ($handle = fopen($filePath, "rb")) {
632 if ($fileOffset >0) fseek($handle, $fileOffset);
633 $bytes_read = 0;
634 while ($fileBytes>0 && $read = fread($handle, max($fileBytes, 131072))) {
635 $fileBytes = $fileBytes - strlen($read);
636 $bytes_read += strlen($read);
637 $rest->data = $rest->data . $read;
638 }
639 fclose($handle);
640 } else {
641 return false;
642 }
643
644 $rest->setHeader('Content-MD5', base64_encode(md5($rest->data, true)));
645 $rest->size = $bytes_read;
646
647 $rest = $rest->getResponse();
648 if (false === $rest->error && 200 !== $rest->code) {
649 $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
650 }
651
652 if (false !== $rest->error) {
653 $this->__triggerError(sprintf("UpdraftPlus_S3::uploadPart(): [%s] %s",
654 $rest->error['code'], $rest->error['message']), __FILE__, __LINE__);
655 return false;
656 }
657 return $rest->headers['hash'];
658 }
659
660 /**
661 * Complete a multi-part upload (http://docs.amazonwebservices.com/AmazonS3/latest/API/mpUploadComplete.html)
662 *
663 * @param string $bucket Bucket name
664 * @param string $uri Object URI
665 * @param string $uploadId uploadId returned previously from initiateMultipartUpload
666 * @param array $parts an ordered list of eTags of previously uploaded parts from uploadPart
667 * @return boolean
668 */
669 public function completeMultipartUpload ($bucket, $uri, $uploadId, $parts) {
670 $rest = new UpdraftPlus_S3Request('POST', $bucket, $uri, $this->endpoint, $this->use_dns_bucket_name, $this);
671 $rest->setParameter('uploadId', $uploadId);
672
673 $xml = "<CompleteMultipartUpload>\n";
674 $partno = 1;
675 foreach ($parts as $etag) {
676 $xml .= "<Part><PartNumber>$partno</PartNumber><ETag>$etag</ETag></Part>\n";
677 $partno++;
678 }
679 $xml .= "</CompleteMultipartUpload>";
680
681 $rest->data = $xml;
682 $rest->size = strlen($rest->data);
683 $rest->setHeader('Content-Type', 'application/xml');
684
685 $rest = $rest->getResponse();
686 if (false === $rest->error && 200 !== $rest->code) {
687 $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
688 }
689
690 if (false !== $rest->error) {
691 // Special case: when the error means "you've already done that". Turn it into success. See in: https://trello.com/c/6jJoiCG5
692 if ('InternalError' == $rest->error['code'] && 'This multipart completion is already in progress' == $rest->error['message']) {
693 return true;
694 }
695 $this->__triggerError(sprintf("UpdraftPlus_S3::completeMultipartUpload(): [%s] %s",
696 $rest->error['code'], $rest->error['message']), __FILE__, __LINE__);
697 return false;
698 }
699 return true;
700
701 }
702
703 /**
704 * Abort a multi-part upload (http://docs.amazonwebservices.com/AmazonS3/latest/API/mpUploadAbort.html)
705 *
706 * @param string $bucket Bucket name
707 * @param string $uri Object URI
708 * @param string $uploadId uploadId returned previously from initiateMultipartUpload
709 * @return boolean
710 */
711 // TODO: From this line
712 public function abortMultipartUpload ($bucket, $uri, $uploadId) {
713 $rest = new UpdraftPlus_S3Request('DELETE', $bucket, $uri, $this->endpoint, $this->use_dns_bucket_name, $this);
714 $rest->setParameter('uploadId', $uploadId);
715 $rest = $rest->getResponse();
716 if (false === $rest->error && 204 !== $rest->code) {
717 $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
718 }
719
720 if (false !== $rest->error) {
721 $this->__triggerError(sprintf("UpdraftPlus_S3::abortMultipartUpload(): [%s] %s",
722 $rest->error['code'], $rest->error['message']), __FILE__, __LINE__);
723 return false;
724 }
725 return true;
726 }
727
728 /**
729 * Put an object
730 *
731 * @param mixed $input Input data
732 * @param string $bucket Bucket name
733 * @param string $uri Object URI
734 * @param string $acl ACL constant
735 * @param array $metaHeaders Array of x-amz-meta-* headers
736 * @param array $requestHeaders Array of request headers or content type as a string
737 * @param string $storageClass Storage class constant
738 *
739 * @return boolean
740 */
741 public function putObject($input, $bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $requestHeaders = array(), $storageClass = self::STORAGE_CLASS_STANDARD) {
742 if ($input === false) return false;
743 $rest = new UpdraftPlus_S3Request('PUT', $bucket, $uri, $this->endpoint, $this->use_dns_bucket_name, $this);
744
745 if (!is_array($input)) $input = array(
746 'data' => $input, 'size' => strlen($input),
747 'md5sum' => base64_encode(md5($input, true)),
748 'sha256sum' => hash('sha256', $input)
749 );
750
751 // Data
752 if (isset($input['fp']))
753 $rest->fp =& $input['fp'];
754 elseif (isset($input['file']) && is_file($input['file']))
755 $rest->fp = @fopen($input['file'], 'rb');
756 elseif (isset($input['data']))
757 $rest->data = $input['data'];
758
759 // Content-Length (required)
760 if (isset($input['size']) && $input['size'] >= 0) {
761 $rest->size = $input['size'];
762 } else {
763 if (isset($input['file']))
764 $rest->size = filesize($input['file']);
765 elseif (isset($input['data']))
766 $rest->size = strlen($input['data']);
767 }
768
769 // Custom request headers (Content-Type, Content-Disposition, Content-Encoding)
770 if (is_array($requestHeaders))
771 foreach ($requestHeaders as $h => $v) $rest->setHeader($h, $v);
772 elseif (is_string($requestHeaders)) // Support for legacy contentType parameter
773 $input['type'] = $requestHeaders;
774
775 // Content-Type
776 if (!isset($input['type'])) {
777 if (isset($requestHeaders['Content-Type']))
778 $input['type'] =& $requestHeaders['Content-Type'];
779 elseif (isset($input['file']))
780 $input['type'] = $this->__getMimeType($input['file']);
781 else
782 $input['type'] = 'application/octet-stream';
783 }
784
785 if ($storageClass !== self::STORAGE_CLASS_STANDARD) // Storage class
786 $rest->setAmzHeader('x-amz-storage-class', $storageClass);
787
788 if (!empty($this->_serverSideEncryption)) {
789 $rest->setAmzHeader('x-amz-server-side-encryption', $this->_serverSideEncryption);
790 }
791 // We need to post with Content-Length and Content-Type, MD5 is optional
792 if ($rest->size >= 0 && (false !== $rest->fp || false !== $rest->data)) {
793 $rest->setHeader('Content-Type', $input['type']);
794 if (isset($input['md5sum'])) $rest->setHeader('Content-MD5', $input['md5sum']);
795
796 if (isset($input['sha256sum'])) $rest->setAmzHeader('x-amz-content-sha256', $input['sha256sum']);
797
798 $rest->setAmzHeader('x-amz-acl', $acl);
799 foreach ($metaHeaders as $h => $v) $rest->setAmzHeader('x-amz-meta-'.$h, $v);
800 $rest->getResponse();
801 } else {
802 $rest->response->error = array('code' => 0, 'message' => 'Missing input parameters');
803 }
804
805 if (false === $rest->response->error && 200 !== $rest->response->code) {
806 $rest->response->error = array('code' => $rest->response->code, 'message' => 'Unexpected HTTP status');
807 }
808
809 if (false !== $rest->response->error) {
810 $this->__triggerError(sprintf("UpdraftPlus_S3::putObject(): [%s] %s",
811 $rest->response->error['code'], $rest->response->error['message']), __FILE__, __LINE__);
812 return false;
813 }
814 return true;
815 }
816
817
818 /**
819 * Put an object from a file (legacy function)
820 *
821 * @param string $file Input file path
822 * @param string $bucket Bucket name
823 * @param string $uri Object URI
824 * @param string $acl ACL constant
825 * @param array $metaHeaders Array of x-amz-meta-* headers
826 * @param string $contentType Content type
827 * @param string $storageClass
828 *
829 * @return boolean
830 */
831 public function putObjectFile($file, $bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $contentType = null, $storageClass = self::STORAGE_CLASS_STANDARD) {
832 return $this->putObject($this->inputFile($file), $bucket, $uri, $acl, $metaHeaders, $contentType, $storageClass);
833 }
834
835
836 /**
837 * Put an object from a string (legacy function)
838 *
839 * @param string $string Input data
840 * @param string $bucket Bucket name
841 * @param string $uri Object URI
842 * @param string $acl ACL constant
843 * @param array $metaHeaders Array of x-amz-meta-* headers
844 * @param string $contentType Content type
845 * @return boolean
846 */
847 public function putObjectString($string, $bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $contentType = 'text/plain') {
848 return $this->putObject($string, $bucket, $uri, $acl, $metaHeaders, $contentType);
849 }
850
851
852 /**
853 * Get an object
854 *
855 * @param string $bucket Bucket name
856 * @param string $uri Object URI
857 * @param mixed $saveTo Filename or resource to write to
858 * @param mixed $resume - if $saveTo is a resource, then this is either false or the value for a Range: header; otherwise, a boolean, indicating whether to resume if possible.
859 * @return mixed
860 */
861 public function getObject($bucket, $uri, $saveTo = false, $resume = false) {
862 $rest = new UpdraftPlus_S3Request('GET', $bucket, $uri, $this->endpoint, $this->use_dns_bucket_name, $this);
863 if (false !== $saveTo) {
864 if (is_resource($saveTo)) {
865 $rest->fp = $saveTo;
866 if (!is_bool($resume)) $rest->setHeader('Range', $resume);
867 } else {
868 if ($resume && file_exists($saveTo)) {
869 if (false !== ($rest->fp = @fopen($saveTo, 'ab'))) {
870 $rest->setHeader('Range', "bytes=".filesize($saveTo).'-');
871 $rest->file = realpath($saveTo);
872 } else {
873 $rest->response->error = array('code' => 0, 'message' => 'Unable to open save file for writing: '.$saveTo);
874 }
875 } else {
876 if (false !== ($rest->fp = @fopen($saveTo, 'wb')))
877 $rest->file = realpath($saveTo);
878 else
879 $rest->response->error = array('code' => 0, 'message' => 'Unable to open save file for writing: '.$saveTo);
880 }
881 }
882 }
883 if (false === $rest->response->error) $rest->getResponse();
884
885 if (false === $rest->response->error && ( !$resume && 200 != $rest->response->code) || ( $resume && 206 != $rest->response->code && 200 != $rest->response->code))
886 $rest->response->error = array('code' => $rest->response->code, 'message' => 'Unexpected HTTP status');
887 if (false !== $rest->response->error) {
888 $this->__triggerError(sprintf("UpdraftPlus_S3::getObject({$bucket}, {$uri}): [%s] %s",
889 $rest->response->error['code'], $rest->response->error['message']), __FILE__, __LINE__);
890 return false;
891 }
892 return $rest->response;
893 }
894
895
896 /**
897 * Get object information
898 *
899 * @param string $bucket Bucket name
900 * @param string $uri Object URI
901 * @param boolean $returnInfo Return response information
902 *
903 * @return mixed | false
904 */
905 public function getObjectInfo($bucket, $uri, $returnInfo = true) {
906 $rest = new UpdraftPlus_S3Request('HEAD', $bucket, $uri, $this->endpoint, $this->use_dns_bucket_name, $this);
907 $rest = $rest->getResponse();
908 if (false === $rest->error && (200 !== $rest->code && 404 !== $rest->code))
909 $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
910 if (false !== $rest->error) {
911 $this->__triggerError(sprintf("UpdraftPlus_S3::getObjectInfo({$bucket}, {$uri}): [%s] %s",
912 $rest->error['code'], $rest->error['message']), __FILE__, __LINE__);
913 return false;
914 }
915 return (200 == $rest->code) ? ($returnInfo ? $rest->headers : true) : false;
916 }
917
918
919 /**
920 * Copy an object
921 *
922 * @param string $bucket Source bucket name
923 * @param string $uri Source object URI
924 * @param string $bucket Destination bucket name
925 * @param string $uri Destination object URI
926 * @param string $acl ACL constant
927 * @param array $metaHeaders Optional array of x-amz-meta-* headers
928 * @param array $requestHeaders Optional array of request headers (content type, disposition, etc.)
929 * @param string $storageClass Storage class constant
930 *
931 * @return mixed | false
932 */
933 public function copyObject($srcBucket, $srcUri, $bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $requestHeaders = array(), $storageClass = self::STORAGE_CLASS_STANDARD) {
934 $rest = new UpdraftPlus_S3Request('PUT', $bucket, $uri, $this->endpoint, $this->use_dns_bucket_name, $this);
935 $rest->setHeader('Content-Length', 0);
936 foreach ($requestHeaders as $h => $v) $rest->setHeader($h, $v);
937 foreach ($metaHeaders as $h => $v) $rest->setAmzHeader('x-amz-meta-'.$h, $v);
938 if (self::STORAGE_CLASS_STANDARD !== $storageClass) // Storage class
939 $rest->setAmzHeader('x-amz-storage-class', $storageClass);
940 $rest->setAmzHeader('x-amz-acl', $acl);
941 $rest->setAmzHeader('x-amz-copy-source', sprintf('/%s/%s', $srcBucket, rawurlencode($srcUri)));
942 if (sizeof($requestHeaders) > 0 || sizeof($metaHeaders) > 0)
943 $rest->setAmzHeader('x-amz-metadata-directive', 'REPLACE');
944
945 $rest = $rest->getResponse();
946 if (false === $rest->error && 200 !== $rest->code) {
947 $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
948 }
949
950 if (false !== $rest->error) {
951 $this->__triggerError(sprintf("UpdraftPlus_S3::copyObject({$srcBucket}, {$srcUri}, {$bucket}, {$uri}): [%s] %s",
952 $rest->error['code'], $rest->error['message']), __FILE__, __LINE__);
953 return false;
954 }
955 return isset($rest->body->LastModified, $rest->body->ETag) ? array(
956 'time' => strtotime((string)$rest->body->LastModified),
957 'hash' => substr((string)$rest->body->ETag, 1, -1)
958 ) : false;
959 }
960
961
962 /**
963 * Set logging for a bucket
964 *
965 * @param string $bucket Bucket name
966 * @param string $targetBucket Target bucket (where logs are stored)
967 * @param string $targetPrefix Log prefix (e,g; domain.com-)
968 *
969 * @return boolean
970 */
971 public function setBucketLogging($bucket, $targetBucket, $targetPrefix = null) {
972 // The S3 log delivery group has to be added to the target bucket's ACP
973 if (null !== $targetBucket && false !== ($acp = $this->getAccessControlPolicy($targetBucket, ''))) {
974 // Only add permissions to the target bucket when they do not exist
975 $aclWriteSet = false;
976 $aclReadSet = false;
977 foreach ($acp['acl'] as $acl)
978 if ('Group' == $acl['type'] && 'http://acs.amazonaws.com/groups/s3/LogDelivery' == $acl['uri']) {
979 if ($acl['permission'] == 'WRITE') $aclWriteSet = true;
980 elseif ($acl['permission'] == 'READ_ACP') $aclReadSet = true;
981 }
982 if (!$aclWriteSet) $acp['acl'][] = array(
983 'type' => 'Group', 'uri' => 'http://acs.amazonaws.com/groups/s3/LogDelivery', 'permission' => 'WRITE'
984 );
985 if (!$aclReadSet) $acp['acl'][] = array(
986 'type' => 'Group', 'uri' => 'http://acs.amazonaws.com/groups/s3/LogDelivery', 'permission' => 'READ_ACP'
987 );
988 if (!$aclReadSet || !$aclWriteSet) $this->setAccessControlPolicy($targetBucket, '', $acp);
989 }
990
991 $dom = new DOMDocument;
992 $bucketLoggingStatus = $dom->createElement('BucketLoggingStatus');
993 $bucketLoggingStatus->setAttribute('xmlns', 'http://s3.amazonaws.com/doc/2006-03-01/');
994 if (null !== $targetBucket) {
995 if (null == $targetPrefix) $targetPrefix = $bucket . '-';
996 $loggingEnabled = $dom->createElement('LoggingEnabled');
997 $loggingEnabled->appendChild($dom->createElement('TargetBucket', $targetBucket));
998 $loggingEnabled->appendChild($dom->createElement('TargetPrefix', $targetPrefix));
999 // TODO: Add TargetGrants?
1000 $bucketLoggingStatus->appendChild($loggingEnabled);
1001 }
1002 $dom->appendChild($bucketLoggingStatus);
1003
1004 $rest = new UpdraftPlus_S3Request('PUT', $bucket, '', $this->endpoint, $this->use_dns_bucket_name, $this);
1005 $rest->setParameter('logging', null);
1006 $rest->data = $dom->saveXML();
1007 $rest->size = strlen($rest->data);
1008 $rest->setHeader('Content-Type', 'application/xml');
1009 $rest = $rest->getResponse();
1010 if (false === $rest->error && 200 !== $rest->code) {
1011 $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
1012 }
1013
1014 if (false !== $rest->error) {
1015 $this->__triggerError(sprintf("UpdraftPlus_S3::setBucketLogging({$bucket}, {$targetBucket}): [%s] %s",
1016 $rest->error['code'], $rest->error['message']), __FILE__, __LINE__);
1017 return false;
1018 }
1019 return true;
1020 }
1021
1022
1023 /**
1024 * Get logging status for a bucket
1025 *
1026 * This will return false if logging is not enabled.
1027 * Note: To enable logging, you also need to grant write access to the log group
1028 *
1029 * @param string $bucket Bucket name
1030 *
1031 * @return array | false
1032 */
1033 public function getBucketLogging($bucket) {
1034 $rest = new UpdraftPlus_S3Request('GET', $bucket, '', $this->endpoint, $this->use_dns_bucket_name, $this);
1035 $rest->setParameter('logging', null);
1036 $rest = $rest->getResponse();
1037 if (false === $rest->error && 200 !== $rest->code) {
1038 $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
1039 }
1040
1041 if (false !== $rest->error) {
1042 $this->__triggerError(sprintf("UpdraftPlus_S3::getBucketLogging({$bucket}): [%s] %s",
1043 $rest->error['code'], $rest->error['message']), __FILE__, __LINE__);
1044 return false;
1045 }
1046 if (!isset($rest->body->LoggingEnabled)) return false; // No logging
1047 return array(
1048 'targetBucket' => (string)$rest->body->LoggingEnabled->TargetBucket,
1049 'targetPrefix' => (string)$rest->body->LoggingEnabled->TargetPrefix,
1050 );
1051 }
1052
1053
1054 /**
1055 * Disable bucket logging
1056 *
1057 * @param string $bucket Bucket name
1058 *
1059 * @return boolean
1060 */
1061 public function disableBucketLogging($bucket) {
1062 return $this->setBucketLogging($bucket, null);
1063 }
1064
1065
1066 /**
1067 * Get a bucket's location
1068 *
1069 * @param string $bucket Bucket name
1070 *
1071 * @return string | false
1072 */
1073 public function getBucketLocation($bucket) {
1074 $rest = new UpdraftPlus_S3Request('GET', $bucket, '', $this->endpoint, $this->use_dns_bucket_name, $this);
1075 $rest->setParameter('location', null);
1076 $rest = $rest->getResponse();
1077 if (false === $rest->error && 200 !== $rest->code) {
1078 $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
1079 }
1080 if (false !== $rest->error) {
1081 $this->__triggerError(sprintf("UpdraftPlus_S3::getBucketLocation({$bucket}): [%s] %s",
1082 $rest->error['code'], $rest->error['message']), __FILE__, __LINE__);
1083 return false;
1084 }
1085
1086 return (isset($rest->body[0]) && (string)$rest->body[0] !== '') ? (string)$rest->body[0] : 'US';
1087 }
1088
1089
1090 /**
1091 * Set object or bucket Access Control Policy
1092 *
1093 * @param string $bucket Bucket name
1094 * @param string $uri Object URI
1095 * @param array $acp Access Control Policy Data (same as the data returned from getAccessControlPolicy)
1096 *
1097 * @return boolean
1098 */
1099 public function setAccessControlPolicy($bucket, $uri = '', $acp = array()) {
1100 $dom = new DOMDocument;
1101 $dom->formatOutput = true;
1102 $accessControlPolicy = $dom->createElement('AccessControlPolicy');
1103 $accessControlList = $dom->createElement('AccessControlList');
1104
1105 // It seems the owner has to be passed along too
1106 $owner = $dom->createElement('Owner');
1107 $owner->appendChild($dom->createElement('ID', $acp['owner']['id']));
1108 $owner->appendChild($dom->createElement('DisplayName', $acp['owner']['name']));
1109 $accessControlPolicy->appendChild($owner);
1110
1111 foreach ($acp['acl'] as $g) {
1112 $grant = $dom->createElement('Grant');
1113 $grantee = $dom->createElement('Grantee');
1114 $grantee->setAttribute('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance');
1115 if (isset($g['id'])) {
1116 // CanonicalUser (DisplayName is omitted)
1117 $grantee->setAttribute('xsi:type', 'CanonicalUser');
1118 $grantee->appendChild($dom->createElement('ID', $g['id']));
1119 } elseif (isset($g['email'])) {
1120 // AmazonCustomerByEmail
1121 $grantee->setAttribute('xsi:type', 'AmazonCustomerByEmail');
1122 $grantee->appendChild($dom->createElement('EmailAddress', $g['email']));
1123 } elseif ('Group' == $g['type']) {
1124 // Group
1125 $grantee->setAttribute('xsi:type', 'Group');
1126 $grantee->appendChild($dom->createElement('URI', $g['uri']));
1127 }
1128 $grant->appendChild($grantee);
1129 $grant->appendChild($dom->createElement('Permission', $g['permission']));
1130 $accessControlList->appendChild($grant);
1131 }
1132
1133 $accessControlPolicy->appendChild($accessControlList);
1134 $dom->appendChild($accessControlPolicy);
1135
1136 $rest = new UpdraftPlus_S3Request('PUT', $bucket, $uri, $this->endpoint, $this->use_dns_bucket_name, $this);
1137 $rest->setParameter('acl', null);
1138 $rest->data = $dom->saveXML();
1139 $rest->size = strlen($rest->data);
1140 $rest->setHeader('Content-Type', 'application/xml');
1141 $rest = $rest->getResponse();
1142 if (false === $rest->error && 200 !== $rest->code) {
1143 $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
1144 }
1145
1146 if (false !== $rest->error) {
1147 $this->__triggerError(sprintf("UpdraftPlus_S3::setAccessControlPolicy({$bucket}, {$uri}): [%s] %s",
1148 $rest->error['code'], $rest->error['message']), __FILE__, __LINE__);
1149 return false;
1150 }
1151 return true;
1152 }
1153
1154
1155 /**
1156 * Get object or bucket Access Control Policy
1157 *
1158 * @param string $bucket Bucket name
1159 * @param string $uri Object URI
1160 * @return mixed | false
1161 */
1162 public function getAccessControlPolicy($bucket, $uri = '') {
1163 $rest = new UpdraftPlus_S3Request('GET', $bucket, $uri, $this->endpoint, $this->use_dns_bucket_name, $this);
1164 $rest->setParameter('acl', null);
1165 $rest = $rest->getResponse();
1166 if (false === $rest->error && 200 !== $rest->code) {
1167 $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
1168 }
1169
1170 if (false !== $rest->error) {
1171 $this->__triggerError(sprintf("UpdraftPlus_S3::getAccessControlPolicy({$bucket}, {$uri}): [%s] %s",
1172 $rest->error['code'], $rest->error['message']), __FILE__, __LINE__);
1173 return false;
1174 }
1175
1176 $acp = array();
1177 if (isset($rest->body->Owner, $rest->body->Owner->ID, $rest->body->Owner->DisplayName))
1178 $acp['owner'] = array(
1179 'id' => (string)$rest->body->Owner->ID, 'name' => (string)$rest->body->Owner->DisplayName
1180 );
1181
1182 if (isset($rest->body->AccessControlList)) {
1183 $acp['acl'] = array();
1184 foreach ($rest->body->AccessControlList->Grant as $grant) {
1185 foreach ($grant->Grantee as $grantee) {
1186 if (isset($grantee->ID, $grantee->DisplayName)) // CanonicalUser
1187 $acp['acl'][] = array(
1188 'type' => 'CanonicalUser',
1189 'id' => (string)$grantee->ID,
1190 'name' => (string)$grantee->DisplayName,
1191 'permission' => (string)$grant->Permission
1192 );
1193 elseif (isset($grantee->EmailAddress)) // AmazonCustomerByEmail
1194 $acp['acl'][] = array(
1195 'type' => 'AmazonCustomerByEmail',
1196 'email' => (string)$grantee->EmailAddress,
1197 'permission' => (string)$grant->Permission
1198 );
1199 elseif (isset($grantee->URI)) // Group
1200 $acp['acl'][] = array(
1201 'type' => 'Group',
1202 'uri' => (string)$grantee->URI,
1203 'permission' => (string)$grant->Permission
1204 );
1205 else continue;
1206 }
1207 }
1208 }
1209 return $acp;
1210 }
1211
1212
1213 /**
1214 * Delete an object
1215 *
1216 * @param string $bucket Bucket name
1217 * @param string $uri Object URI
1218 *
1219 * @return boolean
1220 */
1221 public function deleteObject($bucket, $uri) {
1222 $rest = new UpdraftPlus_S3Request('DELETE', $bucket, $uri, $this->endpoint, $this->use_dns_bucket_name, $this);
1223 $rest = $rest->getResponse();
1224 if (false === $rest->error && 204 !== $rest->code) {
1225 $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
1226 }
1227
1228 if (false !== $rest->error) {
1229 $this->__triggerError(sprintf("UpdraftPlus_S3::deleteObject(): [%s] %s",
1230 $rest->error['code'], $rest->error['message']), __FILE__, __LINE__);
1231 return false;
1232 }
1233 return true;
1234 }
1235
1236
1237 /**
1238 * Get a query string authenticated URL
1239 *
1240 * @param string $bucket Bucket name
1241 * @param string $uri Object URI
1242 * @param integer $lifetime Lifetime in seconds
1243 * @param boolean $hostBucket Use the bucket name as the hostname
1244 * @param boolean $https Use HTTPS ($hostBucket should be false for SSL verification)
1245 *
1246 * @return string
1247 */
1248 public function getAuthenticatedURL($bucket, $uri, $lifetime, $hostBucket = false, $https = false) {
1249 $expires = time() + $lifetime;
1250 $uri = str_replace(array('%2F', '%2B'), array('/', '+'), rawurlencode($uri));
1251 return sprintf(($https ? 'https' : 'http').'://%s/%s?AWSAccessKeyId=%s&Expires=%u&Signature=%s',
1252 // $hostBucket ? $bucket : $bucket.'.s3.amazonaws.com', $uri, $this->__accessKey, $expires,
1253 $hostBucket ? $bucket : 's3.amazonaws.com/'.$bucket, $uri, $this->__accessKey, $expires,
1254 urlencode($this->__getHash("GET\n\n\n{$expires}\n/{$bucket}/{$uri}")));
1255 }
1256
1257
1258 /**
1259 * Get a CloudFront signed policy URL
1260 *
1261 * @param array $policy Policy
1262 *
1263 * @return string
1264 */
1265 public function getSignedPolicyURL($policy) {
1266 $data = json_encode($policy);
1267 $signature = '';
1268 if (!openssl_sign($data, $signature, $this->__signingKeyResource)) return false;
1269
1270 $encoded = str_replace(array('+', '='), array('-', '_', '~'), base64_encode($data));
1271 $signature = str_replace(array('+', '='), array('-', '_', '~'), base64_encode($signature));
1272
1273 $url = $policy['Statement'][0]['Resource'] . '?';
1274 foreach (array('Policy' => $encoded, 'Signature' => $signature, 'Key-Pair-Id' => $this->__signingKeyPairId) as $k => $v)
1275 $url .= $k.'='.str_replace('%2F', '/', rawurlencode($v)).'&';
1276 return substr($url, 0, -1);
1277 }
1278
1279
1280 /**
1281 * Get a CloudFront canned policy URL
1282 *
1283 * @param string $url URL to sign
1284 * @param integer $lifetime URL lifetime
1285 *
1286 * @return string
1287 */
1288 public function getSignedCannedURL($url, $lifetime) {
1289 return $this->getSignedPolicyURL(array(
1290 'Statement' => array(
1291 array('Resource' => $url, 'Condition' => array(
1292 'DateLessThan' => array('AWS:EpochTime' => time() + $lifetime)
1293 ))
1294 )
1295 ));
1296 }
1297
1298
1299 /**
1300 * Get upload POST parameters for form uploads
1301 *
1302 * @param string $bucket Bucket name
1303 * @param string $uriPrefix Object URI prefix
1304 * @param string $acl ACL constant
1305 * @param integer $lifetime Lifetime in seconds
1306 * @param integer $maxFileSize Maximum file size in bytes (default 5MB)
1307 * @param string $successRedirect Redirect URL or 200 / 201 status code
1308 * @param array $amzHeaders Array of x-amz-meta-* headers
1309 * @param array $headers Array of request headers or content type as a string
1310 * @param boolean $flashVars Includes additional "Filename" variable posted by Flash
1311 *
1312 * @return object
1313 */
1314 public function getHttpUploadPostParams($bucket, $uriPrefix = '', $acl = self::ACL_PRIVATE, $lifetime = 3600,
1315 $maxFileSize = 5242880, $successRedirect = "201", $amzHeaders = array(), $headers = array(), $flashVars = false) {
1316 // Create policy object
1317 $policy = new stdClass;
1318 $policy->expiration = gmdate('Y-m-d\TH:i:s\Z', (time() + $lifetime));
1319 $policy->conditions = array();
1320 $obj = new stdClass; $obj->bucket = $bucket; array_push($policy->conditions, $obj);
1321 $obj = new stdClass; $obj->acl = $acl; array_push($policy->conditions, $obj);
1322
1323 $obj = new stdClass; // 200 for non-redirect uploads
1324 if (is_numeric($successRedirect) && in_array((int)$successRedirect, array(200, 201)))
1325 $obj->success_action_status = (string)$successRedirect;
1326 else // URL
1327 $obj->success_action_redirect = $successRedirect;
1328 array_push($policy->conditions, $obj);
1329
1330 if (self::ACL_PUBLIC_READ !== $acl)
1331 array_push($policy->conditions, array('eq', '$acl', $acl));
1332
1333 array_push($policy->conditions, array('starts-with', '$key', $uriPrefix));
1334 if ($flashVars) array_push($policy->conditions, array('starts-with', '$Filename', ''));
1335 foreach (array_keys($headers) as $headerKey)
1336 array_push($policy->conditions, array('starts-with', '$'.$headerKey, ''));
1337 foreach ($amzHeaders as $headerKey => $headerVal) {
1338 $obj = new stdClass;
1339 $obj->{$headerKey} = (string)$headerVal;
1340 array_push($policy->conditions, $obj);
1341 }
1342 array_push($policy->conditions, array('content-length-range', 0, $maxFileSize));
1343 $policy = base64_encode(str_replace('\/', '/', json_encode($policy)));
1344
1345 // Create parameters
1346 $params = new stdClass;
1347 $params->AWSAccessKeyId = $this->__accessKey;
1348 $params->key = $uriPrefix.'${filename}';
1349 $params->acl = $acl;
1350 $params->policy = $policy; unset($policy);
1351 $params->signature = $this->__getHash($params->policy);
1352 if (is_numeric($successRedirect) && in_array((int)$successRedirect, array(200, 201)))
1353 $params->success_action_status = (string)$successRedirect;
1354 else
1355 $params->success_action_redirect = $successRedirect;
1356 foreach ($headers as $headerKey => $headerVal) $params->{$headerKey} = (string)$headerVal;
1357 foreach ($amzHeaders as $headerKey => $headerVal) $params->{$headerKey} = (string)$headerVal;
1358 return $params;
1359 }
1360
1361
1362 /**
1363 * Create a CloudFront distribution
1364 *
1365 * @param string $bucket Bucket name
1366 * @param boolean $enabled Enabled (true/false)
1367 * @param array $cnames Array containing CNAME aliases
1368 * @param string $comment Use the bucket name as the hostname
1369 * @param string $defaultRootObject Default root object
1370 * @param string $originAccessIdentity Origin access identity
1371 * @param array $trustedSigners Array of trusted signers
1372 *
1373 * @return array | false
1374 */
1375 public function createDistribution($bucket, $enabled = true, $cnames = array(), $comment = null, $defaultRootObject = null, $originAccessIdentity = null, $trustedSigners = array()) {
1376 if (!extension_loaded('openssl')) {
1377 $this->__triggerError(sprintf("UpdraftPlus_S3::createDistribution({$bucket}, ".(int)$enabled.", [], '$comment'): %s",
1378 "CloudFront functionality requires SSL"), __FILE__, __LINE__);
1379 return false;
1380 }
1381 $useSSL = $this->useSSL;
1382
1383 $this->useSSL = true; // CloudFront requires SSL
1384 $rest = new UpdraftPlus_S3Request('POST', '', '2010-11-01/distribution', 'cloudfront.amazonaws.com');
1385 $rest->data = $this->__getCloudFrontDistributionConfigXML(
1386 $bucket.'.s3.amazonaws.com',
1387 $enabled,
1388 (string)$comment,
1389 (string)microtime(true),
1390 $cnames,
1391 $defaultRootObject,
1392 $originAccessIdentity,
1393 $trustedSigners
1394 );
1395
1396 $rest->size = strlen($rest->data);
1397 $rest->setHeader('Content-Type', 'application/xml');
1398 $rest = $this->__getCloudFrontResponse($rest);
1399
1400 $this->useSSL = $useSSL;
1401
1402 if (false === $rest->error && 201 !== $rest->code) {
1403 $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
1404 }
1405
1406 if (false !== $rest->error) {
1407 $this->__triggerError(sprintf("UpdraftPlus_S3::createDistribution({$bucket}, ".(int)$enabled.", [], '$comment'): [%s] %s",
1408 $rest->error['code'], $rest->error['message']), __FILE__, __LINE__);
1409 return false;
1410 } elseif ($rest->body instanceof SimpleXMLElement) {
1411 return $this->__parseCloudFrontDistributionConfig($rest->body);
1412 }
1413 return false;
1414 }
1415
1416
1417 /**
1418 * Get CloudFront distribution info
1419 *
1420 * @param string $distributionId Distribution ID from listDistributions()
1421 * @return array | false
1422 */
1423 public function getDistribution($distributionId) {
1424 if (!extension_loaded('openssl')) {
1425 $this->__triggerError(sprintf("UpdraftPlus_S3::getDistribution($distributionId): %s",
1426 "CloudFront functionality requires SSL"), __FILE__, __LINE__);
1427 return false;
1428 }
1429 $useSSL = $this->useSSL;
1430
1431 $this->useSSL = true; // CloudFront requires SSL
1432 $rest = new UpdraftPlus_S3Request('GET', '', '2010-11-01/distribution/'.$distributionId, 'cloudfront.amazonaws.com');
1433 $rest = $this->__getCloudFrontResponse($rest);
1434
1435 $this->useSSL = $useSSL;
1436
1437 if (false === $rest->error && 200 !== $rest->code) {
1438 $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
1439 }
1440
1441 if (false !== $rest->error) {
1442 $this->__triggerError(sprintf("UpdraftPlus_S3::getDistribution($distributionId): [%s] %s",
1443 $rest->error['code'], $rest->error['message']), __FILE__, __LINE__);
1444 return false;
1445 } elseif ($rest->body instanceof SimpleXMLElement) {
1446 $dist = $this->__parseCloudFrontDistributionConfig($rest->body);
1447 $dist['hash'] = $rest->headers['hash'];
1448 $dist['id'] = $distributionId;
1449 return $dist;
1450 }
1451 return false;
1452 }
1453
1454
1455 /**
1456 * Update a CloudFront distribution
1457 *
1458 * @param array $dist Distribution array info identical to output of getDistribution()
1459 *
1460 * @return array | false
1461 */
1462 public function updateDistribution($dist) {
1463 if (!extension_loaded('openssl')) {
1464 $this->__triggerError(sprintf("UpdraftPlus_S3::updateDistribution({$dist['id']}): %s",
1465 "CloudFront functionality requires SSL"), __FILE__, __LINE__);
1466 return false;
1467 }
1468
1469 $useSSL = $this->useSSL;
1470
1471 $this->useSSL = true; // CloudFront requires SSL
1472 $rest = new UpdraftPlus_S3Request('PUT', '', '2010-11-01/distribution/'.$dist['id'].'/config', 'cloudfront.amazonaws.com');
1473 $rest->data = $this->__getCloudFrontDistributionConfigXML(
1474 $dist['origin'],
1475 $dist['enabled'],
1476 $dist['comment'],
1477 $dist['callerReference'],
1478 $dist['cnames'],
1479 $dist['defaultRootObject'],
1480 $dist['originAccessIdentity'],
1481 $dist['trustedSigners']
1482 );
1483
1484 $rest->size = strlen($rest->data);
1485 $rest->setHeader('If-Match', $dist['hash']);
1486 $rest = $this->__getCloudFrontResponse($rest);
1487
1488 $this->useSSL = $useSSL;
1489
1490 if (false === $rest->error && 200 !== $rest->code) {
1491 $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
1492 }
1493
1494 if (false !== $rest->error) {
1495 $this->__triggerError(sprintf("UpdraftPlus_S3::updateDistribution({$dist['id']}): [%s] %s",
1496 $rest->error['code'], $rest->error['message']), __FILE__, __LINE__);
1497 return false;
1498 } else {
1499 $dist = $this->__parseCloudFrontDistributionConfig($rest->body);
1500 $dist['hash'] = $rest->headers['hash'];
1501 return $dist;
1502 }
1503 return false;
1504 }
1505
1506
1507 /**
1508 * Delete a CloudFront distribution
1509 *
1510 * @param array $dist Distribution array info identical to output of getDistribution()
1511 *
1512 * @return boolean
1513 */
1514 public function deleteDistribution($dist) {
1515 if (!extension_loaded('openssl')) {
1516 $this->__triggerError(sprintf("UpdraftPlus_S3::deleteDistribution({$dist['id']}): %s",
1517 "CloudFront functionality requires SSL"), __FILE__, __LINE__);
1518 return false;
1519 }
1520
1521 $useSSL = $this->useSSL;
1522
1523 $this->useSSL = true; // CloudFront requires SSL
1524 $rest = new UpdraftPlus_S3Request('DELETE', '', '2008-06-30/distribution/'.$dist['id'], 'cloudfront.amazonaws.com');
1525 $rest->setHeader('If-Match', $dist['hash']);
1526 $rest = $this->__getCloudFrontResponse($rest);
1527
1528 $this->useSSL = $useSSL;
1529
1530 if (false === $rest->error && 204 !== $rest->code) {
1531 $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
1532 }
1533
1534 if (false !== $rest->error) {
1535 $this->__triggerError(sprintf("UpdraftPlus_S3::deleteDistribution({$dist['id']}): [%s] %s",
1536 $rest->error['code'], $rest->error['message']), __FILE__, __LINE__);
1537 return false;
1538 }
1539 return true;
1540 }
1541
1542
1543 /**
1544 * Get a list of CloudFront distributions
1545 *
1546 * @return array
1547 */
1548 public function listDistributions() {
1549 if (!extension_loaded('openssl')) {
1550 $this->__triggerError(sprintf("UpdraftPlus_S3::listDistributions(): [%s] %s",
1551 "CloudFront functionality requires SSL"), __FILE__, __LINE__);
1552 return false;
1553 }
1554
1555 $useSSL = $this->useSSL;
1556 $this->useSSL = true; // CloudFront requires SSL
1557 $rest = new UpdraftPlus_S3Request('GET', '', '2010-11-01/distribution', 'cloudfront.amazonaws.com');
1558 $rest = $this->__getCloudFrontResponse($rest);
1559 $this->useSSL = $useSSL;
1560
1561 if (false === $rest->error && 200 !== $rest->code) {
1562 $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
1563 }
1564
1565 if (false !== $rest->error) {
1566 $this->__triggerError(sprintf("UpdraftPlus_S3::listDistributions(): [%s] %s",
1567 $rest->error['code'], $rest->error['message']), __FILE__, __LINE__);
1568 return false;
1569 } elseif ($rest->body instanceof SimpleXMLElement && isset($rest->body->DistributionSummary)) {
1570 $list = array();
1571 if (isset($rest->body->Marker, $rest->body->MaxItems, $rest->body->IsTruncated)) {
1572 //$info['marker'] = (string)$rest->body->Marker;
1573 //$info['maxItems'] = (int)$rest->body->MaxItems;
1574 //$info['isTruncated'] = (string)$rest->body->IsTruncated == 'true' ? true : false;
1575 }
1576 foreach ($rest->body->DistributionSummary as $summary)
1577 $list[(string)$summary->Id] = $this->__parseCloudFrontDistributionConfig($summary);
1578
1579 return $list;
1580 }
1581 return array();
1582 }
1583
1584 /**
1585 * List CloudFront Origin Access Identities
1586 *
1587 * @return array
1588 */
1589 public function listOriginAccessIdentities() {
1590 if (!extension_loaded('openssl')) {
1591 $this->__triggerError(sprintf("UpdraftPlus_S3::listOriginAccessIdentities(): [%s] %s",
1592 "CloudFront functionality requires SSL"), __FILE__, __LINE__);
1593 return false;
1594 }
1595
1596 $useSSL = $this->useSSL;
1597 $this->useSSL = true; // CloudFront requires SSL
1598 $rest = new UpdraftPlus_S3Request('GET', '', '2010-11-01/origin-access-identity/cloudfront', 'cloudfront.amazonaws.com');
1599 $rest = $this->__getCloudFrontResponse($rest);
1600 $this->useSSL = $useSSL;
1601
1602 if (false === $rest->error && 200 !== $rest->code) {
1603 $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
1604 }
1605
1606 if (false !== $rest->error) {
1607 trigger_error(sprintf("UpdraftPlus_S3::listOriginAccessIdentities(): [%s] %s",
1608 $rest->error['code'], $rest->error['message']), E_USER_WARNING);
1609 return false;
1610 }
1611
1612 if (isset($rest->body->CloudFrontOriginAccessIdentitySummary)) {
1613 $identities = array();
1614 foreach ($rest->body->CloudFrontOriginAccessIdentitySummary as $identity)
1615 if (isset($identity->S3CanonicalUserId))
1616 $identities[(string)$identity->Id] = array('id' => (string)$identity->Id, 's3CanonicalUserId' => (string)$identity->S3CanonicalUserId);
1617 return $identities;
1618 }
1619 return false;
1620 }
1621
1622
1623 /**
1624 * Invalidate objects in a CloudFront distribution
1625 *
1626 * Thanks to Martin Lindkvist for $this->s3->invalidateDistribution()
1627 *
1628 * @param string $distributionId Distribution ID from listDistributions()
1629 * @param array $paths Array of object paths to invalidate
1630 *
1631 * @return boolean
1632 */
1633 public function invalidateDistribution($distributionId, $paths) {
1634 if (!extension_loaded('openssl')) {
1635 $this->__triggerError(sprintf("UpdraftPlus_S3::invalidateDistribution(): [%s] %s",
1636 "CloudFront functionality requires SSL"), __FILE__, __LINE__);
1637 return false;
1638 }
1639
1640 $useSSL = $this->useSSL;
1641 $this->useSSL = true; // CloudFront requires SSL
1642 $rest = new UpdraftPlus_S3Request('POST', '', '2010-08-01/distribution/'.$distributionId.'/invalidation', 'cloudfront.amazonaws.com');
1643 $rest->data = $this->__getCloudFrontInvalidationBatchXML($paths, (string)microtime(true));
1644 $rest->size = strlen($rest->data);
1645 $rest = $this->__getCloudFrontResponse($rest);
1646 $this->useSSL = $useSSL;
1647
1648 if (false === $rest->error && 201 !== $rest->code) {
1649 $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
1650 }
1651
1652 if (false !== $rest->error) {
1653 trigger_error(sprintf("UpdraftPlus_S3::invalidateDistribution('{$distributionId}',{$paths}): [%s] %s",
1654 $rest->error['code'], $rest->error['message']), E_USER_WARNING);
1655 return false;
1656 }
1657 return true;
1658 }
1659
1660
1661 /**
1662 * Get a InvalidationBatch DOMDocument
1663 *
1664 * @internal Used to create XML in invalidateDistribution()
1665 *
1666 * @param array $paths Paths to objects to invalidateDistribution
1667 * @param string $callerReference
1668 *
1669 * @return string
1670 */
1671 private function __getCloudFrontInvalidationBatchXML($paths, $callerReference = '0') {// phpcs:ignore PHPCompatibility.FunctionNameRestrictions.ReservedFunctionNames.MethodDoubleUnderscore -- Method name "UpdraftPlus_S3Request::__responseHeaderCallback" is discouraged; PHP has reserved all method names with a double underscore prefix for future use.
1672 $dom = new DOMDocument('1.0', 'UTF-8');
1673 $dom->formatOutput = true;
1674 $invalidationBatch = $dom->createElement('InvalidationBatch');
1675 foreach ($paths as $path)
1676 $invalidationBatch->appendChild($dom->createElement('Path', $path));
1677
1678 $invalidationBatch->appendChild($dom->createElement('CallerReference', $callerReference));
1679 $dom->appendChild($invalidationBatch);
1680 return $dom->saveXML();
1681 }
1682
1683
1684 /**
1685 * List your invalidation batches for invalidateDistribution() in a CloudFront distribution
1686 *
1687 * http://docs.amazonwebservices.com/AmazonCloudFront/latest/APIReference/ListInvalidation.html
1688 * returned array looks like this:
1689 * Array
1690 * (
1691 * [I31TWB0CN9V6XD] => InProgress
1692 * [IT3TFE31M0IHZ] => Completed
1693 * [I12HK7MPO1UQDA] => Completed
1694 * [I1IA7R6JKTC3L2] => Completed
1695 * )
1696 *
1697 * @param string $distributionId Distribution ID from listDistributions()
1698 *
1699 * @return array
1700 */
1701 public function getDistributionInvalidationList($distributionId) {
1702 if (!extension_loaded('openssl')) {
1703 $this->__triggerError(sprintf("UpdraftPlus_S3::getDistributionInvalidationList(): [%s] %s",
1704 "CloudFront functionality requires SSL"), __FILE__, __LINE__);
1705 return false;
1706 }
1707
1708 $useSSL = $this->useSSL;
1709 $this->useSSL = true; // CloudFront requires SSL
1710 $rest = new UpdraftPlus_S3Request('GET', '', '2010-11-01/distribution/'.$distributionId.'/invalidation', 'cloudfront.amazonaws.com');
1711 $rest = $this->__getCloudFrontResponse($rest);
1712 $this->useSSL = $useSSL;
1713
1714 if (false === $rest->error && 200 !== $rest->code) {
1715 $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
1716 }
1717
1718 if (false !== $rest->error) {
1719 trigger_error(sprintf("UpdraftPlus_S3::getDistributionInvalidationList('{$distributionId}'): [%s]",
1720 $rest->error['code'], $rest->error['message']), E_USER_WARNING);
1721 return false;
1722 } elseif ($rest->body instanceof SimpleXMLElement && isset($rest->body->InvalidationSummary)) {
1723 $list = array();
1724 foreach ($rest->body->InvalidationSummary as $summary)
1725 $list[(string)$summary->Id] = (string)$summary->Status;
1726
1727 return $list;
1728 }
1729 return array();
1730 }
1731
1732
1733 /**
1734 * Get a DistributionConfig DOMDocument
1735 *
1736 * http://docs.amazonwebservices.com/AmazonCloudFront/latest/APIReference/index.html?PutConfig.html
1737 *
1738 * @internal Used to create XML in createDistribution() and updateDistribution()
1739 * @param string $bucket S3 Origin bucket
1740 * @param boolean $enabled Enabled (true/false)
1741 * @param string $comment Comment to append
1742 * @param string $callerReference Caller reference
1743 * @param array $cnames Array of CNAME aliases
1744 * @param string $defaultRootObject Default root object
1745 * @param string $originAccessIdentity Origin access identity
1746 * @param array $trustedSigners Array of trusted signers
1747 *
1748 * @return string
1749 */
1750 private function __getCloudFrontDistributionConfigXML($bucket, $enabled, $comment, $callerReference = '0', $cnames = array(), $defaultRootObject = null, $originAccessIdentity = null, $trustedSigners = array()) {// phpcs:ignore PHPCompatibility.FunctionNameRestrictions.ReservedFunctionNames.MethodDoubleUnderscore -- Method name "UpdraftPlus_S3Request::__responseHeaderCallback" is discouraged; PHP has reserved all method names with a double underscore prefix for future use.
1751 $dom = new DOMDocument('1.0', 'UTF-8');
1752 $dom->formatOutput = true;
1753 $distributionConfig = $dom->createElement('DistributionConfig');
1754 $distributionConfig->setAttribute('xmlns', 'http://cloudfront.amazonaws.com/doc/2010-11-01/');
1755
1756 $origin = $dom->createElement('S3Origin');
1757 $origin->appendChild($dom->createElement('DNSName', $bucket));
1758 if (null !== $originAccessIdentity) $origin->appendChild($dom->createElement('OriginAccessIdentity', $originAccessIdentity));
1759 $distributionConfig->appendChild($origin);
1760
1761 if (null !== $defaultRootObject) $distributionConfig->appendChild($dom->createElement('DefaultRootObject', $defaultRootObject));
1762
1763 $distributionConfig->appendChild($dom->createElement('CallerReference', $callerReference));
1764 foreach ($cnames as $cname)
1765 $distributionConfig->appendChild($dom->createElement('CNAME', $cname));
1766 if ('' !== $comment) $distributionConfig->appendChild($dom->createElement('Comment', $comment));
1767 $distributionConfig->appendChild($dom->createElement('Enabled', $enabled ? 'true' : 'false'));
1768
1769 if (!empty($trustedSigners)) {
1770 $trusted = $dom->createElement('TrustedSigners');
1771 foreach ($trustedSigners as $id => $type) {
1772 $trusted->appendChild(('' !== $id) ? $dom->createElement($type, $id) : $dom->createElement($type));
1773 }
1774 $distributionConfig->appendChild($trusted);
1775 }
1776
1777 $dom->appendChild($distributionConfig);
1778 //var_dump($dom->saveXML());
1779 return $dom->saveXML();
1780 }
1781
1782
1783 /**
1784 * Parse a CloudFront distribution config
1785 *
1786 * See http://docs.amazonwebservices.com/AmazonCloudFront/latest/APIReference/index.html?GetDistribution.html
1787 *
1788 * @internal Used to parse the CloudFront DistributionConfig node to an array
1789 *
1790 * @param object &$node DOMNode
1791 *
1792 * @return array
1793 */
1794 private function __parseCloudFrontDistributionConfig(&$node) {// phpcs:ignore PHPCompatibility.FunctionNameRestrictions.ReservedFunctionNames.MethodDoubleUnderscore -- Method name "UpdraftPlus_S3Request::__responseHeaderCallback" is discouraged; PHP has reserved all method names with a double underscore prefix for future use.
1795 if (isset($node->DistributionConfig))
1796 return $this->__parseCloudFrontDistributionConfig($node->DistributionConfig);
1797
1798 $dist = array();
1799 if (isset($node->Id, $node->Status, $node->LastModifiedTime, $node->DomainName)) {
1800 $dist['id'] = (string)$node->Id;
1801 $dist['status'] = (string)$node->Status;
1802 $dist['time'] = strtotime((string)$node->LastModifiedTime);
1803 $dist['domain'] = (string)$node->DomainName;
1804 }
1805
1806 if (isset($node->CallerReference))
1807 $dist['callerReference'] = (string)$node->CallerReference;
1808
1809 if (isset($node->Enabled))
1810 $dist['enabled'] = (string)$node->Enabled == 'true' ? true : false;
1811
1812 if (isset($node->S3Origin)) {
1813 if (isset($node->S3Origin->DNSName))
1814 $dist['origin'] = (string)$node->S3Origin->DNSName;
1815
1816 $dist['originAccessIdentity'] = isset($node->S3Origin->OriginAccessIdentity) ?
1817 (string)$node->S3Origin->OriginAccessIdentity : null;
1818 }
1819
1820 $dist['defaultRootObject'] = isset($node->DefaultRootObject) ? (string)$node->DefaultRootObject : null;
1821
1822 $dist['cnames'] = array();
1823 if (isset($node->CNAME))
1824 foreach ($node->CNAME as $cname)
1825 $dist['cnames'][(string)$cname] = (string)$cname;
1826
1827 $dist['trustedSigners'] = array();
1828 if (isset($node->TrustedSigners))
1829 foreach ($node->TrustedSigners as $signer) {
1830 if (isset($signer->Self))
1831 $dist['trustedSigners'][''] = 'Self';
1832 elseif (isset($signer->KeyPairId))
1833 $dist['trustedSigners'][(string)$signer->KeyPairId] = 'KeyPairId';
1834 elseif (isset($signer->AwsAccountNumber))
1835 $dist['trustedSigners'][(string)$signer->AwsAccountNumber] = 'AwsAccountNumber';
1836 }
1837
1838 $dist['comment'] = isset($node->Comment) ? (string)$node->Comment : null;
1839 return $dist;
1840 }
1841
1842
1843 /**
1844 * Grab CloudFront response
1845 *
1846 * @internal Used to parse the CloudFront UpdraftPlus_S3Request::getResponse() output
1847 *
1848 * @param object &$rest UpdraftPlus_S3Request instance
1849 *
1850 * @return object
1851 */
1852 private function __getCloudFrontResponse(&$rest) {// phpcs:ignore PHPCompatibility.FunctionNameRestrictions.ReservedFunctionNames.MethodDoubleUnderscore -- Method name "UpdraftPlus_S3Request::__responseHeaderCallback" is discouraged; PHP has reserved all method names with a double underscore prefix for future use.
1853 $rest->getResponse();
1854 if (false === $rest->response->error && isset($rest->response->body) &&
1855 is_string($rest->response->body) && '<?xml' == substr($rest->response->body, 0, 5)) {
1856 $rest->response->body = simplexml_load_string($rest->response->body);
1857 // Grab CloudFront errors
1858 if (isset($rest->response->body->Error, $rest->response->body->Error->Code,
1859 $rest->response->body->Error->Message)) {
1860 $rest->response->error = array(
1861 'code' => (string)$rest->response->body->Error->Code,
1862 'message' => (string)$rest->response->body->Error->Message
1863 );
1864 unset($rest->response->body);
1865 }
1866 }
1867 return $rest->response;
1868 }
1869
1870
1871 /**
1872 * Get MIME type for file
1873 *
1874 * @internal Used to get mime types
1875 *
1876 * @param string &$file File path
1877 *
1878 * @return string
1879 */
1880 public function __getMimeType(&$file) {// phpcs:ignore PHPCompatibility.FunctionNameRestrictions.ReservedFunctionNames.MethodDoubleUnderscore -- Method name "UpdraftPlus_S3Request::__responseHeaderCallback" is discouraged; PHP has reserved all method names with a double underscore prefix for future use.
1881 $type = false;
1882 // Fileinfo documentation says fileinfo_open() will use the
1883 // MAGIC env var for the magic file
1884 if (extension_loaded('fileinfo') && isset($_ENV['MAGIC']) &&
1885 false !== ($finfo = finfo_open(FILEINFO_MIME, $_ENV['MAGIC']))) {// phpcs:ignore PHPCompatibility.FunctionUse.NewFunctions.finfo_openFound -- The function finfo_open() is not present in PHP version 5.2 or earlier
1886 if (false !== ($type = finfo_file($finfo, $file))) {// phpcs:ignore PHPCompatibility.FunctionUse.NewFunctions.finfo_fileFound -- The function finfo_file() is not present in PHP version 5.2 or earlier
1887 // Remove the charset and grab the last content-type
1888 $type = explode(' ', str_replace('; charset=', ';charset=', $type));
1889 $type = array_pop($type);
1890 $type = explode(';', $type);
1891 $type = trim(array_shift($type));
1892 }
1893 finfo_close($finfo);// phpcs:ignore PHPCompatibility.FunctionUse.NewFunctions.finfo_closeFound -- The function finfo_close() is not present in PHP version 5.2 or earlier
1894
1895 // If anyone is still using mime_content_type()
1896 } elseif (function_exists('mime_content_type')) {
1897 $type = trim(mime_content_type($file));
1898 }
1899
1900 if (false !== $type && strlen($type) > 0) return $type;
1901
1902 // Otherwise do it the old fashioned way
1903 static $exts = array(
1904 'jpg' => 'image/jpeg', 'gif' => 'image/gif', 'png' => 'image/png',
1905 'tif' => 'image/tiff', 'tiff' => 'image/tiff', 'ico' => 'image/x-icon',
1906 'swf' => 'application/x-shockwave-flash', 'pdf' => 'application/pdf',
1907 'zip' => 'application/zip', 'gz' => 'application/x-gzip',
1908 'tar' => 'application/x-tar', 'bz' => 'application/x-bzip',
1909 'bz2' => 'application/x-bzip2', 'txt' => 'text/plain',
1910 'asc' => 'text/plain', 'htm' => 'text/html', 'html' => 'text/html',
1911 'css' => 'text/css', 'js' => 'text/javascript',
1912 'xml' => 'text/xml', 'xsl' => 'application/xsl+xml',
1913 'ogg' => 'application/ogg', 'mp3' => 'audio/mpeg', 'wav' => 'audio/x-wav',
1914 'avi' => 'video/x-msvideo', 'mpg' => 'video/mpeg', 'mpeg' => 'video/mpeg',
1915 'mov' => 'video/quicktime', 'flv' => 'video/x-flv', 'php' => 'text/x-php'
1916 );
1917 $ext = strtolower(pathInfo($file, PATHINFO_EXTENSION));
1918 return isset($exts[$ext]) ? $exts[$ext] : 'application/octet-stream';
1919 }
1920
1921
1922 /**
1923 * Generate the auth string: "AWS AccessKey:Signature"
1924 *
1925 * @internal Used by UpdraftPlus_S3Request::getResponse()
1926 *
1927 * @param string $string String to sign
1928 *
1929 * @return string
1930 */
1931 public function __getSignature($string) {// phpcs:ignore PHPCompatibility.FunctionNameRestrictions.ReservedFunctionNames.MethodDoubleUnderscore -- Method name "UpdraftPlus_S3Request::__responseHeaderCallback" is discouraged; PHP has reserved all method names with a double underscore prefix for future use.
1932 return 'AWS '.$this->__accessKey.':'.$this->__getHash($string);
1933 }
1934
1935
1936 /**
1937 * Creates a HMAC-SHA1 hash
1938 *
1939 * This uses the hash extension if loaded
1940 *
1941 * @internal Used by __getSignature()
1942 *
1943 * @param string $string String to sign
1944 *
1945 * @return string
1946 */
1947 private function __getHash($string) {// phpcs:ignore PHPCompatibility.FunctionNameRestrictions.ReservedFunctionNames.MethodDoubleUnderscore -- Method name "UpdraftPlus_S3Request::__responseHeaderCallback" is discouraged; PHP has reserved all method names with a double underscore prefix for future use.
1948 return base64_encode(extension_loaded('hash') ?
1949 hash_hmac('sha1', $string, $this->__secretKey, true) : pack('H*', sha1(
1950 (str_pad($this->__secretKey, 64, chr(0x00)) ^ (str_repeat(chr(0x5c), 64))) .
1951 pack('H*', sha1((str_pad($this->__secretKey, 64, chr(0x00)) ^
1952 (str_repeat(chr(0x36), 64))) . $string)))));
1953 }
1954
1955 /**
1956 * Generate the headers for AWS Signature V4
1957 *
1958 * @internal Used by UpdraftPlus_S3Request::getResponse()
1959 * @param array $aHeaders amzHeaders
1960 * @param array $headers
1961 * @param string $method
1962 * @param string $uri
1963 * @param string $data
1964 *
1965 * @return array $headers
1966 */
1967 public function __getSignatureV4($aHeaders, $headers, $method = 'GET', $uri = '', $data = '') {// phpcs:ignore PHPCompatibility.FunctionNameRestrictions.ReservedFunctionNames.MethodDoubleUnderscore -- Method name "UpdraftPlus_S3Request::__responseHeaderCallback" is discouraged; PHP has reserved all method names with a double underscore prefix for future use.
1968 $service = 's3';
1969 $region = $this->getRegion();
1970
1971 $algorithm = 'AWS4-HMAC-SHA256';
1972 $amzHeaders = array();
1973 $amzRequests = array();
1974
1975 $amzDate = gmdate('Ymd\THis\Z');
1976 $amzDateStamp = gmdate('Ymd');
1977
1978 // amz-date ISO8601 format? for aws request
1979 $amzHeaders['x-amz-date'] = $amzDate;
1980
1981 // CanonicalHeaders
1982 foreach ($headers as $k => $v) {
1983 $amzHeaders[strtolower($k)] = trim($v);
1984 }
1985
1986 foreach ($aHeaders as $k => $v) {
1987 $amzHeaders[strtolower($k)] = trim($v);
1988 }
1989 uksort($amzHeaders, 'strcmp');
1990
1991 // payload
1992 $payloadHash = isset($amzHeaders['x-amz-content-sha256']) ? $amzHeaders['x-amz-content-sha256'] : hash('sha256', $data);
1993
1994 // parameters
1995 $parameters = array();
1996 if (strpos($uri, '?')) {
1997 list($uri, $query_str) = @explode('?', $uri);
1998 parse_str($query_str, $parameters);
1999 }
2000
2001 // Canonical Requests
2002 $amzRequests[] = $method;
2003 $uriQmPos = strpos($uri, '?');
2004 $amzRequests[] = (false === $uriQmPos ? $uri : substr($uri, 0, $uriQmPos));
2005 $amzRequests[] = http_build_query($parameters);
2006
2007 // add headers as string to requests
2008 foreach ($amzHeaders as $k => $v) {
2009 $amzRequests[] = $k . ':' . $v;
2010 }
2011
2012 // add a blank entry so we end up with an extra line break
2013 $amzRequests[] = '';
2014
2015 // SignedHeaders
2016 $amzRequests[] = implode(';', array_keys($amzHeaders));
2017
2018 // payload hash
2019 $amzRequests[] = $payloadHash;
2020
2021 // request as string
2022 $amzRequestStr = implode("\n", $amzRequests);
2023
2024 // CredentialScope
2025 $credentialScope = array();
2026 $credentialScope[] = $amzDateStamp;
2027 $credentialScope[] = $region;
2028 $credentialScope[] = $service;
2029 $credentialScope[] = 'aws4_request';
2030
2031 // stringToSign
2032 $stringToSign = array();
2033 $stringToSign[] = $algorithm;
2034 $stringToSign[] = $amzDate;
2035 $stringToSign[] = implode('/', $credentialScope);
2036 $stringToSign[] = hash('sha256', $amzRequestStr);
2037
2038 // as string
2039 $stringToSignStr = implode("\n", $stringToSign);
2040
2041 // Make Signature
2042 $kSecret = 'AWS4' . $this->__secretKey;
2043 $kDate = hash_hmac('sha256', $amzDateStamp, $kSecret, true);
2044 $kRegion = hash_hmac('sha256', $region, $kDate, true);
2045 $kService = hash_hmac('sha256', $service, $kRegion, true);
2046 $kSigning = hash_hmac('sha256', 'aws4_request', $kService, true);
2047 $signature = hash_hmac('sha256', $stringToSignStr, $kSigning);
2048
2049 $authorization = array(
2050 'Credential=' . $this->__accessKey . '/' . implode('/', $credentialScope),
2051 'SignedHeaders=' . implode(';', array_keys($amzHeaders)),
2052 'Signature=' . $signature,
2053 );
2054 $authorizationStr = $algorithm . ' ' . implode(',', $authorization);
2055
2056 $resultHeaders = array(
2057 'X-AMZ-DATE' => $amzDate,
2058 'Authorization' => $authorizationStr
2059 );
2060 if (!isset($aHeaders['x-amz-content-sha256'])) {
2061 $resultHeaders['x-amz-content-sha256'] = $payloadHash;
2062 }
2063
2064 return $resultHeaders;
2065 }
2066
2067 }
2068
2069 final class UpdraftPlus_S3Request {
2070 private $endpoint, $verb, $bucket, $uri, $resource = '', $parameters = array(),
2071 $amzHeaders = array(), $headers = array(
2072 'Host' => '', 'Date' => '', 'Content-MD5' => '', 'Content-Type' => ''
2073 );
2074 public $fp = false, $size = 0, $data = false, $response;
2075
2076 private $s3;
2077
2078
2079 /**
2080 * Constructor
2081 *
2082 * @param string $verb Verb
2083 * @param string $bucket Bucket name
2084 * @param string $uri Object URI
2085 * @param string $endpoint Endpoint of storage
2086 * @param boolean $use_dns_bucket_name
2087 * @param object $s3 S3 Object that calls these requests
2088 *
2089 * @return mixed
2090 */
2091 function __construct($verb, $bucket = '', $uri = '', $endpoint = 's3.amazonaws.com', $use_dns_bucket_name = false, $s3 = null) {
2092 $this->endpoint = $endpoint;
2093 $this->verb = $verb;
2094 $this->bucket = $bucket;
2095 $this->uri = $uri !== '' ? '/'.str_replace('%2F', '/', rawurlencode($uri)) : '/';
2096 $this->s3 = $s3;
2097
2098 //if ($this->bucket !== '')
2099 // $this->resource = '/'.$this->bucket.$this->uri;
2100 //else
2101 // $this->resource = $this->uri;
2102
2103 if ('' !== $this->bucket) {
2104 if ($this->__dnsBucketName($this->bucket) || $use_dns_bucket_name) {
2105 $this->headers['Host'] = $this->bucket.'.'.$this->endpoint;
2106 $this->resource = '/'.$this->bucket.$this->uri;
2107 } else {
2108 $this->headers['Host'] = $this->endpoint;
2109 $this->uri = $this->uri;
2110 if ('' !== $this->bucket) $this->uri = '/'.$this->bucket.$this->uri;
2111 $this->bucket = '';
2112 $this->resource = $this->uri;
2113 }
2114 } else {
2115 $this->headers['Host'] = $this->endpoint;
2116 $this->resource = $this->uri;
2117 }
2118
2119 $this->headers['Date'] = gmdate('D, d M Y H:i:s T');
2120 $this->response = new STDClass;
2121 $this->response->error = false;
2122 $this->response->body = null;
2123 }
2124
2125 /**
2126 * Set request parameter
2127 *
2128 * @param string $key Key
2129 * @param string $value Value
2130 *
2131 * @return void
2132 */
2133 public function setParameter($key, $value) {
2134 $this->parameters[$key] = $value;
2135 }
2136
2137
2138 /**
2139 * Set request header
2140 *
2141 * @param string $key Key
2142 * @param string $value Value
2143 *
2144 * @return void
2145 */
2146 public function setHeader($key, $value) {
2147 $this->headers[$key] = $value;
2148 }
2149
2150
2151 /**
2152 * Set x-amz-meta-* header
2153 *
2154 * @param string $key Key
2155 * @param string $value Value
2156 *
2157 * @return void
2158 */
2159 public function setAmzHeader($key, $value) {
2160 $this->amzHeaders[$key] = $value;
2161 }
2162
2163 /**
2164 * Get the S3 response
2165 *
2166 * @return object | false
2167 */
2168 public function getResponse() {
2169 $query = '';
2170 if (sizeof($this->parameters) > 0) {
2171 $query = ('?' !== substr($this->uri, -1)) ? '?' : '&';
2172 foreach ($this->parameters as $var => $value)
2173 if (null == $value || '' == $value) $query .= $var.'&';
2174 else $query .= $var.'='.rawurlencode($value).'&';
2175 $query = substr($query, 0, -1);
2176 $this->uri .= $query;
2177
2178 if (array_key_exists('acl', $this->parameters) ||
2179 array_key_exists('location', $this->parameters) ||
2180 array_key_exists('torrent', $this->parameters) ||
2181 array_key_exists('logging', $this->parameters) ||
2182 array_key_exists('partNumber', $this->parameters) ||
2183 array_key_exists('uploads', $this->parameters) ||
2184 array_key_exists('uploadId', $this->parameters))
2185 $this->resource .= $query;
2186 }
2187 $url = ($this->s3->useSSL ? 'https://' : 'http://') . ('' !== $this->headers['Host'] ? $this->headers['Host'] : $this->endpoint) . $this->uri;
2188
2189 //var_dump('bucket: ' . $this->bucket, 'uri: ' . $this->uri, 'resource: ' . $this->resource, 'url: ' . $url);
2190
2191 $curl = curl_init();
2192 curl_setopt($curl, CURLOPT_USERAGENT, 'S3/php');
2193
2194 if ($this->s3->useSSL) {
2195 // SSL Validation can now be optional for those with broken OpenSSL installations
2196 curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, $this->s3->useSSLValidation ? 2 : 0);
2197 curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, $this->s3->useSSLValidation ? 1 : 0);
2198
2199 if (null !== $this->s3->sslKey) curl_setopt($curl, CURLOPT_SSLKEY, $this->s3->sslKey);
2200 if (null !== $this->s3->sslCert) curl_setopt($curl, CURLOPT_SSLCERT, $this->s3->sslCert);
2201 if (null !== $this->s3->sslCACert && file_exists($this->s3->sslCACert)) curl_setopt($curl, CURLOPT_CAINFO, $this->s3->sslCACert);
2202 }
2203
2204 curl_setopt($curl, CURLOPT_URL, $url);
2205
2206 $wp_proxy = new WP_HTTP_Proxy();
2207
2208 if (null != $this->s3->proxy && isset($this->s3->proxy['host']) && $wp_proxy->send_through_proxy($url)) {
2209 curl_setopt($curl, CURLOPT_PROXY, $this->s3->proxy['host']);
2210 curl_setopt($curl, CURLOPT_PROXYTYPE, $this->s3->proxy['type']);
2211 if (!empty($this->s3->proxy['port'])) curl_setopt($curl,CURLOPT_PROXYPORT, $this->s3->proxy['port']);
2212 if (isset($this->s3->proxy['user'], $this->s3->proxy['pass']) && null != $this->s3->proxy['user'] && null != $this->s3->proxy['pass']) {
2213 curl_setopt($curl, CURLOPT_PROXYAUTH, CURLAUTH_ANY);
2214 curl_setopt($curl, CURLOPT_PROXYUSERPWD, sprintf('%s:%s', $this->s3->proxy['user'], $this->s3->proxy['pass']));
2215 }
2216 }
2217
2218 // Headers
2219 $headers = array(); $amz = array();
2220 foreach ($this->amzHeaders as $header => $value)
2221 if (strlen($value) > 0) $headers[] = $header.': '.$value;
2222 foreach ($this->headers as $header => $value)
2223 if (strlen($value) > 0) $headers[] = $header.': '.$value;
2224
2225 // Collect AMZ headers for signature
2226 foreach ($this->amzHeaders as $header => $value)
2227 if (strlen($value) > 0) $amz[] = strtolower($header).':'.$value;
2228
2229 // AMZ headers must be sorted
2230 if (sizeof($amz) > 0) {
2231 //sort($amz);
2232 usort($amz, array(&$this, '__sortMetaHeadersCmp'));
2233 $amz = "\n".implode("\n", $amz);
2234 } else {
2235 $amz = '';
2236 }
2237
2238 if ($this->s3->hasAuth()) {
2239 // Authorization string (CloudFront stringToSign should only contain a date)
2240 if ('cloudfront.amazonaws.com' == $this->headers['Host']) {
2241 $headers[] = 'Authorization: ' . $this->s3->__getSignature($this->headers['Date']);
2242 } else {
2243 if ('v2' === $this->s3->signVer) {
2244 $headers[] = 'Authorization: ' . $this->s3->__getSignature(
2245 $this->verb."\n".
2246 $this->headers['Content-MD5']."\n".
2247 $this->headers['Content-Type']."\n".
2248 $this->headers['Date'].$amz."\n".
2249 $this->resource
2250 );
2251 } else {
2252 $amzHeaders = $this->s3->__getSignatureV4(
2253 $this->amzHeaders,
2254 $this->headers,
2255 $this->verb,
2256 $this->uri,
2257 $this->data
2258 );
2259 foreach ($amzHeaders as $k => $v) {
2260 $headers[] = $k . ': ' . $v;
2261 }
2262 }
2263 }
2264 }
2265
2266 if (false !== $this->s3->port) curl_setopt($curl, CURLOPT_PORT, $this->s3->port);
2267 curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
2268 curl_setopt($curl, CURLOPT_HEADER, false);
2269 curl_setopt($curl, CURLOPT_RETURNTRANSFER, false);
2270 curl_setopt($curl, CURLOPT_WRITEFUNCTION, array(&$this, '__responseWriteCallback'));
2271 curl_setopt($curl, CURLOPT_HEADERFUNCTION, array(&$this, '__responseHeaderCallback'));
2272 @curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
2273
2274 // Request types
2275 switch ($this->verb) {
2276 case 'GET': break;
2277 case 'PUT': case 'POST':
2278 if (false !== $this->fp) {
2279 curl_setopt($curl, CURLOPT_PUT, true);
2280 curl_setopt($curl, CURLOPT_INFILE, $this->fp);
2281 if ($this->size >= 0) {
2282 curl_setopt($curl, CURLOPT_INFILESIZE, $this->size);
2283 }
2284 } elseif (false !== $this->data) {
2285 curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $this->verb);
2286 curl_setopt($curl, CURLOPT_POSTFIELDS, $this->data);
2287 curl_setopt($curl, CURLOPT_INFILESIZE, strlen($this->data));
2288 } else {
2289 curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $this->verb);
2290 }
2291 break;
2292 case 'HEAD':
2293 curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'HEAD');
2294 curl_setopt($curl, CURLOPT_NOBODY, true);
2295 break;
2296 case 'DELETE':
2297 curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'DELETE');
2298 break;
2299 default: break;
2300 }
2301
2302 // Execute, grab errors
2303 if (curl_exec($curl))
2304 $this->response->code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
2305 else
2306 $this->response->error = array(
2307 'code' => curl_errno($curl),
2308 'message' => curl_error($curl),
2309 'resource' => $this->resource
2310 );
2311
2312 @curl_close($curl);
2313
2314 // Parse body into XML
2315 // The case in which there is not application/xml content-type header is to support a DreamObjects case seen, April 2018
2316 if (false === $this->response->error && isset($this->response->body) && ((isset($this->response->headers['type']) && 'application/xml' == $this->response->headers['type']) || (!isset($this->response->headers['type']) && 0 === strpos($this->response->body, '<?xml')))) {
2317 $this->response->body = simplexml_load_string($this->response->body);
2318
2319 // Grab S3 errors
2320 if (!in_array($this->response->code, array(200, 204, 206)) &&
2321 isset($this->response->body->Code)) {
2322 $this->response->error = array(
2323 'code' => (string)$this->response->body->Code,
2324 );
2325 $this->response->error['message'] = isset($this->response->body->Message) ? $this->response->body->Message : '';
2326 if (isset($this->response->body->Resource))
2327 $this->response->error['resource'] = (string)$this->response->body->Resource;
2328 unset($this->response->body);
2329 }
2330 }
2331
2332 // Clean up file resources
2333 if (false !== $this->fp && is_resource($this->fp)) fclose($this->fp);
2334
2335 return $this->response;
2336 }
2337
2338 /**
2339 * Sort compare for meta headers
2340 *
2341 * @internal Used to sort x-amz meta headers
2342 *
2343 * @param string $a String A
2344 * @param string $b String B
2345 *
2346 * @return integer
2347 */
2348 private function __sortMetaHeadersCmp($a, $b) {// phpcs:ignore PHPCompatibility.FunctionNameRestrictions.ReservedFunctionNames.MethodDoubleUnderscore -- Method name "UpdraftPlus_S3Request::__responseHeaderCallback" is discouraged; PHP has reserved all method names with a double underscore prefix for future use.
2349 $lenA = strpos($a, ':');
2350 $lenB = strpos($b, ':');
2351 $minLen = min($lenA, $lenB);
2352 $ncmp = strncmp($a, $b, $minLen);
2353 if ($lenA == $lenB) return $ncmp;
2354 if (0 == $ncmp) return $lenA < $lenB ? -1 : 1;
2355 return $ncmp;
2356 }
2357
2358 /**
2359 * CURL write callback
2360 *
2361 * @param resource $curl CURL resource
2362 * @param string $data Data
2363 *
2364 * @return integer
2365 */
2366 private function __responseWriteCallback($curl, $data) {// phpcs:ignore PHPCompatibility.FunctionNameRestrictions.ReservedFunctionNames.MethodDoubleUnderscore -- Method name "UpdraftPlus_S3Request::__responseHeaderCallback" is discouraged; PHP has reserved all method names with a double underscore prefix for future use.
2367 if (in_array($this->response->code, array(200, 206)) && false !== $this->fp)
2368 return fwrite($this->fp, $data);
2369 else
2370 $this->response->body = (empty($this->response->body)) ? $data : $this->response->body.$data;
2371 return strlen($data);
2372 }
2373
2374
2375 /**
2376 * Check DNS conformity
2377 *
2378 * @param string $bucket Bucket name
2379 *
2380 * @return boolean
2381 */
2382 private function __dnsBucketName($bucket) {// phpcs:ignore PHPCompatibility.FunctionNameRestrictions.ReservedFunctionNames.MethodDoubleUnderscore -- Method name "UpdraftPlus_S3Request::__responseHeaderCallback" is discouraged; PHP has reserved all method names with a double underscore prefix for future use.
2383 # A DNS bucket name cannot have len>63
2384 # A DNS bucket name must have a character in other than a-z, 0-9, . -
2385 # The purpose of this second check is not clear - is it that there's some limitation somewhere on bucket names that match that pattern that means that the bucket must be accessed by hostname?
2386 if (strlen($bucket) > 63 || !preg_match("/[^a-z0-9\.-]/", $bucket)) return false;
2387 # A DNS bucket name cannot contain -.
2388 if (false !== strstr($bucket, '-.')) return false;
2389 # A DNS bucket name cannot contain ..
2390 if (false !== strstr($bucket, '..')) return false;
2391 # A DNS bucket name must begin with 0-9a-z
2392 if (!preg_match("/^[0-9a-z]/", $bucket)) return false;
2393 # A DNS bucket name must end with 0-9 a-z
2394 if (!preg_match("/[0-9a-z]$/", $bucket)) return false;
2395 return true;
2396 }
2397
2398 /**
2399 * CURL header callback
2400 *
2401 * @param resource $curl CURL resource
2402 * @param string $data Data
2403 * @return integer
2404 */
2405 private function __responseHeaderCallback($curl, $data) {// phpcs:ignore PHPCompatibility.FunctionNameRestrictions.ReservedFunctionNames.MethodDoubleUnderscore -- Method name "UpdraftPlus_S3Request::__responseHeaderCallback" is discouraged; PHP has reserved all method names with a double underscore prefix for future use.
2406 if (($strlen = strlen($data)) <= 2) return $strlen;
2407 if ('HTTP' == substr($data, 0, 4)) {
2408 $this->response->code = (int)substr($data, 9, 3);
2409 } else {
2410 $data = trim($data);
2411 if (false === strpos($data, ': ')) return $strlen;
2412 list($header, $value) = explode(': ', $data, 2);
2413 if ('last-modified' == strtolower($header))
2414 $this->response->headers['time'] = strtotime($value);
2415 elseif ('content-length' == strtolower($header))
2416 $this->response->headers['size'] = (int)$value;
2417 elseif ('content-type' == strtolower($header))
2418 $this->response->headers['type'] = $value;
2419 elseif ('etag' == strtolower($header))
2420 $this->response->headers['hash'] = '"' == $value[0] ? substr($value, 1, -1) : $value;
2421 elseif (preg_match('/^x-amz-meta-.*$/i', $header))
2422 $this->response->headers[strtolower($header)] = $value;
2423 }
2424 return $strlen;
2425 }
2426
2427 }
2428