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 '