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