Files
api/services/nginx/app/routes/plateScannersRoute.php
T
Jeppe B ab6c3ba5b6 Fix route permission instance calls (#344)
## Root cause

`route_t::hasPermission()` and `requirePermission()` are instance
methods. Route code was invoking them with `self::`; the new XL Vask
hall-scope helper made that call from a genuinely static context,
causing PHP to throw:

`Non-static method routes\\xlvaskUsageLogsRoute::hasPermission() cannot
be called statically`

## Changes

- Invoke route permission methods through `$this` across all 273
executable legacy calls in 45 route classes.
- Make `xlvaskUsageLogsRoute::allowedHallIdsForUser()` an instance
helper and update all 13 callers.
- Preserve the existing all-scope and own-scope hall selection rules.
- Add a token-aware regression test that rejects executable
`self::hasPermission()` and `self::requirePermission()` calls, while
ignoring comments.
- Add focused XL Vask tests for global scanner hall scope and
group-limited own scope.
- Update affected route contract assertions to the instance-call form.

## Verification

- PHP lint: all 53 changed PHP files
- Focused PHPStan: changed XL Vask route and both new regression tests —
clean
- Focused regression slice: 58 passed, 748 assertions
- Full local unit suite: 1,300 passed, 9,442 assertions (1 unrelated
existing warning, 1 environment skip)
- Full local API suite: 285 passed, 11,704 assertions
- Exact-SHA GitHub Tests workflow: all 7 jobs passed (unit, API,
integration, legacy, edge gateway, and supporting checks)
- Independent exact-SHA QA gate: PASS, no findings
- Independent exact-SHA security gate: PASS, no findings
- Independent exact-SHA reviewer gate: PASS, no findings
- Remote comparison: exactly one commit ahead of
`40b104abed7723a7d1b7028190ecda0e7aeef829`; all 53 remote blob hashes
matched the reviewed worktree

## Delivery state

Draft only for human review. No merge or deployment is included. Qodana
is skipped while the PR remains draft and is therefore not represented
as a passed gate.
2026-08-04 16:04:41 +02:00

235 lines
10 KiB
PHP

<?php
namespace routes;
use classes\authentication;
use objects\departments_o;
use objects\logs_o;
use objects\plate_scanners_o;
use traits\route_t;
class plateScannersRoute
{
use route_t;
public function run(): void
{
$this->get('/numberplatescanners', function () {
// Require the user to be logged in
global $response;
$this->requirePermission('list_number_plate_scanners');
// Get the user object
$user = (new authentication())->get_user();
// Check if the request was successful
if ($user) {
// Log the incident
(new logs_o())->add('numberplatescanners', 'global', 1, $user->id, 'LIST_NUMBER_PLATE_SCANNERS', 'Successfully listed number plate scanners');
// Return the list of plate scanners
$response->success(
(new plate_scanners_o())
->setSearchableFields([
// The fields that can be searched. This would otherwise make it possible to get secret information from the database, simply by searching for it and getting the result count back
'id',
'department_id',
'name',
'notes'
])
->listObjectsWithPaginationIfSet(
static function (array $scanner): array {
return (new plate_scanners_o())->select((int)$scanner['id'])->asArray();
}
)
);
} else {
// Log the incident
(new logs_o())->add('numberplatescanners', 'global', 1, 0, 'LIST_NUMBER_PLATE_SCANNERS', 'No user found, or invalid session');
// Return an error
$response->error('Invalid session', 400);
}
},
[
'list_number_plate_scanners' => 'List all number plate scanners'
]
);
$this->post('/numberplatescanners', function () {
// Require the user to be logged in
global $response;
$this->requirePermission('add_number_plate_scanner');
// Get the user object
$user = (new authentication())->get_user();
// Check if the request was successful
if ($user) {
// Get the post data
$data = json_decode(file_get_contents('php://input'), true);
// Check if the required fields are set
if (!isset($data['department_id'])) {
$response->error('Department ID is required', 400);
}
if (!isset($data['name'])) {
$response->error('Name is required', 400);
}
if (!isset($data['notes'])) {
$response->error('Notes is required', 400);
}
$laneId = array_key_exists('lane_id', $data) && $data['lane_id'] !== null
? (int)$data['lane_id']
: null;
// Add the number plate scanner
$scanner = new plate_scanners_o();
$scanner->add((int)$data['department_id'], (string)$data['name'], (string)$data['notes'], $laneId);
// Log the incident
(new logs_o())->add('numberplatescanners', 'global', 1, $user->id, 'ADD_NUMBER_PLATE_SCANNER', 'Successfully added a number plate scanner');
// Return a success message
$response->success([
'message' => 'Number plate scanner added',
'scanner' => $scanner->asArray(),
]);
} else {
// Log the incident
(new logs_o())->add('numberplatescanners', 'global', 1, 0, 'ADD_NUMBER_PLATE_SCANNER', 'No user found, or invalid session');
// Return an error
$response->error('Invalid session', 400);
}
},
[
'add_number_plate_scanner' => 'Add a number plate scanner'
]
);
$this->put('/numberplatescanners', function () {
// Require the user to be logged in
global $response;
$this->requirePermission('edit_number_plate_scanner');
// Get the user object
$user = (new authentication())->get_user();
// Check if the request was successful
if ($user) {
// Get the post data
$data = json_decode(file_get_contents('php://input'), true);
// Check if the required fields are set
if (!isset($data['id'])) {
$response->error('ID is required', 400);
}
if (!isset($data['department_id'])) {
$response->error('Department ID is required', 400);
}
if (!isset($data['name'])) {
$response->error('Name is required', 400);
}
if (!isset($data['notes'])) {
$response->error('Notes is required', 400);
}
$laneIdProvided = array_key_exists('lane_id', $data);
$laneId = $laneIdProvided && $data['lane_id'] !== null ? (int)$data['lane_id'] : null;
// Edit the number plate scanner
$scanner = new plate_scanners_o();
$scanner->edit(
(int)$data['id'],
(int)$data['department_id'],
(string)$data['name'],
(string)$data['notes'],
$laneId,
$laneIdProvided
);
// Log the incident
(new logs_o())->add('numberplatescanners', 'global', 1, $user->id, 'EDIT_NUMBER_PLATE_SCANNER', 'Successfully edited a number plate scanner');
// Return a success message
$response->success([
'message' => 'Number plate scanner edited',
'scanner' => $scanner->asArray(),
]);
} else {
// Log the incident
(new logs_o())->add('numberplatescanners', 'global', 1, 0, 'EDIT_NUMBER_PLATE_SCANNER', 'No user found, or invalid session');
// Return an error
$response->error('Invalid session', 400);
}
},
[
'edit_number_plate_scanner' => 'Edit a number plate scanner'
]
);
$this->post('/numberplatescanners/{id}/rotate-key', function () {
global $response;
$this->requirePermission('edit_number_plate_scanner');
$user = (new authentication())->get_user();
if (!$user) {
(new logs_o())->add('numberplatescanners', 'global', 1, 0, 'ROTATE_NUMBER_PLATE_SCANNER_KEY', 'No user found, or invalid session');
$response->error('Invalid session', 400);
}
$scannerId = (int)$this->fromRoute('id');
self::requireParameterIntPositive($scannerId, 'id');
$scanner = (new plate_scanners_o())->select($scannerId);
if (!$scanner->exists()) {
$response->error('Number plate scanner not found', 404);
}
self::requireDepartmentAccess((int)$scanner->department_id->value());
$rotatedScanner = (new plate_scanners_o())->rotateApiKey($scannerId);
(new logs_o())->add('numberplatescanners', 'global', 1, $user->id, 'ROTATE_NUMBER_PLATE_SCANNER_KEY', 'Successfully rotated a number plate scanner API key');
$response->success([
'message' => 'Number plate scanner API key rotated',
'scanner' => $rotatedScanner,
'api_key' => (string)$rotatedScanner['api_key'],
]);
}, [
'edit_number_plate_scanner' => 'Rotate a number plate scanner API key',
]);
self::get('/department/numberplatescanners', function () {
// Require the user to be logged in
global $response;
$this->requirePermission('list_department_number_plate_scanners');
// Get the user object
$user = (new authentication())->get_user();
// Check if the request was successful
if ($user) {
// Get the department ID
self::requireParameters(['id']);
self::requireType((int)self::getParameter('id'), self::type_int());
self::requireParameterIntPositive((int)self::getParameter('id'), 'id');
// Check if the user has access to the department
self::requireDepartmentAccess((int)self::getParameter('id'));
// Check if the department exists
$department = (new departments_o())->select((int)self::getParameter('id'));
$department->requireSelected();
// Log the incident
(new logs_o())->add('numberplatescanners', 'global', 1, $user->id, 'LIST_DEPARTMENT_NUMBER_PLATE_SCANNERS', 'Successfully listed department number plate scanners');
// Get the department scanners.
$result = (new plate_scanners_o())
->getFieldsWhere([
'department_id' => (int)self::getParameter('id')
], [
'id',
'lane_id',
'name',
'notes'
]);
// Parse the result
foreach ( $result as $key => $value ) {
$result[$key]['id'] = (int)$value['id'];
$result[$key]['lane_id'] = $value['lane_id'] === null ? null : (int)$value['lane_id'];
}
// Return the list of plate scanners
$response->success(
$result
);
} else {
// Log the incident
(new logs_o())->add('numberplatescanners', 'global', 1, 0, 'LIST_DEPARTMENT_NUMBER_PLATE_SCANNERS', 'No user found, or invalid session');
// Return an error
$response->error('Invalid session', 400);
}
},
[
'list_department_number_plate_scanners' => 'List all number plate scanners in a department',
'department_access_:id' => 'Access department :id'
]
);
}
}