PluginProbe
UpdraftPlus: WP Backup & Migration Plugin / 1.13.9
UpdraftPlus: WP Backup & Migration Plugin v1.13.9
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.13.9, at includes/S3.php

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