Passed
Push — master ( 74ce7c...3af96c )
by Bjørn
03:41
created

Wpc   A

Complexity

Total Complexity 42

Size/Duplication

Total Lines 281
Duplicated Lines 0 %

Test Coverage

Coverage 82.17%

Importance

Changes 0
Metric Value
eloc 135
dl 0
loc 281
ccs 106
cts 129
cp 0.8217
rs 9.0399
c 0
b 0
f 0
wmc 42

10 Methods

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