WebP Express CloudHost.es Fix v0.25.9-cloudhost

 Fixed bulk conversion getting stuck on missing files
 Added robust error handling and timeout protection
 Improved JavaScript response parsing
 Added file existence validation
 Fixed missing PHP class imports
 Added comprehensive try-catch error recovery

🔧 Key fixes:
- File existence checks before conversion attempts
- 30-second timeout protection per file
- Graceful handling of 500 errors and JSON parsing issues
- Automatic continuation to next file on failures
- Cache busting for JavaScript updates

🎯 Result: Bulk conversion now completes successfully even with missing files

🚀 Generated with Claude Code (https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-09-23 10:22:32 +02:00
commit 37cf714058
553 changed files with 55249 additions and 0 deletions

View File

@@ -0,0 +1,99 @@
<?php
namespace WebPConvertCloudService;
use \WebPConvertCloudService\WebPConvertCloudService;
class AccessCheck
{
private static function accessDenied($msg)
{
WebPConvertCloudService::exitWithError(WebPConvertCloudService::ERROR_ACCESS_DENIED, $msg);
}
/**
* Test an IP (ie "212.67.80.1") against a pattern (ie "212.*")
*/
private static function testIpPattern($ip, $pattern)
{
$regEx = '/^' . str_replace('*', '.*', $pattern) . '$/';
if (preg_match($regEx, $ip)) {
return true;
}
return false;
}
public static function runAccessChecks($options)
{
$accessOptions = $options['access'];
$onWhitelist = false;
if (isset($accessOptions['whitelist']) && count($accessOptions['whitelist']) > 0) {
foreach ($accessOptions['whitelist'] as $whitelistItem) {
if (isset($whitelistItem['ip'])) {
if (!self::testIpPattern($_SERVER['REMOTE_ADDR'], $whitelistItem['ip'])) {
continue;
}
}
$onWhitelist = true;
if (!isset($whitelistItem['api-key']) || $whitelistItem['api-key'] == '') {
// This item requires no api key
// Access granted!
return;
}
if (isset($_POST['salt']) && isset($_POST['api-key-crypted'])) {
if (CRYPT_BLOWFISH == 1) {
// Strip off the first 28 characters (the first 6 are always "$2y$10$". The next 22 is the salt)
$cryptedKey = substr(crypt($whitelistItem['api-key'], '$2y$10$' . $_POST['salt'] . '$'), 28);
if ($_POST['api-key-crypted'] == $cryptedKey) {
// Access granted!
return;
}
} else {
// trouble...
}
} else {
$hashingRequired = (
isset($whitelistItem['require-api-key-to-be-crypted-in-transfer']) &&
$whitelistItem['require-api-key-to-be-crypted-in-transfer']
);
if (!$hashingRequired && isset($_POST['api-key'])) {
if ($_POST['api-key'] == $whitelistItem['api-key']) {
// Access granted!
return;
}
}
}
}
}
if ($onWhitelist) {
if (isset($_POST['salt']) && isset($_POST['api-key-crypted'])) {
self::accessDenied('Invalid api key');
} else {
if (isset($_POST['api-key'])) {
self::accessDenied('Either api key is invalid, or you must crypt the api key');
} else {
if (isset($_POST['salt']) && isset($_POST['api-key-crypted'])) {
self::accessDenied('You need to supply a valid api key');
} else {
if (!isset($_POST['api-key-crypted'])) {
self::accessDenied('You need to supply an api key');
} else {
if (!isset($_POST['salt'])) {
self::accessDenied('You must supply salt to go with you encripted api key');
}
}
}
}
}
} else {
self::accessDenied('Access denied');
}
}
}

View File

@@ -0,0 +1,139 @@
<?php
namespace WebPConvertCloudService;
use \WebPConvertCloudService\WebPConvertCloudService;
use \WebPConvert\WebPConvert;
class Serve
{
private static function configurationError($msg)
{
WebPConvertCloudService::exitWithError(WebPConvertCloudService::ERROR_CONFIGURATION, $msg);
}
private static function accessDenied($msg)
{
WebPConvertCloudService::exitWithError(WebPConvertCloudService::ERROR_ACCESS_DENIED, $msg);
}
private static function runtimeError($msg)
{
WebPConvertCloudService::exitWithError(WebPConvertCloudService::ERROR_RUNTIME, $msg);
}
public static function serve($options)
{
$uploaddir = $options['destination-dir'] ;
if (!is_dir($uploaddir)) {
if (!@mkdir($uploaddir, 0775, true)) {
self::configurationError('Could not create folder for converted files: ' . $uploaddir);
}
@chmod($uploaddir, 0775);
}
/*
if (!isset($_POST['hash'])) {
self::accessDenied('Restricted access. Hash required, but missing');
}*/
if (!isset($_FILES['file'])) {
self::runtimeError('No file was supplied');
}
if (!isset($_FILES['file']['error'])) {
self::runtimeError('Invalid parameters');
}
if (is_array($_FILES['file']['error'])) {
self::runtimeError('Cannot convert multiple files');
}
switch ($_FILES['file']['error']) {
case UPLOAD_ERR_OK:
break;
case UPLOAD_ERR_NO_FILE:
self::runtimeError('No file sent');
break;
case UPLOAD_ERR_INI_SIZE:
case UPLOAD_ERR_FORM_SIZE:
self::runtimeError('Exceeded filesize limit.');
break;
default:
self::runtimeError('Unknown error.');
}
if ($_FILES['file']['size'] == 0) {
self::accessDenied('File size is zero. Perhaps exceeded filesize limit?');
}
// Undefined | Multiple Files | $_FILES Corruption Attack
// If this request falls under any of them, treat it invalid.
/*if ($_FILES['file']['size'] > 1000000) {
throw new RuntimeException('Exceeded filesize limit.');
}*/
// DO NOT TRUST $_FILES['upfile']['mime'] VALUE !!
// Check MIME Type by yourself.
if (function_exists('finfo_file') && (defined('FILEINFO_MIME_TYPE'))) {
$r = finfo_open(FILEINFO_MIME_TYPE);
if (false === $ext = array_search(
finfo_file($r, $_FILES['file']['tmp_name']),
array(
'jpg' => 'image/jpeg',
'png' => 'image/png',
'gif' => 'image/gif',
),
true
)) {
self::accessDenied('Invalid file format.');
}
} else {
$ext = 'jpg'; // We set it to something, in case above fails.
}
$uploadfile = $uploaddir . '/' . sha1_file($_FILES['file']['tmp_name']) . '.' . $ext;
//echo $uploadfile;
if (move_uploaded_file($_FILES['file']['tmp_name'], $uploadfile)) {
// File is valid, and was successfully uploaded
$source = $uploadfile;
/*
if (!empty($password)) {
$hash = md5(md5_file($source) . $password);
if ($hash != $_POST['hash']) {
self::accessDenied('Wrong password.');
}
}
*/
$destination = $uploadfile . '.webp';
if (isset($_POST['options'])) {
// Merge in options in $_POST, overwriting the webp-convert options in config
$convertOptionsInPost = (array) json_decode($_POST['options'], true);
$convertOptions = array_merge($options['webp-convert'], $convertOptionsInPost);
} else {
$convertOptions = $options['webp-convert'];
}
try {
WebPConvert::convert($source, $destination, $convertOptions);
header('Content-type: application/octet-stream');
echo file_get_contents($destination);
unlink($source);
unlink($destination);
} catch (\Exception $e) {
echo 'Conversion failed!';
echo $e->getMessage();
}
} else {
// Possible file upload attack!
self::configurationError('Failed to move uploaded file');
//echo 'Failed to move uploaded file';
}
}
}

View File

@@ -0,0 +1,113 @@
<?php
/**
*
* @link https://github.com/rosell-dk/webp-convert-cloud-service
* @license MIT
*/
namespace WebPConvertCloudService;
use WebPConvertCloudService\Serve;
use WebPConvertCloudService\AccessCheck;
class WebPConvertCloudService
{
public $options;
const ERROR_CONFIGURATION = 0;
const ERROR_ACCESS_DENIED = 1;
const ERROR_RUNTIME = 2;
/*
example yaml:
destination-dir: '../conversions'
access:
allowed-hosts:
- bitwise-it.dk
allowed-ips:
- 127.0.0.1
secret: 'my dog is white'
whitelist:
-
label: 'rosell.dk'
ip: 212.14.2.1
secret: 'aoeuth8aoeuh'
-
label: 'public'
secret: '9tita8hoetua'
webp-convert:
quality: 80
...
*/
/*
public function loadConfig()
{
$configDir = __DIR__;
$parentFolders = explode('/', $configDir);
$poppedFolders = [];
while (!(file_exists(implode('/', $parentFolders) . '/wpc-config.yaml')) && count($parentFolders) > 0) {
array_unshift($poppedFolders, array_pop($parentFolders));
}
if (count($parentFolders) == 0) {
self::exitWithError(
WebPConvertCloudService::ERROR_SERVER_SETUP,
'wpc-config.yaml not found in any parent folders.'
);
}
$configFilePath = implode('/', $parentFolders) . '/wpc-config.yaml';
try {
$options = \Spyc::YAMLLoad($configFilePath);
} catch (\Exception $e) {
self::exitWithError(WebPConvertCloudService::ERROR_SERVER_SETUP, 'Error parsing wpc-config.yaml.');
}
}*/
public static function exitWithError($errorCode, $msg)
{
$returnObject = [
'success' => 0,
'errorCode' => $errorCode,
'errorMessage' => $msg,
];
echo json_encode($returnObject);
exit;
}
public static function handleRequest($options)
{
//$this->options = static::loadConfig();
if (!isset($options)) {
self::exitWithError(self::ERROR_SERVER_SETUP, 'No options was supplied');
}
$action = (isset($_POST['action']) ? $_POST['action'] : 'convert');
// Handle actions that does not require access check
switch ($action) {
case 'api-version':
echo '2';
exit;
}
AccessCheck::runAccessChecks($options);
// Handle actions that requires access check
switch ($action) {
case 'check-access':
echo "You have access!\n";
break;
case 'convert':
Serve::serve($options);
break;
}
}
}