Completed
Push — master ( 81ea10...5035e2 )
by Bjørn
03:47
created

Wpc::createOptions()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 8
nc 1
nop 0
dl 0
loc 11
ccs 8
cts 8
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace WebPConvert\Convert\Converters;
4
5
use WebPConvert\Convert\Converters\AbstractConverter;
6
use WebPConvert\Convert\Converters\ConverterTraits\CloudConverterTrait;
7
use WebPConvert\Convert\Converters\ConverterTraits\CurlTrait;
8
use WebPConvert\Convert\Converters\ConverterTraits\EncodingAutoTrait;
9
use WebPConvert\Convert\Exceptions\ConversionFailedException;
10
use WebPConvert\Convert\Exceptions\ConversionFailed\ConverterNotOperationalException;
11
use WebPConvert\Convert\Exceptions\ConversionFailed\ConverterNotOperational\SystemRequirementsNotMetException;
12
use WebPConvert\Convert\Exceptions\ConversionFailed\ConverterNotOperational\InvalidApiKeyException;
13
use WebPConvert\Options\BooleanOption;
14
use WebPConvert\Options\IntegerOption;
15
use WebPConvert\Options\SensitiveStringOption;
16
17
/**
18
 * Convert images to webp using Wpc (a cloud converter based on WebP Convert).
19
 *
20
 * @package    WebPConvert
21
 * @author     Bjørn Rosell <[email protected]>
22
 * @since      Class available since Release 2.0.0
23
 */
24
class Wpc extends AbstractConverter
25
{
26
    use CloudConverterTrait;
27
    use CurlTrait;
28
    use EncodingAutoTrait;
29
30
    protected function getUnsupportedDefaultOptions()
31
    {
32
        return [];
33
    }
34
35 6
    protected function createOptions()
36
    {
37 6
        parent::createOptions();
38
39 6
        $this->options2->addOptions(
40 6
            new SensitiveStringOption('api-key', ''),   /* for communicating with wpc api v.1+ */
41
            new SensitiveStringOption('secret', ''),    /* for communicating with wpc api v.0 */
42 6
            new SensitiveStringOption('api-url', ''),
43 6
            new SensitiveStringOption('url', ''),       /* DO NOT USE. Only here to keep the protection */
44 6
            new IntegerOption('api-version', 1, 0, 2),
45 6
            new BooleanOption('crypt-api-key-in-transfer', false)  /* new in api v.1 */
46
        );
47 6
    }
48
49 6
    public function passOnEncodingAuto()
50
    {
51
        // We could make this configurable. But I guess passing it on is always to be preferred (except for
52
        // api = 1, but people ought to update their wpc anyway)
53 6
        return true;
54
    }
55
56 2
    private static function createRandomSaltForBlowfish()
57
    {
58 2
        $salt = '';
59 2
        $validCharsForSalt = array_merge(
60 2
            range('A', 'Z'),
61 2
            range('a', 'z'),
62 2
            range('0', '9'),
63 2
            ['.', '/']
64
        );
65
66 2
        for ($i=0; $i<22; $i++) {
67 2
            $salt .= $validCharsForSalt[array_rand($validCharsForSalt)];
68
        }
69 2
        return $salt;
70
    }
71
72
    /**
73
     * Get api key from options or environment variable
74
     *
75
     * @return string  api key or empty string if none is set
76
     */
77 6
    private function getApiKey()
78
    {
79 6
        if ($this->options['api-version'] == 0) {
80 1
            if (!empty($this->options['secret'])) {
81 1
                return $this->options['secret'];
82
            }
83 5
        } elseif ($this->options['api-version'] >= 1) {
84 5
            if (!empty($this->options['api-key'])) {
85 1
                return $this->options['api-key'];
86
            }
87
        }
88 5
        if (!empty(getenv('WPC_API_KEY'))) {
89 5
            return getenv('WPC_API_KEY');
90
        }
91
        return '';
92
    }
93
94
    /**
95
     * Get url from options or environment variable
96
     *
97
     * @return string  URL to WPC or empty string if none is set
98
     */
99 6
    private function getApiUrl()
100
    {
101 6
        if (!empty($this->options['api-url'])) {
102 4
            return $this->options['api-url'];
103
        }
104 2
        if (!empty(getenv('WPC_API_URL'))) {
105 2
            return getenv('WPC_API_URL');
106
        }
107
        return '';
108
    }
109
110
111
    /**
112
     * Check operationality of Wpc converter.
113
     *
114
     * @throws SystemRequirementsNotMetException  if system requirements are not met (curl)
115
     * @throws ConverterNotOperationalException   if key is missing or invalid, or quota has exceeded
116
     */
117 6
    public function checkOperationality()
118
    {
119
120 6
        $options = $this->options;
121
122 6
        $apiVersion = $options['api-version'];
123
124 6
        if ($this->getApiUrl() == '') {
125
            if (isset($this->options['url']) && ($this->options['url'] != '')) {
126
                throw new ConverterNotOperationalException(
127
                    'The "url" option has been renamed to "api-url" in webp-convert 2.0. ' .
128
                    'You must change the configuration accordingly.'
129
                );
130
            }
131
            throw new ConverterNotOperationalException(
132
                'Missing URL. You must install Webp Convert Cloud Service on a server, ' .
133
                'or the WebP Express plugin for Wordpress - and supply the url.'
134
            );
135
        }
136
137 6
        if ($apiVersion == 0) {
138 1
            if (!empty($this->getApiKey())) {
139
                // if secret is set, we need md5() and md5_file() functions
140 1
                if (!function_exists('md5')) {
141
                    throw new ConverterNotOperationalException(
142
                        'A secret has been set, which requires us to create a md5 hash from the secret and the file ' .
143
                        'contents. ' .
144
                        'But the required md5() PHP function is not available.'
145
                    );
146
                }
147 1
                if (!function_exists('md5_file')) {
148
                    throw new ConverterNotOperationalException(
149
                        'A secret has been set, which requires us to create a md5 hash from the secret and the file ' .
150 1
                        'contents. But the required md5_file() PHP function is not available.'
151
                    );
152
                }
153
            }
154 5
        } elseif ($apiVersion >= 1) {
155 5
            if ($options['crypt-api-key-in-transfer']) {
156 2
                if (!function_exists('crypt')) {
157
                    throw new ConverterNotOperationalException(
158
                        'Configured to crypt the api-key, but crypt() function is not available.'
159
                    );
160
                }
161
162 2
                if (!defined('CRYPT_BLOWFISH')) {
163
                    throw new ConverterNotOperationalException(
164
                        'Configured to crypt the api-key. ' .
165
                        'That requires Blowfish encryption, which is not available on your current setup.'
166
                    );
167
                }
168
            }
169
        }
170
171
        // Check for curl requirements
172 6
        $this->checkOperationalityForCurlTrait();
173 6
    }
174
175
    /*
176
    public function checkConvertability()
177
    {
178
        // check upload limits
179
        $this->checkConvertabilityCloudConverterTrait();
180
181
        // TODO: some from below can be moved up here
182
    }
183
    */
184
185 6
    private function createOptionsToSend()
186
    {
187 6
        $optionsToSend = $this->options;
188
189 6
        if ($this->isQualityDetectionRequiredButFailing()) {
190
            // quality was set to "auto", but we could not meassure the quality of the jpeg locally
191
            // Ask the cloud service to do it, rather than using what we came up with.
192
            $optionsToSend['quality'] = 'auto';
193
        } else {
194 6
            $optionsToSend['quality'] = $this->getCalculatedQuality();
195
        }
196
197
        // The following are unset for security reasons.
198 6
        unset($optionsToSend['converters']);
199 6
        unset($optionsToSend['secret']);
200 6
        unset($optionsToSend['api-key']);
201 6
        unset($optionsToSend['api-url']);
202
203 6
        $apiVersion = $optionsToSend['api-version'];
204
205 6
        if ($apiVersion == 1) {
206
            // Lossless can be "auto" in api 2, but in api 1 "auto" is not supported
207
            //unset($optionsToSend['lossless']);
208 1
        } elseif ($apiVersion == 2) {
209
            //unset($optionsToSend['png']);
210
            //unset($optionsToSend['jpeg']);
211
212
            // The following are unset for security reasons.
213
            unset($optionsToSend['cwebp-command-line-options']);
214
            unset($optionsToSend['command-line-options']);
215
        }
216
217 6
        return $optionsToSend;
218
    }
219
220 6
    private function createPostData()
221
    {
222 6
        $options = $this->options;
223
224
        $postData = [
225 6
            'file' => curl_file_create($this->source),
226 6
            'options' => json_encode($this->createOptionsToSend()),
227 6
            'servername' => (isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : '')
228
        ];
229
230 6
        $apiVersion = $options['api-version'];
231
232 6
        $apiKey = $this->getApiKey();
233
234 6
        if ($apiVersion == 0) {
235 1
            $postData['hash'] = md5(md5_file($this->source) . $apiKey);
236 5
        } elseif ($apiVersion == 1) {
237
            //$this->logLn('api key: ' . $apiKey);
238
239 5
            if ($options['crypt-api-key-in-transfer']) {
240 2
                $salt = self::createRandomSaltForBlowfish();
241 2
                $postData['salt'] = $salt;
242
243
                // Strip off the first 28 characters (the first 6 are always "$2y$10$". The next 22 is the salt)
244 2
                $postData['api-key-crypted'] = substr(crypt($apiKey, '$2y$10$' . $salt . '$'), 28);
245
            } else {
246 3
                $postData['api-key'] = $apiKey;
247
            }
248
        }
249 6
        return $postData;
250
    }
251
252 6
    protected function doActualConvert()
253
    {
254 6
        $ch = self::initCurl();
255
256
        //$this->logLn('api url: ' . $this->getApiUrl());
257
258 6
        curl_setopt_array($ch, [
259 6
            CURLOPT_URL => $this->getApiUrl(),
260 6
            CURLOPT_POST => 1,
261 6
            CURLOPT_POSTFIELDS => $this->createPostData(),
262 6
            CURLOPT_BINARYTRANSFER => true,
263 6
            CURLOPT_RETURNTRANSFER => true,
264 6
            CURLOPT_HEADER => false,
265 6
            CURLOPT_SSL_VERIFYPEER => false
266
        ]);
267
268 6
        $response = curl_exec($ch);
269 6
        if (curl_errno($ch)) {
270 1
            $this->logLn('Curl error: ', 'bold');
271 1
            $this->logLn(curl_error($ch));
272 1
            throw new ConverterNotOperationalException('Curl error:');
273
        }
274
275
        // Check if we got a 404
276 5
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
277 5
        if ($httpCode == 404) {
278 1
            curl_close($ch);
279 1
            throw new ConversionFailedException(
280 1
                'WPC was not found at the specified URL - we got a 404 response.'
281
            );
282
        }
283
284
        // Check for empty response
285 4
        if (empty($response)) {
286
            throw new ConversionFailedException(
287
                'Error: Unexpected result. We got nothing back. ' .
288
                    'HTTP CODE: ' . $httpCode . '. ' .
289
                    'Content type:' . curl_getinfo($ch, CURLINFO_CONTENT_TYPE)
290
            );
291
        };
292
293
        // The WPC cloud service either returns an image or an error message
294
        // Images has application/octet-stream.
295
        // Verify that we got an image back.
296 4
        if (curl_getinfo($ch, CURLINFO_CONTENT_TYPE) != 'application/octet-stream') {
297 2
            curl_close($ch);
298
299 2
            if (substr($response, 0, 1) == '{') {
300 1
                $responseObj = json_decode($response, true);
301 1
                if (isset($responseObj['errorCode'])) {
302 1
                    switch ($responseObj['errorCode']) {
303 1
                        case 0:
304
                            throw new ConverterNotOperationalException(
305
                                'There are problems with the server setup: "' .
306
                                $responseObj['errorMessage'] . '"'
307
                            );
308 1
                        case 1:
309 1
                            throw new InvalidApiKeyException(
310 1
                                'Access denied. ' . $responseObj['errorMessage']
311
                            );
312
                        default:
313
                            throw new ConversionFailedException(
314
                                'Conversion failed: "' . $responseObj['errorMessage'] . '"'
315
                            );
316
                    }
317
                }
318
            }
319
320
            // WPC 0.1 returns 'failed![error messag]' when conversion fails. Handle that.
321 1
            if (substr($response, 0, 7) == 'failed!') {
322
                throw new ConversionFailedException(
323
                    'WPC failed converting image: "' . substr($response, 7) . '"'
324
                );
325
            }
326
327 1
            $this->logLn('Bummer, we did not receive an image');
328 1
            $this->log('What we received starts with: "');
329 1
            $this->logLn(
330 1
                str_replace("\r", '', str_replace("\n", '', htmlentities(substr($response, 0, 400)))) . '..."'
331
            );
332
333 1
            throw new ConversionFailedException('Unexpected result. We did not receive an image but something else.');
334
            //throw new ConverterNotOperationalException($response);
335
        }
336
337 2
        $success = file_put_contents($this->destination, $response);
338 2
        curl_close($ch);
339
340 2
        if (!$success) {
341
            throw new ConversionFailedException('Error saving file. Check file permissions');
342
        }
343 2
    }
344
}
345