From 13e1054aa21817ed781df6727a0f5658341f912a Mon Sep 17 00:00:00 2001 From: damac23 Date: Wed, 28 Sep 2011 10:33:08 +0200 Subject: [PATCH 01/49] getDistributionInvalidationList New function getDistributionInvalidationList() to list invalidation batches for a CloudFront distribution. --- S3.php | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/S3.php b/S3.php index 998d5ad9..5bf94268 100644 --- a/S3.php +++ b/S3.php @@ -1404,6 +1404,57 @@ private static function __getCloudFrontInvalidationBatchXML($paths, $callerRefer } + /** + * List your invalidation batches for invalidateDistribution() in a CloudFront distribution + * + * http://docs.amazonwebservices.com/AmazonCloudFront/latest/APIReference/ListInvalidation.html + * returned array looks like this: + * Array + * ( + * [I31TWB0CN9V6XD] => InProgress + * [IT3TFE31M0IHZ] => Completed + * [I12HK7MPO1UQDA] => Completed + * [I1IA7R6JKTC3L2] => Completed + * ) + * + * @param string $distributionId Distribution ID from listDistributions() + * @return array + */ + public static function getDistributionInvalidationList($distributionId) + { + if (!extension_loaded('openssl')) + { + self::__triggerError(sprintf("S3::getDistributionInvalidationList(): [%s] %s", + "CloudFront functionality requires SSL"), __FILE__, __LINE__); + return false; + } + + $useSSL = self::$useSSL; + self::$useSSL = true; // CloudFront requires SSL + $rest = new S3Request('GET', '', '2010-11-01/distribution/'.$distributionId.'/invalidation', 'cloudfront.amazonaws.com'); + $rest = self::__getCloudFrontResponse($rest); + self::$useSSL = $useSSL; + + if ($rest->error === false && $rest->code !== 200) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) + { + trigger_error(sprintf("S3::getDistributionInvalidationList('{$distributionId}'): [%s]", + $rest->error['code'], $rest->error['message']), E_USER_WARNING); + return false; + } + elseif ($rest->body instanceof SimpleXMLElement && isset($rest->body->InvalidationSummary)) + { + $list = array(); + foreach ($rest->body->InvalidationSummary as $summary) + $list[(string)$summary->Id] = (string)$summary->Status; + + return $list; + } + return array(); + } + + /** * Get a DistributionConfig DOMDocument * From 922c43c4e923a92156fd4695bf80f85f69c477fe Mon Sep 17 00:00:00 2001 From: Jakub Vrana Date: Mon, 11 Jun 2012 15:32:55 -0700 Subject: [PATCH 02/49] Fix undeclared variables (found by https://github.com/facebook/hiphop-php analyzer) --- S3.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/S3.php b/S3.php index 6f1e0499..354a6186 100644 --- a/S3.php +++ b/S3.php @@ -728,7 +728,7 @@ public static function setBucketLogging($bucket, $targetBucket, $targetPrefix = $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); if ($rest->error !== false) { - self::__triggerError(sprintf("S3::setBucketLogging({$bucket}, {$uri}): [%s] %s", + self::__triggerError(sprintf("S3::setBucketLogging({$bucket}, {$targetBucket}): [%s] %s", $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); return false; } @@ -1773,7 +1773,7 @@ public function getResponse() { curl_setopt($curl, CURLOPT_PROXY, S3::$proxy['host']); curl_setopt($curl, CURLOPT_PROXYTYPE, S3::$proxy['type']); - if (isset(S3::$proxy['user'], S3::$proxy['pass']) && $proxy['user'] != null && $proxy['pass'] != null) + if (isset(S3::$proxy['user'], S3::$proxy['pass']) && S3::$proxy['user'] != null && S3::$proxy['pass'] != null) curl_setopt($curl, CURLOPT_PROXYUSERPWD, sprintf('%s:%s', S3::$proxy['user'], S3::$proxy['pass'])); } From 403a109d4b9943b1ae2f60b791f367e4d5e4c0b5 Mon Sep 17 00:00:00 2001 From: Jakub Vrana Date: Mon, 11 Jun 2012 16:17:27 -0700 Subject: [PATCH 03/49] Don't pass unused argument --- S3.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/S3.php b/S3.php index 354a6186..0b88be25 100644 --- a/S3.php +++ b/S3.php @@ -327,7 +327,7 @@ public static function getBucket($bucket, $prefix = null, $marker = null, $maxKe $rest->setParameter('marker', $nextMarker); if ($delimiter !== null && $delimiter !== '') $rest->setParameter('delimiter', $delimiter); - if (($response = $rest->getResponse(true)) == false || $response->code !== 200) break; + if (($response = $rest->getResponse()) == false || $response->code !== 200) break; if (isset($response->body, $response->body->Contents)) foreach ($response->body->Contents as $c) From aeb3dfdc18c5c960e4b9965a6c4f7ade6e090ff2 Mon Sep 17 00:00:00 2001 From: Don Date: Tue, 19 Jun 2012 23:36:53 +0200 Subject: [PATCH 04/49] Fixed x-amz-meta sort (issues #12 and #33) --- S3.php | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/S3.php b/S3.php index 0b88be25..4b04071e 100644 --- a/S3.php +++ b/S3.php @@ -652,7 +652,7 @@ public static function copyObject($srcBucket, $srcUri, $bucket, $uri, $acl = sel foreach ($metaHeaders as $h => $v) $rest->setAmzHeader('x-amz-meta-'.$h, $v); if ($storageClass !== self::STORAGE_CLASS_STANDARD) // Storage class $rest->setAmzHeader('x-amz-storage-class', $storageClass); - $rest->setAmzHeader('x-amz-acl', $acl); // Added rawurlencode() for $srcUri (thanks a.yamanoi) + $rest->setAmzHeader('x-amz-acl', $acl); $rest->setAmzHeader('x-amz-copy-source', sprintf('/%s/%s', $srcBucket, rawurlencode($srcUri))); if (sizeof($requestHeaders) > 0 || sizeof($metaHeaders) > 0) $rest->setAmzHeader('x-amz-metadata-directive', 'REPLACE'); @@ -965,7 +965,7 @@ public static function deleteObject($bucket, $uri) public static function getAuthenticatedURL($bucket, $uri, $lifetime, $hostBucket = false, $https = false) { $expires = time() + $lifetime; - $uri = str_replace(array('%2F', '%2B'), array('/', '+'), rawurlencode($uri)); // URI should be encoded (thanks Sean O'Dea) + $uri = str_replace(array('%2F', '%2B'), array('/', '+'), rawurlencode($uri)); return sprintf(($https ? 'https' : 'http').'://%s/%s?AWSAccessKeyId=%s&Expires=%u&Signature=%s', // $hostBucket ? $bucket : $bucket.'.s3.amazonaws.com', $uri, self::$__accessKey, $expires, $hostBucket ? $bucket : 's3.amazonaws.com/'.$bucket, $uri, self::$__accessKey, $expires, @@ -1736,7 +1736,6 @@ public function getResponse() $query = substr($this->uri, -1) !== '?' ? '?' : '&'; foreach ($this->parameters as $var => $value) if ($value == null || $value == '') $query .= $var.'&'; - // Parameters should be encoded (thanks Sean O'Dea) else $query .= $var.'='.rawurlencode($value).'&'; $query = substr($query, 0, -1); $this->uri .= $query; @@ -1791,7 +1790,8 @@ public function getResponse() // AMZ headers must be sorted if (sizeof($amz) > 0) { - sort($amz); + //sort($amz); + usort($amz, array(&$this, '__sortMetaHeadersCmp')); $amz = "\n".implode("\n", $amz); } else $amz = ''; @@ -1887,6 +1887,24 @@ public function getResponse() return $this->response; } + /** + * Sort compare for meta headers + * + * @internal Used to sort x-amz meta headers + * @param string $a String A + * @param string $b String B + * @return integer + */ + private function __sortMetaHeadersCmp($a, $b) + { + $lenA = strpos($a, ':'); + $lenB = strpos($b, ':'); + $minLen = min($lenA, $lenB); + $ncmp = strncmp($a, $b, $minLen); + if ($lenA == $lenB) return $ncmp; + if (0 == $ncmp) return $lenA < $lenB ? -1 : 1; + return $ncmp; + } /** * CURL write callback @@ -1948,7 +1966,7 @@ private function __responseHeaderCallback(&$curl, &$data) elseif ($header == 'ETag') $this->response->headers['hash'] = $value{0} == '"' ? substr($value, 1, -1) : $value; elseif (preg_match('/^x-amz-meta-.*$/', $header)) - $this->response->headers[$header] = is_numeric($value) ? (int)$value : $value; + $this->response->headers[$header] = $value; } return $strlen; } From da648e3c80268c72a7fee3a0d25b64a5b1b030c2 Mon Sep 17 00:00:00 2001 From: Don Date: Tue, 19 Jun 2012 23:44:03 +0200 Subject: [PATCH 05/49] Small fix for issue #17 --- S3.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/S3.php b/S3.php index 4b04071e..cb302e20 100644 --- a/S3.php +++ b/S3.php @@ -471,7 +471,7 @@ public static function putObject($input, $bucket, $uri, $acl = self::ACL_PRIVATE if ($input === false) return false; $rest = new S3Request('PUT', $bucket, $uri, self::$endpoint); - if (is_string($input)) $input = array( + if (!is_array($input)) $input = array( 'data' => $input, 'size' => strlen($input), 'md5sum' => base64_encode(md5($input, true)) ); From 07bb73fe2ad2c74e0d1af395a391ddb8d0fcaa7c Mon Sep 17 00:00:00 2001 From: Tomasz Muras Date: Sun, 25 Nov 2012 17:09:38 +0100 Subject: [PATCH 06/49] Correct value for CURLOPT_SSL_VERIFYHOST - 2 instead of 1. --- S3.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/S3.php b/S3.php index 247d6c38..4bd51114 100644 --- a/S3.php +++ b/S3.php @@ -1809,7 +1809,7 @@ public function getResponse() if (S3::$useSSL) { // SSL Validation can now be optional for those with broken OpenSSL installations - curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, S3::$useSSLValidation ? 1 : 0); + curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, S3::$useSSLValidation ? 2 : 0); curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, S3::$useSSLValidation ? 1 : 0); if (S3::$sslKey !== null) curl_setopt($curl, CURLOPT_SSLKEY, S3::$sslKey); From 458b558d00e2fc0ed270501c3fb9a8a9a88ee8d5 Mon Sep 17 00:00:00 2001 From: Dave Jones Date: Sat, 23 Feb 2013 15:48:47 +0000 Subject: [PATCH 07/49] new function: setBucketRedirect(bucket, location) --- S3.php | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/S3.php b/S3.php index 4bd51114..d1d1a3e2 100644 --- a/S3.php +++ b/S3.php @@ -673,6 +673,47 @@ public static function copyObject($srcBucket, $srcUri, $bucket, $uri, $acl = sel } + /** + * Set up a bucket redirection + * + * @param string $bucket Bucket name + * @param string $location Target host name + * @return boolean + */ + public static function setBucketRedirect($bucket = NULL, $location = NULL) + { + $rest = new S3Request('PUT', $bucket, '', self::$endpoint); + + if( empty($bucket) || empty($location) ) { + self::__triggerError("S3::setBucketRedirect({$bucket}, {$location}): Empty parameter.", __FILE__, __LINE__); + return false; + } + + $dom = new DOMDocument; + $websiteConfiguration = $dom->createElement('WebsiteConfiguration'); + $redirectAllRequestsTo = $dom->createElement('RedirectAllRequestsTo'); + $hostName = $dom->createElement('HostName', $location); + $redirectAllRequestsTo->appendChild($hostName); + $websiteConfiguration->appendChild($redirectAllRequestsTo); + $dom->appendChild($websiteConfiguration); + $rest->setParameter('website', null); + $rest->data = $dom->saveXML(); + $rest->size = strlen($rest->data); + $rest->setHeader('Content-Type', 'application/xml'); + $rest = $rest->getResponse(); + + if ($rest->error === false && $rest->code !== 200) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) + { + self::__triggerError(sprintf("S3::setBucketRedirect({$bucket}, {$location}): [%s] %s", + $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); + return false; + } + return true; + } + + /** * Set logging for a bucket * From 0fe3a80e2f4772bc97e4e3adc86a40569a2fcbee Mon Sep 17 00:00:00 2001 From: Daryl Lozupone Date: Mon, 3 Jun 2013 07:55:34 -0400 Subject: [PATCH 08/49] Updated comments and property declarations to remove all phpdoc generation errors --- S3.php | 241 +++++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 225 insertions(+), 16 deletions(-) diff --git a/S3.php b/S3.php index d1d1a3e2..63dce765 100644 --- a/S3.php +++ b/S3.php @@ -44,25 +44,125 @@ class S3 const STORAGE_CLASS_STANDARD = 'STANDARD'; const STORAGE_CLASS_RRS = 'REDUCED_REDUNDANCY'; - - private static $__accessKey = null; // AWS Access key - private static $__secretKey = null; // AWS Secret key + + /** + * The AWS Access key + * + * @var string + * @access private + * @static + */ + private static $__accessKey = null; + + /** + * AWS Secret Key + * + * @var string + * @access private + * @static + */ + private static $__secretKey = null; + + /** + * SSL Client key + * + * @var string + * @access private + * @static + */ private static $__sslKey = null; - + + /** + * AWS URI + * + * @var string + * @acess public + * @static + */ public static $endpoint = 's3.amazonaws.com'; + + /** + * Proxy information + * + * @var null|array + * @access public + * @static + */ public static $proxy = null; - + + /** + * Connect using SSL? + * + * @var bool + * @access public + * @static + */ public static $useSSL = false; + + /** + * Use SSL validation? + * + * @var bool + * @access public + * @static + */ public static $useSSLValidation = true; + + /** + * Use PHP exceptions? + * + * @var bool + * @access public + * @static + */ public static $useExceptions = false; // SSL CURL SSL options - only needed if you are experiencing problems with your OpenSSL configuration + + /** + * SSL client key + * + * @var bool + * @access public + * @static + */ public static $sslKey = null; + + /** + * SSL client certfificate + * + * @var string + * @acess public + * @static + */ public static $sslCert = null; + + /** + * SSL CA cert (only required if you are having problems with your system CA cert) + * + * @var string + * @access public + * @static + */ public static $sslCACert = null; - - private static $__signingKeyPairId = null; // AWS Key Pair ID - private static $__signingKeyResource = false; // Key resource, freeSigningKey() must be called to clear it from memory + + /** + * AWS Key Pair ID + * + * @var string + * @access private + * @static + */ + private static $__signingKeyPairId = null; + + /** + * Key resource, freeSigningKey() must be called to clear it from memory + * + * @var bool + * @access private + * @static + */ + private static $__signingKeyResource = false; /** @@ -71,6 +171,7 @@ class S3 * @param string $accessKey Access key * @param string $secretKey Secret key * @param boolean $useSSL Enable SSL + * @param string $endpoint Amazon URI * @return void */ public function __construct($accessKey = null, $secretKey = null, $useSSL = false, $endpoint = 's3.amazonaws.com') @@ -83,7 +184,7 @@ public function __construct($accessKey = null, $secretKey = null, $useSSL = fals /** - * Set the sertvice endpoint + * Set the service endpoint * * @param string $host Hostname * @return void @@ -262,7 +363,7 @@ public static function listBuckets($detailed = false) } - /* + /** * Get contents for a bucket * * If maxKeys is null this method will loop through truncated result sets @@ -634,8 +735,8 @@ public static function getObjectInfo($bucket, $uri, $returnInfo = true) /** * Copy an object * - * @param string $bucket Source bucket name - * @param string $uri Source object URI + * @param string $srcBucket Source bucket name + * @param string $srcUri Source object URI * @param string $bucket Destination bucket name * @param string $uri Destination object URI * @param constant $acl ACL constant @@ -1039,7 +1140,7 @@ public static function getSignedPolicyURL($policy) /** * Get a CloudFront canned policy URL * - * @param string $string URL to sign + * @param string $url URL to sign * @param integer $lifetime URL lifetime * @return string */ @@ -1430,6 +1531,7 @@ public static function invalidateDistribution($distributionId, $paths) * * @internal Used to create XML in invalidateDistribution() * @param array $paths Paths to objects to invalidateDistribution + * @param int $callerReference * @return string */ private static function __getCloudFrontInvalidationBatchXML($paths, $callerReference = '0') { @@ -1718,13 +1820,103 @@ private static function __getHash($string) } +/** + * S3 Request class + * + * @link http://undesigned.org.za/2007/10/22/amazon-s3-php-class + * @version 0.5.0-dev + */ final class S3Request { - private $endpoint, $verb, $bucket, $uri, $resource = '', $parameters = array(), - $amzHeaders = array(), $headers = array( + /** + * AWS URI + * + * @var string + * @access pricate + */ + private $endpoint; + + /** + * Verb + * + * @var string + * @access private + */ + private $verb; + + /** + * S3 bucket name + * + * @var string + * @access private + */ + private $bucket; + + /** + * Object URI + * + * @var string + * @access private + */ + private $uri; + + /** + * Final object URI + * + * @var string + * @access private + */ + private $resource = ''; + + /** + * Additional request parameters + * + * @var array + * @access private + */ + private $parameters = array(); + + /** + * Amazon specific request headers + * + * @var array + * @access private + */ + private $amzHeaders = array(), $headers = array( 'Host' => '', 'Date' => '', 'Content-MD5' => '', 'Content-Type' => '' ); - public $fp = false, $size = 0, $data = false, $response; + + /** + * Use HTTP PUT? + * + * @var bool + * @access public + */ + public $fp = false; + + /** + * PUT file size + * + * @var int + * @access public + */ + public $size = 0; + + /** + * PUT post fields + * + * @var array + * @access public + */ + public $data = false; + + /** + * S3 request respone + * + * @var object + * @access public + */ + public $response; /** @@ -1733,10 +1925,12 @@ final class S3Request * @param string $verb Verb * @param string $bucket Bucket name * @param string $uri Object URI + * @param string $endpoint AWS endpoint URI * @return mixed */ function __construct($verb, $bucket = '', $uri = '', $endpoint = 's3.amazonaws.com') { + $this->endpoint = $endpoint; $this->verb = $verb; $this->bucket = $bucket; @@ -2065,7 +2259,22 @@ private function __responseHeaderCallback(&$curl, &$data) } +/** + * S3 exception class + * + * @link http://undesigned.org.za/2007/10/22/amazon-s3-php-class + * @version 0.5.0-dev + */ + class S3Exception extends Exception { + /** + * Class constructor + * + * @param string $message Exception message + * @param string $file File in which exception was created + * @param string $line Line number on which exception was created + * @param int $code Exception code + */ function __construct($message, $file, $line, $code = 0) { parent::__construct($message, $code); From 57d9fa8dd435a40b0e951fb61e6556625817f8ba Mon Sep 17 00:00:00 2001 From: Donovan Schonknecht Date: Tue, 11 Jun 2013 18:31:24 +0200 Subject: [PATCH 09/49] Fix bug in DNS check (fixes issue #22) --- S3.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/S3.php b/S3.php index 63dce765..8d161088 100644 --- a/S3.php +++ b/S3.php @@ -2217,7 +2217,7 @@ private function __responseWriteCallback(&$curl, &$data) */ private function __dnsBucketName($bucket) { - if (strlen($bucket) > 63 || !preg_match("/[^a-z0-9\.-]/", $bucket)) return false; + if (strlen($bucket) > 63 || preg_match("/[^a-z0-9\.-]/", $bucket) > 0) return false; if (strstr($bucket, '-.') !== false) return false; if (strstr($bucket, '..') !== false) return false; if (!preg_match("/^[0-9a-z]/", $bucket)) return false; From daa27e661336664c0dd127567f9889242a1681e1 Mon Sep 17 00:00:00 2001 From: Donovan Schonknecht Date: Tue, 11 Jun 2013 18:49:11 +0200 Subject: [PATCH 10/49] Added SSE parameter to putObject --- S3.php | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/S3.php b/S3.php index 8d161088..79eb8101 100644 --- a/S3.php +++ b/S3.php @@ -44,7 +44,10 @@ class S3 const STORAGE_CLASS_STANDARD = 'STANDARD'; const STORAGE_CLASS_RRS = 'REDUCED_REDUNDANCY'; - + + const SSE_NONE = ''; + const SSE_AES256 = 'AES256'; + /** * The AWS Access key * @@ -565,9 +568,10 @@ public static function inputResource(&$resource, $bufferSize, $md5sum = '') * @param array $metaHeaders Array of x-amz-meta-* headers * @param array $requestHeaders Array of request headers or content type as a string * @param constant $storageClass Storage class constant + * @param constant $serverSideEncryption Server-side encryption * @return boolean */ - public static function putObject($input, $bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $requestHeaders = array(), $storageClass = self::STORAGE_CLASS_STANDARD) + public static function putObject($input, $bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $requestHeaders = array(), $storageClass = self::STORAGE_CLASS_STANDARD, $serverSideEncryption = self::SSE_NONE) { if ($input === false) return false; $rest = new S3Request('PUT', $bucket, $uri, self::$endpoint); @@ -615,6 +619,9 @@ public static function putObject($input, $bucket, $uri, $acl = self::ACL_PRIVATE if ($storageClass !== self::STORAGE_CLASS_STANDARD) // Storage class $rest->setAmzHeader('x-amz-storage-class', $storageClass); + if ($serverSideEncryption !== self::SSE_NONE) // Server-side encryption + $rest->setAmzHeader('x-amz-server-side-encryption', $serverSideEncryption); + // We need to post with Content-Length and Content-Type, MD5 is optional if ($rest->size >= 0 && ($rest->fp !== false || $rest->data !== false)) { From 551bacd25c37e43742a2309eb910bf3a99f308b5 Mon Sep 17 00:00:00 2001 From: Donovan Schonknecht Date: Tue, 11 Jun 2013 20:23:43 +0200 Subject: [PATCH 11/49] Created markdown readme --- README.md | 99 ++++++++++++++++++++++++++++++++++++++++++++++++++ README.txt | 105 ----------------------------------------------------- 2 files changed, 99 insertions(+), 105 deletions(-) create mode 100644 README.md delete mode 100644 README.txt diff --git a/README.md b/README.md new file mode 100644 index 00000000..6643f90b --- /dev/null +++ b/README.md @@ -0,0 +1,99 @@ +# Amazon S3 PHP Class + +## Usage + +OO method (e,g; $s3->getObject(...)): + +```php +$s3 = new S3($awsAccessKey, $awsSecretKey); +``` + +Statically (e,g; S3::getObject(...)): + +```php +S3::setAuth($awsAccessKey, $awsSecretKey); +``` + +### Object Operations + +#### Uploading objects + +Put an object from a file: + +```php +S3::putObject(S3::inputFile($file, false), $bucketName, $uploadName, S3::ACL_PUBLIC_READ) +``` + +Put an object from a string and set its Content-Type: + +```php +S3::putObject($string, $bucketName, $uploadName, S3::ACL_PUBLIC_READ, array(), array('Content-Type' => 'text/plain')) +``` + +Put an object from a resource (buffer/file size is required - note: the resource will be fclose()'d automatically): + +```php +S3::putObject(S3::inputResource(fopen($file, 'rb'), filesize($file)), $bucketName, $uploadName, S3::ACL_PUBLIC_READ) +``` + +#### Retrieving objects + +Get an object: + +```php +S3::getObject($bucketName, $uploadName) +``` + +Save an object to file: + +```php +S3::getObject($bucketName, $uploadName, $saveName) +``` + +Save an object to a resource of any type: + +```php +S3::getObject($bucketName, $uploadName, fopen('savefile.txt', 'wb')) +``` + +#### Copying and deleting objects + +Copy an object: + +```php +S3::copyObject($srcBucket, $srcName, $bucketName, $saveName, $metaHeaders = array(), $requestHeaders = array()) +``` + +Delete an object: + +```php +S3::deleteObject($bucketName, $uploadName) +``` + +### Bucket Operations + +Get a list of buckets: + +```php +S3::listBuckets() // Simple bucket list +S3::listBuckets(true) // Detailed bucket list +``` + +Create a bucket: + +```php +S3::putBucket($bucketName) +``` + +Get the contents of a bucket: + +```php +S3::getBucket($bucketName) +``` + +Delete an empty bucket: + +```php +S3::deleteBucket($bucketName) +``` + diff --git a/README.txt b/README.txt deleted file mode 100644 index 28a9e73f..00000000 --- a/README.txt +++ /dev/null @@ -1,105 +0,0 @@ -AMAZON S3 PHP CLASS - - -USING THE CLASS - -OO method (e,g; $s3->getObject(...)): -$s3 = new S3(awsAccessKey, awsSecretKey); - -Statically (e,g; S3::getObject(...)): -S3::setAuth(awsAccessKey, awsSecretKey); - - -For class documentation see: -http://undesigned.org.za/files/s3-class-documentation/index.html - - -OBJECTS - - -Put an object from a string: - $s3->putObject($string, $bucketName, $uploadName, S3::ACL_PUBLIC_READ) - Legacy function: $s3->putObjectString($string, $bucketName, $uploadName, S3::ACL_PUBLIC_READ) - - -Put an object from a file: - $s3->putObject($s3->inputFile($file, false), $bucketName, $uploadName, S3::ACL_PUBLIC_READ) - Legacy function: $s3->putObjectFile($uploadFile, $bucketName, $uploadName, S3::ACL_PUBLIC_READ) - - -Put an object from a resource (buffer/file size is required): - Please note: the resource will be fclose()'d automatically - $s3->putObject($s3->inputResource(fopen($file, 'rb'), filesize($file)), $bucketName, $uploadName, S3::ACL_PUBLIC_READ) - - -Get an object: - $s3->getObject($bucketName, $uploadName) - - -Save an object to file: - $s3->getObject($bucketName, $uploadName, $saveName) - - -Save an object to a resource of any type: - $s3->getObject($bucketName, $uploadName, fopen('savefile.txt', 'wb')) - - -Copy an object: - $s3->copyObject($srcBucket, $srcName, $bucketName, $saveName, $metaHeaders = array(), $requestHeaders = array()) - - -Delete an object: - $s3->deleteObject($bucketName, $uploadName) - - - -BUCKETS - - -Get a list of buckets: - $s3->listBuckets() // Simple bucket list - $s3->listBuckets(true) // Detailed bucket list - - -Create a public-read bucket: - $s3->putBucket($bucketName, S3::ACL_PUBLIC_READ) - $s3->putBucket($bucketName, S3::ACL_PUBLIC_READ, 'EU') // EU-hosted bucket - - -Get the contents of a bucket: - $s3->getBucket($bucketName) - - -Get a bucket's location: - $s3->getBucketLocation($bucketName) - - -Delete a bucket: - $s3->deleteBucket($bucketName) - - - - -KNOWN ISSUES - - Files larger than 2GB are not supported on 32 bit systems due to PHP’s signed integer problem - - - -MORE INFORMATION - - - Project URL: - http://undesigned.org.za/2007/10/22/amazon-s3-php-class - - Class documentation: - http://undesigned.org.za/files/s3-class-documentation/index.html - - Bug reports: - https://github.com/tpyo/amazon-s3-php-class/issues - - Amazon S3 documentation: - http://docs.amazonwebservices.com/AmazonS3/2006-03-01/ - - -EOF From 9a5ef2b566c282bfd780285c0b939cefff3bc0b3 Mon Sep 17 00:00:00 2001 From: Donovan Schonknecht Date: Tue, 11 Jun 2013 21:09:56 +0200 Subject: [PATCH 12/49] Use file extensions for MIME lookup before magic lookup --- S3.php | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/S3.php b/S3.php index 79eb8101..bb0b4000 100644 --- a/S3.php +++ b/S3.php @@ -1746,15 +1746,17 @@ private static function __getCloudFrontResponse(&$rest) /** * Get MIME type for file * + * To override the putObject() Content-Type, add it to $requestHeaders + * + * To use fileinfo, ensure the MAGIC environment variable is set + * * @internal Used to get mime types * @param string &$file File path * @return string */ - public static function __getMimeType(&$file) + private static function __getMimeType(&$file) { - $type = false; - // Fileinfo documentation says fileinfo_open() will use the - // MAGIC env var for the magic file + // Use fileinfo if available if (extension_loaded('fileinfo') && isset($_ENV['MAGIC']) && ($finfo = finfo_open(FILEINFO_MIME, $_ENV['MAGIC'])) !== false) { @@ -1767,21 +1769,19 @@ public static function __getMimeType(&$file) $type = trim(array_shift($type)); } finfo_close($finfo); + if ($type !== false && strlen($type) > 0) return $type; + } - // If anyone is still using mime_content_type() - } elseif (function_exists('mime_content_type')) - $type = trim(mime_content_type($file)); - - if ($type !== false && strlen($type) > 0) return $type; - - // Otherwise do it the old fashioned way static $exts = array( - 'jpg' => 'image/jpeg', 'gif' => 'image/gif', 'png' => 'image/png', - 'tif' => 'image/tiff', 'tiff' => 'image/tiff', 'ico' => 'image/x-icon', - 'swf' => 'application/x-shockwave-flash', 'pdf' => 'application/pdf', + 'jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'gif' => 'image/gif', + 'png' => 'image/png', 'ico' => 'image/x-icon', 'pdf' => 'application/pdf', + 'tif' => 'image/tiff', 'tiff' => 'image/tiff', 'svg' => 'image/svg+xml', + 'svgz' => 'image/svg+xml', 'swf' => 'application/x-shockwave-flash', 'zip' => 'application/zip', 'gz' => 'application/x-gzip', 'tar' => 'application/x-tar', 'bz' => 'application/x-bzip', - 'bz2' => 'application/x-bzip2', 'txt' => 'text/plain', + 'bz2' => 'application/x-bzip2', 'rar' => 'application/x-rar-compressed', + 'exe' => 'application/x-msdownload', 'msi' => 'application/x-msdownload', + 'cab' => 'application/vnd.ms-cab-compressed', 'txt' => 'text/plain', 'asc' => 'text/plain', 'htm' => 'text/html', 'html' => 'text/html', 'css' => 'text/css', 'js' => 'text/javascript', 'xml' => 'text/xml', 'xsl' => 'application/xsl+xml', @@ -1789,8 +1789,11 @@ public static function __getMimeType(&$file) 'avi' => 'video/x-msvideo', 'mpg' => 'video/mpeg', 'mpeg' => 'video/mpeg', 'mov' => 'video/quicktime', 'flv' => 'video/x-flv', 'php' => 'text/x-php' ); - $ext = strtolower(pathInfo($file, PATHINFO_EXTENSION)); - return isset($exts[$ext]) ? $exts[$ext] : 'application/octet-stream'; + $ext = strtolower(pathinfo($file, PATHINFO_EXTENSION)); + // mime_content_type() is deprecated, fileinfo should be configured + $type = isset($exts[$ext]) ? $exts[$ext] : trim(mime_content_type($file)); + + return ($type !== false && strlen($type) > 0) ? $type : 'application/octet-stream'; } From 2716afaa9de4d6116193de6ba6ee4369c3bccae1 Mon Sep 17 00:00:00 2001 From: Donovan Schonknecht Date: Tue, 11 Jun 2013 21:14:28 +0200 Subject: [PATCH 13/49] Copyright update, indentation fix --- S3.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/S3.php b/S3.php index bb0b4000..7cce1d33 100644 --- a/S3.php +++ b/S3.php @@ -2,7 +2,7 @@ /** * $Id$ * -* Copyright (c) 2011, Donovan Schönknecht. All rights reserved. +* Copyright (c) 2013, Donovan Schönknecht. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -804,7 +804,7 @@ public static function setBucketRedirect($bucket = NULL, $location = NULL) $redirectAllRequestsTo->appendChild($hostName); $websiteConfiguration->appendChild($redirectAllRequestsTo); $dom->appendChild($websiteConfiguration); - $rest->setParameter('website', null); + $rest->setParameter('website', null); $rest->data = $dom->saveXML(); $rest->size = strlen($rest->data); $rest->setHeader('Content-Type', 'application/xml'); @@ -1566,7 +1566,7 @@ private static function __getCloudFrontInvalidationBatchXML($paths, $callerRefer * [I12HK7MPO1UQDA] => Completed * [I1IA7R6JKTC3L2] => Completed * ) - * + * * @param string $distributionId Distribution ID from listDistributions() * @return array */ @@ -1780,8 +1780,8 @@ private static function __getMimeType(&$file) 'zip' => 'application/zip', 'gz' => 'application/x-gzip', 'tar' => 'application/x-tar', 'bz' => 'application/x-bzip', 'bz2' => 'application/x-bzip2', 'rar' => 'application/x-rar-compressed', - 'exe' => 'application/x-msdownload', 'msi' => 'application/x-msdownload', - 'cab' => 'application/vnd.ms-cab-compressed', 'txt' => 'text/plain', + 'exe' => 'application/x-msdownload', 'msi' => 'application/x-msdownload', + 'cab' => 'application/vnd.ms-cab-compressed', 'txt' => 'text/plain', 'asc' => 'text/plain', 'htm' => 'text/html', 'html' => 'text/html', 'css' => 'text/css', 'js' => 'text/javascript', 'xml' => 'text/xml', 'xsl' => 'application/xsl+xml', @@ -2106,7 +2106,7 @@ public function getResponse() $this->resource ); } - } + } curl_setopt($curl, CURLOPT_HTTPHEADER, $headers); curl_setopt($curl, CURLOPT_HEADER, false); From f5a5417b2915c48186d4dcc1bbb41114e51c2c00 Mon Sep 17 00:00:00 2001 From: Donovan Schonknecht Date: Wed, 12 Jun 2013 17:10:16 +0200 Subject: [PATCH 14/49] Fix broken setProxy() params --- S3.php | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/S3.php b/S3.php index 7cce1d33..3d654d85 100644 --- a/S3.php +++ b/S3.php @@ -262,7 +262,7 @@ public static function setSSLAuth($sslCert = null, $sslKey = null, $sslCACert = */ public static function setProxy($host, $user = null, $pass = null, $type = CURLPROXY_SOCKS5) { - self::$proxy = array('host' => $host, 'type' => $type, 'user' => null, 'pass' => 'null'); + self::$proxy = array('host' => $host, 'type' => $type, 'user' => $user, 'pass' => $pass); } @@ -1541,7 +1541,8 @@ public static function invalidateDistribution($distributionId, $paths) * @param int $callerReference * @return string */ - private static function __getCloudFrontInvalidationBatchXML($paths, $callerReference = '0') { + private static function __getCloudFrontInvalidationBatchXML($paths, $callerReference = '0') + { $dom = new DOMDocument('1.0', 'UTF-8'); $dom->formatOutput = true; $invalidationBatch = $dom->createElement('InvalidationBatch'); @@ -1892,10 +1893,18 @@ final class S3Request * @var array * @access private */ - private $amzHeaders = array(), $headers = array( + private $amzHeaders = array(); + + /** + * HTTP request headers + * + * @var array + * @access private + */ + private $headers = array( 'Host' => '', 'Date' => '', 'Content-MD5' => '', 'Content-Type' => '' ); - + /** * Use HTTP PUT? * @@ -1903,7 +1912,7 @@ final class S3Request * @access public */ public $fp = false; - + /** * PUT file size * @@ -1911,7 +1920,7 @@ final class S3Request * @access public */ public $size = 0; - + /** * PUT post fields * @@ -1919,7 +1928,7 @@ final class S3Request * @access public */ public $data = false; - + /** * S3 request respone * From a938ec813cff756939e6195f8734d62b54250663 Mon Sep 17 00:00:00 2001 From: Donovan Schonknecht Date: Wed, 12 Jun 2013 17:20:05 +0200 Subject: [PATCH 15/49] Use endpoint for getAuthenticatedURL --- S3.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/S3.php b/S3.php index 3d654d85..686fb863 100644 --- a/S3.php +++ b/S3.php @@ -1117,7 +1117,7 @@ public static function getAuthenticatedURL($bucket, $uri, $lifetime, $hostBucket $uri = str_replace(array('%2F', '%2B'), array('/', '+'), rawurlencode($uri)); return sprintf(($https ? 'https' : 'http').'://%s/%s?AWSAccessKeyId=%s&Expires=%u&Signature=%s', // $hostBucket ? $bucket : $bucket.'.s3.amazonaws.com', $uri, self::$__accessKey, $expires, - $hostBucket ? $bucket : 's3.amazonaws.com/'.$bucket, $uri, self::$__accessKey, $expires, + $hostBucket ? $bucket : self::$endpoint.'/'.$bucket, $uri, self::$__accessKey, $expires, urlencode(self::__getHash("GET\n\n\n{$expires}\n/{$bucket}/{$uri}"))); } From 31646fee235203e26c36f54bcc910661cedfccb2 Mon Sep 17 00:00:00 2001 From: Donovan Schonknecht Date: Thu, 13 Jun 2013 20:17:57 +0200 Subject: [PATCH 16/49] Make inputResource() find the buffer length when set to false --- S3.php | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/S3.php b/S3.php index 686fb863..ce1982b0 100644 --- a/S3.php +++ b/S3.php @@ -545,13 +545,25 @@ public static function inputFile($file, $md5sum = true) * @param string $md5sum MD5 hash to send (optional) * @return array | false */ - public static function inputResource(&$resource, $bufferSize, $md5sum = '') + public static function inputResource(&$resource, $bufferSize = false, $md5sum = '') { - if (!is_resource($resource) || $bufferSize < 0) + if (!is_resource($resource) || (int)$bufferSize < 0) { self::__triggerError('S3::inputResource(): Invalid resource or buffer size', __FILE__, __LINE__); return false; } + + // Try to figure out the bytesize + if ($bufferSize === false) + { + if (fseek($resource, 0, SEEK_END) < 0 || ($bufferSize = ftell($resource)) === false) + { + self::__triggerError('S3::inputResource(): Unable to obtain resource size', __FILE__, __LINE__); + return false; + } + fseek($resource, 0); + } + $input = array('size' => $bufferSize, 'md5sum' => $md5sum); $input['fp'] =& $resource; return $input; From ff384e51d3b08296385b402c2c87a0d51eb07e49 Mon Sep 17 00:00:00 2001 From: zyberspace Date: Tue, 25 Jun 2013 23:50:46 +0200 Subject: [PATCH 17/49] Added composer.json and .gitignore --- .gitignore | 1 + composer.json | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) create mode 100644 .gitignore create mode 100644 composer.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..48b8bf90 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +vendor/ diff --git a/composer.json b/composer.json new file mode 100644 index 00000000..d197f71d --- /dev/null +++ b/composer.json @@ -0,0 +1,18 @@ +{ + "name": "tpyo/amazon-s3", + "description": "A standalone Amazon S3 (REST) client for PHP 5.2.x using CURL that does not require PEAR.", + "type": "library", + "homepage": "https://github.com/tpyo/amazon-s3-php-class", + "license": "BSD-2-Clause", + "authors": [ + { + "name": "Donovan Schönknecht" + } + ], + "require": { + "php": ">=5.2.0" + }, + "autoload": { + "psr-0": { "S3": "" } + } +} From e898c012e8e3e69f33f3beaaa5a531c47b4f343c Mon Sep 17 00:00:00 2001 From: zyberspace Date: Wed, 26 Jun 2013 14:16:36 +0200 Subject: [PATCH 18/49] Changed autoload to "classmap" S3.php bundles multiple classes, so "classmap" fits better --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index d197f71d..08a27eab 100644 --- a/composer.json +++ b/composer.json @@ -13,6 +13,6 @@ "php": ">=5.2.0" }, "autoload": { - "psr-0": { "S3": "" } + "classmap": ["S3.php"] } } From a7fcd6d5e14990f5c1c138519dcadda93bc9e172 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Donovan=20Sch=C3=B6nknecht?= Date: Wed, 26 Jun 2013 14:48:12 +0200 Subject: [PATCH 19/49] Updated composer.json --- composer.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 08a27eab..4c55bc10 100644 --- a/composer.json +++ b/composer.json @@ -1,12 +1,13 @@ { - "name": "tpyo/amazon-s3", + "name": "tpyo/amazon-s3-php-class", "description": "A standalone Amazon S3 (REST) client for PHP 5.2.x using CURL that does not require PEAR.", "type": "library", "homepage": "https://github.com/tpyo/amazon-s3-php-class", "license": "BSD-2-Clause", "authors": [ { - "name": "Donovan Schönknecht" + "name": "Donovan Schönknecht", + "email": "don@tpyo.net" } ], "require": { From 80b9564c27628bf9737fc659a18328eeac913c22 Mon Sep 17 00:00:00 2001 From: Sasha Date: Tue, 25 Jun 2013 22:34:06 -0400 Subject: [PATCH 20/49] -Fixes PHP 5.5.0 compatibility issue: final S3Request class must initialize response body to avoid ErrorException. --- S3.php | 1 + 1 file changed, 1 insertion(+) diff --git a/S3.php b/S3.php index ce1982b0..c0eaffa4 100644 --- a/S3.php +++ b/S3.php @@ -1998,6 +1998,7 @@ function __construct($verb, $bucket = '', $uri = '', $endpoint = 's3.amazonaws.c $this->headers['Date'] = gmdate('D, d M Y H:i:s T'); $this->response = new STDClass; $this->response->error = false; + $this->response->body = null; } From 2061fa8a5a28de5710551791ac36918fcd331a7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Donovan=20Sch=C3=B6nknecht?= Date: Wed, 26 Jun 2013 15:09:24 +0200 Subject: [PATCH 21/49] Create headers array for response --- S3.php | 1 + 1 file changed, 1 insertion(+) diff --git a/S3.php b/S3.php index c0eaffa4..70e305e4 100644 --- a/S3.php +++ b/S3.php @@ -1999,6 +1999,7 @@ function __construct($verb, $bucket = '', $uri = '', $endpoint = 's3.amazonaws.c $this->response = new STDClass; $this->response->error = false; $this->response->body = null; + $this->response->headers = array(); } From 1a2a3ef603d83981f04ac3856b0999abfd1fabed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Donovan=20Scho=CC=88nknecht?= Date: Sat, 5 Oct 2013 14:30:47 +0200 Subject: [PATCH 22/49] Version bump for 0.5 --- S3.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/S3.php b/S3.php index 70e305e4..91de51d8 100644 --- a/S3.php +++ b/S3.php @@ -32,7 +32,7 @@ * Amazon S3 PHP class * * @link http://undesigned.org.za/2007/10/22/amazon-s3-php-class -* @version 0.5.0-dev +* @version 0.5.0 */ class S3 { From 2cf80b2421db730f02c6f7077c3e9070946834e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Donovan=20Scho=CC=88nknecht?= Date: Sat, 5 Oct 2013 15:16:51 +0200 Subject: [PATCH 23/49] Updated __getMIMEType and removed deprecated mime_content_type() calls (fixes issues #31 and #57) --- S3.php | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/S3.php b/S3.php index 91de51d8..6be5fc85 100644 --- a/S3.php +++ b/S3.php @@ -32,7 +32,7 @@ * Amazon S3 PHP class * * @link http://undesigned.org.za/2007/10/22/amazon-s3-php-class -* @version 0.5.0 +* @version 0.5.1-dev */ class S3 { @@ -623,7 +623,7 @@ public static function putObject($input, $bucket, $uri, $acl = self::ACL_PRIVATE if (isset($requestHeaders['Content-Type'])) $input['type'] =& $requestHeaders['Content-Type']; elseif (isset($input['file'])) - $input['type'] = self::__getMimeType($input['file']); + $input['type'] = self::__getMIMEType($input['file']); else $input['type'] = 'application/octet-stream'; } @@ -1767,24 +1767,8 @@ private static function __getCloudFrontResponse(&$rest) * @param string &$file File path * @return string */ - private static function __getMimeType(&$file) + private static function __getMIMEType(&$file) { - // Use fileinfo if available - if (extension_loaded('fileinfo') && isset($_ENV['MAGIC']) && - ($finfo = finfo_open(FILEINFO_MIME, $_ENV['MAGIC'])) !== false) - { - if (($type = finfo_file($finfo, $file)) !== false) - { - // Remove the charset and grab the last content-type - $type = explode(' ', str_replace('; charset=', ';charset=', $type)); - $type = array_pop($type); - $type = explode(';', $type); - $type = trim(array_shift($type)); - } - finfo_close($finfo); - if ($type !== false && strlen($type) > 0) return $type; - } - static $exts = array( 'jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'gif' => 'image/gif', 'png' => 'image/png', 'ico' => 'image/x-icon', 'pdf' => 'application/pdf', @@ -1802,9 +1786,25 @@ private static function __getMimeType(&$file) 'avi' => 'video/x-msvideo', 'mpg' => 'video/mpeg', 'mpeg' => 'video/mpeg', 'mov' => 'video/quicktime', 'flv' => 'video/x-flv', 'php' => 'text/x-php' ); + $ext = strtolower(pathinfo($file, PATHINFO_EXTENSION)); - // mime_content_type() is deprecated, fileinfo should be configured - $type = isset($exts[$ext]) ? $exts[$ext] : trim(mime_content_type($file)); + if (isset($exts[$ext])) return $exts[$ext]; + + // Use fileinfo if available + if (extension_loaded('fileinfo') && isset($_ENV['MAGIC']) && + ($finfo = finfo_open(FILEINFO_MIME, $_ENV['MAGIC'])) !== false) + { + if (($type = finfo_file($finfo, $file)) !== false) + { + // Remove the charset and grab the last content-type + $type = explode(' ', str_replace('; charset=', ';charset=', $type)); + $type = array_pop($type); + $type = explode(';', $type); + $type = trim(array_shift($type)); + } + finfo_close($finfo); + if ($type !== false && strlen($type) > 0) return $type; + } return ($type !== false && strlen($type) > 0) ? $type : 'application/octet-stream'; } From a5c6f98351ddadeafe17cb6c33f6fa48d146e5d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Donovan=20Scho=CC=88nknecht?= Date: Sat, 5 Oct 2013 16:45:53 +0200 Subject: [PATCH 24/49] Add setTimeCorrectionOffset() to use the AWS server time to calculate an offset to apply to incorrect system times (fixes issues #23 and #25) --- S3.php | 53 ++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 48 insertions(+), 5 deletions(-) diff --git a/S3.php b/S3.php index 6be5fc85..1ef7391b 100644 --- a/S3.php +++ b/S3.php @@ -120,8 +120,13 @@ class S3 */ public static $useExceptions = false; - // SSL CURL SSL options - only needed if you are experiencing problems with your OpenSSL configuration - + /** + * Time offset applied to time() + * @access private + * @static + */ + private static $__timeOffset = 0; + /** * SSL client key * @@ -278,6 +283,30 @@ public static function setExceptions($enabled = true) } + /** + * Set AWS time correction offset (use carefully) + * + * This can be used when an inaccurate system time is generating + * invalid request signatures. It should only be used as a last + * resort when the system time cannot be changed. + * + * @param string $offset Time offset (set to zero to use AWS server time) + * @return void + */ + public static function setTimeCorrectionOffset($offset = 0) + { + if ($offset == 0) + { + $rest = new S3Request('HEAD'); + $rest = $rest->getResponse(); + $awstime = $rest->headers['date']; + $systime = time(); + $offset = $systime > $awstime ? -($systime - $awstime) : ($awstime - $systime); + } + self::$__timeOffset = $offset; + } + + /** * Set signing key * @@ -1125,7 +1154,7 @@ public static function deleteObject($bucket, $uri) */ public static function getAuthenticatedURL($bucket, $uri, $lifetime, $hostBucket = false, $https = false) { - $expires = time() + $lifetime; + $expires = self::__getTime() + $lifetime; $uri = str_replace(array('%2F', '%2B'), array('/', '+'), rawurlencode($uri)); return sprintf(($https ? 'https' : 'http').'://%s/%s?AWSAccessKeyId=%s&Expires=%u&Signature=%s', // $hostBucket ? $bucket : $bucket.'.s3.amazonaws.com', $uri, self::$__accessKey, $expires, @@ -1168,7 +1197,7 @@ public static function getSignedCannedURL($url, $lifetime) return self::getSignedPolicyURL(array( 'Statement' => array( array('Resource' => $url, 'Condition' => array( - 'DateLessThan' => array('AWS:EpochTime' => time() + $lifetime) + 'DateLessThan' => array('AWS:EpochTime' => self::__getTime() + $lifetime) )) ) )); @@ -1194,7 +1223,7 @@ public static function getHttpUploadPostParams($bucket, $uriPrefix = '', $acl = { // Create policy object $policy = new stdClass; - $policy->expiration = gmdate('Y-m-d\TH:i:s\Z', (time() + $lifetime)); + $policy->expiration = gmdate('Y-m-d\TH:i:s\Z', (self::__getTime() + $lifetime)); $policy->conditions = array(); $obj = new stdClass; $obj->bucket = $bucket; array_push($policy->conditions, $obj); $obj = new stdClass; $obj->acl = $acl; array_push($policy->conditions, $obj); @@ -1810,6 +1839,18 @@ private static function __getMIMEType(&$file) } + /** + * Get the current time + * + * @internal Used to apply offsets to sytem time + * @return integer + */ + public static function __getTime() + { + return time() + self::$__timeOffset; + } + + /** * Generate the auth string: "AWS AccessKey:Signature" * @@ -2278,6 +2319,8 @@ private function __responseHeaderCallback(&$curl, &$data) list($header, $value) = explode(': ', $data, 2); if ($header == 'Last-Modified') $this->response->headers['time'] = strtotime($value); + elseif ($header == 'Date') + $this->response->headers['date'] = strtotime($value); elseif ($header == 'Content-Length') $this->response->headers['size'] = (int)$value; elseif ($header == 'Content-Type') From d221e916e16524dec0c2868ccb855b7cef3ddc36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Donovan=20Scho=CC=88nknecht?= Date: Tue, 8 Oct 2013 10:26:41 +0200 Subject: [PATCH 25/49] Fix __getMIMEType error (thanks @mthie) --- S3.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/S3.php b/S3.php index 1ef7391b..b341725e 100644 --- a/S3.php +++ b/S3.php @@ -1835,7 +1835,7 @@ private static function __getMIMEType(&$file) if ($type !== false && strlen($type) > 0) return $type; } - return ($type !== false && strlen($type) > 0) ? $type : 'application/octet-stream'; + return 'application/octet-stream'; } From 7cce95a8aea426c36d1290b3eeb84a46dbecf6e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Donovan=20Scho=CC=88nknecht?= Date: Fri, 25 Oct 2013 11:03:05 +0200 Subject: [PATCH 26/49] Fix DisplayName field in listBuckets() (fixes issue #63) --- S3.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/S3.php b/S3.php index b341725e..2078f929 100644 --- a/S3.php +++ b/S3.php @@ -381,7 +381,7 @@ public static function listBuckets($detailed = false) { if (isset($rest->body->Owner, $rest->body->Owner->ID, $rest->body->Owner->DisplayName)) $results['owner'] = array( - 'id' => (string)$rest->body->Owner->ID, 'name' => (string)$rest->body->Owner->ID + 'id' => (string)$rest->body->Owner->ID, 'name' => (string)$rest->body->Owner->DisplayName ); $results['buckets'] = array(); foreach ($rest->body->Buckets->Bucket as $b) From 1a33bb8089a2b8d65579d15e86501aa842ad01b7 Mon Sep 17 00:00:00 2001 From: Paul Tarjan Date: Wed, 14 May 2014 17:27:58 -0700 Subject: [PATCH 27/49] Don't take curl callback by ref Changing these does nothing to the caller, why would you take them by ref? HHVM doesn't promote this to refs if they can't be modified: https://github.com/facebook/hhvm/issues/2587 --- S3.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/S3.php b/S3.php index 2078f929..55603bbb 100644 --- a/S3.php +++ b/S3.php @@ -2307,7 +2307,7 @@ private function __dnsBucketName($bucket) * @param string &$data Data * @return integer */ - private function __responseHeaderCallback(&$curl, &$data) + private function __responseHeaderCallback($curl, $data) { if (($strlen = strlen($data)) <= 2) return $strlen; if (substr($data, 0, 4) == 'HTTP') From 42a9aa2b1893e7c82d05ed1f73af2b2c8aa413fa Mon Sep 17 00:00:00 2001 From: Paul Tarjan Date: Wed, 14 May 2014 23:00:27 -0700 Subject: [PATCH 28/49] fix docblock gotta love duplication --- S3.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/S3.php b/S3.php index 55603bbb..0b1564b2 100644 --- a/S3.php +++ b/S3.php @@ -2303,8 +2303,8 @@ private function __dnsBucketName($bucket) /** * CURL header callback * - * @param resource &$curl CURL resource - * @param string &$data Data + * @param resource $curl CURL resource + * @param string $data Data * @return integer */ private function __responseHeaderCallback($curl, $data) From 938b855940a1bacf2a6dd2faf25d0c7aff56aeb6 Mon Sep 17 00:00:00 2001 From: Eric Norris Date: Mon, 13 Oct 2014 14:15:59 -0400 Subject: [PATCH 29/49] Allow x-amz headers in $requestHeaders param. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With this, custom headers like ‘x-amz-server-side-encryption-customer-algorithm’ will work. --- S3.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/S3.php b/S3.php index 0b1564b2..f421c0c0 100644 --- a/S3.php +++ b/S3.php @@ -642,7 +642,8 @@ public static function putObject($input, $bucket, $uri, $acl = self::ACL_PRIVATE // Custom request headers (Content-Type, Content-Disposition, Content-Encoding) if (is_array($requestHeaders)) - foreach ($requestHeaders as $h => $v) $rest->setHeader($h, $v); + foreach ($requestHeaders as $h => $v) + strpos($h, 'x-amz-') === 0 ? $rest->setAmzHeader($h, $v) : $rest->setHeader($h, $v); elseif (is_string($requestHeaders)) // Support for legacy contentType parameter $input['type'] = $requestHeaders; @@ -797,7 +798,8 @@ public static function copyObject($srcBucket, $srcUri, $bucket, $uri, $acl = sel { $rest = new S3Request('PUT', $bucket, $uri, self::$endpoint); $rest->setHeader('Content-Length', 0); - foreach ($requestHeaders as $h => $v) $rest->setHeader($h, $v); + foreach ($requestHeaders as $h => $v) + strpos($h, 'x-amz-') === 0 ? $rest->setAmzHeader($h, $v) : $rest->setHeader($h, $v); foreach ($metaHeaders as $h => $v) $rest->setAmzHeader('x-amz-meta-'.$h, $v); if ($storageClass !== self::STORAGE_CLASS_STANDARD) // Storage class $rest->setAmzHeader('x-amz-storage-class', $storageClass); From 75de3abbf536f8f5986482685bd7ae3a073164ff Mon Sep 17 00:00:00 2001 From: gabrieljenik Date: Fri, 24 Oct 2014 16:46:14 -0300 Subject: [PATCH 30/49] Added Default Delimiter Added Default Delimiter to be used while browsing S3 buckets --- S3.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/S3.php b/S3.php index 0b1564b2..40c8a06a 100644 --- a/S3.php +++ b/S3.php @@ -74,6 +74,14 @@ class S3 * @static */ private static $__sslKey = null; + + /** + * Default delimiter to be used, for example while getBucket(). + * @var string + * @access public + * @static + */ + public static $defDelimiter = null; /** * AWS URI @@ -416,6 +424,7 @@ public static function getBucket($bucket, $prefix = null, $marker = null, $maxKe if ($marker !== null && $marker !== '') $rest->setParameter('marker', $marker); if ($maxKeys !== null && $maxKeys !== '') $rest->setParameter('max-keys', $maxKeys); if ($delimiter !== null && $delimiter !== '') $rest->setParameter('delimiter', $delimiter); + else if (!empty(self::$defDelimiter)) $rest->setParameter('delimiter', self::$defDelimiter); $response = $rest->getResponse(); if ($response->error === false && $response->code !== 200) $response->error = array('code' => $response->code, 'message' => 'Unexpected HTTP status'); From 0173fb4dd462e96d52ecf1087f261fac12f93c9f Mon Sep 17 00:00:00 2001 From: clphillips Date: Tue, 30 Dec 2014 11:05:38 -0800 Subject: [PATCH 31/49] fixes stat cache from #89 --- S3.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/S3.php b/S3.php index 0b1564b2..6ec64b8c 100644 --- a/S3.php +++ b/S3.php @@ -561,6 +561,7 @@ public static function inputFile($file, $md5sum = true) self::__triggerError('S3::inputFile(): Unable to open input file: '.$file, __FILE__, __LINE__); return false; } + clearstatcache(false, $file); return array('file' => $file, 'size' => filesize($file), 'md5sum' => $md5sum !== false ? (is_string($md5sum) ? $md5sum : base64_encode(md5_file($file, true))) : ''); } @@ -634,8 +635,10 @@ public static function putObject($input, $bucket, $uri, $acl = self::ACL_PRIVATE if (isset($input['size']) && $input['size'] >= 0) $rest->size = $input['size']; else { - if (isset($input['file'])) + if (isset($input['file'])) { + clearstatcache(false, $input['file']); $rest->size = filesize($input['file']); + } elseif (isset($input['data'])) $rest->size = strlen($input['data']); } From fada987875f9f2de661d2fc9d5cf0c3bc2f21da2 Mon Sep 17 00:00:00 2001 From: clphillips Date: Tue, 30 Dec 2014 11:09:07 -0800 Subject: [PATCH 32/49] fixes stat cache from #89 --- S3.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/S3.php b/S3.php index 6ec64b8c..c23addaf 100644 --- a/S3.php +++ b/S3.php @@ -561,7 +561,7 @@ public static function inputFile($file, $md5sum = true) self::__triggerError('S3::inputFile(): Unable to open input file: '.$file, __FILE__, __LINE__); return false; } - clearstatcache(false, $file); + clearstatcache(false, $file); return array('file' => $file, 'size' => filesize($file), 'md5sum' => $md5sum !== false ? (is_string($md5sum) ? $md5sum : base64_encode(md5_file($file, true))) : ''); } @@ -636,7 +636,7 @@ public static function putObject($input, $bucket, $uri, $acl = self::ACL_PRIVATE $rest->size = $input['size']; else { if (isset($input['file'])) { - clearstatcache(false, $input['file']); + clearstatcache(false, $input['file']); $rest->size = filesize($input['file']); } elseif (isset($input['data'])) From f363dd4c89af8da1b6fe3aab92b8e2fc52e57c63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Donovan=20Sch=C3=B6nknecht?= Date: Tue, 17 Mar 2015 23:42:34 +0200 Subject: [PATCH 33/49] Don't use hostname buckets for SSL (fixes #104) --- S3.php | 1 + 1 file changed, 1 insertion(+) diff --git a/S3.php b/S3.php index 1ef3ff0f..527051e3 100644 --- a/S3.php +++ b/S3.php @@ -2306,6 +2306,7 @@ private function __responseWriteCallback(&$curl, &$data) private function __dnsBucketName($bucket) { if (strlen($bucket) > 63 || preg_match("/[^a-z0-9\.-]/", $bucket) > 0) return false; + if (S3::$useSSL && strstr($bucket, '.') !== false) return false; if (strstr($bucket, '-.') !== false) return false; if (strstr($bucket, '..') !== false) return false; if (!preg_match("/^[0-9a-z]/", $bucket)) return false; From 53e04babd33ba787e29611de7f3c1f22e6f9ec0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Donovan=20Sch=C3=B6nknecht?= Date: Mon, 27 Apr 2015 08:11:13 +0200 Subject: [PATCH 34/49] Use TLS by default --- S3.php | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/S3.php b/S3.php index 527051e3..cb455c74 100644 --- a/S3.php +++ b/S3.php @@ -56,7 +56,7 @@ class S3 * @static */ private static $__accessKey = null; - + /** * AWS Secret Key * @@ -65,7 +65,7 @@ class S3 * @static */ private static $__secretKey = null; - + /** * SSL Client key * @@ -82,7 +82,7 @@ class S3 * @static */ public static $defDelimiter = null; - + /** * AWS URI * @@ -91,7 +91,7 @@ class S3 * @static */ public static $endpoint = 's3.amazonaws.com'; - + /** * Proxy information * @@ -100,7 +100,7 @@ class S3 * @static */ public static $proxy = null; - + /** * Connect using SSL? * @@ -109,7 +109,7 @@ class S3 * @static */ public static $useSSL = false; - + /** * Use SSL validation? * @@ -118,7 +118,16 @@ class S3 * @static */ public static $useSSLValidation = true; - + + /** + * Use SSL version + * + * @var const + * @access public + * @static + */ + public static $useSSLVersion = CURL_SSLVERSION_TLSv1; + /** * Use PHP exceptions? * @@ -210,6 +219,7 @@ public function setEndpoint($host) self::$endpoint = $host; } + /** * Set AWS access key and secret key * @@ -2131,6 +2141,9 @@ public function getResponse() if (S3::$useSSL) { + // Set protocol version + curl_setopt($curl, CURLOPT_SSLVERSION, S3::$useSSLVersion); + // SSL Validation can now be optional for those with broken OpenSSL installations curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, S3::$useSSLValidation ? 2 : 0); curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, S3::$useSSLValidation ? 1 : 0); From 121318e65e857a994b22ffe0aa04a0c55e832bea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Donovan=20Sch=C3=B6nknecht?= Date: Mon, 27 Apr 2015 08:12:39 +0200 Subject: [PATCH 35/49] Version bump --- S3.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/S3.php b/S3.php index cb455c74..75070ded 100644 --- a/S3.php +++ b/S3.php @@ -32,7 +32,7 @@ * Amazon S3 PHP class * * @link http://undesigned.org.za/2007/10/22/amazon-s3-php-class -* @version 0.5.1-dev +* @version 0.5.1 */ class S3 { From 2a033074d312cda888b7e1ef19e644486a82d231 Mon Sep 17 00:00:00 2001 From: Gabriel Jenik Date: Tue, 29 Sep 2015 12:06:49 -0300 Subject: [PATCH 36/49] Added support Infrequent Access storage --- S3.php | 1 + 1 file changed, 1 insertion(+) diff --git a/S3.php b/S3.php index 0b1564b2..6dc5740c 100644 --- a/S3.php +++ b/S3.php @@ -44,6 +44,7 @@ class S3 const STORAGE_CLASS_STANDARD = 'STANDARD'; const STORAGE_CLASS_RRS = 'REDUCED_REDUNDANCY'; + const STORAGE_CLASS_STANDARD_IA = 'STANDARD_IA'; const SSE_NONE = ''; const SSE_AES256 = 'AES256'; From 1aaf1518a12fe552f109797d8564f761012f551d Mon Sep 17 00:00:00 2001 From: Adam Dobrawy Date: Sun, 8 Nov 2015 23:55:51 +0100 Subject: [PATCH 37/49] Case in-sensitive header compare From RFC 2616 - "Hypertext Transfer Protocol -- HTTP/1.1", Section 4.2, "Message Headers": > Each header field consists of a name followed by a colon (":") and the field value. Field names are case-insensitive. The updating RFC 7230 does not list any changes from RFC 2616 at this part. --- S3.php | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/S3.php b/S3.php index 660844c4..8c6960ad 100644 --- a/S3.php +++ b/S3.php @@ -2346,15 +2346,16 @@ private function __responseHeaderCallback($curl, $data) $data = trim($data); if (strpos($data, ': ') === false) return $strlen; list($header, $value) = explode(': ', $data, 2); - if ($header == 'Last-Modified') + $header = strtolower($header); + if ($header == 'last-modified') $this->response->headers['time'] = strtotime($value); - elseif ($header == 'Date') + elseif ($header == 'date') $this->response->headers['date'] = strtotime($value); - elseif ($header == 'Content-Length') + elseif ($header == 'content-length') $this->response->headers['size'] = (int)$value; - elseif ($header == 'Content-Type') + elseif ($header == 'content-type') $this->response->headers['type'] = $value; - elseif ($header == 'ETag') + elseif ($header == 'etag') $this->response->headers['hash'] = $value{0} == '"' ? substr($value, 1, -1) : $value; elseif (preg_match('/^x-amz-meta-.*$/', $header)) $this->response->headers[$header] = $value; From 4ad2e3285643bb2a47218caac46db59e75c48a10 Mon Sep 17 00:00:00 2001 From: Tiger Date: Thu, 23 Jun 2016 20:34:56 +0800 Subject: [PATCH 38/49] fix a wrong word in line 1924 --- S3.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/S3.php b/S3.php index 660844c4..83c01733 100644 --- a/S3.php +++ b/S3.php @@ -1921,7 +1921,7 @@ final class S3Request * AWS URI * * @var string - * @access pricate + * @access private */ private $endpoint; From 32afaa460de825f32df1790a0acbb562653d5886 Mon Sep 17 00:00:00 2001 From: Rack Lin Date: Wed, 14 Dec 2016 20:45:04 +0800 Subject: [PATCH 39/49] Added AWS v4 signature supports. --- README.md | 10 ++++ S3.php | 155 +++++++++++++++++++++++++++++++++++++++++++++++++--- example.php | 3 + 3 files changed, 159 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 6643f90b..d7682248 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,16 @@ Statically (e,g; S3::getObject(...)): S3::setAuth($awsAccessKey, $awsSecretKey); ``` +### AWS V4 Signature + +```php +$s3->setSignatureVersion('v4); +``` +Or statically +```php +S3::setSignatureVersion('v4'); +``` + ### Object Operations #### Uploading objects diff --git a/S3.php b/S3.php index 83c01733..e5e1d7d2 100644 --- a/S3.php +++ b/S3.php @@ -190,6 +190,14 @@ class S3 */ private static $__signingKeyResource = false; + /** + * AWS Signature Version + * + * @var string + * @acess public + * @static + */ + public static $signVer = 'v2'; /** * Constructor - if you're not using the class statically @@ -198,6 +206,7 @@ class S3 * @param string $secretKey Secret key * @param boolean $useSSL Enable SSL * @param string $endpoint Amazon URI + * @param string $region Bucket Region * @return void */ public function __construct($accessKey = null, $secretKey = null, $useSSL = false, $endpoint = 's3.amazonaws.com') @@ -344,6 +353,18 @@ public static function setSigningKey($keyPairId, $signingKey, $isFile = true) } + /** + * Set Signature Version + * + * @param string $version of signature ('v4' or 'v2') + * @return void + */ + public static function setSignatureVersion($version = 'v2') + { + self::$signVer = $version; + } + + /** * Free signing key from memory, MUST be called if you are using setSigningKey() * @@ -583,7 +604,7 @@ public static function inputFile($file, $md5sum = true) } clearstatcache(false, $file); return array('file' => $file, 'size' => filesize($file), 'md5sum' => $md5sum !== false ? - (is_string($md5sum) ? $md5sum : base64_encode(md5_file($file, true))) : ''); + (is_string($md5sum) ? $md5sum : base64_encode(md5_file($file, true))) : '', 'sha256sum' => hash_file('sha256', $file)); } @@ -640,7 +661,8 @@ public static function putObject($input, $bucket, $uri, $acl = self::ACL_PRIVATE if (!is_array($input)) $input = array( 'data' => $input, 'size' => strlen($input), - 'md5sum' => base64_encode(md5($input, true)) + 'md5sum' => base64_encode(md5($input, true)), + 'sha256sum' => hash('sha256', $input) ); // Data @@ -693,6 +715,8 @@ public static function putObject($input, $bucket, $uri, $acl = self::ACL_PRIVATE $rest->setHeader('Content-Type', $input['type']); if (isset($input['md5sum'])) $rest->setHeader('Content-MD5', $input['md5sum']); + if (isset($input['sha256sum'])) $rest->setAmzHeader('x-amz-content-sha256', $input['sha256sum']); + $rest->setAmzHeader('x-amz-acl', $acl); foreach ($metaHeaders as $h => $v) $rest->setAmzHeader('x-amz-meta-'.$h, $v); $rest->getResponse(); @@ -1907,6 +1931,105 @@ private static function __getHash($string) (str_repeat(chr(0x36), 64))) . $string))))); } + + /** + * Generate the headers for AWS Signature V4 + * @internal Used by S3Request::getResponse() + * @param array $aheaders amzHeaders + * @param array $headers + * @param string $method + * @param string $uri + * @param array $parameters + * @return array $headers + */ + public static function __getSignatureV4($aHeaders, $headers, $method='GET', $uri='', $parameters=array()) + { + // calculate the service name and region from the endpoint hostname: dynamodb.us-east-1.amazonaws.com + list( $service, $region, $junk ) = explode( '.', self::$endpoint ); + + $algorithm = 'AWS4-HMAC-SHA256'; + $amzHeaders = array(); + $amzRequests = array(); + + $date = new DateTime( 'UTC' ); + $amzDate = $date->format( 'Ymd\THis\Z' ); + $amzDateStamp = $date->format( 'Ymd' ); + + // amz-date ISO8601 format ? for aws request + $amzHeaders['x-amz-date'] = $amzDate; + + // CanonicalHeaders + foreach ( $headers as $k => $v ) { + $amzHeaders[ strtolower( $k ) ] = trim( $v ); + } + foreach ( $aHeaders as $k => $v ) { + $amzHeaders[ strtolower( $k ) ] = trim( $v ); + } + uksort( $amzHeaders, 'strcmp' ); + + // payload + $payloadHash = isset($amzHeaders['x-amz-content-sha256']) ? $amzHeaders['x-amz-content-sha256'] : hash('sha256', ''); + + // CanonicalRequests + $amzRequests[] = $method; + $amzRequests[] = $uri; + $amzRequests[] = http_build_query($parameters); + // add header as string to requests + foreach ( $amzHeaders as $k => $v ) { + $amzRequests[] = $k . ':' . $v; + } + // add a blank entry so we end up with an extra line break + $amzRequests[] = ''; + // SignedHeaders + $amzRequests[] = implode( ';', array_keys( $amzHeaders ) ); + // payload hash + $amzRequests[] = $payloadHash; + // request as string + $amzRequestStr = implode("\n", $amzRequests); + + // CredentialScope + $credentialScope = array(); + $credentialScope[] = $amzDateStamp; + $credentialScope[] = $region; + $credentialScope[] = $service; + $credentialScope[] = 'aws4_request'; + + // stringToSign + $stringToSign = array(); + $stringToSign[] = $algorithm; + $stringToSign[] = $amzDate; + $stringToSign[] = implode('/', $credentialScope); + $stringToSign[] = hash('sha256', $amzRequestStr); + // as string + $stringToSignStr = implode("\n", $stringToSign); + + // Make Signature + $kSecret = 'AWS4' . self::$__secretKey; + $kDate = hash_hmac( 'sha256', $amzDateStamp, $kSecret, true ); + $kRegion = hash_hmac( 'sha256', $region, $kDate, true ); + $kService = hash_hmac( 'sha256', $service, $kRegion, true ); + $kSigning = hash_hmac( 'sha256', 'aws4_request', $kService, true ); + + $signature = hash_hmac( 'sha256', $stringToSignStr, $kSigning ); + + $authorization = array( + 'Credential=' . self::$__accessKey . '/' . implode( '/', $credentialScope ), + 'SignedHeaders=' . implode( ';', array_keys( $amzHeaders ) ), + 'Signature=' . $signature, + ); + + $authorizationStr = $algorithm . ' ' . implode( ',', $authorization ); + + $resultHeaders = array( + 'X-AMZ-DATE' => $amzDate, + 'Authorization' => $authorizationStr + ); + if (!isset($aHeaders['x-amz-content-sha256'])) $resultHeaders['x-amz-content-sha256'] = $payloadHash; + + return $resultHeaders; + } + + } /** @@ -2190,13 +2313,27 @@ public function getResponse() $headers[] = 'Authorization: ' . S3::__getSignature($this->headers['Date']); else { - $headers[] = 'Authorization: ' . S3::__getSignature( - $this->verb."\n". - $this->headers['Content-MD5']."\n". - $this->headers['Content-Type']."\n". - $this->headers['Date'].$amz."\n". - $this->resource - ); + if (S3::$signVer == 'v2') + { + $headers[] = 'Authorization: ' . S3::__getSignature( + $this->verb."\n". + $this->headers['Content-MD5']."\n". + $this->headers['Content-Type']."\n". + $this->headers['Date'].$amz."\n". + $this->resource + ); + } else { + $amzHeaders = S3::__getSignatureV4( + $this->amzHeaders, + $this->headers, + $this->verb, + $this->uri, + $this->parameters + ); + foreach ($amzHeaders as $k => $v) { + $headers[] = $k .': '. $v; + } + } } } diff --git a/example.php b/example.php index d6c90dd6..664a3f87 100755 --- a/example.php +++ b/example.php @@ -35,6 +35,9 @@ // Instantiate the class $s3 = new S3(awsAccessKey, awsSecretKey); +// using v4 signature +$s3->setSignatureVersion('v4'); + // List your buckets: echo "S3::listBuckets(): ".print_r($s3->listBuckets(), 1)."\n"; From 0fc28158ce66fc14a88423c1a7484065b350a8b2 Mon Sep 17 00:00:00 2001 From: Rack Lin Date: Thu, 15 Dec 2016 09:57:01 +0800 Subject: [PATCH 40/49] remote unused parameter comment. --- S3.php | 1 - 1 file changed, 1 deletion(-) diff --git a/S3.php b/S3.php index e5e1d7d2..2ad97ae2 100644 --- a/S3.php +++ b/S3.php @@ -206,7 +206,6 @@ class S3 * @param string $secretKey Secret key * @param boolean $useSSL Enable SSL * @param string $endpoint Amazon URI - * @param string $region Bucket Region * @return void */ public function __construct($accessKey = null, $secretKey = null, $useSSL = false, $endpoint = 's3.amazonaws.com') From ae6eda449194b694e4a3a32cf6151485fff1c227 Mon Sep 17 00:00:00 2001 From: Rack Lin Date: Thu, 15 Dec 2016 14:42:06 +0800 Subject: [PATCH 41/49] Using gmdate instead of DateTime class for process UTC time. --- S3.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/S3.php b/S3.php index 2ad97ae2..cb06471e 100644 --- a/S3.php +++ b/S3.php @@ -1950,9 +1950,8 @@ public static function __getSignatureV4($aHeaders, $headers, $method='GET', $uri $amzHeaders = array(); $amzRequests = array(); - $date = new DateTime( 'UTC' ); - $amzDate = $date->format( 'Ymd\THis\Z' ); - $amzDateStamp = $date->format( 'Ymd' ); + $amzDate = gmdate( 'Ymd\THis\Z' ); + $amzDateStamp = gmdate( 'Ymd' ); // amz-date ISO8601 format ? for aws request $amzHeaders['x-amz-date'] = $amzDate; From 4ff3b0a8de3ea1bac488b338ecfd08eb26fba10e Mon Sep 17 00:00:00 2001 From: Rack Lin Date: Fri, 16 Dec 2016 09:38:15 +0800 Subject: [PATCH 42/49] Using regular expression to parse region from endpoint if not specific region. fixed putBucket bugs. --- S3.php | 59 ++++++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 53 insertions(+), 6 deletions(-) diff --git a/S3.php b/S3.php index cb06471e..c1ac8367 100644 --- a/S3.php +++ b/S3.php @@ -93,6 +93,15 @@ class S3 */ public static $endpoint = 's3.amazonaws.com'; + /** + * AWS Region + * + * @var string + * @acess public + * @static + */ + public static $region = ''; + /** * Proxy information * @@ -208,12 +217,13 @@ class S3 * @param string $endpoint Amazon URI * @return void */ - public function __construct($accessKey = null, $secretKey = null, $useSSL = false, $endpoint = 's3.amazonaws.com') + public function __construct($accessKey = null, $secretKey = null, $useSSL = false, $endpoint = 's3.amazonaws.com', $region = '') { if ($accessKey !== null && $secretKey !== null) self::setAuth($accessKey, $secretKey); self::$useSSL = $useSSL; self::$endpoint = $endpoint; + self::$region = $region; } @@ -229,6 +239,39 @@ public function setEndpoint($host) } + /** + * Set the service region + * + * @param string $region + * @return void + */ + public function setRegion($region) + { + self::$region = $region; + } + + + /** + * Get the service region + * + * @return string $region + * @static + */ + public static function getRegion() + { + $region = self::$region; + + // parse region from endpoint if not specific + if (empty($region)) { + if (preg_match("/s3[.-](?:website-|dualstack\.)?(.+)\.amazonaws\.com/i",self::$endpoint,$match) !== 0 && strtolower($match[1]) !== "external-1") { + $region = $match[1]; + } + } + + return empty($region) ? 'us-east-1' : $region; + } + + /** * Set AWS access key and secret key * @@ -540,6 +583,8 @@ public static function putBucket($bucket, $acl = self::ACL_PRIVATE, $location = $rest = new S3Request('PUT', $bucket, '', self::$endpoint); $rest->setAmzHeader('x-amz-acl', $acl); + if ($location === false) $location = self::getRegion(); + if ($location !== false) { $dom = new DOMDocument; @@ -1938,13 +1983,14 @@ private static function __getHash($string) * @param array $headers * @param string $method * @param string $uri + * @param string $data * @param array $parameters * @return array $headers */ - public static function __getSignatureV4($aHeaders, $headers, $method='GET', $uri='', $parameters=array()) - { - // calculate the service name and region from the endpoint hostname: dynamodb.us-east-1.amazonaws.com - list( $service, $region, $junk ) = explode( '.', self::$endpoint ); + public static function __getSignatureV4($aHeaders, $headers, $method='GET', $uri='', $data = '', $parameters=array()) + { + $service = 's3'; + $region = S3::getRegion(); $algorithm = 'AWS4-HMAC-SHA256'; $amzHeaders = array(); @@ -1966,7 +2012,7 @@ public static function __getSignatureV4($aHeaders, $headers, $method='GET', $uri uksort( $amzHeaders, 'strcmp' ); // payload - $payloadHash = isset($amzHeaders['x-amz-content-sha256']) ? $amzHeaders['x-amz-content-sha256'] : hash('sha256', ''); + $payloadHash = isset($amzHeaders['x-amz-content-sha256']) ? $amzHeaders['x-amz-content-sha256'] : hash('sha256', $data); // CanonicalRequests $amzRequests[] = $method; @@ -2326,6 +2372,7 @@ public function getResponse() $this->headers, $this->verb, $this->uri, + $this->data, $this->parameters ); foreach ($amzHeaders as $k => $v) { From eca1de29149b67336a302c51da6a36740207a093 Mon Sep 17 00:00:00 2001 From: Rack Lin Date: Fri, 16 Dec 2016 15:43:01 +0800 Subject: [PATCH 43/49] Fixed getBucketLocation / getBucket issues in v4 signature. --- S3.php | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/S3.php b/S3.php index c1ac8367..0d9cfd19 100644 --- a/S3.php +++ b/S3.php @@ -1984,10 +1984,9 @@ private static function __getHash($string) * @param string $method * @param string $uri * @param string $data - * @param array $parameters * @return array $headers */ - public static function __getSignatureV4($aHeaders, $headers, $method='GET', $uri='', $data = '', $parameters=array()) + public static function __getSignatureV4($aHeaders, $headers, $method='GET', $uri='', $data = '') { $service = 's3'; $region = S3::getRegion(); @@ -2014,10 +2013,17 @@ public static function __getSignatureV4($aHeaders, $headers, $method='GET', $uri // payload $payloadHash = isset($amzHeaders['x-amz-content-sha256']) ? $amzHeaders['x-amz-content-sha256'] : hash('sha256', $data); + // parameters + $parameters = array(); + if (strpos($uri, '?')) { + list ($uri, $query_str) = @explode("?", $uri); + parse_str($query_str, $parameters); + } + // CanonicalRequests $amzRequests[] = $method; $amzRequests[] = $uri; - $amzRequests[] = http_build_query($parameters); + $amzRequests[] = http_build_query($parameters); // add header as string to requests foreach ( $amzHeaders as $k => $v ) { $amzRequests[] = $k . ':' . $v; @@ -2372,8 +2378,7 @@ public function getResponse() $this->headers, $this->verb, $this->uri, - $this->data, - $this->parameters + $this->data ); foreach ($amzHeaders as $k => $v) { $headers[] = $k .': '. $v; From 334ff8f023e3184e527500491870ceb7e92e0e2b Mon Sep 17 00:00:00 2001 From: Rack Lin Date: Sat, 15 Apr 2017 01:37:40 +0800 Subject: [PATCH 44/49] Fixes some minor bugs Signed-off-by: Rack Lin --- S3.php | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/S3.php b/S3.php index 0d9cfd19..4fddd1d4 100644 --- a/S3.php +++ b/S3.php @@ -585,7 +585,7 @@ public static function putBucket($bucket, $acl = self::ACL_PRIVATE, $location = if ($location === false) $location = self::getRegion(); - if ($location !== false) + if ($location !== false && $location !== "us-east-1") { $dom = new DOMDocument; $createBucketConfiguration = $dom->createElement('CreateBucketConfiguration'); @@ -1776,11 +1776,13 @@ private static function __getCloudFrontDistributionConfigXML($bucket, $enabled, if ($comment !== '') $distributionConfig->appendChild($dom->createElement('Comment', $comment)); $distributionConfig->appendChild($dom->createElement('Enabled', $enabled ? 'true' : 'false')); - $trusted = $dom->createElement('TrustedSigners'); - foreach ($trustedSigners as $id => $type) - $trusted->appendChild($id !== '' ? $dom->createElement($type, $id) : $dom->createElement($type)); - $distributionConfig->appendChild($trusted); - + if (!empty($trustedSigners)) + { + $trusted = $dom->createElement('TrustedSigners'); + foreach ($trustedSigners as $id => $type) + $trusted->appendChild($id !== '' ? $dom->createElement($type, $id) : $dom->createElement($type)); + $distributionConfig->appendChild($trusted); + } $dom->appendChild($distributionConfig); //var_dump($dom->saveXML()); return $dom->saveXML(); @@ -2022,7 +2024,8 @@ public static function __getSignatureV4($aHeaders, $headers, $method='GET', $uri // CanonicalRequests $amzRequests[] = $method; - $amzRequests[] = $uri; + $uriQmPos = strpos($uri, '?'); + $amzRequests[] = ($uriQmPos === false ? $uri : substr($uri, 0, $uriQmPos)); $amzRequests[] = http_build_query($parameters); // add header as string to requests foreach ( $amzHeaders as $k => $v ) { From 3a8ac22582ac91e958152251e087958c6c8f56c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Donovan=20Sch=C3=B6nknecht?= Date: Sun, 5 May 2019 21:03:11 +0200 Subject: [PATCH 45/49] Bug fixes / code cleanup --- S3.php | 281 +++++++++++++++++++++++++-------------------------------- 1 file changed, 123 insertions(+), 158 deletions(-) diff --git a/S3.php b/S3.php index 4fddd1d4..da5d9983 100644 --- a/S3.php +++ b/S3.php @@ -200,13 +200,13 @@ class S3 private static $__signingKeyResource = false; /** - * AWS Signature Version + * CURL progress function callback * - * @var string - * @acess public - * @static + * @var function + * @access public + * @static */ - public static $signVer = 'v2'; + public static $progressFunction = null; /** * Constructor - if you're not using the class statically @@ -262,8 +262,11 @@ public static function getRegion() $region = self::$region; // parse region from endpoint if not specific - if (empty($region)) { - if (preg_match("/s3[.-](?:website-|dualstack\.)?(.+)\.amazonaws\.com/i",self::$endpoint,$match) !== 0 && strtolower($match[1]) !== "external-1") { + if (empty($region)) + { + if (preg_match("/s3[.-](?:website-|dualstack\.)?(.+)\.amazonaws\.com/i", self::$endpoint, $match) !== 0 + && strtolower($match[1]) !== "external-1") + { $region = $match[1]; } } @@ -395,27 +398,27 @@ public static function setSigningKey($keyPairId, $signingKey, $isFile = true) } + /** - * Set Signature Version + * Free signing key from memory, MUST be called if you are using setSigningKey() * - * @param string $version of signature ('v4' or 'v2') * @return void */ - public static function setSignatureVersion($version = 'v2') + public static function freeSigningKey() { - self::$signVer = $version; + if (self::$__signingKeyResource !== false) + openssl_free_key(self::$__signingKeyResource); } - /** - * Free signing key from memory, MUST be called if you are using setSigningKey() + * Set progress function * + * @param function $func Progress function * @return void */ - public static function freeSigningKey() + public static function setProgressFunction($func = null) { - if (self::$__signingKeyResource !== false) - openssl_free_key(self::$__signingKeyResource); + self::$progressFunction = $func; } @@ -1980,109 +1983,100 @@ private static function __getHash($string) /** * Generate the headers for AWS Signature V4 + * * @internal Used by S3Request::getResponse() - * @param array $aheaders amzHeaders + * @param array $amzHeaders * @param array $headers - * @param string $method + * @param string $method * @param string $uri - * @param string $data - * @return array $headers + * @param array $parameters + * @return array */ - public static function __getSignatureV4($aHeaders, $headers, $method='GET', $uri='', $data = '') + public static function __getSignatureV4($amzHeaders, $headers, $method, $uri, $parameters) { $service = 's3'; $region = S3::getRegion(); - + $algorithm = 'AWS4-HMAC-SHA256'; - $amzHeaders = array(); - $amzRequests = array(); - - $amzDate = gmdate( 'Ymd\THis\Z' ); - $amzDateStamp = gmdate( 'Ymd' ); + $combinedHeaders = array(); - // amz-date ISO8601 format ? for aws request - $amzHeaders['x-amz-date'] = $amzDate; + $amzDateStamp = substr($amzHeaders['x-amz-date'], 0, 8); // CanonicalHeaders - foreach ( $headers as $k => $v ) { - $amzHeaders[ strtolower( $k ) ] = trim( $v ); - } - foreach ( $aHeaders as $k => $v ) { - $amzHeaders[ strtolower( $k ) ] = trim( $v ); - } - uksort( $amzHeaders, 'strcmp' ); - - // payload - $payloadHash = isset($amzHeaders['x-amz-content-sha256']) ? $amzHeaders['x-amz-content-sha256'] : hash('sha256', $data); - - // parameters - $parameters = array(); - if (strpos($uri, '?')) { - list ($uri, $query_str) = @explode("?", $uri); - parse_str($query_str, $parameters); - } - - // CanonicalRequests - $amzRequests[] = $method; - $uriQmPos = strpos($uri, '?'); - $amzRequests[] = ($uriQmPos === false ? $uri : substr($uri, 0, $uriQmPos)); - $amzRequests[] = http_build_query($parameters); + foreach ($headers as $k => $v) + $combinedHeaders[strtolower($k)] = trim($v); + foreach ($amzHeaders as $k => $v) + $combinedHeaders[strtolower($k)] = trim($v); + uksort($combinedHeaders, array('self', '__sortMetaHeadersCmp')); + + // Convert null query string parameters to strings and sort + $parameters = array_map('strval', $parameters); + uksort($parameters, array('self', '__sortMetaHeadersCmp')); + $queryString = http_build_query($parameters, null, '&', PHP_QUERY_RFC3986); + + // Payload + $amzPayload = array($method); + + $qsPos = strpos($uri, '?'); + $amzPayload[] = ($qsPos === false ? $uri : substr($uri, 0, $qsPos)); + + $amzPayload[] = $queryString; // add header as string to requests - foreach ( $amzHeaders as $k => $v ) { - $amzRequests[] = $k . ':' . $v; + foreach ($combinedHeaders as $k => $v ) + { + $amzPayload[] = $k . ':' . $v; } // add a blank entry so we end up with an extra line break - $amzRequests[] = ''; + $amzPayload[] = ''; // SignedHeaders - $amzRequests[] = implode( ';', array_keys( $amzHeaders ) ); + $amzPayload[] = implode(';', array_keys($combinedHeaders)); // payload hash - $amzRequests[] = $payloadHash; + $amzPayload[] = $amzHeaders['x-amz-content-sha256']; // request as string - $amzRequestStr = implode("\n", $amzRequests); - + $amzPayloadStr = implode("\n", $amzPayload); + // CredentialScope - $credentialScope = array(); - $credentialScope[] = $amzDateStamp; - $credentialScope[] = $region; - $credentialScope[] = $service; - $credentialScope[] = 'aws4_request'; + $credentialScope = array($amzDateStamp, $region, $service, 'aws4_request'); // stringToSign - $stringToSign = array(); - $stringToSign[] = $algorithm; - $stringToSign[] = $amzDate; - $stringToSign[] = implode('/', $credentialScope); - $stringToSign[] = hash('sha256', $amzRequestStr); - // as string - $stringToSignStr = implode("\n", $stringToSign); + $stringToSignStr = implode("\n", array($algorithm, $amzHeaders['x-amz-date'], + implode('/', $credentialScope), hash('sha256', $amzPayloadStr))); // Make Signature $kSecret = 'AWS4' . self::$__secretKey; - $kDate = hash_hmac( 'sha256', $amzDateStamp, $kSecret, true ); - $kRegion = hash_hmac( 'sha256', $region, $kDate, true ); - $kService = hash_hmac( 'sha256', $service, $kRegion, true ); - $kSigning = hash_hmac( 'sha256', 'aws4_request', $kService, true ); - - $signature = hash_hmac( 'sha256', $stringToSignStr, $kSigning ); - - $authorization = array( - 'Credential=' . self::$__accessKey . '/' . implode( '/', $credentialScope ), - 'SignedHeaders=' . implode( ';', array_keys( $amzHeaders ) ), - 'Signature=' . $signature, - ); - - $authorizationStr = $algorithm . ' ' . implode( ',', $authorization ); + $kDate = hash_hmac('sha256', $amzDateStamp, $kSecret, true); + $kRegion = hash_hmac('sha256', $region, $kDate, true); + $kService = hash_hmac('sha256', $service, $kRegion, true); + $kSigning = hash_hmac('sha256', 'aws4_request', $kService, true); - $resultHeaders = array( - 'X-AMZ-DATE' => $amzDate, - 'Authorization' => $authorizationStr - ); - if (!isset($aHeaders['x-amz-content-sha256'])) $resultHeaders['x-amz-content-sha256'] = $payloadHash; + $signature = hash_hmac('sha256', $stringToSignStr, $kSigning); - return $resultHeaders; + return $algorithm . ' ' . implode(',', array( + 'Credential=' . self::$__accessKey . '/' . implode('/', $credentialScope), + 'SignedHeaders=' . implode(';', array_keys($combinedHeaders)), + 'Signature=' . $signature, + )); } + /** + * Sort compare for meta headers + * + * @internal Used to sort x-amz meta headers + * @param string $a String A + * @param string $b String B + * @return integer + */ + private static function __sortMetaHeadersCmp($a, $b) + { + $lenA = strlen($a); + $lenB = strlen($b); + $minLen = min($lenA, $lenB); + $ncmp = strncmp($a, $b, $minLen); + if ($lenA == $lenB) return $ncmp; + if (0 == $ncmp) return $lenA < $lenB ? -1 : 1; + return $ncmp; + } } /** @@ -2203,17 +2197,11 @@ final class S3Request */ function __construct($verb, $bucket = '', $uri = '', $endpoint = 's3.amazonaws.com') { - $this->endpoint = $endpoint; $this->verb = $verb; $this->bucket = $bucket; $this->uri = $uri !== '' ? '/'.str_replace('%2F', '/', rawurlencode($uri)) : '/'; - //if ($this->bucket !== '') - // $this->resource = '/'.$this->bucket.$this->uri; - //else - // $this->resource = $this->uri; - if ($this->bucket !== '') { if ($this->__dnsBucketName($this->bucket)) @@ -2223,6 +2211,7 @@ function __construct($verb, $bucket = '', $uri = '', $endpoint = 's3.amazonaws.c } else { + // Old format, deprecated by AWS - removal scheduled for September 30th, 2020 $this->headers['Host'] = $this->endpoint; $this->uri = $this->uri; if ($this->bucket !== '') $this->uri = '/'.$this->bucket.$this->uri; @@ -2310,8 +2299,6 @@ public function getResponse() } $url = (S3::$useSSL ? 'https://' : 'http://') . ($this->headers['Host'] !== '' ? $this->headers['Host'] : $this->endpoint) . $this->uri; - //var_dump('bucket: ' . $this->bucket, 'uri: ' . $this->uri, 'resource: ' . $this->resource, 'url: ' . $url); - // Basic setup $curl = curl_init(); curl_setopt($curl, CURLOPT_USERAGENT, 'S3/php'); @@ -2341,56 +2328,46 @@ public function getResponse() } // Headers - $headers = array(); $amz = array(); - foreach ($this->amzHeaders as $header => $value) - if (strlen($value) > 0) $headers[] = $header.': '.$value; - foreach ($this->headers as $header => $value) - if (strlen($value) > 0) $headers[] = $header.': '.$value; - - // Collect AMZ headers for signature - foreach ($this->amzHeaders as $header => $value) - if (strlen($value) > 0) $amz[] = strtolower($header).':'.$value; - - // AMZ headers must be sorted - if (sizeof($amz) > 0) - { - //sort($amz); - usort($amz, array(&$this, '__sortMetaHeadersCmp')); - $amz = "\n".implode("\n", $amz); - } else $amz = ''; - + $httpHeaders = array(); if (S3::hasAuth()) { // Authorization string (CloudFront stringToSign should only contain a date) if ($this->headers['Host'] == 'cloudfront.amazonaws.com') - $headers[] = 'Authorization: ' . S3::__getSignature($this->headers['Date']); + { + # TODO: Update CloudFront authentication + foreach ($this->amzHeaders as $header => $value) + if (strlen($value) > 0) $httpHeaders[] = $header.': '.$value; + + foreach ($this->headers as $header => $value) + if (strlen($value) > 0) $httpHeaders[] = $header.': '.$value; + + $httpHeaders[] = 'Authorization: ' . S3::__getSignature($this->headers['Date']); + } else { - if (S3::$signVer == 'v2') - { - $headers[] = 'Authorization: ' . S3::__getSignature( - $this->verb."\n". - $this->headers['Content-MD5']."\n". - $this->headers['Content-Type']."\n". - $this->headers['Date'].$amz."\n". - $this->resource - ); - } else { - $amzHeaders = S3::__getSignatureV4( - $this->amzHeaders, - $this->headers, - $this->verb, - $this->uri, - $this->data - ); - foreach ($amzHeaders as $k => $v) { - $headers[] = $k .': '. $v; - } - } + $this->amzHeaders['x-amz-date'] = gmdate('Ymd\THis\Z'); + + if (!isset($this->amzHeaders['x-amz-content-sha256'])) + $this->amzHeaders['x-amz-content-sha256'] = hash('sha256', $this->data); + + foreach ($this->amzHeaders as $header => $value) + if (strlen($value) > 0) $httpHeaders[] = $header.': '.$value; + + foreach ($this->headers as $header => $value) + if (strlen($value) > 0) $httpHeaders[] = $header.': '.$value; + + $httpHeaders[] = 'Authorization: ' . S3::__getSignatureV4( + $this->amzHeaders, + $this->headers, + $this->verb, + $this->uri, + $this->parameters + ); + } } - curl_setopt($curl, CURLOPT_HTTPHEADER, $headers); + curl_setopt($curl, CURLOPT_HTTPHEADER, $httpHeaders); curl_setopt($curl, CURLOPT_HEADER, false); curl_setopt($curl, CURLOPT_RETURNTRANSFER, false); curl_setopt($curl, CURLOPT_WRITEFUNCTION, array(&$this, '__responseWriteCallback')); @@ -2427,6 +2404,12 @@ public function getResponse() default: break; } + // set curl progress function callback + if (S3::$progressFunction) { + curl_setopt($curl, CURLOPT_NOPROGRESS, false); + curl_setopt($curl, CURLOPT_PROGRESSFUNCTION, S3::$progressFunction); + } + // Execute, grab errors if (curl_exec($curl)) $this->response->code = curl_getinfo($curl, CURLINFO_HTTP_CODE); @@ -2465,24 +2448,6 @@ public function getResponse() return $this->response; } - /** - * Sort compare for meta headers - * - * @internal Used to sort x-amz meta headers - * @param string $a String A - * @param string $b String B - * @return integer - */ - private function __sortMetaHeadersCmp($a, $b) - { - $lenA = strpos($a, ':'); - $lenB = strpos($b, ':'); - $minLen = min($lenA, $lenB); - $ncmp = strncmp($a, $b, $minLen); - if ($lenA == $lenB) return $ncmp; - if (0 == $ncmp) return $lenA < $lenB ? -1 : 1; - return $ncmp; - } /** * CURL write callback From cfaab915a3de530fbb8563174edddf03bead66a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Donovan=20Sch=C3=B6nknecht?= Date: Sun, 5 May 2019 21:03:40 +0200 Subject: [PATCH 46/49] Remove function from README --- README.md | 9 --------- 1 file changed, 9 deletions(-) diff --git a/README.md b/README.md index d7682248..636005bd 100644 --- a/README.md +++ b/README.md @@ -14,15 +14,6 @@ Statically (e,g; S3::getObject(...)): S3::setAuth($awsAccessKey, $awsSecretKey); ``` -### AWS V4 Signature - -```php -$s3->setSignatureVersion('v4); -``` -Or statically -```php -S3::setSignatureVersion('v4'); -``` ### Object Operations From bf8abcba855a98d156ba4c4b0f8368de18e65409 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Donovan=20Sch=C3=B6nknecht?= Date: Sun, 5 May 2019 21:04:08 +0200 Subject: [PATCH 47/49] Remove function from example --- example.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/example.php b/example.php index 664a3f87..d6c90dd6 100755 --- a/example.php +++ b/example.php @@ -35,9 +35,6 @@ // Instantiate the class $s3 = new S3(awsAccessKey, awsSecretKey); -// using v4 signature -$s3->setSignatureVersion('v4'); - // List your buckets: echo "S3::listBuckets(): ".print_r($s3->listBuckets(), 1)."\n"; From 221ad7f7f87131bd3367b3a56f3d9649deddd92c Mon Sep 17 00:00:00 2001 From: Luca Tacconi Date: Tue, 4 Feb 2020 09:28:37 +0100 Subject: [PATCH 48/49] {} is deprecated, used [] instead Accessing string characters with {} is deprecated, use [] instead --- S3.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/S3.php b/S3.php index 60d08ae5..b10d441e 100644 --- a/S3.php +++ b/S3.php @@ -2511,7 +2511,7 @@ private function __responseHeaderCallback($curl, $data) elseif ($header == 'content-type') $this->response->headers['type'] = $value; elseif ($header == 'etag') - $this->response->headers['hash'] = $value{0} == '"' ? substr($value, 1, -1) : $value; + $this->response->headers['hash'] = $value[0] == '"' ? substr($value, 1, -1) : $value; elseif (preg_match('/^x-amz-meta-.*$/', $header)) $this->response->headers[$header] = $value; } From 2c1285d449ca7250091ba3517c2f15960286c5dc Mon Sep 17 00:00:00 2001 From: Andrew Lyle Date: Thu, 22 Feb 2024 10:53:34 -0500 Subject: [PATCH 49/49] Update S3.php fix deprecated issue in php 8.3.x "Deprecated: Use of "self" in callables is deprecated" --- S3.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/S3.php b/S3.php index b10d441e..076b4ad3 100644 --- a/S3.php +++ b/S3.php @@ -2007,11 +2007,11 @@ public static function __getSignatureV4($amzHeaders, $headers, $method, $uri, $p $combinedHeaders[strtolower($k)] = trim($v); foreach ($amzHeaders as $k => $v) $combinedHeaders[strtolower($k)] = trim($v); - uksort($combinedHeaders, array('self', '__sortMetaHeadersCmp')); + uksort($combinedHeaders, array(self::class, '__sortMetaHeadersCmp')); // Convert null query string parameters to strings and sort $parameters = array_map('strval', $parameters); - uksort($parameters, array('self', '__sortMetaHeadersCmp')); + uksort($parameters, array(self::class, '__sortMetaHeadersCmp')); $queryString = http_build_query($parameters, null, '&', PHP_QUERY_RFC3986); // Payload