Version 4.1.19

This commit is contained in:
Neo committed 2025-12-19 05:38:56 -08:00
1 parent 28bd025b35
commit 2bd5025018
2674 files changed
+233518 -77766

No files matched your search

+373 -76
View File
@@ -2,9 +2,9 @@
/**
* @brief File Handler: Amazon S3
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - SVN_YYYY Invision Power Services, Inc.
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Social Suite
* @package IPS Community Suite
* @since 12 Jul 2013
* @version SVN_VERSION_NUMBER
*/
@@ -44,6 +44,7 @@ class _Amazon extends \IPS\File
return array(
'bucket' => 'Text',
'endpoint' => array( 'type' => 'Text', 'default' => 's3.amazonaws.com' ),
'bucket_path' => 'Text',
'access_key' => 'Text',
'secret_key' => 'Text',
@@ -53,6 +54,11 @@ class _Amazon extends \IPS\File
'custom_url' => array( 'type' => 'Text', 'default' => '' )
);
}
/**
* @brief Temporarily stored endpoint - when testing settings, we may need to update it automagically
*/
protected static $updatedEndpoint = NULL;
/**
* Test Settings
@@ -88,6 +94,12 @@ class _Amazon extends \IPS\File
{
throw new \DomainException( \IPS\Member::loggedIn()->language()->addToStack( 'file_storage_test_error_amazon_d403', FALSE, array( 'sprintf' => array( $values['bucket'], $response->httpResponseCode ) ) ) );
}
if( static::$updatedEndpoint !== NULL )
{
$values['endpoint'] = static::$updatedEndpoint;
static::$updatedEndpoint = NULL;
}
if ( ! $values['toggle'] )
{
@@ -168,22 +180,48 @@ class _Amazon extends \IPS\File
{
if ( $this->storageExtension and ! array_key_exists( $this->storageExtension, static::$gzipExtensions ) )
{
$bits = explode( '_', $this->storageExtension );
$class = '\IPS\\' . $bits[0] . '\extensions\core\FileStorage\\' . $bits[1];
if ( isset( $class::$storeGzipExtensions ) and is_array( $class::$storeGzipExtensions ) and count( $class::$storeGzipExtensions ) )
static::$gzipExtensions[ $this->storageExtension ] = array();
if( mb_strpos( $this->storageExtension, '_' ) !== FALSE )
{
static::$gzipExtensions[ $this->storageExtension ] = $class::$storeGzipExtensions;
}
else
{
static::$gzipExtensions[ $this->storageExtension ] = array();
$bits = explode( '_', $this->storageExtension );
$class = '\IPS\\' . $bits[0] . '\extensions\core\FileStorage\\' . $bits[1];
if ( isset( $class::$storeGzipExtensions ) and is_array( $class::$storeGzipExtensions ) and count( $class::$storeGzipExtensions ) )
{
static::$gzipExtensions[ $this->storageExtension ] = $class::$storeGzipExtensions;
}
}
}
return $this->storageExtension ? static::$gzipExtensions[ $this->storageExtension ] : array();
}
/**
* Is this a private file?
* This means that it is PUT with bucket owner read-only permissions which means it needs a signed URL to download
*
* @return boolean
*/
public function isPrivate()
{
if ( $this->storageExtension )
{
if ( mb_strpos( $this->storageExtension, '_' ) !== FALSE )
{
$bits = explode( '_', $this->storageExtension );
$class = '\IPS\\' . $bits[0] . '\extensions\core\FileStorage\\' . $bits[1];
if ( isset( $class::$isPrivate ) )
{
return $class::$isPrivate;
}
}
}
return false;
}
/**
* AWS does not gzip content when serving it, so if we want gzip compressed JS and CSS, we need to store a copy ourselves.
*
@@ -202,7 +240,7 @@ class _Amazon extends \IPS\File
*/
public function baseUrl()
{
return preg_replace( '#^http(s)?://#', '//', ( empty( $this->configuration['custom_url'] ) ) ? rtrim( static::buildBaseUrl( $this->configuration ), '/' ) : $this->configuration['custom_url'] );
return preg_replace( '#^http(s)?://#', '//', rtrim( ( empty( $this->configuration['custom_url'] ) ) ? static::buildBaseUrl( $this->configuration ) : $this->configuration['custom_url'], '/' ) );
}
/**
@@ -229,21 +267,42 @@ class _Amazon extends \IPS\File
public function save()
{
$this->container = trim( $this->container, '/' );
$this->url = $this->baseUrl() . "/{$this->container}/{$this->filename}";
$response = static::makeRequest( "{$this->container}/{$this->filename}", 'PUT', $this->configuration, $this->configurationId, (string) $this->contents() );
$this->url = $this->baseUrl() . ( $this->container ? "/{$this->container}" : '' ) . "/{$this->filename}";
$path = $this->container ? "{$this->container}/{$this->filename}" : "{$this->filename}";
$response = static::makeRequest( $path, 'PUT', $this->configuration, $this->configurationId, (string) $this->contents(), $this->storageExtension, FALSE, $this->isPrivate() );
if ( $response->httpResponseCode != 200 )
{
try
{
$xml = $response->decodeXml();
if ( isset( $xml->Code ) and isset( $xml->Message ) )
{
\IPS\Log::log( $xml->Code . ' ' . $xml->Message . ' ' . $path, 'Amazon' );
}
}
catch( \Exception $e ) { }
throw new \RuntimeException('COULD_NOT_SAVE_FILE');
}
/* Write the gzip version */
if ( $this->needsGzipVersion() )
{
$response = static::makeRequest( "{$this->container}/{$this->filename}.gz", 'PUT', $this->configuration, $this->configurationId, gzencode( (string) $this->contents() ) );
$response = static::makeRequest( "{$path}.gz", 'PUT', $this->configuration, $this->configurationId, gzencode( (string) $this->contents() ) );
if ( $response->httpResponseCode != 200 )
{
try
{
$xml = $response->decodeXml();
if ( isset( $xml->Code ) and isset( $xml->Message ) )
{
\IPS\Log::log( $xml->Code . ' ' . $xml->Message . ' ' . $path, 'Amazon' );
}
}
catch( \Exception $e ) { }
throw new \RuntimeException('COULD_NOT_SAVE_FILE');
}
}
@@ -259,7 +318,7 @@ class _Amazon extends \IPS\File
{
if ( $this->contents === NULL or $refresh === TRUE )
{
$this->contents = (string) static::makeRequest( "{$this->container}/{$this->filename}", 'GET', $this->configuration, $this->configurationId );
$this->contents = (string) static::makeRequest( $this->container ? "{$this->container}/{$this->filename}" : "{$this->filename}", 'GET', $this->configuration, $this->configurationId );
}
return $this->contents;
}
@@ -272,25 +331,36 @@ class _Amazon extends \IPS\File
public function delete()
{
$this->container = trim( $this->container, '/' );
$path = $this->container ? "{$this->container}/{$this->filename}" : "{$this->filename}";
$response = static::makeRequest( "{$this->container}/{$this->filename}", 'DELETE', $this->configuration, $this->configurationId );
if ( $response->httpResponseCode == 204 )
try
{
/* Got a gzip version? */
if ( $this->needsGzipVersion() )
$response = static::makeRequest( $path, 'DELETE', $this->configuration, $this->configurationId );
/* Log deletion request */
$this->log( "file_deletion", 'delete', NULL, 'log' );
if ( $response->httpResponseCode == 204 )
{
static::makeRequest( "{$this->container}/{$this->filename}.gz", 'DELETE', $this->configuration, $this->configurationId );
/* Got a gzip version? */
if ( $this->needsGzipVersion() )
{
static::makeRequest( "{$path}.gz", 'DELETE', $this->configuration, $this->configurationId );
}
/* Ok */
return;
}
/* Ok */
return;
if ( $response->httpResponseCode != 200 )
{
$this->log( 'COULD_NOT_DELETE_FILE', 'delete', array( $response->httpResponseCode, $response->httpResponseText ), 'error' );
}
}
if ( $response->httpResponseCode != 200 )
catch( \IPS\Http\Request\Exception $e )
{
$this->log( 'COULD_NOT_DELETE_FILE', 'delete', array( $response->httpResponseCode, $response->httpResponseText ), 'error' );
throw new \RuntimeException('COULD_NOT_DELETE_FILE');
/* If there was a problem deleting the file, don't stop code execution just because of that */
$this->log( 'HTTP_ERROR_DELETE_FILE', 'delete', array( $e->getCode(), $e->getMessage() ), 'error' );
}
}
@@ -324,8 +394,19 @@ class _Amazon extends \IPS\File
static::makeRequest( $result->Key, 'DELETE', $this->configuration, $this->configurationId );
}
/* Log deletion request */
$realContainer = $this->container;
$this->container = $container;
$this->log( "container_deletion", 'delete', NULL, 'log' );
$this->container = $realContainer;
}
/**
* @brief Cached filesize
*/
protected $_cachedFilesize = NULL;
/**
* Get filesize (in bytes)
*
@@ -333,20 +414,67 @@ class _Amazon extends \IPS\File
*/
public function filesize()
{
if( $this->_cachedFilesize !== NULL )
{
return $this->_cachedFilesize;
}
$this->container = trim( $this->container, '/' );
$response = static::makeRequest( "{$this->container}/{$this->filename}", 'HEAD', $this->configuration, $this->configurationId );
$response = static::makeRequest( $this->container ? "{$this->container}/{$this->filename}" : "{$this->filename}", 'HEAD', $this->configuration, $this->configurationId );
if ( $response->httpResponseCode != 200 OR !isset( $response->httpHeaders['Content-Length'] ) )
{
return parent::filesize();
}
return $response->httpHeaders['Content-Length'];
$this->_cachedFilesize = $response->httpHeaders['Content-Length'];
return $this->_cachedFilesize;
}
/* !Amazon Utility Methods */
/**
* Generate a temporary download URL the user can be redirected to
*
* @param $validForSeconds int The number of seconds the link should be valid for
* @return \IPS\Http\Url
*/
public function generateTemporaryDownloadUrl( $validForSeconds = 1200 )
{
$fileUrl = ( $this->container ? ( rawurlencode( $this->container ) . '/' . rawurlencode( $this->filename ) ) : rawurlencode( $this->filename ) );
$url = \IPS\Http\Url::external( static::buildBaseUrl( $this->configuration ) . $fileUrl );
$headers = array();
$queryString = array(
'X-Amz-Expires' => $validForSeconds,
'response-content-disposition' => 'attachment; filename*=UTF-8\'\'' . rawurlencode( $this->originalFilename )
);
$signature = $this->signature( $this->configuration, 'GET', $fileUrl, $headers, $queryString, NULL, TRUE );
$queryString['X-Amz-Signature'] = $signature;
$url = $url->setQueryString( $queryString );
$response = $url->request()->head();
if ( $response->httpResponseCode == 400 )
{
$xml = $url->request()->get()->decodeXml();
if ( !isset( $xml->Region ) )
{
throw new \RuntimeException;
}
$this->configuration['region'] = (string) $xml->Region;
\IPS\Db::i()->update( 'core_file_storage', array( 'configuration' => json_encode( $this->configuration ) ), array( 'id=?', $this->configurationId ) );
unset( \IPS\Data\Store::i()->storageConfigurations );
return $this->generateTemporaryDownloadUrl( $validForSeconds );
}
return $url;
}
/**
* Sign and make request
*
@@ -355,13 +483,19 @@ class _Amazon extends \IPS\File
* @param array $configuration The configuration for this instance
* @param int $configurationId The configuration ID
* @param string|null $content The content to send
* @param bool $skipExtraChecks Skips the endpoint check (to prevent infinite looping)
* @return \IPS\Http\Response
* @throws \IPS\Http\Request\Exception
*/
protected static function makeRequest( $uri, $verb, $configuration, $configurationId, $content=NULL )
protected static function makeRequest( $uri, $verb, $configuration, $configurationId, $content=NULL, $storageExtension=NULL, $skipExtraChecks=FALSE, $isPrivate=false )
{
/* Amazon requires filename characters to be properly encoded - let's urlencode the filename here */
$uriPieces = explode( '/', $uri );
$filename = array_pop( $uriPieces );
$uri = ltrim( implode( '/', $uriPieces ) . '/' . rawurlencode( $filename ), '/' );
/* Build a request */
$request = \IPS\Http\Url::external( static::buildBaseUrl( $configuration ) . $uri )->request();
$request = \IPS\Http\Url::external( static::buildBaseUrl( $configuration ) . $uri )->request( \IPS\LONG_REQUEST_TIMEOUT, NULL, FALSE ); # Amazon will send a 301 header code, but no Location header, if we need to try another endpoint
/* When using virtual hosted–style buckets with SSL, the SSL wild card certificate only matches buckets that do not contain periods. To work around this, use HTTP or write your own certificate verification logic. @link http://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html */
if ( \IPS\Request::i()->isSecure() and mb_strstr( $configuration['bucket'], '.' ) )
@@ -369,75 +503,220 @@ class _Amazon extends \IPS\File
$request->sslCheck( FALSE );
}
$date = date('r');
/* Make sure the file has the correct mime type, even if it is gzipped */
/* Set headers. Make sure the file has the correct mime type, even if it is gzipped */
$mimeUri = ( mb_substr( $uri, -3 ) === '.gz' ) ? mb_substr( $uri, 0, -3 ) : $uri;
$headers = array( 'Date' => $date, 'Content-Type' => \IPS\File::getMimeType( $mimeUri ), 'Content-MD5' => base64_encode( md5( $content, TRUE ) ), 'x-amz-acl' => 'public-read' );
$headers = array(
'Content-Type' => \IPS\File::getMimeType( $mimeUri ),
'Content-MD5' => base64_encode( md5( $content, TRUE ) ),
'X-Amz-Acl' => ( $isPrivate ? 'bucket-owner-read' : 'public-read' )
);
if ( $mimeUri !== $uri )
{
$headers['Content-Encoding'] = 'gzip';
}
/* If uploading a file, need to specify length and cache control */
if( mb_strtoupper( $verb ) === 'PUT' )
{
$headers['Content-Length'] = \strlen( $content );
$cacheSeconds = 3600;
$ext = mb_substr( $mimeUri, ( mb_strrpos( $mimeUri, '.' ) + 1 ) );
if ( preg_match( "/\." . preg_quote( $ext, '/' ) . "\.([a-zA-Z0-9]{32})\." . preg_quote( $ext, '/' ) . "$/", $mimeUri ) )
{
/* This is an obfuscated file, so it will change each time the file is updated so it is safe to cache for longer */
$cacheSeconds = 3600 * 24 * 365;
}
/* Custom Cache-Control */
if( $storageExtension !== NULL AND mb_strpos( $storageExtension, '_' ) !== FALSE )
{
$bits = explode( '_', $storageExtension );
$class = '\IPS\\' . $bits[0] . '\extensions\core\FileStorage\\' . $bits[1];
if ( isset( $class::$cacheControlTtl ) and $class::$cacheControlTtl )
{
$cacheSeconds = $class::$cacheControlTtl;
}
}
$headers['Cache-Control'] = 'max-age=' . $cacheSeconds;
$headers['Expires'] = date( 'r', time() + $cacheSeconds );
}
$cacheSeconds = 3600;
$ext = mb_substr( $mimeUri, ( mb_strrpos( $mimeUri, '.' ) + 1 ) );
if ( preg_match( "/\." . preg_quote( $ext ) . "\.([a-zA-Z0-9]{32})\." . preg_quote( $ext ) . "$/", $mimeUri ) )
{
/* This is an obfuscated file, so it will change each time the file is updated so it is safe to cache for longer */
$cacheSeconds = 3600 * 24 * 365;
}
$headers['Cache-Control'] = 'max-age=' . $cacheSeconds;
$headers['Expires'] = date( 'r', time() + $cacheSeconds );
/* We need to strip query string parameters for the signature, but not always (e.g. a subresource such as ?acl needs to be included and multi-
object delete requests must include the query string params). Let the callee decide to do this or not. */
if( isset( $configuration['_strip_querystring'] ) AND $configuration['_strip_querystring'] === TRUE )
{
$uri = preg_replace( "/^(.*?)\?.*$/", "$1", $uri );
}
$string = implode( "\n", array(
mb_strtoupper( $verb ),
$headers['Content-MD5'],
$headers['Content-Type'],
$date,
"x-amz-acl:{$headers['x-amz-acl']}",
"/{$configuration['bucket']}" . ( ( isset( $configuration['bucket_path'] ) AND ! empty( $configuration['bucket_path'] ) ) ? '/' . trim( $configuration['bucket_path'], '/' ) . '/' : '/' ) . $uri
) );
/* Build a signature */
$signature = base64_encode( hash_hmac( 'sha1', $string, $configuration['secret_key'], true ) );
/* Sign the request */
$headers['Authorization'] = "AWS {$configuration['access_key']}:{$signature}";
$queryString = array();
$authorization = static::signature( $configuration, $verb, $uri, $headers, $queryString, $content );
$headers['Authorization'] = $authorization;
unset( $headers['Host'] );
$request->setHeaders( $headers );
/* Make the request */
$verb = mb_strtolower( $verb );
$response = $request->$verb( $content );
/* Need a different region? */
if ( $response->httpResponseCode == 307 AND $configurationId )
/* If we are skipping extra checks, return response now */
if( $skipExtraChecks )
{
return $response;
}
/* Change endpoint if necessary */
if ( $response->httpResponseCode == 301 )
{
$xml = $response->decodeXml();
$configuration['region'] = mb_substr( $xml->Endpoint, mb_strlen( $configuration['bucket'] ) + 1, -mb_strlen( '.amazonaws.com' ) );
\IPS\Db::i()->update( 'core_file_storage', array( 'configuration' => json_encode( $configuration ) ), array( 'id=?', $configurationId ) );
unset( \IPS\Data\Store::i()->storageConfigurations );
return static::makeRequest( $uri, $verb, $configuration, $configurationId, $content );
if ( isset( $xml->Endpoint ) )
{
/* We have an endpoint, but if we called s3.amazonaws.com then it might be wrong. Try to detect the correct one. */
$configuration['endpoint'] = 's3-us-west-1.amazonaws.com';
$endpointResponse = static::makeRequest( $uri, $verb, $configuration, $configurationId, $content, NULL, TRUE );
$update = FALSE;
/* If the response code is 200, we got lucky and that's our endpoint */
if( $endpointResponse->httpResponseCode == 200 )
{
$update = TRUE;
}
/* If it's a 301 response, we should be able to pull out the correct endpoint now */
elseif( $endpointResponse->httpResponseCode == 301 )
{
$xml = $endpointResponse->decodeXml();
if ( isset( $xml->Endpoint ) )
{
/* Strip out the bucket from the endpoint */
$configuration['endpoint'] = preg_replace( '/^' . preg_quote( $configuration['bucket'], '/' ) . '\./', '', (string) $xml->Endpoint );
$update = TRUE;
}
}
/* If we need to update, do it now and return the result */
if( $update === TRUE )
{
static::$updatedEndpoint = $configuration['endpoint'];
if ( $configurationId )
{
\IPS\Db::i()->update( 'core_file_storage', array( 'configuration' => json_encode( $configuration ) ), array( "id=?", $configurationId ) );
unset( \IPS\Data\Store::i()->storageConfigurations );
}
}
return static::makeRequest( $uri, $verb, $configuration, $configurationId, $content );
}
}
/* Change region if necessary */
if ( $response->httpResponseCode == 400 )
{
try
{
$xml = $response->decodeXml();
if ( isset( $xml->Region ) )
{
$configuration['region'] = (string) $xml->Region;
if ( $configurationId )
{
\IPS\Db::i()->update( 'core_file_storage', array( 'configuration' => json_encode( $configuration ) ), array( 'id=?', $configurationId ) );
unset( \IPS\Data\Store::i()->storageConfigurations );
}
return static::makeRequest( $uri, $verb, $configuration, $configurationId, $content );
}
}
catch ( \Exception $e ) { }
}
/* Return */
return $response;
return $response;
}
/**
* Generate a v4 signature
*
* @param array $configuration The configuration for this instance
* @param string $verb The HTTP verb that will be used in the request
* @param string $uri The URI (relative to the bucket)
* @param array $header The request headers as an array
* @param array $queryString The query string as an array
* @param string|null $content The content to send
* @param bool $signatureIsForQueryString If true, signature will be generated for query string. If false, header.
* @return string
*/
protected static function signature( $configuration, $verb, $uri, &$headers = array(), &$queryString = array(), $content = NULL, $signatureIsForQueryString=FALSE )
{
/* Work out some basic stuff */
$time = time();
$region = ( isset( $configuration['region'] ) ? $configuration['region'] : 'us-east-1' );
$scope = date( 'Ymd', $time ) . '/' . $region . '/s3/aws4_request';
$contentSha256 = ( $signatureIsForQueryString and !$content ) ? 'UNSIGNED-PAYLOAD' : hash( 'sha256', $content );
/* Figure out the canonical headers and query string */
$headers['Host'] = ( isset( $configuration['endpoint'] ) ? $configuration['endpoint'] : "s3.amazonaws.com" );
if ( $signatureIsForQueryString )
{
$queryString['X-Amz-Algorithm'] = 'AWS4-HMAC-SHA256';
$queryString['X-Amz-Content-Sha256'] = $contentSha256;
$queryString['X-Amz-Credential'] = $configuration['access_key'] . '/' . $scope;
$queryString['X-Amz-Date'] = gmdate( 'Ymd', $time ) . 'T' . gmdate( 'His', $time ) . 'Z';
$queryString['X-Amz-SignedHeaders'] = implode( ';', array_map( 'mb_strtolower', array_keys( $headers ) ) );
}
else
{
$headers['X-Amz-Content-Sha256'] = $contentSha256;
$headers['X-Amz-Date'] = gmdate( 'Ymd', $time ) . 'T' . gmdate( 'His', $time ) . 'Z';
}
ksort( $queryString );
ksort( $headers );
$canonicalHeadersAsString = '';
foreach ( $headers as $k => $v )
{
$canonicalHeadersAsString .= mb_strtolower( $k ) . ':' . trim( $v ) . "\n";
}
/* Task 1: Create a Canonical Request */
$canonicalRequest = implode( "\n", array(
mb_strtoupper( $verb ),
'/' . $configuration['bucket'] . static::bucketPath( $configuration ) . '/' . ltrim( $uri, '/' ),
http_build_query( $queryString, '', '&', PHP_QUERY_RFC3986 ),
$canonicalHeadersAsString,
implode( ';', array_map( 'mb_strtolower', array_keys( $headers ) ) ),
$contentSha256
) );
/* Task 2: Create a String to Sign */
$stringToSign = implode( "\n", array(
'AWS4-HMAC-SHA256',
gmdate( 'Ymd', $time ) . 'T' . gmdate( 'His', $time ) . 'Z',
$scope,
hash( 'sha256', $canonicalRequest )
) );
/* Task 3: Calculate Signature */
$dateKey = hash_hmac( 'sha256', date( 'Ymd', $time ), 'AWS4' . $configuration['secret_key'], true );
$dateRegionKey = hash_hmac( 'sha256', $region, $dateKey, true );
$dateRegionServiceKey = hash_hmac( 'sha256', 's3', $dateRegionKey, true );
$signingKey = hash_hmac( 'sha256', 'aws4_request', $dateRegionServiceKey, true );
/* Return */
$signature = hash_hmac( 'sha256', $stringToSign, $signingKey );
if ( $signatureIsForQueryString )
{
return $signature;
}
else
{
return "AWS4-HMAC-SHA256 Credential={$configuration['access_key']}/{$scope},SignedHeaders=" . implode( ';', array_map( 'mb_strtolower', array_keys( $headers ) ) ) . ",Signature={$signature}";
}
}
/**
* Build up the base Amazon URL
* @param array $configuration Configuration data
@@ -446,11 +725,29 @@ class _Amazon extends \IPS\File
public static function buildBaseUrl( $configuration )
{
return (
\IPS\Request::i()->isSecure() ? "https" : "http" )
. "://{$configuration['bucket']}"
. ( ( isset( $configuration['region'] ) AND ! empty( $configuration['region'] ) ) ? ".{$configuration['region']}" : '.s3' )
. ".amazonaws.com"
. ( ( isset( $configuration['bucket_path'] ) AND ! empty( $configuration['bucket_path'] ) ) ? '/' . trim( $configuration['bucket_path'], '/' ) . '/' : '/' );
\IPS\Request::i()->isSecure() ? "https" : "http" ) . "://"
. ( isset( $configuration['endpoint'] ) ? $configuration['endpoint'] : "s3.amazonaws.com" )
. "/{$configuration['bucket']}"
. static::bucketPath( $configuration )
. '/';
}
/**
* Get bucket path
*
* @param array $configuration Configuration data
* @return string
*/
protected static function bucketPath( $configuration )
{
if ( isset( $configuration['bucket_path'] ) AND ! empty( $configuration['bucket_path'] ) )
{
$bucketPath = trim( $configuration['bucket_path'], '/' );
$bucketPath = rawurlencode( $bucketPath ); // The bucket path needs to be mostly url-encoded
$bucketPath = str_replace( '%2F', '/', $bucketPath ); // Except for slashes because it can be multiple-levels deep
return "/{$bucketPath}";
}
return '';
}
/**