PluginProbe
ManageWP Worker / 3.9.28
ManageWP Worker v3.9.28
4.9.38 4.9.37 4.9.36 4.9.35 4.9.34 3.8.7 3.8.8 3.9.0 3.9.1 3.9.10 3.9.11 3.9.12 3.9.13 3.9.14 3.9.15 3.9.16 3.9.17 3.9.18 3.9.19 3.9.2 3.9.20 3.9.21 3.9.22 3.9.23 3.9.24 All 73 releases
worker / lib / s3.php

s3.php in ManageWP Worker 3.9.28, at lib/s3.php

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