Test Failed
Push — master ( 975525...307699 )
by Bjørn
02:39
created

Wpc   B

Complexity

Total Complexity 45

Size/Duplication

Total Lines 309
Duplicated Lines 0 %

Test Coverage

Coverage 78.1%

Importance

Changes 0
Metric Value
eloc 149
dl 0
loc 309
ccs 107
cts 137
cp 0.781
rs 8.8
c 0
b 0
f 0
wmc 45

10 Methods

Rating   Name   Duplication   Size   Complexity  
A getApiUrl() 0 9 3
A createPostData() 0 30 5
A createRandomSaltForBlowfish() 0 14 2
A createOptions() 0 10 1
A getApiKey() 0 15 6
A getUnsupportedDefaultOptions() 0 3 1
A passOnEncodingAuto() 0 4 1
A createOptionsToSend() 0 32 4
B checkOperationality() 0 56 11
B doActualConvert() 0 84 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 6
    use CurlTrait;
28
    use EncodingAutoTrait;
29
30 6
    protected function getUnsupportedDefaultOptions()
31
    {
32
        return [];
33 6
    }
34
35
    protected function createOptions()
36 6
    {
37
        parent::createOptions();
38
39
        $this->options2->addOptions(
40
            new SensitiveStringOption('api-key', ''),   /* new in api v.1 (renamed 'secret' to 'api-key') */
41
            new SensitiveStringOption('secret', ''),    /* only in api v.0 */
42
            new SensitiveStringOption('api-url', ''),
43
            new IntegerOption('api-version', 0, 0, 2),
44 2
            new BooleanOption('crypt-api-key-in-transfer', false),  /* new in api v.1 */
0 ignored issues
show
Bug introduced by
false of type false is incompatible with the type string expected by parameter $defaultValue of WebPConvert\Options\BooleanOption::__construct(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

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