PluginProbe
UpdraftPlus: WP Backup & Migration Plugin / 1.2.39
UpdraftPlus: WP Backup & Migration Plugin v1.2.39
1.26.7 1.26.6 1.26.5 1.26.4 1.26.3 1.9.19 1.9.25 1.9.26 1.9.30 1.9.31 1.9.32 1.9.4 1.9.40 1.9.41 1.9.42 1.9.43 1.9.44 1.9.45 1.9.46 1.9.5 1.9.50 1.9.51 1.9.60 1.9.62 1.9.63 All 371 releases
updraftplus / includes / S3.php

S3.php in UpdraftPlus: WP Backup & Migration Plugin 1.2.39, at includes/S3.php

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