mirror of
https://git.friendi.ca/friendica/friendica-addons.git
synced 2025-07-11 10:58:48 +00:00
Add S3 Storage Backend
This commit is contained in:
parent
95fcf98759
commit
9c4b12f868
63 changed files with 8108 additions and 0 deletions
31
s3_storage/vendor/akeeba/s3/src/Acl.php
vendored
Normal file
31
s3_storage/vendor/akeeba/s3/src/Acl.php
vendored
Normal file
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
*
|
||||
* @package akeebaengine
|
||||
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Postproc\Connector\S3v4;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
/**
|
||||
* Shortcuts to often used access control privileges
|
||||
*/
|
||||
class Acl
|
||||
{
|
||||
const ACL_PRIVATE = 'private';
|
||||
|
||||
const ACL_PUBLIC_READ = 'public-read';
|
||||
|
||||
const ACL_PUBLIC_READ_WRITE = 'public-read-write';
|
||||
|
||||
const ACL_AUTHENTICATED_READ = 'authenticated-read';
|
||||
|
||||
const ACL_BUCKET_OWNER_READ = 'bucket-owner-read';
|
||||
|
||||
const ACL_BUCKET_OWNER_FULL_CONTROL = 'bucket-owner-full-control';
|
||||
}
|
366
s3_storage/vendor/akeeba/s3/src/Configuration.php
vendored
Normal file
366
s3_storage/vendor/akeeba/s3/src/Configuration.php
vendored
Normal file
|
@ -0,0 +1,366 @@
|
|||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
*
|
||||
* @package akeebaengine
|
||||
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Postproc\Connector\S3v4;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
/**
|
||||
* Holds the Amazon S3 confiugration credentials
|
||||
*/
|
||||
class Configuration
|
||||
{
|
||||
/**
|
||||
* Access Key
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $access = '';
|
||||
|
||||
/**
|
||||
* Secret Key
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $secret = '';
|
||||
|
||||
/**
|
||||
* Security token. This is only required with temporary credentials provisioned by an EC2 instance.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $token = '';
|
||||
|
||||
/**
|
||||
* Signature calculation method ('v2' or 'v4')
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signatureMethod = 'v2';
|
||||
|
||||
/**
|
||||
* AWS region, used for v4 signatures
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $region = 'us-east-1';
|
||||
|
||||
/**
|
||||
* Should I use SSL (HTTPS) to communicate to Amazon S3?
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $useSSL = true;
|
||||
|
||||
/**
|
||||
* Should I use SSL (HTTPS) to communicate to Amazon S3?
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $useDualstackUrl = false;
|
||||
|
||||
/**
|
||||
* Should I use legacy, path-style access to the bucket? When it's turned off (default) we use virtual hosting style
|
||||
* paths which are RECOMMENDED BY AMAZON per http://docs.aws.amazon.com/AmazonS3/latest/API/APIRest.html
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $useLegacyPathStyle = false;
|
||||
|
||||
/**
|
||||
* Amazon S3 endpoint. You can use a custom endpoint with v2 signatures to access third party services which offer
|
||||
* S3 compatibility, e.g. OwnCloud, Google Storage etc.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $endpoint = 's3.amazonaws.com';
|
||||
|
||||
/**
|
||||
* Public constructor
|
||||
*
|
||||
* @param string $access Amazon S3 Access Key
|
||||
* @param string $secret Amazon S3 Secret Key
|
||||
* @param string $signatureMethod Signature method (v2 or v4)
|
||||
* @param string $region Region, only required for v4 signatures
|
||||
*/
|
||||
function __construct(string $access, string $secret, string $signatureMethod = 'v2', string $region = '')
|
||||
{
|
||||
$this->setAccess($access);
|
||||
$this->setSecret($secret);
|
||||
$this->setSignatureMethod($signatureMethod);
|
||||
$this->setRegion($region);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Amazon access key
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getAccess(): string
|
||||
{
|
||||
return $this->access;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the Amazon access key
|
||||
*
|
||||
* @param string $access The access key to set
|
||||
*
|
||||
* @throws Exception\InvalidAccessKey
|
||||
*/
|
||||
public function setAccess(string $access): void
|
||||
{
|
||||
if (empty($access))
|
||||
{
|
||||
throw new Exception\InvalidAccessKey;
|
||||
}
|
||||
|
||||
$this->access = $access;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Amazon secret key
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getSecret(): string
|
||||
{
|
||||
return $this->secret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the Amazon secret key
|
||||
*
|
||||
* @param string $secret The secret key to set
|
||||
*
|
||||
* @throws Exception\InvalidSecretKey
|
||||
*/
|
||||
public function setSecret(string $secret): void
|
||||
{
|
||||
if (empty($secret))
|
||||
{
|
||||
throw new Exception\InvalidSecretKey;
|
||||
}
|
||||
|
||||
$this->secret = $secret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the security token. Only for temporary credentials provisioned through an EC2 instance.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getToken(): string
|
||||
{
|
||||
return $this->token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the security token. Only for temporary credentials provisioned through an EC2 instance.
|
||||
*
|
||||
* @param string $token
|
||||
*/
|
||||
public function setToken(string $token): void
|
||||
{
|
||||
$this->token = $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the signature method to use
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getSignatureMethod(): string
|
||||
{
|
||||
return $this->signatureMethod;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the signature method to use
|
||||
*
|
||||
* @param string $signatureMethod One of v2 or v4
|
||||
*
|
||||
* @throws Exception\InvalidSignatureMethod
|
||||
*/
|
||||
public function setSignatureMethod(string $signatureMethod): void
|
||||
{
|
||||
$signatureMethod = strtolower($signatureMethod);
|
||||
$signatureMethod = trim($signatureMethod);
|
||||
|
||||
if (!in_array($signatureMethod, ['v2', 'v4']))
|
||||
{
|
||||
throw new Exception\InvalidSignatureMethod;
|
||||
}
|
||||
|
||||
// If you switch to v2 signatures we unset the region.
|
||||
if ($signatureMethod == 'v2')
|
||||
{
|
||||
$this->setRegion('');
|
||||
|
||||
/**
|
||||
* If we are using Amazon S3 proper (not a custom endpoint) we have to set path style access to false.
|
||||
* Amazon S3 does not support v2 signatures with path style access at all (it returns an error telling
|
||||
* us to use the virtual hosting endpoint BUCKETNAME.s3.amazonaws.com).
|
||||
*/
|
||||
if (strpos($this->endpoint, 'amazonaws.com') !== false)
|
||||
{
|
||||
$this->setUseLegacyPathStyle(false);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
$this->signatureMethod = $signatureMethod;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Amazon S3 region
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getRegion(): string
|
||||
{
|
||||
return $this->region;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the Amazon S3 region
|
||||
*
|
||||
* @param string $region
|
||||
*/
|
||||
public function setRegion(string $region): void
|
||||
{
|
||||
/**
|
||||
* You can only leave the region empty if you're using v2 signatures. Anything else gets you an exception.
|
||||
*/
|
||||
if (empty($region) && ($this->signatureMethod == 'v4'))
|
||||
{
|
||||
throw new Exception\InvalidRegion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setting a Chinese-looking region force-changes the endpoint but ONLY if you were using the original Amazon S3
|
||||
* endpoint. If you're using a custom endpoint and provide a region with 'cn-' in its name we don't override
|
||||
* your custom endpoint.
|
||||
*/
|
||||
if (($this->endpoint == 's3.amazonaws.com') && (substr($region, 0, 3) == 'cn-'))
|
||||
{
|
||||
$this->setEndpoint('amazonaws.com.cn');
|
||||
}
|
||||
|
||||
$this->region = $region;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the connection to be made over HTTPS?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isSSL(): bool
|
||||
{
|
||||
return $this->useSSL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the connection SSL preference
|
||||
*
|
||||
* @param bool $useSSL True to use HTTPS
|
||||
*/
|
||||
public function setSSL(bool $useSSL): void
|
||||
{
|
||||
$this->useSSL = $useSSL ? true : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Amazon S3 endpoint
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getEndpoint(): string
|
||||
{
|
||||
return $this->endpoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the Amazon S3 endpoint. Do NOT use a protocol
|
||||
*
|
||||
* @param string $endpoint Custom endpoint, e.g. 's3.example.com' or 'www.example.com/s3api'
|
||||
*/
|
||||
public function setEndpoint(string $endpoint): void
|
||||
{
|
||||
if (stristr($endpoint, '://'))
|
||||
{
|
||||
throw new Exception\InvalidEndpoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* If you set a custom endpoint we have to switch to v2 signatures since our v4 implementation only supports
|
||||
* Amazon endpoints.
|
||||
*/
|
||||
if ((strpos($endpoint, 'amazonaws.com') === false))
|
||||
{
|
||||
$this->setSignatureMethod('v2');
|
||||
}
|
||||
|
||||
$this->endpoint = $endpoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should I use legacy, path-style access to the bucket? You should only use it with custom endpoints. Amazon itself
|
||||
* is currently deprecating support for path-style access but has extended the migration date to an unknown
|
||||
* time https://aws.amazon.com/blogs/aws/amazon-s3-path-deprecation-plan-the-rest-of-the-story/
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function getUseLegacyPathStyle(): bool
|
||||
{
|
||||
return $this->useLegacyPathStyle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the flag for using legacy, path-style access to the bucket
|
||||
*
|
||||
* @param bool $useLegacyPathStyle
|
||||
*/
|
||||
public function setUseLegacyPathStyle(bool $useLegacyPathStyle): void
|
||||
{
|
||||
$this->useLegacyPathStyle = $useLegacyPathStyle;
|
||||
|
||||
/**
|
||||
* If we are using Amazon S3 proper (not a custom endpoint) we have to set path style access to false.
|
||||
* Amazon S3 does not support v2 signatures with path style access at all (it returns an error telling
|
||||
* us to use the virtual hosting endpoint BUCKETNAME.s3.amazonaws.com).
|
||||
*/
|
||||
if ((strpos($this->endpoint, 'amazonaws.com') !== false) && ($this->signatureMethod == 'v2'))
|
||||
{
|
||||
$this->useLegacyPathStyle = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Should we use the dualstack URL (which will ship traffic over ipv6 in most cases). For more information on these
|
||||
* endpoints please read https://docs.aws.amazon.com/AmazonS3/latest/dev/dual-stack-endpoints.html
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function getDualstackUrl(): bool
|
||||
{
|
||||
return $this->useDualstackUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the flag for using legacy, path-style access to the bucket
|
||||
*
|
||||
* @param bool $useDualstackUrl
|
||||
*/
|
||||
public function setUseDualstackUrl(bool $useDualstackUrl): void
|
||||
{
|
||||
$this->useDualstackUrl = $useDualstackUrl;
|
||||
}
|
||||
}
|
961
s3_storage/vendor/akeeba/s3/src/Connector.php
vendored
Normal file
961
s3_storage/vendor/akeeba/s3/src/Connector.php
vendored
Normal file
|
@ -0,0 +1,961 @@
|
|||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
*
|
||||
* @package akeebaengine
|
||||
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Postproc\Connector\S3v4;
|
||||
|
||||
// Protection against direct access
|
||||
use Akeeba\Engine\Postproc\Connector\S3v4\Exception\CannotDeleteFile;
|
||||
use Akeeba\Engine\Postproc\Connector\S3v4\Exception\CannotGetBucket;
|
||||
use Akeeba\Engine\Postproc\Connector\S3v4\Exception\CannotGetFile;
|
||||
use Akeeba\Engine\Postproc\Connector\S3v4\Exception\CannotListBuckets;
|
||||
use Akeeba\Engine\Postproc\Connector\S3v4\Exception\CannotOpenFileForWrite;
|
||||
use Akeeba\Engine\Postproc\Connector\S3v4\Exception\CannotPutFile;
|
||||
use Akeeba\Engine\Postproc\Connector\S3v4\Response\Error;
|
||||
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
class Connector
|
||||
{
|
||||
/**
|
||||
* Amazon S3 configuration object
|
||||
*
|
||||
* @var Configuration
|
||||
*/
|
||||
private $configuration = null;
|
||||
|
||||
/**
|
||||
* Connector constructor.
|
||||
*
|
||||
* @param Configuration $configuration The configuration object to use
|
||||
*/
|
||||
public function __construct(Configuration $configuration)
|
||||
{
|
||||
$this->configuration = $configuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Put an object to Amazon S3, i.e. upload a file. If the object already exists it will be overwritten.
|
||||
*
|
||||
* @param Input $input Input object
|
||||
* @param string $bucket Bucket name. If you're using v4 signatures it MUST be on the region defined.
|
||||
* @param string $uri Object URI. Think of it as the absolute path of the file in the bucket.
|
||||
* @param string $acl ACL constant, by default the object is private (visible only to the uploading
|
||||
* user)
|
||||
* @param array $requestHeaders Array of request headers
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws CannotPutFile If the upload is not possible
|
||||
*/
|
||||
public function putObject(Input $input, string $bucket, string $uri, string $acl = Acl::ACL_PRIVATE, array $requestHeaders = []): void
|
||||
{
|
||||
$request = new Request('PUT', $bucket, $uri, $this->configuration);
|
||||
$request->setInput($input);
|
||||
|
||||
// Custom request headers (Content-Type, Content-Disposition, Content-Encoding)
|
||||
if (count($requestHeaders))
|
||||
{
|
||||
foreach ($requestHeaders as $h => $v)
|
||||
{
|
||||
if (strtolower(substr($h, 0, 6)) == 'x-amz-')
|
||||
{
|
||||
$request->setAmzHeader(strtolower($h), $v);
|
||||
}
|
||||
else
|
||||
{
|
||||
$request->setHeader($h, $v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($requestHeaders['Content-Type']))
|
||||
{
|
||||
$input->setType($requestHeaders['Content-Type']);
|
||||
}
|
||||
|
||||
if (($input->getSize() <= 0) || (($input->getInputType() == Input::INPUT_DATA) && (!strlen($input->getDataReference()))))
|
||||
{
|
||||
throw new CannotPutFile('Missing input parameters', 0);
|
||||
}
|
||||
|
||||
// We need to post with Content-Length and Content-Type, MD5 is optional
|
||||
$request->setHeader('Content-Type', $input->getType());
|
||||
$request->setHeader('Content-Length', $input->getSize());
|
||||
|
||||
if ($input->getMd5sum())
|
||||
{
|
||||
$request->setHeader('Content-MD5', $input->getMd5sum());
|
||||
}
|
||||
|
||||
$request->setAmzHeader('x-amz-acl', $acl);
|
||||
|
||||
$response = $request->getResponse();
|
||||
|
||||
if ($response->code !== 200)
|
||||
{
|
||||
if (!$response->error->isError())
|
||||
{
|
||||
throw new CannotPutFile("Unexpected HTTP status {$response->code}", $response->code);
|
||||
}
|
||||
|
||||
if (is_object($response->body) && ($response->body instanceof \SimpleXMLElement) && (strpos($input->getSize(), ',') === false))
|
||||
{
|
||||
// For some reason, trying to single part upload files on some hosts comes back with an inexplicable
|
||||
// error from Amazon that we need to set Content-Length:5242880,5242880 instead of
|
||||
// Content-Length:5242880 which is AGAINST Amazon's documentation. In this case we pass the header
|
||||
// 'workaround-braindead-error-from-amazon' and retry. Uh, OK?
|
||||
if (isset($response->body->CanonicalRequest))
|
||||
{
|
||||
$amazonsCanonicalRequest = (string) $response->body->CanonicalRequest;
|
||||
$lines = explode("\n", $amazonsCanonicalRequest);
|
||||
|
||||
foreach ($lines as $line)
|
||||
{
|
||||
if (substr($line, 0, 15) != 'content-length:')
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
[$junk, $stupidAmazonDefinedContentLength] = explode(":", $line);
|
||||
|
||||
if (strpos($stupidAmazonDefinedContentLength, ',') !== false)
|
||||
{
|
||||
if (!isset($requestHeaders['workaround-braindead-error-from-amazon']))
|
||||
{
|
||||
$requestHeaders['workaround-braindead-error-from-amazon'] = 'you can\'t fix stupid';
|
||||
|
||||
$this->putObject($input, $bucket, $uri, $acl, $requestHeaders);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if ($response->error->isError())
|
||||
{
|
||||
throw new CannotPutFile(
|
||||
sprintf(__METHOD__ . "(): [%s] %s\n\nDebug info:\n%s", $response->error->getCode(), $response->error->getMessage(), print_r($response->body, true))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get (download) an object
|
||||
*
|
||||
* @param string $bucket Bucket name
|
||||
* @param string $uri Object URI
|
||||
* @param string|resource|null $saveTo Filename or resource to write to
|
||||
* @param int|null $from Start of the download range, null to download the entire object
|
||||
* @param int|null $to End of the download range, null to download the entire object
|
||||
*
|
||||
* @return string|null No return if $saveTo is specified; data as string otherwise
|
||||
*
|
||||
*/
|
||||
public function getObject(string $bucket, string $uri, $saveTo = null, ?int $from = null, ?int $to = null): ?string
|
||||
{
|
||||
$request = new Request('GET', $bucket, $uri, $this->configuration);
|
||||
|
||||
$fp = null;
|
||||
|
||||
if (!is_resource($saveTo) && is_string($saveTo))
|
||||
{
|
||||
$fp = @fopen($saveTo, 'wb');
|
||||
|
||||
if ($fp === false)
|
||||
{
|
||||
throw new CannotOpenFileForWrite($saveTo);
|
||||
}
|
||||
}
|
||||
|
||||
if (is_resource($saveTo))
|
||||
{
|
||||
$fp = $saveTo;
|
||||
}
|
||||
|
||||
if (is_resource($fp))
|
||||
{
|
||||
$request->setFp($fp);
|
||||
}
|
||||
|
||||
// Set the range header
|
||||
if ((!empty($from) && !empty($to)) || (!is_null($from) && !empty($to)))
|
||||
{
|
||||
$request->setHeader('Range', "bytes=$from-$to");
|
||||
}
|
||||
|
||||
$response = $request->getResponse();
|
||||
|
||||
if (!$response->error->isError() && (($response->code !== 200) && ($response->code !== 206)))
|
||||
{
|
||||
$response->error = new Error(
|
||||
$response->code,
|
||||
"Unexpected HTTP status {$response->code}"
|
||||
);
|
||||
}
|
||||
|
||||
if ($response->error->isError())
|
||||
{
|
||||
throw new CannotGetFile(
|
||||
sprintf(__METHOD__ . "({$bucket}, {$uri}): [%s] %s\n\nDebug info:\n%s",
|
||||
$response->error->getCode(), $response->error->getMessage(), print_r($response->body, true)),
|
||||
$response->error->getCode()
|
||||
);
|
||||
}
|
||||
|
||||
if (!is_resource($fp))
|
||||
{
|
||||
return $response->body;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an object
|
||||
*
|
||||
* @param string $bucket Bucket name
|
||||
* @param string $uri Object URI
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function deleteObject(string $bucket, string $uri): void
|
||||
{
|
||||
$request = new Request('DELETE', $bucket, $uri, $this->configuration);
|
||||
$response = $request->getResponse();
|
||||
|
||||
if (!$response->error->isError() && ($response->code !== 204))
|
||||
{
|
||||
$response->error = new Error(
|
||||
$response->code,
|
||||
"Unexpected HTTP status {$response->code}"
|
||||
);
|
||||
}
|
||||
|
||||
if ($response->error->isError())
|
||||
{
|
||||
throw new CannotDeleteFile(
|
||||
sprintf(__METHOD__ . "({$bucket}, {$uri}): [%s] %s",
|
||||
$response->error->getCode(), $response->error->getMessage()),
|
||||
$response->error->getCode()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a query string authenticated URL
|
||||
*
|
||||
* @param string $bucket Bucket name
|
||||
* @param string $uri Object URI
|
||||
* @param int|null $lifetime Lifetime in seconds
|
||||
* @param bool $https Use HTTPS ($hostBucket should be false for SSL verification)?
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getAuthenticatedURL(string $bucket, string $uri, ?int $lifetime = null, bool $https = false): string
|
||||
{
|
||||
// Get a request from the URI and bucket
|
||||
$questionmarkPos = strpos($uri, '?');
|
||||
$query = '';
|
||||
|
||||
if ($questionmarkPos !== false)
|
||||
{
|
||||
$query = substr($uri, $questionmarkPos + 1);
|
||||
$uri = substr($uri, 0, $questionmarkPos);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
* !!!! DO NOT TOUCH THIS CODE. YOU WILL BREAK PRE-SIGNED URLS WITH v4 SIGNATURES. !!!!
|
||||
* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
*
|
||||
* The following two lines seem weird and possibly extraneous at first glance. However, they are VERY important.
|
||||
* If you remove them pre-signed URLs for v4 signatures will break! That's because pre-signed URLs with v4
|
||||
* signatures follow different rules than with v2 signatures.
|
||||
*
|
||||
* Authenticated (pre-signed) URLs are always made against the generic S3 region endpoint, not the bucket's
|
||||
* virtual-hosting-style domain name. The bucket is always the first component of the path.
|
||||
*
|
||||
* For example, given a bucket called foobar and an object baz.txt in it we are pre-signing the URL
|
||||
* https://s3-eu-west-1.amazonaws.com/foobar/baz.txt, not
|
||||
* https://foobar.s3-eu-west-1.amazonaws.com/foobar/baz.txt (as we'd be doing with v2 signatures).
|
||||
*
|
||||
* The problem is that the Request object needs to be created before we can convey the intent (regular request
|
||||
* or generation of a pre-signed URL). As a result its constructor creates the (immutable) request URI solely
|
||||
* based on whether the Configuration object's getUseLegacyPathStyle() returns false or not.
|
||||
*
|
||||
* Since we want to request URI to contain the bucket name we need to tell the Request object's constructor that
|
||||
* we are creating a Request object for path-style access, i.e. the useLegacyPathStyle flag in the Configuration
|
||||
* object is true. Naturally, the default behavior being virtual-hosting-style access to buckets, this flag is
|
||||
* most likely **false**.
|
||||
*
|
||||
* Therefore we need to clone the Configuration object, set the flag to true and create a Request object using
|
||||
* the falsified Configuration object.
|
||||
*
|
||||
* Note that v2 signatures are not affected. In v2 we are always appending the bucket name to the path, despite
|
||||
* the fact that we include the bucket name in the domain name.
|
||||
*
|
||||
* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
* !!!! DO NOT TOUCH THIS CODE. YOU WILL BREAK PRE-SIGNED URLS WITH v4 SIGNATURES. !!!!
|
||||
* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
*/
|
||||
$newConfig = clone $this->configuration;
|
||||
$newConfig->setUseLegacyPathStyle(true);
|
||||
|
||||
// Create the request object.
|
||||
$uri = str_replace('%2F', '/', rawurlencode($uri));
|
||||
$request = new Request('GET', $bucket, $uri, $newConfig);
|
||||
|
||||
if ($query)
|
||||
{
|
||||
parse_str($query, $parameters);
|
||||
|
||||
if (count($parameters))
|
||||
{
|
||||
foreach ($parameters as $k => $v)
|
||||
{
|
||||
$request->setParameter($k, $v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get the signed URI from the Request object
|
||||
return $request->getAuthenticatedURL($lifetime, $https);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the location (region) of a bucket. You need this to use the V4 API on that bucket!
|
||||
*
|
||||
* @param string $bucket Bucket name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getBucketLocation(string $bucket): string
|
||||
{
|
||||
$request = new Request('GET', $bucket, '', $this->configuration);
|
||||
$request->setParameter('location', null);
|
||||
|
||||
$response = $request->getResponse();
|
||||
|
||||
if (!$response->error->isError() && $response->code !== 200)
|
||||
{
|
||||
$response->error = new Error(
|
||||
$response->code,
|
||||
"Unexpected HTTP status {$response->code}"
|
||||
);
|
||||
}
|
||||
|
||||
if ($response->error->isError())
|
||||
{
|
||||
throw new CannotGetBucket(
|
||||
sprintf(__METHOD__ . "(): [%s] %s", $response->error->getCode(), $response->error->getMessage()),
|
||||
$response->error->getCode()
|
||||
);
|
||||
}
|
||||
|
||||
$result = 'us-east-1';
|
||||
|
||||
if ($response->hasBody())
|
||||
{
|
||||
$result = (string) $response->body;
|
||||
}
|
||||
|
||||
switch ($result)
|
||||
{
|
||||
// "EU" is an alias for 'eu-west-1', however the canonical location name you MUST use is 'eu-west-1'
|
||||
case 'EU':
|
||||
case 'eu':
|
||||
$result = 'eu-west-1';
|
||||
break;
|
||||
|
||||
// If the bucket location is 'us-east-1' you get an empty string. @#$%^&*()!!
|
||||
case '':
|
||||
$result = 'us-east-1';
|
||||
break;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the contents of a bucket
|
||||
*
|
||||
* If maxKeys is null this method will loop through truncated result sets
|
||||
*
|
||||
* @param string $bucket Bucket name
|
||||
* @param string|null $prefix Prefix (directory)
|
||||
* @param string|null $marker Marker (last file listed)
|
||||
* @param int|null $maxKeys Maximum number of keys ("files" and "directories") to return
|
||||
* @param string $delimiter Delimiter, typically "/"
|
||||
* @param bool $returnCommonPrefixes Set to true to return CommonPrefixes
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getBucket(string $bucket, ?string $prefix = null, ?string $marker = null, ?int $maxKeys = null, string $delimiter = '/', bool $returnCommonPrefixes = false): array
|
||||
{
|
||||
$request = new Request('GET', $bucket, '', $this->configuration);
|
||||
|
||||
if (!empty($prefix))
|
||||
{
|
||||
$request->setParameter('prefix', $prefix);
|
||||
}
|
||||
|
||||
if (!empty($marker))
|
||||
{
|
||||
$request->setParameter('marker', $marker);
|
||||
}
|
||||
|
||||
if (!empty($maxKeys))
|
||||
{
|
||||
$request->setParameter('max-keys', $maxKeys);
|
||||
}
|
||||
|
||||
if (!empty($delimiter))
|
||||
{
|
||||
$request->setParameter('delimiter', $delimiter);
|
||||
}
|
||||
|
||||
$response = $request->getResponse();
|
||||
|
||||
if (!$response->error->isError() && $response->code !== 200)
|
||||
{
|
||||
$response->error = new Error(
|
||||
$response->code,
|
||||
"Unexpected HTTP status {$response->code}"
|
||||
);
|
||||
}
|
||||
|
||||
if ($response->error->isError())
|
||||
{
|
||||
throw new CannotGetBucket(
|
||||
sprintf(__METHOD__ . "(): [%s] %s", $response->error->getCode(), $response->error->getMessage()),
|
||||
$response->error->getCode()
|
||||
);
|
||||
}
|
||||
|
||||
$results = [];
|
||||
|
||||
$nextMarker = null;
|
||||
|
||||
if ($response->hasBody() && isset($response->body->Contents))
|
||||
{
|
||||
foreach ($response->body->Contents as $c)
|
||||
{
|
||||
$results[(string) $c->Key] = [
|
||||
'name' => (string) $c->Key,
|
||||
'time' => strtotime((string) $c->LastModified),
|
||||
'size' => (int) $c->Size,
|
||||
'hash' => substr((string) $c->ETag, 1, -1),
|
||||
];
|
||||
|
||||
$nextMarker = (string) $c->Key;
|
||||
}
|
||||
}
|
||||
|
||||
if ($returnCommonPrefixes && $response->hasBody() && isset($response->body->CommonPrefixes))
|
||||
{
|
||||
foreach ($response->body->CommonPrefixes as $c)
|
||||
{
|
||||
$results[(string) $c->Prefix] = ['prefix' => (string) $c->Prefix];
|
||||
}
|
||||
}
|
||||
|
||||
if ($response->hasBody() && isset($response->body->IsTruncated) &&
|
||||
((string) $response->body->IsTruncated == 'false')
|
||||
)
|
||||
{
|
||||
return $results;
|
||||
}
|
||||
|
||||
if ($response->hasBody() && isset($response->body->NextMarker))
|
||||
{
|
||||
$nextMarker = (string) $response->body->NextMarker;
|
||||
}
|
||||
|
||||
// Is it a truncated result?
|
||||
$isTruncated = ($nextMarker !== null) && ((string) $response->body->IsTruncated == 'true');
|
||||
// Is this a truncated result and no maxKeys specified?
|
||||
$isTruncatedAndNoMaxKeys = ($maxKeys == null) && $isTruncated;
|
||||
// Is this a truncated result with less keys than the specified maxKeys; and common prefixes found but not returned to the caller?
|
||||
$isTruncatedAndNeedsContinue = ($maxKeys != null) && $isTruncated && (count($results) < $maxKeys);
|
||||
|
||||
// Loop through truncated results if maxKeys isn't specified
|
||||
if ($isTruncatedAndNoMaxKeys || $isTruncatedAndNeedsContinue)
|
||||
{
|
||||
do
|
||||
{
|
||||
$request = new Request('GET', $bucket, '', $this->configuration);
|
||||
|
||||
if (!empty($prefix))
|
||||
{
|
||||
$request->setParameter('prefix', $prefix);
|
||||
}
|
||||
|
||||
$request->setParameter('marker', $nextMarker);
|
||||
|
||||
if (!empty($delimiter))
|
||||
{
|
||||
$request->setParameter('delimiter', $delimiter);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$response = $request->getResponse();
|
||||
}
|
||||
catch (\Exception $e)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if ($response->hasBody() && isset($response->body->Contents))
|
||||
{
|
||||
foreach ($response->body->Contents as $c)
|
||||
{
|
||||
$results[(string) $c->Key] = [
|
||||
'name' => (string) $c->Key,
|
||||
'time' => strtotime((string) $c->LastModified),
|
||||
'size' => (int) $c->Size,
|
||||
'hash' => substr((string) $c->ETag, 1, -1),
|
||||
];
|
||||
|
||||
$nextMarker = (string) $c->Key;
|
||||
}
|
||||
}
|
||||
|
||||
if ($returnCommonPrefixes && $response->hasBody() && isset($response->body->CommonPrefixes))
|
||||
{
|
||||
foreach ($response->body->CommonPrefixes as $c)
|
||||
{
|
||||
$results[(string) $c->Prefix] = ['prefix' => (string) $c->Prefix];
|
||||
}
|
||||
}
|
||||
|
||||
if ($response->hasBody() && isset($response->body->NextMarker))
|
||||
{
|
||||
$nextMarker = (string) $response->body->NextMarker;
|
||||
}
|
||||
|
||||
$continueCondition = false;
|
||||
|
||||
if ($isTruncatedAndNoMaxKeys)
|
||||
{
|
||||
$continueCondition = !$response->error->isError() && $isTruncated;
|
||||
}
|
||||
|
||||
if ($isTruncatedAndNeedsContinue)
|
||||
{
|
||||
$continueCondition = !$response->error->isError() && $isTruncated && (count($results) < $maxKeys);
|
||||
}
|
||||
} while ($continueCondition);
|
||||
}
|
||||
|
||||
if (!is_null($maxKeys))
|
||||
{
|
||||
$results = array_splice($results, 0, $maxKeys);
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of buckets
|
||||
*
|
||||
* @param bool $detailed Returns detailed bucket list when true
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function listBuckets(bool $detailed = false): array
|
||||
{
|
||||
// When listing buckets with the AWSv4 signature method we MUST set the region to us-east-1. Don't ask...
|
||||
$configuration = clone $this->configuration;
|
||||
$configuration->setRegion('us-east-1');
|
||||
|
||||
$request = new Request('GET', '', '', $configuration);
|
||||
$response = $request->getResponse();
|
||||
|
||||
if (!$response->error->isError() && (($response->code !== 200)))
|
||||
{
|
||||
$response->error = new Error(
|
||||
$response->code,
|
||||
"Unexpected HTTP status {$response->code}"
|
||||
);
|
||||
}
|
||||
|
||||
if ($response->error->isError())
|
||||
{
|
||||
throw new CannotListBuckets(
|
||||
sprintf(__METHOD__ . "(): [%s] %s", $response->error->getCode(), $response->error->getMessage()),
|
||||
$response->error->getCode()
|
||||
);
|
||||
}
|
||||
|
||||
$results = [];
|
||||
|
||||
if (!isset($response->body->Buckets))
|
||||
{
|
||||
return $results;
|
||||
}
|
||||
|
||||
if ($detailed)
|
||||
{
|
||||
if (isset($response->body->Owner, $response->body->Owner->ID, $response->body->Owner->DisplayName))
|
||||
{
|
||||
$results['owner'] = [
|
||||
'id' => (string) $response->body->Owner->ID,
|
||||
'name' => (string) $response->body->Owner->DisplayName,
|
||||
];
|
||||
}
|
||||
|
||||
$results['buckets'] = [];
|
||||
|
||||
foreach ($response->body->Buckets->Bucket as $b)
|
||||
{
|
||||
$results['buckets'][] = [
|
||||
'name' => (string) $b->Name,
|
||||
'time' => strtotime((string) $b->CreationDate),
|
||||
];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach ($response->body->Buckets->Bucket as $b)
|
||||
{
|
||||
$results[] = (string) $b->Name;
|
||||
}
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a multipart upload of an object
|
||||
*
|
||||
* @param Input $input Input data
|
||||
* @param string $bucket Bucket name
|
||||
* @param string $uri Object URI
|
||||
* @param string $acl ACL constant
|
||||
* @param array $requestHeaders Array of request headers
|
||||
*
|
||||
* @return string The upload session ID (UploadId)
|
||||
*/
|
||||
public function startMultipart(Input $input, string $bucket, string $uri, string $acl = Acl::ACL_PRIVATE, array $requestHeaders = []): string
|
||||
{
|
||||
$request = new Request('POST', $bucket, $uri, $this->configuration);
|
||||
$request->setParameter('uploads', '');
|
||||
|
||||
// Custom request headers (Content-Type, Content-Disposition, Content-Encoding)
|
||||
if (is_array($requestHeaders))
|
||||
{
|
||||
foreach ($requestHeaders as $h => $v)
|
||||
{
|
||||
if (strtolower(substr($h, 0, 6)) == 'x-amz-')
|
||||
{
|
||||
$request->setAmzHeader(strtolower($h), $v);
|
||||
}
|
||||
else
|
||||
{
|
||||
$request->setHeader($h, $v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$request->setAmzHeader('x-amz-acl', $acl);
|
||||
|
||||
if (isset($requestHeaders['Content-Type']))
|
||||
{
|
||||
$input->setType($requestHeaders['Content-Type']);
|
||||
}
|
||||
|
||||
$request->setHeader('Content-Type', $input->getType());
|
||||
|
||||
$response = $request->getResponse();
|
||||
|
||||
if (!$response->error->isError() && ($response->code !== 200))
|
||||
{
|
||||
$response->error = new Error(
|
||||
$response->code,
|
||||
"Unexpected HTTP status {$response->code}"
|
||||
);
|
||||
}
|
||||
|
||||
if ($response->error->isError())
|
||||
{
|
||||
throw new CannotPutFile(
|
||||
sprintf(__METHOD__ . "(): [%s] %s\n\nDebug info:\n%s", $response->error->getCode(), $response->error->getMessage(), print_r($response->body, true))
|
||||
);
|
||||
}
|
||||
|
||||
return (string) $response->body->UploadId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads a part of a multipart object upload
|
||||
*
|
||||
* @param Input $input Input data. You MUST specify the UploadID and PartNumber
|
||||
* @param string $bucket Bucket name
|
||||
* @param string $uri Object URI
|
||||
* @param array $requestHeaders Array of request headers or content type as a string
|
||||
* @param int $chunkSize Size of each upload chunk, in bytes. It cannot be less than 5242880 bytes (5Mb)
|
||||
*
|
||||
* @return null|string The ETag of the upload part of null if we have ran out of parts to upload
|
||||
*/
|
||||
public function uploadMultipart(Input $input, string $bucket, string $uri, array $requestHeaders = [], int $chunkSize = 5242880): ?string
|
||||
{
|
||||
if ($chunkSize < 5242880)
|
||||
{
|
||||
$chunkSize = 5242880;
|
||||
}
|
||||
|
||||
// We need a valid UploadID and PartNumber
|
||||
$UploadID = $input->getUploadID();
|
||||
$PartNumber = $input->getPartNumber();
|
||||
|
||||
if (empty($UploadID))
|
||||
{
|
||||
throw new CannotPutFile(
|
||||
__METHOD__ . '(): No UploadID specified'
|
||||
);
|
||||
}
|
||||
|
||||
if (empty($PartNumber))
|
||||
{
|
||||
throw new CannotPutFile(
|
||||
__METHOD__ . '(): No PartNumber specified'
|
||||
);
|
||||
}
|
||||
|
||||
$UploadID = urlencode($UploadID);
|
||||
$PartNumber = (int) $PartNumber;
|
||||
|
||||
$request = new Request('PUT', $bucket, $uri, $this->configuration);
|
||||
$request->setParameter('partNumber', $PartNumber);
|
||||
$request->setParameter('uploadId', $UploadID);
|
||||
$request->setInput($input);
|
||||
|
||||
// Full data length
|
||||
$totalSize = $input->getSize();
|
||||
|
||||
// No Content-Type for multipart uploads
|
||||
$input->setType(null);
|
||||
|
||||
// Calculate part offset
|
||||
$partOffset = $chunkSize * ($PartNumber - 1);
|
||||
|
||||
if ($partOffset > $totalSize)
|
||||
{
|
||||
// This is to signify that we ran out of parts ;)
|
||||
return null;
|
||||
}
|
||||
|
||||
// How many parts are there?
|
||||
$totalParts = floor($totalSize / $chunkSize);
|
||||
|
||||
if ($totalParts * $chunkSize < $totalSize)
|
||||
{
|
||||
$totalParts++;
|
||||
}
|
||||
|
||||
// Calculate Content-Length
|
||||
$size = $chunkSize;
|
||||
|
||||
if ($PartNumber >= $totalParts)
|
||||
{
|
||||
$size = $totalSize - ($PartNumber - 1) * $chunkSize;
|
||||
}
|
||||
|
||||
if ($size <= 0)
|
||||
{
|
||||
// This is to signify that we ran out of parts ;)
|
||||
return null;
|
||||
}
|
||||
|
||||
$input->setSize($size);
|
||||
|
||||
switch ($input->getInputType())
|
||||
{
|
||||
case Input::INPUT_DATA:
|
||||
$input->setData(substr($input->getData(), ($PartNumber - 1) * $chunkSize, $input->getSize()));
|
||||
break;
|
||||
|
||||
case Input::INPUT_FILE:
|
||||
case Input::INPUT_RESOURCE:
|
||||
$fp = $input->getFp();
|
||||
fseek($fp, ($PartNumber - 1) * $chunkSize);
|
||||
break;
|
||||
}
|
||||
|
||||
// Custom request headers (Content-Type, Content-Disposition, Content-Encoding)
|
||||
if (is_array($requestHeaders))
|
||||
{
|
||||
foreach ($requestHeaders as $h => $v)
|
||||
{
|
||||
if (strtolower(substr($h, 0, 6)) == 'x-amz-')
|
||||
{
|
||||
$request->setAmzHeader(strtolower($h), $v);
|
||||
}
|
||||
else
|
||||
{
|
||||
$request->setHeader($h, $v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$request->setHeader('Content-Length', $input->getSize());
|
||||
|
||||
if ($input->getInputType() === Input::INPUT_DATA)
|
||||
{
|
||||
$request->setHeader('Content-Type', "application/x-www-form-urlencoded");
|
||||
}
|
||||
|
||||
$response = $request->getResponse();
|
||||
|
||||
if ($response->code !== 200)
|
||||
{
|
||||
if (!$response->error->isError())
|
||||
{
|
||||
$response->error = new Error(
|
||||
$response->code,
|
||||
"Unexpected HTTP status {$response->code}"
|
||||
);
|
||||
}
|
||||
|
||||
if (is_object($response->body) && ($response->body instanceof \SimpleXMLElement) && (strpos($input->getSize(), ',') === false))
|
||||
{
|
||||
// For some moronic reason, trying to multipart upload files on some hosts comes back with a crazy
|
||||
// error from Amazon that we need to set Content-Length:5242880,5242880 instead of
|
||||
// Content-Length:5242880 which is AGAINST Amazon's documentation. In this case we pass the header
|
||||
// 'workaround-broken-content-length' and retry. Whatever.
|
||||
if (isset($response->body->CanonicalRequest))
|
||||
{
|
||||
$amazonsCanonicalRequest = (string) $response->body->CanonicalRequest;
|
||||
$lines = explode("\n", $amazonsCanonicalRequest);
|
||||
|
||||
foreach ($lines as $line)
|
||||
{
|
||||
if (substr($line, 0, 15) != 'content-length:')
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
[$junk, $stupidAmazonDefinedContentLength] = explode(":", $line);
|
||||
|
||||
if (strpos($stupidAmazonDefinedContentLength, ',') !== false)
|
||||
{
|
||||
if (!isset($requestHeaders['workaround-broken-content-length']))
|
||||
{
|
||||
$requestHeaders['workaround-broken-content-length'] = true;
|
||||
|
||||
// This is required to reset the input size to its default value. If you don't do that
|
||||
// only one part will ever be uploaded. Oops!
|
||||
$input->setSize(-1);
|
||||
|
||||
return $this->uploadMultipart($input, $bucket, $uri, $requestHeaders, $chunkSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
throw new CannotPutFile(
|
||||
sprintf(__METHOD__ . "(): [%s] %s\n\nDebug info:\n%s", $response->error->getCode(), $response->error->getMessage(), print_r($response->body, true))
|
||||
);
|
||||
}
|
||||
|
||||
// Return the ETag header
|
||||
return $response->headers['hash'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Finalizes the multi-part upload. The $input object should contain two keys, etags an array of ETags of the
|
||||
* uploaded parts and UploadID the multipart upload ID.
|
||||
*
|
||||
* @param Input $input The array of input elements
|
||||
* @param string $bucket The bucket where the object is being stored
|
||||
* @param string $uri The key (path) to the object
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function finalizeMultipart(Input $input, string $bucket, string $uri): void
|
||||
{
|
||||
$etags = $input->getEtags();
|
||||
$UploadID = $input->getUploadID();
|
||||
|
||||
if (empty($etags))
|
||||
{
|
||||
throw new CannotPutFile(
|
||||
__METHOD__ . '(): No ETags array specified'
|
||||
);
|
||||
}
|
||||
|
||||
if (empty($UploadID))
|
||||
{
|
||||
throw new CannotPutFile(
|
||||
__METHOD__ . '(): No UploadID specified'
|
||||
);
|
||||
}
|
||||
|
||||
// Create the message
|
||||
$message = "<CompleteMultipartUpload>\n";
|
||||
$part = 0;
|
||||
|
||||
foreach ($etags as $etag)
|
||||
{
|
||||
$part++;
|
||||
$message .= "\t<Part>\n\t\t<PartNumber>$part</PartNumber>\n\t\t<ETag>\"$etag\"</ETag>\n\t</Part>\n";
|
||||
}
|
||||
|
||||
$message .= "</CompleteMultipartUpload>";
|
||||
|
||||
// Get a request query
|
||||
$reqInput = Input::createFromData($message);
|
||||
|
||||
$request = new Request('POST', $bucket, $uri, $this->configuration);
|
||||
$request->setParameter('uploadId', $UploadID);
|
||||
$request->setInput($reqInput);
|
||||
|
||||
// Do post
|
||||
$request->setHeader('Content-Type', 'application/xml'); // Even though the Amazon API doc doesn't mention it, it's required... :(
|
||||
$response = $request->getResponse();
|
||||
|
||||
if (!$response->error->isError() && ($response->code != 200))
|
||||
{
|
||||
$response->error = new Error(
|
||||
$response->code,
|
||||
"Unexpected HTTP status {$response->code}"
|
||||
);
|
||||
}
|
||||
|
||||
if ($response->error->isError())
|
||||
{
|
||||
if ($response->error->getCode() == 'RequestTimeout')
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
throw new CannotPutFile(
|
||||
sprintf(__METHOD__ . "(): [%s] %s\n\nDebug info:\n%s", $response->error->getCode(), $response->error->getMessage(), print_r($response->body, true))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the configuration object
|
||||
*
|
||||
* @return Configuration
|
||||
*/
|
||||
public function getConfiguration(): Configuration
|
||||
{
|
||||
return $this->configuration;
|
||||
}
|
||||
}
|
19
s3_storage/vendor/akeeba/s3/src/Exception/CannotDeleteFile.php
vendored
Normal file
19
s3_storage/vendor/akeeba/s3/src/Exception/CannotDeleteFile.php
vendored
Normal file
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
*
|
||||
* @package akeebaengine
|
||||
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Postproc\Connector\S3v4\Exception;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class CannotDeleteFile extends RuntimeException
|
||||
{
|
||||
}
|
19
s3_storage/vendor/akeeba/s3/src/Exception/CannotGetBucket.php
vendored
Normal file
19
s3_storage/vendor/akeeba/s3/src/Exception/CannotGetBucket.php
vendored
Normal file
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
*
|
||||
* @package akeebaengine
|
||||
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Postproc\Connector\S3v4\Exception;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class CannotGetBucket extends RuntimeException
|
||||
{
|
||||
}
|
19
s3_storage/vendor/akeeba/s3/src/Exception/CannotGetFile.php
vendored
Normal file
19
s3_storage/vendor/akeeba/s3/src/Exception/CannotGetFile.php
vendored
Normal file
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
*
|
||||
* @package akeebaengine
|
||||
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Postproc\Connector\S3v4\Exception;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class CannotGetFile extends RuntimeException
|
||||
{
|
||||
}
|
19
s3_storage/vendor/akeeba/s3/src/Exception/CannotListBuckets.php
vendored
Normal file
19
s3_storage/vendor/akeeba/s3/src/Exception/CannotListBuckets.php
vendored
Normal file
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
*
|
||||
* @package akeebaengine
|
||||
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Postproc\Connector\S3v4\Exception;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class CannotListBuckets extends RuntimeException
|
||||
{
|
||||
}
|
27
s3_storage/vendor/akeeba/s3/src/Exception/CannotOpenFileForRead.php
vendored
Normal file
27
s3_storage/vendor/akeeba/s3/src/Exception/CannotOpenFileForRead.php
vendored
Normal file
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
*
|
||||
* @package akeebaengine
|
||||
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Postproc\Connector\S3v4\Exception;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
use Exception;
|
||||
use RuntimeException;
|
||||
|
||||
class CannotOpenFileForRead extends RuntimeException
|
||||
{
|
||||
public function __construct(string $file = "", int $code = 0, Exception $previous = null)
|
||||
{
|
||||
$message = "Cannot open $file for reading";
|
||||
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
|
||||
}
|
27
s3_storage/vendor/akeeba/s3/src/Exception/CannotOpenFileForWrite.php
vendored
Normal file
27
s3_storage/vendor/akeeba/s3/src/Exception/CannotOpenFileForWrite.php
vendored
Normal file
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
*
|
||||
* @package akeebaengine
|
||||
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Postproc\Connector\S3v4\Exception;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
use Exception;
|
||||
use RuntimeException;
|
||||
|
||||
class CannotOpenFileForWrite extends RuntimeException
|
||||
{
|
||||
public function __construct(string $file = "", int $code = 0, Exception $previous = null)
|
||||
{
|
||||
$message = "Cannot open $file for writing";
|
||||
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
|
||||
}
|
19
s3_storage/vendor/akeeba/s3/src/Exception/CannotPutFile.php
vendored
Normal file
19
s3_storage/vendor/akeeba/s3/src/Exception/CannotPutFile.php
vendored
Normal file
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
*
|
||||
* @package akeebaengine
|
||||
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Postproc\Connector\S3v4\Exception;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class CannotPutFile extends RuntimeException
|
||||
{
|
||||
}
|
23
s3_storage/vendor/akeeba/s3/src/Exception/ConfigurationError.php
vendored
Normal file
23
s3_storage/vendor/akeeba/s3/src/Exception/ConfigurationError.php
vendored
Normal file
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
*
|
||||
* @package akeebaengine
|
||||
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Postproc\Connector\S3v4\Exception;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Configuration error
|
||||
*/
|
||||
abstract class ConfigurationError extends RuntimeException
|
||||
{
|
||||
|
||||
}
|
32
s3_storage/vendor/akeeba/s3/src/Exception/InvalidAccessKey.php
vendored
Normal file
32
s3_storage/vendor/akeeba/s3/src/Exception/InvalidAccessKey.php
vendored
Normal file
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
*
|
||||
* @package akeebaengine
|
||||
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Postproc\Connector\S3v4\Exception;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* Invalid Amazon S3 access key
|
||||
*/
|
||||
class InvalidAccessKey extends ConfigurationError
|
||||
{
|
||||
public function __construct(string $message = "", int $code = 0, Exception $previous = null)
|
||||
{
|
||||
if (empty($message))
|
||||
{
|
||||
$message = 'The Amazon S3 Access Key provided is invalid';
|
||||
}
|
||||
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
|
||||
}
|
33
s3_storage/vendor/akeeba/s3/src/Exception/InvalidBody.php
vendored
Normal file
33
s3_storage/vendor/akeeba/s3/src/Exception/InvalidBody.php
vendored
Normal file
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
*
|
||||
* @package akeebaengine
|
||||
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Postproc\Connector\S3v4\Exception;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
use Exception;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Invalid response body type
|
||||
*/
|
||||
class InvalidBody extends RuntimeException
|
||||
{
|
||||
public function __construct(string $message = "", int $code = 0, Exception $previous = null)
|
||||
{
|
||||
if (empty($message))
|
||||
{
|
||||
$message = 'Invalid response body type';
|
||||
}
|
||||
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
|
||||
}
|
32
s3_storage/vendor/akeeba/s3/src/Exception/InvalidEndpoint.php
vendored
Normal file
32
s3_storage/vendor/akeeba/s3/src/Exception/InvalidEndpoint.php
vendored
Normal file
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
*
|
||||
* @package akeebaengine
|
||||
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Postproc\Connector\S3v4\Exception;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* Invalid Amazon S3 endpoint
|
||||
*/
|
||||
class InvalidEndpoint extends ConfigurationError
|
||||
{
|
||||
public function __construct(string $message = "", int $code = 0, Exception $previous = null)
|
||||
{
|
||||
if (empty($message))
|
||||
{
|
||||
$message = 'The custom S3 endpoint provided is invalid. Do NOT include the protocol (http:// or https://). Valid examples are s3.example.com and www.example.com/s3Api';
|
||||
}
|
||||
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
|
||||
}
|
30
s3_storage/vendor/akeeba/s3/src/Exception/InvalidFilePointer.php
vendored
Normal file
30
s3_storage/vendor/akeeba/s3/src/Exception/InvalidFilePointer.php
vendored
Normal file
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
*
|
||||
* @package akeebaengine
|
||||
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Postproc\Connector\S3v4\Exception;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
use Exception;
|
||||
use InvalidArgumentException;
|
||||
|
||||
class InvalidFilePointer extends InvalidArgumentException
|
||||
{
|
||||
public function __construct(string $message = "", int $code = 0, Exception $previous = null)
|
||||
{
|
||||
if (empty($message))
|
||||
{
|
||||
$message = 'The specified file pointer is not a valid stream resource';
|
||||
}
|
||||
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
|
||||
}
|
32
s3_storage/vendor/akeeba/s3/src/Exception/InvalidRegion.php
vendored
Normal file
32
s3_storage/vendor/akeeba/s3/src/Exception/InvalidRegion.php
vendored
Normal file
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
*
|
||||
* @package akeebaengine
|
||||
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Postproc\Connector\S3v4\Exception;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* Invalid Amazon S3 region
|
||||
*/
|
||||
class InvalidRegion extends ConfigurationError
|
||||
{
|
||||
public function __construct(string $message = "", int $code = 0, Exception $previous = null)
|
||||
{
|
||||
if (empty($message))
|
||||
{
|
||||
$message = 'The Amazon S3 region provided is invalid.';
|
||||
}
|
||||
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
|
||||
}
|
32
s3_storage/vendor/akeeba/s3/src/Exception/InvalidSecretKey.php
vendored
Normal file
32
s3_storage/vendor/akeeba/s3/src/Exception/InvalidSecretKey.php
vendored
Normal file
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
*
|
||||
* @package akeebaengine
|
||||
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Postproc\Connector\S3v4\Exception;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* Invalid Amazon S3 secret key
|
||||
*/
|
||||
class InvalidSecretKey extends ConfigurationError
|
||||
{
|
||||
public function __construct(string $message = "", int $code = 0, Exception $previous = null)
|
||||
{
|
||||
if (empty($message))
|
||||
{
|
||||
$message = 'The Amazon S3 Secret Key provided is invalid';
|
||||
}
|
||||
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
|
||||
}
|
32
s3_storage/vendor/akeeba/s3/src/Exception/InvalidSignatureMethod.php
vendored
Normal file
32
s3_storage/vendor/akeeba/s3/src/Exception/InvalidSignatureMethod.php
vendored
Normal file
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
*
|
||||
* @package akeebaengine
|
||||
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Postproc\Connector\S3v4\Exception;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* Invalid Amazon S3 signature method
|
||||
*/
|
||||
class InvalidSignatureMethod extends ConfigurationError
|
||||
{
|
||||
public function __construct(string $message = "", int $code = 0, Exception $previous = null)
|
||||
{
|
||||
if (empty($message))
|
||||
{
|
||||
$message = 'The Amazon S3 signature method provided is invalid. Only v2 and v4 signatures are supported.';
|
||||
}
|
||||
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
|
||||
}
|
22
s3_storage/vendor/akeeba/s3/src/Exception/PropertyNotFound.php
vendored
Normal file
22
s3_storage/vendor/akeeba/s3/src/Exception/PropertyNotFound.php
vendored
Normal file
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
*
|
||||
* @package akeebaengine
|
||||
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Postproc\Connector\S3v4\Exception;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
use LogicException;
|
||||
|
||||
/**
|
||||
* Invalid magic property name
|
||||
*/
|
||||
class PropertyNotFound extends LogicException
|
||||
{
|
||||
}
|
734
s3_storage/vendor/akeeba/s3/src/Input.php
vendored
Normal file
734
s3_storage/vendor/akeeba/s3/src/Input.php
vendored
Normal file
|
@ -0,0 +1,734 @@
|
|||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
*
|
||||
* @package akeebaengine
|
||||
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Postproc\Connector\S3v4;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
/**
|
||||
* Defines an input source for PUT/POST requests to Amazon S3
|
||||
*/
|
||||
class Input
|
||||
{
|
||||
/**
|
||||
* Input type: resource
|
||||
*/
|
||||
const INPUT_RESOURCE = 1;
|
||||
|
||||
/**
|
||||
* Input type: file
|
||||
*/
|
||||
const INPUT_FILE = 2;
|
||||
|
||||
/**
|
||||
* Input type: raw data
|
||||
*/
|
||||
const INPUT_DATA = 3;
|
||||
|
||||
/**
|
||||
* File pointer, in case we have a resource
|
||||
*
|
||||
* @var resource
|
||||
*/
|
||||
private $fp = null;
|
||||
|
||||
/**
|
||||
* Absolute filename to the file
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $file = null;
|
||||
|
||||
/**
|
||||
* Data to upload, as a string
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $data = null;
|
||||
|
||||
/**
|
||||
* Length of the data to upload
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $size = -1;
|
||||
|
||||
/**
|
||||
* Content type (MIME type)
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
private $type = '';
|
||||
|
||||
/**
|
||||
* MD5 sum of the data to upload, as base64 encoded string. If it's false no MD5 sum will be returned.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
private $md5sum = null;
|
||||
|
||||
/**
|
||||
* SHA-256 sum of the data to upload, as lowercase hex string.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
private $sha256 = null;
|
||||
|
||||
/**
|
||||
* The Upload Session ID used for multipart uploads
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
private $UploadID = null;
|
||||
|
||||
/**
|
||||
* The part number used in multipart uploads
|
||||
*
|
||||
* @var int|null
|
||||
*/
|
||||
private $PartNumber = null;
|
||||
|
||||
/**
|
||||
* The list of ETags used when finalising a multipart upload
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
private $etags = [];
|
||||
|
||||
/**
|
||||
* Create an input object from a file (also: any valid URL wrapper)
|
||||
*
|
||||
* @param string $file Absolute file path or any valid URL fopen() wrapper
|
||||
* @param null|string $md5sum The MD5 sum. null to auto calculate, empty string to never calculate.
|
||||
* @param null|string $sha256sum The SHA256 sum. null to auto calculate.
|
||||
*
|
||||
* @return Input
|
||||
*/
|
||||
public static function createFromFile(string $file, ?string $md5sum = null, ?string $sha256sum = null): self
|
||||
{
|
||||
$input = new Input();
|
||||
|
||||
$input->setFile($file);
|
||||
$input->setMd5sum($md5sum);
|
||||
$input->setSha256($sha256sum);
|
||||
|
||||
return $input;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an input object from a stream resource / file pointer.
|
||||
*
|
||||
* Please note that the contentLength cannot be calculated automatically unless you have a seekable stream resource.
|
||||
*
|
||||
* @param resource $resource The file pointer or stream resource
|
||||
* @param int $contentLength The length of the content in bytes. Set to -1 for auto calculation.
|
||||
* @param null|string $md5sum The MD5 sum. null to auto calculate, empty string to never calculate.
|
||||
* @param null|string $sha256sum The SHA256 sum. null to auto calculate.
|
||||
*
|
||||
* @return Input
|
||||
*/
|
||||
public static function createFromResource(&$resource, int $contentLength, ?string $md5sum = null, ?string $sha256sum = null): self
|
||||
{
|
||||
$input = new Input();
|
||||
|
||||
$input->setFp($resource);
|
||||
$input->setSize($contentLength);
|
||||
$input->setMd5sum($md5sum);
|
||||
$input->setSha256($sha256sum);
|
||||
|
||||
return $input;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an input object from raw data.
|
||||
*
|
||||
* Please bear in mind that the data is being duplicated in memory. Therefore you'll need at least 2xstrlen($data)
|
||||
* of free memory when you are using this method. You can instantiate an object and use assignData to work around
|
||||
* this limitation when handling large amounts of data which may cause memory outages (typically: over 10Mb).
|
||||
*
|
||||
* @param string $data The data to use.
|
||||
* @param null|string $md5sum The MD5 sum. null to auto calculate, empty string to never calculate.
|
||||
* @param null|string $sha256sum The SHA256 sum. null to auto calculate.
|
||||
*
|
||||
* @return Input
|
||||
*/
|
||||
public static function createFromData(string &$data, ?string $md5sum = null, ?string $sha256sum = null): self
|
||||
{
|
||||
$input = new Input();
|
||||
|
||||
$input->setData($data);
|
||||
$input->setMd5sum($md5sum);
|
||||
$input->setSha256($sha256sum);
|
||||
|
||||
return $input;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destructor.
|
||||
*/
|
||||
function __destruct()
|
||||
{
|
||||
if (is_resource($this->fp))
|
||||
{
|
||||
@fclose($this->fp);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the input type (resource, file or data)
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getInputType(): int
|
||||
{
|
||||
if (!empty($this->file))
|
||||
{
|
||||
return self::INPUT_FILE;
|
||||
}
|
||||
|
||||
if (!empty($this->fp))
|
||||
{
|
||||
return self::INPUT_RESOURCE;
|
||||
}
|
||||
|
||||
return self::INPUT_DATA;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the file pointer to the data, or null if this is not a resource input
|
||||
*
|
||||
* @return resource|null
|
||||
*/
|
||||
public function getFp()
|
||||
{
|
||||
if (!is_resource($this->fp))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->fp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the file pointer (or, generally, stream resource)
|
||||
*
|
||||
* @param resource $fp
|
||||
*/
|
||||
public function setFp($fp): void
|
||||
{
|
||||
if (!is_resource($fp))
|
||||
{
|
||||
throw new Exception\InvalidFilePointer('$fp is not a file resource');
|
||||
}
|
||||
|
||||
$this->fp = $fp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the absolute path to the input file, or null if this is not a file input
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getFile(): ?string
|
||||
{
|
||||
if (empty($this->file))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the absolute path to the input file
|
||||
*
|
||||
* @param string $file
|
||||
*/
|
||||
public function setFile(string $file): void
|
||||
{
|
||||
$this->file = $file;
|
||||
$this->data = null;
|
||||
|
||||
if (is_resource($this->fp))
|
||||
{
|
||||
@fclose($this->fp);
|
||||
}
|
||||
|
||||
$this->fp = @fopen($file, 'rb');
|
||||
|
||||
if ($this->fp === false)
|
||||
{
|
||||
throw new Exception\CannotOpenFileForRead($file);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the raw input data, or null if this is a file or stream input
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getData(): ?string
|
||||
{
|
||||
if (empty($this->data) && ($this->getInputType() != self::INPUT_DATA))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the raw input data
|
||||
*
|
||||
* @param string $data
|
||||
*/
|
||||
public function setData(string $data): void
|
||||
{
|
||||
$this->data = $data;
|
||||
|
||||
if (is_resource($this->fp))
|
||||
{
|
||||
@fclose($this->fp);
|
||||
}
|
||||
|
||||
$this->file = null;
|
||||
$this->fp = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a reference to the raw input data
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function &getDataReference(): ?string
|
||||
{
|
||||
if (empty($this->data) && ($this->getInputType() != self::INPUT_DATA))
|
||||
{
|
||||
$this->data = null;
|
||||
}
|
||||
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the raw input data by doing an assignment instead of memory copy. While this conserves memory you cannot use
|
||||
* this with hardcoded strings, method results etc without going through a variable first.
|
||||
*
|
||||
* @param string $data
|
||||
*/
|
||||
public function assignData(string &$data): void
|
||||
{
|
||||
$this->data = $data;
|
||||
|
||||
if (is_resource($this->fp))
|
||||
{
|
||||
@fclose($this->fp);
|
||||
}
|
||||
|
||||
$this->file = null;
|
||||
$this->fp = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the size of the data to be uploaded, in bytes. If it's not already specified it will try to guess.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getSize(): int
|
||||
{
|
||||
if ($this->size < 0)
|
||||
{
|
||||
$this->size = $this->getInputSize();
|
||||
}
|
||||
|
||||
return $this->size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the size of the data to be uploaded.
|
||||
*
|
||||
* @param int $size
|
||||
*/
|
||||
public function setSize(int $size)
|
||||
{
|
||||
$this->size = $size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the MIME type of the data
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getType(): ?string
|
||||
{
|
||||
if (empty($this->type))
|
||||
{
|
||||
$this->type = 'application/octet-stream';
|
||||
|
||||
if ($this->getInputType() == self::INPUT_FILE)
|
||||
{
|
||||
$this->type = $this->getMimeType($this->file);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the MIME type of the data
|
||||
*
|
||||
* @param string|null $type
|
||||
*/
|
||||
public function setType(?string $type)
|
||||
{
|
||||
$this->type = $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the MD5 sum of the content
|
||||
*
|
||||
* @return null|string
|
||||
*/
|
||||
public function getMd5sum(): ?string
|
||||
{
|
||||
if ($this->md5sum === '')
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (is_null($this->md5sum))
|
||||
{
|
||||
$this->md5sum = $this->calculateMd5();
|
||||
}
|
||||
|
||||
return $this->md5sum;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the MD5 sum of the content as a base64 encoded string of the raw MD5 binary value.
|
||||
*
|
||||
* WARNING: Do not set a binary MD5 sum or a hex-encoded MD5 sum, it will result in an invalid signature error!
|
||||
*
|
||||
* Set to null to automatically calculate it from the raw data. Set to an empty string to force it to never be
|
||||
* calculated and no value for it set either.
|
||||
*
|
||||
* @param string|null $md5sum
|
||||
*/
|
||||
public function setMd5sum(?string $md5sum): void
|
||||
{
|
||||
$this->md5sum = $md5sum;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the SHA-256 hash of the content
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getSha256(): string
|
||||
{
|
||||
if (empty($this->sha256))
|
||||
{
|
||||
$this->sha256 = $this->calculateSha256();
|
||||
}
|
||||
|
||||
return $this->sha256;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the SHA-256 sum of the content. It must be a lowercase hexadecimal encoded string.
|
||||
*
|
||||
* Set to null to automatically calculate it from the raw data.
|
||||
*
|
||||
* @param string|null $sha256
|
||||
*/
|
||||
public function setSha256(?string $sha256): void
|
||||
{
|
||||
$this->sha256 = strtolower($sha256);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Upload Session ID for multipart uploads
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getUploadID(): ?string
|
||||
{
|
||||
return $this->UploadID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the Upload Session ID for multipart uploads
|
||||
*
|
||||
* @param string|null $UploadID
|
||||
*/
|
||||
public function setUploadID(?string $UploadID): void
|
||||
{
|
||||
$this->UploadID = $UploadID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the part number for multipart uploads.
|
||||
*
|
||||
* Returns null if the part number has not been set yet.
|
||||
*
|
||||
* @return int|null
|
||||
*/
|
||||
public function getPartNumber(): ?int
|
||||
{
|
||||
return $this->PartNumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the part number for multipart uploads
|
||||
*
|
||||
* @param int $PartNumber
|
||||
*/
|
||||
public function setPartNumber(int $PartNumber): void
|
||||
{
|
||||
// Clamp the part number to integers greater than zero.
|
||||
$this->PartNumber = max(1, (int) $PartNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of ETags for multipart uploads
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function getEtags(): array
|
||||
{
|
||||
return $this->etags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the list of ETags for multipart uploads
|
||||
*
|
||||
* @param string[] $etags
|
||||
*/
|
||||
public function setEtags(array $etags): void
|
||||
{
|
||||
$this->etags = $etags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the upload size from the input source. For data it's the entire raw string length. For a file resource
|
||||
* it's the entire file's length. For seekable stream resources it's the remaining data from the current seek
|
||||
* position to EOF.
|
||||
*
|
||||
* WARNING: You should never try to specify files or resources over 2Gb minus 1 byte otherwise 32-bit versions of
|
||||
* PHP (anything except Linux x64 builds) will fail in unpredictable ways: the internal int representation in PHP
|
||||
* depends on the target platform and is typically a signed 32-bit integer.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
private function getInputSize(): int
|
||||
{
|
||||
switch ($this->getInputType())
|
||||
{
|
||||
case self::INPUT_DATA:
|
||||
return function_exists('mb_strlen') ? mb_strlen($this->data, '8bit') : strlen($this->data);
|
||||
break;
|
||||
|
||||
case self::INPUT_FILE:
|
||||
clearstatcache(true, $this->file);
|
||||
|
||||
$filesize = @filesize($this->file);
|
||||
|
||||
return ($filesize === false) ? 0 : $filesize;
|
||||
break;
|
||||
|
||||
case self::INPUT_RESOURCE:
|
||||
$meta = stream_get_meta_data($this->fp);
|
||||
|
||||
if ($meta['seekable'])
|
||||
{
|
||||
$pos = ftell($this->fp);
|
||||
$endPos = fseek($this->fp, 0, SEEK_END);
|
||||
fseek($this->fp, $pos, SEEK_SET);
|
||||
|
||||
return $endPos - $pos + 1;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the MIME type of a file
|
||||
*
|
||||
* @param string $file The absolute path to the file for which we want to get the MIME type
|
||||
*
|
||||
* @return string The MIME type of the file
|
||||
*/
|
||||
private function getMimeType(string $file): string
|
||||
{
|
||||
$type = false;
|
||||
|
||||
// Fileinfo documentation says fileinfo_open() will use the
|
||||
// MAGIC env var for the magic file
|
||||
if (extension_loaded('fileinfo') && isset($_ENV['MAGIC']) &&
|
||||
($finfo = finfo_open(FILEINFO_MIME, $_ENV['MAGIC'])) !== false
|
||||
)
|
||||
{
|
||||
if (($type = finfo_file($finfo, $file)) !== false)
|
||||
{
|
||||
// Remove the charset and grab the last content-type
|
||||
$type = explode(' ', str_replace('; charset=', ';charset=', $type));
|
||||
$type = array_pop($type);
|
||||
$type = explode(';', $type);
|
||||
$type = trim(array_shift($type));
|
||||
}
|
||||
|
||||
finfo_close($finfo);
|
||||
}
|
||||
elseif (function_exists('mime_content_type'))
|
||||
{
|
||||
$type = trim(mime_content_type($file));
|
||||
}
|
||||
|
||||
if ($type !== false && strlen($type) > 0)
|
||||
{
|
||||
return $type;
|
||||
}
|
||||
|
||||
// Otherwise do it the old fashioned way
|
||||
static $exts = [
|
||||
'jpg' => 'image/jpeg',
|
||||
'gif' => 'image/gif',
|
||||
'png' => 'image/png',
|
||||
'tif' => 'image/tiff',
|
||||
'tiff' => 'image/tiff',
|
||||
'ico' => 'image/x-icon',
|
||||
'swf' => 'application/x-shockwave-flash',
|
||||
'pdf' => 'application/pdf',
|
||||
'zip' => 'application/zip',
|
||||
'gz' => 'application/x-gzip',
|
||||
'tar' => 'application/x-tar',
|
||||
'bz' => 'application/x-bzip',
|
||||
'bz2' => 'application/x-bzip2',
|
||||
'txt' => 'text/plain',
|
||||
'asc' => 'text/plain',
|
||||
'htm' => 'text/html',
|
||||
'html' => 'text/html',
|
||||
'css' => 'text/css',
|
||||
'js' => 'text/javascript',
|
||||
'xml' => 'text/xml',
|
||||
'xsl' => 'application/xsl+xml',
|
||||
'ogg' => 'application/ogg',
|
||||
'mp3' => 'audio/mpeg',
|
||||
'wav' => 'audio/x-wav',
|
||||
'avi' => 'video/x-msvideo',
|
||||
'mpg' => 'video/mpeg',
|
||||
'mpeg' => 'video/mpeg',
|
||||
'mov' => 'video/quicktime',
|
||||
'flv' => 'video/x-flv',
|
||||
'php' => 'text/x-php',
|
||||
];
|
||||
|
||||
$ext = strtolower(pathInfo($file, PATHINFO_EXTENSION));
|
||||
|
||||
return isset($exts[$ext]) ? $exts[$ext] : 'application/octet-stream';
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the MD5 sum of the input data
|
||||
*
|
||||
* @return string Base-64 encoded MD5 sum
|
||||
*/
|
||||
private function calculateMd5(): string
|
||||
{
|
||||
switch ($this->getInputType())
|
||||
{
|
||||
case self::INPUT_DATA:
|
||||
return base64_encode(md5($this->data, true));
|
||||
break;
|
||||
|
||||
case self::INPUT_FILE:
|
||||
return base64_encode(md5_file($this->file, true));
|
||||
break;
|
||||
|
||||
case self::INPUT_RESOURCE:
|
||||
$ctx = hash_init('md5');
|
||||
$pos = ftell($this->fp);
|
||||
$size = $this->getSize();
|
||||
$done = 0;
|
||||
$batch = min(1048576, $size);
|
||||
|
||||
while ($done < $size)
|
||||
{
|
||||
$toRead = min($batch, $done - $size);
|
||||
$data = @fread($this->fp, $toRead);
|
||||
hash_update($ctx, $data);
|
||||
unset($data);
|
||||
}
|
||||
|
||||
fseek($this->fp, $pos, SEEK_SET);
|
||||
|
||||
return base64_encode(hash_final($ctx, true));
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcualte the SHA256 data of the input data
|
||||
*
|
||||
* @return string Lowercase hex representation of the SHA-256 sum
|
||||
*/
|
||||
private function calculateSha256(): string
|
||||
{
|
||||
$inputType = $this->getInputType();
|
||||
switch ($inputType)
|
||||
{
|
||||
case self::INPUT_DATA:
|
||||
return hash('sha256', $this->data, false);
|
||||
break;
|
||||
|
||||
case self::INPUT_FILE:
|
||||
case self::INPUT_RESOURCE:
|
||||
if ($inputType == self::INPUT_FILE)
|
||||
{
|
||||
$filesize = @filesize($this->file);
|
||||
$fPos = @ftell($this->fp);
|
||||
|
||||
if (($filesize == $this->getSize()) && ($fPos === 0))
|
||||
{
|
||||
return hash_file('sha256', $this->file, false);
|
||||
}
|
||||
}
|
||||
|
||||
$ctx = hash_init('sha256');
|
||||
$pos = ftell($this->fp);
|
||||
$size = $this->getSize();
|
||||
$done = 0;
|
||||
$batch = min(1048576, $size);
|
||||
|
||||
while ($done < $size)
|
||||
{
|
||||
$toRead = min($batch, $size - $done);
|
||||
$data = @fread($this->fp, $toRead);
|
||||
$done += $toRead;
|
||||
hash_update($ctx, $data);
|
||||
unset($data);
|
||||
}
|
||||
|
||||
fseek($this->fp, $pos, SEEK_SET);
|
||||
|
||||
return hash_final($ctx, false);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
761
s3_storage/vendor/akeeba/s3/src/Request.php
vendored
Normal file
761
s3_storage/vendor/akeeba/s3/src/Request.php
vendored
Normal file
|
@ -0,0 +1,761 @@
|
|||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
*
|
||||
* @package akeebaengine
|
||||
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Postproc\Connector\S3v4;
|
||||
|
||||
use Akeeba\Engine\Postproc\Connector\S3v4\Response\Error;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
|
||||
class Request
|
||||
{
|
||||
/**
|
||||
* The HTTP verb to use
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $verb = 'GET';
|
||||
|
||||
/**
|
||||
* The bucket we are using
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $bucket = '';
|
||||
|
||||
/**
|
||||
* The object URI, relative to the bucket's root
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $uri = '';
|
||||
|
||||
/**
|
||||
* The remote resource we are querying
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $resource = '';
|
||||
|
||||
/**
|
||||
* Query string parameters
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $parameters = [];
|
||||
|
||||
/**
|
||||
* Amazon-specific headers to pass to the request
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $amzHeaders = [];
|
||||
|
||||
/**
|
||||
* Regular HTTP headers to send in the request
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $headers = [
|
||||
'Host' => '',
|
||||
'Date' => '',
|
||||
'Content-MD5' => '',
|
||||
'Content-Type' => '',
|
||||
];
|
||||
|
||||
/**
|
||||
* Input data for the request
|
||||
*
|
||||
* @var Input
|
||||
*/
|
||||
private $input = null;
|
||||
|
||||
/**
|
||||
* The file resource we are writing data to
|
||||
*
|
||||
* @var resource|null
|
||||
*/
|
||||
private $fp = null;
|
||||
|
||||
/**
|
||||
* The Amazon S3 configuration object
|
||||
*
|
||||
* @var Configuration
|
||||
*/
|
||||
private $configuration = null;
|
||||
|
||||
/**
|
||||
* The response object
|
||||
*
|
||||
* @var Response
|
||||
*/
|
||||
private $response = null;
|
||||
|
||||
/**
|
||||
* The location of the CA certificate cache. It can be a file or a directory. If it's not specified, the location
|
||||
* set in AKEEBA_CACERT_PEM will be used
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
private $caCertLocation = null;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param string $verb HTTP verb, e.g. 'POST'
|
||||
* @param string $bucket Bucket name, e.g. 'example-bucket'
|
||||
* @param string $uri Object URI
|
||||
* @param Configuration $configuration The Amazon S3 configuration object to use
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function __construct(string $verb, string $bucket, string $uri, Configuration $configuration)
|
||||
{
|
||||
$this->verb = $verb;
|
||||
$this->bucket = $bucket;
|
||||
$this->uri = '/';
|
||||
$this->configuration = $configuration;
|
||||
|
||||
if (!empty($uri))
|
||||
{
|
||||
$this->uri = '/' . str_replace('%2F', '/', rawurlencode($uri));
|
||||
}
|
||||
|
||||
$this->headers['Host'] = $this->getHostName($configuration, $this->bucket);
|
||||
$this->resource = $this->uri;
|
||||
|
||||
if (($this->bucket !== '') && $configuration->getUseLegacyPathStyle())
|
||||
{
|
||||
$this->resource = '/' . $this->bucket . $this->uri;
|
||||
|
||||
$this->uri = $this->resource;
|
||||
}
|
||||
|
||||
// The date must always be added as a header
|
||||
$this->headers['Date'] = gmdate('D, d M Y H:i:s O');
|
||||
|
||||
// If there is a security token we need to set up the X-Amz-Security-Token header
|
||||
$token = $this->configuration->getToken();
|
||||
|
||||
if (!empty($token))
|
||||
{
|
||||
$this->setAmzHeader('x-amz-security-token', $token);
|
||||
}
|
||||
|
||||
// Initialize the response object
|
||||
$this->response = new Response();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the input object
|
||||
*
|
||||
* @return Input|null
|
||||
*/
|
||||
public function getInput(): ?Input
|
||||
{
|
||||
return $this->input;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the input object
|
||||
*
|
||||
* @param Input $input
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setInput(Input $input): void
|
||||
{
|
||||
$this->input = $input;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a request parameter
|
||||
*
|
||||
* @param string $key The parameter name
|
||||
* @param string|null $value The parameter value
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setParameter(string $key, ?string $value): void
|
||||
{
|
||||
$this->parameters[$key] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a request header
|
||||
*
|
||||
* @param string $key The header name
|
||||
* @param string $value The header value
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setHeader(string $key, string $value): void
|
||||
{
|
||||
$this->headers[$key] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an x-amz-meta-* header
|
||||
*
|
||||
* @param string $key The header name
|
||||
* @param string $value The header value
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setAmzHeader(string $key, string $value): void
|
||||
{
|
||||
$this->amzHeaders[$key] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the HTTP verb of this request
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getVerb(): string
|
||||
{
|
||||
return $this->verb;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the S3 bucket's name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getBucket(): string
|
||||
{
|
||||
return $this->bucket;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the absolute URI of the resource we're accessing
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getResource(): string
|
||||
{
|
||||
return $this->resource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the parameters array
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getParameters(): array
|
||||
{
|
||||
return $this->parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Amazon headers array
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getAmzHeaders(): array
|
||||
{
|
||||
return $this->amzHeaders;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the other headers array
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getHeaders(): array
|
||||
{
|
||||
return $this->headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a reference to the Amazon configuration object
|
||||
*
|
||||
* @return Configuration
|
||||
*/
|
||||
public function getConfiguration(): Configuration
|
||||
{
|
||||
return $this->configuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the file pointer resource (for PUT and POST requests)
|
||||
*
|
||||
* @return resource|null
|
||||
*/
|
||||
public function &getFp()
|
||||
{
|
||||
return $this->fp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the data resource as a file pointer
|
||||
*
|
||||
* @param resource $fp
|
||||
*/
|
||||
public function setFp($fp): void
|
||||
{
|
||||
$this->fp = $fp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the certificate authority location
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getCaCertLocation(): ?string
|
||||
{
|
||||
if (!empty($this->caCertLocation))
|
||||
{
|
||||
return $this->caCertLocation;
|
||||
}
|
||||
|
||||
if (defined('AKEEBA_CACERT_PEM'))
|
||||
{
|
||||
return AKEEBA_CACERT_PEM;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param null|string $caCertLocation
|
||||
*/
|
||||
public function setCaCertLocation(?string $caCertLocation): void
|
||||
{
|
||||
if (empty($caCertLocation))
|
||||
{
|
||||
$caCertLocation = null;
|
||||
}
|
||||
|
||||
if (!is_null($caCertLocation) && !is_file($caCertLocation) && !is_dir($caCertLocation))
|
||||
{
|
||||
$caCertLocation = null;
|
||||
}
|
||||
|
||||
$this->caCertLocation = $caCertLocation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a pre-signed URL for the request.
|
||||
*
|
||||
* Typically used to pre-sign GET requests to objects, i.e. give shareable pre-authorized URLs for downloading
|
||||
* private or otherwise inaccessible files from S3.
|
||||
*
|
||||
* @param int|null $lifetime Lifetime in seconds
|
||||
* @param bool $https Use HTTPS ($hostBucket should be false for SSL verification)?
|
||||
*
|
||||
* @return string The authenticated URL, complete with signature
|
||||
*/
|
||||
public function getAuthenticatedURL(?int $lifetime = null, bool $https = false): string
|
||||
{
|
||||
$this->processParametersIntoResource();
|
||||
$signer = Signature::getSignatureObject($this, $this->configuration->getSignatureMethod());
|
||||
|
||||
return $signer->getAuthenticatedURL($lifetime, $https);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the S3 response
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function getResponse(): Response
|
||||
{
|
||||
$this->processParametersIntoResource();
|
||||
|
||||
$schema = 'http://';
|
||||
|
||||
if ($this->configuration->isSSL())
|
||||
{
|
||||
$schema = 'https://';
|
||||
}
|
||||
|
||||
// Very special case. IF the URI ends in /?location AND the region is us-east-1 (Host is
|
||||
// s3-external-1.amazonaws.com) THEN the host MUST become s3.amazonaws.com for the request to work. This is case
|
||||
// of us not knowing the region of the bucket, therefore having to use a special endpoint which lets us query
|
||||
// the region of the bucket without knowing its region. See
|
||||
// http://stackoverflow.com/questions/27091816/retrieve-buckets-objects-without-knowing-buckets-region-with-aws-s3-rest-api
|
||||
if ((substr($this->uri, -10) == '/?location') && ($this->headers['Host'] == 's3-external-1.amazonaws.com'))
|
||||
{
|
||||
$this->headers['Host'] = 's3.amazonaws.com';
|
||||
}
|
||||
|
||||
$url = $schema . $this->headers['Host'] . $this->uri;
|
||||
|
||||
// Basic setup
|
||||
$curl = curl_init();
|
||||
curl_setopt($curl, CURLOPT_USERAGENT, 'AkeebaBackupProfessional/S3PostProcessor');
|
||||
|
||||
if ($this->configuration->isSSL())
|
||||
{
|
||||
// Set the CA certificate cache location
|
||||
$caCert = $this->getCaCertLocation();
|
||||
|
||||
if (!empty($caCert))
|
||||
{
|
||||
if (is_dir($caCert))
|
||||
{
|
||||
@curl_setopt($curl, CURLOPT_CAPATH, $caCert);
|
||||
}
|
||||
else
|
||||
{
|
||||
@curl_setopt($curl, CURLOPT_CAINFO, $caCert);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the host name in the certificate and the certificate itself.
|
||||
*
|
||||
* Caveat: if your bucket contains dots in the name we have to turn off host verification due to the way the
|
||||
* S3 SSL certificates are set up.
|
||||
*/
|
||||
$isAmazonS3 = (substr($this->headers['Host'], -14) == '.amazonaws.com') ||
|
||||
substr($this->headers['Host'], -16) == 'amazonaws.com.cn';
|
||||
$tooManyDots = substr_count($this->headers['Host'], '.') > 4;
|
||||
|
||||
$verifyHost = ($isAmazonS3 && $tooManyDots) ? 0 : 2;
|
||||
|
||||
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, $verifyHost);
|
||||
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, true);
|
||||
}
|
||||
|
||||
curl_setopt($curl, CURLOPT_URL, $url);
|
||||
|
||||
$signer = Signature::getSignatureObject($this, $this->configuration->getSignatureMethod());
|
||||
$signer->preProcessHeaders($this->headers, $this->amzHeaders);
|
||||
|
||||
// Headers
|
||||
$headers = [];
|
||||
|
||||
foreach ($this->amzHeaders as $header => $value)
|
||||
{
|
||||
if (strlen($value) > 0)
|
||||
{
|
||||
$headers[] = $header . ': ' . $value;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->headers as $header => $value)
|
||||
{
|
||||
if (strlen($value) > 0)
|
||||
{
|
||||
$headers[] = $header . ': ' . $value;
|
||||
}
|
||||
}
|
||||
|
||||
$headers[] = 'Authorization: ' . $signer->getAuthorizationHeader();
|
||||
|
||||
curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
|
||||
curl_setopt($curl, CURLOPT_HEADER, false);
|
||||
curl_setopt($curl, CURLOPT_RETURNTRANSFER, false);
|
||||
curl_setopt($curl, CURLOPT_WRITEFUNCTION, [$this, '__responseWriteCallback']);
|
||||
curl_setopt($curl, CURLOPT_HEADERFUNCTION, [$this, '__responseHeaderCallback']);
|
||||
curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
|
||||
|
||||
// Request types
|
||||
switch ($this->verb)
|
||||
{
|
||||
case 'GET':
|
||||
break;
|
||||
|
||||
case 'PUT':
|
||||
case 'POST':
|
||||
if (!is_object($this->input) || !($this->input instanceof Input))
|
||||
{
|
||||
$this->input = new Input();
|
||||
}
|
||||
|
||||
$size = $this->input->getSize();
|
||||
$type = $this->input->getInputType();
|
||||
|
||||
if ($type == Input::INPUT_DATA)
|
||||
{
|
||||
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $this->verb);
|
||||
|
||||
$data = $this->input->getDataReference();
|
||||
|
||||
if (strlen($data))
|
||||
{
|
||||
curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
|
||||
}
|
||||
|
||||
if ($size > 0)
|
||||
{
|
||||
curl_setopt($curl, CURLOPT_BUFFERSIZE, $size);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
curl_setopt($curl, CURLOPT_PUT, true);
|
||||
curl_setopt($curl, CURLOPT_INFILE, $this->input->getFp());
|
||||
|
||||
if ($size > 0)
|
||||
{
|
||||
curl_setopt($curl, CURLOPT_INFILESIZE, $size);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
break;
|
||||
|
||||
case 'HEAD':
|
||||
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'HEAD');
|
||||
curl_setopt($curl, CURLOPT_NOBODY, true);
|
||||
break;
|
||||
|
||||
case 'DELETE':
|
||||
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'DELETE');
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// Execute, grab errors
|
||||
$this->response->resetBody();
|
||||
|
||||
if (curl_exec($curl))
|
||||
{
|
||||
$this->response->code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->response->error = new Error(
|
||||
curl_errno($curl),
|
||||
curl_error($curl),
|
||||
$this->resource
|
||||
);
|
||||
}
|
||||
|
||||
@curl_close($curl);
|
||||
|
||||
// Set the body data
|
||||
$this->response->finaliseBody();
|
||||
|
||||
// Clean up file resources
|
||||
if (!is_null($this->fp) && is_resource($this->fp))
|
||||
{
|
||||
fclose($this->fp);
|
||||
}
|
||||
|
||||
return $this->response;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* cURL write callback
|
||||
*
|
||||
* @param resource &$curl cURL resource
|
||||
* @param string &$data Data
|
||||
*
|
||||
* @return int Length in bytes
|
||||
*/
|
||||
protected function __responseWriteCallback($curl, string $data): int
|
||||
{
|
||||
if (in_array($this->response->code, [200, 206]) && !is_null($this->fp) && is_resource($this->fp))
|
||||
{
|
||||
return fwrite($this->fp, $data);
|
||||
}
|
||||
|
||||
$this->response->addToBody($data);
|
||||
|
||||
return strlen($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* cURL header callback
|
||||
*
|
||||
* @param resource $curl cURL resource
|
||||
* @param string &$data Data
|
||||
*
|
||||
* @return int Length in bytes
|
||||
*/
|
||||
protected function __responseHeaderCallback($curl, string $data): int
|
||||
{
|
||||
if (($strlen = strlen($data)) <= 2)
|
||||
{
|
||||
return $strlen;
|
||||
}
|
||||
|
||||
if (substr($data, 0, 4) == 'HTTP')
|
||||
{
|
||||
$this->response->code = (int) substr($data, 9, 3);
|
||||
|
||||
return $strlen;
|
||||
}
|
||||
|
||||
[$header, $value] = explode(': ', trim($data), 2);
|
||||
|
||||
switch (strtolower($header))
|
||||
{
|
||||
case 'last-modified':
|
||||
$this->response->setHeader('time', strtotime($value));
|
||||
break;
|
||||
|
||||
case 'content-length':
|
||||
$this->response->setHeader('size', (int) $value);
|
||||
break;
|
||||
|
||||
case 'content-type':
|
||||
$this->response->setHeader('type', $value);
|
||||
break;
|
||||
|
||||
case 'etag':
|
||||
$this->response->setHeader('hash', $value[0] == '"' ? substr($value, 1, -1) : $value);
|
||||
break;
|
||||
|
||||
default:
|
||||
if (preg_match('/^x-amz-meta-.*$/', $header))
|
||||
{
|
||||
$this->setHeader($header, is_numeric($value) ? (int) $value : $value);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return $strlen;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes $this->parameters as a query string into $this->resource
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function processParametersIntoResource(): void
|
||||
{
|
||||
if (count($this->parameters))
|
||||
{
|
||||
$query = substr($this->uri, -1) !== '?' ? '?' : '&';
|
||||
|
||||
ksort($this->parameters);
|
||||
|
||||
foreach ($this->parameters as $var => $value)
|
||||
{
|
||||
if ($value == null || $value == '')
|
||||
{
|
||||
$query .= $var . '&';
|
||||
}
|
||||
else
|
||||
{
|
||||
// Parameters must be URL-encoded
|
||||
$query .= $var . '=' . rawurlencode($value) . '&';
|
||||
}
|
||||
}
|
||||
|
||||
$query = substr($query, 0, -1);
|
||||
$this->uri .= $query;
|
||||
|
||||
if (array_key_exists('acl', $this->parameters) ||
|
||||
array_key_exists('location', $this->parameters) ||
|
||||
array_key_exists('torrent', $this->parameters) ||
|
||||
array_key_exists('logging', $this->parameters) ||
|
||||
array_key_exists('uploads', $this->parameters) ||
|
||||
array_key_exists('uploadId', $this->parameters) ||
|
||||
array_key_exists('partNumber', $this->parameters)
|
||||
)
|
||||
{
|
||||
$this->resource .= $query;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the region-specific hostname for an operation given a configuration and a bucket name. This ensures we can
|
||||
* always use an HTTPS connection, even with buckets containing dots in their names, without SSL certificate host
|
||||
* name validation issues.
|
||||
*
|
||||
* Please note that this requires the pathStyle flag to be set in Configuration because Amazon RECOMMENDS using the
|
||||
* virtual-hosted style request where applicable. See http://docs.aws.amazon.com/AmazonS3/latest/API/APIRest.html
|
||||
* Quoting this documentation:
|
||||
* "Although the path-style is still supported for legacy applications, we recommend using the virtual-hosted style
|
||||
* where applicable."
|
||||
*
|
||||
* @param Configuration $configuration
|
||||
* @param string $bucket
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function getHostName(Configuration $configuration, string $bucket): string
|
||||
{
|
||||
// http://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region
|
||||
$endpoint = $configuration->getEndpoint();
|
||||
$region = $configuration->getRegion();
|
||||
|
||||
// If it's a bucket in China we need to use a different endpoint
|
||||
if (($endpoint == 's3.amazonaws.com') && (substr($region, 0, 3) == 'cn-'))
|
||||
{
|
||||
$endpoint = 'amazonaws.com.cn';
|
||||
}
|
||||
|
||||
/**
|
||||
* If there is no bucket we use the default endpoint, whatever it is. For Amazon S3 this format is only used
|
||||
* when we are making account-level, cross-region requests, e.g. list all buckets. For S3-compatible APIs it
|
||||
* depends on the API, but generally it's just for listing available buckets.
|
||||
*/
|
||||
if (empty($bucket))
|
||||
{
|
||||
return $endpoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Are we using v2 signatures? In this case we use the endpoint defined by the user without translating it.
|
||||
*/
|
||||
if ($configuration->getSignatureMethod() != 'v4')
|
||||
{
|
||||
// Legacy path style: the hostname is the endpoint
|
||||
if ($configuration->getUseLegacyPathStyle())
|
||||
{
|
||||
return $endpoint;
|
||||
}
|
||||
|
||||
// Virtual hosting style: the hostname is the bucket, dot and endpoint.
|
||||
return $bucket . '.' . $endpoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* When using the Amazon S3 with the v4 signature API we have to use a different hostname per region. The
|
||||
* mapping can be found in https://docs.aws.amazon.com/general/latest/gr/s3.html#s3_region
|
||||
*
|
||||
* This means changing the endpoint to s3.REGION.amazonaws.com with the following exceptions:
|
||||
* For China: s3.REGION.amazonaws.com.cn
|
||||
*
|
||||
* v4 signing does NOT support non-Amazon endpoints.
|
||||
*/
|
||||
|
||||
// Most endpoints: s3-REGION.amazonaws.com
|
||||
$regionalEndpoint = $region . '.amazonaws.com';
|
||||
|
||||
// Exception: China
|
||||
if (substr($region, 0, 3) == 'cn-')
|
||||
{
|
||||
// Chinese endpoint, e.g.: s3.cn-north-1.amazonaws.com.cn
|
||||
$regionalEndpoint = $regionalEndpoint . '.cn';
|
||||
}
|
||||
|
||||
// If dual-stack URLs are enabled then prepend the endpoint
|
||||
if ($configuration->getDualstackUrl())
|
||||
{
|
||||
$endpoint = 's3.dualstack.' . $regionalEndpoint;
|
||||
}
|
||||
else
|
||||
{
|
||||
$endpoint = 's3.' . $regionalEndpoint;
|
||||
}
|
||||
|
||||
// Legacy path style access: return just the endpoint
|
||||
if ($configuration->getUseLegacyPathStyle())
|
||||
{
|
||||
return $endpoint;
|
||||
}
|
||||
|
||||
// Recommended virtual hosting access: bucket, dot, endpoint.
|
||||
return $bucket . '.' . $endpoint;
|
||||
}
|
||||
}
|
345
s3_storage/vendor/akeeba/s3/src/Response.php
vendored
Normal file
345
s3_storage/vendor/akeeba/s3/src/Response.php
vendored
Normal file
|
@ -0,0 +1,345 @@
|
|||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
*
|
||||
* @package akeebaengine
|
||||
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Postproc\Connector\S3v4;
|
||||
|
||||
use Akeeba\Engine\Postproc\Connector\S3v4\Exception\PropertyNotFound;
|
||||
use Akeeba\Engine\Postproc\Connector\S3v4\Response\Error;
|
||||
use SimpleXMLElement;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
/**
|
||||
* Amazon S3 API response object
|
||||
*
|
||||
* @property Error $error Response error object
|
||||
* @property string|SimpleXMLElement|null $body Body data
|
||||
* @property int $code Response code
|
||||
* @property array $headers Any headers we may have
|
||||
*/
|
||||
class Response
|
||||
{
|
||||
/**
|
||||
* Error object
|
||||
*
|
||||
* @var Error
|
||||
*/
|
||||
private $error = null;
|
||||
|
||||
/**
|
||||
* Response body
|
||||
*
|
||||
* @var string|SimpleXMLElement|null
|
||||
*/
|
||||
private $body = null;
|
||||
|
||||
/**
|
||||
* Status code of the response, e.g. 200 for OK, 403 for Forbidden etc
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $code = 0;
|
||||
|
||||
/**
|
||||
* Response headers
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $headers = [];
|
||||
|
||||
/**
|
||||
* Response constructor.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->error = new Error();
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this an error response?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isError(): bool
|
||||
{
|
||||
return is_null($this->error) || $this->error->isError();
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this response have a body?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasBody(): bool
|
||||
{
|
||||
return !empty($this->body);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the response error object
|
||||
*
|
||||
* @return Error
|
||||
*/
|
||||
public function getError(): Error
|
||||
{
|
||||
return $this->error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the response error object
|
||||
*
|
||||
* @param Error $error
|
||||
*/
|
||||
public function setError(Error $error): void
|
||||
{
|
||||
$this->error = $error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the response body
|
||||
*
|
||||
* If there is no body set up you get NULL.
|
||||
*
|
||||
* If the body is binary data (e.g. downloading a file) or other non-XML data you get a string.
|
||||
*
|
||||
* If the body was an XML string – the standard Amazon S3 REST API response type – you get a SimpleXMLElement
|
||||
* object.
|
||||
*
|
||||
* @return string|SimpleXMLElement|null
|
||||
*/
|
||||
public function getBody()
|
||||
{
|
||||
return $this->body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the response body. If it's a string we'll try to parse it as XML.
|
||||
*
|
||||
* @param string|SimpleXMLElement|null $body
|
||||
*/
|
||||
public function setBody($body): void
|
||||
{
|
||||
$this->body = null;
|
||||
|
||||
if (empty($body))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
$this->body = $body;
|
||||
|
||||
$this->finaliseBody();
|
||||
}
|
||||
|
||||
public function resetBody(): void
|
||||
{
|
||||
$this->body = null;
|
||||
}
|
||||
|
||||
public function addToBody(string $data): void
|
||||
{
|
||||
if (empty($this->body))
|
||||
{
|
||||
$this->body = '';
|
||||
}
|
||||
|
||||
$this->body .= $data;
|
||||
}
|
||||
|
||||
public function finaliseBody(): void
|
||||
{
|
||||
if (!$this->hasBody())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isset($this->headers['type']))
|
||||
{
|
||||
$this->headers['type'] = 'text/plain';
|
||||
}
|
||||
|
||||
if (is_string($this->body) &&
|
||||
(($this->headers['type'] == 'application/xml') || (substr($this->body, 0, 5) == '<?xml'))
|
||||
)
|
||||
{
|
||||
$this->body = simplexml_load_string($this->body);
|
||||
}
|
||||
|
||||
if (is_object($this->body) && ($this->body instanceof SimpleXMLElement))
|
||||
{
|
||||
$this->parseBody();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the status code of the response
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getCode(): int
|
||||
{
|
||||
return $this->code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the status code of the response
|
||||
*
|
||||
* @param int $code
|
||||
*/
|
||||
public function setCode(int $code): void
|
||||
{
|
||||
$this->code = $code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the response headers
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getHeaders(): array
|
||||
{
|
||||
return $this->headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the response headers
|
||||
*
|
||||
* @param array $headers
|
||||
*/
|
||||
public function setHeaders(array $headers): void
|
||||
{
|
||||
$this->headers = $headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a single header
|
||||
*
|
||||
* @param string $name The header name
|
||||
* @param string $value The header value
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setHeader(string $name, string $value): void
|
||||
{
|
||||
$this->headers[$name] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does a header by this name exist?
|
||||
*
|
||||
* @param string $name The header to look for
|
||||
*
|
||||
* @return bool True if it exists
|
||||
*/
|
||||
public function hasHeader(string $name): bool
|
||||
{
|
||||
return array_key_exists($name, $this->headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unset a response header
|
||||
*
|
||||
* @param string $name The header to unset
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function unsetHeader(string $name): void
|
||||
{
|
||||
if ($this->hasHeader($name))
|
||||
{
|
||||
unset ($this->headers[$name]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic getter for the protected properties
|
||||
*
|
||||
* @param string $name
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function __get(string $name)
|
||||
{
|
||||
switch ($name)
|
||||
{
|
||||
case 'error':
|
||||
return $this->getError();
|
||||
break;
|
||||
|
||||
case 'body':
|
||||
return $this->getBody();
|
||||
break;
|
||||
|
||||
case 'code':
|
||||
return $this->getCode();
|
||||
break;
|
||||
|
||||
case 'headers':
|
||||
return $this->getHeaders();
|
||||
break;
|
||||
}
|
||||
|
||||
throw new PropertyNotFound("Property $name not found in " . get_class($this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic setter for the protected properties
|
||||
*
|
||||
* @param string $name The name of the property
|
||||
* @param mixed $value The value of the property
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __set(string $name, $value): void
|
||||
{
|
||||
switch ($name)
|
||||
{
|
||||
case 'error':
|
||||
$this->setError($value);
|
||||
break;
|
||||
|
||||
case 'body':
|
||||
$this->setBody($value);
|
||||
break;
|
||||
|
||||
case 'code':
|
||||
$this->setCode($value);
|
||||
break;
|
||||
|
||||
case 'headers':
|
||||
$this->setHeaders($value);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new PropertyNotFound("Property $name not found in " . get_class($this));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans the SimpleXMLElement body for errors and propagates them to the Error object
|
||||
*/
|
||||
protected function parseBody(): void
|
||||
{
|
||||
if (!in_array($this->code, [200, 204]) &&
|
||||
isset($this->body->Code, $this->body->Message)
|
||||
)
|
||||
{
|
||||
$this->error = new Error(
|
||||
$this->code,
|
||||
(string) $this->body->Message
|
||||
);
|
||||
|
||||
if (isset($this->body->Resource))
|
||||
{
|
||||
$this->error->setResource((string) $this->body->Resource);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
139
s3_storage/vendor/akeeba/s3/src/Response/Error.php
vendored
Normal file
139
s3_storage/vendor/akeeba/s3/src/Response/Error.php
vendored
Normal file
|
@ -0,0 +1,139 @@
|
|||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
*
|
||||
* @package akeebaengine
|
||||
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Postproc\Connector\S3v4\Response;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
/**
|
||||
* S3 response error object
|
||||
*/
|
||||
class Error
|
||||
{
|
||||
/**
|
||||
* Error code
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $code = 0;
|
||||
|
||||
/**
|
||||
* Error message
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $message = '';
|
||||
|
||||
/**
|
||||
* URI to the resource that throws the error
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $resource = '';
|
||||
|
||||
/**
|
||||
* Create a new error object
|
||||
*
|
||||
* @param int $code The error code
|
||||
* @param string $message The error message
|
||||
* @param string $resource The URI to the resource throwing the error
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function __construct($code = 0, $message = '', $resource = '')
|
||||
{
|
||||
$this->setCode($code);
|
||||
$this->setMessage($message);
|
||||
$this->setResource($resource);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the error code
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getCode(): int
|
||||
{
|
||||
return $this->code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the error code
|
||||
*
|
||||
* @param int $code Set to zeroo or a negative value to clear errors
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setCode(int $code): void
|
||||
{
|
||||
if ($code <= 0)
|
||||
{
|
||||
$code = 0;
|
||||
$this->setMessage('');
|
||||
$this->setResource('');
|
||||
}
|
||||
|
||||
$this->code = $code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the error message
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getMessage(): string
|
||||
{
|
||||
return $this->message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the error message
|
||||
*
|
||||
* @param string $message The error message to set
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setMessage(string $message): void
|
||||
{
|
||||
$this->message = $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URI of the resource throwing the error
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getResource(): string
|
||||
{
|
||||
return $this->resource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the URI of the resource throwing the error
|
||||
*
|
||||
* @param string $resource
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setResource(string $resource): void
|
||||
{
|
||||
$this->resource = $resource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Do we actually have an error?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isError(): bool
|
||||
{
|
||||
return ($this->code > 0) || !empty($this->message);
|
||||
}
|
||||
}
|
80
s3_storage/vendor/akeeba/s3/src/Signature.php
vendored
Normal file
80
s3_storage/vendor/akeeba/s3/src/Signature.php
vendored
Normal file
|
@ -0,0 +1,80 @@
|
|||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
*
|
||||
* @package akeebaengine
|
||||
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Postproc\Connector\S3v4;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
/**
|
||||
* Base class for request signing objects.
|
||||
*/
|
||||
abstract class Signature
|
||||
{
|
||||
/**
|
||||
* The request we will be signing
|
||||
*
|
||||
* @var Request
|
||||
*/
|
||||
protected $request = null;
|
||||
|
||||
/**
|
||||
* Signature constructor.
|
||||
*
|
||||
* @param Request $request The request we will be signing
|
||||
*/
|
||||
public function __construct(Request $request)
|
||||
{
|
||||
$this->request = $request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a signature object for the request
|
||||
*
|
||||
* @param Request $request The request which needs signing
|
||||
* @param string $method The signature method, "v2" or "v4"
|
||||
*
|
||||
* @return Signature
|
||||
*/
|
||||
public static function getSignatureObject(Request $request, string $method = 'v2'): self
|
||||
{
|
||||
$className = '\\Akeeba\\Engine\\Postproc\\Connector\\S3v4\\Signature\\' . ucfirst($method);
|
||||
|
||||
return new $className($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the authorization header for the request
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
abstract public function getAuthorizationHeader(): string;
|
||||
|
||||
/**
|
||||
* Pre-process the request headers before we convert them to cURL-compatible format. Used by signature engines to
|
||||
* add custom headers, e.g. x-amz-content-sha256
|
||||
*
|
||||
* @param array $headers The associative array of headers to process
|
||||
* @param array $amzHeaders The associative array of amz-* headers to process
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
abstract public function preProcessHeaders(array &$headers, array &$amzHeaders): void;
|
||||
|
||||
/**
|
||||
* Get a pre-signed URL for the request. Typically used to pre-sign GET requests to objects, i.e. give shareable
|
||||
* pre-authorized URLs for downloading files from S3.
|
||||
*
|
||||
* @param integer|null $lifetime Lifetime in seconds. NULL for default lifetime.
|
||||
* @param bool $https Use HTTPS ($hostBucket should be false for SSL verification)?
|
||||
*
|
||||
* @return string The authenticated URL, complete with signature
|
||||
*/
|
||||
abstract public function getAuthenticatedURL(?int $lifetime = null, bool $https = false): string;
|
||||
}
|
209
s3_storage/vendor/akeeba/s3/src/Signature/V2.php
vendored
Normal file
209
s3_storage/vendor/akeeba/s3/src/Signature/V2.php
vendored
Normal file
|
@ -0,0 +1,209 @@
|
|||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
*
|
||||
* @package akeebaengine
|
||||
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Postproc\Connector\S3v4\Signature;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
use Akeeba\Engine\Postproc\Connector\S3v4\Signature;
|
||||
|
||||
/**
|
||||
* Implements the Amazon AWS v2 signatures
|
||||
*
|
||||
* @see http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html
|
||||
*/
|
||||
class V2 extends Signature
|
||||
{
|
||||
/**
|
||||
* Pre-process the request headers before we convert them to cURL-compatible format. Used by signature engines to
|
||||
* add custom headers, e.g. x-amz-content-sha256
|
||||
*
|
||||
* @param array $headers The associative array of headers to process
|
||||
* @param array $amzHeaders The associative array of amz-* headers to process
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function preProcessHeaders(array &$headers, array &$amzHeaders): void
|
||||
{
|
||||
// No pre-processing required for V2 signatures
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a pre-signed URL for the request. Typically used to pre-sign GET requests to objects, i.e. give shareable
|
||||
* pre-authorized URLs for downloading files from S3.
|
||||
*
|
||||
* @param integer|null $lifetime Lifetime in seconds. NULL for default lifetime.
|
||||
* @param bool $https Use HTTPS ($hostBucket should be false for SSL verification)?
|
||||
*
|
||||
* @return string The presigned URL
|
||||
*/
|
||||
public function getAuthenticatedURL(?int $lifetime = null, bool $https = false): string
|
||||
{
|
||||
// Set the Expires header
|
||||
if (is_null($lifetime))
|
||||
{
|
||||
$lifetime = 10;
|
||||
}
|
||||
|
||||
$expires = time() + $lifetime;
|
||||
$this->request->setHeader('Expires', $expires);
|
||||
|
||||
$bucket = $this->request->getBucket();
|
||||
$uri = $this->request->getResource();
|
||||
$headers = $this->request->getHeaders();
|
||||
$accessKey = $this->request->getConfiguration()->getAccess();
|
||||
$protocol = $https ? 'https' : 'http';
|
||||
$signature = $this->getAuthorizationHeader();
|
||||
|
||||
$search = '/' . $bucket;
|
||||
|
||||
if (strpos($uri, $search) === 0)
|
||||
{
|
||||
$uri = substr($uri, strlen($search));
|
||||
}
|
||||
|
||||
$queryParameters = array_merge($this->request->getParameters(), [
|
||||
'AWSAccessKeyId' => $accessKey,
|
||||
'Expires' => sprintf('%u', $expires),
|
||||
'Signature' => $signature,
|
||||
]);
|
||||
|
||||
$query = http_build_query($queryParameters);
|
||||
|
||||
// fix authenticated url for Google Cloud Storage - https://cloud.google.com/storage/docs/access-control/create-signed-urls-program
|
||||
if ($this->request->getConfiguration()->getEndpoint() === "storage.googleapis.com")
|
||||
{
|
||||
// replace host with endpoint
|
||||
$headers['Host'] = 'storage.googleapis.com';
|
||||
// replace "AWSAccessKeyId" with "GoogleAccessId"
|
||||
$query = str_replace('AWSAccessKeyId', 'GoogleAccessId', $query);
|
||||
// add bucket to url
|
||||
$uri = '/' . $bucket . $uri;
|
||||
}
|
||||
|
||||
$url = $protocol . '://' . $headers['Host'] . $uri;
|
||||
$url .= (strpos($uri, '?') !== false) ? '&' : '?';
|
||||
$url .= $query;
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the authorization header for the request
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getAuthorizationHeader(): string
|
||||
{
|
||||
$verb = strtoupper($this->request->getVerb());
|
||||
$resourcePath = $this->request->getResource();
|
||||
$headers = $this->request->getHeaders();
|
||||
$amzHeaders = $this->request->getAmzHeaders();
|
||||
$parameters = $this->request->getParameters();
|
||||
$bucket = $this->request->getBucket();
|
||||
$isPresignedURL = false;
|
||||
|
||||
$amz = [];
|
||||
$amzString = '';
|
||||
|
||||
// Collect AMZ headers for signature
|
||||
foreach ($amzHeaders as $header => $value)
|
||||
{
|
||||
if (strlen($value) > 0)
|
||||
{
|
||||
$amz[] = strtolower($header) . ':' . $value;
|
||||
}
|
||||
}
|
||||
|
||||
// AMZ headers must be sorted and sent as separate lines
|
||||
if (sizeof($amz) > 0)
|
||||
{
|
||||
sort($amz);
|
||||
$amzString = "\n" . implode("\n", $amz);
|
||||
}
|
||||
|
||||
// If the Expires query string parameter is set up we're pre-signing a download URL. The string to sign is a bit
|
||||
// different in this case; it does not include the Date, it includes the Expires.
|
||||
// See http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html#RESTAuthenticationQueryStringAuth
|
||||
if (isset($headers['Expires']))
|
||||
{
|
||||
$headers['Date'] = $headers['Expires'];
|
||||
unset ($headers['Expires']);
|
||||
|
||||
$isPresignedURL = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* The resource path in S3 V2 signatures must ALWAYS contain the bucket name if a bucket is defined, even if we
|
||||
* are not using path-style access to the resource
|
||||
*/
|
||||
if (!empty($bucket) && !$this->request->getConfiguration()->getUseLegacyPathStyle())
|
||||
{
|
||||
$resourcePath = '/' . $bucket . $resourcePath;
|
||||
}
|
||||
|
||||
$stringToSign = $verb . "\n" .
|
||||
(isset($headers['Content-MD5']) ? $headers['Content-MD5'] : '') . "\n" .
|
||||
(isset($headers['Content-Type']) ? $headers['Content-Type'] : '') . "\n" .
|
||||
$headers['Date'] .
|
||||
$amzString . "\n" .
|
||||
$resourcePath;
|
||||
|
||||
// CloudFront only requires a date to be signed
|
||||
if ($headers['Host'] == 'cloudfront.amazonaws.com')
|
||||
{
|
||||
$stringToSign = $headers['Date'];
|
||||
}
|
||||
|
||||
$amazonV2Hash = $this->amazonV2Hash($stringToSign);
|
||||
|
||||
// For presigned URLs we only return the Base64-encoded signature without the AWS format specifier and the
|
||||
// public access key.
|
||||
if ($isPresignedURL)
|
||||
{
|
||||
return $amazonV2Hash;
|
||||
}
|
||||
|
||||
return 'AWS ' .
|
||||
$this->request->getConfiguration()->getAccess() . ':' .
|
||||
$amazonV2Hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a HMAC-SHA1 hash. Uses the hash extension if present, otherwise falls back to slower, manual calculation.
|
||||
*
|
||||
* @param string $stringToSign String to sign
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function amazonV2Hash(string $stringToSign): string
|
||||
{
|
||||
$secret = $this->request->getConfiguration()->getSecret();
|
||||
|
||||
if (extension_loaded('hash'))
|
||||
{
|
||||
$raw = hash_hmac('sha1', $stringToSign, $secret, true);
|
||||
|
||||
return base64_encode($raw);
|
||||
}
|
||||
|
||||
$raw = pack('H*', sha1(
|
||||
(str_pad($secret, 64, chr(0x00)) ^ (str_repeat(chr(0x5c), 64))) .
|
||||
pack('H*', sha1(
|
||||
(str_pad($secret, 64, chr(0x00)) ^ (str_repeat(chr(0x36), 64))) . $stringToSign
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
return base64_encode($raw);
|
||||
}
|
||||
|
||||
}
|
385
s3_storage/vendor/akeeba/s3/src/Signature/V4.php
vendored
Normal file
385
s3_storage/vendor/akeeba/s3/src/Signature/V4.php
vendored
Normal file
|
@ -0,0 +1,385 @@
|
|||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
*
|
||||
* @package akeebaengine
|
||||
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Postproc\Connector\S3v4\Signature;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
use Akeeba\Engine\Postproc\Connector\S3v4\Signature;
|
||||
use DateTime;
|
||||
|
||||
/**
|
||||
* Implements the Amazon AWS v4 signatures
|
||||
*
|
||||
* @see http://docs.aws.amazon.com/general/latest/gr/signature-version-4.html
|
||||
*/
|
||||
class V4 extends Signature
|
||||
{
|
||||
/**
|
||||
* Pre-process the request headers before we convert them to cURL-compatible format. Used by signature engines to
|
||||
* add custom headers, e.g. x-amz-content-sha256
|
||||
*
|
||||
* @param array $headers The associative array of headers to process
|
||||
* @param array $amzHeaders The associative array of amz-* headers to process
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function preProcessHeaders(array &$headers, array &$amzHeaders): void
|
||||
{
|
||||
// Do we already have an SHA-256 payload hash?
|
||||
if (isset($amzHeaders['x-amz-content-sha256']))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the payload hash header
|
||||
$input = $this->request->getInput();
|
||||
|
||||
if (is_object($input))
|
||||
{
|
||||
$requestPayloadHash = $input->getSha256();
|
||||
}
|
||||
else
|
||||
{
|
||||
$requestPayloadHash = hash('sha256', '', false);
|
||||
}
|
||||
|
||||
$amzHeaders['x-amz-content-sha256'] = $requestPayloadHash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a pre-signed URL for the request. Typically used to pre-sign GET requests to objects, i.e. give shareable
|
||||
* pre-authorized URLs for downloading files from S3.
|
||||
*
|
||||
* @param integer|null $lifetime Lifetime in seconds. NULL for default lifetime.
|
||||
* @param bool $https Use HTTPS ($hostBucket should be false for SSL verification)?
|
||||
*
|
||||
* @return string The presigned URL
|
||||
*/
|
||||
public function getAuthenticatedURL(?int $lifetime = null, bool $https = false): string
|
||||
{
|
||||
// Set the Expires header
|
||||
if (is_null($lifetime))
|
||||
{
|
||||
$lifetime = 10;
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticated URLs must always go through the generic regional endpoint, not the virtual hosting-style domain
|
||||
* name. This means that if you have a bucket "example" in the EU West 1 (Ireland) region we have to go through
|
||||
* http://s3-eu-west-1.amazonaws.com/example instead of http://example.amazonaws.com/ for all authenticated URLs
|
||||
*/
|
||||
$region = $this->request->getConfiguration()->getRegion();
|
||||
$hostname = $this->getPresignedHostnameForRegion($region);
|
||||
$this->request->setHeader('Host', $hostname);
|
||||
|
||||
// Set the expiration time in seconds
|
||||
$this->request->setHeader('Expires', (int) $lifetime);
|
||||
|
||||
// Get the query parameters, including the calculated signature
|
||||
$bucket = $this->request->getBucket();
|
||||
$uri = $this->request->getResource();
|
||||
$headers = $this->request->getHeaders();
|
||||
$protocol = $https ? 'https' : 'http';
|
||||
$serialisedParams = $this->getAuthorizationHeader();
|
||||
|
||||
// The query parameters are returned serialized; unserialize them, then build and return the URL.
|
||||
$queryParameters = unserialize($serialisedParams);
|
||||
|
||||
$query = http_build_query($queryParameters);
|
||||
|
||||
$url = $protocol . '://' . $headers['Host'] . $uri;
|
||||
$url .= (strpos($uri, '?') !== false) ? '&' : '?';
|
||||
$url .= $query;
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the authorization header for the request
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getAuthorizationHeader(): string
|
||||
{
|
||||
$verb = strtoupper($this->request->getVerb());
|
||||
$resourcePath = $this->request->getResource();
|
||||
$headers = $this->request->getHeaders();
|
||||
$amzHeaders = $this->request->getAmzHeaders();
|
||||
$parameters = $this->request->getParameters();
|
||||
$bucket = $this->request->getBucket();
|
||||
$isPresignedURL = false;
|
||||
|
||||
// See the Connector class for the explanation behind this ugly workaround
|
||||
$amazonIsBraindead = isset($headers['workaround-braindead-error-from-amazon']);
|
||||
|
||||
if ($amazonIsBraindead)
|
||||
{
|
||||
unset ($headers['workaround-braindead-error-from-amazon']);
|
||||
}
|
||||
|
||||
// Get the credentials scope
|
||||
$signatureDate = new DateTime($headers['Date']);
|
||||
|
||||
$credentialScope = $signatureDate->format('Ymd') . '/' .
|
||||
$this->request->getConfiguration()->getRegion() . '/' .
|
||||
's3/aws4_request';
|
||||
|
||||
/**
|
||||
* If the Expires header is set up we're pre-signing a download URL. The string to sign is a bit
|
||||
* different in this case and we have to pass certain headers as query string parameters.
|
||||
*
|
||||
* @see http://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html
|
||||
*/
|
||||
if (isset($headers['Expires']))
|
||||
{
|
||||
$gmtDate = clone $signatureDate;
|
||||
$gmtDate->setTimezone(new \DateTimeZone('GMT'));
|
||||
|
||||
$parameters['X-Amz-Algorithm'] = "AWS4-HMAC-SHA256";
|
||||
$parameters['X-Amz-Credential'] = $this->request->getConfiguration()->getAccess() . '/' . $credentialScope;
|
||||
$parameters['X-Amz-Date'] = $gmtDate->format('Ymd\THis\Z');
|
||||
$parameters['X-Amz-Expires'] = sprintf('%u', $headers['Expires']);
|
||||
$token = $this->request->getConfiguration()->getToken();
|
||||
|
||||
if (!empty($token))
|
||||
{
|
||||
$parameters['x-amz-security-token'] = $token;
|
||||
}
|
||||
|
||||
unset($headers['Expires']);
|
||||
unset($headers['Date']);
|
||||
unset($headers['Content-MD5']);
|
||||
unset($headers['Content-Type']);
|
||||
|
||||
$isPresignedURL = true;
|
||||
}
|
||||
|
||||
// ========== Step 1: Create a canonical request ==========
|
||||
// See http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
|
||||
|
||||
$canonicalHeaders = "";
|
||||
$signedHeadersArray = [];
|
||||
|
||||
// Calculate the canonical headers and the signed headers
|
||||
if ($isPresignedURL)
|
||||
{
|
||||
// Presigned URLs use UNSIGNED-PAYLOAD instead
|
||||
unset($amzHeaders['x-amz-content-sha256']);
|
||||
}
|
||||
|
||||
$allHeaders = array_merge($headers, $amzHeaders);
|
||||
ksort($allHeaders);
|
||||
|
||||
foreach ($allHeaders as $k => $v)
|
||||
{
|
||||
$lowercaseHeaderName = strtolower($k);
|
||||
|
||||
if ($amazonIsBraindead && ($lowercaseHeaderName == 'content-length'))
|
||||
{
|
||||
/**
|
||||
* I know it looks crazy. It is. Somehow Amazon requires me to do this and only on _some_ servers, mind
|
||||
* you. This is something undocumented and which is not covered by their official SDK. I had to write
|
||||
* my own library because of that and the official SDK's inability to upload large files without using
|
||||
* at least as much memory as the file itself (which doesn't fly well for files around 2Gb, let me tell
|
||||
* you that!).
|
||||
*/
|
||||
$v = "$v,$v";
|
||||
}
|
||||
|
||||
$canonicalHeaders .= $lowercaseHeaderName . ':' . trim($v) . "\n";
|
||||
$signedHeadersArray[] = $lowercaseHeaderName;
|
||||
}
|
||||
|
||||
$signedHeaders = implode(';', $signedHeadersArray);
|
||||
|
||||
if ($isPresignedURL)
|
||||
{
|
||||
$parameters['X-Amz-SignedHeaders'] = $signedHeaders;
|
||||
}
|
||||
|
||||
// The canonical URI is the resource path
|
||||
$canonicalURI = $resourcePath;
|
||||
$bucketResource = '/' . $bucket;
|
||||
$regionalHostname = ($headers['Host'] != 's3.amazonaws.com') && ($headers['Host'] != $bucket . '.s3.amazonaws.com');
|
||||
|
||||
// Special case: if the canonical URI ends in /?location the bucket name DOES count as part of the canonical URL
|
||||
// even though the Host is s3.amazonaws.com (in which case it normally shouldn't count). Yeah, I know, it makes
|
||||
// no sense!!!
|
||||
if (!$regionalHostname && ($headers['Host'] == 's3.amazonaws.com') && (substr($canonicalURI, -10) == '/?location'))
|
||||
{
|
||||
$regionalHostname = true;
|
||||
}
|
||||
|
||||
if (!$regionalHostname && (strpos($canonicalURI, $bucketResource) === 0))
|
||||
{
|
||||
if ($canonicalURI === $bucketResource)
|
||||
{
|
||||
$canonicalURI = '/';
|
||||
}
|
||||
else
|
||||
{
|
||||
$canonicalURI = substr($canonicalURI, strlen($bucketResource));
|
||||
}
|
||||
}
|
||||
|
||||
// If the resource path has a query yank it and parse it into the parameters array
|
||||
$questionMarkPos = strpos($canonicalURI, '?');
|
||||
|
||||
if ($questionMarkPos !== false)
|
||||
{
|
||||
$canonicalURI = substr($canonicalURI, 0, $questionMarkPos);
|
||||
$queryString = @substr($canonicalURI, $questionMarkPos + 1);
|
||||
@parse_str($queryString, $extraQuery);
|
||||
|
||||
if (count($extraQuery))
|
||||
{
|
||||
$parameters = array_merge($parameters, $extraQuery);
|
||||
}
|
||||
}
|
||||
|
||||
// The canonical query string is the string representation of $parameters, alpha sorted by key
|
||||
ksort($parameters);
|
||||
|
||||
// We build the query the hard way because http_build_query in PHP 5.3 does NOT have the fourth parameter
|
||||
// (encoding type), defaulting to RFC 1738 encoding whereas S3 expects RFC 3986 encoding
|
||||
$canonicalQueryString = '';
|
||||
|
||||
if (!empty($parameters))
|
||||
{
|
||||
$temp = [];
|
||||
|
||||
foreach ($parameters as $k => $v)
|
||||
{
|
||||
$temp[] = $this->urlencode($k) . '=' . $this->urlencode($v);
|
||||
}
|
||||
|
||||
$canonicalQueryString = implode('&', $temp);
|
||||
}
|
||||
|
||||
// Get the payload hash
|
||||
$requestPayloadHash = 'UNSIGNED-PAYLOAD';
|
||||
|
||||
if (isset($amzHeaders['x-amz-content-sha256']))
|
||||
{
|
||||
$requestPayloadHash = $amzHeaders['x-amz-content-sha256'];
|
||||
}
|
||||
|
||||
// Calculate the canonical request
|
||||
$canonicalRequest = $verb . "\n" .
|
||||
$canonicalURI . "\n" .
|
||||
$canonicalQueryString . "\n" .
|
||||
$canonicalHeaders . "\n" .
|
||||
$signedHeaders . "\n" .
|
||||
$requestPayloadHash;
|
||||
|
||||
$hashedCanonicalRequest = hash('sha256', $canonicalRequest);
|
||||
|
||||
// ========== Step 2: Create a string to sign ==========
|
||||
// See http://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html
|
||||
|
||||
if (!isset($headers['Date']))
|
||||
{
|
||||
$headers['Date'] = '';
|
||||
}
|
||||
|
||||
$stringToSign = "AWS4-HMAC-SHA256\n" .
|
||||
$headers['Date'] . "\n" .
|
||||
$credentialScope . "\n" .
|
||||
$hashedCanonicalRequest;
|
||||
|
||||
if ($isPresignedURL)
|
||||
{
|
||||
$stringToSign = "AWS4-HMAC-SHA256\n" .
|
||||
$parameters['X-Amz-Date'] . "\n" .
|
||||
$credentialScope . "\n" .
|
||||
$hashedCanonicalRequest;
|
||||
}
|
||||
|
||||
// ========== Step 3: Calculate the signature ==========
|
||||
// See http://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html
|
||||
$kSigning = $this->getSigningKey($signatureDate);
|
||||
|
||||
$signature = hash_hmac('sha256', $stringToSign, $kSigning, false);
|
||||
|
||||
// ========== Step 4: Add the signing information to the Request ==========
|
||||
// See http://docs.aws.amazon.com/general/latest/gr/sigv4-add-signature-to-request.html
|
||||
|
||||
$authorization = 'AWS4-HMAC-SHA256 Credential=' .
|
||||
$this->request->getConfiguration()->getAccess() . '/' . $credentialScope . ', ' .
|
||||
'SignedHeaders=' . $signedHeaders . ', ' .
|
||||
'Signature=' . $signature;
|
||||
|
||||
// For presigned URLs we only return the Base64-encoded signature without the AWS format specifier and the
|
||||
// public access key.
|
||||
if ($isPresignedURL)
|
||||
{
|
||||
$parameters['X-Amz-Signature'] = $signature;
|
||||
|
||||
return serialize($parameters);
|
||||
}
|
||||
|
||||
return $authorization;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the AWS4 signing key
|
||||
*
|
||||
* @param DateTime $signatureDate The date the signing key is good for
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function getSigningKey(DateTime $signatureDate): string
|
||||
{
|
||||
$kSecret = $this->request->getConfiguration()->getSecret();
|
||||
$kDate = hash_hmac('sha256', $signatureDate->format('Ymd'), 'AWS4' . $kSecret, true);
|
||||
$kRegion = hash_hmac('sha256', $this->request->getConfiguration()->getRegion(), $kDate, true);
|
||||
$kService = hash_hmac('sha256', 's3', $kRegion, true);
|
||||
$kSigning = hash_hmac('sha256', 'aws4_request', $kService, true);
|
||||
|
||||
return $kSigning;
|
||||
}
|
||||
|
||||
private function urlencode(?string $toEncode): string
|
||||
{
|
||||
if (empty($toEncode))
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
return str_replace('+', '%20', urlencode($toEncode));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the correct hostname for the given AWS region
|
||||
*
|
||||
* @param string $region
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function getPresignedHostnameForRegion(string $region): string
|
||||
{
|
||||
$endpoint = 's3.' . $region . '.amazonaws.com';
|
||||
$dualstackEnabled = $this->request->getConfiguration()->getDualstackUrl();
|
||||
|
||||
// If dual-stack URLs are enabled then prepend the endpoint
|
||||
if ($dualstackEnabled)
|
||||
{
|
||||
$endpoint = 's3.dualstack.' . $region . '.amazonaws.com';
|
||||
}
|
||||
|
||||
if ($region == 'cn-north-1')
|
||||
{
|
||||
return $endpoint . '.cn';
|
||||
}
|
||||
|
||||
return $endpoint;
|
||||
}
|
||||
}
|
98
s3_storage/vendor/akeeba/s3/src/StorageClass.php
vendored
Normal file
98
s3_storage/vendor/akeeba/s3/src/StorageClass.php
vendored
Normal file
|
@ -0,0 +1,98 @@
|
|||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
*
|
||||
* @package akeebaengine
|
||||
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Postproc\Connector\S3v4;
|
||||
|
||||
/**
|
||||
* Amazon S3 Storage Classes
|
||||
*
|
||||
* When you want to override the default storage class of the bucket pass
|
||||
* array('X-Amz-Storage-Class' => StorageClass::STANDARD)
|
||||
* in the $headers array of Connector::putObject().
|
||||
*
|
||||
* Alternatively, run the $headers array through setStorageClass(), e.g.
|
||||
* $headers = array(); // You can put your stuff here
|
||||
* StorageClass::setStorageClass($headers, StorageClass::STANDARD);
|
||||
* $connector->putObject($myInput, 'bucketname', 'path/to/object.dat', Acl::PRIVATE, $headers)
|
||||
*
|
||||
* @see https://aws.amazon.com/s3/storage-classes/
|
||||
*/
|
||||
class StorageClass
|
||||
{
|
||||
/**
|
||||
* Amazon S3 Standard (S3 Standard)
|
||||
*/
|
||||
const STANDARD = 'STANDARD';
|
||||
|
||||
/**
|
||||
* Reduced redundancy storage
|
||||
*
|
||||
* Not recommended anymore. Use INTELLIGENT_TIERING instead.
|
||||
*/
|
||||
const REDUCED_REDUNDANCY = 'REDUCED_REDUNDANCY';
|
||||
|
||||
/**
|
||||
* Amazon S3 Intelligent-Tiering (S3 Intelligent-Tiering)
|
||||
*/
|
||||
const INTELLIGENT_TIERING = 'INTELLIGENT_TIERING';
|
||||
|
||||
/**
|
||||
* Amazon S3 Standard-Infrequent Access (S3 Standard-IA)
|
||||
*/
|
||||
const STANDARD_IA = 'STANDARD_IA';
|
||||
|
||||
/**
|
||||
* Amazon S3 One Zone-Infrequent Access (S3 One Zone-IA)
|
||||
*/
|
||||
const ONEZONE_IA = 'ONEZONE_IA';
|
||||
|
||||
/**
|
||||
* Amazon S3 Glacier (S3 Glacier)
|
||||
*/
|
||||
const GLACIER = 'GLACIER';
|
||||
|
||||
/**
|
||||
* Amazon S3 Glacier Deep Archive (S3 Glacier Deep Archive)
|
||||
*/
|
||||
const DEEP_ARCHIVE = 'DEEP_ARCHIVE';
|
||||
|
||||
/**
|
||||
* Manipulate the $headers array, setting the X-Amz-Storage-Class header for the requested storage class.
|
||||
*
|
||||
* This method will automatically remove any previously set X-Amz-Storage-Class header, case-insensitive. The reason
|
||||
* for that is that Amazon headers **are** case-insensitive and you could easily end up having two separate headers
|
||||
* with competing storage classes. This would mess up the signature and your request would promptly fail.
|
||||
*
|
||||
* @param array $headers
|
||||
* @param string $storageClass
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function setStorageClass(array &$headers, string $storageClass): void
|
||||
{
|
||||
// Remove all previously set X-Amz-Storage-Class headers (case-insensitive)
|
||||
$killList = [];
|
||||
|
||||
foreach ($headers as $key => $value)
|
||||
{
|
||||
if (strtolower($key) === 'x-amz-storage-class')
|
||||
{
|
||||
$killList[] = $key;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($killList as $key)
|
||||
{
|
||||
unset($headers[$key]);
|
||||
}
|
||||
|
||||
// Add the new X-Amz-Storage-Class header
|
||||
$headers['X-Amz-Storage-Class'] = $storageClass;
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue