Passed
Push — master ( c79c3e...36629a )
by Bjørn
06:12
created

Wpc::getApiUrl()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 5.667

Importance

Changes 0
Metric Value
cc 3
eloc 5
nc 3
nop 0
dl 0
loc 9
ccs 2
cts 6
cp 0.3333
crap 5.667
rs 10
c 0
b 0
f 0
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\AccessDeniedException;
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 4
23
    protected function getOptionDefinitionsExtra()
24
    {
25 4
        return [
26
            ['api-version', 'number', 0],                     /* Can currently be 0 or 1 */
27
            ['secret', 'string', '', true],    /* only in api v.0 */
28 4
            ['api-key', 'string', '', true],   /* new in api v.1 (renamed 'secret' to 'api-key') */
29 4
            ['url', 'string', '', true, true],
30 4
            ['crypt-api-key-in-transfer', 'boolean', false],  /* new in api v.1 */
31
        ];
32
    }
33
34
    private static function createRandomSaltForBlowfish()
35
    {
36
        $salt = '';
37
        $validCharsForSalt = array_merge(
38
            range('A', 'Z'),
39
            range('a', 'z'),
40
            range('0', '9'),
41
            ['.', '/']
42
        );
43
44
        for ($i=0; $i<22; $i++) {
45
            $salt .= $validCharsForSalt[array_rand($validCharsForSalt)];
46
        }
47
        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
        if ($this->options['api-version'] == 0) {
58 4
            if (!empty($this->options['secret'])) {
59
                return $this->options['secret'];
60 4
            }
61
        } elseif ($this->options['api-version'] == 1) {
62 4
            if (!empty($this->options['api-key'])) {
63
                return $this->options['api-key'];
64 4
            }
65 4
        }
66
        if (!empty(getenv('WPC_API_KEY'))) {
67 4
            return getenv('WPC_API_KEY');
68
        }
69
        return '';
70
    }
71
72
    /**
73
     * Get url from options or environment variable
74 4
     *
75
     * @return string  URL to WPC or empty string if none is set
76
     */
77
    private function getApiUrl()
78
    {
79
        if (!empty($this->options['url'])) {
80 4
            return $this->options['url'];
81 4
        }
82
        if (!empty(getenv('WPC_API_URL'))) {
83
            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
    public function checkOperationality()
96
    {
97
        // First check for curl requirements
98 4
        parent::checkOperationality();
99 1
100
        $options = $this->options;
101
102 1
        $apiVersion = $options['api-version'];
103
104 3
        if ($apiVersion == 0) {
105
            if (!empty($this->getApiKey())) {
106
                // if secret is set, we need md5() and md5_file() functions
107
                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 3
                        'contents. ' .
111
                        'But the required md5() PHP function is not available.'
112
                    );
113 3
                }
114
                if (!function_exists('md5_file')) {
115
                    throw new ConverterNotOperationalException(
116 3
                        'A secret has been set, which requires us to create a md5 hash from the secret and the file ' .
117
                        'contents. But the required md5_file() PHP function is not available.'
118 3
                    );
119
                }
120 3
            }
121
        } elseif ($apiVersion == 1) {
122 3
            if ($options['crypt-api-key-in-transfer']) {
123
                if (!function_exists('crypt')) {
124
                    throw new ConverterNotOperationalException(
125
                        'Configured to crypt the api-key, but crypt() function is not available.'
126
                    );
127 3
                }
128
129
                if (!defined('CRYPT_BLOWFISH')) {
130 3
                    throw new ConverterNotOperationalException(
131 3
                        'Configured to crypt the api-key. ' .
132
                        'That requires Blowfish encryption, which is not available on your current setup.'
133 3
                    );
134
                }
135
            }
136 3
        }
137
138 3
        if ($this->getApiUrl() == '') {
139
            throw new ConverterNotOperationalException(
140
                'Missing URL. You must install Webp Convert Cloud Service on a server, ' .
141 3
                'or the WebP Express plugin for Wordpress - and supply the url.'
142 3
            );
143 3
        }
144 3
    }
145
146 3
    /**
147
     * Check if specific file is convertable with current converter / converter settings.
148 3
     *
149 3
     */
150 3
    public function checkConvertability()
151
    {
152
        // First check for upload limits (abstract cloud converter)
153
        parent::checkConvertability();
154
155
        // TODO: some from below can be moved up here
156
    }
157
158
    private function createOptionsToSend()
159
    {
160
        $optionsToSend = $this->options;
161
162
        if ($this->isQualityDetectionRequiredButFailing()) {
163 3
            // 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 3
        } else {
167
            $optionsToSend['quality'] = $this->getCalculatedQuality();
168 3
        }
169
170 3
        unset($optionsToSend['converters']);
171 3
        unset($optionsToSend['secret']);
172 3
        unset($optionsToSend['api-key']);
173 3
        unset($optionsToSend['url']);
174 3
175 3
        return $optionsToSend;
176 3
    }
177 3
178 3
    private function createPostData()
179
    {
180 3
        $options = $this->options;
181 3
182 1
        $postData = [
183
            'file' => curl_file_create($this->source),
184
            'options' => json_encode($this->createOptionsToSend()),
185
            'servername' => (isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : '')
186 2
        ];
187 2
188
        $apiVersion = $options['api-version'];
189
190
        $apiKey = $this->getApiKey();
191
192
        if ($apiVersion == 0) {
193
            $postData['hash'] = md5(md5_file($this->source) . $apiKey);
194
        } 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
                $postData['salt'] = $salt;
200 2
201 2
                // 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 2
            } else {
204 2
                $postData['api-key'] = $apiKey;
205
            }
206
        }
207
        return $postData;
208
    }
209 2
210 2
    protected function doActualConvert()
211 2
    {
212 2
        $ch = self::initCurl();
213
214
        //$this->logLn('api url: ' . $this->getApiUrl());
215
216
        curl_setopt_array($ch, [
217
            CURLOPT_URL => $this->getApiUrl(),
218
            CURLOPT_POST => 1,
219
            CURLOPT_POSTFIELDS => $this->createPostData(),
220
            CURLOPT_BINARYTRANSFER => true,
221
            CURLOPT_RETURNTRANSFER => true,
222
            CURLOPT_HEADER => false,
223
            CURLOPT_SSL_VERIFYPEER => false
224
        ]);
225
226
        $response = curl_exec($ch);
227
        if (curl_errno($ch)) {
228
            throw new ConverterNotOperationalException('Curl error:' . curl_error($ch));
229
        }
230
231
        // Check if we got a 404
232
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
233
        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
        // The WPC cloud service either returns an image or an error message
241
        // Images has application/octet-stream.
242
        // Verify that we got an image back.
243
        if (curl_getinfo($ch, CURLINFO_CONTENT_TYPE) != 'application/octet-stream') {
244
            curl_close($ch);
245
246
            if (substr($response, 0, 1) == '{') {
247
                $responseObj = json_decode($response, true);
248
                if (isset($responseObj['errorCode'])) {
249
                    switch ($responseObj['errorCode']) {
250
                        case 0:
251
                            throw new ConverterNotOperationalException(
252
                                'There are problems with the server setup: "' .
253
                                $responseObj['errorMessage'] . '"'
254
                            );
255
                        case 1:
256
                            throw new AccessDeniedException(
257
                                'Access denied. ' . $responseObj['errorMessage']
258
                            );
259
                        default:
260
                            throw new ConversionFailedException(
261
                                'Conversion failed: "' . $responseObj['errorMessage'] . '"'
262
                            );
263
                    }
264
                }
265
            }
266
267
            // WPC 0.1 returns 'failed![error messag]' when conversion fails. Handle that.
268
            if (substr($response, 0, 7) == 'failed!') {
269
                throw new ConversionFailedException(
270
                    'WPC failed converting image: "' . substr($response, 7) . '"'
271
                );
272
            }
273
274
            if (empty($response)) {
275
                $errorMsg = 'Error: Unexpected result. We got nothing back. HTTP CODE: ' . $httpCode;
276
                throw new ConversionFailedException($errorMsg);
277
            } else {
278
                $errorMsg = 'Error: Unexpected result. We did not receive an image. We received: "';
279
                $errorMsg .= str_replace("\r", '', str_replace("\n", '', htmlentities(substr($response, 0, 400))));
280
                throw new ConversionFailedException($errorMsg . '..."');
281
            }
282
            //throw new ConverterNotOperationalException($response);
283
        }
284
285
        $success = @file_put_contents($this->destination, $response);
286
        curl_close($ch);
287
288
        if (!$success) {
289
            throw new ConversionFailedException('Error saving file. Check file permissions');
290
        }
291
    }
292
}
293