- DashboardController: handle assigned_to='unassigned' before validateUserId()
which discarded the string, causing the filter to never reach TicketModel;
model already correctly converts 'unassigned' to IS NULL in SQL
- dashboard.js: add null guards before .value access on dynamically-created
modal selects (bulkPriority, bulkStatus, quickStatusSelect)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- TicketModel::getAllTickets() now accepts optional $user param and applies
getVisibilityFilter() so non-admin users cannot see internal/confidential
tickets they lack access to from the dashboard listing
- DashboardController passes $GLOBALS['currentUser'] to getAllTickets()
- clone_ticket.php: move Content-Type header to top so all error paths send
correct JSON content type
- AuthMiddleware: filter group names from HTTP header to [a-z0-9_-] only,
preventing header injection via malformed group names
- add_comment.php: return HTTP 201 on success, 500 in catch block
- update_comment.php, delete_comment.php: return 500 in catch blocks
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
StatsModel.getAllStats() now accepts a user array and applies the same
getVisibilityFilter() logic used by ticket listings. Admins continue to
share a single cached result; non-admin users get per-user cache entries
so confidential ticket counts are not leaked in dashboard stats.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Security:
- Fix IDOR in delete/update comment (add ticket visibility check)
- XSS defense-in-depth in DashboardView active filters
- Replace innerHTML with DOM construction in toast.js
- Remove redundant real_escape_string in check_duplicates
- Add rate limiting to get_template, download_attachment, audit_log,
saved_filters, user_preferences endpoints
Bug fixes:
- Session timeout now reads from config instead of hardcoded 18000
- TicketController uses $GLOBALS['config'] instead of duplicate .env parsing
- Add DISCORD_WEBHOOK_URL to centralized config
- Cleanup script uses hashmap for O(1) ticket ID lookups
Dead code removal (~100 lines):
- Remove dead getTicketComments() from TicketModel (wrong bind_param type)
- Remove dead getCategories()/getTypes() from DashboardController
- Remove ~80 lines dead Discord webhook code from update_ticket API
Consolidation:
- Create api/bootstrap.php for shared API setup (auth, CSRF, rate limit)
- Convert 6 API endpoints to use bootstrap
- Extract escapeHtml/getTicketIdFromUrl into shared utils.js
- Batch save for user preferences (1 request instead of 7)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Consolidate StatsModel queries from 12 to 3 using conditional aggregation
- Add input validation to DashboardController (sort columns, dates, priorities)
- Combine getCategories/getTypes into single query
- Add transaction support to BulkOperationsModel with atomic mode option
- Add depth limit (20) to dependency cycle detection to prevent DoS
- Add caching to UserModel.getAllGroups() with 5-minute TTL
- Improve ticket ID generation with 50 attempts, exponential backoff, and fallback
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>