1<?php 2 3/** 4 * EGroupware Sharing base class 5 * 6 * @link http://www.egroupware.org 7 * @author Ralf Becker <rb@stylite.de> 8 * @copyright (c) 2014-16 by Ralf Becker <rb@stylite.de> 9 * @package api 10 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License 11 */ 12 13namespace EGroupware\Api; 14 15use EGroupware\Api\Vfs\HiddenUploadSharing; 16 17/** 18 * VFS sharing 19 * 20 * Token generation uses openssl_random_pseudo_bytes, if available, otherwise 21 * mt_rand based Api\Auth::randomstring is used. 22 * 23 * Existing user sessions are kept whenever possible by an additional mount into regular VFS: 24 * - share owner is current user (no problems with rights, they simply match) 25 * - share owner has owner-right for share: we create a temp. eACL for current user 26 * --> in all other cases session will be replaced with one of the anonymous user, 27 * as we dont support mounting with rights of share owner (VFS uses Vfs::$user!) 28 * 29 * @todo handle mounts of an entry directory /apps/$app/$id 30 * @todo handle mounts inside shared directory (they get currently lost) 31 * @todo handle absolute symlinks (wont work as we use share as root) 32 */ 33class Sharing 34{ 35 /** 36 * Length of base64 encoded token (real length is only 3/4 of it) 37 * 38 * Dropbox uses just 15 chars (letters/numbers 5-6 bit), php sessions use 32 chars (hex = 4bits), 39 * so 32 chars of base64 = 6bits should be plenty. 40 */ 41 const TOKEN_LENGTH = 32; 42 43 /** 44 * Name of table used for storing tokens 45 */ 46 const TABLE = 'egw_sharing'; 47 48 /** 49 * Reference to global db object 50 * 51 * @var Api\Db 52 */ 53 protected static $db; 54 55 /** 56 * Share we are instanciated for 57 * 58 * @var array 59 */ 60 protected $share; 61 62 const READONLY = 'share_ro'; 63 const WRITABLE = 'share_rw'; 64 65 /** 66 * Modes for sharing files 67 * 68 * @var array 69 */ 70 static $modes = array( 71 self::READONLY => array( 72 'label' => 'Readonly share', 73 'title' => 'Link is generated allowing recipients to view entries', 74 ), 75 self::WRITABLE => array( 76 'label' => 'Writable share', 77 'title' => 'Link is generated allowing recipients to view and modify entries' 78 ), 79 ); 80 81 /** 82 * Protected constructor called via self::create_session 83 * 84 * @param string $token 85 * @param array $share 86 */ 87 protected function __construct(array $share) 88 { 89 static::$db = $GLOBALS['egw']->db; 90 $this->share = $share; 91 } 92 93 /** 94 * Get token from url 95 */ 96 public static function get_token() 97 { 98 // WebDAV has no concept of a query string and clients (including cadaver) 99 // seem to pass '?' unencoded, so we need to extract the path info out 100 // of the request URI ourselves 101 // if request URI contains a full url, remove schema and domain 102 $matches = null; 103 if (preg_match('|^https?://[^/]+(/.*)$|', $path_info=$_SERVER['REQUEST_URI'], $matches)) 104 { 105 $path_info = $matches[1]; 106 } 107 $path_info = substr($path_info, strlen($_SERVER['SCRIPT_NAME'])); 108 list(, $token/*, $path*/) = preg_split('|[/?]|', $path_info, 3); 109 110 list($token) = explode(':', $token); 111 return $token; 112 } 113 114 /** 115 * Get root of share 116 * 117 * @return string 118 */ 119 public function get_root() 120 { 121 return $this->share['share_root']; 122 } 123 124 /** 125 * Get share path 126 */ 127 public function get_path() 128 { 129 return $this->share['share_path']; 130 } 131 132 /** 133 * Get share with email addresses 134 */ 135 public function get_share_with() 136 { 137 return $this->share['share_with']; 138 } 139 140 /** 141 * Create sharing session 142 * 143 * Certain cases: 144 * a) there is not session $keep_session === null 145 * --> create new anon session with just filemanager rights and share as fstab 146 * b) there is a session $keep_session === true 147 * b1) current user is share owner (eg. checking the link) 148 * --> mount share under token additionally 149 * b2) current user not share owner 150 * b2a) need/use filemanager UI (eg. directory) 151 * --> destroy current session and continue with a) 152 * b2b) single file or WebDAV 153 * --> modify EGroupware enviroment for that request only, no change in session 154 * 155 * @param boolean $keep_session =null null: create a new session, true: try mounting it into existing (already verified) session 156 * @return string with sessionid 157 */ 158 public static function create_session($keep_session=null) 159 { 160 $share = array(); 161 static::check_token($keep_session, $share); 162 if($share) 163 { 164 $classname = static::get_share_class($share); 165 $classname::setup_share($keep_session, $share); 166 return $classname::login($keep_session, $share); 167 } 168 return ''; 169 } 170 171 protected static function check_token($keep_session, &$share) 172 { 173 self::$db = $GLOBALS['egw']->db; 174 175 $token = static::get_token(); 176 177 // are we called from header include, because session did not verify 178 // --> check if it verifys for our token 179 if ($token && !$keep_session) 180 { 181 $_SERVER['PHP_AUTH_USER'] = $token; 182 if (!isset($_SERVER['PHP_AUTH_PW'])) $_SERVER['PHP_AUTH_PW'] = ''; 183 184 unset($GLOBALS['egw_info']['flags']['autocreate_session_callback']); 185 if (isset($GLOBALS['egw']->session) && $GLOBALS['egw']->session->verify() 186 && isset($GLOBALS['egw']->sharing) && $GLOBALS['egw']->sharing->share['share_token'] === $token) 187 { 188 return $GLOBALS['egw']->session->sessionid; 189 } 190 } 191 192 if (empty($token) || !($share = self::$db->select(self::TABLE, '*', array( 193 'share_token' => $token, 194 '(share_expires IS NULL OR share_expires > '.self::$db->quote(time(), 'date').')', 195 ), __LINE__, __FILE__,false,'',Db::API_APPNAME)->fetch()) || 196 !$GLOBALS['egw']->accounts->exists($share['share_owner'])) 197 { 198 sleep(1); 199 200 return static::share_fail( 201 '404 Not Found', 202 "Requested resource '/".htmlspecialchars($token)."' does NOT exist!\n" 203 ); 204 } 205 // check password, if required 206 if(!static::check_password($share)) 207 { 208 $realm = 'EGroupware share '.$share['share_token']; 209 header('WWW-Authenticate: Basic realm="'.$realm.'"'); 210 return static::share_fail( 211 '401 Unauthorized', 212 "Authorization failed." 213 ); 214 } 215 216 } 217 218 /** 219 * Check to see if the share needs a password, and if it does that the password 220 * provided matches. 221 * 222 * @param Array $share 223 * @return boolean Password OK (or not needed) 224 */ 225 protected static function check_password(Array $share) 226 { 227 if ($share['share_passwd'] && (empty($_SERVER['PHP_AUTH_PW']) || 228 !(Auth::compare_password($_SERVER['PHP_AUTH_PW'], $share['share_passwd'], 'crypt') || 229 Header\Authenticate::decode_password($_SERVER['PHP_AUTH_PW']) && 230 Auth::compare_password($_SERVER['PHP_AUTH_PW'], $share['share_passwd'], 'crypt')))) 231 { 232 return false; 233 } 234 return true; 235 } 236 237 /** 238 * Sub-class specific things needed to be done to the share before we try 239 * to login 240 * 241 * @param boolean $keep_session 242 * @param Array $share 243 */ 244 protected static function setup_share($keep_session, &$share) {} 245 /** 246 * Sub-class specific things needed to be done to the share (or session) 247 * after we login but before we start actually doing anything 248 */ 249 protected function after_login() {} 250 251 252 protected static function login($keep_session, &$share) 253 { 254 // update accessed timestamp 255 self::$db->update(self::TABLE, array( 256 'share_last_accessed' => $share['share_last_accessed']=time(), 257 ), array( 258 'share_id' => $share['share_id'], 259 ), __LINE__, __FILE__); 260 261 // store sharing object in egw object and therefore in session 262 $GLOBALS['egw']->sharing = static::factory($share); 263 264 // we have a session we want to keep, but share owner is different from current user and we need filemanager UI, or no session 265 // --> create a new anon session 266 if ($keep_session === false && $GLOBALS['egw']->sharing->need_session() || is_null($keep_session)) 267 { 268 $sessionid = static::create_new_session(); 269 270 $GLOBALS['egw']->sharing->after_login(); 271 } 272 // we have a session we want to keep, but share owner is different from current user and we dont need filemanager UI 273 // --> we dont need session and close it, to not modifiy it 274 elseif ($keep_session === false) 275 { 276 $GLOBALS['egw']->session->commit_session(); 277 } 278 // need to store new fstab and vfs_user in session to allow GET requests / downloads via WebDAV 279 $GLOBALS['egw_info']['user']['vfs_user'] = Vfs::$user; 280 $GLOBALS['egw_info']['server']['vfs_fstab'] = Vfs::mount(); 281 282 // update modified egw and egw_info again in session, if neccessary 283 if ($keep_session || $sessionid) 284 { 285 $_SESSION[Session::EGW_INFO_CACHE] = $GLOBALS['egw_info']; 286 unset($_SESSION[Session::EGW_INFO_CACHE]['flags']); // dont save the flags, they change on each request 287 288 $_SESSION[Session::EGW_OBJECT_CACHE] = serialize($GLOBALS['egw']); 289 } 290 291 return $sessionid; 292 } 293 294 public static function create_new_session() 295 { 296 // create session without checking auth: create(..., false, false) 297 if (!($sessionid = $GLOBALS['egw']->session->create('anonymous@'.$GLOBALS['egw_info']['user']['domain'], 298 '', 'text', false, false))) 299 { 300 sleep(1); 301 return static::share_fail( 302 '500 Internal Server Error', 303 "Failed to create session: ".$GLOBALS['egw']->session->reason."\n" 304 ); 305 } 306 return $sessionid; 307 } 308 309 /** 310 * Factory method to instanciate a share 311 * 312 * @param array $share 313 * 314 * @return Sharing 315 */ 316 public static function factory($share) 317 { 318 $class = static::get_share_class($share); 319 320 return new $class($share); 321 } 322 323 /** 324 * Get the namespaced class for the given share 325 * 326 * @param string $share 327 */ 328 protected static function get_share_class($share) 329 { 330 try 331 { 332 if(self::is_entry($share)) 333 { 334 list($app, $id) = explode('::', $share['share_path']); 335 if($app && class_exists('\EGroupware\\'. ucfirst($app) . '\Sharing')) 336 { 337 return '\EGroupware\\'. ucfirst($app) . '\Sharing'; 338 } 339 else if(class_exists('\EGroupware\Stylite\Link\Sharing')) 340 { 341 return '\\EGroupware\\Stylite\\Link\\Sharing'; 342 } 343 } 344 else if (class_exists ('\EGroupware\Collabora\Wopi') && (int)$share['share_writable'] === \EGroupware\Collabora\Wopi::WOPI_SHARED) 345 { 346 return '\\EGroupware\\Collabora\\Wopi'; 347 } 348 else if ((int)$share['share_writable'] == HiddenUploadSharing::HIDDEN_UPLOAD) 349 { 350 return '\\'.__NAMESPACE__ . '\\'. 'Vfs\\HiddenUploadSharing'; 351 } 352 } 353 catch(Exception $e){throw $e;} 354 return '\\'.__NAMESPACE__ . '\\'. (self::is_entry($share) ? 'Link' : 'Vfs'). '\\Sharing'; 355 } 356 357 /** 358 * Something failed, stop everything 359 * 360 * @param String $status 361 * @param String $message 362 */ 363 public static function share_fail($status, $message) 364 { 365 header("HTTP/1.1 $status"); 366 header("X-WebDAV-Status: $status", true); 367 echo $message; 368 369 $class = strpos($status, '404') === 0 ? 'EGroupware\Api\Exception\NotFound' : 370 strpos($status, '401') === 0 ? 'EGroupware\Api\Exception\NoPermission' : 371 'EGroupware\Api\Exception'; 372 throw new $class($message); 373 } 374 375 /** 376 * Check if we use filemanager UI 377 * 378 * Only for directories, if browser supports it and filemanager is installed 379 * 380 * @return boolean 381 */ 382 public function use_filemanager() 383 { 384 return !(!Vfs::is_dir($this->share['share_root']) || $_SERVER['REQUEST_METHOD'] != 'GET' || 385 // or unsupported browsers like ie < 10 386 Header\UserAgent::type() == 'msie' && Header\UserAgent::version() < 10.0 || 387 // or if no filemanager installed (WebDAV has own autoindex) 388 !file_exists(__DIR__.'/../../filemanager/inc/class.filemanager_ui.inc.php')); 389 } 390 /** 391 * Check if we should use Collabora UI 392 * 393 * Only for files, if URL says so, and Collabora & Stylite apps are installed 394 */ 395 public function use_collabora() 396 { 397 return !Vfs::is_dir($this->share['share_root']) && 398 array_key_exists('edit', $_REQUEST) && 399 array_key_exists('collabora', $GLOBALS['egw_info']['apps']) && 400 array_key_exists('stylite', $GLOBALS['egw_info']['apps']); 401 402 } 403 404 public function is_entry($share = false) 405 { 406 list($app, $id) = explode('::', $share['share_path']); 407 return $share && $share['share_path'] && 408 $app && $id && !in_array($app, array('filemanager', 'vfs')) ;//&& array_key_exists($app, $GLOBALS['egw_info']['apps']); 409 } 410 411 public function need_session() 412 { 413 return $this->use_filemanager() || static::is_entry($this->session); 414 } 415 416 /** 417 * Get actions for sharing an entry from the given app 418 * 419 * @param string $appname 420 * @param int $group Current menu group 421 */ 422 public static function get_actions($appname, $group = 6) 423 { 424 Translation::add_app('api'); 425 $actions = array( 426 'share' => array( 427 'caption' => lang('Share'), 428 'icon' => 'api/share', 429 'group' => $group, 430 'allowOnMultiple' => false, 431 'children' => array( 432 'shareReadonlyLink' => array( 433 'caption' => lang('Share link'), 434 'group' => 1, 435 'icon' => 'link', 436 'order' => 11, 437 'enabled' => "javaScript:app.$appname.is_share_enabled", 438 'onExecute' => "javaScript:app.$appname.share_link", 439 'hint' => lang("Share this %1 via URL", Link::get_registry($appname, 'entry')) 440 ), 441 'shareWritable' => array( 442 'caption' => lang('Writable'), 443 'group' => 2, 444 'icon' => 'edit', 445 'allowOnMultiple' => true, 446 'enabled' => "javaScript:app.$appname.is_share_enabled", 447 'checkbox' => true, 448 'hint' => lang("Allow editing the %1", Link::get_registry($appname, 'entry')) 449 ), 450 'shareFiles' => array( 451 'caption' => lang('Share files'), 452 'group' => 2, 453 'allowOnMultiple' => true, 454 'enabled' => "javaScript:app.$appname.is_share_enabled", 455 'checkbox' => true, 456 'hint' => lang('Include access to any linked files (Links tab)') 457 ), 458 'shareFilemanager' => array( 459 'caption' => lang('share filemanager directory'), 460 'group' => 10, 461 'icon' => 'link', 462 'order' => 20, 463 'enabled' => "javaScript:app.$appname.is_share_enabled", 464 'onExecute' => "javaScript:app.$appname.share_link", 465 'hint' => lang('Share just the associated filemanager directory, not the %1', Link::get_registry($appname, 'entry')) 466 ), 467 ), 468 )); 469 if(!$GLOBALS['egw_info']['user']['apps']['filemanager']) 470 { 471 unset($actions['share']['children']['shareFilemanager']); 472 } 473 if(!$GLOBALS['egw_info']['user']['apps']['stylite']) 474 { 475 array_unshift($actions['share']['children'], array( 476 'caption' => lang('EPL Only'), 477 'group' => 0 478 )); 479 foreach($actions['share']['children'] as &$child) 480 { 481 $child['enabled'] = false; 482 } 483 } 484 return $actions; 485 } 486 487 /** 488 * Serve a request on a share specified in REQUEST_URI 489 */ 490 public function ServeRequest() 491 { 492 // sharing is for a different share, change to current share 493 if ($this->share['share_token'] !== self::get_token()) 494 { 495 // to keep the session we require the regular user flag "N" AND a user-name not equal to "anonymous" 496 self::create_session($GLOBALS['egw']->session->session_flags === 'N' && 497 $GLOBALS['egw_info']['user']['account_lid'] !== 'anonymous'); 498 499 return $GLOBALS['egw']->sharing->ServeRequest(); 500 } 501 502 // No extended ACL for readonly shares, disable eacl by setting session cache 503 if(!($this->share['share_writable'] & 1)) 504 { 505 Cache::setSession(Vfs\Sqlfs\StreamWrapper::EACL_APPNAME, 'extended_acl', array( 506 '/' => 1, 507 $this->share['share_path'] => 1 508 )); 509 } 510 if($this->use_collabora()) 511 { 512 $ui = new \EGroupware\Collabora\Ui(); 513 return $ui->editor($this->share['share_path']); 514 } 515 // use pure WebDAV for everything but GET requests to directories 516 else if (!$this->use_filemanager() && !static::is_entry($this->share)) 517 { 518 // send a content-disposition header, so browser knows how to name downloaded file 519 if (!Vfs::is_dir($this->share['share_root'])) 520 { 521 Header\Content::disposition(Vfs::basename($this->share['share_path']), false); 522 } 523 $GLOBALS['egw']->session->commit_session(); 524 525 // WebDAV always looks at the original request for a single file so make sure the file is found at the root 526 Vfs::$is_root = true; 527 unset($GLOBALS['egw_info']['server']['vfs_fstab']); 528 Vfs::mount($this->share['resolve_url'], '/', false, false, true); 529 Vfs::clearstatcache(); 530 531 $webdav_server = new Vfs\WebDAV(); 532 $webdav_server->ServeRequest(Vfs::concat('/', $this->share['share_token'])); 533 return; 534 } 535 return $this->get_ui(); 536 } 537 538 /** 539 * Get the user interface for this share 540 * 541 */ 542 public function get_ui() 543 { 544 echo 'Error: missing subclass'; 545 } 546 547 /** 548 * Generate a new token 549 * 550 * @return string 551 */ 552 public static function token() 553 { 554 // generate random token (using oppenssl if available otherwise mt_rand based Api\Auth::randomstring) 555 do { 556 $token = function_exists('openssl_random_pseudo_bytes') ? 557 base64_encode(openssl_random_pseudo_bytes(3*self::TOKEN_LENGTH/4)) : 558 Auth::randomstring(self::TOKEN_LENGTH); 559 // base64 can contain chars not allowed in our vfs-urls eg. / or # 560 } while ($token != urlencode($token)); 561 562 return $token; 563 } 564 565 /** 566 * Name of the async job for cleaning up shares 567 */ 568 const ASYNC_JOB_ID = 'egw_sharing-tmp_cleanup'; 569 570 /** 571 * Create a new share 572 * 573 * @param string $action_id Specific type of share being created, default '' 574 * @param string $path either path in temp_dir or vfs with optional vfs scheme 575 * @param string $mode self::LINK: copy file in users tmp-dir or self::READABLE share given vfs file, 576 * if no vfs behave as self::LINK 577 * @param string $name filename to use for $mode==self::LINK, default basename of $path 578 * @param string|array $recipients one or more recipient email addresses 579 * @param array $extra =array() extra data to store 580 * @return array with share data, eg. value for key 'share_token' 581 * @throw Api\Exception\NotFound if $path not found 582 * @throw Api\Exception\AssertionFailed if user temp. directory does not exist and can not be created 583 */ 584 public static function create(string $action_id, $path, $mode, $name, $recipients, $extra = array()) 585 { 586 if (!isset(static::$db)) static::$db = $GLOBALS['egw']->db; 587 588 if (empty($name)) $name = $path; 589 590 $table_def = static::$db->get_table_definitions(Db::API_APPNAME,static::TABLE); 591 $extra = array_intersect_key($extra, $table_def['fd']); 592 593 // Check if path is mounted somewhere that needs a password 594 static::path_needs_password($path); 595 596 // check if file has been shared before, with identical attributes 597 if (($share = static::$db->select(static::TABLE, '*', $extra+array( 598 'share_path' => $path, 599 'share_owner' => Vfs::$user, 600 'share_expires' => null, 601 'share_passwd' => null, 602 'share_writable'=> false, 603 ), __LINE__, __FILE__, Db::API_APPNAME)->fetch())) 604 { 605 // if yes, just add additional recipients 606 $share['share_with'] = $share['share_with'] ? explode(',', $share['share_with']) : array(); 607 $need_save = false; 608 foreach((array)$recipients as $recipient) 609 { 610 if (!in_array($recipient, $share['share_with'])) 611 { 612 $share['share_with'][] = $recipient; 613 $need_save = true; 614 } 615 } 616 $share['share_with'] = implode(',', $share['share_with']); 617 if ($need_save) 618 { 619 static::$db->update(static::TABLE, array( 620 'share_with' => $share['share_with'], 621 ), array( 622 'share_id' => $share['share_id'], 623 ), __LINE__, __FILE__, Db::API_APPNAME); 624 } 625 } 626 else 627 { 628 $i = 0; 629 while(true) // self::token() can return an existing value 630 { 631 try { 632 static::$db->insert(static::TABLE, $share = array( 633 'share_token' => self::token(), 634 'share_path' => $path, 635 'share_owner' => Vfs::$user, 636 'share_with' => implode(',', (array)$recipients), 637 'share_created' => time(), 638 )+$extra, false, __LINE__, __FILE__, Db::API_APPNAME); 639 640 $share['share_id'] = static::$db->get_last_insert_id(static::TABLE, 'share_id'); 641 break; 642 } 643 catch(Db\Exception $e) { 644 if ($i++ > 3) throw $e; 645 unset($e); 646 } 647 } 648 } 649 650 // if not already installed, install periodic cleanup of shares 651 $async = new Asyncservice(); 652 $method = 'EGroupware\\Api\\Sharing::tmp_cleanup'; 653 if (!($job = $async->read(self::ASYNC_JOB_ID)) || $job[self::ASYNC_JOB_ID]['method'] !== $method) 654 { 655 if ($job) $async->delete(self::ASYNC_JOB_ID); // update not working old class-name 656 657 $async->set_timer(array('day' => 28), self::ASYNC_JOB_ID, $method ,null); 658 } 659 660 return $share; 661 } 662 663 /** 664 * Create a share via AJAX 665 * 666 * @param String $action 667 * @param String $path 668 * @param boolean $writable Allow editing the shared entry / folder / file 669 * @param boolean $files For sharing an application entry, allow access to the linked files 670 * @param $extra Additional extra parameters 671 */ 672 public static function ajax_create($action, $path, $writable = false, $files = false, $extra = array()) 673 { 674 if(!$path) 675 { 676 throw new Exception\WrongParameter('Missing share path. Unable to create share.'); 677 } 678 $extra = (array)$extra + array( 679 'share_writable' => $writable, 680 'include_files' => $files 681 ); 682 $class = self::get_share_class(array('share_path' => $path) + $extra); 683 $share = $class::create( 684 $action, 685 $path, 686 $writable ? Sharing::WRITABLE : Sharing::READONLY, 687 basename($path), 688 array(), 689 $extra 690 ); 691 692 // Store share in session so Merge can find this one and not create a read-only one 693 \EGroupware\Api\Cache::setSession(__CLASS__, $path, $share); 694 $arr = array( 695 'action' => $action, 696 'writable' => $writable, 697 'share_link' => $class::share2link($share), 698 'template' => Etemplate\Widget\Template::rel2url('/filemanager/templates/default/share_dialog.xet') 699 ); 700 switch($action) 701 { 702 case 'shareFilemanager': 703 $arr['title'] = lang('Filemanager directory'); 704 break; 705 case 'shareUploadDir': 706 case 'mail_shareUploadDir': 707 $arr['title'] = lang('Upload directory'); 708 break; 709 } 710 $response = Json\Response::get(); 711 $response->data($arr); 712 } 713 714 /** 715 * Api\Storage\Base instance for egw_sharing table 716 * 717 * @var Api\Storage\Base 718 */ 719 protected static $so; 720 721 /** 722 * Get a so_sql instance initialised for shares 723 */ 724 public static function so() 725 { 726 if (!isset(self::$so)) 727 { 728 self::$so = new Storage\Base('phpgwapi', self::TABLE, null, '', true); 729 self::$so->set_times('string'); 730 } 731 return self::$so; 732 } 733 734 /** 735 * Delete specified shares and unlink temp. files 736 * 737 * @param int|array $keys 738 * @return int number of deleted shares 739 */ 740 public static function delete($keys) 741 { 742 self::$db = $GLOBALS['egw']->db; 743 744 if (is_scalar($keys)) $keys = array('share_id' => $keys); 745 746 // delete specified shares 747 self::$db->delete(self::TABLE, $keys, __LINE__, __FILE__, Db::API_APPNAME); 748 $deleted = self::$db->affected_rows(); 749 750 return $deleted; 751 } 752 753 /** 754 * Home long to keep temp. files: 100 day 755 */ 756 const TMP_KEEP = 8640000; 757 /** 758 * How long to keep automatic created Wopi shares 759 */ 760 const WOPI_KEEP = '-3month'; 761 762 /**. 763 * Periodic (monthly) cleanup of temporary sharing files (download link) 764 * 765 * Exlicit expireds shares are delete, as ones created over 100 days ago and last accessed over 100 days ago. 766 */ 767 public static function tmp_cleanup() 768 { 769 if (!isset(self::$db)) self::$db = $GLOBALS['egw']->db; 770 Vfs::$is_root = true; 771 772 try { 773 $cols = array( 774 'share_path', 775 'MAX(share_expires) AS share_expires', 776 'MAX(share_created) AS share_created', 777 'MAX(share_last_accessed) AS share_last_accessed', 778 ); 779 if (($group_concat = self::$db->group_concat('share_id'))) $cols[] = $group_concat.' AS share_id'; 780 // remove expired tmp-files unconditionally 781 $having = 'HAVING MAX(share_expires) < '.self::$db->quote(self::$db->to_timestamp(time())).' OR '. 782 // remove without expiration date, when created over 100 days ago AND 783 'MAX(share_expires) IS NULL AND MAX(share_created) < '.self::$db->quote(self::$db->to_timestamp(time()-self::TMP_KEEP)). ' AND '. 784 // (last accessed over 100 days ago OR never) 785 '(MAX(share_last_accessed) IS NULL OR MAX(share_last_accessed) < '.self::$db->quote(self::$db->to_timestamp(time()-self::TMP_KEEP)).')'; 786 787 foreach(self::$db->select(self::TABLE, $cols, array( 788 "share_path LIKE '/home/%/.tmp/%'", 789 ), __LINE__, __FILE__, false, 'GROUP BY share_path '.$having) as $row) 790 { 791 Vfs::remove($row['share_path']); 792 793 if ($group_concat) 794 { 795 $share_ids = $row['share_id'] ? explode(',', $row['share_id']) : array(); 796 } 797 else 798 { 799 $share_ids = array(); 800 foreach(self::$db->select(self::TABLE, 'share_id', array( 801 'share_path' => $row['share_path'], 802 ), __LINE__, __FILE__) as $id) 803 { 804 $share_ids[] = $id['share_id']; 805 } 806 } 807 if ($share_ids) 808 { 809 $class = self::get_share_class($row); 810 $class::delete(['share_id' => $share_ids]); 811 } 812 } 813 814 // delete automatic created and expired Collabora shares older then 3 month 815 if (class_exists('EGroupware\\Collabora\\Wopi')) 816 { 817 self::$db->delete(self::TABLE, array( 818 'share_expires < '.self::$db->quote(DateTime::to(self::WOPI_KEEP, 'Y-m-d')), 819 'share_writable IN ('.\EGroupware\Collabora\Wopi::WOPI_WRITABLE.','.\EGroupware\Collabora\Wopi::WOPI_READONLY.')', 820 ), __LINE__, __FILE__); 821 } 822 823 // Now check the remaining shares 824 static::cleanup_missing_paths(); 825 } 826 catch (\Exception $e) { 827 _egw_log_exception($e); 828 } 829 Vfs::$is_root = false; 830 } 831 832 /** 833 * Check share paths and if the path is no longer there / valid, remove the share 834 */ 835 public static function cleanup_missing_paths() 836 { 837 if (!isset(self::$db)) self::$db = $GLOBALS['egw']->db; 838 839 foreach(self::$db->select(self::TABLE, array( 840 'share_id','share_path', 'share_writable' 841 ), array(), __LINE__, __FILE__, false) as $share) 842 { 843 $class = self::get_share_class($share); 844 845 if(!$class::check_path($share)) 846 { 847 $class::delete($share); 848 } 849 } 850 } 851 852 /** 853 * Check that the share path is still valid, and if not, delete it. 854 * This should be overridden. 855 * 856 * @param Array share 857 * 858 * @return boolean Is the share still valid 859 */ 860 protected static function check_path($share) 861 { 862 return true; 863 } 864 865 /** 866 * Generate link from share or share-token 867 * 868 * @param string|array $share share or share-token 869 * @return string full Url incl. schema and host 870 */ 871 public static function share2link($share) 872 { 873 if (is_array($share)) $share = $share['share_token']; 874 875 return Framework::getUrl(Framework::link('/share.php')).'/'.$share; 876 } 877 878 /** 879 * Check to see if the path has a password required for it's mounting (eg: Samba) 880 * we need to deal with it specially. In general, we just throw an exception 881 * if the mount has $pass in it. 882 * 883 * @param string $path 884 * 885 * @throws WrongParameter if you try to share a path that needs a password 886 */ 887 public static function path_needs_password($path) 888 { 889 $mounts = array_reverse(Vfs::mount()); 890 $parts = Vfs::parse_url($path); 891 892 foreach($mounts as $mounted => $url) 893 { 894 if(($mounted == $parts['path'] || $mounted.'/' == substr($parts['path'],0,strlen($mounted)+1)) && strpos($url, '$pass') !== FALSE) 895 { 896 throw new Exception\WrongParameter( 897 'Cannot share a file that needs a password. (' . 898 $path . ' mounted from '. $url . ')' 899 ); 900 } 901 } 902 903 return false; 904 } 905}