mirror of
https://github.com/itflow-org/itflow
synced 2026-09-01 20:35:12 +00:00
1393 lines
76 KiB
PHP
1393 lines
76 KiB
PHP
<?php
|
|
|
|
// Unified sort: "name" is logical field, not DB column
|
|
$sort = "name";
|
|
$order = "ASC";
|
|
|
|
require_once "includes/inc_all_client.php";
|
|
|
|
// Folder
|
|
if (!empty($_GET['folder_id'])) {
|
|
$folder_id = intval($_GET['folder_id']);
|
|
} else {
|
|
$folder_id = 0;
|
|
}
|
|
|
|
// Folder ID (used in forms/etc)
|
|
$get_folder_id = $folder_id;
|
|
|
|
/*
|
|
* 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 {
|
|
$view = 0;
|
|
}
|
|
|
|
// Folder tree expanded state: 1 = expand all, 0 = collapsed (default)
|
|
if (isset($_GET['folders_expanded'])) {
|
|
$folders_expanded = intval($_GET['folders_expanded']);
|
|
} else {
|
|
$folders_expanded = 0;
|
|
}
|
|
|
|
if (!isset($q)) {
|
|
$q = '';
|
|
}
|
|
|
|
// ---------------------------------------------
|
|
// Breadcrumbs: build full folder path
|
|
// ---------------------------------------------
|
|
$folder_path = [];
|
|
$breadcrumb_folder_id = $get_folder_id;
|
|
|
|
while ($breadcrumb_folder_id > 0) {
|
|
$sql_folder = mysqli_query($mysqli, "SELECT folder_name, parent_folder FROM folders WHERE folder_id = $breadcrumb_folder_id AND folder_client_id = $client_id");
|
|
if ($row_folder = mysqli_fetch_assoc($sql_folder)) {
|
|
$folder_name = escapeHtml($row_folder['folder_name']);
|
|
$parent_folder = intval($row_folder['parent_folder']);
|
|
|
|
array_unshift($folder_path, [
|
|
'folder_id' => $breadcrumb_folder_id,
|
|
'folder_name' => $folder_name
|
|
]);
|
|
|
|
$breadcrumb_folder_id = $parent_folder;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------
|
|
// Helper: unified folder tree (no folder_location)
|
|
// ---------------------------------------------
|
|
function isAncestorFolder($folder_id, $current_folder_id, $client_id) {
|
|
global $mysqli;
|
|
|
|
if ($current_folder_id == 0) {
|
|
return false;
|
|
}
|
|
if ($current_folder_id == $folder_id) {
|
|
return true;
|
|
}
|
|
|
|
$result = mysqli_query($mysqli, "SELECT parent_folder FROM folders WHERE folder_id = $current_folder_id AND folder_client_id = $client_id");
|
|
if ($row = mysqli_fetch_assoc($result)) {
|
|
$parent_folder_id = intval($row['parent_folder']);
|
|
return isAncestorFolder($folder_id, $parent_folder_id, $client_id);
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
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, $view, $type_filter;
|
|
|
|
// Always render root (only once)
|
|
if ($parent_folder_id == 0 && $indent == 0) {
|
|
echo '<li class="nav-item">';
|
|
echo '<a class="nav-link ' . ($get_folder_id == 0 ? 'active' : '') . '"';
|
|
echo ' href="?client_id=' . $client_id . '&folder_id=0&view=' . $view . '&type=' . $type_filter . '&archived=' . $archived . '&folders_expanded=' . $folders_expanded . '">';
|
|
echo '/';
|
|
|
|
if ($num_root_items > 0) {
|
|
echo "<span class='badge rounded-pill bg-dark float-end mt-1'>$num_root_items</span>";
|
|
}
|
|
|
|
echo '</a>';
|
|
echo '</li>';
|
|
}
|
|
|
|
$sql_folders = mysqli_query(
|
|
$mysqli,
|
|
"SELECT folder_id, folder_name FROM folders
|
|
WHERE parent_folder = $parent_folder_id
|
|
AND folder_client_id = $client_id
|
|
ORDER BY folder_name ASC"
|
|
);
|
|
|
|
while ($row = mysqli_fetch_assoc($sql_folders)) {
|
|
$folder_id = intval($row['folder_id']);
|
|
$folder_name = escapeHtml($row['folder_name']);
|
|
|
|
$row_files = mysqli_fetch_assoc(mysqli_query(
|
|
$mysqli,
|
|
"SELECT COUNT('file_id') AS num
|
|
FROM files
|
|
WHERE file_folder_id = $folder_id
|
|
AND file_client_id = $client_id
|
|
AND file_$archive_query"
|
|
));
|
|
$num_files = intval($row_files['num']);
|
|
|
|
$row_docs = mysqli_fetch_assoc(mysqli_query(
|
|
$mysqli,
|
|
"SELECT COUNT('document_id') AS num
|
|
FROM documents
|
|
WHERE document_folder_id = $folder_id
|
|
AND document_client_id = $client_id
|
|
AND document_$archive_query"
|
|
));
|
|
$num_docs = intval($row_docs['num']);
|
|
|
|
$num_total = $num_files + $num_docs;
|
|
|
|
$subfolder_result = mysqli_query(
|
|
$mysqli,
|
|
"SELECT COUNT(*) AS count
|
|
FROM folders
|
|
WHERE parent_folder = $folder_id
|
|
AND folder_client_id = $client_id"
|
|
);
|
|
$subfolder_count = intval(mysqli_fetch_assoc($subfolder_result)['count']);
|
|
|
|
// Active or ancestor of active folder = on active path
|
|
$on_active_path = ($get_folder_id == $folder_id) || isAncestorFolder($folder_id, $get_folder_id, $client_id);
|
|
|
|
// Option C: indent with padding (no AdminLTE sidebar CSS required)
|
|
// Tune these numbers if you want tighter/looser indent
|
|
$indent_px = 12 * $indent; // 12px per level
|
|
|
|
echo '<li class="nav-item">';
|
|
echo '<div class="row">';
|
|
echo '<div class="col-10">';
|
|
|
|
echo '<a class="nav-link ' . ($get_folder_id == $folder_id ? 'active' : '') . '"';
|
|
echo ' style="padding-left: ' . (12 + $indent_px) . 'px;"';
|
|
echo ' href="?client_id=' . $client_id . '&folder_id=' . $folder_id . '&view=' . $view . '&type=' . $type_filter . '&archived=' . $archived . '&folders_expanded=' . $folders_expanded . '">';
|
|
|
|
if ($on_active_path) {
|
|
echo '<i class="fas fa-fw fa-folder-open"></i>';
|
|
} else {
|
|
echo '<i class="fas fa-fw fa-folder"></i>';
|
|
}
|
|
|
|
echo ' ' . $folder_name;
|
|
|
|
if ($subfolder_count > 0) {
|
|
$is_expanded = $folders_expanded || $on_active_path;
|
|
|
|
echo '<i class="fas fa-chevron-' . ($is_expanded ? 'down' : 'right') . ' text-muted ms-2"></i>';
|
|
}
|
|
|
|
if ($num_total > 0) {
|
|
echo "<span class='badge rounded-pill bg-dark float-end mt-1'>$num_total</span>";
|
|
}
|
|
|
|
echo '</a>';
|
|
echo '</div>'; // col-10
|
|
|
|
echo '<div class="col-2">';
|
|
?>
|
|
<div class="dropdown">
|
|
<button class="btn btn-sm" type="button" data-bs-toggle="dropdown">
|
|
<i class="fas fa-ellipsis-v"></i>
|
|
</button>
|
|
<div class="dropdown-menu">
|
|
<a class="dropdown-item ajax-modal" href="#"
|
|
data-modal-url="modals/folder/folder_rename.php?id=<?= $folder_id ?>">
|
|
<i class="fas fa-fw fa-edit me-2"></i>Rename
|
|
</a>
|
|
<?php if ($session_user_role == 3 && $num_total == 0 && $subfolder_count == 0) { ?>
|
|
<div class="dropdown-divider"></div>
|
|
<a class="dropdown-item text-danger text-bold confirm-link" href="post.php?delete_folder=<?= $folder_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
|
|
<i class="fas fa-fw fa-trash me-2"></i>Delete
|
|
</a>
|
|
<?php } ?>
|
|
</div>
|
|
</div>
|
|
<?php
|
|
echo '</div>'; // col-2
|
|
echo '</div>'; // row
|
|
|
|
// Collapsed by default: ONLY render children if folder is on active path
|
|
if ($subfolder_count > 0 && ($folders_expanded || $on_active_path)) {
|
|
echo '<ul class="nav nav-pills flex-column bg-light">';
|
|
displayFolders($folder_id, $client_id, $indent + 1);
|
|
echo '</ul>';
|
|
}
|
|
|
|
echo '</li>';
|
|
}
|
|
}
|
|
|
|
|
|
// ---------------------------------------------
|
|
// DATA LOAD
|
|
// view=1 (grid) - every file in the folder, thumbnail or file-type icon
|
|
// view=0 (list) loads ALL files+documents, merges, sorts in PHP
|
|
// ---------------------------------------------
|
|
|
|
$items = [];
|
|
$num_rows = [0];
|
|
|
|
// Both views decide previewability from this, so it is set before the branch -
|
|
// it used to be assigned inside the grid arm only, which left the list arm
|
|
// passing null to in_array()
|
|
$inline_viewable_mime_types = getInlineViewableMimeTypes();
|
|
|
|
if ($view == 1) {
|
|
|
|
/*
|
|
* GRID VIEW: files AND documents, same as the list view beside it.
|
|
*
|
|
* This used to be a single files-only query with SQL_CALC_FOUND_ROWS and a
|
|
* LIMIT. Two sources cannot be paginated by one LIMIT, so it now mirrors
|
|
* exactly what the list view does: fetch both unpaginated, merge, sort by
|
|
* name, and slice in PHP. That also fixes the grid quietly omitting every
|
|
* created document - they were only ever visible in list view.
|
|
*/
|
|
$safe_q = mysqli_real_escape_string($mysqli, $q);
|
|
|
|
if ($get_folder_id == 0 && isset($_GET["q"])) {
|
|
$file_folder_snippet = ""; // search across all folders
|
|
$doc_folder_snippet = "";
|
|
} else {
|
|
$file_folder_snippet = "AND file_folder_id = $folder_id";
|
|
$doc_folder_snippet = "AND document_folder_id = $folder_id";
|
|
}
|
|
|
|
$file_search_snippet = "";
|
|
$doc_search_snippet = "";
|
|
if (!empty($q)) {
|
|
$file_search_snippet = "AND (file_name LIKE '%$safe_q%' OR file_ext LIKE '%$safe_q%' OR file_description LIKE '%$safe_q%')";
|
|
$doc_search_snippet = "AND (MATCH(document_content_raw) AGAINST ('$safe_q') OR document_name LIKE '%$safe_q%')";
|
|
}
|
|
|
|
// Skipped outright when the type filter excludes them
|
|
$sql_grid_files = false;
|
|
if ($type_filter !== 'document') {
|
|
$sql_grid_files = mysqli_query(
|
|
$mysqli,
|
|
"SELECT files.*, users.user_name
|
|
FROM files
|
|
LEFT JOIN users ON file_created_by = user_id
|
|
WHERE file_client_id = $client_id
|
|
AND file_$archive_query
|
|
$file_folder_snippet
|
|
$file_search_snippet"
|
|
);
|
|
}
|
|
|
|
$sql_grid_documents = false;
|
|
if ($type_filter !== 'file') {
|
|
$sql_grid_documents = mysqli_query(
|
|
$mysqli,
|
|
"SELECT documents.*, users.user_name
|
|
FROM documents
|
|
LEFT JOIN users ON document_created_by = user_id
|
|
WHERE document_client_id = $client_id
|
|
AND document_$archive_query
|
|
$doc_folder_snippet
|
|
$doc_search_snippet"
|
|
);
|
|
}
|
|
|
|
while ($sql_grid_files && ($row = mysqli_fetch_assoc($sql_grid_files))) {
|
|
$row['grid_kind'] = 'file';
|
|
$items[] = $row;
|
|
}
|
|
|
|
while ($sql_grid_documents && ($row = mysqli_fetch_assoc($sql_grid_documents))) {
|
|
$row['grid_kind'] = 'document';
|
|
$items[] = $row;
|
|
}
|
|
|
|
usort($items, function ($a, $b) {
|
|
$a_name = strtolower($a['grid_kind'] === 'file' ? $a['file_name'] : $a['document_name']);
|
|
$b_name = strtolower($b['grid_kind'] === 'file' ? $b['file_name'] : $b['document_name']);
|
|
return strcmp($a_name, $b_name);
|
|
});
|
|
|
|
$num_rows = [count($items)];
|
|
$items = array_slice($items, $record_from, $record_to);
|
|
|
|
} else {
|
|
|
|
// -------- LIST VIEW: build unified items[] --------
|
|
|
|
// Folder filter
|
|
if ($get_folder_id == 0 && isset($_GET["q"])) {
|
|
$file_folder_snippet = ""; // search across all folders
|
|
$doc_folder_snippet = "";
|
|
} else {
|
|
$file_folder_snippet = "AND file_folder_id = $folder_id";
|
|
$doc_folder_snippet = "AND document_folder_id = $folder_id";
|
|
}
|
|
|
|
// Search filters
|
|
$safe_q = mysqli_real_escape_string($mysqli, $q);
|
|
|
|
$file_search_snippet = "";
|
|
if (!empty($q)) {
|
|
$file_search_snippet = "AND (file_name LIKE '%$safe_q%' OR file_ext LIKE '%$safe_q%' OR file_description LIKE '%$safe_q%')";
|
|
}
|
|
|
|
$doc_search_snippet = "";
|
|
if (!empty($q)) {
|
|
$doc_search_snippet = "AND (MATCH(document_content_raw) AGAINST ('$safe_q') OR document_name LIKE '%$safe_q%')";
|
|
}
|
|
|
|
// Files query (NO limit - we'll paginate in PHP). Skipped outright when the
|
|
// type filter excludes files, rather than fetching rows to throw away.
|
|
$sql_files = false;
|
|
if ($type_filter !== 'document') {
|
|
$sql_files = mysqli_query(
|
|
$mysqli,
|
|
"SELECT files.*, users.user_name
|
|
FROM files
|
|
LEFT JOIN users ON file_created_by = user_id
|
|
WHERE file_client_id = $client_id
|
|
AND file_$archive_query
|
|
$file_folder_snippet
|
|
$file_search_snippet"
|
|
);
|
|
}
|
|
|
|
// Documents query (NO limit - paginate in PHP)
|
|
$sql_documents = false;
|
|
if ($type_filter !== 'file') {
|
|
$sql_documents = mysqli_query(
|
|
$mysqli,
|
|
"SELECT documents.*, users.user_name
|
|
FROM documents
|
|
LEFT JOIN users ON document_created_by = user_id
|
|
WHERE document_client_id = $client_id
|
|
AND document_$archive_query
|
|
$doc_folder_snippet
|
|
$doc_search_snippet"
|
|
);
|
|
}
|
|
|
|
// Normalize FILES into $items
|
|
while ($sql_files && ($row = mysqli_fetch_assoc($sql_files))) {
|
|
$file_id = intval($row['file_id']);
|
|
$file_name = escapeHtml($row['file_name']);
|
|
$file_description = escapeHtml($row['file_description']);
|
|
$file_reference_name= escapeHtml($row['file_reference_name']);
|
|
$file_ext = escapeHtml($row['file_ext']);
|
|
$file_size = intval($row['file_size']);
|
|
$file_mime_type = escapeHtml($row['file_mime_type']);
|
|
$file_uploaded_by = escapeHtml($row['user_name']);
|
|
$file_created_at = escapeHtml($row['file_created_at']);
|
|
$file_archived_at = $row['file_archived_at'];
|
|
|
|
// determine icon
|
|
if ($file_ext == 'pdf') {
|
|
$file_icon = "file-pdf";
|
|
} elseif (in_array($file_ext, ['gz','tar','zip','7z','rar'])) {
|
|
$file_icon = "file-archive";
|
|
} elseif (in_array($file_ext, ['txt','md'])) {
|
|
$file_icon = "file-alt";
|
|
} elseif ($file_ext == 'msg') {
|
|
$file_icon = "envelope";
|
|
} elseif (in_array($file_ext, ['doc','docx','odt'])) {
|
|
$file_icon = "file-word";
|
|
} elseif (in_array($file_ext, ['xls','xlsx','ods'])) {
|
|
$file_icon = "file-excel";
|
|
} elseif (in_array($file_ext, ['pptx','odp'])) {
|
|
$file_icon = "file-powerpoint";
|
|
} elseif (in_array($file_ext, ['mp3','wav','ogg'])) {
|
|
$file_icon = "file-audio";
|
|
} elseif (in_array($file_ext, ['mov','mp4','av1'])) {
|
|
$file_icon = "file-video";
|
|
} elseif (in_array($file_ext, ['jpg','jpeg','png','gif','webp','bmp','tif'])) {
|
|
$file_icon = "file-image";
|
|
} else {
|
|
$file_icon = "file";
|
|
}
|
|
|
|
$items[] = [
|
|
'kind' => 'file',
|
|
'id' => $file_id,
|
|
'name' => $file_name,
|
|
'description' => $file_description,
|
|
'reference_name' => $file_reference_name,
|
|
'icon' => $file_icon,
|
|
'ext' => $file_ext,
|
|
'mime' => $file_mime_type,
|
|
'size' => $file_size,
|
|
'created_at' => $file_created_at,
|
|
'created_by' => $file_uploaded_by,
|
|
'archived_at' => $file_archived_at,
|
|
];
|
|
}
|
|
|
|
// Normalize DOCUMENTS into $items
|
|
while ($sql_documents && ($row = mysqli_fetch_assoc($sql_documents))) {
|
|
$document_id = intval($row['document_id']);
|
|
$document_name = escapeHtml($row['document_name']);
|
|
$document_description = escapeHtml($row['document_description']);
|
|
$document_created_by_name = escapeHtml($row['user_name']);
|
|
$document_created_at = $row['document_created_at'];
|
|
$document_updated_at = $row['document_updated_at'];
|
|
$document_archived_at = $row['document_archived_at'];
|
|
|
|
$items[] = [
|
|
'kind' => 'document',
|
|
'id' => $document_id,
|
|
'name' => $document_name,
|
|
'description' => $document_description,
|
|
'mime' => 'Document',
|
|
'size' => null,
|
|
'updated_at' => $document_updated_at,
|
|
'created_by' => $document_created_by_name,
|
|
'archived_at' => $document_archived_at,
|
|
];
|
|
}
|
|
|
|
// Sort combined items
|
|
$sort = isset($_GET['sort']) ? $_GET['sort'] : 'name';
|
|
$order = isset($_GET['order']) ? $_GET['order'] : 'ASC';
|
|
|
|
usort($items, function($a, $b) use ($sort, $order) {
|
|
$direction = ($order === 'DESC') ? -1 : 1;
|
|
|
|
if ($sort == 'created') {
|
|
$valA = strtotime($a['created_at']);
|
|
$valB = strtotime($b['created_at']);
|
|
} elseif ($sort == 'type') {
|
|
$valA = strtolower($a['mime']);
|
|
$valB = strtolower($b['mime']);
|
|
} elseif ($sort == 'size') {
|
|
$valA = (int)($a['size'] ?? 0);
|
|
$valB = (int)($b['size'] ?? 0);
|
|
} else {
|
|
// default: name
|
|
$valA = strtolower($a['name']);
|
|
$valB = strtolower($b['name']);
|
|
}
|
|
|
|
if ($valA == $valB) {
|
|
return 0;
|
|
}
|
|
|
|
return ($valA < $valB) ? -1 * $direction : 1 * $direction;
|
|
});
|
|
|
|
// Total items (for pagination footer)
|
|
$total_items = count($items);
|
|
$num_rows = [$total_items];
|
|
|
|
// Apply pagination slice
|
|
$items = array_slice($items, $record_from, $record_to);
|
|
}
|
|
|
|
// ---------------------------------------------
|
|
// Root folder count (for "/" badge)
|
|
// ---------------------------------------------
|
|
$row_root_files = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT('file_id') AS num FROM files WHERE file_folder_id = 0 AND file_client_id = $client_id AND file_$archive_query"));
|
|
$row_root_docs = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT('document_id') AS num FROM documents WHERE document_folder_id = 0 AND document_client_id = $client_id AND document_$archive_query"));
|
|
$num_root_items = intval($row_root_files['num']) + intval($row_root_docs['num']);
|
|
|
|
?>
|
|
|
|
<div class="card card-dark">
|
|
|
|
<div class="card-header py-2">
|
|
<h3 class="card-title mt-2"><i class="fa fa-fw fa-folder me-2"></i>Files</h3>
|
|
|
|
<div class="card-tools">
|
|
|
|
<div class="btn-group">
|
|
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown">
|
|
<i class="fas fa-fw fa-plus me-2"></i>New
|
|
</button>
|
|
<div class="dropdown-menu dropdown-menu-end">
|
|
<a class="dropdown-item text-dark ajax-modal" href="#"
|
|
data-modal-url="modals/file/file_upload.php?client_id=<?= $client_id ?>&folder_id=<?= $get_folder_id ?>">
|
|
<i class="fas fa-fw fa-cloud-upload-alt me-2"></i>Upload File
|
|
</a>
|
|
<div class="dropdown-divider"></div>
|
|
<a class="dropdown-item text-dark ajax-modal" href="#"
|
|
data-modal-url="modals/document/document_add.php?client_id=<?= $client_id ?>&folder_id=<?= $get_folder_id ?>"
|
|
data-modal-size="lg">
|
|
<i class="fas fa-fw fa-file-alt me-2"></i>Document
|
|
</a>
|
|
<a class="dropdown-item text-dark ajax-modal" href="#"
|
|
data-modal-url="modals/document/document_add_from_template.php?client_id=<?= $client_id ?>&folder_id=<?= $get_folder_id ?>">
|
|
<i class="fas fa-fw fa-file me-2"></i>Document from Template
|
|
</a>
|
|
<div class="dropdown-divider"></div>
|
|
<a class="dropdown-item text-dark ajax-modal" href="#"
|
|
data-modal-url="modals/folder/folder_add.php?client_id=<?= $client_id ?>¤t_folder_id=<?= $get_folder_id ?>">
|
|
<i class="fa fa-fw fa-folder-plus me-2"></i>Folder
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card-body">
|
|
<div class="row">
|
|
|
|
<!-- Folders -->
|
|
<div class="col-md-3 border-end mb-3">
|
|
<div class="d-flex align-items-center justify-content-between">
|
|
<h4 class="mb-0">Folders</h4>
|
|
|
|
<?php
|
|
$toggle_value = $folders_expanded ? 0 : 1;
|
|
$toggle_title = $folders_expanded
|
|
? 'Collapse all folders'
|
|
: 'Expand all folders';
|
|
?>
|
|
|
|
<a href="?<?= http_build_query(array_merge($_GET, ['folders_expanded' => $toggle_value])) ?>"
|
|
class="btn btn-tool"
|
|
title="<?= $toggle_title ?>"
|
|
aria-label="<?= $toggle_title ?>">
|
|
<i class="fas <?= $folders_expanded ? 'fa-chevron-down' : 'fa-chevron-right' ?>"></i>
|
|
</a>
|
|
</div>
|
|
<hr>
|
|
<ul class="nav nav-pills flex-column bg-light">
|
|
|
|
<?php
|
|
// Start folder tree from root
|
|
displayFolders(0, $client_id);
|
|
?>
|
|
</ul>
|
|
</div>
|
|
|
|
<!-- Main content -->
|
|
<div class="col-md-9">
|
|
|
|
<!-- Search + view toggle -->
|
|
<form autocomplete="off">
|
|
<input type="hidden" name="client_id" value="<?= $client_id ?>">
|
|
<input type="hidden" name="view" value="<?= $view ?>">
|
|
<input type="hidden" name="type" value="<?= escapeHtml($type_filter) ?>">
|
|
<input type="hidden" name="folder_id" value="<?= $get_folder_id ?>">
|
|
<input type="hidden" name="archived" value="<?= $archived ?>">
|
|
<input type="hidden" name="folders_expanded" value="<?= $folders_expanded ?>">
|
|
<div class="row">
|
|
<div class="col-md-5">
|
|
<div class="input-group mb-3 mb-md-0">
|
|
<input type="search" class="form-control" name="q"
|
|
value="<?php if (isset($q)) { echo stripslashes(escapeHtml($q)); } ?>"
|
|
placeholder="Search files and documents in <?= ($get_folder_id == 0 ? 'all folders' : 'current folder') ?>">
|
|
<button class="btn btn-dark"><i class="fa fa-search"></i></button>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-7">
|
|
<div class="float-end">
|
|
<div class="btn-group me-2">
|
|
<a href="?<?= $url_query_strings_type ?>type=" class="btn <?php if($type_filter === ''){ echo "btn-primary"; } else { echo "btn-outline-secondary"; } ?>" title="Files and documents">All</a>
|
|
<a href="?<?= $url_query_strings_type ?>type=file" class="btn <?php if($type_filter === 'file'){ echo "btn-primary"; } else { echo "btn-outline-secondary"; } ?>" title="Uploaded files only"><i class="fas fa-fw fa-file me-1"></i>Files</a>
|
|
<a href="?<?= $url_query_strings_type ?>type=document" class="btn <?php if($type_filter === 'document'){ echo "btn-primary"; } else { echo "btn-outline-secondary"; } ?>" title="Created documents only"><i class="fas fa-fw fa-file-alt me-1"></i>Docs</a>
|
|
</div>
|
|
<div class="btn-group">
|
|
<a href="?<?= $url_query_strings_sort ?>&view=0&folder_id=<?= $get_folder_id ?>" class="btn <?php if($view == 0){ echo "btn-primary"; } else { echo "btn-outline-secondary"; } ?>" title="List View"><i class="fas fa-list-ul"></i></a>
|
|
<a href="?<?= $url_query_strings_sort ?>&view=1&folder_id=<?= $get_folder_id ?>" class="btn <?php if($view == 1){ echo "btn-primary"; } else { echo "btn-outline-secondary"; } ?>" title="Grid View"><i class="fas fa-th-large"></i></a>
|
|
</div>
|
|
<div class="btn-group">
|
|
<a href="?<?= $url_query_strings_sort ?>&archived=<?php if($archived == 1){ echo 0; } else { echo 1; } ?>"
|
|
class="btn btn-<?php if($archived == 1){ echo "primary"; } else { echo "default"; } ?>">
|
|
<i class="fa fa-fw fa-archive me-2"></i>Archived
|
|
</a>
|
|
</div>
|
|
<div class="btn-group">
|
|
<div class="dropdown ms-2" id="bulkActionButton" hidden>
|
|
<button class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown">
|
|
<i class="fas fa-fw fa-layer-group me-2"></i>Bulk Action (<span id="selectedCount">0</span>)
|
|
</button>
|
|
<div class="dropdown-menu">
|
|
<a class="dropdown-item ajax-modal" href="#"
|
|
data-modal-url="modals/file/file_bulk_move.php?client_id=<?= $client_id ?>¤t_folder_id=<?= $get_folder_id ?>"
|
|
data-bulk="true">
|
|
<i class="fas fa-fw fa-exchange-alt me-2"></i>Move Files
|
|
</a>
|
|
<?php if ($archived) { ?>
|
|
<div class="dropdown-divider"></div>
|
|
<button class="dropdown-item text-info"
|
|
type="submit" form="bulkActions" name="bulk_restore_files">
|
|
<i class="fas fa-fw fa-redo me-2"></i>Restore Files
|
|
</button>
|
|
<div class="dropdown-divider"></div>
|
|
<button class="dropdown-item text-danger text-bold"
|
|
type="submit" form="bulkActions" name="bulk_delete_files">
|
|
<i class="fas fa-fw fa-trash me-2"></i>Delete Files
|
|
</button>
|
|
<?php } else { ?>
|
|
<div class="dropdown-divider"></div>
|
|
<button class="dropdown-item text-danger"
|
|
type="submit" form="bulkActions" name="bulk_archive_files">
|
|
<i class="fas fa-fw fa-archive me-2"></i>Archive Files
|
|
</button>
|
|
<?php } ?>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
|
|
<!-- Breadcrumb -->
|
|
<nav class="mt-3">
|
|
<ol class="breadcrumb">
|
|
<li class="breadcrumb-item">
|
|
<a href="?client_id=<?= $client_id ?>&folder_id=0&view=<?= $view ?>&type=<?= escapeHtml($type_filter) ?>&archived=<?= $archived ?>">
|
|
<i class="fas fa-fw fa-folder me-2"></i>Root
|
|
</a>
|
|
</li>
|
|
<?php foreach ($folder_path as $folder) {
|
|
$bread_crumb_folder_id = $folder['folder_id'];
|
|
$bread_crumb_folder_name = $folder['folder_name']; ?>
|
|
<li class="breadcrumb-item">
|
|
<a href="?client_id=<?= $client_id ?>&folder_id=<?= $bread_crumb_folder_id ?>&view=<?= $view ?>&type=<?= escapeHtml($type_filter) ?>&archived=<?= $archived ?>&folders_expanded=<?= $folders_expanded ?>">
|
|
<i class="fas fa-fw fa-folder-open me-2"></i><?= $bread_crumb_folder_name ?>
|
|
</a>
|
|
</li>
|
|
<?php } ?>
|
|
</ol>
|
|
</nav>
|
|
|
|
<hr>
|
|
|
|
<?php
|
|
/*
|
|
* Payload for the file previewer, filled by whichever view is
|
|
* rendering below. Both build it: the grid from its tiles, the
|
|
* list from its file rows, so a file opens the same previewer
|
|
* either way. Documents are deliberately absent - they open the
|
|
* document viewer, not this.
|
|
*/
|
|
$files = [];
|
|
?>
|
|
|
|
<?php if ($view == 1) { ?>
|
|
|
|
<!-- THUMBNAIL VIEW (files only) -->
|
|
<?php if ($num_rows[0] == 0) { ?>
|
|
<p class="text-muted">
|
|
<i class="fa fa-fw fa-folder-open me-2"></i>
|
|
<?php if ($type_filter === 'file') { ?>
|
|
No files in this folder.
|
|
<?php } elseif ($type_filter === 'document') { ?>
|
|
No documents in this folder.
|
|
<?php } else { ?>
|
|
Nothing in this folder.
|
|
<?php } ?>
|
|
</p>
|
|
<?php } ?>
|
|
|
|
<div class="row">
|
|
<?php
|
|
foreach ($items as $row) {
|
|
|
|
$item_kind = $row['grid_kind'];
|
|
|
|
if ($item_kind === 'document') {
|
|
|
|
/*
|
|
* A created document has no file on disk, so the
|
|
* preview comes from document_content_raw.
|
|
*
|
|
* That column is NOT clean plain text: it is the
|
|
* editor's HTML run through strip_tags(), which
|
|
* drops the tags and leaves every entity behind,
|
|
* so it arrives full of and &.
|
|
* cleanTextExcerpt() decodes those before the
|
|
* value is escaped for output - escaping first
|
|
* printed a literal &nbsp; in every tile.
|
|
*
|
|
* document_model.php also builds the column as
|
|
* name . " " . content, so the excerpt opens with
|
|
* the document's own title, which the tile prints
|
|
* underneath anyway. Drop that prefix.
|
|
*/
|
|
$file_id = intval($row['document_id']);
|
|
$file_name = escapeHtml($row['document_name']);
|
|
$file_ext = 'DOC';
|
|
$file_size_human = '';
|
|
$file_archived_at = escapeHtml($row['document_archived_at']);
|
|
$file_icon = 'file-alt';
|
|
$file_preview_kind = 'document';
|
|
$document_excerpt = cleanTextExcerpt($row['document_content_raw']);
|
|
$document_title = trim($row['document_name']);
|
|
if ($document_title !== '' && stripos($document_excerpt, $document_title) === 0) {
|
|
$document_excerpt = ltrim(mb_substr($document_excerpt, mb_strlen($document_title)));
|
|
}
|
|
$file_excerpt = escapeHtml($document_excerpt);
|
|
$file_open_url = "ajax.php?get_document_content=$file_id";
|
|
$file_download_url = "document.php?client_id=$client_id&document_id=$file_id";
|
|
|
|
} else {
|
|
|
|
$file_id = intval($row['file_id']);
|
|
$file_name = escapeHtml($row['file_name']);
|
|
$file_reference_name= escapeHtml($row['file_reference_name']);
|
|
$file_ext = escapeHtml($row['file_ext']);
|
|
$file_size = intval($row['file_size']);
|
|
$file_size_human = formatBytes($file_size);
|
|
$file_mime_type = escapeHtml($row['file_mime_type']);
|
|
$file_uploaded_by = escapeHtml($row['user_name']);
|
|
$file_archived_at = escapeHtml($row['file_archived_at']);
|
|
$file_icon = getFileIcon($file_ext);
|
|
$file_excerpt = '';
|
|
$file_open_url = "file.php?file_id=$file_id&action=view";
|
|
$file_download_url = "file.php?file_id=$file_id";
|
|
|
|
/*
|
|
* What the preview can show is decided by file.php,
|
|
* which serves inline only for getInlineViewableMimeTypes()
|
|
* and forces a download for everything else. Ask the same
|
|
* list rather than guessing: a prefix test on "image/"
|
|
* would call an SVG previewable, and file.php refuses to
|
|
* serve those inline on purpose, so the modal would frame
|
|
* a URL that answers with an attachment.
|
|
*/
|
|
if (!in_array($file_mime_type, $inline_viewable_mime_types, true)) {
|
|
$file_preview_kind = 'none';
|
|
} elseif ($file_mime_type === 'application/pdf') {
|
|
$file_preview_kind = 'pdf';
|
|
} elseif ($file_mime_type === 'text/plain' || $file_mime_type === 'application/json') {
|
|
$file_preview_kind = 'text';
|
|
$file_excerpt = escapeHtml(getFileTextExcerpt($client_id, $row['file_reference_name']));
|
|
} else {
|
|
$file_preview_kind = 'image';
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Documents sit in the same payload as files so the
|
|
* previewer can page across everything in the folder.
|
|
* They carry no preview URL to frame - their content is
|
|
* fetched from ajax.php when opened - and a "full page"
|
|
* link to document.php for the real thing.
|
|
*/
|
|
$tile_index = count($files);
|
|
$files[] = [
|
|
'id' => $file_id,
|
|
'name' => $file_name,
|
|
'kind' => $file_preview_kind,
|
|
'icon' => $file_icon,
|
|
'ext' => strtoupper($file_ext),
|
|
'size' => $file_size_human,
|
|
'excerpt' => $file_excerpt,
|
|
'preview' => $file_open_url,
|
|
'download' => $file_download_url,
|
|
'action' => ($item_kind === 'document') ? 'Open full page' : 'Download'
|
|
];
|
|
?>
|
|
|
|
<div class="col-xl-2 col-lg-2 col-md-6 col-sm-6 mb-3 text-center">
|
|
|
|
<?php
|
|
/*
|
|
* Documents take a coloured border and badge so a
|
|
* created document is not mistaken for an uploaded
|
|
* file at a glance - they behave differently and
|
|
* open in different places.
|
|
*/
|
|
$tile_border = ($item_kind === 'document') ? 'border-primary' : '';
|
|
?>
|
|
|
|
<a href="#" onclick="openModal(<?= $tile_index ?>); return false;"
|
|
class="d-block text-decoration-none position-relative">
|
|
|
|
<?php if ($item_kind === 'document') { ?>
|
|
<span class="badge text-bg-primary position-absolute top-0 start-0 m-1">Document</span>
|
|
<?php } else { ?>
|
|
<span class="badge text-bg-secondary position-absolute top-0 start-0 m-1"><?= strtoupper($file_ext) ?></span>
|
|
<?php } ?>
|
|
|
|
<?php if ($file_preview_kind === 'image') { ?>
|
|
|
|
<img class="img-thumbnail <?= $tile_border ?>" src="file.php?file_id=<?= $file_id ?>&thumb=1" alt="<?= $file_name ?>">
|
|
|
|
<?php } elseif ($file_preview_kind === 'pdf') { ?>
|
|
|
|
<?php
|
|
/*
|
|
* The browser's own PDF viewer renders the first
|
|
* page. loading="lazy" keeps a folder of 50 PDFs
|
|
* from spawning 50 viewers at once, and
|
|
* pointer-events:none stops the viewer swallowing
|
|
* the click meant for the preview modal.
|
|
*
|
|
* #toolbar=0 is honoured by Firefox and ignored by
|
|
* Chrome, which draws its viewer chrome regardless.
|
|
* So the iframe is made taller than its box and
|
|
* pulled up by that much: the toolbar strip lands
|
|
* above the overflow-hidden edge and is clipped,
|
|
* whichever browser is rendering. The full-size
|
|
* preview in the modal keeps its toolbar - that one
|
|
* is meant to be used.
|
|
*/
|
|
?>
|
|
<div class="img-thumbnail overflow-hidden p-0 <?= $tile_border ?>" style="height:150px;">
|
|
<iframe src="file.php?file_id=<?= $file_id ?>&action=view#toolbar=0&navpanes=0&scrollbar=0&view=FitH"
|
|
loading="lazy" title="<?= $file_name ?>" tabindex="-1"
|
|
style="width:100%; height:calc(100% + 56px); margin-top:-56px; border:0; pointer-events:none;"></iframe>
|
|
</div>
|
|
|
|
<?php } elseif (($file_preview_kind === 'text' || $file_preview_kind === 'document') && $file_excerpt !== '') { ?>
|
|
|
|
<div class="img-thumbnail overflow-hidden text-start <?= $tile_border ?>" style="height:150px;">
|
|
<pre class="small text-body-secondary mb-0" style="white-space:pre-wrap; word-break:break-word;"><?= $file_excerpt ?></pre>
|
|
</div>
|
|
|
|
<?php } else { ?>
|
|
|
|
<div class="img-thumbnail d-flex flex-column justify-content-center align-items-center text-secondary <?= $tile_border ?>" style="height:150px;">
|
|
<i class="fas fa-<?= $file_icon ?> fa-3x"></i>
|
|
<small class="mt-2 text-uppercase"><?= $file_ext ?></small>
|
|
</div>
|
|
|
|
<?php } ?>
|
|
</a>
|
|
|
|
<div>
|
|
<div class="dropdown float-end">
|
|
<button class="btn btn-link btn-sm" type="button" data-bs-toggle="dropdown">
|
|
<i class="fas fa-ellipsis-v"></i>
|
|
</button>
|
|
<div class="dropdown-menu">
|
|
<?php if ($item_kind === 'document') { ?>
|
|
|
|
<a class="dropdown-item" href="document.php?client_id=<?= $client_id ?>&document_id=<?= $file_id ?>">
|
|
<i class="fas fa-fw fa-eye me-2"></i>Open
|
|
</a>
|
|
<a class="dropdown-item" href="#" data-bs-toggle="modal" data-bs-target="#shareModal" onclick="populateShareModal(<?= "$client_id, 'Document', $file_id" ?>)">
|
|
<i class="fas fa-fw fa-share me-2"></i>Share
|
|
</a>
|
|
|
|
<?php } else { ?>
|
|
<a class="dropdown-item" href="file.php?file_id=<?= $file_id; ?>">
|
|
<i class="fas fa-fw fa-cloud-download-alt me-2"></i>Download
|
|
</a>
|
|
<a class="dropdown-item" href="#" data-bs-toggle="modal" data-bs-target="#shareModal" onclick="populateShareModal(<?= "$client_id, 'File', $file_id" ?>)">
|
|
<i class="fas fa-fw fa-share me-2"></i>Share
|
|
</a>
|
|
<a class="dropdown-item ajax-modal" href="#"
|
|
data-modal-url="modals/file/file_rename.php?id=<?= $file_id ?>">
|
|
<i class="fas fa-fw fa-edit me-2"></i>Rename
|
|
</a>
|
|
<a class="dropdown-item ajax-modal" href="#"
|
|
data-modal-url="modals/file/file_move.php?id=<?= $file_id ?>">
|
|
<i class="fas fa-fw fa-exchange-alt me-2"></i>Move
|
|
</a>
|
|
<a class="dropdown-item" href="#" data-bs-toggle="modal" data-bs-target="#linkAssetToFileModal<?= $file_id ?>">
|
|
<i class="fas fa-fw fa-desktop me-2"></i>Link Asset
|
|
</a>
|
|
<?php if ($file_archived_at) { ?>
|
|
<div class="dropdown-divider"></div>
|
|
<a class="dropdown-item text-info" href="post.php?restore_file=<?= $file_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
|
|
<i class="fas fa-fw fa-redo me-2"></i>Restore
|
|
</a>
|
|
<?php if ($session_user_role == 3) { ?>
|
|
<div class="dropdown-divider"></div>
|
|
<a class="dropdown-item text-danger text-bold" href="#" data-bs-toggle="modal" data-bs-target="#deleteFileModal" onclick="populateFileDeleteModal(<?= "$file_id , '$file_name'" ?>)">
|
|
<i class="fas fa-fw fa-trash me-2"></i>Delete
|
|
</a>
|
|
<?php } ?>
|
|
<?php } else { ?>
|
|
<div class="dropdown-divider"></div>
|
|
<a class="dropdown-item text-danger confirm-link" href="post.php?archive_file=<?= $file_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
|
|
<i class="fas fa-fw fa-archive me-2"></i>Archive
|
|
</a>
|
|
<?php } ?>
|
|
|
|
<?php } ?>
|
|
</div>
|
|
</div>
|
|
<small class="text-secondary"><?= $file_name ?></small>
|
|
</div>
|
|
</div>
|
|
|
|
<?php
|
|
}
|
|
|
|
// Once, after the loop. This used to sit inside it, so a
|
|
// folder of 30 files emitted 30 copies of the same modal
|
|
// with the same element ids - getElementById found the
|
|
// first and it happened to work.
|
|
?>
|
|
</div>
|
|
|
|
<?php } else { ?>
|
|
|
|
<!-- LIST VIEW: unified Files + Documents -->
|
|
<form id="bulkActions" action="post.php" method="post">
|
|
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
|
|
<div class="table-responsive-sm">
|
|
<table class="table border mb-0">
|
|
<thead class="table-light <?php if ($num_rows[0] == 0) { echo "d-none"; } ?>">
|
|
<tr>
|
|
<td class="bg-light checkbox-column">
|
|
<div class="form-check">
|
|
<input class="form-check-input" id="selectAllCheckbox" type="checkbox" onclick="checkAll(this)">
|
|
</div>
|
|
</td>
|
|
<th>
|
|
<a class="text-secondary" href="?<?= $url_query_strings_sort ?>&sort=name&order=<?= $disp ?>">
|
|
Name <?php if ($sort == 'name') { echo $order_icon; } ?>
|
|
</a>
|
|
</th>
|
|
<th>
|
|
<a class="text-secondary" href="?<?= $url_query_strings_sort ?>&sort=type&order=<?= $disp ?>">
|
|
Type <?php if ($sort == 'type') { echo $order_icon; } ?>
|
|
</a>
|
|
</th>
|
|
<th>
|
|
<a class="text-secondary" href="?<?= $url_query_strings_sort ?>&sort=size&order=<?= $disp ?>">
|
|
Size <?php if ($sort == 'size') { echo $order_icon; } ?>
|
|
</a>
|
|
</th>
|
|
<th>
|
|
<a class="text-secondary" href="?<?= $url_query_strings_sort ?>&sort=created&order=<?= $disp ?>">
|
|
Updated <?php if ($sort == 'created') { echo $order_icon; } ?>
|
|
</a>
|
|
</th>
|
|
<th></th>
|
|
<th class="text-center">Action</th>
|
|
</tr>
|
|
</thead>
|
|
|
|
<tbody>
|
|
<?php
|
|
foreach ($items as $item) {
|
|
|
|
if ($item['kind'] === 'file') {
|
|
$file_id = $item['id'];
|
|
$file_name = $item['name'];
|
|
$file_description = $item['description'];
|
|
$file_reference_name= $item['reference_name'];
|
|
$file_icon = $item['icon'];
|
|
$file_ext = $item['ext'];
|
|
$file_size = $item['size'];
|
|
$file_size_human = formatBytes($file_size);
|
|
$file_mime_type = $item['mime'];
|
|
$file_uploaded_by = $item['created_by'];
|
|
$file_created_at = $item['created_at'];
|
|
$file_archived_at = $item['archived_at'];
|
|
|
|
// Shared?
|
|
$sql_shared = mysqli_query(
|
|
$mysqli,
|
|
"SELECT item_expire_at, item_recipient FROM shared_items
|
|
WHERE item_client_id = $client_id
|
|
AND item_active = 1
|
|
AND (COALESCE(item_view_limit, 0) = 0 OR item_views < item_view_limit)
|
|
AND item_expire_at > NOW()
|
|
AND item_type = 'File'
|
|
AND item_related_id = $file_id
|
|
LIMIT 1"
|
|
);
|
|
$file_shared = (mysqli_num_rows($sql_shared) > 0);
|
|
if ($file_shared) {
|
|
$row_shared = mysqli_fetch_assoc($sql_shared);
|
|
$item_recipient = escapeHtml($row_shared['item_recipient']);
|
|
$item_expire_at_human = timeAgo($row_shared['item_expire_at']);
|
|
}
|
|
?>
|
|
<tr>
|
|
<td class="bg-light checkbox-column">
|
|
<div class="form-check">
|
|
<input class="form-check-input bulk-select" type="checkbox" name="file_ids[]" value="<?= $file_id ?>">
|
|
</div>
|
|
</td>
|
|
<?php
|
|
/*
|
|
* A file the previewer can actually show opens
|
|
* it; anything else keeps the direct link, which
|
|
* downloads. Sending a .docx to the previewer
|
|
* would only show "cannot be previewed" and make
|
|
* the download a second click.
|
|
*
|
|
* Documents are untouched below - they go to
|
|
* document.php, which is where a document
|
|
* belongs.
|
|
*/
|
|
$file_preview_index = -1;
|
|
if (in_array($file_mime_type, $inline_viewable_mime_types, true)) {
|
|
$file_preview_index = count($files);
|
|
$files[] = [
|
|
'id' => $file_id,
|
|
'name' => basename($file_name),
|
|
'kind' => ($file_mime_type === 'application/pdf') ? 'pdf'
|
|
: (($file_mime_type === 'text/plain' || $file_mime_type === 'application/json') ? 'text' : 'image'),
|
|
'icon' => $file_icon,
|
|
'ext' => strtoupper($file_ext),
|
|
'size' => $file_size_human,
|
|
'excerpt' => '',
|
|
'preview' => "file.php?file_id=$file_id&action=view",
|
|
'download' => "file.php?file_id=$file_id",
|
|
'action' => 'Download'
|
|
];
|
|
}
|
|
?>
|
|
<td>
|
|
<?php if ($file_preview_index >= 0) { ?>
|
|
<a href="#" onclick="openModal(<?= $file_preview_index ?>); return false;">
|
|
<?php } else { ?>
|
|
<a href="file.php?file_id=<?= $file_id; ?>&action=view" target="_blank">
|
|
<?php } ?>
|
|
<div class="d-flex">
|
|
<i class="fa fa-fw fa-2x fa-<?= $file_icon ?> text-dark me-3"></i>
|
|
<div class="flex-grow-1">
|
|
<p>
|
|
<?= basename($file_name) ?>
|
|
<br>
|
|
<small class="text-secondary"><?= $file_description ?></small>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</a>
|
|
</td>
|
|
<td><?= $file_mime_type ?></td>
|
|
<td><?= $file_size_human ?></td>
|
|
<td>
|
|
<?= $file_created_at ?>
|
|
<div class="text-secondary mt-1"><?= $file_uploaded_by ?></div>
|
|
</td>
|
|
<td>
|
|
<?php if ($file_shared) { ?>
|
|
<div class="d-flex" title="Expires <?= $item_expire_at_human ?>">
|
|
<i class="fas fa-link me-2 mt-1"></i>
|
|
<div class="flex-grow-1">Shared
|
|
<br>
|
|
<small class="text-secondary"><?= $item_recipient ?></small>
|
|
</div>
|
|
</div>
|
|
<?php } ?>
|
|
</td>
|
|
<td>
|
|
<div class="dropdown dropstart text-center">
|
|
<button class="btn btn-secondary btn-sm" type="button" data-bs-toggle="dropdown">
|
|
<i class="fas fa-ellipsis-h"></i>
|
|
</button>
|
|
<div class="dropdown-menu">
|
|
<a class="dropdown-item" href="file.php?file_id=<?= $file_id ?>">
|
|
<i class="fas fa-fw fa-cloud-download-alt me-2"></i>Download
|
|
</a>
|
|
<a class="dropdown-item" href="#" data-bs-toggle="modal" data-bs-target="#shareModal" onclick="populateShareModal(<?= "$client_id, 'File', $file_id" ?>)">
|
|
<i class="fas fa-fw fa-share me-2"></i>Share
|
|
</a>
|
|
<a class="dropdown-item ajax-modal" href="#"
|
|
data-modal-url="modals/file/file_rename.php?id=<?= $file_id ?>">
|
|
<i class="fas fa-fw fa-edit me-2"></i>Rename
|
|
</a>
|
|
<a class="dropdown-item ajax-modal" href="#"
|
|
data-modal-url="modals/file/file_move.php?id=<?= $file_id ?>">
|
|
<i class="fas fa-fw fa-exchange-alt me-2"></i>Move
|
|
</a>
|
|
<a class="dropdown-item" href="#" data-bs-toggle="modal" data-bs-target="#linkAssetToFileModal<?= $file_id ?>">
|
|
<i class="fas fa-fw fa-desktop me-2"></i>Link Asset
|
|
</a>
|
|
<?php if ($file_archived_at) { ?>
|
|
<div class="dropdown-divider"></div>
|
|
<a class="dropdown-item text-info" href="post.php?restore_file=<?= $file_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
|
|
<i class="fas fa-fw fa-redo me-2"></i>Restore
|
|
</a>
|
|
<?php if ($session_user_role == 3) { ?>
|
|
<div class="dropdown-divider"></div>
|
|
<a class="dropdown-item text-danger text-bold" href="#" data-bs-toggle="modal" data-bs-target="#deleteFileModal" onclick="populateFileDeleteModal(<?= "$file_id , '$file_name'" ?>)">
|
|
<i class="fas fa-fw fa-trash me-2"></i>Delete
|
|
</a>
|
|
<?php } ?>
|
|
<?php } else { ?>
|
|
<div class="dropdown-divider"></div>
|
|
<a class="dropdown-item text-danger confirm-link" href="post.php?archive_file=<?= $file_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
|
|
<i class="fas fa-fw fa-archive me-2"></i>Archive
|
|
</a>
|
|
<?php } ?>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<?php
|
|
require "modals/file/file_link_asset.php";
|
|
|
|
} else {
|
|
// DOCUMENT ROW
|
|
$document_id = $item['id'];
|
|
$document_name = $item['name'];
|
|
$document_description = $item['description'];
|
|
$document_created_by_name = $item['created_by'];
|
|
$document_created_at = date("m/d/Y", strtotime($item['updated_at']));
|
|
//$document_updated_at = date("m/d/Y", strtotime($item['updated_at']));
|
|
$document_archived_at = $item['archived_at'];
|
|
|
|
$sql_shared = mysqli_query(
|
|
$mysqli,
|
|
"SELECT item_expire_at, item_recipient FROM shared_items
|
|
WHERE item_client_id = $client_id
|
|
AND item_active = 1
|
|
AND (COALESCE(item_view_limit, 0) = 0 OR item_views < item_view_limit)
|
|
AND item_expire_at > NOW()
|
|
AND item_type = 'Document'
|
|
AND item_related_id = $document_id
|
|
LIMIT 1"
|
|
);
|
|
$doc_shared = (mysqli_num_rows($sql_shared) > 0);
|
|
if ($doc_shared) {
|
|
$row_shared = mysqli_fetch_assoc($sql_shared);
|
|
$item_recipient = escapeHtml($row_shared['item_recipient']);
|
|
$item_expire_at_human = timeAgo($row_shared['item_expire_at']);
|
|
}
|
|
?>
|
|
<tr>
|
|
<td class="bg-light pe-0">
|
|
<div class="form-check">
|
|
<input class="form-check-input bulk-select" type="checkbox" name="document_ids[]" value="<?= $document_id ?>">
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<?php /* Blue icon and badge, same as the grid tile - a created
|
|
document and an uploaded file behave differently and open
|
|
in different places, so they should not look alike. */ ?>
|
|
<a href="document.php?client_id=<?= $client_id ?>&document_id=<?= $document_id ?>">
|
|
<div class="d-flex">
|
|
<i class="fa fa-fw fa-2x fa-file-alt text-primary me-3"></i>
|
|
<div class="flex-grow-1">
|
|
<p>
|
|
<?= $document_name ?>
|
|
<br>
|
|
<small class="text-secondary"><?= $document_description ?></small>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</a>
|
|
</td>
|
|
<td><span class="badge text-bg-primary">Document</span></td>
|
|
<td>-</td>
|
|
<td>
|
|
<?= $document_created_at ?>
|
|
<div class="text-secondary mt-1"><?= $document_created_by_name ?></div>
|
|
</td>
|
|
<td>
|
|
<?php if ($doc_shared) { ?>
|
|
<div class="d-flex" title="Expires <?= $item_expire_at_human ?>">
|
|
<i class="fas fa-link me-2 mt-1"></i>
|
|
<div class="flex-grow-1">Shared
|
|
<br>
|
|
<small class="text-secondary"><?= $item_recipient ?></small>
|
|
</div>
|
|
</div>
|
|
<?php } ?>
|
|
</td>
|
|
<td>
|
|
<div class="dropdown dropstart text-center">
|
|
<button class="btn btn-secondary btn-sm" type="button" data-bs-toggle="dropdown">
|
|
<i class="fas fa-ellipsis-h"></i>
|
|
</button>
|
|
<div class="dropdown-menu">
|
|
<a class="dropdown-item ajax-modal" href="#"
|
|
data-modal-size="lg"
|
|
data-modal-url="modals/document/document_view.php?id=<?= $document_id ?>">
|
|
<i class="fas fa-fw fa-eye me-2"></i>Quick View
|
|
</a>
|
|
<div class="dropdown-divider"></div>
|
|
<a class="dropdown-item ajax-modal" href="#"
|
|
data-modal-size="lg"
|
|
data-modal-url="modals/document/document_edit.php?id=<?= $document_id ?>">
|
|
<i class="fas fa-fw fa-pencil-alt me-2"></i>Edit
|
|
</a>
|
|
<div class="dropdown-divider"></div>
|
|
<a class="dropdown-item" href="#" data-bs-toggle="modal" data-bs-target="#shareModal" onclick="populateShareModal(<?= "$client_id, 'Document', $document_id" ?>)">
|
|
<i class="fas fa-fw fa-share me-2"></i>Share
|
|
</a>
|
|
<div class="dropdown-divider"></div>
|
|
<a class="dropdown-item ajax-modal" href="#"
|
|
data-modal-url="modals/document/document_rename.php?id=<?= $document_id ?>">
|
|
<i class="fas fa-fw fa-pencil-alt me-2"></i>Rename
|
|
</a>
|
|
<a class="dropdown-item ajax-modal" href="#"
|
|
data-modal-url="modals/document/document_move.php?id=<?= $document_id ?>">
|
|
<i class="fas fa-fw fa-exchange-alt me-2"></i>Move
|
|
</a>
|
|
<?php if ($document_archived_at) { ?>
|
|
<div class="dropdown-divider"></div>
|
|
<a class="dropdown-item text-info" href="post.php?restore_document=<?= $document_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
|
|
<i class="fas fa-fw fa-redo me-2"></i>Restore
|
|
</a>
|
|
<?php if ($session_user_role == 3) { ?>
|
|
<div class="dropdown-divider"></div>
|
|
<a class="dropdown-item text-danger text-bold" href="post.php?delete_document=<?= $document_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
|
|
<i class="fas fa-fw fa-trash me-2"></i>Delete
|
|
</a>
|
|
<?php } ?>
|
|
<?php } else { ?>
|
|
<div class="dropdown-divider"></div>
|
|
<a class="dropdown-item text-danger" href="post.php?archive_document=<?= $document_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
|
|
<i class="fas fa-fw fa-archive me-2"></i>Archive
|
|
</a>
|
|
<?php } ?>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<?php
|
|
}
|
|
}
|
|
?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</form>
|
|
|
|
<?php } ?>
|
|
|
|
<?php
|
|
// After both branches - the previewer serves the grid and the
|
|
// list, and $files is whichever view just filled it
|
|
require "modals/file/file_view.php";
|
|
?>
|
|
<script>
|
|
var files = <?= json_encode($files) ?>;
|
|
var currentIndex = 0;
|
|
</script>
|
|
|
|
<?php require_once "../includes/filter_footer.php"; ?>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
function openModal(index) {
|
|
currentIndex = index;
|
|
updateModalContent();
|
|
bootstrap.Modal.getOrCreateInstance(document.getElementById('viewFileModal')).show();
|
|
}
|
|
|
|
function updateModalContent() {
|
|
var file = files[currentIndex];
|
|
var image = document.getElementById('modalImage');
|
|
var frame = document.getElementById('modalFrame');
|
|
var fallback = document.getElementById('modalFallback');
|
|
var text = document.getElementById('modalText');
|
|
var docPanel = document.getElementById('modalDocument');
|
|
|
|
document.getElementById('modalTitle').innerText = file.name;
|
|
document.getElementById('modalMeta').innerText = file.size ? (file.ext + ' \u00b7 ' + file.size) : file.ext;
|
|
var action = document.getElementById('modalDownload');
|
|
action.href = file.download;
|
|
action.innerHTML = '<i class="fas fa-fw fa-' + (file.kind === 'document' ? 'external-link-alt' : 'cloud-download-alt') + ' me-2"></i>' + file.action;
|
|
document.getElementById('modalPosition').innerText = (currentIndex + 1) + ' of ' + files.length;
|
|
|
|
// Blank the previous file's source before switching panels, or a heavy PDF
|
|
// keeps rendering behind the next image until the browser gets round to it
|
|
image.classList.add('d-none');
|
|
image.removeAttribute('src');
|
|
frame.classList.add('d-none');
|
|
frame.removeAttribute('src');
|
|
fallback.classList.add('d-none');
|
|
text.classList.add('d-none');
|
|
docPanel.classList.add('d-none');
|
|
docPanel.innerHTML = '';
|
|
|
|
if (file.kind === 'document') {
|
|
|
|
/*
|
|
* Fetched rather than carried in the page payload - see the endpoint's
|
|
* note in ajax.php. The index is captured so a fast click through to the
|
|
* next file does not get overwritten by a response that arrives late.
|
|
*/
|
|
var requestedIndex = currentIndex;
|
|
docPanel.innerHTML = '<p class="text-secondary mb-0">Loading...</p>';
|
|
docPanel.classList.remove('d-none');
|
|
|
|
fetch(file.preview, { headers: { 'X-Requested-With': 'XMLHttpRequest' } })
|
|
.then(function (r) { return r.json(); })
|
|
.then(function (data) {
|
|
if (requestedIndex !== currentIndex) {
|
|
return;
|
|
}
|
|
// The endpoint returns HTMLPurifier output, which is what
|
|
// document.php renders too
|
|
docPanel.innerHTML = data.content || '<p class="text-secondary mb-0">This document is empty.</p>';
|
|
})
|
|
.catch(function () {
|
|
if (requestedIndex === currentIndex) {
|
|
docPanel.innerHTML = '<p class="text-danger mb-0">Could not load this document.</p>';
|
|
}
|
|
});
|
|
|
|
} else if (file.kind === 'image') {
|
|
image.src = file.preview;
|
|
image.alt = file.name;
|
|
image.classList.remove('d-none');
|
|
} else if (file.kind === 'pdf' || file.kind === 'text') {
|
|
frame.src = file.preview;
|
|
frame.classList.remove('d-none');
|
|
} else {
|
|
document.getElementById('modalFallbackIcon').className = 'fas fa-' + file.icon + ' fa-4x text-secondary';
|
|
fallback.classList.remove('d-none');
|
|
}
|
|
|
|
// One file in the folder means nothing to page through
|
|
var single = files.length < 2;
|
|
document.getElementById('modalPrev').classList.toggle('d-none', single);
|
|
document.getElementById('modalNext').classList.toggle('d-none', single);
|
|
}
|
|
|
|
function nextFile() {
|
|
currentIndex = (currentIndex + 1) % files.length;
|
|
updateModalContent();
|
|
}
|
|
|
|
function prevFile() {
|
|
currentIndex = (currentIndex - 1 + files.length) % files.length;
|
|
updateModalContent();
|
|
}
|
|
|
|
// Arrow keys page the gallery while the preview is open
|
|
document.addEventListener('keydown', function (e) {
|
|
var modal = document.getElementById('viewFileModal');
|
|
if (!modal || !modal.classList.contains('show') || files.length < 2) {
|
|
return;
|
|
}
|
|
if (e.key === 'ArrowRight') {
|
|
nextFile();
|
|
} else if (e.key === 'ArrowLeft') {
|
|
prevFile();
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<script src="../js/bulk_actions.js"></script>
|
|
|
|
<?php
|
|
require_once "modals/share_modal.php";
|
|
require_once "modals/file/file_delete.php";
|
|
//require_once "modals/document/document_add_from_template.php";
|
|
require_once "../includes/footer.php";
|