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
|
|
|
public function supportsLossless() |
36
|
|
|
{ |
37
|
|
|
return ($this->options['api-version'] >= 2); |
38
|
|
|
} |
39
|
|
|
|
40
|
3 |
|
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
|
3 |
|
return ($this->options['api-version'] >= 2); |
45
|
|
|
} |
46
|
|
|
|
47
|
3 |
|
protected function createOptions() |
48
|
|
|
{ |
49
|
3 |
|
parent::createOptions(); |
50
|
|
|
|
51
|
3 |
|
$this->options2->addOptions( |
52
|
3 |
|
new SensitiveStringOption('api-key', ''), /* for communicating with wpc api v.1+ */ |
53
|
|
|
new SensitiveStringOption('secret', ''), /* for communicating with wpc api v.0 */ |
54
|
3 |
|
new SensitiveStringOption('api-url', ''), |
55
|
3 |
|
new SensitiveStringOption('url', ''), /* DO NOT USE. Only here to keep the protection */ |
56
|
3 |
|
new IntegerOption('api-version', 2, 0, 2), |
57
|
3 |
|
new BooleanOption('crypt-api-key-in-transfer', false) /* new in api v.1 */ |
58
|
|
|
); |
59
|
3 |
|
} |
60
|
|
|
|
61
|
|
|
private static function createRandomSaltForBlowfish() |
62
|
|
|
{ |
63
|
|
|
$salt = ''; |
64
|
|
|
$validCharsForSalt = array_merge( |
65
|
|
|
range('A', 'Z'), |
66
|
|
|
range('a', 'z'), |
67
|
|
|
range('0', '9'), |
68
|
|
|
['.', '/'] |
69
|
|
|
); |
70
|
|
|
|
71
|
|
|
for ($i=0; $i<22; $i++) { |
72
|
|
|
$salt .= $validCharsForSalt[array_rand($validCharsForSalt)]; |
73
|
|
|
} |
74
|
|
|
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
|
3 |
|
private function getApiKey() |
83
|
|
|
{ |
84
|
3 |
|
if ($this->options['api-version'] == 0) { |
85
|
|
|
if (!empty($this->options['secret'])) { |
86
|
|
|
return $this->options['secret']; |
87
|
|
|
} |
88
|
3 |
|
} elseif ($this->options['api-version'] >= 1) { |
89
|
3 |
|
if (!empty($this->options['api-key'])) { |
90
|
|
|
return $this->options['api-key']; |
91
|
|
|
} |
92
|
|
|
} |
93
|
3 |
|
if (defined('WEBPCONVERT_WPC_API_KEY')) { |
94
|
|
|
return constant('WEBPCONVERT_WPC_API_KEY'); |
95
|
|
|
} |
96
|
3 |
|
if (!empty(getenv('WEBPCONVERT_WPC_API_KEY'))) { |
97
|
|
|
return getenv('WEBPCONVERT_WPC_API_KEY'); |
98
|
|
|
} |
99
|
3 |
|
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
|
3 |
|
private function getApiUrl() |
108
|
|
|
{ |
109
|
3 |
|
if (!empty($this->options['api-url'])) { |
110
|
3 |
|
return $this->options['api-url']; |
111
|
|
|
} |
112
|
|
|
if (defined('WEBPCONVERT_WPC_API_URL')) { |
113
|
|
|
return constant('WEBPCONVERT_WPC_API_URL'); |
114
|
|
|
} |
115
|
|
|
if (!empty(getenv('WEBPCONVERT_WPC_API_URL'))) { |
116
|
|
|
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
|
3 |
|
public function checkOperationality() |
129
|
|
|
{ |
130
|
|
|
|
131
|
3 |
|
$options = $this->options; |
132
|
|
|
|
133
|
3 |
|
$apiVersion = $options['api-version']; |
134
|
|
|
|
135
|
3 |
|
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
|
3 |
|
if ($apiVersion == 0) { |
149
|
|
|
if (!empty($this->getApiKey())) { |
150
|
|
|
// if secret is set, we need md5() and md5_file() functions |
151
|
|
|
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
|
|
|
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
|
|
|
'contents. But the required md5_file() PHP function is not available.' |
162
|
|
|
); |
163
|
|
|
} |
164
|
|
|
} |
165
|
3 |
|
} elseif ($apiVersion >= 1) { |
166
|
3 |
|
if ($options['crypt-api-key-in-transfer']) { |
167
|
|
|
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
|
|
|
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
|
3 |
|
$this->checkOperationalityForCurlTrait(); |
184
|
3 |
|
} |
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
|
3 |
|
private function createOptionsToSend() |
197
|
|
|
{ |
198
|
3 |
|
$optionsToSend = $this->options; |
199
|
|
|
|
200
|
3 |
|
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
|
3 |
|
$optionsToSend['quality'] = $this->getCalculatedQuality(); |
206
|
|
|
} |
207
|
|
|
|
208
|
|
|
// The following are unset for security reasons. |
209
|
3 |
|
unset($optionsToSend['converters']); |
210
|
3 |
|
unset($optionsToSend['secret']); |
211
|
3 |
|
unset($optionsToSend['api-key']); |
212
|
3 |
|
unset($optionsToSend['api-url']); |
213
|
|
|
|
214
|
3 |
|
$apiVersion = $optionsToSend['api-version']; |
215
|
|
|
|
216
|
3 |
|
if ($apiVersion == 1) { |
217
|
|
|
// Lossless can be "auto" in api 2, but in api 1 "auto" is not supported |
218
|
|
|
//unset($optionsToSend['lossless']); |
219
|
3 |
|
} 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
|
3 |
|
return $optionsToSend; |
229
|
|
|
} |
230
|
|
|
|
231
|
3 |
|
private function createPostData() |
232
|
|
|
{ |
233
|
3 |
|
$options = $this->options; |
234
|
|
|
|
235
|
|
|
$postData = [ |
236
|
3 |
|
'file' => curl_file_create($this->source), |
237
|
3 |
|
'options' => json_encode($this->createOptionsToSend()), |
238
|
3 |
|
'servername' => (isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : '') |
239
|
|
|
]; |
240
|
|
|
|
241
|
3 |
|
$apiVersion = $options['api-version']; |
242
|
|
|
|
243
|
3 |
|
$apiKey = $this->getApiKey(); |
244
|
|
|
|
245
|
3 |
|
if ($apiVersion == 0) { |
246
|
|
|
$postData['hash'] = md5(md5_file($this->source) . $apiKey); |
247
|
3 |
|
} elseif ($apiVersion == 1) { |
248
|
|
|
//$this->logLn('api key: ' . $apiKey); |
249
|
|
|
|
250
|
|
|
if ($options['crypt-api-key-in-transfer']) { |
251
|
|
|
$salt = self::createRandomSaltForBlowfish(); |
252
|
|
|
$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
|
|
|
$postData['api-key-crypted'] = substr(crypt($apiKey, '$2y$10$' . $salt . '$'), 28); |
256
|
|
|
} else { |
257
|
|
|
$postData['api-key'] = $apiKey; |
258
|
|
|
} |
259
|
|
|
} |
260
|
3 |
|
return $postData; |
261
|
|
|
} |
262
|
|
|
|
263
|
3 |
|
protected function doActualConvert() |
264
|
|
|
{ |
265
|
3 |
|
$ch = self::initCurl(); |
266
|
|
|
|
267
|
|
|
//$this->logLn('api url: ' . $this->getApiUrl()); |
268
|
|
|
|
269
|
3 |
|
curl_setopt_array($ch, [ |
270
|
3 |
|
CURLOPT_URL => $this->getApiUrl(), |
271
|
3 |
|
CURLOPT_POST => 1, |
272
|
3 |
|
CURLOPT_POSTFIELDS => $this->createPostData(), |
273
|
3 |
|
CURLOPT_BINARYTRANSFER => true, |
274
|
3 |
|
CURLOPT_RETURNTRANSFER => true, |
275
|
3 |
|
CURLOPT_HEADER => false, |
276
|
3 |
|
CURLOPT_SSL_VERIFYPEER => false |
277
|
|
|
]); |
278
|
|
|
|
279
|
3 |
|
$response = curl_exec($ch); |
280
|
3 |
|
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
|
2 |
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); |
288
|
2 |
|
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
|
1 |
|
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
|
1 |
|
if (curl_getinfo($ch, CURLINFO_CONTENT_TYPE) != 'application/octet-stream') { |
308
|
1 |
|
curl_close($ch); |
309
|
|
|
|
310
|
1 |
|
if (substr($response, 0, 1) == '{') { |
311
|
|
|
$responseObj = json_decode($response, true); |
312
|
|
|
if (isset($responseObj['errorCode'])) { |
313
|
|
|
switch ($responseObj['errorCode']) { |
314
|
|
|
case 0: |
315
|
|
|
throw new ConverterNotOperationalException( |
316
|
|
|
'There are problems with the server setup: "' . |
317
|
|
|
$responseObj['errorMessage'] . '"' |
318
|
|
|
); |
319
|
|
|
case 1: |
320
|
|
|
throw new InvalidApiKeyException( |
321
|
|
|
'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
|
|
|
$success = file_put_contents($this->destination, $response); |
349
|
|
|
curl_close($ch); |
350
|
|
|
|
351
|
|
|
if (!$success) { |
352
|
|
|
throw new ConversionFailedException('Error saving file. Check file permissions'); |
353
|
|
|
} |
354
|
|
|
} |
355
|
|
|
} |
356
|
|
|
|