S3.php 63 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923
  1. <?php
  2. /**
  3. * $Id$
  4. *
  5. * Copyright (c) 2011, Donovan Schönknecht. All rights reserved.
  6. *
  7. * Redistribution and use in source and binary forms, with or without
  8. * modification, are permitted provided that the following conditions are met:
  9. *
  10. * - Redistributions of source code must retain the above copyright notice,
  11. * this list of conditions and the following disclaimer.
  12. * - Redistributions in binary form must reproduce the above copyright
  13. * notice, this list of conditions and the following disclaimer in the
  14. * documentation and/or other materials provided with the distribution.
  15. *
  16. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
  17. * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  18. * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  19. * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
  20. * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
  21. * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
  22. * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
  23. * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
  24. * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  25. * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  26. * POSSIBILITY OF SUCH DAMAGE.
  27. *
  28. * Amazon S3 is a trademark of Amazon.com, Inc. or its affiliates.
  29. */
  30. /**
  31. * Amazon S3 PHP class
  32. *
  33. * @link http://undesigned.org.za/2007/10/22/amazon-s3-php-class
  34. * @version 0.5.0-dev
  35. */
  36. class S3
  37. {
  38. // ACL flags
  39. const ACL_PRIVATE = 'private';
  40. const ACL_PUBLIC_READ = 'public-read';
  41. const ACL_PUBLIC_READ_WRITE = 'public-read-write';
  42. const ACL_AUTHENTICATED_READ = 'authenticated-read';
  43. const STORAGE_CLASS_STANDARD = 'STANDARD';
  44. const STORAGE_CLASS_RRS = 'REDUCED_REDUNDANCY';
  45. private static $__accessKey = null; // AWS Access key
  46. private static $__secretKey = null; // AWS Secret key
  47. private static $__sslKey = null;
  48. public static $endpoint = 's3.amazonaws.com';
  49. public static $proxy = null;
  50. public static $useSSL = false;
  51. public static $useSSLValidation = true;
  52. public static $useExceptions = false;
  53. // SSL CURL SSL options - only needed if you are experiencing problems with your OpenSSL configuration
  54. public static $sslKey = null;
  55. public static $sslCert = null;
  56. public static $sslCACert = null;
  57. private static $__signingKeyPairId = null; // AWS Key Pair ID
  58. private static $__signingKeyResource = false; // Key resource, freeSigningKey() must be called to clear it from memory
  59. /**
  60. * Constructor - if you're not using the class statically
  61. *
  62. * @param string $accessKey Access key
  63. * @param string $secretKey Secret key
  64. * @param boolean $useSSL Enable SSL
  65. * @return void
  66. */
  67. public function __construct($accessKey = null, $secretKey = null, $useSSL = false, $endpoint = 's3.amazonaws.com')
  68. {
  69. if ($accessKey !== null && $secretKey !== null)
  70. self::setAuth($accessKey, $secretKey);
  71. self::$useSSL = $useSSL;
  72. self::$endpoint = $endpoint;
  73. }
  74. /**
  75. * Set the sertvice endpoint
  76. *
  77. * @param string $host Hostname
  78. * @return void
  79. */
  80. public function setEndpoint($host)
  81. {
  82. self::$endpoint = $host;
  83. }
  84. /**
  85. * Set AWS access key and secret key
  86. *
  87. * @param string $accessKey Access key
  88. * @param string $secretKey Secret key
  89. * @return void
  90. */
  91. public static function setAuth($accessKey, $secretKey)
  92. {
  93. self::$__accessKey = $accessKey;
  94. self::$__secretKey = $secretKey;
  95. }
  96. /**
  97. * Check if AWS keys have been set
  98. *
  99. * @return boolean
  100. */
  101. public static function hasAuth() {
  102. return (self::$__accessKey !== null && self::$__secretKey !== null);
  103. }
  104. /**
  105. * Set SSL on or off
  106. *
  107. * @param boolean $enabled SSL enabled
  108. * @param boolean $validate SSL certificate validation
  109. * @return void
  110. */
  111. public static function setSSL($enabled, $validate = true)
  112. {
  113. self::$useSSL = $enabled;
  114. self::$useSSLValidation = $validate;
  115. }
  116. /**
  117. * Set SSL client certificates (experimental)
  118. *
  119. * @param string $sslCert SSL client certificate
  120. * @param string $sslKey SSL client key
  121. * @param string $sslCACert SSL CA cert (only required if you are having problems with your system CA cert)
  122. * @return void
  123. */
  124. public static function setSSLAuth($sslCert = null, $sslKey = null, $sslCACert = null)
  125. {
  126. self::$sslCert = $sslCert;
  127. self::$sslKey = $sslKey;
  128. self::$sslCACert = $sslCACert;
  129. }
  130. /**
  131. * Set proxy information
  132. *
  133. * @param string $host Proxy hostname and port (localhost:1234)
  134. * @param string $user Proxy username
  135. * @param string $pass Proxy password
  136. * @param constant $type CURL proxy type
  137. * @return void
  138. */
  139. public static function setProxy($host, $user = null, $pass = null, $type = CURLPROXY_SOCKS5)
  140. {
  141. self::$proxy = array('host' => $host, 'type' => $type, 'user' => null, 'pass' => 'null');
  142. }
  143. /**
  144. * Set the error mode to exceptions
  145. *
  146. * @param boolean $enabled Enable exceptions
  147. * @return void
  148. */
  149. public static function setExceptions($enabled = true)
  150. {
  151. self::$useExceptions = $enabled;
  152. }
  153. /**
  154. * Set signing key
  155. *
  156. * @param string $keyPairId AWS Key Pair ID
  157. * @param string $signingKey Private Key
  158. * @param boolean $isFile Load private key from file, set to false to load string
  159. * @return boolean
  160. */
  161. public static function setSigningKey($keyPairId, $signingKey, $isFile = true)
  162. {
  163. self::$__signingKeyPairId = $keyPairId;
  164. if ((self::$__signingKeyResource = openssl_pkey_get_private($isFile ?
  165. file_get_contents($signingKey) : $signingKey)) !== false) return true;
  166. self::__triggerError('S3::setSigningKey(): Unable to open load private key: '.$signingKey, __FILE__, __LINE__);
  167. return false;
  168. }
  169. /**
  170. * Free signing key from memory, MUST be called if you are using setSigningKey()
  171. *
  172. * @return void
  173. */
  174. public static function freeSigningKey()
  175. {
  176. if (self::$__signingKeyResource !== false)
  177. openssl_free_key(self::$__signingKeyResource);
  178. }
  179. /**
  180. * Internal error handler
  181. *
  182. * @internal Internal error handler
  183. * @param string $message Error message
  184. * @param string $file Filename
  185. * @param integer $line Line number
  186. * @param integer $code Error code
  187. * @return void
  188. */
  189. private static function __triggerError($message, $file, $line, $code = 0)
  190. {
  191. if (self::$useExceptions)
  192. throw new S3Exception($message, $file, $line, $code);
  193. else
  194. trigger_error($message, E_USER_WARNING);
  195. }
  196. /**
  197. * Get a list of buckets
  198. *
  199. * @param boolean $detailed Returns detailed bucket list when true
  200. * @return array | false
  201. */
  202. public static function listBuckets($detailed = false)
  203. {
  204. $rest = new S3Request('GET', '', '', self::$endpoint);
  205. $rest = $rest->getResponse();
  206. if ($rest->error === false && $rest->code !== 200)
  207. $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
  208. if ($rest->error !== false)
  209. {
  210. self::__triggerError(sprintf("S3::listBuckets(): [%s] %s", $rest->error['code'],
  211. $rest->error['message']), __FILE__, __LINE__);
  212. return false;
  213. }
  214. $results = array();
  215. if (!isset($rest->body->Buckets)) return $results;
  216. if ($detailed)
  217. {
  218. if (isset($rest->body->Owner, $rest->body->Owner->ID, $rest->body->Owner->DisplayName))
  219. $results['owner'] = array(
  220. 'id' => (string)$rest->body->Owner->ID, 'name' => (string)$rest->body->Owner->ID
  221. );
  222. $results['buckets'] = array();
  223. foreach ($rest->body->Buckets->Bucket as $b)
  224. $results['buckets'][] = array(
  225. 'name' => (string)$b->Name, 'time' => strtotime((string)$b->CreationDate)
  226. );
  227. } else
  228. foreach ($rest->body->Buckets->Bucket as $b) $results[] = (string)$b->Name;
  229. return $results;
  230. }
  231. /*
  232. * Get contents for a bucket
  233. *
  234. * If maxKeys is null this method will loop through truncated result sets
  235. *
  236. * @param string $bucket Bucket name
  237. * @param string $prefix Prefix
  238. * @param string $marker Marker (last file listed)
  239. * @param string $maxKeys Max keys (maximum number of keys to return)
  240. * @param string $delimiter Delimiter
  241. * @param boolean $returnCommonPrefixes Set to true to return CommonPrefixes
  242. * @return array | false
  243. */
  244. public static function getBucket($bucket, $prefix = null, $marker = null, $maxKeys = null, $delimiter = null, $returnCommonPrefixes = false)
  245. {
  246. $rest = new S3Request('GET', $bucket, '', self::$endpoint);
  247. if ($maxKeys == 0) $maxKeys = null;
  248. if ($prefix !== null && $prefix !== '') $rest->setParameter('prefix', $prefix);
  249. if ($marker !== null && $marker !== '') $rest->setParameter('marker', $marker);
  250. if ($maxKeys !== null && $maxKeys !== '') $rest->setParameter('max-keys', $maxKeys);
  251. if ($delimiter !== null && $delimiter !== '') $rest->setParameter('delimiter', $delimiter);
  252. $response = $rest->getResponse();
  253. if ($response->error === false && $response->code !== 200)
  254. $response->error = array('code' => $response->code, 'message' => 'Unexpected HTTP status');
  255. if ($response->error !== false)
  256. {
  257. self::__triggerError(sprintf("S3::getBucket(): [%s] %s",
  258. $response->error['code'], $response->error['message']), __FILE__, __LINE__);
  259. return false;
  260. }
  261. $results = array();
  262. $nextMarker = null;
  263. if (isset($response->body, $response->body->Contents))
  264. foreach ($response->body->Contents as $c)
  265. {
  266. $results[(string)$c->Key] = array(
  267. 'name' => (string)$c->Key,
  268. 'time' => strtotime((string)$c->LastModified),
  269. 'size' => (int)$c->Size,
  270. 'hash' => substr((string)$c->ETag, 1, -1)
  271. );
  272. $nextMarker = (string)$c->Key;
  273. }
  274. if ($returnCommonPrefixes && isset($response->body, $response->body->CommonPrefixes))
  275. foreach ($response->body->CommonPrefixes as $c)
  276. $results[(string)$c->Prefix] = array('prefix' => (string)$c->Prefix);
  277. if (isset($response->body, $response->body->IsTruncated) &&
  278. (string)$response->body->IsTruncated == 'false') return $results;
  279. if (isset($response->body, $response->body->NextMarker))
  280. $nextMarker = (string)$response->body->NextMarker;
  281. // Loop through truncated results if maxKeys isn't specified
  282. if ($maxKeys == null && $nextMarker !== null && (string)$response->body->IsTruncated == 'true')
  283. do
  284. {
  285. $rest = new S3Request('GET', $bucket, '', self::$endpoint);
  286. if ($prefix !== null && $prefix !== '') $rest->setParameter('prefix', $prefix);
  287. $rest->setParameter('marker', $nextMarker);
  288. if ($delimiter !== null && $delimiter !== '') $rest->setParameter('delimiter', $delimiter);
  289. if (($response = $rest->getResponse(true)) == false || $response->code !== 200) break;
  290. if (isset($response->body, $response->body->Contents))
  291. foreach ($response->body->Contents as $c)
  292. {
  293. $results[(string)$c->Key] = array(
  294. 'name' => (string)$c->Key,
  295. 'time' => strtotime((string)$c->LastModified),
  296. 'size' => (int)$c->Size,
  297. 'hash' => substr((string)$c->ETag, 1, -1)
  298. );
  299. $nextMarker = (string)$c->Key;
  300. }
  301. if ($returnCommonPrefixes && isset($response->body, $response->body->CommonPrefixes))
  302. foreach ($response->body->CommonPrefixes as $c)
  303. $results[(string)$c->Prefix] = array('prefix' => (string)$c->Prefix);
  304. if (isset($response->body, $response->body->NextMarker))
  305. $nextMarker = (string)$response->body->NextMarker;
  306. } while ($response !== false && (string)$response->body->IsTruncated == 'true');
  307. return $results;
  308. }
  309. /**
  310. * Put a bucket
  311. *
  312. * @param string $bucket Bucket name
  313. * @param constant $acl ACL flag
  314. * @param string $location Set as "EU" to create buckets hosted in Europe
  315. * @return boolean
  316. */
  317. public static function putBucket($bucket, $acl = self::ACL_PRIVATE, $location = false)
  318. {
  319. $rest = new S3Request('PUT', $bucket, '', self::$endpoint);
  320. $rest->setAmzHeader('x-amz-acl', $acl);
  321. if ($location !== false)
  322. {
  323. $dom = new DOMDocument;
  324. $createBucketConfiguration = $dom->createElement('CreateBucketConfiguration');
  325. $locationConstraint = $dom->createElement('LocationConstraint', strtoupper($location));
  326. $createBucketConfiguration->appendChild($locationConstraint);
  327. $dom->appendChild($createBucketConfiguration);
  328. $rest->data = $dom->saveXML();
  329. $rest->size = strlen($rest->data);
  330. $rest->setHeader('Content-Type', 'application/xml');
  331. }
  332. $rest = $rest->getResponse();
  333. if ($rest->error === false && $rest->code !== 200)
  334. $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
  335. if ($rest->error !== false)
  336. {
  337. self::__triggerError(sprintf("S3::putBucket({$bucket}, {$acl}, {$location}): [%s] %s",
  338. $rest->error['code'], $rest->error['message']), __FILE__, __LINE__);
  339. return false;
  340. }
  341. return true;
  342. }
  343. /**
  344. * Delete an empty bucket
  345. *
  346. * @param string $bucket Bucket name
  347. * @return boolean
  348. */
  349. public static function deleteBucket($bucket)
  350. {
  351. $rest = new S3Request('DELETE', $bucket, '', self::$endpoint);
  352. $rest = $rest->getResponse();
  353. if ($rest->error === false && $rest->code !== 204)
  354. $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
  355. if ($rest->error !== false)
  356. {
  357. self::__triggerError(sprintf("S3::deleteBucket({$bucket}): [%s] %s",
  358. $rest->error['code'], $rest->error['message']), __FILE__, __LINE__);
  359. return false;
  360. }
  361. return true;
  362. }
  363. /**
  364. * Create input info array for putObject()
  365. *
  366. * @param string $file Input file
  367. * @param mixed $md5sum Use MD5 hash (supply a string if you want to use your own)
  368. * @return array | false
  369. */
  370. public static function inputFile($file, $md5sum = true)
  371. {
  372. if (!file_exists($file) || !is_file($file) || !is_readable($file))
  373. {
  374. self::__triggerError('S3::inputFile(): Unable to open input file: '.$file, __FILE__, __LINE__);
  375. return false;
  376. }
  377. clearstatcache(true, $file);
  378. return array('file' => $file, 'size' => filesize($file), 'md5sum' => $md5sum !== false ?
  379. (is_string($md5sum) ? $md5sum : base64_encode(md5_file($file, true))) : '');
  380. }
  381. /**
  382. * Create input array info for putObject() with a resource
  383. *
  384. * @param string $resource Input resource to read from
  385. * @param integer $bufferSize Input byte size
  386. * @param string $md5sum MD5 hash to send (optional)
  387. * @return array | false
  388. */
  389. public static function inputResource(&$resource, $bufferSize, $md5sum = '')
  390. {
  391. if (!is_resource($resource) || $bufferSize < 0)
  392. {
  393. self::__triggerError('S3::inputResource(): Invalid resource or buffer size', __FILE__, __LINE__);
  394. return false;
  395. }
  396. $input = array('size' => $bufferSize, 'md5sum' => $md5sum);
  397. $input['fp'] =& $resource;
  398. return $input;
  399. }
  400. /**
  401. * Put an object
  402. *
  403. * @param mixed $input Input data
  404. * @param string $bucket Bucket name
  405. * @param string $uri Object URI
  406. * @param constant $acl ACL constant
  407. * @param array $metaHeaders Array of x-amz-meta-* headers
  408. * @param array $requestHeaders Array of request headers or content type as a string
  409. * @param constant $storageClass Storage class constant
  410. * @return boolean
  411. */
  412. public static function putObject($input, $bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $requestHeaders = array(), $storageClass = self::STORAGE_CLASS_STANDARD)
  413. {
  414. if ($input === false) return false;
  415. $rest = new S3Request('PUT', $bucket, $uri, self::$endpoint);
  416. if (is_string($input)) $input = array(
  417. 'data' => $input, 'size' => strlen($input),
  418. 'md5sum' => base64_encode(md5($input, true))
  419. );
  420. // Data
  421. if (isset($input['fp']))
  422. $rest->fp =& $input['fp'];
  423. elseif (isset($input['file']))
  424. $rest->fp = @fopen($input['file'], 'rb');
  425. elseif (isset($input['data']))
  426. $rest->data = $input['data'];
  427. // Content-Length (required)
  428. if (isset($input['size']) && $input['size'] >= 0)
  429. $rest->size = $input['size'];
  430. else {
  431. if (isset($input['file']))
  432. $rest->size = filesize($input['file']);
  433. elseif (isset($input['data']))
  434. $rest->size = strlen($input['data']);
  435. }
  436. // Custom request headers (Content-Type, Content-Disposition, Content-Encoding)
  437. if (is_array($requestHeaders))
  438. foreach ($requestHeaders as $h => $v) $rest->setHeader($h, $v);
  439. elseif (is_string($requestHeaders)) // Support for legacy contentType parameter
  440. $input['type'] = $requestHeaders;
  441. // Content-Type
  442. if (!isset($input['type']))
  443. {
  444. if (isset($requestHeaders['Content-Type']))
  445. $input['type'] =& $requestHeaders['Content-Type'];
  446. elseif (isset($input['file']))
  447. $input['type'] = self::__getMimeType($input['file']);
  448. else
  449. $input['type'] = 'application/octet-stream';
  450. }
  451. if ($storageClass !== self::STORAGE_CLASS_STANDARD) // Storage class
  452. $rest->setAmzHeader('x-amz-storage-class', $storageClass);
  453. // We need to post with Content-Length and Content-Type, MD5 is optional
  454. if ($rest->size >= 0 && ($rest->fp !== false || $rest->data !== false))
  455. {
  456. $rest->setHeader('Content-Type', $input['type']);
  457. if (isset($input['md5sum'])) $rest->setHeader('Content-MD5', $input['md5sum']);
  458. $rest->setAmzHeader('x-amz-acl', $acl);
  459. foreach ($metaHeaders as $h => $v) $rest->setAmzHeader('x-amz-meta-'.$h, $v);
  460. $rest->getResponse();
  461. } else
  462. $rest->response->error = array('code' => 0, 'message' => 'Missing input parameters');
  463. if ($rest->response->error === false && $rest->response->code !== 200)
  464. $rest->response->error = array('code' => $rest->response->code, 'message' => 'Unexpected HTTP status');
  465. if ($rest->response->error !== false)
  466. {
  467. self::__triggerError(sprintf("S3::putObject(): [%s] %s",
  468. $rest->response->error['code'], $rest->response->error['message']), __FILE__, __LINE__);
  469. return false;
  470. }
  471. return true;
  472. }
  473. /**
  474. * Put an object from a file (legacy function)
  475. *
  476. * @param string $file Input file path
  477. * @param string $bucket Bucket name
  478. * @param string $uri Object URI
  479. * @param constant $acl ACL constant
  480. * @param array $metaHeaders Array of x-amz-meta-* headers
  481. * @param string $contentType Content type
  482. * @return boolean
  483. */
  484. public static function putObjectFile($file, $bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $contentType = null)
  485. {
  486. return self::putObject(self::inputFile($file), $bucket, $uri, $acl, $metaHeaders, $contentType);
  487. }
  488. /**
  489. * Put an object from a string (legacy function)
  490. *
  491. * @param string $string Input data
  492. * @param string $bucket Bucket name
  493. * @param string $uri Object URI
  494. * @param constant $acl ACL constant
  495. * @param array $metaHeaders Array of x-amz-meta-* headers
  496. * @param string $contentType Content type
  497. * @return boolean
  498. */
  499. public static function putObjectString($string, $bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $contentType = 'text/plain')
  500. {
  501. return self::putObject($string, $bucket, $uri, $acl, $metaHeaders, $contentType);
  502. }
  503. /**
  504. * Get an object
  505. *
  506. * @param string $bucket Bucket name
  507. * @param string $uri Object URI
  508. * @param mixed $saveTo Filename or resource to write to
  509. * @return mixed
  510. */
  511. public static function getObject($bucket, $uri, $saveTo = false)
  512. {
  513. $rest = new S3Request('GET', $bucket, $uri, self::$endpoint);
  514. if ($saveTo !== false)
  515. {
  516. if (is_resource($saveTo))
  517. $rest->fp =& $saveTo;
  518. else
  519. if (($rest->fp = @fopen($saveTo, 'wb')) !== false)
  520. $rest->file = realpath($saveTo);
  521. else
  522. $rest->response->error = array('code' => 0, 'message' => 'Unable to open save file for writing: '.$saveTo);
  523. }
  524. if ($rest->response->error === false) $rest->getResponse();
  525. if ($rest->response->error === false && $rest->response->code !== 200)
  526. $rest->response->error = array('code' => $rest->response->code, 'message' => 'Unexpected HTTP status');
  527. if ($rest->response->error !== false)
  528. {
  529. self::__triggerError(sprintf("S3::getObject({$bucket}, {$uri}): [%s] %s",
  530. $rest->response->error['code'], $rest->response->error['message']), __FILE__, __LINE__);
  531. return false;
  532. }
  533. return $rest->response;
  534. }
  535. /**
  536. * Get object information
  537. *
  538. * @param string $bucket Bucket name
  539. * @param string $uri Object URI
  540. * @param boolean $returnInfo Return response information
  541. * @return mixed | false
  542. */
  543. public static function getObjectInfo($bucket, $uri, $returnInfo = true)
  544. {
  545. $rest = new S3Request('HEAD', $bucket, $uri, self::$endpoint);
  546. $rest = $rest->getResponse();
  547. if ($rest->error === false && ($rest->code !== 200 && $rest->code !== 404))
  548. $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
  549. if ($rest->error !== false)
  550. {
  551. self::__triggerError(sprintf("S3::getObjectInfo({$bucket}, {$uri}): [%s] %s",
  552. $rest->error['code'], $rest->error['message']), __FILE__, __LINE__);
  553. return false;
  554. }
  555. return $rest->code == 200 ? $returnInfo ? $rest->headers : true : false;
  556. }
  557. /**
  558. * Copy an object
  559. *
  560. * @param string $bucket Source bucket name
  561. * @param string $uri Source object URI
  562. * @param string $bucket Destination bucket name
  563. * @param string $uri Destination object URI
  564. * @param constant $acl ACL constant
  565. * @param array $metaHeaders Optional array of x-amz-meta-* headers
  566. * @param array $requestHeaders Optional array of request headers (content type, disposition, etc.)
  567. * @param constant $storageClass Storage class constant
  568. * @return mixed | false
  569. */
  570. public static function copyObject($srcBucket, $srcUri, $bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $requestHeaders = array(), $storageClass = self::STORAGE_CLASS_STANDARD)
  571. {
  572. $rest = new S3Request('PUT', $bucket, $uri, self::$endpoint);
  573. $rest->setHeader('Content-Length', 0);
  574. foreach ($requestHeaders as $h => $v) $rest->setHeader($h, $v);
  575. foreach ($metaHeaders as $h => $v) $rest->setAmzHeader('x-amz-meta-'.$h, $v);
  576. if ($storageClass !== self::STORAGE_CLASS_STANDARD) // Storage class
  577. $rest->setAmzHeader('x-amz-storage-class', $storageClass);
  578. $rest->setAmzHeader('x-amz-acl', $acl); // Added rawurlencode() for $srcUri (thanks a.yamanoi)
  579. $rest->setAmzHeader('x-amz-copy-source', sprintf('/%s/%s', $srcBucket, rawurlencode($srcUri)));
  580. if (sizeof($requestHeaders) > 0 || sizeof($metaHeaders) > 0)
  581. $rest->setAmzHeader('x-amz-metadata-directive', 'REPLACE');
  582. $rest = $rest->getResponse();
  583. if ($rest->error === false && $rest->code !== 200)
  584. $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
  585. if ($rest->error !== false)
  586. {
  587. self::__triggerError(sprintf("S3::copyObject({$srcBucket}, {$srcUri}, {$bucket}, {$uri}): [%s] %s",
  588. $rest->error['code'], $rest->error['message']), __FILE__, __LINE__);
  589. return false;
  590. }
  591. return isset($rest->body->LastModified, $rest->body->ETag) ? array(
  592. 'time' => strtotime((string)$rest->body->LastModified),
  593. 'hash' => substr((string)$rest->body->ETag, 1, -1)
  594. ) : false;
  595. }
  596. /**
  597. * Set logging for a bucket
  598. *
  599. * @param string $bucket Bucket name
  600. * @param string $targetBucket Target bucket (where logs are stored)
  601. * @param string $targetPrefix Log prefix (e,g; domain.com-)
  602. * @return boolean
  603. */
  604. public static function setBucketLogging($bucket, $targetBucket, $targetPrefix = null)
  605. {
  606. // The S3 log delivery group has to be added to the target bucket's ACP
  607. if ($targetBucket !== null && ($acp = self::getAccessControlPolicy($targetBucket, '')) !== false)
  608. {
  609. // Only add permissions to the target bucket when they do not exist
  610. $aclWriteSet = false;
  611. $aclReadSet = false;
  612. foreach ($acp['acl'] as $acl)
  613. if ($acl['type'] == 'Group' && $acl['uri'] == 'http://acs.amazonaws.com/groups/s3/LogDelivery')
  614. {
  615. if ($acl['permission'] == 'WRITE') $aclWriteSet = true;
  616. elseif ($acl['permission'] == 'READ_ACP') $aclReadSet = true;
  617. }
  618. if (!$aclWriteSet) $acp['acl'][] = array(
  619. 'type' => 'Group', 'uri' => 'http://acs.amazonaws.com/groups/s3/LogDelivery', 'permission' => 'WRITE'
  620. );
  621. if (!$aclReadSet) $acp['acl'][] = array(
  622. 'type' => 'Group', 'uri' => 'http://acs.amazonaws.com/groups/s3/LogDelivery', 'permission' => 'READ_ACP'
  623. );
  624. if (!$aclReadSet || !$aclWriteSet) self::setAccessControlPolicy($targetBucket, '', $acp);
  625. }
  626. $dom = new DOMDocument;
  627. $bucketLoggingStatus = $dom->createElement('BucketLoggingStatus');
  628. $bucketLoggingStatus->setAttribute('xmlns', 'http://s3.amazonaws.com/doc/2006-03-01/');
  629. if ($targetBucket !== null)
  630. {
  631. if ($targetPrefix == null) $targetPrefix = $bucket . '-';
  632. $loggingEnabled = $dom->createElement('LoggingEnabled');
  633. $loggingEnabled->appendChild($dom->createElement('TargetBucket', $targetBucket));
  634. $loggingEnabled->appendChild($dom->createElement('TargetPrefix', $targetPrefix));
  635. // TODO: Add TargetGrants?
  636. $bucketLoggingStatus->appendChild($loggingEnabled);
  637. }
  638. $dom->appendChild($bucketLoggingStatus);
  639. $rest = new S3Request('PUT', $bucket, '', self::$endpoint);
  640. $rest->setParameter('logging', null);
  641. $rest->data = $dom->saveXML();
  642. $rest->size = strlen($rest->data);
  643. $rest->setHeader('Content-Type', 'application/xml');
  644. $rest = $rest->getResponse();
  645. if ($rest->error === false && $rest->code !== 200)
  646. $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
  647. if ($rest->error !== false)
  648. {
  649. self::__triggerError(sprintf("S3::setBucketLogging({$bucket}, {$uri}): [%s] %s",
  650. $rest->error['code'], $rest->error['message']), __FILE__, __LINE__);
  651. return false;
  652. }
  653. return true;
  654. }
  655. /**
  656. * Get logging status for a bucket
  657. *
  658. * This will return false if logging is not enabled.
  659. * Note: To enable logging, you also need to grant write access to the log group
  660. *
  661. * @param string $bucket Bucket name
  662. * @return array | false
  663. */
  664. public static function getBucketLogging($bucket)
  665. {
  666. $rest = new S3Request('GET', $bucket, '', self::$endpoint);
  667. $rest->setParameter('logging', null);
  668. $rest = $rest->getResponse();
  669. if ($rest->error === false && $rest->code !== 200)
  670. $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
  671. if ($rest->error !== false)
  672. {
  673. self::__triggerError(sprintf("S3::getBucketLogging({$bucket}): [%s] %s",
  674. $rest->error['code'], $rest->error['message']), __FILE__, __LINE__);
  675. return false;
  676. }
  677. if (!isset($rest->body->LoggingEnabled)) return false; // No logging
  678. return array(
  679. 'targetBucket' => (string)$rest->body->LoggingEnabled->TargetBucket,
  680. 'targetPrefix' => (string)$rest->body->LoggingEnabled->TargetPrefix,
  681. );
  682. }
  683. /**
  684. * Disable bucket logging
  685. *
  686. * @param string $bucket Bucket name
  687. * @return boolean
  688. */
  689. public static function disableBucketLogging($bucket)
  690. {
  691. return self::setBucketLogging($bucket, null);
  692. }
  693. /**
  694. * Get a bucket's location
  695. *
  696. * @param string $bucket Bucket name
  697. * @return string | false
  698. */
  699. public static function getBucketLocation($bucket)
  700. {
  701. $rest = new S3Request('GET', $bucket, '', self::$endpoint);
  702. $rest->setParameter('location', null);
  703. $rest = $rest->getResponse();
  704. if ($rest->error === false && $rest->code !== 200)
  705. $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
  706. if ($rest->error !== false)
  707. {
  708. self::__triggerError(sprintf("S3::getBucketLocation({$bucket}): [%s] %s",
  709. $rest->error['code'], $rest->error['message']), __FILE__, __LINE__);
  710. return false;
  711. }
  712. return (isset($rest->body[0]) && (string)$rest->body[0] !== '') ? (string)$rest->body[0] : 'US';
  713. }
  714. /**
  715. * Set object or bucket Access Control Policy
  716. *
  717. * @param string $bucket Bucket name
  718. * @param string $uri Object URI
  719. * @param array $acp Access Control Policy Data (same as the data returned from getAccessControlPolicy)
  720. * @return boolean
  721. */
  722. public static function setAccessControlPolicy($bucket, $uri = '', $acp = array())
  723. {
  724. $dom = new DOMDocument;
  725. $dom->formatOutput = true;
  726. $accessControlPolicy = $dom->createElement('AccessControlPolicy');
  727. $accessControlList = $dom->createElement('AccessControlList');
  728. // It seems the owner has to be passed along too
  729. $owner = $dom->createElement('Owner');
  730. $owner->appendChild($dom->createElement('ID', $acp['owner']['id']));
  731. $owner->appendChild($dom->createElement('DisplayName', $acp['owner']['name']));
  732. $accessControlPolicy->appendChild($owner);
  733. foreach ($acp['acl'] as $g)
  734. {
  735. $grant = $dom->createElement('Grant');
  736. $grantee = $dom->createElement('Grantee');
  737. $grantee->setAttribute('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance');
  738. if (isset($g['id']))
  739. { // CanonicalUser (DisplayName is omitted)
  740. $grantee->setAttribute('xsi:type', 'CanonicalUser');
  741. $grantee->appendChild($dom->createElement('ID', $g['id']));
  742. }
  743. elseif (isset($g['email']))
  744. { // AmazonCustomerByEmail
  745. $grantee->setAttribute('xsi:type', 'AmazonCustomerByEmail');
  746. $grantee->appendChild($dom->createElement('EmailAddress', $g['email']));
  747. }
  748. elseif ($g['type'] == 'Group')
  749. { // Group
  750. $grantee->setAttribute('xsi:type', 'Group');
  751. $grantee->appendChild($dom->createElement('URI', $g['uri']));
  752. }
  753. $grant->appendChild($grantee);
  754. $grant->appendChild($dom->createElement('Permission', $g['permission']));
  755. $accessControlList->appendChild($grant);
  756. }
  757. $accessControlPolicy->appendChild($accessControlList);
  758. $dom->appendChild($accessControlPolicy);
  759. $rest = new S3Request('PUT', $bucket, $uri, self::$endpoint);
  760. $rest->setParameter('acl', null);
  761. $rest->data = $dom->saveXML();
  762. $rest->size = strlen($rest->data);
  763. $rest->setHeader('Content-Type', 'application/xml');
  764. $rest = $rest->getResponse();
  765. if ($rest->error === false && $rest->code !== 200)
  766. $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
  767. if ($rest->error !== false)
  768. {
  769. self::__triggerError(sprintf("S3::setAccessControlPolicy({$bucket}, {$uri}): [%s] %s",
  770. $rest->error['code'], $rest->error['message']), __FILE__, __LINE__);
  771. return false;
  772. }
  773. return true;
  774. }
  775. /**
  776. * Get object or bucket Access Control Policy
  777. *
  778. * @param string $bucket Bucket name
  779. * @param string $uri Object URI
  780. * @return mixed | false
  781. */
  782. public static function getAccessControlPolicy($bucket, $uri = '')
  783. {
  784. $rest = new S3Request('GET', $bucket, $uri, self::$endpoint);
  785. $rest->setParameter('acl', null);
  786. $rest = $rest->getResponse();
  787. if ($rest->error === false && $rest->code !== 200)
  788. $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
  789. if ($rest->error !== false)
  790. {
  791. self::__triggerError(sprintf("S3::getAccessControlPolicy({$bucket}, {$uri}): [%s] %s",
  792. $rest->error['code'], $rest->error['message']), __FILE__, __LINE__);
  793. return false;
  794. }
  795. $acp = array();
  796. if (isset($rest->body->Owner, $rest->body->Owner->ID, $rest->body->Owner->DisplayName))
  797. $acp['owner'] = array(
  798. 'id' => (string)$rest->body->Owner->ID, 'name' => (string)$rest->body->Owner->DisplayName
  799. );
  800. if (isset($rest->body->AccessControlList))
  801. {
  802. $acp['acl'] = array();
  803. foreach ($rest->body->AccessControlList->Grant as $grant)
  804. {
  805. foreach ($grant->Grantee as $grantee)
  806. {
  807. if (isset($grantee->ID, $grantee->DisplayName)) // CanonicalUser
  808. $acp['acl'][] = array(
  809. 'type' => 'CanonicalUser',
  810. 'id' => (string)$grantee->ID,
  811. 'name' => (string)$grantee->DisplayName,
  812. 'permission' => (string)$grant->Permission
  813. );
  814. elseif (isset($grantee->EmailAddress)) // AmazonCustomerByEmail
  815. $acp['acl'][] = array(
  816. 'type' => 'AmazonCustomerByEmail',
  817. 'email' => (string)$grantee->EmailAddress,
  818. 'permission' => (string)$grant->Permission
  819. );
  820. elseif (isset($grantee->URI)) // Group
  821. $acp['acl'][] = array(
  822. 'type' => 'Group',
  823. 'uri' => (string)$grantee->URI,
  824. 'permission' => (string)$grant->Permission
  825. );
  826. else continue;
  827. }
  828. }
  829. }
  830. return $acp;
  831. }
  832. /**
  833. * Delete an object
  834. *
  835. * @param string $bucket Bucket name
  836. * @param string $uri Object URI
  837. * @return boolean
  838. */
  839. public static function deleteObject($bucket, $uri)
  840. {
  841. $rest = new S3Request('DELETE', $bucket, $uri, self::$endpoint);
  842. $rest = $rest->getResponse();
  843. if ($rest->error === false && $rest->code !== 204)
  844. $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
  845. if ($rest->error !== false)
  846. {
  847. self::__triggerError(sprintf("S3::deleteObject(): [%s] %s",
  848. $rest->error['code'], $rest->error['message']), __FILE__, __LINE__);
  849. return false;
  850. }
  851. return true;
  852. }
  853. /**
  854. * Get a query string authenticated URL
  855. *
  856. * @param string $bucket Bucket name
  857. * @param string $uri Object URI
  858. * @param integer $lifetime Lifetime in seconds
  859. * @param boolean $hostBucket Use the bucket name as the hostname
  860. * @param boolean $https Use HTTPS ($hostBucket should be false for SSL verification)
  861. * @return string
  862. */
  863. public static function getAuthenticatedURL($bucket, $uri, $lifetime, $hostBucket = false, $https = false)
  864. {
  865. $expires = time() + $lifetime;
  866. $uri = str_replace('%2F', '/', rawurlencode($uri)); // URI should be encoded (thanks Sean O'Dea)
  867. return sprintf(($https ? 'https' : 'http').'://%s/%s?AWSAccessKeyId=%s&Expires=%u&Signature=%s',
  868. $hostBucket ? $bucket : $bucket.'.s3.amazonaws.com', $uri, self::$__accessKey, $expires,
  869. urlencode(self::__getHash("GET\n\n\n{$expires}\n/{$bucket}/{$uri}")));
  870. }
  871. /**
  872. * Get a CloudFront signed policy URL
  873. *
  874. * @param array $policy Policy
  875. * @return string
  876. */
  877. public static function getSignedPolicyURL($policy)
  878. {
  879. $data = json_encode($policy);
  880. $signature = '';
  881. if (!openssl_sign($data, $signature, self::$__signingKeyResource)) return false;
  882. $encoded = str_replace(array('+', '='), array('-', '_', '~'), base64_encode($data));
  883. $signature = str_replace(array('+', '='), array('-', '_', '~'), base64_encode($signature));
  884. $url = $policy['Statement'][0]['Resource'] . '?';
  885. foreach (array('Policy' => $encoded, 'Signature' => $signature, 'Key-Pair-Id' => self::$__signingKeyPairId) as $k => $v)
  886. $url .= $k.'='.str_replace('%2F', '/', rawurlencode($v)).'&';
  887. return substr($url, 0, -1);
  888. }
  889. /**
  890. * Get a CloudFront canned policy URL
  891. *
  892. * @param string $string URL to sign
  893. * @param integer $lifetime URL lifetime
  894. * @return string
  895. */
  896. public static function getSignedCannedURL($url, $lifetime)
  897. {
  898. return self::getSignedPolicyURL(array(
  899. 'Statement' => array(
  900. array('Resource' => $url, 'Condition' => array(
  901. 'DateLessThan' => array('AWS:EpochTime' => time() + $lifetime)
  902. ))
  903. )
  904. ));
  905. }
  906. /**
  907. * Get upload POST parameters for form uploads
  908. *
  909. * @param string $bucket Bucket name
  910. * @param string $uriPrefix Object URI prefix
  911. * @param constant $acl ACL constant
  912. * @param integer $lifetime Lifetime in seconds
  913. * @param integer $maxFileSize Maximum filesize in bytes (default 5MB)
  914. * @param string $successRedirect Redirect URL or 200 / 201 status code
  915. * @param array $amzHeaders Array of x-amz-meta-* headers
  916. * @param array $headers Array of request headers or content type as a string
  917. * @param boolean $flashVars Includes additional "Filename" variable posted by Flash
  918. * @return object
  919. */
  920. public static function getHttpUploadPostParams($bucket, $uriPrefix = '', $acl = self::ACL_PRIVATE, $lifetime = 3600,
  921. $maxFileSize = 5242880, $successRedirect = "201", $amzHeaders = array(), $headers = array(), $flashVars = false)
  922. {
  923. // Create policy object
  924. $policy = new stdClass;
  925. $policy->expiration = gmdate('Y-m-d\TH:i:s\Z', (time() + $lifetime));
  926. $policy->conditions = array();
  927. $obj = new stdClass; $obj->bucket = $bucket; array_push($policy->conditions, $obj);
  928. $obj = new stdClass; $obj->acl = $acl; array_push($policy->conditions, $obj);
  929. $obj = new stdClass; // 200 for non-redirect uploads
  930. if (is_numeric($successRedirect) && in_array((int)$successRedirect, array(200, 201)))
  931. $obj->success_action_status = (string)$successRedirect;
  932. else // URL
  933. $obj->success_action_redirect = $successRedirect;
  934. array_push($policy->conditions, $obj);
  935. if ($acl !== self::ACL_PUBLIC_READ)
  936. array_push($policy->conditions, array('eq', '$acl', $acl));
  937. array_push($policy->conditions, array('starts-with', '$key', $uriPrefix));
  938. if ($flashVars) array_push($policy->conditions, array('starts-with', '$Filename', ''));
  939. foreach (array_keys($headers) as $headerKey)
  940. array_push($policy->conditions, array('starts-with', '$'.$headerKey, ''));
  941. foreach ($amzHeaders as $headerKey => $headerVal)
  942. {
  943. $obj = new stdClass;
  944. $obj->{$headerKey} = (string)$headerVal;
  945. array_push($policy->conditions, $obj);
  946. }
  947. array_push($policy->conditions, array('content-length-range', 0, $maxFileSize));
  948. $policy = base64_encode(str_replace('\/', '/', json_encode($policy)));
  949. // Create parameters
  950. $params = new stdClass;
  951. $params->AWSAccessKeyId = self::$__accessKey;
  952. $params->key = $uriPrefix.'${filename}';
  953. $params->acl = $acl;
  954. $params->policy = $policy; unset($policy);
  955. $params->signature = self::__getHash($params->policy);
  956. if (is_numeric($successRedirect) && in_array((int)$successRedirect, array(200, 201)))
  957. $params->success_action_status = (string)$successRedirect;
  958. else
  959. $params->success_action_redirect = $successRedirect;
  960. foreach ($headers as $headerKey => $headerVal) $params->{$headerKey} = (string)$headerVal;
  961. foreach ($amzHeaders as $headerKey => $headerVal) $params->{$headerKey} = (string)$headerVal;
  962. return $params;
  963. }
  964. /**
  965. * Create a CloudFront distribution
  966. *
  967. * @param string $bucket Bucket name
  968. * @param boolean $enabled Enabled (true/false)
  969. * @param array $cnames Array containing CNAME aliases
  970. * @param string $comment Use the bucket name as the hostname
  971. * @param string $defaultRootObject Default root object
  972. * @param string $originAccessIdentity Origin access identity
  973. * @param array $trustedSigners Array of trusted signers
  974. * @return array | false
  975. */
  976. public static function createDistribution($bucket, $enabled = true, $cnames = array(), $comment = null, $defaultRootObject = null, $originAccessIdentity = null, $trustedSigners = array())
  977. {
  978. if (!extension_loaded('openssl'))
  979. {
  980. self::__triggerError(sprintf("S3::createDistribution({$bucket}, ".(int)$enabled.", [], '$comment'): %s",
  981. "CloudFront functionality requires SSL"), __FILE__, __LINE__);
  982. return false;
  983. }
  984. $useSSL = self::$useSSL;
  985. self::$useSSL = true; // CloudFront requires SSL
  986. $rest = new S3Request('POST', '', '2010-11-01/distribution', 'cloudfront.amazonaws.com');
  987. $rest->data = self::__getCloudFrontDistributionConfigXML(
  988. $bucket.'.s3.amazonaws.com',
  989. $enabled,
  990. (string)$comment,
  991. (string)microtime(true),
  992. $cnames,
  993. $defaultRootObject,
  994. $originAccessIdentity,
  995. $trustedSigners
  996. );
  997. $rest->size = strlen($rest->data);
  998. $rest->setHeader('Content-Type', 'application/xml');
  999. $rest = self::__getCloudFrontResponse($rest);
  1000. self::$useSSL = $useSSL;
  1001. if ($rest->error === false && $rest->code !== 201)
  1002. $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
  1003. if ($rest->error !== false)
  1004. {
  1005. self::__triggerError(sprintf("S3::createDistribution({$bucket}, ".(int)$enabled.", [], '$comment'): [%s] %s",
  1006. $rest->error['code'], $rest->error['message']), __FILE__, __LINE__);
  1007. return false;
  1008. } elseif ($rest->body instanceof SimpleXMLElement)
  1009. return self::__parseCloudFrontDistributionConfig($rest->body);
  1010. return false;
  1011. }
  1012. /**
  1013. * Get CloudFront distribution info
  1014. *
  1015. * @param string $distributionId Distribution ID from listDistributions()
  1016. * @return array | false
  1017. */
  1018. public static function getDistribution($distributionId)
  1019. {
  1020. if (!extension_loaded('openssl'))
  1021. {
  1022. self::__triggerError(sprintf("S3::getDistribution($distributionId): %s",
  1023. "CloudFront functionality requires SSL"), __FILE__, __LINE__);
  1024. return false;
  1025. }
  1026. $useSSL = self::$useSSL;
  1027. self::$useSSL = true; // CloudFront requires SSL
  1028. $rest = new S3Request('GET', '', '2010-11-01/distribution/'.$distributionId, 'cloudfront.amazonaws.com');
  1029. $rest = self::__getCloudFrontResponse($rest);
  1030. self::$useSSL = $useSSL;
  1031. if ($rest->error === false && $rest->code !== 200)
  1032. $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
  1033. if ($rest->error !== false)
  1034. {
  1035. self::__triggerError(sprintf("S3::getDistribution($distributionId): [%s] %s",
  1036. $rest->error['code'], $rest->error['message']), __FILE__, __LINE__);
  1037. return false;
  1038. }
  1039. elseif ($rest->body instanceof SimpleXMLElement)
  1040. {
  1041. $dist = self::__parseCloudFrontDistributionConfig($rest->body);
  1042. $dist['hash'] = $rest->headers['hash'];
  1043. $dist['id'] = $distributionId;
  1044. return $dist;
  1045. }
  1046. return false;
  1047. }
  1048. /**
  1049. * Update a CloudFront distribution
  1050. *
  1051. * @param array $dist Distribution array info identical to output of getDistribution()
  1052. * @return array | false
  1053. */
  1054. public static function updateDistribution($dist)
  1055. {
  1056. if (!extension_loaded('openssl'))
  1057. {
  1058. self::__triggerError(sprintf("S3::updateDistribution({$dist['id']}): %s",
  1059. "CloudFront functionality requires SSL"), __FILE__, __LINE__);
  1060. return false;
  1061. }
  1062. $useSSL = self::$useSSL;
  1063. self::$useSSL = true; // CloudFront requires SSL
  1064. $rest = new S3Request('PUT', '', '2010-11-01/distribution/'.$dist['id'].'/config', 'cloudfront.amazonaws.com');
  1065. $rest->data = self::__getCloudFrontDistributionConfigXML(
  1066. $dist['origin'],
  1067. $dist['enabled'],
  1068. $dist['comment'],
  1069. $dist['callerReference'],
  1070. $dist['cnames'],
  1071. $dist['defaultRootObject'],
  1072. $dist['originAccessIdentity'],
  1073. $dist['trustedSigners']
  1074. );
  1075. $rest->size = strlen($rest->data);
  1076. $rest->setHeader('If-Match', $dist['hash']);
  1077. $rest = self::__getCloudFrontResponse($rest);
  1078. self::$useSSL = $useSSL;
  1079. if ($rest->error === false && $rest->code !== 200)
  1080. $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
  1081. if ($rest->error !== false)
  1082. {
  1083. self::__triggerError(sprintf("S3::updateDistribution({$dist['id']}): [%s] %s",
  1084. $rest->error['code'], $rest->error['message']), __FILE__, __LINE__);
  1085. return false;
  1086. } else {
  1087. $dist = self::__parseCloudFrontDistributionConfig($rest->body);
  1088. $dist['hash'] = $rest->headers['hash'];
  1089. return $dist;
  1090. }
  1091. return false;
  1092. }
  1093. /**
  1094. * Delete a CloudFront distribution
  1095. *
  1096. * @param array $dist Distribution array info identical to output of getDistribution()
  1097. * @return boolean
  1098. */
  1099. public static function deleteDistribution($dist)
  1100. {
  1101. if (!extension_loaded('openssl'))
  1102. {
  1103. self::__triggerError(sprintf("S3::deleteDistribution({$dist['id']}): %s",
  1104. "CloudFront functionality requires SSL"), __FILE__, __LINE__);
  1105. return false;
  1106. }
  1107. $useSSL = self::$useSSL;
  1108. self::$useSSL = true; // CloudFront requires SSL
  1109. $rest = new S3Request('DELETE', '', '2008-06-30/distribution/'.$dist['id'], 'cloudfront.amazonaws.com');
  1110. $rest->setHeader('If-Match', $dist['hash']);
  1111. $rest = self::__getCloudFrontResponse($rest);
  1112. self::$useSSL = $useSSL;
  1113. if ($rest->error === false && $rest->code !== 204)
  1114. $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
  1115. if ($rest->error !== false)
  1116. {
  1117. self::__triggerError(sprintf("S3::deleteDistribution({$dist['id']}): [%s] %s",
  1118. $rest->error['code'], $rest->error['message']), __FILE__, __LINE__);
  1119. return false;
  1120. }
  1121. return true;
  1122. }
  1123. /**
  1124. * Get a list of CloudFront distributions
  1125. *
  1126. * @return array
  1127. */
  1128. public static function listDistributions()
  1129. {
  1130. if (!extension_loaded('openssl'))
  1131. {
  1132. self::__triggerError(sprintf("S3::listDistributions(): [%s] %s",
  1133. "CloudFront functionality requires SSL"), __FILE__, __LINE__);
  1134. return false;
  1135. }
  1136. $useSSL = self::$useSSL;
  1137. self::$useSSL = true; // CloudFront requires SSL
  1138. $rest = new S3Request('GET', '', '2010-11-01/distribution', 'cloudfront.amazonaws.com');
  1139. $rest = self::__getCloudFrontResponse($rest);
  1140. self::$useSSL = $useSSL;
  1141. if ($rest->error === false && $rest->code !== 200)
  1142. $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
  1143. if ($rest->error !== false)
  1144. {
  1145. self::__triggerError(sprintf("S3::listDistributions(): [%s] %s",
  1146. $rest->error['code'], $rest->error['message']), __FILE__, __LINE__);
  1147. return false;
  1148. }
  1149. elseif ($rest->body instanceof SimpleXMLElement && isset($rest->body->DistributionSummary))
  1150. {
  1151. $list = array();
  1152. if (isset($rest->body->Marker, $rest->body->MaxItems, $rest->body->IsTruncated))
  1153. {
  1154. //$info['marker'] = (string)$rest->body->Marker;
  1155. //$info['maxItems'] = (int)$rest->body->MaxItems;
  1156. //$info['isTruncated'] = (string)$rest->body->IsTruncated == 'true' ? true : false;
  1157. }
  1158. foreach ($rest->body->DistributionSummary as $summary)
  1159. $list[(string)$summary->Id] = self::__parseCloudFrontDistributionConfig($summary);
  1160. return $list;
  1161. }
  1162. return array();
  1163. }
  1164. /**
  1165. * List CloudFront Origin Access Identities
  1166. *
  1167. * @return array
  1168. */
  1169. public static function listOriginAccessIdentities()
  1170. {
  1171. if (!extension_loaded('openssl'))
  1172. {
  1173. self::__triggerError(sprintf("S3::listOriginAccessIdentities(): [%s] %s",
  1174. "CloudFront functionality requires SSL"), __FILE__, __LINE__);
  1175. return false;
  1176. }
  1177. self::$useSSL = true; // CloudFront requires SSL
  1178. $rest = new S3Request('GET', '', '2010-11-01/origin-access-identity/cloudfront', 'cloudfront.amazonaws.com');
  1179. $rest = self::__getCloudFrontResponse($rest);
  1180. $useSSL = self::$useSSL;
  1181. if ($rest->error === false && $rest->code !== 200)
  1182. $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
  1183. if ($rest->error !== false)
  1184. {
  1185. trigger_error(sprintf("S3::listOriginAccessIdentities(): [%s] %s",
  1186. $rest->error['code'], $rest->error['message']), E_USER_WARNING);
  1187. return false;
  1188. }
  1189. if (isset($rest->body->CloudFrontOriginAccessIdentitySummary))
  1190. {
  1191. $identities = array();
  1192. foreach ($rest->body->CloudFrontOriginAccessIdentitySummary as $identity)
  1193. if (isset($identity->S3CanonicalUserId))
  1194. $identities[(string)$identity->Id] = array('id' => (string)$identity->Id, 's3CanonicalUserId' => (string)$identity->S3CanonicalUserId);
  1195. return $identities;
  1196. }
  1197. return false;
  1198. }
  1199. /**
  1200. * Invalidate objects in a CloudFront distribution
  1201. *
  1202. * Thanks to Martin Lindkvist for S3::invalidateDistribution()
  1203. *
  1204. * @param string $distributionId Distribution ID from listDistributions()
  1205. * @param array $paths Array of object paths to invalidate
  1206. * @return boolean
  1207. */
  1208. public static function invalidateDistribution($distributionId, $paths)
  1209. {
  1210. if (!extension_loaded('openssl'))
  1211. {
  1212. self::__triggerError(sprintf("S3::invalidateDistribution(): [%s] %s",
  1213. "CloudFront functionality requires SSL"), __FILE__, __LINE__);
  1214. return false;
  1215. }
  1216. $useSSL = self::$useSSL;
  1217. self::$useSSL = true; // CloudFront requires SSL
  1218. $rest = new S3Request('POST', '', '2010-08-01/distribution/'.$distributionId.'/invalidation', 'cloudfront.amazonaws.com');
  1219. $rest->data = self::__getCloudFrontInvalidationBatchXML($paths, (string)microtime(true));
  1220. $rest->size = strlen($rest->data);
  1221. $rest = self::__getCloudFrontResponse($rest);
  1222. self::$useSSL = $useSSL;
  1223. if ($rest->error === false && $rest->code !== 201)
  1224. $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
  1225. if ($rest->error !== false)
  1226. {
  1227. trigger_error(sprintf("S3::invalidate('{$distributionId}',{$paths}): [%s] %s",
  1228. $rest->error['code'], $rest->error['message']), E_USER_WARNING);
  1229. return false;
  1230. }
  1231. return true;
  1232. }
  1233. /**
  1234. * Get a InvalidationBatch DOMDocument
  1235. *
  1236. * @internal Used to create XML in invalidateDistribution()
  1237. * @param array $paths Paths to objects to invalidateDistribution
  1238. * @return string
  1239. */
  1240. private static function __getCloudFrontInvalidationBatchXML($paths, $callerReference = '0') {
  1241. $dom = new DOMDocument('1.0', 'UTF-8');
  1242. $dom->formatOutput = true;
  1243. $invalidationBatch = $dom->createElement('InvalidationBatch');
  1244. foreach ($paths as $path)
  1245. $invalidationBatch->appendChild($dom->createElement('Path', $path));
  1246. $invalidationBatch->appendChild($dom->createElement('CallerReference', $callerReference));
  1247. $dom->appendChild($invalidationBatch);
  1248. return $dom->saveXML();
  1249. }
  1250. /**
  1251. * Get a DistributionConfig DOMDocument
  1252. *
  1253. * http://docs.amazonwebservices.com/AmazonCloudFront/latest/APIReference/index.html?PutConfig.html
  1254. *
  1255. * @internal Used to create XML in createDistribution() and updateDistribution()
  1256. * @param string $bucket S3 Origin bucket
  1257. * @param boolean $enabled Enabled (true/false)
  1258. * @param string $comment Comment to append
  1259. * @param string $callerReference Caller reference
  1260. * @param array $cnames Array of CNAME aliases
  1261. * @param string $defaultRootObject Default root object
  1262. * @param string $originAccessIdentity Origin access identity
  1263. * @param array $trustedSigners Array of trusted signers
  1264. * @return string
  1265. */
  1266. private static function __getCloudFrontDistributionConfigXML($bucket, $enabled, $comment, $callerReference = '0', $cnames = array(), $defaultRootObject = null, $originAccessIdentity = null, $trustedSigners = array())
  1267. {
  1268. $dom = new DOMDocument('1.0', 'UTF-8');
  1269. $dom->formatOutput = true;
  1270. $distributionConfig = $dom->createElement('DistributionConfig');
  1271. $distributionConfig->setAttribute('xmlns', 'http://cloudfront.amazonaws.com/doc/2010-11-01/');
  1272. $origin = $dom->createElement('S3Origin');
  1273. $origin->appendChild($dom->createElement('DNSName', $bucket));
  1274. if ($originAccessIdentity !== null) $origin->appendChild($dom->createElement('OriginAccessIdentity', $originAccessIdentity));
  1275. $distributionConfig->appendChild($origin);
  1276. if ($defaultRootObject !== null) $distributionConfig->appendChild($dom->createElement('DefaultRootObject', $defaultRootObject));
  1277. $distributionConfig->appendChild($dom->createElement('CallerReference', $callerReference));
  1278. foreach ($cnames as $cname)
  1279. $distributionConfig->appendChild($dom->createElement('CNAME', $cname));
  1280. if ($comment !== '') $distributionConfig->appendChild($dom->createElement('Comment', $comment));
  1281. $distributionConfig->appendChild($dom->createElement('Enabled', $enabled ? 'true' : 'false'));
  1282. $trusted = $dom->createElement('TrustedSigners');
  1283. foreach ($trustedSigners as $id => $type)
  1284. $trusted->appendChild($id !== '' ? $dom->createElement($type, $id) : $dom->createElement($type));
  1285. $distributionConfig->appendChild($trusted);
  1286. $dom->appendChild($distributionConfig);
  1287. //var_dump($dom->saveXML());
  1288. return $dom->saveXML();
  1289. }
  1290. /**
  1291. * Parse a CloudFront distribution config
  1292. *
  1293. * See http://docs.amazonwebservices.com/AmazonCloudFront/latest/APIReference/index.html?GetDistribution.html
  1294. *
  1295. * @internal Used to parse the CloudFront DistributionConfig node to an array
  1296. * @param object &$node DOMNode
  1297. * @return array
  1298. */
  1299. private static function __parseCloudFrontDistributionConfig(&$node)
  1300. {
  1301. if (isset($node->DistributionConfig))
  1302. return self::__parseCloudFrontDistributionConfig($node->DistributionConfig);
  1303. $dist = array();
  1304. if (isset($node->Id, $node->Status, $node->LastModifiedTime, $node->DomainName))
  1305. {
  1306. $dist['id'] = (string)$node->Id;
  1307. $dist['status'] = (string)$node->Status;
  1308. $dist['time'] = strtotime((string)$node->LastModifiedTime);
  1309. $dist['domain'] = (string)$node->DomainName;
  1310. }
  1311. if (isset($node->CallerReference))
  1312. $dist['callerReference'] = (string)$node->CallerReference;
  1313. if (isset($node->Enabled))
  1314. $dist['enabled'] = (string)$node->Enabled == 'true' ? true : false;
  1315. if (isset($node->S3Origin))
  1316. {
  1317. if (isset($node->S3Origin->DNSName))
  1318. $dist['origin'] = (string)$node->S3Origin->DNSName;
  1319. $dist['originAccessIdentity'] = isset($node->S3Origin->OriginAccessIdentity) ?
  1320. (string)$node->S3Origin->OriginAccessIdentity : null;
  1321. }
  1322. $dist['defaultRootObject'] = isset($node->DefaultRootObject) ? (string)$node->DefaultRootObject : null;
  1323. $dist['cnames'] = array();
  1324. if (isset($node->CNAME))
  1325. foreach ($node->CNAME as $cname)
  1326. $dist['cnames'][(string)$cname] = (string)$cname;
  1327. $dist['trustedSigners'] = array();
  1328. if (isset($node->TrustedSigners))
  1329. foreach ($node->TrustedSigners as $signer)
  1330. {
  1331. if (isset($signer->Self))
  1332. $dist['trustedSigners'][''] = 'Self';
  1333. elseif (isset($signer->KeyPairId))
  1334. $dist['trustedSigners'][(string)$signer->KeyPairId] = 'KeyPairId';
  1335. elseif (isset($signer->AwsAccountNumber))
  1336. $dist['trustedSigners'][(string)$signer->AwsAccountNumber] = 'AwsAccountNumber';
  1337. }
  1338. $dist['comment'] = isset($node->Comment) ? (string)$node->Comment : null;
  1339. return $dist;
  1340. }
  1341. /**
  1342. * Grab CloudFront response
  1343. *
  1344. * @internal Used to parse the CloudFront S3Request::getResponse() output
  1345. * @param object &$rest S3Request instance
  1346. * @return object
  1347. */
  1348. private static function __getCloudFrontResponse(&$rest)
  1349. {
  1350. $rest->getResponse();
  1351. if ($rest->response->error === false && isset($rest->response->body) &&
  1352. is_string($rest->response->body) && substr($rest->response->body, 0, 5) == '<?xml')
  1353. {
  1354. $rest->response->body = simplexml_load_string($rest->response->body);
  1355. // Grab CloudFront errors
  1356. if (isset($rest->response->body->Error, $rest->response->body->Error->Code,
  1357. $rest->response->body->Error->Message))
  1358. {
  1359. $rest->response->error = array(
  1360. 'code' => (string)$rest->response->body->Error->Code,
  1361. 'message' => (string)$rest->response->body->Error->Message
  1362. );
  1363. unset($rest->response->body);
  1364. }
  1365. }
  1366. return $rest->response;
  1367. }
  1368. /**
  1369. * Get MIME type for file
  1370. *
  1371. * @internal Used to get mime types
  1372. * @param string &$file File path
  1373. * @return string
  1374. */
  1375. public static function __getMimeType(&$file)
  1376. {
  1377. $type = false;
  1378. // Fileinfo documentation says fileinfo_open() will use the
  1379. // MAGIC env var for the magic file
  1380. if (extension_loaded('fileinfo') && isset($_ENV['MAGIC']) &&
  1381. ($finfo = finfo_open(FILEINFO_MIME, $_ENV['MAGIC'])) !== false)
  1382. {
  1383. if (($type = finfo_file($finfo, $file)) !== false)
  1384. {
  1385. // Remove the charset and grab the last content-type
  1386. $type = explode(' ', str_replace('; charset=', ';charset=', $type));
  1387. $type = array_pop($type);
  1388. $type = explode(';', $type);
  1389. $type = trim(array_shift($type));
  1390. }
  1391. finfo_close($finfo);
  1392. // If anyone is still using mime_content_type()
  1393. } elseif (function_exists('mime_content_type'))
  1394. $type = trim(mime_content_type($file));
  1395. if ($type !== false && strlen($type) > 0) return $type;
  1396. // Otherwise do it the old fashioned way
  1397. static $exts = array(
  1398. 'jpg' => 'image/jpeg', 'gif' => 'image/gif', 'png' => 'image/png',
  1399. 'tif' => 'image/tiff', 'tiff' => 'image/tiff', 'ico' => 'image/x-icon',
  1400. 'swf' => 'application/x-shockwave-flash', 'pdf' => 'application/pdf',
  1401. 'zip' => 'application/zip', 'gz' => 'application/x-gzip',
  1402. 'tar' => 'application/x-tar', 'bz' => 'application/x-bzip',
  1403. 'bz2' => 'application/x-bzip2', 'txt' => 'text/plain',
  1404. 'asc' => 'text/plain', 'htm' => 'text/html', 'html' => 'text/html',
  1405. 'css' => 'text/css', 'js' => 'text/javascript',
  1406. 'xml' => 'text/xml', 'xsl' => 'application/xsl+xml',
  1407. 'ogg' => 'application/ogg', 'mp3' => 'audio/mpeg', 'wav' => 'audio/x-wav',
  1408. 'avi' => 'video/x-msvideo', 'mpg' => 'video/mpeg', 'mpeg' => 'video/mpeg',
  1409. 'mov' => 'video/quicktime', 'flv' => 'video/x-flv', 'php' => 'text/x-php'
  1410. );
  1411. $ext = strtolower(pathInfo($file, PATHINFO_EXTENSION));
  1412. return isset($exts[$ext]) ? $exts[$ext] : 'application/octet-stream';
  1413. }
  1414. /**
  1415. * Generate the auth string: "AWS AccessKey:Signature"
  1416. *
  1417. * @internal Used by S3Request::getResponse()
  1418. * @param string $string String to sign
  1419. * @return string
  1420. */
  1421. public static function __getSignature($string)
  1422. {
  1423. return 'AWS '.self::$__accessKey.':'.self::__getHash($string);
  1424. }
  1425. /**
  1426. * Creates a HMAC-SHA1 hash
  1427. *
  1428. * This uses the hash extension if loaded
  1429. *
  1430. * @internal Used by __getSignature()
  1431. * @param string $string String to sign
  1432. * @return string
  1433. */
  1434. private static function __getHash($string)
  1435. {
  1436. return base64_encode(extension_loaded('hash') ?
  1437. hash_hmac('sha1', $string, self::$__secretKey, true) : pack('H*', sha1(
  1438. (str_pad(self::$__secretKey, 64, chr(0x00)) ^ (str_repeat(chr(0x5c), 64))) .
  1439. pack('H*', sha1((str_pad(self::$__secretKey, 64, chr(0x00)) ^
  1440. (str_repeat(chr(0x36), 64))) . $string)))));
  1441. }
  1442. }
  1443. final class S3Request
  1444. {
  1445. private $endpoint, $verb, $bucket, $uri, $resource = '', $parameters = array(),
  1446. $amzHeaders = array(), $headers = array(
  1447. 'Host' => '', 'Date' => '', 'Content-MD5' => '', 'Content-Type' => ''
  1448. );
  1449. public $fp = false, $size = 0, $data = false, $response;
  1450. /**
  1451. * Constructor
  1452. *
  1453. * @param string $verb Verb
  1454. * @param string $bucket Bucket name
  1455. * @param string $uri Object URI
  1456. * @return mixed
  1457. */
  1458. function __construct($verb, $bucket = '', $uri = '', $endpoint = 's3.amazonaws.com')
  1459. {
  1460. $this->endpoint = $endpoint;
  1461. $this->verb = $verb;
  1462. $this->bucket = $bucket;
  1463. $this->uri = $uri !== '' ? '/'.str_replace('%2F', '/', rawurlencode($uri)) : '/';
  1464. if ($this->bucket !== '')
  1465. {
  1466. $this->headers['Host'] = $this->bucket.'.'.$this->endpoint;
  1467. $this->resource = '/'.$this->bucket.$this->uri;
  1468. }
  1469. else
  1470. {
  1471. $this->headers['Host'] = $this->endpoint;
  1472. $this->resource = $this->uri;
  1473. }
  1474. $this->headers['Date'] = gmdate('D, d M Y H:i:s T');
  1475. $this->response = new STDClass;
  1476. $this->response->error = false;
  1477. }
  1478. /**
  1479. * Set request parameter
  1480. *
  1481. * @param string $key Key
  1482. * @param string $value Value
  1483. * @return void
  1484. */
  1485. public function setParameter($key, $value)
  1486. {
  1487. $this->parameters[$key] = $value;
  1488. }
  1489. /**
  1490. * Set request header
  1491. *
  1492. * @param string $key Key
  1493. * @param string $value Value
  1494. * @return void
  1495. */
  1496. public function setHeader($key, $value)
  1497. {
  1498. $this->headers[$key] = $value;
  1499. }
  1500. /**
  1501. * Set x-amz-meta-* header
  1502. *
  1503. * @param string $key Key
  1504. * @param string $value Value
  1505. * @return void
  1506. */
  1507. public function setAmzHeader($key, $value)
  1508. {
  1509. $this->amzHeaders[$key] = $value;
  1510. }
  1511. /**
  1512. * Get the S3 response
  1513. *
  1514. * @return object | false
  1515. */
  1516. public function getResponse()
  1517. {
  1518. $query = '';
  1519. if (sizeof($this->parameters) > 0)
  1520. {
  1521. $query = substr($this->uri, -1) !== '?' ? '?' : '&';
  1522. foreach ($this->parameters as $var => $value)
  1523. if ($value == null || $value == '') $query .= $var.'&';
  1524. // Parameters should be encoded (thanks Sean O'Dea)
  1525. else $query .= $var.'='.rawurlencode($value).'&';
  1526. $query = substr($query, 0, -1);
  1527. $this->uri .= $query;
  1528. if (array_key_exists('acl', $this->parameters) ||
  1529. array_key_exists('location', $this->parameters) ||
  1530. array_key_exists('torrent', $this->parameters) ||
  1531. array_key_exists('logging', $this->parameters))
  1532. $this->resource .= $query;
  1533. }
  1534. $url = (S3::$useSSL ? 'https://' : 'http://') . $this->headers['Host'].$this->uri;
  1535. //var_dump($this->bucket, $this->uri, $this->resource, $url);
  1536. // Basic setup
  1537. $curl = curl_init();
  1538. curl_setopt($curl, CURLOPT_USERAGENT, 'S3/php');
  1539. if (S3::$useSSL)
  1540. {
  1541. // SSL Validation can now be optional for those with broken OpenSSL installations
  1542. curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, S3::$useSSLValidation ? 1 : 0);
  1543. curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, S3::$useSSLValidation ? 1 : 0);
  1544. if (S3::$sslKey !== null) curl_setopt($curl, CURLOPT_SSLKEY, S3::$sslKey);
  1545. if (S3::$sslCert !== null) curl_setopt($curl, CURLOPT_SSLCERT, S3::$sslCert);
  1546. if (S3::$sslCACert !== null) curl_setopt($curl, CURLOPT_CAINFO, S3::$sslCACert);
  1547. }
  1548. curl_setopt($curl, CURLOPT_URL, $url);
  1549. if (S3::$proxy != null && isset(S3::$proxy['host']))
  1550. {
  1551. curl_setopt($curl, CURLOPT_PROXY, S3::$proxy['host']);
  1552. curl_setopt($curl, CURLOPT_PROXYTYPE, S3::$proxy['type']);
  1553. if (isset(S3::$proxy['user'], S3::$proxy['pass']) && $proxy['user'] != null && $proxy['pass'] != null)
  1554. curl_setopt($curl, CURLOPT_PROXYUSERPWD, sprintf('%s:%s', S3::$proxy['user'], S3::$proxy['pass']));
  1555. }
  1556. // Headers
  1557. $headers = array(); $amz = array();
  1558. foreach ($this->amzHeaders as $header => $value)
  1559. if (strlen($value) > 0) $headers[] = $header.': '.$value;
  1560. foreach ($this->headers as $header => $value)
  1561. if (strlen($value) > 0) $headers[] = $header.': '.$value;
  1562. // Collect AMZ headers for signature
  1563. foreach ($this->amzHeaders as $header => $value)
  1564. if (strlen($value) > 0) $amz[] = strtolower($header).':'.$value;
  1565. // AMZ headers must be sorted
  1566. if (sizeof($amz) > 0)
  1567. {
  1568. sort($amz);
  1569. $amz = "\n".implode("\n", $amz);
  1570. } else $amz = '';
  1571. if (S3::hasAuth())
  1572. {
  1573. // Authorization string (CloudFront stringToSign should only contain a date)
  1574. $headers[] = 'Authorization: ' . S3::__getSignature(
  1575. $this->headers['Host'] == 'cloudfront.amazonaws.com' ? $this->headers['Date'] :
  1576. $this->verb."\n".$this->headers['Content-MD5']."\n".
  1577. $this->headers['Content-Type']."\n".$this->headers['Date'].$amz."\n".$this->resource
  1578. );
  1579. }
  1580. curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
  1581. curl_setopt($curl, CURLOPT_HEADER, false);
  1582. curl_setopt($curl, CURLOPT_RETURNTRANSFER, false);
  1583. curl_setopt($curl, CURLOPT_WRITEFUNCTION, array(&$this, '__responseWriteCallback'));
  1584. curl_setopt($curl, CURLOPT_HEADERFUNCTION, array(&$this, '__responseHeaderCallback'));
  1585. curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
  1586. // Request types
  1587. switch ($this->verb)
  1588. {
  1589. case 'GET': break;
  1590. case 'PUT': case 'POST': // POST only used for CloudFront
  1591. if ($this->fp !== false)
  1592. {
  1593. curl_setopt($curl, CURLOPT_PUT, true);
  1594. curl_setopt($curl, CURLOPT_INFILE, $this->fp);
  1595. if ($this->size >= 0)
  1596. curl_setopt($curl, CURLOPT_INFILESIZE, $this->size);
  1597. }
  1598. elseif ($this->data !== false)
  1599. {
  1600. curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $this->verb);
  1601. curl_setopt($curl, CURLOPT_POSTFIELDS, $this->data);
  1602. }
  1603. else
  1604. curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $this->verb);
  1605. break;
  1606. case 'HEAD':
  1607. curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'HEAD');
  1608. curl_setopt($curl, CURLOPT_NOBODY, true);
  1609. break;
  1610. case 'DELETE':
  1611. curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'DELETE');
  1612. break;
  1613. default: break;
  1614. }
  1615. // Execute, grab errors
  1616. if (curl_exec($curl))
  1617. $this->response->code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
  1618. else
  1619. $this->response->error = array(
  1620. 'code' => curl_errno($curl),
  1621. 'message' => curl_error($curl),
  1622. 'resource' => $this->resource
  1623. );
  1624. @curl_close($curl);
  1625. // Parse body into XML
  1626. if ($this->response->error === false && isset($this->response->headers['type']) &&
  1627. $this->response->headers['type'] == 'application/xml' && isset($this->response->body))
  1628. {
  1629. $this->response->body = simplexml_load_string($this->response->body);
  1630. // Grab S3 errors
  1631. if (!in_array($this->response->code, array(200, 204, 206)) &&
  1632. isset($this->response->body->Code, $this->response->body->Message))
  1633. {
  1634. $this->response->error = array(
  1635. 'code' => (string)$this->response->body->Code,
  1636. 'message' => (string)$this->response->body->Message
  1637. );
  1638. if (isset($this->response->body->Resource))
  1639. $this->response->error['resource'] = (string)$this->response->body->Resource;
  1640. unset($this->response->body);
  1641. }
  1642. }
  1643. // Clean up file resources
  1644. if ($this->fp !== false && is_resource($this->fp)) fclose($this->fp);
  1645. return $this->response;
  1646. }
  1647. /**
  1648. * CURL write callback
  1649. *
  1650. * @param resource &$curl CURL resource
  1651. * @param string &$data Data
  1652. * @return integer
  1653. */
  1654. private function __responseWriteCallback(&$curl, &$data)
  1655. {
  1656. if (in_array($this->response->code, array(200, 206)) && $this->fp !== false)
  1657. return fwrite($this->fp, $data);
  1658. else
  1659. $this->response->body .= $data;
  1660. return strlen($data);
  1661. }
  1662. /**
  1663. * CURL header callback
  1664. *
  1665. * @param resource &$curl CURL resource
  1666. * @param string &$data Data
  1667. * @return integer
  1668. */
  1669. private function __responseHeaderCallback(&$curl, &$data)
  1670. {
  1671. if (($strlen = strlen($data)) <= 2) return $strlen;
  1672. if (substr($data, 0, 4) == 'HTTP')
  1673. $this->response->code = (int)substr($data, 9, 3);
  1674. else
  1675. {
  1676. $data = trim($data);
  1677. if (strpos($data, ': ') === false) return $strlen;
  1678. list($header, $value) = explode(': ', $data, 2);
  1679. if ($header == 'Last-Modified')
  1680. $this->response->headers['time'] = strtotime($value);
  1681. elseif ($header == 'Content-Length')
  1682. $this->response->headers['size'] = (int)$value;
  1683. elseif ($header == 'Content-Type')
  1684. $this->response->headers['type'] = $value;
  1685. elseif ($header == 'ETag')
  1686. $this->response->headers['hash'] = $value{0} == '"' ? substr($value, 1, -1) : $value;
  1687. elseif (preg_match('/^x-amz-meta-.*$/', $header))
  1688. $this->response->headers[$header] = is_numeric($value) ? (int)$value : $value;
  1689. }
  1690. return $strlen;
  1691. }
  1692. }
  1693. class S3Exception extends Exception {
  1694. function __construct($message, $file, $line, $code = 0)
  1695. {
  1696. parent::__construct($message, $code);
  1697. $this->file = $file;
  1698. $this->line = $line;
  1699. }
  1700. }