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

Wpc::doActualConvert()   C

Complexity

Conditions 11
Paths 12

Size

Total Lines 90
Code Lines 52

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 40
CRAP Score 11.968

Importance

Changes 5
Bugs 1 Features 0
Metric Value
cc 11
eloc 52
c 5
b 1
f 0
nc 12
nop 0
dl 0
loc 90
ccs 40
cts 50
cp 0.8
crap 11.968
rs 6.9006

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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