Completed
Push — master ( d03abb...fe3c08 )
by Alex
01:26
created

StreamProviderWrapper::_loadStreamProvider()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 11
rs 9.2
c 0
b 0
f 0
cc 4
eloc 6
nc 3
nop 0
1
<?php
2
3
namespace POData\Providers\Stream;
4
5
use POData\IService;
6
use POData\Providers\Metadata\ResourceStreamInfo;
7
use POData\OperationContext\ServiceHost;
8
use POData\Common\Version;
9
use POData\Common\ODataException;
10
use POData\Common\ODataConstants;
11
use POData\Common\Messages;
12
use POData\Common\InvalidOperationException;
13
14
/**
15
 * Class StreamProviderWrapper Wrapper over IDSSP and IDSSP2 implementations.
16
 */
17
class StreamProviderWrapper
18
{
19
    /**
20
     * Holds reference to the data service instance.
21
     *
22
     * @var IService
23
     */
24
    private $_service;
25
26
    /**
27
     * Holds reference to the implementation of IStreamProvider or IStreamProvider2.
28
     *
29
     *
30
     * @var IStreamProvider|IStreamProvider2
31
     */
32
    private $_streamProvider;
33
34
    /**
35
     * Used to check whether interface implementation modified response content type header or not.
36
     *
37
     *
38
     * @var string|null
39
     */
40
    private $_responseContentType;
41
42
    /**
43
     * Used to check whether interface implementation modified ETag header or not.
44
     *
45
     * @var string|null
46
     */
47
    private $_responseETag;
48
49
    /**
50
     * To set reference to the data service instance.
51
     *
52
     * @param IService $service The data service instance
53
     */
54
    public function setService(IService $service)
55
    {
56
        $this->_service = $service;
57
    }
58
59
    /**
60
     * To get stream associated with the given media resource.
61
     *
62
     * @param object             $entity             The media resource
63
     * @param ResourceStreamInfo $resourceStreamInfo This will be null if media
64
     *                                               resource is MLE, if media
65
     *                                               resource is named
66
     *                                               stream then will be the
67
     *                                               ResourceStreamInfo instance
68
     *                                               holding the details of
69
     *                                               named stream
70
     *
71
     * @return string|null
72
     */
73
    public function getReadStream($entity, $resourceStreamInfo)
74
    {
75
        $requestETag = null;
76
        $checkETagForEquality = null;
77
        $this->_getETagFromHeaders($requestETag, $checkETagForEquality);
78
        $stream = null;
79
        try {
80
            $this->_saveContentTypeAndETag();
81
            $opContext = $this->_service->getOperationContext();
82
            if (is_null($resourceStreamInfo)) {
83
                $this->_loadAndValidateStreamProvider();
84
                $stream = $this->_streamProvider->getReadStream(
85
                    $entity,
86
                    $requestETag,
87
                    $checkETagForEquality,
88
                    $opContext
89
                );
90
            } else {
91
                $this->_loadAndValidateStreamProvider2();
92
                $stream = $this->_streamProvider->getReadStream2(
0 ignored issues
show
Bug introduced by
The method getReadStream2 does only exist in POData\Providers\Stream\IStreamProvider2, but not in POData\Providers\Stream\IStreamProvider.

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...
93
                    $entity,
94
                    $resourceStreamInfo,
95
                    $requestETag,
96
                    $checkETagForEquality,
97
                    $opContext
98
                );
99
            }
100
101
            $this->_verifyContentTypeOrETagModified('IDSSP::getReadStream');
102
        } catch (ODataException $ex) {
103
            //Check for status code 304 (stream Not Modified)
104
            if (304 == $ex->getStatusCode()) {
105
                $eTag = $this->getStreamETag($entity, $resourceStreamInfo);
0 ignored issues
show
Bug introduced by
It seems like $resourceStreamInfo can be null; however, getStreamETag() 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...
106
                if (!is_null($eTag)) {
107
                    $this->_service->getHost()->setResponseETag($eTag);
108
                }
109
            }
110
            throw $ex;
111
        }
112
113
        if (null == $stream) {
114
            if (null == $resourceStreamInfo) {
115
                // For default streams, we always expect getReadStream()
116
                // to return a non-null stream.  If we reach here, blow up.
117
                throw new InvalidOperationException(
118
                    Messages::streamProviderWrapperInvalidStreamFromGetReadStream()
119
                );
120
            } else {
121
                // For named streams, getReadStream() can return null to indicate
122
                // that the stream has not been created.
123
                // 204 == no content
124
                $this->_service->getHost()->setResponseStatusCode(204);
125
            }
126
        }
127
128
        return $stream;
129
    }
130
131
    /**
132
     * Gets the IANA content type (aka media type) of the stream associated with
133
     * the specified media resource.
134
     *
135
     * @param object             $entity             The entity instance
136
     *                                               (media resource) associated with
137
     *                                               the stream for which the content
138
     *                                               type is to be obtained
139
     * @param ResourceStreamInfo $resourceStreamInfo This will be null if
140
     *                                               media resource is MLE,
141
     *                                               if media resource is named
142
     *                                               stream then will be the
143
     *                                               ResourceStreamInfo instance
144
     *                                               holding the details of
145
     *                                               named stream
146
     *
147
     * @return string|null
148
     */
149 View Code Duplication
    public function getStreamContentType($entity, $resourceStreamInfo)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
150
    {
151
        $contentType = null;
0 ignored issues
show
Unused Code introduced by
$contentType is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
152
        $this->_saveContentTypeAndETag();
153
        $opContext = $this->_service->getOperationContext();
154
        if (is_null($resourceStreamInfo)) {
155
            $this->_loadAndValidateStreamProvider();
156
            $contentType = $this->_streamProvider->getStreamContentType($entity, $opContext);
157
            if (is_null($contentType)) {
158
                throw new InvalidOperationException(
159
                    Messages::streamProviderWrapperGetStreamContentTypeReturnsEmptyOrNull()
160
                );
161
            }
162
        } else {
163
            $this->_loadAndValidateStreamProvider2();
164
            assert($this->_streamProvider instanceof IStreamProvider2);
165
            $contentType = $this->_streamProvider->getStreamContentType2($entity, $resourceStreamInfo, $opContext);
166
        }
167
168
        $this->_verifyContentTypeOrETagModified('IDSSP::getStreamContentType');
169
170
        return $contentType;
171
    }
172
173
    /**
174
     * Get the ETag of the stream associated with the entity specified.
175
     *
176
     * @param object             $entity             The entity instance
177
     *                                               (media resource) associated
178
     *                                               with the stream for which
179
     *                                               the etag is to be obtained
180
     * @param ResourceStreamInfo $resourceStreamInfo This will be null if
181
     *                                               media resource is MLE,
182
     *                                               if media resource is named
183
     *                                               stream then will be the
184
     *                                               ResourceStreamInfo
185
     *                                               instance holding the
186
     *                                               details of named stream
187
     *
188
     * @throws InvalidOperationException
189
     *
190
     * @return string Etag
191
     */
192 View Code Duplication
    public function getStreamETag($entity, $resourceStreamInfo)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
193
    {
194
        $eTag = null;
0 ignored issues
show
Unused Code introduced by
$eTag is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
195
        $this->_saveContentTypeAndETag();
196
        $opContext = $this->_service->getOperationContext();
197
        if (is_null($resourceStreamInfo)) {
198
            $this->_loadAndValidateStreamProvider();
199
            $eTag = $this->_streamProvider->getStreamETag($entity, $opContext);
200
        } else {
201
            $this->_loadAndValidateStreamProvider2();
202
            assert($this->_streamProvider instanceof IStreamProvider2);
203
            $eTag = $this->_streamProvider->getStreamETag2($entity, $resourceStreamInfo, $opContext);
204
        }
205
206
        $this->_verifyContentTypeOrETagModified('IDSSP::getStreamETag');
207
        if (!self::isETagValueValid($eTag, true)) {
208
            throw new InvalidOperationException(
209
                Messages::streamProviderWrapperGetStreamETagReturnedInvalidETagFormat()
210
            );
211
        }
212
213
        return $eTag;
214
    }
215
216
    /**
217
     * Gets the URI clients should use when making retrieve (ie. GET) requests
218
     * to the stream.
219
     *
220
     * @param object             $entity             The entity instance
221
     *                                               associated with the
222
     *                                               stream for which a
223
     *                                               read stream URI is to
224
     *                                               be obtained
225
     * @param ResourceStreamInfo $resourceStreamInfo This will be null
226
     *                                               if media resource
227
     *                                               is MLE, if media
228
     *                                               resource is named
229
     *                                               stream then will be
230
     *                                               the ResourceStreamInfo
231
     *                                               instance holding the
232
     *                                               details of named stream
233
     * @param string             $mediaLinkEntryUri  MLE uri
234
     *
235
     * @return string
236
     *
237
     * @throws InvalidOperationException
238
     */
239
    public function getReadStreamUri(
240
        $entity,
241
        $resourceStreamInfo,
242
        $mediaLinkEntryUri
243
    ) {
244
        $readStreamUri = null;
0 ignored issues
show
Unused Code introduced by
$readStreamUri is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
245
        $this->_saveContentTypeAndETag();
246
        $opContext = $this->_service->getOperationContext();
247
        if (is_null($resourceStreamInfo)) {
248
            $this->_loadAndValidateStreamProvider();
249
            $readStreamUri = $this->_streamProvider->getReadStreamUri($entity, $opContext);
250
        } else {
251
            $this->_loadAndValidateStreamProvider2();
252
            assert($this->_streamProvider instanceof IStreamProvider2);
253
            $readStreamUri = $this->_streamProvider->getReadStreamUri2($entity, $resourceStreamInfo, $opContext);
254
        }
255
256
        $this->_verifyContentTypeOrETagModified('IDSSP::getReadStreamUri');
257
        if (!is_null($readStreamUri)) {
258
            try {
259
                new \POData\Common\Url($readStreamUri);
260
            } catch (\POData\Common\UrlFormatException $ex) {
261
                throw new InvalidOperationException(
262
                    Messages::streamProviderWrapperGetReadStreamUriMustReturnAbsoluteUriOrNull()
263
                );
264
            }
265
        } else {
266
            if (is_null($resourceStreamInfo)) {
267
                // For MLEs the content src attribute is
268
                //required so we cannot return null.
269
                $readStreamUri = $this->getDefaultStreamEditMediaUri($mediaLinkEntryUri, null);
270
            }
271
        }
272
273
        // Note if readStreamUri is null, the self link for the
274
        // named stream will be omitted.
275
        return $readStreamUri;
276
    }
277
278
    /**
279
     * Checks the given value is a valid eTag.
280
     *
281
     * @param string $etag            eTag to validate
282
     * @param bool   $allowStrongEtag True if strong eTag is allowed
283
     *                                False otherwise
284
     *
285
     * @return bool
286
     */
287
    public static function isETagValueValid($etag, $allowStrongEtag)
288
    {
289
        if (is_null($etag) || '*' === $etag) {
290
            return true;
291
        }
292
293
        // HTTP RFC 2616, section 3.11:
294
        //   entity-tag = [ weak ] opaque-tag
295
        //   weak       = "W/"
296
        //   opaque-tag = quoted-string
297
        $etagValueStartIndex = 1;
298
        $eTagLength = strlen($etag);
299
        $isLastCharDubQuotes = ('"' == $etag[$eTagLength - 1]);
300
301
        if (0 === strpos($etag, 'W/"') && $isLastCharDubQuotes) {
302
            $etagValueStartIndex = 3;
303
        } elseif (!$allowStrongEtag || '"' != $etag[0] || !$isLastCharDubQuotes) {
304
            return false;
305
        }
306
307
        for ($i = $etagValueStartIndex; $i < $eTagLength - 1; ++$i) {
308
            // Format of etag looks something like: W/"etag property values"
309
            // or "strong etag value" according to HTTP RFC 2616, if someone
310
            // wants to specify more than 1 etag value, then need to specify
311
            // something like this: W/"etag values", W/"etag values", ...
312
            // To make sure only one etag is specified, we need to ensure
313
            // that if " is part of the key value, it needs to be escaped.
314
            if ('"' == $etag[$i]) {
315
                return false;
316
            }
317
        }
318
319
        return true;
320
    }
321
322
    /**
323
     * Get ETag header value from request header.
324
     *
325
     * @param mixed &$eTag                 On return, this parameter will hold
326
     *                                     value of IfMatch or IfNoneMatch
327
     *                                     header, if this header is absent then
328
     *                                     this parameter will hold NULL
329
     * @param mixed &$checkETagForEquality On return, this parameter will hold
330
     *                                     true if IfMatch is present, false if
331
     *                                     IfNoneMatch header is present, null
332
     *                                     otherwise
333
     */
334
    private function _getETagFromHeaders(&$eTag, &$checkETagForEquality)
335
    {
336
        $dataServiceHost = $this->_service->getHost();
337
        //TODO Do check for mutual exclusion of RequestIfMatch and
338
        //RequestIfNoneMatch in ServiceHost
339
        $eTag = $dataServiceHost->getRequestIfMatch();
340
        if (!is_null($eTag)) {
341
            $checkETagForEquality = true;
342
343
            return;
344
        }
345
346
        $eTag = $dataServiceHost->getRequestIfNoneMatch();
347
        if (!is_null($eTag)) {
348
            $checkETagForEquality = false;
349
350
            return;
351
        }
352
353
        $checkETagForEquality = null;
354
    }
355
356
    /**
357
     * Validates that an implementation of IStreamProvider exists and
358
     * load it.
359
     *
360
     *
361
     * @throws ODataException
362
     */
363
    private function _loadAndValidateStreamProvider()
364
    {
365
        if (is_null($this->_streamProvider)) {
366
367
            $this->_loadStreamProvider();
368
            if (is_null($this->_streamProvider)) {
369
                throw ODataException::createInternalServerError(
370
                    Messages::streamProviderWrapperMustImplementIStreamProviderToSupportStreaming()
371
                );
372
            }
373
        }
374
    }
375
376
    /**
377
     * Validates that an implementation of IStreamProvider2 exists and
378
     * load it.
379
     *
380
     *
381
     * @throws ODataException
382
     */
383
    private function _loadAndValidateStreamProvider2()
384
    {
385
        $maxServiceVersion = $this->_service->getConfiguration()->getMaxDataServiceVersion();
386
        if ($maxServiceVersion->compare(new Version(3, 0)) < 0) {
387
            throw ODataException::createInternalServerError(
388
                Messages::streamProviderWrapperMaxProtocolVersionMustBeV3OrAboveToSupportNamedStreams()
389
            );
390
        }
391
392
        if (is_null($this->_streamProvider)) {
393
            $this->_loadStreamProvider();
394
            if (is_null($this->_streamProvider)) {
395
                throw ODataException::createInternalServerError(
396
                    Messages::streamProviderWrapperMustImplementStreamProvider2ToSupportNamedStreams()
397
                );
398
            } elseif (!$this->_streamProvider instanceof IStreamProvider2) {
399
                throw ODataException::createInternalServerError(
400
                    Messages::streamProviderWrapperInvalidStream2Instance()
401
                );
402
            }
403
        }
404
    }
405
406
    /**
407
     * Ask data service to load stream provider instance.
408
     *
409
     *
410
     * @throws ODataException
411
     */
412
    private function _loadStreamProvider()
413
    {
414
        if (is_null($this->_streamProvider)) {
415
            $this->_streamProvider = $this->_service->getStreamProviderX();
416
            if (!is_null($this->_streamProvider) && (!$this->_streamProvider instanceof IStreamProvider2)) {
417
                throw ODataException::createInternalServerError(
418
                    Messages::streamProviderWrapperInvalidStream2Instance()
419
                );
420
            }
421
        }
422
    }
423
424
    /**
425
     * Construct the default edit media uri from the given media link entry uri.
426
     *
427
     * @param string                    $mediaLinkEntryUri  Uri to the media link entry
428
     * @param ResourceStreamInfo|null   $resourceStreamInfo Stream info instance, if its
429
     *                                                      null default stream is assumed
430
     *
431
     * @return string Uri to the media resource
432
     */
433
    public function getDefaultStreamEditMediaUri($mediaLinkEntryUri, $resourceStreamInfo)
434
    {
435
        $base = rtrim($mediaLinkEntryUri, '/') . '/';
436
        $end = (null == $resourceStreamInfo) ? ODataConstants::URI_VALUE_SEGMENT
437
            : ltrim($resourceStreamInfo->getName(), '/');
438
        return $base . $end;
439
    }
440
441
    /**
442
     * Save value of content type and etag headers before invoking implementor
443
     * methods.
444
     */
445
    private function _saveContentTypeAndETag()
446
    {
447
        $this->_responseContentType = $this->_service->getHost()->getResponseContentType();
448
        $this->_responseETag = $this->_service->getHost()->getResponseETag();
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->_service->getHost()->getResponseETag() of type object<POData\OperationContext\Web\reference> is incompatible with the declared type string|null of property $_responseETag.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
449
    }
450
451
    /**
452
     * Check whether implementor modified content type or etag header
453
     * if so throw InvalidOperationException.
454
     *
455
     * @param string $methodName NAme of the method
456
     *
457
     * @throws InvalidOperationException
458
     */
459
    private function _verifyContentTypeOrETagModified($methodName)
460
    {
461
        if ($this->_responseContentType !== $this->_service->getHost()->getResponseContentType()
462
            || $this->_responseETag !== $this->_service->getHost()->getResponseETag()
463
        ) {
464
            throw new InvalidOperationException(
465
                Messages::streamProviderWrapperMustNotSetContentTypeAndEtag($methodName)
466
            );
467
        }
468
    }
469
}
470