Vfs::lock()   D
last analyzed

Complexity

Conditions 23
Paths 57

Size

Total Lines 73
Code Lines 46

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 23
eloc 46
nc 57
nop 8
dl 0
loc 73
rs 4.1666
c 0
b 0
f 0

How to fix   Long Method    Complexity    Many Parameters   

Long Method

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

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

Commonly applied refactorings include:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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