1<?php 2/** 3 * EGroupware - Filemanager - user interface 4 * 5 * @link http://www.egroupware.org 6 * @package filemanager 7 * @author Ralf Becker <RalfBecker-AT-outdoor-training.de> 8 * @copyright (c) 2008-17 by Ralf Becker <RalfBecker-AT-outdoor-training.de> 9 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License 10 * @version $Id$ 11 */ 12 13use EGroupware\Api; 14use EGroupware\Api\Egw; 15use EGroupware\Api\Etemplate; 16use EGroupware\Api\Framework; 17use EGroupware\Api\Link; 18use EGroupware\Api\Vfs; 19 20/** 21 * Filemanage user interface class 22 */ 23class filemanager_ui 24{ 25 /** 26 * Methods callable via menuaction 27 * 28 * @var array 29 */ 30 var $public_functions = array( 31 'index' => true, 32 'file' => true, 33 'editor' => true 34 ); 35 36 /** 37 * Views available from plugins 38 * 39 * @var array 40 */ 41 public static $views = array( 42 'filemanager_ui::listview' => 'Listview', 43 ); 44 public static $views_init = false; 45 46 /** 47 * vfs namespace for document merge properties 48 * 49 */ 50 public static $merge_prop_namespace = ''; 51 protected $etemplate; 52 const LIST_TEMPLATE = 'filemanager.index'; 53 54 /** 55 * Constructor 56 * 57 */ 58 function __construct() 59 { 60 // strip slashes from _GET parameters, if someone still has magic_quotes_gpc on 61 if (get_magic_quotes_gpc() && $_GET) 62 { 63 $_GET = array_stripslashes($_GET); 64 } 65 // do we have root rights 66 if (Api\Cache::getSession('filemanager', 'is_root')) 67 { 68 Vfs::$is_root = true; 69 } 70 71 static::init_views(); 72 static::$merge_prop_namespace = Vfs::DEFAULT_PROP_NAMESPACE.$GLOBALS['egw_info']['flags']['currentapp']; 73 } 74 75 /** 76 * Initialise and return available views 77 * 78 * @return array with method => label pairs 79 */ 80 public static function init_views() 81 { 82 if (!static::$views_init) 83 { 84 // translate our labels 85 foreach(static::$views as &$label) 86 { 87 $label = lang($label); 88 } 89 // search for plugins with additional filemanager views 90 foreach(Api\Hooks::process('filemanager_views') as $views) 91 { 92 if (is_array($views)) static::$views += $views; 93 } 94 static::$views_init = true; 95 } 96 return static::$views; 97 } 98 99 /** 100 * Get active view 101 * 102 * @return string 103 */ 104 public static function get_view() 105 { 106 $view =& Api\Cache::getSession('filemanager', 'view'); 107 if (isset($_GET['view'])) 108 { 109 $view = $_GET['view']; 110 } 111 if (!isset(static::$views[$view])) 112 { 113 reset(static::$views); 114 $view = key(static::$views); 115 } 116 return $view; 117 } 118 119 /** 120 * Method to build select options out of actions 121 * @param type $actions 122 * @return type 123 */ 124 public static function convertActionsToselOptions ($actions) 125 { 126 $sel_options = array (); 127 foreach ($actions as $action => $value) 128 { 129 $sel_options[$action] = array ( 130 'label' => $value['caption'], 131 'icon' => $value['icon'] 132 ); 133 } 134 return $sel_options; 135 } 136 137 /** 138 * Context menu 139 * 140 * @return array 141 */ 142 public static function get_actions() 143 { 144 $actions = array( 145 'open' => array( 146 'caption' => lang('Open'), 147 'icon' => '', 148 'group' => $group=1, 149 'allowOnMultiple' => false, 150 'onExecute' => 'javaScript:app.filemanager.open', 151 'default' => true 152 ), 153 'new' => array( 154 'caption' => 'New', 155 'group' => $group, 156 'disableClass' => 'noEdit', 157 'children' => array ( 158 'document' => array ( 159 'caption' => 'Document', 160 'icon' => 'new', 161 'onExecute' => 'javaScript:app.filemanager.create_new', 162 ) 163 ) 164 ), 165 'mkdir' => array( 166 'caption' => lang('Create directory'), 167 'icon' => 'filemanager/button_createdir', 168 'group' => $group, 169 'allowOnMultiple' => false, 170 'disableClass' => 'noEdit', 171 'onExecute' => 'javaScript:app.filemanager.createdir' 172 ), 173 'edit' => array( 174 'caption' => lang('Edit settings'), 175 'group' => $group, 176 'allowOnMultiple' => false, 177 'onExecute' => Api\Header\UserAgent::mobile()?'javaScript:app.filemanager.viewEntry':'javaScript:app.filemanager.editprefs', 178 'mobileViewTemplate' => 'file?'.filemtime(Api\Etemplate\Widget\Template::rel2path('/filemanager/templates/mobile/file.xet')) 179 ), 180 'saveas' => array( 181 'caption' => lang('Save as'), 182 'group' => $group, 183 'allowOnMultiple' => true, 184 'icon' => 'filesave', 185 'onExecute' => 'javaScript:app.filemanager.force_download', 186 'disableClass' => 'isDir', 187 'enabled' => 'javaScript:app.filemanager.is_multiple_allowed', 188 'shortcut' => array('ctrl' => true, 'shift' => true, 'keyCode' => 83, 'caption' => 'Ctrl + Shift + S'), 189 ), 190 'saveaszip' => array( 191 'caption' => lang('Save as ZIP'), 192 'group' => $group, 193 'allowOnMultiple' => true, 194 'icon' => 'save_zip', 195 'postSubmit' => true, 196 'shortcut' => array('ctrl' => true, 'shift' => true, 'keyCode' => 90, 'caption' => 'Ctrl + Shift + Z'), 197 ), 198 'egw_paste' => array( 199 'enabled' => false, 200 'group' => $group + 0.5, 201 'hideOnDisabled' => true 202 ), 203 'paste' => array( 204 'caption' => lang('Paste'), 205 'acceptedTypes' => 'file', 206 'group' => $group + 0.5, 207 'order' => 10, 208 'enabled' => 'javaScript:app.filemanager.paste_enabled', 209 'children' => array() 210 ), 211 'copylink' => array( 212 'caption' => lang('Copy link address'), 213 'group' => $group + 0.5, 214 'icon' => 'copy', 215 'allowOnMultiple' => false, 216 'order' => 10, 217 'onExecute' => 'javaScript:app.filemanager.copy_link' 218 ), 219 'share' => EGroupware\Api\Vfs\HiddenUploadSharing::get_actions('filemanager', ++$group)['share'], 220 'documents' => filemanager_merge::document_action( 221 $GLOBALS['egw_info']['user']['preferences']['filemanager']['document_dir'], 222 ++$group, 'Insert in document', 'document_', 223 $GLOBALS['egw_info']['user']['preferences']['filemanager']['default_document'] 224 ), 225 'delete' => array( 226 'caption' => lang('Delete'), 227 'group' => ++$group, 228 'confirm' => 'Delete these files or directories?', 229 'onExecute' => 'javaScript:app.filemanager.action', 230 'disableClass' => 'noDelete' 231 ), 232 // DRAG and DROP events 233 'file_drag' => array( 234 'dragType' => array('file','link'), 235 'type' => 'drag', 236 'onExecute' => 'javaScript:app.filemanager.drag' 237 ), 238 'file_drop_mail' => array( 239 'type' => 'drop', 240 'acceptedTypes' => 'mail', 241 'onExecute' => 'javaScript:app.filemanager.drop', 242 'hideOnDisabled' => true 243 ), 244 'file_drop_move' => array( 245 'icon' => 'stylite/move', 246 'acceptedTypes' => 'file', 247 'caption' => lang('Move into folder'), 248 'type' => 'drop', 249 'onExecute' => 'javaScript:app.filemanager.drop', 250 'default' => true 251 ), 252 'file_drop_copy' => array( 253 'icon' => 'stylite/copy', 254 'acceptedTypes' => 'file', 255 'caption' => lang('Copy into folder'), 256 'type' => 'drop', 257 'onExecute' => 'javaScript:app.filemanager.drop' 258 ), 259 'file_drop_symlink' => array( 260 'icon' => 'linkpaste', 261 'acceptedTypes' => 'file', 262 'caption' => lang('Link into folder'), 263 'type' => 'drop', 264 'onExecute' => 'javaScript:app.filemanager.drop' 265 ) 266 ); 267 268 // This one makes no sense in filemanager 269 unset($actions['share']['children']['shareFilemanager']); 270 if (isset($GLOBALS['egw_info']['user']['apps']['mail'])) { 271 $actions['share']['children']['share_mail'] = array( 272 'caption' => lang('Mail'), 273 'icon' => 'mail', 274 'group' => 1, 275 'order' => 0, 276 'allowOnMultiple' => true, 277 ); 278 foreach(Vfs\Sharing::$modes as $mode => $data) 279 { 280 $actions['share']['children']['share_mail']['children']['mail_'.$mode] = array( 281 'caption' => $data['label'], 282 'hint' => $data['title'], 283 'icon' => $mode == Vfs\Sharing::ATTACH ? 284 'mail/attach' : 'api/link', 285 'group' => 2, 286 'onExecute' => 'javaScript:app.filemanager.mail', 287 ); 288 if ($mode == Vfs\Sharing::ATTACH || $mode == Vfs\Sharing::LINK) 289 { 290 $actions['share']['children']['share_mail']['children']['mail_'.$mode]['disableClass'] = 'isDir'; 291 } 292 } 293 foreach(Vfs\HiddenUploadSharing::$modes as $mode => $data) 294 { 295 $actions['share']['children']['share_mail']['children']['mail_shareUploadDir'] = array( 296 'caption' => $data['label'], 297 'hint' => $data['title'], 298 'icon' => 'api/link', 299 'group' => 3, 300 'data' => ['share_writable' => $mode], 301 'enabled' => 'javaScript:app.filemanager.hidden_upload_enabled', 302 'onExecute' => 'javaScript:app.filemanager.mail_share_link', 303 ); 304 } 305 } 306 307 // This would be done automatically, but we're overriding 308 foreach($actions as $action_id => $action) 309 { 310 if($action['type'] == 'drop' && $action['caption']) 311 { 312 $action['type'] = 'popup'; 313 if($action['acceptedTypes'] == 'file') 314 { 315 $action['enabled'] = 'javaScript:app.filemanager.paste_enabled'; 316 } 317 $actions['paste']['children']["{$action_id}_paste"] = $action; 318 } 319 } 320 return $actions; 321 } 322 323 /** 324 * Get mergeapp property for given path 325 * 326 * @param string $path 327 * @param string $scope (default) or 'parents' 328 * $scope == 'self' query only the given path 329 * $scope == 'parents' query only path parents for property (first parent in hierarchy upwards wins) 330 * 331 * @return string merge application or NULL if no property found 332 */ 333 private static function get_mergeapp($path, $scope='self') 334 { 335 $app = null; 336 switch($scope) 337 { 338 case 'self': 339 $props = Vfs::propfind($path, static::$merge_prop_namespace); 340 $app = empty($props) ? null : $props[0]['val']; 341 break; 342 case 'parents': 343 // search for props in parent directories 344 $currentpath = $path; 345 while($dir = Vfs::dirname($currentpath)) 346 { 347 $props = Vfs::propfind($dir, static::$merge_prop_namespace); 348 if(!empty($props)) 349 { 350 // found prop in parent directory 351 return $app = $props[0]['val']; 352 } 353 $currentpath = $dir; 354 } 355 break; 356 } 357 358 return $app; 359 } 360 361 /** 362 * Main filemanager page 363 * 364 * @param array $content 365 * @param string $msg 366 */ 367 function index(array $content=null,$msg=null) 368 { 369 if (!is_array($content)) 370 { 371 $content = array( 372 'nm' => Api\Cache::getSession('filemanager', 'index'), 373 ); 374 if (!is_array($content['nm'])) 375 { 376 $content['nm'] = array( 377 'get_rows' => 'filemanager.filemanager_ui.get_rows', // I method/callback to request the data for the rows eg. 'notes.bo.get_rows' 378 'filter' => '', // current dir only 379 'no_filter2' => True, // I disable the 2. filter (params are the same as for filter) 380 'no_cat' => True, // I disable the cat-selectbox 381 'lettersearch' => True, // I show a lettersearch 382 'searchletter' => false, // I0 active letter of the lettersearch or false for [all] 383 'start' => 0, // IO position in list 384 'order' => 'name', // IO name of the column to sort after (optional for the sortheaders) 385 'sort' => 'ASC', // IO direction of the sort: 'ASC' or 'DESC' 386 'default_cols' => '!comment,ctime', // I columns to use if there's no user or default pref (! as first char uses all but the named columns), default all columns 387 'csv_fields' => false, // I false=disable csv export, true or unset=enable it with auto-detected fieldnames, 388 //or array with name=>label or name=>array('label'=>label,'type'=>type) pairs (type is a eT widget-type) 389 'row_id' => 'path', 390 'row_modified' => 'mtime', 391 'parent_id' => 'dir', 392 'is_parent' => 'is_dir', 393 'favorites' => true 394 ); 395 $content['nm']['path'] = static::get_home_dir(); 396 } 397 $content['nm']['actions'] = static::get_actions(); 398 $content['nm']['home_dir'] = static::get_home_dir(); 399 $content['nm']['view'] = $GLOBALS['egw_info']['user']['preferences']['filemanager']['nm_view']; 400 $content['nm']['placeholder_actions'] = array('mkdir','paste','share','file_drop_mail','file_drop_move','file_drop_copy','file_drop_symlink'); 401 402 if (isset($_GET['msg'])) $msg = $_GET['msg']; 403 404 // Blank favorite set via GET needs special handling for path 405 if (isset($_GET['favorite']) && $_GET['favorite'] == 'blank') 406 { 407 $content['nm']['path'] = static::get_home_dir(); 408 } 409 // switch to projectmanager folders 410 if (isset($_GET['pm_id'])) 411 { 412 $_GET['path'] = '/apps/projectmanager'.((int)$_GET['pm_id'] ? '/'.(int)$_GET['pm_id'] : ''); 413 } 414 if (isset($_GET['path']) && ($path = $_GET['path'])) 415 { 416 switch($path) 417 { 418 case '..': 419 $path = Vfs::dirname($content['nm']['path']); 420 break; 421 case '~': 422 $path = static::get_home_dir(); 423 break; 424 } 425 if ($path && $path[0] == '/' && Vfs::stat($path,true) && Vfs::is_dir($path) && Vfs::check_access($path,Vfs::READABLE)) 426 { 427 $content['nm']['path'] = $path; 428 } 429 else 430 { 431 $msg .= lang('The requested path %1 is not available.', $path ? Vfs::decodePath($path) : "false"); 432 } 433 // reset lettersearch as it confuses users (they think the dir is empty) 434 $content['nm']['searchletter'] = false; 435 // switch recusive display off 436 if (!$content['nm']['filter']) $content['nm']['filter'] = ''; 437 } 438 } 439 $view = static::get_view(); 440 441 call_user_func($view,$content,$msg); 442 } 443 444 /** 445 * Make the current user (vfs) root 446 * 447 * The user/pw is either the setup config user or a specially configured vfs_root user 448 * 449 * @param string $user setup config user to become root or '' to log off as root 450 * @param string $password setup config password to become root 451 * @param boolean &$is_setup=null on return true if authenticated user is setup config user, false otherwise 452 * @return boolean true is root user given, false otherwise (including logout / empty $user) 453 */ 454 protected function sudo($user='',$password=null,&$is_setup=null) 455 { 456 if (!$user) 457 { 458 $is_root = $is_setup = false; 459 } 460 else 461 { 462 // config user & password 463 $is_setup = Api\Session::user_pw_hash($user,$password) === $GLOBALS['egw_info']['server']['config_hash']; 464 // or vfs root user from setup >> configuration 465 $is_root = $is_setup || $GLOBALS['egw_info']['server']['vfs_root_user'] && 466 in_array($user,preg_split('/, */',$GLOBALS['egw_info']['server']['vfs_root_user'])) && 467 $GLOBALS['egw']->auth->authenticate($user, $password, 'text'); 468 } 469 //error_log(__METHOD__."('$user','$password',$is_setup) user_pw_hash(...)='".Api\Session::user_pw_hash($user,$password)."', config_hash='{$GLOBALS['egw_info']['server']['config_hash']}' --> returning ".array2string($is_root)); 470 Api\Cache::setSession('filemanager', 'is_setup',$is_setup); 471 Api\Cache::setSession('filemanager', 'is_root',Vfs::$is_root = $is_root); 472 return Vfs::$is_root; 473 } 474 475 /** 476 * Filemanager listview 477 * 478 * @param array $content 479 * @param string $msg 480 */ 481 function listview(array $content=null,$msg=null) 482 { 483 $tpl = $this->etemplate ? $this->etemplate : new Etemplate(static::LIST_TEMPLATE); 484 485 if ($msg) 486 { 487 Framework::message($msg); 488 } 489 490 if (($content['nm']['action'] || $content['nm']['rows']) && (empty($content['button']) || !isset($content['button']))) 491 { 492 if ($content['nm']['action']) 493 { 494 $msg = static::action($content['nm']['action'], $content['nm']['selected'], $content['nm']['path']); 495 if ($msg) 496 { 497 Framework::message($msg); 498 } 499 500 // clean up after action 501 unset($content['nm']['selected']); 502 // reset any occasion where action may be stored, as it may be ressurected out of the helpers by etemplate, which is quite unconvenient in case of action delete 503 if (isset($content['nm']['action'])) 504 { 505 unset($content['nm']['action']); 506 } 507 if (isset($content['nm']['nm_action'])) 508 { 509 unset($content['nm']['nm_action']); 510 } 511 if (isset($content['nm_action'])) 512 { 513 unset($content['nm_action']); 514 } 515 // we dont use ['nm']['rows']['delete'], so unset it, if it is present 516 if (isset($content['nm']['rows']['delete'])) 517 { 518 unset($content['nm']['rows']['delete']); 519 } 520 } 521 elseif ($content['nm']['rows']['delete']) 522 { 523 $msg = static::action('delete', array_keys($content['nm']['rows']['delete']), $content['nm']['path']); 524 if ($msg) 525 { 526 Framework::message($msg); 527 } 528 529 // clean up after action 530 unset($content['nm']['rows']['delete']); 531 // reset any occasion where action may be stored, as we use ['nm']['rows']['delete'] anyhow 532 // we clean this up, as it may be ressurected out of the helpers by etemplate, which is quite unconvenient in case of action delete 533 if (isset($content['nm']['action'])) 534 { 535 unset($content['nm']['action']); 536 } 537 if (isset($content['nm']['nm_action'])) 538 { 539 unset($content['nm']['nm_action']); 540 } 541 if (isset($content['nm_action'])) 542 { 543 unset($content['nm_action']); 544 } 545 if (isset($content['nm']['selected'])) 546 { 547 unset($content['nm']['selected']); 548 } 549 } 550 unset($content['nm']['rows']); 551 Api\Cache::setSession('filemanager', 'index', $content['nm']); 552 } 553 554 // be tolerant with (in previous versions) not correct urlencoded pathes 555 if ($content['nm']['path'][0] == '/' && !Vfs::stat($content['nm']['path'], true) && Vfs::stat(urldecode($content['nm']['path']))) 556 { 557 $content['nm']['path'] = urldecode($content['nm']['path']); 558 } 559 if ($content['button']) 560 { 561 if ($content['button']) 562 { 563 $button = key($content['button']); 564 unset($content['button']); 565 } 566 switch ($button) 567 { 568 case 'upload': 569 if (!$content['upload']) 570 { 571 Framework::message(lang('You need to select some files first!'), 'error'); 572 break; 573 } 574 $upload_success = $upload_failure = array(); 575 foreach (isset($content['upload'][0]) ? $content['upload'] : array($content['upload']) as $upload) 576 { 577 // encode chars which special meaning in url/vfs (some like / get removed!) 578 $to = Vfs::concat($content['nm']['path'], Vfs::encodePathComponent($upload['name'])); 579 if ($upload && 580 (Vfs::is_writable($content['nm']['path']) || Vfs::is_writable($to)) && 581 copy($upload['tmp_name'], Vfs::PREFIX . $to)) 582 { 583 $upload_success[] = $upload['name']; 584 } 585 else 586 { 587 $upload_failure[] = $upload['name']; 588 } 589 } 590 $content['nm']['msg'] = ''; 591 if ($upload_success) 592 { 593 Framework::message(count($upload_success) == 1 && !$upload_failure ? lang('File successful uploaded.') : 594 lang('%1 successful uploaded.', implode(', ', $upload_success))); 595 } 596 if ($upload_failure) 597 { 598 Framework::message(lang('Error uploading file!') . "\n" . etemplate::max_upload_size_message(), 'error'); 599 } 600 break; 601 } 602 } 603 $readonlys['button[mailpaste]'] = !isset($GLOBALS['egw_info']['user']['apps']['mail']); 604 605 $sel_options['filter'] = array( 606 '' => 'Current directory', 607 '2' => 'Directories sorted in', 608 '3' => 'Show hidden files', 609 '4' => 'All subdirectories', 610 '5' => 'Files from links', 611 '0' => 'Files from subdirectories', 612 ); 613 614 $sel_options['new'] = self::convertActionsToselOptions($content['nm']['actions']['new']['children']); 615 616 // sharing has no divAppbox, we need to set popupMainDiv instead, to be able to drop files everywhere 617 if (substr($_SERVER['SCRIPT_FILENAME'], -10) == '/share.php') 618 { 619 $tpl->setElementAttribute('nm[upload]', 'drop_target', 'popupMainDiv'); 620 } 621 // Set view button to match current settings 622 if ($content['nm']['view'] == 'tile') 623 { 624 $tpl->setElementAttribute('nm[button][change_view]', 'statustext', lang('List view')); 625 $tpl->setElementAttribute('nm[button][change_view]', 'image', 'list_row'); 626 } 627 // if initial load is done via GET request (idots template or share.php) 628 // get_rows cant call app.filemanager.set_readonly, so we need to do that here 629 if (!array_key_exists('initial_path_readonly', $content)) 630 { 631 $content['initial_path_readonly'] = !Vfs::is_writable($content['nm']['path']); 632 } 633 634 $tpl->exec('filemanager.filemanager_ui.index',$content,$sel_options,$readonlys,array('nm' => $content['nm'])); 635 } 636 637 /** 638 * Get the configured start directory for the current user 639 * 640 * @return string 641 */ 642 static function get_home_dir() 643 { 644 $start = '/home/'.$GLOBALS['egw_info']['user']['account_lid']; 645 646 // check if user specified a valid startpath in his prefs --> use it 647 if (($path = $GLOBALS['egw_info']['user']['preferences']['filemanager']['startfolder']) && 648 $path[0] == '/' && Vfs::is_dir($path) && Vfs::check_access($path, Vfs::READABLE)) 649 { 650 $start = $path; 651 } 652 elseif (!Vfs::is_dir($start) && Vfs::check_access($start, Vfs::READABLE)) 653 { 654 $start = '/'; 655 } 656 return $start; 657 } 658 659 /** 660 * Run a certain action with the selected file 661 * 662 * @param string $action 663 * @param array $selected selected pathes 664 * @param mixed $dir current directory 665 * @param int &$errs=null on return number of errors 666 * @param int &$dirs=null on return number of dirs deleted 667 * @param int &$files=null on return number of files deleted 668 * @return string success or failure message displayed to the user 669 */ 670 static public function action($action,$selected,$dir=null,&$errs=null,&$files=null,&$dirs=null) 671 { 672 if (!count($selected)) 673 { 674 return lang('You need to select some files first!'); 675 } 676 $errs = $dirs = $files = 0; 677 678 switch($action) 679 { 680 681 case 'delete': 682 return static::do_delete($selected,$errs,$files,$dirs); 683 684 case 'mail': 685 case 'copy': 686 foreach($selected as $path) 687 { 688 if (strpos($path, 'mail::') === 0 && $path = substr($path, 6)) 689 { 690 // Support for dropping mail in filemanager - Pass mail back to mail app 691 if(ExecMethod2('mail.mail_ui.vfsSaveMessages', $path, $dir)) 692 { 693 ++$files; 694 } 695 else 696 { 697 ++$errs; 698 } 699 } 700 elseif (!Vfs::is_dir($path)) 701 { 702 $to = Vfs::concat($dir,Vfs::basename($path)); 703 if ($path != $to && Vfs::copy($path,$to)) 704 { 705 ++$files; 706 } 707 else 708 { 709 ++$errs; 710 } 711 } 712 else 713 { 714 $len = strlen(dirname($path)); 715 foreach(Vfs::find($path) as $p) 716 { 717 $to = $dir.substr($p,$len); 718 if ($to == $p) // cant copy into itself! 719 { 720 ++$errs; 721 continue; 722 } 723 if (($is_dir = Vfs::is_dir($p)) && Vfs::mkdir($to,null,STREAM_MKDIR_RECURSIVE)) 724 { 725 ++$dirs; 726 } 727 elseif(!$is_dir && Vfs::copy($p,$to)) 728 { 729 ++$files; 730 } 731 else 732 { 733 ++$errs; 734 } 735 } 736 } 737 } 738 if ($errs) 739 { 740 return lang('%1 errors copying (%2 diretories and %3 files copied)!',$errs,$dirs,$files); 741 } 742 return $dirs ? lang('%1 directories and %2 files copied.',$dirs,$files) : lang('%1 files copied.',$files); 743 744 case 'move': 745 foreach($selected as $path) 746 { 747 $to = Vfs::is_dir($dir) || count($selected) > 1 ? Vfs::concat($dir,Vfs::basename($path)) : $dir; 748 if ($path != $to && Vfs::rename($path,$to)) 749 { 750 ++$files; 751 } 752 else 753 { 754 ++$errs; 755 } 756 } 757 if ($errs) 758 { 759 return lang('%1 errors moving (%2 files moved)!',$errs,$files); 760 } 761 return lang('%1 files moved.',$files); 762 763 case 'symlink': // symlink given files to $dir 764 foreach((array)$selected as $target) 765 { 766 $link = Vfs::concat($dir, Vfs::basename($target)); 767 if (!Vfs::stat($dir) || ($ok = Vfs::mkdir($dir,0,true))) 768 { 769 if(!$ok) 770 { 771 $errs++; 772 continue; 773 } 774 } 775 if ($target[0] != '/') $target = Vfs::concat($dir, $target); 776 if (!Vfs::stat($target)) 777 { 778 return lang('Link target %1 not found!', Vfs::decodePath($target)); 779 } 780 if ($target != $link && Vfs::symlink($target, $link)) 781 { 782 ++$files; 783 } 784 else 785 { 786 ++$errs; 787 } 788 } 789 if (count((array)$selected) == 1) 790 { 791 return $files ? lang('Symlink to %1 created.', Vfs::decodePath($target)) : 792 lang('Error creating symlink to target %1!', Vfs::decodePath($target)); 793 } 794 $ret = lang('%1 elements linked.', $files); 795 if ($errs) 796 { 797 $ret = lang('%1 errors linking (%2)!',$errs, $ret); 798 } 799 return $ret;//." Vfs::symlink('$target', '$link')"; 800 801 case 'createdir': 802 $dst = Vfs::concat($dir, is_array($selected) ? $selected[0] : $selected); 803 if (Vfs::mkdir($dst, null, STREAM_MKDIR_RECURSIVE)) 804 { 805 return lang("Directory successfully created."); 806 } 807 return lang("Error while creating directory."); 808 809 case 'saveaszip': 810 Vfs::download_zip($selected); 811 exit; 812 813 default: 814 list($action, $settings) = explode('_', $action, 2); 815 switch($action) 816 { 817 case 'document': 818 if (!$settings) $settings = $GLOBALS['egw_info']['user']['preferences']['filemanager']['default_document']; 819 $document_merge = new filemanager_merge(Vfs::decodePath($dir)); 820 $msg = $document_merge->download($settings, $selected, '', $GLOBALS['egw_info']['user']['preferences']['filemanager']['document_dir']); 821 if($msg) return $msg; 822 $errs = count($selected); 823 return false; 824 } 825 } 826 return "Unknown action '$action'!"; 827 } 828 829 /** 830 * Delete selected files and return success or error message 831 * 832 * @param array $selected 833 * @param int &$errs=null on return number of errors 834 * @param int &$dirs=null on return number of dirs deleted 835 * @param int &$files=null on return number of files deleted 836 * @return string 837 */ 838 public static function do_delete(array $selected, &$errs=null, &$dirs=null, &$files=null) 839 { 840 $dirs = $files = $errs = 0; 841 // we first delete all selected links (and files) 842 // feeding the links to dirs to Vfs::find() deletes the content of the dirs, not just the link! 843 foreach($selected as $key => $path) 844 { 845 if (!Vfs::is_dir($path) || Vfs::is_link($path)) 846 { 847 if (Vfs::unlink($path)) 848 { 849 ++$files; 850 } 851 else 852 { 853 ++$errs; 854 } 855 unset($selected[$key]); 856 } 857 } 858 if ($selected) // somethings left to delete 859 { 860 // some precaution to never allow to (recursivly) remove /, /apps or /home 861 foreach((array)$selected as $path) 862 { 863 if (Vfs::isProtectedDir($path)) 864 { 865 $errs++; 866 return lang("Cautiously rejecting to remove folder '%1'!",Vfs::decodePath($path)); 867 } 868 } 869 // now we use find to loop through all files and dirs: (selected only contains dirs now) 870 // - depth=true to get first the files and then the dir containing it 871 // - hidden=true to also return hidden files (eg. Thumbs.db), as we cant delete non-empty dirs 872 // - show-deleted=false to not (finally) deleted versioned files 873 foreach(Vfs::find($selected,array('depth'=>true,'hidden'=>true,'show-deleted'=>false)) as $path) 874 { 875 if (($is_dir = Vfs::is_dir($path) && !Vfs::is_link($path)) && Vfs::rmdir($path,0)) 876 { 877 ++$dirs; 878 } 879 elseif (!$is_dir && Vfs::unlink($path)) 880 { 881 ++$files; 882 } 883 else 884 { 885 ++$errs; 886 } 887 } 888 } 889 if ($errs) 890 { 891 return lang('%1 errors deleteting (%2 directories and %3 files deleted)!',$errs,$dirs,$files); 892 } 893 if ($dirs) 894 { 895 return lang('%1 directories and %2 files deleted.',$dirs,$files); 896 } 897 return $files == 1 ? lang('File deleted.') : lang('%1 files deleted.',$files); 898 } 899 900 /** 901 * Callback to fetch the rows for the nextmatch widget 902 * 903 * @param array $query 904 * @param array &$rows 905 */ 906 function get_rows(&$query, &$rows) 907 { 908 $old_session = Api\Cache::getSession('filemanager','index'); 909 910 // do NOT store query, if hierarchical data / children are requested 911 if (!$query['csv_export']) 912 { 913 Api\Cache::setSession('filemanager', 'index', 914 array_diff_key ($query, array_flip(array('rows','actions','action_links','placeholder_actions')))); 915 } 916 if(!$query['path']) $query['path'] = static::get_home_dir(); 917 918 // Change template to match selected view 919 if($query['view']) 920 { 921 $query['template'] = ($query['view'] == 'row' ? 'filemanager.index.rows' : 'filemanager.tile'); 922 923 // Store as preference but only for index, not home 924 if($query['get_rows'] == 'filemanager.filemanager_ui.get_rows') 925 { 926 $GLOBALS['egw']->preferences->add('filemanager','nm_view',$query['view']); 927 $GLOBALS['egw']->preferences->save_repository(); 928 } 929 } 930 // be tolerant with (in previous versions) not correct urlencoded pathes 931 if (!Vfs::stat($query['path'],true) && Vfs::stat(urldecode($query['path']))) 932 { 933 $query['path'] = urldecode($query['path']); 934 } 935 if (!Vfs::stat($query['path'],true) || !Vfs::is_dir($query['path']) || !Vfs::check_access($query['path'],Vfs::READABLE)) 936 { 937 // only redirect, if it would be to some other location, gives redirect-loop otherwise 938 foreach([$old_session['path'], static::get_home_dir()] as $new_path) 939 { 940 if ($new_path && Vfs::stat($new_path) && $query['path'] != $new_path) 941 { 942 // we will leave here, since we are not allowed, or the location does not exist. Index must handle that, and give 943 // an appropriate message 944 Egw::redirect_link('/index.php', array('menuaction' => 'filemanager.filemanager_ui.index', 945 'path' => $new_path, 946 'msg' => lang('The requested path %1 is not available.', Vfs::decodePath($query['path'])), 947 'ajax' => 'true' 948 )); 949 break; 950 } 951 } 952 $rows = array(); 953 return 0; 954 } 955 $GLOBALS['egw']->session->commit_session(); 956 $rows = $dir_is_writable = array(); 957 $vfs_options = $this->get_vfs_options($query); 958 foreach(Vfs::find(!empty($query['col_filter']['dir']) ? $query['col_filter']['dir'] : $query['path'],$vfs_options,true) as $path => $row) 959 { 960 //echo $path; _debug_array($row); 961 962 $dir = dirname($path); 963 if (!isset($dir_is_writable[$dir])) 964 { 965 $dir_is_writable[$dir] = Vfs::is_writable($dir); 966 } 967 if (Vfs::is_dir($path)) 968 { 969 if (!isset($dir_is_writable[$path])) 970 { 971 $dir_is_writable[$path] = Vfs::is_writable($path); 972 } 973 974 $row['class'] .= 'isDir '; 975 $row['is_dir'] = 1; 976 if(!$dir_is_writable[$path]) 977 { 978 $row['class'] .= 'noEdit '; 979 } 980 if(!Vfs::is_executable($path)) 981 { 982 $row['class'] .= 'noExecute'; 983 } 984 } 985 elseif (!$dir_is_writable[Vfs::dirname($path)]) 986 { 987 $row['class'] .= 'noEdit '; 988 } 989 990 $row['class'] .= !$dir_is_writable[$dir] ? 'noDelete' : ''; 991 $row['download_url'] = Vfs::download_url($path); 992 $row['gid'] = -abs($row['gid']); // gid are positive, but we use negagive account_id for groups internal 993 994 foreach(['mtime','ctime'] as $date_field) 995 { 996 $row[$date_field] = Api\DateTime::server2user($row[$date_field]); 997 } 998 $rows[++$n] = $row; 999 $path2n[$path] = $n; 1000 } 1001 // query comments and cf's for the displayed rows 1002 $cols_to_show = explode(',',$GLOBALS['egw_info']['user']['preferences']['filemanager']['nextmatch-filemanager.index.rows']); 1003 1004 // Always include comment in tiles 1005 if($query['view'] == 'tile') 1006 { 1007 $cols_to_show[] = 'comment'; 1008 } 1009 $all_cfs = in_array('customfields',$cols_to_show) && $cols_to_show[count($cols_to_show)-1][0] != '#'; 1010 if ($path2n && (in_array('comment',$cols_to_show) || in_array('customfields',$cols_to_show)) && 1011 ($path2props = Vfs::propfind(array_keys($path2n)))) 1012 { 1013 foreach($path2props as $path => $props) 1014 { 1015 unset($row); // fixes a weird problem with php5.1, does NOT happen with php5.2 1016 $row =& $rows[$path2n[$path]]; 1017 if ( !is_array($props) ) continue; 1018 foreach($props as $prop) 1019 { 1020 if (!$all_cfs && $prop['name'][0] == '#' && !in_array($prop['name'],$cols_to_show)) continue; 1021 $row[$prop['name']] = strlen($prop['val']) < 64 ? $prop['val'] : substr($prop['val'],0,64).' ...'; 1022 } 1023 } 1024 } 1025 // tell client-side if directory is writeable or not 1026 $response = Api\Json\Response::get(); 1027 $response->call('app.filemanager.set_readonly', $query['path'], !Vfs::is_writable($query['path'])); 1028 1029 //_debug_array($readonlys); 1030 if ($GLOBALS['egw_info']['flags']['currentapp'] == 'projectmanager') 1031 { 1032 // we need our app.css file 1033 if (!file_exists(EGW_SERVER_ROOT.($css_file='/filemanager/templates/'.$GLOBALS['egw_info']['server']['template_set'].'/app.css'))) 1034 { 1035 $css_file = '/filemanager/templates/default/app.css'; 1036 } 1037 $GLOBALS['egw_info']['flags']['css'] .= "\n\t\t</style>\n\t\t".'<link href="'.$GLOBALS['egw_info']['server']['webserver_url']. 1038 $css_file.'?'.filemtime(EGW_SERVER_ROOT.$css_file).'" type="text/css" rel="StyleSheet" />'."\n\t\t<style>\n\t\t\t"; 1039 } 1040 return Vfs::$find_total; 1041 } 1042 1043 1044 /** 1045 * Get the VFS options (for get rows) 1046 */ 1047 protected function get_vfs_options($query) 1048 { 1049 if($query['searchletter'] && !empty($query['search'])) 1050 { 1051 $namefilter = '/^'.$query['searchletter'].'.*'.str_replace(array('\\?','\\*'),array('.{1}','.*'),preg_quote($query['search'])).'/i'; 1052 if ($query['searchletter'] == strtolower($query['search'][0])) 1053 { 1054 $namefilter = '/^('.$query['searchletter'].'.*'.str_replace(array('\\?','\\*'),array('.{1}','.*'),preg_quote($query['search'])).'|'. 1055 str_replace(array('\\?','\\*'),array('.{1}','.*'),preg_quote($query['search'])).')/i'; 1056 } 1057 } 1058 elseif ($query['searchletter']) 1059 { 1060 $namefilter = '/^'.$query['searchletter'].'/i'; 1061 } 1062 elseif(!empty($query['search'])) 1063 { 1064 $namefilter = '/'.str_replace(array('\\?','\\*'),array('.{1}','.*'),preg_quote($query['search'])).'/i'; 1065 } 1066 1067 // Re-map so 'No filters' favorite ('') is depth 1 1068 $filter = $query['filter'] === '' ? 1 : $query['filter']; 1069 1070 $maxdepth = $filter && $filter != 4 ? (int)(boolean)$filter : null; 1071 if($filter == 5) $maxdepth = 2; 1072 $n = 0; 1073 $vfs_options = array( 1074 'mindepth' => 1, 1075 'maxdepth' => $maxdepth, 1076 'dirsontop' => $filter <= 1, 1077 'type' => $filter && $filter != 5 ? ($filter == 4 ? 'd' : null) : ($filter == 5 ? 'F':'f'), 1078 'order' => $query['order'], 'sort' => $query['sort'], 1079 'limit' => (int)$query['num_rows'].','.(int)$query['start'], 1080 'need_mime' => true, 1081 'name_preg' => $namefilter, 1082 'hidden' => $filter == 3, 1083 'follow' => $filter == 5, 1084 ); 1085 if($query['col_filter']['mime']) 1086 { 1087 $vfs_options['mime'] = $query['col_filter']['mime']; 1088 } 1089 if($namefilter) 1090 { 1091 $vfs_options['name'] = $query['search']; 1092 } 1093 1094 return $vfs_options; 1095 } 1096 1097 /** 1098 * Preferences of a file/directory 1099 * 1100 * @param array $content 1101 * @param string $msg 1102 */ 1103 function file(array $content=null,$msg='') 1104 { 1105 $tpl = new Etemplate('filemanager.file'); 1106 1107 if (!is_array($content)) 1108 { 1109 if (isset($_GET['msg'])) 1110 { 1111 $msg .= $_GET['msg']; 1112 } 1113 if (!($path = str_replace(array('#','?'),array('%23','%3F'),$_GET['path'])) || // ?, # need to stay encoded! 1114 // actions enclose pathes containing comma with " 1115 ($path[0] == '"' && substr($path,-1) == '"' && !($path = substr(str_replace('""','"',$path),1,-1))) || 1116 !($stat = Vfs::lstat($path))) 1117 { 1118 $msg .= lang('File or directory not found!')." path='$path', stat=".array2string($stat); 1119 } 1120 else 1121 { 1122 $content = $stat; 1123 $content['name'] = $content['itempicker_merge']['name'] = Vfs::basename($path); 1124 $content['dir'] = $content['itempicker_merge']['dir'] = ($dir = Vfs::dirname($path)) ? Vfs::decodePath($dir) : ''; 1125 $content['path'] = $path; 1126 $content['hsize'] = Vfs::hsize($stat['size']); 1127 $content['mime'] = Vfs::mime_content_type($path); 1128 $content['gid'] *= -1; // our widgets use negative gid's 1129 if (($props = Vfs::propfind($path))) 1130 { 1131 foreach($props as $prop) 1132 { 1133 $content[$prop['name']] = $prop['val']; 1134 } 1135 } 1136 if (($content['is_link'] = Vfs::is_link($path))) 1137 { 1138 $content['symlink'] = Vfs::readlink($path); 1139 } 1140 } 1141 $content['tabs'] = $_GET['tabs']; 1142 if (!($content['is_dir'] = Vfs::is_dir($path) && !Vfs::is_link($path))) 1143 { 1144 $content['perms']['executable'] = (int)!!($content['mode'] & 0111); 1145 $mask = 6; 1146 if (preg_match('/^text/',$content['mime']) && $content['size'] < 100000) 1147 { 1148 $content['text_content'] = file_get_contents(Vfs::PREFIX.$path); 1149 } 1150 } 1151 else 1152 { 1153 //currently not implemented in backend $content['perms']['sticky'] = (int)!!($content['mode'] & 0x201); 1154 $mask = 7; 1155 } 1156 foreach(array('owner' => 6,'group' => 3,'other' => 0) as $name => $shift) 1157 { 1158 $content['perms'][$name] = ($content['mode'] >> $shift) & $mask; 1159 } 1160 $content['is_owner'] = Vfs::has_owner_rights($path,$content); 1161 } 1162 else 1163 { 1164 //_debug_array($content); 1165 $path =& $content['path']; 1166 1167 $button = @key($content['button']); 1168 unset($content['button']); 1169 if(!$button && $content['sudo']) 1170 { 1171 // Button to stop sudo is not in button namespace 1172 $button = 'sudo'; 1173 unset($content['sudo']); 1174 } 1175 // need to check 'setup' button (submit button in sudo popup), as some browsers (eg. chrome) also fill the hidden field 1176 if ($button == 'sudo' && Vfs::$is_root || $button == 'setup' && $content['sudo']['user']) 1177 { 1178 $msg = $this->sudo($button == 'setup' ? $content['sudo']['user'] : '',$content['sudo']['passwd']) ? 1179 lang('Root access granted.') : ($button == 'setup' && $content['sudo']['user'] ? 1180 lang('Wrong username or password!') : lang('Root access stopped.')); 1181 unset($content['sudo']); 1182 $content['is_owner'] = Vfs::has_owner_rights($path); 1183 } 1184 if (in_array($button,array('save','apply'))) 1185 { 1186 $props = array(); 1187 $perm_changed = $perm_failed = 0; 1188 foreach($content['old'] as $name => $old_value) 1189 { 1190 if (isset($content[$name]) && ($old_value != $content[$name] || 1191 // do not check for modification, if modify_subs is checked! 1192 $content['modify_subs'] && in_array($name,array('uid','gid','perms'))) && 1193 ($name != 'uid' || Vfs::$is_root)) 1194 { 1195 if ($name == 'name') 1196 { 1197 if (!($dir = Vfs::dirname($path))) 1198 { 1199 $msg .= lang('File or directory not found!')." Vfs::dirname('$path')===false"; 1200 if ($button == 'save') $button = 'apply'; 1201 continue; 1202 } 1203 $to = Vfs::concat($dir, $content['name']); 1204 if (file_exists(Vfs::PREFIX.$to) && $content['confirm_overwrite'] !== $to) 1205 { 1206 $tpl->set_validation_error('name',lang("There's already a file with that name!").'<br />'. 1207 lang('To overwrite the existing file store again.',lang($button))); 1208 $content['confirm_overwrite'] = $to; 1209 if ($button == 'save') $button = 'apply'; 1210 continue; 1211 } 1212 if (Vfs::rename($path,$to)) 1213 { 1214 $msg .= lang('Renamed %1 to %2.',Vfs::decodePath(basename($path)),Vfs::decodePath(basename($to))).' '; 1215 $content['old']['name'] = $content[$name]; 1216 $path = $to; 1217 $content['mime'] = Api\MimeMagic::filename2mime($path); // recheck mime type 1218 $refresh_path = Vfs::dirname($path); // for renames, we have to refresh the parent 1219 } 1220 else 1221 { 1222 $msg .= lang('Rename of %1 to %2 failed!',Vfs::decodePath(basename($path)),Vfs::decodePath(basename($to))).' '; 1223 if (Vfs::deny_script($to)) 1224 { 1225 $msg .= lang('You are NOT allowed to upload a script!').' '; 1226 } 1227 } 1228 } 1229 elseif ($name[0] == '#' || $name == 'comment') 1230 { 1231 $props[] = array('name' => $name, 'val' => $content[$name] ? $content[$name] : null); 1232 } 1233 elseif ($name == 'mergeapp') 1234 { 1235 $mergeprop = array( 1236 array( 1237 'ns' => static::$merge_prop_namespace, 1238 'name' => 'mergeapp', 1239 'val' => (!empty($content[$name]) ? $content[$name] : null), 1240 ), 1241 ); 1242 if (Vfs::proppatch($path,$mergeprop)) 1243 { 1244 $content['old'][$name] = $content[$name]; 1245 $msg .= lang('Setting for document merge saved.'); 1246 } 1247 else 1248 { 1249 $msg .= lang('Saving setting for document merge failed!'); 1250 } 1251 } 1252 else 1253 { 1254 static $name2cmd = array('uid' => 'chown','gid' => 'chgrp','perms' => 'chmod'); 1255 $cmd = array('EGroupware\\Api\\Vfs',$name2cmd[$name]); 1256 $value = $name == 'perms' ? static::perms2mode($content['perms']) : $content[$name]; 1257 if(!$value) continue; 1258 if ($content['modify_subs']) 1259 { 1260 if ($name == 'perms') 1261 { 1262 $changed = Vfs::find($path,array('type'=>'d'),$cmd,array($value)); 1263 $changed += Vfs::find($path,array('type'=>'f'),$cmd,array($value & 0666)); // no execute for files 1264 } 1265 else 1266 { 1267 $changed = Vfs::find($path,null,$cmd,array($value)); 1268 } 1269 $ok = $failed = 0; 1270 foreach($changed as $sub_path => &$r) 1271 { 1272 if ($r) 1273 { 1274 ++$ok; 1275 // Changing owner does not change mtime. Clear subs on UI so they get reloaded 1276 if($sub_path == $path) continue; 1277 Api\Json\Response::get()->apply('egw.dataStoreUID',['filemanager::'.$sub_path,null]); 1278 } 1279 else 1280 { 1281 ++$failed; 1282 } 1283 } 1284 if ($ok && !$failed) 1285 { 1286 if(!$perm_changed++) $msg .= lang('Permissions of %1 changed.',$path.' '.lang('and all it\'s childeren')); 1287 $content['old'][$name] = $content[$name]; 1288 } 1289 elseif($failed) 1290 { 1291 if(!$perm_failed++) $msg .= lang('Failed to change permissions of %1!',$path.lang('and all it\'s childeren'). 1292 ($ok ? ' ('.lang('%1 failed, %2 succeded',$failed,$ok).')' : '')); 1293 } 1294 } 1295 elseif (call_user_func_array($cmd,array($path,$value))) 1296 { 1297 $msg .= lang('Permissions of %1 changed.',$path); 1298 $content['old'][$name] = $content[$name]; 1299 } 1300 else 1301 { 1302 $msg .= lang('Failed to change permissions of %1!',$path); 1303 } 1304 } 1305 } 1306 } 1307 if ($props) 1308 { 1309 if (Vfs::proppatch($path,$props)) 1310 { 1311 foreach($props as $prop) 1312 { 1313 $content['old'][$prop['name']] = $prop['val']; 1314 } 1315 $msg .= lang('Properties saved.'); 1316 } 1317 else 1318 { 1319 $msg .= lang('Saving properties failed!'); 1320 } 1321 } 1322 } 1323 elseif ($content['eacl'] && $content['is_owner']) 1324 { 1325 if ($content['eacl']['delete']) 1326 { 1327 $ino_owner = key($content['eacl']['delete']); 1328 list(, $owner) = explode('-',$ino_owner,2); // $owner is a group and starts with a minus! 1329 $msg .= Vfs::eacl($path,null,$owner) ? lang('ACL deleted.') : lang('Error deleting the ACL entry!'); 1330 } 1331 elseif ($button == 'eacl') 1332 { 1333 if (!$content['eacl_owner']) 1334 { 1335 $msg .= lang('You need to select an owner!'); 1336 } 1337 else 1338 { 1339 $msg .= Vfs::eacl($path,$content['eacl']['rights'],$content['eacl_owner']) ? 1340 lang('ACL added.') : lang('Error adding the ACL!'); 1341 } 1342 } 1343 } 1344 Framework::refresh_opener($msg, 'filemanager', $refresh_path ? $refresh_path : $path, 'edit', null, '&path=[^&]*'); 1345 if ($button == 'save') Framework::window_close(); 1346 } 1347 if ($content['is_link'] && !Vfs::stat($path)) 1348 { 1349 $msg .= ($msg ? "\n" : '').lang('Link target %1 not found!',$content['symlink']); 1350 } 1351 $content['link'] = Egw::link(Vfs::download_url($path)); 1352 $content['icon'] = Vfs::mime_icon($content['mime']); 1353 $content['msg'] = $msg; 1354 1355 if (($readonlys['uid'] = !Vfs::$is_root) && !$content['uid']) $content['ro_uid_root'] = 'root'; 1356 // only owner can change group & perms 1357 if (($readonlys['gid'] = !$content['is_owner'] || 1358 Vfs::parse_url(Vfs::resolve_url($content['path']),PHP_URL_SCHEME) == 'oldvfs') ||// no uid, gid or perms in oldvfs 1359 !Vfs::is_writable($path)) 1360 { 1361 if (!$content['gid']) $content['ro_gid_root'] = 'root'; 1362 foreach($content['perms'] as $name => $value) 1363 { 1364 $readonlys['perms['.$name.']'] = true; 1365 } 1366 } 1367 $readonlys['gid'] = $readonlys['gid'] || !Vfs::is_writable($path); 1368 $readonlys['name'] = $path == '/' || !($dir = Vfs::dirname($path)) || !Vfs::is_writable($dir); 1369 $readonlys['comment'] = !Vfs::is_writable($path); 1370 $readonlys['tabs']['filemanager.file.preview'] = $readonlys['tabs']['filemanager.file.perms'] = $content['is_link']; 1371 1372 // Don't allow permission changes for these, even for root - it causes too many problems. 1373 // Use the CLI if you really need to make changes 1374 if(in_array($content['path'], ['/','/home','/apps'])) 1375 { 1376 foreach($content['perms'] as $name => $value) 1377 { 1378 $readonlys['perms['.$name.']'] = true; 1379 } 1380 $readonlys['gid'] = true; 1381 $readonlys['uid'] = true; 1382 $readonlys['modify_subs'] = true; 1383 } 1384 1385 // if neither owner nor is writable --> disable save&apply 1386 $readonlys['button[save]'] = $readonlys['button[apply]'] = !$content['is_owner'] && !Vfs::is_writable($path); 1387 1388 if (!($cfs = Api\Storage\Customfields::get('filemanager'))) 1389 { 1390 $readonlys['tabs']['custom'] = true; 1391 } 1392 elseif (!Vfs::is_writable($path)) 1393 { 1394 foreach($cfs as $name => $data) 1395 { 1396 $readonlys['#'.$name] = true; 1397 } 1398 } 1399 $readonlys['tabs']['filemanager.file.eacl'] = true; // eacl off by default 1400 if ($content['is_dir']) 1401 { 1402 $readonlys['tabs']['filemanager.file.preview'] = true; // no preview tab for dirs 1403 $sel_options['rights']=$sel_options['owner']=$sel_options['group']=$sel_options['other'] = array( 1404 7 => lang('Display and modification of content'), 1405 5 => lang('Display of content'), 1406 0 => lang('No access'), 1407 ); 1408 if(($content['eacl'] = Vfs::get_eacl($content['path'])) !== false && // backend supports eacl 1409 $GLOBALS['egw_info']['user']['account_id'] == Vfs::$user) // leave eACL tab disabled for sharing 1410 { 1411 unset($readonlys['tabs']['filemanager.file.eacl']); // --> switch the tab on again 1412 foreach($content['eacl'] as &$eacl) 1413 { 1414 $eacl['path'] = rtrim(Vfs::parse_url($eacl['path'],PHP_URL_PATH),'/'); 1415 $readonlys['delete['.$eacl['ino'].'-'.$eacl['owner'].']'] = $eacl['ino'] != $content['ino'] || 1416 $eacl['path'] != $content['path'] || !$content['is_owner']; 1417 } 1418 array_unshift($content['eacl'],false); // make the keys start with 1, not 0 1419 $content['eacl']['owner'] = 0; 1420 $content['eacl']['rights'] = 5; 1421 } 1422 } 1423 else 1424 { 1425 $sel_options['owner']=$sel_options['group']=$sel_options['other'] = array( 1426 6 => lang('Read & write access'), 1427 4 => lang('Read access only'), 1428 0 => lang('No access'), 1429 ); 1430 } 1431 1432 // Times are in server time, convert to user timezone 1433 foreach(['mtime','ctime'] as $date_field) 1434 { 1435 $time = new Api\DateTime($content[$date_field],Api\DateTime::$server_timezone); 1436 $time->setUser(); 1437 $content[$date_field] = $time->format('ts'); 1438 } 1439 1440 // mergeapp 1441 $content['mergeapp'] = static::get_mergeapp($path, 'self'); 1442 $content['mergeapp_parent'] = static::get_mergeapp($path, 'parents'); 1443 if(!empty($content['mergeapp'])) 1444 { 1445 $content['mergeapp_effective'] = $content['mergeapp']; 1446 } 1447 elseif(!empty($content['mergeapp_parent'])) 1448 { 1449 $content['mergeapp_effective'] = $content['mergeapp_parent']; 1450 } 1451 else 1452 { 1453 $content['mergeapp_effective'] = null; 1454 } 1455 // mergeapp select options 1456 $mergeapp_list = Link::app_list('merge'); 1457 unset($mergeapp_list[$GLOBALS['egw_info']['flags']['currentapp']]); // exclude filemanager from list 1458 $mergeapp_empty = !empty($content['mergeapp_parent']) 1459 ? $mergeapp_list[$content['mergeapp_parent']] . ' (parent setting)' : ''; 1460 $sel_options['mergeapp'] = array('' => $mergeapp_empty); 1461 $sel_options['mergeapp'] = $sel_options['mergeapp'] + $mergeapp_list; 1462 // mergeapp other gui options 1463 $content['mergeapp_itempicker_disabled'] = $content['is_dir'] || empty($content['mergeapp_effective']); 1464 1465 $preserve = $content; 1466 if (!isset($preserve['old'])) 1467 { 1468 $preserve['old'] = array( 1469 'perms' => $content['perms'], 1470 'name' => $content['name'], 1471 'uid' => $content['uid'], 1472 'gid' => $content['gid'], 1473 'comment' => (string)$content['comment'], 1474 'mergeapp' => $content['mergeapp'] 1475 ); 1476 if ($cfs) foreach($cfs as $name => $data) 1477 { 1478 $preserve['old']['#'.$name] = (string)$content['#'.$name]; 1479 } 1480 } 1481 if (Vfs::$is_root) 1482 { 1483 $tpl->setElementAttribute('sudouser', 'label', 'Logout'); 1484 $tpl->setElementAttribute('sudouser', 'help','Log out as superuser'); 1485 // Need a more complex submit because button type is buttononly, which doesn't submit 1486 $tpl->setElementAttribute('sudouser', 'onclick','app.filemanager.set_sudoButton(widget,"login")'); 1487 1488 } 1489 elseif ($button == 'sudo') 1490 { 1491 $tpl->setElementAttribute('sudouser', 'label', 'Superuser'); 1492 $tpl->setElementAttribute('sudouser', 'help','Enter setup user and password to get root rights'); 1493 $tpl->setElementAttribute('sudouser', 'onclick','app.filemanager.set_sudoButton(widget,"logout")'); 1494 } 1495 else if (self::is_anonymous($GLOBALS['egw_info']['user']['account_id'])) 1496 { 1497 // Just hide sudo for anonymous users 1498 $readonlys['sudouser'] = true; 1499 } 1500 if (($extra_tabs = Vfs::getExtraInfo($path,$content))) 1501 { 1502 // add to existing tabs in template 1503 $tpl->setElementAttribute('tabs', 'add_tabs', true); 1504 1505 $tabs =& $tpl->getElementAttribute('tabs','tabs'); 1506 if (true) $tabs = array(); 1507 1508 foreach(isset($extra_tabs[0]) ? $extra_tabs : array($extra_tabs) as $extra_tab) 1509 { 1510 $tabs[] = array( 1511 'label' => $extra_tab['label'], 1512 'template' => $extra_tab['name'] 1513 ); 1514 if ($extra_tab['data'] && is_array($extra_tab['data'])) 1515 { 1516 $content = array_merge($content, $extra_tab['data']); 1517 } 1518 if ($extra_tab['preserve'] && is_array($extra_tab['preserve'])) 1519 { 1520 $preserve = array_merge($preserve, $extra_tab['preserve']); 1521 } 1522 if ($extra_tab['readonlys'] && is_array($extra_tab['readonlys'])) 1523 { 1524 $readonlys = array_merge($readonlys, $extra_tab['readonlys']); 1525 } 1526 } 1527 } 1528 Framework::window_focus(); 1529 $GLOBALS['egw_info']['flags']['app_header'] = lang('Preferences').' '.Vfs::decodePath($path); 1530 1531 $tpl->exec('filemanager.filemanager_ui.file',$content,$sel_options,$readonlys,$preserve,2); 1532 } 1533 1534 /** 1535 * Check if the user is anonymous user 1536 * @param integer $account_id 1537 */ 1538 protected static function is_anonymous($account_id) 1539 { 1540 $acl = new Api\Acl($account_id); 1541 $acl->read_repository(); 1542 return $acl->check('anonymous', 1, 'phpgwapi'); 1543 } 1544 1545 /** 1546 * Run given action on given path(es) and return array/object with values for keys 'msg', 'errs', 'dirs', 'files' 1547 * 1548 * @param string $action eg. 'delete', ... 1549 * @param array $selected selected path(s) 1550 * @param string $dir=null current directory 1551 * @see static::action() 1552 */ 1553 public static function ajax_action($action, $selected, $dir=null, $props=null) 1554 { 1555 // do we have root rights, need to run here too, as method is static and therefore does NOT run __construct 1556 if (Api\Cache::getSession('filemanager', 'is_root')) 1557 { 1558 Vfs::$is_root = true; 1559 } 1560 $response = Api\Json\Response::get(); 1561 1562 $arr = array( 1563 'msg' => '', 1564 'action' => $action, 1565 'errs' => 0, 1566 'dirs' => 0, 1567 'files' => 0, 1568 ); 1569 1570 if (!isset($dir)) $dir = array_pop($selected); 1571 1572 switch($action) 1573 { 1574 case 'upload': 1575 static::handle_upload_action($action, $selected, $dir, $props, $arr); 1576 break; 1577 case 'shareWritableLink': 1578 case 'shareReadonlyLink': 1579 if ($action === 'shareWritableLink') 1580 { 1581 $share = Vfs\Sharing::create( 1582 '', $selected, Vfs\Sharing::WRITABLE, basename($selected), array(), array('share_writable' => true) 1583 ); 1584 } 1585 else 1586 { 1587 $share = Vfs\Sharing::create( 1588 '', $selected, Vfs\Sharing::READONLY, basename($selected), array() 1589 ); 1590 } 1591 $arr["share_link"] = $link = Vfs\Sharing::share2link($share); 1592 $arr["template"] = Api\Etemplate\Widget\Template::rel2url('/filemanager/templates/default/share_dialog.xet'); 1593 break; 1594 1595 // Upload, then link 1596 case 'link': 1597 // First upload 1598 $arr = static::ajax_action('upload', $selected, $dir, $props); 1599 $app_dir = Link::vfs_path($props['entry']['app'],$props['entry']['id'],'',true); 1600 1601 foreach($arr['uploaded'] as $file) 1602 { 1603 $target=Vfs::concat($dir,Vfs::encodePathComponent($file['name'])); 1604 if (Vfs::file_exists($target) && $app_dir) 1605 { 1606 if (!Vfs::file_exists($app_dir)) Vfs::mkdir($app_dir); 1607 error_log("Symlinking $target to $app_dir"); 1608 Vfs::symlink($target, Vfs::concat($app_dir,Vfs::encodePathComponent($file['name']))); 1609 } 1610 } 1611 // Must return to avoid adding to $response again 1612 return; 1613 1614 default: 1615 $arr['msg'] = static::action($action, $selected, $dir, $arr['errs'], $arr['dirs'], $arr['files']); 1616 } 1617 $response->data($arr); 1618 //error_log(__METHOD__."('$action',".array2string($selected).') returning '.array2string($arr)); 1619 return $arr; 1620 } 1621 1622 /** 1623 * Deal with an uploaded file 1624 * 1625 * @param string $action Should be 'upload' 1626 * @param $selected Array of file information 1627 * @param string $dir Target directory 1628 * @param $props 1629 * @param string[] $arr Result 1630 * 1631 * @throws Api\Exception\AssertionFailed 1632 */ 1633 protected static function handle_upload_action(string $action, $selected, $dir, $props, &$arr) 1634 { 1635 $script_error = 0; 1636 $conflict = $selected['conflict']; 1637 unset($selected['conflict']); 1638 1639 foreach($selected as $tmp_name => &$data) 1640 { 1641 $path = Vfs::concat($dir, Vfs::encodePathComponent($data['name'])); 1642 1643 if(Vfs::deny_script($path)) 1644 { 1645 if (!isset($script_error)) 1646 { 1647 $arr['msg'] .= ($arr['msg'] ? "\n" : '').lang('You are NOT allowed to upload a script!'); 1648 } 1649 ++$script_error; 1650 ++$arr['errs']; 1651 unset($selected[$tmp_name]); 1652 continue; 1653 } 1654 elseif (Vfs::is_dir($path)) 1655 { 1656 $data['confirm'] = 'is_dir'; 1657 continue; 1658 } 1659 elseif (!$data['confirmed'] && Vfs::stat($path)) 1660 { 1661 // File exists, what to do? 1662 switch($conflict) 1663 { 1664 case 'overwrite': 1665 unset($data['confirm']); 1666 $data['confirmed'] = true; 1667 break; 1668 case 'rename': 1669 // Find a unique name 1670 $i = 1; 1671 $info = pathinfo($path); 1672 while(Vfs::file_exists($path)) 1673 { 1674 $path = $info['dirname'] . '/'. $info['filename'] . " ($i)." . $info['extension']; 1675 $i++; 1676 } 1677 break; 1678 case 'ask': 1679 default: 1680 $data['confirm'] = true; 1681 } 1682 } 1683 if(!$data['confirm']) 1684 { 1685 if (is_dir($GLOBALS['egw_info']['server']['temp_dir']) && is_writable($GLOBALS['egw_info']['server']['temp_dir'])) 1686 { 1687 $tmp_path = $GLOBALS['egw_info']['server']['temp_dir'] . '/' . basename($tmp_name); 1688 } 1689 else 1690 { 1691 $tmp_path = ini_get('upload_tmp_dir') . '/' . basename($tmp_name); 1692 } 1693 1694 if (Vfs::copy_uploaded($tmp_path, $path, $props, false)) 1695 { 1696 ++$arr['files']; 1697 $uploaded[] = $data['name']; 1698 } 1699 else 1700 { 1701 ++$arr['errs']; 1702 } 1703 } 1704 } 1705 if ($arr['errs'] > $script_error) 1706 { 1707 $arr['msg'] .= ($arr['msg'] ? "\n" : '').lang('Error uploading file!'); 1708 } 1709 if ($arr['files']) 1710 { 1711 $arr['msg'] .= ($arr['msg'] ? "\n" : '').lang('%1 successful uploaded.', implode(', ', $uploaded)); 1712 } 1713 $arr['uploaded'] = $selected; 1714 $arr['path'] = $dir; 1715 $arr['props'] = $props; 1716 } 1717 1718 /** 1719 * Convert perms array back to integer mode 1720 * 1721 * @param array $perms with keys owner, group, other, executable, sticky 1722 * @return int 1723 */ 1724 private function perms2mode(array $perms) 1725 { 1726 $mode = $perms['owner'] << 6 | $perms['group'] << 3 | $perms['other']; 1727 if ($mode['executable']) 1728 { 1729 $mode |= 0111; 1730 } 1731 if ($mode['sticky']) 1732 { 1733 $mode |= 0x201; 1734 } 1735 return $mode; 1736 } 1737} 1738