Passed
Push — master ( 5a82de...f5919d )
by Roeland
11:39 queued 12s
created

SeekableHttpStream::stream_set_option()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
nc 1
nop 3
dl 0
loc 2
rs 10
c 1
b 0
f 0
1
<?php
2
/**
3
 *
4
 * @copyright Copyright (c) 2020, Lukas Stabe ([email protected])
5
 *
6
 * @license GNU AGPL version 3 or any later version
7
 *
8
 * This program is free software: you can redistribute it and/or modify
9
 * it under the terms of the GNU Affero General Public License as
10
 * published by the Free Software Foundation, either version 3 of the
11
 * License, or (at your option) any later version.
12
 *
13
 * This program is distributed in the hope that it will be useful,
14
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16
 * GNU Affero General Public License for more details.
17
 *
18
 * You should have received a copy of the GNU Affero General Public License
19
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
20
 *
21
 */
22
23
namespace OC\Files\Stream;
24
25
use Icewind\Streams\File;
26
27
/**
28
 * A stream wrapper that uses http range requests to provide a seekable stream for http reading
29
 */
30
class SeekableHttpStream implements File {
31
	private const PROTOCOL = 'httpseek';
32
33
	private static $registered = false;
34
35
	/**
36
	 * Registers the stream wrapper using the `httpseek://` url scheme
37
	 * $return void
38
	 */
39
	private static function registerIfNeeded() {
40
		if (!self::$registered) {
41
			stream_wrapper_register(
42
				self::PROTOCOL,
43
				self::class
44
			);
45
			self::$registered = true;
46
		}
47
	}
48
49
	/**
50
	 * Open a readonly-seekable http stream
51
	 *
52
	 * The provided callback will be called with byte range and should return an http stream for the requested range
53
	 *
54
	 * @param callable $callback
55
	 * @return false|resource
56
	 */
57
	public static function open(callable $callback) {
58
		$context = stream_context_create([
59
			SeekableHttpStream::PROTOCOL => [
60
				'callback' => $callback
61
			],
62
		]);
63
64
		SeekableHttpStream::registerIfNeeded();
65
		return fopen(SeekableHttpStream::PROTOCOL . '://', 'r', false, $context);
66
	}
67
68
	/** @var resource */
69
	public $context;
70
71
	/** @var callable */
72
	private $openCallback;
73
74
	/** @var resource */
75
	private $current;
76
	/** @var int */
77
	private $offset = 0;
78
79
	private function reconnect(int $start) {
80
		$range = $start . '-';
81
		if ($this->current != null) {
82
			fclose($this->current);
83
		}
84
85
		$this->current = ($this->openCallback)($range);
86
87
		if ($this->current === false) {
88
			return false;
89
		}
90
91
		$responseHead = stream_get_meta_data($this->current)['wrapper_data'];
92
		$rangeHeaders = array_values(array_filter($responseHead, function ($v) {
93
			return preg_match('#^content-range:#i', $v) === 1;
94
		}));
95
		if (!$rangeHeaders) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $rangeHeaders of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
96
			return false;
97
		}
98
		$contentRange = $rangeHeaders[0];
99
100
		$content = trim(explode(':', $contentRange)[1]);
101
		$range = trim(explode(' ', $content)[1]);
102
		$begin = intval(explode('-', $range)[0]);
103
104
		if ($begin !== $start) {
105
			return false;
106
		}
107
108
		$this->offset = $begin;
109
110
		return true;
111
	}
112
113
	public function stream_open($path, $mode, $options, &$opened_path) {
114
		$options = stream_context_get_options($this->context)[self::PROTOCOL];
115
		$this->openCallback = $options['callback'];
116
117
		return $this->reconnect(0);
118
	}
119
120
	public function stream_read($count) {
121
		if (!$this->current) {
122
			return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the return type mandated by Icewind\Streams\File::stream_read() of string.

In the issue above, the returned value is violating the contract defined by the mentioned interface.

Let's take a look at an example:

interface HasName {
    /** @return string */
    public function getName();
}

class Name {
    public $name;
}

class User implements HasName {
    /** @return string|Name */
    public function getName() {
        return new Name('foo'); // This is a violation of the ``HasName`` interface
                                // which only allows a string value to be returned.
    }
}
Loading history...
123
		}
124
		$ret = fread($this->current, $count);
125
		$this->offset += strlen($ret);
126
		return $ret;
127
	}
128
129
	public function stream_seek($offset, $whence = SEEK_SET) {
130
		switch ($whence) {
131
			case SEEK_SET:
132
				if ($offset === $this->offset) {
133
					return true;
134
				}
135
				return $this->reconnect($offset);
136
			case SEEK_CUR:
137
				if ($offset === 0) {
138
					return true;
139
				}
140
				return $this->reconnect($this->offset + $offset);
141
			case SEEK_END:
142
				return false;
143
		}
144
		return false;
145
	}
146
147
	public function stream_tell() {
148
		return $this->offset;
149
	}
150
151
	public function stream_stat() {
152
		return fstat($this->current);
153
	}
154
155
	public function stream_eof() {
156
		return feof($this->current);
157
	}
158
159
	public function stream_close() {
160
		fclose($this->current);
161
	}
162
163
	public function stream_write($data) {
164
		return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the return type mandated by Icewind\Streams\File::stream_write() of integer.

In the issue above, the returned value is violating the contract defined by the mentioned interface.

Let's take a look at an example:

interface HasName {
    /** @return string */
    public function getName();
}

class Name {
    public $name;
}

class User implements HasName {
    /** @return string|Name */
    public function getName() {
        return new Name('foo'); // This is a violation of the ``HasName`` interface
                                // which only allows a string value to be returned.
    }
}
Loading history...
165
	}
166
167
	public function stream_set_option($option, $arg1, $arg2) {
168
		return false;
169
	}
170
171
	public function stream_truncate($size) {
172
		return false;
173
	}
174
175
	public function stream_lock($operation) {
176
		return false;
177
	}
178
179
	public function stream_flush() {
180
		return; //noop because readonly stream
181
	}
182
}
183