Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
57.02% |
69 / 121 |
|
18.18% |
2 / 11 |
CRAP | |
0.00% |
0 / 1 |
Wpc | |
57.02% |
69 / 121 |
|
18.18% |
2 / 11 |
230.87 | |
0.00% |
0 / 1 |
getUnsupportedDefaultOptions | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getUniqueOptions | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
supportsLossless | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
passOnEncodingAuto | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
createRandomSaltForBlowfish | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
6 | |||
getApiKey | |
54.55% |
6 / 11 |
|
0.00% |
0 / 1 |
11.60 | |||
getApiUrl | |
71.43% |
5 / 7 |
|
0.00% |
0 / 1 |
4.37 | |||
checkOperationality | |
44.44% |
8 / 18 |
|
0.00% |
0 / 1 |
31.75 | |||
createOptionsToSend | |
92.86% |
13 / 14 |
|
0.00% |
0 / 1 |
4.01 | |||
createPostData | |
73.33% |
11 / 15 |
|
0.00% |
0 / 1 |
4.30 | |||
doActualConvert | |
54.76% |
23 / 42 |
|
0.00% |
0 / 1 |
25.33 |
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 | use WebPConvert\Options\OptionFactory; |
17 | |
18 | /** |
19 | * Convert images to webp using Wpc (a cloud converter based on WebP Convert). |
20 | * |
21 | * @package WebPConvert |
22 | * @author Bjørn Rosell <it@rosell.dk> |
23 | * @since Class available since Release 2.0.0 |
24 | */ |
25 | class Wpc extends AbstractConverter |
26 | { |
27 | use CloudConverterTrait; |
28 | use CurlTrait; |
29 | use EncodingAutoTrait; |
30 | |
31 | protected function getUnsupportedDefaultOptions() |
32 | { |
33 | return []; |
34 | } |
35 | |
36 | public function getUniqueOptions($imageType) |
37 | { |
38 | return OptionFactory::createOptions([ |
39 | ['api-key', 'string', [ |
40 | 'title' => 'API key', |
41 | 'description' => 'The API key is set up on the remote. Copy that.', |
42 | 'default' => '', |
43 | 'sensitive' => true, |
44 | 'ui' => [ |
45 | 'component' => 'password', |
46 | 'advanced' => false, |
47 | 'display' => "option('wpc-api-version') != 0" |
48 | ] |
49 | ]], |
50 | ['secret', 'string', [ |
51 | 'title' => 'Secret', |
52 | 'description' => '', |
53 | 'default' => '', |
54 | 'sensitive' => true, |
55 | 'ui' => [ |
56 | 'component' => 'password', |
57 | 'advanced' => false, |
58 | 'display' => "option('wpc-api-version') == 0" |
59 | ] |
60 | ]], |
61 | ['api-url', 'string', [ |
62 | 'title' => 'API url', |
63 | 'description' => 'The endpoint of the web service. Copy it from the remote setup', |
64 | 'default' => '', |
65 | 'sensitive' => true, |
66 | 'ui' => [ |
67 | 'component' => 'password', |
68 | 'advanced' => false, |
69 | ] |
70 | ]], |
71 | ['api-version', 'int', [ |
72 | 'title' => 'API version', |
73 | 'description' => |
74 | 'Refers to the major version of Wpc. ' . |
75 | 'It is probably 2, as it is a long time since 2.0 was released', |
76 | 'default' => 2, |
77 | 'minimum' => 0, |
78 | 'maximum' => 2, |
79 | 'ui' => [ |
80 | 'component' => 'select', |
81 | 'advanced' => false, |
82 | 'options' => [0, 1, 2], |
83 | ] |
84 | ]], |
85 | ['crypt-api-key-in-transfer', 'boolean', [ |
86 | 'title' => 'Crypt API key in transfer', |
87 | 'description' => |
88 | 'If checked, the api key will be crypted in requests. ' . |
89 | 'Crypting the api-key protects it from being stolen during transfer', |
90 | 'default' => false, |
91 | 'ui' => [ |
92 | 'component' => 'checkbox', |
93 | 'advanced' => true, |
94 | 'display' => "option('wpc-api-version') >= 1" |
95 | ] |
96 | ]], |
97 | ]); |
98 | |
99 | /*return [ |
100 | new SensitiveStringOption('api-key', ''), |
101 | new SensitiveStringOption('secret', ''), |
102 | new SensitiveStringOption('api-url', ''), |
103 | new SensitiveStringOption('url', ''), // DO NOT USE. Only here to keep the protection |
104 | new IntegerOption('api-version', 2, 0, 2), |
105 | new BooleanOption('crypt-api-key-in-transfer', false) // new in api v.1 |
106 | ];*/ |
107 | } |
108 | |
109 | public function supportsLossless() |
110 | { |
111 | return ($this->options['api-version'] >= 2); |
112 | } |
113 | |
114 | public function passOnEncodingAuto() |
115 | { |
116 | // We could make this configurable. But I guess passing it on is always to be preferred |
117 | // for api >= 2. |
118 | return ($this->options['api-version'] >= 2); |
119 | } |
120 | |
121 | private static function createRandomSaltForBlowfish() |
122 | { |
123 | $salt = ''; |
124 | $validCharsForSalt = array_merge( |
125 | range('A', 'Z'), |
126 | range('a', 'z'), |
127 | range('0', '9'), |
128 | ['.', '/'] |
129 | ); |
130 | |
131 | for ($i = 0; $i < 22; $i++) { |
132 | $salt .= $validCharsForSalt[array_rand($validCharsForSalt)]; |
133 | } |
134 | return $salt; |
135 | } |
136 | |
137 | /** |
138 | * Get api key from options or environment variable |
139 | * |
140 | * @return string api key or empty string if none is set |
141 | */ |
142 | private function getApiKey() |
143 | { |
144 | if ($this->options['api-version'] == 0) { |
145 | if (!empty($this->options['secret'])) { |
146 | return $this->options['secret']; |
147 | } |
148 | } elseif ($this->options['api-version'] >= 1) { |
149 | if (!empty($this->options['api-key'])) { |
150 | return $this->options['api-key']; |
151 | } |
152 | } |
153 | if (defined('WEBPCONVERT_WPC_API_KEY')) { |
154 | return constant('WEBPCONVERT_WPC_API_KEY'); |
155 | } |
156 | if (!empty(getenv('WEBPCONVERT_WPC_API_KEY'))) { |
157 | return getenv('WEBPCONVERT_WPC_API_KEY'); |
158 | } |
159 | return ''; |
160 | } |
161 | |
162 | /** |
163 | * Get url from options or environment variable |
164 | * |
165 | * @return string URL to WPC or empty string if none is set |
166 | */ |
167 | private function getApiUrl() |
168 | { |
169 | if (!empty($this->options['api-url'])) { |
170 | return $this->options['api-url']; |
171 | } |
172 | if (defined('WEBPCONVERT_WPC_API_URL')) { |
173 | return constant('WEBPCONVERT_WPC_API_URL'); |
174 | } |
175 | if (!empty(getenv('WEBPCONVERT_WPC_API_URL'))) { |
176 | return getenv('WEBPCONVERT_WPC_API_URL'); |
177 | } |
178 | return ''; |
179 | } |
180 | |
181 | |
182 | /** |
183 | * Check operationality of Wpc converter. |
184 | * |
185 | * @throws SystemRequirementsNotMetException if system requirements are not met (curl) |
186 | * @throws ConverterNotOperationalException if key is missing or invalid, or quota has exceeded |
187 | */ |
188 | public function checkOperationality() |
189 | { |
190 | |
191 | $options = $this->options; |
192 | |
193 | $apiVersion = $options['api-version']; |
194 | |
195 | if ($this->getApiUrl() == '') { |
196 | if (isset($this->options['url']) && ($this->options['url'] != '')) { |
197 | throw new ConverterNotOperationalException( |
198 | 'The "url" option has been renamed to "api-url" in webp-convert 2.0. ' . |
199 | 'You must change the configuration accordingly.' |
200 | ); |
201 | } |
202 | throw new ConverterNotOperationalException( |
203 | 'Missing URL. You must install Webp Convert Cloud Service on a server, ' . |
204 | 'or the WebP Express plugin for Wordpress - and supply the url.' |
205 | ); |
206 | } |
207 | |
208 | if ($apiVersion == 0) { |
209 | if (!empty($this->getApiKey())) { |
210 | // if secret is set, we need md5() and md5_file() functions |
211 | if (!function_exists('md5')) { |
212 | throw new ConverterNotOperationalException( |
213 | 'A secret has been set, which requires us to create a md5 hash from the secret and the file ' . |
214 | 'contents. ' . |
215 | 'But the required md5() PHP function is not available.' |
216 | ); |
217 | } |
218 | if (!function_exists('md5_file')) { |
219 | throw new ConverterNotOperationalException( |
220 | 'A secret has been set, which requires us to create a md5 hash from the secret and the file ' . |
221 | 'contents. But the required md5_file() PHP function is not available.' |
222 | ); |
223 | } |
224 | } |
225 | } else { |
226 | if ($options['crypt-api-key-in-transfer']) { |
227 | if (!function_exists('crypt')) { |
228 | throw new ConverterNotOperationalException( |
229 | 'Configured to crypt the api-key, but crypt() function is not available.' |
230 | ); |
231 | } |
232 | |
233 | if (!defined('CRYPT_BLOWFISH')) { |
234 | throw new ConverterNotOperationalException( |
235 | 'Configured to crypt the api-key. ' . |
236 | 'That requires Blowfish encryption, which is not available on your current setup.' |
237 | ); |
238 | } |
239 | } |
240 | } |
241 | |
242 | // Check for curl requirements |
243 | $this->checkOperationalityForCurlTrait(); |
244 | } |
245 | |
246 | /* |
247 | public function checkConvertability() |
248 | { |
249 | // check upload limits |
250 | $this->checkConvertabilityCloudConverterTrait(); |
251 | |
252 | // TODO: some from below can be moved up here |
253 | } |
254 | */ |
255 | |
256 | private function createOptionsToSend() |
257 | { |
258 | $optionsToSend = $this->options; |
259 | |
260 | if ($this->isQualityDetectionRequiredButFailing()) { |
261 | // quality was set to "auto", but we could not meassure the quality of the jpeg locally |
262 | // Ask the cloud service to do it, rather than using what we came up with. |
263 | $optionsToSend['quality'] = 'auto'; |
264 | } else { |
265 | $optionsToSend['quality'] = $this->getCalculatedQuality(); |
266 | } |
267 | |
268 | // The following are unset for security reasons. |
269 | unset($optionsToSend['converters']); |
270 | unset($optionsToSend['secret']); |
271 | unset($optionsToSend['api-key']); |
272 | unset($optionsToSend['api-url']); |
273 | |
274 | $apiVersion = $optionsToSend['api-version']; |
275 | |
276 | if ($apiVersion == 1) { |
277 | // Lossless can be "auto" in api 2, but in api 1 "auto" is not supported |
278 | //unset($optionsToSend['lossless']); |
279 | } elseif ($apiVersion == 2) { |
280 | //unset($optionsToSend['png']); |
281 | //unset($optionsToSend['jpeg']); |
282 | |
283 | // The following are unset for security reasons. |
284 | unset($optionsToSend['cwebp-command-line-options']); |
285 | unset($optionsToSend['command-line-options']); |
286 | } |
287 | |
288 | return $optionsToSend; |
289 | } |
290 | |
291 | private function createPostData() |
292 | { |
293 | $options = $this->options; |
294 | |
295 | $postData = [ |
296 | 'file' => curl_file_create($this->source), |
297 | 'options' => json_encode($this->createOptionsToSend()), |
298 | 'servername' => (isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : '') |
299 | ]; |
300 | |
301 | $apiVersion = $options['api-version']; |
302 | |
303 | $apiKey = $this->getApiKey(); |
304 | |
305 | if ($apiVersion == 0) { |
306 | $postData['hash'] = md5(md5_file($this->source) . $apiKey); |
307 | } else { |
308 | //$this->logLn('api key: ' . $apiKey); |
309 | |
310 | if ($options['crypt-api-key-in-transfer']) { |
311 | $salt = self::createRandomSaltForBlowfish(); |
312 | $postData['salt'] = $salt; |
313 | |
314 | // Strip off the first 28 characters (the first 6 are always "$2y$10$". The next 22 is the salt) |
315 | $postData['api-key-crypted'] = substr(crypt($apiKey, '$2y$10$' . $salt . '$'), 28); |
316 | } else { |
317 | $postData['api-key'] = $apiKey; |
318 | } |
319 | } |
320 | return $postData; |
321 | } |
322 | |
323 | protected function doActualConvert() |
324 | { |
325 | $ch = self::initCurl(); |
326 | |
327 | //$this->logLn('api url: ' . $this->getApiUrl()); |
328 | |
329 | curl_setopt_array($ch, [ |
330 | CURLOPT_URL => $this->getApiUrl(), |
331 | CURLOPT_POST => 1, |
332 | CURLOPT_POSTFIELDS => $this->createPostData(), |
333 | CURLOPT_BINARYTRANSFER => true, |
334 | CURLOPT_RETURNTRANSFER => true, |
335 | CURLOPT_HEADER => false, |
336 | CURLOPT_SSL_VERIFYPEER => false |
337 | ]); |
338 | |
339 | $response = curl_exec($ch); |
340 | if (curl_errno($ch)) { |
341 | $this->logLn('Curl error: ', 'bold'); |
342 | $this->logLn(curl_error($ch)); |
343 | throw new ConverterNotOperationalException('Curl error:'); |
344 | } |
345 | |
346 | // Check if we got a 404 |
347 | $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); |
348 | if ($httpCode == 404) { |
349 | curl_close($ch); |
350 | throw new ConversionFailedException( |
351 | 'WPC was not found at the specified URL - we got a 404 response.' |
352 | ); |
353 | } |
354 | |
355 | // Check for empty response |
356 | if (empty($response)) { |
357 | throw new ConversionFailedException( |
358 | 'Error: Unexpected result. We got nothing back. ' . |
359 | 'HTTP CODE: ' . $httpCode . '. ' . |
360 | 'Content type:' . curl_getinfo($ch, CURLINFO_CONTENT_TYPE) |
361 | ); |
362 | }; |
363 | |
364 | // The WPC cloud service either returns an image or an error message |
365 | // Images has application/octet-stream. |
366 | // Verify that we got an image back. |
367 | if (curl_getinfo($ch, CURLINFO_CONTENT_TYPE) != 'application/octet-stream') { |
368 | curl_close($ch); |
369 | |
370 | if (substr($response, 0, 1) == '{') { |
371 | $responseObj = json_decode($response, true); |
372 | if (isset($responseObj['errorCode'])) { |
373 | switch ($responseObj['errorCode']) { |
374 | case 0: |
375 | throw new ConverterNotOperationalException( |
376 | 'There are problems with the server setup: "' . |
377 | $responseObj['errorMessage'] . '"' |
378 | ); |
379 | case 1: |
380 | throw new InvalidApiKeyException( |
381 | 'Access denied. ' . $responseObj['errorMessage'] |
382 | ); |
383 | default: |
384 | throw new ConversionFailedException( |
385 | 'Conversion failed: "' . $responseObj['errorMessage'] . '"' |
386 | ); |
387 | } |
388 | } |
389 | } |
390 | |
391 | // WPC 0.1 returns 'failed![error messag]' when conversion fails. Handle that. |
392 | if (substr($response, 0, 7) == 'failed!') { |
393 | throw new ConversionFailedException( |
394 | 'WPC failed converting image: "' . substr($response, 7) . '"' |
395 | ); |
396 | } |
397 | |
398 | $this->logLn('Bummer, we did not receive an image'); |
399 | $this->log('What we received starts with: "'); |
400 | $this->logLn( |
401 | str_replace("\r", '', str_replace("\n", '', htmlentities(substr($response, 0, 400)))) . '..."' |
402 | ); |
403 | |
404 | throw new ConversionFailedException('Unexpected result. We did not receive an image but something else.'); |
405 | //throw new ConverterNotOperationalException($response); |
406 | } |
407 | |
408 | $success = file_put_contents($this->destination, $response); |
409 | curl_close($ch); |
410 | |
411 | if (!$success) { |
412 | throw new ConversionFailedException('Error saving file. Check file permissions'); |
413 | } |
414 | } |
415 | } |