| Total Complexity | 544 |
| Total Lines | 2525 |
| Duplicated Lines | 0 % |
| Changes | 0 | ||
Complex classes like Vfs often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use Vfs, and based on these observations, apply Extract Interface, too.
| 1 | <?php |
||
| 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); |
||
|
|
|||
| 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); |
||
| 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 |
||
| 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 |
||
| 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 |
||
| 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) |
||
| 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 |
||
| 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']) || |
||
| 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'] || |
||
| 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']) || |
||
| 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) |
||
| 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) |
||
| 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)) |
||
| 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) |
||
| 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] === '') |
||
| 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) |
||
| 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()); |
||
| 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) |
||
| 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) |
||
| 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) |
||
| 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); |
||
| 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) |
||
| 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 |