Completed
Push — master ( 08a7d1...bfa3ea )
by Alex
15s queued 13s
created

BaseService::getResponseContentType()   D

Complexity

Conditions 22
Paths 102

Size

Total Lines 130
Code Lines 74

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 22
eloc 74
c 1
b 0
f 0
nc 102
nop 2
dl 0
loc 130
rs 4.15

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace POData;
6
7
use POData\BatchProcessor\BatchProcessor;
8
use POData\Common\ErrorHandler;
9
use POData\Common\HttpStatus;
10
use POData\Common\InvalidOperationException;
11
use POData\Common\Messages;
12
use POData\Common\MimeTypes;
13
use POData\Common\NotImplementedException;
14
use POData\Common\ODataConstants;
15
use POData\Common\ODataException;
16
use POData\Common\ReflectionHandler;
17
use POData\Common\Version;
18
use POData\Configuration\IServiceConfiguration;
19
use POData\Configuration\ServiceConfiguration;
20
use POData\ObjectModel\IObjectSerialiser;
21
use POData\ObjectModel\ObjectModelSerializer;
22
use POData\ObjectModel\ODataFeed;
23
use POData\ObjectModel\ODataURLCollection;
24
use POData\OperationContext\HTTPRequestMethod;
25
use POData\OperationContext\IOperationContext;
26
use POData\OperationContext\ServiceHost;
27
use POData\Providers\Metadata\IMetadataProvider;
28
use POData\Providers\Metadata\ResourceType;
29
use POData\Providers\Metadata\Type\Binary;
30
use POData\Providers\Metadata\Type\IType;
31
use POData\Providers\ProvidersWrapper;
32
use POData\Providers\Query\IQueryProvider;
33
use POData\Providers\Query\QueryResult;
34
use POData\Providers\Stream\StreamProviderWrapper;
35
use POData\Readers\Atom\AtomODataReader;
36
use POData\Readers\ODataReaderRegistry;
37
use POData\UriProcessor\Interfaces\IUriProcessor;
38
use POData\UriProcessor\RequestDescription;
39
use POData\UriProcessor\ResourcePathProcessor\SegmentParser\TargetKind;
40
use POData\UriProcessor\UriProcessor;
41
use POData\UriProcessor\UriProcessorNew;
42
use POData\Writers\Atom\AtomODataWriter;
43
use POData\Writers\Json\JsonLightMetadataLevel;
44
use POData\Writers\Json\JsonLightODataWriter;
45
use POData\Writers\Json\JsonODataV1Writer;
46
use POData\Writers\Json\JsonODataV2Writer;
47
use POData\Writers\ODataWriterRegistry;
48
use POData\Writers\ResponseWriter;
49
50
/**
51
 * Class BaseService.
52
 *
53
 * The base class for all BaseService specific classes. This class implements
54
 * the following interfaces:
55
 *  (1) IRequestHandler
56
 *      Implementing this interface requires defining the function
57
 *      'handleRequest' that will be invoked by dispatcher
58
 *  (2) IService
59
 *      Force BaseService class to implement functions for custom
60
 *      data service providers
61
 */
62
abstract class BaseService implements IRequestHandler, IService
63
{
64
    /**
65
     * The wrapper over IQueryProvider and IMetadataProvider implementations.
66
     *
67
     * @var ProvidersWrapper
68
     */
69
    private $providersWrapper;
70
71
    /**
72
     * The wrapper over IStreamProvider implementation.
73
     *
74
     * @var StreamProviderWrapper
75
     */
76
    protected $streamProvider;
77
78
    /**
79
     * Hold reference to the ServiceHost instance created by dispatcher,
80
     * using this library can access headers and body of Http Request
81
     * dispatcher received and the Http Response Dispatcher is going to send.
82
     *
83
     * @var ServiceHost
84
     */
85
    private $serviceHost;
86
87
    /**
88
     * To hold reference to ServiceConfiguration instance where the
89
     * service specific rules (page limit, resource set access rights
90
     * etc...) are defined.
91
     *
92
     * @var IServiceConfiguration
93
     */
94
    protected $config;
95
96
    /**
97
     * Hold reference to object serialiser - bit wot turns PHP objects
98
     * into message traffic on wire.
99
     *
100
     * @var IObjectSerialiser
101
     */
102
    protected $objectSerialiser;
103
104
    /**
105
     * Get reference to object serialiser - bit wot turns PHP objects
106
     * into message traffic on wire.
107
     *
108
     * @return IObjectSerialiser
109
     */
110
    public function getObjectSerialiser(): IObjectSerialiser
111
    {
112
        assert(null != $this->objectSerialiser);
113
114
        return $this->objectSerialiser;
115
    }
116
117
    /**
118
     * BaseService constructor.
119
     * @param IObjectSerialiser|null $serialiser
120
     */
121
    protected function __construct(IObjectSerialiser $serialiser = null)
122
    {
123
        if (null != $serialiser) {
124
            $serialiser->setService($this);
125
        } else {
126
            $serialiser = new ObjectModelSerializer($this, null);
127
        }
128
        $this->objectSerialiser = $serialiser;
129
    }
130
131
    /**
132
     * Gets reference to ServiceConfiguration instance so that
133
     * service specific rules defined by the developer can be
134
     * accessed.
135
     *
136
     * @return IServiceConfiguration
137
     */
138
    public function getConfiguration(): IServiceConfiguration
139
    {
140
        assert(null != $this->config);
141
142
        return $this->config;
143
    }
144
145
    //TODO: shouldn't we hide this from the interface..if we need it at all.
146
147
    /**
148
     * Get the wrapper over developer's IQueryProvider and IMetadataProvider implementation.
149
     *
150
     * @return ProvidersWrapper
151
     */
152
    public function getProvidersWrapper(): ProvidersWrapper
153
    {
154
        return $this->providersWrapper;
155
    }
156
157
    /**
158
     * Gets reference to wrapper class instance over IDSSP implementation.
159
     *
160
     * @return StreamProviderWrapper
161
     */
162
    public function getStreamProviderWrapper()
163
    {
164
        return $this->streamProvider;
165
    }
166
167
    /**
168
     * Get reference to the data service host instance.
169
     *
170
     * @return ServiceHost
171
     */
172
    public function getHost(): ServiceHost
173
    {
174
        assert(null != $this->serviceHost);
175
176
        return $this->serviceHost;
177
    }
178
179
    /**
180
     * Sets the data service host instance.
181
     *
182
     * @param ServiceHost $serviceHost The data service host instance
183
     */
184
    public function setHost(ServiceHost $serviceHost): void
185
    {
186
        $this->serviceHost = $serviceHost;
187
    }
188
189
    /**
190
     * To get reference to operation context where we have direct access to
191
     * headers and body of Http Request, we have received and the Http Response
192
     * We are going to send.
193
     *
194
     * @return IOperationContext
195
     */
196
    public function getOperationContext(): IOperationContext
197
    {
198
        return $this->getHost()->getOperationContext();
199
    }
200
201
    /**
202
     * Get reference to the wrapper over IStreamProvider or
203
     * IStreamProvider2 implementations.
204
     *
205
     * @return StreamProviderWrapper
206
     */
207
    public function getStreamProvider(): StreamProviderWrapper
208
    {
209
        if (null === $this->streamProvider) {
210
            $this->streamProvider = new StreamProviderWrapper();
211
            $this->streamProvider->setService($this);
212
        }
213
214
        return $this->streamProvider;
215
    }
216
217
    /**
218
     * Top-level handler invoked by Dispatcher against any request to this
219
     * service. This method will hand over request processing task to other
220
     * functions which process the request, set required headers and Response
221
     * stream (if any in Atom/Json format) in
222
     * WebOperationContext::Current()::OutgoingWebResponseContext.
223
     * Once this function returns, dispatcher uses global WebOperationContext
224
     * to write out the request response to client.
225
     * This function will perform the following operations:
226
     * (1) Check whether the top level service class implements
227
     *     IServiceProvider which means the service is a custom service, in
228
     *     this case make sure the top level service class implements
229
     *     IMetaDataProvider and IQueryProvider.
230
     *     These are the minimal interfaces that a custom service to be
231
     *     implemented in order to expose its data as OData. Save reference to
232
     *     These interface implementations.
233
     *     NOTE: Here we will ensure only providers for IDSQP and IDSMP. The
234
     *     IDSSP will be ensured only when there is an GET request on MLE/Named
235
     *     stream.
236
     *
237
     * (2). Invoke 'Initialize' method of top level service for
238
     *      collecting the configuration rules set by the developer for this
239
     *      service.
240
     *
241
     * (3). Invoke the Uri processor to process the request URI. The uri
242
     *      processor will do the following:
243
     *      (a). Validate the request uri syntax using OData uri rules
244
     *      (b). Validate the request using metadata of this service
245
     *      (c). Parse the request uri and using, IQueryProvider
246
     *           implementation, fetches the resources pointed by the uri
247
     *           if required
248
     *      (d). Build a RequestDescription which encapsulate everything
249
     *           related to request uri (e.g. type of resource, result
250
     *           etc...)
251
     * (3). Invoke handleRequest2 for further processing
252
     * @throws ODataException
253
     */
254
    public function handleRequest()
255
    {
256
        try {
257
            $this->createProviders();
258
            $this->getHost()->validateQueryParameters();
259
            $uriProcessor = UriProcessorNew::process($this);
260
            $request      = $uriProcessor->getRequest();
261
            if (TargetKind::BATCH() == $request->getTargetKind()) {
262
                //dd($request);
263
                $this->getProvidersWrapper()->startTransaction(true);
264
                try {
265
                    $this->handleBatchRequest($request);
266
                } catch (\Exception $ex) {
267
                    $this->getProvidersWrapper()->rollBackTransaction();
268
                    throw $ex;
269
                }
270
                $this->getProvidersWrapper()->commitTransaction();
271
            } else {
272
                $this->serializeResult($request, $uriProcessor);
273
            }
274
        } catch (\Exception $exception) {
275
            ErrorHandler::handleException($exception, $this);
276
            // Return to dispatcher for writing serialized exception
277
            return;
278
        }
279
    }
280
281
    /**
282
     * @param $request
283
     * @throws ODataException
284
     */
285
    private function handleBatchRequest($request)
286
    {
287
        $cloneThis      = clone $this;
288
        $batchProcessor = new BatchProcessor($cloneThis, $request);
289
        $batchProcessor->handleBatch();
290
        $response = $batchProcessor->getResponse();
291
        $this->getHost()->setResponseStatusCode(HttpStatus::CODE_ACCEPTED);
292
        $this->getHost()->setResponseContentType('multipart/mixed; boundary=' . $batchProcessor->getBoundary());
293
        // Hack: this needs to be sorted out in the future as we hookup other versions.
294
        $this->getHost()->setResponseVersion('3.0;');
295
        $this->getHost()->setResponseCacheControl(ODataConstants::HTTPRESPONSE_HEADER_CACHECONTROL_NOCACHE);
296
        $this->getHost()->getOperationContext()->outgoingResponse()->setStream($response);
297
    }
298
299
    /**
300
     * @return IQueryProvider|null
301
     */
302
    abstract public function getQueryProvider(): ?IQueryProvider;
303
304
    /**
305
     * @return IMetadataProvider
306
     */
307
    abstract public function getMetadataProvider();
308
309
    /**
310
     *  @return \POData\Providers\Stream\IStreamProvider2
311
     */
312
    abstract public function getStreamProviderX();
313
314
    /** @var ODataWriterRegistry */
315
    protected $writerRegistry;
316
317
    /** @var ODataReaderRegistry */
318
    protected $readerRegistry;
319
320
    /**
321
     * Returns the ODataWriterRegistry to use when writing the response to a service document or resource request.
322
     *
323
     * @return ODataWriterRegistry
324
     */
325
    public function getODataWriterRegistry(): ODataWriterRegistry
326
    {
327
        assert(null != $this->writerRegistry);
328
329
        return $this->writerRegistry;
330
    }
331
332
    /**
333
     * Returns the ODataReaderRegistry to use when writing the response to a service document or resource request.
334
     *
335
     * @return ODataReaderRegistry
336
     */
337
    public function getODataReaderRegistry(): ODataReaderRegistry
338
    {
339
        assert(null != $this->writerRegistry);
340
341
        return $this->readerRegistry;
342
    }
343
    /**
344
     * This method will query and validates for IMetadataProvider and IQueryProvider implementations, invokes
345
     * BaseService::Initialize to initialize service specific policies.
346
     *
347
     * @throws ODataException
348
     * @throws \Exception
349
     */
350
    protected function createProviders()
351
    {
352
        $metadataProvider = $this->getMetadataProvider();
353
        if (null === $metadataProvider) {
354
            throw ODataException::createInternalServerError(Messages::providersWrapperNull());
355
        }
356
357
        if (!$metadataProvider instanceof IMetadataProvider) {
0 ignored issues
show
introduced by
$metadataProvider is always a sub-type of POData\Providers\Metadata\IMetadataProvider.
Loading history...
358
            throw ODataException::createInternalServerError(Messages::invalidMetadataInstance());
359
        }
360
361
        $queryProvider = $this->getQueryProvider();
362
363
        if (null === $queryProvider) {
364
            throw ODataException::createInternalServerError(Messages::providersWrapperNull());
365
        }
366
367
        $this->config           = new ServiceConfiguration($metadataProvider);
368
        $this->providersWrapper = new ProvidersWrapper(
369
            $metadataProvider,
370
            $queryProvider,
371
            $this->config
372
        );
373
374
        $this->initialize($this->config);
375
376
        //TODO: this seems like a bad spot to do this
377
        $this->writerRegistry = new ODataWriterRegistry();
378
        $this->readerRegistry = new ODataReaderRegistry();
379
        $this->registerWriters();
380
        $this->registerReaders();
381
    }
382
383
    //TODO: i don't want this to be public..but it's the only way to test it right now...
384
385
    /**
386
     * @throws \Exception
387
     */
388
    public function registerWriters()
389
    {
390
        $registry       = $this->getODataWriterRegistry();
391
        $serviceVersion = $this->getConfiguration()->getMaxDataServiceVersion();
392
        $serviceURI     = $this->getHost()->getAbsoluteServiceUri()->getUrlAsString();
393
394
        //We always register the v1 stuff
395
        $registry->register(new JsonODataV1Writer());
396
        $registry->register(new AtomODataWriter($serviceURI));
397
398
        if (-1 < $serviceVersion->compare(Version::v2())) {
399
            $registry->register(new JsonODataV2Writer());
400
        }
401
402
        if (-1 < $serviceVersion->compare(Version::v3())) {
403
            $registry->register(new JsonLightODataWriter(JsonLightMetadataLevel::NONE(), $serviceURI));
404
            $registry->register(new JsonLightODataWriter(JsonLightMetadataLevel::MINIMAL(), $serviceURI));
405
            $registry->register(new JsonLightODataWriter(JsonLightMetadataLevel::FULL(), $serviceURI));
406
        }
407
    }
408
409
    public function registerReaders()
410
    {
411
        $registry = $this->getODataReaderRegistry();
412
        //We always register the v1 stuff
413
        $registry->register(new AtomODataReader());
414
    }
415
416
    /**
417
     * Serialize the requested resource.
418
     *
419
     * @param RequestDescription $request      The description of the request  submitted by the client
420
     * @param IUriProcessor      $uriProcessor Reference to the uri processor
421
     *
422
     * @throws Common\HttpHeaderFailure
423
     * @throws Common\UrlFormatException
424
     * @throws InvalidOperationException
425
     * @throws ODataException
426
     * @throws \ReflectionException
427
     * @throws \Exception
428
     */
429
    protected function serializeResult(RequestDescription $request, IUriProcessor $uriProcessor)
430
    {
431
        $isETagHeaderAllowed = $request->isETagHeaderAllowed();
432
433
        if ($this->getConfiguration()->getValidateETagHeader() && !$isETagHeaderAllowed) {
434
            if (null !== $this->getHost()->getRequestIfMatch()
435
                || null !== $this->getHost()->getRequestIfNoneMatch()
436
            ) {
437
                throw ODataException::createBadRequestError(
438
                    Messages::eTagCannotBeSpecified($this->getHost()->getAbsoluteRequestUri()->getUrlAsString())
439
                );
440
            }
441
        }
442
443
        $responseContentType = $this->getResponseContentType($request, $uriProcessor);
444
445
        if (null === $responseContentType && $request->getTargetKind() != TargetKind::MEDIA_RESOURCE()) {
446
            //the responseContentType can ONLY be null if it's a stream (media resource) and
447
            // that stream is storing null as the content type
448
            throw new ODataException(Messages::unsupportedMediaType(), 415);
449
        }
450
451
        $odataModelInstance = null;
452
        $hasResponseBody    = true;
453
        // Execution required at this point if request points to any resource other than
454
455
        // (1) media resource - For Media resource 'getResponseContentType' already performed execution as
456
        // it needs to know the mime type of the stream
457
        // (2) metadata - internal resource
458
        // (3) service directory - internal resource
459
        if ($request->needExecution()) {
460
            $method = $this->getHost()->getOperationContext()->incomingRequest()->getMethod();
461
            $uriProcessor->execute();
462
            if (HTTPRequestMethod::DELETE() == $method) {
463
                $this->getHost()->setResponseStatusCode(HttpStatus::CODE_NOCONTENT);
464
465
                return;
466
            }
467
468
            $objectModelSerializer = $this->getObjectSerialiser();
469
            $objectModelSerializer->setRequest($request);
470
471
            $targetResourceType = $request->getTargetResourceType();
472
            if (null === $targetResourceType) {
473
                throw new InvalidOperationException('Target resource type cannot be null');
474
            }
475
476
            $methodIsNotPost   = (HTTPRequestMethod::POST() != $method);
477
            $methodIsNotDelete = (HTTPRequestMethod::DELETE() != $method);
478
            if (!$request->isSingleResult() && $methodIsNotPost) {
479
                // Code path for collection (feed or links)
480
                $entryObjects = $request->getTargetResult();
481
                if (!$entryObjects instanceof QueryResult) {
482
                    throw new InvalidOperationException('!$entryObjects instanceof QueryResult');
483
                }
484
                if (!is_array($entryObjects->results)) {
485
                    throw new InvalidOperationException('!is_array($entryObjects->results)');
486
                }
487
                // If related resource set is empty for an entry then we should
488
                // not throw error instead response must be empty feed or empty links
489
                if ($request->isLinkUri()) {
490
                    $odataModelInstance = $objectModelSerializer->writeUrlElements($entryObjects);
491
                    if (!$odataModelInstance instanceof ODataURLCollection) {
0 ignored issues
show
introduced by
$odataModelInstance is always a sub-type of POData\ObjectModel\ODataURLCollection.
Loading history...
492
                        throw new InvalidOperationException('!$odataModelInstance instanceof ODataURLCollection');
493
                    }
494
                } else {
495
                    $odataModelInstance = $objectModelSerializer->writeTopLevelElements($entryObjects);
496
                    if (!$odataModelInstance instanceof ODataFeed) {
0 ignored issues
show
introduced by
$odataModelInstance is always a sub-type of POData\ObjectModel\ODataFeed.
Loading history...
497
                        throw new InvalidOperationException('!$odataModelInstance instanceof ODataFeed');
498
                    }
499
                }
500
            } else {
501
                // Code path for entity, complex, bag, resource reference link,
502
                // primitive type or primitive value
503
                $result = $request->getTargetResult();
504
                if (!$result instanceof QueryResult) {
505
                    $result          = new QueryResult();
506
                    $result->results = $request->getTargetResult();
507
                }
508
                $requestTargetKind = $request->getTargetKind();
509
                $requestProperty   = $request->getProjectedProperty();
510
                if ($request->isLinkUri()) {
511
                    // In the query 'Orders(1245)/$links/Customer', the targeted
512
                    // Customer might be null
513
                    if (null === $result->results && $methodIsNotPost && $methodIsNotDelete) {
514
                        throw ODataException::createResourceNotFoundError($request->getIdentifier());
515
                    }
516
                    if ($methodIsNotPost && $methodIsNotDelete) {
517
                        $odataModelInstance = $objectModelSerializer->writeUrlElement($result);
518
                    }
519
                } elseif (TargetKind::RESOURCE() == $requestTargetKind
520
                          || TargetKind::SINGLETON() == $requestTargetKind) {
521
                    if (null !== $this->getHost()->getRequestIfMatch()
522
                        && null !== $this->getHost()->getRequestIfNoneMatch()
523
                    ) {
524
                        throw ODataException::createBadRequestError(
525
                            Messages::bothIfMatchAndIfNoneMatchHeaderSpecified()
526
                        );
527
                    }
528
                    // handle entry resource
529
                    $needToSerializeResponse = true;
530
                    $eTag                    = $this->compareETag(
531
                        $result,
532
                        $targetResourceType,
533
                        $needToSerializeResponse
534
                    );
535
                    if ($needToSerializeResponse) {
536
                        if (null === $result || null === $result->results) {
537
                            // In the query 'Orders(1245)/Customer', the targeted
538
                            // Customer might be null
539
                            // set status code to 204 => 'No Content'
540
                            $this->getHost()->setResponseStatusCode(HttpStatus::CODE_NOCONTENT);
541
                            $hasResponseBody = false;
542
                        } else {
543
                            $odataModelInstance = $objectModelSerializer->writeTopLevelElement($result);
544
                        }
545
                    } else {
546
                        // Resource is not modified so set status code
547
                        // to 304 => 'Not Modified'
548
                        $this->getHost()->setResponseStatusCode(HttpStatus::CODE_NOT_MODIFIED);
549
                        $hasResponseBody = false;
550
                    }
551
552
                    // if resource has eTagProperty then eTag header needs to written
553
                    if (null !== $eTag) {
554
                        $this->getHost()->setResponseETag($eTag);
555
                    }
556
                } elseif (TargetKind::COMPLEX_OBJECT() == $requestTargetKind) {
557
                    if (null === $requestProperty) {
558
                        throw new InvalidOperationException('Projected request property cannot be null');
559
                    }
560
                    $odataModelInstance = $objectModelSerializer->writeTopLevelComplexObject(
561
                        $result,
562
                        $requestProperty->getName(),
563
                        $targetResourceType
564
                    );
565
                } elseif (TargetKind::BAG() == $requestTargetKind) {
566
                    if (null === $requestProperty) {
567
                        throw new InvalidOperationException('Projected request property cannot be null');
568
                    }
569
                    $odataModelInstance = $objectModelSerializer->writeTopLevelBagObject(
570
                        $result,
571
                        $requestProperty->getName(),
572
                        $targetResourceType
573
                    );
574
                } elseif (TargetKind::PRIMITIVE() == $requestTargetKind) {
575
                    $odataModelInstance = $objectModelSerializer->writeTopLevelPrimitive(
576
                        $result,
577
                        $requestProperty
578
                    );
579
                } elseif (TargetKind::PRIMITIVE_VALUE() == $requestTargetKind) {
580
                    // Code path for primitive value (Since its primitive no need for
581
                    // object model serialization)
582
                    // Customers('ANU')/CompanyName/$value => string
583
                    // Employees(1)/Photo/$value => binary stream
584
                    // Customers/$count => string
585
                } else {
586
                    throw new InvalidOperationException('Unexpected resource target kind');
587
                }
588
            }
589
        }
590
591
        //Note: Response content type can be null for named stream
592
        if ($hasResponseBody && null !== $responseContentType) {
593
            if (TargetKind::MEDIA_RESOURCE() != $request->getTargetKind()
594
                && MimeTypes::MIME_APPLICATION_OCTETSTREAM != $responseContentType) {
595
                //append charset for everything except:
596
                //stream resources as they have their own content type
597
                //binary properties (they content type will be App Octet for those...is this a good way?
598
                //we could also decide based upon the projected property
599
600
                $responseContentType .= ';charset=utf-8';
601
            }
602
        }
603
604
        if ($hasResponseBody) {
605
            ResponseWriter::write($this, $request, $odataModelInstance, $responseContentType);
606
        }
607
    }
608
609
    /**
610
     * Gets the response format for the requested resource.
611
     *
612
     * @param RequestDescription $request      The request submitted by client and it's execution result
613
     * @param IUriProcessor      $uriProcessor The reference to the IUriProcessor
614
     *
615
     * @throws Common\HttpHeaderFailure
616
     * @throws InvalidOperationException
617
     * @throws ODataException            , HttpHeaderFailure
618
     * @throws \ReflectionException
619
     * @throws Common\UrlFormatException
620
     * @return string|null               the response content-type, a null value means the requested resource
621
     *                                   is named stream and IDSSP2::getStreamContentType returned null
622
     */
623
    public function getResponseContentType(
624
        RequestDescription $request,
625
        IUriProcessor $uriProcessor
626
    ): ?string {
627
        $baseMimeTypes = [
628
            MimeTypes::MIME_APPLICATION_JSON,
629
            MimeTypes::MIME_APPLICATION_JSON_FULL_META,
630
            MimeTypes::MIME_APPLICATION_JSON_NO_META,
631
            MimeTypes::MIME_APPLICATION_JSON_MINIMAL_META,
632
            MimeTypes::MIME_APPLICATION_JSON_VERBOSE, ];
633
634
        // The Accept request-header field specifies media types which are acceptable for the response
635
636
        $host              = $this->getHost();
637
        $requestAcceptText = $host->getRequestAccept();
638
        $requestVersion    = $request->getResponseVersion();
639
640
        //if the $format header is present it overrides the accepts header
641
        $format = $host->getQueryStringItem(ODataConstants::HTTPQUERY_STRING_FORMAT);
642
        if (null !== $format) {
643
            //There's a strange edge case..if application/json is supplied and it's V3
644
            if (MimeTypes::MIME_APPLICATION_JSON == $format && Version::v3() == $requestVersion) {
645
                //then it's actual minimalmetadata
646
                //TODO: should this be done with the header text too?
647
                $format = MimeTypes::MIME_APPLICATION_JSON_MINIMAL_META;
648
            }
649
650
            $requestAcceptText = ServiceHost::translateFormatToMime($requestVersion, $format);
651
        }
652
653
        //The response format can be dictated by the target resource kind. IE a $value will be different then expected
654
        //getTargetKind doesn't deal with link resources directly and this can change things
655
        $targetKind         = $request->isLinkUri() ? TargetKind::LINK() : $request->getTargetKind();
656
657
        switch ($targetKind) {
658
            case TargetKind::METADATA():
659
                return HttpProcessUtility::selectMimeType(
660
                    $requestAcceptText,
661
                    [MimeTypes::MIME_APPLICATION_XML]
662
                );
663
664
            case TargetKind::SERVICE_DIRECTORY():
665
                return HttpProcessUtility::selectMimeType(
666
                    $requestAcceptText,
667
                    array_merge(
668
                        [MimeTypes::MIME_APPLICATION_ATOMSERVICE],
669
                        $baseMimeTypes
670
                    )
671
                );
672
673
            case TargetKind::PRIMITIVE_VALUE():
674
                $supportedResponseMimeTypes = [MimeTypes::MIME_TEXTPLAIN];
675
676
                if ('$count' != $request->getIdentifier()) {
677
                    $projectedProperty = $request->getProjectedProperty();
678
                    if (null === $projectedProperty) {
679
                        throw new InvalidOperationException('is_null($projectedProperty)');
680
                    }
681
                    $type = $projectedProperty->getInstanceType();
682
                    if (!$type instanceof IType) {
683
                        throw new InvalidOperationException('!$type instanceof IType');
684
                    }
685
                    if ($type instanceof Binary) {
686
                        $supportedResponseMimeTypes = [MimeTypes::MIME_APPLICATION_OCTETSTREAM];
687
                    }
688
                }
689
690
                return HttpProcessUtility::selectMimeType(
691
                    $requestAcceptText,
692
                    $supportedResponseMimeTypes
693
                );
694
695
            case TargetKind::PRIMITIVE():
696
            case TargetKind::COMPLEX_OBJECT():
697
            case TargetKind::BAG():
698
            case TargetKind::LINK():
699
                return HttpProcessUtility::selectMimeType(
700
                    $requestAcceptText,
701
                    array_merge(
702
                        [MimeTypes::MIME_APPLICATION_XML,
703
                            MimeTypes::MIME_TEXTXML, ],
704
                        $baseMimeTypes
705
                    )
706
                );
707
708
            case TargetKind::SINGLETON():
709
            case TargetKind::RESOURCE():
710
                return HttpProcessUtility::selectMimeType(
711
                    $requestAcceptText,
712
                    array_merge(
713
                        [MimeTypes::MIME_APPLICATION_ATOM],
714
                        $baseMimeTypes
715
                    )
716
                );
717
718
            case TargetKind::MEDIA_RESOURCE():
719
                if (!$request->isNamedStream() && !$request->getTargetResourceType()->isMediaLinkEntry()) {
720
                    throw ODataException::createBadRequestError(
721
                        Messages::badRequestInvalidUriForMediaResource(
722
                            $host->getAbsoluteRequestUri()->getUrlAsString()
723
                        )
724
                    );
725
                }
726
727
                $uriProcessor->execute();
728
                $request->setExecuted();
729
                // DSSW::getStreamContentType can throw error in 2 cases
730
                // 1. If the required stream implementation not found
731
                // 2. If IDSSP::getStreamContentType returns NULL for MLE
732
                $responseContentType = $this->getStreamProviderWrapper()
733
                    ->getStreamContentType(
734
                        $request->getTargetResult(),
735
                        $request->getResourceStreamInfo()
736
                    );
737
738
                // Note StreamWrapper::getStreamContentType can return NULL if the requested named stream has not
739
                // yet been uploaded. But for an MLE if IDSSP::getStreamContentType returns NULL
740
                // then StreamWrapper will throw error
741
                if (null !== $responseContentType) {
0 ignored issues
show
introduced by
The condition null !== $responseContentType is always true.
Loading history...
742
                    $responseContentType = HttpProcessUtility::selectMimeType(
743
                        $requestAcceptText,
744
                        [$responseContentType]
745
                    );
746
                }
747
748
                return $responseContentType;
749
        }
750
751
        //If we got here, we just don't know what it is...
752
        throw new ODataException(Messages::unsupportedMediaType(), 415);
753
    }
754
755
    /**
756
     * For the given entry object compare its eTag (if it has eTag properties)
757
     * with current eTag request headers (if present).
758
     *
759
     * @param mixed        &$entryObject             entity resource for which etag
760
     *                                               needs to be checked
761
     * @param ResourceType &$resourceType            Resource type of the entry
762
     *                                               object
763
     * @param bool         &$needToSerializeResponse On return, this will contain
764
     *                                               True if response needs to be
765
     *                                               serialized, False otherwise
766
     *
767
     * @throws ODataException
768
     * @throws InvalidOperationException
769
     * @throws \ReflectionException
770
     * @return string|null               The ETag for the entry object if it has eTag properties
771
     *                                   NULL otherwise
772
     */
773
    protected function compareETag(
774
        &$entryObject,
775
        ResourceType &$resourceType,
776
        &$needToSerializeResponse
777
    ): ?string {
778
        $needToSerializeResponse = true;
779
        $eTag                    = null;
780
        $ifMatch                 = $this->getHost()->getRequestIfMatch();
781
        $ifNoneMatch             = $this->getHost()->getRequestIfNoneMatch();
782
        if (null === $entryObject) {
783
            if (null !== $ifMatch) {
784
                throw ODataException::createPreConditionFailedError(
785
                    Messages::eTagNotAllowedForNonExistingResource()
786
                );
787
            }
788
789
            return null;
790
        }
791
792
        if ($this->getConfiguration()->getValidateETagHeader() && !$resourceType->hasETagProperties()) {
793
            if (null !== $ifMatch || null !== $ifNoneMatch) {
794
                // No eTag properties but request has eTag headers, bad request
795
                throw ODataException::createBadRequestError(
796
                    Messages::noETagPropertiesForType()
797
                );
798
            }
799
800
            // We need write the response but no eTag header
801
            return null;
802
        }
803
804
        if (!$this->getConfiguration()->getValidateETagHeader()) {
805
            // Configuration says do not validate ETag, so we will not write ETag header in the
806
            // response even though the requested resource support it
807
            return null;
808
        }
809
810
        if (null === $ifMatch && null === $ifNoneMatch) {
811
            // No request eTag header, we need to write the response
812
            // and eTag header
813
        } elseif (0 === strcmp(strval($ifMatch), '*')) {
814
            // If-Match:* => we need to write the response and eTag header
815
        } elseif (0 === strcmp(strval($ifNoneMatch), '*')) {
816
            // if-None-Match:* => Do not write the response (304 not modified),
817
            // but write eTag header
818
            $needToSerializeResponse = false;
819
        } else {
820
            $eTag = $this->getETagForEntry($entryObject, $resourceType);
821
            // Note: The following code for attaching the prefix W\"
822
            // and the suffix " can be done in getETagForEntry function
823
            // but that is causing an issue in Linux env where the
824
            // firefox browser is unable to parse the ETag in this case.
825
            // Need to follow up PHP core devs for this.
826
            $eTag = ODataConstants::HTTP_WEAK_ETAG_PREFIX . $eTag . '"';
827
            if (null !== $ifMatch) {
828
                if (0 != strcmp($eTag, $ifMatch)) {
829
                    // Requested If-Match value does not match with current
830
                    // eTag Value then pre-condition error
831
                    // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
832
                    throw ODataException::createPreConditionFailedError(
833
                        Messages::eTagValueDoesNotMatch()
834
                    );
835
                }
836
            } elseif (0 === strcmp($eTag, $ifNoneMatch)) {
837
                //304 not modified, but in write eTag header
838
                $needToSerializeResponse = false;
839
            }
840
        }
841
842
        if (null === $eTag) {
843
            $eTag = $this->getETagForEntry($entryObject, $resourceType);
844
            // Note: The following code for attaching the prefix W\"
845
            // and the suffix " can be done in getETagForEntry function
846
            // but that is causing an issue in Linux env where the
847
            // firefox browser is unable to parse the ETag in this case.
848
            // Need to follow up PHP core devs for this.
849
            $eTag = ODataConstants::HTTP_WEAK_ETAG_PREFIX . $eTag . '"';
850
        }
851
852
        return $eTag;
853
    }
854
855
    /**
856
     * Returns the etag for the given resource.
857
     * Note: This function will not add W\" prefix and " suffix, that is caller's
858
     * responsibility.
859
     *
860
     * @param mixed        &$entryObject  Resource for which etag value needs to
861
     *                                    be returned
862
     * @param ResourceType &$resourceType Resource type of the $entryObject
863
     *
864
     * @throws ODataException
865
     * @throws InvalidOperationException
866
     * @throws \ReflectionException
867
     * @return string|null               ETag value for the given resource (with values encoded
868
     *                                   for use in a URI) there are etag properties, NULL if
869
     *                                   there is no etag property
870
     */
871
    protected function getETagForEntry(&$entryObject, ResourceType &$resourceType): ?string
872
    {
873
        $eTag  = null;
874
        $comma = null;
875
        foreach ($resourceType->getETagProperties() as $eTagProperty) {
876
            $type = $eTagProperty->getInstanceType();
877
            if (!$type instanceof IType) {
878
                throw new InvalidOperationException('!$type instanceof IType');
879
            }
880
881
            $value    = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $value is dead and can be removed.
Loading history...
882
            $property = $eTagProperty->getName();
883
            try {
884
                //TODO #88...also this seems like dupe work
885
                $value = ReflectionHandler::getProperty($entryObject, $property);
886
            } catch (\ReflectionException $reflectionException) {
887
                throw ODataException::createInternalServerError(
888
                    Messages::failedToAccessProperty($property, $resourceType->getName())
889
                );
890
            }
891
892
            $eTagBase = $eTag . $comma;
893
            $eTag     = $eTagBase . ((null == $value) ? 'null' : $type->convertToOData($value));
894
895
            $comma = ',';
896
        }
897
898
        if (null !== $eTag) {
899
            // If eTag is made up of datetime or string properties then the above
900
            // IType::convertToOData will perform utf8 and url encode. But we don't
901
            // want this for eTag value.
902
            $eTag = urldecode(utf8_decode($eTag));
903
904
            return rtrim($eTag, ',');
905
        }
906
        return null;
907
    }
908
}
909