Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
58.77% |
67 / 114 |
|
30.00% |
3 / 10 |
CRAP | |
0.00% |
0 / 1 |
Ewww | |
58.77% |
67 / 114 |
|
30.00% |
3 / 10 |
172.57 | |
0.00% |
0 / 1 |
getUniqueOptions | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getUnsupportedDefaultOptions | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getKey | |
71.43% |
5 / 7 |
|
0.00% |
0 / 1 |
4.37 | |||
checkOperationality | |
76.47% |
13 / 17 |
|
0.00% |
0 / 1 |
8.83 | |||
doActualConvert | |
83.87% |
26 / 31 |
|
0.00% |
0 / 1 |
10.42 | |||
keepSubscriptionAlive | |
0.00% |
0 / 22 |
|
0.00% |
0 / 1 |
56 | |||
getKeyStatus | |
76.00% |
19 / 25 |
|
0.00% |
0 / 1 |
10.12 | |||
isWorkingKey | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isValidKey | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getQuota | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 |
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\Exceptions\ConversionFailedException; |
9 | use WebPConvert\Convert\Exceptions\ConversionFailed\ConverterNotOperationalException; |
10 | use WebPConvert\Convert\Exceptions\ConversionFailed\ConverterNotOperational\InvalidApiKeyException; |
11 | use WebPConvert\Convert\Exceptions\ConversionFailed\ConverterNotOperational\SystemRequirementsNotMetException; |
12 | use WebPConvert\Options\BooleanOption; |
13 | use WebPConvert\Options\SensitiveStringOption; |
14 | use WebPConvert\Options\OptionFactory; |
15 | |
16 | /** |
17 | * Convert images to webp using ewww cloud service. |
18 | * |
19 | * @package WebPConvert |
20 | * @author Bjørn Rosell <it@rosell.dk> |
21 | * @since Class available since Release 2.0.0 |
22 | */ |
23 | class Ewww extends AbstractConverter |
24 | { |
25 | use CloudConverterTrait; |
26 | use CurlTrait; |
27 | |
28 | /** @var array|null Array of invalid or exceeded api keys discovered during conversions (during the request) */ |
29 | public static $nonFunctionalApiKeysDiscoveredDuringConversion; |
30 | |
31 | public function getUniqueOptions($imageType) |
32 | { |
33 | return OptionFactory::createOptions([ |
34 | ['api-key', 'string', [ |
35 | 'title' => 'Ewww API key', |
36 | 'description' => 'ewww API key. ' . |
37 | 'If you choose "auto", webp-convert will ' . |
38 | 'convert to both lossy and lossless and pick the smallest result', |
39 | 'default' => '', |
40 | 'sensitive' => true, |
41 | 'ui' => [ |
42 | 'component' => 'password', |
43 | ] |
44 | ]], |
45 | ['check-key-status-before-converting', 'boolean', [ |
46 | 'title' => 'Check key status before converting', |
47 | 'description' => |
48 | 'If enabled, the api key will be validated (relative inexpensive) before trying ' . |
49 | 'to convert. For automatic conversions, you should enable it. Otherwise you run the ' . |
50 | 'risk that the same files will be uploaded to ewww cloud service over and over again, ' . |
51 | 'in case the key has expired. For manually triggered conversions, you can safely disable ' . |
52 | 'the option.', |
53 | 'default' => true, |
54 | 'ui' => [ |
55 | 'component' => 'checkbox', |
56 | ] |
57 | ]], |
58 | ]); |
59 | } |
60 | |
61 | protected function getUnsupportedDefaultOptions() |
62 | { |
63 | return [ |
64 | 'alpha-quality', |
65 | 'auto-filter', |
66 | 'encoding', |
67 | 'low-memory', |
68 | 'method', |
69 | 'near-lossless', |
70 | 'preset', |
71 | 'sharp-yuv', |
72 | 'size-in-percentage', |
73 | ]; |
74 | } |
75 | |
76 | /** |
77 | * Get api key from options or environment variable |
78 | * |
79 | * @return string|false api key or false if none is set |
80 | */ |
81 | private function getKey() |
82 | { |
83 | if (!empty($this->options['api-key'])) { |
84 | return $this->options['api-key']; |
85 | } |
86 | if (defined('WEBPCONVERT_EWWW_API_KEY')) { |
87 | return constant('WEBPCONVERT_EWWW_API_KEY'); |
88 | } |
89 | if (!empty(getenv('WEBPCONVERT_EWWW_API_KEY'))) { |
90 | return getenv('WEBPCONVERT_EWWW_API_KEY'); |
91 | } |
92 | return false; |
93 | } |
94 | |
95 | |
96 | /** |
97 | * Check operationality of Ewww converter. |
98 | * |
99 | * @throws SystemRequirementsNotMetException if system requirements are not met (curl) |
100 | * @throws ConverterNotOperationalException if key is missing or invalid, or quota has exceeded |
101 | */ |
102 | public function checkOperationality() |
103 | { |
104 | |
105 | $apiKey = $this->getKey(); |
106 | |
107 | if ($apiKey === false) { |
108 | if (isset($this->options['key'])) { |
109 | throw new InvalidApiKeyException( |
110 | 'The "key" option has been renamed to "api-key" in webp-convert 2.0. ' . |
111 | 'You must change the configuration accordingly.' |
112 | ); |
113 | } |
114 | |
115 | throw new InvalidApiKeyException('Missing API key.'); |
116 | } |
117 | |
118 | if (strlen($apiKey) < 20) { |
119 | throw new InvalidApiKeyException( |
120 | 'Api key is invalid. Api keys are supposed to be 32 characters long - ' . |
121 | 'the provided api key is much shorter' |
122 | ); |
123 | } |
124 | |
125 | // Check for curl requirements |
126 | $this->checkOperationalityForCurlTrait(); |
127 | |
128 | if ($this->options['check-key-status-before-converting']) { |
129 | $keyStatus = self::getKeyStatus($apiKey); |
130 | switch ($keyStatus) { |
131 | case 'great': |
132 | break; |
133 | case 'exceeded': |
134 | throw new ConverterNotOperationalException('Quota has exceeded'); |
135 | //break; |
136 | case 'invalid': |
137 | throw new InvalidApiKeyException('Api key is invalid'); |
138 | //break; |
139 | } |
140 | } |
141 | } |
142 | |
143 | /* |
144 | public function checkConvertability() |
145 | { |
146 | // check upload limits |
147 | $this->checkConvertabilityCloudConverterTrait(); |
148 | } |
149 | */ |
150 | |
151 | // Although this method is public, do not call directly. |
152 | // You should rather call the static convert() function, defined in AbstractConverter, which |
153 | // takes care of preparing stuff before calling doConvert, and validating after. |
154 | protected function doActualConvert() |
155 | { |
156 | |
157 | $options = $this->options; |
158 | |
159 | $ch = self::initCurl(); |
160 | |
161 | //$this->logLn('api key:' . $this->getKey()); |
162 | |
163 | $postData = [ |
164 | 'api_key' => $this->getKey(), |
165 | 'webp' => '1', |
166 | 'file' => curl_file_create($this->source), |
167 | 'quality' => $this->getCalculatedQuality(), |
168 | 'metadata' => ($options['metadata'] == 'none' ? '0' : '1') |
169 | ]; |
170 | |
171 | curl_setopt_array( |
172 | $ch, |
173 | [ |
174 | CURLOPT_URL => "https://optimize.exactlywww.com/v2/", |
175 | CURLOPT_HTTPHEADER => [ |
176 | 'User-Agent: WebPConvert', |
177 | 'Accept: image/*' |
178 | ], |
179 | CURLOPT_POSTFIELDS => $postData, |
180 | CURLOPT_BINARYTRANSFER => true, |
181 | CURLOPT_RETURNTRANSFER => true, |
182 | CURLOPT_HEADER => false, |
183 | CURLOPT_SSL_VERIFYPEER => false |
184 | ] |
185 | ); |
186 | |
187 | $response = curl_exec($ch); |
188 | |
189 | if (curl_errno($ch)) { |
190 | throw new ConversionFailedException(curl_error($ch)); |
191 | } |
192 | |
193 | // The API does not always return images. |
194 | // For example, it may return a message such as '{"error":"invalid","t":"exceeded"} |
195 | // Messages has a http content type of ie 'text/html; charset=UTF-8 |
196 | // Images has application/octet-stream. |
197 | // So verify that we got an image back. |
198 | if (curl_getinfo($ch, CURLINFO_CONTENT_TYPE) != 'application/octet-stream') { |
199 | //echo curl_getinfo($ch, CURLINFO_CONTENT_TYPE); |
200 | curl_close($ch); |
201 | |
202 | /* |
203 | For bogus or expired key it returns: {"error":"invalid","t":"exceeded"} |
204 | For exceeded key it returns: {"error":"exceeded"} |
205 | */ |
206 | $responseObj = json_decode($response); |
207 | if (isset($responseObj->error)) { |
208 | $this->logLn('We received the following error response: ' . $responseObj->error); |
209 | $this->logLn('Complete response: ' . json_encode($responseObj)); |
210 | |
211 | // Store the invalid key in array so it can be received once the Stack is completed |
212 | // (even when stack succeeds) |
213 | if (!isset(self::$nonFunctionalApiKeysDiscoveredDuringConversion)) { |
214 | self::$nonFunctionalApiKeysDiscoveredDuringConversion = []; |
215 | } |
216 | if (!in_array($options['api-key'], self::$nonFunctionalApiKeysDiscoveredDuringConversion)) { |
217 | self::$nonFunctionalApiKeysDiscoveredDuringConversion[] = $options['api-key']; |
218 | } |
219 | if ($responseObj->error == "invalid") { |
220 | throw new InvalidApiKeyException('The api key is invalid (or expired)'); |
221 | } else { |
222 | throw new InvalidApiKeyException('The quota is exceeded for the api-key'); |
223 | } |
224 | } |
225 | |
226 | throw new ConversionFailedException( |
227 | 'ewww api did not return an image. It could be that the key is invalid. Response: ' |
228 | . $response |
229 | ); |
230 | } |
231 | |
232 | // Not sure this can happen. So just in case |
233 | if ($response == '') { |
234 | throw new ConversionFailedException('ewww api did not return anything'); |
235 | } |
236 | |
237 | $success = file_put_contents($this->destination, $response); |
238 | |
239 | if (!$success) { |
240 | throw new ConversionFailedException('Error saving file'); |
241 | } |
242 | } |
243 | |
244 | /** |
245 | * Keep subscription alive by optimizing a jpeg |
246 | * (ewww closes accounts after 6 months of inactivity - and webp conversions seems not to be counted? ) |
247 | */ |
248 | public static function keepSubscriptionAlive($source, $key) |
249 | { |
250 | try { |
251 | $ch = curl_init(); |
252 | } catch (\Exception $e) { |
253 | return 'curl is not installed'; |
254 | } |
255 | if ($ch === false) { |
256 | return 'curl could not be initialized'; |
257 | } |
258 | curl_setopt_array( |
259 | $ch, |
260 | [ |
261 | CURLOPT_URL => "https://optimize.exactlywww.com/v2/", |
262 | CURLOPT_HTTPHEADER => [ |
263 | 'User-Agent: WebPConvert', |
264 | 'Accept: image/*' |
265 | ], |
266 | CURLOPT_POSTFIELDS => [ |
267 | 'api_key' => $key, |
268 | 'webp' => '0', |
269 | 'file' => curl_file_create($source), |
270 | 'domain' => $_SERVER['HTTP_HOST'], |
271 | 'quality' => 60, |
272 | 'metadata' => 0 |
273 | ], |
274 | CURLOPT_BINARYTRANSFER => true, |
275 | CURLOPT_RETURNTRANSFER => true, |
276 | CURLOPT_HEADER => false, |
277 | CURLOPT_SSL_VERIFYPEER => false |
278 | ] |
279 | ); |
280 | |
281 | $response = curl_exec($ch); |
282 | if (curl_errno($ch)) { |
283 | return 'curl error' . curl_error($ch); |
284 | } |
285 | if (curl_getinfo($ch, CURLINFO_CONTENT_TYPE) != 'application/octet-stream') { |
286 | curl_close($ch); |
287 | |
288 | /* May return this: {"error":"invalid","t":"exceeded"} */ |
289 | $responseObj = json_decode($response); |
290 | if (isset($responseObj->error)) { |
291 | return 'The key is invalid'; |
292 | } |
293 | |
294 | return 'ewww api did not return an image. It could be that the key is invalid. Response: ' . $response; |
295 | } |
296 | |
297 | // Not sure this can happen. So just in case |
298 | if ($response == '') { |
299 | return 'ewww api did not return anything'; |
300 | } |
301 | |
302 | return true; |
303 | } |
304 | |
305 | /* |
306 | public static function blacklistKey($key) |
307 | { |
308 | } |
309 | |
310 | public static function isKeyBlacklisted($key) |
311 | { |
312 | }*/ |
313 | |
314 | /** |
315 | * Return "great", "exceeded" or "invalid" |
316 | */ |
317 | public static function getKeyStatus($key) |
318 | { |
319 | $ch = self::initCurl(); |
320 | |
321 | curl_setopt($ch, CURLOPT_URL, "https://optimize.exactlywww.com/verify/"); |
322 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); |
323 | curl_setopt($ch, CURLOPT_POSTFIELDS, [ |
324 | 'api_key' => $key |
325 | ]); |
326 | |
327 | curl_setopt($ch, CURLOPT_USERAGENT, 'WebPConvert'); |
328 | |
329 | $response = curl_exec($ch); |
330 | // echo $response; |
331 | if (curl_errno($ch)) { |
332 | throw new \Exception(curl_error($ch)); |
333 | } |
334 | curl_close($ch); |
335 | |
336 | // Possible responses: |
337 | // “great” = verification successful |
338 | // “exceeded” = indicates a valid key with no remaining image credits. |
339 | // an empty response indicates that the key is not valid |
340 | |
341 | if ($response == '') { |
342 | return 'invalid'; |
343 | } |
344 | $responseObj = json_decode($response); |
345 | if (isset($responseObj->error)) { |
346 | if ($responseObj->error == 'invalid') { |
347 | return 'invalid'; |
348 | } else { |
349 | if ($responseObj->error == 'bye invalid') { |
350 | return 'invalid'; |
351 | } else { |
352 | throw new \Exception('Ewww returned unexpected error: ' . $response); |
353 | } |
354 | } |
355 | } |
356 | if (!isset($responseObj->status)) { |
357 | throw new \Exception('Ewww returned unexpected response to verify request: ' . $response); |
358 | } |
359 | switch ($responseObj->status) { |
360 | case 'great': |
361 | case 'exceeded': |
362 | return $responseObj->status; |
363 | } |
364 | throw new \Exception('Ewww returned unexpected status to verify request: "' . $responseObj->status . '"'); |
365 | } |
366 | |
367 | public static function isWorkingKey($key) |
368 | { |
369 | return (self::getKeyStatus($key) == 'great'); |
370 | } |
371 | |
372 | public static function isValidKey($key) |
373 | { |
374 | return (self::getKeyStatus($key) != 'invalid'); |
375 | } |
376 | |
377 | public static function getQuota($key) |
378 | { |
379 | $ch = self::initCurl(); |
380 | |
381 | curl_setopt($ch, CURLOPT_URL, "https://optimize.exactlywww.com/quota/"); |
382 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); |
383 | curl_setopt($ch, CURLOPT_POSTFIELDS, [ |
384 | 'api_key' => $key |
385 | ]); |
386 | curl_setopt($ch, CURLOPT_USERAGENT, 'WebPConvert'); |
387 | |
388 | $response = curl_exec($ch); |
389 | return $response; // ie -830 23. Seems to return empty for invalid keys |
390 | // or empty |
391 | //echo $response; |
392 | } |
393 | } |