PluginProbe
WP Database Backup – Unlimited Database & Files Backup by Backup for WP / 7.13
WP Database Backup – Unlimited Database & Files Backup by Backup for WP v7.13
7.13 7.12 trunk 1.1 2.1.1 5.9 6.0 6.1 6.10 6.11 6.12 6.12.1 6.2 6.3 6.4 6.5 6.5.1 6.6 6.7 6.8 6.9 7.0 7.0.1 7.1 7.10 All 34 releases
wp-database-backup / includes / admin / Destination / S3 / S3.php

S3.php in WP Database Backup – Unlimited Database & Files Backup by Backup for WP 7.13, at includes/admin/Destination/S3/S3.php

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