Files
itflow/agent/post/document.php
johnnyq 3ee183a09a Document templates stage 2, filled in from the client: placeholders [Client], [Website], [Site], [Address], [Site Phone], [Contact], [First Name], [Contact Email], [Contact Phone], [Agent], [Date], [Month], [Quarter] and [Year] are filled in across a template's name, summary and content when a document is made, using the client's primary site and contact unless another or none is picked; tables of records [Contacts], [Locations], [Assets], [Servers], [Workstations], [Network Equipment], [Printers], [Networks], [Domains], [Certificates], [Licenses], [Vendors] and [Services] put in the client's current records, as a table on a line of their own (an empty row when there are none) or as names in a sentence, all made safe; any other bracketed word is a fill-in asked for when a document is made, set up on the template page as text, date, list or yes/no, required or with help, renamed along with the template text, reset or removed, and dragged into order, with answers checked by ticketFillInAnswers and blanks left in the document; the template page gets Fill-ins and Filled in from the client cards with each kind of word marked in its own colour, and the editor gets a button for each placeholder and table to insert it at the cursor (js/document_template_form.js); New document for a client and Document from template draw the site and contact pickers and fill-ins once the template and client are picked (agent/ajax.php document_template_asks, agent/js/document_template_make.js) and open larger; copying a template copies its fill-ins; Draft it with AI is told the placeholders and tables; migration 3.2.6 adds document_template_fill_ins; fixed the fill-in add and reset messages showing raw HTML around the bracketed name on document and ticket templates; changelog updated
2026-10-18 19:47:57 -04:00

446 lines
18 KiB
PHP

<?php
/*
* ITFlow - GET/POST request handler for client documents
*
* Linking documents to files, assets, contacts, licenses and vendors lives in
* post/link.php.
*/
defined('FROM_POST_HANDLER') || die("Direct file access is not allowed");
/** The document row a handler works on, with its client checked - or a flash and back. */
function documentForHandler($document_id) {
global $mysqli, $client_id;
$document_id = intval($document_id);
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT document_id, document_name, document_description, document_content,
document_client_id, document_folder_id, document_client_visible, document_archived_at,
document_created_at, document_created_by, document_updated_at, document_updated_by
FROM documents WHERE document_id = $document_id"));
if (!$row) {
flashAlert('That document no longer exists.', 'error');
redirect();
}
$client_id = intval($row['document_client_id']);
enforceClientAccess();
return $row;
}
/** Keep a document's current content as a version, before it is replaced. Returns the version id. */
function documentKeepVersion($row) {
global $mysqli;
$by = intval($row['document_updated_by']) ?: intval($row['document_created_by']);
$at = escapeSql($row['document_updated_at'] ?: $row['document_created_at']);
mysqli_query($mysqli, "INSERT INTO document_versions SET
document_version_name = '" . escapeSql($row['document_name']) . "',
document_version_description = '" . escapeSql($row['document_description']) . "',
document_version_content = '" . mysqli_real_escape_string($mysqli, (string) $row['document_content']) . "',
document_version_created_by = $by,
document_version_created_at = '$at',
document_version_document_id = " . intval($row['document_id']));
return mysqli_insert_id($mysqli);
}
if (isset($_POST['add_document'])) {
validateCSRFToken();
enforceUserPermission('module_support', 2);
require_once 'document_model.php';
$client_id = intval($_POST['client_id']);
enforceClientAccess();
// Only the client's own folder, contact, asset, license, location and vendor
$folder = folderOwnedId($folder, $client_id);
$contact_id = assetClientOwnedId('contact', $_POST['contact'] ?? 0, $client_id);
$asset = linkRecord('asset', $_POST['asset'] ?? 0);
$asset_id = $asset && intval($asset['client_id']) === $client_id ? intval($asset['id']) : 0;
$software = linkRecord('software', $_POST['software'] ?? 0);
$software_id = $software && intval($software['client_id']) === $client_id ? intval($software['id']) : 0;
$location_id = assetClientOwnedId('location', $_POST['location'] ?? 0, $client_id);
$vendor_id = assetClientOwnedId('vendor', $_POST['vendor'] ?? 0, $client_id);
$service = linkRecord('service', $_POST['service'] ?? 0);
$service_id = $service && intval($service['client_id']) === $client_id && !$service['archived_at'] ? intval($service['id']) : 0;
$client_visible = intval($_POST['client_visible'] ?? 0) ? 1 : 0;
if ($name === '') {
flashAlert('A document needs a name.', 'error');
redirect();
}
mysqli_query($mysqli, "INSERT INTO documents SET document_name = '$name', document_description = '$description', document_content = '', document_content_raw = '$content_raw',
document_client_visible = $client_visible, document_folder_id = $folder, document_created_by = $session_user_id, document_client_id = $client_id");
$document_id = mysqli_insert_id($mysqli);
// Pasted images become files under uploads/documents/<id>/, so it needs its id first
$processed_content = mysqli_real_escape_string($mysqli, saveBase64Images(
$_POST['content'] ?? '',
$_SERVER['DOCUMENT_ROOT'] . "/uploads/documents/",
"uploads/documents/",
$document_id
));
mysqli_query($mysqli, "UPDATE documents SET document_content = '$processed_content' WHERE document_id = $document_id");
if ($contact_id) {
mysqli_query($mysqli, "INSERT IGNORE INTO contact_documents SET contact_id = $contact_id, document_id = $document_id");
}
if ($asset_id) {
mysqli_query($mysqli, "INSERT IGNORE INTO asset_documents SET asset_id = $asset_id, document_id = $document_id");
}
if ($software_id) {
mysqli_query($mysqli, "INSERT IGNORE INTO software_documents SET software_id = $software_id, document_id = $document_id");
}
if ($location_id) {
mysqli_query($mysqli, "INSERT IGNORE INTO location_documents SET location_id = $location_id, document_id = $document_id");
}
if ($vendor_id) {
mysqli_query($mysqli, "INSERT IGNORE INTO vendor_documents SET vendor_id = $vendor_id, document_id = $document_id");
}
if ($service_id) {
mysqli_query($mysqli, "INSERT IGNORE INTO service_documents SET service_id = $service_id, document_id = $document_id");
logAudit("Service", "Link", "$session_name linked new document $name to service " . escapeSql($service['name']), $client_id, $service_id);
}
tagSetForRecord('document', $document_id, (array) ($_POST['tags'] ?? []));
logAudit("Document", "Create", "$session_name created document $name", $client_id, $document_id);
flashAlert("Document <strong>" . trim($_POST['name']) . "</strong> created");
// From an asset, a contact, a license, a location, a vendor or a service, back to it; otherwise to the new document
redirect(($contact_id || $asset_id || $software_id || $location_id || $vendor_id || $service_id) ? null : "document.php?client_id=$client_id&document_id=$document_id");
}
if (isset($_POST['add_document_from_template'])) {
validateCSRFToken();
enforceUserPermission('module_support', 2);
$client_id = intval($_POST['client_id'] ?? 0);
$document_template_id = intval($_POST['document_template_id'] ?? 0);
enforceClientAccess();
if (!mysqli_num_rows(mysqli_query($mysqli, "SELECT 1 FROM clients WHERE client_id = $client_id"))) {
flashAlert('Pick the client it is for.', 'error');
redirect();
}
$folder = folderOwnedId($_POST['folder'] ?? 0, $client_id);
$template = documentTemplateRow($document_template_id);
if (!$template || $template['document_template_archived_at']) {
flashAlert('That template is gone or archived.', 'error');
redirect();
}
// What it asks for, answered - a required one left blank, or an answer that isn't one, says so
$answers = ticketFillInAnswers(documentTemplateFillInsFor($document_template_id), $_POST['fill_ins'] ?? [], $fill_in_error);
if ($answers === null) {
flashAlert($fill_in_error, 'error');
redirect();
}
// A copy of what it says and its images, filled in for the client and remembering the template (functions/document_template.php)
$document_id = documentTemplateMakeDocument($document_template_id, $client_id, $_POST['name'] ?? '', $_POST['description'] ?? '', $folder,
intval($_POST['location_id'] ?? 0), intval($_POST['contact_id'] ?? 0), $answers);
$document_name = (string) getFieldById('documents', $document_id, 'document_name');
logAudit("Document", "Create", "$session_name created document " . escapeSql($document_name) . " from template " . escapeSql($template['document_template_name']), $client_id, $document_id);
flashAlert("Document <strong>" . escapeHtml($document_name) . "</strong> created from template <strong>" . escapeHtml($template['document_template_name']) . "</strong>");
redirect("/agent/document.php?client_id=$client_id&document_id=$document_id");
}
if (isset($_POST['edit_document'])) {
validateCSRFToken();
enforceUserPermission('module_support', 2);
require_once 'document_model.php';
$row = documentForHandler($_POST['document_id']);
$document_id = intval($row['document_id']);
if ($name === '') {
flashAlert('A document needs a name.', 'error');
redirect();
}
// What it said before, kept as a version
$document_version_id = documentKeepVersion($row);
$processed_html = saveBase64Images(
$_POST['content'] ?? '',
$_SERVER['DOCUMENT_ROOT'] . "/uploads/documents/",
"uploads/documents/",
$document_id
);
$content = mysqli_real_escape_string($mysqli, $processed_html);
$content_raw = escapeSql(documentContentRaw($_POST['name'], $processed_html));
// The portal switch is only on the form when the portal is on - left alone otherwise
$visible_sql = isset($_POST['client_visible']) ? ', document_client_visible = ' . (intval($_POST['client_visible']) ? 1 : 0) : '';
mysqli_query($mysqli, "UPDATE documents SET
document_name = '$name',
document_description = '$description',
document_content = '$content',
document_content_raw = '$content_raw',
document_updated_by = $session_user_id
$visible_sql
WHERE document_id = $document_id");
// Only a form that showed the tags changes them
if (isset($_POST['tags_form'])) {
tagSetForRecord('document', $document_id, (array) ($_POST['tags'] ?? []));
}
logAudit("Document", "Edit", "$session_name edited document $name, previous version kept", $client_id, $document_id);
flashAlert("Document <strong>" . trim($_POST['name']) . "</strong> saved - the previous version is kept");
redirect("document.php?client_id=$client_id&document_id=$document_id");
}
if (isset($_POST['move_document'])) {
validateCSRFToken();
enforceUserPermission('module_support', 2);
$row = documentForHandler($_POST['document_id']);
$document_id = intval($row['document_id']);
// Only into one of this client's folders
$folder_id = folderOwnedId($_POST['folder'] ?? 0, $client_id);
$folder_name = folderDisplayName($folder_id);
mysqli_query($mysqli, "UPDATE documents SET document_folder_id = $folder_id, document_updated_at = document_updated_at WHERE document_id = $document_id");
logAudit("Document", "Move", "$session_name moved document " . escapeSql($row['document_name']) . " to folder " . escapeSql($folder_name), $client_id, $document_id);
flashAlert("Document <strong>{$row['document_name']}</strong> moved to <strong>$folder_name</strong>");
redirect();
}
if (isset($_POST['rename_document'])) {
validateCSRFToken();
enforceUserPermission('module_support', 2);
$row = documentForHandler($_POST['document_id']);
$document_id = intval($row['document_id']);
$name = escapeSql(trim($_POST['name'] ?? ''));
$description_sql = isset($_POST['description']) ? ", document_description = '" . escapeSql($_POST['description']) . "'" : '';
if ($name === '') {
flashAlert('A document needs a name.', 'error');
redirect();
}
mysqli_query($mysqli, "UPDATE documents SET document_name = '$name' $description_sql, document_updated_at = document_updated_at WHERE document_id = $document_id");
logAudit("Document", "Edit", "$session_name renamed document " . escapeSql($row['document_name']) . " to $name", $client_id, $document_id);
flashAlert("Document <strong>" . trim($_POST['name']) . "</strong> saved");
redirect();
}
if (isset($_POST['toggle_document_visibility'])) {
validateCSRFToken();
enforceUserPermission('module_support', 2);
$row = documentForHandler($_POST['document_id']);
$document_id = intval($row['document_id']);
$document_visible = intval($_POST['document_visible'] ?? 0) ? 1 : 0;
$wording = $document_visible ? 'visible' : 'hidden';
mysqli_query($mysqli, "UPDATE documents SET document_client_visible = $document_visible, document_updated_at = document_updated_at WHERE document_id = $document_id");
logAudit("Document", "Edit", "$session_name made document " . escapeSql($row['document_name']) . " $wording in the client portal", $client_id, $document_id);
flashAlert("Document <strong>{$row['document_name']}</strong> is now <strong>$wording</strong> in the client portal");
redirect();
}
if (isset($_GET['export_document'])) {
validateCSRFToken();
// Reading, not changing - anyone who can open the document can take a PDF of it
enforceUserPermission('module_client');
$row = documentForHandler($_GET['export_document']);
$document_id = intval($row['document_id']);
logAudit("Document", "Export", "$session_name exported document " . escapeSql($row['document_name']), $client_id, $document_id);
require_once('../libs/TCPDF/tcpdf.php');
$pdf = new TCPDF();
$pdf->SetCreator(PDF_CREATOR);
$pdf->SetAuthor($session_name);
$pdf->SetTitle($row['document_name']);
$pdf->AddPage();
$pdf->SetFont('helvetica', '', 12);
$pdf->writeHTML('<h1>' . escapeHtml($row['document_name']) . '</h1>' . $row['document_content'], true, false, true, false, '');
// The PDF is the response - nothing may follow it
$pdf->Output(preg_replace('/[^A-Za-z0-9 ._-]/', '', $row['document_name']) . '.pdf', 'I');
exit;
}
if (isset($_GET['archive_document'])) {
validateCSRFToken();
enforceUserPermission('module_support', 2);
$row = documentForHandler($_GET['archive_document']);
$document_id = intval($row['document_id']);
mysqli_query($mysqli, "UPDATE documents SET document_archived_at = NOW(), document_updated_at = document_updated_at WHERE document_id = $document_id");
// An archived document drops its links, as it always has
linkRemoveAll('document', $document_id);
logAudit("Document", "Archive", "$session_name archived document " . escapeSql($row['document_name']), $client_id, $document_id);
flashAlert("Document <strong>{$row['document_name']}</strong> archived", 'error');
redirect();
}
if (isset($_GET['restore_document'])) {
validateCSRFToken();
enforceUserPermission('module_support', 2);
$row = documentForHandler($_GET['restore_document']);
$document_id = intval($row['document_id']);
mysqli_query($mysqli, "UPDATE documents SET document_archived_at = NULL, document_updated_at = document_updated_at WHERE document_id = $document_id");
logAudit("Document", "Restore", "$session_name restored document " . escapeSql($row['document_name']), $client_id, $document_id);
flashAlert("Document <strong>{$row['document_name']}</strong> restored");
redirect();
}
/*
* Put an older version back. What the document says now is kept as a version
* first, so a restore can itself be undone.
*/
if (isset($_POST['restore_document_version'])) {
validateCSRFToken();
enforceUserPermission('module_support', 2);
$document_version_id = intval($_POST['document_version_id']);
$version = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT document_version_document_id, document_version_name, document_version_description,
document_version_content, document_version_created_at
FROM document_versions WHERE document_version_id = $document_version_id"));
if (!$version) {
flashAlert('That version no longer exists.', 'error');
redirect();
}
$row = documentForHandler($version['document_version_document_id']);
$document_id = intval($row['document_id']);
documentKeepVersion($row);
$name = escapeSql($version['document_version_name']);
$content = mysqli_real_escape_string($mysqli, (string) $version['document_version_content']);
$content_raw = escapeSql(documentContentRaw($version['document_version_name'], $version['document_version_content']));
mysqli_query($mysqli, "UPDATE documents SET
document_name = '$name',
document_description = '" . escapeSql($version['document_version_description']) . "',
document_content = '$content',
document_content_raw = '$content_raw',
document_updated_by = $session_user_id
WHERE document_id = $document_id");
$version_date = date('M j, Y g:ia', strtotime($version['document_version_created_at']));
logAudit("Document", "Edit", "$session_name restored document $name to its version of " . escapeSql($version_date) . ", the current one kept", $client_id, $document_id);
flashAlert("Document <strong>{$version['document_version_name']}</strong> restored to the version of $version_date - what it said before is kept as a version");
redirect("document.php?client_id=$client_id&document_id=$document_id");
}
if (isset($_GET['delete_document_version'])) {
validateCSRFToken();
enforceUserPermission('module_support', 3);
$document_version_id = intval($_GET['delete_document_version']);
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT document_version_name, document_client_id FROM documents, document_versions
WHERE document_version_document_id = document_id AND document_version_id = $document_version_id"));
if (!$row) {
flashAlert('That version no longer exists.', 'error');
redirect();
}
$client_id = intval($row['document_client_id']);
enforceClientAccess();
mysqli_query($mysqli, "DELETE FROM document_versions WHERE document_version_id = $document_version_id");
logAudit("Document Version", "Delete", "$session_name deleted document version " . escapeSql($row['document_version_name']), $client_id);
flashAlert("A version of <strong>{$row['document_version_name']}</strong> deleted", 'error');
redirect();
}
if (isset($_GET['delete_document'])) {
validateCSRFToken();
enforceUserPermission('module_support', 3);
$document_name = documentDeleteForGood(intval($_GET['delete_document']));
if ($document_name === null) {
flashAlert('That document no longer exists.', 'error');
redirect();
}
flashAlert("Document <strong>$document_name</strong> and all its versions deleted", 'error');
// From the document's own page there is nothing to go back to
if (($_GET['from'] ?? '') === 'document_details') {
redirect("files.php?client_id=$client_id");
}
redirect();
}