BaseService::getConfiguration()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
1
<?php
2
3
namespace POData;
4
5
use POData\Common\MimeTypes;
6
use POData\Common\Version;
7
use POData\OperationContext\HTTPRequestMethod;
8
use POData\Providers\Metadata\ResourceTypeKind;
9
use POData\Common\ErrorHandler;
10
use POData\Common\Messages;
11
use POData\Common\ODataException;
12
use POData\Common\ODataConstants;
13
use POData\Common\NotImplementedException;
14
use POData\Common\InvalidOperationException;
15
use POData\Common\HttpStatus;
16
use POData\Providers\Metadata\Type\IType;
17
use POData\Providers\ProvidersWrapper;
18
use POData\Providers\Stream\StreamProviderWrapper;
19
use POData\Configuration\ServiceConfiguration;
20
use POData\UriProcessor\UriProcessor;
21
use POData\UriProcessor\RequestDescription;
22
use POData\UriProcessor\ResourcePathProcessor\SegmentParser\TargetKind;
23
use POData\OperationContext\ServiceHost;
24
use POData\Providers\Metadata\ResourceType;
25
use POData\Providers\Metadata\Type\Binary;
26
use POData\ObjectModel\ObjectModelSerializer;
27
use POData\Writers\Atom\AtomODataWriter;
28
use POData\Writers\Json\JsonLightMetadataLevel;
29
use POData\Writers\Json\JsonLightODataWriter;
30
use POData\Writers\Json\JsonODataV1Writer;
31
use POData\Writers\Json\JsonODataV2Writer;
32
use POData\Writers\ResponseWriter;
33
34
use POData\Providers\Query\IQueryProvider;
35
use POData\Providers\Metadata\IMetadataProvider;
36
use POData\OperationContext\IOperationContext;
37
38
use POData\Writers\ODataWriterRegistry;
39
40
/**
41
 * Class BaseService
42
 *
43
 * The base class for all BaseService specific classes. This class implements
44
 * the following interfaces:
45
 *  (1) IRequestHandler
46
 *      Implementing this interface requires defining the function
47
 *      'handleRequest' that will be invoked by dispatcher
48
 *  (2) IService
49
 *      Force BaseService class to implement functions for custom
50
 *      data service providers
51
 *
52
 * @package POData
53
 */
54
abstract class BaseService implements IRequestHandler, IService
55
{
56
    /** 
57
     * The wrapper over IQueryProvider and IMetadataProvider implementations.
58
     * 
59
     * @var ProvidersWrapper
60
     */
61
    private $providersWrapper;
62
63
    /**
64
     * The wrapper over IStreamProvider implementation
65
     * 
66
     * @var StreamProviderWrapper
67
     */
68
    private $_streamProvider;
69
70
    /**
71
     * Hold reference to the ServiceHost instance created by dispatcher,
72
     * using this library can access headers and body of Http Request 
73
     * dispatcher received and the Http Response Dispatcher is going to send.
74
     * 
75
     * @var ServiceHost
76
     */
77
    private $_serviceHost;
78
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 ServiceConfiguration
86
     */
87
    private $config;
88
89
    /**
90
     * Gets reference to ServiceConfiguration instance so that
91
     * service specific rules defined by the developer can be 
92
     * accessed.
93
     * 
94
     * @return ServiceConfiguration
95
     */
96
    public function getConfiguration()
97
    {
98
        return $this->config;
99
    }
100
101
102
	//TODO: shouldn't we hide this from the interface..if we need it at all.
103
    /**
104
     * Get the wrapper over developer's IQueryProvider and IMetadataProvider implementation.
105
     * 
106
     * @return ProvidersWrapper
107
     */
108
    public function getProvidersWrapper()
109
    {
110
          return $this->providersWrapper;
111
    }
112
113
    /**
114
     * Gets reference to wrapper class instance over IDSSP implementation
115
     * 
116
     * @return StreamProviderWrapper
117
     */
118
    public function getStreamProviderWrapper()
119
    {
120
        return $this->_streamProvider;
121
    }
122
123
    /**
124
     * Get reference to the data service host instance.
125
     * 
126
     * @return ServiceHost
127
     */
128
    public function getHost()
129
    {
130
        return $this->_serviceHost;
131
    }
132
133
    /**
134
     * Sets the data service host instance.
135
     * 
136
     * @param ServiceHost $serviceHost The data service host instance.
137
     * 
138
     * @return void
139
     */
140
    public function setHost(ServiceHost $serviceHost)
141
    {
142
        $this->_serviceHost = $serviceHost;
143
    }
144
145
    /**
146
     * To get reference to operation context where we have direct access to
147
     * headers and body of Http Request we have received and the Http Response
148
     * We are going to send.
149
     * 
150
     * @return IOperationContext
151
     */
152
    public function getOperationContext()
153
    {
154
        return $this->_serviceHost->getOperationContext();
155
    }
156
157
    /**
158
     * Get reference to the wrapper over IStreamProvider or
159
     * IStreamProvider2 implementations.
160
     * 
161
     * @return StreamProviderWrapper
162
     */
163
    public function getStreamProvider()
164
    {
165
        if (is_null($this->_streamProvider)) {
166
            $this->_streamProvider = new StreamProviderWrapper();
167
            $this->_streamProvider->setService($this);
168
        }
169
170
        return $this->_streamProvider;
171
    }
172
173
    /**
174
     * Top-level handler invoked by Dispatcher against any request to this 
175
     * service. This method will hand over request processing task to other 
176
     * functions which process the request, set required headers and Response 
177
     * stream (if any in Atom/Json format) in 
178
     * WebOperationContext::Current()::OutgoingWebResponseContext.
179
     * Once this function returns, dispatcher uses global WebOperationContext 
180
     * to write out the request response to client.
181
     * This function will perform the following operations:
182
     * (1) Check whether the top level service class implements 
183
     *     IServiceProvider which means the service is a custom service, in 
184
     *     this case make sure the top level service class implements 
185
     *     IMetaDataProvider and IQueryProvider.
186
     *     These are the minimal interfaces that a custom service to be 
187
     *     implemented in order to expose its data as OData. Save reference to
188
     *     These interface implementations. 
189
     *     NOTE: Here we will ensure only providers for IDSQP and IDSMP. The 
190
     *     IDSSP will be ensured only when there is an GET request on MLE/Named 
191
     *     stream.
192
     *  
193
     * (2). Invoke 'Initialize' method of top level service for
194
     *      collecting the configuration rules set by the developer for this 
195
     *      service. 
196
     *  
197
     * (3). Invoke the Uri processor to process the request URI. The uri 
198
     *      processor will do the following:
199
     *      (a). Validate the request uri syntax using OData uri rules
200
     *      (b). Validate the request using metadata of this service
201
     *      (c). Parse the request uri and using, IQueryProvider
202
     *           implementation, fetches the resources pointed by the uri 
203
     *           if required
204
     *      (d). Build a RequestDescription which encapsulate everything 
205
     *           related to request uri (e.g. type of resource, result 
206
     *           etc...)
207
     * (3). Invoke handleRequest2 for further processing
208
     * 
209
     * @return void
210
     */
211
    public function handleRequest()
212
    {
213
        try {
214
            $this->createProviders();
215
            $this->_serviceHost->validateQueryParameters();
216
            $requestMethod = $this->getOperationContext()->incomingRequest()->getMethod();
217
            if ($requestMethod != HTTPRequestMethod::GET()) {
218
                throw ODataException::createNotImplementedError(Messages::onlyReadSupport($requestMethod));
219
            }          
220
221
            $uriProcessor = UriProcessor::process($this);
222
            $request = $uriProcessor->getRequest();
223
            $this->serializeResult($request, $uriProcessor);
224
        } catch (\Exception $exception) {
225
            ErrorHandler::handleException($exception, $this);
226
            // Return to dispatcher for writing serialized exception
227
            return;
228
        }
229
    }
230
231
	/**
232
	 * @return IQueryProvider
233
	 */
234
	public abstract function getQueryProvider();
0 ignored issues
show
Coding Style introduced by
The abstract declaration must precede the visibility declaration
Loading history...
235
236
	/**
237
	 * @return IMetadataProvider
238
	 */
239
	public abstract function getMetadataProvider();
0 ignored issues
show
Coding Style introduced by
The abstract declaration must precede the visibility declaration
Loading history...
240
241
	/**
242
	 * @return \POData\Providers\Stream\IStreamProvider
243
	 */
244
	public abstract function getStreamProviderX();
0 ignored issues
show
Coding Style introduced by
The abstract declaration must precede the visibility declaration
Loading history...
245
246
247
	/** @var  ODataWriterRegistry */
248
	private $writerRegistry;
249
250
	/**
251
	 * Returns the ODataWriterRegistry to use when writing the response to a service document or resource request
252
	 * @return ODataWriterRegistry
253
	 */
254
	public function getODataWriterRegistry()
255
	{
256
		return $this->writerRegistry;
257
	}
258
259
260
    /**
261
     * This method will query and validates for IMetadataProvider and IQueryProvider implementations, invokes
262
     * BaseService::Initialize to initialize service specific policies.
263
     *
264
     * 
265
     * @throws ODataException
266
     */
267
    protected function createProviders()
268
    { 
269
270
        $metadataProvider = $this->getMetadataProvider();
271
        if (is_null($metadataProvider)) {
272
            throw ODataException::createInternalServerError(Messages::providersWrapperNull());
273
        }
274
    
275
        if (!is_object($metadataProvider) || !$metadataProvider instanceof IMetadataProvider) {
276
            throw ODataException::createInternalServerError(Messages::invalidMetadataInstance());
277
        }
278
279
        $queryProvider = $this->getQueryProvider();
280
281
        if (is_null($queryProvider)) {
282
            throw ODataException::createInternalServerError(Messages::providersWrapperNull());
283
        }
284
285
        if (!is_object($queryProvider)) {
286
            throw ODataException::createInternalServerError(Messages::invalidQueryInstance());
287
        }
288
289
        if (!$queryProvider instanceof IQueryProvider) {
290
            throw ODataException::createInternalServerError(Messages::invalidQueryInstance());
291
        }
292
293
        $this->config = new ServiceConfiguration($metadataProvider);
294
        $this->providersWrapper = new ProvidersWrapper(
295
            $metadataProvider, 
296
            $queryProvider, 
297
            $this->config
298
        );
299
300
        
301
        $this->initialize($this->config);
302
303
	    //TODO: this seems like a bad spot to do this
304
	    $this->writerRegistry = new ODataWriterRegistry();
305
	    $this->registerWriters();
306
    }
307
308
	//TODO: i don't want this to be public..but it's the only way to test it right now...
309
	public function registerWriters()
310
	{
311
		$registry = $this->getODataWriterRegistry();
312
		$serviceVersion = $this->getConfiguration()->getMaxDataServiceVersion();
313
		$serviceURI = $this->getHost()->getAbsoluteServiceUri()->getUrlAsString();
314
315
		//We always register the v1 stuff
316
		$registry->register(new JsonODataV1Writer());
317
		$registry->register(new AtomODataWriter($serviceURI));
318
319
		if($serviceVersion->compare(Version::v2()) > -1){
320
			$registry->register(new JsonODataV2Writer());
321
		}
322
323
		if($serviceVersion->compare(Version::v3()) > -1){
324
			$registry->register(new JsonLightODataWriter(JsonLightMetadataLevel::NONE(), $serviceURI));
325
			$registry->register(new JsonLightODataWriter(JsonLightMetadataLevel::MINIMAL(), $serviceURI));
326
			$registry->register(new JsonLightODataWriter(JsonLightMetadataLevel::FULL(), $serviceURI));
327
		}
328
	}
329
330
    /**
331
     * Serialize the requested resource.
332
     * 
333
     * @param RequestDescription $request The description of the request  submitted by the client.
334
     * @param UriProcessor $uriProcessor Reference to the uri processor.
335
     * 
336
     * @return void
337
     */
338
    protected function serializeResult(RequestDescription $request, UriProcessor $uriProcessor) {
339
        $isETagHeaderAllowed = $request->isETagHeaderAllowed();
340
        if ($this->config->getValidateETagHeader() && !$isETagHeaderAllowed) {
341
            if (!is_null($this->_serviceHost->getRequestIfMatch())
342
                ||!is_null($this->_serviceHost->getRequestIfNoneMatch())
343
            ) {
344
                throw ODataException::createBadRequestError(
345
                    Messages::eTagCannotBeSpecified($this->getHost()->getAbsoluteRequestUri()->getUrlAsString())
346
                );
347
            }
348
        }
349
350
        $responseContentType = self::getResponseContentType($request, $uriProcessor, $this);
351
352
	    if (is_null($responseContentType) && $request->getTargetKind() != TargetKind::MEDIA_RESOURCE()) {
353
		    //the responseContentType can ONLY be null if it's a stream (media resource) and that stream is storing null as the content type
354
		    throw new ODataException( Messages::unsupportedMediaType(), 415 );
355
	    }
356
357
	    $odataModelInstance = null;
358
        $hasResponseBody = true;
359
        // Execution required at this point if request target to any resource other than
360
        //
361
        // (1) media resource - For Media resource 'getResponseContentType' already performed execution as it needs to know the mime type of the stream
362
        // (2) metadata - internal resource
363
        // (3) service directory - internal resource
364
        if ($request->needExecution()) {
365
            $uriProcessor->execute();
366
            $objectModelSerializer = new ObjectModelSerializer($this, $request);
367
            if (!$request->isSingleResult()) {
368
                // Code path for collection (feed or links)
369
                $entryObjects = $request->getTargetResult();
370
                self::assert(
371
                    !is_null($entryObjects) && is_array($entryObjects), 
372
                    '!is_null($entryObjects) && is_array($entryObjects)'
373
                );
374
                // If related resource set is empty for an entry then we should 
375
                // not throw error instead response must be empty feed or empty links
376
                if ($request->isLinkUri()) {
377
                    $odataModelInstance = $objectModelSerializer->writeUrlElements($entryObjects);
378
                    self::assert(
379
                        $odataModelInstance instanceof \POData\ObjectModel\ODataURLCollection, 
380
                        '$odataModelInstance instanceof ODataURLCollection'
381
                    );
382
                } else {
383
                    $odataModelInstance = $objectModelSerializer->writeTopLevelElements($entryObjects);
384
                    self::assert(
385
                        $odataModelInstance instanceof \POData\ObjectModel\ODataFeed, 
386
                        '$odataModelInstance instanceof ODataFeed'
387
                    );
388
                }
389
            } else {
390
                // Code path for entry, complex, bag, resource reference link, 
391
                // primitive type or primitive value
392
                $result = $request->getTargetResult();
393
                $requestTargetKind = $request->getTargetKind();
394
                if ($request->isLinkUri()) {
395
                    // In the query 'Orders(1245)/$links/Customer', the targeted
396
                    // Customer might be null
397
                    if (is_null($result)) {
398
						throw ODataException::createResourceNotFoundError(
399
                            $request->getIdentifier()
400
                        );
401
                    }
402
403
                    $odataModelInstance = $objectModelSerializer->writeUrlElement($result);
404
                } else if ($requestTargetKind == TargetKind::RESOURCE()) {
405
                    if (!is_null($this->_serviceHost->getRequestIfMatch())
406
                        && !is_null($this->_serviceHost->getRequestIfNoneMatch())
407
                    ) {
408
						throw ODataException::createBadRequestError(
409
                            Messages::bothIfMatchAndIfNoneMatchHeaderSpecified()
410
                        );
411
                    }
412
                    // handle entry resource
413
                    $needToSerializeResponse = true;
414
                    $targetResourceType = $request->getTargetResourceType();
415
                    $eTag = $this->compareETag(
416
                        $result, 
417
                        $targetResourceType, 
0 ignored issues
show
Bug introduced by
It seems like $targetResourceType defined by $request->getTargetResourceType() on line 414 can be null; however, POData\BaseService::compareETag() 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...
418
                        $needToSerializeResponse
419
                    );
420
421
                    if ($needToSerializeResponse) {
422
                        if (is_null($result)) {
423
                            // In the query 'Orders(1245)/Customer', the targeted
424
                            // Customer might be null
425
                            // set status code to 204 => 'No Content'
426
                            $this->_serviceHost->setResponseStatusCode(HttpStatus::CODE_NOCONTENT);
427
                            $hasResponseBody = false;
428
                        } else {
429
                            $odataModelInstance = $objectModelSerializer->writeTopLevelElement($result);
430
                        }
431
                    } else {
432
                        // Resource is not modified so set status code 
433
                        // 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...
434
                        $this->_serviceHost->setResponseStatusCode(HttpStatus::CODE_NOT_MODIFIED);
435
                        $hasResponseBody = false;
436
                    }
437
438
                    // if resource has eTagProperty then eTag header needs to written
439
                    if (!is_null($eTag)) {
440
                        $this->_serviceHost->setResponseETag($eTag);
441
                    }
442
                } else if ($requestTargetKind == TargetKind::COMPLEX_OBJECT()) {
443
444
	                $odataModelInstance = $objectModelSerializer->writeTopLevelComplexObject(
445
                        $result, 
446
                        $request->getProjectedProperty()->getName(),
447
	                    $request->getTargetResourceType()
0 ignored issues
show
Bug introduced by
$request->getTargetResourceType() cannot be passed to writetoplevelcomplexobject() as the parameter $resourceType expects a reference.
Loading history...
Bug introduced by
It seems like $request->getTargetResourceType() can be null; however, writeTopLevelComplexObject() 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...
448
	                );
449
                } else if ($requestTargetKind == TargetKind::BAG()) {
450
                    $odataModelInstance = $objectModelSerializer->writeTopLevelBagObject(
451
                        $result, 
452
                        $request->getProjectedProperty()->getName(),
453
	                    $request->getTargetResourceType(),
0 ignored issues
show
Bug introduced by
$request->getTargetResourceType() cannot be passed to writetoplevelbagobject() as the parameter $resourceType expects a reference.
Loading history...
Bug introduced by
It seems like $request->getTargetResourceType() can be null; however, writeTopLevelBagObject() 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...
454
                        $odataModelInstance
0 ignored issues
show
Unused Code introduced by
The call to ObjectModelSerializer::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...
455
                    );
456
                } else if ($requestTargetKind == TargetKind::PRIMITIVE()) {
457
                    $odataModelInstance = $objectModelSerializer->writeTopLevelPrimitive(
458
                        $result,
459
	                    $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...
460
                        $odataModelInstance
0 ignored issues
show
Unused Code introduced by
The call to ObjectModelSerializer::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...
461
                    );
462
                } else if ($requestTargetKind == TargetKind::PRIMITIVE_VALUE()) {
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...
463
                    // Code path for primitive value (Since its primitve no need for
464
                    // object model serialization) 
465
                    // 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...
466
                    // Employees(1)/Photo/$value => binary stream
467
                    // Customers/$count => string
468
                } else {
469
                    self::assert(false, 'Unexpected resource target kind');
470
                }
471
            }
472
        }
473
474
        //Note: Response content type can be null for named stream
475
        if ($hasResponseBody && !is_null($responseContentType)) {
476
            if ($request->getTargetKind() != TargetKind::MEDIA_RESOURCE() && $responseContentType != MimeTypes::MIME_APPLICATION_OCTETSTREAM) {
477
	            //append charset for everything except:
478
	            //stream resources as they have their own content type
479
	            //binary properties (they content type will be App Octet for those...is this a good way? we could also decide based upon the projected property
480
	            //
481
                $responseContentType .= ';charset=utf-8';
482
            }
483
        }
484
485
        if ($hasResponseBody) {
486
            ResponseWriter::write(
487
                $this, 
488
                $request,
489
                $odataModelInstance, 
490
                $responseContentType
491
            );
492
        }
493
    }
494
495
    /**
496
     * Gets the response format for the requested resource.
497
     * 
498
     * @param RequestDescription $request The request submitted by client and it's execution result.
499
     * @param UriProcessor $uriProcessor The reference to the UriProcessor.
500
     * @param IService $service Reference to the service implementation instance
501
     *
502
     * @return string the response content-type, a null value means the requested resource
503
     * is named stream and IDSSP2::getStreamContentType returned null
504
     * 
505
     * @throws ODataException, HttpHeaderFailure
506
     */
507
    public static function getResponseContentType(
508
	    RequestDescription $request,
509
        UriProcessor $uriProcessor,
510
        IService $service
511
    ) {
512
513
        // The Accept request-header field specifies media types which are acceptable for the response
514
515
        $host = $service->getHost();
516
	    $requestAcceptText = $host->getRequestAccept();
517
        $requestVersion = $request->getResponseVersion();
518
519
	    //if the $format header is present it overrides the accepts header
520
	    $format = $host->getQueryStringItem(ODataConstants::HTTPQUERY_STRING_FORMAT);
521
	    if(!is_null($format)){
522
523
            //There's a strange edge case..if application/json is supplied and it's V3
524
            if($format == MimeTypes::MIME_APPLICATION_JSON && $requestVersion == Version::v3()){
525
                //then it's actual minimalmetadata
526
                //TODO: should this be done with the header text too?
527
                $format = MimeTypes::MIME_APPLICATION_JSON_MINIMAL_META;
528
            }
529
530
		    $requestAcceptText = ServiceHost::translateFormatToMime($requestVersion, $format);
531
	    }
532
533
534
535
536
	    //The response format can be dictated by the target resource kind. IE a $value will be different then expected
537
	    //getTargetKind doesn't deal with link resources directly and this can change things
538
	    $targetKind = $request->isLinkUri() ? TargetKind::LINK() : $request->getTargetKind();
539
540
	    switch($targetKind){
541
		    case TargetKind::METADATA():
542
			    return HttpProcessUtility::selectMimeType(
543
				    $requestAcceptText,
544
				    array(MimeTypes::MIME_APPLICATION_XML)
545
			    );
546
547 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...
548
			    return HttpProcessUtility::selectMimeType(
549
				    $requestAcceptText,
550
				    array(
551
					    MimeTypes::MIME_APPLICATION_XML,
552
					    MimeTypes::MIME_APPLICATION_ATOMSERVICE,
553
					    MimeTypes::MIME_APPLICATION_JSON,
554
                        MimeTypes::MIME_APPLICATION_JSON_FULL_META,
555
                        MimeTypes::MIME_APPLICATION_JSON_NO_META,
556
                        MimeTypes::MIME_APPLICATION_JSON_MINIMAL_META,
557
					    MimeTypes::MIME_APPLICATION_JSON_VERBOSE,
558
559
                    )
560
			    );
561
562
		    case TargetKind::PRIMITIVE_VALUE():
563
			    $supportedResponseMimeTypes = array(MimeTypes::MIME_TEXTPLAIN);
564
565
			    if ($request->getIdentifier() != '$count') {
566
				    $projectedProperty = $request->getProjectedProperty();
567
				    self::assert(
568
					    !is_null($projectedProperty),
569
					    '!is_null($projectedProperty)'
570
				    );
571
				    $type = $projectedProperty->getInstanceType();
572
				    self::assert(
573
					    !is_null($type) && $type instanceof IType,
574
					    '!is_null($type) && $type instanceof IType'
575
				    );
576
				    if ($type instanceof Binary) {
577
					    $supportedResponseMimeTypes = array(MimeTypes::MIME_APPLICATION_OCTETSTREAM);
578
				    }
579
			    }
580
581
			    return HttpProcessUtility::selectMimeType(
582
				    $requestAcceptText,
583
				    $supportedResponseMimeTypes
584
			    );
585
586
		    case TargetKind::PRIMITIVE():
587
		    case TargetKind::COMPLEX_OBJECT():
588
		    case TargetKind::BAG():
589 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...
590
			    return HttpProcessUtility::selectMimeType(
591
				    $requestAcceptText,
592
				    array(
593
					    MimeTypes::MIME_APPLICATION_XML,
594
					    MimeTypes::MIME_TEXTXML,
595
					    MimeTypes::MIME_APPLICATION_JSON,
596
                        MimeTypes::MIME_APPLICATION_JSON_FULL_META,
597
                        MimeTypes::MIME_APPLICATION_JSON_NO_META,
598
                        MimeTypes::MIME_APPLICATION_JSON_MINIMAL_META,
599
					    MimeTypes::MIME_APPLICATION_JSON_VERBOSE,
600
				    )
601
			    );
602
603 View Code Duplication
		    case TargetKind::RESOURCE():
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...
604
			    return HttpProcessUtility::selectMimeType(
605
				    $requestAcceptText,
606
				    array(
607
					    MimeTypes::MIME_APPLICATION_ATOM,
608
					    MimeTypes::MIME_APPLICATION_JSON,
609
                        MimeTypes::MIME_APPLICATION_JSON_FULL_META,
610
                        MimeTypes::MIME_APPLICATION_JSON_NO_META,
611
                        MimeTypes::MIME_APPLICATION_JSON_MINIMAL_META,
612
					    MimeTypes::MIME_APPLICATION_JSON_VERBOSE,
613
				    )
614
			    );
615
616
		    case TargetKind::MEDIA_RESOURCE():
617
			    if (!$request->isNamedStream() && !$request->getTargetResourceType()->isMediaLinkEntry()){
618
					throw ODataException::createBadRequestError(
619
					    Messages::badRequestInvalidUriForMediaResource(
620
						    $host->getAbsoluteRequestUri()->getUrlAsString()
621
					    )
622
				    );
623
			    }
624
625
			    $uriProcessor->execute();
626
			    $request->setExecuted();
627
			    // DSSW::getStreamContentType can throw error in 2 cases
628
			    // 1. If the required stream implementation not found
629
			    // 2. If IDSSP::getStreamContentType returns NULL for MLE
630
			    $responseContentType = $service->getStreamProviderWrapper()
631
				    ->getStreamContentType(
632
					    $request->getTargetResult(),
633
					    $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...
634
				    );
635
636
637
			    // Note StreamWrapper::getStreamContentType can return NULL if the requested named stream has not
638
			    // yet been uploaded. But for an MLE if IDSSP::getStreamContentType returns NULL then StreamWrapper will throw error
639
			    if (!is_null($responseContentType)) {
640
				    $responseContentType = HttpProcessUtility::selectMimeType(
641
					    $requestAcceptText,
642
					    array($responseContentType)
643
				    );
644
			    }
645
646
			    return $responseContentType;
647
	    }
648
649
650
	    //If we got here, we just don't know what it is...
651
        throw new ODataException( Messages::unsupportedMediaType(), 415 );
652
653
    }
654
655
656
657
    /**
658
     * For the given entry object compare it's eTag (if it has eTag properties)
659
     * with current eTag request headers (if it present).
660
     * 
661
     * @param mixed        &$entryObject             entity resource for which etag 
662
     *                                               needs to be checked.
663
     * @param ResourceType &$resourceType            Resource type of the entry 
664
     *                                               object.
665
     * @param boolean      &$needToSerializeResponse On return, this will contain 
666
     *                                               True if response needs to be
667
     *                                               serialized, False otherwise.
668
     *                                              
669
     * @return string|null The ETag for the entry object if it has eTag properties 
670
     *                     NULL otherwise.
671
     */
672
    protected function compareETag(&$entryObject, ResourceType &$resourceType, 
673
        &$needToSerializeResponse
674
    ) {      
675
        $needToSerializeResponse = true;
676
        $eTag = null;
677
        $ifMatch = $this->_serviceHost->getRequestIfMatch();
678
        $ifNoneMatch = $this->_serviceHost->getRequestIfNoneMatch();
679
        if (is_null($entryObject)) {
680
            if (!is_null($ifMatch)) {
681
                throw ODataException::createPreConditionFailedError(
682
                    Messages::eTagNotAllowedForNonExistingResource()
683
                ); 
684
            }
685
686
            return null;
687
        }
688
     
689
        if ($this->config->getValidateETagHeader() && !$resourceType->hasETagProperties()) {
690
            if (!is_null($ifMatch) || !is_null($ifNoneMatch)) {
691
                // No eTag properties but request has eTag headers, bad request
692
				throw ODataException::createBadRequestError(
693
                    Messages::noETagPropertiesForType()
694
                );
695
            }
696
697
            // We need write the response but no eTag header 
698
            return null;
699
        }
700
701
        if (!$this->config->getValidateETagHeader()) {
702
            // Configuration says do not validate ETag so we will not write ETag header in the 
703
            // response even though the requested resource support it
704
            return null;
705
        }
706
707
        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...
708
            // No request eTag header, we need to write the response 
709
            // and eTag header 
710
        } else if (strcmp($ifMatch, '*') == 0) {
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...
711
            // If-Match:* => we need to write the response and eTag header 
712
        } else if (strcmp($ifNoneMatch, '*') == 0) {
713
            // if-None-Match:* => Do not write the response (304 not modified), 
714
            // but write eTag header
715
            $needToSerializeResponse = false;
716
        } else {
717
            $eTag = $this->getETagForEntry($entryObject, $resourceType);
718
            // Note: The following code for attaching the prefix W\"
719
            // and the suffix " can be done in getETagForEntry function
720
            // but that is causing an issue in Linux env where the 
721
            // firefix browser is unable to parse the ETag in this case.
722
            // Need to follow up PHP core devs for this. 
723
            $eTag = ODataConstants::HTTP_WEAK_ETAG_PREFIX . $eTag . '"';
724
            if (!is_null($ifMatch)) {
725
                if (strcmp($eTag, $ifMatch) != 0) {
726
                    // Requested If-Match value does not match with current 
727
                    // eTag Value then pre-condition error
728
                    // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
729
                    throw ODataException::createPreConditionFailedError(
730
                        Messages::eTagValueDoesNotMatch()
731
                    );
732
                }
733
            } else if (strcmp($eTag, $ifNoneMatch) == 0) {
734
                //304 not modified, but in write eTag header
735
                $needToSerializeResponse = false;
736
            }
737
        }
738
739
        if (is_null($eTag)) {
740
            $eTag = $this->getETagForEntry($entryObject, $resourceType);
741
            // Note: The following code for attaching the prefix W\"
742
            // and the suffix " can be done in getETagForEntry function
743
            // but that is causing an issue in Linux env where the 
744
            // firefix browser is unable to parse the ETag in this case.
745
            // Need to follow up PHP core devs for this. 
746
            $eTag = ODataConstants::HTTP_WEAK_ETAG_PREFIX . $eTag . '"';
747
        }
748
749
        return $eTag;
750
    }
751
752
    /**
753
     * Returns the etag for the given resource.
754
     * Note: This function will not add W\" prefix and " suffix, its callers
755
     * repsonsability.
756
     *
757
     * @param mixed        &$entryObject  Resource for which etag value needs to 
758
     *                                    be returned
759
     * @param ResourceType &$resourceType Resource type of the $entryObject
760
     * 
761
     * @return string|null ETag value for the given resource (with values encoded 
762
     *                     for use in a URI) there are etag properties, NULL if 
763
     *                     there is no etag property.
764
     */
765
    protected function getETagForEntry(&$entryObject, ResourceType &$resourceType)
766
    {
767
        $eTag = null;
768
        $comma = null;
769
        foreach ($resourceType->getETagProperties() as $eTagProperty) {
770
            $type = $eTagProperty->getInstanceType();
771
            self::assert(
772
                !is_null($type) && $type instanceof IType,
773
                '!is_null($type) && $type instanceof IType'
774
            );
775
      
776
            $value = null; 
777
            try {
778
779
	            //TODO #88...also this seems like dupe work
780
                $reflectionProperty  = new \ReflectionProperty($entryObject, $eTagProperty->getName() );
781
                $value = $reflectionProperty->getValue($entryObject);
782
            } catch (\ReflectionException $reflectionException) {
783
                throw ODataException::createInternalServerError(
784
                    Messages::failedToAccessProperty($eTagProperty->getName(), $resourceType->getName() )
785
                );
786
            }
787
788 View Code Duplication
            if (is_null($value)) {
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...
789
                $eTag = $eTag . $comma. 'null';
790
            } else {
791
                $eTag = $eTag . $comma . $type->convertToOData($value);
0 ignored issues
show
Bug introduced by
The method convertToOData does only exist in POData\Providers\Metadata\Type\IType, but not in ReflectionClass.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
792
            }
793
794
            $comma = ',';
795
        }
796
797 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...
798
            // If eTag is made up of datetime or string properties then the above
799
            // IType::convertToOData will perform utf8 and url encode. But we don't
800
            // want this for eTag value.
801
            $eTag = urldecode(utf8_decode($eTag));
802
            return rtrim($eTag, ',');
803
        }
804
805
        return null;
806
    }
807
808
    /**
809
     * This function will perform the following operations:
810
     * (1) Invoke delegateRequestProcessing method to process the request based 
811
     *     on request method (GET, PUT/MERGE, POST, DELETE)
812
     * (3) If the result of processing of request needs to be serialized as HTTP 
813
     *     response body (e.g. GET request result in single resource or resource 
814
     *     collection, successful POST operation for an entity need inserted 
815
     *     entity to be serialized back etc..), Serialize the result by using 
816
     *     'serializeReultForResponseBody' method
817
     *     Set the serialized result to 
818
     *     WebOperationContext::Current()::OutgoingWebResponseContext::Stream.
819
     *     
820
     *     @return void
821
     */
822
    protected function handleRequest2()
823
    {
824
    }
825
826
    /**
827
     * This method will perform the following operations:
828
     * (1) If request method is GET, then result is already there in the 
829
     *     RequestDescription so simply return the RequestDescription
830
     * (2). If request method is for CDU 
831
     *      (Create/Delete/Update - POST/DELETE/PUT-MERGE) hand
832
     *      over the responsibility to respective handlers. The handler 
833
     *      methods are:
834
     *      (a) handlePOSTOperation() => POST
835
     *      (b) handlePUTOperation() => PUT/MERGE
836
     *      (c) handleDELETEOperation() => DELETE
837
     * (3). Check whether its required to write any result to the response 
838
     *      body 
839
     *      (a). Request method is GET
840
     *      (b). Request is a POST for adding NEW Entry
841
     *      (c). Request is a POST for adding Media Resource Stream
842
     *      (d). Request is a POST for adding a link
843
     *      (e). Request is a DELETE for deleting entry or relationship
844
     *      (f). Request is a PUT/MERGE for updating an entry
845
     *      (g). Request is a PUT for updating a link
846
     *     In case a, b and c we need to write the result to response body, 
847
     *     for d, e, f and g no body content.
848
     * 
849
     * @return RequestDescription|null Instance of RequestDescription with
850
     *         result to be write back Null if no result to write.
851
     */
852
    protected function delegateRequestProcessing()
853
    {
854
    }
855
856
    /**
857
     * Serialize the result in the current request description using 
858
     * appropriate odata writer (AtomODataWriter/JSONODataWriter)
859
     * 
860
     * @return void
861
     * 
862
     */
863
    protected function serializeReultForResponseBody()
864
    {
865
    }
866
867
    /**
868
     * Handle POST request.
869
     * 
870
     * @return void
871
     * 
872
     * @throws NotImplementedException
873
     */
874
    protected function handlePOSTOperation()
875
    {
876
    }
877
878
    /**
879
     * Handle PUT/MERGE request.
880
     * 
881
     * @return void
882
     * 
883
     * @throws NotImplementedException
884
     */
885
    protected function handlePUTOperation()
886
    {
887
    }
888
889
    /**
890
     * Handle DELETE request.
891
     * 
892
     * @return void
893
     * 
894
     * @throws NotImplementedException
895
     */
896
    protected function handleDELETEOperation()
897
    {
898
    }
899
900
    /**
901
     * Assert that the given condition is true.
902
     * 
903
     * @param boolean $condition         The condtion to check.
904
     * @param string  $conditionAsString Message to show if assertion fails.
905
     * 
906
     * @return void
907
     * 
908
     * @throws InvalidOperationException
909
     */
910
    protected static function assert($condition, $conditionAsString)
911
    {
912
        if (!$condition) {
913
            throw new InvalidOperationException(
914
                "Unexpected state, expecting $conditionAsString"
915
            );
916
        }
917
    }
918
}