Vfs   F
last analyzed

Complexity

Total Complexity 544

Size/Duplication

Total Lines 2525
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 824
dl 0
loc 2525
rs 1.7759
c 0
b 0
f 0
wmc 544

78 Methods

Rating   Name   Duplication   Size   Complexity  
A _check_num() 0 14 6
B concat() 0 16 7
D lock() 0 73 23
B int_size() 0 32 10
A download_url() 0 13 4
A is_writable() 0 3 1
A mount() 0 3 1
F copy_uploaded() 0 67 31
F download_zip() 0 167 25
D check_access() 0 114 31
A string_stream() 0 20 4
A encodePathComponent() 0 3 1
A mount_url() 0 3 1
A stat() 0 12 4
F find() 0 209 82
A dir() 0 7 2
A init_static() 0 8 3
A clearstatcache() 0 6 1
A resolve_url() 0 3 1
A mkdir() 0 3 2
B compare() 0 19 9
A chown() 0 3 3
A move_files() 0 23 5
A is_link() 0 3 2
B unlock() 0 17 7
A file_exists() 0 3 2
A eacl() 0 13 4
A is_dir() 0 3 2
A basename() 0 6 1
A find_prop() 0 12 5
C _call_on_backend() 0 57 17
A get_minimum_file_id() 0 7 2
A deny_script() 0 3 1
A is_executable() 0 3 1
A remove() 0 12 3
D mode2int() 0 55 16
F int2mode() 0 54 23
A resolve_url_symlinks() 0 4 1
A rename() 0 4 1
A get_home_dir() 0 16 6
A touch() 0 3 2
A load_wrapper() 0 3 1
A symlink() 0 7 2
A build_url() 0 9 6
A is_readable() 0 3 1
B dirname() 0 18 10
A propfind() 0 3 1
A _rm_rmdir() 0 12 4
B parse_url() 0 42 7
A encodePath() 0 3 1
A __construct() 0 2 1
A get_eacl() 0 27 6
A scandir() 0 7 2
A chgrp() 0 3 3
A is_hidden() 0 6 5
A lstat() 0 12 4
A rmdir() 0 3 2
A unlink() 0 3 2
A readlink() 0 5 1
C mime_content_type() 0 38 15
C thumbnail_url() 0 43 12
A decodePath() 0 3 1
A scheme2class() 0 3 1
A proppatch() 0 3 1
A app_entry_lock_path() 0 3 1
A hsize() 0 6 4
A fopen() 0 7 3
F _check_add() 0 80 40
A umount() 0 3 1
C checkLock() 0 30 12
B mime_icon() 0 22 8
A getExtraInfo() 0 20 5
A chmod() 0 3 2
B has_owner_rights() 0 12 7
B copy_files() 0 52 9
A opendir() 0 7 3
A isProtectedDir() 0 5 2
A copy() 0 19 5

How to fix   Complexity   

Complex Class

Complex classes like Vfs often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Vfs, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * EGroupware API: VFS - static methods to use the new eGW virtual file system
4
 *
5
 * @link https://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-19 by Ralf Becker <RalfBecker-AT-outdoor-training.de>
11
 */
12
13
namespace EGroupware\Api;
14
15
// explicitly import old phpgwapi classes used:
16
use HTTP_WebDAV_Server;
17
18
/**
19
 * Class containing static methods to use the new eGW virtual file system
20
 *
21
 * This extension of the vfs stream-wrapper allows to use the following static functions,
22
 * which only allow access to the eGW VFS and need no 'vfs://default' prefix for filenames:
23
 *
24
 * All examples require a: use EGroupware\Api\Vfs;
25
 *
26
 * - resource Vfs::fopen($path,$mode) like fopen, returned resource can be used with fwrite etc.
27
 * - resource Vfs::opendir($path) like opendir, returned resource can be used with readdir etc.
28
 * - boolean Vfs::copy($from,$to) like copy
29
 * - boolean Vfs::rename($old,$new) renaming or moving a file in the vfs
30
 * - boolean Vfs::mkdir($path) creating a new dir in the vfs
31
 * - boolean Vfs::rmdir($path) removing (an empty) directory
32
 * - boolean Vfs::unlink($path) removing a file
33
 * - boolean Vfs::touch($path,$mtime=null) touch a file
34
 * - boolean Vfs::stat($path) returning status of file like stat(), but only with string keys (no numerical indexes)!
35
 *
36
 * With the exception of Vfs::touch() (not yet part of the stream_wrapper interface)
37
 * you can always use the standard php functions, if you add a 'vfs://default' prefix
38
 * to every filename or path. Be sure to always add the prefix, as the user otherwise gains
39
 * access to the real filesystem of the server!
40
 *
41
 * The two following methods can be used to persitently mount further filesystems (without editing the code):
42
 *
43
 * - boolean|array Vfs::mount($url,$path) to mount $ur on $path or to return the fstab when called without argument
44
 * - boolean Vfs::umount($path) to unmount a path or url
45
 *
46
 * The stream wrapper interface allows to access hugh files in junks to not be limited by the
47
 * memory_limit setting of php. To do you should pass the opened file as resource and not the content:
48
 *
49
 * 		$file = Vfs::fopen('/home/user/somefile','r');
50
 * 		$content = fread($file,1024);
51
 *
52
 * You can also attach stream filters, to eg. base64 encode or compress it on the fly,
53
 * without the need to hold the content of the whole file in memmory.
54
 *
55
 * If you want to copy a file, you can use stream_copy_to_stream to do a copy of a file far bigger then
56
 * php's memory_limit:
57
 *
58
 * 		$from = Vfs::fopen('/home/user/fromfile','r');
59
 * 		$to = Vfs::fopen('/home/user/tofile','w');
60
 *
61
 * 		stream_copy_to_stream($from,$to);
62
 *
63
 * The static Vfs::copy() method does exactly that, but you have to do it eg. on your own, if
64
 * you want to copy eg. an uploaded file into the vfs.
65
 *
66
 * Vfs::parse_url($url, $component=-1), Vfs::dirname($url) and Vfs::basename($url) work
67
 * on urls containing utf-8 characters, which get NOT urlencoded in our VFS!
68
 */
69
class Vfs
70
{
71
	const PREFIX = 'vfs://default';
72
	/**
73
	 * Scheme / protocol used for this stream-wrapper
74
	 */
75
	const SCHEME = Vfs\StreamWrapper::SCHEME;
76
	/**
77
	 * Mime type of directories, the old vfs used 'Directory', while eg. WebDAV uses 'httpd/unix-directory'
78
	 */
79
	const DIR_MIME_TYPE = Vfs\StreamWrapper::DIR_MIME_TYPE;
80
	/**
81
	 * Readable bit, for dirs traversable
82
	 */
83
	const READABLE = 4;
84
	/**
85
	 * Writable bit, for dirs delete or create files in that dir
86
	 */
87
	const WRITABLE = 2;
88
	/**
89
	 * Excecutable bit, here only use to check if user is allowed to search dirs
90
	 */
91
	const EXECUTABLE = 1;
92
	/**
93
	 * mode-bits, which have to be set for links
94
	 */
95
	const MODE_LINK = Vfs\StreamWrapper::MODE_LINK;
96
	/**
97
	 * Name of the lock table
98
	 */
99
	const LOCK_TABLE = 'egw_locks';
100
	/**
101
	 * How much should be logged to the apache error-log
102
	 *
103
	 * 0 = Nothing
104
	 * 1 = only errors
105
	 * 2 = all function calls and errors (contains passwords too!)
106
	 */
107
	const LOG_LEVEL = 1;
108
	/**
109
	 * Current user has root rights, no access checks performed!
110
	 *
111
	 * @var boolean
112
	 */
113
	static $is_root = false;
114
	/**
115
	 * Current user id, in case we ever change if away from $GLOBALS['egw_info']['user']['account_id']
116
	 *
117
	 * @var int
118
	 */
119
	static $user;
120
	/**
121
	 * Current user is an eGW admin
122
	 *
123
	 * @var boolean
124
	 */
125
	static $is_admin = false;
126
	/**
127
	 * Total of last find call
128
	 *
129
	 * @var int
130
	 */
131
	static $find_total;
132
	/**
133
	 * Reference to the global db object
134
	 *
135
	 * @var Db
136
	 */
137
	static $db;
138
139
	/**
140
	 * fopen working on just the eGW VFS
141
	 *
142
	 * @param string $path filename with absolute path in the eGW VFS
143
	 * @param string $mode 'r', 'w', ... like fopen
144
	 * @param resource $context =null context to pass to stream-wrapper
145
	 * @return resource
146
	 */
147
	static function fopen($path, $mode, $context=null)
148
	{
149
		if ($path[0] != '/')
150
		{
151
			throw new Exception\AssertionFailed("Filename '$path' is not an absolute path!");
152
		}
153
		return $context ? fopen(self::PREFIX.$path, $mode, false, $context) : fopen(self::PREFIX.$path, $mode);
0 ignored issues
show
introduced by
$context is of type null|resource, thus it always evaluated to false.
Loading history...
154
	}
155
156
	/**
157
	 * opendir working on just the eGW VFS: returns resource for readdir() etc.
158
	 *
159
	 * @param string $path filename with absolute path in the eGW VFS
160
	 * @param resource $context =null context to pass to stream-wrapper
161
	 * @return resource
162
	 */
163
	static function opendir($path, $context=null)
164
	{
165
		if ($path[0] != '/')
166
		{
167
			throw new Exception\AssertionFailed("Directory '$path' is not an absolute path!");
168
		}
169
		return $context ? opendir(self::PREFIX.$path, $context) : opendir(self::PREFIX.$path);
0 ignored issues
show
introduced by
$context is of type null|resource, thus it always evaluated to false.
Loading history...
170
	}
171
172
	/**
173
	 * dir working on just the eGW VFS: returns directory object
174
	 *
175
	 * @param string $path filename with absolute path in the eGW VFS
176
	 * @return Directory
0 ignored issues
show
Bug introduced by
The type EGroupware\Api\Directory was not found. Did you mean Directory? If so, make sure to prefix the type with \.
Loading history...
177
	 */
178
	static function dir($path)
179
	{
180
		if ($path[0] != '/')
181
		{
182
			throw new Exception\AssertionFailed("Directory '$path' is not an absolute path!");
183
		}
184
		return dir(self::PREFIX.$path);
185
	}
186
187
	/**
188
	 * scandir working on just the eGW VFS: returns array with filenames as values
189
	 *
190
	 * @param string $path filename with absolute path in the eGW VFS
191
	 * @param int $sorting_order =0 !$sorting_order (default) alphabetical in ascending order, $sorting_order alphabetical in descending order.
192
	 * @return array
193
	 */
194
	static function scandir($path,$sorting_order=0)
195
	{
196
		if ($path[0] != '/')
197
		{
198
			throw new Exception\AssertionFailed("Directory '$path' is not an absolute path!");
199
		}
200
		return scandir(self::PREFIX.$path,$sorting_order);
201
	}
202
203
	/**
204
	 * copy working on just the eGW VFS
205
	 *
206
	 * @param string $from
207
	 * @param string $to
208
	 * @return boolean
209
	 */
210
	static function copy($from,$to)
211
	{
212
		$old_props = self::file_exists($to) ? self::propfind($to,null) : array();
213
		// copy properties (eg. file comment), if there are any and evtl. existing old properties
214
		$props = self::propfind($from,null);
215
		if(!$props)
216
		{
217
			$props = array();
218
		}
219
		foreach($old_props as $prop)
220
		{
221
			if (!self::find_prop($props,$prop))
222
			{
223
				$prop['val'] = null;	// null = delete prop
224
				$props[] = $prop;
225
			}
226
		}
227
		// using self::copy_uploaded() to treat copying incl. properties as atomar operation in respect of notifications
228
		return self::copy_uploaded(self::PREFIX.$from,$to,$props,false);	// false = no is_uploaded_file check!
229
	}
230
231
	/**
232
	 * Find a specific property in an array of properties (eg. returned by propfind)
233
	 *
234
	 * @param array &$props
235
	 * @param array|string $name property array or name
236
	 * @param string $ns =self::DEFAULT_PROP_NAMESPACE namespace, only if $prop is no array
237
	 * @return &array reference to property in $props or null if not found
0 ignored issues
show
Documentation Bug introduced by
The doc comment &array at position 0 could not be parsed: Unknown type name '&' at position 0 in &array.
Loading history...
238
	 */
239
	static function &find_prop(array &$props,$name,$ns=self::DEFAULT_PROP_NAMESPACE)
240
	{
241
		if (is_array($name))
242
		{
243
			$ns = $name['ns'];
244
			$name = $name['name'];
245
		}
246
		foreach($props as &$prop)
247
		{
248
			if ($prop['name'] == $name && $prop['ns'] == $ns) return $prop;
249
		}
250
		return null;
251
	}
252
253
	/**
254
	 * stat working on just the eGW VFS (alias of url_stat)
255
	 *
256
	 * @param string $path filename with absolute path in the eGW VFS
257
	 * @param boolean $try_create_home =false should a non-existing home-directory be automatically created
258
	 * @return array
259
	 */
260
	static function stat($path,$try_create_home=false)
261
	{
262
		if ($path[0] != '/' && strpos($path, self::PREFIX.'/') !== 0)
263
		{
264
			throw new Exception\AssertionFailed("File '$path' is not an absolute path!");
265
		}
266
		$vfs = new Vfs\StreamWrapper();
267
		if (($stat = $vfs->url_stat($path,0,$try_create_home)))
268
		{
269
			$stat = array_slice($stat,13);	// remove numerical indices 0-12
270
		}
271
		return $stat;
272
	}
273
274
	/**
275
	 * lstat (not resolving symbolic links) working on just the eGW VFS (alias of url_stat)
276
	 *
277
	 * @param string $path filename with absolute path in the eGW VFS
278
	 * @param boolean $try_create_home =false should a non-existing home-directory be automatically created
279
	 * @return array
280
	 */
281
	static function lstat($path,$try_create_home=false)
282
	{
283
		if ($path[0] != '/' && strpos($path, self::PREFIX.'/') !== 0)
284
		{
285
			throw new Exception\AssertionFailed("File '$path' is not an absolute path!");
286
		}
287
		$vfs = new Vfs\StreamWrapper();
288
		if (($stat = $vfs->url_stat($path,STREAM_URL_STAT_LINK,$try_create_home)))
289
		{
290
			$stat = array_slice($stat,13);	// remove numerical indices 0-12
291
		}
292
		return $stat;
293
	}
294
295
	/**
296
	 * is_dir() version working only inside the vfs
297
	 *
298
	 * @param string $path
299
	 * @return boolean
300
	 */
301
	static function is_dir($path)
302
	{
303
		return $path[0] == '/' && is_dir(self::PREFIX.$path);
304
	}
305
306
	/**
307
	 * is_link() version working only inside the vfs
308
	 *
309
	 * @param string $path
310
	 * @return boolean
311
	 */
312
	static function is_link($path)
313
	{
314
		return $path[0] == '/' && is_link(self::PREFIX.$path);
315
	}
316
317
	/**
318
	 * file_exists() version working only inside the vfs
319
	 *
320
	 * @param string $path
321
	 * @return boolean
322
	 */
323
	static function file_exists($path)
324
	{
325
		return $path[0] == '/' && file_exists(self::PREFIX.$path);
326
	}
327
328
	/**
329
	 * Mounts $url under $path in the vfs, called without parameter it returns the fstab
330
	 *
331
	 * The fstab is stored in the eGW configuration and used for all eGW users.
332
	 *
333
	 * @param string $url =null url of the filesystem to mount, eg. oldvfs://default/
334
	 * @param string $path =null path to mount the filesystem in the vfs, eg. /
335
	 * @param boolean $check_url =null check if url is an existing directory, before mounting it
336
	 * 	default null only checks if url does not contain a $ as used in $user or $pass
337
	 * @param boolean $persitent_mount =true create a persitent mount, or only a temprary for current request
338
	 * @param boolean $clear_fstab =false true clear current fstab, false (default) only add given mount
339
	 * @return array|boolean array with fstab, if called without parameter or true on successful mount
340
	 */
341
	static function mount($url=null,$path=null,$check_url=null,$persitent_mount=true,$clear_fstab=false)
342
	{
343
		return Vfs\StreamWrapper::mount($url, $path, $check_url, $persitent_mount, $clear_fstab);
344
	}
345
346
	/**
347
	 * Unmounts a filesystem part of the vfs
348
	 *
349
	 * @param string $path url or path of the filesystem to unmount
350
	 */
351
	static function umount($path)
352
	{
353
		return Vfs\StreamWrapper::umount($path);
354
	}
355
356
	/**
357
	 * Returns mount url of a full url returned by resolve_url
358
	 *
359
	 * @param string $fullurl full url returned by resolve_url
360
	 * @return string|NULL mount url or null if not found
361
	 */
362
	static function mount_url($fullurl)
363
	{
364
		return Vfs\StreamWrapper::mount_url($fullurl);
365
	}
366
367
	/**
368
	 * Check if file is hidden: name starts with a '.' or is Thumbs.db or _gsdata_
369
	 *
370
	 * @param string $path
371
	 * @param boolean $allow_versions =false allow .versions or .attic
372
	 * @return boolean
373
	 */
374
	public static function is_hidden($path, $allow_versions=false)
375
	{
376
		$file = self::basename($path);
377
378
		return $file[0] == '.' && (!$allow_versions || !in_array($file, array('.versions', '.attic'))) ||
379
			$file == 'Thumbs.db' || $file == '_gsdata_';
380
	}
381
382
	/**
383
	 * find = recursive search over the filesystem
384
	 *
385
	 * @param string|array $base base of the search
386
	 * @param array $options =null the following keys are allowed:
387
	 * - type => {d|f|F|!l} d=dirs, f=files (incl. symlinks), F=files (incl. symlinks to files), !l=no symlinks, default all
388
	 * - depth => {true|false(default)} put the contents of a dir before the dir itself
389
	 * - dirsontop => {true(default)|false} allways return dirs before the files (two distinct blocks)
390
	 * - mindepth,maxdepth minimal or maximal depth to be returned
391
	 * - name,path => pattern with *,? wildcards, eg. "*.php"
392
	 * - name_preg,path_preg => preg regular expresion, eg. "/(vfs|wrapper)/"
393
	 * - uid,user,gid,group,nouser,nogroup file belongs to user/group with given name or (numerical) id
394
	 * - mime => type[/subtype] or perl regular expression starting with a "/" eg. "/^(image|video)\\//i"
395
	 * - empty,size => (+|-|)N
396
	 * - cmin/mmin => (+|-|)N file/dir create/modified in the last N minutes
397
	 * - ctime/mtime => (+|-|)N file/dir created/modified in the last N days
398
	 * - url => false(default),true allow (and return) full URL's instead of VFS pathes (only set it, if you know what you doing securitywise!)
399
	 * - need_mime => false(default),true should we return the mime type
400
	 * - order => name order rows by name column
401
	 * - sort => (ASC|DESC) sort, default ASC
402
	 * - limit => N,[n=0] return N entries from position n on, which defaults to 0
403
	 * - follow => {true|false(default)} follow symlinks
404
	 * - hidden => {true|false(default)} include hidden files (name starts with a '.' or is Thumbs.db)
405
	 * - show-deleted => {true|false(default)} get also set by hidden, if not explicitly set otherwise (requires versioning!)
406
	 * @param string|array/true $exec =null function to call with each found file/dir as first param and stat array as last param or
0 ignored issues
show
Documentation Bug introduced by
The doc comment string|array/true at position 2 could not be parsed: Unknown type name 'array/true' at position 2 in string|array/true.
Loading history...
407
	 * 	true to return file => stat pairs
408
	 * @param array $exec_params =null further params for exec as array, path is always the first param and stat the last!
409
	 * @return array of pathes if no $exec, otherwise path => stat pairs
410
	 */
411
	static function find($base,$options=null,$exec=null,$exec_params=null)
412
	{
413
		//error_log(__METHOD__."(".print_r($base,true).",".print_r($options,true).",".print_r($exec,true).",".print_r($exec_params,true).")\n");
414
415
		$type = $options['type'];	// 'd', 'f' or 'F'
416
		$dirs_last = $options['depth'];	// put content of dirs before the dir itself
417
		// show dirs on top by default, if no recursive listing (allways disabled if $type specified, as unnecessary)
418
		$dirsontop = !$type && (isset($options['dirsontop']) ? (boolean)$options['dirsontop'] : isset($options['maxdepth'])&&$options['maxdepth']>0);
419
		if ($dirsontop) $options['need_mime'] = true;	// otherwise dirsontop can NOT work
420
421
		// process some of the options (need to be done only once)
422
		if (isset($options['name']) && !isset($options['name_preg']))	// change from simple *,? wildcards to preg regular expression once
423
		{
424
			$options['name_preg'] = '/^'.str_replace(array('\\?','\\*'),array('.{1}','.*'),preg_quote($options['name'])).'$/i';
425
		}
426
		if (isset($options['path']) && !isset($options['preg_path']))	// change from simple *,? wildcards to preg regular expression once
427
		{
428
			$options['path_preg'] = '/^'.str_replace(array('\\?','\\*'),array('.{1}','.*'),preg_quote($options['path'])).'$/i';
429
		}
430
		if (!isset($options['uid']))
431
		{
432
			if (isset($options['user']))
433
			{
434
				$options['uid'] = $GLOBALS['egw']->accounts->name2id($options['user'],'account_lid','u');
435
			}
436
			elseif (isset($options['nouser']))
437
			{
438
				$options['uid'] = 0;
439
			}
440
		}
441
		if (!isset($options['gid']))
442
		{
443
			if (isset($options['group']))
444
			{
445
				$options['gid'] = abs($GLOBALS['egw']->accounts->name2id($options['group'],'account_lid','g'));
446
			}
447
			elseif (isset($options['nogroup']))
448
			{
449
				$options['gid'] = 0;
450
			}
451
		}
452
		if ($options['order'] == 'mime')
453
		{
454
			$options['need_mime'] = true;	// we need to return the mime colum
455
		}
456
		// implicit show deleted files, if hidden is enabled (requires versioning!)
457
		if (!empty($options['hidden']) && !isset($options['show-deleted']))
458
		{
459
			$options['show-deleted'] = true;
460
		}
461
462
		// make all find options available as stream context option "find", to allow plugins to use them
463
		$context = stream_context_create(array(self::SCHEME => array('find' => $options)));
464
465
		$url = $options['url'];
466
467
		if (!is_array($base))
468
		{
469
			$base = array($base);
470
		}
471
		$result = array();
472
		foreach($base as $path)
473
		{
474
			if (!$url)
475
			{
476
				if ($path[0] != '/' || !self::stat($path)) continue;
477
				$path = self::PREFIX . $path;
478
			}
479
			if (!isset($options['remove']))
480
			{
481
				$options['remove'] = count($base) == 1 ? count(explode('/',$path))-3+(int)(substr($path,-1)!='/') : 0;
482
			}
483
			$is_dir = is_dir($path);
484
			if ((int)$options['mindepth'] == 0 && (!$dirs_last || !$is_dir))
485
			{
486
				self::_check_add($options,$path,$result);
487
			}
488
			if ($is_dir && (!isset($options['maxdepth']) || ($options['maxdepth'] > 0 &&
489
				$options['depth'] < $options['maxdepth'])) &&
490
				($dir = @opendir($path, $context)))
491
			{
492
				while(($fname = readdir($dir)) !== false)
493
				{
494
					if ($fname == '.' || $fname == '..') continue;	// ignore current and parent dir!
495
496
					if (self::is_hidden($fname, $options['show-deleted']) && !$options['hidden']) continue;	// ignore hidden files
497
498
					$file = self::concat($path, $fname);
499
500
					if ((int)$options['mindepth'] <= 1)
501
					{
502
						self::_check_add($options,$file,$result);
503
					}
504
					// only descend into subdirs, if it's a real dir (no link to a dir) or we should follow symlinks
505
					if (is_dir($file) && ($options['follow'] || !is_link($file)) && (!isset($options['maxdepth']) || $options['maxdepth'] > 1))
506
					{
507
						$opts = $options;
508
						if ($opts['mindepth']) $opts['mindepth']--;
509
						if ($opts['maxdepth']) $opts['depth']++;
510
						unset($opts['order']);
511
						unset($opts['limit']);
512
						foreach(self::find($options['url']?$file:self::parse_url($file,PHP_URL_PATH),$opts,true) as $p => $s)
513
						{
514
							unset($result[$p]);
515
							$result[$p] = $s;
516
						}
517
					}
518
				}
519
				closedir($dir);
520
			}
521
			if ($is_dir && (int)$options['mindepth'] == 0 && $dirs_last)
522
			{
523
				self::_check_add($options,$path,$result);
524
			}
525
		}
526
		// ordering of the rows
527
		if (isset($options['order']))
528
		{
529
			$sort_desc = strtolower($options['sort']) == 'desc';
530
			switch($order = $options['order'])
531
			{
532
				// sort numerical
533
				case 'size':
534
				case 'uid':
535
				case 'gid':
536
				case 'mode':
537
				case 'ctime':
538
				case 'mtime':
539
					$ok = uasort($result, function($a, $b) use ($dirsontop, $sort_desc, $order)
0 ignored issues
show
Unused Code introduced by
The assignment to $ok is dead and can be removed.
Loading history...
540
					{
541
						$cmp = $a[$order] - $b[$order];
542
						// sort code, to place directories before files, if $dirsontop enabled
543
						if ($dirsontop && ($a['mime'] == self::DIR_MIME_TYPE) !== ($b['mime'] == self::DIR_MIME_TYPE))
544
						{
545
							$cmp = $a['mime' ] == self::DIR_MIME_TYPE ? -1 : 1;
546
						}
547
						// reverse sort for descending, if no directory sorted to top
548
						elseif ($sort_desc)
549
						{
550
							 $cmp *= -1;
551
						}
552
						// always use name as second sort criteria
553
						if (!$cmp) $cmp = strcasecmp($a['name'], $b['name']);
554
						return $cmp;
555
					});
556
					break;
557
558
				// sort alphanumerical
559
				default:
560
					$order = 'name';
561
					// fall throught
562
				case 'name':
563
				case 'mime':
564
					$ok = uasort($result, function($a, $b) use ($dirsontop, $order, $sort_desc)
565
					{
566
						$cmp = strcasecmp($a[$order], $b[$order]);
567
						// sort code, to place directories before files, if $dirsontop enabled
568
						if ($dirsontop && ($a['mime'] == self::DIR_MIME_TYPE) !== ($b['mime'] == self::DIR_MIME_TYPE))
569
						{
570
							$cmp = $a['mime' ] == self::DIR_MIME_TYPE ? -1 : 1;
571
						}
572
						// reverse sort for descending
573
						elseif ($sort_desc)
574
						{
575
							$cmp *= -1;
576
						}
577
						// always use name as second sort criteria
578
						if (!$cmp && $order != 'name') $cmp = strcasecmp($a['name'], $b['name']);
579
						return $cmp;
580
					});
581
					break;
582
			}
583
		}
584
		// limit resultset
585
		self::$find_total = count($result);
586
		if (isset($options['limit']))
587
		{
588
			list($limit,$start) = explode(',',$options['limit']);
589
			if (!$limit && !($limit = $GLOBALS['egw_info']['user']['preferences']['comman']['maxmatches'])) $limit = 15;
590
			//echo "total=".self::$find_total.", limit=$options[limit] --> start=$start, limit=$limit<br>\n";
591
592
			if ((int)$start || self::$find_total > $limit)
593
			{
594
				$result = array_slice($result,(int)$start,(int)$limit,true);
595
			}
596
		}
597
		//echo $path; _debug_array($result);
598
		if ($exec !== true && is_callable($exec))
599
		{
600
			if (!is_array($exec_params))
601
			{
602
				$exec_params = is_null($exec_params) ? array() : array($exec_params);
603
			}
604
			foreach($result as $path => &$stat)
605
			{
606
				$options = $exec_params;
607
				array_unshift($options,$path);
608
				array_push($options,$stat);
609
				//echo "calling ".print_r($exec,true).print_r($options,true)."\n";
610
				$stat = call_user_func_array($exec,$options);
611
			}
612
			return $result;
613
		}
614
		//error_log("self::find($path)=".print_r(array_keys($result),true));
615
		if ($exec !== true)
616
		{
617
			return array_keys($result);
618
		}
619
		return $result;
620
	}
621
622
	/**
623
	 * Function carying out the various (optional) checks, before files&dirs get returned as result of find
624
	 *
625
	 * @param array $options options, see self::find(,$options)
626
	 * @param string $path name of path to add
627
	 * @param array &$result here we add the stat for the key $path, if the checks are successful
628
	 */
629
	private static function _check_add($options,$path,&$result)
630
	{
631
		$type = $options['type'];	// 'd' or 'f'
632
633
		if ($options['url'])
634
		{
635
			if (($stat = @lstat($path)))
636
			{
637
				$stat = array_slice($stat,13);	// remove numerical indices 0-12
638
			}
639
		}
640
		else
641
		{
642
			$stat = self::lstat($path);
643
		}
644
		if (!$stat)
645
		{
646
			return;	// not found, should not happen
647
		}
648
		if ($type && (($type == 'd') == !($stat['mode'] & Vfs\Sqlfs\StreamWrapper::MODE_DIR) ||	// != is_dir() which can be true for symlinks
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: ($type && $type == 'd' =...ware\Api\Vfs::MODE_LINK, Probably Intended Meaning: $type && ($type == 'd' =...are\Api\Vfs::MODE_LINK)
Loading history...
649
		    $type == 'F' && is_dir($path)) ||	// symlink to a directory
650
			$type == '!l' && ($stat['mode'] & Vfs::MODE_LINK)) // Symlink
651
		{
652
			return;	// wrong type
653
		}
654
		$stat['path'] = self::parse_url($path,PHP_URL_PATH);
655
		$stat['name'] = $options['remove'] > 0 ? implode('/',array_slice(explode('/',$stat['path']),$options['remove'])) : self::basename($path);
656
657
		if ($options['mime'] || $options['need_mime'])
658
		{
659
			$stat['mime'] = self::mime_content_type($path);
660
		}
661
		if (isset($options['name_preg']) && !preg_match($options['name_preg'],$stat['name']) ||
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: (IssetNode && ! preg_mat...ns['path_preg'], $path), Probably Intended Meaning: IssetNode && (! preg_mat...s['path_preg'], $path))
Loading history...
662
			isset($options['path_preg']) && !preg_match($options['path_preg'],$path))
663
		{
664
			//echo "<p>!preg_match('{$options['name_preg']}','{$stat['name']}')</p>\n";
665
			return;	// wrong name or path
666
		}
667
		if (isset($options['gid']) && $stat['gid'] != $options['gid'] ||
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: (IssetNode && $stat['gid...id'] != $options['uid'], Probably Intended Meaning: IssetNode && ($stat['gid...d'] != $options['uid'])
Loading history...
668
			isset($options['uid']) && $stat['uid'] != $options['uid'])
669
		{
670
			return;	// wrong user or group
671
		}
672
		if (isset($options['mime']) && $options['mime'] != $stat['mime'])
673
		{
674
			if ($options['mime'][0] == '/')	// perl regular expression given
675
			{
676
				if (!preg_match($options['mime'], $stat['mime']))
677
				{
678
					return;	// wrong mime-type
679
				}
680
			}
681
			else
682
			{
683
				list($type,$subtype) = explode('/',$options['mime']);
684
				// no subtype (eg. 'image') --> check only the main type
685
				if ($subtype || substr($stat['mime'],0,strlen($type)+1) != $type.'/')
686
				{
687
					return;	// wrong mime-type
688
				}
689
			}
690
		}
691
		if (isset($options['size']) && !self::_check_num($stat['size'],$options['size']) ||
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: (IssetNode && ! self::_c...y'] !== ! $stat['size'], Probably Intended Meaning: IssetNode && (! self::_c...'] !== ! $stat['size'])
Loading history...
692
			(isset($options['empty']) && !!$options['empty'] !== !$stat['size']))
693
		{
694
			return;	// wrong size
695
		}
696
		if (isset($options['cmin']) && !self::_check_num(round((time()-$stat['ctime'])/60),$options['cmin']) ||
697
			isset($options['mmin']) && !self::_check_num(round((time()-$stat['mtime'])/60),$options['mmin']) ||
698
			isset($options['ctime']) && !self::_check_num(round((time()-$stat['ctime'])/86400),$options['ctime']) ||
699
			isset($options['mtime']) && !self::_check_num(round((time()-$stat['mtime'])/86400),$options['mtime']))
700
		{
701
			return;	// not create/modified in the spezified time
702
		}
703
		// do we return url or just vfs pathes
704
		if (!$options['url'])
705
		{
706
			$path = self::parse_url($path,PHP_URL_PATH);
707
		}
708
		$result[$path] = $stat;
709
	}
710
711
	private static function _check_num($value,$argument)
712
	{
713
		if (is_int($argument) && $argument >= 0 || $argument[0] != '-' && $argument[0] != '+')
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: (is_int($argument) && $a... && $argument[0] != '+', Probably Intended Meaning: is_int($argument) && ($a...&& $argument[0] != '+')
Loading history...
714
		{
715
			//echo "_check_num($value,$argument) check = == ".(int)($value == $argument)."\n";
716
			return $value == $argument;
717
		}
718
		if ($argument < 0)
719
		{
720
			//echo "_check_num($value,$argument) check < == ".(int)($value < abs($argument))."\n";
721
			return $value < abs($argument);
722
		}
723
		//echo "_check_num($value,$argument) check > == ".(int)($value > (int)substr($argument,1))."\n";
724
		return $value > (int) substr($argument,1);
725
	}
726
727
	/**
728
	 * Check if given directory is protected (user not allowed to remove or rename)
729
	 *
730
	 * Following directorys are protected:
731
	 * - /
732
	 * - /apps incl. subdirectories
733
	 * - /home
734
	 * - /templates incl. subdirectories
735
	 *
736
	 * @param string $dir path or url
737
	 * @return boolean true for protected dirs, false otherwise
738
	 */
739
	static function isProtectedDir($dir)
740
	{
741
		if ($dir[0] != '/') $dir = self::parse_url($dir, PHP_URL_PATH);
742
743
		return preg_match('#^/(apps(/[^/]+)?|home|templates(/[^/]+)?)?/*$#', $dir) > 0;
744
	}
745
746
	/**
747
	 * Recursiv remove all given url's, including it's content if they are files
748
	 *
749
	 * @param string|array $urls url or array of url's
750
	 * @param boolean $allow_urls =false allow to use url's, default no only pathes (to stay within the vfs)
751
	 * @throws Vfs\Exception\ProtectedDirectory if trying to delete a protected directory, see Vfs::isProtected()
752
	 * @return array
753
	 */
754
	static function remove($urls,$allow_urls=false)
755
	{
756
		//error_log(__METHOD__.'('.array2string($urls).')');
757
		foreach((array)$urls as $url)
758
		{
759
			// some precaution to never allow to (recursivly) remove /, /apps or /home, see Vfs::isProtected()
760
			if (self::isProtectedDir($url))
761
			{
762
				throw new Vfs\Exception\ProtectedDirectory("Deleting protected directory '$url' rejected!");
763
			}
764
		}
765
		return self::find($urls, array('depth'=>true,'url'=>$allow_urls,'hidden'=>true), __CLASS__.'::_rm_rmdir');
766
	}
767
768
	/**
769
	 * Helper function for remove: either rmdir or unlink given url (depending if it's a dir or file)
770
	 *
771
	 * @param string $url
772
	 * @return boolean
773
	 */
774
	static function _rm_rmdir($url)
775
	{
776
		if ($url[0] == '/')
777
		{
778
			$url = self::PREFIX . $url;
779
		}
780
		$vfs = new Vfs\StreamWrapper();
781
		if (is_dir($url) && !is_link($url))
782
		{
783
			return $vfs->rmdir($url,0);
784
		}
785
		return $vfs->unlink($url);
786
	}
787
788
	/**
789
	 * The stream_wrapper interface checks is_{readable|writable|executable} against the webservers uid,
790
	 * which is wrong in case of our vfs, as we use the current users id and memberships
791
	 *
792
	 * @param string $path
793
	 * @param int $check mode to check: one or more or'ed together of: 4 = self::READABLE,
794
	 * 	2 = self::WRITABLE, 1 = self::EXECUTABLE
795
	 * @return boolean
796
	 */
797
	static function is_readable($path,$check = self::READABLE)
798
	{
799
		return self::check_access($path,$check);
800
	}
801
802
	/**
803
	 * The stream_wrapper interface checks is_{readable|writable|executable} against the webservers uid,
804
	 * which is wrong in case of our vfs, as we use the current users id and memberships
805
	 *
806
	 * @param string $path path
807
	 * @param int $check mode to check: one or more or'ed together of: 4 = self::READABLE,
808
	 * 	2 = self::WRITABLE, 1 = self::EXECUTABLE
809
	 * @param array|boolean $stat =null stat array or false, to not query it again
810
	 * @param int $user =null user used for check, if not current user (self::$user)
811
	 * @return boolean
812
	 */
813
	static function check_access($path, $check, $stat=null, $user=null)
814
	{
815
		static $vfs = null;
816
817
		if (is_null($stat) && $user && $user != self::$user)
0 ignored issues
show
Bug Best Practice introduced by
The expression $user of type integer|null is loosely compared to true; 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...
818
		{
819
			static $path_user_stat = array();
820
821
			$backup_user = self::$user;
822
			self::$user = $user;
823
824
			if (!isset($path_user_stat[$path]) || !isset($path_user_stat[$path][$user]))
825
			{
826
				self::clearstatcache($path);
827
828
				if (!isset($vfs)) $vfs = new Vfs\StreamWrapper();
829
				$path_user_stat[$path][$user] = $vfs->url_stat($path, 0);
830
831
				self::clearstatcache($path);	// we need to clear the stat-cache after the call too, as the next call might be the regular user again!
832
			}
833
			if (($stat = $path_user_stat[$path][$user]))
834
			{
835
				// some backend mounts use $user:$pass in their url, for them we have to deny access!
836
				if (strpos(self::resolve_url($path, false, false, false), '$user') !== false)
837
				{
838
					$ret = false;
839
				}
840
				else
841
				{
842
					$ret = self::check_access($path, $check, $stat);
843
				}
844
			}
845
			else
846
			{
847
				$ret = false;	// no access, if we can not stat the file
848
			}
849
			self::$user = $backup_user;
850
851
			// we need to clear stat-cache again, after restoring original user, as eg. eACL is stored in session
852
			self::clearstatcache($path);
853
854
			//error_log(__METHOD__."(path=$path||stat[name]={$stat['name']},stat[mode]=".sprintf('%o',$stat['mode']).",$check,$user) ".array2string($ret));
855
			return $ret;
856
		}
857
858
		if (self::$is_root)
859
		{
860
			return true;
861
		}
862
863
		// throw exception if stat array is used insead of path, can be removed soon
864
		if (is_array($path))
0 ignored issues
show
introduced by
The condition is_array($path) is always false.
Loading history...
865
		{
866
			throw new Exception\WrongParameter('path has to be string, use check_access($path,$check,$stat=null)!');
867
		}
868
		// query stat array, if not given
869
		if (is_null($stat))
870
		{
871
			if (!isset($vfs)) $vfs = new Vfs\StreamWrapper();
872
			$stat = $vfs->url_stat($path,0);
873
		}
874
		//error_log(__METHOD__."(path=$path||stat[name]={$stat['name']},stat[mode]=".sprintf('%o',$stat['mode']).",$check)");
875
876
		if (!$stat)
877
		{
878
			//error_log(__METHOD__."(path=$path||stat[name]={$stat['name']},stat[mode]=".sprintf('%o',$stat['mode']).",$check) no stat array!");
879
			return false;	// file not found
880
		}
881
		// check if we use an EGroupwre stream wrapper, or a stock php one
882
		// if it's not an EGroupware one, we can NOT use uid, gid and mode!
883
		if (($scheme = self::parse_url($stat['url'],PHP_URL_SCHEME)) && !(class_exists(self::scheme2class($scheme))))
884
		{
885
			switch($check)
886
			{
887
				case self::READABLE:
888
					return is_readable($stat['url']);
889
				case self::WRITABLE:
890
					return is_writable($stat['url']);
891
				case self::EXECUTABLE:
892
					return is_executable($stat['url']);
893
			}
894
		}
895
		// check if other rights grant access
896
		if (($stat['mode'] & $check) == $check)
897
		{
898
			//error_log(__METHOD__."(path=$path||stat[name]={$stat['name']},stat[mode]=".sprintf('%o',$stat['mode']).",$check) access via other rights!");
899
			return true;
900
		}
901
		// check if there's owner access and we are the owner
902
		if (($stat['mode'] & ($check << 6)) == ($check << 6) && $stat['uid'] && $stat['uid'] == self::$user)
903
		{
904
			//error_log(__METHOD__."(path=$path||stat[name]={$stat['name']},stat[mode]=".sprintf('%o',$stat['mode']).",$check) access via owner rights!");
905
			return true;
906
		}
907
		// check if there's a group access and we have the right membership
908
		if (($stat['mode'] & ($check << 3)) == ($check << 3) && $stat['gid'])
909
		{
910
			if (($memberships = $GLOBALS['egw']->accounts->memberships(self::$user, true)) && in_array(-abs($stat['gid']), $memberships))
911
			{
912
				//error_log(__METHOD__."(path=$path||stat[name]={$stat['name']},stat[mode]=".sprintf('%o',$stat['mode']).",$check) access via group rights!");
913
				return true;
914
			}
915
		}
916
		// if we check writable and have a readonly mount --> return false, as backends dont know about r/o url parameter
917
		if ($check == self::WRITABLE && Vfs\StreamWrapper::url_is_readonly($stat['url']))
918
		{
919
			//error_log(__METHOD__."(path=$path, check=writable, ...) failed because mount is readonly");
920
			return false;
921
		}
922
		// check backend for extended acls (only if path given)
923
		$ret = $path && self::_call_on_backend('check_extended_acl',array(isset($stat['url'])?$stat['url']:$path,$check),true);	// true = fail silent if backend does not support
924
925
		//error_log(__METHOD__."(path=$path||stat[name]={$stat['name']},stat[mode]=".sprintf('%o',$stat['mode']).",$check) ".($ret ? 'backend extended acl granted access.' : 'no access!!!'));
926
		return $ret;
927
	}
928
929
	/**
930
	 * The stream_wrapper interface checks is_{readable|writable|executable} against the webservers uid,
931
	 * which is wrong in case of our vfs, as we use the current users id and memberships
932
	 *
933
	 * @param string $path
934
	 * @return boolean
935
	 */
936
	static function is_writable($path)
937
	{
938
		return self::is_readable($path,self::WRITABLE);
939
	}
940
941
	/**
942
	 * The stream_wrapper interface checks is_{readable|writable|executable} against the webservers uid,
943
	 * which is wrong in case of our vfs, as we use the current users id and memberships
944
	 *
945
	 * @param string $path
946
	 * @return boolean
947
	 */
948
	static function is_executable($path)
949
	{
950
		return self::is_readable($path,self::EXECUTABLE);
951
	}
952
953
	/**
954
	 * Check if path is a script and write access would be denied by backend
955
	 *
956
	 * @param string $path
957
	 * @return boolean true if $path is a script AND exec mount-option is NOT set, false otherwise
958
	 */
959
	static function deny_script($path)
960
	{
961
		return self::_call_on_backend('deny_script',array($path),true);
962
	}
963
964
	/**
965
	 * Name of EACL array in session
966
	 */
967
	const SESSION_EACL = 'session-eacl';
968
969
	/**
970
	 * Set or delete extended acl for a given path and owner (or delete  them if is_null($rights)
971
	 *
972
	 * Does NOT check if user has the rights to set the extended acl for the given url/path!
973
	 *
974
	 * @param string $url string with path
975
	 * @param int $rights =null rights to set, or null to delete the entry
976
	 * @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
977
	 * @param boolean $session_only =false true: set eacl only for this session, does NO further checks currently!
978
	 * @return boolean true if acl is set/deleted, false on error
979
	 */
980
	static function eacl($url,$rights=null,$owner=null,$session_only=false)
981
	{
982
		if ($session_only)
983
		{
984
			$session_eacls =& Cache::getSession(__CLASS__, self::SESSION_EACL);
985
			$session_eacls[] = array(
986
				'path'   => $url[0] == '/' ? $url : self::parse_url($url, PHP_URL_PATH),
987
				'owner'  => $owner ? $owner : self::$user,
988
				'rights' => $rights,
989
			);
990
			return true;
991
		}
992
		return self::_call_on_backend('eacl',array($url,$rights,$owner));
993
	}
994
995
	/**
996
	 * Get all ext. ACL set for a path
997
	 *
998
	 * Calls itself recursive, to get the parent directories
999
	 *
1000
	 * @param string $path
1001
	 * @return array|boolean array with array('path'=>$path,'owner'=>$owner,'rights'=>$rights) or false if $path not found
1002
	 */
1003
	static function get_eacl($path)
1004
	{
1005
		$eacls = self::_call_on_backend('get_eacl',array($path),true);	// true = fail silent (no PHP Warning)
1006
1007
		$session_eacls =& Cache::getSession(__CLASS__, self::SESSION_EACL);
1008
		if ($session_eacls)
1009
		{
1010
			// eacl is recursive, therefore we have to match all parent-dirs too
1011
			$paths = array($path);
1012
			while ($path && $path != '/')
1013
			{
1014
				$paths[] = $path = self::dirname($path);
1015
			}
1016
			foreach((array)$session_eacls as $eacl)
1017
			{
1018
				if (in_array($eacl['path'], $paths))
1019
				{
1020
					$eacls[] = $eacl;
1021
				}
1022
			}
1023
1024
			// sort by length descending, to show precedence
1025
			usort($eacls, function($a, $b) {
1026
				return strlen($b['path']) - strlen($a['path']);
1027
			});
1028
		}
1029
		return $eacls;
1030
	}
1031
1032
	/**
1033
	 * Store properties for a single ressource (file or dir)
1034
	 *
1035
	 * @param string $path string with path
1036
	 * @param array $props array of array with values for keys 'name', 'ns', 'val' (null to delete the prop)
1037
	 * @return boolean true if props are updated, false otherwise (eg. ressource not found)
1038
	 */
1039
	static function proppatch($path,array $props)
1040
	{
1041
		return self::_call_on_backend('proppatch',array($path,$props));
1042
	}
1043
1044
	/**
1045
	 * Default namespace for properties set by eGroupware: comment or custom fields (leading #)
1046
	 *
1047
	 */
1048
	const DEFAULT_PROP_NAMESPACE = 'http://egroupware.org/';
1049
1050
	/**
1051
	 * Read properties for a ressource (file, dir or all files of a dir)
1052
	 *
1053
	 * @param array|string $path (array of) string with path
1054
	 * @param string $ns ='http://egroupware.org/' namespace if propfind should be limited to a single one, otherwise use null
1055
	 * @return array|boolean array with props (values for keys 'name', 'ns', 'val'), or path => array of props for is_array($path)
1056
	 * 	false if $path does not exist
1057
	 */
1058
	static function propfind($path,$ns=self::DEFAULT_PROP_NAMESPACE)
1059
	{
1060
		return self::_call_on_backend('propfind',array($path,$ns),true);	// true = fail silent (no PHP Warning)
1061
	}
1062
1063
	/**
1064
	 * Private constructor to prevent instanciating this class, only it's static methods should be used
1065
	 */
1066
	private function __construct()
1067
	{
1068
1069
	}
1070
1071
	/**
1072
	 * Convert a symbolic mode string or octal mode to an integer
1073
	 *
1074
	 * @param string|int $set comma separated mode string to set [ugo]+[+=-]+[rwx]+
1075
	 * @param int $mode =0 current mode of the file, necessary for +/- operation
1076
	 * @return int
1077
	 */
1078
	static function mode2int($set,$mode=0)
1079
	{
1080
		if (is_int($set))		// already an integer
1081
		{
1082
			return $set;
1083
		}
1084
		if (is_numeric($set))	// octal string
1085
		{
1086
			//error_log(__METHOD__."($set,$mode) returning ".(int)base_convert($set,8,10));
1087
			return (int)base_convert($set,8,10);	// convert octal to decimal
1088
		}
1089
		foreach(explode(',',$set) as $s)
1090
		{
1091
			$matches = null;
1092
			if (!preg_match($use='/^([ugoa]*)([+=-]+)([rwx]+)$/',$s,$matches))
1093
			{
1094
				$use = str_replace(array('/','^','$','(',')'),'',$use);
1095
				throw new Exception\WrongUserinput("$s is not an allowed mode, use $use !");
1096
			}
1097
			$base = (strpos($matches[3],'r') !== false ? self::READABLE : 0) |
1098
				(strpos($matches[3],'w') !== false ? self::WRITABLE : 0) |
1099
				(strpos($matches[3],'x') !== false ? self::EXECUTABLE : 0);
1100
1101
			for($n = $m = 0; $n < strlen($matches[1]); $n++)
1102
			{
1103
				switch($matches[1][$n])
1104
				{
1105
					case 'o':
1106
						$m |= $base;
1107
						break;
1108
					case 'g':
1109
						$m |= $base << 3;
1110
						break;
1111
					case 'u':
1112
						$m |= $base << 6;
1113
						break;
1114
					default:
1115
					case 'a':
1116
						$m = $base | ($base << 3) | ($base << 6);
1117
				}
1118
			}
1119
			switch($matches[2])
1120
			{
1121
				case '+':
1122
					$mode |= $m;
1123
					break;
1124
				case '=':
1125
					$mode = $m;
1126
					break;
1127
				case '-':
1128
					$mode &= ~$m;
1129
			}
1130
		}
1131
		//error_log(__METHOD__."($set,) returning ".sprintf('%o',$mode));
1132
		return $mode;
1133
	}
1134
1135
	/**
1136
	 * Convert a numerical mode to a symbolic mode-string
1137
	 *
1138
	 * @param int $mode
1139
	 * @return string
1140
	 */
1141
	static function int2mode( $mode )
1142
	{
1143
		if(($mode & self::MODE_LINK) == self::MODE_LINK) // Symbolic Link
1144
		{
1145
			$sP = 'l';
1146
		}
1147
		elseif(($mode & 0xC000) == 0xC000) // Socket
1148
		{
1149
			$sP = 's';
1150
		}
1151
		elseif($mode & 0x1000)     // FIFO pipe
1152
		{
1153
			$sP = 'p';
1154
		}
1155
		elseif($mode & 0x2000) // Character special
1156
		{
1157
			$sP = 'c';
1158
		}
1159
		elseif($mode & 0x4000) // Directory
1160
		{
1161
			$sP = 'd';
1162
		}
1163
		elseif($mode & 0x6000) // Block special
1164
		{
1165
			$sP = 'b';
1166
		}
1167
		elseif($mode & 0x8000) // Regular
1168
		{
1169
			$sP = '-';
1170
		}
1171
		else                         // UNKNOWN
1172
		{
1173
			$sP = 'u';
1174
		}
1175
1176
		// owner
1177
		$sP .= (($mode & 0x0100) ? 'r' : '-') .
1178
		(($mode & 0x0080) ? 'w' : '-') .
1179
		(($mode & 0x0040) ? (($mode & 0x0800) ? 's' : 'x' ) :
1180
		(($mode & 0x0800) ? 'S' : '-'));
1181
1182
		// group
1183
		$sP .= (($mode & 0x0020) ? 'r' : '-') .
1184
		(($mode & 0x0010) ? 'w' : '-') .
1185
		(($mode & 0x0008) ? (($mode & 0x0400) ? 's' : 'x' ) :
1186
		(($mode & 0x0400) ? 'S' : '-'));
1187
1188
		// world
1189
		$sP .= (($mode & 0x0004) ? 'r' : '-') .
1190
		(($mode & 0x0002) ? 'w' : '-') .
1191
		(($mode & 0x0001) ? (($mode & 0x0200) ? 't' : 'x' ) :
1192
		(($mode & 0x0200) ? 'T' : '-'));
1193
1194
		return $sP;
1195
	}
1196
1197
	/**
1198
	 * Get the closest mime icon
1199
	 *
1200
	 * @param string $mime_type
1201
	 * @param boolean $et_image =true return $app/$icon string for etemplate (default) or url for false
1202
	 * @param int $size =128
1203
	 * @return string
1204
	 */
1205
	static function mime_icon($mime_type, $et_image=true, $size=128)
1206
	{
1207
		if ($mime_type == self::DIR_MIME_TYPE)
1208
		{
1209
			$mime_type = 'Directory';
1210
		}
1211
		if(!$mime_type)
1212
		{
1213
			$mime_type = 'unknown';
1214
		}
1215
		$mime_full = strtolower(str_replace	('/','_',$mime_type));
1216
		list($mime_part) = explode('_',$mime_full);
1217
1218
		if (!($img=Image::find('etemplate',$icon='mime'.$size.'_'.$mime_full)) &&
1219
			// check mime-alias-map before falling back to more generic icons
1220
			!(isset(MimeMagic::$mime_alias_map[$mime_type]) &&
1221
				($img=Image::find('etemplate',$icon='mime'.$size.'_'.str_replace('/','_',MimeMagic::$mime_alias_map[$mime_full])))) &&
1222
			!($img=Image::find('etemplate',$icon='mime'.$size.'_'.$mime_part)))
1223
		{
1224
			$img = Image::find('etemplate',$icon='mime'.$size.'_unknown');
1225
		}
1226
		return $et_image ? 'etemplate/'.$icon : $img;
1227
	}
1228
1229
	/**
1230
	 * Human readable size values in k, M or G
1231
	 *
1232
	 * @param int $size
1233
	 * @return string
1234
	 */
1235
	static function hsize($size)
1236
	{
1237
		if ($size < 1024) return $size;
1238
		if ($size < 1024*1024) return sprintf('%3.1lfk',(float)$size/1024);
1239
		if ($size < 1024*1024*1024) return sprintf('%3.1lfM',(float)$size/(1024*1024));
1240
		return sprintf('%3.1lfG',(float)$size/(1024*1024*1024));
1241
	}
1242
1243
	/**
1244
	 * Size in bytes, from human readable
1245
	 *
1246
	 * From PHP ini_get docs, Ivo Mandalski 15-Nov-2011 08:27
1247
	 */
1248
	static function int_size($_val)
1249
	{
1250
		if(empty($_val))return 0;
1251
1252
		$val = trim($_val);
1253
1254
		$matches = null;
1255
		preg_match('#([0-9]+)[\s]*([a-z]+)#i', $val, $matches);
1256
1257
		$last = '';
1258
		if(isset($matches[2])){
1259
			$last = $matches[2];
1260
		}
1261
1262
		if(isset($matches[1])){
1263
			$val = (int) $matches[1];
1264
		}
1265
1266
		switch (strtolower($last))
1267
		{
1268
			case 'g':
1269
			case 'gb':
1270
			$val *= 1024;
1271
			case 'm':
1272
			case 'mb':
1273
			$val *= 1024;
1274
			case 'k':
1275
			case 'kb':
1276
			$val *= 1024;
1277
		}
1278
1279
		return (int) $val;
1280
	}
1281
1282
	/**
1283
	 * like basename($path), but also working if the 1. char of the basename is non-ascii
1284
	 *
1285
	 * @param string $_path
1286
	 * @return string
1287
	 */
1288
	static function basename($_path)
1289
	{
1290
		list($path) = explode('?',$_path);	// remove query
1291
		$parts = explode('/',$path);
1292
1293
		return array_pop($parts);
1294
	}
1295
1296
	/**
1297
	 * Utf-8 save version of parse_url
1298
	 *
1299
	 * Does caching withing request, to not have to parse urls over and over again.
1300
	 *
1301
	 * @param string $url
1302
	 * @param int $component =-1 PHP_URL_* constants
1303
	 * @return array|string|boolean on success array or string, if $component given, or false on failure
1304
	 */
1305
	static function parse_url($url, $component=-1)
1306
	{
1307
		static $component2str = array(
1308
			PHP_URL_SCHEME => 'scheme',
1309
			PHP_URL_HOST => 'host',
1310
			PHP_URL_PORT => 'port',
1311
			PHP_URL_USER => 'user',
1312
			PHP_URL_PASS => 'pass',
1313
			PHP_URL_PATH => 'path',
1314
			PHP_URL_QUERY => 'query',
1315
			PHP_URL_FRAGMENT => 'fragment',
1316
		);
1317
		static $cache = array();	// some caching
1318
1319
		$result =& $cache[$url];
1320
1321
		if (!isset($result))
1322
		{
1323
			// Build arrays of values we need to decode before parsing
1324
			static $entities = array('%21', '%2A', '%27', '%28', '%29', '%3B', '%3A', '%40', '%26', '%3D', '%24', '%2C', '%2F', '%3F', '%23', '%5B', '%5D');
1325
			static $replacements = array('!', '*', "'", "(", ")", ";", ":", "@", "&", "=", "$", ",", "/", "?", "#", "[", "]");
1326
			static $str_replace = null;
1327
			if (!isset($str_replace)) $str_replace = function_exists('mb_str_replace') ? 'mb_str_replace' : 'str_replace';
1328
1329
			// Create encoded URL with special URL characters decoded so it can be parsed
1330
			// All other characters will be encoded
1331
			$encodedURL = $str_replace($entities, $replacements, urlencode($url));
1332
1333
			// Parse the encoded URL
1334
			$result = $encodedParts = parse_url($encodedURL);
1335
1336
			// Now, decode each value of the resulting array
1337
			if ($encodedParts)
0 ignored issues
show
Bug Best Practice introduced by
The expression $encodedParts 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...
1338
			{
1339
				$result = array();
1340
				foreach ($encodedParts as $key => $value)
1341
				{
1342
					$result[$key] = urldecode($str_replace($replacements, $entities, $value));
1343
				}
1344
			}
1345
		}
1346
		return $component >= 0 ? $result[$component2str[$component]] : $result;
1347
	}
1348
1349
	/**
1350
	 * Get the directory / parent of a given path or url(!), return false for '/'!
1351
	 *
1352
	 * Also works around PHP under Windows returning dirname('/something') === '\\', which is NOT understood by EGroupware's VFS!
1353
	 *
1354
	 * @param string $_url path or url
1355
	 * @return string|boolean parent or false if there's none ($path == '/')
1356
	 */
1357
	static function dirname($_url)
1358
	{
1359
		list($url,$query) = explode('?',$_url,2);	// strip the query first, as it can contain slashes
1360
1361
		if ($url == '/' || $url[0] != '/' && self::parse_url($url,PHP_URL_PATH) == '/')
1362
		{
1363
			//error_log(__METHOD__."($url) returning FALSE: already in root!");
1364
			return false;
1365
		}
1366
		$parts = explode('/',$url);
1367
		if (substr($url,-1) == '/') array_pop($parts);
1368
		array_pop($parts);
1369
		if ($url[0] != '/' && count($parts) == 3 || count($parts) == 1 && $parts[0] === '')
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: ($url[0] != '/' && count...= 1 && $parts[0] === '', Probably Intended Meaning: $url[0] != '/' && (count... 1 && $parts[0] === '')
Loading history...
1370
		{
1371
			array_push($parts,'');	// scheme://host is wrong (no path), has to be scheme://host/
1372
		}
1373
		//error_log(__METHOD__."($url)=".implode('/',$parts).($query ? '?'.$query : ''));
1374
		return implode('/',$parts).($query ? '?'.$query : '');
1375
	}
1376
1377
	/**
1378
	 * Check if the current use has owner rights for the given path or stat
1379
	 *
1380
	 * We define all eGW admins the owner of the group directories!
1381
	 *
1382
	 * @param string $path
1383
	 * @param array $stat =null stat for path, default queried by this function
1384
	 * @return boolean
1385
	 */
1386
	static function has_owner_rights($path,array $stat=null)
1387
	{
1388
		if (!$stat)
1389
		{
1390
			$vfs = new Vfs\StreamWrapper();
1391
			$stat = $vfs->url_stat($path,0);
1392
		}
1393
		return $stat['uid'] == self::$user &&	// (current) user is the owner
1394
				// in sharing current user != self::$user and should NOT have owner rights
1395
				$GLOBALS['egw_info']['user']['account_id'] == self::$user ||
1396
			self::$is_root ||					// class runs with root rights
1397
			!$stat['uid'] && $stat['gid'] && self::$is_admin;	// group directory and user is an eGW admin
1398
	}
1399
1400
	/**
1401
	 * Concat a relative path to an url, taking into account, that the url might already end with a slash or the path starts with one or is empty
1402
	 *
1403
	 * Also normalizing the path, as the relative path can contain ../
1404
	 *
1405
	 * @param string $_url base url or path, might end in a /
1406
	 * @param string $relative relative path to add to $url
1407
	 * @return string
1408
	 */
1409
	static function concat($_url,$relative)
1410
	{
1411
		list($url,$query) = explode('?',$_url,2);
1412
		if (substr($url,-1) == '/') $url = substr($url,0,-1);
1413
		$ret = ($relative === '' || $relative[0] == '/' ? $url.$relative : $url.'/'.$relative);
1414
1415
		// now normalize the path (remove "/something/..")
1416
		while (strpos($ret,'/../') !== false)
1417
		{
1418
			list($a_str,$b_str) = explode('/../',$ret,2);
1419
			$a = explode('/',$a_str);
1420
			array_pop($a);
1421
			$b = explode('/',$b_str);
1422
			$ret = implode('/',array_merge($a,$b));
1423
		}
1424
		return $ret.($query ? (strpos($url,'?')===false ? '?' : '&').$query : '');
1425
	}
1426
1427
	/**
1428
	 * Build an url from it's components (reverse of parse_url)
1429
	 *
1430
	 * @param array $url_parts values for keys 'scheme', 'host', 'user', 'pass', 'query', 'fragment' (all but 'path' are optional)
1431
	 * @return string
1432
	 */
1433
	static function build_url(array $url_parts)
1434
	{
1435
		$url = (!isset($url_parts['scheme'])?'':$url_parts['scheme'].'://'.
1436
			(!isset($url_parts['user'])?'':$url_parts['user'].(!isset($url_parts['pass'])?'':':'.$url_parts['pass']).'@').
1437
			$url_parts['host']).$url_parts['path'].
1438
			(!isset($url_parts['query'])?'':'?'.$url_parts['query']).
1439
			(!isset($url_parts['fragment'])?'':'?'.$url_parts['fragment']);
1440
		//error_log(__METHOD__.'('.array2string($url_parts).") = '".$url."'");
1441
		return $url;
1442
	}
1443
1444
	/**
1445
	 * URL to download a file
1446
	 *
1447
	 * We use our webdav handler as download url instead of an own download method.
1448
	 * The webdav hander (filemanager/webdav.php) recognices eGW's session cookie and of cause understands regular GET requests.
1449
	 *
1450
	 * Please note: If you dont use eTemplate or the html class, you have to run this url throught egw::link() to get a full url
1451
	 *
1452
	 * @param string $path
1453
	 * @param boolean $force_download =false add header('Content-disposition: filename="' . basename($path) . '"'), currently not supported!
1454
	 * @todo get $force_download working through webdav
1455
	 * @return string
1456
	 */
1457
	static function download_url($path,$force_download=false)
1458
	{
1459
		if (($url = self::_call_on_backend('download_url',array($path,$force_download),true)))
1460
		{
1461
			return $url;
1462
		}
1463
		if ($path[0] != '/')
1464
		{
1465
			$path = self::parse_url($path,PHP_URL_PATH);
1466
		}
1467
		// we do NOT need to encode % itself, as our path are already url encoded, with the exception of ' ' and '+'
1468
		// we urlencode double quotes '"', as that fixes many problems in html markup
1469
		return '/webdav.php'.strtr($path,array('+' => '%2B',' ' => '%20','"' => '%22')).($force_download ? '?download' : '');
1470
	}
1471
1472
	/**
1473
	 * Download the given file list as a ZIP
1474
	 *
1475
	 * @param array $_files List of files to include in the zip
1476
	 * @param string $name optional Zip file name.  If not provided, it will be determined automatically from the files
1477
	 *
1478
	 * @todo use https://github.com/maennchen/ZipStream-PHP to not assamble all files in memmory
1479
	 */
1480
	public static function download_zip(Array $_files, $name = false)
1481
	{
1482
		//error_log(__METHOD__ . ': '.implode(',',$_files));
1483
1484
		// Create zip file
1485
		$zip_file = tempnam($GLOBALS['egw_info']['server']['temp_dir'], 'zip');
1486
1487
		$zip = new \ZipArchive();
1488
		if (!$zip->open($zip_file, \ZipArchive::OVERWRITE))
1489
		{
1490
			throw new Exception("Cannot open zip file for writing.");
1491
		}
1492
1493
		// Find lowest common directory, to use relative paths
1494
		// eg: User selected /home/nathan/picture.jpg, /home/Pictures/logo.jpg
1495
		// We want /home
1496
		$dirs = array();
1497
		foreach($_files as $file)
1498
		{
1499
			$dirs[] = self::dirname($file);
1500
		}
1501
		$paths = array_unique($dirs);
1502
		if(count($paths) > 0)
1503
		{
1504
			// Shortest to longest
1505
			usort($paths, function($a, $b) {
1506
				return strlen($a) - strlen($b);
1507
			});
1508
1509
			// Start with shortest, pop off sub-directories that don't match
1510
			$parts = explode('/',$paths[0]);
1511
			foreach($paths as $path)
1512
			{
1513
				$dirs = explode('/',$path);
1514
				foreach($dirs as $dir_index => $dir)
1515
				{
1516
					if($parts[$dir_index] && $parts[$dir_index] != $dir)
1517
					{
1518
						unset($parts[$dir_index]);
1519
					}
1520
				}
1521
			}
1522
			$base_dir = implode('/', $parts);
1523
		}
1524
		else
1525
		{
1526
			$base_dir = $paths[0];
1527
		}
1528
1529
		// Remove 'unsafe' filename characters
1530
		// (en.wikipedia.org/wiki/Filename#Reserved_characters_and_words)
1531
		$replace = array(
1532
			// Linux
1533
			'/',
1534
			// Windows
1535
			'\\','?','%','*',':','|',/*'.',*/ '"','<','>'
1536
		);
1537
1538
		// A nice name for the user,
1539
		$filename = $GLOBALS['egw_info']['server']['site_title'] . '_' .
1540
			str_replace($replace,'_',(
1541
			$name ? $name : (
1542
			count($_files) == 1 ?
1543
			// Just one file (hopefully a directory?) selected
1544
			self::basename($_files[0]) :
1545
			// Use the lowest common directory (eg: Infolog, Open, nathan)
1546
			self::basename($base_dir))
1547
		)) . '.zip';
1548
1549
		// Make sure basename is a dir
1550
		if(substr($base_dir, -1) != '/')
1551
		{
1552
			$base_dir .='/';
1553
		}
1554
1555
		// Go into directories, find them all
1556
		$files = self::find($_files);
1557
		$links = array();
1558
1559
		// We need to remove them _after_ we're done
1560
		$tempfiles = array();
1561
1562
		// Give 1 second per file, but try to allow more time for big files when amount of files is low
1563
		set_time_limit((count($files)<=9?10:count($files)));
1564
1565
		// Add files to archive
1566
		foreach($files as &$addfile)
1567
		{
1568
			// Use relative paths inside zip
1569
			$relative = substr($addfile, strlen($base_dir));
1570
1571
			// Use safe names - replace unsafe chars, convert to ASCII (ZIP spec says CP437, but we'll try)
1572
			$path = explode('/',$relative);
1573
			$_name = Translation::convert(Translation::to_ascii(implode('/', str_replace($replace,'_',$path))),false,'ASCII');
1574
1575
			// Don't go infinite with app entries
1576
			if(self::is_link($addfile))
1577
			{
1578
				if(in_array($addfile, $links)) continue;
1579
				$links[] = $addfile;
1580
			}
1581
			// Add directory - if empty, client app might not show it though
1582
			if(self::is_dir($addfile))
1583
			{
1584
				// Zip directories
1585
				$zip->addEmptyDir($addfile);
1586
			}
1587
			else if(self::is_readable($addfile))
1588
			{
1589
				// Copy to temp file, as ZipArchive fails to read VFS
1590
				$temp = tempnam($GLOBALS['egw_info']['server']['temp_dir'], 'zip_');
1591
				$from = self::fopen($addfile,'r');
1592
		 		$to = fopen($temp,'w');
1593
				if(!stream_copy_to_stream($from,$to) || !$zip->addFile($temp, $_name))
1594
				{
1595
					unlink($temp);
1596
					trigger_error("Could not add $addfile to ZIP file", E_USER_ERROR);
1597
					continue;
1598
				}
1599
				// Keep temp file until _after_ zipping is done
1600
				$tempfiles[] = $temp;
1601
1602
				// Add comment in
1603
				$props = self::propfind($addfile);
1604
				if($props)
1605
				{
1606
					$comment = self::find_prop($props,'comment');
1607
					if($comment)
1608
					{
1609
						$zip->setCommentName($_name, $comment);
1610
					}
1611
				}
1612
				unset($props);
1613
			}
1614
		}
1615
1616
		// Set a comment to help tell them apart
1617
		$zip->setArchiveComment(lang('Created by %1', $GLOBALS['egw_info']['user']['account_lid']) . ' ' .DateTime::to());
0 ignored issues
show
Unused Code introduced by
The call to lang() has too many arguments starting with $GLOBALS['egw_info']['user']['account_lid']. ( Ignorable by Annotation )

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

1617
		$zip->setArchiveComment(/** @scrutinizer ignore-call */ lang('Created by %1', $GLOBALS['egw_info']['user']['account_lid']) . ' ' .DateTime::to());

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
1618
1619
		// Record total for debug, not available after close()
1620
		//$total_files = $zip->numFiles;
1621
1622
		$result = $zip->close();
1623
		if(!$result || !filesize($zip_file))
1624
		{
1625
			error_log('close() result: '.array2string($result));
1626
			return 'Error creating zip file';
1627
		}
1628
1629
		//error_log("Total files: " . $total_files . " Peak memory to zip: " . self::hsize(memory_get_peak_usage(true)));
1630
1631
		// Stop any buffering
1632
		while(ob_get_level() > 0)
1633
		{
1634
			ob_end_clean();
1635
		}
1636
1637
		// Stream the file to the client
1638
		header("Content-Type: application/zip");
1639
		header("Content-Length: " . filesize($zip_file));
1640
		header("Content-Disposition: attachment; filename=\"$filename\"");
1641
		readfile($zip_file);
1642
1643
		unlink($zip_file);
1644
		foreach($tempfiles as $temp_file)
1645
		{
1646
			unlink($temp_file);
1647
		}
1648
1649
		// Make sure to exit after, if you don't want to add to the ZIP
1650
	}
1651
1652
	/**
1653
	 * We cache locks within a request, as HTTP_WebDAV_Server generates so many, that it can be a bottleneck
1654
	 *
1655
	 * @var array
1656
	 */
1657
	static protected $lock_cache;
1658
1659
	/**
1660
	 * Log (to error log) all calls to lock(), unlock() or checkLock()
1661
	 *
1662
	 */
1663
	const LOCK_DEBUG = false;
1664
1665
	/**
1666
	 * lock a ressource/path
1667
	 *
1668
	 * @param string $path path or url
1669
	 * @param string &$token
1670
	 * @param int &$timeout
1671
	 * @param string &$owner
1672
	 * @param string &$scope
1673
	 * @param string &$type
1674
	 * @param boolean $update =false
1675
	 * @param boolean $check_writable =true should we check if the ressource is writable, before granting locks, default yes
1676
	 * @return boolean true on success
1677
	 */
1678
	static function lock($path,&$token,&$timeout,&$owner,&$scope,&$type,$update=false,$check_writable=true)
1679
	{
1680
		// we require write rights to lock/unlock a resource
1681
		if (!$path || $update && !$token || $check_writable &&
1682
			!(self::is_writable($path) || !self::file_exists($path) && ($dir=self::dirname($path)) && self::is_writable($dir)))
1683
		{
1684
			return false;
1685
		}
1686
    	// remove the lock info evtl. set in the cache
1687
    	unset(self::$lock_cache[$path]);
1688
1689
    	if ($timeout < 1000000)	// < 1000000 is a relative timestamp, so we add the current time
1690
    	{
1691
    		$timeout += time();
1692
    	}
1693
1694
		if ($update)	// Lock Update
1695
		{
1696
			if (($ret = (boolean)($row = self::$db->select(self::LOCK_TABLE,array('lock_owner','lock_exclusive','lock_write'),array(
1697
				'lock_path' => $path,
1698
				'lock_token' => $token,
1699
			),__LINE__,__FILE__)->fetch())))
1700
			{
1701
				$owner = $row['lock_owner'];
1702
				$scope = Db::from_bool($row['lock_exclusive']) ? 'exclusive' : 'shared';
1703
				$type  = Db::from_bool($row['lock_write']) ? 'write' : 'read';
1704
1705
				self::$db->update(self::LOCK_TABLE,array(
1706
					'lock_expires' => $timeout,
1707
					'lock_modified' => time(),
1708
				),array(
1709
					'lock_path' => $path,
1710
					'lock_token' => $token,
1711
				),__LINE__,__FILE__);
1712
			}
1713
		}
1714
		// HTTP_WebDAV_Server does this check before calling LOCK, but we want to be complete and usable outside WebDAV
1715
		elseif(($lock = self::checkLock($path)) && ($lock['scope'] == 'exclusive' || $scope == 'exclusive'))
1716
		{
1717
			$ret = false;	// there's alread a lock
1718
		}
1719
		else
1720
		{
1721
			// HTTP_WebDAV_Server sets owner and token, but we want to be complete and usable outside WebDAV
1722
			if (!$owner || $owner == 'unknown')
1723
			{
1724
				$owner = 'mailto:'.$GLOBALS['egw_info']['user']['account_email'];
1725
			}
1726
			if (!$token)
1727
			{
1728
				require_once(__DIR__.'/WebDAV/Server.php');
1729
				$token = HTTP_WebDAV_Server::_new_locktoken();
1730
			}
1731
			try {
1732
				self::$db->insert(self::LOCK_TABLE,array(
1733
					'lock_token' => $token,
1734
					'lock_path'  => $path,
1735
					'lock_created' => time(),
1736
					'lock_modified' => time(),
1737
					'lock_owner' => $owner,
1738
					'lock_expires' => $timeout,
1739
					'lock_exclusive' => $scope == 'exclusive',
1740
					'lock_write' => $type == 'write',
1741
				),false,__LINE__,__FILE__);
1742
				$ret = true;
1743
			}
1744
			catch(Db\Exception $e) {
1745
				unset($e);
1746
				$ret = false;	// there's already a lock
1747
			}
1748
		}
1749
		if (self::LOCK_DEBUG) error_log(__METHOD__."($path,$token,$timeout,$owner,$scope,$type,update=$update,check_writable=$check_writable) returns ".($ret ? 'true' : 'false'));
1750
		return $ret;
1751
	}
1752
1753
    /**
1754
     * unlock a ressource/path
1755
     *
1756
     * @param string $path path to unlock
1757
     * @param string $token locktoken
1758
	 * @param boolean $check_writable =true should we check if the ressource is writable, before granting locks, default yes
1759
     * @return boolean true on success
1760
     */
1761
    static function unlock($path,$token,$check_writable=true)
1762
    {
1763
		// we require write rights to lock/unlock a resource
1764
		if ($check_writable && !self::is_writable($path))
1765
		{
1766
			return false;
1767
		}
1768
        if (($ret = self::$db->delete(self::LOCK_TABLE,array(
1769
        	'lock_path' => $path,
1770
        	'lock_token' => $token,
1771
        ),__LINE__,__FILE__) && self::$db->affected_rows()))
1772
        {
1773
        	// remove the lock from the cache too
1774
        	unset(self::$lock_cache[$path]);
1775
        }
1776
		if (self::LOCK_DEBUG) error_log(__METHOD__."($path,$token,$check_writable) returns ".($ret ? 'true' : 'false'));
1777
		return $ret;
1778
    }
1779
1780
	/**
1781
	 * checkLock() helper
1782
	 *
1783
	 * @param  string resource path to check for locks
1784
	 * @return array|boolean false if there's no lock, else array with lock info
1785
	 */
1786
	static function checkLock($path)
1787
	{
1788
		if (isset(self::$lock_cache[$path]))
1789
		{
1790
			if (self::LOCK_DEBUG) error_log(__METHOD__."($path) returns from CACHE ".str_replace(array("\n",'    '),'',print_r(self::$lock_cache[$path],true)));
1791
			return self::$lock_cache[$path];
1792
		}
1793
		$where = 'lock_path='.self::$db->quote($path);
1794
		// ToDo: additional check parent dirs for locks and children of the requested directory
1795
		//$where .= ' OR '.self::$db->quote($path).' LIKE '.self::$db->concat('lock_path',"'%'").' OR lock_path LIKE '.self::$db->quote($path.'%');
1796
		// ToDo: shared locks can return multiple rows
1797
		if (($result = self::$db->select(self::LOCK_TABLE,'*',$where,__LINE__,__FILE__)->fetch()))
1798
		{
1799
			$result = Db::strip_array_keys($result,'lock_');
1800
			$result['type']  = Db::from_bool($result['write']) ? 'write' : 'read';
1801
			$result['scope'] = Db::from_bool($result['exclusive']) ? 'exclusive' : 'shared';
1802
			$result['depth'] = Db::from_bool($result['recursive']) ? 'infinite' : 0;
1803
		}
1804
		if ($result && $result['expires'] < time())	// lock is expired --> remove it
1805
		{
1806
	        self::$db->delete(self::LOCK_TABLE,array(
1807
	        	'lock_path' => $result['path'],
1808
	        	'lock_token' => $result['token'],
1809
	        ),__LINE__,__FILE__);
1810
1811
			if (self::LOCK_DEBUG) error_log(__METHOD__."($path) lock is expired at ".date('Y-m-d H:i:s',$result['expires'])." --> removed");
1812
	        $result = false;
1813
		}
1814
		if (self::LOCK_DEBUG) error_log(__METHOD__."($path) returns ".($result?array2string($result):'false'));
1815
		return self::$lock_cache[$path] = $result;
1816
	}
1817
1818
	/**
1819
	 * Get backend specific information (data and etemplate), to integrate as tab in filemanagers settings dialog
1820
	 *
1821
	 * @param string $path
1822
	 * @param array $content =null
1823
	 * @return array|boolean array with values for keys 'data','etemplate','name','label','help' or false if not supported by backend
1824
	 */
1825
	static function getExtraInfo($path,array $content=null)
1826
	{
1827
		$extra = array();
1828
		if (($extra_info = self::_call_on_backend('extra_info',array($path,$content),true)))	// true = fail silent if backend does NOT support it
1829
		{
1830
			$extra[] = $extra_info;
1831
		}
1832
1833
		if (($vfs_extra = Hooks::process(array(
1834
			'location' => 'vfs_extra',
1835
			'path' => $path,
1836
			'content' => $content,
1837
		))))
1838
		{
1839
			foreach($vfs_extra as $data)
1840
			{
1841
				$extra = $extra ? array_merge($extra, $data) : $data;
1842
			}
1843
		}
1844
		return $extra;
1845
	}
1846
1847
	/**
1848
	 * Mapps entries of applications to a path for the locking
1849
	 *
1850
	 * @param string $app
1851
	 * @param int|string $id
1852
	 * @return string
1853
	 */
1854
	static function app_entry_lock_path($app,$id)
1855
	{
1856
		return "/apps/$app/entry/$id";
1857
	}
1858
1859
	/**
1860
	 * Encoding of various special characters, which can NOT be unencoded in file-names, as they have special meanings in URL's
1861
	 *
1862
	 * @var array
1863
	 */
1864
	static public $encode = array(
1865
		'%' => '%25',
1866
		'#' => '%23',
1867
		'?' => '%3F',
1868
		'/' => '',	// better remove it completly
1869
	);
1870
1871
	/**
1872
	 * Encode a path component: replacing certain chars with their urlencoded counterparts
1873
	 *
1874
	 * Not all chars get encoded, slashes '/' are silently removed!
1875
	 *
1876
	 * To reverse the encoding, eg. to display a filename to the user, you have to use self::decodePath()
1877
	 *
1878
	 * @param string|array $component
1879
	 * @return string|array
1880
	 */
1881
	static public function encodePathComponent($component)
1882
	{
1883
		return str_replace(array_keys(self::$encode),array_values(self::$encode),$component);
1884
	}
1885
1886
	/**
1887
	 * Encode a path: replacing certain chars with their urlencoded counterparts
1888
	 *
1889
	 * To reverse the encoding, eg. to display a filename to the user, you have to use self::decodePath()
1890
	 *
1891
	 * @param string $path
1892
	 * @return string
1893
	 */
1894
	static public function encodePath($path)
1895
	{
1896
		return implode('/',self::encodePathComponent(explode('/',$path)));
1897
	}
1898
1899
	/**
1900
	 * Decode a path: rawurldecode(): mostly urldecode(), but do NOT decode '+', as we're NOT encoding it!
1901
	 *
1902
	 * Used eg. to translate a path for displaying to the User.
1903
	 *
1904
	 * @param string $path
1905
	 * @return string
1906
	 */
1907
	static public function decodePath($path)
1908
	{
1909
		return rawurldecode($path);
1910
	}
1911
1912
	/**
1913
	 * Initialise our static vars
1914
	 */
1915
	static function init_static()
1916
	{
1917
		// if special user/vfs_user given (eg. from sharing) use it instead default user/account_id
1918
		self::$user = (int)(isset($GLOBALS['egw_info']['user']['vfs_user']) ?
1919
			$GLOBALS['egw_info']['user']['vfs_user'] : $GLOBALS['egw_info']['user']['account_id']);
1920
		self::$is_admin = isset($GLOBALS['egw_info']['user']['apps']['admin']);
1921
		self::$db = isset($GLOBALS['egw_setup']->db) ? $GLOBALS['egw_setup']->db : $GLOBALS['egw']->db;
1922
		self::$lock_cache = array();
1923
	}
1924
1925
	/**
1926
	 * Returns the URL to the thumbnail of the given file. The thumbnail may simply
1927
	 * be the mime-type icon, or - if activated - the preview with the given thsize.
1928
	 *
1929
	 * @param string $file name of the file
1930
	 * @param int $thsize the size of the preview - false if the default should be used.
1931
	 * @param string $mime if you already know the mime type of the file, you can supply
1932
	 * 	it here. Otherwise supply "false".
1933
	 */
1934
	public static function thumbnail_url($file, $thsize = false, $mime = false)
1935
	{
1936
		// Retrive the mime-type of the file
1937
		if (!$mime)
1938
		{
1939
			$mime = self::mime_content_type($file);
1940
		}
1941
1942
		$image = "";
1943
1944
		// Seperate the mime type into the primary and the secondary part
1945
		list($mime_main, $mime_sub) = explode('/', $mime);
1946
1947
		if ($mime_main == 'egw')
1948
		{
1949
			$image = Image::find($mime_sub, 'navbar');
1950
		}
1951
		else if ($file && $mime_main == 'image' && in_array($mime_sub, array('png','jpeg','jpg','gif','bmp')) &&
1952
		         (string)$GLOBALS['egw_info']['server']['link_list_thumbnail'] != '0' &&
1953
		         (string)$GLOBALS['egw_info']['user']['preferences']['common']['link_list_thumbnail'] != '0' &&
1954
		         ($stat = self::stat($file)) && $stat['size'] < 1500000)
1955
		{
1956
			if (substr($file, 0, 6) == '/apps/')
1957
			{
1958
				$file = self::parse_url(self::resolve_url_symlinks($file), PHP_URL_PATH);
1959
			}
1960
1961
			//Assemble the thumbnail parameters
1962
			$thparams = array();
1963
			$thparams['path'] = $file;
1964
			if ($thsize)
0 ignored issues
show
Bug Best Practice introduced by
The expression $thsize of type false|integer is loosely compared to true; this is ambiguous if the integer can be 0. 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 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...
1965
			{
1966
				$thparams['thsize'] = $thsize;
1967
			}
1968
			$image = $GLOBALS['egw']->link('/api/thumbnail.php', $thparams);
1969
		}
1970
		else
1971
		{
1972
			list($app, $name) = explode("/", self::mime_icon($mime), 2);
1973
			$image = Image::find($app, $name);
1974
		}
1975
1976
		return $image;
1977
	}
1978
1979
	/**
1980
	 * Get the configured start directory for the current user
1981
	 *
1982
	 * @return string
1983
	 */
1984
	static public function get_home_dir()
1985
	{
1986
		// with sharing active we have no home, use /
1987
		if ($GLOBALS['egw_info']['user']['account_id'] != self::$user)
1988
		{
1989
			return '/';
1990
		}
1991
		$start = '/home/'.$GLOBALS['egw_info']['user']['account_lid'];
1992
1993
		// check if user specified a valid startpath in his prefs --> use it
1994
		if (($path = $GLOBALS['egw_info']['user']['preferences']['filemanager']['startfolder']) &&
1995
			$path[0] == '/' && self::is_dir($path) && self::check_access($path, self::READABLE))
1996
		{
1997
			$start = $path;
1998
		}
1999
		return $start;
2000
	}
2001
2002
	/**
2003
	 * Copies the files given in $src to $dst.
2004
	 *
2005
	 * @param array $src contains the source file
2006
	 * @param string $dst is the destination directory
2007
	 * @param int& $errs =null on return number of errors happened
2008
	 * @param array& $copied =null on return files copied
2009
	 * @return boolean true for no errors, false otherwise
2010
	 */
2011
	static public function copy_files(array $src, $dst, &$errs=null, array &$copied=null)
2012
	{
2013
		if (self::is_dir($dst))
2014
		{
2015
			foreach ($src as $file)
2016
			{
2017
				// Check whether the file has already been copied - prevents from
2018
				// recursion
2019
				if (!in_array($file, $copied))
2020
				{
2021
					// Calculate the target filename
2022
					$target = self::concat($dst, self::basename($file));
2023
2024
					if (self::is_dir($file))
2025
					{
2026
						if ($file !== $target)
2027
						{
2028
							// Create the target directory
2029
							self::mkdir($target,null,STREAM_MKDIR_RECURSIVE);
2030
2031
							$copied[] = $file;
2032
							$copied[] = $target; // < newly created folder must not be copied again!
2033
							if (self::copy_files(self::find($file), $target,
2034
								$errs, $copied))
2035
							{
2036
								continue;
2037
							}
2038
						}
2039
2040
						$errs++;
2041
					}
2042
					else
2043
					{
2044
						// Copy a single file - check whether the file should be
2045
						// copied onto itself.
2046
						// TODO: Check whether target file already exists and give
2047
						// return those files so that a dialog might be displayed
2048
						// on the client side which lets the user decide.
2049
						if ($target !== $file && self::copy($file, $target))
2050
						{
2051
							$copied[] = $file;
2052
						}
2053
						else
2054
						{
2055
							$errs++;
2056
						}
2057
					}
2058
				}
2059
			}
2060
		}
2061
2062
		return $errs == 0;
2063
	}
2064
2065
	/**
2066
	 * Moves the files given in src to dst
2067
	 */
2068
	static public function move_files(array $src, $dst, &$errs, array &$moved)
2069
	{
2070
		if (self::is_dir($dst))
2071
		{
2072
			$vfs = new Vfs\StreamWrapper();
2073
			foreach($src as $file)
2074
			{
2075
				$target = self::concat($dst, self::basename($file));
2076
2077
				if ($file != $target && $vfs->rename($file, $target))
2078
				{
2079
					$moved[] = $file;
2080
				}
2081
				else
2082
				{
2083
					++$errs;
2084
				}
2085
			}
2086
2087
			return $errs == 0;
2088
		}
2089
2090
		return false;
2091
	}
2092
2093
	/**
2094
	 * Copy an uploaded file into the vfs, optionally set some properties (eg. comment or other cf's)
2095
	 *
2096
	 * Treat copying incl. properties as atomar operation in respect of notifications (one notification about an added file).
2097
	 *
2098
	 * @param array|string|resource $src path to uploaded file or etemplate file array (value for key 'tmp_name'), or resource with opened file
2099
	 * @param string $target path or directory to copy uploaded file
2100
	 * @param array|string $props =null array with properties (name => value pairs, eg. 'comment' => 'FooBar','#cfname' => 'something'),
2101
	 * 	array as for proppatch (array of array with values for keys 'name', 'val' and optional 'ns') or string with comment
2102
	 * @param boolean $check_is_uploaded_file =true should method perform an is_uploaded_file check, default yes
2103
	 * @return boolean|array stat array on success, false on error
2104
	 */
2105
	static public function copy_uploaded($src,$target,$props=null,$check_is_uploaded_file=true)
2106
	{
2107
		$tmp_name = is_array($src) ? $src['tmp_name'] : $src;
2108
2109
		if (self::stat($target) && self::is_dir($target))
2110
		{
2111
			$target = self::concat($target, self::encodePathComponent(is_array($src) ? $src['name'] : basename($tmp_name)));
2112
		}
2113
		if ($check_is_uploaded_file && !is_resource($tmp_name) && !is_uploaded_file($tmp_name))
2114
		{
2115
			if (self::LOG_LEVEL) error_log(__METHOD__."($tmp_name, $target, ".array2string($props).",$check_is_uploaded_file) returning FALSE !is_uploaded_file()");
2116
			return false;
2117
		}
2118
		if (!(self::is_writable($target) || ($dir = self::dirname($target)) && self::is_writable($dir)))
2119
		{
2120
			if (self::LOG_LEVEL) error_log(__METHOD__."($tmp_name, $target, ".array2string($props).",$check_is_uploaded_file) returning FALSE !writable");
2121
			return false;
2122
		}
2123
		if ($props)
2124
		{
2125
			if (!is_array($props)) $props = array(array('name' => 'comment','val' => $props));
2126
2127
			// if $props is name => value pairs, convert it to internal array or array with values for keys 'name', 'val' and optional 'ns'
2128
			if (!isset($props[0]))
2129
			{
2130
				foreach($props as $name => $val)
2131
				{
2132
					if (($name == 'comment' || $name[0] == '#') && $val)	// only copy 'comment' and cfs
2133
					{
2134
						$vfs_props[] = array(
2135
							'name' => $name,
2136
							'val'  => $val,
2137
						);
2138
					}
2139
				}
2140
				$props = $vfs_props;
2141
			}
2142
		}
2143
		if ($props)
2144
		{
2145
			// set props before copying the file, so notifications already contain them
2146
			if (!self::stat($target))
2147
			{
2148
				self::touch($target);	// create empty file, to be able to attach properties
2149
				// tell vfs stream-wrapper to treat file in following copy as a new file notification-wises
2150
				$context = stream_context_create(array(
2151
					self::SCHEME => array('treat_as_new' => true)
2152
				));
2153
			}
2154
			self::proppatch($target, $props);
2155
		}
2156
		if (is_resource($tmp_name))
2157
		{
2158
			$ret = ($dest = self::fopen($target, 'w', $context)) &&
2159
				stream_copy_to_stream($tmp_name, $dest) !== false &&
2160
				fclose($dest) ? self::stat($target) : false;
2161
2162
			fclose($tmp_name);
2163
		}
2164
		else
2165
		{
2166
			$ret = ($context ? copy($tmp_name, self::PREFIX.$target, $context) :
2167
				copy($tmp_name, self::PREFIX.$target)) ?
2168
				self::stat($target) : false;
2169
		}
2170
		if (self::LOG_LEVEL > 1 || !$ret && self::LOG_LEVEL) error_log(__METHOD__."($tmp_name, $target, ".array2string($props).") returning ".array2string($ret));
2171
		return $ret;
2172
	}
2173
2174
	/**
2175
	 * Compare two files from vfs or local file-system for identical content
2176
	 *
2177
	 * VFS files must use URL, to be able to distinguish them eg. from temp. files!
2178
	 *
2179
	 * @param string $file1 vfs-url or local path, eg. /tmp/some-file.txt or vfs://default/home/user/some-file.txt
2180
	 * @param string $file2 -- " --
2181
	 * @return boolean true: if files are identical, false: if not or file not found
2182
	 */
2183
	public static function compare($file1, $file2)
2184
	{
2185
		if (filesize($file1) != filesize($file2) ||
2186
			!($fp1 = fopen($file1, 'r')) || !($fp2 = fopen($file2, 'r')))
2187
		{
2188
			//error_log(__METHOD__."($file1, $file2) returning FALSE (different size)");
2189
			return false;
2190
		}
2191
		while (($read1 = fread($fp1, 8192)) !== false &&
2192
			($read2 = fread($fp2, 8192)) !== false &&
2193
			$read1 === $read2 && !feof($fp1) && !feof($fp2))
2194
		{
2195
			// just loop until we find a difference
2196
		}
2197
2198
		fclose($fp1);
2199
		fclose($fp2);
2200
		//error_log(__METHOD__."($file1, $file2) returning ".array2string($read1 === $read2)." (content differs)");
2201
		return $read1 === $read2;
2202
	}
2203
2204
	/**
2205
	 * Resolve the given path according to our fstab AND symlinks
2206
	 *
2207
	 * @param string $_path
2208
	 * @param boolean $file_exists =true true if file needs to exists, false if not
2209
	 * @param boolean $resolve_last_symlink =true
2210
	 * @param array|boolean &$stat=null on return: stat of existing file or false for non-existing files
2211
	 * @return string|boolean false if the url cant be resolved, should not happen if fstab has a root entry
2212
	 */
2213
	static function resolve_url_symlinks($_path,$file_exists=true,$resolve_last_symlink=true,&$stat=null)
2214
	{
2215
		$vfs = new Vfs\StreamWrapper();
2216
		return $vfs->resolve_url_symlinks($_path, $file_exists, $resolve_last_symlink, $stat);
2217
	}
2218
2219
	/**
2220
	 * Resolve the given path according to our fstab
2221
	 *
2222
	 * @param string $_path
2223
	 * @param boolean $do_symlink =true is a direct match allowed, default yes (must be false for a lstat or readlink!)
2224
	 * @param boolean $use_symlinkcache =true
2225
	 * @param boolean $replace_user_pass_host =true replace $user,$pass,$host in url, default true, if false result is not cached
2226
	 * @param boolean $fix_url_query =false true append relativ path to url query parameter, default not
2227
	 * @return string|boolean false if the url cant be resolved, should not happen if fstab has a root entry
2228
	 */
2229
	static function resolve_url($_path,$do_symlink=true,$use_symlinkcache=true,$replace_user_pass_host=true,$fix_url_query=false)
2230
	{
2231
		return Vfs\StreamWrapper::resolve_url($_path, $do_symlink, $use_symlinkcache, $replace_user_pass_host, $fix_url_query);
2232
	}
2233
2234
	/**
2235
	 * This method is called in response to mkdir() calls on URL paths associated with the wrapper.
2236
	 *
2237
	 * It should attempt to create the directory specified by path.
2238
	 * In order for the appropriate error message to be returned, do not define this method if your wrapper does not support creating directories.
2239
	 *
2240
	 * @param string $path
2241
	 * @param int $mode =0750
2242
	 * @param boolean $recursive =false true: create missing parents too
2243
	 * @return boolean TRUE on success or FALSE on failure
2244
	 */
2245
	static function mkdir ($path, $mode=0750, $recursive=false)
2246
	{
2247
		return $path[0] == '/' && mkdir(self::PREFIX.$path, $mode, $recursive);
2248
	}
2249
2250
	/**
2251
	 * This method is called in response to rmdir() calls on URL paths associated with the wrapper.
2252
	 *
2253
	 * It should attempt to remove the directory specified by path.
2254
	 * In order for the appropriate error message to be returned, do not define this method if your wrapper does not support removing directories.
2255
	 *
2256
	 * @param string $path
2257
	 * @param int $options Possible values include STREAM_REPORT_ERRORS.
2258
	 * @return boolean TRUE on success or FALSE on failure.
2259
	 */
2260
	static function rmdir($path)
2261
	{
2262
		return $path[0] == '/' && rmdir(self::PREFIX.$path);
2263
	}
2264
2265
	/**
2266
	 * This method is called in response to unlink() calls on URL paths associated with the wrapper.
2267
	 *
2268
	 * It should attempt to delete the item specified by path.
2269
	 * In order for the appropriate error message to be returned, do not define this method if your wrapper does not support unlinking!
2270
	 *
2271
	 * @param string $path
2272
	 * @return boolean TRUE on success or FALSE on failure
2273
	 */
2274
	static function unlink ( $path )
2275
	{
2276
		return $path[0] == '/' && unlink(self::PREFIX.$path);
2277
	}
2278
2279
	/**
2280
	 * Allow to call methods of the underlying stream wrapper: touch, chmod, chgrp, chown, ...
2281
	 *
2282
	 * We cant use a magic __call() method, as it does not work for static methods!
2283
	 *
2284
	 * @param string $name
2285
	 * @param array $params first param has to be the path, otherwise we can not determine the correct wrapper
2286
	 * @param boolean $fail_silent =false should only false be returned if function is not supported by the backend,
2287
	 * 	or should an E_USER_WARNING error be triggered (default)
2288
	 * @param int $path_param_key =0 key in params containing the path, default 0
2289
	 * @return mixed return value of backend or false if function does not exist on backend
2290
	 */
2291
	static protected function _call_on_backend($name,$params,$fail_silent=false,$path_param_key=0)
2292
	{
2293
		$pathes = $params[$path_param_key];
2294
2295
		$scheme2urls = array();
2296
		foreach(is_array($pathes) ? $pathes : array($pathes) as $path)
2297
		{
2298
			if (!($url = self::resolve_url_symlinks($path,false,false)))
2299
			{
2300
				return false;
2301
			}
2302
			$k=(string)self::parse_url($url,PHP_URL_SCHEME);
2303
			if (!(is_array($scheme2urls[$k]))) $scheme2urls[$k] = array();
2304
			$scheme2urls[$k][$path] = $url;
2305
		}
2306
		$ret = array();
2307
		foreach($scheme2urls as $scheme => $urls)
2308
		{
2309
			if ($scheme)
2310
			{
2311
				if (!class_exists($class = self::scheme2class($scheme)) || !method_exists($class,$name))
2312
				{
2313
					if (!$fail_silent) trigger_error("Can't $name for scheme $scheme!\n",E_USER_WARNING);
2314
					return false;
2315
				}
2316
				if (!is_array($pathes))
2317
				{
2318
					$params[$path_param_key] = $url;
2319
2320
					return call_user_func_array(array($class,$name),$params);
2321
				}
2322
				$params[$path_param_key] = $urls;
2323
				if (!is_array($r = call_user_func_array(array($class,$name),$params)))
2324
				{
2325
					return $r;
2326
				}
2327
				// we need to re-translate the urls to pathes, as they can eg. contain symlinks
2328
				foreach($urls as $path => $url)
2329
				{
2330
					if (isset($r[$url]) || isset($r[$url=self::parse_url($url,PHP_URL_PATH)]))
2331
					{
2332
						$ret[$path] = $r[$url];
2333
					}
2334
				}
2335
			}
2336
			// call the filesystem specific function (dont allow to use arrays!)
2337
			elseif(!function_exists($name) || is_array($pathes))
2338
			{
2339
				return false;
2340
			}
2341
			else
2342
			{
2343
				$time = null;
2344
				return $name($url,$time);
2345
			}
2346
		}
2347
		return $ret;
2348
	}
2349
2350
	/**
2351
	 * touch just running on VFS path
2352
	 *
2353
	 * @param string $path
2354
	 * @param int $time =null modification time (unix timestamp), default null = current time
2355
	 * @param int $atime =null access time (unix timestamp), default null = current time, not implemented in the vfs!
2356
	 * @return boolean true on success, false otherwise
2357
	 */
2358
	static function touch($path,$time=null,$atime=null)
2359
	{
2360
		return $path[0] == '/' && touch(self::PREFIX.$path, $time, $atime);
2361
	}
2362
2363
	/**
2364
	 * chmod just running on VFS path
2365
	 *
2366
	 * Requires owner or root rights!
2367
	 *
2368
	 * @param string $path
2369
	 * @param string $mode mode string see Vfs::mode2int
2370
	 * @return boolean true on success, false otherwise
2371
	 */
2372
	static function chmod($path,$mode)
2373
	{
2374
		return $path[0] == '/' && chmod(self::PREFIX.$path, $mode);
2375
	}
2376
2377
	/**
2378
	 * chmod just running on VFS path
2379
	 *
2380
	 * Requires root rights!
2381
	 *
2382
	 * @param string $path
2383
	 * @param int|string $owner numeric user id or account-name
2384
	 * @return boolean true on success, false otherwise
2385
	 */
2386
	static function chown($path,$owner)
2387
	{
2388
		return $path[0] == '/' && chown(self::PREFIX.$path, is_numeric($owner) ? abs($owner) : $owner);
2389
	}
2390
2391
	/**
2392
	 * chgrp just running on VFS path
2393
	 *
2394
	 * Requires owner or root rights!
2395
	 *
2396
	 * @param string $path
2397
	 * @param int|string $group numeric group id or group-name
2398
	 * @return boolean true on success, false otherwise
2399
	 */
2400
	static function chgrp($path,$group)
2401
	{
2402
		return $path[0] == '/' && chgrp(self::PREFIX.$path, is_numeric($group) ? abs($group) : $group);
2403
	}
2404
2405
	/**
2406
	 * Returns the target of a symbolic link
2407
	 *
2408
	 * This is not (yet) a stream-wrapper function, but it's necessary and can be used static
2409
	 *
2410
	 * @param string $path
2411
	 * @return string|boolean link-data or false if no link
2412
	 */
2413
	static function readlink($path)
2414
	{
2415
		$ret = self::_call_on_backend('readlink',array($path),true);	// true = fail silent, if backend does not support readlink
2416
		//error_log(__METHOD__."('$path') returning ".array2string($ret).' '.function_backtrace());
2417
		return $ret;
2418
	}
2419
2420
	/**
2421
	 * Creates a symbolic link
2422
	 *
2423
	 * This is not (yet) a stream-wrapper function, but it's necessary and can be used static
2424
	 *
2425
	 * @param string $target target of the link
2426
	 * @param string $link path of the link to create
2427
	 * @return boolean true on success, false on error
2428
	 */
2429
	static function symlink($target,$link)
2430
	{
2431
		if (($ret = self::_call_on_backend('symlink',array($target,$link),false,1)))	// 1=path is in $link!
2432
		{
2433
			Vfs\StreamWrapper::symlinkCache_remove($link);
2434
		}
2435
		return $ret;
2436
	}
2437
2438
	/**
2439
	 * This is not (yet) a stream-wrapper function, but it's necessary and can be used static
2440
	 *
2441
	 * The methods use the following ways to get the mime type (in that order)
2442
	 * - directories (is_dir()) --> self::DIR_MIME_TYPE
2443
	 * - stream implemented by class defining the STAT_RETURN_MIME_TYPE constant --> use mime-type returned by url_stat
2444
	 * - for regular filesystem use mime_content_type function if available
2445
	 * - use eGW's mime-magic class
2446
	 *
2447
	 * @param string $path
2448
	 * @param boolean $recheck =false true = do a new check, false = rely on stored mime type (if existing)
2449
	 * @return string mime-type (self::DIR_MIME_TYPE for directories)
2450
	 */
2451
	static function mime_content_type($path,$recheck=false)
2452
	{
2453
		if (!($url = self::resolve_url_symlinks($path)))
2454
		{
2455
			return false;
2456
		}
2457
		if (($scheme = self::parse_url($url,PHP_URL_SCHEME)) && !$recheck)
2458
		{
2459
			// check it it's an eGW stream wrapper returning mime-type via url_stat
2460
			// we need to first check if the constant is defined, as we get a fatal error in php5.3 otherwise
2461
			if (class_exists($class = self::scheme2class($scheme)) &&
2462
				defined($class.'::STAT_RETURN_MIME_TYPE') &&
2463
				($mime_attr = constant($class.'::STAT_RETURN_MIME_TYPE')))
2464
			{
2465
				$inst = new $class;
2466
				$stat = $inst->url_stat(self::parse_url($url,PHP_URL_PATH),0);
2467
				if ($stat && $stat[$mime_attr])
2468
				{
2469
					$mime = $stat[$mime_attr];
2470
				}
2471
			}
2472
		}
2473
		if (!$mime && is_dir($url))
2474
		{
2475
			$mime = self::DIR_MIME_TYPE;
2476
		}
2477
		// if we operate on the regular filesystem and the mime_content_type function is available --> use it
2478
		if (!$mime && !$scheme && function_exists('mime_content_type'))
2479
		{
2480
			$mime = mime_content_type($path);
2481
		}
2482
		// using EGw's own mime magic (currently only checking the extension!)
2483
		if (!$mime)
2484
		{
2485
			$mime = MimeMagic::filename2mime(self::parse_url($url,PHP_URL_PATH));
2486
		}
2487
		//error_log(__METHOD__."($path,$recheck) mime=$mime");
2488
		return $mime;
2489
	}
2490
2491
	/**
2492
	 * Get the class-name for a scheme
2493
	 *
2494
	 * 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
2495
	 * --> we replace dots in scheme with underscored to get the class-name
2496
	 *
2497
	 * @param string $scheme eg. vfs
2498
	 * @return string
2499
	 */
2500
	static function scheme2class($scheme)
2501
	{
2502
		return Vfs\StreamWrapper::scheme2class($scheme);
2503
	}
2504
2505
	/**
2506
	 * Clears our internal stat and symlink cache
2507
	 *
2508
	 * Normaly not necessary, as it is automatically cleared/updated, UNLESS Vfs::$user changes!
2509
	 *
2510
	 * We have to clear the symlink cache before AND after calling the backend,
2511
	 * because auf traversal rights may be different when Vfs::$user changes!
2512
	 *
2513
	 * @param string $path ='/' path of backend, whos cache to clear
2514
	 */
2515
	static function clearstatcache($path='/')
2516
	{
2517
		//error_log(__METHOD__."('$path')");
2518
		Vfs\StreamWrapper::clearstatcache($path);
0 ignored issues
show
Unused Code introduced by
The call to EGroupware\Api\Vfs\StreamWrapper::clearstatcache() has too many arguments starting with $path. ( Ignorable by Annotation )

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

2518
		Vfs\StreamWrapper::/** @scrutinizer ignore-call */ 
2519
                     clearstatcache($path);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
2519
		self::_call_on_backend('clearstatcache', array($path), true, 0);
2520
		Vfs\StreamWrapper::clearstatcache($path);
2521
	}
2522
2523
	/**
2524
	 * This method is called in response to rename() calls on URL paths associated with the wrapper.
2525
	 *
2526
	 * It should attempt to rename the item specified by path_from to the specification given by path_to.
2527
	 * In order for the appropriate error message to be returned, do not define this method if your wrapper does not support renaming.
2528
	 *
2529
	 * The regular filesystem stream-wrapper returns an error, if $url_from and $url_to are not either both files or both dirs!
2530
	 *
2531
	 * @param string $path_from
2532
	 * @param string $path_to
2533
	 * @return boolean TRUE on success or FALSE on failure
2534
	 */
2535
	static function rename ( $path_from, $path_to )
2536
	{
2537
		$vfs = new Vfs\StreamWrapper();
2538
		return $vfs->rename($path_from, $path_to);
2539
	}
2540
2541
	/**
2542
	 * Load stream wrapper for a given schema
2543
	 *
2544
	 * @param string $scheme
2545
	 * @return boolean
2546
	 */
2547
	static function load_wrapper($scheme)
2548
	{
2549
		return Vfs\StreamWrapper::load_wrapper($scheme);
2550
	}
2551
2552
	/**
2553
	 * Return stream with given string as content
2554
	 *
2555
	 * @param string $string
2556
	 * @return boolean|resource stream or false on error
2557
	 */
2558
	static function string_stream($string)
2559
	{
2560
		if (!($fp = fopen('php://temp', 'rw')))
2561
		{
2562
			return false;
2563
		}
2564
		$pos = 0;
2565
		$len = strlen($string);
2566
		do {
2567
			if (!($written = fwrite($fp, substr($string, $pos))))
2568
			{
2569
				return false;
2570
			}
2571
			$pos += $written;
2572
		}
2573
		while ($len < $pos);
2574
2575
		rewind($fp);
2576
2577
		return $fp;
2578
	}
2579
2580
	/**
2581
	 * Get the lowest fs_id for a given path
2582
	 *
2583
	 * @param string $path
2584
	 *
2585
	 * @return integer|boolean Lowest fs_id for that path, or false
2586
	 */
2587
	static function get_minimum_file_id($path)
2588
	{
2589
		if(!self::file_exists($path))
2590
		{
2591
			return false;
2592
		}
2593
		return self::_call_on_backend('get_minimum_file_id', array($path));
2594
	}
2595
}
2596
2597
Vfs::init_static();
2598