diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..d115c339 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,9 @@ +/README.md export-ignore +/example.php export-ignore +/example-cloudfront.php export-ignore +/example-form.php export-ignore +/example-wrapper.php export-ignore +/example-helpers.php export-ignore +/example-custom-endpoint.php export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore diff --git a/.gitignore b/.gitignore index 48b8bf90..467da90d 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ vendor/ +/composer.lock +/composer.phar diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..f97f3277 --- /dev/null +++ b/LICENSE @@ -0,0 +1,26 @@ +BSD 2-Clause License + +Copyright (c) 2013, Donovan Schönknecht +Copyright (c) 2019, Red Matter +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/S3.php b/S3.php index 60d08ae5..e39d5786 100644 --- a/S3.php +++ b/S3.php @@ -28,6 +28,30 @@ * Amazon S3 is a trademark of Amazon.com, Inc. or its affiliates. */ +// Add curl constants that are missing till PHP v5.5 +defined('CURL_SSLVERSION_DEFAULT') || define('CURL_SSLVERSION_DEFAULT', 0); +defined('CURL_SSLVERSION_TLSv1') || define('CURL_SSLVERSION_TLSv1', 1); +defined('CURL_SSLVERSION_SSLv2') || define('CURL_SSLVERSION_SSLv2', 2); +defined('CURL_SSLVERSION_SSLv3') || define('CURL_SSLVERSION_SSLv3', 3); + +if (getenv('SIGNATURE_DEBUG') === 'ON') { + function SigDebug($format) + { + $args = func_get_args(); + if (count($args) === 0) { + return; + } + + $args[0] .= "\n"; + array_unshift($args, STDERR); + call_user_func_array('fprintf', $args); + } +} else { + function SigDebug() + {} +} + + /** * Amazon S3 PHP class * @@ -49,49 +73,30 @@ class S3 const SSE_NONE = ''; const SSE_AES256 = 'AES256'; - /** - * The AWS Access key - * - * @var string - * @access private - * @static - */ - private static $__accessKey = null; + const SigV2 = 'sigv2'; + const SigV4 = 'sigv4'; /** - * AWS Secret Key + * Default credentials to access AWS * - * @var string - * @access private - * @static + * @var S3Credentials|null */ - private static $__secretKey = null; + private static $__defaultCredentials; /** - * SSL Client key + * Default endpoint * - * @var string - * @access private - * @static + * @var S3EndpointConfig|null */ - private static $__sslKey = null; + private static $__defaultEndpoint; /** * Default delimiter to be used, for example while getBucket(). - * @var string + * @var string|null * @access public * @static */ - public static $defDelimiter = null; - - /** - * AWS URI - * - * @var string - * @acess public - * @static - */ - public static $endpoint = 's3.amazonaws.com'; + public static $defDelimiter; /** * AWS Region @@ -102,42 +107,6 @@ class S3 */ public static $region = ''; - /** - * 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 SSL version - * - * @var const - * @access public - * @static - */ - public static $useSSLVersion = CURL_SSLVERSION_TLSv1; - /** * Use PHP exceptions? * @@ -154,46 +123,19 @@ class S3 */ private static $__timeOffset = 0; - /** - * 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; - /** * AWS Key Pair ID * - * @var string + * @var string|null * @access private * @static */ - private static $__signingKeyPairId = null; + private static $__signingKeyPairId; /** * Key resource, freeSigningKey() must be called to clear it from memory * - * @var bool + * @var resource|bool * @access private * @static */ @@ -202,40 +144,86 @@ class S3 /** * CURL progress function callback * - * @var function + * @var callable|null * @access public * @static */ - public static $progressFunction = null; + public static $progressFunction; /** - * Constructor - if you're not using the class statically - * - * @param string $accessKey Access key - * @param string $secretKey Secret key - * @param boolean $useSSL Enable SSL - * @param string $endpoint Amazon URI - * @return void - */ + * Constructor + * + * @param string $accessKey Access key + * @param string $secretKey Secret key + * @param boolean $useSSL Enable SSL + * @param string $endpoint Amazon URI + * @param string $region AWS auth region for SigV4 + * + * @deprecated Use static initializer S3::Init() instead + */ public function __construct($accessKey = null, $secretKey = null, $useSSL = false, $endpoint = 's3.amazonaws.com', $region = '') { + $creds = null; if ($accessKey !== null && $secretKey !== null) - self::setAuth($accessKey, $secretKey); - self::$useSSL = $useSSL; - self::$endpoint = $endpoint; + { + $creds = new S3Credentials($accessKey, $secretKey); + } + + $endpointCfg = new S3EndpointConfig($endpoint); + $endpointCfg->withSSLEnabled($useSSL); + + self::Init($creds, $region, $endpointCfg); + } + + /** + * Initialise default parameters + * + * @param S3Credentials $credentials Default credentials + * @param string $region Auth region for SigV4 + * @param S3EndpointConfig|null $endpoint Endpoint configuration, null for AWS S3 settings + */ + public static function Init(S3Credentials $credentials, $region = '', S3EndpointConfig $endpoint = null) + { + self::setCredentials($credentials); + self::$region = $region; + + if ($endpoint === null) { + $endpoint = new S3EndpointConfig(); + } + + self::setEndpoint($endpoint); + } + + /** + * Endpoint override helper + * + * @param S3EndpointConfig|null $endpoint + * @return S3EndpointConfig + */ + public static function getEndpoint(S3EndpointConfig $endpoint = null) + { + if ($endpoint !== null) { + return $endpoint; + } + + if (self::$__defaultEndpoint === null) { + self::$__defaultEndpoint = new S3EndpointConfig(); + } + + return self::$__defaultEndpoint; } /** - * Set the service endpoint - * - * @param string $host Hostname - * @return void - */ - public function setEndpoint($host) + * Set the service endpoint + * + * @param S3EndpointConfig $endpoint + * @return void + */ + public static function setEndpoint(S3EndpointConfig $endpoint) { - self::$endpoint = $host; + self::$__defaultEndpoint = $endpoint; } @@ -252,26 +240,22 @@ public function setRegion($region) /** - * Get the service region - * - * @return string $region - * @static - */ - public static function getRegion() + * Get the service region + * + * @param S3EndpointConfig|null $endpoint + * @return string + * @static + */ + public static function getRegion(S3EndpointConfig $endpoint = null) { - $region = self::$region; - - // parse region from endpoint if not specific - if (empty($region)) + if (!empty(self::$region)) { - if (preg_match("/s3[.-](?:website-|dualstack\.)?(.+)\.amazonaws\.com/i", self::$endpoint, $match) !== 0 - && strtolower($match[1]) !== "external-1") - { - $region = $match[1]; - } + return self::$region; } - return empty($region) ? 'us-east-1' : $region; + // deduce region from default endpoint + return self::getEndpoint($endpoint) + ->getRegion(); } @@ -284,8 +268,18 @@ public static function getRegion() */ public static function setAuth($accessKey, $secretKey) { - self::$__accessKey = $accessKey; - self::$__secretKey = $secretKey; + self::setCredentials(new S3Credentials($accessKey, $secretKey)); + } + + + /** + * Set default credentials + * + * @param S3Credentials $creds + */ + public static function setCredentials(S3Credentials $creds) + { + self::$__defaultCredentials = $creds; } @@ -295,7 +289,27 @@ public static function setAuth($accessKey, $secretKey) * @return boolean */ public static function hasAuth() { - return (self::$__accessKey !== null && self::$__secretKey !== null); + return self::$__defaultCredentials !== null && self::$__defaultCredentials->isInitialised(); + } + + + /** + * Get access-key if set, otherwise null + * + * @param S3Credentials|null $creds + * @return S3Credentials|null + */ + public static function getCredentials(S3Credentials $creds = null) + { + if ($creds !== null && $creds->isInitialised()) { + return $creds; + } + + if (self::hasAuth()) { + return self::$__defaultCredentials; + } + + return null; } @@ -308,8 +322,9 @@ public static function hasAuth() { */ public static function setSSL($enabled, $validate = true) { - self::$useSSL = $enabled; - self::$useSSLValidation = $validate; + self::getEndpoint() + ->withSSLEnabled($enabled) + ->withSSLValidationEnabled($validate); } @@ -323,9 +338,10 @@ public static function setSSL($enabled, $validate = true) */ public static function setSSLAuth($sslCert = null, $sslKey = null, $sslCACert = null) { - self::$sslCert = $sslCert; - self::$sslKey = $sslKey; - self::$sslCACert = $sslCACert; + self::getEndpoint() + ->withSSLEnabled() + ->withSSLValidationEnabled() + ->withSSLAuth($sslCert, $sslKey, $sslCACert); } @@ -335,12 +351,13 @@ public static function setSSLAuth($sslCert = null, $sslKey = null, $sslCACert = * @param string $host Proxy hostname and port (localhost:1234) * @param string $user Proxy username * @param string $pass Proxy password - * @param constant $type CURL proxy type + * @param int $type CURL proxy type (constants defined in curl module) * @return void */ public static function setProxy($host, $user = null, $pass = null, $type = CURLPROXY_SOCKS5) { - self::$proxy = array('host' => $host, 'type' => $type, 'user' => $user, 'pass' => $pass); + self::getEndpoint() + ->withProxy($host, $user, $pass, $type); } @@ -357,20 +374,21 @@ 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) + * 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 int $offset Time offset (set to zero to use AWS server time) + * @param S3EndpointConfig|null $endpoint + * @return void + */ + public static function setTimeCorrectionOffset($offset = 0, S3EndpointConfig $endpoint = null) { - if ($offset == 0) + if ($offset === 0) { - $rest = new S3Request('HEAD'); + $rest = new S3Request('HEAD', null, '', $endpoint); $rest = $rest->getResponse(); $awstime = $rest->headers['date']; $systime = time(); @@ -381,20 +399,38 @@ public static function setTimeCorrectionOffset($offset = 0) /** - * Set signing key - * - * @param string $keyPairId AWS Key Pair ID - * @param string $signingKey Private Key - * @param boolean $isFile Load private key from file, set to false to load string - * @return boolean - */ + * Set signing key + * + * @param string $keyPairId AWS Key Pair ID + * @param string $signingKey Private Key + * @param boolean $isFile Load private key from file, set to false to load string + * @return boolean + */ public static function setSigningKey($keyPairId, $signingKey, $isFile = true) { self::$__signingKeyPairId = $keyPairId; - if ((self::$__signingKeyResource = openssl_pkey_get_private($isFile ? - file_get_contents($signingKey) : $signingKey)) !== false) return true; - self::__triggerError('S3::setSigningKey(): Unable to open load private key: '.$signingKey, __FILE__, __LINE__); - return false; + + if ($isFile) + { + $signingKeyMaterial = file_get_contents($signingKey); + } + else + { + $signingKeyMaterial = $signingKey; + } + + if ($signingKeyMaterial !== false) + { + self::$__signingKeyResource = openssl_pkey_get_private($signingKeyMaterial); + } + + if ($signingKeyMaterial === false || self::$__signingKeyResource === false) + { + self::__triggerError('S3::setSigningKey(): Unable to open load private key: ' . $signingKey, __FILE__, __LINE__); + return false; + } + + return true; } @@ -407,13 +443,15 @@ public static function setSigningKey($keyPairId, $signingKey, $isFile = true) public static function freeSigningKey() { if (self::$__signingKeyResource !== false) + { openssl_free_key(self::$__signingKeyResource); + } } /** * Set progress function * - * @param function $func Progress function + * @param callable $func Progress function * @return void */ public static function setProgressFunction($func = null) @@ -423,40 +461,46 @@ public static function setProgressFunction($func = null) /** - * Internal error handler - * - * @internal Internal error handler - * @param string $message Error message - * @param string $file Filename - * @param integer $line Line number - * @param integer $code Error code - * @return void - */ + * Internal error handler + * + * @param string $message Error message + * @param string $file Filename + * @param integer $line Line number + * @param integer $code Error code + * @return void + * @internal Internal error handler + * + * @throws null + */ private static function __triggerError($message, $file, $line, $code = 0) { if (self::$useExceptions) + { throw new S3Exception($message, $file, $line, $code); - else - trigger_error($message, E_USER_WARNING); + } + + trigger_error($message, E_USER_WARNING); } /** - * Get a list of buckets - * - * @param boolean $detailed Returns detailed bucket list when true - * @return array | false - */ - public static function listBuckets($detailed = false) + * Get a list of buckets + * + * @param boolean $detailed Returns detailed bucket list when true + * @param S3EndpointConfig|null $endpoint + * @param S3Credentials|null $creds + * @return array | false + */ + public static function listBuckets($detailed = false, S3EndpointConfig $endpoint = null, S3Credentials $creds = null) { - $rest = new S3Request('GET', '', '', self::$endpoint); + $rest = new S3Request('GET', null, '', $endpoint, $creds); $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::listBuckets(): [%s] %s", $rest->error['code'], - $rest->error['message']), __FILE__, __LINE__); + $rest->error['message']), __FILE__, __LINE__); return false; } $results = array(); @@ -481,22 +525,26 @@ public static function listBuckets($detailed = false) /** - * Get contents for a bucket - * - * If maxKeys is null this method will loop through truncated result sets - * - * @param string $bucket Bucket name - * @param string $prefix Prefix - * @param string $marker Marker (last file listed) - * @param string $maxKeys Max keys (maximum number of keys to return) - * @param string $delimiter Delimiter - * @param boolean $returnCommonPrefixes Set to true to return CommonPrefixes - * @return array | false - */ - public static function getBucket($bucket, $prefix = null, $marker = null, $maxKeys = null, $delimiter = null, $returnCommonPrefixes = false) + * Get contents for a bucket + * + * If maxKeys is null this method will loop through truncated result sets + * + * @param string|S3BucketConfig $bucket Bucket name + * @param string $prefix Prefix + * @param string $marker Marker (last file listed) + * @param string $maxKeys Max keys (maximum number of keys to return) + * @param string $delimiter Delimiter + * @param boolean $returnCommonPrefixes Set to true to return CommonPrefixes + * @param S3EndpointConfig|null $endpoint + * @param S3Credentials|null $creds + * @return array | false + */ + public static function getBucket($bucket, $prefix = null, $marker = null, $maxKeys = null, $delimiter = null, + $returnCommonPrefixes = false, + S3EndpointConfig $endpoint = null, S3Credentials $creds = null) { - $rest = new S3Request('GET', $bucket, '', self::$endpoint); - if ($maxKeys == 0) $maxKeys = null; + $rest = new S3Request('GET', $bucket, '', $endpoint, $creds); + if ($maxKeys === 0) $maxKeys = null; if ($prefix !== null && $prefix !== '') $rest->setParameter('prefix', $prefix); if ($marker !== null && $marker !== '') $rest->setParameter('marker', $marker); if ($maxKeys !== null && $maxKeys !== '') $rest->setParameter('max-keys', $maxKeys); @@ -508,7 +556,7 @@ public static function getBucket($bucket, $prefix = null, $marker = null, $maxKe if ($response->error !== false) { self::__triggerError(sprintf("S3::getBucket(): [%s] %s", - $response->error['code'], $response->error['message']), __FILE__, __LINE__); + $response->error['code'], $response->error['message']), __FILE__, __LINE__); return false; } @@ -532,21 +580,21 @@ public static function getBucket($bucket, $prefix = null, $marker = null, $maxKe $results[(string)$c->Prefix] = array('prefix' => (string)$c->Prefix); if (isset($response->body, $response->body->IsTruncated) && - (string)$response->body->IsTruncated == 'false') return $results; + (string)$response->body->IsTruncated === 'false') return $results; if (isset($response->body, $response->body->NextMarker)) $nextMarker = (string)$response->body->NextMarker; // Loop through truncated results if maxKeys isn't specified - if ($maxKeys == null && $nextMarker !== null && (string)$response->body->IsTruncated == 'true') + if ($maxKeys === null && $nextMarker !== null && (string)$response->body->IsTruncated === 'true') do { - $rest = new S3Request('GET', $bucket, '', self::$endpoint); + $rest = new S3Request('GET', $bucket, '', $endpoint, $creds); if ($prefix !== null && $prefix !== '') $rest->setParameter('prefix', $prefix); $rest->setParameter('marker', $nextMarker); if ($delimiter !== null && $delimiter !== '') $rest->setParameter('delimiter', $delimiter); - if (($response = $rest->getResponse()) == 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) @@ -567,68 +615,92 @@ public static function getBucket($bucket, $prefix = null, $marker = null, $maxKe if (isset($response->body, $response->body->NextMarker)) $nextMarker = (string)$response->body->NextMarker; - } while ($response !== false && (string)$response->body->IsTruncated == 'true'); + } while ($response !== false && (string)$response->body->IsTruncated === 'true'); return $results; } /** - * Put a bucket - * - * @param string $bucket Bucket name - * @param constant $acl ACL flag - * @param string $location Set as "EU" to create buckets hosted in Europe - * @return boolean - */ - public static function putBucket($bucket, $acl = self::ACL_PRIVATE, $location = false) + * Put a bucket + * + * @param string $bucket Bucket name + * @param string $acl ACL flag + * @param string|bool $location Set as "EU" to create buckets hosted in Europe + * @param S3EndpointConfig|null $endpoint + * @param S3Credentials|null $creds + * @return boolean + */ + public static function putBucket($bucket, $acl = self::ACL_PRIVATE, $location = false, S3EndpointConfig $endpoint = null, S3Credentials $creds = null) { - $rest = new S3Request('PUT', $bucket, '', self::$endpoint); + $endpoint = self::getEndpoint($endpoint); + + if ($location === false) + { + $location = self::getRegion($endpoint); + if (empty($location)) + { + self::__triggerError("S3::putBucket({$bucket}, {$acl}, {$location}): Could not deduce region-code", __FILE__, __LINE__); + return false; + } + } + + $rest = new S3Request('PUT', new S3BucketConfig($bucket, $endpoint->defaultRegion), '', $endpoint, $creds); $rest->setAmzHeader('x-amz-acl', $acl); - if ($location === false) $location = self::getRegion(); + if ($endpoint->hostname !== S3EndpointConfig::AWS_S3_DEFAULT_HOST + || $location !== S3EndpointConfig::AWS_S3_DEFAULT_REGION) { - if ($location !== false && $location !== "us-east-1") - { $dom = new DOMDocument; $createBucketConfiguration = $dom->createElement('CreateBucketConfiguration'); $locationConstraint = $dom->createElement('LocationConstraint', $location); $createBucketConfiguration->appendChild($locationConstraint); $dom->appendChild($createBucketConfiguration); + $rest->data = $dom->saveXML(); $rest->size = strlen($rest->data); $rest->setHeader('Content-Type', 'application/xml'); + } else { + $rest->data = ''; + $rest->size = 0; } + $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::putBucket({$bucket}, {$acl}, {$location}): [%s] %s", - $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); + $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); return false; } + return true; } /** - * Delete an empty bucket - * - * @param string $bucket Bucket name - * @return boolean - */ - public static function deleteBucket($bucket) + * Delete an empty bucket + * + * @param string|S3BucketConfig $bucket Bucket name + * @param S3EndpointConfig|null $endpoint + * @param S3Credentials|null $creds + * @return boolean + */ + public static function deleteBucket($bucket, S3EndpointConfig $endpoint = null, S3Credentials $creds = null) { - $rest = new S3Request('DELETE', $bucket, '', self::$endpoint); + $rest = new S3Request('DELETE', $bucket, '', $endpoint, $creds); $rest = $rest->getResponse(); if ($rest->error === false && $rest->code !== 204) $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); if ($rest->error !== false) { self::__triggerError(sprintf("S3::deleteBucket({$bucket}): [%s] %s", - $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); + $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); return false; } return true; @@ -636,12 +708,12 @@ public static function deleteBucket($bucket) /** - * Create input info array for putObject() - * - * @param string $file Input file - * @param mixed $md5sum Use MD5 hash (supply a string if you want to use your own) - * @return array | false - */ + * Create input info array for putObject() + * + * @param string $file Input file + * @param mixed $md5sum Use MD5 hash (supply a string if you want to use your own) + * @return array | false + */ public static function inputFile($file, $md5sum = true) { if (!file_exists($file) || !is_file($file) || !is_readable($file)) @@ -650,19 +722,30 @@ public static function inputFile($file, $md5sum = true) 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))) : '', 'sha256sum' => hash_file('sha256', $file)); + + if ($md5sum === false) { + $md5sum = ''; + } elseif (!is_string($md5sum)) { + $md5sum = base64_encode(md5_file($file, true)); + } + + return array( + 'file' => $file, + 'size' => filesize($file), + 'md5sum' => $md5sum, + 'sha256sum' => hash_file('sha256', $file) + ); } /** - * Create input array info for putObject() with a resource - * - * @param string $resource Input resource to read from - * @param integer $bufferSize Input byte size - * @param string $md5sum MD5 hash to send (optional) - * @return array | false - */ + * Create input array info for putObject() with a resource + * + * @param resource $resource Input resource to read from + * @param int|bool $bufferSize Input byte size + * @param string $md5sum MD5 hash to send (optional) + * @return array | false + */ public static function inputResource(&$resource, $bufferSize = false, $md5sum = '') { if (!is_resource($resource) || (int)$bufferSize < 0) @@ -689,22 +772,24 @@ public static function inputResource(&$resource, $bufferSize = false, $md5sum = /** - * Put an object - * - * @param mixed $input Input data - * @param string $bucket Bucket name - * @param string $uri Object URI - * @param constant $acl ACL constant - * @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, $serverSideEncryption = self::SSE_NONE) + * Put an object + * + * @param mixed $input Input data + * @param string|S3BucketConfig $bucket Bucket name + * @param string $uri Object URI + * @param string $acl ACL constant + * @param array $metaHeaders Array of x-amz-meta-* headers + * @param array|string $requestHeaders Array of request headers or content type as a string + * @param string $storageClass Storage class constant + * @param string $serverSideEncryption Server-side encryption + * @param S3EndpointConfig|null $endpoint + * @param S3Credentials|null $creds + * @return boolean + */ + public static function putObject($input, $bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $requestHeaders = array(), $storageClass = self::STORAGE_CLASS_STANDARD, $serverSideEncryption = self::SSE_NONE, S3EndpointConfig $endpoint = null, S3Credentials $creds = null) { if ($input === false) return false; - $rest = new S3Request('PUT', $bucket, $uri, self::$endpoint); + $rest = new S3Request('PUT', $bucket, $uri, $endpoint, $creds); if (!is_array($input)) $input = array( 'data' => $input, 'size' => strlen($input), @@ -723,14 +808,12 @@ public static function putObject($input, $bucket, $uri, $acl = self::ACL_PRIVATE // Content-Length (required) if (isset($input['size']) && $input['size'] >= 0) $rest->size = $input['size']; - else { - if (isset($input['file'])) { - clearstatcache(false, $input['file']); - $rest->size = filesize($input['file']); - } - elseif (isset($input['data'])) - $rest->size = strlen($input['data']); + else if (isset($input['file'])) { + clearstatcache(false, $input['file']); + $rest->size = filesize($input['file']); } + elseif (isset($input['data'])) + $rest->size = strlen($input['data']); // Custom request headers (Content-Type, Content-Disposition, Content-Encoding) if (is_array($requestHeaders)) @@ -775,7 +858,7 @@ public static function putObject($input, $bucket, $uri, $acl = self::ACL_PRIVATE if ($rest->response->error !== false) { self::__triggerError(sprintf("S3::putObject(): [%s] %s", - $rest->response->error['code'], $rest->response->error['message']), __FILE__, __LINE__); + $rest->response->error['code'], $rest->response->error['message']), __FILE__, __LINE__); return false; } return true; @@ -783,59 +866,72 @@ public static function putObject($input, $bucket, $uri, $acl = self::ACL_PRIVATE /** - * Put an object from a file (legacy function) - * - * @param string $file Input file path - * @param string $bucket Bucket name - * @param string $uri Object URI - * @param constant $acl ACL constant - * @param array $metaHeaders Array of x-amz-meta-* headers - * @param string $contentType Content type - * @return boolean - */ - public static function putObjectFile($file, $bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $contentType = null) + * Put an object from a file (legacy function) + * + * @param string $file Input file path + * @param string|S3BucketConfig $bucket Bucket name + * @param string $uri Object URI + * @param string $acl ACL constant + * @param array $metaHeaders Array of x-amz-meta-* headers + * @param string $contentType Content type + * @param string $storageClass + * @param string $serverSideEncryption + * @param S3EndpointConfig|null $endpoint + * @param S3Credentials|null $creds + * @return boolean + */ + public static function putObjectFile( + $file, $bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $contentType = null, + $storageClass = self::STORAGE_CLASS_STANDARD, $serverSideEncryption = self::SSE_NONE, + S3EndpointConfig $endpoint = null, S3Credentials $creds = null) { - return self::putObject(self::inputFile($file), $bucket, $uri, $acl, $metaHeaders, $contentType); + return self::putObject(self::inputFile($file), $bucket, $uri, $acl, $metaHeaders, $contentType, $storageClass, $serverSideEncryption, $endpoint, $creds); } /** - * Put an object from a string (legacy function) - * - * @param string $string Input data - * @param string $bucket Bucket name - * @param string $uri Object URI - * @param constant $acl ACL constant - * @param array $metaHeaders Array of x-amz-meta-* headers - * @param string $contentType Content type - * @return boolean - */ - public static function putObjectString($string, $bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $contentType = 'text/plain') + * Put an object from a string (legacy function) + * + * @param string $string Input data + * @param string|S3BucketConfig $bucket Bucket name + * @param string $uri Object URI + * @param string $acl ACL constant + * @param array $metaHeaders Array of x-amz-meta-* headers + * @param string $contentType Content type + * @param string $storageClass + * @param string $serverSideEncryption + * @param S3EndpointConfig|null $endpoint + * @param S3Credentials|null $creds + * @return boolean + */ + public static function putObjectString( + $string, $bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $contentType = 'text/plain', + $storageClass = self::STORAGE_CLASS_STANDARD, $serverSideEncryption = self::SSE_NONE, + S3EndpointConfig $endpoint = null, S3Credentials $creds = null) { - return self::putObject($string, $bucket, $uri, $acl, $metaHeaders, $contentType); + return self::putObject($string, $bucket, $uri, $acl, $metaHeaders, $contentType, $storageClass, $serverSideEncryption, $endpoint, $creds); } /** - * Get an object - * - * @param string $bucket Bucket name - * @param string $uri Object URI - * @param mixed $saveTo Filename or resource to write to - * @return mixed - */ - public static function getObject($bucket, $uri, $saveTo = false) + * Get an object + * + * @param string|S3BucketConfig $bucket Bucket name + * @param string $uri Object URI + * @param mixed $saveTo Filename or resource to write to + * @param S3EndpointConfig|null $endpoint + * @param S3Credentials|null $creds + * @return mixed + */ + public static function getObject($bucket, $uri, $saveTo = false, S3EndpointConfig $endpoint = null, S3Credentials $creds = null) { - $rest = new S3Request('GET', $bucket, $uri, self::$endpoint); + $rest = new S3Request('GET', $bucket, $uri, $endpoint, $creds); if ($saveTo !== false) { if (is_resource($saveTo)) $rest->fp =& $saveTo; - else - if (($rest->fp = @fopen($saveTo, 'wb')) !== false) - $rest->file = realpath($saveTo); - else - $rest->response->error = array('code' => 0, 'message' => 'Unable to open save file for writing: '.$saveTo); + else if (($rest->fp = @fopen($saveTo, 'wb')) === false) + $rest->response->error = array('code' => 0, 'message' => 'Unable to open save file for writing: ' . $saveTo); } if ($rest->response->error === false) $rest->getResponse(); @@ -844,7 +940,7 @@ public static function getObject($bucket, $uri, $saveTo = false) if ($rest->response->error !== false) { self::__triggerError(sprintf("S3::getObject({$bucket}, {$uri}): [%s] %s", - $rest->response->error['code'], $rest->response->error['message']), __FILE__, __LINE__); + $rest->response->error['code'], $rest->response->error['message']), __FILE__, __LINE__); return false; } return $rest->response; @@ -852,45 +948,55 @@ public static function getObject($bucket, $uri, $saveTo = false) /** - * Get object information - * - * @param string $bucket Bucket name - * @param string $uri Object URI - * @param boolean $returnInfo Return response information - * @return mixed | false - */ - public static function getObjectInfo($bucket, $uri, $returnInfo = true) + * Get object information + * + * @param string|S3BucketConfig $bucket Bucket name + * @param string $uri Object URI + * @param boolean $returnInfo Return response information + * @param S3EndpointConfig|null $endpoint + * @param S3Credentials|null $creds + * @return mixed | false + */ + public static function getObjectInfo($bucket, $uri, $returnInfo = true, S3EndpointConfig $endpoint = null, S3Credentials $creds = null) { - $rest = new S3Request('HEAD', $bucket, $uri, self::$endpoint); + $rest = new S3Request('HEAD', $bucket, $uri, $endpoint, $creds); $rest = $rest->getResponse(); if ($rest->error === false && ($rest->code !== 200 && $rest->code !== 404)) $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); if ($rest->error !== false) { self::__triggerError(sprintf("S3::getObjectInfo({$bucket}, {$uri}): [%s] %s", - $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); + $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); + return false; + } + + if ($rest->code !== 200) + { return false; } - return $rest->code == 200 ? $returnInfo ? $rest->headers : true : false; + + return $returnInfo ? $rest->headers : true; } /** - * Copy an object - * - * @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 - * @param array $metaHeaders Optional array of x-amz-meta-* headers - * @param array $requestHeaders Optional array of request headers (content type, disposition, etc.) - * @param constant $storageClass Storage class constant - * @return mixed | false - */ - public static function copyObject($srcBucket, $srcUri, $bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $requestHeaders = array(), $storageClass = self::STORAGE_CLASS_STANDARD) + * Copy an object + * + * @param string $srcBucket Source bucket name + * @param string $srcUri Source object URI + * @param string|S3BucketConfig $bucket Destination bucket name + * @param string $uri Destination object URI + * @param string $acl ACL constant + * @param array $metaHeaders Optional array of x-amz-meta-* headers + * @param array $requestHeaders Optional array of request headers (content type, disposition, etc.) + * @param string $storageClass Storage class constant + * @param S3EndpointConfig|null $endpoint + * @param S3Credentials|null $creds + * @return mixed | false + */ + public static function copyObject($srcBucket, $srcUri, $bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $requestHeaders = array(), $storageClass = self::STORAGE_CLASS_STANDARD, S3EndpointConfig $endpoint = null, S3Credentials $creds = null) { - $rest = new S3Request('PUT', $bucket, $uri, self::$endpoint); + $rest = new S3Request('PUT', $bucket, $uri, $endpoint, $creds); $rest->setHeader('Content-Length', 0); foreach ($requestHeaders as $h => $v) strpos($h, 'x-amz-') === 0 ? $rest->setAmzHeader($h, $v) : $rest->setHeader($h, $v); @@ -899,7 +1005,7 @@ public static function copyObject($srcBucket, $srcUri, $bucket, $uri, $acl = sel $rest->setAmzHeader('x-amz-storage-class', $storageClass); $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) + if (count($requestHeaders) > 0 || count($metaHeaders) > 0) $rest->setAmzHeader('x-amz-metadata-directive', 'REPLACE'); $rest = $rest->getResponse(); @@ -908,7 +1014,7 @@ public static function copyObject($srcBucket, $srcUri, $bucket, $uri, $acl = sel if ($rest->error !== false) { self::__triggerError(sprintf("S3::copyObject({$srcBucket}, {$srcUri}, {$bucket}, {$uri}): [%s] %s", - $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); + $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); return false; } return isset($rest->body->LastModified, $rest->body->ETag) ? array( @@ -919,15 +1025,17 @@ 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) + * Set up a bucket redirection + * + * @param string|S3BucketConfig $bucket Bucket name + * @param string $location Target host name + * @param S3EndpointConfig|null $endpoint + * @param S3Credentials|null $creds + * @return boolean + */ + public static function setBucketRedirect($bucket = NULL, $location = NULL, S3EndpointConfig $endpoint = null, S3Credentials $creds = null) { - $rest = new S3Request('PUT', $bucket, '', self::$endpoint); + $rest = new S3Request('PUT', $bucket, '', $endpoint, $creds); if( empty($bucket) || empty($location) ) { self::__triggerError("S3::setBucketRedirect({$bucket}, {$location}): Empty parameter.", __FILE__, __LINE__); @@ -952,7 +1060,7 @@ public static function setBucketRedirect($bucket = NULL, $location = NULL) if ($rest->error !== false) { self::__triggerError(sprintf("S3::setBucketRedirect({$bucket}, {$location}): [%s] %s", - $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); + $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); return false; } return true; @@ -960,26 +1068,28 @@ public static function setBucketRedirect($bucket = NULL, $location = NULL) /** - * Set logging for a bucket - * - * @param string $bucket Bucket name - * @param string $targetBucket Target bucket (where logs are stored) - * @param string $targetPrefix Log prefix (e,g; domain.com-) - * @return boolean - */ - public static function setBucketLogging($bucket, $targetBucket, $targetPrefix = null) + * Set logging for a bucket + * + * @param string|S3BucketConfig $bucket Bucket name + * @param string $targetBucket Target bucket (where logs are stored) + * @param string $targetPrefix Log prefix (e,g; domain.com-) + * @param S3EndpointConfig|null $endpoint + * @param S3Credentials|null $creds + * @return boolean + */ + public static function setBucketLogging($bucket, $targetBucket, $targetPrefix = null, S3EndpointConfig $endpoint = null, S3Credentials $creds = null) { // The S3 log delivery group has to be added to the target bucket's ACP - if ($targetBucket !== null && ($acp = self::getAccessControlPolicy($targetBucket, '')) !== false) + if ($targetBucket !== null && ($acp = self::getAccessControlPolicy($targetBucket)) !== false) { // Only add permissions to the target bucket when they do not exist $aclWriteSet = false; $aclReadSet = false; foreach ($acp['acl'] as $acl) - if ($acl['type'] == 'Group' && $acl['uri'] == 'http://acs.amazonaws.com/groups/s3/LogDelivery') + if ($acl['type'] === 'Group' && $acl['uri'] === 'http://acs.amazonaws.com/groups/s3/LogDelivery') { - if ($acl['permission'] == 'WRITE') $aclWriteSet = true; - elseif ($acl['permission'] == 'READ_ACP') $aclReadSet = true; + if ($acl['permission'] === 'WRITE') $aclWriteSet = true; + elseif ($acl['permission'] === 'READ_ACP') $aclReadSet = true; } if (!$aclWriteSet) $acp['acl'][] = array( 'type' => 'Group', 'uri' => 'http://acs.amazonaws.com/groups/s3/LogDelivery', 'permission' => 'WRITE' @@ -995,7 +1105,7 @@ public static function setBucketLogging($bucket, $targetBucket, $targetPrefix = $bucketLoggingStatus->setAttribute('xmlns', 'http://s3.amazonaws.com/doc/2006-03-01/'); if ($targetBucket !== null) { - if ($targetPrefix == null) $targetPrefix = $bucket . '-'; + if ($targetPrefix === null) $targetPrefix = $bucket . '-'; $loggingEnabled = $dom->createElement('LoggingEnabled'); $loggingEnabled->appendChild($dom->createElement('TargetBucket', $targetBucket)); $loggingEnabled->appendChild($dom->createElement('TargetPrefix', $targetPrefix)); @@ -1004,7 +1114,7 @@ public static function setBucketLogging($bucket, $targetBucket, $targetPrefix = } $dom->appendChild($bucketLoggingStatus); - $rest = new S3Request('PUT', $bucket, '', self::$endpoint); + $rest = new S3Request('PUT', $bucket, '', $endpoint, $creds); $rest->setParameter('logging', null); $rest->data = $dom->saveXML(); $rest->size = strlen($rest->data); @@ -1015,7 +1125,7 @@ public static function setBucketLogging($bucket, $targetBucket, $targetPrefix = if ($rest->error !== false) { self::__triggerError(sprintf("S3::setBucketLogging({$bucket}, {$targetBucket}): [%s] %s", - $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); + $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); return false; } return true; @@ -1023,17 +1133,19 @@ public static function setBucketLogging($bucket, $targetBucket, $targetPrefix = /** - * Get logging status for a bucket - * - * This will return false if logging is not enabled. - * Note: To enable logging, you also need to grant write access to the log group - * - * @param string $bucket Bucket name - * @return array | false - */ - public static function getBucketLogging($bucket) + * Get logging status for a bucket + * + * This will return false if logging is not enabled. + * Note: To enable logging, you also need to grant write access to the log group + * + * @param string|S3BucketConfig $bucket Bucket name + * @param S3EndpointConfig|null $endpoint + * @param S3Credentials|null $creds + * @return array | false + */ + public static function getBucketLogging($bucket, S3EndpointConfig $endpoint = null, S3Credentials $creds = null) { - $rest = new S3Request('GET', $bucket, '', self::$endpoint); + $rest = new S3Request('GET', $bucket, '', $endpoint, $creds); $rest->setParameter('logging', null); $rest = $rest->getResponse(); if ($rest->error === false && $rest->code !== 200) @@ -1041,7 +1153,7 @@ public static function getBucketLogging($bucket) if ($rest->error !== false) { self::__triggerError(sprintf("S3::getBucketLogging({$bucket}): [%s] %s", - $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); + $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); return false; } if (!isset($rest->body->LoggingEnabled)) return false; // No logging @@ -1053,26 +1165,34 @@ public static function getBucketLogging($bucket) /** - * Disable bucket logging - * - * @param string $bucket Bucket name - * @return boolean - */ - public static function disableBucketLogging($bucket) + * Disable bucket logging + * + * @param string|S3BucketConfig $bucket Bucket name + * @param S3EndpointConfig|null $endpoint + * @param S3Credentials|null $creds + * @return boolean + */ + public static function disableBucketLogging($bucket, S3EndpointConfig $endpoint = null, S3Credentials $creds = null) { - return self::setBucketLogging($bucket, null); + return self::setBucketLogging($bucket, null, null, $endpoint, $creds); } /** - * Get a bucket's location - * - * @param string $bucket Bucket name - * @return string | false - */ - public static function getBucketLocation($bucket) + * Get a bucket's location + * + * @param string $bucket Bucket name + * @param S3EndpointConfig|null $endpoint + * @param S3Credentials|null $creds + * @return string | false + */ + public static function getBucketLocation($bucket, S3EndpointConfig $endpoint = null, S3Credentials $creds = null) { - $rest = new S3Request('GET', $bucket, '', self::$endpoint); + // https://github.com/aws/aws-sdk-js/issues/462 + // Setting up any region other than 'us-east-1' + $bucketConfig = new S3BucketConfig($bucket, 'us-west-2'); + + $rest = new S3Request('GET', $bucketConfig, '', $endpoint, $creds); $rest->setParameter('location', null); $rest = $rest->getResponse(); if ($rest->error === false && $rest->code !== 200) @@ -1080,7 +1200,7 @@ public static function getBucketLocation($bucket) if ($rest->error !== false) { self::__triggerError(sprintf("S3::getBucketLocation({$bucket}): [%s] %s", - $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); + $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); return false; } return (isset($rest->body[0]) && (string)$rest->body[0] !== '') ? (string)$rest->body[0] : 'US'; @@ -1088,14 +1208,16 @@ public static function getBucketLocation($bucket) /** - * Set object or bucket Access Control Policy - * - * @param string $bucket Bucket name - * @param string $uri Object URI - * @param array $acp Access Control Policy Data (same as the data returned from getAccessControlPolicy) - * @return boolean - */ - public static function setAccessControlPolicy($bucket, $uri = '', $acp = array()) + * Set object or bucket Access Control Policy + * + * @param string|S3BucketConfig $bucket Bucket name + * @param string $uri Object URI + * @param array $acp Access Control Policy Data (same as the data returned from getAccessControlPolicy) + * @param S3EndpointConfig|null $endpoint + * @param S3Credentials|null $creds + * @return boolean + */ + public static function setAccessControlPolicy($bucket, $uri = '', $acp = array(), S3EndpointConfig $endpoint = null, S3Credentials $creds = null) { $dom = new DOMDocument; $dom->formatOutput = true; @@ -1123,7 +1245,7 @@ public static function setAccessControlPolicy($bucket, $uri = '', $acp = array() $grantee->setAttribute('xsi:type', 'AmazonCustomerByEmail'); $grantee->appendChild($dom->createElement('EmailAddress', $g['email'])); } - elseif ($g['type'] == 'Group') + elseif ($g['type'] === 'Group') { // Group $grantee->setAttribute('xsi:type', 'Group'); $grantee->appendChild($dom->createElement('URI', $g['uri'])); @@ -1136,7 +1258,7 @@ public static function setAccessControlPolicy($bucket, $uri = '', $acp = array() $accessControlPolicy->appendChild($accessControlList); $dom->appendChild($accessControlPolicy); - $rest = new S3Request('PUT', $bucket, $uri, self::$endpoint); + $rest = new S3Request('PUT', $bucket, $uri, $endpoint, $creds); $rest->setParameter('acl', null); $rest->data = $dom->saveXML(); $rest->size = strlen($rest->data); @@ -1147,7 +1269,7 @@ public static function setAccessControlPolicy($bucket, $uri = '', $acp = array() if ($rest->error !== false) { self::__triggerError(sprintf("S3::setAccessControlPolicy({$bucket}, {$uri}): [%s] %s", - $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); + $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); return false; } return true; @@ -1155,15 +1277,17 @@ public static function setAccessControlPolicy($bucket, $uri = '', $acp = array() /** - * Get object or bucket Access Control Policy - * - * @param string $bucket Bucket name - * @param string $uri Object URI - * @return mixed | false - */ - public static function getAccessControlPolicy($bucket, $uri = '') + * Get object or bucket Access Control Policy + * + * @param string $bucket Bucket name + * @param string $uri Object URI + * @param S3EndpointConfig|null $endpoint + * @param S3Credentials|null $creds + * @return mixed | false + */ + public static function getAccessControlPolicy($bucket, $uri = '', S3EndpointConfig $endpoint = null, S3Credentials $creds = null) { - $rest = new S3Request('GET', $bucket, $uri, self::$endpoint); + $rest = new S3Request('GET', $bucket, $uri, $endpoint, $creds); $rest->setParameter('acl', null); $rest = $rest->getResponse(); if ($rest->error === false && $rest->code !== 200) @@ -1171,7 +1295,7 @@ public static function getAccessControlPolicy($bucket, $uri = '') if ($rest->error !== false) { self::__triggerError(sprintf("S3::getAccessControlPolicy({$bucket}, {$uri}): [%s] %s", - $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); + $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); return false; } @@ -1216,22 +1340,24 @@ public static function getAccessControlPolicy($bucket, $uri = '') /** - * Delete an object - * - * @param string $bucket Bucket name - * @param string $uri Object URI - * @return boolean - */ - public static function deleteObject($bucket, $uri) + * Delete an object + * + * @param string $bucket Bucket name + * @param string $uri Object URI + * @param S3EndpointConfig|null $endpoint + * @param S3Credentials|null $creds + * @return boolean + */ + public static function deleteObject($bucket, $uri, S3EndpointConfig $endpoint = null, S3Credentials $creds = null) { - $rest = new S3Request('DELETE', $bucket, $uri, self::$endpoint); + $rest = new S3Request('DELETE', $bucket, $uri, $endpoint, $creds); $rest = $rest->getResponse(); if ($rest->error === false && $rest->code !== 204) $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); if ($rest->error !== false) { self::__triggerError(sprintf("S3::deleteObject(): [%s] %s", - $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); + $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); return false; } return true; @@ -1239,23 +1365,29 @@ public static function deleteObject($bucket, $uri) /** - * Get a query string authenticated URL - * - * @param string $bucket Bucket name - * @param string $uri Object URI - * @param integer $lifetime Lifetime in seconds - * @param boolean $hostBucket Use the bucket name as the hostname - * @param boolean $https Use HTTPS ($hostBucket should be false for SSL verification) - * @return string - */ + * Get a query string authenticated URL + * + * @param string $bucket Bucket name + * @param string $uri Object URI + * @param integer $lifetime Lifetime in seconds + * @param boolean $hostBucket Use the bucket name as the hostname + * @param boolean $https Use HTTPS ($hostBucket should be false for SSL verification) + * @return string|false + */ public static function getAuthenticatedURL($bucket, $uri, $lifetime, $hostBucket = false, $https = false) { + $creds = self::getCredentials(); + if ($creds === null) { + self::__triggerError("S3::getAuthenticatedURL({$bucket}, {$uri}, ...): No credentials set", __FILE__, __LINE__); + return false; + } + $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, - $hostBucket ? $bucket : self::$endpoint.'/'.$bucket, $uri, self::$__accessKey, $expires, - urlencode(self::__getHash("GET\n\n\n{$expires}\n/{$bucket}/{$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 : self::getEndpoint()->hostname . '/' . $bucket, $uri, $creds->accessKey, $expires, + urlencode(self::__getHash("GET\n\n\n{$expires}\n/{$bucket}/{$uri}", $creds->secretKey))); } @@ -1305,56 +1437,63 @@ public static function getSignedCannedURL($url, $lifetime) * * @param string $bucket Bucket name * @param string $uriPrefix Object URI prefix - * @param constant $acl ACL constant + * @param string $acl ACL constant * @param integer $lifetime Lifetime in seconds * @param integer $maxFileSize Maximum filesize in bytes (default 5MB) * @param string $successRedirect Redirect URL or 200 / 201 status code * @param array $amzHeaders Array of x-amz-meta-* headers * @param array $headers Array of request headers or content type as a string * @param boolean $flashVars Includes additional "Filename" variable posted by Flash - * @return object + * @return object|false */ - public static function getHttpUploadPostParams($bucket, $uriPrefix = '', $acl = self::ACL_PRIVATE, $lifetime = 3600, - $maxFileSize = 5242880, $successRedirect = "201", $amzHeaders = array(), $headers = array(), $flashVars = false) + public static function getHttpUploadPostParams( + $bucket, $uriPrefix = '', $acl = self::ACL_PRIVATE, $lifetime = 3600, $maxFileSize = 5242880, + $successRedirect = "201", $amzHeaders = array(), $headers = array(), $flashVars = false) { + $creds = self::getCredentials(); + if ($creds === null) { + self::__triggerError("S3::getHttpUploadPostParams({$bucket}, ...): No credentials set", __FILE__, __LINE__); + return false; + } + // Create policy object $policy = new stdClass; $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); + $obj = new stdClass; $obj->bucket = $bucket; $policy->conditions[] = $obj; + $obj = new stdClass; $obj->acl = $acl; $policy->conditions[] = $obj; $obj = new stdClass; // 200 for non-redirect uploads - if (is_numeric($successRedirect) && in_array((int)$successRedirect, array(200, 201))) + if (is_numeric($successRedirect) && in_array((int)$successRedirect, array(200, 201), true)) $obj->success_action_status = (string)$successRedirect; else // URL $obj->success_action_redirect = $successRedirect; - array_push($policy->conditions, $obj); + $policy->conditions[] = $obj; if ($acl !== self::ACL_PUBLIC_READ) - array_push($policy->conditions, array('eq', '$acl', $acl)); + $policy->conditions[] = array('eq', '$acl', $acl); - array_push($policy->conditions, array('starts-with', '$key', $uriPrefix)); - if ($flashVars) array_push($policy->conditions, array('starts-with', '$Filename', '')); + $policy->conditions[] = array('starts-with', '$key', $uriPrefix); + if ($flashVars) $policy->conditions[] = array('starts-with', '$Filename', ''); foreach (array_keys($headers) as $headerKey) - array_push($policy->conditions, array('starts-with', '$'.$headerKey, '')); + $policy->conditions[] = array('starts-with', '$' . $headerKey, ''); foreach ($amzHeaders as $headerKey => $headerVal) { $obj = new stdClass; $obj->{$headerKey} = (string)$headerVal; - array_push($policy->conditions, $obj); + $policy->conditions[] = $obj; } - array_push($policy->conditions, array('content-length-range', 0, $maxFileSize)); + $policy->conditions[] = array('content-length-range', 0, $maxFileSize); $policy = base64_encode(str_replace('\/', '/', json_encode($policy))); // Create parameters $params = new stdClass; - $params->AWSAccessKeyId = self::$__accessKey; + $params->AWSAccessKeyId = $creds->accessKey; $params->key = $uriPrefix.'${filename}'; $params->acl = $acl; $params->policy = $policy; unset($policy); - $params->signature = self::__getHash($params->policy); - if (is_numeric($successRedirect) && in_array((int)$successRedirect, array(200, 201))) + $params->signature = self::__getHash($params->policy, $creds); + if (is_numeric($successRedirect) && in_array((int)$successRedirect, array(200, 201), true)) $params->success_action_status = (string)$successRedirect; else $params->success_action_redirect = $successRedirect; @@ -1365,29 +1504,29 @@ public static function getHttpUploadPostParams($bucket, $uriPrefix = '', $acl = /** - * Create a CloudFront distribution - * - * @param string $bucket Bucket name - * @param boolean $enabled Enabled (true/false) - * @param array $cnames Array containing CNAME aliases - * @param string $comment Use the bucket name as the hostname - * @param string $defaultRootObject Default root object - * @param string $originAccessIdentity Origin access identity - * @param array $trustedSigners Array of trusted signers - * @return array | false - */ - public static function createDistribution($bucket, $enabled = true, $cnames = array(), $comment = null, $defaultRootObject = null, $originAccessIdentity = null, $trustedSigners = array()) + * Create a CloudFront distribution + * + * @param string $bucket Bucket name + * @param boolean $enabled Enabled (true/false) + * @param array $cnames Array containing CNAME aliases + * @param string $comment Use the bucket name as the hostname + * @param string $defaultRootObject Default root object + * @param string $originAccessIdentity Origin access identity + * @param array $trustedSigners Array of trusted signers + * @param S3Credentials|null $creds + * @return array | false + */ + public static function createDistribution($bucket, $enabled = true, $cnames = array(), $comment = null, $defaultRootObject = null, $originAccessIdentity = null, $trustedSigners = array(), S3Credentials $creds = null) { if (!extension_loaded('openssl')) { - self::__triggerError(sprintf("S3::createDistribution({$bucket}, ".(int)$enabled.", [], '$comment'): %s", - "CloudFront functionality requires SSL"), __FILE__, __LINE__); + self::__triggerError(sprintf("S3::createDistribution({$bucket}, " . (int)$enabled . ", [], '$comment'): %s", + "CloudFront functionality requires SSL"), __FILE__, __LINE__); return false; } - $useSSL = self::$useSSL; - self::$useSSL = true; // CloudFront requires SSL - $rest = new S3Request('POST', '', '2010-11-01/distribution', 'cloudfront.amazonaws.com'); + $cloudfrontEndpoint = new S3EndpointConfig('cloudfront.amazonaws.com'); + $rest = new S3Request('POST', null, '2010-11-01/distribution', $cloudfrontEndpoint, $creds); $rest->data = self::__getCloudFrontDistributionConfigXML( $bucket.'.s3.amazonaws.com', $enabled, @@ -1403,14 +1542,12 @@ public static function createDistribution($bucket, $enabled = true, $cnames = ar $rest->setHeader('Content-Type', 'application/xml'); $rest = self::__getCloudFrontResponse($rest); - self::$useSSL = $useSSL; - if ($rest->error === false && $rest->code !== 201) $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); if ($rest->error !== false) { - self::__triggerError(sprintf("S3::createDistribution({$bucket}, ".(int)$enabled.", [], '$comment'): [%s] %s", - $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); + self::__triggerError(sprintf("S3::createDistribution({$bucket}, " . (int)$enabled . ", [], '$comment'): [%s] %s", + $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); return false; } elseif ($rest->body instanceof SimpleXMLElement) return self::__parseCloudFrontDistributionConfig($rest->body); @@ -1419,65 +1556,64 @@ public static function createDistribution($bucket, $enabled = true, $cnames = ar /** - * Get CloudFront distribution info - * - * @param string $distributionId Distribution ID from listDistributions() - * @return array | false - */ - public static function getDistribution($distributionId) + * Get CloudFront distribution info + * + * @param string $distributionId Distribution ID from listDistributions() + * @param S3Credentials|null $creds + * @return array | false + */ + public static function getDistribution($distributionId, S3Credentials $creds = null) { if (!extension_loaded('openssl')) { self::__triggerError(sprintf("S3::getDistribution($distributionId): %s", - "CloudFront functionality requires SSL"), __FILE__, __LINE__); + "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, 'cloudfront.amazonaws.com'); + $cloudfrontEndpoint = new S3EndpointConfig('cloudfront.amazonaws.com'); + $rest = new S3Request('GET', null, '2010-11-01/distribution/'.$distributionId, $cloudfrontEndpoint, $creds); $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) { self::__triggerError(sprintf("S3::getDistribution($distributionId): [%s] %s", - $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); + $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); return false; } - elseif ($rest->body instanceof SimpleXMLElement) + + if ($rest->body instanceof SimpleXMLElement) { $dist = self::__parseCloudFrontDistributionConfig($rest->body); $dist['hash'] = $rest->headers['hash']; $dist['id'] = $distributionId; return $dist; } + return false; } /** - * Update a CloudFront distribution - * - * @param array $dist Distribution array info identical to output of getDistribution() - * @return array | false - */ - public static function updateDistribution($dist) + * Update a CloudFront distribution + * + * @param array $dist Distribution array info identical to output of getDistribution() + * @param S3Credentials|null $creds + * @return array | false + */ + public static function updateDistribution($dist, S3Credentials $creds = null) { if (!extension_loaded('openssl')) { self::__triggerError(sprintf("S3::updateDistribution({$dist['id']}): %s", - "CloudFront functionality requires SSL"), __FILE__, __LINE__); + "CloudFront functionality requires SSL"), __FILE__, __LINE__); return false; } - $useSSL = self::$useSSL; - - self::$useSSL = true; // CloudFront requires SSL - $rest = new S3Request('PUT', '', '2010-11-01/distribution/'.$dist['id'].'/config', 'cloudfront.amazonaws.com'); + $cloudfrontEndpoint = new S3EndpointConfig('cloudfront.amazonaws.com'); + $rest = new S3Request('PUT', null, '2010-11-01/distribution/'.$dist['id'].'/config', $cloudfrontEndpoint, $creds); $rest->data = self::__getCloudFrontDistributionConfigXML( $dist['origin'], $dist['enabled'], @@ -1493,54 +1629,48 @@ public static function updateDistribution($dist) $rest->setHeader('If-Match', $dist['hash']); $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) { self::__triggerError(sprintf("S3::updateDistribution({$dist['id']}): [%s] %s", - $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); + $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); return false; - } else { - $dist = self::__parseCloudFrontDistributionConfig($rest->body); - $dist['hash'] = $rest->headers['hash']; - return $dist; } - return false; + + $dist = self::__parseCloudFrontDistributionConfig($rest->body); + $dist['hash'] = $rest->headers['hash']; + return $dist; } /** - * Delete a CloudFront distribution - * - * @param array $dist Distribution array info identical to output of getDistribution() - * @return boolean - */ - public static function deleteDistribution($dist) + * Delete a CloudFront distribution + * + * @param array $dist Distribution array info identical to output of getDistribution() + * @param S3Credentials|null $creds + * @return boolean + */ + public static function deleteDistribution($dist, S3Credentials $creds = null) { if (!extension_loaded('openssl')) { self::__triggerError(sprintf("S3::deleteDistribution({$dist['id']}): %s", - "CloudFront functionality requires SSL"), __FILE__, __LINE__); + "CloudFront functionality requires SSL"), __FILE__, __LINE__); return false; } - $useSSL = self::$useSSL; - - self::$useSSL = true; // CloudFront requires SSL - $rest = new S3Request('DELETE', '', '2008-06-30/distribution/'.$dist['id'], 'cloudfront.amazonaws.com'); + $cloudfrontEndpoint = new S3EndpointConfig('cloudfront.amazonaws.com'); + $rest = new S3Request('DELETE', null, '2008-06-30/distribution/'.$dist['id'], $cloudfrontEndpoint, $creds); $rest->setHeader('If-Match', $dist['hash']); $rest = self::__getCloudFrontResponse($rest); - self::$useSSL = $useSSL; - if ($rest->error === false && $rest->code !== 204) $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); if ($rest->error !== false) { self::__triggerError(sprintf("S3::deleteDistribution({$dist['id']}): [%s] %s", - $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); + $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); return false; } return true; @@ -1548,42 +1678,35 @@ public static function deleteDistribution($dist) /** - * Get a list of CloudFront distributions - * - * @return array - */ - public static function listDistributions() + * Get a list of CloudFront distributions + * + * @param S3Credentials|null $creds + * @return array|bool + */ + public static function listDistributions(S3Credentials $creds = null) { if (!extension_loaded('openssl')) { - self::__triggerError(sprintf("S3::listDistributions(): [%s] %s", - "CloudFront functionality requires SSL"), __FILE__, __LINE__); + self::__triggerError('S3::listDistributions(): 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', 'cloudfront.amazonaws.com'); + $cloudfrontEndpoint = new S3EndpointConfig('cloudfront.amazonaws.com'); + $rest = new S3Request('GET', null, '2010-11-01/distribution', $cloudfrontEndpoint, $creds); $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) { - self::__triggerError(sprintf("S3::listDistributions(): [%s] %s", - $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); + self::__triggerError(sprintf('S3::listDistributions(): [%s] %s', + $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); return false; } - elseif ($rest->body instanceof SimpleXMLElement && isset($rest->body->DistributionSummary)) + + if ($rest->body instanceof SimpleXMLElement && isset($rest->body->DistributionSummary)) { $list = array(); - if (isset($rest->body->Marker, $rest->body->MaxItems, $rest->body->IsTruncated)) - { - //$info['marker'] = (string)$rest->body->Marker; - //$info['maxItems'] = (int)$rest->body->MaxItems; - //$info['isTruncated'] = (string)$rest->body->IsTruncated == 'true' ? true : false; - } foreach ($rest->body->DistributionSummary as $summary) $list[(string)$summary->Id] = self::__parseCloudFrontDistributionConfig($summary); @@ -1593,23 +1716,22 @@ public static function listDistributions() } /** - * List CloudFront Origin Access Identities - * - * @return array - */ - public static function listOriginAccessIdentities() + * List CloudFront Origin Access Identities + * + * @param S3Credentials|null $creds + * @return array|bool + */ + public static function listOriginAccessIdentities(S3Credentials $creds = null) { if (!extension_loaded('openssl')) { - self::__triggerError(sprintf("S3::listOriginAccessIdentities(): [%s] %s", - "CloudFront functionality requires SSL"), __FILE__, __LINE__); + self::__triggerError('S3::listOriginAccessIdentities(): CloudFront functionality requires SSL', __FILE__, __LINE__); return false; } - self::$useSSL = true; // CloudFront requires SSL - $rest = new S3Request('GET', '', '2010-11-01/origin-access-identity/cloudfront', 'cloudfront.amazonaws.com'); + $cloudfrontEndpoint = new S3EndpointConfig('cloudfront.amazonaws.com'); + $rest = new S3Request('GET', null, '2010-11-01/origin-access-identity/cloudfront', $cloudfrontEndpoint, $creds); $rest = self::__getCloudFrontResponse($rest); - $useSSL = self::$useSSL; if ($rest->error === false && $rest->code !== 200) $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); @@ -1633,30 +1755,29 @@ public static function listOriginAccessIdentities() /** - * Invalidate objects in a CloudFront distribution - * - * Thanks to Martin Lindkvist for S3::invalidateDistribution() - * - * @param string $distributionId Distribution ID from listDistributions() - * @param array $paths Array of object paths to invalidate - * @return boolean - */ - public static function invalidateDistribution($distributionId, $paths) + * Invalidate objects in a CloudFront distribution + * + * Thanks to Martin Lindkvist for S3::invalidateDistribution() + * + * @param string $distributionId Distribution ID from listDistributions() + * @param array $paths Array of object paths to invalidate + * @param S3Credentials|null $creds + * @return bool + */ + public static function invalidateDistribution($distributionId, $paths, S3Credentials $creds = null) { if (!extension_loaded('openssl')) { - self::__triggerError(sprintf("S3::invalidateDistribution(): [%s] %s", - "CloudFront functionality requires SSL"), __FILE__, __LINE__); + self::__triggerError('S3::invalidateDistribution(): CloudFront functionality requires SSL', __FILE__, __LINE__); return false; } - $useSSL = self::$useSSL; - self::$useSSL = true; // CloudFront requires SSL - $rest = new S3Request('POST', '', '2010-08-01/distribution/'.$distributionId.'/invalidation', 'cloudfront.amazonaws.com'); + $cloudfrontEndpoint = new S3EndpointConfig('cloudfront.amazonaws.com'); + + $rest = new S3Request('POST', null, '2010-08-01/distribution/'.$distributionId.'/invalidation', $cloudfrontEndpoint, $creds); $rest->data = self::__getCloudFrontInvalidationBatchXML($paths, (string)microtime(true)); $rest->size = strlen($rest->data); $rest = self::__getCloudFrontResponse($rest); - self::$useSSL = $useSSL; if ($rest->error === false && $rest->code !== 201) $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); @@ -1671,13 +1792,13 @@ public static function invalidateDistribution($distributionId, $paths) /** - * Get a InvalidationBatch DOMDocument - * - * @internal Used to create XML in invalidateDistribution() - * @param array $paths Paths to objects to invalidateDistribution - * @param int $callerReference - * @return string - */ + * Get a InvalidationBatch DOMDocument + * + * @param array $paths Paths to objects to invalidateDistribution + * @param string $callerReference + * @return string + * @internal Used to create XML in invalidateDistribution() + */ private static function __getCloudFrontInvalidationBatchXML($paths, $callerReference = '0') { $dom = new DOMDocument('1.0', 'UTF-8'); @@ -1693,45 +1814,45 @@ 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) + * 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() + * @param S3Credentials|null $creds + * @return array|bool + */ + public static function getDistributionInvalidationList($distributionId, S3Credentials $creds = null) { if (!extension_loaded('openssl')) { - self::__triggerError(sprintf("S3::getDistributionInvalidationList(): [%s] %s", - "CloudFront functionality requires SSL"), __FILE__, __LINE__); + self::__triggerError('S3::getDistributionInvalidationList(): 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'); + $cloudfrontEndpoint = new S3EndpointConfig('cloudfront.amazonaws.com'); + + $rest = new S3Request('GET', null, '2010-11-01/distribution/'.$distributionId.'/invalidation', $cloudfrontEndpoint, $creds); $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); + trigger_error(sprintf("S3::getDistributionInvalidationList('{$distributionId}'): [%s]: %s", + $rest->error['code'], $rest->error['message']), E_USER_WARNING); return false; } - elseif ($rest->body instanceof SimpleXMLElement && isset($rest->body->InvalidationSummary)) + + if ($rest->body instanceof SimpleXMLElement && isset($rest->body->InvalidationSummary)) { $list = array(); foreach ($rest->body->InvalidationSummary as $summary) @@ -1739,6 +1860,7 @@ public static function getDistributionInvalidationList($distributionId) return $list; } + return array(); } @@ -1819,7 +1941,7 @@ private static function __parseCloudFrontDistributionConfig(&$node) $dist['callerReference'] = (string)$node->CallerReference; if (isset($node->Enabled)) - $dist['enabled'] = (string)$node->Enabled == 'true' ? true : false; + $dist['enabled'] = (string)$node->Enabled === 'true'; if (isset($node->S3Origin)) { @@ -1865,7 +1987,7 @@ private static function __getCloudFrontResponse(&$rest) { $rest->getResponse(); if ($rest->response->error === false && isset($rest->response->body) && - is_string($rest->response->body) && substr($rest->response->body, 0, 5) == 'response->body) && strpos($rest->response->body, 'response->body = simplexml_load_string($rest->response->body); // Grab CloudFront errors @@ -1918,7 +2040,7 @@ private static function __getMIMEType(&$file) if (isset($exts[$ext])) return $exts[$ext]; // Use fileinfo if available - if (extension_loaded('fileinfo') && isset($_ENV['MAGIC']) && + if (isset($_ENV['MAGIC']) && extension_loaded('fileinfo') && ($finfo = finfo_open(FILEINFO_MIME, $_ENV['MAGIC'])) !== false) { if (($type = finfo_file($finfo, $file)) !== false) @@ -1930,7 +2052,7 @@ private static function __getMIMEType(&$file) $type = trim(array_shift($type)); } finfo_close($finfo); - if ($type !== false && strlen($type) > 0) return $type; + if ($type !== false && $type !== '') return $type; } return 'application/octet-stream'; @@ -1940,7 +2062,7 @@ private static function __getMIMEType(&$file) /** * Get the current time * - * @internal Used to apply offsets to sytem time + * @internal Used to apply offsets to system time * @return integer */ public static function __getTime() @@ -1950,52 +2072,60 @@ public static function __getTime() /** - * Generate the auth string: "AWS AccessKey:Signature" - * - * @internal Used by S3Request::getResponse() - * @param string $string String to sign - * @return string - */ - public static function __getSignature($string) + * Generate the auth string: "AWS AccessKey:Signature" + * + * @param string $string String to sign + * @param S3Credentials $creds + * @return string + * @internal Used by S3Request::getResponse() + */ + public static function __getSignature($string, S3Credentials $creds) { - return 'AWS '.self::$__accessKey.':'.self::__getHash($string); + return 'AWS '.$creds->accessKey.':'.self::__getHash($string, $creds->secretKey); } /** - * Creates a HMAC-SHA1 hash - * - * This uses the hash extension if loaded - * - * @internal Used by __getSignature() - * @param string $string String to sign - * @return string - */ - private static function __getHash($string) + * Creates a HMAC-SHA1 hash + * + * This uses the hash extension if loaded + * + * @param string $string String to sign + * @param string $secretKey + * @return string + * @internal Used by __getSignature() + */ + private static function __getHash($string, $secretKey) { - return base64_encode(extension_loaded('hash') ? - hash_hmac('sha1', $string, self::$__secretKey, true) : pack('H*', sha1( - (str_pad(self::$__secretKey, 64, chr(0x00)) ^ (str_repeat(chr(0x5c), 64))) . - pack('H*', sha1((str_pad(self::$__secretKey, 64, chr(0x00)) ^ - (str_repeat(chr(0x36), 64))) . $string))))); + if (extension_loaded('hash')) { + return base64_encode( + hash_hmac('sha1', $string, $secretKey, true)); + } + + return base64_encode( + pack('H*', sha1( + (str_pad($secretKey, 64, chr(0x00)) ^ str_repeat(chr(0x5c), 64)) . + pack('H*', sha1( + (str_pad($secretKey, 64, chr(0x00)) ^ str_repeat(chr(0x36), 64)) . $string))))); } /** - * Generate the headers for AWS Signature V4 - * - * @internal Used by S3Request::getResponse() - * @param array $amzHeaders - * @param array $headers - * @param string $method - * @param string $uri - * @param array $parameters - * @return array - */ - public static function __getSignatureV4($amzHeaders, $headers, $method, $uri, $parameters) - { + * Generate the headers for AWS Signature V4 + * + * @param array $amzHeaders + * @param array $headers + * @param string $method + * @param string $uri + * @param array $parameters + * @param S3Credentials $creds + * @param string $region + * @return string + * @internal Used by S3Request::getResponse() + */ + public static function __getSignatureV4($amzHeaders, $headers, $method, $uri, $parameters, S3Credentials $creds, $region) + { $service = 's3'; - $region = S3::getRegion(); $algorithm = 'AWS4-HMAC-SHA256'; $combinedHeaders = array(); @@ -2012,7 +2142,7 @@ public static function __getSignatureV4($amzHeaders, $headers, $method, $uri, $p // 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); + $queryString = http_build_query($parameters, null, '&'); // Payload $amzPayload = array($method); @@ -2035,24 +2165,33 @@ public static function __getSignatureV4($amzHeaders, $headers, $method, $uri, $p // request as string $amzPayloadStr = implode("\n", $amzPayload); + SigDebug('Payload to sign: [%s]', $amzPayloadStr); + // CredentialScope $credentialScope = array($amzDateStamp, $region, $service, 'aws4_request'); // stringToSign - $stringToSignStr = implode("\n", array($algorithm, $amzHeaders['x-amz-date'], - implode('/', $credentialScope), hash('sha256', $amzPayloadStr))); + $stringToSignStr = implode("\n", array($algorithm, $amzHeaders['x-amz-date'], + implode('/', $credentialScope), hash('sha256', $amzPayloadStr))); + + SigDebug('String to sign: [%s]', $stringToSignStr); // Make Signature - $kSecret = 'AWS4' . self::$__secretKey; + $kSecret = 'AWS4' . $creds->secretKey; + SigDebug('$kSecret: %s', crc32($kSecret)); $kDate = hash_hmac('sha256', $amzDateStamp, $kSecret, true); + SigDebug('$kDate: %s', crc32($kDate)); $kRegion = hash_hmac('sha256', $region, $kDate, true); + SigDebug('$kRegion: %s', crc32($kRegion)); $kService = hash_hmac('sha256', $service, $kRegion, true); + SigDebug('$kService: %s', crc32($kService)); $kSigning = hash_hmac('sha256', 'aws4_request', $kService, true); + SigDebug('$kSigning: %s', crc32($kSigning)); $signature = hash_hmac('sha256', $stringToSignStr, $kSigning); return $algorithm . ' ' . implode(',', array( - 'Credential=' . self::$__accessKey . '/' . implode('/', $credentialScope), + 'Credential=' . $creds->accessKey . '/' . implode('/', $credentialScope), 'SignedHeaders=' . implode(';', array_keys($combinedHeaders)), 'Signature=' . $signature, )); @@ -2073,10 +2212,27 @@ private static function __sortMetaHeadersCmp($a, $b) $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; + if ($lenA === $lenB) return $ncmp; + if (0 === $ncmp) return $lenA < $lenB ? -1 : 1; return $ncmp; } + + + /** + * Helper to transition from bucket names to bucket-config + * + * @param string|S3BucketConfig $bucket + * @param S3EndpointConfig $endpoint + * @return S3BucketConfig + */ + public static function makeBucketConfig($bucket, S3EndpointConfig $endpoint) + { + if ($bucket instanceof S3BucketConfig) { + return $bucket; + } + + return new S3BucketConfig($bucket, self::getRegion($endpoint)); + } } /** @@ -2088,25 +2244,28 @@ private static function __sortMetaHeadersCmp($a, $b) final class S3Request { /** - * AWS URI + * endpoint config * - * @var string - * @access private + * @var S3EndpointConfig */ private $endpoint; - + + /** + * @var S3Credentials + */ + private $credentials; + /** - * Verb + * HTTP Verb * * @var string - * @access private */ private $verb; /** - * S3 bucket name + * bucket config * - * @var string + * @var S3BucketConfig * @access private */ private $bucket; @@ -2125,7 +2284,7 @@ final class S3Request * @var string * @access private */ - private $resource = ''; + private $resource; /** * Additional request parameters @@ -2156,7 +2315,7 @@ final class S3Request /** * Use HTTP PUT? * - * @var bool + * @var resource * @access public */ public $fp = false; @@ -2172,7 +2331,7 @@ final class S3Request /** * PUT post fields * - * @var array + * @var string * @access public */ public $data = false; @@ -2187,44 +2346,29 @@ final class S3Request /** - * Constructor - * - * @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') + * Constructor + * + * @param string $verb Verb + * @param S3BucketConfig|string $bucket Bucket config + * @param string $uri Object URI + * @param S3EndpointConfig $endpoint AWS endpoint URI + * @param S3Credentials|null $creds + */ + public function __construct($verb, $bucket = null, $uri = '', S3EndpointConfig $endpoint = null, S3Credentials $creds = null) { - $this->endpoint = $endpoint; + $this->endpoint = S3::getEndpoint($endpoint); + $this->credentials = S3::getCredentials($creds); + if ($bucket !== null) { + $this->bucket = S3::makeBucketConfig($bucket, $this->endpoint); + } + $this->verb = $verb; - $this->bucket = $bucket; $this->uri = $uri !== '' ? '/'.str_replace('%2F', '/', rawurlencode($uri)) : '/'; - if ($this->bucket !== '') - { - if ($this->__dnsBucketName($this->bucket)) - { - $this->headers['Host'] = $this->bucket.'.'.$this->endpoint; - $this->resource = '/'.$this->bucket.$this->uri; - } - 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; - $this->bucket = ''; - $this->resource = $this->uri; - } - } - else - { - $this->headers['Host'] = $this->endpoint; - $this->resource = $this->uri; - } - + $res = $this->endpoint->resolveHostUriAndResource($this->uri, $this->bucket); + $this->headers['Host'] = $res['host']; + $this->uri = $res['uri']; + $this->resource = $res['resource']; $this->headers['Date'] = gmdate('D, d M Y H:i:s T'); $this->response = new STDClass; @@ -2280,12 +2424,11 @@ public function setAmzHeader($key, $value) */ public function getResponse() { - $query = ''; - if (sizeof($this->parameters) > 0) + if (count($this->parameters) > 0) { $query = substr($this->uri, -1) !== '?' ? '?' : '&'; foreach ($this->parameters as $var => $value) - if ($value == null || $value == '') $query .= $var.'&'; + if ($value === null || $value === '') $query .= $var.'&'; else $query .= $var.'='.rawurlencode($value).'&'; $query = substr($query, 0, -1); $this->uri .= $query; @@ -2297,51 +2440,59 @@ public function getResponse() array_key_exists('logging', $this->parameters)) $this->resource .= $query; } - $url = (S3::$useSSL ? 'https://' : 'http://') . ($this->headers['Host'] !== '' ? $this->headers['Host'] : $this->endpoint) . $this->uri; + $url = ($this->endpoint->useSSL ? 'https://' : 'http://') . ($this->headers['Host'] !== '' ? $this->headers['Host'] : $this->endpoint->hostname) . $this->uri; // Basic setup $curl = curl_init(); curl_setopt($curl, CURLOPT_USERAGENT, 'S3/php'); - if (S3::$useSSL) + if ('ON' === strtoupper(getenv('CURL_VERBOSE'))) { + curl_setopt($curl, CURLOPT_VERBOSE, true); + } + + if ($this->endpoint->useSSL) { // Set protocol version - curl_setopt($curl, CURLOPT_SSLVERSION, S3::$useSSLVersion); + curl_setopt($curl, CURLOPT_SSLVERSION, $this->endpoint->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); + curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, $this->endpoint->useSSLValidation ? 2 : 0); + curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, $this->endpoint->useSSLValidation ? 1 : 0); - if (S3::$sslKey !== null) curl_setopt($curl, CURLOPT_SSLKEY, S3::$sslKey); - if (S3::$sslCert !== null) curl_setopt($curl, CURLOPT_SSLCERT, S3::$sslCert); - if (S3::$sslCACert !== null) curl_setopt($curl, CURLOPT_CAINFO, S3::$sslCACert); + if ($this->endpoint->sslKey !== null) curl_setopt($curl, CURLOPT_SSLKEY, $this->endpoint->sslKey); + if ($this->endpoint->sslCert !== null) curl_setopt($curl, CURLOPT_SSLCERT, $this->endpoint->sslCert); + if ($this->endpoint->sslCACert !== null) curl_setopt($curl, CURLOPT_CAINFO, $this->endpoint->sslCACert); } curl_setopt($curl, CURLOPT_URL, $url); - if (S3::$proxy != null && isset(S3::$proxy['host'])) + if ($this->endpoint->proxy !== null && isset($this->endpoint->proxy['host'])) { - curl_setopt($curl, CURLOPT_PROXY, S3::$proxy['host']); - curl_setopt($curl, CURLOPT_PROXYTYPE, S3::$proxy['type']); - 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'])); + curl_setopt($curl, CURLOPT_PROXY, $this->endpoint->proxy['host']); + curl_setopt($curl, CURLOPT_PROXYTYPE, $this->endpoint->proxy['type']); + if (isset($this->endpoint->proxy['user'], $this->endpoint->proxy['pass']) + && $this->endpoint->proxy['user'] !== null + && $this->endpoint->proxy['pass'] !== null) + { + curl_setopt($curl, CURLOPT_PROXYUSERPWD, sprintf('%s:%s', $this->endpoint->proxy['user'], $this->endpoint->proxy['pass'])); + } } // Headers $httpHeaders = array(); - if (S3::hasAuth()) + if ($this->credentials !== null) { // Authorization string (CloudFront stringToSign should only contain a date) - if ($this->headers['Host'] == 'cloudfront.amazonaws.com') + if ($this->endpoint->signatureVersion === S3::SigV2 || $this->headers['Host'] === 'cloudfront.amazonaws.com') { # TODO: Update CloudFront authentication foreach ($this->amzHeaders as $header => $value) - if (strlen($value) > 0) $httpHeaders[] = $header.': '.$value; + if ($value !== '') $httpHeaders[] = $header.': '.$value; foreach ($this->headers as $header => $value) - if (strlen($value) > 0) $httpHeaders[] = $header.': '.$value; + if ($value !== '') $httpHeaders[] = $header.': '.$value; - $httpHeaders[] = 'Authorization: ' . S3::__getSignature($this->headers['Date']); + $httpHeaders[] = 'Authorization: ' . S3::__getSignature($this->headers['Date'], $this->credentials); } else { @@ -2351,17 +2502,30 @@ public function getResponse() $this->amzHeaders['x-amz-content-sha256'] = hash('sha256', $this->data); foreach ($this->amzHeaders as $header => $value) - if (strlen($value) > 0) $httpHeaders[] = $header.': '.$value; + if ($value !== '') $httpHeaders[] = $header.': '.$value; foreach ($this->headers as $header => $value) - if (strlen($value) > 0) $httpHeaders[] = $header.': '.$value; + if ($value !== '') $httpHeaders[] = $header.': '.$value; + + $authRegion = ''; + if ($this->bucket !== null) { + $authRegion = $this->bucket->region; + } elseif ($this->headers['Host'] === $this->endpoint->hostname) { + $authRegion = $this->endpoint->defaultRegion; + } + + if (empty($authRegion) && !empty(S3::$region)) { + $authRegion = S3::$region; + } $httpHeaders[] = 'Authorization: ' . S3::__getSignatureV4( $this->amzHeaders, - $this->headers, - $this->verb, + $this->headers, + $this->verb, $this->uri, - $this->parameters + $this->parameters, + $this->credentials, + $authRegion ); } @@ -2377,7 +2541,7 @@ public function getResponse() // Request types switch ($this->verb) { - case 'GET': break; + // case 'GET': break; case 'PUT': case 'POST': // POST only used for CloudFront if ($this->fp !== false) { @@ -2412,7 +2576,7 @@ public function getResponse() // Execute, grab errors if (curl_exec($curl)) - $this->response->code = curl_getinfo($curl, CURLINFO_HTTP_CODE); + $this->response->code = (int)curl_getinfo($curl, CURLINFO_HTTP_CODE); else $this->response->error = array( 'code' => curl_errno($curl), @@ -2423,14 +2587,14 @@ public function getResponse() @curl_close($curl); // Parse body into XML - if ($this->response->error === false && isset($this->response->headers['type']) && - $this->response->headers['type'] == 'application/xml' && isset($this->response->body)) + if ($this->response->error === false && isset($this->response->headers['type'], $this->response->body) + && $this->response->headers['type'] === 'application/xml') { $this->response->body = simplexml_load_string($this->response->body); // Grab S3 errors - if (!in_array($this->response->code, array(200, 204, 206)) && - isset($this->response->body->Code, $this->response->body->Message)) + if (isset($this->response->body->Code, $this->response->body->Message) && + !in_array($this->response->code, array(200, 204, 206), true)) { $this->response->error = array( 'code' => (string)$this->response->body->Code, @@ -2458,7 +2622,7 @@ public function getResponse() */ private function __responseWriteCallback(&$curl, &$data) { - if (in_array($this->response->code, array(200, 206)) && $this->fp !== false) + if ($this->fp !== false && in_array($this->response->code, array(200, 206), true)) return fwrite($this->fp, $data); else $this->response->body .= $data; @@ -2466,24 +2630,6 @@ private function __responseWriteCallback(&$curl, &$data) } - /** - * Check DNS conformity - * - * @param string $bucket Bucket name - * @return boolean - */ - 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; - if (!preg_match("/[0-9a-z]$/", $bucket)) return false; - return true; - } - - /** * CURL header callback * @@ -2494,7 +2640,7 @@ private function __dnsBucketName($bucket) private function __responseHeaderCallback($curl, $data) { if (($strlen = strlen($data)) <= 2) return $strlen; - if (substr($data, 0, 4) == 'HTTP') + if (strpos($data, 'HTTP') === 0) $this->response->code = (int)substr($data, 9, 3); else { @@ -2502,22 +2648,21 @@ private function __responseHeaderCallback($curl, $data) if (strpos($data, ': ') === false) return $strlen; list($header, $value) = explode(': ', $data, 2); $header = strtolower($header); - if ($header == 'last-modified') + 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') - $this->response->headers['hash'] = $value{0} == '"' ? substr($value, 1, -1) : $value; + 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; } return $strlen; } - } /** @@ -2536,10 +2681,314 @@ class S3Exception extends Exception { * @param string $line Line number on which exception was created * @param int $code Exception code */ - function __construct($message, $file, $line, $code = 0) + public function __construct($message, $file, $line, $code = 0) { parent::__construct($message, $code); $this->file = $file; $this->line = $line; } + + public function __toString() + { + return sprintf('%s @ %s:%d', parent::__toString(), $this->file, $this->line); + } +} + +/** + * S3EndpointConfig class + */ +final class S3EndpointConfig +{ + const AWS_S3_SUFFIX = '.amazonaws.com'; + const AWS_S3_SUFFIX_LENGTH = 14; + + const AWS_S3_DEFAULT_HOST = 's3.amazonaws.com'; + const AWS_S3_DEFAULT_REGION = 'us-east-1'; + + /** + * Auth signature version + * + * @var string + */ + public $signatureVersion = S3::SigV4; + + /** + * Enable SSL + * + * @var bool + */ + public $useSSL = true; + + /** + * Use SSL version + * + * @var int constant as defined by curl module + */ + public $useSSLVersion = CURL_SSLVERSION_TLSv1; + + /** + * Enable SSL host & peer validation + * + * @var bool + */ + public $useSSLValidation = true; + + /** + * SSL client key + * + * @var string + */ + public $sslKey; + + /** + * SSL client certfificate + * + * @var string + */ + public $sslCert; + + /** + * SSL CA cert (only required if you are having problems with your system CA cert) + * + * @var string + */ + public $sslCACert; + + /** + * Hostname + * + * @var string + */ + public $hostname = self::AWS_S3_DEFAULT_HOST; + + /** + * default region + * + * @var string + */ + public $defaultRegion = self::AWS_S3_DEFAULT_REGION; + + /** + * Force Path-Style bucket reference in URL + * + * @var bool + */ + public $forcePathStyle = false; + + /** + * Proxy details + * @var array + */ + public $proxy; + + public function __construct($hostname = self::AWS_S3_DEFAULT_HOST, $defaultRegion = null) + { + if ($defaultRegion === null && $hostname === self::AWS_S3_DEFAULT_HOST) { + $defaultRegion = self::AWS_S3_DEFAULT_REGION; + } + + $this->hostname = $hostname; + $this->defaultRegion = $defaultRegion; + } + + /** + * If it is an AWS S3 endpoint, extract region code from the hostname. + * @see https://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region + * + * @return string + */ + public function getRegion() + { + // if not an AWS endpoint, return the default region + if (1 !== preg_match("/s3[.-](?:website-|dualstack\.)?(.+)\.amazonaws\.com/i", $this->hostname, $match)) { + return $this->defaultRegion; + } + + if (strtolower($match[1]) === "external-1") { + return $this->defaultRegion; + } + + return $match[1]; + } + + /** + * Check DNS conformity + * + * @param string $bucket Bucket name + * @return boolean + */ + private function isDnsSafeName($bucket) + { + switch (true) { + case strlen($bucket) > 63: + case 0 !== preg_match("/[^a-z0-9\.-]/", $bucket): + case $this->useSSL && strpos($bucket, '.') !== false: + case strpos($bucket, '-.') !== false: + case strpos($bucket, '..') !== false: + case !preg_match('/^[0-9a-z]/', $bucket): + case !preg_match('/[0-9a-z]$/', $bucket): + return false; + } + + return true; + } + + /** + * @param $uri + * @param S3BucketConfig|null $bucket + * @return array + */ + public function resolveHostUriAndResource($uri, S3BucketConfig $bucket = null) + { + if ($bucket === null) { + return array( + 'host' => $this->hostname, + 'uri' => $uri, + 'resource' => $uri, + ); + } + + $pathStyle = $this->forcePathStyle || !$this->isDnsSafeName($bucket->name); + + if ($pathStyle) { + return array( + 'host' => $this->hostname, + 'uri' => '/' . $bucket->name . $uri, + 'resource' => $uri, + ); + } + + if ($this->hostname !== self::AWS_S3_DEFAULT_HOST || empty($bucket->region)) { + $hostname = $bucket->name . '.' . $this->hostname; + } else { + $hostname = sprintf('%s.s3.%s.amazonaws.com', $bucket->name, $bucket->region); + } + + return array( + 'host' => $hostname, + 'uri' => $uri, + 'resource' => '/' . $bucket->name . $uri, + ); + } + + public function withHostname($hostname) + { + $this->hostname = $hostname; + return $this; + } + + public function withSSLEnabled($enabled = true) + { + $this->useSSL = $enabled; + return $this; + } + + public function withSSLValidationEnabled($enabled = true) + { + $this->useSSLValidation = $enabled; + return $this; + } + + public function withPathStyleEnabled($enabled = true) + { + $this->forcePathStyle = $enabled; + return $this; + } + + public function withSSLVersion($sslVersion = CURL_SSLVERSION_TLSv1) + { + $this->useSSLVersion = $sslVersion; + return $this; + } + + public function withSignatureVersion($version = S3::SigV4) + { + $this->signatureVersion = $version; + return $this; + } + + public function withDefaultRegion($defaultRegion) + { + $this->defaultRegion = $defaultRegion; + return $this; + } + + /** + * Set SSL client certificates (experimental) + * + * @param string $sslCert SSL client certificate + * @param string $sslKey SSL client key + * @param string $sslCACert SSL CA cert (only required if you are having problems with your system CA cert) + * + * @return self + */ + public function withSSLAuth($sslCert = null, $sslKey = null, $sslCACert = null) + { + $this->sslCert = $sslCert; + $this->sslKey = $sslKey; + $this->sslCACert = $sslCACert; + + return $this; + } + + /** + * Set proxy information + * + * @param string $host Proxy hostname and port (localhost:1234) + * @param string $user Proxy username + * @param string $pass Proxy password + * @param int $type CURL proxy type + * @return self + */ + public function withProxy($host, $user = null, $pass = null, $type = CURLPROXY_SOCKS5) + { + $this->proxy = array( + 'host' => $host, + 'type' => $type, + 'user' => $user, + 'pass' => $pass + ); + + return $this; + } +} + +/** + * S3BucketConfig class + */ +final class S3BucketConfig +{ + public $name; + public $region; + + public function __construct($name, $region) + { + $this->name = $name; + $this->region = $region; + } + + public function __toString() + { + return $this->name; + } +} + +/** + * S3Credentials class + */ +final class S3Credentials +{ + public $accessKey; + public $secretKey; + + public function __construct($accessKey, $secretKey) + { + $this->accessKey = $accessKey; + $this->secretKey = $secretKey; + } + + public function isInitialised() + { + return $this->accessKey !== null && $this->secretKey !== null; + } } diff --git a/composer.json b/composer.json index 4c55bc10..0fe34b6d 100644 --- a/composer.json +++ b/composer.json @@ -11,9 +11,18 @@ } ], "require": { - "php": ">=5.2.0" + "php": ">=5.2.0", + "ext-curl": "*", + "ext-dom": "*", + "ext-openssl": "*", + "ext-json": "*", + "ext-simplexml": "*", + "ext-fileinfo": "*" }, "autoload": { "classmap": ["S3.php"] + }, + "autoload-dev": { + "files": ["example-helpers.php"] } } diff --git a/example-cloudfront.php b/example-cloudfront.php index f96a077a..cd6654bb 100755 --- a/example-cloudfront.php +++ b/example-cloudfront.php @@ -6,24 +6,18 @@ * S3 class - CloudFront usage */ -if (!class_exists('S3')) require_once 'S3.php'; +require_once 'vendor/autoload.php'; -// AWS access info -if (!defined('awsAccessKey')) define('awsAccessKey', 'change-this'); -if (!defined('awsSecretKey')) define('awsSecretKey', 'change-this'); +// File to upload, we'll use this file since it exists +list($uploadFile) = get_included_files(); +$bucketName = uniqid('s3test', false); // Temporary bucket -// Check for CURL -if (!extension_loaded('curl') && !@dl(PHP_SHLIB_SUFFIX == 'so' ? 'curl.so' : 'php_curl.dll')) - exit("\nERROR: CURL extension not loaded\n\n"); - -// Pointless without your keys! -if (awsAccessKey == 'change-this' || awsSecretKey == 'change-this') - exit("\nERROR: AWS access information required\n\nPlease edit the following lines in this file:\n\n". - "define('awsAccessKey', 'change-me');\ndefine('awsSecretKey', 'change-me');\n\n"); - - -S3::setAuth(awsAccessKey, awsSecretKey); +// Initialise S3 +S3::Init( + new S3Credentials(_getenv('ACCESS_KEY'), _getenv('SECRET_KEY')), + _getenv('REGION', 'us-west-1') + ); function test_createDistribution($bucket, $cnames = array()) { @@ -36,7 +30,7 @@ function test_createDistribution($bucket, $cnames = array()) { function test_listDistributions() { if (($dists = S3::listDistributions()) !== false) { - if (sizeof($dists) == 0) echo "listDistributions(): No distributions\n"; + if (count($dists) === 0) echo "listDistributions(): No distributions\n"; foreach ($dists as $dist) { var_dump($dist); } @@ -63,7 +57,7 @@ function test_deleteDistribution($distributionId) { // To delete a distribution configuration you must first set enable=false with // the updateDistrubution() method and wait for status=Deployed: if (($dist = S3::getDistribution($distributionId)) !== false) { - if ($dist['status'] == 'Deployed') { + if ($dist['status'] === 'Deployed') { echo "deleteDistribution($distributionId): "; var_dump(S3::deleteDistribution($dist)); } else { echo "deleteDistribution($distributionId): Distribution not ready for deletion (status is not 'Deployed')\n"; diff --git a/example-custom-endpoint.php b/example-custom-endpoint.php new file mode 100755 index 00000000..891dab10 --- /dev/null +++ b/example-custom-endpoint.php @@ -0,0 +1,52 @@ +#!/usr/local/bin/php +withPathStyleEnabled(_getenv('USE_PATH_STYLE', 'YES') === 'YES') + ->withSSLEnabled(_getenv('USE_SSL', 'NO') === 'YES'); + +$creds = new S3Credentials(_getenv('ACCESS_KEY'), _getenv('SECRET_KEY')); + +$bucketName = _getenv('BUCKET'); + +// Initialise S3 +S3::Init($creds, $region, $endpoint); + +// Put our file (also with public read access) +echo "S3::putObjectFile(): Copying {$uploadFile} to {$bucketName}/" . baseName($uploadFile) . PHP_EOL; +if (S3::putObjectFile($uploadFile, $bucketName, baseName($uploadFile), S3::ACL_PUBLIC_READ)) { + echo "S3::putObjectFile(): File copied to {$bucketName}/" . baseName($uploadFile) . PHP_EOL; + + + // Get object info + $info = S3::getObjectInfo($bucketName, baseName($uploadFile)); + echo "S3::getObjectInfo(): Info for {$bucketName}/" . baseName($uploadFile) . ': ' . print_r($info, 1); + + + // You can also fetch the object into memory + var_dump("S3::getObject() to memory", S3::getObject($bucketName, baseName($uploadFile))); + + + // Delete our file + if (S3::deleteObject($bucketName, baseName($uploadFile))) { + echo "S3::deleteObject(): Deleted file\n"; + } else { + echo "S3::deleteObject(): Failed to delete file\n"; + } +} else { + echo "S3::putObjectFile(): Failed to copy file\n"; +} + diff --git a/example-form.php b/example-form.php index 2d180f86..26a799f2 100644 --- a/example-form.php +++ b/example-form.php @@ -5,25 +5,18 @@ * S3 form upload example */ -if (!class_exists('S3')) require_once 'S3.php'; +require_once 'vendor/autoload.php'; -// AWS access info -if (!defined('awsAccessKey')) define('awsAccessKey', 'change-this'); -if (!defined('awsSecretKey')) define('awsSecretKey', 'change-this'); +// File to upload, we'll use this file since it exists +list($uploadFile) = get_included_files(); -// Check for CURL -if (!extension_loaded('curl') && !@dl(PHP_SHLIB_SUFFIX == 'so' ? 'curl.so' : 'php_curl.dll')) - exit("\nERROR: CURL extension not loaded\n\n"); - -// Pointless without your keys! -if (awsAccessKey == 'change-this' || awsSecretKey == 'change-this') - exit("\nERROR: AWS access information required\n\nPlease edit the following lines in this file:\n\n". - "define('awsAccessKey', 'change-me');\ndefine('awsSecretKey', 'change-me');\n\n"); - - -S3::setAuth(awsAccessKey, awsSecretKey); +// Initialise S3 +S3::Init( + new S3Credentials(_getenv('ACCESS_KEY'), _getenv('SECRET_KEY')), + _getenv('REGION', 'us-west-1') +); -$bucket = 'upload-bucket'; +$bucket = _getenv('BUCKET_NAME', 'upload-bucket'); $path = 'myfiles/'; // Can be empty '' $lifetime = 3600; // Period for which the parameters are valid diff --git a/example-helpers.php b/example-helpers.php new file mode 100644 index 00000000..4d2bdf76 --- /dev/null +++ b/example-helpers.php @@ -0,0 +1,25 @@ +__getURL($path); return (($info = self::getObjectInfo($this->url['host'], $this->url['path'])) !== false) ? array('size' => $info['size'], 'mtime' => $info['time'], 'ctime' => $info['time']) : false; } public function unlink($path) { - self::__getURL($path); + $this->__getURL($path); return self::deleteObject($this->url['host'], $this->url['path']); } public function mkdir($path, $mode, $options) { - self::__getURL($path); - return self::putBucket($this->url['host'], self::__translateMode($mode)); + $this->__getURL($path); + return self::putBucket($this->url['host'], $this->__translateMode($mode)); } public function rmdir($path) { - self::__getURL($path); + $this->__getURL($path); return self::deleteBucket($this->url['host']); } public function dir_opendir($path, $options) { - self::__getURL($path); + $this->__getURL($path); if (($contents = self::getBucket($this->url['host'], $this->url['path'])) !== false) { $pathlen = strlen($this->url['path']); - if (substr($this->url['path'], -1) == '/') $pathlen++; + if (substr($this->url['path'], -1) === '/') $pathlen++; $this->buffer = array(); foreach ($contents as $file) { if ($pathlen > 0) $file['name'] = substr($file['name'], $pathlen); @@ -78,7 +64,7 @@ public function dir_closedir() { } public function stream_close() { - if ($this->mode == 'w') { + if ($this->mode === 'w') { self::putObject($this->buffer, $this->url['host'], $this->url['path']); } $this->position = 0; @@ -105,9 +91,9 @@ public function stream_flush() { public function stream_open($path, $mode, $options, &$opened_path) { if (!in_array($mode, array('r', 'rb', 'w', 'wb'))) return false; // Mode not supported $this->mode = substr($mode, 0, 1); - self::__getURL($path); + $this->__getURL($path); $this->position = 0; - if ($this->mode == 'r') { + if ($this->mode === 'r') { if (($this->buffer = self::getObject($this->url['host'], $this->url['path'])) !== false) { if (is_object($this->buffer->body)) $this->buffer->body = (string)$this->buffer->body; } else return false; @@ -142,7 +128,7 @@ public function stream_eof() { public function stream_seek($offset, $whence) { switch ($whence) { case SEEK_SET: - if ($offset < strlen($this->buffer->body) && $offset >= 0) { + if ($offset >= 0 && $offset < strlen($this->buffer->body)) { $this->position = $offset; return true; } else return false; @@ -180,16 +166,21 @@ private function __translateMode($mode) { $acl = self::ACL_PUBLIC_READ; //$acl = self::ACL_PUBLIC_READ_WRITE; return $acl; } -} stream_wrapper_register('s3', 'S3Wrapper'); +} +stream_wrapper_register('s3', 'S3Wrapper'); -################################################################################ +################################################################################ -S3::setAuth(awsAccessKey, awsSecretKey); +$bucketName = uniqid('s3test', false); // Temporary bucket -$bucketName = uniqid('s3test'); +// Initialise S3 +S3::Init( + new S3Credentials(_getenv('ACCESS_KEY'), _getenv('SECRET_KEY')), + _getenv('REGION', 'us-west-1') +); echo "Creating bucket: {$bucketName}\n"; var_dump(mkdir("s3://{$bucketName}")); diff --git a/example.php b/example.php index d6c90dd6..6ea579a3 100755 --- a/example.php +++ b/example.php @@ -6,98 +6,82 @@ * S3 class usage */ -if (!class_exists('S3')) require_once 'S3.php'; +require_once 'vendor/autoload.php'; -// AWS access info -if (!defined('awsAccessKey')) define('awsAccessKey', 'change-this'); -if (!defined('awsSecretKey')) define('awsSecretKey', 'change-this'); +// File to upload, we'll use this file since it exists +list($uploadFile) = get_included_files(); -$uploadFile = dirname(__FILE__).'/S3.php'; // File to upload, we'll use the S3 class since it exists -$bucketName = uniqid('s3test'); // Temporary bucket +$bucketName = uniqid('s3test', false); // Temporary bucket -// If you want to use PECL Fileinfo for MIME types: -//if (!extension_loaded('fileinfo') && @dl('fileinfo.so')) $_ENV['MAGIC'] = '/usr/share/file/magic'; - - -// Check if our upload file exists -if (!file_exists($uploadFile) || !is_file($uploadFile)) - exit("\nERROR: No such file: $uploadFile\n\n"); - -// Check for CURL -if (!extension_loaded('curl') && !@dl(PHP_SHLIB_SUFFIX == 'so' ? 'curl.so' : 'php_curl.dll')) - exit("\nERROR: CURL extension not loaded\n\n"); - -// Pointless without your keys! -if (awsAccessKey == 'change-this' || awsSecretKey == 'change-this') - exit("\nERROR: AWS access information required\n\nPlease edit the following lines in this file:\n\n". - "define('awsAccessKey', 'change-me');\ndefine('awsSecretKey', 'change-me');\n\n"); - -// Instantiate the class -$s3 = new S3(awsAccessKey, awsSecretKey); +// Initialise S3 +S3::Init( + new S3Credentials(_getenv('ACCESS_KEY'), _getenv('SECRET_KEY')), + _getenv('REGION', 'us-west-1') +); // List your buckets: -echo "S3::listBuckets(): ".print_r($s3->listBuckets(), 1)."\n"; +echo "S3::listBuckets(): ".print_r(S3::listBuckets(), 1)."\n"; // Create a bucket with public read access -if ($s3->putBucket($bucketName, S3::ACL_PUBLIC_READ)) { +if (S3::putBucket($bucketName, S3::ACL_PUBLIC_READ)) { echo "Created bucket {$bucketName}".PHP_EOL; // Put our file (also with public read access) - if ($s3->putObjectFile($uploadFile, $bucketName, baseName($uploadFile), S3::ACL_PUBLIC_READ)) { + if (S3::putObjectFile($uploadFile, $bucketName, baseName($uploadFile), S3::ACL_PUBLIC_READ)) { echo "S3::putObjectFile(): File copied to {$bucketName}/".baseName($uploadFile).PHP_EOL; // Get the contents of our bucket - $contents = $s3->getBucket($bucketName); + $contents = S3::getBucket($bucketName); echo "S3::getBucket(): Files in bucket {$bucketName}: ".print_r($contents, 1); // Get object info - $info = $s3->getObjectInfo($bucketName, baseName($uploadFile)); + $info = S3::getObjectInfo($bucketName, baseName($uploadFile)); echo "S3::getObjectInfo(): Info for {$bucketName}/".baseName($uploadFile).': '.print_r($info, 1); // You can also fetch the object into memory - // var_dump("S3::getObject() to memory", $s3->getObject($bucketName, baseName($uploadFile))); + // var_dump("S3::getObject() to memory", S3::getObject($bucketName, baseName($uploadFile))); // Or save it into a file (write stream) - // var_dump("S3::getObject() to savefile.txt", $s3->getObject($bucketName, baseName($uploadFile), 'savefile.txt')); + // var_dump("S3::getObject() to savefile.txt", S3::getObject($bucketName, baseName($uploadFile), 'savefile.txt')); // Or write it to a resource (write stream) - // var_dump("S3::getObject() to resource", $s3->getObject($bucketName, baseName($uploadFile), fopen('savefile.txt', 'wb'))); + // var_dump("S3::getObject() to resource", S3::getObject($bucketName, baseName($uploadFile), fopen('savefile.txt', 'wb'))); // Get the access control policy for a bucket: - // $acp = $s3->getAccessControlPolicy($bucketName); + // $acp = S3::getAccessControlPolicy($bucketName); // echo "S3::getAccessControlPolicy(): {$bucketName}: ".print_r($acp, 1); // Update an access control policy ($acp should be the same as the data returned by S3::getAccessControlPolicy()) - // $s3->setAccessControlPolicy($bucketName, '', $acp); - // $acp = $s3->getAccessControlPolicy($bucketName); + // S3::setAccessControlPolicy($bucketName, '', $acp); + // $acp = S3::getAccessControlPolicy($bucketName); // echo "S3::getAccessControlPolicy(): {$bucketName}: ".print_r($acp, 1); // Enable logging for a bucket: - // $s3->setBucketLogging($bucketName, 'logbucket', 'prefix'); + // S3::setBucketLogging($bucketName, 'logbucket', 'prefix'); - // if (($logging = $s3->getBucketLogging($bucketName)) !== false) { + // if (($logging = S3::getBucketLogging($bucketName)) !== false) { // echo "S3::getBucketLogging(): Logging for {$bucketName}: ".print_r($contents, 1); // } else { // echo "S3::getBucketLogging(): Logging for {$bucketName} not enabled\n"; // } // Disable bucket logging: - // var_dump($s3->disableBucketLogging($bucketName)); + // var_dump(S3::disableBucketLogging($bucketName)); // Delete our file - if ($s3->deleteObject($bucketName, baseName($uploadFile))) { + if (S3::deleteObject($bucketName, baseName($uploadFile))) { echo "S3::deleteObject(): Deleted file\n"; // Delete the bucket we created (a bucket has to be empty to be deleted) - if ($s3->deleteBucket($bucketName)) { + if (S3::deleteBucket($bucketName)) { echo "S3::deleteBucket(): Deleted bucket {$bucketName}\n"; } else { echo "S3::deleteBucket(): Failed to delete bucket (it probably isn't empty)\n";