From bd7f67aa5b4d3b9c24e021605af80fa812dce44c Mon Sep 17 00:00:00 2001 From: johnnyq Date: Fri, 28 Aug 2026 17:33:04 -0400 Subject: [PATCH] Enhancement: Files: Document and Files are easily distinguashable now with the option to choose all docs or just files. Thumbnails view now shows all files, Previews now available for documents, pdfs, txts etc, List view and Thumbnail view now retain in search param when navigating folders --- agent/ajax.php | 48 +++ agent/file.php | 9 +- agent/files.php | 573 +++++++++++++++++++++++++++----- agent/modals/file/file_view.php | 59 +++- agent/ticket_attachment.php | 9 +- client/file.php | 9 +- client/functions.php | 31 -- client/ticket_attachment.php | 9 +- functions/files.php | 170 ++++++++++ 9 files changed, 758 insertions(+), 159 deletions(-) diff --git a/agent/ajax.php b/agent/ajax.php index 799c08f9c..558a8daa9 100644 --- a/agent/ajax.php +++ b/agent/ajax.php @@ -413,6 +413,54 @@ if (isset($_GET['get_canned_response'])) { } +/* + * Returns a document's rendered content for the file previewer on agent/files.php. + * + * The previewer shows files by pointing an iframe at file.php, but a document is + * rows in a table, not bytes on disk. Rather than embedding every document's + * HTML in the page payload - a folder of long documents would be megabytes of + * JSON on a page that shows two dozen tiles - it is fetched when the document is + * actually opened. + * + * Purified here rather than at save time, the same way canned responses above + * and agent/document.php do it. + */ +if (isset($_GET['get_document_content'])) { + enforceUserPermission('module_support'); + + $document_id = intval($_GET['get_document_content']); + + $document_sql = mysqli_query($mysqli, "SELECT document_client_id, document_content, document_name + FROM documents WHERE document_id = $document_id LIMIT 1"); + + $document_row = mysqli_fetch_assoc($document_sql); + + $response = []; + + if ($document_row) { + // Scope the fetch to the client the document belongs to - this endpoint + // takes an id straight from the query string + $client_id = intval($document_row['document_client_id']); + enforceClientAccess(); + + require_once "../libs/htmlpurifier/HTMLPurifier.standalone.php"; + + $document_purifier_config = HTMLPurifier_Config::createDefault(); + $document_purifier_config->set('Cache.DefinitionImpl', null); + $document_purifier_config->set('URI.AllowedSchemes', ['data' => true, 'src' => true, 'http' => true, 'https' => true]); + $document_purifier = new HTMLPurifier($document_purifier_config); + + $response['name'] = $document_row['document_name']; + $response['content'] = $document_purifier->purify($document_row['document_content']); + } else { + $response['name'] = ''; + $response['content'] = ''; + } + + echo json_encode($response); + +} + /* * Returns ordered list of active contacts for a specified client */ diff --git a/agent/file.php b/agent/file.php index 455a64f7f..edc7fc452 100644 --- a/agent/file.php +++ b/agent/file.php @@ -54,14 +54,7 @@ enforceClientAccess(); // MIME types that are safe to render inline in the browser // Everything else (esp. HTML/SVG - stored XSS risk) falls back to download -$inline_allowed_mime_types = array( - "application/pdf", - "image/png", - "image/jpeg", - "image/gif", - "image/webp", - "text/plain" -); +$inline_allowed_mime_types = getInlineViewableMimeTypes(); if ($disposition == "inline" && !in_array($file_mime_type, $inline_allowed_mime_types, true)) { $disposition = "attachment"; diff --git a/agent/files.php b/agent/files.php index 7265c98d5..8d7340cba 100644 --- a/agent/files.php +++ b/agent/files.php @@ -16,7 +16,40 @@ if (!empty($_GET['folder_id'])) { // Folder ID (used in forms/etc) $get_folder_id = $folder_id; -// View Mode -- 0 List, 1 Thumbnail (thumbnail = files only) +/* + * Type filter -- '' all, 'file' uploads only, 'document' created documents only. + * + * Whitelisted rather than trusted: it decides which of the two queries below run + * at all, so an unexpected value should mean "everything", not an empty page. + * + * Like the view mode, it rides along on every folder link - a filter that + * silently clears itself the moment you open a folder is worse than no filter. + */ +$type_filter = $_GET['type'] ?? ''; +if (!in_array($type_filter, ['file', 'document'], true)) { + $type_filter = ''; +} + +/* + * Links for the filter buttons. Built from a copy with 'type' removed so the + * new value does not land next to the old one, and with 'page' removed because + * switching filter while on page 4 of the files would otherwise open page 4 of + * a shorter list and show nothing. + */ +$type_get_copy = $_GET; +unset($type_get_copy['type'], $type_get_copy['page'], $type_get_copy['sort'], $type_get_copy['order']); +$url_query_strings_type = http_build_query($type_get_copy); +if ($url_query_strings_type !== '') { + $url_query_strings_type .= '&'; +} + +/* + * View Mode -- 0 List, 1 Grid + * + * Carried on every folder link below. Without it, opening a folder dropped the + * parameter and the page fell back to list view, so picking grid only lasted + * until the next click. + */ if (!empty($_GET['view'])) { $view = intval($_GET['view']); } else { @@ -80,13 +113,13 @@ function isAncestorFolder($folder_id, $current_folder_id, $client_id) { } function displayFolders($parent_folder_id, $client_id, $indent = 0, $render_root = false) { - global $mysqli, $get_folder_id, $session_user_role, $archive_query, $archived, $num_root_items, $folders_expanded; + global $mysqli, $get_folder_id, $session_user_role, $archive_query, $archived, $num_root_items, $folders_expanded, $view, $type_filter; // Always render root (only once) if ($parent_folder_id == 0 && $indent == 0) { echo '