Passed
Push — master ( e8c17c...92b8a3 )
by Bjørn
02:37
created

Wpc   B

Complexity

Total Complexity 49

Size/Duplication

Total Lines 329
Duplicated Lines 0 %

Test Coverage

Coverage 80.38%

Importance

Changes 17
Bugs 2 Features 0
Metric Value
eloc 157
c 17
b 2
f 0
dl 0
loc 329
ccs 123
cts 153
cp 0.8038
rs 8.48
wmc 49

11 Methods

Rating   Name   Duplication   Size   Complexity  
A getUnsupportedDefaultOptions() 0 3 1
A createRandomSaltForBlowfish() 0 14 2
A getApiUrl() 0 12 4
B getApiKey() 0 18 7
A supportsLossless() 0 3 1
A createPostData() 0 30 5
A createOptions() 0 11 1
A passOnEncodingAuto() 0 5 1
A createOptionsToSend() 0 33 4
C checkOperationality() 0 56 12
C doActualConvert() 0 90 11

How to fix   Complexity   

Complex Class

Complex classes like Wpc often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Wpc, and based on these observations, apply Extract Interface, too.

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