diff --git a/CHANGELOG.md b/CHANGELOG.md index 426ff3f7c..e4b9d3283 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,37 @@ This file documents all notable changes made to ITFlow. +## [26.08.1] Maint Release + +### Upgrading to 26.08.1 + +Update the files from Settings > Update as normal. This release moves the database to 2.6.7 and the web updater completes it for you — the command line step that 26.08 required is not needed again. + +### Breaking Changes and Notes + +- Client access: agents with restricted client access now see records that have no client assigned. Previously this varied by page — unassigned tickets and projects were visible, unassigned expenses and credentials were not. It is now consistent everywhere. + +### Bug Fixes +- Setup: fixed the wizard closing itself after the first user, which left new 26.08 installs stuck in a redirect loop between `/setup` and `/login.php`. +- API: tightened client scoping on the expense read and record update endpoints. +- Income: revenue rows now respect restricted client access. +- Client PDF Export: fixed the export producing a CSV file, and each section is now gated on the module that owns it. +- AI: fixed model creation, per-use-case model selection, configurable temperature, and error reporting. +- Ticket: system-generated replies no longer record time worked that was never worked. +- Ticket: fixed an error when scheduling a ticket. +- Ticket: cancelling a schedule now cancels the calendar event on the recipient's calendar. +- Ticket: history no longer records a status change when the status did not change. +- Recurring Ticket: bulk priority changes no longer deny access to agents who are not administrators. +- Contact: deleting a contact now removes the linked portal user, and anonymizing now redacts the phone number. +- Calendar: fixed event deletion. + +### New Features & Updates +- Performance: queries now select only the columns they use instead of `SELECT *`, cutting memory use and query time across the app and especially in the crons. +- Performance: removed client joins that were only there for scoping — side nav badge counts are significantly faster. +- Client scoping: added a `clientScopeSql()` helper so list queries scope on the owning column instead of a joined `clients.client_id`. +- Contributing: documented the column-selection and client-scoping conventions. + + ## [26.08] ### Upgrading to 26.08 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3e076677d..7ebf94f02 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -74,6 +74,10 @@ if (isset($_POST['edit_ticket_priority'])) { ### 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. --- @@ -171,7 +175,25 @@ Everywhere else — anything under `agent/post/` — the call belongs in the blo ### 4. Client scoping is enforced, not assumed. -After loading a record, call `enforceClientAccess()` (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 — including the "skip if the record has no client" case. +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. @@ -206,8 +228,29 @@ Per [SECURITY.md](SECURITY.md) — never in a public issue. --- ## Conventions - -**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 and is why queries can `SELECT *` across joins safely. New tables must follow it. + +**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. @@ -224,6 +267,8 @@ A single update run applies every pending migration in order, stopping at the fi **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. diff --git a/admin/ai_models.php b/admin/ai_models.php index e30dc6db3..db8f96d81 100644 --- a/admin/ai_models.php +++ b/admin/ai_models.php @@ -6,7 +6,8 @@ $order = "ASC"; require_once "includes/inc_all_admin.php"; -$sql = mysqli_query($mysqli, "SELECT * FROM ai_models LEFT JOIN ai_providers ON ai_model_ai_provider_id = ai_provider_id ORDER BY $sort $order"); +$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); diff --git a/admin/ai_providers.php b/admin/ai_providers.php index 3bb6b5729..b9b728314 100644 --- a/admin/ai_providers.php +++ b/admin/ai_providers.php @@ -6,7 +6,7 @@ $order = "ASC"; require_once "includes/inc_all_admin.php"; -$sql = mysqli_query($mysqli, "SELECT * FROM ai_providers ORDER BY $sort $order"); +$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); diff --git a/admin/api_keys.php b/admin/api_keys.php index 1d6173b81..c0ed6cae9 100644 --- a/admin/api_keys.php +++ b/admin/api_keys.php @@ -8,7 +8,7 @@ require_once "includes/inc_all_admin.php"; $sql = mysqli_query( $mysqli, - "SELECT SQL_CALC_FOUND_ROWS * FROM api_keys + "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" diff --git a/admin/app_logs.php b/admin/app_logs.php index cf95b9478..2a1f7dce6 100644 --- a/admin/app_logs.php +++ b/admin/app_logs.php @@ -28,7 +28,7 @@ if (isset($_GET['category']) & !empty($_GET['catergory'])) { $sql = mysqli_query( $mysqli, - "SELECT SQL_CALC_FOUND_ROWS * FROM app_logs + "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 diff --git a/admin/audit_logs.php b/admin/audit_logs.php index fd91cd8f6..0ce0d9709 100644 --- a/admin/audit_logs.php +++ b/admin/audit_logs.php @@ -48,7 +48,8 @@ if (isset($_GET['action']) & !empty($_GET['action'])) { $sql = mysqli_query( $mysqli, - "SELECT SQL_CALC_FOUND_ROWS * FROM logs + "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%') @@ -87,7 +88,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()")); - All Users -

Nothing to see here

Go Back"; diff --git a/admin/document_templates.php b/admin/document_templates.php index 74692ca63..a09cba04a 100644 --- a/admin/document_templates.php +++ b/admin/document_templates.php @@ -8,7 +8,8 @@ $sql = mysqli_query( $mysqli, - "SELECT SQL_CALC_FOUND_ROWS * FROM document_templates + "SELECT SQL_CALC_FOUND_ROWS document_template_content, document_template_created_at, document_template_description, + document_template_id, document_template_name, document_template_updated_at, user_name FROM document_templates LEFT JOIN users ON document_template_created_by = user_id WHERE user_name LIKE '%$q%' OR document_template_name LIKE '%$q%' ORDER BY $sort $order LIMIT $record_from, $record_to" diff --git a/admin/includes/side_nav.php b/admin/includes/side_nav.php index fe2ba5465..48636100a 100644 --- a/admin/includes/side_nav.php +++ b/admin/includes/side_nav.php @@ -314,7 +314,7 @@ +
+ +
+
+ +
+ +
+ Optional. Leave blank to let the provider use its default - some newer models reject every other value. +
+
diff --git a/admin/modals/ai/ai_model_edit.php b/admin/modals/ai/ai_model_edit.php index d624ea62a..05413b98a 100644 --- a/admin/modals/ai/ai_model_edit.php +++ b/admin/modals/ai/ai_model_edit.php @@ -4,13 +4,14 @@ require_once '../../includes/modal_header.php'; $model_id = intval($_GET['id']); -$sql = mysqli_query($mysqli, "SELECT * FROM ai_models WHERE ai_model_id = $model_id LIMIT 1"); +$sql = mysqli_query($mysqli, "SELECT ai_model_ai_provider_id, ai_model_id, ai_model_name, ai_model_prompt, ai_model_use_case, ai_model_temperature FROM ai_models WHERE ai_model_id = $model_id LIMIT 1"); $row = mysqli_fetch_assoc($sql); $ai_model_ai_provider_id = intval($row['ai_model_ai_provider_id']); $model_id = intval($row['ai_model_id']); $model_name = escapeHtml($row['ai_model_name']); $use_case = escapeHtml($row['ai_model_use_case']); +$temperature = escapeHtml($row['ai_model_temperature']); $prompt = escapeHtml($row['ai_model_prompt']); // Generate the HTML form content using output buffering. @@ -38,7 +39,7 @@ ob_start(); + + Optional. Leave blank to let the provider use its default - some newer models reject every other value. +
diff --git a/admin/modals/ai/ai_provider_edit.php b/admin/modals/ai/ai_provider_edit.php index 9973be998..933834477 100644 --- a/admin/modals/ai/ai_provider_edit.php +++ b/admin/modals/ai/ai_provider_edit.php @@ -4,7 +4,7 @@ require_once '../../includes/modal_header.php'; $provider_id = intval($_GET['id']); -$sql = mysqli_query($mysqli, "SELECT * FROM ai_providers WHERE ai_provider_id = $provider_id LIMIT 1"); +$sql = mysqli_query($mysqli, "SELECT ai_provider_api_key, ai_provider_api_url, ai_provider_name FROM ai_providers WHERE ai_provider_id = $provider_id LIMIT 1"); $row = mysqli_fetch_assoc($sql); $provider_name = escapeHtml($row['ai_provider_name']); diff --git a/admin/modals/api/api_key_edit.php b/admin/modals/api/api_key_edit.php index 7429342dc..4b773aa08 100644 --- a/admin/modals/api/api_key_edit.php +++ b/admin/modals/api/api_key_edit.php @@ -4,7 +4,7 @@ require_once '../../includes/modal_header.php'; $api_key_id = intval($_GET['id']); -$sql = mysqli_query($mysqli, "SELECT * FROM api_keys WHERE api_key_id = $api_key_id LIMIT 1"); +$sql = mysqli_query($mysqli, "SELECT api_key_expire, api_key_name, api_key_user_id FROM api_keys WHERE api_key_id = $api_key_id LIMIT 1"); $row = mysqli_fetch_assoc($sql); $api_key_name = escapeHtml($row['api_key_name']); $api_key_expire = escapeHtml($row['api_key_expire']); diff --git a/admin/modals/category/category_edit.php b/admin/modals/category/category_edit.php index 23c053650..f9141baa6 100644 --- a/admin/modals/category/category_edit.php +++ b/admin/modals/category/category_edit.php @@ -4,7 +4,7 @@ require_once '../../includes/modal_header.php'; $category_id = intval($_GET['id']); -$sql = mysqli_query($mysqli, "SELECT * FROM categories WHERE category_id = $category_id LIMIT 1"); +$sql = mysqli_query($mysqli, "SELECT category_color, category_description, category_name, category_type FROM categories WHERE category_id = $category_id LIMIT 1"); $row = mysqli_fetch_assoc($sql); $category_name = escapeHtml($row['category_name']); diff --git a/admin/modals/contract_template/contract_template_edit.php b/admin/modals/contract_template/contract_template_edit.php index 2eda674fe..302db896a 100644 --- a/admin/modals/contract_template/contract_template_edit.php +++ b/admin/modals/contract_template/contract_template_edit.php @@ -7,7 +7,13 @@ $contract_types_array = ['Fully Managed', 'Partialy Managed', 'Break/Fix']; $update_frequency_array = ['Manual', 'Annually', '2 Year', '3 Year', '5 Year', '7 Year']; // Fetch existing template -$sql = mysqli_query($mysqli, "SELECT * FROM contract_templates WHERE contract_template_id = $contract_template_id LIMIT 1"); +$sql = mysqli_query($mysqli, "SELECT contract_template_description, contract_template_details, contract_template_name, + contract_template_net_terms, contract_template_rate_after_hours, + contract_template_rate_standard, contract_template_renewal_frequency, + contract_template_sla_high_resolution_time, contract_template_sla_high_response_time, + contract_template_sla_low_resolution_time, contract_template_sla_low_response_time, + contract_template_sla_medium_resolution_time, contract_template_sla_medium_response_time, + contract_template_support_hours, contract_template_type FROM contract_templates WHERE contract_template_id = $contract_template_id LIMIT 1"); $row = mysqli_fetch_assoc($sql); // Assign locals diff --git a/admin/modals/cron/cron_edit.php b/admin/modals/cron/cron_edit.php index d2cc429b0..d0e817a4b 100644 --- a/admin/modals/cron/cron_edit.php +++ b/admin/modals/cron/cron_edit.php @@ -5,7 +5,8 @@ require_once '../../../includes/cron_jobs.php'; $cron_job_id = intval($_GET['id']); -$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT * FROM cron_jobs WHERE cron_job_id = $cron_job_id LIMIT 1")); +$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT cron_job_daily_at, cron_job_enabled, cron_job_interval_minutes, cron_job_name, + cron_job_schedule FROM cron_jobs WHERE cron_job_id = $cron_job_id LIMIT 1")); $registry = cronJobRegistryByName(); $job = $registry[$row['cron_job_name']] ?? null; diff --git a/admin/modals/custom_link/custom_link_edit.php b/admin/modals/custom_link/custom_link_edit.php index 395a8c919..eb5fb035d 100644 --- a/admin/modals/custom_link/custom_link_edit.php +++ b/admin/modals/custom_link/custom_link_edit.php @@ -4,7 +4,8 @@ require_once '../../includes/modal_header.php'; $custom_link_id = intval($_GET['id']); -$sql = mysqli_query($mysqli, "SELECT * FROM custom_links WHERE custom_link_id = $custom_link_id LIMIT 1"); +$sql = mysqli_query($mysqli, "SELECT custom_link_icon, custom_link_location, custom_link_name, custom_link_new_tab, + custom_link_order, custom_link_uri FROM custom_links WHERE custom_link_id = $custom_link_id LIMIT 1"); $row = mysqli_fetch_assoc($sql); $custom_link_name = escapeHtml($row['custom_link_name']); $custom_link_uri = escapeHtml($row['custom_link_uri']); diff --git a/admin/modals/document_template/document_template_edit.php b/admin/modals/document_template/document_template_edit.php index 18ef24d04..387f547f2 100644 --- a/admin/modals/document_template/document_template_edit.php +++ b/admin/modals/document_template/document_template_edit.php @@ -4,7 +4,7 @@ require_once '../../includes/modal_header.php'; $document_template_id = intval($_GET['id']); -$sql = mysqli_query($mysqli, "SELECT * FROM document_templates WHERE document_template_id = $document_template_id LIMIT 1"); +$sql = mysqli_query($mysqli, "SELECT document_template_content, document_template_description, document_template_name FROM document_templates WHERE document_template_id = $document_template_id LIMIT 1"); $row = mysqli_fetch_assoc($sql); $document_template_name = escapeHtml($row['document_template_name']); $document_template_description = escapeHtml($row['document_template_description']); diff --git a/admin/modals/mail_queue/mail_queue_message_view.php b/admin/modals/mail_queue/mail_queue_message_view.php index 141555d10..025cbdf62 100644 --- a/admin/modals/mail_queue/mail_queue_message_view.php +++ b/admin/modals/mail_queue/mail_queue_message_view.php @@ -12,7 +12,9 @@ $purifier_config->set('Cache.DefinitionImpl', null); // Disable cache by setting $purifier_config->set('URI.AllowedSchemes', ['data' => true, 'src' => true, 'http' => true, 'https' => true]); $purifier = new HTMLPurifier($purifier_config); -$sql = mysqli_query($mysqli, "SELECT * FROM email_queue WHERE email_id = $email_id LIMIT 1"); +$sql = mysqli_query($mysqli, "SELECT email_attempts, email_content, email_failed_at, email_from, email_from_name, + email_queued_at, email_recipient, email_recipient_name, email_sent_at, email_status, + email_subject FROM email_queue WHERE email_id = $email_id LIMIT 1"); $row = mysqli_fetch_assoc($sql); $email_from = escapeHtml($row['email_from']); diff --git a/admin/modals/payment_method/payment_method_edit.php b/admin/modals/payment_method/payment_method_edit.php index 1c0415f74..23c0f5e42 100644 --- a/admin/modals/payment_method/payment_method_edit.php +++ b/admin/modals/payment_method/payment_method_edit.php @@ -4,7 +4,7 @@ require_once '../../includes/modal_header.php'; $payment_method_id = intval($_GET['id']); -$sql = mysqli_query($mysqli, "SELECT * FROM payment_methods WHERE payment_method_id = $payment_method_id LIMIT 1"); +$sql = mysqli_query($mysqli, "SELECT payment_method_description, payment_method_id, payment_method_name FROM payment_methods WHERE payment_method_id = $payment_method_id LIMIT 1"); $row = mysqli_fetch_assoc($sql); $payment_method_id = intval($row['payment_method_id']); diff --git a/admin/modals/payment_provider/payment_provider_edit.php b/admin/modals/payment_provider/payment_provider_edit.php index fb188b3f2..0e9aa0bd9 100644 --- a/admin/modals/payment_provider/payment_provider_edit.php +++ b/admin/modals/payment_provider/payment_provider_edit.php @@ -4,7 +4,9 @@ require_once '../../includes/modal_header.php'; $provider_id = intval($_GET['id']); -$sql = mysqli_query($mysqli, "SELECT * FROM payment_providers WHERE payment_provider_id = $provider_id LIMIT 1"); +$sql = mysqli_query($mysqli, "SELECT payment_provider_account, payment_provider_expense_category, + payment_provider_expense_vendor, payment_provider_name, payment_provider_private_key, + payment_provider_public_key, payment_provider_threshold FROM payment_providers WHERE payment_provider_id = $provider_id LIMIT 1"); $row = mysqli_fetch_assoc($sql); $provider_name = escapeHtml($row['payment_provider_name']); diff --git a/admin/modals/project_template/project_template_edit.php b/admin/modals/project_template/project_template_edit.php index a9f48f3a8..ac77eed8a 100644 --- a/admin/modals/project_template/project_template_edit.php +++ b/admin/modals/project_template/project_template_edit.php @@ -4,7 +4,7 @@ require_once '../../includes/modal_header.php'; $project_template_id = intval($_GET['project_template_id']); -$sql = mysqli_query($mysqli, "SELECT * FROM project_templates WHERE project_template_id = $project_template_id LIMIT 1"); +$sql = mysqli_query($mysqli, "SELECT project_template_description, project_template_name FROM project_templates WHERE project_template_id = $project_template_id LIMIT 1"); $row = mysqli_fetch_assoc($sql); $project_template_name = escapeHtml($row['project_template_name']); $project_template_description = escapeHtml($row['project_template_description']); diff --git a/admin/modals/role/role_add.php b/admin/modals/role/role_add.php index f22e2a942..abc8f6061 100644 --- a/admin/modals/role/role_add.php +++ b/admin/modals/role/role_add.php @@ -79,7 +79,7 @@ ob_start(); - No - diff --git a/admin/modals/ticket_template/ticket_template_task_edit.php b/admin/modals/ticket_template/ticket_template_task_edit.php index 24e1874fe..467e217f0 100644 --- a/admin/modals/ticket_template/ticket_template_task_edit.php +++ b/admin/modals/ticket_template/ticket_template_task_edit.php @@ -4,7 +4,7 @@ require_once '../../includes/modal_header.php'; $task_template_id = intval($_GET['id']); -$sql = mysqli_query($mysqli, "SELECT * FROM task_templates WHERE task_template_id = $task_template_id LIMIT 1"); +$sql = mysqli_query($mysqli, "SELECT task_template_completion_estimate, task_template_name, task_template_order FROM task_templates WHERE task_template_id = $task_template_id LIMIT 1"); $row = mysqli_fetch_assoc($sql); $task_template_name = escapeHtml($row['task_template_name']); diff --git a/admin/modals/user/user_add.php b/admin/modals/user/user_add.php index e89fe85eb..211f76aa1 100644 --- a/admin/modals/user/user_add.php +++ b/admin/modals/user/user_add.php @@ -75,7 +75,7 @@ ob_start(); - None - diff --git a/agent/modals/asset/asset_bulk_assign_contact.php b/agent/modals/asset/asset_bulk_assign_contact.php index 6d0489b76..ab301a310 100644 --- a/agent/modals/asset/asset_bulk_assign_contact.php +++ b/agent/modals/asset/asset_bulk_assign_contact.php @@ -37,7 +37,7 @@ ob_start(); - Select Location - - Select Contact - - Select Network - - Select Vendor - - Select Location - - Select Contact - - Select Network - - Select Vendor - - Calendar - - Client - - Client - diff --git a/agent/modals/calendar/calendar_share.php b/agent/modals/calendar/calendar_share.php index a2aa25abf..4a12ae21f 100644 --- a/agent/modals/calendar/calendar_share.php +++ b/agent/modals/calendar/calendar_share.php @@ -4,7 +4,8 @@ require_once '../../../includes/modal_header.php'; $calendar_id = intval($_GET['id']); -$sql = mysqli_query($mysqli, "SELECT * FROM calendars WHERE calendar_id = $calendar_id LIMIT 1"); +$sql = mysqli_query($mysqli, "SELECT calendar_color, calendar_feed_accessed_at, calendar_feed_busy_only, + calendar_feed_created_at, calendar_feed_key, calendar_name FROM calendars WHERE calendar_id = $calendar_id LIMIT 1"); $row = mysqli_fetch_assoc($sql); $calendar_name = escapeHtml($row['calendar_name']); diff --git a/agent/modals/certificate/certificate_add.php b/agent/modals/certificate/certificate_add.php index 6fb80d1dd..4b8c9dedc 100644 --- a/agent/modals/certificate/certificate_add.php +++ b/agent/modals/certificate/certificate_add.php @@ -50,7 +50,7 @@ ob_start(); @@ -93,7 +93,7 @@ ob_start(); diff --git a/agent/modals/client/client_download_pdf.php b/agent/modals/client/client_download_pdf.php index 312918f48..81a6aebc2 100644 --- a/agent/modals/client/client_download_pdf.php +++ b/agent/modals/client/client_download_pdf.php @@ -12,168 +12,7 @@ '$transfer_created_at' OR account_archived_at IS NULL) ORDER BY account_archived_at ASC, account_name ASC"); + $sql_accounts = mysqli_query($mysqli, "SELECT account_archived_at, account_id, account_name, opening_balance FROM accounts WHERE (account_archived_at > '$transfer_created_at' OR account_archived_at IS NULL) ORDER BY account_archived_at ASC, account_name ASC"); while ($row = mysqli_fetch_assoc($sql_accounts)) { $account_id_select = intval($row['account_id']); $account_name_select = escapeHtml($row['account_name']); @@ -119,7 +119,7 @@ ob_start(); @@ -252,7 +264,8 @@ if (isset($_GET['recurring_invoice_id'])) { - +
diff --git a/agent/recurring_invoices.php b/agent/recurring_invoices.php index b92760468..7d455bee6 100644 --- a/agent/recurring_invoices.php +++ b/agent/recurring_invoices.php @@ -29,7 +29,13 @@ if (isset($_GET['status']) && $_GET['status'] == "inactive") { $sql = mysqli_query( $mysqli, - "SELECT SQL_CALC_FOUND_ROWS * FROM recurring_invoices + "SELECT SQL_CALC_FOUND_ROWS category_id, category_name, client_currency_code, client_id, client_name, + recurring_invoice_amount, recurring_invoice_created_at, recurring_invoice_currency_code, + recurring_invoice_discount_amount, recurring_invoice_frequency, recurring_invoice_id, + recurring_invoice_last_sent, recurring_invoice_next_date, recurring_invoice_number, + recurring_invoice_prefix, recurring_invoice_scope, recurring_invoice_status, + recurring_payment_id, recurring_payment_recurring_invoice_id, + recurring_payment_saved_payment_id FROM recurring_invoices LEFT JOIN clients ON recurring_invoice_client_id = client_id LEFT JOIN categories ON recurring_invoice_category_id = category_id LEFT JOIN recurring_payments ON recurring_payment_recurring_invoice_id = recurring_invoice_id @@ -37,7 +43,7 @@ $sql = mysqli_query( AND DATE(recurring_invoice_created_at) BETWEEN '$dtf' AND '$dtt' $status_query $client_query - $access_permission_query + " . clientScopeSql('recurring_invoice_client_id') . " ORDER BY $sort $order LIMIT $record_from, $record_to"); @@ -218,7 +224,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()")); - 0) { ?>
diff --git a/agent/recurring_tickets.php b/agent/recurring_tickets.php index 156f1cb6a..b655527b9 100644 --- a/agent/recurring_tickets.php +++ b/agent/recurring_tickets.php @@ -62,7 +62,7 @@ $sql = mysqli_query( LEFT JOIN users ON user_id = recurring_ticket_assigned_to LEFT JOIN ticket_templates ON ticket_template_id = recurring_ticket_ticket_template_id WHERE (recurring_tickets.recurring_ticket_subject LIKE '%$q%' OR category_name LIKE '%$q%') - $access_permission_query + " . clientScopeSql('recurring_ticket_client_id') . " $category_query $assigned_agent_query $billable_query diff --git a/agent/reports/credential_rotation.php b/agent/reports/credential_rotation.php index 2579f0421..253c8afa1 100644 --- a/agent/reports/credential_rotation.php +++ b/agent/reports/credential_rotation.php @@ -16,6 +16,7 @@ $passwords_not_rotated_sql = mysqli_query($mysqli, FROM credentials LEFT JOIN clients ON credential_client_id = client_id WHERE DATE(credential_password_changed_at) < DATE_SUB(CURDATE(), INTERVAL $days DAY) + " . clientScopeSql('credential_client_id') . " ORDER BY client_name" ); diff --git a/agent/reports/expense_summary.php b/agent/reports/expense_summary.php index 698231712..0f4df0927 100644 --- a/agent/reports/expense_summary.php +++ b/agent/reports/expense_summary.php @@ -12,7 +12,7 @@ if (isset($_GET['year'])) { $sql_expense_years = mysqli_query($mysqli, "SELECT DISTINCT YEAR(expense_date) AS expense_year FROM expenses WHERE expense_category_id > 0 ORDER BY expense_year DESC"); -$sql_categories = mysqli_query($mysqli, "SELECT * FROM categories WHERE category_type = 'Expense' ORDER BY category_name ASC"); +$sql_categories = mysqli_query($mysqli, "SELECT category_id, category_name FROM categories WHERE category_type = 'Expense' ORDER BY category_name ASC"); // For chart Y-axis max $largest_expense_month = 0; diff --git a/agent/reports/includes/reports_side_nav.php b/agent/reports/includes/reports_side_nav.php index 364701163..2fe243bea 100644 --- a/agent/reports/includes/reports_side_nav.php +++ b/agent/reports/includes/reports_side_nav.php @@ -139,7 +139,7 @@ diff --git a/agent/services.php b/agent/services.php index d482af913..6061a6e0e 100644 --- a/agent/services.php +++ b/agent/services.php @@ -33,11 +33,13 @@ if (!$client_url) { // Overview SQL query $sql = mysqli_query( $mysqli, - "SELECT SQL_CALC_FOUND_ROWS * FROM services + "SELECT SQL_CALC_FOUND_ROWS client_id, client_name, service_backup, service_category, service_created_at, + service_description, service_id, service_importance, service_name, service_notes, + service_review_due, service_updated_at FROM services LEFT JOIN clients ON client_id = service_client_id WHERE (service_name LIKE '%$q%' OR service_description LIKE '%$q%' OR service_category LIKE '%$q%' OR client_name LIKE '%$q%') AND client_archived_at IS NULL - $access_permission_query + " . clientScopeSql('service_client_id') . " $client_query ORDER BY $sort $order LIMIT $record_from, $record_to" ); @@ -85,7 +87,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()")); FROM clients JOIN services ON service_client_id = client_id WHERE client_archived_at IS NULL - $access_permission_query + " . clientScopeSql('clients.client_id') . " ORDER BY client_name ASC "); while ($row = mysqli_fetch_assoc($sql_clients_filter)) { diff --git a/agent/software.php b/agent/software.php index 35c23096e..38394640d 100644 --- a/agent/software.php +++ b/agent/software.php @@ -63,12 +63,14 @@ if (!$client_url) { $sql = mysqli_query( $mysqli, - "SELECT SQL_CALC_FOUND_ROWS * FROM software + "SELECT SQL_CALC_FOUND_ROWS client_id, client_name, software_created_at, software_description, software_expire, + software_id, software_license_type, software_name, software_seats, software_type, + software_version, vendor_id, vendor_name FROM software LEFT JOIN clients ON client_id = software_client_id LEFT JOIN vendors ON vendor_id = software_vendor_id WHERE (software_name LIKE '%$q%' OR software_type LIKE '%$q%' OR software_key LIKE '%$q%' OR client_name LIKE '%$q%') AND $archive_query - $access_permission_query + " . clientScopeSql('software_client_id') . " $client_query $expire_query ORDER BY $sort $order LIMIT $record_from, $record_to"); @@ -131,7 +133,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()")); FROM clients JOIN software ON software_client_id = client_id WHERE $archive_query - $access_permission_query + " . clientScopeSql('clients.client_id') . " ORDER BY client_name ASC "); while ($row = mysqli_fetch_assoc($sql_clients_filter)) { diff --git a/agent/ticket.php b/agent/ticket.php index e683e1b6c..710b66e76 100644 --- a/agent/ticket.php +++ b/agent/ticket.php @@ -9,11 +9,8 @@ if (isset($_GET['client_id'])) { $client_url = ''; } -// Ticket client access overide - This is the only way to show tickets without a client to agents with restricted client access -$access_permission_query_overide = ''; -if ($client_access_string) { - $access_permission_query_overide = "AND ticket_client_id IN (0,$client_access_string)"; -} +// Tickets with no client stay visible to restricted agents - clientScopeSql() includes 0 +$access_permission_query_overide = clientScopeSql('ticket_client_id'); // Perms enforceUserPermission('module_support'); @@ -267,7 +264,9 @@ if (isset($_GET['ticket_id'])) { ))['user_names']); // Get ticket replies - $sql_ticket_replies = mysqli_query($mysqli, "SELECT * FROM ticket_replies + $sql_ticket_replies = mysqli_query($mysqli, "SELECT contact_name, contact_photo, ticket_reply, ticket_reply_created_at, ticket_reply_id, + ticket_reply_time_worked, ticket_reply_type, ticket_reply_updated_at, user_avatar, user_id, + user_name FROM ticket_replies LEFT JOIN users ON ticket_reply_by = user_id LEFT JOIN contacts ON ticket_reply_by = contact_id WHERE ticket_reply_ticket_id = $ticket_id @@ -296,18 +295,18 @@ if (isset($_GET['ticket_id'])) { } // Get Watchers - $sql_ticket_watchers = mysqli_query($mysqli, "SELECT * FROM ticket_watchers WHERE watcher_ticket_id = $ticket_id ORDER BY watcher_email DESC"); + $sql_ticket_watchers = mysqli_query($mysqli, "SELECT watcher_email, watcher_id FROM ticket_watchers WHERE watcher_ticket_id = $ticket_id ORDER BY watcher_email DESC"); $watcher_count = mysqli_num_rows($sql_ticket_watchers); // Get Additional Assets - $sql_additional_assets = mysqli_query($mysqli, "SELECT * FROM assets, ticket_assets + $sql_additional_assets = mysqli_query($mysqli, "SELECT assets.asset_id, asset_name, asset_type FROM assets, ticket_assets WHERE assets.asset_id = ticket_assets.asset_id AND ticket_id = $ticket_id AND assets.asset_id != $asset_id" ); // Get Tasks - $sql_tasks = mysqli_query($mysqli, "SELECT * FROM tasks WHERE task_ticket_id = $ticket_id ORDER BY task_order ASC, task_id ASC"); + $sql_tasks = mysqli_query($mysqli, "SELECT task_completed_at, task_completion_estimate, task_id, task_name FROM tasks WHERE task_ticket_id = $ticket_id ORDER BY task_order ASC, task_id ASC"); $task_count = mysqli_num_rows($sql_tasks); $completed_task_count = intval(mysqli_fetch_row(mysqli_query( diff --git a/agent/tickets.php b/agent/tickets.php index a298627c4..9ef5438f8 100644 --- a/agent/tickets.php +++ b/agent/tickets.php @@ -247,11 +247,8 @@ if ($q !== '') { $active_filters[] = array('label' => 'Search', 'value' => stripslashes(escapeHtml($q)), 'drop' => 'q'); } -// Ticket client access overide - This is the only way to show tickets without a client to agents with restricted client access -$access_permission_query_overide = ''; -if ($client_access_string) { - $access_permission_query_overide = "AND ticket_client_id IN (0,$client_access_string)"; -} +// Tickets with no client stay visible to restricted agents - clientScopeSql() includes 0 +$access_permission_query_overide = clientScopeSql('ticket_client_id'); /* * Columns the two views need. Explicit rather than SELECT * - the tickets diff --git a/agent/transactions.php b/agent/transactions.php index 8218257fe..f2f12f4bc 100644 --- a/agent/transactions.php +++ b/agent/transactions.php @@ -86,7 +86,7 @@ if ($sort == 'transaction_date') { if ($account_filter) { // Account details - opening balance feeds the running balance, currency feeds the summary - $row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT * FROM accounts WHERE account_id = $account_filter LIMIT 1")); + $row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT account_currency_code, opening_balance FROM accounts WHERE account_id = $account_filter LIMIT 1")); $account_currency_code = escapeHtml($row['account_currency_code']); $account_opening_balance = floatval($row['opening_balance']); diff --git a/agent/transfers.php b/agent/transfers.php index 6df181a35..409c1c88e 100644 --- a/agent/transfers.php +++ b/agent/transfers.php @@ -182,7 +182,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()")); $expense_id = intval($row['transfer_expense_id']); $revenue_id = intval($row['transfer_revenue_id']); - $sql_from = mysqli_query($mysqli, "SELECT * FROM accounts WHERE account_id = $transfer_account_from"); + $sql_from = mysqli_query($mysqli, "SELECT account_archived_at, account_name FROM accounts WHERE account_id = $transfer_account_from"); $row = mysqli_fetch_assoc($sql_from); $account_name_from = escapeHtml($row['account_name']); $account_from_archived_at = escapeHtml($row['account_archived_at']); @@ -192,7 +192,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()")); $account_from_archived_display = "Archived - "; } - $sql_to = mysqli_query($mysqli, "SELECT * FROM accounts WHERE account_id = $transfer_account_to"); + $sql_to = mysqli_query($mysqli, "SELECT account_archived_at, account_name FROM accounts WHERE account_id = $transfer_account_to"); $row = mysqli_fetch_assoc($sql_to); $account_name_to = escapeHtml($row['account_name']); $account_to_archived_at = escapeHtml($row['account_archived_at']); diff --git a/agent/trips.php b/agent/trips.php index cd0458a3e..0cf4576f6 100644 --- a/agent/trips.php +++ b/agent/trips.php @@ -17,14 +17,15 @@ if (isset($_GET['client_id'])) { $sql = mysqli_query( $mysqli, - "SELECT SQL_CALC_FOUND_ROWS * FROM trips + "SELECT SQL_CALC_FOUND_ROWS client_id, client_name, round_trip, trip_archived_at, trip_created_at, trip_date, + trip_destination, trip_id, trip_miles, trip_purpose, trip_source, trip_user_id, user_name FROM trips LEFT JOIN clients ON trip_client_id = client_id LEFT JOIN users ON trip_user_id = user_id WHERE (trip_purpose LIKE '%$q%' OR trip_source LIKE '%$q%' OR trip_destination LIKE '%$q%' OR trip_miles LIKE '%$q%' OR client_name LIKE '%$q%' OR user_name LIKE '%$q%') AND DATE(trip_date) BETWEEN '$dtf' AND '$dtt' AND trip_archived_at IS NULL $client_query - $access_permission_query + " . clientScopeSql('trip_client_id') . " ORDER BY $sort $order LIMIT $record_from, $record_to" ); diff --git a/agent/user/user_activity.php b/agent/user/user_activity.php index 642646ab4..0a842e54e 100644 --- a/agent/user/user_activity.php +++ b/agent/user/user_activity.php @@ -2,12 +2,12 @@ require_once "includes/inc_all_user.php"; -$sql_recent_logins = mysqli_query($mysqli, "SELECT * FROM logs +$sql_recent_logins = mysqli_query($mysqli, "SELECT log_created_at, log_id, log_ip, log_user_agent FROM logs WHERE log_type = 'Login' OR log_type = 'Login 2FA' AND log_action = 'Success' AND log_user_id = $session_user_id ORDER BY log_id DESC LIMIT 3" ); -$sql_recent_logs = mysqli_query($mysqli, "SELECT * FROM logs +$sql_recent_logs = mysqli_query($mysqli, "SELECT log_action, log_created_at, log_description, log_id, log_type FROM logs WHERE log_user_id = $session_user_id AND log_type NOT LIKE 'Login' ORDER BY log_id DESC LIMIT 5" ); diff --git a/agent/user/user_security.php b/agent/user/user_security.php index 96193a74f..b2e89bcac 100644 --- a/agent/user/user_security.php +++ b/agent/user/user_security.php @@ -2,7 +2,7 @@ require_once "includes/inc_all_user.php"; // User remember me tokens -$sql_remember_tokens = mysqli_query($mysqli, "SELECT * FROM remember_tokens WHERE remember_token_user_id = $session_user_id"); +$sql_remember_tokens = mysqli_query($mysqli, "SELECT remember_token_created_at, remember_token_id FROM remember_tokens WHERE remember_token_user_id = $session_user_id"); $remember_token_count = mysqli_num_rows($sql_remember_tokens); ?> diff --git a/agent/vendors.php b/agent/vendors.php index b8f2bd32b..71bb3ff6b 100644 --- a/agent/vendors.php +++ b/agent/vendors.php @@ -17,13 +17,16 @@ if (isset($_GET['client_id'])) { $sql = mysqli_query( $mysqli, - "SELECT SQL_CALC_FOUND_ROWS * FROM vendors + "SELECT SQL_CALC_FOUND_ROWS vendor_account_number, vendor_archived_at, vendor_code, vendor_contact_name, + vendor_created_at, vendor_description, vendor_email, vendor_extension, vendor_hours, + vendor_id, vendor_name, vendor_notes, vendor_phone, vendor_phone_country_code, vendor_sla, + vendor_templates.vendor_template_id, vendor_template_name, vendor_website FROM vendors LEFT JOIN clients ON client_id = vendor_client_id LEFT JOIN vendor_templates ON vendors.vendor_template_id = vendor_templates.vendor_template_id WHERE vendor_$archive_query AND (vendor_name LIKE '%$q%' OR vendor_description LIKE '%$q%' OR vendor_account_number LIKE '%$q%' OR vendor_website LIKE '%$q%' OR vendor_contact_name LIKE '%$q%' OR vendor_email LIKE '%$q%' OR vendor_phone LIKE '%$phone_query%') $client_query - $access_permission_query + " . clientScopeSql('vendor_client_id') . " ORDER BY $sort $order LIMIT $record_from, $record_to" ); diff --git a/api/v1/assets/delete.php b/api/v1/assets/delete.php index 871e53c5f..e87e9caf0 100644 --- a/api/v1/assets/delete.php +++ b/api/v1/assets/delete.php @@ -12,7 +12,7 @@ $asset_id = intval($_POST['asset_id']); $delete_count = false; if (!empty($asset_id)) { - $row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT * FROM assets WHERE asset_id = $asset_id AND asset_client_id = $client_id LIMIT 1")); + $row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT asset_name FROM assets WHERE asset_id = $asset_id AND asset_client_id = $client_id LIMIT 1")); $asset_name = $row['asset_name']; $delete_sql = mysqli_query($mysqli, "DELETE FROM assets WHERE asset_id = $asset_id AND asset_client_id = $client_id LIMIT 1"); diff --git a/api/v1/assets/update.php b/api/v1/assets/update.php index db90b3e52..58e15684b 100644 --- a/api/v1/assets/update.php +++ b/api/v1/assets/update.php @@ -21,7 +21,7 @@ if (!empty($asset_id)) { $update_sql = mysqli_query($mysqli, "UPDATE assets SET asset_name = '$name', asset_description = '$description', asset_type = '$type', asset_make = '$make', asset_model = '$model', asset_serial = '$serial', asset_os = '$os', asset_uri = '$uri', asset_uri_2 = '$uri_2', asset_status = '$status', asset_location_id = $location, asset_vendor_id = $vendor, asset_contact_id = $contact, asset_purchase_date = $purchase_date, asset_warranty_expire = $warranty_expire, asset_install_date = $install_date, asset_notes = '$notes' WHERE asset_id = $asset_id AND asset_client_id = $client_id LIMIT 1"); // Check insert & get insert ID - if ($update_sql) { + if ($update_sql && $asset_row) { $update_count = mysqli_affected_rows($mysqli); // Update Primary Interface diff --git a/api/v1/clients/archive.php b/api/v1/clients/archive.php index 64a248a8c..818043c92 100644 --- a/api/v1/clients/archive.php +++ b/api/v1/clients/archive.php @@ -21,7 +21,7 @@ if (!empty($client_id)) { $client_name = escapeSql($row['client_name']); // Stop recurring invoices - $sql_recurring_invoices = mysqli_query($mysqli, "SELECT * FROM recurring_invoices WHERE recurring_invoice_client_id = $client_id AND recurring_invoice_status = 1"); + $sql_recurring_invoices = mysqli_query($mysqli, "SELECT recurring_invoice_id FROM recurring_invoices WHERE recurring_invoice_client_id = $client_id AND recurring_invoice_status = 1"); while ($row = mysqli_fetch_assoc($sql_recurring_invoices)) { $recurring_invoice_id = intval($row['recurring_invoice_id']); mysqli_query($mysqli,"UPDATE recurring_invoices SET recurring_invoice_status = 0 WHERE recurring_invoice_id = $recurring_invoice_id AND recurring_invoice_client_id = $client_id"); diff --git a/api/v1/contacts/create.php b/api/v1/contacts/create.php index 3079b7cc0..34b0a6b92 100644 --- a/api/v1/contacts/create.php +++ b/api/v1/contacts/create.php @@ -15,7 +15,7 @@ $insert_id = false; if (!empty($name) && !empty($email) && !empty($client_id)) { // Check contact with $email doesn't already exist - $email_duplication_sql = mysqli_query($mysqli, "SELECT * FROM contacts WHERE contact_email = '$email' AND contact_client_id = '$client_id'"); + $email_duplication_sql = mysqli_query($mysqli, "SELECT 1 FROM contacts WHERE contact_email = '$email' AND contact_client_id = '$client_id'"); if (mysqli_num_rows($email_duplication_sql) == 0) { diff --git a/api/v1/contacts/delete.php b/api/v1/contacts/delete.php index 8066fdd33..3332e2234 100644 --- a/api/v1/contacts/delete.php +++ b/api/v1/contacts/delete.php @@ -12,7 +12,7 @@ $contact_id = intval($_POST['contact_id']); $delete_count = false; if (!empty($contact_id)) { - $row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT * FROM contacts WHERE contact_id = $contact_id AND contact_client_id = $client_id LIMIT 1")); + $row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT contact_name FROM contacts WHERE contact_id = $contact_id AND contact_client_id = $client_id LIMIT 1")); $contact_name = $row['contact_name']; $delete_sql = mysqli_query($mysqli, "DELETE FROM contacts WHERE contact_id = $contact_id AND contact_client_id = $client_id LIMIT 1"); diff --git a/api/v1/contacts/update.php b/api/v1/contacts/update.php index c21ba9c5e..3dd27a693 100644 --- a/api/v1/contacts/update.php +++ b/api/v1/contacts/update.php @@ -23,7 +23,7 @@ if (!empty($contact_id)) { mysqli_query($mysqli,"UPDATE contacts SET contact_primary = 0 WHERE contact_client_id = $client_id"); } - $update_sql = mysqli_query($mysqli, "UPDATE contacts SET contact_name = '$name', contact_title = '$title', contact_department = '$department', contact_email = '$email', contact_phone = '$phone', contact_extension = '$extension', contact_mobile = '$mobile', contact_notes = '$notes', contact_primary = '$primary', contact_important = '$important', contact_billing = '$billing', contact_technical = '$technical', contact_location_id = $location_id, contact_client_id = $client_id WHERE contact_id = $contact_id LIMIT 1"); + $update_sql = mysqli_query($mysqli, "UPDATE contacts SET contact_name = '$name', contact_title = '$title', contact_department = '$department', contact_email = '$email', contact_phone = '$phone', contact_extension = '$extension', contact_mobile = '$mobile', contact_notes = '$notes', contact_primary = '$primary', contact_important = '$important', contact_billing = '$billing', contact_technical = '$technical', contact_location_id = $location_id, contact_client_id = $client_id WHERE contact_id = $contact_id AND contact_client_id = $client_id LIMIT 1"); // Check insert & get insert ID if ($update_sql) { diff --git a/api/v1/credentials/read.php b/api/v1/credentials/read.php index 584f6d8e9..01ee1ce2e 100644 --- a/api/v1/credentials/read.php +++ b/api/v1/credentials/read.php @@ -25,7 +25,7 @@ if (isset($_GET['credential_id']) && !empty($api_key_decrypt_password)) { } elseif (!empty($api_key_decrypt_password)) { // All credentials ("credentials") - $sql = mysqli_query($mysqli, "SELECT * FROM credentials WHERE 1=1 " . apiClientScopeSql('credential_client_id') . " ORDER BY credential_id LIMIT $limit OFFSET $offset"); + $sql = mysqli_query($mysqli, "SELECT credential_password, credential_username FROM credentials WHERE 1=1 " . apiClientScopeSql('credential_client_id') . " ORDER BY credential_id LIMIT $limit OFFSET $offset"); } diff --git a/api/v1/credentials/update.php b/api/v1/credentials/update.php index 049a02f07..a800253cb 100644 --- a/api/v1/credentials/update.php +++ b/api/v1/credentials/update.php @@ -20,7 +20,7 @@ if (!empty($_POST['api_key_decrypt_password']) && !empty($credential_id)) { $update_sql = mysqli_query($mysqli,"UPDATE credentials SET credential_name = '$name', credential_description = '$description', credential_uri = '$uri', credential_uri_2 = '$uri_2', credential_username = '$username', credential_password = '$password', credential_otp_secret = '$otp_secret', credential_note = '$note', credential_favorite = $favorite, credential_contact_id = $contact_id, credential_asset_id = $asset_id, credential_client_id = $client_id WHERE credential_id = '$credential_id' AND credential_client_id = $client_id LIMIT 1"); // Check insert & get insert ID - if ($update_sql) { + if ($update_sql && $credential_row) { $update_count = mysqli_affected_rows($mysqli); if ($password_changed) { diff --git a/api/v1/documents/update.php b/api/v1/documents/update.php index 166a02128..8c845e8a4 100644 --- a/api/v1/documents/update.php +++ b/api/v1/documents/update.php @@ -14,7 +14,8 @@ if (!empty($document_id)) { // 1) Load the current document (scoped to this client) $sql_original_document = mysqli_query( $mysqli, - "SELECT * FROM documents + "SELECT document_content, document_created_at, document_created_by, document_description, + document_name, document_updated_at, document_updated_by FROM documents WHERE document_client_id = $client_id AND document_id = $document_id LIMIT 1" diff --git a/api/v1/enforce_api_rbac.php b/api/v1/enforce_api_rbac.php index c36ac9e3c..ef2b5b01f 100644 --- a/api/v1/enforce_api_rbac.php +++ b/api/v1/enforce_api_rbac.php @@ -45,24 +45,10 @@ function apiUserCanAccessClient($client_id) { } // Client-scope SQL fragment for a read query, from the user's allow / deny lists. -// Admin and unrestricted users get no restriction. Column-aware, so it works on any -// resource. Returns " AND ..." or "" (used after a "WHERE 1=1" anchor). +// Thin wrapper over clientScopeSql() in functions/auth.php so the API and the UI share one +// implementation. Kept under the api* name because every endpoint already calls it. function apiClientScopeSql($column) { - global $session_is_admin, $client_access_array, $client_deny_array; - if ($session_is_admin) { - return ''; - } - if (empty($client_access_array) && empty($client_deny_array)) { - return ''; // unrestricted user - all clients - } - $sql = ''; - if (!empty($client_access_array)) { - $sql .= " AND $column IN (" . implode(',', array_map('intval', $client_access_array)) . ")"; - } - if (!empty($client_deny_array)) { - $sql .= " AND $column NOT IN (" . implode(',', array_map('intval', $client_deny_array)) . ")"; - } - return $sql; + return clientScopeSql($column); } // --- Every key must be tied to a user (legacy keys were removed in the 2.4.7 migration) --- diff --git a/api/v1/expenses/read.php b/api/v1/expenses/read.php index ddcd47c77..3bbb05779 100644 --- a/api/v1/expenses/read.php +++ b/api/v1/expenses/read.php @@ -5,18 +5,16 @@ require_once '../validate_api_key.php'; require_once '../require_get_method.php'; -// Expenses aren't client-scoped; access is gated by module_financial in enforce_api_rbac.php - if (isset($_GET['expense_id'])) { // Expense via ID (single) $id = intval($_GET['expense_id']); - $sql = mysqli_query($mysqli, "SELECT * FROM expenses WHERE expense_id = '$id'"); + $sql = mysqli_query($mysqli, "SELECT * FROM expenses WHERE expense_id = '$id' AND 1=1 " . apiClientScopeSql('expense_client_id') . ""); } else { // All expenses - $sql = mysqli_query($mysqli, "SELECT * FROM expenses ORDER BY expense_id LIMIT $limit OFFSET $offset"); + $sql = mysqli_query($mysqli, "SELECT * FROM expenses WHERE 1=1 " . apiClientScopeSql('expense_client_id') . " ORDER BY expense_id LIMIT $limit OFFSET $offset"); } // Output diff --git a/api/v1/ticket_replies/create.php b/api/v1/ticket_replies/create.php index 009443acf..27a9b5e96 100644 --- a/api/v1/ticket_replies/create.php +++ b/api/v1/ticket_replies/create.php @@ -70,6 +70,7 @@ if (!empty($ticket_id) && !empty($reply)) { $ticket_url_key = escapeSql($ticket_row['ticket_url_key']); $ticket_first_response_at = escapeSql($ticket_row['ticket_first_response_at']); $client_id = intval($ticket_row['ticket_client_id']); + $original_ticket_status = intval($ticket_row['ticket_status']); // Mark first response time if required - internal notes don't count as a response if (empty($ticket_first_response_at) && $reply_type == 'Public') { @@ -87,11 +88,15 @@ if (!empty($ticket_id) && !empty($reply)) { if (!empty($reply_ticket_status)) { mysqli_query($mysqli, "UPDATE tickets SET ticket_status = $reply_ticket_status WHERE ticket_id = $ticket_id LIMIT 1"); - $new_status_name = escapeSql(getTicketStatusName($reply_ticket_status)); - logTicketHistory($ticket_id, "Status set to $new_status_name via the API ($api_key_name)"); + // Only record a status change when the status actually changed - + // Resolved is left out because the resolve block below logs it + if ($reply_ticket_status !== $original_ticket_status && $reply_ticket_status != 4) { + $new_status_name = escapeSql(getTicketStatusName($reply_ticket_status)); + logTicketHistory($ticket_id, "Status set to $new_status_name via the API ($api_key_name)"); + } - // Resolve the ticket, if set - if ($reply_ticket_status == 4) { + // Resolve the ticket, if it is actually moving into Resolved + if ($reply_ticket_status == 4 && $original_ticket_status != 4) { mysqli_query($mysqli, "UPDATE tickets SET ticket_resolved_at = NOW() WHERE ticket_id = $ticket_id AND ticket_resolved_at IS NULL LIMIT 1"); setTicketResolutionSlaMet($ticket_id); diff --git a/api/v1/tickets/resolve.php b/api/v1/tickets/resolve.php index 3790f0ec8..31be07310 100644 --- a/api/v1/tickets/resolve.php +++ b/api/v1/tickets/resolve.php @@ -15,7 +15,7 @@ $update_count = false; if (!empty($ticket_id)) { - $ticket_row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT * FROM tickets WHERE ticket_id = '$ticket_id' AND ticket_resolved_at IS NULL AND ticket_client_id = $client_id LIMIT 1")); + $ticket_row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT ticket_first_response_at, ticket_id, ticket_number, ticket_prefix FROM tickets WHERE ticket_id = '$ticket_id' AND ticket_resolved_at IS NULL AND ticket_client_id = $client_id LIMIT 1")); // Grab what we need, not using the model $ticket_id = intval($ticket_row['ticket_id']); // Override so things fail if this is bad diff --git a/api/v1/validate_api_key.php b/api/v1/validate_api_key.php index 2404bc8c5..98084ae88 100644 --- a/api/v1/validate_api_key.php +++ b/api/v1/validate_api_key.php @@ -70,7 +70,7 @@ if (isset($_POST['api_key'])) { if (isset($api_key)) { $api_key = escapeSql($api_key); - $sql = mysqli_query($mysqli, "SELECT * FROM api_keys WHERE api_key_secret = '$api_key' AND api_key_expire > NOW() LIMIT 1"); + $sql = mysqli_query($mysqli, "SELECT api_key_decrypt_hash, api_key_name, api_key_user_id FROM api_keys WHERE api_key_secret = '$api_key' AND api_key_expire > NOW() LIMIT 1"); // Failed if (mysqli_num_rows($sql) !== 1) { diff --git a/client/assets.php b/client/assets.php index 4caa26b69..061d20171 100644 --- a/client/assets.php +++ b/client/assets.php @@ -10,7 +10,9 @@ require_once "includes/inc_all.php"; enforceContactCan('itdoc'); -$assets_sql = mysqli_query($mysqli, "SELECT * FROM assets LEFT JOIN contacts ON asset_contact_id = contact_id WHERE asset_client_id = $session_client_id AND asset_archived_at IS NULL ORDER BY asset_type ASC, asset_name ASC"); +$assets_sql = mysqli_query($mysqli, "SELECT asset_description, asset_id, asset_make, asset_model, asset_name, asset_purchase_date, + asset_serial, asset_status, asset_type, asset_uri_client, asset_warranty_expire, + contact_name FROM assets LEFT JOIN contacts ON asset_contact_id = contact_id WHERE asset_client_id = $session_client_id AND asset_archived_at IS NULL ORDER BY asset_type ASC, asset_name ASC"); ?>
diff --git a/client/includes/check_login.php b/client/includes/check_login.php index 25fb5af86..223ab841c 100644 --- a/client/includes/check_login.php +++ b/client/includes/check_login.php @@ -25,7 +25,7 @@ $session_contact_id = intval($_SESSION['contact_id']); $session_user_id = intval($_SESSION['user_id']); // Load user session vars -$sql = mysqli_query($mysqli, "SELECT * FROM users WHERE users.user_id = $session_user_id"); +$sql = mysqli_query($mysqli, "SELECT user_archived_at, user_avatar, user_status, user_type FROM users WHERE users.user_id = $session_user_id"); $row = mysqli_fetch_assoc($sql); @@ -56,7 +56,7 @@ if ($session_user_archived_at !== null) { } // Load company session vars -$sql = mysqli_query($mysqli, "SELECT * FROM companies WHERE company_id = 1"); +$sql = mysqli_query($mysqli, "SELECT company_country, company_currency, company_locale, company_logo, company_name FROM companies WHERE company_id = 1"); $row = mysqli_fetch_assoc($sql); $session_company_name = $row['company_name']; @@ -67,7 +67,8 @@ $currency_format = numfmt_create($session_company_locale, NumberFormatter::CURRE $session_company_logo = $row['company_logo']; // Load contact session vars -$contact_sql = mysqli_query($mysqli, "SELECT * FROM contacts WHERE contact_id = $session_contact_id AND contact_client_id = $session_client_id"); +$contact_sql = mysqli_query($mysqli, "SELECT contact_billing, contact_email, contact_name, contact_photo, contact_pin, contact_primary, + contact_technical, contact_title FROM contacts WHERE contact_id = $session_contact_id AND contact_client_id = $session_client_id"); $contact = mysqli_fetch_assoc($contact_sql); $session_contact_name = escapeSql($contact['contact_name']); @@ -88,7 +89,7 @@ if ($contact['contact_billing'] == 1) { } // Load client session vars -$client_sql = mysqli_query($mysqli, "SELECT * FROM clients WHERE client_id = $session_client_id"); +$client_sql = mysqli_query($mysqli, "SELECT client_name FROM clients WHERE client_id = $session_client_id"); $client = mysqli_fetch_assoc($client_sql); $session_client_name = $client['client_name']; diff --git a/client/includes/header.php b/client/includes/header.php index 93d6851be..408faa996 100644 --- a/client/includes/header.php +++ b/client/includes/header.php @@ -80,7 +80,7 @@ header("X-Frame-Options: DENY"); // Legacy

Invoices

diff --git a/client/login_microsoft.php b/client/login_microsoft.php index 2c603b608..5a4179b86 100644 --- a/client/login_microsoft.php +++ b/client/login_microsoft.php @@ -126,7 +126,7 @@ if (isset($_GET['code']) || isset($_GET['error'])) { $upn = mysqli_real_escape_string($mysqli, $msgraph_response["userPrincipalName"]); - $sql = mysqli_query($mysqli, "SELECT * FROM users + $sql = mysqli_query($mysqli, "SELECT contact_client_id, contact_id, user_auth_method, user_email, user_id FROM users LEFT JOIN contacts ON user_id = contact_user_id LEFT JOIN clients ON contact_client_id = client_id WHERE user_email = '$upn' diff --git a/client/login_reset.php b/client/login_reset.php index 584fd105a..18bdbac9a 100644 --- a/client/login_reset.php +++ b/client/login_reset.php @@ -109,7 +109,7 @@ if ($_SERVER['REQUEST_METHOD'] == "POST") { $client = intval($_POST['client']); // Query user - $sql = mysqli_query($mysqli, "SELECT * FROM users LEFT JOIN contacts ON user_id = contact_user_id WHERE user_email = '$email' AND user_password_reset_token = '$token' AND contact_client_id = $client AND user_auth_method = 'local' AND user_type = 2 AND user_status = 1 AND user_archived_at IS NULL LIMIT 1"); + $sql = mysqli_query($mysqli, "SELECT contact_id, contact_name, user_id, user_password_reset_token FROM users LEFT JOIN contacts ON user_id = contact_user_id WHERE user_email = '$email' AND user_password_reset_token = '$token' AND contact_client_id = $client AND user_auth_method = 'local' AND user_type = 2 AND user_status = 1 AND user_archived_at IS NULL LIMIT 1"); $user_row = mysqli_fetch_assoc($sql); $contact_id = intval($user_row['contact_id']); $user_id = intval($user_row['user_id']); @@ -207,7 +207,7 @@ if ($_SERVER['REQUEST_METHOD'] == "POST") { $email = escapeSql($_GET['email']); $client = intval($_GET['client']); - $sql = mysqli_query($mysqli, "SELECT * FROM users LEFT JOIN contacts ON user_id = contact_user_id WHERE user_email = '$email' AND user_password_reset_token = '$token' AND contact_client_id = $client LIMIT 1"); + $sql = mysqli_query($mysqli, "SELECT user_password_reset_token FROM users LEFT JOIN contacts ON user_id = contact_user_id WHERE user_email = '$email' AND user_password_reset_token = '$token' AND contact_client_id = $client LIMIT 1"); $user_row = mysqli_fetch_assoc($sql); // Sanity check diff --git a/client/post.php b/client/post.php index 3b333d7bd..d824709d6 100644 --- a/client/post.php +++ b/client/post.php @@ -105,13 +105,20 @@ if (isset($_POST['add_ticket_comment'])) { $ticket_reply_id = mysqli_insert_id($mysqli); // Update Ticket Last Response Field & set ticket to open as client has replied + $original_row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT ticket_status FROM tickets WHERE ticket_id = $ticket_id LIMIT 1")); + $original_ticket_status = intval($original_row['ticket_status'] ?? 0); + mysqli_query($mysqli, "UPDATE tickets SET ticket_status = 2 WHERE ticket_id = $ticket_id AND ticket_client_id = $session_client_id LIMIT 1"); syncTicketSlaClock($ticket_id); - logTicketHistory($ticket_id, "$session_contact_name replied from the client portal, reopening the ticket"); + + // Only record the reopen when the ticket was not already open + if ($original_ticket_status !== 2) { + logTicketHistory($ticket_id, "$session_contact_name replied from the client portal, reopening the ticket"); + } // Get ticket details & Notify the assigned tech (if any) - $ticket_details = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT * FROM tickets LEFT JOIN clients ON ticket_client_id = client_id WHERE ticket_id = $ticket_id LIMIT 1")); + $ticket_details = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT client_name, ticket_assigned_to, ticket_number, ticket_subject FROM tickets LEFT JOIN clients ON ticket_client_id = client_id WHERE ticket_id = $ticket_id LIMIT 1")); $ticket_number = intval($ticket_details['ticket_number']); $ticket_assigned_to = intval($ticket_details['ticket_assigned_to']); @@ -166,7 +173,8 @@ if (isset($_GET['approve_ticket_task'])) { $approval_id = intval($_GET['approval_id']); $url_key = escapeSql($_GET['approval_url_key']); - $approval_row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT * FROM task_approvals LEFT JOIN tasks on task_id = approval_task_id WHERE approval_id = $approval_id AND approval_task_id = $task_id AND approval_url_key = '$url_key' AND approval_status = 'pending' AND approval_scope = 'client'")); + $approval_row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT approval_created_by, approval_required_user_id, approval_scope, approval_type, task_name, + task_ticket_id FROM task_approvals LEFT JOIN tasks on task_id = approval_task_id WHERE approval_id = $approval_id AND approval_task_id = $task_id AND approval_url_key = '$url_key' AND approval_status = 'pending' AND approval_scope = 'client'")); $task_name = escapeHtml($approval_row['task_name']); $scope = escapeHtml($approval_row['approval_scope']); @@ -236,7 +244,7 @@ if (isset($_GET['resolve_ticket'])) { $ticket_id = intval($_GET['resolve_ticket']); // Get ticket details for logging - $row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT * FROM tickets WHERE ticket_id = $ticket_id LIMIT 1")); + $row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT ticket_number, ticket_prefix FROM tickets WHERE ticket_id = $ticket_id LIMIT 1")); $ticket_prefix = escapeSql($row['ticket_prefix']); $ticket_number = intval($row['ticket_number']); @@ -274,7 +282,7 @@ if (isset($_GET['reopen_ticket'])) { $ticket_id = intval($_GET['reopen_ticket']); // Get ticket details for logging - $row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT * FROM tickets WHERE ticket_id = $ticket_id LIMIT 1")); + $row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT ticket_number, ticket_prefix FROM tickets WHERE ticket_id = $ticket_id LIMIT 1")); $ticket_prefix = escapeSql($row['ticket_prefix']); $ticket_number = intval($row['ticket_number']); @@ -312,7 +320,7 @@ if (isset($_GET['close_ticket'])) { $ticket_id = intval($_GET['close_ticket']); // Get ticket details for logging - $row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT * FROM tickets WHERE ticket_id = $ticket_id LIMIT 1")); + $row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT ticket_number, ticket_prefix FROM tickets WHERE ticket_id = $ticket_id LIMIT 1")); $ticket_prefix = escapeSql($row['ticket_prefix']); $ticket_number = intval($row['ticket_number']); @@ -481,7 +489,10 @@ if (isset($_GET['add_payment_by_provider'])) { $saved_payment_id = intval($_GET['add_payment_by_provider']); // Get invoice details - $sql = mysqli_query($mysqli,"SELECT * FROM invoices + $sql = mysqli_query($mysqli,"SELECT client_id, client_name, contact_email, contact_extension, contact_mobile, + contact_mobile_country_code, contact_name, contact_phone, contact_phone_country_code, + invoice_amount, invoice_currency_code, invoice_number, invoice_prefix, invoice_status, + invoice_url_key FROM invoices LEFT JOIN clients ON invoice_client_id = client_id LEFT JOIN contacts ON client_id = contact_client_id AND contact_primary = 1 WHERE invoice_id = $invoice_id AND client_id = $session_client_id" @@ -503,7 +514,8 @@ if (isset($_GET['add_payment_by_provider'])) { $contact_mobile = escapeSql(formatPhoneNumber($row['contact_mobile'], $row['contact_mobile_country_code'])); // Get ITFlow company details - $sql = mysqli_query($mysqli,"SELECT * FROM companies WHERE company_id = 1"); + $sql = mysqli_query($mysqli,"SELECT company_address, company_city, company_country, company_email, company_name, company_phone, + company_phone_country_code, company_state, company_website, company_zip FROM companies WHERE company_id = 1"); $row = mysqli_fetch_assoc($sql); $company_name = escapeSql($row['company_name']); $company_country = escapeSql($row['company_country']); @@ -520,7 +532,9 @@ if (isset($_GET['add_payment_by_provider'])) { $config_invoice_from_email = escapeSql($config_invoice_from_email); // Get Client Payment Details - $sql = mysqli_query($mysqli, "SELECT * FROM client_saved_payment_methods LEFT JOIN payment_providers ON saved_payment_provider_id = payment_provider_id LEFT JOIN client_payment_provider ON saved_payment_client_id = client_id WHERE saved_payment_id = $saved_payment_id AND saved_payment_client_id = $session_client_id LIMIT 1"); + $sql = mysqli_query($mysqli, "SELECT payment_provider_account, payment_provider_client, payment_provider_private_key, + payment_provider_public_key, saved_payment_client_id, saved_payment_description, + saved_payment_provider_method FROM client_saved_payment_methods LEFT JOIN payment_providers ON saved_payment_provider_id = payment_provider_id LEFT JOIN client_payment_provider ON saved_payment_client_id = client_id WHERE saved_payment_id = $saved_payment_id AND saved_payment_client_id = $session_client_id LIMIT 1"); $row = mysqli_fetch_assoc($sql); $public_key = escapeSql($row['payment_provider_public_key']); @@ -671,7 +685,7 @@ if (isset($_POST['create_stripe_customer'])) { // Get Stripe provider $stripe_provider_result = mysqli_query($mysqli, " - SELECT * FROM payment_providers + SELECT payment_provider_id, payment_provider_private_key FROM payment_providers WHERE payment_provider_name = 'Stripe' AND payment_provider_active = 1 LIMIT 1 @@ -759,7 +773,7 @@ if (isset($_GET['create_stripe_checkout'])) { // Fetch Stripe provider info $stripe_provider_result = mysqli_query($mysqli, " - SELECT * FROM payment_providers + SELECT payment_provider_id, payment_provider_private_key FROM payment_providers WHERE payment_provider_name = 'Stripe' AND payment_provider_active = 1 LIMIT 1 @@ -842,7 +856,7 @@ if (isset($_GET['stripe_save_card'])) { // Get Stripe provider $stripe_provider_result = mysqli_query($mysqli, " - SELECT * FROM payment_providers + SELECT payment_provider_id, payment_provider_private_key FROM payment_providers WHERE payment_provider_name = 'Stripe' AND payment_provider_active = 1 LIMIT 1 @@ -925,7 +939,8 @@ if (isset($_GET['stripe_save_card'])) { // Email Confirmation $sql_settings = mysqli_query($mysqli, " - SELECT * FROM companies, settings + SELECT company_name, company_phone, company_phone_country_code, config_invoice_from_email, + config_invoice_from_name, config_smtp_host FROM companies, settings WHERE companies.company_id = settings.company_id AND companies.company_id = 1 "); @@ -973,7 +988,7 @@ if (isset($_GET['delete_saved_payment'])) { // Get Stripe provider info $stripe_provider_result = mysqli_query($mysqli, " - SELECT * FROM payment_providers + SELECT payment_provider_id, payment_provider_private_key FROM payment_providers WHERE payment_provider_name = 'Stripe' AND payment_provider_active = 1 LIMIT 1 @@ -1073,7 +1088,8 @@ if (isset($_POST['set_recurring_payment'])) { $saved_payment_id = intval($_POST['saved_payment_id']); // Get Recurring Invoice Info for logging and alerting - $sql = mysqli_query($mysqli, "SELECT * FROM recurring_invoices WHERE recurring_invoice_id = $recurring_invoice_id AND recurring_invoice_client_id = $session_client_id"); + $sql = mysqli_query($mysqli, "SELECT recurring_invoice_amount, recurring_invoice_currency_code, recurring_invoice_number, + recurring_invoice_prefix FROM recurring_invoices WHERE recurring_invoice_id = $recurring_invoice_id AND recurring_invoice_client_id = $session_client_id"); $row = mysqli_fetch_assoc($sql); $recurring_invoice_prefix = escapeSql($row['recurring_invoice_prefix']); $recurring_invoice_number = intval($row['recurring_invoice_number']); @@ -1084,7 +1100,8 @@ if (isset($_POST['set_recurring_payment'])) { // Get Payment provider and method $sql = mysqli_query($mysqli, " - SELECT * FROM payment_providers + SELECT payment_provider_account, payment_provider_id, payment_provider_name, + saved_payment_description FROM payment_providers LEFT JOIN client_saved_payment_methods ON saved_payment_provider_id = payment_provider_id WHERE saved_payment_id = $saved_payment_id AND saved_payment_client_id = $session_client_id diff --git a/client/quotes.php b/client/quotes.php index 081da951f..b49a4efda 100644 --- a/client/quotes.php +++ b/client/quotes.php @@ -10,7 +10,8 @@ require_once "includes/inc_all.php"; enforceContactCan('accounting'); -$quotes_sql = mysqli_query($mysqli, "SELECT * FROM quotes WHERE quote_client_id = $session_client_id AND quote_status != 'Draft' ORDER BY quote_date DESC"); +$quotes_sql = mysqli_query($mysqli, "SELECT quote_amount, quote_date, quote_id, quote_number, quote_prefix, quote_scope, quote_status, + quote_url_key FROM quotes WHERE quote_client_id = $session_client_id AND quote_status != 'Draft' ORDER BY quote_date DESC"); ?>

Quotes

diff --git a/client/recurring_invoices.php b/client/recurring_invoices.php index bdec469ea..ffdfe38e9 100644 --- a/client/recurring_invoices.php +++ b/client/recurring_invoices.php @@ -11,7 +11,10 @@ require_once "includes/inc_all.php"; enforceContactCan('accounting'); -$recurring_invoices_sql = mysqli_query($mysqli, "SELECT * FROM recurring_invoices +$recurring_invoices_sql = mysqli_query($mysqli, "SELECT recurring_invoice_amount, recurring_invoice_frequency, recurring_invoice_id, + recurring_invoice_next_date, recurring_invoice_number, recurring_invoice_prefix, + recurring_invoice_scope, recurring_invoice_status, recurring_payment_id, + recurring_payment_recurring_invoice_id, recurring_payment_saved_payment_id FROM recurring_invoices LEFT JOIN recurring_payments ON recurring_payment_recurring_invoice_id = recurring_invoice_id WHERE recurring_invoice_client_id = $session_client_id AND recurring_invoice_status = 1 @@ -19,7 +22,7 @@ $recurring_invoices_sql = mysqli_query($mysqli, "SELECT * FROM recurring_invoice ); // Get Payment Provide Details -$payment_provider_sql = mysqli_query($mysqli, "SELECT * FROM payment_providers WHERE payment_provider_active = 1 LIMIT 1"); +$payment_provider_sql = mysqli_query($mysqli, "SELECT payment_provider_id, payment_provider_name, payment_provider_threshold FROM payment_providers WHERE payment_provider_active = 1 LIMIT 1"); $row = mysqli_fetch_assoc($payment_provider_sql); $payment_provider_id = intval($row['payment_provider_id']); $payment_provider_name = escapeHtml($row['payment_provider_name']); @@ -74,7 +77,7 @@ $payment_provider_threshold = floatval($row['payment_provider_threshold']); ly - 0) { ?> diff --git a/client/saved_payment_methods.php b/client/saved_payment_methods.php index 0f82c70e6..89b02c800 100644 --- a/client/saved_payment_methods.php +++ b/client/saved_payment_methods.php @@ -12,7 +12,7 @@ require_once '../includes/stripe_init.php'; // Get Stripe provider info $stripe_provider_query = mysqli_query($mysqli, " - SELECT * FROM payment_providers WHERE payment_provider_name = 'Stripe' LIMIT 1 + SELECT payment_provider_id, payment_provider_private_key, payment_provider_public_key FROM payment_providers WHERE payment_provider_name = 'Stripe' LIMIT 1 "); $stripe_provider = mysqli_fetch_assoc($stripe_provider_query); @@ -28,7 +28,7 @@ $stripe_secret_key = escapeHtml($stripe_provider['payment_provider_private_key'] // Get client's Stripe customer ID $stripe_customer_query = mysqli_query($mysqli, " - SELECT * FROM client_payment_provider + SELECT payment_provider_client FROM client_payment_provider WHERE client_id = $session_client_id AND payment_provider_id = $stripe_provider_id LIMIT 1 "); diff --git a/client/ticket.php b/client/ticket.php index 75bf15c17..ad8fcda55 100644 --- a/client/ticket.php +++ b/client/ticket.php @@ -53,18 +53,18 @@ if (isset($_GET['id']) && intval($_GET['id'])) { // Get Ticket Attachments (not associated with a specific reply) $sql_ticket_attachments = mysqli_query( $mysqli, - "SELECT * FROM ticket_attachments + "SELECT ticket_attachment_id, ticket_attachment_name FROM ticket_attachments WHERE ticket_attachment_reply_id IS NULL AND ticket_attachment_ticket_id = $ticket_id" ); // Get Tasks - $sql_tasks = mysqli_query( $mysqli, "SELECT * FROM tasks WHERE task_ticket_id = $ticket_id ORDER BY task_order ASC, task_id ASC"); + $sql_tasks = mysqli_query( $mysqli, "SELECT 1 FROM tasks WHERE task_ticket_id = $ticket_id ORDER BY task_order ASC, task_id ASC"); $task_count = mysqli_num_rows($sql_tasks); // Get Completed Task Count $sql_tasks_completed = mysqli_query($mysqli, - "SELECT * FROM tasks + "SELECT 1 FROM tasks WHERE task_ticket_id = $ticket_id AND task_completed_at IS NOT NULL" ); @@ -269,7 +269,9 @@ if (isset($_GET['id']) && intval($_GET['id'])) {
0) { /** ======================================================================= * SEND: status = 0 (Queued) * ======================================================================= */ -$sql_queue = mysqli_query($mysqli, "SELECT * FROM email_queue WHERE email_status = 0 AND email_queued_at <= NOW()"); +$sql_queue = mysqli_query($mysqli, "SELECT email_attachments, email_cal_str, email_content, email_from, email_from_name, email_id, + email_recipient, email_recipient_name, email_subject FROM email_queue WHERE email_status = 0 AND email_queued_at <= NOW()"); if (mysqli_num_rows($sql_queue) > 0) { while ($rowq = mysqli_fetch_assoc($sql_queue)) { @@ -417,7 +422,8 @@ if (mysqli_num_rows($sql_queue) > 0) { */ $sql_failed_queue = mysqli_query( $mysqli, - "SELECT * FROM email_queue + "SELECT email_attachments, email_attempts, email_cal_str, email_content, email_from, + email_from_name, email_id, email_recipient, email_recipient_name, email_subject FROM email_queue WHERE email_status = 2 AND email_attempts < 4 AND email_failed_at <= NOW() - INTERVAL 30 MINUTE" diff --git a/cron/nightly_tasks.php b/cron/nightly_tasks.php index 997da02f9..fbebc2b1d 100644 --- a/cron/nightly_tasks.php +++ b/cron/nightly_tasks.php @@ -26,7 +26,20 @@ require_once "../config.php"; require_once "../includes/inc_set_timezone.php"; require_once "../functions.php"; -$sql_companies = mysqli_query($mysqli, "SELECT * FROM companies, settings WHERE companies.company_id = settings.company_id AND companies.company_id = 1"); +$sql_companies = mysqli_query($mysqli, "SELECT company_city, company_country, company_currency, company_email, company_locale, + company_name, company_phone, company_phone_country_code, company_state, company_website, + config_enable_alert_domain_expire, config_enable_cron, config_invoice_from_email, + config_invoice_from_name, config_invoice_late_fee_enable, config_invoice_late_fee_percent, + config_invoice_prefix, config_log_retention, config_login_remember_me_expire, + config_mail_from_email, config_mail_from_name, config_module_enable_accounting, + config_module_enable_itdoc, config_module_enable_ticketing, + config_recurring_auto_send_invoice, config_send_invoice_reminders, config_smtp_encryption, + config_smtp_host, config_smtp_password, config_smtp_port, config_smtp_provider, + config_smtp_username, config_telemetry, config_theme, config_ticket_autoclose_hours, + config_ticket_client_general_notifications, config_ticket_email_parse, + config_ticket_from_email, config_ticket_from_name, + config_ticket_new_ticket_notification_email, config_ticket_prefix, + config_whitelabel_enabled, config_whitelabel_key FROM companies, settings WHERE companies.company_id = settings.company_id AND companies.company_id = 1"); $row = mysqli_fetch_assoc($sql_companies); @@ -218,7 +231,7 @@ if ($config_enable_alert_domain_expire == 1) { //Get Domains Expiring $sql = mysqli_query( $mysqli, - "SELECT * FROM domains + "SELECT client_id, client_name, domain_expire, domain_id, domain_name FROM domains LEFT JOIN clients ON domain_client_id = client_id WHERE domain_expire IS NOT NULL AND domain_expire = CURDATE() + INTERVAL $day DAY" ); @@ -248,7 +261,8 @@ foreach ($certificateAlertArray as $day) { //Get Certs Expiring $sql = mysqli_query( $mysqli, - "SELECT * FROM certificates + "SELECT certificate_domain, certificate_expire, certificate_id, certificate_name, + certificate_public_key, client_id, client_name FROM certificates LEFT JOIN clients ON certificate_client_id = client_id WHERE certificate_expire = CURDATE() + INTERVAL $day DAY" ); @@ -298,7 +312,7 @@ foreach ($warranty_alert_array as $day) { //Get Asset Warranty Expiring $sql = mysqli_query( $mysqli, - "SELECT * FROM assets + "SELECT asset_id, asset_name, asset_warranty_expire, client_id, client_name FROM assets LEFT JOIN clients ON asset_client_id = client_id WHERE asset_warranty_expire = CURDATE() + INTERVAL $day DAY" ); @@ -335,7 +349,10 @@ if ($tickets_pending_assignment > 0) { // Recurring tickets // Get recurring tickets for today -$sql_recurring_tickets = mysqli_query($mysqli, "SELECT * FROM recurring_tickets WHERE recurring_ticket_next_run = CURDATE()"); +$sql_recurring_tickets = mysqli_query($mysqli, "SELECT recurring_ticket_asset_id, recurring_ticket_assigned_to, recurring_ticket_billable, + recurring_ticket_category, recurring_ticket_client_id, recurring_ticket_contact_id, + recurring_ticket_created_by, recurring_ticket_details, recurring_ticket_frequency, + recurring_ticket_id, recurring_ticket_priority, recurring_ticket_subject FROM recurring_tickets WHERE recurring_ticket_next_run = CURDATE()"); if (mysqli_num_rows($sql_recurring_tickets) > 0) { while ($row = mysqli_fetch_assoc($sql_recurring_tickets)) { @@ -492,7 +509,7 @@ if (mysqli_num_rows($sql_recurring_tickets) > 0) { } // Flag any active recurring "next run" dates that are in the past -$sql_invalid_recurring_tickets = mysqli_query($mysqli, "SELECT * FROM recurring_tickets WHERE recurring_ticket_next_run < CURDATE()"); +$sql_invalid_recurring_tickets = mysqli_query($mysqli, "SELECT recurring_ticket_subject FROM recurring_tickets WHERE recurring_ticket_next_run < CURDATE()"); while ($row = mysqli_fetch_assoc($sql_invalid_recurring_tickets)) { $subject = escapeSql($row['recurring_ticket_subject']); appNotify("Ticket", "Recurring ticket $subject next run date is in the past!", "/agent/recurring_tickets.php"); @@ -507,7 +524,8 @@ while ($row = mysqli_fetch_assoc($sql_invalid_recurring_tickets)) { $sql_resolved_tickets_to_close = mysqli_query( $mysqli, - "SELECT * FROM tickets + "SELECT ticket_assigned_to, ticket_client_id, ticket_id, ticket_number, ticket_prefix, + ticket_status, ticket_subject FROM tickets WHERE ticket_status = 4 AND ticket_updated_at < NOW() - INTERVAL $config_ticket_autoclose_hours HOUR" ); @@ -544,7 +562,9 @@ if ($config_send_invoice_reminders == 1) { $sql = mysqli_query( $mysqli, - "SELECT * FROM invoices + "SELECT client_id, client_name, contact_email, contact_name, invoice_amount, invoice_currency_code, + invoice_date, invoice_due, invoice_id, invoice_number, invoice_prefix, invoice_status, + invoice_url_key FROM invoices LEFT JOIN clients ON invoice_client_id = client_id LEFT JOIN contacts ON clients.client_id = contacts.contact_client_id AND contact_primary = 1 WHERE invoice_status != 'Draft' @@ -654,7 +674,13 @@ if ($config_send_invoice_reminders == 1) { // Send Recurring Invoices that match todays date and are active //Loop through all recurring that match today's date and is active -$sql_recurring_invoices = mysqli_query($mysqli, "SELECT * FROM recurring_invoices +$sql_recurring_invoices = mysqli_query($mysqli, "SELECT client_name, client_net_terms, recurring_invoice_amount, recurring_invoice_category_id, + recurring_invoice_client_id, recurring_invoice_currency_code, + recurring_invoice_discount_amount, recurring_invoice_email_notify, + recurring_invoice_frequency, recurring_invoice_id, recurring_invoice_last_sent, + recurring_invoice_next_date, recurring_invoice_note, recurring_invoice_scope, + recurring_invoice_status, recurring_payment_account_id, recurring_payment_currency_code, + recurring_payment_method, recurring_payment_recurring_invoice_id FROM recurring_invoices LEFT JOIN recurring_payments ON recurring_invoice_id = recurring_payment_recurring_invoice_id LEFT JOIN clients ON client_id = recurring_invoice_client_id WHERE recurring_invoice_next_date = CURDATE() @@ -702,7 +728,8 @@ while ($row = mysqli_fetch_assoc($sql_recurring_invoices)) { $new_invoice_id = mysqli_insert_id($mysqli); //Copy Items from original recurring invoice to new invoice - $sql_invoice_items = mysqli_query($mysqli, "SELECT * FROM recurring_invoice_items WHERE item_recurring_invoice_id = $recurring_invoice_id ORDER BY item_id ASC"); + $sql_invoice_items = mysqli_query($mysqli, "SELECT item_description, item_id, item_name, item_order, item_price, item_quantity, item_subtotal, + item_tax, item_tax_id, item_total FROM recurring_invoice_items WHERE item_recurring_invoice_id = $recurring_invoice_id ORDER BY item_id ASC"); while ($row = mysqli_fetch_assoc($sql_invoice_items)) { $item_id = intval($row['item_id']); @@ -734,7 +761,8 @@ while ($row = mysqli_fetch_assoc($sql_recurring_invoices)) { // Get details of the newly generated invoice $sql = mysqli_query( $mysqli, - "SELECT * FROM invoices + "SELECT client_id, client_name, contact_email, contact_name, invoice_amount, invoice_date, + invoice_due, invoice_number, invoice_prefix, invoice_scope, invoice_url_key FROM invoices LEFT JOIN clients ON invoice_client_id = client_id LEFT JOIN contacts ON clients.client_id = contacts.contact_client_id AND contact_primary = 1 WHERE invoice_id = $new_invoice_id" @@ -812,7 +840,7 @@ while ($row = mysqli_fetch_assoc($sql_recurring_invoices)) { } //End Recurring Invoices Loop // Start Flag any active recurring "next run" dates that are in the past -$sql_invalid_recurring_invoices = mysqli_query($mysqli, "SELECT * FROM recurring_invoices WHERE recurring_invoice_next_date < CURDATE() AND recurring_invoice_status = 1"); +$sql_invalid_recurring_invoices = mysqli_query($mysqli, "SELECT recurring_invoice_number, recurring_invoice_prefix FROM recurring_invoices WHERE recurring_invoice_next_date < CURDATE() AND recurring_invoice_status = 1"); while ($row = mysqli_fetch_assoc($sql_invalid_recurring_invoices)) { $invoice_prefix = escapeSql($row['recurring_invoice_prefix']); $invoice_number = intval($row['recurring_invoice_number']); @@ -823,7 +851,10 @@ while ($row = mysqli_fetch_assoc($sql_invalid_recurring_invoices)) { // Start Recurring Payments $sql_recurring_payments = mysqli_query($mysqli, " - SELECT * FROM recurring_payments + SELECT client_id, client_name, contact_email, contact_name, invoice_amount, invoice_currency_code, + invoice_date, invoice_due, invoice_id, invoice_number, invoice_prefix, invoice_scope, + invoice_url_key, recurring_payment_account_id, recurring_payment_currency_code, + recurring_payment_method, recurring_payment_saved_payment_id FROM recurring_payments LEFT JOIN invoices ON invoice_recurring_invoice_id = recurring_payment_recurring_invoice_id LEFT JOIN clients ON client_id = invoice_client_id LEFT JOIN contacts ON client_id = contact_client_id AND contact_primary = 1 @@ -860,7 +891,8 @@ while ($row = mysqli_fetch_assoc($sql_recurring_payments)) { if ($recurring_payment_saved_payment_id) { // Get the saved payment method and provider details $saved_payment = mysqli_fetch_assoc(mysqli_query($mysqli, " - SELECT * FROM client_saved_payment_methods + SELECT payment_provider_account, payment_provider_id, payment_provider_name, + payment_provider_private_key, saved_payment_description, saved_payment_provider_method FROM client_saved_payment_methods LEFT JOIN payment_providers ON saved_payment_provider_id = payment_provider_id WHERE saved_payment_id = $recurring_payment_saved_payment_id AND saved_payment_client_id = $client_id @@ -1066,7 +1098,11 @@ if ($stripe_provider) { // Recurring Expenses // Loop through all recurring expenses that match today's date and is active -$sql_recurring_expenses = mysqli_query($mysqli, "SELECT * FROM recurring_expenses WHERE recurring_expense_next_date = CURDATE() AND recurring_expense_status = 1"); +$sql_recurring_expenses = mysqli_query($mysqli, "SELECT recurring_expense_account_id, recurring_expense_amount, recurring_expense_category_id, + recurring_expense_client_id, recurring_expense_currency_code, recurring_expense_day, + recurring_expense_description, recurring_expense_frequency, recurring_expense_id, + recurring_expense_month, recurring_expense_payment_method, recurring_expense_reference, + recurring_expense_vendor_id FROM recurring_expenses WHERE recurring_expense_next_date = CURDATE() AND recurring_expense_status = 1"); while ($row = mysqli_fetch_assoc($sql_recurring_expenses)) { $recurring_expense_id = intval($row['recurring_expense_id']); @@ -1107,7 +1143,7 @@ while ($row = mysqli_fetch_assoc($sql_recurring_expenses)) { } //End Recurring expenses loop // Flag any active recurring "next run" dates that are in the past -$sql_invalid_recurring_expenses = mysqli_query($mysqli, "SELECT * FROM recurring_expenses WHERE recurring_expense_next_date < CURDATE() AND recurring_expense_status = 1"); +$sql_invalid_recurring_expenses = mysqli_query($mysqli, "SELECT recurring_expense_description FROM recurring_expenses WHERE recurring_expense_next_date < CURDATE() AND recurring_expense_status = 1"); while ($row = mysqli_fetch_assoc($sql_invalid_recurring_expenses)) { $recurring_expense_description = escapeSql($row['recurring_expense_description']); appNotify("Expense", "Recurring expense $recurring_expense_description next run date is in the past!", "/agent/recurring_expenses.php"); diff --git a/cron/ticket_email_parser.php b/cron/ticket_email_parser.php index 95fc84aa4..37fc7f3a2 100644 --- a/cron/ticket_email_parser.php +++ b/cron/ticket_email_parser.php @@ -37,7 +37,7 @@ $config_ticket_from_name = escapeSql($config_ticket_from_name); $config_ticket_email_parse_unknown_senders = intval($row['config_ticket_email_parse_unknown_senders']); // Get company name & phone & timezone -$sql = mysqli_query($mysqli, "SELECT * FROM companies, settings WHERE companies.company_id = settings.company_id AND companies.company_id = 1"); +$sql = mysqli_query($mysqli, "SELECT company_name, company_phone, company_phone_country_code FROM companies, settings WHERE companies.company_id = settings.company_id AND companies.company_id = 1"); $row = mysqli_fetch_assoc($sql); $company_name = escapeSql($row['company_name']); $company_phone = escapeSql(formatPhoneNumber($row['company_phone'], $row['company_phone_country_code'])); @@ -356,7 +356,10 @@ function addReply($from_email, $date, $subject, $ticket_number, $message, $attac mysqli_query($mysqli, "UPDATE tickets SET ticket_status = 2, ticket_resolved_at = NULL WHERE ticket_id = $ticket_id AND ticket_client_id = $client_id LIMIT 1"); resetTicketResolutionSla($ticket_id); - logTicketHistory($ticket_id, "$from_email_esc replied by email, reopening the ticket"); + // Only record the reopen when the ticket was not already open + if (intval($ticket_status) !== 2) { + logTicketHistory($ticket_id, "$from_email_esc replied by email, reopening the ticket"); + } logAudit("Ticket", "Edit", "Email parser: Client contact $from_email_esc updated ticket $config_ticket_prefix$ticket_number_esc ($subject)", $client_id, $ticket_id); triggerCustomAction('ticket_reply_client', $ticket_id); @@ -764,7 +767,7 @@ foreach ($messages as $message) { } else { // Else: check if sender domain is registered $from_domain_esc = mysqli_real_escape_string($mysqli, $from_domain); - $domain_sql = mysqli_query($mysqli, "SELECT * FROM domains WHERE domain_name = '$from_domain_esc' AND domain_archived_at IS NULL LIMIT 1"); + $domain_sql = mysqli_query($mysqli, "SELECT domain_client_id, domain_name FROM domains WHERE domain_name = '$from_domain_esc' AND domain_archived_at IS NULL LIMIT 1"); $domain_row = mysqli_fetch_assoc($domain_sql); if ($domain_row && $from_domain == $domain_row['domain_name']) { @@ -816,7 +819,7 @@ foreach ($messages as $message) { // 4. A known domain? if (!$email_processed) { $from_domain_esc = mysqli_real_escape_string($mysqli, $from_domain); - $domain_sql = mysqli_query($mysqli, "SELECT * FROM domains WHERE domain_name = '$from_domain_esc' AND domain_archived_at IS NULL LIMIT 1"); + $domain_sql = mysqli_query($mysqli, "SELECT domain_client_id, domain_name FROM domains WHERE domain_name = '$from_domain_esc' AND domain_archived_at IS NULL LIMIT 1"); $rowd = mysqli_fetch_assoc($domain_sql); if ($rowd && $from_domain == $rowd['domain_name']) { diff --git a/db.sql b/db.sql index c0bcebd73..e039a630c 100644 --- a/db.sql +++ b/db.sql @@ -50,6 +50,7 @@ CREATE TABLE `ai_models` ( `ai_model_name` varchar(200) NOT NULL, `ai_model_prompt` text DEFAULT NULL, `ai_model_use_case` varchar(200) DEFAULT NULL, + `ai_model_temperature` decimal(3,2) DEFAULT NULL, `ai_model_created_at` datetime NOT NULL DEFAULT current_timestamp(), `ai_model_updated_at` datetime DEFAULT NULL ON UPDATE current_timestamp(), `ai_model_ai_provider_id` int(11) NOT NULL, diff --git a/functions.php b/functions.php index ff70057de..fbea6e270 100644 --- a/functions.php +++ b/functions.php @@ -22,6 +22,7 @@ require_once __DIR__ . '/functions/logging.php'; require_once __DIR__ . '/functions/app.php'; require_once __DIR__ . '/functions/payments.php'; require_once __DIR__ . '/functions/sla.php'; +require_once __DIR__ . '/functions/ai.php'; require_once __DIR__ . '/functions/export.php'; require_once __DIR__ . '/functions/calendar.php'; require_once __DIR__ . '/functions/backup.php'; diff --git a/functions/ai.php b/functions/ai.php new file mode 100644 index 000000000..76e88e94f --- /dev/null +++ b/functions/ai.php @@ -0,0 +1,132 @@ + true, 'content' => '...'] + * ['ok' => false, 'error' => 'short message safe to show the user'] + * + * Provider detail - status code, error type, code and message - goes to the app log + * so a misconfiguration is diagnosable. The API key and the message bodies never do. + */ +function callAiApi($model, $messages) { + + $data = [ + 'model' => $model['ai_model_name'], + 'messages' => $messages, + ]; + + // Only send a temperature when the model has one configured. Newer OpenAI models + // accept nothing but their own default and 400 on anything else, which is what + // the old hardcoded 0.5 / 0.3 ran into. + if (isset($model['ai_model_temperature']) && $model['ai_model_temperature'] !== '') { + $data['temperature'] = floatval($model['ai_model_temperature']); + } + + $ch = curl_init($model['ai_provider_api_url']); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data)); + curl_setopt($ch, CURLOPT_TIMEOUT, AI_REQUEST_TIMEOUT); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'Content-Type: application/json', + 'Authorization: Bearer ' . $model['ai_provider_api_key'], + ]); + + $response = curl_exec($ch); + $status = intval(curl_getinfo($ch, CURLINFO_RESPONSE_CODE)); + $transport_error = curl_error($ch); + curl_close($ch); + + // Enough to tell one provider/model pairing from another in the log + $context = $model['ai_provider_name'] . ' / ' . $model['ai_model_name']; + + if ($response === false) { + logApp('AI', 'error', "$context - could not reach the provider: $transport_error"); + return ['ok' => false, 'error' => 'Could not reach the AI provider.']; + } + + $decoded = json_decode($response, true); + + if ($status < 200 || $status > 299) { + $provider_error = $decoded['error'] ?? []; + $detail = "$context - HTTP $status"; + foreach (['type', 'code', 'param'] as $field) { + if (!empty($provider_error[$field])) { + $detail .= " $field=" . $provider_error[$field]; + } + } + if (!empty($provider_error['message'])) { + $detail .= ' - ' . $provider_error['message']; + } + logApp('AI', 'error', $detail); + return ['ok' => false, 'error' => 'The AI provider rejected the request - see Admin > App Logs.']; + } + + if (!isset($decoded['choices'][0]['message']['content'])) { + logApp('AI', 'error', "$context - HTTP $status but the response carried no choices[0].message.content"); + return ['ok' => false, 'error' => 'The AI provider returned an unexpected response - see Admin > App Logs.']; + } + + return ['ok' => true, 'content' => $decoded['choices'][0]['message']['content']]; +} + +/* + * What to say when nothing is configured for a use case. Logged as well as shown, + * because "no model" and "model rejected the request" look identical from the UI. + */ +function aiModelMissingError($use_case) { + logApp('AI', 'warning', "No AI model configured for use case '$use_case' and no General model to fall back on"); + return "No AI model is configured for $use_case. Add one under Admin > AI Models."; +} diff --git a/functions/app.php b/functions/app.php index 48d039e3c..df45126dc 100644 --- a/functions/app.php +++ b/functions/app.php @@ -100,7 +100,7 @@ function addTasksFromTicketTemplate($ticket_id, $ticket_template_id) { return 0; } - $sql_task_templates = mysqli_query($mysqli, "SELECT * FROM task_templates WHERE task_template_ticket_template_id = $ticket_template_id ORDER BY task_template_order ASC"); + $sql_task_templates = mysqli_query($mysqli, "SELECT task_template_completion_estimate, task_template_name, task_template_order FROM task_templates WHERE task_template_ticket_template_id = $ticket_template_id ORDER BY task_template_order ASC"); $tasks_added = 0; @@ -270,7 +270,7 @@ function getFieldById($table, $id, $field) { function displayFolderOptions($parent_folder_id, $client_id, $indent = 0) { global $mysqli; - $sql_folders = mysqli_query($mysqli, "SELECT * FROM folders WHERE parent_folder = $parent_folder_id AND folder_client_id = $client_id ORDER BY folder_name ASC"); + $sql_folders = mysqli_query($mysqli, "SELECT folder_id, folder_name FROM folders WHERE parent_folder = $parent_folder_id AND folder_client_id = $client_id ORDER BY folder_name ASC"); while ($row = mysqli_fetch_assoc($sql_folders)) { $folder_id = intval($row['folder_id']); $folder_name = escapeHtml($row['folder_name']); @@ -415,8 +415,17 @@ function addToMailQueue($data) { return true; } -function createiCalStr($datetime, $title, $description, $location) { - require_once "libs/zapcal/zapcallib.php"; +function getTicketCalendarUid($ticket_id) { + // An invite and its later cancellation MUST carry the same UID or the + // recipient's calendar client cannot match them up. Derive it from the + // ticket so it is stable across both, rather than from the current time. + $ticket_id = intval($ticket_id); + $host = $_SERVER['SERVER_NAME'] ?? 'itflow'; + return "ticket-$ticket_id@$host"; +} + +function createiCalStr($datetime, $title, $description, $location, $uid = null) { + require_once "../libs/zapcal/zapcallib.php"; // Create the iCal object $cal_event = new ZCiCal(); @@ -431,8 +440,11 @@ function createiCalStr($datetime, $title, $description, $location) { // Todo: adjust this for actual duration $event->addNode(new ZCiCalDataNode("DTEND:" . ZCiCal::fromSqlDateTime($datetime))); $event->addNode(new ZCiCalDataNode("DTSTAMP:" . ZCiCal::fromSqlDateTime())); - $uid = date('Y-m-d-H-i-s') . "@" . $_SERVER['SERVER_NAME']; + if (empty($uid)) { + $uid = date('Y-m-d-H-i-s') . "@" . ($_SERVER['SERVER_NAME'] ?? 'itflow'); + } $event->addNode(new ZCiCalDataNode("UID:" . $uid)); + $event->addNode(new ZCiCalDataNode("SEQUENCE:0")); $event->addNode(new ZCiCalDataNode("LOCATION:" . $location)); $event->addNode(new ZCiCalDataNode("DESCRIPTION:" . $description)); // Todo: add organizer details @@ -442,31 +454,27 @@ function createiCalStr($datetime, $title, $description, $location) { return $cal_event->export(); } -function createiCalStrCancel($originaliCalStr) { - require_once "libs/zapcal/zapcallib.php"; +function createiCalStrCancel($datetime, $title, $uid) { + require_once "../libs/zapcal/zapcallib.php"; - // Import the original iCal string - $cal_event = new ZCiCal($originaliCalStr); + // Build the cancellation fresh. There is no stored copy of the original + // invite to reopen - the match is made by UID, not by the body. + $cal_event = new ZCiCal(); - // Iterate through the iCalendar object to find VEVENT nodes - foreach($cal_event->tree->child as $node) { - if($node->getName() == "VEVENT") { - // Check if STATUS node exists, update it, or add a new one - $statusFound = false; - foreach($node->data as $key => $value) { - if($key == "STATUS") { - $value->setValue("CANCELLED"); - $statusFound = true; - break; // Exit the loop once the STATUS is updated - } - } - // If STATUS node is not found, add a new STATUS node - if (!$statusFound) { - $node->addNode(new ZCiCalDataNode("STATUS:CANCELLED")); - } - } + // METHOD belongs on the VCALENDAR, not on the VEVENT + $cal_event->tree->data['METHOD'] = new ZCiCalDataNode("METHOD:CANCEL"); + + $event = new ZCiCalNode("VEVENT", $cal_event->curnode); + $event->addNode(new ZCiCalDataNode("UID:" . $uid)); + $event->addNode(new ZCiCalDataNode("SUMMARY:" . $title)); + if (!empty($datetime)) { + $event->addNode(new ZCiCalDataNode("DTSTART:" . ZCiCal::fromSqlDateTime($datetime))); + $event->addNode(new ZCiCalDataNode("DTEND:" . ZCiCal::fromSqlDateTime($datetime))); } + $event->addNode(new ZCiCalDataNode("DTSTAMP:" . ZCiCal::fromSqlDateTime())); + // Must outrank the invite's SEQUENCE:0 or clients ignore the cancellation + $event->addNode(new ZCiCalDataNode("SEQUENCE:1")); + $event->addNode(new ZCiCalDataNode("STATUS:CANCELLED")); - // Return the modified iCal string return $cal_event->export(); } diff --git a/functions/auth.php b/functions/auth.php index f25cbb549..d6acd13c7 100644 --- a/functions/auth.php +++ b/functions/auth.php @@ -62,6 +62,42 @@ function enforceUserPermission($module, $check_access_level = 1) { } } +// Client-scope SQL fragment for a list query, built from the signed-in user's allow / deny lists. +// Admin and unrestricted users get no restriction. This is the list-level counterpart to +// enforceClientAccess(), which gates a single record. +// +// Column-aware on purpose: it scopes on the resource's OWN client column rather than a joined +// clients.client_id, so a row with no client (column = 0) is judged on its real value instead of +// becoming NULL through a LEFT JOIN and silently dropping out of the result set. +// +// Returns " AND ..." or "" - append it after a WHERE clause (add "WHERE 1=1" if there isn't one). +function clientScopeSql($column) { + global $session_is_admin, $client_access_array, $client_deny_array; + + if ($session_is_admin) { + return ''; + } + + if (empty($client_access_array) && empty($client_deny_array)) { + return ''; // Unrestricted user - all clients + } + + $sql = ''; + + // 0 is included deliberately: a record with no client isn't any client's data, so a + // restricted user keeps seeing it. This also matches the deny branch below, where 0 + // already passes NOT IN, and the old hand-rolled ticket override that did IN (0,...). + if (!empty($client_access_array)) { + $sql .= " AND $column IN (0," . implode(',', array_map('intval', $client_access_array)) . ")"; + } + + if (!empty($client_deny_array)) { + $sql .= " AND $column NOT IN (" . implode(',', array_map('intval', $client_deny_array)) . ")"; + } + + return $sql; +} + function enforceClientAccess($client_id = null) { global $mysqli, $session_user_id, $session_is_admin, $session_name; diff --git a/functions/export.php b/functions/export.php index d80739f29..c977b6e6c 100644 --- a/functions/export.php +++ b/functions/export.php @@ -348,6 +348,17 @@ function resolveExportFormat($format) { return ($format === 'pdf') ? 'pdf' : 'csv'; } +/* + * The gate every export handler opens on. Keying on isset() alone means any other + * field that happens to share the trigger's name fires the export - the client PDF + * pack's section checkboxes (export_assets=1, export_contacts=1, ...) did exactly + * that, and since post.php loads every handler, the first match won and streamed a + * CSV instead. Only 'csv' or 'pdf' - what renderExportButtons() posts - counts. + */ +function isExportRequest($trigger) { + return isset($_POST[$trigger]) && in_array($_POST[$trigger], ['csv', 'pdf'], true); +} + /* * PDF is capped - see EXPORT_PDF_MAX_ROWS. Call this after the row count is known * and before beginExport(); it redirects rather than returning on refusal. @@ -797,3 +808,107 @@ function finishExport(&$export) { return $export['rows']; } + +/* + * --------------------------------------------------------------------------- + * Client PDF pack + * --------------------------------------------------------------------------- + * + * The sections the client "Export Data" PDF can contain, in document order. + * + * 'label' Checkbox label in the export modal. + * 'icon' Font Awesome class shown beside it. + * 'module' Permission module that owns the section - same mapping the list + * exports use. A role without read access to it never sees the + * checkbox and never gets the section, so one missing module drops a + * section rather than refusing the whole export. + * 'default' Whether the box starts ticked. + * + * The modal renders from this and the handler resolves from it, so a section + * can't be offered by one and ignored by the other. + */ +function getClientPackSections() { + return [ + 'contacts' => ['label' => 'Contacts', 'icon' => 'fa-users', 'module' => 'module_client', 'default' => true], + 'locations' => ['label' => 'Locations', 'icon' => 'fa-map-marker-alt', 'module' => 'module_client', 'default' => true], + 'vendors' => ['label' => 'Vendors', 'icon' => 'fa-building', 'module' => 'module_client', 'default' => true], + 'credentials' => ['label' => 'Credentials', 'icon' => 'fa-key', 'module' => 'module_credential', 'default' => false], + 'assets' => ['label' => 'Assets', 'icon' => 'fa-desktop', 'module' => 'module_support', 'default' => true], + 'software' => ['label' => 'Software / Licenses', 'icon' => 'fa-cube', 'module' => 'module_support', 'default' => true], + 'networks' => ['label' => 'Networks', 'icon' => 'fa-network-wired', 'module' => 'module_support', 'default' => true], + 'domains' => ['label' => 'Domains', 'icon' => 'fa-globe', 'module' => 'module_support', 'default' => true], + 'certificates' => ['label' => 'Certificates', 'icon' => 'fa-lock', 'module' => 'module_support', 'default' => true], + ]; +} + +/* + * Read access per module, resolved once rather than once per section. + */ +function getClientPackSectionAccess() { + $access = []; + foreach (getClientPackSections() as $section) { + if (!isset($access[$section['module']])) { + $access[$section['module']] = lookupUserPermission($section['module']) >= 1; + } + } + return $access; +} + +/* + * Resolves the posted checkboxes against the signed-in role. Returns + * ['contacts' => 1|0, ...]. A section the role can't read is 0 whatever was + * posted, so a hand-rolled POST can't pull in a section the modal never offered. + */ +function resolveClientPackSections() { + + $access = getClientPackSectionAccess(); + + $selected = []; + foreach (getClientPackSections() as $key => $section) { + $selected[$key] = ($access[$section['module']] && !empty($_POST["include_$key"])) ? 1 : 0; + } + + return $selected; +} + +/* + * The section checkboxes for the client PDF pack modal, split across two columns. + * Sections the role can't read are left out entirely rather than shown and then + * silently dropped server side. + */ +function renderClientPackSections() { + + $access = getClientPackSectionAccess(); + + $visible = []; + foreach (getClientPackSections() as $key => $section) { + if ($access[$section['module']]) { + $visible[$key] = $section; + } + } + + $split = (int) ceil(count($visible) / 2); + $index = 0; + + ?> +
+
+ $section) { ?> + +
+
+ +
  • +
    + > + +
    +
  • + + +
    +
    + NOW() LIMIT 1"); + $sql = mysqli_query($mysqli, "SELECT item_active, item_client_id, item_related_id, item_type, item_view_limit, item_views FROM shared_items WHERE item_id = $item_id AND item_key = '$item_key' AND item_expire_at > NOW() LIMIT 1"); $row = mysqli_fetch_assoc($sql); $item_active = intval($row['item_active']); @@ -49,7 +49,7 @@ if (isset($_GET['id']) && isset($_GET['key'])) { } } - $file_sql = mysqli_query($mysqli, "SELECT * FROM files WHERE file_id = $item_related_id AND file_client_id = $client_id LIMIT 1"); + $file_sql = mysqli_query($mysqli, "SELECT file_client_id, file_name, file_reference_name FROM files WHERE file_id = $item_related_id AND file_client_id = $client_id LIMIT 1"); $file_row = mysqli_fetch_assoc($file_sql); if (mysqli_num_rows($file_sql) !== 1 || !$file_row) { diff --git a/guest/guest_pay_invoice_stripe.php b/guest/guest_pay_invoice_stripe.php index 8109a55f7..d32258aa3 100644 --- a/guest/guest_pay_invoice_stripe.php +++ b/guest/guest_pay_invoice_stripe.php @@ -5,7 +5,7 @@ require_once 'includes/inc_all_guest.php'; DEFINE("WORDING_PAYMENT_FAILED", "

    There was an error verifying your payment. Please contact us for more information before attempting payment again.

    "); // --- Get Stripe config from payment_providers table --- -$stripe_provider = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT * FROM payment_providers")); +$stripe_provider = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT payment_provider_account, payment_provider_private_key, payment_provider_public_key FROM payment_providers")); $stripe_publishable = escapeHtml($stripe_provider['payment_provider_public_key']); @@ -21,7 +21,9 @@ if (isset($_GET['invoice_id'], $_GET['url_key']) && !isset($_GET['payment_intent // Query invoice details $sql = mysqli_query( $mysqli, - "SELECT * FROM invoices + "SELECT client_id, client_name, invoice_amount, invoice_currency_code, invoice_date, + invoice_discount_amount, invoice_due, invoice_id, invoice_number, invoice_prefix, + invoice_status FROM invoices LEFT JOIN clients ON invoice_client_id = client_id WHERE invoice_id = $invoice_id AND invoice_url_key = '$invoice_url_key' @@ -54,7 +56,6 @@ if (isset($_GET['invoice_id'], $_GET['url_key']) && !isset($_GET['payment_intent $sql_company = mysqli_query($mysqli, "SELECT * FROM companies WHERE company_id = 1"); $company_row = mysqli_fetch_assoc($sql_company); $company_locale = escapeHtml($company_row['company_locale']); - $config_base_url = escapeHtml($company_row['company_base_url'] ?? ''); // You might want to pull from settings if needed // Add up all payments made to the invoice $sql_amount_paid = mysqli_query($mysqli, "SELECT SUM(payment_amount) AS amount_paid FROM payments WHERE payment_invoice_id = $invoice_id"); @@ -62,7 +63,7 @@ if (isset($_GET['invoice_id'], $_GET['url_key']) && !isset($_GET['payment_intent $balance_to_pay = round($invoice_amount - $amount_paid, 2); // Get invoice items - $sql_invoice_items = mysqli_query($mysqli, "SELECT * FROM invoice_items WHERE item_invoice_id = $invoice_id ORDER BY item_id ASC"); + $sql_invoice_items = mysqli_query($mysqli, "SELECT item_name, item_quantity, item_total FROM invoice_items WHERE item_invoice_id = $invoice_id ORDER BY item_id ASC"); // Currency formatting $currency_format = numfmt_create($company_locale, NumberFormatter::CURRENCY); @@ -182,7 +183,8 @@ if (isset($_GET['invoice_id'], $_GET['url_key']) && !isset($_GET['payment_intent // Get/Check invoice (& client/primary contact) $invoice_sql = mysqli_query( $mysqli, - "SELECT * FROM invoices + "SELECT client_id, client_name, contact_email, contact_name, invoice_amount, invoice_currency_code, + invoice_id, invoice_number, invoice_prefix, invoice_url_key FROM invoices LEFT JOIN clients ON invoice_client_id = client_id LEFT JOIN contacts ON clients.client_id = contacts.contact_client_id AND contact_primary = 1 WHERE invoice_id = $pi_invoice_id @@ -206,7 +208,7 @@ if (isset($_GET['invoice_id'], $_GET['url_key']) && !isset($_GET['payment_intent $contact_name = escapeSql($row['contact_name']); $contact_email = escapeSql($row['contact_email']); - $sql_company = mysqli_query($mysqli, "SELECT * FROM companies WHERE company_id = 1"); + $sql_company = mysqli_query($mysqli, "SELECT company_locale, company_name, company_phone FROM companies WHERE company_id = 1"); $row = mysqli_fetch_assoc($sql_company); $company_name = escapeSql($row['company_name']); $company_phone = escapeSql(formatPhoneNumber($row['company_phone'])); @@ -252,7 +254,8 @@ if (isset($_GET['invoice_id'], $_GET['url_key']) && !isset($_GET['payment_intent mysqli_query($mysqli, "INSERT INTO logs SET log_type = 'Payment', log_action = 'Create', log_description = 'Stripe payment of $pi_currency $pi_amount_paid against invoice $invoice_prefix$invoice_number - $pi_id $extended_log_desc', log_ip = '$ip', log_user_agent = '$user_agent', log_client_id = $pi_client_id"); // Email Receipt - $sql_settings = mysqli_query($mysqli, "SELECT * FROM settings WHERE company_id = 1"); + $sql_settings = mysqli_query($mysqli, "SELECT config_invoice_from_email, config_invoice_from_name, + config_invoice_paid_notification_email, config_smtp_host FROM settings WHERE company_id = 1"); $settings = mysqli_fetch_assoc($sql_settings); $config_smtp_host = $settings['config_smtp_host']; diff --git a/guest/guest_post.php b/guest/guest_post.php index b23a1ae2c..5dda1dce2 100644 --- a/guest/guest_post.php +++ b/guest/guest_post.php @@ -48,7 +48,9 @@ if (isset($_GET['accept_quote'], $_GET['url_key'])) { $row = mysqli_fetch_assoc($sql_company); $company_name = escapeSql($row['company_name']); - $sql_settings = mysqli_query($mysqli, "SELECT * FROM settings WHERE company_id = 1"); + $sql_settings = mysqli_query($mysqli, "SELECT config_quote_from_email, config_quote_from_name, config_quote_notification_email, + config_smtp_encryption, config_smtp_host, config_smtp_password, config_smtp_port, + config_smtp_username FROM settings WHERE company_id = 1"); $row = mysqli_fetch_assoc($sql_settings); $config_smtp_host = $row['config_smtp_host']; $config_smtp_port = intval($row['config_smtp_port']); @@ -121,7 +123,9 @@ if (isset($_GET['decline_quote'], $_GET['url_key'])) { $row = mysqli_fetch_assoc($sql_company); $company_name = escapeSql($row['company_name']); - $sql_settings = mysqli_query($mysqli, "SELECT * FROM settings WHERE company_id = 1"); + $sql_settings = mysqli_query($mysqli, "SELECT config_quote_from_email, config_quote_from_name, config_quote_notification_email, + config_smtp_encryption, config_smtp_host, config_smtp_password, config_smtp_port, + config_smtp_username FROM settings WHERE company_id = 1"); $row = mysqli_fetch_assoc($sql_settings); $config_smtp_host = $row['config_smtp_host']; $config_smtp_port = intval($row['config_smtp_port']); @@ -253,7 +257,8 @@ if (isset($_GET['approve_ticket_task'])) { $approval_id = intval($_GET['approval_id']); $url_key = escapeSql($_GET['approval_url_key']); - $approval_row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT * FROM task_approvals LEFT JOIN tasks on task_id = approval_task_id WHERE approval_id = $approval_id AND approval_task_id = $task_id AND approval_url_key = '$url_key' AND approval_status = 'pending'")); + $approval_row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT approval_created_by, approval_required_user_id, approval_scope, approval_type, task_name, + task_ticket_id FROM task_approvals LEFT JOIN tasks on task_id = approval_task_id WHERE approval_id = $approval_id AND approval_task_id = $task_id AND approval_url_key = '$url_key' AND approval_status = 'pending'")); $task_name = escapeHtml($approval_row['task_name']); $scope = escapeHtml($approval_row['approval_scope']); @@ -287,7 +292,12 @@ if (isset($_GET['export_quote_pdf'])) { $sql = mysqli_query( $mysqli, - "SELECT * FROM quotes + "SELECT client_currency_code, client_id, client_name, client_net_terms, client_website, + contact_email, contact_extension, contact_mobile, contact_mobile_country_code, + contact_phone, contact_phone_country_code, location_address, location_city, + location_country, location_state, location_zip, quote_amount, quote_category_id, + quote_created_at, quote_currency_code, quote_date, quote_discount_amount, quote_expire, + quote_id, quote_note, quote_number, quote_prefix, quote_scope, quote_status, quote_url_key FROM quotes LEFT JOIN clients ON quote_client_id = client_id LEFT JOIN contacts ON clients.client_id = contacts.contact_client_id AND contact_primary = 1 LEFT JOIN locations ON clients.client_id = locations.location_client_id AND location_primary = 1 @@ -332,7 +342,9 @@ if (isset($_GET['export_quote_pdf'])) { $client_net_terms = $config_default_net_terms; } - $sql = mysqli_query($mysqli, "SELECT * FROM companies, settings WHERE companies.company_id = settings.company_id AND companies.company_id = 1"); + $sql = mysqli_query($mysqli, "SELECT company_address, company_city, company_country, company_email, settings.company_id, + company_locale, company_logo, company_name, company_phone, company_phone_country_code, + company_state, company_website, company_zip FROM companies, settings WHERE companies.company_id = settings.company_id AND companies.company_id = 1"); $row = mysqli_fetch_assoc($sql); $company_id = intval($row['company_id']); @@ -423,7 +435,7 @@ if (isset($_GET['export_quote_pdf'])) { $sub_total = 0; $total_tax = 0; - $sql_items = mysqli_query($mysqli, "SELECT * FROM quote_items WHERE item_quote_id = $quote_id ORDER BY item_order ASC"); + $sql_items = mysqli_query($mysqli, "SELECT item_description, item_name, item_price, item_quantity, item_tax, item_total FROM quote_items WHERE item_quote_id = $quote_id ORDER BY item_order ASC"); while ($item = mysqli_fetch_assoc($sql_items)) { $name = $item['item_name']; $desc = $item['item_description']; @@ -488,7 +500,13 @@ if (isset($_GET['export_invoice_pdf'])) { $sql = mysqli_query( $mysqli, - "SELECT * FROM invoices + "SELECT client_currency_code, client_id, client_name, client_net_terms, client_website, + contact_email, contact_extension, contact_mobile, contact_mobile_country_code, + contact_phone, contact_phone_country_code, invoice_amount, invoice_category_id, + invoice_created_at, invoice_currency_code, invoice_date, invoice_discount_amount, + invoice_due, invoice_id, invoice_note, invoice_number, invoice_prefix, invoice_scope, + invoice_status, invoice_url_key, location_address, location_city, location_country, + location_state, location_zip FROM invoices LEFT JOIN clients ON invoice_client_id = client_id LEFT JOIN contacts ON clients.client_id = contacts.contact_client_id AND contact_primary = 1 LEFT JOIN locations ON clients.client_id = locations.location_client_id AND location_primary = 1 @@ -533,7 +551,9 @@ if (isset($_GET['export_invoice_pdf'])) { $client_net_terms = $config_default_net_terms; } - $sql = mysqli_query($mysqli, "SELECT * FROM companies WHERE company_id = 1"); + $sql = mysqli_query($mysqli, "SELECT company_address, company_city, company_country, company_email, company_id, company_locale, + company_logo, company_name, company_phone, company_phone_country_code, company_state, + company_tax_id, company_website, company_zip FROM companies WHERE company_id = 1"); $row = mysqli_fetch_assoc($sql); $company_id = intval($row['company_id']); $company_name = escapeHtml($row['company_name']); @@ -646,7 +666,7 @@ if (isset($_GET['export_invoice_pdf'])) { $sub_total = 0; $total_tax = 0; - $sql_items = mysqli_query($mysqli, "SELECT * FROM invoice_items WHERE item_invoice_id = $invoice_id ORDER BY item_order ASC"); + $sql_items = mysqli_query($mysqli, "SELECT item_description, item_name, item_price, item_quantity, item_tax, item_total FROM invoice_items WHERE item_invoice_id = $invoice_id ORDER BY item_order ASC"); while ($item = mysqli_fetch_assoc($sql_items)) { $name = $item['item_name']; $desc = $item['item_description']; @@ -757,7 +777,7 @@ if (isset($_POST['guest_quote_upload_file'])) { $dest_path = $upload_file_dir . $file_reference_name; // Get/Create a top-level folder called Client Uploads - $folder_sql = mysqli_query($mysqli, "SELECT * FROM folders WHERE folder_name = 'Client Uploads' AND parent_folder = 0 AND folder_client_id = $client_id LIMIT 1"); + $folder_sql = mysqli_query($mysqli, "SELECT folder_id FROM folders WHERE folder_name = 'Client Uploads' AND parent_folder = 0 AND folder_client_id = $client_id LIMIT 1"); if (mysqli_num_rows($folder_sql) == 1) { // Get $row = mysqli_fetch_assoc($folder_sql); diff --git a/guest/guest_view_invoice.php b/guest/guest_view_invoice.php index f430c3d5f..62a03ff31 100644 --- a/guest/guest_view_invoice.php +++ b/guest/guest_view_invoice.php @@ -14,7 +14,12 @@ $invoice_id = intval($_GET['invoice_id']); $sql = mysqli_query( $mysqli, - "SELECT * FROM invoices + "SELECT client_currency_code, client_id, client_name, client_net_terms, client_website, + contact_email, contact_extension, contact_mobile, contact_mobile_country_code, + contact_phone, contact_phone_country_code, invoice_amount, invoice_category_id, + invoice_currency_code, invoice_date, invoice_discount_amount, invoice_due, invoice_id, + invoice_note, invoice_number, invoice_prefix, invoice_status, location_address, + location_city, location_country, location_state, location_zip FROM invoices LEFT JOIN clients ON invoice_client_id = client_id LEFT JOIN locations ON clients.client_id = locations.location_client_id AND location_primary = 1 LEFT JOIN contacts ON clients.client_id = contacts.contact_client_id AND contact_primary = 1 @@ -61,7 +66,9 @@ $client_website = escapeHtml($row['client_website']); $client_currency_code = escapeHtml($row['client_currency_code']); $client_net_terms = intval($row['client_net_terms']); -$sql = mysqli_query($mysqli, "SELECT * FROM companies, settings WHERE companies.company_id = settings.company_id AND companies.company_id = 1"); +$sql = mysqli_query($mysqli, "SELECT company_address, company_city, company_country, company_email, company_locale, + company_logo, company_name, company_phone, company_phone_country_code, company_state, + company_tax_id, company_website, company_zip, config_invoice_footer FROM companies, settings WHERE companies.company_id = settings.company_id AND companies.company_id = 1"); $row = mysqli_fetch_assoc($sql); $company_name = escapeHtml($row['company_name']); @@ -88,7 +95,7 @@ $company_locale = escapeHtml($row['company_locale']); $config_invoice_footer = escapeHtml($row['config_invoice_footer']); // Get Payment Provide Details -$sql = mysqli_query($mysqli, "SELECT * FROM payment_providers WHERE payment_provider_active = 1 LIMIT 1"); +$sql = mysqli_query($mysqli, "SELECT payment_provider_id, payment_provider_name, payment_provider_threshold FROM payment_providers WHERE payment_provider_active = 1 LIMIT 1"); $row = mysqli_fetch_assoc($sql); $payment_provider_id = intval($row['payment_provider_id']); $payment_provider_name = escapeHtml($row['payment_provider_name']); @@ -135,7 +142,7 @@ if ($invoice_status !== "Paid" && $invoice_status !== "Draft" && $invoice_status } // Invoice individual items -$sql_invoice_items = mysqli_query($mysqli, "SELECT * FROM invoice_items WHERE item_invoice_id = $invoice_id ORDER BY item_order ASC"); +$sql_invoice_items = mysqli_query($mysqli, "SELECT item_description, item_id, item_name, item_price, item_quantity, item_tax, item_total FROM invoice_items WHERE item_invoice_id = $invoice_id ORDER BY item_order ASC"); // Get Total Account Balance @@ -358,7 +365,8 @@ if ($balance > 0) { // CURRENT INVOICES -$sql_current_invoices = mysqli_query($mysqli, "SELECT * FROM invoices WHERE invoice_client_id = $client_id AND invoice_due > CURDATE() AND(invoice_status = 'Sent' OR invoice_status = 'Viewed' OR invoice_status = 'Partial') ORDER BY invoice_number DESC"); +$sql_current_invoices = mysqli_query($mysqli, "SELECT invoice_amount, invoice_currency_code, invoice_date, invoice_due, invoice_id, + invoice_number, invoice_prefix, invoice_url_key FROM invoices WHERE invoice_client_id = $client_id AND invoice_due > CURDATE() AND(invoice_status = 'Sent' OR invoice_status = 'Viewed' OR invoice_status = 'Partial') ORDER BY invoice_number DESC"); $current_invoices_count = mysqli_num_rows($sql_current_invoices); @@ -420,7 +428,8 @@ if ($current_invoices_count > 0) { ?> // OUTSTANDING INVOICES -$sql_outstanding_invoices = mysqli_query($mysqli, "SELECT * FROM invoices WHERE invoice_client_id = $client_id AND invoice_due < CURDATE() AND(invoice_status = 'Sent' OR invoice_status = 'Viewed' OR invoice_status = 'Partial') ORDER BY invoice_date DESC"); +$sql_outstanding_invoices = mysqli_query($mysqli, "SELECT invoice_amount, invoice_currency_code, invoice_date, invoice_due, invoice_id, + invoice_number, invoice_prefix, invoice_url_key FROM invoices WHERE invoice_client_id = $client_id AND invoice_due < CURDATE() AND(invoice_status = 'Sent' OR invoice_status = 'Viewed' OR invoice_status = 'Partial') ORDER BY invoice_date DESC"); $outstanding_invoices_count = mysqli_num_rows($sql_outstanding_invoices); diff --git a/guest/guest_view_item.php b/guest/guest_view_item.php index f513c5161..1515f63fd 100644 --- a/guest/guest_view_item.php +++ b/guest/guest_view_item.php @@ -15,7 +15,9 @@ $purifier_config->set('Cache.DefinitionImpl', null); // Disable cache by setting $purifier_config->set('URI.AllowedSchemes', ['data' => true, 'src' => true, 'http' => true, 'https' => true]); $purifier = new HTMLPurifier($purifier_config); -$sql = mysqli_query($mysqli, "SELECT * FROM companies, settings WHERE companies.company_id = settings.company_id AND companies.company_id = 1"); +$sql = mysqli_query($mysqli, "SELECT company_address, company_city, company_email, company_locale, company_logo, company_name, + company_phone, company_phone_country_code, company_state, company_website, company_zip, + config_invoice_footer FROM companies, settings WHERE companies.company_id = settings.company_id AND companies.company_id = 1"); $row = mysqli_fetch_assoc($sql); $company_name = escapeHtml($row['company_name']); @@ -47,7 +49,9 @@ if (!isset($_GET['id']) || !isset($_GET['key'])) { $item_id = intval($_GET['id']); $item_key = escapeSql($_GET['key']); -$sql = mysqli_query($mysqli, "SELECT * FROM shared_items WHERE item_id = $item_id AND item_key = '$item_key' AND item_expire_at > NOW() LIMIT 1"); +$sql = mysqli_query($mysqli, "SELECT item_active, item_client_id, item_created_at, item_encrypted_credential, + item_encrypted_username, item_expire_at, item_note, item_recipient, item_related_id, + item_type, item_view_limit, item_views FROM shared_items WHERE item_id = $item_id AND item_key = '$item_key' AND item_expire_at > NOW() LIMIT 1"); $row = mysqli_fetch_assoc($sql); // Check we got a result @@ -171,7 +175,7 @@ if ($item_type == "Document") { } elseif ($item_type == "Credential") { $encryption_key = $_GET['ek']; - $credential_sql = mysqli_query($mysqli, "SELECT * FROM credentials WHERE credential_id = $item_related_id AND credential_client_id = $client_id LIMIT 1"); + $credential_sql = mysqli_query($mysqli, "SELECT credential_id, credential_name, credential_note, credential_otp_secret, credential_uri FROM credentials WHERE credential_id = $item_related_id AND credential_client_id = $client_id LIMIT 1"); $credential_row = mysqli_fetch_assoc($credential_sql); if (mysqli_num_rows($credential_sql) !== 1 || !$credential_row) { echo "
    Error retrieving login.
    "; diff --git a/guest/guest_view_quote.php b/guest/guest_view_quote.php index f9ce5f4b6..e4363843a 100644 --- a/guest/guest_view_quote.php +++ b/guest/guest_view_quote.php @@ -16,7 +16,12 @@ $quote_id = intval($_GET['quote_id']); $sql = mysqli_query( $mysqli, - "SELECT * FROM quotes + "SELECT client_currency_code, client_id, client_name, client_website, contact_email, + contact_extension, contact_mobile, contact_mobile_country_code, contact_phone, + contact_phone_country_code, location_address, location_city, location_country, + location_state, location_zip, quote_amount, quote_currency_code, quote_date, + quote_discount_amount, quote_expire, quote_id, quote_note, quote_number, quote_prefix, + quote_status FROM quotes LEFT JOIN clients ON quote_client_id = client_id LEFT JOIN contacts ON clients.client_id = contacts.contact_client_id AND contact_primary = 1 LEFT JOIN locations ON clients.client_id = locations.location_client_id AND location_primary = 1 @@ -61,7 +66,9 @@ $contact_mobile = escapeHtml(formatPhoneNumber($row['contact_mobile'], $contact_ $client_website = escapeHtml($row['client_website']); $client_currency_code = escapeHtml($row['client_currency_code']); -$sql = mysqli_query($mysqli, "SELECT * FROM companies, settings WHERE companies.company_id = settings.company_id AND companies.company_id = 1"); +$sql = mysqli_query($mysqli, "SELECT company_address, company_city, company_country, company_email, company_locale, + company_logo, company_name, company_phone, company_phone_country_code, company_state, + company_website, company_zip, config_quote_footer FROM companies, settings WHERE companies.company_id = settings.company_id AND companies.company_id = 1"); $row = mysqli_fetch_assoc($sql); $company_name = escapeHtml($row['company_name']); $company_address = escapeHtml($row['company_address']); @@ -174,7 +181,7 @@ if ($quote_status == "Draft" || $quote_status == "Sent" || $quote_status == "Vie
    - +
    diff --git a/guest/guest_view_ticket.php b/guest/guest_view_ticket.php index 8a5544362..b4e4e4270 100644 --- a/guest/guest_view_ticket.php +++ b/guest/guest_view_ticket.php @@ -147,7 +147,9 @@ if ($ticket_row) {
    { - editor.undoManager.transact(function() { - editor.setContent(data.rewordedText || 'Error: Could not reword the text.'); - }); - editor.setProgressState(false); rewordButtonApi.setEnabled(true); + // Leave the user's text alone if the reword failed + if (data.error || !data.rewordedText) { + editor.notificationManager.open({ + text: data.error || 'Could not reword the text.', + type: 'error', + timeout: 8000 + }); + return; + } + + editor.undoManager.transact(function() { + editor.setContent(data.rewordedText); + }); + editor.notificationManager.open({ text: 'Text reworded successfully!', type: 'success', @@ -245,7 +255,7 @@ $(document).ready(function() { rewordButtonApi.setEnabled(false); editor.setProgressState(true); - fetch('ajax.php?ai_reword', { + fetch('ajax.php?ai_reword&use_case=Tickets', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: content }), @@ -255,11 +265,22 @@ $(document).ready(function() { return response.json(); }) .then(data => { - editor.undoManager.transact(function() { - editor.setContent(data.rewordedText || 'Error: Could not reword the text.'); - }); editor.setProgressState(false); rewordButtonApi.setEnabled(true); + + // Leave the user's text alone if the reword failed + if (data.error || !data.rewordedText) { + editor.notificationManager.open({ + text: data.error || 'Could not reword the text.', + type: 'error', + timeout: 8000 + }); + return; + } + + editor.undoManager.transact(function() { + editor.setContent(data.rewordedText); + }); editor.notificationManager.open({ text: 'Text reworded successfully!', type: 'success', diff --git a/post/misc.php b/post/misc.php index 492545ff8..321bf92f4 100644 --- a/post/misc.php +++ b/post/misc.php @@ -39,7 +39,7 @@ if (isset($_GET['dismiss_all_notifications'])) { validateCSRFToken(); - $sql = mysqli_query($mysqli,"SELECT * FROM notifications WHERE notification_user_id = $session_user_id AND notification_dismissed_at IS NULL"); + $sql = mysqli_query($mysqli,"SELECT notification_dismissed_at, notification_id FROM notifications WHERE notification_user_id = $session_user_id AND notification_dismissed_at IS NULL"); $num_notifications = mysqli_num_rows($sql); diff --git a/scripts/setup_cli.php b/scripts/setup_cli.php index fe00d8e1f..7cbf9f024 100644 --- a/scripts/setup_cli.php +++ b/scripts/setup_cli.php @@ -301,7 +301,8 @@ if (!$non_interactive) { echo "Any comments to include? Press Enter if none: "; $comments = trim(fgets(STDIN)); - $sql = mysqli_query($mysqli,"SELECT * FROM companies WHERE company_id = 1"); + $sql = mysqli_query($mysqli,"SELECT company_city, company_country, company_currency, company_name, company_state, + company_website FROM companies WHERE company_id = 1"); $row = mysqli_fetch_assoc($sql); $company_name_db = $row['company_name']; $website_db = $row['company_website']; diff --git a/setup/index.php b/setup/index.php index 03e5ddc5e..72d4f8dad 100644 --- a/setup/index.php +++ b/setup/index.php @@ -15,22 +15,25 @@ $can_show_restore = false; $should_skip_to_user = false; /* - * An install with users in it is a live install, and setup is closed on one whatever - * config.php says. + * How far through the wizard this install already is. Each step is a separate question + * because each one guards a different handler below - answering all of them with "are there + * users?" is what used to lock the wizard out three steps early. * - * This used to default $config_enable_setup to 1 when the flag was absent, which fails the - * wrong way: config.php is written when the database step completes but the flag is only - * appended at the very end of a successful run, so an install abandoned in between - or one - * where that final append failed - left the restore below reachable with no authentication - * at all. That endpoint drops every table and imports whatever archive it is handed, and it - * rewrites the uploads directory, including the .htaccess that stops PHP running there. + * $install_is_live - users exist. A restore would destroy real data, so restore closes + * and points at the CLI. One user row is enough. + * $company_exists - the company step has run (companies row, and settings seeded). + * $localization_done - the localization step has run (company_locale filled in). */ $install_is_live = false; +$company_exists = false; +$localization_done = false; +$resume_step = 'checks'; if (file_exists("../config.php") && $mysqli_available) { $table_result = mysqli_query($mysqli, "SHOW TABLES LIKE 'users'"); if ($table_result && mysqli_num_rows($table_result) > 0) { $should_skip_to_user = true; + $resume_step = 'user'; $user_count_result = mysqli_query($mysqli, "SELECT COUNT(*) AS user_count FROM users"); if ($user_count_result) { @@ -44,6 +47,26 @@ if (file_exists("../config.php") && $mysqli_available) { } } + if ($install_is_live) { + $resume_step = 'company'; + + $company_result = mysqli_query($mysqli, "SELECT company_locale FROM companies WHERE company_id = 1"); + if (!$company_result) { + // Cannot prove either step is outstanding, so treat both as done + $company_exists = true; + $localization_done = true; + $resume_step = 'telemetry'; + } elseif ($company_row = mysqli_fetch_assoc($company_result)) { + $company_exists = true; + $resume_step = 'localization'; + + if (trim($company_row['company_locale'] ?? '') !== '') { + $localization_done = true; + $resume_step = 'telemetry'; + } + } + } + // Restore needs a database connection and an empty install. A populated one restores // from the command line instead - scripts/restore_cli.php. if (!$install_is_live) { @@ -54,11 +77,27 @@ if (file_exists("../config.php") && $mysqli_available) { } } +/* + * config.php is written when the database step completes, but $config_enable_setup is only + * appended to it by the LAST step, so the flag is absent for the whole middle of an install + * and the wizard has to stay open across that gap or it cannot be finished. + * + * Deriving the flag from the database instead - closing setup as soon as the install looked + * "live" - is what stranded people: the first user made it live, three steps before there + * were companies or settings rows, and /setup and /login.php then redirected at each other + * until the browser gave up. Deriving it from any later step has the same shape, because the + * step that writes the flag is behind the gate that reads it. + * + * So the page stays open until the flag says otherwise, and each handler below refuses to run + * a second time on its own. That keeps the reason the derived flag was added in the first + * place - the restore handler drops every table, imports whatever archive it is handed and + * rewrites the uploads directory - without the page-level gate that came with it. + */ if (!isset($config_enable_setup)) { - $config_enable_setup = $install_is_live ? 0 : 1; + $config_enable_setup = 1; } -if ($config_enable_setup == 0 || $install_is_live) { +if ($config_enable_setup == 0) { header("Location: /login.php"); exit; } @@ -246,8 +285,12 @@ if (isset($_POST['restore'])) { } if (isset($_POST['add_user'])) { - $user_count = mysqli_num_rows(mysqli_query($mysqli,"SELECT COUNT(*) FROM users")); - if ($user_count < 0) { + + // SELECT COUNT(*) returns exactly one row whatever the count is, so the mysqli_num_rows() + // test this replaces was always 1 and never fired: a resubmitted form created a second + // user and then died on the duplicate user_settings row. $install_is_live is the same + // count, taken at the top of the file, and it fails closed. + if ($install_is_live) { $_SESSION['alert_message'] = "Users already exist in the database. Clear them to reconfigure here."; header("Location: ?company"); exit; @@ -265,7 +308,10 @@ if (isset($_POST['add_user'])) { mysqli_query($mysqli,"INSERT INTO users SET user_name = '$name', user_email = '$email', user_password = '$password', user_specific_encryption_ciphertext = '$user_specific_encryption_ciphertext', user_role_id = 3"); - mkdirMissing("../uploads/users/1"); + // Normally 1, but the table's AUTO_INCREMENT can already have moved on, so ask for it. + $user_id = intval(mysqli_insert_id($mysqli)); + + mkdirMissing("../uploads/users/$user_id"); //Check to see if a file is attached if ($_FILES['file']['tmp_name'] != '') { @@ -295,13 +341,13 @@ if (isset($_POST['add_user'])) { if ($file_error == 0) { // directory in which the uploaded file will be moved - $upload_file_dir = "../uploads/users/1/"; + $upload_file_dir = "../uploads/users/$user_id/"; $dest_path = $upload_file_dir . $new_file_name; move_uploaded_file($file_tmp_path, $dest_path); //Set Avatar - mysqli_query($mysqli,"UPDATE users SET user_avatar = '$new_file_name' WHERE user_id = 1"); + mysqli_query($mysqli,"UPDATE users SET user_avatar = '$new_file_name' WHERE user_id = $user_id"); $_SESSION['alert_message'] = 'File successfully uploaded.'; } else { @@ -311,7 +357,7 @@ if (isset($_POST['add_user'])) { } //Create Settings - mysqli_query($mysqli,"INSERT INTO user_settings SET user_id = 1"); + mysqli_query($mysqli,"INSERT INTO user_settings SET user_id = $user_id"); $_SESSION['alert_message'] = "User $name created"; @@ -322,6 +368,13 @@ if (isset($_POST['add_user'])) { if (isset($_POST['add_company_settings'])) { + // Run once. A second pass would add a second companies row and re-seed the defaults. + if ($company_exists) { + $_SESSION['alert_message'] = "Company details have already been saved."; + header("Location: ?localization"); + exit; + } + $name = escapeSql($_POST['name']); $country = escapeSql($_POST['country']); $address = escapeSql($_POST['address']); @@ -388,6 +441,13 @@ if (isset($_POST['add_company_settings'])) { if (isset($_POST['add_localization_settings'])) { + // Run once. A second pass would add a second Cash account. + if ($localization_done) { + $_SESSION['alert_message'] = "Localization has already been saved."; + header("Location: ?telemetry"); + exit; + } + $locale = escapeSql($_POST['locale']); $currency_code = escapeSql($_POST['currency_code']); $timezone = escapeSql($_POST['timezone']); @@ -414,7 +474,8 @@ if (isset($_POST['add_telemetry'])) { $comments = escapeSql($_POST['comments']); - $sql = mysqli_query($mysqli,"SELECT * FROM companies WHERE company_id = 1"); + $sql = mysqli_query($mysqli,"SELECT company_city, company_country, company_currency, company_name, company_state, + company_website FROM companies WHERE company_id = 1"); $row = mysqli_fetch_assoc($sql); $company_name = $row['company_name']; @@ -1047,6 +1108,14 @@ if (isset($_POST['add_telemetry'])) {
    + + +

    This install already has a user - the rest of your team is added from Admin > Users once you are logged in.

    +
    + Continue Setup + + +
    @@ -1090,6 +1159,9 @@ if (isset($_POST['add_telemetry'])) { + + +
    @@ -1100,6 +1172,14 @@ if (isset($_POST['add_telemetry'])) {

    Step 4 - Company Details

    + + + +

    Company details have already been saved - they can be changed later from Admin > Settings.

    +
    + Continue Setup + +
    @@ -1219,6 +1299,9 @@ if (isset($_POST['add_telemetry'])) { + + +
    @@ -1229,6 +1312,14 @@ if (isset($_POST['add_telemetry'])) {

    Step 5 - Region and Language

    + + + +

    Localization has already been saved - it can be changed later from Admin > Settings.

    +
    + Continue Setup + +
    @@ -1283,6 +1374,9 @@ if (isset($_POST['add_telemetry'])) { + + +
    @@ -1355,7 +1449,11 @@ if (isset($_POST['add_telemetry'])) {
  • Don't hesitate to reach out on the forums if you need any assistance
  • Apache/PHP Error log:
  • -

    A database must be created before proceeding - click on the button below to get started.

    + +

    This install was left part-way through setup - click on the button below to pick up where it stopped.

    + +

    A database must be created before proceeding - click on the button below to get started.

    +

    ITFlow is free software: you can redistribute and/or modify it under the terms of the GNU General Public License.
    It is distributed in the hope that it will be useful, but without any warranty; without even the implied warranty of merchantability or fitness for a particular purpose.


    - + + + Continue Setup + + Create First User