StreamWrapper::limit_filename()   A
last analyzed

Complexity

Conditions 5
Paths 6

Size

Total Lines 20
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 10
nc 6
nop 1
dl 0
loc 20
rs 9.6111
c 0
b 0
f 0
1
<?php
2
/**
3
 * EGroupware API: VFS - new DB based VFS stream wrapper
4
 *
5
 * @link http://www.egroupware.org
6
 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
7
 * @package api
8
 * @subpackage vfs
9
 * @author Ralf Becker <RalfBecker-AT-outdoor-training.de>
10
 * @copyright (c) 2008-16 by Ralf Becker <RalfBecker-AT-outdoor-training.de>
11
 * @version $Id$
12
 */
13
14
namespace EGroupware\Api\Vfs\Sqlfs;
15
16
use EGroupware\Api\Vfs;
17
use EGroupware\Api;
18
19
/**
20
 * EGroupware API: VFS - new DB based VFS stream wrapper
21
 *
22
 * The sqlfs stream wrapper has 2 operation modi:
23
 * - content of files is stored in the filesystem (eGW's files_dir) (default)
24
 * - content of files is stored as BLOB in the DB (can be enabled by mounting sqlfs://...?storage=db)
25
 *   please note the current (php5.2.6) problems:
26
 *   a) retriving files via streams does NOT work for PDO_mysql (bindColum(,,\PDO::PARAM_LOB) does NOT work, string returned)
27
 * 		(there's a workaround implemented, but it requires to allocate memory for the whole file!)
28
 *   b) uploading/writing files > 1M fail on PDOStatement::execute() (setting \PDO::MYSQL_ATTR_MAX_BUFFER_SIZE does NOT help)
29
 *      (not sure if that's a bug in PDO/PDO_mysql or an accepted limitation)
30
 *
31
 * I use the PDO DB interface, as it allows to access BLOB's as streams (avoiding to hold them complete in memory).
32
 *
33
 * The stream wrapper interface is according to the docu on php.net
34
 *
35
 * @link http://www.php.net/manual/en/function.stream-wrapper-register.php
36
 */
37
class StreamWrapper extends Api\Db\Pdo implements Vfs\StreamWrapperIface
38
{
39
	/**
40
	 * Mime type of directories, the old vfs uses 'Directory', while eg. WebDAV uses 'httpd/unix-directory'
41
	 */
42
	const DIR_MIME_TYPE = 'httpd/unix-directory';
43
	/**
44
	 * Mime type for symlinks
45
	 */
46
	const SYMLINK_MIME_TYPE = 'application/x-symlink';
47
	/**
48
	 * Scheme / protocoll used for this stream-wrapper
49
	 */
50
	const SCHEME = 'sqlfs';
51
	/**
52
	 * Does url_stat returns a mime type, or has it to be determined otherwise (string with attribute name)
53
	 */
54
	const STAT_RETURN_MIME_TYPE = 'mime';
55
	/**
56
	 * Our tablename
57
	 */
58
	const TABLE = 'egw_sqlfs';
59
	/**
60
	 * Name of our property table
61
	 */
62
	const PROPS_TABLE = 'egw_sqlfs_props';
63
	/**
64
	 * mode-bits, which have to be set for files
65
	 */
66
	const MODE_FILE = 0100000;
67
	/**
68
	 * mode-bits, which have to be set for directories
69
	 */
70
	const MODE_DIR =   040000;
71
	/**
72
	 * mode-bits, which have to be set for links
73
	 */
74
	const MODE_LINK = 0120000;
75
	/**
76
	 * How much should be logged to the apache error-log
77
	 *
78
	 * 0 = Nothing
79
	 * 1 = only errors
80
	 * 2 = all function calls and errors (contains passwords too!)
81
	 * 3 = log line numbers in sql statements
82
	 */
83
	const LOG_LEVEL = 1;
84
85
	/**
86
	 * We store the content in the DB (no versioning)
87
	 */
88
	const STORE2DB = 1;
89
	/**
90
	 * We store the content in the filesystem (egw_info/server/files_dir) (no versioning)
91
	 */
92
	const STORE2FS = 2;
93
	/**
94
	 * default for operation, change that if you want to test with STORE2DB atm
95
	 */
96
	const DEFAULT_OPERATION = self::STORE2FS;
97
98
	/**
99
	 * operation mode of the opened file
100
	 *
101
	 * @var int
102
	 */
103
	protected $operation = self::DEFAULT_OPERATION;
104
105
	/**
106
	 * optional context param when opening the stream, null if no context passed
107
	 *
108
	 * @var mixed
109
	 */
110
	var $context;
111
112
	/**
113
	 * Path off the file opened by stream_open
114
	 *
115
	 * @var string
116
	 */
117
	protected $opened_path;
118
	/**
119
	 * Mode of the file opened by stream_open
120
	 *
121
	 * @var int
122
	 */
123
	protected $opened_mode;
124
	/**
125
	 * Stream of the opened file, either from the DB via PDO or the filesystem
126
	 *
127
	 * @var resource
128
	 */
129
	protected $opened_stream;
130
	/**
131
	 * fs_id of opened file
132
	 *
133
	 * @var int
134
	 */
135
	protected $opened_fs_id;
136
	/**
137
	 * Cache containing stat-infos from previous url_stat calls AND dir_opendir calls
138
	 *
139
	 * It's values are the columns read from the DB (fs_*), not the ones returned by url_stat!
140
	 *
141
	 * @var array $path => info-array pairs
142
	 */
143
	static protected $stat_cache = array();
144
	/**
145
	 * Array with filenames of dir opened with dir_opendir
146
	 *
147
	 * @var array
148
	 */
149
	protected $opened_dir;
150
151
	/**
152
	 * Extra columns added since the intitial introduction of sqlfs
153
	 *
154
	 * Can be set to empty, so get queries running on old versions of sqlfs, eg. for schema updates
155
	 *
156
	 * @var string;
157
	 */
158
	static public $extra_columns = ',fs_link';
159
160
	/**
161
	 * @var array $overwrite_new =null if set create new file with values overwriten by the given ones
162
	 */
163
	protected $overwrite_new;
164
165
	/**
166
	 * Clears our stat-cache
167
	 *
168
	 * Normaly not necessary, as it is automatically cleared/updated, UNLESS Vfs::$user changes!
169
	 *
170
	 * @param string $path ='/'
171
	 */
172
	public static function clearstatcache($path='/')
173
	{
174
		//error_log(__METHOD__."('$path')");
175
		unset($path);	// not used
176
177
		self::$stat_cache = array();
178
179
		Api\Cache::setSession(self::EACL_APPNAME, 'extended_acl', self::$extended_acl = null);
180
	}
181
182
	/**
183
	 * This method is called immediately after your stream object is created.
184
	 *
185
	 * @param string $url URL that was passed to fopen() and that this object is expected to retrieve
186
	 * @param string $mode mode used to open the file, as detailed for fopen()
187
	 * @param int $options additional flags set by the streams API (or'ed together):
188
	 * - STREAM_USE_PATH      If path is relative, search for the resource using the include_path.
189
	 * - STREAM_REPORT_ERRORS If this flag is set, you are responsible for raising errors using trigger_error() during opening of the stream.
190
	 *                        If this flag is not set, you should not raise any errors.
191
	 * @param string &$opened_path full path of the file/resource, if the open was successfull and STREAM_USE_PATH was set
192
	 * @return boolean true if the ressource was opened successful, otherwise false
193
	 */
194
	function stream_open ($url, $mode, $options, &$opened_path)
195
	{
196
		if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url,$mode,$options)");
197
198
		$path = Vfs::parse_url($url,PHP_URL_PATH);
199
		$this->operation = self::url2operation($url);
200
		$dir = Vfs::dirname($url);
201
202
		$this->opened_path = $opened_path = $path;
203
		$this->opened_mode = $mode = str_replace('b','',$mode);	// we are always binary, like every Linux system
0 ignored issues
show
Documentation Bug introduced by
The property $opened_mode was declared of type integer, but $mode = str_replace('b', '', $mode) is of type string. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
204
		$this->opened_stream = null;
205
206
		parse_str(parse_url($url, PHP_URL_QUERY), $this->dir_url_params);
0 ignored issues
show
Bug Best Practice introduced by
The property dir_url_params does not exist on EGroupware\Api\Vfs\Sqlfs\StreamWrapper. Did you maybe forget to declare it?
Loading history...
207
208
		if (!is_null($this->overwrite_new) || !($stat = $this->url_stat($path,STREAM_URL_STAT_QUIET)) || $mode[0] == 'x')	// file not found or file should NOT exist
0 ignored issues
show
introduced by
The condition is_null($this->overwrite_new) is always false.
Loading history...
209
		{
210
			if (!$dir || $mode[0] == 'r' ||	// does $mode require the file to exist (r,r+)
211
				$mode[0] == 'x' && $stat ||	// or file should not exist, but does
212
				!($dir_stat=$this->url_stat($dir,STREAM_URL_STAT_QUIET)) ||	// or parent dir does not exist																																			create it
213
				!Vfs::check_access($dir,Vfs::WRITABLE,$dir_stat))	// or we are not allowed to 																																			create it
214
			{
215
				self::_remove_password($url);
216
				if (self::LOG_LEVEL) error_log(__METHOD__."($url,$mode,$options) file does not exist or can not be created!");
217
				if (($options & STREAM_REPORT_ERRORS))
218
				{
219
					trigger_error(__METHOD__."($url,$mode,$options) file does not exist or can not be created!",E_USER_WARNING);
220
				}
221
				$this->opened_stream = $this->opened_path = $this->opened_mode = null;
222
				return false;
223
			}
224
			// new file --> create it in the DB
225
			$new_file = true;
226
			$query = 'INSERT INTO '.self::TABLE.' (fs_name,fs_dir,fs_mode,fs_uid,fs_gid,fs_created,fs_modified,fs_creator,fs_mime,fs_size,fs_active'.
227
				') VALUES (:fs_name,:fs_dir,:fs_mode,:fs_uid,:fs_gid,:fs_created,:fs_modified,:fs_creator,:fs_mime,:fs_size,:fs_active)';
228
			if (self::LOG_LEVEL > 2) $query = '/* '.__METHOD__.': '.__LINE__.' */ '.$query;
229
			$stmt = self::$pdo->prepare($query);
230
			$values = array(
231
				'fs_name' => self::limit_filename(Vfs::basename($path)),
232
				'fs_dir'  => $dir_stat['ino'],
233
				// we use the mode of the dir, so files in group dirs stay accessible by all members
234
				'fs_mode' => $dir_stat['mode'] & 0666,
235
				// for the uid we use the uid of the dir if not 0=root or the current user otherwise
236
				'fs_uid'  => $dir_stat['uid'] ? $dir_stat['uid'] : Vfs::$user,
237
				// we allways use the group of the dir
238
				'fs_gid'  => $dir_stat['gid'],
239
				'fs_created'  => self::_pdo_timestamp(time()),
240
				'fs_modified' => self::_pdo_timestamp(time()),
241
				'fs_creator'  => Vfs::$user,
242
				'fs_mime'     => 'application/octet-stream',	// required NOT NULL!
243
				'fs_size'     => 0,
244
				'fs_active'   => self::_pdo_boolean(true),
245
			);
246
			if ($this->overwrite_new) $values = array_merge($values, $this->overwrite_new);
247
			if (!$stmt->execute($values) || !($this->opened_fs_id = self::$pdo->lastInsertId('egw_sqlfs_fs_id_seq')))
0 ignored issues
show
Documentation Bug introduced by
The property $opened_fs_id was declared of type integer, but self::pdo->lastInsertId('egw_sqlfs_fs_id_seq') is of type string. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
248
			{
249
				$this->opened_stream = $this->opened_path = $this->opened_mode = null;
250
				if (self::LOG_LEVEL) error_log(__METHOD__."($url,$mode,$options) execute() failed: ".self::$pdo->errorInfo());
0 ignored issues
show
Bug introduced by
Are you sure self::pdo->errorInfo() of type array can be used in concatenation? ( Ignorable by Annotation )

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

250
				if (self::LOG_LEVEL) error_log(__METHOD__."($url,$mode,$options) execute() failed: "./** @scrutinizer ignore-type */ self::$pdo->errorInfo());
Loading history...
251
				return false;
252
			}
253
			if ($this->operation == self::STORE2DB)
254
			{
255
				// we buffer all write operations in a temporary file, which get's written on close
256
				$this->opened_stream = tmpfile();
0 ignored issues
show
Documentation Bug introduced by
It seems like tmpfile() can also be of type false. However, the property $opened_stream is declared as type resource. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

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

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
257
			}
258
			// create the hash-dirs, if they not yet exist
259
			elseif(!file_exists($fs_dir=Vfs::dirname(self::_fs_path($this->opened_fs_id))))
260
			{
261
				$umaskbefore = umask();
262
				if (self::LOG_LEVEL > 1) error_log(__METHOD__." about to call mkdir for $fs_dir # Present UMASK:".decoct($umaskbefore)." called from:".function_backtrace());
263
				self::mkdir_recursive($fs_dir,0700,true);
264
			}
265
		}
266
		// check if opend file is a directory
267
		elseif($stat && ($stat['mode'] & self::MODE_DIR) == self::MODE_DIR)
268
		{
269
				if (self::LOG_LEVEL) error_log(__METHOD__."($url,$mode,$options) Is a directory!");
270
				if (($options & STREAM_REPORT_ERRORS))
271
				{
272
					trigger_error(__METHOD__."($url,$mode,$options) Is a directory!",E_USER_WARNING);
273
				}
274
				$this->opened_stream = $this->opened_path = $this->opened_mode = null;
275
				return false;
276
		}
277
		else
278
		{
279
			if ($mode == 'r' && !Vfs::check_access($url,Vfs::READABLE ,$stat) ||// we are not allowed to read
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: ($mode == 'r' && ! EGrou...i\Vfs::WRITABLE, $stat), Probably Intended Meaning: $mode == 'r' && (! EGrou...\Vfs::WRITABLE, $stat))
Loading history...
280
				$mode != 'r' && !Vfs::check_access($url,Vfs::WRITABLE,$stat))	// or edit it
281
			{
282
				self::_remove_password($url);
283
				$op = $mode == 'r' ? 'read' : 'edited';
284
				if (self::LOG_LEVEL) error_log(__METHOD__."($url,$mode,$options) file can not be $op!");
285
				if (($options & STREAM_REPORT_ERRORS))
286
				{
287
					trigger_error(__METHOD__."($url,$mode,$options) file can not be $op!",E_USER_WARNING);
288
				}
289
				$this->opened_stream = $this->opened_path = $this->opened_mode = null;
290
				return false;
291
			}
292
			$this->opened_fs_id = $stat['ino'];
293
294
			if ($this->operation == self::STORE2DB)
295
			{
296
				$stmt = self::$pdo->prepare($sql='SELECT fs_content FROM '.self::TABLE.' WHERE fs_id=?');
297
				$stmt->execute(array($stat['ino']));
298
				$stmt->bindColumn(1,$this->opened_stream,\PDO::PARAM_LOB);
299
				$stmt->fetch(\PDO::FETCH_BOUND);
300
				// hack to work around a current php bug (http://bugs.php.net/bug.php?id=40913)
301
				// PDOStatement::bindColumn(,,\PDO::PARAM_LOB) is not working for MySQL, content is returned as string :-(
302
				if (is_string($this->opened_stream))
303
				{
304
					$tmp = fopen('php://temp', 'wb');
305
					fwrite($tmp, $this->opened_stream);
306
					fseek($tmp, 0, SEEK_SET);
307
					unset($this->opened_stream);
308
					$this->opened_stream = $tmp;
309
				}
310
				//echo 'gettype($this->opened_stream)='; var_dump($this->opened_stream);
311
			}
312
		}
313
		// do we operate directly on the filesystem --> open file from there
314
		if ($this->operation == self::STORE2FS)
315
		{
316
			if (self::LOG_LEVEL > 1) error_log(__METHOD__." fopen (may create a directory? mkdir) ($this->opened_fs_id,$mode,$options)");
317
			if (!($this->opened_stream = fopen(self::_fs_path($this->opened_fs_id),$mode)) && $new_file)
318
			{
319
				// delete db entry again, if we are not able to open a new(!) file
320
				unset($stmt);
321
				$stmt = self::$pdo->prepare('DELETE FROM '.self::TABLE.' WHERE fs_id=:fs_id');
322
				$stmt->execute(array('fs_id' => $this->opened_fs_id));
323
			}
324
		}
325
		if ($mode[0] == 'a')	// append modes: a, a+
326
		{
327
			$this->stream_seek(0,SEEK_END);
328
		}
329
		if (!is_resource($this->opened_stream)) error_log(__METHOD__."($url,$mode,$options) NO stream, returning false!");
330
331
		return is_resource($this->opened_stream);
332
	}
333
334
	/**
335
	 * This method is called when the stream is closed, using fclose().
336
	 *
337
	 * You must release any resources that were locked or allocated by the stream.
338
	 */
339
	function stream_close ( )
340
	{
341
		if (self::LOG_LEVEL > 1) error_log(__METHOD__."()");
342
343
		if (is_null($this->opened_path) || !is_resource($this->opened_stream) || !$this->opened_fs_id)
344
		{
345
			return false;
346
		}
347
348
		if ($this->opened_mode != 'r')
349
		{
350
			$this->stream_seek(0,SEEK_END);
351
352
			// we need to update the mime-type, size and content (if STORE2DB)
353
			$values = array(
354
				'fs_size' => $this->stream_tell(),
355
				// todo: analyse the file for the mime-type
356
				'fs_mime' => Api\MimeMagic::filename2mime($this->opened_path),
357
				'fs_id'   => $this->opened_fs_id,
358
				'fs_modifier' => Vfs::$user,
359
				'fs_modified' => self::_pdo_timestamp(time()),
360
			);
361
362
			if ($this->operation == self::STORE2FS)
363
			{
364
				$stmt = self::$pdo->prepare('UPDATE '.self::TABLE.' SET fs_size=:fs_size,fs_mime=:fs_mime,fs_modifier=:fs_modifier,fs_modified=:fs_modified WHERE fs_id=:fs_id');
365
				if (!($ret = $stmt->execute($values)))
366
				{
367
					error_log(__METHOD__."() execute() failed! errorInfo()=".array2string(self::$pdo->errorInfo()));
368
				}
369
			}
370
			else
371
			{
372
				$stmt = self::$pdo->prepare('UPDATE '.self::TABLE.' SET fs_size=:fs_size,fs_mime=:fs_mime,fs_modifier=:fs_modifier,fs_modified=:fs_modified,fs_content=:fs_content WHERE fs_id=:fs_id');
373
				$this->stream_seek(0,SEEK_SET);	// rewind to the start
374
				foreach($values as $name => &$value)
375
				{
376
					$stmt->bindParam($name,$value);
377
				}
378
				$stmt->bindParam('fs_content', $this->opened_stream, \PDO::PARAM_LOB);
379
				if (!($ret = $stmt->execute()))
380
				{
381
					error_log(__METHOD__."() execute() failed! errorInfo()=".array2string(self::$pdo->errorInfo()));
382
				}
383
			}
384
		}
385
		else
386
		{
387
			$ret = true;
388
		}
389
		$ret = fclose($this->opened_stream) && $ret;
390
391
		unset(self::$stat_cache[$this->opened_path]);
392
		$this->opened_stream = $this->opened_path = $this->opened_mode = $this->opened_fs_id = null;
393
		$this->operation = self::DEFAULT_OPERATION;
394
395
		return $ret;
396
	}
397
398
	/**
399
	 * This method is called in response to fread() and fgets() calls on the stream.
400
	 *
401
	 * You must return up-to count bytes of data from the current read/write position as a string.
402
	 * If there are less than count bytes available, return as many as are available.
403
	 * If no more data is available, return either FALSE or an empty string.
404
	 * You must also update the read/write position of the stream by the number of bytes that were successfully read.
405
	 *
406
	 * @param int $count
407
	 * @return string/false up to count bytes read or false on EOF
0 ignored issues
show
Documentation Bug introduced by
The doc comment string/false at position 0 could not be parsed: Unknown type name 'string/false' at position 0 in string/false.
Loading history...
408
	 */
409
	function stream_read ( $count )
410
	{
411
		if (self::LOG_LEVEL > 1) error_log(__METHOD__."($count) pos=$this->opened_pos");
0 ignored issues
show
Bug introduced by
The property opened_pos does not exist on EGroupware\Api\Vfs\Sqlfs\StreamWrapper. Did you mean opened_path?
Loading history...
412
413
		if (is_resource($this->opened_stream))
414
		{
415
			return fread($this->opened_stream,$count);
416
		}
417
		return false;
418
	}
419
420
	/**
421
	 * This method is called in response to fwrite() calls on the stream.
422
	 *
423
	 * You should store data into the underlying storage used by your stream.
424
	 * If there is not enough room, try to store as many bytes as possible.
425
	 * You should return the number of bytes that were successfully stored in the stream, or 0 if none could be stored.
426
	 * You must also update the read/write position of the stream by the number of bytes that were successfully written.
427
	 *
428
	 * @param string $data
429
	 * @return integer
430
	 */
431
	function stream_write ( $data )
432
	{
433
		if (self::LOG_LEVEL > 1) error_log(__METHOD__."($data)");
434
435
		if (is_resource($this->opened_stream))
436
		{
437
			return fwrite($this->opened_stream,$data);
438
		}
439
		return false;
440
	}
441
442
 	/**
443
 	 * This method is called in response to feof() calls on the stream.
444
 	 *
445
 	 * Important: PHP 5.0 introduced a bug that wasn't fixed until 5.1: the return value has to be the oposite!
446
 	 *
447
 	 * if(version_compare(PHP_VERSION,'5.0','>=') && version_compare(PHP_VERSION,'5.1','<'))
448
  	 * {
449
 	 * 		$eof = !$eof;
450
 	 * }
451
  	 *
452
 	 * @return boolean true if the read/write position is at the end of the stream and no more data availible, false otherwise
453
 	 */
454
	function stream_eof ( )
455
	{
456
		if (is_resource($this->opened_stream))
457
		{
458
			return feof($this->opened_stream);
459
		}
460
		return false;
461
	}
462
463
	/**
464
	 * This method is called in response to ftell() calls on the stream.
465
	 *
466
	 * @return integer current read/write position of the stream
467
	 */
468
 	function stream_tell ( )
469
 	{
470
		if (self::LOG_LEVEL > 1) error_log(__METHOD__."()");
471
472
		if (is_resource($this->opened_stream))
473
		{
474
			return ftell($this->opened_stream);
475
		}
476
		return false;
477
 	}
478
479
 	/**
480
 	 * This method is called in response to fseek() calls on the stream.
481
 	 *
482
 	 * You should update the read/write position of the stream according to offset and whence.
483
 	 * See fseek() for more information about these parameters.
484
 	 *
485
 	 * @param integer $offset
486
  	 * @param integer $whence	SEEK_SET - 0 - Set position equal to offset bytes
487
 	 * 							SEEK_CUR - 1 - Set position to current location plus offset.
488
 	 * 							SEEK_END - 2 - Set position to end-of-file plus offset. (To move to a position before the end-of-file, you need to pass a negative value in offset.)
489
 	 * @return boolean TRUE if the position was updated, FALSE otherwise.
490
 	 */
491
	function stream_seek ( $offset, $whence )
492
	{
493
		if (self::LOG_LEVEL > 1) error_log(__METHOD__."($offset,$whence)");
494
495
		if (is_resource($this->opened_stream))
496
		{
497
			return !fseek($this->opened_stream,$offset,$whence);	// fseek returns 0 on success and -1 on failure
498
		}
499
		return false;
500
	}
501
502
	/**
503
	 * This method is called in response to fflush() calls on the stream.
504
	 *
505
	 * If you have cached data in your stream but not yet stored it into the underlying storage, you should do so now.
506
	 *
507
	 * @return booelan TRUE if the cached data was successfully stored (or if there was no data to store), or FALSE if the data could not be stored.
0 ignored issues
show
Bug introduced by
The type EGroupware\Api\Vfs\Sqlfs\booelan was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
508
	 */
509
	function stream_flush ( )
510
	{
511
		if (self::LOG_LEVEL > 1) error_log(__METHOD__."()");
512
513
		if (is_resource($this->opened_stream))
514
		{
515
			return fflush($this->opened_stream);
516
		}
517
		return false;
518
	}
519
520
	/**
521
	 * This method is called in response to fstat() calls on the stream.
522
	 *
523
	 * If you plan to use your wrapper in a require_once you need to define stream_stat().
524
	 * If you plan to allow any other tests like is_file()/is_dir(), you have to define url_stat().
525
	 * stream_stat() must define the size of the file, or it will never be included.
526
	 * url_stat() must define mode, or is_file()/is_dir()/is_executable(), and any of those functions affected by clearstatcache() simply won't work.
527
	 * It's not documented, but directories must be a mode like 040777 (octal), and files a mode like 0100666.
528
	 * If you wish the file to be executable, use 7s instead of 6s.
529
	 * The last 3 digits are exactly the same thing as what you pass to chmod.
530
	 * 040000 defines a directory, and 0100000 defines a file.
531
	 *
532
	 * @return array containing the same values as appropriate for the stream.
533
	 */
534
	function stream_stat ( )
535
	{
536
		if (self::LOG_LEVEL > 1) error_log(__METHOD__."($this->opened_path)");
537
538
		return $this->url_stat($this->opened_path,0);
539
	}
540
541
	/**
542
	 * This method is called in response to unlink() calls on URL paths associated with the wrapper.
543
	 *
544
	 * It should attempt to delete the item specified by path.
545
	 * In order for the appropriate error message to be returned, do not define this method if your wrapper does not support unlinking!
546
	 *
547
	 * @param string $url
548
	 * @return boolean TRUE on success or FALSE on failure
549
	 */
550
	function unlink ( $url )
551
	{
552
		if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url)");
553
554
		$path = Vfs::parse_url($url,PHP_URL_PATH);
555
556
		// need to get parent stat from Sqlfs, not Vfs
557
		$parent_stat = !($dir = Vfs::dirname($path)) ? false :
558
			$this->url_stat($dir, STREAM_URL_STAT_LINK);
559
560
		if (!$parent_stat || !($stat = $this->url_stat($path,STREAM_URL_STAT_LINK)) ||
561
			!$dir || !Vfs::check_access($dir, Vfs::WRITABLE, $parent_stat))
562
		{
563
			self::_remove_password($url);
564
			if (self::LOG_LEVEL) error_log(__METHOD__."($url) permission denied!");
565
			return false;	// no permission or file does not exist
566
		}
567
		if ($stat['mime'] == self::DIR_MIME_TYPE)
568
		{
569
			self::_remove_password($url);
570
			if (self::LOG_LEVEL) error_log(__METHOD__."($url) is NO file!");
571
			return false;	// no permission or file does not exist
572
		}
573
		$stmt = self::$pdo->prepare('DELETE FROM '.self::TABLE.' WHERE fs_id=:fs_id');
574
		unset(self::$stat_cache[$path]);
575
576
		if (($ret = $stmt->execute(array('fs_id' => $stat['ino']))))
577
		{
578
			if (self::url2operation($url) == self::STORE2FS &&
579
				($stat['mode'] & self::MODE_LINK) != self::MODE_LINK)
580
			{
581
				unlink(self::_fs_path($stat['ino']));
582
			}
583
			// delete props
584
			unset($stmt);
585
			$stmt = self::$pdo->prepare('DELETE FROM '.self::PROPS_TABLE.' WHERE fs_id=?');
586
			$stmt->execute(array($stat['ino']));
587
		}
588
		return $ret;
589
	}
590
591
	/**
592
	 * This method is called in response to rename() calls on URL paths associated with the wrapper.
593
	 *
594
	 * It should attempt to rename the item specified by path_from to the specification given by path_to.
595
	 * In order for the appropriate error message to be returned, do not define this method if your wrapper does not support renaming.
596
	 *
597
	 * The regular filesystem stream-wrapper returns an error, if $url_from and $url_to are not either both files or both dirs!
598
	 *
599
	 * @param string $url_from
600
	 * @param string $url_to
601
	 * @return boolean TRUE on success or FALSE on failure
602
	 */
603
	function rename ( $url_from, $url_to)
604
	{
605
		if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url_from,$url_to)");
606
607
		$path_from = Vfs::parse_url($url_from,PHP_URL_PATH);
608
		$from_dir = Vfs::dirname($path_from);
609
		$path_to = Vfs::parse_url($url_to,PHP_URL_PATH);
610
		$to_dir = Vfs::dirname($path_to);
611
612
		if (!($from_stat = $this->url_stat($path_from, 0)) || !$from_dir ||
613
			!Vfs::check_access($from_dir, Vfs::WRITABLE, $from_dir_stat = $this->url_stat($from_dir, 0)))
614
		{
615
			self::_remove_password($url_from);
616
			self::_remove_password($url_to);
617
			if (self::LOG_LEVEL) error_log(__METHOD__."($url_from,$url_to): $path_from permission denied!");
618
			return false;	// no permission or file does not exist
619
		}
620
		if (!$to_dir || !Vfs::check_access($to_dir, Vfs::WRITABLE, $to_dir_stat = $this->url_stat($to_dir, 0)))
621
		{
622
			self::_remove_password($url_from);
623
			self::_remove_password($url_to);
624
			if (self::LOG_LEVEL) error_log(__METHOD__."($url_from,$url_to): $path_to permission denied!");
625
			return false;	// no permission or parent-dir does not exist
626
		}
627
		// the filesystem stream-wrapper does NOT allow to rename files to directories, as this makes problems
628
		// for our vfs too, we abort here with an error, like the filesystem one does
629
		if (($to_stat = $this->url_stat($path_to, 0)) &&
630
			($to_stat['mime'] === self::DIR_MIME_TYPE) !== ($from_stat['mime'] === self::DIR_MIME_TYPE))
631
		{
632
			self::_remove_password($url_from);
633
			self::_remove_password($url_to);
634
			$is_dir = $to_stat['mime'] === self::DIR_MIME_TYPE ? 'a' : 'no';
635
			if (self::LOG_LEVEL) error_log(__METHOD__."($url_to,$url_from) $path_to is $is_dir directory!");
636
			return false;	// no permission or file does not exist
637
		}
638
		// if destination file already exists, delete it
639
		if ($to_stat && !$this->unlink($url_to))
0 ignored issues
show
Bug Best Practice introduced by
The expression $to_stat of type array<string,integer|mixed> 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...
640
		{
641
			self::_remove_password($url_to);
642
			if (self::LOG_LEVEL) error_log(__METHOD__."($url_to,$url_from) can't unlink existing $url_to!");
643
			return false;
644
		}
645
		unset(self::$stat_cache[$path_from]);
646
		unset(self::$stat_cache[$path_to]);
647
648
		$stmt = self::$pdo->prepare('UPDATE '.self::TABLE.' SET fs_dir=:fs_dir,fs_name=:fs_name'.
649
			' WHERE fs_dir=:old_dir AND fs_name'.self::$case_sensitive_equal.':old_name');
650
		$ok = $stmt->execute(array(
651
			'fs_dir'   => $to_dir_stat['ino'],
652
			'fs_name' => self::limit_filename(Vfs::basename($path_to)),
653
			'old_dir'  => $from_dir_stat['ino'],
654
			'old_name' => $from_stat['name'],
655
		));
656
		unset($stmt);
657
658
		// check if extension changed and update mime-type in that case (as we currently determine mime-type by it's extension!)
659
		// fixes eg. problems with MsWord storing file with .tmp extension and then renaming to .doc
660
		if ($ok && ($new_mime = Vfs::mime_content_type($url_to,true)) != Vfs::mime_content_type($url_to))
661
		{
662
			//echo "<p>Vfs::nime_content_type($url_to,true) = $new_mime</p>\n";
663
			$stmt = self::$pdo->prepare('UPDATE '.self::TABLE.' SET fs_mime=:fs_mime WHERE fs_id=:fs_id');
664
			$stmt->execute(array(
665
				'fs_mime' => $new_mime,
666
				'fs_id'   => $from_stat['ino'],
667
			));
668
			unset(self::$stat_cache[$path_to]);
669
		}
670
		return $ok;
671
	}
672
673
	/**
674
	 * due to problems with recursive directory creation, we have our own here
675
	 */
676
	protected static function mkdir_recursive($pathname, $mode, $depth=0)
677
	{
678
		$maxdepth=10;
679
		$depth2propagate = (int)$depth + 1;
680
		if ($depth2propagate > $maxdepth) return is_dir($pathname);
681
    	is_dir(Vfs::dirname($pathname)) || self::mkdir_recursive(Vfs::dirname($pathname), $mode, $depth2propagate);
682
    	return is_dir($pathname) || @mkdir($pathname, $mode);
683
	}
684
685
	/**
686
	 * This method is called in response to mkdir() calls on URL paths associated with the wrapper.
687
	 *
688
	 * It should attempt to create the directory specified by path.
689
	 * In order for the appropriate error message to be returned, do not define this method if your wrapper does not support creating directories.
690
	 *
691
	 * @param string $url
692
	 * @param int $mode
693
	 * @param int $options Posible values include STREAM_REPORT_ERRORS and STREAM_MKDIR_RECURSIVE
694
	 * @return boolean TRUE on success or FALSE on failure
695
	 */
696
	function mkdir ( $url, $mode, $options )
697
	{
698
		if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url,$mode,$options)");
699
		if (self::LOG_LEVEL > 1) error_log(__METHOD__." called from:".function_backtrace());
700
		$path = Vfs::parse_url($url,PHP_URL_PATH);
701
702
		if ($this->url_stat($path,STREAM_URL_STAT_QUIET))
703
		{
704
			self::_remove_password($url);
705
			if (self::LOG_LEVEL) error_log(__METHOD__."('$url',$mode,$options) already exist!");
706
			if (!($options & STREAM_REPORT_ERRORS))
707
			{
708
				trigger_error(__METHOD__."('$url',$mode,$options) already exist!",E_USER_WARNING);
709
			}
710
			return false;
711
		}
712
		if (!($parent_path = Vfs::dirname($path)))
713
		{
714
			self::_remove_password($url);
715
			if (self::LOG_LEVEL) error_log(__METHOD__."('$url',$mode,$options) dirname('$path')===false!");
716
			if (!($options & STREAM_REPORT_ERRORS))
717
			{
718
				trigger_error(__METHOD__."('$url',$mode,$options) dirname('$path')===false!", E_USER_WARNING);
719
			}
720
			return false;
721
		}
722
		if (($query = Vfs::parse_url($url,PHP_URL_QUERY))) $parent_path .= '?'.$query;
0 ignored issues
show
Bug introduced by
Are you sure $query of type array|string|true can be used in concatenation? ( Ignorable by Annotation )

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

722
		if (($query = Vfs::parse_url($url,PHP_URL_QUERY))) $parent_path .= '?'./** @scrutinizer ignore-type */ $query;
Loading history...
723
		$parent = $this->url_stat($parent_path,STREAM_URL_STAT_QUIET);
724
725
		// check if we should also create all non-existing path components and our parent does not exist,
726
		// if yes call ourself recursive with the parent directory
727
		if (($options & STREAM_MKDIR_RECURSIVE) && $parent_path != '/' && !$parent)
0 ignored issues
show
Bug Best Practice introduced by
The expression $parent of type array<string,integer|mixed> 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...
introduced by
$parent is a non-empty array, thus ! $parent is always false.
Loading history...
728
		{
729
			if (self::LOG_LEVEL > 1) error_log(__METHOD__." creating parents: $parent_path, $mode");
730
			if (!$this->mkdir($parent_path,$mode,$options))
731
			{
732
				return false;
733
			}
734
			$parent = $this->url_stat($parent_path,0);
735
		}
736
		if (!$parent || !Vfs::check_access($parent_path,Vfs::WRITABLE,$parent))
0 ignored issues
show
introduced by
$parent is a non-empty array, thus ! $parent is always false.
Loading history...
Bug Best Practice introduced by
The expression $parent of type array<string,integer|mixed> 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...
737
		{
738
			self::_remove_password($url);
739
			if (self::LOG_LEVEL) error_log(__METHOD__."('$url',$mode,$options) permission denied!");
740
			if (!($options & STREAM_REPORT_ERRORS))
741
			{
742
				trigger_error(__METHOD__."('$url',$mode,$options) permission denied!",E_USER_WARNING);
743
			}
744
			return false;	// no permission or file does not exist
745
		}
746
		unset(self::$stat_cache[$path]);
747
		$stmt = self::$pdo->prepare('INSERT INTO '.self::TABLE.' (fs_name,fs_dir,fs_mode,fs_uid,fs_gid,fs_size,fs_mime,fs_created,fs_modified,fs_creator'.
748
					') VALUES (:fs_name,:fs_dir,:fs_mode,:fs_uid,:fs_gid,:fs_size,:fs_mime,:fs_created,:fs_modified,:fs_creator)');
749
		if (($ok = $stmt->execute(array(
750
			'fs_name' => self::limit_filename(Vfs::basename($path)),
751
			'fs_dir'  => $parent['ino'],
752
			'fs_mode' => $parent['mode'],
753
			'fs_uid'  => $parent['uid'],
754
			'fs_gid'  => $parent['gid'],
755
			'fs_size' => 0,
756
			'fs_mime' => self::DIR_MIME_TYPE,
757
			'fs_created'  => self::_pdo_timestamp(time()),
758
			'fs_modified' => self::_pdo_timestamp(time()),
759
			'fs_creator'  => Vfs::$user,
760
		))))
761
		{
762
			// check if some other process created the directory parallel to us (sqlfs would gives SQL errors later!)
763
			$new_fs_id = self::$pdo->lastInsertId('egw_sqlfs_fs_id_seq');
764
765
			unset($stmt);	// free statement object, on some installs a new prepare fails otherwise!
766
767
			$stmt = self::$pdo->prepare($q='SELECT COUNT(*) FROM '.self::TABLE.
768
				' WHERE fs_dir=:fs_dir AND fs_active=:fs_active AND fs_name'.self::$case_sensitive_equal.':fs_name');
769
			if ($stmt->execute(array(
770
				'fs_dir'  => $parent['ino'],
771
				'fs_active' => self::_pdo_boolean(true),
772
				'fs_name' => self::limit_filename(Vfs::basename($path)),
773
			)) && $stmt->fetchColumn() > 1)	// if there's more then one --> remove our new dir
774
			{
775
				self::$pdo->query('DELETE FROM '.self::TABLE.' WHERE fs_id='.$new_fs_id);
776
			}
777
		}
778
		return $ok;
779
	}
780
781
	/**
782
	 * This method is called in response to rmdir() calls on URL paths associated with the wrapper.
783
	 *
784
	 * It should attempt to remove the directory specified by path.
785
	 * In order for the appropriate error message to be returned, do not define this method if your wrapper does not support removing directories.
786
	 *
787
	 * @param string $url
788
	 * @param int $options Possible values include STREAM_REPORT_ERRORS.
789
	 * @return boolean TRUE on success or FALSE on failure.
790
	 */
791
	function rmdir ( $url, $options )
792
	{
793
		if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url)");
794
795
		$path = Vfs::parse_url($url,PHP_URL_PATH);
796
797
		if (!($parent = Vfs::dirname($path)) ||
798
			!($stat = $this->url_stat($path, 0)) || $stat['mime'] != self::DIR_MIME_TYPE ||
799
			!Vfs::check_access($parent, Vfs::WRITABLE, $this->url_stat($parent,0)))
800
		{
801
			self::_remove_password($url);
802
			$err_msg = __METHOD__."($url,$options) ".(!$stat ? 'not found!' :
803
				($stat['mime'] != self::DIR_MIME_TYPE ? 'not a directory!' : 'permission denied!'));
804
			if (self::LOG_LEVEL) error_log($err_msg);
805
			if (!($options & STREAM_REPORT_ERRORS))
806
			{
807
				trigger_error($err_msg,E_USER_WARNING);
808
			}
809
			return false;	// no permission or file does not exist
810
		}
811
		$stmt = self::$pdo->prepare('SELECT COUNT(*) FROM '.self::TABLE.' WHERE fs_dir=?');
812
		$stmt->execute(array($stat['ino']));
813
		if ($stmt->fetchColumn())
814
		{
815
			self::_remove_password($url);
816
			if (self::LOG_LEVEL) error_log(__METHOD__."($url,$options) dir is not empty!");
817
			if (!($options & STREAM_REPORT_ERRORS))
818
			{
819
				trigger_error(__METHOD__."('$url',$options) dir is not empty!",E_USER_WARNING);
820
			}
821
			return false;
822
		}
823
		unset(self::$stat_cache[$path]);
824
		unset($stmt);	// free statement object, on some installs a new prepare fails otherwise!
825
826
		$del_stmt = self::$pdo->prepare('DELETE FROM '.self::TABLE.' WHERE fs_id=?');
827
		if (($ret = $del_stmt->execute(array($stat['ino']))))
828
		{
829
			self::eacl($path,null,false,$stat['ino']);	// remove all (=false) evtl. existing extended acl for that dir
830
			// delete props
831
			unset($del_stmt);
832
			$del_stmt = self::$pdo->prepare('DELETE FROM '.self::PROPS_TABLE.' WHERE fs_id=?');
833
			$del_stmt->execute(array($stat['ino']));
834
		}
835
		return $ret;
836
	}
837
838
	/**
839
	 * StreamWrapper method (PHP 5.4+) for touch, chmod, chown and chgrp
840
	 *
841
	 * We use protected helper methods touch, chmod, chown and chgrp to implement the functionality.
842
	 *
843
	 * @param string $path
844
	 * @param int $option STREAM_META_(TOUCH|ACCESS|((OWNER|GROUP)(_NAME)?))
845
	 * @param array|int|string $value
846
	 * - STREAM_META_TOUCH array($time, $atime)
847
	 * - STREAM_META_ACCESS int
848
	 * - STREAM_(OWNER|GROUP) int
849
	 * - STREAM_(OWNER|GROUP)_NAME string
850
	 * @return boolean true on success, false on failure
851
	 */
852
	function stream_metadata($path, $option, $value)
853
	{
854
		if (self::LOG_LEVEL > 1) error_log(__METHOD__."($path, $option, ".array2string($value).")");
855
856
		switch($option)
857
		{
858
			case STREAM_META_TOUCH:
859
				return $this->touch($path, $value[0]);	// atime is not supported
860
861
			case STREAM_META_ACCESS:
862
				return $this->chmod($path, $value);
863
864
			case STREAM_META_OWNER_NAME:
865
				if (($value = $GLOBALS['egw']->accounts->name2id($value, 'account_lid', 'u')) === false)
866
					return false;
867
				// fall through
868
			case STREAM_META_OWNER:
869
				return $this->chown($path, $value);
870
871
			case STREAM_META_GROUP_NAME:
872
				if (($value = $GLOBALS['egw']->accounts->name2id($value, 'account_lid', 'g')) === false)
873
					return false;
874
				// fall through
875
			case STREAM_META_GROUP:
876
				return $this->chgrp($path, $value);
877
		}
878
		return false;
879
	}
880
881
	/**
882
	 * This is not (yet) a stream-wrapper function, but it's necessary and can be used static
883
	 *
884
	 * @param string $url
885
	 * @param int $time =null modification time (unix timestamp), default null = current time
886
	 * @param int $atime =null access time (unix timestamp), default null = current time, not implemented in the vfs!
887
	 */
888
	protected function touch($url,$time=null,$atime=null)
889
	{
890
		unset($atime);	// not used
891
		if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url, $time)");
892
893
		$path = Vfs::parse_url($url,PHP_URL_PATH);
894
895
		$vfs = new self();
896
		if (!($stat = $vfs->url_stat($path,STREAM_URL_STAT_QUIET)))
897
		{
898
			// file does not exist --> create an empty one
899
			if (!($f = fopen(self::SCHEME.'://default'.$path,'w')) || !fclose($f))
0 ignored issues
show
Bug introduced by
Are you sure $path of type array|boolean|string can be used in concatenation? ( Ignorable by Annotation )

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

899
			if (!($f = fopen(self::SCHEME.'://default'./** @scrutinizer ignore-type */ $path,'w')) || !fclose($f))
Loading history...
900
			{
901
				return false;
902
			}
903
			if (!$time)
0 ignored issues
show
Bug Best Practice introduced by
The expression $time of type integer|null is loosely compared to false; this is ambiguous if the integer can be 0. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
904
			{
905
				return true;	// new (empty) file created with current mod time
906
			}
907
			$stat = $vfs->url_stat($path,0);
908
		}
909
		unset(self::$stat_cache[$path]);
910
		$stmt = self::$pdo->prepare('UPDATE '.self::TABLE.' SET fs_modified=:fs_modified,fs_modifier=:fs_modifier WHERE fs_id=:fs_id');
911
912
		return $stmt->execute(array(
913
			'fs_modified' => self::_pdo_timestamp($time ? $time : time()),
914
			'fs_modifier' => Vfs::$user,
915
			'fs_id' => $stat['ino'],
916
		));
917
	}
918
919
	/**
920
	 * Chown command, not yet a stream-wrapper function, but necessary
921
	 *
922
	 * @param string $url
923
	 * @param int $owner
924
	 * @return boolean
925
	 */
926
	protected function chown($url,$owner)
927
	{
928
		if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url,$owner)");
929
930
		$path = Vfs::parse_url($url,PHP_URL_PATH);
931
932
		$vfs = new self();
933
		if (!($stat = $vfs->url_stat($path,0)))
934
		{
935
			if (self::LOG_LEVEL) error_log(__METHOD__."($url,$owner) no such file or directory!");
936
			trigger_error("No such file or directory $url !",E_USER_WARNING);
937
			return false;
938
		}
939
		if (!Vfs::$is_root)
940
		{
941
			if (self::LOG_LEVEL) error_log(__METHOD__."($url,$owner) only root can do that!");
942
			trigger_error("Only root can do that!",E_USER_WARNING);
943
			return false;
944
		}
945
		if ($owner < 0 || $owner && !$GLOBALS['egw']->accounts->id2name($owner))	// not a user (0 == root)
946
		{
947
			if (self::LOG_LEVEL) error_log(__METHOD__."($url,$owner) unknown (numeric) user id!");
948
			trigger_error(__METHOD__."($url,$owner) Unknown (numeric) user id!",E_USER_WARNING);
949
			//throw new Exception(__METHOD__."($url,$owner) Unknown (numeric) user id!");
950
			return false;
951
		}
952
		$stmt = self::$pdo->prepare('UPDATE '.self::TABLE.' SET fs_uid=:fs_uid WHERE fs_id=:fs_id');
953
954
		// update stat-cache
955
		if ($path != '/' && substr($path,-1) == '/') $path = substr($path, 0, -1);
956
		self::$stat_cache[$path]['fs_uid'] = $owner;
957
958
		return $stmt->execute(array(
959
			'fs_uid' => (int) $owner,
960
			'fs_id' => $stat['ino'],
961
		));
962
	}
963
964
	/**
965
	 * Chgrp command, not yet a stream-wrapper function, but necessary
966
	 *
967
	 * @param string $url
968
	 * @param int $owner
969
	 * @return boolean
970
	 */
971
	protected function chgrp($url,$owner)
972
	{
973
		if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url,$owner)");
974
975
		$path = Vfs::parse_url($url,PHP_URL_PATH);
976
977
		$vfs = new self();
978
		if (!($stat = $vfs->url_stat($path,0)))
979
		{
980
			if (self::LOG_LEVEL) error_log(__METHOD__."($url,$owner) no such file or directory!");
981
			trigger_error("No such file or directory $url !",E_USER_WARNING);
982
			return false;
983
		}
984
		if (!Vfs::has_owner_rights($path,$stat))
985
		{
986
			if (self::LOG_LEVEL) error_log(__METHOD__."($url,$owner) only owner or root can do that!");
987
			trigger_error("Only owner or root can do that!",E_USER_WARNING);
988
			return false;
989
		}
990
		if ($owner < 0) $owner = -$owner;	// sqlfs uses a positiv group id's!
991
992
		if ($owner && !$GLOBALS['egw']->accounts->id2name(-$owner))	// not a group
993
		{
994
			if (self::LOG_LEVEL) error_log(__METHOD__."($url,$owner) unknown (numeric) group id!");
995
			trigger_error("Unknown (numeric) group id!",E_USER_WARNING);
996
			return false;
997
		}
998
		$stmt = self::$pdo->prepare('UPDATE '.self::TABLE.' SET fs_gid=:fs_gid WHERE fs_id=:fs_id');
999
1000
		// update stat-cache
1001
		if ($path != '/' && substr($path,-1) == '/') $path = substr($path, 0, -1);
1002
		self::$stat_cache[$path]['fs_gid'] = $owner;
1003
1004
		return $stmt->execute(array(
1005
			'fs_gid' => $owner,
1006
			'fs_id' => $stat['ino'],
1007
		));
1008
	}
1009
1010
	/**
1011
	 * Chmod command, not yet a stream-wrapper function, but necessary
1012
	 *
1013
	 * @param string $url
1014
	 * @param int $mode
1015
	 * @return boolean
1016
	 */
1017
	protected function chmod($url,$mode)
1018
	{
1019
		if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url, $mode)");
1020
1021
		$path = Vfs::parse_url($url,PHP_URL_PATH);
1022
1023
		$vfs = new self();
1024
		if (!($stat = $vfs->url_stat($path,0)))
1025
		{
1026
			if (self::LOG_LEVEL) error_log(__METHOD__."($url, $mode) no such file or directory!");
1027
			trigger_error("No such file or directory $url !",E_USER_WARNING);
1028
			return false;
1029
		}
1030
		if (!Vfs::has_owner_rights($path,$stat))
1031
		{
1032
			if (self::LOG_LEVEL) error_log(__METHOD__."($url, $mode) only owner or root can do that!");
1033
			trigger_error("Only owner or root can do that!",E_USER_WARNING);
1034
			return false;
1035
		}
1036
		if (!is_numeric($mode))	// not a mode
0 ignored issues
show
introduced by
The condition is_numeric($mode) is always true.
Loading history...
1037
		{
1038
			if (self::LOG_LEVEL) error_log(__METHOD__."($url, $mode) no (numeric) mode!");
1039
			trigger_error("No (numeric) mode!",E_USER_WARNING);
1040
			return false;
1041
		}
1042
		$stmt = self::$pdo->prepare('UPDATE '.self::TABLE.' SET fs_mode=:fs_mode WHERE fs_id=:fs_id');
1043
1044
		// update stat cache
1045
		if ($path != '/' && substr($path,-1) == '/') $path = substr($path, 0, -1);
1046
		self::$stat_cache[$path]['fs_mode'] = ((int) $mode) & 0777;
1047
1048
		return $stmt->execute(array(
1049
			'fs_mode' => ((int) $mode) & 0777,		// we dont store the file and dir bits, give int overflow!
1050
			'fs_id' => $stat['ino'],
1051
		));
1052
	}
1053
1054
1055
	/**
1056
	 * This method is called immediately when your stream object is created for examining directory contents with opendir().
1057
	 *
1058
	 * @param string $url URL that was passed to opendir() and that this object is expected to explore.
1059
	 * @param int $options
1060
	 * @return booelan
1061
	 */
1062
	function dir_opendir ( $url, $options )
1063
	{
1064
		$this->opened_dir = null;
1065
1066
		$path = Vfs::parse_url($url,PHP_URL_PATH);
1067
1068
		if (!($stat = $this->url_stat($url,0)) || 		// dir not found
1069
			!($stat['mode'] & self::MODE_DIR) && $stat['mime'] != self::DIR_MIME_TYPE ||		// no dir
1070
			!Vfs::check_access($url,Vfs::EXECUTABLE|Vfs::READABLE,$stat))	// no access
1071
		{
1072
			self::_remove_password($url);
1073
			$msg = !($stat['mode'] & self::MODE_DIR) && $stat['mime'] != self::DIR_MIME_TYPE ?
1074
				"$url is no directory" : 'permission denied';
1075
			if (self::LOG_LEVEL) error_log(__METHOD__."('$url',$options) $msg!");
1076
			$this->opened_dir = null;
1077
			return false;
1078
		}
1079
		$this->opened_dir = array();
1080
		$query = 'SELECT fs_id,fs_name,fs_mode,fs_uid,fs_gid,fs_size,fs_mime,fs_created,fs_modified'.self::$extra_columns.
1081
			' FROM '.self::TABLE.' WHERE fs_dir=? AND fs_active='.self::_pdo_boolean(true).
1082
			" ORDER BY fs_mime='httpd/unix-directory' DESC, fs_name ASC";
1083
		//if (self::LOG_LEVEL > 2) $query = '/* '.__METHOD__.': '.__LINE__.' */ '.$query;
1084
		if (self::LOG_LEVEL > 2) $query = '/* '.__METHOD__."($url,$options)".' */ '.$query;
1085
1086
		$stmt = self::$pdo->prepare($query);
1087
		$stmt->setFetchMode(\PDO::FETCH_ASSOC);
1088
		if ($stmt->execute(array($stat['ino'])))
1089
		{
1090
			foreach($stmt as $file)
1091
			{
1092
				$this->opened_dir[] = $file['fs_name'];
1093
				self::$stat_cache[Vfs::concat($path,$file['fs_name'])] = $file;
1094
			}
1095
		}
1096
		if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url,$options): ".implode(', ',$this->opened_dir));
1097
		reset($this->opened_dir);
1098
1099
		return true;
1100
	}
1101
1102
	/**
1103
	 * This method is called in response to stat() calls on the URL paths associated with the wrapper.
1104
	 *
1105
	 * It should return as many elements in common with the system function as possible.
1106
	 * Unknown or unavailable values should be set to a rational value (usually 0).
1107
	 *
1108
	 * If you plan to use your wrapper in a require_once you need to define stream_stat().
1109
	 * If you plan to allow any other tests like is_file()/is_dir(), you have to define url_stat().
1110
	 * stream_stat() must define the size of the file, or it will never be included.
1111
	 * url_stat() must define mode, or is_file()/is_dir()/is_executable(), and any of those functions affected by clearstatcache() simply won't work.
1112
	 * It's not documented, but directories must be a mode like 040777 (octal), and files a mode like 0100666.
1113
	 * If you wish the file to be executable, use 7s instead of 6s.
1114
	 * The last 3 digits are exactly the same thing as what you pass to chmod.
1115
	 * 040000 defines a directory, and 0100000 defines a file.
1116
	 *
1117
	 * @param string $url
1118
	 * @param int $flags holds additional flags set by the streams API. It can hold one or more of the following values OR'd together:
1119
	 * - STREAM_URL_STAT_LINK	For resources with the ability to link to other resource (such as an HTTP Location: forward,
1120
	 *                          or a filesystem symlink). This flag specified that only information about the link itself should be returned,
1121
	 *                          not the resource pointed to by the link.
1122
	 *                          This flag is set in response to calls to lstat(), is_link(), or filetype().
1123
	 * - STREAM_URL_STAT_QUIET	If this flag is set, your wrapper should not raise any errors. If this flag is not set,
1124
	 *                          you are responsible for reporting errors using the trigger_error() function during stating of the path.
1125
	 *                          stat triggers it's own warning anyway, so it makes no sense to trigger one by our stream-wrapper!
1126
	 * @return array
1127
	 */
1128
	function url_stat ( $url, $flags )
1129
	{
1130
		static $max_subquery_depth=null;
1131
		if (is_null($max_subquery_depth))
1132
		{
1133
			$max_subquery_depth = $GLOBALS['egw_info']['server']['max_subquery_depth'];
1134
			if (!$max_subquery_depth) $max_subquery_depth = 7;	// setting current default of 7, if nothing set
1135
		}
1136
		if (self::LOG_LEVEL > 1) error_log(__METHOD__."('$url',$flags)");
1137
1138
		$path = Vfs::parse_url($url,PHP_URL_PATH);
1139
1140
		// webdav adds a trailing slash to dirs, which causes url_stat to NOT find the file otherwise
1141
		if ($path != '/' && substr($path,-1) == '/')
1142
		{
1143
			$path = substr($path,0,-1);
1144
		}
1145
		if (empty($path))
1146
		{
1147
			return false;	// is invalid and gives sql error
1148
		}
1149
		// check if we already have the info from the last dir_open call, as the old vfs reads it anyway from the db
1150
		if (self::$stat_cache && isset(self::$stat_cache[$path]) && self::$stat_cache[$path] !== false)
0 ignored issues
show
Bug Best Practice introduced by
The expression self::stat_cache 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...
1151
		{
1152
			return self::$stat_cache[$path] ? self::_vfsinfo2stat(self::$stat_cache[$path]) : false;
1153
		}
1154
1155
		if (!is_object(self::$pdo))
1156
		{
1157
			self::_pdo();
1158
		}
1159
		$base_query = 'SELECT fs_id,fs_name,fs_mode,fs_uid,fs_gid,fs_size,fs_mime,fs_created,fs_modified'.self::$extra_columns.
1160
			' FROM '.self::TABLE.' WHERE fs_active='.self::_pdo_boolean(true).
1161
			' AND fs_name'.self::$case_sensitive_equal.'? AND fs_dir=';
1162
		$parts = explode('/',$path);
1163
1164
		// if we have extended acl access to the url, we dont need and can NOT include the sql for the readable check
1165
		$eacl_access = static::check_extended_acl($path,Vfs::READABLE);
1166
1167
		try {
1168
			foreach($parts as $n => $name)
1169
			{
1170
				if ($n == 0)
1171
				{
1172
					$query = (int) ($path != '/');	// / always has fs_id == 1, no need to query it ($path=='/' needs fs_dir=0!)
1173
				}
1174
				elseif ($n < count($parts)-1)
1175
				{
1176
					// MySQL 5.0 has a nesting limit for subqueries
1177
					// --> we replace the so far cumulated subqueries with their result
1178
					// no idea about the other DBMS, but this does NOT hurt ...
1179
					// --> depth limit of subqueries is now dynamicly decremented in catch
1180
					if ($n > 1 && !(($n-1) % $max_subquery_depth) && !($query = self::$pdo->query($query)->fetchColumn()))
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $query seems to be defined later in this foreach loop on line 1172. Are you sure it is defined here?
Loading history...
1181
					{
1182
						if (self::LOG_LEVEL > 1)
1183
						{
1184
							self::_remove_password($url);
1185
							error_log(__METHOD__."('$url',$flags) file or directory not found!");
1186
						}
1187
						// we also store negatives (all methods creating new files/dirs have to unset the stat-cache!)
1188
						return self::$stat_cache[$path] = false;
1189
					}
1190
					$query = 'SELECT fs_id FROM '.self::TABLE.' WHERE fs_dir=('.$query.') AND fs_active='.
1191
						self::_pdo_boolean(true).' AND fs_name'.self::$case_sensitive_equal.self::$pdo->quote($name);
1192
1193
					// if we are not root AND have no extended acl access, we need to make sure the user has the right to tranverse all parent directories (read-rights)
1194
					if (!Vfs::$is_root && !$eacl_access)
1195
					{
1196
						if (!Vfs::$user)
1197
						{
1198
							self::_remove_password($url);
1199
							if (self::LOG_LEVEL > 1) error_log(__METHOD__."('$url',$flags) permission denied, no user-id and not root!");
1200
							return false;
1201
						}
1202
						$query .= ' AND '.self::_sql_readable();
1203
					}
1204
				}
1205
				else
1206
				{
1207
					$query = str_replace('fs_name'.self::$case_sensitive_equal.'?','fs_name'.self::$case_sensitive_equal.self::$pdo->quote($name),$base_query).'('.$query.')';
1208
				}
1209
			}
1210
			if (self::LOG_LEVEL > 2) $query = '/* '.__METHOD__."($url,$flags) eacl_access=$eacl_access".' */ '.$query;
1211
			//if (self::LOG_LEVEL > 2) $query = '/* '.__METHOD__.': '.__LINE__.' */ '.$query;
1212
1213
			if (!($result = self::$pdo->query($query)) || !($info = $result->fetch(\PDO::FETCH_ASSOC)))
1214
			{
1215
				if (self::LOG_LEVEL > 1)
1216
				{
1217
					self::_remove_password($url);
1218
					error_log(__METHOD__."('$url',$flags) file or directory not found!");
1219
				}
1220
				// we also store negatives (all methods creating new files/dirs have to unset the stat-cache!)
1221
				return self::$stat_cache[$path] = false;
1222
			}
1223
		}
1224
		catch (\PDOException $e) {
1225
			// decrement subquery limit by 1 and try again, if not already smaller then 3
1226
			if ($max_subquery_depth < 3)
1227
			{
1228
				throw new Api\Db\Exception($e->getMessage());
1229
			}
1230
			$GLOBALS['egw_info']['server']['max_subquery_depth'] = --$max_subquery_depth;
1231
			error_log(__METHOD__."() decremented max_subquery_depth to $max_subquery_depth");
1232
			Api\Config::save_value('max_subquery_depth', $max_subquery_depth, 'phpgwapi');
1233
			if (method_exists($GLOBALS['egw'],'invalidate_session_cache')) $GLOBALS['egw']->invalidate_session_cache();
1234
			return $this->url_stat($url, $flags);
1235
		}
1236
		self::$stat_cache[$path] = $info;
1237
1238
		if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url,$flags)=".array2string($info));
1239
		return self::_vfsinfo2stat($info);
1240
	}
1241
1242
	/**
1243
	 * Return readable check as sql (to be AND'ed into the query), only use if !Vfs::$is_root
1244
	 *
1245
	 * @return string
1246
	 */
1247
	protected static function _sql_readable()
1248
	{
1249
		static $sql_read_acl=null;
1250
1251
		if (is_null($sql_read_acl))
1252
		{
1253
			foreach($GLOBALS['egw']->accounts->memberships(Vfs::$user,true) as $gid)
1254
			{
1255
				$memberships[] = abs($gid);	// sqlfs stores the gid's positiv
1256
			}
1257
			// using octal numbers with mysql leads to funny results (select 384 & 0400 --> 384 not 256=0400)
1258
			// 256 = 0400, 32 = 040
1259
			$sql_read_acl = '((fs_mode & 4)=4 OR (fs_mode & 256)=256 AND fs_uid='.(int)Vfs::$user.
1260
				($memberships ? ' OR (fs_mode & 32)=32 AND fs_gid IN('.implode(',',$memberships).')' : '').')';
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $memberships seems to be defined by a foreach iteration on line 1253. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
1261
			//error_log(__METHOD__."() Vfs::\$user=".array2string(Vfs::$user).' --> memberships='.array2string($memberships).' --> '.$sql_read_acl.($memberships?'':': '.function_backtrace()));
1262
		}
1263
		return $sql_read_acl;
1264
	}
1265
1266
	/**
1267
	 * This method is called in response to readdir().
1268
	 *
1269
	 * It should return a string representing the next filename in the location opened by dir_opendir().
1270
	 *
1271
	 * @return string
1272
	 */
1273
	function dir_readdir ( )
1274
	{
1275
		if (self::LOG_LEVEL > 1) error_log(__METHOD__."( )");
1276
1277
		if (!is_array($this->opened_dir)) return false;
0 ignored issues
show
introduced by
The condition is_array($this->opened_dir) is always true.
Loading history...
1278
1279
		$file = current($this->opened_dir); next($this->opened_dir);
1280
1281
		return $file;
1282
	}
1283
1284
	/**
1285
	 * This method is called in response to rewinddir().
1286
	 *
1287
	 * It should reset the output generated by dir_readdir(). i.e.:
1288
	 * The next call to dir_readdir() should return the first entry in the location returned by dir_opendir().
1289
	 *
1290
	 * @return boolean
1291
	 */
1292
	function dir_rewinddir ( )
1293
	{
1294
		if (self::LOG_LEVEL > 1) error_log(__METHOD__."( )");
1295
1296
		if (!is_array($this->opened_dir)) return false;
0 ignored issues
show
introduced by
The condition is_array($this->opened_dir) is always true.
Loading history...
1297
1298
		reset($this->opened_dir);
1299
1300
		return true;
1301
	}
1302
1303
	/**
1304
	 * This method is called in response to closedir().
1305
	 *
1306
	 * You should release any resources which were locked or allocated during the opening and use of the directory stream.
1307
	 *
1308
	 * @return boolean
1309
	 */
1310
	function dir_closedir ( )
1311
	{
1312
		if (self::LOG_LEVEL > 1) error_log(__METHOD__."( )");
1313
1314
		if (!is_array($this->opened_dir)) return false;
0 ignored issues
show
introduced by
The condition is_array($this->opened_dir) is always true.
Loading history...
1315
1316
		$this->opened_dir = null;
1317
1318
		return true;
1319
	}
1320
1321
	/**
1322
	 * This method is called in response to readlink().
1323
	 *
1324
	 * The readlink value is read by url_stat or dir_opendir and therefore cached in the stat-cache.
1325
	 *
1326
	 * @param string $path
1327
	 * @return string|boolean content of the symlink or false if $url is no symlink (or not found)
1328
	 */
1329
	static function readlink($path)
1330
	{
1331
		$vfs = new self();
1332
		$link = !($lstat = $vfs->url_stat($path,STREAM_URL_STAT_LINK)) || is_null($lstat['readlink']) ? false : $lstat['readlink'];
1333
1334
		if (self::LOG_LEVEL > 1) error_log(__METHOD__."('$path') = $link");
1335
1336
		return $link;
1337
	}
1338
1339
	/**
1340
	 * Method called for symlink()
1341
	 *
1342
	 * @param string $target
1343
	 * @param string $link
1344
	 * @return boolean true on success false on error
1345
	 */
1346
	static function symlink($target,$link)
1347
	{
1348
		if (self::LOG_LEVEL > 1) error_log(__METHOD__."('$target','$link')");
1349
1350
		$inst = new static();
1351
		if ($inst->url_stat($link,0))
1352
		{
1353
			if (self::LOG_LEVEL > 0) error_log(__METHOD__."('$target','$link') $link exists, returning false!");
1354
			return false;	// $link already exists
1355
		}
1356
		if (!($dir = Vfs::dirname($link)) ||
1357
			!Vfs::check_access($dir,Vfs::WRITABLE,$dir_stat=$inst->url_stat($dir,0)))
1358
		{
1359
			if (self::LOG_LEVEL > 0) error_log(__METHOD__."('$target','$link') returning false! (!is_writable('$dir'), dir_stat=".array2string($dir_stat).")");
1360
			return false;	// parent dir does not exist or is not writable
1361
		}
1362
		$query = 'INSERT INTO '.self::TABLE.' (fs_name,fs_dir,fs_mode,fs_uid,fs_gid,fs_created,fs_modified,fs_creator,fs_mime,fs_size,fs_link'.
1363
			') VALUES (:fs_name,:fs_dir,:fs_mode,:fs_uid,:fs_gid,:fs_created,:fs_modified,:fs_creator,:fs_mime,:fs_size,:fs_link)';
1364
		if (self::LOG_LEVEL > 2) $query = '/* '.__METHOD__.': '.__LINE__.' */ '.$query;
1365
		$stmt = self::$pdo->prepare($query);
1366
		unset(self::$stat_cache[Vfs::parse_url($link,PHP_URL_PATH)]);
1367
1368
		return !!$stmt->execute(array(
1369
			'fs_name' => self::limit_filename(Vfs::basename($link)),
1370
			'fs_dir'  => $dir_stat['ino'],
1371
			'fs_mode' => ($dir_stat['mode'] & 0666),
1372
			'fs_uid'  => $dir_stat['uid'] ? $dir_stat['uid'] : Vfs::$user,
1373
			'fs_gid'  => $dir_stat['gid'],
1374
			'fs_created'  => self::_pdo_timestamp(time()),
1375
			'fs_modified' => self::_pdo_timestamp(time()),
1376
			'fs_creator'  => Vfs::$user,
1377
			'fs_mime'     => self::SYMLINK_MIME_TYPE,
1378
			'fs_size'     => bytes($target),
1379
			'fs_link'     => $target,
1380
		));
1381
	}
1382
1383
	private static $extended_acl;
1384
1385
	/**
1386
	 * Check if extendes ACL (stored in eGW's ACL table) grants access
1387
	 *
1388
	 * The extended ACL is inherited, so it's valid for all subdirs and the included files!
1389
	 * The used algorithm break on the first match. It could be used, to disallow further access.
1390
	 *
1391
	 * @param string $url url to check
1392
	 * @param int $check mode to check: one or more or'ed together of: 4 = read, 2 = write, 1 = executable
1393
	 * @return boolean
1394
	 */
1395
	static function check_extended_acl($url,$check)
1396
	{
1397
		$url_path = Vfs::parse_url($url,PHP_URL_PATH);
1398
1399
		if (is_null(self::$extended_acl))
1400
		{
1401
			self::_read_extended_acl();
1402
		}
1403
		$access = false;
1404
		foreach(self::$extended_acl as $path => $rights)
1405
		{
1406
			if ($path == $url_path || substr($url_path,0,strlen($path)+1) == $path.'/')
1407
			{
1408
				$access = ($rights & $check) == $check;
1409
				break;
1410
			}
1411
		}
1412
		if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url,$check) ".($access?"access granted by $path=$rights":'no access!!!'));
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $rights seems to be defined by a foreach iteration on line 1404. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
Comprehensibility Best Practice introduced by
The variable $path seems to be defined by a foreach iteration on line 1404. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
1413
		return $access;
1414
	}
1415
1416
	/**
1417
	 * Read the extended acl via acl::get_grants('sqlfs')
1418
	 *
1419
	 */
1420
	static protected function _read_extended_acl()
1421
	{
1422
		if ((self::$extended_acl = Api\Cache::getSession(self::EACL_APPNAME, 'extended_acl')))
1423
		{
1424
			return;		// ext. ACL read from session.
1425
		}
1426
		self::$extended_acl = array();
1427
		if (($rights = $GLOBALS['egw']->acl->get_all_location_rights(Vfs::$user,self::EACL_APPNAME)))
1428
		{
1429
			$pathes = self::id2path(array_keys($rights));
1430
		}
1431
		foreach($rights as $fs_id => $right)
1432
		{
1433
			$path = $pathes[$fs_id];
1434
			if (isset($path))
1435
			{
1436
				self::$extended_acl[$path] = (int)$right;
1437
			}
1438
		}
1439
		// sort by length descending, to allow more specific pathes to have precedence
1440
		uksort(self::$extended_acl, function($a,$b) {
1441
			return strlen($b)-strlen($a);
1442
		});
1443
		Api\Cache::setSession(self::EACL_APPNAME, 'extended_acl', self::$extended_acl);
1444
		if (self::LOG_LEVEL > 1) error_log(__METHOD__.'() '.array2string(self::$extended_acl));
1445
	}
1446
1447
	/**
1448
	 * Appname used with the acl class to store the extended acl
1449
	 */
1450
	const EACL_APPNAME = 'sqlfs';
1451
1452
	/**
1453
	 * Set or delete extended acl for a given path and owner (or delete  them if is_null($rights)
1454
	 *
1455
	 * Only root, the owner of the path or an eGW admin (only if there's no owner but a group) are allowd to set eACL's!
1456
	 *
1457
	 * @param string $path string with path
1458
	 * @param int $rights =null rights to set, or null to delete the entry
1459
	 * @param int|boolean $owner =null owner for whom to set the rights, null for the current user, or false to delete all rights for $path
1460
	 * @param int $fs_id =null fs_id to use, to not query it again (eg. because it's already deleted)
1461
	 * @return boolean true if acl is set/deleted, false on error
1462
	 */
1463
	static function eacl($path,$rights=null,$owner=null,$fs_id=null)
1464
	{
1465
		if ($path[0] != '/')
1466
		{
1467
			$path = Vfs::parse_url($path,PHP_URL_PATH);
1468
		}
1469
		if (is_null($fs_id))
1470
		{
1471
			$vfs = new self();
1472
			if (!($stat = $vfs->url_stat($path,0)))
1473
			{
1474
				if (self::LOG_LEVEL) error_log(__METHOD__."($path,$rights,$owner,$fs_id) no such file or directory!");
1475
				return false;	// $path not found
1476
			}
1477
			if (!Vfs::has_owner_rights($path,$stat))		// not group dir and user is eGW admin
1478
			{
1479
				if (self::LOG_LEVEL) error_log(__METHOD__."($path,$rights,$owner,$fs_id) permission denied!");
1480
				return false;	// permission denied
1481
			}
1482
			$fs_id = $stat['ino'];
1483
		}
1484
		if (is_null($owner))
1485
		{
1486
			$owner = Vfs::$user;
1487
		}
1488
		if (is_null($rights) || $owner === false)
1489
		{
1490
			// delete eacl
1491
			if (is_null($owner) || $owner == Vfs::$user ||
1492
				$owner < 0 && Vfs::$user && in_array($owner,$GLOBALS['egw']->accounts->memberships(Vfs::$user,true)))
1493
			{
1494
				self::$extended_acl = null;	// force new read of eACL, as there could be multiple eACL for that path
1495
			}
1496
			$ret = $GLOBALS['egw']->acl->delete_repository(self::EACL_APPNAME, $fs_id, (int)$owner, false);
1497
		}
1498
		else
1499
		{
1500
			if (isset(self::$extended_acl) && ($owner == Vfs::$user ||
1501
				$owner < 0 && Vfs::$user && in_array($owner,$GLOBALS['egw']->accounts->memberships(Vfs::$user,true))))
1502
			{
1503
				// set rights for this class, if applicable
1504
				self::$extended_acl[$path] |= $rights;
1505
			}
1506
			$ret = $GLOBALS['egw']->acl->add_repository(self::EACL_APPNAME, $fs_id, $owner, $rights, false);
1507
		}
1508
		if ($ret)
1509
		{
1510
			Api\Cache::setSession(self::EACL_APPNAME, 'extended_acl', self::$extended_acl);
1511
		}
1512
		if (self::LOG_LEVEL > 1) error_log(__METHOD__."($path,$rights,$owner,$fs_id)=".(int)$ret);
1513
		return $ret;
1514
	}
1515
1516
	/**
1517
	 * Get all ext. ACL set for a path
1518
	 *
1519
	 * Calls itself recursive, to get the parent directories
1520
	 *
1521
	 * @param string $path
1522
	 * @return array|boolean array with array('path'=>$path,'owner'=>$owner,'rights'=>$rights) or false if $path not found
1523
	 */
1524
	static function get_eacl($path)
1525
	{
1526
		$inst = new static();
1527
		if (!($stat = $inst->url_stat($path, STREAM_URL_STAT_QUIET)))
1528
		{
1529
			error_log(__METHOD__.__LINE__.' '.array2string($path).' not found!');
1530
			return false;	// not found
1531
		}
1532
		$eacls = array();
1533
		foreach($GLOBALS['egw']->acl->get_all_rights($stat['ino'],self::EACL_APPNAME) as $owner => $rights)
1534
		{
1535
			$eacls[] = array(
1536
				'path'   => $path,
1537
				'owner'  => $owner,
1538
				'rights' => $rights,
1539
				'ino'    => $stat['ino'],
1540
			);
1541
		}
1542
		if (($path = Vfs::dirname($path)))
1543
		{
1544
			$eacls = array_merge((array)self::get_eacl($path),$eacls);
1545
		}
1546
		// sort by length descending, to show precedence
1547
		usort($eacls, function($a, $b) {
1548
			return strlen($b['path']) - strlen($a['path']);
1549
		});
1550
		//error_log(__METHOD__."('$_path') returning ".array2string($eacls));
1551
		return $eacls;
1552
	}
1553
1554
	/**
1555
	 * Get the lowest file id (fs_id) for a given path
1556
	 *
1557
	 * @param string $path
1558
	 * @return integer
1559
	 */
1560
	static function get_minimum_file_id($path)
1561
	{
1562
		$vfs = new self();
1563
		$stat = $vfs->url_stat($path,0);
1564
		if ($stat['readlink'])
1565
		{
1566
			$stat = $vfs->url_stat($stat['readlink'], 0);
1567
		}
1568
		$fs_id = $stat['ino'];
1569
1570
		$query = 'SELECT MIN(B.fs_id)
1571
FROM '.self::TABLE.' as A
1572
JOIN '.self::TABLE.' AS B ON A.fs_name = B.fs_name AND A.fs_dir = B.fs_dir AND A.fs_active = '.
1573
			self::_pdo_boolean(true).' AND B.fs_active = '.self::_pdo_boolean(false).'
1574
WHERE A.fs_id=?
1575
GROUP BY A.fs_id';
1576
		if (self::LOG_LEVEL > 2)
1577
		{
1578
			$query = '/* '.__METHOD__.': '.__LINE__.' */ '.$query;
1579
		}
1580
		$stmt = self::$pdo->prepare($query);
1581
1582
		$stmt->execute(array($fs_id));
1583
		$min = $stmt->fetchColumn();
1584
1585
		return $min ? $min : $fs_id;
1586
	}
1587
1588
	/**
1589
	 * Max allowed sub-directory depth, to be able to break infinit recursion by wrongly linked directories
1590
	 */
1591
	const MAX_ID2PATH_RECURSION = 100;
1592
1593
	/**
1594
	 * Return the path of given fs_id(s)
1595
	 *
1596
	 * Searches the stat_cache first and then the db.
1597
	 * Calls itself recursive to to determine the path of the parent/directory
1598
	 *
1599
	 * @param int|array $fs_ids integer fs_id or array of them
1600
	 * @param int $recursion_count =0 internally used to break infinit recursions
1601
	 * @return false|string|array path or array or pathes indexed by fs_id, or false on error
1602
	 */
1603
	static function id2path($fs_ids, $recursion_count=0)
1604
	{
1605
		if (self::LOG_LEVEL > 1) error_log(__METHOD__.'('.array2string($fs_ids).')');
1606
		if ($recursion_count > self::MAX_ID2PATH_RECURSION)
1607
		{
1608
			error_log(__METHOD__."(".array2string($fs_ids).", $recursion_count) max recursion depth reached, probably broken filesystem!");
1609
			return false;
1610
		}
1611
		$ids = (array)$fs_ids;
1612
		$pathes = array();
1613
		// first check our stat-cache for the ids
1614
		foreach(self::$stat_cache as $path => $stat)
1615
		{
1616
			if (($key = array_search($stat['fs_id'],$ids)) !== false)
1617
			{
1618
				$pathes[$stat['fs_id']] = $path;
1619
				unset($ids[$key]);
1620
				if (!$ids)
0 ignored issues
show
Bug Best Practice introduced by
The expression $ids 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...
1621
				{
1622
					if (self::LOG_LEVEL > 1) error_log(__METHOD__.'('.array2string($fs_ids).')='.array2string($pathes).' *from stat_cache*');
1623
					return is_array($fs_ids) ? $pathes : array_shift($pathes);
1624
				}
1625
			}
1626
		}
1627
		// now search via the database
1628
		if (count($ids) > 1) $ids = array_map(function($v) { return (int)$v; }, $ids);
1629
		$query = 'SELECT fs_id,fs_dir,fs_name FROM '.self::TABLE.' WHERE fs_id'.
1630
			(count($ids) == 1 ? '='.(int)$ids[0] : ' IN ('.implode(',',$ids).')');
1631
		if (self::LOG_LEVEL > 2) $query = '/* '.__METHOD__.': '.__LINE__.' */ '.$query;
1632
1633
		if (!is_object(self::$pdo))
1634
		{
1635
			self::_pdo();
1636
		}
1637
		$stmt = self::$pdo->prepare($query);
1638
		$stmt->setFetchMode(\PDO::FETCH_ASSOC);
1639
		if (!$stmt->execute())
1640
		{
1641
			return false;	// not found
1642
		}
1643
		$parents = array();
1644
		foreach($stmt as $row)
1645
		{
1646
			if ($row['fs_dir'] > 1 && !in_array($row['fs_dir'],$parents))
1647
			{
1648
				$parents[] = $row['fs_dir'];
1649
			}
1650
			$rows[$row['fs_id']] = $row;
1651
		}
1652
		unset($stmt);
1653
1654
		if ($parents && !($parents = self::id2path($parents, $recursion_count+1)))
1655
		{
1656
			return false;	// parent not found, should never happen ...
1657
		}
1658
		if (self::LOG_LEVEL > 1) error_log(__METHOD__." trying foreach with:".print_r($rows,true)."#");
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $rows seems to be defined by a foreach iteration on line 1644. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
1659
		foreach((array)$rows as $fs_id => $row)
1660
		{
1661
			$parent = $row['fs_dir'] > 1 ? $parents[$row['fs_dir']] : '';
1662
1663
			$pathes[$fs_id] = $parent . '/' . $row['fs_name'];
1664
		}
1665
		if (self::LOG_LEVEL > 1) error_log(__METHOD__.'('.array2string($fs_ids).')='.array2string($pathes));
1666
		return is_array($fs_ids) ? $pathes : array_shift($pathes);
1667
	}
1668
1669
	/**
1670
	 * Limit filename to precision of column while keeping the extension
1671
	 *
1672
	 * @param string $name
1673
	 * @return string
1674
	 */
1675
	static protected function limit_filename($name)
1676
	{
1677
		static $fs_name_precision = null;
1678
		if (!isset($fs_name_precision))
1679
		{
1680
			$fs_name_precision = $GLOBALS['egw']->db->get_column_attribute('fs_name', self::TABLE, 'phpgwapi', 'precision');
1681
		}
1682
		if (mb_strlen($name) > $fs_name_precision)
1683
		{
1684
			$parts = explode('.', $name);
1685
			if ($parts > 1 && mb_strlen($extension = '.'.array_pop($parts)) <= $fs_name_precision)
1686
			{
1687
				$name = mb_substr(implode('.', $parts), 0, $fs_name_precision-mb_strlen($extension)).$extension;
1688
			}
1689
			else
1690
			{
1691
				$name = mb_substr(implode('.', $parts), 0, $fs_name_precision);
1692
			}
1693
		}
1694
		return $name;
1695
	}
1696
1697
	/**
1698
	 * Convert a sqlfs-file-info into a stat array
1699
	 *
1700
	 * @param array $info
1701
	 * @return array
1702
	 */
1703
	static protected function _vfsinfo2stat($info)
1704
	{
1705
		$stat = array(
1706
			'ino'   => $info['fs_id'],
1707
			'name'  => $info['fs_name'],
1708
			'mode'  => $info['fs_mode'] |
1709
				($info['fs_mime'] == self::DIR_MIME_TYPE ? self::MODE_DIR :
1710
				($info['fs_mime'] == self::SYMLINK_MIME_TYPE ? self::MODE_LINK : self::MODE_FILE)),	// required by the stream wrapper
1711
			'size'  => $info['fs_size'],
1712
			'uid'   => $info['fs_uid'],
1713
			'gid'   => $info['fs_gid'],
1714
			'mtime' => strtotime($info['fs_modified']),
1715
			'ctime' => strtotime($info['fs_created']),
1716
			'nlink' => $info['fs_mime'] == self::DIR_MIME_TYPE ? 2 : 1,
1717
			// eGW addition to return some extra values
1718
			'mime'  => $info['fs_mime'],
1719
			'readlink' => $info['fs_link'],
1720
		);
1721
		if (self::LOG_LEVEL > 1) error_log(__METHOD__."($info[name]) = ".array2string($stat));
1722
		return $stat;
1723
	}
1724
1725
	/**
1726
	 * Maximum value for a single hash element (should be 10^N): 10, 100 (default), 1000, ...
1727
	 *
1728
	 * DONT change this value, once you have files stored, they will no longer be found!
1729
	 */
1730
	const HASH_MAX = 100;
1731
1732
	/**
1733
	 * Return the path of the stored content of a file if $this->operation == self::STORE2FS
1734
	 *
1735
	 * To limit the number of files stored in one directory, we create a hash from the fs_id:
1736
	 * 	1     --> /00/1
1737
	 * 	34    --> /00/34
1738
	 * 	123   --> /01/123
1739
	 * 	4567  --> /45/4567
1740
	 * 	99999 --> /09/99/99999
1741
	 * --> so one directory contains maximum 2 * HASH_MAY entries (HASH_MAX dirs + HASH_MAX files)
1742
	 * @param int $id id of the file
1743
	 * @return string
1744
	 */
1745
	static function _fs_path($id)
1746
	{
1747
		if (!is_numeric($id))
0 ignored issues
show
introduced by
The condition is_numeric($id) is always true.
Loading history...
1748
		{
1749
			throw new Api\Exception\WrongParameter(__METHOD__."(id=$id) id has to be an integer!");
1750
		}
1751
		if (!isset($GLOBALS['egw_info']['server']['files_dir']))
1752
		{
1753
			if (is_object($GLOBALS['egw_setup']->db))	// if we run under setup, query the db for the files dir
1754
			{
1755
				$GLOBALS['egw_info']['server']['files_dir'] = $GLOBALS['egw_setup']->db->select('egw_config','config_value',array(
1756
					'config_name' => 'files_dir',
1757
					'config_app' => 'phpgwapi',
1758
				),__LINE__,__FILE__)->fetchColumn();
1759
			}
1760
		}
1761
		if (!$GLOBALS['egw_info']['server']['files_dir'])
1762
		{
1763
			throw  new Api\Exception\AssertionFailed("\$GLOBALS['egw_info']['server']['files_dir'] not set!");
1764
		}
1765
		$hash = array();
1766
		$n = $id;
1767
		while(($n = (int) ($n / self::HASH_MAX)))
1768
		{
1769
			$hash[] = sprintf('%02d',$n % self::HASH_MAX);
1770
		}
1771
		if (!$hash) $hash[] = '00';		// we need at least one directory, to not conflict with the dir-names
1772
		array_unshift($hash,$id);
1773
1774
		$path = '/sqlfs/'.implode('/',array_reverse($hash));
1775
		//error_log(__METHOD__."($id) = '$path'");
1776
		return $GLOBALS['egw_info']['server']['files_dir'].$path;
1777
	}
1778
1779
	/**
1780
	 * Replace the password of an url with '...' for error messages
1781
	 *
1782
	 * @param string &$url
1783
	 */
1784
	static protected function _remove_password(&$url)
1785
	{
1786
		$parts = Vfs::parse_url($url);
1787
1788
		if ($parts['pass'] || $parts['scheme'])
1789
		{
1790
			$url = $parts['scheme'].'://'.($parts['user'] ? $parts['user'].($parts['pass']?':...':'').'@' : '').
1791
				$parts['host'].$parts['path'];
1792
		}
1793
	}
1794
1795
	/**
1796
	 * Get storage mode from url (get parameter 'storage', eg. ?storage=db)
1797
	 *
1798
	 * @param string|array $url complete url or array of url-parts from parse_url
1799
	 * @return int self::STORE2FS or self::STORE2DB
1800
	 */
1801
	static function url2operation($url)
1802
	{
1803
		$operation = self::DEFAULT_OPERATION;
1804
1805
		if (strpos(is_array($url) ? $url['query'] : $url,'storage=') !== false)
1806
		{
1807
			$query = null;
1808
			parse_str(is_array($url) ? $url['query'] : Vfs::parse_url($url,PHP_URL_QUERY), $query);
1809
			switch ($query['storage'])
1810
			{
1811
				case 'db':
1812
					$operation = self::STORE2DB;
1813
					break;
1814
				case 'fs':
1815
				default:
1816
					$operation = self::STORE2FS;
1817
					break;
1818
			}
1819
		}
1820
		//error_log(__METHOD__."('$url') = $operation (1=DB, 2=FS)");
1821
		return $operation;
1822
	}
1823
1824
	/**
1825
	 * Store properties for a single ressource (file or dir)
1826
	 *
1827
	 * @param string|int $path string with path or integer fs_id
1828
	 * @param array $props array of array with values for keys 'name', 'ns', 'val' (null to delete the prop)
1829
	 * @return boolean true if props are updated, false otherwise (eg. ressource not found)
1830
	 */
1831
	static function proppatch($path,array $props)
1832
	{
1833
		static $inst = null;
1834
		if (self::LOG_LEVEL > 1) error_log(__METHOD__."(".array2string($path).','.array2string($props));
1835
		if (!is_numeric($path))
1836
		{
1837
			if (!isset($inst)) $inst = new self();
1838
			if (!($stat = $inst->url_stat($path,0)))
1839
			{
1840
				return false;
1841
			}
1842
			$id = $stat['ino'];
1843
		}
1844
		elseif(!($path = self::id2path($id=$path)))
1845
		{
1846
			return false;
1847
		}
1848
		if (!Vfs::check_access($path,Api\Acl::EDIT,$stat))
1849
		{
1850
			return false;	// permission denied
1851
		}
1852
		$ins_stmt = $del_stmt = null;
1853
		foreach($props as &$prop)
1854
		{
1855
			if (!isset($prop['ns'])) $prop['ns'] = Vfs::DEFAULT_PROP_NAMESPACE;
1856
1857
			if (!isset($prop['val']) || self::$pdo_type != 'mysql')	// for non mysql, we have to delete the prop anyway, as there's no REPLACE!
1858
			{
1859
				if (!isset($del_stmt))
1860
				{
1861
					$del_stmt = self::$pdo->prepare('DELETE FROM '.self::PROPS_TABLE.' WHERE fs_id=:fs_id AND prop_namespace=:prop_namespace AND prop_name=:prop_name');
1862
				}
1863
				$del_stmt->execute(array(
1864
					'fs_id'          => $id,
1865
					'prop_namespace' => $prop['ns'],
1866
					'prop_name'      => $prop['name'],
1867
				));
1868
			}
1869
			if (isset($prop['val']))
1870
			{
1871
				if (!isset($ins_stmt))
1872
				{
1873
					$ins_stmt = self::$pdo->prepare((self::$pdo_type == 'mysql' ? 'REPLACE' : 'INSERT').
1874
						' INTO '.self::PROPS_TABLE.' (fs_id,prop_namespace,prop_name,prop_value) VALUES (:fs_id,:prop_namespace,:prop_name,:prop_value)');
1875
				}
1876
				if (!$ins_stmt->execute(array(
1877
					'fs_id'          => $id,
1878
					'prop_namespace' => $prop['ns'],
1879
					'prop_name'      => $prop['name'],
1880
					'prop_value'     => $prop['val'],
1881
				)))
1882
				{
1883
					return false;
1884
				}
1885
			}
1886
		}
1887
		return true;
1888
	}
1889
1890
	/**
1891
	 * Read properties for a ressource (file, dir or all files of a dir)
1892
	 *
1893
	 * @param array|string|int $path_ids (array of) string with path or integer fs_id
1894
	 * @param string $ns ='http://egroupware.org/' namespace if propfind should be limited to a single one, use null for all
1895
	 * @return array|boolean false on error ($path_ids does not exist), array with props (values for keys 'name', 'ns', 'value'), or
1896
	 * 	fs_id/path => array of props for $depth==1 or is_array($path_ids)
1897
	 */
1898
	static function propfind($path_ids,$ns=Vfs::DEFAULT_PROP_NAMESPACE)
1899
	{
1900
		static $inst = null;
1901
1902
		$ids = is_array($path_ids) ? $path_ids : array($path_ids);
1903
		foreach($ids as &$id)
1904
		{
1905
			if (!is_numeric($id))
1906
			{
1907
				if (!isset($inst)) $inst = new self();
1908
				if (!($stat = $inst->url_stat($id,0)))
1909
				{
1910
					if (self::LOG_LEVEL) error_log(__METHOD__."(".array2string($path_ids).",$ns) path '$id' not found!");
1911
					return false;
1912
				}
1913
				$id = $stat['ino'];
1914
			}
1915
		}
1916
		if (count($ids) >= 1) $ids = array_map(function($v) { return (int)$v; }, $ids);
1917
		$query = 'SELECT * FROM '.self::PROPS_TABLE.' WHERE (fs_id'.
1918
			(count($ids) == 1 ? '='.(int)implode('',$ids) : ' IN ('.implode(',',$ids).')').')'.
1919
			(!is_null($ns) ? ' AND prop_namespace=?' : '');
0 ignored issues
show
introduced by
The condition is_null($ns) is always false.
Loading history...
1920
		if (self::LOG_LEVEL > 2) $query = '/* '.__METHOD__.': '.__LINE__.' */ '.$query;
1921
1922
		$stmt = self::$pdo->prepare($query);
1923
		$stmt->setFetchMode(\PDO::FETCH_ASSOC);
1924
		$stmt->execute(!is_null($ns) ? array($ns) : array());
0 ignored issues
show
introduced by
The condition is_null($ns) is always false.
Loading history...
1925
1926
		$props = array();
1927
		foreach($stmt as $row)
1928
		{
1929
			$props[$row['fs_id']][] = array(
1930
				'val'  => $row['prop_value'],
1931
				'name' => $row['prop_name'],
1932
				'ns'   => $row['prop_namespace'],
1933
			);
1934
		}
1935
		if (!is_array($path_ids))
1936
		{
1937
			$props = $props[$row['fs_id']] ? $props[$row['fs_id']] : array();	// return empty array for no props
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $row seems to be defined by a foreach iteration on line 1927. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
1938
		}
1939
		elseif ($props && isset($stat) && is_array($id2path = self::id2path(array_keys($props))))	// need to map fs_id's to pathes
1940
		{
1941
			foreach($id2path as $id => $path)
1942
			{
1943
				$props[$path] =& $props[$id];
1944
				unset($props[$id]);
1945
			}
1946
		}
1947
		if (self::LOG_LEVEL > 1)
1948
		{
1949
			foreach((array)$props as $k => $v)
1950
			{
1951
				error_log(__METHOD__."($path_ids,$ns) $k => ".array2string($v));
1952
			}
1953
		}
1954
		return $props;
1955
	}
1956
1957
	/**
1958
	 * Register __CLASS__ for self::SCHEMA
1959
	 */
1960
	public static function register()
1961
	{
1962
		stream_register_wrapper(self::SCHEME, __CLASS__);
1963
	}
1964
}
1965
1966
StreamWrapper::register();
1967