Add order attachments module

- Implemented CRUD operations for order attachments, including adding, listing, downloading, and deleting.
- Updated `db_object_t` and `attachments` to enhance object-attachment interactions with new methods for formatting, creating, and managing attachments.
- Added new routes (`/orders/attachments` and `/attachments/upload`) for attachment-related functionality.
- Adjusted `file_server.php` to handle temp file downloads and attachment storage.
- Improved typing and error handling across attachment helper methods and classes.
This commit is contained in:
Jeppe Bundgaard
2025-09-22 12:31:53 +02:00
parent 33a26cffc5
commit 538aa24dc7
8 changed files with 323 additions and 8 deletions
+17 -2
View File
@@ -49,12 +49,27 @@ class attachments implements attachments_i
{
return (new object_attachments_o())->select($attachment_id) ?: null;
}
/**
* Format an object_attachments_o to an attachment object
* @param object_attachments_o $object_attachments_o The object to format
* @return attachment The formatted attachment
* @throws Exception If the attachment cannot be formatted
*/
public function format(object_attachments_o $object_attachments_o): attachment
{
$data = (object)$object_attachments_o->toArray();
// If content is JSON, decode it
if (is_string($data->content) && $data->content !== '' && $data->content[0] === '{') {
$data->content = json_decode($data->content, true);
}
return (new attachment())->populate($data);
}
/**
* @inheritDoc
* @throws Exception If the attachment cannot be created
*/
public function create(string $type, int $object_id, attachment_content $attachment_content): bool
public function create(string $type, int $object_id, attachment_content $attachment_content): int|bool
{
$object_attachments_o = new object_attachments_o();
$object_attachments_o->add([
@@ -62,7 +77,7 @@ class attachments implements attachments_i
'object_id' => $object_id,
'content' => json_encode($attachment_content->toArray()), // Store as JSON
]);
return (bool)$object_attachments_o->id;
return $object_attachments_o->id ?: false;
}
/**
+34 -1
View File
@@ -2,11 +2,19 @@
// Get the file name from the URL
$file = $_SERVER['REQUEST_URI'];
$isPDF = false;
$isPDFStore = false;
$isAttachment = false;
// Check if the filetype is .pdf
if (preg_match('/\.pdf$/', $file)) {
$isPDF = true;
// Check if the file name contains "temp_file_"
if (!str_contains($file, 'temp_file_')) {
$isPDFStore = true;
} else {
$isAttachment = true;
}
}
if ($isPDF) {
if ($isPDF && $isPDFStore) {
$file = str_replace('/modules/washcertificates/output/certificates/', '', $file);
// Download the file from minio, and send it to the client
@@ -27,6 +35,29 @@ if ($isPDF) {
header('Content-Disposition: inline; filename="' . $file . '"');
header('Content-Length: ' . filesize($certificate_path));
readfile($certificate_path);
// Delete the certificate from /tmp after sending it
unlink($certificate_path);
exit;
}
// If the file is an attachment, serve it as a download
if ($isAttachment) {
$attachment_store = new \classes\attachment_store();
$file = str_replace('/files/', '', $file);
// Check if the file exists in the attachment store
if (!$attachment_store->isFileInStore($file)) {
echo 'Attachment not found in store';
exit;
}
// Download the file from the store to /tmp
$file_path = $attachment_store->download($file);
// Send the file to the client
header('Content-Type: ' . mime_content_type($file_path));
header('Content-Disposition: attachment; filename="' . basename($file_path) . '"');
header('Content-Length: ' . filesize($file_path));
readfile($file_path);
// Delete the file from /tmp after sending it
unlink($file_path);
exit;
}
@@ -47,5 +78,7 @@ if (!$isPDF) {
header('Content-Disposition: inline; filename="' . basename($file_path) . '"');
header('Content-Length: ' . filesize($file_path));
readfile($file_path);
// Delete the file from /tmp after sending it
unlink($file_path);
exit;
}
@@ -26,10 +26,10 @@ interface attachments_i
* @param string $type The type of the entity parent. (E.g. users_o, groups_o, etc.)
* @param int $object_id The ID of the entity.
* @param attachment_content $attachment_content The content of the attachment to be added.
* @return bool True on success, false on failure.
* @return int|bool The ID of the newly created attachment on success, or false on failure.
* @see attachment_content
*/
public function create(string $type, int $object_id, attachment_content $attachment_content): bool;
public function create(string $type, int $object_id, attachment_content $attachment_content): int|bool;
/**
* Delete an attachment by its ID.
* @param int $attachment_id The ID of the attachment to be deleted.
@@ -43,7 +43,7 @@ class object_attachments_o extends db
public function getObjectProperties(): void
{
$this->object_type = new object_property($this->table, $this->id, 'object_type', 'string', false);
$this->object_id = new object_property($this->table, $this->id, 'object_id', 'number', false);
$this->object_id = new object_property($this->table, $this->id, 'object_id', 'int', false);
$this->content = new object_property($this->table, $this->id, 'content', 'string', false);
$this->created_at = new object_property($this->table, $this->id, 'created_at', 'string', false);
$this->updated_at = new object_property($this->table, $this->id, 'updated_at', 'string', false);
@@ -54,4 +54,17 @@ class object_attachments_o extends db
{
//TODO: Add cache invalidation
}
public function toArray(): array
{
return [
'id' => (int)$this->id,
'object_type' => (string)$this->object_type->value(),
'object_id' => (int)$this->object_id->value(),
'content' => json_decode($this->content->value(), true),
'created_at' => $this->created_at->value(),
'updated_at' => $this->updated_at->value(),
'deleted_at' => $this->deleted_at->value(),
];
}
}
@@ -4,7 +4,9 @@ namespace routes;
use attachments\helpers\attachment;
use attachments\helpers\attachment_content;
use classes\attachment_store;
use classes\attachments;
use classes\response;
use objects\orders_o;
use traits\route_t;
@@ -23,5 +25,20 @@ class attachmentsRoute
$orders_o->addAttachment((new attachment_content())->setOther('Hello World!'));
$response->success($orders_o->listAttachments());
});
$this->post('/attachments/upload', function () {
global $response;
self::requireParameters(['base64_image']);
$base64_image = self::getParameter('base64_image');
//self::requirePermission('modules_scanner_lpr');
if (empty($base64_image)) {
$response->error('Base64 image is required.');
}
$uploads = new attachment_store();
$object_name = $uploads->storeTempImageFromBase64(
$base64_image
);
echo $object_name;
$response->success(['object_name' => $object_name]);
});
}
}
+195
View File
@@ -2,6 +2,9 @@
namespace routes;
use attachments\helpers\attachment_content;
use classes\attachment_store;
use classes\attachments;
use classes\authentication;
use classes\response;
use classes\stripe;
@@ -314,6 +317,198 @@ class ordersRoute
]
);
$this->get('/orders/attachments/download', function () {
// Require the user to be logged in
global $response;
$this->requirePermission('download_order_attachments');
// Get the user object
$user = (new authentication())->get_user();
// Check if the request was successful
if ($user) {
// Get the order ID and attachment ID from the request
self::requireParameters([
'order_id',
'attachment_id'
]);
$order_id = self::getParameter('order_id');
$attachment_id = self::getParameter('attachment_id');
if (!is_numeric($order_id) || (int)$order_id < 1) {
$response->error('Invalid order ID', 400);
}
if (!is_numeric($attachment_id) || (int)$attachment_id < 1) {
$response->error('Invalid attachment ID', 400);
}
// Get the current order
$order = (new orders_o())->getOrderById((int)$order_id);
// Check if the order exists
if (!$order->exists()) {
$response->error('Order not found', 400);
}
// Get the attachment
$attachment = $order->getAttachment((int)$attachment_id);
if (!$attachment->exists()) {
$response->error('Attachment not found', 400);
}
// Create a download link
$attachment_store = new attachment_store();
$attachments = new attachments();
$attachment_formatted = $attachments->format($attachment);
$download_link = $attachment_store->generateDirectDownloadUrl(
$attachment_formatted->content->document
);
// Log the incident
(new logs_o())->add('orders', $order->department_id->value(), 1, $user->id, 'DOWNLOAD_ORDER_ATTACHMENT', 'Successfully downloaded an attachment for an order (Order ID: ' . $order_id . ', Attachment ID: ' . $attachment_id . ')');
// Return the download link
$response->success(['download_link' => $download_link]);
} else {
// Log the incident
(new logs_o())->add('orders', 'global', 1, 0, 'DOWNLOAD_ORDER_ATTACHMENT', 'No user found, or invalid session');
// Return an error
$response->error('Invalid session', 400);
}
},
[
'download_order_attachments' => 'Download attachments for an order'
]
);
$this->get('/orders/attachments', function () {
// Require the user to be logged in
global $response;
$this->requirePermission('list_order_attachments');
// Get the user object
$user = (new authentication())->get_user();
// Check if the request was successful
if ($user) {
// Get the order ID from the request
self::requireParameters([
'id'
]);
$order_id = self::getParameter('id');
if (!is_numeric($order_id) || (int)$order_id < 1) {
$response->error('Invalid order ID', 400);
}
// Get the current order
$order = (new orders_o())->getOrderById((int)$order_id);
// Check if the order exists
if (!$order->exists()) {
$response->error('Order not found', 400);
}
// Get the attachments
$attachments = $order->listAttachments();
// Log the incident
(new logs_o())->add('orders', $order->department_id->value(), 1, $user->id, 'LIST_ORDER_ATTACHMENTS', 'Successfully listed attachments for an order (ID: ' . $order_id . ')');
// Return the attachments
$response->success($attachments);
} else {
// Log the incident
(new logs_o())->add('orders', 'global', 1, 0, 'LIST_ORDER_ATTACHMENTS', 'No user found, or invalid session');
// Return an error
$response->error('Invalid session', 400);
}
},
[
'list_order_attachments' => 'List attachments for an order'
]
);
$this->post('/orders/attachments/upload', function () {
// Require the user to be logged in
global $response;
$this->requirePermission('add_order_attachments');
// Get the user object
$user = (new authentication())->get_user();
// Check if the request was successful
if ($user) {
// Check if the order ID and file are set
$this->requireParameters(['order_id', 'base64_file', 'file_name']);
$order_id = (int)$this->getParameter('order_id');
if (!is_numeric($order_id) || (int)$order_id < 1) {
$response->error('Invalid order ID', 400);
}
// Get the current order
$order = (new orders_o())->getOrderById((int)$order_id);
// Check if the order exists
if (!$order->exists()) {
$response->error('Order not found', 400);
}
// Get the base64 file
$base64_file = (string)$this->getParameter('base64_file');
$attachment_store = new attachment_store();
// Determine the file extension from the file name
$file_name = (string)$this->getParameter('file_name');
$extension = pathinfo($file_name, PATHINFO_EXTENSION);
$object_name = $attachment_store->storeTempFileFromBase64(
$base64_file,
$extension
);
if ($object_name === false) {
$response->error('Failed to store the attachment file.', 500);
}
$object_attachment = $order->addAttachment((new attachment_content())->setDocument($object_name)->setOther((string)self::getParameter('file_name')));
// Log the incident
(new logs_o())->add('orders', $order->department_id->value(), 1, $user->id, 'ADD_ORDER_ATTACHMENT', 'Successfully added an attachment for an order (Order ID: ' . $order_id . ')');
// Return a success message
$attachments = new attachments();
$response->success($attachments->format($attachments->get($object_attachment->id)));
} else {
// Log the incident
(new logs_o())->add('orders', 'global', 1, 0, 'ADD_ORDER_ATTACHMENT', 'No user found, or invalid session');
// Return an error
$response->error('Invalid session', 400);
}
},
[
'add_order_attachments' => 'Add attachments for an order'
]
);
$this->delete('/orders/attachments', function () {
// Require the user to be logged in
global $response;
$this->requirePermission('delete_order_attachments');
// Get the user object
$user = (new authentication())->get_user();
// Check if the request was successful
if ($user) {
// Get the order ID and attachment ID from the request
self::requireParameters([
'order_id',
'attachment_id'
]);
$order_id = self::getParameter('order_id');
$attachment_id = self::getParameter('attachment_id');
if (!is_numeric($order_id) || (int)$order_id < 1) {
$response->error('Invalid order ID', 400);
}
if (!is_numeric($attachment_id) || (int)$attachment_id < 1) {
$response->error('Invalid attachment ID', 400);
}
// Get the current order
$order = (new orders_o())->getOrderById((int)$order_id);
// Check if the order exists
if (!$order->exists()) {
$response->error('Order not found', 400);
}
// Delete the attachment
$order->removeAttachment((int)$attachment_id);
// Log the incident
(new logs_o())->add('orders', $order->department_id->value(), 1, $user->id, 'DELETE_ORDER_ATTACHMENT', 'Successfully deleted an attachment for an order (Order ID: ' . $order_id . ', Attachment ID: ' . $attachment_id . ')');
// Return a success message
$response->success(['message' => 'Attachment deleted successfully']);
} else {
// Log the incident
(new logs_o())->add('orders', 'global', 1, 0, 'DELETE_ORDER_ATTACHMENT', 'No user found, or invalid session');
// Return an error
$response->error('Invalid session', 400);
}
},
[
'delete_order_attachments' => 'Delete attachments for an order'
]
);
$this->post('/orders/mark_as_completed', function () {
// Require the user to be logged in
global $response;
+5 -2
View File
@@ -2,6 +2,7 @@
namespace traits;
use attachments\helpers\attachment;
use attachments\helpers\attachment_content;
use classes\attachments;
use classes\db;
@@ -1151,14 +1152,16 @@ trait db_object_t
/**
* Add an attachment to the (current) object
* @param attachment_content $content The content of the attachment
* @return object_attachments_o The attachment object
* @throws Exception If the object is not selected, it throws an exception
*/
public function addAttachment(attachment_content $content): void
public function addAttachment(attachment_content $content): object_attachments_o
{
self::requireSelected();
// Add the attachment to the object
$attachment = new attachments();
$attachment->create($this->table, $this->id, $content);
$id = $attachment->create($this->table, $this->id, $content);
return (new object_attachments_o())->select($id);
}
/**
* List attachments of the (current) object
+39
View File
@@ -189,6 +189,45 @@ trait minio_t
return self::getS3Client()->doesObjectExist(self::getBucket(), $key);
}
/**
* Store temp file from base64 string
* @param string $base64 The base64 encoded string
* @param string $fileExtension The file extension (e.g., 'txt', 'jpg')
* @return string|bool The path to the temporary file if successful, false otherwise
*/
public function storeTempFileFromBase64(string $base64, string $fileExtension = 'txt'): string|bool
{
// Check if the base64 string is valid
if (empty($base64) || !preg_match('/^data:.*;base64,/', $base64)) {
return false; // Invalid base64 string
}
// Decode the base64 data
$fileData = base64_decode(preg_replace('/^data:.*;base64,/', '', $base64));
if ($fileData === false) {
return false; // Failed to decode base64 data
}
// Temporary file path
$tempFileName = uniqid('temp_file_', true) . '.' . $fileExtension;
$tempFilePath = '/tmp/' . $tempFileName;
// Create a temporary file
$tempFile = fopen($tempFilePath, 'wb');
if ($tempFile === false) {
return false; // Failed to create temp file
}
// Write the file data to the temporary file
if (fwrite($tempFile, $fileData) === 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;
}
/**
* Store temp image file from base64 string
* @param string $base64 The base64 encoded image string (E.g. data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAA)