PluginProbe ʕ •ᴥ•ʔ
Backup Migration / 2.1.2
Backup Migration v2.1.2
2.1.6 2.1.5.2 trunk 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.6.1 1.4.7 1.4.8 1.4.9 1.4.9.1 2.0.0 2.1.0 2.1.1 2.1.2 2.1.3 2.1.4 2.1.5 2.1.5.1
backup-backup / includes / vendor / s3-client / s3.php
backup-backup / includes / vendor / s3-client Last commit date
s3.php 3 months ago
s3.php
2239 lines
1 <?php
2
3 namespace BMI\Plugin\External;
4
5
6 // Exit on direct access
7 if (!defined('ABSPATH')) {
8 exit;
9 }
10
11 use \DOMDocument;
12 use \stdClass;
13
14 /**
15 * Copyright (c) 2013, Donovan Schönknecht. All rights reserved.
16 *
17 * Redistribution and use in source and binary forms, with or without
18 * modification, are permitted provided that the following conditions are met:
19 *
20 * - Redistributions of source code must retain the above copyright notice,
21 * this list of conditions and the following disclaimer.
22 * - Redistributions in binary form must reproduce the above copyright
23 * notice, this list of conditions and the following disclaimer in the
24 * documentation and/or other materials provided with the distribution.
25 *
26 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
27 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
28 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
29 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
30 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
31 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
32 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
33 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
34 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
35 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
36 * POSSIBILITY OF SUCH DAMAGE.
37 *
38 * Amazon S3 is a trademark of Amazon.com, Inc. or its affiliates.
39 */
40
41
42 /**
43 * S3 exception class
44 *
45 * @link http://undesigned.org.za/2007/10/22/amazon-s3-php-class
46 * @version 0.5.0-dev
47 */
48
49 class S3Exception extends \Exception {
50 /**
51 * Class constructor
52 *
53 * @param string $message Exception message
54 * @param string $file File in which exception was created
55 * @param string $line Line number on which exception was created
56 * @param int $code Exception code
57 */
58 function __construct($message, $file, $line, $code = 0)
59 {
60 parent::__construct($message, $code);
61 $this->file = $file;
62 $this->line = $line;
63 }
64 }
65
66 /**
67 * Amazon S3 PHP class
68 *
69 * @link http://undesigned.org.za/2007/10/22/amazon-s3-php-class
70 * @version 0.5.1
71 */
72 class S3
73 {
74 // ACL flags
75 const ACL_PRIVATE = 'private';
76 const ACL_PUBLIC_READ = 'public-read';
77 const ACL_PUBLIC_READ_WRITE = 'public-read-write';
78 const ACL_AUTHENTICATED_READ = 'authenticated-read';
79
80 const STORAGE_CLASS_STANDARD = 'STANDARD';
81 const STORAGE_CLASS_RRS = 'REDUCED_REDUNDANCY';
82 const STORAGE_CLASS_STANDARD_IA = 'STANDARD_IA';
83
84 const SSE_NONE = '';
85 const SSE_AES256 = 'AES256';
86
87 /**
88 * The AWS Access key
89 *
90 * @var string
91 * @access private
92 */
93 private $__accessKey = null;
94
95 /**
96 * AWS Secret Key
97 *
98 * @var string
99 * @access private
100 */
101 private $__secretKey = null;
102
103 /**
104 * SSL Client key
105 *
106 * @var string
107 * @access private
108 */
109 private $__sslKey = null;
110
111 /**
112 * Default delimiter to be used, for example while getBucket().
113 * @var string
114 * @access public
115 */
116 public $defDelimiter = null;
117
118 /**
119 * AWS URI
120 *
121 * @var string
122 * @acess public
123 */
124 public $endpoint = 's3.amazonaws.com';
125
126 /**
127 * AWS Region
128 *
129 * @var string
130 * @acess public
131 */
132 public $region = '';
133
134 /**
135 * Proxy information
136 *
137 * @var null|array
138 * @access public
139 */
140 public $proxy = null;
141
142 /**
143 * Connect using SSL?
144 *
145 * @var bool
146 * @access public
147 */
148 public $useSSL = false;
149
150 /**
151 * Use SSL validation?
152 *
153 * @var bool
154 * @access public
155 */
156 public $useSSLValidation = true;
157
158 /**
159 * Use SSL version
160 *
161 * @var int
162 * @access public
163 */
164 public $useSSLVersion = CURL_SSLVERSION_TLSv1;
165
166 /**
167 * Use PHP exceptions?
168 *
169 * @var bool
170 * @access public
171 */
172 public $useExceptions = false;
173
174 /**
175 * Time offset applied to time()
176 * @access private
177 */
178 private $__timeOffset = 0;
179
180 /**
181 * SSL client key
182 *
183 * @var bool
184 * @access public
185 */
186 public $sslKey = null;
187
188 /**
189 * SSL client certfificate
190 *
191 * @var string
192 * @acess public
193 */
194 public $sslCert = null;
195
196 /**
197 * SSL CA cert (only required if you are having problems with your system CA cert)
198 *
199 * @var string
200 * @access public
201 */
202 public $sslCACert = null;
203
204 /**
205 * AWS Key Pair ID
206 *
207 * @var string
208 * @access private
209 */
210 private $__signingKeyPairId = null;
211
212 /**
213 * Key resource, freeSigningKey() must be called to clear it from memory
214 *
215 * @var bool
216 * @access private
217 */
218 private $__signingKeyResource = false;
219
220 /**
221 * CURL progress function callback
222 *
223 * @var callable
224 * @access public
225 */
226 public $progressFunction = null;
227
228 /**
229 * Storage class for objects
230 *
231 * @var string
232 * @access public
233 */
234 public $storageClass = self::STORAGE_CLASS_STANDARD;
235
236 /**
237 * Server-side encryption
238 *
239 * @var string
240 * @access public
241 */
242 public $serverSideEncryption = self::SSE_NONE;
243
244 /**
245 * Constructor - if you're not using the class statically
246 *
247 * @param string $accessKey Access key
248 * @param string $secretKey Secret key
249 * @param boolean $useSSL Enable SSL
250 * @param string $endpoint Amazon URI
251 * @param string $region AWS region
252 * @param string $storageClass Storage class for objects
253 * @param string $sse Server-side encryption setting
254 * @return void
255 */
256 public function __construct($accessKey = null, $secretKey = null, $useSSL = false, $endpoint = 's3.amazonaws.com', $region = '', $storageClass = self::STORAGE_CLASS_STANDARD, $sse = self::SSE_NONE)
257 {
258 if ($accessKey !== null && $secretKey !== null)
259 $this->setAuth($accessKey, $secretKey);
260 $this->useSSL = $useSSL;
261 $this->endpoint = $endpoint;
262 $this->region = $region;
263 $this->storageClass = $storageClass;
264 $this->serverSideEncryption = $sse;
265 }
266
267 /**
268 * Set the storage class for objects
269 *
270 *
271 * @param string $storageClass The storage class to set
272 * @return void
273 */
274 public function setStorageClass($storageClass)
275 {
276 $this->storageClass = $storageClass;
277 }
278
279 /**
280 * Set the server-side encryption setting
281 *
282 *
283 * @param string $sse The server-side encryption setting to set
284 * @return void
285 */
286 public function setServerSideEncryption($sse)
287 {
288 $this->serverSideEncryption = $sse;
289 }
290
291
292 /**
293 * Set the service endpoint
294 *
295 * @param string $host Hostname
296 * @return void
297 */
298 public function setEndpoint($host)
299 {
300 $this->endpoint = $host;
301 }
302
303
304 /**
305 * Set the service region
306 *
307 * @param string $region
308 * @return void
309 */
310 public function setRegion($region)
311 {
312 $this->region = $region;
313
314 $this->updateEndpointIfRequired();
315 }
316
317 private function updateEndpointIfRequired()
318 {
319 if ($this->endpoint == 's3.wasabisys.com' && $this->region !== 'us-east-1')
320 {
321 $this->endpoint = 's3.'. $this->region .'.wasabisys.com';
322 }
323 }
324
325
326 /**
327 * Get the service region
328 *
329 * @return string $region
330 */
331 public function getRegion()
332 {
333 $region = $this->region;
334
335 // parse region from endpoint if not specific
336 if (empty($region))
337 {
338 if (preg_match("/s3[.-](?:website-|dualstack\.)?(.+)\.amazonaws\.com/i", $this->endpoint, $match) !== 0
339 && strtolower($match[1]) !== "external-1")
340 {
341 $region = $match[1];
342 }
343 }
344
345 return empty($region) ? 'us-east-1' : $region;
346 }
347
348
349 /**
350 * Set AWS access key and secret key
351 *
352 * @param string $accessKey Access key
353 * @param string $secretKey Secret key
354 * @return void
355 */
356 public function setAuth($accessKey, $secretKey)
357 {
358 $this->__accessKey = $accessKey;
359 $this->__secretKey = $secretKey;
360 }
361
362
363 /**
364 * Check if AWS keys have been set
365 *
366 * @return boolean
367 */
368 public function hasAuth() {
369 return ($this->__accessKey !== null && $this->__secretKey !== null);
370 }
371
372
373 /**
374 * Set SSL on or off
375 *
376 * @param boolean $enabled SSL enabled
377 * @param boolean $validate SSL certificate validation
378 * @return void
379 */
380 public function setSSL($enabled, $validate = true)
381 {
382 $this->useSSL = $enabled;
383 $this->useSSLValidation = $validate;
384 }
385
386
387 /**
388 * Set SSL client certificates (experimental)
389 *
390 * @param string $sslCert SSL client certificate
391 * @param string $sslKey SSL client key
392 * @param string $sslCACert SSL CA cert (only required if you are having problems with your system CA cert)
393 * @return void
394 */
395 public function setSSLAuth($sslCert = null, $sslKey = null, $sslCACert = null)
396 {
397 $this->sslCert = $sslCert;
398 $this->sslKey = $sslKey;
399 $this->sslCACert = $sslCACert;
400 }
401
402
403 /**
404 * Set proxy information
405 *
406 * @param string $host Proxy hostname and port (localhost:1234)
407 * @param string $user Proxy username
408 * @param string $pass Proxy password
409 * @param int $type CURL proxy type
410 * @return void
411 */
412 public function setProxy($host, $user = null, $pass = null, $type = CURLPROXY_SOCKS5)
413 {
414 $this->proxy = array('host' => $host, 'type' => $type, 'user' => $user, 'pass' => $pass);
415 }
416
417
418 /**
419 * Set the error mode to exceptions
420 *
421 * @param boolean $enabled Enable exceptions
422 * @return void
423 */
424 public function setExceptions($enabled = true)
425 {
426 $this->useExceptions = $enabled;
427 }
428
429
430 /**
431 * Set AWS time correction offset (use carefully)
432 *
433 * This can be used when an inaccurate system time is generating
434 * invalid request signatures. It should only be used as a last
435 * resort when the system time cannot be changed.
436 *
437 * @param string $offset Time offset (set to zero to use AWS server time)
438 * @return void
439 */
440 public function setTimeCorrectionOffset($offset = 0)
441 {
442 if ($offset == 0)
443 {
444 $rest = new S3Request( $this, 'HEAD');
445 $rest = $rest->getResponse();
446 $awstime = $rest->headers['date'];
447 $systime = time();
448 $offset = $systime > $awstime ? -($systime - $awstime) : ($awstime - $systime);
449 }
450 $this->__timeOffset = $offset;
451 }
452
453 /**
454 * Create folder in bucket
455 *
456 * @param string $bucket Bucket name
457 * @param string $folder Folder name
458 * @param string $acl ACL constant
459 * @return boolean
460 */
461 public function createFolder($bucket, $folder, $acl = self::ACL_PRIVATE)
462 {
463 $rest = new S3Request( $this, 'PUT', $bucket, $folder.'/', $this->endpoint);
464 $rest->setAmzHeader('x-amz-acl', $acl);
465 $rest->setHeader('Content-Length', 0);
466 $rest = $rest->getResponse();
467 if ($rest->error === false && $rest->code !== 200)
468 $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
469 if ($rest->error !== false)
470 {
471 $this->__triggerError(sprintf("S3::createFolder({$bucket}, {$folder}, {$acl}): [%s] %s",
472 $rest->error['code'], $rest->error['message']), __FILE__, __LINE__, $rest->code);
473 return false;
474 }
475 return true;
476 }
477
478 /**
479 * Internal error handler
480 *
481 * @internal Internal error handler
482 * @param string $message Error message
483 * @param string $file Filename
484 * @param integer $line Line number
485 * @param integer $code Error code
486 * @return void
487 */
488 private function __triggerError($message, $file, $line, $code = 0)
489 {
490 if ($this->useExceptions)
491 throw new S3Exception($message, $file, $line, $code);
492 else
493 {
494 $context = [
495 'error' => $message,
496 'file' => $file,
497 'line' => $line,
498 'code' => $code
499 ];
500 trigger_error(json_encode($context), E_USER_WARNING);
501 }
502 }
503
504
505 /**
506 * Get a list of buckets
507 *
508 * @param boolean $detailed Returns detailed bucket list when true
509 * @return array | false
510 */
511 public function listBuckets($detailed = false)
512 {
513 $rest = new S3Request( $this, 'GET', '', '', $this->endpoint);
514 $rest = $rest->getResponse();
515 if ($rest->error === false && $rest->code !== 200)
516 $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
517 if ($rest->error !== false)
518 {
519 $this->__triggerError(sprintf("S3::listBuckets(): [%s] %s", $rest->error['code'],
520 $rest->error['message']), __FILE__, __LINE__, $rest->code);
521 return false;
522 }
523 $results = array();
524 if (!isset($rest->body->Buckets)) return $results;
525
526 if ($detailed)
527 {
528 if (isset($rest->body->Owner, $rest->body->Owner->ID, $rest->body->Owner->DisplayName))
529 $results['owner'] = array(
530 'id' => (string)$rest->body->Owner->ID, 'name' => (string)$rest->body->Owner->DisplayName
531 );
532 $results['buckets'] = array();
533 foreach ($rest->body->Buckets->Bucket as $b)
534 $results['buckets'][] = array(
535 'name' => (string)$b->Name, 'time' => strtotime((string)$b->CreationDate)
536 );
537 } else
538 foreach ($rest->body->Buckets->Bucket as $b) $results[] = (string)$b->Name;
539
540 return $results;
541 }
542
543
544 /**
545 * Get contents for a bucket
546 *
547 * If maxKeys is null this method will loop through truncated result sets
548 *
549 * @param string $bucket Bucket name
550 * @param string $prefix Prefix
551 * @param string $marker Marker (last file listed)
552 * @param string $maxKeys Max keys (maximum number of keys to return)
553 * @param string $delimiter Delimiter
554 * @param boolean $returnCommonPrefixes Set to true to return CommonPrefixes
555 * @return array | false
556 */
557 public function getBucket($bucket, $prefix = null, $marker = null, $maxKeys = null, $delimiter = null, $returnCommonPrefixes = false)
558 {
559 $rest = new S3Request( $this, 'GET', $bucket, '', $this->endpoint);
560 if ($maxKeys == 0) $maxKeys = null;
561 if ($prefix !== null && $prefix !== '') $rest->setParameter('prefix', $prefix);
562 if ($marker !== null && $marker !== '') $rest->setParameter('marker', $marker);
563 if ($maxKeys !== null && $maxKeys !== '') $rest->setParameter('max-keys', $maxKeys);
564 if ($delimiter !== null && $delimiter !== '') $rest->setParameter('delimiter', $delimiter);
565 else if (!empty($this->defDelimiter)) $rest->setParameter('delimiter', $this->defDelimiter);
566 $response = $rest->getResponse();
567 if ($response->error === false && $response->code !== 200)
568 $response->error = array('code' => $response->code, 'message' => 'Unexpected HTTP status');
569 if ($response->error !== false)
570 {
571 $this->__triggerError(sprintf("S3::getBucket(): [%s] %s",
572 $response->error['code'], $response->error['message']), __FILE__, __LINE__, $response->code);
573 error_log("HEREEEEEEEEEEEE");
574 return false;
575 }
576
577 $results = array();
578
579 $nextMarker = null;
580 if (isset($response->body, $response->body->Contents))
581 foreach ($response->body->Contents as $c)
582 {
583 $results[(string)$c->Key] = array(
584 'name' => (string)$c->Key,
585 'time' => strtotime((string)$c->LastModified),
586 'size' => (int)$c->Size,
587 'hash' => substr((string)$c->ETag, 1, -1)
588 );
589 $nextMarker = (string)$c->Key;
590 }
591
592 if ($returnCommonPrefixes && isset($response->body, $response->body->CommonPrefixes))
593 foreach ($response->body->CommonPrefixes as $c)
594 $results[(string)$c->Prefix] = array('prefix' => (string)$c->Prefix);
595
596 if (isset($response->body, $response->body->IsTruncated) &&
597 (string)$response->body->IsTruncated == 'false') return $results;
598
599 if (isset($response->body, $response->body->NextMarker))
600 $nextMarker = (string)$response->body->NextMarker;
601
602 // Loop through truncated results if maxKeys isn't specified
603 if ($maxKeys == null && $nextMarker !== null && (string)$response->body->IsTruncated == 'true')
604 do
605 {
606 $rest = new S3Request( $this, 'GET', $bucket, '', $this->endpoint);
607 if ($prefix !== null && $prefix !== '') $rest->setParameter('prefix', $prefix);
608 $rest->setParameter('marker', $nextMarker);
609 if ($delimiter !== null && $delimiter !== '') $rest->setParameter('delimiter', $delimiter);
610
611 if (($response = $rest->getResponse()) == false || $response->code !== 200) break;
612
613 if (isset($response->body, $response->body->Contents))
614 foreach ($response->body->Contents as $c)
615 {
616 $results[(string)$c->Key] = array(
617 'name' => (string)$c->Key,
618 'time' => strtotime((string)$c->LastModified),
619 'size' => (int)$c->Size,
620 'hash' => substr((string)$c->ETag, 1, -1)
621 );
622 $nextMarker = (string)$c->Key;
623 }
624
625 if ($returnCommonPrefixes && isset($response->body, $response->body->CommonPrefixes))
626 foreach ($response->body->CommonPrefixes as $c)
627 $results[(string)$c->Prefix] = array('prefix' => (string)$c->Prefix);
628
629 if (isset($response->body, $response->body->NextMarker))
630 $nextMarker = (string)$response->body->NextMarker;
631
632 } while ($response !== false && (string)$response->body->IsTruncated == 'true');
633
634 return $results;
635 }
636
637
638 /**
639 * Put a bucket
640 *
641 * @param string $bucket Bucket name
642 * @param string $acl ACL flag
643 * @param string $location Set as "EU" to create buckets hosted in Europe
644 * @return boolean
645 */
646 public function putBucket($bucket, $acl = self::ACL_PRIVATE, $location = false)
647 {
648 $rest = new S3Request( $this, 'PUT', $bucket, '', $this->endpoint);
649 $rest->setAmzHeader('x-amz-acl', $acl);
650
651 if ($location === false) $location = $this->getRegion();
652
653 if ($location !== false && $location !== "us-east-1")
654 {
655 $dom = new DOMDocument;
656 $createBucketConfiguration = $dom->createElement('CreateBucketConfiguration');
657 $locationConstraint = $dom->createElement('LocationConstraint', $location);
658 $createBucketConfiguration->appendChild($locationConstraint);
659 $dom->appendChild($createBucketConfiguration);
660 $rest->data = $dom->saveXML();
661 $rest->size = strlen($rest->data);
662 $rest->setHeader('Content-Type', 'application/xml');
663 }
664 $rest = $rest->getResponse();
665
666 if ($rest->error === false && $rest->code !== 200)
667 $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
668 if ($rest->error !== false)
669 {
670 $this->__triggerError(sprintf("S3::putBucket({$bucket}, {$acl}, {$location}): [%s] %s",
671 $rest->error['code'], $rest->error['message']), __FILE__, __LINE__, $rest->code);
672 return false;
673 }
674 return true;
675 }
676
677
678 /**
679 * Delete an empty bucket
680 *
681 * @param string $bucket Bucket name
682 * @return boolean
683 */
684 public function deleteBucket($bucket)
685 {
686 $rest = new S3Request( $this, 'DELETE', $bucket, '', $this->endpoint);
687 $rest = $rest->getResponse();
688 if ($rest->error === false && $rest->code !== 204)
689 $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
690 if ($rest->error !== false)
691 {
692 $this->__triggerError(sprintf("S3::deleteBucket({$bucket}): [%s] %s",
693 $rest->error['code'], $rest->error['message']), __FILE__, __LINE__, $rest->code);
694 return false;
695 }
696 return true;
697 }
698
699
700 /**
701 * Create input info array for putObject()
702 *
703 * @param string $file Input file
704 * @param mixed $md5sum Use MD5 hash (supply a string if you want to use your own)
705 * @return array | false
706 */
707 public function inputFile($file, $md5sum = true)
708 {
709 if (!file_exists($file) || !is_file($file) || !is_readable($file))
710 {
711 $this->__triggerError('S3::inputFile(): Unable to open input file: '.$file, __FILE__, __LINE__);
712 return false;
713 }
714 clearstatcache(false, $file);
715 return array('file' => $file, 'size' => filesize($file), 'md5sum' => $md5sum !== false ?
716 (is_string($md5sum) ? $md5sum : base64_encode(md5_file($file, true))) : '', 'sha256sum' => hash_file('sha256', $file));
717 }
718
719
720 /**
721 * Create input array info for putObject() with a resource
722 *
723 * @param string $resource Input resource to read from
724 * @param integer $bufferSize Input byte size
725 * @param string $md5sum MD5 hash to send (optional)
726 * @return array | false
727 */
728 public function inputResource(&$resource, $bufferSize = false, $md5sum = '')
729 {
730 if (!is_resource($resource) || (int)$bufferSize < 0)
731 {
732 $this->__triggerError('S3::inputResource(): Invalid resource or buffer size', __FILE__, __LINE__);
733 return false;
734 }
735
736 // Try to figure out the bytesize
737 if ($bufferSize === false)
738 {
739 if (fseek($resource, 0, SEEK_END) < 0 || ($bufferSize = ftell($resource)) === false)
740 {
741 $this->__triggerError('S3::inputResource(): Unable to obtain resource size', __FILE__, __LINE__);
742 return false;
743 }
744 fseek($resource, 0);
745 }
746
747 $input = array('size' => $bufferSize, 'md5sum' => $md5sum);
748 $input['fp'] =& $resource;
749 return $input;
750 }
751
752
753 /**
754 * Put an object
755 *
756 * @param mixed $input Input data
757 * @param string $bucket Bucket name
758 * @param string $uri Object URI
759 * @param string $acl ACL constant
760 * @param array $metaHeaders Array of x-amz-meta-* headers
761 * @param array|string $requestHeaders Array of request headers or content type as a string
762 * @return boolean
763 */
764 public function putObject($input, $bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $requestHeaders = array())
765 {
766 if ($input === false) return false;
767 $rest = new S3Request( $this, 'PUT', $bucket, $uri, $this->endpoint);
768
769 if (!is_array($input)) $input = array(
770 'data' => $input, 'size' => strlen($input),
771 'md5sum' => base64_encode(md5($input, true)),
772 'sha256sum' => hash('sha256', $input)
773 );
774
775 // Data
776 if (isset($input['fp']))
777 $rest->fp =& $input['fp'];
778 elseif (isset($input['file']))
779 $rest->fp = @fopen($input['file'], 'rb');
780 elseif (isset($input['data']))
781 $rest->data = $input['data'];
782
783 // Content-Length (required)
784 if (isset($input['size']) && $input['size'] >= 0)
785 $rest->size = $input['size'];
786 else {
787 if (isset($input['file'])) {
788 clearstatcache(false, $input['file']);
789 $rest->size = filesize($input['file']);
790 }
791 elseif (isset($input['data']))
792 $rest->size = strlen($input['data']);
793 }
794
795 // Custom request headers (Content-Type, Content-Disposition, Content-Encoding)
796 if (is_array($requestHeaders))
797 foreach ($requestHeaders as $h => $v)
798 strpos($h, 'x-amz-') === 0 ? $rest->setAmzHeader($h, $v) : $rest->setHeader($h, $v);
799 elseif (is_string($requestHeaders)) // Support for legacy contentType parameter
800 $input['type'] = $requestHeaders;
801
802 // Content-Type
803 if (!isset($input['type']))
804 {
805 if (isset($requestHeaders['Content-Type']))
806 $input['type'] =& $requestHeaders['Content-Type'];
807 elseif (isset($input['file']))
808 $input['type'] = $this->__getMIMEType($input['file']);
809 else
810 $input['type'] = 'application/octet-stream';
811 }
812
813 if ($this->storageClass !== self::STORAGE_CLASS_STANDARD) // Storage class
814 $rest->setAmzHeader('x-amz-storage-class', $this->storageClass);
815
816 if ($this->serverSideEncryption !== self::SSE_NONE) // Server-side encryption
817 $rest->setAmzHeader('x-amz-server-side-encryption', $this->serverSideEncryption);
818
819 // We need to post with Content-Length and Content-Type, MD5 is optional
820 if ($rest->size >= 0 && ($rest->fp !== false || $rest->data !== false))
821 {
822 $rest->setHeader('Content-Type', $input['type']);
823 if (isset($input['md5sum'])) $rest->setHeader('Content-MD5', $input['md5sum']);
824
825 if (isset($input['sha256sum'])) $rest->setAmzHeader('x-amz-content-sha256', $input['sha256sum']);
826
827 $rest->setAmzHeader('x-amz-acl', $acl);
828 foreach ($metaHeaders as $h => $v) $rest->setAmzHeader('x-amz-meta-'.$h, $v);
829 $rest->getResponse();
830 } else
831 $rest->response->error = array('code' => 0, 'message' => 'Missing input parameters');
832
833 if ($rest->response->error === false && $rest->response->code !== 200)
834 $rest->response->error = array('code' => $rest->response->code, 'message' => 'Unexpected HTTP status');
835 if ($rest->response->error !== false)
836 {
837 $this->__triggerError(sprintf("S3::putObject(): [%s] %s",
838 $rest->response->error['code'], $rest->response->error['message']), __FILE__, __LINE__, $rest->response->code);
839 return false;
840 }
841 return true;
842 }
843
844
845 /**
846 * Put an object from a file (legacy function)
847 *
848 * @param string $file Input file path
849 * @param string $bucket Bucket name
850 * @param string $uri Object URI
851 * @param string $acl ACL constant
852 * @param array $metaHeaders Array of x-amz-meta-* headers
853 * @param string $contentType Content type
854 * @return boolean
855 */
856 public function putObjectFile($file, $bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $contentType = null)
857 {
858 return $this->putObject($this->inputFile($file), $bucket, $uri, $acl, $metaHeaders, $contentType);
859 }
860
861
862 /**
863 * Put an object from a string (legacy function)
864 *
865 * @param string $string Input data
866 * @param string $bucket Bucket name
867 * @param string $uri Object URI
868 * @param string $acl ACL constant
869 * @param array $metaHeaders Array of x-amz-meta-* headers
870 * @param string $contentType Content type
871 * @return boolean
872 */
873 public function putObjectString($string, $bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $contentType = 'text/plain')
874 {
875 return $this->putObject($string, $bucket, $uri, $acl, $metaHeaders, $contentType);
876 }
877
878
879
880 /**
881 * Create a multipart upload
882 *
883 * @param string $bucket Bucket name
884 * @param string $uri Object URI
885 * @param string $acl ACL constant
886 * @param array $metaHeaders Array of x-amz-meta-* headers
887 * @param array $requestHeaders Array of request headers (content type, disposition, etc.)
888 *
889 * @return string|false
890 */
891 public function createMultipartUpload($bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $requestHeaders = array())
892 {
893 $rest = new S3Request( $this, 'POST', $bucket, $uri, $this->endpoint);
894 $rest->setParameter('uploads', '');
895
896 if (is_array($requestHeaders))
897 foreach ($requestHeaders as $h => $v) $rest->setHeader($h, $v);
898
899 if ($this->storageClass !== self::STORAGE_CLASS_STANDARD) // Storage class
900 $rest->setAmzHeader('x-amz-storage-class', $this->storageClass);
901
902 if ($this->serverSideEncryption !== self::SSE_NONE) // Server-side encryption
903 $rest->setAmzHeader('x-amz-server-side-encryption', $this->serverSideEncryption);
904
905 $rest->setAmzHeader('x-amz-acl', $acl);
906 foreach ($metaHeaders as $h => $v) $rest->setAmzHeader('x-amz-meta-'.$h, $v);
907
908 $rest = $rest->getResponse();
909
910 if ($rest->error === false && $rest->code !== 200)
911 $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
912
913 if ($rest->error !== false){
914 $this->__triggerError(sprintf("S3::initiateMultipartUpload(): [%s] %s",
915 $rest->error['code'], $rest->error['message']), __FILE__, __LINE__, $rest->code);
916 return false;
917 }
918
919 $uploadId = (string) $rest->body->UploadId;
920 return $uploadId;
921 }
922
923 /**
924 * Upload a part in a multipart upload
925 *
926 * @param string $bucket Bucket name
927 * @param string $uri Object URI
928 * @param string $uploadId Upload ID
929 * @param int $partNumber Part number
930 * @param array|string $input Input data
931 *
932 * @return string|false
933 */
934 public function uploadPart($bucket, $uri, $uploadId, $partNumber, $input)
935 {
936 if ($input === false) return false;
937 $rest = new S3Request( $this, 'PUT', $bucket, $uri, $this->endpoint);
938 $rest->setParameter('partNumber', $partNumber);
939 $rest->setParameter('uploadId', $uploadId);
940
941 $rest->setHeader('Content-Type', 'application/octet-stream');
942
943 if (!is_array($input)) $input = array(
944 'data' => $input, 'size' => strlen($input),
945 'md5sum' => base64_encode(md5($input, true)),
946 'sha256sum' => hash('sha256', $input)
947 );
948
949 // Data
950 if (isset($input['fp']))
951 $rest->fp =& $input['fp'];
952 elseif (isset($input['file']))
953 $rest->fp = @fopen($input['file'], 'rb');
954 elseif (isset($input['data']))
955 $rest->data = $input['data'];
956
957 // Content-Length (required)
958 if (isset($input['size']) && $input['size'] >= 0)
959 $rest->size = $input['size'];
960 else {
961 if (isset($input['file'])) {
962 clearstatcache(false, $input['file']);
963 $rest->size = filesize($input['file']);
964 }
965 elseif (isset($input['data']))
966 $rest->size = strlen($input['data']);
967 }
968
969 if (isset($input['md5sum'])) $rest->setHeader('Content-MD5', $input['md5sum']);
970
971 $rest = $rest->getResponse();
972
973 if ($rest->error === false && $rest->code !== 200)
974 $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
975
976 if ($rest->error !== false){
977 $this->__triggerError(sprintf("S3::uploadPart(): [%s] %s",
978 $rest->error['code'], $rest->error['message']), __FILE__, __LINE__, $rest->code);
979 return false;
980 }
981
982 $etag = (string) $rest->headers['hash'];
983 return $etag;
984 }
985
986 /**
987 * Complete a multipart upload
988 *
989 * @param string $bucket Bucket name
990 * @param string $uri Object URI
991 * @param string $uploadId Upload ID
992 * @param array $parts Array of parts [ PartNumber => PartETag ]
993 *
994 * @return bool
995 */
996 public function completeMultipartUpload($bucket, $uri, $uploadId, $parts)
997 {
998 $rest = new S3Request( $this, 'POST', $bucket, $uri, $this->endpoint);
999 $rest->setParameter('uploadId', $uploadId);
1000
1001
1002 $dom = new DOMDocument;
1003 $completeMultipartUpload = $dom->createElement('CompleteMultipartUpload');
1004 $dom->appendChild($completeMultipartUpload);
1005
1006 foreach ($parts as $partNumber => $etag) {
1007 $part = $dom->createElement('Part');
1008 $partNumberNode = $dom->createElement('PartNumber', $partNumber);
1009 $etagNode = $dom->createElement('ETag', $etag);
1010 $part->appendChild($partNumberNode);
1011 $part->appendChild($etagNode);
1012 $completeMultipartUpload->appendChild($part);
1013 }
1014
1015 $rest->data = $dom->saveXML();
1016 $rest->size = strlen($rest->data);
1017 $rest->setHeader('Content-Type', 'application/xml');
1018
1019 $rest = $rest->getResponse();
1020
1021 if ($rest->error === false && $rest->code !== 200)
1022 $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
1023
1024 if ($rest->error !== false){
1025 $this->__triggerError(sprintf("S3::completeMultipartUpload(): [%s] %s",
1026 $rest->error['code'], $rest->error['message']), __FILE__, __LINE__, $rest->code);
1027 return false;
1028 }
1029
1030 return true;
1031 }
1032
1033 /**
1034 * Abort a multipart upload
1035 *
1036 * @param string $bucket Bucket name
1037 * @param string $uri Object URI
1038 * @param string $uploadId Upload ID
1039 *
1040 * @return bool
1041 */
1042 public function abortMultipartUpload($bucket, $uri, $uploadId)
1043 {
1044 $rest = new S3Request( $this, 'DELETE', $bucket, $uri, $this->endpoint);
1045 $rest->setParameter('uploadId', $uploadId);
1046
1047 $rest = $rest->getResponse();
1048
1049 if ($rest->error === false && $rest->code !== 204)
1050 $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
1051
1052 if ($rest->error !== false){
1053 $this->__triggerError(sprintf("S3::abortMultipartUpload(): [%s] %s",
1054 $rest->error['code'], $rest->error['message']), __FILE__, __LINE__, $rest->code);
1055 return false;
1056 }
1057
1058 return true;
1059 }
1060
1061
1062 /**
1063 * Get an object
1064 *
1065 * @param string $bucket Bucket name
1066 * @param string $uri Object URI
1067 * @param mixed $saveTo Filename or resource to write to
1068 * @param string $range Range
1069 * @return mixed
1070 */
1071 public function getObject($bucket, $uri, $saveTo = false, $range = null)
1072 {
1073 $rest = new S3Request( $this, 'GET', $bucket, $uri, $this->endpoint);
1074 if ($range !== null) $rest->setHeader('Range', $range);
1075 if ($saveTo !== false)
1076 {
1077 if (is_resource($saveTo))
1078 $rest->fp =& $saveTo;
1079 else
1080 if (($rest->fp = @fopen($saveTo, 'wb')) !== false)
1081 $rest->file = realpath($saveTo);
1082 else
1083 $rest->response->error = array('code' => 0, 'message' => 'Unable to open save file for writing: '.$saveTo);
1084 }
1085 if ($rest->response->error === false) $rest->getResponse();
1086
1087 if ($rest->response->error === false && !in_array($rest->response->code, array(200, 206)))
1088 $rest->response->error = array('code' => $rest->response->code, 'message' => 'Unexpected HTTP status');
1089 if ($rest->response->error !== false)
1090 {
1091 $this->__triggerError(sprintf("S3::getObject({$bucket}, {$uri}): [%s] %s",
1092 $rest->response->error['code'], $rest->response->error['message']), __FILE__, __LINE__, $rest->response->code);
1093 return false;
1094 }
1095 return $rest->response;
1096 }
1097
1098
1099 /**
1100 * Get object information
1101 *
1102 * @param string $bucket Bucket name
1103 * @param string $uri Object URI
1104 * @param boolean $returnInfo Return response information
1105 * @return mixed | false
1106 */
1107 public function getObjectInfo($bucket, $uri, $returnInfo = true)
1108 {
1109 $rest = new S3Request( $this, 'HEAD', $bucket, $uri, $this->endpoint);
1110 $rest = $rest->getResponse();
1111 if ($rest->error === false && ($rest->code !== 200 && $rest->code !== 404))
1112 $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
1113 if ($rest->error !== false)
1114 {
1115 $this->__triggerError(sprintf("S3::getObjectInfo({$bucket}, {$uri}): [%s] %s",
1116 $rest->error['code'], $rest->error['message']), __FILE__, __LINE__, $rest->code);
1117 return false;
1118 }
1119 return $rest->code == 200 ? $returnInfo ? $rest->headers : true : false;
1120 }
1121
1122
1123 /**
1124 * Copy an object
1125 *
1126 * @param string $srcBucket Source bucket name
1127 * @param string $srcUri Source object URI
1128 * @param string $bucket Destination bucket name
1129 * @param string $uri Destination object URI
1130 * @param string $acl ACL constant
1131 * @param array $metaHeaders Optional array of x-amz-meta-* headers
1132 * @param array $requestHeaders Optional array of request headers (content type, disposition, etc.)
1133 * @return mixed | false
1134 */
1135 public function copyObject($srcBucket, $srcUri, $bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $requestHeaders = array())
1136 {
1137 $rest = new S3Request( $this, 'PUT', $bucket, $uri, $this->endpoint);
1138 $rest->setHeader('Content-Length', 0);
1139 foreach ($requestHeaders as $h => $v)
1140 strpos($h, 'x-amz-') === 0 ? $rest->setAmzHeader($h, $v) : $rest->setHeader($h, $v);
1141 foreach ($metaHeaders as $h => $v) $rest->setAmzHeader('x-amz-meta-'.$h, $v);
1142 if ($this->storageClass !== self::STORAGE_CLASS_STANDARD) // Storage class
1143 $rest->setAmzHeader('x-amz-storage-class', $this->storageClass);
1144 $rest->setAmzHeader('x-amz-acl', $acl);
1145 $rest->setAmzHeader('x-amz-copy-source', sprintf('/%s/%s', $srcBucket, rawurlencode($srcUri)));
1146 if (sizeof($requestHeaders) > 0 || sizeof($metaHeaders) > 0)
1147 $rest->setAmzHeader('x-amz-metadata-directive', 'REPLACE');
1148
1149 $rest = $rest->getResponse();
1150 if ($rest->error === false && $rest->code !== 200)
1151 $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
1152 if ($rest->error !== false)
1153 {
1154 $this->__triggerError(sprintf("S3::copyObject({$srcBucket}, {$srcUri}, {$bucket}, {$uri}): [%s] %s",
1155 $rest->error['code'], $rest->error['message']), __FILE__, __LINE__, $rest->code);
1156 return false;
1157 }
1158 return isset($rest->body->LastModified, $rest->body->ETag) ? array(
1159 'time' => strtotime((string)$rest->body->LastModified),
1160 'hash' => substr((string)$rest->body->ETag, 1, -1)
1161 ) : false;
1162 }
1163
1164
1165 /**
1166 * Set up a bucket redirection
1167 *
1168 * @param string $bucket Bucket name
1169 * @param string $location Target host name
1170 * @return boolean
1171 */
1172 public function setBucketRedirect($bucket = NULL, $location = NULL)
1173 {
1174 $rest = new S3Request( $this, 'PUT', $bucket, '', $this->endpoint);
1175
1176 if( empty($bucket) || empty($location) ) {
1177 $this->__triggerError("S3::setBucketRedirect({$bucket}, {$location}): Empty parameter.", __FILE__, __LINE__);
1178 return false;
1179 }
1180
1181 $dom = new DOMDocument;
1182 $websiteConfiguration = $dom->createElement('WebsiteConfiguration');
1183 $redirectAllRequestsTo = $dom->createElement('RedirectAllRequestsTo');
1184 $hostName = $dom->createElement('HostName', $location);
1185 $redirectAllRequestsTo->appendChild($hostName);
1186 $websiteConfiguration->appendChild($redirectAllRequestsTo);
1187 $dom->appendChild($websiteConfiguration);
1188 $rest->setParameter('website', null);
1189 $rest->data = $dom->saveXML();
1190 $rest->size = strlen($rest->data);
1191 $rest->setHeader('Content-Type', 'application/xml');
1192 $rest = $rest->getResponse();
1193
1194 if ($rest->error === false && $rest->code !== 200)
1195 $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
1196 if ($rest->error !== false)
1197 {
1198 $this->__triggerError(sprintf("S3::setBucketRedirect({$bucket}, {$location}): [%s] %s",
1199 $rest->error['code'], $rest->error['message']), __FILE__, __LINE__, $rest->code);
1200 return false;
1201 }
1202 return true;
1203 }
1204
1205
1206 /**
1207 * Set logging for a bucket
1208 *
1209 * @param string $bucket Bucket name
1210 * @param string $targetBucket Target bucket (where logs are stored)
1211 * @param string $targetPrefix Log prefix (e,g; domain.com-)
1212 * @return boolean
1213 */
1214 public function setBucketLogging($bucket, $targetBucket, $targetPrefix = null)
1215 {
1216 // The S3 log delivery group has to be added to the target bucket's ACP
1217 if ($targetBucket !== null && ($acp = $this->getAccessControlPolicy($targetBucket, '')) !== false)
1218 {
1219 // Only add permissions to the target bucket when they do not exist
1220 $aclWriteSet = false;
1221 $aclReadSet = false;
1222 foreach ($acp['acl'] as $acl)
1223 if ($acl['type'] == 'Group' && $acl['uri'] == 'http://acs.amazonaws.com/groups/s3/LogDelivery')
1224 {
1225 if ($acl['permission'] == 'WRITE') $aclWriteSet = true;
1226 elseif ($acl['permission'] == 'READ_ACP') $aclReadSet = true;
1227 }
1228 if (!$aclWriteSet) $acp['acl'][] = array(
1229 'type' => 'Group', 'uri' => 'http://acs.amazonaws.com/groups/s3/LogDelivery', 'permission' => 'WRITE'
1230 );
1231 if (!$aclReadSet) $acp['acl'][] = array(
1232 'type' => 'Group', 'uri' => 'http://acs.amazonaws.com/groups/s3/LogDelivery', 'permission' => 'READ_ACP'
1233 );
1234 if (!$aclReadSet || !$aclWriteSet) $this->setAccessControlPolicy($targetBucket, '', $acp);
1235 }
1236
1237 $dom = new DOMDocument;
1238 $bucketLoggingStatus = $dom->createElement('BucketLoggingStatus');
1239 $bucketLoggingStatus->setAttribute('xmlns', 'http://s3.amazonaws.com/doc/2006-03-01/');
1240 if ($targetBucket !== null)
1241 {
1242 if ($targetPrefix == null) $targetPrefix = $bucket . '-';
1243 $loggingEnabled = $dom->createElement('LoggingEnabled');
1244 $loggingEnabled->appendChild($dom->createElement('TargetBucket', $targetBucket));
1245 $loggingEnabled->appendChild($dom->createElement('TargetPrefix', $targetPrefix));
1246 // TODO: Add TargetGrants?
1247 $bucketLoggingStatus->appendChild($loggingEnabled);
1248 }
1249 $dom->appendChild($bucketLoggingStatus);
1250
1251 $rest = new S3Request( $this, 'PUT', $bucket, '', $this->endpoint);
1252 $rest->setParameter('logging', null);
1253 $rest->data = $dom->saveXML();
1254 $rest->size = strlen($rest->data);
1255 $rest->setHeader('Content-Type', 'application/xml');
1256 $rest = $rest->getResponse();
1257 if ($rest->error === false && $rest->code !== 200)
1258 $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
1259 if ($rest->error !== false)
1260 {
1261 $this->__triggerError(sprintf("S3::setBucketLogging({$bucket}, {$targetBucket}): [%s] %s",
1262 $rest->error['code'], $rest->error['message']), __FILE__, __LINE__, $rest->code);
1263 return false;
1264 }
1265 return true;
1266 }
1267
1268
1269 /**
1270 * Get logging status for a bucket
1271 *
1272 * This will return false if logging is not enabled.
1273 * Note: To enable logging, you also need to grant write access to the log group
1274 *
1275 * @param string $bucket Bucket name
1276 * @return array | false
1277 */
1278 public function getBucketLogging($bucket)
1279 {
1280 $rest = new S3Request( $this, 'GET', $bucket, '', $this->endpoint);
1281 $rest->setParameter('logging', null);
1282 $rest = $rest->getResponse();
1283 if ($rest->error === false && $rest->code !== 200)
1284 $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
1285 if ($rest->error !== false)
1286 {
1287 $this->__triggerError(sprintf("S3::getBucketLogging({$bucket}): [%s] %s",
1288 $rest->error['code'], $rest->error['message']), __FILE__, __LINE__, $rest->code);
1289 return false;
1290 }
1291 if (!isset($rest->body->LoggingEnabled)) return false; // No logging
1292 return array(
1293 'targetBucket' => (string)$rest->body->LoggingEnabled->TargetBucket,
1294 'targetPrefix' => (string)$rest->body->LoggingEnabled->TargetPrefix,
1295 );
1296 }
1297
1298
1299 /**
1300 * Disable bucket logging
1301 *
1302 * @param string $bucket Bucket name
1303 * @return boolean
1304 */
1305 public function disableBucketLogging($bucket)
1306 {
1307 return $this->setBucketLogging($bucket, null);
1308 }
1309
1310
1311 /**
1312 * Get a bucket's location
1313 *
1314 * @param string $bucket Bucket name
1315 * @return string | false
1316 */
1317 public function getBucketLocation($bucket)
1318 {
1319 $rest = new S3Request( $this, 'GET', $bucket, '', $this->endpoint);
1320 $rest->setParameter('location', null);
1321 $rest = $rest->getResponse();
1322
1323 if (isset($rest->error['region']))
1324 return $rest->error['region'];
1325
1326 if ($rest->error === false && $rest->code !== 200)
1327 $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
1328 if ($rest->error !== false)
1329 {
1330 $this->__triggerError(sprintf("S3::getBucketLocation({$bucket}): [%s] %s",
1331 $rest->error['code'], $rest->error['message']), __FILE__, __LINE__, $rest->code);
1332 return false;
1333 }
1334 return (isset($rest->body[0]) && (string)$rest->body[0] !== '') ? (string)$rest->body[0] : 'us-east-1';
1335 }
1336
1337
1338 /**
1339 * Set object or bucket Access Control Policy
1340 *
1341 * @param string $bucket Bucket name
1342 * @param string $uri Object URI
1343 * @param array $acp Access Control Policy Data (same as the data returned from getAccessControlPolicy)
1344 * @return boolean
1345 */
1346 public function setAccessControlPolicy($bucket, $uri = '', $acp = array())
1347 {
1348 $dom = new DOMDocument;
1349 $dom->formatOutput = true;
1350 $accessControlPolicy = $dom->createElement('AccessControlPolicy');
1351 $accessControlList = $dom->createElement('AccessControlList');
1352
1353 // It seems the owner has to be passed along too
1354 $owner = $dom->createElement('Owner');
1355 $owner->appendChild($dom->createElement('ID', $acp['owner']['id']));
1356 $owner->appendChild($dom->createElement('DisplayName', $acp['owner']['name']));
1357 $accessControlPolicy->appendChild($owner);
1358
1359 foreach ($acp['acl'] as $g)
1360 {
1361 $grant = $dom->createElement('Grant');
1362 $grantee = $dom->createElement('Grantee');
1363 $grantee->setAttribute('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance');
1364 if (isset($g['id']))
1365 { // CanonicalUser (DisplayName is omitted)
1366 $grantee->setAttribute('xsi:type', 'CanonicalUser');
1367 $grantee->appendChild($dom->createElement('ID', $g['id']));
1368 }
1369 elseif (isset($g['email']))
1370 { // AmazonCustomerByEmail
1371 $grantee->setAttribute('xsi:type', 'AmazonCustomerByEmail');
1372 $grantee->appendChild($dom->createElement('EmailAddress', $g['email']));
1373 }
1374 elseif ($g['type'] == 'Group')
1375 { // Group
1376 $grantee->setAttribute('xsi:type', 'Group');
1377 $grantee->appendChild($dom->createElement('URI', $g['uri']));
1378 }
1379 $grant->appendChild($grantee);
1380 $grant->appendChild($dom->createElement('Permission', $g['permission']));
1381 $accessControlList->appendChild($grant);
1382 }
1383
1384 $accessControlPolicy->appendChild($accessControlList);
1385 $dom->appendChild($accessControlPolicy);
1386
1387 $rest = new S3Request( $this, 'PUT', $bucket, $uri, $this->endpoint);
1388 $rest->setParameter('acl', null);
1389 $rest->data = $dom->saveXML();
1390 $rest->size = strlen($rest->data);
1391 $rest->setHeader('Content-Type', 'application/xml');
1392 $rest = $rest->getResponse();
1393 if ($rest->error === false && $rest->code !== 200)
1394 $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
1395 if ($rest->error !== false)
1396 {
1397 $this->__triggerError(sprintf("S3::setAccessControlPolicy({$bucket}, {$uri}): [%s] %s",
1398 $rest->error['code'], $rest->error['message']), __FILE__, __LINE__, $rest->code);
1399 return false;
1400 }
1401 return true;
1402 }
1403
1404
1405 /**
1406 * Get object or bucket Access Control Policy
1407 *
1408 * @param string $bucket Bucket name
1409 * @param string $uri Object URI
1410 * @return mixed | false
1411 */
1412 public function getAccessControlPolicy($bucket, $uri = '')
1413 {
1414 $rest = new S3Request( $this, 'GET', $bucket, $uri, $this->endpoint);
1415 $rest->setParameter('acl', null);
1416 $rest = $rest->getResponse();
1417 if ($rest->error === false && $rest->code !== 200)
1418 $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
1419 if ($rest->error !== false)
1420 {
1421 $this->__triggerError(sprintf("S3::getAccessControlPolicy({$bucket}, {$uri}): [%s] %s",
1422 $rest->error['code'], $rest->error['message']), __FILE__, __LINE__, $rest->code);
1423 return false;
1424 }
1425
1426 $acp = array();
1427 if (isset($rest->body->Owner, $rest->body->Owner->ID, $rest->body->Owner->DisplayName))
1428 $acp['owner'] = array(
1429 'id' => (string)$rest->body->Owner->ID, 'name' => (string)$rest->body->Owner->DisplayName
1430 );
1431
1432 if (isset($rest->body->AccessControlList))
1433 {
1434 $acp['acl'] = array();
1435 foreach ($rest->body->AccessControlList->Grant as $grant)
1436 {
1437 foreach ($grant->Grantee as $grantee)
1438 {
1439 if (isset($grantee->ID, $grantee->DisplayName)) // CanonicalUser
1440 $acp['acl'][] = array(
1441 'type' => 'CanonicalUser',
1442 'id' => (string)$grantee->ID,
1443 'name' => (string)$grantee->DisplayName,
1444 'permission' => (string)$grant->Permission
1445 );
1446 elseif (isset($grantee->EmailAddress)) // AmazonCustomerByEmail
1447 $acp['acl'][] = array(
1448 'type' => 'AmazonCustomerByEmail',
1449 'email' => (string)$grantee->EmailAddress,
1450 'permission' => (string)$grant->Permission
1451 );
1452 elseif (isset($grantee->URI)) // Group
1453 $acp['acl'][] = array(
1454 'type' => 'Group',
1455 'uri' => (string)$grantee->URI,
1456 'permission' => (string)$grant->Permission
1457 );
1458 else continue;
1459 }
1460 }
1461 }
1462 return $acp;
1463 }
1464
1465
1466 /**
1467 * Delete an object
1468 *
1469 * @param string $bucket Bucket name
1470 * @param string $uri Object URI
1471 * @return boolean
1472 */
1473 public function deleteObject($bucket, $uri)
1474 {
1475 $rest = new S3Request( $this, 'DELETE', $bucket, $uri, $this->endpoint);
1476 $rest = $rest->getResponse();
1477 if ($rest->error === false && $rest->code !== 204)
1478 $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
1479 if ($rest->error !== false)
1480 {
1481 $this->__triggerError(sprintf("S3::deleteObject(): [%s] %s",
1482 $rest->error['code'], $rest->error['message']), __FILE__, __LINE__, $rest->code);
1483 return false;
1484 }
1485 return true;
1486 }
1487
1488
1489 /**
1490 * Get a query string authenticated URL
1491 *
1492 * @param string $bucket Bucket name
1493 * @param string $uri Object URI
1494 * @param integer $lifetime Lifetime in seconds
1495 * @param boolean $hostBucket Use the bucket name as the hostname
1496 * @param boolean $https Use HTTPS ($hostBucket should be false for SSL verification)
1497 * @return string
1498 */
1499 public function getAuthenticatedURL($bucket, $uri, $lifetime, $hostBucket = false, $https = false)
1500 {
1501 $expires = $this->__getTime() + $lifetime;
1502 $uri = str_replace(array('%2F', '%2B'), array('/', '+'), rawurlencode($uri));
1503 return sprintf(($https ? 'https' : 'http').'://%s/%s?AWSAccessKeyId=%s&Expires=%u&Signature=%s',
1504 // $hostBucket ? $bucket : $bucket.'.s3.amazonaws.com', $uri, $this->__accessKey, $expires,
1505 $hostBucket ? $bucket : $this->endpoint.'/'.$bucket, $uri, $this->__accessKey, $expires,
1506 urlencode($this->__getHash("GET\n\n\n{$expires}\n/{$bucket}/{$uri}")));
1507 }
1508
1509
1510 /**
1511 * Get upload POST parameters for form uploads
1512 *
1513 * @param string $bucket Bucket name
1514 * @param string $uriPrefix Object URI prefix
1515 * @param string $acl ACL constant
1516 * @param integer $lifetime Lifetime in seconds
1517 * @param integer $maxFileSize Maximum filesize in bytes (default 5MB)
1518 * @param string $successRedirect Redirect URL or 200 / 201 status code
1519 * @param array $amzHeaders Array of x-amz-meta-* headers
1520 * @param array $headers Array of request headers or content type as a string
1521 * @param boolean $flashVars Includes additional "Filename" variable posted by Flash
1522 * @return object
1523 */
1524 public function getHttpUploadPostParams($bucket, $uriPrefix = '', $acl = self::ACL_PRIVATE, $lifetime = 3600,
1525 $maxFileSize = 5242880, $successRedirect = "201", $amzHeaders = array(), $headers = array(), $flashVars = false)
1526 {
1527 // Create policy object
1528 $policy = new stdClass;
1529 $policy->expiration = gmdate('Y-m-d\TH:i:s\Z', ($this->__getTime() + $lifetime));
1530 $policy->conditions = array();
1531 $obj = new stdClass; $obj->bucket = $bucket; array_push($policy->conditions, $obj);
1532 $obj = new stdClass; $obj->acl = $acl; array_push($policy->conditions, $obj);
1533
1534 $obj = new stdClass; // 200 for non-redirect uploads
1535 if (is_numeric($successRedirect) && in_array((int)$successRedirect, array(200, 201)))
1536 $obj->success_action_status = (string)$successRedirect;
1537 else // URL
1538 $obj->success_action_redirect = $successRedirect;
1539 array_push($policy->conditions, $obj);
1540
1541 if ($acl !== self::ACL_PUBLIC_READ)
1542 array_push($policy->conditions, array('eq', '$acl', $acl));
1543
1544 array_push($policy->conditions, array('starts-with', '$key', $uriPrefix));
1545 if ($flashVars) array_push($policy->conditions, array('starts-with', '$Filename', ''));
1546 foreach (array_keys($headers) as $headerKey)
1547 array_push($policy->conditions, array('starts-with', '$'.$headerKey, ''));
1548 foreach ($amzHeaders as $headerKey => $headerVal)
1549 {
1550 $obj = new stdClass;
1551 $obj->{$headerKey} = (string)$headerVal;
1552 array_push($policy->conditions, $obj);
1553 }
1554 array_push($policy->conditions, array('content-length-range', 0, $maxFileSize));
1555 $policy = base64_encode(str_replace('\/', '/', json_encode($policy)));
1556
1557 // Create parameters
1558 $params = new stdClass;
1559 $params->AWSAccessKeyId = $this->__accessKey;
1560 $params->key = $uriPrefix.'${filename}';
1561 $params->acl = $acl;
1562 $params->policy = $policy; unset($policy);
1563 $params->signature = $this->__getHash($params->policy);
1564 if (is_numeric($successRedirect) && in_array((int)$successRedirect, array(200, 201)))
1565 $params->success_action_status = (string)$successRedirect;
1566 else
1567 $params->success_action_redirect = $successRedirect;
1568 foreach ($headers as $headerKey => $headerVal) $params->{$headerKey} = (string)$headerVal;
1569 foreach ($amzHeaders as $headerKey => $headerVal) $params->{$headerKey} = (string)$headerVal;
1570 return $params;
1571 }
1572
1573 /**
1574 * Get MIME type for file
1575 *
1576 * To override the putObject() Content-Type, add it to $requestHeaders
1577 *
1578 * To use fileinfo, ensure the MAGIC environment variable is set
1579 *
1580 * @internal Used to get mime types
1581 * @param string &$file File path
1582 * @return string
1583 */
1584 private function __getMIMEType(&$file)
1585 {
1586 static $exts = array(
1587 'jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'gif' => 'image/gif',
1588 'png' => 'image/png', 'ico' => 'image/x-icon', 'pdf' => 'application/pdf',
1589 'tif' => 'image/tiff', 'tiff' => 'image/tiff', 'svg' => 'image/svg+xml',
1590 'svgz' => 'image/svg+xml', 'swf' => 'application/x-shockwave-flash',
1591 'zip' => 'application/zip', 'gz' => 'application/x-gzip',
1592 'tar' => 'application/x-tar', 'bz' => 'application/x-bzip',
1593 'bz2' => 'application/x-bzip2', 'rar' => 'application/x-rar-compressed',
1594 'exe' => 'application/x-msdownload', 'msi' => 'application/x-msdownload',
1595 'cab' => 'application/vnd.ms-cab-compressed', 'txt' => 'text/plain',
1596 'asc' => 'text/plain', 'htm' => 'text/html', 'html' => 'text/html',
1597 'css' => 'text/css', 'js' => 'text/javascript',
1598 'xml' => 'text/xml', 'xsl' => 'application/xsl+xml',
1599 'ogg' => 'application/ogg', 'mp3' => 'audio/mpeg', 'wav' => 'audio/x-wav',
1600 'avi' => 'video/x-msvideo', 'mpg' => 'video/mpeg', 'mpeg' => 'video/mpeg',
1601 'mov' => 'video/quicktime', 'flv' => 'video/x-flv', 'php' => 'text/x-php'
1602 );
1603
1604 $ext = strtolower(pathinfo($file, PATHINFO_EXTENSION));
1605 if (isset($exts[$ext])) return $exts[$ext];
1606
1607 // Use fileinfo if available
1608 if (extension_loaded('fileinfo') && isset($_ENV['MAGIC']) &&
1609 ($finfo = finfo_open(FILEINFO_MIME, $_ENV['MAGIC'])) !== false)
1610 {
1611 if (($type = finfo_file($finfo, $file)) !== false)
1612 {
1613 // Remove the charset and grab the last content-type
1614 $type = explode(' ', str_replace('; charset=', ';charset=', $type));
1615 $type = array_pop($type);
1616 $type = explode(';', $type);
1617 $type = trim(array_shift($type));
1618 }
1619 finfo_close($finfo);
1620 if ($type !== false && strlen($type) > 0) return $type;
1621 }
1622
1623 return 'application/octet-stream';
1624 }
1625
1626
1627 /**
1628 * Get the current time
1629 *
1630 * @internal Used to apply offsets to sytem time
1631 * @return integer
1632 */
1633 public function __getTime()
1634 {
1635 return time() + $this->__timeOffset;
1636 }
1637
1638
1639 /**
1640 * Generate the auth string: "AWS AccessKey:Signature"
1641 *
1642 * @internal Used by S3Request::getResponse()
1643 * @param string $string String to sign
1644 * @return string
1645 */
1646 public function __getSignature($string)
1647 {
1648 return 'AWS '.$this->__accessKey.':'.$this->__getHash($string);
1649 }
1650
1651
1652 /**
1653 * Creates a HMAC-SHA1 hash
1654 *
1655 * This uses the hash extension if loaded
1656 *
1657 * @internal Used by __getSignature()
1658 * @param string $string String to sign
1659 * @return string
1660 */
1661 private function __getHash($string)
1662 {
1663 return base64_encode(extension_loaded('hash') ?
1664 hash_hmac('sha1', $string, $this->__secretKey, true) : pack('H*', sha1(
1665 (str_pad($this->__secretKey, 64, chr(0x00)) ^ (str_repeat(chr(0x5c), 64))) .
1666 pack('H*', sha1((str_pad($this->__secretKey, 64, chr(0x00)) ^
1667 (str_repeat(chr(0x36), 64))) . $string)))));
1668 }
1669
1670
1671 /**
1672 * Generate the headers for AWS Signature V4
1673 *
1674 * @internal Used by S3Request::getResponse()
1675 * @param array $amzHeaders
1676 * @param array $headers
1677 * @param string $method
1678 * @param string $uri
1679 * @param array $parameters
1680 * @return string
1681 */
1682 public function __getSignatureV4($amzHeaders, $headers, $method, $uri, $parameters)
1683 {
1684 $service = 's3';
1685 $region = $this->getRegion();
1686
1687 $algorithm = 'AWS4-HMAC-SHA256';
1688 $combinedHeaders = array();
1689
1690 $amzDateStamp = substr($amzHeaders['x-amz-date'], 0, 8);
1691
1692 // CanonicalHeaders
1693 foreach ($headers as $k => $v)
1694 $combinedHeaders[strtolower($k)] = trim($v);
1695 foreach ($amzHeaders as $k => $v)
1696 $combinedHeaders[strtolower($k)] = trim($v);
1697 uksort($combinedHeaders, [$this, '__sortMetaHeadersCmp']);
1698
1699 // Convert null query string parameters to strings and sort
1700 $parameters = array_map('strval', $parameters);
1701 uksort($parameters, [$this, '__sortMetaHeadersCmp']);
1702 $queryString = http_build_query($parameters, '', '&', PHP_QUERY_RFC3986);
1703
1704 // Payload
1705 $amzPayload = array($method);
1706
1707 $qsPos = strpos($uri, '?');
1708 $amzPayload[] = ($qsPos === false ? $uri : substr($uri, 0, $qsPos));
1709
1710 $amzPayload[] = $queryString;
1711 // add header as string to requests
1712 foreach ($combinedHeaders as $k => $v )
1713 {
1714 $amzPayload[] = $k . ':' . $v;
1715 }
1716 // add a blank entry so we end up with an extra line break
1717 $amzPayload[] = '';
1718 // SignedHeaders
1719 $amzPayload[] = implode(';', array_keys($combinedHeaders));
1720 // payload hash
1721 $amzPayload[] = $amzHeaders['x-amz-content-sha256'];
1722 // request as string
1723 $amzPayloadStr = implode("\n", $amzPayload);
1724
1725 // CredentialScope
1726 $credentialScope = array($amzDateStamp, $region, $service, 'aws4_request');
1727
1728 // stringToSign
1729 $stringToSignStr = implode("\n", array($algorithm, $amzHeaders['x-amz-date'],
1730 implode('/', $credentialScope), hash('sha256', $amzPayloadStr)));
1731
1732 // Make Signature
1733 $kSecret = 'AWS4' . $this->__secretKey;
1734 $kDate = hash_hmac('sha256', $amzDateStamp, $kSecret, true);
1735 $kRegion = hash_hmac('sha256', $region, $kDate, true);
1736 $kService = hash_hmac('sha256', $service, $kRegion, true);
1737 $kSigning = hash_hmac('sha256', 'aws4_request', $kService, true);
1738
1739 $signature = hash_hmac('sha256', $stringToSignStr, $kSigning);
1740
1741 return $algorithm . ' ' . implode(',', array(
1742 'Credential=' . $this->__accessKey . '/' . implode('/', $credentialScope),
1743 'SignedHeaders=' . implode(';', array_keys($combinedHeaders)),
1744 'Signature=' . $signature,
1745 ));
1746 }
1747
1748
1749 /**
1750 * Sort compare for meta headers
1751 *
1752 * @internal Used to sort x-amz meta headers
1753 * @param string $a String A
1754 * @param string $b String B
1755 * @return integer
1756 */
1757 private function __sortMetaHeadersCmp($a, $b)
1758 {
1759 $lenA = strlen($a);
1760 $lenB = strlen($b);
1761 $minLen = min($lenA, $lenB);
1762 $ncmp = strncmp($a, $b, $minLen);
1763 if ($lenA == $lenB) return $ncmp;
1764 if (0 == $ncmp) return $lenA < $lenB ? -1 : 1;
1765 return $ncmp;
1766 }
1767 }
1768
1769 /**
1770 * S3 Request class
1771 *
1772 * @link http://undesigned.org.za/2007/10/22/amazon-s3-php-class
1773 * @version 0.5.0-dev
1774 */
1775 final class S3Request
1776 {
1777
1778 /**
1779 * S3 Instance
1780 *
1781 * @var S3
1782 * @access private
1783 */
1784 private $s3;
1785
1786 /**
1787 * AWS URI
1788 *
1789 * @var string
1790 * @access private
1791 */
1792 private $endpoint;
1793
1794 /**
1795 * Verb
1796 *
1797 * @var string
1798 * @access private
1799 */
1800 private $verb;
1801
1802 /**
1803 * S3 bucket name
1804 *
1805 * @var string
1806 * @access private
1807 */
1808 private $bucket;
1809
1810 /**
1811 * Object URI
1812 *
1813 * @var string
1814 * @access private
1815 */
1816 private $uri;
1817
1818 /**
1819 * Final object URI
1820 *
1821 * @var string
1822 * @access private
1823 */
1824 private $resource = '';
1825
1826 /**
1827 * Additional request parameters
1828 *
1829 * @var array
1830 * @access private
1831 */
1832 private $parameters = array();
1833
1834 /**
1835 * Amazon specific request headers
1836 *
1837 * @var array
1838 * @access private
1839 */
1840 private $amzHeaders = array();
1841
1842 /**
1843 * HTTP request headers
1844 *
1845 * @var array
1846 * @access private
1847 */
1848 private $headers = array(
1849 'Host' => '', 'Date' => '', 'Content-MD5' => '', 'Content-Type' => ''
1850 );
1851
1852 /**
1853 * Use HTTP PUT?
1854 *
1855 * @var resource|bool
1856 * @access public
1857 */
1858 public $fp = false;
1859
1860 /**
1861 * PUT file size
1862 *
1863 * @var int
1864 * @access public
1865 */
1866 public $size = 0;
1867
1868 /**
1869 * PUT post fields
1870 *
1871 * @var array
1872 * @access public
1873 */
1874 public $data = false;
1875
1876 /**
1877 * S3 request respone
1878 *
1879 * @var object
1880 * @access public
1881 */
1882 public $response;
1883
1884
1885 /**
1886 * Constructor
1887 *
1888 * @param string $verb Verb
1889 * @param string $bucket Bucket name
1890 * @param string $uri Object URI
1891 * @param string $endpoint AWS endpoint URI
1892 * @return mixed
1893 */
1894 function __construct($s3, $verb, $bucket = '', $uri = '', $endpoint = 's3.amazonaws.com')
1895 {
1896 $this->s3 = $s3;
1897 $this->endpoint = $endpoint;
1898 $this->verb = $verb;
1899 $this->bucket = $bucket;
1900 $this->uri = $uri !== '' ? '/'.str_replace('%2F', '/', rawurlencode($uri)) : '/';
1901
1902 if ($this->bucket !== '')
1903 {
1904 if ($this->__dnsBucketName($this->bucket))
1905 {
1906 $this->headers['Host'] = $this->bucket.'.'.$this->endpoint;
1907 $this->resource = '/'.$this->bucket.$this->uri;
1908 }
1909 else
1910 {
1911 // Old format, deprecated by AWS - removal scheduled for September 30th, 2020
1912 $this->headers['Host'] = $this->endpoint;
1913 if ($this->bucket !== '') $this->uri = '/'.$this->bucket.$this->uri;
1914 $this->bucket = '';
1915 $this->resource = $this->uri;
1916 }
1917 }
1918 else
1919 {
1920 $this->headers['Host'] = $this->endpoint;
1921 $this->resource = $this->uri;
1922 }
1923
1924
1925 $this->headers['Date'] = gmdate('D, d M Y H:i:s T');
1926 $this->response = new STDClass;
1927 $this->response->error = false;
1928 $this->response->body = null;
1929 $this->response->headers = array();
1930 }
1931
1932
1933 /**
1934 * Set request parameter
1935 *
1936 * @param string $key Key
1937 * @param string $value Value
1938 * @return void
1939 */
1940 public function setParameter($key, $value)
1941 {
1942 $this->parameters[$key] = $value;
1943 }
1944
1945
1946 /**
1947 * Set request header
1948 *
1949 * @param string $key Key
1950 * @param string $value Value
1951 * @return void
1952 */
1953 public function setHeader($key, $value)
1954 {
1955 $this->headers[$key] = $value;
1956 }
1957
1958
1959 /**
1960 * Set x-amz-meta-* header
1961 *
1962 * @param string $key Key
1963 * @param string $value Value
1964 * @return void
1965 */
1966 public function setAmzHeader($key, $value)
1967 {
1968 $this->amzHeaders[$key] = $value;
1969 }
1970
1971
1972 /**
1973 * Get the S3 response
1974 *
1975 * @return object | false
1976 */
1977 public function getResponse()
1978 {
1979 $query = '';
1980 if (sizeof($this->parameters) > 0)
1981 {
1982 $query = substr($this->uri, -1) !== '?' ? '?' : '&';
1983 foreach ($this->parameters as $var => $value)
1984 if ($value == null || $value == '') $query .= $var.'&';
1985 else $query .= $var.'='.rawurlencode($value).'&';
1986 $query = substr($query, 0, -1);
1987 $this->uri .= $query;
1988
1989 if (array_key_exists('acl', $this->parameters) ||
1990 array_key_exists('location', $this->parameters) ||
1991 array_key_exists('torrent', $this->parameters) ||
1992 array_key_exists('website', $this->parameters) ||
1993 array_key_exists('logging', $this->parameters))
1994 $this->resource .= $query;
1995 }
1996 $url = ($this->s3->useSSL ? 'https://' : 'http://') . ($this->headers['Host'] !== '' ? $this->headers['Host'] : $this->endpoint) . $this->uri;
1997
1998 // Basic setup
1999 $curl = curl_init();
2000 curl_setopt($curl, CURLOPT_USERAGENT, 'S3/php');
2001
2002 if ($this->s3->useSSL)
2003 {
2004 // Set protocol version
2005 curl_setopt($curl, CURLOPT_SSLVERSION, $this->s3->useSSLVersion);
2006
2007 // SSL Validation can now be optional for those with broken OpenSSL installations
2008 curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, $this->s3->useSSLValidation ? 2 : 0);
2009 curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, $this->s3->useSSLValidation ? 1 : 0);
2010
2011 if ($this->s3->sslKey !== null) curl_setopt($curl, CURLOPT_SSLKEY, $this->s3->sslKey);
2012 if ($this->s3->sslCert !== null) curl_setopt($curl, CURLOPT_SSLCERT, $this->s3->sslCert);
2013 if ($this->s3->sslCACert !== null) curl_setopt($curl, CURLOPT_CAINFO, $this->s3->sslCACert);
2014 }
2015
2016 curl_setopt($curl, CURLOPT_URL, $url);
2017
2018 if ($this->s3->proxy != null && isset($this->s3->proxy['host']))
2019 {
2020 curl_setopt($curl, CURLOPT_PROXY, $this->s3->proxy['host']);
2021 curl_setopt($curl, CURLOPT_PROXYTYPE, $this->s3->proxy['type']);
2022 if (isset($this->s3->proxy['user'], $this->s3->proxy['pass']) && $this->s3->proxy['user'] != null && $this->s3->proxy['pass'] != null)
2023 curl_setopt($curl, CURLOPT_PROXYUSERPWD, sprintf('%s:%s', $this->s3->proxy['user'], $this->s3->proxy['pass']));
2024 }
2025
2026 // Headers
2027 $httpHeaders = array();
2028 if ($this->s3->hasAuth())
2029 {
2030 // Authorization string (CloudFront stringToSign should only contain a date)
2031 if ($this->headers['Host'] == 'cloudfront.amazonaws.com')
2032 {
2033 # TODO: Update CloudFront authentication
2034 foreach ($this->amzHeaders as $header => $value)
2035 if (strlen($value) > 0) $httpHeaders[] = $header.': '.$value;
2036
2037 foreach ($this->headers as $header => $value)
2038 if (strlen($value) > 0) $httpHeaders[] = $header.': '.$value;
2039
2040 $httpHeaders[] = 'Authorization: ' . $this->s3->__getSignature($this->headers['Date']);
2041 }
2042 else
2043 {
2044 $this->amzHeaders['x-amz-date'] = gmdate('Ymd\THis\Z');
2045
2046 if (!isset($this->amzHeaders['x-amz-content-sha256']))
2047 $this->amzHeaders['x-amz-content-sha256'] = hash('sha256', $this->data);
2048
2049 foreach ($this->amzHeaders as $header => $value)
2050 if (strlen($value) > 0) $httpHeaders[] = $header.': '.$value;
2051
2052 foreach ($this->headers as $header => $value)
2053 if (strlen($value) > 0) $httpHeaders[] = $header.': '.$value;
2054
2055 $httpHeaders[] = 'Authorization: ' . $this->s3->__getSignatureV4(
2056 $this->amzHeaders,
2057 $this->headers,
2058 $this->verb,
2059 $this->uri,
2060 $this->parameters
2061 );
2062
2063 }
2064 }
2065
2066 curl_setopt($curl, CURLOPT_HTTPHEADER, $httpHeaders);
2067 curl_setopt($curl, CURLOPT_HEADER, false);
2068 curl_setopt($curl, CURLOPT_RETURNTRANSFER, false);
2069 curl_setopt($curl, CURLOPT_WRITEFUNCTION, array(&$this, '__responseWriteCallback'));
2070 curl_setopt($curl, CURLOPT_HEADERFUNCTION, array(&$this, '__responseHeaderCallback'));
2071 curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
2072
2073 // Request types
2074 switch ($this->verb)
2075 {
2076 case 'GET': break;
2077 case 'PUT': case 'POST': // POST only used for CloudFront
2078 if ($this->fp !== false)
2079 {
2080 curl_setopt($curl, CURLOPT_PUT, true);
2081 curl_setopt($curl, CURLOPT_INFILE, $this->fp);
2082 if ($this->size >= 0)
2083 curl_setopt($curl, CURLOPT_INFILESIZE, $this->size);
2084 }
2085 elseif ($this->data !== false)
2086 {
2087 curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $this->verb);
2088 curl_setopt($curl, CURLOPT_POSTFIELDS, $this->data);
2089 }
2090 else
2091 curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $this->verb);
2092 break;
2093 case 'HEAD':
2094 curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'HEAD');
2095 curl_setopt($curl, CURLOPT_NOBODY, true);
2096 break;
2097 case 'DELETE':
2098 curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'DELETE');
2099 break;
2100 default: break;
2101 }
2102
2103 // set curl progress function callback
2104 if ($this->s3->progressFunction) {
2105 curl_setopt($curl, CURLOPT_NOPROGRESS, false);
2106 curl_setopt($curl, CURLOPT_PROGRESSFUNCTION, $this->s3->progressFunction);
2107 }
2108
2109 // Execute, grab errors
2110 if (curl_exec($curl))
2111 $this->response->code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
2112 else
2113 $this->response->error = array(
2114 'code' => curl_errno($curl),
2115 'message' => curl_error($curl),
2116 'resource' => $this->resource
2117 );
2118
2119 @curl_close($curl);
2120
2121 // Parse body into XML
2122 if ($this->response->error === false && isset($this->response->body))
2123 {
2124 if ($this->isValidXml($this->response->body)) $this->response->body = simplexml_load_string($this->response->body);
2125
2126 // Grab S3 errors
2127 if (!in_array($this->response->code, array(200, 204, 206)) &&
2128 isset($this->response->body->Code, $this->response->body->Message))
2129 {
2130 $this->response->error = array(
2131 'code' => (string)$this->response->body->Code,
2132 'message' => (string)$this->response->body->Message
2133 );
2134 if (isset($this->response->body->Region))
2135 $this->response->error['region'] = (string)$this->response->body->Region;
2136 if (isset($this->response->body->Resource))
2137 $this->response->error['resource'] = (string)$this->response->body->Resource;
2138 unset($this->response->body);
2139 }
2140 }
2141
2142 // Clean up file resources
2143 if ($this->fp !== false && is_resource($this->fp)) fclose($this->fp);
2144
2145 return $this->response;
2146 }
2147
2148
2149 /**
2150 * CURL write callback
2151 *
2152 * @param resource $curl CURL resource
2153 * @param string $data Data
2154 * @return integer
2155 */
2156 private function __responseWriteCallback($curl, $data)
2157 {
2158 if (in_array($this->response->code, array(200, 206)) && $this->fp !== false)
2159 return fwrite($this->fp, $data);
2160 else
2161 $this->response->body .= $data;
2162 return strlen($data);
2163 }
2164
2165
2166 /**
2167 * Check DNS conformity
2168 *
2169 * @param string $bucket Bucket name
2170 * @return boolean
2171 */
2172 private function __dnsBucketName($bucket)
2173 {
2174 if (strlen($bucket) > 63 || preg_match("/[^a-z0-9\.-]/", $bucket) > 0) return false;
2175 if ($this->s3->useSSL && strstr($bucket, '.') !== false) return false;
2176 if (strstr($bucket, '-.') !== false) return false;
2177 if (strstr($bucket, '..') !== false) return false;
2178 if (!preg_match("/^[0-9a-z]/", $bucket)) return false;
2179 if (!preg_match("/[0-9a-z]$/", $bucket)) return false;
2180 return true;
2181 }
2182
2183
2184 /**
2185 * CURL header callback
2186 *
2187 * @param resource $curl CURL resource
2188 * @param string $data Data
2189 * @return integer
2190 */
2191 private function __responseHeaderCallback($curl, $data)
2192 {
2193 if (($strlen = strlen($data)) <= 2) return $strlen;
2194 if (substr($data, 0, 4) == 'HTTP')
2195 $this->response->code = (int)substr($data, 9, 3);
2196 else
2197 {
2198 $data = trim($data);
2199 if (strpos($data, ': ') === false) return $strlen;
2200 list($header, $value) = explode(': ', $data, 2);
2201 $header = strtolower($header);
2202 if ($header == 'last-modified')
2203 $this->response->headers['time'] = strtotime($value);
2204 elseif ($header == 'date')
2205 $this->response->headers['date'] = strtotime($value);
2206 elseif ($header == 'content-length')
2207 $this->response->headers['size'] = (int)$value;
2208 elseif ($header == 'content-type')
2209 $this->response->headers['type'] = $value;
2210 elseif ($header == 'etag')
2211 $this->response->headers['hash'] = $value[0] == '"' ? substr($value, 1, -1) : $value;
2212 elseif (preg_match('/^x-amz-meta-.*$/', $header))
2213 $this->response->headers[$header] = $value;
2214 }
2215 return $strlen;
2216 }
2217
2218 public function isValidXml($content)
2219 {
2220 $content = trim($content);
2221 if (empty($content)) {
2222 return false;
2223 }
2224 //html go to hell!
2225 if (stripos($content, '<!DOCTYPE html>') !== false) {
2226 return false;
2227 }
2228
2229 libxml_use_internal_errors(true);
2230 simplexml_load_string($content);
2231 $errors = libxml_get_errors();
2232 libxml_clear_errors();
2233
2234 return empty($errors);
2235 }
2236
2237 }
2238
2239