Passed
Push — master ( 8298cf...50f59c )
by Bjørn
02:59
created

Wpc::supportsLossless()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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