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