Passed
Push — 16.1 ( 3ae7fc...22ba35 )
by Ralf
12:47
created

StreamWrapper::mkdir()   C

Complexity

Conditions 11
Paths 7

Size

Total Lines 35
Code Lines 19

Duplication

Lines 8
Ratio 22.86 %

Importance

Changes 0
Metric Value
cc 11
eloc 19
nc 7
nop 3
dl 8
loc 35
rs 5.2653
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * EGroupware API: VFS - stream wrapper interface
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;
15
16
use EGroupware\Api\Vfs;
17
use EGroupware\Api;
18
19
/**
20
 * eGroupWare API: VFS - stream wrapper interface
21
 *
22
 * The new vfs stream wrapper uses a kind of fstab to mount different filesystems / stream wrapper types
23
 * together for eGW's virtual file system.
24
 *
25
 * @link http://www.php.net/manual/en/function.stream-wrapper-register.php
26
 */
27
class StreamWrapper implements StreamWrapperIface
28
{
29
	/**
30
	 * Scheme / protocol used for this stream-wrapper
31
	 */
32
	const SCHEME = 'vfs';
33
	/**
34
	 * Mime type of directories, the old vfs used 'Directory', while eg. WebDAV uses 'httpd/unix-directory'
35
	 */
36
	const DIR_MIME_TYPE = 'httpd/unix-directory';
37
	/**
38
	 * Should unreadable entries in a not writable directory be hidden, default yes
39
	 */
40
	const HIDE_UNREADABLES = true;
41
42
	/**
43
	 * optional context param when opening the stream, null if no context passed
44
	 *
45
	 * @var mixed
46
	 */
47
	var $context;
48
	/**
49
	 * mode-bits, which have to be set for links
50
	 */
51
	const MODE_LINK = 0120000;
52
53
	/**
54
	 * How much should be logged to the apache error-log
55
	 *
56
	 * 0 = Nothing
57
	 * 1 = only errors
58
	 * 2 = all function calls and errors (contains passwords too!)
59
	 */
60
	const LOG_LEVEL = 1;
61
62
	/**
63
	 * Maximum depth of symlinks, if exceeded url_stat will return false
64
	 *
65
	 * Used to prevent infinit recursion by circular links
66
	 */
67
	const MAX_SYMLINK_DEPTH = 10;
68
69
	/**
70
	 * Our fstab in the form mount-point => url
71
	 *
72
	 * The entry for root has to be the first, or more general if you mount into subdirs the parent has to be before!
73
	 *
74
	 * @var array
75
	 */
76
	protected static $fstab = array(
77
		'/' => 'sqlfs://$host/',
78
		'/apps' => 'links://$host/apps',
79
	);
80
81
	/**
82
	 * stream / ressouce this class is opened for by stream_open
83
	 *
84
	 * @var ressource
85
	 */
86
	private $opened_stream;
87
	/**
88
	 * Mode of opened stream, eg. "r" or "w"
89
	 *
90
	 * @var string
91
	 */
92
	private $opened_stream_mode;
93
	/**
94
	 * Path of opened stream
95
	 *
96
	 * @var string
97
	 */
98
	private $opened_stream_path;
99
	/**
100
	 * URL of opened stream
101
	 *
102
	 * @var string
103
	 */
104
	private $opened_stream_url;
105
	/**
106
	 * Opened stream is a new file, false for existing files
107
	 *
108
	 * @var boolean
109
	 */
110
	private $opened_stream_is_new;
111
	/**
112
	 * directory-ressouce this class is opened for by dir_open
113
	 *
114
	 * @var ressource
115
	 */
116
	private $opened_dir;
117
	/**
118
	 * URL of the opened dir, used to build the complete URL of files in the dir
119
	 *
120
	 * @var string
121
	 */
122
	private $opened_dir_url;
123
124
	/**
125
	 * Options for the opened directory
126
	 * (backup, etc.)
127
	 */
128
	protected $dir_url_params = array();
129
130
	/**
131
	 * Flag if opened dir is writable, in which case we return un-readable entries too
132
	 *
133
	 * @var boolean
134
	 */
135
	private $opened_dir_writable;
136
	/**
137
	 * Extra dirs from our fstab in the current opened dir
138
	 *
139
	 * @var array
140
	 */
141
	private $extra_dirs;
142
	/**
143
	 * Pointer in the extra dirs
144
	 *
145
	 * @var int
146
	 */
147
	private $extra_dir_ptr;
148
149
	private static $wrappers;
150
151
	/**
152
	 * Resolve the given path according to our fstab AND symlinks
153
	 *
154
	 * @param string $_path
155
	 * @param boolean $file_exists =true true if file needs to exists, false if not
156
	 * @param boolean $resolve_last_symlink =true
157
	 * @param array|boolean &$stat=null on return: stat of existing file or false for non-existing files
158
	 * @return string|boolean false if the url cant be resolved, should not happen if fstab has a root entry
159
	 */
160
	function resolve_url_symlinks($_path,$file_exists=true,$resolve_last_symlink=true,&$stat=null)
161
	{
162
		$path = self::get_path($_path);
163
164
		if (!($stat = $this->url_stat($path,$resolve_last_symlink?0:STREAM_URL_STAT_LINK)) && !$file_exists)
165
		{
166
			$url = null;
167
			$stat = self::check_symlink_components($path,0,$url);
168
			if (self::LOG_LEVEL > 1) $log = " (check_symlink_components('$path',0,'$url') = $stat)";
169
		}
170
		else
171
		{
172
			$url = $stat['url'];
173
		}
174
		// if the url resolves to a symlink to the vfs, resolve this vfs:// url direct
175
		if ($url && Vfs::parse_url($url,PHP_URL_SCHEME) == self::SCHEME)
176
		{
177
			$url = self::resolve_url(Vfs::parse_url($url,PHP_URL_PATH));
178
		}
179
		if (self::LOG_LEVEL > 1) error_log(__METHOD__."($path,file_exists=$file_exists,resolve_last_symlink=$resolve_last_symlink) = '$url'$log");
180
		return $url;
181
	}
182
183
	/**
184
	 * Cache of already resolved urls
185
	 *
186
	 * @var array with path => target
187
	 */
188
	private static $resolve_url_cache = array();
189
190
	/**
191
	 * Resolve the given path according to our fstab
192
	 *
193
	 * @param string $_path
194
	 * @param boolean $do_symlink =true is a direct match allowed, default yes (must be false for a lstat or readlink!)
195
	 * @param boolean $use_symlinkcache =true
196
	 * @param boolean $replace_user_pass_host =true replace $user,$pass,$host in url, default true, if false result is not cached
197
	 * @param boolean $fix_url_query =false true append relativ path to url query parameter, default not
198
	 * @return string|boolean false if the url cant be resolved, should not happen if fstab has a root entry
199
	 */
200
	static function resolve_url($_path,$do_symlink=true,$use_symlinkcache=true,$replace_user_pass_host=true,$fix_url_query=false)
201
	{
202
		$path = self::get_path($_path);
203
204
		// we do some caching here
205
		if (isset(self::$resolve_url_cache[$path]) && $replace_user_pass_host)
206
		{
207
			if (self::LOG_LEVEL > 1) error_log(__METHOD__."('$path') = '".self::$resolve_url_cache[$path]."' (from cache)");
208
			return self::$resolve_url_cache[$path];
209
		}
210
		// check if we can already resolve path (or a part of it) with a known symlinks
211
		if ($use_symlinkcache)
212
		{
213
			$path = self::symlinkCache_resolve($path,$do_symlink);
0 ignored issues
show
Bug introduced by
It seems like $path can also be of type boolean; however, EGroupware\Api\Vfs\Strea...:symlinkCache_resolve() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
214
		}
215
		// setting default user, passwd and domain, if it's not contained int the url
216
		static $defaults=null;
217
		if (is_null($defaults))
218
		{
219
			$defaults = array(
220
				'user' => $GLOBALS['egw_info']['user']['account_lid'],
221
				'pass' => urlencode($GLOBALS['egw_info']['user']['passwd']),
222
				'host' => $GLOBALS['egw_info']['user']['domain'],
223
				'home' => str_replace(array('\\\\','\\'),array('','/'),$GLOBALS['egw_info']['user']['homedirectory']),
224
			);
225
		}
226
		$parts = array_merge(Vfs::parse_url($path),$defaults);
227
		if (!$parts['host']) $parts['host'] = 'default';	// otherwise we get an invalid url (scheme:///path/to/something)!
228
229
		if (!empty($parts['scheme']) && $parts['scheme'] != self::SCHEME)
230
		{
231
			if (self::LOG_LEVEL > 1) error_log(__METHOD__."('$path') = '$path' (path is already an url)");
232
			return $path;	// path is already a non-vfs url --> nothing to do
233
		}
234
		if (empty($parts['path'])) $parts['path'] = '/';
235
236
		foreach(array_reverse(self::$fstab) as $mounted => $url)
237
		{
238
			if ($mounted == '/' || $mounted == $parts['path'] || $mounted.'/' == substr($parts['path'],0,strlen($mounted)+1))
239
			{
240
				$scheme = Vfs::parse_url($url,PHP_URL_SCHEME);
241
				if (is_null(self::$wrappers) || !in_array($scheme,self::$wrappers))
242
				{
243
					self::load_wrapper($scheme);
244
				}
245
				if (($relative = substr($parts['path'],strlen($mounted))))
246
				{
247
					$url = Vfs::concat($url,$relative);
248
				}
249
				// if url contains url parameter, eg. from filesystem streamwrapper, we need to append relative path here too
250
				$matches = null;
251
				if ($fix_url_query && preg_match('|([?&]url=)([^&]+)|', $url, $matches))
252
				{
253
					$url = str_replace($matches[0], $matches[1].Vfs::concat($matches[2], substr($parts['path'],strlen($mounted))), $url);
254
				}
255
256
				if ($replace_user_pass_host)
257
				{
258
					$url = str_replace(array('$user','$pass','$host','$home'),array($parts['user'],$parts['pass'],$parts['host'],$parts['home']),$url);
259
				}
260
				if ($parts['query']) $url .= '?'.$parts['query'];
261
				if ($parts['fragment']) $url .= '#'.$parts['fragment'];
262
263
				if (self::LOG_LEVEL > 1) error_log(__METHOD__."('$path') = '$url'");
264
265
				if ($replace_user_pass_host) self::$resolve_url_cache[$path] = $url;
266
267
				return $url;
268
			}
269
		}
270
		if (self::LOG_LEVEL > 0) error_log(__METHOD__."('$path') can't resolve path!\n");
271
		trigger_error(__METHOD__."($path) can't resolve path!\n",E_USER_WARNING);
272
		return false;
273
	}
274
275
	/**
276
	 * Returns mount url of a full url returned by resolve_url
277
	 *
278
	 * @param string $fullurl full url returned by resolve_url
279
	 * @return string|NULL mount url or null if not found
280
	 */
281
	static function mount_url($fullurl)
282
	{
283
		foreach(array_reverse(self::$fstab) as $url)
284
		{
285
			list($url_no_query) = explode('?',$url);
286
			if (substr($fullurl,0,1+strlen($url_no_query)) === $url_no_query.'/')
287
			{
288
				return $url;
289
			}
290
		}
291
		return null;
292
	}
293
294
	/**
295
	 * This method is called immediately after your stream object is created.
296
	 *
297
	 * @param string $path URL that was passed to fopen() and that this object is expected to retrieve
298
	 * @param string $mode mode used to open the file, as detailed for fopen()
299
	 * @param int $options additional flags set by the streams API (or'ed together):
300
	 * - STREAM_USE_PATH      If path is relative, search for the resource using the include_path.
301
	 * - STREAM_REPORT_ERRORS If this flag is set, you are responsible for raising errors using trigger_error() during opening of the stream.
302
	 *                        If this flag is not set, you should not raise any errors.
303
	 * @param string $opened_path full path of the file/resource, if the open was successfull and STREAM_USE_PATH was set
304
	 * @return boolean true if the ressource was opened successful, otherwise false
305
	 */
306
	function stream_open ( $path, $mode, $options, &$opened_path )
307
	{
308
		unset($options,$opened_path);	// not used but required by function signature
309
		$this->opened_stream = null;
310
311
		$stat = null;
312
		if (!($url = $this->resolve_url_symlinks($path,$mode[0]=='r',true,$stat)))
313
		{
314
			return false;
315
		}
316
		if (str_replace('b', '', $mode) != 'r' && self::url_is_readonly($url))
317
		{
318
			return false;
319
		}
320
		if (!($this->opened_stream = $this->context ?
321
			fopen($url, $mode, false, $this->context) : fopen($url, $mode, false)))
322
		{
323
			return false;
324
		}
325
		$this->opened_stream_mode = $mode;
326
		$this->opened_stream_path = $path[0] == '/' ? $path : Vfs::parse_url($path, PHP_URL_PATH);
0 ignored issues
show
Documentation Bug introduced by
It seems like $path[0] == '/' ? $path ...rl($path, PHP_URL_PATH) can also be of type array or boolean. However, the property $opened_stream_path is declared as type string. 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...
327
		$this->opened_stream_url = $url;
0 ignored issues
show
Documentation Bug introduced by
It seems like $url can also be of type boolean. However, the property $opened_stream_url is declared as type string. 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...
328
		$this->opened_stream_is_new = !$stat;
329
330
		// are we requested to treat the opened file as new file (only for files opened NOT for reading)
331
		if ($mode[0] != 'r' && !$this->opened_stream_is_new && $this->context &&
332
			($opts = stream_context_get_params($this->context)) &&
333
			$opts['options'][self::SCHEME]['treat_as_new'])
334
		{
335
			$this->opened_stream_is_new = true;
336
			//error_log(__METHOD__."($path,$mode,...) stat=$stat, context=".array2string($opts)." --> ".array2string($this->opened_stream_is_new));
337
		}
338
		return true;
339
	}
340
341
	/**
342
	 * This method is called when the stream is closed, using fclose().
343
	 *
344
	 * You must release any resources that were locked or allocated by the stream.
345
	 *
346
	 * VFS calls either "vfs_read", "vfs_added" or "vfs_modified" hook
347
	 */
348
	function stream_close ( )
349
	{
350
		$ret = fclose($this->opened_stream);
351
		// clear PHP's stat cache, it contains wrong size of just closed file,
352
		// causing eg. notifications to be ignored, because of previous size 0, when using WebDAV
353
		clearstatcache(false);
354
355
		if (!class_exists('setup_process', false))
356
		{
357
			Api\Hooks::process(array(
358
				'location' => str_replace('b','',$this->opened_stream_mode) == 'r' ? 'vfs_read' :
359
					($this->opened_stream_is_new ? 'vfs_added' : 'vfs_modified'),
360
				'path' => $this->opened_stream_path,
361
				'mode' => $this->opened_stream_mode,
362
				'url'  => $this->opened_stream_url,
363
			),'',true);
364
		}
365
		$this->opened_stream = $this->opened_stream_mode = $this->opened_stream_path = $this->opened_stream_url = $this->opened_stream_is_new = null;
366
367
		return $ret;
368
	}
369
370
	/**
371
	 * This method is called in response to fread() and fgets() calls on the stream.
372
	 *
373
	 * You must return up-to count bytes of data from the current read/write position as a string.
374
	 * If there are less than count bytes available, return as many as are available.
375
	 * If no more data is available, return either FALSE or an empty string.
376
	 * You must also update the read/write position of the stream by the number of bytes that were successfully read.
377
	 *
378
	 * @param int $count
379
	 * @return string/false up to count bytes read or false on EOF
380
	 */
381
	function stream_read ( $count )
382
	{
383
		return fread($this->opened_stream,$count);
384
	}
385
386
	/**
387
	 * This method is called in response to fwrite() calls on the stream.
388
	 *
389
	 * You should store data into the underlying storage used by your stream.
390
	 * If there is not enough room, try to store as many bytes as possible.
391
	 * You should return the number of bytes that were successfully stored in the stream, or 0 if none could be stored.
392
	 * You must also update the read/write position of the stream by the number of bytes that were successfully written.
393
	 *
394
	 * @param string $data
395
	 * @return integer
396
	 */
397
	function stream_write ( $data )
398
	{
399
		return fwrite($this->opened_stream,$data);
400
	}
401
402
 	/**
0 ignored issues
show
Coding Style introduced by
There is some trailing whitespace on this line which should be avoided as per coding-style.
Loading history...
403
 	 * This method is called in response to feof() calls on the stream.
404
 	 *
405
 	 * Important: PHP 5.0 introduced a bug that wasn't fixed until 5.1: the return value has to be the oposite!
406
 	 *
407
 	 * if(version_compare(PHP_VERSION,'5.0','>=') && version_compare(PHP_VERSION,'5.1','<'))
408
  	 * {
409
 	 * 		$eof = !$eof;
410
 	 * }
411
  	 *
412
 	 * @return boolean true if the read/write position is at the end of the stream and no more data availible, false otherwise
413
 	 */
414
	function stream_eof ( )
415
	{
416
		return feof($this->opened_stream);
417
	}
418
419
	/**
420
	 * This method is called in response to ftell() calls on the stream.
421
	 *
422
	 * @return integer current read/write position of the stream
423
	 */
424
 	function stream_tell ( )
425
 	{
426
 		return ftell($this->opened_stream);
427
 	}
428
429
 	/**
0 ignored issues
show
Coding Style introduced by
There is some trailing whitespace on this line which should be avoided as per coding-style.
Loading history...
430
 	 * This method is called in response to fseek() calls on the stream.
431
 	 *
432
 	 * You should update the read/write position of the stream according to offset and whence.
433
 	 * See fseek() for more information about these parameters.
434
 	 *
435
 	 * @param integer $offset
436
 	 * @param integer $whence	SEEK_SET - 0 - Set position equal to offset bytes
437
 	 * 							SEEK_CUR - 1 - Set position to current location plus offset.
438
 	 * 							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.)
439
 	 * @return boolean TRUE if the position was updated, FALSE otherwise.
440
 	 */
441
	function stream_seek ( $offset, $whence )
442
	{
443
		return !fseek($this->opened_stream,$offset,$whence);	// fseek returns 0 on success and -1 on failure
444
	}
445
446
	/**
447
	 * This method is called in response to fflush() calls on the stream.
448
	 *
449
	 * If you have cached data in your stream but not yet stored it into the underlying storage, you should do so now.
450
	 *
451
	 * @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.
452
	 */
453
	function stream_flush ( )
454
	{
455
		return fflush($this->opened_stream);
456
	}
457
458
	/**
459
	 * This method is called in response to fstat() calls on the stream.
460
	 *
461
	 * If you plan to use your wrapper in a require_once you need to define stream_stat().
462
	 * If you plan to allow any other tests like is_file()/is_dir(), you have to define url_stat().
463
	 * stream_stat() must define the size of the file, or it will never be included.
464
	 * url_stat() must define mode, or is_file()/is_dir()/is_executable(), and any of those functions affected by clearstatcache() simply won't work.
465
	 * It's not documented, but directories must be a mode like 040777 (octal), and files a mode like 0100666.
466
	 * If you wish the file to be executable, use 7s instead of 6s.
467
	 * The last 3 digits are exactly the same thing as what you pass to chmod.
468
	 * 040000 defines a directory, and 0100000 defines a file.
469
	 *
470
	 * @return array containing the same values as appropriate for the stream.
471
	 */
472
	function stream_stat ( )
473
	{
474
		return fstat($this->opened_stream);
475
	}
476
477
	/**
478
	 * StreamWrapper method (PHP 5.4+) for touch, chmod, chown and chgrp
479
	 *
480
	 * @param string $path
481
	 * @param int $option STREAM_META_(TOUCH|ACCESS|((OWNER|GROUP)(_NAME)?))
482
	 * @param array|int|string $value
483
	 * - STREAM_META_TOUCH array($time, $atime)
484
	 * - STREAM_META_ACCESS int
485
	 * - STREAM_(OWNER|GROUP) int
486
	 * - STREAM_(OWNER|GROUP)_NAME string
487
	 * @return boolean true on success, false on failure
488
	 */
489
	function stream_metadata($path, $option, $value)
490
	{
491
		if (!($url = $this->resolve_url_symlinks($path, $option != STREAM_META_TOUCH, false)))	// true,false file need to exist, but do not resolve last component
492
		{
493
			return false;
494
		}
495
		if (self::url_is_readonly($url))
496
		{
497
			return false;
498
		}
499 View Code Duplication
		if (self::LOG_LEVEL > 1) error_log(__METHOD__."('$path', $option, ".array2string($value).") url=$url");
500
501
		switch($option)
502
		{
503
			case STREAM_META_TOUCH:
504
				return touch($url, $value[0]);	// atime is not supported
505
506
			case STREAM_META_ACCESS:
507
				return chmod($url, $value);
508
509 View Code Duplication
			case STREAM_META_OWNER_NAME:
510
				if (($value = $GLOBALS['egw']->accounts->name2id($value, 'account_lid', 'u')) === false)
511
					return false;
512
				// fall through
513
			case STREAM_META_OWNER:
514
				return chown($url, $value);
515
516 View Code Duplication
			case STREAM_META_GROUP_NAME:
517
				if (($value = $GLOBALS['egw']->accounts->name2id($value, 'account_lid', 'g')) === false)
518
					return false;
519
				// fall through
520
			case STREAM_META_GROUP:
521
				return chgrp($url, $value);
522
		}
523
		return false;
524
	}
525
526
	/**
527
	 * This method is called in response to unlink() calls on URL paths associated with the wrapper.
528
	 *
529
	 * It should attempt to delete the item specified by path.
530
	 * In order for the appropriate error message to be returned, do not define this method if your wrapper does not support unlinking!
531
	 *
532
	 * @param string $path
533
	 * @return boolean TRUE on success or FALSE on failure
534
	 */
535
	function unlink ( $path )
536
	{
537
		if (!($url = $this->resolve_url_symlinks($path,true,false)))	// true,false file need to exist, but do not resolve last component
538
		{
539
			return false;
540
		}
541
		if (self::url_is_readonly($url))
542
		{
543
			return false;
544
		}
545
		$stat = $this->url_stat($path, STREAM_URL_STAT_LINK);
546
547
		self::symlinkCache_remove($path);
548
		$ok = unlink($url);
549
550
		// call "vfs_unlink" hook only after successful unlink, with data from (not longer possible) stat call
551 View Code Duplication
		if ($ok && !class_exists('setup_process', false))
552
		{
553
			Api\Hooks::process(array(
554
				'location' => 'vfs_unlink',
555
				'path' => $path[0] == '/' ? $path : Vfs::parse_url($path, PHP_URL_PATH),
556
				'url'  => $url,
557
				'stat' => $stat,
558
			),'',true);
559
		}
560
		return $ok;
561
	}
562
563
	/**
564
	 * This method is called in response to rename() calls on URL paths associated with the wrapper.
565
	 *
566
	 * It should attempt to rename the item specified by path_from to the specification given by path_to.
567
	 * In order for the appropriate error message to be returned, do not define this method if your wrapper does not support renaming.
568
	 *
569
	 * The regular filesystem stream-wrapper returns an error, if $url_from and $url_to are not either both files or both dirs!
570
	 *
571
	 * @param string $path_from
572
	 * @param string $path_to
573
	 * @return boolean TRUE on success or FALSE on failure
574
	 * @throws Exception\ProtectedDirectory if trying to delete a protected directory, see Vfs::isProtected()
575
	 */
576
	function rename ( $path_from, $path_to )
577
	{
578
		if (Vfs::isProtectedDir($path_from))
579
		{
580
			throw new Exception\ProtectedDirectory("Renaming protected directory '$path_from' rejected!");
581
		}
582
		if (!($url_from = $this->resolve_url_symlinks($path_from,true,false)) ||
583
			!($url_to = $this->resolve_url_symlinks($path_to,false)))
584
		{
585
			return false;
586
		}
587
		// refuse to modify readonly target (eg. readonly share)
588
		if (self::url_is_readonly($url_to))
589
		{
590
			return false;
591
		}
592
		// if file is moved from one filesystem / wrapper to an other --> copy it (rename fails cross wrappers)
593
		if (Vfs::parse_url($url_from,PHP_URL_SCHEME) == Vfs::parse_url($url_to,PHP_URL_SCHEME))
594
		{
595
			self::symlinkCache_remove($path_from);
596
			$ret = rename($url_from,$url_to);
597
		}
598
		elseif (($from = fopen($url_from,'r')) && ($to = fopen($url_to,'w')))
599
		{
600
			$ret = stream_copy_to_stream($from,$to) !== false;
601
			fclose($from);
602
			fclose($to);
603
			if ($ret) $this->unlink($path_from);
604
		}
605
		else
606
		{
607
			$ret = false;
608
		}
609
		if (self::LOG_LEVEL > 1 || self::LOG_LEVEL && !$ret)
610
		{
611
			error_log(__METHOD__."('$path_from','$path_to') url_from='$url_from', url_to='$url_to' returning ".array2string($ret));
612
		}
613
		// call "vfs_rename" hook
614
		if ($ret && !class_exists('setup_process', false))
615
		{
616
			Api\Hooks::process(array(
617
				'location' => 'vfs_rename',
618
				'from' => $path_from[0] == '/' ? $path_from : Vfs::parse_url($path_from, PHP_URL_PATH),
619
				'to' => $path_to[0] == '/' ? $path_to : Vfs::parse_url($path_to, PHP_URL_PATH),
620
				'url_from' => $url_from,
621
				'url_to' => $url_to,
622
			),'',true);
623
		}
624
		return $ret;
625
	}
626
627
	/**
628
	 * This method is called in response to mkdir() calls on URL paths associated with the wrapper.
629
	 *
630
	 * Not all wrappers, eg. smb(client) support recursive directory creation.
631
	 * Therefore we handle that here instead of passing the options to underlaying wrapper.
632
	 *
633
	 * @param string $path
634
	 * @param int $mode
635
	 * @param int $options Posible values include STREAM_REPORT_ERRORS and STREAM_MKDIR_RECURSIVE
636
	 * @return boolean TRUE on success or FALSE on failure
637
	 */
638
	function mkdir ( $path, $mode, $options )
639
	{
640
		if (!($url = $this->resolve_url_symlinks($path,false)))	// false = directory does not need to exists
641
		{
642
			return false;
643
		}
644
		// refuse to modify readonly target (eg. readonly share)
645
		if (self::url_is_readonly($url))
646
		{
647
			return false;
648
		}
649
		// check if recursive option is set and needed
650
		if (($options & STREAM_MKDIR_RECURSIVE) &&
651
			($parent_url = Vfs::dirname($url)) &&
652
			!($this->url_stat($parent_url, STREAM_URL_STAT_QUIET)) &&
653
			Vfs::parse_url($parent_url, PHP_URL_PATH) !== '/')
654
		{
655
			if (!mkdir($parent_url, $mode, $options)) return false;
656
		}
657
		// unset it now, as it was handled above
658
		$options &= ~STREAM_MKDIR_RECURSIVE;
659
660
		$ret = mkdir($url,$mode,$options);
661
662
		// call "vfs_mkdir" hook
663 View Code Duplication
		if ($ret && !class_exists('setup_process', false))
664
		{
665
			Api\Hooks::process(array(
666
				'location' => 'vfs_mkdir',
667
				'path' => $path[0] == '/' ? $path : Vfs::parse_url($path, PHP_URL_PATH),
668
				'url' => $url,
669
			),'',true);
670
		}
671
		return $ret;
672
	}
673
674
	/**
675
	 * This method is called in response to rmdir() calls on URL paths associated with the wrapper.
676
	 *
677
	 * It should attempt to remove the directory specified by path.
678
	 * In order for the appropriate error message to be returned, do not define this method if your wrapper does not support removing directories.
679
	 *
680
	 * @param string $path
681
	 * @param int $options Possible values include STREAM_REPORT_ERRORS.
682
	 * @return boolean TRUE on success or FALSE on failure.
683
	 * @throws Exception\ProtectedDirectory if trying to delete a protected directory, see Vfs::isProtected()
684
	 */
685
	function rmdir ( $path, $options )
686
	{
687
		if (Vfs::isProtectedDir($path))
688
		{
689
			throw new Exception\ProtectedDirectory("Deleting protected directory '$path' rejected!");
690
		}
691
		unset($options);	// not uses but required by function signature
692
		if (!($url = $this->resolve_url_symlinks($path)))
693
		{
694
			return false;
695
		}
696
		if (self::url_is_readonly($url))
697
		{
698
			return false;
699
		}
700
		$stat = $this->url_stat($path, STREAM_URL_STAT_LINK);
701
702
		self::symlinkCache_remove($path);
703
		$ok = rmdir($url);
704
705
		// call "vfs_rmdir" hook, only after successful rmdir
706 View Code Duplication
		if ($ok && !class_exists('setup_process', false))
707
		{
708
			Api\Hooks::process(array(
709
				'location' => 'vfs_rmdir',
710
				'path' => $path[0] == '/' ? $path : Vfs::parse_url($path, PHP_URL_PATH),
711
				'url' => $url,
712
				'stat' => $stat,
713
			),'',true);
714
		}
715
		return $ok;
716
	}
717
718
	/**
719
	 * This method is called immediately when your stream object is created for examining directory contents with opendir().
720
	 *
721
	 * @param string $path URL that was passed to opendir() and that this object is expected to explore.
722
	 * @return booelan
723
	 */
724
	function dir_opendir ( $path, $options )
725
	{
726
		$this->opened_dir = $this->extra_dirs = null;
727
		$this->dir_url_params = array();
728
		$this->extra_dir_ptr = 0;
729
730
		if (!($this->opened_dir_url = $this->resolve_url_symlinks($path)))
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->resolve_url_symlinks($path) can also be of type boolean. However, the property $opened_dir_url is declared as type string. 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...
731
		{
732
			if (self::LOG_LEVEL > 0) error_log(__METHOD__."( $path,$options) resolve_url_symlinks() failed!");
733
			return false;
734
		}
735
		if (!($this->opened_dir = $this->context ?
736
			opendir($this->opened_dir_url, $this->context) : opendir($this->opened_dir_url)))
737
		{
738
			if (self::LOG_LEVEL > 0) error_log(__METHOD__."( $path,$options) opendir($this->opened_dir_url) failed!");
739
			return false;
740
		}
741
		$this->opened_dir_writable = Vfs::check_access($this->opened_dir_url,Vfs::WRITABLE);
0 ignored issues
show
Bug introduced by
It seems like $this->opened_dir_url can also be of type boolean; however, EGroupware\Api\Vfs::check_access() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
742
		// check our fstab if we need to add some of the mountpoints
743
		$basepath = Vfs::parse_url($path,PHP_URL_PATH);
744
		foreach(array_keys(self::$fstab) as $mounted)
745
		{
746
			if (((Vfs::dirname($mounted) == $basepath || Vfs::dirname($mounted).'/' == $basepath) && $mounted != '/') &&
747
				// only return children readable by the user, if dir is not writable
748
				(!self::HIDE_UNREADABLES || $this->opened_dir_writable ||
749
					Vfs::check_access($mounted,Vfs::READABLE)))
750
			{
751
				$this->extra_dirs[] = basename($mounted);
752
			}
753
		}
754
755
756
		if (self::LOG_LEVEL > 1) error_log(__METHOD__."( $path,$options): opendir($this->opened_dir_url)=$this->opened_dir, extra_dirs=".array2string($this->extra_dirs).', '.function_backtrace());
757
		return true;
758
	}
759
760
	/**
761
	 * This method is called in response to stat() calls on the URL paths associated with the wrapper.
762
	 *
763
	 * It should return as many elements in common with the system function as possible.
764
	 * Unknown or unavailable values should be set to a rational value (usually 0).
765
	 *
766
	 * If you plan to use your wrapper in a require_once you need to define stream_stat().
767
	 * If you plan to allow any other tests like is_file()/is_dir(), you have to define url_stat().
768
	 * stream_stat() must define the size of the file, or it will never be included.
769
	 * url_stat() must define mode, or is_file()/is_dir()/is_executable(), and any of those functions affected by clearstatcache() simply won't work.
770
	 * It's not documented, but directories must be a mode like 040777 (octal), and files a mode like 0100666.
771
	 * If you wish the file to be executable, use 7s instead of 6s.
772
	 * The last 3 digits are exactly the same thing as what you pass to chmod.
773
	 * 040000 defines a directory, and 0100000 defines a file.
774
	 *
775
	 * @param string $path
776
	 * @param int $flags holds additional flags set by the streams API. It can hold one or more of the following values OR'd together:
777
	 * - STREAM_URL_STAT_LINK	For resources with the ability to link to other resource (such as an HTTP Location: forward,
778
	 *                          or a filesystem symlink). This flag specified that only information about the link itself should be returned,
779
	 *                          not the resource pointed to by the link.
780
	 *                          This flag is set in response to calls to lstat(), is_link(), or filetype().
781
	 * - STREAM_URL_STAT_QUIET	If this flag is set, your wrapper should not raise any errors. If this flag is not set,
782
	 *                          you are responsible for reporting errors using the trigger_error() function during stating of the path.
783
	 *                          stat triggers it's own warning anyway, so it makes no sense to trigger one by our stream-wrapper!
784
	 * @param boolean $try_create_home =false should a user home-directory be created automatic, if it does not exist
785
	 * @param boolean $check_symlink_components =true check if path contains symlinks in path components other then the last one
786
	 * @return array
787
	 */
788
	function url_stat ( $path, $flags, $try_create_home=false, $check_symlink_components=true, $check_symlink_depth=self::MAX_SYMLINK_DEPTH, $try_reconnect=true )
789
	{
790
		if (!($url = self::resolve_url($path,!($flags & STREAM_URL_STAT_LINK), $check_symlink_components)))
791
		{
792
			if (self::LOG_LEVEL > 0) error_log(__METHOD__."('$path',$flags) can NOT resolve path!");
793
			return false;
794
		}
795
796
		try {
797
			if ($flags & STREAM_URL_STAT_LINK)
798
			{
799
				$stat = @lstat($url);	// suppressed the stat failed warnings
800
			}
801
			else
802
			{
803
				$stat = @stat($url);	// suppressed the stat failed warnings
804
805
				if ($stat && ($stat['mode'] & self::MODE_LINK))
806
				{
807
					if (!$check_symlink_depth)
808
					{
809
						if (self::LOG_LEVEL > 0) error_log(__METHOD__."('$path',$flags) maximum symlink depth exceeded, might be a circular symlink!");
810
						$stat = false;
811
					}
812
					elseif (($lpath = Vfs::readlink($url)))
813
					{
814 View Code Duplication
						if ($lpath[0] != '/')	// concat relative path
815
						{
816
							$lpath = Vfs::concat(Vfs::parse_url($path,PHP_URL_PATH),'../'.$lpath);
817
						}
818
						$u_query = parse_url($url,PHP_URL_QUERY);
819
						$url = Vfs::PREFIX.$lpath;
820
						if (self::LOG_LEVEL > 1) error_log(__METHOD__."($path,$flags) symlif (substr($path,-1) == '/' && $path != '/') $path = substr($path,0,-1);	// remove trailing slash eg. added by WebDAVink found and resolved to $url");
821
						// try reading the stat of the link
822
						if (($stat = $this->url_stat($lpath, STREAM_URL_STAT_QUIET, false, true, $check_symlink_depth-1)))
823
						{
824
							$stat_query = parse_url($stat['url'], PHP_URL_QUERY);
825
							if($u_query || $stat_query)
0 ignored issues
show
Bug Best Practice introduced by
The expression $u_query of type string|false is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

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

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
Bug Best Practice introduced by
The expression $stat_query of type string|false is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

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

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
826
							{
827
								$stat_url = parse_url($stat['url']);
828
								parse_str($stat_query,$stat_query);
829
								parse_str($u_query, $u_query);
830
								$stat_query = http_build_query(array_merge($stat_query, $u_query));
831
								$stat['url'] = $stat_url['scheme'].'://'.$stat_url['host'].$stat_url['path'].'?'.$stat_query;
832
							}
833
							if(isset($stat['url'])) $url = $stat['url'];	// if stat returns an url use that, as there might be more links ...
834
							self::symlinkCache_add($path,$url);
835
						}
836
					}
837
				}
838
			}
839
		}
840
		catch (Api\Db\Exception $e) {
841
			// some long running operations, eg. merge-print, run into situation that DB closes our separate sqlfs connection
842
			// we try now to reconnect Vfs\Sqlfs\StreamWrapper once
843
			// it's done here in vfs_stream_wrapper as situation can happen in sqlfs, links, stylite.links or stylite.versioning
844
			if ($try_reconnect)
845
			{
846
				// reconnect to db
847
				Vfs\Sqlfs\StreamWrapper::reconnect();
848
				return $this->url_stat($path, $flags, $try_create_home, $check_symlink_components, $check_symlink_depth, false);
849
			}
850
			// if numer of tries is exceeded, re-throw exception
851
			throw $e;
852
		}
853
		// check if a failed url_stat was for a home dir, in that case silently create it
854
		if (!$stat && $try_create_home && Vfs::dirname(Vfs::parse_url($path,PHP_URL_PATH)) == '/home' &&
855
			($id = $GLOBALS['egw']->accounts->name2id(basename($path))) &&
856
			$GLOBALS['egw']->accounts->id2name($id) == basename($path))	// make sure path has the right case!
857
		{
858
			$hook_data = array(
859
				'location' => $GLOBALS['egw']->accounts->get_type($id) == 'g' ? 'addgroup' : 'addaccount',
860
				'account_id' => $id,
861
				'account_lid' => basename($path),
862
				'account_name' => basename($path),
863
			);
864
			call_user_func(array(__NAMESPACE__.'\\Hooks',$hook_data['location']),$hook_data);
865
			unset($hook_data);
866
			$stat = $this->url_stat($path,$flags,false);
867
		}
868
		$query = parse_url($url, PHP_URL_QUERY);
869
		if (!$stat && $check_symlink_components)	// check if there's a symlink somewhere inbetween the path
870
		{
871
			$stat = self::check_symlink_components($path,$flags,$url);
872
			if ($stat && isset($stat['url']) && !$query) self::symlinkCache_add($path,$stat['url']);
873
		}
874
		elseif(is_array($stat) && !isset($stat['url']))
875
		{
876
			$stat['url'] = $url;
877
		}
878
		if (($stat['mode'] & 0222) && self::url_is_readonly($stat['url']))
879
		{
880
			$stat['mode'] &= ~0222;
881
		}
882
		if($stat['url'] && $query && strpos($stat['url'],'?'.$query)===false)
0 ignored issues
show
Bug Best Practice introduced by
The expression $query of type string|false is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

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

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
883
		{
884
			$stat['url'] .= '?'.$query;
885
		}
886
887
		if (self::LOG_LEVEL > 1) error_log(__METHOD__."('$path',$flags,try_create_home=$try_create_home,check_symlink_components=$check_symlink_components) returning ".array2string($stat));
888
889
		return $stat;
890
891
		/* Todo: if we hide non readables, we should return false on url_stat for consitency (if dir is not writabel)
892
		// Problem: this does NOT stop (calles itself infinit recursive)!
893
		if (self::HIDE_UNREADABLES && !Vfs::check_access($path,Vfs::READABLE,$stat) &&
894
			!Vfs::check_access(Vfs::dirname($path,Vfs::WRITABLE)))
895
		{
896
			return false;
897
		}
898
		return $stat;*/
899
	}
900
901
	/**
902
	 * Check if path (which fails the stat call) contains symlinks in path-components other then the last one
903
	 *
904
	 * @param string $path
905
	 * @param int $flags =0 see url_stat
906
	 * @param string &$url=null already resolved path
907
	 * @return array|boolean stat array or false if not found
908
	 */
909
	private function check_symlink_components($path,$flags=0,&$url=null)
910
	{
911
		if (is_null($url) && !($url = self::resolve_url($path)))
912
		{
913 View Code Duplication
			if (self::LOG_LEVEL > 0) error_log(__METHOD__."('$path',$flags,'$url') can NOT resolve path: ".function_backtrace(1));
914
			return false;
915
		}
916 View Code Duplication
		if (self::LOG_LEVEL > 1) error_log(__METHOD__."('$path',$flags,'$url'): ".function_backtrace(1));
917
918
		while (($rel_path = Vfs::basename($url).($rel_path ? '/'.$rel_path : '')) &&
919
		       ($url = Vfs::dirname($url)))
920
		{
921
			if (($stat = $this->url_stat($url, 0, false, false)))
922
			{
923
				if (is_link($url) && ($lpath = Vfs::readlink($url)))
924
				{
925
					if (self::LOG_LEVEL > 1) $log = "rel_path='$rel_path', url='$url': lpath='$lpath'";
926
927 View Code Duplication
					if ($lpath[0] != '/')
928
					{
929
						$lpath = Vfs::concat(Vfs::parse_url($url,PHP_URL_PATH),'../'.$lpath);
930
					}
931
					//self::symlinkCache_add($path,Vfs::PREFIX.$lpath);
932
					$url = Vfs::PREFIX.Vfs::concat($lpath,$rel_path);
933
					if (self::LOG_LEVEL > 1) error_log("$log --> lpath='$lpath', url='$url'");
934
					return $this->url_stat($url,$flags);
935
				}
936
				$url = Vfs::concat($url,$rel_path);
937
				if (self::LOG_LEVEL > 1) error_log(__METHOD__."('$path',$flags,'$url') returning null");
938
				return null;
939
			}
940
		}
941
		if (self::LOG_LEVEL > 1) error_log(__METHOD__."('$path',$flags,'$url') returning false");
942
		return false;	// $path does not exist
943
	}
944
945
	/**
946
	 * Cache of already resolved symlinks
947
	 *
948
	 * @var array with path => target
949
	 */
950
	private static $symlink_cache = array();
951
952
	/**
953
	 * Add a resolved symlink to cache
954
	 *
955
	 * @param string $_path vfs path
956
	 * @param string $target target path
957
	 */
958
	static protected function symlinkCache_add($_path,$target)
959
	{
960
		$path = self::get_path($_path);
961
962
		if (isset(self::$symlink_cache[$path])) return;	// nothing to do
963
964
		if ($target[0] != '/') $target = Vfs::parse_url($target,PHP_URL_PATH);
965
966
		self::$symlink_cache[$path] = $target;
967
968
		// sort longest path first
969
		uksort(self::$symlink_cache, function($b, $a)
970
		{
971
			return strlen($a) - strlen($b);
972
		});
973 View Code Duplication
		if (self::LOG_LEVEL > 1) error_log(__METHOD__."($path,$target) cache now ".array2string(self::$symlink_cache));
974
	}
975
976
	/**
977
	 * Remove a resolved symlink from cache
978
	 *
979
	 * @param string $_path vfs path
980
	 */
981
	static public function symlinkCache_remove($_path)
982
	{
983
		$path = self::get_path($_path);
984
985
		unset(self::$symlink_cache[$path]);
986 View Code Duplication
		if (self::LOG_LEVEL > 1) error_log(__METHOD__."($path) cache now ".array2string(self::$symlink_cache));
987
	}
988
989
	/**
990
	 * Resolve a path from our symlink cache
991
	 *
992
	 * The cache is sorted from longer to shorter pathes.
993
	 *
994
	 * @param string $_path
995
	 * @param boolean $do_symlink =true is a direct match allowed, default yes (must be false for a lstat or readlink!)
996
	 * @return string target or path, if path not found
997
	 */
998
	static public function symlinkCache_resolve($_path,$do_symlink=true)
999
	{
1000
		// remove vfs scheme, but no other schemes (eg. filesystem!)
1001
		$path = self::get_path($_path);
1002
1003
		$strlen_path = strlen($path);
1004
1005
		foreach(self::$symlink_cache as $p => $t)
1006
		{
1007
			if (($strlen_p = strlen($p)) > $strlen_path) continue;	// $path can NOT start with $p
1008
1009
			if ($path == $p)
1010
			{
1011
				if ($do_symlink) $target = $t;
1012
				break;
1013
			}
1014
			elseif (substr($path,0,$strlen_p+1) == $p.'/')
1015
			{
1016
				$target = $t . substr($path,$strlen_p);
1017
				break;
1018
			}
1019
		}
1020
		if (self::LOG_LEVEL > 1 && isset($target)) error_log(__METHOD__."($path) = $target");
1021
		return isset($target) ? $target : $path;
1022
	}
1023
1024
	/**
1025
	 * Clears our internal stat and symlink cache
1026
	 *
1027
	 * Normaly not necessary, as it is automatically cleared/updated, UNLESS Vfs::$user changes!
1028
	 */
1029
	static function clearstatcache()
1030
	{
1031
		self::$symlink_cache = self::$resolve_url_cache = array();
1032
	}
1033
1034
	/**
1035
	 * This method is called in response to readdir().
1036
	 *
1037
	 * It should return a string representing the next filename in the location opened by dir_opendir().
1038
	 *
1039
	 * Unless other filesystem, we only return files readable by the user, if the dir is not writable for him.
1040
	 * This is done to hide files and dirs not accessible by the user (eg. other peoples home-dirs in /home).
1041
	 *
1042
	 * @return string
1043
	 */
1044
	function dir_readdir ( )
1045
	{
1046
		if ($this->extra_dirs && count($this->extra_dirs) > $this->extra_dir_ptr)
1047
		{
1048
			$file = $this->extra_dirs[$this->extra_dir_ptr++];
1049
		}
1050
		else
1051
		{
1052
			 // only return children readable by the user, if dir is not writable
1053
			do {
1054
				$file = readdir($this->opened_dir);
1055
			}
1056
			while($file !== false &&
1057
				(is_array($this->extra_dirs) && in_array($file,$this->extra_dirs) || // do NOT return extra_dirs twice
1058
				self::HIDE_UNREADABLES && !$this->opened_dir_writable &&
1059
				!Vfs::check_access(Vfs::concat($this->opened_dir_url,$file),Vfs::READABLE)));
1060
		}
1061
		if (self::LOG_LEVEL > 1) error_log(__METHOD__."( $this->opened_dir ) = '$file'");
1062
		return $file;
1063
	}
1064
1065
	/**
1066
	 * This method is called in response to rewinddir().
1067
	 *
1068
	 * It should reset the output generated by dir_readdir(). i.e.:
1069
	 * The next call to dir_readdir() should return the first entry in the location returned by dir_opendir().
1070
	 *
1071
	 * @return boolean
1072
	 */
1073
	function dir_rewinddir ( )
1074
	{
1075
		$this->extra_dir_ptr = 0;
1076
1077
		return rewinddir($this->opened_dir);
1078
	}
1079
1080
	/**
1081
	 * This method is called in response to closedir().
1082
	 *
1083
	 * You should release any resources which were locked or allocated during the opening and use of the directory stream.
1084
	 *
1085
	 * @return boolean
1086
	 */
1087
	function dir_closedir ( )
1088
	{
1089
		$ret = closedir($this->opened_dir);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $ret is correct as closedir($this->opened_dir) (which targets closedir()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
1090
1091
		$this->opened_dir = $this->extra_dirs = null;
1092
1093
		return $ret;
1094
	}
1095
1096
	/**
1097
	 * Load stream wrapper for a given schema
1098
	 *
1099
	 * @param string $scheme
1100
	 * @return boolean
1101
	 */
1102
	static function load_wrapper($scheme)
1103
	{
1104
		if (!in_array($scheme,self::get_wrappers()))
1105
		{
1106
			switch($scheme)
1107
			{
1108
				case 'webdav':
1109
				case 'webdavs':
1110
					require_once('HTTP/WebDAV/Client.php');
1111
					self::$wrappers[] = $scheme;
1112
					break;
1113
				case '':
1114
					break;	// default file, always loaded
1115
				default:
1116
					// check if scheme is buildin in php or one of our own stream wrappers
1117
					if (in_array($scheme,stream_get_wrappers()) || class_exists(self::scheme2class($scheme)))
1118
					{
1119
						self::$wrappers[] = $scheme;
1120
					}
1121
					else
1122
					{
1123
						trigger_error("Can't load stream-wrapper for scheme '$scheme'!",E_USER_WARNING);
1124
						return false;
1125
					}
1126
			}
1127
		}
1128
		return true;
1129
	}
1130
1131
	/**
1132
	 * Return already loaded stream wrappers
1133
	 *
1134
	 * @return array
1135
	 */
1136
	static function get_wrappers()
1137
	{
1138
		if (is_null(self::$wrappers))
1139
		{
1140
			self::$wrappers = stream_get_wrappers();
1141
		}
1142
		return self::$wrappers;
1143
	}
1144
1145
	/**
1146
	 * Get the class-name for a scheme
1147
	 *
1148
	 * A scheme is not allowed to contain an underscore, but allows a dot and a class names only allow or need underscores, but no dots
1149
	 * --> we replace dots in scheme with underscored to get the class-name
1150
	 *
1151
	 * @param string $scheme eg. vfs
1152
	 * @return string
1153
	 */
1154
	static function scheme2class($scheme)
1155
	{
1156
		list($app, $app_scheme) = explode('.', $scheme);
1157
		foreach(array(
1158
			empty($app_scheme) ? 'EGroupware\\Api\\Vfs\\'.ucfirst($scheme).'\\StreamWrapper' :	// streamwrapper in Api\Vfs
1159
				'EGroupware\\'.ucfirst($app).'\\Vfs\\'.ucfirst($app_scheme).'\\StreamWrapper', // streamwrapper in $app\Vfs
1160
			str_replace('.','_',$scheme).'_stream_wrapper',	// old (flat) name
1161
		) as $class)
1162
		{
1163
			//error_log(__METHOD__."('$scheme') class_exists('$class')=".array2string(class_exists($class)));
1164
			if (class_exists($class))  return $class;
1165
		}
1166
	}
1167
1168
	/**
1169
	 * Getting the path from an url (or path) AND removing trailing slashes
1170
	 *
1171
	 * @param string $path url or path (might contain trailing slash from WebDAV!)
1172
	 * @param string $only_remove_scheme =self::SCHEME if given only that scheme get's removed
1173
	 * @return string path without training slash
1174
	 */
1175
	static protected function get_path($path,$only_remove_scheme=self::SCHEME)
1176
	{
1177
		if ($path[0] != '/' && (!$only_remove_scheme || Vfs::parse_url($path, PHP_URL_SCHEME) == $only_remove_scheme))
1178
		{
1179
			$path = Vfs::parse_url($path, PHP_URL_PATH);
1180
		}
1181
		// remove trailing slashes eg. added by WebDAV, but do NOT remove / from "sqlfs://default/"!
1182
		if ($path != '/')
1183
		{
1184
			while (mb_substr($path, -1) == '/' && $path != '/' && ($path[0] == '/' || Vfs::parse_url($path, PHP_URL_PATH) != '/'))
1185
			{
1186
				$path = mb_substr($path,0,-1);
1187
			}
1188
		}
1189
		return $path;
1190
	}
1191
1192
	/**
1193
	 * Check if url contains ro=1 parameter to mark mount readonly
1194
	 *
1195
	 * @param string $url
1196
	 * @return boolean
1197
	 */
1198
	static function url_is_readonly($url)
1199
	{
1200
		static $cache = array();
1201
		$ret =& $cache[$url];
1202
		if (!isset($ret))
1203
		{
1204
			$matches = null;
1205
			$ret = preg_match('/\?(.*&)?ro=([^&]+)/', $url, $matches) && $matches[2];
1206
		}
1207
		return $ret;
1208
	}
1209
1210
	/**
1211
	 * Mounts $url under $path in the vfs, called without parameter it returns the fstab
1212
	 *
1213
	 * The fstab is stored in the eGW configuration and used for all eGW users.
1214
	 *
1215
	 * @param string $url =null url of the filesystem to mount, eg. oldvfs://default/
1216
	 * @param string $path =null path to mount the filesystem in the vfs, eg. /
1217
	 * @param boolean $check_url =null check if url is an existing directory, before mounting it
1218
	 * 	default null only checks if url does not contain a $ as used in $user or $pass
1219
	 * @param boolean $persitent_mount =true create a persitent mount, or only a temprary for current request
1220
	 * @param boolean $clear_fstab =false true clear current fstab, false (default) only add given mount
1221
	 * @return array|boolean array with fstab, if called without parameter or true on successful mount
1222
	 */
1223
	static function mount($url=null,$path=null,$check_url=null,$persitent_mount=true,$clear_fstab=false)
1224
	{
1225
		if (is_null($check_url)) $check_url = strpos($url,'$') === false;
1226
1227
		if (!isset($GLOBALS['egw_info']['server']['vfs_fstab']))	// happens eg. in setup
1228
		{
1229
			$api_config = Api\Config::read('phpgwapi');
1230
			if (isset($api_config['vfs_fstab']) && is_array($api_config['vfs_fstab']))
1231
			{
1232
				self::$fstab = $api_config['vfs_fstab'];
1233
			}
1234
			else
1235
			{
1236
				self::$fstab = array(
1237
					'/' => 'sqlfs://$host/',
1238
					'/apps' => 'links://$host/apps',
1239
				);
1240
			}
1241
			unset($api_config);
1242
		}
1243
		if (is_null($url) || is_null($path))
1244
		{
1245
			if (self::LOG_LEVEL > 1) error_log(__METHOD__.'('.array2string($url).','.array2string($path).') returns '.array2string(self::$fstab));
1246
			return self::$fstab;
1247
		}
1248 View Code Duplication
		if (!Vfs::$is_root)
1249
		{
1250
			if (self::LOG_LEVEL > 0) error_log(__METHOD__.'('.array2string($url).','.array2string($path).') permission denied, you are NOT root!');
1251
			return false;	// only root can mount
1252
		}
1253
		if ($clear_fstab)
1254
		{
1255
			self::$fstab = array();
1256
		}
1257
		if (isset(self::$fstab[$path]) && self::$fstab[$path] === $url)
1258
		{
1259 View Code Duplication
			if (self::LOG_LEVEL > 0) error_log(__METHOD__.'('.array2string($url).','.array2string($path).') already mounted.');
1260
			return true;	// already mounted
1261
		}
1262
		self::load_wrapper(Vfs::parse_url($url,PHP_URL_SCHEME));
1263
1264
		if ($check_url && (!file_exists($url) || opendir($url) === false))
1265
		{
1266 View Code Duplication
			if (self::LOG_LEVEL > 0) error_log(__METHOD__.'('.array2string($url).','.array2string($path).') url does NOT exist!');
1267
			return false;	// url does not exist
1268
		}
1269
		self::$fstab[$path] = $url;
1270
1271
		uksort(self::$fstab, function($a, $b)
1272
		{
1273
			return strlen($a) - strlen($b);
1274
		});
1275
1276
		if ($persitent_mount)
1277
		{
1278
			Api\Config::save_value('vfs_fstab',self::$fstab,'phpgwapi');
1279
			$GLOBALS['egw_info']['server']['vfs_fstab'] = self::$fstab;
1280
			// invalidate session cache
1281
			if (method_exists($GLOBALS['egw'],'invalidate_session_cache'))	// egw object in setup is limited
1282
			{
1283
				$GLOBALS['egw']->invalidate_session_cache();
1284
			}
1285
		}
1286 View Code Duplication
		if (self::LOG_LEVEL > 1) error_log(__METHOD__.'('.array2string($url).','.array2string($path).') returns true (successful new mount).');
1287
		return true;
1288
	}
1289
1290
	/**
1291
	 * Unmounts a filesystem part of the vfs
1292
	 *
1293
	 * @param string $path url or path of the filesystem to unmount
1294
	 */
1295
	static function umount($path)
1296
	{
1297 View Code Duplication
		if (!Vfs::$is_root)
1298
		{
1299
			if (self::LOG_LEVEL > 0) error_log(__METHOD__.'('.array2string($path).','.array2string($path).') permission denied, you are NOT root!');
1300
			return false;	// only root can mount
1301
		}
1302
		if (!isset(self::$fstab[$path]) && ($path = array_search($path,self::$fstab)) === false)
1303
		{
1304
			if (self::LOG_LEVEL > 0) error_log(__METHOD__.'('.array2string($path).') NOT mounted!');
1305
			return false;	// $path not mounted
1306
		}
1307
		unset(self::$fstab[$path]);
1308
1309
		Api\Config::save_value('vfs_fstab',self::$fstab,'phpgwapi');
1310
		$GLOBALS['egw_info']['server']['vfs_fstab'] = self::$fstab;
1311
		// invalidate session cache
1312
		if (method_exists($GLOBALS['egw'],'invalidate_session_cache'))	// egw object in setup is limited
1313
		{
1314
			$GLOBALS['egw']->invalidate_session_cache();
1315
		}
1316
		if (self::LOG_LEVEL > 1) error_log(__METHOD__.'('.array2string($path).') returns true (successful unmount).');
1317
		return true;
1318
	}
1319
1320
	/**
1321
	 * Init our static properties and register this wrapper
1322
	 *
1323
	 */
1324
	static function init_static()
1325
	{
1326
		stream_register_wrapper(self::SCHEME,__CLASS__);
1327
1328
		if (($fstab = $GLOBALS['egw_info']['server']['vfs_fstab']) && is_array($fstab) && count($fstab))
1329
		{
1330
			self::$fstab = $fstab;
1331
		}
1332
	}
1333
}
1334
1335
StreamWrapper::init_static();
1336