style: auto-fix 1340 phpcs PSR-12 violations via phpcbf; exclude MissingNamespace and SideEffects
This commit is contained in:
+21
-10
@@ -1,11 +1,14 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* ApiKeyModel - Handles API key generation and validation
|
||||
*/
|
||||
class ApiKeyModel {
|
||||
class ApiKeyModel
|
||||
{
|
||||
private $conn;
|
||||
|
||||
public function __construct($conn) {
|
||||
public function __construct($conn)
|
||||
{
|
||||
$this->conn = $conn;
|
||||
}
|
||||
|
||||
@@ -17,7 +20,8 @@ class ApiKeyModel {
|
||||
* @param int|null $expiresInDays Number of days until expiration (null for no expiration)
|
||||
* @return array Array with 'success', 'api_key' (plaintext), 'key_prefix', 'error'
|
||||
*/
|
||||
public function createKey($keyName, $createdBy, $expiresInDays = null) {
|
||||
public function createKey($keyName, $createdBy, $expiresInDays = null)
|
||||
{
|
||||
// Generate random API key (32 bytes = 64 hex characters)
|
||||
$apiKey = bin2hex(random_bytes(32));
|
||||
|
||||
@@ -67,7 +71,8 @@ class ApiKeyModel {
|
||||
* @param string $apiKey Plaintext API key to validate
|
||||
* @return array|null API key record if valid, null if invalid
|
||||
*/
|
||||
public function validateKey($apiKey) {
|
||||
public function validateKey($apiKey)
|
||||
{
|
||||
if (empty($apiKey)) {
|
||||
return null;
|
||||
}
|
||||
@@ -111,7 +116,8 @@ class ApiKeyModel {
|
||||
* @param int $keyId API key ID
|
||||
* @return bool Success status
|
||||
*/
|
||||
private function updateLastUsed($keyId) {
|
||||
private function updateLastUsed($keyId)
|
||||
{
|
||||
$stmt = $this->conn->prepare("UPDATE api_keys SET last_used = NOW() WHERE api_key_id = ?");
|
||||
$stmt->bind_param("i", $keyId);
|
||||
$success = $stmt->execute();
|
||||
@@ -125,7 +131,8 @@ class ApiKeyModel {
|
||||
* @param int $keyId API key ID
|
||||
* @return bool Success status
|
||||
*/
|
||||
public function revokeKey($keyId) {
|
||||
public function revokeKey($keyId)
|
||||
{
|
||||
$stmt = $this->conn->prepare("UPDATE api_keys SET is_active = 0 WHERE api_key_id = ?");
|
||||
$stmt->bind_param("i", $keyId);
|
||||
$success = $stmt->execute();
|
||||
@@ -139,7 +146,8 @@ class ApiKeyModel {
|
||||
* @param int $keyId API key ID
|
||||
* @return bool Success status
|
||||
*/
|
||||
public function deleteKey($keyId) {
|
||||
public function deleteKey($keyId)
|
||||
{
|
||||
$stmt = $this->conn->prepare("DELETE FROM api_keys WHERE api_key_id = ?");
|
||||
$stmt->bind_param("i", $keyId);
|
||||
$success = $stmt->execute();
|
||||
@@ -152,7 +160,8 @@ class ApiKeyModel {
|
||||
*
|
||||
* @return array Array of API key records (without hashes)
|
||||
*/
|
||||
public function getAllKeys() {
|
||||
public function getAllKeys()
|
||||
{
|
||||
$stmt = $this->conn->prepare(
|
||||
"SELECT ak.*, u.username, u.display_name
|
||||
FROM api_keys ak
|
||||
@@ -179,7 +188,8 @@ class ApiKeyModel {
|
||||
* @param int $keyId API key ID
|
||||
* @return array|null API key record (without hash) or null if not found
|
||||
*/
|
||||
public function getKeyById($keyId) {
|
||||
public function getKeyById($keyId)
|
||||
{
|
||||
$stmt = $this->conn->prepare(
|
||||
"SELECT ak.*, u.username, u.display_name
|
||||
FROM api_keys ak
|
||||
@@ -208,7 +218,8 @@ class ApiKeyModel {
|
||||
* @param int $userId User ID
|
||||
* @return array Array of API key records
|
||||
*/
|
||||
public function getKeysByUser($userId) {
|
||||
public function getKeysByUser($userId)
|
||||
{
|
||||
$stmt = $this->conn->prepare(
|
||||
"SELECT * FROM api_keys WHERE created_by = ? ORDER BY created_at DESC"
|
||||
);
|
||||
|
||||
+25
-13
@@ -1,19 +1,23 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* AttachmentModel - Handles ticket file attachments
|
||||
*/
|
||||
|
||||
class AttachmentModel {
|
||||
class AttachmentModel
|
||||
{
|
||||
private $conn;
|
||||
|
||||
public function __construct($conn) {
|
||||
public function __construct($conn)
|
||||
{
|
||||
$this->conn = $conn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all attachments for a ticket
|
||||
*/
|
||||
public function getAttachments($ticketId) {
|
||||
public function getAttachments($ticketId)
|
||||
{
|
||||
$sql = "SELECT a.*, u.username, u.display_name
|
||||
FROM ticket_attachments a
|
||||
LEFT JOIN users u ON a.uploaded_by = u.user_id
|
||||
@@ -37,7 +41,8 @@ class AttachmentModel {
|
||||
/**
|
||||
* Get a single attachment by ID
|
||||
*/
|
||||
public function getAttachment($attachmentId) {
|
||||
public function getAttachment($attachmentId)
|
||||
{
|
||||
$sql = "SELECT a.*, u.username, u.display_name
|
||||
FROM ticket_attachments a
|
||||
LEFT JOIN users u ON a.uploaded_by = u.user_id
|
||||
@@ -56,7 +61,8 @@ class AttachmentModel {
|
||||
/**
|
||||
* Add a new attachment record
|
||||
*/
|
||||
public function addAttachment($ticketId, $filename, $originalFilename, $fileSize, $mimeType, $uploadedBy) {
|
||||
public function addAttachment($ticketId, $filename, $originalFilename, $fileSize, $mimeType, $uploadedBy)
|
||||
{
|
||||
$sql = "INSERT INTO ticket_attachments (ticket_id, filename, original_filename, file_size, mime_type, uploaded_by)
|
||||
VALUES (?, ?, ?, ?, ?, ?)";
|
||||
|
||||
@@ -77,7 +83,8 @@ class AttachmentModel {
|
||||
/**
|
||||
* Delete an attachment record
|
||||
*/
|
||||
public function deleteAttachment($attachmentId) {
|
||||
public function deleteAttachment($attachmentId)
|
||||
{
|
||||
$sql = "DELETE FROM ticket_attachments WHERE attachment_id = ?";
|
||||
|
||||
$stmt = $this->conn->prepare($sql);
|
||||
@@ -91,7 +98,8 @@ class AttachmentModel {
|
||||
/**
|
||||
* Get total attachment size for a ticket
|
||||
*/
|
||||
public function getTotalSizeForTicket($ticketId) {
|
||||
public function getTotalSizeForTicket($ticketId)
|
||||
{
|
||||
$sql = "SELECT COALESCE(SUM(file_size), 0) as total_size
|
||||
FROM ticket_attachments
|
||||
WHERE ticket_id = ?";
|
||||
@@ -109,7 +117,8 @@ class AttachmentModel {
|
||||
/**
|
||||
* Get attachment count for a ticket
|
||||
*/
|
||||
public function getAttachmentCount($ticketId) {
|
||||
public function getAttachmentCount($ticketId)
|
||||
{
|
||||
$sql = "SELECT COUNT(*) as count FROM ticket_attachments WHERE ticket_id = ?";
|
||||
|
||||
$stmt = $this->conn->prepare($sql);
|
||||
@@ -125,7 +134,8 @@ class AttachmentModel {
|
||||
/**
|
||||
* Check if user can delete attachment (owner or admin)
|
||||
*/
|
||||
public function canUserDelete($attachmentId, $userId, $isAdmin = false) {
|
||||
public function canUserDelete($attachmentId, $userId, $isAdmin = false)
|
||||
{
|
||||
if ($isAdmin) {
|
||||
return true;
|
||||
}
|
||||
@@ -137,7 +147,8 @@ class AttachmentModel {
|
||||
/**
|
||||
* Format file size for display
|
||||
*/
|
||||
public static function formatFileSize($bytes) {
|
||||
public static function formatFileSize($bytes)
|
||||
{
|
||||
if ($bytes >= 1073741824) {
|
||||
return number_format($bytes / 1073741824, 2) . ' GB';
|
||||
} elseif ($bytes >= 1048576) {
|
||||
@@ -152,7 +163,8 @@ class AttachmentModel {
|
||||
/**
|
||||
* Get file icon based on mime type
|
||||
*/
|
||||
public static function getFileIcon($mimeType) {
|
||||
public static function getFileIcon($mimeType)
|
||||
{
|
||||
if (strpos($mimeType, 'image/') === 0) {
|
||||
return '🖼️';
|
||||
} elseif (strpos($mimeType, 'video/') === 0) {
|
||||
@@ -177,7 +189,8 @@ class AttachmentModel {
|
||||
/**
|
||||
* Validate file type against allowed types
|
||||
*/
|
||||
public static function isAllowedType($mimeType) {
|
||||
public static function isAllowedType($mimeType)
|
||||
{
|
||||
$allowedTypes = $GLOBALS['config']['ALLOWED_FILE_TYPES'] ?? [
|
||||
'image/jpeg', 'image/png', 'image/gif', 'image/webp',
|
||||
'application/pdf',
|
||||
@@ -192,5 +205,4 @@ class AttachmentModel {
|
||||
|
||||
return in_array($mimeType, $allowedTypes);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+55
-27
@@ -1,8 +1,10 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* AuditLogModel - Handles audit trail logging for all user actions
|
||||
*/
|
||||
class AuditLogModel {
|
||||
class AuditLogModel
|
||||
{
|
||||
private $conn;
|
||||
|
||||
/** @var int Maximum allowed limit for pagination */
|
||||
@@ -23,7 +25,8 @@ class AuditLogModel {
|
||||
'template', 'attachment', 'group'
|
||||
];
|
||||
|
||||
public function __construct($conn) {
|
||||
public function __construct($conn)
|
||||
{
|
||||
$this->conn = $conn;
|
||||
}
|
||||
|
||||
@@ -33,7 +36,8 @@ class AuditLogModel {
|
||||
* @param int $limit Requested limit
|
||||
* @return int Validated limit
|
||||
*/
|
||||
private function validateLimit(int $limit): int {
|
||||
private function validateLimit(int $limit): int
|
||||
{
|
||||
if ($limit < 1) {
|
||||
return self::DEFAULT_LIMIT;
|
||||
}
|
||||
@@ -46,7 +50,8 @@ class AuditLogModel {
|
||||
* @param int $offset Requested offset
|
||||
* @return int Validated offset (non-negative)
|
||||
*/
|
||||
private function validateOffset(int $offset): int {
|
||||
private function validateOffset(int $offset): int
|
||||
{
|
||||
return max(0, $offset);
|
||||
}
|
||||
|
||||
@@ -56,7 +61,8 @@ class AuditLogModel {
|
||||
* @param string $date Date string
|
||||
* @return string|null Validated date or null if invalid
|
||||
*/
|
||||
private function validateDate(string $date): ?string {
|
||||
private function validateDate(string $date): ?string
|
||||
{
|
||||
// Check format
|
||||
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
|
||||
return null;
|
||||
@@ -77,7 +83,8 @@ class AuditLogModel {
|
||||
* @param string $actionType Action type to validate
|
||||
* @return bool True if valid
|
||||
*/
|
||||
private function isValidActionType(string $actionType): bool {
|
||||
private function isValidActionType(string $actionType): bool
|
||||
{
|
||||
return in_array($actionType, self::VALID_ACTION_TYPES, true);
|
||||
}
|
||||
|
||||
@@ -87,7 +94,8 @@ class AuditLogModel {
|
||||
* @param string $entityType Entity type to validate
|
||||
* @return bool True if valid
|
||||
*/
|
||||
private function isValidEntityType(string $entityType): bool {
|
||||
private function isValidEntityType(string $entityType): bool
|
||||
{
|
||||
return in_array($entityType, self::VALID_ENTITY_TYPES, true);
|
||||
}
|
||||
|
||||
@@ -102,7 +110,8 @@ class AuditLogModel {
|
||||
* @param string|null $ipAddress IP address of the user
|
||||
* @return bool Success status
|
||||
*/
|
||||
public function log($userId, $actionType, $entityType, $entityId = null, $details = null, $ipAddress = null) {
|
||||
public function log($userId, $actionType, $entityType, $entityId = null, $details = null, $ipAddress = null)
|
||||
{
|
||||
// Convert details array to JSON
|
||||
$detailsJson = null;
|
||||
if ($details !== null) {
|
||||
@@ -134,7 +143,8 @@ class AuditLogModel {
|
||||
* @param int $limit Maximum number of logs to return
|
||||
* @return array Array of audit log records
|
||||
*/
|
||||
public function getLogsByEntity($entityType, $entityId, $limit = 100) {
|
||||
public function getLogsByEntity($entityType, $entityId, $limit = 100)
|
||||
{
|
||||
$limit = $this->validateLimit((int)$limit);
|
||||
|
||||
$stmt = $this->conn->prepare(
|
||||
@@ -169,7 +179,8 @@ class AuditLogModel {
|
||||
* @param int $limit Maximum number of logs to return
|
||||
* @return array Array of audit log records
|
||||
*/
|
||||
public function getLogsByUser($userId, $limit = 100) {
|
||||
public function getLogsByUser($userId, $limit = 100)
|
||||
{
|
||||
$limit = $this->validateLimit((int)$limit);
|
||||
$userId = max(0, (int)$userId);
|
||||
|
||||
@@ -205,7 +216,8 @@ class AuditLogModel {
|
||||
* @param int $offset Offset for pagination
|
||||
* @return array Array of audit log records
|
||||
*/
|
||||
public function getRecentLogs($limit = 50, $offset = 0) {
|
||||
public function getRecentLogs($limit = 50, $offset = 0)
|
||||
{
|
||||
$limit = $this->validateLimit((int)$limit);
|
||||
$offset = $this->validateOffset((int)$offset);
|
||||
|
||||
@@ -240,7 +252,8 @@ class AuditLogModel {
|
||||
* @param int $limit Maximum number of logs to return
|
||||
* @return array Array of audit log records
|
||||
*/
|
||||
public function getLogsByAction($actionType, $limit = 100) {
|
||||
public function getLogsByAction($actionType, $limit = 100)
|
||||
{
|
||||
$limit = $this->validateLimit((int)$limit);
|
||||
|
||||
// Validate action type to prevent unexpected queries
|
||||
@@ -278,7 +291,8 @@ class AuditLogModel {
|
||||
*
|
||||
* @return int Total count
|
||||
*/
|
||||
public function getTotalCount() {
|
||||
public function getTotalCount()
|
||||
{
|
||||
$result = $this->conn->query("SELECT COUNT(*) as count FROM audit_log");
|
||||
$row = $result->fetch_assoc();
|
||||
return (int)$row['count'];
|
||||
@@ -290,7 +304,8 @@ class AuditLogModel {
|
||||
* @param int $daysToKeep Number of days of logs to keep
|
||||
* @return int Number of deleted records
|
||||
*/
|
||||
public function deleteOldLogs($daysToKeep = 90) {
|
||||
public function deleteOldLogs($daysToKeep = 90)
|
||||
{
|
||||
$stmt = $this->conn->prepare(
|
||||
"DELETE FROM audit_log WHERE created_at < DATE_SUB(NOW(), INTERVAL ? DAY)"
|
||||
);
|
||||
@@ -307,7 +322,8 @@ class AuditLogModel {
|
||||
*
|
||||
* @return string Client IP address
|
||||
*/
|
||||
private function getClientIP() {
|
||||
private function getClientIP()
|
||||
{
|
||||
$ipAddress = '';
|
||||
|
||||
// Check for proxy headers
|
||||
@@ -336,7 +352,8 @@ class AuditLogModel {
|
||||
* @param array $ticketData Ticket data
|
||||
* @return bool Success status
|
||||
*/
|
||||
public function logTicketCreate($userId, $ticketId, $ticketData) {
|
||||
public function logTicketCreate($userId, $ticketId, $ticketData)
|
||||
{
|
||||
return $this->log(
|
||||
$userId,
|
||||
'create',
|
||||
@@ -354,7 +371,8 @@ class AuditLogModel {
|
||||
* @param array $changes Array of changed fields
|
||||
* @return bool Success status
|
||||
*/
|
||||
public function logTicketUpdate($userId, $ticketId, $changes) {
|
||||
public function logTicketUpdate($userId, $ticketId, $changes)
|
||||
{
|
||||
return $this->log($userId, 'update', 'ticket', $ticketId, $changes);
|
||||
}
|
||||
|
||||
@@ -366,7 +384,8 @@ class AuditLogModel {
|
||||
* @param string $ticketId Associated ticket ID
|
||||
* @return bool Success status
|
||||
*/
|
||||
public function logCommentCreate($userId, $commentId, $ticketId) {
|
||||
public function logCommentCreate($userId, $commentId, $ticketId)
|
||||
{
|
||||
return $this->log(
|
||||
$userId,
|
||||
'comment',
|
||||
@@ -383,7 +402,8 @@ class AuditLogModel {
|
||||
* @param string $ticketId Ticket ID
|
||||
* @return bool Success status
|
||||
*/
|
||||
public function logTicketView($userId, $ticketId) {
|
||||
public function logTicketView($userId, $ticketId)
|
||||
{
|
||||
return $this->log($userId, 'view', 'ticket', $ticketId);
|
||||
}
|
||||
|
||||
@@ -399,7 +419,8 @@ class AuditLogModel {
|
||||
* @param int|null $userId User ID if known
|
||||
* @return bool Success status
|
||||
*/
|
||||
public function logSecurityEvent($eventType, $details = [], $userId = null) {
|
||||
public function logSecurityEvent($eventType, $details = [], $userId = null)
|
||||
{
|
||||
$details['event_type'] = $eventType;
|
||||
$details['user_agent'] = $_SERVER['HTTP_USER_AGENT'] ?? 'Unknown';
|
||||
return $this->log($userId, 'security_event', 'security', null, $details);
|
||||
@@ -412,7 +433,8 @@ class AuditLogModel {
|
||||
* @param string $reason Reason for failure
|
||||
* @return bool Success status
|
||||
*/
|
||||
public function logFailedAuth($username, $reason = 'Invalid credentials') {
|
||||
public function logFailedAuth($username, $reason = 'Invalid credentials')
|
||||
{
|
||||
return $this->logSecurityEvent('failed_auth', [
|
||||
'username' => $username,
|
||||
'reason' => $reason
|
||||
@@ -426,7 +448,8 @@ class AuditLogModel {
|
||||
* @param int|null $userId User ID if session exists
|
||||
* @return bool Success status
|
||||
*/
|
||||
public function logCsrfFailure($endpoint, $userId = null) {
|
||||
public function logCsrfFailure($endpoint, $userId = null)
|
||||
{
|
||||
return $this->logSecurityEvent('csrf_failure', [
|
||||
'endpoint' => $endpoint,
|
||||
'method' => $_SERVER['REQUEST_METHOD'] ?? 'Unknown'
|
||||
@@ -440,7 +463,8 @@ class AuditLogModel {
|
||||
* @param int|null $userId User ID if session exists
|
||||
* @return bool Success status
|
||||
*/
|
||||
public function logRateLimitExceeded($endpoint, $userId = null) {
|
||||
public function logRateLimitExceeded($endpoint, $userId = null)
|
||||
{
|
||||
return $this->logSecurityEvent('rate_limit_exceeded', [
|
||||
'endpoint' => $endpoint
|
||||
], $userId);
|
||||
@@ -453,7 +477,8 @@ class AuditLogModel {
|
||||
* @param int|null $userId User ID if session exists
|
||||
* @return bool Success status
|
||||
*/
|
||||
public function logUnauthorizedAccess($resource, $userId = null) {
|
||||
public function logUnauthorizedAccess($resource, $userId = null)
|
||||
{
|
||||
return $this->logSecurityEvent('unauthorized_access', [
|
||||
'resource' => $resource
|
||||
], $userId);
|
||||
@@ -466,7 +491,8 @@ class AuditLogModel {
|
||||
* @param int $offset Offset for pagination
|
||||
* @return array Security events
|
||||
*/
|
||||
public function getSecurityEvents($limit = 100, $offset = 0) {
|
||||
public function getSecurityEvents($limit = 100, $offset = 0)
|
||||
{
|
||||
$limit = $this->validateLimit((int)$limit);
|
||||
$offset = $this->validateOffset((int)$offset);
|
||||
|
||||
@@ -501,7 +527,8 @@ class AuditLogModel {
|
||||
* @param string $ticketId Ticket ID
|
||||
* @return array Timeline events
|
||||
*/
|
||||
public function getTicketTimeline($ticketId) {
|
||||
public function getTicketTimeline($ticketId)
|
||||
{
|
||||
$stmt = $this->conn->prepare(
|
||||
"SELECT al.*, u.username, u.display_name
|
||||
FROM audit_log al
|
||||
@@ -534,7 +561,8 @@ class AuditLogModel {
|
||||
* @param int $offset Offset for pagination
|
||||
* @return array Array containing logs and total count
|
||||
*/
|
||||
public function getFilteredLogs($filters = [], $limit = 50, $offset = 0) {
|
||||
public function getFilteredLogs($filters = [], $limit = 50, $offset = 0)
|
||||
{
|
||||
// Validate pagination parameters
|
||||
$limit = $this->validateLimit((int)$limit);
|
||||
$offset = $this->validateOffset((int)$offset);
|
||||
|
||||
+103
-72
@@ -1,11 +1,14 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* BulkOperationsModel - Handles bulk ticket operations (Admin only)
|
||||
*/
|
||||
class BulkOperationsModel {
|
||||
class BulkOperationsModel
|
||||
{
|
||||
private $conn;
|
||||
|
||||
public function __construct($conn) {
|
||||
public function __construct($conn)
|
||||
{
|
||||
$this->conn = $conn;
|
||||
}
|
||||
|
||||
@@ -18,7 +21,8 @@ class BulkOperationsModel {
|
||||
* @param array|null $parameters Operation parameters
|
||||
* @return int|false Operation ID or false on failure
|
||||
*/
|
||||
public function createBulkOperation($type, $ticketIds, $userId, $parameters = null) {
|
||||
public function createBulkOperation($type, $ticketIds, $userId, $parameters = null)
|
||||
{
|
||||
// Validate ticket IDs to prevent injection via implode
|
||||
$ticketIds = array_values(array_filter(
|
||||
array_map('strval', $ticketIds),
|
||||
@@ -56,7 +60,8 @@ class BulkOperationsModel {
|
||||
* @param bool $atomic If true, rollback all changes on any failure
|
||||
* @return array Result with processed and failed counts
|
||||
*/
|
||||
public function processBulkOperation($operationId, bool $atomic = false) {
|
||||
public function processBulkOperation($operationId, bool $atomic = false)
|
||||
{
|
||||
// Get operation details
|
||||
$sql = "SELECT * FROM bulk_operations WHERE operation_id = ?";
|
||||
$stmt = $this->conn->prepare($sql);
|
||||
@@ -91,16 +96,16 @@ class BulkOperationsModel {
|
||||
|
||||
try {
|
||||
foreach ($ticketIds as $ticketId) {
|
||||
$ticketId = trim($ticketId);
|
||||
$success = false;
|
||||
$ticketId = trim($ticketId);
|
||||
$success = false;
|
||||
|
||||
try {
|
||||
switch ($operation['operation_type']) {
|
||||
case 'bulk_close':
|
||||
// Get current ticket from pre-loaded batch
|
||||
$currentTicket = $ticketsById[$ticketId] ?? null;
|
||||
if ($currentTicket) {
|
||||
$updateResult = $ticketModel->updateTicket([
|
||||
try {
|
||||
switch ($operation['operation_type']) {
|
||||
case 'bulk_close':
|
||||
// Get current ticket from pre-loaded batch
|
||||
$currentTicket = $ticketsById[$ticketId] ?? null;
|
||||
if ($currentTicket) {
|
||||
$updateResult = $ticketModel->updateTicket([
|
||||
'ticket_id' => $ticketId,
|
||||
'title' => $currentTicket['title'],
|
||||
'description' => $currentTicket['description'],
|
||||
@@ -108,31 +113,41 @@ class BulkOperationsModel {
|
||||
'type' => $currentTicket['type'],
|
||||
'status' => 'Closed',
|
||||
'priority' => $currentTicket['priority']
|
||||
], $operation['performed_by']);
|
||||
$success = $updateResult['success'];
|
||||
], $operation['performed_by']);
|
||||
$success = $updateResult['success'];
|
||||
|
||||
if ($success) {
|
||||
$auditLogModel->log($operation['performed_by'], 'update', 'ticket', $ticketId,
|
||||
['status' => 'Closed', 'bulk_operation_id' => $operationId]);
|
||||
if ($success) {
|
||||
$auditLogModel->log(
|
||||
$operation['performed_by'],
|
||||
'update',
|
||||
'ticket',
|
||||
$ticketId,
|
||||
['status' => 'Closed', 'bulk_operation_id' => $operationId]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
break;
|
||||
|
||||
case 'bulk_assign':
|
||||
if (isset($parameters['assigned_to'])) {
|
||||
$success = $ticketModel->assignTicket($ticketId, $parameters['assigned_to'], $operation['performed_by']);
|
||||
if ($success) {
|
||||
$auditLogModel->log($operation['performed_by'], 'assign', 'ticket', $ticketId,
|
||||
['assigned_to' => $parameters['assigned_to'], 'bulk_operation_id' => $operationId]);
|
||||
case 'bulk_assign':
|
||||
if (isset($parameters['assigned_to'])) {
|
||||
$success = $ticketModel->assignTicket($ticketId, $parameters['assigned_to'], $operation['performed_by']);
|
||||
if ($success) {
|
||||
$auditLogModel->log(
|
||||
$operation['performed_by'],
|
||||
'assign',
|
||||
'ticket',
|
||||
$ticketId,
|
||||
['assigned_to' => $parameters['assigned_to'], 'bulk_operation_id' => $operationId]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
break;
|
||||
|
||||
case 'bulk_priority':
|
||||
if (isset($parameters['priority'])) {
|
||||
$currentTicket = $ticketsById[$ticketId] ?? null;
|
||||
if ($currentTicket) {
|
||||
$updateResult = $ticketModel->updateTicket([
|
||||
case 'bulk_priority':
|
||||
if (isset($parameters['priority'])) {
|
||||
$currentTicket = $ticketsById[$ticketId] ?? null;
|
||||
if ($currentTicket) {
|
||||
$updateResult = $ticketModel->updateTicket([
|
||||
'ticket_id' => $ticketId,
|
||||
'title' => $currentTicket['title'],
|
||||
'description' => $currentTicket['description'],
|
||||
@@ -140,22 +155,27 @@ class BulkOperationsModel {
|
||||
'type' => $currentTicket['type'],
|
||||
'status' => $currentTicket['status'],
|
||||
'priority' => $parameters['priority']
|
||||
], $operation['performed_by']);
|
||||
$success = $updateResult['success'];
|
||||
], $operation['performed_by']);
|
||||
$success = $updateResult['success'];
|
||||
|
||||
if ($success) {
|
||||
$auditLogModel->log($operation['performed_by'], 'update', 'ticket', $ticketId,
|
||||
['priority' => $parameters['priority'], 'bulk_operation_id' => $operationId]);
|
||||
if ($success) {
|
||||
$auditLogModel->log(
|
||||
$operation['performed_by'],
|
||||
'update',
|
||||
'ticket',
|
||||
$ticketId,
|
||||
['priority' => $parameters['priority'], 'bulk_operation_id' => $operationId]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
break;
|
||||
|
||||
case 'bulk_status':
|
||||
if (isset($parameters['status'])) {
|
||||
$currentTicket = $ticketsById[$ticketId] ?? null;
|
||||
if ($currentTicket) {
|
||||
$updateResult = $ticketModel->updateTicket([
|
||||
case 'bulk_status':
|
||||
if (isset($parameters['status'])) {
|
||||
$currentTicket = $ticketsById[$ticketId] ?? null;
|
||||
if ($currentTicket) {
|
||||
$updateResult = $ticketModel->updateTicket([
|
||||
'ticket_id' => $ticketId,
|
||||
'title' => $currentTicket['title'],
|
||||
'description' => $currentTicket['description'],
|
||||
@@ -163,37 +183,47 @@ class BulkOperationsModel {
|
||||
'type' => $currentTicket['type'],
|
||||
'status' => $parameters['status'],
|
||||
'priority' => $currentTicket['priority']
|
||||
], $operation['performed_by']);
|
||||
$success = $updateResult['success'];
|
||||
], $operation['performed_by']);
|
||||
$success = $updateResult['success'];
|
||||
|
||||
if ($success) {
|
||||
$auditLogModel->log($operation['performed_by'], 'update', 'ticket', $ticketId,
|
||||
['status' => $parameters['status'], 'bulk_operation_id' => $operationId]);
|
||||
if ($success) {
|
||||
$auditLogModel->log(
|
||||
$operation['performed_by'],
|
||||
'update',
|
||||
'ticket',
|
||||
$ticketId,
|
||||
['status' => $parameters['status'], 'bulk_operation_id' => $operationId]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
break;
|
||||
|
||||
case 'bulk_delete':
|
||||
$success = $ticketModel->deleteTicket($ticketId);
|
||||
if ($success) {
|
||||
$auditLogModel->log($operation['performed_by'], 'delete', 'ticket', $ticketId,
|
||||
['bulk_operation_id' => $operationId]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'bulk_delete':
|
||||
$success = $ticketModel->deleteTicket($ticketId);
|
||||
if ($success) {
|
||||
$auditLogModel->log(
|
||||
$operation['performed_by'],
|
||||
'delete',
|
||||
'ticket',
|
||||
$ticketId,
|
||||
['bulk_operation_id' => $operationId]
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if ($success) {
|
||||
$processed++;
|
||||
} else {
|
||||
if ($success) {
|
||||
$processed++;
|
||||
} else {
|
||||
$failed++;
|
||||
$errors[] = "Ticket $ticketId: Update failed";
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$failed++;
|
||||
$errors[] = "Ticket $ticketId: Update failed";
|
||||
$errors[] = "Ticket $ticketId: " . $e->getMessage();
|
||||
error_log("Bulk operation error for ticket $ticketId: " . $e->getMessage());
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$failed++;
|
||||
$errors[] = "Ticket $ticketId: " . $e->getMessage();
|
||||
error_log("Bulk operation error for ticket $ticketId: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// If atomic mode and any failures, rollback everything
|
||||
@@ -219,7 +249,6 @@ class BulkOperationsModel {
|
||||
|
||||
// Commit the transaction
|
||||
$this->conn->commit();
|
||||
|
||||
} catch (Exception $e) {
|
||||
// Rollback on any unexpected error
|
||||
$this->conn->rollback();
|
||||
@@ -255,7 +284,8 @@ class BulkOperationsModel {
|
||||
* @param int $operationId Operation ID
|
||||
* @return array|null Operation record or null
|
||||
*/
|
||||
public function getOperationById($operationId) {
|
||||
public function getOperationById($operationId)
|
||||
{
|
||||
$sql = "SELECT * FROM bulk_operations WHERE operation_id = ?";
|
||||
$stmt = $this->conn->prepare($sql);
|
||||
$stmt->bind_param("i", $operationId);
|
||||
@@ -273,7 +303,8 @@ class BulkOperationsModel {
|
||||
* @param int $limit Result limit
|
||||
* @return array Array of operations
|
||||
*/
|
||||
public function getOperationsByUser($userId, $limit = 50) {
|
||||
public function getOperationsByUser($userId, $limit = 50)
|
||||
{
|
||||
$sql = "SELECT * FROM bulk_operations WHERE performed_by = ?
|
||||
ORDER BY created_at DESC LIMIT ?";
|
||||
$stmt = $this->conn->prepare($sql);
|
||||
|
||||
+36
-20
@@ -1,8 +1,11 @@
|
||||
<?php
|
||||
class CommentModel {
|
||||
|
||||
class CommentModel
|
||||
{
|
||||
private $conn;
|
||||
|
||||
public function __construct($conn) {
|
||||
public function __construct($conn)
|
||||
{
|
||||
$this->conn = $conn;
|
||||
}
|
||||
|
||||
@@ -12,7 +15,8 @@ class CommentModel {
|
||||
* @param string $text Comment text
|
||||
* @return array Array of mentioned usernames
|
||||
*/
|
||||
public function extractMentions($text) {
|
||||
public function extractMentions($text)
|
||||
{
|
||||
$mentions = [];
|
||||
// Match @username patterns (alphanumeric, underscores, hyphens)
|
||||
if (preg_match_all('/@([a-zA-Z0-9_-]+)/', $text, $matches)) {
|
||||
@@ -27,7 +31,8 @@ class CommentModel {
|
||||
* @param array $usernames Array of usernames
|
||||
* @return array Array of user records with user_id, username, display_name
|
||||
*/
|
||||
public function getMentionedUsers($usernames) {
|
||||
public function getMentionedUsers($usernames)
|
||||
{
|
||||
if (empty($usernames)) {
|
||||
return [];
|
||||
}
|
||||
@@ -49,11 +54,12 @@ class CommentModel {
|
||||
|
||||
return $users;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get total comment count for a ticket
|
||||
*/
|
||||
public function getCommentCount(int $ticketId): int {
|
||||
public function getCommentCount(int $ticketId): int
|
||||
{
|
||||
$stmt = $this->conn->prepare(
|
||||
"SELECT COUNT(*) as total FROM ticket_comments WHERE ticket_id = ?"
|
||||
);
|
||||
@@ -70,7 +76,8 @@ class CommentModel {
|
||||
* @param int $limit Max root-level comments to return (0 = all)
|
||||
* @param int $offset Root-level comment offset for pagination
|
||||
*/
|
||||
public function getCommentsByTicketId($ticketId, $threaded = true, int $limit = 0, int $offset = 0) {
|
||||
public function getCommentsByTicketId($ticketId, $threaded = true, int $limit = 0, int $offset = 0)
|
||||
{
|
||||
$hasThreading = $this->hasThreadingSupport();
|
||||
|
||||
// When paginating with threading we fetch root comments page first,
|
||||
@@ -139,7 +146,8 @@ class CommentModel {
|
||||
/**
|
||||
* Paginated threaded comments: fetch one page of root comments + all their replies.
|
||||
*/
|
||||
private function getThreadedCommentsPaged(int $ticketId, int $limit, int $offset): array {
|
||||
private function getThreadedCommentsPaged(int $ticketId, int $limit, int $offset): array
|
||||
{
|
||||
// Page of root comments
|
||||
$rootSql = "SELECT tc.*, u.display_name, u.username
|
||||
FROM ticket_comments tc
|
||||
@@ -203,7 +211,8 @@ class CommentModel {
|
||||
/**
|
||||
* Check if threading columns exist
|
||||
*/
|
||||
private function hasThreadingSupport() {
|
||||
private function hasThreadingSupport()
|
||||
{
|
||||
static $hasSupport = null;
|
||||
if ($hasSupport !== null) {
|
||||
return $hasSupport;
|
||||
@@ -217,16 +226,19 @@ class CommentModel {
|
||||
/**
|
||||
* Recursively build comment thread
|
||||
*/
|
||||
private function buildCommentThread($comment, &$allComments) {
|
||||
private function buildCommentThread($comment, &$allComments)
|
||||
{
|
||||
$comment['replies'] = [];
|
||||
foreach ($allComments as $c) {
|
||||
if ((int)$c['parent_comment_id'] === (int)$comment['comment_id']
|
||||
&& isset($allComments[$c['comment_id']])) {
|
||||
if (
|
||||
(int)$c['parent_comment_id'] === (int)$comment['comment_id']
|
||||
&& isset($allComments[$c['comment_id']])
|
||||
) {
|
||||
$comment['replies'][] = $this->buildCommentThread($c, $allComments);
|
||||
}
|
||||
}
|
||||
// Sort replies by date ascending
|
||||
usort($comment['replies'], function($a, $b) {
|
||||
usort($comment['replies'], function ($a, $b) {
|
||||
return strtotime($a['created_at']) - strtotime($b['created_at']);
|
||||
});
|
||||
return $comment;
|
||||
@@ -235,11 +247,13 @@ class CommentModel {
|
||||
/**
|
||||
* Get flat list of comments (for backward compatibility)
|
||||
*/
|
||||
public function getCommentsByTicketIdFlat($ticketId) {
|
||||
public function getCommentsByTicketIdFlat($ticketId)
|
||||
{
|
||||
return $this->getCommentsByTicketId($ticketId, false);
|
||||
}
|
||||
|
||||
public function addComment($ticketId, $commentData, $userId = null) {
|
||||
|
||||
public function addComment($ticketId, $commentData, $userId = null)
|
||||
{
|
||||
// Check if threading is supported
|
||||
$hasThreading = $this->hasThreadingSupport();
|
||||
|
||||
@@ -310,7 +324,8 @@ class CommentModel {
|
||||
/**
|
||||
* Get a single comment by ID
|
||||
*/
|
||||
public function getCommentById($commentId) {
|
||||
public function getCommentById($commentId)
|
||||
{
|
||||
$sql = "SELECT tc.*, u.display_name, u.username
|
||||
FROM ticket_comments tc
|
||||
LEFT JOIN users u ON tc.user_id = u.user_id
|
||||
@@ -326,7 +341,8 @@ class CommentModel {
|
||||
* Update an existing comment
|
||||
* Only the comment owner or an admin can update
|
||||
*/
|
||||
public function updateComment($commentId, $commentText, $markdownEnabled, $userId, $isAdmin = false) {
|
||||
public function updateComment($commentId, $commentText, $markdownEnabled, $userId, $isAdmin = false)
|
||||
{
|
||||
// First check if user owns this comment or is admin
|
||||
$comment = $this->getCommentById($commentId);
|
||||
|
||||
@@ -372,7 +388,8 @@ class CommentModel {
|
||||
* Delete a comment
|
||||
* Only the comment owner or an admin can delete
|
||||
*/
|
||||
public function deleteComment($commentId, $userId, $isAdmin = false) {
|
||||
public function deleteComment($commentId, $userId, $isAdmin = false)
|
||||
{
|
||||
// First check if user owns this comment or is admin
|
||||
$comment = $this->getCommentById($commentId);
|
||||
|
||||
@@ -401,4 +418,3 @@ class CommentModel {
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
+27
-14
@@ -1,12 +1,15 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* CustomFieldModel - Manages custom field definitions and values
|
||||
*/
|
||||
|
||||
class CustomFieldModel {
|
||||
class CustomFieldModel
|
||||
{
|
||||
private $conn;
|
||||
|
||||
public function __construct($conn) {
|
||||
public function __construct($conn)
|
||||
{
|
||||
$this->conn = $conn;
|
||||
}
|
||||
|
||||
@@ -17,7 +20,8 @@ class CustomFieldModel {
|
||||
/**
|
||||
* Get all field definitions
|
||||
*/
|
||||
public function getAllDefinitions($category = null, $activeOnly = true) {
|
||||
public function getAllDefinitions($category = null, $activeOnly = true)
|
||||
{
|
||||
$sql = "SELECT * FROM custom_field_definitions WHERE 1=1";
|
||||
$params = [];
|
||||
$types = '';
|
||||
@@ -61,7 +65,8 @@ class CustomFieldModel {
|
||||
/**
|
||||
* Get a single field definition
|
||||
*/
|
||||
public function getDefinition($fieldId) {
|
||||
public function getDefinition($fieldId)
|
||||
{
|
||||
$sql = "SELECT * FROM custom_field_definitions WHERE field_id = ?";
|
||||
$stmt = $this->conn->prepare($sql);
|
||||
$stmt->bind_param('i', $fieldId);
|
||||
@@ -80,7 +85,8 @@ class CustomFieldModel {
|
||||
/**
|
||||
* Create a new field definition
|
||||
*/
|
||||
public function createDefinition($data) {
|
||||
public function createDefinition($data)
|
||||
{
|
||||
$options = null;
|
||||
if (isset($data['field_options']) && !empty($data['field_options'])) {
|
||||
$options = json_encode($data['field_options']);
|
||||
@@ -91,7 +97,8 @@ class CustomFieldModel {
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)";
|
||||
|
||||
$stmt = $this->conn->prepare($sql);
|
||||
$stmt->bind_param('sssssiii',
|
||||
$stmt->bind_param(
|
||||
'sssssiii',
|
||||
$data['field_name'],
|
||||
$data['field_label'],
|
||||
$data['field_type'],
|
||||
@@ -116,7 +123,8 @@ class CustomFieldModel {
|
||||
/**
|
||||
* Update a field definition
|
||||
*/
|
||||
public function updateDefinition($fieldId, $data) {
|
||||
public function updateDefinition($fieldId, $data)
|
||||
{
|
||||
$options = null;
|
||||
if (isset($data['field_options']) && !empty($data['field_options'])) {
|
||||
$options = json_encode($data['field_options']);
|
||||
@@ -128,7 +136,8 @@ class CustomFieldModel {
|
||||
WHERE field_id = ?";
|
||||
|
||||
$stmt = $this->conn->prepare($sql);
|
||||
$stmt->bind_param('sssssiiii',
|
||||
$stmt->bind_param(
|
||||
'sssssiiii',
|
||||
$data['field_name'],
|
||||
$data['field_label'],
|
||||
$data['field_type'],
|
||||
@@ -148,7 +157,8 @@ class CustomFieldModel {
|
||||
/**
|
||||
* Delete a field definition
|
||||
*/
|
||||
public function deleteDefinition($fieldId) {
|
||||
public function deleteDefinition($fieldId)
|
||||
{
|
||||
// This will cascade delete all values due to FK constraint
|
||||
$sql = "DELETE FROM custom_field_definitions WHERE field_id = ?";
|
||||
$stmt = $this->conn->prepare($sql);
|
||||
@@ -165,7 +175,8 @@ class CustomFieldModel {
|
||||
/**
|
||||
* Get all field values for a ticket
|
||||
*/
|
||||
public function getValuesForTicket($ticketId) {
|
||||
public function getValuesForTicket($ticketId)
|
||||
{
|
||||
$sql = "SELECT cfv.*, cfd.field_name, cfd.field_label, cfd.field_type, cfd.field_options
|
||||
FROM custom_field_values cfv
|
||||
JOIN custom_field_definitions cfd ON cfv.field_id = cfd.field_id
|
||||
@@ -192,7 +203,8 @@ class CustomFieldModel {
|
||||
/**
|
||||
* Set a field value for a ticket (insert or update)
|
||||
*/
|
||||
public function setValue($ticketId, $fieldId, $value) {
|
||||
public function setValue($ticketId, $fieldId, $value)
|
||||
{
|
||||
$sql = "INSERT INTO custom_field_values (ticket_id, field_id, field_value)
|
||||
VALUES (?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE field_value = VALUES(field_value), updated_at = CURRENT_TIMESTAMP";
|
||||
@@ -207,7 +219,8 @@ class CustomFieldModel {
|
||||
/**
|
||||
* Set multiple field values for a ticket
|
||||
*/
|
||||
public function setValues($ticketId, $values) {
|
||||
public function setValues($ticketId, $values)
|
||||
{
|
||||
$results = [];
|
||||
foreach ($values as $fieldId => $value) {
|
||||
$results[$fieldId] = $this->setValue($ticketId, $fieldId, $value);
|
||||
@@ -218,7 +231,8 @@ class CustomFieldModel {
|
||||
/**
|
||||
* Delete all field values for a ticket
|
||||
*/
|
||||
public function deleteValuesForTicket($ticketId) {
|
||||
public function deleteValuesForTicket($ticketId)
|
||||
{
|
||||
$sql = "DELETE FROM custom_field_values WHERE ticket_id = ?";
|
||||
$stmt = $this->conn->prepare($sql);
|
||||
$stmt->bind_param('s', $ticketId);
|
||||
@@ -227,4 +241,3 @@ class CustomFieldModel {
|
||||
return ['success' => $success];
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
+21
-10
@@ -1,11 +1,14 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* DependencyModel - Manages ticket dependencies
|
||||
*/
|
||||
class DependencyModel {
|
||||
class DependencyModel
|
||||
{
|
||||
private $conn;
|
||||
|
||||
public function __construct($conn) {
|
||||
public function __construct($conn)
|
||||
{
|
||||
$this->conn = $conn;
|
||||
}
|
||||
|
||||
@@ -15,7 +18,8 @@ class DependencyModel {
|
||||
* @param string $ticketId Ticket ID
|
||||
* @return array Dependencies grouped by type
|
||||
*/
|
||||
public function getDependencies($ticketId) {
|
||||
public function getDependencies($ticketId)
|
||||
{
|
||||
$sql = "SELECT d.*, t.title, t.status, t.priority
|
||||
FROM ticket_dependencies d
|
||||
LEFT JOIN tickets t ON d.depends_on_id = t.ticket_id
|
||||
@@ -53,7 +57,8 @@ class DependencyModel {
|
||||
* @param string $ticketId Ticket ID
|
||||
* @return array Dependent tickets
|
||||
*/
|
||||
public function getDependentTickets($ticketId) {
|
||||
public function getDependentTickets($ticketId)
|
||||
{
|
||||
$sql = "SELECT d.*, t.title, t.status, t.priority
|
||||
FROM ticket_dependencies d
|
||||
LEFT JOIN tickets t ON d.ticket_id = t.ticket_id
|
||||
@@ -88,7 +93,8 @@ class DependencyModel {
|
||||
* @param int $createdBy User ID who created the dependency
|
||||
* @return array Result with success status
|
||||
*/
|
||||
public function addDependency($ticketId, $dependsOnId, $type = 'blocks', $createdBy = null) {
|
||||
public function addDependency($ticketId, $dependsOnId, $type = 'blocks', $createdBy = null)
|
||||
{
|
||||
// Validate dependency type
|
||||
$validTypes = ['blocks', 'blocked_by', 'relates_to', 'duplicates'];
|
||||
if (!in_array($type, $validTypes)) {
|
||||
@@ -142,7 +148,8 @@ class DependencyModel {
|
||||
* @param int $dependencyId Dependency ID
|
||||
* @return bool Success status
|
||||
*/
|
||||
public function removeDependency($dependencyId) {
|
||||
public function removeDependency($dependencyId)
|
||||
{
|
||||
$sql = "DELETE FROM ticket_dependencies WHERE dependency_id = ?";
|
||||
$stmt = $this->conn->prepare($sql);
|
||||
$stmt->bind_param("i", $dependencyId);
|
||||
@@ -159,7 +166,8 @@ class DependencyModel {
|
||||
* @param string $type Dependency type
|
||||
* @return bool Success status
|
||||
*/
|
||||
public function removeDependencyByTickets($ticketId, $dependsOnId, $type) {
|
||||
public function removeDependencyByTickets($ticketId, $dependsOnId, $type)
|
||||
{
|
||||
$sql = "DELETE FROM ticket_dependencies
|
||||
WHERE ticket_id = ? AND depends_on_id = ? AND dependency_type = ?";
|
||||
$stmt = $this->conn->prepare($sql);
|
||||
@@ -180,7 +188,8 @@ class DependencyModel {
|
||||
* @param string $type Dependency type
|
||||
* @return bool True if it would create a cycle
|
||||
*/
|
||||
private function wouldCreateCycle($ticketId, $dependsOnId, $type): bool {
|
||||
private function wouldCreateCycle($ticketId, $dependsOnId, $type): bool
|
||||
{
|
||||
// Only check for cycles in blocking relationships
|
||||
if (!in_array($type, ['blocks', 'blocked_by'])) {
|
||||
return false;
|
||||
@@ -203,7 +212,8 @@ class DependencyModel {
|
||||
* @param int $depth Current recursion depth
|
||||
* @return bool True if path exists
|
||||
*/
|
||||
private function hasDependencyPath($source, $target, array &$visited, int $depth): bool {
|
||||
private function hasDependencyPath($source, $target, array &$visited, int $depth): bool
|
||||
{
|
||||
// Depth limit to prevent DoS and stack overflow
|
||||
if ($depth >= self::MAX_DEPENDENCY_DEPTH) {
|
||||
error_log("Dependency cycle detection hit max depth ({$depth}) from {$source} to {$target}");
|
||||
@@ -250,7 +260,8 @@ class DependencyModel {
|
||||
* @param array $ticketIds Array of ticket IDs
|
||||
* @return array Dependencies indexed by ticket ID
|
||||
*/
|
||||
public function getDependenciesBatch($ticketIds) {
|
||||
public function getDependenciesBatch($ticketIds)
|
||||
{
|
||||
if (empty($ticketIds)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* RecurringTicketModel - Manages recurring ticket schedules
|
||||
*/
|
||||
|
||||
class RecurringTicketModel {
|
||||
class RecurringTicketModel
|
||||
{
|
||||
private $conn;
|
||||
|
||||
public function __construct($conn) {
|
||||
public function __construct($conn)
|
||||
{
|
||||
$this->conn = $conn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all recurring tickets
|
||||
*/
|
||||
public function getAll($includeInactive = false) {
|
||||
public function getAll($includeInactive = false)
|
||||
{
|
||||
$sql = "SELECT rt.*, u1.display_name as assigned_name, u1.username as assigned_username,
|
||||
u2.display_name as creator_name, u2.username as creator_username
|
||||
FROM recurring_tickets rt
|
||||
@@ -37,7 +41,8 @@ class RecurringTicketModel {
|
||||
/**
|
||||
* Get a single recurring ticket by ID
|
||||
*/
|
||||
public function getById($recurringId) {
|
||||
public function getById($recurringId)
|
||||
{
|
||||
$sql = "SELECT * FROM recurring_tickets WHERE recurring_id = ?";
|
||||
$stmt = $this->conn->prepare($sql);
|
||||
$stmt->bind_param('i', $recurringId);
|
||||
@@ -51,14 +56,16 @@ class RecurringTicketModel {
|
||||
/**
|
||||
* Create a new recurring ticket
|
||||
*/
|
||||
public function create($data) {
|
||||
public function create($data)
|
||||
{
|
||||
$sql = "INSERT INTO recurring_tickets
|
||||
(title_template, description_template, category, type, priority, assigned_to,
|
||||
schedule_type, schedule_day, schedule_time, next_run_at, is_active, created_by)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
|
||||
|
||||
$stmt = $this->conn->prepare($sql);
|
||||
$stmt->bind_param('ssssiiisssii',
|
||||
$stmt->bind_param(
|
||||
'ssssiiisssii',
|
||||
$data['title_template'],
|
||||
$data['description_template'],
|
||||
$data['category'],
|
||||
@@ -87,7 +94,8 @@ class RecurringTicketModel {
|
||||
/**
|
||||
* Update a recurring ticket
|
||||
*/
|
||||
public function update($recurringId, $data) {
|
||||
public function update($recurringId, $data)
|
||||
{
|
||||
$sql = "UPDATE recurring_tickets SET
|
||||
title_template = ?, description_template = ?, category = ?, type = ?,
|
||||
priority = ?, assigned_to = ?, schedule_type = ?, schedule_day = ?,
|
||||
@@ -95,7 +103,8 @@ class RecurringTicketModel {
|
||||
WHERE recurring_id = ?";
|
||||
|
||||
$stmt = $this->conn->prepare($sql);
|
||||
$stmt->bind_param('ssssiissssii',
|
||||
$stmt->bind_param(
|
||||
'ssssiissssii',
|
||||
$data['title_template'],
|
||||
$data['description_template'],
|
||||
$data['category'],
|
||||
@@ -118,7 +127,8 @@ class RecurringTicketModel {
|
||||
/**
|
||||
* Delete a recurring ticket
|
||||
*/
|
||||
public function delete($recurringId) {
|
||||
public function delete($recurringId)
|
||||
{
|
||||
$sql = "DELETE FROM recurring_tickets WHERE recurring_id = ?";
|
||||
$stmt = $this->conn->prepare($sql);
|
||||
$stmt->bind_param('i', $recurringId);
|
||||
@@ -130,7 +140,8 @@ class RecurringTicketModel {
|
||||
/**
|
||||
* Get recurring tickets due for execution
|
||||
*/
|
||||
public function getDueRecurringTickets() {
|
||||
public function getDueRecurringTickets()
|
||||
{
|
||||
$sql = "SELECT * FROM recurring_tickets WHERE is_active = 1 AND next_run_at <= NOW()";
|
||||
$result = $this->conn->query($sql);
|
||||
$items = [];
|
||||
@@ -143,7 +154,8 @@ class RecurringTicketModel {
|
||||
/**
|
||||
* Update last run and calculate next run time
|
||||
*/
|
||||
public function updateAfterRun($recurringId) {
|
||||
public function updateAfterRun($recurringId)
|
||||
{
|
||||
$recurring = $this->getById($recurringId);
|
||||
if (!$recurring) {
|
||||
return false;
|
||||
@@ -166,7 +178,8 @@ class RecurringTicketModel {
|
||||
/**
|
||||
* Calculate the next run time based on schedule
|
||||
*/
|
||||
private function calculateNextRunTime($scheduleType, $scheduleDay, $scheduleTime) {
|
||||
private function calculateNextRunTime($scheduleType, $scheduleDay, $scheduleTime)
|
||||
{
|
||||
$now = new DateTime();
|
||||
$time = new DateTime($scheduleTime);
|
||||
|
||||
@@ -202,7 +215,8 @@ class RecurringTicketModel {
|
||||
/**
|
||||
* Toggle active status
|
||||
*/
|
||||
public function toggleActive($recurringId) {
|
||||
public function toggleActive($recurringId)
|
||||
{
|
||||
$sql = "UPDATE recurring_tickets SET is_active = NOT is_active WHERE recurring_id = ?";
|
||||
$stmt = $this->conn->prepare($sql);
|
||||
$stmt->bind_param('i', $recurringId);
|
||||
@@ -211,4 +225,3 @@ class RecurringTicketModel {
|
||||
return ['success' => $success];
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* SavedFiltersModel
|
||||
* Handles saving, loading, and managing user's custom search filters
|
||||
*/
|
||||
class SavedFiltersModel {
|
||||
class SavedFiltersModel
|
||||
{
|
||||
private $conn;
|
||||
|
||||
public function __construct($conn) {
|
||||
public function __construct($conn)
|
||||
{
|
||||
$this->conn = $conn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all saved filters for a user
|
||||
*/
|
||||
public function getUserFilters($userId) {
|
||||
public function getUserFilters($userId)
|
||||
{
|
||||
$sql = "SELECT filter_id, filter_name, filter_criteria, is_default, created_at, updated_at
|
||||
FROM saved_filters
|
||||
WHERE user_id = ?
|
||||
@@ -34,7 +38,8 @@ class SavedFiltersModel {
|
||||
/**
|
||||
* Get a specific saved filter
|
||||
*/
|
||||
public function getFilter($filterId, $userId) {
|
||||
public function getFilter($filterId, $userId)
|
||||
{
|
||||
$sql = "SELECT filter_id, filter_name, filter_criteria, is_default
|
||||
FROM saved_filters
|
||||
WHERE filter_id = ? AND user_id = ?";
|
||||
@@ -53,7 +58,8 @@ class SavedFiltersModel {
|
||||
/**
|
||||
* Save a new filter
|
||||
*/
|
||||
public function saveFilter($userId, $filterName, $filterCriteria, $isDefault = false) {
|
||||
public function saveFilter($userId, $filterName, $filterCriteria, $isDefault = false)
|
||||
{
|
||||
$this->conn->begin_transaction();
|
||||
try {
|
||||
// If this is set as default, unset all other defaults for this user
|
||||
@@ -89,7 +95,8 @@ class SavedFiltersModel {
|
||||
/**
|
||||
* Update an existing filter
|
||||
*/
|
||||
public function updateFilter($filterId, $userId, $filterName, $filterCriteria, $isDefault = false) {
|
||||
public function updateFilter($filterId, $userId, $filterName, $filterCriteria, $isDefault = false)
|
||||
{
|
||||
// Verify ownership
|
||||
$existing = $this->getFilter($filterId, $userId);
|
||||
if (!$existing) {
|
||||
@@ -118,7 +125,8 @@ class SavedFiltersModel {
|
||||
/**
|
||||
* Delete a saved filter
|
||||
*/
|
||||
public function deleteFilter($filterId, $userId) {
|
||||
public function deleteFilter($filterId, $userId)
|
||||
{
|
||||
$sql = "DELETE FROM saved_filters WHERE filter_id = ? AND user_id = ?";
|
||||
$stmt = $this->conn->prepare($sql);
|
||||
$stmt->bind_param("ii", $filterId, $userId);
|
||||
@@ -132,7 +140,8 @@ class SavedFiltersModel {
|
||||
/**
|
||||
* Set a filter as default
|
||||
*/
|
||||
public function setDefaultFilter($filterId, $userId) {
|
||||
public function setDefaultFilter($filterId, $userId)
|
||||
{
|
||||
$this->conn->begin_transaction();
|
||||
try {
|
||||
$this->clearDefaultFilters($userId);
|
||||
@@ -157,7 +166,8 @@ class SavedFiltersModel {
|
||||
/**
|
||||
* Get the default filter for a user
|
||||
*/
|
||||
public function getDefaultFilter($userId) {
|
||||
public function getDefaultFilter($userId)
|
||||
{
|
||||
$sql = "SELECT filter_id, filter_name, filter_criteria
|
||||
FROM saved_filters
|
||||
WHERE user_id = ? AND is_default = 1
|
||||
@@ -177,7 +187,8 @@ class SavedFiltersModel {
|
||||
/**
|
||||
* Clear all default filters for a user (helper method)
|
||||
*/
|
||||
private function clearDefaultFilters($userId) {
|
||||
private function clearDefaultFilters($userId)
|
||||
{
|
||||
$sql = "UPDATE saved_filters SET is_default = 0 WHERE user_id = ?";
|
||||
$stmt = $this->conn->prepare($sql);
|
||||
$stmt->bind_param("i", $userId);
|
||||
@@ -187,7 +198,8 @@ class SavedFiltersModel {
|
||||
/**
|
||||
* Get filter ID by name (helper method)
|
||||
*/
|
||||
private function getFilterIdByName($userId, $filterName) {
|
||||
private function getFilterIdByName($userId, $filterName)
|
||||
{
|
||||
$sql = "SELECT filter_id FROM saved_filters WHERE user_id = ? AND filter_name = ?";
|
||||
$stmt = $this->conn->prepare($sql);
|
||||
$stmt->bind_param("is", $userId, $filterName);
|
||||
@@ -200,4 +212,3 @@ class SavedFiltersModel {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
+14
-7
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* StatsModel - Dashboard statistics and metrics
|
||||
*
|
||||
@@ -9,7 +10,8 @@
|
||||
require_once dirname(__DIR__) . '/helpers/CacheHelper.php';
|
||||
require_once dirname(__DIR__) . '/models/TicketModel.php';
|
||||
|
||||
class StatsModel {
|
||||
class StatsModel
|
||||
{
|
||||
private mysqli $conn;
|
||||
|
||||
/** Cache TTL for dashboard stats in seconds */
|
||||
@@ -18,14 +20,16 @@ class StatsModel {
|
||||
/** Cache prefix for stats */
|
||||
private const CACHE_PREFIX = 'stats';
|
||||
|
||||
public function __construct(mysqli $conn) {
|
||||
public function __construct(mysqli $conn)
|
||||
{
|
||||
$this->conn = $conn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tickets by assignee (top 5)
|
||||
*/
|
||||
public function getTicketsByAssignee(int $limit = 8): array {
|
||||
public function getTicketsByAssignee(int $limit = 8): array
|
||||
{
|
||||
$sql = "SELECT
|
||||
u.user_id,
|
||||
u.display_name,
|
||||
@@ -64,7 +68,8 @@ class StatsModel {
|
||||
* @param bool $forceRefresh Force a cache refresh
|
||||
* @return array All dashboard statistics
|
||||
*/
|
||||
public function getAllStats(array $user = [], bool $forceRefresh = false): array {
|
||||
public function getAllStats(array $user = [], bool $forceRefresh = false): array
|
||||
{
|
||||
$isAdmin = !empty($user['is_admin']);
|
||||
// Admins share one cache entry; non-admins get a per-user cache entry
|
||||
$cacheKey = $isAdmin ? 'dashboard_all' : 'dashboard_user_' . ($user['user_id'] ?? 'anon');
|
||||
@@ -76,7 +81,7 @@ class StatsModel {
|
||||
return CacheHelper::remember(
|
||||
self::CACHE_PREFIX,
|
||||
$cacheKey,
|
||||
function() use ($user) {
|
||||
function () use ($user) {
|
||||
return $this->fetchAllStats($user);
|
||||
},
|
||||
self::STATS_CACHE_TTL
|
||||
@@ -91,7 +96,8 @@ class StatsModel {
|
||||
* @param array $user Current user array
|
||||
* @return array All dashboard statistics
|
||||
*/
|
||||
private function fetchAllStats(array $user = []): array {
|
||||
private function fetchAllStats(array $user = []): array
|
||||
{
|
||||
$ticketModel = new TicketModel($this->conn);
|
||||
$visFilter = $ticketModel->getVisibilityFilter($user);
|
||||
$visSQL = $visFilter['sql'];
|
||||
@@ -191,7 +197,8 @@ class StatsModel {
|
||||
*
|
||||
* Call this method when ticket data changes to ensure fresh stats.
|
||||
*/
|
||||
public function invalidateCache(): void {
|
||||
public function invalidateCache(): void
|
||||
{
|
||||
CacheHelper::delete(self::CACHE_PREFIX, null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* TemplateModel - Handles ticket template operations
|
||||
*/
|
||||
class TemplateModel {
|
||||
class TemplateModel
|
||||
{
|
||||
private mysqli $conn;
|
||||
|
||||
public function __construct(mysqli $conn) {
|
||||
public function __construct(mysqli $conn)
|
||||
{
|
||||
$this->conn = $conn;
|
||||
}
|
||||
|
||||
@@ -14,7 +17,8 @@ class TemplateModel {
|
||||
*
|
||||
* @return array Array of template records
|
||||
*/
|
||||
public function getAllTemplates(): array {
|
||||
public function getAllTemplates(): array
|
||||
{
|
||||
$sql = "SELECT * FROM ticket_templates WHERE is_active = TRUE ORDER BY template_name";
|
||||
$result = $this->conn->query($sql);
|
||||
|
||||
@@ -31,7 +35,8 @@ class TemplateModel {
|
||||
* @param int $templateId Template ID
|
||||
* @return array|null Template record or null if not found
|
||||
*/
|
||||
public function getTemplateById(int $templateId): ?array {
|
||||
public function getTemplateById(int $templateId): ?array
|
||||
{
|
||||
$sql = "SELECT * FROM ticket_templates WHERE template_id = ? AND is_active = TRUE";
|
||||
$stmt = $this->conn->prepare($sql);
|
||||
$stmt->bind_param("i", $templateId);
|
||||
@@ -50,12 +55,14 @@ class TemplateModel {
|
||||
* @param int $createdBy User ID creating the template
|
||||
* @return bool Success status
|
||||
*/
|
||||
public function createTemplate(array $data, int $createdBy): bool {
|
||||
public function createTemplate(array $data, int $createdBy): bool
|
||||
{
|
||||
$sql = "INSERT INTO ticket_templates (template_name, title_template, description_template,
|
||||
category, type, default_priority, created_by)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)";
|
||||
$stmt = $this->conn->prepare($sql);
|
||||
$stmt->bind_param("sssssii",
|
||||
$stmt->bind_param(
|
||||
"sssssii",
|
||||
$data['template_name'],
|
||||
$data['title_template'],
|
||||
$data['description_template'],
|
||||
@@ -77,7 +84,8 @@ class TemplateModel {
|
||||
* @param array $data Template data to update
|
||||
* @return bool Success status
|
||||
*/
|
||||
public function updateTemplate(int $templateId, array $data): bool {
|
||||
public function updateTemplate(int $templateId, array $data): bool
|
||||
{
|
||||
$sql = "UPDATE ticket_templates SET
|
||||
template_name = ?,
|
||||
title_template = ?,
|
||||
@@ -87,7 +95,8 @@ class TemplateModel {
|
||||
default_priority = ?
|
||||
WHERE template_id = ?";
|
||||
$stmt = $this->conn->prepare($sql);
|
||||
$stmt->bind_param("sssssii",
|
||||
$stmt->bind_param(
|
||||
"sssssii",
|
||||
$data['template_name'],
|
||||
$data['title_template'],
|
||||
$data['description_template'],
|
||||
@@ -108,7 +117,8 @@ class TemplateModel {
|
||||
* @param int $templateId Template ID
|
||||
* @return bool Success status
|
||||
*/
|
||||
public function deactivateTemplate(int $templateId): bool {
|
||||
public function deactivateTemplate(int $templateId): bool
|
||||
{
|
||||
$sql = "UPDATE ticket_templates SET is_active = FALSE WHERE template_id = ?";
|
||||
$stmt = $this->conn->prepare($sql);
|
||||
$stmt->bind_param("i", $templateId);
|
||||
|
||||
+47
-27
@@ -1,12 +1,16 @@
|
||||
<?php
|
||||
class TicketModel {
|
||||
|
||||
class TicketModel
|
||||
{
|
||||
private mysqli $conn;
|
||||
|
||||
public function __construct(mysqli $conn) {
|
||||
public function __construct(mysqli $conn)
|
||||
{
|
||||
$this->conn = $conn;
|
||||
}
|
||||
|
||||
public function getTicketById(int $id): ?array {
|
||||
public function getTicketById(int $id): ?array
|
||||
{
|
||||
$sql = "SELECT t.*,
|
||||
u_created.username as creator_username,
|
||||
u_created.display_name as creator_display_name,
|
||||
@@ -30,8 +34,9 @@ class TicketModel {
|
||||
|
||||
return $result->fetch_assoc();
|
||||
}
|
||||
|
||||
public function getAllTickets(int $page = 1, int $limit = 15, ?string $status = 'Open', string $sortColumn = 'ticket_id', string $sortDirection = 'desc', ?string $category = null, ?string $type = null, ?string $search = null, array $filters = [], ?array $user = null): array {
|
||||
|
||||
public function getAllTickets(int $page = 1, int $limit = 15, ?string $status = 'Open', string $sortColumn = 'ticket_id', string $sortDirection = 'desc', ?string $category = null, ?string $type = null, ?string $search = null, array $filters = [], ?array $user = null): array
|
||||
{
|
||||
// Calculate offset
|
||||
$offset = ($page - 1) * $limit;
|
||||
|
||||
@@ -162,12 +167,12 @@ class TicketModel {
|
||||
$paramTypes .= 'i';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$whereClause = '';
|
||||
if (!empty($whereConditions)) {
|
||||
$whereClause = 'WHERE ' . implode(' AND ', $whereConditions);
|
||||
}
|
||||
|
||||
|
||||
// Validate sort column to prevent SQL injection
|
||||
$allowedColumns = ['ticket_id', 'title', 'status', 'priority', 'category', 'type', 'created_at', 'updated_at', 'created_by', 'assigned_to'];
|
||||
if (!in_array($sortColumn, $allowedColumns)) {
|
||||
@@ -230,7 +235,7 @@ class TicketModel {
|
||||
'current_page' => $page
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Update a ticket with optional optimistic locking
|
||||
*
|
||||
@@ -239,7 +244,8 @@ class TicketModel {
|
||||
* @param string|null $expectedUpdatedAt If provided, update will fail if ticket was modified since this timestamp
|
||||
* @return array ['success' => bool, 'error' => string|null, 'conflict' => bool]
|
||||
*/
|
||||
public function updateTicket(array $ticketData, ?int $updatedBy = null, ?string $expectedUpdatedAt = null): array {
|
||||
public function updateTicket(array $ticketData, ?int $updatedBy = null, ?string $expectedUpdatedAt = null): array
|
||||
{
|
||||
// closed_at: set on close (preserve if already set), clear on reopen
|
||||
$closedAtClause = "closed_at = CASE WHEN ? = 'Closed' THEN COALESCE(closed_at, NOW()) ELSE NULL END";
|
||||
|
||||
@@ -332,8 +338,9 @@ class TicketModel {
|
||||
|
||||
return ['success' => true, 'error' => null, 'conflict' => false];
|
||||
}
|
||||
|
||||
public function createTicket(array $ticketData, ?int $createdBy = null): array {
|
||||
|
||||
public function createTicket(array $ticketData, ?int $createdBy = null): array
|
||||
{
|
||||
// Generate unique ticket ID (9-digit format with leading zeros)
|
||||
// Uses cryptographically secure random numbers for better distribution
|
||||
// Includes exponential backoff and fallback for reliability under high load
|
||||
@@ -486,16 +493,17 @@ class TicketModel {
|
||||
}
|
||||
}
|
||||
|
||||
public function addComment(int $ticketId, array $commentData): array {
|
||||
public function addComment(int $ticketId, array $commentData): array
|
||||
{
|
||||
$sql = "INSERT INTO ticket_comments (ticket_id, user_name, comment_text, markdown_enabled)
|
||||
VALUES (?, ?, ?, ?)";
|
||||
|
||||
|
||||
$stmt = $this->conn->prepare($sql);
|
||||
|
||||
|
||||
// Set default username
|
||||
$username = $commentData['user_name'] ?? 'User';
|
||||
$markdownEnabled = $commentData['markdown_enabled'] ? 1 : 0;
|
||||
|
||||
|
||||
$stmt->bind_param(
|
||||
"issi",
|
||||
$ticketId,
|
||||
@@ -503,7 +511,7 @@ class TicketModel {
|
||||
$commentData['comment_text'],
|
||||
$markdownEnabled
|
||||
);
|
||||
|
||||
|
||||
if ($stmt->execute()) {
|
||||
return [
|
||||
'success' => true,
|
||||
@@ -526,7 +534,8 @@ class TicketModel {
|
||||
* @param int $assignedBy User ID performing the assignment
|
||||
* @return bool Success status
|
||||
*/
|
||||
public function assignTicket(int $ticketId, int $userId, int $assignedBy): bool {
|
||||
public function assignTicket(int $ticketId, int $userId, int $assignedBy): bool
|
||||
{
|
||||
$sql = "UPDATE tickets SET assigned_to = ?, updated_by = ?, updated_at = NOW() WHERE ticket_id = ?";
|
||||
$stmt = $this->conn->prepare($sql);
|
||||
$stmt->bind_param("iii", $userId, $assignedBy, $ticketId);
|
||||
@@ -542,7 +551,8 @@ class TicketModel {
|
||||
* @param int $updatedBy User ID performing the unassignment
|
||||
* @return bool Success status
|
||||
*/
|
||||
public function unassignTicket(int $ticketId, int $updatedBy): bool {
|
||||
public function unassignTicket(int $ticketId, int $updatedBy): bool
|
||||
{
|
||||
$sql = "UPDATE tickets SET assigned_to = NULL, updated_by = ?, updated_at = NOW() WHERE ticket_id = ?";
|
||||
$stmt = $this->conn->prepare($sql);
|
||||
$stmt->bind_param("ii", $updatedBy, $ticketId);
|
||||
@@ -558,7 +568,8 @@ class TicketModel {
|
||||
* @param array $ticketIds Array of ticket IDs
|
||||
* @return array Associative array keyed by ticket_id
|
||||
*/
|
||||
public function getTicketsByIds(array $ticketIds): array {
|
||||
public function getTicketsByIds(array $ticketIds): array
|
||||
{
|
||||
if (empty($ticketIds)) {
|
||||
return [];
|
||||
}
|
||||
@@ -604,7 +615,8 @@ class TicketModel {
|
||||
* @param array $user The user data (must include user_id, is_admin, groups)
|
||||
* @return bool True if user can access the ticket
|
||||
*/
|
||||
public function canUserAccessTicket(array $ticket, array $user): bool {
|
||||
public function canUserAccessTicket(array $ticket, array $user): bool
|
||||
{
|
||||
// Admins can access all tickets
|
||||
if (!empty($user['is_admin'])) {
|
||||
return true;
|
||||
@@ -644,7 +656,8 @@ class TicketModel {
|
||||
* @param array $user The current user
|
||||
* @return array ['sql' => string, 'params' => array, 'types' => string]
|
||||
*/
|
||||
public function getVisibilityFilter(array $user): array {
|
||||
public function getVisibilityFilter(array $user): array
|
||||
{
|
||||
// Admins see all tickets
|
||||
if (!empty($user['is_admin'])) {
|
||||
return ['sql' => '1=1', 'params' => [], 'types' => ''];
|
||||
@@ -697,7 +710,8 @@ class TicketModel {
|
||||
* @param int $updatedBy User ID
|
||||
* @return bool
|
||||
*/
|
||||
public function updateVisibility(int $ticketId, string $visibility, ?string $visibilityGroups, int $updatedBy): bool {
|
||||
public function updateVisibility(int $ticketId, string $visibility, ?string $visibilityGroups, int $updatedBy): bool
|
||||
{
|
||||
$allowedVisibilities = ['public', 'internal', 'confidential'];
|
||||
if (!in_array($visibility, $allowedVisibilities)) {
|
||||
$visibility = 'public';
|
||||
@@ -728,7 +742,8 @@ class TicketModel {
|
||||
* @param string $ticketId Ticket ID
|
||||
* @return bool Success status
|
||||
*/
|
||||
public function deleteTicket(string $ticketId): bool {
|
||||
public function deleteTicket(string $ticketId): bool
|
||||
{
|
||||
// Collect attachment filenames before deleting DB rows
|
||||
$attachmentFiles = [];
|
||||
$attStmt = $this->conn->prepare("SELECT filename FROM ticket_attachments WHERE ticket_id = ?");
|
||||
@@ -754,7 +769,9 @@ class TicketModel {
|
||||
foreach ($children as $sql) {
|
||||
try {
|
||||
$stmt = $this->conn->prepare($sql);
|
||||
if (!$stmt) continue;
|
||||
if (!$stmt) {
|
||||
continue;
|
||||
}
|
||||
// ticket_dependencies uses two placeholders
|
||||
if (strpos($sql, 'depends_on_id') !== false) {
|
||||
$stmt->bind_param('ss', $ticketId, $ticketId);
|
||||
@@ -772,7 +789,9 @@ class TicketModel {
|
||||
}
|
||||
|
||||
$stmt = $this->conn->prepare("DELETE FROM tickets WHERE ticket_id = ?");
|
||||
if (!$stmt) return false;
|
||||
if (!$stmt) {
|
||||
return false;
|
||||
}
|
||||
$stmt->bind_param('s', $ticketId);
|
||||
$result = $stmt->execute();
|
||||
$affected = $stmt->affected_rows;
|
||||
@@ -802,7 +821,8 @@ class TicketModel {
|
||||
* Check whether the FULLTEXT index on tickets(title, description) exists.
|
||||
* Result is cached for the process lifetime (static).
|
||||
*/
|
||||
private function hasFulltextIndex(): bool {
|
||||
private function hasFulltextIndex(): bool
|
||||
{
|
||||
static $result = null;
|
||||
if ($result !== null) {
|
||||
return $result;
|
||||
@@ -817,4 +837,4 @@ class TicketModel {
|
||||
$result = $r && (int)$r->fetch_assoc()['cnt'] > 0;
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+31
-15
@@ -1,20 +1,24 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* UserModel - Handles user authentication and management
|
||||
*/
|
||||
class UserModel {
|
||||
class UserModel
|
||||
{
|
||||
private mysqli $conn;
|
||||
private static array $userCache = []; // ['key' => ['data' => ..., 'expires' => timestamp]]
|
||||
private static int $cacheTTL = 300; // 5 minutes
|
||||
|
||||
public function __construct(mysqli $conn) {
|
||||
public function __construct(mysqli $conn)
|
||||
{
|
||||
$this->conn = $conn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cached user data if not expired
|
||||
*/
|
||||
private static function getCached(string $key): ?array {
|
||||
private static function getCached(string $key): ?array
|
||||
{
|
||||
if (isset(self::$userCache[$key])) {
|
||||
$cached = self::$userCache[$key];
|
||||
if ($cached['expires'] > time()) {
|
||||
@@ -29,7 +33,8 @@ class UserModel {
|
||||
/**
|
||||
* Store user data in cache with expiration
|
||||
*/
|
||||
private static function setCached(string $key, array $data): void {
|
||||
private static function setCached(string $key, array $data): void
|
||||
{
|
||||
self::$userCache[$key] = [
|
||||
'data' => $data,
|
||||
'expires' => time() + self::$cacheTTL
|
||||
@@ -39,7 +44,8 @@ class UserModel {
|
||||
/**
|
||||
* Invalidate specific user cache entry
|
||||
*/
|
||||
public static function invalidateCache(?int $userId = null, ?string $username = null): void {
|
||||
public static function invalidateCache(?int $userId = null, ?string $username = null): void
|
||||
{
|
||||
if ($userId !== null) {
|
||||
unset(self::$userCache["user_id_$userId"]);
|
||||
}
|
||||
@@ -57,7 +63,8 @@ class UserModel {
|
||||
* @param string $groups Comma-separated groups from Remote-Groups header
|
||||
* @return array User data array
|
||||
*/
|
||||
public function syncUserFromAuthelia(string $username, string $displayName = '', string $email = '', string $groups = ''): array {
|
||||
public function syncUserFromAuthelia(string $username, string $displayName = '', string $email = '', string $groups = ''): array
|
||||
{
|
||||
// Check cache first
|
||||
$cacheKey = "user_$username";
|
||||
$cached = self::getCached($cacheKey);
|
||||
@@ -122,7 +129,8 @@ class UserModel {
|
||||
*
|
||||
* @return array|null System user data or null if not found
|
||||
*/
|
||||
public function getSystemUser(): ?array {
|
||||
public function getSystemUser(): ?array
|
||||
{
|
||||
// Check cache first
|
||||
$cached = self::getCached('system');
|
||||
if ($cached !== null) {
|
||||
@@ -150,7 +158,8 @@ class UserModel {
|
||||
* @param int $userId User ID
|
||||
* @return array|null User data or null if not found
|
||||
*/
|
||||
public function getUserById(int $userId): ?array {
|
||||
public function getUserById(int $userId): ?array
|
||||
{
|
||||
// Check cache first
|
||||
$cacheKey = "user_id_$userId";
|
||||
$cached = self::getCached($cacheKey);
|
||||
@@ -180,7 +189,8 @@ class UserModel {
|
||||
* @param string $username Username
|
||||
* @return array|null User data or null if not found
|
||||
*/
|
||||
public function getUserByUsername(string $username): ?array {
|
||||
public function getUserByUsername(string $username): ?array
|
||||
{
|
||||
// Check cache first
|
||||
$cacheKey = "user_$username";
|
||||
$cached = self::getCached($cacheKey);
|
||||
@@ -210,7 +220,8 @@ class UserModel {
|
||||
* @param string $groups Comma-separated group names
|
||||
* @return bool True if user is in admin group
|
||||
*/
|
||||
private function checkAdminStatus(string $groups): bool {
|
||||
private function checkAdminStatus(string $groups): bool
|
||||
{
|
||||
if (empty($groups)) {
|
||||
return false;
|
||||
}
|
||||
@@ -226,7 +237,8 @@ class UserModel {
|
||||
* @param array $user User data array
|
||||
* @return bool True if user is admin
|
||||
*/
|
||||
public function isAdmin(array $user): bool {
|
||||
public function isAdmin(array $user): bool
|
||||
{
|
||||
return isset($user['is_admin']) && (int)$user['is_admin'] === 1;
|
||||
}
|
||||
|
||||
@@ -237,7 +249,8 @@ class UserModel {
|
||||
* @param array $requiredGroups Array of required group names
|
||||
* @return bool True if user is in at least one required group
|
||||
*/
|
||||
public function hasGroupAccess(array $user, array $requiredGroups = ['admin', 'employee']): bool {
|
||||
public function hasGroupAccess(array $user, array $requiredGroups = ['admin', 'employee']): bool
|
||||
{
|
||||
if (empty($user['groups'])) {
|
||||
return false;
|
||||
}
|
||||
@@ -253,7 +266,8 @@ class UserModel {
|
||||
*
|
||||
* @return array Array of user records
|
||||
*/
|
||||
public function getAllUsers(): array {
|
||||
public function getAllUsers(): array
|
||||
{
|
||||
$stmt = $this->conn->prepare("SELECT * FROM users ORDER BY created_at DESC");
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result();
|
||||
@@ -276,7 +290,8 @@ class UserModel {
|
||||
*
|
||||
* @return array Array of unique group names
|
||||
*/
|
||||
public function getAllGroups(): array {
|
||||
public function getAllGroups(): array
|
||||
{
|
||||
$cacheKey = 'all_groups';
|
||||
|
||||
// Check cache first
|
||||
@@ -311,7 +326,8 @@ class UserModel {
|
||||
* Invalidate the groups cache
|
||||
* Call this when user groups are modified
|
||||
*/
|
||||
public static function invalidateGroupsCache(): void {
|
||||
public static function invalidateGroupsCache(): void
|
||||
{
|
||||
unset(self::$userCache['all_groups']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* UserPreferencesModel
|
||||
* Handles user-specific preferences and settings with caching
|
||||
*/
|
||||
|
||||
require_once dirname(__DIR__) . '/helpers/CacheHelper.php';
|
||||
|
||||
class UserPreferencesModel {
|
||||
class UserPreferencesModel
|
||||
{
|
||||
private mysqli $conn;
|
||||
private static string $CACHE_PREFIX = 'user_prefs';
|
||||
private static int $CACHE_TTL = 300; // 5 minutes
|
||||
|
||||
public function __construct(mysqli $conn) {
|
||||
public function __construct(mysqli $conn)
|
||||
{
|
||||
$this->conn = $conn;
|
||||
}
|
||||
|
||||
@@ -19,8 +23,9 @@ class UserPreferencesModel {
|
||||
* @param int $userId User ID
|
||||
* @return array Associative array of preference_key => preference_value
|
||||
*/
|
||||
public function getUserPreferences(int $userId): array {
|
||||
return CacheHelper::remember(self::$CACHE_PREFIX, $userId, function() use ($userId) {
|
||||
public function getUserPreferences(int $userId): array
|
||||
{
|
||||
return CacheHelper::remember(self::$CACHE_PREFIX, $userId, function () use ($userId) {
|
||||
$sql = "SELECT preference_key, preference_value
|
||||
FROM user_preferences
|
||||
WHERE user_id = ?";
|
||||
@@ -45,7 +50,8 @@ class UserPreferencesModel {
|
||||
* @param string $value Preference value
|
||||
* @return bool Success status
|
||||
*/
|
||||
public function setPreference(int $userId, string $key, string $value): bool {
|
||||
public function setPreference(int $userId, string $key, string $value): bool
|
||||
{
|
||||
$sql = "INSERT INTO user_preferences (user_id, preference_key, preference_value)
|
||||
VALUES (?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE preference_value = VALUES(preference_value)";
|
||||
@@ -69,7 +75,8 @@ class UserPreferencesModel {
|
||||
* @param mixed $default Default value if preference doesn't exist
|
||||
* @return mixed Preference value or default
|
||||
*/
|
||||
public function getPreference(int $userId, string $key, $default = null) {
|
||||
public function getPreference(int $userId, string $key, $default = null)
|
||||
{
|
||||
$prefs = $this->getUserPreferences($userId);
|
||||
return $prefs[$key] ?? $default;
|
||||
}
|
||||
@@ -80,7 +87,8 @@ class UserPreferencesModel {
|
||||
* @param string $key Preference key
|
||||
* @return bool Success status
|
||||
*/
|
||||
public function deletePreference(int $userId, string $key): bool {
|
||||
public function deletePreference(int $userId, string $key): bool
|
||||
{
|
||||
$sql = "DELETE FROM user_preferences
|
||||
WHERE user_id = ? AND preference_key = ?";
|
||||
$stmt = $this->conn->prepare($sql);
|
||||
@@ -101,7 +109,8 @@ class UserPreferencesModel {
|
||||
* @param int $userId User ID
|
||||
* @return bool Success status
|
||||
*/
|
||||
public function deleteAllPreferences(int $userId): bool {
|
||||
public function deleteAllPreferences(int $userId): bool
|
||||
{
|
||||
$sql = "DELETE FROM user_preferences WHERE user_id = ?";
|
||||
$stmt = $this->conn->prepare($sql);
|
||||
$stmt->bind_param("i", $userId);
|
||||
@@ -119,7 +128,8 @@ class UserPreferencesModel {
|
||||
/**
|
||||
* Clear all user preferences cache
|
||||
*/
|
||||
public static function clearCache(): void {
|
||||
public static function clearCache(): void
|
||||
{
|
||||
CacheHelper::delete(self::$CACHE_PREFIX);
|
||||
}
|
||||
}
|
||||
|
||||
+20
-10
@@ -1,17 +1,21 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* WorkflowModel - Handles status transition workflows and validation
|
||||
*
|
||||
* Uses caching for frequently accessed transition rules since they rarely change.
|
||||
*/
|
||||
|
||||
require_once dirname(__DIR__) . '/helpers/CacheHelper.php';
|
||||
|
||||
class WorkflowModel {
|
||||
class WorkflowModel
|
||||
{
|
||||
private mysqli $conn;
|
||||
private static string $CACHE_PREFIX = 'workflow';
|
||||
private static int $CACHE_TTL = 600; // 10 minutes
|
||||
|
||||
public function __construct(mysqli $conn) {
|
||||
public function __construct(mysqli $conn)
|
||||
{
|
||||
$this->conn = $conn;
|
||||
}
|
||||
|
||||
@@ -20,8 +24,9 @@ class WorkflowModel {
|
||||
*
|
||||
* @return array All active transitions indexed by from_status
|
||||
*/
|
||||
private function getAllTransitions(): array {
|
||||
return CacheHelper::remember(self::$CACHE_PREFIX, 'all_transitions', function() {
|
||||
private function getAllTransitions(): array
|
||||
{
|
||||
return CacheHelper::remember(self::$CACHE_PREFIX, 'all_transitions', function () {
|
||||
$sql = "SELECT from_status, to_status, requires_comment, requires_admin
|
||||
FROM status_transitions
|
||||
WHERE is_active = TRUE";
|
||||
@@ -54,7 +59,8 @@ class WorkflowModel {
|
||||
* @param string $currentStatus Current ticket status
|
||||
* @return array Array of allowed transitions with requirements
|
||||
*/
|
||||
public function getAllowedTransitions(string $currentStatus): array {
|
||||
public function getAllowedTransitions(string $currentStatus): array
|
||||
{
|
||||
$allTransitions = $this->getAllTransitions();
|
||||
|
||||
if (!isset($allTransitions[$currentStatus])) {
|
||||
@@ -72,7 +78,8 @@ class WorkflowModel {
|
||||
* @param bool $isAdmin Whether user is admin
|
||||
* @return bool True if transition is allowed
|
||||
*/
|
||||
public function isTransitionAllowed(string $fromStatus, string $toStatus, bool $isAdmin = false): bool {
|
||||
public function isTransitionAllowed(string $fromStatus, string $toStatus, bool $isAdmin = false): bool
|
||||
{
|
||||
// Allow same status (no change)
|
||||
if ($fromStatus === $toStatus) {
|
||||
return true;
|
||||
@@ -98,8 +105,9 @@ class WorkflowModel {
|
||||
*
|
||||
* @return array Array of unique status values
|
||||
*/
|
||||
public function getAllStatuses(): array {
|
||||
return CacheHelper::remember(self::$CACHE_PREFIX, 'all_statuses', function() {
|
||||
public function getAllStatuses(): array
|
||||
{
|
||||
return CacheHelper::remember(self::$CACHE_PREFIX, 'all_statuses', function () {
|
||||
$sql = "SELECT DISTINCT from_status as status FROM status_transitions
|
||||
UNION
|
||||
SELECT DISTINCT to_status as status FROM status_transitions
|
||||
@@ -126,7 +134,8 @@ class WorkflowModel {
|
||||
* @param string $toStatus Desired status
|
||||
* @return array|null Transition requirements or null if not found
|
||||
*/
|
||||
public function getTransitionRequirements(string $fromStatus, string $toStatus): ?array {
|
||||
public function getTransitionRequirements(string $fromStatus, string $toStatus): ?array
|
||||
{
|
||||
$allTransitions = $this->getAllTransitions();
|
||||
|
||||
if (!isset($allTransitions[$fromStatus][$toStatus])) {
|
||||
@@ -143,7 +152,8 @@ class WorkflowModel {
|
||||
/**
|
||||
* Clear workflow cache (call when transitions are modified)
|
||||
*/
|
||||
public static function clearCache(): void {
|
||||
public static function clearCache(): void
|
||||
{
|
||||
CacheHelper::delete(self::$CACHE_PREFIX);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user