722 Commits

Author SHA1 Message Date
johnnyq
d5ee0bff74 update_cli.php now does both db and forces app updates all in one go no more switches 2026-08-15 15:25:14 -04:00
johnnyq
77319f8fc1 update_cli.php now updates both the app and all database updates in one go and the switches --update only updates the app --db_update only updates the db 2026-08-15 15:12:45 -04:00
johnnyq
bdab434707 Fix Margins in various areas 2026-08-15 13:34:38 -04:00
johnnyq
56d9e04eac Update Theme select to use new bootstrap 5 radio button 2026-08-15 13:17:32 -04:00
johnnyq
72c037c581 Fix Date Range FlatPickr defaulting to 1970 for All time now default to current month 2026-08-15 13:04:55 -04:00
johnnyq
2375e69f2d Update badge classes to bg 2026-08-15 13:00:41 -04:00
johnnyq
73e10e72f2 Update filter bar, increase height on top navbar, primary buttons now inherit theme color 2026-08-15 12:55:24 -04:00
johnnyq
c44fa30792 Fix Mail settings tabs regression and user menu coloring 2026-08-15 12:29:39 -04:00
johnnyq
8c0e5246b4 Fix Card Margin bottoms client header and ticket cards on the right 2026-08-14 19:08:15 -04:00
johnnyq
35239b563d Resize top nav and compoents 2026-08-14 18:53:20 -04:00
johnnyq
e2512b0f86 Fixed Side nav titles, Fixed ajax on bootstrap modals, fixed pagination all regressions 2026-08-14 18:32:33 -04:00
johnnyq
1c5803dc12 Removed dead libs pdfmake dropzone and popper, fixed missing label for ids and migrated datatables to use bootstrap 5 libs instead of of 4 2026-08-14 17:50:17 -04:00
johnnyq
6fc94d9733 Migrated confirm modal to sweetalert2 lib 2026-08-14 17:38:21 -04:00
johnnyq
bbbfff7413 All Jquery removed replaced with stand JS and non jqery dependent libs 2026-08-14 17:28:06 -04:00
johnnyq
2354dd1511 Migrate fully away from Jquery Stage 1 2026-08-14 17:11:01 -04:00
johnnyq
dc6b191eed Update Font, Migrated away from JQUERY dependent libs: toastr to bootstrap5 toasts, daterangepicker to flatpickr, select2 to Tom Select, InputMask to IMask 2026-08-14 17:08:40 -04:00
johnnyq
0fe6d51bdb Theme Fixes for adminlte 4 2026-08-14 14:11:13 -04:00
johnnyq
e6da3fd0e3 Remove script 2026-08-14 13:22:47 -04:00
johnnyq
a084e37c39 Initial migration from AdminLTE 3 to 4, bootstrap 4 to 5 2026-08-14 13:22:11 -04:00
wrongecho
691059aab2 Debug - bump php memory flag to 512M 2026-08-13 09:45:01 +01:00
wrongecho
eda32f7d57 Update security 2026-08-12 15:27:40 +01:00
wrongecho
c44ae74557 Update readme 2026-08-12 15:20:48 +01:00
johnnyq
41900191af Bump ITFlow version and update Changelog 2026-08-10 11:33:38 -04:00
johnnyq
076f45abb0 Remove Stale SMTP IMAP host if switching Standard SMTP.IMAP to OAUTH 2026-08-09 23:59:54 -04:00
johnnyq
f7548af234 Fix Export Missing coluns due to the select star sweep 2026-08-09 14:33:06 -04:00
johnnyq
de46184a5b Fix calendar queries missing columns lost in the SELECT * sweep 2026-08-09 13:41:01 -04:00
johnnyq
f237993fd7 DB Structure update for AI temperature 2026-08-09 13:29:46 -04:00
johnnyq
a96324628a Fix broken cron page due to the select * sweep, note thi sdid not break cron itself 2026-08-09 13:27:51 -04:00
johnnyq
b903ee02fe Bump version 2026-08-08 13:15:24 -04:00
johnnyq
6ff454134b Update changelog 2026-08-08 13:14:25 -04:00
johnnyq
04991a0d79 Fix ticket schedule cancellation never cancelling the calendar event 2026-08-08 13:00:01 -04:00
johnnyq
46a63b191c Fix bulk recurring ticket priority change denying access to all non-admin users 2026-08-08 12:59:31 -04:00
johnnyq
6b65c8150f Fix contact delete leaving an orphaned portal user and anonymize not redacting the phone number 2026-08-08 12:58:46 -04:00
johnnyq
d484629ebc do not update ticket status in history if it is the same 2026-08-08 12:41:34 -04:00
johnnyq
656aa95a4e Fix WebUI setup regression locking out after first step 2026-08-07 18:09:01 -04:00
johnnyq
db96766ab0 Fix Error on Scheduling Tickets, Fixed Zapcal Path 2026-08-07 12:44:56 -04:00
johnnyq
10f0c9de58 remove audit system time script 2026-08-06 19:26:43 -04:00
johnnyq
ac598f6379 Fix system-generated ticket replies booking fake time worked 2026-08-06 19:25:39 -04:00
johnnyq
605c7811e4 Fix AI 2026-08-06 19:18:34 -04:00
johnnyq
942fe41b18 Apply further gating to client pdf export 2026-08-06 19:03:47 -04:00
johnnyq
64423cbefa Fix Client PDF Export 2026-08-06 18:51:09 -04:00
johnnyq
3cae52067a Fix calendar event Delete bug 2026-08-06 14:21:57 -04:00
johnnyq
abb724b40b Removed Orphaned vars 2026-08-06 14:03:44 -04:00
johnnyq
f8899e592f More Select * Queries convert 2026-08-06 13:57:46 -04:00
johnnyq
108781db5b Move more select * to column select for further optimization 2026-08-06 13:44:02 -04:00
johnnyq
b3744e9ed5 More sweeps to replace select * with actual returned column names dramatically reduces php memory usage and speeds up processing dramatically especially in the crons since they run all the time 2026-08-06 13:10:23 -04:00
johnnyq
b8c9d5b4cf Query Optimization: Select only needed columns instead of SELECT * on looped queries 2026-08-06 12:59:43 -04:00
johnnyq
77defcf7d8 Global Search Query Optimization only returns columns needed 2026-08-06 12:30:27 -04:00
johnnyq
9324d0822e Drop more dead client left joins big saver is the badge counts 2026-08-06 12:14:43 -04:00
johnnyq
eb2cc3d653 Update Contributing with the new clientScopeSql function 2026-08-06 11:55:39 -04:00
johnnyq
ddd9c2b56a clientScopeSql sweep whicn now removed the extra unessesary client joins 2026-08-06 11:45:56 -04:00
johnnyq
83383599a1 Add clientScopeSql helper for client-scoped list queries 2026-08-06 11:25:11 -04:00
johnnyq
49a3c2d266 Enforce client scoping on API expense reads and record updates
Fixes (expenses/read.php missing apiClientScopeSql), plus an unscoped UPDATE in contacts/update.php and follow-up writes firing on zero-row updates in assets/credentials.
2026-08-06 11:00:49 -04:00
johnnyq
cbf8922f5b Update Changelog 2026-08-04 15:13:57 -04:00
johnnyq
b39f3c0b23 Redesing task bar and remove redundant task count in ticket details 2026-08-04 12:40:47 -04:00
johnnyq
f9ae7598ec Bump FullCalendar from 7.0.0 to 7.0.2 2026-08-04 12:39:04 -04:00
johnnyq
261bdab61a Gate SLA option in ticket details its gated everywhere else 2026-08-04 12:14:56 -04:00
johnnyq
1b386567fa Bump DataTables from 2.3.7 to 3.0.1 2026-08-04 12:12:42 -04:00
johnnyq
5eba64afad Bump TinyMCE to 8.8.2 2026-08-04 12:05:23 -04:00
johnnyq
c30c12674a Sync DB Seed data between setup and setup cli 2026-08-03 22:41:31 -04:00
wrongecho
8aaf5b450f Merge branch 'develop' of https://github.com/itflow-org/itflow into develop 2026-08-03 20:55:57 +01:00
wrongecho
d74c9743be Feature: Add invoice_items API endpoint (adding line items to an invoice) 2026-08-03 20:53:53 +01:00
johnnyq
0311a3d056 Fix remaing undefine vars for audits and flash messages 2026-08-03 15:40:44 -04:00
johnnyq
c5288f9e30 Fix undefined variables in expense, asset and contact audit/flash messages 2026-08-03 15:34:18 -04:00
johnnyq
e800064202 Fix Expense Description logging spelling 2026-08-03 15:28:36 -04:00
johnnyq
abd79c0aed Feature: Add Income Bulk Actions (Account, Payment Method and category 2026-08-03 15:23:50 -04:00
wrongecho
a313278623 Merge branch 'develop' of https://github.com/itflow-org/itflow into develop 2026-08-03 18:58:58 +01:00
wrongecho
f87d43881e Add very basic product import via CSV 2026-08-03 18:58:53 +01:00
johnnyq
ea3d6a2fc6 Merge branch 'develop' of github.com:itflow-org/itflow into develop 2026-08-03 13:28:53 -04:00
johnnyq
7a44cdd438 Fix Autofill products in invoice, quotes, recurring invoice, tax field wasnt updating and a dash was being put in front 2026-08-03 13:28:41 -04:00
wrongecho
ad234c5e46 Merge branch 'develop' of https://github.com/itflow-org/itflow into develop 2026-08-03 18:01:47 +01:00
wrongecho
f16e7893ac Bump supported version 2026-08-03 18:01:40 +01:00
johnnyq
3b0d3839ae Merge branch 'develop' of github.com:itflow-org/itflow into develop 2026-08-03 12:55:24 -04:00
johnnyq
ffe32adbd3 Move categories and tag types to a left side nav instead of top nav 2026-08-03 12:54:33 -04:00
wrongecho
5ee1c9580f Enhance .htaccess rules 2026-08-03 17:51:04 +01:00
johnnyq
315768d651 Add Category column and filter into income along with the exports 2026-08-02 18:23:18 -04:00
johnnyq
6b5be0f881 Rework Update UI and fix banch 2026-08-02 18:07:24 -04:00
johnnyq
a27019511e Remove unused overdue invoice setting move master cron switch out of notificaiton and into cron 2026-08-02 17:22:28 -04:00
johnnyq
ad15fabbb3 Credential length guard 2026-08-02 16:30:14 -04:00
johnnyq
e793804203 Migrate credential password from varbinary to varchar and set max length for passwords 2026-08-02 16:18:36 -04:00
johnnyq
89fba247c5 Add some missing maxlength form options to prevent info overflow and result in error 500 2026-08-02 16:08:01 -04:00
johnnyq
3e532fc792 Update imapengine dependancies too 2026-08-02 01:26:40 -04:00
johnnyq
cf4446f405 Bump imapengine from 1.25.3 to 1.25.4 2026-08-02 01:21:29 -04:00
johnnyq
ab41543578 Remove unused DB wrappers 2026-08-01 19:49:22 -04:00
johnnyq
c8dab1a7e5 Revert PAyment cleanInput too 2026-08-01 18:48:07 -04:00
johnnyq
056db263a5 Rever old test run of using prepared mysql statement for client add /edit 2026-08-01 18:20:43 -04:00
johnnyq
6107d9f833 Update Contributing 2026-08-01 15:42:56 -04:00
johnnyq
23a09f54fc Reworked getFieldById to remove escaping and and reworked everything that uses the function to also escape, was causing double escaping in many places this standardizes the function 2026-08-01 15:33:48 -04:00
johnnyq
75028ba09a Fix some Ticket History gaps 2026-08-01 14:33:37 -04:00
johnnyq
cdb4a43b89 Major UI/UX overhaul on Tickets listing / Kanban and ticket details 2026-08-01 14:12:03 -04:00
johnnyq
4fec57709a Recurring Expense: Fix edit not keeping client 2026-08-01 12:09:39 -04:00
johnnyq
abe02d7d8e Fix Wording on changelog 2026-07-31 16:57:19 -04:00
johnnyq
53f45fa008 Fix queued backups never building, per-type retention, and cron connection handling 2026-07-31 16:35:39 -04:00
johnnyq
ae468d6cee Encrypted backups with types, scheduling and CLI restore
Backups are now AES-256 encrypted zips in three types (full, database
only, master key), catalogued in a new backups table, built by cron rather
than the web request, and kept under uploads/backups with retention in the
nightly job. The encryption key is one value per install held in config.php,
never in the database and never in the file name.

Restore is shared by the setup wizard and the new scripts/restore_cli.php,
which is the only path without an upload size limit. It verifies the key and
unpacks the archive before dropping anything, and dumps the current database
first so a failed import is rolled back.

A backup dumps, zips and encrypts for minutes without issuing a query, so on
a server with a short wait_timeout the connection is closed underneath it and
the UPDATE marking the backup complete is what fails - long after the archive
was written correctly. The connection is now held open for the job and
re-established before any write that follows long file work, including the
database phase of a restore. Retention recovers rows a dropped connection
left behind: still Running after six hours becomes Complete if the archive is
on disk, Failed if it is not.

cron.php's own failure path is hardened to match. It recorded job failures
through the same connection the failing job had just killed, so an uncaught
exception ended the dispatch and no trace of the original error survived.
Failures now also echo to stdout, so cron mails something useful when the
database is unreachable.

Security: the setup wizard's restore step is now closed on any install that
has users, whatever config.php says. $config_enable_setup defaulted to
enabled when the flag was absent, and the flag is only written at the end of
a successful install, so an install abandoned partway left an unauthenticated
endpoint that would drop every table, import an attacker-supplied archive,
and overwrite uploads/ including the .htaccess that stops PHP running there.
Affects 26.07 and earlier. Restoring over a live install is now CLI only.
2026-07-31 16:18:20 -04:00
johnnyq
6a5cf6704a Add Indexes to foreign key in ticket replies and attachments fixes slow ticket access issue with many replies with large inline images 2026-07-31 14:16:19 -04:00
johnnyq
16000efcf7 Drop SVG extension in the ticket mail parser 2026-07-31 12:39:10 -04:00
johnnyq
293ba534e2 Simplified Changelog 2026-07-30 19:18:05 -04:00
johnnyq
7c5579677d Cron: daily jobs catch up when the dispatcher is invoked before their scheduled time, so an old single daily crontab entry still runs them 2026-07-30 18:21:47 -04:00
johnnyq
09a74d6d26 Update changelog and Important Upgrade Steps 2026-07-30 18:01:35 -04:00
johnnyq
8436cd6296 Nightly tasks: apply late fees, overdue reminder emails, and autopay retries at most once per invoice per day, and lock nightly to the daily schedule 2026-07-30 17:50:11 -04:00
johnnyq
36c9c030c0 DB Update 2026-07-30 17:07:38 -04:00
johnnyq
0631c45040 Fix Cron Schedule and new directory includes created inside cron for the cron lock 2026-07-30 17:06:07 -04:00
johnnyq
119334ab71 Cron Fix 2026-07-30 16:44:56 -04:00
johnnyq
38e8edea19 Cron runs all jobs from a single dispatcher cron.php which should now be run every minute and all other cron jobs eliminated from cron 2026-07-30 13:48:39 -04:00
johnnyq
1005b73416 DB Structure Update 2026-07-30 02:30:15 -04:00
johnnyq
f2d5ac4a29 Feature: Calendars are now exportable shareable so Third Party Calendar services can read them, also added recoccurence to calendar, and all day along with sperating the time fields and click in a box to add calendar event is possible 2026-07-30 02:18:30 -04:00
johnnyq
3c8f812a16 Feature: On all export modals added Filter Tab and Selectable Columns tab with default selected, also you can now export to a PDF 2026-07-30 00:46:15 -04:00
johnnyq
729d22d19b Move Attachments to Assignment in new ticket get rid of help texts in reply for attachments and update changelog 2026-07-29 18:39:43 -04:00
johnnyq
2395a3ee16 Phase 2 of ticket attachements by agents 2026-07-29 18:10:34 -04:00
johnnyq
7c8c93c01c Feature allow agents to attach files to tickets in the app 2026-07-29 17:58:04 -04:00
johnnyq
e74307eaea Feature: Add Dynamic Task editing in add ticket and add recurring ticket, recurring tickets now have their own task table as well 2026-07-29 17:48:41 -04:00
johnnyq
74a5359b80 Move Watchers into assignment in add ticket 2026-07-29 17:28:57 -04:00
johnnyq
d01024931b Retire v1 Ticket and merge into 1 add ticket 2026-07-29 17:24:55 -04:00
johnnyq
8a092852cf Fixed Asset section in recurring ticket if global added project in add ticket 2026-07-29 16:52:47 -04:00
johnnyq
3296e48ac6 Unlink template from recurring ticket if template is deleted 2026-07-29 16:36:47 -04:00
johnnyq
eaf3f5abd1 Add Badge next to recurring tickets with a template 2026-07-29 16:28:17 -04:00
johnnyq
75d8b4d37c Fix get FieldByID spots 2026-07-29 16:16:32 -04:00
johnnyq
7245ff0650 Fix Cron URL Key on recurring tickets 2026-07-29 16:11:36 -04:00
johnnyq
bc64c9e3d1 Feature: Abiliry to assign Ticket Templates to Recurring Tickets 2026-07-29 15:55:14 -04:00
johnnyq
9c123c42d1 Update Changelog 2026-07-29 14:34:59 -04:00
johnnyq
af9990ff13 Fix Contact Notes and Fix a few broken links 2026-07-29 14:22:18 -04:00
johnnyq
a900b2cd74 Update DB Structure 2026-07-29 14:15:15 -04:00
johnnyq
fc5cdeea82 Add UI Elements for Asset Notes similar to contact notes 2026-07-29 14:13:55 -04:00
johnnyq
bb1f0d5489 SLA Fixes 2026-07-29 13:50:00 -04:00
johnnyq
566d74c15d Fix SLA resolve-time reporting, stale settings cache on save, and report N+1 2026-07-29 13:34:34 -04:00
johnnyq
750c2e58cc Update DB Structure 2026-07-29 13:23:02 -04:00
johnnyq
9533da4053 Repair SLA verdicts missed by kanban/portal resolve; record first response on kanban resolve 2026-07-29 13:21:37 -04:00
johnnyq
cc651c18be DB Structure Update 2026-07-29 13:04:05 -04:00
johnnyq
162a597da9 Add SLA pausing, SLA reports, SLA filtering and kanban SLA state (phase 3)
Statuses can be flagged to pause the resolution clock; sla_history records the intervals a ticket's clock actually ran and the deadline is re-based on remaining budget when it resumes. Adds SLA Summary and SLA by Client reports, an SLA state filter on the ticket list, SLA colouring on kanban cards, and an Urgent column on the Tickets by Client report. DB update 2.5.1. Also fixes resolution SLA verdicts being skipped when resolving via kanban or the client portal.
2026-07-29 13:02:27 -04:00
johnnyq
e76b384606 Add ticket reply API endpoints
GET /api/v1/ticket_replies/read.php and POST create.php, so an RMM or
monitoring system can append to a ticket it did not open.

Replies default to Internal so an integration cannot email a client by
omitting a parameter. Public replies mark first response, notify the
contact and watchers, and fire the same custom actions as the agent
reply handler. An optional ticket_status also sets the status, and
resolves the ticket and marks the resolution SLA when set to 4.

Replies are always joined to tickets so ticket_client_id is checked
against the key user's client scope, and the client_id named on a write
must match the ticket's own client. The reply is attributed to the user
the API key runs as.

read.php resolves ticket_reply_by_name, since that column holds a
user_id on Internal and Public replies but a contact_id on Client ones.
2026-07-29 01:53:18 -04:00
johnnyq
07c73a6a04 Add Urgent Priority, move Per Client SLA Settings to notes tab in client add / edit modal remove unnessesary sla admin setting for client overide 2026-07-29 00:49:31 -04:00
johnnyq
d5ed05017b Allow selecting client SLA assignments on client create/edit 2026-07-29 00:21:13 -04:00
johnnyq
dd45a0f4f1 Update DB Structure 2026-07-29 00:03:08 -04:00
johnnyq
d08ef6184a Add optional ticket SLAs
Response/resolution targets stamped at creation from per-client/priority assignments, business-hours due date math, warn/breach alert stages via cron/ticket_sla.php, ticket list coloring, per-ticket SLA override, admin page for plans/assignments/business hours. DB update 2.5.0. No behavior change unless SLAs are assigned. Bulk reply now only counts Public replies as first response.
2026-07-29 00:00:42 -04:00
johnnyq
78ee38b39f Revert DB Update 2026-07-28 22:08:01 -04:00
johnnyq
1494d2cb5e Backed out of Refunds which still keeps the fix to properly set the invoice status when payment is deleted 2026-07-28 22:03:35 -04:00
johnnyq
31921ee99b Add Refunding capability along with Stripe Refunding 2026-07-28 21:34:06 -04:00
johnnyq
b3f959ac55 Use Short echo tags across the code 2026-07-28 18:47:52 -04:00
johnnyq
0031438bce Update Contrubuting 2026-07-28 18:12:42 -04:00
johnnyq
556ab22c79 Enfoce Sales Permission Read on products export 2026-07-28 17:53:13 -04:00
johnnyq
78e7e1c49e Remove Normalize Scripts 2026-07-28 17:46:39 -04:00
johnnyq
c5ff3e2a3f Normalize line endings to LF; add .gitattributes and .editorconfig 2026-07-28 17:45:52 -04:00
johnnyq
7f7880a4a7 Add Quick Payments view modal in Invoices 2026-07-28 12:49:57 -04:00
johnnyq
5f3a0bec46 Remove old payments and revenues and all ties to the pages as these are combined in income now 2026-07-28 12:38:29 -04:00
johnnyq
c3896ba3d6 Add Export CSV to the new combined income page 2026-07-28 12:26:50 -04:00
johnnyq
7327ebb37c Stop parallel login attempts from bypassing the rate limits 2026-07-28 01:01:43 -04:00
johnnyq
2e855f62c9 Require credential module access to view or share credentials 2026-07-28 00:40:33 -04:00
johnnyq
4c65b8c561 Clear mail bodies after successful delivery 2026-07-28 00:18:07 -04:00
johnnyq
c64c76c577 Rotate session ID on login and fix client portal Entra SSO 2026-07-27 23:49:09 -04:00
johnnyq
22e93589cb Fix share link view accounting and quote response handling 2026-07-27 21:11:45 -04:00
johnnyq
f129311b2e Remove patch and update gitignore 2026-07-27 21:02:13 -04:00
johnnyq
70d165e8bc Claim shared item views atomically and log guest audit IPs 2026-07-27 20:58:37 -04:00
johnnyq
b555a300bc Give the vault session key full entropy and guard empty-key credential writes
generateUserSessionKey() built its AES-128 key with randomString(16), which
draws 12 random bytes and base64url-encodes them into 16 printable characters.
Those 16 characters were handed to openssl_encrypt as the 16-byte key, so the
key that wraps the site master key in the user's session carried 96 bits of
entropy instead of 128. Same for the IV.

The key and IV are now random_bytes(16) and stay raw for the cipher. Base64 is
applied only for transport - the cookie and the session copy of the IV - and
decoded again at the three read sites.

Sessions issued before this change will not decode and are not supported;
anyone logged in at deploy time is prompted to log in again. Nothing is stored
long term in this path, so there is nothing to migrate.

Also guards encryptCredentialEntry against a master key that failed to decrypt.
openssl_encrypt accepts false as a key and silently encrypts under an empty
key, so a session whose cookie had expired could write a credential that no
key would ever recover. It now returns false instead. This was reachable
before this change too, whenever a cookie expired mid-session.
2026-07-27 20:27:23 -04:00
johnnyq
829527244e Fix broken ajax 2026-07-27 20:25:48 -04:00
johnnyq
50352fb779 Revert vault session key entropy change 2026-07-27 20:13:46 -04:00
johnnyq
8dcb45fc9d Remove Patch 2026-07-27 20:00:42 -04:00
johnnyq
b18544c69b Give the vault session key full entropy and guard empty-key credential writes
generateUserSessionKey() built its AES-128 key with randomString(16), which
draws 12 random bytes and base64url-encodes them into 16 printable characters.
Those 16 characters were handed to openssl_encrypt as the 16-byte key, so the
key that wraps the site master key in the user's session carried 96 bits of
entropy instead of 128. Same for the IV.

The key and IV are now random_bytes(16) and stay raw for the cipher. Base64 is
applied only for transport - the cookie and the session copy of the IV - and
decoded again at the three read sites.

Sessions issued before this change will not decode and are not supported;
anyone logged in at deploy time is prompted to log in again. Nothing is stored
long term in this path, so there is nothing to migrate.

Also guards encryptCredentialEntry against a master key that failed to decrypt.
openssl_encrypt accepts false as a key and silently encrypts under an empty
key, so a session whose cookie had expired could write a credential that no
key would ever recover. It now returns false instead. This was reachable
before this change too, whenever a cookie expired mid-session.
2026-07-27 20:00:09 -04:00
johnnyq
8825661360 Dump new DB Structure 2026-07-27 18:38:48 -04:00
johnnyq
a4fdced10f Rate limit 2FA code attempts and narrow the TOTP acceptance window 2026-07-27 18:36:43 -04:00
johnnyq
97490bef3b Security Fix in recurring invoices in cron 2026-07-27 18:07:15 -04:00
johnnyq
a844d7b428 Adds a shared flock guard used by all five cron entry points, keyed per
script and per install. Replaces the mail queue's own lock file, which
was not atomic and could be deleted out from under a long run. Bounds
the SMTP conversation so an unresponsive server cannot hold the lock.
Recovers rows left at Sending by a run that died, which nothing
previously picked up.
2026-07-27 17:35:39 -04:00
johnnyq
686fca99e1 Fix Possible duplicate emails in a race condition in mail queue 2026-07-27 16:59:10 -04:00
johnnyq
775e8f58f6 Fix Cents Calc 2026-07-27 16:55:16 -04:00
johnnyq
409d769eb7 Scope the cron lock per install so instances don't block each other
Report an unopenable cron lock file instead of claiming cron is running
2026-07-27 16:41:21 -04:00
johnnyq
cff416dd87 Remove Patch 2026-07-27 15:22:46 -04:00
johnnyq
65de8c35e6 Prevent duplicate Stripe payment bookings and overlapping cron runs 2026-07-27 15:22:16 -04:00
johnnyq
815802fb7c Gate Admin UI Modals to Admin 2026-07-27 14:50:32 -04:00
johnnyq
53842a50d2 Add URI Tabs to mail settings keeps the tab active upon saving 2026-07-26 01:52:25 -04:00
johnnyq
5fc2f93b38 Allow negative numbers in add expense 2026-07-26 01:40:11 -04:00
johnnyq
32be252261 Remove patch file 2026-07-26 01:11:27 -04:00
johnnyq
04014da21f Combined Payments and Revenues as one View called income keeps all income in one place and still can add revenue not related to invoice 2026-07-26 01:10:52 -04:00
johnnyq
276e84ad0b Expenses: Set Current Date in new expense 2026-07-25 20:38:58 -04:00
johnnyq
cd34236716 Fix saved-payment setup Checkout: embedded_page ui_mode + attach existing Stripe customer 2026-07-25 19:00:06 -04:00
johnnyq
a26ac2c4fc Fixed adding saved credit cards in stipe due to API change 2026-07-25 18:47:13 -04:00
johnnyq
497ea3b669 Centralize client portal access checks through contactCan()
Replaces the duplicated primary/billing/technical checks across portal pages, post.php handlers, nav, and dashboard with contactCan()/enforceContactCan(). Same behavior, but the rules now live in one place instead of being copy-pasted, which is what let them drift before. file.php keeps its 404 response; ticket-visibility and approval-routing checks are intentionally left as-is.
2026-07-25 17:55:30 -04:00
johnnyq
887063394a Prevent client portal contacts from editing their own contact record 2026-07-25 17:28:57 -04:00
johnnyq
04bf3c234f Read API credential decrypt password from body instead of query string
Keeps the credential decryption secret out of the URL, so it no longer lands in web-server access logs, proxy logs, or browser history. credentials/read.php now reads api_key_decrypt_password from the request body, matching create/update. Consumers reading decrypted credentials must send it in the body, not the query string.
2026-07-25 17:08:22 -04:00
johnnyq
e6daf00528 Fix some broken API calls after RBAC update 2026-07-25 16:59:03 -04:00
johnnyq
c509b7f693 Add user-based RBAC for API keys
API keys now run as a user and inherit that user's module, operation, and client permissions. Drops per-key client scoping and removes existing keys (must be recreated). Adds an edit modal to change a key's user.
2026-07-25 16:50:58 -04:00
johnnyq
8d46e2a7db Fix Client Permission overides to include deny allow list in projects prohect and edit ticket 2026-07-25 13:28:47 -04:00
johnnyq
3756561458 Feature: Add Deny Client Access to Client Access Permssions - BREAKS UI, MUST UPDATE DB USING php scripts/cli_update.php --db_update 2026-07-25 13:22:52 -04:00
johnnyq
ff4b69b511 Client Enforcement via function on client edit instad of in sql query 2026-07-25 00:35:47 -04:00
johnnyq
761b2a7af4 Module permissions modal enforcement sweep to match its post counterpart 2026-07-25 00:28:42 -04:00
johnnyq
7a793cbbcc Add more enforceClientAccess to agent ajax gets 2026-07-24 22:51:57 -04:00
johnnyq
113ef4b9a4 Add more enforceClientAccess 2026-07-24 22:49:18 -04:00
johnnyq
3524010d7a Add enforceClientAccess to 4 bulk modals initially missing 2026-07-24 22:06:42 -04:00
johnnyq
8993bce53f Force No Indexes of empty directories even if enabled using .htaccess in uploads 2026-07-24 20:52:35 -04:00
johnnyq
8813b2c066 Use imapEngine lib for testing imap connection 2026-07-24 18:56:16 -04:00
johnnyq
5c4805b822 Neutralize CSV formula injection in generated exports using the new created escapeCsvFormula Function 2026-07-24 18:12:45 -04:00
johnnyq
28f1a965b8 Remove dead code 2026-07-24 17:33:09 -04:00
johnnyq
565aff35f6 Set SameSite=Lax on the session cookie
Applied at all seven session_start sites. keepalive.php and guest_post.php
had no cookie flags at all and now set httponly and secure too.
2026-07-24 14:09:52 -04:00
johnnyq
3549c2ef2a Harden CSRF handling and session cookies
Return 403 on CSRF failure instead of a 302, which anything following
redirects records as a 200. Set SameSite=Lax at every session_start;
keepalive.php and guest_post.php had no cookie flags at all.
2026-07-24 14:06:04 -04:00
johnnyq
8afc5aea89 Typo fix 2026-07-24 13:45:52 -04:00
johnnyq
af51c4316f Updated validateCSRFToken to automatically grab and compare get or post csrf token no longer need to pass an argument to the function which fixed the issue if no hash was present 2026-07-24 13:40:37 -04:00
johnnyq
dd358da65d Update missing CSRF endpoint checks missed update_db 2026-07-24 13:22:17 -04:00
johnnyq
91a1676e89 Update missing CSRF endpoint checks 2026-07-24 13:16:51 -04:00
johnnyq
e7698f15bd Make better use of formatAddress function for clients listing 2026-07-24 12:22:15 -04:00
johnnyq
c7b34fe107 Remove leftover getFallBack in client pdf export in favor of ?: '-' 2026-07-23 18:29:11 -04:00
johnnyq
5846119ec9 remove duplicate formatAddress 2026-07-23 18:16:57 -04:00
johnnyq
41b118ab87 Add formatAddress function and replace all addresses with it 2026-07-23 18:10:39 -04:00
johnnyq
9c65644adc Update the rest of the PHP functions to use camelCase 2026-07-23 17:39:06 -04:00
johnnyq
3d94846a61 Fix broken link to service modal in services 2026-07-23 13:48:00 -04:00
johnnyq
90bc8ed029 Fix broken links to modals and remove _details from service 2026-07-23 13:43:04 -04:00
johnnyq
40be80981d Fix vendor modal link in service details 2026-07-23 13:34:08 -04:00
johnnyq
b853c24c96 Remove a few more unused vendor contact modals and update the escaping in rename modal vendor_details.php to vendor.php 2026-07-23 13:26:06 -04:00
johnnyq
5f26d75e18 Update Functions in ticket_edit_vendor and document link vendor and deleted legacy code unused that had legacy functions tied to them 2026-07-23 13:17:41 -04:00
johnnyq
8f43b92496 Use stripe_init include to init stripe and updated code everywhere 2026-07-23 13:00:01 -04:00
johnnyq
7ccdc942fe Bump imapEngine from v1.25.2 to v1.25.3 2026-07-23 00:59:07 -04:00
johnnyq
2b756f6ea4 Split DB Updates into seperate files, with the cutoff being 2.0.0 2026-07-22 18:43:11 -04:00
johnnyq
17e4c61067 Spelling Fix 2026-07-22 17:40:30 -04:00
johnnyq
fad62ca045 Remove custom_hosting tables 2026-07-22 17:36:55 -04:00
johnnyq
5402578ce6 Remove Static payment processing fees from database and update field hints with useful info when adding / editing payment provider 2026-07-22 17:32:01 -04:00
johnnyq
8ee780566e Cleanup Expense Code in post blocks and remove single use function since stripe payments get reconciled during nightly cron 2026-07-22 17:06:10 -04:00
johnnyq
08334b22c3 Add Stripe fee reconciliation to cron
Balance transactions aren't always attached when a payment completes,
so the fee expense can be skipped at payment time. Daily cron pass now
finds recent Stripe payments with no matching fee expense and records
the actual fee once available. Dedupes by expense reference prefix,
30-day lookback.
2026-07-22 16:47:28 -04:00
johnnyq
f4b1b6585b Move Expense block for Stripe after intent 2026-07-22 16:16:40 -04:00
johnnyq
66b38b7f19 Get Expense from Stripe instead of Static Entry 2026-07-22 16:07:39 -04:00
johnnyq
54e2005224 Icon change for Mail Queue 2026-07-21 14:18:33 -04:00
johnnyq
d5a1305192 Reorg Admin Side nav menu 2026-07-21 14:17:16 -04:00
johnnyq
4bb497ea9c Reorganize Client Side nav and the renamed Client overview to All Client Docs side nav 2026-07-21 13:43:56 -04:00
johnnyq
97ab1df762 Feature: Add Transactions, reorg Main side nav 2026-07-21 13:21:23 -04:00
johnnyq
058e31c290 Fix Truncated Client name in side nav needs to be truncated before escaped 2026-07-20 19:31:31 -04:00
johnnyq
d173357b51 Add Bulk and Single Refresh certificate Actions 2026-07-20 19:14:38 -04:00
johnnyq
e134c89c3a Add Bulk and Single Refresh Domain Actions 2026-07-20 18:54:01 -04:00
johnnyq
a35ef025dc Add Expiring in Filter Field to assets, licenses, domains and certificate and wire that into the dashboard as well 2026-07-20 18:25:44 -04:00
johnnyq
ce0a62b0c3 Add Expiring Asset Warranties and Liceneses to the Dashboard 2026-07-20 17:45:03 -04:00
johnnyq
1a3d7a1e0d Bump ImapEngine from 1.25.1 to 1.25.2 2026-07-18 11:17:53 -04:00
johnnyq
9cc7e5ff3c Move Guest Function formatBytes to root shared functions and use in files and files section in contact 2026-07-16 20:25:18 -04:00
johnnyq
113d6220bb Add back missing function call 2026-07-16 19:32:52 -04:00
johnnyq
e40926c08d Restore native DNS/RDAP domain lookups (lost in functions.php split)
getDnsRecords and getDomainExpirationDate had regressed to shell_exec
(dig/whois) when domain code was moved into functions/domain.php; the
RDAP helper stack was dropped entirely. Restores the dns_get_record +
RDAP + port-43-socket-whois implementation from d1e1609b under the
current function names. getSslCertificate was unaffected.
2026-07-16 19:30:38 -04:00
johnnyq
e11d8f3252 Harden checkFileUpload: drop content hashing for random storage names
Replace md5(file_contents)+randomString(2) naming with randomString(32).
No longer reads the file into memory (removes file_get_contents), so
validation is O(1) regardless of size or upload count. Add is_uploaded_file()
and UPLOAD_ERR_OK checks, use pathinfo() for extension extraction, and
return false consistently on all failures (oversize previously returned a
truthy error string that callers treated as a valid filename).
2026-07-16 19:18:08 -04:00
johnnyq
41d029937a More Client Scoping fixes 2026-07-16 14:29:23 -04:00
johnnyq
1251944dcc More Client Enforcement in modals across the app 2026-07-16 13:33:17 -04:00
johnnyq
2c09ce097c More Client Enforcement in modals across the app 2026-07-16 13:10:02 -04:00
johnnyq
9e84ce4c07 Ticket Merge: EnforceClientAccess on modal and ticket list 2026-07-16 12:14:36 -04:00
johnnyq
a8ad16758c Update Contributing 2026-07-16 00:44:51 -04:00
johnnyq
9f11af0d4d Update Contributing 2026-07-16 00:42:10 -04:00
johnnyq
906f58473d Fix guest view credential totp 2026-07-16 00:26:25 -04:00
johnnyq
e39ecb7ac9 Remove Legacy OTP code and fix guest view credential 2026-07-15 22:02:52 -04:00
johnnyq
86b0e11e95 Enforce ClientAccess in asset modal 2026-07-15 21:37:48 -04:00
johnnyq
2076bb2a93 Added Audit Log on Edit Credential Modal as password can be viewed here 2026-07-15 21:13:07 -04:00
johnnyq
7c9bd77603 Remove unused credential view model, add enforceClient Check on export client pdf 2026-07-15 21:09:11 -04:00
johnnyq
b1c57fc5c7 Feature: Add new js function to retrieve and Audit log Password reveals, still need to get the edit modals 2026-07-15 20:32:50 -04:00
johnnyq
89cb9074ab UI: Add Monospace font to timestamps in notifications listing 2026-07-15 18:59:05 -04:00
johnnyq
590bad3bcc Global Search: use secure file handler on files in global search 2026-07-15 18:55:40 -04:00
johnnyq
60bae12024 Clean up admin post handler to match agent
- Anchor includes and glob to __DIR__ so loading doesn't depend on CWD
- Replace _model.php regex check with str_ends_with()
2026-07-15 18:48:40 -04:00
johnnyq
e48ea57db9 Clean up agent post handler
- Remove dead referer/module parsing (unused since modules are glob-loaded)
- Anchor includes and glob to __DIR__ so loading doesn't depend on CWD
- Replace _model.php regex check with str_ends_with()
2026-07-15 18:45:27 -04:00
johnnyq
3838d052f0 Add secure file handler for client ticket attachment and file view along with client and contact permission isolation, add the handler links in asset and contact 2026-07-15 18:35:32 -04:00
johnnyq
c8f616ebee Ticket UI: Migrated ticket Attachments to a small table class 2026-07-15 13:52:52 -04:00
johnnyq
33a2929ab6 Feature: Secure File Download Handler for ticket attachments Implemented 2026-07-15 13:42:19 -04:00
johnnyq
df3a24eab7 Feature: Secure File Download Handler Implemented 2026-07-15 13:11:02 -04:00
johnnyq
6dbe84dee4 Update admin/ breadcrumbs to match plural file names 2026-07-15 00:48:16 -04:00
johnnyq
6e20800a98 Update Tax to Tax Rate in headr title for tax rates 2026-07-15 00:44:32 -04:00
johnnyq
dfedc31c04 Update Tax to Tax Rates in admin side nav 2026-07-15 00:43:09 -04:00
johnnyq
dae7f09650 Remove _details, fix plural and singular file naming in admin, update admin post to match agent post 2026-07-15 00:38:34 -04:00
johnnyq
38a042d435 Remove _details and use singular filename for certain objects 2026-07-15 00:12:27 -04:00
johnnyq
c3a438ee59 Replace the rest of the getFallBack Functions with simple ?: '-' 2026-07-14 19:17:26 -04:00
johnnyq
bcb244c245 Update skipped function renames in anything named vendor 2026-07-14 19:09:32 -04:00
johnnyq
cddcedbf1c Cleanup format Functions 2026-07-14 18:28:53 -04:00
johnnyq
eac913eb07 Remove function removeEmoji() as this is no longer needed as the DB Charset is set to the standard utf8mb4 across the board 2026-07-14 18:27:02 -04:00
johnnyq
ee7ef71855 Correct plugins to libs path for zapCal library in functions 2026-07-14 18:15:32 -04:00
johnnyq
2181e92e44 Removed valueOrDash Function was able to replace with ?: '-' 2026-07-14 18:13:34 -04:00
johnnyq
746e919aff Replace Function getFallBack with valueOrDash() 2026-07-14 18:00:12 -04:00
johnnyq
71f7299337 Replace Function roundToNearest15() with roundToNearest15Min() 2026-07-14 17:55:59 -04:00
johnnyq
21a23e79b0 Replace Function getSSL() with getSslCertificate() 2026-07-14 17:54:31 -04:00
johnnyq
52b5063066 Replace Function getDomainRecords() with getDnsRecords() 2026-07-14 17:53:16 -04:00
johnnyq
8f824a1868 Replace Function shortenClient() with shortenClientName() 2026-07-14 17:46:25 -04:00
johnnyq
9d540f4bfe Replace Function customAction() with triggerCustomAction() 2026-07-14 17:43:55 -04:00
johnnyq
b8873039a4 Rename Functions: flash_alert, sanitizeFilename and display_folder_options using camelCase instead to match other custom php functions 2026-07-14 17:41:41 -04:00
johnnyq
f1836c70cd Replace Function logAction() with logAudit() 2026-07-14 17:35:23 -04:00
johnnyq
29a0b3ead8 Replace Function fetchUpdates() with checkForUpdates() 2026-07-14 17:31:54 -04:00
johnnyq
31cc3f3ac3 Replace Function strtoAZaz09() with toAlphanumeric() 2026-07-14 17:30:13 -04:00
johnnyq
8de3cd300d Replace Function key32gen() with generateTotpSecret() 2026-07-14 17:27:02 -04:00
johnnyq
b57ddc0e5c Replace Function sanitizeInput() with just escapeSql() and update all instances throughout 2026-07-14 17:17:50 -04:00
johnnyq
7bc47a58fe Replace Function nullable_htmlentities() with just escapeHtml() and update all instances throughout 2026-07-14 17:10:45 -04:00
johnnyq
d62b6e2ae7 Update comment 2026-07-14 16:58:16 -04:00
johnnyq
5eb9f6b6d5 Fix weak RNG in key32gen (TOTP secret generation)
Replace srand()/rand() with random_int() for cryptographically secure,
unbiased key generation. The previous implementation seeded rand() from
microtime(), making TOTP secrets predictable if the generation time
could be approximated. Also removes modulo bias and dead while(1)
wrapper.

Output format is unchanged: 32 chars from the base32 alphabet (A-Z,
2-7), so existing TOTP enrollments are unaffected.
2026-07-14 16:57:16 -04:00
johnnyq
bf0d799caf Remove old function comment 2026-07-14 16:51:34 -04:00
johnnyq
29bea9517d Removed the last of legacy validate functions and replaced with the new enforce fumctions. 2026-07-14 16:49:14 -04:00
johnnyq
a2a96f9752 Removed unused legacy functions and split the functions into their own file and use function.php to call them 2026-07-14 16:32:45 -04:00
johnnyq
c1447f5bad Bump TinyMCE from 8.6.0 to 8.7.0 2026-07-14 16:08:05 -04:00
johnnyq
79032de032 Certificates: Allow to search by description 2026-07-14 11:23:54 -04:00
johnnyq
b81e57db28 Show Invoice Stats in Invoice only for user permissable clients 2026-07-13 17:19:32 -04:00
johnnyq
95441dc3bb Show 7 Characters of client name in client side nav instead of Abbreviation 2026-07-13 13:29:24 -04:00
johnnyq
fe7e9b2398 Main Side Nav: Only shows counts to the user who has permission to see 2026-07-13 13:08:03 -04:00
johnnyq
91e9f6097b Client Overview Side Nav: Only shows counts to the user who has permission to see 2026-07-13 12:45:59 -04:00
johnnyq
0f0aa89f75 Bump ImapEngine from 1.25.0 to 1.25.1 2026-07-10 18:37:16 -04:00
johnnyq
8da3a107fb Rename plugins to libs and update all file references 2026-07-10 13:24:20 -04:00
johnnyq
7ba1571400 Revert Billable placement in tickets, create CONTRIBUTING.md file 2026-07-10 12:32:46 -04:00
johnnyq
d2026a417f Add Client to top of the modals trips, ticket, recurring ticket, invoice, recurring invoice, quote and rework the UI for Ticket/Recurring ticket create Put Billable beside subject so it doesnt take up a whole row 2026-07-09 18:50:31 -04:00
johnnyq
fc59481476 Enforce Client Access in client selectbox 2026-07-09 16:17:00 -04:00
johnnyq
21328c2dee clientEnforcement on Recurring Invoices and payments listings 2026-07-09 16:13:39 -04:00
johnnyq
317fe91359 Add Missing clientEnforcement in various modals, already handled on POST also add client selection in edit trip and enforce client permissions on trips 2026-07-09 16:07:54 -04:00
johnnyq
68f705b135 Set EnforceClientAccess Perms throughout expenses, modals and post along with listing 2026-07-09 14:51:51 -04:00
johnnyq
c01645c4e2 Expense: On create enforce client Access only if Client is selected 2026-07-08 18:13:14 -04:00
johnnyq
ae5c9fe8ec Mail Parser: Add logic when creating ITFlow folder to determine if the namespace is INBOX. or can be created in root directory. Fixes issue with CPanel Dovecot Maildir++ configuration where their namespace is just INBOX 2026-07-08 15:58:00 -04:00
johnnyq
de6d2b1595 OAUTH Send Quote Fix: was reading smtp host var needed to read smtp provider as smtp host is not filled in when OAUTH2 is selected 2026-07-07 16:20:21 -04:00
johnnyq
47a5825d3e OAUTH Send Invoice Fix: was reading smtp host var needed to read smtp provider as smtp host is not filled in when OAUTH2 is selected 2026-07-07 15:04:48 -04:00
johnnyq
78c3dd0eed Remove Dig and Whois binary requirements, revert fetchUpdates function 2026-07-05 16:05:19 -04:00
johnnyq
d1e1609b8a Right — same commit or separate, here's the combined version covering both:
Remove exec/shell_exec from update checker and domain lookups

Update checker (fetchUpdates) no longer shells out to git:

- Current commit read directly from .git/HEAD, following the branch
  ref through loose refs with a packed-refs fallback (survives git gc);
  detached HEAD handled
- Latest commit fetched from the GitHub API via curl using the
  Accept: application/vnd.github.sha header (returns bare SHA, no
  JSON parsing)
- Repo owner/name derived from the origin remote in .git/config so
  forks check against their own remote, falling back to
  itflow-org/itflow
- Failures now distinguish unreadable .git (permissions) from API
  errors (network/rate limit) instead of silently returning empty

Domain lookups no longer shell out to dig and whois:

- DNS records (A/NS/MX/TXT) via dns_get_record()
- Registration data via RDAP (JSON over HTTPS, curl), the ICANN
  successor to port-43 whois; RDAP server per TLD resolved from
  IANA's bootstrap registry, cached locally for a week, rdap.org as
  secondary lookup
- Expiration date from RDAP's structured expiration event, replacing
  regex/date-format guessing for RDAP-covered TLDs
- Port-43 whois retained as socket-based fallback (fsockopen) for
  ccTLDs without RDAP, with IANA server discovery and one registrar
  referral follow
- RDAP responses cached per-run: getDomainRecords() and
  getDomainExpirationDate() on the same domain = one HTTP request

Fixes whois rate limiting, removes the exec dependency for hardened
hosts (Snuffleupagus etc.), and eliminates the shell injection
surface - no shell, nothing to escape.
2026-07-05 15:33:51 -04:00
johnnyq
a32c4b5172 Update Changelog 2026-07-03 12:29:10 -04:00
johnnyq
c34e4f5ff0 EnforceClientAccess on Generating Guest Share Links 2026-07-03 12:27:46 -04:00
johnnyq
aab8b34970 Bump ITFlow Version and changelog 2026-07-03 12:15:05 -04:00
johnnyq
1a1f6d80a1 Possible Fix for OAUTH2 M365 2026-07-02 16:08:59 -04:00
johnnyq
fe9e3274fe Fix missing ) 2026-07-01 11:11:30 -04:00
johnnyq
9f14e97d58 Update changelog 2026-06-29 19:04:59 -04:00
johnnyq
fab5dbea04 Don't set Theme to Gray amd blue when in client section always Keep configured theme 2026-06-29 18:52:56 -04:00
johnnyq
1bcc535e99 Updated Changelog and Bump ITFlow version and update logs timestamp fields to use monospace font 2026-06-27 21:52:02 -04:00
johnnyq
6a448668ad Clients: Only show 3 tags per line 2026-06-26 13:28:03 -04:00
johnnyq
166a606ba9 Remove client counts on clients listing page 2026-06-26 13:26:25 -04:00
johnnyq
48c33ae1ab Enforce Client Access and Role Access for AI Ticket Summary 2026-06-26 12:40:47 -04:00
johnnyq
e7b53388a0 Check CSRF and Enforce ClientAccess when deactivating Shared Items 2026-06-26 12:17:38 -04:00
johnnyq
f14fa22222 USe enforceClientAccess and enforceUserPermissions on Client related edit / view modals as we do with credential edit modal 2026-06-26 12:05:11 -04:00
johnnyq
c16a1753a5 Add text MonoSpace in places where it needs to be, reduced info icon size in clients, Fixed navigation highlight for files when in document details and is asset details 2026-06-25 18:06:39 -04:00
johnnyq
a53d828a73 MAil Settings: Further UI Sprucing added more placeholder text field with only number input for ports etc 2026-06-25 16:01:11 -04:00
johnnyq
298569a2c2 Mail Setting UI: Rework the entire Mail Configuration 2026-06-25 15:49:09 -04:00
johnnyq
8e9cd7a8a5 Bump Sortablejs from 1.15.6 to 1.15.7 2026-06-24 14:28:21 -04:00
johnnyq
2010acaaa2 Bump TinyMCE from 8.5.0 to 8.6.0 2026-06-24 14:26:03 -04:00
johnnyq
ceec38d3f1 Bump Fullcalendar from 6.1.20 to 7.0.0 amd convert existing code to comply with 7.0, also make calendar more printable 2026-06-24 14:22:56 -04:00
johnnyq
bd310d1e5d Bump PHPMailer from 7.0.2 to 7.1.1 2026-06-24 13:53:50 -04:00
johnnyq
3d858eb761 Update to latest version of imapengine 1.25.0 2026-06-24 13:48:26 -04:00
johnnyq
171a0d38f8 Mail Parser: Completely remove Webklex IMAP and all dependcies 2026-06-24 13:39:07 -04:00
johnnyq
63ad3256ee CLI Setup: Add repo_branch option defaults to master 2026-06-24 13:18:01 -04:00
johnnyq
6e7d2b2d6a Invoice Late Overdue notices shows correct balance when late fees are attached and if partial invoice was paid 2026-06-24 13:07:52 -04:00
johnnyq
a819dff19a Mail: Update mail configuration check to use smtp_provider instead of smtp_host as host is blank upon using oath2 2026-06-24 12:47:41 -04:00
johnnyq
6532c2dde4 User Preferences Avatar: Fix creating user upload directory if doesnt exist, and remove Avatar now properly deletes the old avatar image 2026-06-24 12:04:19 -04:00
johnnyq
b1a5b34725 Do not send an inapp alert on successful cron execution, keep it in logging only 2026-06-23 18:22:20 -04:00
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
johnnyq
43dbc69358 Remove custom tables from db.sql 2026-03-14 12:25:15 -04:00
johnnyq
5c24813d36 Move client column back to the end in networks 2026-03-10 20:43:01 -04:00
johnnyq
c8dd7f2970 Network: Fix Assignable IPs field not posting 2026-03-10 20:35:37 -04:00
johnnyq
d13104bb5a Update changelog order 2026-03-10 14:06:34 -04:00
johnnyq
5d85810e58 Update Changelog 2026-03-10 14:01:59 -04:00
johnnyq
b00014771d tickets: Add Tooltip to Approve task 2026-03-10 13:58:39 -04:00
johnnyq
b133f63736 Ticket: Fix Task Approval 2026-03-10 13:54:54 -04:00
johnnyq
e45563ad66 Update Changelog 2026-03-10 13:25:19 -04:00
johnnyq
cf639cbf98 Update Changelog 2026-03-10 13:22:22 -04:00
johnnyq
b365b7c810 Rename unarchive to restore in assets 2026-03-09 15:33:32 -04:00
johnnyq
b394f484d3 Fix Vars add empty value placeholder for Ticket Frequency 2026-03-09 15:29:22 -04:00
johnnyq
ada336ceea Update var names 2026-03-09 15:16:29 -04:00
johnnyq
26ebac450f Setup: Fix config_base_url if not using the script so /setup is not appended 2026-03-09 12:04:21 -04:00
johnnyq
6e33c071ef Hide New Invoice, Recurring Invoices, Tickets, revenues, products for appropriate permissions 2026-03-09 11:44:45 -04:00
johnnyq
5bd21a2bd4 add .unifi to the allowed extensions on file uploads 2026-03-09 11:22:46 -04:00
johnnyq
e2547122a6 Update Var naming schemes to match the post var name 2026-03-08 14:25:12 -04:00
johnnyq
edad83cc37 Fix Regression of chnage post name from client to client_id Add / Edit Expense 2026-03-08 13:43:46 -04:00
johnnyq
b0190f462c Invoice: Fix Regression caused by renaming post var clien to client_id 2026-03-08 13:33:36 -04:00
johnnyq
c936823912 Update Changelog 2026-03-08 12:54:58 -04:00
johnnyq
7acaf55331 Bump App Version and Update Changelog 2026-03-08 12:47:02 -04:00
johnnyq
6122efc870 Updated all Queries related to invoice items to reflect their new tables quote, invoice, recurring invoice items 2026-03-07 19:35:44 -05:00
johnnyq
7f180eb6d9 Migrate Invoice Quote Recurring Invoice Items to their new respecting tables 2026-03-07 18:47:28 -05:00
johnnyq
72091670fd Bump DB to to include tables for quote items and recurring invoice item for future splitting of line items 2026-03-07 18:23:19 -05:00
johnnyq
2b6ebca4c3 Fix Project templates listing to not open edit modal when project is clicked 2026-03-07 18:00:32 -05:00
johnnyq
e44d571331 Allow Project template ticket templates to be Drag and drop sorted instead of manually typing in the order 2026-03-07 17:56:39 -05:00
johnnyq
526fa1aff5 Bump stripe-php from 19.0.0 to 19.4.1 2026-03-07 17:19:48 -05:00
johnnyq
11ba077726 Bump fullcalendar from 6.1.19 to 6.1.20 2026-03-07 17:09:01 -05:00
johnnyq
ffb97006ec Bump TCPDF from 6.10.1 to 6.11.2 2026-03-07 17:01:53 -05:00
johnnyq
8dbbef37bb Fix file upload missing client_id 2026-03-07 16:50:11 -05:00
johnnyq
92b98b95ee Better field arrangedment for better workflow for rack edit add 2026-03-07 16:39:07 -05:00
johnnyq
083262fedb Dont require gateway on network edit also make vlan field numeric 2026-03-07 16:31:39 -05:00
johnnyq
fe9bc05937 Rearrange fields so they make a better workflow in software edit and add 2026-03-07 16:17:34 -05:00
johnnyq
726b1cd42b Fix Stripe Add Payment in client portal regression on CSRF check 2026-03-07 13:17:24 -05:00
johnnyq
8de7b20ba1 Add CSRF Checks in the client portal 2026-03-07 13:11:08 -05:00
johnnyq
6d2cb0aea3 Split out item edits into seperate posts and modals for editing invoice, quote and recurring invoice items 2026-03-07 12:12:27 -05:00
johnnyq
c8273b2052 Client related modal and post move client_id get after post 2026-03-07 11:48:56 -05:00
johnnyq
60b6c245ef Change expense client to client_id post var 2026-03-06 18:40:01 -05:00
johnnyq
fcf098f494 use client_id as post var name not client 2026-03-06 18:33:25 -05:00
johnnyq
74ce8f4e98 Trips: enforceClientAccess only if client is referenced 2026-03-06 18:26:14 -05:00
johnnyq
6e3a36d8c4 payments: enforceClientAccess 2026-03-06 18:20:58 -05:00
johnnyq
6f0a2a5a73 quotes: enforceClientAccess 2026-03-06 18:11:35 -05:00
johnnyq
111a30f13c recurring invoices: enforceClientAccess 2026-03-06 18:01:20 -05:00
johnnyq
202f55c2ff invoices: remove client_id post from edit and link modals as it should get the client_id in post, enforceClientAccess 2026-03-06 17:53:44 -05:00
johnnyq
8ad8fd07b3 files, folders, documents: remove client_id post from edit and link modals as it should get the client_id in post, enforceClientAccess 2026-03-06 17:16:04 -05:00
johnnyq
a1931f59f8 services: remove client_id post from edit service modal as it should get the client_id in post, enforceClientAccess 2026-03-06 17:01:34 -05:00
johnnyq
e7b70c7992 domains: remove client_id post from edit domain modal as it should get the client_id in post, enforceClientAccess 2026-03-06 16:59:15 -05:00
johnnyq
7563148182 certificates: remove client_id post from edit certificate modal as it should get the client_id in post, enforceClientAccess 2026-03-06 16:53:20 -05:00
johnnyq
a252ff717e Racks: remove client_id post from edit rack and add rack device modal as it should get the client_id in post, enforceClientAccess 2026-03-06 16:43:56 -05:00
johnnyq
7d41782013 Credential: remove client_id post from edit credential modal as it should get the client_id in post, enforceClientAccess 2026-03-06 16:34:01 -05:00
johnnyq
91976cc415 Software: remove client_id post from edit software modal as it should get the client_id in post, enforceClientAccess 2026-03-06 16:23:53 -05:00
johnnyq
8afa2dbf75 Assets: remove client_id from edit asset modal as it should get the client_id in post, enforceClientAccess 2026-03-06 16:13:56 -05:00
johnnyq
3983c45cac Calendar Events: use client_id instead of client in POST and enforceClientAccess if client is assigned to an event 2026-03-06 15:59:18 -05:00
johnnyq
df5c350765 Vendors: enforceClientAccess in POST only if vendor is assigned a client, as vendor_client_id 0 is for global vendors 2026-03-06 15:43:44 -05:00
johnnyq
de8b9df4da Projects: enforceClientAccess in POST only if a client is assigned to the project 2026-03-06 15:25:30 -05:00
johnnyq
3be815c749 Recurring tickets: enforceClientAccess in POST 2026-03-06 14:35:19 -05:00
johnnyq
71b19e125e Locations and tickets enforceClientAccess in POST fix a few missing client_id vars which were bale to be used for logging too 2026-03-06 14:09:51 -05:00
johnnyq
8fc3dfed1f New Function enforceClientAccess() and added to contact post and client inc all This enforces user client access if set at post and in other places easily 2026-03-06 13:05:11 -05:00
johnnyq
30357b9cf7 Add CSRF Checks to notifications and ensure the user dismissing the notification is their own notification 2026-03-05 17:51:20 -05:00
johnnyq
a81edc122d Add CSRF Checks on Agent ajax endpoints that would update / delete or add something to the db 2026-03-05 17:12:44 -05:00
johnnyq
994526e2c8 Asset details Modal: Reduce top bar Icon Size, Add IPv6 to interfaces, rearrange interfaces to match full page asset details and cleanup code 2026-03-04 19:48:51 -05:00
johnnyq
203b161e82 Add Billing Time Increment Option in Client Ticket Time Detail Report, this option will later be available globally 2026-03-04 18:33:08 -05:00
johnnyq
47b8ec6f96 Simple Calendar naming 2026-03-04 17:18:54 -05:00
johnnyq
302914c789 Add New Calendar Hint 2026-03-04 17:13:02 -05:00
johnnyq
f77bd4f0c6 Calendar: Add Delete Calendar, rename System calendar to Built-in calendar with builtin calendar names, moved calendar select on top for both edit / add event 2026-03-04 17:09:13 -05:00
johnnyq
af4327ff27 Show Full Event title in calendar on hover 2026-03-04 16:27:22 -05:00
johnnyq
5cbed128e6 Use new custom CSS Class for Column Checkboxes so they are a static 40px 2026-03-04 11:56:20 -05:00
johnnyq
dd5fde4069 Client Overview: Cleaned up UI shrunk table sizes and card headers 2026-03-03 16:11:52 -05:00
johnnyq
220e1d4e24 Make new ticket first in client top head 2026-03-03 15:51:50 -05:00
johnnyq
3481431eb5 Add New ticket button in client top header button, spruce up credential favorites on overview 2026-03-03 15:50:14 -05:00
johnnyq
b94604ae71 Assets List: Major UI / UX Update removed some columns to reduce space and add them as a secondary row instead 2026-03-03 15:20:11 -05:00
johnnyq
fe406487de UI Update for Role Listing 2026-03-03 11:44:22 -05:00
johnnyq
99218d02ad Document Templates: UI Enhancement / Cleanup Document templates 2026-03-03 11:16:36 -05:00
johnnyq
8cd1668ff2 Cleanup Ticket Template Details header 2026-03-03 09:50:27 -05:00
johnnyq
c0f26204c2 Swapped Location and Contact Columns in Clients Listing for better scannabilty 2026-03-02 23:59:17 -05:00
johnnyq
ef3a7d0490 Rearrange columns for network interfaces make better logicl sense 2026-03-02 23:52:01 -05:00
johnnyq
f9558d4701 Assets: Added IPv6 Address to assets list and interface list, Updated UI of add / edit network interfaces updated icons wording and bettr arrangement 2026-03-02 23:40:53 -05:00
johnnyq
9d9ebe7b9e Added Bulk Net Terms editing in clients 2026-03-02 22:43:14 -05:00
johnnyq
918b40afbe Add missing CSRF Checks in admin area and settings 2026-03-02 22:15:36 -05:00
johnnyq
6da8821f2c Add hidden form field to folder modals as well 2026-03-02 20:51:12 -05:00
johnnyq
90171064a8 document and files and folders: Add missing CSRF checks, add missing permission checks 2026-03-02 20:49:35 -05:00
johnnyq
6bbe887f8b services: Add missing CSRF checks rename unarchive to restore 2026-03-02 20:34:55 -05:00
johnnyq
b5fb14ec96 domains: Add missing CSRF checks rename unarchive to restore 2026-03-02 20:32:20 -05:00
johnnyq
dd2ac00aa2 certificates: Add missing CSRF checks rename unarchive to restore 2026-03-02 20:26:43 -05:00
johnnyq
e7b5e7120a racks: Add missing CSRF checks rename unarchive to restore 2026-03-02 20:22:36 -05:00
johnnyq
8bac4f9e53 networks: Add missing CSRF checks rename unarchive to restore 2026-03-02 20:17:26 -05:00
johnnyq
023cb4ff11 credentials: Add missing CSRF checks 2026-03-02 20:11:56 -05:00
johnnyq
1740599b61 software licenses: Add missing CSRF checks, add missing permission checks 2026-03-02 19:37:27 -05:00
johnnyq
24b244b612 assets: Add missing CSRF checks, add missing permission checks, renamed unarchive to restore 2026-03-02 19:32:45 -05:00
johnnyq
c71d1f190e locations: Add missing CSRF checks, add missing permission checks, renamed unarchive to restore 2026-03-02 19:15:52 -05:00
johnnyq
18e180eca5 Vendors: Check if client_id is set to enforce the right permission check. If client vendor then check client module otherwise check financial module 2026-03-02 18:40:07 -05:00
johnnyq
d936339f07 Contacts: Add missing CSRF checks, add missing permission checks, renamed unarchive to restore 2026-03-02 18:28:53 -05:00
johnnyq
ad16e92763 Use true Decimal minute reporting in Client Time Audit Detail Report 2026-03-02 18:06:54 -05:00
johnnyq
8a1335174d Trips: Add missing CSRF checks, we may need another permission module check for trips for now only admin and financial lv3 can delete a trip 2026-03-02 17:50:04 -05:00
johnnyq
7b438e2889 Transfers: Add missing CSRF checks 2026-03-02 17:38:20 -05:00
johnnyq
550980719e Accounts: Add missing CSRF checks 2026-03-02 17:35:18 -05:00
johnnyq
3d80d1519e Recurring Expenses: Add missing CSRF, Add missing permission checks 2026-03-02 17:32:18 -05:00
johnnyq
1d5fceeecd Expenses: Add missing CSRF, Add missing perms 2026-03-02 17:27:56 -05:00
johnnyq
5b49908438 Vendors: Add missing CSRF, need to update the permissions as a vendor can be client or global and permissions need to be set based off if the referal url has client_id or not 2026-03-02 17:20:00 -05:00
johnnyq
7e515afb79 Payments: Add missing CSRF and additonal perm check 2026-03-02 16:38:17 -05:00
wrongecho
dee5085f4a Allow associating a quote with a ticket 2026-03-02 10:54:44 +00:00
wrongecho
795144b288 Fix documents => files redirect after deletion (since we merged these) 2026-03-02 08:12:07 +00:00
wrongecho
5c3ff91f21 Lower autoclose minimum allowed value to 24 hours (this still works off cron so is not exact) 2026-03-02 08:07:43 +00:00
johnnyq
36ac006438 Products: Add missing CSRF checks change wording from unarchive to restore 2026-03-01 22:50:04 -05:00
johnnyq
af1ebfea41 Revenues: Add missing CSRF checks 2026-03-01 22:42:37 -05:00
johnnyq
f653752026 Recurring Invoice: Add missing CSRF checks and missing permissions in POST 2026-03-01 22:37:51 -05:00
johnnyq
4440581f14 Invoice: Add missing CSRF checks and missing permissions 2026-03-01 22:22:21 -05:00
johnnyq
2c47001b19 Quote: Add missing CSRF checks and missing permission on export quote pdf 2026-03-01 22:02:43 -05:00
johnnyq
e1daa14087 Calendar: Add missing CSRF checks 2026-03-01 21:52:28 -05:00
johnnyq
308dc6e550 Ticket Tasks: Add missing CSRF checks and other CSRF checks missed in he previous commits 2026-03-01 21:45:26 -05:00
johnnyq
54638428e3 rename unarchive project to restore project 2026-03-01 21:32:24 -05:00
johnnyq
1508e6b055 Project: Add missing CSRF checks 2026-03-01 21:30:41 -05:00
johnnyq
9213249f7b Recurring Ticket: Add missing CSRF checks 2026-03-01 21:21:37 -05:00
johnnyq
63f6faf1e8 Ticket: Add missing CSRF checks 2026-03-01 21:14:58 -05:00
johnnyq
c2cbfc5558 Add Hidden Inpout csrf token to client download pdf 2026-03-01 20:43:44 -05:00
johnnyq
ef1b65de09 Client: Add missing CSRF checks, and missing enforcements 2026-03-01 20:42:18 -05:00
johnnyq
ae81092112 Updated Placeholders to set better formal expectations 2026-02-28 20:52:53 -05:00
johnnyq
f09be24188 Rearrange add/edit Asset field Order, updated icons, minor ui work 2026-02-28 20:12:51 -05:00
johnnyq
10c37946b1 UI: Update icoms for description inputs 2026-02-28 19:14:00 -05:00
johnnyq
adb43703ed Update Networks, remove subnet mask in favor of CIDR, Global View show clients in first column, allow for IPv6 and provide IPv6 placeholder examples, gateway no longer required reword DHCP Range to IP Range moved DNS to the end, chnaged out some icons 2026-02-28 18:41:03 -05:00
johnnyq
92fba49a91 Revert "Add new optional beta email parser thats based on ImapEngine instead of Webklex"
This reverts commit 9cb1ff7330.
2026-02-26 16:44:49 -05:00
johnnyq
ac4bb32081 Revert "Beta Mail Parser Add inline images as attachments keeps the DB clean"
This reverts commit 87fd23f443.
2026-02-26 16:44:39 -05:00
johnnyq
7927312f4f Revert "Revert "Beta Mail Parser Add inline images as attachments keeps the DB clean""
This reverts commit 04a74b5a34.
2026-02-26 16:41:50 -05:00
johnnyq
04a74b5a34 Revert "Beta Mail Parser Add inline images as attachments keeps the DB clean"
This reverts commit 87fd23f443.
2026-02-26 16:41:03 -05:00
johnnyq
87fd23f443 Beta Mail Parser Add inline images as attachments keeps the DB clean 2026-02-26 16:30:29 -05:00
johnnyq
9cb1ff7330 Add new optional beta email parser thats based on ImapEngine instead of Webklex 2026-02-26 16:11:49 -05:00
wrongecho
1ba19cc249 New Ticket Parsing - Anyone CC'ed onto the original email that created the ticket is added as a ticket watcher 2026-02-26 12:49:32 +00:00
wrongecho
702d9689d6 clarify PR stance 2026-02-26 10:24:44 +00:00
johnnyq
8efa354fdb Fix Task Sorting in ticket templates 2026-02-21 14:49:46 -05:00
johnnyq
129e95fd3a Added Net 45 Days, removed 14 Days as this isnt typically used anywhere 2026-02-19 12:34:30 -05:00
johnnyq
ef7794c411 Added non payment invoice reminder / Late fee if enabled after 1 day an invoice is overdue. currently it was jumping right to 30,60,90... now its 1,30,90... 2026-02-19 12:20:26 -05:00
johnnyq
12b3775041 Update Changelog 2026-02-14 15:50:27 -05:00
johnnyq
52b509fdc5 Update Changelog 2026-02-14 15:49:40 -05:00
johnnyq
f9d0a8bf43 Ticket: Dont display Updated at if null, Move Subject from top ticket bar to header in ticket details, add additional comments about the client_permission_overide 2026-02-14 15:48:40 -05:00
johnnyq
2349ef33d1 Remove 0 in access_permission_query 2026-02-14 15:07:42 -05:00
johnnyq
ccaf15268c Add a 0 IN ticket_client_id for no client tickets so agents that are restricted to certain clients and still view no clients in tickets list by using a , also restrict change clients on tickets for restricted agents 2026-02-14 15:06:17 -05:00
johnnyq
616635f951 client edit modal: Restrict Access to client restricted agents 2026-02-14 13:17:15 -05:00
johnnyq
ce7daaf1cf ticket edit modal: Restrict Access to client restricted agents 2026-02-14 12:55:34 -05:00
johnnyq
fb9f5d986d ticket Details: Restrict Access to ticket details by restricted agents 2026-02-14 12:51:58 -05:00
johnnyq
3148906cfe ticket Details: Restrict Access to ticket details by restricted Agents 2026-02-14 12:30:49 -05:00
johnnyq
6c057f4fd0 Restrict Tickets presented in ticket listing to client restricted Agents 2026-02-14 12:16:22 -05:00
johnnyq
3014bba66d Update Changelog, Bump App Release 2026-02-14 12:01:49 -05:00
johnnyq
97958d5e22 Missing Generate Password js code in Create Credential modal 2026-02-13 14:43:34 -05:00
johnnyq
1a7452cc27 Restrict Agents that are restricted to view certain clients from calendar events. 2026-02-13 14:28:45 -05:00
johnnyq
34ba5d0570 Properly get the new ticket number for logging in ticket merge 2026-02-13 14:14:01 -05:00
johnnyq
c0fe9813dc Fix Ticket Merging regressed from ticket select now use ticket_id instead of ticket_number 2026-02-13 14:09:05 -05:00
johnnyq
fd29eb7c15 Fix Transfer Asset to client due to field change from asset_important to asset_favorite 2026-02-13 13:39:41 -05:00
johnnyq
1bee085b33 Add Report for Client Detail Time Auditing 2026-02-13 13:35:02 -05:00
johnnyq
1d06e6d9c8 Revert to old ajax-modal js code for now, Fix Assets not lising in create ticket. 2026-02-11 13:33:28 -05:00
wrongecho
bdf8038148 Merge pull request #1265 from MydsiIversen/Time-API
feat: Add API endpoint to retrieve time worked by technicians
2026-02-09 14:23:02 +00:00
Mads Iversen
62fb2c91a1 feat: Add API endpoint to retrieve time worked by technicians on tickets with filtering by date and technician. 2026-02-09 12:09:56 +00:00
johnnyq
d1eeba67fc Add bulk Fav / unfav creds and added fav creds to the client overview page 2026-02-07 18:42:53 -05:00
johnnyq
2ca8112daf Revert Git change test to ticket_email_parser.php 2026-02-06 15:14:37 -05:00
johnnyq
546f10bc82 Testing Git changes on ticket_email_parser.php 2026-02-06 15:13:54 -05:00
johnnyq
ec7f1d561d Fix Version 2026-02-06 13:12:34 -05:00
johnnyq
8d54bc3a2f Another change 2026-02-06 13:03:07 -05:00
johnnyq
1d8f77f799 Update Changelog for 26.2 Release 2026-02-06 13:01:37 -05:00
johnnyq
1c6795dd55 Bump app version 2026-02-06 12:09:32 -05:00
Johnny
4d895a56e4 Merge pull request #1261 from cs2000/develop
Add full Microsoft 365 and Google OAuth mail support
2026-02-05 14:04:22 -05:00
johnnyq
bdd56c91f7 Update Credential Important to Favorite 2026-02-05 12:24:21 -05:00
johnnyq
be172b5bd9 Add star-toggle class to add asset 2026-02-05 12:15:41 -05:00
johnnyq
d224d71f59 Bump Datatables from 2.3.4 to 2.3.7 2026-02-05 11:50:33 -05:00
johnnyq
065630b975 Bump PHPMailer from 7.0.1 to 7.0.2 2026-02-05 11:46:28 -05:00
johnnyq
decea604ab Bump TinyMCE 8.2.2 to 8.3.2 2026-02-05 11:38:22 -05:00
johnnyq
004b3b2bdc Draft of ticket_by_client report v2 not yet in the side menu of reports 2026-02-05 11:31:40 -05:00
cs2000
dca0cc34e9 Revert to original file from dev branch
Accidentally comitted an older file.
2026-02-05 16:08:04 +00:00
cs2000
9afb165fc4 SonarCube Suggestion
Remove duplicates of https://login.microsoftonline.com/ and make them use a constant.
2026-02-05 10:26:51 +00:00
cs2000
6a6eb4a714 SonarCube Suggestion
Define a constant "MICROSOFT_OAUTH_BASE_URL" instead of duplicating "https://login.microsoftonline.com/" multiple times in the same file.
2026-02-05 10:22:06 +00:00
cs2000
629f4e0c81 SonarCube Suggestion
Defined a constant $settings_mail_path at the top of the file instead of duplicating "/admin/settings_mail.php" in the code multiple times.
2026-02-05 10:19:13 +00:00
cs2000
7cbe9bf7fc SonarCube Suggestion
Removed two instances of unecessary double parentheses.
2026-02-05 10:17:28 +00:00
cs2000
fc33312e79 SonarCube Suggestion
Rename multiple functions to conform to project naming rules (^[a-z][a-zA-Z0-9]*$). This is a non-functional refactor for style/CI compliance and consistency.
2026-02-05 10:15:52 +00:00
cs2000
563c0ea9c4 Changes for M365 oAuth using dev code
My bad, my changes were made previously using my installed version and not the latest changes in the dev branch, i have pulled the dev branch and merged my changes with the latest codebase.
2026-02-05 09:38:09 +00:00
cs2000
10d1a902d9 Changes for M365 oAuth now using latest dev code
My bad, my changes were made previously using my installed version and not the latest changes in the dev branch, i have pulled the dev branch and merged my changes with the latest codebase.
2026-02-05 09:37:22 +00:00
johnnyq
4079257739 Update UI/UX for Adding and editing roles, Permissions can be set upon role add nice blocker style radios buttons instead of select boxes 2026-02-04 13:12:24 -05:00
johnnyq
91aba13c0d Fix Empty col in client overview if no fabvorite assets, fix creds page rename var from important to favorite 2026-02-04 11:11:12 -05:00
cs2000
2cca4f2f0e Changes for M365 oAuth
- Added null-safe guard around folder path logging during message move failure to prevent property_exists() fatal when folder object is null.
2026-02-04 13:26:52 +00:00
cs2000
d2e8dc1439 Changes for M365 oAuth
- Fixed email queue gating for OAuth SMTP setups by treating configured config_smtp_provider as mail-enabled, even when config_smtp_host is blank.
- Restores queueing for public ticket reply emails (including “Public Comment & Email”) and related ticket notification paths.
2026-02-04 13:26:17 +00:00
cs2000
f3f9d0dd71 Changes for M365 oAuth
- Added OAuth token lifecycle helpers (expiry check, refresh, persistence).
- Updated SMTP XOAUTH2 send path to automatically refresh expired/missing access tokens for Microsoft/Google providers before sending queued mail.
2026-02-04 13:25:32 +00:00
cs2000
f6845a046f Changes for M365 oAuth
- New callback endpoint to complete Microsoft OAuth web flow.
- Validates admin session + OAuth state, exchanges authorization code for tokens, stores refresh/access tokens and expiry, and redirects with success/error feedback.
2026-02-04 13:24:50 +00:00
cs2000
a50a4f274f Changes for M365 oAuth
- Added handler to start Microsoft OAuth Authorization Code flow (oauth_connect_microsoft_mail) with state generation/validation prep.
- Added handler to test OAuth token refresh from admin UI and persist refreshed tokens/expiry.
- Updated IMAP test handler to support OAuth token refresh + XOAUTH2 authentication (in addition to legacy LOGIN).
2026-02-04 13:23:53 +00:00
cs2000
6b6d847756 Changes for M365 oAuth
- Added web-based Microsoft OAuth onboarding UI in Mail settings, including a Connect Microsoft 365 button and auto-generated callback URI display.
- Added Test OAuth Token Refresh UI section.
- Updated visibility logic so Test Email Sending and Test IMAP Connection show correctly for OAuth-based configs (not only host/password configs).
2026-02-04 13:23:03 +00:00
johnnyq
65d1f59e9b Feature: Favorites added to assets, Bulk Fav/unfav, adds favs to client overview 2026-02-03 22:23:20 -05:00
johnnyq
f39e6ccbc9 Remove old ticket merge code, rename kanban_v2 to to just kanban 2026-02-03 17:20:19 -05:00
johnnyq
747b62d78c Remove extra double quote 2026-02-03 17:09:07 -05:00
johnnyq
af0f3ac25f Update ticket Merge bulk with a dropdown select box to select a ticket instead of entering a number manually 2026-02-03 16:35:20 -05:00
johnnyq
6aefe99c2f Update ticket Merge with a dropdown select box to select a ticket instead of entering a number manually 2026-02-03 16:08:33 -05:00
johnnyq
61c5595a68 Fix Get type on add asset 2026-02-02 16:05:38 -05:00
johnnyq
3d11611699 Attempt to Fix duplicate data entries by fast clicking submit in modals using ajax_modal.js include 2026-01-31 15:14:01 -05:00
johnnyq
38b4ed4b96 Tidy check_login for client portal 2026-01-30 14:24:34 -05:00
johnnyq
34308a5f9a check if archived and active for client login as well along with loading extended client user session vars in /client/includes/check_login.php 2026-01-30 14:10:59 -05:00
johnnyq
118cc10804 During load user session check if user is archived and active, prevents users from making further actions if they are disabled or archived but are still logged in 2026-01-30 13:36:18 -05:00
johnnyq
ae3386f2d5 Fix Vendor phone formatting in Ticket details 2026-01-29 16:36:10 -05:00
johnnyq
bf1ccc62f5 Fix Collected Tax Report, was not adding totals OCT-Dec was misaligned and Grand Total wasnt displaying 2026-01-29 14:02:50 -05:00
johnnyq
1ce4dd932e Updated PL report - changed Gross Profit to Gross Revenue as is more accurate for taxation 2026-01-29 13:55:09 -05:00
johnnyq
ff0bb49926 Tidy up Ticket to Invoice Description 2026-01-29 13:13:24 -05:00
johnnyq
018642cbb8 Fix Ticket View / filtering 2026-01-28 23:33:12 -05:00
johnnyq
bf1d0655c4 Update Ticket list ui smaller badge for ticket and smaller task progress bar and more natural dates 2026-01-28 22:52:56 -05:00
johnnyq
874b07f4d7 More UI/UX updated to ticket details shrink padding on right cards, update wordings, changed header paddings, made unassigned a clickable badge no longer red, swapped trash cans for times icons, much cleaner overall 2026-01-28 22:06:50 -05:00
johnnyq
19fa210743 Merge branch 'develop' of github.com:itflow-org/itflow into develop 2026-01-28 18:16:54 -05:00
johnnyq
c8a6513d1c Fix new times on new date time displays in ticket details 2026-01-28 18:16:38 -05:00
wrongecho
01288c8452 Merge branch 'develop' of https://github.com/itflow-org/itflow into develop 2026-01-28 23:10:54 +00:00
johnnyq
a60d3bb3a0 More Ticket Details UI/UX Sprucing better label naming etc 2026-01-28 17:41:14 -05:00
johnnyq
0470159c55 Added function formatDuration to format times like worked etc, updated Time worked in Ticket details to use the new function and some other minor ui/ux cleanups in ticket details 2026-01-28 16:08:39 -05:00
wrongecho
e1a93035fd Undo client scoping uses = instead of LIKE in tickets resolve.php --- see forum thread 2667 2026-01-28 14:57:28 +00:00
wrongecho
c0b7a26905 API bugfixes before release:
- apiEncryptLoginEntry should be apiEncryptCredentialEntry
- Client scoping uses = instead of LIKE in tickets resolve.php
2026-01-28 14:48:41 +00:00
johnnyq
704d770ec2 Fix Icon to Chevron down if Expand all folders is clicked 2026-01-27 16:32:17 -05:00
johnnyq
d42d2d99b1 Remember folders expanded by Passing folders_expanded var in breadcrumb links and search in files 2026-01-27 16:27:04 -05:00
johnnyq
ebfcc15927 Feature: File Folders are collapsed by default with a global option to exapand all folders or expand individual folder by click 2026-01-27 16:15:18 -05:00
johnnyq
4693aa3c93 Render root in display_folders function in files instead of it being seperate 2026-01-27 15:25:20 -05:00
wrongecho
07df657848 Bugfix: newline char showing in parsed emails 2026-01-27 17:25:53 +00:00
wrongecho
ef9436a1fb Bugfix: Ticket edit modal not setting assigned agent 2026-01-27 12:32:41 +00:00
johnnyq
9318f42ce0 Remove Folder option in edit document modal as this is reserved for move to folder modal 2026-01-24 01:37:00 -05:00
johnnyq
85c37d78a8 Do not update document_updated_at field if document is moved, toggle visibity, renamed, archived or restored same with bulk 2026-01-24 01:27:50 -05:00
johnnyq
ff02ab0cea Show Document Updated at instead of created at. This fixes where it should show the initial document creation date instead of the most updated date, also renamed uploaded column to updated 2026-01-23 13:06:06 -05:00
johnnyq
bd8bf026f0 Allow svg email attachemment for mail parser 2026-01-23 11:24:26 -05:00
johnnyq
644dc95380 Merge branch 'develop' of github.com:itflow-org/itflow into develop 2026-01-21 12:40:43 -05:00
johnnyq
f4935efed3 Remove deprecated quick adds to the client overview 2026-01-21 12:40:30 -05:00
wrongecho
1712d34846 Kanban - move to v2 (rewritten style) 2026-01-21 13:15:23 +00:00
johnnyq
f0f1134be6 Fix phone number formatting is both asset details page and modal 2026-01-19 17:26:40 -05:00
johnnyq
e0f7460e08 If not client_id then do not include &client_id= in ticket uris as this would cause ticket viewing to break if client_id is 0. 2026-01-19 17:01:38 -05:00
johnnyq
ae1d71dcd7 append client_id to the uri for agent ticket links so that the when clicked will take them to the client section and show client header along with the bread crumbs link of client tickets 2026-01-19 16:24:31 -05:00
johnnyq
3cfe38c948 updated UI for contact details so menu is on the left instead of top. Also load the first tab based off the count 2026-01-17 17:35:05 -05:00
johnnyq
c1f0b63101 Fix login not passing master key if agent is client and agent and if MFA is enabled 2026-01-17 17:07:22 -05:00
johnnyq
0a658d7cab Fix Document Update API 2026-01-17 13:17:17 -05:00
johnnyq
3ed2582a9b Remove TaxID from packing slip 2026-01-15 14:09:03 -05:00
johnnyq
2ea68776f6 Merge branch 'develop' of github.com:itflow-org/itflow into develop 2026-01-15 13:13:25 -05:00
johnnyq
c1ff22298f Remove xcustom from .gitignore as its been superceded by custom 2026-01-15 13:13:15 -05:00
wrongecho
e94d2f93ea Merge branch 'develop' of https://github.com/itflow-org/itflow into develop 2026-01-15 17:35:35 +00:00
wrongecho
e7f6f8a4c7 Mail Parser - NDR Parsing 2026-01-15 17:35:13 +00:00
johnnyq
4ffe75683b Merge branch 'develop' of github.com:itflow-org/itflow into develop 2026-01-15 12:28:34 -05:00
johnnyq
687920743d Fix user_id not being set in audit logs for agent and client logins, also fix issie where user_id wasnt being set in client portal. 2026-01-15 12:28:20 -05:00
wrongecho
1a93149643 Basic asset history tracking (transfers) - See https://tasks.dev.itflow.org/task_details.php?task_id=19 2026-01-15 14:58:23 +00:00
wrongecho
11d6654763 Basic asset history tracking - See https://tasks.dev.itflow.org/task_details.php?task_id=19 2026-01-15 14:49:41 +00:00
wrongecho
512d65c17a Keep asset PO/ref when copying 2026-01-15 14:22:22 +00:00
wrongecho
5a274061f3 Invoice - add a basic picking/packaging slip 2026-01-15 13:58:53 +00:00
wrongecho
a398ac7a8d Mail Parser - Support flowed text 2026-01-15 11:54:37 +00:00
wrongecho
41df4c4b9f API Keys - Revoke then Delete 2026-01-15 11:37:17 +00:00
wrongecho
7e7909cec1 Mail Parser - Do not automatically send new ticket notifications to noreply/donotreply addresses 2026-01-15 10:38:25 +00:00
wrongecho
7322bd66df Invoices - Show agent names in history 2026-01-15 10:19:56 +00:00
wrongecho
39affa5528 Show trips on main nav for everyone if accounting enabled 2026-01-15 10:04:48 +00:00
wrongecho
374111c88d Add digiBandit as sponsor 2026-01-15 09:28:52 +00:00
wrongecho
72fc0015bf Add digiBandit as sponsor 2026-01-15 09:28:03 +00:00
johnnyq
7ab406b3f5 Convert mysqli_fetch_assoc to mysqli_fetch_row in setup for table import 2026-01-14 18:02:11 -05:00
johnnyq
df280cd574 Fix missing isset in view in tickets.php 2026-01-14 17:36:06 -05:00
johnnyq
0a30300bde replace all instances of mysqli_fetch_array with mysqli_fetch_assoc for better performance and memory usage 2026-01-14 17:30:23 -05:00
johnnyq
cb8b99d6ae up the permissions to added edit Payment 2026-01-12 18:12:20 -05:00
johnnyq
1d3f29d385 added edit Payment 2026-01-12 18:11:41 -05:00
johnnyq
c154113474 Do not add Signature on ticket comment if Public only when reply is emailed 2026-01-12 15:18:22 -05:00
johnnyq
e02a6fc5e6 Use mysqli_fetch_assoc in login 2026-01-12 12:30:12 -05:00
johnnyq
78040573d1 Fix Remove Powered by ITFlow on Login Screen when WhiteLabel is enabled 2026-01-12 12:27:44 -05:00
wrongecho
981f9ace04 TinyMCE - add all options to mobile (we may regret this) 2026-01-12 16:12:06 +00:00
wrongecho
208f7ac8f0 Tickets Kanban
- Keep the selected view (kanban/list) when applying other filters
- Allow filtering tickets by project (main and kanban v2)
2026-01-12 16:11:36 +00:00
wrongecho
661f8db10b Tickets Kanban
- Keep the selected view (kanban/list) when applying other filters
- Allow filtering tickets by project (main and kanban v2)
2026-01-12 16:04:02 +00:00
wrongecho
b48168ffec Rewrite kanban in more procedural code to match existing codebase (as a v2, delete old and rename once we're happy) 2026-01-12 15:36:06 +00:00
wrongecho
908ebb46d9 Merge pull request #1258 from itflow-org/ticket-task-approvals
Ticket task approvals
2026-01-12 12:27:24 +00:00
wrongecho
58bcb38617 Add task approval system (tidy) 2026-01-12 12:21:10 +00:00
johnnyq
1de023f9df Fix Role Archiving, Roles can only be archived if no users are assigned to the role 2026-01-11 16:46:53 -05:00
johnnyq
5815ef2f75 Added bulk delete, archive and restore and invidual file/document restore functions to files 2026-01-10 20:38:04 -05:00
johnnyq
e5dab8b1ca Update files to use mysqli_fetch_assoc for better performance and memory optimization. Add title for List View and Grid View 2026-01-10 19:12:22 -05:00
johnnyq
56c4b7fbe6 Add show archived logic to files 2026-01-10 19:03:28 -05:00
johnnyq
365b65e5b2 fix Bulk Delete Documents and Files 2026-01-10 16:27:12 -05:00
johnnyq
2193cd8d3e switched from mysqli_fetch_array to mysqli_fetch_assoc in client modals 2026-01-10 16:09:03 -05:00
johnnyq
8b221bc055 switched from mysqli_fetch_array to mysqli_fetch_assoc in clients listing page. This will have a major query speed and memory optimization impact 2026-01-10 15:54:19 -05:00
wrongecho
77e4d2b566 Add task approval system 2026-01-09 17:14:44 +00:00
wrongecho
88a29b7599 Bugfix: Mail queue loop not sending invoices to all billing contacts 2026-01-09 16:56:11 +00:00
wrongecho
64525750b6 Fix readme demo creds 2026-01-09 13:56:29 +00:00
wrongecho
30499123f1 Bugfix: Portal not showing contact user id in session 2026-01-09 13:50:46 +00:00
johnnyq
79703042ff Update client table responsiveness viewpoint to fix issue when window is a certain width on the desktop 2025-12-30 16:58:44 -05:00
johnnyq
ccd5605d97 Remove unused code 2025-12-29 18:03:53 -05:00
johnnyq
908277065b Fix Ticket Template auto filling for v1 ticket creation 2025-12-29 17:47:24 -05:00
johnnyq
f2d4eb0486 Fix Ticket Template auto filling 2025-12-29 17:46:23 -05:00
johnnyq
f784b659e8 Remove extra agent client wording 2025-12-20 15:05:47 -05:00
johnnyq
e60a7a59f9 Fix Login flow where user agent and client exists and agent has MFA but will not let them continue, also update some wording and button colors. Also dont show email password fields again after success and login as agent and client is shown. 2025-12-20 14:30:57 -05:00
johnnyq
cab81ca170 Fix Billable sort 2025-12-18 20:39:47 -05:00
johnnyq
a82e2c7ea1 Billable and non billable status use icons check and minus 2025-12-18 20:38:15 -05:00
johnnyq
a277380441 Set API key back to 32 Chars 2025-12-18 20:03:33 -05:00
johnnyq
ad5710b1d8 Fix Invoice CSV Exporting 2025-12-18 20:00:56 -05:00
johnnyq
3e3531a6ce Set API Key to 32 Chars 2025-12-18 14:28:24 -05:00
johnnyq
a79ce23ae5 Fix randomString() to generate cryptographically secure URL-safe tokens, reduced url keys to 32 Characters for performance and easy copy and paste and compatibility while still mainitaining ubreakable cryptographic keys 2025-12-18 14:24:53 -05:00
johnnyq
32f996d034 If login key is set and it is not provided show Client Email instead of just Email for placeholder 2025-12-14 13:42:38 -05:00
johnnyq
312eb4dffc Allow use of login key only for agents 2025-12-14 13:16:54 -05:00
johnnyq
1916456c84 Fix White Label not displaying on the login page 2025-12-14 13:04:53 -05:00
johnnyq
9b8d37b577 Updated changelog 2025-12-13 15:47:28 -05:00
johnnyq
05018e5f17 Added Reset favicon 2025-12-13 15:45:02 -05:00
johnnyq
72ef918452 Update changelog and bump app version to 25.12.1 2025-12-12 16:51:34 -05:00
johnnyq
27fde82aff Fixed Adding Payment provider not adding an account, now adding you can customize the income/expense account, expense category, and Expense vendor. Moved Saved Payment Provider Methods into Payment Providers as a link instead of on the admin side nav. Same with AI Provider and AI Models. 2025-12-12 16:42:09 -05:00
johnnyq
b27ffe6635 Refine DB Helpers 2025-12-10 18:32:46 -05:00
johnnyq
84cc4a094a Add DB helpers to make MySQLi Prepared statements less bloated and require less code 2025-12-10 17:09:34 -05:00
johnnyq
e75600ee05 Fix New ticket button in contact details in the related tickets section 2025-12-10 13:18:41 -05:00
johnnyq
871ad2ea7e Update legacy client login links to use the new unified login method 2025-12-10 12:21:12 -05:00
wrongecho
8b5f2e0f3f Update + credit 2025-12-10 08:54:24 +00:00
johnnyq
58d6ab7342 Unify Agent and Client login, if same user exists as a client and an agent then offer a selection of client portal or agent portal 2025-12-09 13:39:16 -05:00
johnnyq
ca5fb2e010 refined php-xml ext notice 2025-12-06 13:46:09 -05:00
johnnyq
da561b296e Added php-xml as a requirement 2025-12-06 13:38:40 -05:00
johnnyq
523da0dea0 Added a few more things to the changelog 2025-12-06 12:35:20 -05:00
johnnyq
0e4cc76a84 Bump Version for 25.12 updated changelog 2025-12-06 12:18:11 -05:00
johnnyq
7e39a7ed89 Merge branch 'develop' of github.com:itflow-org/itflow into develop 2025-12-05 12:46:52 -05:00
johnnyq
4a26ea7ed9 Hide Permission Modules on sidenav admin menu as this is not ready for custom modules 2025-12-05 12:46:44 -05:00
wrongecho
7c83ba15b9 Mail queue - minor comment syntax error 2025-12-03 15:43:55 +00:00
wrongecho
10bfbed4bb Mail queue - introduce a --no-mx-validation flag to bypass recipient domain MX validation 2025-12-03 15:30:26 +00:00
wrongecho
81550bd7a8 Ticket merge input - strip text 2025-12-03 15:13:52 +00:00
johnnyq
a430bb917e Add CSV Escaping to the Sample Export CSV Files 2025-12-02 15:18:20 -05:00
johnnyq
e1a579387f Convert all Import / Export Modals to Ajax and a few other lingering modals that were not converted yet 2025-12-02 15:04:05 -05:00
johnnyq
fe8df66c67 Migrate Import/export clients to ajax-modals 2025-12-01 20:33:01 -05:00
johnnyq
5bb410f80c Converted all Document Link Modals to the new ajax-modal, also switched trash icons for unlink icons in document details 2025-12-01 17:16:59 -05:00
johnnyq
29b79b9d4e Bump TinyMCE from 8.2.0 to 8.2.2 2025-11-28 17:37:38 -05:00
johnnyq
0f8a8d1464 Bump TCPDF from 6.10.0 to 6.10.1 2025-11-28 17:34:16 -05:00
johnnyq
cc92a4b7ee Bump stripe-php from 18.1.0 to 19.0.0 2025-11-28 17:31:04 -05:00
johnnyq
3ffef6df51 Remove library phpMimeParser as its no longer needed and php-imap webklex is not doing this function 2025-11-28 17:27:05 -05:00
johnnyq
78e4787b99 Bump PHPMailer from 7.0.0 to 7.0.1 2025-11-28 17:24:12 -05:00
johnnyq
540512a156 remove folder location vars as no longer needed as files and documents have been merged 2025-11-28 15:26:26 -05:00
johnnyq
7737dbc65d Migrate Invoice to Recurring Invoice to an ajax modal 2025-11-28 15:15:01 -05:00
johnnyq
faa94d888d Convert Quote to Invoice to use ajax-modal 2025-11-28 14:44:47 -05:00
johnnyq
99e2487d2b Fix dupe race condition with ticket, invoice, quote, project, recurring ticket numbering when being created in parallel Atomically update and get the next ticket number in one SQL query everywhere. 2025-11-28 14:14:46 -05:00
johnnyq
f09d8ffe05 Fix dupe ticket numbering when being created in parallel Atomically update and get the next ticket number in one SQL query. 2025-11-28 13:07:56 -05:00
johnnyq
c486e3fe62 Conver the what should be the last bulk asset interfaces modals to the new ajax 2025-11-27 14:13:33 -05:00
johnnyq
ba2d6b6709 Convert Bulk Edit Product Category modal to ajax 2025-11-27 13:41:53 -05:00
johnnyq
a388a279bc Convert Bulk Edit Product Category modal to ajax 2025-11-27 13:40:09 -05:00
johnnyq
5a64b19a06 Convert Document from Template modal to ajax 2025-11-27 13:17:14 -05:00
johnnyq
53178b8d20 Updated bulk action js to pass the checkboxe names into the get array this allows the use of multiple checkbox name arrays to be passed at once instead of just selected_ids had to update each bulk model from selected_ids to to the array that was passed. This was important so we could mix files and documents together 2025-11-27 12:48:59 -05:00
5377 changed files with 259038 additions and 203839 deletions

20
.editorconfig Normal file
View File

@@ -0,0 +1,20 @@
# Editor defaults for ITFlow - see CONTRIBUTING.md ("Style")
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 4
[*.md]
trim_trailing_whitespace = false
# Vendored - do not reformat
[libs/**]
indent_style = unset
indent_size = unset
trim_trailing_whitespace = false
insert_final_newline = false

44
.gitattributes vendored Normal file
View File

@@ -0,0 +1,44 @@
# ITFlow line-ending policy
#
# Everything ITFlow ships is LF in the repository and LF in the working tree.
# Contributors on Windows get LF too - this is deliberate. ITFlow is deployed
# to Linux/Apache and edited over sftp/ssh as often as it is cloned, so a
# checkout must be byte-identical everywhere.
* text=auto eol=lf
# Explicit for the file types we author, so nothing depends on git's guess.
*.php text eol=lf
*.js text eol=lf
*.css text eol=lf
*.html text eol=lf
*.sql text eol=lf
*.md text eol=lf
*.json text eol=lf
*.yml text eol=lf
*.xsd text eol=lf
*.svg text eol=lf
*.txt text eol=lf
*.ini text eol=lf
.htaccess text eol=lf
# Binary assets: never touched, never diffed as text.
*.png binary
*.gif binary
*.jpg binary
*.jpeg binary
*.webp binary
*.ico binary
*.icc binary
*.woff binary
*.woff2 binary
*.ttf binary
*.eot binary
*.crt binary
*.ser binary
*.z binary
# Vendored third-party code is preserved byte-for-byte as shipped upstream.
# Per CONTRIBUTING.md libs/ is never edited in place - it is replaced wholesale -
# so normalizing it here would create spurious diffs on the next library update.
libs/** -text

10
.gitignore vendored
View File

@@ -20,6 +20,10 @@ uploads/users/*
!uploads/users/index.php
uploads/tmp/*
!uploads/tmp/index.php
!uploads/tmp/.htaccess
uploads/backups/*
!uploads/backups/index.php
!uploads/backups/.htaccess
uploads/tickets/*
!uploads/tickets/index.php
uploads/ticket_templates/*
@@ -32,10 +36,6 @@ plugins/htmlpurifier/standalone/HTMLPurifier/DefinitionCache/Serializer/URI/*
plugins/htmlpurifier/standalone/HTMLPurifier/DefinitionCache/Serializer/CSS/*
!plugins/htmlpurifier/standalone/HTMLPurifier/DefinitionCache/Serializer/CSS/.gitkeep
.vscode/settings.json
xcustom/*
!xcustom/readme.php
post/xcustom
!post/xcustom/readme.php
admin/custom/*
!admin/custom/readme.php
agent/custom/*
@@ -54,4 +54,4 @@ api/v1/custom/*
!api/v1/custom/readme.php
.zed
*.patch

View File

@@ -1,2 +1,2 @@
# Prevent access to .git, .github, and config.php
RedirectMatch 404 ^/(\.git|\.github|config\.php)
RedirectMatch 401 ^/(\.git|\.github|config\.php)

View File

@@ -2,6 +2,444 @@
This file documents all notable changes made to ITFlow.
## [26.08.2] Maint Release
### Upgrading to 26.08.2
Update the files from Settings > Update as normal. There is no database change in this release, so nothing else is required.
### Bug Fixes
- Calendar: fixed the agent calendar showing no events.
- Calendar: shared calendar feeds set to publish busy blocks only were publishing full event titles, locations and descriptions to anyone holding the subscription link.
- Cron: fixed Maintenance > Cron failing to load. Scheduled jobs themselves were unaffected and kept running.
- Exports: restored the missing columns on the ticket, quote, recurring invoice, software and user exports.
- API: restored the full record on the credentials list endpoint.
- Mail: switching an existing install from Standard SMTP/IMAP to Microsoft 365 or Google OAuth no longer leaves the old mail server behind, which stopped sending and ticket email fetching from working. The connection settings for OAuth providers are now fixed by the provider and cleared on save.
## [26.08.1] Maint Release
### Upgrading to 26.08.1
Update the files from Settings > Update as normal. This release moves the database to 2.6.7 and the web updater completes it for you — the command line step that 26.08 required is not needed again.
### Breaking Changes and Notes
- Client access: agents with restricted client access now see records that have no client assigned. Previously this varied by page — unassigned tickets and projects were visible, unassigned expenses and credentials were not. It is now consistent everywhere.
### Bug Fixes
- Setup: fixed the wizard closing itself after the first user, which left new 26.08 installs stuck in a redirect loop between `/setup` and `/login.php`.
- API: tightened client scoping on the expense read and record update endpoints.
- Income: revenue rows now respect restricted client access.
- Client PDF Export: fixed the export producing a CSV file, and each section is now gated on the module that owns it.
- AI: fixed model creation, per-use-case model selection, configurable temperature, and error reporting.
- Ticket: system-generated replies no longer record time worked that was never worked.
- Ticket: fixed an error when scheduling a ticket.
- Ticket: cancelling a schedule now cancels the calendar event on the recipient's calendar.
- Ticket: history no longer records a status change when the status did not change.
- Recurring Ticket: bulk priority changes no longer deny access to agents who are not administrators.
- Contact: deleting a contact now removes the linked portal user, and anonymizing now redacts the phone number.
- Calendar: fixed event deletion.
### New Features & Updates
- Performance: queries now select only the columns they use instead of `SELECT *`, cutting memory use and query time across the app and especially in the crons.
- Performance: removed client joins that were only there for scoping — side nav badge counts are significantly faster.
- Client scoping: added a `clientScopeSql()` helper so list queries scope on the owning column instead of a joined `clients.client_id`.
- Contributing: documented the column-selection and client-scoping conventions.
## [26.08]
### Upgrading to 26.08
> **Read this before you start.** Done out of order this update will break your instance. The database structure changes, every API key is deleted, and the whole cron setup is replaced.
1. **Back everything up.** Take a full VM backup or snapshot before you start.
2. **Remove every ITFlow line from your crontab** (or delete `/etc/cron.d/itflow`). The old per-minute jobs must not keep firing against a half-updated install. You put the new one in at step 5.
3. **Update the files with the normal web updater**, from Settings > Update. It will pull the new files, report the update as successful, and then error out as it drops you back into the app. That is normal — the new code is now running against the old database.
4. **Run the database update from the command line.** Just this once it cannot be done from the web interface. Run it as the user that owns the ITFlow files — the script tells you which user if you get it wrong:
```bash
sudo -u www-data php /path/to/itflow/scripts/update_cli.php --update_db
```
It applies every pending version in order and reports each one as it goes. On an install with a lot of ticket history it can take a minute or more, so let it finish. If a step fails it stops there without advancing the recorded version, so you can fix the problem and run it again. The 500s stop as soon as it completes.
5. **Add the new cron entry.** One line runs everything now, and the schedules are managed in ITFlow under Settings > Cron:
```
* * * * * www-data php /path/to/itflow/cron/cron.php >/dev/null
```
Drop the `www-data` column if this goes in a user crontab rather than `/etc/cron.d`.
6. **Recreate your API keys.** Every existing key is deleted by this update. Issue new ones and update anything that talks to the ITFlow API.
7. **Check it took.** Open Settings > Cron — the green "Cron last checked in" banner should appear within a couple of minutes and every job should pick up a schedule.
Only this release needs the command line for the database update. Normal updates go back to running from Settings > Update as usual.
### Breaking Changes and Notes
- Cron: the crontab collapses to a single entry. `cron/cron.php` is now a dispatcher that runs every minute and works out which jobs in `cron/` are due, and the old nightly work has moved to `cron/nightly_tasks.php` which it runs at 03:00.
- Cron: an existing crontab keeps working — the per-minute scripts still run and still lock correctly, and a single daily `cron.php` entry still runs the daily jobs — but any job added in this or a future release only runs once the dispatcher is scheduled.
- Cron: ticket SLAs need no entry of their own. `cron/ticket_sla.php` is in the dispatcher's job list and runs every minute once the new entry is in place. Without it SLA targets are still worked out and displayed, but warnings and breaches never fire.
- Backups: write down your backup encryption key. It is generated on first use and stored in `config.php`, never in the database, and without it a backup cannot be restored.
- API: every existing key is deleted by this update and must be recreated. Keys are now owned by a user and inherit that user's role, module and client permissions instead of carrying their own client scope, and existing keys cannot be safely mapped to a user.
- API: credential decrypt passwords are now read from the request body instead of the query string. Any caller passing that value in the URL needs updating.
- Client access permissions now support deny rules as well as allow, and the permissions UI will not load until the database update has run.
- Business hours are new and default to Monday to Friday, 09:00 to 17:00 in your configured timezone. SLA targets are measured against them, so set them before assigning SLAs.
- The `plugins` directory is now `libs`. Anything pointing at `plugins/` directly — custom scripts, reverse proxy rules, web server config — needs updating.
- Several pages dropped the `_details` suffix and moved to consistent singular and plural filenames, so old bookmarks and external links will 404.
- Credential passwords moved from `varbinary` to `varchar(500)` and now have a length guard. Existing credentials are migrated by the database update.
### New Features & Updates
- Backups are now encrypted, catalogued, schedulable and restorable from the command line. Three types — Full (database and uploads), Database Only, and Master Key — and every archive is an AES-256 encrypted zip. Open them with 7-Zip, WinZip, PeaZip or Keka; `unzip`, Windows Explorer and the macOS Archive Utility do not support AES.
- Backups: one encryption key per install, generated on first use and stored in `config.php`, never in the database and never in the file name. It is shown in Maintenance > Backup.
- Backups are built by cron rather than by your browser. The button queues the work and the dispatcher picks it up within the minute, then notifies you — a dump of a real install takes longer than a web request is allowed to live, which is why the old Download Backup button timed out on large instances.
- Backups: scheduled backups are a new `backup` cron job, off by default, turned on in Maintenance > Cron. Retention by age and by count runs in the nightly job and never deletes the newest backup.
- Backups are stored outside the web-served path under `uploads/backups/` with a deny-all rule and downloaded through an admin-only handler. Set `$config_backup_path` in `config.php` to keep them off the web root entirely.
- Backups: restore from the command line with `php scripts/restore_cli.php --file=/path/to/backup.zip`. This is the only restore path with no size limit — the setup wizard's restore is capped by PHP's upload limits and a full backup is usually larger. `--inspect` checks an archive without changing anything.
- Backups: restores validate before they destroy. The key is checked and the archive unpacked before any table is dropped, and the current database is dumped first and put back automatically if the import fails.
- Cron: one entry instead of five, and a page to manage it. Jobs are tracked in a new `cron_jobs` table, so a job whose slot was missed runs at the next opportunity rather than waiting a day, and each job locks for its own run so a slow mailbox or a long nightly pass no longer holds anything else up.
- Cron: new Settings > Cron page listing every job with its schedule, last run, duration, outcome and next due time. Jobs can be disabled, rescheduled, or run on demand — Run Now hands the job to the next dispatch so it starts within a minute and still runs on the command line. The last error is kept until dismissed rather than vanishing behind the next success, and the page says plainly when the crontab entry itself is missing.
- Cron: the nightly run is safe to repeat. Late fees, overdue invoice reminders and autopay retries now apply at most once per invoice per day, so a Run Now after the scheduled pass no longer stacks fees or re-emails clients. Nightly Tasks only accepts the daily schedule.
- Cron: the master enable switch moved out of Notifications and into the Cron settings where it belongs, and the unused overdue invoice setting has been removed.
- Ticket: major UI overhaul of the ticket list, the kanban board and the ticket detail page.
- Ticket: redesigned the task bar on ticket details and removed the redundant task count.
- Ticket SLAs, optional throughout. An SLA sets a response target and an optional resolution target, assigned per client and priority with a global default and an explicit "no SLA" override. Targets are measured against your business hours. Tickets show time remaining and turn yellow at a configurable warning threshold and red on breach, on both the ticket list and the kanban board, and can be filtered by SLA state. Nominated statuses pause the resolution clock for "waiting on customer", preserving the remaining budget. Two new reports, SLA Summary and SLA by Client. With no assignments defined nothing behaves any differently.
- Ticket: added an Urgent priority.
- Ticket: agents can attach files to tickets from inside the app, both when raising a ticket and on a reply, and attachments are emailed to the contact through the mail queue. A 10 MB ceiling applies per message; anything that does not fit stays on the ticket to download.
- Ticket: tasks can be added and edited inline in the add ticket and add recurring ticket modals.
- Ticket: the older add ticket modal has been retired, there is one add ticket modal now.
- Ticket: watchers and attachments have moved into the assignment section of the add ticket modal.
- Ticket: recurring tickets can be assigned a ticket template. Picking one fills in the subject and details and stamps the template's task list onto every ticket the schedule raises, from the nightly run and a forced run alike. The recurring ticket list shows which schedules carry a template and how many tasks it adds.
- Ticket: recurring tickets now own their task list. The template fills it in when picked but it can then be edited per schedule, and it is those edits the run reads. Existing schedules are backfilled from their template by the database update.
- API: added ticket reply endpoints for creating and reading replies.
- API: added an invoice_items endpoint for adding line items to an invoice.
- Calendar: calendars can be published as a read-only iCalendar (ICS) subscription feed and read by Google Calendar, Nextcloud, Apple Calendar, Thunderbird or anything else that takes a feed URL. The link carries a secret key and needs no login, can be regenerated or revoked at any time, and a busy only option publishes time blocks without titles, descriptions or locations. Refresh timing belongs to the subscribing client — Google refreshes on its own schedule and cannot be forced, and Nextcloud defaults to weekly and refuses feed URLs resolving to private IPs.
- Calendar: events can be marked all day, and the date and time are now separate fields. Previously all day was inferred from a midnight start, which made a genuine midnight appointment indistinguishable from an all-day event. Existing events are backfilled by the database update using the old rule, so nothing changes appearance.
- Calendar: repeating events now work. The Repeat field was present but disabled and the stored value was never drawn. It is now selectable daily, weekly, monthly or yearly, and monthly and yearly series skip dates that do not exist in a period rather than sliding into the next month. Recurrence is series-wide — editing any occurrence edits the whole series, and individual occurrences cannot yet be moved or cancelled. Repeating events are marked with an icon and a hover note, and the delete action reads Delete series and asks for confirmation.
- Calendar: clicking empty space creates an event there. Clicking a day or a time slot, or dragging across several, opens the New Event modal with the start and end already filled in and the All day switch set to match. A range dragged out or lengthened by hand is no longer overwritten by the end-time-follows-start behaviour.
- Exports: every export modal now has a Filter tab and a Selectable Columns tab with sensible defaults, and can export to PDF as well as CSV.
- Combined Payments and Revenues into a single Income page with CSV export. Revenue not tied to an invoice is still added there and payments are still added from invoices. The standalone Payments and Revenues pages are gone.
- Income: added a Category column and filter, carried through to the exports.
- Income: added bulk actions for account, payment method and category.
- New Transactions page — a per-account ledger of transfers, revenues, payments and expenses with filtering by type, category, client, payment method, amount range and date, a running balance, summary cards, account balances in the account picker, and CSV export.
- Products: added a basic product import via CSV.
- Added user based RBAC for API keys, so a key runs as a user and inherits that user's permissions.
- Added deny rules to client access permissions, so access can be granted broadly and revoked for specific clients.
- New secure file download handler for files and ticket attachments, with client and contact permission isolation on the client portal.
- Invoices: clicking the Paid or Partial status badge opens a read-only breakdown of the payments recorded against that invoice.
- Assets: multiple notes per asset, same as contact notes, with categorized note types (Maintenance, Repair, Configuration, Upgrade, Inspection, Note).
- Reworked the Maintenance > Update page, and fixed the branch handling on it.
- Categories and tag types moved from a top button nav to a left side nav.
- Dashboard: added expiring asset warranties and licenses, along with an "Expiring in" filter for assets, licenses, domains and certificates.
- Added bulk and single refresh actions for domains and certificates.
- Stripe gateway fees now come from the actual Stripe balance transaction rather than a static percentage and flat fee configured in ITFlow, with a nightly pass to backfill fees that were not available at payment time. The static fee fields are gone from payment provider settings.
- Database updates are now split into per-version files under `admin/database_updates/`, the latest version is derived from the directory listing, and one run applies everything pending. Migration history before 2.0.0 has been pruned.
- Reorganized the main, client, admin and reports side navigation menus.
- Mail settings tabs are now URL addressable and stay on the active tab after saving.
- Removed the legacy vendor contacts feature.
- `dig` and `whois` are no longer required, domain lookups use native DNS and RDAP.
- Tightened the `.htaccess` rules, and added one for `uploads/tmp`.
- Bumped the minimum supported PHP version.
### Security
- Rate limited 2FA code attempts and narrowed the TOTP acceptance window.
- Rotate the session ID on login to prevent session fixation.
- Stopped parallel login attempts from bypassing the login rate limits.
- Tightened validation on the recurring invoice frequency used by the billing run.
- Admin UI modals are now gated to admins. Previously any logged-in user could open them directly and read stored payment provider and AI provider API keys — rotate those keys when you update, as there is no record of who may have viewed them.
- Global search returned credentials to users without credential module access. It is now gated like every other credential surface.
- Credential password reveals are now written to the audit log, on both the reveal endpoint and the TOTP code.
- Swept module and client permission enforcement across modals and ajax endpoints to match the post handlers, closing a number of cases where a user restricted to certain clients could read another client's records by ID.
- Products: the CSV export now requires sales module read access.
- Neutralized CSV formula injection in generated exports.
- Fixed weak random number generation in TOTP secret generation.
- Shared item views are now claimed atomically so the view limit cannot be exceeded by simultaneous requests, and guest audit IPs are logged.
- Hardened CSRF handling and session cookies, and set `SameSite=Lax` on the session cookie.
- Hardened file upload handling to use random storage names.
- Client Portal: contacts can no longer edit their own contact record.
- The setup wizard's restore step is now closed on any install that has users, whatever `config.php` says. Previously, if `$config_enable_setup` was missing from `config.php` — a state an install can be left in when setup does not reach its final step — the restore step stayed reachable. Restoring over a live install is now done from the command line.
- A restore no longer takes ITFlow's `uploads/.htaccess` from the archive. The guards are rewritten afterwards regardless of what the backup contained, so restoring a backup taken before those guards existed no longer removes them.
- Tightened the directory guards under `uploads`.
### Bug Fixes
- Deleting a payment now correctly recalculates and sets the invoice status.
- Fixed contact notes, and several broken modal links in contacts, assets and file linking.
- Client Portal: fixed adding saved payment methods and cards following a Stripe API change.
- Fixed sending invoices and quotes over OAUTH2, which was reading the SMTP host instead of the SMTP provider — the host is not filled in when OAUTH2 is selected.
- Mail Parser: correctly work out whether the ITFlow folder belongs under the `INBOX` namespace or the root directory, fixing folder creation on cPanel Dovecot Maildir++ setups.
- Fixed possible duplicate emails caused by a race condition in the mail queue.
- Added a shared lock guard across every cron entry point, scoped per script and per install, replacing the mail queue's non-atomic lock file. Rows left in a sending state by a run that died are now recovered.
- Prevented duplicate Stripe payment bookings and overlapping cron runs.
- Mail bodies are cleared after successful delivery.
- Reworked `getFieldById` to stop escaping its return value, and reworked every caller to escape at the point of use — it was causing double escaping in a lot of places.
- Added missing `maxlength` attributes to forms backed by length-limited columns, so an overlong value no longer throws a 500.
- Fixed undefined variables in the audit log and flash messages for expenses, assets, contacts and several other handlers, which were logging blanks in place of the record name.
- Fixed the spelling of the expense description in audit logging.
- Fixed autofill on invoices, quotes and recurring invoices, where the tax field was not updating and a dash was being placed in front of the product.
- Fixed gaps in ticket history.
- Recurring Expense: fixed editing not keeping the client.
- Fixed client name truncation in the side navigation being applied after escaping.
- Side navigation counts are only shown to users with permission to see them.
- Invoice statistics now only reflect clients the user has permission to see.
- The agent category handler no longer drops the category description.
- Expenses: allowed negative amounts, and the current date is now prefilled.
- Certificates can now be searched by description.
- Fixed cents calculation rounding.
- Fixed guest view credential TOTP display, and removed the legacy OTP code path.
- Gated the SLA option in ticket details, which was gated everywhere else.
- Deleting a ticket template task or a payment provider recorded the wrong name in the audit log and the confirmation message, reading an unrelated record's id in place of the name.
- Bulk-creating tickets from a template against multiple assets only added the template's tasks to the first ticket, and dropped each task's completion estimate.
- Deleting a ticket template now unlinks it from any recurring ticket that referenced it, instead of leaving the schedule pointing at a template that no longer exists.
- Fixed the asset section in the recurring ticket modal when opened outside a client, and project selection when raising a ticket.
- Tickets raised by the nightly recurring schedule were created without a guest URL key, so the "View ticket" link in reply and task approval emails could not be opened. Cron now generates a key like every other path that raises a ticket, and existing tickets missing one are backfilled by the database update.
- Calendar: fixed the last day of a multi-day all-day event not being drawn or published to subscribed feeds. `event_end` holds the last day the event covers, which is what the event modal asks for, but FullCalendar and iCalendar both treat an all-day end as exclusive.
- Fixed `confirm-link` doing nothing inside an ajax modal, where the handler was only bound to links present at page load.
### Developer Updates
- Line endings normalized to LF across the codebase, with `.gitattributes` and `.editorconfig` added. Vendored code under `libs/` is marked so it stays byte identical to upstream.
- Converted to the short echo tag `<?=` throughout.
- `functions.php` is now a loader, with helpers split into topical files under `functions/`. Unused legacy functions removed, including an unused database wrapper layer.
- PHP functions renamed to camelCase throughout, including `nullable_htmlentities` to `escapeHtml`, `sanitizeInput` to `escapeSql`, `logAction` to `logAudit`, and `key32gen` to `generateTotpSecret`.
- Seed data is now shared between the setup wizard and `setup_cli.php` from one file, so a headless install gets the same starter content as a browser install.
- `CONTRIBUTING.md` added and expanded, covering the security rules, style conventions, database column prefix convention and migration pairing.
### Library Updates
- Bump TinyMCE from 8.6.0 to 8.8.2.
- Bump DataTables from 2.3.7 to 3.0.1.
- Bump FullCalendar from 7.0.0 to 7.0.2.
- Bump ImapEngine from 1.25.0 to 1.25.4, along with its dependencies — notably zbateson/mail-mime-parser 3.0.6 to 4.0.3 and guzzlehttp/psr7 2.12.3 to 3.0.0.
## [26.07.1]
### Bug fixes
- Fixed broken M365 and Google Workspaces OAUTH2 in Mail Settings.
- Security Fix.
## [26.07]
### Major Changes
- Migrated from Webklex php Library to IMAPEngine.
- Major Rewrite of the mail settings page to better support Microsoft 365 and Google OAUTH2.
### Bug fixes
- Many Security Fixes.
- Microsoft 365 and Google can now specify Licensed User.
- Clients - Only show 3 Tags per line instead of streching all the way across.
- Login: Make Email field email type instead of text.
- Fixed Invoice Late Overdue notices now shows correct balance when late fees are attached and if partial invoice was paid.
- User Preferences Avatar: Fix creating user upload directory if doesn't exist, and remove Avatar now properly deletes the old avatar image.
- Do not send an in-app alert on successful cron execution, keep it in logging only.
- Fix Issues with Clients Signing in via Entra in the client Portal, items were broken because CSRF token was not being generated.
### New Features & Updates
- Clients: Removed Entity Stat Counter was slow and unused.
- Added Monospace text in areas where it deserves it like, IPs, Amounts Costs in Tabular data forms etc.
- Cicking into the client section no longer turns the main nav and text gray, it keeps the configured theme across the app. It was implmented long ago to differentiate between the client section and the main section of ITFLow but didn't work very well.
- Bump Sortablejs from 1.15.6 to 1.15.7.
- Bump TinyMCE from 8.5.0 to 8.6.0.
- Bump Fullcalendar from 6.1.20 to 7.0.0 amd convert existing code to comply with 7.0, also make calendar more printable.
- Bump PHPMailer from 7.0.2 to 7.1.1.
## [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 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.
- Clients: Swapped location and contact column, add PopOver with Details such as created, abbreviation, DB ID instead of taking up space underneath client, rounded tag pills and increased padding, removed info badges and added one info badge that displays a popover with details.
- Clients: Added New Ticket to Client Top Header Menu.
- Clients: Client Overview: UI Sprucing.
- Invoice: Send reminder 1 day after due date.
- Invoices/Quotes/Recurring Invoices: Split Items tables into their own POST logic and Modal UIs and tables (quote_items, invoice_items, recurring_items).
- Tickets: New Ticket Parsing - Anyone CC'ed onto the original email that created the ticket is added as a ticket watcher.
- Ticket/Quotes: Quotes can now be associated with a ticket.
- Networks: Removed Subnet Mask Field, Use CIDR instead.
- Networks: Rearranged fields, Updated placeholders, Add/Edit/list for better flow.
- Networks: Renamed DHCP to IP Range to allow for you use of both DHCP and or Usable IPs.
- Assets: Rearranged fields, Updated placeholders, Add/Edit/list for better flow.
- Assets: Added IPv6 if available under IP, Make and Model are now one line with Serial Underneath. Added OS under Type. use pill for status.
- Calendar: Event thats are cut off can now be viewed as a tooltip on hover.
- Calendar: Renamed System Calendars to built-in calendars and added the names and color dot for reference.
- Calendar: You can now delete a custom calendar.
- Report: Client Ticket Time Detail Audit: Selectable Billing Time Increment, will later be avauilable globally.
- Roles/Permissions: Now complete and is out of beta all permission roles are strictly enforced, except for in Trips and Calendar, new enforce modules will be added for these at a later date.
- Project Templates: Ticket Template order can now be dragged and dropped.
- Global: Introduced new checkbox class to all Checkbox select columns to keep consistency and reduce space and enhance ui.
- Global: CSRF Checks everywhere instead of just deletion calls.
- Global: Renamed the rest of the unarchive post and label calls to restore.
- Files: Allow upload of .unifi extension.
- Bump Libraries:
- stripe-php from 19.0.0 to 19.4.1.
- fullcalendar from 6.1.19 to 6.1.20
- TCPDF from 6.10.1 to 6.11.2
## [26.02.1] Maint Release
### Bug Fixes
- Credentials: Fix Password Generator.
- Calendar: Restrict Events for client restricted agents.
- Ticket Merge: Fix.
- Asset Transfer: Fix.
- Ticket Listing: Restrict Tickets presented in ticket list view from client restricted agents.
- Ticket Details: Deny access to client restricted agents to view tickets without client_id in uri.
- Tickets: Allow agents with restricted client access to view and edit tickets without a client.
- Ticket Change client: Limit selection for agents with restricted client access.
- Ticket Details: Don't display updated at when null.
### New Features & Updates
- Report: Added Client Detail Auditing.
- API: Added Endpoint to retrieve time worked by agent.
- ajax-modal: Revert to previous JS implementation before 26.02 release.
- Ticket: Move Subject from Ticket main ticket header to ticket details card header.
## [26.02] Stable Release
### Bug Fixes
- Mail Parser - Do not automatically send new ticket notifications to noreply/donotreply addresses.
- Ticket: removed newline \n on Parsed emails.
- Show Trips for everyone if accounting module is enabled.
- Fix Invoice Exporting.
- Fix Billable Column not sorting correctly in tickets.
- Fix Login flow where user agent and client user exists and agent has MFA but will not let them continue.
- Fix passing missing user_id var in client portal.
- Fix Ticket Templates not auto filling when selected.
- Fix Invoices not being sent to all billings contacts when manaully sent.
- Fix Documents and Files not able to be bulk deleted.
- Fix Role Archiving, can be archived as long as no users are assigned to the role.
- Fix showing Powered By ITFlow visibility on the login screen when Whitelabel is enabled.
- Missing username in audit log on successful login due to missing passed user_id to logging.
- API: Fix updating all documents instead of the intended document.
- Documents: Fix Document created at not showing the correct creation date of the master document.
- Ticket: Fixed Using edit ticket modal agent was not able to be set.
- Always check if a user is archived and or disabled instead of just during login.
- Report: Fix Collected tax report not totalling all tax categories.
### New Features & Updates
- Task Approval System for ticket tasks: Once an approval is requested, the task cannot be marked as complete until approved. Internal Approvals Any other technician, or Specific technician, Client Approvals Anyone (usually the requestor) Tech contacts Billing contacts.
- Printable Invoice Packing Slips now available.
- Drastic Performance Bump: Up to 50% faster queries accross the board and reduced server memory usage by 40% by switching Database Query method from mysqli_fetch_array to mysqli_fetch_assoc.
- Added Connect to Microsoft 365 Button to mail settings.
- OAUTH2 support for Microsoft 365 and Google Workspaces is now considered stable and working.
- Favorites: Assets and Credentials now can be favorited singly or by Bulk action. Favorited items appear in the client overview now.
- Files/Documents: Collapsable folders feature, collapsed by default with a button to expand all.
- URL Keys and such are now set to a more manageable 32 Characters by default.
- Various UI/UX Updates throughout the app, with focus oin ticket details, contact details modal etc.
- Added Show Archived files and documents to the files section.
- Added Bulk Archive and restore options to files and documents.
- Rewrite of the Kanban Ticket view to match our procedural style of coding.
- All options are available in TinyMCE now in Mobile mode.
- Agent names appear now in Invoice History section.
- Mail Parser: Support flowed text.
- Assets: Keep Purchase reference when copying.
- Assets: Add basic tracking history: Archiving, restoring, name changes, transferimg to new clients.
- Mail Parser: NDR Parsing.
- Allow SVG files in mail attachments.
- Tickets: Use a more friendly time worked instead of 02:41:00 translates to 2h 41m.
- Update wording on ticket to invoice item details.
- Merge Tickets: Now wth a ticket merge dropdown list of tickets instead of a text field.
- Role Permissions can now be set during role creation, update Permission UI to use radio buttons instead of select boxes.
- Bump TinyMCE 8.2.2 to 8.3.2.
- Bump PHPMailer from 7.0.1 to 7.0.2.
- Bump Datatables from 2.3.4 to 2.3.7.
## [25.12.1] Maint Release
### Major Changes
- Unified the Client/Agent Login and process (Note only Client Users can Reset passwords from the login page, does not apply to agent users).
### Bug Fixes
- Fix Payment Provider not adding an account.
- Fix New ticket button in contact details in the related tickets section.
### New Features & Updates
- You can now Set Payment Provider income/expense account, expense vendor and expense category upond creation or editing.
- Moved Saved Payment Provider Methods away from admin side nav to the count link within Payment Providers page.
- Moved AI Models from the admin side nav to the model count link within AI Providers.
- Add Favicon Reset.
## [25.12] Stable Release
### Breaking Changes ###
- For Existing installs: **php-xml** extension needs to be installed for document creation and editing, new install script does this for you as of Dec 6th 2025. To install php-xml: `sudo apt install php-xml`
### Major Changes
- Consolidated "Files" and "Documents" into a single section called **Files**.
### Bug Fixes
- Resolved issue with updating asset notes in asset details.
- Fixed problem with bulk ticket merging.
- Corrected issue where decimal inputs (e.g., price, cost) werent displaying on iPhones in certain forms.
- Added CSV escaping to the sample export data in areas where a sample CSV template is provided.
- Fix a race condition where dupe tickets, invoices, recurring invoices, recurring tickets, quotes will be created using the same number if created in parallel espcecially when using the API.
### New Features & Updates
- Introduced automatic subject-based ticket merging/reply detection. Now, if an email comes from a known contact or domain and the subject matches 95% of a ticket opened in the last 7 days, it will be merged automatically.
- Added `cleanInput` function to sanitize data before inserting it into the database when using MySQLi prepared statements.
- Migrated client post functionality to use MySQLi prepared statements.
- Updated payment method post functionality to use MySQLi prepared statements.
- Implemented `saveBase64Images()` to convert base64-encoded `<img>` tags into actual image files stored under `/uploads/<module>/<id>/` with secure filenames. Added wrapper functions, and updated document creation to use processed image paths.
- For new documents and document templates, images are now stored in `/uploads/documents/$document_id` instead of being stored as base64 in the database, using the `saveBase64Images()` function.
- UI/UX improvements made to the document details page.
- Removed sidebar quick-add options.
- Created new folders in the uploads directory: `documents`, `document_templates`, and `recurring_tickets`.
- Reworked the bulk action function to pass the name arrays, instead of a generic `selected_ids` array. This allows multiple bulk name arrays to be passed at once, currently used for the new file-document merge.
- Big task: Converted the remaining modals to use the new `ajax-modal` system, enabling more flexible flow expansion going forward.
- Mail queue: Added a `--no-mx-validation` flag to bypass recipient domain MX validation.
- Bump PHPMailer from 7.0.0 to 7.0.1.
- Bump stripe-php from 18.1.0 to 19.0.0.
- Bump TCPDF from 6.10.0 to 6.10.1.
- Bump TinyMCE from 8.2.0 to 8.2.2.
## [25.11.1] Maint Release
### Fixes
@@ -286,7 +724,7 @@ We will provide example code with directory structure for each custom directory
---
### Fixed
- Several security vulnerabilities patched.
- Several security vulnerabilities patched (with thanks to www.helx.io).
- Ticket status is no longer updated when scheduling.
- Client Portal: Tech contacts can no longer edit their own details.
- Fixed overlapping logo issue in Invoice/Quote PDF exports.

296
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,296 @@
# Contributing to ITFlow
Thanks for your interest in contributing! ITFlow is intentionally simple: plain procedural PHP, MySQL via `mysqli`, and vanilla Bootstrap/AdminLTE. There is no framework, no ORM, no template engine, and no build step. If you can read a PHP file top to bottom, you can read ITFlow.
That simplicity comes with a trade-off: **safety and correctness depend on following conventions at every call site.** This document is the list of those conventions. Read it once, fully, before opening a PR — most review feedback we give is a restatement of something on this page.
---
## Quick start (development)
1. Clone the repo into a webroot served by Apache/PHP 8.x with the `mysqli`.
2. Create a MySQL/MariaDB database and browse to `/setup/` — or import `db.sql` directly.
3. Rename/skip setup as prompted; `config.php` is generated at the root (and is gitignored).
There is no `composer install` or `npm install` step. All third-party libraries are vendored in `/libs/`. This is deliberate — ITFlow is distributed as "unzip and go" — so **never add a runtime Composer/npm dependency**. If a new library is truly needed, discuss it in an issue first; if accepted, it gets vendored into `/libs/`.
---
## Architecture map
| Path | Purpose |
|---|---|
| `agent/` | The main technician-facing app. Most feature work happens here. |
| `admin/` | Settings, configuration, roles, mail, migrations. Admin-only. |
| `client/` | The logged-in client portal (contacts of a client). |
| `guest/` | Unauthenticated flows via URL keys (view/pay invoice, view quote/ticket, view shared credentials/files/documents). |
| `api/v1/` | Key-authenticated JSON CRUD API, one directory per module. |
| `cron/` | Scheduled jobs. `cron.php` is the dispatcher and the only entry in the crontab; everything else in the directory is a job it runs, with `cron/includes/` for the parts only cron uses. See [Cron](#cron). |
| `functions.php` + `functions/` | Shared helper functions, split into topical files (`sanitize.php`, `auth.php`, `logging.php`, …) loaded by `functions.php`. New helpers go in the topical file that matches their concern. |
| `includes/` (root) | **Shared** across portals: session/auth bootstrap, DB, layout partials. |
| `post/` (root) | **Shared** POST handlers (logout, misc). |
| `modals/` (root) | **Shared** modals used by both agent and admin. |
| `js/`, `css/` (root) | Shared front-end assets (portals also have their own). |
| `libs/` | Vendored third-party libraries. Never edit these; update them wholesale. |
| `setup/` | First-run installer. |
| `scripts/` | Helper/utility scripts — `setup_cli.php`, `update_cli.php`, `restore_cli.php`. CLI only; the directory denies web access. |
Rule of thumb: **root-level `includes/`, `post/`, `modals/`, `js/`, `css/` are shared code; everything inside a portal directory is scoped to that portal.**
### `custom/` directories
`agent/`, `admin/`, `client/`, `guest/`, and `cron/` each contain a `custom/` directory. These are hook points for site-specific code that survives updates. `triggerCustomAction($trigger, $entity_id)` fires named triggers (e.g. `ticket_resolve`) into `custom/custom_action_handler.php` if one exists. Core code should **call** `triggerCustomAction()` at meaningful events but never depend on anything inside `custom/`.
---
## Request lifecycle (how a page works)
**Read pages** (`agent/tickets.php`, etc.) start by requiring an `inc_all*.php` from the portal's `includes/`. That chain loads, in order: `config.php``functions.php` (a loader that pulls in the topical helper files under `functions/` — security, sanitize, auth, logging, etc.) → `check_login.php` (auth) → header/nav/layout partials. It also establishes the implicit globals every page relies on: `$mysqli`, `$session_user_id`, `$session_name`, and — on client-scoped pages via `inc_all_client.php``$client_id` (already `intval()`'d).
If your code "can't find" a variable, check which include chain the page uses before adding a query. The variable probably already exists.
**Write actions** go through the portal's `post.php` dispatcher, which:
1. Requires config, functions, and the login check.
2. Defines the constant `FROM_POST_HANDLER`.
3. Loads the handler files in `post/` (excluding `*_model.php`).
Every handler file must start with:
```php
defined('FROM_POST_HANDLER') || die("Direct file access is not allowed");
```
Handlers are a series of independent blocks, one per action:
```php
if (isset($_POST['edit_ticket_priority'])) {
validateCSRFToken($_POST['csrf_token']);
enforceUserPermission('module_support', 2);
// ... fetch, check client access, act, log, notify, redirect
}
```
**Copy the nearest existing block as your starting point** — but understand every line you copy. The next section explains why each one is there.
### The `_model.php` pattern
Files named `agent/post/*_model.php` hold shared field collection/sanitization logic used by both the create and edit blocks of a module (e.g. `asset_model.php` is included by both `add_asset` and `edit_asset`). If create and edit share more than a couple of fields, use this pattern rather than duplicating. Model files carry the same `FROM_POST_HANDLER` guard and are excluded from the dispatcher's auto-load.
**`_model.php` is a reserved suffix.** The exclusion is a filename match, so a *handler* named `*_model.php` is silently never loaded — its form posts, nothing claims the request, and the user gets a blank page with no error anywhere. This is what happened to `admin/post/ai_model.php`, which is why the AI Models handler is now `admin/post/ai_models.php`. Name entity handlers around the suffix (`ai_models.php`, `users.php`, `api_keys.php`).
A POST that reaches the end of `admin/post.php` or `agent/post.php` without a handler claiming it is logged to App Logs as a `Request` warning, which is the fastest way to spot this class of mistake.
---
## Cron
One crontab entry runs everything:
```
* * * * * php /path/to/itflow/cron/cron.php >/dev/null
```
`cron/cron.php` is a dispatcher. It wakes every minute, works out which scripts in `cron/` are due, and requires them into its own process. Adding a job is a new script in `cron/` plus an entry in `includes/cron_jobs.php`. The crontab never changes again.
That registry is the only thing that decides **which** scripts can run, and the schedule in it is only a default: it seeds the job's `cron_jobs` row the first time the dispatcher meets the job, and from then on the row is what runs, because Maintenance > Cron writes to it. The database therefore holds **when and whether**, never **what** — a row naming a script that is not in the registry is ignored, so nothing that reaches the database can point the dispatcher at an arbitrary file. Keep it that way.
Run Now in the admin UI does not execute anything in the web request: these scripts are CLI-only and some take minutes, so the button sets `cron_job_run_now` and the next dispatch picks it up, through the same lock and claim as a scheduled run.
Due-ness is recorded in the `cron_jobs` table rather than matched against the clock, so a job whose minute was missed — machine down, previous run still going — runs at the next opportunity instead of being skipped for the day. A job is claimed *before* it runs, not after: a run that dies half way through is not repeated, which matters because `nightly_tasks.php` generates invoices and charges cards. Each job is also locked individually for the length of its own run (`cron/includes/cron_lock.php`), so a long or hung job holds up only itself — the next minute's dispatch picks up everything else in a second process.
Because the jobs share one PHP process, job code has three rules:
1. **Never `exit()` or `die()`.** It ends the whole cycle and every job after it. Use `cronJobStop($message, $exit_code)` instead: it exits when the script was run directly and unwinds back to the dispatcher when it wasn't, so both paths behave as they always have.
2. **Never declare a function or class another job might declare.** Two jobs each declaring the same helper is a fatal `Cannot redeclare` the moment they share a process. Shared helpers belong in `functions/`.
3. **Be safe to run twice in one day.** The dispatcher's lock stops overlap, but nothing stops a repeat: an admin presses Run Now after the scheduled pass, or a schedule is misconfigured. Work selected by a date match (`... = CURDATE()`) fires again on every run of that day unless something records that it happened — nightly's late fees and overdue reminders guard on the history rows they write. A job whose work cannot be made repeat-safe declares `'interval_safe' => false` in `includes/cron_jobs.php`, which locks it to the daily schedule in Maintenance > Cron and in the dispatcher.
4. **Set what you read.** One global scope and one set of `require_once` includes are shared across the cycle — a job's own `require_once "../config.php"` is a no-op if an earlier job already loaded it, and any variable an earlier job left behind is still there. Do not rely on the state a fresh process would have given you.
A job can also ship switched off with `'enabled' => 0` in the registry. The row is seeded disabled and stays that way until somebody turns it on in Maintenance > Cron. Use it for work an install should opt into rather than inherit silently from an upgrade — `backup` ships this way, because a full backup can be gigabytes a night.
### The master switch
`config_enable_cron` is a second, coarser switch that sits above the per-job ones. It is **not** enforced by the dispatcher — every job checks it in its own header and stops itself with `cronJobStop()`. A new job has to make that check too; one that skips it keeps running on an install that believes cron is off, which is exactly the trap `ticket_email_parser` and `ticket_sla` sat in until 26.08.
Be precise about what it does, because it is easy to oversell. It is **not** a guard on restored data: a full backup dumps the `settings` table, so a restored copy comes back with `config_enable_cron = 1` alongside every enabled `cron_jobs` row, exactly as production had them. What it gives you is one reversible bit — the fastest way to stop an install acting on live data once you have noticed, and the only way to stop everything without editing seven rows.
That last part is the reason it is not redundant with `cron_job_enabled`. Turning the switch off and back on returns you to exactly the configuration you had. Sweeping all seven rows off and back on does not: `backup` ships `'enabled' => 0`, so the sweep quietly turns on nightly backups nobody asked for, along with anything else that was deliberately disabled.
It defaults to `0`. That is a weaker guard than it looks — an install with no crontab entry runs nothing whatever the switch says, so the entry is the real gate — but it does mean adding the crontab line to a half-configured install is not enough on its own to start emailing. Both setup paths name the step on the way out.
## Backups
`functions/backup.php` is the whole engine, and all three entry points go through it: Maintenance > Backup, `cron/backup.php`, and `scripts/restore_cli.php`. Nothing else should dump, zip, or import a database.
Archives are AES-256 encrypted zips. The key is one value per install, generated on first use and appended to `config.php`**never** the database and **never** the file name. That is the point: a backup that leaks cannot be opened with anything the backup itself contains, and a URL or an access log never carries the key. The 32 random characters in the file name are an unguessable path component, nothing more. Note that `unzip`, Windows Explorer and the macOS Archive Utility cannot read AES zips; 7-Zip, WinZip, PeaZip and Keka can.
The web tier never builds an archive in the request. It writes a `Pending` row and `cron/backup.php` does the work, because a dump of a real install outlives `request_terminate_timeout` and `set_time_limit()` does not help. Same reasoning as Run Now.
Two rules for anything touching restore:
1. **Validate before you destroy.** The key is checked and the archive unpacked before a single table is dropped, and the live database is dumped to a rollback file first. If the import fails the rollback goes back in. `mysqli` throws rather than returning false under PHP 8.1's default report mode, so every statement in the import path is wrapped — an uncaught throw there leaves an install with no database at all.
2. **The archive does not get to decide what our guards say.** A restore wipes `uploads/`, and an archive is allowed to contain a `.htaccess`. `backupAssertUploadsGuards()` rewrites ours afterwards unconditionally, and the backup storage directory is preserved through the wipe so a restore cannot destroy every other archive on the box.
Retention lives in `nightly_tasks.php`, never in the backup job, so a failed backup cannot delete the archive it was supposed to replace. It never removes the newest complete backup, and an archive on disk with no row is **adopted** rather than deleted — after a restore the `backups` table is the old one, so everything made since looks unknown.
Setup's restore step closes itself once the `users` table has rows, whatever `config.php` says. It used to default `$config_enable_setup` to `1` when the flag was absent, which fails the wrong way: the flag is only appended at the end of a successful install, so an install abandoned in between left an unauthenticated endpoint that dropped every table, imported an arbitrary archive, and rewrote `uploads/` including the `.htaccess` that stops PHP running there.
Every script in `cron/` still runs standalone (`php cron/mail_queue.php`) and still takes its own lock when it does, so anything can be run by hand for testing.
---
## Security rules (non-negotiable)
ITFlow does not use prepared statements or an ORM; queries are built as strings. That works **only** if every value is neutralized before interpolation. The rules:
### 1. Every value interpolated into SQL is cast or sanitized. No exceptions.
- **Integers** (IDs, flags, counts): `intval($_POST['ticket_id'])`. Interpolate unquoted.
- **Strings**: `escapeSql($_POST['subject'])`. This normalizes encoding to UTF-8, then runs `strip_tags()`, `trim()`, and `mysqli_real_escape_string()`. Because it relies on SQL escaping, the value **must be placed inside quotes in the query** (`'$subject'`). An escaped string interpolated without quotes is still injectable.
- **Values read back from the database** get the same treatment before reuse in another query (you will see `escapeSql($row['ticket_prefix'])` throughout — this is why).
If you write a query and even one variable in it skipped these, that is a SQL injection. This is the single most common review rejection.
**Fetch helpers return raw values — you escape them.** `getFieldById()` and `getTicketStatusName()` hand back exactly what is in the column. Escaping is the call site's job, the same as any other row you read:
```php
$client_name = escapeSql(getFieldById('clients', $client_id, 'client_name')); // into a query
$client_name = escapeHtml(getFieldById('clients', $client_id, 'client_name')); // into a page
$client_id = intval(getFieldById('tickets', $ticket_id, 'ticket_client_id')); // an id
```
Both helpers used to escape internally, via an `$escape_method` argument. It went badly: most call sites wrapped them in `escapeSql()` anyway and got a double-escaped value, so a client named `O'Brien` came back as `O\'Brien` and the backslash reached export filenames, PDF headings, flash messages and — on the user restore path — the database itself. A value that is already safe cannot be made safer, only wrong.
### 2. Every state-changing action validates CSRF.
`validateCSRFToken()` is the first line of every action block. It takes no argument — it reads `csrf_token` from `$_POST`, then `$_GET`, itself, so the same call covers form posts and link-style actions. (The signature still accepts an explicit token for callers that need one, but no call site in the tree passes one; use the bare form.) Forms and action links must include the token; copy how existing modals do it.
### 3. Every action enforces permissions.
`enforceUserPermission('module_x', level)` where level is `1` = read, `2` = write, `3` = full/delete. Current modules: `module_client`, `module_support`, `module_sales`, `module_financial`, `module_credential`, `module_reporting`. Read pages enforce level 1; create/edit enforce 2; destructive actions enforce 3. CSV/PDF exports are reads — gate them with the bare one-argument form, e.g. `enforceUserPermission('module_sales')`.
Two portals are gated differently, which is why their handlers look like they are missing the call:
- **Admin.** `admin/post.php` only loads anything in `admin/post/` when `$session_is_admin` is set, so admin handlers inherit the gate from the dispatcher and do not call `enforceUserPermission()` themselves.
- **Client portal.** `client/post.php` is a single file of action blocks rather than a dispatcher, and gates on the contact's own capabilities with `enforceContactCan('accounting'|'contacts'|'itdoc')`.
Everywhere else — anything under `agent/post/` — the call belongs in the block.
### 4. Client scoping is enforced, not assumed.
A user can be restricted to a subset of clients through `user_client_permissions`. Enforcing that has two halves, and a page usually needs both.
**One record — `enforceClientAccess()`.** After loading a record, call it (optionally with the record's client ID) so technicians restricted to specific clients cannot touch other clients' data by editing an ID in the URL. Look at how `resolve_ticket` does it.
**A list — `clientScopeSql()`.** Any query returning more than one row appends the fragment for that resource's own client column:
```php
$sql = mysqli_query($mysqli, "SELECT expense_id, expense_date, expense_amount, expense_description
FROM expenses
WHERE expense_archived_at IS NULL
" . clientScopeSql('expense_client_id') . "
ORDER BY expense_date DESC");
```
It returns `" AND ..."` or `""`, so it needs a `WHERE` to hang off — add `WHERE 1=1` if the query has no other condition. It is column-aware and takes an alias fine (`clientScopeSql('t.ticket_client_id')`). The API calls the same helper through the `apiClientScopeSql()` wrapper.
Scope on the resource's **own** column, not on a joined `clients.client_id`. Joining `clients` just to scope makes the filter depend on the join: with a `LEFT JOIN`, a row whose client column is `0` produces `NULL`, and `NULL IN (...)` is neither true nor false, so the row silently vanishes. If the query joins `clients` for `client_name`, keep the join for that — but still scope on the owning column.
**Records with no client (`0`) stay visible to restricted users.** `clientScopeSql()` emits `IN (0,...)` deliberately. Client restrictions partition *client* data, and a record belonging to no client is not any client's data to withhold. Do not hand-roll a variant that drops the `0` — the tree had accumulated several before this helper existed, disagreeing with each other, and reconciling them is what surfaced the inconsistency.
### 5. Escape on output.
Anything rendered into HTML goes through `escapeHtml()`. `escapeSql()` on the way in is **not** output escaping — data can enter the DB through other paths (API, email parser, older versions).
In practice the escaping happens **where the row is read, not where it is echoed**. A page or modal fetches its row and assigns each field through `escapeHtml()` once, then echoes the resulting variable raw:
```php
$row = mysqli_fetch_assoc($sql);
$asset_id = intval($row['asset_id']); // ints: intval, not escapeHtml
$asset_name = escapeHtml($row['asset_name']);
...
<strong><?php echo $asset_name; ?></strong>
```
Follow that pattern. Escaping at the echo instead would double-escape a value that is already safe, and mixing the two is how fields get missed. If you introduce a view variable that does not come from a row, escape it at assignment so the rule still holds at the top of the file.
A value that arrives through a fetch helper rather than a row read follows the same rule — `escapeHtml(getFieldById(...))` at the assignment, not at the echo. See rule 1.
Rich-text fields (TinyMCE content) are the exception and have their own handling; follow the existing pattern for the specific field rather than inventing one.
### 6. No shell-outs. No `eval`.
The project has deliberately moved off `shell_exec`/`exec` in favor of native PHP — `dns_get_record()` instead of `dig`, RDAP instead of `whois`, and so on. **Do not add new shell execution or `eval`.** PRs introducing either will be declined.
A handful of legacy call sites survive, all of them wrapping `git` or `which` in the self-update and diagnostics paths: `admin/debug.php`, `admin/update.php`, `admin/post/update.php`, `admin/post/backup.php`, `cron/cron.php`, `functions/app.php`, `scripts/update_cli.php`, `setup/index.php`. They are on the list to be replaced with direct `.git` file reads; treat them as debt, not as precedent.
### 7. Report vulnerabilities privately.
Per [SECURITY.md](SECURITY.md) — never in a public issue.
---
## Conventions
**Only technician-entered time is time worked.** `ticket_replies.ticket_reply_time_worked` is billable labour and feeds ticket totals, the technician and client time reports, project totals, invoicing and the API. A reply the *system* writes — assignment, priority change, merge, close, invoice/quote created, schedule edited, task completed or reopened — is an audit trail, not work, and records `'00:00:00'`. Only a value the technician actually typed goes in that column. Task completion estimates are planning information and stay on the task; they are never converted into time worked. `agent/ticket.php` hides the clock badge on a reply whose time is exactly `00:00:00`, so a zero renders as no time rather than as "0m".
**Database naming.** Every column is prefixed with the singular name of the entity it belongs to: `tickets.ticket_id`, `tickets.ticket_subject`, `clients.client_name`. This makes JOIN results unambiguous, so a `SELECT *` across joins is never *wrong*. New tables must follow it.
**Select the columns you use, not `*`.** Unambiguous is not the same as cheap. `SELECT *` across three joined tables fetches every column of all three, including the `*_notes` and `*_details` TEXT columns, and throws away whatever the page never renders. A search result list that shows five fields was pulling sixty. List the columns instead:
```php
$sql = mysqli_query($mysqli, "SELECT ticket_id, ticket_prefix, ticket_number, ticket_subject, client_name
FROM tickets
LEFT JOIN clients ON ticket_client_id = client_id
WHERE ticket_archived_at IS NULL
" . clientScopeSql('ticket_client_id') . "");
```
Two things follow from that:
- A query whose result only feeds `mysqli_num_rows()` needs no columns at all — write `SELECT 1`. Do not select a primary key "just in case": if the query joins two tables that both carry that column name, an unqualified `SELECT ticket_template_id` is an ambiguous-column error.
- Keep the join even when no column of the joined table survives into the `SELECT`, if the join is doing work — supplying a `WHERE` term, an `ORDER BY`, or the client column you scope on. Dropping a join is a separate decision from trimming the column list.
The trade is real and worth stating: `SELECT *` picks up new columns for free, an explicit list does not. Add a column to a table and every query that needs it must be updated by hand, and the failure mode is a blank field or a PHP 8 undefined-key warning rather than an error. That is the price of not fetching data nobody reads, and the project has decided to pay it on anything that loops or touches a TEXT column.
The exception is `api/v1/*/read.php`. Those endpoints hand the whole row to `read_output.php`, which serialises it straight into the JSON response — there the row *is* the output contract, so `SELECT *` is correct and trimming it would silently drop fields from every consumer.
The prefix is the entity name, which is usually but not always the singular of the table name. Where a table is named for its container rather than its row, the prefix follows the row: `calendar_events``event_*`, `asset_interfaces``interface_*`, `invoice_items` / `quote_items``item_*`, `rack_units``unit_*`, `user_roles``role_*`, `product_stock``stock_*`. Pick the prefix your columns will read best as and use it for every column in the table.
Two standing exceptions: junction tables (`client_tags`, `service_assets`, …) carry the two parent FK names unprefixed, and `settings` / `user_settings` use `config_*` / `user_config_*`.
**Schema changes require two edits in one PR:**
1. `db.sql` — so fresh installs get the new schema.
2. `admin/database_updates/<x.y.z>.php` — a new file named for the version it upgrades **to**, containing only the queries that apply the change. Migrations are sequential and rolling-release; never edit a historical file.
That is the whole job. `LATEST_DATABASE_VERSION` is derived from the highest-numbered filename in `admin/database_updates/`, and the runner (`admin/database_updates.php`) steps `config_current_database_version` after each file succeeds — so there is no constant to bump and no version-bump query to write. Each migration file needs the standard `defined('FROM_DB_UPDATER') || die(...)` guard at the top; copy an existing file's header.
A single update run applies every pending migration in order, stopping at the first failure with the version left at the last file that completed, so a re-run resumes at the one that broke.
**After acting, log and notify.** State changes call `logAudit($type, $action, $description, $client_id, $entity_id)` for the audit trail. User-facing events may also call `appNotify()`. Fire `triggerCustomAction()` where a site might reasonably want a hook. Then call `flashAlert($message, $type)` and `redirect()` (defaults to the referer) rather than setting session keys or `header()` manually.
**Function names (post-rename).** Helpers were renamed for clarity in 2026; the old names **no longer exist** — code calling them fatals. If you're rebasing an old PR or following an old tutorial, translate: `sanitizeInput``escapeSql`, `nullable_htmlentities``escapeHtml`, `logAction``logAudit`, `flash_alert``flashAlert`, `customAction``triggerCustomAction`, `encryptLoginEntry`/`decryptLoginEntry``encryptCredentialEntry`/`decryptCredentialEntry`, `strtoAZaz09``toAlphanumeric`, `fetchUpdates``checkForUpdates`, `sanitize_url``escapeUrl`.
One removed **variable** deserves its own warning: the old `$access_permission_query` global is gone, replaced by `clientScopeSql()` (security rule 4). Unlike a removed function, it does not fatal — an undefined variable interpolates as an empty string, so a rebased query keeps running with **no client scoping at all**. Grep for it before rebasing anything that touches a list query.
**Helpers that fetch data return it raw.** If you add a `getXById()`-style helper, return the column value untouched and let callers escape it (security rule 1). Validating what the helper interpolates into its *own* query — table and column names, the id — is still the helper's job; that is query construction, not output escaping, and the two are not the same thing.
**Bulk vs. single actions.** If you change the behavior of a single action (e.g. resolving a ticket), check whether a `bulk_*` counterpart exists and update it too. They are currently parallel implementations and drift between them is a known bug source.
**UI.** Bootstrap 4 / AdminLTE, modals per-module under `<portal>/modals/<module>/`, DataTables for lists, monospace styling for technical data (IPs, serials, keys) and proportional for human text. Match the page you're standing in.
**Modals post to the portal you are standing in, not the one they live in.** Modal forms use `action="post.php"`, which the browser resolves against the *page* URL, not the modal's own path. A modal under `admin/modals/` that an agent page opens by relative path therefore submits to `agent/post.php` and is handled by `agent/post/`, not `admin/post/`. If you reuse a modal across portals, every portal that can open it needs a handler that accepts the same field set — otherwise fields are silently dropped on one side.
**Style.** Procedural PHP, 4-space indentation, LF line endings, code and comments in English. Match the surrounding code rather than importing a personal style. Don't reformat code you aren't changing — it buries the real diff.
Line endings and indentation are enforced by `.gitattributes` and `.editorconfig` at the repo root, so an editor that respects EditorConfig needs no configuration. `.gitattributes` marks `libs/` as `-text`: vendored code is preserved byte-for-byte as shipped upstream and must never be normalized, or the next wholesale library update turns into an unreviewable diff.
---
## Pull requests
- **Small, focused diffs.** One feature or one fix per PR. Never mix relocation/reformatting with logic changes — split them into separate commits or PRs so each is reviewable on its own.
- Describe **what** and **why**, and note any schema changes prominently.
- CI runs PHP lint and db.sql lint; SonarCloud scans for security issues. Green checks are required but not sufficient — the conventions above are checked by human review.
- Test your change against a real install: fresh setup from `db.sql` **and** an upgrade via `database_updates.php` if you touched schema.
- For anything larger than a bug fix, **open an issue first** and discuss the approach. ITFlow's roadmap favors incremental modernization of the existing PHP codebase; large rewrites, framework introductions, and new runtime dependencies are out of scope.
## Getting help
Open a GitHub issue using the templates, or ask in the community forum linked from the README. When in doubt about a convention, find the closest existing example in the codebase and follow it — consistency beats novelty here.

View File

@@ -16,7 +16,7 @@
<br />
<a href="https://demo.itflow.org"><strong>View demo</strong></a>
<br />
Username: <b>demo@demo</b> | Password: <b>demo</b>
Username: <b>demo@demo.com</b> | Password: <b>demo</b>
<br />
<br />
<a href="https://itflow.org/#about">About</a>
@@ -27,7 +27,7 @@
·
<a href="https://forum.itflow.org/t/bug">Report Bug</a>
·
<a href="https://forum.itflow.org/t/features">Request Feature</a>
<a href="https://forum.itflow.org/t/features">Suggest Feature</a>
·
<a href="https://github.com/itflow-org/itflow/security/policy">Security</a>
</p>
@@ -44,16 +44,16 @@
### The Problem
- You're a small but busy managed service provider with 101 things to do. Information about your clients is unorganised, unstructured and outdated.
- For some work, you seem to spend longer looking for the relevant documentation than actually working on the issue/project.
- On top of the technical day to day, you also have to take care of the financial side of the business - consistent pricing, quotes/invoicing, and accounting.
- MSP tools can be expensive, with recurring licensing costs adding up quickly.
- On top of the technical day-to-day, you also have to take care of the financial side of the business - consistent pricing, quotes/invoicing, and accounting.
### The Solution: ITFlow
- ITFlow consolidates common MSP needs (documentation, ticketing and billing) into one unified system.
- ITFlow consolidates common MSP needs (documentation, ticketing and billing) into one unified, free platform that you control.
<!-- GETTING STARTED -->
## Getting Started
### Self Hosting
- The best installation method is to use the [install script](https://docs.itflow.org/installation_script) on Ubuntu/Debian. A video walk through is available [here](https://www.youtube.com/watch?v=kKz9NOU_1XE).
- The best installation method is to use the [install script](https://docs.itflow.org/installation_script) on Ubuntu/Debian. See the [video](https://www.youtube.com/watch?v=kKz9NOU_1XE) walk through.
```
wget -O itflow_install.sh https://github.com/itflow-org/itflow-install-script/raw/main/itflow_install.sh
bash itflow_install.sh
@@ -61,28 +61,28 @@
- Other manual installation methods are available in the [docs](https://docs.itflow.org/installation).
### Managed Hosting
- If you'd prefer, we can [host ITFlow for you](https://services.itflow.org/hosting.php).
- If you'd prefer, we can [host ITFlow for you](https://services.itflow.org/hosting.php) (coming soon).
<!-- FEATURES -->
## Key Features
* Client documentation - assets, contacts, domains, docs, files, passwords, and more
* Accounting / Billing - finance dashboard, quotes, invoices, accounting, expenses, etc
* Client Portal - self service quote/invoice/ticket management for clients
* Alerting - account balance, invoices, domain/SSL renewals
* Completely free & open-source alternative to ITGlue and Hudu
* Client documentation - assets, contacts, domains, docs/files, passwords, tickets, and more
* Billing - quotes, invoices and expenses, with Stripe integration for online payments
* Alerting - domain and SSL expiry/renewals
* Client Portal - self service quote, invoice, and ticket management for clients
<!-- ROADMAP -->
## Roadmap / Future to-do
We track the implementation of confirmed features and bugs via [TaskFlow](https://tasks.dev.itflow.org/tasks.php). Use the [forum](https://forum.itflow.org) to request features or raise bug reports.
We track the implementation of confirmed features and bugs via [TaskFlow](https://tasks.dev.itflow.org/tasks.php). Use the [forum](https://forum.itflow.org) to suggest new features or report bugs.
<!-- CONTRIBUTING -->
## Support & Contributions
### Forum
For help using ITFlow, bugs, feature requests, and general ideas / discussions please use the community [forum](https://forum.itflow.org).
For help using ITFlow, bugs, feature suggestions, and other general ideas or discussions, please use the community [forum](https://forum.itflow.org).
### Contributing
If you want to improve ITFlow, feel free to fork the repo and create a pull reques. Make sure to discuss significant changes or new features with fellow contributors on the forum first. This helps ensure that your contributions are aligned with project goals, and saves time for everyone. All contributions should follow our [code standards](https://docs.itflow.org/code_standards). See the [contributing guide](https://docs.itflow.org/contribute).
We have temporarily paused PRs from the community.
#### Contributors
<a href="https://github.com/itflow-org/itflow/graphs/contributors">
@@ -90,16 +90,16 @@ If you want to improve ITFlow, feel free to fork the repo and create a pull requ
</a>
### Supporters
Were incredibly grateful to the organizations and individuals who support the project - a big thank you to:
Were incredibly grateful to the organizations and individuals who [sponsor](https://services.itflow.org) the project - a big thank you to:
- CompuMatter
- F1 for HELP
- JetBrains (PhpStorm)
- digiBandit
## License
ITFlow is distributed "as is" under the GPL License, WITHOUT WARRANTY OF ANY KIND. See [`LICENSE`](https://github.com/itflow-org/itflow/blob/master/LICENSE) for details.
## Security
* As of 2025, we now have a stable release of the project.
* We've had a stable release of the project since Jan 2025.
* Whilst we are confident in the safety of the code, no system is risk-free. Nearly all software has bugs. Use your best judgement before storing highly confidential information in ITFlow.
* If you have a security concern, privately report it [here](https://github.com/itflow-org/itflow/security/policy).

View File

@@ -1,24 +1,26 @@
# Security Policy
## **Please do NOT report security concerns/vulnerabilities publicly (Issues/forum)**
## **Please do NOT report security concerns/vulnerabilities publicly**
**We take security seriously**
**ITFlow stores important information, so we take security seriously**
- Whilst we are confident in the safety of the code, no system is risk-free. Nearly all software has bugs. Use your best judgement before storing highly confidential information in ITFlow.
- We attempt to follow security best practices where possible, including [automated code scanning](https://sonarcloud.io/component_measures?id=itflow-org_itflow&metric=security_rating&view=list).
- [![Security](https://sonarcloud.io/api/project_badges/measure?project=itflow-org_itflow&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=itflow-org_itflow)
- Whilst we are confident in the safety of the code, no system is risk-free. Nearly all software has bugs.
- Use your best judgement before storing highly confidential information in ITFlow.
- We attempt to follow security best practices where possible, including automated code scanning.
## Supported Versions
We operate a rolling release model. Any bug fixes will be released into latest version of ITFlow, so you must stay up-to-date.
| Version | Supported |
|---------| ------------------ |
| 25.05 | :white_check_mark: |
| 26.08 | :white_check_mark: |
## Reporting a Vulnerability via GitHub Security Advisories
**Security contact: [GitHub Security Advisories](https://github.com/itflow-org/itflow/security/advisories/new)**
If you have discovered a security issue, please **[report it](https://github.com/itflow-org/itflow/security/advisories/new)** to us in as much detail as possible, so we can fix it.
If you have discovered a security issue, please **[report it](https://github.com/itflow-org/itflow/security/advisories/new)** to us so we can fix it.
You should expect to receive an initial acknowledgement within 72 hours. If you don't receive any feedback, we may have missed the initial email from GitHub (we're human!). Please raise a forum discussion quoting ONLY the assigned GHSA ref.
You should expect to receive an initial acknowledgement within 7 days. If you don't receive any feedback, we may have missed the initial email from GitHub (we're human!). Please raise a forum discussion quoting ONLY the assigned GHSA ref.
Note: Please don't submit AI slop. AI can be useful for drafting and research, but you are responsible for reviewing and validating anything you submit. We reserve the right to restrict repository access if you repeatedly ignore this requirement.

View File

@@ -1,107 +0,0 @@
<?php
// Default Column Sortby Filter
$sort = "ai_model_name";
$order = "ASC";
require_once "includes/inc_all_admin.php";
$sql = mysqli_query($mysqli, "SELECT * FROM ai_models LEFT JOIN ai_providers ON ai_model_ai_provider_id = ai_provider_id ORDER BY $sort $order");
$num_rows = mysqli_num_rows($sql);
?>
<div class="card card-dark">
<div class="card-header py-2">
<h3 class="card-title mt-2"><i class="fas fa-fw fa-robot mr-2"></i>AI Models</h3>
<div class="card-tools">
<button type="button" class="btn btn-primary ajax-modal" data-modal-url="modals/ai/ai_model_add.php"><i class="fas fa-plus mr-2"></i>Add Model</button>
</div>
</div>
<div class="card-body">
<div class="table-responsive-sm">
<table class="table table-striped table-borderless table-hover">
<thead class="text-dark <?php if ($num_rows == 0) { echo "d-none"; } ?>">
<tr>
<th>
<a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=ai_model_name&order=<?php echo $disp; ?>">
Model <?php if ($sort == 'ai_model_name') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=ai_provider_name&order=<?php echo $disp; ?>">
Provider <?php if ($sort == 'ai_provider_name') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=ai_model_use_case&order=<?php echo $disp; ?>">
Use Case<?php if ($sort == 'ai_model_use_case') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-dark">Prompt</a>
</th>
<th class="text-center">Action</th>
</tr>
</thead>
<tbody>
<?php
while ($row = mysqli_fetch_array($sql)) {
$provider_id = intval($row['ai_provider_id']);
$provider_name = nullable_htmlentities($row['ai_provider_name']);
$model_id = intval($row['ai_model_id']);
$model_name = nullable_htmlentities($row['ai_model_name']);
$use_case = nullable_htmlentities($row['ai_model_use_case']);
$prompt = nl2br(nullable_htmlentities($row['ai_model_prompt']));
?>
<tr>
<td>
<a class="text-dark text-bold ajax-modal" href="#"
data-modal-url="modals/ai/ai_model_edit.php?id=<?= $model_id ?>">
<?php echo $model_name; ?>
</a>
</td>
<td><?php echo $provider_name; ?></td>
<td><?php echo $use_case; ?></td>
<td><?php echo $prompt; ?></td>
<td>
<div class="dropdown dropleft text-center">
<button class="btn btn-secondary btn-sm" type="button" data-toggle="dropdown">
<i class="fas fa-ellipsis-h"></i>
</button>
<div class="dropdown-menu">
<a class="dropdown-item ajax-modal" href="#"
data-modal-url="modals/ai/ai_model_edit.php?id=<?= $model_id ?>">
<i class="fas fa-fw fa-edit mr-2"></i>Edit
</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item text-danger confirm-link" href="post.php?delete_ai_model=<?php echo $model_id; ?>&csrf_token=<?php echo $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-trash mr-2"></i>Delete
</a>
</div>
</div>
</td>
</tr>
<?php
}
if ($num_rows == 0) {
echo "<h3 class='text-secondary mt-3' style='text-align: center'>No Records Here</h3>";
}
?>
</tbody>
</table>
</div>
</div>
</div>
<?php
require_once "../includes/footer.php";

118
admin/ai_models.php Normal file
View File

@@ -0,0 +1,118 @@
<?php
// Default Column Sortby Filter
$sort = "ai_model_name";
$order = "ASC";
require_once "includes/inc_all_admin.php";
$sql = mysqli_query($mysqli, "SELECT ai_model_id, ai_model_name, ai_model_prompt, ai_model_use_case, ai_provider_id,
ai_provider_name FROM ai_models LEFT JOIN ai_providers ON ai_model_ai_provider_id = ai_provider_id ORDER BY $sort $order");
$num_rows = mysqli_num_rows($sql);
?>
<ol class="breadcrumb d-print-none">
<li class="breadcrumb-item">
<a href="/admin">Admin</a>
</li>
<li class="breadcrumb-item">
<a href="ai_providers.php">AI Providers</a>
</li>
<li class="breadcrumb-item active">AI Models</li>
</ol>
<div class="card card-dark">
<div class="card-header py-2">
<h3 class="card-title mt-2"><i class="fas fa-fw fa-robot me-2"></i>AI Models</h3>
<div class="card-tools">
<button type="button" class="btn btn-primary ajax-modal" data-modal-url="modals/ai/ai_model_add.php"><i class="fas fa-plus me-2"></i>Add Model</button>
</div>
</div>
<div class="card-body">
<div class="table-responsive-sm">
<table class="table table-striped table-borderless table-hover">
<thead class="text-dark <?php if ($num_rows == 0) { echo "d-none"; } ?>">
<tr>
<th>
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=ai_model_name&order=<?= $disp ?>">
Model <?php if ($sort == 'ai_model_name') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=ai_provider_name&order=<?= $disp ?>">
Provider <?php if ($sort == 'ai_provider_name') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=ai_model_use_case&order=<?= $disp ?>">
Use Case<?php if ($sort == 'ai_model_use_case') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-dark">Prompt</a>
</th>
<th class="text-center">Action</th>
</tr>
</thead>
<tbody>
<?php
while ($row = mysqli_fetch_assoc($sql)) {
$provider_id = intval($row['ai_provider_id']);
$provider_name = escapeHtml($row['ai_provider_name']);
$model_id = intval($row['ai_model_id']);
$model_name = escapeHtml($row['ai_model_name']);
$use_case = escapeHtml($row['ai_model_use_case']);
$prompt = nl2br(escapeHtml($row['ai_model_prompt']));
?>
<tr>
<td>
<a class="text-dark text-bold ajax-modal" href="#"
data-modal-url="modals/ai/ai_model_edit.php?id=<?= $model_id ?>">
<?= $model_name ?>
</a>
</td>
<td><?= $provider_name ?></td>
<td><?= $use_case ?></td>
<td><?= $prompt ?></td>
<td>
<div class="dropdown dropstart text-center">
<button class="btn btn-secondary btn-sm" type="button" data-bs-toggle="dropdown">
<i class="fas fa-ellipsis-h"></i>
</button>
<div class="dropdown-menu">
<a class="dropdown-item ajax-modal" href="#"
data-modal-url="modals/ai/ai_model_edit.php?id=<?= $model_id ?>">
<i class="fas fa-fw fa-edit me-2"></i>Edit
</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item text-danger confirm-link" href="post.php?delete_ai_model=<?= $model_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-trash me-2"></i>Delete
</a>
</div>
</div>
</td>
</tr>
<?php
}
if ($num_rows == 0) {
echo "<h3 class='text-secondary mt-3' style='text-align: center'>No Records Here</h3>";
}
?>
</tbody>
</table>
</div>
</div>
</div>
<?php
require_once "../includes/footer.php";

View File

@@ -1,108 +0,0 @@
<?php
// Default Column Sortby Filter
$sort = "ai_provider_name";
$order = "ASC";
require_once "includes/inc_all_admin.php";
$sql = mysqli_query($mysqli, "SELECT * FROM ai_providers ORDER BY $sort $order");
$num_rows = mysqli_num_rows($sql);
?>
<div class="card card-dark">
<div class="card-header py-2">
<h3 class="card-title mt-2"><i class="fas fa-fw fa-robot mr-2"></i>AI Providers</h3>
<div class="card-tools">
<button type="button" class="btn btn-primary ajax-modal" data-modal-url="modals/ai/ai_provider_add.php"><i class="fas fa-plus mr-2"></i>Add Provider</button>
</div>
</div>
<div class="card-body">
<div class="table-responsive-sm">
<table class="table table-striped table-borderless table-hover">
<thead class="text-dark <?php if ($num_rows == 0) { echo "d-none"; } ?>">
<tr>
<th>
<a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=ai_provider_name&order=<?php echo $disp; ?>">
Provider <?php if ($sort == 'ai_provider_name') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=ai_provider_api_url&order=<?php echo $disp; ?>">
URL <?php if ($sort == 'ai_provider_api_url') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=ai_provider_api_key&order=<?php echo $disp; ?>">
Key <?php if ($sort == 'ai_provider_api_key') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-dark">Models</a>
</th>
<th class="text-center">Action</th>
</tr>
</thead>
<tbody>
<?php
while ($row = mysqli_fetch_array($sql)) {
$provider_id = intval($row['ai_provider_id']);
$provider_name = nullable_htmlentities($row['ai_provider_name']);
$url = nullable_htmlentities($row['ai_provider_api_url']);
$key = nullable_htmlentities($row['ai_provider_api_key']);
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT('ai_model_id') AS ai_model_count FROM ai_models WHERE ai_model_ai_provider_id = $provider_id"));
$ai_model_count = intval($row['ai_model_count']);
?>
<tr>
<td>
<a class="text-dark text-bold ajax-modal" href="#"
data-modal-url="modals/ai/ai_provider_edit.php?id=<?= $provider_id ?>">
<?php echo $provider_name; ?>
</a>
</td>
<td><?php echo $url; ?></td>
<td><?php echo $key; ?></td>
<td><?php echo $ai_model_count; ?></td>
<td>
<div class="dropdown dropleft text-center">
<button class="btn btn-secondary btn-sm" type="button" data-toggle="dropdown">
<i class="fas fa-ellipsis-h"></i>
</button>
<div class="dropdown-menu">
<a class="dropdown-item ajax-modal" href="#"
data-modal-url="modals/ai/ai_provider_edit.php?id=<?= $provider_id ?>">
<i class="fas fa-fw fa-edit mr-2"></i>Edit
</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item text-danger confirm-link" href="post.php?delete_ai_provider=<?php echo $provider_id; ?>&csrf_token=<?php echo $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-trash mr-2"></i>Delete
</a>
</div>
</div>
</td>
</tr>
<?php
}
if ($num_rows == 0) {
echo "<h3 class='text-secondary mt-3' style='text-align: center'>No Records Here</h3>";
}
?>
</tbody>
</table>
</div>
</div>
</div>
<?php
require_once "../includes/footer.php";

109
admin/ai_providers.php Normal file
View File

@@ -0,0 +1,109 @@
<?php
// Default Column Sortby Filter
$sort = "ai_provider_name";
$order = "ASC";
require_once "includes/inc_all_admin.php";
$sql = mysqli_query($mysqli, "SELECT ai_provider_api_key, ai_provider_api_url, ai_provider_id, ai_provider_name FROM ai_providers ORDER BY $sort $order");
$num_rows = mysqli_num_rows($sql);
?>
<div class="card card-dark">
<div class="card-header py-2">
<h3 class="card-title mt-2"><i class="fas fa-fw fa-robot me-2"></i>AI Providers</h3>
<div class="card-tools">
<button type="button" class="btn btn-primary ajax-modal" data-modal-url="modals/ai/ai_provider_add.php"><i class="fas fa-plus me-2"></i>Add Provider</button>
</div>
</div>
<div class="card-body">
<div class="table-responsive-sm">
<table class="table table-striped table-borderless table-hover">
<thead class="text-dark <?php if ($num_rows == 0) { echo "d-none"; } ?>">
<tr>
<th>
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=ai_provider_name&order=<?= $disp ?>">
Provider <?php if ($sort == 'ai_provider_name') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=ai_provider_api_url&order=<?= $disp ?>">
URL <?php if ($sort == 'ai_provider_api_url') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=ai_provider_api_key&order=<?= $disp ?>">
Key <?php if ($sort == 'ai_provider_api_key') { echo $order_icon; } ?>
</a>
</th>
<th class="text-center">
<a class="text-dark">Models</a>
</th>
<th class="text-center">Action</th>
</tr>
</thead>
<tbody>
<?php
while ($row = mysqli_fetch_assoc($sql)) {
$provider_id = intval($row['ai_provider_id']);
$provider_name = escapeHtml($row['ai_provider_name']);
$url = escapeHtml($row['ai_provider_api_url']);
$key = escapeHtml($row['ai_provider_api_key']);
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT('ai_model_id') AS ai_model_count FROM ai_models WHERE ai_model_ai_provider_id = $provider_id"));
$ai_model_count = intval($row['ai_model_count']);
?>
<tr>
<td>
<a class="text-dark text-bold ajax-modal" href="#"
data-modal-url="modals/ai/ai_provider_edit.php?id=<?= $provider_id ?>">
<?= $provider_name ?>
</a>
</td>
<td><?= $url ?></td>
<td><?= $key ?></td>
<td class="text-center">
<a class="badge bg-dark rounded-pill p-2" href="ai_models.php"><?= $ai_model_count ?></a>
<td>
<div class="dropdown dropstart text-center">
<button class="btn btn-secondary btn-sm" type="button" data-bs-toggle="dropdown">
<i class="fas fa-ellipsis-h"></i>
</button>
<div class="dropdown-menu">
<a class="dropdown-item ajax-modal" href="#"
data-modal-url="modals/ai/ai_provider_edit.php?id=<?= $provider_id ?>">
<i class="fas fa-fw fa-edit me-2"></i>Edit
</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item text-danger confirm-link" href="post.php?delete_ai_provider=<?= $provider_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-trash me-2"></i>Delete
</a>
</div>
</div>
</td>
</tr>
<?php
}
if ($num_rows == 0) {
echo "<h3 class='text-secondary mt-3' style='text-align: center'>No Records Here</h3>";
}
?>
</tbody>
</table>
</div>
</div>
</div>
<?php
require_once "../includes/footer.php";

View File

@@ -8,8 +8,8 @@ require_once "includes/inc_all_admin.php";
$sql = mysqli_query(
$mysqli,
"SELECT SQL_CALC_FOUND_ROWS * FROM api_keys
LEFT JOIN clients on api_keys.api_key_client_id = clients.client_id
"SELECT SQL_CALC_FOUND_ROWS api_key_created_at, api_key_expire, api_key_id, api_key_name, api_key_secret, user_name FROM api_keys
LEFT JOIN users on api_key_user_id = user_id
WHERE (api_key_name LIKE '%$q%')
ORDER BY $sort $order LIMIT $record_from, $record_to"
);
@@ -20,9 +20,9 @@ $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="fas fa-fw fa-key mr-2"></i>API Keys</h3>
<h3 class="card-title mt-2"><i class="fas fa-fw fa-key me-2"></i>API Keys</h3>
<div class="card-tools">
<button type="button" class="btn btn-primary ajax-modal" data-modal-url="modals/api/api_key_add.php"><i class="fas fa-plus mr-2"></i>New API Key</button>
<button type="button" class="btn btn-primary ajax-modal" data-modal-url="modals/api/api_key_add.php"><i class="fas fa-plus me-2"></i>New API Key</button>
</div>
</div>
@@ -33,23 +33,21 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
<div class="col-md-4">
<div class="input-group mb-3 mb-md-0">
<input type="search" class="form-control" name="q" value="<?php if (isset($q)) { echo stripslashes(nullable_htmlentities($q)); } ?>" placeholder="Search keys">
<div class="input-group-append">
<input type="search" class="form-control" name="q" value="<?php if (isset($q)) { echo stripslashes(escapeHtml($q)); } ?>" placeholder="Search keys">
<button class="btn btn-primary"><i class="fa fa-search"></i></button>
</div>
</div>
</div>
<div class="col-md-8">
<div class="btn-group float-right">
<div class="dropdown ml-2" id="bulkActionButton" hidden>
<button class="btn btn-secondary dropdown-toggle" type="button" data-toggle="dropdown">
<i class="fas fa-fw fa-layer-group mr-2"></i>Bulk Action (<span id="selectedCount">0</span>)
<div class="btn-group float-end">
<div class="dropdown ms-2" id="bulkActionButton" hidden>
<button class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown">
<i class="fas fa-fw fa-layer-group me-2"></i>Bulk Action (<span id="selectedCount">0</span>)
</button>
<div class="dropdown-menu">
<button class="dropdown-item text-danger text-bold"
type="submit" form="bulkActions" name="bulk_delete_api_keys">
<i class="fas fa-fw fa-trash mr-2"></i>Revoke
<i class="fas fa-fw fa-trash me-2"></i>Delete
</button>
</div>
</div>
@@ -64,38 +62,38 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
<div class="table-responsive-sm">
<form id="bulkActions" action="post.php" method="post">
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token'] ?>">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<table class="table table-striped table-borderless table-hover">
<thead class="text-dark <?php if ($num_rows[0] == 0) { echo "d-none"; } ?>">
<tr>
<td class="pr-0">
<td class="pe-0">
<div class="form-check">
<input class="form-check-input" type="checkbox" onclick="checkAll(this)">
</div>
</td>
<th>
<a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=api_key_name&order=<?php echo $disp; ?>">
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=api_key_name&order=<?= $disp ?>">
Name <?php if ($sort == 'api_key_name') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=api_key_client_id&order=<?php echo $disp; ?>">
Client <?php if ($sort == 'api_key_client_id') { echo $order_icon; } ?>
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=api_key_user_id&order=<?= $disp ?>">
User <?php if ($sort == 'api_key_user_id') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=api_key_secret&order=<?php echo $disp; ?>">
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=api_key_secret&order=<?= $disp ?>">
Secret <?php if ($sort == 'api_key_secret') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=api_key_created_at&order=<?php echo $disp; ?>">
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=api_key_created_at&order=<?= $disp ?>">
Created <?php if ($sort == 'api_key_created_at') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=api_key_expire&order=<?php echo $disp; ?>">
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=api_key_expire&order=<?= $disp ?>">
Expires <?php if ($sort == 'api_key_expire') { echo $order_icon; } ?>
</a>
</th>
@@ -105,43 +103,49 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
<tbody>
<?php
while ($row = mysqli_fetch_array($sql)) {
while ($row = mysqli_fetch_assoc($sql)) {
$api_key_id = intval($row['api_key_id']);
$api_key_name = nullable_htmlentities($row['api_key_name']);
$api_key_secret = nullable_htmlentities("************" . substr($row['api_key_secret'], -4));
$api_key_created_at = nullable_htmlentities($row['api_key_created_at']);
$api_key_expire = nullable_htmlentities($row['api_key_expire']);
$api_key_name = escapeHtml($row['api_key_name']);
$api_key_secret = escapeHtml("************" . substr($row['api_key_secret'], -4));
$api_key_created_at = escapeHtml($row['api_key_created_at']);
$api_key_expire = escapeHtml($row['api_key_expire']);
if ($api_key_expire < date("Y-m-d H:i:s")) {
$api_key_expire = $api_key_expire . " (Expired)";
}
if ($row['api_key_client_id'] == 0) {
$api_key_client = "<i>All Clients</i>";
} else {
$api_key_client = nullable_htmlentities($row['client_name']);
}
$api_key_user = !empty($row['user_name']) ? escapeHtml($row['user_name']) : "<i>None</i>";
?>
<tr>
<td class="pr-0">
<td class="pe-0">
<div class="form-check">
<input class="form-check-input bulk-select" type="checkbox" name="api_key_ids[]" value="<?php echo $api_key_id ?>">
<input class="form-check-input bulk-select" type="checkbox" name="api_key_ids[]" value="<?= $api_key_id ?>">
</div>
</td>
<td class="text-bold"><?php echo $api_key_name; ?></td>
<td><?php echo $api_key_client; ?></td>
<td><?php echo $api_key_secret; ?></td>
<td><?php echo $api_key_created_at; ?></td>
<td><?php echo $api_key_expire; ?></td>
<td class="text-bold"><?= $api_key_name ?></td>
<td><?= $api_key_user ?></td>
<td><?= $api_key_secret ?></td>
<td><?= $api_key_created_at ?></td>
<td><?= $api_key_expire ?></td>
<td>
<div class="dropdown dropleft text-center">
<button class="btn btn-secondary btn-sm" type="button" data-toggle="dropdown">
<div class="dropdown dropstart text-center">
<button class="btn btn-secondary btn-sm" type="button" data-bs-toggle="dropdown">
<i class="fas fa-ellipsis-h"></i>
</button>
<div class="dropdown-menu">
<a class="dropdown-item text-danger text-bold confirm-link" href="post.php?delete_api_key=<?php echo $api_key_id; ?>&csrf_token=<?php echo $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-times mr-2"></i>Revoke
<a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/api/api_key_edit.php?id=<?= $api_key_id ?>">
<i class="fas fa-fw fa-edit me-2"></i>Edit
</a>
<?php if ($api_key_expire > date("Y-m-d H:i:s")) { ?>
<a class="dropdown-item text-danger text-bold confirm-link" href="post.php?revoke_api_key=<?= $api_key_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-times me-2"></i>Revoke
</a>
<?php } ?>
<?php if ($api_key_expire < date("Y-m-d H:i:s")) { ?>
<a class="dropdown-item text-danger text-bold confirm-link" href="post.php?delete_api_key=<?= $api_key_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-times me-2"></i>Delete
</a>
<?php } ?>
</div>
</div>
</td>
@@ -164,4 +168,3 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
<?php
require_once "../includes/footer.php";

View File

@@ -1,173 +0,0 @@
<?php
// Default Column Sortby Filter
$sort = "app_log_id";
$order = "DESC";
require_once "includes/inc_all_admin.php";
// Log Type Filter
if (isset($_GET['type']) & !empty($_GET['type'])) {
$log_type_query = "AND (app_log_type = '" . sanitizeInput($_GET['type']) . "')";
$type_filter = nullable_htmlentities($_GET['type']);
} else {
// Default - any
$log_type_query = '';
$type_filter = '';
}
// Log Category Filter
if (isset($_GET['category']) & !empty($_GET['catergory'])) {
$log_category_query = "AND (app_log_category = '" . sanitizeInput($_GET['category']) . "')";
$category_filter = nullable_htmlentities($_GET['category']);
} else {
// Default - any
$log_category_query = '';
$category_filter = '';
}
$sql = mysqli_query(
$mysqli,
"SELECT SQL_CALC_FOUND_ROWS * FROM app_logs
WHERE (app_log_type LIKE '%$q%' OR app_log_category LIKE '%$q%' OR app_log_details LIKE '%$q%')
AND DATE(app_log_created_at) BETWEEN '$dtf' AND '$dtt'
$log_type_query
$log_category_query
ORDER BY $sort $order LIMIT $record_from, $record_to"
);
$num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
?>
<div class="card card-dark">
<div class="card-header py-3">
<h3 class="card-title"><i class="fas fa-fw fa-history mr-2"></i>App Logs</h3>
</div>
<div class="card-body">
<form autocomplete="off">
<div class="row">
<div class="col-sm-4">
<div class="form-group">
<div class="input-group">
<input type="search" class="form-control" name="q" value="<?php if (isset($q)) { echo stripslashes(nullable_htmlentities($q)); } ?>" placeholder="Search app logs">
<div class="input-group-append">
<button class="btn btn-secondary" type="button" data-toggle="collapse" data-target="#advancedFilter"><i class="fas fa-filter"></i></button>
<button class="btn btn-primary"><i class="fa fa-search"></i></button>
</div>
</div>
</div>
</div>
<div class="col-sm-2">
<div class="form-group">
<select class="form-control select2" name="type" onchange="this.form.submit()">
<option value="">- All Types -</option>
<?php
$sql_types_filter = mysqli_query($mysqli, "SELECT DISTINCT app_log_type FROM app_logs ORDER BY app_log_type ASC");
while ($row = mysqli_fetch_array($sql_types_filter)) {
$log_type = nullable_htmlentities($row['app_log_type']);
?>
<option <?php if ($type_filter == $log_type) { echo "selected"; } ?>><?php echo $log_type; ?></option>
<?php
}
?>
</select>
</div>
</div>
<div class="col-sm-2">
<div class="form-group">
<select class="form-control select2" name="category" onchange="this.form.submit()">
<option value="">- All Categories -</option>
<?php
$sql_categories_filter = mysqli_query($mysqli, "SELECT DISTINCT app_log_category FROM app_logs ORDER BY app_log_category ASC");
while ($row = mysqli_fetch_array($sql_categories_filter)) {
$log_category = nullable_htmlentities($row['app_log_category']);
?>
<option <?php if ($category_filter == $log_category) { echo "selected"; } ?>><?php echo $log_category; ?></option>
<?php
}
?>
</select>
</div>
</div>
</div>
<div class="collapse mt-3 <?php if (isset($_GET['dtf']) && $_GET['dtf'] !== '1970-01-01') { echo "show"; } ?>" id="advancedFilter">
<div class="row">
<div class="col-md-3">
<div class="form-group">
<label>Date range</label>
<input type="text" id="dateFilter" class="form-control" autocomplete="off">
<input type="hidden" name="canned_date" id="canned_date" value="<?php echo nullable_htmlentities($_GET['canned_date']) ?? ''; ?>">
<input type="hidden" name="dtf" id="dtf" value="<?php echo nullable_htmlentities($dtf ?? ''); ?>">
<input type="hidden" name="dtt" id="dtt" value="<?php echo nullable_htmlentities($dtt ?? ''); ?>">
</div>
</div>
</div>
</div>
</form>
<hr>
<div class="table-responsive-sm">
<table class="table table-sm table-striped table-borderless table-hover">
<thead class="text-dark <?php if ($num_rows[0] == 0) { echo "d-none"; } ?>">
<tr>
<th>
<a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=app_log_created_at&order=<?php echo $disp; ?>">
Timestamp <?php if ($sort == 'app_log_created_at') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=app_log_type&order=<?php echo $disp; ?>">
Type <?php if ($sort == 'app_log_type') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=app_log_category&order=<?php echo $disp; ?>">
Category <?php if ($sort == 'app_log_category') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=app_log_details&order=<?php echo $disp; ?>">
Details <?php if ($sort == 'app_log_details') { echo $order_icon; } ?>
</a>
</th>
</tr>
</thead>
<tbody>
<?php
while ($row = mysqli_fetch_array($sql)) {
$log_id = intval($row['app_log_id']);
$log_type = nullable_htmlentities($row['app_log_type']);
$log_category = nullable_htmlentities($row['app_log_category']);
$log_details = nullable_htmlentities($row['app_log_details']);
$log_created_at = nullable_htmlentities($row['app_log_created_at']);
?>
<tr>
<td><?php echo $log_created_at; ?></td>
<td><?php echo $log_type; ?></td>
<td><?php echo $log_category; ?></td>
<td><?php echo $log_details; ?></td>
</tr>
<?php
}
?>
</tbody>
</table>
</div>
<?php require_once "../includes/filter_footer.php";
?>
</div>
</div>
<?php
require_once "../includes/footer.php";

171
admin/app_logs.php Normal file
View File

@@ -0,0 +1,171 @@
<?php
// Default Column Sortby Filter
$sort = "app_log_id";
$order = "DESC";
require_once "includes/inc_all_admin.php";
// Log Type Filter
if (isset($_GET['type']) & !empty($_GET['type'])) {
$log_type_query = "AND (app_log_type = '" . escapeSql($_GET['type']) . "')";
$type_filter = escapeHtml($_GET['type']);
} else {
// Default - any
$log_type_query = '';
$type_filter = '';
}
// Log Category Filter
if (isset($_GET['category']) & !empty($_GET['catergory'])) {
$log_category_query = "AND (app_log_category = '" . escapeSql($_GET['category']) . "')";
$category_filter = escapeHtml($_GET['category']);
} else {
// Default - any
$log_category_query = '';
$category_filter = '';
}
$sql = mysqli_query(
$mysqli,
"SELECT SQL_CALC_FOUND_ROWS app_log_category, app_log_created_at, app_log_details, app_log_id, app_log_type FROM app_logs
WHERE (app_log_type LIKE '%$q%' OR app_log_category LIKE '%$q%' OR app_log_details LIKE '%$q%')
AND DATE(app_log_created_at) BETWEEN '$dtf' AND '$dtt'
$log_type_query
$log_category_query
ORDER BY $sort $order LIMIT $record_from, $record_to"
);
$num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
?>
<div class="card card-dark">
<div class="card-header py-3">
<h3 class="card-title"><i class="fas fa-fw fa-history me-2"></i>App Logs</h3>
</div>
<div class="card-body">
<form autocomplete="off">
<div class="row">
<div class="col-sm-4">
<div class="mb-3">
<div class="input-group">
<input type="search" class="form-control" name="q" value="<?php if (isset($q)) { echo stripslashes(escapeHtml($q)); } ?>" placeholder="Search app logs">
<button class="btn btn-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#advancedFilter"><i class="fas fa-filter"></i></button>
<button class="btn btn-primary"><i class="fa fa-search"></i></button>
</div>
</div>
</div>
<div class="col-sm-2">
<div class="mb-3">
<select class="form-select select2" name="type" onchange="this.form.submit()">
<option value="">- All Types -</option>
<?php
$sql_types_filter = mysqli_query($mysqli, "SELECT DISTINCT app_log_type FROM app_logs ORDER BY app_log_type ASC");
while ($row = mysqli_fetch_assoc($sql_types_filter)) {
$log_type = escapeHtml($row['app_log_type']);
?>
<option <?php if ($type_filter == $log_type) { echo "selected"; } ?>><?= $log_type ?></option>
<?php
}
?>
</select>
</div>
</div>
<div class="col-sm-2">
<div class="mb-3">
<select class="form-select select2" name="category" onchange="this.form.submit()">
<option value="">- All Categories -</option>
<?php
$sql_categories_filter = mysqli_query($mysqli, "SELECT DISTINCT app_log_category FROM app_logs ORDER BY app_log_category ASC");
while ($row = mysqli_fetch_assoc($sql_categories_filter)) {
$log_category = escapeHtml($row['app_log_category']);
?>
<option <?php if ($category_filter == $log_category) { echo "selected"; } ?>><?= $log_category ?></option>
<?php
}
?>
</select>
</div>
</div>
</div>
<div class="collapse mt-3 <?php if (isset($_GET['dtf']) && $_GET['dtf'] !== '1970-01-01') { echo "show"; } ?>" id="advancedFilter">
<div class="row">
<div class="col-md-3">
<div class="mb-3">
<label>Date range</label>
<input type="text" id="dateFilter" class="form-control" autocomplete="off">
<input type="hidden" name="canned_date" id="canned_date" value="<?= escapeHtml($_GET['canned_date']) ?? '' ?>">
<input type="hidden" name="dtf" id="dtf" value="<?= escapeHtml($dtf ?? '') ?>">
<input type="hidden" name="dtt" id="dtt" value="<?= escapeHtml($dtt ?? '') ?>">
</div>
</div>
</div>
</div>
</form>
<hr>
<div class="table-responsive-sm">
<table class="table table-sm table-striped table-borderless table-hover">
<thead class="text-dark <?php if ($num_rows[0] == 0) { echo "d-none"; } ?>">
<tr>
<th>
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=app_log_created_at&order=<?= $disp ?>">
Timestamp <?php if ($sort == 'app_log_created_at') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=app_log_type&order=<?= $disp ?>">
Type <?php if ($sort == 'app_log_type') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=app_log_category&order=<?= $disp ?>">
Category <?php if ($sort == 'app_log_category') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=app_log_details&order=<?= $disp ?>">
Details <?php if ($sort == 'app_log_details') { echo $order_icon; } ?>
</a>
</th>
</tr>
</thead>
<tbody>
<?php
while ($row = mysqli_fetch_assoc($sql)) {
$log_id = intval($row['app_log_id']);
$log_type = escapeHtml($row['app_log_type']);
$log_category = escapeHtml($row['app_log_category']);
$log_details = escapeHtml($row['app_log_details']);
$log_created_at = escapeHtml($row['app_log_created_at']);
?>
<tr>
<td class="font-monospace"><?= $log_created_at ?></td>
<td><?= $log_type ?></td>
<td><?= $log_category ?></td>
<td><?= $log_details ?></td>
</tr>
<?php
}
?>
</tbody>
</table>
</div>
<?php require_once "../includes/filter_footer.php";
?>
</div>
</div>
<?php
require_once "../includes/footer.php";

View File

@@ -1,283 +0,0 @@
<?php
// Default Column Sortby Filter
$sort = "log_id";
$order = "DESC";
require_once "includes/inc_all_admin.php";
// User Filter
if (isset($_GET['user']) & !empty($_GET['user'])) {
$user_query = 'AND (log_user_id = ' . intval($_GET['user']) . ')';
$user_filter = intval($_GET['user']);
} else {
// Default - any
$user_query = '';
$user_filter = '';
}
// Client Filter
if (isset($_GET['client']) & !empty($_GET['client'])) {
$client_query = 'AND (log_client_id = ' . intval($_GET['client']) . ')';
$client_filter = intval($_GET['client']);
} else {
// Default - any
$client_query = '';
$client_filter = '';
}
// Log Type Filter
if (isset($_GET['type']) & !empty($_GET['type'])) {
$log_type_query = "AND (log_type = '" . sanitizeInput($_GET['type']) . "')";
$type_filter = nullable_htmlentities($_GET['type']);
} else {
// Default - any
$log_type_query = '';
$type_filter = '';
}
// Log Action Filter
if (isset($_GET['action']) & !empty($_GET['action'])) {
$log_action_query = "AND (log_action = '" . sanitizeInput($_GET['action']) . "')";
$action_filter = nullable_htmlentities($_GET['action']);
} else {
// Default - any
$log_action_query = '';
$action_filter = '';
}
$sql = mysqli_query(
$mysqli,
"SELECT SQL_CALC_FOUND_ROWS * FROM logs
LEFT JOIN users ON log_user_id = user_id
LEFT JOIN clients ON log_client_id = client_id
WHERE (log_type LIKE '%$q%' OR log_action LIKE '%$q%' OR log_description LIKE '%$q%' OR log_ip LIKE '%$q%' OR log_user_agent LIKE '%$q%' OR user_name LIKE '%$q%' OR client_name LIKE '%$q%')
AND DATE(log_created_at) BETWEEN '$dtf' AND '$dtt'
$user_query
$client_query
$log_type_query
$log_action_query
ORDER BY $sort $order LIMIT $record_from, $record_to"
);
$num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
?>
<div class="card card-dark">
<div class="card-header py-3">
<h3 class="card-title"><i class="fas fa-fw fa-history mr-2"></i>Audit Logs</h3>
</div>
<div class="card-body">
<form class="mb-4" autocomplete="off">
<div class="row">
<div class="col-sm-4">
<div class="input-group mb-3 mb-md-0">
<input type="search" class="form-control" name="q" value="<?php if (isset($q)) { echo stripslashes(nullable_htmlentities($q)); } ?>" placeholder="Search audit logs">
<div class="input-group-append">
<button class="btn btn-secondary" type="button" data-toggle="collapse" data-target="#advancedFilter"><i class="fas fa-filter"></i></button>
<button class="btn btn-primary"><i class="fa fa-search"></i></button>
</div>
</div>
</div>
<div class="col-sm-2">
<div class="input-group mb-3 mb-md-0">
<select class="form-control select2" name="client" onchange="this.form.submit()">
<option value="">- All Clients -</option>
<?php
$sql_clients_filter = mysqli_query($mysqli, "SELECT * FROM clients ORDER BY client_name ASC");
while ($row = mysqli_fetch_array($sql_clients_filter)) {
$client_id = intval($row['client_id']);
$client_name = nullable_htmlentities($row['client_name']);
?>
<option <?php if ($client_filter == $client_id) { echo "selected"; } ?> value="<?php echo $client_id; ?>"><?php echo $client_name; ?></option>
<?php
}
?>
</select>
</div>
</div>
<div class="col-sm-2">
<div class="input-group mb-3 mb-md-0">
<select class="form-control select2" name="user" onchange="this.form.submit()">
<option value="">- All Users -</option>
<?php
$sql_users_filter = mysqli_query($mysqli, "SELECT * FROM users ORDER BY user_name ASC");
while ($row = mysqli_fetch_array($sql_users_filter)) {
$user_id = intval($row['user_id']);
$user_name = nullable_htmlentities($row['user_name']);
?>
<option <?php if ($user_filter == $user_id) { echo "selected"; } ?> value="<?php echo $user_id; ?>"><?php echo $user_name; ?></option>
<?php
}
?>
</select>
</div>
</div>
<div class="col-sm-2">
<div class="input-group mb-3 mb-md-0">
<select class="form-control select2" name="type" onchange="this.form.submit()">
<option value="">- All Types -</option>
<?php
$sql_types_filter = mysqli_query($mysqli, "SELECT DISTINCT log_type FROM logs ORDER BY log_type ASC");
while ($row = mysqli_fetch_array($sql_types_filter)) {
$log_type = nullable_htmlentities($row['log_type']);
?>
<option <?php if ($type_filter == $log_type) { echo "selected"; } ?>><?php echo $log_type; ?></option>
<?php
}
?>
</select>
</div>
</div>
<div class="col-sm-2">
<div class="input-group mb-3 mb-md-0">
<select class="form-control select2" name="action" onchange="this.form.submit()">
<option value="">- All Actions -</option>
<?php
$sql_actions_filter = mysqli_query($mysqli, "SELECT DISTINCT log_action FROM logs ORDER BY log_action ASC");
while ($row = mysqli_fetch_array($sql_actions_filter)) {
$log_action = nullable_htmlentities($row['log_action']);
?>
<option <?php if ($action_filter == $log_action) { echo "selected"; } ?>><?php echo $log_action; ?></option>
<?php
}
?>
</select>
</div>
</div>
</div>
<div class="collapse mt-3 <?php if (isset($_GET['dtf']) && $_GET['dtf'] !== '1970-01-01') { echo "show"; } ?>" id="advancedFilter">
<div class="row">
<div class="col-md-3">
<div class="form-group">
<label>Date range</label>
<input type="text" id="dateFilter" class="form-control" autocomplete="off">
<input type="hidden" name="canned_date" id="canned_date" value="<?php echo nullable_htmlentities($_GET['canned_date']) ?? ''; ?>">
<input type="hidden" name="dtf" id="dtf" value="<?php echo nullable_htmlentities($dtf ?? ''); ?>">
<input type="hidden" name="dtt" id="dtt" value="<?php echo nullable_htmlentities($dtt ?? ''); ?>">
</div>
</div>
</div>
</div>
</form>
<hr>
<div class="table-responsive-sm">
<table class="table table-sm table-striped table-borderless table-hover">
<thead class="text-dark <?php if ($num_rows[0] == 0) { echo "d-none"; } ?> text-nowrap">
<tr>
<th>
<a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=log_created_at&order=<?php echo $disp; ?>">
Timestamp <?php if ($sort == 'log_created_at') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=user_name&order=<?php echo $disp; ?>">
User <?php if ($sort == 'user_name') { echo $order_icon; } ?>
</a>
</th>
<?php if (empty($client)) { ?>
<th>
<a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=client_name&order=<?php echo $disp; ?>">
Client <?php if ($sort == 'client_name') { echo $order_icon; } ?>
</a>
</th>
<?php } ?>
<th>
<a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=log_type&order=<?php echo $disp; ?>">
Type <?php if ($sort == 'log_type') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=log_action&order=<?php echo $disp; ?>">
Action <?php if ($sort == 'log_action') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=log_description&order=<?php echo $disp; ?>">
Description <?php if ($sort == 'log_description') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=log_ip&order=<?php echo $disp; ?>">
IP Address <?php if ($sort == 'log_ip') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=log_user_agent&order=<?php echo $disp; ?>">
User Agent <?php if ($sort == 'log_user_agent') { echo $order_icon; } ?>
</a>
</th>
</tr>
</thead>
<tbody>
<?php
while ($row = mysqli_fetch_array($sql)) {
$log_id = intval($row['log_id']);
$log_type = nullable_htmlentities($row['log_type']);
$log_action = nullable_htmlentities($row['log_action']);
$log_description = nullable_htmlentities($row['log_description']);
$log_ip = nullable_htmlentities($row['log_ip']);
$log_user_agent = nullable_htmlentities($row['log_user_agent']);
$log_user_os = getOS($log_user_agent);
$log_user_browser = getWebBrowser($log_user_agent);
$log_created_at = nullable_htmlentities($row['log_created_at']);
$user_id = intval($row['user_id']);
$user_name = nullable_htmlentities($row['user_name']);
if (empty($user_name)) {
$user_name_display = "-";
} else {
$user_name_display = $user_name;
}
$client_name = nullable_htmlentities($row['client_name']);
$client_id = intval($row['client_id']);
if (empty($client_name)) {
$client_name_display = "-";
} else {
$client_name_display = "<a href='../agent/client_overview.php?client_id=$client_id'>$client_name</a>";
}
$log_entity_id = intval($row['log_entity_id']);
?>
<tr>
<td><?php echo $log_created_at; ?></td>
<td><?php echo $user_name_display; ?></td>
<?php if(empty($client)) { ?>
<td><?php echo $client_name_display; ?></td>
<?php } ?>
<td><?php echo $log_type; ?></td>
<td><?php echo $log_action; ?></td>
<td><?php echo $log_description; ?></td>
<td><?php echo $log_ip; ?></td>
<td><?php echo "$log_user_os<div class='text-secondary'>$log_user_browser</div>"; ?></td>
</tr>
<?php
}
?>
</tbody>
</table>
</div>
<?php require_once "../includes/filter_footer.php";
?>
</div>
</div>
<?php
require_once "../includes/footer.php";

281
admin/audit_logs.php Normal file
View File

@@ -0,0 +1,281 @@
<?php
// Default Column Sortby Filter
$sort = "log_id";
$order = "DESC";
require_once "includes/inc_all_admin.php";
// User Filter
if (isset($_GET['user']) & !empty($_GET['user'])) {
$user_query = 'AND (log_user_id = ' . intval($_GET['user']) . ')';
$user_filter = intval($_GET['user']);
} else {
// Default - any
$user_query = '';
$user_filter = '';
}
// Client Filter
if (isset($_GET['client']) & !empty($_GET['client'])) {
$client_query = 'AND (log_client_id = ' . intval($_GET['client']) . ')';
$client_filter = intval($_GET['client']);
} else {
// Default - any
$client_query = '';
$client_filter = '';
}
// Log Type Filter
if (isset($_GET['type']) & !empty($_GET['type'])) {
$log_type_query = "AND (log_type = '" . escapeSql($_GET['type']) . "')";
$type_filter = escapeHtml($_GET['type']);
} else {
// Default - any
$log_type_query = '';
$type_filter = '';
}
// Log Action Filter
if (isset($_GET['action']) & !empty($_GET['action'])) {
$log_action_query = "AND (log_action = '" . escapeSql($_GET['action']) . "')";
$action_filter = escapeHtml($_GET['action']);
} else {
// Default - any
$log_action_query = '';
$action_filter = '';
}
$sql = mysqli_query(
$mysqli,
"SELECT SQL_CALC_FOUND_ROWS client_id, client_name, log_action, log_created_at, log_description, log_entity_id, log_id,
log_ip, log_type, log_user_agent, user_id, user_name FROM logs
LEFT JOIN users ON log_user_id = user_id
LEFT JOIN clients ON log_client_id = client_id
WHERE (log_type LIKE '%$q%' OR log_action LIKE '%$q%' OR log_description LIKE '%$q%' OR log_ip LIKE '%$q%' OR log_user_agent LIKE '%$q%' OR user_name LIKE '%$q%' OR client_name LIKE '%$q%')
AND DATE(log_created_at) BETWEEN '$dtf' AND '$dtt'
$user_query
$client_query
$log_type_query
$log_action_query
ORDER BY $sort $order LIMIT $record_from, $record_to"
);
$num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
?>
<div class="card card-dark">
<div class="card-header py-3">
<h3 class="card-title"><i class="fas fa-fw fa-history me-2"></i>Audit Logs</h3>
</div>
<div class="card-body">
<form class="mb-4" autocomplete="off">
<div class="row">
<div class="col-sm-4">
<div class="input-group mb-3 mb-md-0">
<input type="search" class="form-control" name="q" value="<?php if (isset($q)) { echo stripslashes(escapeHtml($q)); } ?>" placeholder="Search audit logs">
<button class="btn btn-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#advancedFilter"><i class="fas fa-filter"></i></button>
<button class="btn btn-primary"><i class="fa fa-search"></i></button>
</div>
</div>
<div class="col-sm-2">
<div class="input-group mb-3 mb-md-0">
<select class="form-select select2" name="client" onchange="this.form.submit()">
<option value="">- All Clients -</option>
<?php
$sql_clients_filter = mysqli_query($mysqli, "SELECT client_id, client_name FROM clients ORDER BY client_name ASC");
while ($row = mysqli_fetch_assoc($sql_clients_filter)) {
$client_id = intval($row['client_id']);
$client_name = escapeHtml($row['client_name']);
?>
<option <?php if ($client_filter == $client_id) { echo "selected"; } ?> value="<?= $client_id ?>"><?= $client_name ?></option>
<?php
}
?>
</select>
</div>
</div>
<div class="col-sm-2">
<div class="input-group mb-3 mb-md-0">
<select class="form-select select2" name="user" onchange="this.form.submit()">
<option value="">- All Users -</option>
<?php
$sql_users_filter = mysqli_query($mysqli, "SELECT user_id, user_name FROM users ORDER BY user_name ASC");
while ($row = mysqli_fetch_assoc($sql_users_filter)) {
$user_id = intval($row['user_id']);
$user_name = escapeHtml($row['user_name']);
?>
<option <?php if ($user_filter == $user_id) { echo "selected"; } ?> value="<?= $user_id ?>"><?= $user_name ?></option>
<?php
}
?>
</select>
</div>
</div>
<div class="col-sm-2">
<div class="input-group mb-3 mb-md-0">
<select class="form-select select2" name="type" onchange="this.form.submit()">
<option value="">- All Types -</option>
<?php
$sql_types_filter = mysqli_query($mysqli, "SELECT DISTINCT log_type FROM logs ORDER BY log_type ASC");
while ($row = mysqli_fetch_assoc($sql_types_filter)) {
$log_type = escapeHtml($row['log_type']);
?>
<option <?php if ($type_filter == $log_type) { echo "selected"; } ?>><?= $log_type ?></option>
<?php
}
?>
</select>
</div>
</div>
<div class="col-sm-2">
<div class="input-group mb-3 mb-md-0">
<select class="form-select select2" name="action" onchange="this.form.submit()">
<option value="">- All Actions -</option>
<?php
$sql_actions_filter = mysqli_query($mysqli, "SELECT DISTINCT log_action FROM logs ORDER BY log_action ASC");
while ($row = mysqli_fetch_assoc($sql_actions_filter)) {
$log_action = escapeHtml($row['log_action']);
?>
<option <?php if ($action_filter == $log_action) { echo "selected"; } ?>><?= $log_action ?></option>
<?php
}
?>
</select>
</div>
</div>
</div>
<div class="collapse mt-3 <?php if (isset($_GET['dtf']) && $_GET['dtf'] !== '1970-01-01') { echo "show"; } ?>" id="advancedFilter">
<div class="row">
<div class="col-md-3">
<div class="mb-3">
<label>Date range</label>
<input type="text" id="dateFilter" class="form-control" autocomplete="off">
<input type="hidden" name="canned_date" id="canned_date" value="<?= escapeHtml($_GET['canned_date']) ?? '' ?>">
<input type="hidden" name="dtf" id="dtf" value="<?= escapeHtml($dtf ?? '') ?>">
<input type="hidden" name="dtt" id="dtt" value="<?= escapeHtml($dtt ?? '') ?>">
</div>
</div>
</div>
</div>
</form>
<hr>
<div class="table-responsive-sm">
<table class="table table-sm table-striped table-borderless table-hover">
<thead class="text-dark <?php if ($num_rows[0] == 0) { echo "d-none"; } ?> text-nowrap">
<tr>
<th>
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=log_created_at&order=<?= $disp ?>">
Timestamp <?php if ($sort == 'log_created_at') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=user_name&order=<?= $disp ?>">
User <?php if ($sort == 'user_name') { echo $order_icon; } ?>
</a>
</th>
<?php if (empty($client)) { ?>
<th>
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=client_name&order=<?= $disp ?>">
Client <?php if ($sort == 'client_name') { echo $order_icon; } ?>
</a>
</th>
<?php } ?>
<th>
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=log_type&order=<?= $disp ?>">
Type <?php if ($sort == 'log_type') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=log_action&order=<?= $disp ?>">
Action <?php if ($sort == 'log_action') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=log_description&order=<?= $disp ?>">
Description <?php if ($sort == 'log_description') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=log_ip&order=<?= $disp ?>">
IP Address <?php if ($sort == 'log_ip') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=log_user_agent&order=<?= $disp ?>">
User Agent <?php if ($sort == 'log_user_agent') { echo $order_icon; } ?>
</a>
</th>
</tr>
</thead>
<tbody>
<?php
while ($row = mysqli_fetch_assoc($sql)) {
$log_id = intval($row['log_id']);
$log_type = escapeHtml($row['log_type']);
$log_action = escapeHtml($row['log_action']);
$log_description = escapeHtml($row['log_description']);
$log_ip = escapeHtml($row['log_ip']);
$log_user_agent = escapeHtml($row['log_user_agent']);
$log_user_os = getOS($log_user_agent);
$log_user_browser = getWebBrowser($log_user_agent);
$log_created_at = escapeHtml($row['log_created_at']);
$user_id = intval($row['user_id']);
$user_name = escapeHtml($row['user_name']);
if (empty($user_name)) {
$user_name_display = "-";
} else {
$user_name_display = $user_name;
}
$client_name = escapeHtml($row['client_name']);
$client_id = intval($row['client_id']);
if (empty($client_name)) {
$client_name_display = "-";
} else {
$client_name_display = "<a href='../agent/client_overview.php?client_id=$client_id'>$client_name</a>";
}
$log_entity_id = intval($row['log_entity_id']);
?>
<tr>
<td class="font-monospace"><?= $log_created_at ?></td>
<td><?= $user_name_display ?></td>
<?php if(empty($client)) { ?>
<td><?= $client_name_display ?></td>
<?php } ?>
<td><?= $log_type ?></td>
<td><?= $log_action ?></td>
<td><?= $log_description ?></td>
<td class="font-monospace"><?= $log_ip ?></td>
<td><?= "$log_user_os<div class='text-secondary'>$log_user_browser</div>" ?></td>
</tr>
<?php
}
?>
</tbody>
</table>
</div>
<?php require_once "../includes/filter_footer.php";
?>
</div>
</div>
<?php
require_once "../includes/footer.php";

View File

@@ -1,32 +1,286 @@
<?php
require_once "includes/inc_all_admin.php";
$backup_key = backupEncryptionKey();
$backup_dir = backupStorageDir();
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT config_enable_cron, config_cron_last_dispatch_at, config_backup_retention_days, config_backup_retention_count, config_backup_cron_type FROM settings WHERE company_id = 1"));
$config_enable_cron = intval($row['config_enable_cron']);
$cron_last_dispatch_at = $row['config_cron_last_dispatch_at'];
$config_backup_retention_days = intval($row['config_backup_retention_days']);
$config_backup_retention_count = intval($row['config_backup_retention_count']);
$config_backup_cron_type = $row['config_backup_cron_type'];
// Same heartbeat rule as Maintenance > Cron - archives are built by the dispatcher, so a dead
// crontab means the buttons below queue work that never runs
$cron_is_running = $cron_last_dispatch_at !== null && (time() - strtotime($cron_last_dispatch_at)) < 300;
$backup_job = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT cron_job_enabled, cron_job_daily_at FROM cron_jobs WHERE cron_job_name = 'backup'"));
$backups = mysqli_query($mysqli, "SELECT backup_created_at, backup_error, backup_id, backup_size, backup_source, backup_status,
backup_type FROM backups ORDER BY backup_created_at DESC LIMIT 100");
$pending_count = intval(mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT(*) AS c FROM backups WHERE backup_status IN ('Pending','Running')"))['c']);
?>
<?php
// Shown once, immediately after an export, then dropped - it must not survive a refresh
if (!empty($_SESSION['backup_master_key_reveal'])) {
$master_key_reveal = $_SESSION['backup_master_key_reveal'];
unset($_SESSION['backup_master_key_reveal']);
?>
<div class="alert alert-warning">
<h5><i class="fas fa-fw fa-key me-2"></i>Master encryption key</h5>
<p class="mb-2">Shown once. Refreshing this page will not show it again.</p>
<input type="text" class="form-control font-monospace" value="<?= escapeHtml($master_key_reveal) ?>" readonly onclick="this.select();">
</div>
<?php } ?>
<?php if ($backup_key === '') { ?>
<div class="alert alert-danger">
<h5><i class="fas fa-fw fa-exclamation-triangle me-2"></i>No backup encryption key</h5>
ITFlow could not write a backup encryption key to <strong>config.php</strong>, so it cannot produce an encrypted backup.
Make config.php writable by the web server user and reload this page, or add a line like
<code>$config_backup_key = '&lt;32 random characters&gt;';</code> to it yourself.
</div>
<?php } ?>
<?php if (!$cron_is_running) { ?>
<div class="alert alert-danger">
<h5><i class="fas fa-fw fa-exclamation-triangle me-2"></i>Cron is not running</h5>
Backups are built by the cron dispatcher, not by your browser. Until cron is running, anything you
start here will sit in the queue. See <a href="cron.php">Maintenance &gt; Cron</a>.
</div>
<?php } elseif ($config_enable_cron == 0) { ?>
<div class="alert alert-warning">
<i class="fas fa-fw fa-exclamation-circle me-2"></i>Cron is switched off in
<a href="cron.php">Maintenance &gt; Cron</a>.
</div>
<?php } ?>
<div class="card card-dark mb-3">
<div class="card-header py-3">
<h3 class="card-title"><i class="fas fa-fw fa-database mr-2"></i>Download Database</h3>
<h3 class="card-title"><i class="fas fa-fw fa-cloud-upload-alt me-2"></i>Create a Backup</h3>
</div>
<div class="card-body" style="text-align: center;">
<div class="alert alert-secondary">If you are unable to back up the entire VM, you'll need to back up the files & database individually. There is no built-in restore. See the <a href="https://docs.itflow.org/backups" target="_blank">docs here</a>.</div>
<a class="btn btn-primary btn-lg p-3" href="post.php?download_backup&csrf_token=<?php echo $_SESSION['csrf_token'] ?>"><i class="fas fa-fw fa-4x fa-download"></i><br><br>Download Backup</a>
<div class="card-body">
<?php if ($pending_count > 0) { ?>
<div class="alert alert-info">
<i class="fas fa-fw fa-spinner me-2"></i><strong><?= $pending_count ?></strong> backup<?= $pending_count == 1 ? ' is' : 's are' ?>
queued or building. You will get a notification when ready - this page does not refresh itself.
</div>
<?php } ?>
<div class="row">
<div class="col-md-4 mb-3">
<div class="border rounded p-3 h-100 text-center">
<i class="fas fa-fw fa-3x fa-box-open text-dark mb-3"></i>
<h5>Full Backup</h5>
<p class="text-muted small">The database and everything in the uploads folder. This is the one to keep.</p>
<a class="btn btn-primary <?= $backup_key === '' ? 'disabled' : '' ?>" href="post.php?queue_backup=full&csrf_token=<?= $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-play me-2"></i>Start
</a>
</div>
</div>
<div class="col-md-4 mb-3">
<div class="border rounded p-3 h-100 text-center">
<i class="fas fa-fw fa-3x fa-database text-dark mb-3"></i>
<h5>Database Only</h5>
<p class="text-muted small">Just the SQL dump. Much smaller and much quicker, but no attachments or documents.</p>
<a class="btn btn-primary <?= $backup_key === '' ? 'disabled' : '' ?>" href="post.php?queue_backup=database&csrf_token=<?= $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-play me-2"></i>Start
</a>
</div>
</div>
<div class="col-md-4 mb-3">
<div class="border rounded p-3 h-100 text-center">
<i class="fas fa-fw fa-3x fa-key text-dark mb-3"></i>
<h5>Master Key</h5>
<p class="text-muted small">The credential vault key. Only needed if every user password is lost - a normal restore recovers the vault on its own.</p>
<button type="button" class="btn btn-secondary" data-bs-toggle="modal" data-bs-target="#masterKeyModal">
<i class="fas fa-fw fa-key me-2"></i>Export
</button>
</div>
</div>
</div>
</div>
</div>
<div class="card card-dark mb-3">
<div class="card-header py-3">
<h3 class="card-title"><i class="fas fa-fw fa-archive me-2"></i>Backups</h3>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-striped table-borderless mb-0">
<thead class="text-dark">
<tr>
<th>Type</th>
<th>Created</th>
<th>Size</th>
<th>Source</th>
<th>Status</th>
<th class="text-end">Action</th>
</tr>
</thead>
<tbody>
<?php if (mysqli_num_rows($backups) === 0) { ?>
<tr><td colspan="6" class="text-center text-muted py-4">No backups yet.</td></tr>
<?php } ?>
<?php while ($backup = mysqli_fetch_assoc($backups)) {
$backup_id = intval($backup['backup_id']);
$status = $backup['backup_status'];
$badge = 'secondary';
if ($status === 'Complete') { $badge = 'success'; }
if ($status === 'Failed') { $badge = 'danger'; }
if ($status === 'Missing') { $badge = 'warning'; }
if ($status === 'Running' || $status === 'Pending') { $badge = 'info'; }
?>
<tr>
<td><?= escapeHtml(backupTypeLabel($backup['backup_type'])) ?></td>
<td><?= escapeHtml($backup['backup_created_at']) ?></td>
<td><?= $backup['backup_size'] > 0 ? escapeHtml(backupFormatBytes($backup['backup_size'])) : '-' ?></td>
<td><?= escapeHtml($backup['backup_source']) ?></td>
<td>
<span class="badge text-bg-<?= $badge ?>"><?= escapeHtml($status) ?></span>
<?php if (!empty($backup['backup_error'])) { ?>
<br><small class="text-danger"><?= escapeHtml($backup['backup_error']) ?></small>
<?php } ?>
</td>
<td class="text-end">
<?php if ($status === 'Complete') { ?>
<a class="btn btn-sm btn-primary" href="backup_download.php?backup_id=<?= $backup_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-download"></i>
</a>
<?php } ?>
<a class="btn btn-sm btn-danger confirm-link" href="post.php?delete_backup=<?= $backup_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-trash"></i>
</a>
</td>
</tr>
<?php } ?>
</tbody>
</table>
</div>
</div>
</div>
<div class="card card-dark mb-3">
<div class="card-header py-3">
<h3 class="card-title"><i class="fas fa-fw fa-lock me-2"></i>Encryption Key</h3>
</div>
<div class="card-body">
<div class="alert alert-warning mb-3">
<i class="fas fa-fw fa-exclamation-triangle me-2"></i>
<strong>Write this down and keep it somewhere other than this server.</strong>
Every backup is encrypted with it, and without it a backup cannot be restored - not by you,
not by anyone. It is stored in config.php and never in the database, which is what stops a
stolen backup from carrying its own key.
</div>
<?php if ($backup_key !== '') { ?>
<div class="input-group col-md-6 px-0">
<input type="text" class="form-control font-monospace" value="<?= escapeHtml($backup_key) ?>" readonly onclick="this.select();">
<button class="btn btn-secondary" type="button" onclick="navigator.clipboard.writeText('<?= escapeHtml($backup_key) ?>');">
<i class="fas fa-fw fa-copy"></i>
</button>
</div>
<?php } ?>
<p class="text-muted small mt-3 mb-0">
Archives are AES-256 encrypted zips. <strong>7-Zip, WinZip, PeaZip and Keka</strong> can open them with this key.
The <code>unzip</code> command, Windows Explorer and the macOS Archive Utility cannot - they do not support AES.
</p>
</div>
</div>
<div class="card card-dark mb-3">
<div class="card-header py-3">
<h3 class="card-title"><i class="fas fa-fw fa-clock me-2"></i>Scheduled Backups &amp; Retention</h3>
</div>
<div class="card-body">
<form action="post.php" method="POST" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<div class="row g-2">
<div class="mb-3 col-md-4">
<label>Scheduled backup type</label>
<select class="form-select" name="config_backup_cron_type">
<option <?= $config_backup_cron_type === 'full' ? 'selected' : '' ?> value="full">Full Backup</option>
<option <?= $config_backup_cron_type === 'database' ? 'selected' : '' ?> value="database">Database Only</option>
</select>
</div>
<div class="mb-3 col-md-4">
<label>Keep backups for (days)</label>
<input type="number" class="form-control" name="config_backup_retention_days" min="0" value="<?= intval($config_backup_retention_days) ?>">
<small class="text-muted">0 disables age-based deletion.</small>
</div>
<div class="mb-3 col-md-4">
<label>Keep at most (per type)</label>
<input type="number" class="form-control" name="config_backup_retention_count" min="1" value="<?= intval($config_backup_retention_count) ?>">
<small class="text-muted">Counted separately for each type. The newest of each is never deleted.</small>
</div>
</div>
<button type="submit" name="edit_backup_settings" class="btn btn-primary"><i class="fas fa-fw fa-check me-2"></i>Save</button>
</form>
<hr>
<p class="mb-0">
<?php if (!empty($backup_job) && intval($backup_job['cron_job_enabled']) === 1) { ?>
<i class="fas fa-fw fa-check text-success me-2"></i>Scheduled backups run daily at
<strong><?= escapeHtml(substr((string)$backup_job['cron_job_daily_at'], 0, 5)) ?></strong>.
<?php } else { ?>
<i class="fas fa-fw fa-times text-danger me-2"></i>Scheduled backups are switched off.
<?php } ?>
Turn them on or change the time in <a href="cron.php">Maintenance &gt; Cron</a>.
</p>
<p class="text-muted small mt-2 mb-0">
Old backups are removed by the nightly job, never by the backup itself, so a failed nightly
cannot delete an archive that was never replaced.
</p>
</div>
</div>
<div class="card card-dark">
<div class="card-header py-3">
<h3 class="card-title"><i class="fas fa-fw fa-key mr-2"></i>Backup Master Encryption Key</h3>
<h3 class="card-title"><i class="fas fa-fw fa-undo me-2"></i>Restoring</h3>
</div>
<div class="card-body">
<div class="card-body">
<form action="post.php" method="POST">
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token'] ?>">
<div class="d-flex justify-content-center">
<div class="input-group col-sm-4">
<input type="password" class="form-control" placeholder="Enter your account password" name="password" autocomplete="new-password" required>
<div class="input-group-append">
<button class="btn btn-primary" type="submit" name="backup_master_key"><i class="fas fa-key"></i></button>
</div>
<p>Restoring replaces the database and the uploads folder with what is in the archive. It cannot be done from here, on purpose - a running install is the wrong place to be dropping its own tables from a browser.</p>
<p class="mb-2"><strong>From the command line</strong> - the only option that works for large backups:</p>
<pre class="bg-dark text-white p-2"><?= escapeHtml("php " . dirname(__DIR__) . "/scripts/restore_cli.php --file=/path/to/backup.zip") ?></pre>
<p class="mb-0"><strong>From a browser</strong>, on a fresh install only, the setup wizard has a restore step at <code>/setup</code>. Once an install has users, that step closes itself.</p>
</div>
</div>
<div class="modal" id="masterKeyModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="fas fa-fw fa-key me-2"></i>Export Master Key</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form action="post.php" method="POST" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<div class="modal-body">
<div class="alert alert-warning">
This key decrypts every credential in this install. It is shown on screen and is not written anywhere.
</div>
<div class="mb-3">
<label>Confirm your account password</label>
<input type="password" class="form-control" name="password" autocomplete="new-password" required>
</div>
</div>
<div class="modal-footer">
<button type="submit" name="backup_master_key" class="btn btn-primary"><i class="fas fa-fw fa-key me-2"></i>Show Master Key</button>
</div>
</form>
</div>
@@ -35,4 +289,3 @@ require_once "includes/inc_all_admin.php";
<?php
require_once "../includes/footer.php";

61
admin/backup_download.php Normal file
View File

@@ -0,0 +1,61 @@
<?php
/*
* ITFlow - GET /admin/backup_download.php
* Streams a backup archive to an administrator
*
* Deliberately NOT agent/file.php: that gates on module_client and resolves paths under
* uploads/clients/<id>/, which would hand a full database dump to any agent with client
* read access. A backup is an admin artifact and gets an admin-only path of its own.
*/
require_once "../config.php";
require_once "../functions.php";
require_once "../includes/check_login.php";
enforceAdminPermission();
validateCSRFToken();
if (!isset($_GET['backup_id'])) {
http_response_code(400);
exit("Backup ID required");
}
$backup_id = intval($_GET['backup_id']);
$sql = mysqli_query($mysqli, "SELECT backup_file_name FROM backups WHERE backup_id = $backup_id AND backup_status = 'Complete' LIMIT 1");
if (mysqli_num_rows($sql) !== 1) {
http_response_code(404);
exit("Backup not found");
}
$row = mysqli_fetch_assoc($sql);
$file_path = backupResolvePath($row['backup_file_name']);
if ($file_path === false || !is_file($file_path)) {
mysqli_query($mysqli, "UPDATE backups SET backup_status = 'Missing' WHERE backup_id = $backup_id");
http_response_code(404);
exit("Backup file is no longer on disk");
}
$file_name = basename($file_path);
logAudit("Backup", "Download", ($session_name ?? 'Unknown User') . " downloaded backup " . escapeSql($file_name));
mysqli_query($mysqli, "UPDATE backups SET backup_downloaded_at = NOW() WHERE backup_id = $backup_id");
header("Content-Type: application/zip");
header("Content-Disposition: attachment; filename=\"$file_name\"");
header("Content-Length: " . filesize($file_path));
header("X-Content-Type-Options: nosniff");
header("Cache-Control: private, no-store");
header("Pragma: no-cache");
// Clear output buffers so a multi-gigabyte archive streams instead of loading into memory
while (ob_get_level()) {
ob_end_clean();
}
readfile($file_path);
exit;

217
admin/categories.php Normal file
View File

@@ -0,0 +1,217 @@
<?php
// Default Column Sortby Filter
$sort = "category_name";
$order = "ASC";
require_once "includes/inc_all_admin.php";
if (isset($_GET['category'])) {
$category = escapeSql($_GET['category']);
} else {
$category = "Expense";
}
$sql = mysqli_query(
$mysqli,
"SELECT SQL_CALC_FOUND_ROWS category_color, category_description, category_id, category_name FROM categories
WHERE category_name LIKE '%$q%'
AND category_type = '$category'
AND category_$archive_query
ORDER BY $sort $order LIMIT $record_from, $record_to"
);
$num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
// Category types shown in the left nav
$category_types = [
'Expense' => ['label' => 'Expense', 'icon' => 'fa-shopping-cart'],
'Income' => ['label' => 'Income', 'icon' => 'fa-hand-holding-usd'],
'Referral' => ['label' => 'Referral', 'icon' => 'fa-share-alt'],
'Ticket' => ['label' => 'Ticket', 'icon' => 'fa-life-ring'],
'network_interface' => ['label' => 'Network Interface', 'icon' => 'fa-ethernet'],
'asset_status' => ['label' => 'Asset Status', 'icon' => 'fa-heartbeat'],
'software_type' => ['label' => 'Software Type', 'icon' => 'fa-cube'],
'rack_type' => ['label' => 'Rack Type', 'icon' => 'fa-server'],
'contact_note_type' => ['label' => 'Contact Note Type', 'icon' => 'fa-address-book'],
'asset_note_type' => ['label' => 'Asset Note Type', 'icon' => 'fa-desktop'],
];
// Label for the selected type, falling back for anything not in the map
$category_label = $category_types[$category]['label'] ?? ucwords(str_replace('_', ' ', $category));
// Row count per type for the nav badges, respecting the archived view
$category_type_counts = [];
$sql_category_type_counts = mysqli_query(
$mysqli,
"SELECT category_type, COUNT(category_id) AS category_type_count FROM categories
WHERE category_$archive_query
GROUP BY category_type"
);
while ($row = mysqli_fetch_assoc($sql_category_type_counts)) {
$category_type_counts[$row['category_type']] = intval($row['category_type_count']);
}
// Archived nav item toggles the view while holding the selected type/search
$archive_toggle_query = $_GET;
unset($archive_toggle_query['page']);
$archive_toggle_query['category'] = $category;
if ($archived) {
unset($archive_toggle_query['archived']);
} else {
$archive_toggle_query['archived'] = 1;
}
$archive_toggle_url = '?' . http_build_query($archive_toggle_query);
?>
<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 me-2"></i>
<?= escapeHtml($category_label) ?> 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=<?= escapeHtml($category) ?>"><i
class="fas fa-plus me-2"></i>New <?= escapeHtml($category_label) ?> Category</button>
</div>
<?php
}
?>
</div>
<div class="card-body">
<div class="row">
<!-- Category types -->
<div class="col-md-3 border-end mb-3">
<ul class="nav nav-pills flex-column bg-light">
<?php foreach ($category_types as $category_type => $category_type_details) { ?>
<li class="nav-item">
<a class="nav-link<?php if ($category == $category_type) {
echo ' active';
} ?>" href="?category=<?= urlencode($category_type) ?>">
<i class="fa fa-fw <?= $category_type_details['icon'] ?> me-2"></i><?= escapeHtml($category_type_details['label']) ?>
<span class="badge rounded-pill bg-dark float-end mt-1"><?= $category_type_counts[$category_type] ?? 0 ?></span>
</a>
</li>
<?php } ?>
</ul>
</div>
<!-- Categories -->
<div class="col-md-9">
<form autocomplete="off">
<input type="hidden" name="category" value="<?= escapeHtml($category) ?>">
<?php if ($archived) { ?>
<input type="hidden" name="archived" value="1">
<?php } ?>
<div class="row">
<div class="col-sm-6 mb-2">
<div class="input-group">
<input type="search" class="form-control" name="q"
value="<?php if (isset($q)) {
echo stripslashes(escapeHtml($q));
} ?>"
placeholder="Search <?= escapeHtml($category_label) ?> Categories ">
<button class="btn btn-primary"><i class="fa fa-search"></i></button>
</div>
</div>
<div class="col-sm-6 mb-2">
<a href="<?= $archive_toggle_url ?>"
class="btn float-end <?php if ($archived) {
echo 'btn-primary';
} else {
echo 'btn-default';
} ?>"><i
class="fas fa-fw fa-archive me-2"></i>Archived</a>
</div>
</div>
</form>
<hr>
<div class="table-responsive-sm">
<table class="table table-striped table-borderless table-hover">
<thead class="text-dark <?php if ($num_rows[0] == 0) { echo "d-none"; } ?>">
<tr>
<th>
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=category_name&order=<?= $disp ?>">
Name <?php if ($sort == 'category_name') { echo $order_icon; } ?>
</a>
</th>
<th>Color</th>
<th class="text-center">Action</th>
</tr>
</thead>
<tbody>
<?php
while ($row = mysqli_fetch_assoc($sql)) {
$category_id = intval($row['category_id']);
$category_name = escapeHtml($row['category_name']);
$category_description = escapeHtml($row['category_description']);
$category_color = escapeHtml($row['category_color']);
?>
<tr>
<td>
<a class="text-dark ajax-modal" href="#"
data-modal-url="modals/category/category_edit.php?id=<?= $category_id ?>">
<?= $category_name ?>
<div><small class="text-secondary"><?= $category_description ?></small></div>
</a>
</td>
<td><i class="fa fa-3x fa-circle" style="color:<?= $category_color ?>;"></i></td>
<td>
<div class="dropdown dropstart text-center">
<button class="btn btn-secondary btn-sm" type="button" data-bs-toggle="dropdown">
<i class="fas fa-ellipsis-h"></i>
</button>
<div class="dropdown-menu">
<?php
if ($archived) {
?>
<a class="dropdown-item text-info confirm-link"
href="post.php?restore_category=<?= $category_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-redo me-2"></i>Restore
</a>
<a class="dropdown-item text-danger confirm-link"
href="post.php?delete_category=<?= $category_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-trash me-2"></i>Delete
</a>
<?php
} else {
?>
<a class="dropdown-item ajax-modal" href="#"
data-modal-url="modals/category/category_edit.php?id=<?= $category_id ?>">
<i class="fas fa-fw fa-edit me-2"></i>Edit
</a>
<a class="dropdown-item text-danger confirm-link"
href="post.php?archive_category=<?= $category_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-archive me-2"></i>Archive
</a>
<?php
}
?>
</div>
</div>
</td>
</tr>
<?php
}
?>
</tbody>
</table>
</div>
<?php require_once "../includes/filter_footer.php"; ?>
</div>
</div>
</div>
</div>
<?php
require_once "../includes/footer.php";

View File

@@ -1,179 +0,0 @@
<?php
// Default Column Sortby Filter
$sort = "category_name";
$order = "ASC";
require_once "includes/inc_all_admin.php";
if (isset($_GET['category'])) {
$category = sanitizeInput($_GET['category']);
} else {
$category = "Expense";
}
$sql = mysqli_query(
$mysqli,
"SELECT SQL_CALC_FOUND_ROWS * FROM categories
WHERE category_name LIKE '%$q%'
AND category_type = '$category'
AND category_$archive_query
ORDER BY $sort $order LIMIT $record_from, $record_to"
);
$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
</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>
</div>
<?php
}
?>
</div>
<div class="card-body">
<form autocomplete="off">
<input type="hidden" name="category" value="<?php echo nullable_htmlentities($category); ?>">
<div class="row">
<div class="col-sm-4 mb-2">
<div class="input-group">
<input type="search" class="form-control" name="q"
value="<?php if (isset($q)) {
echo stripslashes(nullable_htmlentities($q));
} ?>"
placeholder="Search <?php echo nullable_htmlentities($category); ?> Categories ">
<div class="input-group-append">
<button class="btn btn-primary"><i class="fa fa-search"></i></button>
</div>
</div>
</div>
<div class="col-sm-8">
<div class="btn-group float-right">
<a href="?category=Expense"
class="btn <?php if ($category == 'Expense') {
echo 'btn-primary';
} else {
echo 'btn-default';
} ?>">Expense</a>
<a href="?category=Income"
class="btn <?php if ($category == 'Income') {
echo 'btn-primary';
} else {
echo 'btn-default';
} ?>">Income</a>
<a href="?category=Referral"
class="btn <?php if ($category == 'Referral') {
echo 'btn-primary';
} else {
echo 'btn-default';
} ?>">Referral</a>
<a href="?category=Ticket"
class="btn <?php if ($category == 'Ticket') {
echo 'btn-primary';
} else {
echo 'btn-default';
} ?>">Ticket</a>
<a href="?<?php echo $url_query_strings_sort ?>&archived=1"
class="btn <?php if (isset($_GET['archived'])) {
echo 'btn-primary';
} else {
echo 'btn-default';
} ?>"><i
class="fas fa-fw fa-archive mr-2"></i>Archived</a>
</div>
</div>
</div>
</form>
<hr>
<div class="table-responsive-sm">
<table class="table table-striped table-borderless table-hover">
<thead class="text-dark <?php if ($num_rows[0] == 0) { echo "d-none"; } ?>">
<tr>
<th>
<a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=category_name&order=<?php echo $disp; ?>">
Name <?php if ($sort == 'category_name') { echo $order_icon; } ?>
</a>
</th>
<th>Color</th>
<th class="text-center">Action</th>
</tr>
</thead>
<tbody>
<?php
while ($row = mysqli_fetch_array($sql)) {
$category_id = intval($row['category_id']);
$category_name = nullable_htmlentities($row['category_name']);
$category_color = nullable_htmlentities($row['category_color']);
?>
<tr>
<td>
<a class="text-dark ajax-modal" href="#"
data-modal-url="modals/category/category_edit.php?id=<?= $category_id ?>">
<?php echo $category_name; ?>
</a>
</td>
<td><i class="fa fa-3x fa-circle" style="color:<?php echo $category_color; ?>;"></i></td>
<td>
<div class="dropdown dropleft text-center">
<button class="btn btn-secondary btn-sm" type="button" data-toggle="dropdown">
<i class="fas fa-ellipsis-h"></i>
</button>
<div class="dropdown-menu">
<?php
if ($archived) {
?>
<a class="dropdown-item text-success confirm-link"
href="post.php?unarchive_category=<?php echo $category_id; ?>">
<i class="fas fa-fw fa-archive mr-2"></i>Unarchive
</a>
<a class="dropdown-item text-danger confirm-link"
href="post.php?delete_category=<?php echo $category_id; ?>">
<i class="fas fa-fw fa-trash mr-2"></i>Delete
</a>
<?php
} else {
?>
<a class="dropdown-item ajax-modal" href="#"
data-modal-url="modals/category/category_edit.php?id=<?= $category_id ?>">
<i class="fas fa-fw fa-edit mr-2"></i>Edit
</a>
<a class="dropdown-item text-danger confirm-link"
href="post.php?archive_category=<?php echo $category_id; ?>">
<i class="fas fa-fw fa-archive mr-2"></i>Archive
</a>
<?php
}
?>
</div>
</div>
</td>
</tr>
<?php
}
?>
</tbody>
</table>
</div>
<?php require_once "../includes/filter_footer.php";
?>
</div>
</div>
<?php
require_once "../includes/footer.php";

View File

@@ -1,125 +0,0 @@
<?php
// Default Column Sort by Filter
$sort = "contract_template_name";
$order = "ASC";
require_once "includes/inc_all_admin.php";
// Search query
$sql = mysqli_query(
$mysqli,
"SELECT SQL_CALC_FOUND_ROWS * FROM contract_templates
WHERE contract_template_name LIKE '%$q%' OR contract_template_type LIKE '%$q%' OR contract_template_name LIKE '%$q%'
ORDER BY $sort $order LIMIT $record_from, $record_to"
);
$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-file-contract mr-2"></i>Contract Templates</h3>
<div class="card-tools">
<button type="button" class="btn btn-primary ajax-modal" data-modal-url="modals/contract_template/contract_template_add.php" data-modal-size="lg">
<i class="fas fa-plus mr-2"></i>New Template
</button>
</div>
</div>
<div class="card-body">
<form autocomplete="off">
<div class="input-group">
<input type="search" class="form-control" name="q" value="<?php if (isset($q)) { echo stripslashes(nullable_htmlentities($q)); } ?>" placeholder="Search templates">
<div class="input-group-append">
<button class="btn btn-secondary"><i class="fa fa-search"></i></button>
</div>
</div>
</form>
<hr>
<div class="table-responsive-sm">
<table class="table table-striped table-borderless table-hover">
<thead class="text-dark <?php if ($num_rows[0] == 0) { echo "d-none"; } ?>">
<tr>
<th>Template Name</th>
<th>Type</th>
<th>Update Frequency</th>
<th>SLA (L/M/H Response)</th>
<th>SLA (L/M/H Resolution)</th>
<th>Hourly Rate</th>
<th>After Hours Rate</th>
<th>Support Hours</th>
<th>Net Terms</th>
<th>Created</th>
<th>Updated</th>
<th class="text-center">Action</th>
</tr>
</thead>
<tbody>
<?php
while ($row = mysqli_fetch_array($sql)) {
$id = intval($row['contract_template_id']);
$name = nullable_htmlentities($row['contract_template_name']);
$type = nullable_htmlentities($row['contract_template_type']);
$freq = nullable_htmlentities($row['contract_template_update_frequency']);
$sla_low_resp = nullable_htmlentities($row['sla_low_response_time']);
$sla_med_resp = nullable_htmlentities($row['sla_medium_response_time']);
$sla_high_resp = nullable_htmlentities($row['sla_high_response_time']);
$sla_low_res = nullable_htmlentities($row['sla_low_resolution_time']);
$sla_med_res = nullable_htmlentities($row['sla_medium_resolution_time']);
$sla_high_res = nullable_htmlentities($row['sla_high_resolution_time']);
$hourly_rate = nullable_htmlentities($row['contract_template_hourly_rate']);
$after_hours = nullable_htmlentities($row['contract_template_after_hours_hourly_rate']);
$support_hours = nullable_htmlentities($row['contract_template_support_hours']);
$net_terms = nullable_htmlentities($row['contract_template_net_terms']);
$created = nullable_htmlentities($row['contract_template_created_at']);
$updated = nullable_htmlentities($row['contract_template_updated_at']);
?>
<tr>
<td>
<a class="text-bold" href="contract_template_details.php?contract_template_id=<?php echo $id; ?>">
<i class="fas fa-fw fa-file-alt text-dark"></i> <?php echo $name; ?>
</a>
<div class="mt-1 text-secondary"><?php echo nullable_htmlentities($row['contract_template_description']); ?></div>
</td>
<td><?php echo $type; ?></td>
<td><?php echo $freq; ?></td>
<td><?php echo "$sla_low_resp / $sla_med_resp / $sla_high_resp"; ?></td>
<td><?php echo "$sla_low_res / $sla_med_res / $sla_high_res"; ?></td>
<td><?php echo $hourly_rate; ?></td>
<td><?php echo $after_hours; ?></td>
<td><?php echo $support_hours; ?></td>
<td><?php echo $net_terms; ?></td>
<td><?php echo $created; ?></td>
<td><?php echo $updated; ?></td>
<td>
<div class="dropdown dropleft text-center">
<button class="btn btn-secondary btn-sm" type="button" data-toggle="dropdown">
<i class="fas fa-ellipsis-h"></i>
</button>
<div class="dropdown-menu">
<a class="dropdown-item ajax-modal" href="#"
data-modal-size="xl"
data-modal-url="modals/contract_template/contract_template_edit.php?id=<?= $id ?>">
<i class="fas fa-fw fa-edit mr-2"></i>Edit
</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item text-danger text-bold" href="post.php?delete_contract_template=<?php echo $id; ?>">
<i class="fas fa-fw fa-trash mr-2"></i>Delete
</a>
</div>
</div>
</td>
</tr>
<?php } ?>
</tbody>
</table>
<br>
</div>
<?php require_once "../includes/filter_footer.php"; ?>
</div>
</div>
<?php require_once "../includes/footer.php"; ?>

View File

@@ -0,0 +1,123 @@
<?php
// Default Column Sort by Filter
$sort = "contract_template_name";
$order = "ASC";
require_once "includes/inc_all_admin.php";
// Search query
$sql = mysqli_query(
$mysqli,
"SELECT SQL_CALC_FOUND_ROWS * FROM contract_templates
WHERE contract_template_name LIKE '%$q%' OR contract_template_type LIKE '%$q%' OR contract_template_name LIKE '%$q%'
ORDER BY $sort $order LIMIT $record_from, $record_to"
);
$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-file-contract me-2"></i>Contract Templates</h3>
<div class="card-tools">
<button type="button" class="btn btn-primary ajax-modal" data-modal-url="modals/contract_template/contract_template_add.php" data-modal-size="lg">
<i class="fas fa-plus me-2"></i>New Template
</button>
</div>
</div>
<div class="card-body">
<form autocomplete="off">
<div class="input-group">
<input type="search" class="form-control" name="q" value="<?php if (isset($q)) { echo stripslashes(escapeHtml($q)); } ?>" placeholder="Search templates">
<button class="btn btn-secondary"><i class="fa fa-search"></i></button>
</div>
</form>
<hr>
<div class="table-responsive-sm">
<table class="table table-striped table-borderless table-hover">
<thead class="text-dark <?php if ($num_rows[0] == 0) { echo "d-none"; } ?>">
<tr>
<th>Template Name</th>
<th>Type</th>
<th>Update Frequency</th>
<th>SLA (L/M/H Response)</th>
<th>SLA (L/M/H Resolution)</th>
<th>Hourly Rate</th>
<th>After Hours Rate</th>
<th>Support Hours</th>
<th>Net Terms</th>
<th>Created</th>
<th>Updated</th>
<th class="text-center">Action</th>
</tr>
</thead>
<tbody>
<?php
while ($row = mysqli_fetch_assoc($sql)) {
$id = intval($row['contract_template_id']);
$name = escapeHtml($row['contract_template_name']);
$type = escapeHtml($row['contract_template_type']);
$freq = escapeHtml($row['contract_template_renewal_frequency']);
$sla_low_resp = escapeHtml($row['contract_template_sla_low_response_time']);
$sla_med_resp = escapeHtml($row['contract_template_sla_medium_response_time']);
$sla_high_resp = escapeHtml($row['contract_template_sla_high_response_time']);
$sla_low_res = escapeHtml($row['contract_template_sla_low_resolution_time']);
$sla_med_res = escapeHtml($row['contract_template_sla_medium_resolution_time']);
$sla_high_res = escapeHtml($row['contract_template_sla_high_resolution_time']);
$hourly_rate = escapeHtml($row['contract_template_rate_standard']);
$after_hours = escapeHtml($row['contract_template_rate_after_hours']);
$support_hours = escapeHtml($row['contract_template_support_hours']);
$net_terms = escapeHtml($row['contract_template_net_terms']);
$created = escapeHtml($row['contract_template_created_at']);
$updated = escapeHtml($row['contract_template_updated_at']);
?>
<tr>
<td>
<a class="text-bold" href="contract_template_details.php?contract_template_id=<?= $id ?>">
<i class="fas fa-fw fa-file-alt text-dark"></i> <?= $name ?>
</a>
<div class="mt-1 text-secondary"><?= escapeHtml($row['contract_template_description']) ?></div>
</td>
<td><?= $type ?></td>
<td><?= $freq ?></td>
<td><?= "$sla_low_resp / $sla_med_resp / $sla_high_resp" ?></td>
<td><?= "$sla_low_res / $sla_med_res / $sla_high_res" ?></td>
<td><?= $hourly_rate ?></td>
<td><?= $after_hours ?></td>
<td><?= $support_hours ?></td>
<td><?= $net_terms ?></td>
<td><?= $created ?></td>
<td><?= $updated ?></td>
<td>
<div class="dropdown dropstart text-center">
<button class="btn btn-secondary btn-sm" type="button" data-bs-toggle="dropdown">
<i class="fas fa-ellipsis-h"></i>
</button>
<div class="dropdown-menu">
<a class="dropdown-item ajax-modal" href="#"
data-modal-size="xl"
data-modal-url="modals/contract_template/contract_template_edit.php?id=<?= $id ?>">
<i class="fas fa-fw fa-edit me-2"></i>Edit
</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item text-danger text-bold" href="post.php?delete_contract_template=<?= $id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-trash me-2"></i>Delete
</a>
</div>
</div>
</td>
</tr>
<?php } ?>
</tbody>
</table>
<br>
</div>
<?php require_once "../includes/filter_footer.php"; ?>
</div>
</div>
<?php require_once "../includes/footer.php"; ?>

208
admin/cron.php Normal file
View File

@@ -0,0 +1,208 @@
<?php
require_once "includes/inc_all_admin.php";
require_once $_SERVER['DOCUMENT_ROOT'] . '/includes/cron_jobs.php';
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT config_enable_cron, config_cron_last_dispatch_at FROM settings WHERE company_id = 1"));
$config_enable_cron = intval($row['config_enable_cron']);
$cron_last_dispatch_at = $row['config_cron_last_dispatch_at'];
// The dispatcher writes its heartbeat before it runs anything, so anything older than a few
// minutes means the crontab entry itself is missing or failing - a different problem from a
// job that is disabled or erroring, and the one people spend the longest not finding.
$cron_is_running = $cron_last_dispatch_at !== null && (time() - strtotime($cron_last_dispatch_at)) < 300;
$cron_command = "* * * * * php " . dirname(__DIR__) . "/cron/cron.php >/dev/null";
// Registry order is dispatch order, so the table reads the way the cycle runs
$cron_jobs = [];
foreach (cronJobRegistry() as $job) {
$cron_jobs[$job['name']] = $job;
$cron_jobs[$job['name']]['row'] = null;
}
$sql = mysqli_query($mysqli, "SELECT cron_job_daily_at, cron_job_enabled, cron_job_id, cron_job_interval_minutes,
cron_job_last_duration, cron_job_last_error, cron_job_last_error_at, cron_job_last_run_at, cron_job_last_status,
cron_job_name, cron_job_run_now, cron_job_schedule FROM cron_jobs");
while ($job_row = mysqli_fetch_assoc($sql)) {
if (isset($cron_jobs[$job_row['cron_job_name']])) {
$cron_jobs[$job_row['cron_job_name']]['row'] = $job_row;
}
}
?>
<div class="card card-dark">
<div class="card-header py-3">
<h3 class="card-title"><i class="fas fa-fw fa-clock me-2"></i>Cron</h3>
</div>
<div class="card-body">
<?php if (!$cron_is_running) { ?>
<div class="alert alert-danger">
<h5><i class="fas fa-fw fa-exclamation-triangle me-2"></i>Cron is not running</h5>
ITFlow last heard from cron <strong><?= escapeHtml(strtolower(cronJobTimeAgo($cron_last_dispatch_at))) ?></strong>.
Nothing below will run - no mail is being sent, no email is being turned into tickets, and invoices are not being generated.
Add this line to the crontab of the user that owns the ITFlow files:
<pre class="bg-dark text-white p-2 mt-2 mb-0"><?= escapeHtml($cron_command) ?></pre>
</div>
<?php } else { ?>
<div class="alert alert-success">
<i class="fas fa-fw fa-check me-2"></i>Cron last checked in <strong><?= escapeHtml(strtolower(cronJobTimeAgo($cron_last_dispatch_at))) ?></strong>.
<span class="text-muted ms-2"><?= escapeHtml($cron_command) ?></span>
</div>
<?php } ?>
<?php if ($config_enable_cron == 0) { ?>
<div class="alert alert-warning">
<div class="float-end">
<a class="btn btn-sm btn-warning" href="post.php?enable_cron=1&amp;csrf_token=<?= $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-power-off me-2"></i>Turn cron on
</a>
</div>
<h5><i class="fas fa-fw fa-exclamation-circle me-2"></i>Cron is switched off</h5>
The dispatcher is running, but every job below stops itself immediately while this is off -
no mail is sent, no email becomes a ticket, and nothing is invoiced.
</div>
<?php } else { ?>
<p class="text-muted">
<small>
<i class="fas fa-fw fa-power-off me-1"></i>The master switch is <strong>on</strong>. Turning it off
stops every job at once without touching their schedules, which is what you want on a restored
backup or a staging clone - those come up with every job enabled and will otherwise email clients
and charge cards. Switching back on returns you to exactly this configuration.
<a href="post.php?disable_cron=1&amp;csrf_token=<?= $_SESSION['csrf_token'] ?>">Turn cron off</a>.
</small>
</p>
<?php } ?>
<div class="table-responsive-sm">
<table class="table table-borderless table-hover">
<thead class="text-secondary">
<tr>
<th>Job</th>
<th>Schedule</th>
<th>Last Run</th>
<th>Duration</th>
<th>Status</th>
<th>Next Run</th>
<th class="text-center">Action</th>
</tr>
</thead>
<tbody>
<?php foreach ($cron_jobs as $job) {
$job_row = $job['row'];
// A job the dispatcher has not met yet has no row. It gets one on the next
// pass, seeded from the schedule in the registry, so show that meanwhile.
$cron_job_id = $job_row ? intval($job_row['cron_job_id']) : 0;
$enabled = $job_row ? intval($job_row['cron_job_enabled']) : 1;
$schedule = $job_row ? $job_row['cron_job_schedule'] : $job['schedule'];
$interval_minutes = $job_row ? intval($job_row['cron_job_interval_minutes']) : intval($job['interval_minutes'] ?? 1);
$daily_at = $job_row ? $job_row['cron_job_daily_at'] : ($job['daily_at'] ?? null);
$run_now = $job_row ? intval($job_row['cron_job_run_now']) : 0;
$last_run_at = $job_row ? $job_row['cron_job_last_run_at'] : null;
$last_duration = $job_row ? $job_row['cron_job_last_duration'] : null;
$last_status = $job_row ? $job_row['cron_job_last_status'] : null;
$last_error = $job_row ? $job_row['cron_job_last_error'] : null;
$last_error_at = $job_row ? $job_row['cron_job_last_error_at'] : null;
$next_run = $job_row ? cronJobNextRun($job_row) : null;
if ($run_now) {
$status_badge = '<span class="badge bg-warning text-dark">Queued</span>';
} elseif ($last_status === 'Running') {
$status_badge = '<span class="badge bg-info text-dark">Running</span>';
} elseif ($last_status === 'Completed') {
$status_badge = '<span class="badge bg-success">Completed</span>';
} elseif ($last_status === 'Failed') {
$status_badge = '<span class="badge bg-danger">Failed</span>';
} elseif ($last_status !== null) {
$status_badge = '<span class="badge bg-secondary">Stopped</span>';
} else {
$status_badge = '<span class="badge bg-light text-dark">Never run</span>';
}
?>
<tr class="<?= $enabled ? '' : 'text-muted' ?>">
<td>
<strong><?= escapeHtml($job['label']) ?></strong>
<?php if (!$enabled) { ?><span class="badge bg-secondary ms-1">Disabled</span><?php } ?>
<br><small class="text-secondary"><?= escapeHtml($job['description']) ?></small>
<br><small class="text-muted"><code>cron/<?= escapeHtml($job['script']) ?></code></small>
</td>
<td><?= escapeHtml(cronJobScheduleDescription($schedule, $interval_minutes, $daily_at)) ?></td>
<td>
<?= escapeHtml(cronJobTimeAgo($last_run_at)) ?>
<?php if ($last_run_at) { ?><br><small class="text-muted"><?= escapeHtml(date('M j, g:i A', strtotime($last_run_at))) ?></small><?php } ?>
</td>
<td><?= $last_duration === null ? '-' : escapeHtml($last_duration) . 's' ?></td>
<td>
<?= $status_badge ?>
<?php if ($last_status !== null && $last_status !== 'Running' && strlen($last_status) > 9) { ?>
<br><small class="text-muted"><?= escapeHtml($last_status) ?></small>
<?php } ?>
</td>
<td><?= $next_run === null ? '-' : escapeHtml(cronJobTimeAgo($next_run)) ?></td>
<td class="text-center">
<div class="dropdown dropstart text-center">
<button class="btn btn-secondary btn-sm" type="button" data-bs-toggle="dropdown">
<i class="fas fa-ellipsis-v"></i>
</button>
<div class="dropdown-menu">
<a class="dropdown-item <?= $cron_job_id === 0 ? 'disabled' : '' ?>" href="post.php?run_cron_job=<?= $cron_job_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-play me-2"></i>Run Now
</a>
<button class="dropdown-item ajax-modal <?= $cron_job_id === 0 ? 'disabled' : '' ?>" type="button" data-toggle="ajax-modal"
data-modal-url="modals/cron/cron_edit.php?id=<?= $cron_job_id ?>">
<i class="fas fa-fw fa-edit me-2"></i>Edit Schedule
</button>
<?php if ($enabled) { ?>
<a class="dropdown-item text-danger" href="post.php?disable_cron_job=<?= $cron_job_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-pause me-2"></i>Disable
</a>
<?php } else { ?>
<a class="dropdown-item text-success" href="post.php?enable_cron_job=<?= $cron_job_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-play-circle me-2"></i>Enable
</a>
<?php } ?>
</div>
</div>
</td>
</tr>
<?php if (!empty($last_error)) { ?>
<tr>
<td colspan="7" class="pt-0">
<div class="alert alert-danger mb-0 py-2">
<div class="float-end">
<a class="text-danger" href="post.php?clear_cron_error=<?= $cron_job_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>" title="Dismiss">
<i class="fas fa-fw fa-times"></i>
</a>
</div>
<strong><i class="fas fa-fw fa-exclamation-triangle me-2"></i>Last error</strong>
<small class="text-muted ms-2"><?= escapeHtml(cronJobTimeAgo($last_error_at)) ?></small>
<div class="mt-1"><small><?= escapeHtml($last_error) ?></small></div>
</div>
</td>
</tr>
<?php } ?>
<?php } ?>
</tbody>
</table>
</div>
<p class="text-muted mb-0">
<small>
<i class="fas fa-fw fa-info-circle me-1"></i>Run Now does not start the job in your browser - it asks the
dispatcher to pick it up on its next pass, so a job starts within a minute and still runs on the command
line with the same locking as a scheduled run. Detailed per-job output is in
<a href="app_logs.php">App Logs</a>.
</small>
</p>
</div>
</div>
<?php require_once "../includes/footer.php"; ?>

View File

@@ -1,148 +0,0 @@
<?php
// Default Column Sortby Filter
$sort = "custom_link_name";
$order = "ASC";
require_once "includes/inc_all_admin.php";
$sql = mysqli_query(
$mysqli,
"SELECT SQL_CALC_FOUND_ROWS * FROM custom_links
WHERE custom_link_name LIKE '%$q%'
ORDER BY $sort $order LIMIT $record_from, $record_to"
);
$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="fas fa-fw fa-external-link-alt mr-2"></i>Custom Links</h3>
<div class="card-tools">
<button type="button" class="btn btn-primary ajax-modal" data-modal-url="modals/custom_link/custom_link_add.php"><i class="fas fa-plus mr-2"></i>New Link</button>
</div>
</div>
<div class="card-body">
<div class="row">
<div class="col-sm-4 mb-2">
<form autocomplete="off">
<div class="input-group">
<input type="search" class="form-control" name="q" value="<?php if (isset($q)) { echo stripslashes(nullable_htmlentities($q)); } ?>" placeholder="Search Links">
<div class="input-group-append">
<button class="btn btn-primary"><i class="fa fa-search"></i></button>
</div>
</div>
</form>
</div>
<div class="col-sm-8">
</div>
</div>
<hr>
<div class="table-responsive-sm">
<table class="table table-striped table-borderless table-hover">
<thead class="text-dark <?php if ($num_rows[0] == 0) { echo "d-none"; } ?>">
<tr>
<th>
<a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=custom_link_name&order=<?php echo $disp; ?>">
Name <?php if ($sort == 'custom_link_name') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=custom_link_order&order=<?php echo $disp; ?>">
Order <?php if ($sort == 'custom_link_order') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=custom_link_uri&order=<?php echo $disp; ?>">
URI / <span class="text-secondary">New Tab</span> <?php if ($sort == 'custom_link_uri') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=custom_link_location&order=<?php echo $disp; ?>">
Location <?php if ($sort == 'custom_link_location') { echo $order_icon; } ?>
</a>
</th>
<th class="text-center">Action</th>
</tr>
</thead>
<tbody>
<?php
while ($row = mysqli_fetch_array($sql)) {
$custom_link_id = intval($row['custom_link_id']);
$custom_link_name = nullable_htmlentities($row['custom_link_name']);
$custom_link_uri = nullable_htmlentities($row['custom_link_uri']);
$custom_link_icon = nullable_htmlentities($row['custom_link_icon']);
$custom_link_new_tab = intval($row['custom_link_new_tab']);
if ($custom_link_new_tab == 1 ) {
$custom_link_new_tab_display = "<i class='fas fa-fw fa-checkmark'></i>";
} else {
$custom_link_new_tab_display = "";
}
$custom_link_order = intval($row['custom_link_order']);
if ($custom_link_order == 0 ) {
$custom_link_order_display = "-";
} else {
$custom_link_order_display = $custom_link_order;
}
$custom_link_location = intval($row['custom_link_location']);
if ($custom_link_location == 1) {
$custom_link_location_display = "Main Side Nav";
} elseif ($custom_link_location == 2) {
$custom_link_location_display = "Top Nav";
} elseif ($custom_link_location == 3) {
$custom_link_location_display = "Client Portal Nav";
} elseif ($custom_link_location == 4) {
$custom_link_location_display = "Admin Nav";
} elseif ($custom_link_location == 5) {
$custom_link_location_display = "Reports Nav";
}
?>
<tr>
<td>
<a class="ajax-modal" href="#"
data-modal-url="modals/custom_link/custom_link_edit.php?id=<?= $custom_link_id ?>">
<i class="fa fa-fw fa-<?php echo $custom_link_icon; ?> mr-2"></i><?php echo $custom_link_name;?>
</a>
</td>
<td><?php echo $custom_link_order_display; ?></td>
<td><?php echo "$custom_link_uri $custom_link_new_tab_display"; ?></td>
<td><?php echo $custom_link_location_display; ?></td>
<td>
<div class="dropdown dropleft text-center">
<button class="btn btn-secondary btn-sm" type="button" data-toggle="dropdown">
<i class="fas fa-ellipsis-h"></i>
</button>
<div class="dropdown-menu">
<a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/custom_link/custom_link_edit.php?id=<?= $custom_link_id ?>">
<i class="fas fa-fw fa-edit mr-2"></i>Edit
</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item text-danger text-bold confirm-link" href="post.php?delete_custom_link=<?php echo $custom_link_id; ?>">
<i class="fas fa-fw fa-trash mr-2"></i>Delete
</a>
</div>
</div>
</td>
</tr>
<?php
}
?>
</tbody>
</table>
</div>
<?php require_once "../includes/filter_footer.php";
?>
</div>
</div>
<?php
require_once "../includes/footer.php";

147
admin/custom_links.php Normal file
View File

@@ -0,0 +1,147 @@
<?php
// Default Column Sortby Filter
$sort = "custom_link_name";
$order = "ASC";
require_once "includes/inc_all_admin.php";
$sql = mysqli_query(
$mysqli,
"SELECT SQL_CALC_FOUND_ROWS custom_link_icon, custom_link_id, custom_link_location, custom_link_name,
custom_link_new_tab, custom_link_order, custom_link_uri FROM custom_links
WHERE custom_link_name LIKE '%$q%'
ORDER BY $sort $order LIMIT $record_from, $record_to"
);
$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="fas fa-fw fa-external-link-alt me-2"></i>Custom Links</h3>
<div class="card-tools">
<button type="button" class="btn btn-primary ajax-modal" data-modal-url="modals/custom_link/custom_link_add.php"><i class="fas fa-plus me-2"></i>New Link</button>
</div>
</div>
<div class="card-body">
<div class="row">
<div class="col-sm-4 mb-2">
<form autocomplete="off">
<div class="input-group">
<input type="search" class="form-control" name="q" value="<?php if (isset($q)) { echo stripslashes(escapeHtml($q)); } ?>" placeholder="Search Links">
<button class="btn btn-primary"><i class="fa fa-search"></i></button>
</div>
</form>
</div>
<div class="col-sm-8">
</div>
</div>
<hr>
<div class="table-responsive-sm">
<table class="table table-striped table-borderless table-hover">
<thead class="text-dark <?php if ($num_rows[0] == 0) { echo "d-none"; } ?>">
<tr>
<th>
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=custom_link_name&order=<?= $disp ?>">
Name <?php if ($sort == 'custom_link_name') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=custom_link_order&order=<?= $disp ?>">
Order <?php if ($sort == 'custom_link_order') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=custom_link_uri&order=<?= $disp ?>">
URI / <span class="text-secondary">New Tab</span> <?php if ($sort == 'custom_link_uri') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=custom_link_location&order=<?= $disp ?>">
Location <?php if ($sort == 'custom_link_location') { echo $order_icon; } ?>
</a>
</th>
<th class="text-center">Action</th>
</tr>
</thead>
<tbody>
<?php
while ($row = mysqli_fetch_assoc($sql)) {
$custom_link_id = intval($row['custom_link_id']);
$custom_link_name = escapeHtml($row['custom_link_name']);
$custom_link_uri = escapeHtml($row['custom_link_uri']);
$custom_link_icon = escapeHtml($row['custom_link_icon']);
$custom_link_new_tab = intval($row['custom_link_new_tab']);
if ($custom_link_new_tab == 1 ) {
$custom_link_new_tab_display = "<i class='fas fa-fw fa-checkmark'></i>";
} else {
$custom_link_new_tab_display = "";
}
$custom_link_order = intval($row['custom_link_order']);
if ($custom_link_order == 0 ) {
$custom_link_order_display = "-";
} else {
$custom_link_order_display = $custom_link_order;
}
$custom_link_location = intval($row['custom_link_location']);
if ($custom_link_location == 1) {
$custom_link_location_display = "Main Side Nav";
} elseif ($custom_link_location == 2) {
$custom_link_location_display = "Top Nav";
} elseif ($custom_link_location == 3) {
$custom_link_location_display = "Client Portal Nav";
} elseif ($custom_link_location == 4) {
$custom_link_location_display = "Admin Nav";
} elseif ($custom_link_location == 5) {
$custom_link_location_display = "Reports Nav";
}
?>
<tr>
<td>
<a class="ajax-modal" href="#"
data-modal-url="modals/custom_link/custom_link_edit.php?id=<?= $custom_link_id ?>">
<i class="fa fa-fw fa-<?= $custom_link_icon ?> me-2"></i><?= $custom_link_name ?>
</a>
</td>
<td><?= $custom_link_order_display ?></td>
<td><?= "$custom_link_uri $custom_link_new_tab_display" ?></td>
<td><?= $custom_link_location_display ?></td>
<td>
<div class="dropdown dropstart text-center">
<button class="btn btn-secondary btn-sm" type="button" data-bs-toggle="dropdown">
<i class="fas fa-ellipsis-h"></i>
</button>
<div class="dropdown-menu">
<a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/custom_link/custom_link_edit.php?id=<?= $custom_link_id ?>">
<i class="fas fa-fw fa-edit me-2"></i>Edit
</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item text-danger text-bold confirm-link" href="post.php?delete_custom_link=<?= $custom_link_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-trash me-2"></i>Delete
</a>
</div>
</div>
</td>
</tr>
<?php
}
?>
</tbody>
</table>
</div>
<?php require_once "../includes/filter_footer.php";
?>
</div>
</div>
<?php
require_once "../includes/footer.php";

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,184 @@
<?php
/*
* ITFlow - Database update to version 2.0.0 (from 1.9.9)
* Included by admin/database_updates.php - do not access directly
*/
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
mysqli_query($mysqli, "RENAME TABLE `logins` TO `credentials`");
mysqli_query($mysqli, "
ALTER TABLE `credentials`
CHANGE COLUMN `login_id` `credential_id` INT(11) NOT NULL AUTO_INCREMENT,
CHANGE COLUMN `login_name` `credential_name` VARCHAR(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
CHANGE COLUMN `login_description` `credential_description` VARCHAR(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
CHANGE COLUMN `login_category` `credential_category` VARCHAR(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
CHANGE COLUMN `login_uri` `credential_uri` VARCHAR(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
CHANGE COLUMN `login_uri_2` `credential_uri_2` VARCHAR(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
CHANGE COLUMN `login_username` `credential_username` VARCHAR(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
CHANGE COLUMN `login_password` `credential_password` VARBINARY(200) NULL DEFAULT NULL,
CHANGE COLUMN `login_otp_secret` `credential_otp_secret` VARCHAR(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
CHANGE COLUMN `login_note` `credential_note` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
CHANGE COLUMN `login_important` `credential_important` TINYINT(1) NOT NULL DEFAULT '0',
CHANGE COLUMN `login_created_at` `credential_created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP(),
CHANGE COLUMN `login_updated_at` `credential_updated_at` DATETIME NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP(),
CHANGE COLUMN `login_archived_at` `credential_archived_at` DATETIME NULL DEFAULT NULL,
CHANGE COLUMN `login_accessed_at` `credential_accessed_at` DATETIME NULL DEFAULT NULL,
CHANGE COLUMN `login_password_changed_at` `credential_password_changed_at` DATETIME NULL DEFAULT CURRENT_TIMESTAMP(),
CHANGE COLUMN `login_folder_id` `credential_folder_id` INT(11) NOT NULL DEFAULT '0',
CHANGE COLUMN `login_contact_id` `credential_contact_id` INT(11) NOT NULL DEFAULT '0',
CHANGE COLUMN `login_asset_id` `credential_asset_id` INT(11) NOT NULL DEFAULT '0',
CHANGE COLUMN `login_client_id` `credential_client_id` INT(11) NOT NULL DEFAULT '0'
");
// Rename table contact_logins to contact_credentials
mysqli_query($mysqli, "RENAME TABLE `contact_logins` TO `contact_credentials`");
// Alter contact_credentials table and change login_id to credential_id
mysqli_query($mysqli, "
ALTER TABLE `contact_credentials`
CHANGE COLUMN `login_id` `credential_id` INT(11) NOT NULL
");
// Clean up orphaned contact_id rows in contact_credentials
mysqli_query($mysqli, "
DELETE FROM `contact_credentials`
WHERE `contact_id` NOT IN (SELECT `contact_id` FROM `contacts`);
");
// Clean up orphaned credential_id rows in contact_credentials
mysqli_query($mysqli, "
DELETE FROM `contact_credentials`
WHERE `credential_id` NOT IN (SELECT `credential_id` FROM `credentials`);
");
// Add foreign keys to contact_credentials
mysqli_query($mysqli, "
ALTER TABLE `contact_credentials`
ADD FOREIGN KEY (`contact_id`) REFERENCES `contacts`(`contact_id`) ON DELETE CASCADE,
ADD FOREIGN KEY (`credential_id`) REFERENCES `credentials`(`credential_id`) ON DELETE CASCADE
");
// Rename table service_logins to service_credentials
mysqli_query($mysqli, "RENAME TABLE `service_logins` TO `service_credentials`");
// Alter service_credentials table and change login_id to credential_id
mysqli_query($mysqli, "
ALTER TABLE `service_credentials`
CHANGE COLUMN `login_id` `credential_id` INT(11) NOT NULL
");
// Clean up orphaned service_id rows in service_credentials
mysqli_query($mysqli, "
DELETE FROM `service_credentials`
WHERE `service_id` NOT IN (SELECT `service_id` FROM `services`);
");
// Clean up orphaned credential_id rows in service_credentials
mysqli_query($mysqli, "
DELETE FROM `service_credentials`
WHERE `credential_id` NOT IN (SELECT `credential_id` FROM `credentials`);
");
// Add foreign keys to service_credentials
mysqli_query($mysqli, "
ALTER TABLE `service_credentials`
ADD FOREIGN KEY (`service_id`) REFERENCES `services`(`service_id`) ON DELETE CASCADE,
ADD FOREIGN KEY (`credential_id`) REFERENCES `credentials`(`credential_id`) ON DELETE CASCADE
");
// Rename table software_logins to software_credentials
mysqli_query($mysqli, "RENAME TABLE `software_logins` TO `software_credentials`");
// Alter software_credentials table and change login_id to credential_id
mysqli_query($mysqli, "
ALTER TABLE `software_credentials`
CHANGE COLUMN `login_id` `credential_id` INT(11) NOT NULL
");
// Clean up orphaned software_id rows in software_credentials
mysqli_query($mysqli, "
DELETE FROM `software_credentials`
WHERE `software_id` NOT IN (SELECT `software_id` FROM `software`);
");
// Clean up orphaned credential_id rows in software_credentials
mysqli_query($mysqli, "
DELETE FROM `software_credentials`
WHERE `credential_id` NOT IN (SELECT `credential_id` FROM `credentials`);
");
// Add foreign keys to software_credentials
mysqli_query($mysqli, "
ALTER TABLE `software_credentials`
ADD FOREIGN KEY (`software_id`) REFERENCES `software`(`software_id`) ON DELETE CASCADE,
ADD FOREIGN KEY (`credential_id`) REFERENCES `credentials`(`credential_id`) ON DELETE CASCADE
");
// Rename table vendor_logins to vendor_credentials
mysqli_query($mysqli, "RENAME TABLE `vendor_logins` TO `vendor_credentials`");
// Alter vendor_credentials table and change login_id to credential_id
mysqli_query($mysqli, "
ALTER TABLE `vendor_credentials`
CHANGE COLUMN `login_id` `credential_id` INT(11) NOT NULL
");
// Clean up orphaned vendor_id rows in vendor_credentials
mysqli_query($mysqli, "
DELETE FROM `vendor_credentials`
WHERE `vendor_id` NOT IN (SELECT `vendor_id` FROM `vendors`);
");
// Clean up orphaned credential_id rows in vendor_credentials
mysqli_query($mysqli, "
DELETE FROM `vendor_credentials`
WHERE `credential_id` NOT IN (SELECT `credential_id` FROM `credentials`);
");
// Add foreign keys to vendor_credentials
mysqli_query($mysqli, "
ALTER TABLE `vendor_credentials`
ADD FOREIGN KEY (`vendor_id`) REFERENCES `vendors`(`vendor_id`) ON DELETE CASCADE,
ADD FOREIGN KEY (`credential_id`) REFERENCES `credentials`(`credential_id`) ON DELETE CASCADE
");
// Rename table login_tags to credential_tags
mysqli_query($mysqli, "RENAME TABLE `login_tags` TO `credential_tags`");
// Alter credential_tags table and change login_id to credential_id
mysqli_query($mysqli, "
ALTER TABLE `credential_tags`
CHANGE COLUMN `login_id` `credential_id` INT(11) NOT NULL
");
// Clean up orphaned tag_id rows in credential_tags
mysqli_query($mysqli, "
DELETE FROM `credential_tags`
WHERE `tag_id` NOT IN (SELECT `tag_id` FROM `tags`);
");
// Clean up orphaned credential_id rows in credential_tags
mysqli_query($mysqli, "
DELETE FROM `credential_tags`
WHERE `credential_id` NOT IN (SELECT `credential_id` FROM `credentials`);
");
// Add foreign keys to credential_tags
mysqli_query($mysqli, "
ALTER TABLE `credential_tags`
ADD FOREIGN KEY (`tag_id`) REFERENCES `tags`(`tag_id`) ON DELETE CASCADE,
ADD FOREIGN KEY (`credential_id`) REFERENCES `credentials`(`credential_id`) ON DELETE CASCADE
");
// Create asset_credentials table with foreign keys
mysqli_query($mysqli, "
CREATE TABLE `asset_credentials` (
`credential_id` INT(11) NOT NULL,
`asset_id` INT(11) NOT NULL,
PRIMARY KEY (`credential_id`, `asset_id`),
FOREIGN KEY (`credential_id`) REFERENCES `credentials`(`credential_id`) ON DELETE CASCADE,
FOREIGN KEY (`asset_id`) REFERENCES `assets`(`asset_id`) ON DELETE CASCADE
)
");

View File

@@ -0,0 +1,15 @@
<?php
/*
* ITFlow - Database update to version 2.0.1 (from 2.0.0)
* Included by admin/database_updates.php - do not access directly
*/
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
//Dropping patch panel as a patch panel can be documented as an asset with interfaces.
mysqli_query($mysqli, "DROP TABLE `patch_panel_ports`");
mysqli_query($mysqli, "DROP TABLE `patch_panels`");
mysqli_query($mysqli, "RENAME TABLE `events` TO `calendar_events`");
mysqli_query($mysqli, "RENAME TABLE `event_attendees` TO `calendar_event_attendees`");

View File

@@ -0,0 +1,153 @@
<?php
/*
* ITFlow - Database update to version 2.0.2 (from 2.0.1)
* Included by admin/database_updates.php - do not access directly
*/
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
// Clean up orphaned data before adding foreign keys
// Clean up orphaned asset_custom_asset_id rows in asset_custom
mysqli_query($mysqli, "
DELETE FROM `asset_custom`
WHERE `asset_custom_asset_id` NOT IN (SELECT `asset_id` FROM `assets`);
");
// Add foreign key to asset_custom
mysqli_query($mysqli, "
ALTER TABLE `asset_custom`
ADD FOREIGN KEY (`asset_custom_asset_id`) REFERENCES `assets`(`asset_id`) ON DELETE CASCADE
");
// Clean up orphaned asset_id rows in asset_documents
mysqli_query($mysqli, "
DELETE FROM `asset_documents`
WHERE `asset_id` NOT IN (SELECT `asset_id` FROM `assets`);
");
// Clean up orphaned document_id rows in asset_documents
mysqli_query($mysqli, "
DELETE FROM `asset_documents`
WHERE `document_id` NOT IN (SELECT `document_id` FROM `documents`);
");
// Add foreign keys to asset_documents
mysqli_query($mysqli, "
ALTER TABLE `asset_documents`
ADD FOREIGN KEY (`asset_id`) REFERENCES `assets`(`asset_id`) ON DELETE CASCADE,
ADD FOREIGN KEY (`document_id`) REFERENCES `documents`(`document_id`) ON DELETE CASCADE
");
// Clean up orphaned asset_id rows in asset_files
mysqli_query($mysqli, "
DELETE FROM `asset_files`
WHERE `asset_id` NOT IN (SELECT `asset_id` FROM `assets`);
");
// Clean up orphaned file_id rows in asset_files
mysqli_query($mysqli, "
DELETE FROM `asset_files`
WHERE `file_id` NOT IN (SELECT `file_id` FROM `files`);
");
// Add foreign keys to asset_files
mysqli_query($mysqli, "
ALTER TABLE `asset_files`
ADD FOREIGN KEY (`asset_id`) REFERENCES `assets`(`asset_id`) ON DELETE CASCADE,
ADD FOREIGN KEY (`file_id`) REFERENCES `files`(`file_id`) ON DELETE CASCADE
");
// Clean up orphaned asset_history_asset_id rows in asset_history
mysqli_query($mysqli, "
DELETE FROM `asset_history`
WHERE `asset_history_asset_id` NOT IN (SELECT `asset_id` FROM `assets`);
");
// Add foreign key to asset_history
mysqli_query($mysqli, "
ALTER TABLE `asset_history`
ADD FOREIGN KEY (`asset_history_asset_id`) REFERENCES `assets`(`asset_id`) ON DELETE CASCADE
");
// Clean up orphaned interface_asset_id rows in asset_interfaces
mysqli_query($mysqli, "
DELETE FROM `asset_interfaces`
WHERE `interface_asset_id` NOT IN (SELECT `asset_id` FROM `assets`);
");
// Add foreign key to asset_interfaces
mysqli_query($mysqli, "
ALTER TABLE `asset_interfaces`
ADD FOREIGN KEY (`interface_asset_id`) REFERENCES `assets`(`asset_id`) ON DELETE CASCADE
");
// Clean up orphaned asset_note_asset_id rows in asset_notes
mysqli_query($mysqli, "
DELETE FROM `asset_notes`
WHERE `asset_note_asset_id` NOT IN (SELECT `asset_id` FROM `assets`);
");
// Add foreign key to asset_notes
mysqli_query($mysqli, "
ALTER TABLE `asset_notes`
ADD FOREIGN KEY (`asset_note_asset_id`) REFERENCES `assets`(`asset_id`) ON DELETE CASCADE
");
// Clean up orphaned contact_id rows in contact_assets
mysqli_query($mysqli, "
DELETE FROM `contact_assets`
WHERE `contact_id` NOT IN (SELECT `contact_id` FROM `contacts`);
");
// Clean up orphaned asset_id rows in contact_assets
mysqli_query($mysqli, "
DELETE FROM `contact_assets`
WHERE `asset_id` NOT IN (SELECT `asset_id` FROM `assets`);
");
// Add foreign keys to contact_assets
mysqli_query($mysqli, "
ALTER TABLE `contact_assets`
ADD FOREIGN KEY (`contact_id`) REFERENCES `contacts`(`contact_id`) ON DELETE CASCADE,
ADD FOREIGN KEY (`asset_id`) REFERENCES `assets`(`asset_id`) ON DELETE CASCADE
");
// Clean up orphaned service_id rows in service_assets
mysqli_query($mysqli, "
DELETE FROM `service_assets`
WHERE `service_id` NOT IN (SELECT `service_id` FROM `services`);
");
// Clean up orphaned asset_id rows in service_assets
mysqli_query($mysqli, "
DELETE FROM `service_assets`
WHERE `asset_id` NOT IN (SELECT `asset_id` FROM `assets`);
");
// Add foreign keys to service_assets
mysqli_query($mysqli, "
ALTER TABLE `service_assets`
ADD FOREIGN KEY (`service_id`) REFERENCES `services`(`service_id`) ON DELETE CASCADE,
ADD FOREIGN KEY (`asset_id`) REFERENCES `assets`(`asset_id`) ON DELETE CASCADE
");
// Clean up orphaned software_id rows in software_assets
mysqli_query($mysqli, "
DELETE FROM `software_assets`
WHERE `software_id` NOT IN (SELECT `software_id` FROM `software`);
");
// Clean up orphaned asset_id rows in software_assets
mysqli_query($mysqli, "
DELETE FROM `software_assets`
WHERE `asset_id` NOT IN (SELECT `asset_id` FROM `assets`);
");
// Add foreign keys to software_assets
mysqli_query($mysqli, "
ALTER TABLE `software_assets`
ADD FOREIGN KEY (`software_id`) REFERENCES `software`(`software_id`) ON DELETE CASCADE,
ADD FOREIGN KEY (`asset_id`) REFERENCES `assets`(`asset_id`) ON DELETE CASCADE
");

View File

@@ -0,0 +1,31 @@
<?php
/*
* ITFlow - Database update to version 2.0.3 (from 2.0.2)
* Included by admin/database_updates.php - do not access directly
*/
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
// Clean up orphans
mysqli_query($mysqli, "
DELETE FROM `calendar_event_attendees`
WHERE `attendee_event_id` NOT IN (SELECT `event_id` FROM `calendar_events`);
");
mysqli_query($mysqli, "
DELETE FROM `calendar_events`
WHERE `event_calendar_id` NOT IN (SELECT `calendar_id` FROM `calendars`);
");
// Add foreign key to calendar_event_attendees
mysqli_query($mysqli, "
ALTER TABLE `calendar_event_attendees`
ADD FOREIGN KEY (`attendee_event_id`) REFERENCES `calendar_events`(`event_id`) ON DELETE CASCADE
");
// Add foreign key to calendar_events
mysqli_query($mysqli, "
ALTER TABLE `calendar_events`
ADD FOREIGN KEY (`event_calendar_id`) REFERENCES `calendars`(`calendar_id`) ON DELETE CASCADE
");

View File

@@ -0,0 +1,20 @@
<?php
/*
* ITFlow - Database update to version 2.0.4 (from 2.0.3)
* Included by admin/database_updates.php - do not access directly
*/
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
// Clean up orphaned history
mysqli_query($mysqli, "
DELETE FROM `certificate_history`
WHERE `certificate_history_certificate_id` NOT IN (SELECT `certificate_id` FROM `certificates`);
");
// Add foreign key certificate history
mysqli_query($mysqli, "
ALTER TABLE `certificate_history`
ADD FOREIGN KEY (`certificate_history_certificate_id`) REFERENCES `certificates`(`certificate_id`) ON DELETE CASCADE
");

View File

@@ -0,0 +1,364 @@
<?php
/*
* ITFlow - Database update to version 2.0.5 (from 2.0.4)
* Included by admin/database_updates.php - do not access directly
*/
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
// Clean up orphaned history
mysqli_query($mysqli, "
DELETE FROM `client_notes`
WHERE `client_note_client_id` NOT IN (SELECT `client_id` FROM `clients`);
");
// Add foreign key
mysqli_query($mysqli, "
ALTER TABLE `client_notes`
ADD FOREIGN KEY (`client_note_client_id`) REFERENCES `clients`(`client_id`) ON DELETE CASCADE
");
// Clean up orphaned history
mysqli_query($mysqli, "
DELETE FROM `client_tags`
WHERE `client_id` NOT IN (SELECT `client_id` FROM `clients`);
");
// Clean up orphaned history
mysqli_query($mysqli, "
DELETE FROM `client_tags`
WHERE `tag_id` NOT IN (SELECT `tag_id` FROM `tags`);
");
// Add foreign key
mysqli_query($mysqli, "
ALTER TABLE `client_tags`
ADD FOREIGN KEY (`client_id`) REFERENCES `clients`(`client_id`) ON DELETE CASCADE,
ADD FOREIGN KEY (`tag_id`) REFERENCES `tags`(`tag_id`) ON DELETE CASCADE
");
//Contact Assets
// Clean up orphaned history
mysqli_query($mysqli, "
DELETE FROM `contact_assets`
WHERE `contact_id` NOT IN (SELECT `contact_id` FROM `contacts`);
");
mysqli_query($mysqli, "
DELETE FROM `contact_assets`
WHERE `asset_id` NOT IN (SELECT `asset_id` FROM `assets`);
");
// Add foreign key
mysqli_query($mysqli, "
ALTER TABLE `contact_assets`
ADD FOREIGN KEY (`contact_id`) REFERENCES `contacts`(`contact_id`) ON DELETE CASCADE,
ADD FOREIGN KEY (`asset_id`) REFERENCES `assets`(`asset_id`) ON DELETE CASCADE
");
// Contact Documents
// Clean up orphaned history
mysqli_query($mysqli, "
DELETE FROM `contact_documents`
WHERE `contact_id` NOT IN (SELECT `contact_id` FROM `contacts`);
");
mysqli_query($mysqli, "
DELETE FROM `contact_documents`
WHERE `document_id` NOT IN (SELECT `document_id` FROM `documents`);
");
// Add foreign key
mysqli_query($mysqli, "
ALTER TABLE `contact_documents`
ADD FOREIGN KEY (`contact_id`) REFERENCES `contacts`(`contact_id`) ON DELETE CASCADE,
ADD FOREIGN KEY (`document_id`) REFERENCES `documents`(`document_id`) ON DELETE CASCADE
");
// contact_files
// Clean up orphaned rows
mysqli_query($mysqli, "
DELETE FROM `contact_files`
WHERE `contact_id` NOT IN (SELECT `contact_id` FROM `contacts`);
");
mysqli_query($mysqli, "
DELETE FROM `contact_files`
WHERE `file_id` NOT IN (SELECT `file_id` FROM `files`);
");
// Add foreign key
mysqli_query($mysqli, "
ALTER TABLE `contact_files`
ADD FOREIGN KEY (`contact_id`) REFERENCES `contacts`(`contact_id`) ON DELETE CASCADE,
ADD FOREIGN KEY (`file_id`) REFERENCES `files`(`file_id`) ON DELETE CASCADE
");
// contact_notes
// Clean up orphaned rows
mysqli_query($mysqli, "
DELETE FROM `contact_notes`
WHERE `contact_note_contact_id` NOT IN (SELECT `contact_id` FROM `contacts`);
");
// Add foreign key
mysqli_query($mysqli, "
ALTER TABLE `contact_notes`
ADD FOREIGN KEY (`contact_note_contact_id`) REFERENCES `contacts`(`contact_id`) ON DELETE CASCADE
");
// contact_tags
// Clean up orphaned rows
mysqli_query($mysqli, "
DELETE FROM `contact_tags`
WHERE `contact_id` NOT IN (SELECT `contact_id` FROM `contacts`);
");
mysqli_query($mysqli, "
DELETE FROM `contact_tags`
WHERE `tag_id` NOT IN (SELECT `tag_id` FROM `tags`);
");
// Add foreign key
mysqli_query($mysqli, "
ALTER TABLE `contact_tags`
ADD FOREIGN KEY (`contact_id`) REFERENCES `contacts`(`contact_id`) ON DELETE CASCADE,
ADD FOREIGN KEY (`tag_id`) REFERENCES `tags`(`tag_id`) ON DELETE CASCADE
");
// document_files
// Clean up orphaned rows
mysqli_query($mysqli, "
DELETE FROM `document_files`
WHERE `document_id` NOT IN (SELECT `document_id` FROM `documents`);
");
mysqli_query($mysqli, "
DELETE FROM `document_files`
WHERE `file_id` NOT IN (SELECT `file_id` FROM `files`);
");
// Add foreign key
mysqli_query($mysqli, "
ALTER TABLE `document_files`
ADD FOREIGN KEY (`document_id`) REFERENCES `documents`(`document_id`) ON DELETE CASCADE,
ADD FOREIGN KEY (`file_id`) REFERENCES `files`(`file_id`) ON DELETE CASCADE
");
// domain_history
// Clean up orphaned rows
mysqli_query($mysqli, "
DELETE FROM `domain_history`
WHERE `domain_history_domain_id` NOT IN (SELECT `domain_id` FROM `domains`);
");
// Add foreign key
mysqli_query($mysqli, "
ALTER TABLE `domain_history`
ADD FOREIGN KEY (`domain_history_domain_id`) REFERENCES `domains`(`domain_id`) ON DELETE CASCADE
");
// location_tags
// Clean up orphaned rows
mysqli_query($mysqli, "
DELETE FROM `location_tags`
WHERE `location_id` NOT IN (SELECT `location_id` FROM `locations`);
");
mysqli_query($mysqli, "
DELETE FROM `location_tags`
WHERE `tag_id` NOT IN (SELECT `tag_id` FROM `tags`);
");
// Add foreign key
mysqli_query($mysqli, "
ALTER TABLE `location_tags`
ADD FOREIGN KEY (`location_id`) REFERENCES `locations`(`location_id`) ON DELETE CASCADE,
ADD FOREIGN KEY (`tag_id`) REFERENCES `tags`(`tag_id`) ON DELETE CASCADE
");
// quote_files
// Clean up orphaned rows
mysqli_query($mysqli, "
DELETE FROM `quote_files`
WHERE `quote_id` NOT IN (SELECT `quote_id` FROM `quotes`);
");
mysqli_query($mysqli, "
DELETE FROM `quote_files`
WHERE `file_id` NOT IN (SELECT `file_id` FROM `files`);
");
// Add foreign key
mysqli_query($mysqli, "
ALTER TABLE `quote_files`
ADD FOREIGN KEY (`quote_id`) REFERENCES `quotes`(`quote_id`) ON DELETE CASCADE,
ADD FOREIGN KEY (`file_id`) REFERENCES `files`(`file_id`) ON DELETE CASCADE
");
// service_certificates
// Clean up orphaned rows
mysqli_query($mysqli, "
DELETE FROM `service_certificates`
WHERE `service_id` NOT IN (SELECT `service_id` FROM `services`);
");
mysqli_query($mysqli, "
DELETE FROM `service_certificates`
WHERE `certificate_id` NOT IN (SELECT `certificate_id` FROM `certificates`);
");
// Add foreign key
mysqli_query($mysqli, "
ALTER TABLE `service_certificates`
ADD FOREIGN KEY (`service_id`) REFERENCES `services`(`service_id`) ON DELETE CASCADE,
ADD FOREIGN KEY (`certificate_id`) REFERENCES `certificates`(`certificate_id`) ON DELETE CASCADE
");
// service_contacts
// Clean up orphaned rows
mysqli_query($mysqli, "
DELETE FROM `service_contacts`
WHERE `service_id` NOT IN (SELECT `service_id` FROM `services`);
");
mysqli_query($mysqli, "
DELETE FROM `service_contacts`
WHERE `contact_id` NOT IN (SELECT `contact_id` FROM `contacts`);
");
// Add foreign key
mysqli_query($mysqli, "
ALTER TABLE `service_contacts`
ADD FOREIGN KEY (`service_id`) REFERENCES `services`(`service_id`) ON DELETE CASCADE,
ADD FOREIGN KEY (`contact_id`) REFERENCES `contacts`(`contact_id`) ON DELETE CASCADE
");
// service_documents
// Clean up orphaned rows
mysqli_query($mysqli, "
DELETE FROM `service_documents`
WHERE `service_id` NOT IN (SELECT `service_id` FROM `services`);
");
mysqli_query($mysqli, "
DELETE FROM `service_documents`
WHERE `document_id` NOT IN (SELECT `document_id` FROM `documents`);
");
// Add foreign key
mysqli_query($mysqli, "
ALTER TABLE `service_documents`
ADD FOREIGN KEY (`service_id`) REFERENCES `services`(`service_id`) ON DELETE CASCADE,
ADD FOREIGN KEY (`document_id`) REFERENCES `documents`(`document_id`) ON DELETE CASCADE
");
// service_domains
// Clean up orphaned rows
mysqli_query($mysqli, "
DELETE FROM `service_domains`
WHERE `service_id` NOT IN (SELECT `service_id` FROM `services`);
");
mysqli_query($mysqli, "
DELETE FROM `service_domains`
WHERE `domain_id` NOT IN (SELECT `domain_id` FROM `domains`);
");
// Add foreign key
mysqli_query($mysqli, "
ALTER TABLE `service_domains`
ADD FOREIGN KEY (`service_id`) REFERENCES `services`(`service_id`) ON DELETE CASCADE,
ADD FOREIGN KEY (`domain_id`) REFERENCES `domains`(`domain_id`) ON DELETE CASCADE
");
// service_vendors
// Clean up orphaned rows
mysqli_query($mysqli, "
DELETE FROM `service_vendors`
WHERE `service_id` NOT IN (SELECT `service_id` FROM `services`);
");
mysqli_query($mysqli, "
DELETE FROM `service_vendors`
WHERE `vendor_id` NOT IN (SELECT `vendor_id` FROM `vendors`);
");
// Add foreign key
mysqli_query($mysqli, "
ALTER TABLE `service_vendors`
ADD FOREIGN KEY (`service_id`) REFERENCES `services`(`service_id`) ON DELETE CASCADE,
ADD FOREIGN KEY (`vendor_id`) REFERENCES `vendors`(`vendor_id`) ON DELETE CASCADE
");
// software_contacts
// Clean up orphaned rows
mysqli_query($mysqli, "
DELETE FROM `software_contacts`
WHERE `software_id` NOT IN (SELECT `software_id` FROM `software`);
");
mysqli_query($mysqli, "
DELETE FROM `software_contacts`
WHERE `contact_id` NOT IN (SELECT `contact_id` FROM `contacts`);
");
// Add foreign key
mysqli_query($mysqli, "
ALTER TABLE `software_contacts`
ADD FOREIGN KEY (`software_id`) REFERENCES `software`(`software_id`) ON DELETE CASCADE,
ADD FOREIGN KEY (`contact_id`) REFERENCES `contacts`(`contact_id`) ON DELETE CASCADE
");
// software_documents
// Clean up orphaned rows
mysqli_query($mysqli, "
DELETE FROM `software_documents`
WHERE `software_id` NOT IN (SELECT `software_id` FROM `software`);
");
mysqli_query($mysqli, "
DELETE FROM `software_documents`
WHERE `document_id` NOT IN (SELECT `document_id` FROM `documents`);
");
// Add foreign key
mysqli_query($mysqli, "
ALTER TABLE `software_documents`
ADD FOREIGN KEY (`software_id`) REFERENCES `software`(`software_id`) ON DELETE CASCADE,
ADD FOREIGN KEY (`document_id`) REFERENCES `documents`(`document_id`) ON DELETE CASCADE
");
// software_files
// Clean up orphaned rows
mysqli_query($mysqli, "
DELETE FROM `software_files`
WHERE `software_id` NOT IN (SELECT `software_id` FROM `software`);
");
mysqli_query($mysqli, "
DELETE FROM `software_files`
WHERE `file_id` NOT IN (SELECT `file_id` FROM `files`);
");
// Add foreign key
mysqli_query($mysqli, "
ALTER TABLE `software_files`
ADD FOREIGN KEY (`software_id`) REFERENCES `software`(`software_id`) ON DELETE CASCADE,
ADD FOREIGN KEY (`file_id`) REFERENCES `files`(`file_id`) ON DELETE CASCADE
");
// vendor_documents
// Clean up orphaned rows
mysqli_query($mysqli, "
DELETE FROM `vendor_documents`
WHERE `vendor_id` NOT IN (SELECT `vendor_id` FROM `vendors`);
");
mysqli_query($mysqli, "
DELETE FROM `vendor_documents`
WHERE `document_id` NOT IN (SELECT `document_id` FROM `documents`);
");
// Add foreign key
mysqli_query($mysqli, "
ALTER TABLE `vendor_documents`
ADD FOREIGN KEY (`vendor_id`) REFERENCES `vendors`(`vendor_id`) ON DELETE CASCADE,
ADD FOREIGN KEY (`document_id`) REFERENCES `documents`(`document_id`) ON DELETE CASCADE
");
// vendor_files
// Clean up orphaned rows
mysqli_query($mysqli, "
DELETE FROM `vendor_files`
WHERE `vendor_id` NOT IN (SELECT `vendor_id` FROM `vendors`);
");
mysqli_query($mysqli, "
DELETE FROM `vendor_files`
WHERE `file_id` NOT IN (SELECT `file_id` FROM `files`);
");
// Add foreign key
mysqli_query($mysqli, "
ALTER TABLE `vendor_files`
ADD FOREIGN KEY (`vendor_id`) REFERENCES `vendors`(`vendor_id`) ON DELETE CASCADE,
ADD FOREIGN KEY (`file_id`) REFERENCES `files`(`file_id`) ON DELETE CASCADE
");

View File

@@ -0,0 +1,36 @@
<?php
/*
* ITFlow - Database update to version 2.0.6 (from 2.0.5)
* Included by admin/database_updates.php - do not access directly
*/
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
// CONVERT All tables TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci
$tables = [
'accounts', 'api_keys', 'app_logs', 'asset_credentials', 'asset_custom', 'asset_documents',
'asset_files', 'asset_history', 'asset_interface_links', 'asset_interfaces', 'asset_notes', 'assets',
'auth_logs', 'budget', 'calendar_event_attendees', 'calendar_events', 'calendars', 'categories',
'certificate_history', 'certificates', 'client_notes', 'client_stripe', 'client_tags', 'clients',
'companies', 'contact_assets', 'contact_credentials', 'contact_documents', 'contact_files', 'contact_notes',
'contact_tags', 'contacts', 'credential_tags', 'credentials', 'custom_fields', 'custom_links',
'custom_values', 'document_files', 'documents', 'domain_history', 'domains', 'email_queue', 'expenses',
'files', 'folders', 'history', 'invoice_items', 'invoices', 'location_tags', 'locations', 'logs',
'modules', 'networks', 'notifications', 'payments', 'products', 'project_template_ticket_templates',
'project_templates', 'projects', 'quote_files', 'quotes', 'rack_units', 'racks', 'records',
'recurring_expenses', 'recurring_invoices', 'recurring_payments', 'recurring_ticket_assets', 'recurring_tickets',
'remember_tokens', 'revenues', 'service_assets', 'service_certificates', 'service_contacts', 'service_credentials',
'service_documents', 'service_domains', 'service_vendors', 'services', 'settings', 'shared_items',
'software', 'software_assets', 'software_contacts', 'software_credentials', 'software_documents', 'software_files',
'tags', 'task_templates', 'tasks', 'taxes', 'ticket_assets', 'ticket_attachments', 'ticket_history', 'ticket_replies',
'ticket_statuses', 'ticket_templates', 'ticket_views', 'ticket_watchers', 'tickets', 'transfers', 'trips',
'user_client_permissions', 'user_role_permissions', 'user_roles', 'user_settings', 'users', 'vendor_credentials',
'vendor_documents', 'vendor_files', 'vendors'
];
foreach ($tables as $table) {
$sql = "ALTER TABLE `$table` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;";
mysqli_query($mysqli, $sql);
}

View File

@@ -0,0 +1,11 @@
<?php
/*
* ITFlow - Database update to version 2.0.7 (from 2.0.6)
* Included by admin/database_updates.php - do not access directly
*/
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
// Fix service_domains to yse InnoDB instead of MyISAM
mysqli_query($mysqli, "ALTER TABLE service_domains ENGINE = InnoDB;");

View File

@@ -0,0 +1,10 @@
<?php
/*
* ITFlow - Database update to version 2.0.8 (from 2.0.7)
* Included by admin/database_updates.php - do not access directly
*/
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
mysqli_query($mysqli, "ALTER TABLE `files` DROP `file_hash`");

View File

@@ -0,0 +1,12 @@
<?php
/*
* ITFlow - Database update to version 2.0.9 (from 2.0.8)
* Included by admin/database_updates.php - do not access directly
*/
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
mysqli_query($mysqli, "ALTER TABLE `files` DROP `file_has_thumbnail`");
mysqli_query($mysqli, "ALTER TABLE `files` DROP `file_has_preview`");
mysqli_query($mysqli, "ALTER TABLE `files` DROP `file_asset_id`");

View File

@@ -0,0 +1,19 @@
<?php
/*
* ITFlow - Database update to version 2.1.0 (from 2.0.9)
* Included by admin/database_updates.php - do not access directly
*/
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
mysqli_query($mysqli, "ALTER TABLE `contacts` ADD `contact_phone_country_code` VARCHAR(10) DEFAULT 1 AFTER `contact_email`");
mysqli_query($mysqli, "ALTER TABLE `contacts` ADD `contact_mobile_country_code` VARCHAR(10) DEFAULT 1 AFTER `contact_extension`");
mysqli_query($mysqli, "ALTER TABLE `locations` ADD `location_phone_country_code` VARCHAR(10) DEFAULT 1 AFTER `location_zip`");
mysqli_query($mysqli, "ALTER TABLE `locations` ADD `location_phone_extension` VARCHAR(10) DEFAULT NULL AFTER `location_phone`");
mysqli_query($mysqli, "ALTER TABLE `locations` ADD `location_fax_country_code` VARCHAR(10) DEFAULT 1 AFTER `location_phone_extension`");
mysqli_query($mysqli, "ALTER TABLE `vendors` ADD `vendor_phone_country_code` VARCHAR(10) DEFAULT 1 AFTER `vendor_contact_name`");
mysqli_query($mysqli, "ALTER TABLE `companies` ADD `company_phone_country_code` VARCHAR(10) DEFAULT 1 AFTER `company_country`");

View File

@@ -0,0 +1,10 @@
<?php
/*
* ITFlow - Database update to version 2.1.1 (from 2.1.0)
* Included by admin/database_updates.php - do not access directly
*/
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
mysqli_query($mysqli, "ALTER TABLE `user_settings` ADD `user_config_signature` TEXT DEFAULT NULL AFTER `user_config_calendar_first_day`");

View File

@@ -0,0 +1,10 @@
<?php
/*
* ITFlow - Database update to version 2.1.2 (from 2.1.1)
* Included by admin/database_updates.php - do not access directly
*/
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
mysqli_query($mysqli, "ALTER TABLE `settings` DROP `config_phone_mask`");

View File

@@ -0,0 +1,36 @@
<?php
/*
* ITFlow - Database update to version 2.1.3 (from 2.1.2)
* Included by admin/database_updates.php - do not access directly
*/
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
// Update country_code to NULL for `contacts` table
mysqli_query($mysqli, "ALTER TABLE `contacts` MODIFY `contact_phone_country_code` VARCHAR(10) DEFAULT NULL");
mysqli_query($mysqli, "ALTER TABLE `contacts` MODIFY `contact_mobile_country_code` VARCHAR(10) DEFAULT NULL");
// Update country_code to NULL for `locations` table
mysqli_query($mysqli, "ALTER TABLE `locations` MODIFY `location_phone_country_code` VARCHAR(10) DEFAULT NULL");
mysqli_query($mysqli, "ALTER TABLE `locations` MODIFY `location_fax_country_code` VARCHAR(10) DEFAULT NULL");
// Update country_code to NULL for `vendors` table
mysqli_query($mysqli, "ALTER TABLE `vendors` MODIFY `vendor_phone_country_code` VARCHAR(10) DEFAULT NULL");
// Update country_code to NULL for `companies` table
mysqli_query($mysqli, "ALTER TABLE `companies` MODIFY `company_phone_country_code` VARCHAR(10) DEFAULT NULL");
// Set country_code to NULL for `contacts` table
mysqli_query($mysqli, "UPDATE `contacts` SET `contact_phone_country_code` = NULL");
mysqli_query($mysqli, "UPDATE `contacts` SET `contact_mobile_country_code` = NULL");
// Set country_code to NULL for `locations` table
mysqli_query($mysqli, "UPDATE `locations` SET `location_phone_country_code` = NULL");
mysqli_query($mysqli, "UPDATE `locations` SET `location_fax_country_code` = NULL");
// Set country_code to NULL for `vendors` table
mysqli_query($mysqli, "UPDATE `vendors` SET `vendor_phone_country_code` = NULL");
// Set country_code to NULL for `companies` table
mysqli_query($mysqli, "UPDATE `companies` SET `company_phone_country_code` = NULL");

View File

@@ -0,0 +1,11 @@
<?php
/*
* ITFlow - Database update to version 2.1.4 (from 2.1.3)
* Included by admin/database_updates.php - do not access directly
*/
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
mysqli_query($mysqli, "ALTER TABLE `client_stripe` ADD `stripe_pm_details` VARCHAR(200) DEFAULT NULL AFTER `stripe_pm`");
mysqli_query($mysqli, "ALTER TABLE `client_stripe` ADD `stripe_pm_created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP AFTER `stripe_pm_details`");

View File

@@ -0,0 +1,13 @@
<?php
/*
* ITFlow - Database update to version 2.1.5 (from 2.1.4)
* Included by admin/database_updates.php - do not access directly
*/
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
mysqli_query($mysqli, "ALTER TABLE `settings` ADD `config_ticket_timer_autostart` TINYINT(1) NOT NULL DEFAULT '0' AFTER `config_ticket_default_billable`");
mysqli_query($mysqli, "ALTER TABLE `tickets` ADD `ticket_due_at` DATETIME DEFAULT NULL AFTER `ticket_updated_at`");
mysqli_query($mysqli, "ALTER TABLE `companies` ADD `company_tax_id` VARCHAR(200) DEFAULT NULL AFTER `company_currency`");
mysqli_query($mysqli, "ALTER TABLE `settings` ADD `config_invoice_show_tax_id` TINYINT(1) NOT NULL DEFAULT '0' AFTER `config_invoice_paid_notification_email`");

View File

@@ -0,0 +1,27 @@
<?php
/*
* ITFlow - Database update to version 2.1.6 (from 2.1.5)
* Included by admin/database_updates.php - do not access directly
*/
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
mysqli_query($mysqli, "CREATE TABLE `document_versions` (
`document_version_id` INT(11) NOT NULL AUTO_INCREMENT,
`document_version_name` VARCHAR(200) NOT NULL,
`document_version_description` TEXT DEFAULT NULL,
`document_version_content` LONGTEXT NOT NULL,
`document_version_created_by` INT(11) DEFAULT 0,
`document_version_created_at` DATETIME NOT NULL,
`document_version_document_id` INT(11) NOT NULL,
PRIMARY KEY (`document_version_id`)
)");
// Delete all Current Document Versions
mysqli_query($mysqli, "
DELETE FROM `documents`
WHERE `document_parent` > 0 AND `document_parent` != `document_id`
");
mysqli_query($mysqli, "ALTER TABLE `documents` DROP `document_parent`");

View File

@@ -0,0 +1,52 @@
<?php
/*
* ITFlow - Database update to version 2.1.7 (from 2.1.6)
* Included by admin/database_updates.php - do not access directly
*/
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
mysqli_query($mysqli, "CREATE TABLE `document_templates` (
`document_template_id` INT(11) NOT NULL AUTO_INCREMENT,
`document_template_name` VARCHAR(200) NOT NULL,
`document_template_description` TEXT DEFAULT NULL,
`document_template_content` LONGTEXT NOT NULL,
`document_template_created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`document_template_updated_at` DATETIME NULL ON UPDATE CURRENT_TIMESTAMP,
`document_template_archived_at` DATETIME NULL DEFAULT NULL,
`document_template_created_by` INT(11) NOT NULL DEFAULT 0,
`document_template_updated_by` INT(11) NOT NULL DEFAULT 0,
PRIMARY KEY (`document_template_id`)
)");
// Copy Document Templates over to new document templates table
mysqli_query($mysqli, "
INSERT INTO document_templates (
document_template_name,
document_template_description,
document_template_content,
document_template_created_at,
document_template_updated_at,
document_template_archived_at,
document_template_created_by,
document_template_updated_by
)
SELECT
document_name,
document_description,
document_content,
document_created_at,
document_updated_at,
document_archived_at,
document_created_by,
document_updated_by
FROM
documents
WHERE
document_template = 1
");
mysqli_query($mysqli, "DELETE FROM documents WHERE document_template = 1");
mysqli_query($mysqli, "ALTER TABLE `documents` DROP `document_template`");

View File

@@ -0,0 +1,57 @@
<?php
/*
* ITFlow - Database update to version 2.1.8 (from 2.1.7)
* Included by admin/database_updates.php - do not access directly
*/
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
mysqli_query($mysqli, "CREATE TABLE `software_templates` (
`software_template_id` INT(11) NOT NULL AUTO_INCREMENT,
`software_template_name` VARCHAR(200) NOT NULL,
`software_template_description` TEXT DEFAULT NULL,
`software_template_version` VARCHAR(200) DEFAULT NULL,
`software_template_type` VARCHAR(200) NOT NULL,
`software_template_license_type` VARCHAR(200) DEFAULT NULL,
`software_template_notes` TEXT DEFAULT NULL,
`software_template_created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`software_template_updated_at` DATETIME NULL ON UPDATE CURRENT_TIMESTAMP,
`software_template_archived_at` DATETIME NULL DEFAULT NULL,
PRIMARY KEY (`software_template_id`)
)");
// Copy software Templates over to new software templates table
mysqli_query($mysqli, "
INSERT INTO software_templates (
software_template_name,
software_template_description,
software_template_version,
software_template_type,
software_template_license_type,
software_template_notes,
software_template_created_at,
software_template_updated_at,
software_template_archived_at
)
SELECT
software_name,
software_description,
software_version,
software_type,
software_license_type,
software_notes,
software_created_at,
software_updated_at,
software_archived_at
FROM
software
WHERE
software_template = 1
");
mysqli_query($mysqli, "DELETE FROM software WHERE software_template = 1");
mysqli_query($mysqli, "ALTER TABLE `software` DROP `software_template`");
mysqli_query($mysqli, "ALTER TABLE `software` DROP `software_template_id`");

View File

@@ -0,0 +1,76 @@
<?php
/*
* ITFlow - Database update to version 2.1.9 (from 2.1.8)
* Included by admin/database_updates.php - do not access directly
*/
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
mysqli_query($mysqli, "CREATE TABLE `vendor_templates` (
`vendor_template_id` INT(11) NOT NULL AUTO_INCREMENT,
`vendor_template_name` VARCHAR(200) NOT NULL,
`vendor_template_description` VARCHAR(200) DEFAULT NULL,
`vendor_template_contact_name` VARCHAR(200) DEFAULT NULL,
`vendor_template_phone_country_code` VARCHAR(10) DEFAULT NULL,
`vendor_template_phone` VARCHAR(200) DEFAULT NULL,
`vendor_template_extension` VARCHAR(200) DEFAULT NULL,
`vendor_template_email` VARCHAR(200) DEFAULT NULL,
`vendor_template_website` VARCHAR(200) DEFAULT NULL,
`vendor_template_hours` VARCHAR(200) DEFAULT NULL,
`vendor_template_sla` VARCHAR(200) DEFAULT NULL,
`vendor_template_code` VARCHAR(200) DEFAULT NULL,
`vendor_template_account_number` VARCHAR(200) DEFAULT NULL,
`vendor_template_notes` TEXT DEFAULT NULL,
`vendor_template_created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`vendor_template_updated_at` DATETIME NULL ON UPDATE CURRENT_TIMESTAMP,
`vendor_template_archived_at` DATETIME NULL DEFAULT NULL,
PRIMARY KEY (`vendor_template_id`)
)");
// Copy Vendor Templates over to new vendor templates table
mysqli_query($mysqli, "
INSERT INTO vendor_templates (
vendor_template_name,
vendor_template_description,
vendor_template_contact_name,
vendor_template_phone_country_code,
vendor_template_phone,
vendor_template_extension,
vendor_template_email,
vendor_template_website,
vendor_template_hours,
vendor_template_sla,
vendor_template_code,
vendor_template_account_number,
vendor_template_notes,
vendor_template_created_at,
vendor_template_updated_at,
vendor_template_archived_at
)
SELECT
vendor_name,
vendor_description,
vendor_contact_name,
vendor_phone_country_code,
vendor_phone,
vendor_extension,
vendor_email,
vendor_website,
vendor_hours,
vendor_sla,
vendor_code,
vendor_account_number,
vendor_notes,
vendor_created_at,
vendor_updated_at,
vendor_archived_at
FROM
vendors
WHERE
vendor_template = 1
");
mysqli_query($mysqli, "DELETE FROM vendors WHERE vendor_template = 1");
mysqli_query($mysqli, "ALTER TABLE `vendors` DROP `vendor_template`");

View File

@@ -0,0 +1,10 @@
<?php
/*
* ITFlow - Database update to version 2.2.0 (from 2.1.9)
* Included by admin/database_updates.php - do not access directly
*/
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
mysqli_query($mysqli, "ALTER TABLE `companies` MODIFY `company_currency` VARCHAR(200) DEFAULT 'USD'");

View File

@@ -0,0 +1,10 @@
<?php
/*
* ITFlow - Database update to version 2.2.1 (from 2.2.0)
* Included by admin/database_updates.php - do not access directly
*/
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
mysqli_query($mysqli, "ALTER TABLE `tickets` ADD `ticket_quote_id` INT(11) NOT NULL DEFAULT 0 AFTER `ticket_asset_id`");

View File

@@ -0,0 +1,34 @@
<?php
/*
* ITFlow - Database update to version 2.2.2 (from 2.2.1)
* Included by admin/database_updates.php - do not access directly
*/
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
mysqli_query($mysqli, "CREATE TABLE `ai_providers` (
`ai_provider_id` INT(11) NOT NULL AUTO_INCREMENT,
`ai_provider_name` VARCHAR(200) NOT NULL,
`ai_provider_api_url` VARCHAR(200) NOT NULL,
`ai_provider_api_key` VARCHAR(200) DEFAULT NULL,
`ai_provider_created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`ai_provider_updated_at` DATETIME NULL ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`ai_provider_id`)
)");
mysqli_query($mysqli, "
CREATE TABLE `ai_models` (
`ai_model_id` INT(11) NOT NULL AUTO_INCREMENT,
`ai_model_name` VARCHAR(200) NOT NULL,
`ai_model_prompt` TEXT DEFAULT NULL,
`ai_model_use_case` VARCHAR(200) DEFAULT NULL,
`ai_model_created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`ai_model_updated_at` DATETIME NULL ON UPDATE CURRENT_TIMESTAMP,
`ai_model_ai_provider_id` INT(11) NOT NULL,
PRIMARY KEY (`ai_model_id`),
FOREIGN KEY (`ai_model_ai_provider_id`)
REFERENCES `ai_providers`(`ai_provider_id`)
ON DELETE CASCADE
)
");

View File

@@ -0,0 +1,62 @@
<?php
/*
* ITFlow - Database update to version 2.2.3 (from 2.2.2)
* Included by admin/database_updates.php - do not access directly
*/
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
mysqli_query($mysqli, "CREATE TABLE `payment_methods` (
`payment_method_id` INT(11) NOT NULL AUTO_INCREMENT,
`payment_method_name` VARCHAR(200) NOT NULL,
`payment_method_description` VARCHAR(250) DEFAULT NULL,
`payment_method_created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`payment_method_updated_at` DATETIME NULL ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`payment_method_id`)
)");
mysqli_query($mysqli, "CREATE TABLE `payment_providers` (
`payment_provider_id` INT(11) NOT NULL AUTO_INCREMENT,
`payment_provider_name` VARCHAR(200) NOT NULL,
`payment_provider_description` VARCHAR(250) DEFAULT NULL,
`payment_provider_public_key` VARCHAR(250) DEFAULT NULL,
`payment_provider_private_key` VARCHAR(250) DEFAULT NULL,
`payment_provider_threshold` DECIMAL(15,2) DEFAULT NULL,
`payment_provider_active` TINYINT(1) NOT NULL DEFAULT 1,
`payment_provider_account` INT(11) NOT NULL,
`payment_provider_expense_vendor` INT(11) NOT NULL DEFAULT 0,
`payment_provider_expense_category` INT(11) NOT NULL DEFAULT 0,
`payment_provider_expense_percentage_fee` DECIMAL(4,4) DEFAULT NULL,
`payment_provider_expense_flat_fee` DECIMAL(15,2) DEFAULT NULL,
`payment_provider_created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`payment_provider_updated_at` DATETIME NULL ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`payment_provider_id`)
)");
mysqli_query($mysqli, "CREATE TABLE `client_saved_payment_methods` (
`saved_payment_id` INT(11) NOT NULL AUTO_INCREMENT,
`saved_payment_provider_method` VARCHAR(200) NOT NULL,
`saved_payment_description` VARCHAR(200) DEFAULT NULL,
`saved_payment_client_id` INT(11) NOT NULL,
`saved_payment_provider_id` INT(11) NOT NULL,
`saved_payment_created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`saved_payment_updated_at` DATETIME NULL ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`saved_payment_id`),
FOREIGN KEY (`saved_payment_client_id`) REFERENCES clients(`client_id`) ON DELETE CASCADE,
FOREIGN KEY (`saved_payment_provider_id`) REFERENCES payment_providers(`payment_provider_id`) ON DELETE CASCADE
)");
mysqli_query($mysqli, "CREATE TABLE `client_payment_provider` (
`client_id` INT(11) NOT NULL,
`payment_provider_id` INT(11) NOT NULL,
`payment_provider_client` VARCHAR(200) NOT NULL,
`client_payment_provider_created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`client_id`, `payment_provider_id`),
FOREIGN KEY (`client_id`) REFERENCES clients(`client_id`) ON DELETE CASCADE,
FOREIGN KEY (`payment_provider_id`) REFERENCES payment_providers(`payment_provider_id`) ON DELETE CASCADE
)");
mysqli_query($mysqli, "ALTER TABLE `recurring_payments` ADD `recurring_payment_saved_payment_id` INT(11) DEFAULT NULL AFTER `recurring_payment_recurring_invoice_id`");
mysqli_query($mysqli, "ALTER TABLE `recurring_payments` ADD CONSTRAINT `fk_recurring_saved_payment` FOREIGN KEY (`recurring_payment_saved_payment_id`) REFERENCES `client_saved_payment_methods`(`saved_payment_id`) ON DELETE CASCADE");

View File

@@ -0,0 +1,34 @@
<?php
/*
* ITFlow - Database update to version 2.2.4 (from 2.2.3)
* Included by admin/database_updates.php - do not access directly
*/
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
mysqli_query($mysqli, "CREATE TABLE `credits` (
`credit_id` INT(11) NOT NULL AUTO_INCREMENT,
`credit_amount` DECIMAL(15,2) NOT NULL,
`credit_reference` VARCHAR(250) DEFAULT NULL,
`credit_created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP(),
`credit_created_by` INT(11) NOT NULL,
`credit_expire_at` DATE DEFAULT NULL,
`credit_client_id` INT(11) NOT NULL,
PRIMARY KEY (`credit_id`)
)");
mysqli_query($mysqli, "ALTER TABLE `invoices` ADD `invoice_credit_amount` DECIMAL(15,2) NOT NULL DEFAULT 0.00 AFTER `invoice_discount_amount`");
mysqli_query($mysqli, "CREATE TABLE `discount_codes` (
`discount_code_id` INT(11) NOT NULL AUTO_INCREMENT,
`discount_code_description` VARCHAR(250) DEFAULT NULL,
`discount_code_amount` DECIMAL(15,2) NOT NULL,
`discount_code` VARCHAR(200) NOT NULL,
`discount_code_created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP(),
`discount_code_created_by` INT(11) NOT NULL,
`discount_code_updated_at` DATETIME NULL ON UPDATE CURRENT_TIMESTAMP,
`discount_code_archived_at` DATETIME NULL DEFAULT NULL,
`discount_code_expire_at` DATE DEFAULT NULL,
PRIMARY KEY (`discount_code_id`)
)");

View File

@@ -0,0 +1,10 @@
<?php
/*
* ITFlow - Database update to version 2.2.5 (from 2.2.4)
* Included by admin/database_updates.php - do not access directly
*/
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
mysqli_query($mysqli, "ALTER TABLE `settings` ADD `config_theme_dark` TINYINT(1) NOT NULL DEFAULT 0 AFTER `config_theme`");

View File

@@ -0,0 +1,10 @@
<?php
/*
* ITFlow - Database update to version 2.2.6 (from 2.2.5)
* Included by admin/database_updates.php - do not access directly
*/
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
mysqli_query($mysqli, "ALTER TABLE `assets` ADD `asset_uri_client` VARCHAR(500) NULL DEFAULT NULL AFTER `asset_uri_2`");

View File

@@ -0,0 +1,16 @@
<?php
/*
* ITFlow - Database update to version 2.2.7 (from 2.2.6)
* Included by admin/database_updates.php - do not access directly
*/
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
mysqli_query($mysqli, "ALTER TABLE `credits` DROP `credit_reference`");
mysqli_query($mysqli, "ALTER TABLE `credits` ADD `credit_type` ENUM('prepaid', 'manual', 'refund', 'promotion', 'usage') NOT NULL DEFAULT 'manual' AFTER `credit_amount`");
mysqli_query($mysqli, "ALTER TABLE `credits` ADD `credit_note` TEXT NULL DEFAULT NULL AFTER `credit_type`");
mysqli_query($mysqli, "ALTER TABLE `credits` ADD `credit_invoice_id` INT(11) NULL DEFAULT NULL AFTER `credit_expire_at`");
mysqli_query($mysqli, "ALTER TABLE `credits` ADD INDEX (`credit_client_id`)");
mysqli_query($mysqli, "ALTER TABLE `credits` ADD INDEX (`credit_invoice_id`)");
mysqli_query($mysqli, "ALTER TABLE `credits` ADD INDEX (`credit_created_at`)");

View File

@@ -0,0 +1,11 @@
<?php
/*
* ITFlow - Database update to version 2.2.8 (from 2.2.7)
* Included by admin/database_updates.php - do not access directly
*/
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
mysqli_query($mysqli, "ALTER TABLE `user_settings` ADD `user_config_theme_dark` TINYINT(1) NOT NULL DEFAULT 0 AFTER `user_config_signature`");
mysqli_query($mysqli, "ALTER TABLE `settings` DROP `config_theme_dark`");

View File

@@ -0,0 +1,23 @@
<?php
/*
* ITFlow - Database update to version 2.2.9 (from 2.2.8)
* Included by admin/database_updates.php - do not access directly
*/
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
mysqli_query($mysqli, "ALTER TABLE `products` ADD `product_type` ENUM('service', 'product') NOT NULL DEFAULT 'service' AFTER `product_name`");
mysqli_query($mysqli, "ALTER TABLE `products` ADD `product_code` VARCHAR(200) DEFAULT NULL AFTER `product_description`");
mysqli_query($mysqli, "ALTER TABLE `products` ADD `product_location` VARCHAR(250) DEFAULT NULL AFTER `product_code`");
mysqli_query($mysqli, "CREATE TABLE `product_stock` (
`stock_id` INT(11) NOT NULL AUTO_INCREMENT,
`stock_qty` INT(11) NOT NULL,
`stock_note` TEXT DEFAULT NULL,
`stock_created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP(),
`stock_expense_id` INT(11) DEFAULT NULL,
`stock_item_id` INT(11) DEFAULT NULL,
`stock_product_id` INT(11) NOT NULL,
PRIMARY KEY (`stock_id`)
)");

View File

@@ -0,0 +1,84 @@
<?php
/*
* ITFlow - Database update to version 2.3.0 (from 2.2.9)
* Included by admin/database_updates.php - do not access directly
*/
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
// Migrate Stripe Settings over to new Tables
// Get Current Stripe Settings
$sql_stripe_settings = mysqli_query($mysqli, "SELECT * FROM settings WHERE company_id = 1");
$row = mysqli_fetch_assoc($sql_stripe_settings);
$config_stripe_enable = intval($row['config_stripe_enable']);
if ($config_stripe_enable === 1) {
$config_stripe_publishable = mysqli_real_escape_string($mysqli, $row['config_stripe_publishable']);
$config_stripe_secret = mysqli_real_escape_string($mysqli, $row['config_stripe_secret']);
$config_stripe_account = intval($row['config_stripe_account']);
$config_stripe_expense_vendor = intval($row['config_stripe_expense_vendor']);
$config_stripe_expense_category = intval($row['config_stripe_expense_category']);
$config_stripe_percentage_fee = floatval($row['config_stripe_percentage_fee']);
$config_stripe_flat_fee = floatval($row['config_stripe_flat_fee']);
mysqli_query($mysqli,"INSERT INTO payment_providers SET
payment_provider_name = 'Stripe',
payment_provider_public_key = '$config_stripe_publishable',
payment_provider_private_key = '$config_stripe_secret',
payment_provider_account = $config_stripe_account,
payment_provider_expense_vendor = $config_stripe_expense_vendor,
payment_provider_expense_category = $config_stripe_expense_category,
payment_provider_expense_percentage_fee = $config_stripe_percentage_fee,
payment_provider_expense_flat_fee = $config_stripe_flat_fee"
);
$provider_id = mysqli_insert_id($mysqli);
// Migrate Clients and Payment Method over
$sql_stripe_clients = mysqli_query($mysqli, "SELECT * FROM client_stripe WHERE stripe_pm IS NOT NULL AND stripe_pm != ''");
while ($row = mysqli_fetch_assoc($sql_stripe_clients)) {
$client_id = intval($row['client_id']);
$stripe_id = mysqli_real_escape_string($mysqli, $row['stripe_id']);
$stripe_pm = mysqli_real_escape_string($mysqli, $row['stripe_pm']);
$stripe_pm_details = mysqli_real_escape_string($mysqli, $row['stripe_pm_details'] ?? 'Saved Card');
mysqli_query($mysqli,"INSERT INTO client_payment_provider SET
client_id = $client_id,
payment_provider_id = $provider_id,
payment_provider_client = '$stripe_id'"
);
mysqli_query($mysqli,"INSERT INTO client_saved_payment_methods SET
saved_payment_provider_method = '$stripe_pm',
saved_payment_description = '$stripe_pm_details',
saved_payment_client_id = $client_id,
saved_payment_provider_id = $provider_id"
);
}
}
// Get Stripe provider id
$res = mysqli_query($mysqli, "
SELECT payment_provider_id
FROM payment_providers
WHERE payment_provider_name = 'Stripe'
ORDER BY payment_provider_id DESC
LIMIT 1
");
$stripe = mysqli_fetch_assoc($res);
$stripe_provider_id = intval($stripe['payment_provider_id']);
// Correct mapping: RP -> Recurring Invoice -> Client -> Client's Stripe saved method
mysqli_query($mysqli, "
UPDATE recurring_payments rp
INNER JOIN recurring_invoices ri
ON ri.recurring_invoice_id = rp.recurring_payment_recurring_invoice_id
INNER JOIN client_saved_payment_methods spm
ON spm.saved_payment_client_id = ri.recurring_invoice_client_id
AND spm.saved_payment_provider_id = $stripe_provider_id
SET
rp.recurring_payment_method = 'Credit Card',
rp.recurring_payment_saved_payment_id = spm.saved_payment_id
WHERE rp.recurring_payment_method = 'Stripe'
");

View File

@@ -0,0 +1,17 @@
<?php
/*
* ITFlow - Database update to version 2.3.1 (from 2.3.0)
* Included by admin/database_updates.php - do not access directly
*/
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
// Migrate Payment Methods from Categories Table to new payment_methods table
$sql_categories = mysqli_query($mysqli, "SELECT category_name FROM categories WHERE category_type = 'Payment Method' AND category_name != 'Stripe' AND category_archived_at IS NULL");
while ($row = mysqli_fetch_assoc($sql_categories)) {
$category_name = escapeSql($row['category_name']);
mysqli_query($mysqli,"INSERT INTO payment_methods SET payment_method_name = '$category_name'");
}

View File

@@ -0,0 +1,31 @@
<?php
/*
* ITFlow - Database update to version 2.3.2 (from 2.3.1)
* Included by admin/database_updates.php - do not access directly
*/
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
// Delete all Recurring Payments that are Stripe
mysqli_query($mysqli, "DELETE FROM recurring_payments WHERE recurring_payment_method = 'Stripe'");
// Delete Stripe Specific ITFlow Client Stripe Client Relationship Table
mysqli_query($mysqli, "DROP TABLE client_stripe");
// Delete Unused Stripe and AI Settings now in their own tables
mysqli_query($mysqli, "ALTER TABLE `settings`
DROP `config_stripe_enable`,
DROP `config_stripe_publishable`,
DROP `config_stripe_secret`,
DROP `config_stripe_account`,
DROP `config_stripe_expense_vendor`,
DROP `config_stripe_expense_category`,
DROP `config_stripe_percentage_fee`,
DROP `config_stripe_flat_fee`,
DROP `config_ai_enable`,
DROP `config_ai_provider`,
DROP `config_ai_model`,
DROP `config_ai_url`,
DROP `config_ai_api_key`
");

View File

@@ -0,0 +1,18 @@
<?php
/*
* ITFlow - Database update to version 2.3.3 (from 2.3.2)
* Included by admin/database_updates.php - do not access directly
*/
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
mysqli_query($mysqli, "ALTER TABLE settings
ADD `config_imap_provider` ENUM('standard_imap','google_oauth','microsoft_oauth') NULL DEFAULT NULL AFTER `config_mail_from_name`,
ADD `config_mail_oauth_client_id` VARCHAR(255) NULL AFTER `config_imap_provider`,
ADD `config_mail_oauth_client_secret` VARCHAR(255) NULL AFTER `config_mail_oauth_client_id`,
ADD `config_mail_oauth_tenant_id` VARCHAR(255) NULL AFTER `config_mail_oauth_client_secret`,
ADD `config_mail_oauth_refresh_token` TEXT NULL AFTER `config_mail_oauth_tenant_id`,
ADD `config_mail_oauth_access_token` TEXT NULL AFTER `config_mail_oauth_refresh_token`,
ADD `config_mail_oauth_access_token_expires_at` DATETIME NULL AFTER `config_mail_oauth_access_token`
");

View File

@@ -0,0 +1,12 @@
<?php
/*
* ITFlow - Database update to version 2.3.4 (from 2.3.3)
* Included by admin/database_updates.php - do not access directly
*/
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
mysqli_query($mysqli, "ALTER TABLE settings
ADD `config_smtp_provider` ENUM('standard_smtp','google_oauth','microsoft_oauth') NULL DEFAULT NULL AFTER `config_start_page`
");

View File

@@ -0,0 +1,37 @@
<?php
/*
* ITFlow - Database update to version 2.3.5 (from 2.3.4)
* Included by admin/database_updates.php - do not access directly
*/
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
// Add Software Keys
mysqli_query($mysqli, "CREATE TABLE `software_keys` (
`software_key_id` INT(11) NOT NULL AUTO_INCREMENT,
`software_key` VARCHAR(400) NOT NULL,
`software_key_software_id` INT(11) NOT NULL,
PRIMARY KEY (`software_key_id`),
FOREIGN KEY (`software_key_software_id`) REFERENCES `software`(`software_id`) ON DELETE CASCADE
)");
// Software Key Assignments to Contacts
mysqli_query($mysqli, "CREATE TABLE `software_key_contact_assignments` (
`software_key_id` INT(11) NOT NULL,
`contact_id` INT(11) NOT NULL,
`software_key_assigned_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`software_key_id`, `contact_id`),
FOREIGN KEY (`software_key_id`) REFERENCES `software_keys`(`software_key_id`) ON DELETE CASCADE,
FOREIGN KEY (`contact_id`) REFERENCES `contacts`(`contact_id`) ON DELETE CASCADE
)");
// Software Key Assignments to Assets
mysqli_query($mysqli, "CREATE TABLE `software_key_asset_assignments` (
`software_key_id` INT(11) NOT NULL,
`asset_id` INT(11) NOT NULL,
`software_key_assigned_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`software_key_id`, `asset_id`),
FOREIGN KEY (`software_key_id`) REFERENCES `software_keys`(`software_key_id`) ON DELETE CASCADE,
FOREIGN KEY (`asset_id`) REFERENCES `assets`(`asset_id`) ON DELETE CASCADE
)");

View File

@@ -0,0 +1,11 @@
<?php
/*
* ITFlow - Database update to version 2.3.6 (from 2.3.5)
* Included by admin/database_updates.php - do not access directly
*/
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
mysqli_query($mysqli, "ALTER TABLE `settings` CHANGE `config_smtp_provider` `config_smtp_provider` VARCHAR(200) DEFAULT NULL");
mysqli_query($mysqli, "ALTER TABLE `settings` CHANGE `config_imap_provider` `config_imap_provider` VARCHAR(200) DEFAULT NULL");

View File

@@ -0,0 +1,84 @@
<?php
/*
* ITFlow - Database update to version 2.3.7 (from 2.3.6)
* Included by admin/database_updates.php - do not access directly
*/
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
// Create New Contract Templates Table
mysqli_query($mysqli, "CREATE TABLE `contract_templates` (
`contract_template_id` INT(11) AUTO_INCREMENT PRIMARY KEY,
`contract_template_name` VARCHAR(255) NOT NULL,
`contract_template_description` TEXT NULL DEFAULT NULL,
`contract_template_type` VARCHAR(50) NULL DEFAULT NULL,
`contract_template_sla_low_response_time` INT(11) NULL DEFAULT NULL,
`contract_template_sla_low_resolution_time` INT(11) NULL DEFAULT NULL,
`contract_template_sla_medium_response_time` INT(11) NULL DEFAULT NULL,
`contract_template_sla_medium_resolution_time` INT(11) NULL DEFAULT NULL,
`contract_template_sla_high_response_time` INT(11) NULL DEFAULT NULL,
`contract_template_sla_high_resolution_time` INT(11) NULL DEFAULT NULL,
`contract_template_rate_standard` DECIMAL(10,2) NULL DEFAULT NULL,
`contract_template_rate_after_hours` DECIMAL(10,2) NULL DEFAULT NULL,
`contract_template_net_terms` VARCHAR(50) NULL DEFAULT NULL,
`contract_template_support_hours` VARCHAR(100) NULL DEFAULT NULL,
`contract_template_renewal_frequency` VARCHAR(50) NULL DEFAULT NULL,
`contract_template_details` TEXT NULL DEFAULT NULL,
`contract_template_created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
`contract_template_updated_at` DATETIME NULL ON UPDATE CURRENT_TIMESTAMP,
`contract_template_archived_at` DATETIME NULL DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;");
// Create New Contracts Table
mysqli_query($mysqli, "CREATE TABLE `contracts` (
`contract_id` INT(11) AUTO_INCREMENT PRIMARY KEY,
`contract_name` VARCHAR(255) NOT NULL,
`contract_status` VARCHAR(50) NOT NULL,
`contract_type` VARCHAR(50) NOT NULL,
`contract_sla_low_response_time` INT(11) NULL DEFAULT NULL,
`contract_sla_low_resolution_time` INT(11) NULL DEFAULT NULL,
`contract_sla_medium_response_time` INT(11) NULL DEFAULT NULL,
`contract_sla_medium_resolution_time` INT(11) NULL DEFAULT NULL,
`contract_sla_high_response_time` INT(11) NULL DEFAULT NULL,
`contract_sla_high_resolution_time` INT(11) NULL DEFAULT NULL,
`contract_details` TEXT NULL DEFAULT NULL,
`contract_client_id` INT(11) NULL DEFAULT NULL,
`contract_client_name` VARCHAR(255) NULL DEFAULT NULL,
`contract_client_address` TEXT NULL DEFAULT NULL,
`contract_client_email` VARCHAR(255) NULL DEFAULT NULL,
`contract_client_phone` VARCHAR(100) NULL DEFAULT NULL,
`contract_contact_name` VARCHAR(255) NULL DEFAULT NULL,
`contract_contact_signature` TEXT NULL DEFAULT NULL,
`contract_contact_signature_date` DATETIME NULL DEFAULT NULL,
`contract_agent_name` VARCHAR(255) NULL DEFAULT NULL,
`contract_agent_signature` TEXT NULL DEFAULT NULL,
`contract_agent_signature_date` DATETIME NULL DEFAULT NULL,
`contract_rate_standard` DECIMAL(10,2) NULL DEFAULT NULL,
`contract_rate_after_hours` DECIMAL(10,2) NULL DEFAULT NULL,
`contract_net_terms` VARCHAR(50) NULL DEFAULT NULL,
`contract_support_hours` VARCHAR(100) NULL DEFAULT NULL,
`contract_start_date` DATE NULL DEFAULT NULL,
`contract_end_date` DATE NULL DEFAULT NULL,
`contract_renewal_frequency` VARCHAR(50) NULL DEFAULT NULL,
`contract_created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
`contract_updated_at` DATETIME NULL ON UPDATE CURRENT_TIMESTAMP,
`contract_archived_at` DATETIME NULL DEFAULT NULL,
FOREIGN KEY (`contract_client_id`) REFERENCES `clients`(`client_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;");

View File

@@ -0,0 +1,24 @@
<?php
/*
* ITFlow - Database update to version 2.3.8 (from 2.3.7)
* Included by admin/database_updates.php - do not access directly
*/
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
mysqli_query($mysqli, "
CREATE TABLE `asset_tags` (
`asset_tag_asset_id` INT(11) NOT NULL,
`asset_tag_tag_id` INT(11) NOT NULL,
PRIMARY KEY (`asset_tag_asset_id`, `asset_tag_tag_id`),
CONSTRAINT `fk_asset`
FOREIGN KEY (`asset_tag_asset_id`)
REFERENCES `assets`(`asset_id`)
ON DELETE CASCADE,
CONSTRAINT `fk_tag`
FOREIGN KEY (`asset_tag_tag_id`)
REFERENCES `tags`(`tag_id`)
ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
");

View File

@@ -0,0 +1,23 @@
<?php
/*
* ITFlow - Database update to version 2.3.9 (from 2.3.8)
* Included by admin/database_updates.php - do not access directly
*/
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
mysqli_query($mysqli, "
CREATE TABLE `task_approvals` (
`approval_id` int(11) NOT NULL AUTO_INCREMENT,
`approval_scope` enum('client','internal') NOT NULL,
`approval_type` enum('any','technical','billing','specific') NOT NULL,
`approval_required_user_id` int(11) DEFAULT NULL,
`approval_status` enum('pending','approved','declined') NOT NULL,
`approval_created_by` int(11) NOT NULL,
`approval_approved_by` varchar(255) DEFAULT NULL,
`approval_url_key` varchar(200) NOT NULL,
`approval_task_id` int(11) NOT NULL,
PRIMARY KEY (`approval_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
");

View File

@@ -0,0 +1,43 @@
<?php
/*
* ITFlow - Database update to version 2.4.0 (from 2.3.9)
* Included by admin/database_updates.php - do not access directly
*/
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
mysqli_query($mysqli, "ALTER TABLE `clients` ADD `client_favorite` TINYINT(1) NOT NULL DEFAULT '0' AFTER `client_notes`");
mysqli_query($mysqli, "ALTER TABLE `locations` ADD `location_favorite` TINYINT(1) NOT NULL DEFAULT '0' AFTER `location_notes`");
mysqli_query($mysqli, "ALTER TABLE `vendors` ADD `vendor_favorite` TINYINT(1) NOT NULL DEFAULT '0' AFTER `vendor_notes`");
mysqli_query($mysqli, "ALTER TABLE `software` ADD `software_favorite` TINYINT(1) NOT NULL DEFAULT '0' AFTER `software_notes`");
mysqli_query(
$mysqli,
"ALTER TABLE `credentials`
CHANGE `credential_important` `credential_favorite`
TINYINT(1) NOT NULL DEFAULT 0
AFTER `credential_note`"
);
mysqli_query($mysqli, "ALTER TABLE `assets` DROP `asset_important`");
mysqli_query($mysqli, "ALTER TABLE `assets` ADD `asset_favorite` TINYINT(1) NOT NULL DEFAULT '0' AFTER `asset_notes`");
mysqli_query($mysqli, "ALTER TABLE `documents` DROP `document_important`");
mysqli_query($mysqli, "ALTER TABLE `documents` ADD `document_favorite` TINYINT(1) NOT NULL DEFAULT '0' AFTER `document_client_visible`");
mysqli_query($mysqli, "ALTER TABLE `racks` ADD `rack_favorite` TINYINT(1) NOT NULL DEFAULT '0' AFTER `rack_notes`");
mysqli_query($mysqli, "ALTER TABLE `files` DROP `file_important`");
mysqli_query($mysqli, "ALTER TABLE `files` ADD `file_favorite` TINYINT(1) NOT NULL DEFAULT '0' AFTER `file_mime_type`");
mysqli_query($mysqli, "ALTER TABLE `networks` ADD `network_favorite` TINYINT(1) NOT NULL DEFAULT '0' AFTER `network_notes`");
mysqli_query($mysqli, "ALTER TABLE `domains` ADD `domain_favorite` TINYINT(1) NOT NULL DEFAULT '0' AFTER `domain_notes`");
mysqli_query($mysqli, "ALTER TABLE `certificates` ADD `certificate_favorite` TINYINT(1) NOT NULL DEFAULT '0' AFTER `certificate_notes`");
mysqli_query($mysqli, "ALTER TABLE `services` ADD `service_favorite` TINYINT(1) NOT NULL DEFAULT '0' AFTER `service_notes`");

View File

@@ -0,0 +1,50 @@
<?php
/*
* ITFlow - Database update to version 2.4.1 (from 2.4.0)
* Included by admin/database_updates.php - do not access directly
*/
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
mysqli_query($mysqli, "
CREATE TABLE `quote_items` (
`item_id` int(11) NOT NULL AUTO_INCREMENT,
`item_name` varchar(200) NOT NULL,
`item_description` text DEFAULT NULL,
`item_quantity` decimal(15,2) NOT NULL DEFAULT 0.00,
`item_price` decimal(15,2) NOT NULL DEFAULT 0.00,
`item_subtotal` decimal(15,2) NOT NULL DEFAULT 0.00,
`item_tax` decimal(15,2) NOT NULL DEFAULT 0.00,
`item_total` decimal(15,2) NOT NULL DEFAULT 0.00,
`item_order` int(11) NOT NULL DEFAULT 0,
`item_created_at` datetime NOT NULL DEFAULT current_timestamp(),
`item_updated_at` datetime DEFAULT NULL ON UPDATE current_timestamp(),
`item_archived_at` datetime DEFAULT NULL,
`item_tax_id` int(11) NOT NULL DEFAULT 0,
`item_product_id` int(11) NOT NULL DEFAULT 0,
`item_quote_id` int(11) NOT NULL,
PRIMARY KEY (`item_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
");
mysqli_query($mysqli, "
CREATE TABLE `recurring_invoice_items` (
`item_id` int(11) NOT NULL AUTO_INCREMENT,
`item_name` varchar(200) NOT NULL,
`item_description` text DEFAULT NULL,
`item_quantity` decimal(15,2) NOT NULL DEFAULT 0.00,
`item_price` decimal(15,2) NOT NULL DEFAULT 0.00,
`item_subtotal` decimal(15,2) NOT NULL DEFAULT 0.00,
`item_tax` decimal(15,2) NOT NULL DEFAULT 0.00,
`item_total` decimal(15,2) NOT NULL DEFAULT 0.00,
`item_order` int(11) NOT NULL DEFAULT 0,
`item_created_at` datetime NOT NULL DEFAULT current_timestamp(),
`item_updated_at` datetime DEFAULT NULL ON UPDATE current_timestamp(),
`item_archived_at` datetime DEFAULT NULL,
`item_tax_id` int(11) NOT NULL DEFAULT 0,
`item_product_id` int(11) NOT NULL DEFAULT 0,
`item_recurring_invoice_id` int(11) NOT NULL,
PRIMARY KEY (`item_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
");

View File

@@ -0,0 +1,97 @@
<?php
/*
* ITFlow - Database update to version 2.4.2 (from 2.4.1)
* Included by admin/database_updates.php - do not access directly
*/
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
// Migrate Items
mysqli_query($mysqli, "
INSERT INTO `recurring_invoice_items` (
`item_name`,
`item_description`,
`item_quantity`,
`item_price`,
`item_subtotal`,
`item_tax`,
`item_total`,
`item_order`,
`item_created_at`,
`item_updated_at`,
`item_archived_at`,
`item_tax_id`,
`item_product_id`,
`item_recurring_invoice_id`
)
SELECT
`item_name`,
`item_description`,
`item_quantity`,
`item_price`,
`item_subtotal`,
`item_tax`,
`item_total`,
`item_order`,
`item_created_at`,
`item_updated_at`,
`item_archived_at`,
`item_tax_id`,
`item_product_id`,
`item_recurring_invoice_id`
FROM `invoice_items`
WHERE `item_recurring_invoice_id` != 0
");
mysqli_query($mysqli, "
INSERT INTO `quote_items` (
`item_name`,
`item_description`,
`item_quantity`,
`item_price`,
`item_subtotal`,
`item_tax`,
`item_total`,
`item_order`,
`item_created_at`,
`item_updated_at`,
`item_archived_at`,
`item_tax_id`,
`item_product_id`,
`item_quote_id`
)
SELECT
`item_name`,
`item_description`,
`item_quantity`,
`item_price`,
`item_subtotal`,
`item_tax`,
`item_total`,
`item_order`,
`item_created_at`,
`item_updated_at`,
`item_archived_at`,
`item_tax_id`,
`item_product_id`,
`item_quote_id`
FROM `invoice_items`
WHERE `item_quote_id` != 0
");
mysqli_query($mysqli, "
DELETE FROM `invoice_items`
WHERE `item_recurring_invoice_id` != 0
");
mysqli_query($mysqli, "
DELETE FROM `invoice_items`
WHERE `item_quote_id` != 0
");
mysqli_query($mysqli, "
ALTER TABLE `invoice_items`
DROP COLUMN `item_quote_id`,
DROP COLUMN `item_recurring_invoice_id`
");

View File

@@ -0,0 +1,21 @@
<?php
/*
* ITFlow - Database update to version 2.4.3 (from 2.4.2)
* Included by admin/database_updates.php - do not access directly
*/
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
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

View File

@@ -0,0 +1,42 @@
<?php
/*
* ITFlow - Database update to version 2.4.4 (from 2.4.3)
* Included by admin/database_updates.php - do not access directly
*/
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
// 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

View File

@@ -0,0 +1,11 @@
<?php
/*
* ITFlow - Database update to version 2.4.5 (from 2.4.4)
* Included by admin/database_updates.php - do not access directly
*/
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
// Gateway fee expense now uses the actual fee from Stripe's balance transaction
mysqli_query($mysqli, "ALTER TABLE `payment_providers` DROP `payment_provider_expense_percentage_fee`, DROP `payment_provider_expense_flat_fee`");

View File

@@ -0,0 +1,11 @@
<?php
/*
* ITFlow - Database update to version 2.4.6 (from 2.4.5)
* Included by admin/database_updates.php - do not access directly
*/
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
// Add Allow Deny Client Access to the Enforce Client Permissions
mysqli_query($mysqli, "ALTER TABLE `user_client_permissions` ADD COLUMN `permission_type` ENUM('allow','deny') NOT NULL DEFAULT 'allow'");

View File

@@ -0,0 +1,18 @@
<?php
/*
* ITFlow - Database update to version 2.4.7 (from 2.4.6)
* Included by admin/database_updates.php - do not access directly
*/
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
// API keys now run as a user and inherit that user's RBAC (module / operation / client).
// Existing keys predate this and can't be safely mapped to a user, so they are removed
// and must be recreated with an owning user.
mysqli_query($mysqli, "DELETE FROM api_keys");
// Tie keys to a user; client access now derives from that user, so the old per-key
// client scope is removed.
mysqli_query($mysqli, "ALTER TABLE `api_keys` ADD COLUMN `api_key_user_id` INT(11) NOT NULL DEFAULT 0");
mysqli_query($mysqli, "ALTER TABLE `api_keys` DROP COLUMN `api_key_client_id`");

View File

@@ -0,0 +1,16 @@
<?php
/*
* ITFlow - Database update to version 2.4.8 (from 2.4.7)
* Included by admin/database_updates.php - do not access directly
*/
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
// The login lockout queries run on every login POST and filter the logs table by
// a 10 minute window. With only PRIMARY KEY (log_id) that is a full scan of a
// table that grows with every action in the app, which an attacker can trigger
// once per request. Indexing log_created_at bounds both lockout queries to the
// recent rows, and also speeds up the nightly log purge in cron.php, which had
// the same problem.
mysqli_query($mysqli, "ALTER TABLE `logs` ADD INDEX `log_created_at` (`log_created_at`)");

View File

@@ -0,0 +1,59 @@
<?php
/*
* ITFlow - Database update to version 2.5.0 (from 2.4.8)
* Included by admin/database_updates.php - do not access directly
*
* (2.4.9 was briefly used by the reverted refunds change - skipping that
* number so instances that applied it before the revert still pick this up)
*/
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
// Ticket SLAs. slas holds the response/resolution targets and
// sla_assignments maps a client + priority to an SLA (client 0 rows are
// the global default, an assignment pointing at SLA 0 is an explicit "no
// SLA" override). SLAs are optional - with no assignments defined, nothing
// in the app changes behaviour.
mysqli_query($mysqli, "CREATE TABLE `sla_assignments` (
`sla_assignment_id` int(11) NOT NULL AUTO_INCREMENT,
`sla_assignment_client_id` int(11) NOT NULL DEFAULT 0,
`sla_assignment_priority` varchar(200) NOT NULL,
`sla_assignment_sla_id` int(11) NOT NULL DEFAULT 0,
PRIMARY KEY (`sla_assignment_id`),
UNIQUE KEY `sla_assignment_client_priority` (`sla_assignment_client_id`,`sla_assignment_priority`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci");
mysqli_query($mysqli, "CREATE TABLE `slas` (
`sla_id` int(11) NOT NULL AUTO_INCREMENT,
`sla_name` varchar(200) NOT NULL,
`sla_description` varchar(500) DEFAULT NULL,
`sla_response_minutes` int(11) NOT NULL,
`sla_resolution_minutes` int(11) DEFAULT NULL,
`sla_created_at` datetime NOT NULL DEFAULT current_timestamp(),
`sla_archived_at` datetime DEFAULT NULL,
PRIMARY KEY (`sla_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci");
// SLA targets are computed once at write time and stored on the ticket, so
// the ticket list and cron/ticket_sla.php only ever compare datetimes. The
// due date indexes bound the cron's every-minute scans the same way the
// 2.4.8 logs index bounded the login lockout queries.
mysqli_query($mysqli, "ALTER TABLE `tickets`
ADD COLUMN `ticket_sla_id` int(11) NOT NULL DEFAULT 0 AFTER `ticket_status`,
ADD COLUMN `ticket_response_due_at` datetime DEFAULT NULL AFTER `ticket_first_response_at`,
ADD COLUMN `ticket_resolution_due_at` datetime DEFAULT NULL AFTER `ticket_response_due_at`,
ADD COLUMN `ticket_response_sla_met` tinyint(1) DEFAULT NULL AFTER `ticket_resolution_due_at`,
ADD COLUMN `ticket_resolution_sla_met` tinyint(1) DEFAULT NULL AFTER `ticket_response_sla_met`,
ADD COLUMN `ticket_response_sla_alert_stage` tinyint(1) NOT NULL DEFAULT 0 AFTER `ticket_resolution_sla_met`,
ADD COLUMN `ticket_resolution_sla_alert_stage` tinyint(1) NOT NULL DEFAULT 0 AFTER `ticket_response_sla_alert_stage`,
ADD INDEX `ticket_response_due_at` (`ticket_response_due_at`),
ADD INDEX `ticket_resolution_due_at` (`ticket_resolution_due_at`)");
// Business hours + SLA notification settings
mysqli_query($mysqli, "ALTER TABLE `settings`
ADD COLUMN `config_business_days` varchar(20) NOT NULL DEFAULT '1,2,3,4,5' AFTER `config_timezone`,
ADD COLUMN `config_business_hours_start` time NOT NULL DEFAULT '09:00:00' AFTER `config_business_days`,
ADD COLUMN `config_business_hours_end` time NOT NULL DEFAULT '17:00:00' AFTER `config_business_hours_start`,
ADD COLUMN `config_sla_warning_percent` tinyint(3) NOT NULL DEFAULT 75 AFTER `config_business_hours_end`,
ADD COLUMN `config_sla_notification_email` varchar(200) DEFAULT NULL AFTER `config_sla_warning_percent`");

View File

@@ -0,0 +1,41 @@
<?php
/*
* ITFlow - Database update to version 2.5.1 (from 2.5.0)
* Included by admin/database_updates.php - do not access directly
*/
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
// SLA pausing. sla_history records the intervals during which a ticket's
// resolution clock was actually running - one open row (ended_at NULL) per
// running ticket. Statuses flagged below stop the clock, and the ticket's
// resolution due date is recomputed from the remaining budget on resume.
mysqli_query($mysqli, "CREATE TABLE `sla_history` (
`sla_history_id` int(11) NOT NULL AUTO_INCREMENT,
`sla_history_started_at` datetime NOT NULL,
`sla_history_ended_at` datetime DEFAULT NULL,
`sla_history_minutes` int(11) DEFAULT NULL,
`sla_history_ticket_id` int(11) NOT NULL,
PRIMARY KEY (`sla_history_id`),
KEY `sla_history_ticket_id` (`sla_history_ticket_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci");
// Which statuses pause the resolution clock. Nothing pauses by default, so
// SLA behaviour is unchanged until an admin opts a status in.
mysqli_query($mysqli, "ALTER TABLE `ticket_statuses`
ADD COLUMN `ticket_status_pauses_sla` tinyint(1) NOT NULL DEFAULT 0 AFTER `ticket_status_active`");
// Backfill an open interval for every ticket already running a resolution
// clock, anchored at creation. Without this their consumed time would read
// as zero and the first pause/resume would hand back the full budget.
mysqli_query($mysqli, "INSERT INTO sla_history (sla_history_started_at, sla_history_ticket_id)
SELECT ticket_created_at, ticket_id
FROM tickets
LEFT JOIN slas ON ticket_sla_id = sla_id
WHERE ticket_sla_id > 0
AND sla_resolution_minutes > 0
AND ticket_resolution_due_at IS NOT NULL
AND ticket_resolved_at IS NULL
AND ticket_closed_at IS NULL
AND ticket_archived_at IS NULL");

View File

@@ -0,0 +1,32 @@
<?php
/*
* ITFlow - Database update to version 2.5.2 (from 2.5.1)
* Included by admin/database_updates.php - do not access directly
*/
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
// Resolving a ticket from the kanban board or the client portal recorded
// the resolution timestamp but never judged the resolution SLA, so those
// tickets carry a target, a resolution time and no verdict. They read as
// still-in-flight and are left out of SLA reporting entirely.
//
// Both timestamps were stored, so the verdict is simply recomputed from
// them. Only rows with no verdict at all are touched - anything already
// judged keeps the answer it was given at the time.
mysqli_query($mysqli, "UPDATE tickets
SET ticket_resolution_sla_met = (ticket_resolved_at <= ticket_resolution_due_at)
WHERE ticket_sla_id > 0
AND ticket_resolution_due_at IS NOT NULL
AND ticket_resolved_at IS NOT NULL
AND ticket_resolution_sla_met IS NULL");
// Same repair on the response track, for any ticket whose first response
// was recorded without the verdict being written alongside it
mysqli_query($mysqli, "UPDATE tickets
SET ticket_response_sla_met = (ticket_first_response_at <= ticket_response_due_at)
WHERE ticket_sla_id > 0
AND ticket_response_due_at IS NOT NULL
AND ticket_first_response_at IS NOT NULL
AND ticket_response_sla_met IS NULL");

View File

@@ -0,0 +1,19 @@
<?php
/*
* ITFlow - Database update to version 2.5.3 (from 2.5.2)
* Included by admin/database_updates.php - do not access directly
*/
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
// Asset note types
// The asset_notes table itself already ships in db.sql (and picked up its
// foreign key back in 2.0.2) - it was simply never wired to a UI, so the
// only thing missing for multiple notes per asset is the type list
mysqli_query($mysqli, "INSERT INTO categories SET category_name = 'Maintenance', category_description = 'Routine or scheduled maintenance performed on the asset', category_icon = 'fa-tools', category_type = 'asset_note_type', category_order = 1"); // 1
mysqli_query($mysqli, "INSERT INTO categories SET category_name = 'Repair', category_description = 'Repair work or hardware replacement', category_icon = 'fa-wrench', category_type = 'asset_note_type', category_order = 2"); // 2
mysqli_query($mysqli, "INSERT INTO categories SET category_name = 'Configuration', category_description = 'Configuration or settings change made to the asset', category_icon = 'fa-sliders-h', category_type = 'asset_note_type', category_order = 3"); // 3
mysqli_query($mysqli, "INSERT INTO categories SET category_name = 'Upgrade', category_description = 'Hardware or software upgrade', category_icon = 'fa-arrow-circle-up', category_type = 'asset_note_type', category_order = 4"); // 4
mysqli_query($mysqli, "INSERT INTO categories SET category_name = 'Inspection', category_description = 'Physical inspection or audit of the asset', category_icon = 'fa-clipboard-check', category_type = 'asset_note_type', category_order = 5"); // 5
mysqli_query($mysqli, "INSERT INTO categories SET category_name = 'Note', category_description = 'General note or internal comment', category_icon = 'fa-sticky-note', category_type = 'asset_note_type', category_order = 6"); // 6

View File

@@ -0,0 +1,16 @@
<?php
/*
* ITFlow - Database update to version 2.5.4 (from 2.5.3)
* Included by admin/database_updates.php - do not access directly
*/
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
// Recurring tickets can now carry a ticket template. The template's subject
// and details are copied into the recurring ticket when it is picked, so the
// link exists for one reason only: to stamp the template's task list onto
// every ticket the schedule raises. 0 means no template, matching the other
// optional relations on this table.
mysqli_query($mysqli, "ALTER TABLE `recurring_tickets`
ADD COLUMN `recurring_ticket_ticket_template_id` int(11) NOT NULL DEFAULT 0 AFTER `recurring_ticket_asset_id`");

View File

@@ -0,0 +1,24 @@
<?php
/*
* ITFlow - Database update to version 2.5.5 (from 2.5.4)
* Included by admin/database_updates.php - do not access directly
*/
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
// Backfill guest URL keys. Every path that raises a ticket generates one
// except the recurring block in cron.php, which omitted the column - so
// every ticket the nightly schedule has ever created carries no key. The
// guest view matches on ticket_url_key, so those tickets cannot be opened
// from the "View ticket" link in reply and task-approval emails. It fails
// closed rather than open (NULL never matches), so this is a broken link
// rather than an exposure, but the links stay broken until a key exists.
$sql_tickets_without_url_key = mysqli_query($mysqli, "SELECT ticket_id FROM tickets WHERE ticket_url_key IS NULL OR ticket_url_key = ''");
while ($row = mysqli_fetch_assoc($sql_tickets_without_url_key)) {
$ticket_id = intval($row['ticket_id']);
$url_key = randomString(32);
mysqli_query($mysqli, "UPDATE tickets SET ticket_url_key = '$url_key' WHERE ticket_id = $ticket_id");
}

View File

@@ -0,0 +1,34 @@
<?php
/*
* ITFlow - Database update to version 2.5.6 (from 2.5.5)
* Included by admin/database_updates.php - do not access directly
*/
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
// Recurring tickets now own their task list rather than reading the linked
// ticket template at every run. The template still fills the list in when it
// is picked, but the list can then be edited per schedule - which is only
// meaningful if the edits are what the run actually reads.
mysqli_query($mysqli, "CREATE TABLE `recurring_ticket_tasks` (
`recurring_ticket_task_id` int(11) NOT NULL AUTO_INCREMENT,
`recurring_ticket_task_name` varchar(255) NOT NULL,
`recurring_ticket_task_order` int(11) NOT NULL DEFAULT 0,
`recurring_ticket_task_completion_estimate` int(11) NOT NULL DEFAULT 0,
`recurring_ticket_task_recurring_ticket_id` int(11) NOT NULL,
PRIMARY KEY (`recurring_ticket_task_id`),
KEY `recurring_ticket_task_recurring_ticket_id` (`recurring_ticket_task_recurring_ticket_id`),
CONSTRAINT `recurring_ticket_tasks_ibfk_1` FOREIGN KEY (`recurring_ticket_task_recurring_ticket_id`) REFERENCES `recurring_tickets` (`recurring_ticket_id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci");
// Copy each linked template's current tasks onto the schedule that links it, so
// every existing recurring ticket keeps raising exactly the tasks it raises
// today. Without this the switch to an owned list would silently produce
// taskless tickets on the next run.
mysqli_query($mysqli, "INSERT INTO recurring_ticket_tasks
(recurring_ticket_task_name, recurring_ticket_task_order, recurring_ticket_task_completion_estimate, recurring_ticket_task_recurring_ticket_id)
SELECT task_template_name, task_template_order, task_template_completion_estimate, recurring_ticket_id
FROM recurring_tickets
INNER JOIN task_templates ON task_template_ticket_template_id = recurring_ticket_ticket_template_id
WHERE recurring_ticket_ticket_template_id > 0");

View File

@@ -0,0 +1,16 @@
<?php
/*
* ITFlow - Database update to version 2.5.7 (from 2.5.6)
* Included by admin/database_updates.php - do not access directly
*/
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
// Queued mail can now carry file attachments. The column holds a JSON manifest
// of app-root-relative paths and display names rather than file contents, so
// the queue table stays small and the files stay in uploads/ where the ticket
// attachment endpoints already serve them from. It is scrubbed on delivery
// alongside the body, like email_cal_str.
mysqli_query($mysqli, "ALTER TABLE `email_queue`
ADD COLUMN `email_attachments` text DEFAULT NULL AFTER `email_cal_str`");

View File

@@ -0,0 +1,27 @@
<?php
/*
* ITFlow - Database update to version 2.5.8 (from 2.5.7)
* Included by admin/database_updates.php - do not access directly
*/
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
// Calendars can now be published as a read-only ICS feed for Google Calendar,
// Nextcloud and any other subscription client. The key is stored in cleartext,
// matching invoice_url_key and shared_items.item_key - hashing it would buy
// nothing here (anyone holding the database already holds the events the key
// grants access to) and would make the URL impossible to re-copy for a second
// device without breaking every existing subscriber.
//
// The UNIQUE key is on a nullable column, so unshared calendars all keep NULL.
// The column is explicitly utf8mb4_bin: the table default is utf8mb4_general_ci,
// which compares case-insensitively, and the key alphabet is mixed-case
// base64url - a _ci column would throw away entropy on lookup and make the
// UNIQUE index blind to case.
mysqli_query($mysqli, "ALTER TABLE `calendars`
ADD COLUMN `calendar_feed_key` varchar(64) COLLATE utf8mb4_bin DEFAULT NULL AFTER `calendar_color`,
ADD COLUMN `calendar_feed_busy_only` tinyint(1) NOT NULL DEFAULT 0 AFTER `calendar_feed_key`,
ADD COLUMN `calendar_feed_created_at` datetime DEFAULT NULL AFTER `calendar_feed_busy_only`,
ADD COLUMN `calendar_feed_accessed_at` datetime DEFAULT NULL AFTER `calendar_feed_created_at`,
ADD UNIQUE KEY `calendar_feed_key` (`calendar_feed_key`)");

View File

@@ -0,0 +1,28 @@
<?php
/*
* ITFlow - Database update to version 2.5.9 (from 2.5.8)
* Included by admin/database_updates.php - do not access directly
*/
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
// Events can now be marked all-day explicitly. Until now calendar_events had
// no all-day column at all - FullCalendar inferred it from a start value with
// no time component, which meant a genuine midnight appointment was
// indistinguishable from an all-day event.
mysqli_query($mysqli, "ALTER TABLE `calendar_events`
ADD COLUMN `event_all_day` tinyint(1) NOT NULL DEFAULT 0 AFTER `event_end`");
// Backfill using the same rule the calendar already rendered by, so existing
// events keep displaying exactly as they do today: a midnight start, and
// either no end or a midnight end.
mysqli_query($mysqli, "UPDATE `calendar_events`
SET `event_all_day` = 1
WHERE TIME(`event_start`) = '00:00:00'
AND (`event_end` IS NULL OR TIME(`event_end`) = '00:00:00')");
// Corrects the collation on installs that already applied 2.5.8 before the
// column was pinned to utf8mb4_bin. Harmless to re-run.
mysqli_query($mysqli, "ALTER TABLE `calendars`
MODIFY COLUMN `calendar_feed_key` varchar(64) COLLATE utf8mb4_bin DEFAULT NULL");

View File

@@ -0,0 +1,26 @@
<?php
/*
* ITFlow - Database update to version 2.6.0 (from 2.5.9)
* Included by admin/database_updates.php - do not access directly
*/
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
// cron/cron.php is now a dispatcher: it runs every minute and decides which of the
// scripts in cron/ are due, so the crontab only needs one line. That decision needs
// somewhere durable to record when each job last ran - a job whose minute was missed
// has to be picked up at the next opportunity rather than skipped, and one that is
// half way through must not be started again.
//
// Rows are created by the dispatcher the first time it sees a job, so adding a job
// later needs a line in cron.php and nothing here.
mysqli_query($mysqli, "CREATE TABLE IF NOT EXISTS `cron_jobs` (
`cron_job_id` int(11) NOT NULL AUTO_INCREMENT,
`cron_job_name` varchar(200) NOT NULL,
`cron_job_last_run_at` datetime DEFAULT NULL,
`cron_job_last_finished_at` datetime DEFAULT NULL,
`cron_job_last_status` varchar(200) DEFAULT NULL,
PRIMARY KEY (`cron_job_id`),
UNIQUE KEY `cron_job_name` (`cron_job_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci");

View File

@@ -0,0 +1,32 @@
<?php
/*
* ITFlow - Database update to version 2.6.1 (from 2.6.0)
* Included by admin/database_updates.php - do not access directly
*/
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
// The cron dispatcher's schedule moves out of code and into the database so it can be
// managed from Maintenance > Cron. The registry in includes/cron_jobs.php still decides
// which scripts exist and seeds these columns the first time it meets a job; from then
// on the row is what runs. Nothing here can name a script - a row whose job is not in
// the registry is ignored.
mysqli_query($mysqli, "ALTER TABLE `cron_jobs`
ADD COLUMN `cron_job_enabled` tinyint(1) NOT NULL DEFAULT 1 AFTER `cron_job_name`,
ADD COLUMN `cron_job_schedule` varchar(200) NOT NULL DEFAULT 'Interval' AFTER `cron_job_enabled`,
ADD COLUMN `cron_job_interval_minutes` int(11) NOT NULL DEFAULT 1 AFTER `cron_job_schedule`,
ADD COLUMN `cron_job_daily_at` time DEFAULT NULL AFTER `cron_job_interval_minutes`,
ADD COLUMN `cron_job_run_now` tinyint(1) NOT NULL DEFAULT 0 AFTER `cron_job_daily_at`");
// Duration is here to make a job that is quietly getting slower visible before it starts
// overrunning its own interval.
mysqli_query($mysqli, "ALTER TABLE `cron_jobs`
ADD COLUMN `cron_job_last_duration` decimal(10,2) DEFAULT NULL AFTER `cron_job_last_finished_at`,
ADD COLUMN `cron_job_last_error` text DEFAULT NULL AFTER `cron_job_last_status`,
ADD COLUMN `cron_job_last_error_at` datetime DEFAULT NULL AFTER `cron_job_last_error`");
// Written by the dispatcher every minute before it runs anything, so the admin page can
// tell "no job happened to be due" apart from "the crontab entry is missing".
mysqli_query($mysqli, "ALTER TABLE `settings`
ADD COLUMN `config_cron_last_dispatch_at` datetime DEFAULT NULL");

View File

@@ -0,0 +1,38 @@
<?php
/*
* ITFlow - Database update to version 2.6.2 (from 2.6.1)
* Included by admin/database_updates.php - do not access directly
*/
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
// Two things, both about rows that already existed before 2.6.1 ran.
//
// 2.6.1 added the schedule columns but could not set them, so those rows took the column
// defaults - every minute - which is wrong for three jobs and harmful for the nightly run,
// whose overdue invoice reminders re-send on every pass. The registry in
// includes/cron_jobs.php only ever seeds a row it is creating, so it cannot fix them.
//
// The domain refresher also moves onto the nightly schedule here, which is why it is
// matched at either of the intervals it may be sitting on: 1 from the column default, or 5
// if it was already put back by hand.
//
// Only rows still carrying one of those shipped values are touched, so a schedule someone
// has deliberately changed is left alone. Deliberately hardcoded rather than read from the
// registry: a migration has to keep meaning the same thing years from now, whatever that
// file says by then.
mysqli_query($mysqli, "UPDATE `cron_jobs`
SET `cron_job_schedule` = 'Daily', `cron_job_daily_at` = '03:00:00'
WHERE `cron_job_name` = 'nightly_tasks'
AND `cron_job_schedule` = 'Interval' AND `cron_job_interval_minutes` = 1");
mysqli_query($mysqli, "UPDATE `cron_jobs`
SET `cron_job_schedule` = 'Daily', `cron_job_daily_at` = '03:30:00'
WHERE `cron_job_name` = 'certificate_refresher'
AND `cron_job_schedule` = 'Interval' AND `cron_job_interval_minutes` = 1");
mysqli_query($mysqli, "UPDATE `cron_jobs`
SET `cron_job_schedule` = 'Daily', `cron_job_daily_at` = '04:00:00'
WHERE `cron_job_name` = 'domain_refresher'
AND `cron_job_schedule` = 'Interval' AND `cron_job_interval_minutes` IN (1, 5)");

View File

@@ -0,0 +1,13 @@
<?php
/*
* ITFlow - Database update to version 2.6.3 (from 2.6.2)
* Included by admin/database_updates.php - do not access directly
*/
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
// Add Indexes to Ticket Replies foreign key to ticket_id and same with Attachment Fixes a slow down issue with many Replies with large bodies.
mysqli_query($mysqli, "ALTER TABLE ticket_replies ADD INDEX (ticket_reply_ticket_id, ticket_reply_archived_at)");
mysqli_query($mysqli, "ALTER TABLE ticket_attachments ADD INDEX (ticket_attachment_reply_id)");

View File

@@ -0,0 +1,38 @@
<?php
/*
* ITFlow - Database update to version 2.6.4 (from 2.6.3)
* Included by admin/database_updates.php - do not access directly
*/
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
// Backup catalogue - one row per archive produced, so the app knows what exists
// without trusting a directory listing
mysqli_query($mysqli, "CREATE TABLE IF NOT EXISTS `backups` (
`backup_id` int(11) NOT NULL AUTO_INCREMENT,
`backup_type` varchar(20) NOT NULL DEFAULT 'full',
`backup_file_name` varchar(255) NOT NULL,
`backup_size` bigint(20) NOT NULL DEFAULT 0,
`backup_sha256` varchar(64) DEFAULT NULL,
`backup_status` varchar(20) NOT NULL DEFAULT 'Pending',
`backup_error` text DEFAULT NULL,
`backup_source` varchar(20) NOT NULL DEFAULT 'Manual',
`backup_created_by` varchar(200) DEFAULT NULL,
`backup_created_at` datetime NOT NULL DEFAULT current_timestamp(),
`backup_completed_at` datetime DEFAULT NULL,
`backup_downloaded_at` datetime DEFAULT NULL,
PRIMARY KEY (`backup_id`),
KEY `backup_status_created` (`backup_status`, `backup_created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci");
// Retention and what the scheduled backup produces
mysqli_query($mysqli, "ALTER TABLE settings ADD COLUMN IF NOT EXISTS `config_backup_retention_days` int(11) NOT NULL DEFAULT 30");
mysqli_query($mysqli, "ALTER TABLE settings ADD COLUMN IF NOT EXISTS `config_backup_retention_count` int(11) NOT NULL DEFAULT 5");
mysqli_query($mysqli, "ALTER TABLE settings ADD COLUMN IF NOT EXISTS `config_backup_cron_type` varchar(20) NOT NULL DEFAULT 'full'");
// Seed the scheduled backup job. The dispatcher would create this row itself the first
// time it sees the job, but seeding it here means the schedule is right on an install
// that already has cron_jobs rows - the every-minute default bit us once already.
mysqli_query($mysqli, "INSERT IGNORE INTO cron_jobs SET cron_job_name = 'backup', cron_job_enabled = 0, cron_job_schedule = 'Daily', cron_job_daily_at = '02:00'");

View File

@@ -0,0 +1,26 @@
<?php
/*
* ITFlow - Database update to version 2.6.5 (from 2.6.4)
* Included by admin/database_updates.php - do not access directly
*/
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
// credential_password was inherited as VARBINARY(200) when 2.0.0 renamed login_password.
// What it actually stores is a 16-char IV followed by base64 AES-128-CBC ciphertext -
// pure ASCII, the same shape as credential_username (varchar(500)) and
// users.user_specific_encryption_ciphertext (varchar(200)). Nothing compares, indexes,
// sorts or searches on the column, so binary semantics were never buying anything.
//
// The width was the real problem: base64 expands ~1.37x, so 200 bytes capped the
// cleartext at 127 chars while the credential form offered 350. Anything longer
// overflowed and errored the save. varchar(500) matches credential_username and makes
// 350 the correct form limit for both fields.
// Widen while still binary first. If the charset conversion below fails on an install
// with unexpected bytes, the column is at least already wide enough and the app keeps
// working - varbinary(500) holds the same values just fine.
mysqli_query($mysqli, "ALTER TABLE `credentials` MODIFY `credential_password` varbinary(500) DEFAULT NULL");
mysqli_query($mysqli, "ALTER TABLE `credentials` MODIFY `credential_password` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL");

View File

@@ -0,0 +1,19 @@
<?php
/*
* ITFlow - Database update to version 2.6.6 (from 2.6.5)
* Included by admin/database_updates.php - do not access directly
*/
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
// config_invoice_overdue_reminders never did anything. Both setup paths seeded it with
// '1,3,7' and two files read it, but the only line that would have used it was commented
// out in the nightly job - the reminder schedule is a hardcoded array. The read in
// includes/load_global_settings.php also ran intval() over a comma-separated string, so
// the global was 1 whatever the column said.
//
// Nothing in the app has ever written to it either, so no install has a value worth
// keeping and there is nothing to migrate anywhere.
mysqli_query($mysqli, "ALTER TABLE `settings` DROP COLUMN IF EXISTS `config_invoice_overdue_reminders`");

View File

@@ -0,0 +1,18 @@
<?php
/*
* ITFlow - Database update to version 2.6.7 (from 2.6.6)
* Included by admin/database_updates.php - do not access directly
*/
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
// The AI endpoints used to send a hardcoded temperature (0.5, or 0.3 for ticket
// summaries). Newer OpenAI models accept nothing but their own default and reject
// the request outright, which surfaced as "Failed to get a response from the AI API".
//
// Temperature is now per-model and optional: NULL means don't send the parameter
// at all, which is the setting that works on every provider. Existing rows get
// NULL so they stop sending it.
mysqli_query($mysqli, "ALTER TABLE `ai_models` ADD COLUMN IF NOT EXISTS `ai_model_temperature` decimal(3,2) DEFAULT NULL AFTER `ai_model_use_case`");

View File

@@ -52,6 +52,7 @@ $extensions = [
'php-mbstring' => 'mbstring',
'php-gd' => 'gd',
'php-zip' => 'zip',
'php-xml' => 'xml',
];
foreach ($extensions as $name => $ext) {
@@ -78,7 +79,7 @@ $phpConfig[] = [
];
// Check upload_max_filesize and post_max_size >= 500M
function return_bytes($val) {
function toBytes($val) {
$val = trim($val);
$unit = strtolower(substr($val, -1));
$num = (float)$val;
@@ -98,8 +99,8 @@ $required_bytes = 500 * 1024 * 1024; // 500M in bytes
$upload_max_filesize = ini_get('upload_max_filesize');
$post_max_size = ini_get('post_max_size');
$upload_passed = return_bytes($upload_max_filesize) >= $required_bytes;
$post_passed = return_bytes($post_max_size) >= $required_bytes;
$upload_passed = toBytes($upload_max_filesize) >= $required_bytes;
$post_passed = toBytes($post_max_size) >= $required_bytes;
$phpConfig[] = [
'name' => 'upload_max_filesize >= 500M',
@@ -113,12 +114,12 @@ $phpConfig[] = [
'value' => $post_max_size,
];
// PHP Memory Limit >= 128M
// PHP Memory Limit >= 512M
$memoryLimit = ini_get('memory_limit');
$memoryLimitBytes = return_bytes($memoryLimit);
$memoryLimitPassed = $memoryLimitBytes >= (128 * 1024 * 1024);
$memoryLimitBytes = toBytes($memoryLimit);
$memoryLimitPassed = $memoryLimitBytes >= (512 * 1024 * 1024);
$phpConfig[] = [
'name' => 'PHP Memory Limit >= 128M',
'name' => 'PHP Memory Limit >= 512M',
'passed' => $memoryLimitPassed,
'value' => $memoryLimit,
];
@@ -146,7 +147,7 @@ $phpConfig[] = [
$shellCommands = [];
if ($shell_exec_enabled) {
$commands = ['whois', 'dig', 'git'];
$commands = ['git'];
foreach ($commands as $command) {
$which = trim(shell_exec("which $command 2>/dev/null"));
@@ -159,7 +160,7 @@ if ($shell_exec_enabled) {
}
} else {
// If shell_exec is disabled, mark commands as unavailable
foreach (['whois', 'dig', 'git'] as $command) {
foreach (['git'] as $command) {
$shellCommands[] = [
'name' => "Command '$command' available",
'passed' => false,
@@ -507,7 +508,7 @@ $mysqli->close();
<div class="card card-dark">
<div class="card-header py-3">
<h3 class="card-title"><i class="fas fa-fw fa-bug mr-2"></i>Debug</h3>
<h3 class="card-title"><i class="fas fa-fw fa-bug me-2"></i>Debug</h3>
</div>
<div class="card-body">
@@ -522,19 +523,19 @@ $mysqli->close();
<table class="table table-bordered mb-3">
<tr>
<th>ITFlow release version</th>
<th><?php echo APP_VERSION; ?></th>
<th><?= APP_VERSION ?></th>
</tr>
<tr>
<td>Current DB Version</td>
<td><?php echo CURRENT_DATABASE_VERSION; ?></td>
<td><?= CURRENT_DATABASE_VERSION ?></td>
</tr>
<tr>
<td>Current Code Commit</td>
<td><?php echo $commitHash; ?></td>
<td><?= $commitHash ?></td>
</tr>
<tr>
<td>Current Branch</td>
<td><?php echo $gitBranch; ?></td>
<td><?= $gitBranch ?></td>
</tr>
</table>
</div>
@@ -682,7 +683,7 @@ $mysqli->close();
</tbody>
</table>
</div>
<!-- Database Structure Comparison Table -->
<h3 class="mt-3">Database Structure Comparison</h3>
@@ -765,4 +766,3 @@ $mysqli->close();
<?php
require_once "../includes/footer.php";

View File

@@ -1,124 +1,71 @@
<?php
// Default Column Sort by Filter
$sort = "document_template_name";
$order = "ASC";
require_once "includes/inc_all_admin.php";
require_once "includes/inc_all_admin.php";
$sql = mysqli_query(
$mysqli,
"SELECT SQL_CALC_FOUND_ROWS * FROM document_templates
LEFT JOIN users ON document_template_created_by = user_id
WHERE user_name LIKE '%$q%' OR document_template_name LIKE '%$q%'
ORDER BY $sort $order LIMIT $record_from, $record_to"
);
//Initialize the HTML Purifier to prevent XSS
require "../libs/htmlpurifier/HTMLPurifier.standalone.php";
$num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
$purifier_config = HTMLPurifier_Config::createDefault();
$purifier_config->set('Cache.DefinitionImpl', null); // Disable cache by setting a non-existent directory or an invalid one
$purifier_config->set('URI.AllowedSchemes', ['data' => true, 'src' => true, 'http' => true, 'https' => true]);
$purifier = new HTMLPurifier($purifier_config);
if (isset($_GET['document_template_id'])) {
$document_template_id = intval($_GET['document_template_id']);
}
$sql_document = mysqli_query($mysqli, "SELECT document_template_content, document_template_created_at, document_template_description,
document_template_name, document_template_updated_at FROM document_templates WHERE document_template_id = $document_template_id LIMIT 1");
if (mysqli_num_rows($sql_document) == 0) {
echo "<center><h1 class='text-secondary mt-5'>Nothing to see here</h1><a class='btn btn-lg btn-secondary mt-3' href='javascript:history.back()'><i class='fa fa-fw fa-arrow-left'></i> Go Back</a></center>";
require_once "../includes/footer.php";
exit();
}
$row = mysqli_fetch_assoc($sql_document);
$document_template_name = escapeHtml($row['document_template_name']);
$document_template_description = escapeHtml($row['document_template_description']);
$document_template_content = $purifier->purify($row['document_template_content']);
$document_template_created_at = escapeHtml($row['document_template_created_at']);
$document_template_updated_at = escapeHtml($row['document_template_updated_at']);
?>
<ol class="breadcrumb d-print-none">
<li class="breadcrumb-item">
<a href="../">Home</a>
</li>
<li class="breadcrumb-item">
<a href="users.php">Admin</a>
</li>
<li class="breadcrumb-item">
<a href="document_templates.php">Document Templates</a>
</li>
<li class="breadcrumb-item active"><i class="fas fa-file-alt me-2"></i><?= $document_template_name ?></li>
</ol>
<div class="card card-dark">
<div class="card-header py-2">
<h3 class="card-title mt-2"><i class="fa fa-fw fa-file mr-2"></i>Document Templates</h3>
<div class="card-header">
<h3 class="card-title mt-1"><i class="fa fa-fw fa-file-alt me-2"></i><?= $document_template_name ?></h3>
<div class="card-tools">
<button type="button" class="btn btn-primary ajax-modal" data-modal-url="modals/document_template/document_template_add.php" data-modal-size="xl">
<i class="fas fa-plus mr-2"></i>New Template
<button type="button" class="btn btn-tool ajax-modal"
data-modal-size="xl"
data-modal-url="modals/document_template/document_template_edit.php?id=<?= $document_template_id ?>">
<i class="fas fa-edit me-2"></i>
</button>
</div>
</div>
<div class="card-body">
<form autocomplete="off">
<div class="input-group">
<input type="search" class="form-control " name="q" value="<?php if (isset($q)) { echo stripslashes(nullable_htmlentities($q)); } ?>" placeholder="Search templates">
<div class="input-group-append">
<button class="btn btn-secondary"><i class="fa fa-search"></i></button>
</div>
</div>
</form>
<hr>
<div class="table-responsive-sm">
<table class="table table-striped table-borderless table-hover">
<thead class="text-dark <?php if ($num_rows[0] == 0) { echo "d-none"; } ?>">
<tr>
<th>
<a class="text-secondary" href="?<?php echo $url_query_strings_sort; ?>&sort=document_template_name&order=<?php echo $disp; ?>">
Template Name <?php if ($sort == 'document_template_name') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-secondary" href="?<?php echo $url_query_strings_sort; ?>&sort=document_template_created_at&order=<?php echo $disp; ?>">
Created <?php if ($sort == 'document_template_created_at') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-secondary" href="?<?php echo $url_query_strings_sort; ?>&sort=document_template_updated_at&order=<?php echo $disp; ?>">
Updated <?php if ($sort == 'document_template_updated_at') { echo $order_icon; } ?>
</a>
</th>
<th class="text-center">
Action
</th>
</tr>
</thead>
<tbody>
<?php
while ($row = mysqli_fetch_array($sql)) {
$document_template_id = intval($row['document_template_id']);
$document_template_name = nullable_htmlentities($row['document_template_name']);
$document_template_description = nullable_htmlentities($row['document_template_description']);
$document_template_content = nullable_htmlentities($row['document_template_content']);
$document_template_created_by_name = nullable_htmlentities($row['user_name']);
$document_template_created_at = nullable_htmlentities($row['document_template_created_at']);
$document_template_updated_at = nullable_htmlentities($row['document_template_updated_at']);
?>
<tr>
<td>
<a class="text-bold" href="document_template_details.php?document_template_id=<?php echo $document_template_id; ?>"><i class="fas fa-fw fa-file-alt text-dark"></i> <?php echo $document_template_name; ?></a>
<div class="mt-1 text-secondary"><?php echo $document_template_description; ?></div>
</td>
<td>
<?php echo $document_template_created_at; ?>
<div class="text-secondary"><?php echo $document_template_created_by_name; ?></div>
</td>
<td><?php echo $document_template_updated_at; ?></td>
<td>
<div class="dropdown dropleft text-center">
<button class="btn btn-secondary btn-sm" type="button" data-toggle="dropdown">
<i class="fas fa-ellipsis-h"></i>
</button>
<div class="dropdown-menu">
<a class="dropdown-item ajax-modal" href="#"
data-modal-size="xl"
data-modal-url="modals/document_template/document_template_edit.php?id=<?= $document_template_id ?>">
<i class="fas fa-fw fa-edit mr-2"></i>Edit
</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item text-danger text-bold" href="post.php?delete_document_template=<?php echo $document_template_id; ?>">
<i class="fas fa-fw fa-trash mr-2"></i>Delete
</a>
</div>
</div>
</td>
</tr>
<?php
}
?>
</tbody>
</table>
<br>
</div>
<?php require_once "../includes/filter_footer.php"; ?>
<div class="card-body prettyContent">
<?= $document_template_content ?>
</div>
</div>
<?php require_once "../includes/footer.php";
<script src="../js/pretty_content.js"></script>
<?php
require_once "../includes/footer.php";

View File

@@ -1,70 +0,0 @@
<?php
require_once "includes/inc_all_admin.php";
//Initialize the HTML Purifier to prevent XSS
require "../plugins/htmlpurifier/HTMLPurifier.standalone.php";
$purifier_config = HTMLPurifier_Config::createDefault();
$purifier_config->set('Cache.DefinitionImpl', null); // Disable cache by setting a non-existent directory or an invalid one
$purifier_config->set('URI.AllowedSchemes', ['data' => true, 'src' => true, 'http' => true, 'https' => true]);
$purifier = new HTMLPurifier($purifier_config);
if (isset($_GET['document_template_id'])) {
$document_template_id = intval($_GET['document_template_id']);
}
$sql_document = mysqli_query($mysqli, "SELECT * FROM document_templates WHERE document_template_id = $document_template_id LIMIT 1");
if (mysqli_num_rows($sql_document) == 0) {
echo "<center><h1 class='text-secondary mt-5'>Nothing to see here</h1><a class='btn btn-lg btn-secondary mt-3' href='javascript:history.back()'><i class='fa fa-fw fa-arrow-left'></i> Go Back</a></center>";
require_once "../includes/footer.php";
exit();
}
$row = mysqli_fetch_array($sql_document);
$document_template_name = nullable_htmlentities($row['document_template_name']);
$document_template_description = nullable_htmlentities($row['document_template_description']);
$document_template_content = $purifier->purify($row['document_template_content']);
$document_template_created_at = nullable_htmlentities($row['document_template_created_at']);
$document_template_updated_at = nullable_htmlentities($row['document_template_updated_at']);
?>
<ol class="breadcrumb d-print-none">
<li class="breadcrumb-item">
<a href="../">Home</a>
</li>
<li class="breadcrumb-item">
<a href="users.php">Admin</a>
</li>
<li class="breadcrumb-item">
<a href="document_template.php">Document Templates</a>
</li>
<li class="breadcrumb-item active"><i class="fas fa-file mr-2"></i><?php echo $document_template_name; ?></li>
</ol>
<div class="card card-dark">
<div class="card-header py-2">
<h3 class="card-title mt-2"><i class="fa fa-fw fa-file mr-2"></i><?php echo $document_template_name; ?></h3>
<div class="card-tools">
<button type="button" class="btn btn-primary ajax-modal"
data-modal-size="xl"
data-modal-url="modals/document_template/document_template_edit.php?id=<?= $document_template_id ?>">
<i class="fas fa-edit mr-2"></i>Edit
</button>
</div>
</div>
<div class="card-body prettyContent">
<?php echo $document_template_content; ?>
</div>
</div>
<script src="../js/pretty_content.js"></script>
<?php
require_once "../includes/footer.php";

View File

@@ -0,0 +1,136 @@
<?php
// Default Column Sort by Filter
$sort = "document_template_name";
$order = "ASC";
require_once "includes/inc_all_admin.php";
$sql = mysqli_query(
$mysqli,
"SELECT SQL_CALC_FOUND_ROWS document_template_content, document_template_created_at, document_template_description,
document_template_id, document_template_name, document_template_updated_at, user_name FROM document_templates
LEFT JOIN users ON document_template_created_by = user_id
WHERE user_name LIKE '%$q%' OR document_template_name LIKE '%$q%'
ORDER BY $sort $order LIMIT $record_from, $record_to"
);
$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-file-alt me-2"></i>Document Templates</h3>
<div class="card-tools">
<button type="button" class="btn btn-primary ajax-modal" data-modal-url="modals/document_template/document_template_add.php" data-modal-size="xl">
<i class="fas fa-plus me-2"></i>New Template
</button>
</div>
</div>
<div class="card-body">
<form autocomplete="off">
<div class="input-group">
<input type="search" class="form-control " name="q" value="<?php if (isset($q)) { echo stripslashes(escapeHtml($q)); } ?>" placeholder="Search templates">
<button class="btn btn-secondary"><i class="fa fa-search"></i></button>
</div>
</form>
<hr>
<div class="table-responsive-sm">
<table class="table table-striped table-borderless table-hover">
<thead class="text-dark <?php if ($num_rows[0] == 0) { echo "d-none"; } ?>">
<tr>
<th>
<a class="text-secondary" href="?<?= $url_query_strings_sort ?>&sort=document_template_name&order=<?= $disp ?>">
Template Name <?php if ($sort == 'document_template_name') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-secondary" href="?<?= $url_query_strings_sort ?>&sort=document_template_created_at&order=<?= $disp ?>">
Created <?php if ($sort == 'document_template_created_at') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-secondary" href="?<?= $url_query_strings_sort ?>&sort=document_template_updated_at&order=<?= $disp ?>">
Updated <?php if ($sort == 'document_template_updated_at') { echo $order_icon; } ?>
</a>
</th>
<th class="text-center">
Action
</th>
</tr>
</thead>
<tbody>
<?php
while ($row = mysqli_fetch_assoc($sql)) {
$document_template_id = intval($row['document_template_id']);
$document_template_name = escapeHtml($row['document_template_name']);
$document_template_description = escapeHtml($row['document_template_description']);
$document_template_content = escapeHtml($row['document_template_content']);
$document_template_created_by_name = escapeHtml($row['user_name']);
$document_template_created_at = escapeHtml($row['document_template_created_at']);
$document_template_updated_at = escapeHtml($row['document_template_updated_at']) ?: '-';
?>
<tr>
<td>
<a class="text-dark ajax-modal" href="#"
data-modal-size="xl"
data-modal-url="modals/document_template/document_template_edit.php?id=<?= $document_template_id ?>">
<div class="d-flex">
<i class="fas fa-fw fa-2x fa-file-alt me-2"></i>
<div class="flex-grow-1">
<div><?= $document_template_name ?></div>
<div><small class="text-secondary"><?= $document_template_description ?></small></div>
</div>
</div>
</a>
</td>
<td>
<?= $document_template_created_at ?>
<div class="text-secondary"><?= $document_template_created_by_name ?></div>
</td>
<td><?= $document_template_updated_at ?></td>
<td>
<div class="dropdown dropstart text-center">
<button class="btn btn-secondary btn-sm" type="button" data-bs-toggle="dropdown">
<i class="fas fa-ellipsis-h"></i>
</button>
<div class="dropdown-menu">
<a class="dropdown-item" href="document_template.php?document_template_id=<?= $document_template_id ?>">
<i class="fas fa-fw fa-eye me-2"></i>View
</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item ajax-modal" href="#"
data-modal-size="xl"
data-modal-url="modals/document_template/document_template_edit.php?id=<?= $document_template_id ?>">
<i class="fas fa-fw fa-edit me-2"></i>Edit
</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item text-danger text-bold" href="post.php?delete_document_template=<?= $document_template_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-trash me-2"></i>Delete
</a>
</div>
</div>
</td>
</tr>
<?php
}
?>
</tbody>
</table>
<br>
</div>
<?php require_once "../includes/filter_footer.php"; ?>
</div>
</div>
<?php require_once "../includes/footer.php";

View File

@@ -1,58 +0,0 @@
<?php
require_once "includes/inc_all_admin.php";
?>
<div class="card card-dark">
<div class="card-header py-3">
<h3 class="card-title"><i class="fas fa-fw fa-fingerprint mr-2"></i>Identity Providers</h3>
</div>
<div class="card-body">
<form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token'] ?>">
<h4>Client Portal SSO via Microsoft Entra</h4>
<div class="form-group">
<label>Identity Provider <small class='text-secondary'>(Currently only works with Microsoft Entra ID/AAD)</small></label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-fingerprint"></i></span>
</div>
<select class="form-control select2" readonly>
<option <?php if (empty($config_azure_client_id)) { echo "selected"; } ?>>Disabled</option>
<option <?php if ($config_azure_client_id) { echo "selected"; } ?>>Microsoft Entra</option>
<option>Google (WIP)</option>
<option>Custom SSO (WIP)</option>
</select>
</div>
</div>
<div class="form-group">
<label>MS Entra OAuth App (Client) ID</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-user"></i></span>
</div>
<input type="text" class="form-control" name="azure_client_id" placeholder="e721e3b6-01d6-50e8-7f22-c84d951a52e7" value="<?php echo nullable_htmlentities($config_azure_client_id); ?>">
</div>
</div>
<div class="form-group">
<label>MS Entra OAuth Secret</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-key"></i></span>
</div>
<input type="password" class="form-control" name="azure_client_secret" placeholder="Auto-generated from App Registration" value="<?php echo nullable_htmlentities($config_azure_client_secret); ?>" autocomplete="new-password">
</div>
</div>
<hr>
<button type="submit" name="edit_identity_provider" class="btn btn-primary text-bold"><i class="fa fa-check mr-2"></i>Save</button>
</form>
</div>
</div>
<?php require_once "../includes/footer.php";

View File

@@ -0,0 +1,52 @@
<?php
require_once "includes/inc_all_admin.php";
?>
<div class="card card-dark">
<div class="card-header py-3">
<h3 class="card-title"><i class="fas fa-fw fa-fingerprint me-2"></i>Identity Providers</h3>
</div>
<div class="card-body">
<form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<h4>Client Portal SSO via Microsoft Entra</h4>
<div class="mb-3">
<label>Identity Provider <small class='text-secondary'>(Currently only works with Microsoft Entra ID/AAD)</small></label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-fingerprint"></i></span>
<select class="form-select select2" readonly>
<option <?php if (empty($config_azure_client_id)) { echo "selected"; } ?>>Disabled</option>
<option <?php if ($config_azure_client_id) { echo "selected"; } ?>>Microsoft Entra</option>
<option>Google (WIP)</option>
<option>Custom SSO (WIP)</option>
</select>
</div>
</div>
<div class="mb-3">
<label>MS Entra OAuth App (Client) ID</label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-user"></i></span>
<input type="text" class="form-control" name="azure_client_id" placeholder="e721e3b6-01d6-50e8-7f22-c84d951a52e7" maxlength="200" value="<?= escapeHtml($config_azure_client_id) ?>">
</div>
</div>
<div class="mb-3">
<label>MS Entra OAuth Secret</label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-key"></i></span>
<input type="password" class="form-control" name="azure_client_secret" placeholder="Auto-generated from App Registration" maxlength="200" value="<?= escapeHtml($config_azure_client_secret) ?>" autocomplete="new-password">
</div>
</div>
<hr>
<button type="submit" name="edit_identity_provider" class="btn btn-primary text-bold"><i class="fa fa-check me-2"></i>Save</button>
</form>
</div>
</div>
<?php require_once "../includes/footer.php";

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