mirror of
https://github.com/itflow-org/itflow
synced 2026-08-04 22:57:14 +00:00
Feature allow agents to attach files to tickets in the app
This commit is contained in:
@@ -20,7 +20,7 @@ ob_start();
|
||||
<span>×</span>
|
||||
</button>
|
||||
</div>
|
||||
<form action="post.php" method="post" autocomplete="off">
|
||||
<form action="post.php" method="post" enctype="multipart/form-data" autocomplete="off">
|
||||
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
|
||||
<!-- Hidden/System fields -->
|
||||
<?php if ($client_id) { ?>
|
||||
@@ -203,6 +203,11 @@ ob_start();
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
<div class="form-group">
|
||||
<label><i class="fa fa-fw fa-paperclip mr-1"></i>Attachments</label>
|
||||
<input type="file" class="form-control-file" name="attachments[]" multiple accept=".jpg, .jpeg, .gif, .png, .webp, .pdf, .txt, .md, .doc, .docx, .odt, .csv, .xls, .xlsx, .ods, .pptx, .odp, .zip, .tar, .gz, .xml, .msg, .json, .wav, .mp3, .ogg, .mov, .mp4, .av1, .ovpn">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade" id="pills-add-tasks">
|
||||
|
||||
@@ -89,6 +89,9 @@ if (isset($_POST['add_ticket'])) {
|
||||
addTasksFromTicketTemplate($ticket_id, $ticket_template_id);
|
||||
}
|
||||
|
||||
// Store any attached files against the ticket itself
|
||||
saveTicketAttachments($ticket_id, null);
|
||||
|
||||
// Add Watchers
|
||||
if (isset($_POST['watchers'])) {
|
||||
foreach ($_POST['watchers'] as $watcher) {
|
||||
@@ -1957,12 +1960,62 @@ if (isset($_POST['add_ticket_reply'])) {
|
||||
flashAlert("Ticket updated");
|
||||
}
|
||||
|
||||
// Store any attached files. They hang off the reply when there is one, and off
|
||||
// the ticket itself when a file was uploaded without any accompanying text.
|
||||
saveTicketAttachments($ticket_id, $ticket_reply_id ?: null);
|
||||
|
||||
logAudit("Ticket", "Reply", "$session_name replied to ticket $ticket_prefix$ticket_number - $ticket_subject and was a $ticket_reply_type reply", $client_id, $ticket_id);
|
||||
|
||||
redirect();
|
||||
|
||||
}
|
||||
|
||||
if (isset($_GET['delete_ticket_attachment'])) {
|
||||
|
||||
validateCSRFToken();
|
||||
|
||||
enforceUserPermission('module_support', 3);
|
||||
|
||||
$attachment_id = intval($_GET['delete_ticket_attachment']);
|
||||
|
||||
$sql = mysqli_query($mysqli, "SELECT ticket_attachment_name, ticket_attachment_reference_name, ticket_attachment_ticket_id FROM ticket_attachments WHERE ticket_attachment_id = $attachment_id LIMIT 1");
|
||||
|
||||
if (mysqli_num_rows($sql) !== 1) {
|
||||
flashAlert("Attachment not found", 'error');
|
||||
redirect();
|
||||
}
|
||||
|
||||
$row = mysqli_fetch_assoc($sql);
|
||||
$attachment_name = escapeSql($row['ticket_attachment_name']);
|
||||
$attachment_reference_name = $row['ticket_attachment_reference_name'];
|
||||
$ticket_id = intval($row['ticket_attachment_ticket_id']);
|
||||
|
||||
$client_id = intval(getFieldById('tickets', $ticket_id, 'ticket_client_id'));
|
||||
|
||||
// Don't Enforce Client Access if Ticket doesn't have an assigned client
|
||||
if ($client_id) {
|
||||
enforceClientAccess();
|
||||
}
|
||||
|
||||
// Resolve the path and confirm it is still inside uploads before unlinking,
|
||||
// the same guard the download endpoint applies
|
||||
$uploads_base = realpath(__DIR__ . "/../../uploads");
|
||||
$file_path = realpath(__DIR__ . "/../../uploads/tickets/$ticket_id/$attachment_reference_name");
|
||||
|
||||
if ($file_path !== false && $uploads_base !== false && strpos($file_path, $uploads_base) === 0) {
|
||||
unlink($file_path);
|
||||
}
|
||||
|
||||
mysqli_query($mysqli, "DELETE FROM ticket_attachments WHERE ticket_attachment_id = $attachment_id");
|
||||
|
||||
logAudit("Ticket", "Delete", "$session_name deleted ticket attachment $attachment_name", $client_id, $ticket_id);
|
||||
|
||||
flashAlert("Attachment <strong>$attachment_name</strong> deleted", 'error');
|
||||
|
||||
redirect();
|
||||
|
||||
}
|
||||
|
||||
if (isset($_POST['edit_ticket_reply'])) {
|
||||
|
||||
validateCSRFToken();
|
||||
|
||||
@@ -596,7 +596,7 @@ if (isset($_GET['ticket_id'])) {
|
||||
<tr>
|
||||
<td><i class='fas fa-fw fa-paperclip text-secondary mr-1'></i><?= $name ?></td>
|
||||
<td>
|
||||
<a target='_blank' class='mr-1 ml-1' href='ticket_attachment.php?attachment_id=<?= $ticket_attachment_id; ?>&action=view'>[View]</a><a href='ticket_attachment.php?attachment_id=<?= $ticket_attachment_id; ?>'>[Download]</a>
|
||||
<a target='_blank' class='mr-1 ml-1' href='ticket_attachment.php?attachment_id=<?= $ticket_attachment_id; ?>&action=view'>[View]</a><a href='ticket_attachment.php?attachment_id=<?= $ticket_attachment_id; ?>'>[Download]</a><?php if (lookupUserPermission("module_support") >= 3) { ?><a class='confirm-link ml-1 text-danger' href='post.php?delete_ticket_attachment=<?= $ticket_attachment_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>'>[Delete]</a><?php } ?>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -611,7 +611,7 @@ if (isset($_GET['ticket_id'])) {
|
||||
<!-- Only show ticket reply modal if status is not closed -->
|
||||
<?php if (lookupUserPermission("module_support") >= 2 && empty($ticket_resolved_at) && empty($ticket_closed_at)) { ?>
|
||||
|
||||
<form action="post.php" method="post" autocomplete="off">
|
||||
<form action="post.php" method="post" enctype="multipart/form-data" autocomplete="off">
|
||||
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
|
||||
<input type="hidden" name="ticket_id" id="ticket_id" value="<?= $ticket_id ?>">
|
||||
|
||||
@@ -642,6 +642,12 @@ if (isset($_GET['ticket_id'])) {
|
||||
</textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="text-muted small mb-1"><i class="fa fa-fw fa-paperclip mr-1"></i>Attachments</label>
|
||||
<input type="file" class="form-control-file" name="attachments[]" multiple accept=".jpg, .jpeg, .gif, .png, .webp, .pdf, .txt, .md, .doc, .docx, .odt, .csv, .xls, .xlsx, .ods, .pptx, .odp, .zip, .tar, .gz, .xml, .msg, .json, .wav, .mp3, .ogg, .mov, .mp4, .av1, .ovpn">
|
||||
<small class="form-text text-muted">Stored against this reply, or against the ticket itself if you submit without any text. Attachments are not included in the emailed copy of a public reply.</small>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="col-md-3">
|
||||
<div class="form-group">
|
||||
@@ -830,7 +836,7 @@ if (isset($_GET['ticket_id'])) {
|
||||
<tr>
|
||||
<td><i class='fas fa-fw fa-paperclip text-secondary mr-1'></i><?= $name ?></td>
|
||||
<td>
|
||||
<a target='_blank' class='mr-1 ml-1' href='ticket_attachment.php?attachment_id=<?= $ticket_attachment_id; ?>&action=view'>[View]</a><a href='ticket_attachment.php?attachment_id=<?= $ticket_attachment_id; ?>'>[Download]</a>
|
||||
<a target='_blank' class='mr-1 ml-1' href='ticket_attachment.php?attachment_id=<?= $ticket_attachment_id; ?>&action=view'>[View]</a><a href='ticket_attachment.php?attachment_id=<?= $ticket_attachment_id; ?>'>[Download]</a><?php if (lookupUserPermission("module_support") >= 3) { ?><a class='confirm-link ml-1 text-danger' href='post.php?delete_ticket_attachment=<?= $ticket_attachment_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>'>[Delete]</a><?php } ?>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
||||
@@ -142,42 +142,8 @@ if (isset($_POST['add_ticket_comment'])) {
|
||||
|
||||
}
|
||||
|
||||
// Store any attached any files
|
||||
if (!empty($_FILES)) {
|
||||
|
||||
// Define & create directories, as required
|
||||
mkdirMissing('../uploads/tickets/');
|
||||
$upload_file_dir = "../uploads/tickets/" . $ticket_id . "/";
|
||||
mkdirMissing($upload_file_dir);
|
||||
|
||||
for ($i = 0; $i < count($_FILES['file']['name']); $i++) {
|
||||
// Extract file details for this iteration
|
||||
$single_file = [
|
||||
'name' => $_FILES['file']['name'][$i],
|
||||
'type' => $_FILES['file']['type'][$i],
|
||||
'tmp_name' => $_FILES['file']['tmp_name'][$i],
|
||||
'error' => $_FILES['file']['error'][$i],
|
||||
'size' => $_FILES['file']['size'][$i]
|
||||
];
|
||||
|
||||
if ($ticket_attachment_ref_name = checkFileUpload($single_file, array('jpg', 'jpeg', 'gif', 'png', 'webp', 'pdf', 'txt', 'md', 'doc', 'docx', 'odt', 'csv', 'xls', 'xlsx', 'ods', 'pptx', 'odp', 'zip', 'tar', 'gz', 'xml', 'msg', 'json', 'wav', 'mp3', 'ogg', 'mov', 'mp4', 'av1', 'ovpn'))) {
|
||||
|
||||
$file_tmp_path = $_FILES['file']['tmp_name'][$i];
|
||||
|
||||
$file_name = escapeSql($_FILES['file']['name'][$i]);
|
||||
$extarr = explode('.', $_FILES['file']['name'][$i]);
|
||||
$file_extension = escapeSql(strtolower(end($extarr)));
|
||||
|
||||
// Define destination file path
|
||||
$dest_path = $upload_file_dir . $ticket_attachment_ref_name;
|
||||
|
||||
move_uploaded_file($file_tmp_path, $dest_path);
|
||||
|
||||
mysqli_query($mysqli, "INSERT INTO ticket_attachments SET ticket_attachment_name = '$file_name', ticket_attachment_reference_name = '$ticket_attachment_ref_name', ticket_attachment_reply_id = $ticket_reply_id, ticket_attachment_ticket_id = $ticket_id");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
// Store any attached files against this reply
|
||||
saveTicketAttachments($ticket_id, $ticket_reply_id, 'file');
|
||||
|
||||
// Custom action/notif handler
|
||||
triggerCustomAction('ticket_reply_client', $ticket_id);
|
||||
|
||||
@@ -166,3 +166,86 @@ function cleanupUnusedImages(string $html, string $folderFsPath, string $folderW
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Ticket attachment uploads
|
||||
*
|
||||
* Shared by the agent ticket page, the new ticket modal and the client portal
|
||||
* reply form, so the allowed extension list has one definition rather than one
|
||||
* per caller.
|
||||
*
|
||||
* Files land in uploads/tickets/<ticket id>/ under an unguessable reference name
|
||||
* and are only ever served back through the ticket_attachment.php endpoints,
|
||||
* which re-check permissions and force a safe Content-Type.
|
||||
*
|
||||
* Pass $reply_id to attach to a specific reply, or null to attach to the ticket
|
||||
* itself - the ticket page reads reply_id IS NULL as "belongs to the ticket".
|
||||
*
|
||||
* Returns the number of files stored. Anything the extension allow-list or
|
||||
* checkFileUpload() rejects is skipped silently, as it always has been.
|
||||
*/
|
||||
function saveTicketAttachments($ticket_id, $reply_id = null, $field_name = 'attachments') {
|
||||
|
||||
global $mysqli;
|
||||
|
||||
$ticket_id = intval($ticket_id);
|
||||
|
||||
if (!$ticket_id || empty($_FILES[$field_name]) || !isset($_FILES[$field_name]['name'])) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$allowed_extensions = array(
|
||||
'jpg', 'jpeg', 'gif', 'png', 'webp', 'pdf', 'txt', 'md', 'doc', 'docx',
|
||||
'odt', 'csv', 'xls', 'xlsx', 'ods', 'pptx', 'odp', 'zip', 'tar', 'gz',
|
||||
'xml', 'msg', 'json', 'wav', 'mp3', 'ogg', 'mov', 'mp4', 'av1', 'ovpn'
|
||||
);
|
||||
|
||||
// A single-file input posts scalars, a multiple one posts arrays - normalize
|
||||
$names = $_FILES[$field_name]['name'];
|
||||
if (!is_array($names)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
mkdirMissing('../uploads/tickets/');
|
||||
$upload_file_dir = "../uploads/tickets/" . $ticket_id . "/";
|
||||
mkdirMissing($upload_file_dir);
|
||||
|
||||
if ($reply_id === null) {
|
||||
$reply_id_sql = 'NULL';
|
||||
} else {
|
||||
$reply_id_sql = intval($reply_id);
|
||||
}
|
||||
|
||||
$files_stored = 0;
|
||||
|
||||
for ($i = 0; $i < count($names); $i++) {
|
||||
|
||||
$single_file = [
|
||||
'name' => $_FILES[$field_name]['name'][$i],
|
||||
'type' => $_FILES[$field_name]['type'][$i],
|
||||
'tmp_name' => $_FILES[$field_name]['tmp_name'][$i],
|
||||
'error' => $_FILES[$field_name]['error'][$i],
|
||||
'size' => $_FILES[$field_name]['size'][$i]
|
||||
];
|
||||
|
||||
$attachment_reference_name = checkFileUpload($single_file, $allowed_extensions);
|
||||
|
||||
if (!$attachment_reference_name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$destination_path = $upload_file_dir . $attachment_reference_name;
|
||||
|
||||
if (!move_uploaded_file($single_file['tmp_name'], $destination_path)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$attachment_name = escapeSql($single_file['name']);
|
||||
|
||||
mysqli_query($mysqli, "INSERT INTO ticket_attachments SET ticket_attachment_name = '$attachment_name', ticket_attachment_reference_name = '$attachment_reference_name', ticket_attachment_reply_id = $reply_id_sql, ticket_attachment_ticket_id = $ticket_id");
|
||||
|
||||
$files_stored++;
|
||||
}
|
||||
|
||||
return $files_stored;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user