PageRenderTime 156ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 1ms

/repository/s3/S3.php

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