Completed
Push — master ( 2bf901...7d3f30 )
by Christopher
24s queued 15s
created

BaseService::initializeDefaultConfig()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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