Passed
Push — master ( 4ab488...bcfbc7 )
by Bálint
03:58
created

StreamProviderWrapper::getReadStreamUri()   A

Complexity

Conditions 5
Paths 8

Size

Total Lines 44
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

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

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

97
                /** @scrutinizer ignore-call */ 
98
                $stream = $this->_streamProvider->getReadStream2(

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
98
                    $entity,
99
                    $resourceStreamInfo,
100
                    $requestETag,
101
                    $checkETagForEquality,
102
                    $this->_service->getOperationContext()
103
                );
104
            }
105
106
            $this->_verifyContentTypeOrETagModified('IDSSP::getReadStream');
107
        } catch (ODataException $ex) {
108
            //Check for status code 304 (stream Not Modified)
109
            if ($ex->getStatusCode() == 304) {
110
                $eTag = $this->getStreamETag($entity, $resourceStreamInfo);
111
                if (!is_null($eTag)) {
0 ignored issues
show
introduced by
The condition is_null($eTag) is always false.
Loading history...
112
                    $this->_service->getHost()->setResponseETag($eTag);
113
                }
114
            }
115
            throw $ex;
116
        }
117
118
        if ($resourceStreamInfo == null) {
119
            // For default streams, we always expect getReadStream()
120
            // to return a non-null stream.
121
            if (is_null($stream)) {
122
                throw new InvalidOperationException(
123
                    Messages::streamProviderWrapperInvalidStreamFromGetReadStream()
124
                );
125
            }
126
        } else {
127
            // For named streams, getReadStream() can return null to indicate
128
            // that the stream has not been created.
129
            if (is_null($stream)) {
130
                // 204 == no content                
131
                $this->_service->getHost()->setResponseStatusCode(204);
132
            }
133
        }
134
135
        return $stream;
136
    }
137
138
    /**
139
     * Gets the IANA content type (aka media type) of the stream associated with 
140
     * the specified media resource.
141
     * 
142
     * @param object             $entity             The entity instance 
143
     *                                               (media resource) associated with
144
     *                                               the stream for which the content
145
     *                                               type is to be obtained.
146
     * @param ResourceStreamInfo $resourceStreamInfo This will be null if 
147
     *                                               media resource is MLE, 
148
     *                                               if media resource is named
149
     *                                               stream then will be the 
150
     *                                               ResourceStreamInfo instance 
151
     *                                               holding the details of 
152
     *                                               named stream.
153
     * 
154
     * @return string|null
155
     */
156
    public function getStreamContentType($entity, $resourceStreamInfo)
157
    {
158
        $contentType = null;
159
        $this->_saveContentTypeAndETag();
160
        if (is_null($resourceStreamInfo)) {
161
            $this->_loadAndValidateStreamProvider();
162
            $contentType = $this->_streamProvider->getStreamContentType(
163
                $entity,
164
                $this->_service->getOperationContext()
165
            );
166
            if (is_null($contentType)) {
0 ignored issues
show
introduced by
The condition is_null($contentType) is always false.
Loading history...
167
                throw new InvalidOperationException(
168
                    Messages::streamProviderWrapperGetStreamContentTypeReturnsEmptyOrNull()
169
                );
170
            }
171
        } else {
172
            $this->_loadAndValidateStreamProvider2();
173
            $contentType = $this->_streamProvider->getStreamContentType2(
0 ignored issues
show
Bug introduced by
The method getStreamContentType2() does not exist on POData\Providers\Stream\IStreamProvider. Did you maybe mean getStreamContentType()? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

173
            /** @scrutinizer ignore-call */ 
174
            $contentType = $this->_streamProvider->getStreamContentType2(

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
174
                $entity,
175
                $resourceStreamInfo,
176
                $this->_service->getOperationContext()
177
            );
178
        }
179
180
        $this->_verifyContentTypeOrETagModified('IDSSP::getStreamContentType');
181
        return $contentType;
182
    }
183
184
    /**
185
     * Get the ETag of the stream associated with the entity specified.
186
     * 
187
     * @param object             $entity             The entity instance 
188
     *                                               (media resource) associated
189
     *                                               with the stream for which 
190
     *                                               the etag is to be obtained.
191
     * @param ResourceStreamInfo $resourceStreamInfo This will be null if 
192
     *                                               media resource is MLE, 
193
     *                                               if media resource is named
194
     *                                               stream then will be the 
195
     *                                               ResourceStreamInfo
196
     *                                               instance holding the 
197
     *                                               details of named stream.
198
     * 
199
     * @throws InvalidOperationException
200
     * @return String Etag
201
     */
202
    public function getStreamETag($entity, $resourceStreamInfo)
203
    {
204
        $eTag = null;
205
        $this->_saveContentTypeAndETag();
206
        if (is_null($resourceStreamInfo)) {
207
            $this->_loadAndValidateStreamProvider();
208
            $eTag = $this->_streamProvider->getStreamETag(
209
                $entity,
210
                $this->_service->getOperationContext()
211
            );
212
        } else {
213
            $this->_loadAndValidateStreamProvider2();
214
            $eTag = $this->_streamProvider->getStreamETag2(
0 ignored issues
show
Bug introduced by
The method getStreamETag2() does not exist on POData\Providers\Stream\IStreamProvider. Did you maybe mean getStreamETag()? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

214
            /** @scrutinizer ignore-call */ 
215
            $eTag = $this->_streamProvider->getStreamETag2(

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
215
                $entity,
216
                $resourceStreamInfo,
217
                $this->_service->getOperationContext()
218
            );
219
        }
220
221
        $this->_verifyContentTypeOrETagModified('IDSSP::getStreamETag');
222
        if (!self::isETagValueValid($eTag, true)) {
223
            throw new InvalidOperationException(
224
                Messages::streamProviderWrapperGetStreamETagReturnedInvalidETagFormat()
225
            );
226
        }
227
228
        return $eTag;
229
    }
230
231
    /**
232
     * Gets the URI clients should use when making retrieve (ie. GET) requests 
233
     * to the stream.
234
     * 
235
     * @param object             $entity             The entity instance 
236
     *                                               associated with the
237
     *                                               stream for which a 
238
     *                                               read stream URI is to
239
     *                                               be obtained.
240
     * @param ResourceStreamInfo $resourceStreamInfo This will be null 
241
     *                                               if media resource
242
     *                                               is MLE, if media 
243
     *                                               resource is named
244
     *                                               stream then will be 
245
     *                                               the ResourceStreamInfo
246
     *                                               instance holding the 
247
     *                                               details of named stream.
248
     * @param string             $mediaLinkEntryUri  MLE uri.
249
     * 
250
     * @return string
251
     * 
252
     * @throws InvalidOperationException
253
     */
254
    public function getReadStreamUri($entity, $resourceStreamInfo, 
255
        $mediaLinkEntryUri
256
    ) {
257
        $readStreamUri = null;
258
        $this->_saveContentTypeAndETag();
259
        if (is_null($resourceStreamInfo)) {
260
            $this->_loadAndValidateStreamProvider();
261
            $readStreamUri = $this->_streamProvider->getReadStreamUri(
262
                $entity,
263
                $this->_service->getOperationContext()
264
            );
265
        } else {
266
            $this->_loadAndValidateStreamProvider2();
267
            $readStreamUri = $this->_streamProvider->getReadStreamUri2(
0 ignored issues
show
Bug introduced by
The method getReadStreamUri2() does not exist on POData\Providers\Stream\IStreamProvider. Did you maybe mean getReadStreamUri()? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

267
            /** @scrutinizer ignore-call */ 
268
            $readStreamUri = $this->_streamProvider->getReadStreamUri2(

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

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

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

446
                /** @scrutinizer ignore-call */ 
447
                $this->_streamProvider = $this->_service->getService('IStreamProvider2');
Loading history...
447
                if (!is_null($this->_streamProvider) && (!is_object($this->_streamProvider) || !$this->_streamProvider instanceof IStreamProvider2)) {
448
                    throw ODataException::createInternalServerError(
449
                        Messages::streamProviderWrapperInvalidStream2Instance()
450
                    ); 
451
                }
452
            }
453
454
            if (is_null($this->_streamProvider)) {
455
                $this->_streamProvider = $this->_service->getService('IStreamProvider');
456
                if (!is_null($this->_streamProvider) && (!is_object($this->_streamProvider) || !$this->_streamProvider instanceof IStreamProvider)) {
457
                    throw ODataException::createInternalServerError(
458
                        Messages::streamProviderWrapperInvalidStreamInstance()
459
                    );
460
                }
461
            }
462
        }
463
    }
464
465
    /**
466
     * Construct the default edit media uri from the given media link entry uri.
467
     * 
468
     * @param string             $mediaLinkEntryUri  Uri to the media link entry.
469
     * @param ResourceStreamInfo $resourceStreamInfo Stream info instance, if its
470
     *                                               null default stream is assumed.
471
     * 
472
     * @return string Uri to the media resource.
473
     */
474
    public function getDefaultStreamEditMediaUri($mediaLinkEntryUri, $resourceStreamInfo)
475
    {
476
        if (is_null($resourceStreamInfo)) {
477
            return rtrim($mediaLinkEntryUri, '/') . '/' . ODataConstants::URI_VALUE_SEGMENT;
478
        } else {
479
            return rtrim($mediaLinkEntryUri, '/') . '/' . ltrim($resourceStreamInfo->getName(), '/');
480
        }
481
    }
482
483
    /**
484
     * Save value of content type and etag headers before invoking implementor
485
     * methods.
486
     * 
487
     * @return void
488
     */
489
    private function _saveContentTypeAndETag()
490
    {
491
        $this->_responseContentType = $this->_service->getHost()->getResponseContentType();
492
        $this->_responseETag = $this->_service->getHost()->getResponseETag();
493
    }
494
495
    /**
496
     * Check whether implementor modified content type or etag header
497
     * if so throw InvalidOperationException.
498
     * 
499
     * @param string $methodName NAme of the method
500
     * 
501
     * @return void
502
     * 
503
     * @throws InvalidOperationException
504
     */
505
    private function _verifyContentTypeOrETagModified($methodName)
506
    {
507
        if ($this->_responseContentType !== $this->_service->getHost()->getResponseContentType()
508
            || $this->_responseETag !== $this->_service->getHost()->getResponseETag()
509
        ) {
510
            throw new InvalidOperationException(
511
                Messages::streamProviderWrapperMustNotSetContentTypeAndEtag($methodName)
512
            );
513
        }
514
    }
515
}
516