Add image upload support, file handling improvements, and OpenAI integration

- Introduced `uploadRoute` to handle image uploads with MIME type validation and size restrictions.
- Added `upload_store` class for managing file storage and generating presigned URLs for upload/download.
- Enhanced file server to differentiate between PDFs and uploaded files, supporting dynamic content delivery.
- Integrated OpenAI module for License Plate Recognition (LPR), including API configuration and schema validation.
- Updated core structure with new interfaces (`minio_uploads_i`, `openai_i`) and classes (`openai`, `upload_store`).
- Adjusted `index.php` and file routes to support dynamic MIME checks and direct link generation.
This commit is contained in:
Jeppe Bundgaard
2025-08-18 16:50:30 +02:00
parent 841062bfca
commit a0fd41fba1
14 changed files with 777 additions and 26 deletions
+251
View File
@@ -0,0 +1,251 @@
<?php
namespace classes;
require_once WD . '/modules/openAI/openAI_c.php';
use Exception;
use interfaces\openai_i;
use openAI\openAI_c;
class openai implements openai_i
{
/**
* The configuration of the module
* @var openAI_c
*/
public openAI_c $config;
/**
* API URL
* @var string
*/
private string $api_url = 'https://api.openai.com/v1/responses';
protected string $model = 'gpt-4.1-mini';
protected string $temperature = '0.7';
protected string $max_tokens = '1000';
public string $prompt_LPR = 'You are an AI assistant specialized in reading license plates. Your task is to extract the license plate number from the provided image. The license plate number can be in various formats, including letters and numbers. Please provide the license plate number in a clear and concise format.';
public function __construct()
{
$this->config = new openAI_c();
}
/**
* @inheritDoc
*/
public function requireModuleEnabled(): void
{
if (!(bool)$this->config->enabled->getVariableValue()) {
throw new Exception('OpenAI module is not enabled.');
}
}
protected function getLPRSchema(): array
{
return [
/**
* "format": {
* "type": "json_schema",
* "name": "math_reasoning",
* "schema": {
* "type": "object",
* "properties": {
* "steps": {
* "type": "array",
* "items": {
* "type": "object",
* "properties": {
* "explanation": { "type": "string" },
* "output": { "type": "string" }
* },
* "required": ["explanation", "output"],
* "additionalProperties": false
* }
* },
* "final_answer": { "type": "string" }
* },
* "required": ["steps", "final_answer"],
* "additionalProperties": false
* },
* "strict": true
* }
*/
'format' => [
'type' => 'json_schema',
'name' => 'lpr',
'schema' => [
'type' => 'object',
'properties' => [
'success' => [
'type' => 'boolean',
'description' => 'Indicates whether the license plate number was successfully extracted.'
],
'license_plate_number' => [
'type' => 'string',
'description' => 'The extracted license plate number from the image.'
],
],
'required' => ['license_plate_number', 'success'],
'additionalProperties' => false
],
'strict' => true
]
];
}
/**
* @inheritDoc
* @return array The extracted license plate number (If there is a valid response)
* @throws Exception If the API response cannot be parsed
* @throws Exception If the image base64 is not valid.
* @throws Exception If the module is not enabled or if there is an error in the API request
*/
public function lpr(string $base64_image): array
{
$this->requireModuleEnabled();
if (empty($base64_image)) {
throw new Exception('Image URL cannot be empty.');
}
$prompt = $this->prompt_LPR;
$data = [
'model' => $this->model,
'input' => [
[
'role' => 'user',
'content' => [
['type' => 'input_text', 'text' => $prompt],
[
'type' => 'input_image',
'image_url' => $this->generateTemporaryDirectDownloadLink($base64_image) // Generate a temporary direct download link for the base64 image
]
]
]
],
'text' => [
...self::getLPRSchema(), // Apply the LPR schema { success, license_plate_number }
],
'temperature' => (float)$this->temperature,
];
// Send the request to the OpenAI API
$response = $this->sendRequest($data);
// Check if the response contains the expected format
return $this->parseLPRResponse($response);
}
/**
* Generates a temporary direct download link for the image.
*
* @param string $image The image object key name
* @return string The temporary direct download link for the image.
*/
private function generateTemporaryDirectDownloadLink(string $image): string
{
$uploads = new upload_store();
// Store the image temporarily and return the direct download link
return $uploads->generateDirectDownloadUrl($image);
}
/**
* Parses the response from the OpenAI API for the LPR task.
*
* @param mixed $response The response object from the OpenAI API.
* @return array The extracted license plate number if successful, otherwise an empty string.
* @throws Exception If the response does not contain the expected format or if parsing fails.
*/
private function parseLPRResponse(mixed $response): array
{
/**
* {"success":true,"data":{"id":"resp_68a2e2d5eb608191a546ef0540797d3b0148dcae28786ad6","object":"response","created_at":1755505366,"status":"completed","background":false,"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4.1-mini-2025-04-14","output":[{"id":"msg_68a2e2d91050819183cc2c1d9be252950148dcae28786ad6","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"{\"success\":true,\"license_plate_number\":\"CU 61 297\"}"}],"role":"assistant"}],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"default","store":true,"temperature":0.7,"text":{"format":{"type":"json_schema","description":null,"name":"lpr","schema":{"type":"object","properties":{"success":{"type":"boolean","description":"Indicates whether the license plate number was successfully extracted."},"license_plate_number":{"type":"string","description":"The extracted license plate number from the image."}},"required":["license_plate_number","success"],"additionalProperties":false},"strict":true},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1,"truncation":"disabled","usage":{"input_tokens":2470,"input_tokens_details":{"cached_tokens":0},"output_tokens":16,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":2486},"user":null,"metadata":[]},"meta":[],"includes":[]}
*/
// Check if the response contains the expected format
if (!isset($response['output'][0]['content'][0]['text'])) {
throw new Exception('Invalid response format from OpenAI API. (Missing text field)');
}
$output = $response['output'][0]['content'][0]['text'];
// Decode the JSON response
$decodedOutput = json_decode($output, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new Exception('Error parsing JSON response: ' . json_last_error_msg());
}
// Check if the response contains the expected fields
if (!isset($decodedOutput['success']) || !isset($decodedOutput['license_plate_number'])) {
throw new Exception('Invalid response format from OpenAI API. (Missing success or license_plate_number field)');
}
// Return the license plate number if successful
if ($decodedOutput['success']) {
// Remove any spaces or unwanted characters from the license plate number
return [
'success' => $decodedOutput['success'],
'license_plate_number' => $this->cleanLicensePlate($decodedOutput['license_plate_number']),
];
// return $decodedOutput['license_plate_number']);
} else {
throw new Exception('License plate extraction failed.');
}
}
/**
* Cleans the license plate number by removing unwanted characters and spaces.
*
* @param string $license_plate The license plate number to clean.
* @return string The cleaned license plate number.
*/
private function cleanLicensePlate(string $license_plate): string
{
// Remove any unwanted characters (e.g., spaces, special characters)
$cleaned = preg_replace('/[^A-Za-z0-9]/', '', $license_plate);
// Return the cleaned license plate number
return strtoupper(trim($cleaned)); // Convert to uppercase and trim any whitespace
}
/**
* Sends a request to the OpenAI API and returns the response.
*
* @param array $data The data to send in the request.
* @return array The response from the API, typically an associative array containing the response data.
* @throws Exception If the API request fails or if the response cannot be parsed.
*/
private function sendRequest(array $data): array
{
$this->requireModuleEnabled();
/**
* curl https://api.openai.com/v1/responses \
* -H "Content-Type: application/json" \
* -H "Authorization: Bearer $OPENAI_API_KEY" \
* -d '{
* "model": "gpt-4.1-mini",
* "input": [
* {
* "role": "user",
* "content": [
* {"type": "input_text", "text": "what is in this image?"},
* {
* "type": "input_image",
* "image_url": "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg"
* }
* ]
* }
* ]
* }'
*/
$curl = curl_init($this->api_url);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_POST, true);
curl_setopt($curl, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Authorization: Bearer ' . $this->config->api_key->getVariableValue()
]);
curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($data));
$response = curl_exec($curl);
if (curl_errno($curl)) {
throw new Exception('cURL error: ' . curl_error($curl));
}
curl_close($curl);
$responseData = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new Exception('Error parsing JSON response: ' . json_last_error_msg());
}
//print_r($responseData);
return $responseData;
}
}
@@ -0,0 +1,86 @@
<?php
namespace classes;
use interfaces\minio_uploads_i;
use traits\minio_t;
class upload_store implements minio_uploads_i
{
use minio_t;
public function __construct()
{
self::setBucket('uploads'); // Change the bucket to 'pdfs'
}
/**
* @inheritDoc
*/
public function generatePresignedUrl(string $objectName, int $expiry = 3600): string
{
// Generate a presigned URL for the given object name with the specified expiry time
return self::getPresignedUrl($objectName, $expiry, true);
}
/**
* @inheritDoc
*/
public function isValidFileName(string $fileName): bool
{
// Check if the file name is valid
return preg_match('/^[a-zA-Z0-9_\-.]+$/', $fileName) === 1;
}
/**
* @inheritDoc
*/
public function isValidFilePath(string $filePath): bool
{
// Check if the file path is valid
return preg_match('/^[a-zA-Z0-9_\-\/.]+$/', $filePath) === 1;
}
/**
* @inheritDoc
*/
public function requireValidFileName(string $fileName): void
{
if (!$this->isValidFileName($fileName)) {
throw new \InvalidArgumentException("Invalid file name: $fileName");
}
}
/**
* @inheritDoc
*/
public function requireValidFilePath(string $filePath): void
{
if (!$this->isValidFilePath($filePath)) {
throw new \InvalidArgumentException("Invalid file path: $filePath");
}
}
public function isFileInStore(string $fileName): bool
{
// Check if the file exists in the store
return self::doesObjectExist($fileName);
}
public function download(string $file): string
{
$path = '/tmp/' . $file;
$result = self::getS3Client()->getObject([
'Bucket' => self::getBucket(),
'Key' => $file,
'SaveAs' => $path
]);
return $path;
}
public function generateDirectDownloadUrl(string $fileName): string
{
// Generate a direct download URL for the given file name
return 'https://api.truckwash.dk:4433/files/' . $fileName;
}
}
+41 -14
View File
@@ -1,24 +1,51 @@
<?php
// Get the file name from the URL
$file = $_SERVER['REQUEST_URI'];
$file = str_replace('/modules/washcertificates/output/certificates/', '', $file);
$isPDF = false;
// Check if the filetype is .pdf
if (preg_match('/\.pdf$/', $file)) {
$isPDF = true;
}
if ($isPDF) {
$file = str_replace('/modules/washcertificates/output/certificates/', '', $file);
// Download the file from minio, and send it to the client
$wash_certificate_store = new \classes\wash_certificate_store();
$wash_certificate_store = new \classes\wash_certificate_store();
// Check if the certificate exists
if (!$wash_certificate_store->isFileInStore($file)) {
echo 'Certificate not found in store';
//header('HTTP/1.1 404 Not Found');
if (!$wash_certificate_store->isFileInStore($file)) {
echo 'Certificate not found in store';
//header('HTTP/1.1 404 Not Found');
exit;
}
// Download the certificate from the store to /tmp
$certificate_path = $wash_certificate_store->download($file);
// Send the certificate to the client
header('Content-Type: application/pdf');
header('Content-Disposition: inline; filename="' . $file . '"');
header('Content-Length: ' . filesize($certificate_path));
readfile($certificate_path);
exit;
}
// Download the certificate from the store to /tmp
$certificate_path = $wash_certificate_store->download($file);
// Send the certificate to the client
header('Content-Type: application/pdf');
header('Content-Disposition: inline; filename="' . $file . '"');
header('Content-Length: ' . filesize($certificate_path));
readfile($certificate_path);
exit;
// Check if the file might be a temporary static file
if (!$isPDF) {
$uploads = new \classes\upload_store();
$file = str_replace('/files/', '', $file);
// Check if the file exists in the upload store
if (!$uploads->isFileInStore($file)) {
echo 'File not found in store';
exit;
}
// Download the file from the store to /tmp
$file_path = $uploads->download($file);
// Send the file to the client
$mime_type = mime_content_type($file_path);
header('Content-Type: ' . $mime_type);
header('Content-Disposition: inline; filename="' . basename($file_path) . '"');
header('Content-Length: ' . filesize($file_path));
readfile($file_path);
exit;
}
+7
View File
@@ -69,7 +69,9 @@ require_once 'classes/xlvask.php';
require_once 'classes/entra.php';
require_once 'classes/limble.php';
require_once 'classes/ocr_space.php';
require_once 'classes/openai.php';
require_once 'classes/image_processor.php';
require_once 'classes/upload_store.php';
/**
* Modules
@@ -134,6 +136,11 @@ if (str_contains($_SERVER['REQUEST_URI'], '.pdf')) {
require_once 'file_server.php';
exit;
}
// If the route ends with a MIME type, then require the file_server.php
if (preg_match('/\.(jpg|jpeg|png)$/', $_SERVER['REQUEST_URI'])) {
require_once 'file_server.php';
exit;
}
// Autoload all the routes
$router->auto_load_routes(WD . '/routes');
@@ -0,0 +1,69 @@
<?php
namespace interfaces;
use InvalidArgumentException;
use RuntimeException;
interface minio_uploads_i
{
/**
* Generate a pre-signed URL for uploading a file to the MinIO server.
* @param string $objectName The name of the object in the bucket.
* @param int $expiry The expiry time in seconds for the pre-signed URL.
* @return string The pre-signed URL for uploading the file.
* @note This method generates a pre-signed URL that can be used to upload a file to the specified bucket in the MinIO server.
* @note The pre-signed URL is valid for the specified expiry time.
* @throws InvalidArgumentException if the bucket name or object name is not valid.
* @throws RuntimeException if the pre-signed URL generation fails.
* @see requireValidFileName() - This method should be used to ensure that the file name is valid before proceeding with the upload.
* @see isValidFileName() - This method should be used to check if the file name is valid before proceeding with the upload.
* @see requireValidFilePath() - This method should be used to ensure that the file path is valid before proceeding with the upload.
* @see isValidFilePath() - This method should be used to check if the file path is valid before proceeding with the upload.
* @note The pre-signed URL can be used to upload files directly to the MinIO server without needing to authenticate the user.
*/
public function generatePresignedUrl(
string $objectName,
int $expiry = 3600
): string;
// Validation methods for file names and paths
/**
* Is the file name valid?
* @param string $fileName The name of the file to validate.
* @return bool True if the file name is valid, false otherwise.
* @note This method checks if the file name meets the criteria for valid uploads.
* @see requireValidFileName() - This method should be used to ensure that the file name is valid before proceeding with any operations.
*/
public function isValidFileName(string $fileName): bool;
/**
* Is the file path valid?
* @param string $filePath The path of the file to validate.
* @return bool True if the file path is valid, false otherwise.
* @note This method checks if the file path meets the criteria for valid uploads.
* @see requireValidFilePath() - This method should be used to ensure that the file path is valid before proceeding with any operations.
*/
public function isValidFilePath(string $filePath): bool;
/**
* Require the file name to be valid.
* @param string $fileName The name of the file to validate.
* @return void
* @note This method throws an exception if the file name is not valid.
* @throws InvalidArgumentException if the file name is not valid.
* @throws RuntimeException if the file name is not accessible or does not exist.
* @note This ensures that the file name meets the criteria for valid uploads.
* @see isValidFileName() - This method should be used to check if the file name is valid before proceeding with any operations.
*/
public function requireValidFileName(string $fileName): void;
/**
* Require the file path to be valid.
* @param string $filePath The path of the file to validate.
* @return void
* @note This method throws an exception if the file path is not valid.
* @note It ensures that the file path meets the criteria for valid uploads.
* @throws InvalidArgumentException if the file path is not valid.
* @throws RuntimeException if the file path is not accessible or does not exist.
* @see isValidFilePath() - This method should be used to check if the file path is valid before proceeding with any operations.
*/
public function requireValidFilePath(string $filePath): void;
}
@@ -0,0 +1,14 @@
<?php
namespace interfaces;
interface openai_i extends universal_module_i
{
/**
* Get the registration number from the image.
* @note This method is used to extract the registration number from an image url
* @param string $base64_image The base64 encoded image string
* @return mixed The extracted registration number
*/
public function lpr(string $base64_image): mixed;
}
@@ -0,0 +1,29 @@
<?php
namespace openAI\config;
use Exception;
use traits\module_config_variable;
class openAI_api_key_c
{
use module_config_variable;
/**
* @throws Exception
*/
public function __construct()
{
self::setupConfigVariable(
'openAI',
'api_key',
'string',
false,
null,
'The secret key for openAI API',
'1',
true,
''
);
}
}
@@ -0,0 +1,29 @@
<?php
namespace openAI\config;
use Exception;
use traits\module_config_variable;
class openAI_enabled_c
{
use module_config_variable;
/**
* @throws Exception
*/
public function __construct()
{
self::setupConfigVariable(
'openAI',
'enabled',
'bool',
true,
null,
'Whether openAI is enabled',
'1',
false,
'false'
);
}
}
@@ -0,0 +1,36 @@
<?php
namespace openAI;
require_once WD . '/modules/openAI/config/openAI_enabled_c.php';
require_once WD . '/modules/openAI/config/openAI_api_key_c.php';
use openAI\config\openAI_api_key_c;
use openAI\config\openAI_enabled_c;
use traits\module_config_t;
class openAI_c
{
use module_config_t;
/**
* The status of openAI, whether it is enabled or not
* @var openAI_enabled_c
*/
public openAI_enabled_c $enabled;
/**
* The API key for openAI
* @var openAI_api_key_c
*/
public openAI_api_key_c $api_key;
public function __construct()
{
$this->setupConfig('openAI');
$this->allowUpdate([
openAI_enabled_c::class,
openAI_api_key_c::class,
]);
$this->enabled = new openAI_enabled_c();
$this->api_key = new openAI_api_key_c();
}
}
@@ -525,5 +525,43 @@ class moduleConfigRoute
'modules_ocrspace_config' => 'Update ocrspace config'
]
);
/** OpenAI config > GET */
$this->get('/openai/config', function () {
global $response;
$this->requirePermission('modules_openai_config');
$user = (new authentication())->get_user();
if ($user) {
(new logs_o())->add('openai_config', 'global', 1, $user->id, 'OPENAI_CONFIG', 'Successfully fetched openai config');
$response->success(
(new \classes\openai())->config->getConfigRequest()
);
} else {
(new logs_o())->add('openai_config', 'global', 1, 0, 'OPENAI_CONFIG', 'No user found, or invalid session');
$response->error('Invalid session', 400);
}
},
[
'modules_openai_config' => 'Get openai config'
]
);
/** OpenAI config > POST */
$this->post('/openai/config', function () {
global $response;
$this->requirePermission('modules_openai_config');
$user = (new authentication())->get_user();
if ($user) {
(new logs_o())->add('openai_config', 'global', 1, $user->id, 'OPENAI_CONFIG', 'Successfully updated openai config');
$response->success(
(new \classes\openai())->config->postConfigRequest()
);
} else {
(new logs_o())->add('openai_config', 'global', 1, 0, 'OPENAI_CONFIG', 'No user found, or invalid session');
$response->error('Invalid session', 400);
}
},
[
'modules_openai_config' => 'Update openai config'
]
);
}
}
@@ -4,8 +4,10 @@ namespace routes;
use classes\authentication;
use classes\image_processor;
use classes\openai;
use classes\response;
use classes\router;
use classes\upload_store;
use objects\logs_o;
use traits\route_t;
@@ -39,10 +41,13 @@ class moduleScannerRoute
$response->error('Base64 image is required.');}
$image_processor = new image_processor();
$image_processor->set_image_base64($base64_image);
$response->success([
//'ocr' => $image_processor->process_ocr(),
'base64_image' => $base64_image
]);
$uploads = new upload_store();
$object_name = $uploads->storeTempImageFromBase64(
$base64_image
);
$openai = new openai();
//$response->success('CU61297');
$response->success($openai->lpr($object_name));
},
[
'modules_scanner_lpr' => 'License Plate Recognition',
+62
View File
@@ -0,0 +1,62 @@
<?php
namespace routes;
use classes\authentication;
use classes\openai;
use classes\upload_store;
use objects\logs_o;
use traits\route_t;
class uploadRoute
{
use route_t;
protected array $allowedFileTypes = [
'image/jpeg', 'jpg',
'image/png', 'png',
'image/gif', 'gif',
'image/webp', 'webp',
];
public function run(): void
{
$this->post('/upload/image', function () {
global $response;
// Get the uploaded file
$file = $_FILES['file'] ?? null;
if (!$file) {
$response->error('No file uploaded');
}
// Validate the file type
$fileType = mime_content_type($file['tmp_name']);
if (!in_array($fileType, $this->allowedFileTypes)) {
$response->error('Invalid file type');
}
// Validate the file size (optional, e.g., max 5MB)
if ($file['size'] > 5 * 1024 * 1024) {
$response->error('File size exceeds the limit of 5MB');
}
// Generate a unique object key for the file
$objectKey = uniqid('image_', true) . '.' . pathinfo($file['name'], PATHINFO_EXTENSION);
$uploads = new upload_store();
if(!$file = $uploads->uploadFile(
$objectKey,
$file
)) {
$response->error('Failed to upload file');
}
$response->success($uploads->getPresignedUrl($objectKey));
});
$this->get('/openai/test', function () {
global $response;
// Validate the input parameters
// Example image URL for testing
$image = 'TRUCK_WASH_EXAMPLE_5.jpg';
// Return the response
$openai = new openai();
$response->success($openai->lpr($image));
});
}
}
+79 -4
View File
@@ -144,15 +144,23 @@ trait minio_t
/**
* Generate a presigned URL for the object in the bucket (valid for 20 minutes)
* @param string $key The key of the object (file)
* @param int|null $expires The expiration time in seconds (default is not set, which means 20 minutes)
* @param bool $isUpload Whether the URL is for uploading (true) or downloading (false)
* @note If $expires is null, it defaults to 1200 seconds (20 minutes).
* @note If $isUpload is true, the URL will be for uploading the object.
* @note If $isUpload is false, the URL will be for downloading the object
* @return string
*/
public function getPresignedUrl(string $key): string
public function getPresignedUrl(string $key, ?int $expires = null, bool $isUpload = false): string
{
$command = self::getS3Client()->getCommand('GetObject', [
$commandAction = $isUpload ? 'PutObject' : 'GetObject';
$command = self::getS3Client()->getCommand($commandAction, [
'Bucket' => self::getBucket(),
'Key' => $key
'Key' => $key,
]);
return (string)self::getS3Client()->createPresignedRequest($command, '+20 minutes')->getUri();
$duration_seconds = $expires ?? 1200; // Default to 20 minutes if not specified
$duration_attribute = '+' . ($duration_seconds / 60) . ' minutes';
return (string)self::getS3Client()->createPresignedRequest($command, $duration_attribute)->getUri();
}
/**
@@ -180,4 +188,71 @@ trait minio_t
{
return self::getS3Client()->doesObjectExist(self::getBucket(), $key);
}
/**
* Store temp image file from base64 string
* @param string $base64 The base64 encoded image string (E.g. data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAA)
* @return string|bool The path to the temporary file if successful, false otherwise
*/
public function storeTempImageFromBase64(string $base64): string|bool
{
// Check if the base64 string is valid
if (empty($base64) || !preg_match('/^data:image\/(\w+);base64,/', $base64)) {
return false; // Invalid base64 string
}
// Extract the base64 data from the string
$imageData = preg_replace('/^data:image\/\w+;base64,/', '', $base64);
// Decode the base64 data
$imageData = base64_decode($imageData);
if ($imageData === false) {
return false; // Failed to decode base64 data
}
// Temporary file path
$tempFileName = uniqid('temp_image_', true) . '.' . $this->getImageExtension($base64);
$tempFilePath = '/tmp/' . $tempFileName;
// Create a temporary file
$tempFile = fopen($tempFilePath, 'wb');
if ($tempFile === false) {
return false; // Failed to create temp file
}
// Write the image data to the temporary file
if (fwrite($tempFile, $imageData) === false) {
fclose($tempFile);
return false; // Failed to write to temp file
}
fclose($tempFile);
// Upload the temporary file to the bucket
$result = self::uploadFile($tempFileName, $tempFilePath);
// Clean up the temporary file
unlink($tempFilePath);
return $result ? $tempFileName : false;
}
/**
* Get the image extension from the base64 string
* @param string $base64 The base64 encoded image string
* @return string The image extension (e.g., 'jpg', 'png')
*/
private function getImageExtension(string $base64): string
{
// Check if the base64 string contains a data URL prefix
if (preg_match('/^data:image\/(\w+);base64,/', $base64, $matches)) {
// Return the image extension
switch ($matches[1]) {
case 'png':
return 'png';
case 'gif':
return 'gif';
case 'webp':
return 'webp';
case 'bmp':
return 'bmp';
default:
return 'jpg'; // Default to jpg if unknown
}
}
// If no match, return a default extension (e.g., 'jpg')
return 'jpg';
}
}
+27 -4
View File
@@ -5,6 +5,7 @@ namespace traits;
use classes\authentication;
use classes\recaptcha;
use classes\response;
use Exception;
use objects\logs_o;
trait route_t
@@ -54,6 +55,28 @@ trait route_t
return true;
}
/**
* Require a value to be in the given array
* @param mixed $value The value to check
* @param array $array The array to check against
* @return bool
* @throws Exception If the value is not in the array
*/
public function requireInArray(mixed $value, array $array): bool
{
global $response;
// Check if the value is in the array
if (!in_array($value, $array)) {
$response->error(json_encode([
'message' => 'Unexpected value',
'expected' => $array,
'got' => $value,
'type' => gettype($value)
]), 400);
}
return true;
}
public function requireSameLength(mixed $arg1, mixed $arg2): bool
{
global $response;
@@ -214,7 +237,7 @@ trait route_t
(new logs_o())->add('global', 'global', 1, $user->id, 'PERMISSION_DENIED', 'Permission denied. Missing permission: ' . $permission);
$response->error('Permission denied. Missing permission: ' . $permission . ' for user: ' . $user->id . ' In group: ' . $user->group_id->value(), 403);
}
} catch (\Exception $e) {
} catch (Exception $e) {
$response->error($e->getMessage(), 400);
}
return true;
@@ -237,7 +260,7 @@ trait route_t
$response->error('Authentication failed. Invalid or missing token.', 401);
}
return $user->hasPermission($permission);
} catch (\Exception $e) {
} catch (Exception $e) {
$response->error($e->getMessage(), 400);
}
}
@@ -416,7 +439,7 @@ trait route_t
(new logs_o())->add('global', 'global', 1, 0, 'AUTHENTICATION_FAILED', 'Authentication failed. Invalid, or missing API key');
$response->error('Authentication failed. Invalid or missing API key.', 401);
}
} catch (\Exception $e) {
} catch (Exception $e) {
$response->error($e->getMessage(), 400);
}
return true;
@@ -450,7 +473,7 @@ trait route_t
(new logs_o())->add('global', 'global', 1, 0, 'AUTHENTICATION_FAILED', 'Authentication failed. Invalid, or missing reCAPTCHA');
$response->error('Authentication failed. Invalid or missing reCAPTCHA.', 401);
}
} catch (\Exception $e) {
} catch (Exception $e) {
$response->error($e->getMessage(), 400);
}
return true;