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