adam-prescott

AWS S3 Class

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