PageRenderTime 50ms CodeModel.GetById 12ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/Molinos/S3/Interface.php

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