Passed
Push — master ( 8cb26d...46ab0d )
by Alex
03:35
created

BaseService::handlePUTOperation()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
eloc 1
nc 1
nop 0
1
<?php
2
3
namespace POData;
4
5
use POData\Common\ErrorHandler;
6
use POData\Common\HttpStatus;
7
use POData\Common\Messages;
8
use POData\Common\MimeTypes;
9
use POData\Common\NotImplementedException;
10
use POData\Common\ODataConstants;
11
use POData\Common\ODataException;
12
use POData\Common\ReflectionHandler;
13
use POData\Common\Version;
14
use POData\Configuration\IServiceConfiguration;
15
use POData\Configuration\ServiceConfiguration;
16
use POData\ObjectModel\IObjectSerialiser;
17
use POData\ObjectModel\ObjectModelSerializer;
18
use POData\ObjectModel\ODataFeed;
19
use POData\ObjectModel\ODataURLCollection;
20
use POData\OperationContext\HTTPRequestMethod;
21
use POData\OperationContext\IOperationContext;
22
use POData\OperationContext\ServiceHost;
23
use POData\Providers\Metadata\IMetadataProvider;
24
use POData\Providers\Metadata\ResourceType;
25
use POData\Providers\Metadata\Type\Binary;
26
use POData\Providers\Metadata\Type\IType;
27
use POData\Providers\ProvidersWrapper;
28
use POData\Providers\Query\IQueryProvider;
29
use POData\Providers\Query\QueryResult;
30
use POData\Providers\Stream\StreamProviderWrapper;
31
use POData\UriProcessor\Interfaces\IUriProcessor;
32
use POData\UriProcessor\RequestDescription;
33
use POData\UriProcessor\ResourcePathProcessor\SegmentParser\TargetKind;
34
use POData\UriProcessor\UriProcessor;
35
use POData\UriProcessor\UriProcessorNew;
36
use POData\Writers\Atom\AtomODataWriter;
37
use POData\Writers\Json\JsonLightMetadataLevel;
38
use POData\Writers\Json\JsonLightODataWriter;
39
use POData\Writers\Json\JsonODataV1Writer;
40
use POData\Writers\Json\JsonODataV2Writer;
41
use POData\Writers\ODataWriterRegistry;
42
use POData\Writers\ResponseWriter;
43
44
/**
45
 * Class BaseService.
46
 *
47
 * The base class for all BaseService specific classes. This class implements
48
 * the following interfaces:
49
 *  (1) IRequestHandler
50
 *      Implementing this interface requires defining the function
51
 *      'handleRequest' that will be invoked by dispatcher
52
 *  (2) IService
53
 *      Force BaseService class to implement functions for custom
54
 *      data service providers
55
 */
56
abstract class BaseService implements IRequestHandler, IService
57
{
58
    /**
59
     * The wrapper over IQueryProvider and IMetadataProvider implementations.
60
     *
61
     * @var ProvidersWrapper
62
     */
63
    private $providersWrapper;
64
65
    /**
66
     * The wrapper over IStreamProvider implementation.
67
     *
68
     * @var StreamProviderWrapper
69
     */
70
    protected $streamProvider;
71
72
    /**
73
     * Hold reference to the ServiceHost instance created by dispatcher,
74
     * using this library can access headers and body of Http Request
75
     * dispatcher received and the Http Response Dispatcher is going to send.
76
     *
77
     * @var ServiceHost
78
     */
79
    private $_serviceHost;
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 IServiceConfiguration
87
     */
88
    protected $config;
89
90
    /**
91
     * Hold reference to object serialiser - bit wot turns PHP objects
92
     * into message traffic on wire.
93
     *
94
     * @var IObjectSerialiser
95
     */
96
    protected $objectSerialiser;
97
98
    /**
99
     * Get reference to object serialiser - bit wot turns PHP objects
100
     * into message traffic on wire.
101
     *
102
     * @var IObjectSerialiser
103
     */
104
    public function getObjectSerialiser()
105
    {
106
        assert(null != $this->objectSerialiser);
107
108
        return $this->objectSerialiser;
109
    }
110
111
    protected function __construct(IObjectSerialiser $serialiser = null)
112
    {
113
        if (null != $serialiser) {
114
            $serialiser->setService($this);
115
        } else {
116
            $serialiser = new ObjectModelSerializer($this, null);
117
        }
118
        $this->objectSerialiser = $serialiser;
119
    }
120
121
    /**
122
     * Gets reference to ServiceConfiguration instance so that
123
     * service specific rules defined by the developer can be
124
     * accessed.
125
     *
126
     * @return IServiceConfiguration
127
     */
128
    public function getConfiguration()
129
    {
130
        assert(null != $this->config);
131
132
        return $this->config;
133
    }
134
135
    //TODO: shouldn't we hide this from the interface..if we need it at all.
136
137
    /**
138
     * Get the wrapper over developer's IQueryProvider and IMetadataProvider implementation.
139
     *
140
     * @return ProvidersWrapper
141
     */
142
    public function getProvidersWrapper()
143
    {
144
        return $this->providersWrapper;
145
    }
146
147
    /**
148
     * Gets reference to wrapper class instance over IDSSP implementation.
149
     *
150
     * @return StreamProviderWrapper
151
     */
152
    public function getStreamProviderWrapper()
153
    {
154
        return $this->streamProvider;
155
    }
156
157
    /**
158
     * Get reference to the data service host instance.
159
     *
160
     * @return ServiceHost
161
     */
162
    public function getHost()
163
    {
164
        assert(null != $this->_serviceHost);
165
166
        return $this->_serviceHost;
167
    }
168
169
    /**
170
     * Sets the data service host instance.
171
     *
172
     * @param ServiceHost $serviceHost The data service host instance
173
     */
174
    public function setHost(ServiceHost $serviceHost)
175
    {
176
        $this->_serviceHost = $serviceHost;
177
    }
178
179
    /**
180
     * To get reference to operation context where we have direct access to
181
     * headers and body of Http Request, we have received and the Http Response
182
     * We are going to send.
183
     *
184
     * @return IOperationContext
185
     */
186
    public function getOperationContext()
187
    {
188
        return $this->getHost()->getOperationContext();
189
    }
190
191
    /**
192
     * Get reference to the wrapper over IStreamProvider or
193
     * IStreamProvider2 implementations.
194
     *
195
     * @return StreamProviderWrapper
196
     */
197
    public function getStreamProvider()
198
    {
199
        if (is_null($this->streamProvider)) {
200
            $this->streamProvider = new StreamProviderWrapper();
201
            $this->streamProvider->setService($this);
202
        }
203
204
        return $this->streamProvider;
205
    }
206
207
    /**
208
     * Top-level handler invoked by Dispatcher against any request to this
209
     * service. This method will hand over request processing task to other
210
     * functions which process the request, set required headers and Response
211
     * stream (if any in Atom/Json format) in
212
     * WebOperationContext::Current()::OutgoingWebResponseContext.
213
     * Once this function returns, dispatcher uses global WebOperationContext
214
     * to write out the request response to client.
215
     * This function will perform the following operations:
216
     * (1) Check whether the top level service class implements
217
     *     IServiceProvider which means the service is a custom service, in
218
     *     this case make sure the top level service class implements
219
     *     IMetaDataProvider and IQueryProvider.
220
     *     These are the minimal interfaces that a custom service to be
221
     *     implemented in order to expose its data as OData. Save reference to
222
     *     These interface implementations.
223
     *     NOTE: Here we will ensure only providers for IDSQP and IDSMP. The
224
     *     IDSSP will be ensured only when there is an GET request on MLE/Named
225
     *     stream.
226
     *
227
     * (2). Invoke 'Initialize' method of top level service for
228
     *      collecting the configuration rules set by the developer for this
229
     *      service.
230
     *
231
     * (3). Invoke the Uri processor to process the request URI. The uri
232
     *      processor will do the following:
233
     *      (a). Validate the request uri syntax using OData uri rules
234
     *      (b). Validate the request using metadata of this service
235
     *      (c). Parse the request uri and using, IQueryProvider
236
     *           implementation, fetches the resources pointed by the uri
237
     *           if required
238
     *      (d). Build a RequestDescription which encapsulate everything
239
     *           related to request uri (e.g. type of resource, result
240
     *           etc...)
241
     * (3). Invoke handleRequest2 for further processing
242
     */
243
    public function handleRequest()
244
    {
245
        try {
246
            $this->createProviders();
247
            $this->getHost()->validateQueryParameters();
248
            //$requestMethod = $this->getOperationContext()->incomingRequest()->getMethod();
0 ignored issues
show
Unused Code Comprehensibility introduced by
67% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
249
            //if ($requestMethod != HTTPRequestMethod::GET()) {
0 ignored issues
show
Unused Code Comprehensibility introduced by
57% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
250
            // Now supporting GET and trying to support PUT
251
            //throw ODataException::createNotImplementedError(Messages::onlyReadSupport($requestMethod));
0 ignored issues
show
Unused Code Comprehensibility introduced by
65% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
252
            //}
253
254
            $uriProcessor = UriProcessorNew::process($this);
255
            $request = $uriProcessor->getRequest();
256
            $this->serializeResult($request, $uriProcessor);
257
        } catch (\Exception $exception) {
258
            ErrorHandler::handleException($exception, $this);
259
            // Return to dispatcher for writing serialized exception
260
            return;
261
        }
262
    }
263
264
    /**
265
     * @return IQueryProvider
266
     */
267
    abstract public function getQueryProvider();
268
269
    /**
270
     * @return IMetadataProvider
271
     */
272
    abstract public function getMetadataProvider();
273
274
    /**
275
     *  @return \POData\Providers\Stream\IStreamProvider2
276
     */
277
    abstract public function getStreamProviderX();
278
279
    /** @var ODataWriterRegistry */
280
    protected $writerRegistry;
281
282
    /**
283
     * Returns the ODataWriterRegistry to use when writing the response to a service document or resource request.
284
     *
285
     * @return ODataWriterRegistry
286
     */
287
    public function getODataWriterRegistry()
288
    {
289
        assert(null != $this->writerRegistry);
290
291
        return $this->writerRegistry;
292
    }
293
294
    /**
295
     * This method will query and validates for IMetadataProvider and IQueryProvider implementations, invokes
296
     * BaseService::Initialize to initialize service specific policies.
297
     *
298
     * @throws ODataException
299
     */
300
    protected function createProviders()
301
    {
302
        $metadataProvider = $this->getMetadataProvider();
303
        if (is_null($metadataProvider)) {
304
            throw ODataException::createInternalServerError(Messages::providersWrapperNull());
305
        }
306
307
        if (!$metadataProvider instanceof IMetadataProvider) {
308
            throw ODataException::createInternalServerError(Messages::invalidMetadataInstance());
309
        }
310
311
        $queryProvider = $this->getQueryProvider();
312
313
        if (is_null($queryProvider)) {
314
            throw ODataException::createInternalServerError(Messages::providersWrapperNull());
315
        }
316
317
        if (!$queryProvider instanceof IQueryProvider) {
318
            throw ODataException::createInternalServerError(Messages::invalidQueryInstance());
319
        }
320
321
        $this->config = new ServiceConfiguration($metadataProvider);
322
        $this->providersWrapper = new ProvidersWrapper(
323
            $metadataProvider,
324
            $queryProvider,
325
            $this->config
326
        );
327
328
        $this->initialize($this->config);
329
330
        //TODO: this seems like a bad spot to do this
331
        $this->writerRegistry = new ODataWriterRegistry();
332
        $this->registerWriters();
333
    }
334
335
    //TODO: i don't want this to be public..but it's the only way to test it right now...
336
    public function registerWriters()
337
    {
338
        $registry = $this->getODataWriterRegistry();
339
        $serviceVersion = $this->getConfiguration()->getMaxDataServiceVersion();
340
        $serviceURI = $this->getHost()->getAbsoluteServiceUri()->getUrlAsString();
341
342
        //We always register the v1 stuff
343
        $registry->register(new JsonODataV1Writer());
344
        $registry->register(new AtomODataWriter($serviceURI));
345
346
        if (-1 < $serviceVersion->compare(Version::v2())) {
347
            $registry->register(new JsonODataV2Writer());
348
        }
349
350
        if (-1 < $serviceVersion->compare(Version::v3())) {
351
            $registry->register(new JsonLightODataWriter(JsonLightMetadataLevel::NONE(), $serviceURI));
352
            $registry->register(new JsonLightODataWriter(JsonLightMetadataLevel::MINIMAL(), $serviceURI));
353
            $registry->register(new JsonLightODataWriter(JsonLightMetadataLevel::FULL(), $serviceURI));
354
        }
355
    }
356
357
    /**
358
     * Serialize the requested resource.
359
     *
360
     * @param RequestDescription $request      The description of the request  submitted by the client
361
     * @param IUriProcessor      $uriProcessor Reference to the uri processor
362
     */
363
    protected function serializeResult(RequestDescription $request, IUriProcessor $uriProcessor)
364
    {
365
        $isETagHeaderAllowed = $request->isETagHeaderAllowed();
366
367
        if ($this->getConfiguration()->getValidateETagHeader() && !$isETagHeaderAllowed) {
368
            if (!is_null($this->getHost()->getRequestIfMatch())
369
                || !is_null($this->getHost()->getRequestIfNoneMatch())
370
            ) {
371
                throw ODataException::createBadRequestError(
372
                    Messages::eTagCannotBeSpecified($this->getHost()->getAbsoluteRequestUri()->getUrlAsString())
373
                );
374
            }
375
        }
376
377
        $responseContentType = $this->getResponseContentType($request, $uriProcessor);
378
379
        if (is_null($responseContentType) && $request->getTargetKind() != TargetKind::MEDIA_RESOURCE()) {
380
            //the responseContentType can ONLY be null if it's a stream (media resource) and
381
            // that stream is storing null as the content type
382
            throw new ODataException(Messages::unsupportedMediaType(), 415);
383
        }
384
385
        $odataModelInstance = null;
386
        $hasResponseBody = true;
387
        // Execution required at this point if request points to any resource other than
388
389
        // (1) media resource - For Media resource 'getResponseContentType' already performed execution as
390
        // it needs to know the mime type of the stream
391
        // (2) metadata - internal resource
392
        // (3) service directory - internal resource
393
        if ($request->needExecution()) {
394
            $method = $this->getHost()->getOperationContext()->incomingRequest()->getMethod();
395
            $uriProcessor->execute();
396
            if (HTTPRequestMethod::DELETE() == $method) {
397
                $this->getHost()->setResponseStatusCode(HttpStatus::CODE_OK);
398
399
                return;
400
            }
401
402
            $objectModelSerializer = $this->getObjectSerialiser();
403
            $objectModelSerializer->setRequest($request);
404
405
            $targetResourceType = $request->getTargetResourceType();
406
            assert(null != $targetResourceType, 'Target resource type cannot be null');
407
408
            $method = (HTTPRequestMethod::POST() != $method);
409
            if (!$request->isSingleResult() && $method) {
410
                // Code path for collection (feed or links)
411
                $entryObjects = $request->getTargetResult();
412
                assert($entryObjects instanceof QueryResult, '!$entryObjects instanceof QueryResult');
413
                assert(is_array($entryObjects->results), '!is_array($entryObjects->results)');
414
                // If related resource set is empty for an entry then we should
415
                // not throw error instead response must be empty feed or empty links
416
                if ($request->isLinkUri()) {
417
                    $odataModelInstance = $objectModelSerializer->writeUrlElements($entryObjects);
418
                    assert(
419
                        $odataModelInstance instanceof ODataURLCollection,
420
                        '!$odataModelInstance instanceof ODataURLCollection'
421
                    );
422
                } else {
423
                    $odataModelInstance = $objectModelSerializer->writeTopLevelElements($entryObjects);
424
                    assert($odataModelInstance instanceof ODataFeed, '!$odataModelInstance instanceof ODataFeed');
425
                }
426
            } else {
427
                // Code path for entity, complex, bag, resource reference link,
428
                // primitive type or primitive value
429
                $result = $request->getTargetResult();
430
                if (!$result instanceof QueryResult) {
431
                    $result = new QueryResult();
432
                    $result->results = $request->getTargetResult();
433
                }
434
                $requestTargetKind = $request->getTargetKind();
435
                $requestProperty = $request->getProjectedProperty();
436
                if ($request->isLinkUri()) {
437
                    // In the query 'Orders(1245)/$links/Customer', the targeted
438
                    // Customer might be null
439
                    if (is_null($result->results)) {
440
                        throw ODataException::createResourceNotFoundError($request->getIdentifier());
441
                    }
442
                    $odataModelInstance = $objectModelSerializer->writeUrlElement($result);
443
                } elseif (TargetKind::RESOURCE() == $requestTargetKind
444
                          || TargetKind::SINGLETON() == $requestTargetKind) {
445
                    if (!is_null($this->getHost()->getRequestIfMatch())
446
                        && !is_null($this->getHost()->getRequestIfNoneMatch())
447
                    ) {
448
                        throw ODataException::createBadRequestError(
449
                            Messages::bothIfMatchAndIfNoneMatchHeaderSpecified()
450
                        );
451
                    }
452
                    // handle entry resource
453
                    $needToSerializeResponse = true;
454
                    $eTag = $this->compareETag($result, $targetResourceType, $needToSerializeResponse);
455
456
                    if ($needToSerializeResponse) {
457
                        if (is_null($result)) {
458
                            // In the query 'Orders(1245)/Customer', the targeted
459
                            // Customer might be null
460
                            // set status code to 204 => 'No Content'
461
                            $this->getHost()->setResponseStatusCode(HttpStatus::CODE_NOCONTENT);
462
                            $hasResponseBody = false;
463
                        } else {
464
                            $odataModelInstance = $objectModelSerializer->writeTopLevelElement($result);
465
                        }
466
                    } else {
467
                        // Resource is not modified so set status code
468
                        // to 304 => 'Not Modified'
0 ignored issues
show
Unused Code Comprehensibility introduced by
38% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
469
                        $this->getHost()->setResponseStatusCode(HttpStatus::CODE_NOT_MODIFIED);
470
                        $hasResponseBody = false;
471
                    }
472
473
                    // if resource has eTagProperty then eTag header needs to written
474
                    if (!is_null($eTag)) {
475
                        $this->getHost()->setResponseETag($eTag);
476
                    }
477
                } elseif (TargetKind::COMPLEX_OBJECT() == $requestTargetKind) {
478
                    assert(null != $requestProperty, 'Projected request property cannot be null');
479
                    $odataModelInstance = $objectModelSerializer->writeTopLevelComplexObject(
480
                        $result,
481
                        $requestProperty->getName(),
482
                        $targetResourceType
483
                    );
484
                } elseif (TargetKind::BAG() == $requestTargetKind) {
485
                    assert(null != $requestProperty, 'Projected request property cannot be null');
486
                    $odataModelInstance = $objectModelSerializer->writeTopLevelBagObject(
487
                        $result,
488
                        $requestProperty->getName(),
489
                        $targetResourceType,
490
                        $odataModelInstance
0 ignored issues
show
Unused Code introduced by
The call to IObjectSerialiser::writeTopLevelBagObject() has too many arguments starting with $odataModelInstance.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
491
                    );
492
                } elseif (TargetKind::PRIMITIVE() == $requestTargetKind) {
493
                    $odataModelInstance = $objectModelSerializer->writeTopLevelPrimitive(
494
                        $result,
495
                        $requestProperty,
496
                        $odataModelInstance
0 ignored issues
show
Unused Code introduced by
The call to IObjectSerialiser::writeTopLevelPrimitive() has too many arguments starting with $odataModelInstance.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
497
                    );
498
                } elseif (TargetKind::PRIMITIVE_VALUE() == $requestTargetKind) {
0 ignored issues
show
Unused Code introduced by
This elseif statement is empty, and could be removed.

This check looks for the bodies of elseif statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These elseif bodies can be removed. If you have an empty elseif but statements in the else branch, consider inverting the condition.

Loading history...
499
                    // Code path for primitive value (Since its primitve no need for
500
                    // object model serialization)
501
                    // Customers('ANU')/CompanyName/$value => string
0 ignored issues
show
Unused Code Comprehensibility introduced by
39% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
502
                    // Employees(1)/Photo/$value => binary stream
503
                    // Customers/$count => string
504
                } else {
505
                    assert(false, 'Unexpected resource target kind');
506
                }
507
            }
508
        }
509
510
        //Note: Response content type can be null for named stream
511
        if ($hasResponseBody && !is_null($responseContentType)) {
512
            if (TargetKind::MEDIA_RESOURCE() != $request->getTargetKind()
513
                && MimeTypes::MIME_APPLICATION_OCTETSTREAM != $responseContentType) {
514
                //append charset for everything except:
515
                //stream resources as they have their own content type
516
                //binary properties (they content type will be App Octet for those...is this a good way?
517
                //we could also decide based upon the projected property
518
519
                $responseContentType .= ';charset=utf-8';
520
            }
521
        }
522
523
        if ($hasResponseBody) {
524
            ResponseWriter::write($this, $request, $odataModelInstance, $responseContentType);
525
        }
526
    }
527
528
    /**
529
     * Gets the response format for the requested resource.
530
     *
531
     * @param RequestDescription $request      The request submitted by client and it's execution result
532
     * @param IUriProcessor      $uriProcessor The reference to the IUriProcessor
533
     *
534
     * @throws ODataException, HttpHeaderFailure
535
     *
536
     * @return string the response content-type, a null value means the requested resource
537
     *                is named stream and IDSSP2::getStreamContentType returned null
538
     */
539
    public function getResponseContentType(
540
        RequestDescription $request,
541
        IUriProcessor $uriProcessor
542
    ) {
543
        $baseMimeTypes = [
544
            MimeTypes::MIME_APPLICATION_JSON,
545
            MimeTypes::MIME_APPLICATION_JSON_FULL_META,
546
            MimeTypes::MIME_APPLICATION_JSON_NO_META,
547
            MimeTypes::MIME_APPLICATION_JSON_MINIMAL_META,
548
            MimeTypes::MIME_APPLICATION_JSON_VERBOSE, ];
549
550
        // The Accept request-header field specifies media types which are acceptable for the response
551
552
        $host = $this->getHost();
553
        $requestAcceptText = $host->getRequestAccept();
554
        $requestVersion = $request->getResponseVersion();
555
556
        //if the $format header is present it overrides the accepts header
557
        $format = $host->getQueryStringItem(ODataConstants::HTTPQUERY_STRING_FORMAT);
558
        if (!is_null($format)) {
559
            //There's a strange edge case..if application/json is supplied and it's V3
560
            if (MimeTypes::MIME_APPLICATION_JSON == $format && Version::v3() == $requestVersion) {
561
                //then it's actual minimalmetadata
562
                //TODO: should this be done with the header text too?
563
                $format = MimeTypes::MIME_APPLICATION_JSON_MINIMAL_META;
564
            }
565
566
            $requestAcceptText = ServiceHost::translateFormatToMime($requestVersion, $format);
567
        }
568
569
        //The response format can be dictated by the target resource kind. IE a $value will be different then expected
570
        //getTargetKind doesn't deal with link resources directly and this can change things
571
        $targetKind = $request->isLinkUri() ? TargetKind::LINK() : $request->getTargetKind();
572
573
        switch ($targetKind) {
574
            case TargetKind::METADATA():
575
                return HttpProcessUtility::selectMimeType(
576
                    $requestAcceptText,
577
                    [MimeTypes::MIME_APPLICATION_XML]
578
                );
579
580
            case TargetKind::SERVICE_DIRECTORY():
581
                return HttpProcessUtility::selectMimeType(
582
                    $requestAcceptText,
583
                    array_merge(
584
                        [MimeTypes::MIME_APPLICATION_ATOMSERVICE],
585
                        $baseMimeTypes
586
                    )
587
                );
588
589
            case TargetKind::PRIMITIVE_VALUE():
590
                $supportedResponseMimeTypes = [MimeTypes::MIME_TEXTPLAIN];
591
592
                if ('$count' != $request->getIdentifier()) {
593
                    $projectedProperty = $request->getProjectedProperty();
594
                    assert(!is_null($projectedProperty), 'is_null($projectedProperty)');
595
                    $type = $projectedProperty->getInstanceType();
596
                    assert($type instanceof IType, '!$type instanceof IType');
597
                    if ($type instanceof Binary) {
598
                        $supportedResponseMimeTypes = [MimeTypes::MIME_APPLICATION_OCTETSTREAM];
599
                    }
600
                }
601
602
                return HttpProcessUtility::selectMimeType(
603
                    $requestAcceptText,
604
                    $supportedResponseMimeTypes
605
                );
606
607
            case TargetKind::PRIMITIVE():
608
            case TargetKind::COMPLEX_OBJECT():
609
            case TargetKind::BAG():
610
            case TargetKind::LINK():
611
                return HttpProcessUtility::selectMimeType(
612
                    $requestAcceptText,
613
                    array_merge(
614
                        [MimeTypes::MIME_APPLICATION_XML,
615
                            MimeTypes::MIME_TEXTXML, ],
616
                        $baseMimeTypes
617
                    )
618
                );
619
620
            case TargetKind::SINGLETON():
621
            case TargetKind::RESOURCE():
622
                return HttpProcessUtility::selectMimeType(
623
                    $requestAcceptText,
624
                    array_merge(
625
                        [MimeTypes::MIME_APPLICATION_ATOM],
626
                        $baseMimeTypes
627
                    )
628
                );
629
630
            case TargetKind::MEDIA_RESOURCE():
631
                if (!$request->isNamedStream() && !$request->getTargetResourceType()->isMediaLinkEntry()) {
632
                    throw ODataException::createBadRequestError(
633
                        Messages::badRequestInvalidUriForMediaResource(
634
                            $host->getAbsoluteRequestUri()->getUrlAsString()
635
                        )
636
                    );
637
                }
638
639
                $uriProcessor->execute();
640
                $request->setExecuted();
641
                // DSSW::getStreamContentType can throw error in 2 cases
642
                // 1. If the required stream implementation not found
643
                // 2. If IDSSP::getStreamContentType returns NULL for MLE
644
                $responseContentType = $this->getStreamProviderWrapper()
645
                    ->getStreamContentType(
646
                        $request->getTargetResult(),
647
                        $request->getResourceStreamInfo()
648
                    );
649
650
                // Note StreamWrapper::getStreamContentType can return NULL if the requested named stream has not
651
                // yet been uploaded. But for an MLE if IDSSP::getStreamContentType returns NULL
652
                // then StreamWrapper will throw error
653
                if (!is_null($responseContentType)) {
654
                    $responseContentType = HttpProcessUtility::selectMimeType(
655
                        $requestAcceptText,
656
                        [$responseContentType]
657
                    );
658
                }
659
660
                return $responseContentType;
661
        }
662
663
        //If we got here, we just don't know what it is...
664
        throw new ODataException(Messages::unsupportedMediaType(), 415);
665
    }
666
667
    /**
668
     * For the given entry object compare its eTag (if it has eTag properties)
669
     * with current eTag request headers (if present).
670
     *
671
     * @param mixed        &$entryObject             entity resource for which etag
672
     *                                               needs to be checked
673
     * @param ResourceType &$resourceType            Resource type of the entry
674
     *                                               object
675
     * @param bool         &$needToSerializeResponse On return, this will contain
676
     *                                               True if response needs to be
677
     *                                               serialized, False otherwise
678
     * @param bool         $needToSerializeResponse
679
     *
680
     * @return string|null The ETag for the entry object if it has eTag properties
681
     *                     NULL otherwise
682
     */
683
    protected function compareETag(
684
        &$entryObject,
685
        ResourceType & $resourceType,
686
        &$needToSerializeResponse
687
    ) {
688
        $needToSerializeResponse = true;
689
        $eTag = null;
690
        $ifMatch = $this->getHost()->getRequestIfMatch();
691
        $ifNoneMatch = $this->getHost()->getRequestIfNoneMatch();
692
        if (is_null($entryObject)) {
693
            if (!is_null($ifMatch)) {
694
                throw ODataException::createPreConditionFailedError(
695
                    Messages::eTagNotAllowedForNonExistingResource()
696
                );
697
            }
698
699
            return;
700
        }
701
702
        if ($this->getConfiguration()->getValidateETagHeader() && !$resourceType->hasETagProperties()) {
703
            if (!is_null($ifMatch) || !is_null($ifNoneMatch)) {
704
                // No eTag properties but request has eTag headers, bad request
705
                throw ODataException::createBadRequestError(
706
                    Messages::noETagPropertiesForType()
707
                );
708
            }
709
710
            // We need write the response but no eTag header
711
            return;
712
        }
713
714
        if (!$this->getConfiguration()->getValidateETagHeader()) {
715
            // Configuration says do not validate ETag, so we will not write ETag header in the
716
            // response even though the requested resource support it
717
            return;
718
        }
719
720
        if (is_null($ifMatch) && is_null($ifNoneMatch)) {
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
721
            // No request eTag header, we need to write the response
722
            // and eTag header
723
        } elseif (0 === strcmp($ifMatch, '*')) {
0 ignored issues
show
Unused Code introduced by
This elseif statement is empty, and could be removed.

This check looks for the bodies of elseif statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These elseif bodies can be removed. If you have an empty elseif but statements in the else branch, consider inverting the condition.

Loading history...
724
            // If-Match:* => we need to write the response and eTag header
725
        } elseif (0 === strcmp($ifNoneMatch, '*')) {
726
            // if-None-Match:* => Do not write the response (304 not modified),
727
            // but write eTag header
728
            $needToSerializeResponse = false;
729
        } else {
730
            $eTag = $this->getETagForEntry($entryObject, $resourceType);
731
            // Note: The following code for attaching the prefix W\"
732
            // and the suffix " can be done in getETagForEntry function
733
            // but that is causing an issue in Linux env where the
734
            // firefix browser is unable to parse the ETag in this case.
735
            // Need to follow up PHP core devs for this.
736
            $eTag = ODataConstants::HTTP_WEAK_ETAG_PREFIX . $eTag . '"';
737
            if (!is_null($ifMatch)) {
738
                if (0 != strcmp($eTag, $ifMatch)) {
739
                    // Requested If-Match value does not match with current
740
                    // eTag Value then pre-condition error
741
                    // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
742
                    throw ODataException::createPreConditionFailedError(
743
                        Messages::eTagValueDoesNotMatch()
744
                    );
745
                }
746
            } elseif (0 === strcmp($eTag, $ifNoneMatch)) {
747
                //304 not modified, but in write eTag header
748
                $needToSerializeResponse = false;
749
            }
750
        }
751
752
        if (is_null($eTag)) {
753
            $eTag = $this->getETagForEntry($entryObject, $resourceType);
754
            // Note: The following code for attaching the prefix W\"
755
            // and the suffix " can be done in getETagForEntry function
756
            // but that is causing an issue in Linux env where the
757
            // firefix browser is unable to parse the ETag in this case.
758
            // Need to follow up PHP core devs for this.
759
            $eTag = ODataConstants::HTTP_WEAK_ETAG_PREFIX . $eTag . '"';
760
        }
761
762
        return $eTag;
763
    }
764
765
    /**
766
     * Returns the etag for the given resource.
767
     * Note: This function will not add W\" prefix and " suffix, its callers
768
     * repsonsibility.
769
     *
770
     * @param mixed        &$entryObject  Resource for which etag value needs to
771
     *                                    be returned
772
     * @param ResourceType &$resourceType Resource type of the $entryObject
773
     *
774
     * @return string|null ETag value for the given resource (with values encoded
775
     *                     for use in a URI) there are etag properties, NULL if
776
     *                     there is no etag property
777
     */
778
    protected function getETagForEntry(&$entryObject, ResourceType & $resourceType)
779
    {
780
        $eTag = null;
781
        $comma = null;
782
        foreach ($resourceType->getETagProperties() as $eTagProperty) {
783
            $type = $eTagProperty->getInstanceType();
784
            assert($type instanceof IType, '!$type instanceof IType');
785
786
            $value = null;
787
            $property = $eTagProperty->getName();
788
            try {
789
                //TODO #88...also this seems like dupe work
790
                $value = ReflectionHandler::getProperty($entryObject, $property);
791
            } catch (\ReflectionException $reflectionException) {
792
                throw ODataException::createInternalServerError(
793
                    Messages::failedToAccessProperty($property, $resourceType->getName())
794
                );
795
            }
796
797
            $eTagBase = $eTag . $comma;
798
            $eTag = $eTagBase . ((null == $value) ? 'null' : $type->convertToOData($value));
799
800
            $comma = ',';
801
        }
802
803 View Code Duplication
        if (!is_null($eTag)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
804
            // If eTag is made up of datetime or string properties then the above
805
            // IType::convertToOData will perform utf8 and url encode. But we don't
806
            // want this for eTag value.
807
            $eTag = urldecode(utf8_decode($eTag));
808
809
            return rtrim($eTag, ',');
810
        }
811
        return null;
812
    }
813
}
814