mirror of
https://github.com/itflow-org/itflow
synced 2026-08-16 12:35:11 +00:00
Compare commits
1 Commits
master
...
Before-Mul
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
762bbecb63 |
@@ -1,20 +0,0 @@
|
||||
# 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
44
.gitattributes
vendored
@@ -1,44 +0,0 @@
|
||||
# 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
|
||||
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -1 +1 @@
|
||||
custom: ["https://services.itflow.org"]
|
||||
custom: ["https://www.paypal.com/donate/?business=QP4U384PMVBMU&no_recurring=0&item_name=Help+support+the+development+of+ITFlow¤cy_code=USD"]
|
||||
|
||||
31
.github/ISSUE_TEMPLATE/bug_report.md
vendored
31
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,23 +1,32 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Please report bugs on the Forum @ https://forum.itflow.org/t/bug
|
||||
title: 'Please report bugs on the Forum'
|
||||
labels: Support
|
||||
about: Something not working quite right? Create a report to help us improve!
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
We're now using GitHub Issues exclusively for development.
|
||||
-
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
Going forward, GitHub Issues will be used to track confirmed bugs & planned features via Github Projects. This allows us to keep GitHub clean & tidy, whilst maintaining an active and relaxed community experience on the Forum.
|
||||
**Can you reproduce this on the demo at demo.itflow.org**
|
||||
Yes/No/NA
|
||||
|
||||
Please raise bugs on the forum @ https://forum.itflow.org/t/bug. Make sure to mention whether you can replicate the bug on demo.itflow.org.
|
||||
**Are you on the latest available version of ITFlow, with an up-to-date database structure?**
|
||||
Yes/No
|
||||
|
||||
Thanks,
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
4. See error
|
||||
|
||||
The ITFlow team :)
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen, if not obvious.
|
||||
|
||||
--
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
To privately discuss a security issue, please see https://github.com/itflow-org/itflow/security
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
|
||||
19
.github/ISSUE_TEMPLATE/feature_request.md
vendored
19
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,25 +1,16 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Please discuss new features on the Forum @ https://forum.itflow.org/t/features
|
||||
title: 'Please discuss new features on the Forum'
|
||||
title: ''
|
||||
labels: Support
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
We're now using GitHub Issues exclusively for development.
|
||||
-
|
||||
We're now using GitHub just to track features we're definitely planning to implement (and bugs!).
|
||||
|
||||
Going forward, GitHub Issues will be used to track confirmed bugs & planned features via Github Projects. This allows us to keep GitHub clean & tidy, whilst maintaining an active and relaxed community experience on the Forum.
|
||||
Please discuss new feature requests on the forum @ https://forum.itflow.org/t/features. This allows us to gather interest & feedback on the features people feel are most important, whilst keeping GitHub cleaner and more about the code.
|
||||
|
||||
Please discuss new feature requests on the forum @ https://forum.itflow.org/t/features. When creating discussions, try to imagine how your proposed feature would also benefit other users.
|
||||
New feature requests here will be closed.
|
||||
|
||||
All new feature requests raised here will be closed, unless agreed otherwise.
|
||||
|
||||
Thanks,
|
||||
|
||||
The ITFlow team :)
|
||||
|
||||
--
|
||||
|
||||
To privately discuss a security issue, please see https://github.com/itflow-org/itflow/security
|
||||
Thanks :)
|
||||
|
||||
19
.github/ISSUE_TEMPLATE/support.md
vendored
19
.github/ISSUE_TEMPLATE/support.md
vendored
@@ -1,25 +1,18 @@
|
||||
---
|
||||
name: Support
|
||||
about: Please request support on the Forum @ https://forum.itflow.org/t/support
|
||||
title: 'Please visit the Forum for support'
|
||||
about: Please visit the Forum or Discord for support
|
||||
title: ''
|
||||
labels: Support
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
We're now using GitHub Issues exclusively for development.
|
||||
-
|
||||
Please visit the Forum or Discord for support
|
||||
|
||||
Going forward, GitHub Issues will be used to track confirmed bugs & planned features via Github Projects. This allows us to keep GitHub clean & tidy, whilst maintaining an active and relaxed community experience on the Forum.
|
||||
Forum - https://forum.itflow.org/
|
||||
|
||||
Please use the forum for support queries/issues: https://forum.itflow.org/t/support
|
||||
|
||||
All new support requests raised here will be closed.
|
||||
|
||||
Thanks,
|
||||
|
||||
The ITFlow team :)
|
||||
Discord - https://discord.gg/ZjCcBzTUDr
|
||||
|
||||
--
|
||||
|
||||
To privately discuss a security issue, please see https://github.com/itflow-org/itflow/security
|
||||
To discuss a security issue, please see: https://i.imgur.com/P03o0Sy.png
|
||||
|
||||
37
.github/workflows/dbsql-lint.yml
vendored
37
.github/workflows/dbsql-lint.yml
vendored
@@ -1,37 +0,0 @@
|
||||
name: SQL Syntax Check for db.sql
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'db.sql'
|
||||
|
||||
jobs:
|
||||
syntax_check:
|
||||
name: Check db.sql SQL Syntax
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
services:
|
||||
mariadb:
|
||||
image: mariadb:latest
|
||||
ports:
|
||||
- "3306:3306"
|
||||
env:
|
||||
MYSQL_RANDOM_ROOT_PASSWORD: "yes"
|
||||
MARIADB_USER: user
|
||||
MARIADB_PASSWORD: password
|
||||
MARIADB_DATABASE: itfsyntaxdb
|
||||
options: >-
|
||||
--health-cmd="healthcheck.sh --connect --innodb_initialized"
|
||||
--health-interval=10s
|
||||
--health-timeout=5s
|
||||
--health-retries=3
|
||||
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Import & Lint db.sql
|
||||
run: mysql --host 127.0.0.1 -uuser -ppassword itfsyntaxdb < db.sql
|
||||
|
||||
- name: Show imported tables
|
||||
run: mysql --host 127.0.0.1 -uuser -ppassword itfsyntaxdb -e "show tables;"
|
||||
8
.github/workflows/first-interaction.yml
vendored
8
.github/workflows/first-interaction.yml
vendored
@@ -10,20 +10,18 @@ jobs:
|
||||
run:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/first-interaction@v1.2.0
|
||||
- uses: actions/first-interaction@v1.1.1
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
issue-message: |
|
||||
Hello & Welcome! :)
|
||||
|
||||
Thanks for taking the time to get in touch.
|
||||
Thanks for taking the time to get in touch. We'll review this issue shortly.
|
||||
|
||||
We ask that all bugs/feature/support requests are raised via the [forum](https://forum.itflow.org). We'll be in touch shortly to confirm.
|
||||
Whilst you're waiting, please feel free to check out the [forum](https://forum.itflow.org).
|
||||
pr-message: |
|
||||
Hello & Welcome! :)
|
||||
|
||||
Thanks for taking the time to help improve ITFlow. We're excited to review your contributions - we'll review this PR as soon as we can!
|
||||
|
||||
Whilst you're waiting, please feel free to check out the [forum](https://forum.itflow.org).
|
||||
|
||||
Just so you know, all contributions to ITFlow are licensed under the GNU GPL. By contributing you grant us a perpetual & irrevocable license to include your work in ITFlow.
|
||||
|
||||
2
.github/workflows/php-lint.yml
vendored
2
.github/workflows/php-lint.yml
vendored
@@ -12,4 +12,4 @@ jobs:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Check PHP syntax errors
|
||||
uses: overtrue/phplint@9.4.1
|
||||
uses: overtrue/phplint@4.1.0
|
||||
|
||||
48
.gitignore
vendored
48
.gitignore
vendored
@@ -1,57 +1,17 @@
|
||||
node_modules
|
||||
config.php
|
||||
|
||||
uploads/favicon.ico
|
||||
uploads/clients/*
|
||||
!uploads/clients/index.php
|
||||
uploads/custom/*
|
||||
!uploads/custom/index.php
|
||||
uploads/documents/*
|
||||
!uploads/documents/index.php
|
||||
uploads/document_templates/*
|
||||
!uploads/document_templates/index.php
|
||||
uploads/expenses/*
|
||||
!uploads/expenses/index.php
|
||||
uploads/recurring_tickets/*
|
||||
!uploads/recurring_tickets/index.php
|
||||
uploads/settings/*
|
||||
!uploads/settings/index.php
|
||||
uploads/users/*
|
||||
!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/*
|
||||
!uploads/ticket_templates/index.php
|
||||
.idea/*
|
||||
plugins/htmlpurifier/standalone/HTMLPurifier/DefinitionCache/Serializer/HTML/*
|
||||
!plugins/htmlpurifier/standalone/HTMLPurifier/DefinitionCache/Serializer/HTML/.gitkeep
|
||||
plugins/htmlpurifier/standalone/HTMLPurifier/DefinitionCache/Serializer/URI/*
|
||||
!plugins/htmlpurifier/standalone/HTMLPurifier/DefinitionCache/Serializer/URI/.gitkeep
|
||||
plugins/htmlpurifier/standalone/HTMLPurifier/DefinitionCache/Serializer/CSS/*
|
||||
!plugins/htmlpurifier/standalone/HTMLPurifier/DefinitionCache/Serializer/CSS/.gitkeep
|
||||
.vscode/settings.json
|
||||
admin/custom/*
|
||||
!admin/custom/readme.php
|
||||
agent/custom/*
|
||||
!agent/custom/readme.php
|
||||
client/custom/*
|
||||
!client/custom/readme.php
|
||||
guest/custom/*
|
||||
!guest/custom/readme.php
|
||||
cron/custom/*
|
||||
!cron/custom/readme.php
|
||||
scripts/custom/*
|
||||
!scripts/custom/readme.php
|
||||
setup/custom/*
|
||||
!setup/custom/readme.php
|
||||
api/v1/custom/*
|
||||
!api/v1/custom/readme.php
|
||||
.zed
|
||||
|
||||
*.patch
|
||||
backups/*
|
||||
!backups/index.php
|
||||
!backups/.htaccess
|
||||
.idea/*
|
||||
@@ -1,2 +0,0 @@
|
||||
# Prevent access to .git, .github, and config.php
|
||||
RedirectMatch 401 ^/(\.git|\.github|config\.php)
|
||||
1120
CHANGELOG.md
1120
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@@ -60,7 +60,7 @@ representative at an online or offline event.
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
the forums.
|
||||
the forums / Discord.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
|
||||
296
CONTRIBUTING.md
296
CONTRIBUTING.md
@@ -1,296 +0,0 @@
|
||||
# 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.
|
||||
101
README.md
101
README.md
@@ -3,10 +3,15 @@
|
||||
<!-- PROJECT SHIELDS -->
|
||||
[![Contributors][contributors-shield]][contributors-url]
|
||||
[![Stargazers][stars-shield]][stars-url]
|
||||
[![Issues][issues-shield]][issues-url]
|
||||
[![Commits][commit-shield]][commit-url]
|
||||
[![GPL License][license-shield]][license-url]
|
||||
|
||||
<!-- PROJECT LOGO -->
|
||||
<div align="center">
|
||||
<!-- <a href="https://github.com/itflow-org/itflow">
|
||||
<img src="images/logo.png" alt="Logo" width="80" height="80">
|
||||
</a> -->
|
||||
|
||||
<h3 align="center">ITFlow</h3>
|
||||
|
||||
@@ -16,20 +21,18 @@
|
||||
<br />
|
||||
<a href="https://demo.itflow.org"><strong>View demo</strong></a>
|
||||
<br />
|
||||
Username: <b>demo@demo.com</b> | Password: <b>demo</b>
|
||||
Username: <b>demo@demo</b> | Password: <b>demo</b>
|
||||
<br />
|
||||
<br />
|
||||
<a href="https://itflow.org/#about">About</a>
|
||||
<a href="https://itflow.org/index.php?page=About">About</a>
|
||||
·
|
||||
<a href="https://docs.itflow.org">Docs</a>
|
||||
<a href="https://itflow.org/docs.php">Docs</a>
|
||||
·
|
||||
<a href="https://forum.itflow.org/">Forum</a>
|
||||
·
|
||||
<a href="https://forum.itflow.org/t/bug">Report Bug</a>
|
||||
<a href="https://github.com/itflow-org/itflow/issues">Report Bug</a>
|
||||
·
|
||||
<a href="https://forum.itflow.org/t/features">Request Feature</a>
|
||||
·
|
||||
<a href="https://github.com/itflow-org/itflow/security/policy">Security</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -42,26 +45,58 @@
|
||||
|
||||
|
||||
### 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.
|
||||
- You're a busy MSP with 101 things to do.
|
||||
- Information about your clients is unorganised and unstructured: scattered in random tickets or folders - when you do eventually find it, it's out of date.
|
||||
- For some issues, you spend longer looking for the relevant documentation than actually working the ticket.
|
||||
- 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 accounting) into one system
|
||||
|
||||
### In Beta
|
||||
* This project is in beta with many ongoing changes. Updates may unintentionally introduce bugs/security issues.
|
||||
* Whilst we are confident the code is safe, nothing in life is 100% safe or risk-free. Use your best judgement before deciding to store highly confidential information in ITFlow.
|
||||
* We are hoping to have a stable 1.0 release by April/May 2023.
|
||||
|
||||
<!-- BUILT WITH -->
|
||||
### Built With
|
||||
|
||||
* Backend / PHP libs
|
||||
* PHP
|
||||
* MariaDB
|
||||
* PHPMailer
|
||||
* HTML Purifier
|
||||
* PHP Mime Mail Parser
|
||||
|
||||
* CSS
|
||||
* Bootstrap
|
||||
* AdminLTE
|
||||
* fontawesome
|
||||
|
||||
* JS Libraries
|
||||
* chart.js
|
||||
* moments.js
|
||||
* jQuery
|
||||
* pdfmake
|
||||
* Select2
|
||||
* SummerNote
|
||||
* FullCalendar.io
|
||||
|
||||
<!-- GETTING STARTED -->
|
||||
## Getting Started
|
||||
## Getting Started / Installation
|
||||
|
||||
### 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).
|
||||
```
|
||||
wget -O itflow_install.sh https://github.com/itflow-org/itflow-install-script/raw/main/itflow_install.sh
|
||||
bash itflow_install.sh
|
||||
```
|
||||
- Other manual installation methods are available in the [docs](https://docs.itflow.org/installation).
|
||||
ITFlow is self-hosted. There is a full installation guide in the [docs](https://wiki.itflow.org/doku.php?id=wiki:installation), but the main steps are:
|
||||
|
||||
### Managed Hosting
|
||||
- If you'd prefer, we can [host ITFlow for you](https://services.itflow.org/hosting.php).
|
||||
1. Install a LAMP stack (Linux, Apache, MariaDB, PHP)
|
||||
```sh
|
||||
sudo apt install git apache2 php libapache2-mod-php php-intl php-imap php-mailparse php-mysqli php-curl mariadb-server
|
||||
```
|
||||
2. Clone the repo
|
||||
```sh
|
||||
git clone https://github.com/itflow-org/itflow.git /var/www/html
|
||||
```
|
||||
3. Create a MariaDB Database
|
||||
4. Point your browser to your HTTPS web server to begin setup
|
||||
|
||||
<!-- FEATURES -->
|
||||
## Key Features
|
||||
@@ -73,36 +108,36 @@
|
||||
|
||||
<!-- 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.
|
||||
* Comprehensive API to allow custom third party integration
|
||||
* CalDAV to integrate with 3rd party calendars
|
||||
* CardDAV to integrate with 3rd party Address books
|
||||
* Recent caller toast alerts to click and bring up the clients account right away
|
||||
* FIDO2 WebAuthn Support for passwordless auth (TPM Fingerprint), (USB Hardware keys such as Yubikey)
|
||||
|
||||
See the [forum](https://forum.itflow.org/d/11-road-map) and the [open issues](https://github.com/itflow-org/itflow/issues) for a full list of proposed features & known issues.
|
||||
|
||||
|
||||
<!-- 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, feature requests, and general ideas / discussions please use the community [forum](https://forum.itflow.org).
|
||||
For bugs, please raise an [issue](https://github.com/itflow-org/itflow/issues).
|
||||
|
||||
### Contributing
|
||||
We have temporarily paused PRs from the community.
|
||||
If you are able to make a contribution that would make ITFlow better, please fork the repo and create a pull request. Please make sure you're following our [code standards](https://wiki.itflow.org/doku.php?id=wiki:code_standards).
|
||||
For large changes / new features, please discuss the issue with other contributors first.
|
||||
|
||||
#### Contributors
|
||||
<a href="https://github.com/itflow-org/itflow/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=itflow-org/itflow" />
|
||||
</a>
|
||||
|
||||
### Supporters
|
||||
We’re incredibly grateful to the organizations and individuals who support the project - a big thank you to:
|
||||
- CompuMatter
|
||||
- F1 for HELP
|
||||
- digiBandit
|
||||
- JetBrains (PhpStorm)
|
||||
|
||||
<!-- LICENSE -->
|
||||
## 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.
|
||||
* 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).
|
||||
|
||||
<!-- MARKDOWN LINKS & IMAGES -->
|
||||
<!-- https://www.markdownguide.org/basic-syntax/#reference-style-links -->
|
||||
|
||||
25
SECURITY.md
25
SECURITY.md
@@ -1,24 +1,23 @@
|
||||
# Security Policy
|
||||
|
||||
## **Please do NOT report security concerns/vulnerabilities publicly (Issues/forum)**
|
||||
## In Beta
|
||||
|
||||
**We take security seriously**
|
||||
ITFlow is currently in beta and is a work in progress.
|
||||
|
||||
- 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).
|
||||
- [](https://sonarcloud.io/summary/new_code?id=itflow-org_itflow)
|
||||
**We take security seriously.** Whilst we are confident the code is safe, nothing in life is 100% safe or risk-free. You should use your best judgment before entering confidential information into the app.
|
||||
|
||||
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).
|
||||
|
||||
## 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 |
|
||||
|---------| ------------------ |
|
||||
| 26.08 | :white_check_mark: |
|
||||
| ------- | ------------------ |
|
||||
| Beta | :white_check_mark: |
|
||||
|
||||
## Reporting a Vulnerability via GitHub Security Advisories
|
||||
## Reporting a Vulnerability
|
||||
|
||||
**<ins>Please do not report security vulnerabilities through public GitHub issues.</ins>**
|
||||
|
||||
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. You should expect to receive an initial acknowledgement within 72 hours.
|
||||
|
||||
**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.
|
||||
|
||||
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.
|
||||
|
||||
61
account_add_modal.php
Normal file
61
account_add_modal.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<div class="modal" id="addAccountModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content bg-dark">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><i class="fa fa-fw fa-piggy-bank mr-2"></i>New Account</h5>
|
||||
<button type="button" class="close text-white" data-dismiss="modal">
|
||||
<span>×</span>
|
||||
</button>
|
||||
</div>
|
||||
<form action="post.php" method="post" autocomplete="off">
|
||||
<div class="modal-body bg-white">
|
||||
|
||||
<div class="form-group">
|
||||
<label>Account Name <strong class="text-danger">*</strong></label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-piggy-bank"></i></span>
|
||||
</div>
|
||||
<input type="text" class="form-control" name="name" placeholder="Account name" required autofocus>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Opening Balance</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-dollar-sign"></i></span>
|
||||
</div>
|
||||
<input type="number" class="form-control" step="0.01" min="0" name="opening_balance" placeholder="Opening Balance" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Currency <strong class="text-danger">*</strong></label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-money-bill"></i></span>
|
||||
</div>
|
||||
<select class="form-control select2" name="currency_code" required>
|
||||
<option value="">- Currency -</option>
|
||||
<?php foreach ($currencies_array as $currency_code => $currency_name) { ?>
|
||||
<option <?php if ($session_company_currency == $currency_code) { echo "selected"; } ?> value="<?php echo $currency_code; ?>"><?php echo "$currency_code - $currency_name"; ?></option>
|
||||
<?php } ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Notes</label>
|
||||
<textarea class="form-control" rows="5" placeholder="Enter some notes" name="notes"></textarea>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="modal-footer bg-white">
|
||||
<button type="submit" name="add_account" class="btn btn-primary text-bold"><i class="fa fa-check mr-2"></i>Create</button>
|
||||
<button type="button" class="btn btn-light" data-dismiss="modal"><i class="fa fa-times mr-2"></i>Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
37
account_edit_modal.php
Normal file
37
account_edit_modal.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<div class="modal" id="editAccountModal<?php echo $account_id; ?>" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content bg-dark">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><i class="fa fa-fw fa-piggy-bank mr-2"></i>Editing account: <strong><?php echo $account_name; ?></strong></h5>
|
||||
<button type="button" class="close text-white" data-dismiss="modal">
|
||||
<span>×</span>
|
||||
</button>
|
||||
</div>
|
||||
<form action="post.php" method="post" autocomplete="off">
|
||||
<input type="hidden" name="account_id" value="<?php echo $account_id; ?>">
|
||||
<div class="modal-body bg-white">
|
||||
|
||||
<div class="form-group">
|
||||
<label>Account Name <strong class="text-danger">*</strong></label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-piggy-bank"></i></span>
|
||||
</div>
|
||||
<input type="text" class="form-control" name="name" value="<?php echo $account_name; ?>" placeholder="Account name" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Notes</label>
|
||||
<textarea class="form-control" rows="5" placeholder="Enter some notes" name="notes"><?php echo $account_notes; ?></textarea>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="modal-footer bg-white">
|
||||
<button type="submit" name="edit_account" class="btn btn-primary text-bold"><i class="fa fa-check mr-2"></i>Save</button>
|
||||
<button type="button" class="btn btn-light" data-dismiss="modal"><i class="fa fa-times mr-2"></i>Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
114
accounts.php
Normal file
114
accounts.php
Normal file
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
// Default Column Sortby Filter
|
||||
$sb = "account_name";
|
||||
$o = "ASC";
|
||||
|
||||
require_once("inc_all.php");
|
||||
|
||||
//Rebuild URL
|
||||
$url_query_strings_sb = http_build_query(array_merge($_GET, array('sb' => $sb, 'o' => $o)));
|
||||
|
||||
$sql = mysqli_query(
|
||||
$mysqli,
|
||||
"SELECT SQL_CALC_FOUND_ROWS * FROM accounts
|
||||
WHERE account_name LIKE '%$q%' AND company_id = $session_company_id
|
||||
ORDER BY $sb $o 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-piggy-bank mr-2"></i>Accounts</h3>
|
||||
<div class="card-tools">
|
||||
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#addAccountModal"><i class="fas fa-plus mr-2"></i>New Account</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form autocomplete="off">
|
||||
<div class="input-group">
|
||||
<input type="search" class="form-control col-md-4" name="q" value="<?php if (isset($q)) { echo stripslashes(htmlentities($q)); } ?>" placeholder="Search Accounts">
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-primary"><i class="fa fa-search"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<hr>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-borderless table-hover">
|
||||
<thead class="text-dark <?php if ($num_rows[0] == 0) { echo "d-none"; } ?>">
|
||||
<tr>
|
||||
<th><a class="text-dark" href="?<?php echo $url_query_strings_sb; ?>&sb=account_name&o=<?php echo $disp; ?>">Name</a></th>
|
||||
<th><a class="text-dark" href="?<?php echo $url_query_strings_sb; ?>&sb=account_currency_code&o=<?php echo $disp; ?>">Currency</a></th>
|
||||
<th class="text-right">Balance</th>
|
||||
<th class="text-center">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php
|
||||
|
||||
while ($row = mysqli_fetch_array($sql)) {
|
||||
$account_id = intval($row['account_id']);
|
||||
$account_name = htmlentities($row['account_name']);
|
||||
$opening_balance = floatval($row['opening_balance']);
|
||||
$account_currency_code = htmlentities($row['account_currency_code']);
|
||||
$account_notes = htmlentities($row['account_notes']);
|
||||
|
||||
$sql_payments = mysqli_query($mysqli, "SELECT SUM(payment_amount) AS total_payments FROM payments WHERE payment_account_id = $account_id");
|
||||
$row = mysqli_fetch_array($sql_payments);
|
||||
$total_payments = floatval($row['total_payments']);
|
||||
|
||||
$sql_revenues = mysqli_query($mysqli, "SELECT SUM(revenue_amount) AS total_revenues FROM revenues WHERE revenue_account_id = $account_id");
|
||||
$row = mysqli_fetch_array($sql_revenues);
|
||||
$total_revenues = floatval($row['total_revenues']);
|
||||
|
||||
$sql_expenses = mysqli_query($mysqli, "SELECT SUM(expense_amount) AS total_expenses FROM expenses WHERE expense_account_id = $account_id");
|
||||
$row = mysqli_fetch_array($sql_expenses);
|
||||
$total_expenses = floatval($row['total_expenses']);
|
||||
|
||||
$balance = $opening_balance + $total_payments + $total_revenues - $total_expenses;
|
||||
?>
|
||||
|
||||
<tr>
|
||||
<td><a class="text-dark" href="#" data-toggle="modal" data-target="#editAccountModal<?php echo $account_id; ?>"><?php echo $account_name; ?></a></td>
|
||||
<td><?php echo $account_currency_code; ?></td>
|
||||
<td class="text-right"><?php echo numfmt_format_currency($currency_format, $balance, $account_currency_code); ?></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" href="#" data-toggle="modal" data-target="#editAccountModal<?php echo $account_id; ?>">
|
||||
<i class="fas fa-fw fa-edit mr-2"></i>Edit
|
||||
</a>
|
||||
<?php if ($balance == 0) { //Cannot Archive an Account until it reaches 0 Balance ?>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item text-danger" href="post.php?archive_account=<?php echo $account_id; ?>">
|
||||
<i class="fas fa-fw fa-archive mr-2"></i>Archive
|
||||
</a>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<?php
|
||||
include("account_edit_modal.php");
|
||||
}
|
||||
?>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php require_once("pagination.php"); ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
|
||||
require_once("account_add_modal.php");
|
||||
require_once("footer.php");
|
||||
@@ -1,118 +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 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 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="?<?= $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 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=<?= $model_id ?>&csrf_token=<?= $_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";
|
||||
@@ -1,109 +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 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 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="?<?= $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 badge-dark badge-pill p-2" href="ai_models.php"><?= $ai_model_count ?></a>
|
||||
<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=<?= $provider_id ?>&csrf_token=<?= $_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";
|
||||
@@ -1,172 +0,0 @@
|
||||
<?php
|
||||
|
||||
// Default Column Sortby Filter
|
||||
$sort = "api_key_name";
|
||||
$order = "ASC";
|
||||
|
||||
require_once "includes/inc_all_admin.php";
|
||||
|
||||
$sql = mysqli_query(
|
||||
$mysqli,
|
||||
"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"
|
||||
);
|
||||
|
||||
$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>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
|
||||
<form autocomplete="off">
|
||||
<div class="row">
|
||||
|
||||
<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(escapeHtml($q)); } ?>" placeholder="Search keys">
|
||||
<div class="input-group-append">
|
||||
<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>)
|
||||
</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>Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</form>
|
||||
<hr>
|
||||
|
||||
<div class="table-responsive-sm">
|
||||
|
||||
<form id="bulkActions" action="post.php" method="post">
|
||||
<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">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" onclick="checkAll(this)">
|
||||
</div>
|
||||
</td>
|
||||
<th>
|
||||
<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="?<?= $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="?<?= $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="?<?= $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="?<?= $url_query_strings_sort ?>&sort=api_key_expire&order=<?= $disp ?>">
|
||||
Expires <?php if ($sort == 'api_key_expire') { echo $order_icon; } ?>
|
||||
</a>
|
||||
</th>
|
||||
<th class="text-center">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php
|
||||
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
$api_key_id = intval($row['api_key_id']);
|
||||
$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)";
|
||||
}
|
||||
|
||||
$api_key_user = !empty($row['user_name']) ? escapeHtml($row['user_name']) : "<i>None</i>";
|
||||
|
||||
?>
|
||||
<tr>
|
||||
<td class="pr-0">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input bulk-select" type="checkbox" name="api_key_ids[]" value="<?= $api_key_id ?>">
|
||||
</div>
|
||||
</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">
|
||||
<i class="fas fa-ellipsis-h"></i>
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
<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 mr-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 mr-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 mr-2"></i>Delete
|
||||
</a>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<?php } ?>
|
||||
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</form>
|
||||
|
||||
</div>
|
||||
<?php require_once "../includes/filter_footer.php"; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="../js/bulk_actions.js"></script>
|
||||
|
||||
<?php
|
||||
require_once "../includes/footer.php";
|
||||
@@ -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 = '" . 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 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(escapeHtml($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_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="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_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="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="<?= 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="text-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";
|
||||
@@ -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 = '" . 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 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(escapeHtml($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 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-control 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-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_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-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_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="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="<?= 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="text-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="text-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";
|
||||
293
admin/backup.php
293
admin/backup.php
@@ -1,293 +0,0 @@
|
||||
<?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 mr-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 text-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 mr-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 = '<32 random characters>';</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 mr-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 > Cron</a>.
|
||||
</div>
|
||||
<?php } elseif ($config_enable_cron == 0) { ?>
|
||||
<div class="alert alert-warning">
|
||||
<i class="fas fa-fw fa-exclamation-circle mr-2"></i>Cron is switched off in
|
||||
<a href="cron.php">Maintenance > 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-cloud-upload-alt mr-2"></i>Create a Backup</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
|
||||
<?php if ($pending_count > 0) { ?>
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-fw fa-spinner mr-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 mr-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 mr-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-toggle="modal" data-target="#masterKeyModal">
|
||||
<i class="fas fa-fw fa-key mr-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 mr-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-right">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 badge-<?= $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-right">
|
||||
<?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 mr-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 mr-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 text-monospace" value="<?= escapeHtml($backup_key) ?>" readonly onclick="this.select();">
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-secondary" type="button" onclick="navigator.clipboard.writeText('<?= escapeHtml($backup_key) ?>');">
|
||||
<i class="fas fa-fw fa-copy"></i>
|
||||
</button>
|
||||
</div>
|
||||
</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 mr-2"></i>Scheduled Backups & 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="form-row">
|
||||
<div class="form-group col-md-4">
|
||||
<label>Scheduled backup type</label>
|
||||
<select class="form-control" 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="form-group 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="form-group 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 mr-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 mr-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 mr-2"></i>Scheduled backups are switched off.
|
||||
<?php } ?>
|
||||
Turn them on or change the time in <a href="cron.php">Maintenance > 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-undo mr-2"></i>Restoring</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<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 mr-2"></i>Export Master Key</h5>
|
||||
<button type="button" class="close" data-dismiss="modal"><span>×</span></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="form-group">
|
||||
<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 mr-2"></i>Show Master Key</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
require_once "../includes/footer.php";
|
||||
@@ -1,61 +0,0 @@
|
||||
<?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;
|
||||
@@ -1,219 +0,0 @@
|
||||
<?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 mr-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 mr-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-right 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'] ?> mr-2"></i><?= escapeHtml($category_type_details['label']) ?>
|
||||
<span class="badge badge-pill badge-dark float-right 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 ">
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-primary"><i class="fa fa-search"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 mb-2">
|
||||
<a href="<?= $archive_toggle_url ?>"
|
||||
class="btn float-right <?php if ($archived) {
|
||||
echo 'btn-primary';
|
||||
} else {
|
||||
echo 'btn-default';
|
||||
} ?>"><i
|
||||
class="fas fa-fw fa-archive mr-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 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-info confirm-link"
|
||||
href="post.php?restore_category=<?= $category_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
|
||||
<i class="fas fa-fw fa-redo mr-2"></i>Restore
|
||||
</a>
|
||||
<a class="dropdown-item text-danger confirm-link"
|
||||
href="post.php?delete_category=<?= $category_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
|
||||
<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=<?= $category_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
require_once "../includes/footer.php";
|
||||
@@ -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(escapeHtml($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_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 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=<?= $id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
|
||||
<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"; ?>
|
||||
208
admin/cron.php
208
admin/cron.php
@@ -1,208 +0,0 @@
|
||||
<?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 mr-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 mr-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 mr-2"></i>Cron last checked in <strong><?= escapeHtml(strtolower(cronJobTimeAgo($cron_last_dispatch_at))) ?></strong>.
|
||||
<span class="text-muted ml-2"><?= escapeHtml($cron_command) ?></span>
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
<?php if ($config_enable_cron == 0) { ?>
|
||||
<div class="alert alert-warning">
|
||||
<div class="float-right">
|
||||
<a class="btn btn-sm btn-warning" href="post.php?enable_cron=1&csrf_token=<?= $_SESSION['csrf_token'] ?>">
|
||||
<i class="fas fa-fw fa-power-off mr-2"></i>Turn cron on
|
||||
</a>
|
||||
</div>
|
||||
<h5><i class="fas fa-fw fa-exclamation-circle mr-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 mr-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&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 badge-warning">Queued</span>';
|
||||
} elseif ($last_status === 'Running') {
|
||||
$status_badge = '<span class="badge badge-info">Running</span>';
|
||||
} elseif ($last_status === 'Completed') {
|
||||
$status_badge = '<span class="badge badge-success">Completed</span>';
|
||||
} elseif ($last_status === 'Failed') {
|
||||
$status_badge = '<span class="badge badge-danger">Failed</span>';
|
||||
} elseif ($last_status !== null) {
|
||||
$status_badge = '<span class="badge badge-secondary">Stopped</span>';
|
||||
} else {
|
||||
$status_badge = '<span class="badge badge-light">Never run</span>';
|
||||
}
|
||||
|
||||
?>
|
||||
<tr class="<?= $enabled ? '' : 'text-muted' ?>">
|
||||
<td>
|
||||
<strong><?= escapeHtml($job['label']) ?></strong>
|
||||
<?php if (!$enabled) { ?><span class="badge badge-secondary ml-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 dropleft text-center">
|
||||
<button class="btn btn-secondary btn-sm" type="button" data-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 mr-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 mr-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 mr-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 mr-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-right">
|
||||
<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 mr-2"></i>Last error</strong>
|
||||
<small class="text-muted ml-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 mr-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"; ?>
|
||||
@@ -1,8 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
|
||||
- Custom Pages -
|
||||
If you wish to add custom pages to ITFlow, add them to this directory"
|
||||
Link to Documentation for File Directory Structure and examples
|
||||
*/
|
||||
@@ -1,149 +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 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 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(escapeHtml($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="?<?= $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 ?> mr-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 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=<?= $custom_link_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
|
||||
<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";
|
||||
@@ -1,69 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow
|
||||
* Applies pending database migrations from admin/database_updates/
|
||||
* Used in conjunction with database_version.php
|
||||
*
|
||||
* Each file in admin/database_updates/ is named for the database version it
|
||||
* upgrades TO (e.g. 2.4.5.php contains the queries that bring 2.4.4 to 2.4.5).
|
||||
* This runner applies every file newer than the current database version, in
|
||||
* order, and bumps config_current_database_version after each one - so a
|
||||
* single run brings the database all the way to the latest version.
|
||||
*
|
||||
* To add a migration: create admin/database_updates/<new version>.php - that
|
||||
* is the whole job. The latest version is derived from the directory listing
|
||||
* (see includes/database_version.php) and this runner handles the version
|
||||
* bump, so there is no constant to update and no bump query to remember.
|
||||
*/
|
||||
|
||||
// Check if our database versions are defined
|
||||
// If undefined, the file is probably being accessed directly rather than called via post.php?update_db or update_cli.php
|
||||
if (!defined("LATEST_DATABASE_VERSION") || !defined("CURRENT_DATABASE_VERSION") || !isset($mysqli)) {
|
||||
echo "Cannot access this file directly.";
|
||||
exit();
|
||||
}
|
||||
|
||||
// Migration files include-guard against this constant
|
||||
define("FROM_DB_UPDATER", true);
|
||||
|
||||
// Outputs for the caller (post/update.php, update_cli.php)
|
||||
$database_updates_applied = []; // Versions successfully applied this run
|
||||
$database_updates_error = null; // "version: error message" if a migration failed
|
||||
|
||||
// Collect and order the migration files by version (glob sorts alphabetically,
|
||||
// which would put 2.4.10 before 2.4.9 - version_compare gets it right)
|
||||
$database_update_files = [];
|
||||
foreach (glob(__DIR__ . "/database_updates/*.php") as $file) {
|
||||
$version = basename($file, ".php");
|
||||
if (preg_match('/^\d+(\.\d+)+$/', $version)) {
|
||||
$database_update_files[$version] = $file;
|
||||
}
|
||||
}
|
||||
uksort($database_update_files, "version_compare");
|
||||
|
||||
// Apply everything newer than the current database version, in order
|
||||
// (CURRENT_DATABASE_VERSION is a constant frozen at page load, so track progress in a variable)
|
||||
$database_current_version = CURRENT_DATABASE_VERSION;
|
||||
|
||||
foreach ($database_update_files as $version => $file) {
|
||||
|
||||
if (version_compare($version, $database_current_version, "<=")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
require $file;
|
||||
} catch (Throwable $e) {
|
||||
// Stop here - config_current_database_version still points at the last
|
||||
// completed migration, so a re-run resumes at this file
|
||||
$database_updates_error = "$version: " . $e->getMessage();
|
||||
error_log("ITFlow database update to version $version failed: " . $e->getMessage());
|
||||
break;
|
||||
}
|
||||
|
||||
// Migration succeeded - bump the database version
|
||||
mysqli_query($mysqli, "UPDATE `settings` SET `config_current_database_version` = '" . escapeSql($version) . "'");
|
||||
$database_current_version = $version;
|
||||
$database_updates_applied[] = $version;
|
||||
}
|
||||
@@ -1,184 +0,0 @@
|
||||
<?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
|
||||
)
|
||||
");
|
||||
@@ -1,15 +0,0 @@
|
||||
<?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`");
|
||||
@@ -1,153 +0,0 @@
|
||||
<?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
|
||||
");
|
||||
@@ -1,31 +0,0 @@
|
||||
<?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
|
||||
");
|
||||
@@ -1,20 +0,0 @@
|
||||
<?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
|
||||
");
|
||||
@@ -1,364 +0,0 @@
|
||||
<?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
|
||||
");
|
||||
@@ -1,36 +0,0 @@
|
||||
<?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);
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
<?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;");
|
||||
@@ -1,10 +0,0 @@
|
||||
<?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`");
|
||||
@@ -1,12 +0,0 @@
|
||||
<?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`");
|
||||
@@ -1,19 +0,0 @@
|
||||
<?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`");
|
||||
@@ -1,10 +0,0 @@
|
||||
<?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`");
|
||||
@@ -1,10 +0,0 @@
|
||||
<?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`");
|
||||
@@ -1,36 +0,0 @@
|
||||
<?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");
|
||||
@@ -1,11 +0,0 @@
|
||||
<?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`");
|
||||
@@ -1,13 +0,0 @@
|
||||
<?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`");
|
||||
@@ -1,27 +0,0 @@
|
||||
<?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`");
|
||||
@@ -1,52 +0,0 @@
|
||||
<?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`");
|
||||
@@ -1,57 +0,0 @@
|
||||
<?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`");
|
||||
@@ -1,76 +0,0 @@
|
||||
<?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`");
|
||||
@@ -1,10 +0,0 @@
|
||||
<?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'");
|
||||
@@ -1,10 +0,0 @@
|
||||
<?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`");
|
||||
@@ -1,34 +0,0 @@
|
||||
<?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
|
||||
)
|
||||
");
|
||||
@@ -1,62 +0,0 @@
|
||||
<?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");
|
||||
@@ -1,34 +0,0 @@
|
||||
<?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`)
|
||||
)");
|
||||
@@ -1,10 +0,0 @@
|
||||
<?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`");
|
||||
@@ -1,10 +0,0 @@
|
||||
<?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`");
|
||||
@@ -1,16 +0,0 @@
|
||||
<?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`)");
|
||||
@@ -1,11 +0,0 @@
|
||||
<?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`");
|
||||
@@ -1,23 +0,0 @@
|
||||
<?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`)
|
||||
)");
|
||||
@@ -1,84 +0,0 @@
|
||||
<?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'
|
||||
");
|
||||
@@ -1,17 +0,0 @@
|
||||
<?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'");
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
<?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`
|
||||
");
|
||||
@@ -1,18 +0,0 @@
|
||||
<?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`
|
||||
");
|
||||
@@ -1,12 +0,0 @@
|
||||
<?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`
|
||||
");
|
||||
@@ -1,37 +0,0 @@
|
||||
<?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
|
||||
)");
|
||||
@@ -1,11 +0,0 @@
|
||||
<?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");
|
||||
@@ -1,84 +0,0 @@
|
||||
<?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;");
|
||||
@@ -1,24 +0,0 @@
|
||||
<?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;
|
||||
");
|
||||
@@ -1,23 +0,0 @@
|
||||
<?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;
|
||||
");
|
||||
@@ -1,43 +0,0 @@
|
||||
<?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`");
|
||||
@@ -1,50 +0,0 @@
|
||||
<?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;
|
||||
");
|
||||
@@ -1,97 +0,0 @@
|
||||
<?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`
|
||||
");
|
||||
@@ -1,21 +0,0 @@
|
||||
<?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
|
||||
@@ -1,42 +0,0 @@
|
||||
<?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
|
||||
@@ -1,11 +0,0 @@
|
||||
<?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`");
|
||||
@@ -1,11 +0,0 @@
|
||||
<?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'");
|
||||
@@ -1,18 +0,0 @@
|
||||
<?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`");
|
||||
@@ -1,16 +0,0 @@
|
||||
<?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`)");
|
||||
@@ -1,59 +0,0 @@
|
||||
<?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`");
|
||||
@@ -1,41 +0,0 @@
|
||||
<?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");
|
||||
@@ -1,32 +0,0 @@
|
||||
<?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");
|
||||
@@ -1,19 +0,0 @@
|
||||
<?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
|
||||
@@ -1,16 +0,0 @@
|
||||
<?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`");
|
||||
@@ -1,24 +0,0 @@
|
||||
<?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");
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
<?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");
|
||||
@@ -1,16 +0,0 @@
|
||||
<?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`");
|
||||
@@ -1,27 +0,0 @@
|
||||
<?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`)");
|
||||
@@ -1,28 +0,0 @@
|
||||
<?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");
|
||||
@@ -1,26 +0,0 @@
|
||||
<?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");
|
||||
@@ -1,32 +0,0 @@
|
||||
<?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");
|
||||
@@ -1,38 +0,0 @@
|
||||
<?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)");
|
||||
@@ -1,13 +0,0 @@
|
||||
<?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)");
|
||||
@@ -1,38 +0,0 @@
|
||||
<?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'");
|
||||
@@ -1,26 +0,0 @@
|
||||
<?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");
|
||||
@@ -1,19 +0,0 @@
|
||||
<?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`");
|
||||
@@ -1,18 +0,0 @@
|
||||
<?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`");
|
||||
768
admin/debug.php
768
admin/debug.php
@@ -1,768 +0,0 @@
|
||||
<?php
|
||||
|
||||
require_once "includes/inc_all_admin.php";
|
||||
require_once "../includes/database_version.php";
|
||||
require_once "../config.php";
|
||||
|
||||
$checks = [];
|
||||
|
||||
// Execute the git command to get the latest commit hash
|
||||
$commitHash = shell_exec('git log -1 --format=%H');
|
||||
|
||||
// Get branch info
|
||||
$gitBranch = shell_exec('git rev-parse --abbrev-ref HEAD');
|
||||
|
||||
// Section: System Information
|
||||
$systemInfo = [];
|
||||
|
||||
// Operating System and Version
|
||||
$os = php_uname();
|
||||
$systemInfo[] = [
|
||||
'name' => 'Operating System',
|
||||
'value' => $os,
|
||||
];
|
||||
|
||||
// Web Server and Version
|
||||
$webServer = $_SERVER['SERVER_SOFTWARE'] ?? 'Unknown';
|
||||
$systemInfo[] = [
|
||||
'name' => 'Web Server',
|
||||
'value' => $webServer,
|
||||
];
|
||||
|
||||
// Kernel and Version
|
||||
$kernelVersion = php_uname('r');
|
||||
$systemInfo[] = [
|
||||
'name' => 'Kernel Version',
|
||||
'value' => $kernelVersion,
|
||||
];
|
||||
|
||||
// Database and Version
|
||||
$dbVersion = $mysqli->server_info;
|
||||
$systemInfo[] = [
|
||||
'name' => 'Database Version',
|
||||
'value' => $dbVersion,
|
||||
];
|
||||
|
||||
// Section: PHP Extensions
|
||||
$phpExtensions = [];
|
||||
$extensions = [
|
||||
'php-mysqli' => 'mysqli',
|
||||
'php-intl' => 'intl',
|
||||
'php-curl' => 'curl',
|
||||
'php-mbstring' => 'mbstring',
|
||||
'php-gd' => 'gd',
|
||||
'php-zip' => 'zip',
|
||||
'php-xml' => 'xml',
|
||||
];
|
||||
|
||||
foreach ($extensions as $name => $ext) {
|
||||
$loaded = extension_loaded($ext);
|
||||
$phpExtensions[] = [
|
||||
'name' => "$name installed",
|
||||
'passed' => $loaded,
|
||||
'value' => $loaded ? 'Installed' : 'Not Installed',
|
||||
];
|
||||
}
|
||||
|
||||
// Section: PHP Configuration
|
||||
$phpConfig = [];
|
||||
|
||||
// Check if shell_exec is enabled
|
||||
$disabled_functions = explode(',', ini_get('disable_functions'));
|
||||
$disabled_functions = array_map('trim', $disabled_functions);
|
||||
$shell_exec_enabled = !in_array('shell_exec', $disabled_functions);
|
||||
|
||||
$phpConfig[] = [
|
||||
'name' => 'shell_exec is enabled',
|
||||
'passed' => $shell_exec_enabled,
|
||||
'value' => $shell_exec_enabled ? 'Enabled' : 'Disabled',
|
||||
];
|
||||
|
||||
// Check upload_max_filesize and post_max_size >= 500M
|
||||
function toBytes($val) {
|
||||
$val = trim($val);
|
||||
$unit = strtolower(substr($val, -1));
|
||||
$num = (float)$val;
|
||||
switch ($unit) {
|
||||
case 'g':
|
||||
$num *= 1024;
|
||||
case 'm':
|
||||
$num *= 1024;
|
||||
case 'k':
|
||||
$num *= 1024;
|
||||
}
|
||||
return $num;
|
||||
}
|
||||
|
||||
$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 = toBytes($upload_max_filesize) >= $required_bytes;
|
||||
$post_passed = toBytes($post_max_size) >= $required_bytes;
|
||||
|
||||
$phpConfig[] = [
|
||||
'name' => 'upload_max_filesize >= 500M',
|
||||
'passed' => $upload_passed,
|
||||
'value' => $upload_max_filesize,
|
||||
];
|
||||
|
||||
$phpConfig[] = [
|
||||
'name' => 'post_max_size >= 500M',
|
||||
'passed' => $post_passed,
|
||||
'value' => $post_max_size,
|
||||
];
|
||||
|
||||
// PHP Memory Limit >= 128M
|
||||
$memoryLimit = ini_get('memory_limit');
|
||||
$memoryLimitBytes = toBytes($memoryLimit);
|
||||
$memoryLimitPassed = $memoryLimitBytes >= (128 * 1024 * 1024);
|
||||
$phpConfig[] = [
|
||||
'name' => 'PHP Memory Limit >= 128M',
|
||||
'passed' => $memoryLimitPassed,
|
||||
'value' => $memoryLimit,
|
||||
];
|
||||
|
||||
// Max Execution Time >= 300 seconds
|
||||
$maxExecutionTime = ini_get('max_execution_time');
|
||||
$maxExecutionTimePassed = $maxExecutionTime >= 300;
|
||||
$phpConfig[] = [
|
||||
'name' => 'Max Execution Time >= 300 seconds',
|
||||
'passed' => $maxExecutionTimePassed,
|
||||
'value' => $maxExecutionTime . ' seconds',
|
||||
];
|
||||
|
||||
// Check PHP version >= 8.2.0
|
||||
$php_version = PHP_VERSION;
|
||||
$php_passed = version_compare($php_version, '8.2.0', '>=');
|
||||
|
||||
$phpConfig[] = [
|
||||
'name' => 'PHP version >= 8.2.0',
|
||||
'passed' => $php_passed,
|
||||
'value' => $php_version,
|
||||
];
|
||||
|
||||
// Section: Shell Commands
|
||||
$shellCommands = [];
|
||||
|
||||
if ($shell_exec_enabled) {
|
||||
$commands = ['git'];
|
||||
|
||||
foreach ($commands as $command) {
|
||||
$which = trim(shell_exec("which $command 2>/dev/null"));
|
||||
$exists = !empty($which);
|
||||
$shellCommands[] = [
|
||||
'name' => "Command '$command' available",
|
||||
'passed' => $exists,
|
||||
'value' => $exists ? $which : 'Not Found',
|
||||
];
|
||||
}
|
||||
} else {
|
||||
// If shell_exec is disabled, mark commands as unavailable
|
||||
foreach (['git'] as $command) {
|
||||
$shellCommands[] = [
|
||||
'name' => "Command '$command' available",
|
||||
'passed' => false,
|
||||
'value' => 'shell_exec Disabled',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Section: SSL Checks
|
||||
$sslChecks = [];
|
||||
|
||||
// Check if accessing via HTTPS
|
||||
$https = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') || $_SERVER['SERVER_PORT'] == 443;
|
||||
$sslChecks[] = [
|
||||
'name' => 'Accessing via HTTPS',
|
||||
'passed' => $https,
|
||||
'value' => $https ? 'Yes' : 'No',
|
||||
];
|
||||
|
||||
// SSL Certificate Validity Check
|
||||
if ($https) {
|
||||
$streamContext = stream_context_create(["ssl" => ["capture_peer_cert" => true]]);
|
||||
$socket = @stream_socket_client("ssl://{$_SERVER['HTTP_HOST']}:443", $errno, $errstr, 30, STREAM_CLIENT_CONNECT, $streamContext);
|
||||
|
||||
if ($socket) {
|
||||
$params = stream_context_get_params($socket);
|
||||
$cert = $params['options']['ssl']['peer_certificate'];
|
||||
$certInfo = openssl_x509_parse($cert);
|
||||
|
||||
$validFrom = $certInfo['validFrom_time_t'];
|
||||
$validTo = $certInfo['validTo_time_t'];
|
||||
$currentTime = time();
|
||||
|
||||
$certValid = ($currentTime >= $validFrom && $currentTime <= $validTo);
|
||||
|
||||
$sslChecks[] = [
|
||||
'name' => 'SSL Certificate is valid',
|
||||
'passed' => $certValid,
|
||||
'value' => $certValid ? 'Valid' : 'Invalid or Expired',
|
||||
];
|
||||
} else {
|
||||
$sslChecks[] = [
|
||||
'name' => 'SSL Certificate is valid',
|
||||
'passed' => false,
|
||||
'value' => 'Unable to retrieve certificate',
|
||||
];
|
||||
}
|
||||
} else {
|
||||
$sslChecks[] = [
|
||||
'name' => 'SSL Certificate is valid',
|
||||
'passed' => false,
|
||||
'value' => 'Not using HTTPS',
|
||||
];
|
||||
}
|
||||
|
||||
// Section: Domain Checks
|
||||
$domainChecks = [];
|
||||
|
||||
// Check if the site has a valid FQDN
|
||||
$fqdn = $_SERVER['HTTP_HOST'];
|
||||
$isValidFqdn = (bool) filter_var('http://' . $fqdn, FILTER_VALIDATE_URL) && preg_match('/^[a-z0-9.-]+\.[a-z]{2,}$/i', $fqdn);
|
||||
|
||||
$domainChecks[] = [
|
||||
'name' => 'Site has a valid FQDN',
|
||||
'passed' => $isValidFqdn,
|
||||
'value' => $fqdn,
|
||||
];
|
||||
|
||||
// Section: File Permissions
|
||||
$filePermissions = [];
|
||||
|
||||
// Check if web user has write access to webroot directory
|
||||
$webroot = $_SERVER['DOCUMENT_ROOT'];
|
||||
$writable = is_writable($webroot);
|
||||
$filePermissions[] = [
|
||||
'name' => 'Web user has write access to webroot directory',
|
||||
'passed' => $writable,
|
||||
'value' => $webroot,
|
||||
];
|
||||
|
||||
// Section: Uploads Directory Stats
|
||||
$uploadsStats = [];
|
||||
|
||||
// Define the uploads directory path
|
||||
$uploadsDir = __DIR__ . '/../uploads'; // Adjust the path if needed
|
||||
|
||||
if (is_dir($uploadsDir)) {
|
||||
// Function to recursively count files and calculate total size
|
||||
function getDirStats($dir) {
|
||||
$files = 0;
|
||||
$size = 0;
|
||||
|
||||
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir));
|
||||
foreach ($iterator as $file) {
|
||||
if ($file->isFile()) {
|
||||
$files++;
|
||||
$size += $file->getSize();
|
||||
}
|
||||
}
|
||||
return ['files' => $files, 'size' => $size];
|
||||
}
|
||||
|
||||
$stats = getDirStats($uploadsDir);
|
||||
$sizeInMB = round($stats['size'] / (1024 * 1024), 2);
|
||||
|
||||
$uploadsStats[] = [
|
||||
'name' => 'Number of files in uploads directory',
|
||||
'value' => $stats['files'],
|
||||
];
|
||||
|
||||
$uploadsStats[] = [
|
||||
'name' => 'Total size of uploads directory (MB)',
|
||||
'value' => $sizeInMB . ' MB',
|
||||
];
|
||||
} else {
|
||||
$uploadsStats[] = [
|
||||
'name' => 'Uploads directory exists',
|
||||
'value' => 'Directory not found',
|
||||
];
|
||||
}
|
||||
|
||||
// Section: Database Stats
|
||||
$databaseStats = [];
|
||||
|
||||
// Get list of tables
|
||||
$tablesResult = $mysqli->query("SHOW TABLE STATUS");
|
||||
if ($tablesResult) {
|
||||
$totalTables = 0;
|
||||
$totalFields = 0;
|
||||
$totalRows = 0;
|
||||
$totalSize = 0;
|
||||
$tableDetails = [];
|
||||
|
||||
while ($table = $tablesResult->fetch_assoc()) {
|
||||
$tableName = $table['Name'];
|
||||
|
||||
// Accurate row count
|
||||
$countResult = $mysqli->query("SELECT COUNT(*) AS cnt FROM `$tableName`");
|
||||
$countRow = $countResult->fetch_assoc();
|
||||
$tableRows = $countRow['cnt'];
|
||||
$countResult->free();
|
||||
|
||||
$dataLength = $table['Data_length'];
|
||||
$indexLength = $table['Index_length'];
|
||||
$tableSize = ($dataLength + $indexLength) / (1024 * 1024); // Size in MB
|
||||
|
||||
// Get number of fields
|
||||
$fieldsResult = $mysqli->query("SHOW COLUMNS FROM `$tableName`");
|
||||
$numFields = $fieldsResult->num_rows;
|
||||
$fieldsResult->free();
|
||||
|
||||
$totalTables++;
|
||||
$totalFields += $numFields;
|
||||
$totalRows += $tableRows;
|
||||
$totalSize += $tableSize;
|
||||
|
||||
$tableDetails[] = [
|
||||
'name' => $tableName,
|
||||
'fields' => $numFields,
|
||||
'rows' => $tableRows,
|
||||
'size' => round($tableSize, 2),
|
||||
];
|
||||
}
|
||||
$tablesResult->free();
|
||||
|
||||
$databaseStats[] = [
|
||||
'name' => 'Total number of tables',
|
||||
'value' => $totalTables,
|
||||
];
|
||||
$databaseStats[] = [
|
||||
'name' => 'Total number of fields',
|
||||
'value' => $totalFields,
|
||||
];
|
||||
$databaseStats[] = [
|
||||
'name' => 'Total number of rows',
|
||||
'value' => $totalRows,
|
||||
];
|
||||
$databaseStats[] = [
|
||||
'name' => 'Total database size (MB)',
|
||||
'value' => round($totalSize, 2) . ' MB',
|
||||
];
|
||||
}
|
||||
|
||||
// Section: Database Structure Comparison
|
||||
$dbComparison = [];
|
||||
|
||||
// Path to the db.sql file
|
||||
$dbSqlFile = __DIR__ . '/../db.sql';
|
||||
|
||||
if (file_exists($dbSqlFile)) {
|
||||
// Read the db.sql file
|
||||
$sqlContent = file_get_contents($dbSqlFile);
|
||||
|
||||
// Remove comments and empty lines
|
||||
$lines = explode("\n", $sqlContent);
|
||||
$sqlStatements = [];
|
||||
$statement = '';
|
||||
|
||||
foreach ($lines as $line) {
|
||||
// Remove single-line comments
|
||||
$line = preg_replace('/--.*$/', '', $line);
|
||||
$line = preg_replace('/\/\*.*?\*\//', '', $line);
|
||||
|
||||
// Skip empty lines
|
||||
if (trim($line) == '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Append line to the current statement
|
||||
$statement .= $line . "\n";
|
||||
|
||||
// Check if the statement ends with a semicolon
|
||||
if (preg_match('/;\s*$/', $line)) {
|
||||
$sqlStatements[] = $statement;
|
||||
$statement = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Parse the CREATE TABLE statements
|
||||
$sqlTables = [];
|
||||
|
||||
foreach ($sqlStatements as $sql) {
|
||||
if (preg_match('/CREATE TABLE\s+`?([^` ]+)`?\s*\((.*)\)(.*?);/msi', $sql, $match)) {
|
||||
$tableName = $match[1];
|
||||
$columnsDefinition = $match[2];
|
||||
|
||||
// Extract column names and data types
|
||||
$columns = [];
|
||||
$columnLines = explode("\n", $columnsDefinition);
|
||||
foreach ($columnLines as $line) {
|
||||
$line = trim($line);
|
||||
// Skip empty lines and lines that do not define columns
|
||||
if ($line == '' || strpos($line, 'PRIMARY KEY') !== false || strpos($line, 'UNIQUE KEY') !== false || strpos($line, 'KEY') === 0 || strpos($line, 'CONSTRAINT') === 0 || strpos($line, ')') === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Remove trailing comma if present
|
||||
$line = rtrim($line, ',');
|
||||
|
||||
// Match column definition
|
||||
if (preg_match('/^`([^`]+)`\s+(.+)/', $line, $colMatch)) {
|
||||
$colName = $colMatch[1];
|
||||
$colDefinition = $colMatch[2];
|
||||
|
||||
// Extract the data type from the column definition
|
||||
$tokens = preg_split('/\s+/', $colDefinition);
|
||||
$colType = $tokens[0];
|
||||
|
||||
// Handle data types with parentheses (e.g., varchar(255), decimal(15,2))
|
||||
if (preg_match('/^([a-zA-Z]+)\(([^)]+)\)/', $colType, $typeMatch)) {
|
||||
$colType = $typeMatch[1] . '(' . $typeMatch[2] . ')';
|
||||
}
|
||||
|
||||
$columns[$colName] = $colType;
|
||||
}
|
||||
}
|
||||
$sqlTables[$tableName] = $columns;
|
||||
}
|
||||
}
|
||||
|
||||
// Get current database table structures
|
||||
$dbTables = [];
|
||||
$tablesResult = $mysqli->query("SHOW TABLES");
|
||||
while ($row = $tablesResult->fetch_row()) {
|
||||
$tableName = $row[0];
|
||||
$columnsResult = $mysqli->query("SHOW COLUMNS FROM `$tableName`");
|
||||
$columns = [];
|
||||
while ($col = $columnsResult->fetch_assoc()) {
|
||||
$columns[$col['Field']] = $col['Type'];
|
||||
}
|
||||
$columnsResult->free();
|
||||
$dbTables[$tableName] = $columns;
|
||||
}
|
||||
$tablesResult->free();
|
||||
|
||||
// Compare the structures
|
||||
foreach ($sqlTables as $tableName => $sqlColumns) {
|
||||
if (!isset($dbTables[$tableName])) {
|
||||
$dbComparison[] = [
|
||||
'name' => "Table `$tableName` missing in database",
|
||||
'status' => 'Missing Table',
|
||||
];
|
||||
continue;
|
||||
}
|
||||
|
||||
// Compare columns
|
||||
$dbColumns = $dbTables[$tableName];
|
||||
foreach ($sqlColumns as $colName => $colType) {
|
||||
if (!isset($dbColumns[$colName])) {
|
||||
$dbComparison[] = [
|
||||
'name' => "Column `$colName` missing in table `$tableName`",
|
||||
'status' => 'Missing Column',
|
||||
];
|
||||
} else {
|
||||
// Normalize data types for comparison
|
||||
$sqlColType = strtolower($colType);
|
||||
$dbColType = strtolower($dbColumns[$colName]);
|
||||
|
||||
// Remove attributes and constraints
|
||||
$sqlColType = preg_replace('/\s+.*$/', '', $sqlColType);
|
||||
$dbColType = preg_replace('/\s+.*$/', '', $dbColType);
|
||||
|
||||
// Remove additional attributes like unsigned, zerofill, etc.
|
||||
$sqlColType = preg_replace('/\s+unsigned|\s+zerofill|\s+binary/', '', $sqlColType);
|
||||
$dbColType = preg_replace('/\s+unsigned|\s+zerofill|\s+binary/', '', $dbColType);
|
||||
|
||||
if ($sqlColType != $dbColType) {
|
||||
$dbComparison[] = [
|
||||
'name' => "Data type mismatch for `$colName` in table `$tableName`",
|
||||
'status' => "Expected: $colType, Found: {$dbColumns[$colName]}",
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for extra columns in the database that are not in the SQL file
|
||||
foreach ($dbColumns as $colName => $colType) {
|
||||
if (!isset($sqlColumns[$colName])) {
|
||||
$dbComparison[] = [
|
||||
'name' => "Extra column `$colName` in table `$tableName` not present in db.sql",
|
||||
'status' => 'Extra Column',
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for tables in the database not present in the db.sql file
|
||||
foreach ($dbTables as $tableName => $dbColumns) {
|
||||
if (!isset($sqlTables[$tableName])) {
|
||||
$dbComparison[] = [
|
||||
'name' => "Extra table `$tableName` in database not present in db.sql",
|
||||
'status' => 'Extra Table',
|
||||
];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$dbComparison[] = [
|
||||
'name' => 'db.sql file not found',
|
||||
'status' => 'File Missing',
|
||||
];
|
||||
}
|
||||
|
||||
$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>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
|
||||
<h2>Debugging</h2>
|
||||
<ul>
|
||||
<li>If you are experiencing a problem with ITFlow, this page should help you identify any configuration issues.</li>
|
||||
<li>Note: You might also need to gather <a href="https://docs.itflow.org/gathering_logs#error_logs">error logs</a></li>
|
||||
</ul>
|
||||
<hr>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered mb-3">
|
||||
<tr>
|
||||
<th>ITFlow release version</th>
|
||||
<th><?= APP_VERSION ?></th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Current DB Version</td>
|
||||
<td><?= CURRENT_DATABASE_VERSION ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Current Code Commit</td>
|
||||
<td><?= $commitHash ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Current Branch</td>
|
||||
<td><?= $gitBranch ?></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- System Information Table -->
|
||||
<h3>System Information</h3>
|
||||
<table class="table table-sm table-bordered">
|
||||
<tbody>
|
||||
<?php foreach ($systemInfo as $info): ?>
|
||||
<tr>
|
||||
<td><?= htmlspecialchars($info['name']); ?></td>
|
||||
<td><?= htmlspecialchars($info['value']); ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- PHP Extensions and Configuration Table -->
|
||||
<h3 class="mt-3">PHP Extensions and Configuration</h3>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-bordered">
|
||||
<!-- PHP Extensions Section -->
|
||||
<thead>
|
||||
<tr class="table-secondary">
|
||||
<th colspan="3">PHP Extensions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($phpExtensions as $check): ?>
|
||||
<tr>
|
||||
<td><?= htmlspecialchars($check['name']); ?></td>
|
||||
<td class="text-center">
|
||||
<?php if ($check['passed']): ?>
|
||||
<i class="fas fa-check" style="color:green"></i>
|
||||
<?php else: ?>
|
||||
<i class="fas fa-times" style="color:red"></i>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?= htmlspecialchars($check['value']); ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
<!-- PHP Configuration Section -->
|
||||
<thead>
|
||||
<tr class="table-secondary">
|
||||
<th colspan="3">PHP Configuration</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($phpConfig as $check): ?>
|
||||
<tr>
|
||||
<td><?= htmlspecialchars($check['name']); ?></td>
|
||||
<td class="text-center">
|
||||
<?php if ($check['passed']): ?>
|
||||
<i class="fas fa-check" style="color:green"></i>
|
||||
<?php else: ?>
|
||||
<i class="fas fa-times" style="color:red"></i>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?= htmlspecialchars($check['value']); ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
<thead>
|
||||
<tr class="table-secondary">
|
||||
<th colspan="3">Shell Commands</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($shellCommands as $check): ?>
|
||||
<tr>
|
||||
<td><?= htmlspecialchars($check['name']); ?></td>
|
||||
<td class="text-center">
|
||||
<?php if ($check['passed']): ?>
|
||||
<i class="fas fa-check" style="color:green"></i>
|
||||
<?php else: ?>
|
||||
<i class="fas fa-times" style="color:red"></i>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?= htmlspecialchars($check['value']); ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
<thead>
|
||||
<tr class="table-secondary">
|
||||
<th colspan="3">SSL Checks</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($sslChecks as $check): ?>
|
||||
<tr>
|
||||
<td><?= htmlspecialchars($check['name']); ?></td>
|
||||
<td class="text-center">
|
||||
<?php if ($check['passed']): ?>
|
||||
<i class="fas fa-check" style="color:green"></i>
|
||||
<?php else: ?>
|
||||
<i class="fas fa-times" style="color:red"></i>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?= htmlspecialchars($check['value']); ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
<thead>
|
||||
<tr class="table-secondary">
|
||||
<th colspan="3">Domain Checks</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($domainChecks as $check): ?>
|
||||
<tr>
|
||||
<td><?= htmlspecialchars($check['name']); ?></td>
|
||||
<td class="text-center">
|
||||
<?php if ($check['passed']): ?>
|
||||
<i class="fas fa-check" style="color:green"></i>
|
||||
<?php else: ?>
|
||||
<i class="fas fa-times" style="color:red"></i>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?= htmlspecialchars($check['value']); ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
|
||||
<!-- File Permissions Table -->
|
||||
<thead>
|
||||
<tr class="table-secondary">
|
||||
<th colspan="3">File Permissions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($filePermissions as $check): ?>
|
||||
<tr>
|
||||
<td><?= htmlspecialchars($check['name']); ?></td>
|
||||
<td class="text-center">
|
||||
<?php if ($check['passed']): ?>
|
||||
<i class="fas fa-check" style="color:green"></i>
|
||||
<?php else: ?>
|
||||
<i class="fas fa-times" style="color:red"></i>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?= htmlspecialchars($check['value']); ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Database Structure Comparison Table -->
|
||||
<h3 class="mt-3">Database Structure Comparison</h3>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-bordered">
|
||||
<tbody>
|
||||
<?php if (!empty($dbComparison)): ?>
|
||||
<?php foreach ($dbComparison as $issue): ?>
|
||||
<tr>
|
||||
<td><?= htmlspecialchars($issue['name']); ?></td>
|
||||
<td colspan="2"><?= htmlspecialchars($issue['status']); ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<tr>
|
||||
<td colspan="3">No discrepancies found between the database and db.sql file.</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Uploads Directory Stats Table -->
|
||||
<h3 class="mt-3">Uploads Directory Stats</h3>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-bordered">
|
||||
<tbody>
|
||||
<?php foreach ($uploadsStats as $stat): ?>
|
||||
<tr>
|
||||
<td><?= htmlspecialchars($stat['name']); ?></td>
|
||||
<td colspan="2"><?= htmlspecialchars($stat['value']); ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Database Stats Table -->
|
||||
<h3 class="mt-3">Database Stats</h3>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-bordered">
|
||||
<tbody>
|
||||
<?php foreach ($databaseStats as $stat): ?>
|
||||
<tr>
|
||||
<td><?= htmlspecialchars($stat['name']); ?></td>
|
||||
<td colspan="2"><?= htmlspecialchars($stat['value']); ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Table Stats Table -->
|
||||
<h3 class="mt-3">Table Stats</h3>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Table Name</th>
|
||||
<th>Fields / Rows</th>
|
||||
<th>Size (MB)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($tableDetails as $table): ?>
|
||||
<tr>
|
||||
<td><?= htmlspecialchars($table['name']); ?></td>
|
||||
<td><?= htmlspecialchars("Fields: {$table['fields']}, Rows: {$table['rows']}"); ?></td>
|
||||
<td><?= htmlspecialchars($table['size'] . ' MB'); ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<?php
|
||||
|
||||
require_once "../includes/footer.php";
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user