Completed
Push — master ( 6bef90...172c37 )
by Alex
18s
created

ServiceHost::getRequestMaxVersion()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 5
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 3
nc 1
nop 0
1
<?php
2
3
namespace POData\OperationContext;
4
5
use Illuminate\Http\Request;
6
use POData\Common\HttpStatus;
7
use POData\Common\Messages;
8
use POData\Common\MimeTypes;
9
use POData\Common\ODataConstants;
10
use POData\Common\ODataException;
11
use POData\Common\Url;
12
use POData\Common\UrlFormatException;
13
use POData\Common\Version;
14
use POData\OperationContext\Web\Illuminate\IlluminateOperationContext;
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>
67
     */
68
    private $queryOptions;
69
70
    /**
71
     * Gets reference to the operation context.
72
     *
73
     * @return IOperationContext
74
     */
75
    public function getOperationContext()
76
    {
77
        return $this->operationContext;
78
    }
79
80
    /**
81
     * @param IOperationContext|null    $context    the OperationContext implementation to use.
82
     *                                              If null the IlluminateOperationContext will be used.  Default null.
83
     *
84
     * Currently we are forcing the input request to be of type
85
     * \Illuminate\Http\Request but in the future we could make this more flexible if needed
86
     * @param Request $incomingRequest
87
     *
88
     * @throws ODataException
89
     */
90
    public function __construct(IOperationContext $context = null, Request $incomingRequest)
0 ignored issues
show
Coding Style introduced by
Parameters which have default values should be placed at the end.

If you place a parameter with a default value before a parameter with a default value, the default value of the first parameter will never be used as it will always need to be passed anyway:

// $a must always be passed; it's default value is never used.
function someFunction($a = 5, $b) { }
Loading history...
91
    {
92
        if (null === $context) {
93
            $this->operationContext = new IlluminateOperationContext($incomingRequest);
94
        } else {
95
            $this->operationContext = $context;
96
        }
97
98
        // getAbsoluteRequestUri can throw UrlFormatException
99
        // let Dispatcher handle it
100
        $this->absoluteRequestUri = $this->getAbsoluteRequestUri();
101
        $this->absoluteServiceUri = null;
102
103
        //Dev Note: Andrew Clinton 5/19/16
104
        //absoluteServiceUri is never being set from what I can tell
105
        //so for now we'll set it as such
106
        $this->setServiceUri($this->getServiceUri());
107
    }
108
109
    /**
110
     * Gets the absolute request Uri as Url instance
111
     * Note: This method will be called first time from constructor.
112
     *
113
     * @throws ODataException if AbsoluteRequestUri is not a valid URI
114
     *
115
     * @return Url
116
     */
117
    public function getAbsoluteRequestUri()
118
    {
119
        if (null === $this->absoluteRequestUri) {
120
            $this->absoluteRequestUriAsString = $this->getOperationContext()->incomingRequest()->getRawUrl();
121
            // Validate the uri first
122
            try {
123
                new Url($this->absoluteRequestUriAsString);
124
            } catch (UrlFormatException $exception) {
125
                throw ODataException::createBadRequestError($exception->getMessage());
126
            }
127
128
            $queryStartIndex = strpos($this->absoluteRequestUriAsString, '?');
129
            if ($queryStartIndex !== false) {
130
                $this->absoluteRequestUriAsString = substr(
131
                    $this->absoluteRequestUriAsString,
132
                    0,
133
                    $queryStartIndex
134
                );
135
            }
136
137
            // We need the absolute uri only not associated components
138
            // (query, fragments etc..)
139
            $this->absoluteRequestUri = new Url($this->absoluteRequestUriAsString);
140
            $this->absoluteRequestUriAsString = rtrim($this->absoluteRequestUriAsString, '/');
141
        }
142
143
        return $this->absoluteRequestUri;
144
    }
145
146
    /**
147
     * Gets the absolute request Uri as string
148
     * Note: This will not contain query string.
149
     *
150
     * @return string
151
     */
152
    public function getAbsoluteRequestUriAsString()
153
    {
154
        return $this->absoluteRequestUriAsString;
155
    }
156
157
    /**
158
     * Sets the service url from which the OData URL is parsed.
159
     *
160
     * @param string $serviceUri The service url, absolute or relative
161
     *
162
     * @throws ODataException If the base uri in the configuration is malformed
163
     */
164
    public function setServiceUri($serviceUri)
165
    {
166
        $builtServiceUri = null;
167
        if (null === $this->absoluteServiceUri) {
168
            $isAbsoluteServiceUri = (0 === strpos($serviceUri, 'http://')) || (0 === strpos($serviceUri, 'https://'));
169
            try {
170
                $this->absoluteServiceUri = new Url($serviceUri, $isAbsoluteServiceUri);
171
            } catch (UrlFormatException $exception) {
172
                throw ODataException::createInternalServerError(Messages::hostMalFormedBaseUriInConfig(false));
173
            }
174
175
            $segments = $this->absoluteServiceUri->getSegments();
176
            $lastSegment = $segments[count($segments) - 1];
177
            $sLen = strlen('.svc');
178
            $endsWithSvc = (0 === substr_compare($lastSegment, '.svc', -$sLen, $sLen));
179
            if (!$endsWithSvc
180
                || null !== $this->absoluteServiceUri->getQuery()
181
                || null !== $this->absoluteServiceUri->getFragment()
182
            ) {
183
                throw ODataException::createInternalServerError(Messages::hostMalFormedBaseUriInConfig(true));
184
            }
185
186
            if (!$isAbsoluteServiceUri) {
187
                $requestUriSegments = $this->getAbsoluteRequestUri()->getSegments();
188
                $requestUriScheme = $this->getAbsoluteRequestUri()->getScheme();
189
                $requestUriPort = $this->getAbsoluteRequestUri()->getPort();
190
                $i = count($requestUriSegments) - 1;
191
                // Find index of segment in the request uri that end with .svc
192
                // There will be always a .svc segment in the request uri otherwise
193
                // uri redirection will not happen.
194
                for (; $i >= 0; --$i) {
195
                    $endsWithSvc = (0 === substr_compare($requestUriSegments[$i], '.svc', -$sLen, $sLen));
196
                    if ($endsWithSvc) {
197
                        break;
198
                    }
199
                }
200
201
                $j = count($segments) - 1;
0 ignored issues
show
Comprehensibility introduced by
Avoid variables with short names like $j. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
202
                $k = $i;
0 ignored issues
show
Comprehensibility introduced by
Avoid variables with short names like $k. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
203
                if ($j > $i) {
204
                    throw ODataException::createBadRequestError(
205
                        Messages::hostRequestUriIsNotBasedOnRelativeUriInConfig(
206
                            $this->absoluteRequestUriAsString,
207
                            $serviceUri
208
                        )
209
                    );
210
                }
211
212
                while (0 <= $j && ($requestUriSegments[$i] === $segments[$j])) {
213
                    --$i;
214
                    --$j;
215
                }
216
217
                if (-1 != $j) {
218
                    throw ODataException::createBadRequestError(
219
                        Messages::hostRequestUriIsNotBasedOnRelativeUriInConfig(
220
                            $this->absoluteRequestUriAsString,
221
                            $serviceUri
222
                        )
223
                    );
224
                }
225
226
                $builtServiceUri = $requestUriScheme .'://' . $this->getAbsoluteRequestUri()->getHost();
227
228
                if (($requestUriScheme == 'http' && $requestUriPort != '80') ||
229
                    ($requestUriScheme == 'https' && $requestUriPort != '443')
230
                ) {
231
                    $builtServiceUri .= ':' . $requestUriPort;
232
                }
233
234
                for ($l = 0; $l <= $k; ++$l) {
235
                    $builtServiceUri .= '/' . $requestUriSegments[$l];
236
                }
237
238
                $this->absoluteServiceUri = new Url($builtServiceUri);
239
            }
240
241
            $this->absoluteServiceUriAsString = $isAbsoluteServiceUri ? $serviceUri : $builtServiceUri;
242
        }
243
    }
244
245
    /**
246
     * Gets the absolute Uri to the service as Url instance.
247
     * Note: This will be the value taken from configuration file.
248
     *
249
     * @return Url
250
     */
251
    public function getAbsoluteServiceUri()
252
    {
253
        return $this->absoluteServiceUri;
254
    }
255
256
    /**
257
     * Gets the absolute Uri to the service as string
258
     * Note: This will be the value taken from configuration file.
259
     *
260
     * @return string
261
     */
262
    public function getAbsoluteServiceUriAsString()
263
    {
264
        return $this->absoluteServiceUriAsString;
265
    }
266
267
    /**
268
     * This method verifies the client provided url query parameters.
269
     *
270
     * A query parameter is valid if and only if all the following conditions hold:
271
     * 1. It does not duplicate another parameter
272
     * 2. It has a supplied value.
273
     * 3. If a non-OData query parameter, its name does not start with $.
274
     * A valid parameter is then stored in _queryOptions, while an invalid parameter
275
     * trips an ODataException
276
     *
277
     * @throws ODataException
278
     */
279
    public function validateQueryParameters()
280
    {
281
        $queryOptions = $this->getOperationContext()->incomingRequest()->getQueryParameters();
282
283
        reset($queryOptions);
284
        $namesFound = [];
285
        while ($queryOption = current($queryOptions)) {
286
            $optionName = key($queryOption);
287
            $optionValue = current($queryOption);
288
            if (!is_string($optionValue)) {
289
                $optionName = array_keys($optionValue)[0];
290
                $optionValue = $optionValue[$optionName];
291
            }
292
            if (empty($optionName)) {
293
                if (!empty($optionValue)) {
294
                    if ('$' == $optionValue[0]) {
295
                        if ($this->isODataQueryOption($optionValue)) {
296
                            throw ODataException::createBadRequestError(
297
                                Messages::hostODataQueryOptionFoundWithoutValue($optionValue)
298
                            );
299
                        } else {
300
                            throw ODataException::createBadRequestError(
301
                                Messages::hostNonODataOptionBeginsWithSystemCharacter($optionValue)
302
                            );
303
                        }
304
                    }
305
                }
306
            } else {
307
                if ('$' == $optionName[0]) {
308
                    if (!$this->isODataQueryOption($optionName)) {
309
                        throw ODataException::createBadRequestError(
310
                            Messages::hostNonODataOptionBeginsWithSystemCharacter($optionName)
311
                        );
312
                    }
313
314
                    if (false !== array_search($optionName, $namesFound)) {
315
                        throw ODataException::createBadRequestError(
316
                            Messages::hostODataQueryOptionCannotBeSpecifiedMoreThanOnce($optionName)
317
                        );
318
                    }
319
320
                    if (empty($optionValue) && '0' !== $optionValue) {
321
                        throw ODataException::createBadRequestError(
322
                            Messages::hostODataQueryOptionFoundWithoutValue($optionName)
323
                        );
324
                    }
325
326
                    $namesFound[] = $optionName;
327
                }
328
            }
329
330
            next($queryOptions);
331
        }
332
333
        $this->queryOptions = $queryOptions;
334
    }
335
336
    /**
337
     * Dev Note: Andrew Clinton
338
     * 5/19/16.
339
     *
340
     * Currently it doesn't seem that the service URI is ever being built
341
     * so I am doing that here.
342
     *
343
     * return void
344
     */
345
    private function getServiceUri()
346
    {
347
        if (($pos = strpos($this->absoluteRequestUriAsString, '.svc')) !== false) {
348
            $serviceUri = substr($this->absoluteRequestUriAsString, 0, $pos + strlen('.svc'));
349
350
            return $serviceUri;
351
        }
352
353
        return $this->absoluteRequestUriAsString;
354
    }
355
356
    /**
357
     * Verifies the given url option is a valid odata query option.
358
     *
359
     * @param string $optionName option to validate
360
     *
361
     * @return bool True if the given option is a valid odata option False otherwise
362
     */
363
    private function isODataQueryOption($optionName)
364
    {
365
        return $optionName === ODataConstants::HTTPQUERY_STRING_FILTER ||
366
                $optionName === ODataConstants::HTTPQUERY_STRING_EXPAND ||
367
                $optionName === ODataConstants::HTTPQUERY_STRING_INLINECOUNT ||
368
                $optionName === ODataConstants::HTTPQUERY_STRING_ORDERBY ||
369
                $optionName === ODataConstants::HTTPQUERY_STRING_SELECT ||
370
                $optionName === ODataConstants::HTTPQUERY_STRING_SKIP ||
371
                $optionName === ODataConstants::HTTPQUERY_STRING_SKIPTOKEN ||
372
                $optionName === ODataConstants::HTTPQUERY_STRING_TOP ||
373
                $optionName === ODataConstants::HTTPQUERY_STRING_FORMAT;
374
    }
375
376
    /**
377
     * Gets the value for the specified item in the request query string
378
     * Remark: This method assumes 'validateQueryParameters' has already been
379
     * called.
380
     *
381
     * @param string $item The query item to get the value of
382
     *
383
     * @return string|null The value for the specified item in the request
384
     *                     query string NULL if the query option is absent
385
     */
386
    public function getQueryStringItem($item)
387
    {
388
        foreach ($this->queryOptions as $queryOption) {
389
            if (array_key_exists($item, $queryOption)) {
390
                return $queryOption[$item];
391
            }
392
        }
393
        return null;
394
    }
395
396
    /**
397
     * Gets the value for the DataServiceVersion header of the request.
398
     *
399
     * @return string|null
400
     */
401
    public function getRequestVersion()
402
    {
403
        $headerType = ODataConstants::HTTPREQUEST_HEADER_DATA_SERVICE_VERSION;
404
        return $this->getRequestHeader($headerType);
405
    }
406
407
    /**
408
     * Gets the value of MaxDataServiceVersion header of the request.
409
     *
410
     * @return string|null
411
     */
412
    public function getRequestMaxVersion()
413
    {
414
        $headerType = ODataConstants::HTTPREQUEST_HEADER_MAX_DATA_SERVICE_VERSION;
415
        return $this->getRequestHeader($headerType);
416
    }
417
418
    /**
419
     * Get comma separated list of client-supported MIME Accept types.
420
     *
421
     * @return string|null
422
     */
423
    public function getRequestAccept()
424
    {
425
        $headerType = ODataConstants::HTTPREQUEST_HEADER_ACCEPT;
426
        return $this->getRequestHeader($headerType);
427
    }
428
429
    /**
430
     * Get the character set encoding that the client requested.
431
     *
432
     * @return string|null
433
     */
434
    public function getRequestAcceptCharSet()
435
    {
436
        $headerType = ODataConstants::HTTPREQUEST_HEADER_ACCEPT_CHARSET;
437
        return $this->getRequestHeader($headerType);
438
    }
439
440
    /**
441
     * Get the value of If-Match header of the request.
442
     *
443
     * @return string|null
444
     */
445
    public function getRequestIfMatch()
446
    {
447
        $headerType = ODataConstants::HTTPREQUEST_HEADER_IF_MATCH;
448
        return $this->getRequestHeader($headerType);
449
    }
450
451
    /**
452
     * Gets the value of If-None-Match header of the request.
453
     *
454
     * @return string|null
455
     */
456
    public function getRequestIfNoneMatch()
457
    {
458
        $headerType = ODataConstants::HTTPREQUEST_HEADER_IF_NONE;
459
        return $this->getRequestHeader($headerType);
460
    }
461
462
    /**
463
     * Gets the value of Content-Type header of the request.
464
     *
465
     * @return string|null
466
     */
467
    public function getRequestContentType()
468
    {
469
        $headerType = ODataConstants::HTTP_CONTENTTYPE;
470
        return $this->getRequestHeader($headerType);
471
    }
472
473
    /**
474
     * Set the Cache-Control header on the response.
475
     *
476
     * @param string $value The cache-control value
477
     */
478
    public function setResponseCacheControl($value)
479
    {
480
        $this->getOperationContext()->outgoingResponse()->setCacheControl($value);
481
    }
482
483
    /**
484
     * Gets the HTTP MIME type of the output stream.
485
     *
486
     * @return string
487
     */
488
    public function getResponseContentType()
489
    {
490
        return $this->getOperationContext()->outgoingResponse()->getContentType();
491
    }
492
493
    /**
494
     * Sets the HTTP MIME type of the output stream.
495
     *
496
     * @param string $value The HTTP MIME type
497
     * @return void
498
     */
499
    public function setResponseContentType($value)
500
    {
501
        $this->getOperationContext()->outgoingResponse()->setContentType($value);
502
    }
503
504
    /**
505
     * Sets the content length of the output stream.
506
     *
507
     * @param string $value The content length
508
     *
509
     * @throws ODataException
510
     * @return void
511
     */
512
    public function setResponseContentLength($value)
513
    {
514
        if (preg_match('/[0-9]+/', $value)) {
515
            $this->getOperationContext()->outgoingResponse()->setContentLength($value);
516
        } else {
517
            throw ODataException::notAcceptableError(
518
                'ContentLength: '.$value.' is invalid'
519
            );
520
        }
521
    }
522
523
    /**
524
     * Gets the value of the ETag header on the response.
525
     *
526
     * @return string|null
527
     */
528
    public function getResponseETag()
529
    {
530
        return $this->getOperationContext()->outgoingResponse()->getETag();
531
    }
532
533
    /**
534
     * Sets the value of the ETag header on the response.
535
     *
536
     * @param string $value The ETag value
537
     */
538
    public function setResponseETag($value)
539
    {
540
        $this->getOperationContext()->outgoingResponse()->setETag($value);
541
    }
542
543
    /**
544
     * Sets the value Location header on the response.
545
     *
546
     * @param string $value The location
547
     */
548
    public function setResponseLocation($value)
549
    {
550
        $this->getOperationContext()->outgoingResponse()->setLocation($value);
551
    }
552
553
    /**
554
     * Sets the value status code header on the response.
555
     *
556
     * @param string $value The status code
557
     *
558
     * @throws ODataException
559
     */
560
    public function setResponseStatusCode($value)
561
    {
562
        if (!is_numeric($value)) {
563
            $msg = 'Invalid, non-numeric, status code: '.$value;
564
            throw ODataException::createInternalServerError($msg);
565
        }
566
        $floor = floor($value/100);
567
        if ($floor >= 1 && $floor <= 5) {
568
            $statusDescription = HttpStatus::getStatusDescription($value);
569
            if (null !== $statusDescription) {
570
                $statusDescription = ' ' . $statusDescription;
571
            }
572
573
            $this->getOperationContext()->outgoingResponse()->setStatusCode($value . $statusDescription);
574
        } else {
575
            $msg = 'Invalid status code: ' . $value;
576
            throw ODataException::createInternalServerError($msg);
577
        }
578
    }
579
580
    /**
581
     * Sets the value status description header on the response.
582
     *
583
     * @param string $value The status description
584
     */
585
    public function setResponseStatusDescription($value)
586
    {
587
        $this->getOperationContext()->outgoingResponse()->setStatusDescription($value);
588
    }
589
590
    /**
591
     * Sets the value stream to be send a response.
592
     *
593
     * @param string &$value The stream
594
     */
595
    public function setResponseStream(&$value)
596
    {
597
        $this->getOperationContext()->outgoingResponse()->setStream($value);
598
    }
599
600
    /**
601
     * Sets the DataServiceVersion response header.
602
     *
603
     * @param string $value The version
604
     */
605
    public function setResponseVersion($value)
606
    {
607
        $this->getOperationContext()->outgoingResponse()->setServiceVersion($value);
608
    }
609
610
    /**
611
     * Get the response headers.
612
     *
613
     * @return array<string,string>
614
     */
615
    public function &getResponseHeaders()
616
    {
617
        return $this->getOperationContext()->outgoingResponse()->getHeaders();
618
    }
619
620
    /**
621
     * Add a header to response header collection.
622
     *
623
     * @param string $headerName  The name of the header
624
     * @param string $headerValue The value of the header
625
     */
626
    public function addResponseHeader($headerName, $headerValue)
627
    {
628
        $this->getOperationContext()->outgoingResponse()->addHeader($headerName, $headerValue);
629
    }
630
631
    /**
632
     * Translates the short $format forms into the full mime type forms.
633
     *
634
     * @param Version $responseVersion the version scheme to interpret the short form with
635
     * @param string  $format          the short $format form
636
     *
637
     * @return string the full mime type corresponding to the short format form for the given version
638
     */
639
    public static function translateFormatToMime(Version $responseVersion, $format)
640
    {
641
        //TODO: should the version switches be off of the requestVersion, not the response version? see #91
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
642
643
        switch ($format) {
644
            case ODataConstants::FORMAT_XML:
645
                $format = MimeTypes::MIME_APPLICATION_XML;
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $format. This often makes code more readable.
Loading history...
646
                break;
647
648
            case ODataConstants::FORMAT_ATOM:
649
                $format = MimeTypes::MIME_APPLICATION_ATOM;
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $format. This often makes code more readable.
Loading history...
650
                break;
651
652
            case ODataConstants::FORMAT_VERBOSE_JSON:
653
                if ($responseVersion == Version::v3()) {
654
                    //only translatable in 3.0 systems
655
                    $format = MimeTypes::MIME_APPLICATION_JSON_VERBOSE;
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $format. This often makes code more readable.
Loading history...
656
                }
657
                break;
658
659
            case ODataConstants::FORMAT_JSON:
660
                if ($responseVersion == Version::v3()) {
661
                    $format = MimeTypes::MIME_APPLICATION_JSON_MINIMAL_META;
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $format. This often makes code more readable.
Loading history...
662
                } else {
663
                    $format = MimeTypes::MIME_APPLICATION_JSON;
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $format. This often makes code more readable.
Loading history...
664
                }
665
                break;
666
        }
667
668
        return $format . ';q=1.0';
669
    }
670
671
    /**
672
     * @param $headerType
673
     * @return null|string
674
     */
675
    private function getRequestHeader($headerType)
676
    {
677
        $result = $this->getOperationContext()->incomingRequest()->getRequestHeader($headerType);
678
        assert(null === $result || is_string($result));
679
        return $result;
680
    }
681
}
682