Test Failed
Push — master ( 91c2b4...b6afb1 )
by Bálint
10:56 queued 12s
created

BaseService::assert()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 5
c 0
b 0
f 0
rs 10
cc 2
nc 2
nop 2
1
<?php
2
3
namespace POData;
4
5
use Exception;
6
use POData\Common\MimeTypes;
7
use POData\Common\Version;
8
use POData\OperationContext\HTTPRequestMethod;
9
use POData\Common\ErrorHandler;
10
use POData\Common\Messages;
11
use POData\Common\ODataException;
12
use POData\Common\ODataConstants;
13
use POData\Common\NotImplementedException;
14
use POData\Common\InvalidOperationException;
15
use POData\Common\HttpStatus;
16
use POData\Providers\Metadata\IMetadataProvider;
17
use POData\Providers\Metadata\Type\IType;
18
use POData\Providers\ProvidersWrapper;
19
use POData\Providers\Stream\StreamProviderWrapper;
20
use POData\Providers\Query\IQueryProvider;
21
use POData\Configuration\ServiceConfiguration;
22
use POData\UriProcessor\UriProcessor;
23
use POData\UriProcessor\RequestDescription;
24
use POData\UriProcessor\ResourcePathProcessor\SegmentParser\TargetKind;
25
use POData\OperationContext\ServiceHost;
26
use POData\Providers\Metadata\ResourceType;
27
use POData\Providers\Metadata\Type\Binary;
28
use POData\ObjectModel\ObjectModelSerializer;
29
use POData\Writers\Atom\AtomODataWriter;
30
use POData\Writers\Json\JsonLightMetadataLevel;
31
use POData\Writers\Json\JsonLightODataWriter;
32
use POData\Writers\Json\JsonODataV1Writer;
33
use POData\Writers\Json\JsonODataV2Writer;
34
use POData\Writers\ODataWriterRegistry;
35
use POData\Writers\ResponseWriter;
36
use POData\OperationContext\IOperationContext;
37
use POData\OperationContext\Web\Illuminate\IlluminateOperationContext;
38
use POData\Common\Url;
39
40
41
/**
42
 * Class BaseService
43
 *
44
 * The base class for all BaseService specific classes. This class implements
45
 * the following interfaces:
46
 *  (1) IRequestHandler
47
 *      Implementing this interface requires defining the function
48
 *      'handleRequest' that will be invoked by dispatcher
49
 *  (2) IService
50
 *      Force BaseService class to implement functions for custom
51
 *      data service providers
52
 *
53
 * @package POData
54
 */
55
abstract class BaseService implements IRequestHandler, IService
56
{
57
    /**
58
     * The wrapper over IQueryProvider and IMetadataProvider implementations.
59
     *
60
     * @var ProvidersWrapper
61
     */
62
    private $providersWrapper;
63
64
    /**
65
     * The wrapper over IStreamProvider implementation
66
     *
67
     * @var StreamProviderWrapper
68
     */
69
    private $_streamProvider;
70
71
    /**
72
     * Hold reference to the ServiceHost instance created by dispatcher,
73
     * using this library can access headers and body of Http Request
74
     * dispatcher received and the Http Response Dispatcher is going to send.
75
     *
76
     * @var ServiceHost
77
     */
78
    private $_serviceHost;
79
80
81
    /**
82
     * To hold reference to ServiceConfiguration instance where the
83
     * service specific rules (page limit, resource set access rights
84
     * etc...) are defined.
85
     *
86
     * @var ServiceConfiguration
87
     */
88
    private $config;
89
90
    /**
91
     * Gets reference to ServiceConfiguration instance so that
92
     * service specific rules defined by the developer can be
93
     * accessed.
94
     *
95
     * @return ServiceConfiguration
96
     */
97
    public function getConfiguration()
98
    {
99
        return $this->config;
100
    }
101
102
103
    //TODO: shouldn't we hide this from the interface..if we need it at all.
104
    /**
105
     * Get the wrapper over developer's IQueryProvider and IMetadataProvider implementation.
106
     *
107
     * @return ProvidersWrapper
108
     */
109
    public function getProvidersWrapper()
110
    {
111
            return $this->providersWrapper;
112
    }
113
114
    /**
115
     * Gets reference to wrapper class instance over IDSSP implementation
116
     *
117
     * @return StreamProviderWrapper
118
     */
119
    public function getStreamProviderWrapper()
120
    {
121
        return $this->_streamProvider;
122
    }
123
124
    /**
125
     * Get reference to the data service host instance.
126
     *
127
     * @return ServiceHost
128
     */
129
    public function getHost()
130
    {
131
        return $this->_serviceHost;
132
    }
133
134
    /**
135
     * Sets the data service host instance.
136
     *
137
     * @param ServiceHost $serviceHost The data service host instance.
138
     *
139
     * @return void
140
     */
141
    public function setHost(ServiceHost $serviceHost)
142
    {
143
        $this->_serviceHost = $serviceHost;
144
    }
145
146
    /**
147
     * To get reference to operation context where we have direct access to
148
     * headers and body of Http Request we have received and the Http Response
149
     * We are going to send.
150
     *
151
     * @return IOperationContext
152
     */
153
    public function getOperationContext()
154
    {
155
        return $this->_serviceHost->getOperationContext();
156
    }
157
158
    /**
159
     * Get reference to the wrapper over IStreamProvider or
160
     * IStreamProvider2 implementations.
161
     *
162
     * @return StreamProviderWrapper
163
     */
164
    public function getStreamProvider()
165
    {
166
        if (is_null($this->_streamProvider)) {
167
            $this->_streamProvider = new StreamProviderWrapper();
168
            $this->_streamProvider->setService($this);
169
        }
170
171
        return $this->_streamProvider;
172
    }
173
174
    /**
175
     * Top-level handler invoked by Dispatcher against any request to this
176
     * service. This method will hand over request processing task to other
177
     * functions which process the request, set required headers and Response
178
     * stream (if any in Atom/Json format) in
179
     * WebOperationContext::Current()::OutgoingWebResponseContext.
180
     * Once this function returns, dispatcher uses global WebOperationContext
181
     * to write out the request response to client.
182
     * This function will perform the following operations:
183
     * (1) Check whether the top level service class implements
184
     *     IServiceProvider which means the service is a custom service, in
185
     *     this case make sure the top level service class implements
186
     *     IMetaDataProvider and IQueryProvider.
187
     *     These are the minimal interfaces that a custom service to be
188
     *     implemented in order to expose its data as OData. Save reference to
189
     *     These interface implementations.
190
     *     NOTE: Here we will ensure only providers for IDSQP and IDSMP. The
191
     *     IDSSP will be ensured only when there is an GET request on MLE/Named
192
     *     stream.
193
     *
194
     * (2). Invoke 'Initialize' method of top level service for
195
     *      collecting the configuration rules set by the developer for this
196
     *      service.
197
     *
198
     * (3). Invoke the Uri processor to process the request URI. The uri
199
     *      processor will do the following:
200
     *      (a). Validate the request uri syntax using OData uri rules
201
     *      (b). Validate the request using metadata of this service
202
     *      (c). Parse the request uri and using, IQueryProvider
203
     *           implementation, fetches the resources pointed by the uri
204
     *           if required
205
     *      (d). Build a RequestDescription which encapsulate everything
206
     *           related to request uri (e.g. type of resource, result
207
     *           etc...)
208
     * (3). Invoke handleRequest2 for further processing
209
     *
210
     * @return void
211
     */
212
    public function handleRequest()
213
    {
214
        try {
215
            $this->createProviders();
216
            $this->_serviceHost->getFullAbsoluteRequestUri()->validateQueryParameters();
217
            //$requestMethod = $this->getOperationContext()->incomingRequest()->getMethod();
218
            //if ($requestMethod != HTTPRequestMethod::GET) {
219
                # Now supporting GET and trying to support PUT
220
                //throw ODataException::createNotImplementedError(Messages::onlyReadSupport($requestMethod));
221
            //}
222
223
            $uriProcessor = UriProcessor::process($this);
224
            $request = $uriProcessor->getRequest();
225
            ob_clean();
226
            $this->serializeResult($request, $uriProcessor);
227
        } catch (Exception $exception) {
228
            ErrorHandler::handleException($exception, $this);
229
            // Return to dispatcher for writing serialized exception
230
            return;
231
        }
232
    }
233
234
    /**
235
     * @return IQueryProvider
236
     */
237
    public abstract function getQueryProvider();
238
239
    /**
240
     * @return IMetadataProvider
241
     */
242
    public abstract function getMetadataProvider();
243
244
    /**
245
     * @return \POData\Providers\Stream\IStreamProvider
246
     */
247
    public abstract function getStreamProviderX();
248
249
250
    /** @var  ODataWriterRegistry */
251
    private $writerRegistry;
252
253
    /**
254
     * Returns the ODataWriterRegistry to use when writing the response to a service document or resource request
255
     * @return ODataWriterRegistry
256
     */
257
    public function getODataWriterRegistry()
258
    {
259
        return $this->writerRegistry;
260
    }
261
262
263
    /**
264
     * This method will query and validates for IMetadataProvider and IQueryProvider implementations, invokes
265
     * BaseService::Initialize to initialize service specific policies.
266
     *
267
     *
268
     * @throws ODataException
269
     */
270
    protected function createProviders()
271
    {
272
273
        $metadataProvider = $this->getMetadataProvider();
274
        if (is_null($metadataProvider)) {
275
            throw ODataException::createInternalServerError(Messages::providersWrapperNull());
276
        }
277
278
        if (!is_object($metadataProvider) || !$metadataProvider instanceof IMetadataProvider) {
279
            throw ODataException::createInternalServerError(Messages::invalidMetadataInstance());
280
        }
281
282
        $queryProvider = $this->getQueryProvider();
283
284
        if (is_null($queryProvider)) {
285
            throw ODataException::createInternalServerError(Messages::providersWrapperNull());
286
        }
287
288
        if (!is_object($queryProvider)) {
289
            throw ODataException::createInternalServerError(Messages::invalidQueryInstance());
290
        }
291
292
        if (!$queryProvider instanceof IQueryProvider) {
0 ignored issues
show
introduced by
$queryProvider is always a sub-type of POData\Providers\Query\IQueryProvider.
Loading history...
293
            throw ODataException::createInternalServerError(Messages::invalidQueryInstance());
294
        }
295
296
        $this->config = new ServiceConfiguration($metadataProvider);
297
        $this->providersWrapper = new ProvidersWrapper(
298
            $metadataProvider,
299
            $queryProvider,
300
            $this->config
301
        );
302
303
304
        $this->initialize($this->config);
305
306
        //TODO: this seems like a bad spot to do this
307
        $this->writerRegistry = new ODataWriterRegistry();
308
        $this->registerWriters();
309
    }
310
311
    //TODO: i don't want this to be public..but it's the only way to test it right now...
312
    public function registerWriters()
313
    {
314
        $registry = $this->getODataWriterRegistry();
315
        $serviceVersion = $this->getConfiguration()->getMaxDataServiceVersion();
316
        $serviceURI = $this->getHost()->getAbsoluteServiceUri()->getUrlAsString();
317
318
        //We always register the v1 stuff
319
        $registry->register(new JsonODataV1Writer());
320
        $registry->register(new AtomODataWriter($serviceURI));
321
322
        if ($serviceVersion->compare(Version::v2()) > -1) {
323
            $registry->register(new JsonODataV2Writer());
324
        }
325
326
        if ($serviceVersion->compare(Version::v3()) > -1) {
327
            $registry->register(new JsonLightODataWriter(JsonLightMetadataLevel::NONE, $serviceURI));
328
            $registry->register(new JsonLightODataWriter(JsonLightMetadataLevel::MINIMAL, $serviceURI));
329
            $registry->register(new JsonLightODataWriter(JsonLightMetadataLevel::FULL, $serviceURI));
330
        }
331
    }
332
333
    /**
334
     * Serialize the requested resource.
335
     *
336
     * @param RequestDescription $request The description of the request  submitted by the client.
337
     * @param UriProcessor $uriProcessor Reference to the uri processor.
338
     *
339
     * @return void
340
     */
341
    protected function serializeResult(RequestDescription $request, UriProcessor $uriProcessor) {
342
        $isETagHeaderAllowed = $request->isETagHeaderAllowed();
343
        if ($this->config->getValidateETagHeader() && !$isETagHeaderAllowed) {
344
            if (!is_null($this->_serviceHost->getRequestIfMatch())
345
                ||!is_null($this->_serviceHost->getRequestIfNoneMatch())
346
            ) {
347
                throw ODataException::createBadRequestError(
348
                    Messages::eTagCannotBeSpecified($this->getHost()->getAbsoluteRequestUri()->getUrlAsString())
349
                );
350
            }
351
        }
352
353
        $responseContentType = self::getResponseContentType($request, $uriProcessor, $this);
354
355
        if (is_null($responseContentType) && $request->getTargetKind() != TargetKind::MEDIA_RESOURCE) {
0 ignored issues
show
introduced by
The condition is_null($responseContentType) is always false.
Loading history...
356
            //the responseContentType can ONLY be null if it's a stream (media resource) and that stream is storing null as the content type
357
            throw new ODataException(Messages::unsupportedMediaType(), 415);
358
        }
359
360
        $odataModelInstance = null;
361
        $hasResponseBody = true;
362
363
        $write = function($service, $request, $odataModelInstance) use ($hasResponseBody, $responseContentType) {
364
            //Note: Response content type can be null for named stream
365
            if ($hasResponseBody && !is_null($responseContentType)) {
0 ignored issues
show
introduced by
The condition is_null($responseContentType) is always false.
Loading history...
366
                if ($request->getTargetKind() != TargetKind::MEDIA_RESOURCE && $responseContentType != MimeTypes::MIME_APPLICATION_OCTETSTREAM) {
367
                    //append charset for everything except:
368
                    //stream resources as they have their own content type
369
                    //binary properties (they content type will be App Octet for those...is this a good way? we could also decide based upon the projected property
370
                    //
371
                    $responseContentType .= ';charset=utf-8';
372
                }
373
            }
374
375
            if ($hasResponseBody) {
0 ignored issues
show
introduced by
The condition $hasResponseBody is always true.
Loading history...
376
                ResponseWriter::write(
377
                    $service,
378
                    $request,
379
                    $odataModelInstance,
380
                    $responseContentType
381
                );
382
            }
383
        };
384
385
        // Execution required at this point if request target to any resource other than
386
        //
387
        // (1) media resource - For Media resource 'getResponseContentType' already performed execution as it needs to know the mime type of the stream
388
        // (2) metadata - internal resource
389
        // (3) service directory - internal resource
390
        if (!in_array($request->getTargetKind(), [TargetKind::MEDIA_RESOURCE, TargetKind::METADATA, TargetKind::SERVICE_DIRECTORY])) {
391
392
            if ($request->needExecution()) {
393
                $uriProcessor->execute();
394
            }
395
396
            $objectModelSerializer = new ObjectModelSerializer($this, $request);
397
            if (!$request->isSingleResult()) {
398
                // Code path for collection (feed or links)
399
                $entryObjects = $request->getTargetResult();
400
                self::assert(
401
                    !is_null($entryObjects) && is_iterable($entryObjects),
402
                    '!is_null($entryObjects) && is_iterable($entryObjects)'
403
                );
404
                // If related resource set is empty for an entry then we should
405
                // not throw error instead response must be empty feed or empty links
406
                if ($request->isLinkUri()) {
407
                    $odataModelInstance = $objectModelSerializer->writeUrlElements($entryObjects);
408
                    self::assert(
409
                        $odataModelInstance instanceof \POData\ObjectModel\ODataURLCollection,
410
                        '$odataModelInstance instanceof ODataURLCollection'
411
                    );
412
                } else {
413
                    if (!empty($request->getParts())) {
414
                        $context = $this->_serviceHost->getOperationContext();
415
                        $response = $context->outgoingResponse();
416
                        $boundary = uniqid('batch_');
417
                        $streams = [];
418
                        foreach ($request->getParts() as $key => $part) {
419
                            $_objectModelSerializer = new ObjectModelSerializer($this, $part);
0 ignored issues
show
Unused Code introduced by
The assignment to $_objectModelSerializer is dead and can be removed.
Loading history...
420
                            $status = "200 Ok";
421
                            try {
422
                                $this->serializeResult($part, $uriProcessor);
423
                            } catch (Exception $exception) {
424
                                ErrorHandler::handleException($exception, $this);
425
                                $status = "500 Internal Server Error";
426
                            }
427
                            $body = $response->getStream();
428
                            $stream = [
429
                                'body' => $body,
430
                                'length' => strlen($body),
431
                                'content_id' => $part->getContentID(),
432
                                'status' => $status,
433
                                'Content-Type' => $response->getHeaders()['Content-Type']
434
                            ];
435
                            $streams[] = $stream;
436
                        }
437
438
                        $output_stream = '';
439
                        foreach ($streams as $stream) {
440
                            $output_stream .= "--{$boundary}\r\n";
441
                            if (!is_null($stream['content_id'])) {
442
                                $output_stream .= "Content-ID: {$stream['content_id']}\r\n";
443
                            }
444
                            $output_stream .= "Content-Type: application/http\r\n";
445
                            $output_stream .= "Content-Transfer-Encoding: binary\r\n";
446
                            $output_stream .= "\r\n";
447
                            $output_stream .= "{$_SERVER['SERVER_PROTOCOL']} {$stream['status']}\r\n";
448
                            $output_stream .= "Content-Type: {$stream['Content-Type']}\r\n";
449
                            $output_stream .= "Content-Length: {$stream['length']}\r\n";
450
                            $output_stream .= "\r\n";
451
                            $output_stream .= $stream['body'] . "\r\n";
452
                            $output_stream .= "\r\n";
453
                        }
454
                        $output_stream .= "--{$boundary}--\r\n";
455
                        $response->setStream($output_stream);
456
                        $this->_serviceHost->setResponseStatusCode(HttpStatus::CODE_ACCEPTED);
457
                        $this->_serviceHost->setResponseContentType("multipart/mixed; boundary={$boundary}");
458
                        return;
459
                    } else {
460
                        $odataModelInstance = $objectModelSerializer->writeTopLevelElements($entryObjects);
461
                        self::assert(
462
                            $odataModelInstance instanceof \POData\ObjectModel\ODataFeed,
463
                            '$odataModelInstance instanceof ODataFeed'
464
                        );
465
                    }
466
                }
467
            } else {
468
                // Code path for entry, complex, bag, resource reference link,
469
                // primitive type or primitive value
470
                $result = $request->getTargetResult();
471
                $requestTargetKind = $request->getTargetKind();
472
                if ($request->isLinkUri()) {
473
                    // In the query 'Orders(1245)/$links/Customer', the targeted
474
                    // Customer might be null
475
                    if (is_null($result)) {
476
                        throw ODataException::createResourceNotFoundError(
477
                            $request->getIdentifier()
478
                        );
479
                    }
480
481
                    $odataModelInstance = $objectModelSerializer->writeUrlElement($result);
482
                } else if ($requestTargetKind == TargetKind::RESOURCE) {
483
                    if (!is_null($this->_serviceHost->getRequestIfMatch())
484
                        && !is_null($this->_serviceHost->getRequestIfNoneMatch())
485
                    ) {
486
                        throw ODataException::createBadRequestError(
487
                            Messages::bothIfMatchAndIfNoneMatchHeaderSpecified()
488
                        );
489
                    }
490
                    // handle entry resource
491
                    $needToSerializeResponse = true;
492
                    $targetResourceType = $request->getTargetResourceType();
493
                    $eTag = $this->compareETag(
494
                        $result,
495
                        $targetResourceType,
496
                        $needToSerializeResponse
497
                    );
498
499
                    if ($needToSerializeResponse) {
500
                        if (is_null($result)) {
501
                            // In the query 'Orders(1245)/Customer', the targeted
502
                            // Customer might be null
503
                            // set status code to 204 => 'No Content'
504
                            $this->_serviceHost->setResponseStatusCode(HttpStatus::CODE_NOCONTENT);
505
                            $hasResponseBody = false;
0 ignored issues
show
Unused Code introduced by
The assignment to $hasResponseBody is dead and can be removed.
Loading history...
506
                        } else {
507
                            $odataModelInstance = $objectModelSerializer->writeTopLevelElement($result);
508
                        }
509
                    } else {
510
                        // Resource is not modified so set status code
511
                        // to 304 => 'Not Modified'
512
                        $this->_serviceHost->setResponseStatusCode(HttpStatus::CODE_NOT_MODIFIED);
513
                        $hasResponseBody = false;
514
                    }
515
516
                    // if resource has eTagProperty then eTag header needs to written
517
                    if (!is_null($eTag)) {
518
                        $this->_serviceHost->setResponseETag($eTag);
519
                    }
520
                } else if ($requestTargetKind == TargetKind::COMPLEX_OBJECT) {
521
                    $targetResourceType = $request->getTargetResourceType();
522
                    $odataModelInstance = $objectModelSerializer->writeTopLevelComplexObject(
523
                        $result,
524
                        $request->getProjectedProperty()->getName(),
525
                        $targetResourceType
526
                    );
527
                } else if ($requestTargetKind == TargetKind::BAG) {
528
                    $targetResourceType = $request->getTargetResourceType();
529
                    $odataModelInstance = $objectModelSerializer->writeTopLevelBagObject(
530
                        $result,
531
                        $request->getProjectedProperty()->getName(),
532
                        $targetResourceType
533
                    );
534
                } else if ($requestTargetKind == TargetKind::PRIMITIVE) {
535
                    $projectedProperty = $request->getProjectedProperty();
536
                    $odataModelInstance = $objectModelSerializer->writeTopLevelPrimitive(
537
                        $result,
538
                        $projectedProperty
539
                    );
540
                } else if ($requestTargetKind == TargetKind::PRIMITIVE_VALUE) {
541
                    // Code path for primitive value (Since its primitve no need for
542
                    // object model serialization)
543
                    // Customers('ANU')/CompanyName/$value => string
544
                    // Employees(1)/Photo/$value => binary stream
545
                    // Customers/$count => string
546
                } else {
547
                    self::assert(false, 'Unexpected resource target kind');
548
                }
549
            }
550
        }
551
552
        $write($this, $request, $odataModelInstance);
553
    }
554
555
    /**
556
     * Gets the response format for the requested resource.
557
     *
558
     * @param RequestDescription $request The request submitted by client and it's execution result.
559
     * @param UriProcessor $uriProcessor The reference to the UriProcessor.
560
     * @param IService $service Reference to the service implementation instance
561
     *
562
     * @return string the response content-type, a null value means the requested resource
563
     * is named stream and IDSSP2::getStreamContentType returned null
564
     *
565
     * @throws ODataException, HttpHeaderFailure
566
     */
567
    public static function getResponseContentType(
568
        RequestDescription $request,
569
        UriProcessor $uriProcessor,
570
        IService $service
571
    ) {
572
573
        // The Accept request-header field specifies media types which are acceptable for the response
574
575
        $host = $service->getHost();
576
        $requestAcceptText = $host->getRequestAccept();
577
        $requestVersion = $request->getResponseVersion();
578
579
        //if the $format header is present it overrides the accepts header
580
        $format = $request->getQueryStringItem(ODataConstants::HTTPQUERY_STRING_FORMAT);
581
        if (!is_null($format)) {
582
583
            //There's a strange edge case..if application/json is supplied and it's V3
584
            if ($format == MimeTypes::MIME_APPLICATION_JSON && $requestVersion == Version::v3()) {
585
                //then it's actual minimalmetadata
586
                //TODO: should this be done with the header text too?
587
                $format = MimeTypes::MIME_APPLICATION_JSON_MINIMAL_META;
588
            }
589
590
            $requestAcceptText = ServiceHost::translateFormatToMime($requestVersion, $format);
591
        }
592
593
594
595
596
        //The response format can be dictated by the target resource kind. IE a $value will be different then expected
597
        //getTargetKind doesn't deal with link resources directly and this can change things
598
        $targetKind = $request->isLinkUri() ? TargetKind::LINK : $request->getTargetKind();
599
600
        switch ($targetKind) {
601
            case TargetKind::METADATA:
602
                return HttpProcessUtility::selectMimeType(
603
                    $requestAcceptText,
604
                    array(MimeTypes::MIME_APPLICATION_XML)
605
                );
606
607
            case TargetKind::SERVICE_DIRECTORY:
608
                return HttpProcessUtility::selectMimeType(
609
                    $requestAcceptText,
610
                    array(
611
                        MimeTypes::MIME_APPLICATION_XML,
612
                        MimeTypes::MIME_APPLICATION_ATOMSERVICE,
613
                        MimeTypes::MIME_APPLICATION_JSON,
614
                        MimeTypes::MIME_APPLICATION_JSON_FULL_META,
615
                        MimeTypes::MIME_APPLICATION_JSON_NO_META,
616
                        MimeTypes::MIME_APPLICATION_JSON_MINIMAL_META,
617
                        MimeTypes::MIME_APPLICATION_JSON_VERBOSE,
618
619
                    )
620
                );
621
622
            case TargetKind::PRIMITIVE_VALUE:
623
                $supportedResponseMimeTypes = array(MimeTypes::MIME_TEXTPLAIN);
624
625
                if ($request->getIdentifier() != '$count') {
626
                    $projectedProperty = $request->getProjectedProperty();
627
                    self::assert(
628
                        !is_null($projectedProperty),
629
                        '!is_null($projectedProperty)'
630
                    );
631
                    $type = $projectedProperty->getInstanceType();
632
                    self::assert(
633
                        !is_null($type) && $type instanceof IType,
634
                        '!is_null($type) && $type instanceof IType'
635
                    );
636
                    if ($type instanceof Binary) {
637
                        $supportedResponseMimeTypes = array(MimeTypes::MIME_APPLICATION_OCTETSTREAM);
638
                    }
639
                }
640
641
                return HttpProcessUtility::selectMimeType(
642
                    $requestAcceptText,
643
                    $supportedResponseMimeTypes
644
                );
645
646
            case TargetKind::PRIMITIVE:
647
            case TargetKind::COMPLEX_OBJECT:
648
            case TargetKind::BAG:
649
            case TargetKind::LINK:
650
                return HttpProcessUtility::selectMimeType(
651
                    $requestAcceptText,
652
                    array(
653
                        MimeTypes::MIME_APPLICATION_XML,
654
                        MimeTypes::MIME_TEXTXML,
655
                        MimeTypes::MIME_APPLICATION_JSON,
656
                        MimeTypes::MIME_APPLICATION_JSON_FULL_META,
657
                        MimeTypes::MIME_APPLICATION_JSON_NO_META,
658
                        MimeTypes::MIME_APPLICATION_JSON_MINIMAL_META,
659
                        MimeTypes::MIME_APPLICATION_JSON_VERBOSE,
660
                    )
661
                );
662
663
            case TargetKind::BATCH:
664
            case TargetKind::RESOURCE:
665
                return HttpProcessUtility::selectMimeType(
666
                    $requestAcceptText,
667
                    array(
668
                        MimeTypes::MIME_APPLICATION_ATOM,
669
                        MimeTypes::MIME_APPLICATION_JSON,
670
                        MimeTypes::MIME_APPLICATION_JSON_FULL_META,
671
                        MimeTypes::MIME_APPLICATION_JSON_NO_META,
672
                        MimeTypes::MIME_APPLICATION_JSON_MINIMAL_META,
673
                        MimeTypes::MIME_APPLICATION_JSON_VERBOSE,
674
                    )
675
                );
676
677
            case TargetKind::MEDIA_RESOURCE:
678
                if (!$request->isNamedStream() && !$request->getTargetResourceType()->isMediaLinkEntry()) {
679
                    throw ODataException::createBadRequestError(
680
                        Messages::badRequestInvalidUriForMediaResource(
681
                            $host->getAbsoluteRequestUri()->getUrlAsString()
682
                        )
683
                    );
684
                }
685
686
                $uriProcessor->execute();
687
                $request->setExecuted();
688
                // DSSW::getStreamContentType can throw error in 2 cases
689
                // 1. If the required stream implementation not found
690
                // 2. If IDSSP::getStreamContentType returns NULL for MLE
691
                $responseContentType = $service->getStreamProviderWrapper()
692
                    ->getStreamContentType(
693
                        $request->getTargetResult(),
0 ignored issues
show
Bug introduced by
It seems like $request->getTargetResult() can also be of type array and array; however, parameter $entity of POData\Providers\Stream\...:getStreamContentType() does only seem to accept object, 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

693
                        /** @scrutinizer ignore-type */ $request->getTargetResult(),
Loading history...
694
                        $request->getResourceStreamInfo()
695
                    );
696
697
698
                // Note StreamWrapper::getStreamContentType can return NULL if the requested named stream has not
699
                // yet been uploaded. But for an MLE if IDSSP::getStreamContentType returns NULL then StreamWrapper will throw error
700
                if (!is_null($responseContentType)) {
0 ignored issues
show
introduced by
The condition is_null($responseContentType) is always false.
Loading history...
701
                    $responseContentType = HttpProcessUtility::selectMimeType(
702
                        $requestAcceptText,
703
                        array($responseContentType)
704
                    );
705
                }
706
707
                return $responseContentType;
708
        }
709
710
711
        //If we got here, we just don't know what it is...
712
        throw new ODataException(Messages::unsupportedMediaType(), 415);
713
714
    }
715
716
717
718
    /**
719
     * For the given entry object compare it's eTag (if it has eTag properties)
720
     * with current eTag request headers (if it present).
721
     *
722
     * @param mixed        &$entryObject             entity resource for which etag
723
     *                                               needs to be checked.
724
     * @param ResourceType &$resourceType            Resource type of the entry
725
     *                                               object.
726
     * @param boolean      &$needToSerializeResponse On return, this will contain
727
     *                                               True if response needs to be
728
     *                                               serialized, False otherwise.
729
     * @param boolean $needToSerializeResponse
730
     *
731
     * @return string|null The ETag for the entry object if it has eTag properties
732
     *                     NULL otherwise.
733
     */
734
    protected function compareETag(&$entryObject, ResourceType &$resourceType,
735
        &$needToSerializeResponse
736
    ) {
737
        $needToSerializeResponse = true;
738
        $eTag = null;
739
        $ifMatch = $this->_serviceHost->getRequestIfMatch();
740
        $ifNoneMatch = $this->_serviceHost->getRequestIfNoneMatch();
741
        if (is_null($entryObject)) {
742
            if (!is_null($ifMatch)) {
743
                throw ODataException::createPreConditionFailedError(
744
                    Messages::eTagNotAllowedForNonExistingResource()
745
                );
746
            }
747
748
            return null;
749
        }
750
751
        if ($this->config->getValidateETagHeader() && !$resourceType->hasETagProperties()) {
752
            if (!is_null($ifMatch) || !is_null($ifNoneMatch)) {
753
                // No eTag properties but request has eTag headers, bad request
754
                throw ODataException::createBadRequestError(
755
                    Messages::noETagPropertiesForType()
756
                );
757
            }
758
759
            // We need write the response but no eTag header
760
            return null;
761
        }
762
763
        if (!$this->config->getValidateETagHeader()) {
764
            // Configuration says do not validate ETag so we will not write ETag header in the
765
            // response even though the requested resource support it
766
            return null;
767
        }
768
769
        if (is_null($ifMatch) && is_null($ifNoneMatch)) {
770
            // No request eTag header, we need to write the response
771
            // and eTag header
772
        } else if (is_string($ifMatch) && strcmp($ifMatch, '*') == 0) {
773
            // If-Match:* => we need to write the response and eTag header
774
        } else if (is_string($ifNoneMatch) && strcmp($ifNoneMatch, '*') == 0) {
775
            // if-None-Match:* => Do not write the response (304 not modified),
776
            // but write eTag header
777
            $needToSerializeResponse = false;
778
        } else {
779
            $eTag = $this->getETagForEntry($entryObject, $resourceType);
780
            // Note: The following code for attaching the prefix W\"
781
            // and the suffix " can be done in getETagForEntry function
782
            // but that is causing an issue in Linux env where the
783
            // firefix browser is unable to parse the ETag in this case.
784
            // Need to follow up PHP core devs for this.
785
            $eTag = ODataConstants::HTTP_WEAK_ETAG_PREFIX . $eTag . '"';
786
            if (!is_null($ifMatch)) {
787
                if (strcmp($eTag, $ifMatch) != 0) {
788
                    // Requested If-Match value does not match with current
789
                    // eTag Value then pre-condition error
790
                    // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
791
                    throw ODataException::createPreConditionFailedError(
792
                        Messages::eTagValueDoesNotMatch()
793
                    );
794
                }
795
            } else if (is_string($ifNoneMatch) && strcmp($eTag, $ifNoneMatch) == 0) {
796
                //304 not modified, but in write eTag header
797
                $needToSerializeResponse = false;
798
            }
799
        }
800
801
        if (is_null($eTag)) {
802
            $eTag = $this->getETagForEntry($entryObject, $resourceType);
803
            // Note: The following code for attaching the prefix W\"
804
            // and the suffix " can be done in getETagForEntry function
805
            // but that is causing an issue in Linux env where the
806
            // firefix browser is unable to parse the ETag in this case.
807
            // Need to follow up PHP core devs for this.
808
            $eTag = ODataConstants::HTTP_WEAK_ETAG_PREFIX . $eTag . '"';
809
        }
810
811
        return $eTag;
812
    }
813
814
    /**
815
     * Returns the etag for the given resource.
816
     * Note: This function will not add W\" prefix and " suffix, its callers
817
     * repsonsability.
818
     *
819
     * @param mixed        &$entryObject  Resource for which etag value needs to
820
     *                                    be returned
821
     * @param ResourceType &$resourceType Resource type of the $entryObject
822
     *
823
     * @return string|null ETag value for the given resource (with values encoded
824
     *                     for use in a URI) there are etag properties, NULL if
825
     *                     there is no etag property.
826
     */
827
    protected function getETagForEntry(&$entryObject, ResourceType &$resourceType)
828
    {
829
        $eTag = null;
830
        $comma = null;
831
        foreach ($resourceType->getETagProperties() as $eTagProperty) {
832
            $type = $eTagProperty->getInstanceType();
833
            self::assert(
834
                !is_null($type) && $type instanceof IType,
835
                '!is_null($type) && $type instanceof IType'
836
            );
837
838
            $value = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $value is dead and can be removed.
Loading history...
839
            try {
840
841
                //TODO #88...also this seems like dupe work
842
                $reflectionProperty = new \ReflectionProperty($entryObject, $eTagProperty->getName());
843
                $value = $reflectionProperty->getValue($entryObject);
844
            } catch (\ReflectionException $reflectionException) {
845
                throw ODataException::createInternalServerError(
846
                    Messages::failedToAccessProperty($eTagProperty->getName(), $resourceType->getName())
847
                );
848
            }
849
850
            if (is_null($value)) {
851
                $eTag = $eTag . $comma . 'null';
852
            } else {
853
                $eTag = $eTag . $comma . $type->convertToOData($value);
854
            }
855
856
            $comma = ',';
857
        }
858
859
        if (!is_null($eTag)) {
860
            // If eTag is made up of datetime or string properties then the above
861
            // IType::convertToOData will perform utf8 and url encode. But we don't
862
            // want this for eTag value.
863
            $eTag = urldecode(utf8_decode($eTag));
864
            return rtrim($eTag, ',');
865
        }
866
867
        return null;
868
    }
869
870
    /**
871
     * This function will perform the following operations:
872
     * (1) Invoke delegateRequestProcessing method to process the request based
873
     *     on request method (GET, PUT/MERGE, POST, DELETE)
874
     * (3) If the result of processing of request needs to be serialized as HTTP
875
     *     response body (e.g. GET request result in single resource or resource
876
     *     collection, successful POST operation for an entity need inserted
877
     *     entity to be serialized back etc..), Serialize the result by using
878
     *     'serializeReultForResponseBody' method
879
     *     Set the serialized result to
880
     *     WebOperationContext::Current()::OutgoingWebResponseContext::Stream.
881
     *
882
     *     @return void
883
     */
884
    protected function handleRequest2()
885
    {
886
    }
887
888
    /**
889
     * This method will perform the following operations:
890
     * (1) If request method is GET, then result is already there in the
891
     *     RequestDescription so simply return the RequestDescription
892
     * (2). If request method is for CDU
893
     *      (Create/Delete/Update - POST/DELETE/PUT-MERGE) hand
894
     *      over the responsibility to respective handlers. The handler
895
     *      methods are:
896
     *      (a) handlePOSTOperation() => POST
897
     *      (b) handlePUTOperation() => PUT/MERGE
898
     *      (c) handleDELETEOperation() => DELETE
899
     * (3). Check whether its required to write any result to the response
900
     *      body
901
     *      (a). Request method is GET
902
     *      (b). Request is a POST for adding NEW Entry
903
     *      (c). Request is a POST for adding Media Resource Stream
904
     *      (d). Request is a POST for adding a link
905
     *      (e). Request is a DELETE for deleting entry or relationship
906
     *      (f). Request is a PUT/MERGE for updating an entry
907
     *      (g). Request is a PUT for updating a link
908
     *     In case a, b and c we need to write the result to response body,
909
     *     for d, e, f and g no body content.
910
     *
911
     * @return RequestDescription|null Instance of RequestDescription with
912
     *         result to be write back Null if no result to write.
913
     */
914
    protected function delegateRequestProcessing()
915
    {
916
    }
917
918
    /**
919
     * Serialize the result in the current request description using
920
     * appropriate odata writer (AtomODataWriter/JSONODataWriter)
921
     *
922
     * @return void
923
     *
924
     */
925
    protected function serializeReultForResponseBody()
926
    {
927
    }
928
929
    /**
930
     * Handle POST request.
931
     *
932
     * @return void
933
     *
934
     * @throws NotImplementedException
935
     */
936
    protected function handlePOSTOperation()
937
    {
938
    }
939
940
    /**
941
     * Handle PUT/MERGE request.
942
     *
943
     * @return void
944
     *
945
     * @throws NotImplementedException
946
     */
947
    protected function handlePUTOperation()
948
    {
949
    }
950
951
    /**
952
     * Handle DELETE request.
953
     *
954
     * @return void
955
     *
956
     * @throws NotImplementedException
957
     */
958
    protected function handleDELETEOperation()
959
    {
960
    }
961
962
    /**
963
     * Assert that the given condition is true.
964
     *
965
     * @param boolean $condition         The condtion to check.
966
     * @param string  $conditionAsString Message to show if assertion fails.
967
     *
968
     * @return void
969
     *
970
     * @throws InvalidOperationException
971
     */
972
    protected static function assert($condition, $conditionAsString)
973
    {
974
        if (!$condition) {
975
            throw new InvalidOperationException(
976
                "Unexpected state, expecting $conditionAsString"
977
            );
978
        }
979
    }
980
}
981