diff --git a/src/S3/PostObject.php b/src/S3/PostObject.php index 73de7a8826..20c18ef868 100644 --- a/src/S3/PostObject.php +++ b/src/S3/PostObject.php @@ -5,151 +5,49 @@ /** * Encapsulates the logic for getting the data for an S3 object POST upload form + * + * @link http://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectPOST.html */ class PostObject { - /** @var S3Client The S3 client being used to sign the policy */ private $client; - - /** @var string The bucket name where the object will be posted */ private $bucket; - - /** @var array The
tag attributes as an array */ private $formAttributes; - - /** @var array The form's elements as an array */ private $formInputs; - - /** @var string The raw json policy */ private $jsonPolicy; /** - * Constructs the PostObject - * - * The options array accepts the following keys: - * - * - acl: The access control setting to apply to the uploaded file. Accepts - * any of the CannedAcl constants. - * - Cache-Control: The Cache-Control HTTP header value to apply to the - * uploaded file - * - Content-Disposition: The Content-Disposition HTTP header value to - * apply to the uploaded file - * - Content-Encoding: The Content-Encoding HTTP header value to apply to - * the uploaded file. - * - Content-Type: The Content-Type HTTP header value to apply to the - * uploaded file. The default value is `application/octet-stream`. - * - Expires: The Expires HTTP header value to apply to the uploaded file - * - key: The location where the file should be uploaded to. The default - * value is `^${filename}` which will use the name of the uploaded file. - * - policy: A raw policy in JSON format. By default, the PostObject - * creates one for you. - * - policy_callback: A callback used to modify the policy before encoding - * and signing it. The method signature for the callback should accept an - * array of the policy data as the 1st argument, (optionally) the - * PostObject as the 2nd argument, and return the policy data with the - * desired modifications. - * - success_action_redirect: The URI for Amazon S3 to redirect to upon - * successful upload. - * - success_action_status: The status code for Amazon S3 to return upon - * successful upload. - * - ttd: The expiration time for the generated upload form data - * - x-amz-meta-*: Any custom meta tag that should be set to the object - * - x-amz-server-side-encryption: The server-side encryption mechanism to - * use - * - x-amz-storage-class: The storage setting to apply to the object - * - x-amz-storage-class: The storage setting to apply to the object - * - x-amz-server-side-encryption-customer-algorithm: The SSE-C algorithm - * - x-amz-server-side-encryption-customer-key: The SSE-C secret key - * - x-amz-server-side-encryption-customer-key-MD5: MD5 hash of the - * SSE-C customer secret key + * Constructs the PostObject. * - * For the Cache-Control, Content-Disposition, Content-Encoding, - * Content-Type, Expires, and key options, to use a "starts-with" comparison - * instead of an equals comparison, prefix the value with a ^ (carat) - * character. - * - * @param S3Client $client Client used with the POST object - * @param string $bucket Bucket to use - * @param array $options Associative array of options + * @param S3Client $client Client used with the POST object + * @param string $bucket Bucket to use + * @param array $formInputs Associative array of form input fields. + * @param string|array $jsonPolicy JSON encoded POST policy document. The + * policy will be base64 encoded and applied + * to the form on your behalf. */ - public function __construct(S3Client $client, $bucket, array $options = []) - { + public function __construct( + S3Client $client, + $bucket, + array $formInputs, + $jsonPolicy + ) { $this->client = $client; $this->bucket = $bucket; - parent::__construct($options); - } - /** - * Prepares the POST object to be utilzed to build a POST form. - * - * @return PostObject - */ - public function prepareData() - { - // Validate required options - $options = Collection::fromConfig($this->data, [ - 'ttd' => '+1 hour', - 'key' => '^${filename}', - ]); - - $ttd = $this->pluckTtd($options); - - // If a policy or policy callback were provided, extract those from - // the options. - $rawJsonPolicy = $options['policy']; - $policyCallback = $options['policy_callback']; - unset($options['policy'], $options['policy_callback']); - - // Setup policy document - $policy = [ - 'expiration' => gmdate('Y-m-d\TH:i:s\Z', $ttd), - 'conditions' => [['bucket' => $this->bucket]] - ]; + if (is_array($jsonPolicy)) { + $jsonPolicy = json_encode($jsonPolicy); + } - // Setup basic form + $this->jsonPolicy = $jsonPolicy; $this->formAttributes = [ - 'action' => $this->generateUrl($options), + 'action' => $this->generateUri(), 'method' => 'POST', 'enctype' => 'multipart/form-data' ]; - - $this->formInputs = [ - 'AWSAccessKeyId' => $this->client->getCredentials()->getAccessKeyId() - ]; - - // Add success action status - $status = (int) $options->get('success_action_status'); - - if ($status && in_array($status, [200, 201, 204])) { - $this->formInputs['success_action_status'] = (string) $status; - $policy['conditions'][] = [ - 'success_action_status' => (string) $status - ]; - unset($options['success_action_status']); - } - - // Add other options - foreach ($options as $key => $value) { - $value = (string) $value; - if ($value[0] === '^') { - $value = substr($value, 1); - $this->formInputs[$key] = $value; - $value = preg_replace('/\$\{(\w*)\}/', '', $value); - $policy['conditions'][] = ['starts-with', '$' . $key, $value]; - } else { - $this->formInputs[$key] = $value; - $policy['conditions'][] = [$key => $value]; - } - } - - // Handle the policy - $policy = is_callable($policyCallback) - ? $policyCallback($policy, $this) - : $policy; - $this->jsonPolicy = $rawJsonPolicy ?: json_encode($policy); - $this->applyPolicy(); - - return $this; + $this->formInputs = $formInputs + ['key' => '${filename}']; + $this->formInputs['AWSAccessKeyId'] = $client->getCredentials()->getAccessKeyId(); + $this->formInputs += $this->getPolicyAndSignature(); } /** @@ -182,6 +80,17 @@ public function getFormAttributes() return $this->formAttributes; } + /** + * Set a form attribute. + * + * @param string $attribute Form attribute to set. + * @param string $value Value to set. + */ + public function setFormAttribute($attribute, $value) + { + $this->formAttributes[$attribute] = $value; + } + /** * Gets the form inputs as an array. * @@ -192,6 +101,17 @@ public function getFormInputs() return $this->formInputs; } + /** + * Set a form input. + * + * @param string $field Field name to set + * @param string $value Value to set. + */ + public function setFormInput($field, $value) + { + $this->formInputs[$field] = $value; + } + /** * Gets the raw JSON policy. * @@ -202,45 +122,35 @@ public function getJsonPolicy() return $this->jsonPolicy; } - private function pluckTtd(Collection $options) + private function generateUri() { - $ttd = $options['ttd']; - $ttd = is_numeric($ttd) ? (int) $ttd : strtotime($ttd); - unset($options['ttd']); + $uri = new Uri($this->client->getEndpoint()); - return $ttd; - } - - private function generateUrl() - { - $url = Url::fromString($this->client->getEndpoint()); - - if ($url->getScheme() === 'https' && - strpos($this->bucket, '.') !== false + if ($uri->getScheme() === 'https' + && strpos($this->bucket, '.') !== false ) { // Use path-style URLs - $url->setPath($this->bucket); + $uri = $uri->withPath($this->bucket); } else { // Use virtual-style URLs - $url->setHost($this->bucket . '.' . $url->getHost()); + $uri = $uri->withHost($this->bucket . '.' . $uri->getHost()); } - return $url; + return (string) $uri; } - /** - * Handles the encoding, signing, and injecting of the policy - */ - protected function applyPolicy() + protected function getPolicyAndSignature() { $jsonPolicy64 = base64_encode($this->jsonPolicy); - $this->formInputs['policy'] = $jsonPolicy64; - - $this->formInputs['signature'] = base64_encode(hash_hmac( - 'sha1', - $jsonPolicy64, - $this->client->getCredentials()->getSecretKey(), - true - )); + + return [ + 'policy' => $jsonPolicy64, + 'signature' => base64_encode(hash_hmac( + 'sha1', + $jsonPolicy64, + $this->client->getCredentials()->getSecretKey(), + true + )) + ]; } } diff --git a/tests/S3/PostObjectTest.php b/tests/S3/PostObjectTest.php index e7907f85f8..a557fb6bd1 100644 --- a/tests/S3/PostObjectTest.php +++ b/tests/S3/PostObjectTest.php @@ -1,6 +1,7 @@ getMockBuilder('Aws\Credentials\Credentials') - ->disableOriginalConstructor() - ->getMock(); - $credentials->expects($this->any()) - ->method('getAccessKeyId') - ->will($this->returnValue('AKIAXXXXXXXXXXXXXXX')); - $credentials->expects($this->any()) - ->method('getSecretKey') - ->will($this->returnValue('XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX')); - $this->client = $this->getTestClient( 's3', - ['credentials' => $credentials] + [ + 'credentials' => new Credentials( + 'AKIAXXXXXXXXXXXXXXX', + 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' + ) + ] ); } - public function getDataForPostObjectTest() + public function testSignsPostPolicy() { - $cases = []; - - // Inputs capturing starts-with and success_action_status behaviors - $cases[] = [ - // Options - [ - 'Content-Type' => '^text/', - 'ttd' => 'Nov 24, 1984, midnight GMT', - 'acl' => 'private', - 'success_action_status' => 201, - 'key' => '^foo/bar/${filename}', - 'policy_callback' => function (array $policy) { - $policy['conditions'][] = ['fizz' => 'buzz']; - return $policy; - } - ], - // Expected Results - [ - 'attributes' => [ - 'action' => 'https://foo.s3.amazonaws.com', - 'method' => 'POST', - 'enctype' => 'multipart/form-data' - ], - 'inputs' => [ - 'AWSAccessKeyId' => 'AKIAXXXXXXXXXXXXXXX', - 'success_action_status' => '201', - 'key' => 'foo/bar/${filename}', - 'Content-Type' => 'text/', - 'acl' => 'private', - 'policy' => 'eyJleHBpcmF0aW9uIjoiMTk4NC0xMS0yNFQwMDowMDowMFoiLCJjb25kaXRpb25zIjpbeyJidWNrZXQiOiJmb28ifSx7InN1Y2Nlc3NfYWN0aW9uX3N0YXR1cyI6IjIwMSJ9LFsic3RhcnRzLXdpdGgiLCIkQ29udGVudC1UeXBlIiwidGV4dFwvIl0seyJhY2wiOiJwcml2YXRlIn0sWyJzdGFydHMtd2l0aCIsIiRrZXkiLCJmb29cL2JhclwvIl0seyJmaXp6IjoiYnV6eiJ9XX0=', - 'signature' => 'XKwHh/c1moTcCw1L5xY/xmb/b58=' - ], - 'policy' => '{"expiration":"1984-11-24T00:00:00Z","conditions":[{"bucket":"foo"},{"success_action_status":"201"},["starts-with","$Content-Type","text\/"],{"acl":"private"},["starts-with","$key","foo\/bar\/"],{"fizz":"buzz"}]}' - ] - ]; - - // Passing in a raw policy - $cases[] = [ - // Options - [ - 'policy' => '{"expiration":"1984-11-24T00:00:00Z","conditions":[{"bucket":"foo"},{"success_action_stat' - . 'us":"201"},["starts-with","$key","foo\\/bar\\/"],["starts-with","$Content-Type","text\\/"]]}' - ], - // Expected Results - [ - 'attributes' => [ - 'action' => 'https://foo.s3.amazonaws.com', - 'method' => 'POST', - 'enctype' => 'multipart/form-data' - ], - 'inputs' => [ - 'AWSAccessKeyId' => 'AKIAXXXXXXXXXXXXXXX', - 'key' => '${filename}', - 'policy' => 'eyJleHBpcmF0aW9uIjoiMTk4NC0xMS0yNFQwMDowMDowMFoiLCJjb25kaXRpb25zIjpbeyJidWNrZXQiOiJmb' - . '28ifSx7InN1Y2Nlc3NfYWN0aW9uX3N0YXR1cyI6IjIwMSJ9LFsic3RhcnRzLXdpdGgiLCIka2V5IiwiZm9vXC9iYXJc' - . 'LyJdLFsic3RhcnRzLXdpdGgiLCIkQ29udGVudC1UeXBlIiwidGV4dFwvIl1dfQ==', - 'signature' => 'h92mKuUkaKTNmJMqnHDZ51+2+GY=' - ], - 'policy' => '{"expiration":"1984-11-24T00:00:00Z","conditions":[{"bucket":"foo"},{"success_action_stat' - . 'us":"201"},["starts-with","$key","foo\\/bar\\/"],["starts-with","$Content-Type","text\\/"]]}' + $policy = [ + 'expiration' => '2007-12-01T12:00:00.000Z', + 'conditions' => [ + 'acl' => 'public-read' ] ]; - - return $cases; - } - - /** - * @dataProvider getDataForPostObjectTest - */ - public function testGetPostObjectData(array $options, array $expected) - { - $postObject = new PostObject($this->client, 'foo', $options); - $postObject->prepareData(); + $p = new PostObject($this->client, 'foo', [], $policy); + $a = $p->getFormInputs(); + $this->assertSame( + 'eyJleHBpcmF0aW9uIjoiMjAwNy0xMi0wMVQxMjowMDowMC4wMDBaIiwiY29uZGl0aW9ucyI6eyJhY2wiOiJwdWJsaWMtcmVhZCJ9fQ==', + $a['policy'] + ); + $this->assertEquals('ffajJbr1afVRb3qoFwdn9RK+qfM=', $a['signature']); $this->assertEquals( - $expected['attributes'], - $postObject->getFormAttributes() + '{"expiration":"2007-12-01T12:00:00.000Z","conditions":{"acl":"public-read"}}', + $p->getJsonPolicy() ); - $this->assertEquals($expected['inputs'], $postObject->getFormInputs()); - $this->assertEquals($expected['policy'], $postObject->getJsonPolicy()); } public function testClientAndBucketGetters() { - $postObject = new PostObject($this->client, 'foo'); + $postObject = new PostObject($this->client, 'foo', [], ''); $this->assertSame($this->client, $postObject->getClient()); $this->assertSame('foo', $postObject->getBucket()); + $postObject->setFormInput('a', 'b'); + $this->assertEquals('b', $postObject->getFormInputs()['a']); + $postObject->setFormAttribute('c', 'd'); + $this->assertEquals('d', $postObject->getFormAttributes()['c']); + $this->assertEquals('', $postObject->getJsonPolicy()); } public function testCanHandleDomainsWithDots() { - $postObject = new PostObject($this->client, 'foo.bar'); - $postObject->prepareData(); + $postObject = new PostObject($this->client, 'foo.bar', [], ''); $formAttrs = $postObject->getFormAttributes(); $this->assertEquals( 'https://s3.amazonaws.com/foo.bar',