1<?php 2/** 3 * EGroupware API: VFS - new DB based VFS stream wrapper 4 * 5 * @link http://www.egroupware.org 6 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License 7 * @package api 8 * @subpackage vfs 9 * @author Ralf Becker <RalfBecker-AT-outdoor-training.de> 10 * @copyright (c) 2008-16 by Ralf Becker <RalfBecker-AT-outdoor-training.de> 11 * @version $Id$ 12 */ 13 14namespace EGroupware\Api\Vfs\Sqlfs; 15 16use EGroupware\Api\Vfs; 17use EGroupware\Api; 18 19/** 20 * EGroupware API: VFS - new DB based VFS stream wrapper 21 * 22 * The sqlfs stream wrapper has 2 operation modi: 23 * - content of files is stored in the filesystem (eGW's files_dir) (default) 24 * - content of files is stored as BLOB in the DB (can be enabled by mounting sqlfs://...?storage=db) 25 * please note the current (php5.2.6) problems: 26 * a) retriving files via streams does NOT work for PDO_mysql (bindColum(,,\PDO::PARAM_LOB) does NOT work, string returned) 27 * (there's a workaround implemented, but it requires to allocate memory for the whole file!) 28 * b) uploading/writing files > 1M fail on PDOStatement::execute() (setting \PDO::MYSQL_ATTR_MAX_BUFFER_SIZE does NOT help) 29 * (not sure if that's a bug in PDO/PDO_mysql or an accepted limitation) 30 * 31 * I use the PDO DB interface, as it allows to access BLOB's as streams (avoiding to hold them complete in memory). 32 * 33 * The stream wrapper interface is according to the docu on php.net 34 * 35 * @link http://www.php.net/manual/en/function.stream-wrapper-register.php 36 */ 37class StreamWrapper extends Api\Db\Pdo implements Vfs\StreamWrapperIface 38{ 39 /** 40 * Mime type of directories, the old vfs uses 'Directory', while eg. WebDAV uses 'httpd/unix-directory' 41 */ 42 const DIR_MIME_TYPE = 'httpd/unix-directory'; 43 /** 44 * Mime type for symlinks 45 */ 46 const SYMLINK_MIME_TYPE = 'application/x-symlink'; 47 /** 48 * Scheme / protocoll used for this stream-wrapper 49 */ 50 const SCHEME = 'sqlfs'; 51 /** 52 * Does url_stat returns a mime type, or has it to be determined otherwise (string with attribute name) 53 */ 54 const STAT_RETURN_MIME_TYPE = 'mime'; 55 /** 56 * Our tablename 57 */ 58 const TABLE = 'egw_sqlfs'; 59 /** 60 * Name of our property table 61 */ 62 const PROPS_TABLE = 'egw_sqlfs_props'; 63 /** 64 * mode-bits, which have to be set for files 65 */ 66 const MODE_FILE = 0100000; 67 /** 68 * mode-bits, which have to be set for directories 69 */ 70 const MODE_DIR = 040000; 71 /** 72 * mode-bits, which have to be set for links 73 */ 74 const MODE_LINK = 0120000; 75 /** 76 * How much should be logged to the apache error-log 77 * 78 * 0 = Nothing 79 * 1 = only errors 80 * 2 = all function calls and errors (contains passwords too!) 81 * 3 = log line numbers in sql statements 82 */ 83 const LOG_LEVEL = 1; 84 85 /** 86 * We store the content in the DB (no versioning) 87 */ 88 const STORE2DB = 1; 89 /** 90 * We store the content in the filesystem (egw_info/server/files_dir) (no versioning) 91 */ 92 const STORE2FS = 2; 93 /** 94 * default for operation, change that if you want to test with STORE2DB atm 95 */ 96 const DEFAULT_OPERATION = self::STORE2FS; 97 98 /** 99 * operation mode of the opened file 100 * 101 * @var int 102 */ 103 protected $operation = self::DEFAULT_OPERATION; 104 105 /** 106 * optional context param when opening the stream, null if no context passed 107 * 108 * @var mixed 109 */ 110 var $context; 111 112 /** 113 * Path off the file opened by stream_open 114 * 115 * @var string 116 */ 117 protected $opened_path; 118 /** 119 * Mode of the file opened by stream_open 120 * 121 * @var int 122 */ 123 protected $opened_mode; 124 /** 125 * Stream of the opened file, either from the DB via PDO or the filesystem 126 * 127 * @var resource 128 */ 129 protected $opened_stream; 130 /** 131 * fs_id of opened file 132 * 133 * @var int 134 */ 135 protected $opened_fs_id; 136 /** 137 * Cache containing stat-infos from previous url_stat calls AND dir_opendir calls 138 * 139 * It's values are the columns read from the DB (fs_*), not the ones returned by url_stat! 140 * 141 * @var array $path => info-array pairs 142 */ 143 static protected $stat_cache = array(); 144 /** 145 * Array with filenames of dir opened with dir_opendir 146 * 147 * @var array 148 */ 149 protected $opened_dir; 150 151 /** 152 * Extra columns added since the intitial introduction of sqlfs 153 * 154 * Can be set to empty, so get queries running on old versions of sqlfs, eg. for schema updates 155 * 156 * @var string; 157 */ 158 static public $extra_columns = ',fs_link'; 159 160 /** 161 * @var array $overwrite_new =null if set create new file with values overwriten by the given ones 162 */ 163 protected $overwrite_new; 164 165 /** 166 * Clears our stat-cache 167 * 168 * Normaly not necessary, as it is automatically cleared/updated, UNLESS Vfs::$user changes! 169 * 170 * @param string $path ='/' 171 */ 172 public static function clearstatcache($path='/') 173 { 174 //error_log(__METHOD__."('$path')"); 175 unset($path); // not used 176 177 self::$stat_cache = array(); 178 179 Api\Cache::setSession(self::EACL_APPNAME, 'extended_acl', self::$extended_acl = null); 180 } 181 182 /** 183 * This method is called immediately after your stream object is created. 184 * 185 * @param string $url URL that was passed to fopen() and that this object is expected to retrieve 186 * @param string $mode mode used to open the file, as detailed for fopen() 187 * @param int $options additional flags set by the streams API (or'ed together): 188 * - STREAM_USE_PATH If path is relative, search for the resource using the include_path. 189 * - STREAM_REPORT_ERRORS If this flag is set, you are responsible for raising errors using trigger_error() during opening of the stream. 190 * If this flag is not set, you should not raise any errors. 191 * @param string &$opened_path full path of the file/resource, if the open was successfull and STREAM_USE_PATH was set 192 * @return boolean true if the ressource was opened successful, otherwise false 193 */ 194 function stream_open ($url, $mode, $options, &$opened_path) 195 { 196 if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url,$mode,$options)"); 197 198 $path = Vfs::parse_url($url,PHP_URL_PATH); 199 $this->operation = self::url2operation($url); 200 $dir = Vfs::dirname($url); 201 202 $this->opened_path = $opened_path = $path; 203 $this->opened_mode = $mode = str_replace('b','',$mode); // we are always binary, like every Linux system 204 $this->opened_stream = null; 205 206 parse_str(parse_url($url, PHP_URL_QUERY), $this->dir_url_params); 207 208 if (!is_null($this->overwrite_new) || !($stat = $this->url_stat($path,STREAM_URL_STAT_QUIET)) || $mode[0] == 'x') // file not found or file should NOT exist 209 { 210 if (!$dir || $mode[0] == 'r' || // does $mode require the file to exist (r,r+) 211 $mode[0] == 'x' && $stat || // or file should not exist, but does 212 !($dir_stat=$this->url_stat($dir,STREAM_URL_STAT_QUIET)) || // or parent dir does not exist create it 213 !Vfs::check_access($dir,Vfs::WRITABLE,$dir_stat)) // or we are not allowed to create it 214 { 215 self::_remove_password($url); 216 if (self::LOG_LEVEL) error_log(__METHOD__."($url,$mode,$options) file does not exist or can not be created!"); 217 if (($options & STREAM_REPORT_ERRORS)) 218 { 219 trigger_error(__METHOD__."($url,$mode,$options) file does not exist or can not be created!",E_USER_WARNING); 220 } 221 $this->opened_stream = $this->opened_path = $this->opened_mode = null; 222 return false; 223 } 224 // new file --> create it in the DB 225 $new_file = true; 226 $query = 'INSERT INTO '.self::TABLE.' (fs_name,fs_dir,fs_mode,fs_uid,fs_gid,fs_created,fs_modified,fs_creator,fs_mime,fs_size,fs_active'. 227 ') VALUES (:fs_name,:fs_dir,:fs_mode,:fs_uid,:fs_gid,:fs_created,:fs_modified,:fs_creator,:fs_mime,:fs_size,:fs_active)'; 228 if (self::LOG_LEVEL > 2) $query = '/* '.__METHOD__.': '.__LINE__.' */ '.$query; 229 $stmt = self::$pdo->prepare($query); 230 $values = array( 231 'fs_name' => self::limit_filename(Vfs::basename($path)), 232 'fs_dir' => $dir_stat['ino'], 233 // we use the mode of the dir, so files in group dirs stay accessible by all members 234 'fs_mode' => $dir_stat['mode'] & 0666, 235 // for the uid we use the uid of the dir if not 0=root or the current user otherwise 236 'fs_uid' => $dir_stat['uid'] ? $dir_stat['uid'] : Vfs::$user, 237 // we allways use the group of the dir 238 'fs_gid' => $dir_stat['gid'], 239 'fs_created' => self::_pdo_timestamp(time()), 240 'fs_modified' => self::_pdo_timestamp(time()), 241 'fs_creator' => Vfs::$user, 242 'fs_mime' => 'application/octet-stream', // required NOT NULL! 243 'fs_size' => 0, 244 'fs_active' => self::_pdo_boolean(true), 245 ); 246 if ($this->overwrite_new) $values = array_merge($values, $this->overwrite_new); 247 if (!$stmt->execute($values) || !($this->opened_fs_id = self::$pdo->lastInsertId('egw_sqlfs_fs_id_seq'))) 248 { 249 $this->opened_stream = $this->opened_path = $this->opened_mode = null; 250 if (self::LOG_LEVEL) error_log(__METHOD__."($url,$mode,$options) execute() failed: ".self::$pdo->errorInfo()); 251 return false; 252 } 253 if ($this->operation == self::STORE2DB) 254 { 255 // we buffer all write operations in a temporary file, which get's written on close 256 $this->opened_stream = tmpfile(); 257 } 258 // create the hash-dirs, if they not yet exist 259 elseif(!file_exists($fs_dir=Vfs::dirname(self::_fs_path($this->opened_fs_id)))) 260 { 261 $umaskbefore = umask(); 262 if (self::LOG_LEVEL > 1) error_log(__METHOD__." about to call mkdir for $fs_dir # Present UMASK:".decoct($umaskbefore)." called from:".function_backtrace()); 263 self::mkdir_recursive($fs_dir,0700,true); 264 } 265 } 266 // check if opend file is a directory 267 elseif($stat && ($stat['mode'] & self::MODE_DIR) == self::MODE_DIR) 268 { 269 if (self::LOG_LEVEL) error_log(__METHOD__."($url,$mode,$options) Is a directory!"); 270 if (($options & STREAM_REPORT_ERRORS)) 271 { 272 trigger_error(__METHOD__."($url,$mode,$options) Is a directory!",E_USER_WARNING); 273 } 274 $this->opened_stream = $this->opened_path = $this->opened_mode = null; 275 return false; 276 } 277 else 278 { 279 if ($mode == 'r' && !Vfs::check_access($url,Vfs::READABLE ,$stat) ||// we are not allowed to read 280 $mode != 'r' && !Vfs::check_access($url,Vfs::WRITABLE,$stat)) // or edit it 281 { 282 self::_remove_password($url); 283 $op = $mode == 'r' ? 'read' : 'edited'; 284 if (self::LOG_LEVEL) error_log(__METHOD__."($url,$mode,$options) file can not be $op!"); 285 if (($options & STREAM_REPORT_ERRORS)) 286 { 287 trigger_error(__METHOD__."($url,$mode,$options) file can not be $op!",E_USER_WARNING); 288 } 289 $this->opened_stream = $this->opened_path = $this->opened_mode = null; 290 return false; 291 } 292 $this->opened_fs_id = $stat['ino']; 293 294 if ($this->operation == self::STORE2DB) 295 { 296 $stmt = self::$pdo->prepare($sql='SELECT fs_content FROM '.self::TABLE.' WHERE fs_id=?'); 297 $stmt->execute(array($stat['ino'])); 298 $stmt->bindColumn(1,$this->opened_stream,\PDO::PARAM_LOB); 299 $stmt->fetch(\PDO::FETCH_BOUND); 300 // hack to work around a current php bug (http://bugs.php.net/bug.php?id=40913) 301 // PDOStatement::bindColumn(,,\PDO::PARAM_LOB) is not working for MySQL, content is returned as string :-( 302 if (is_string($this->opened_stream)) 303 { 304 $tmp = fopen('php://temp', 'wb'); 305 fwrite($tmp, $this->opened_stream); 306 fseek($tmp, 0, SEEK_SET); 307 unset($this->opened_stream); 308 $this->opened_stream = $tmp; 309 } 310 //echo 'gettype($this->opened_stream)='; var_dump($this->opened_stream); 311 } 312 } 313 // do we operate directly on the filesystem --> open file from there 314 if ($this->operation == self::STORE2FS) 315 { 316 if (self::LOG_LEVEL > 1) error_log(__METHOD__." fopen (may create a directory? mkdir) ($this->opened_fs_id,$mode,$options)"); 317 if (!($this->opened_stream = fopen(self::_fs_path($this->opened_fs_id),$mode)) && $new_file) 318 { 319 // delete db entry again, if we are not able to open a new(!) file 320 unset($stmt); 321 $stmt = self::$pdo->prepare('DELETE FROM '.self::TABLE.' WHERE fs_id=:fs_id'); 322 $stmt->execute(array('fs_id' => $this->opened_fs_id)); 323 } 324 } 325 if ($mode[0] == 'a') // append modes: a, a+ 326 { 327 $this->stream_seek(0,SEEK_END); 328 } 329 if (!is_resource($this->opened_stream)) error_log(__METHOD__."($url,$mode,$options) NO stream, returning false!"); 330 331 return is_resource($this->opened_stream); 332 } 333 334 /** 335 * This method is called when the stream is closed, using fclose(). 336 * 337 * You must release any resources that were locked or allocated by the stream. 338 */ 339 function stream_close ( ) 340 { 341 if (self::LOG_LEVEL > 1) error_log(__METHOD__."()"); 342 343 if (is_null($this->opened_path) || !is_resource($this->opened_stream) || !$this->opened_fs_id) 344 { 345 return false; 346 } 347 348 if ($this->opened_mode != 'r') 349 { 350 $this->stream_seek(0,SEEK_END); 351 352 // we need to update the mime-type, size and content (if STORE2DB) 353 $values = array( 354 'fs_size' => $this->stream_tell(), 355 // todo: analyse the file for the mime-type 356 'fs_mime' => Api\MimeMagic::filename2mime($this->opened_path), 357 'fs_id' => $this->opened_fs_id, 358 'fs_modifier' => Vfs::$user, 359 'fs_modified' => self::_pdo_timestamp(time()), 360 ); 361 362 if ($this->operation == self::STORE2FS) 363 { 364 $stmt = self::$pdo->prepare('UPDATE '.self::TABLE.' SET fs_size=:fs_size,fs_mime=:fs_mime,fs_modifier=:fs_modifier,fs_modified=:fs_modified WHERE fs_id=:fs_id'); 365 if (!($ret = $stmt->execute($values))) 366 { 367 error_log(__METHOD__."() execute() failed! errorInfo()=".array2string(self::$pdo->errorInfo())); 368 } 369 } 370 else 371 { 372 $stmt = self::$pdo->prepare('UPDATE '.self::TABLE.' SET fs_size=:fs_size,fs_mime=:fs_mime,fs_modifier=:fs_modifier,fs_modified=:fs_modified,fs_content=:fs_content WHERE fs_id=:fs_id'); 373 $this->stream_seek(0,SEEK_SET); // rewind to the start 374 foreach($values as $name => &$value) 375 { 376 $stmt->bindParam($name,$value); 377 } 378 $stmt->bindParam('fs_content', $this->opened_stream, \PDO::PARAM_LOB); 379 if (!($ret = $stmt->execute())) 380 { 381 error_log(__METHOD__."() execute() failed! errorInfo()=".array2string(self::$pdo->errorInfo())); 382 } 383 } 384 } 385 else 386 { 387 $ret = true; 388 } 389 $ret = fclose($this->opened_stream) && $ret; 390 391 unset(self::$stat_cache[$this->opened_path]); 392 $this->opened_stream = $this->opened_path = $this->opened_mode = $this->opened_fs_id = null; 393 $this->operation = self::DEFAULT_OPERATION; 394 395 return $ret; 396 } 397 398 /** 399 * This method is called in response to fread() and fgets() calls on the stream. 400 * 401 * You must return up-to count bytes of data from the current read/write position as a string. 402 * If there are less than count bytes available, return as many as are available. 403 * If no more data is available, return either FALSE or an empty string. 404 * You must also update the read/write position of the stream by the number of bytes that were successfully read. 405 * 406 * @param int $count 407 * @return string/false up to count bytes read or false on EOF 408 */ 409 function stream_read ( $count ) 410 { 411 if (self::LOG_LEVEL > 1) error_log(__METHOD__."($count) pos=$this->opened_pos"); 412 413 if (is_resource($this->opened_stream)) 414 { 415 return fread($this->opened_stream,$count); 416 } 417 return false; 418 } 419 420 /** 421 * This method is called in response to fwrite() calls on the stream. 422 * 423 * You should store data into the underlying storage used by your stream. 424 * If there is not enough room, try to store as many bytes as possible. 425 * You should return the number of bytes that were successfully stored in the stream, or 0 if none could be stored. 426 * You must also update the read/write position of the stream by the number of bytes that were successfully written. 427 * 428 * @param string $data 429 * @return integer 430 */ 431 function stream_write ( $data ) 432 { 433 if (self::LOG_LEVEL > 1) error_log(__METHOD__."($data)"); 434 435 if (is_resource($this->opened_stream)) 436 { 437 return fwrite($this->opened_stream,$data); 438 } 439 return false; 440 } 441 442 /** 443 * This method is called in response to feof() calls on the stream. 444 * 445 * Important: PHP 5.0 introduced a bug that wasn't fixed until 5.1: the return value has to be the oposite! 446 * 447 * if(version_compare(PHP_VERSION,'5.0','>=') && version_compare(PHP_VERSION,'5.1','<')) 448 * { 449 * $eof = !$eof; 450 * } 451 * 452 * @return boolean true if the read/write position is at the end of the stream and no more data availible, false otherwise 453 */ 454 function stream_eof ( ) 455 { 456 if (is_resource($this->opened_stream)) 457 { 458 return feof($this->opened_stream); 459 } 460 return false; 461 } 462 463 /** 464 * This method is called in response to ftell() calls on the stream. 465 * 466 * @return integer current read/write position of the stream 467 */ 468 function stream_tell ( ) 469 { 470 if (self::LOG_LEVEL > 1) error_log(__METHOD__."()"); 471 472 if (is_resource($this->opened_stream)) 473 { 474 return ftell($this->opened_stream); 475 } 476 return false; 477 } 478 479 /** 480 * This method is called in response to fseek() calls on the stream. 481 * 482 * You should update the read/write position of the stream according to offset and whence. 483 * See fseek() for more information about these parameters. 484 * 485 * @param integer $offset 486 * @param integer $whence SEEK_SET - 0 - Set position equal to offset bytes 487 * SEEK_CUR - 1 - Set position to current location plus offset. 488 * SEEK_END - 2 - Set position to end-of-file plus offset. (To move to a position before the end-of-file, you need to pass a negative value in offset.) 489 * @return boolean TRUE if the position was updated, FALSE otherwise. 490 */ 491 function stream_seek ( $offset, $whence ) 492 { 493 if (self::LOG_LEVEL > 1) error_log(__METHOD__."($offset,$whence)"); 494 495 if (is_resource($this->opened_stream)) 496 { 497 return !fseek($this->opened_stream,$offset,$whence); // fseek returns 0 on success and -1 on failure 498 } 499 return false; 500 } 501 502 /** 503 * This method is called in response to fflush() calls on the stream. 504 * 505 * If you have cached data in your stream but not yet stored it into the underlying storage, you should do so now. 506 * 507 * @return booelan TRUE if the cached data was successfully stored (or if there was no data to store), or FALSE if the data could not be stored. 508 */ 509 function stream_flush ( ) 510 { 511 if (self::LOG_LEVEL > 1) error_log(__METHOD__."()"); 512 513 if (is_resource($this->opened_stream)) 514 { 515 return fflush($this->opened_stream); 516 } 517 return false; 518 } 519 520 /** 521 * This method is called in response to fstat() calls on the stream. 522 * 523 * If you plan to use your wrapper in a require_once you need to define stream_stat(). 524 * If you plan to allow any other tests like is_file()/is_dir(), you have to define url_stat(). 525 * stream_stat() must define the size of the file, or it will never be included. 526 * url_stat() must define mode, or is_file()/is_dir()/is_executable(), and any of those functions affected by clearstatcache() simply won't work. 527 * It's not documented, but directories must be a mode like 040777 (octal), and files a mode like 0100666. 528 * If you wish the file to be executable, use 7s instead of 6s. 529 * The last 3 digits are exactly the same thing as what you pass to chmod. 530 * 040000 defines a directory, and 0100000 defines a file. 531 * 532 * @return array containing the same values as appropriate for the stream. 533 */ 534 function stream_stat ( ) 535 { 536 if (self::LOG_LEVEL > 1) error_log(__METHOD__."($this->opened_path)"); 537 538 return $this->url_stat($this->opened_path,0); 539 } 540 541 /** 542 * This method is called in response to unlink() calls on URL paths associated with the wrapper. 543 * 544 * It should attempt to delete the item specified by path. 545 * In order for the appropriate error message to be returned, do not define this method if your wrapper does not support unlinking! 546 * 547 * @param string $url 548 * @return boolean TRUE on success or FALSE on failure 549 */ 550 function unlink ( $url ) 551 { 552 if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url)"); 553 554 $path = Vfs::parse_url($url,PHP_URL_PATH); 555 556 // need to get parent stat from Sqlfs, not Vfs 557 $parent_stat = !($dir = Vfs::dirname($path)) ? false : 558 $this->url_stat($dir, STREAM_URL_STAT_LINK); 559 560 if (!$parent_stat || !($stat = $this->url_stat($path,STREAM_URL_STAT_LINK)) || 561 !$dir || !Vfs::check_access($dir, Vfs::WRITABLE, $parent_stat)) 562 { 563 self::_remove_password($url); 564 if (self::LOG_LEVEL) error_log(__METHOD__."($url) permission denied!"); 565 return false; // no permission or file does not exist 566 } 567 if ($stat['mime'] == self::DIR_MIME_TYPE) 568 { 569 self::_remove_password($url); 570 if (self::LOG_LEVEL) error_log(__METHOD__."($url) is NO file!"); 571 return false; // no permission or file does not exist 572 } 573 $stmt = self::$pdo->prepare('DELETE FROM '.self::TABLE.' WHERE fs_id=:fs_id'); 574 unset(self::$stat_cache[$path]); 575 576 if (($ret = $stmt->execute(array('fs_id' => $stat['ino'])))) 577 { 578 if (self::url2operation($url) == self::STORE2FS && 579 ($stat['mode'] & self::MODE_LINK) != self::MODE_LINK) 580 { 581 unlink(self::_fs_path($stat['ino'])); 582 } 583 // delete props 584 unset($stmt); 585 $stmt = self::$pdo->prepare('DELETE FROM '.self::PROPS_TABLE.' WHERE fs_id=?'); 586 $stmt->execute(array($stat['ino'])); 587 } 588 return $ret; 589 } 590 591 /** 592 * This method is called in response to rename() calls on URL paths associated with the wrapper. 593 * 594 * It should attempt to rename the item specified by path_from to the specification given by path_to. 595 * In order for the appropriate error message to be returned, do not define this method if your wrapper does not support renaming. 596 * 597 * The regular filesystem stream-wrapper returns an error, if $url_from and $url_to are not either both files or both dirs! 598 * 599 * @param string $url_from 600 * @param string $url_to 601 * @return boolean TRUE on success or FALSE on failure 602 */ 603 function rename ( $url_from, $url_to) 604 { 605 if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url_from,$url_to)"); 606 607 $path_from = Vfs::parse_url($url_from,PHP_URL_PATH); 608 $from_dir = Vfs::dirname($path_from); 609 $path_to = Vfs::parse_url($url_to,PHP_URL_PATH); 610 $to_dir = Vfs::dirname($path_to); 611 612 if (!($from_stat = $this->url_stat($path_from, 0)) || !$from_dir || 613 !Vfs::check_access($from_dir, Vfs::WRITABLE, $from_dir_stat = $this->url_stat($from_dir, 0))) 614 { 615 self::_remove_password($url_from); 616 self::_remove_password($url_to); 617 if (self::LOG_LEVEL) error_log(__METHOD__."($url_from,$url_to): $path_from permission denied!"); 618 return false; // no permission or file does not exist 619 } 620 if (!$to_dir || !Vfs::check_access($to_dir, Vfs::WRITABLE, $to_dir_stat = $this->url_stat($to_dir, 0))) 621 { 622 self::_remove_password($url_from); 623 self::_remove_password($url_to); 624 if (self::LOG_LEVEL) error_log(__METHOD__."($url_from,$url_to): $path_to permission denied!"); 625 return false; // no permission or parent-dir does not exist 626 } 627 // the filesystem stream-wrapper does NOT allow to rename files to directories, as this makes problems 628 // for our vfs too, we abort here with an error, like the filesystem one does 629 if (($to_stat = $this->url_stat($path_to, 0)) && 630 ($to_stat['mime'] === self::DIR_MIME_TYPE) !== ($from_stat['mime'] === self::DIR_MIME_TYPE)) 631 { 632 self::_remove_password($url_from); 633 self::_remove_password($url_to); 634 $is_dir = $to_stat['mime'] === self::DIR_MIME_TYPE ? 'a' : 'no'; 635 if (self::LOG_LEVEL) error_log(__METHOD__."($url_to,$url_from) $path_to is $is_dir directory!"); 636 return false; // no permission or file does not exist 637 } 638 // if destination file already exists, delete it 639 if ($to_stat && !$this->unlink($url_to)) 640 { 641 self::_remove_password($url_to); 642 if (self::LOG_LEVEL) error_log(__METHOD__."($url_to,$url_from) can't unlink existing $url_to!"); 643 return false; 644 } 645 unset(self::$stat_cache[$path_from]); 646 unset(self::$stat_cache[$path_to]); 647 648 $stmt = self::$pdo->prepare('UPDATE '.self::TABLE.' SET fs_dir=:fs_dir,fs_name=:fs_name'. 649 ' WHERE fs_dir=:old_dir AND fs_name'.self::$case_sensitive_equal.':old_name'); 650 $ok = $stmt->execute(array( 651 'fs_dir' => $to_dir_stat['ino'], 652 'fs_name' => self::limit_filename(Vfs::basename($path_to)), 653 'old_dir' => $from_dir_stat['ino'], 654 'old_name' => $from_stat['name'], 655 )); 656 unset($stmt); 657 658 // check if extension changed and update mime-type in that case (as we currently determine mime-type by it's extension!) 659 // fixes eg. problems with MsWord storing file with .tmp extension and then renaming to .doc 660 if ($ok && ($new_mime = Vfs::mime_content_type($url_to,true)) != Vfs::mime_content_type($url_to)) 661 { 662 //echo "<p>Vfs::nime_content_type($url_to,true) = $new_mime</p>\n"; 663 $stmt = self::$pdo->prepare('UPDATE '.self::TABLE.' SET fs_mime=:fs_mime WHERE fs_id=:fs_id'); 664 $stmt->execute(array( 665 'fs_mime' => $new_mime, 666 'fs_id' => $from_stat['ino'], 667 )); 668 unset(self::$stat_cache[$path_to]); 669 } 670 return $ok; 671 } 672 673 /** 674 * due to problems with recursive directory creation, we have our own here 675 */ 676 protected static function mkdir_recursive($pathname, $mode, $depth=0) 677 { 678 $maxdepth=10; 679 $depth2propagate = (int)$depth + 1; 680 if ($depth2propagate > $maxdepth) return is_dir($pathname); 681 is_dir(Vfs::dirname($pathname)) || self::mkdir_recursive(Vfs::dirname($pathname), $mode, $depth2propagate); 682 return is_dir($pathname) || @mkdir($pathname, $mode); 683 } 684 685 /** 686 * This method is called in response to mkdir() calls on URL paths associated with the wrapper. 687 * 688 * It should attempt to create the directory specified by path. 689 * In order for the appropriate error message to be returned, do not define this method if your wrapper does not support creating directories. 690 * 691 * @param string $url 692 * @param int $mode 693 * @param int $options Posible values include STREAM_REPORT_ERRORS and STREAM_MKDIR_RECURSIVE 694 * @return boolean TRUE on success or FALSE on failure 695 */ 696 function mkdir ( $url, $mode, $options ) 697 { 698 if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url,$mode,$options)"); 699 if (self::LOG_LEVEL > 1) error_log(__METHOD__." called from:".function_backtrace()); 700 $path = Vfs::parse_url($url,PHP_URL_PATH); 701 702 if ($this->url_stat($path,STREAM_URL_STAT_QUIET)) 703 { 704 self::_remove_password($url); 705 if (self::LOG_LEVEL) error_log(__METHOD__."('$url',$mode,$options) already exist!"); 706 if (!($options & STREAM_REPORT_ERRORS)) 707 { 708 trigger_error(__METHOD__."('$url',$mode,$options) already exist!",E_USER_WARNING); 709 } 710 return false; 711 } 712 if (!($parent_path = Vfs::dirname($path))) 713 { 714 self::_remove_password($url); 715 if (self::LOG_LEVEL) error_log(__METHOD__."('$url',$mode,$options) dirname('$path')===false!"); 716 if (!($options & STREAM_REPORT_ERRORS)) 717 { 718 trigger_error(__METHOD__."('$url',$mode,$options) dirname('$path')===false!", E_USER_WARNING); 719 } 720 return false; 721 } 722 if (($query = Vfs::parse_url($url,PHP_URL_QUERY))) $parent_path .= '?'.$query; 723 $parent = $this->url_stat($parent_path,STREAM_URL_STAT_QUIET); 724 725 // check if we should also create all non-existing path components and our parent does not exist, 726 // if yes call ourself recursive with the parent directory 727 if (($options & STREAM_MKDIR_RECURSIVE) && $parent_path != '/' && !$parent) 728 { 729 if (self::LOG_LEVEL > 1) error_log(__METHOD__." creating parents: $parent_path, $mode"); 730 if (!$this->mkdir($parent_path,$mode,$options)) 731 { 732 return false; 733 } 734 $parent = $this->url_stat($parent_path,0); 735 } 736 if (!$parent || !Vfs::check_access($parent_path,Vfs::WRITABLE,$parent)) 737 { 738 self::_remove_password($url); 739 if (self::LOG_LEVEL) error_log(__METHOD__."('$url',$mode,$options) permission denied!"); 740 if (!($options & STREAM_REPORT_ERRORS)) 741 { 742 trigger_error(__METHOD__."('$url',$mode,$options) permission denied!",E_USER_WARNING); 743 } 744 return false; // no permission or file does not exist 745 } 746 unset(self::$stat_cache[$path]); 747 $stmt = self::$pdo->prepare('INSERT INTO '.self::TABLE.' (fs_name,fs_dir,fs_mode,fs_uid,fs_gid,fs_size,fs_mime,fs_created,fs_modified,fs_creator'. 748 ') VALUES (:fs_name,:fs_dir,:fs_mode,:fs_uid,:fs_gid,:fs_size,:fs_mime,:fs_created,:fs_modified,:fs_creator)'); 749 if (($ok = $stmt->execute(array( 750 'fs_name' => self::limit_filename(Vfs::basename($path)), 751 'fs_dir' => $parent['ino'], 752 'fs_mode' => $parent['mode'], 753 'fs_uid' => $parent['uid'], 754 'fs_gid' => $parent['gid'], 755 'fs_size' => 0, 756 'fs_mime' => self::DIR_MIME_TYPE, 757 'fs_created' => self::_pdo_timestamp(time()), 758 'fs_modified' => self::_pdo_timestamp(time()), 759 'fs_creator' => Vfs::$user, 760 )))) 761 { 762 // check if some other process created the directory parallel to us (sqlfs would gives SQL errors later!) 763 $new_fs_id = self::$pdo->lastInsertId('egw_sqlfs_fs_id_seq'); 764 765 unset($stmt); // free statement object, on some installs a new prepare fails otherwise! 766 767 $stmt = self::$pdo->prepare($q='SELECT COUNT(*) FROM '.self::TABLE. 768 ' WHERE fs_dir=:fs_dir AND fs_active=:fs_active AND fs_name'.self::$case_sensitive_equal.':fs_name'); 769 if ($stmt->execute(array( 770 'fs_dir' => $parent['ino'], 771 'fs_active' => self::_pdo_boolean(true), 772 'fs_name' => self::limit_filename(Vfs::basename($path)), 773 )) && $stmt->fetchColumn() > 1) // if there's more then one --> remove our new dir 774 { 775 self::$pdo->query('DELETE FROM '.self::TABLE.' WHERE fs_id='.$new_fs_id); 776 } 777 } 778 return $ok; 779 } 780 781 /** 782 * This method is called in response to rmdir() calls on URL paths associated with the wrapper. 783 * 784 * It should attempt to remove the directory specified by path. 785 * In order for the appropriate error message to be returned, do not define this method if your wrapper does not support removing directories. 786 * 787 * @param string $url 788 * @param int $options Possible values include STREAM_REPORT_ERRORS. 789 * @return boolean TRUE on success or FALSE on failure. 790 */ 791 function rmdir ( $url, $options ) 792 { 793 if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url)"); 794 795 $path = Vfs::parse_url($url,PHP_URL_PATH); 796 797 if (!($parent = Vfs::dirname($path)) || 798 !($stat = $this->url_stat($path, 0)) || $stat['mime'] != self::DIR_MIME_TYPE || 799 !Vfs::check_access($parent, Vfs::WRITABLE, $this->url_stat($parent,0))) 800 { 801 self::_remove_password($url); 802 $err_msg = __METHOD__."($url,$options) ".(!$stat ? 'not found!' : 803 ($stat['mime'] != self::DIR_MIME_TYPE ? 'not a directory!' : 'permission denied!')); 804 if (self::LOG_LEVEL) error_log($err_msg); 805 if (!($options & STREAM_REPORT_ERRORS)) 806 { 807 trigger_error($err_msg,E_USER_WARNING); 808 } 809 return false; // no permission or file does not exist 810 } 811 $stmt = self::$pdo->prepare('SELECT COUNT(*) FROM '.self::TABLE.' WHERE fs_dir=?'); 812 $stmt->execute(array($stat['ino'])); 813 if ($stmt->fetchColumn()) 814 { 815 self::_remove_password($url); 816 if (self::LOG_LEVEL) error_log(__METHOD__."($url,$options) dir is not empty!"); 817 if (!($options & STREAM_REPORT_ERRORS)) 818 { 819 trigger_error(__METHOD__."('$url',$options) dir is not empty!",E_USER_WARNING); 820 } 821 return false; 822 } 823 unset(self::$stat_cache[$path]); 824 unset($stmt); // free statement object, on some installs a new prepare fails otherwise! 825 826 $del_stmt = self::$pdo->prepare('DELETE FROM '.self::TABLE.' WHERE fs_id=?'); 827 if (($ret = $del_stmt->execute(array($stat['ino'])))) 828 { 829 self::eacl($path,null,false,$stat['ino']); // remove all (=false) evtl. existing extended acl for that dir 830 // delete props 831 unset($del_stmt); 832 $del_stmt = self::$pdo->prepare('DELETE FROM '.self::PROPS_TABLE.' WHERE fs_id=?'); 833 $del_stmt->execute(array($stat['ino'])); 834 } 835 return $ret; 836 } 837 838 /** 839 * StreamWrapper method (PHP 5.4+) for touch, chmod, chown and chgrp 840 * 841 * We use protected helper methods touch, chmod, chown and chgrp to implement the functionality. 842 * 843 * @param string $path 844 * @param int $option STREAM_META_(TOUCH|ACCESS|((OWNER|GROUP)(_NAME)?)) 845 * @param array|int|string $value 846 * - STREAM_META_TOUCH array($time, $atime) 847 * - STREAM_META_ACCESS int 848 * - STREAM_(OWNER|GROUP) int 849 * - STREAM_(OWNER|GROUP)_NAME string 850 * @return boolean true on success, false on failure 851 */ 852 function stream_metadata($path, $option, $value) 853 { 854 if (self::LOG_LEVEL > 1) error_log(__METHOD__."($path, $option, ".array2string($value).")"); 855 856 switch($option) 857 { 858 case STREAM_META_TOUCH: 859 return $this->touch($path, $value[0]); // atime is not supported 860 861 case STREAM_META_ACCESS: 862 return $this->chmod($path, $value); 863 864 case STREAM_META_OWNER_NAME: 865 if (($value = $GLOBALS['egw']->accounts->name2id($value, 'account_lid', 'u')) === false) 866 return false; 867 // fall through 868 case STREAM_META_OWNER: 869 return $this->chown($path, $value); 870 871 case STREAM_META_GROUP_NAME: 872 if (($value = $GLOBALS['egw']->accounts->name2id($value, 'account_lid', 'g')) === false) 873 return false; 874 // fall through 875 case STREAM_META_GROUP: 876 return $this->chgrp($path, $value); 877 } 878 return false; 879 } 880 881 /** 882 * This is not (yet) a stream-wrapper function, but it's necessary and can be used static 883 * 884 * @param string $url 885 * @param int $time =null modification time (unix timestamp), default null = current time 886 * @param int $atime =null access time (unix timestamp), default null = current time, not implemented in the vfs! 887 */ 888 protected function touch($url,$time=null,$atime=null) 889 { 890 unset($atime); // not used 891 if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url, $time)"); 892 893 $path = Vfs::parse_url($url,PHP_URL_PATH); 894 895 $vfs = new self(); 896 if (!($stat = $vfs->url_stat($path,STREAM_URL_STAT_QUIET))) 897 { 898 // file does not exist --> create an empty one 899 if (!($f = fopen(self::SCHEME.'://default'.$path,'w')) || !fclose($f)) 900 { 901 return false; 902 } 903 if (!$time) 904 { 905 return true; // new (empty) file created with current mod time 906 } 907 $stat = $vfs->url_stat($path,0); 908 } 909 unset(self::$stat_cache[$path]); 910 $stmt = self::$pdo->prepare('UPDATE '.self::TABLE.' SET fs_modified=:fs_modified,fs_modifier=:fs_modifier WHERE fs_id=:fs_id'); 911 912 return $stmt->execute(array( 913 'fs_modified' => self::_pdo_timestamp($time ? $time : time()), 914 'fs_modifier' => Vfs::$user, 915 'fs_id' => $stat['ino'], 916 )); 917 } 918 919 /** 920 * Chown command, not yet a stream-wrapper function, but necessary 921 * 922 * @param string $url 923 * @param int $owner 924 * @return boolean 925 */ 926 protected function chown($url,$owner) 927 { 928 if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url,$owner)"); 929 930 $path = Vfs::parse_url($url,PHP_URL_PATH); 931 932 $vfs = new self(); 933 if (!($stat = $vfs->url_stat($path,0))) 934 { 935 if (self::LOG_LEVEL) error_log(__METHOD__."($url,$owner) no such file or directory!"); 936 trigger_error("No such file or directory $url !",E_USER_WARNING); 937 return false; 938 } 939 if (!Vfs::$is_root) 940 { 941 if (self::LOG_LEVEL) error_log(__METHOD__."($url,$owner) only root can do that!"); 942 trigger_error("Only root can do that!",E_USER_WARNING); 943 return false; 944 } 945 if ($owner < 0 || $owner && !$GLOBALS['egw']->accounts->id2name($owner)) // not a user (0 == root) 946 { 947 if (self::LOG_LEVEL) error_log(__METHOD__."($url,$owner) unknown (numeric) user id!"); 948 trigger_error(__METHOD__."($url,$owner) Unknown (numeric) user id!",E_USER_WARNING); 949 //throw new Exception(__METHOD__."($url,$owner) Unknown (numeric) user id!"); 950 return false; 951 } 952 $stmt = self::$pdo->prepare('UPDATE '.self::TABLE.' SET fs_uid=:fs_uid WHERE fs_id=:fs_id'); 953 954 // update stat-cache 955 if ($path != '/' && substr($path,-1) == '/') $path = substr($path, 0, -1); 956 self::$stat_cache[$path]['fs_uid'] = $owner; 957 958 return $stmt->execute(array( 959 'fs_uid' => (int) $owner, 960 'fs_id' => $stat['ino'], 961 )); 962 } 963 964 /** 965 * chown but for all files a user owns 966 * 967 * @param $old_uid 968 * @param $new_uid 969 */ 970 public static function chownAll($old_uid, $new_uid) 971 { 972 $stmt = self::$pdo->prepare('UPDATE '.self::TABLE.' SET fs_uid=:fs_uid WHERE fs_uid=:old_uid'); 973 return $stmt->execute(array( 974 'fs_uid' => (int) $new_uid, 975 'old_uid' => $old_uid, 976 )); 977 } 978 979 /** 980 * Chgrp command, not yet a stream-wrapper function, but necessary 981 * 982 * @param string $url 983 * @param int $owner 984 * @return boolean 985 */ 986 protected function chgrp($url,$owner) 987 { 988 if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url,$owner)"); 989 990 $path = Vfs::parse_url($url,PHP_URL_PATH); 991 992 $vfs = new self(); 993 if (!($stat = $vfs->url_stat($path,0))) 994 { 995 if (self::LOG_LEVEL) error_log(__METHOD__."($url,$owner) no such file or directory!"); 996 trigger_error("No such file or directory $url !",E_USER_WARNING); 997 return false; 998 } 999 if (!Vfs::has_owner_rights($path,$stat)) 1000 { 1001 if (self::LOG_LEVEL) error_log(__METHOD__."($url,$owner) only owner or root can do that!"); 1002 trigger_error("Only owner or root can do that!",E_USER_WARNING); 1003 return false; 1004 } 1005 if ($owner < 0) $owner = -$owner; // sqlfs uses a positiv group id's! 1006 1007 if ($owner && !$GLOBALS['egw']->accounts->id2name(-$owner)) // not a group 1008 { 1009 if (self::LOG_LEVEL) error_log(__METHOD__."($url,$owner) unknown (numeric) group id!"); 1010 trigger_error("Unknown (numeric) group id!",E_USER_WARNING); 1011 return false; 1012 } 1013 $stmt = self::$pdo->prepare('UPDATE '.self::TABLE.' SET fs_gid=:fs_gid WHERE fs_id=:fs_id'); 1014 1015 // update stat-cache 1016 if ($path != '/' && substr($path,-1) == '/') $path = substr($path, 0, -1); 1017 self::$stat_cache[$path]['fs_gid'] = $owner; 1018 1019 return $stmt->execute(array( 1020 'fs_gid' => $owner, 1021 'fs_id' => $stat['ino'], 1022 )); 1023 } 1024 1025 /** 1026 * Chmod command, not yet a stream-wrapper function, but necessary 1027 * 1028 * @param string $url 1029 * @param int $mode 1030 * @return boolean 1031 */ 1032 protected function chmod($url,$mode) 1033 { 1034 if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url, $mode)"); 1035 1036 $path = Vfs::parse_url($url,PHP_URL_PATH); 1037 1038 $vfs = new self(); 1039 if (!($stat = $vfs->url_stat($path,0))) 1040 { 1041 if (self::LOG_LEVEL) error_log(__METHOD__."($url, $mode) no such file or directory!"); 1042 trigger_error("No such file or directory $url !",E_USER_WARNING); 1043 return false; 1044 } 1045 if (!Vfs::has_owner_rights($path,$stat)) 1046 { 1047 if (self::LOG_LEVEL) error_log(__METHOD__."($url, $mode) only owner or root can do that!"); 1048 trigger_error("Only owner or root can do that!",E_USER_WARNING); 1049 return false; 1050 } 1051 if (!is_numeric($mode)) // not a mode 1052 { 1053 if (self::LOG_LEVEL) error_log(__METHOD__."($url, $mode) no (numeric) mode!"); 1054 trigger_error("No (numeric) mode!",E_USER_WARNING); 1055 return false; 1056 } 1057 $stmt = self::$pdo->prepare('UPDATE '.self::TABLE.' SET fs_mode=:fs_mode WHERE fs_id=:fs_id'); 1058 1059 // update stat cache 1060 if ($path != '/' && substr($path,-1) == '/') $path = substr($path, 0, -1); 1061 self::$stat_cache[$path]['fs_mode'] = ((int) $mode) & 0777; 1062 1063 return $stmt->execute(array( 1064 'fs_mode' => ((int) $mode) & 0777, // we dont store the file and dir bits, give int overflow! 1065 'fs_id' => $stat['ino'], 1066 )); 1067 } 1068 1069 1070 /** 1071 * This method is called immediately when your stream object is created for examining directory contents with opendir(). 1072 * 1073 * @param string $url URL that was passed to opendir() and that this object is expected to explore. 1074 * @param int $options 1075 * @return booelan 1076 */ 1077 function dir_opendir ( $url, $options ) 1078 { 1079 $this->opened_dir = null; 1080 1081 $path = Vfs::parse_url($url,PHP_URL_PATH); 1082 1083 if (!($stat = $this->url_stat($url,0)) || // dir not found 1084 !($stat['mode'] & self::MODE_DIR) && $stat['mime'] != self::DIR_MIME_TYPE || // no dir 1085 !Vfs::check_access($url,Vfs::EXECUTABLE|Vfs::READABLE,$stat)) // no access 1086 { 1087 self::_remove_password($url); 1088 $msg = !($stat['mode'] & self::MODE_DIR) && $stat['mime'] != self::DIR_MIME_TYPE ? 1089 "$url is no directory" : 'permission denied'; 1090 if (self::LOG_LEVEL) error_log(__METHOD__."('$url',$options) $msg!"); 1091 $this->opened_dir = null; 1092 return false; 1093 } 1094 $this->opened_dir = array(); 1095 $query = 'SELECT fs_id,fs_name,fs_mode,fs_uid,fs_gid,fs_size,fs_mime,fs_created,fs_modified'.self::$extra_columns. 1096 ' FROM '.self::TABLE.' WHERE fs_dir=? AND fs_active='.self::_pdo_boolean(true). 1097 " ORDER BY fs_mime='httpd/unix-directory' DESC, fs_name ASC"; 1098 //if (self::LOG_LEVEL > 2) $query = '/* '.__METHOD__.': '.__LINE__.' */ '.$query; 1099 if (self::LOG_LEVEL > 2) $query = '/* '.__METHOD__."($url,$options)".' */ '.$query; 1100 1101 $stmt = self::$pdo->prepare($query); 1102 $stmt->setFetchMode(\PDO::FETCH_ASSOC); 1103 if ($stmt->execute(array($stat['ino']))) 1104 { 1105 foreach($stmt as $file) 1106 { 1107 $this->opened_dir[] = $file['fs_name']; 1108 self::$stat_cache[Vfs::concat($path,$file['fs_name'])] = $file; 1109 } 1110 } 1111 if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url,$options): ".implode(', ',$this->opened_dir)); 1112 reset($this->opened_dir); 1113 1114 return true; 1115 } 1116 1117 /** 1118 * This method is called in response to stat() calls on the URL paths associated with the wrapper. 1119 * 1120 * It should return as many elements in common with the system function as possible. 1121 * Unknown or unavailable values should be set to a rational value (usually 0). 1122 * 1123 * If you plan to use your wrapper in a require_once you need to define stream_stat(). 1124 * If you plan to allow any other tests like is_file()/is_dir(), you have to define url_stat(). 1125 * stream_stat() must define the size of the file, or it will never be included. 1126 * url_stat() must define mode, or is_file()/is_dir()/is_executable(), and any of those functions affected by clearstatcache() simply won't work. 1127 * It's not documented, but directories must be a mode like 040777 (octal), and files a mode like 0100666. 1128 * If you wish the file to be executable, use 7s instead of 6s. 1129 * The last 3 digits are exactly the same thing as what you pass to chmod. 1130 * 040000 defines a directory, and 0100000 defines a file. 1131 * 1132 * @param string $url 1133 * @param int $flags holds additional flags set by the streams API. It can hold one or more of the following values OR'd together: 1134 * - STREAM_URL_STAT_LINK For resources with the ability to link to other resource (such as an HTTP Location: forward, 1135 * or a filesystem symlink). This flag specified that only information about the link itself should be returned, 1136 * not the resource pointed to by the link. 1137 * This flag is set in response to calls to lstat(), is_link(), or filetype(). 1138 * - STREAM_URL_STAT_QUIET If this flag is set, your wrapper should not raise any errors. If this flag is not set, 1139 * you are responsible for reporting errors using the trigger_error() function during stating of the path. 1140 * stat triggers it's own warning anyway, so it makes no sense to trigger one by our stream-wrapper! 1141 * @return array 1142 */ 1143 function url_stat ( $url, $flags ) 1144 { 1145 static $max_subquery_depth=null; 1146 if (is_null($max_subquery_depth)) 1147 { 1148 $max_subquery_depth = $GLOBALS['egw_info']['server']['max_subquery_depth']; 1149 if (!$max_subquery_depth) $max_subquery_depth = 7; // setting current default of 7, if nothing set 1150 } 1151 if (self::LOG_LEVEL > 1) error_log(__METHOD__."('$url',$flags)"); 1152 1153 $path = Vfs::parse_url($url,PHP_URL_PATH); 1154 1155 // webdav adds a trailing slash to dirs, which causes url_stat to NOT find the file otherwise 1156 if ($path != '/' && substr($path,-1) == '/') 1157 { 1158 $path = substr($path,0,-1); 1159 } 1160 if (empty($path)) 1161 { 1162 return false; // is invalid and gives sql error 1163 } 1164 // check if we already have the info from the last dir_open call, as the old vfs reads it anyway from the db 1165 if (self::$stat_cache && isset(self::$stat_cache[$path]) && self::$stat_cache[$path] !== false) 1166 { 1167 return self::$stat_cache[$path] ? self::_vfsinfo2stat(self::$stat_cache[$path]) : false; 1168 } 1169 1170 if (!is_object(self::$pdo)) 1171 { 1172 self::_pdo(); 1173 } 1174 $base_query = 'SELECT fs_id,fs_name,fs_mode,fs_uid,fs_gid,fs_size,fs_mime,fs_created,fs_modified'.self::$extra_columns. 1175 ' FROM '.self::TABLE.' WHERE fs_active='.self::_pdo_boolean(true). 1176 ' AND fs_name'.self::$case_sensitive_equal.'? AND fs_dir='; 1177 $parts = explode('/',$path); 1178 1179 // if we have extended acl access to the url, we dont need and can NOT include the sql for the readable check 1180 $eacl_access = static::check_extended_acl($path,Vfs::READABLE); 1181 1182 try { 1183 foreach($parts as $n => $name) 1184 { 1185 if ($n == 0) 1186 { 1187 $query = (int) ($path != '/'); // / always has fs_id == 1, no need to query it ($path=='/' needs fs_dir=0!) 1188 } 1189 elseif ($n < count($parts)-1) 1190 { 1191 // MySQL 5.0 has a nesting limit for subqueries 1192 // --> we replace the so far cumulated subqueries with their result 1193 // no idea about the other DBMS, but this does NOT hurt ... 1194 // --> depth limit of subqueries is now dynamicly decremented in catch 1195 if ($n > 1 && !(($n-1) % $max_subquery_depth) && !($query = self::$pdo->query($query)->fetchColumn())) 1196 { 1197 if (self::LOG_LEVEL > 1) 1198 { 1199 self::_remove_password($url); 1200 error_log(__METHOD__."('$url',$flags) file or directory not found!"); 1201 } 1202 // we also store negatives (all methods creating new files/dirs have to unset the stat-cache!) 1203 return self::$stat_cache[$path] = false; 1204 } 1205 $query = 'SELECT fs_id FROM '.self::TABLE.' WHERE fs_dir=('.$query.') AND fs_active='. 1206 self::_pdo_boolean(true).' AND fs_name'.self::$case_sensitive_equal.self::$pdo->quote($name); 1207 1208 // if we are not root AND have no extended acl access, we need to make sure the user has the right to tranverse all parent directories (read-rights) 1209 if (!Vfs::$is_root && !$eacl_access) 1210 { 1211 if (!Vfs::$user) 1212 { 1213 self::_remove_password($url); 1214 if (self::LOG_LEVEL > 1) error_log(__METHOD__."('$url',$flags) permission denied, no user-id and not root!"); 1215 return false; 1216 } 1217 $query .= ' AND '.self::_sql_readable(); 1218 } 1219 } 1220 else 1221 { 1222 $query = str_replace('fs_name'.self::$case_sensitive_equal.'?','fs_name'.self::$case_sensitive_equal.self::$pdo->quote($name),$base_query).'('.$query.')'; 1223 } 1224 } 1225 if (self::LOG_LEVEL > 2) $query = '/* '.__METHOD__."($url,$flags) eacl_access=$eacl_access".' */ '.$query; 1226 //if (self::LOG_LEVEL > 2) $query = '/* '.__METHOD__.': '.__LINE__.' */ '.$query; 1227 1228 if (!($result = self::$pdo->query($query)) || !($info = $result->fetch(\PDO::FETCH_ASSOC))) 1229 { 1230 if (self::LOG_LEVEL > 1) 1231 { 1232 self::_remove_password($url); 1233 error_log(__METHOD__."('$url',$flags) file or directory not found!"); 1234 } 1235 // we also store negatives (all methods creating new files/dirs have to unset the stat-cache!) 1236 return self::$stat_cache[$path] = false; 1237 } 1238 } 1239 catch (\PDOException $e) { 1240 // decrement subquery limit by 1 and try again, if not already smaller then 3 1241 if ($max_subquery_depth < 3) 1242 { 1243 throw new Api\Db\Exception($e->getMessage()); 1244 } 1245 $GLOBALS['egw_info']['server']['max_subquery_depth'] = --$max_subquery_depth; 1246 error_log(__METHOD__."() decremented max_subquery_depth to $max_subquery_depth"); 1247 Api\Config::save_value('max_subquery_depth', $max_subquery_depth, 'phpgwapi'); 1248 if (method_exists($GLOBALS['egw'],'invalidate_session_cache')) $GLOBALS['egw']->invalidate_session_cache(); 1249 return $this->url_stat($url, $flags); 1250 } 1251 self::$stat_cache[$path] = $info; 1252 1253 if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url,$flags)=".array2string($info)); 1254 return self::_vfsinfo2stat($info); 1255 } 1256 1257 /** 1258 * Return readable check as sql (to be AND'ed into the query), only use if !Vfs::$is_root 1259 * 1260 * @return string 1261 */ 1262 protected static function _sql_readable() 1263 { 1264 static $sql_read_acl=null; 1265 1266 if (is_null($sql_read_acl)) 1267 { 1268 foreach($GLOBALS['egw']->accounts->memberships(Vfs::$user,true) as $gid) 1269 { 1270 $memberships[] = abs($gid); // sqlfs stores the gid's positiv 1271 } 1272 // using octal numbers with mysql leads to funny results (select 384 & 0400 --> 384 not 256=0400) 1273 // 256 = 0400, 32 = 040 1274 $sql_read_acl = '((fs_mode & 4)=4 OR (fs_mode & 256)=256 AND fs_uid='.(int)Vfs::$user. 1275 ($memberships ? ' OR (fs_mode & 32)=32 AND fs_gid IN('.implode(',',$memberships).')' : '').')'; 1276 //error_log(__METHOD__."() Vfs::\$user=".array2string(Vfs::$user).' --> memberships='.array2string($memberships).' --> '.$sql_read_acl.($memberships?'':': '.function_backtrace())); 1277 } 1278 return $sql_read_acl; 1279 } 1280 1281 /** 1282 * This method is called in response to readdir(). 1283 * 1284 * It should return a string representing the next filename in the location opened by dir_opendir(). 1285 * 1286 * @return string 1287 */ 1288 function dir_readdir ( ) 1289 { 1290 if (self::LOG_LEVEL > 1) error_log(__METHOD__."( )"); 1291 1292 if (!is_array($this->opened_dir)) return false; 1293 1294 $file = current($this->opened_dir); next($this->opened_dir); 1295 1296 return $file; 1297 } 1298 1299 /** 1300 * This method is called in response to rewinddir(). 1301 * 1302 * It should reset the output generated by dir_readdir(). i.e.: 1303 * The next call to dir_readdir() should return the first entry in the location returned by dir_opendir(). 1304 * 1305 * @return boolean 1306 */ 1307 function dir_rewinddir ( ) 1308 { 1309 if (self::LOG_LEVEL > 1) error_log(__METHOD__."( )"); 1310 1311 if (!is_array($this->opened_dir)) return false; 1312 1313 reset($this->opened_dir); 1314 1315 return true; 1316 } 1317 1318 /** 1319 * This method is called in response to closedir(). 1320 * 1321 * You should release any resources which were locked or allocated during the opening and use of the directory stream. 1322 * 1323 * @return boolean 1324 */ 1325 function dir_closedir ( ) 1326 { 1327 if (self::LOG_LEVEL > 1) error_log(__METHOD__."( )"); 1328 1329 if (!is_array($this->opened_dir)) return false; 1330 1331 $this->opened_dir = null; 1332 1333 return true; 1334 } 1335 1336 /** 1337 * This method is called in response to readlink(). 1338 * 1339 * The readlink value is read by url_stat or dir_opendir and therefore cached in the stat-cache. 1340 * 1341 * @param string $path 1342 * @return string|boolean content of the symlink or false if $url is no symlink (or not found) 1343 */ 1344 static function readlink($path) 1345 { 1346 $vfs = new self(); 1347 $link = !($lstat = $vfs->url_stat($path,STREAM_URL_STAT_LINK)) || is_null($lstat['readlink']) ? false : $lstat['readlink']; 1348 1349 if (self::LOG_LEVEL > 1) error_log(__METHOD__."('$path') = $link"); 1350 1351 return $link; 1352 } 1353 1354 /** 1355 * Method called for symlink() 1356 * 1357 * @param string $target 1358 * @param string $link 1359 * @return boolean true on success false on error 1360 */ 1361 static function symlink($target,$link) 1362 { 1363 if (self::LOG_LEVEL > 1) error_log(__METHOD__."('$target','$link')"); 1364 1365 $inst = new static(); 1366 if ($inst->url_stat($link,0)) 1367 { 1368 if (self::LOG_LEVEL > 0) error_log(__METHOD__."('$target','$link') $link exists, returning false!"); 1369 return false; // $link already exists 1370 } 1371 if (!($dir = Vfs::dirname($link)) || 1372 !Vfs::check_access($dir,Vfs::WRITABLE,$dir_stat=$inst->url_stat($dir,0))) 1373 { 1374 if (self::LOG_LEVEL > 0) error_log(__METHOD__."('$target','$link') returning false! (!is_writable('$dir'), dir_stat=".array2string($dir_stat).")"); 1375 return false; // parent dir does not exist or is not writable 1376 } 1377 $query = 'INSERT INTO '.self::TABLE.' (fs_name,fs_dir,fs_mode,fs_uid,fs_gid,fs_created,fs_modified,fs_creator,fs_mime,fs_size,fs_link'. 1378 ') VALUES (:fs_name,:fs_dir,:fs_mode,:fs_uid,:fs_gid,:fs_created,:fs_modified,:fs_creator,:fs_mime,:fs_size,:fs_link)'; 1379 if (self::LOG_LEVEL > 2) $query = '/* '.__METHOD__.': '.__LINE__.' */ '.$query; 1380 $stmt = self::$pdo->prepare($query); 1381 unset(self::$stat_cache[Vfs::parse_url($link,PHP_URL_PATH)]); 1382 1383 return !!$stmt->execute(array( 1384 'fs_name' => self::limit_filename(Vfs::basename($link)), 1385 'fs_dir' => $dir_stat['ino'], 1386 'fs_mode' => ($dir_stat['mode'] & 0666), 1387 'fs_uid' => $dir_stat['uid'] ? $dir_stat['uid'] : Vfs::$user, 1388 'fs_gid' => $dir_stat['gid'], 1389 'fs_created' => self::_pdo_timestamp(time()), 1390 'fs_modified' => self::_pdo_timestamp(time()), 1391 'fs_creator' => Vfs::$user, 1392 'fs_mime' => self::SYMLINK_MIME_TYPE, 1393 'fs_size' => bytes($target), 1394 'fs_link' => $target, 1395 )); 1396 } 1397 1398 private static $extended_acl; 1399 1400 /** 1401 * Check if extendes ACL (stored in eGW's ACL table) grants access 1402 * 1403 * The extended ACL is inherited, so it's valid for all subdirs and the included files! 1404 * The used algorithm break on the first match. It could be used, to disallow further access. 1405 * 1406 * @param string $url url to check 1407 * @param int $check mode to check: one or more or'ed together of: 4 = read, 2 = write, 1 = executable 1408 * @return boolean 1409 */ 1410 static function check_extended_acl($url,$check) 1411 { 1412 $url_path = Vfs::parse_url($url,PHP_URL_PATH); 1413 1414 if (is_null(self::$extended_acl)) 1415 { 1416 self::_read_extended_acl(); 1417 } 1418 $access = false; 1419 foreach(self::$extended_acl as $path => $rights) 1420 { 1421 if ($path == $url_path || substr($url_path,0,strlen($path)+1) == $path.'/') 1422 { 1423 $access = ($rights & $check) == $check; 1424 break; 1425 } 1426 } 1427 if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url,$check) ".($access?"access granted by $path=$rights":'no access!!!')); 1428 return $access; 1429 } 1430 1431 /** 1432 * Read the extended acl via acl::get_grants('sqlfs') 1433 * 1434 */ 1435 static protected function _read_extended_acl() 1436 { 1437 if ((self::$extended_acl = Api\Cache::getSession(self::EACL_APPNAME, 'extended_acl'))) 1438 { 1439 return; // ext. ACL read from session. 1440 } 1441 self::$extended_acl = array(); 1442 if (($rights = $GLOBALS['egw']->acl->get_all_location_rights(Vfs::$user,self::EACL_APPNAME))) 1443 { 1444 $pathes = self::id2path(array_keys($rights)); 1445 } 1446 foreach($rights as $fs_id => $right) 1447 { 1448 $path = $pathes[$fs_id]; 1449 if (isset($path)) 1450 { 1451 self::$extended_acl[$path] = (int)$right; 1452 } 1453 } 1454 // sort by length descending, to allow more specific pathes to have precedence 1455 uksort(self::$extended_acl, function($a,$b) { 1456 return strlen($b)-strlen($a); 1457 }); 1458 Api\Cache::setSession(self::EACL_APPNAME, 'extended_acl', self::$extended_acl); 1459 if (self::LOG_LEVEL > 1) error_log(__METHOD__.'() '.array2string(self::$extended_acl)); 1460 } 1461 1462 /** 1463 * Appname used with the acl class to store the extended acl 1464 */ 1465 const EACL_APPNAME = 'sqlfs'; 1466 1467 /** 1468 * Set or delete extended acl for a given path and owner (or delete them if is_null($rights) 1469 * 1470 * Only root, the owner of the path or an eGW admin (only if there's no owner but a group) are allowd to set eACL's! 1471 * 1472 * @param string $path string with path 1473 * @param int $rights =null rights to set, or null to delete the entry 1474 * @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 1475 * @param int $fs_id =null fs_id to use, to not query it again (eg. because it's already deleted) 1476 * @return boolean true if acl is set/deleted, false on error 1477 */ 1478 static function eacl($path,$rights=null,$owner=null,$fs_id=null) 1479 { 1480 if ($path[0] != '/') 1481 { 1482 $path = Vfs::parse_url($path,PHP_URL_PATH); 1483 } 1484 if (is_null($fs_id)) 1485 { 1486 $vfs = new self(); 1487 if (!($stat = $vfs->url_stat($path,0))) 1488 { 1489 if (self::LOG_LEVEL) error_log(__METHOD__."($path,$rights,$owner,$fs_id) no such file or directory!"); 1490 return false; // $path not found 1491 } 1492 if (!Vfs::has_owner_rights($path,$stat)) // not group dir and user is eGW admin 1493 { 1494 if (self::LOG_LEVEL) error_log(__METHOD__."($path,$rights,$owner,$fs_id) permission denied!"); 1495 return false; // permission denied 1496 } 1497 $fs_id = $stat['ino']; 1498 } 1499 if (is_null($owner)) 1500 { 1501 $owner = Vfs::$user; 1502 } 1503 if (is_null($rights) || $owner === false) 1504 { 1505 // delete eacl 1506 if (is_null($owner) || $owner == Vfs::$user || 1507 $owner < 0 && Vfs::$user && in_array($owner,$GLOBALS['egw']->accounts->memberships(Vfs::$user,true))) 1508 { 1509 self::$extended_acl = null; // force new read of eACL, as there could be multiple eACL for that path 1510 } 1511 $ret = $GLOBALS['egw']->acl->delete_repository(self::EACL_APPNAME, $fs_id, (int)$owner, false); 1512 } 1513 else 1514 { 1515 if (isset(self::$extended_acl) && ($owner == Vfs::$user || 1516 $owner < 0 && Vfs::$user && in_array($owner,$GLOBALS['egw']->accounts->memberships(Vfs::$user,true)))) 1517 { 1518 // set rights for this class, if applicable 1519 self::$extended_acl[$path] |= $rights; 1520 } 1521 $ret = $GLOBALS['egw']->acl->add_repository(self::EACL_APPNAME, $fs_id, $owner, $rights, false); 1522 } 1523 if ($ret) 1524 { 1525 Api\Cache::setSession(self::EACL_APPNAME, 'extended_acl', self::$extended_acl); 1526 } 1527 if (self::LOG_LEVEL > 1) error_log(__METHOD__."($path,$rights,$owner,$fs_id)=".(int)$ret); 1528 return $ret; 1529 } 1530 1531 /** 1532 * Get all ext. ACL set for a path 1533 * 1534 * Calls itself recursive, to get the parent directories 1535 * 1536 * @param string $path 1537 * @return array|boolean array with array('path'=>$path,'owner'=>$owner,'rights'=>$rights) or false if $path not found 1538 */ 1539 static function get_eacl($path) 1540 { 1541 $inst = new static(); 1542 if (!($stat = $inst->url_stat($path, STREAM_URL_STAT_QUIET))) 1543 { 1544 error_log(__METHOD__.__LINE__.' '.array2string($path).' not found!'); 1545 return false; // not found 1546 } 1547 $eacls = array(); 1548 foreach($GLOBALS['egw']->acl->get_all_rights($stat['ino'],self::EACL_APPNAME) as $owner => $rights) 1549 { 1550 $eacls[] = array( 1551 'path' => $path, 1552 'owner' => $owner, 1553 'rights' => $rights, 1554 'ino' => $stat['ino'], 1555 ); 1556 } 1557 if (($path = Vfs::dirname($path))) 1558 { 1559 $eacls = array_merge((array)self::get_eacl($path),$eacls); 1560 } 1561 // sort by length descending, to show precedence 1562 usort($eacls, function($a, $b) { 1563 return strlen($b['path']) - strlen($a['path']); 1564 }); 1565 //error_log(__METHOD__."('$_path') returning ".array2string($eacls)); 1566 return $eacls; 1567 } 1568 1569 /** 1570 * Get the lowest file id (fs_id) for a given path 1571 * 1572 * @param string $path 1573 * @return integer 1574 */ 1575 static function get_minimum_file_id($path) 1576 { 1577 $vfs = new self(); 1578 $stat = $vfs->url_stat($path,0); 1579 if ($stat['readlink']) 1580 { 1581 $stat = $vfs->url_stat($stat['readlink'], 0); 1582 } 1583 $fs_id = $stat['ino']; 1584 1585 $query = 'SELECT MIN(B.fs_id) 1586FROM '.self::TABLE.' as A 1587JOIN '.self::TABLE.' AS B ON A.fs_name = B.fs_name AND A.fs_dir = B.fs_dir AND A.fs_active = '. 1588 self::_pdo_boolean(true).' AND B.fs_active = '.self::_pdo_boolean(false).' 1589WHERE A.fs_id=? 1590GROUP BY A.fs_id'; 1591 if (self::LOG_LEVEL > 2) 1592 { 1593 $query = '/* '.__METHOD__.': '.__LINE__.' */ '.$query; 1594 } 1595 $stmt = self::$pdo->prepare($query); 1596 1597 $stmt->execute(array($fs_id)); 1598 $min = $stmt->fetchColumn(); 1599 1600 return $min ? $min : $fs_id; 1601 } 1602 1603 /** 1604 * Max allowed sub-directory depth, to be able to break infinit recursion by wrongly linked directories 1605 */ 1606 const MAX_ID2PATH_RECURSION = 100; 1607 1608 /** 1609 * Return the path of given fs_id(s) 1610 * 1611 * Searches the stat_cache first and then the db. 1612 * Calls itself recursive to to determine the path of the parent/directory 1613 * 1614 * @param int|array $fs_ids integer fs_id or array of them 1615 * @param int $recursion_count =0 internally used to break infinit recursions 1616 * @return false|string|array path or array or pathes indexed by fs_id, or false on error 1617 */ 1618 static function id2path($fs_ids, $recursion_count=0) 1619 { 1620 if (self::LOG_LEVEL > 1) error_log(__METHOD__.'('.array2string($fs_ids).')'); 1621 if ($recursion_count > self::MAX_ID2PATH_RECURSION) 1622 { 1623 error_log(__METHOD__."(".array2string($fs_ids).", $recursion_count) max recursion depth reached, probably broken filesystem!"); 1624 return false; 1625 } 1626 $ids = (array)$fs_ids; 1627 $pathes = array(); 1628 // first check our stat-cache for the ids 1629 foreach(self::$stat_cache as $path => $stat) 1630 { 1631 if (($key = array_search($stat['fs_id'],$ids)) !== false) 1632 { 1633 $pathes[$stat['fs_id']] = $path; 1634 unset($ids[$key]); 1635 if (!$ids) 1636 { 1637 if (self::LOG_LEVEL > 1) error_log(__METHOD__.'('.array2string($fs_ids).')='.array2string($pathes).' *from stat_cache*'); 1638 return is_array($fs_ids) ? $pathes : array_shift($pathes); 1639 } 1640 } 1641 } 1642 // now search via the database 1643 if (count($ids) > 1) $ids = array_map(function($v) { return (int)$v; }, $ids); 1644 $query = 'SELECT fs_id,fs_dir,fs_name FROM '.self::TABLE.' WHERE fs_id'. 1645 (count($ids) == 1 ? '='.(int)$ids[0] : ' IN ('.implode(',',$ids).')'); 1646 if (self::LOG_LEVEL > 2) $query = '/* '.__METHOD__.': '.__LINE__.' */ '.$query; 1647 1648 if (!is_object(self::$pdo)) 1649 { 1650 self::_pdo(); 1651 } 1652 $stmt = self::$pdo->prepare($query); 1653 $stmt->setFetchMode(\PDO::FETCH_ASSOC); 1654 if (!$stmt->execute()) 1655 { 1656 return false; // not found 1657 } 1658 $parents = array(); 1659 foreach($stmt as $row) 1660 { 1661 if ($row['fs_dir'] > 1 && !in_array($row['fs_dir'],$parents)) 1662 { 1663 $parents[] = $row['fs_dir']; 1664 } 1665 $rows[$row['fs_id']] = $row; 1666 } 1667 unset($stmt); 1668 1669 if ($parents && !($parents = self::id2path($parents, $recursion_count+1))) 1670 { 1671 return false; // parent not found, should never happen ... 1672 } 1673 if (self::LOG_LEVEL > 1) error_log(__METHOD__." trying foreach with:".print_r($rows,true)."#"); 1674 foreach((array)$rows as $fs_id => $row) 1675 { 1676 $parent = $row['fs_dir'] > 1 ? $parents[$row['fs_dir']] : ''; 1677 1678 $pathes[$fs_id] = $parent . '/' . $row['fs_name']; 1679 } 1680 if (self::LOG_LEVEL > 1) error_log(__METHOD__.'('.array2string($fs_ids).')='.array2string($pathes)); 1681 return is_array($fs_ids) ? $pathes : array_shift($pathes); 1682 } 1683 1684 /** 1685 * Limit filename to precision of column while keeping the extension 1686 * 1687 * @param string $name 1688 * @return string 1689 */ 1690 static protected function limit_filename($name) 1691 { 1692 static $fs_name_precision = null; 1693 if (!isset($fs_name_precision)) 1694 { 1695 $fs_name_precision = $GLOBALS['egw']->db->get_column_attribute('fs_name', self::TABLE, 'phpgwapi', 'precision'); 1696 } 1697 if (mb_strlen($name) > $fs_name_precision) 1698 { 1699 $parts = explode('.', $name); 1700 if ($parts > 1 && mb_strlen($extension = '.'.array_pop($parts)) <= $fs_name_precision) 1701 { 1702 $name = mb_substr(implode('.', $parts), 0, $fs_name_precision-mb_strlen($extension)).$extension; 1703 } 1704 else 1705 { 1706 $name = mb_substr(implode('.', $parts), 0, $fs_name_precision); 1707 } 1708 } 1709 return $name; 1710 } 1711 1712 /** 1713 * Convert a sqlfs-file-info into a stat array 1714 * 1715 * @param array $info 1716 * @return array 1717 */ 1718 static protected function _vfsinfo2stat($info) 1719 { 1720 $stat = array( 1721 'ino' => $info['fs_id'], 1722 'name' => $info['fs_name'], 1723 'mode' => $info['fs_mode'] | 1724 ($info['fs_mime'] == self::DIR_MIME_TYPE ? self::MODE_DIR : 1725 ($info['fs_mime'] == self::SYMLINK_MIME_TYPE ? self::MODE_LINK : self::MODE_FILE)), // required by the stream wrapper 1726 'size' => $info['fs_size'], 1727 'uid' => $info['fs_uid'], 1728 'gid' => $info['fs_gid'], 1729 'mtime' => strtotime($info['fs_modified']), 1730 'ctime' => strtotime($info['fs_created']), 1731 'nlink' => $info['fs_mime'] == self::DIR_MIME_TYPE ? 2 : 1, 1732 // eGW addition to return some extra values 1733 'mime' => $info['fs_mime'], 1734 'readlink' => $info['fs_link'], 1735 ); 1736 if (self::LOG_LEVEL > 1) error_log(__METHOD__."($info[name]) = ".array2string($stat)); 1737 return $stat; 1738 } 1739 1740 /** 1741 * Maximum value for a single hash element (should be 10^N): 10, 100 (default), 1000, ... 1742 * 1743 * DONT change this value, once you have files stored, they will no longer be found! 1744 */ 1745 const HASH_MAX = 100; 1746 1747 /** 1748 * Return the path of the stored content of a file if $this->operation == self::STORE2FS 1749 * 1750 * To limit the number of files stored in one directory, we create a hash from the fs_id: 1751 * 1 --> /00/1 1752 * 34 --> /00/34 1753 * 123 --> /01/123 1754 * 4567 --> /45/4567 1755 * 99999 --> /09/99/99999 1756 * --> so one directory contains maximum 2 * HASH_MAY entries (HASH_MAX dirs + HASH_MAX files) 1757 * @param int $id id of the file 1758 * @return string 1759 */ 1760 static function _fs_path($id) 1761 { 1762 if (!is_numeric($id)) 1763 { 1764 throw new Api\Exception\WrongParameter(__METHOD__."(id=$id) id has to be an integer!"); 1765 } 1766 if (!isset($GLOBALS['egw_info']['server']['files_dir'])) 1767 { 1768 if (is_object($GLOBALS['egw_setup']->db)) // if we run under setup, query the db for the files dir 1769 { 1770 $GLOBALS['egw_info']['server']['files_dir'] = $GLOBALS['egw_setup']->db->select('egw_config','config_value',array( 1771 'config_name' => 'files_dir', 1772 'config_app' => 'phpgwapi', 1773 ),__LINE__,__FILE__)->fetchColumn(); 1774 } 1775 } 1776 if (!$GLOBALS['egw_info']['server']['files_dir']) 1777 { 1778 throw new Api\Exception\AssertionFailed("\$GLOBALS['egw_info']['server']['files_dir'] not set!"); 1779 } 1780 $hash = array(); 1781 $n = $id; 1782 while(($n = (int) ($n / self::HASH_MAX))) 1783 { 1784 $hash[] = sprintf('%02d',$n % self::HASH_MAX); 1785 } 1786 if (!$hash) $hash[] = '00'; // we need at least one directory, to not conflict with the dir-names 1787 array_unshift($hash,$id); 1788 1789 $path = '/sqlfs/'.implode('/',array_reverse($hash)); 1790 //error_log(__METHOD__."($id) = '$path'"); 1791 return $GLOBALS['egw_info']['server']['files_dir'].$path; 1792 } 1793 1794 /** 1795 * Replace the password of an url with '...' for error messages 1796 * 1797 * @param string &$url 1798 */ 1799 static protected function _remove_password(&$url) 1800 { 1801 $parts = Vfs::parse_url($url); 1802 1803 if ($parts['pass'] || $parts['scheme']) 1804 { 1805 $url = $parts['scheme'].'://'.($parts['user'] ? $parts['user'].($parts['pass']?':...':'').'@' : ''). 1806 $parts['host'].$parts['path']; 1807 } 1808 } 1809 1810 /** 1811 * Get storage mode from url (get parameter 'storage', eg. ?storage=db) 1812 * 1813 * @param string|array $url complete url or array of url-parts from parse_url 1814 * @return int self::STORE2FS or self::STORE2DB 1815 */ 1816 static function url2operation($url) 1817 { 1818 $operation = self::DEFAULT_OPERATION; 1819 1820 if (strpos(is_array($url) ? $url['query'] : $url,'storage=') !== false) 1821 { 1822 $query = null; 1823 parse_str(is_array($url) ? $url['query'] : Vfs::parse_url($url,PHP_URL_QUERY), $query); 1824 switch ($query['storage']) 1825 { 1826 case 'db': 1827 $operation = self::STORE2DB; 1828 break; 1829 case 'fs': 1830 default: 1831 $operation = self::STORE2FS; 1832 break; 1833 } 1834 } 1835 //error_log(__METHOD__."('$url') = $operation (1=DB, 2=FS)"); 1836 return $operation; 1837 } 1838 1839 /** 1840 * Store properties for a single ressource (file or dir) 1841 * 1842 * @param string|int $path string with path or integer fs_id 1843 * @param array $props array of array with values for keys 'name', 'ns', 'val' (null to delete the prop) 1844 * @return boolean true if props are updated, false otherwise (eg. ressource not found) 1845 */ 1846 static function proppatch($path,array $props) 1847 { 1848 static $inst = null; 1849 if (self::LOG_LEVEL > 1) error_log(__METHOD__."(".array2string($path).','.array2string($props)); 1850 if (!is_numeric($path)) 1851 { 1852 if (!isset($inst)) $inst = new self(); 1853 if (!($stat = $inst->url_stat($path,0))) 1854 { 1855 return false; 1856 } 1857 $id = $stat['ino']; 1858 } 1859 elseif(!($path = self::id2path($id=$path))) 1860 { 1861 return false; 1862 } 1863 if (!Vfs::check_access($path,Api\Acl::EDIT,$stat)) 1864 { 1865 return false; // permission denied 1866 } 1867 $ins_stmt = $del_stmt = null; 1868 foreach($props as &$prop) 1869 { 1870 if (!isset($prop['ns'])) $prop['ns'] = Vfs::DEFAULT_PROP_NAMESPACE; 1871 1872 if (!isset($prop['val']) || self::$pdo_type != 'mysql') // for non mysql, we have to delete the prop anyway, as there's no REPLACE! 1873 { 1874 if (!isset($del_stmt)) 1875 { 1876 $del_stmt = self::$pdo->prepare('DELETE FROM '.self::PROPS_TABLE.' WHERE fs_id=:fs_id AND prop_namespace=:prop_namespace AND prop_name=:prop_name'); 1877 } 1878 $del_stmt->execute(array( 1879 'fs_id' => $id, 1880 'prop_namespace' => $prop['ns'], 1881 'prop_name' => $prop['name'], 1882 )); 1883 } 1884 if (isset($prop['val'])) 1885 { 1886 if (!isset($ins_stmt)) 1887 { 1888 $ins_stmt = self::$pdo->prepare((self::$pdo_type == 'mysql' ? 'REPLACE' : 'INSERT'). 1889 ' INTO '.self::PROPS_TABLE.' (fs_id,prop_namespace,prop_name,prop_value) VALUES (:fs_id,:prop_namespace,:prop_name,:prop_value)'); 1890 } 1891 if (!$ins_stmt->execute(array( 1892 'fs_id' => $id, 1893 'prop_namespace' => $prop['ns'], 1894 'prop_name' => $prop['name'], 1895 'prop_value' => $prop['val'], 1896 ))) 1897 { 1898 return false; 1899 } 1900 } 1901 } 1902 return true; 1903 } 1904 1905 /** 1906 * Read properties for a ressource (file, dir or all files of a dir) 1907 * 1908 * @param array|string|int $path_ids (array of) string with path or integer fs_id 1909 * @param string $ns ='http://egroupware.org/' namespace if propfind should be limited to a single one, use null for all 1910 * @return array|boolean false on error ($path_ids does not exist), array with props (values for keys 'name', 'ns', 'value'), or 1911 * fs_id/path => array of props for $depth==1 or is_array($path_ids) 1912 */ 1913 static function propfind($path_ids,$ns=Vfs::DEFAULT_PROP_NAMESPACE) 1914 { 1915 static $inst = null; 1916 1917 $ids = is_array($path_ids) ? $path_ids : array($path_ids); 1918 foreach($ids as &$id) 1919 { 1920 if (!is_numeric($id)) 1921 { 1922 if (!isset($inst)) $inst = new self(); 1923 if (!($stat = $inst->url_stat($id,0))) 1924 { 1925 if (self::LOG_LEVEL) error_log(__METHOD__."(".array2string($path_ids).",$ns) path '$id' not found!"); 1926 return false; 1927 } 1928 $id = $stat['ino']; 1929 } 1930 } 1931 if (count($ids) >= 1) $ids = array_map(function($v) { return (int)$v; }, $ids); 1932 $query = 'SELECT * FROM '.self::PROPS_TABLE.' WHERE (fs_id'. 1933 (count($ids) == 1 ? '='.(int)implode('',$ids) : ' IN ('.implode(',',$ids).')').')'. 1934 (!is_null($ns) ? ' AND prop_namespace=?' : ''); 1935 if (self::LOG_LEVEL > 2) $query = '/* '.__METHOD__.': '.__LINE__.' */ '.$query; 1936 1937 $stmt = self::$pdo->prepare($query); 1938 $stmt->setFetchMode(\PDO::FETCH_ASSOC); 1939 $stmt->execute(!is_null($ns) ? array($ns) : array()); 1940 1941 $props = array(); 1942 foreach($stmt as $row) 1943 { 1944 $props[$row['fs_id']][] = array( 1945 'val' => $row['prop_value'], 1946 'name' => $row['prop_name'], 1947 'ns' => $row['prop_namespace'], 1948 ); 1949 } 1950 if (!is_array($path_ids)) 1951 { 1952 $props = $props[$row['fs_id']] ? $props[$row['fs_id']] : array(); // return empty array for no props 1953 } 1954 elseif ($props && isset($stat) && is_array($id2path = self::id2path(array_keys($props)))) // need to map fs_id's to pathes 1955 { 1956 foreach($id2path as $id => $path) 1957 { 1958 $props[$path] =& $props[$id]; 1959 unset($props[$id]); 1960 } 1961 } 1962 if (self::LOG_LEVEL > 1) 1963 { 1964 foreach((array)$props as $k => $v) 1965 { 1966 error_log(__METHOD__."($path_ids,$ns) $k => ".array2string($v)); 1967 } 1968 } 1969 return $props; 1970 } 1971 1972 /** 1973 * Register __CLASS__ for self::SCHEMA 1974 */ 1975 public static function register() 1976 { 1977 stream_register_wrapper(self::SCHEME, __CLASS__); 1978 } 1979} 1980 1981StreamWrapper::register(); 1982