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

/S3.php

https://github.com/obxdesignworks/WP-S3-Backups-Revised
PHP | 1005 lines | 601 code | 115 blank | 289 comment | 154 complexity | b29bd7b17954652df685d60228f52d8e MD5 | raw file
  1. <?php
  2. /**
  3. * $Id: S3.php 33 2008-07-30 17:30:20Z don.schonknecht $
  4. *
  5. * Copyright (c) 2007, Donovan Schonknecht. 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. /**
  29. * Amazon S3 PHP class
  30. *
  31. * @link http://undesigned.org.za/2007/10/22/amazon-s3-php-class
  32. * @version 0.3.3
  33. */
  34. class S3 {
  35. // ACL flags
  36. const ACL_PRIVATE = 'private';
  37. const ACL_PUBLIC_READ = 'public-read';
  38. const ACL_PUBLIC_READ_WRITE = 'public-read-write';
  39. public static $useSSL = true;
  40. public static $__accessKey; // AWS Access key
  41. public static $__secretKey; // AWS Secret key
  42. /**
  43. * Constructor, used if you're not calling the class statically
  44. *
  45. * @param string $accessKey Access key
  46. * @param string $secretKey Secret key
  47. * @param boolean $useSSL Whether or not to use SSL
  48. * @return void
  49. */
  50. public function __construct($accessKey = null, $secretKey = null, $useSSL = true) {
  51. if ($accessKey !== null && $secretKey !== null)
  52. self::setAuth($accessKey, $secretKey);
  53. self::$useSSL = $useSSL;
  54. }
  55. /**
  56. * Set access information
  57. *
  58. * @param string $accessKey Access key
  59. * @param string $secretKey Secret key
  60. * @return void
  61. */
  62. public static function setAuth($accessKey, $secretKey) {
  63. self::$__accessKey = $accessKey;
  64. self::$__secretKey = $secretKey;
  65. }
  66. /**
  67. * Get a list of buckets
  68. *
  69. * @param boolean $detailed Returns detailed bucket list when true
  70. * @return array | false
  71. */
  72. public static function listBuckets($detailed = false) {
  73. $rest = new S3Request('GET', '', '');
  74. $rest = $rest->getResponse();
  75. if ($rest->error === false && $rest->code !== 200)
  76. $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
  77. if ($rest->error !== false) {
  78. trigger_error(sprintf("S3::listBuckets(): [%s] %s", $rest->error['code'], $rest->error['message']), E_USER_WARNING);
  79. return false;
  80. }
  81. $results = array(); //var_dump($rest->body);
  82. if (!isset($rest->body->Buckets)) return $results;
  83. if ($detailed) {
  84. if (isset($rest->body->Owner, $rest->body->Owner->ID, $rest->body->Owner->DisplayName))
  85. $results['owner'] = array(
  86. 'id' => (string)$rest->body->Owner->ID, 'name' => (string)$rest->body->Owner->ID
  87. );
  88. $results['buckets'] = array();
  89. foreach ($rest->body->Buckets->Bucket as $b)
  90. $results['buckets'][] = array(
  91. 'name' => (string)$b->Name, 'time' => strtotime((string)$b->CreationDate)
  92. );
  93. } else
  94. foreach ($rest->body->Buckets->Bucket as $b) $results[] = (string)$b->Name;
  95. return $results;
  96. }
  97. /*
  98. * Get contents for a bucket
  99. *
  100. * If maxKeys is null this method will loop through truncated result sets
  101. *
  102. * @param string $bucket Bucket name
  103. * @param string $prefix Prefix
  104. * @param string $marker Marker (last file listed)
  105. * @param string $maxKeys Max keys (maximum number of keys to return)
  106. * @param string $delimiter Delimiter
  107. * @return array | false
  108. */
  109. public static function getBucket($bucket, $prefix = null, $marker = null, $maxKeys = null, $delimiter = null) {
  110. $rest = new S3Request('GET', $bucket, '');
  111. if ($prefix !== null && $prefix !== '') $rest->setParameter('prefix', $prefix);
  112. if ($marker !== null && $marker !== '') $rest->setParameter('marker', $marker);
  113. if ($maxKeys !== null && $maxKeys !== '') $rest->setParameter('max-keys', $maxKeys);
  114. if ($delimiter !== null && $delimiter !== '') $rest->setParameter('delimiter', $delimiter);
  115. $response = $rest->getResponse();
  116. if ($response->error === false && $response->code !== 200)
  117. $response->error = array('code' => $response->code, 'message' => 'Unexpected HTTP status');
  118. if ($response->error !== false) {
  119. trigger_error(sprintf("S3::getBucket(): [%s] %s", $response->error['code'], $response->error['message']), E_USER_WARNING);
  120. return false;
  121. }
  122. $results = array();
  123. $lastMarker = null;
  124. if (isset($response->body, $response->body->Contents))
  125. foreach ($response->body->Contents as $c) {
  126. $results[(string)$c->Key] = array(
  127. 'name' => (string)$c->Key,
  128. 'time' => strtotime((string)$c->LastModified),
  129. 'size' => (int)$c->Size,
  130. 'hash' => substr((string)$c->ETag, 1, -1)
  131. );
  132. $lastMarker = (string)$c->Key;
  133. //$response->body->IsTruncated = 'true'; break;
  134. }
  135. if (isset($response->body->IsTruncated) &&
  136. (string)$response->body->IsTruncated == 'false') return $results;
  137. // Loop through truncated results if maxKeys isn't specified
  138. if ($maxKeys == null && $lastMarker !== null && (string)$response->body->IsTruncated == 'true')
  139. do {
  140. $rest = new S3Request('GET', $bucket, '');
  141. if ($prefix !== null) $rest->setParameter('prefix', $prefix);
  142. $rest->setParameter('marker', $lastMarker);
  143. if (($response = $rest->getResponse(true)) == false || $response->code !== 200) break;
  144. if (isset($response->body, $response->body->Contents))
  145. foreach ($response->body->Contents as $c) {
  146. $results[(string)$c->Key] = array(
  147. 'name' => (string)$c->Key,
  148. 'time' => strtotime((string)$c->LastModified),
  149. 'size' => (int)$c->Size,
  150. 'hash' => substr((string)$c->ETag, 1, -1)
  151. );
  152. $lastMarker = (string)$c->Key;
  153. }
  154. } while ($response !== false && (string)$response->body->IsTruncated == 'true');
  155. return $results;
  156. }
  157. /**
  158. * Put a bucket
  159. *
  160. * @param string $bucket Bucket name
  161. * @param constant $acl ACL flag
  162. * @param string $location Set as "EU" to create buckets hosted in Europe
  163. * @return boolean
  164. */
  165. public static function putBucket($bucket, $acl = self::ACL_PRIVATE, $location = false) {
  166. $rest = new S3Request('PUT', $bucket, '');
  167. $rest->setAmzHeader('x-amz-acl', $acl);
  168. if ($location !== false) {
  169. $dom = new DOMDocument;
  170. $createBucketConfiguration = $dom->createElement('CreateBucketConfiguration');
  171. $locationConstraint = $dom->createElement('LocationConstraint', strtoupper($location));
  172. $createBucketConfiguration->appendChild($locationConstraint);
  173. $dom->appendChild($createBucketConfiguration);
  174. $rest->data = $dom->saveXML();
  175. $rest->size = strlen($rest->data);
  176. $rest->setHeader('Content-Type', 'application/xml');
  177. }
  178. $rest = $rest->getResponse();
  179. if ($rest->error === false && $rest->code !== 200)
  180. $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
  181. if ($rest->error !== false) {
  182. trigger_error(sprintf("S3::putBucket({$bucket}, {$acl}, {$location}): [%s] %s",
  183. $rest->error['code'], $rest->error['message']), E_USER_WARNING);
  184. return false;
  185. }
  186. return true;
  187. }
  188. /**
  189. * Delete an empty bucket
  190. *
  191. * @param string $bucket Bucket name
  192. * @return boolean
  193. */
  194. public static function deleteBucket($bucket = '') {
  195. $rest = new S3Request('DELETE', $bucket);
  196. $rest = $rest->getResponse();
  197. if ($rest->error === false && $rest->code !== 204)
  198. $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
  199. if ($rest->error !== false) {
  200. trigger_error(sprintf("S3::deleteBucket({$bucket}): [%s] %s",
  201. $rest->error['code'], $rest->error['message']), E_USER_WARNING);
  202. return false;
  203. }
  204. return true;
  205. }
  206. /**
  207. * Create input info array for putObject()
  208. *
  209. * @param string $file Input file
  210. * @param mixed $md5sum Use MD5 hash (supply a string if you want to use your own)
  211. * @return array | false
  212. */
  213. public static function inputFile($file, $md5sum = true) {
  214. if (!file_exists($file) || !is_file($file) || !is_readable($file)) {
  215. trigger_error('S3::inputFile(): Unable to open input file: '.$file, E_USER_WARNING);
  216. return false;
  217. }
  218. return array('file' => $file, 'size' => filesize($file),
  219. 'md5sum' => $md5sum !== false ? (is_string($md5sum) ? $md5sum :
  220. base64_encode(md5_file($file, true))) : '');
  221. }
  222. /**
  223. * Use a resource for input
  224. *
  225. * @param string $file Input file
  226. * @param integer $bufferSize Input byte size
  227. * @param string $md5sum MD5 hash to send (optional)
  228. * @return array | false
  229. */
  230. public static function inputResource(&$resource, $bufferSize, $md5sum = '') {
  231. if (!is_resource($resource) || $bufferSize <= 0) {
  232. trigger_error('S3::inputResource(): Invalid resource or buffer size', E_USER_WARNING);
  233. return false;
  234. }
  235. $input = array('size' => $bufferSize, 'md5sum' => $md5sum);
  236. $input['fp'] =& $resource;
  237. return $input;
  238. }
  239. /**
  240. * Put an object
  241. *
  242. * @param mixed $input Input data
  243. * @param string $bucket Bucket name
  244. * @param string $uri Object URI
  245. * @param constant $acl ACL constant
  246. * @param array $metaHeaders Array of x-amz-meta-* headers
  247. * @param mixed $requestHeaders Array of request headers or content type as a string
  248. * @return boolean
  249. */
  250. public static function putObject($input, $bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $requestHeaders = array()) {
  251. if ($input == false) return false;
  252. $rest = new S3Request('PUT', $bucket, $uri);
  253. if (is_string($input)) $input = array(
  254. 'data' => $input, 'size' => strlen($input),
  255. 'md5sum' => base64_encode(md5($input, true))
  256. );
  257. // Data
  258. if (isset($input['fp']))
  259. $rest->fp =& $input['fp'];
  260. elseif (isset($input['file']))
  261. $rest->fp = @fopen($input['file'], 'rb');
  262. elseif (isset($input['data']))
  263. $rest->data = $input['data'];
  264. // Content-Length (required)
  265. if (isset($input['size']) && $input['size'] > 0)
  266. $rest->size = $input['size'];
  267. else {
  268. if (isset($input['file']))
  269. $rest->size = filesize($input['file']);
  270. elseif (isset($input['data']))
  271. $rest->size = strlen($input['data']);
  272. }
  273. // Custom request headers (Content-Type, Content-Disposition, Content-Encoding)
  274. if (is_array($requestHeaders))
  275. foreach ($requestHeaders as $h => $v) $rest->setHeader($h, $v);
  276. elseif (is_string($requestHeaders)) // Support for legacy contentType parameter
  277. $input['type'] = $requestHeaders;
  278. // Content-Type
  279. if (!isset($input['type'])) {
  280. if (isset($requestHeaders['Content-Type']))
  281. $input['type'] =& $requestHeaders['Content-Type'];
  282. elseif (isset($input['file']))
  283. $input['type'] = self::__getMimeType($input['file']);
  284. else
  285. $input['type'] = 'application/octet-stream';
  286. }
  287. // We need to post with Content-Length and Content-Type, MD5 is optional
  288. if ($rest->size > 0 && ($rest->fp !== false || $rest->data !== false)) {
  289. $rest->setHeader('Content-Type', $input['type']);
  290. if (isset($input['md5sum'])) $rest->setHeader('Content-MD5', $input['md5sum']);
  291. $rest->setAmzHeader('x-amz-acl', $acl);
  292. foreach ($metaHeaders as $h => $v) $rest->setAmzHeader('x-amz-meta-'.$h, $v);
  293. $rest->getResponse();
  294. } else
  295. $rest->response->error = array('code' => 0, 'message' => 'Missing input parameters');
  296. if ($rest->response->error === false && $rest->response->code !== 200)
  297. $rest->response->error = array('code' => $rest->response->code, 'message' => 'Unexpected HTTP status');
  298. if ($rest->response->error !== false) {
  299. trigger_error(sprintf("S3::putObject(): [%s] %s", $rest->response->error['code'], $rest->response->error['message']), E_USER_WARNING);
  300. return false;
  301. }
  302. return true;
  303. }
  304. /**
  305. * Puts an object from a file (legacy function)
  306. *
  307. * @param string $file Input file path
  308. * @param string $bucket Bucket name
  309. * @param string $uri Object URI
  310. * @param constant $acl ACL constant
  311. * @param array $metaHeaders Array of x-amz-meta-* headers
  312. * @param string $contentType Content type
  313. * @return boolean
  314. */
  315. public static function putObjectFile($file, $bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $contentType = null) {
  316. return self::putObject(self::inputFile($file), $bucket, $uri, $acl, $metaHeaders, $contentType);
  317. }
  318. /**
  319. * Put an object from a string (legacy function)
  320. *
  321. * @param string $string Input data
  322. * @param string $bucket Bucket name
  323. * @param string $uri Object URI
  324. * @param constant $acl ACL constant
  325. * @param array $metaHeaders Array of x-amz-meta-* headers
  326. * @param string $contentType Content type
  327. * @return boolean
  328. */
  329. public static function putObjectString($string, $bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $contentType = 'text/plain') {
  330. return self::putObject($string, $bucket, $uri, $acl, $metaHeaders, $contentType);
  331. }
  332. /**
  333. * Get an object
  334. *
  335. * @param string $bucket Bucket name
  336. * @param string $uri Object URI
  337. * @param mixed &$saveTo Filename or resource to write to
  338. * @return mixed
  339. */
  340. public static function getObject($bucket = '', $uri = '', $saveTo = false) {
  341. $rest = new S3Request('GET', $bucket, $uri);
  342. if ($saveTo !== false) {
  343. if (is_resource($saveTo))
  344. $rest->fp =& $saveTo;
  345. else
  346. if (($rest->fp = @fopen($saveTo, 'wb')) == false)
  347. $rest->response->error = array('code' => 0, 'message' => 'Unable to open save file for writing: '.$saveTo);
  348. }
  349. if ($rest->response->error === false) $rest->getResponse();
  350. if ($rest->response->error === false && $rest->response->code !== 200)
  351. $rest->response->error = array('code' => $rest->response->code, 'message' => 'Unexpected HTTP status');
  352. if ($rest->response->error !== false) {
  353. trigger_error(sprintf("S3::getObject({$bucket}, {$uri}): [%s] %s",
  354. $rest->response->error['code'], $rest->response->error['message']), E_USER_WARNING);
  355. return false;
  356. }
  357. $rest->file = realpath($saveTo);
  358. return $rest->response;
  359. }
  360. /**
  361. * Get object information
  362. *
  363. * @param string $bucket Bucket name
  364. * @param string $uri Object URI
  365. * @param boolean $returnInfo Return response information
  366. * @return mixed | false
  367. */
  368. public static function getObjectInfo($bucket = '', $uri = '', $returnInfo = true) {
  369. $rest = new S3Request('HEAD', $bucket, $uri);
  370. $rest = $rest->getResponse();
  371. if ($rest->error === false && ($rest->code !== 200 && $rest->code !== 404))
  372. $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
  373. if ($rest->error !== false) {
  374. trigger_error(sprintf("S3::getObjectInfo({$bucket}, {$uri}): [%s] %s",
  375. $rest->error['code'], $rest->error['message']), E_USER_WARNING);
  376. return false;
  377. }
  378. return $rest->code == 200 ? $returnInfo ? $rest->headers : true : false;
  379. }
  380. /**
  381. * Copy an object
  382. *
  383. * @param string $bucket Source bucket name
  384. * @param string $uri Source object URI
  385. * @param string $bucket Destination bucket name
  386. * @param string $uri Destination object URI
  387. * @return mixed | false
  388. */
  389. public static function copyObject($srcBucket, $srcUri, $bucket, $uri) {
  390. $rest = new S3Request('PUT', $bucket, $uri);
  391. $rest->setAmzHeader('x-amz-copy-source', sprintf('/%s/%s', $srcBucket, $srcUri));
  392. $rest = $rest->getResponse();
  393. if ($rest->error === false && $rest->code !== 200)
  394. $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
  395. if ($rest->error !== false) {
  396. trigger_error(sprintf("S3::copyObject({$srcBucket}, {$srcUri}, {$bucket}, {$uri}): [%s] %s",
  397. $rest->error['code'], $rest->error['message']), E_USER_WARNING);
  398. return false;
  399. }
  400. return isset($rest->body->LastModified, $rest->body->ETag) ? array(
  401. 'time' => strtotime((string)$rest->body->LastModified),
  402. 'hash' => substr((string)$rest->body->ETag, 1, -1)
  403. ) : false;
  404. }
  405. /**
  406. * Set logging for a bucket
  407. *
  408. * @param string $bucket Bucket name
  409. * @param string $targetBucket Target bucket (where logs are stored)
  410. * @param string $targetPrefix Log prefix (e,g; domain.com-)
  411. * @return boolean
  412. */
  413. public static function setBucketLogging($bucket, $targetBucket, $targetPrefix) {
  414. $dom = new DOMDocument;
  415. $bucketLoggingStatus = $dom->createElement('BucketLoggingStatus');
  416. $bucketLoggingStatus->setAttribute('xmlns', 'http://s3.amazonaws.com/doc/2006-03-01/');
  417. $loggingEnabled = $dom->createElement('LoggingEnabled');
  418. $loggingEnabled->appendChild($dom->createElement('TargetBucket', $targetBucket));
  419. $loggingEnabled->appendChild($dom->createElement('TargetPrefix', $targetPrefix));
  420. // TODO: Add TargetGrants
  421. $bucketLoggingStatus->appendChild($loggingEnabled);
  422. $dom->appendChild($bucketLoggingStatus);
  423. $rest = new S3Request('PUT', $bucket, '');
  424. $rest->setParameter('logging', null);
  425. $rest->data = $dom->saveXML();
  426. $rest->size = strlen($rest->data);
  427. $rest->setHeader('Content-Type', 'application/xml');
  428. $rest = $rest->getResponse();
  429. if ($rest->error === false && $rest->code !== 200)
  430. $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
  431. if ($rest->error !== false) {
  432. trigger_error(sprintf("S3::setBucketLogging({$bucket}, {$uri}): [%s] %s",
  433. $rest->error['code'], $rest->error['message']), E_USER_WARNING);
  434. return false;
  435. }
  436. return true;
  437. }
  438. /**
  439. * Get logging status for a bucket
  440. *
  441. * This will return false if logging is not enabled.
  442. * Note: To enable logging, you also need to grant write access to the log group
  443. *
  444. * @param string $bucket Bucket name
  445. * @return array | false
  446. */
  447. public static function getBucketLogging($bucket = '') {
  448. $rest = new S3Request('GET', $bucket, '');
  449. $rest->setParameter('logging', null);
  450. $rest = $rest->getResponse();
  451. if ($rest->error === false && $rest->code !== 200)
  452. $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
  453. if ($rest->error !== false) {
  454. trigger_error(sprintf("S3::getBucketLogging({$bucket}): [%s] %s",
  455. $rest->error['code'], $rest->error['message']), E_USER_WARNING);
  456. return false;
  457. }
  458. if (!isset($rest->body->LoggingEnabled)) return false; // No logging
  459. return array(
  460. 'targetBucket' => (string)$rest->body->LoggingEnabled->TargetBucket,
  461. 'targetPrefix' => (string)$rest->body->LoggingEnabled->TargetPrefix,
  462. );
  463. }
  464. /**
  465. * Get a bucket's location
  466. *
  467. * @param string $bucket Bucket name
  468. * @return string | false
  469. */
  470. public static function getBucketLocation($bucket) {
  471. $rest = new S3Request('GET', $bucket, '');
  472. $rest->setParameter('location', null);
  473. $rest = $rest->getResponse();
  474. if ($rest->error === false && $rest->code !== 200)
  475. $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
  476. if ($rest->error !== false) {
  477. trigger_error(sprintf("S3::getBucketLocation({$bucket}): [%s] %s", $rest->error['code'], $rest->error['message']), E_USER_WARNING);
  478. return false;
  479. }
  480. return (isset($rest->body[0]) && (string)$rest->body[0] !== '') ? (string)$rest->body[0] : 'US';
  481. }
  482. /**
  483. * Set object or bucket Access Control Policy
  484. *
  485. * @param string $bucket Bucket name
  486. * @param string $uri Object URI
  487. * @param array $acp Access Control Policy Data (same as the data returned from getAccessControlPolicy)
  488. * @return boolean
  489. */
  490. public static function setAccessControlPolicy($bucket, $uri = '', $acp = array()) {
  491. $dom = new DOMDocument;
  492. $dom->formatOutput = true;
  493. $accessControlPolicy = $dom->createElement('AccessControlPolicy');
  494. $accessControlList = $dom->createElement('AccessControlList');
  495. // It seems the owner has to be passed along too
  496. $owner = $dom->createElement('Owner');
  497. $owner->appendChild($dom->createElement('ID', $acp['owner']['id']));
  498. $owner->appendChild($dom->createElement('DisplayName', $acp['owner']['name']));
  499. $accessControlPolicy->appendChild($owner);
  500. foreach ($acp['acl'] as $g) {
  501. $grant = $dom->createElement('Grant');
  502. $grantee = $dom->createElement('Grantee');
  503. $grantee->setAttribute('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance');
  504. if (isset($g['id'])) { // CanonicalUser (DisplayName is omitted)
  505. $grantee->setAttribute('xsi:type', 'CanonicalUser');
  506. $grantee->appendChild($dom->createElement('ID', $g['id']));
  507. } elseif (isset($g['email'])) { // AmazonCustomerByEmail
  508. $grantee->setAttribute('xsi:type', 'AmazonCustomerByEmail');
  509. $grantee->appendChild($dom->createElement('EmailAddress', $g['email']));
  510. } elseif ($g['type'] == 'Group') { // Group
  511. $grantee->setAttribute('xsi:type', 'Group');
  512. $grantee->appendChild($dom->createElement('URI', $g['uri']));
  513. }
  514. $grant->appendChild($grantee);
  515. $grant->appendChild($dom->createElement('Permission', $g['permission']));
  516. $accessControlList->appendChild($grant);
  517. }
  518. $accessControlPolicy->appendChild($accessControlList);
  519. $dom->appendChild($accessControlPolicy);
  520. $rest = new S3Request('PUT', $bucket, $uri);
  521. $rest->setParameter('acl', null);
  522. $rest->data = $dom->saveXML();
  523. $rest->size = strlen($rest->data);
  524. $rest->setHeader('Content-Type', 'application/xml');
  525. $rest = $rest->getResponse();
  526. if ($rest->error === false && $rest->code !== 200)
  527. $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
  528. if ($rest->error !== false) {
  529. trigger_error(sprintf("S3::setAccessControlPolicy({$bucket}, {$uri}): [%s] %s",
  530. $rest->error['code'], $rest->error['message']), E_USER_WARNING);
  531. return false;
  532. }
  533. return true;
  534. }
  535. /**
  536. * Get object or bucket Access Control Policy
  537. *
  538. * Currently this will trigger an error if there is no ACL on an object (will fix soon)
  539. *
  540. * @param string $bucket Bucket name
  541. * @param string $uri Object URI
  542. * @return mixed | false
  543. */
  544. public static function getAccessControlPolicy($bucket, $uri = '') {
  545. $rest = new S3Request('GET', $bucket, $uri);
  546. $rest->setParameter('acl', null);
  547. $rest = $rest->getResponse();
  548. if ($rest->error === false && $rest->code !== 200)
  549. $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
  550. if ($rest->error !== false) {
  551. trigger_error(sprintf("S3::getAccessControlPolicy({$bucket}, {$uri}): [%s] %s",
  552. $rest->error['code'], $rest->error['message']), E_USER_WARNING);
  553. return false;
  554. }
  555. $acp = array();
  556. if (isset($rest->body->Owner, $rest->body->Owner->ID, $rest->body->Owner->DisplayName)) {
  557. $acp['owner'] = array(
  558. 'id' => (string)$rest->body->Owner->ID, 'name' => (string)$rest->body->Owner->DisplayName
  559. );
  560. }
  561. if (isset($rest->body->AccessControlList)) {
  562. $acp['acl'] = array();
  563. foreach ($rest->body->AccessControlList->Grant as $grant) {
  564. foreach ($grant->Grantee as $grantee) {
  565. if (isset($grantee->ID, $grantee->DisplayName)) // CanonicalUser
  566. $acp['acl'][] = array(
  567. 'type' => 'CanonicalUser',
  568. 'id' => (string)$grantee->ID,
  569. 'name' => (string)$grantee->DisplayName,
  570. 'permission' => (string)$grant->Permission
  571. );
  572. elseif (isset($grantee->EmailAddress)) // AmazonCustomerByEmail
  573. $acp['acl'][] = array(
  574. 'type' => 'AmazonCustomerByEmail',
  575. 'email' => (string)$grantee->EmailAddress,
  576. 'permission' => (string)$grant->Permission
  577. );
  578. elseif (isset($grantee->URI)) // Group
  579. $acp['acl'][] = array(
  580. 'type' => 'Group',
  581. 'uri' => (string)$grantee->URI,
  582. 'permission' => (string)$grant->Permission
  583. );
  584. else continue;
  585. }
  586. }
  587. }
  588. return $acp;
  589. }
  590. /**
  591. * Delete an object
  592. *
  593. * @param string $bucket Bucket name
  594. * @param string $uri Object URI
  595. * @return mixed
  596. */
  597. public static function deleteObject($bucket = '', $uri = '') {
  598. $rest = new S3Request('DELETE', $bucket, $uri);
  599. $rest = $rest->getResponse();
  600. if ($rest->error === false && $rest->code !== 204)
  601. $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
  602. if ($rest->error !== false) {
  603. trigger_error(sprintf("S3::deleteObject(): [%s] %s", $rest->error['code'], $rest->error['message']), E_USER_WARNING);
  604. return false;
  605. }
  606. return true;
  607. }
  608. /**
  609. * Get MIME type for file
  610. *
  611. * @internal Used to get mime types
  612. * @param string &$file File path
  613. * @return string
  614. */
  615. public static function __getMimeType(&$file) {
  616. $type = false;
  617. // Fileinfo documentation says fileinfo_open() will use the
  618. // MAGIC env var for the magic file
  619. if (extension_loaded('fileinfo') && isset($_ENV['MAGIC']) &&
  620. ($finfo = finfo_open(FILEINFO_MIME, $_ENV['MAGIC'])) !== false) {
  621. if (($type = finfo_file($finfo, $file)) !== false) {
  622. // Remove the charset and grab the last content-type
  623. $type = explode(' ', str_replace('; charset=', ';charset=', $type));
  624. $type = array_pop($type);
  625. $type = explode(';', $type);
  626. $type = array_shift($type);
  627. }
  628. finfo_close($finfo);
  629. // If anyone is still using mime_content_type()
  630. } elseif (function_exists('mime_content_type'))
  631. $type = mime_content_type($file);
  632. if ($type !== false && strlen($type) > 0) return $type;
  633. // Otherwise do it the old fashioned way
  634. static $exts = array(
  635. 'jpg' => 'image/jpeg', 'gif' => 'image/gif', 'png' => 'image/png',
  636. 'tif' => 'image/tiff', 'tiff' => 'image/tiff', 'ico' => 'image/x-icon',
  637. 'swf' => 'application/x-shockwave-flash', 'pdf' => 'application/pdf',
  638. 'zip' => 'application/zip', 'gz' => 'application/x-gzip',
  639. 'tar' => 'application/x-tar', 'bz' => 'application/x-bzip',
  640. 'bz2' => 'application/x-bzip2', 'txt' => 'text/plain',
  641. 'asc' => 'text/plain', 'htm' => 'text/html', 'html' => 'text/html',
  642. 'xml' => 'text/xml', 'xsl' => 'application/xsl+xml',
  643. 'ogg' => 'application/ogg', 'mp3' => 'audio/mpeg', 'wav' => 'audio/x-wav',
  644. 'avi' => 'video/x-msvideo', 'mpg' => 'video/mpeg', 'mpeg' => 'video/mpeg',
  645. 'mov' => 'video/quicktime', 'flv' => 'video/x-flv', 'php' => 'text/x-php'
  646. );
  647. $ext = strToLower(pathInfo($file, PATHINFO_EXTENSION));
  648. return isset($exts[$ext]) ? $exts[$ext] : 'application/octet-stream';
  649. }
  650. /**
  651. * Generate the auth string: "AWS AccessKey:Signature"
  652. *
  653. * This uses the hash extension if loaded
  654. *
  655. * @internal Signs the request
  656. * @param string $string String to sign
  657. * @return string
  658. */
  659. public static function __getSignature($string) {
  660. return 'AWS '.self::$__accessKey.':'.base64_encode(extension_loaded('hash') ?
  661. hash_hmac('sha1', $string, self::$__secretKey, true) : pack('H*', sha1(
  662. (str_pad(self::$__secretKey, 64, chr(0x00)) ^ (str_repeat(chr(0x5c), 64))) .
  663. pack('H*', sha1((str_pad(self::$__secretKey, 64, chr(0x00)) ^
  664. (str_repeat(chr(0x36), 64))) . $string)))));
  665. }
  666. public function getObjectURL($bucket, $uri, $expire = 300, $ssl = true) {
  667. $expire = time() + $expire;
  668. $url = ($ssl ? 'https' : 'http') . '://' . $bucket . '.s3.amazonaws.com/' . $uri . '?';
  669. $url .= 'AWSAccessKeyId=' . self::$__accessKey;
  670. $url .= '&Expires=' . $expire;
  671. $url .= '&Signature=' . urlencode(next(explode(":", self::__getSignature("GET\n\n\n" . $expire . "\n/" . $bucket . '/' . $uri))));
  672. return $url;
  673. }
  674. }
  675. final class S3Request {
  676. private $verb, $bucket, $uri, $resource = '', $parameters = array(),
  677. $amzHeaders = array(), $headers = array(
  678. 'Host' => '', 'Date' => '', 'Content-MD5' => '', 'Content-Type' => ''
  679. );
  680. public $fp = false, $size = 0, $data = false, $response;
  681. /**
  682. * Constructor
  683. *
  684. * @param string $verb Verb
  685. * @param string $bucket Bucket name
  686. * @param string $uri Object URI
  687. * @return mixed
  688. */
  689. function __construct($verb, $bucket = '', $uri = '') {
  690. $this->verb = $verb;
  691. $this->bucket = strtolower($bucket);
  692. $this->uri = $uri !== '' ? '/'.$uri : '/';
  693. if ($this->bucket !== '') {
  694. $this->bucket = explode('/', $this->bucket);
  695. $this->resource = '/'.$this->bucket[0].$this->uri;
  696. $this->headers['Host'] = $this->bucket[0].'.s3.amazonaws.com';
  697. $this->bucket = implode('/', $this->bucket);
  698. } else {
  699. $this->headers['Host'] = 's3.amazonaws.com';
  700. if (strlen($this->uri) > 1)
  701. $this->resource = '/'.$this->bucket.$this->uri;
  702. else $this->resource = $this->uri;
  703. }
  704. $this->headers['Date'] = gmdate('D, d M Y H:i:s T');
  705. $this->response = new STDClass;
  706. $this->response->error = false;
  707. }
  708. /**
  709. * Set request parameter
  710. *
  711. * @param string $key Key
  712. * @param string $value Value
  713. * @return void
  714. */
  715. public function setParameter($key, $value) {
  716. $this->parameters[$key] = $value;
  717. }
  718. /**
  719. * Set request header
  720. *
  721. * @param string $key Key
  722. * @param string $value Value
  723. * @return void
  724. */
  725. public function setHeader($key, $value) {
  726. $this->headers[$key] = $value;
  727. }
  728. /**
  729. * Set x-amz-meta-* header
  730. *
  731. * @param string $key Key
  732. * @param string $value Value
  733. * @return void
  734. */
  735. public function setAmzHeader($key, $value) {
  736. $this->amzHeaders[$key] = $value;
  737. }
  738. /**
  739. * Get the S3 response
  740. *
  741. * @return object | false
  742. */
  743. public function getResponse() {
  744. $query = '';
  745. if (sizeof($this->parameters) > 0) {
  746. $query = substr($this->uri, -1) !== '?' ? '?' : '&';
  747. foreach ($this->parameters as $var => $value)
  748. if ($value == null || $value == '') $query .= $var.'&';
  749. else $query .= $var.'='.$value.'&';
  750. $query = substr($query, 0, -1);
  751. $this->uri .= $query;
  752. if (array_key_exists('acl', $this->parameters) ||
  753. array_key_exists('location', $this->parameters) ||
  754. array_key_exists('torrent', $this->parameters) ||
  755. array_key_exists('logging', $this->parameters))
  756. $this->resource .= $query;
  757. }
  758. $url = ((S3::$useSSL && extension_loaded('openssl')) ?
  759. 'https://':'http://').$this->headers['Host'].$this->uri;
  760. //var_dump($this->bucket, $this->uri, $this->resource, $url);
  761. // Basic setup
  762. $curl = curl_init();
  763. curl_setopt($curl, CURLOPT_USERAGENT, 'S3/php');
  764. curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 0);
  765. curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0);
  766. curl_setopt($curl, CURLOPT_URL, $url);
  767. // Headers
  768. $headers = array(); $amz = array();
  769. foreach ($this->amzHeaders as $header => $value)
  770. if (strlen($value) > 0) $headers[] = $header.': '.$value;
  771. foreach ($this->headers as $header => $value)
  772. if (strlen($value) > 0) $headers[] = $header.': '.$value;
  773. // Collect AMZ headers for signature
  774. foreach ($this->amzHeaders as $header => $value)
  775. if (strlen($value) > 0) $amz[] = strToLower($header).':'.$value;
  776. // AMZ headers must be sorted (thanks Malone)
  777. if (sizeof($amz) > 0) {
  778. sort($amz);
  779. $amz = "\n".implode("\n", $amz);
  780. } else $amz = '';
  781. // Authorization string
  782. $headers[] = 'Authorization: ' . S3::__getSignature(
  783. $this->verb."\n".
  784. $this->headers['Content-MD5']."\n".
  785. $this->headers['Content-Type']."\n".
  786. $this->headers['Date'].$amz."\n".$this->resource
  787. );
  788. curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
  789. curl_setopt($curl, CURLOPT_HEADER, false);
  790. curl_setopt($curl, CURLOPT_RETURNTRANSFER, false);
  791. curl_setopt($curl, CURLOPT_WRITEFUNCTION, array(&$this, '__responseWriteCallback'));
  792. curl_setopt($curl, CURLOPT_HEADERFUNCTION, array(&$this, '__responseHeaderCallback'));
  793. curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
  794. // Request types
  795. switch ($this->verb) {
  796. case 'GET': break;
  797. case 'PUT':
  798. if ($this->fp !== false) {
  799. curl_setopt($curl, CURLOPT_PUT, true);
  800. curl_setopt($curl, CURLOPT_INFILE, $this->fp);
  801. if ($this->size > 0)
  802. curl_setopt($curl, CURLOPT_INFILESIZE, $this->size);
  803. } elseif ($this->data !== false) {
  804. curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'PUT');
  805. curl_setopt($curl, CURLOPT_POSTFIELDS, $this->data);
  806. if ($this->size > 0)
  807. curl_setopt($curl, CURLOPT_BUFFERSIZE, $this->size);
  808. } else
  809. curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'PUT');
  810. break;
  811. case 'HEAD':
  812. curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'HEAD');
  813. curl_setopt($curl, CURLOPT_NOBODY, true);
  814. break;
  815. case 'DELETE':
  816. curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'DELETE');
  817. break;
  818. default: break;
  819. }
  820. // Execute, grab errors
  821. if (curl_exec($curl))
  822. $this->response->code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
  823. else
  824. $this->response->error = array(
  825. 'code' => curl_errno($curl),
  826. 'message' => curl_error($curl),
  827. 'resource' => $this->resource
  828. );
  829. @curl_close($curl);
  830. // Parse body into XML
  831. if ($this->response->error === false && isset($this->response->headers['type']) &&
  832. $this->response->headers['type'] == 'application/xml' && isset($this->response->body)) {
  833. $this->response->body = simplexml_load_string($this->response->body);
  834. // Grab S3 errors
  835. if (!in_array($this->response->code, array(200, 204)) &&
  836. isset($this->response->body->Code, $this->response->body->Message)) {
  837. $this->response->error = array(
  838. 'code' => (string)$this->response->body->Code,
  839. 'message' => (string)$this->response->body->Message
  840. );
  841. if (isset($this->response->body->Resource))
  842. $this->response->error['resource'] = (string)$this->response->body->Resource;
  843. unset($this->response->body);
  844. }
  845. }
  846. // Clean up file resources
  847. if ($this->fp !== false && is_resource($this->fp)) fclose($this->fp);
  848. return $this->response;
  849. }
  850. /**
  851. * CURL write callback
  852. *
  853. * @param resource &$curl CURL resource
  854. * @param string &$data Data
  855. * @return integer
  856. */
  857. private function __responseWriteCallback(&$curl, &$data) {
  858. if ($this->response->code == 200 && $this->fp !== false)
  859. return fwrite($this->fp, $data);
  860. else
  861. $this->response->body .= $data;
  862. return strlen($data);
  863. }
  864. /**
  865. * CURL header callback
  866. *
  867. * @param resource &$curl CURL resource
  868. * @param string &$data Data
  869. * @return integer
  870. */
  871. private function __responseHeaderCallback(&$curl, &$data) {
  872. if (($strlen = strlen($data)) <= 2) return $strlen;
  873. if (substr($data, 0, 4) == 'HTTP')
  874. $this->response->code = (int)substr($data, 9, 3);
  875. else {
  876. list($header, $value) = explode(': ', trim($data), 2);
  877. if ($header == 'Last-Modified')
  878. $this->response->headers['time'] = strtotime($value);
  879. elseif ($header == 'Content-Length')
  880. $this->response->headers['size'] = (int)$value;
  881. elseif ($header == 'Content-Type')
  882. $this->response->headers['type'] = $value;
  883. elseif ($header == 'ETag')
  884. $this->response->headers['hash'] = substr($value, 1, -1);
  885. elseif (preg_match('/^x-amz-meta-.*$/', $header))
  886. $this->response->headers[$header] = is_numeric($value) ? (int)$value : $value;
  887. }
  888. return $strlen;
  889. }
  890. }