Passed
Push — master ( 2b99ce...30750d )
by Christopher
05:48
created

UriProcessor::checkUriValidForSuppliedVerb()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 9
Ratio 100 %

Importance

Changes 0
Metric Value
dl 9
loc 9
rs 9.6666
c 0
b 0
f 0
cc 3
eloc 5
nc 2
nop 4
1
<?php
2
3
namespace POData\UriProcessor;
4
5
use POData\Common\Messages;
6
use POData\Common\ODataConstants;
7
use POData\Common\ODataException;
8
use POData\IService;
9
use POData\OperationContext\HTTPRequestMethod;
10
use POData\Providers\Metadata\ResourcePropertyKind;
11
use POData\Providers\ProvidersWrapper;
12
use POData\Providers\Query\QueryResult;
13
use POData\Providers\Query\QueryType;
14
use POData\UriProcessor\Interfaces\IUriProcessor;
15
use POData\UriProcessor\QueryProcessor\QueryProcessor;
16
use POData\UriProcessor\ResourcePathProcessor\ResourcePathProcessor;
17
use POData\UriProcessor\ResourcePathProcessor\SegmentParser\SegmentDescriptor;
18
use POData\UriProcessor\ResourcePathProcessor\SegmentParser\TargetKind;
19
use POData\UriProcessor\ResourcePathProcessor\SegmentParser\TargetSource;
20
21
class UriProcessor implements IUriProcessor
22
{
23
    /**
24
     * Description of the OData request that a client has submitted.
25
     *
26
     * @var RequestDescription
27
     */
28
    private $request;
29
30
    /**
31
     * Holds reference to the data service instance.
32
     *
33
     * @var IService
34
     */
35
    private $service;
36
37
    /**
38
     * Holds reference to the wrapper over IDSMP and IDSQP implementation.
39
     *
40
     * @var ProvidersWrapper
41
     */
42
    private $providers;
43
44
    /**
45
     * Holds reference to request expander.
46
     *
47
     * @var RequestExpander
48
     */
49
    private $expander;
50
51
    /**
52
     * Constructs a new instance of UriProcessor.
53
     *
54
     * @param IService $service Reference to the data service instance
55
     */
56
    private function __construct(IService $service)
57
    {
58
        $this->service = $service;
59
        $this->providers = $service->getProvidersWrapper();
60
    }
61
62
    public static function process(IService $service)
63
    {
64
        $absoluteRequestUri = $service->getHost()->getAbsoluteRequestUri();
65
        $absoluteServiceUri = $service->getHost()->getAbsoluteServiceUri();
66
67
        if (!$absoluteServiceUri->isBaseOf($absoluteRequestUri)) {
68
            throw ODataException::createInternalServerError(
69
                Messages::uriProcessorRequestUriDoesNotHaveTheRightBaseUri(
70
                    $absoluteRequestUri->getUrlAsString(),
71
                    $absoluteServiceUri->getUrlAsString()
72
                )
73
            );
74
        }
75
76
        $uriProcessor = new self($service);
77
        //Parse the resource path part of the request Uri.
78
        $uriProcessor->request = ResourcePathProcessor::process($service);
79
        $uriProcessor->expander = new RequestExpander(
80
            $uriProcessor->getRequest(),
81
            $uriProcessor->getService(),
82
            $uriProcessor->getProviders()
83
        );
84
85
        $uriProcessor->getRequest()->setUriProcessor($uriProcessor);
86
87
        //Parse the query string options of the request Uri.
88
        QueryProcessor::process($uriProcessor->request, $service);
89
90
        return $uriProcessor;
91
    }
92
93
    /**
94
     * @return RequestDescription
95
     */
96
    public function getRequest()
97
    {
98
        return $this->request;
99
    }
100
101
    /**
102
     * Gets reference to the request submitted by client.
103
     *
104
     * @return ProvidersWrapper
105
     */
106
    public function getProviders()
107
    {
108
        return $this->providers;
109
    }
110
111
    /**
112
     * Gets the data service instance.
113
     *
114
     * @return IService
115
     */
116
    public function getService()
117
    {
118
        return $this->service;
119
    }
120
121
    /**
122
     * Gets the request expander instance.
123
     *
124
     * @return RequestExpander
125
     */
126
    public function getExpander()
127
    {
128
        return $this->expander;
129
    }
130
131
    /**
132
     * Execute the client submitted request against the data source.
133
     */
134
    public function execute()
135
    {
136
        $service = $this->getService();
137
        $operationContext = !isset($service) ? null : $service->getOperationContext();
138
        if (!$operationContext) {
139
            $this->executeBase();
140
141
            return;
142
        }
143
144
        $requestMethod = $operationContext->incomingRequest()->getMethod();
145
        if ($requestMethod == HTTPRequestMethod::GET()) {
146
            $this->executeGet();
147
        } elseif ($requestMethod == HTTPRequestMethod::POST()) {
148
            $this->executePost();
149
        } elseif ($requestMethod == HTTPRequestMethod::PUT()) {
150
            $this->executePut();
151
        } elseif ($requestMethod == HTTPRequestMethod::DELETE()) {
152
            $this->executeDelete();
153
            //TODO: we probably need these verbs eventually.
154
        /*} elseif ($requestMethod == HTTPRequestMethod::PATCH()) {
0 ignored issues
show
Unused Code Comprehensibility introduced by
58% 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...
155
            $this->executePatch();
156
        } elseif ($requestMethod == HTTPRequestMethod::MERGE()) {
157
            $this->executeMerge();*/
158
        } else {
159
            throw ODataException::createNotImplementedError(Messages::onlyReadSupport($requestMethod));
160
        }
161
    }
162
163
    /**
164
     * Execute the client submitted request against the data source (GET).
165
     */
166
    protected function executeGet()
167
    {
168
        $this->executeBase();
169
    }
170
171
    /**
172
     * Execute the client submitted request against the data source (POST).
173
     */
174
    protected function executePost()
175
    {
176
        $segments = $this->getRequest()->getSegments();
177
178
        foreach ($segments as $segment) {
179
            $requestTargetKind = $segment->getTargetKind();
180
            if ($requestTargetKind == TargetKind::RESOURCE()) {
181
                $requestMethod = $this->getService()->getOperationContext()->incomingRequest()->getMethod();
182
                $resourceSet = $segment->getTargetResourceSetWrapper();
183
                $keyDescriptor = $segment->getKeyDescriptor();
184
                if (!$resourceSet) {
185
                    $url = $this->getService()->getHost()->getAbsoluteRequestUri()->getUrlAsString();
186
                    throw ODataException::createBadRequestError(
187
                        Messages::badRequestInvalidUriForThisVerb($url, $requestMethod)
188
                    );
189
                }
190
                $data = $this->getRequest()->getData();
191
                if (empty($data)) {
192
                    throw ODataException::createBadRequestError(Messages::noDataForThisVerb($requestMethod));
193
                }
194
                $queryResult = $this->getProviders()->createResourceforResourceSet($resourceSet, $keyDescriptor, $data);
0 ignored issues
show
Documentation introduced by
$data is of type array, but the function expects a object.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
195
                $segment->setResult($queryResult);
196
            }
197
        }
198
        //return $this->executeBase();
0 ignored issues
show
Unused Code Comprehensibility introduced by
75% 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...
199
    }
200
201
    /**
202
     * Execute the client submitted request against the data source (PUT).
203
     */
204
    protected function executePut()
205
    {
206
        $this->executeBase(function ($uriProcessor, $segment) {
207
            $requestMethod = $uriProcessor->getService()->getOperationContext()->incomingRequest()->getMethod();
208
            $resourceSet = $segment->getTargetResourceSetWrapper();
209
            $keyDescriptor = $segment->getKeyDescriptor();
210
211
            $uriProcessor->checkUriValidForSuppliedVerb($uriProcessor, $resourceSet, $keyDescriptor, $requestMethod);
212
213
            $data = $uriProcessor->getRequest()->getData();
214
            if (!$data) {
215
                throw ODataException::createBadRequestError(Messages::noDataForThisVerb($requestMethod));
216
            }
217
218
            $queryResult = $uriProcessor->getProviders()->updateResource(
219
                $resourceSet,
220
                $segment->getResult(),
221
                $keyDescriptor,
222
                $data,
223
                false
224
            );
225
            $segment->setResult($queryResult);
226
227
            return $queryResult;
228
        });
229
    }
230
231
    /**
232
     * Execute the client submitted request against the data source (DELETE).
233
     */
234
    protected function executeDelete()
235
    {
236
        $this->executeBase(function ($uriProcessor, $segment) {
237
            $requestMethod = $uriProcessor->getService()->getOperationContext()->incomingRequest()->getMethod();
238
            $resourceSet = $segment->getTargetResourceSetWrapper();
239
            $keyDescriptor = $segment->getKeyDescriptor();
240
            $uriProcessor->checkUriValidForSuppliedVerb($uriProcessor, $resourceSet, $keyDescriptor, $requestMethod);
241
242
            return $uriProcessor->getProviders()->deleteResource($resourceSet, $segment->getResult());
243
        });
244
    }
245
246
    /**
247
     * Execute the client submitted request against the data source.
248
     *
249
     * @param callable $callback Function, what must be called
250
     */
251
    protected function executeBase($callback = null)
252
    {
253
        $segments = $this->getRequest()->getSegments();
254
255
        foreach ($segments as $segment) {
256
            $requestTargetKind = $segment->getTargetKind();
257
258
            if (TargetKind::SINGLETON() == $requestTargetKind) {
259
                $singleton = $this->getService()->getProvidersWrapper()->resolveSingleton($segment->getIdentifier());
260
                $segment->setResult($singleton->get());
261
            } elseif ($segment->getTargetSource() == TargetSource::ENTITY_SET) {
262
                $this->handleSegmentTargetsToResourceSet($segment);
263
            } elseif ($requestTargetKind == TargetKind::RESOURCE()) {
264
                if (is_null($segment->getPrevious()->getResult())) {
265
                    throw ODataException::createResourceNotFoundError(
266
                        $segment->getPrevious()->getIdentifier()
267
                    );
268
                }
269
                $this->handleSegmentTargetsToRelatedResource($segment);
270
            } elseif ($requestTargetKind == TargetKind::LINK()) {
271
                $segment->setResult($segment->getPrevious()->getResult());
272
            } elseif ($segment->getIdentifier() == ODataConstants::URI_COUNT_SEGMENT) {
273
                // we are done, $count will the last segment and
274
                // taken care by _applyQueryOptions method
275
                $segment->setResult($this->getRequest()->getCountValue());
276
                break;
277
            } else {
278
                if ($requestTargetKind == TargetKind::MEDIA_RESOURCE()) {
279
                    if (is_null($segment->getPrevious()->getResult())) {
280
                        throw ODataException::createResourceNotFoundError(
281
                            $segment->getPrevious()->getIdentifier()
282
                        );
283
                    }
284
                    // For MLE and Named Stream the result of last segment
285
                    // should be that of previous segment, this is required
286
                    // while retrieving content type or stream from IDSSP
287
                    $segment->setResult($segment->getPrevious()->getResult());
288
                    // we are done, as named stream property or $value on
289
                    // media resource will be the last segment
290
                    break;
291
                }
292
293
                $value = $segment->getPrevious()->getResult();
294
                while (!is_null($segment)) {
295
                    //TODO: what exactly is this doing here?  Once a null's found it seems everything will be null
296
                    if (!is_null($value)) {
297
                        $value = null;
298
                    } else {
299
                        // This is theoretically impossible to reach, but should that be changed, this will need to call
300
                        // ResourceType::getPropertyValue... somehow
301
                        try {
302
                            //see #88
303
                            $value = \POData\Common\ReflectionHandler::getProperty($value, $segment->getIdentifier());
304
                        } catch (\ReflectionException $reflectionException) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
305
                        }
306
                    }
307
308
                    $segment->setResult($value);
309
                    $segment = $segment->getNext();
310
                    if (!is_null($segment) && ODataConstants::URI_VALUE_SEGMENT == $segment->getIdentifier()) {
311
                        $segment->setResult($value);
312
                        $segment = $segment->getNext();
313
                    }
314
                }
315
316
                break;
317
            }
318
319
            if (is_null($segment->getNext())
320
                || ODataConstants::URI_COUNT_SEGMENT == $segment->getNext()->getIdentifier()
321
            ) {
322
                $this->applyQueryOptions($segment, $callback);
323
            }
324
        }
325
326
            // Apply $select and $expand options to result set, this function will be always applied
327
            // irrespective of return value of IDSQP2::canApplyQueryOptions which means library will
328
            // not delegate $expand/$select operation to IDSQP2 implementation
329
        $this->handleExpansion();
330
    }
331
332
    /**
333
     * Query for a resource set pointed by the given segment descriptor and update the descriptor with the result.
334
     *
335
     * @param SegmentDescriptor $segment Describes the resource set to query
336
     */
337
    private function handleSegmentTargetsToResourceSet(SegmentDescriptor $segment)
338
    {
339
        if ($segment->isSingleResult()) {
340
            $entityInstance = $this->getProviders()->getResourceFromResourceSet(
341
                $segment->getTargetResourceSetWrapper(),
342
                $segment->getKeyDescriptor()
343
            );
344
345
            $segment->setResult($entityInstance);
346
        } else {
347
            $skip = (null == $this->getRequest()) ? 0 : $this->getRequest()->getSkipCount();
348
            $skip = (null == $skip) ? 0 : $skip;
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing $skip of type integer|null against null; this is ambiguous if the integer can be zero. Consider using a strict comparison === instead.
Loading history...
349
            $skipToken = $this->getRequest()->getInternalSkipTokenInfo();
350
            $skipToken = (null != $skipToken) ? $skipToken->getSkipTokenInfo() : null;
351
            $queryResult = $this->getProviders()->getResourceSet(
352
                $this->getRequest()->queryType,
353
                $segment->getTargetResourceSetWrapper(),
354
                $this->getRequest()->getFilterInfo(),
355
                $this->getRequest()->getInternalOrderByInfo(),
356
                $this->getRequest()->getTopCount(),
357
                $skip,
358
                $skipToken
359
            );
360
            $segment->setResult($queryResult);
361
        }
362
    }
363
364
    /**
365
     * Query for a related resource set or resource set reference pointed by the
366
     * given segment descriptor and update the descriptor with the result.
367
     *
368
     * @param SegmentDescriptor &$segment Describes the related resource
369
     *                                    to query
370
     */
371
    private function handleSegmentTargetsToRelatedResource(SegmentDescriptor $segment)
372
    {
373
        $projectedProperty = $segment->getProjectedProperty();
374
        $projectedPropertyKind = $projectedProperty->getKind();
375
376
        if ($projectedPropertyKind == ResourcePropertyKind::RESOURCESET_REFERENCE) {
377
            if ($segment->isSingleResult()) {
378
                $entityInstance = $this->getProviders()->getResourceFromRelatedResourceSet(
379
                    $segment->getPrevious()->getTargetResourceSetWrapper(),
380
                    $segment->getPrevious()->getResult(),
381
                    $segment->getTargetResourceSetWrapper(),
382
                    $projectedProperty,
383
                    $segment->getKeyDescriptor()
384
                );
385
386
                $segment->setResult($entityInstance);
387
            } else {
388
                $skipToken = $this->getRequest()->getInternalSkipTokenInfo();
389
                $skipToken = (null != $skipToken) ? $skipToken->getSkipTokenInfo() : null;
390
                $queryResult = $this->getProviders()->getRelatedResourceSet(
391
                    $this->getRequest()->queryType,
392
                    $segment->getPrevious()->getTargetResourceSetWrapper(),
393
                    $segment->getPrevious()->getResult(),
394
                    $segment->getTargetResourceSetWrapper(),
395
                    $segment->getProjectedProperty(),
396
                    $this->getRequest()->getFilterInfo(),
397
                    //TODO: why are these null?  see #98
398
                    null, // $orderby
399
                    null, // $top
400
                    null,  // $skip
401
                    $skipToken
402
                );
403
404
                $segment->setResult($queryResult);
405
            }
406
        } elseif ($projectedPropertyKind == ResourcePropertyKind::RESOURCE_REFERENCE) {
407
            $entityInstance = $this->getProviders()->getRelatedResourceReference(
408
                $segment->getPrevious()->getTargetResourceSetWrapper(),
409
                $segment->getPrevious()->getResult(),
410
                $segment->getTargetResourceSetWrapper(),
411
                $segment->getProjectedProperty()
412
            );
413
414
            $segment->setResult($entityInstance);
415
        } else {
0 ignored issues
show
Unused Code introduced by
This else statement is empty and can be removed.

This check looks for the else branches 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 else branches can be removed.

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

could be turned into

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

This is much more concise to read.

Loading history...
416
            //Unexpected state
417
        }
418
    }
419
420
    /**
421
     * Applies the query options to the resource(s) retrieved from the data source.
422
     *
423
     * @param SegmentDescriptor $segment  The descriptor which holds resource(s) on which query options to be applied
424
     * @param callable          $callback Function, what must be called
425
     */
426
    private function applyQueryOptions(SegmentDescriptor $segment, $callback = null)
427
    {
428
        // For non-GET methods
429
        if ($callback) {
430
            $callback($this, $segment);
431
432
            return;
433
        }
434
435
        //TODO: I'm not really happy with this..i think i'd rather keep the result the QueryResult
436
        //not even bother with the setCountValue stuff (shouldn't counts be on segments?)
437
        //and just work with the QueryResult in the object model serializer
438
        $result = $segment->getResult();
439
440
        if (!$result instanceof QueryResult) {
441
            //If the segment isn't a query result, then there's no paging or counting to be done
442
            return;
443
        }
444
445
        // Note $inlinecount=allpages means include the total count regardless of paging..so we set the counts first
446
        // regardless if POData does the paging or not.
447 View Code Duplication
        if ($this->getRequest()->queryType == QueryType::ENTITIES_WITH_COUNT()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
448
            if ($this->getProviders()->handlesOrderedPaging()) {
449
                $this->getRequest()->setCountValue($result->count);
450
            } else {
451
                $this->getRequest()->setCountValue(count($result->results));
452
            }
453
        }
454
455
        //Have POData perform paging if necessary
456
        if (!$this->getProviders()->handlesOrderedPaging() && !empty($result->results)) {
457
            $result->results = $this->performPaging($result->results);
458
        }
459
460
        //a bit surprising, but $skip and $top affects $count so update it here, not above
461
        //IE  data.svc/Collection/$count?$top=10 returns 10 even if Collection has 11+ entries
462 View Code Duplication
        if ($this->getRequest()->queryType == QueryType::COUNT()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
463
            if ($this->getProviders()->handlesOrderedPaging()) {
464
                $this->getRequest()->setCountValue($result->count);
465
            } else {
466
                $this->getRequest()->setCountValue(count($result->results));
467
            }
468
        }
469
470
        $segment->setResult($result);
471
    }
472
473
    /**
474
     * If the provider does not perform the paging (ordering, top, skip) then this method does it.
475
     *
476
     * @param array $result
477
     *
478
     * @return array
479
     */
480
    private function performPaging(array $result)
481
    {
482
        //Apply (implicit and explicit) $orderby option
483
        $internalOrderByInfo = $this->getRequest()->getInternalOrderByInfo();
484
        if (!is_null($internalOrderByInfo)) {
485
            $orderByFunction = $internalOrderByInfo->getSorterFunction();
486
            usort($result, $orderByFunction);
487
        }
488
489
        //Apply $skiptoken option
490
        $internalSkipTokenInfo = $this->getRequest()->getInternalSkipTokenInfo();
491
        if (!is_null($internalSkipTokenInfo)) {
492
            $matchingIndex = $internalSkipTokenInfo->getIndexOfFirstEntryInTheNextPage($result);
493
            $result = array_slice($result, $matchingIndex);
494
        }
495
496
        //Apply $top and $skip option
497
        if (!empty($result)) {
498
            $top = $this->getRequest()->getTopCount();
499
            $skip = $this->getRequest()->getSkipCount();
500
            if (is_null($skip)) {
501
                $skip = 0;
502
            }
503
504
            $result = array_slice($result, $skip, $top);
505
        }
506
507
        return $result;
508
    }
509
510
    /**
511
     * Perform expansion.
512
     */
513
    private function handleExpansion()
514
    {
515
        $this->getExpander()->handleExpansion();
516
    }
517
518
    /**
519
     * @param $uriProcessor
520
     * @param $resourceSet
521
     * @param $keyDescriptor
522
     * @param $requestMethod
523
     * @throws ODataException
524
     */
525 View Code Duplication
    public function checkUriValidForSuppliedVerb($uriProcessor, $resourceSet, $keyDescriptor, $requestMethod)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
526
    {
527
        if (!$resourceSet || !$keyDescriptor) {
528
            $url = $uriProcessor->getService()->getHost()->getAbsoluteRequestUri()->getUrlAsString();
529
            throw ODataException::createBadRequestError(
530
                Messages::badRequestInvalidUriForThisVerb($url, $requestMethod)
531
            );
532
        }
533
    }
534
}
535