| 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 |
use WebPConvert\Options\OptionFactory; |
| 17 |
|
| 18 |
/** |
| 19 |
* Convert images to webp using Wpc (a cloud converter based on WebP Convert). |
| 20 |
* |
| 21 |
* @package WebPConvert |
| 22 |
* @author Bjørn Rosell <[email protected]> |
| 23 |
* @since Class available since Release 2.0.0 |
| 24 |
*/ |
| 25 |
class Wpc extends AbstractConverter |
| 26 |
{ |
| 27 |
use CloudConverterTrait; |
| 28 |
use CurlTrait; |
| 29 |
use EncodingAutoTrait; |
| 30 |
|
| 31 |
protected function getUnsupportedDefaultOptions() |
| 32 |
{ |
| 33 |
return []; |
| 34 |
} |
| 35 |
|
| 36 |
public function getUniqueOptions($imageType) |
| 37 |
{ |
| 38 |
return OptionFactory::createOptions([ |
| 39 |
['api-key', 'string', [ |
| 40 |
'title' => 'API key', |
| 41 |
'description' => 'The API key is set up on the remote. Copy that.', |
| 42 |
'default' => '', |
| 43 |
'sensitive' => true, |
| 44 |
'ui' => [ |
| 45 |
'component' => 'password', |
| 46 |
'advanced' => false, |
| 47 |
'display' => "option('wpc-api-version') != 0" |
| 48 |
] |
| 49 |
]], |
| 50 |
['secret', 'string', [ |
| 51 |
'title' => 'Secret', |
| 52 |
'description' => '', |
| 53 |
'default' => '', |
| 54 |
'sensitive' => true, |
| 55 |
'ui' => [ |
| 56 |
'component' => 'password', |
| 57 |
'advanced' => false, |
| 58 |
'display' => "option('wpc-api-version') == 0" |
| 59 |
] |
| 60 |
]], |
| 61 |
['api-url', 'string', [ |
| 62 |
'title' => 'API url', |
| 63 |
'description' => 'The endpoint of the web service. Copy it from the remote setup', |
| 64 |
'default' => '', |
| 65 |
'sensitive' => true, |
| 66 |
'ui' => [ |
| 67 |
'component' => 'password', |
| 68 |
'advanced' => false, |
| 69 |
] |
| 70 |
]], |
| 71 |
['api-version', 'int', [ |
| 72 |
'title' => 'API version', |
| 73 |
'description' => |
| 74 |
'Refers to the major version of Wpc. ' . |
| 75 |
'It is probably 2, as it is a long time since 2.0 was released', |
| 76 |
'default' => 2, |
| 77 |
'minimum' => 0, |
| 78 |
'maximum' => 2, |
| 79 |
'ui' => [ |
| 80 |
'component' => 'select', |
| 81 |
'advanced' => false, |
| 82 |
'options' => [0, 1, 2], |
| 83 |
] |
| 84 |
]], |
| 85 |
['crypt-api-key-in-transfer', 'boolean', [ |
| 86 |
'title' => 'Crypt API key in transfer', |
| 87 |
'description' => |
| 88 |
'If checked, the api key will be crypted in requests. ' . |
| 89 |
'Crypting the api-key protects it from being stolen during transfer', |
| 90 |
'default' => false, |
| 91 |
'ui' => [ |
| 92 |
'component' => 'checkbox', |
| 93 |
'advanced' => true, |
| 94 |
'display' => "option('wpc-api-version') >= 1" |
| 95 |
] |
| 96 |
]], |
| 97 |
]); |
| 98 |
|
| 99 |
/*return [ |
| 100 |
new SensitiveStringOption('api-key', ''), |
| 101 |
new SensitiveStringOption('secret', ''), |
| 102 |
new SensitiveStringOption('api-url', ''), |
| 103 |
new SensitiveStringOption('url', ''), // DO NOT USE. Only here to keep the protection |
| 104 |
new IntegerOption('api-version', 2, 0, 2), |
| 105 |
new BooleanOption('crypt-api-key-in-transfer', false) // new in api v.1 |
| 106 |
];*/ |
| 107 |
} |
| 108 |
|
| 109 |
public function supportsLossless() |
| 110 |
{ |
| 111 |
return ($this->options['api-version'] >= 2); |
| 112 |
} |
| 113 |
|
| 114 |
public function passOnEncodingAuto() |
| 115 |
{ |
| 116 |
// We could make this configurable. But I guess passing it on is always to be preferred |
| 117 |
// for api >= 2. |
| 118 |
return ($this->options['api-version'] >= 2); |
| 119 |
} |
| 120 |
|
| 121 |
private static function createRandomSaltForBlowfish() |
| 122 |
{ |
| 123 |
$salt = ''; |
| 124 |
$validCharsForSalt = array_merge( |
| 125 |
range('A', 'Z'), |
| 126 |
range('a', 'z'), |
| 127 |
range('0', '9'), |
| 128 |
['.', '/'] |
| 129 |
); |
| 130 |
|
| 131 |
for ($i = 0; $i < 22; $i++) { |
| 132 |
$salt .= $validCharsForSalt[array_rand($validCharsForSalt)]; |
| 133 |
} |
| 134 |
return $salt; |
| 135 |
} |
| 136 |
|
| 137 |
/** |
| 138 |
* Get api key from options or environment variable |
| 139 |
* |
| 140 |
* @return string api key or empty string if none is set |
| 141 |
*/ |
| 142 |
private function getApiKey() |
| 143 |
{ |
| 144 |
if ($this->options['api-version'] == 0) { |
| 145 |
if (!empty($this->options['secret'])) { |
| 146 |
return $this->options['secret']; |
| 147 |
} |
| 148 |
} elseif ($this->options['api-version'] >= 1) { |
| 149 |
if (!empty($this->options['api-key'])) { |
| 150 |
return $this->options['api-key']; |
| 151 |
} |
| 152 |
} |
| 153 |
if (defined('WEBPCONVERT_WPC_API_KEY')) { |
| 154 |
return constant('WEBPCONVERT_WPC_API_KEY'); |
| 155 |
} |
| 156 |
if (!empty(getenv('WEBPCONVERT_WPC_API_KEY'))) { |
| 157 |
return getenv('WEBPCONVERT_WPC_API_KEY'); |
| 158 |
} |
| 159 |
return ''; |
| 160 |
} |
| 161 |
|
| 162 |
/** |
| 163 |
* Get url from options or environment variable |
| 164 |
* |
| 165 |
* @return string URL to WPC or empty string if none is set |
| 166 |
*/ |
| 167 |
private function getApiUrl() |
| 168 |
{ |
| 169 |
if (!empty($this->options['api-url'])) { |
| 170 |
return $this->options['api-url']; |
| 171 |
} |
| 172 |
if (defined('WEBPCONVERT_WPC_API_URL')) { |
| 173 |
return constant('WEBPCONVERT_WPC_API_URL'); |
| 174 |
} |
| 175 |
if (!empty(getenv('WEBPCONVERT_WPC_API_URL'))) { |
| 176 |
return getenv('WEBPCONVERT_WPC_API_URL'); |
| 177 |
} |
| 178 |
return ''; |
| 179 |
} |
| 180 |
|
| 181 |
|
| 182 |
/** |
| 183 |
* Check operationality of Wpc converter. |
| 184 |
* |
| 185 |
* @throws SystemRequirementsNotMetException if system requirements are not met (curl) |
| 186 |
* @throws ConverterNotOperationalException if key is missing or invalid, or quota has exceeded |
| 187 |
*/ |
| 188 |
public function checkOperationality() |
| 189 |
{ |
| 190 |
|
| 191 |
$options = $this->options; |
| 192 |
|
| 193 |
$apiVersion = $options['api-version']; |
| 194 |
|
| 195 |
if ($this->getApiUrl() == '') { |
| 196 |
if (isset($this->options['url']) && ($this->options['url'] != '')) { |
| 197 |
throw new ConverterNotOperationalException( |
| 198 |
'The "url" option has been renamed to "api-url" in webp-convert 2.0. ' . |
| 199 |
'You must change the configuration accordingly.' |
| 200 |
); |
| 201 |
} |
| 202 |
throw new ConverterNotOperationalException( |
| 203 |
'Missing URL. You must install Webp Convert Cloud Service on a server, ' . |
| 204 |
'or the WebP Express plugin for Wordpress - and supply the url.' |
| 205 |
); |
| 206 |
} |
| 207 |
|
| 208 |
if ($apiVersion == 0) { |
| 209 |
if (!empty($this->getApiKey())) { |
| 210 |
// if secret is set, we need md5() and md5_file() functions |
| 211 |
if (!function_exists('md5')) { |
| 212 |
throw new ConverterNotOperationalException( |
| 213 |
'A secret has been set, which requires us to create a md5 hash from the secret and the file ' . |
| 214 |
'contents. ' . |
| 215 |
'But the required md5() PHP function is not available.' |
| 216 |
); |
| 217 |
} |
| 218 |
if (!function_exists('md5_file')) { |
| 219 |
throw new ConverterNotOperationalException( |
| 220 |
'A secret has been set, which requires us to create a md5 hash from the secret and the file ' . |
| 221 |
'contents. But the required md5_file() PHP function is not available.' |
| 222 |
); |
| 223 |
} |
| 224 |
} |
| 225 |
} else { |
| 226 |
if ($options['crypt-api-key-in-transfer']) { |
| 227 |
if (!function_exists('crypt')) { |
| 228 |
throw new ConverterNotOperationalException( |
| 229 |
'Configured to crypt the api-key, but crypt() function is not available.' |
| 230 |
); |
| 231 |
} |
| 232 |
|
| 233 |
if (!defined('CRYPT_BLOWFISH')) { |
| 234 |
throw new ConverterNotOperationalException( |
| 235 |
'Configured to crypt the api-key. ' . |
| 236 |
'That requires Blowfish encryption, which is not available on your current setup.' |
| 237 |
); |
| 238 |
} |
| 239 |
} |
| 240 |
} |
| 241 |
|
| 242 |
// Check for curl requirements |
| 243 |
$this->checkOperationalityForCurlTrait(); |
| 244 |
} |
| 245 |
|
| 246 |
/* |
| 247 |
public function checkConvertability() |
| 248 |
{ |
| 249 |
// check upload limits |
| 250 |
$this->checkConvertabilityCloudConverterTrait(); |
| 251 |
|
| 252 |
// TODO: some from below can be moved up here |
| 253 |
} |
| 254 |
*/ |
| 255 |
|
| 256 |
private function createOptionsToSend() |
| 257 |
{ |
| 258 |
$optionsToSend = $this->options; |
| 259 |
|
| 260 |
if ($this->isQualityDetectionRequiredButFailing()) { |
| 261 |
// quality was set to "auto", but we could not meassure the quality of the jpeg locally |
| 262 |
// Ask the cloud service to do it, rather than using what we came up with. |
| 263 |
$optionsToSend['quality'] = 'auto'; |
| 264 |
} else { |
| 265 |
$optionsToSend['quality'] = $this->getCalculatedQuality(); |
| 266 |
} |
| 267 |
|
| 268 |
// The following are unset for security reasons. |
| 269 |
unset($optionsToSend['converters']); |
| 270 |
unset($optionsToSend['secret']); |
| 271 |
unset($optionsToSend['api-key']); |
| 272 |
unset($optionsToSend['api-url']); |
| 273 |
|
| 274 |
$apiVersion = $optionsToSend['api-version']; |
| 275 |
|
| 276 |
if ($apiVersion == 1) { |
| 277 |
// Lossless can be "auto" in api 2, but in api 1 "auto" is not supported |
| 278 |
//unset($optionsToSend['lossless']); |
| 279 |
} elseif ($apiVersion == 2) { |
| 280 |
//unset($optionsToSend['png']); |
| 281 |
//unset($optionsToSend['jpeg']); |
| 282 |
|
| 283 |
// The following are unset for security reasons. |
| 284 |
unset($optionsToSend['cwebp-command-line-options']); |
| 285 |
unset($optionsToSend['command-line-options']); |
| 286 |
} |
| 287 |
|
| 288 |
return $optionsToSend; |
| 289 |
} |
| 290 |
|
| 291 |
private function createPostData() |
| 292 |
{ |
| 293 |
$options = $this->options; |
| 294 |
|
| 295 |
$postData = [ |
| 296 |
'file' => curl_file_create($this->source), |
| 297 |
'options' => json_encode($this->createOptionsToSend()), |
| 298 |
'servername' => (isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : '') |
| 299 |
]; |
| 300 |
|
| 301 |
$apiVersion = $options['api-version']; |
| 302 |
|
| 303 |
$apiKey = $this->getApiKey(); |
| 304 |
|
| 305 |
if ($apiVersion == 0) { |
| 306 |
$postData['hash'] = md5(md5_file($this->source) . $apiKey); |
| 307 |
} else { |
| 308 |
//$this->logLn('api key: ' . $apiKey); |
| 309 |
|
| 310 |
if ($options['crypt-api-key-in-transfer']) { |
| 311 |
$salt = self::createRandomSaltForBlowfish(); |
| 312 |
$postData['salt'] = $salt; |
| 313 |
|
| 314 |
// Strip off the first 28 characters (the first 6 are always "$2y$10$". The next 22 is the salt) |
| 315 |
$postData['api-key-crypted'] = substr(crypt($apiKey, '$2y$10$' . $salt . '$'), 28); |
| 316 |
} else { |
| 317 |
$postData['api-key'] = $apiKey; |
| 318 |
} |
| 319 |
} |
| 320 |
return $postData; |
| 321 |
} |
| 322 |
|
| 323 |
protected function doActualConvert() |
| 324 |
{ |
| 325 |
$ch = self::initCurl(); |
| 326 |
|
| 327 |
//$this->logLn('api url: ' . $this->getApiUrl()); |
| 328 |
|
| 329 |
curl_setopt_array($ch, [ |
| 330 |
CURLOPT_URL => $this->getApiUrl(), |
| 331 |
CURLOPT_POST => 1, |
| 332 |
CURLOPT_POSTFIELDS => $this->createPostData(), |
| 333 |
CURLOPT_BINARYTRANSFER => true, |
| 334 |
CURLOPT_RETURNTRANSFER => true, |
| 335 |
CURLOPT_HEADER => false, |
| 336 |
CURLOPT_SSL_VERIFYPEER => false |
| 337 |
]); |
| 338 |
|
| 339 |
$response = curl_exec($ch); |
| 340 |
if (curl_errno($ch)) { |
| 341 |
$this->logLn('Curl error: ', 'bold'); |
| 342 |
$this->logLn(curl_error($ch)); |
| 343 |
throw new ConverterNotOperationalException('Curl error:'); |
| 344 |
} |
| 345 |
|
| 346 |
// Check if we got a 404 |
| 347 |
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); |
| 348 |
if ($httpCode == 404) { |
| 349 |
curl_close($ch); |
| 350 |
throw new ConversionFailedException( |
| 351 |
'WPC was not found at the specified URL - we got a 404 response.' |
| 352 |
); |
| 353 |
} |
| 354 |
|
| 355 |
// Check for empty response |
| 356 |
if (empty($response)) { |
| 357 |
throw new ConversionFailedException( |
| 358 |
'Error: Unexpected result. We got nothing back. ' . |
| 359 |
'HTTP CODE: ' . $httpCode . '. ' . |
| 360 |
'Content type:' . curl_getinfo($ch, CURLINFO_CONTENT_TYPE) |
| 361 |
); |
| 362 |
}; |
| 363 |
|
| 364 |
// The WPC cloud service either returns an image or an error message |
| 365 |
// Images has application/octet-stream. |
| 366 |
// Verify that we got an image back. |
| 367 |
if (curl_getinfo($ch, CURLINFO_CONTENT_TYPE) != 'application/octet-stream') { |
| 368 |
curl_close($ch); |
| 369 |
|
| 370 |
if (substr($response, 0, 1) == '{') { |
| 371 |
$responseObj = json_decode($response, true); |
| 372 |
if (isset($responseObj['errorCode'])) { |
| 373 |
switch ($responseObj['errorCode']) { |
| 374 |
case 0: |
| 375 |
throw new ConverterNotOperationalException( |
| 376 |
'There are problems with the server setup: "' . |
| 377 |
$responseObj['errorMessage'] . '"' |
| 378 |
); |
| 379 |
case 1: |
| 380 |
throw new InvalidApiKeyException( |
| 381 |
'Access denied. ' . $responseObj['errorMessage'] |
| 382 |
); |
| 383 |
default: |
| 384 |
throw new ConversionFailedException( |
| 385 |
'Conversion failed: "' . $responseObj['errorMessage'] . '"' |
| 386 |
); |
| 387 |
} |
| 388 |
} |
| 389 |
} |
| 390 |
|
| 391 |
// WPC 0.1 returns 'failed![error messag]' when conversion fails. Handle that. |
| 392 |
if (substr($response, 0, 7) == 'failed!') { |
| 393 |
throw new ConversionFailedException( |
| 394 |
'WPC failed converting image: "' . substr($response, 7) . '"' |
| 395 |
); |
| 396 |
} |
| 397 |
|
| 398 |
$this->logLn('Bummer, we did not receive an image'); |
| 399 |
$this->log('What we received starts with: "'); |
| 400 |
$this->logLn( |
| 401 |
str_replace("\r", '', str_replace("\n", '', htmlentities(substr($response, 0, 400)))) . '..."' |
| 402 |
); |
| 403 |
|
| 404 |
throw new ConversionFailedException('Unexpected result. We did not receive an image but something else.'); |
| 405 |
//throw new ConverterNotOperationalException($response); |
| 406 |
} |
| 407 |
|
| 408 |
$success = file_put_contents($this->destination, $response); |
| 409 |
curl_close($ch); |
| 410 |
|
| 411 |
if (!$success) { |
| 412 |
throw new ConversionFailedException('Error saving file. Check file permissions'); |
| 413 |
} |
| 414 |
} |
| 415 |
} |
| 416 |
|