PageRenderTime 36ms CodeModel.GetById 20ms RepoModel.GetById 1ms app.codeStats 0ms

/repository/s3/S3.php

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