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