Dropbox
10 years ago
Google
10 years ago
cloudfiles
12 years ago
images
10 years ago
labelauty
10 years ago
phpseclib
10 years ago
selectric
10 years ago
S3.php
10 years ago
S3compat.php
10 years ago
cacert.pem
10 years ago
class-partialfileservlet.php
10 years ago
class-semaphore.php
12 years ago
ftp.class.php
10 years ago
get-cpanel-quota-usage.pl
12 years ago
google-extensions.php
10 years ago
jquery-ui.custom.css
10 years ago
jquery.blockUI.js
10 years ago
updraft-admin-ui.js
10 years ago
S3.php
2251 lines
| 1 | <?php |
| 2 | /** |
| 3 | * $Id$ |
| 4 | * |
| 5 | * Copyright (c) 2011, Donovan Schönknecht. All rights reserved. |
| 6 | * Portions copyright (c) 2012-3, David Anderson (http://www.simbahosting.co.uk). All rights reserved. |
| 7 | * |
| 8 | * Redistribution and use in source and binary forms, with or without |
| 9 | * modification, are permitted provided that the following conditions are met: |
| 10 | * |
| 11 | * - Redistributions of source code must retain the above copyright notice, |
| 12 | * this list of conditions and the following disclaimer. |
| 13 | * - Redistributions in binary form must reproduce the above copyright |
| 14 | * notice, this list of conditions and the following disclaimer in the |
| 15 | * documentation and/or other materials provided with the distribution. |
| 16 | * |
| 17 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" |
| 18 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
| 19 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE |
| 20 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE |
| 21 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
| 22 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
| 23 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
| 24 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
| 25 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| 26 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE |
| 27 | * POSSIBILITY OF SUCH DAMAGE. |
| 28 | * |
| 29 | * Amazon S3 is a trademark of Amazon.com, Inc. or its affiliates. |
| 30 | */ |
| 31 | |
| 32 | /** |
| 33 | * Amazon S3 PHP class |
| 34 | * |
| 35 | * @link http://undesigned.org.za/2007/10/22/amazon-s3-php-class |
| 36 | * @version 0.5.0-dev |
| 37 | */ |
| 38 | class UpdraftPlus_S3 |
| 39 | { |
| 40 | // ACL flags |
| 41 | const ACL_PRIVATE = 'private'; |
| 42 | const ACL_PUBLIC_READ = 'public-read'; |
| 43 | const ACL_PUBLIC_READ_WRITE = 'public-read-write'; |
| 44 | const ACL_AUTHENTICATED_READ = 'authenticated-read'; |
| 45 | |
| 46 | const STORAGE_CLASS_STANDARD = 'STANDARD'; |
| 47 | const STORAGE_CLASS_RRS = 'REDUCED_REDUNDANCY'; |
| 48 | |
| 49 | private static $__accessKey = null; // AWS Access key |
| 50 | private static $__secretKey = null; // AWS Secret key |
| 51 | private static $__sslKey = null; |
| 52 | |
| 53 | public static $endpoint = 's3.amazonaws.com'; |
| 54 | public static $proxy = null; |
| 55 | |
| 56 | // Added to cope with a particular situation where the user had no pernmission to check the bucket location, which necessitated using DNS-based endpoints. |
| 57 | public static $use_dns_bucket_name = false; |
| 58 | |
| 59 | public static $useSSL = false; |
| 60 | public static $useSSLValidation = true; |
| 61 | public static $useExceptions = false; |
| 62 | |
| 63 | // SSL CURL SSL options - only needed if you are experiencing problems with your OpenSSL configuration |
| 64 | public static $sslKey = null; |
| 65 | public static $sslCert = null; |
| 66 | public static $sslCACert = null; |
| 67 | |
| 68 | private static $__signingKeyPairId = null; // AWS Key Pair ID |
| 69 | private static $__signingKeyResource = false; // Key resource, freeSigningKey() must be called to clear it from memory |
| 70 | |
| 71 | |
| 72 | /** |
| 73 | * Constructor - if you're not using the class statically |
| 74 | * |
| 75 | * @param string $accessKey Access key |
| 76 | * @param string $secretKey Secret key |
| 77 | * @param boolean $useSSL Enable SSL |
| 78 | * @return void |
| 79 | */ |
| 80 | public function __construct($accessKey = null, $secretKey = null, $useSSL = true, $sslCACert = true, $endpoint = null) |
| 81 | { |
| 82 | if ($accessKey !== null && $secretKey !== null) |
| 83 | self::setAuth($accessKey, $secretKey); |
| 84 | self::$useSSL = $useSSL; |
| 85 | self::$sslCACert = $sslCACert; |
| 86 | if (!empty($endpoint)) self::$endpoint = $endpoint; |
| 87 | |
| 88 | if (!function_exists('curl_init')) { |
| 89 | global $updraftplus; |
| 90 | $updraftplus->log('The PHP cURL extension must be installed and enabled to use this remote storage method'); |
| 91 | throw new Exception('The PHP cURL extension must be installed and enabled to use this remote storage method'); |
| 92 | } |
| 93 | |
| 94 | } |
| 95 | |
| 96 | |
| 97 | /** |
| 98 | * Set the service endpoint |
| 99 | * |
| 100 | * @param string $host Hostname |
| 101 | * @return void |
| 102 | */ |
| 103 | public function setEndpoint($host) |
| 104 | { |
| 105 | self::$endpoint = $host; |
| 106 | } |
| 107 | |
| 108 | /** |
| 109 | * Set AWS access key and secret key |
| 110 | * |
| 111 | * @param string $accessKey Access key |
| 112 | * @param string $secretKey Secret key |
| 113 | * @return void |
| 114 | */ |
| 115 | public static function setAuth($accessKey, $secretKey) |
| 116 | { |
| 117 | self::$__accessKey = $accessKey; |
| 118 | self::$__secretKey = $secretKey; |
| 119 | } |
| 120 | |
| 121 | /** |
| 122 | * Check if AWS keys have been set |
| 123 | * |
| 124 | * @return boolean |
| 125 | */ |
| 126 | public static function hasAuth() { |
| 127 | return (self::$__accessKey !== null && self::$__secretKey !== null); |
| 128 | } |
| 129 | |
| 130 | |
| 131 | /** |
| 132 | * Set SSL on or off |
| 133 | * |
| 134 | * @param boolean $enabled SSL enabled |
| 135 | * @param boolean $validate SSL certificate validation |
| 136 | * @return void |
| 137 | */ |
| 138 | public static function setSSL($enabled, $validate = true) |
| 139 | { |
| 140 | self::$useSSL = $enabled; |
| 141 | self::$useSSLValidation = $validate; |
| 142 | } |
| 143 | |
| 144 | public static function getuseSSL() { |
| 145 | return self::$useSSL; |
| 146 | } |
| 147 | |
| 148 | /** |
| 149 | * Set SSL client certificates (experimental) |
| 150 | * |
| 151 | * @param string $sslCert SSL client certificate |
| 152 | * @param string $sslKey SSL client key |
| 153 | * @param string $sslCACert SSL CA cert (only required if you are having problems with your system CA cert) |
| 154 | * @return void |
| 155 | */ |
| 156 | public static function setSSLAuth($sslCert = null, $sslKey = null, $sslCACert = null) |
| 157 | { |
| 158 | self::$sslCert = $sslCert; |
| 159 | self::$sslKey = $sslKey; |
| 160 | self::$sslCACert = $sslCACert; |
| 161 | } |
| 162 | |
| 163 | |
| 164 | /** |
| 165 | * Set proxy information |
| 166 | * |
| 167 | * @param string $host Proxy hostname and port (localhost:1234) |
| 168 | * @param string $user Proxy username |
| 169 | * @param string $pass Proxy password |
| 170 | * @param constant $type CURL proxy type |
| 171 | * @return void |
| 172 | */ |
| 173 | public static function setProxy($host, $user = null, $pass = null, $type = CURLPROXY_SOCKS5, $port = null) |
| 174 | { |
| 175 | self::$proxy = array('host' => $host, 'type' => $type, 'user' => $user, 'pass' => $pass, 'port' => $port); |
| 176 | } |
| 177 | |
| 178 | |
| 179 | /** |
| 180 | * Set the error mode to exceptions |
| 181 | * |
| 182 | * @param boolean $enabled Enable exceptions |
| 183 | * @return void |
| 184 | */ |
| 185 | public static function setExceptions($enabled = true) |
| 186 | { |
| 187 | self::$useExceptions = $enabled; |
| 188 | } |
| 189 | |
| 190 | |
| 191 | /** |
| 192 | * Set signing key |
| 193 | * |
| 194 | * @param string $keyPairId AWS Key Pair ID |
| 195 | * @param string $signingKey Private Key |
| 196 | * @param boolean $isFile Load private key from file, set to false to load string |
| 197 | * @return boolean |
| 198 | */ |
| 199 | public static function setSigningKey($keyPairId, $signingKey, $isFile = true) |
| 200 | { |
| 201 | self::$__signingKeyPairId = $keyPairId; |
| 202 | if ((self::$__signingKeyResource = openssl_pkey_get_private($isFile ? |
| 203 | file_get_contents($signingKey) : $signingKey)) !== false) return true; |
| 204 | self::__triggerError('UpdraftPlus_S3::setSigningKey(): Unable to open load private key: '.$signingKey, __FILE__, __LINE__); |
| 205 | return false; |
| 206 | } |
| 207 | |
| 208 | |
| 209 | /** |
| 210 | * Free signing key from memory, MUST be called if you are using setSigningKey() |
| 211 | * |
| 212 | * @return void |
| 213 | */ |
| 214 | public static function freeSigningKey() |
| 215 | { |
| 216 | if (self::$__signingKeyResource !== false) |
| 217 | openssl_free_key(self::$__signingKeyResource); |
| 218 | } |
| 219 | |
| 220 | |
| 221 | /** |
| 222 | * Internal error handler |
| 223 | * |
| 224 | * @internal Internal error handler |
| 225 | * @param string $message Error message |
| 226 | * @param string $file Filename |
| 227 | * @param integer $line Line number |
| 228 | * @param integer $code Error code |
| 229 | * @return void |
| 230 | */ |
| 231 | private static function __triggerError($message, $file, $line, $code = 0) |
| 232 | { |
| 233 | if (self::$useExceptions) |
| 234 | throw new UpdraftPlus_S3Exception($message, $file, $line, $code); |
| 235 | else { |
| 236 | trigger_error($message, E_USER_WARNING); |
| 237 | } |
| 238 | } |
| 239 | |
| 240 | |
| 241 | /** |
| 242 | * Get a list of buckets |
| 243 | * |
| 244 | * @param boolean $detailed Returns detailed bucket list when true |
| 245 | * @return array | false |
| 246 | */ |
| 247 | public static function listBuckets($detailed = false) |
| 248 | { |
| 249 | $rest = new UpdraftPlus_S3Request('GET', '', '', self::$endpoint, self::$use_dns_bucket_name); |
| 250 | $rest = $rest->getResponse(); |
| 251 | if ($rest->error === false && $rest->code !== 200) |
| 252 | $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); |
| 253 | if ($rest->error !== false) |
| 254 | { |
| 255 | self::__triggerError(sprintf("UpdraftPlus_S3::listBuckets(): [%s] %s", $rest->error['code'], |
| 256 | $rest->error['message']), __FILE__, __LINE__); |
| 257 | return false; |
| 258 | } |
| 259 | $results = array(); |
| 260 | if (!isset($rest->body->Buckets)) return $results; |
| 261 | |
| 262 | if ($detailed) |
| 263 | { |
| 264 | if (isset($rest->body->Owner, $rest->body->Owner->ID, $rest->body->Owner->DisplayName)) |
| 265 | $results['owner'] = array( |
| 266 | 'id' => (string)$rest->body->Owner->ID, 'name' => (string)$rest->body->Owner->ID |
| 267 | ); |
| 268 | $results['buckets'] = array(); |
| 269 | foreach ($rest->body->Buckets->Bucket as $b) |
| 270 | $results['buckets'][] = array( |
| 271 | 'name' => (string)$b->Name, 'time' => strtotime((string)$b->CreationDate) |
| 272 | ); |
| 273 | } else |
| 274 | foreach ($rest->body->Buckets->Bucket as $b) $results[] = (string)$b->Name; |
| 275 | |
| 276 | return $results; |
| 277 | } |
| 278 | |
| 279 | public static function useDNSBucketName($use = true, $bucket = '') { |
| 280 | self::$use_dns_bucket_name = $use; |
| 281 | return true; |
| 282 | } |
| 283 | |
| 284 | /* |
| 285 | * Get contents for a bucket |
| 286 | * |
| 287 | * If maxKeys is null this method will loop through truncated result sets |
| 288 | * |
| 289 | * @param string $bucket Bucket name |
| 290 | * @param string $prefix Prefix |
| 291 | * @param string $marker Marker (last file listed) |
| 292 | * @param string $maxKeys Max keys (maximum number of keys to return) |
| 293 | * @param string $delimiter Delimiter |
| 294 | * @param boolean $returnCommonPrefixes Set to true to return CommonPrefixes |
| 295 | * @return array | false |
| 296 | */ |
| 297 | public static function getBucket($bucket, $prefix = null, $marker = null, $maxKeys = null, $delimiter = null, $returnCommonPrefixes = false) |
| 298 | { |
| 299 | $rest = new UpdraftPlus_S3Request('GET', $bucket, '', self::$endpoint, self::$use_dns_bucket_name); |
| 300 | if ($maxKeys == 0) $maxKeys = null; |
| 301 | if ($prefix !== null && $prefix !== '') $rest->setParameter('prefix', $prefix); |
| 302 | if ($marker !== null && $marker !== '') $rest->setParameter('marker', $marker); |
| 303 | if ($maxKeys !== null && $maxKeys !== '') $rest->setParameter('max-keys', $maxKeys); |
| 304 | if ($delimiter !== null && $delimiter !== '') $rest->setParameter('delimiter', $delimiter); |
| 305 | $response = $rest->getResponse(); |
| 306 | if ($response->error === false && $response->code !== 200) |
| 307 | $response->error = array('code' => $response->code, 'message' => 'Unexpected HTTP status'); |
| 308 | if ($response->error !== false) |
| 309 | { |
| 310 | self::__triggerError(sprintf("UpdraftPlus_S3::getBucket(): [%s] %s", |
| 311 | $response->error['code'], $response->error['message']), __FILE__, __LINE__); |
| 312 | return false; |
| 313 | } |
| 314 | |
| 315 | $results = array(); |
| 316 | |
| 317 | $nextMarker = null; |
| 318 | if (isset($response->body, $response->body->Contents)) |
| 319 | foreach ($response->body->Contents as $c) |
| 320 | { |
| 321 | $results[(string)$c->Key] = array( |
| 322 | 'name' => (string)$c->Key, |
| 323 | 'time' => strtotime((string)$c->LastModified), |
| 324 | 'size' => (int)$c->Size, |
| 325 | 'hash' => substr((string)$c->ETag, 1, -1) |
| 326 | ); |
| 327 | $nextMarker = (string)$c->Key; |
| 328 | } |
| 329 | |
| 330 | if ($returnCommonPrefixes && isset($response->body, $response->body->CommonPrefixes)) |
| 331 | foreach ($response->body->CommonPrefixes as $c) |
| 332 | $results[(string)$c->Prefix] = array('prefix' => (string)$c->Prefix); |
| 333 | |
| 334 | if (isset($response->body, $response->body->IsTruncated) && |
| 335 | (string)$response->body->IsTruncated == 'false') return $results; |
| 336 | |
| 337 | if (isset($response->body, $response->body->NextMarker)) |
| 338 | $nextMarker = (string)$response->body->NextMarker; |
| 339 | |
| 340 | // Loop through truncated results if maxKeys isn't specified |
| 341 | if ($maxKeys == null && $nextMarker !== null && (string)$response->body->IsTruncated == 'true') |
| 342 | do |
| 343 | { |
| 344 | $rest = new UpdraftPlus_S3Request('GET', $bucket, '', self::$endpoint, self::$use_dns_bucket_name); |
| 345 | if ($prefix !== null && $prefix !== '') $rest->setParameter('prefix', $prefix); |
| 346 | $rest->setParameter('marker', $nextMarker); |
| 347 | if ($delimiter !== null && $delimiter !== '') $rest->setParameter('delimiter', $delimiter); |
| 348 | |
| 349 | if (($response = $rest->getResponse()) == false || $response->code !== 200) break; |
| 350 | |
| 351 | if (isset($response->body, $response->body->Contents)) |
| 352 | foreach ($response->body->Contents as $c) |
| 353 | { |
| 354 | $results[(string)$c->Key] = array( |
| 355 | 'name' => (string)$c->Key, |
| 356 | 'time' => strtotime((string)$c->LastModified), |
| 357 | 'size' => (int)$c->Size, |
| 358 | 'hash' => substr((string)$c->ETag, 1, -1) |
| 359 | ); |
| 360 | $nextMarker = (string)$c->Key; |
| 361 | } |
| 362 | |
| 363 | if ($returnCommonPrefixes && isset($response->body, $response->body->CommonPrefixes)) |
| 364 | foreach ($response->body->CommonPrefixes as $c) |
| 365 | $results[(string)$c->Prefix] = array('prefix' => (string)$c->Prefix); |
| 366 | |
| 367 | if (isset($response->body, $response->body->NextMarker)) |
| 368 | $nextMarker = (string)$response->body->NextMarker; |
| 369 | |
| 370 | } while ($response !== false && (string)$response->body->IsTruncated == 'true'); |
| 371 | |
| 372 | return $results; |
| 373 | } |
| 374 | |
| 375 | |
| 376 | /** |
| 377 | * Put a bucket |
| 378 | * |
| 379 | * @param string $bucket Bucket name |
| 380 | * @param constant $acl ACL flag |
| 381 | * @param string $location Set as "EU" to create buckets hosted in Europe |
| 382 | * @return boolean |
| 383 | */ |
| 384 | public static function putBucket($bucket, $acl = self::ACL_PRIVATE, $location = false) |
| 385 | { |
| 386 | $rest = new UpdraftPlus_S3Request('PUT', $bucket, '', self::$endpoint, self::$use_dns_bucket_name); |
| 387 | $rest->setAmzHeader('x-amz-acl', $acl); |
| 388 | |
| 389 | if ($location !== false) |
| 390 | { |
| 391 | $dom = new DOMDocument; |
| 392 | $createBucketConfiguration = $dom->createElement('CreateBucketConfiguration'); |
| 393 | $locationConstraint = $dom->createElement('LocationConstraint', $location); |
| 394 | $createBucketConfiguration->appendChild($locationConstraint); |
| 395 | $dom->appendChild($createBucketConfiguration); |
| 396 | $rest->data = $dom->saveXML(); |
| 397 | $rest->size = strlen($rest->data); |
| 398 | $rest->setHeader('Content-Type', 'application/xml'); |
| 399 | } |
| 400 | $rest = $rest->getResponse(); |
| 401 | |
| 402 | if ($rest->error === false && $rest->code !== 200) |
| 403 | $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); |
| 404 | if ($rest->error !== false) |
| 405 | { |
| 406 | self::__triggerError(sprintf("UpdraftPlus_S3::putBucket({$bucket}, {$acl}, {$location}): [%s] %s", |
| 407 | $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); |
| 408 | return false; |
| 409 | } |
| 410 | return true; |
| 411 | } |
| 412 | |
| 413 | |
| 414 | /** |
| 415 | * Delete an empty bucket |
| 416 | * |
| 417 | * @param string $bucket Bucket name |
| 418 | * @return boolean |
| 419 | */ |
| 420 | public static function deleteBucket($bucket) |
| 421 | { |
| 422 | $rest = new UpdraftPlus_S3Request('DELETE', $bucket, '', self::$endpoint, self::$use_dns_bucket_name); |
| 423 | $rest = $rest->getResponse(); |
| 424 | if ($rest->error === false && $rest->code !== 204) |
| 425 | $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); |
| 426 | if ($rest->error !== false) |
| 427 | { |
| 428 | self::__triggerError(sprintf("UpdraftPlus_S3::deleteBucket({$bucket}): [%s] %s", |
| 429 | $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); |
| 430 | return false; |
| 431 | } |
| 432 | return true; |
| 433 | } |
| 434 | |
| 435 | |
| 436 | /** |
| 437 | * Create input info array for putObject() |
| 438 | * |
| 439 | * @param string $file Input file |
| 440 | * @param mixed $md5sum Use MD5 hash (supply a string if you want to use your own) |
| 441 | * @return array | false |
| 442 | */ |
| 443 | public static function inputFile($file, $md5sum = true) |
| 444 | { |
| 445 | if (!file_exists($file) || !is_file($file) || !is_readable($file)) |
| 446 | { |
| 447 | self::__triggerError('UpdraftPlus_S3::inputFile(): Unable to open input file: '.$file, __FILE__, __LINE__); |
| 448 | return false; |
| 449 | } |
| 450 | return array('file' => $file, 'size' => filesize($file), 'md5sum' => $md5sum !== false ? |
| 451 | (is_string($md5sum) ? $md5sum : base64_encode(md5_file($file, true))) : ''); |
| 452 | } |
| 453 | |
| 454 | |
| 455 | /** |
| 456 | * Create input array info for putObject() with a resource |
| 457 | * |
| 458 | * @param string $resource Input resource to read from |
| 459 | * @param integer $bufferSize Input byte size |
| 460 | * @param string $md5sum MD5 hash to send (optional) |
| 461 | * @return array | false |
| 462 | */ |
| 463 | public static function inputResource(&$resource, $bufferSize, $md5sum = '') |
| 464 | { |
| 465 | if (!is_resource($resource) || $bufferSize < 0) |
| 466 | { |
| 467 | self::__triggerError('UpdraftPlus_S3::inputResource(): Invalid resource or buffer size', __FILE__, __LINE__); |
| 468 | return false; |
| 469 | } |
| 470 | $input = array('size' => $bufferSize, 'md5sum' => $md5sum); |
| 471 | $input['fp'] =& $resource; |
| 472 | return $input; |
| 473 | } |
| 474 | |
| 475 | /** |
| 476 | * Initiate a multi-part upload (http://docs.amazonwebservices.com/AmazonS3/latest/API/mpUploadInitiate.html) |
| 477 | * |
| 478 | * @param string $bucket Bucket name |
| 479 | * @param string $uri Object URI |
| 480 | * @param constant $acl ACL constant |
| 481 | * @param array $metaHeaders Array of x-amz-meta-* headers |
| 482 | * @param array $requestHeaders Array of request headers or content type as a string |
| 483 | * @param constant $storageClass Storage class constant |
| 484 | * @return string | false |
| 485 | */ |
| 486 | |
| 487 | public static function initiateMultipartUpload ($bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $requestHeaders = array(), $storageClass = self::STORAGE_CLASS_STANDARD) |
| 488 | { |
| 489 | |
| 490 | $rest = new UpdraftPlus_S3Request('POST', $bucket, $uri, self::$endpoint, self::$use_dns_bucket_name); |
| 491 | $rest->setParameter('uploads',''); |
| 492 | |
| 493 | // Custom request headers (Content-Type, Content-Disposition, Content-Encoding) |
| 494 | if (is_array($requestHeaders)) |
| 495 | foreach ($requestHeaders as $h => $v) $rest->setHeader($h, $v); |
| 496 | |
| 497 | // Set storage class |
| 498 | if ($storageClass !== self::STORAGE_CLASS_STANDARD) // Storage class |
| 499 | $rest->setAmzHeader('x-amz-storage-class', $storageClass); |
| 500 | |
| 501 | // Set ACL headers |
| 502 | $rest->setAmzHeader('x-amz-acl', $acl); |
| 503 | foreach ($metaHeaders as $h => $v) $rest->setAmzHeader('x-amz-meta-'.$h, $v); |
| 504 | |
| 505 | // Carry out the HTTP operation |
| 506 | $rest->getResponse(); |
| 507 | |
| 508 | if ($rest->response->error === false && $rest->response->code !== 200) |
| 509 | $rest->response->error = array('code' => $rest->response->code, 'message' => 'Unexpected HTTP status'); |
| 510 | if ($rest->response->error !== false) |
| 511 | { |
| 512 | self::__triggerError(sprintf("UpdraftPlus_S3::initiateMultipartUpload(): [%s] %s", |
| 513 | $rest->response->error['code'], $rest->response->error['message']), __FILE__, __LINE__); |
| 514 | return false; |
| 515 | } elseif (isset($rest->response->body)) |
| 516 | { |
| 517 | // DreamObjects already returns a SimpleXMLElement here. Not sure how that works. |
| 518 | if (is_a($rest->response->body, 'SimpleXMLElement')) { |
| 519 | $body = $rest->response->body; |
| 520 | } else { |
| 521 | $body = new SimpleXMLElement($rest->response->body); |
| 522 | } |
| 523 | return (string) $body->UploadId; |
| 524 | } |
| 525 | |
| 526 | // It is a programming error if we reach this line |
| 527 | return false; |
| 528 | |
| 529 | } |
| 530 | |
| 531 | /** |
| 532 | /* Upload a part of a multi-part set (http://docs.amazonwebservices.com/AmazonS3/latest/API/mpUploadUploadPart.html) |
| 533 | * The chunk is read into memory, so make sure that you have enough (or patch this function to work another way!) |
| 534 | * |
| 535 | * @param string $bucket Bucket name |
| 536 | * @param string $uri Object URI |
| 537 | * @param string $uploadId uploadId returned previously from initiateMultipartUpload |
| 538 | * @param integer $partNumber sequential part number to upload |
| 539 | * @param string $filePath file to upload content from |
| 540 | * @param integer $partSize number of bytes in each part (though final part may have fewer) - pass the same value each time (for this particular upload) - default 5Mb (which is Amazon's minimum) |
| 541 | * @return string (ETag) | false |
| 542 | */ |
| 543 | |
| 544 | public static function uploadPart ($bucket, $uri, $uploadId, $filePath, $partNumber, $partSize = 5242880) |
| 545 | { |
| 546 | |
| 547 | $rest = new UpdraftPlus_S3Request('PUT', $bucket, $uri, self::$endpoint, self::$use_dns_bucket_name); |
| 548 | $rest->setParameter('partNumber', $partNumber); |
| 549 | $rest->setParameter('uploadId', $uploadId); |
| 550 | |
| 551 | // Where to begin |
| 552 | $fileOffset = ($partNumber - 1 ) * $partSize; |
| 553 | |
| 554 | // Download the smallest of the remaining bytes and the part size |
| 555 | $fileBytes = min(filesize($filePath) - $fileOffset, $partSize); |
| 556 | if ($fileBytes < 0) $fileBytes = 0; |
| 557 | |
| 558 | $rest->setHeader('Content-Type', 'application/octet-stream'); |
| 559 | $rest->data = ""; |
| 560 | |
| 561 | if ($handle = fopen($filePath, "rb")) { |
| 562 | if ($fileOffset >0) fseek($handle, $fileOffset); |
| 563 | $bytes_read = 0; |
| 564 | while ($fileBytes>0 && $read = fread($handle, max($fileBytes, 131072))) { |
| 565 | $fileBytes = $fileBytes - strlen($read); |
| 566 | $bytes_read += strlen($read); |
| 567 | $rest->data = $rest->data . $read; |
| 568 | } |
| 569 | fclose($handle); |
| 570 | } else { |
| 571 | return false; |
| 572 | } |
| 573 | |
| 574 | $rest->setHeader('Content-MD5', base64_encode(md5($rest->data, true))); |
| 575 | $rest->size = $bytes_read; |
| 576 | |
| 577 | $rest = $rest->getResponse(); |
| 578 | if ($rest->error === false && $rest->code !== 200) |
| 579 | $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); |
| 580 | if ($rest->error !== false) |
| 581 | { |
| 582 | self::__triggerError(sprintf("UpdraftPlus_S3::uploadPart(): [%s] %s", |
| 583 | $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); |
| 584 | return false; |
| 585 | } |
| 586 | return $rest->headers['hash']; |
| 587 | |
| 588 | } |
| 589 | |
| 590 | /** |
| 591 | * Complete a multi-part upload (http://docs.amazonwebservices.com/AmazonS3/latest/API/mpUploadComplete.html) |
| 592 | * |
| 593 | * @param string $bucket Bucket name |
| 594 | * @param string $uri Object URI |
| 595 | * @param string $uploadId uploadId returned previously from initiateMultipartUpload |
| 596 | * @param array $parts an ordered list of eTags of previously uploaded parts from uploadPart |
| 597 | * @return boolean |
| 598 | */ |
| 599 | |
| 600 | public static function completeMultipartUpload ($bucket, $uri, $uploadId, $parts) |
| 601 | { |
| 602 | $rest = new UpdraftPlus_S3Request('POST', $bucket, $uri, self::$endpoint, self::$use_dns_bucket_name); |
| 603 | $rest->setParameter('uploadId', $uploadId); |
| 604 | |
| 605 | $xml = "<CompleteMultipartUpload>\n"; |
| 606 | $partno = 1; |
| 607 | foreach ($parts as $etag) { |
| 608 | $xml .= "<Part><PartNumber>$partno</PartNumber><ETag>$etag</ETag></Part>\n"; |
| 609 | $partno++; |
| 610 | } |
| 611 | $xml .= "</CompleteMultipartUpload>"; |
| 612 | |
| 613 | $rest->data = $xml; |
| 614 | $rest->size = strlen($rest->data); |
| 615 | $rest->setHeader('Content-Type', 'application/xml'); |
| 616 | |
| 617 | $rest = $rest->getResponse(); |
| 618 | if ($rest->error === false && $rest->code !== 200) |
| 619 | $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); |
| 620 | if ($rest->error !== false) |
| 621 | { |
| 622 | self::__triggerError(sprintf("UpdraftPlus_S3::completeMultipartUpload(): [%s] %s", |
| 623 | $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); |
| 624 | return false; |
| 625 | } |
| 626 | return true; |
| 627 | |
| 628 | } |
| 629 | |
| 630 | /** |
| 631 | * Abort a multi-part upload (http://docs.amazonwebservices.com/AmazonS3/latest/API/mpUploadAbort.html) |
| 632 | * |
| 633 | * @param string $bucket Bucket name |
| 634 | * @param string $uri Object URI |
| 635 | * @param string $uploadId uploadId returned previously from initiateMultipartUpload |
| 636 | * @return boolean |
| 637 | */ |
| 638 | |
| 639 | public static function abortMultipartUpload ($bucket, $uri, $uploadId) |
| 640 | { |
| 641 | $rest = new UpdraftPlus_S3Request('DELETE', $bucket, $uri, self::$endpoint, self::$use_dns_bucket_name); |
| 642 | $rest->setParameter('uploadId', $uploadId); |
| 643 | $rest = $rest->getResponse(); |
| 644 | if ($rest->error === false && $rest->code !== 204) |
| 645 | $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); |
| 646 | if ($rest->error !== false) |
| 647 | { |
| 648 | self::__triggerError(sprintf("UpdraftPlus_S3::abortMultipartUpload(): [%s] %s", |
| 649 | $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); |
| 650 | return false; |
| 651 | } |
| 652 | return true; |
| 653 | } |
| 654 | |
| 655 | /** |
| 656 | * Put an object |
| 657 | * |
| 658 | * @param mixed $input Input data |
| 659 | * @param string $bucket Bucket name |
| 660 | * @param string $uri Object URI |
| 661 | * @param constant $acl ACL constant |
| 662 | * @param array $metaHeaders Array of x-amz-meta-* headers |
| 663 | * @param array $requestHeaders Array of request headers or content type as a string |
| 664 | * @param constant $storageClass Storage class constant |
| 665 | * @return boolean |
| 666 | */ |
| 667 | public static function putObject($input, $bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $requestHeaders = array(), $storageClass = self::STORAGE_CLASS_STANDARD) |
| 668 | { |
| 669 | if ($input === false) return false; |
| 670 | $rest = new UpdraftPlus_S3Request('PUT', $bucket, $uri, self::$endpoint, self::$use_dns_bucket_name); |
| 671 | |
| 672 | if (!is_array($input)) $input = array( |
| 673 | 'data' => $input, 'size' => strlen($input), |
| 674 | 'md5sum' => base64_encode(md5($input, true)) |
| 675 | ); |
| 676 | |
| 677 | // Data |
| 678 | if (isset($input['fp'])) |
| 679 | $rest->fp =& $input['fp']; |
| 680 | elseif (isset($input['file']) && is_file($input['file'])) |
| 681 | $rest->fp = @fopen($input['file'], 'rb'); |
| 682 | elseif (isset($input['data'])) |
| 683 | $rest->data = $input['data']; |
| 684 | |
| 685 | // Content-Length (required) |
| 686 | if (isset($input['size']) && $input['size'] >= 0) |
| 687 | $rest->size = $input['size']; |
| 688 | else { |
| 689 | if (isset($input['file'])) |
| 690 | $rest->size = filesize($input['file']); |
| 691 | elseif (isset($input['data'])) |
| 692 | $rest->size = strlen($input['data']); |
| 693 | } |
| 694 | |
| 695 | // Custom request headers (Content-Type, Content-Disposition, Content-Encoding) |
| 696 | if (is_array($requestHeaders)) |
| 697 | foreach ($requestHeaders as $h => $v) $rest->setHeader($h, $v); |
| 698 | elseif (is_string($requestHeaders)) // Support for legacy contentType parameter |
| 699 | $input['type'] = $requestHeaders; |
| 700 | |
| 701 | // Content-Type |
| 702 | if (!isset($input['type'])) |
| 703 | { |
| 704 | if (isset($requestHeaders['Content-Type'])) |
| 705 | $input['type'] =& $requestHeaders['Content-Type']; |
| 706 | elseif (isset($input['file'])) |
| 707 | $input['type'] = self::__getMimeType($input['file']); |
| 708 | else |
| 709 | $input['type'] = 'application/octet-stream'; |
| 710 | } |
| 711 | |
| 712 | if ($storageClass !== self::STORAGE_CLASS_STANDARD) // Storage class |
| 713 | $rest->setAmzHeader('x-amz-storage-class', $storageClass); |
| 714 | |
| 715 | // We need to post with Content-Length and Content-Type, MD5 is optional |
| 716 | if ($rest->size >= 0 && ($rest->fp !== false || $rest->data !== false)) |
| 717 | { |
| 718 | $rest->setHeader('Content-Type', $input['type']); |
| 719 | if (isset($input['md5sum'])) $rest->setHeader('Content-MD5', $input['md5sum']); |
| 720 | |
| 721 | $rest->setAmzHeader('x-amz-acl', $acl); |
| 722 | foreach ($metaHeaders as $h => $v) $rest->setAmzHeader('x-amz-meta-'.$h, $v); |
| 723 | $rest->getResponse(); |
| 724 | } else |
| 725 | $rest->response->error = array('code' => 0, 'message' => 'Missing input parameters'); |
| 726 | |
| 727 | if ($rest->response->error === false && $rest->response->code !== 200) |
| 728 | $rest->response->error = array('code' => $rest->response->code, 'message' => 'Unexpected HTTP status'); |
| 729 | if ($rest->response->error !== false) |
| 730 | { |
| 731 | self::__triggerError(sprintf("UpdraftPlus_S3::putObject(): [%s] %s", |
| 732 | $rest->response->error['code'], $rest->response->error['message']), __FILE__, __LINE__); |
| 733 | return false; |
| 734 | } |
| 735 | return true; |
| 736 | } |
| 737 | |
| 738 | |
| 739 | /** |
| 740 | * Put an object from a file (legacy function) |
| 741 | * |
| 742 | * @param string $file Input file path |
| 743 | * @param string $bucket Bucket name |
| 744 | * @param string $uri Object URI |
| 745 | * @param constant $acl ACL constant |
| 746 | * @param array $metaHeaders Array of x-amz-meta-* headers |
| 747 | * @param string $contentType Content type |
| 748 | * @return boolean |
| 749 | */ |
| 750 | public static function putObjectFile($file, $bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $contentType = null, $storageClass = self::STORAGE_CLASS_STANDARD) |
| 751 | { |
| 752 | return self::putObject(self::inputFile($file), $bucket, $uri, $acl, $metaHeaders, $contentType, $storageClass); |
| 753 | } |
| 754 | |
| 755 | |
| 756 | /** |
| 757 | * Put an object from a string (legacy function) |
| 758 | * |
| 759 | * @param string $string Input data |
| 760 | * @param string $bucket Bucket name |
| 761 | * @param string $uri Object URI |
| 762 | * @param constant $acl ACL constant |
| 763 | * @param array $metaHeaders Array of x-amz-meta-* headers |
| 764 | * @param string $contentType Content type |
| 765 | * @return boolean |
| 766 | */ |
| 767 | public static function putObjectString($string, $bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $contentType = 'text/plain') |
| 768 | { |
| 769 | return self::putObject($string, $bucket, $uri, $acl, $metaHeaders, $contentType); |
| 770 | } |
| 771 | |
| 772 | |
| 773 | /** |
| 774 | * Get an object |
| 775 | * |
| 776 | * @param string $bucket Bucket name |
| 777 | * @param string $uri Object URI |
| 778 | * @param mixed $saveTo Filename or resource to write to |
| 779 | * @param boolean resume, if possible |
| 780 | * @return mixed |
| 781 | */ |
| 782 | public static function getObject($bucket, $uri, $saveTo = false, $resume = false) |
| 783 | { |
| 784 | $rest = new UpdraftPlus_S3Request('GET', $bucket, $uri, self::$endpoint, self::$use_dns_bucket_name); |
| 785 | if ($saveTo !== false) |
| 786 | { |
| 787 | if (is_resource($saveTo)) |
| 788 | $rest->fp = $saveTo; |
| 789 | else |
| 790 | if ($resume && file_exists($saveTo)) { |
| 791 | if (($rest->fp = @fopen($saveTo, 'ab')) !== false) { |
| 792 | $rest->setHeader('Range', "bytes=".filesize($saveTo).'-'); |
| 793 | $rest->file = realpath($saveTo); |
| 794 | } else { |
| 795 | $rest->response->error = array('code' => 0, 'message' => 'Unable to open save file for writing: '.$saveTo); |
| 796 | } |
| 797 | } else { |
| 798 | if (($rest->fp = @fopen($saveTo, 'wb')) !== false) |
| 799 | $rest->file = realpath($saveTo); |
| 800 | else |
| 801 | $rest->response->error = array('code' => 0, 'message' => 'Unable to open save file for writing: '.$saveTo); |
| 802 | } |
| 803 | } |
| 804 | if ($rest->response->error === false) $rest->getResponse(); |
| 805 | |
| 806 | if ($rest->response->error === false && ( !$resume && $rest->response->code != 200) || ( $resume && $rest->response->code != 206 && $rest->response->code != 200)) |
| 807 | $rest->response->error = array('code' => $rest->response->code, 'message' => 'Unexpected HTTP status'); |
| 808 | if ($rest->response->error !== false) |
| 809 | { |
| 810 | self::__triggerError(sprintf("UpdraftPlus_S3::getObject({$bucket}, {$uri}): [%s] %s", |
| 811 | $rest->response->error['code'], $rest->response->error['message']), __FILE__, __LINE__); |
| 812 | return false; |
| 813 | } |
| 814 | return $rest->response; |
| 815 | } |
| 816 | |
| 817 | |
| 818 | /** |
| 819 | * Get object information |
| 820 | * |
| 821 | * @param string $bucket Bucket name |
| 822 | * @param string $uri Object URI |
| 823 | * @param boolean $returnInfo Return response information |
| 824 | * @return mixed | false |
| 825 | */ |
| 826 | public static function getObjectInfo($bucket, $uri, $returnInfo = true) |
| 827 | { |
| 828 | $rest = new UpdraftPlus_S3Request('HEAD', $bucket, $uri, self::$endpoint, self::$use_dns_bucket_name); |
| 829 | $rest = $rest->getResponse(); |
| 830 | if ($rest->error === false && ($rest->code !== 200 && $rest->code !== 404)) |
| 831 | $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); |
| 832 | if ($rest->error !== false) |
| 833 | { |
| 834 | self::__triggerError(sprintf("UpdraftPlus_S3::getObjectInfo({$bucket}, {$uri}): [%s] %s", |
| 835 | $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); |
| 836 | return false; |
| 837 | } |
| 838 | return $rest->code == 200 ? $returnInfo ? $rest->headers : true : false; |
| 839 | } |
| 840 | |
| 841 | |
| 842 | /** |
| 843 | * Copy an object |
| 844 | * |
| 845 | * @param string $bucket Source bucket name |
| 846 | * @param string $uri Source object URI |
| 847 | * @param string $bucket Destination bucket name |
| 848 | * @param string $uri Destination object URI |
| 849 | * @param constant $acl ACL constant |
| 850 | * @param array $metaHeaders Optional array of x-amz-meta-* headers |
| 851 | * @param array $requestHeaders Optional array of request headers (content type, disposition, etc.) |
| 852 | * @param constant $storageClass Storage class constant |
| 853 | * @return mixed | false |
| 854 | */ |
| 855 | public static function copyObject($srcBucket, $srcUri, $bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $requestHeaders = array(), $storageClass = self::STORAGE_CLASS_STANDARD) |
| 856 | { |
| 857 | $rest = new UpdraftPlus_S3Request('PUT', $bucket, $uri, self::$endpoint, self::$use_dns_bucket_name); |
| 858 | $rest->setHeader('Content-Length', 0); |
| 859 | foreach ($requestHeaders as $h => $v) $rest->setHeader($h, $v); |
| 860 | foreach ($metaHeaders as $h => $v) $rest->setAmzHeader('x-amz-meta-'.$h, $v); |
| 861 | if ($storageClass !== self::STORAGE_CLASS_STANDARD) // Storage class |
| 862 | $rest->setAmzHeader('x-amz-storage-class', $storageClass); |
| 863 | $rest->setAmzHeader('x-amz-acl', $acl); |
| 864 | $rest->setAmzHeader('x-amz-copy-source', sprintf('/%s/%s', $srcBucket, rawurlencode($srcUri))); |
| 865 | if (sizeof($requestHeaders) > 0 || sizeof($metaHeaders) > 0) |
| 866 | $rest->setAmzHeader('x-amz-metadata-directive', 'REPLACE'); |
| 867 | |
| 868 | $rest = $rest->getResponse(); |
| 869 | if ($rest->error === false && $rest->code !== 200) |
| 870 | $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); |
| 871 | if ($rest->error !== false) |
| 872 | { |
| 873 | self::__triggerError(sprintf("UpdraftPlus_S3::copyObject({$srcBucket}, {$srcUri}, {$bucket}, {$uri}): [%s] %s", |
| 874 | $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); |
| 875 | return false; |
| 876 | } |
| 877 | return isset($rest->body->LastModified, $rest->body->ETag) ? array( |
| 878 | 'time' => strtotime((string)$rest->body->LastModified), |
| 879 | 'hash' => substr((string)$rest->body->ETag, 1, -1) |
| 880 | ) : false; |
| 881 | } |
| 882 | |
| 883 | |
| 884 | /** |
| 885 | * Set logging for a bucket |
| 886 | * |
| 887 | * @param string $bucket Bucket name |
| 888 | * @param string $targetBucket Target bucket (where logs are stored) |
| 889 | * @param string $targetPrefix Log prefix (e,g; domain.com-) |
| 890 | * @return boolean |
| 891 | */ |
| 892 | public static function setBucketLogging($bucket, $targetBucket, $targetPrefix = null) |
| 893 | { |
| 894 | // The S3 log delivery group has to be added to the target bucket's ACP |
| 895 | if ($targetBucket !== null && ($acp = self::getAccessControlPolicy($targetBucket, '')) !== false) |
| 896 | { |
| 897 | // Only add permissions to the target bucket when they do not exist |
| 898 | $aclWriteSet = false; |
| 899 | $aclReadSet = false; |
| 900 | foreach ($acp['acl'] as $acl) |
| 901 | if ($acl['type'] == 'Group' && $acl['uri'] == 'http://acs.amazonaws.com/groups/s3/LogDelivery') |
| 902 | { |
| 903 | if ($acl['permission'] == 'WRITE') $aclWriteSet = true; |
| 904 | elseif ($acl['permission'] == 'READ_ACP') $aclReadSet = true; |
| 905 | } |
| 906 | if (!$aclWriteSet) $acp['acl'][] = array( |
| 907 | 'type' => 'Group', 'uri' => 'http://acs.amazonaws.com/groups/s3/LogDelivery', 'permission' => 'WRITE' |
| 908 | ); |
| 909 | if (!$aclReadSet) $acp['acl'][] = array( |
| 910 | 'type' => 'Group', 'uri' => 'http://acs.amazonaws.com/groups/s3/LogDelivery', 'permission' => 'READ_ACP' |
| 911 | ); |
| 912 | if (!$aclReadSet || !$aclWriteSet) self::setAccessControlPolicy($targetBucket, '', $acp); |
| 913 | } |
| 914 | |
| 915 | $dom = new DOMDocument; |
| 916 | $bucketLoggingStatus = $dom->createElement('BucketLoggingStatus'); |
| 917 | $bucketLoggingStatus->setAttribute('xmlns', 'http://s3.amazonaws.com/doc/2006-03-01/'); |
| 918 | if ($targetBucket !== null) |
| 919 | { |
| 920 | if ($targetPrefix == null) $targetPrefix = $bucket . '-'; |
| 921 | $loggingEnabled = $dom->createElement('LoggingEnabled'); |
| 922 | $loggingEnabled->appendChild($dom->createElement('TargetBucket', $targetBucket)); |
| 923 | $loggingEnabled->appendChild($dom->createElement('TargetPrefix', $targetPrefix)); |
| 924 | // TODO: Add TargetGrants? |
| 925 | $bucketLoggingStatus->appendChild($loggingEnabled); |
| 926 | } |
| 927 | $dom->appendChild($bucketLoggingStatus); |
| 928 | |
| 929 | $rest = new UpdraftPlus_S3Request('PUT', $bucket, '', self::$endpoint, self::$use_dns_bucket_name); |
| 930 | $rest->setParameter('logging', null); |
| 931 | $rest->data = $dom->saveXML(); |
| 932 | $rest->size = strlen($rest->data); |
| 933 | $rest->setHeader('Content-Type', 'application/xml'); |
| 934 | $rest = $rest->getResponse(); |
| 935 | if ($rest->error === false && $rest->code !== 200) |
| 936 | $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); |
| 937 | if ($rest->error !== false) |
| 938 | { |
| 939 | self::__triggerError(sprintf("UpdraftPlus_S3::setBucketLogging({$bucket}, {$targetBucket}): [%s] %s", |
| 940 | $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); |
| 941 | return false; |
| 942 | } |
| 943 | return true; |
| 944 | } |
| 945 | |
| 946 | |
| 947 | /** |
| 948 | * Get logging status for a bucket |
| 949 | * |
| 950 | * This will return false if logging is not enabled. |
| 951 | * Note: To enable logging, you also need to grant write access to the log group |
| 952 | * |
| 953 | * @param string $bucket Bucket name |
| 954 | * @return array | false |
| 955 | */ |
| 956 | public static function getBucketLogging($bucket) |
| 957 | { |
| 958 | $rest = new UpdraftPlus_S3Request('GET', $bucket, '', self::$endpoint, self::$use_dns_bucket_name); |
| 959 | $rest->setParameter('logging', null); |
| 960 | $rest = $rest->getResponse(); |
| 961 | if ($rest->error === false && $rest->code !== 200) |
| 962 | $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); |
| 963 | if ($rest->error !== false) |
| 964 | { |
| 965 | self::__triggerError(sprintf("UpdraftPlus_S3::getBucketLogging({$bucket}): [%s] %s", |
| 966 | $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); |
| 967 | return false; |
| 968 | } |
| 969 | if (!isset($rest->body->LoggingEnabled)) return false; // No logging |
| 970 | return array( |
| 971 | 'targetBucket' => (string)$rest->body->LoggingEnabled->TargetBucket, |
| 972 | 'targetPrefix' => (string)$rest->body->LoggingEnabled->TargetPrefix, |
| 973 | ); |
| 974 | } |
| 975 | |
| 976 | |
| 977 | /** |
| 978 | * Disable bucket logging |
| 979 | * |
| 980 | * @param string $bucket Bucket name |
| 981 | * @return boolean |
| 982 | */ |
| 983 | public static function disableBucketLogging($bucket) |
| 984 | { |
| 985 | return self::setBucketLogging($bucket, null); |
| 986 | } |
| 987 | |
| 988 | |
| 989 | /** |
| 990 | * Get a bucket's location |
| 991 | * |
| 992 | * @param string $bucket Bucket name |
| 993 | * @return string | false |
| 994 | */ |
| 995 | public static function getBucketLocation($bucket) |
| 996 | { |
| 997 | |
| 998 | $rest = new UpdraftPlus_S3Request('GET', $bucket, '', self::$endpoint, self::$use_dns_bucket_name); |
| 999 | $rest->setParameter('location', null); |
| 1000 | $rest = $rest->getResponse(); |
| 1001 | if ($rest->error === false && $rest->code !== 200) |
| 1002 | $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); |
| 1003 | if ($rest->error !== false) |
| 1004 | { |
| 1005 | self::__triggerError(sprintf("UpdraftPlus_S3::getBucketLocation({$bucket}): [%s] %s", |
| 1006 | $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); |
| 1007 | return false; |
| 1008 | } |
| 1009 | return (isset($rest->body[0]) && (string)$rest->body[0] !== '') ? (string)$rest->body[0] : 'US'; |
| 1010 | } |
| 1011 | |
| 1012 | |
| 1013 | /** |
| 1014 | * Set object or bucket Access Control Policy |
| 1015 | * |
| 1016 | * @param string $bucket Bucket name |
| 1017 | * @param string $uri Object URI |
| 1018 | * @param array $acp Access Control Policy Data (same as the data returned from getAccessControlPolicy) |
| 1019 | * @return boolean |
| 1020 | */ |
| 1021 | public static function setAccessControlPolicy($bucket, $uri = '', $acp = array()) |
| 1022 | { |
| 1023 | $dom = new DOMDocument; |
| 1024 | $dom->formatOutput = true; |
| 1025 | $accessControlPolicy = $dom->createElement('AccessControlPolicy'); |
| 1026 | $accessControlList = $dom->createElement('AccessControlList'); |
| 1027 | |
| 1028 | // It seems the owner has to be passed along too |
| 1029 | $owner = $dom->createElement('Owner'); |
| 1030 | $owner->appendChild($dom->createElement('ID', $acp['owner']['id'])); |
| 1031 | $owner->appendChild($dom->createElement('DisplayName', $acp['owner']['name'])); |
| 1032 | $accessControlPolicy->appendChild($owner); |
| 1033 | |
| 1034 | foreach ($acp['acl'] as $g) |
| 1035 | { |
| 1036 | $grant = $dom->createElement('Grant'); |
| 1037 | $grantee = $dom->createElement('Grantee'); |
| 1038 | $grantee->setAttribute('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance'); |
| 1039 | if (isset($g['id'])) |
| 1040 | { // CanonicalUser (DisplayName is omitted) |
| 1041 | $grantee->setAttribute('xsi:type', 'CanonicalUser'); |
| 1042 | $grantee->appendChild($dom->createElement('ID', $g['id'])); |
| 1043 | } |
| 1044 | elseif (isset($g['email'])) |
| 1045 | { // AmazonCustomerByEmail |
| 1046 | $grantee->setAttribute('xsi:type', 'AmazonCustomerByEmail'); |
| 1047 | $grantee->appendChild($dom->createElement('EmailAddress', $g['email'])); |
| 1048 | } |
| 1049 | elseif ($g['type'] == 'Group') |
| 1050 | { // Group |
| 1051 | $grantee->setAttribute('xsi:type', 'Group'); |
| 1052 | $grantee->appendChild($dom->createElement('URI', $g['uri'])); |
| 1053 | } |
| 1054 | $grant->appendChild($grantee); |
| 1055 | $grant->appendChild($dom->createElement('Permission', $g['permission'])); |
| 1056 | $accessControlList->appendChild($grant); |
| 1057 | } |
| 1058 | |
| 1059 | $accessControlPolicy->appendChild($accessControlList); |
| 1060 | $dom->appendChild($accessControlPolicy); |
| 1061 | |
| 1062 | $rest = new UpdraftPlus_S3Request('PUT', $bucket, $uri, self::$endpoint, self::$use_dns_bucket_name); |
| 1063 | $rest->setParameter('acl', null); |
| 1064 | $rest->data = $dom->saveXML(); |
| 1065 | $rest->size = strlen($rest->data); |
| 1066 | $rest->setHeader('Content-Type', 'application/xml'); |
| 1067 | $rest = $rest->getResponse(); |
| 1068 | if ($rest->error === false && $rest->code !== 200) |
| 1069 | $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); |
| 1070 | if ($rest->error !== false) |
| 1071 | { |
| 1072 | self::__triggerError(sprintf("UpdraftPlus_S3::setAccessControlPolicy({$bucket}, {$uri}): [%s] %s", |
| 1073 | $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); |
| 1074 | return false; |
| 1075 | } |
| 1076 | return true; |
| 1077 | } |
| 1078 | |
| 1079 | |
| 1080 | /** |
| 1081 | * Get object or bucket Access Control Policy |
| 1082 | * |
| 1083 | * @param string $bucket Bucket name |
| 1084 | * @param string $uri Object URI |
| 1085 | * @return mixed | false |
| 1086 | */ |
| 1087 | public static function getAccessControlPolicy($bucket, $uri = '') |
| 1088 | { |
| 1089 | $rest = new UpdraftPlus_S3Request('GET', $bucket, $uri, self::$endpoint, self::$use_dns_bucket_name); |
| 1090 | $rest->setParameter('acl', null); |
| 1091 | $rest = $rest->getResponse(); |
| 1092 | if ($rest->error === false && $rest->code !== 200) |
| 1093 | $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); |
| 1094 | if ($rest->error !== false) |
| 1095 | { |
| 1096 | self::__triggerError(sprintf("UpdraftPlus_S3::getAccessControlPolicy({$bucket}, {$uri}): [%s] %s", |
| 1097 | $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); |
| 1098 | return false; |
| 1099 | } |
| 1100 | |
| 1101 | $acp = array(); |
| 1102 | if (isset($rest->body->Owner, $rest->body->Owner->ID, $rest->body->Owner->DisplayName)) |
| 1103 | $acp['owner'] = array( |
| 1104 | 'id' => (string)$rest->body->Owner->ID, 'name' => (string)$rest->body->Owner->DisplayName |
| 1105 | ); |
| 1106 | |
| 1107 | if (isset($rest->body->AccessControlList)) |
| 1108 | { |
| 1109 | $acp['acl'] = array(); |
| 1110 | foreach ($rest->body->AccessControlList->Grant as $grant) |
| 1111 | { |
| 1112 | foreach ($grant->Grantee as $grantee) |
| 1113 | { |
| 1114 | if (isset($grantee->ID, $grantee->DisplayName)) // CanonicalUser |
| 1115 | $acp['acl'][] = array( |
| 1116 | 'type' => 'CanonicalUser', |
| 1117 | 'id' => (string)$grantee->ID, |
| 1118 | 'name' => (string)$grantee->DisplayName, |
| 1119 | 'permission' => (string)$grant->Permission |
| 1120 | ); |
| 1121 | elseif (isset($grantee->EmailAddress)) // AmazonCustomerByEmail |
| 1122 | $acp['acl'][] = array( |
| 1123 | 'type' => 'AmazonCustomerByEmail', |
| 1124 | 'email' => (string)$grantee->EmailAddress, |
| 1125 | 'permission' => (string)$grant->Permission |
| 1126 | ); |
| 1127 | elseif (isset($grantee->URI)) // Group |
| 1128 | $acp['acl'][] = array( |
| 1129 | 'type' => 'Group', |
| 1130 | 'uri' => (string)$grantee->URI, |
| 1131 | 'permission' => (string)$grant->Permission |
| 1132 | ); |
| 1133 | else continue; |
| 1134 | } |
| 1135 | } |
| 1136 | } |
| 1137 | return $acp; |
| 1138 | } |
| 1139 | |
| 1140 | |
| 1141 | /** |
| 1142 | * Delete an object |
| 1143 | * |
| 1144 | * @param string $bucket Bucket name |
| 1145 | * @param string $uri Object URI |
| 1146 | * @return boolean |
| 1147 | */ |
| 1148 | public static function deleteObject($bucket, $uri) |
| 1149 | { |
| 1150 | $rest = new UpdraftPlus_S3Request('DELETE', $bucket, $uri, self::$endpoint, self::$use_dns_bucket_name); |
| 1151 | $rest = $rest->getResponse(); |
| 1152 | if ($rest->error === false && $rest->code !== 204) |
| 1153 | $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); |
| 1154 | if ($rest->error !== false) |
| 1155 | { |
| 1156 | self::__triggerError(sprintf("UpdraftPlus_S3::deleteObject(): [%s] %s", |
| 1157 | $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); |
| 1158 | return false; |
| 1159 | } |
| 1160 | return true; |
| 1161 | } |
| 1162 | |
| 1163 | |
| 1164 | /** |
| 1165 | * Get a query string authenticated URL |
| 1166 | * |
| 1167 | * @param string $bucket Bucket name |
| 1168 | * @param string $uri Object URI |
| 1169 | * @param integer $lifetime Lifetime in seconds |
| 1170 | * @param boolean $hostBucket Use the bucket name as the hostname |
| 1171 | * @param boolean $https Use HTTPS ($hostBucket should be false for SSL verification) |
| 1172 | * @return string |
| 1173 | */ |
| 1174 | public static function getAuthenticatedURL($bucket, $uri, $lifetime, $hostBucket = false, $https = false) |
| 1175 | { |
| 1176 | $expires = time() + $lifetime; |
| 1177 | $uri = str_replace(array('%2F', '%2B'), array('/', '+'), rawurlencode($uri)); |
| 1178 | return sprintf(($https ? 'https' : 'http').'://%s/%s?AWSAccessKeyId=%s&Expires=%u&Signature=%s', |
| 1179 | // $hostBucket ? $bucket : $bucket.'.s3.amazonaws.com', $uri, self::$__accessKey, $expires, |
| 1180 | $hostBucket ? $bucket : 's3.amazonaws.com/'.$bucket, $uri, self::$__accessKey, $expires, |
| 1181 | urlencode(self::__getHash("GET\n\n\n{$expires}\n/{$bucket}/{$uri}"))); |
| 1182 | } |
| 1183 | |
| 1184 | |
| 1185 | /** |
| 1186 | * Get a CloudFront signed policy URL |
| 1187 | * |
| 1188 | * @param array $policy Policy |
| 1189 | * @return string |
| 1190 | */ |
| 1191 | public static function getSignedPolicyURL($policy) |
| 1192 | { |
| 1193 | $data = json_encode($policy); |
| 1194 | $signature = ''; |
| 1195 | if (!openssl_sign($data, $signature, self::$__signingKeyResource)) return false; |
| 1196 | |
| 1197 | $encoded = str_replace(array('+', '='), array('-', '_', '~'), base64_encode($data)); |
| 1198 | $signature = str_replace(array('+', '='), array('-', '_', '~'), base64_encode($signature)); |
| 1199 | |
| 1200 | $url = $policy['Statement'][0]['Resource'] . '?'; |
| 1201 | foreach (array('Policy' => $encoded, 'Signature' => $signature, 'Key-Pair-Id' => self::$__signingKeyPairId) as $k => $v) |
| 1202 | $url .= $k.'='.str_replace('%2F', '/', rawurlencode($v)).'&'; |
| 1203 | return substr($url, 0, -1); |
| 1204 | } |
| 1205 | |
| 1206 | |
| 1207 | /** |
| 1208 | * Get a CloudFront canned policy URL |
| 1209 | * |
| 1210 | * @param string $string URL to sign |
| 1211 | * @param integer $lifetime URL lifetime |
| 1212 | * @return string |
| 1213 | */ |
| 1214 | public static function getSignedCannedURL($url, $lifetime) |
| 1215 | { |
| 1216 | return self::getSignedPolicyURL(array( |
| 1217 | 'Statement' => array( |
| 1218 | array('Resource' => $url, 'Condition' => array( |
| 1219 | 'DateLessThan' => array('AWS:EpochTime' => time() + $lifetime) |
| 1220 | )) |
| 1221 | ) |
| 1222 | )); |
| 1223 | } |
| 1224 | |
| 1225 | |
| 1226 | /** |
| 1227 | * Get upload POST parameters for form uploads |
| 1228 | * |
| 1229 | * @param string $bucket Bucket name |
| 1230 | * @param string $uriPrefix Object URI prefix |
| 1231 | * @param constant $acl ACL constant |
| 1232 | * @param integer $lifetime Lifetime in seconds |
| 1233 | * @param integer $maxFileSize Maximum filesize in bytes (default 5MB) |
| 1234 | * @param string $successRedirect Redirect URL or 200 / 201 status code |
| 1235 | * @param array $amzHeaders Array of x-amz-meta-* headers |
| 1236 | * @param array $headers Array of request headers or content type as a string |
| 1237 | * @param boolean $flashVars Includes additional "Filename" variable posted by Flash |
| 1238 | * @return object |
| 1239 | */ |
| 1240 | public static function getHttpUploadPostParams($bucket, $uriPrefix = '', $acl = self::ACL_PRIVATE, $lifetime = 3600, |
| 1241 | $maxFileSize = 5242880, $successRedirect = "201", $amzHeaders = array(), $headers = array(), $flashVars = false) |
| 1242 | { |
| 1243 | // Create policy object |
| 1244 | $policy = new stdClass; |
| 1245 | $policy->expiration = gmdate('Y-m-d\TH:i:s\Z', (time() + $lifetime)); |
| 1246 | $policy->conditions = array(); |
| 1247 | $obj = new stdClass; $obj->bucket = $bucket; array_push($policy->conditions, $obj); |
| 1248 | $obj = new stdClass; $obj->acl = $acl; array_push($policy->conditions, $obj); |
| 1249 | |
| 1250 | $obj = new stdClass; // 200 for non-redirect uploads |
| 1251 | if (is_numeric($successRedirect) && in_array((int)$successRedirect, array(200, 201))) |
| 1252 | $obj->success_action_status = (string)$successRedirect; |
| 1253 | else // URL |
| 1254 | $obj->success_action_redirect = $successRedirect; |
| 1255 | array_push($policy->conditions, $obj); |
| 1256 | |
| 1257 | if ($acl !== self::ACL_PUBLIC_READ) |
| 1258 | array_push($policy->conditions, array('eq', '$acl', $acl)); |
| 1259 | |
| 1260 | array_push($policy->conditions, array('starts-with', '$key', $uriPrefix)); |
| 1261 | if ($flashVars) array_push($policy->conditions, array('starts-with', '$Filename', '')); |
| 1262 | foreach (array_keys($headers) as $headerKey) |
| 1263 | array_push($policy->conditions, array('starts-with', '$'.$headerKey, '')); |
| 1264 | foreach ($amzHeaders as $headerKey => $headerVal) |
| 1265 | { |
| 1266 | $obj = new stdClass; |
| 1267 | $obj->{$headerKey} = (string)$headerVal; |
| 1268 | array_push($policy->conditions, $obj); |
| 1269 | } |
| 1270 | array_push($policy->conditions, array('content-length-range', 0, $maxFileSize)); |
| 1271 | $policy = base64_encode(str_replace('\/', '/', json_encode($policy))); |
| 1272 | |
| 1273 | // Create parameters |
| 1274 | $params = new stdClass; |
| 1275 | $params->AWSAccessKeyId = self::$__accessKey; |
| 1276 | $params->key = $uriPrefix.'${filename}'; |
| 1277 | $params->acl = $acl; |
| 1278 | $params->policy = $policy; unset($policy); |
| 1279 | $params->signature = self::__getHash($params->policy); |
| 1280 | if (is_numeric($successRedirect) && in_array((int)$successRedirect, array(200, 201))) |
| 1281 | $params->success_action_status = (string)$successRedirect; |
| 1282 | else |
| 1283 | $params->success_action_redirect = $successRedirect; |
| 1284 | foreach ($headers as $headerKey => $headerVal) $params->{$headerKey} = (string)$headerVal; |
| 1285 | foreach ($amzHeaders as $headerKey => $headerVal) $params->{$headerKey} = (string)$headerVal; |
| 1286 | return $params; |
| 1287 | } |
| 1288 | |
| 1289 | |
| 1290 | /** |
| 1291 | * Create a CloudFront distribution |
| 1292 | * |
| 1293 | * @param string $bucket Bucket name |
| 1294 | * @param boolean $enabled Enabled (true/false) |
| 1295 | * @param array $cnames Array containing CNAME aliases |
| 1296 | * @param string $comment Use the bucket name as the hostname |
| 1297 | * @param string $defaultRootObject Default root object |
| 1298 | * @param string $originAccessIdentity Origin access identity |
| 1299 | * @param array $trustedSigners Array of trusted signers |
| 1300 | * @return array | false |
| 1301 | */ |
| 1302 | public static function createDistribution($bucket, $enabled = true, $cnames = array(), $comment = null, $defaultRootObject = null, $originAccessIdentity = null, $trustedSigners = array()) |
| 1303 | { |
| 1304 | if (!extension_loaded('openssl')) |
| 1305 | { |
| 1306 | self::__triggerError(sprintf("UpdraftPlus_S3::createDistribution({$bucket}, ".(int)$enabled.", [], '$comment'): %s", |
| 1307 | "CloudFront functionality requires SSL"), __FILE__, __LINE__); |
| 1308 | return false; |
| 1309 | } |
| 1310 | $useSSL = self::$useSSL; |
| 1311 | |
| 1312 | self::$useSSL = true; // CloudFront requires SSL |
| 1313 | $rest = new UpdraftPlus_S3Request('POST', '', '2010-11-01/distribution', 'cloudfront.amazonaws.com'); |
| 1314 | $rest->data = self::__getCloudFrontDistributionConfigXML( |
| 1315 | $bucket.'.s3.amazonaws.com', |
| 1316 | $enabled, |
| 1317 | (string)$comment, |
| 1318 | (string)microtime(true), |
| 1319 | $cnames, |
| 1320 | $defaultRootObject, |
| 1321 | $originAccessIdentity, |
| 1322 | $trustedSigners |
| 1323 | ); |
| 1324 | |
| 1325 | $rest->size = strlen($rest->data); |
| 1326 | $rest->setHeader('Content-Type', 'application/xml'); |
| 1327 | $rest = self::__getCloudFrontResponse($rest); |
| 1328 | |
| 1329 | self::$useSSL = $useSSL; |
| 1330 | |
| 1331 | if ($rest->error === false && $rest->code !== 201) |
| 1332 | $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); |
| 1333 | if ($rest->error !== false) |
| 1334 | { |
| 1335 | self::__triggerError(sprintf("UpdraftPlus_S3::createDistribution({$bucket}, ".(int)$enabled.", [], '$comment'): [%s] %s", |
| 1336 | $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); |
| 1337 | return false; |
| 1338 | } elseif ($rest->body instanceof SimpleXMLElement) |
| 1339 | return self::__parseCloudFrontDistributionConfig($rest->body); |
| 1340 | return false; |
| 1341 | } |
| 1342 | |
| 1343 | |
| 1344 | /** |
| 1345 | * Get CloudFront distribution info |
| 1346 | * |
| 1347 | * @param string $distributionId Distribution ID from listDistributions() |
| 1348 | * @return array | false |
| 1349 | */ |
| 1350 | public static function getDistribution($distributionId) |
| 1351 | { |
| 1352 | if (!extension_loaded('openssl')) |
| 1353 | { |
| 1354 | self::__triggerError(sprintf("UpdraftPlus_S3::getDistribution($distributionId): %s", |
| 1355 | "CloudFront functionality requires SSL"), __FILE__, __LINE__); |
| 1356 | return false; |
| 1357 | } |
| 1358 | $useSSL = self::$useSSL; |
| 1359 | |
| 1360 | self::$useSSL = true; // CloudFront requires SSL |
| 1361 | $rest = new UpdraftPlus_S3Request('GET', '', '2010-11-01/distribution/'.$distributionId, 'cloudfront.amazonaws.com'); |
| 1362 | $rest = self::__getCloudFrontResponse($rest); |
| 1363 | |
| 1364 | self::$useSSL = $useSSL; |
| 1365 | |
| 1366 | if ($rest->error === false && $rest->code !== 200) |
| 1367 | $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); |
| 1368 | if ($rest->error !== false) |
| 1369 | { |
| 1370 | self::__triggerError(sprintf("UpdraftPlus_S3::getDistribution($distributionId): [%s] %s", |
| 1371 | $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); |
| 1372 | return false; |
| 1373 | } |
| 1374 | elseif ($rest->body instanceof SimpleXMLElement) |
| 1375 | { |
| 1376 | $dist = self::__parseCloudFrontDistributionConfig($rest->body); |
| 1377 | $dist['hash'] = $rest->headers['hash']; |
| 1378 | $dist['id'] = $distributionId; |
| 1379 | return $dist; |
| 1380 | } |
| 1381 | return false; |
| 1382 | } |
| 1383 | |
| 1384 | |
| 1385 | /** |
| 1386 | * Update a CloudFront distribution |
| 1387 | * |
| 1388 | * @param array $dist Distribution array info identical to output of getDistribution() |
| 1389 | * @return array | false |
| 1390 | */ |
| 1391 | public static function updateDistribution($dist) |
| 1392 | { |
| 1393 | if (!extension_loaded('openssl')) |
| 1394 | { |
| 1395 | self::__triggerError(sprintf("UpdraftPlus_S3::updateDistribution({$dist['id']}): %s", |
| 1396 | "CloudFront functionality requires SSL"), __FILE__, __LINE__); |
| 1397 | return false; |
| 1398 | } |
| 1399 | |
| 1400 | $useSSL = self::$useSSL; |
| 1401 | |
| 1402 | self::$useSSL = true; // CloudFront requires SSL |
| 1403 | $rest = new UpdraftPlus_S3Request('PUT', '', '2010-11-01/distribution/'.$dist['id'].'/config', 'cloudfront.amazonaws.com'); |
| 1404 | $rest->data = self::__getCloudFrontDistributionConfigXML( |
| 1405 | $dist['origin'], |
| 1406 | $dist['enabled'], |
| 1407 | $dist['comment'], |
| 1408 | $dist['callerReference'], |
| 1409 | $dist['cnames'], |
| 1410 | $dist['defaultRootObject'], |
| 1411 | $dist['originAccessIdentity'], |
| 1412 | $dist['trustedSigners'] |
| 1413 | ); |
| 1414 | |
| 1415 | $rest->size = strlen($rest->data); |
| 1416 | $rest->setHeader('If-Match', $dist['hash']); |
| 1417 | $rest = self::__getCloudFrontResponse($rest); |
| 1418 | |
| 1419 | self::$useSSL = $useSSL; |
| 1420 | |
| 1421 | if ($rest->error === false && $rest->code !== 200) |
| 1422 | $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); |
| 1423 | if ($rest->error !== false) |
| 1424 | { |
| 1425 | self::__triggerError(sprintf("UpdraftPlus_S3::updateDistribution({$dist['id']}): [%s] %s", |
| 1426 | $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); |
| 1427 | return false; |
| 1428 | } else { |
| 1429 | $dist = self::__parseCloudFrontDistributionConfig($rest->body); |
| 1430 | $dist['hash'] = $rest->headers['hash']; |
| 1431 | return $dist; |
| 1432 | } |
| 1433 | return false; |
| 1434 | } |
| 1435 | |
| 1436 | |
| 1437 | /** |
| 1438 | * Delete a CloudFront distribution |
| 1439 | * |
| 1440 | * @param array $dist Distribution array info identical to output of getDistribution() |
| 1441 | * @return boolean |
| 1442 | */ |
| 1443 | public static function deleteDistribution($dist) |
| 1444 | { |
| 1445 | if (!extension_loaded('openssl')) |
| 1446 | { |
| 1447 | self::__triggerError(sprintf("UpdraftPlus_S3::deleteDistribution({$dist['id']}): %s", |
| 1448 | "CloudFront functionality requires SSL"), __FILE__, __LINE__); |
| 1449 | return false; |
| 1450 | } |
| 1451 | |
| 1452 | $useSSL = self::$useSSL; |
| 1453 | |
| 1454 | self::$useSSL = true; // CloudFront requires SSL |
| 1455 | $rest = new UpdraftPlus_S3Request('DELETE', '', '2008-06-30/distribution/'.$dist['id'], 'cloudfront.amazonaws.com'); |
| 1456 | $rest->setHeader('If-Match', $dist['hash']); |
| 1457 | $rest = self::__getCloudFrontResponse($rest); |
| 1458 | |
| 1459 | self::$useSSL = $useSSL; |
| 1460 | |
| 1461 | if ($rest->error === false && $rest->code !== 204) |
| 1462 | $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); |
| 1463 | if ($rest->error !== false) |
| 1464 | { |
| 1465 | self::__triggerError(sprintf("UpdraftPlus_S3::deleteDistribution({$dist['id']}): [%s] %s", |
| 1466 | $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); |
| 1467 | return false; |
| 1468 | } |
| 1469 | return true; |
| 1470 | } |
| 1471 | |
| 1472 | |
| 1473 | /** |
| 1474 | * Get a list of CloudFront distributions |
| 1475 | * |
| 1476 | * @return array |
| 1477 | */ |
| 1478 | public static function listDistributions() |
| 1479 | { |
| 1480 | if (!extension_loaded('openssl')) |
| 1481 | { |
| 1482 | self::__triggerError(sprintf("UpdraftPlus_S3::listDistributions(): [%s] %s", |
| 1483 | "CloudFront functionality requires SSL"), __FILE__, __LINE__); |
| 1484 | return false; |
| 1485 | } |
| 1486 | |
| 1487 | $useSSL = self::$useSSL; |
| 1488 | self::$useSSL = true; // CloudFront requires SSL |
| 1489 | $rest = new UpdraftPlus_S3Request('GET', '', '2010-11-01/distribution', 'cloudfront.amazonaws.com'); |
| 1490 | $rest = self::__getCloudFrontResponse($rest); |
| 1491 | self::$useSSL = $useSSL; |
| 1492 | |
| 1493 | if ($rest->error === false && $rest->code !== 200) |
| 1494 | $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); |
| 1495 | if ($rest->error !== false) |
| 1496 | { |
| 1497 | self::__triggerError(sprintf("UpdraftPlus_S3::listDistributions(): [%s] %s", |
| 1498 | $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); |
| 1499 | return false; |
| 1500 | } |
| 1501 | elseif ($rest->body instanceof SimpleXMLElement && isset($rest->body->DistributionSummary)) |
| 1502 | { |
| 1503 | $list = array(); |
| 1504 | if (isset($rest->body->Marker, $rest->body->MaxItems, $rest->body->IsTruncated)) |
| 1505 | { |
| 1506 | //$info['marker'] = (string)$rest->body->Marker; |
| 1507 | //$info['maxItems'] = (int)$rest->body->MaxItems; |
| 1508 | //$info['isTruncated'] = (string)$rest->body->IsTruncated == 'true' ? true : false; |
| 1509 | } |
| 1510 | foreach ($rest->body->DistributionSummary as $summary) |
| 1511 | $list[(string)$summary->Id] = self::__parseCloudFrontDistributionConfig($summary); |
| 1512 | |
| 1513 | return $list; |
| 1514 | } |
| 1515 | return array(); |
| 1516 | } |
| 1517 | |
| 1518 | /** |
| 1519 | * List CloudFront Origin Access Identities |
| 1520 | * |
| 1521 | * @return array |
| 1522 | */ |
| 1523 | public static function listOriginAccessIdentities() |
| 1524 | { |
| 1525 | if (!extension_loaded('openssl')) |
| 1526 | { |
| 1527 | self::__triggerError(sprintf("UpdraftPlus_S3::listOriginAccessIdentities(): [%s] %s", |
| 1528 | "CloudFront functionality requires SSL"), __FILE__, __LINE__); |
| 1529 | return false; |
| 1530 | } |
| 1531 | |
| 1532 | self::$useSSL = true; // CloudFront requires SSL |
| 1533 | $rest = new UpdraftPlus_S3Request('GET', '', '2010-11-01/origin-access-identity/cloudfront', 'cloudfront.amazonaws.com'); |
| 1534 | $rest = self::__getCloudFrontResponse($rest); |
| 1535 | $useSSL = self::$useSSL; |
| 1536 | |
| 1537 | if ($rest->error === false && $rest->code !== 200) |
| 1538 | $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); |
| 1539 | if ($rest->error !== false) |
| 1540 | { |
| 1541 | trigger_error(sprintf("UpdraftPlus_S3::listOriginAccessIdentities(): [%s] %s", |
| 1542 | $rest->error['code'], $rest->error['message']), E_USER_WARNING); |
| 1543 | return false; |
| 1544 | } |
| 1545 | |
| 1546 | if (isset($rest->body->CloudFrontOriginAccessIdentitySummary)) |
| 1547 | { |
| 1548 | $identities = array(); |
| 1549 | foreach ($rest->body->CloudFrontOriginAccessIdentitySummary as $identity) |
| 1550 | if (isset($identity->S3CanonicalUserId)) |
| 1551 | $identities[(string)$identity->Id] = array('id' => (string)$identity->Id, 's3CanonicalUserId' => (string)$identity->S3CanonicalUserId); |
| 1552 | return $identities; |
| 1553 | } |
| 1554 | return false; |
| 1555 | } |
| 1556 | |
| 1557 | |
| 1558 | /** |
| 1559 | * Invalidate objects in a CloudFront distribution |
| 1560 | * |
| 1561 | * Thanks to Martin Lindkvist for UpdraftPlus_S3::invalidateDistribution() |
| 1562 | * |
| 1563 | * @param string $distributionId Distribution ID from listDistributions() |
| 1564 | * @param array $paths Array of object paths to invalidate |
| 1565 | * @return boolean |
| 1566 | */ |
| 1567 | public static function invalidateDistribution($distributionId, $paths) |
| 1568 | { |
| 1569 | if (!extension_loaded('openssl')) |
| 1570 | { |
| 1571 | self::__triggerError(sprintf("UpdraftPlus_S3::invalidateDistribution(): [%s] %s", |
| 1572 | "CloudFront functionality requires SSL"), __FILE__, __LINE__); |
| 1573 | return false; |
| 1574 | } |
| 1575 | |
| 1576 | $useSSL = self::$useSSL; |
| 1577 | self::$useSSL = true; // CloudFront requires SSL |
| 1578 | $rest = new UpdraftPlus_S3Request('POST', '', '2010-08-01/distribution/'.$distributionId.'/invalidation', 'cloudfront.amazonaws.com'); |
| 1579 | $rest->data = self::__getCloudFrontInvalidationBatchXML($paths, (string)microtime(true)); |
| 1580 | $rest->size = strlen($rest->data); |
| 1581 | $rest = self::__getCloudFrontResponse($rest); |
| 1582 | self::$useSSL = $useSSL; |
| 1583 | |
| 1584 | if ($rest->error === false && $rest->code !== 201) |
| 1585 | $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); |
| 1586 | if ($rest->error !== false) |
| 1587 | { |
| 1588 | trigger_error(sprintf("UpdraftPlus_S3::invalidate('{$distributionId}',{$paths}): [%s] %s", |
| 1589 | $rest->error['code'], $rest->error['message']), E_USER_WARNING); |
| 1590 | return false; |
| 1591 | } |
| 1592 | return true; |
| 1593 | } |
| 1594 | |
| 1595 | |
| 1596 | /** |
| 1597 | * Get a InvalidationBatch DOMDocument |
| 1598 | * |
| 1599 | * @internal Used to create XML in invalidateDistribution() |
| 1600 | * @param array $paths Paths to objects to invalidateDistribution |
| 1601 | * @return string |
| 1602 | */ |
| 1603 | private static function __getCloudFrontInvalidationBatchXML($paths, $callerReference = '0') { |
| 1604 | $dom = new DOMDocument('1.0', 'UTF-8'); |
| 1605 | $dom->formatOutput = true; |
| 1606 | $invalidationBatch = $dom->createElement('InvalidationBatch'); |
| 1607 | foreach ($paths as $path) |
| 1608 | $invalidationBatch->appendChild($dom->createElement('Path', $path)); |
| 1609 | |
| 1610 | $invalidationBatch->appendChild($dom->createElement('CallerReference', $callerReference)); |
| 1611 | $dom->appendChild($invalidationBatch); |
| 1612 | return $dom->saveXML(); |
| 1613 | } |
| 1614 | |
| 1615 | |
| 1616 | /** |
| 1617 | * List your invalidation batches for invalidateDistribution() in a CloudFront distribution |
| 1618 | * |
| 1619 | * http://docs.amazonwebservices.com/AmazonCloudFront/latest/APIReference/ListInvalidation.html |
| 1620 | * returned array looks like this: |
| 1621 | * Array |
| 1622 | * ( |
| 1623 | * [I31TWB0CN9V6XD] => InProgress |
| 1624 | * [IT3TFE31M0IHZ] => Completed |
| 1625 | * [I12HK7MPO1UQDA] => Completed |
| 1626 | * [I1IA7R6JKTC3L2] => Completed |
| 1627 | * ) |
| 1628 | * |
| 1629 | * @param string $distributionId Distribution ID from listDistributions() |
| 1630 | * @return array |
| 1631 | */ |
| 1632 | public static function getDistributionInvalidationList($distributionId) |
| 1633 | { |
| 1634 | if (!extension_loaded('openssl')) |
| 1635 | { |
| 1636 | self::__triggerError(sprintf("UpdraftPlus_S3::getDistributionInvalidationList(): [%s] %s", |
| 1637 | "CloudFront functionality requires SSL"), __FILE__, __LINE__); |
| 1638 | return false; |
| 1639 | } |
| 1640 | |
| 1641 | $useSSL = self::$useSSL; |
| 1642 | self::$useSSL = true; // CloudFront requires SSL |
| 1643 | $rest = new UpdraftPlus_S3Request('GET', '', '2010-11-01/distribution/'.$distributionId.'/invalidation', 'cloudfront.amazonaws.com'); |
| 1644 | $rest = self::__getCloudFrontResponse($rest); |
| 1645 | self::$useSSL = $useSSL; |
| 1646 | |
| 1647 | if ($rest->error === false && $rest->code !== 200) |
| 1648 | $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); |
| 1649 | if ($rest->error !== false) |
| 1650 | { |
| 1651 | trigger_error(sprintf("UpdraftPlus_S3::getDistributionInvalidationList('{$distributionId}'): [%s]", |
| 1652 | $rest->error['code'], $rest->error['message']), E_USER_WARNING); |
| 1653 | return false; |
| 1654 | } |
| 1655 | elseif ($rest->body instanceof SimpleXMLElement && isset($rest->body->InvalidationSummary)) |
| 1656 | { |
| 1657 | $list = array(); |
| 1658 | foreach ($rest->body->InvalidationSummary as $summary) |
| 1659 | $list[(string)$summary->Id] = (string)$summary->Status; |
| 1660 | |
| 1661 | return $list; |
| 1662 | } |
| 1663 | return array(); |
| 1664 | } |
| 1665 | |
| 1666 | |
| 1667 | /** |
| 1668 | * Get a DistributionConfig DOMDocument |
| 1669 | * |
| 1670 | * http://docs.amazonwebservices.com/AmazonCloudFront/latest/APIReference/index.html?PutConfig.html |
| 1671 | * |
| 1672 | * @internal Used to create XML in createDistribution() and updateDistribution() |
| 1673 | * @param string $bucket S3 Origin bucket |
| 1674 | * @param boolean $enabled Enabled (true/false) |
| 1675 | * @param string $comment Comment to append |
| 1676 | * @param string $callerReference Caller reference |
| 1677 | * @param array $cnames Array of CNAME aliases |
| 1678 | * @param string $defaultRootObject Default root object |
| 1679 | * @param string $originAccessIdentity Origin access identity |
| 1680 | * @param array $trustedSigners Array of trusted signers |
| 1681 | * @return string |
| 1682 | */ |
| 1683 | private static function __getCloudFrontDistributionConfigXML($bucket, $enabled, $comment, $callerReference = '0', $cnames = array(), $defaultRootObject = null, $originAccessIdentity = null, $trustedSigners = array()) |
| 1684 | { |
| 1685 | $dom = new DOMDocument('1.0', 'UTF-8'); |
| 1686 | $dom->formatOutput = true; |
| 1687 | $distributionConfig = $dom->createElement('DistributionConfig'); |
| 1688 | $distributionConfig->setAttribute('xmlns', 'http://cloudfront.amazonaws.com/doc/2010-11-01/'); |
| 1689 | |
| 1690 | $origin = $dom->createElement('S3Origin'); |
| 1691 | $origin->appendChild($dom->createElement('DNSName', $bucket)); |
| 1692 | if ($originAccessIdentity !== null) $origin->appendChild($dom->createElement('OriginAccessIdentity', $originAccessIdentity)); |
| 1693 | $distributionConfig->appendChild($origin); |
| 1694 | |
| 1695 | if ($defaultRootObject !== null) $distributionConfig->appendChild($dom->createElement('DefaultRootObject', $defaultRootObject)); |
| 1696 | |
| 1697 | $distributionConfig->appendChild($dom->createElement('CallerReference', $callerReference)); |
| 1698 | foreach ($cnames as $cname) |
| 1699 | $distributionConfig->appendChild($dom->createElement('CNAME', $cname)); |
| 1700 | if ($comment !== '') $distributionConfig->appendChild($dom->createElement('Comment', $comment)); |
| 1701 | $distributionConfig->appendChild($dom->createElement('Enabled', $enabled ? 'true' : 'false')); |
| 1702 | |
| 1703 | $trusted = $dom->createElement('TrustedSigners'); |
| 1704 | foreach ($trustedSigners as $id => $type) |
| 1705 | $trusted->appendChild($id !== '' ? $dom->createElement($type, $id) : $dom->createElement($type)); |
| 1706 | $distributionConfig->appendChild($trusted); |
| 1707 | |
| 1708 | $dom->appendChild($distributionConfig); |
| 1709 | //var_dump($dom->saveXML()); |
| 1710 | return $dom->saveXML(); |
| 1711 | } |
| 1712 | |
| 1713 | |
| 1714 | /** |
| 1715 | * Parse a CloudFront distribution config |
| 1716 | * |
| 1717 | * See http://docs.amazonwebservices.com/AmazonCloudFront/latest/APIReference/index.html?GetDistribution.html |
| 1718 | * |
| 1719 | * @internal Used to parse the CloudFront DistributionConfig node to an array |
| 1720 | * @param object &$node DOMNode |
| 1721 | * @return array |
| 1722 | */ |
| 1723 | private static function __parseCloudFrontDistributionConfig(&$node) |
| 1724 | { |
| 1725 | if (isset($node->DistributionConfig)) |
| 1726 | return self::__parseCloudFrontDistributionConfig($node->DistributionConfig); |
| 1727 | |
| 1728 | $dist = array(); |
| 1729 | if (isset($node->Id, $node->Status, $node->LastModifiedTime, $node->DomainName)) |
| 1730 | { |
| 1731 | $dist['id'] = (string)$node->Id; |
| 1732 | $dist['status'] = (string)$node->Status; |
| 1733 | $dist['time'] = strtotime((string)$node->LastModifiedTime); |
| 1734 | $dist['domain'] = (string)$node->DomainName; |
| 1735 | } |
| 1736 | |
| 1737 | if (isset($node->CallerReference)) |
| 1738 | $dist['callerReference'] = (string)$node->CallerReference; |
| 1739 | |
| 1740 | if (isset($node->Enabled)) |
| 1741 | $dist['enabled'] = (string)$node->Enabled == 'true' ? true : false; |
| 1742 | |
| 1743 | if (isset($node->S3Origin)) |
| 1744 | { |
| 1745 | if (isset($node->S3Origin->DNSName)) |
| 1746 | $dist['origin'] = (string)$node->S3Origin->DNSName; |
| 1747 | |
| 1748 | $dist['originAccessIdentity'] = isset($node->S3Origin->OriginAccessIdentity) ? |
| 1749 | (string)$node->S3Origin->OriginAccessIdentity : null; |
| 1750 | } |
| 1751 | |
| 1752 | $dist['defaultRootObject'] = isset($node->DefaultRootObject) ? (string)$node->DefaultRootObject : null; |
| 1753 | |
| 1754 | $dist['cnames'] = array(); |
| 1755 | if (isset($node->CNAME)) |
| 1756 | foreach ($node->CNAME as $cname) |
| 1757 | $dist['cnames'][(string)$cname] = (string)$cname; |
| 1758 | |
| 1759 | $dist['trustedSigners'] = array(); |
| 1760 | if (isset($node->TrustedSigners)) |
| 1761 | foreach ($node->TrustedSigners as $signer) |
| 1762 | { |
| 1763 | if (isset($signer->Self)) |
| 1764 | $dist['trustedSigners'][''] = 'Self'; |
| 1765 | elseif (isset($signer->KeyPairId)) |
| 1766 | $dist['trustedSigners'][(string)$signer->KeyPairId] = 'KeyPairId'; |
| 1767 | elseif (isset($signer->AwsAccountNumber)) |
| 1768 | $dist['trustedSigners'][(string)$signer->AwsAccountNumber] = 'AwsAccountNumber'; |
| 1769 | } |
| 1770 | |
| 1771 | $dist['comment'] = isset($node->Comment) ? (string)$node->Comment : null; |
| 1772 | return $dist; |
| 1773 | } |
| 1774 | |
| 1775 | |
| 1776 | /** |
| 1777 | * Grab CloudFront response |
| 1778 | * |
| 1779 | * @internal Used to parse the CloudFront UpdraftPlus_S3Request::getResponse() output |
| 1780 | * @param object &$rest UpdraftPlus_S3Request instance |
| 1781 | * @return object |
| 1782 | */ |
| 1783 | private static function __getCloudFrontResponse(&$rest) |
| 1784 | { |
| 1785 | $rest->getResponse(); |
| 1786 | if ($rest->response->error === false && isset($rest->response->body) && |
| 1787 | is_string($rest->response->body) && substr($rest->response->body, 0, 5) == '<?xml') |
| 1788 | { |
| 1789 | $rest->response->body = simplexml_load_string($rest->response->body); |
| 1790 | // Grab CloudFront errors |
| 1791 | if (isset($rest->response->body->Error, $rest->response->body->Error->Code, |
| 1792 | $rest->response->body->Error->Message)) |
| 1793 | { |
| 1794 | $rest->response->error = array( |
| 1795 | 'code' => (string)$rest->response->body->Error->Code, |
| 1796 | 'message' => (string)$rest->response->body->Error->Message |
| 1797 | ); |
| 1798 | unset($rest->response->body); |
| 1799 | } |
| 1800 | } |
| 1801 | return $rest->response; |
| 1802 | } |
| 1803 | |
| 1804 | |
| 1805 | /** |
| 1806 | * Get MIME type for file |
| 1807 | * |
| 1808 | * @internal Used to get mime types |
| 1809 | * @param string &$file File path |
| 1810 | * @return string |
| 1811 | */ |
| 1812 | public static function __getMimeType(&$file) |
| 1813 | { |
| 1814 | $type = false; |
| 1815 | // Fileinfo documentation says fileinfo_open() will use the |
| 1816 | // MAGIC env var for the magic file |
| 1817 | if (extension_loaded('fileinfo') && isset($_ENV['MAGIC']) && |
| 1818 | ($finfo = finfo_open(FILEINFO_MIME, $_ENV['MAGIC'])) !== false) |
| 1819 | { |
| 1820 | if (($type = finfo_file($finfo, $file)) !== false) |
| 1821 | { |
| 1822 | // Remove the charset and grab the last content-type |
| 1823 | $type = explode(' ', str_replace('; charset=', ';charset=', $type)); |
| 1824 | $type = array_pop($type); |
| 1825 | $type = explode(';', $type); |
| 1826 | $type = trim(array_shift($type)); |
| 1827 | } |
| 1828 | finfo_close($finfo); |
| 1829 | |
| 1830 | // If anyone is still using mime_content_type() |
| 1831 | } elseif (function_exists('mime_content_type')) |
| 1832 | $type = trim(mime_content_type($file)); |
| 1833 | |
| 1834 | if ($type !== false && strlen($type) > 0) return $type; |
| 1835 | |
| 1836 | // Otherwise do it the old fashioned way |
| 1837 | static $exts = array( |
| 1838 | 'jpg' => 'image/jpeg', 'gif' => 'image/gif', 'png' => 'image/png', |
| 1839 | 'tif' => 'image/tiff', 'tiff' => 'image/tiff', 'ico' => 'image/x-icon', |
| 1840 | 'swf' => 'application/x-shockwave-flash', 'pdf' => 'application/pdf', |
| 1841 | 'zip' => 'application/zip', 'gz' => 'application/x-gzip', |
| 1842 | 'tar' => 'application/x-tar', 'bz' => 'application/x-bzip', |
| 1843 | 'bz2' => 'application/x-bzip2', 'txt' => 'text/plain', |
| 1844 | 'asc' => 'text/plain', 'htm' => 'text/html', 'html' => 'text/html', |
| 1845 | 'css' => 'text/css', 'js' => 'text/javascript', |
| 1846 | 'xml' => 'text/xml', 'xsl' => 'application/xsl+xml', |
| 1847 | 'ogg' => 'application/ogg', 'mp3' => 'audio/mpeg', 'wav' => 'audio/x-wav', |
| 1848 | 'avi' => 'video/x-msvideo', 'mpg' => 'video/mpeg', 'mpeg' => 'video/mpeg', |
| 1849 | 'mov' => 'video/quicktime', 'flv' => 'video/x-flv', 'php' => 'text/x-php' |
| 1850 | ); |
| 1851 | $ext = strtolower(pathInfo($file, PATHINFO_EXTENSION)); |
| 1852 | return isset($exts[$ext]) ? $exts[$ext] : 'application/octet-stream'; |
| 1853 | } |
| 1854 | |
| 1855 | |
| 1856 | /** |
| 1857 | * Generate the auth string: "AWS AccessKey:Signature" |
| 1858 | * |
| 1859 | * @internal Used by UpdraftPlus_S3Request::getResponse() |
| 1860 | * @param string $string String to sign |
| 1861 | * @return string |
| 1862 | */ |
| 1863 | public static function __getSignature($string) |
| 1864 | { |
| 1865 | return 'AWS '.self::$__accessKey.':'.self::__getHash($string); |
| 1866 | } |
| 1867 | |
| 1868 | |
| 1869 | /** |
| 1870 | * Creates a HMAC-SHA1 hash |
| 1871 | * |
| 1872 | * This uses the hash extension if loaded |
| 1873 | * |
| 1874 | * @internal Used by __getSignature() |
| 1875 | * @param string $string String to sign |
| 1876 | * @return string |
| 1877 | */ |
| 1878 | private static function __getHash($string) |
| 1879 | { |
| 1880 | return base64_encode(extension_loaded('hash') ? |
| 1881 | hash_hmac('sha1', $string, self::$__secretKey, true) : pack('H*', sha1( |
| 1882 | (str_pad(self::$__secretKey, 64, chr(0x00)) ^ (str_repeat(chr(0x5c), 64))) . |
| 1883 | pack('H*', sha1((str_pad(self::$__secretKey, 64, chr(0x00)) ^ |
| 1884 | (str_repeat(chr(0x36), 64))) . $string))))); |
| 1885 | } |
| 1886 | |
| 1887 | } |
| 1888 | |
| 1889 | final class UpdraftPlus_S3Request |
| 1890 | { |
| 1891 | private $endpoint, $verb, $bucket, $uri, $resource = '', $parameters = array(), |
| 1892 | $amzHeaders = array(), $headers = array( |
| 1893 | 'Host' => '', 'Date' => '', 'Content-MD5' => '', 'Content-Type' => '' |
| 1894 | ); |
| 1895 | public $fp = false, $size = 0, $data = false, $response; |
| 1896 | |
| 1897 | |
| 1898 | /** |
| 1899 | * Constructor |
| 1900 | * |
| 1901 | * @param string $verb Verb |
| 1902 | * @param string $bucket Bucket name |
| 1903 | * @param string $uri Object URI |
| 1904 | * @return mixed |
| 1905 | */ |
| 1906 | function __construct($verb, $bucket = '', $uri = '', $endpoint = 's3.amazonaws.com', $use_dns_bucket_name = false) |
| 1907 | { |
| 1908 | $this->endpoint = $endpoint; |
| 1909 | $this->verb = $verb; |
| 1910 | $this->bucket = $bucket; |
| 1911 | $this->uri = $uri !== '' ? '/'.str_replace('%2F', '/', rawurlencode($uri)) : '/'; |
| 1912 | |
| 1913 | //if ($this->bucket !== '') |
| 1914 | // $this->resource = '/'.$this->bucket.$this->uri; |
| 1915 | //else |
| 1916 | // $this->resource = $this->uri; |
| 1917 | |
| 1918 | if ($this->bucket !== '') |
| 1919 | { |
| 1920 | if ($this->__dnsBucketName($this->bucket) || $use_dns_bucket_name) |
| 1921 | { |
| 1922 | $this->headers['Host'] = $this->bucket.'.'.$this->endpoint; |
| 1923 | $this->resource = '/'.$this->bucket.$this->uri; |
| 1924 | } |
| 1925 | else |
| 1926 | { |
| 1927 | $this->headers['Host'] = $this->endpoint; |
| 1928 | $this->uri = $this->uri; |
| 1929 | if ($this->bucket !== '') $this->uri = '/'.$this->bucket.$this->uri; |
| 1930 | $this->bucket = ''; |
| 1931 | $this->resource = $this->uri; |
| 1932 | } |
| 1933 | } |
| 1934 | else |
| 1935 | { |
| 1936 | $this->headers['Host'] = $this->endpoint; |
| 1937 | $this->resource = $this->uri; |
| 1938 | } |
| 1939 | |
| 1940 | |
| 1941 | $this->headers['Date'] = gmdate('D, d M Y H:i:s T'); |
| 1942 | $this->response = new STDClass; |
| 1943 | $this->response->error = false; |
| 1944 | $this->response->body = null; |
| 1945 | } |
| 1946 | |
| 1947 | |
| 1948 | /** |
| 1949 | * Set request parameter |
| 1950 | * |
| 1951 | * @param string $key Key |
| 1952 | * @param string $value Value |
| 1953 | * @return void |
| 1954 | */ |
| 1955 | public function setParameter($key, $value) |
| 1956 | { |
| 1957 | $this->parameters[$key] = $value; |
| 1958 | } |
| 1959 | |
| 1960 | |
| 1961 | /** |
| 1962 | * Set request header |
| 1963 | * |
| 1964 | * @param string $key Key |
| 1965 | * @param string $value Value |
| 1966 | * @return void |
| 1967 | */ |
| 1968 | public function setHeader($key, $value) |
| 1969 | { |
| 1970 | $this->headers[$key] = $value; |
| 1971 | } |
| 1972 | |
| 1973 | |
| 1974 | /** |
| 1975 | * Set x-amz-meta-* header |
| 1976 | * |
| 1977 | * @param string $key Key |
| 1978 | * @param string $value Value |
| 1979 | * @return void |
| 1980 | */ |
| 1981 | public function setAmzHeader($key, $value) |
| 1982 | { |
| 1983 | $this->amzHeaders[$key] = $value; |
| 1984 | } |
| 1985 | |
| 1986 | |
| 1987 | /** |
| 1988 | * Get the S3 response |
| 1989 | * |
| 1990 | * @return object | false |
| 1991 | */ |
| 1992 | public function getResponse() |
| 1993 | { |
| 1994 | $query = ''; |
| 1995 | if (sizeof($this->parameters) > 0) |
| 1996 | { |
| 1997 | $query = substr($this->uri, -1) !== '?' ? '?' : '&'; |
| 1998 | foreach ($this->parameters as $var => $value) |
| 1999 | if ($value == null || $value == '') $query .= $var.'&'; |
| 2000 | else $query .= $var.'='.rawurlencode($value).'&'; |
| 2001 | $query = substr($query, 0, -1); |
| 2002 | $this->uri .= $query; |
| 2003 | |
| 2004 | if (array_key_exists('acl', $this->parameters) || |
| 2005 | array_key_exists('location', $this->parameters) || |
| 2006 | array_key_exists('torrent', $this->parameters) || |
| 2007 | array_key_exists('logging', $this->parameters) || |
| 2008 | array_key_exists('partNumber', $this->parameters) || |
| 2009 | array_key_exists('uploads', $this->parameters) || |
| 2010 | array_key_exists('uploadId', $this->parameters)) |
| 2011 | $this->resource .= $query; |
| 2012 | } |
| 2013 | $url = (UpdraftPlus_S3::$useSSL ? 'https://' : 'http://') . ($this->headers['Host'] !== '' ? $this->headers['Host'] : $this->endpoint) . $this->uri; |
| 2014 | |
| 2015 | //var_dump('bucket: ' . $this->bucket, 'uri: ' . $this->uri, 'resource: ' . $this->resource, 'url: ' . $url); |
| 2016 | |
| 2017 | $curl = curl_init(); |
| 2018 | curl_setopt($curl, CURLOPT_USERAGENT, 'S3/php'); |
| 2019 | |
| 2020 | if (UpdraftPlus_S3::$useSSL) |
| 2021 | { |
| 2022 | // SSL Validation can now be optional for those with broken OpenSSL installations |
| 2023 | curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, UpdraftPlus_S3::$useSSLValidation ? 2 : 0); |
| 2024 | curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, UpdraftPlus_S3::$useSSLValidation ? 1 : 0); |
| 2025 | |
| 2026 | if (UpdraftPlus_S3::$sslKey !== null) curl_setopt($curl, CURLOPT_SSLKEY, UpdraftPlus_S3::$sslKey); |
| 2027 | if (UpdraftPlus_S3::$sslCert !== null) curl_setopt($curl, CURLOPT_SSLCERT, UpdraftPlus_S3::$sslCert); |
| 2028 | if (UpdraftPlus_S3::$sslCACert !== null) curl_setopt($curl, CURLOPT_CAINFO, UpdraftPlus_S3::$sslCACert); |
| 2029 | } |
| 2030 | |
| 2031 | curl_setopt($curl, CURLOPT_URL, $url); |
| 2032 | |
| 2033 | $wp_proxy = new WP_HTTP_Proxy(); |
| 2034 | |
| 2035 | if (UpdraftPlus_S3::$proxy != null && isset(UpdraftPlus_S3::$proxy['host']) && $wp_proxy->send_through_proxy($url)) |
| 2036 | { |
| 2037 | curl_setopt($curl, CURLOPT_PROXY, UpdraftPlus_S3::$proxy['host']); |
| 2038 | curl_setopt($curl, CURLOPT_PROXYTYPE, UpdraftPlus_S3::$proxy['type']); |
| 2039 | if (!empty(UpdraftPlus_S3::$proxy['port'])) curl_setopt($curl,CURLOPT_PROXYPORT, UpdraftPlus_S3::$proxy['port']); |
| 2040 | if (isset(UpdraftPlus_S3::$proxy['user'], UpdraftPlus_S3::$proxy['pass']) && UpdraftPlus_S3::$proxy['user'] != null && UpdraftPlus_S3::$proxy['pass'] != null) { |
| 2041 | curl_setopt($curl, CURLOPT_PROXYAUTH, CURLAUTH_ANY); |
| 2042 | curl_setopt($curl, CURLOPT_PROXYUSERPWD, sprintf('%s:%s', UpdraftPlus_S3::$proxy['user'], UpdraftPlus_S3::$proxy['pass'])); |
| 2043 | } |
| 2044 | } |
| 2045 | |
| 2046 | // Headers |
| 2047 | $headers = array(); $amz = array(); |
| 2048 | foreach ($this->amzHeaders as $header => $value) |
| 2049 | if (strlen($value) > 0) $headers[] = $header.': '.$value; |
| 2050 | foreach ($this->headers as $header => $value) |
| 2051 | if (strlen($value) > 0) $headers[] = $header.': '.$value; |
| 2052 | |
| 2053 | // Collect AMZ headers for signature |
| 2054 | foreach ($this->amzHeaders as $header => $value) |
| 2055 | if (strlen($value) > 0) $amz[] = strtolower($header).':'.$value; |
| 2056 | |
| 2057 | // AMZ headers must be sorted |
| 2058 | if (sizeof($amz) > 0) |
| 2059 | { |
| 2060 | //sort($amz); |
| 2061 | usort($amz, array(&$this, '__sortMetaHeadersCmp')); |
| 2062 | $amz = "\n".implode("\n", $amz); |
| 2063 | } else $amz = ''; |
| 2064 | |
| 2065 | if (UpdraftPlus_S3::hasAuth()) |
| 2066 | { |
| 2067 | // Authorization string (CloudFront stringToSign should only contain a date) |
| 2068 | if ($this->headers['Host'] == 'cloudfront.amazonaws.com') |
| 2069 | $headers[] = 'Authorization: ' . UpdraftPlus_S3::__getSignature($this->headers['Date']); |
| 2070 | else |
| 2071 | { |
| 2072 | $headers[] = 'Authorization: ' . UpdraftPlus_S3::__getSignature( |
| 2073 | $this->verb."\n". |
| 2074 | $this->headers['Content-MD5']."\n". |
| 2075 | $this->headers['Content-Type']."\n". |
| 2076 | $this->headers['Date'].$amz."\n". |
| 2077 | $this->resource |
| 2078 | ); |
| 2079 | } |
| 2080 | } |
| 2081 | |
| 2082 | curl_setopt($curl, CURLOPT_HTTPHEADER, $headers); |
| 2083 | curl_setopt($curl, CURLOPT_HEADER, false); |
| 2084 | curl_setopt($curl, CURLOPT_RETURNTRANSFER, false); |
| 2085 | curl_setopt($curl, CURLOPT_WRITEFUNCTION, array(&$this, '__responseWriteCallback')); |
| 2086 | curl_setopt($curl, CURLOPT_HEADERFUNCTION, array(&$this, '__responseHeaderCallback')); |
| 2087 | @curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true); |
| 2088 | |
| 2089 | // Request types |
| 2090 | switch ($this->verb) |
| 2091 | { |
| 2092 | case 'GET': break; |
| 2093 | case 'PUT': case 'POST': |
| 2094 | if ($this->fp !== false) |
| 2095 | { |
| 2096 | curl_setopt($curl, CURLOPT_PUT, true); |
| 2097 | curl_setopt($curl, CURLOPT_INFILE, $this->fp); |
| 2098 | if ($this->size >= 0) { |
| 2099 | curl_setopt($curl, CURLOPT_INFILESIZE, $this->size); |
| 2100 | } |
| 2101 | } |
| 2102 | elseif ($this->data !== false) |
| 2103 | { |
| 2104 | curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $this->verb); |
| 2105 | curl_setopt($curl, CURLOPT_POSTFIELDS, $this->data); |
| 2106 | curl_setopt($curl, CURLOPT_INFILESIZE, strlen($this->data)); |
| 2107 | } |
| 2108 | else { |
| 2109 | curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $this->verb); |
| 2110 | } |
| 2111 | break; |
| 2112 | case 'HEAD': |
| 2113 | curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'HEAD'); |
| 2114 | curl_setopt($curl, CURLOPT_NOBODY, true); |
| 2115 | break; |
| 2116 | case 'DELETE': |
| 2117 | curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'DELETE'); |
| 2118 | break; |
| 2119 | default: break; |
| 2120 | } |
| 2121 | |
| 2122 | // Execute, grab errors |
| 2123 | if (curl_exec($curl)) |
| 2124 | $this->response->code = curl_getinfo($curl, CURLINFO_HTTP_CODE); |
| 2125 | else |
| 2126 | $this->response->error = array( |
| 2127 | 'code' => curl_errno($curl), |
| 2128 | 'message' => curl_error($curl), |
| 2129 | 'resource' => $this->resource |
| 2130 | ); |
| 2131 | |
| 2132 | @curl_close($curl); |
| 2133 | |
| 2134 | // Parse body into XML |
| 2135 | if ($this->response->error === false && isset($this->response->headers['type']) && |
| 2136 | $this->response->headers['type'] == 'application/xml' && isset($this->response->body)) |
| 2137 | { |
| 2138 | $this->response->body = simplexml_load_string($this->response->body); |
| 2139 | |
| 2140 | // Grab S3 errors |
| 2141 | if (!in_array($this->response->code, array(200, 204, 206)) && |
| 2142 | isset($this->response->body->Code, $this->response->body->Message)) |
| 2143 | { |
| 2144 | $this->response->error = array( |
| 2145 | 'code' => (string)$this->response->body->Code, |
| 2146 | 'message' => (string)$this->response->body->Message |
| 2147 | ); |
| 2148 | if (isset($this->response->body->Resource)) |
| 2149 | $this->response->error['resource'] = (string)$this->response->body->Resource; |
| 2150 | unset($this->response->body); |
| 2151 | } |
| 2152 | } |
| 2153 | |
| 2154 | // Clean up file resources |
| 2155 | if ($this->fp !== false && is_resource($this->fp)) fclose($this->fp); |
| 2156 | |
| 2157 | return $this->response; |
| 2158 | } |
| 2159 | |
| 2160 | /** |
| 2161 | * Sort compare for meta headers |
| 2162 | * |
| 2163 | * @internal Used to sort x-amz meta headers |
| 2164 | * @param string $a String A |
| 2165 | * @param string $b String B |
| 2166 | * @return integer |
| 2167 | */ |
| 2168 | private function __sortMetaHeadersCmp($a, $b) |
| 2169 | { |
| 2170 | $lenA = strpos($a, ':'); |
| 2171 | $lenB = strpos($b, ':'); |
| 2172 | $minLen = min($lenA, $lenB); |
| 2173 | $ncmp = strncmp($a, $b, $minLen); |
| 2174 | if ($lenA == $lenB) return $ncmp; |
| 2175 | if (0 == $ncmp) return $lenA < $lenB ? -1 : 1; |
| 2176 | return $ncmp; |
| 2177 | } |
| 2178 | |
| 2179 | /** |
| 2180 | * CURL write callback |
| 2181 | * |
| 2182 | * @param resource $curl CURL resource |
| 2183 | * @param string $data Data |
| 2184 | * @return integer |
| 2185 | */ |
| 2186 | private function __responseWriteCallback($curl, $data) |
| 2187 | { |
| 2188 | if (in_array($this->response->code, array(200, 206)) && $this->fp !== false) |
| 2189 | return fwrite($this->fp, $data); |
| 2190 | else |
| 2191 | $this->response->body = (empty($this->response->body)) ? $data : $this->response->body.$data; |
| 2192 | return strlen($data); |
| 2193 | } |
| 2194 | |
| 2195 | |
| 2196 | /** |
| 2197 | * Check DNS conformity |
| 2198 | * |
| 2199 | * @param string $bucket Bucket name |
| 2200 | * @return boolean |
| 2201 | */ |
| 2202 | private function __dnsBucketName($bucket) |
| 2203 | { |
| 2204 | # A DNS bucket name cannot have len>63 |
| 2205 | # A DNS bucket name must have a character in other than a-z, 0-9, . - |
| 2206 | # 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? |
| 2207 | if (strlen($bucket) > 63 || !preg_match("/[^a-z0-9\.-]/", $bucket)) return false; |
| 2208 | # A DNS bucket name cannot contain -. |
| 2209 | if (strstr($bucket, '-.') !== false) return false; |
| 2210 | # A DNS bucket name cannot contain .. |
| 2211 | if (strstr($bucket, '..') !== false) return false; |
| 2212 | # A DNS bucket name must begin with 0-9a-z |
| 2213 | if (!preg_match("/^[0-9a-z]/", $bucket)) return false; |
| 2214 | # A DNS bucket name must end with 0-9 a-z |
| 2215 | if (!preg_match("/[0-9a-z]$/", $bucket)) return false; |
| 2216 | return true; |
| 2217 | } |
| 2218 | |
| 2219 | /** |
| 2220 | * CURL header callback |
| 2221 | * |
| 2222 | * @param resource $curl CURL resource |
| 2223 | * @param string $data Data |
| 2224 | * @return integer |
| 2225 | */ |
| 2226 | private function __responseHeaderCallback($curl, $data) |
| 2227 | { |
| 2228 | if (($strlen = strlen($data)) <= 2) return $strlen; |
| 2229 | if (substr($data, 0, 4) == 'HTTP') |
| 2230 | $this->response->code = (int)substr($data, 9, 3); |
| 2231 | else |
| 2232 | { |
| 2233 | $data = trim($data); |
| 2234 | if (strpos($data, ': ') === false) return $strlen; |
| 2235 | list($header, $value) = explode(': ', $data, 2); |
| 2236 | if (strtolower($header) == 'last-modified') |
| 2237 | $this->response->headers['time'] = strtotime($value); |
| 2238 | elseif (strtolower($header) == 'content-length') |
| 2239 | $this->response->headers['size'] = (int)$value; |
| 2240 | elseif (strtolower($header) == 'content-type') |
| 2241 | $this->response->headers['type'] = $value; |
| 2242 | elseif (strtolower($header) == 'etag') |
| 2243 | $this->response->headers['hash'] = $value{0} == '"' ? substr($value, 1, -1) : $value; |
| 2244 | elseif (preg_match('/^x-amz-meta-.*$/i', $header)) |
| 2245 | $this->response->headers[strtolower($header)] = $value; |
| 2246 | } |
| 2247 | return $strlen; |
| 2248 | } |
| 2249 | |
| 2250 | } |
| 2251 |