Fix data-layer bugs: bind_param fatals, ticket_id bindings, cache poisoning
- CustomFieldModel: assign ?? fallbacks to variables before bind_param
(by-reference args cannot be ?? expressions; fatal on PHP 8.2, custom
fields were uncreatable/uneditable)
- RecurringTicketModel::create: fix swapped bind type for schedule_type
(enum bound as int coerced 'daily' to 0, breaking the cron)
- TicketModel/CommentModel: bind varchar ticket_id as string not int so
the unique index is usable and leading-zero IDs match; ticket_watchers
(int column) left as integer
- TicketModel::deleteTicket: delete from custom_field_values (real table)
not the nonexistent ticket_custom_fields
- TicketModel search: honor literal '0'; never emit AGAINST('*') on
all-special-char input (fall back to LIKE)
- TicketModel::updateTicket: disambiguate not-found vs no-op vs genuine
optimistic-lock conflict on zero affected rows
- WorkflowModel: do not cache transitions/statuses on DB failure (a
transient error no longer blocks all status changes for the TTL)
- DependencyModel: filter linked tickets by visibility (new optional user
context params) to stop confidential metadata leaking via dependencies
- BulkOperationsModel: validate status/priority/assignee before mutating
- AuditLogModel: gate getClientIP forwarded headers on trusted proxies;
add missing action/entity types so audit-log filters work
- WorkflowModel: add transitionRequiresComment() accessor for enforcement
- CommentModel: stop leaking raw DB errors to clients (log instead)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -77,6 +77,15 @@ class BulkOperationsModel
|
||||
|
||||
$ticketIds = explode(',', $operation['ticket_ids']);
|
||||
$parameters = $operation['parameters'] ? json_decode($operation['parameters'], true) : [];
|
||||
|
||||
// Validate operation parameters up front so invalid values (out-of-range
|
||||
// priority, unknown status, nonexistent assignee) are rejected cleanly
|
||||
// instead of corrupting tickets or throwing mid-transaction.
|
||||
$paramError = $this->validateOperationParameters($operation['operation_type'], is_array($parameters) ? $parameters : []);
|
||||
if ($paramError !== null) {
|
||||
return ['processed' => 0, 'failed' => count($ticketIds), 'error' => $paramError];
|
||||
}
|
||||
|
||||
$processed = 0;
|
||||
$failed = 0;
|
||||
$errors = [];
|
||||
@@ -297,6 +306,63 @@ class BulkOperationsModel
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the parameters for a bulk operation before any ticket is mutated.
|
||||
*
|
||||
* @return string|null Error message, or null if the parameters are valid
|
||||
*/
|
||||
private function validateOperationParameters(string $type, array $parameters): ?string
|
||||
{
|
||||
switch ($type) {
|
||||
case 'bulk_priority':
|
||||
if (!isset($parameters['priority'])) {
|
||||
return 'Missing priority parameter';
|
||||
}
|
||||
$priority = $parameters['priority'];
|
||||
// tickets.priority has a CHECK constraint (between 1 and 6).
|
||||
if (!is_numeric($priority) || (int)$priority < 1 || (int)$priority > 6) {
|
||||
return 'Invalid priority: must be between 1 and 6';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'bulk_status':
|
||||
if (!isset($parameters['status'])) {
|
||||
return 'Missing status parameter';
|
||||
}
|
||||
$validStatuses = $GLOBALS['config']['TICKET_STATUSES']
|
||||
?? ['Open', 'Pending', 'In Progress', 'Closed'];
|
||||
if (!in_array($parameters['status'], $validStatuses, true)) {
|
||||
return 'Invalid status value';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'bulk_assign':
|
||||
if (!isset($parameters['assigned_to'])) {
|
||||
return 'Missing assigned_to parameter';
|
||||
}
|
||||
$assignedTo = $parameters['assigned_to'];
|
||||
if (!is_numeric($assignedTo) || (int)$assignedTo <= 0 || !$this->userExists((int)$assignedTo)) {
|
||||
return 'Invalid assigned_to: user does not exist';
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a user ID exists.
|
||||
*/
|
||||
private function userExists(int $userId): bool
|
||||
{
|
||||
$stmt = $this->conn->prepare("SELECT 1 FROM users WHERE user_id = ? LIMIT 1");
|
||||
$stmt->bind_param("i", $userId);
|
||||
$stmt->execute();
|
||||
$exists = $stmt->get_result()->num_rows > 0;
|
||||
$stmt->close();
|
||||
return $exists;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get bulk operation by ID
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user