PluginProbe
InfiniteWP Client / trunk
InfiniteWP Client vtrunk
1.13.10 1.13.7 trunk 0.1.4 0.1.5 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.1.0 1.1.1 1.1.10 1.1.2 1.1.3 1.1.4 1.1.5 1.1.6 1.1.7 1.1.8 1.1.9 1.11.0 1.11.1 1.12.1 1.12.3 All 92 releases
iwp-client / lib / S3.php

S3.php in InfiniteWP Client trunk, at lib/S3.php

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