BaseService::serializeResult()   F
last analyzed

Complexity

Conditions 43
Paths 359

Size

Total Lines 177
Code Lines 99

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 43
eloc 99
c 1
b 0
f 0
nc 359
nop 2
dl 0
loc 177
rs 1.3458

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

875
            } elseif (0 === strcmp($eTag, /** @scrutinizer ignore-type */ $ifNoneMatch)) {
Loading history...
876
                //304 not modified, but in write eTag header
877
                $needToSerializeResponse = false;
878
            }
879
        }
880
881
        if (null === $eTag) {
882
            $eTag = $this->getETagForEntry($entryObject, $resourceType);
883
            // Note: The following code for attaching the prefix W\"
884
            // and the suffix " can be done in getETagForEntry function
885
            // but that is causing an issue in Linux env where the
886
            // firefox browser is unable to parse the ETag in this case.
887
            // Need to follow up PHP core devs for this.
888
            $eTag = ODataConstants::HTTP_WEAK_ETAG_PREFIX . $eTag . '"';
889
        }
890
891
        return $eTag;
892
    }
893
894
    /**
895
     * Returns the etag for the given resource.
896
     * Note: This function will not add W\" prefix and " suffix, that is caller's
897
     * responsibility.
898
     *
899
     * @param mixed        &$entryObject  Resource for which etag value needs to
900
     *                                    be returned
901
     * @param ResourceType &$resourceType Resource type of the $entryObject
902
     *
903
     * @throws InvalidOperationException
904
     * @throws ReflectionException
905
     * @throws ODataException
906
     * @return string|null               ETag value for the given resource (with values encoded
907
     *                                   for use in a URI) there are etag properties, NULL if
908
     *                                   there is no etag property
909
     */
910
    protected function getETagForEntry(&$entryObject, ResourceType &$resourceType): ?string
911
    {
912
        $eTag  = null;
913
        $comma = null;
914
        foreach ($resourceType->getETagProperties() as $eTagProperty) {
915
            $type = $eTagProperty->getInstanceType();
916
            if (!$type instanceof IType) {
917
                throw new InvalidOperationException('!$type instanceof IType');
918
            }
919
920
            $value    = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $value is dead and can be removed.
Loading history...
921
            $property = $eTagProperty->getName();
922
            try {
923
                //TODO #88...also this seems like dupe work
924
                $value = ReflectionHandler::getProperty($entryObject, $property);
925
            } catch (ReflectionException $reflectionException) {
926
                throw ODataException::createInternalServerError(
927
                    Messages::failedToAccessProperty($property, $resourceType->getName())
928
                );
929
            }
930
931
            $eTagBase = $eTag . $comma;
932
            $eTag     = $eTagBase . ((null == $value) ? 'null' : $type->convertToOData($value));
933
934
            $comma = ',';
935
        }
936
937
        if (null !== $eTag) {
938
            // If eTag is made up of datetime or string properties then the above
939
            // IType::convertToOData will perform utf8 and url encode. But we don't
940
            // want this for eTag value.
941
            $eTag = urldecode(utf8_decode($eTag));
942
943
            return rtrim($eTag, ',');
944
        }
945
        return null;
946
    }
947
948
    /**
949
     * @return IStreamProvider2
950
     */
951
    abstract public function getStreamProviderX();
952
}
953