HomeHelpTrac

source: trunk/plugins/S3.php @ 15512

Revision 15512, 62.7 KB checked in by Alexander Trofimov, 8 months ago (diff)

Images Transcoder

Line 
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*/
30
31/**
32* Amazon S3 PHP class
33*
34* @link http://undesigned.org.za/2007/10/22/amazon-s3-php-class
35* @version 0.5.0-dev
36*/
37class S3
38{
39    // ACL flags
40    const ACL_PRIVATE = 'private';
41    const ACL_PUBLIC_READ = 'public-read';
42    const ACL_PUBLIC_READ_WRITE = 'public-read-write';
43    const ACL_AUTHENTICATED_READ = 'authenticated-read';
44
45    const STORAGE_CLASS_STANDARD = 'STANDARD';
46    const STORAGE_CLASS_RRS = 'REDUCED_REDUNDANCY';
47
48    private static $__accessKey = null; // AWS Access key
49    private static $__secretKey = null; // AWS Secret key
50    private static $__sslKey = null;
51
52    public static $endpoint = 's3.amazonaws.com';
53    public static $proxy = null;
54
55    public static $useSSL = false;
56    public static $useSSLValidation = true;
57    public static $useExceptions = false;
58
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;
63
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
66
67
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    }
83
84
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    }
95
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    }
108
109
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    }
118
119
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    }
132
133
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    }
148
149
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    }
163
164
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    }
175
176
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    }
193
194
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    }
205
206
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    }
224
225
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;
246
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;
260
261        return $results;
262    }
263
264
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        }
295
296        $results = array();
297
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        }
310
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);
314
315        if (isset($response->body, $response->body->IsTruncated) &&
316        (string)$response->body->IsTruncated == 'false') return $results;
317
318        if (isset($response->body, $response->body->NextMarker))
319            $nextMarker = (string)$response->body->NextMarker;
320
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);
329
330            if (($response = $rest->getResponse(true)) == false || $response->code !== 200) break;
331
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            }
343
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);
347
348            if (isset($response->body, $response->body->NextMarker))
349                $nextMarker = (string)$response->body->NextMarker;
350
351        } while ($response !== false && (string)$response->body->IsTruncated == 'true');
352
353        return $results;
354    }
355
356
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);
369
370        if ($location !== false)
371        {
372            $dom = new DOMDocument;
373            $createBucketConfiguration = $dom->createElement('CreateBucketConfiguration');
374            $locationConstraint = $dom->createElement('LocationConstraint', strtoupper($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();
382
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    }
393
394
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    }
415
416
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        clearstatcache(true, $file);
432        return array('file' => $file, 'size' => filesize($file), 'md5sum' => $md5sum !== false ?
433        (is_string($md5sum) ? $md5sum : base64_encode(md5_file($file, true))) : '');
434    }
435
436
437    /**
438    * Create input array info for putObject() with a resource
439    *
440    * @param string $resource Input resource to read from
441    * @param integer $bufferSize Input byte size
442    * @param string $md5sum MD5 hash to send (optional)
443    * @return array | false
444    */
445    public static function inputResource(&$resource, $bufferSize, $md5sum = '')
446    {
447        if (!is_resource($resource) || $bufferSize < 0)
448        {
449            self::__triggerError('S3::inputResource(): Invalid resource or buffer size', __FILE__, __LINE__);
450            return false;
451        }
452        $input = array('size' => $bufferSize, 'md5sum' => $md5sum);
453        $input['fp'] =& $resource;
454        return $input;
455    }
456
457
458    /**
459    * Put an object
460    *
461    * @param mixed $input Input data
462    * @param string $bucket Bucket name
463    * @param string $uri Object URI
464    * @param constant $acl ACL constant
465    * @param array $metaHeaders Array of x-amz-meta-* headers
466    * @param array $requestHeaders Array of request headers or content type as a string
467    * @param constant $storageClass Storage class constant
468    * @return boolean
469    */
470    public static function putObject($input, $bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $requestHeaders = array(), $storageClass = self::STORAGE_CLASS_STANDARD)
471    {
472        if ($input === false) return false;
473        $rest = new S3Request('PUT', $bucket, $uri, self::$endpoint);
474
475        if (is_string($input)) $input = array(
476            'data' => $input, 'size' => strlen($input),
477            'md5sum' => base64_encode(md5($input, true))
478        );
479
480        // Data
481        if (isset($input['fp']))
482            $rest->fp =& $input['fp'];
483        elseif (isset($input['file']))
484            $rest->fp = @fopen($input['file'], 'rb');
485        elseif (isset($input['data']))
486            $rest->data = $input['data'];
487
488        // Content-Length (required)
489        if (isset($input['size']) && $input['size'] >= 0)
490            $rest->size = $input['size'];
491        else {
492            if (isset($input['file']))
493                $rest->size = filesize($input['file']);
494            elseif (isset($input['data']))
495                $rest->size = strlen($input['data']);
496        }
497
498        // Custom request headers (Content-Type, Content-Disposition, Content-Encoding)
499        if (is_array($requestHeaders))
500            foreach ($requestHeaders as $h => $v) $rest->setHeader($h, $v);
501        elseif (is_string($requestHeaders)) // Support for legacy contentType parameter
502            $input['type'] = $requestHeaders;
503
504        // Content-Type
505        if (!isset($input['type']))
506        {
507            if (isset($requestHeaders['Content-Type']))
508                $input['type'] =& $requestHeaders['Content-Type'];
509            elseif (isset($input['file']))
510                $input['type'] = self::__getMimeType($input['file']);
511            else
512                $input['type'] = 'application/octet-stream';
513        }
514
515        if ($storageClass !== self::STORAGE_CLASS_STANDARD) // Storage class
516            $rest->setAmzHeader('x-amz-storage-class', $storageClass);
517
518        // We need to post with Content-Length and Content-Type, MD5 is optional
519        if ($rest->size >= 0 && ($rest->fp !== false || $rest->data !== false))
520        {
521            $rest->setHeader('Content-Type', $input['type']);
522            if (isset($input['md5sum'])) $rest->setHeader('Content-MD5', $input['md5sum']);
523
524            $rest->setAmzHeader('x-amz-acl', $acl);
525            foreach ($metaHeaders as $h => $v) $rest->setAmzHeader('x-amz-meta-'.$h, $v);
526            $rest->getResponse();
527        } else
528            $rest->response->error = array('code' => 0, 'message' => 'Missing input parameters');
529
530        if ($rest->response->error === false && $rest->response->code !== 200)
531            $rest->response->error = array('code' => $rest->response->code, 'message' => 'Unexpected HTTP status');
532        if ($rest->response->error !== false)
533        {
534            self::__triggerError(sprintf("S3::putObject(): [%s] %s",
535            $rest->response->error['code'], $rest->response->error['message']), __FILE__, __LINE__);
536            return false;
537        }
538        return true;
539    }
540
541
542    /**
543    * Put an object from a file (legacy function)
544    *
545    * @param string $file Input file path
546    * @param string $bucket Bucket name
547    * @param string $uri Object URI
548    * @param constant $acl ACL constant
549    * @param array $metaHeaders Array of x-amz-meta-* headers
550    * @param string $contentType Content type
551    * @return boolean
552    */
553    public static function putObjectFile($file, $bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $contentType = null)
554    {
555        return self::putObject(self::inputFile($file), $bucket, $uri, $acl, $metaHeaders, $contentType);
556    }
557
558
559    /**
560    * Put an object from a string (legacy function)
561    *
562    * @param string $string Input data
563    * @param string $bucket Bucket name
564    * @param string $uri Object URI
565    * @param constant $acl ACL constant
566    * @param array $metaHeaders Array of x-amz-meta-* headers
567    * @param string $contentType Content type
568    * @return boolean
569    */
570    public static function putObjectString($string, $bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $contentType = 'text/plain')
571    {
572        return self::putObject($string, $bucket, $uri, $acl, $metaHeaders, $contentType);
573    }
574
575
576    /**
577    * Get an object
578    *
579    * @param string $bucket Bucket name
580    * @param string $uri Object URI
581    * @param mixed $saveTo Filename or resource to write to
582    * @return mixed
583    */
584    public static function getObject($bucket, $uri, $saveTo = false)
585    {
586        $rest = new S3Request('GET', $bucket, $uri, self::$endpoint);
587        if ($saveTo !== false)
588        {
589            if (is_resource($saveTo))
590                $rest->fp =& $saveTo;
591            else
592                if (($rest->fp = @fopen($saveTo, 'wb')) !== false)
593                    $rest->file = realpath($saveTo);
594                else
595                    $rest->response->error = array('code' => 0, 'message' => 'Unable to open save file for writing: '.$saveTo);
596        }
597        if ($rest->response->error === false) $rest->getResponse();
598
599        if ($rest->response->error === false && $rest->response->code !== 200)
600            $rest->response->error = array('code' => $rest->response->code, 'message' => 'Unexpected HTTP status');
601        if ($rest->response->error !== false)
602        {
603            self::__triggerError(sprintf("S3::getObject({$bucket}, {$uri}): [%s] %s",
604            $rest->response->error['code'], $rest->response->error['message']), __FILE__, __LINE__);
605            return false;
606        }
607        return $rest->response;
608    }
609
610
611    /**
612    * Get object information
613    *
614    * @param string $bucket Bucket name
615    * @param string $uri Object URI
616    * @param boolean $returnInfo Return response information
617    * @return mixed | false
618    */
619    public static function getObjectInfo($bucket, $uri, $returnInfo = true)
620    {
621        $rest = new S3Request('HEAD', $bucket, $uri, self::$endpoint);
622        $rest = $rest->getResponse();
623        if ($rest->error === false && ($rest->code !== 200 && $rest->code !== 404))
624            $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
625        if ($rest->error !== false)
626        {
627            self::__triggerError(sprintf("S3::getObjectInfo({$bucket}, {$uri}): [%s] %s",
628            $rest->error['code'], $rest->error['message']), __FILE__, __LINE__);
629            return false;
630        }
631        return $rest->code == 200 ? $returnInfo ? $rest->headers : true : false;
632    }
633
634
635    /**
636    * Copy an object
637    *
638    * @param string $bucket Source bucket name
639    * @param string $uri Source object URI
640    * @param string $bucket Destination bucket name
641    * @param string $uri Destination object URI
642    * @param constant $acl ACL constant
643    * @param array $metaHeaders Optional array of x-amz-meta-* headers
644    * @param array $requestHeaders Optional array of request headers (content type, disposition, etc.)
645    * @param constant $storageClass Storage class constant
646    * @return mixed | false
647    */
648    public static function copyObject($srcBucket, $srcUri, $bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $requestHeaders = array(), $storageClass = self::STORAGE_CLASS_STANDARD)
649    {
650        $rest = new S3Request('PUT', $bucket, $uri, self::$endpoint);
651        $rest->setHeader('Content-Length', 0);
652        foreach ($requestHeaders as $h => $v) $rest->setHeader($h, $v);
653        foreach ($metaHeaders as $h => $v) $rest->setAmzHeader('x-amz-meta-'.$h, $v);
654        if ($storageClass !== self::STORAGE_CLASS_STANDARD) // Storage class
655            $rest->setAmzHeader('x-amz-storage-class', $storageClass);
656        $rest->setAmzHeader('x-amz-acl', $acl); // Added rawurlencode() for $srcUri (thanks a.yamanoi)
657        $rest->setAmzHeader('x-amz-copy-source', sprintf('/%s/%s', $srcBucket, rawurlencode($srcUri)));
658        if (sizeof($requestHeaders) > 0 || sizeof($metaHeaders) > 0)
659            $rest->setAmzHeader('x-amz-metadata-directive', 'REPLACE');
660
661        $rest = $rest->getResponse();
662        if ($rest->error === false && $rest->code !== 200)
663            $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
664        if ($rest->error !== false)
665        {
666            self::__triggerError(sprintf("S3::copyObject({$srcBucket}, {$srcUri}, {$bucket}, {$uri}): [%s] %s",
667            $rest->error['code'], $rest->error['message']), __FILE__, __LINE__);
668            return false;
669        }
670        return isset($rest->body->LastModified, $rest->body->ETag) ? array(
671            'time' => strtotime((string)$rest->body->LastModified),
672            'hash' => substr((string)$rest->body->ETag, 1, -1)
673        ) : false;
674    }
675
676
677    /**
678    * Set logging for a bucket
679    *
680    * @param string $bucket Bucket name
681    * @param string $targetBucket Target bucket (where logs are stored)
682    * @param string $targetPrefix Log prefix (e,g; domain.com-)
683    * @return boolean
684    */
685    public static function setBucketLogging($bucket, $targetBucket, $targetPrefix = null)
686    {
687        // The S3 log delivery group has to be added to the target bucket's ACP
688        if ($targetBucket !== null && ($acp = self::getAccessControlPolicy($targetBucket, '')) !== false)
689        {
690            // Only add permissions to the target bucket when they do not exist
691            $aclWriteSet = false;
692            $aclReadSet = false;
693            foreach ($acp['acl'] as $acl)
694            if ($acl['type'] == 'Group' && $acl['uri'] == 'http://acs.amazonaws.com/groups/s3/LogDelivery')
695            {
696                if ($acl['permission'] == 'WRITE') $aclWriteSet = true;
697                elseif ($acl['permission'] == 'READ_ACP') $aclReadSet = true;
698            }
699            if (!$aclWriteSet) $acp['acl'][] = array(
700                'type' => 'Group', 'uri' => 'http://acs.amazonaws.com/groups/s3/LogDelivery', 'permission' => 'WRITE'
701            );
702            if (!$aclReadSet) $acp['acl'][] = array(
703                'type' => 'Group', 'uri' => 'http://acs.amazonaws.com/groups/s3/LogDelivery', 'permission' => 'READ_ACP'
704            );
705            if (!$aclReadSet || !$aclWriteSet) self::setAccessControlPolicy($targetBucket, '', $acp);
706        }
707
708        $dom = new DOMDocument;
709        $bucketLoggingStatus = $dom->createElement('BucketLoggingStatus');
710        $bucketLoggingStatus->setAttribute('xmlns', 'http://s3.amazonaws.com/doc/2006-03-01/');
711        if ($targetBucket !== null)
712        {
713            if ($targetPrefix == null) $targetPrefix = $bucket . '-';
714            $loggingEnabled = $dom->createElement('LoggingEnabled');
715            $loggingEnabled->appendChild($dom->createElement('TargetBucket', $targetBucket));
716            $loggingEnabled->appendChild($dom->createElement('TargetPrefix', $targetPrefix));
717            // TODO: Add TargetGrants?
718            $bucketLoggingStatus->appendChild($loggingEnabled);
719        }
720        $dom->appendChild($bucketLoggingStatus);
721
722        $rest = new S3Request('PUT', $bucket, '', self::$endpoint);
723        $rest->setParameter('logging', null);
724        $rest->data = $dom->saveXML();
725        $rest->size = strlen($rest->data);
726        $rest->setHeader('Content-Type', 'application/xml');
727        $rest = $rest->getResponse();
728        if ($rest->error === false && $rest->code !== 200)
729            $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
730        if ($rest->error !== false)
731        {
732            self::__triggerError(sprintf("S3::setBucketLogging({$bucket}, {$uri}): [%s] %s",
733            $rest->error['code'], $rest->error['message']), __FILE__, __LINE__);
734            return false;
735        }
736        return true;
737    }
738
739
740    /**
741    * Get logging status for a bucket
742    *
743    * This will return false if logging is not enabled.
744    * Note: To enable logging, you also need to grant write access to the log group
745    *
746    * @param string $bucket Bucket name
747    * @return array | false
748    */
749    public static function getBucketLogging($bucket)
750    {
751        $rest = new S3Request('GET', $bucket, '', self::$endpoint);
752        $rest->setParameter('logging', null);
753        $rest = $rest->getResponse();
754        if ($rest->error === false && $rest->code !== 200)
755            $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
756        if ($rest->error !== false)
757        {
758            self::__triggerError(sprintf("S3::getBucketLogging({$bucket}): [%s] %s",
759            $rest->error['code'], $rest->error['message']), __FILE__, __LINE__);
760            return false;
761        }
762        if (!isset($rest->body->LoggingEnabled)) return false; // No logging
763        return array(
764            'targetBucket' => (string)$rest->body->LoggingEnabled->TargetBucket,
765            'targetPrefix' => (string)$rest->body->LoggingEnabled->TargetPrefix,
766        );
767    }
768
769
770    /**
771    * Disable bucket logging
772    *
773    * @param string $bucket Bucket name
774    * @return boolean
775    */
776    public static function disableBucketLogging($bucket)
777    {
778        return self::setBucketLogging($bucket, null);
779    }
780
781
782    /**
783    * Get a bucket's location
784    *
785    * @param string $bucket Bucket name
786    * @return string | false
787    */
788    public static function getBucketLocation($bucket)
789    {
790        $rest = new S3Request('GET', $bucket, '', self::$endpoint);
791        $rest->setParameter('location', null);
792        $rest = $rest->getResponse();
793        if ($rest->error === false && $rest->code !== 200)
794            $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
795        if ($rest->error !== false)
796        {
797            self::__triggerError(sprintf("S3::getBucketLocation({$bucket}): [%s] %s",
798            $rest->error['code'], $rest->error['message']), __FILE__, __LINE__);
799            return false;
800        }
801        return (isset($rest->body[0]) && (string)$rest->body[0] !== '') ? (string)$rest->body[0] : 'US';
802    }
803
804
805    /**
806    * Set object or bucket Access Control Policy
807    *
808    * @param string $bucket Bucket name
809    * @param string $uri Object URI
810    * @param array $acp Access Control Policy Data (same as the data returned from getAccessControlPolicy)
811    * @return boolean
812    */
813    public static function setAccessControlPolicy($bucket, $uri = '', $acp = array())
814    {
815        $dom = new DOMDocument;
816        $dom->formatOutput = true;
817        $accessControlPolicy = $dom->createElement('AccessControlPolicy');
818        $accessControlList = $dom->createElement('AccessControlList');
819
820        // It seems the owner has to be passed along too
821        $owner = $dom->createElement('Owner');
822        $owner->appendChild($dom->createElement('ID', $acp['owner']['id']));
823        $owner->appendChild($dom->createElement('DisplayName', $acp['owner']['name']));
824        $accessControlPolicy->appendChild($owner);
825
826        foreach ($acp['acl'] as $g)
827        {
828            $grant = $dom->createElement('Grant');
829            $grantee = $dom->createElement('Grantee');
830            $grantee->setAttribute('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance');
831            if (isset($g['id']))
832            { // CanonicalUser (DisplayName is omitted)
833                $grantee->setAttribute('xsi:type', 'CanonicalUser');
834                $grantee->appendChild($dom->createElement('ID', $g['id']));
835            }
836            elseif (isset($g['email']))
837            { // AmazonCustomerByEmail
838                $grantee->setAttribute('xsi:type', 'AmazonCustomerByEmail');
839                $grantee->appendChild($dom->createElement('EmailAddress', $g['email']));
840            }
841            elseif ($g['type'] == 'Group')
842            { // Group
843                $grantee->setAttribute('xsi:type', 'Group');
844                $grantee->appendChild($dom->createElement('URI', $g['uri']));
845            }
846            $grant->appendChild($grantee);
847            $grant->appendChild($dom->createElement('Permission', $g['permission']));
848            $accessControlList->appendChild($grant);
849        }
850
851        $accessControlPolicy->appendChild($accessControlList);
852        $dom->appendChild($accessControlPolicy);
853
854        $rest = new S3Request('PUT', $bucket, $uri, self::$endpoint);
855        $rest->setParameter('acl', null);
856        $rest->data = $dom->saveXML();
857        $rest->size = strlen($rest->data);
858        $rest->setHeader('Content-Type', 'application/xml');
859        $rest = $rest->getResponse();
860        if ($rest->error === false && $rest->code !== 200)
861            $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
862        if ($rest->error !== false)
863        {
864            self::__triggerError(sprintf("S3::setAccessControlPolicy({$bucket}, {$uri}): [%s] %s",
865            $rest->error['code'], $rest->error['message']), __FILE__, __LINE__);
866            return false;
867        }
868        return true;
869    }
870
871
872    /**
873    * Get object or bucket Access Control Policy
874    *
875    * @param string $bucket Bucket name
876    * @param string $uri Object URI
877    * @return mixed | false
878    */
879    public static function getAccessControlPolicy($bucket, $uri = '')
880    {
881        $rest = new S3Request('GET', $bucket, $uri, self::$endpoint);
882        $rest->setParameter('acl', null);
883        $rest = $rest->getResponse();
884        if ($rest->error === false && $rest->code !== 200)
885            $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
886        if ($rest->error !== false)
887        {
888            self::__triggerError(sprintf("S3::getAccessControlPolicy({$bucket}, {$uri}): [%s] %s",
889            $rest->error['code'], $rest->error['message']), __FILE__, __LINE__);
890            return false;
891        }
892
893        $acp = array();
894        if (isset($rest->body->Owner, $rest->body->Owner->ID, $rest->body->Owner->DisplayName))
895            $acp['owner'] = array(
896                'id' => (string)$rest->body->Owner->ID, 'name' => (string)$rest->body->Owner->DisplayName
897            );
898
899        if (isset($rest->body->AccessControlList))
900        {
901            $acp['acl'] = array();
902            foreach ($rest->body->AccessControlList->Grant as $grant)
903            {
904                foreach ($grant->Grantee as $grantee)
905                {
906                    if (isset($grantee->ID, $grantee->DisplayName)) // CanonicalUser
907                        $acp['acl'][] = array(
908                            'type' => 'CanonicalUser',
909                            'id' => (string)$grantee->ID,
910                            'name' => (string)$grantee->DisplayName,
911                            'permission' => (string)$grant->Permission
912                        );
913                    elseif (isset($grantee->EmailAddress)) // AmazonCustomerByEmail
914                        $acp['acl'][] = array(
915                            'type' => 'AmazonCustomerByEmail',
916                            'email' => (string)$grantee->EmailAddress,
917                            'permission' => (string)$grant->Permission
918                        );
919                    elseif (isset($grantee->URI)) // Group
920                        $acp['acl'][] = array(
921                            'type' => 'Group',
922                            'uri' => (string)$grantee->URI,
923                            'permission' => (string)$grant->Permission
924                        );
925                    else continue;
926                }
927            }
928        }
929        return $acp;
930    }
931
932
933    /**
934    * Delete an object
935    *
936    * @param string $bucket Bucket name
937    * @param string $uri Object URI
938    * @return boolean
939    */
940    public static function deleteObject($bucket, $uri)
941    {
942        $rest = new S3Request('DELETE', $bucket, $uri, self::$endpoint);
943        $rest = $rest->getResponse();
944        if ($rest->error === false && $rest->code !== 204)
945            $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
946        if ($rest->error !== false)
947        {
948            self::__triggerError(sprintf("S3::deleteObject(): [%s] %s",
949            $rest->error['code'], $rest->error['message']), __FILE__, __LINE__);
950            return false;
951        }
952        return true;
953    }
954
955
956    /**
957    * Get a query string authenticated URL
958    *
959    * @param string $bucket Bucket name
960    * @param string $uri Object URI
961    * @param integer $lifetime Lifetime in seconds
962    * @param boolean $hostBucket Use the bucket name as the hostname
963    * @param boolean $https Use HTTPS ($hostBucket should be false for SSL verification)
964    * @return string
965    */
966    public static function getAuthenticatedURL($bucket, $uri, $lifetime, $hostBucket = false, $https = false)
967    {
968        $expires = time() + $lifetime;
969        $uri = str_replace('%2F', '/', rawurlencode($uri)); // URI should be encoded (thanks Sean O'Dea)
970        return sprintf(($https ? 'https' : 'http').'://%s/%s?AWSAccessKeyId=%s&Expires=%u&Signature=%s',
971        $hostBucket ? $bucket : $bucket.'.s3.amazonaws.com', $uri, self::$__accessKey, $expires,
972        urlencode(self::__getHash("GET\n\n\n{$expires}\n/{$bucket}/{$uri}")));
973    }
974
975
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;
987
988        $encoded = str_replace(array('+', '='), array('-', '_', '~'), base64_encode($data));
989        $signature = str_replace(array('+', '='), array('-', '_', '~'), base64_encode($signature));
990
991        $url = $policy['Statement'][0]['Resource'] . '?';
992
993        foreach (array('Policy' => $encoded, 'Signature' => $signature, 'Key-Pair-Id' => self::$__signingKeyPairId) as $k => $v)
994            $url .= $k.'='.str_replace('%2F', '/', rawurlencode($v)).'&';
995        return substr($url, 0, -1);
996    }
997
998
999    /**
1000    * Get a CloudFront canned policy URL
1001    *
1002    * @param string $string URL to sign
1003    * @param integer $lifetime URL lifetime
1004    * @return string
1005    */
1006    public static function getSignedCannedURL($url, $lifetime)
1007    {
1008        return self::getSignedPolicyURL(array(
1009            'Statement' => array(
1010                array('Resource' => $url, 'Condition' => array(
1011                    'DateLessThan' => array('AWS:EpochTime' => time() + $lifetime)
1012                ))
1013            )
1014        ));
1015    }
1016
1017
1018    /**
1019    * Get upload POST parameters for form uploads
1020    *
1021    * @param string $bucket Bucket name
1022    * @param string $uriPrefix Object URI prefix
1023    * @param constant $acl ACL constant
1024    * @param integer $lifetime Lifetime in seconds
1025    * @param integer $maxFileSize Maximum filesize in bytes (default 5MB)
1026    * @param string $successRedirect Redirect URL or 200 / 201 status code
1027    * @param array $amzHeaders Array of x-amz-meta-* headers
1028    * @param array $headers Array of request headers or content type as a string
1029    * @param boolean $flashVars Includes additional "Filename" variable posted by Flash
1030    * @return object
1031    */
1032    public static function getHttpUploadPostParams($bucket, $uriPrefix = '', $acl = self::ACL_PRIVATE, $lifetime = 3600,
1033    $maxFileSize = 5242880, $successRedirect = "201", $amzHeaders = array(), $headers = array(), $flashVars = false)
1034    {
1035        // Create policy object
1036        $policy = new stdClass;
1037        $policy->expiration = gmdate('Y-m-d\TH:i:s\Z', (time() + $lifetime));
1038        $policy->conditions = array();
1039        $obj = new stdClass; $obj->bucket = $bucket; array_push($policy->conditions, $obj);
1040        $obj = new stdClass; $obj->acl = $acl; array_push($policy->conditions, $obj);
1041
1042        $obj = new stdClass; // 200 for non-redirect uploads
1043        if (is_numeric($successRedirect) && in_array((int)$successRedirect, array(200, 201)))
1044            $obj->success_action_status = (string)$successRedirect;
1045        else // URL
1046            $obj->success_action_redirect = $successRedirect;
1047        array_push($policy->conditions, $obj);
1048
1049        if ($acl !== self::ACL_PUBLIC_READ)
1050            array_push($policy->conditions, array('eq', '$acl', $acl));
1051
1052        array_push($policy->conditions, array('starts-with', '$key', $uriPrefix));
1053        if ($flashVars) array_push($policy->conditions, array('starts-with', '$Filename', ''));
1054        foreach (array_keys($headers) as $headerKey)
1055            array_push($policy->conditions, array('starts-with', '$'.$headerKey, ''));
1056        foreach ($amzHeaders as $headerKey => $headerVal)
1057        {
1058            $obj = new stdClass;
1059            $obj->{$headerKey} = (string)$headerVal;
1060            array_push($policy->conditions, $obj);
1061        }
1062        array_push($policy->conditions, array('content-length-range', 0, $maxFileSize));
1063        $policy = base64_encode(str_replace('\/', '/', json_encode($policy)));
1064
1065        // Create parameters
1066        $params = new stdClass;
1067        $params->AWSAccessKeyId = self::$__accessKey;
1068        $params->key = $uriPrefix.'${filename}';
1069        $params->acl = $acl;
1070        $params->policy = $policy; unset($policy);
1071        $params->signature = self::__getHash($params->policy);
1072        if (is_numeric($successRedirect) && in_array((int)$successRedirect, array(200, 201)))
1073            $params->success_action_status = (string)$successRedirect;
1074        else
1075            $params->success_action_redirect = $successRedirect;
1076        foreach ($headers as $headerKey => $headerVal) $params->{$headerKey} = (string)$headerVal;
1077        foreach ($amzHeaders as $headerKey => $headerVal) $params->{$headerKey} = (string)$headerVal;
1078        return $params;
1079    }
1080
1081
1082    /**
1083    * Create a CloudFront distribution
1084    *
1085    * @param string $bucket Bucket name
1086    * @param boolean $enabled Enabled (true/false)
1087    * @param array $cnames Array containing CNAME aliases
1088    * @param string $comment Use the bucket name as the hostname
1089    * @param string $defaultRootObject Default root object
1090    * @param string $originAccessIdentity Origin access identity
1091    * @param array $trustedSigners Array of trusted signers
1092    * @return array | false
1093    */
1094    public static function createDistribution($bucket, $enabled = true, $cnames = array(), $comment = null, $defaultRootObject = null, $originAccessIdentity = null, $trustedSigners = array())
1095    {
1096        if (!extension_loaded('openssl'))
1097        {
1098            self::__triggerError(sprintf("S3::createDistribution({$bucket}, ".(int)$enabled.", [], '$comment'): %s",
1099            "CloudFront functionality requires SSL"), __FILE__, __LINE__);
1100            return false;
1101        }
1102        $useSSL = self::$useSSL;
1103
1104        self::$useSSL = true; // CloudFront requires SSL
1105        $rest = new S3Request('POST', '', '2010-11-01/distribution', 'cloudfront.amazonaws.com');
1106        $rest->data = self::__getCloudFrontDistributionConfigXML(
1107            $bucket.'.s3.amazonaws.com',
1108            $enabled,
1109            (string)$comment,
1110            (string)microtime(true),
1111            $cnames,
1112            $defaultRootObject,
1113            $originAccessIdentity,
1114            $trustedSigners
1115        );
1116
1117        $rest->size = strlen($rest->data);
1118        $rest->setHeader('Content-Type', 'application/xml');
1119        $rest = self::__getCloudFrontResponse($rest);
1120
1121        self::$useSSL = $useSSL;
1122
1123        if ($rest->error === false && $rest->code !== 201)
1124            $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
1125        if ($rest->error !== false)
1126        {
1127            self::__triggerError(sprintf("S3::createDistribution({$bucket}, ".(int)$enabled.", [], '$comment'): [%s] %s",
1128            $rest->error['code'], $rest->error['message']), __FILE__, __LINE__);
1129            return false;
1130        } elseif ($rest->body instanceof SimpleXMLElement)
1131            return self::__parseCloudFrontDistributionConfig($rest->body);
1132        return false;
1133    }
1134
1135
1136    /**
1137    * Get CloudFront distribution info
1138    *
1139    * @param string $distributionId Distribution ID from listDistributions()
1140    * @return array | false
1141    */
1142    public static function getDistribution($distributionId)
1143    {
1144        if (!extension_loaded('openssl'))
1145        {
1146            self::__triggerError(sprintf("S3::getDistribution($distributionId): %s",
1147            "CloudFront functionality requires SSL"), __FILE__, __LINE__);
1148            return false;
1149        }
1150        $useSSL = self::$useSSL;
1151
1152        self::$useSSL = true; // CloudFront requires SSL
1153        $rest = new S3Request('GET', '', '2010-11-01/distribution/'.$distributionId, 'cloudfront.amazonaws.com');
1154        $rest = self::__getCloudFrontResponse($rest);
1155
1156        self::$useSSL = $useSSL;
1157
1158        if ($rest->error === false && $rest->code !== 200)
1159            $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
1160        if ($rest->error !== false)
1161        {
1162            self::__triggerError(sprintf("S3::getDistribution($distributionId): [%s] %s",
1163            $rest->error['code'], $rest->error['message']), __FILE__, __LINE__);
1164            return false;
1165        }
1166        elseif ($rest->body instanceof SimpleXMLElement)
1167        {
1168            $dist = self::__parseCloudFrontDistributionConfig($rest->body);
1169            $dist['hash'] = $rest->headers['hash'];
1170            $dist['id'] = $distributionId;
1171            return $dist;
1172        }
1173        return false;
1174    }
1175
1176
1177    /**
1178    * Update a CloudFront distribution
1179    *
1180    * @param array $dist Distribution array info identical to output of getDistribution()
1181    * @return array | false
1182    */
1183    public static function updateDistribution($dist)
1184    {
1185        if (!extension_loaded('openssl'))
1186        {
1187            self::__triggerError(sprintf("S3::updateDistribution({$dist['id']}): %s",
1188            "CloudFront functionality requires SSL"), __FILE__, __LINE__);
1189            return false;
1190        }
1191
1192        $useSSL = self::$useSSL;
1193
1194        self::$useSSL = true; // CloudFront requires SSL
1195        $rest = new S3Request('PUT', '', '2010-11-01/distribution/'.$dist['id'].'/config', 'cloudfront.amazonaws.com');
1196        $rest->data = self::__getCloudFrontDistributionConfigXML(
1197            $dist['origin'],
1198            $dist['enabled'],
1199            $dist['comment'],
1200            $dist['callerReference'],
1201            $dist['cnames'],
1202            $dist['defaultRootObject'],
1203            $dist['originAccessIdentity'],
1204            $dist['trustedSigners']
1205        );
1206
1207        $rest->size = strlen($rest->data);
1208        $rest->setHeader('If-Match', $dist['hash']);
1209        $rest = self::__getCloudFrontResponse($rest);
1210
1211        self::$useSSL = $useSSL;
1212
1213        if ($rest->error === false && $rest->code !== 200)
1214            $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
1215        if ($rest->error !== false)
1216        {
1217            self::__triggerError(sprintf("S3::updateDistribution({$dist['id']}): [%s] %s",
1218            $rest->error['code'], $rest->error['message']), __FILE__, __LINE__);
1219            return false;
1220        } else {
1221            $dist = self::__parseCloudFrontDistributionConfig($rest->body);
1222            $dist['hash'] = $rest->headers['hash'];
1223            return $dist;
1224        }
1225        return false;
1226    }
1227
1228
1229    /**
1230    * Delete a CloudFront distribution
1231    *
1232    * @param array $dist Distribution array info identical to output of getDistribution()
1233    * @return boolean
1234    */
1235    public static function deleteDistribution($dist)
1236    {
1237        if (!extension_loaded('openssl'))
1238        {
1239            self::__triggerError(sprintf("S3::deleteDistribution({$dist['id']}): %s",
1240            "CloudFront functionality requires SSL"), __FILE__, __LINE__);
1241            return false;
1242        }
1243
1244        $useSSL = self::$useSSL;
1245
1246        self::$useSSL = true; // CloudFront requires SSL
1247        $rest = new S3Request('DELETE', '', '2008-06-30/distribution/'.$dist['id'], 'cloudfront.amazonaws.com');
1248        $rest->setHeader('If-Match', $dist['hash']);
1249        $rest = self::__getCloudFrontResponse($rest);
1250
1251        self::$useSSL = $useSSL;
1252
1253        if ($rest->error === false && $rest->code !== 204)
1254            $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
1255        if ($rest->error !== false)
1256        {
1257            self::__triggerError(sprintf("S3::deleteDistribution({$dist['id']}): [%s] %s",
1258            $rest->error['code'], $rest->error['message']), __FILE__, __LINE__);
1259            return false;
1260        }
1261        return true;
1262    }
1263
1264
1265    /**
1266    * Get a list of CloudFront distributions
1267    *
1268    * @return array
1269    */
1270    public static function listDistributions()
1271    {
1272        if (!extension_loaded('openssl'))
1273        {
1274            self::__triggerError(sprintf("S3::listDistributions(): [%s] %s",
1275            "CloudFront functionality requires SSL"), __FILE__, __LINE__);
1276            return false;
1277        }
1278
1279        $useSSL = self::$useSSL;
1280        self::$useSSL = true; // CloudFront requires SSL
1281        $rest = new S3Request('GET', '', '2010-11-01/distribution', 'cloudfront.amazonaws.com');
1282        $rest = self::__getCloudFrontResponse($rest);
1283        self::$useSSL = $useSSL;
1284
1285        if ($rest->error === false && $rest->code !== 200)
1286            $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
1287        if ($rest->error !== false)
1288        {
1289            self::__triggerError(sprintf("S3::listDistributions(): [%s] %s",
1290            $rest->error['code'], $rest->error['message']), __FILE__, __LINE__);
1291            return false;
1292        }
1293        elseif ($rest->body instanceof SimpleXMLElement && isset($rest->body->DistributionSummary))
1294        {
1295            $list = array();
1296            if (isset($rest->body->Marker, $rest->body->MaxItems, $rest->body->IsTruncated))
1297            {
1298                //$info['marker'] = (string)$rest->body->Marker;
1299                //$info['maxItems'] = (int)$rest->body->MaxItems;
1300                //$info['isTruncated'] = (string)$rest->body->IsTruncated == 'true' ? true : false;
1301            }
1302            foreach ($rest->body->DistributionSummary as $summary)
1303                $list[(string)$summary->Id] = self::__parseCloudFrontDistributionConfig($summary);
1304
1305            return $list;
1306        }
1307        return array();
1308    }
1309
1310    /**
1311    * List CloudFront Origin Access Identities
1312    *
1313    * @return array
1314    */
1315    public static function listOriginAccessIdentities()
1316    {
1317        if (!extension_loaded('openssl'))
1318        {
1319            self::__triggerError(sprintf("S3::listOriginAccessIdentities(): [%s] %s",
1320            "CloudFront functionality requires SSL"), __FILE__, __LINE__);
1321            return false;
1322        }
1323
1324        self::$useSSL = true; // CloudFront requires SSL
1325        $rest = new S3Request('GET', '', '2010-11-01/origin-access-identity/cloudfront', 'cloudfront.amazonaws.com');
1326        $rest = self::__getCloudFrontResponse($rest);
1327        $useSSL = self::$useSSL;
1328
1329        if ($rest->error === false && $rest->code !== 200)
1330            $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
1331        if ($rest->error !== false)
1332        {
1333            trigger_error(sprintf("S3::listOriginAccessIdentities(): [%s] %s",
1334            $rest->error['code'], $rest->error['message']), E_USER_WARNING);
1335            return false;
1336        }
1337
1338        if (isset($rest->body->CloudFrontOriginAccessIdentitySummary))
1339        {
1340            $identities = array();
1341            foreach ($rest->body->CloudFrontOriginAccessIdentitySummary as $identity)
1342                if (isset($identity->S3CanonicalUserId))
1343                    $identities[(string)$identity->Id] = array('id' => (string)$identity->Id, 's3CanonicalUserId' => (string)$identity->S3CanonicalUserId);
1344            return $identities;
1345        }
1346        return false;
1347    }
1348
1349
1350    /**
1351    * Invalidate objects in a CloudFront distribution
1352    *
1353    * Thanks to Martin Lindkvist for S3::invalidateDistribution()
1354    *
1355    * @param string $distributionId Distribution ID from listDistributions()
1356    * @param array $paths Array of object paths to invalidate
1357    * @return boolean
1358    */
1359    public static function invalidateDistribution($distributionId, $paths)
1360    {
1361        if (!extension_loaded('openssl'))
1362        {
1363            self::__triggerError(sprintf("S3::invalidateDistribution(): [%s] %s",
1364            "CloudFront functionality requires SSL"), __FILE__, __LINE__);
1365            return false;
1366        }
1367
1368        $useSSL = self::$useSSL;
1369        self::$useSSL = true; // CloudFront requires SSL
1370        $rest = new S3Request('POST', '', '2010-08-01/distribution/'.$distributionId.'/invalidation', 'cloudfront.amazonaws.com');
1371        $rest->data = self::__getCloudFrontInvalidationBatchXML($paths, (string)microtime(true));
1372        $rest->size = strlen($rest->data);
1373        $rest = self::__getCloudFrontResponse($rest);
1374        self::$useSSL = $useSSL;
1375
1376        if ($rest->error === false && $rest->code !== 201)
1377            $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
1378        if ($rest->error !== false)
1379        {
1380            trigger_error(sprintf("S3::invalidate('{$distributionId}',{$paths}): [%s] %s",
1381            $rest->error['code'], $rest->error['message']), E_USER_WARNING);
1382            return false;
1383        }
1384        return true;
1385    }
1386
1387
1388    /**
1389    * Get a InvalidationBatch DOMDocument
1390    *
1391    * @internal Used to create XML in invalidateDistribution()
1392    * @param array $paths Paths to objects to invalidateDistribution
1393    * @return string
1394    */
1395    private static function __getCloudFrontInvalidationBatchXML($paths, $callerReference = '0') {
1396        $dom = new DOMDocument('1.0', 'UTF-8');
1397        $dom->formatOutput = true;
1398        $invalidationBatch = $dom->createElement('InvalidationBatch');
1399        foreach ($paths as $path)
1400            $invalidationBatch->appendChild($dom->createElement('Path', $path));
1401
1402        $invalidationBatch->appendChild($dom->createElement('CallerReference', $callerReference));
1403        $dom->appendChild($invalidationBatch);
1404        return $dom->saveXML();
1405    }
1406
1407
1408    /**
1409    * Get a DistributionConfig DOMDocument
1410    *
1411    * http://docs.amazonwebservices.com/AmazonCloudFront/latest/APIReference/index.html?PutConfig.html
1412    *
1413    * @internal Used to create XML in createDistribution() and updateDistribution()
1414    * @param string $bucket S3 Origin bucket
1415    * @param boolean $enabled Enabled (true/false)
1416    * @param string $comment Comment to append
1417    * @param string $callerReference Caller reference
1418    * @param array $cnames Array of CNAME aliases
1419    * @param string $defaultRootObject Default root object
1420    * @param string $originAccessIdentity Origin access identity
1421    * @param array $trustedSigners Array of trusted signers
1422    * @return string
1423    */
1424    private static function __getCloudFrontDistributionConfigXML($bucket, $enabled, $comment, $callerReference = '0', $cnames = array(), $defaultRootObject = null, $originAccessIdentity = null, $trustedSigners = array())
1425    {
1426        $dom = new DOMDocument('1.0', 'UTF-8');
1427        $dom->formatOutput = true;
1428        $distributionConfig = $dom->createElement('DistributionConfig');
1429        $distributionConfig->setAttribute('xmlns', 'http://cloudfront.amazonaws.com/doc/2010-11-01/');
1430
1431        $origin = $dom->createElement('S3Origin');
1432        $origin->appendChild($dom->createElement('DNSName', $bucket));
1433        if ($originAccessIdentity !== null) $origin->appendChild($dom->createElement('OriginAccessIdentity', $originAccessIdentity));
1434        $distributionConfig->appendChild($origin);
1435
1436        if ($defaultRootObject !== null) $distributionConfig->appendChild($dom->createElement('DefaultRootObject', $defaultRootObject));
1437
1438        $distributionConfig->appendChild($dom->createElement('CallerReference', $callerReference));
1439        foreach ($cnames as $cname)
1440            $distributionConfig->appendChild($dom->createElement('CNAME', $cname));
1441        if ($comment !== '') $distributionConfig->appendChild($dom->createElement('Comment', $comment));
1442        $distributionConfig->appendChild($dom->createElement('Enabled', $enabled ? 'true' : 'false'));
1443
1444        $trusted = $dom->createElement('TrustedSigners');
1445        foreach ($trustedSigners as $id => $type)
1446            $trusted->appendChild($id !== '' ? $dom->createElement($type, $id) : $dom->createElement($type));
1447        $distributionConfig->appendChild($trusted);
1448
1449        $dom->appendChild($distributionConfig);
1450        //var_dump($dom->saveXML());
1451        return $dom->saveXML();
1452    }
1453
1454
1455    /**
1456    * Parse a CloudFront distribution config
1457    *
1458    * See http://docs.amazonwebservices.com/AmazonCloudFront/latest/APIReference/index.html?GetDistribution.html
1459    *
1460    * @internal Used to parse the CloudFront DistributionConfig node to an array
1461    * @param object &$node DOMNode
1462    * @return array
1463    */
1464    private static function __parseCloudFrontDistributionConfig(&$node)
1465    {
1466        if (isset($node->DistributionConfig))
1467            return self::__parseCloudFrontDistributionConfig($node->DistributionConfig);
1468
1469        $dist = array();
1470        if (isset($node->Id, $node->Status, $node->LastModifiedTime, $node->DomainName))
1471        {
1472            $dist['id'] = (string)$node->Id;
1473            $dist['status'] = (string)$node->Status;
1474            $dist['time'] = strtotime((string)$node->LastModifiedTime);
1475            $dist['domain'] = (string)$node->DomainName;
1476        }
1477
1478        if (isset($node->CallerReference))
1479            $dist['callerReference'] = (string)$node->CallerReference;
1480
1481        if (isset($node->Enabled))
1482            $dist['enabled'] = (string)$node->Enabled == 'true' ? true : false;
1483
1484        if (isset($node->S3Origin))
1485        {
1486            if (isset($node->S3Origin->DNSName))
1487                $dist['origin'] = (string)$node->S3Origin->DNSName;
1488
1489            $dist['originAccessIdentity'] = isset($node->S3Origin->OriginAccessIdentity) ?
1490            (string)$node->S3Origin->OriginAccessIdentity : null;
1491        }
1492
1493        $dist['defaultRootObject'] = isset($node->DefaultRootObject) ? (string)$node->DefaultRootObject : null;
1494
1495        $dist['cnames'] = array();
1496        if (isset($node->CNAME))
1497            foreach ($node->CNAME as $cname)
1498                $dist['cnames'][(string)$cname] = (string)$cname;
1499
1500        $dist['trustedSigners'] = array();
1501        if (isset($node->TrustedSigners))
1502            foreach ($node->TrustedSigners as $signer)
1503            {
1504                if (isset($signer->Self))
1505                    $dist['trustedSigners'][''] = 'Self';
1506                elseif (isset($signer->KeyPairId))
1507                    $dist['trustedSigners'][(string)$signer->KeyPairId] = 'KeyPairId';
1508                elseif (isset($signer->AwsAccountNumber))
1509                    $dist['trustedSigners'][(string)$signer->AwsAccountNumber] = 'AwsAccountNumber';
1510            }
1511
1512        $dist['comment'] = isset($node->Comment) ? (string)$node->Comment : null;
1513        return $dist;
1514    }
1515
1516
1517    /**
1518    * Grab CloudFront response
1519    *
1520    * @internal Used to parse the CloudFront S3Request::getResponse() output
1521    * @param object &$rest S3Request instance
1522    * @return object
1523    */
1524    private static function __getCloudFrontResponse(&$rest)
1525    {
1526        $rest->getResponse();
1527        if ($rest->response->error === false && isset($rest->response->body) &&
1528        is_string($rest->response->body) && substr($rest->response->body, 0, 5) == '<?xml')
1529        {
1530            $rest->response->body = simplexml_load_string($rest->response->body);
1531            // Grab CloudFront errors
1532            if (isset($rest->response->body->Error, $rest->response->body->Error->Code,
1533            $rest->response->body->Error->Message))
1534            {
1535                $rest->response->error = array(
1536                    'code' => (string)$rest->response->body->Error->Code,
1537                    'message' => (string)$rest->response->body->Error->Message
1538                );
1539                unset($rest->response->body);
1540            }
1541        }
1542        return $rest->response;
1543    }
1544
1545
1546    /**
1547    * Get MIME type for file
1548    *
1549    * @internal Used to get mime types
1550    * @param string &$file File path
1551    * @return string
1552    */
1553    public static function __getMimeType(&$file)
1554    {
1555        $type = false;
1556        // Fileinfo documentation says fileinfo_open() will use the
1557        // MAGIC env var for the magic file
1558        if (extension_loaded('fileinfo') && isset($_ENV['MAGIC']) &&
1559        ($finfo = finfo_open(FILEINFO_MIME, $_ENV['MAGIC'])) !== false)
1560        {
1561            if (($type = finfo_file($finfo, $file)) !== false)
1562            {
1563                // Remove the charset and grab the last content-type
1564                $type = explode(' ', str_replace('; charset=', ';charset=', $type));
1565                $type = array_pop($type);
1566                $type = explode(';', $type);
1567                $type = trim(array_shift($type));
1568            }
1569            finfo_close($finfo);
1570
1571        // If anyone is still using mime_content_type()
1572        } elseif (function_exists('mime_content_type'))
1573            $type = trim(mime_content_type($file));
1574
1575        if ($type !== false && strlen($type) > 0) return $type;
1576
1577        // Otherwise do it the old fashioned way
1578        static $exts = array(
1579            'jpg' => 'image/jpeg', 'gif' => 'image/gif', 'png' => 'image/png',
1580            'tif' => 'image/tiff', 'tiff' => 'image/tiff', 'ico' => 'image/x-icon',
1581            'swf' => 'application/x-shockwave-flash', 'pdf' => 'application/pdf',
1582            'zip' => 'application/zip', 'gz' => 'application/x-gzip',
1583            'tar' => 'application/x-tar', 'bz' => 'application/x-bzip',
1584            'bz2' => 'application/x-bzip2', 'txt' => 'text/plain',
1585            'asc' => 'text/plain', 'htm' => 'text/html', 'html' => 'text/html',
1586            'css' => 'text/css', 'js' => 'text/javascript',
1587            'xml' => 'text/xml', 'xsl' => 'application/xsl+xml',
1588            'ogg' => 'application/ogg', 'mp3' => 'audio/mpeg', 'wav' => 'audio/x-wav',
1589            'avi' => 'video/x-msvideo', 'mpg' => 'video/mpeg', 'mpeg' => 'video/mpeg',
1590            'mov' => 'video/quicktime', 'flv' => 'video/x-flv', 'php' => 'text/x-php'
1591        );
1592        $ext = strtolower(pathInfo($file, PATHINFO_EXTENSION));
1593        return isset($exts[$ext]) ? $exts[$ext] : 'application/octet-stream';
1594    }
1595
1596
1597    /**
1598    * Generate the auth string: "AWS AccessKey:Signature"
1599    *
1600    * @internal Used by S3Request::getResponse()
1601    * @param string $string String to sign
1602    * @return string
1603    */
1604    public static function __getSignature($string)
1605    {
1606        return 'AWS '.self::$__accessKey.':'.self::__getHash($string);
1607    }
1608
1609
1610    /**
1611    * Creates a HMAC-SHA1 hash
1612    *
1613    * This uses the hash extension if loaded
1614    *
1615    * @internal Used by __getSignature()
1616    * @param string $string String to sign
1617    * @return string
1618    */
1619    private static function __getHash($string)
1620    {
1621        return base64_encode(extension_loaded('hash') ?
1622        hash_hmac('sha1', $string, self::$__secretKey, true) : pack('H*', sha1(
1623        (str_pad(self::$__secretKey, 64, chr(0x00)) ^ (str_repeat(chr(0x5c), 64))) .
1624        pack('H*', sha1((str_pad(self::$__secretKey, 64, chr(0x00)) ^
1625        (str_repeat(chr(0x36), 64))) . $string)))));
1626    }
1627
1628}
1629
1630final class S3Request
1631{
1632    private $endpoint, $verb, $bucket, $uri, $resource = '', $parameters = array(),
1633    $amzHeaders = array(), $headers = array(
1634        'Host' => '', 'Date' => '', 'Content-MD5' => '', 'Content-Type' => ''
1635    );
1636    public $fp = false, $size = 0, $data = false, $response;
1637
1638
1639    /**
1640    * Constructor
1641    *
1642    * @param string $verb Verb
1643    * @param string $bucket Bucket name
1644    * @param string $uri Object URI
1645    * @return mixed
1646    */
1647    function __construct($verb, $bucket = '', $uri = '', $endpoint = 's3.amazonaws.com')
1648    {
1649        $this->endpoint = $endpoint;
1650        $this->verb = $verb;
1651        $this->bucket = $bucket;
1652        $this->uri = $uri !== '' ? '/'.str_replace('%2F', '/', rawurlencode($uri)) : '/';
1653
1654        if ($this->bucket !== '')
1655        {
1656            $this->headers['Host'] = $this->bucket.'.'.$this->endpoint;
1657            $this->resource = '/'.$this->bucket.$this->uri;
1658        }
1659        else
1660        {
1661            $this->headers['Host'] = $this->endpoint;
1662            $this->resource = $this->uri;
1663        }
1664        $this->headers['Date'] = gmdate('D, d M Y H:i:s T');
1665
1666        $this->response = new STDClass;
1667        $this->response->error = false;
1668    }
1669
1670
1671    /**
1672    * Set request parameter
1673    *
1674    * @param string $key Key
1675    * @param string $value Value
1676    * @return void
1677    */
1678    public function setParameter($key, $value)
1679    {
1680        $this->parameters[$key] = $value;
1681    }
1682
1683
1684    /**
1685    * Set request header
1686    *
1687    * @param string $key Key
1688    * @param string $value Value
1689    * @return void
1690    */
1691    public function setHeader($key, $value)
1692    {
1693        $this->headers[$key] = $value;
1694    }
1695
1696
1697    /**
1698    * Set x-amz-meta-* header
1699    *
1700    * @param string $key Key
1701    * @param string $value Value
1702    * @return void
1703    */
1704    public function setAmzHeader($key, $value)
1705    {
1706        $this->amzHeaders[$key] = $value;
1707    }
1708
1709
1710    /**
1711    * Get the S3 response
1712    *
1713    * @return object | false
1714    */
1715    public function getResponse()
1716    {
1717        $query = '';
1718        if (sizeof($this->parameters) > 0)
1719        {
1720            $query = substr($this->uri, -1) !== '?' ? '?' : '&';
1721            foreach ($this->parameters as $var => $value)
1722                if ($value == null || $value == '') $query .= $var.'&';
1723                // Parameters should be encoded (thanks Sean O'Dea)
1724                else $query .= $var.'='.rawurlencode($value).'&';
1725            $query = substr($query, 0, -1);
1726            $this->uri .= $query;
1727
1728            if (array_key_exists('acl', $this->parameters) ||
1729            array_key_exists('location', $this->parameters) ||
1730            array_key_exists('torrent', $this->parameters) ||
1731            array_key_exists('logging', $this->parameters))
1732                $this->resource .= $query;
1733        }
1734        $url = (S3::$useSSL ? 'https://' : 'http://') . $this->headers['Host'].$this->uri;
1735        //var_dump($this->bucket, $this->uri, $this->resource, $url);
1736
1737        // Basic setup
1738        $curl = curl_init();
1739        curl_setopt($curl, CURLOPT_USERAGENT, 'S3/php');
1740
1741        if (S3::$useSSL)
1742        {
1743            // SSL Validation can now be optional for those with broken OpenSSL installations
1744            curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, S3::$useSSLValidation ? 1 : 0);
1745            curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, S3::$useSSLValidation ? 1 : 0);
1746
1747            if (S3::$sslKey !== null) curl_setopt($curl, CURLOPT_SSLKEY, S3::$sslKey);
1748            if (S3::$sslCert !== null) curl_setopt($curl, CURLOPT_SSLCERT, S3::$sslCert);
1749            if (S3::$sslCACert !== null) curl_setopt($curl, CURLOPT_CAINFO, S3::$sslCACert);
1750        }
1751
1752        curl_setopt($curl, CURLOPT_URL, $url);
1753
1754        if (S3::$proxy != null && isset(S3::$proxy['host']))
1755        {
1756            curl_setopt($curl, CURLOPT_PROXY, S3::$proxy['host']);
1757            curl_setopt($curl, CURLOPT_PROXYTYPE, S3::$proxy['type']);
1758            if (isset(S3::$proxy['user'], S3::$proxy['pass']) && $proxy['user'] != null && $proxy['pass'] != null)
1759                curl_setopt($curl, CURLOPT_PROXYUSERPWD, sprintf('%s:%s', S3::$proxy['user'], S3::$proxy['pass']));
1760        }
1761
1762        // Headers
1763        $headers = array(); $amz = array();
1764        foreach ($this->amzHeaders as $header => $value)
1765            if (strlen($value) > 0) $headers[] = $header.': '.$value;
1766        foreach ($this->headers as $header => $value)
1767            if (strlen($value) > 0) $headers[] = $header.': '.$value;
1768
1769        // Collect AMZ headers for signature
1770        foreach ($this->amzHeaders as $header => $value)
1771            if (strlen($value) > 0) $amz[] = strtolower($header).':'.$value;
1772
1773        // AMZ headers must be sorted
1774        if (sizeof($amz) > 0)
1775        {
1776            sort($amz);
1777            $amz = "\n".implode("\n", $amz);
1778        } else $amz = '';
1779
1780        if (S3::hasAuth())
1781        {
1782            // Authorization string (CloudFront stringToSign should only contain a date)
1783            $headers[] = 'Authorization: ' . S3::__getSignature(
1784                $this->headers['Host'] == 'cloudfront.amazonaws.com' ? $this->headers['Date'] :
1785                $this->verb."\n".$this->headers['Content-MD5']."\n".
1786                $this->headers['Content-Type']."\n".$this->headers['Date'].$amz."\n".$this->resource
1787            );
1788        }
1789
1790        curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
1791        curl_setopt($curl, CURLOPT_HEADER, false);
1792        curl_setopt($curl, CURLOPT_RETURNTRANSFER, false);
1793        curl_setopt($curl, CURLOPT_WRITEFUNCTION, array(&$this, '__responseWriteCallback'));
1794        curl_setopt($curl, CURLOPT_HEADERFUNCTION, array(&$this, '__responseHeaderCallback'));
1795        curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
1796
1797        // Request types
1798        switch ($this->verb)
1799        {
1800            case 'GET': break;
1801            case 'PUT': case 'POST': // POST only used for CloudFront
1802                if ($this->fp !== false)
1803                {
1804                    curl_setopt($curl, CURLOPT_PUT, true);
1805                    curl_setopt($curl, CURLOPT_INFILE, $this->fp);
1806                    if ($this->size >= 0)
1807                        curl_setopt($curl, CURLOPT_INFILESIZE, $this->size);
1808                }
1809                elseif ($this->data !== false)
1810                {
1811                    curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $this->verb);
1812                    curl_setopt($curl, CURLOPT_POSTFIELDS, $this->data);
1813                }
1814                else
1815                    curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $this->verb);
1816            break;
1817            case 'HEAD':
1818                curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'HEAD');
1819                curl_setopt($curl, CURLOPT_NOBODY, true);
1820            break;
1821            case 'DELETE':
1822                curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'DELETE');
1823            break;
1824            default: break;
1825        }
1826
1827        // Execute, grab errors
1828        if (curl_exec($curl))
1829            $this->response->code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
1830        else
1831            $this->response->error = array(
1832                'code' => curl_errno($curl),
1833                'message' => curl_error($curl),
1834                'resource' => $this->resource
1835            );
1836
1837        @curl_close($curl);
1838
1839        // Parse body into XML
1840        if ($this->response->error === false && isset($this->response->headers['type']) &&
1841        $this->response->headers['type'] == 'application/xml' && isset($this->response->body))
1842        {
1843            $this->response->body = simplexml_load_string($this->response->body);
1844
1845            // Grab S3 errors
1846            if (!in_array($this->response->code, array(200, 204, 206)) &&
1847            isset($this->response->body->Code, $this->response->body->Message))
1848            {
1849                $this->response->error = array(
1850                    'code' => (string)$this->response->body->Code,
1851                    'message' => (string)$this->response->body->Message
1852                );
1853                if (isset($this->response->body->Resource))
1854                    $this->response->error['resource'] = (string)$this->response->body->Resource;
1855                unset($this->response->body);
1856            }
1857        }
1858
1859        // Clean up file resources
1860        if ($this->fp !== false && is_resource($this->fp)) fclose($this->fp);
1861
1862        return $this->response;
1863    }
1864
1865
1866    /**
1867    * CURL write callback
1868    *
1869    * @param resource &$curl CURL resource
1870    * @param string &$data Data
1871    * @return integer
1872    */
1873    private function __responseWriteCallback(&$curl, &$data)
1874    {
1875        if (in_array($this->response->code, array(200, 206)) && $this->fp !== false)
1876            return fwrite($this->fp, $data);
1877        else
1878            $this->response->body .= $data;
1879        return strlen($data);
1880    }
1881
1882
1883    /**
1884    * CURL header callback
1885    *
1886    * @param resource &$curl CURL resource
1887    * @param string &$data Data
1888    * @return integer
1889    */
1890    private function __responseHeaderCallback(&$curl, &$data)
1891    {
1892        if (($strlen = strlen($data)) <= 2) return $strlen;
1893        if (substr($data, 0, 4) == 'HTTP')
1894            $this->response->code = (int)substr($data, 9, 3);
1895        else
1896        {
1897            $data = trim($data);
1898            if (strpos($data, ': ') === false) return $strlen;
1899            list($header, $value) = explode(': ', $data, 2);
1900            if ($header == 'Last-Modified')
1901                $this->response->headers['time'] = strtotime($value);
1902            elseif ($header == 'Content-Length')
1903                $this->response->headers['size'] = (int)$value;
1904            elseif ($header == 'Content-Type')
1905                $this->response->headers['type'] = $value;
1906            elseif ($header == 'ETag')
1907                $this->response->headers['hash'] = $value{0} == '"' ? substr($value, 1, -1) : $value;
1908            elseif (preg_match('/^x-amz-meta-.*$/', $header))
1909                $this->response->headers[$header] = is_numeric($value) ? (int)$value : $value;
1910        }
1911        return $strlen;
1912    }
1913
1914}
1915
1916class S3Exception extends Exception {
1917    function __construct($message, $file, $line, $code = 0)
1918    {
1919        parent::__construct($message, $code);
1920        $this->file = $file;
1921        $this->line = $line;
1922    }
1923}
Note: See TracBrowser for help on using the repository browser.