if (!isset($aHeaders['x-amz-content-sha256'])) {
$resultHeaders['x-amz-content-sha256'] = $payloadHash;
final class UpdraftPlus_S3Request {
private $endpoint, $verb, $bucket, $uri, $resource = '', $parameters = array(),
$amzHeaders = array(), $headers = array(
'Host' => '', 'Date' => '', 'Content-MD5' => '', 'Content-Type' => ''
public $fp = false, $size = 0, $data = false, $response;
* @param string $verb Verb
* @param string $bucket Bucket name
* @param string $uri Object URI
* @param string $endpoint Endpoint of storage
* @param boolean $use_dns_bucket_name
* @param object $s3 S3 Object that calls these requests
function __construct($verb, $bucket = '', $uri = '', $endpoint = 's3.amazonaws.com', $use_dns_bucket_name = false, $s3 = null) {
$this->endpoint = $endpoint;
$this->uri = $uri !== '' ? '/'.str_replace('%2F', '/', rawurlencode($uri)) : '/';
//if ($this->bucket !== '')
// $this->resource = '/'.$this->bucket.$this->uri;
// $this->resource = $this->uri;
if ('' !== $this->bucket) {
if ($this->__dnsBucketName($this->bucket) || $use_dns_bucket_name) {
$this->headers['Host'] = $this->bucket.'.'.$this->endpoint;
$this->resource = '/'.$this->bucket.$this->uri;
$this->headers['Host'] = $this->endpoint;
if ('' !== $this->bucket) $this->uri = '/'.$this->bucket.$this->uri;
$this->resource = $this->uri;
$this->headers['Host'] = $this->endpoint;
$this->resource = $this->uri;
$this->headers['Date'] = gmdate('D, d M Y H:i:s T');
$this->response = new STDClass;
$this->response->error = false;
$this->response->body = null;
* @param string $value Value
public function setParameter($key, $value) {
$this->parameters[$key] = $value;
* @param string $value Value
public function setHeader($key, $value) {
$this->headers[$key] = $value;
* Set x-amz-meta-* header
* @param string $value Value
public function setAmzHeader($key, $value) {
$this->amzHeaders[$key] = $value;
public function getResponse() {
if (sizeof($this->parameters) > 0) {
$query = ('?' !== substr($this->uri, -1)) ? '?' : '&';
foreach ($this->parameters as $var => $value)
if (null == $value || '' == $value) $query .= $var.'&';
else $query .= $var.'='.rawurlencode($value).'&';
$query = substr($query, 0, -1);
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('partNumber', $this->parameters) ||
array_key_exists('uploads', $this->parameters) ||
array_key_exists('uploadId', $this->parameters))
$this->resource .= $query;
$url = ($this->s3->useSSL ? 'https://' : 'http://') . ('' !== $this->headers['Host'] ? $this->headers['Host'] : $this->endpoint) . $this->uri;
//var_dump('bucket: ' . $this->bucket, 'uri: ' . $this->uri, 'resource: ' . $this->resource, 'url: ' . $url);
curl_setopt($curl, CURLOPT_USERAGENT, 'S3/php');
// SSL Validation can now be optional for those with broken OpenSSL installations
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, $this->s3->useSSLValidation ? 2 : 0);
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, $this->s3->useSSLValidation ? 1 : 0);
if (null !== $this->s3->sslKey) curl_setopt($curl, CURLOPT_SSLKEY, $this->s3->sslKey);
if (null !== $this->s3->sslCert) curl_setopt($curl, CURLOPT_SSLCERT, $this->s3->sslCert);
if (null !== $this->s3->sslCACert && file_exists($this->s3->sslCACert)) curl_setopt($curl, CURLOPT_CAINFO, $this->s3->sslCACert);
curl_setopt($curl, CURLOPT_URL, $url);
$wp_proxy = new WP_HTTP_Proxy();
if (null != $this->s3->proxy && isset($this->s3->proxy['host']) && $wp_proxy->send_through_proxy($url)) {
curl_setopt($curl, CURLOPT_PROXY, $this->s3->proxy['host']);
curl_setopt($curl, CURLOPT_PROXYTYPE, $this->s3->proxy['type']);
if (!empty($this->s3->proxy['port'])) curl_setopt($curl,CURLOPT_PROXYPORT, $this->s3->proxy['port']);
if (isset($this->s3->proxy['user'], $this->s3->proxy['pass']) && null != $this->s3->proxy['user'] && null != $this->s3->proxy['pass']) {
curl_setopt($curl, CURLOPT_PROXYAUTH, CURLAUTH_ANY);
curl_setopt($curl, CURLOPT_PROXYUSERPWD, sprintf('%s:%s', $this->s3->proxy['user'], $this->s3->proxy['pass']));
$headers = array(); $amz = array();
foreach ($this->amzHeaders as $header => $value)
if (strlen($value) > 0) $headers[] = $header.': '.$value;
foreach ($this->headers as $header => $value)
if (strlen($value) > 0) $headers[] = $header.': '.$value;
// Collect AMZ headers for signature
foreach ($this->amzHeaders as $header => $value)
if (strlen($value) > 0) $amz[] = strtolower($header).':'.$value;
// AMZ headers must be sorted
usort($amz, array(&$this, '__sortMetaHeadersCmp'));
$amz = "\n".implode("\n", $amz);
if ($this->s3->hasAuth()) {
// Authorization string (CloudFront stringToSign should only contain a date)
if ('cloudfront.amazonaws.com' == $this->headers['Host']) {
$headers[] = 'Authorization: ' . $this->s3->__getSignature($this->headers['Date']);
if ('v2' === $this->s3->signVer) {
$headers[] = 'Authorization: ' . $this->s3->__getSignature(
$this->headers['Content-MD5']."\n".
$this->headers['Content-Type']."\n".
$this->headers['Date'].$amz."\n".
$amzHeaders = $this->s3->__getSignatureV4(
foreach ($amzHeaders as $k => $v) {
$headers[] = $k . ': ' . $v;
if (false !== $this->s3->port) curl_setopt($curl, CURLOPT_PORT, $this->s3->port);
curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
curl_setopt($curl, CURLOPT_HEADER, false);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, false);
curl_setopt($curl, CURLOPT_WRITEFUNCTION, array(&$this, '__responseWriteCallback'));
curl_setopt($curl, CURLOPT_HEADERFUNCTION, array(&$this, '__responseHeaderCallback'));
@curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
if (false !== $this->fp) {
curl_setopt($curl, CURLOPT_PUT, true);
curl_setopt($curl, CURLOPT_INFILE, $this->fp);
curl_setopt($curl, CURLOPT_INFILESIZE, $this->size);
} elseif (false !== $this->data) {
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $this->verb);
curl_setopt($curl, CURLOPT_POSTFIELDS, $this->data);
curl_setopt($curl, CURLOPT_INFILESIZE, strlen($this->data));
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $this->verb);
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'HEAD');
curl_setopt($curl, CURLOPT_NOBODY, true);
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'DELETE');
$this->response->code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
$this->response->error = array(
'code' => curl_errno($curl),
'message' => curl_error($curl),
'resource' => $this->resource
// The case in which there is not application/xml content-type header is to support a DreamObjects case seen, April 2018
if (false === $this->response->error && isset($this->response->body) && ((isset($this->response->headers['type']) && false !== strpos($this->response->headers['type'], 'application/xml')) || (!isset($this->response->headers['type']) && 0 === strpos($this->response->body, '<?xml')))) {
$this->response->body = simplexml_load_string($this->response->body);
if (!in_array($this->response->code, array(200, 204, 206)) &&
isset($this->response->body->Code)) {
$this->response->error = array(
'code' => (string)$this->response->body->Code,
$this->response->error['message'] = isset($this->response->body->Message) ? $this->response->body->Message : '';
if (isset($this->response->body->Resource))
$this->response->error['resource'] = (string)$this->response->body->Resource;
unset($this->response->body);
// Clean up file resources
if (false !== $this->fp && is_resource($this->fp)) fclose($this->fp);
* Sort compare for meta headers
* @internal Used to sort x-amz meta headers
* @param string $a String A
* @param string $b String B
private function __sortMetaHeadersCmp($a, $b) {// phpcs:ignore PHPCompatibility.FunctionNameRestrictions.ReservedFunctionNames.MethodDoubleUnderscore -- Method name "UpdraftPlus_S3Request::__responseHeaderCallback" is discouraged; PHP has reserved all method names with a double underscore prefix for future use.
$minLen = min($lenA, $lenB);
$ncmp = strncmp($a, $b, $minLen);
if ($lenA == $lenB) return $ncmp;
if (0 == $ncmp) return $lenA < $lenB ? -1 : 1;
* @param resource $curl CURL resource
* @param string $data Data
private function __responseWriteCallback($curl, $data) {// phpcs:ignore PHPCompatibility.FunctionNameRestrictions.ReservedFunctionNames.MethodDoubleUnderscore -- Method name "UpdraftPlus_S3Request::__responseHeaderCallback" is discouraged; PHP has reserved all method names with a double underscore prefix for future use.
if (in_array($this->response->code, array(200, 206)) && false !== $this->fp)
return fwrite($this->fp, $data);
$this->response->body = (empty($this->response->body)) ? $data : $this->response->body.$data;
* @param string $bucket Bucket name
private function __dnsBucketName($bucket) {// phpcs:ignore PHPCompatibility.FunctionNameRestrictions.ReservedFunctionNames.MethodDoubleUnderscore -- Method name "UpdraftPlus_S3Request::__responseHeaderCallback" is discouraged; PHP has reserved all method names with a double underscore prefix for future use.
# A DNS bucket name cannot have len>63
# A DNS bucket name must have a character in other than a-z, 0-9, . -
# The purpose of this second check is not clear - is it that there's some limitation somewhere on bucket names that match that pattern that means that the bucket must be accessed by hostname?
if (strlen($bucket) > 63 || !preg_match("/[^a-z0-9\.-]/", $bucket)) return false;
# A DNS bucket name cannot contain -.
if (false !== strstr($bucket, '-.')) return false;
# A DNS bucket name cannot contain ..
if (false !== strstr($bucket, '..')) return false;
# A DNS bucket name must begin with 0-9a-z
if (!preg_match("/^[0-9a-z]/", $bucket)) return false;
# A DNS bucket name must end with 0-9 a-z
if (!preg_match("/[0-9a-z]$/", $bucket)) return false;
* @param resource $curl CURL resource
* @param string $data Data
private function __responseHeaderCallback($curl, $data) {// phpcs:ignore PHPCompatibility.FunctionNameRestrictions.ReservedFunctionNames.MethodDoubleUnderscore -- Method name "UpdraftPlus_S3Request::__responseHeaderCallback" is discouraged; PHP has reserved all method names with a double underscore prefix for future use.
if (($strlen = strlen($data)) <= 2) return $strlen;
if ('HTTP' == substr($data, 0, 4)) {
$this->response->code = (int)substr($data, 9, 3);
if (false === strpos($data, ': ')) return $strlen;
list($header, $value) = explode(': ', $data, 2);
if ('last-modified' == strtolower($header))
$this->response->headers['time'] = strtotime($value);
elseif ('content-length' == strtolower($header))
$this->response->headers['size'] = (int)$value;
elseif ('content-type' == strtolower($header))
$this->response->headers['type'] = $value;
elseif ('etag' == strtolower($header))
$this->response->headers['hash'] = '"' == $value[0] ? substr($value, 1, -1) : $value;
elseif (preg_match('/^x-amz-meta-.*$/i', $header))
$this->response->headers[strtolower($header)] = $value;