Completed
Push — master ( 0751c2...fd3704 )
by Thomas
09:32
created

AssemblyStream::loadContext()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 14
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 11
nc 3
nop 1
dl 0
loc 14
rs 9.2
c 0
b 0
f 0
1
<?php
2
/**
3
 * @author Lukas Reschke <[email protected]>
4
 * @author Thomas Müller <[email protected]>
5
 *
6
 * @copyright Copyright (c) 2016, ownCloud GmbH.
7
 * @license AGPL-3.0
8
 *
9
 * This code is free software: you can redistribute it and/or modify
10
 * it under the terms of the GNU Affero General Public License, version 3,
11
 * as published by the Free Software Foundation.
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, version 3,
19
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
20
 *
21
 */
22
namespace OCA\DAV\Upload;
23
24
use Sabre\DAV\IFile;
25
26
/**
27
 * Class AssemblyStream
28
 *
29
 * The assembly stream is a virtual stream that wraps multiple chunks.
30
 * Reading from the stream transparently accessed the underlying chunks and
31
 * give a representation as if they were already merged together.
32
 *
33
 * @package OCA\DAV\Upload
34
 */
35
class AssemblyStream implements \Icewind\Streams\File {
36
37
	/** @var resource */
38
	private $context;
39
40
	/** @var IFile[] */
41
	private $nodes;
42
43
	/** @var int */
44
	private $pos = 0;
45
46
	/** @var array */
47
	private $sortedNodes;
48
49
	/** @var int */
50
	private $size;
51
52
	/** @var resource */
53
	private $currentStream = null;
54
55
	/**
56
	 * @param string $path
57
	 * @param string $mode
58
	 * @param int $options
59
	 * @param string &$opened_path
60
	 * @return bool
61
	 */
62
	public function stream_open($path, $mode, $options, &$opened_path) {
63
		$this->loadContext('assembly');
64
65
		// sort the nodes
66
		$nodes = $this->nodes;
67
		// http://stackoverflow.com/a/10985500
68
		@usort($nodes, function(IFile $a, IFile $b) {
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
69
			return strcmp($a->getName(), $b->getName());
70
		});
71
		$this->nodes = $nodes;
72
73
		// build additional information
74
		$this->sortedNodes = [];
75
		$start = 0;
76
		foreach($this->nodes as $node) {
77
			$size = $node->getSize();
78
			$name = $node->getName();
79
			$this->sortedNodes[$name] = ['node' => $node, 'start' => $start, 'end' => $start + $size];
80
			$start += $size;
81
			$this->size = $start;
82
		}
83
		return true;
84
	}
85
86
	/**
87
	 * @param string $offset
88
	 * @param int $whence
89
	 * @return bool
90
	 */
91
	public function stream_seek($offset, $whence = SEEK_SET) {
92
		return false;
93
	}
94
95
	/**
96
	 * @return int
97
	 */
98
	public function stream_tell() {
99
		return $this->pos;
100
	}
101
102
	/**
103
	 * @param int $count
104
	 * @return string
105
	 */
106
	public function stream_read($count) {
107
		do {
108
			if ($this->currentStream === null) {
109
				list($node, $posInNode) = $this->getNodeForPosition($this->pos);
110
				if (is_null($node)) {
111
					// reached last node, no more data
112
					return '';
113
				}
114
				$this->currentStream = $this->getStream($node);
115
				fseek($this->currentStream, $posInNode);
116
			}
117
118
			$data = fread($this->currentStream, $count);
119
			// isset is faster than strlen
120
			if (isset($data[$count - 1])) {
121
				// we read the full count
122
				$read = $count;
123
			} else {
124
				// reaching end of stream, which happens less often so strlen is ok
125
				$read = strlen($data);
126
			}
127
128
			if (feof($this->currentStream)) {
129
				fclose($this->currentStream);
130
				$this->currentNode = null;
0 ignored issues
show
Bug introduced by
The property currentNode does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
131
				$this->currentStream = null;
132
			}
133
			// if no data read, try again with the next node because
134
			// returning empty data can make the caller think there is no more
135
			// data left to read
136
		} while ($read === 0);
137
138
		// update position
139
		$this->pos += $read;
140
		return $data;
141
	}
142
143
	/**
144
	 * @param string $data
145
	 * @return int
146
	 */
147
	public function stream_write($data) {
148
		return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type declared by the interface Icewind\Streams\File::stream_write of type integer.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
149
	}
150
151
	/**
152
	 * @param int $option
153
	 * @param int $arg1
154
	 * @param int $arg2
155
	 * @return bool
156
	 */
157
	public function stream_set_option($option, $arg1, $arg2) {
158
		return false;
159
	}
160
161
	/**
162
	 * @param int $size
163
	 * @return bool
164
	 */
165
	public function stream_truncate($size) {
166
		return false;
167
	}
168
169
	/**
170
	 * @return array
171
	 */
172
	public function stream_stat() {
173
		return [];
174
	}
175
176
	/**
177
	 * @param int $operation
178
	 * @return bool
179
	 */
180
	public function stream_lock($operation) {
181
		return false;
182
	}
183
184
	/**
185
	 * @return bool
186
	 */
187
	public function stream_flush() {
188
		return false;
189
	}
190
191
	/**
192
	 * @return bool
193
	 */
194
	public function stream_eof() {
195
		return $this->pos >= $this->size;
196
	}
197
198
	/**
199
	 * @return bool
200
	 */
201
	public function stream_close() {
202
		return true;
203
	}
204
205
206
	/**
207
	 * Load the source from the stream context and return the context options
208
	 *
209
	 * @param string $name
210
	 * @return array
211
	 * @throws \Exception
212
	 */
213
	protected function loadContext($name) {
214
		$context = stream_context_get_options($this->context);
215
		if (isset($context[$name])) {
216
			$context = $context[$name];
217
		} else {
218
			throw new \BadMethodCallException('Invalid context, "' . $name . '" options not set');
219
		}
220
		if (isset($context['nodes']) and is_array($context['nodes'])) {
221
			$this->nodes = $context['nodes'];
222
		} else {
223
			throw new \BadMethodCallException('Invalid context, nodes not set');
224
		}
225
		return $context;
226
	}
227
228
	/**
229
	 * @param IFile[] $nodes
230
	 * @return resource
231
	 *
232
	 * @throws \BadMethodCallException
233
	 */
234
	public static function wrap(array $nodes) {
235
		$context = stream_context_create([
236
			'assembly' => [
237
				'nodes' => $nodes]
238
		]);
239
		stream_wrapper_register('assembly', '\OCA\DAV\Upload\AssemblyStream');
240
		try {
241
			$wrapped = fopen('assembly://', 'r', null, $context);
242
		} catch (\BadMethodCallException $e) {
243
			stream_wrapper_unregister('assembly');
244
			throw $e;
245
		}
246
		stream_wrapper_unregister('assembly');
247
		return $wrapped;
248
	}
249
250
	/**
251
	 * @param $pos
252
	 * @return IFile | null
253
	 */
254
	private function getNodeForPosition($pos) {
255
		foreach($this->sortedNodes as $node) {
256
			if ($pos >= $node['start'] && $pos < $node['end']) {
257
				return [$node['node'], $pos - $node['start']];
258
			}
259
		}
260
		return null;
261
	}
262
263
	/**
264
	 * @param IFile $node
265
	 * @return resource
266
	 */
267
	private function getStream(IFile $node) {
268
		$data = $node->get();
269
		if (is_resource($data)) {
270
			return $data;
271
		}
272
273
		return fopen('data://text/plain,' . $data,'r');
274
	}
275
276
}
277