PageRenderTime 99ms CodeModel.GetById 28ms RepoModel.GetById 0ms app.codeStats 1ms

/wp-content/plugins/w3-total-cache/lib/S3.php

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