PluginProbe
WP Database Backup – Unlimited Database & Files Backup by Backup for WP / 6.12
WP Database Backup – Unlimited Database & Files Backup by Backup for WP v6.12
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 6.12, at includes/admin/Destination/S3/S3.php

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