PluginProbe
WP Database Backup – Unlimited Database & Files Backup by Backup for WP / 6.11
WP Database Backup – Unlimited Database & Files Backup by Backup for WP v6.11
7.13 7.12 trunk 1.1 2.1.1 5.9 6.0 6.1 6.10 6.11 6.12 6.12.1 6.2 6.3 6.4 6.5 6.5.1 6.6 6.7 6.8 6.9 7.0 7.0.1 7.1 7.10 All 34 releases
wp-database-backup / includes / admin / Destination / S3 / S3.php

S3.php in WP Database Backup – Unlimited Database & Files Backup by Backup for WP 6.11, at includes/admin/Destination/S3/S3.php

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