Passed
Push — master ( 26c56b...7d441f )
by Bjørn
08:31
created

Wpc   A

Complexity

Total Complexity 41

Size/Duplication

Total Lines 276
Duplicated Lines 0 %

Test Coverage

Coverage 68.5%

Importance

Changes 0
Metric Value
eloc 135
dl 0
loc 276
ccs 87
cts 127
cp 0.685
rs 9.1199
c 0
b 0
f 0
wmc 41

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