77 Commits

Author SHA1 Message Date
johnnyq
d2d1a75448 Generate CSRF Token during Client Portal login when using Entra 2026-06-12 17:38:58 -04:00
johnnyq
d3a93652f3 Allow PHP-8.2 and up Compatibility instead of just PHP-8.4 2026-06-12 17:06:10 -04:00
johnnyq
2204bd52f4 Rewrite email parser using ImapEngine, harden processing loop
Replace webklex/php-imap with directorytree/imapengine in the ticket
email parser. ImapEngine is pure PHP over sockets.

Parser improvements:
- Wrap per-message processing in try/catch so one malformed email
  can't abort the run; failures are flagged and logged with UID
- Query unseen + unflagged so previously-failed (flagged) messages
  are no longer re-processed on every cron run
- Skip vacation/auto-responder emails (RFC 3834) to prevent mail
  loops with the ticket auto-reply
- Cap messages per run (50) and attachment size (15MB); inline
  images over 2MB are stored as attachments instead of base64-embedded
  in ticket details
- Atomic lock file creation
- preg_quote() the ticket prefix in subject matching
- Dedupe CC watchers and exclude the sender
- Map webklex 'tls' encryption setting to STARTTLS for compatibility

NDR/DSN parsing now walks MIME parts via the underlying
zbateson parser instead of relying on attachment extraction.
2026-06-12 16:56:39 -04:00
johnnyq
300a1aff9f Login: Uodate Email text field to email type 2026-06-07 13:01:28 -04:00
johnnyq
63d86917ae Generate Secure link: Use intval for item_expires and include HOUR in the insert query 2026-06-02 14:56:21 -04:00
johnnyq
9b77bbdd0b declare intval 2026-05-22 11:26:20 -04:00
johnnyq
a02d78bde5 Bump Version 2026-05-21 10:38:56 -04:00
wrongecho
bd15cbe375 - Stripe saved cards - lock down invoice and client IDs
- Stop app log handling from breaking due to quotes
2026-05-20 17:51:54 +01:00
wrongecho
c5d67cd4f9 Kanban - Enforce per-client perms (ajax) 2026-05-20 14:01:55 +01:00
wrongecho
7211426292 Invoices - Secure date/frequency input handling 2026-05-20 13:32:21 +01:00
johnnyq
67382a4c4c Fix extra } 2026-05-04 16:56:37 -04:00
johnnyq
351e81231f Update Changelog Bump Release 2026-05-04 16:50:18 -04:00
johnnyq
0ecd76329c Bump TCPDF from 6.11.2 to 6.11.3 2026-05-04 16:15:30 -04:00
johnnyq
f5da94a278 Bump TinyMCE from 8.4.0 to 8.5.0 2026-05-04 16:07:56 -04:00
johnnyq
e0f2fc1e1b Enforce Client Access Restriction on ajax call get_totp_token_via_id 2026-05-04 15:48:40 -04:00
johnnyq
8094e6e761 Credentials: Enforce Credential Perm and client perm on credient edit / view modals 2026-05-04 15:37:02 -04:00
johnnyq
a7c4136d60 Enforce Credential Permissions on contact, asset details both full page and modals 2026-05-04 15:30:33 -04:00
johnnyq
67f9f9ec76 Client Overview: Enforce visibility permissions on credentials in client overview 2026-05-04 13:38:31 -04:00
johnnyq
90232c82c0 Fix broken add saved card, by commenting CSRF Check on add saved card in client post 2026-05-04 12:59:01 -04:00
johnnyq
0cdb780b88 Stripe Payment: Rollback stipe-php from 20.0.0 back to 19.4.1 to fix a isses with adding saved paymentss, Stripe updated their API in which we will update to a later date 2026-05-04 12:32:12 -04:00
johnnyq
a6d996b83f Projects: Fixing missing ' regression 2026-04-27 13:17:21 -04:00
johnnyq
b6e3176ed0 Projects: Fix slow load by using an optimized query to count tickets and tasks 2026-04-27 13:15:11 -04:00
wrongecho
e762ebb88b Show correct currency for the account balance when adding payment to invoice 2026-04-27 14:07:05 +01:00
wrongecho
163581cbcd Expire all PW reset tokens nightly with cron 2026-04-27 13:36:39 +01:00
johnnyq
e9c3ee4661 Shared Items via secure link: Do not delete shared items that have not been viewed before cron runs 2026-04-24 11:40:38 -04:00
johnnyq
90de4e4fe3 Client: Fix Client Abbreviation being converted to an int on edit 2026-04-17 12:15:56 -04:00
johnnyq
cb9ac33fbe Update changelog 2026-04-16 15:18:47 -04:00
johnnyq
319ccbad01 Revert "API: Add some missing end points"
This reverts commit c4ba2bc326.
2026-04-16 15:16:08 -04:00
johnnyq
4e862053e8 Fix Missing CSRF in unused modals 2026-04-13 13:19:12 -04:00
johnnyq
9c0c8ec239 Update Changelog 2026-04-12 11:41:15 -04:00
johnnyq
715aadb9d3 Update Changelog 2026-04-12 11:34:46 -04:00
johnnyq
f94458690e API: Remove Payment Endpoint for now 2026-04-12 11:26:56 -04:00
johnnyq
134b5e6491 Update changelog 2026-04-11 18:47:35 -04:00
johnnyq
c4ba2bc326 API: Add some missing end points 2026-04-11 18:21:03 -04:00
johnnyq
1e02322382 API: Invoice_items only return if item_id or invoice_id is specified and do not return all invoice items 2026-04-11 17:57:22 -04:00
johnnyq
81f87e3960 Update Changelog 2026-04-11 14:07:11 -04:00
johnnyq
efaeac3c14 Networks: Added Import Networks 2026-04-11 14:06:31 -04:00
johnnyq
bb06ced05b update Changelog 2026-04-11 13:43:26 -04:00
johnnyq
ed564f64ff Merge branch 'develop' of github.com:itflow-org/itflow into develop 2026-04-11 13:33:30 -04:00
johnnyq
830f40edee API: Add Invoice Items read endpoint 2026-04-11 13:32:50 -04:00
wrongecho
ec9a148e97 API - Fix documents not falling back to DB values 2026-04-09 16:07:44 +01:00
wrongecho
5efdfc53ff Asset API - History 2026-04-09 15:25:21 +01:00
johnnyq
c1bb1694a0 Update Changelog 2026-04-08 12:54:05 -04:00
johnnyq
82b5613335 Bump TinyMCE from 8.3.2 to 8.4.0 2026-04-08 12:51:25 -04:00
johnnyq
123a581583 Bump stripe-php from 19.4.1 to 20.0.0 2026-04-08 12:44:45 -04:00
johnnyq
346d7ed9f0 Update the changelog 2026-04-05 12:06:12 -04:00
johnnyq
b79a6c390e Move some arrays out of load global settings and into their own entity 2026-04-05 12:00:56 -04:00
johnnyq
fae74a8b1e Software: Add Additional License Types 2026-04-05 11:48:07 -04:00
johnnyq
c434b5e6f0 Categories: Fix Restore function and restore icon and color 2026-04-04 18:22:05 -04:00
johnnyq
e7ed88e10e Remove custom_hosting tables from db.sql 2026-04-04 18:14:13 -04:00
johnnyq
78971d1ccb Setup cli: add categories to mimic setup web ui 2026-04-04 18:09:29 -04:00
johnnyq
8a4bb2e80d Update App version 2026-04-04 17:51:14 -04:00
johnnyq
c25b5aac9d Update Changelog 2026-04-04 17:49:48 -04:00
johnnyq
4856c5cb1f Update Changelog 2026-04-04 17:46:56 -04:00
johnnyq
065e674a68 Quotes: Fix Quote Copy missing client 2026-04-04 17:38:12 -04:00
wrongecho
20be416fa6 API
- Fix credential errors since field names changed
- Sort domains properly
2026-04-04 11:20:52 +01:00
wrongecho
4617f44cda - Prevent error 500s when existing data can't be cleanly re-inserted to database 2026-04-04 11:01:34 +01:00
wrongecho
0ae4c2fac9 - Prevent error 500s when existing data can't be cleanly re-inserted to database
- Full support for asset_uri_2
2026-04-04 10:52:32 +01:00
wrongecho
9389f2cc91 More helpful error 2026-04-04 10:49:50 +01:00
wrongecho
52c2ba69cb Don't set client ID from POST - this is properly done via require_post_method instead only if it's an all-clients key. 2026-04-04 10:49:40 +01:00
wrongecho
8591758cd7 Fix asset read api - uri_2 2026-04-04 10:18:21 +01:00
johnnyq
02d217402c Categories: Moved note type, software type, rack type to be creatable/editable Categories with common defaults and descriptions 2026-03-31 18:05:16 -04:00
johnnyq
52eb0b9c21 Files: Allow .swb file extensions for MikroTik Backups 2026-03-31 17:19:27 -04:00
johnnyq
5698da9c5b Update Changelog 2026-03-30 17:41:52 -04:00
johnnyq
aa19472512 Ticket: Fix missing contact in version 1 Create Ticket 2026-03-30 17:38:44 -04:00
johnnyq
11288ce782 Fix missing setting DB Version 2026-03-30 17:31:15 -04:00
johnnyq
383897ee27 Assets: Moved asset status to be creatable/editable Categories with common defaults and descriptions also Add category description to add / edit modals and description underneath category name 2026-03-30 17:25:51 -04:00
johnnyq
7bed0231d2 Categories: Make category type pretty Capitalize words, replace _ with space 2026-03-30 15:29:13 -04:00
johnnyq
d8359e1049 Network Interface Types: Moved to creatable/editable Categories with common defaults 2026-03-30 15:24:09 -04:00
johnnyq
35fb8b1ee6 Update Changelog for 26.03.1 Release 2026-03-30 11:58:22 -04:00
wrongecho
793b14885b Remove unnecessary blank line in account.php 2026-03-30 10:28:45 +01:00
johnnyq
bb3e311fb7 Racks: Fix Device Removal 2026-03-27 17:02:13 -04:00
johnnyq
65b53dd7f4 Tables: Removed removed -sm in table-responsive to fix some responsive issues also adding margin between top and bottom tags in clients and moved the tags in its own div 2026-03-26 13:30:07 -04:00
johnnyq
0e75106e1a Client POST: Added comment regarding MySQLi Prepared statement usage within Client add/edit POST 2026-03-23 13:30:03 -04:00
wrongecho
619dd0d472 Fix missing csrf tokens in category modals 2026-03-23 16:36:15 +00:00
johnnyq
22d33847c3 Fix edit client 2026-03-22 19:53:35 -04:00
johnnyq
6d1b654d0c Invoice: Do not apply late fee on first overdue reminder (day 1) 2026-03-20 17:44:33 -04:00
1078 changed files with 120405 additions and 4063 deletions

View File

@@ -2,16 +2,61 @@
This file documents all notable changes made to ITFlow.
## [26.05.1] Stable Release
- Security Fixes.
## [26.05] Stable Release
### Bug Fixes
- Stripe Payment: Fix adding saved cards on client portal.
- Various client and module enforments fixes.
- Projects: Fix slow load by using an optimized query to count tickets and tasks.
- Show correct currency for the account balance when adding payment to invoice.
- Expire all Password reset tokens nightly with cron.
- Shared Items via secure link: Do not delete shared items that have not been viewed before cron runs.
- Client: Fix Client Abbreviation being converted to an int on edit.
### New Features & Updates
- Bump TinyMCE from 8.4.0 to 8.5.0.
- Bump TCPDF from 6.11.2 to 6.11.3.
- DeBump stripe-php from 20.0.0 to 19.4.1.
## [26.04] Stable Release
### Bug Fixes
- Racks: Fix Device Removal.
- Table Lists: replace class table-responsive-sm with just table-reponsive was causing ui issues with certain screen sizes.
- Client: Fix Edit erroring on certain characters.
- Category: Fix Add/Edit due to missing CSRF fields.
- Category: Fix Restore function and Icon and text color.
- Invoice: Do not apply late fee on first overdue reminder (1 day).
- Ticket: Fix issue with contact not being added with Add contact modal v1.
- Quote: Fix Copy was missing client.
- API: Don't set client ID from POST - this is properly done via require_post_method instead only if it's an all-clients key.
- API: Prevent error 500s when existing data can't be cleanly re-inserted to database.
- API: Add more helpful errors.
- API: Fix asset read uri_2 field.
- API: Various other field fixes.
### New Features & Updates
- Categories: Add Description Field.
- Categories: Add DB Field for order.
- Categories: Move Asset Status and Network Interface Type to categories so custom ones can be created and edited.
- Categories: Moved note type, software type, rack type to be creatable/editable Categories with common defaults and descriptions
- Files: Allow .swb file for MikroTik Backup Files.
- Software: Added additonal License Types including Perpetual, Site, etc.
- API: Invoice Items: Add read endpoint.
- Networks: Added Import.
- Bump TinyMCE from 8.3.2 to 8.4.0.
- Bump stripe-php from 19.4.1 to 20.0.0.
## [26.03] Stable Release
### Bug Fixes
- Ticket Templates: Fix Task Sorting.
- Ticket Templates: Fix Task Sortinhahahg.
- Ticket: Lower autoclose setting minimum value from 48 to 24 Hours.
- Ticket: Fix Task Approval.
- Recurring Ticket: add empty value placeholder for Ticket Frequency.
- Documents/Files: Fix redirect after File Upload to redirect to files instead of the non existent documents.
- Setup: Fix base url tacking on /setup when not installing via script.
### New Features & Updates
- Clients: Net Terms: Added common 45 and 15 Days, removed 14 Days not as common.
- Clients: Bulk Action Set Net Terms Added.

View File

@@ -28,14 +28,14 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
<div class="card card-dark">
<div class="card-header py-2">
<h3 class="card-title mt-2"><i class="fa fa-fw fa-list-ul mr-2"></i>
<?php echo nullable_htmlentities($category); ?> Categories
<?= nullable_htmlentities(ucwords(str_replace('_', ' ', $category))); ?> Categories
</h3>
<?php
if (!isset($_GET['archived'])) {
?>
<div class="card-tools">
<button type="button" class="btn btn-primary ajax-modal" data-modal-url="modals/category/category_add.php?category=<?= nullable_htmlentities($category) ?>"><i
class="fas fa-plus mr-2"></i>New <?php echo nullable_htmlentities($category); ?> Category</button>
class="fas fa-plus mr-2"></i>New <?= nullable_htmlentities(ucwords(str_replace('_', ' ', $category))); ?> Category</button>
</div>
<?php
}
@@ -51,7 +51,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
value="<?php if (isset($q)) {
echo stripslashes(nullable_htmlentities($q));
} ?>"
placeholder="Search <?php echo nullable_htmlentities($category); ?> Categories ">
placeholder="Search <?= nullable_htmlentities(ucwords(str_replace('_', ' ', $category))); ?> Categories ">
<div class="input-group-append">
<button class="btn btn-primary"><i class="fa fa-search"></i></button>
</div>
@@ -83,6 +83,36 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
} else {
echo 'btn-default';
} ?>">Ticket</a>
<a href="?category=network_interface"
class="btn <?php if ($category == 'network_interface') {
echo 'btn-primary';
} else {
echo 'btn-default';
} ?>">Network Interface</a>
<a href="?category=asset_status"
class="btn <?php if ($category == 'asset_status') {
echo 'btn-primary';
} else {
echo 'btn-default';
} ?>">Asset Status</a>
<a href="?category=software_type"
class="btn <?php if ($category == 'software_type') {
echo 'btn-primary';
} else {
echo 'btn-default';
} ?>">Software Type</a>
<a href="?category=rack_type"
class="btn <?php if ($category == 'rack_type') {
echo 'btn-primary';
} else {
echo 'btn-default';
} ?>">Rack Type</a>
<a href="?category=contact_note_type"
class="btn <?php if ($category == 'contact_note_type') {
echo 'btn-primary';
} else {
echo 'btn-default';
} ?>">Contact Note Type</a>
<a href="?<?php echo $url_query_strings_sort ?>&archived=1"
class="btn <?php if (isset($_GET['archived'])) {
echo 'btn-primary';
@@ -114,6 +144,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
while ($row = mysqli_fetch_assoc($sql)) {
$category_id = intval($row['category_id']);
$category_name = nullable_htmlentities($row['category_name']);
$category_description = nullable_htmlentities($row['category_description']);
$category_color = nullable_htmlentities($row['category_color']);
?>
@@ -122,6 +153,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
<a class="text-dark ajax-modal" href="#"
data-modal-url="modals/category/category_edit.php?id=<?= $category_id ?>">
<?php echo $category_name; ?>
<div><small class="text-secondary"><?= $category_description ?></small></div>
</a>
</td>
<td><i class="fa fa-3x fa-circle" style="color:<?php echo $category_color; ?>;"></i></td>
@@ -134,9 +166,9 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
<?php
if ($archived) {
?>
<a class="dropdown-item text-success confirm-link"
<a class="dropdown-item text-info confirm-link"
href="post.php?restore_category=<?php echo $category_id; ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-archive mr-2"></i>Restore
<i class="fas fa-fw fa-redo mr-2"></i>Restore
</a>
<a class="dropdown-item text-danger confirm-link"
href="post.php?delete_category=<?php echo $category_id; ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">

View File

@@ -4334,11 +4334,70 @@ if (LATEST_DATABASE_VERSION > CURRENT_DATABASE_VERSION) {
mysqli_query($mysqli, "UPDATE `settings` SET `config_current_database_version` = '2.4.2'");
}
//
// // if (CURRENT_DATABASE_VERSION == '2.4.2') {
// // Insert queries here required to update to DB version 2.4.3
if (CURRENT_DATABASE_VERSION == '2.4.2') {
mysqli_query($mysqli, "ALTER TABLE `categories` ADD `category_description` VARCHAR(255) DEFAULT NULL AFTER `category_name`");
mysqli_query($mysqli, "ALTER TABLE `categories` ADD `category_order` INT(11) NOT NULL DEFAULT 0 AFTER `category_icon`");
// Create network_interfaces
mysqli_query($mysqli, "INSERT INTO categories SET category_name = 'Ethernet', category_type = 'network_interface', category_order = 1"); // 1
mysqli_query($mysqli, "INSERT INTO categories SET category_name = 'SFP', category_type = 'network_interface', category_order = 2"); // 2
mysqli_query($mysqli, "INSERT INTO categories SET category_name = 'SFP+', category_type = 'network_interface', category_order = 3"); // 3
mysqli_query($mysqli, "INSERT INTO categories SET category_name = 'QSFP28', category_type = 'network_interface', category_order = 4"); // 4
mysqli_query($mysqli, "INSERT INTO categories SET category_name = 'QSFP-DD', category_type = 'network_interface', category_order = 5"); // 5
mysqli_query($mysqli, "INSERT INTO categories SET category_name = 'Coaxial', category_type = 'network_interface', category_order = 6"); // 6
mysqli_query($mysqli, "INSERT INTO categories SET category_name = 'Fiber', category_type = 'network_interface', category_order = 7"); // 7
mysqli_query($mysqli, "INSERT INTO categories SET category_name = 'WiFi', category_type = 'network_interface', category_order = 8"); // 8
mysqli_query($mysqli, "UPDATE `settings` SET `config_current_database_version` = '2.4.3'");
}
if (CURRENT_DATABASE_VERSION == '2.4.3') {
// Asset Status
mysqli_query($mysqli, "INSERT INTO categories SET category_name = 'Ready to Deploy', category_description = 'Asset is configured and ready to be assigned', category_type = 'asset_status', category_order = 1"); // 1
mysqli_query($mysqli, "INSERT INTO categories SET category_name = 'Deployed', category_description = 'Asset is actively in use and assigned to a client or location', category_type = 'asset_status', category_order = 2"); // 2
mysqli_query($mysqli, "INSERT INTO categories SET category_name = 'Out for Repair', category_description = 'Asset has been sent out for servicing or repair', category_type = 'asset_status', category_order = 3"); // 3
mysqli_query($mysqli, "INSERT INTO categories SET category_name = 'Lost', category_description = 'Asset location is unknown and cannot be accounted for', category_type = 'asset_status', category_order = 4"); // 4
mysqli_query($mysqli, "INSERT INTO categories SET category_name = 'Stolen', category_description = 'Asset has been reported stolen', category_type = 'asset_status', category_order = 5"); // 5
mysqli_query($mysqli, "INSERT INTO categories SET category_name = 'Retired', category_description = 'Asset has been decommissioned and is no longer in service', category_type = 'asset_status', category_order = 6"); // 6
// Contact note types
mysqli_query($mysqli, "INSERT INTO categories SET category_name = 'Call', category_description = 'Phone call with a client or contact', category_icon = 'fa-phone-alt', category_type = 'contact_note_type', category_order = 1"); // 1
mysqli_query($mysqli, "INSERT INTO categories SET category_name = 'Email', category_description = 'Email correspondence with a client or contact', category_icon = 'fa-envelope', category_type = 'contact_note_type', category_order = 2"); // 2
mysqli_query($mysqli, "INSERT INTO categories SET category_name = 'Meeting', category_description = 'Scheduled meeting with a client or contact', category_icon = 'fa-handshake', category_type = 'contact_note_type', category_order = 3"); // 3
mysqli_query($mysqli, "INSERT INTO categories SET category_name = 'In Person', category_description = 'In person visit or on-site interaction', category_icon = 'fa-people-arrows', category_type = 'contact_note_type', category_order = 4"); // 4
mysqli_query($mysqli, "INSERT INTO categories SET category_name = 'Note', category_description = 'General note or internal comment', category_icon = 'fa-sticky-note', category_type = 'contact_note_type', category_order = 5"); // 5
// Rack Types
mysqli_query($mysqli, "INSERT INTO categories SET category_name = '2-Post Open Frame', category_description = 'Two-post open frame rack for patch panels and lightweight equipment', category_type = 'rack_type', category_order = 1"); // 1
mysqli_query($mysqli, "INSERT INTO categories SET category_name = '4-Post Open Frame', category_description = 'Four-post open frame rack for servers and heavier equipment', category_type = 'rack_type', category_order = 2"); // 2
mysqli_query($mysqli, "INSERT INTO categories SET category_name = '4-Post Enclosed Cabinet', category_description = 'Four-post enclosed cabinet with doors and sides for secure equipment housing', category_type = 'rack_type', category_order = 3"); // 3
mysqli_query($mysqli, "INSERT INTO categories SET category_name = 'Wall-Mount Open', category_description = 'Open frame rack mounted directly to a wall for small deployments', category_type = 'rack_type', category_order = 4"); // 4
mysqli_query($mysqli, "INSERT INTO categories SET category_name = 'Wall-Mount Enclosed', category_description = 'Enclosed cabinet rack mounted to a wall with a locking door', category_type = 'rack_type', category_order = 5"); // 5
mysqli_query($mysqli, "INSERT INTO categories SET category_name = 'Other', category_description = 'Rack type does not fit any standard category', category_type = 'rack_type', category_order = 6"); // 6
// Software Types
mysqli_query($mysqli, "INSERT INTO categories SET category_name = 'Software as a Service (SaaS)', category_description = 'Cloud-hosted software accessed via a web browser or API', category_type = 'software_type', category_order = 1"); // 1
mysqli_query($mysqli, "INSERT INTO categories SET category_name = 'Productivity Suite', category_description = 'Bundled office and collaboration tools such as Microsoft 365 or Google Workspace', category_type = 'software_type', category_order = 2"); // 2
mysqli_query($mysqli, "INSERT INTO categories SET category_name = 'Web Application', category_description = 'Application hosted on a web server and accessed through a browser', category_type = 'software_type', category_order = 3"); // 3
mysqli_query($mysqli, "INSERT INTO categories SET category_name = 'Desktop Application', category_description = 'Application installed and run locally on a workstation or laptop', category_type = 'software_type', category_order = 4"); // 4
mysqli_query($mysqli, "INSERT INTO categories SET category_name = 'Mobile Application', category_description = 'Application installed and run on a mobile device or tablet', category_type = 'software_type', category_order = 5"); // 5
mysqli_query($mysqli, "INSERT INTO categories SET category_name = 'Security Software', category_description = 'Software providing antivirus, endpoint protection, or security monitoring', category_type = 'software_type', category_order = 6"); // 6
mysqli_query($mysqli, "INSERT INTO categories SET category_name = 'System Software', category_description = 'Low-level software managing hardware resources and system operations', category_type = 'software_type', category_order = 7"); // 7
mysqli_query($mysqli, "INSERT INTO categories SET category_name = 'Operating System', category_description = 'Core software managing hardware and providing a platform for applications', category_type = 'software_type', category_order = 8"); // 8
mysqli_query($mysqli, "INSERT INTO categories SET category_name = 'Other', category_description = 'Software type does not fit any standard category', category_type = 'software_type', category_order = 9"); // 9
mysqli_query($mysqli, "UPDATE `settings` SET `config_current_database_version` = '2.4.4'");
}
// if (CURRENT_DATABASE_VERSION == '2.4.4') {
// // Insert queries here required to update to DB version 2.4.5
// // Then, update the database to the next sequential version
// mysqli_query($mysqli, "UPDATE `settings` SET `config_current_database_version` = '2.4.3'");
// mysqli_query($mysqli, "UPDATE `settings` SET `config_current_database_version` = '2.4.5'");
// }
} else {

View File

@@ -9,12 +9,13 @@ $category_types_array = ['Expense', 'Income', 'Referral', 'Ticket'];
?>
<div class="modal-header bg-dark">
<h5 class="modal-title"><i class="fa fa-fw fa-list-ul mr-2"></i>New <strong><?= nullable_htmlentities($category) ?></strong> Category</h5>
<h5 class="modal-title"><i class="fa fa-fw fa-list-ul mr-2"></i>New <strong><?= nullable_htmlentities(ucwords(str_replace('_', ' ', $category))); ?></strong> Category</h5>
<button type="button" class="close text-white" data-dismiss="modal">
<span>&times;</span>
</button>
</div>
<form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token'] ?>">
<div class="modal-body">
@@ -59,6 +60,16 @@ $category_types_array = ['Expense', 'Income', 'Referral', 'Ticket'];
</div>
</div>
<div class="form-group">
<label>Description</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-fw fa-align-left"></i></span>
</div>
<input type="text" class="form-control" name="description" placeholder="Enter a description" maxlength="200">
</div>
</div>
</div>
<div class="modal-footer">
<button type="submit" name="add_category" class="btn btn-primary text-bold"><i class="fa fa-check mr-2"></i>Create Category</button>

View File

@@ -8,6 +8,7 @@ $sql = mysqli_query($mysqli, "SELECT * FROM categories WHERE category_id = $cate
$row = mysqli_fetch_assoc($sql);
$category_name = nullable_htmlentities($row['category_name']);
$category_description = nullable_htmlentities($row['category_description']);
$category_color = nullable_htmlentities($row['category_color']);
$category_type = nullable_htmlentities($row['category_type']);
@@ -21,6 +22,7 @@ ob_start();
</button>
</div>
<form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token'] ?>">
<input type="hidden" name="category_id" value="<?php echo $category_id; ?>">
<input type="hidden" name="type" value="<?php echo $category_type; ?>">
<div class="modal-body">
@@ -45,6 +47,16 @@ ob_start();
</div>
</div>
<div class="form-group">
<label>Description</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-fw fa-align-left"></i></span>
</div>
<input type="text" class="form-control" name="description" placeholder="Enter a description" maxlength="200" value="<?= $category_description ?>">
</div>
</div>
</div>
<div class="modal-footer">
<button type="submit" name="edit_category" class="btn btn-primary text-bold"><i class="fa fa-check mr-2"></i>Save</button>

View File

@@ -30,6 +30,8 @@ ob_start();
</ul>
<form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token'] ?>">
<div class="modal-body">
<div class="tab-content" id="contractTemplateTabContent">

View File

@@ -52,6 +52,7 @@ ob_start();
</ul>
<form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token'] ?>">
<input type="hidden" name="contract_template_id" value="<?php echo $contract_template_id; ?>">
<div class="modal-body">

View File

@@ -8,6 +8,7 @@
</button>
</div>
<form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token'] ?>">
<input type="hidden" name="table" value="<?php echo nullable_htmlentities($table); ?>">
<div class="modal-body">

View File

@@ -8,6 +8,7 @@
</button>
</div>
<form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token'] ?>">
<input type="hidden" name="custom_field_id" value="<?php echo $custom_field_id; ?>">
<div class="modal-body">

View File

@@ -2,6 +2,16 @@
require_once '../../../includes/modal_header.php';
$license_types_array = array (
'Device',
'User',
'Site',
'Concurrent',
'Trial',
'Perpetual',
'Usage-based'
);
ob_start();
?>
@@ -54,9 +64,18 @@ ob_start();
<span class="input-group-text"><i class="fa fa-fw fa-tag"></i></span>
</div>
<select class="form-control select2" name="type" required>
<option value="">- Type -</option>
<?php foreach($software_types_array as $software_type) { ?>
<option><?php echo $software_type; ?></option>
<option value="">- Select Type -</option>
<<?php
$sql_software_types_select = mysqli_query($mysqli, "
SELECT category_name FROM categories
WHERE category_type = 'software_type'
AND category_archived_at IS NULL
ORDER BY category_order ASC, category_name ASC
");
while ($row = mysqli_fetch_assoc($sql_software_types_select)) {
$software_type_select = nullable_htmlentities($row['category_name']);
?>
<option><?= $software_type_select ?></option>
<?php } ?>
</select>
</div>

View File

@@ -13,8 +13,18 @@ $software_type = nullable_htmlentities($row['software_template_type']);
$software_license_type = nullable_htmlentities($row['software_template_license_type']);
$software_notes = nullable_htmlentities($row['software_template_notes']);
// Generate the HTML form content using output buffering.
$license_types_array = array (
'Device',
'User',
'Site',
'Concurrent',
'Trial',
'Perpetual',
'Usage-based'
);
ob_start();
?>
<div class="modal-header bg-dark">
@@ -66,8 +76,20 @@ ob_start();
<span class="input-group-text"><i class="fa fa-fw fa-tag"></i></span>
</div>
<select class="form-control select2" name="type" required>
<?php foreach($software_types_array as $software_type_select) { ?>
<option <?php if($software_type == $software_type_select) { echo "selected"; } ?>><?php echo $software_type_select; ?></option>
<option value="">- Select Type -</option>
<<?php
$sql_software_types_select = mysqli_query($mysqli, "
SELECT category_name FROM categories
WHERE category_type = 'software_type'
AND category_archived_at IS NULL
ORDER BY category_order ASC, category_name ASC
");
while ($row = mysqli_fetch_assoc($sql_software_types_select)) {
$software_type_select = nullable_htmlentities($row['category_name']);
?>
<option <?php if($software_type == $software_type_select) { echo "selected"; } ?>>
<?= $software_type_select ?>
</option>
<?php } ?>
</select>
</div>

View File

@@ -13,6 +13,7 @@ ob_start();
</button>
</div>
<form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token'] ?>">
<div class="modal-body">
</div>

View File

@@ -12,7 +12,7 @@ if (isset($_POST['add_category'])) {
require_once 'category_model.php';
mysqli_query($mysqli,"INSERT INTO categories SET category_name = '$name', category_type = '$type', category_color = '$color'");
mysqli_query($mysqli,"INSERT INTO categories SET category_name = '$name', category_description = '$description', category_type = '$type', category_color = '$color'");
$category_id = mysqli_insert_id($mysqli);
@@ -32,7 +32,7 @@ if (isset($_POST['edit_category'])) {
$category_id = intval($_POST['category_id']);
mysqli_query($mysqli,"UPDATE categories SET category_name = '$name', category_type = '$type', category_color = '$color' WHERE category_id = $category_id");
mysqli_query($mysqli,"UPDATE categories SET category_name = '$name', category_description = '$description', category_type = '$type', category_color = '$color' WHERE category_id = $category_id");
logAction("Category", "Edit", "$session_name edited category $type $name", 0, $category_id);
@@ -68,7 +68,7 @@ if (isset($_GET['restore_category'])) {
validateCSRFToken($_GET['csrf_token']);
$category_id = intval($_GET['retore_category']);
$category_id = intval($_GET['restore_category']);
// Get Category Name and Type for logging
$sql = mysqli_query($mysqli,"SELECT category_name, category_type FROM categories WHERE category_id = $category_id");

View File

@@ -2,5 +2,6 @@
defined('FROM_POST_HANDLER') || die("Direct file access is not allowed");
$name = sanitizeInput($_POST['name']);
$description = sanitizeInput($_POST['description']);
$type = sanitizeInput($_POST['type']);
$color = sanitizeInput($_POST['color']);

View File

@@ -1,6 +1,24 @@
<?php
require_once "includes/inc_all_admin.php";
$start_page_select_array = array (
'dashboard.php'=>'Dashboard',
'clients.php'=> 'Client Management',
'tickets.php'=> 'Support Tickets',
'invoices.php' => 'Invoices'
);
$net_terms_array = array (
'0'=>'On Receipt',
'7'=>'7 Days',
'10'=>'10 Days',
'15'=>'15 Days',
'30'=>'30 Days',
'45'=>'45 Days',
'60'=>'60 Days',
'90'=>'90 Days'
);
?>
<div class="card card-dark">

View File

@@ -1,5 +1,27 @@
<?php
require_once "includes/inc_all_admin.php";
$theme_colors_array = array (
'lightblue',
'blue',
'cyan',
'green',
'olive',
'teal',
'red',
'maroon',
'pink',
'purple',
'indigo',
'fuchsia',
'yellow',
'orange',
'yellow',
'black',
'navy',
'gray'
);
?>
<div class="card card-dark">

View File

@@ -38,7 +38,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
</div>
</form>
<hr>
<div class="table-responsive-sm">
<div class="table-responsive">
<table class="table table-striped table-borderless table-hover">
<thead class="text-dark <?php if ($num_rows[0] == 0) { echo "d-none"; } ?>">
<tr>

View File

@@ -170,15 +170,15 @@ if (isset($_GET['share_generate_link'])) {
if ($item_view_limit == 1) {
$item_view_limit_wording = " and may only be viewed <strong>once</strong>, before the link is destroyed.";
}
$item_expires = sanitizeInput($_GET['expires']);
$item_expires = intval($_GET['expires']);
$item_expires_friendly = "never"; // default never
if ($item_expires == "1 HOUR") {
if ($item_expires == 1) {
$item_expires_friendly = "1 hour";
} elseif ($item_expires == "24 HOUR") {
} elseif ($item_expires == 24) {
$item_expires_friendly = "1 day";
} elseif ($item_expires == "168 HOUR") {
} elseif ($item_expires == 168) {
$item_expires_friendly = "1 week";
} elseif ($item_expires == "730 HOUR") {
} elseif ($item_expires == 730) {
$item_expires_friendly = "1 month";
}
@@ -215,7 +215,7 @@ if (isset($_GET['share_generate_link'])) {
}
// Insert entry into DB
$sql = mysqli_query($mysqli, "INSERT INTO shared_items SET item_active = 1, item_key = '$item_key', item_type = '$item_type', item_related_id = $item_id, item_encrypted_username = '$item_encrypted_username', item_encrypted_credential = '$item_encrypted_credential', item_note = '$item_note', item_recipient = '$item_email', item_views = 0, item_view_limit = $item_view_limit, item_expire_at = NOW() + INTERVAL + $item_expires, item_client_id = $client_id");
$sql = mysqli_query($mysqli, "INSERT INTO shared_items SET item_active = 1, item_key = '$item_key', item_type = '$item_type', item_related_id = $item_id, item_encrypted_username = '$item_encrypted_username', item_encrypted_credential = '$item_encrypted_credential', item_note = '$item_note', item_recipient = '$item_email', item_views = 0, item_view_limit = $item_view_limit, item_expire_at = NOW() + INTERVAL + $item_expires HOUR, item_client_id = $client_id");
$share_id = $mysqli->insert_id;
// Return URL
@@ -403,6 +403,8 @@ if (isset($_GET['get_totp_token_via_id'])) {
$totp_secret = $sql['credential_otp_secret'];
$client_id = intval($sql['credential_client_id']);
enforceClientAccess();
$otp = TokenAuth6238::getTokenCode(strtoupper($totp_secret));
echo json_encode($otp);
@@ -452,6 +454,12 @@ if (isset($_POST['update_kanban_ticket'])) {
foreach ($positions as $position) {
$ticket_id = intval($position['ticket_id']);
// Client perms check
$client_query = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT ticket_client_id FROM tickets WHERE ticket_id = $ticket_id"));
$client_id = intval($client_query['ticket_client_id']);
enforceClientAccess();
$kanban = intval($position['ticket_order']); // ticket kanban position
$status = intval($position['ticket_status']); // ticket statuses
$oldStatus = intval($position['ticket_oldStatus']); // ticket old status if moved

View File

@@ -615,6 +615,8 @@ if (isset($_GET['asset_id'])) {
</form>
</div>
<?php if (lookupUserPermission('module_credential')) { // Begin Credential Enforcement ?>
<div class="card card-dark <?php if ($credential_count == 0) { echo "d-none"; } ?>">
<div class="card-header">
<h3 class="card-title"><i class="fa fa-fw fa-key mr-2"></i>Credentials</h3>
@@ -744,6 +746,8 @@ if (isset($_GET['asset_id'])) {
</div>
</div>
<?php } // End Credential Enforcement ?>
<div class="card card-dark <?php if ($software_count == 0) { echo "d-none"; } ?>">
<div class="card-header py-2">
<h3 class="card-title mt-2"><i class="fa fa-fw fa-cube mr-2"></i>Licenses</h3>

View File

@@ -148,7 +148,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
</div>
</form>
<hr>
<div class="table-responsive-sm">
<div class="table-responsive">
<form id="bulkActions" action="post.php" method="post">
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token'] ?>">

View File

@@ -349,7 +349,7 @@ $sql_asset_retired = mysqli_query(
<?php } ?>
<?php if (mysqli_num_rows($sql_favorite_credentials) > 0) { ?>
<?php if ((mysqli_num_rows($sql_favorite_credentials) > 0) && (lookupUserPermission('module_credential'))) { ?>
<div class="col-md-4">

View File

@@ -444,7 +444,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
}
$client_tag_id_array[] = $client_tag_id;
$client_tag_name_display_array[] = "<a href='clients.php?tags[]=$client_tag_id'><span class='badge badge-pill text-light p-2 mr-1' style='background-color: $client_tag_color;'><i class='fas fa-$client_tag_icon fa-fw mr-1'></i>$client_tag_name</span></a>";
$client_tag_name_display_array[] = "<a href='clients.php?tags[]=$client_tag_id'><span class='mt-1 badge badge-pill text-light p-2 mr-1' style='background-color: $client_tag_color;'><i class='fas fa-$client_tag_icon fa-fw mr-1'></i>$client_tag_name</span></a>";
}
$client_tags_display = implode('', $client_tag_name_display_array);
@@ -512,10 +512,10 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
<div class="media-body">
<div class="text-bold"><?= $client_name ?></div>
<div class="text-secondary"><?= $client_type ?></div>
<div><?php if ($client_tags_display) { echo $client_tags_display; } ?></div>
</div>
</div>
</div>
</div>
<div class="ml-2"><?php if ($client_tags_display) { echo $client_tags_display; } ?></div>
</a>
</td>
<td>

View File

@@ -507,6 +507,8 @@ if (isset($_GET['contact_id'])) {
</div>
</div>
<?php if (lookupUserPermission('module_credential')) { // Begin Credential Enforcement ?>
<div class="card card-dark <?php if ($credential_count == 0) { echo "d-none"; } ?>">
<div class="card-header py-2">
<h3 class="card-title mt-2"><i class="fa fa-fw fa-key mr-2"></i>Credentials</h3>
@@ -644,6 +646,8 @@ if (isset($_GET['contact_id'])) {
</div>
</div>
<?php } // End Credential Enforcement ?>
<div class="card card-dark <?php if ($software_count == 0) { echo "d-none"; } ?>">
<div class="card-header py-2">
<h3 class="card-title mt-2"><i class="fa fa-fw fa-cube mr-2"></i>Related Licenses</h3>
@@ -1101,6 +1105,14 @@ if (isset($_GET['contact_id'])) {
<tbody>
<?php
$note_types_array = array (
'Call'=>'fa-phone-alt',
'Email'=>'fa-envelope',
'Meeting'=>'fa-handshake',
'In Person'=>'fa-people-arrows',
'Note'=>'fa-sticky-note'
);
while ($row = mysqli_fetch_assoc($sql_related_notes)) {
$contact_note_id = intval($row['contact_note_id']);
$contact_note_type = nullable_htmlentities($row['contact_note_type']);

View File

@@ -288,7 +288,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
<form id="bulkActions" action="post.php" method="post" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token'] ?>">
<div class="table-responsive-sm">
<div class="table-responsive">
<table class="table border">
<thead class="thead-light <?php if (!$num_rows[0]) { echo "d-none"; } ?>">
<tr>

View File

@@ -276,7 +276,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
<form id="bulkActions" action="post.php" method="post">
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token'] ?>">
<div class="table-responsive-sm">
<div class="table-responsive">
<table class="table table-striped table-borderless table-hover">
<thead class="text-dark <?php if ($num_rows[0] == 0) { echo "d-none"; } ?> text-nowrap">
<tr>

View File

@@ -172,7 +172,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
</div>
</form>
<hr>
<div class="table-responsive-sm">
<div class="table-responsive">
<form id="bulkActions" action="post.php" method="post">
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token'] ?>">

View File

@@ -202,7 +202,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
</form>
<hr>
<div class="table-responsive-sm">
<div class="table-responsive">
<table class="table table-striped table-borderless table-hover">
<thead class="text-dark <?php if ($num_rows[0] == 0) { echo "d-none"; } ?>">
<tr>

View File

@@ -257,7 +257,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
<hr>
<form id="bulkActions" action="post.php" method="post">
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token'] ?>">
<div class="table-responsive-sm">
<div class="table-responsive">
<table class="table table-striped table-borderless table-hover">
<thead class="text-dark <?php if ($num_rows[0] == 0) { echo "d-none"; } ?> text-nowrap">
<tr>

View File

@@ -216,7 +216,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
<form id="bulkActions" action="post.php" method="post" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token'] ?>">
<div class="table-responsive-sm">
<div class="table-responsive">
<table class="table table-striped table-borderless table-hover">
<thead class="<?php if ($num_rows[0] == 0) { echo "d-none"; } ?>">
<tr>

View File

@@ -258,8 +258,17 @@ ob_start();
</div>
<select class="form-control select2" name="status">
<option value="">- Select Status -</option>
<?php foreach($asset_status_array as $asset_status) { ?>
<option><?php echo $asset_status; ?></option>
<?php
$sql_interface_types_select = mysqli_query($mysqli, "
SELECT category_name FROM categories
WHERE category_type = 'asset_status'
AND category_archived_at IS NULL
ORDER BY category_order ASC, category_name ASC
");
while ($row = mysqli_fetch_assoc($sql_interface_types_select)) {
$asset_status_select = nullable_htmlentities($row['category_name']);
?>
<option><?= $asset_status_select ?></option>
<?php } ?>
</select>
</div>

View File

@@ -26,12 +26,21 @@ ob_start();
<label>Status</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-info"></i></span>
<span class="input-group-text"><i class="fa fa-fw fa-circle"></i></span>
</div>
<select class="form-control select2" name="bulk_status">
<option value="">- Status -</option>
<?php foreach($asset_status_array as $asset_status) { ?>
<option><?php echo $asset_status; ?></option>
<option value="">- Select Status -</option>
<?php
$sql_interface_types_select = mysqli_query($mysqli, "
SELECT category_name FROM categories
WHERE category_type = 'asset_status'
AND category_archived_at IS NULL
ORDER BY category_order ASC, category_name ASC
");
while ($row = mysqli_fetch_assoc($sql_interface_types_select)) {
$asset_status_select = nullable_htmlentities($row['category_name']);
?>
<option><?= $asset_status_select ?></option>
<?php } ?>
</select>
</div>

View File

@@ -228,8 +228,20 @@ ob_start();
<span class="input-group-text"><i class="fa fa-fw fa-circle"></i></span>
</div>
<select class="form-control select2" name="status">
<?php foreach($asset_status_array as $asset_status_select) { ?>
<option <?php if ($asset_status_select == $asset_status) { echo "selected"; } ?>><?php echo $asset_status_select; ?></option>
<option value="">- Select Status -</option>
<?php
$sql_interface_types_select = mysqli_query($mysqli, "
SELECT category_name FROM categories
WHERE category_type = 'asset_status'
AND category_archived_at IS NULL
ORDER BY category_order ASC, category_name ASC
");
while ($row = mysqli_fetch_assoc($sql_interface_types_select)) {
$asset_status_select = nullable_htmlentities($row['category_name']);
?>
<option <?php if ($asset_status_select == $asset_status) { echo "selected"; } ?>>
<?= $asset_status_select ?>
</option>
<?php } ?>
</select>
</div>

View File

@@ -495,7 +495,7 @@ ob_start();
</div>
<?php } ?>
<?php if ($credential_count) { ?>
<?php if (lookupUserPermission('module_credential') && ($credential_count)) { ?>
<div class="tab-pane fade" id="pills-asset-credentials">
<div class="table-responsive-sm-sm">
<table class="table table-sm table-striped table-borderless table-hover">

View File

@@ -270,8 +270,20 @@ ob_start();
<span class="input-group-text"><i class="fa fa-fw fa-circle"></i></span>
</div>
<select class="form-control select2" name="status">
<?php foreach($asset_status_array as $asset_status_select) { ?>
<option <?php if ($asset_status_select == $asset_status) { echo "selected"; } ?>><?= $asset_status_select ?></option>
<option value="">- Select Status -</option>
<?php
$sql_interface_types_select = mysqli_query($mysqli, "
SELECT category_name FROM categories
WHERE category_type = 'asset_status'
AND category_archived_at IS NULL
ORDER BY category_order ASC, category_name ASC
");
while ($row = mysqli_fetch_assoc($sql_interface_types_select)) {
$asset_status_select = nullable_htmlentities($row['category_name']);
?>
<option <?php if ($asset_status_select == $asset_status) { echo "selected"; } ?>>
<?= $asset_status_select ?>
</option>
<?php } ?>
</select>
</div>

View File

@@ -57,15 +57,24 @@ ob_start();
<!-- Type -->
<div class="form-group">
<label for="network">Type</label>
<label for="network">Interface Type</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-plug"></i></span>
</div>
<select class="form-control select2" name="type">
<option value="">- Select Type -</option>
<?php foreach($interface_types_array as $interface_type) { ?>
<option><?php echo $interface_type; ?></option>
<?php
$sql_interface_types_select = mysqli_query($mysqli, "
SELECT category_name FROM categories
WHERE category_type = 'network_interface'
AND category_archived_at IS NULL
ORDER BY category_order ASC, category_name ASC
");
while ($row = mysqli_fetch_assoc($sql_interface_types_select)) {
$interface_type_select = nullable_htmlentities($row['category_name']);
?>
<option><?= $interface_type_select ?></option>
<?php } ?>
</select>
</div>

View File

@@ -24,16 +24,26 @@ ob_start();
<div class="modal-body">
<!-- Type -->
<div class="form-group">
<label>Interface Type</label>
<label for="network">Interface Type</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-ethernet"></i></span>
<span class="input-group-text"><i class="fa fa-fw fa-plug"></i></span>
</div>
<select class="form-control select2" name="bulk_type">
<option value="">- Select a Type -</option>
<?php foreach($interface_types_array as $interface_type_select) { ?>
<option><?php echo $interface_type_select; ?></option>
<option value="">- Select Type -</option>
<?php
$sql_interface_types_select = mysqli_query($mysqli, "
SELECT category_name FROM categories
WHERE category_type = 'network_interface'
AND category_archived_at IS NULL
ORDER BY category_order ASC, category_name ASC
");
while ($row = mysqli_fetch_assoc($sql_interface_types_select)) {
$interface_type_select = nullable_htmlentities($row['category_name']);
?>
<option><?= $interface_type_select ?></option>
<?php } ?>
</select>
</div>

View File

@@ -122,16 +122,25 @@ ob_start();
<!-- Type -->
<div class="form-group">
<label for="network">Type</label>
<label for="network">Interface Type</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-plug"></i></span>
</div>
<select class="form-control select2" name="type">
<option value="">- Select Type -</option>
<?php foreach($interface_types_array as $interface_type_select) { ?>
<?php
$sql_interface_types_select = mysqli_query($mysqli, "
SELECT category_name FROM categories
WHERE category_type = 'network_interface'
AND category_archived_at IS NULL
ORDER BY category_order ASC, category_name ASC
");
while ($row = mysqli_fetch_assoc($sql_interface_types_select)) {
$interface_type_select = nullable_htmlentities($row['category_name']);
?>
<option <?php if($interface_type == $interface_type_select) { echo "selected"; } ?>>
<?php echo $interface_type_select; ?>
<?= $interface_type_select ?>
</option>
<?php } ?>
</select>

View File

@@ -38,15 +38,24 @@
<!-- Type -->
<div class="form-group">
<label for="network">Type</label>
<label for="network">Interface Type</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-plug"></i></span>
</div>
<select class="form-control select2" name="type">
<option value="">- Select Type -</option>
<?php foreach($interface_types_array as $interface_type) { ?>
<option><?php echo $interface_type; ?></option>
<?php
$sql_interface_types_select = mysqli_query($mysqli, "
SELECT category_name FROM categories
WHERE category_type = 'network_interface'
AND category_archived_at IS NULL
ORDER BY category_order ASC, category_name ASC
");
while ($row = mysqli_fetch_assoc($sql_interface_types_select)) {
$interface_type_select = nullable_htmlentities($row['category_name']);
?>
<option><?= $interface_type_select ?></option>
<?php } ?>
</select>
</div>

View File

@@ -10,6 +10,17 @@ $referral_sql = mysqli_query($mysqli, "SELECT * FROM categories WHERE category_t
$sql_tags_select = mysqli_query($mysqli, "SELECT * FROM tags WHERE tag_type = 1 ORDER BY tag_name ASC");
$net_terms_array = array (
'0'=>'On Receipt',
'7'=>'7 Days',
'10'=>'10 Days',
'15'=>'15 Days',
'30'=>'30 Days',
'45'=>'45 Days',
'60'=>'60 Days',
'90'=>'90 Days'
);
ob_start();
?>

View File

@@ -6,6 +6,17 @@ $client_ids = array_map('intval', $_GET['client_ids'] ?? []);
$count = count($client_ids);
$net_terms_array = array (
'0'=>'On Receipt',
'7'=>'7 Days',
'10'=>'10 Days',
'15'=>'15 Days',
'30'=>'30 Days',
'45'=>'45 Days',
'60'=>'60 Days',
'90'=>'90 Days'
);
ob_start();
?>

View File

@@ -28,9 +28,21 @@ while ($row = mysqli_fetch_assoc($sql_client_tags)) {
$client_tag_id_array[] = $client_tag_id;
}
// Generate the HTML form content using output buffering.
$net_terms_array = array (
'0'=>'On Receipt',
'7'=>'7 Days',
'10'=>'10 Days',
'15'=>'15 Days',
'30'=>'30 Days',
'45'=>'45 Days',
'60'=>'60 Days',
'90'=>'90 Days'
);
ob_start();
?>
<div class="modal-header bg-dark">
<h5 class="modal-title"><i class='fa fa-fw fa-user-edit mr-2'></i>Editing Client: <strong><?php echo $client_name; ?></strong></h5>
<button type="button" class="close text-white" data-dismiss="modal">

View File

@@ -334,7 +334,8 @@ ob_start();
</a>
<?php } ?>
<?php if ($credential_count) { ?>
<?php
if (lookupUserPermission('module_credential') && ($credential_count)) { ?>
<a class="nav-link <?= ($first_tab === "credentials") ? "active" : "" ?>"
data-toggle="pill"
href="#pills-contact-credentials<?= $contact_id ?>"
@@ -519,7 +520,7 @@ ob_start();
</div>
<?php } ?>
<?php if ($credential_count) { ?>
<?php if (lookupUserPermission('module_credential') && ($credential_count)) { ?>
<div class="tab-pane fade <?= ($first_tab === "credentials") ? "show active" : "" ?>" id="pills-contact-credentials<?= $contact_id ?>">
<div class="table-responsive-sm">
<table class="table table-striped table-borderless table-hover table-sm dataTables" style="width:100%">

View File

@@ -32,8 +32,17 @@ ob_start();
<span class="input-group-text"><i class="fa fa-fw fa-comment"></i></span>
</div>
<select class="form-control select2" name="type">
<?php foreach ($note_types_array as $note_type => $note_type_icon) { ?>
<option><?php echo nullable_htmlentities($note_type); ?></option>
<?php
$sql_contact_note_types_select = mysqli_query($mysqli, "
SELECT category_name FROM categories
WHERE category_type = 'contact_note_type'
AND category_archived_at IS NULL
ORDER BY category_order ASC, category_name ASC
");
while ($row = mysqli_fetch_assoc($sql_contact_note_types_select)) {
$contact_note_type_select = nullable_htmlentities($row['category_name']);
?>
<option><?= $contact_note_type_select ?></option>
<?php } ?>
</select>
</div>

View File

@@ -2,6 +2,8 @@
require_once '../../../includes/modal_header.php';
enforceUserPermission('module_credential', 2);
$credential_id = intval($_GET['id']);
$sql = mysqli_query($mysqli, "SELECT * FROM credentials WHERE credential_id = $credential_id LIMIT 1");
@@ -32,6 +34,8 @@ while ($row = mysqli_fetch_assoc($sql_credential_tags)) {
$credential_tag_id_array[] = $credential_tag_id;
}
enforceClientAccess();
// Generate the HTML form content using output buffering.
ob_start();
?>

View File

@@ -2,11 +2,14 @@
require_once '../../../includes/modal_header.php';
enforceUserPermission('module_credential');
$credential_id = intval($_GET['id']);
$sql = mysqli_query($mysqli, "SELECT * FROM credentials WHERE credential_id = $credential_id LIMIT 1");
$row = mysqli_fetch_assoc($sql);
$client_id = intval($row['credential_client_id']);
$credential_name = nullable_htmlentities($row['credential_name']);
$credential_description = nullable_htmlentities($row['credential_description']);
$credential_uri = nullable_htmlentities($row['credential_uri']);
@@ -23,6 +26,8 @@ if (empty($credential_otp_secret)) {
$credential_note = nullable_htmlentities($row['credential_note']);
$credential_created_at = nullable_htmlentities($row['credential_created_at']);
enforceClientAccess();
// Generate the HTML form content using output buffering.
ob_start();
?>

View File

@@ -50,7 +50,7 @@ ob_start();
</div>
<div class="form-group">
<input type="file" class="form-control-file" name="file[]" multiple id="fileInput" accept=".jpg, .jpeg, .gif, .png, .webp, .pdf, .txt, .md, .doc, .docx, .odt, .csv, .xls, .xlsx, .ods, .pptx, .odp, .zip, .tar, .gz, .msg, .json, .wav, .mp3, .ogg, .mov, .mp4, .av1, .ovpn, .cfg, .ps1, .vsdx, .drawio, .pfx, .unf, .key, .stk, .bat">
<input type="file" class="form-control-file" name="file[]" multiple id="fileInput" accept=".jpg, .jpeg, .gif, .png, .webp, .pdf, .txt, .md, .doc, .docx, .odt, .csv, .xls, .xlsx, .ods, .pptx, .odp, .zip, .tar, .gz, .msg, .json, .wav, .mp3, .ogg, .mov, .mp4, .av1, .ovpn, .cfg, .ps1, .vsdx, .drawio, .pfx, .unf, .key, .stk, .bat, .swb">
</div>
<small class="text-secondary">Up to 20 files can be uploaded at once by holding down CTRL and selecting files</small>

View File

@@ -0,0 +1,37 @@
<?php
require_once '../../../includes/modal_header.php';
$client_id = intval($_GET['client_id'] ?? 0);
ob_start();
?>
<div class="modal-header bg-dark">
<h5 class="modal-title"><i class="fas fa-fw fa-network-wired mr-2"></i>Import Networks</h5>
<button type="button" class="close text-white" data-dismiss="modal">
<span>&times;</span>
</button>
</div>
<form action="post.php" method="post" enctype="multipart/form-data" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<input type="hidden" name="client_id" value="<?= $client_id ?>">
<div class="modal-body">
<p><strong>Format csv file with headings &amp; data:</strong><br>Name, Description, VLAN, Network (CIDR), Gateway, IP Range, Primary DNS, Secondary DNS</p>
<hr>
<div class="form-group my-4">
<input type="file" class="form-control-file" name="file" accept=".csv" required>
</div>
<hr>
<div>Download: <a class="text-bold" href="post.php?download_networks_csv_template=<?= $client_id ?>">sample csv template</a></div>
</div>
<div class="modal-footer">
<button type="submit" name="import_networks_csv" class="btn btn-primary text-bold"><i class="fa fa-upload mr-2"></i>Import</button>
<button type="button" class="btn btn-light" data-dismiss="modal"><i class="fa fa-times mr-2"></i>Cancel</button>
</div>
</form>
<?php
require_once '../../../includes/modal_footer.php';

View File

@@ -95,6 +95,7 @@ ob_start();
$account_id = intval($row['account_id']);
$account_name = nullable_htmlentities($row['account_name']);
$opening_balance = floatval($row['opening_balance']);
$account_currency = nullable_htmlentities($row['account_currency_code']);
$sql_payments = mysqli_query($mysqli, "SELECT SUM(payment_amount) AS total_payments FROM payments WHERE payment_account_id = $account_id");
$row = mysqli_fetch_assoc($sql_payments);
@@ -113,7 +114,7 @@ ob_start();
?>
<option <?php if ($config_default_payment_account == $account_id) { echo "selected"; } ?>
value="<?php echo $account_id; ?>">
<?php echo $account_name; ?> [$<?php echo number_format($account_balance, 2); ?>]
<?php echo $account_name; ?> [<?php echo numfmt_format_currency($currency_format, $account_balance, $account_currency); ?>]
</option>
<?php

View File

@@ -29,7 +29,7 @@ ob_start();
</div>
<?php if ($client_id) { ?>
<input type="hidden" name="client" value="<?php echo $client_id; ?>">
<input type="hidden" name="client_id" value="<?php echo $client_id; ?>">
<?php } else { ?>
<div class="form-group">
@@ -38,7 +38,7 @@ ob_start();
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-user"></i></span>
</div>
<select class="form-control select2" name="client" required>
<select class="form-control select2" name="client_id" required>
<option value="">- Client -</option>
<?php

View File

@@ -43,8 +43,17 @@ ob_start();
</div>
<select class="form-control select2" name="type" required>
<option value="">- Type -</option>
<?php foreach($rack_type_select_array as $rack_type) { ?>
<option><?php echo $rack_type; ?></option>
<?php
$sql_rack_types_select = mysqli_query($mysqli, "
SELECT category_name FROM categories
WHERE category_type = 'rack_type'
AND category_archived_at IS NULL
ORDER BY category_order ASC, category_name ASC
");
while ($row = mysqli_fetch_assoc($sql_rack_types_select)) {
$rack_type_select = nullable_htmlentities($row['category_name']);
?>
<option><?= $rack_type_select ?></option>
<?php } ?>
</select>
</div>

View File

@@ -59,8 +59,19 @@ ob_start();
</div>
<select class="form-control select2" name="type" required>
<option value="">- Type -</option>
<?php foreach($rack_type_select_array as $rack_type_select) { ?>
<option <?php if ($rack_type == $rack_type_select) { echo "selected"; } ?>><?php echo $rack_type_select; ?></option>
<?php
$sql_rack_types_select = mysqli_query($mysqli, "
SELECT category_name FROM categories
WHERE category_type = 'rack_type'
AND category_archived_at IS NULL
ORDER BY category_order ASC, category_name ASC
");
while ($row = mysqli_fetch_assoc($sql_rack_types_select)) {
$rack_type_select = nullable_htmlentities($row['category_name']);
?>
<option <?php if ($rack_type == $rack_type_select) { echo "selected"; } ?>>
<?= $rack_type_select ?>
</option>
<?php } ?>
</select>
</div>

View File

@@ -4,6 +4,16 @@ require_once '../../../includes/modal_header.php';
$client_id = intval($_GET['client_id'] ?? 0);
$license_types_array = array (
'Device',
'User',
'Site',
'Concurrent',
'Trial',
'Perpetual',
'Usage-based'
);
ob_start();
?>
@@ -79,8 +89,17 @@ ob_start();
</div>
<select class="form-control select2" name="type" required>
<option value="">- Select Type -</option>
<?php foreach ($software_types_array as $software_type) { ?>
<option><?php echo $software_type; ?></option>
<<?php
$sql_software_types_select = mysqli_query($mysqli, "
SELECT category_name FROM categories
WHERE category_type = 'software_type'
AND category_archived_at IS NULL
ORDER BY category_order ASC, category_name ASC
");
while ($row = mysqli_fetch_assoc($sql_software_types_select)) {
$software_type_select = nullable_htmlentities($row['category_name']);
?>
<option><?= $software_type_select ?></option>
<?php } ?>
</select>
</div>

View File

@@ -41,9 +41,20 @@ while ($row = mysqli_fetch_assoc($contact_licenses_sql)) {
}
$contact_licenses = implode(',', $contact_licenses_array);
// Generate the HTML form content using output buffering.
$license_types_array = array (
'Device',
'User',
'Site',
'Concurrent',
'Trial',
'Perpetual',
'Usage-based'
);
ob_start();
?>
<div class="modal-header bg-dark">
<h5 class="modal-title"><i class="fa fa-fw fa-cube mr-2"></i>Editing license: <strong><?php echo $software_name; ?></strong></h5>
<button type="button" class="close text-white" data-dismiss="modal">
@@ -86,8 +97,20 @@ ob_start();
<span class="input-group-text"><i class="fa fa-fw fa-tag"></i></span>
</div>
<select class="form-control select2" name="type" required>
<?php foreach($software_types_array as $software_type_select) { ?>
<option <?php if ($software_type == $software_type_select) { echo "selected"; } ?>><?php echo $software_type_select; ?></option>
<option value="">- Select Type -</option>
<<?php
$sql_software_types_select = mysqli_query($mysqli, "
SELECT category_name FROM categories
WHERE category_type = 'software_type'
AND category_archived_at IS NULL
ORDER BY category_order ASC, category_name ASC
");
while ($row = mysqli_fetch_assoc($sql_software_types_select)) {
$software_type_select = nullable_htmlentities($row['category_name']);
?>
<option <?php if ($software_type == $software_type_select) { echo "selected"; } ?>>
<?= $software_type_select ?>
</option>
<?php } ?>
</select>
</div>

View File

@@ -247,7 +247,7 @@ ob_start();
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-user"></i></span>
</div>
<select class="form-control select2" name="contact">
<select class="form-control select2" name="contact_id">
<option value="0">- No One -</option>
<?php
$sql = mysqli_query($mysqli, "SELECT contact_id, contact_name, contact_title, contact_primary, contact_technical FROM contacts WHERE contact_client_id = $client_id AND contact_archived_at IS NULL ORDER BY contact_primary DESC, contact_technical DESC, contact_name ASC");

View File

@@ -79,15 +79,21 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
<div class="card-tools">
<div class="btn-group">
<button type="button" class="btn btn-primary ajax-modal" data-modal-url="modals/network/network_add.php?<?= $client_url ?>&location_id=<?= $location_filter ?>"><i class="fas fa-plus mr-2"></i>New Network</button>
<?php if ($num_rows[0] > 0) { ?>
<button type="button" class="btn btn-primary dropdown-toggle dropdown-toggle-split" data-toggle="dropdown"></button>
<div class="dropdown-menu">
<?php if ($num_rows[0] > 0) { ?>
<a class="dropdown-item text-dark ajax-modal" href="#"
data-modal-url="modals/network/network_export.php?<?= $client_url ?>">
<i class="fa fa-fw fa-download mr-2"></i>Export
</a>
</div>
<?php } ?>
<div class="dropdown-divider"></div>
<a class="dropdown-item text-dark ajax-modal" href="#"
data-modal-url="modals/network/network_import.php?<?= $client_url ?>">
<i class="fa fa-fw fa-upload mr-2"></i>Import
</a>
</div>
</div>
</div>
@@ -187,7 +193,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
</div>
</form>
<hr>
<div class="table-responsive-sm">
<div class="table-responsive">
<form id="bulkActions" action="post.php" method="post">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">

View File

@@ -138,7 +138,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
</div>
</form>
<hr>
<div class="table-responsive-sm">
<div class="table-responsive">
<table class="table table-striped table-borderless table-hover">
<thead class="text-dark <?php if ($num_rows[0] == 0) { echo "d-none"; } ?> text-nowrap">
<tr>

View File

@@ -12,7 +12,6 @@ if (isset($_POST['add_account'])) {
enforceUserPermission('module_financial', 2);
$name = sanitizeInput($_POST['name']);
$opening_balance = floatval($_POST['opening_balance']);
$currency_code = sanitizeInput($_POST['currency_code']);

View File

@@ -8,6 +8,8 @@ defined('FROM_POST_HANDLER') || die("Direct file access is not allowed");
if (isset($_POST['add_client'])) {
// JQ - Using Prepared MySQLi Statements here for show this is not our standard and is only used in the client add/edit POST.
validateCSRFToken($_POST['csrf_token']);
enforceUserPermission('module_client', 2);
@@ -260,25 +262,63 @@ if (isset($_POST['edit_client'])) {
$client_id = intval($_POST['client_id']);
mysqli_query($mysqli, "UPDATE clients SET client_name = '$name', client_type = '$type', client_website = '$website', client_referral = '$referral', client_rate = $rate, client_net_terms = $net_terms, client_tax_id_number = '$tax_id_number', client_lead = $lead, client_abbreviation = '$abbreviation', client_notes = '$notes' WHERE client_id = $client_id");
// Update client using prepared statement
$query = mysqli_prepare(
$mysqli,
"UPDATE clients SET
client_name = ?,
client_type = ?,
client_website = ?,
client_referral = ?,
client_rate = ?,
client_net_terms = ?,
client_tax_id_number = ?,
client_lead = ?,
client_abbreviation = ?,
client_notes = ?
WHERE client_id = ?"
);
mysqli_stmt_bind_param(
$query,
"ssssdisissi",
$name,
$type,
$website,
$referral,
$rate,
$net_terms,
$tax_id_number,
$lead,
$abbreviation,
$notes,
$client_id
);
mysqli_stmt_execute($query);
// Create Referral if it doesn't exist
$sql = mysqli_query($mysqli, "SELECT category_name FROM categories WHERE category_type = 'Referral' AND category_archived_at IS NULL AND category_name = '$referral'");
if(mysqli_num_rows($sql) == 0) {
mysqli_query($mysqli, "INSERT INTO categories SET category_name = '$referral', category_type = 'Referral'");
// Create referral category if it doesn't exist
$query = mysqli_prepare($mysqli, "SELECT category_name FROM categories WHERE category_type = 'Referral' AND category_archived_at IS NULL AND category_name = ?");
mysqli_stmt_bind_param($query, "s", $referral);
mysqli_stmt_execute($query);
mysqli_stmt_store_result($query);
if (mysqli_stmt_num_rows($query) == 0) {
$query = mysqli_prepare($mysqli, "INSERT INTO categories SET category_name = ?, category_type = 'Referral'");
mysqli_stmt_bind_param($query, "s", $referral);
mysqli_stmt_execute($query);
logAction("Category", "Create", "$session_name created referral category $referral");
}
// Tags
// Delete existing tags
mysqli_query($mysqli, "DELETE FROM client_tags WHERE client_id = $client_id");
// Tags - delete existing and re-insert
$query = mysqli_prepare($mysqli, "DELETE FROM client_tags WHERE client_id = ?");
mysqli_stmt_bind_param($query, "i", $client_id);
mysqli_stmt_execute($query);
// Add new tags
if (isset($_POST['tags'])) {
$query = mysqli_prepare($mysqli, "INSERT INTO client_tags SET client_id = ?, tag_id = ?");
foreach ($_POST['tags'] as $tag) {
$tag = intval($tag);
mysqli_query($mysqli, "INSERT INTO client_tags SET client_id = $client_id, tag_id = $tag");
mysqli_stmt_bind_param($query, "ii", $client_id, $tag);
mysqli_stmt_execute($query);
}
}

View File

@@ -33,7 +33,7 @@ if (isset($_POST['upload_files'])) {
'odt', 'csv', 'xls', 'xlsx', 'ods', 'pptx', 'odp', 'zip', 'tar', 'gz',
'msg', 'json', 'wav', 'mp3', 'ogg', 'mov', 'mp4', 'av1', 'ovpn',
'cfg', 'ps1', 'vsdx', 'drawio', 'pfx', 'pages', 'numbers', 'unf', 'unifi',
'key', 'bat', 'stk'
'key', 'bat', 'stk', 'swb'
];
// Loop through each uploaded file

View File

@@ -542,8 +542,8 @@ if (isset($_GET['email_invoice'])) {
$invoice_number = intval($row['invoice_number']);
$invoice_scope = sanitizeInput($row['invoice_scope']);
$invoice_status = sanitizeInput($row['invoice_status']);
$invoice_date = sanitizeInput($row['invoice_date']);
$invoice_due = sanitizeInput($row['invoice_due']);
$invoice_date = sanitizeInput(validateDate($row['invoice_date']));
$invoice_due = sanitizeInput(validateDate($row['invoice_due']));
$invoice_amount = floatval($row['invoice_amount']);
$invoice_url_key = sanitizeInput($row['invoice_url_key']);
$invoice_currency_code = sanitizeInput($row['invoice_currency_code']);

View File

@@ -188,7 +188,7 @@ if (isset($_POST['export_networks_csv'])) {
$file_name_prepend = "$session_company_name-";
}
$sql = mysqli_query($mysqli,"SELECT * FROM networks LEFT JOIN client ON client_id = network_client_id WHERE network_archived_at IS NULL $client_query $access_permission_query ORDER BY network_name ASC");
$sql = mysqli_query($mysqli,"SELECT * FROM networks LEFT JOIN clients ON client_id = network_client_id WHERE network_archived_at IS NULL $client_query $access_permission_query ORDER BY network_name ASC");
$num_rows = mysqli_num_rows($sql);
@@ -227,3 +227,161 @@ if (isset($_POST['export_networks_csv'])) {
exit;
}
// ============================================================
// Add these two blocks to agent/post/network.php
// Place them alongside the existing export_networks_csv block.
// ============================================================
// ----------------------------------------------------------
// CSV Template Download
// GET: post.php?download_networks_csv_template=<client_id>
// ----------------------------------------------------------
if (isset($_GET['download_networks_csv_template'])) {
$delimiter = ",";
$enclosure = '"';
$escape = '\\';
$filename = "Networks-Template.csv";
$f = fopen('php://memory', 'w');
$fields = array('Name', 'Description', 'VLAN', 'Network (CIDR)', 'Gateway', 'IP Range', 'Primary DNS', 'Secondary DNS');
fputcsv($f, $fields, $delimiter, $enclosure, $escape);
// One example row so the user can see expected formatting
$example = array('Office LAN', 'Main office network', '10', '192.168.1.0/24', '192.168.1.1', '192.168.1.100-192.168.1.200', '8.8.8.8', '8.8.4.4');
fputcsv($f, $example, $delimiter, $enclosure, $escape);
fseek($f, 0);
header('Content-Type: text/csv');
header('Content-Disposition: attachment; filename="' . $filename . '";');
fpassthru($f);
exit;
}
// ----------------------------------------------------------
// CSV Import
// POST: post.php (name="import_networks_csv")
// ----------------------------------------------------------
if (isset($_POST['import_networks_csv'])) {
validateCSRFToken($_POST['csrf_token']);
enforceUserPermission('module_support', 2);
$client_id = intval($_POST['client_id']);
enforceClientAccess();
$error = false;
// File provided?
if (!empty($_FILES['file']['tmp_name'])) {
$file_name = $_FILES['file']['tmp_name'];
} else {
flash_alert("Please select a file to upload.", 'error');
redirect();
}
// Check extension
$file_extension = strtolower(end(explode('.', $_FILES['file']['name'])));
if ($file_extension !== 'csv') {
$error = true;
flash_alert("Bad file extension — only .csv files are accepted.", 'error');
}
// Check not empty
elseif ($_FILES['file']['size'] < 1) {
$error = true;
flash_alert("Bad file size (empty file?).", 'error');
}
// Check column count matches the 8-column export/template format
else {
$f = fopen($file_name, 'r');
$f_columns = fgetcsv($f, 1000, ',');
fclose($f);
if (count($f_columns) !== 8) {
$error = true;
flash_alert("Bad column count — expected 8 columns: Name, Description, VLAN, Network (CIDR), Gateway, IP Range, Primary DNS, Secondary DNS.", 'error');
}
}
// Parse and insert
if (!$error) {
$file = fopen($file_name, 'r');
fgetcsv($file, 1000, ','); // Skip header row
$row_count = 0;
$duplicate_count = 0;
while (($column = fgetcsv($file, 1000, ',')) !== false) {
$duplicate_detect = 0;
$name = isset($column[0]) ? sanitizeInput($column[0]) : '';
$description = isset($column[1]) ? sanitizeInput($column[1]) : '';
$vlan = isset($column[2]) ? intval($column[2]) : 0;
$network = isset($column[3]) ? sanitizeInput($column[3]) : '';
$gateway = isset($column[4]) ? sanitizeInput($column[4]) : '';
$dhcp_range = isset($column[5]) ? sanitizeInput($column[5]) : '';
$primary_dns = isset($column[6]) ? sanitizeInput($column[6]) : '';
$secondary_dns = isset($column[7]) ? sanitizeInput($column[7]) : '';
// Skip rows with no name
if ($name === '') {
continue;
}
// Duplicate check — same name + network address for this client
$dup_check = mysqli_query($mysqli,
"SELECT network_id FROM networks
WHERE network_name = '$name'
AND network = '$network'
AND network_client_id = $client_id
AND network_archived_at IS NULL
LIMIT 1"
);
if (mysqli_num_rows($dup_check) > 0) {
$duplicate_detect = 1;
}
if ($duplicate_detect === 0) {
mysqli_query($mysqli,
"INSERT INTO networks SET
network_name = '$name',
network_description = '$description',
network_vlan = $vlan,
network = '$network',
network_gateway = '$gateway',
network_dhcp_range = '$dhcp_range',
network_primary_dns = '$primary_dns',
network_secondary_dns = '$secondary_dns',
network_client_id = $client_id"
);
$row_count++;
} else {
$duplicate_count++;
}
}
fclose($file);
logAction("Network", "Import", "$session_name imported $row_count network(s). $duplicate_count duplicate(s) found and not imported", $client_id);
flash_alert("$row_count Network(s) imported, $duplicate_count duplicate(s) detected and not imported");
redirect();
}
if ($error) {
redirect();
}
}

View File

@@ -14,7 +14,7 @@ if (isset($_POST['add_quote'])) {
require_once 'quote_model.php';
$client_id = intval($_POST['client']);
$client_id = intval($_POST['client_id']);
enforceClientAccess();
@@ -55,7 +55,7 @@ if (isset($_POST['add_quote_copy'])) {
enforceUserPermission('module_sales', 2);
$quote_id = intval($_POST['quote_id']);
$client_id = intval($_POST['client']);
$client_id = intval($_POST['client_id']);
$date = sanitizeInput($_POST['date']);
$expire = sanitizeInput($_POST['expire']);

View File

@@ -265,7 +265,7 @@ if (isset($_POST['edit_rack_unit'])) {
if (isset($_GET['remove_rack_unit'])) {
validateCSRFToken($_POST['csrf_token']);
validateCSRFToken($_GET['csrf_token']);
enforceUserPermission('module_support', 2);

View File

@@ -13,13 +13,13 @@ if (isset($_POST['add_invoice_recurring'])) {
enforceUserPermission('module_sales', 2);
$invoice_id = intval($_POST['invoice_id']);
$recurring_invoice_frequency = sanitizeInput($_POST['frequency']);
$recurring_invoice_frequency = ($_POST['frequency'] === 'year') ? 'year' : 'month';
$sql = mysqli_query($mysqli,"SELECT * FROM invoices WHERE invoice_id = $invoice_id");
$row = mysqli_fetch_assoc($sql);
$invoice_prefix = sanitizeInput($row['invoice_prefix']);
$invoice_number = intval($row['invoice_number']);
$invoice_date = sanitizeInput($row['invoice_date']);
$invoice_date = sanitizeInput(validateDate($row['invoice_date']));
$invoice_amount = floatval($row['invoice_amount']);
$invoice_currency_code = sanitizeInput($row['invoice_currency_code']);
$invoice_scope = sanitizeInput($row['invoice_scope']);
@@ -394,7 +394,7 @@ if (isset($_GET['force_recurring'])) {
$row = mysqli_fetch_assoc($sql_recurring_invoices);
$recurring_invoice_id = intval($row['recurring_invoice_id']);
$recurring_invoice_scope = sanitizeInput($row['recurring_invoice_scope']);
$recurring_invoice_frequency = sanitizeInput($row['recurring_invoice_frequency']);
$recurring_invoice_frequency = ($_POST['frequency'] === 'year') ? 'year' : 'month';
$recurring_invoice_status = sanitizeInput($row['recurring_invoice_status']);
$recurring_invoice_last_sent = sanitizeInput($row['recurring_invoice_last_sent']);
$recurring_invoice_next_date = sanitizeInput($row['recurring_invoice_next_date']);
@@ -480,7 +480,7 @@ if (isset($_GET['force_recurring'])) {
$invoice_prefix = sanitizeInput($row['invoice_prefix']);
$invoice_number = intval($row['invoice_number']);
$invoice_scope = sanitizeInput($row['invoice_scope']);
$invoice_date = sanitizeInput($row['invoice_date']);
$invoice_date = sanitizeInput(validateDate($row['invoice_date']));
$invoice_due = sanitizeInput($row['invoice_due']);
$invoice_amount = floatval($row['invoice_amount']);
$invoice_url_key = sanitizeInput($row['invoice_url_key']);

View File

@@ -27,7 +27,7 @@ if (isset($_POST['add_software_from_template'])) {
$type = sanitizeInput($row['software_template_type']);
$license_type = sanitizeInput($row['software_template_license_type']);
$notes = sanitizeInput($row['software_template_notes']);
$vendor = sanitizeInput($_POST['vendor'] ?? 0);
$vendor = intval($_POST['vendor'] ?? 0);
// Software add query
mysqli_query($mysqli,"INSERT INTO software SET software_name = '$name', software_version = '$version', software_description = '$description', software_type = '$type', software_license_type = '$license_type', software_notes = '$notes', software_vendor_id = $vendor, software_client_id = $client_id");

View File

@@ -169,7 +169,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
<form id="bulkActions" action="post.php" method="post">
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token'] ?>">
<div class="table-responsive-sm">
<div class="table-responsive">
<table class="table table-striped table-borderless table-hover">
<thead class="text-dark <?php if ($num_rows[0] == 0) { echo "d-none"; } ?> text-nowrap">
<tr>

View File

@@ -341,13 +341,13 @@ if (isset($_GET['project_id'])) {
<div class="card-body p-0">
<form id="bulkActions" action="post.php" method="post">
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token'] ?>">
<div class="table-responsive-sm">
<div class="table-responsive">
<table class="table table-border table-hover">
<thead class="thead-light">
<tr>
<td class="bg-light checkbox-column">
<div class="form-check">
<input class="form-check-input" id="selectAllCheckbox" type="checkbox" onclick="checkAll(this)" onkeydown="checkAll(this)">
<input class="form-check-input" id="selectAllCheckbox" type="checkbox" onclick="checkAll(this)">
</div>
</td>
<th>
@@ -494,7 +494,6 @@ if (isset($_GET['project_id'])) {
<td><?php echo $client_name; ?></td>
</tr>
<?php } ?>
</tbody>

View File

@@ -108,7 +108,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
</div>
</form>
<hr>
<div class="table-responsive-sm">
<div class="table-responsive">
<table class="table table-striped table-hover table-borderless">
<thead class="<?php if ($num_rows[0] == 0) { echo "d-none"; } ?> text-nowrap">
<tr>
@@ -190,34 +190,24 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
// Get Tasks and Tickets Stats
// Get Tickets
$sql_tickets = mysqli_query($mysqli, "SELECT * FROM tickets WHERE ticket_project_id = $project_id");
$ticket_count = mysqli_num_rows($sql_tickets);
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT('ticket_id') AS count FROM tickets WHERE ticket_project_id = $project_id"));
$ticket_count = $row['count'];
// Get Closed Ticket Count
$sql_closed_tickets = mysqli_query($mysqli, "SELECT * FROM tickets WHERE ticket_project_id = $project_id AND ticket_closed_at IS NOT NULL");
$closed_ticket_count = mysqli_num_rows($sql_closed_tickets);
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT('ticket_id') AS count FROM tickets WHERE ticket_project_id = $project_id AND ticket_closed_at IS NOT NULL"));
$closed_ticket_count = $row['count'];
// Ticket Closed Percent
if($ticket_count) {
$tickets_closed_percent = round(($closed_ticket_count / $ticket_count) * 100);
}
// Get All Tasks
$sql_tasks = mysqli_query($mysqli,
"SELECT * FROM tickets, tasks
WHERE ticket_id = task_ticket_id
AND ticket_project_id = $project_id"
);
$task_count = mysqli_num_rows($sql_tasks);
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT('task_id') AS count FROM tickets, tasks WHERE ticket_id = task_ticket_id AND ticket_project_id = $project_id"));
$task_count = $row['count'];
// Get Completed Task Count
$sql_tasks_completed = mysqli_query($mysqli,
"SELECT * FROM tickets, tasks
WHERE ticket_id = task_ticket_id
AND ticket_project_id = $project_id
AND task_completed_at IS NOT NULL"
);
$completed_task_count = mysqli_num_rows($sql_tasks_completed);
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT('task_id') AS count FROM tickets, tasks WHERE ticket_id = task_ticket_id AND ticket_project_id = $project_id AND task_completed_at IS NOT NULL"));
$completed_task_count = $row['count'];
// Tasks Completed Percent
if($task_count) {

View File

@@ -91,7 +91,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
</div>
</form>
<hr>
<div class="table-responsive-sm">
<div class="table-responsive">
<table class="table table-striped table-borderless table-hover">
<thead class="text-dark <?php if ($num_rows[0] == 0) { echo "d-none"; } ?> text-nowrap">
<tr>

View File

@@ -63,7 +63,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
</div>
</form>
<hr>
<div class="table-responsive-sm">
<div class="table-responsive">
<table class="table table-striped table-borderless table-hover">
<thead class="text-dark <?php if ($num_rows[0] == 0) { echo "d-none"; } ?>">
<tr>

View File

@@ -93,7 +93,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
</div>
</form>
<hr>
<div class="table-responsive-sm">
<div class="table-responsive">
<table class="table table-striped table-borderless table-hover">
<thead class="text-dark <?php if ($num_rows[0] == 0) { echo "d-none"; } ?> text-nowrap">
<tr>

View File

@@ -212,7 +212,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
</form>
<hr>
<div class="table-responsive-sm">
<div class="table-responsive">
<form id="bulkActions" action="post.php" method="post">
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token'] ?>">

View File

@@ -60,7 +60,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
</div>
</form>
<hr>
<div class="table-responsive-sm">
<div class="table-responsive">
<table class="table table-striped table-borderless table-hover">
<thead class="<?php if ($num_rows[0] == 0) { echo "d-none"; } ?> text-nowrap">
<tr>

View File

@@ -110,7 +110,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
</form>
<hr>
<div class="table-responsive-sm">
<div class="table-responsive">
<table class="table table-striped table-borderless table-hover">
<thead class="<?php if ($num_rows[0] == 0) { echo "d-none"; } ?>">
<tr>

View File

@@ -146,7 +146,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
</div>
</form>
<hr>
<div class="table-responsive-sm">
<div class="table-responsive">
<table class="table table-borderless table-hover">
<thead class="text-dark <?php if ($num_rows[0] == 0) { echo "d-none"; } ?> text-nowrap">
<tr>

View File

@@ -3,7 +3,7 @@
<form id="bulkActions" action="post.php" method="post">
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token'] ?>">
<div class="table-responsive-sm">
<div class="table-responsive">
<table class="table table-striped table-borderless table-hover">
<thead class="text-dark <?php if (!$num_rows[0]) { echo "d-none"; } ?> text-nowrap">
<tr>
@@ -172,6 +172,9 @@
// Get Tasks
// Get Tasks
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT('task_id') AS count FROM tickets, tasks WHERE ticket_id = task_ticket_id AND ticket_project_id = $project_id"));
$task_count = $row['count'];
$sql_tasks = mysqli_query( $mysqli, "SELECT * FROM tasks WHERE task_ticket_id = $ticket_id ORDER BY task_created_at ASC");
$task_count = mysqli_num_rows($sql_tasks);
// Get Completed Task Count

View File

@@ -120,7 +120,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
</div>
</form>
<hr>
<div class="table-responsive-sm">
<div class="table-responsive">
<table class="table table-striped table-borderless table-hover">
<thead class="text-dark <?php if ($num_rows[0] == 0) { echo "d-none"; } ?> text-nowrap">
<tr>

View File

@@ -85,7 +85,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
</div>
</form>
<hr>
<div class="table-responsive-sm">
<div class="table-responsive">
<table class="table table-striped table-borderless table-hover">
<thead class="text-dark <?php if ($num_rows[0] == 0) { echo "d-none"; } ?>">
<tr>

View File

@@ -113,7 +113,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
<form id="bulkActions" action="post.php" method="post">
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token'] ?>">
<div class="table-responsive-sm">
<div class="table-responsive">
<table class="table table-striped table-borderless table-hover">
<thead class="text-dark <?php if ($num_rows[0] == 0) { echo "d-none"; } ?> text-nowrap">
<tr>

View File

@@ -4,7 +4,7 @@
if (isset($_POST['asset_name'])) {
$name = sanitizeInput($_POST['asset_name']);
} elseif (isset($asset_row) && isset($asset_row['asset_name'])) {
$name = $asset_row['asset_name'];
$name = mysqli_real_escape_string($mysqli, $asset_row['asset_name']);
} else {
$name = '';
}
@@ -12,7 +12,7 @@ if (isset($_POST['asset_name'])) {
if (isset($_POST['asset_description'])) {
$description = sanitizeInput($_POST['asset_description']);
} elseif (isset($asset_row) && isset($asset_row['asset_description'])) {
$description = $asset_row['asset_description'];
$description = mysqli_real_escape_string($mysqli, $asset_row['asset_description']);
} else {
$description = '';
}
@@ -20,7 +20,7 @@ if (isset($_POST['asset_description'])) {
if (isset($_POST['asset_type'])) {
$type = sanitizeInput($_POST['asset_type']);
} elseif (isset($asset_row) && isset($asset_row['asset_type'])) {
$type = $asset_row['asset_type'];
$type = mysqli_real_escape_string($mysqli, $asset_row['asset_type']);
} else {
$type = '';
}
@@ -28,14 +28,14 @@ if (isset($_POST['asset_type'])) {
if (isset($_POST['asset_make'])) {
$make = sanitizeInput($_POST['asset_make']);
} elseif (isset($asset_row) && isset($asset_row['asset_make'])) {
$make = $asset_row['asset_make'];
$make = mysqli_real_escape_string($mysqli, $asset_row['asset_make']);
} else {
$make = '';
}
if (isset($_POST['asset_model'])) {
$model = sanitizeInput($_POST['asset_model']);
} elseif (isset($asset_row) && isset($asset_row['asset_model'])) {
$model = $asset_row['asset_model'];
$model = mysqli_real_escape_string($mysqli, $asset_row['asset_model']);
} else {
$model = '';
}
@@ -43,7 +43,7 @@ if (isset($_POST['asset_model'])) {
if (isset($_POST['asset_serial'])) {
$serial = sanitizeInput($_POST['asset_serial']);
} elseif (isset($asset_row) && isset($asset_row['asset_serial'])) {
$serial = $asset_row['asset_serial'];
$serial = mysqli_real_escape_string($mysqli, $asset_row['asset_serial']);
} else {
$serial = '';
}
@@ -51,7 +51,7 @@ if (isset($_POST['asset_serial'])) {
if (isset($_POST['asset_os'])) {
$os = sanitizeInput($_POST['asset_os']);
} elseif (isset($asset_row) && isset($asset_row['asset_os'])) {
$os = $asset_row['asset_os'];
$os = mysqli_real_escape_string($mysqli, $asset_row['asset_os']);
} else {
$os = '';
}
@@ -59,7 +59,7 @@ if (isset($_POST['asset_os'])) {
if (isset($_POST['asset_ip'])) {
$ip = sanitizeInput($_POST['asset_ip']);
} elseif (isset($asset_row) && isset($asset_row['interface_ip'])) {
$ip = $asset_row['interface_ip'];
$ip = mysqli_real_escape_string($mysqli, $asset_row['interface_ip']);
} else {
$ip = '';
}
@@ -67,7 +67,7 @@ if (isset($_POST['asset_ip'])) {
if (isset($_POST['asset_mac'])) {
$mac = sanitizeInput($_POST['asset_mac']);
} elseif (isset($asset_row) && isset($asset_row['interface_mac'])) {
$mac = $asset_row['interface_mac'];
$mac = mysqli_real_escape_string($mysqli, $asset_row['interface_mac']);
} else {
$mac = '';
}
@@ -75,15 +75,23 @@ if (isset($_POST['asset_mac'])) {
if (isset($_POST['asset_uri'])) {
$uri = sanitizeInput($_POST['asset_uri']);
} elseif (isset($asset_row) && isset($asset_row['asset_uri'])) {
$uri = $asset_row['asset_uri'];
$uri = mysqli_real_escape_string($mysqli, $asset_row['asset_uri']);
} else {
$uri = '';
}
if (isset($_POST['asset_uri_2'])) {
$uri_2 = sanitizeInput($_POST['asset_uri_2']);
} elseif (isset($asset_row) && isset($asset_row['asset_uri_2'])) {
$uri_2 = mysqli_real_escape_string($mysqli, $asset_row['asset_uri_2']);
} else {
$uri_2 = '';
}
if (isset($_POST['asset_status'])) {
$status = sanitizeInput($_POST['asset_status']);
} elseif (isset($asset_row) && isset($asset_row['asset_status'])) {
$status = $asset_row['asset_status'];
$status = mysqli_real_escape_string($mysqli, $asset_row['asset_status']);
} else {
$status = '';
}
@@ -91,7 +99,7 @@ if (isset($_POST['asset_status'])) {
if (isset($_POST['asset_purchase_date']) && !empty($_POST['asset_purchase_date'])) {
$purchase_date = "'" . sanitizeInput($_POST['asset_purchase_date']) . "'";
} elseif (isset($asset_row) && isset($asset_row['asset_purchase_date'])) {
$purchase_date = "'" . $asset_row['asset_purchase_date'] . "'";
$purchase_date = "'" . mysqli_real_escape_string($mysqli, $asset_row['asset_purchase_date']) . "'";
} else {
$purchase_date = "NULL";
}
@@ -99,7 +107,7 @@ if (isset($_POST['asset_purchase_date']) && !empty($_POST['asset_purchase_date']
if (isset($_POST['asset_warranty_expire']) && !empty($_POST['asset_warranty_expire'])) {
$warranty_expire = "'" . sanitizeInput($_POST['asset_warranty_expire']) . "'";
} elseif (isset($asset_row) && isset($asset_row['asset_warranty_expire'])) {
$warranty_expire = "'" . $asset_row['asset_warranty_expire'] . "'";
$warranty_expire = "'" . mysqli_real_escape_string($mysqli, $asset_row['asset_warranty_expire']) . "'";
} else {
$warranty_expire = "NULL";
}
@@ -107,7 +115,7 @@ if (isset($_POST['asset_warranty_expire']) && !empty($_POST['asset_warranty_expi
if (isset($_POST['asset_install_date']) && !empty($_POST['asset_install_date'])) {
$install_date = "'" . sanitizeInput($_POST['asset_install_date']) . "'";
} elseif (isset($asset_row) && isset($asset_row['asset_install_date'])) {
$install_date = "'" . $asset_row['asset_install_date'] . "'";
$install_date = "'" . mysqli_real_escape_string($mysqli, $asset_row['asset_install_date']) . "'";
} else {
$install_date = "NULL";
}
@@ -115,7 +123,7 @@ if (isset($_POST['asset_install_date']) && !empty($_POST['asset_install_date']))
if (isset($_POST['asset_notes'])) {
$notes = sanitizeInput($_POST['asset_notes']);
} elseif (isset($asset_row) && isset($asset_row['asset_notes'])) {
$notes = $asset_row['asset_notes'];
$notes = mysqli_real_escape_string($mysqli, $asset_row['asset_notes']);
} else {
$notes = '';
}

View File

@@ -37,7 +37,7 @@ if (isset($_GET['asset_id'])) {
} elseif (isset($_GET['asset_uri_2'])) {
// Asset query via uri2
$uri2 = mysqli_real_escape_string($mysqli, $_GET['asset_uri']);
$uri2 = mysqli_real_escape_string($mysqli, $_GET['asset_uri_2']);
$sql = mysqli_query($mysqli, "SELECT * FROM assets WHERE asset_uri_2 = '$uri2' AND asset_client_id LIKE '$client_id' ORDER BY asset_id LIMIT $limit OFFSET $offset");
}else {

View File

@@ -18,8 +18,7 @@ if (!empty($asset_id)) {
// Variable assignment from POST - assigning the current database value if a value is not provided
require_once 'asset_model.php';
$update_sql = mysqli_query($mysqli, "UPDATE assets SET asset_name = '$name', asset_description = '$description', asset_type = '$type', asset_make = '$make', asset_model = '$model', asset_serial = '$serial', asset_os = '$os', asset_uri = '$uri', asset_status = '$status', asset_location_id = $location, asset_vendor_id = $vendor, asset_contact_id = $contact, asset_purchase_date = $purchase_date, asset_warranty_expire = $warranty_expire, asset_install_date = $install_date, asset_notes = '$notes' WHERE asset_id = $asset_id AND asset_client_id = $client_id LIMIT 1");
$update_sql = mysqli_query($mysqli, "UPDATE assets SET asset_name = '$name', asset_description = '$description', asset_type = '$type', asset_make = '$make', asset_model = '$model', asset_serial = '$serial', asset_os = '$os', asset_uri = '$uri', asset_uri_2 = '$uri_2', asset_status = '$status', asset_location_id = $location, asset_vendor_id = $vendor, asset_contact_id = $contact, asset_purchase_date = $purchase_date, asset_warranty_expire = $warranty_expire, asset_install_date = $install_date, asset_notes = '$notes' WHERE asset_id = $asset_id AND asset_client_id = $client_id LIMIT 1");
// Check insert & get insert ID
if ($update_sql) {
@@ -28,6 +27,9 @@ if (!empty($asset_id)) {
// Update Primary Interface
mysqli_query($mysqli,"UPDATE asset_interfaces SET interface_mac = '$mac', interface_ip = '$ip', interface_network_id = $network WHERE interface_asset_id = $asset_id AND interface_primary = 1");
// Add to History
mysqli_query($mysqli,"INSERT INTO asset_history SET asset_history_status = '$status', asset_history_description = 'API updated $name ($api_key_name)', asset_history_asset_id = $asset_id");
// Logging
logAction("Asset", "Edit", "$name via API ($api_key_name)", $client_id);
logAction("API", "Success", "Edited asset $name via API ($api_key_name)", $client_id);

View File

@@ -5,7 +5,7 @@
if (isset($_POST['client_name'])) {
$name = sanitizeInput($_POST['client_name']);
} elseif ($client_row) {
$name = $client_row['client_name'];
$name = mysqli_real_escape_string($mysqli, $client_row['client_name']);
} else {
$name = '';
}
@@ -13,7 +13,7 @@ if (isset($_POST['client_name'])) {
if (isset($_POST['client_type'])) {
$type = sanitizeInput($_POST['client_type']);
} elseif ($client_row) {
$type = $client_row['client_type'];
$type = mysqli_real_escape_string($mysqli, $client_row['client_type']);
} else {
$type = '';
}
@@ -21,7 +21,7 @@ if (isset($_POST['client_type'])) {
if (isset($_POST['client_website'])) {
$website = preg_replace("(^https?://)", "", sanitizeInput($_POST['client_website']));
} elseif ($client_row) {
$website = $client_row['client_website'];
$website = mysqli_real_escape_string($mysqli, $client_row['client_website']);
} else {
$website = '';
}
@@ -29,7 +29,7 @@ if (isset($_POST['client_website'])) {
if (isset($_POST['client_referral'])) {
$referral = sanitizeInput($_POST['client_referral']);
} elseif ($client_row) {
$referral = $client_row['client_referral'];
$referral = mysqli_real_escape_string($mysqli, $client_row['client_referral']);
} else {
$referral = '';
}
@@ -45,7 +45,7 @@ if (isset($_POST['client_rate'])) {
if (isset($_POST['client_currency_code'])) {
$currency_code = sanitizeInput($_POST['client_currency_code']);
} elseif ($client_row) {
$currency_code = $client_row['client_currency_code'];
$currency_code = mysqli_real_escape_string($mysqli, $client_row['client_currency_code']);
} else {
$currency_code = '';
}
@@ -61,7 +61,7 @@ if (isset($_POST['client_net_terms'])) {
if (isset($_POST['client_tax_id_number'])) {
$tax_id_number = sanitizeInput($_POST['client_tax_id_number']);
} elseif ($client_row) {
$tax_id_number = $client_row['client_tax_id_number'];
$tax_id_number = mysqli_real_escape_string($mysqli, $client_row['client_tax_id_number']);
} else {
$tax_id_number = '';
}
@@ -69,7 +69,7 @@ if (isset($_POST['client_tax_id_number'])) {
if (isset($_POST['client_abbreviation'])) {
$abbreviation = sanitizeInput(substr($_POST['client_abbreviation'], 0, 6));
} elseif ($client_row) {
$abbreviation = $client_row['client_abbreviation'];
$abbreviation = mysqli_real_escape_string($mysqli, $client_row['client_abbreviation']);
} else {
$abbreviation = '';
}
@@ -85,7 +85,7 @@ if (isset($_POST['client_is_lead'])) {
if (isset($_POST['client_notes'])) {
$notes = sanitizeInput($_POST['client_notes']);
} elseif ($client_row) {
$notes = $client_row['client_notes'];
$notes = mysqli_real_escape_string($mysqli, $client_row['client_notes']);
} else {
$notes = '';
}

View File

@@ -3,9 +3,6 @@
require_once '../validate_api_key.php';
require_once '../require_post_method.php';
// Parse Info
$client_id = intval($_POST['client_id']);
// Default
$update_count = false;

View File

@@ -5,7 +5,7 @@ define('number_regex', '/[^0-9]/');
if (isset($_POST['contact_name'])) {
$name = sanitizeInput($_POST['contact_name']);
} elseif ($contact_row) {
$name = $contact_row['contact_name'];
$name = mysqli_real_escape_string($mysqli, $contact_row['contact_name']);
} else {
$name = '';
}
@@ -13,7 +13,7 @@ if (isset($_POST['contact_name'])) {
if (isset($_POST['contact_title'])) {
$title = sanitizeInput($_POST['contact_title']);
} elseif ($contact_row) {
$title = $contact_row['contact_title'];
$title = mysqli_real_escape_string($mysqli, $contact_row['contact_title']);
} else {
$title = '';
}
@@ -21,7 +21,7 @@ if (isset($_POST['contact_title'])) {
if (isset($_POST['contact_department'])) {
$department = sanitizeInput($_POST['contact_department']);
} elseif ($contact_row) {
$department = $contact_row['contact_department'];
$department = mysqli_real_escape_string($mysqli, $contact_row['contact_department']);
} else {
$department = '';
}
@@ -29,7 +29,7 @@ if (isset($_POST['contact_department'])) {
if (isset($_POST['contact_email'])) {
$email = sanitizeInput($_POST['contact_email']);
} elseif ($contact_row) {
$email = $contact_row['contact_email'];
$email = mysqli_real_escape_string($mysqli, $contact_row['contact_email']);
} else {
$email = '';
}
@@ -37,7 +37,7 @@ if (isset($_POST['contact_email'])) {
if (isset($_POST['contact_phone'])) {
$phone = preg_replace(number_regex, '', $_POST['contact_phone']);
} elseif ($contact_row) {
$phone = $contact_row['contact_phone'];
$phone = mysqli_real_escape_string($mysqli, $contact_row['contact_phone']);
} else {
$phone = '';
}
@@ -45,7 +45,7 @@ if (isset($_POST['contact_phone'])) {
if (isset($_POST['contact_extension'])) {
$extension = preg_replace(number_regex, '', $_POST['contact_extension']);
} elseif ($contact_row) {
$extension = $contact_row['contact_extension'];
$extension = mysqli_real_escape_string($mysqli, $contact_row['contact_extension']);
} else {
$extension = '';
}
@@ -53,7 +53,7 @@ if (isset($_POST['contact_extension'])) {
if (isset($_POST['contact_mobile'])) {
$mobile = preg_replace(number_regex, '', $_POST['contact_mobile']);
} elseif ($contact_row) {
$mobile = $contact_row['contact_mobile'];
$mobile = mysqli_real_escape_string($mysqli, $contact_row['contact_mobile']);
} else {
$mobile = '';
}
@@ -61,7 +61,7 @@ if (isset($_POST['contact_mobile'])) {
if (isset($_POST['contact_notes'])) {
$notes = sanitizeInput($_POST['contact_notes']);
} elseif ($contact_row) {
$notes = $contact_row['contact_notes'];
$notes = mysqli_real_escape_string($mysqli, $contact_row['contact_notes']);
} else {
$notes = '';
}

View File

@@ -13,7 +13,7 @@ $insert_id = false;
if (!empty($api_key_decrypt_password) && !empty($name) && !(empty($password))) {
// Add credential
$insert_sql = mysqli_query($mysqli,"INSERT INTO credentials SET credential_name = '$name', credential_description = '$description', credential_uri = '$uri', credential_uri_2 = '$uri_2', credential_username = '$username', credential_password = '$password', credential_otp_secret = '$otp_secret', credential_note = '$note', credential_important = $important, credential_contact_id = $contact_id, credential_vendor_id = $vendor_id, credential_asset_id = $asset_id, credential_software_id = $software_id, credential_client_id = $client_id");
$insert_sql = mysqli_query($mysqli,"INSERT INTO credentials SET credential_name = '$name', credential_description = '$description', credential_uri = '$uri', credential_uri_2 = '$uri_2', credential_username = '$username', credential_password = '$password', credential_otp_secret = '$otp_secret', credential_note = '$note', credential_favorite = $favorite, credential_contact_id = $contact_id, credential_asset_id = $asset_id, credential_client_id = $client_id");
// Check insert & get insert ID
if ($insert_sql) {

View File

@@ -11,7 +11,7 @@ if (isset($_POST['api_key_decrypt_password'])) {
if (isset($_POST['credential_name'])) {
$name = sanitizeInput($_POST['credential_name']);
} elseif (isset($credential_row) && isset($credential_row['credential_name'])) {
$name = $credential_row['credential_name'];
$name = mysqli_real_escape_string($mysqli, $credential_row['credential_name']);
} else {
$name = '';
}
@@ -19,7 +19,7 @@ if (isset($_POST['credential_name'])) {
if (isset($_POST['credential_description'])) {
$description = sanitizeInput($_POST['credential_description']);
} elseif (isset($credential_row) && isset($credential_row['credential_description'])) {
$description = $credential_row['credential_description'];
$description = mysqli_real_escape_string($mysqli, $credential_row['credential_description']);
} else {
$description = '';
}
@@ -27,7 +27,7 @@ if (isset($_POST['credential_description'])) {
if (isset($_POST['credential_uri'])) {
$uri = sanitizeInput($_POST['credential_uri']);
} elseif (isset($credential_row) && isset($credential_row['credential_uri'])) {
$uri = $credential_row['credential_uri'];
$uri = mysqli_real_escape_string($mysqli, $credential_row['credential_uri']);
} else {
$uri = '';
}
@@ -35,7 +35,7 @@ if (isset($_POST['credential_uri'])) {
if (isset($_POST['credential_uri_2'])) {
$uri_2 = sanitizeInput($_POST['credential_uri_2']);
} elseif (isset($credential_row) && isset($credential_row['credential_uri_2'])) {
$uri_2 = $credential_row['credential_uri_2'];
$uri_2 = mysqli_real_escape_string($mysqli, $credential_row['credential_uri_2']);
} else {
$uri_2 = '';
}
@@ -61,12 +61,10 @@ if (isset($_POST['credential_password'])) {
$password_changed = false;
}
if (isset($_POST['credential_otp_secret'])) {
$otp_secret = sanitizeInput($_POST['credential_otp_secret']);
} elseif (isset($credential_row) && isset($credential_row['credential_otp_secret'])) {
$otp_secret = $credential_row['credential_otp_secret'];
$otp_secret = mysqli_real_escape_string($mysqli, $credential_row['credential_otp_secret']);
} else {
$otp_secret = '';
}
@@ -74,17 +72,17 @@ if (isset($_POST['credential_otp_secret'])) {
if (isset($_POST['credential_note'])) {
$note = sanitizeInput($_POST['credential_note']);
} elseif (isset($credential_row) && isset($credential_row['credential_note'])) {
$note = $credential_row['credential_note'];
$note = mysqli_real_escape_string($mysqli, $credential_row['credential_note']);
} else {
$note = '';
}
if (isset($_POST['credential_important'])) {
$important = intval($_POST['credential_important']);
} elseif (isset($credential_row) && isset($credential_row['credential_important'])) {
$important = $credential_row['credential_important'];
if (isset($_POST['credential_favorite'])) {
$favorite = intval($_POST['credential_favorite']);
} elseif (isset($credential_row) && isset($credential_row['credential_favorite'])) {
$favorite = $credential_row['credential_favorite'];
} else {
$important = '';
$favorite = 0;
}
if (isset($_POST['credential_contact_id'])) {
@@ -92,7 +90,7 @@ if (isset($_POST['credential_contact_id'])) {
} elseif (isset($credential_row) && isset($credential_row['credential_contact_id'])) {
$contact_id = $credential_row['credential_contact_id'];
} else {
$contact_id = '';
$contact_id = 0;
}
if (isset($_POST['credential_vendor_id'])) {
@@ -100,7 +98,7 @@ if (isset($_POST['credential_vendor_id'])) {
} elseif (isset($credential_row) && isset($credential_row['credential_vendor_id'])) {
$vendor_id = $credential_row['credential_vendor_id'];
} else {
$vendor_id = '';
$vendor_id = 0;
}
if (isset($_POST['credential_asset_id'])) {
@@ -108,7 +106,7 @@ if (isset($_POST['credential_asset_id'])) {
} elseif (isset($credential_row) && isset($credential_row['credential_asset_id'])) {
$asset_id = $credential_row['credential_asset_id'];
} else {
$asset_id = '';
$asset_id = 0;
}
if (isset($_POST['credential_software_id'])) {
@@ -116,5 +114,5 @@ if (isset($_POST['credential_software_id'])) {
} elseif (isset($credential_row) && isset($credential_row['credential_software_id'])) {
$software_id = $credential_row['credential_software_id'];
} else {
$software_id = '';
$software_id = 0;
}

View File

@@ -17,7 +17,7 @@ if (!empty($_POST['api_key_decrypt_password']) && !empty($credential_id)) {
// Variable assignment from POST - assigning the current database value if a value is not provided
require_once 'credential_model.php';
$update_sql = mysqli_query($mysqli,"UPDATE credentials SET credential_name = '$name', credential_description = '$description', credential_uri = '$uri', credential_uri_2 = '$uri_2', credential_username = '$username', credential_password = '$password', credential_otp_secret = '$otp_secret', credential_note = '$note', credential_important = $important, credential_contact_id = $contact_id, credential_vendor_id = $vendor_id, credential_asset_id = $asset_id, credential_software_id = $software_id, credential_client_id = $client_id WHERE credential_id = '$credential_id' AND credential_client_id = $client_id LIMIT 1");
$update_sql = mysqli_query($mysqli,"UPDATE credentials SET credential_name = '$name', credential_description = '$description', credential_uri = '$uri', credential_uri_2 = '$uri_2', credential_username = '$username', credential_password = '$password', credential_otp_secret = '$otp_secret', credential_note = '$note', credential_favorite = $favorite, credential_contact_id = $contact_id, credential_asset_id = $asset_id, credential_client_id = $client_id WHERE credential_id = '$credential_id' AND credential_client_id = $client_id LIMIT 1");
// Check insert & get insert ID
if ($update_sql) {

View File

@@ -4,7 +4,7 @@
if (isset($_POST['document_name'])) {
$name = sanitizeInput($_POST['document_name']);
} elseif (isset($document_row) && isset($document_row['document_name'])) {
$name = $document_row['document_name'];
$name = mysqli_real_escape_string($mysqli, $document_row['document_name']);
} else {
$name = '';
}
@@ -12,7 +12,7 @@ if (isset($_POST['document_name'])) {
if (isset($_POST['document_description'])) {
$description = sanitizeInput($_POST['document_description']);
} elseif (isset($document_row) && isset($document_row['document_description'])) {
$description = $document_row['document_description'];
$description = mysqli_real_escape_string($mysqli, $document_row['document_description']);
} else {
$description = '';
}
@@ -20,7 +20,7 @@ if (isset($_POST['document_description'])) {
if (isset($_POST['document_content'])) {
$content = mysqli_real_escape_string($mysqli, $_POST['document_content']);
} elseif (isset($document_row) && isset($document_row['document_content'])) {
$content = $document_row['document_content'];
$content = mysqli_real_escape_string($mysqli, $document_row['document_content']);
} else {
$content = '';
}
@@ -29,7 +29,7 @@ if (isset($_POST['document_content'])) {
if (isset($_POST['document_content'])) {
$content_raw = sanitizeInput($_POST['document_name'] . $_POST['document_description'] . " " . str_replace("<", " <", $_POST['document_content']));
} elseif (isset($document_row) && isset($document_row['document_content_raw'])) {
$content_raw = $document_row['document_content_raw'];
$content_raw = mysqli_real_escape_string($mysqli, $document_row['document_content_raw']);
} else {
$content_raw = '';
}

View File

@@ -60,8 +60,18 @@ if (!empty($document_id)) {
$document_version_id = mysqli_insert_id($mysqli);
// 3) Variable assignment from POST (uses trigger you already have)
// 3) Variable assignment from POST
// This should set: $name, $description, $content (raw html), $folder, etc.
// Fetch current doc data (fresh)
$document_row = mysqli_fetch_assoc(mysqli_query($mysqli, "
SELECT * FROM documents
WHERE document_client_id = $client_id
AND document_id = $document_id
LIMIT 1
"));
// Assign variables from POST or fallback to DB
require_once 'document_model.php';
// Process NEW HTML content: save base64 images to /uploads/documents/<document_id>/

View File

@@ -13,7 +13,7 @@ if (isset($_GET['domain_id'])) {
} elseif (isset($_GET['domain_name'])) {
// Domain by name
$name = mysqli_real_escape_string($mysqli, $_GET['domain_name']);
$sql = mysqli_query($mysqli, "SELECT * FROM domains WHERE domain_name = '$name' AND domain_client_id LIKE '$client_id' ORDER BY asset_id LIMIT $limit OFFSET $offset");
$sql = mysqli_query($mysqli, "SELECT * FROM domains WHERE domain_name = '$name' AND domain_client_id LIKE '$client_id' ORDER BY domain_id LIMIT $limit OFFSET $offset");
} else {
// All domains (by client ID or all in general if key permits)

View File

@@ -0,0 +1,64 @@
<?php
/*
* API - Invoice Items - Read
* GET /api/v1/invoice_items/read.php
*
* Returns line items belonging to invoices scoped to the API key's client.
*
* Parameters (GET):
* api_key required - Your API key
* invoice_id required* - Return items for a single invoice
* item_id required* - Return a single line item by its own ID
* * One of invoice_id or item_id must be provided
* limit optional - Max rows to return (default 50)
* offset optional - Offset for pagination (default 0)
*
* Security:
* - invoice_items are always joined to invoices so that invoice_client_id
* is checked against the API key's client scope. A scoped key can never
* read items belonging to another client, even when item_id is supplied
* directly.
* - $client_id is set to "%" by validate_api_key.php for All-Clients keys,
* which causes the LIKE to match every client — consistent with other
* endpoints in this API.
*/
require_once '../validate_api_key.php';
require_once '../require_get_method.php';
if (isset($_GET['item_id'])) {
// Single line item by item_id — still JOIN to invoices to enforce client scope
$item_id = intval($_GET['item_id']);
$sql = mysqli_query($mysqli,
"SELECT ii.*
FROM invoice_items ii
INNER JOIN invoices i ON i.invoice_id = ii.item_invoice_id
WHERE ii.item_id = '$item_id'
AND i.invoice_client_id LIKE '$client_id'
LIMIT 1"
);
} elseif (isset($_GET['invoice_id'])) {
// All items on a specific invoice
$invoice_id = intval($_GET['invoice_id']);
$sql = mysqli_query($mysqli,
"SELECT ii.*
FROM invoice_items ii
INNER JOIN invoices i ON i.invoice_id = ii.item_invoice_id
WHERE ii.item_invoice_id = '$invoice_id'
AND i.invoice_client_id LIKE '$client_id'
ORDER BY ii.item_order ASC, ii.item_id ASC
LIMIT $limit OFFSET $offset"
);
} else {
// No filter supplied — reject the request
http_response_code(400);
echo json_encode([
'success' => 'False',
'message' => 'A filter is required. Please supply either invoice_id or item_id.',
'count' => 0,
'data' => []
]);
exit;
}
// Output
require_once "../read_output.php";

View File

@@ -5,7 +5,7 @@
if (isset($_POST['location_name'])) {
$name = sanitizeInput($_POST['location_name']);
} elseif ($location_row) {
$name = $location_row['location_name'];
$name = mysqli_real_escape_string($mysqli, $location_row['location_name']);
} else {
$name = '';
}
@@ -13,7 +13,7 @@ if (isset($_POST['location_name'])) {
if (isset($_POST['location_description'])) {
$description = sanitizeInput($_POST['location_description']);
} elseif ($location_row) {
$description = $location_row['location_description'];
$description = mysqli_real_escape_string($mysqli, $location_row['location_description']);
} else {
$description = '';
}
@@ -21,7 +21,7 @@ if (isset($_POST['location_description'])) {
if (isset($_POST['location_country'])) {
$country = sanitizeInput($_POST['location_country']);
} elseif ($location_row) {
$country = $location_row['location_country'];
$country = mysqli_real_escape_string($mysqli, $location_row['location_country']);
} else {
$country = '';
}
@@ -29,7 +29,7 @@ if (isset($_POST['location_country'])) {
if (isset($_POST['location_address'])) {
$address = sanitizeInput($_POST['location_address']);
} elseif ($location_row) {
$address = $location_row['location_address'];
$address = mysqli_real_escape_string($mysqli, $location_row['location_address']);
} else {
$address = '';
}
@@ -37,7 +37,7 @@ if (isset($_POST['location_address'])) {
if (isset($_POST['location_city'])) {
$city = sanitizeInput($_POST['location_city']);
} elseif ($location_row) {
$city = $location_row['location_city'];
$city = mysqli_real_escape_string($mysqli, $location_row['location_city']);
} else {
$city = '';
}
@@ -45,7 +45,7 @@ if (isset($_POST['location_city'])) {
if (isset($_POST['location_state'])) {
$state = sanitizeInput($_POST['location_state']);
} elseif ($location_row) {
$state = $location_row['location_state'];
$state = mysqli_real_escape_string($mysqli, $location_row['location_state']);
} else {
$state = '';
}
@@ -53,7 +53,7 @@ if (isset($_POST['location_state'])) {
if (isset($_POST['location_zip'])) {
$zip = sanitizeInput($_POST['location_zip']);
} elseif ($location_row) {
$zip = $location_row['location_zip'];
$zip = mysqli_real_escape_string($mysqli, $location_row['location_zip']);
} else {
$zip = '';
}
@@ -61,7 +61,7 @@ if (isset($_POST['location_zip'])) {
if (isset($_POST['location_hours'])) {
$hours = sanitizeInput($_POST['location_hours']);
} elseif ($location_row) {
$hours = $location_row['location_hours'];
$hours = mysqli_real_escape_string($mysqli, $location_row['location_hours']);
} else {
$hours = '';
}
@@ -69,7 +69,7 @@ if (isset($_POST['location_hours'])) {
if (isset($_POST['location_notes'])) {
$notes = sanitizeInput($_POST['location_notes']);
} elseif ($location_row) {
$notes = $location_row['location_notes'];
$notes = mysqli_real_escape_string($mysqli, $location_row['location_notes']);
} else {
$notes = '';
}

View File

@@ -1,31 +0,0 @@
<?php
require_once '../validate_api_key.php';
require_once '../require_get_method.php';
// Payments aren't stored against client IDs, so we instead validate the API key is for All Clients
if (isset($_GET['payment_id']) && $client_id == "%") {
// Payment via ID (single)
$id = intval($_GET['payment_id']);
$sql = mysqli_query($mysqli, "SELECT * FROM payments WHERE payment_id = '$id'");
} elseif (isset($_GET['payment_invoice_id']) && $client_id == "%") {
// Payments for an invoice
$id = intval($_GET['payment_invoice_id']);
$sql = mysqli_query($mysqli, "SELECT * FROM payments WHERE payment_invoice_id = '$id'");
} elseif ($client_id == "%") {
// All payments
$sql = mysqli_query($mysqli, "SELECT * FROM payments ORDER BY payment_id LIMIT $limit OFFSET $offset");
}
// Output
require_once "../read_output.php";

View File

@@ -21,7 +21,7 @@ if (isset($_POST['ticket_asset_id'])) {
if (isset($_POST['ticket_subject'])) {
$subject = sanitizeInput($_POST['ticket_subject']);
} elseif ($ticket_row) {
$subject = $ticket_row['ticket_subject'];
$subject = mysqli_real_escape_string($mysqli, $ticket_row['ticket_subject']);
} else {
$subject = '';
}
@@ -30,16 +30,16 @@ if (isset($_POST['ticket_subject'])) {
if (isset($_POST['ticket_priority'])) {
$priority = sanitizeInput($_POST['ticket_priority']);
} elseif ($ticket_row) {
$priority = $ticket_row['ticket_priority'];
$priority = mysqli_real_escape_string($mysqli, $ticket_row['ticket_priority']);
} else {
$priority = 'Low';
}
if (isset($_POST['ticket_details'])) {
$details = mysqli_escape_string($mysqli, $_POST['ticket_details'] . "<br>");
$details = mysqli_real_escape_string($mysqli, $_POST['ticket_details'] . "<br>");
} elseif ($ticket_row) {
$details = $ticket_row['ticket_details'];
$details = mysqli_real_escape_string($mysqli, $ticket_row['ticket_details']);
} else {
$details = '< blank ><br>';
}

View File

@@ -16,7 +16,7 @@ if (isset($update_count) && is_numeric($update_count) && $update_count > 0) {
// Query returned false: something went wrong, or it was declined due to required variables missing
else {
$return_arr['success'] = "False";
$return_arr['message'] = "Auth success but update query failed/returned no results. Ensure ALL required variables are provided and database schema is up-to-date. Most likely cause: non-existent module ID (i.e. bad contact ID/ticket ID/etc).";
$return_arr['message'] = "Auth success but update query failed/returned no results. Ensure ALL required variables are provided and database schema is up-to-date. Most likely cause: non-existent module ID (i.e. bad contact ID/ticket ID/etc) or no rows changed.";
// Log any database/schema related errors to the PHP Error log
if (mysqli_error($mysqli)) {

View File

@@ -125,6 +125,7 @@ if (isset($_POST['code']) && $_POST['state'] == session_id()) {
$_SESSION['user_id'] = $user_id;
$_SESSION['user_type'] = 2;
$_SESSION['contact_id'] = $contact_id;
$_SESSION['csrf_token'] = randomString(32);
$_SESSION['login_method'] = "azure";
// Logging

View File

@@ -524,12 +524,6 @@ if (isset($_GET['add_payment_by_provider'])) {
$contact_extension = preg_replace("/[^0-9]/", '',$row['contact_extension']);
$contact_mobile = sanitizeInput(formatPhoneNumber($row['contact_mobile'], $row['contact_mobile_country_code']));
// Check to make sure saved payment method belongs to logged in client
if ($client_id !== $session_client_id) {
flash_alert("Saved Payment method does not belong to you!", 'danger');
redirect();
}
// Get ITFlow company details
$sql = mysqli_query($mysqli,"SELECT * FROM companies WHERE company_id = 1");
$row = mysqli_fetch_assoc($sql);
@@ -548,7 +542,7 @@ if (isset($_GET['add_payment_by_provider'])) {
$config_invoice_from_email = sanitizeInput($config_invoice_from_email);
// Get Client Payment Details
$sql = mysqli_query($mysqli, "SELECT * FROM client_saved_payment_methods LEFT JOIN payment_providers ON saved_payment_provider_id = payment_provider_id LEFT JOIN client_payment_provider ON saved_payment_client_id = client_id WHERE saved_payment_id = $saved_payment_id LIMIT 1");
$sql = mysqli_query($mysqli, "SELECT * FROM client_saved_payment_methods LEFT JOIN payment_providers ON saved_payment_provider_id = payment_provider_id LEFT JOIN client_payment_provider ON saved_payment_client_id = client_id WHERE saved_payment_id = $saved_payment_id AND saved_payment_client_id = $session_client_id LIMIT 1");
$row = mysqli_fetch_assoc($sql);
$public_key = sanitizeInput($row['payment_provider_public_key']);
@@ -561,9 +555,17 @@ if (isset($_GET['add_payment_by_provider'])) {
$payment_provider_client = sanitizeInput($row['payment_provider_client']);
$saved_payment_method = sanitizeInput($row['saved_payment_provider_method']);
$saved_payment_description = sanitizeInput($row['saved_payment_description']);
$payment_client_id = intval($row['saved_payment_client_id']);
// Sanity checks
if (!$payment_provider_client || !$saved_payment_method) {
// Check to make invoice belongs to logged in client
if ($client_id !== $session_client_id) {
flash_alert("Invoice does not belong to you!", 'danger');
redirect();
} elseif ($payment_client_id !== $session_client_id) {
flash_alert("Saved Payment method does not belong to you!", 'danger');
redirect();
} elseif (!$payment_provider_client || !$saved_payment_method) {
flash_alert("Stripe not enabled or no client card saved", 'error');
redirect();
} elseif ($invoice_status !== 'Sent' && $invoice_status !== 'Viewed') {
@@ -855,7 +857,7 @@ if (isset($_GET['create_stripe_checkout'])) {
if (isset($_GET['stripe_save_card'])) {
validateCSRFToken($_GET['csrf_token']);
// validateCSRFToken($_GET['csrf_token']); Broken with Stripe Save Card JQ 2026-5-4
if ($session_contact_primary == 0 && !$session_contact_is_billing_contact) {
redirect("post.php?logout");

View File

@@ -105,13 +105,14 @@ logApp("Cron", "info", "Cron Started");
mysqli_query($mysqli, "TRUNCATE TABLE ticket_views");
// Clean-up shared items that have been used
mysqli_query($mysqli, "DELETE FROM shared_items WHERE item_views = item_view_limit");
mysqli_query($mysqli, "DELETE FROM shared_items WHERE item_view_limit > 0 AND item_views >= item_view_limit");
// Clean-up shared items that have expired
mysqli_query($mysqli, "DELETE FROM shared_items WHERE item_expire_at < NOW()");
// Invalidate any password reset links
mysqli_query($mysqli, "UPDATE users SET user_password_reset_token = NULL WHERE user_archived_at IS NULL");
mysqli_query($mysqli, "UPDATE users SET user_password_reset_token = NULL"); // TODO: Make this 'expired' tokens only when we actually use expiry
// Clean-up old dismissed notifications
mysqli_query($mysqli, "DELETE FROM notifications WHERE notification_dismissed_at < CURDATE() - INTERVAL 90 DAY");
@@ -524,7 +525,7 @@ if ($config_send_invoice_reminders == 1) {
// Late Charges
if ($config_invoice_late_fee_enable == 1) {
if ($config_invoice_late_fee_enable == 1 && $day > 1) {
$todays_date = date('Y-m-d');
$late_fee_amount = ($invoice_amount * $config_invoice_late_fee_percent) / 100;

View File

@@ -0,0 +1,932 @@
<?php
/*
* CRON - Email Parser (Webklex PHP-IMAP)
* Process emails and create/update tickets using Webklex\PHPIMAP instead of native IMAP
*/
// Start the timer
$script_start_time = microtime(true);
// Set working directory to the directory this cron script lives at.
chdir(dirname(__FILE__));
// Ensure we're running from command line
if (php_sapi_name() !== 'cli') {
die("This script must be run from the command line.\n");
}
// Autoload (Webklex & any composer deps)
require_once "../plugins/vendor/autoload.php";
// Get ITFlow config & helper functions
require_once "../config.php";
// Set Timezone
require_once "../includes/inc_set_timezone.php";
require_once "../functions.php";
// Get settings for the "default" company
require_once "../includes/load_global_settings.php";
$config_ticket_prefix = sanitizeInput($config_ticket_prefix);
$config_ticket_from_name = sanitizeInput($config_ticket_from_name);
$config_ticket_email_parse_unknown_senders = intval($row['config_ticket_email_parse_unknown_senders']);
// Get company name & phone & timezone
$sql = mysqli_query($mysqli, "SELECT * FROM companies, settings WHERE companies.company_id = settings.company_id AND companies.company_id = 1");
$row = mysqli_fetch_assoc($sql);
$company_name = sanitizeInput($row['company_name']);
$company_phone = sanitizeInput(formatPhoneNumber($row['company_phone'], $row['company_phone_country_code']));
// Check setting enabled
if ($config_ticket_email_parse == 0) {
logApp("Cron-Email-Parser", "error", "Cron Email Parser unable to run - not enabled in admin settings.");
exit("Email Parser: Feature is not enabled - check Settings > Ticketing > Email-to-ticket parsing. See https://docs.itflow.org/ticket_email_parse -- Quitting..");
}
// System temp directory & lock
$temp_dir = sys_get_temp_dir();
$lock_file_path = "{$temp_dir}/itflow_email_parser_{$installation_id}.lock";
if (file_exists($lock_file_path)) {
$file_age = time() - filemtime($lock_file_path);
if ($file_age > 300) {
unlink($lock_file_path);
logApp("Cron-Email-Parser", "warning", "Cron Email Parser detected a lock file was present but was over 5 minutes old so it removed it.");
} else {
logApp("Cron-Email-Parser", "warning", "Lock file present. Cron Email Parser attempted to execute but was already executing, so instead it terminated.");
exit("Script is already running. Exiting.");
}
}
file_put_contents($lock_file_path, "Locked");
// Ensure lock gets removed even on fatal error
register_shutdown_function(function() use ($lock_file_path) {
if (file_exists($lock_file_path)) {
@unlink($lock_file_path);
}
});
// Allowed attachment extensions
$allowed_extensions = array('jpg', 'jpeg', 'gif', 'png', 'webp', 'svg', 'pdf', 'txt', 'md', 'doc', 'docx', 'csv', 'xls', 'xlsx', 'xlsm', 'zip', 'tar', 'gz');
/** ------------------------------------------------------------------
* Ticket / Reply helpers (unchanged)
* ------------------------------------------------------------------ */
function addTicket($contact_id, $contact_name, $contact_email, $client_id, $date, $subject, $message, $attachments, $original_message_file, $ccs) {
global $mysqli, $config_app_name, $company_name, $company_phone, $config_ticket_prefix, $config_ticket_client_general_notifications, $config_ticket_new_ticket_notification_email, $config_base_url, $config_ticket_from_name, $config_ticket_from_email, $config_ticket_default_billable, $allowed_extensions;
$bad_pattern = "/do[\W_]*not[\W_]*reply|no[\W_]*reply/i"; // Email addresses to ignore
// Atomically increment and get the new ticket number
mysqli_query($mysqli, "
UPDATE settings
SET
config_ticket_next_number = LAST_INSERT_ID(config_ticket_next_number),
config_ticket_next_number = config_ticket_next_number + 1
WHERE company_id = 1
");
$ticket_number = mysqli_insert_id($mysqli);
// Clean up the message
$message = trim($message);
// Remove DOCTYPE and meta tags
$message = preg_replace('/<!DOCTYPE[^>]*>/i', '', $message);
$message = preg_replace('/<meta[^>]*>/i', '', $message);
// Remove <html>, <head>, <body> and their closing tags
$message = preg_replace('/<\/?(html|head|body)[^>]*>/i', '', $message);
// Collapse excess whitespace
$message = preg_replace('/\s+/', ' ', $message);
// Convert newlines to <br>
$message = nl2br($message);
// Wrap final formatted message
$message = "<i>Email from: <b>$contact_name</b> &lt;$contact_email&gt; at $date:-</i> <br><br><div style='line-height:1.5;'>$message</div>";
$ticket_prefix_esc = mysqli_real_escape_string($mysqli, $config_ticket_prefix);
$message_esc = mysqli_real_escape_string($mysqli, $message);
$contact_email_esc = mysqli_real_escape_string($mysqli, $contact_email);
$client_id = intval($client_id);
$url_key = randomString(32);
mysqli_query($mysqli, "INSERT INTO tickets SET ticket_prefix = '$ticket_prefix_esc', ticket_number = $ticket_number, ticket_source = 'Email', ticket_subject = '$subject', ticket_details = '$message_esc', ticket_priority = 'Low', ticket_status = 1, ticket_billable = $config_ticket_default_billable, ticket_created_by = 0, ticket_contact_id = $contact_id, ticket_url_key = '$url_key', ticket_client_id = $client_id");
$id = mysqli_insert_id($mysqli);
// Logging
logAction("Ticket", "Create", "Email parser: Client contact $contact_email_esc created ticket $ticket_prefix_esc$ticket_number ($subject) ($id)", $client_id, $id);
mkdirMissing('../uploads/tickets/');
$att_dir = "../uploads/tickets/" . $id . "/";
mkdirMissing($att_dir);
// Move original .eml into the ticket folder
rename("../uploads/tmp/{$original_message_file}", "{$att_dir}/{$original_message_file}");
$original_message_file_esc = mysqli_real_escape_string($mysqli, $original_message_file);
mysqli_query($mysqli, "INSERT INTO ticket_attachments SET ticket_attachment_name = 'Original-parsed-email.eml', ticket_attachment_reference_name = '$original_message_file_esc', ticket_attachment_ticket_id = $id");
// Save non-inline attachments
foreach ($attachments as $attachment) {
$att_name = $attachment['name'];
$att_extension = strtolower(pathinfo($att_name, PATHINFO_EXTENSION));
if (in_array($att_extension, $allowed_extensions)) {
$att_saved_filename = md5(uniqid(rand(), true)) . '.' . $att_extension;
$att_saved_path = $att_dir . $att_saved_filename;
file_put_contents($att_saved_path, $attachment['content']);
$ticket_attachment_name = sanitizeInput($att_name);
$ticket_attachment_reference_name = sanitizeInput($att_saved_filename);
$ticket_attachment_name_esc = mysqli_real_escape_string($mysqli, $ticket_attachment_name);
$ticket_attachment_reference_name_esc = mysqli_real_escape_string($mysqli, $ticket_attachment_reference_name);
mysqli_query($mysqli, "INSERT INTO ticket_attachments SET ticket_attachment_name = '$ticket_attachment_name_esc', ticket_attachment_reference_name = '$ticket_attachment_reference_name_esc', ticket_attachment_ticket_id = $id");
} else {
$ticket_attachment_name_esc = mysqli_real_escape_string($mysqli, $att_name);
logAction("Ticket", "Edit", "Email parser: Blocked attachment $ticket_attachment_name_esc from Client contact $contact_email_esc for ticket $ticket_prefix_esc$ticket_number", $client_id, $id);
}
}
// Add unknown guests as ticket watcher
if ($client_id == 0 && !preg_match($bad_pattern, $contact_email_esc)) {
mysqli_query($mysqli, "INSERT INTO ticket_watchers SET watcher_email = '$contact_email_esc', watcher_ticket_id = $id");
}
// Add CCs as ticket watchers
foreach ($ccs as $cc) {
if (filter_var($cc, FILTER_VALIDATE_EMAIL) && !preg_match($bad_pattern, $cc)) {
$cc_esc = mysqli_real_escape_string($mysqli, $cc);
mysqli_query($mysqli, "INSERT INTO ticket_watchers SET watcher_email = '$cc_esc', watcher_ticket_id = $id");
}
}
// External email
$data = [];
if ($config_ticket_client_general_notifications == 1 && !preg_match($bad_pattern, $contact_email)) {
$subject_email = "Ticket created - [$config_ticket_prefix$ticket_number] - $subject";
$body = "<i style='color: #808080'>##- Please type your reply above this line -##</i><br><br>Hello $contact_name,<br><br>Thank you for your email. A ticket regarding \"$subject\" has been automatically created for you.<br><br>Ticket: $config_ticket_prefix$ticket_number<br>Subject: $subject<br>Status: New<br>Portal: <a href='https://$config_base_url/guest/guest_view_ticket.php?ticket_id=$id&url_key=$url_key'>View ticket</a><br><br>--<br>$company_name - Support<br>$config_ticket_from_email<br>$company_phone";
$data[] = [
'from' => $config_ticket_from_email,
'from_name' => $config_ticket_from_name,
'recipient' => $contact_email,
'recipient_name' => $contact_name,
'subject' => $subject_email,
'body' => mysqli_real_escape_string($mysqli, $body)
];
}
// Internal email
if ($config_ticket_new_ticket_notification_email) {
if ($client_id == 0) {
$client_name = "Guest";
$client_uri = '';
} else {
$client_sql = mysqli_query($mysqli, "SELECT client_name FROM clients WHERE client_id = $client_id");
$client_row = mysqli_fetch_assoc($client_sql);
$client_name = sanitizeInput($client_row['client_name']);
$client_uri = "&client_id=$client_id";
}
$email_subject = "$config_app_name - New Ticket - $client_name: $subject";
$email_body = "Hello, <br><br>This is a notification that a new ticket has been raised in ITFlow. <br>Client: $client_name<br>Priority: Low (email parsed)<br>Link: https://$config_base_url/agent/ticket.php?ticket_id=$id$client_uri <br><br>--------------------------------<br><br><b>$subject</b><br>$message";
$data[] = [
'from' => $config_ticket_from_email,
'from_name' => $config_ticket_from_name,
'recipient' => $config_ticket_new_ticket_notification_email,
'recipient_name' => $config_ticket_from_name,
'subject' => $email_subject,
'body' => mysqli_real_escape_string($mysqli, $email_body)
];
}
addToMailQueue($data);
customAction('ticket_create', $id);
return true;
}
function addReply($from_email, $date, $subject, $ticket_number, $message, $attachments) {
global $mysqli, $config_app_name, $company_name, $company_phone, $config_ticket_prefix, $config_base_url, $config_ticket_from_name, $config_ticket_from_email, $allowed_extensions;
$ticket_reply_type = 'Client';
// $message contains the raw HTML body from IMAP
// 1) Remove the reply separator and everything below it (HTML-aware)
// This matches: <i ...>##- Please type your reply above this line -##</i> and EVERYTHING after it
$message = preg_replace(
'/<i[^>]*>##-\s*Please\s+type\s+your\s+reply\s+above\s+this\s+line\s*-##<\/i>.*$/is',
'',
$message
);
// 2) Clean up the remaining message
// Remove DOCTYPE and meta tags
$message = preg_replace('/<!DOCTYPE[^>]*>/i', '', $message);
$message = preg_replace('/<meta[^>]*>/i', '', $message);
// Remove <html>, <head>, <body> and their closing tags
$message = preg_replace('/<\/?(html|head|body)[^>]*>/i', '', $message);
// Trim leading/trailing whitespace
$message = trim($message);
// Normalize line breaks to spaces
$message = preg_replace('/\r\n|\r|\n/', ' ', $message);
// Convert to <br> for HTML display
$message = nl2br($message);
// 3) Final wrapper
$message = "<i>Email from: $from_email at $date:-</i><br><br><div style='line-height:1.5;'>$message</div>";
$ticket_number_esc = intval($ticket_number);
$message_esc = mysqli_real_escape_string($mysqli, $message);
$from_email_esc = mysqli_real_escape_string($mysqli, $from_email);
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT ticket_id, ticket_subject, ticket_status, ticket_contact_id, ticket_client_id, contact_email, client_name
FROM tickets
LEFT JOIN contacts on tickets.ticket_contact_id = contacts.contact_id
LEFT JOIN clients on tickets.ticket_client_id = clients.client_id
WHERE ticket_number = $ticket_number_esc LIMIT 1"));
if ($row) {
$ticket_id = intval($row['ticket_id']);
$ticket_subject = sanitizeInput($row['ticket_subject']);
$ticket_status = sanitizeInput($row['ticket_status']);
$ticket_reply_contact = intval($row['ticket_contact_id']);
$ticket_contact_email = sanitizeInput($row['contact_email']);
$client_id = intval($row['ticket_client_id']);
if ($client_id) {
$client_uri = "&client_id=$client_id";
} else {
$client_uri = '';
}
$client_name = sanitizeInput($row['client_name']);
if ($ticket_status == 5) {
$config_ticket_prefix_esc = mysqli_real_escape_string($mysqli, $config_ticket_prefix);
$ticket_number_esc2 = mysqli_real_escape_string($mysqli, $ticket_number);
appNotify("Ticket", "Email parser: $from_email attempted to re-open ticket $config_ticket_prefix_esc$ticket_number_esc2 (ID $ticket_id) - check inbox manually to see email", "/agent/ticket.php?ticket_id=$ticket_id$client_uri", $client_id);
$email_subject = "Action required: This ticket is already closed";
$email_body = "Hi there, <br><br>You've tried to reply to a ticket that is closed - we won't see your response. <br><br>Please raise a new ticket by sending a new e-mail to our support address below. <br><br>--<br>$company_name - Support<br>$config_ticket_from_email<br>$company_phone";
$data = [
[
'from' => $config_ticket_from_email,
'from_name' => $config_ticket_from_name,
'recipient' => $from_email,
'recipient_name' => $from_email,
'subject' => $email_subject,
'body' => mysqli_real_escape_string($mysqli, $email_body)
]
];
addToMailQueue($data);
return true;
}
if (empty($ticket_contact_email) || $ticket_contact_email !== $from_email) {
$from_email_esc2 = mysqli_real_escape_string($mysqli, $from_email);
$row2 = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT contact_id FROM contacts WHERE contact_email = '$from_email_esc2' AND contact_client_id = $client_id LIMIT 1"));
if ($row2) {
$ticket_reply_contact = intval($row2['contact_id']);
} else {
$ticket_reply_type = 'Internal';
$ticket_reply_contact = '0';
$message = "<b>WARNING: Contact email mismatch</b><br>$message";
$message_esc = mysqli_real_escape_string($mysqli, $message);
}
}
mysqli_query($mysqli, "INSERT INTO ticket_replies SET ticket_reply = '$message_esc', ticket_reply_type = '$ticket_reply_type', ticket_reply_time_worked = '00:00:00', ticket_reply_by = $ticket_reply_contact, ticket_reply_ticket_id = $ticket_id");
$reply_id = mysqli_insert_id($mysqli);
$ticket_dir = "../uploads/tickets/" . $ticket_id . "/";
mkdirMissing($ticket_dir);
foreach ($attachments as $attachment) {
$att_name = $attachment['name'];
$att_extension = strtolower(pathinfo($att_name, PATHINFO_EXTENSION));
if (in_array($att_extension, $allowed_extensions)) {
$att_saved_filename = md5(uniqid(rand(), true)) . '.' . $att_extension;
$att_saved_path = $ticket_dir . $att_saved_filename;
file_put_contents($att_saved_path, $attachment['content']);
$ticket_attachment_name = sanitizeInput($att_name);
$ticket_attachment_reference_name = sanitizeInput($att_saved_filename);
$ticket_attachment_name_esc = mysqli_real_escape_string($mysqli, $ticket_attachment_name);
$ticket_attachment_reference_name_esc = mysqli_real_escape_string($mysqli, $ticket_attachment_reference_name);
mysqli_query($mysqli, "INSERT INTO ticket_attachments SET ticket_attachment_name = '$ticket_attachment_name_esc', ticket_attachment_reference_name = '$ticket_attachment_reference_name_esc', ticket_attachment_reply_id = $reply_id, ticket_attachment_ticket_id = $ticket_id");
} else {
$ticket_attachment_name_esc = mysqli_real_escape_string($mysqli, $att_name);
logAction("Ticket", "Edit", "Email parser: Blocked attachment $ticket_attachment_name_esc from Client contact $from_email_esc for ticket $config_ticket_prefix$ticket_number_esc", $client_id, $ticket_id);
}
}
$ticket_assigned_to_sql = mysqli_query($mysqli, "SELECT ticket_assigned_to FROM tickets WHERE ticket_id = $ticket_id LIMIT 1");
if ($ticket_assigned_to_sql) {
$row3 = mysqli_fetch_assoc($ticket_assigned_to_sql);
$ticket_assigned_to = intval($row3['ticket_assigned_to']);
if ($ticket_assigned_to) {
$tech_sql = mysqli_query($mysqli, "SELECT user_email, user_name FROM users WHERE user_id = $ticket_assigned_to LIMIT 1");
$tech_row = mysqli_fetch_assoc($tech_sql);
$tech_email = sanitizeInput($tech_row['user_email']);
$tech_name = sanitizeInput($tech_row['user_name']);
$email_subject = "$config_app_name - Ticket updated - [$config_ticket_prefix$ticket_number] $ticket_subject";
$email_body = "Hello $tech_name,<br><br>A new reply has been added to the below ticket.<br><br>Client: $client_name<br>Ticket: $config_ticket_prefix$ticket_number<br>Subject: $ticket_subject<br>Link: https://$config_base_url/agent/ticket.php?ticket_id=$ticket_id$client_uri<br><br>--------------------------------<br>$message_esc";
$data = [
[
'from' => $config_ticket_from_email,
'from_name' => $config_ticket_from_name,
'recipient' => $tech_email,
'recipient_name' => $tech_name,
'subject' => mysqli_real_escape_string($mysqli, $email_subject),
'body' => mysqli_real_escape_string($mysqli, $email_body)
]
];
addToMailQueue($data);
}
}
mysqli_query($mysqli, "UPDATE tickets SET ticket_status = 2, ticket_resolved_at = NULL WHERE ticket_id = $ticket_id AND ticket_client_id = $client_id LIMIT 1");
logAction("Ticket", "Edit", "Email parser: Client contact $from_email_esc updated ticket $config_ticket_prefix$ticket_number_esc ($subject)", $client_id, $ticket_id);
customAction('ticket_reply_client', $ticket_id);
return true;
} else {
return false;
}
}
/** ------------------------------------------------------------------
* OAuth helpers + provider guard
* ------------------------------------------------------------------ */
// returns true if expires_at ('Y-m-d H:i:s') is in the past (or missing)
function tokenExpired(?string $expires_at): bool {
if (empty($expires_at)) return true;
$ts = strtotime($expires_at);
if ($ts === false) return true;
// refresh a little early (60s) to avoid race
return ($ts - 60) <= time();
}
// very small form-encoded POST helper using curl
function httpFormPost(string $url, array $fields): array {
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($fields, '', '&'));
curl_setopt($ch, CURLOPT_TIMEOUT, 20);
$raw = curl_exec($ch);
$err = curl_error($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return ['ok' => ($raw !== false && $code >= 200 && $code < 300), 'body' => $raw, 'code' => $code, 'err' => $err];
}
/**
* Get a valid access token for Google Workspace IMAP via refresh token if needed.
* Uses settings: config_mail_oauth_client_id / _client_secret / _refresh_token / _access_token / _access_token_expires_at
* Updates globals if refreshed (so later logging can reflect it if you want to persist).
*/
function getGoogleAccessToken(string $username): ?string {
// pull from global settings variables you already load
global $mysqli,
$config_mail_oauth_client_id,
$config_mail_oauth_client_secret,
$config_mail_oauth_refresh_token,
$config_mail_oauth_access_token,
$config_mail_oauth_access_token_expires_at;
// If we have a not-expired token, use it
if (!empty($config_mail_oauth_access_token) && !tokenExpired($config_mail_oauth_access_token_expires_at)) {
return $config_mail_oauth_access_token;
}
// Need to refresh?
if (empty($config_mail_oauth_client_id) || empty($config_mail_oauth_client_secret) || empty($config_mail_oauth_refresh_token)) {
// Nothing we can do
return null;
}
$resp = httpFormPost(
'https://oauth2.googleapis.com/token',
[
'client_id' => $config_mail_oauth_client_id,
'client_secret' => $config_mail_oauth_client_secret,
'refresh_token' => $config_mail_oauth_refresh_token,
'grant_type' => 'refresh_token',
]
);
if (!$resp['ok']) return null;
$json = json_decode($resp['body'], true);
if (!is_array($json) || empty($json['access_token'])) return null;
// Calculate new expiry
$expires_at = date('Y-m-d H:i:s', time() + (int)($json['expires_in'] ?? 3600));
// Update in-memory globals (and persist to DB)
$config_mail_oauth_access_token = $json['access_token'];
$config_mail_oauth_access_token_expires_at = $expires_at;
$at_esc = mysqli_real_escape_string($mysqli, $config_mail_oauth_access_token);
$exp_esc = mysqli_real_escape_string($mysqli, $config_mail_oauth_access_token_expires_at);
mysqli_query($mysqli, "UPDATE settings SET
config_mail_oauth_access_token = '{$at_esc}',
config_mail_oauth_access_token_expires_at = '{$exp_esc}'
WHERE company_id = 1
");
return $config_mail_oauth_access_token;
}
/**
* Get a valid access token for Microsoft 365 IMAP via refresh token if needed.
* Uses settings: config_mail_oauth_client_id / _client_secret / _tenant_id / _refresh_token / _access_token / _access_token_expires_at
*/
function getMicrosoftAccessToken(string $username): ?string {
global $mysqli,
$config_mail_oauth_client_id,
$config_mail_oauth_client_secret,
$config_mail_oauth_tenant_id,
$config_mail_oauth_refresh_token,
$config_mail_oauth_access_token,
$config_mail_oauth_access_token_expires_at;
if (!empty($config_mail_oauth_access_token) && !tokenExpired($config_mail_oauth_access_token_expires_at)) {
return $config_mail_oauth_access_token;
}
if (empty($config_mail_oauth_client_id) || empty($config_mail_oauth_client_secret) || empty($config_mail_oauth_refresh_token) || empty($config_mail_oauth_tenant_id)) {
return null;
}
$url = "https://login.microsoftonline.com/".rawurlencode($config_mail_oauth_tenant_id)."/oauth2/v2.0/token";
$resp = httpFormPost($url, [
'client_id' => $config_mail_oauth_client_id,
'client_secret' => $config_mail_oauth_client_secret,
'refresh_token' => $config_mail_oauth_refresh_token,
'grant_type' => 'refresh_token',
// IMAP/SMTP scopes typically included at initial consent; not needed for refresh
]);
if (!$resp['ok']) return null;
$json = json_decode($resp['body'], true);
if (!is_array($json) || empty($json['access_token'])) return null;
$expires_at = date('Y-m-d H:i:s', time() + (int)($json['expires_in'] ?? 3600));
$config_mail_oauth_access_token = $json['access_token'];
$config_mail_oauth_access_token_expires_at = $expires_at;
$at_esc = mysqli_real_escape_string($mysqli, $config_mail_oauth_access_token);
$exp_esc = mysqli_real_escape_string($mysqli, $config_mail_oauth_access_token_expires_at);
mysqli_query($mysqli, "UPDATE settings SET
config_mail_oauth_access_token = '{$at_esc}',
config_mail_oauth_access_token_expires_at = '{$exp_esc}'
WHERE company_id = 1
");
return $config_mail_oauth_access_token;
}
// Provider from settings (may be NULL/empty to disable IMAP polling)
$imap_provider = $config_imap_provider ?? '';
if ($imap_provider === null) $imap_provider = '';
if ($imap_provider === '') {
// IMAP disabled by admin: exit cleanly
logApp("Cron-Email-Parser", "info", "IMAP polling skipped: provider not configured.");
@unlink($lock_file_path);
exit(0);
}
/** ------------------------------------------------------------------
* Webklex IMAP setup (supports Standard / Google OAuth / Microsoft OAuth)
* ------------------------------------------------------------------ */
use Webklex\PHPIMAP\ClientManager;
$validate_cert = true;
// Defaults from settings (standard IMAP)
$host = $config_imap_host;
$port = (int)$config_imap_port;
$encr = !empty($config_imap_encryption) ? $config_imap_encryption : 'notls'; // 'ssl'|'tls'|'notls'
$user = $config_imap_username;
$pass = $config_imap_password;
$auth = null; // 'oauth' for OAuth providers
if ($imap_provider === 'google_oauth') {
$host = 'imap.gmail.com';
$port = 993;
$encr = 'ssl';
$auth = 'oauth';
$pass = getGoogleAccessToken($user);
if (empty($pass)) {
logApp("Cron-Email-Parser", "error", "Google OAuth: no usable access token (check refresh token/client credentials).");
@unlink($lock_file_path);
exit(1);
}
} elseif ($imap_provider === 'microsoft_oauth') {
$host = 'outlook.office365.com';
$port = 993;
$encr = 'ssl';
$auth = 'oauth';
$pass = getMicrosoftAccessToken($user);
if (empty($pass)) {
logApp("Cron-Email-Parser", "error", "Microsoft OAuth: no usable access token (check refresh token/client credentials/tenant).");
@unlink($lock_file_path);
exit(1);
}
} else {
// standard_imap (username/password)
if (empty($host) || empty($port) || empty($user)) {
logApp("Cron-Email-Parser", "error", "Standard IMAP: missing host/port/username.");
@unlink($lock_file_path);
exit(1);
}
}
$cm = new ClientManager();
$client = $cm->make(array_filter([
'host' => $host,
'port' => $port,
'encryption' => $encr, // 'ssl' | 'tls' | null
'validate_cert' => (bool)$validate_cert,
'username' => $user, // full mailbox address (OAuth uses user as principal)
'password' => $pass, // access token when $auth === 'oauth'
'authentication' => $auth, // 'oauth' or null
'protocol' => 'imap',
]));
try {
$client->connect();
} catch (\Throwable $e) {
echo "Error connecting to IMAP server: " . $e->getMessage();
@unlink($lock_file_path);
exit(1);
}
$inbox = $client->getFolderByPath('INBOX');
$targetFolderPath = 'ITFlow';
try {
$targetFolder = $client->getFolderByPath($targetFolderPath);
} catch (\Throwable $e) {
$client->createFolder($targetFolderPath);
$targetFolder = $client->getFolderByPath($targetFolderPath);
}
// Fetch unseen messages
$messages = $inbox->messages()->leaveUnread()->unseen()->get();
// Counters
$processed_count = 0;
$unprocessed_count = 0;
// Process messages
foreach ($messages as $message) {
$email_processed = false;
// Save original message as .eml (getRawMessage() doesn't seem to work properly)
mkdirMissing('../uploads/tmp/');
$original_message_file = "processed-eml-" . randomString(200) . ".eml";
$raw_message = (string)$message->getHeader()->raw . "\r\n\r\n" . ($message->getRawBody() ?? $message->getHTMLBody() ?? $message->getTextBody());
file_put_contents("../uploads/tmp/{$original_message_file}", $raw_message);
// From
$from_col = $message->getFrom();
$from_first = ($from_col && $from_col->count()) ? $from_col->first() : null;
$from_email = sanitizeInput($from_first->mail ?? 'itflow-guest@example.com');
$from_name = sanitizeInput($from_first->personal ?? 'Unknown');
$from_domain = explode("@", $from_email);
$from_domain = sanitizeInput(end($from_domain));
// Subject
$subject = sanitizeInput((string)$message->getSubject() ?: 'No Subject');
// CC
$ccs = array();
$cc_attr = $message->header->cc;
$cc_list = $cc_attr->toArray();
foreach ($cc_list as $cc_addr) {
if ($cc_addr instanceof \Webklex\PHPIMAP\Address) {
$ccs[] = $cc_addr->mail;
}
}
// Date (string)
$dateAttr = $message->getDate(); // Attribute
$dateRaw = $dateAttr ? (string)$dateAttr : ''; // e.g. "Tue, 10 Sep 2025 13:22:05 +0000"
$ts = $dateRaw ? strtotime($dateRaw) : false;
$date = sanitizeInput($ts !== false ? date('Y-m-d H:i:s', $ts) : date('Y-m-d H:i:s'));
// Body (prefer HTML)
$message_body_html = $message->getHTMLBody();
$message_body_text = $message->getTextBody();
$message_body_raw = $message->getRawBody();
if (!empty($message_body_html)) {
$message_body = $message_body_html;
} elseif (!empty($message_body_text)) {
$message_body = nl2br(htmlspecialchars($message_body_text));
} else {
// Final fallback
$message_body = nl2br(htmlspecialchars($message_body_raw));
}
// Handle attachments (inline vs regular)
$attachments = [];
foreach ($message->getAttachments() as $att) {
$attrs = $att->getAttributes(); // v6.2: canonical source
$dispo = strtolower((string)($attrs['disposition'] ?? ''));
$cid = $attrs['id'] ?? null; // Content-ID
$content = $attrs['content'] ?? null; // binary
$mime = $att->getMimeType();
$name = $att->getName() ?: 'attachment';
$is_inline = false;
if ($dispo === 'inline' && $cid && $content !== null) {
$cid_trim = trim($cid, '<>');
$dataUri = "data:$mime;base64,".base64_encode($content);
$message_body = str_replace(["cid:$cid_trim", "cid:$cid"], $dataUri, $message_body);
$is_inline = true;
}
if (!$is_inline && $content !== null) {
$attachments[] = ['name' => $name, 'content' => $content];
}
}
// 1. Reply to existing ticket with the number in subject
if (preg_match("/\[$config_ticket_prefix(\d+)\]/", $subject, $ticket_number_matches)) {
$ticket_number = intval($ticket_number_matches[1]);
$email_processed = addReply($from_email, $date, $subject, $ticket_number, $message_body, $attachments);
}
// 2. Fuzzy duplicate check using a known contact/domain and similar_text subject
if (!$email_processed && strlen(trim($subject)) > 10) {
$contact_id = 0;
$client_id = 0;
// First: check if sender is a registered contact
$from_email_esc = mysqli_real_escape_string($mysqli, $from_email);
$contact_sql = mysqli_query($mysqli, "SELECT * FROM contacts WHERE contact_email = '$from_email_esc' AND contact_archived_at IS NULL LIMIT 1");
$contact_row = mysqli_fetch_assoc($contact_sql);
if ($contact_row) {
$contact_id = intval($contact_row['contact_id']);
$client_id = intval($contact_row['contact_client_id']);
} else {
// Else: check if sender domain is registered
$from_domain_esc = mysqli_real_escape_string($mysqli, $from_domain);
$domain_sql = mysqli_query($mysqli, "SELECT * FROM domains WHERE domain_name = '$from_domain_esc' AND domain_archived_at IS NULL LIMIT 1");
$domain_row = mysqli_fetch_assoc($domain_sql);
if ($domain_row && $from_domain == $domain_row['domain_name']) {
$client_id = intval($domain_row['domain_client_id']);
}
}
// If we found either a contact or a domain, check recent tickets for a matching subject
if ($client_id) {
$recent_tickets_sql = mysqli_query($mysqli,
"SELECT ticket_id, ticket_number, ticket_subject
FROM tickets
WHERE ticket_client_id = $client_id AND ticket_resolved_at IS NULL
AND ticket_created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)"
);
while ($rowt = mysqli_fetch_assoc($recent_tickets_sql)) {
$ticket_number = intval($rowt['ticket_number']);
$existing_subject = $rowt['ticket_subject'];
// Calculate similarity percentage
similar_text(strtolower($subject), strtolower($existing_subject), $percent);
if ($percent >= 95) {
// Treat as a reply/duplicate
$email_processed = addReply($from_email, $date, $subject, $ticket_number, $message_body, $attachments);
break;
}
}
}
}
// 3. A known, registered contact?
if (!$email_processed) {
$from_email_esc = mysqli_real_escape_string($mysqli, $from_email);
$any_contact_sql = mysqli_query($mysqli, "SELECT * FROM contacts WHERE contact_email = '$from_email_esc' AND contact_archived_at IS NULL LIMIT 1");
$rowc = mysqli_fetch_assoc($any_contact_sql);
if ($rowc) {
$contact_name = sanitizeInput($rowc['contact_name']);
$contact_id = intval($rowc['contact_id']);
$contact_email = sanitizeInput($rowc['contact_email']);
$client_id = intval($rowc['contact_client_id']);
$email_processed = addTicket($contact_id, $contact_name, $contact_email, $client_id, $date, $subject, $message_body, $attachments, $original_message_file, $ccs);
}
}
// 4. A known domain?
if (!$email_processed) {
$from_domain_esc = mysqli_real_escape_string($mysqli, $from_domain);
$domain_sql = mysqli_query($mysqli, "SELECT * FROM domains WHERE domain_name = '$from_domain_esc' AND domain_archived_at IS NULL LIMIT 1");
$rowd = mysqli_fetch_assoc($domain_sql);
if ($rowd && $from_domain == $rowd['domain_name']) {
$client_id = intval($rowd['domain_client_id']);
// Create a new contact
$contact_name = $from_name;
$contact_email = $from_email;
mysqli_query($mysqli, "INSERT INTO contacts SET contact_name = '".mysqli_real_escape_string($mysqli, $contact_name)."', contact_email = '".mysqli_real_escape_string($mysqli, $contact_email)."', contact_notes = 'Added automatically via email parsing.', contact_client_id = $client_id");
$contact_id = mysqli_insert_id($mysqli);
logAction("Contact", "Create", "Email parser: created contact " . mysqli_real_escape_string($mysqli, $contact_name), $client_id, $contact_id);
customAction('contact_create', $contact_id);
$email_processed = addTicket($contact_id, $contact_name, $contact_email, $client_id, $date, $subject, $message_body, $attachments, $original_message_file, $ccs);
}
}
// 5. Unknown sender allowed?
if (!$email_processed && $config_ticket_email_parse_unknown_senders) {
$bad_from_pattern = "/daemon|postmaster|bounce|mta/i"; // Stop NDRs with bad subjects raising new tickets
if (!preg_match($bad_from_pattern, $from_email)) {
$email_processed = addTicket(0, $from_name, $from_email, 0, $date, $subject, $message_body, $attachments, $original_message_file, $ccs);
} else {
// Probably an NDR message without a ticket ref in the subject
$failed_recipient = null;
$diagnostic_code = null;
$status_code = null;
$original_subject = null;
$original_to = null;
// Webklex stores DSN info in attachments, not parts
foreach ($message->getAttachments() as $attachment) {
$ctype = strtolower($attachment->getContentType());
$body = $attachment->getContent() ?? '';
// 1. Delivery status block
if (strpos($ctype, 'delivery-status') !== false) {
if (preg_match('/Final-Recipient:\s*rfc822;\s*(.+)/i', $body, $m)) {
$failed_recipient = sanitizeInput(trim($m[1]));
}
if (preg_match('/Diagnostic-Code:\s*(.+)/i', $body, $m)) {
$diagnostic_code = sanitizeInput(trim($m[1]));
}
if (preg_match('/Status:\s*([0-9\.]+)/i', $body, $m)) {
$status_code = sanitizeInput(trim($m[1]));
}
}
// 2. Original message headers
if (strpos($ctype, 'message/rfc822') !== false) {
if (preg_match('/^To:\s*(.+)$/mi', $body, $m)) {
$original_to = sanitizeInput(trim($m[1]));
}
if (preg_match('/^Subject:\s*(.+)$/mi', $body, $m)) {
$original_subject = sanitizeInput(trim($m[1]));
}
}
}
// 3. Fallback: extract diagnostic from human-readable text/plain
if (!$diagnostic_code) {
$text = $message->getTextBody() ?? '';
// Exim puts diagnostics on an indented line
if (preg_match('/\n\s{2,}(.+)/', $text, $m)) {
$diagnostic_code = sanitizeInput(trim($m[1]));
}
}
// Fallbacks
$failed_recipient = $failed_recipient ?: 'unknown recipient';
$diagnostic_code = $diagnostic_code ?: 'unknown diagnostic code';
$status_code = $status_code ?: 'unknown status code';
$original_subject = $original_subject ?: $subject;
appNotify(
"Ticket",
"Email parser NDR: Message to $failed_recipient bounced. Subject: $original_subject Diagnostics: $status_code / $diagnostic_code - check ITFlow folder manually to see email",
"",
0
);
// If the original subject has a ticket, add the NDR there too
if (preg_match("/\[$config_ticket_prefix(\d+)\]/", $original_subject, $ticket_number_matches)) {
$ticket_number = intval($ticket_number_matches[1]);
// Craft a clean bounce message
$reply_body = "Email delivery failed.\n".
"Recipient: $failed_recipient\n".
"Status: $status_code\n".
"Diagnostic: $diagnostic_code\n";
// No attachments
addReply(
$from_email,
$date,
$original_subject,
$ticket_number,
$reply_body,
[]
);
}
$email_processed = true;
}
}
// Flag/move based on processing result
if ($email_processed) {
$processed_count++; // increment first so a move failure doesn't hide the success
try {
$message->setFlag('Seen');
// Move using the Folder object (top-level "ITFlow")
$message->move($targetFolderPath);
// optional: logApp("Cron-Email-Parser", "info", "Moved message to ITFlow");
} catch (\Throwable $e) {
// >>> Put the extra logging RIGHT HERE
$subj = (string)$message->getSubject();
$uid = method_exists($message, 'getUid') ? $message->getUid() : 'n/a';
$path = (is_object($targetFolder) && property_exists($targetFolder, 'path')) ? (string)$targetFolder->path : $targetFolderPath;
logApp(
"Cron-Email-Parser",
"warning",
"Move failed (subject=\"$subj\", uid=$uid) to [$path]: ".$e->getMessage()
);
}
} else {
$unprocessed_count++;
try {
$message->setFlag('Flagged');
$message->unsetFlag('Seen');
} catch (\Throwable $e) {
logApp("Cron-Email-Parser", "warning", "Flag update failed: ".$e->getMessage());
}
}
// Cleanup temp .eml if still present (e.g., reply path)
if (isset($original_message_file)) {
$tmp_path = "../uploads/tmp/{$original_message_file}";
if (file_exists($tmp_path)) { @unlink($tmp_path); }
}
}
// Expunge & disconnect
try {
$client->expunge();
} catch (\Throwable $e) {
// ignore
}
$client->disconnect();
// Execution timing (optional)
$script_end_time = microtime(true);
$execution_time = $script_end_time - $script_start_time;
$execution_time_formatted = number_format($execution_time, 2);
$processed_info = "Processed: $processed_count email(s), Unprocessed: $unprocessed_count email(s)";
// logAction("Cron-Email-Parser", "Execution", "Cron Email Parser executed in $execution_time_formatted seconds. $processed_info");
// Remove the lock file
unlink($lock_file_path);
// DEBUG
echo "\nLock File Path: $lock_file_path\n";
if (file_exists($lock_file_path)) {
echo "\nLock is present\n\n";
}
echo "Processed Emails: $processed_count\n";
echo "Unprocessed Emails: $unprocessed_count\n";

Some files were not shown because too many files have changed in this diff Show More