Passed
Branch master (950424)
by Christopher
11:06
created

BaseService::getOperationContext()   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 0
dl 0
loc 3
rs 10
c 0
b 0
f 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
     * @return IObjectSerialiser
103
     */
104
    public function getObjectSerialiser()
105
    {
106
        assert(null != $this->objectSerialiser);
0 ignored issues
show
Bug introduced by
The call to assert() has too few arguments starting with description. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

106
        /** @scrutinizer ignore-call */ 
107
        assert(null != $this->objectSerialiser);

This check compares calls to functions or methods with their respective definitions. If the call has less 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. Please note the @ignore annotation hint above.

Loading history...
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);
0 ignored issues
show
Bug introduced by
The call to assert() has too few arguments starting with description. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

130
        /** @scrutinizer ignore-call */ 
131
        assert(null != $this->config);

This check compares calls to functions or methods with their respective definitions. If the call has less 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. Please note the @ignore annotation hint above.

Loading history...
131
132
        return $this->config;
133
    }
134
135
    //TODO: shouldn't we hide this from the interface..if we need it at all.
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
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);
0 ignored issues
show
Bug introduced by
The call to assert() has too few arguments starting with description. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

164
        /** @scrutinizer ignore-call */ 
165
        assert(null != $this->serviceHost);

This check compares calls to functions or methods with their respective definitions. If the call has less 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. Please note the @ignore annotation hint above.

Loading history...
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 (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
249
            $uriProcessor = UriProcessorNew::process($this);
250
            $request = $uriProcessor->getRequest();
251
            $this->serializeResult($request, $uriProcessor);
252
        } catch (\Exception $exception) {
253
            ErrorHandler::handleException($exception, $this);
254
            // Return to dispatcher for writing serialized exception
255
            return;
256
        }
257
    }
258
259
    /**
260
     * @return IQueryProvider
261
     */
262
    abstract public function getQueryProvider();
263
264
    /**
265
     * @return IMetadataProvider
266
     */
267
    abstract public function getMetadataProvider();
268
269
    /**
270
     *  @return \POData\Providers\Stream\IStreamProvider2
271
     */
272
    abstract public function getStreamProviderX();
273
274
    /** @var ODataWriterRegistry */
275
    protected $writerRegistry;
276
277
    /**
278
     * Returns the ODataWriterRegistry to use when writing the response to a service document or resource request.
279
     *
280
     * @return ODataWriterRegistry
281
     */
282
    public function getODataWriterRegistry()
283
    {
284
        assert(null != $this->writerRegistry);
0 ignored issues
show
Bug introduced by
The call to assert() has too few arguments starting with description. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

284
        /** @scrutinizer ignore-call */ 
285
        assert(null != $this->writerRegistry);

This check compares calls to functions or methods with their respective definitions. If the call has less 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. Please note the @ignore annotation hint above.

Loading history...
285
286
        return $this->writerRegistry;
287
    }
288
289
    /**
290
     * This method will query and validates for IMetadataProvider and IQueryProvider implementations, invokes
291
     * BaseService::Initialize to initialize service specific policies.
292
     *
293
     * @throws ODataException
294
     */
295
    protected function createProviders()
296
    {
297
        $metadataProvider = $this->getMetadataProvider();
298
        if (null === $metadataProvider) {
299
            throw ODataException::createInternalServerError(Messages::providersWrapperNull());
300
        }
301
302
        if (!$metadataProvider instanceof IMetadataProvider) {
303
            throw ODataException::createInternalServerError(Messages::invalidMetadataInstance());
304
        }
305
306
        $queryProvider = $this->getQueryProvider();
307
308
        if (null === $queryProvider) {
309
            throw ODataException::createInternalServerError(Messages::providersWrapperNull());
310
        }
311
312
        if (!$queryProvider instanceof IQueryProvider) {
313
            throw ODataException::createInternalServerError(Messages::invalidQueryInstance());
314
        }
315
316
        $this->config = new ServiceConfiguration($metadataProvider);
317
        $this->providersWrapper = new ProvidersWrapper(
318
            $metadataProvider,
319
            $queryProvider,
320
            $this->config
321
        );
322
323
        $this->initialize($this->config);
324
325
        //TODO: this seems like a bad spot to do this
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
326
        $this->writerRegistry = new ODataWriterRegistry();
327
        $this->registerWriters();
328
    }
329
330
    //TODO: i don't want this to be public..but it's the only way to test it right now...
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
331
    public function registerWriters()
332
    {
333
        $registry = $this->getODataWriterRegistry();
334
        $serviceVersion = $this->getConfiguration()->getMaxDataServiceVersion();
335
        $serviceURI = $this->getHost()->getAbsoluteServiceUri()->getUrlAsString();
336
337
        //We always register the v1 stuff
338
        $registry->register(new JsonODataV1Writer());
339
        $registry->register(new AtomODataWriter($serviceURI));
340
341
        if (-1 < $serviceVersion->compare(Version::v2())) {
342
            $registry->register(new JsonODataV2Writer());
343
        }
344
345
        if (-1 < $serviceVersion->compare(Version::v3())) {
346
            $registry->register(new JsonLightODataWriter(JsonLightMetadataLevel::NONE(), $serviceURI));
347
            $registry->register(new JsonLightODataWriter(JsonLightMetadataLevel::MINIMAL(), $serviceURI));
348
            $registry->register(new JsonLightODataWriter(JsonLightMetadataLevel::FULL(), $serviceURI));
349
        }
350
    }
351
352
    /**
353
     * Serialize the requested resource.
354
     *
355
     * @param RequestDescription $request      The description of the request  submitted by the client
356
     * @param IUriProcessor      $uriProcessor Reference to the uri processor
357
     *
358
     * @throws ODataException
359
     */
360
    protected function serializeResult(RequestDescription $request, IUriProcessor $uriProcessor)
361
    {
362
        $isETagHeaderAllowed = $request->isETagHeaderAllowed();
363
364
        if ($this->getConfiguration()->getValidateETagHeader() && !$isETagHeaderAllowed) {
365
            if (null !== $this->getHost()->getRequestIfMatch()
366
                || null !== $this->getHost()->getRequestIfNoneMatch()
367
            ) {
368
                throw ODataException::createBadRequestError(
369
                    Messages::eTagCannotBeSpecified($this->getHost()->getAbsoluteRequestUri()->getUrlAsString())
370
                );
371
            }
372
        }
373
374
        $responseContentType = $this->getResponseContentType($request, $uriProcessor);
375
376
        if (null === $responseContentType && $request->getTargetKind() != TargetKind::MEDIA_RESOURCE()) {
377
            //the responseContentType can ONLY be null if it's a stream (media resource) and
378
            // that stream is storing null as the content type
379
            throw new ODataException(Messages::unsupportedMediaType(), 415);
380
        }
381
382
        $odataModelInstance = null;
383
        $hasResponseBody = true;
384
        // Execution required at this point if request points to any resource other than
385
386
        // (1) media resource - For Media resource 'getResponseContentType' already performed execution as
387
        // it needs to know the mime type of the stream
388
        // (2) metadata - internal resource
389
        // (3) service directory - internal resource
390
        if ($request->needExecution()) {
391
            $method = $this->getHost()->getOperationContext()->incomingRequest()->getMethod();
392
            $uriProcessor->execute();
393
            if (HTTPRequestMethod::DELETE() == $method) {
394
                $this->getHost()->setResponseStatusCode(HttpStatus::CODE_NOCONTENT);
395
396
                return;
397
            }
398
399
            $objectModelSerializer = $this->getObjectSerialiser();
400
            $objectModelSerializer->setRequest($request);
401
402
            $targetResourceType = $request->getTargetResourceType();
403
            assert(null != $targetResourceType, 'Target resource type cannot be null');
404
405
            $methodIsNotPost = (HTTPRequestMethod::POST() != $method);
406
            $methodIsNotDelete = (HTTPRequestMethod::DELETE() != $method);
407
            if (!$request->isSingleResult() && $methodIsNotPost) {
408
                // Code path for collection (feed or links)
409
                $entryObjects = $request->getTargetResult();
410
                assert($entryObjects instanceof QueryResult, '!$entryObjects instanceof QueryResult');
411
                assert(is_array($entryObjects->results), '!is_array($entryObjects->results)');
412
                // If related resource set is empty for an entry then we should
413
                // not throw error instead response must be empty feed or empty links
414
                if ($request->isLinkUri()) {
415
                    $odataModelInstance = $objectModelSerializer->writeUrlElements($entryObjects);
416
                    assert(
417
                        $odataModelInstance instanceof ODataURLCollection,
418
                        '!$odataModelInstance instanceof ODataURLCollection'
419
                    );
420
                } else {
421
                    $odataModelInstance = $objectModelSerializer->writeTopLevelElements($entryObjects);
422
                    assert($odataModelInstance instanceof ODataFeed, '!$odataModelInstance instanceof ODataFeed');
423
                }
424
            } else {
425
                // Code path for entity, complex, bag, resource reference link,
426
                // primitive type or primitive value
427
                $result = $request->getTargetResult();
428
                if (!$result instanceof QueryResult) {
429
                    $result = new QueryResult();
430
                    $result->results = $request->getTargetResult();
431
                }
432
                $requestTargetKind = $request->getTargetKind();
433
                $requestProperty = $request->getProjectedProperty();
434
                if ($request->isLinkUri()) {
435
                    // In the query 'Orders(1245)/$links/Customer', the targeted
436
                    // Customer might be null
437
                    if (null === $result->results && $methodIsNotPost && $methodIsNotDelete) {
438
                        throw ODataException::createResourceNotFoundError($request->getIdentifier());
439
                    }
440
                    if ($methodIsNotPost && $methodIsNotDelete) {
441
                        $odataModelInstance = $objectModelSerializer->writeUrlElement($result);
442
                    }
443
                } elseif (TargetKind::RESOURCE() == $requestTargetKind
444
                          || TargetKind::SINGLETON() == $requestTargetKind) {
445
                    if (null !== $this->getHost()->getRequestIfMatch()
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->getHost()->getRequestIfMatch() targeting POData\OperationContext\...st::getRequestIfMatch() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
446
                        && null !== $this->getHost()->getRequestIfNoneMatch()
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->getHost()->getRequestIfNoneMatch() targeting POData\OperationContext\...getRequestIfNoneMatch() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
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 (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 (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 POData\ObjectModel\IObje...riteTopLevelBagObject() has too many arguments starting with $odataModelInstance. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

490
                    /** @scrutinizer ignore-call */ 
491
                    $odataModelInstance = $objectModelSerializer->writeTopLevelBagObject(

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. Please note the @ignore annotation hint above.

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 POData\ObjectModel\IObje...riteTopLevelPrimitive() has too many arguments starting with $odataModelInstance. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

496
                    /** @scrutinizer ignore-call */ 
497
                    $odataModelInstance = $objectModelSerializer->writeTopLevelPrimitive(

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. Please note the @ignore annotation hint above.

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 primitive 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 && 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|null 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 (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?
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
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
        assert(is_string($requestAcceptText) || !isset($requestAcceptText));
0 ignored issues
show
Bug introduced by
The call to assert() has too few arguments starting with description. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

572
        /** @scrutinizer ignore-call */ 
573
        assert(is_string($requestAcceptText) || !isset($requestAcceptText));

This check compares calls to functions or methods with their respective definitions. If the call has less 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. Please note the @ignore annotation hint above.

Loading history...
573
574
        switch ($targetKind) {
575
            case TargetKind::METADATA():
576
                return HttpProcessUtility::selectMimeType(
577
                    $requestAcceptText,
578
                    [MimeTypes::MIME_APPLICATION_XML]
579
                );
580
581
            case TargetKind::SERVICE_DIRECTORY():
582
                return HttpProcessUtility::selectMimeType(
583
                    $requestAcceptText,
584
                    array_merge(
585
                        [MimeTypes::MIME_APPLICATION_ATOMSERVICE],
586
                        $baseMimeTypes
587
                    )
588
                );
589
590
            case TargetKind::PRIMITIVE_VALUE():
591
                $supportedResponseMimeTypes = [MimeTypes::MIME_TEXTPLAIN];
592
593
                if ('$count' != $request->getIdentifier()) {
594
                    $projectedProperty = $request->getProjectedProperty();
595
                    assert(null !== $projectedProperty, 'is_null($projectedProperty)');
596
                    $type = $projectedProperty->getInstanceType();
597
                    assert($type instanceof IType, '!$type instanceof IType');
598
                    if ($type instanceof Binary) {
599
                        $supportedResponseMimeTypes = [MimeTypes::MIME_APPLICATION_OCTETSTREAM];
600
                    }
601
                }
602
603
                return HttpProcessUtility::selectMimeType(
604
                    $requestAcceptText,
605
                    $supportedResponseMimeTypes
606
                );
607
608
            case TargetKind::PRIMITIVE():
609
            case TargetKind::COMPLEX_OBJECT():
610
            case TargetKind::BAG():
611
            case TargetKind::LINK():
612
                return HttpProcessUtility::selectMimeType(
613
                    $requestAcceptText,
614
                    array_merge(
615
                        [MimeTypes::MIME_APPLICATION_XML,
616
                            MimeTypes::MIME_TEXTXML, ],
617
                        $baseMimeTypes
618
                    )
619
                );
620
621
            case TargetKind::SINGLETON():
622
            case TargetKind::RESOURCE():
623
                return HttpProcessUtility::selectMimeType(
624
                    $requestAcceptText,
625
                    array_merge(
626
                        [MimeTypes::MIME_APPLICATION_ATOM],
627
                        $baseMimeTypes
628
                    )
629
                );
630
631
            case TargetKind::MEDIA_RESOURCE():
632
                if (!$request->isNamedStream() && !$request->getTargetResourceType()->isMediaLinkEntry()) {
633
                    throw ODataException::createBadRequestError(
634
                        Messages::badRequestInvalidUriForMediaResource(
635
                            $host->getAbsoluteRequestUri()->getUrlAsString()
636
                        )
637
                    );
638
                }
639
640
                $uriProcessor->execute();
641
                $request->setExecuted();
642
                // DSSW::getStreamContentType can throw error in 2 cases
643
                // 1. If the required stream implementation not found
644
                // 2. If IDSSP::getStreamContentType returns NULL for MLE
645
                $responseContentType = $this->getStreamProviderWrapper()
646
                    ->getStreamContentType(
647
                        $request->getTargetResult(),
648
                        $request->getResourceStreamInfo()
649
                    );
650
651
                // Note StreamWrapper::getStreamContentType can return NULL if the requested named stream has not
652
                // yet been uploaded. But for an MLE if IDSSP::getStreamContentType returns NULL
653
                // then StreamWrapper will throw error
654
                if (null !== $responseContentType) {
655
                    $responseContentType = HttpProcessUtility::selectMimeType(
656
                        $requestAcceptText,
657
                        [$responseContentType]
658
                    );
659
                }
660
661
                return $responseContentType;
662
        }
663
664
        //If we got here, we just don't know what it is...
665
        throw new ODataException(Messages::unsupportedMediaType(), 415);
666
    }
667
668
    /**
669
     * For the given entry object compare its eTag (if it has eTag properties)
670
     * with current eTag request headers (if present).
671
     *
672
     * @param mixed        &$entryObject             entity resource for which etag
673
     *                                               needs to be checked
674
     * @param ResourceType &$resourceType            Resource type of the entry
675
     *                                               object
676
     * @param bool         &$needToSerializeResponse On return, this will contain
677
     *                                               True if response needs to be
678
     *                                               serialized, False otherwise
679
     * @param bool         $needToSerializeResponse
680
     *
681
     * @throws ODataException
682
     * @return string|null    The ETag for the entry object if it has eTag properties
683
     *                        NULL otherwise
684
     */
685
    protected function compareETag(
686
        &$entryObject,
687
        ResourceType &$resourceType,
688
        &$needToSerializeResponse
689
    ) {
690
        $needToSerializeResponse = true;
691
        $eTag = null;
692
        $ifMatch = $this->getHost()->getRequestIfMatch();
693
        $ifNoneMatch = $this->getHost()->getRequestIfNoneMatch();
694
        if (null === $entryObject) {
695
            if (null !== $ifMatch) {
696
                throw ODataException::createPreConditionFailedError(
697
                    Messages::eTagNotAllowedForNonExistingResource()
698
                );
699
            }
700
701
            return null;
702
        }
703
704
        if ($this->getConfiguration()->getValidateETagHeader() && !$resourceType->hasETagProperties()) {
705
            if (null !== $ifMatch || null !== $ifNoneMatch) {
706
                // No eTag properties but request has eTag headers, bad request
707
                throw ODataException::createBadRequestError(
708
                    Messages::noETagPropertiesForType()
709
                );
710
            }
711
712
            // We need write the response but no eTag header
713
            return null;
714
        }
715
716
        if (!$this->getConfiguration()->getValidateETagHeader()) {
717
            // Configuration says do not validate ETag, so we will not write ETag header in the
718
            // response even though the requested resource support it
719
            return null;
720
        }
721
722
        if (null === $ifMatch && 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...
723
            // No request eTag header, we need to write the response
724
            // and eTag header
725
        } 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...
726
            // If-Match:* => we need to write the response and eTag header
727
        } elseif (0 === strcmp($ifNoneMatch, '*')) {
728
            // if-None-Match:* => Do not write the response (304 not modified),
729
            // but write eTag header
730
            $needToSerializeResponse = false;
731
        } else {
732
            $eTag = $this->getETagForEntry($entryObject, $resourceType);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $eTag is correct as $this->getETagForEntry($...yObject, $resourceType) targeting POData\BaseService::getETagForEntry() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
733
            // Note: The following code for attaching the prefix W\"
734
            // and the suffix " can be done in getETagForEntry function
735
            // but that is causing an issue in Linux env where the
736
            // firefox browser is unable to parse the ETag in this case.
737
            // Need to follow up PHP core devs for this.
738
            $eTag = ODataConstants::HTTP_WEAK_ETAG_PREFIX . $eTag . '"';
739
            if (null !== $ifMatch) {
740
                if (0 != strcmp($eTag, $ifMatch)) {
741
                    // Requested If-Match value does not match with current
742
                    // eTag Value then pre-condition error
743
                    // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
744
                    throw ODataException::createPreConditionFailedError(
745
                        Messages::eTagValueDoesNotMatch()
746
                    );
747
                }
748
            } elseif (0 === strcmp($eTag, $ifNoneMatch)) {
749
                //304 not modified, but in write eTag header
750
                $needToSerializeResponse = false;
751
            }
752
        }
753
754
        if (null === $eTag) {
755
            $eTag = $this->getETagForEntry($entryObject, $resourceType);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $eTag is correct as $this->getETagForEntry($...yObject, $resourceType) targeting POData\BaseService::getETagForEntry() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
756
            // Note: The following code for attaching the prefix W\"
757
            // and the suffix " can be done in getETagForEntry function
758
            // but that is causing an issue in Linux env where the
759
            // firefox browser is unable to parse the ETag in this case.
760
            // Need to follow up PHP core devs for this.
761
            $eTag = ODataConstants::HTTP_WEAK_ETAG_PREFIX . $eTag . '"';
762
        }
763
764
        return $eTag;
765
    }
766
767
    /**
768
     * Returns the etag for the given resource.
769
     * Note: This function will not add W\" prefix and " suffix, that is caller's
770
     * responsibility.
771
     *
772
     * @param mixed        &$entryObject  Resource for which etag value needs to
773
     *                                    be returned
774
     * @param ResourceType &$resourceType Resource type of the $entryObject
775
     *
776
     * @throws ODataException
777
     * @return string|null    ETag value for the given resource (with values encoded
778
     *                        for use in a URI) there are etag properties, NULL if
779
     *                        there is no etag property
780
     */
781
    protected function getETagForEntry(&$entryObject, ResourceType &$resourceType)
782
    {
783
        $eTag = null;
784
        $comma = null;
785
        foreach ($resourceType->getETagProperties() as $eTagProperty) {
786
            $type = $eTagProperty->getInstanceType();
787
            assert($type instanceof IType, '!$type instanceof IType');
788
789
            $value = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $value is dead and can be removed.
Loading history...
790
            $property = $eTagProperty->getName();
791
            try {
792
                //TODO #88...also this seems like dupe work
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
793
                $value = ReflectionHandler::getProperty($entryObject, $property);
794
            } catch (\ReflectionException $reflectionException) {
795
                throw ODataException::createInternalServerError(
796
                    Messages::failedToAccessProperty($property, $resourceType->getName())
797
                );
798
            }
799
800
            $eTagBase = $eTag . $comma;
801
            $eTag = $eTagBase . ((null == $value) ? 'null' : $type->convertToOData($value));
0 ignored issues
show
Bug introduced by
The method convertToOData() does not exist on ReflectionClass. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

801
            $eTag = $eTagBase . ((null == $value) ? 'null' : $type->/** @scrutinizer ignore-call */ convertToOData($value));

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

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