diff --git a/.gitignore b/.gitignore index 751f2adf..7d06cba2 100644 --- a/.gitignore +++ b/.gitignore @@ -4,8 +4,16 @@ config.php uploads/favicon.ico uploads/clients/* !uploads/clients/index.php +uploads/custom/* +!uploads/custom/index.php +uploads/documents/* +!uploads/documents/index.php +uploads/document_templates/* +!uploads/document_templates/index.php uploads/expenses/* !uploads/expenses/index.php +uploads/recurring_tickets/* +!uploads/recurring_tickets/index.php uploads/settings/* !uploads/settings/index.php uploads/users/* @@ -14,6 +22,8 @@ uploads/tmp/* !uploads/tmp/index.php uploads/tickets/* !uploads/tickets/index.php +uploads/ticket_templates/* +!uploads/ticket_templates/index.php .idea/* plugins/htmlpurifier/standalone/HTMLPurifier/DefinitionCache/Serializer/HTML/* !plugins/htmlpurifier/standalone/HTMLPurifier/DefinitionCache/Serializer/HTML/.gitkeep diff --git a/CHANGELOG.md b/CHANGELOG.md index d8af4496..9b9d93cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,39 @@ This file documents all notable changes made to ITFlow. +## [25.12] Stable Release + +### Breaking Changes ### +- For Existing installs: **php-xml** extension needs to be installed for document creation and editing, new install script does this for you as of Dec 6th 2025. To install php-xml: `sudo apt install php-xml` + +### Major Changes +- Consolidated "Files" and "Documents" into a single section called **Files**. + +### Bug Fixes +- Resolved issue with updating asset notes in asset details. +- Fixed problem with bulk ticket merging. +- Corrected issue where decimal inputs (e.g., price, cost) weren’t displaying on iPhones in certain forms. +- Added CSV escaping to the sample export data in areas where a sample CSV template is provided. +- Fix a race condition where dupe tickets, invoices, recurring invoices, recurring tickets, quotes will be created using the same number if created in parallel espcecially when using the API. + +### New Features & Updates +- Introduced automatic subject-based ticket merging/reply detection. Now, if an email comes from a known contact or domain and the subject matches 95% of a ticket opened in the last 7 days, it will be merged automatically. +- Added `cleanInput` function to sanitize data before inserting it into the database when using MySQLi prepared statements. +- Migrated client post functionality to use MySQLi prepared statements. +- Updated payment method post functionality to use MySQLi prepared statements. +- Implemented `saveBase64Images()` to convert base64-encoded `` tags into actual image files stored under `/uploads///` with secure filenames. Added wrapper functions, and updated document creation to use processed image paths. +- For new documents and document templates, images are now stored in `/uploads/documents/$document_id` instead of being stored as base64 in the database, using the `saveBase64Images()` function. +- UI/UX improvements made to the document details page. +- Removed sidebar quick-add options. +- Created new folders in the uploads directory: `documents`, `document_templates`, and `recurring_tickets`. +- Reworked the bulk action function to pass the name arrays, instead of a generic `selected_ids` array. This allows multiple bulk name arrays to be passed at once, currently used for the new file-document merge. +- Big task: Converted the remaining modals to use the new `ajax-modal` system, enabling more flexible flow expansion going forward. +- Mail queue: Added a `--no-mx-validation` flag to bypass recipient domain MX validation. +- Bump PHPMailer from 7.0.0 to 7.0.1. +- Bump stripe-php from 18.1.0 to 19.0.0. +- Bump TCPDF from 6.10.0 to 6.10.1. +- Bump TinyMCE from 8.2.0 to 8.2.2. + ## [25.11.1] Maint Release ### Fixes diff --git a/admin/debug.php b/admin/debug.php index f40f7881..c59c1532 100644 --- a/admin/debug.php +++ b/admin/debug.php @@ -52,6 +52,7 @@ $extensions = [ 'php-mbstring' => 'mbstring', 'php-gd' => 'gd', 'php-zip' => 'zip', + 'php-xml' => 'xml', ]; foreach ($extensions as $name => $ext) { @@ -682,7 +683,7 @@ $mysqli->close(); - +

Database Structure Comparison

@@ -765,4 +766,3 @@ $mysqli->close(); "> -

- Users - -

+

Users

+ @@ -56,38 +46,26 @@ - + @@ -154,46 +126,31 @@ @@ -341,7 +298,7 @@ - + Will not show as an option at Checkout if invoice amount is above this number, 0 disables the threshold check. @@ -79,7 +79,7 @@ ob_start();
- + See here for the latest Stripe Fees. @@ -90,7 +90,7 @@ ob_start();
- + See here for the latest Stripe Fees. diff --git a/admin/modals/payment_provider/payment_provider_edit.php b/admin/modals/payment_provider/payment_provider_edit.php index f5e65cb6..154a6074 100644 --- a/admin/modals/payment_provider/payment_provider_edit.php +++ b/admin/modals/payment_provider/payment_provider_edit.php @@ -58,7 +58,7 @@ ob_start();
- + Will not show as an option at Checkout if above this number @@ -79,7 +79,7 @@ ob_start();
- + See here for the latest Stripe Fees. @@ -90,7 +90,7 @@ ob_start();
- + See here for the latest Stripe Fees. diff --git a/admin/modals/user/user_export.php b/admin/modals/user/user_export.php index b0aa26e2..fc4ead57 100644 --- a/admin/modals/user/user_export.php +++ b/admin/modals/user/user_export.php @@ -1,21 +1,26 @@ - diff --git a/admin/users.php b/admin/users.php index 185ec602..f6370c16 100644 --- a/admin/users.php +++ b/admin/users.php @@ -33,9 +33,16 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()")); @@ -54,7 +61,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
- "> Archived @@ -235,5 +242,4 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
- +
Bulk Action (0) - - diff --git a/agent/assets.php b/agent/assets.php index 237ed926..43d76c3d 100644 --- a/agent/assets.php +++ b/agent/assets.php @@ -92,7 +92,7 @@ if (isset($_GET['tags']) && is_array($_GET['tags']) && !empty($_GET['tags'])) { //Get Asset Counts $row = mysqli_fetch_assoc(mysqli_query($mysqli, " - SELECT + SELECT COUNT(*) AS all_count, SUM(CASE WHEN asset_type IN ('laptop', 'desktop') THEN 1 ELSE 0 END) AS workstation_count, SUM(CASE WHEN asset_type = 'server' THEN 1 ELSE 0 END) AS server_count, @@ -102,8 +102,8 @@ $row = mysqli_fetch_assoc(mysqli_query($mysqli, " FROM ( SELECT assets.* FROM assets LEFT JOIN clients ON client_id = asset_client_id - LEFT JOIN contacts ON asset_contact_id = contact_id - LEFT JOIN locations ON asset_location_id = location_id + LEFT JOIN contacts ON asset_contact_id = contact_id + LEFT JOIN locations ON asset_location_id = location_id LEFT JOIN asset_interfaces ON interface_asset_id = asset_id AND interface_primary = 1 LEFT JOIN asset_tags ON asset_tag_asset_id = asset_id LEFT JOIN tags ON tag_id = asset_tag_tag_id @@ -133,15 +133,12 @@ $network_count = intval($row['network_count']); //Other Count $other_count = intval($row['other_count']); -//Rebuild URL -$url_query_strings_sort = http_build_query($get_copy); - $sql = mysqli_query( $mysqli, "SELECT SQL_CALC_FOUND_ROWS * FROM assets LEFT JOIN clients ON asset_client_id = client_id - LEFT JOIN contacts ON asset_contact_id = contact_id - LEFT JOIN locations ON asset_location_id = location_id + LEFT JOIN contacts ON asset_contact_id = contact_id + LEFT JOIN locations ON asset_location_id = location_id LEFT JOIN asset_interfaces ON interface_asset_id = asset_id AND interface_primary = 1 LEFT JOIN asset_tags ON asset_tag_asset_id = asset_id LEFT JOIN tags ON tag_id = asset_tag_tag_id @@ -203,14 +200,16 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()")); @@ -792,8 +791,4 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()")); 0) { ?> @@ -78,7 +79,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
- + @@ -103,7 +104,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
- "> Archived @@ -283,7 +284,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()")); Archive - +
@@ -299,15 +300,10 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()")); - + - - @@ -128,7 +130,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()")); -
" id="advancedFilter" @@ -220,10 +222,10 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
@@ -364,11 +366,11 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()")); } // Counts - + // Contact Count $row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT('contact_id') AS num FROM contacts WHERE contact_client_id = $client_id AND contact_archived_at IS NULL")); $contact_count = $row['num']; - if ($contact_count) { + if ($contact_count) { $contact_count_display = "$contact_count"; } else { $contact_count_display = ''; @@ -377,34 +379,34 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()")); // Vendors Count $row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT('vendor_id') AS num FROM vendors WHERE vendor_client_id = $client_id AND vendor_archived_at IS NULL")); $vendor_count = $row['num']; - if ($vendor_count) { + if ($vendor_count) { $vendor_count_display = "$vendor_count"; } else { $vendor_count_display = ''; } - + // Asset Count $row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT('asset_id') AS num FROM assets WHERE asset_client_id = $client_id AND asset_archived_at IS NULL")); $asset_count = $row['num']; - if ($asset_count) { + if ($asset_count) { $asset_count_display = "$asset_count"; } else { $asset_count_display = ''; } - + // Credential Count $row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT('credential_id') AS num FROM credentials WHERE credential_client_id = $client_id AND credential_archived_at IS NULL")); $credential_count = $row['num']; - if ($credential_count) { + if ($credential_count) { $credential_count_display = "$credential_count"; } else { $credential_count_display = ''; } - + // Software Count $row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT('software_id') AS num FROM software WHERE software_client_id = $client_id AND software_archived_at IS NULL")); $software_count = $row['num']; - if ($software_count) { + if ($software_count) { $software_count_display = "$software_count"; } else { $software_count_display = ''; @@ -413,7 +415,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()")); // Ticket Count $row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT('ticket_id') AS num FROM tickets WHERE ticket_client_id = $client_id AND ticket_archived_at IS NULL")); $ticket_count = $row['num']; - if ($ticket_count) { + if ($ticket_count) { $ticket_count_display = "$ticket_count"; } else { $ticket_count_display = ''; @@ -518,10 +520,10 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()")); } if (!empty($contact_name)) { ?> -
+ - +
- + Import - + Export
@@ -112,7 +114,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
- + @@ -134,7 +136,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
- "> Archived @@ -219,7 +221,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()")); Bulk Action (0)
- + @@ -566,10 +568,4 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
- +
@@ -198,7 +200,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()")); @@ -366,7 +368,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()")); $credential_contact_display = ''; } - if ($credential_asset_id) { + if ($credential_asset_id) { $credential_asset_display = " @@ -528,15 +530,10 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()")); - - @@ -92,9 +92,9 @@ $page_title = $row['document_name']; } ?> @@ -104,18 +104,31 @@ $page_title = $row['document_name'];
- -

()

-
-
Date:
- -
Prepared By:
- +
+
+ +
+ +
+
+
+
+ Date: + +
+ +
+ Prepared By: + +
+ +
+
- +

Documentation Revision History

@@ -151,15 +164,15 @@ $page_title = $row['document_name']; ?> - - - - - + + + + + - @@ -176,10 +189,10 @@ $page_title = $row['document_name']; - + @@ -189,13 +202,14 @@ $page_title = $row['document_name'];
Related Items
Files -
Contacts -
"> - +
Assets -
@@ -282,7 +297,7 @@ $page_title = $row['document_name']; - +
Licenses -
- +
Vendors -
Portal Collaboration
Document is - + visible"; @@ -414,11 +432,5 @@ $page_title = $row['document_name']; 0) { - $sql_folder = mysqli_query($mysqli, "SELECT folder_name, parent_folder FROM folders WHERE folder_id = $folder_id"); - if ($row_folder = mysqli_fetch_assoc($sql_folder)) { - $folder_name = nullable_htmlentities($row_folder['folder_name']); - $parent_folder = intval($row_folder['parent_folder']); - - // Prepend the folder to the beginning of the array - array_unshift($folder_path, array('folder_id' => $folder_id, 'folder_name' => $folder_name)); - - // Move up to the parent folder - $folder_id = $parent_folder; - } else { - // If the folder is not found, break the loop - break; - } -} - -?> - -
- - -
- - - -
-
-
- "> -
- -
- -
-
-
-
- -
-
-
- - -
- - -
-
-

Folders

-
- -
- -
- - - -
- - -
- - "> - - - - - - - - - - - NOW() - AND item_type = 'Document' - AND item_related_id = $document_id - LIMIT 1" - ); - $row = mysqli_fetch_array($sql_shared); - if($row) { - $item_id = intval($row['item_id']); - $item_active = nullable_htmlentities($row['item_active']); - $item_key = nullable_htmlentities($row['item_key']); - $item_type = nullable_htmlentities($row['item_type']); - $item_related_id = intval($row['item_related_id']); - $item_note = nullable_htmlentities($row['item_note']); - $item_recipient = nullable_htmlentities($row['item_recipient']); - $item_views = nullable_htmlentities($row['item_views']); - $item_view_limit = nullable_htmlentities($row['item_view_limit']); - $item_created_at = nullable_htmlentities($row['item_created_at']); - $item_expire_at = nullable_htmlentities($row['item_expire_at']); - $item_expire_at_human = timeAgo($row['item_expire_at']); - } - - ?> - - - - - - - - - - - - - -
-
- -
-
- - Name - - - - Created - - - - Last Update - - Action
-
- -
-
- -
-
- -
-
- 0) { ?> -
- -
Shared -
- -
-
- -
- -
-
-
-
- -
-
-
-
- - - - -
-
-

Domains

-
-
- - 0) { ?> - - - -
-
-
- -
-
- - +
+
+

Domains

+
+
+ + 0) { ?> + + - -
-
-
- -
- -
-
-
- - -
- -
-
- -
-
- - -
-
- "> - Archived - - -
-
- -
- -
-
- -
- - - - - - text-nowrap"> - - - - - - - - - - - - - - - - - $domain_registrar_name - " : "-"; - $domain_webhost_name_display = $domain_webhost_name ? " - - $domain_webhost_name - " : "-"; - $domain_dnshost_name_display = $domain_dnshost_name ? " - - $domain_dnshost_name - " : "-"; - $domain_mailhost_name_display = $domain_mailhost_name ? " - - $domain_mailhost_name - " : "-"; - - ?> - - - - - - - - - - - - - - - - - -
-
- -
-
- - Domain - - - - Registrar - - - - Web Host - - - - DNS Host - - - - Mail Host - - - - Expires - - - - Client - - Action
-
- -
-
- -
- -
-
-
-
-
-
-
-
- -
- -
- -
- -
-
- +
+
+ + + + +
+
+
+ +
+ +
+
+
+ + +
+ +
+
+ +
+
+ + +
+
+ "> + Archived + + +
+
+ +
+
+
+
+ +
+ + + + + + text-nowrap"> + + + + + + + + + + + + + + + + + $domain_registrar_name + " : "-"; + $domain_webhost_name_display = $domain_webhost_name ? " + + $domain_webhost_name + " : "-"; + $domain_dnshost_name_display = $domain_dnshost_name ? " + + $domain_dnshost_name + " : "-"; + $domain_mailhost_name_display = $domain_mailhost_name ? " + + $domain_mailhost_name + " : "-"; + + ?> + + + + + + + + + + + + + + + + + +
+
+ +
+
+ + Domain + + + + Registrar + + + + Web Host + + + + DNS Host + + + + Mail Host + + + + Expires + + + + Client + + Action
+
+ +
+
+ +
+ +
+
+
+
+
+
+
+
+ +
+ +
+ +
+ +
+
+ +
+
New Expense @@ -94,26 +95,26 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()")); Bulk Action (0)