Completed
Pull Request — master (#61)
by Alex
03:53
created

BaseService::getObjectSerialiser()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Loading history...
470
                    );
471
                } elseif (TargetKind::PRIMITIVE() == $requestTargetKind) {
472
                    $odataModelInstance = $objectModelSerializer->writeTopLevelPrimitive(
473
                        $result,
474
                        $request->getProjectedProperty(),
0 ignored issues
show
Bug introduced by
$request->getProjectedProperty() cannot be passed to writetoplevelprimitive() as the parameter $resourceProperty expects a reference.
Loading history...
Bug introduced by
It seems like $request->getProjectedProperty() can be null; however, writeTopLevelPrimitive() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
475
                        $odataModelInstance
0 ignored issues
show
Unused Code introduced by
The call to IObjectSerialiser::writeTopLevelPrimitive() has too many arguments starting with $odataModelInstance.

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

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

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

Loading history...
476
                    );
477
                } 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...
478
                    // Code path for primitive value (Since its primitve no need for
479
                    // object model serialization)
480
                    // 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...
481
                    // Employees(1)/Photo/$value => binary stream
482
                    // Customers/$count => string
483
                } else {
484
                    assert(false, 'Unexpected resource target kind');
485
                }
486
            }
487
        }
488
489
        //Note: Response content type can be null for named stream
490
        if ($hasResponseBody && !is_null($responseContentType)) {
491
            if (TargetKind::MEDIA_RESOURCE() != $request->getTargetKind()
492
                && MimeTypes::MIME_APPLICATION_OCTETSTREAM != $responseContentType) {
493
                //append charset for everything except:
494
                //stream resources as they have their own content type
495
                //binary properties (they content type will be App Octet for those...is this a good way?
496
                //we could also decide based upon the projected property
497
498
                $responseContentType .= ';charset=utf-8';
499
            }
500
        }
501
502
        if ($hasResponseBody) {
503
            ResponseWriter::write($this, $request, $odataModelInstance, $responseContentType);
504
        }
505
    }
506
507
    /**
508
     * Gets the response format for the requested resource.
509
     *
510
     * @param RequestDescription $request      The request submitted by client and it's execution result
511
     * @param UriProcessor       $uriProcessor The reference to the UriProcessor
512
     *
513
     * @return string the response content-type, a null value means the requested resource
514
     *                is named stream and IDSSP2::getStreamContentType returned null
515
     *
516
     * @throws ODataException, HttpHeaderFailure
517
     */
518
    public function getResponseContentType(
519
        RequestDescription $request,
520
        UriProcessor $uriProcessor
521
    ) {
522
523
        $baseMimeTypes = [
524
            MimeTypes::MIME_APPLICATION_JSON,
525
            MimeTypes::MIME_APPLICATION_JSON_FULL_META,
526
            MimeTypes::MIME_APPLICATION_JSON_NO_META,
527
            MimeTypes::MIME_APPLICATION_JSON_MINIMAL_META,
528
            MimeTypes::MIME_APPLICATION_JSON_VERBOSE];
529
530
531
        // The Accept request-header field specifies media types which are acceptable for the response
532
533
        $host = $this->getHost();
534
        $requestAcceptText = $host->getRequestAccept();
535
        $requestVersion = $request->getResponseVersion();
536
537
        //if the $format header is present it overrides the accepts header
538
        $format = $host->getQueryStringItem(ODataConstants::HTTPQUERY_STRING_FORMAT);
539
        if (!is_null($format)) {
540
            //There's a strange edge case..if application/json is supplied and it's V3
541
            if (MimeTypes::MIME_APPLICATION_JSON == $format && Version::v3() == $requestVersion) {
542
                //then it's actual minimalmetadata
543
                //TODO: should this be done with the header text too?
544
                $format = MimeTypes::MIME_APPLICATION_JSON_MINIMAL_META;
545
            }
546
547
            $requestAcceptText = ServiceHost::translateFormatToMime($requestVersion, $format);
548
        }
549
550
        //The response format can be dictated by the target resource kind. IE a $value will be different then expected
551
        //getTargetKind doesn't deal with link resources directly and this can change things
552
        $targetKind = $request->isLinkUri() ? TargetKind::LINK() : $request->getTargetKind();
553
554
        switch ($targetKind) {
555
            case TargetKind::METADATA():
556
                return HttpProcessUtility::selectMimeType(
557
                    $requestAcceptText,
558
                    array(MimeTypes::MIME_APPLICATION_XML)
559
                );
560
561 View Code Duplication
            case TargetKind::SERVICE_DIRECTORY():
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...
562
                return HttpProcessUtility::selectMimeType(
563
                    $requestAcceptText,
564
                    array_merge(
565
                        [MimeTypes::MIME_APPLICATION_ATOMSERVICE,
566
                            MimeTypes::MIME_APPLICATION_XML],
567
                        $baseMimeTypes
568
                    )
569
                );
570
571
            case TargetKind::PRIMITIVE_VALUE():
572
                $supportedResponseMimeTypes = array(MimeTypes::MIME_TEXTPLAIN);
573
574
                if ('$count' != $request->getIdentifier()) {
575
                    $projectedProperty = $request->getProjectedProperty();
576
                    assert(!is_null($projectedProperty), 'is_null($projectedProperty)');
577
                    $type = $projectedProperty->getInstanceType();
578
                    assert($type instanceof IType, '!$type instanceof IType');
579
                    if ($type instanceof Binary) {
580
                        $supportedResponseMimeTypes = array(MimeTypes::MIME_APPLICATION_OCTETSTREAM);
581
                    }
582
                }
583
584
                return HttpProcessUtility::selectMimeType(
585
                    $requestAcceptText,
586
                    $supportedResponseMimeTypes
587
                );
588
589
            case TargetKind::PRIMITIVE():
590
            case TargetKind::COMPLEX_OBJECT():
591
            case TargetKind::BAG():
592 View Code Duplication
            case TargetKind::LINK():
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...
593
                return HttpProcessUtility::selectMimeType(
594
                    $requestAcceptText,
595
                    array_merge(
596
                        [MimeTypes::MIME_APPLICATION_XML,
597
                            MimeTypes::MIME_TEXTXML],
598
                        $baseMimeTypes
599
                    )
600
                );
601
602
            case TargetKind::RESOURCE():
603
                return HttpProcessUtility::selectMimeType(
604
                    $requestAcceptText,
605
                    array_merge(
606
                        [MimeTypes::MIME_APPLICATION_ATOM],
607
                        $baseMimeTypes
608
                    )
609
                );
610
611
            case TargetKind::MEDIA_RESOURCE():
612
                if (!$request->isNamedStream() && !$request->getTargetResourceType()->isMediaLinkEntry()) {
613
                    throw ODataException::createBadRequestError(
614
                        Messages::badRequestInvalidUriForMediaResource(
615
                            $host->getAbsoluteRequestUri()->getUrlAsString()
616
                        )
617
                    );
618
                }
619
620
                $uriProcessor->execute();
621
                $request->setExecuted();
622
                // DSSW::getStreamContentType can throw error in 2 cases
623
                // 1. If the required stream implementation not found
624
                // 2. If IDSSP::getStreamContentType returns NULL for MLE
625
                $responseContentType = $this->getStreamProviderWrapper()
626
                    ->getStreamContentType(
627
                        $request->getTargetResult(),
628
                        $request->getResourceStreamInfo()
0 ignored issues
show
Bug introduced by
It seems like $request->getResourceStreamInfo() can be null; however, getStreamContentType() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
629
                    );
630
631
                // Note StreamWrapper::getStreamContentType can return NULL if the requested named stream has not
632
                // yet been uploaded. But for an MLE if IDSSP::getStreamContentType returns NULL
633
                // then StreamWrapper will throw error
634
                if (!is_null($responseContentType)) {
635
                    $responseContentType = HttpProcessUtility::selectMimeType(
636
                        $requestAcceptText,
637
                        array($responseContentType)
638
                    );
639
                }
640
641
                return $responseContentType;
642
        }
643
644
        //If we got here, we just don't know what it is...
645
        throw new ODataException(Messages::unsupportedMediaType(), 415);
646
    }
647
648
    /**
649
     * For the given entry object compare its eTag (if it has eTag properties)
650
     * with current eTag request headers (if present).
651
     *
652
     * @param mixed        &$entryObject             entity resource for which etag
653
     *                                               needs to be checked
654
     * @param ResourceType &$resourceType            Resource type of the entry
655
     *                                               object
656
     * @param bool         &$needToSerializeResponse On return, this will contain
657
     *                                               True if response needs to be
658
     *                                               serialized, False otherwise
659
     * @param bool         $needToSerializeResponse
660
     *
661
     * @return string|null The ETag for the entry object if it has eTag properties
662
     *                     NULL otherwise
663
     */
664
    protected function compareETag(
665
        &$entryObject,
666
        ResourceType & $resourceType,
667
        &$needToSerializeResponse
668
    ) {
669
        $needToSerializeResponse = true;
670
        $eTag = null;
671
        $ifMatch = $this->getHost()->getRequestIfMatch();
672
        $ifNoneMatch = $this->getHost()->getRequestIfNoneMatch();
673
        if (is_null($entryObject)) {
674
            if (!is_null($ifMatch)) {
675
                throw ODataException::createPreConditionFailedError(
676
                    Messages::eTagNotAllowedForNonExistingResource()
677
                );
678
            }
679
680
            return null;
681
        }
682
683
        if ($this->getConfiguration()->getValidateETagHeader() && !$resourceType->hasETagProperties()) {
684
            if (!is_null($ifMatch) || !is_null($ifNoneMatch)) {
685
                // No eTag properties but request has eTag headers, bad request
686
                throw ODataException::createBadRequestError(
687
                    Messages::noETagPropertiesForType()
688
                );
689
            }
690
691
            // We need write the response but no eTag header
692
            return null;
693
        }
694
695
        if (!$this->getConfiguration()->getValidateETagHeader()) {
696
            // Configuration says do not validate ETag, so we will not write ETag header in the
697
            // response even though the requested resource support it
698
            return null;
699
        }
700
701
        if (is_null($ifMatch) && is_null($ifNoneMatch)) {
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

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

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

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

could be turned into

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

This is much more concise to read.

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

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

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

Loading history...
785
            // If eTag is made up of datetime or string properties then the above
786
            // IType::convertToOData will perform utf8 and url encode. But we don't
787
            // want this for eTag value.
788
            $eTag = urldecode(utf8_decode($eTag));
789
790
            return rtrim($eTag, ',');
791
        }
792
793
        return null;
794
    }
795
796
    /**
797
     * This function will perform the following operations:
798
     * (1) Invoke delegateRequestProcessing method to process the request based
799
     *     on request method (GET, PUT/MERGE, POST, DELETE)
800
     * (3) If the result of processing of request needs to be serialized as HTTP
801
     *     response body (e.g. GET request result in single resource or resource
802
     *     collection, successful POST operation for an entity need inserted
803
     *     entity to be serialized back etc..), Serialize the result by using
804
     *     'serializeReultForResponseBody' method
805
     *     Set the serialized result to
806
     *     WebOperationContext::Current()::OutgoingWebResponseContext::Stream.
807
     */
808
    protected function handleRequest2()
809
    {
810
    }
811
812
    /**
813
     * This method will perform the following operations:
814
     * (1) If request method is GET, then result is already there in the
815
     *     RequestDescription so simply return the RequestDescription
816
     * (2). If request method is for CDU
817
     *      (Create/Delete/Update - POST/DELETE/PUT-MERGE) hand
818
     *      over the responsibility to respective handlers. The handler
819
     *      methods are:
820
     *      (a) handlePOSTOperation() => POST
821
     *      (b) handlePUTOperation() => PUT/MERGE
822
     *      (c) handleDELETEOperation() => DELETE
823
     * (3). Check whether its required to write any result to the response
824
     *      body
825
     *      (a). Request method is GET
826
     *      (b). Request is a POST for adding NEW Entry
827
     *      (c). Request is a POST for adding Media Resource Stream
828
     *      (d). Request is a POST for adding a link
829
     *      (e). Request is a DELETE for deleting entry or relationship
830
     *      (f). Request is a PUT/MERGE for updating an entry
831
     *      (g). Request is a PUT for updating a link
832
     *     In case a, b and c we need to write the result to response body,
833
     *     for d, e, f and g no body content.
834
     *
835
     * @return RequestDescription|null Instance of RequestDescription with
836
     *                                 result to be write back Null if no result to write
837
     */
838
    protected function delegateRequestProcessing()
839
    {
840
    }
841
842
    /**
843
     * Serialize the result in the current request description using
844
     * appropriate odata writer (AtomODataWriter/JSONODataWriter).
845
     */
846
    protected function serializeResultForResponseBody()
847
    {
848
    }
849
850
    /**
851
     * Handle POST request.
852
     *
853
     *
854
     * @throws NotImplementedException
855
     */
856
    protected function handlePOSTOperation()
857
    {
858
    }
859
860
    /**
861
     * Handle PUT/MERGE request.
862
     *
863
     *
864
     * @throws NotImplementedException
865
     */
866
    protected function handlePUTOperation()
867
    {
868
    }
869
870
    /**
871
     * Handle DELETE request.
872
     *
873
     *
874
     * @throws NotImplementedException
875
     */
876
    protected function handleDELETEOperation()
877
    {
878
    }
879
}
880