Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
55.67% covered (warning)
55.67%
108 / 194
30.00% covered (danger)
30.00%
3 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
Ewww
55.67% covered (warning)
55.67%
108 / 194
30.00% covered (danger)
30.00%
3 / 10
230.33
0.00% covered (danger)
0.00%
0 / 1
 getUniqueOptions
100.00% covered (success)
100.00%
26 / 26
100.00% covered (success)
100.00%
1 / 1
1
 getUnsupportedDefaultOptions
0.00% covered (danger)
0.00%
0 / 11
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
68.18% covered (warning)
68.18%
15 / 22
0.00% covered (danger)
0.00%
0 / 1
10.06
 doActualConvert
80.39% covered (warning)
80.39%
41 / 51
0.00% covered (danger)
0.00%
0 / 1
11.91
 keepSubscriptionAlive
0.00% covered (danger)
0.00%
0 / 39
0.00% covered (danger)
0.00%
0 / 1
72
 getKeyStatus
70.37% covered (warning)
70.37%
19 / 27
0.00% covered (danger)
0.00%
0 / 1
12.60
 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 / 9
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_RETURNTRANSFER => true,
181            CURLOPT_HEADER => false,
182            CURLOPT_SSL_VERIFYPEER => false
183            ]
184        );
185
186        $response = curl_exec($ch);
187
188        if (curl_errno($ch)) {
189            throw new ConversionFailedException(curl_error($ch));
190        }
191
192        // The API does not always return images.
193        // For example, it may return a message such as '{"error":"invalid","t":"exceeded"}
194        // Messages has a http content type of ie 'text/html; charset=UTF-8
195        // Images has application/octet-stream.
196        // So verify that we got an image back.
197        $contentType = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
198        if (($contentType != 'application/octet-stream') && ($contentType != 'image/webp')) {
199            //echo curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
200            self::closeCurl($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                . ". Content type: "
230                . curl_getinfo($ch, CURLINFO_CONTENT_TYPE)
231            );
232        }
233
234        // Not sure this can happen. So just in case
235        if ($response == '') {
236            throw new ConversionFailedException('ewww api did not return anything');
237        }
238
239        $success = file_put_contents($this->destination, $response);
240
241        if (!$success) {
242            throw new ConversionFailedException('Error saving file');
243        }
244    }
245
246    /**
247     *  Keep subscription alive by optimizing a jpeg
248     *  (ewww closes accounts after 6 months of inactivity - and webp conversions seems not to be counted? )
249     */
250    public static function keepSubscriptionAlive($source, $key)
251    {
252        try {
253            $ch = curl_init();
254        } catch (\Exception $e) {
255            return 'curl is not installed';
256        }
257        if ($ch === false) {
258            return 'curl could not be initialized';
259        }
260        curl_setopt_array(
261            $ch,
262            [
263            CURLOPT_URL => "https://optimize.exactlywww.com/v2/",
264            CURLOPT_HTTPHEADER => [
265                'User-Agent: WebPConvert',
266                'Accept: image/*'
267            ],
268            CURLOPT_POSTFIELDS => [
269                'api_key' => $key,
270                'webp' => '0',
271                'file' => curl_file_create($source),
272                'domain' => $_SERVER['HTTP_HOST'],
273                'quality' => 60,
274                'metadata' => 0
275            ],
276            CURLOPT_RETURNTRANSFER => true,
277            CURLOPT_HEADER => false,
278            CURLOPT_SSL_VERIFYPEER => false
279            ]
280        );
281
282        $response = curl_exec($ch);
283        if (curl_errno($ch)) {
284            return 'curl error' . curl_error($ch);
285        }
286        $contentType = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
287        if (($contentType != 'application/octet-stream') && ($contentType != 'image/webp')) {
288            self::closeCurl($ch);
289
290            /* May return this: {"error":"invalid","t":"exceeded"} */
291            $responseObj = json_decode($response);
292            if (isset($responseObj->error)) {
293                return 'The key is invalid';
294            }
295
296            return 'ewww api did not return an image. It could be that the key is invalid. Response: ' . $response;
297        }
298
299        // Not sure this can happen. So just in case
300        if ($response == '') {
301            return 'ewww api did not return anything';
302        }
303
304        return true;
305    }
306
307    /*
308        public static function blacklistKey($key)
309        {
310        }
311
312        public static function isKeyBlacklisted($key)
313        {
314        }*/
315
316    /**
317     *  Return "great", "exceeded" or "invalid"
318     */
319    public static function getKeyStatus($key)
320    {
321        $ch = self::initCurl();
322
323        curl_setopt($ch, CURLOPT_URL, "https://optimize.exactlywww.com/verify/");
324        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
325        curl_setopt($ch, CURLOPT_POSTFIELDS, [
326            'api_key' => $key
327        ]);
328
329        curl_setopt($ch, CURLOPT_USERAGENT, 'WebPConvert');
330
331        $response = curl_exec($ch);
332        // echo $response;
333        if (curl_errno($ch)) {
334            throw new \Exception(curl_error($ch));
335        }
336        self::closeCurl($ch);
337
338        // Possible responses:
339        // “great” = verification successful
340        // “exceeded” = indicates a valid key with no remaining image credits.
341        // an empty response indicates that the key is not valid
342
343        if ($response == '') {
344            return 'invalid';
345        }
346        $responseObj = json_decode($response);
347        if (isset($responseObj->error)) {
348            if (($responseObj->error == 'invalid') || ($responseObj->error == 'bye invalid')) {
349                return 'invalid';
350            } else {
351                if ($responseObj->error == 'bye invalid') {
352                    return 'invalid';
353                } else {
354                    throw new \Exception('Ewww returned unexpected error: ' . $response);
355                }
356            }
357        }
358        if (!isset($responseObj->status)) {
359            throw new \Exception('Ewww returned unexpected response to verify request: ' . $response);
360        }
361        switch ($responseObj->status) {
362            case 'great':
363            case 'exceeded':
364                return $responseObj->status;
365        }
366        throw new \Exception('Ewww returned unexpected status to verify request: "' . $responseObj->status . '"');
367    }
368
369    public static function isWorkingKey($key)
370    {
371        return (self::getKeyStatus($key) == 'great');
372    }
373
374    public static function isValidKey($key)
375    {
376        return (self::getKeyStatus($key) != 'invalid');
377    }
378
379    public static function getQuota($key)
380    {
381        $ch = self::initCurl();
382
383        curl_setopt($ch, CURLOPT_URL, "https://optimize.exactlywww.com/quota/");
384        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
385        curl_setopt($ch, CURLOPT_POSTFIELDS, [
386            'api_key' => $key
387        ]);
388        curl_setopt($ch, CURLOPT_USERAGENT, 'WebPConvert');
389
390        $response = curl_exec($ch);
391        return $response; // ie -830 23. Seems to return empty for invalid keys
392        // or empty
393        //echo $response;
394    }
395}