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