Issues (85)

Security Analysis    no request data  

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

src/Calendar.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
namespace CultuurNet\UDB3;
4
5
use Broadway\Serializer\SerializableInterface;
6
use CultuurNet\UDB3\Calendar\OpeningHour;
7
use CultuurNet\UDB3\Event\ValueObjects\Status;
8
use CultuurNet\UDB3\Event\ValueObjects\StatusType;
9
use CultuurNet\UDB3\Model\ValueObject\Calendar\Calendar as Udb3ModelCalendar;
10
use CultuurNet\UDB3\Model\ValueObject\Calendar\CalendarWithDateRange;
11
use CultuurNet\UDB3\Model\ValueObject\Calendar\CalendarWithOpeningHours;
12
use CultuurNet\UDB3\Model\ValueObject\Calendar\CalendarWithSubEvents;
13
use CultuurNet\UDB3\Model\ValueObject\Calendar\OpeningHours\OpeningHour as Udb3ModelOpeningHour;
14
use CultuurNet\UDB3\Model\ValueObject\Calendar\SubEvent;
15
use DateTime;
16
use DateTimeInterface;
17
use DateTimeZone;
18
use InvalidArgumentException;
19
20
final class Calendar implements CalendarInterface, JsonLdSerializableInterface, SerializableInterface
21
{
22
    /**
23
     * @var CalendarType
24
     */
25
    protected $type;
26
27
    /**
28
     * @var DateTimeInterface
29
     */
30
    protected $startDate;
31
32
    /**
33
     * @var DateTimeInterface
34
     */
35
    protected $endDate;
36
37
    /**
38
     * @var Timestamp[]
39
     */
40
    protected $timestamps = [];
41
42
    /**
43
     * @var OpeningHour[]
44
     */
45
    protected $openingHours = [];
46
47
    /**
48
     * @var Status
49
     */
50
    protected $status;
51
52
    /**
53
     * @param CalendarType $type
54
     * @param DateTimeInterface|null $startDate
55
     * @param DateTimeInterface|null $endDate
56
     * @param Timestamp[] $timestamps
57
     * @param OpeningHour[] $openingHours
58
     */
59
    public function __construct(
60
        CalendarType $type,
61
        ?DateTimeInterface $startDate = null,
62
        ?DateTimeInterface $endDate = null,
63
        array $timestamps = [],
64
        array $openingHours = []
65
    ) {
66
        if (empty($timestamps) && ($type->is(CalendarType::SINGLE()) || $type->is(CalendarType::MULTIPLE()))) {
67
            throw new \UnexpectedValueException('A single or multiple calendar should have timestamps.');
68
        }
69
70
        if (($startDate === null || $endDate === null) && $type->is(CalendarType::PERIODIC())) {
71
            throw new \UnexpectedValueException('A period should have a start- and end-date.');
72
        }
73
74
        foreach ($timestamps as $timestamp) {
75
            if (!is_a($timestamp, Timestamp::class)) {
76
                throw new \InvalidArgumentException('Timestamps should have type TimeStamp.');
77
            }
78
        }
79
80
        foreach ($openingHours as $openingHour) {
81
            if (!is_a($openingHour, OpeningHour::class)) {
82
                throw new \InvalidArgumentException('OpeningHours should have type OpeningHour.');
83
            }
84
        }
85
86
        $this->type = $type->toNative();
87
        $this->startDate = $startDate;
88
        $this->endDate = $endDate;
89
        $this->openingHours = $openingHours;
90
91
        usort($timestamps, function (Timestamp $timestamp, Timestamp $otherTimestamp) {
92
            return $timestamp->getStartDate() <=> $otherTimestamp->getStartDate();
93
        });
94
95
        $this->timestamps = $timestamps;
96
97
        $this->status = new Status($this->deriveStatusTypeFromSubEvents(), []);
98
    }
99
100
    public function withStatus(Status $status): self
101
    {
102
        $clone = clone $this;
103
        $clone->status = $status;
104
        return $clone;
105
    }
106
107
    public function withStatusOnTimestamps(Status $status): self
108
    {
109
        $clone = clone $this;
110
        $clone->timestamps = \array_map(
111
            function (Timestamp $timestamp) use ($status) : Timestamp {
112
                return $timestamp->withStatus($status);
113
            },
114
            $clone->getTimestamps()
115
        );
116
        return $clone;
117
    }
118
119
    public function getStatus(): Status
120
    {
121
        return $this->status;
122
    }
123
124
    public function getType(): CalendarType
125
    {
126
        return CalendarType::fromNative($this->type);
127
    }
128
129
    public function serialize(): array
130
    {
131
        $serializedTimestamps = array_map(
132
            function (Timestamp $timestamp) {
133
                return $timestamp->serialize();
134
            },
135
            $this->timestamps
136
        );
137
138
        $serializedOpeningHours = array_map(
139
            function (OpeningHour $openingHour) {
140
                return $openingHour->serialize();
141
            },
142
            $this->openingHours
143
        );
144
145
        $calendar = [
146
            'type' => $this->type,
147
            'status' => $this->status->serialize(),
148
        ];
149
150
        empty($this->startDate) ?: $calendar['startDate'] = $this->startDate->format(DateTime::ATOM);
151
        empty($this->endDate) ?: $calendar['endDate'] = $this->endDate->format(DateTime::ATOM);
152
        empty($serializedTimestamps) ?: $calendar['timestamps'] = $serializedTimestamps;
153
        empty($serializedOpeningHours) ?: $calendar['openingHours'] = $serializedOpeningHours;
154
155
        return $calendar;
156
    }
157
158
    public static function deserialize(array $data): Calendar
159
    {
160
        $calendarType = CalendarType::fromNative($data['type']);
161
162
        // Backwards compatibility for serialized single or multiple calendar types that are missing timestamps but do
163
        // have a start and end date.
164
        $defaultTimeStamps = [];
165
        if ($calendarType->sameValueAs(CalendarType::SINGLE()) || $calendarType->sameValueAs(CalendarType::MULTIPLE())) {
166
            $defaultTimeStampStartDate = !empty($data['startDate']) ? self::deserializeDateTime($data['startDate']) : null;
167
            $defaultTimeStampEndDate = !empty($data['endDate']) ? self::deserializeDateTime($data['endDate']) : $defaultTimeStampStartDate;
168
            $defaultTimeStamp = $defaultTimeStampStartDate && $defaultTimeStampEndDate ? new Timestamp($defaultTimeStampStartDate, $defaultTimeStampEndDate) : null;
169
            $defaultTimeStamps = $defaultTimeStamp ? [$defaultTimeStamp] : [];
170
        }
171
172
        $calendar = new self(
173
            $calendarType,
174
            !empty($data['startDate']) ? self::deserializeDateTime($data['startDate']) : null,
175
            !empty($data['endDate']) ? self::deserializeDateTime($data['endDate']) : null,
176
            !empty($data['timestamps']) ? array_map(
177
                function ($timestamp) {
178
                    return Timestamp::deserialize($timestamp);
179
                },
180
                $data['timestamps']
181
            ) : $defaultTimeStamps,
182
            !empty($data['openingHours']) ? array_map(
183
                function ($openingHour) {
184
                    return OpeningHour::deserialize($openingHour);
185
                },
186
                $data['openingHours']
187
            ) : []
188
        );
189
190
        if (!empty($data['status'])) {
191
            $calendar->status = Status::deserialize($data['status']);
192
        }
193
194
        return $calendar;
195
    }
196
197
    /**
198
     * This deserialization function takes into account old data that might be missing a timezone.
199
     * It will fall back to creating a DateTime object and assume Brussels.
200
     * If this still fails an error will be thrown.
201
     */
202
    private static function deserializeDateTime(string $dateTimeData): DateTime
203
    {
204
        $dateTime = DateTime::createFromFormat(DateTime::ATOM, $dateTimeData);
205
206
        if ($dateTime === false) {
207
            $dateTime = DateTime::createFromFormat('Y-m-d\TH:i:s', $dateTimeData, new DateTimeZone('Europe/Brussels'));
208
209
            if (!$dateTime) {
210
                throw new InvalidArgumentException('Invalid date string provided for timestamp, ISO8601 expected!');
211
            }
212
        }
213
214
        return $dateTime;
215
    }
216
217
    public function getStartDate(): ?DateTimeInterface
218
    {
219
        $timestamps = $this->getTimestamps();
220
221
        if (empty($timestamps)) {
222
            return $this->startDate;
223
        }
224
225
        $startDate = null;
226
        foreach ($timestamps as $timestamp) {
227
            if ($startDate === null || $timestamp->getStartDate() < $startDate) {
228
                $startDate = $timestamp->getStartDate();
229
            }
230
        }
231
232
        return $startDate;
233
    }
234
235
    public function getEndDate(): ?DateTimeInterface
236
    {
237
        $timestamps = $this->getTimestamps();
238
239
        if (empty($timestamps)) {
240
            return $this->endDate;
241
        }
242
243
        $endDate = null;
244
        foreach ($this->getTimestamps() as $timestamp) {
245
            if ($endDate === null || $timestamp->getEndDate() > $endDate) {
246
                $endDate = $timestamp->getEndDate();
247
            }
248
        }
249
250
        return $endDate;
251
    }
252
253
    /**
254
     * @return array|OpeningHour[]
255
     */
256
    public function getOpeningHours(): array
257
    {
258
        return $this->openingHours;
259
    }
260
261
    /**
262
     * @return array|Timestamp[]
263
     */
264
    public function getTimestamps(): array
265
    {
266
        return $this->timestamps;
267
    }
268
269
    private function deriveStatusTypeFromSubEvents(): StatusType
270
    {
271
        $statusTypeCounts = [];
272
        $statusTypeCounts[StatusType::available()->toNative()] = 0;
273
        $statusTypeCounts[StatusType::temporarilyUnavailable()->toNative()] = 0;
274
        $statusTypeCounts[StatusType::unavailable()->toNative()] = 0;
275
276
        foreach ($this->timestamps as $timestamp) {
277
            ++$statusTypeCounts[$timestamp->getStatus()->getType()->toNative()];
278
        }
279
280
        if ($statusTypeCounts[StatusType::available()->toNative()] > 0) {
281
            return StatusType::available();
282
        }
283
284
        if ($statusTypeCounts[StatusType::temporarilyUnavailable()->toNative()] > 0) {
285
            return StatusType::temporarilyUnavailable();
286
        }
287
288
        if ($statusTypeCounts[StatusType::unavailable()->toNative()] > 0) {
289
            return StatusType::unavailable();
290
        }
291
292
        // This extra return is needed for events with calendar type of permanent or periodic.
293
        return StatusType::available();
294
    }
295
296
    public function toJsonLd(): array
297
    {
298
        $jsonLd = [];
299
300
        $jsonLd['calendarType'] = $this->getType()->toNative();
301
302
        $startDate = $this->getStartDate();
303
        $endDate = $this->getEndDate();
304
        if ($startDate !== null) {
305
            $jsonLd['startDate'] = $startDate->format(DateTime::ATOM);
306
        }
307
        if ($endDate !== null) {
308
            $jsonLd['endDate'] = $endDate->format(DateTime::ATOM);
309
        }
310
311
        $jsonLd['status'] = $this->determineCorrectTopStatusForProjection()->serialize();
312
313
        $timestamps = $this->getTimestamps();
314
        if (!empty($timestamps)) {
315
            $jsonLd['subEvent'] = [];
316
            foreach ($timestamps as $timestamp) {
317
                $jsonLd['subEvent'][] = $timestamp->toJsonLd();
318
            }
319
        }
320
321
        $openingHours = $this->getOpeningHours();
322
        if (!empty($openingHours)) {
323
            $jsonLd['openingHours'] = [];
324
            foreach ($openingHours as $openingHour) {
325
                $jsonLd['openingHours'][] = $openingHour->serialize();
326
            }
327
        }
328
329
        return $jsonLd;
330
    }
331
332
    public function sameAs(Calendar $otherCalendar): bool
333
    {
334
        return $this->toJsonLd() === $otherCalendar->toJsonLd();
335
    }
336
337
    public static function fromUdb3ModelCalendar(Udb3ModelCalendar $udb3Calendar): Calendar
338
    {
339
        $type = CalendarType::fromNative($udb3Calendar->getType()->toString());
340
341
        $startDate = null;
342
        $endDate = null;
343
        $timestamps = [];
344
        $openingHours = [];
345
346
        if ($udb3Calendar instanceof CalendarWithDateRange) {
347
            $startDate = $udb3Calendar->getStartDate();
348
            $endDate = $udb3Calendar->getEndDate();
349
        }
350
351
        if ($udb3Calendar instanceof CalendarWithSubEvents) {
352
            $timestamps = array_map(
353
                function (SubEvent $subEvent) {
354
                    return Timestamp::fromUdb3ModelSubEvent($subEvent);
355
                },
356
                $udb3Calendar->getSubEvents()->toArray()
357
            );
358
        }
359
360
        if ($udb3Calendar instanceof CalendarWithOpeningHours) {
361
            $openingHours = array_map(
362
                function (Udb3ModelOpeningHour $openingHour) {
363
                    return OpeningHour::fromUdb3ModelOpeningHour($openingHour);
364
                },
365
                $udb3Calendar->getOpeningHours()->toArray()
366
            );
367
        }
368
369
        $calendar = new self($type, $startDate, $endDate, $timestamps, $openingHours);
370
        $calendar->status = Status::fromUdb3ModelStatus($udb3Calendar->getStatus());
0 ignored issues
show
Documentation Bug introduced by
It seems like \CultuurNet\UDB3\Event\V...3Calendar->getStatus()) of type object<self> is incompatible with the declared type object<CultuurNet\UDB3\Event\ValueObjects\Status> of property $status.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
371
        return $calendar;
372
    }
373
374
    /**
375
     * If the calendar has subEvents (timestamps), and a status manually set through an import or full calendar update
376
     * through the API, the top status might be incorrect.
377
     * For example the top status can not be Available if all the subEvents are Unavailable or TemporarilyUnavailable.
378
     * However we want to be flexible in what we accept from API clients since otherwise they will have to implement a
379
     * lot of (new) logic to make sure the top status they're sending is correct.
380
     * So we accept the top status as-is, and correct it during projection.
381
     * That way if the correction is bugged, we can always fix it and replay it with the original data.
382
     */
383
    private function determineCorrectTopStatusForProjection(): Status
384
    {
385
        // If the calendar has no subEvents, the top level status is always valid.
386
        if (empty($this->timestamps)) {
387
            return $this->status;
388
        }
389
390
        // If the calendar has subEvents, the top level status is valid if it is the same type as the type derived from
391
        // the subEvents. In that case return $this->status so we include the top-level reason (if it has one).
392
        $expectedStatusType = $this->deriveStatusTypeFromSubEvents();
393
        if ($this->status->getType()->equals($expectedStatusType)) {
394
            return $this->status;
395
        }
396
397
        // If the top-level status is invalid compared to the status type derived from the subEvents, return the
398
        // expected status type without any reason. (If the top level status had a reason it's probably not applicable
399
        // for the new status type.)
400
        return new Status($expectedStatusType, []);
401
    }
402
}
403