/libraries/s3.php

https://github.com/SixPonyHitch/CodeIgniter-Amazon-S3-SDK · PHP · 2052 lines · 1269 code · 236 blank · 547 comment · 275 complexity · e64bf91284f27d4c0fbc3f105bbcdebe MD5 · raw file

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