Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
58.77% covered (warning)
58.77%
67 / 114
30.00% covered (danger)
30.00%
3 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
Ewww
58.77% covered (warning)
58.77%
67 / 114
30.00% covered (danger)
30.00%
3 / 10
172.57
0.00% covered (danger)
0.00%
0 / 1
 getUniqueOptions
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getUnsupportedDefaultOptions
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getKey
71.43% covered (warning)
71.43%
5 / 7
0.00% covered (danger)
0.00%
0 / 1
4.37
 checkOperationality
76.47% covered (warning)
76.47%
13 / 17
0.00% covered (danger)
0.00%
0 / 1
8.83
 doActualConvert
83.87% covered (warning)
83.87%
26 / 31
0.00% covered (danger)
0.00%
0 / 1
10.42
 keepSubscriptionAlive
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
56
 getKeyStatus
76.00% covered (warning)
76.00%
19 / 25
0.00% covered (danger)
0.00%
0 / 1
10.12
 isWorkingKey
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isValidKey
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getQuota
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace WebPConvert\Convert\Converters;
4
5use WebPConvert\Convert\Converters\AbstractConverter;
6use WebPConvert\Convert\Converters\ConverterTraits\CloudConverterTrait;
7use WebPConvert\Convert\Converters\ConverterTraits\CurlTrait;
8use WebPConvert\Convert\Exceptions\ConversionFailedException;
9use WebPConvert\Convert\Exceptions\ConversionFailed\ConverterNotOperationalException;
10use WebPConvert\Convert\Exceptions\ConversionFailed\ConverterNotOperational\InvalidApiKeyException;
11use WebPConvert\Convert\Exceptions\ConversionFailed\ConverterNotOperational\SystemRequirementsNotMetException;
12use WebPConvert\Options\BooleanOption;
13use WebPConvert\Options\SensitiveStringOption;
14use 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 */
23class 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}