ServiceHost::setResponseETag()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace POData\OperationContext;
6
7
use POData\Common\HttpStatus;
8
use POData\Common\Messages;
9
use POData\Common\MimeTypes;
10
use POData\Common\ODataConstants;
11
use POData\Common\ODataException;
12
use POData\Common\Url;
13
use POData\Common\UrlFormatException;
14
use POData\Common\Version;
15
16
/**
17
 * Class ServiceHost.
18
 *
19
 * It uses an IOperationContext implementation to get/set all context related
20
 * headers/stream info It also validates the each header value
21
 */
22
class ServiceHost
23
{
24
    /**
25
     * Holds reference to the underlying operation context.
26
     *
27
     * @var IOperationContext
28
     */
29
    private $operationContext;
30
31
    /**
32
     * The absolute request Uri as Url instance.
33
     * Note: This will not contain query string.
34
     *
35
     * @var Url
36
     */
37
    private $absoluteRequestUri;
38
39
    /**
40
     * The absolute request Uri as string
41
     * Note: This will not contain query string.
42
     *
43
     * @var string
44
     */
45
    private $absoluteRequestUriAsString = null;
46
47
    /**
48
     * The absolute service uri as Url instance.
49
     * Note: This value will be taken from configuration file.
50
     *
51
     * @var Url
52
     */
53
    private $absoluteServiceUri;
54
55
    /**
56
     * The absolute service uri string.
57
     * Note: This value will be taken from configuration file.
58
     *
59
     * @var string
60
     */
61
    private $absoluteServiceUriAsString = null;
62
63
    /**
64
     * array of query-string parameters.
65
     *
66
     * @var array<string|array>
67
     */
68
    private $queryOptions;
69
70
    /**
71
     * @param IOperationContext $context the OperationContext implementation to use
72
     *
73
     * @throws ODataException
74
     * @throws UrlFormatException
75
     */
76
    public function __construct(IOperationContext $context)
77
    {
78
        $this->operationContext = $context;
79
80
        // getAbsoluteRequestUri can throw UrlFormatException
81
        // let Dispatcher handle it
82
        $this->absoluteRequestUri = $this->getAbsoluteRequestUri();
83
        $this->absoluteServiceUri = null;
84
85
        //Dev Note: Andrew Clinton 5/19/16
86
        //absoluteServiceUri is never being set from what I can tell
87
        //so for now we'll set it as such
88
        $this->setServiceUri($this->getServiceUri());
89
    }
90
91
    /**
92
     * Gets the absolute request Uri as Url instance
93
     * Note: This method will be called first time from constructor.
94
     *
95
     * @throws UrlFormatException
96
     * @throws ODataException     if AbsoluteRequestUri is not a valid URI
97
     * @return Url
98
     */
99
    public function getAbsoluteRequestUri(): Url
100
    {
101
        if (null === $this->absoluteRequestUri) {
102
            $this->absoluteRequestUriAsString = $this->getOperationContext()->incomingRequest()->getRawUrl();
103
            // Validate the uri first
104
            try {
105
                new Url($this->absoluteRequestUriAsString);
106
            } catch (UrlFormatException $exception) {
107
                throw ODataException::createBadRequestError($exception->getMessage());
108
            }
109
110
            $queryStartIndex = strpos($this->absoluteRequestUriAsString, '?');
111
            if ($queryStartIndex !== false) {
112
                $this->absoluteRequestUriAsString = substr(
113
                    $this->absoluteRequestUriAsString,
114
                    0,
115
                    $queryStartIndex
116
                );
117
            }
118
119
            // We need the absolute uri only not associated components
120
            // (query, fragments etc..)
121
            $this->absoluteRequestUri         = new Url($this->absoluteRequestUriAsString);
122
            $this->absoluteRequestUriAsString = rtrim($this->absoluteRequestUriAsString, '/');
123
        }
124
125
        return $this->absoluteRequestUri;
126
    }
127
128
    /**
129
     * Gets reference to the operation context.
130
     *
131
     * @return IOperationContext
132
     */
133
    public function getOperationContext()
134
    {
135
        return $this->operationContext;
136
    }
137
138
    /**
139
     * Sets the service url from which the OData URL is parsed.
140
     *
141
     * @param string $serviceUri The service url, absolute or relative
142
     *
143
     * @throws ODataException     If the base uri in the configuration is malformed
144
     * @throws UrlFormatException
145
     */
146
    public function setServiceUri($serviceUri): void
147
    {
148
        $builtServiceUri = null;
149
        if (null === $this->absoluteServiceUri) {
150
            $isAbsoluteServiceUri = (0 === strpos($serviceUri, 'http://')) || (0 === strpos($serviceUri, 'https://'));
151
            try {
152
                $this->absoluteServiceUri = new Url($serviceUri, $isAbsoluteServiceUri);
153
            } catch (UrlFormatException $exception) {
154
                throw ODataException::createInternalServerError(Messages::hostMalFormedBaseUriInConfig(false));
155
            }
156
157
            $segments    = $this->absoluteServiceUri->getSegments();
158
            $lastSegment = $segments[count($segments) - 1];
159
            $sLen        = strlen('.svc');
160
            $endsWithSvc = (0 === substr_compare($lastSegment, '.svc', -$sLen, $sLen));
161
            if (!$endsWithSvc
162
                || null !== $this->absoluteServiceUri->getQuery()
163
                || null !== $this->absoluteServiceUri->getFragment()
164
            ) {
165
                throw ODataException::createInternalServerError(Messages::hostMalFormedBaseUriInConfig(true));
166
            }
167
168
            if (!$isAbsoluteServiceUri) {
169
                $requestUriSegments = $this->getAbsoluteRequestUri()->getSegments();
170
                $requestUriScheme   = $this->getAbsoluteRequestUri()->getScheme();
171
                $requestUriPort     = $this->getAbsoluteRequestUri()->getPort();
172
                $i                  = count($requestUriSegments) - 1;
173
                // Find index of segment in the request uri that end with .svc
174
                // There will be always a .svc segment in the request uri otherwise
175
                // uri redirection will not happen.
176
                for (; $i >= 0; --$i) {
177
                    $endsWithSvc = (0 === substr_compare($requestUriSegments[$i], '.svc', -$sLen, $sLen));
178
                    if ($endsWithSvc) {
179
                        break;
180
                    }
181
                }
182
183
                $j = count($segments) - 1;
184
                $k = $i;
185
                if ($j > $i) {
186
                    throw ODataException::createBadRequestError(
187
                        Messages::hostRequestUriIsNotBasedOnRelativeUriInConfig(
188
                            $this->absoluteRequestUriAsString,
189
                            $serviceUri
190
                        )
191
                    );
192
                }
193
194
                while (0 <= $j && ($requestUriSegments[$i] === $segments[$j])) {
195
                    --$i;
196
                    --$j;
197
                }
198
199
                if (-1 != $j) {
200
                    throw ODataException::createBadRequestError(
201
                        Messages::hostRequestUriIsNotBasedOnRelativeUriInConfig(
202
                            $this->absoluteRequestUriAsString,
203
                            $serviceUri
204
                        )
205
                    );
206
                }
207
208
                $builtServiceUri = $requestUriScheme . '://' . $this->getAbsoluteRequestUri()->getHost();
209
210
                if (($requestUriScheme == 'http' && $requestUriPort != '80') ||
211
                    ($requestUriScheme == 'https' && $requestUriPort != '443')
212
                ) {
213
                    $builtServiceUri .= ':' . $requestUriPort;
214
                }
215
216
                for ($l = 0; $l <= $k; ++$l) {
217
                    $builtServiceUri .= '/' . $requestUriSegments[$l];
218
                }
219
220
                $this->absoluteServiceUri = new Url($builtServiceUri);
221
            }
222
223
            $this->absoluteServiceUriAsString = $isAbsoluteServiceUri ? $serviceUri : $builtServiceUri;
224
        }
225
    }
226
227
    /**
228
     * Dev Note: Andrew Clinton
229
     * 5/19/16.
230
     *
231
     * Currently it doesn't seem that the service URI is ever being built
232
     * so I am doing that here.
233
     *
234
     * return string
235
     */
236
    private function getServiceUri(): string
237
    {
238
        if (($pos = strpos($this->absoluteRequestUriAsString, '.svc')) !== false) {
239
            $serviceUri = substr($this->absoluteRequestUriAsString, 0, $pos + strlen('.svc'));
240
241
            return $serviceUri;
242
        }
243
244
        return $this->absoluteRequestUriAsString;
245
    }
246
247
    /**
248
     * Translates the short $format forms into the full mime type forms.
249
     *
250
     * @param Version $responseVersion the version scheme to interpret the short form with
251
     * @param string  $format          the short $format form
252
     *
253
     * @return string the full mime type corresponding to the short format form for the given version
254
     */
255
    public static function translateFormatToMime(Version $responseVersion, string $format): string
256
    {
257
        //TODO: should the version switches be off of the requestVersion, not the response version? see #91
258
259
        switch ($format) {
260
            case ODataConstants::FORMAT_XML:
261
                $format = MimeTypes::MIME_APPLICATION_XML;
262
                break;
263
264
            case ODataConstants::FORMAT_ATOM:
265
                $format = MimeTypes::MIME_APPLICATION_ATOM;
266
                break;
267
268
            case ODataConstants::FORMAT_VERBOSE_JSON:
269
                if ($responseVersion == Version::v3()) {
270
                    //only translatable in 3.0 systems
271
                    $format = MimeTypes::MIME_APPLICATION_JSON_VERBOSE;
272
                }
273
                break;
274
275
            case ODataConstants::FORMAT_JSON:
276
                if ($responseVersion == Version::v3()) {
277
                    $format = MimeTypes::MIME_APPLICATION_JSON_MINIMAL_META;
278
                } else {
279
                    $format = MimeTypes::MIME_APPLICATION_JSON;
280
                }
281
                break;
282
        }
283
284
        return $format . ';q=1.0';
285
    }
286
287
    /**
288
     * Gets the absolute request Uri as string
289
     * Note: This will not contain query string.
290
     *
291
     * @return string
292
     */
293
    public function getAbsoluteRequestUriAsString(): string
294
    {
295
        return $this->absoluteRequestUriAsString;
296
    }
297
298
    /**
299
     * Gets the absolute Uri to the service as Url instance.
300
     * Note: This will be the value taken from configuration file.
301
     *
302
     * @return Url
303
     */
304
    public function getAbsoluteServiceUri(): Url
305
    {
306
        return $this->absoluteServiceUri;
307
    }
308
309
    /**
310
     * Gets the absolute Uri to the service as string
311
     * Note: This will be the value taken from configuration file.
312
     *
313
     * @return string
314
     */
315
    public function getAbsoluteServiceUriAsString(): string
316
    {
317
        return $this->absoluteServiceUriAsString;
318
    }
319
320
    /**
321
     * This method verifies the client provided url query parameters.
322
     *
323
     * A query parameter is valid if and only if all the following conditions hold:
324
     * 1. It does not duplicate another parameter
325
     * 2. It has a supplied value.
326
     * 3. If a non-OData query parameter, its name does not start with $.
327
     * A valid parameter is then stored in _queryOptions, while an invalid parameter
328
     * trips an ODataException
329
     *
330
     * @throws ODataException
331
     */
332
    public function validateQueryParameters(): void
333
    {
334
        $queryOptions = $this->getOperationContext()->incomingRequest()->getQueryParameters();
335
336
        reset($queryOptions);
337
        $namesFound = [];
338
        while ($queryOption = current($queryOptions)) {
339
            $optionName  = key($queryOption);
340
            $optionValue = current($queryOption);
341
            if (!is_string($optionValue)) {
342
                $optionName  = array_keys($optionValue)[0];
343
                $optionValue = $optionValue[$optionName];
344
            }
345
            if (empty($optionName)) {
346
                if (!empty($optionValue)) {
347
                    if ('$' == $optionValue[0]) {
348
                        if ($this->isODataQueryOption($optionValue)) {
349
                            throw ODataException::createBadRequestError(
350
                                Messages::hostODataQueryOptionFoundWithoutValue($optionValue)
351
                            );
352
                        } else {
353
                            throw ODataException::createBadRequestError(
354
                                Messages::hostNonODataOptionBeginsWithSystemCharacter($optionValue)
355
                            );
356
                        }
357
                    }
358
                }
359
            } else {
360
                if ('$' == $optionName[0]) {
361
                    if (!$this->isODataQueryOption($optionName)) {
362
                        throw ODataException::createBadRequestError(
363
                            Messages::hostNonODataOptionBeginsWithSystemCharacter($optionName)
364
                        );
365
                    }
366
367
                    if (false !== array_search($optionName, $namesFound)) {
368
                        throw ODataException::createBadRequestError(
369
                            Messages::hostODataQueryOptionCannotBeSpecifiedMoreThanOnce($optionName)
370
                        );
371
                    }
372
373
                    if (empty($optionValue) && '0' !== $optionValue) {
374
                        throw ODataException::createBadRequestError(
375
                            Messages::hostODataQueryOptionFoundWithoutValue($optionName)
376
                        );
377
                    }
378
379
                    $namesFound[] = $optionName;
380
                }
381
            }
382
383
            next($queryOptions);
384
        }
385
386
        $this->queryOptions = $queryOptions;
387
    }
388
389
    /**
390
     * Verifies the given url option is a valid odata query option.
391
     *
392
     * @param string $optionName option to validate
393
     *
394
     * @return bool True if the given option is a valid odata option False otherwise
395
     */
396
    private function isODataQueryOption($optionName): bool
397
    {
398
        return $optionName === ODataConstants::HTTPQUERY_STRING_FILTER ||
399
            $optionName === ODataConstants::HTTPQUERY_STRING_EXPAND ||
400
            $optionName === ODataConstants::HTTPQUERY_STRING_INLINECOUNT ||
401
            $optionName === ODataConstants::HTTPQUERY_STRING_ORDERBY ||
402
            $optionName === ODataConstants::HTTPQUERY_STRING_SELECT ||
403
            $optionName === ODataConstants::HTTPQUERY_STRING_SKIP ||
404
            $optionName === ODataConstants::HTTPQUERY_STRING_SKIPTOKEN ||
405
            $optionName === ODataConstants::HTTPQUERY_STRING_TOP ||
406
            $optionName === ODataConstants::HTTPQUERY_STRING_FORMAT;
407
    }
408
409
    /**
410
     * Gets the value for the specified item in the request query string
411
     * Remark: This method assumes 'validateQueryParameters' has already been
412
     * called.
413
     *
414
     * @param string $item The query item to get the value of
415
     *
416
     * @return string|null The value for the specified item in the request
417
     *                     query string NULL if the query option is absent
418
     */
419
    public function getQueryStringItem(string $item): ?string
420
    {
421
        foreach ($this->queryOptions as $queryOption) {
422
            if (array_key_exists($item, $queryOption)) {
0 ignored issues
show
Bug introduced by
It seems like $queryOption can also be of type string; however, parameter $array of array_key_exists() does only seem to accept ArrayObject|array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

422
            if (array_key_exists($item, /** @scrutinizer ignore-type */ $queryOption)) {
Loading history...
423
                return $queryOption[$item];
424
            }
425
        }
426
        return null;
427
    }
428
429
    /**
430
     * Gets the value for the DataServiceVersion header of the request.
431
     *
432
     * @return string|null
433
     */
434
    public function getRequestVersion(): ?string
435
    {
436
        $headerType = ODataConstants::HTTPREQUEST_HEADER_DATA_SERVICE_VERSION;
437
        return $this->getRequestHeader($headerType);
438
    }
439
440
    /**
441
     * @param  string      $headerType
442
     * @return null|string
443
     */
444
    private function getRequestHeader(string $headerType): ?string
445
    {
446
        $result = $this->getOperationContext()->incomingRequest()->getRequestHeader($headerType);
447
        assert(null === $result || is_string($result));
448
        return $result;
449
    }
450
451
    /**
452
     * Gets the value of MaxDataServiceVersion header of the request.
453
     *
454
     * @return string|null
455
     */
456
    public function getRequestMaxVersion(): ?string
457
    {
458
        $headerType = ODataConstants::HTTPREQUEST_HEADER_MAX_DATA_SERVICE_VERSION;
459
        return $this->getRequestHeader($headerType);
460
    }
461
462
    /**
463
     * Get comma separated list of client-supported MIME Accept types.
464
     *
465
     * @return string|null
466
     */
467
    public function getRequestAccept(): ?string
468
    {
469
        $headerType = ODataConstants::HTTPREQUEST_HEADER_ACCEPT;
470
        return $this->getRequestHeader($headerType) ?? '*/*';
471
    }
472
473
    /**
474
     * Get the character set encoding that the client requested.
475
     *
476
     * @return string|null
477
     */
478
    public function getRequestAcceptCharSet(): ?string
479
    {
480
        $headerType = ODataConstants::HTTPREQUEST_HEADER_ACCEPT_CHARSET;
481
        return $this->getRequestHeader($headerType);
482
    }
483
484
    /**
485
     * Get the value of If-Match header of the request.
486
     *
487
     * @return string|null
488
     */
489
    public function getRequestIfMatch(): ?string
490
    {
491
        $headerType = ODataConstants::HTTPREQUEST_HEADER_IF_MATCH;
492
        return $this->getRequestHeader($headerType);
493
    }
494
495
    /**
496
     * Gets the value of If-None-Match header of the request.
497
     *
498
     * @return string|null
499
     */
500
    public function getRequestIfNoneMatch(): ?string
501
    {
502
        $headerType = ODataConstants::HTTPREQUEST_HEADER_IF_NONE;
503
        return $this->getRequestHeader($headerType);
504
    }
505
506
    /**
507
     * Gets the value of Content-Type header of the request.
508
     *
509
     * @return string|null
510
     */
511
    public function getRequestContentType(): ?string
512
    {
513
        $headerType = ODataConstants::HTTP_CONTENTTYPE;
514
        return $this->getRequestHeader($headerType);
515
    }
516
517
    /**
518
     * Set the Cache-Control header on the response.
519
     *
520
     * @param string $value The cache-control value
521
     */
522
    public function setResponseCacheControl($value): void
523
    {
524
        $this->getOperationContext()->outgoingResponse()->setCacheControl($value);
525
    }
526
527
    /**
528
     * Gets the HTTP MIME type of the output stream.
529
     *
530
     * @return string
531
     */
532
    public function getResponseContentType(): string
533
    {
534
        return $this->getOperationContext()->outgoingResponse()->getContentType();
535
    }
536
537
    /**
538
     * Sets the HTTP MIME type of the output stream.
539
     *
540
     * @param  string $value The HTTP MIME type
541
     * @return void
542
     */
543
    public function setResponseContentType($value): void
544
    {
545
        $this->getOperationContext()->outgoingResponse()->setContentType($value);
546
    }
547
548
    /**
549
     * Sets the content length of the output stream.
550
     *
551
     * @param string $value The content length
552
     *
553
     * @throws ODataException
554
     * @return void
555
     */
556
    public function setResponseContentLength($value): void
557
    {
558
        if (preg_match('/[0-9]+/', $value)) {
559
            $this->getOperationContext()->outgoingResponse()->setContentLength($value);
560
        } else {
561
            throw ODataException::notAcceptableError(
562
                'ContentLength: ' . $value . ' is invalid'
563
            );
564
        }
565
    }
566
567
    /**
568
     * Gets the value of the ETag header on the response.
569
     *
570
     * @return string|null
571
     */
572
    public function getResponseETag(): ?string
573
    {
574
        return $this->getOperationContext()->outgoingResponse()->getETag();
575
    }
576
577
    /**
578
     * Sets the value of the ETag header on the response.
579
     *
580
     * @param string $value The ETag value
581
     */
582
    public function setResponseETag(string $value): void
583
    {
584
        $this->getOperationContext()->outgoingResponse()->setETag($value);
585
    }
586
587
    /**
588
     * Sets the value Location header on the response.
589
     *
590
     * @param string $value The location
591
     */
592
    public function setResponseLocation(string $value): void
593
    {
594
        $this->getOperationContext()->outgoingResponse()->setLocation($value);
595
    }
596
597
    /**
598
     * Sets the value status code header on the response.
599
     *
600
     * @param int $value The status code
601
     *
602
     * @throws ODataException
603
     */
604
    public function setResponseStatusCode(int $value): void
605
    {
606
        $floor = floor($value / 100);
607
        if ($floor >= 1 && $floor <= 5) {
608
            $statusDescription = HttpStatus::getStatusDescription($value);
609
            if (null !== $statusDescription) {
610
                $statusDescription = ' ' . $statusDescription;
611
            }
612
613
            $this->getOperationContext()->outgoingResponse()->setStatusCode($value . $statusDescription);
614
        } else {
615
            $msg = 'Invalid status code: ' . $value;
616
            throw ODataException::createInternalServerError($msg);
617
        }
618
    }
619
620
    /**
621
     * Sets the value status description header on the response.
622
     *
623
     * @param string $value The status description
624
     */
625
    public function setResponseStatusDescription(string $value): void
626
    {
627
        $this->getOperationContext()->outgoingResponse()->setStatusDescription($value);
628
    }
629
630
    /**
631
     * Sets the value stream to be send a response.
632
     *
633
     * @param string &$value The stream
634
     */
635
    public function setResponseStream(string &$value): void
636
    {
637
        $this->getOperationContext()->outgoingResponse()->setStream($value);
638
    }
639
640
    /**
641
     * Sets the DataServiceVersion response header.
642
     *
643
     * @param string $value The version
644
     */
645
    public function setResponseVersion(string $value): void
646
    {
647
        $this->getOperationContext()->outgoingResponse()->setServiceVersion($value);
648
    }
649
650
    /**
651
     * Get the response headers.
652
     *
653
     * @return array<string,string>
654
     */
655
    public function &getResponseHeaders(): array
656
    {
657
        return $this->getOperationContext()->outgoingResponse()->getHeaders();
658
    }
659
660
    /**
661
     * Add a header to response header collection.
662
     *
663
     * @param string $headerName  The name of the header
664
     * @param string $headerValue The value of the header
665
     */
666
    public function addResponseHeader(string $headerName, string $headerValue): void
667
    {
668
        $this->getOperationContext()->outgoingResponse()->addHeader($headerName, $headerValue);
669
    }
670
}
671