· 6 years ago · Apr 01, 2019, 12:12 PM
1<?php
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*/
37class 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 = 'sgp1.digitaloceanspaces.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 = 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('\/', '/', 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 */
1918final class S3Request
1919{
1920 /**
1921 * AWS URI
1922 *
1923 * @var string
1924 * @access private
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
2374class 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}