Completed
Push — master ( baf356...7843c7 )
by Alex
07:30 queued 03:24
created

BaseService::getObjectSerialiser()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 5
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace POData;
4
5
use POData\BatchProcessor\BatchProcessor;
6
use POData\Common\ErrorHandler;
7
use POData\Common\HttpStatus;
8
use POData\Common\Messages;
9
use POData\Common\MimeTypes;
10
use POData\Common\NotImplementedException;
11
use POData\Common\ODataConstants;
12
use POData\Common\ODataException;
13
use POData\Common\ReflectionHandler;
14
use POData\Common\Version;
15
use POData\Configuration\IServiceConfiguration;
16
use POData\Configuration\ServiceConfiguration;
17
use POData\ObjectModel\IObjectSerialiser;
18
use POData\ObjectModel\ObjectModelSerializer;
19
use POData\ObjectModel\ODataFeed;
20
use POData\ObjectModel\ODataURLCollection;
21
use POData\OperationContext\HTTPRequestMethod;
22
use POData\OperationContext\IOperationContext;
23
use POData\OperationContext\ServiceHost;
24
use POData\Providers\Metadata\IMetadataProvider;
25
use POData\Providers\Metadata\ResourceType;
26
use POData\Providers\Metadata\Type\Binary;
27
use POData\Providers\Metadata\Type\IType;
28
use POData\Providers\ProvidersWrapper;
29
use POData\Providers\Query\IQueryProvider;
30
use POData\Providers\Query\QueryResult;
31
use POData\Providers\Stream\StreamProviderWrapper;
32
use POData\UriProcessor\Interfaces\IUriProcessor;
33
use POData\UriProcessor\RequestDescription;
34
use POData\UriProcessor\ResourcePathProcessor\SegmentParser\TargetKind;
35
use POData\UriProcessor\UriProcessor;
36
use POData\UriProcessor\UriProcessorNew;
37
use POData\Writers\Atom\AtomODataWriter;
38
use POData\Writers\Json\JsonLightMetadataLevel;
39
use POData\Writers\Json\JsonLightODataWriter;
40
use POData\Writers\Json\JsonODataV1Writer;
41
use POData\Writers\Json\JsonODataV2Writer;
42
use POData\Writers\ODataWriterRegistry;
43
use POData\Writers\ResponseWriter;
44
45
/**
46
 * Class BaseService.
47
 *
48
 * The base class for all BaseService specific classes. This class implements
49
 * the following interfaces:
50
 *  (1) IRequestHandler
51
 *      Implementing this interface requires defining the function
52
 *      'handleRequest' that will be invoked by dispatcher
53
 *  (2) IService
54
 *      Force BaseService class to implement functions for custom
55
 *      data service providers
56
 */
57
abstract class BaseService implements IRequestHandler, IService
58
{
59
    /**
60
     * The wrapper over IQueryProvider and IMetadataProvider implementations.
61
     *
62
     * @var ProvidersWrapper
63
     */
64
    private $providersWrapper;
65
66
    /**
67
     * The wrapper over IStreamProvider implementation.
68
     *
69
     * @var StreamProviderWrapper
70
     */
71
    protected $streamProvider;
72
73
    /**
74
     * Hold reference to the ServiceHost instance created by dispatcher,
75
     * using this library can access headers and body of Http Request
76
     * dispatcher received and the Http Response Dispatcher is going to send.
77
     *
78
     * @var ServiceHost
79
     */
80
    private $serviceHost;
81
82
    /**
83
     * To hold reference to ServiceConfiguration instance where the
84
     * service specific rules (page limit, resource set access rights
85
     * etc...) are defined.
86
     *
87
     * @var IServiceConfiguration
88
     */
89
    protected $config;
90
91
    /**
92
     * Hold reference to object serialiser - bit wot turns PHP objects
93
     * into message traffic on wire.
94
     *
95
     * @var IObjectSerialiser
96
     */
97
    protected $objectSerialiser;
98
99
    /**
100
     * Get reference to object serialiser - bit wot turns PHP objects
101
     * into message traffic on wire.
102
     *
103
     * @return IObjectSerialiser
104
     */
105
    public function getObjectSerialiser()
106
    {
107
        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

107
        /** @scrutinizer ignore-call */ 
108
        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...
108
109
        return $this->objectSerialiser;
110
    }
111
112
    protected function __construct(IObjectSerialiser $serialiser = null)
113
    {
114
        if (null != $serialiser) {
115
            $serialiser->setService($this);
116
        } else {
117
            $serialiser = new ObjectModelSerializer($this, null);
118
        }
119
        $this->objectSerialiser = $serialiser;
120
    }
121
122
    /**
123
     * Gets reference to ServiceConfiguration instance so that
124
     * service specific rules defined by the developer can be
125
     * accessed.
126
     *
127
     * @return IServiceConfiguration
128
     */
129
    public function getConfiguration()
130
    {
131
        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

131
        /** @scrutinizer ignore-call */ 
132
        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...
132
133
        return $this->config;
134
    }
135
136
    //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...
137
138
    /**
139
     * Get the wrapper over developer's IQueryProvider and IMetadataProvider implementation.
140
     *
141
     * @return ProvidersWrapper
142
     */
143
    public function getProvidersWrapper()
144
    {
145
        return $this->providersWrapper;
146
    }
147
148
    /**
149
     * Gets reference to wrapper class instance over IDSSP implementation.
150
     *
151
     * @return StreamProviderWrapper
152
     */
153
    public function getStreamProviderWrapper()
154
    {
155
        return $this->streamProvider;
156
    }
157
158
    /**
159
     * Get reference to the data service host instance.
160
     *
161
     * @return ServiceHost
162
     */
163
    public function getHost()
164
    {
165
        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

165
        /** @scrutinizer ignore-call */ 
166
        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...
166
167
        return $this->serviceHost;
168
    }
169
170
    /**
171
     * Sets the data service host instance.
172
     *
173
     * @param ServiceHost $serviceHost The data service host instance
174
     */
175
    public function setHost(ServiceHost $serviceHost)
176
    {
177
        $this->serviceHost = $serviceHost;
178
    }
179
180
    /**
181
     * To get reference to operation context where we have direct access to
182
     * headers and body of Http Request, we have received and the Http Response
183
     * We are going to send.
184
     *
185
     * @return IOperationContext
186
     */
187
    public function getOperationContext()
188
    {
189
        return $this->getHost()->getOperationContext();
190
    }
191
192
    /**
193
     * Get reference to the wrapper over IStreamProvider or
194
     * IStreamProvider2 implementations.
195
     *
196
     * @return StreamProviderWrapper
197
     */
198
    public function getStreamProvider()
199
    {
200
        if (null === $this->streamProvider) {
201
            $this->streamProvider = new StreamProviderWrapper();
202
            $this->streamProvider->setService($this);
203
        }
204
205
        return $this->streamProvider;
206
    }
207
208
    /**
209
     * Top-level handler invoked by Dispatcher against any request to this
210
     * service. This method will hand over request processing task to other
211
     * functions which process the request, set required headers and Response
212
     * stream (if any in Atom/Json format) in
213
     * WebOperationContext::Current()::OutgoingWebResponseContext.
214
     * Once this function returns, dispatcher uses global WebOperationContext
215
     * to write out the request response to client.
216
     * This function will perform the following operations:
217
     * (1) Check whether the top level service class implements
218
     *     IServiceProvider which means the service is a custom service, in
219
     *     this case make sure the top level service class implements
220
     *     IMetaDataProvider and IQueryProvider.
221
     *     These are the minimal interfaces that a custom service to be
222
     *     implemented in order to expose its data as OData. Save reference to
223
     *     These interface implementations.
224
     *     NOTE: Here we will ensure only providers for IDSQP and IDSMP. The
225
     *     IDSSP will be ensured only when there is an GET request on MLE/Named
226
     *     stream.
227
     *
228
     * (2). Invoke 'Initialize' method of top level service for
229
     *      collecting the configuration rules set by the developer for this
230
     *      service.
231
     *
232
     * (3). Invoke the Uri processor to process the request URI. The uri
233
     *      processor will do the following:
234
     *      (a). Validate the request uri syntax using OData uri rules
235
     *      (b). Validate the request using metadata of this service
236
     *      (c). Parse the request uri and using, IQueryProvider
237
     *           implementation, fetches the resources pointed by the uri
238
     *           if required
239
     *      (d). Build a RequestDescription which encapsulate everything
240
     *           related to request uri (e.g. type of resource, result
241
     *           etc...)
242
     * (3). Invoke handleRequest2 for further processing
243
     */
244
    public function handleRequest()
245
    {
246
        try {
247
            $this->createProviders();
248
            $this->getHost()->validateQueryParameters();
249
            $uriProcessor = UriProcessorNew::process($this);
250
            $request = $uriProcessor->getRequest();
251
            if (TargetKind::BATCH() == $request->getTargetKind()) {
252
                //dd($request);
253
                $this->handleBatchRequest($request);
254
            } else {
255
                $this->serializeResult($request, $uriProcessor);
256
            }
257
        } catch (\Exception $exception) {
258
            ErrorHandler::handleException($exception, $this);
259
            // Return to dispatcher for writing serialized exception
260
            return;
261
        }
262
    }
263
264
    private function handleBatchRequest($request)
265
    {
266
        $cloneThis = clone $this;
267
        $batchProcesser = new BatchProcessor($cloneThis, $request);
268
        $batchProcesser->handleBatch();
269
        $response = $batchProcesser->getResponse();
270
        $this->getHost()->setResponseStatusCode(HttpStatus::CODE_ACCEPTED);
271
        $this->getHost()->setResponseContentType('Content-Type: multipart/mixed; boundary=' .
272
            $batchProcesser->getBoundary());
273
        // Hack: this needs to be sorted out in the future as we hookup other versions.
274
        $this->getHost()->setResponseVersion('3.0;');
275
        $this->getHost()->setResponseCacheControl(ODataConstants::HTTPRESPONSE_HEADER_CACHECONTROL_NOCACHE);
276
        $this->getHost()->getOperationContext()->outgoingResponse()->setStream($response);
277
    }
278
279
    /**
280
     * @return IQueryProvider
281
     */
282
    abstract public function getQueryProvider();
283
284
    /**
285
     * @return IMetadataProvider
286
     */
287
    abstract public function getMetadataProvider();
288
289
    /**
290
     *  @return \POData\Providers\Stream\IStreamProvider2
291
     */
292
    abstract public function getStreamProviderX();
293
294
    /** @var ODataWriterRegistry */
295
    protected $writerRegistry;
296
297
    /**
298
     * Returns the ODataWriterRegistry to use when writing the response to a service document or resource request.
299
     *
300
     * @return ODataWriterRegistry
301
     */
302
    public function getODataWriterRegistry()
303
    {
304
        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

304
        /** @scrutinizer ignore-call */ 
305
        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...
305
306
        return $this->writerRegistry;
307
    }
308
309
    /**
310
     * This method will query and validates for IMetadataProvider and IQueryProvider implementations, invokes
311
     * BaseService::Initialize to initialize service specific policies.
312
     *
313
     * @throws ODataException
314
     */
315
    protected function createProviders()
316
    {
317
        $metadataProvider = $this->getMetadataProvider();
318
        if (null === $metadataProvider) {
319
            throw ODataException::createInternalServerError(Messages::providersWrapperNull());
320
        }
321
322
        if (!$metadataProvider instanceof IMetadataProvider) {
323
            throw ODataException::createInternalServerError(Messages::invalidMetadataInstance());
324
        }
325
326
        $queryProvider = $this->getQueryProvider();
327
328
        if (null === $queryProvider) {
329
            throw ODataException::createInternalServerError(Messages::providersWrapperNull());
330
        }
331
332
        if (!$queryProvider instanceof IQueryProvider) {
333
            throw ODataException::createInternalServerError(Messages::invalidQueryInstance());
334
        }
335
336
        $this->config = new ServiceConfiguration($metadataProvider);
337
        $this->providersWrapper = new ProvidersWrapper(
338
            $metadataProvider,
339
            $queryProvider,
340
            $this->config
341
        );
342
343
        $this->initialize($this->config);
344
345
        //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...
346
        $this->writerRegistry = new ODataWriterRegistry();
347
        $this->registerWriters();
348
    }
349
350
    //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...
351
    public function registerWriters()
352
    {
353
        $registry = $this->getODataWriterRegistry();
354
        $serviceVersion = $this->getConfiguration()->getMaxDataServiceVersion();
355
        $serviceURI = $this->getHost()->getAbsoluteServiceUri()->getUrlAsString();
356
357
        //We always register the v1 stuff
358
        $registry->register(new JsonODataV1Writer());
359
        $registry->register(new AtomODataWriter($serviceURI));
360
361
        if (-1 < $serviceVersion->compare(Version::v2())) {
362
            $registry->register(new JsonODataV2Writer());
363
        }
364
365
        if (-1 < $serviceVersion->compare(Version::v3())) {
366
            $registry->register(new JsonLightODataWriter(JsonLightMetadataLevel::NONE(), $serviceURI));
367
            $registry->register(new JsonLightODataWriter(JsonLightMetadataLevel::MINIMAL(), $serviceURI));
368
            $registry->register(new JsonLightODataWriter(JsonLightMetadataLevel::FULL(), $serviceURI));
369
        }
370
    }
371
372
    /**
373
     * Serialize the requested resource.
374
     *
375
     * @param RequestDescription $request      The description of the request  submitted by the client
376
     * @param IUriProcessor      $uriProcessor Reference to the uri processor
377
     *
378
     * @throws ODataException
379
     */
380
    protected function serializeResult(RequestDescription $request, IUriProcessor $uriProcessor)
381
    {
382
        $isETagHeaderAllowed = $request->isETagHeaderAllowed();
383
384
        if ($this->getConfiguration()->getValidateETagHeader() && !$isETagHeaderAllowed) {
385
            if (null !== $this->getHost()->getRequestIfMatch()
386
                || null !== $this->getHost()->getRequestIfNoneMatch()
387
            ) {
388
                throw ODataException::createBadRequestError(
389
                    Messages::eTagCannotBeSpecified($this->getHost()->getAbsoluteRequestUri()->getUrlAsString())
390
                );
391
            }
392
        }
393
394
        $responseContentType = $this->getResponseContentType($request, $uriProcessor);
395
396
        if (null === $responseContentType && $request->getTargetKind() != TargetKind::MEDIA_RESOURCE()) {
397
            //the responseContentType can ONLY be null if it's a stream (media resource) and
398
            // that stream is storing null as the content type
399
            throw new ODataException(Messages::unsupportedMediaType(), 415);
400
        }
401
402
        $odataModelInstance = null;
403
        $hasResponseBody = true;
404
        // Execution required at this point if request points to any resource other than
405
406
        // (1) media resource - For Media resource 'getResponseContentType' already performed execution as
407
        // it needs to know the mime type of the stream
408
        // (2) metadata - internal resource
409
        // (3) service directory - internal resource
410
        if ($request->needExecution()) {
411
            $method = $this->getHost()->getOperationContext()->incomingRequest()->getMethod();
412
            $uriProcessor->execute();
413
            if (HTTPRequestMethod::DELETE() == $method) {
414
                $this->getHost()->setResponseStatusCode(HttpStatus::CODE_NOCONTENT);
415
416
                return;
417
            }
418
419
            $objectModelSerializer = $this->getObjectSerialiser();
420
            $objectModelSerializer->setRequest($request);
421
422
            $targetResourceType = $request->getTargetResourceType();
423
            assert(null != $targetResourceType, 'Target resource type cannot be null');
424
425
            $methodIsNotPost = (HTTPRequestMethod::POST() != $method);
426
            $methodIsNotDelete = (HTTPRequestMethod::DELETE() != $method);
427
            if (!$request->isSingleResult() && $methodIsNotPost) {
428
                // Code path for collection (feed or links)
429
                $entryObjects = $request->getTargetResult();
430
                assert($entryObjects instanceof QueryResult, '!$entryObjects instanceof QueryResult');
431
                assert(is_array($entryObjects->results), '!is_array($entryObjects->results)');
432
                // If related resource set is empty for an entry then we should
433
                // not throw error instead response must be empty feed or empty links
434
                if ($request->isLinkUri()) {
435
                    $odataModelInstance = $objectModelSerializer->writeUrlElements($entryObjects);
436
                    assert(
437
                        $odataModelInstance instanceof ODataURLCollection,
438
                        '!$odataModelInstance instanceof ODataURLCollection'
439
                    );
440
                } else {
441
                    $odataModelInstance = $objectModelSerializer->writeTopLevelElements($entryObjects);
442
                    assert($odataModelInstance instanceof ODataFeed, '!$odataModelInstance instanceof ODataFeed');
443
                }
444
            } else {
445
                // Code path for entity, complex, bag, resource reference link,
446
                // primitive type or primitive value
447
                $result = $request->getTargetResult();
448
                if (!$result instanceof QueryResult) {
449
                    $result = new QueryResult();
450
                    $result->results = $request->getTargetResult();
451
                }
452
                $requestTargetKind = $request->getTargetKind();
453
                $requestProperty = $request->getProjectedProperty();
454
                if ($request->isLinkUri()) {
455
                    // In the query 'Orders(1245)/$links/Customer', the targeted
456
                    // Customer might be null
457
                    if (null === $result->results && $methodIsNotPost && $methodIsNotDelete) {
458
                        throw ODataException::createResourceNotFoundError($request->getIdentifier());
459
                    }
460
                    if ($methodIsNotPost && $methodIsNotDelete) {
461
                        $odataModelInstance = $objectModelSerializer->writeUrlElement($result);
462
                    }
463
                } elseif (TargetKind::RESOURCE() == $requestTargetKind
464
                          || TargetKind::SINGLETON() == $requestTargetKind) {
465
                    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...
466
                        && 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...
467
                    ) {
468
                        throw ODataException::createBadRequestError(
469
                            Messages::bothIfMatchAndIfNoneMatchHeaderSpecified()
470
                        );
471
                    }
472
                    // handle entry resource
473
                    $needToSerializeResponse = true;
474
                    $eTag = $this->compareETag($result, $targetResourceType, $needToSerializeResponse);
475
476
                    if ($needToSerializeResponse) {
477
                        if (null === $result) {
478
                            // In the query 'Orders(1245)/Customer', the targeted
479
                            // Customer might be null
480
                            // set status code to 204 => 'No Content'
481
                            $this->getHost()->setResponseStatusCode(HttpStatus::CODE_NOCONTENT);
482
                            $hasResponseBody = false;
483
                        } else {
484
                            $odataModelInstance = $objectModelSerializer->writeTopLevelElement($result);
485
                        }
486
                    } else {
487
                        // Resource is not modified so set status code
488
                        // 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...
489
                        $this->getHost()->setResponseStatusCode(HttpStatus::CODE_NOT_MODIFIED);
490
                        $hasResponseBody = false;
491
                    }
492
493
                    // if resource has eTagProperty then eTag header needs to written
494
                    if (null !== $eTag) {
495
                        $this->getHost()->setResponseETag($eTag);
496
                    }
497
                } elseif (TargetKind::COMPLEX_OBJECT() == $requestTargetKind) {
498
                    assert(null != $requestProperty, 'Projected request property cannot be null');
499
                    $odataModelInstance = $objectModelSerializer->writeTopLevelComplexObject(
500
                        $result,
501
                        $requestProperty->getName(),
502
                        $targetResourceType
503
                    );
504
                } elseif (TargetKind::BAG() == $requestTargetKind) {
505
                    assert(null != $requestProperty, 'Projected request property cannot be null');
506
                    $odataModelInstance = $objectModelSerializer->writeTopLevelBagObject(
507
                        $result,
508
                        $requestProperty->getName(),
509
                        $targetResourceType,
510
                        $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

510
                    /** @scrutinizer ignore-call */ 
511
                    $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...
511
                    );
512
                } elseif (TargetKind::PRIMITIVE() == $requestTargetKind) {
513
                    $odataModelInstance = $objectModelSerializer->writeTopLevelPrimitive(
514
                        $result,
515
                        $requestProperty,
516
                        $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

516
                    /** @scrutinizer ignore-call */ 
517
                    $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...
517
                    );
518
                } 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...
519
                    // Code path for primitive value (Since its primitive no need for
520
                    // object model serialization)
521
                    // 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...
522
                    // Employees(1)/Photo/$value => binary stream
523
                    // Customers/$count => string
524
                } else {
525
                    assert(false, 'Unexpected resource target kind');
526
                }
527
            }
528
        }
529
530
        //Note: Response content type can be null for named stream
531
        if ($hasResponseBody && null !== $responseContentType) {
532
            if (TargetKind::MEDIA_RESOURCE() != $request->getTargetKind()
533
                && MimeTypes::MIME_APPLICATION_OCTETSTREAM != $responseContentType) {
534
                //append charset for everything except:
535
                //stream resources as they have their own content type
536
                //binary properties (they content type will be App Octet for those...is this a good way?
537
                //we could also decide based upon the projected property
538
539
                $responseContentType .= ';charset=utf-8';
540
            }
541
        }
542
543
        if ($hasResponseBody) {
544
            ResponseWriter::write($this, $request, $odataModelInstance, $responseContentType);
545
        }
546
    }
547
548
    /**
549
     * Gets the response format for the requested resource.
550
     *
551
     * @param RequestDescription $request      The request submitted by client and it's execution result
552
     * @param IUriProcessor      $uriProcessor The reference to the IUriProcessor
553
     *
554
     * @throws ODataException, HttpHeaderFailure
555
     *
556
     * @return string|null the response content-type, a null value means the requested resource
557
     *                     is named stream and IDSSP2::getStreamContentType returned null
558
     */
559
    public function getResponseContentType(
560
        RequestDescription $request,
561
        IUriProcessor $uriProcessor
562
    ) {
563
        $baseMimeTypes = [
564
            MimeTypes::MIME_APPLICATION_JSON,
565
            MimeTypes::MIME_APPLICATION_JSON_FULL_META,
566
            MimeTypes::MIME_APPLICATION_JSON_NO_META,
567
            MimeTypes::MIME_APPLICATION_JSON_MINIMAL_META,
568
            MimeTypes::MIME_APPLICATION_JSON_VERBOSE, ];
569
570
        // The Accept request-header field specifies media types which are acceptable for the response
571
572
        $host = $this->getHost();
573
        $requestAcceptText = $host->getRequestAccept();
574
        $requestVersion = $request->getResponseVersion();
575
576
        //if the $format header is present it overrides the accepts header
577
        $format = $host->getQueryStringItem(ODataConstants::HTTPQUERY_STRING_FORMAT);
578
        if (null !== $format) {
579
            //There's a strange edge case..if application/json is supplied and it's V3
580
            if (MimeTypes::MIME_APPLICATION_JSON == $format && Version::v3() == $requestVersion) {
581
                //then it's actual minimalmetadata
582
                //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...
583
                $format = MimeTypes::MIME_APPLICATION_JSON_MINIMAL_META;
584
            }
585
586
            $requestAcceptText = ServiceHost::translateFormatToMime($requestVersion, $format);
587
        }
588
589
        //The response format can be dictated by the target resource kind. IE a $value will be different then expected
590
        //getTargetKind doesn't deal with link resources directly and this can change things
591
        $targetKind = $request->isLinkUri() ? TargetKind::LINK() : $request->getTargetKind();
592
        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

592
        /** @scrutinizer ignore-call */ 
593
        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...
593
594
        switch ($targetKind) {
595
            case TargetKind::METADATA():
596
                return HttpProcessUtility::selectMimeType(
597
                    $requestAcceptText,
598
                    [MimeTypes::MIME_APPLICATION_XML]
599
                );
600
601
            case TargetKind::SERVICE_DIRECTORY():
602
                return HttpProcessUtility::selectMimeType(
603
                    $requestAcceptText,
604
                    array_merge(
605
                        [MimeTypes::MIME_APPLICATION_ATOMSERVICE],
606
                        $baseMimeTypes
607
                    )
608
                );
609
610
            case TargetKind::PRIMITIVE_VALUE():
611
                $supportedResponseMimeTypes = [MimeTypes::MIME_TEXTPLAIN];
612
613
                if ('$count' != $request->getIdentifier()) {
614
                    $projectedProperty = $request->getProjectedProperty();
615
                    assert(null !== $projectedProperty, 'is_null($projectedProperty)');
616
                    $type = $projectedProperty->getInstanceType();
617
                    assert($type instanceof IType, '!$type instanceof IType');
618
                    if ($type instanceof Binary) {
619
                        $supportedResponseMimeTypes = [MimeTypes::MIME_APPLICATION_OCTETSTREAM];
620
                    }
621
                }
622
623
                return HttpProcessUtility::selectMimeType(
624
                    $requestAcceptText,
625
                    $supportedResponseMimeTypes
626
                );
627
628
            case TargetKind::PRIMITIVE():
629
            case TargetKind::COMPLEX_OBJECT():
630
            case TargetKind::BAG():
631
            case TargetKind::LINK():
632
                return HttpProcessUtility::selectMimeType(
633
                    $requestAcceptText,
634
                    array_merge(
635
                        [MimeTypes::MIME_APPLICATION_XML,
636
                            MimeTypes::MIME_TEXTXML, ],
637
                        $baseMimeTypes
638
                    )
639
                );
640
641
            case TargetKind::SINGLETON():
642
            case TargetKind::RESOURCE():
643
                return HttpProcessUtility::selectMimeType(
644
                    $requestAcceptText,
645
                    array_merge(
646
                        [MimeTypes::MIME_APPLICATION_ATOM],
647
                        $baseMimeTypes
648
                    )
649
                );
650
651
            case TargetKind::MEDIA_RESOURCE():
652
                if (!$request->isNamedStream() && !$request->getTargetResourceType()->isMediaLinkEntry()) {
653
                    throw ODataException::createBadRequestError(
654
                        Messages::badRequestInvalidUriForMediaResource(
655
                            $host->getAbsoluteRequestUri()->getUrlAsString()
656
                        )
657
                    );
658
                }
659
660
                $uriProcessor->execute();
661
                $request->setExecuted();
662
                // DSSW::getStreamContentType can throw error in 2 cases
663
                // 1. If the required stream implementation not found
664
                // 2. If IDSSP::getStreamContentType returns NULL for MLE
665
                $responseContentType = $this->getStreamProviderWrapper()
666
                    ->getStreamContentType(
667
                        $request->getTargetResult(),
668
                        $request->getResourceStreamInfo()
669
                    );
670
671
                // Note StreamWrapper::getStreamContentType can return NULL if the requested named stream has not
672
                // yet been uploaded. But for an MLE if IDSSP::getStreamContentType returns NULL
673
                // then StreamWrapper will throw error
674
                if (null !== $responseContentType) {
675
                    $responseContentType = HttpProcessUtility::selectMimeType(
676
                        $requestAcceptText,
677
                        [$responseContentType]
678
                    );
679
                }
680
681
                return $responseContentType;
682
        }
683
684
        //If we got here, we just don't know what it is...
685
        throw new ODataException(Messages::unsupportedMediaType(), 415);
686
    }
687
688
    /**
689
     * For the given entry object compare its eTag (if it has eTag properties)
690
     * with current eTag request headers (if present).
691
     *
692
     * @param mixed        &$entryObject             entity resource for which etag
693
     *                                               needs to be checked
694
     * @param ResourceType &$resourceType            Resource type of the entry
695
     *                                               object
696
     * @param bool         &$needToSerializeResponse On return, this will contain
697
     *                                               True if response needs to be
698
     *                                               serialized, False otherwise
699
     * @param bool         $needToSerializeResponse
700
     *
701
     * @throws ODataException
702
     * @return string|null    The ETag for the entry object if it has eTag properties
703
     *                        NULL otherwise
704
     */
705
    protected function compareETag(
706
        &$entryObject,
707
        ResourceType &$resourceType,
708
        &$needToSerializeResponse
709
    ) {
710
        $needToSerializeResponse = true;
711
        $eTag = null;
712
        $ifMatch = $this->getHost()->getRequestIfMatch();
713
        $ifNoneMatch = $this->getHost()->getRequestIfNoneMatch();
714
        if (null === $entryObject) {
715
            if (null !== $ifMatch) {
716
                throw ODataException::createPreConditionFailedError(
717
                    Messages::eTagNotAllowedForNonExistingResource()
718
                );
719
            }
720
721
            return null;
722
        }
723
724
        if ($this->getConfiguration()->getValidateETagHeader() && !$resourceType->hasETagProperties()) {
725
            if (null !== $ifMatch || null !== $ifNoneMatch) {
726
                // No eTag properties but request has eTag headers, bad request
727
                throw ODataException::createBadRequestError(
728
                    Messages::noETagPropertiesForType()
729
                );
730
            }
731
732
            // We need write the response but no eTag header
733
            return null;
734
        }
735
736
        if (!$this->getConfiguration()->getValidateETagHeader()) {
737
            // Configuration says do not validate ETag, so we will not write ETag header in the
738
            // response even though the requested resource support it
739
            return null;
740
        }
741
742
        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...
743
            // No request eTag header, we need to write the response
744
            // and eTag header
745
        } 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...
746
            // If-Match:* => we need to write the response and eTag header
747
        } elseif (0 === strcmp($ifNoneMatch, '*')) {
748
            // if-None-Match:* => Do not write the response (304 not modified),
749
            // but write eTag header
750
            $needToSerializeResponse = false;
751
        } else {
752
            $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...
753
            // Note: The following code for attaching the prefix W\"
754
            // and the suffix " can be done in getETagForEntry function
755
            // but that is causing an issue in Linux env where the
756
            // firefox browser is unable to parse the ETag in this case.
757
            // Need to follow up PHP core devs for this.
758
            $eTag = ODataConstants::HTTP_WEAK_ETAG_PREFIX . $eTag . '"';
759
            if (null !== $ifMatch) {
760
                if (0 != strcmp($eTag, $ifMatch)) {
761
                    // Requested If-Match value does not match with current
762
                    // eTag Value then pre-condition error
763
                    // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
764
                    throw ODataException::createPreConditionFailedError(
765
                        Messages::eTagValueDoesNotMatch()
766
                    );
767
                }
768
            } elseif (0 === strcmp($eTag, $ifNoneMatch)) {
769
                //304 not modified, but in write eTag header
770
                $needToSerializeResponse = false;
771
            }
772
        }
773
774
        if (null === $eTag) {
775
            $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...
776
            // Note: The following code for attaching the prefix W\"
777
            // and the suffix " can be done in getETagForEntry function
778
            // but that is causing an issue in Linux env where the
779
            // firefox browser is unable to parse the ETag in this case.
780
            // Need to follow up PHP core devs for this.
781
            $eTag = ODataConstants::HTTP_WEAK_ETAG_PREFIX . $eTag . '"';
782
        }
783
784
        return $eTag;
785
    }
786
787
    /**
788
     * Returns the etag for the given resource.
789
     * Note: This function will not add W\" prefix and " suffix, that is caller's
790
     * responsibility.
791
     *
792
     * @param mixed        &$entryObject  Resource for which etag value needs to
793
     *                                    be returned
794
     * @param ResourceType &$resourceType Resource type of the $entryObject
795
     *
796
     * @throws ODataException
797
     * @return string|null    ETag value for the given resource (with values encoded
798
     *                        for use in a URI) there are etag properties, NULL if
799
     *                        there is no etag property
800
     */
801
    protected function getETagForEntry(&$entryObject, ResourceType &$resourceType)
802
    {
803
        $eTag = null;
804
        $comma = null;
805
        foreach ($resourceType->getETagProperties() as $eTagProperty) {
806
            $type = $eTagProperty->getInstanceType();
807
            assert($type instanceof IType, '!$type instanceof IType');
808
809
            $value = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $value is dead and can be removed.
Loading history...
810
            $property = $eTagProperty->getName();
811
            try {
812
                //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...
813
                $value = ReflectionHandler::getProperty($entryObject, $property);
814
            } catch (\ReflectionException $reflectionException) {
815
                throw ODataException::createInternalServerError(
816
                    Messages::failedToAccessProperty($property, $resourceType->getName())
817
                );
818
            }
819
820
            $eTagBase = $eTag . $comma;
821
            $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

821
            $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...
822
823
            $comma = ',';
824
        }
825
826
        if (null !== $eTag) {
827
            // If eTag is made up of datetime or string properties then the above
828
            // IType::convertToOData will perform utf8 and url encode. But we don't
829
            // want this for eTag value.
830
            $eTag = urldecode(utf8_decode($eTag));
831
832
            return rtrim($eTag, ',');
833
        }
834
        return null;
835
    }
836
}
837