Completed
Pull Request — master (#2)
by
unknown
02:39
created

Signature   A

Complexity

Total Complexity 27

Size/Duplication

Total Lines 295
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 27
lcom 1
cbo 2
dl 0
loc 295
rs 10
c 1
b 0
f 0

17 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 10 1
A setAwsCredentials() 0 16 3
A getFormUrl() 0 13 2
A getOptions() 0 4 1
A setOptions() 0 5 1
A getSignature() 0 9 2
B getFormInputs() 0 26 3
A getFormInputsAsHtml() 0 8 2
A generateScope() 0 11 1
A generatePolicy() 0 19 1
A generateSignature() 0 18 2
A keyHash() 0 4 1
A populateTime() 0 6 2
A mbToBytes() 0 7 2
A getShortDateFormat() 0 4 1
A getFullDateFormat() 0 4 1
A getExpirationDate() 0 5 1
1
<?php
2
3
namespace DirectUpload;
4
5
/**
6
 * Class Signature
7
 *
8
 * Build an AWS Signature, ready for direct upload.
9
 *
10
 * @package EddTurtle\DirectUpload
11
 */
12
class Signature
13
{
14
15
    CONST ALGORITHM = "AWS4-HMAC-SHA256";
16
    CONST SERVICE = "s3";
17
    CONST REQUEST_TYPE = "aws4_request";
18
19
    /**
20
     * Default options, these can be overwritten within the constructor.
21
     *
22
     * @var array
23
     */
24
    protected $options = [
25
        
26
        // If the upload is a success, the http code we get back.
27
        'success_status' => '201',
28
29
        // If the file should be private/public-read/public-write.
30
        // This is file specific, not bucket. More info: http://amzn.to/1SSOgwO
31
        'acl' => 'private',
32
33
        // The file's name, can be set with JS by changing the input[name="key"]
34
        // ${filename} will just mean the filename of the file being uploaded.
35
        'default_filename' => '${filename}',
36
37
        // The maximum file size of an upload in MB.
38
        'max_file_size' => '500'
39
        
40
    ];
41
42
    private $key;
43
    private $secret;
44
45
    private $bucket;
46
    private $region;
47
48
    private $time = null;
49
50
    private $credentials = null;
51
    private $base64Policy = null;
52
    private $signature = null;
53
54
    /**
55
     * Signature constructor.
56
     *
57
     * @param string $key     the AWS API Key to use.
58
     * @param string $secret  the AWS API Secret to use.
59
     * @param string $bucket  the bucket to upload the file into.
60
     * @param string $region  the s3 region this bucket is within. More info: http://amzn.to/1FtPG6r
61
     * @param array  $options any additional options, like acl and success status.
62
     */
63
    public function __construct($key, $secret, $bucket, $region = "us-east-1", $options = [])
64
    {
65
        $this->setAwsCredentials($key, $secret);
66
        $this->populateTime();
67
68
        $this->bucket = $bucket;
69
        $this->region = new Region($region);
70
71
        $this->setOptions($options);
72
    }
73
74
    /**
75
     * Set the AWS Credentials
76
     *
77
     * @param string $key    the AWS API Key to use.
78
     * @param string $secret the AWS API Secret to use.
79
     */
80
    public function setAwsCredentials($key, $secret)
81
    {
82
        // Key
83
        if (!empty($key)) {
84
            $this->key = $key;
85
        } else {
86
            throw new \InvalidArgumentException("Invalid AWS Key");
87
        }
88
89
        // Secret
90
        if (!empty($secret)) {
91
            $this->secret = $secret;
92
        } else {
93
            throw new \InvalidArgumentException("Invalid AWS Secret");
94
        }
95
    }
96
97
    /**
98
     * Build the form url for sending files, this will include the bucket and the region name.
99
     *
100
     * @return string the s3 bucket's url.
101
     */
102
    public function getFormUrl()
103
    {
104
        $region = $this->region->getName();
105
106
        // Only the us-east-1 region is exempt from needing the region in the url.
107
        if ($region !== "us-east-1") {
108
            $middle = "-" . $region;
109
        } else {
110
            $middle = "";
111
        }
112
113
        return "//" . $this->bucket . "." . self::SERVICE . $middle . ".amazonaws.com";
114
    }
115
116
    /**
117
     * Get all options.
118
     *
119
     * @return array
120
     */
121
    public function getOptions()
122
    {
123
        return $this->options;
124
    }
125
126
    /**
127
     * Set/overwrite any default options.
128
     *
129
     * @param $options
130
     */
131
    public function setOptions($options)
132
    {
133
        $this->options = $options + $this->options;
134
        $this->options['acl'] = new Acl($this->options['acl']);
135
    }
136
137
    /**
138
     * Get an AWS Signature V4 generated.
139
     *
140
     * @return string the signature.
141
     */
142
    public function getSignature()
143
    {
144
        if (is_null($this->signature)) {
145
            $this->generateScope();
146
            $this->generatePolicy();
147
            $this->generateSignature();
148
        }
149
        return $this->signature;
150
    }
151
152
    /**
153
     * Generate the necessary hidden inputs to go within the form.
154
     *
155
     * @param bool $addKey whether to add the 'key' input (filename), defaults to yes.
156
     *
157
     * @return array of the form inputs.
158
     */
159
    public function getFormInputs($addKey = true)
160
    {
161
        // Only generate the signature once
162
        if (is_null($this->signature)) {
163
            $this->getSignature();
164
        }
165
166
        $inputs = [
167
            'Content-Type' => '',
168
            'acl' => $this->options['acl']->getName(),
169
            'success_action_status' => $this->options['success_status'],
170
            'policy' => $this->base64Policy,
171
            'X-amz-credential' => $this->credentials,
172
            'X-amz-algorithm' => self::ALGORITHM,
173
            'X-amz-date' => $this->getFullDateFormat(),
174
            'X-amz-signature' => $this->signature
175
        ];
176
177
        if ($addKey) {
178
            // Note: The Key (filename) will need to be populated with JS on upload
179
            // if anything other than the filename is wanted.
180
            $inputs['key'] = $this->options['default_filename'];
181
        }
182
183
        return $inputs;
184
    }
185
186
    /**
187
     * Based on getFormInputs(), this will build up the html to go within the form.
188
     *
189
     * @return string html of hidden form inputs.
190
     */
191
    public function getFormInputsAsHtml()
192
    {
193
        $html = "";
194
        foreach ($this->getFormInputs() as $name => $value) {
195
            $html .= '<input type="hidden" name="' . $name . '" value="' . $value . '" />' . PHP_EOL;
196
        }
197
        return $html;
198
    }
199
200
201
    // Where the magic begins ;)
202
203
    /**
204
     * Step 1: Generate the Scope
205
     */
206
    protected function generateScope()
207
    {
208
        $scope = [
209
            $this->key,
210
            $this->getShortDateFormat(),
211
            $this->region->getName(),
212
            self::SERVICE,
213
            self::REQUEST_TYPE
214
        ];
215
        $this->credentials = implode('/', $scope);
216
    }
217
218
    /**
219
     * Step 2: Generate a Base64 Policy
220
     */
221
    protected function generatePolicy()
222
    {
223
        $maxSize = $this->mbToBytes($this->options['max_file_size']);
224
        $policy = [
225
            'expiration' => $this->getExpirationDate(),
226
            'conditions' => [
227
                ['bucket' => $this->bucket],
228
                ['acl' => $this->options['acl']->getName()],
229
                ['starts-with', '$key', ''],
230
                ['starts-with', '$Content-Type', ''],
231
                ['content-length-range', 0, $maxSize],
232
                ['success_action_status' => $this->options['success_status']],
233
                ['x-amz-credential' => $this->credentials],
234
                ['x-amz-algorithm' => self::ALGORITHM],
235
                ['x-amz-date' => $this->getFullDateFormat()]
236
            ]
237
        ];
238
        $this->base64Policy = base64_encode(json_encode($policy));
239
    }
240
241
    /**
242
     * Step 3: Generate and sign the Signature (v4)
243
     */
244
    protected function generateSignature()
245
    {
246
        $signatureData = [
247
            $this->getShortDateFormat(),
248
            $this->region->getName(),
249
            self::SERVICE,
250
            self::REQUEST_TYPE
251
        ];
252
253
        // Iterates over the data, hashing it each time.
254
        $signingKey = 'AWS4' . $this->secret;
255
        foreach ($signatureData as $data) {
256
            $signingKey = $this->keyHash($data, $signingKey);
257
        }
258
259
        // Finally, use the signing key to hash the policy.
260
        $this->signature = $this->keyHash($this->base64Policy, $signingKey, false);
261
    }
262
263
264
    // Helper functions
265
266
    private function keyHash($date, $key, $raw = true)
267
    {
268
        return hash_hmac('sha256', $date, $key, $raw);
269
    }
270
271
    private function populateTime()
272
    {
273
        if (is_null($this->time)) {
274
            $this->time = time();
275
        }
276
    }
277
278
    private function mbToBytes($mb)
0 ignored issues
show
Comprehensibility introduced by
Avoid variables with short names like $mb. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
279
    {
280
        if (is_numeric($mb)) {
281
            return $mb * pow(1024, 2);
282
        }
283
        return 0;
284
    }
285
286
287
    // Dates
288
289
    private function getShortDateFormat()
290
    {
291
        return gmdate("Ymd", $this->time);
292
    }
293
294
    private function getFullDateFormat()
295
    {
296
        return gmdate("Ymd\THis\Z", $this->time);
297
    }
298
299
    private function getExpirationDate()
300
    {
301
        // Note: using \DateTime::ISO8601 doesn't work :(
302
        return gmdate('Y-m-d\TG:i:s\Z', strtotime('+6 hours', $this->time));
303
    }
304
305
306
}