Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
74.58% covered (warning)
74.58%
44 / 59
0.00% covered (danger)
0.00%
0 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
AvailabilityHistoryByScope
74.58% covered (warning)
74.58%
44 / 59
0.00% covered (danger)
0.00%
0 / 4
28.25
0.00% covered (danger)
0.00%
0 / 1
 readResponse
90.32% covered (success)
90.32%
28 / 31
0.00% covered (danger)
0.00%
0 / 1
7.04
 assertTechAdminAccess
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
4.07
 resolveDateRange
73.33% covered (warning)
73.33%
11 / 15
0.00% covered (danger)
0.00%
0 / 1
5.47
 parseDateParam
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
30
1<?php
2
3/**
4 * @package ZMS API
5 * @copyright BerlinOnline Stadtportal GmbH & Co. KG
6 **/
7
8namespace BO\Zmsbackend\Availability\Api;
9
10use App;
11use BO\Mellon\Validator;
12use BO\Slim\Render;
13use BO\Zmsbackend\Availability\Service\AvailabilityHistory as AvailabilityHistoryService;
14use BO\Zmsbackend\Helper\User;
15use BO\Zmsentities\Exception\UserAccountMissingLogin;
16use BO\Zmsentities\Exception\UserAccountMissingRights;
17use Psr\Http\Message\RequestInterface;
18use Psr\Http\Message\ResponseInterface;
19
20/** @psalm-api */
21class AvailabilityHistoryByScope extends \BO\Zmsbackend\Api\BaseController
22{
23    /**
24     * @SuppressWarnings(Param)
25     */
26    #[\Override]
27    public function readResponse(
28        RequestInterface $request,
29        ResponseInterface $response,
30        array $args
31    ): ResponseInterface {
32        $this->assertTechAdminAccess($request);
33
34        $scope = (new \BO\Zmsbackend\Scope\Service\Scope())->readEntity($args['id'], 0);
35        if (!$scope || !$scope->hasId()) {
36            throw new \BO\Zmsbackend\Scope\Exception\ScopeNotFound();
37        }
38
39        [$from, $to] = $this->resolveDateRange();
40        $availabilityId = Validator::param('availabilityId')->isNumber()->getValue();
41        $action = Validator::param('action')->isString()->getValue();
42        if ($action !== null && $action !== '') {
43            if (
44                !in_array(
45                    $action,
46                    \BO\Zmsentities\AvailabilityHistory::ACTIONS,
47                    true
48                )
49            ) {
50                throw new \BO\Zmsbackend\Exception\BadRequest(
51                    'Parameter action must be one of: created, updated, deleted, dldb_slot_update'
52                );
53            }
54        } else {
55            $action = null;
56        }
57
58        $message = \BO\Zmsbackend\Api\Response\Message::create($request);
59        $message->data = (new AvailabilityHistoryService())->readListByScopeId(
60            (int) $scope->getId(),
61            $from,
62            $to,
63            $availabilityId ? (int) $availabilityId : null,
64            $action
65        );
66        $message->setUpdatedMetaData();
67        // Empty history is a valid list. setUpdatedMetaData() otherwise maps it to
68        // meta.error = "Not found", which zmsadmin/zmsclient turns into HTTP 500.
69        $message->statuscode = 200;
70        $message->meta['error'] = false;
71        $message->meta['message'] = '';
72
73        $response = Render::withLastModified($response, time(), '0');
74        return Render::withJson($response, $message, 200);
75    }
76
77    protected function assertTechAdminAccess(RequestInterface $request): void
78    {
79        new User($request, 1);
80        $useraccount = User::readWorkstation()->getUseraccount();
81        if (!$useraccount->hasId()) {
82            throw new UserAccountMissingLogin();
83        }
84        if (!$useraccount->isSuperUser() && !$useraccount->hasRole('system_admin')) {
85            throw new UserAccountMissingRights();
86        }
87    }
88
89    protected function resolveDateRange(): array
90    {
91        $fromParam = Validator::param('from')->isString()->getValue();
92        $toParam = Validator::param('to')->isString()->getValue();
93
94        // History rows use MySQL CURRENT_TIMESTAMP (wall clock). Do not use App::$now:
95        // ZMS_TIMEADJUST on remote freezes that clock, so GET misses real inserts.
96        $timezone = App::$now instanceof \DateTimeInterface
97            ? App::$now->getTimezone()
98            : new \DateTimeZone('Europe/Berlin');
99        $now = new \DateTimeImmutable('now', $timezone);
100        $to = $toParam
101            ? $this->parseDateParam($toParam, true)
102            : $now->setTime(23, 59, 59);
103        $from = $fromParam
104            ? $this->parseDateParam($fromParam, false)
105            : $to->modify('-' . AvailabilityHistoryService::DEFAULT_RETENTION_DAYS . ' days')->setTime(0, 0, 0);
106
107        if ($from > $to) {
108            throw new \BO\Zmsbackend\Exception\BadRequest('Parameter from must be before or equal to to');
109        }
110
111        return [$from, $to];
112    }
113
114    protected function parseDateParam(string $value, bool $endOfDay): \DateTimeImmutable
115    {
116        $date = \DateTimeImmutable::createFromFormat('Y-m-d', $value);
117        $errors = \DateTimeImmutable::getLastErrors();
118        if (
119            !$date
120            || ($errors['warning_count'] ?? 0) > 0
121            || ($errors['error_count'] ?? 0) > 0
122        ) {
123            throw new \BO\Zmsbackend\Exception\BadRequest('Date parameters must use Y-m-d format');
124        }
125
126        return $endOfDay ? $date->setTime(23, 59, 59) : $date->setTime(0, 0, 0);
127    }
128}