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