PageRenderTime 71ms CodeModel.GetById 29ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/Milk/Utils/S3.php

https://github.com/geekbuntu/milk
PHP | 1338 lines | 828 code | 146 blank | 364 comment | 216 complexity | 125a12c230f166f3ef1f27e4f6a6ff75 MD5 | raw file
Possible License(s): GPL-2.0

Large files files are truncated, but you can click here to view the full file

  1. <?php
  2. namespace Milk\Utils;
  3. /**
  4. Amazon S3 util
  5. Modified and adapted for use with Milk
  6. Note: This will later be replaced by a proper consumer class
  7. Project Milk
  8. @copyright 2011, Team Project Milk
  9. @author Marcus Ekwall <marcus.ekwall@gmail.com>
  10. Original Amazon S3 PHP class
  11. @copyright 2008, Donovan Schönknecht
  12. @author Donovan Schönknecht
  13. Copyright (c) 2008. All rights reserved.
  14. Redistribution and use in source and binary forms, with or without
  15. modification, are permitted provided that the following conditions are met:
  16. - Redistributions of source code must retain the above copyright notice,
  17. this list of conditions and the following disclaimer.
  18. - Redistributions in binary form must reproduce the above copyright
  19. notice, this list of conditions and the following disclaimer in the
  20. documentation and/or other materials provided with the distribution.
  21. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
  22. AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  23. IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  24. ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
  25. LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
  26. CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
  27. SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
  28. INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
  29. CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  30. ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  31. POSSIBILITY OF SUCH DAMAGE.
  32. Amazon S3 is a trademark of Amazon.com, Inc. or its affiliates.
  33. **/
  34. use \StdClass,
  35. \DOMDocument,
  36. \SimpleXMLElement;
  37. use Milk\Utils\Files;
  38. /**
  39. Amazon S3 PHP class
  40. @link http://undesigned.org.za/2007/10/22/amazon-s3-php-class
  41. @version 0.4.0
  42. **/
  43. class S3 {
  44. // ACL flags
  45. const ACL_PRIVATE = 'private';
  46. const ACL_PUBLIC_READ = 'public-read';
  47. const ACL_PUBLIC_READ_WRITE = 'public-read-write';
  48. const ACL_AUTHENTICATED_READ = 'authenticated-read';
  49. public static $useSSL = true;
  50. private static $__accessKey; // AWS Access key
  51. private static $__secretKey; // AWS Secret key
  52. /**
  53. Constructor - if you're not using the class statically
  54. @param string $accessKey Access key
  55. @param string $secretKey Secret key
  56. @param boolean $useSSL Enable SSL
  57. @return void
  58. **/
  59. public function __construct($accessKey = null, $secretKey = null, $useSSL = true) {
  60. if ($accessKey !== null && $secretKey !== null)
  61. self::setAuth($accessKey, $secretKey);
  62. self::$useSSL = $useSSL;
  63. }
  64. /**
  65. Set AWS access key and secret key
  66. @param string $accessKey Access key
  67. @param string $secretKey Secret key
  68. @return void
  69. **/
  70. public static function setAuth($accessKey, $secretKey) {
  71. self::$__accessKey = $accessKey;
  72. self::$__secretKey = $secretKey;
  73. }
  74. /**
  75. Get a list of buckets
  76. @param boolean $detailed Returns detailed bucket list when true
  77. @return array | false
  78. **/
  79. public static function listBuckets($detailed = false) {
  80. $rest = new S3Request('GET', '', '');
  81. $rest = $rest->getResponse();
  82. if ($rest->error === false && $rest->code !== 200)
  83. $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
  84. if ($rest->error !== false) {
  85. trigger_error(sprintf("S3::listBuckets(): [%s] %s", $rest->error['code'], $rest->error['message']), E_USER_WARNING);
  86. return false;
  87. }
  88. $results = array();
  89. if (!isset($rest->body->Buckets)) return $results;
  90. if ($detailed) {
  91. if (isset($rest->body->Owner, $rest->body->Owner->ID, $rest->body->Owner->DisplayName))
  92. $results['owner'] = array(
  93. 'id' => (string)$rest->body->Owner->ID, 'name' => (string)$rest->body->Owner->ID
  94. );
  95. $results['buckets'] = array();
  96. foreach ($rest->body->Buckets->Bucket as $b)
  97. $results['buckets'][] = array(
  98. 'name' => (string)$b->Name, 'time' => strtotime((string)$b->CreationDate)
  99. );
  100. } else
  101. foreach ($rest->body->Buckets->Bucket as $b) $results[] = (string)$b->Name;
  102. return $results;
  103. }
  104. /**
  105. Get contents for a bucket
  106. If maxKeys is null this method will loop through truncated result sets
  107. @param string $bucket Bucket name
  108. @param string $prefix Prefix
  109. @param string $marker Marker (last file listed)
  110. @param string $maxKeys Max keys (maximum number of keys to return)
  111. @param string $delimiter Delimiter
  112. @param boolean $returnCommonPrefixes Set to true to return CommonPrefixes
  113. @return array | false
  114. **/
  115. public static function getBucket($bucket, $prefix = null, $marker = null, $maxKeys = null, $delimiter = null, $returnCommonPrefixes = false) {
  116. $rest = new S3Request('GET', $bucket, '');
  117. if ($prefix !== null && $prefix !== '') $rest->setParameter('prefix', $prefix);
  118. if ($marker !== null && $marker !== '') $rest->setParameter('marker', $marker);
  119. if ($maxKeys !== null && $maxKeys !== '') $rest->setParameter('max-keys', $maxKeys);
  120. if ($delimiter !== null && $delimiter !== '') $rest->setParameter('delimiter', $delimiter);
  121. $response = $rest->getResponse();
  122. if ($response->error === false && $response->code !== 200)
  123. $response->error = array('code' => $response->code, 'message' => 'Unexpected HTTP status');
  124. if ($response->error !== false) {
  125. trigger_error(sprintf("S3::getBucket(): [%s] %s", $response->error['code'], $response->error['message']), E_USER_WARNING);
  126. return false;
  127. }
  128. $results = array();
  129. $nextMarker = null;
  130. if (isset($response->body, $response->body->Contents))
  131. foreach ($response->body->Contents as $c) {
  132. $results[(string)$c->Key] = array(
  133. 'name' => (string)$c->Key,
  134. 'time' => strtotime((string)$c->LastModified),
  135. 'size' => (int)$c->Size,
  136. 'hash' => substr((string)$c->ETag, 1, -1)
  137. );
  138. $nextMarker = (string)$c->Key;
  139. }
  140. if ($returnCommonPrefixes && isset($response->body, $response->body->CommonPrefixes))
  141. foreach ($response->body->CommonPrefixes as $c)
  142. $results[(string)$c->Prefix] = array('prefix' => (string)$c->Prefix);
  143. if (isset($response->body, $response->body->IsTruncated) &&
  144. (string)$response->body->IsTruncated == 'false') return $results;
  145. if (isset($response->body, $response->body->NextMarker))
  146. $nextMarker = (string)$response->body->NextMarker;
  147. // Loop through truncated results if maxKeys isn't specified
  148. if ($maxKeys == null && $nextMarker !== null && (string)$response->body->IsTruncated == 'true')
  149. do {
  150. $rest = new S3Request('GET', $bucket, '');
  151. if ($prefix !== null && $prefix !== '') $rest->setParameter('prefix', $prefix);
  152. $rest->setParameter('marker', $nextMarker);
  153. if ($delimiter !== null && $delimiter !== '') $rest->setParameter('delimiter', $delimiter);
  154. if (($response = $rest->getResponse(true)) == false || $response->code !== 200) break;
  155. if (isset($response->body, $response->body->Contents))
  156. foreach ($response->body->Contents as $c) {
  157. $results[(string)$c->Key] = array(
  158. 'name' => (string)$c->Key,
  159. 'time' => strtotime((string)$c->LastModified),
  160. 'size' => (int)$c->Size,
  161. 'hash' => substr((string)$c->ETag, 1, -1)
  162. );
  163. $nextMarker = (string)$c->Key;
  164. }
  165. if ($returnCommonPrefixes && isset($response->body, $response->body->CommonPrefixes))
  166. foreach ($response->body->CommonPrefixes as $c)
  167. $results[(string)$c->Prefix] = array('prefix' => (string)$c->Prefix);
  168. if (isset($response->body, $response->body->NextMarker))
  169. $nextMarker = (string)$response->body->NextMarker;
  170. } while ($response !== false && (string)$response->body->IsTruncated == 'true');
  171. return $results;
  172. }
  173. /**
  174. Put a bucket
  175. @param string $bucket Bucket name
  176. @param constant $acl ACL flag
  177. @param string $location Set as "EU" to create buckets hosted in Europe
  178. @return boolean
  179. **/
  180. public static function putBucket($bucket, $acl = self::ACL_PRIVATE, $location = false) {
  181. $rest = new S3Request('PUT', $bucket, '');
  182. $rest->setAmzHeader('x-amz-acl', $acl);
  183. if ($location !== false) {
  184. $dom = new DOMDocument;
  185. $createBucketConfiguration = $dom->createElement('CreateBucketConfiguration');
  186. $locationConstraint = $dom->createElement('LocationConstraint', strtoupper($location));
  187. $createBucketConfiguration->appendChild($locationConstraint);
  188. $dom->appendChild($createBucketConfiguration);
  189. $rest->data = $dom->saveXML();
  190. $rest->size = strlen($rest->data);
  191. $rest->setHeader('Content-Type', 'application/xml');
  192. }
  193. $rest = $rest->getResponse();
  194. if ($rest->error === false && $rest->code !== 200)
  195. $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
  196. if ($rest->error !== false) {
  197. trigger_error(sprintf("S3::putBucket({$bucket}, {$acl}, {$location}): [%s] %s",
  198. $rest->error['code'], $rest->error['message']), E_USER_WARNING);
  199. return false;
  200. }
  201. return true;
  202. }
  203. /**
  204. Delete an empty bucket
  205. @param string $bucket Bucket name
  206. @return boolean
  207. **/
  208. public static function deleteBucket($bucket) {
  209. $rest = new S3Request('DELETE', $bucket);
  210. $rest = $rest->getResponse();
  211. if ($rest->error === false && $rest->code !== 204)
  212. $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
  213. if ($rest->error !== false) {
  214. trigger_error(sprintf("S3::deleteBucket({$bucket}): [%s] %s",
  215. $rest->error['code'], $rest->error['message']), E_USER_WARNING);
  216. return false;
  217. }
  218. return true;
  219. }
  220. /**
  221. Create input info array for putObject()
  222. @param string $file Input file
  223. @param mixed $md5sum Use MD5 hash (supply a string if you want to use your own)
  224. @return array | false
  225. **/
  226. public static function inputFile($file, $md5sum = true) {
  227. if (!file_exists($file) || !is_file($file) || !is_readable($file)) {
  228. trigger_error('S3::inputFile(): Unable to open input file: '.$file, E_USER_WARNING);
  229. return false;
  230. }
  231. return array('file' => $file, 'size' => filesize($file),
  232. 'md5sum' => $md5sum !== false ? (is_string($md5sum) ? $md5sum :
  233. base64_encode(md5_file($file, true))) : '');
  234. }
  235. /**
  236. Create input array info for putObject() with a resource
  237. @param string $resource Input resource to read from
  238. @param integer $bufferSize Input byte size
  239. @param string $md5sum MD5 hash to send (optional)
  240. @return array | false
  241. **/
  242. public static function inputResource(&$resource, $bufferSize, $md5sum = '') {
  243. if (!is_resource($resource) || $bufferSize < 0) {
  244. trigger_error('S3::inputResource(): Invalid resource or buffer size', E_USER_WARNING);
  245. return false;
  246. }
  247. $input = array('size' => $bufferSize, 'md5sum' => $md5sum);
  248. $input['fp'] =& $resource;
  249. return $input;
  250. }
  251. /**
  252. Put an object
  253. @param mixed $input Input data
  254. @param string $bucket Bucket name
  255. @param string $uri Object URI
  256. @param constant $acl ACL constant
  257. @param array $metaHeaders Array of x-amz-meta-* headers
  258. @param array $requestHeaders Array of request headers or content type as a string
  259. @return boolean
  260. **/
  261. public static function putObject($input, $bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $requestHeaders = array()) {
  262. if ($input === false) return false;
  263. $rest = new S3Request('PUT', $bucket, $uri);
  264. if (is_string($input)) $input = array(
  265. 'data' => $input, 'size' => strlen($input),
  266. 'md5sum' => base64_encode(md5($input, true))
  267. );
  268. // Data
  269. if (isset($input['fp']))
  270. $rest->fp =& $input['fp'];
  271. elseif (isset($input['file']))
  272. $rest->fp = @fopen($input['file'], 'rb');
  273. elseif (isset($input['data']))
  274. $rest->data = $input['data'];
  275. // Content-Length (required)
  276. if (isset($input['size']) && $input['size'] >= 0)
  277. $rest->size = $input['size'];
  278. else {
  279. if (isset($input['file']))
  280. $rest->size = filesize($input['file']);
  281. elseif (isset($input['data']))
  282. $rest->size = strlen($input['data']);
  283. }
  284. // Custom request headers (Content-Type, Content-Disposition, Content-Encoding)
  285. if (is_array($requestHeaders))
  286. foreach ($requestHeaders as $h => $v) $rest->setHeader($h, $v);
  287. elseif (is_string($requestHeaders)) // Support for legacy contentType parameter
  288. $input['type'] = $requestHeaders;
  289. // Content-Type
  290. if (!isset($input['type'])) {
  291. if (isset($requestHeaders['Content-Type']))
  292. $input['type'] =& $requestHeaders['Content-Type'];
  293. elseif (isset($input['file']))
  294. $input['type'] = Files::getMimeType($input['file']);
  295. else
  296. $input['type'] = 'application/octet-stream';
  297. }
  298. // We need to post with Content-Length and Content-Type, MD5 is optional
  299. if ($rest->size >= 0 && ($rest->fp !== false || $rest->data !== false)) {
  300. $rest->setHeader('Content-Type', $input['type']);
  301. if (isset($input['md5sum'])) $rest->setHeader('Content-MD5', $input['md5sum']);
  302. $rest->setAmzHeader('x-amz-acl', $acl);
  303. foreach ($metaHeaders as $h => $v) $rest->setAmzHeader('x-amz-meta-'.$h, $v);
  304. $rest->getResponse();
  305. } else
  306. $rest->response->error = array('code' => 0, 'message' => 'Missing input parameters');
  307. if ($rest->response->error === false && $rest->response->code !== 200)
  308. $rest->response->error = array('code' => $rest->response->code, 'message' => 'Unexpected HTTP status');
  309. if ($rest->response->error !== false) {
  310. trigger_error(sprintf("S3::putObject(): [%s] %s", $rest->response->error['code'], $rest->response->error['message']), E_USER_WARNING);
  311. return false;
  312. }
  313. return true;
  314. }
  315. /**
  316. Put an object from a file (legacy function)
  317. @param string $file Input file path
  318. @param string $bucket Bucket name
  319. @param string $uri Object URI
  320. @param constant $acl ACL constant
  321. @param array $metaHeaders Array of x-amz-meta-* headers
  322. @param string $contentType Content type
  323. @return boolean
  324. **/
  325. public static function putObjectFile($file, $bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $contentType = null) {
  326. return self::putObject(self::inputFile($file), $bucket, $uri, $acl, $metaHeaders, $contentType);
  327. }
  328. /**
  329. Put an object from a string (legacy function)
  330. @param string $string Input data
  331. @param string $bucket Bucket name
  332. @param string $uri Object URI
  333. @param constant $acl ACL constant
  334. @param array $metaHeaders Array of x-amz-meta-* headers
  335. @param string $contentType Content type
  336. @return boolean
  337. **/
  338. public static function putObjectString($string, $bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $contentType = 'text/plain') {
  339. return self::putObject($string, $bucket, $uri, $acl, $metaHeaders, $contentType);
  340. }
  341. /**
  342. Get an object
  343. @param string $bucket Bucket name
  344. @param string $uri Object URI
  345. @param mixed $saveTo Filename or resource to write to
  346. @return mixed
  347. **/
  348. public static function getObject($bucket, $uri, $saveTo = false) {
  349. $rest = new S3Request('GET', $bucket, $uri);
  350. if ($saveTo !== false) {
  351. if (is_resource($saveTo))
  352. $rest->fp =& $saveTo;
  353. else
  354. if (($rest->fp = @fopen($saveTo, 'wb')) !== false)
  355. $rest->file = realpath($saveTo);
  356. else
  357. $rest->response->error = array('code' => 0, 'message' => 'Unable to open save file for writing: '.$saveTo);
  358. }
  359. if ($rest->response->error === false) $rest->getResponse();
  360. if ($rest->response->error === false && $rest->response->code !== 200)
  361. $rest->response->error = array('code' => $rest->response->code, 'message' => 'Unexpected HTTP status');
  362. if ($rest->response->error !== false) {
  363. trigger_error(sprintf("S3::getObject({$bucket}, {$uri}): [%s] %s",
  364. $rest->response->error['code'], $rest->response->error['message']), E_USER_WARNING);
  365. return false;
  366. }
  367. return $rest->response;
  368. }
  369. /**
  370. Get object information
  371. @param string $bucket Bucket name
  372. @param string $uri Object URI
  373. @param boolean $returnInfo Return response information
  374. @return mixed | false
  375. **/
  376. public static function getObjectInfo($bucket, $uri, $returnInfo = true) {
  377. $rest = new S3Request('HEAD', $bucket, $uri);
  378. $rest = $rest->getResponse();
  379. if ($rest->error === false && ($rest->code !== 200 && $rest->code !== 404))
  380. $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
  381. if ($rest->error !== false) {
  382. trigger_error(sprintf("S3::getObjectInfo({$bucket}, {$uri}): [%s] %s",
  383. $rest->error['code'], $rest->error['message']), E_USER_WARNING);
  384. return false;
  385. }
  386. return $rest->code == 200 ? $returnInfo ? $rest->headers : true : false;
  387. }
  388. /**
  389. Copy an object
  390. @param string $bucket Source bucket name
  391. @param string $uri Source object URI
  392. @param string $bucket Destination bucket name
  393. @param string $uri Destination object URI
  394. @param constant $acl ACL constant
  395. @param array $metaHeaders Optional array of x-amz-meta-* headers
  396. @param array $requestHeaders Optional array of request headers (content type, disposition, etc.)
  397. @return mixed | false
  398. **/
  399. public static function copyObject($srcBucket, $srcUri, $bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $requestHeaders = array()) {
  400. $rest = new S3Request('PUT', $bucket, $uri);
  401. $rest->setHeader('Content-Length', 0);
  402. foreach ($requestHeaders as $h => $v) $rest->setHeader($h, $v);
  403. foreach ($metaHeaders as $h => $v) $rest->setAmzHeader('x-amz-meta-'.$h, $v);
  404. $rest->setAmzHeader('x-amz-acl', $acl);
  405. $rest->setAmzHeader('x-amz-copy-source', sprintf('/%s/%s', $srcBucket, $srcUri));
  406. if (sizeof($requestHeaders) > 0 || sizeof($metaHeaders) > 0)
  407. $rest->setAmzHeader('x-amz-metadata-directive', 'REPLACE');
  408. $rest = $rest->getResponse();
  409. if ($rest->error === false && $rest->code !== 200)
  410. $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
  411. if ($rest->error !== false) {
  412. trigger_error(sprintf("S3::copyObject({$srcBucket}, {$srcUri}, {$bucket}, {$uri}): [%s] %s",
  413. $rest->error['code'], $rest->error['message']), E_USER_WARNING);
  414. return false;
  415. }
  416. return isset($rest->body->LastModified, $rest->body->ETag) ? array(
  417. 'time' => strtotime((string)$rest->body->LastModified),
  418. 'hash' => substr((string)$rest->body->ETag, 1, -1)
  419. ) : false;
  420. }
  421. /**
  422. Set logging for a bucket
  423. @param string $bucket Bucket name
  424. @param string $targetBucket Target bucket (where logs are stored)
  425. @param string $targetPrefix Log prefix (e,g; domain.com-)
  426. @return boolean
  427. **/
  428. public static function setBucketLogging($bucket, $targetBucket, $targetPrefix = null) {
  429. // The S3 log delivery group has to be added to the target bucket's ACP
  430. if ($targetBucket !== null && ($acp = self::getAccessControlPolicy($targetBucket, '')) !== false) {
  431. // Only add permissions to the target bucket when they do not exist
  432. $aclWriteSet = false;
  433. $aclReadSet = false;
  434. foreach ($acp['acl'] as $acl)
  435. if ($acl['type'] == 'Group' && $acl['uri'] == 'http://acs.amazonaws.com/groups/s3/LogDelivery') {
  436. if ($acl['permission'] == 'WRITE') $aclWriteSet = true;
  437. elseif ($acl['permission'] == 'READ_ACP') $aclReadSet = true;
  438. }
  439. if (!$aclWriteSet) $acp['acl'][] = array(
  440. 'type' => 'Group', 'uri' => 'http://acs.amazonaws.com/groups/s3/LogDelivery', 'permission' => 'WRITE'
  441. );
  442. if (!$aclReadSet) $acp['acl'][] = array(
  443. 'type' => 'Group', 'uri' => 'http://acs.amazonaws.com/groups/s3/LogDelivery', 'permission' => 'READ_ACP'
  444. );
  445. if (!$aclReadSet || !$aclWriteSet) self::setAccessControlPolicy($targetBucket, '', $acp);
  446. }
  447. $dom = new DOMDocument;
  448. $bucketLoggingStatus = $dom->createElement('BucketLoggingStatus');
  449. $bucketLoggingStatus->setAttribute('xmlns', 'http://s3.amazonaws.com/doc/2006-03-01/');
  450. if ($targetBucket !== null) {
  451. if ($targetPrefix == null) $targetPrefix = $bucket . '-';
  452. $loggingEnabled = $dom->createElement('LoggingEnabled');
  453. $loggingEnabled->appendChild($dom->createElement('TargetBucket', $targetBucket));
  454. $loggingEnabled->appendChild($dom->createElement('TargetPrefix', $targetPrefix));
  455. // TODO: Add TargetGrants?
  456. $bucketLoggingStatus->appendChild($loggingEnabled);
  457. }
  458. $dom->appendChild($bucketLoggingStatus);
  459. $rest = new S3Request('PUT', $bucket, '');
  460. $rest->setParameter('logging', null);
  461. $rest->data = $dom->saveXML();
  462. $rest->size = strlen($rest->data);
  463. $rest->setHeader('Content-Type', 'application/xml');
  464. $rest = $rest->getResponse();
  465. if ($rest->error === false && $rest->code !== 200)
  466. $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
  467. if ($rest->error !== false) {
  468. trigger_error(sprintf("S3::setBucketLogging({$bucket}, {$uri}): [%s] %s",
  469. $rest->error['code'], $rest->error['message']), E_USER_WARNING);
  470. return false;
  471. }
  472. return true;
  473. }
  474. /**
  475. Get logging status for a bucket
  476. This will return false if logging is not enabled.
  477. Note: To enable logging, you also need to grant write access to the log group
  478. @param string $bucket Bucket name
  479. @return array | false
  480. **/
  481. public static function getBucketLogging($bucket) {
  482. $rest = new S3Request('GET', $bucket, '');
  483. $rest->setParameter('logging', null);
  484. $rest = $rest->getResponse();
  485. if ($rest->error === false && $rest->code !== 200)
  486. $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
  487. if ($rest->error !== false) {
  488. trigger_error(sprintf("S3::getBucketLogging({$bucket}): [%s] %s",
  489. $rest->error['code'], $rest->error['message']), E_USER_WARNING);
  490. return false;
  491. }
  492. if (!isset($rest->body->LoggingEnabled)) return false; // No logging
  493. return array(
  494. 'targetBucket' => (string)$rest->body->LoggingEnabled->TargetBucket,
  495. 'targetPrefix' => (string)$rest->body->LoggingEnabled->TargetPrefix,
  496. );
  497. }
  498. /**
  499. Disable bucket logging
  500. @param string $bucket Bucket name
  501. @return boolean
  502. **/
  503. public static function disableBucketLogging($bucket) {
  504. return self::setBucketLogging($bucket, null);
  505. }
  506. /**
  507. Get a bucket's location
  508. @param string $bucket Bucket name
  509. @return string | false
  510. **/
  511. public static function getBucketLocation($bucket) {
  512. $rest = new S3Request('GET', $bucket, '');
  513. $rest->setParameter('location', null);
  514. $rest = $rest->getResponse();
  515. if ($rest->error === false && $rest->code !== 200)
  516. $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
  517. if ($rest->error !== false) {
  518. trigger_error(sprintf("S3::getBucketLocation({$bucket}): [%s] %s",
  519. $rest->error['code'], $rest->error['message']), E_USER_WARNING);
  520. return false;
  521. }
  522. return (isset($rest->body[0]) && (string)$rest->body[0] !== '') ? (string)$rest->body[0] : 'US';
  523. }
  524. /**
  525. Set object or bucket Access Control Policy
  526. @param string $bucket Bucket name
  527. @param string $uri Object URI
  528. @param array $acp Access Control Policy Data (same as the data returned from getAccessControlPolicy)
  529. @return boolean
  530. **/
  531. public static function setAccessControlPolicy($bucket, $uri = '', $acp = array()) {
  532. $dom = new DOMDocument;
  533. $dom->formatOutput = true;
  534. $accessControlPolicy = $dom->createElement('AccessControlPolicy');
  535. $accessControlList = $dom->createElement('AccessControlList');
  536. // It seems the owner has to be passed along too
  537. $owner = $dom->createElement('Owner');
  538. $owner->appendChild($dom->createElement('ID', $acp['owner']['id']));
  539. $owner->appendChild($dom->createElement('DisplayName', $acp['owner']['name']));
  540. $accessControlPolicy->appendChild($owner);
  541. foreach ($acp['acl'] as $g) {
  542. $grant = $dom->createElement('Grant');
  543. $grantee = $dom->createElement('Grantee');
  544. $grantee->setAttribute('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance');
  545. if (isset($g['id'])) { // CanonicalUser (DisplayName is omitted)
  546. $grantee->setAttribute('xsi:type', 'CanonicalUser');
  547. $grantee->appendChild($dom->createElement('ID', $g['id']));
  548. } elseif (isset($g['email'])) { // AmazonCustomerByEmail
  549. $grantee->setAttribute('xsi:type', 'AmazonCustomerByEmail');
  550. $grantee->appendChild($dom->createElement('EmailAddress', $g['email']));
  551. } elseif ($g['type'] == 'Group') { // Group
  552. $grantee->setAttribute('xsi:type', 'Group');
  553. $grantee->appendChild($dom->createElement('URI', $g['uri']));
  554. }
  555. $grant->appendChild($grantee);
  556. $grant->appendChild($dom->createElement('Permission', $g['permission']));
  557. $accessControlList->appendChild($grant);
  558. }
  559. $accessControlPolicy->appendChild($accessControlList);
  560. $dom->appendChild($accessControlPolicy);
  561. $rest = new S3Request('PUT', $bucket, $uri);
  562. $rest->setParameter('acl', null);
  563. $rest->data = $dom->saveXML();
  564. $rest->size = strlen($rest->data);
  565. $rest->setHeader('Content-Type', 'application/xml');
  566. $rest = $rest->getResponse();
  567. if ($rest->error === false && $rest->code !== 200)
  568. $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
  569. if ($rest->error !== false) {
  570. trigger_error(sprintf("S3::setAccessControlPolicy({$bucket}, {$uri}): [%s] %s",
  571. $rest->error['code'], $rest->error['message']), E_USER_WARNING);
  572. return false;
  573. }
  574. return true;
  575. }
  576. /**
  577. Get object or bucket Access Control Policy
  578. @param string $bucket Bucket name
  579. @param string $uri Object URI
  580. @return mixed | false
  581. **/
  582. public static function getAccessControlPolicy($bucket, $uri = '') {
  583. $rest = new S3Request('GET', $bucket, $uri);
  584. $rest->setParameter('acl', null);
  585. $rest = $rest->getResponse();
  586. if ($rest->error === false && $rest->code !== 200)
  587. $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
  588. if ($rest->error !== false) {
  589. trigger_error(sprintf("S3::getAccessControlPolicy({$bucket}, {$uri}): [%s] %s",
  590. $rest->error['code'], $rest->error['message']), E_USER_WARNING);
  591. return false;
  592. }
  593. $acp = array();
  594. if (isset($rest->body->Owner, $rest->body->Owner->ID, $rest->body->Owner->DisplayName)) {
  595. $acp['owner'] = array(
  596. 'id' => (string)$rest->body->Owner->ID, 'name' => (string)$rest->body->Owner->DisplayName
  597. );
  598. }
  599. if (isset($rest->body->AccessControlList)) {
  600. $acp['acl'] = array();
  601. foreach ($rest->body->AccessControlList->Grant as $grant) {
  602. foreach ($grant->Grantee as $grantee) {
  603. if (isset($grantee->ID, $grantee->DisplayName)) // CanonicalUser
  604. $acp['acl'][] = array(
  605. 'type' => 'CanonicalUser',
  606. 'id' => (string)$grantee->ID,
  607. 'name' => (string)$grantee->DisplayName,
  608. 'permission' => (string)$grant->Permission
  609. );
  610. elseif (isset($grantee->EmailAddress)) // AmazonCustomerByEmail
  611. $acp['acl'][] = array(
  612. 'type' => 'AmazonCustomerByEmail',
  613. 'email' => (string)$grantee->EmailAddress,
  614. 'permission' => (string)$grant->Permission
  615. );
  616. elseif (isset($grantee->URI)) // Group
  617. $acp['acl'][] = array(
  618. 'type' => 'Group',
  619. 'uri' => (string)$grantee->URI,
  620. 'permission' => (string)$grant->Permission
  621. );
  622. else continue;
  623. }
  624. }
  625. }
  626. return $acp;
  627. }
  628. /**
  629. Delete an object
  630. @param string $bucket Bucket name
  631. @param string $uri Object URI
  632. @return boolean
  633. **/
  634. public static function deleteObject($bucket, $uri) {
  635. $rest = new S3Request('DELETE', $bucket, $uri);
  636. $rest = $rest->getResponse();
  637. if ($rest->error === false && $rest->code !== 204)
  638. $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
  639. if ($rest->error !== false) {
  640. trigger_error(sprintf("S3::deleteObject(): [%s] %s",
  641. $rest->error['code'], $rest->error['message']), E_USER_WARNING);
  642. return false;
  643. }
  644. return true;
  645. }
  646. /**
  647. Get a query string authenticated URL
  648. @param string $bucket Bucket name
  649. @param string $uri Object URI
  650. @param integer $lifetime Lifetime in seconds
  651. @param boolean $hostBucket Use the bucket name as the hostname
  652. @param boolean $https Use HTTPS ($hostBucket should be false for SSL verification)
  653. @return string
  654. **/
  655. public static function getAuthenticatedURL($bucket, $uri, $lifetime, $hostBucket = false, $https = false) {
  656. $expires = time() + $lifetime;
  657. $uri = str_replace('%2F', '/', rawurlencode($uri)); // URI should be encoded (thanks Sean O'Dea)
  658. return sprintf(($https ? 'https' : 'http').'://%s/%s?AWSAccessKeyId=%s&Expires=%u&Signature=%s',
  659. $hostBucket ? $bucket : $bucket.'.s3.amazonaws.com', $uri, self::$__accessKey, $expires,
  660. urlencode(self::__getHash("GET\n\n\n{$expires}\n/{$bucket}/{$uri}")));
  661. }
  662. /**
  663. Get upload POST parameters for form uploads
  664. @param string $bucket Bucket name
  665. @param string $uriPrefix Object URI prefix
  666. @param constant $acl ACL constant
  667. @param integer $lifetime Lifetime in seconds
  668. @param integer $maxFileSize Maximum filesize in bytes (default 5MB)
  669. @param string $successRedirect Redirect URL or 200 / 201 status code
  670. @param array $amzHeaders Array of x-amz-meta-* headers
  671. @param array $headers Array of request headers or content type as a string
  672. @param boolean $flashVars Includes additional "Filename" variable posted by Flash
  673. @return object
  674. **/
  675. public static function getHttpUploadPostParams($bucket, $uriPrefix = '', $acl = self::ACL_PRIVATE, $lifetime = 3600, $maxFileSize = 5242880, $successRedirect = "201", $amzHeaders = array(), $headers = array(), $flashVars = false) {
  676. // Create policy object
  677. $policy = new stdClass;
  678. $policy->expiration = gmdate('Y-m-d\TH:i:s\Z', (time() + $lifetime));
  679. $policy->conditions = array();
  680. $obj = new stdClass; $obj->bucket = $bucket; array_push($policy->conditions, $obj);
  681. $obj = new stdClass; $obj->acl = $acl; array_push($policy->conditions, $obj);
  682. $obj = new stdClass; // 200 for non-redirect uploads
  683. if (is_numeric($successRedirect) && in_array((int)$successRedirect, array(200, 201)))
  684. $obj->success_action_status = (string)$successRedirect;
  685. else // URL
  686. $obj->success_action_redirect = $successRedirect;
  687. array_push($policy->conditions, $obj);
  688. array_push($policy->conditions, array('starts-with', '$key', $uriPrefix));
  689. if ($flashVars) array_push($policy->conditions, array('starts-with', '$Filename', ''));
  690. foreach (array_keys($headers) as $headerKey)
  691. array_push($policy->conditions, array('starts-with', '$'.$headerKey, ''));
  692. foreach ($amzHeaders as $headerKey => $headerVal) {
  693. $obj = new stdClass; $obj->{$headerKey} = (string)$headerVal; array_push($policy->conditions, $obj);
  694. }
  695. array_push($policy->conditions, array('content-length-range', 0, $maxFileSize));
  696. $policy = base64_encode(str_replace('\/', '/', json_encode($policy)));
  697. // Create parameters
  698. $params = new stdClass;
  699. $params->AWSAccessKeyId = self::$__accessKey;
  700. $params->key = $uriPrefix.'${filename}';
  701. $params->acl = $acl;
  702. $params->policy = $policy; unset($policy);
  703. $params->signature = self::__getHash($params->policy);
  704. if (is_numeric($successRedirect) && in_array((int)$successRedirect, array(200, 201)))
  705. $params->success_action_status = (string)$successRedirect;
  706. else
  707. $params->success_action_redirect = $successRedirect;
  708. foreach ($headers as $headerKey => $headerVal) $params->{$headerKey} = (string)$headerVal;
  709. foreach ($amzHeaders as $headerKey => $headerVal) $params->{$headerKey} = (string)$headerVal;
  710. return $params;
  711. }
  712. /**
  713. Create a CloudFront distribution
  714. @param string $bucket Bucket name
  715. @param boolean $enabled Enabled (true/false)
  716. @param array $cnames Array containing CNAME aliases
  717. @param string $comment Use the bucket name as the hostname
  718. @return array | false
  719. **/
  720. public static function createDistribution($bucket, $enabled = true, $cnames = array(), $comment = '') {
  721. self::$useSSL = true; // CloudFront requires SSL
  722. $rest = new S3Request('POST', '', '2010-11-01/distribution', 'cloudfront.amazonaws.com');
  723. $rest->data = self::__getCloudFrontDistributionConfigXML($bucket.'.s3.amazonaws.com', $enabled, $comment, (string)microtime(true), $cnames);
  724. $rest->size = strlen($rest->data);
  725. $rest->setHeader('Content-Type', 'application/xml');
  726. $rest = self::__getCloudFrontResponse($rest);
  727. if ($rest->error === false && $rest->code !== 201)
  728. $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
  729. if ($rest->error !== false) {
  730. trigger_error(sprintf("S3::createDistribution({$bucket}, ".(int)$enabled.", '$comment'): [%s] %s",
  731. $rest->error['code'], $rest->error['message']), E_USER_WARNING);
  732. return false;
  733. } elseif ($rest->body instanceof SimpleXMLElement)
  734. return self::__parseCloudFrontDistributionConfig($rest->body);
  735. return false;
  736. }
  737. /**
  738. Get CloudFront distribution info
  739. @param string $distributionId Distribution ID from listDistributions()
  740. @return array | false
  741. **/
  742. public static function getDistribution($distributionId) {
  743. self::$useSSL = true; // CloudFront requires SSL
  744. $rest = new S3Request('GET', '', '2010-11-01/distribution/'.$distributionId, 'cloudfront.amazonaws.com');
  745. $rest = self::__getCloudFrontResponse($rest);
  746. if ($rest->error === false && $rest->code !== 200)
  747. $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
  748. if ($rest->error !== false) {
  749. trigger_error(sprintf("S3::getDistribution($distributionId): [%s] %s",
  750. $rest->error['code'], $rest->error['message']), E_USER_WARNING);
  751. return false;
  752. } elseif ($rest->body instanceof SimpleXMLElement) {
  753. $dist = self::__parseCloudFrontDistributionConfig($rest->body);
  754. $dist['hash'] = $rest->headers['hash'];
  755. return $dist;
  756. }
  757. return false;
  758. }
  759. /**
  760. Update a CloudFront distribution
  761. @param array $dist Distribution array info identical to output of getDistribution()
  762. @return array | false
  763. **/
  764. public static function updateDistribution($dist) {
  765. self::$useSSL = true; // CloudFront requires SSL
  766. $rest = new S3Request('PUT', '', '2010-11-01/distribution/'.$dist['id'].'/config', 'cloudfront.amazonaws.com');
  767. $rest->data = self::__getCloudFrontDistributionConfigXML($dist['origin'], $dist['enabled'], $dist['comment'], $dist['callerReference'], $dist['cnames']);
  768. $rest->size = strlen($rest->data);
  769. $rest->setHeader('If-Match', $dist['hash']);
  770. $rest = self::__getCloudFrontResponse($rest);
  771. if ($rest->error === false && $rest->code !== 200)
  772. $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
  773. if ($rest->error !== false) {
  774. trigger_error(sprintf("S3::updateDistribution({$dist['id']}, ".(int)$enabled.", '$comment'): [%s] %s",
  775. $rest->error['code'], $rest->error['message']), E_USER_WARNING);
  776. return false;
  777. } else {
  778. $dist = self::__parseCloudFrontDistributionConfig($rest->body);
  779. $dist['hash'] = $rest->headers['hash'];
  780. return $dist;
  781. }
  782. return false;
  783. }
  784. /**
  785. Invalidate paths in a CloudFront distribution
  786. @param array $dist Distribution array info identical to output of getDistribution()
  787. @return array | false
  788. **/
  789. public static function invalidatePaths($dist, $paths) {
  790. self::$useSSL = true; // CloudFront requires SSL
  791. $rest = new S3Request('POST', '', '2010-11-01/distribution/'.$dist['id'].'/invalidation', 'cloudfront.amazonaws.com');
  792. $rest->data = self::__getCloudFrontInvalidationBatchXML($paths);
  793. $rest->size = strlen($rest->data);
  794. $rest->setHeader('If-Match', $dist['hash']);
  795. $rest = self::__getCloudFrontResponse($rest);
  796. if ($rest->error === false && $rest->code > 299)
  797. $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
  798. if ($rest->error !== false) {
  799. trigger_error(sprintf("S3::invalidatePaths({$dist['id']}, ".(array)$paths."): [%s] %s",
  800. $rest->error['code'], $rest->error['message']), E_USER_WARNING);
  801. return false;
  802. } else {
  803. $dist = self::__parseCloudFrontDistributionConfig($rest->body);
  804. if (isset($rest->headers['hash']))
  805. $dist['hash'] = $rest->headers['hash'];
  806. return $dist;
  807. }
  808. return false;
  809. }
  810. /**
  811. Delete a CloudFront distribution
  812. @param array $dist Distribution array info identical to output of getDistribution()
  813. @return boolean
  814. **/
  815. public static function deleteDistribution($dist) {
  816. self::$useSSL = true; // CloudFront requires SSL
  817. $rest = new S3Request('DELETE', '', '2010-11-01/distribution/'.$dist['id'], 'cloudfront.amazonaws.com');
  818. $rest->setHeader('If-Match', $dist['hash']);
  819. $rest = self::__getCloudFrontResponse($rest);
  820. if ($rest->error === false && $rest->code !== 204)
  821. $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
  822. if ($rest->error !== false) {
  823. trigger_error(sprintf("S3::deleteDistribution({$dist['id']}): [%s] %s",
  824. $rest->error['code'], $rest->error['message']), E_USER_WARNING);
  825. return false;
  826. }
  827. return true;
  828. }
  829. /**
  830. Get a list of CloudFront distributions
  831. @return array
  832. **/
  833. public static function listDistributions() {
  834. self::$useSSL = true; // CloudFront requires SSL
  835. $rest = new S3Request('GET', '', '2010-11-01/distribution', 'cloudfront.amazonaws.com');
  836. $rest = self::__getCloudFrontResponse($rest);
  837. if ($rest->error === false && $rest->code !== 200)
  838. $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
  839. if ($rest->error !== false) {
  840. trigger_error(sprintf("S3::listDistributions(): [%s] %s",
  841. $rest->error['code'], $rest->error['message']), E_USER_WARNING);
  842. return false;
  843. } elseif ($rest->body instanceof SimpleXMLElement && isset($rest->body->DistributionSummary)) {
  844. $list = array();
  845. if (isset($rest->body->Marker, $rest->body->MaxItems, $rest->body->IsTruncated)) {
  846. //$info['marker'] = (string)$rest->body->Marker;
  847. //$info['maxItems'] = (int)$rest->body->MaxItems;
  848. //$info['isTruncated'] = (string)$rest->body->IsTruncated == 'true' ? true : false;
  849. }
  850. foreach ($rest->body->DistributionSummary as $summary) {
  851. $list[(string)$summary->Id] = self::__parseCloudFrontDistributionConfig($summary);
  852. }
  853. return $list;
  854. }
  855. return array();
  856. }
  857. /**
  858. Get a DistributionConfig DOMDocument
  859. @internal Used to create XML in createDistribution() and updateDistribution()
  860. @param string $bucket Origin bucket
  861. @param boolean $enabled Enabled (true/false)
  862. @param string $comment Comment to append
  863. @param string $callerReference Caller reference
  864. @param array $cnames Array of CNAME aliases
  865. @return string
  866. **/
  867. private static function __getCloudFrontDistributionConfigXML($bucket, $enabled, $comment, $callerReference='0', $cnames = array()) {
  868. $dom = new DOMDocument('1.0', 'UTF-8');
  869. $dom->formatOutput = true;
  870. $distributionConfig = $dom->createElement('DistributionConfig');
  871. $distributionConfig->setAttribute('xmlns', 'http://cloudfront.amazonaws.com/doc/2010-11-01/');
  872. $distributionConfig->appendChild($dom->createElement('Origin', $bucket));
  873. $distributionConfig->appendChild($dom->createElement('CallerReference', $callerReference));
  874. foreach ($cnames as $cname)
  875. $distributionConfig->appendChild($dom->createElement('CNAME', $cname));
  876. if ($comment !== '') $distributionConfig->appendChild($dom->createElement('Comment', $comment));
  877. $distributionConfig->appendChild($dom->createElement('Enabled', $enabled ? 'true' : 'false'));
  878. $dom->appendChild($distributionConfig);
  879. return $dom->saveXML();
  880. }
  881. /**
  882. Parse a CloudFront distribution config
  883. @internal Used to parse the CloudFront DistributionConfig node to an array
  884. @param object &$node DOMNode
  885. @return array
  886. **/
  887. private static function __parseCloudFrontDistributionConfig(&$node) {
  888. $dist = array();
  889. if (isset($node->Id, $node->Status, $node->LastModifiedTime, $node->DomainName)) {
  890. $dist['id'] = (string)$node->Id;
  891. $dist['status'] = (string)$node->Status;
  892. $dist['time'] = strtotime((string)$node->LastModifiedTime);
  893. $dist['domain'] = (string)$node->DomainName;
  894. }
  895. if (isset($node->CallerReference))
  896. $dist['callerReference'] = (string)$node->CallerReference;
  897. if (isset($node->Comment))
  898. $dist['comment'] = (string)$node->Comment;
  899. if (isset($node->Enabled, $node->Origin)) {
  900. $dist['origin'] = (string)$node->Origin;
  901. $dist['enabled'] = (string)$node->Enabled == 'true' ? true : false;
  902. } elseif (isset($node->DistributionConfig)) {
  903. $dist = array_merge($dist, self::__parseCloudFrontDistributionConfig($node->DistributionConfig));
  904. }
  905. if (isset($node->CNAME)) {
  906. $dist['cnames'] = array();
  907. foreach ($node->CNAME as $cname) $dist['cnames'][(string)$cname] = (string)$cname;
  908. }
  909. return $dist;
  910. }
  911. /**
  912. Get a InvalidationBatch DOMDocument
  913. @internal Used to create XML in invalidatePaths()
  914. @param array $files Files to invalidate
  915. @param string $callerReference Caller reference
  916. @return string
  917. **/
  918. private static function __getCloudFrontInvalidationBatchXML($paths) {
  919. $dom = new DOMDocument('1.0', 'UTF-8');
  920. $dom->formatOutput = true;
  921. $invalidationBatch = $dom->createElement('InvalidationBatch');
  922. foreach ($paths as $path) {
  923. $invalidationBatch->appendChild($dom->createElement('Path', '/'.$path));
  924. }
  925. $invalidationBatch->appendChild($dom->createElement('CallerReference', time()));
  926. $dom->appendChild($invalidationBatch);
  927. return $dom->saveXML();
  928. }
  929. /**
  930. Grab CloudFront response
  931. @internal Used to parse the CloudFront S3Request::getResponse() output
  932. @param object &$rest S3Request instance
  933. @return object
  934. **/
  935. private static function __getCloudFrontResponse(&$rest) {
  936. $rest->getResponse();
  937. if ($rest->response->error === false && isset($rest->response->body) &&
  938. is_string($rest->response->body) && substr($rest->response->body, 0, 5) == '<?xml') {
  939. $rest->response->body = simplexml_load_string($rest->response->body);
  940. // Grab CloudFront errors
  941. if (isset($rest->response->body->Error, $rest->response->body->Error->Code,
  942. $rest->response->body->Error->Message)) {
  943. $rest->response->error = array(
  944. 'code' => (string)$rest->response->body->Error->Code,
  945. 'message' => (string)$rest->response->body->Error->Message
  946. );
  947. unset($rest->response->body);
  948. }
  949. }
  950. return $rest->response;
  951. }
  952. /**
  953. Generate the auth string: "AWS AccessKey:Signature"
  954. @internal Used by S3Request::getResponse()
  955. @param string $string String to sign
  956. @return string
  957. **/
  958. public static function __getSignature($string) {
  959. return 'AWS '.self::$__accessKey.':'.self::__getHash($string);
  960. }
  961. /**
  962. Creates a HMAC-SHA1 hash
  963. This uses the hash extension if loaded
  964. @internal Used by __getSignature()
  965. @param string $string String to sign
  966. @return string
  967. **/
  968. private static function __getHash($string) {
  969. return base64_encode(extension_loaded('hash') ?
  970. hash_hmac('sha1', $string, self::$__secretKey, true) : pack('H*', sha1(
  971. (str_pad(self::$__secretKey, 64, chr(0x00)) ^ (str_repeat(chr(0x5c), 64))) .
  972. pack('H*', sha1((str_pad(self::$__secretKey, 64, chr(0x00)) ^
  973. (str_repeat(chr(0x36), 64))) . $string)))));
  974. }
  975. }
  976. final class S3Request {
  977. private $verb, $bucket, $uri, $resource = '', $parameters = array(),
  978. $amzHeaders = array(), $headers = array(
  979. 'Host' => '', 'Date' => '', 'Content-MD5' => '', 'Content-Type' => ''
  980. );
  981. public $fp = false, $size = 0, $data = false, $response;
  982. /**
  983. Constructor
  984. @param string $verb Verb
  985. @param string $bucket Bucket name
  986. @param string $uri Object URI
  987. @return mixed
  988. **/
  989. function __construct($verb, $bucket = '', $uri = '', $defaultHost = 's3.amazonaws.com') {
  990. $this->verb = $verb;
  991. $this->bucket = strtolower($bucket);
  992. $this->uri = $uri !== '' ? '/'.str_replace('%2F', '/', rawurlencode($uri)) : '/';
  993. if ($this->bucket !== '') {
  994. $this->headers['Host'] = $this->bucket.'.'.$defaultHost;
  995. $this->resource = '/'.$this->bucket.$this->uri;
  996. } else {
  997. $this->headers['Host'] = $defaultHost;
  998. //$this->resource = strlen($this->uri) > 1 ? '/'.$this->bucket.$this->uri : $this->uri;
  999. $this->resource = $this->uri;
  1000. }
  1001. $this->headers['Date'] = gmdate('D, d M Y H:i:s T');
  1002. $this->response = new stdClass;
  1003. $this->response->error = false;
  1004. }
  1005. /**
  1006. Set request parameter
  1007. @param string $key Key
  1008. @param string $value Value
  1009. @return void
  1010. **/
  1011. public function setParameter($key, $value) {
  1012. $this->parameters[$key] = $value;
  1013. }
  1014. /**
  1015. Set request header
  1016. @param string $key Key
  1017. @param string $value Value
  1018. @return void
  1019. **/
  1020. public function setHeader($key, $value) {
  1021. $this->headers[$key] = $value;
  1022. }
  1023. /**
  1024. Set x-amz-meta-* header
  1025. @param string $key Key
  1026. @param string $value Value
  1027. @return void
  1028. **/
  1029. public function setAmzHeader($key, $value) {
  1030. $this->amzHeaders[$key] = $value;
  1031. }
  1032. /**
  1033. Get the S3 response
  1034. @return object | false
  1035. **/
  1036. public function getResponse() {
  1037. $query = '';
  1038. if (sizeof($this->parameters) > 0) {
  1039. $query = substr($this->uri, -1) !== '?' ? '?' : '&';
  1040. foreach ($this->parameters as $var => $value)
  1041. if ($value == null || $value == '') $query .= $var.'&';
  1042. // Parameters should be encoded (thanks Sean O'Dea)
  1043. else $query .= $var.'='.rawurlencode($value).'&';
  1044. $query = substr($query, 0, -1);
  1045. $this->uri .= $query;
  1046. if (array_key_exists('acl', $this->parameters) ||
  1047. array_key_exists('location', $this->parameters) ||
  1048. array_key_exists('torrent', $this->parameters) ||
  1049. array_key_exists('logging', $this->parameters))
  1050. $this->resource .= $query;
  1051. }
  1052. $url = ((S3::$useSSL && extension_loaded('openssl')) ?
  1053. 'https://':'http://').$this->headers['Host'].$this->uri;
  1054. //var_dump($this->bucket, $this->uri, $this->resource, $url);
  1055. // Basic setup
  1056. $curl = curl_init();
  1057. curl_setopt($curl, CURLOPT_USERAGENT, 'S3/php');
  1058. if (S3::$useSSL) {
  1059. curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 1);
  1060. curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 1);
  1061. }
  1062. curl_setopt($curl, CURLOPT_URL, $url);
  1063. // Headers
  1064. $headers = array(); $amz = array();
  1065. foreach ($this->amzHeaders as $header => $value)
  1066. if (strlen($value) > 0) $headers[] = $header.': '.$value;
  1067. foreach ($this->headers as $header => $value)
  1068. if (strlen($value) > 0) $headers[] = $header.': '.$value;
  1069. // Collect AMZ headers for signature
  1070. foreach ($this->amzHeaders as $header => $value)
  1071. if (strlen($value) > 0) $amz[] = strtolower($header).':'.$value;
  1072. // AMZ headers must be sorted
  1073. if (sizeof($amz) > 0) {
  1074. sort($amz);
  1075. $amz = "\n".implode("\n", $amz);
  1076. } else $amz = '';
  1077. // Authorization string (CloudFront stringToSign should only contain a date)
  1078. $headers[] = 'Authorization: ' . S3::__getSignature(
  1079. $this->headers['Host'] == 'cloudfront.amazonaws.com' ? $this->headers['Date'] :
  1080. $this->verb."\n".$this->headers['Content-MD5']."\n".
  1081. $this->headers['Content-Type']."\n".$this->headers['Date'].$amz."\n".$this->resource
  1082. );
  1083. curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
  1084. curl_setopt($curl, CURLOPT_HEADER, false);
  1085. curl_setopt($curl, CURLOPT_RETURNTRANSFER, false);
  1086. curl_setopt($curl, CURLOPT_WRITEFUNCTION, array(&$this, '__responseWriteCallback'));
  1087. curl_setopt($curl, CURLOPT_HEADERFUNCTION, array(&$this, '__responseHeaderCallback'));
  1088. curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
  1089. // Request types
  1090. switch ($this->verb) {
  1091. case 'GET': break;
  1092. case 'PUT': case 'POST': // POST only used for CloudFront
  1093. if ($this->fp !== false) {
  1094. curl_setopt($curl, CURLOPT_PUT, true);
  1095. curl_setopt($curl, CURLOPT_INFILE, $this->fp);
  1096. if ($this->size >= 0)
  1097. curl_setopt($curl, CURLOPT_INFILESIZE, $this->size);
  1098. } elseif ($this->data !== false) {
  1099. curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $this->verb);
  1100. curl_setopt($curl, CURLOPT_POSTFIELDS, $this->data);
  1101. if ($this->size >= 0)
  1102. curl_setopt($curl, CURLOPT_BUFFERSIZE, $this->size);
  1103. } else
  1104. curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $this->verb);
  1105. break;
  1106. case 'HEAD':
  1107. curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'HEAD');
  1108. curl_setopt($curl, CURLOPT_NOBODY, true);
  1109. break;
  1110. case 'DELETE':
  1111. curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'DELETE');
  1112. break;
  1113. default: break;
  1114. }
  1115. // Execute, grab errors
  1116. if (curl_exec($curl))
  1117. $this->response->code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
  1118. else
  1119. $this->response->error = array(
  1120. 'code' => curl_errno($curl),
  1121. 'message' => curl_error($curl),
  1122. 'resource' => $this->resource
  1123. );
  1124. @curl_close($curl);
  1125. // Parse body into XML
  1126. if ($this->response->error === false && isset($this->response->headers['type']) &&
  1127. $this->response->headers['type'] == 'application/xml' && isset($this->response->body)) {
  1128. $this->response->body = simplexml_load_string($this->response->body);
  1129. // Grab S3 errors
  1130. if (!in_array($this->response->code, array(200, 204)) &&
  1131. isset($this->response->body->Code, $this->response->body->Message)) {
  1132. $this->response->error = array(
  1133. 'code' => (string)$this->response->body->Code,
  1134. 'message' => (string)$this->response->body->Message
  1135. );
  1136. if (isset($this->response->body->Resource))
  1137. $this->response->error['resource'] = (string)$this->response->body->Resource;
  1138. unset($this->response->body);
  1139. }
  1140. }
  1141. // Clean up file resources
  1142. if ($this->fp !== false && is_resource($this->fp)) fclose($this->fp);
  1143. return $this->response;
  1144. }
  1145. /**
  1146. CURL write callback
  1147. @param resource &$curl CURL resource
  1148. @param string &$data Data
  1149. @return integer
  1150. **/
  1151. private function __responseWriteCallback(&$curl, &$data) {
  1152. if ($this->response->code == 200 && $this->fp !== false)
  1153. return fwrite($this->fp, $data);
  1154. else
  1155. $this->response->body .= $data;
  1156. return strlen($data);
  1157. }
  1158. /**
  1159. CURL header callback
  1160. @param resource &$curl CURL resource
  1161. @param string &$data Data
  1162. @return integer
  1163. **/
  1164. private function __responseHeaderCallback(&$curl, &$data) {
  1165. if (($strlen = strlen($data)) <= 2) return $strlen;
  1166. if (substr($data, 0, 4) == 'HTTP')
  1167. $this->response->code = (int)substr($data, 9, 3);
  1168. else {
  1169. list($header, $value) = explode(': ', trim($data), 2);
  1170. if ($header == 'Last-Modified')
  1171. $this->response->headers['time'] = strtotime($value);
  1172. elseif ($header == 'Content-Length')
  1173. $this->response->headers['size'] = (int)$value;
  1174. elseif ($header == 'Content-Type')
  1175. $this->response->headers['type'] = $value;
  1176. elseif ($header == 'ETag')
  1177. $this->response->headers['hash'] = $value{0} == '"' ? substr($value, 1, -1) : $value;
  1178. elseif (preg_match('/^x-amz-meta-.*$/', $header))
  1179. $this->response->headers[$header] = is_numeric($value) ? (int)$value : $value;
  1180. }
  1181. return

Large files files are truncated, but you can click here to view the full file