1<?php 2/** 3 * EGroupware API - Interapplicaton links 4 * 5 * Links have two ends each pointing to an entry, each entry is a double: 6 * - app app-name or directory-name of an egw application, eg. 'infolog' 7 * - id this is the id, eg. an integer or a tupple like '0:INBOX:1234' 8 * 9 * @link http://www.egroupware.org 10 * @author Ralf Becker <RalfBecker-AT-outdoor-training.de> 11 * @copyright 2001-2016 by RalfBecker@outdoor-training.de 12 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License 13 * @package api 14 * @subpackage link 15 * @version $Id$ 16 */ 17 18namespace EGroupware\Api; 19 20/** 21 * Generalized linking between entries of EGroupware apps 22 * 23 * Please note: this class can NOT and does not need to be initialised, all methods are static 24 * 25 * To participate in the linking an applications has to implement the following hooks: 26 * 27 * /** 28 * * Hook called by link-class to include app in the appregistry of the linkage 29 * * 30 * * @param array|string $location location and other parameters (not used) 31 * * @return array with method-names 32 * *% 33 * function search_link($location) 34 * { 35 * return array( 36 * 'query' => 'app.class.link_query', // method to search app for a pattern: array link_query(string $pattern, array $options) 37 * 'title' => 'app.class.link_title', // method to return title of an entry of app: string/false/null link_title(int/string $id) 38 * 'titles' => 'app.class.link_titles', // method to return multiple titles: array link_title(array $ids) 39 * 'view' => array( // get parameters to view an entry of app 40 * 'menuaction' => 'app.class.method', 41 * ), 42 * 'types' => array( // Optional list of sub-types to filter (eg organisations), app to handle different queries 43 * 'type_key' => array( 44 * 'name' => 'Human Reference', 45 * 'icon' => 'app/icon' // Optional icon to use for that sub-type 46 * ) 47 * ), 48 * 'view_id' => 'app_id', // name of get parameter of the id 49 * 'view_popup' => '400x300', // size of popup (XxY), if view is in popup 50 * 'view_list' => 'app.class.method' // deprecated use 'list' instead 51 * 'list' => array( // Method to be called to display a list of links, method should check $_GET['search'] to filter 52 * 'menuaction' => 'app.class.method', 53 * ), 54 * 'list_popup' => '400x300' 55 * 'add' => array( // get parameter to add an empty entry to app 56 * 'menuaction' => 'app.class.method', 57 * ), 58 * 'add_app' => 'link_app', // name of get parameter to add links to other app 59 * 'add_id' => 'link_id', // --------------------- " ------------------- id 60 * 'add_popup' => '400x300', // size of popup (XxY), if add is in popup 61 * 'notify' => 'app.class.method', // method to be called if an other applications links or unlinks with app: notify(array $data) 62 * 'file_access' => 'app.class.method', // method to be called to check file access rights of a given user, see links_stream_wrapper class 63 * // boolean file_access(string $id,int $check,string $rel_path=null,int $user=null) 64 * 'file_access_user' => false, // true if file_access method supports 4th parameter $user, if app is NOT supporting it 65 * // Link::file_access() returns false for $user != current user! 66 * 'file_dir' => 'app/sub', // sub file dir for uploaded files/links 67 * 'find_extra' => array('name_preg' => '/^(?!.picture.jpg)$/') // extra options to Vfs::find, to eg. remove some files from the list of attachments 68 * 'edit' => array( 69 * 'menuaction' => 'app.class.method', 70 * ), 71 * 'edit_id' => 'app_id', 72 * 'edit_popup' => '400x300', 73 * 'name' => 'Some name', // Name to use instead of app-name 74 * 'icon' => 'app/icon', // Optional icon to use instead of app-icon 75 * 'entry' => 'Contact', // Optional name for single entry of app, eg. "contact" used instead of appname 76 * 'entries' => 'Contacts', // Optional name for multiple entries of app, eg. "contacts" used instead of appname 77 * 'modification_time' => array( // Optional location of entry's last modification 78 * 'column' => {string} table.column // Table & column name 79 * 'type' => {string} longint // Data type for the column, if it's not a timestamp 80 * ), 81 * 'mime' => array( // Optional register mime-types application can open 82 * 'text/something' => array( 83 * 'mime_url' => $attr, // either mime_url or mime_data is required for server-side processing! 84 * 'mime_data' => $attr, // md5-hash returned from Link::set_data() to retrive content (only server-side) 85 * 'menuaction' => 'app.class.method', // method to call 86 * 'mime_popup' => '400x300', // optional size of popup 87 * 'mime_target' => '_self', // optional target, default _blank 88 * // other get-parameters to set in url 89 * ), 90 * // further mime types supported ... 91 * ), 92 * 'fetch' => 'app.class.method', // method to return entry data for a given id. the method called should support id, and expected mime-type 93 * // basically you should return something like array(id, title, mimetype, body, linked-files) 94 * 95 * 'push_data' => <callable> | "key" | ["key1", ...] // keys of ACL relevant and privacy save data needed for push of changes to client 96 * // or callable to do the cleaning eg. used in calendar 97 * 98 * 'additional' => array( // allow one app to register sub-types, 99 * 'app-sub' => array( // different from 'types' approach above 100 * // every value defined above 101 * ) 102 * ) 103 * } 104 * All entries are optional, thought you only get conected functionality, if you implement them ... 105 * 106 * The BO-layer implementes some extra features on top of the so-layer: 107 * 1) It handles links to not already existing entries. This is used by the eTemplate link-widget, which allows to 108 * setup links even for new / not already existing entries, before they get saved. 109 * In that case you have to set the first id to 0 for the link-static function and pass the array returned in that id 110 * (not the return-value) after saveing your new entry again to the link static function. 111 * 2) Attaching files: they are saved in the vfs and not the link-table (!). 112 * Attached files are stored under $vfs_basedir='/infolog' in the vfs! 113 * 3) It manages the link-registry, in which apps can register themselfs by implementing some hooks 114 * 4) It notifies apps, who registered for that service, about changes in the links their entries 115 * 116 * Modification times in links (and deleted timestamp) are always in server-time! 117 * (We dont convert them here, as most apps ignore them anyway) 118 */ 119class Link extends Link\Storage 120{ 121 /** 122 * appname used for returned attached files (!= 'filemanager'!) 123 */ 124 const VFS_APPNAME = 'file'; // pseudo-appname for own file-attachments in vfs, this is NOT the vfs-app 125 126 /** 127 * Appname used of files stored via Link::set_data() 128 */ 129 const DATA_APPNAME = 'egw-data'; 130 131 /** 132 * appname used for linking existing files to VFS 133 */ 134 const VFS_LINK = 'link'; 135 136 /** 137 * Baseurl for the attachments in the vfs 138 */ 139 const VFS_BASEURL = 'vfs://default/apps'; 140 /** 141 * Turns on debug-messages 142 */ 143 const DEBUG = false; 144 /** 145 * other apps can participate in the linking by implementing a 'search_link' hook, which 146 * has to return an array in the format of an app_register entry below 147 * 148 * @var array 149 */ 150 static $app_register = array( 151 'api-accounts' => array( // user need run-rights for home 152 'app' => 'api', 153 'name' => 'Accounts', 154 'icon' => 'addressbook/accounts', 155 'query' => 'EGroupware\\Api\\Accounts::link_query', 156 'title' => 'EGroupware\\Api\\Accounts::username', 157 'view' => array('menuaction'=>'addressbook.addressbook_ui.view','ajax'=>'true'), 158 'view_id' => 'account_id' 159 ), 160 'api' => array( 161 // handling of text or pdf files by browser in a popup window 162 'mime' => array( 163 'application/pdf' => array( 164 'mime_popup' => '640x480', 165 'mime_target' => '_blank', 166 ), 167 '/^text\\/(plain|html|diff)/' => array( // text/(mimetypes which can be opened as recognised popups) 168 'mime_popup' => '640x480', 169 'mime_target' => '_blank', 170 ), 171 '/^image\\//' => array( // image 172 'mime_popup' => '640x480', 173 'mime_target' => '_blank', 174 ), 175 ), 176 ), 177 ); 178 179 /** 180 * Max. number of titles stored in session (older once get removed) 181 */ 182 const TITLE_CACHE_SIZE = 500; 183 /** 184 * Caches link titles for a better performance 185 * 186 * @var array 187 */ 188 private static $title_cache = array(); 189 190 /** 191 * Max. number of titles stored in session (older once get removed) 192 */ 193 const FILE_ACCESS_CACHE_SIZE = 1000; 194 /** 195 * Cache file access permissions 196 * 197 * @var array 198 */ 199 private static $file_access_cache = array(); 200 201 /** 202 * Private constructor to forbid instanciated use 203 * 204 */ 205 private function __construct() 206 { 207 208 } 209 210 /** 211 * initialize our static vars 212 * 213 * @param boolean $clear_all do not use session AND not permission check for app-registry 214 */ 215 static function init_static($clear_all=false) 216 { 217 // FireFox 36 can not display pdf with it's internal viewer in an iframe used by mobile theme/template for popups 218 // same is true for all mobile devices 219 if (Header\UserAgent::type() == 'firefox' && $GLOBALS['egw_info']['user']['preferences']['common']['theme'] == 'mobile' || 220 Header\UserAgent::mobile()) 221 { 222 unset(self::$app_register['api']['mime']['application/pdf']); 223 } 224 // other apps can participate in the linking by implementing a search_link hook, which 225 // has to return an array in the format of an app_register entry 226 // for performance reasons, we do it only once / cache it in the session 227 if ($clear_all || !($search_link_hooks = Cache::getSession(__CLASS__, 'search_link_hooks'))) 228 { 229 $search_link_hooks = Hooks::process('search_link',array(), $clear_all || (bool)$GLOBALS['egw_info']['flags']['async-service']); 230 Cache::setSession(__CLASS__, 'search_link_hooks', $search_link_hooks); 231 } 232 if (is_array($search_link_hooks)) 233 { 234 foreach($search_link_hooks as $app => $data) 235 { 236 // allow apps to register additional types 237 if (isset($data['additional'])) 238 { 239 foreach($data['additional'] as $name => $values) 240 { 241 $values['app'] = $app; // store name of registring app, to be able to check access 242 self::$app_register[$name] = $values; 243 } 244 unset($data['additional']); 245 } 246 // support deprecated view_list attribute instead of new index attribute 247 if (isset($data['view_list']) && !isset($data['list'])) 248 { 249 $data['list'] = array('menuaction' => $data['view_list']); 250 } 251 elseif(isset($data['list']) && !isset($data['view_list'])) 252 { 253 $data['view_list'] = $data['list']['menuaction']; 254 } 255 if (is_array($data)) 256 { 257 self::$app_register[$app] = $data; 258 } 259 } 260 } 261 // disable ability to link to accounts for non-admins, if account-selection is disabled 262 if ($GLOBALS['egw_info']['user']['preferences']['common']['account_selection'] == 'none' && 263 !isset($GLOBALS['egw_info']['user']['apps']['admin'])) 264 { 265 unset(self::$app_register['api-accounts']); 266 } 267 if (!(self::$title_cache = Cache::getSession(__CLASS__, 'link_title_cache'))) 268 { 269 self::$title_cache = array(); 270 } 271 if (!(self::$file_access_cache = Cache::getSession(__CLASS__, 'link_file_access_cache'))) 272 { 273 self::$file_access_cache = array(); 274 } 275 276 // register self::save_session_cache to run on shutdown 277 Egw::on_shutdown(array(__CLASS__, 'save_session_cache')); 278 279 //error_log(__METHOD__.'() items in title-cache: '.count(self::$title_cache).' file-access-cache: '.count(self::$file_access_cache)); 280 } 281 282 /** 283 * Get clientside relevant attributes from app registry in json format 284 * 285 * Only transfering relevant information cuts approx. half of the size. 286 * Also only transfering information relevant to apps user has access too. 287 * Important eg. for mime-registry, to not use calendar for opening iCal files, if user has no calendar! 288 * As app can store additonal types, we have to check the registring app $data['app'] too! 289 * 290 * @return string json encoded object with app: object pairs with attributes "(view|add|edit)(|_id|_popup)" 291 */ 292 public static function json_registry() 293 { 294 $to_json = array(); 295 foreach(self::$app_register as $app => $data) 296 { 297 if (isset($GLOBALS['egw_info']['user']['apps'][$app]) || 298 isset($data['app']) && isset($GLOBALS['egw_info']['user']['apps'][$data['app']])) 299 { 300 $to_json[$app] = array_intersect_key($data, array_flip(array( 301 'view','view_id','view_popup', 302 'add','add_app','add_id','add_popup', 303 'edit','edit_id','edit_popup', 304 'list','list_popup', 305 'name','icon','query', 306 'mime','entry','entries', 307 ))); 308 } 309 } 310 return json_encode($to_json); 311 } 312 313 /** 314 * Called by Egw::shutdown to store the title-cache in session and run notifications 315 * 316 * Would probably better called shutdown as well. 317 */ 318 static function save_session_cache() 319 { 320 if (isset($GLOBALS['egw']->session)) // eg. cron-jobs use it too, without any session 321 { 322 //error_log(__METHOD__.'() items in title-cache: '.count(self::$title_cache).' file-access-cache: '.count(self::$file_access_cache)); 323 324 if (count(self::$title_cache) > self::TITLE_CACHE_SIZE) 325 { 326 self::$title_cache = array_slice(self::$title_cache, -self::TITLE_CACHE_SIZE); 327 } 328 Cache::setSession(__CLASS__, 'link_title_cache', self::$title_cache); 329 330 if (count(self::$file_access_cache) > self::FILE_ACCESS_CACHE_SIZE) 331 { 332 self::$file_access_cache = array_slice(self::$file_access_cache, -self::FILE_ACCESS_CACHE_SIZE); 333 } 334 Cache::setSession(__CLASS__, 'link_file_access_cache', self::$file_access_cache); 335 } 336 } 337 338 /** 339 * creats a link between $app1,$id1 and $app2,$id2 - $id1 does NOT need to exist yet 340 * 341 * Does NOT check if link already exists. 342 * File-attachments return a negative link-id !!! 343 * 344 * @param string $app1 app of $id1 345 * @param string|array &$id1 id of item to linkto or 0 if item not yet created or array with links 346 * of not created item or $file-array if $app1 == self::VFS_APPNAME (see below). 347 * If $id==0 it will be set on return to an array with the links for the new item. 348 * @param string|array $app2 app of 2.linkend or array with links ($id2 not used) 349 * @param string $id2 ='' id of 2. item of $file-array if $app2 == self::VFS_APPNAME or self::DATA_APPNAME 350 * $file array with informations about the file in format of the etemplate file-type 351 * $file['name'] name of the file (no directory) 352 * $file['type'] mime-type of the file 353 * $file['tmp_name'] name of the uploaded file (incl. directory) for self::VFS_APPNAME or 354 * $file['egw_data'] id of Link::set_data() call for self::DATA_APPNAME 355 * @param string $remark ='' Remark to be saved with the link (defaults to '') 356 * @param int $owner =0 Owner of the link (defaults to user) 357 * @param int $lastmod =0 timestamp of last modification (defaults to now=time()) 358 * @param int $no_notify =0 &1 dont notify $app1, &2 dont notify $app2 359 * @return int/boolean False (for db or param-error) or on success link_id (Please not the return-value of $id1) 360 */ 361 static function link( $app1,&$id1,$app2,$id2='',$remark='',$owner=0,$lastmod=0,$no_notify=0 ) 362 { 363 if (self::DEBUG) 364 { 365 echo "<p>Link::link('$app1',$id1,'".print_r($app2,true)."',".print_r($id2,true).",'$remark',$owner,$lastmod)</p>\n"; 366 } 367 if (!$app1 || !$app2 || $app1 == $app2 && $id1 == $id2) 368 { 369 return False; 370 } 371 if (is_array($app2) && !$id2) 372 { 373 reset($app2); 374 $link_id = True; 375 while ($link_id && $link = current($app2)) 376 { 377 if (!is_array($link)) // check for unlink-marker 378 { 379 //echo "<b>link='$link' is no array</b><br>\n"; 380 next($app2); 381 continue; 382 } 383 if (is_array($id1) || !$id1) // create link only in $id1 array 384 { 385 self::link($app1, $id1, $link['app'], $link['id'], $link['remark'],$link['owner'],$link['lastmod']); 386 next($app2); 387 continue; 388 } 389 switch ($link['app']) 390 { 391 case self::DATA_APPNAME: 392 if (!($link['id']['tmp_name'] = self::get_data($link['id']['egw_data'], true))) 393 { 394 $link_id = false; 395 break; 396 } 397 // fall through 398 case self::VFS_APPNAME: 399 $link_id = self::attach_file($app1,$id1,$link['id'],$link['remark']); 400 break; 401 402 case self::VFS_LINK: 403 $link_id = self::link_file($app1,$id1, $link['id'],$link['remark']); 404 break; 405 406 default: 407 $link_id = Link\Storage::link($app1,$id1,$link['app'],$link['id'], 408 $link['remark'],$link['owner'],$link['lastmod']); 409 // notify both sides 410 if (!($no_notify&2)) self::notify('link',$link['app'],$link['id'],$app1,$id1,$link_id); 411 if (!($no_notify&1)) self::notify('link',$app1,$id1,$link['app'],$link['id'],$link_id); 412 break; 413 } 414 next($app2); 415 } 416 return $link_id; 417 } 418 if (is_array($id1) || !$id1) // create link only in $id1 array 419 { 420 if (!is_array($id1)) 421 { 422 $id1 = array( ); 423 } 424 $link_id = self::temp_link_id($app2,$id2); 425 426 $id1[$link_id] = array( 427 'app' => $app2, 428 'id' => $id2, 429 'remark' => $remark, 430 'owner' => $owner, 431 'link_id' => $link_id, 432 'lastmod' => time() 433 ); 434 if (self::DEBUG) 435 { 436 _debug_array($id1); 437 } 438 return $link_id; 439 } 440 if ($app1 == self::VFS_LINK) 441 { 442 return self::link_file($app2,$id2,$id1,$remark); 443 } 444 elseif ($app2 == self::VFS_LINK) 445 { 446 return self::link_file($app1,$id1,$id2,$remark); 447 } 448 if ($app1 == self::VFS_APPNAME) 449 { 450 return self::attach_file($app2,$id2,$id1,$remark); 451 } 452 elseif ($app2 == self::VFS_APPNAME) 453 { 454 return self::attach_file($app1,$id1,$id2,$remark); 455 } 456 $link_id = Link\Storage::link($app1,$id1,$app2,$id2,$remark,$owner); 457 458 if (!($no_notify&2)) self::notify('link',$app2,$id2,$app1,$id1,$link_id); 459 if (!($no_notify&1)) self::notify('link',$app1,$id1,$app2,$id2,$link_id); 460 461 return $link_id; 462 } 463 464 /** 465 * generate temporary link_id used as array-key 466 * 467 * @param string $app app-name 468 * @param mixed $id 469 * @return string 470 */ 471 static function temp_link_id($app,$id) 472 { 473 return $app.':'.(!in_array($app, array(self::VFS_APPNAME,self::VFS_LINK, self::DATA_APPNAME)) ? $id : $id['name']); 474 } 475 476 /** 477 * returns array of links to $app,$id (reimplemented to deal with not yet created items) 478 * 479 * @param string $app appname 480 * @param string|array $id id(s) in $app 481 * @param string $only_app ='' if set return only links from $only_app (eg. only addressbook-entries) or NOT from if $only_app[0]=='!' 482 * @param string $order ='link_lastmod DESC' defaults to newest links first 483 * @param boolean $cache_titles =false should all titles be queryed and cached (allows to query each link app only once!) 484 * This option also removes links not viewable by current user from the result! 485 * @param boolean $deleted =false Include links that have been flagged as deleted, waiting for purge of linked record. 486 * @param int $limit =null number of entries to return, only affects links, attachments are allways reported! 487 * @return array id => links pairs if $id is an array or just the links (only_app: ids) or empty array if no matching links found 488 */ 489 static function get_links($app, $id, $only_app='', $order='link_lastmod DESC',$cache_titles=false, $deleted=false, $limit=null) 490 { 491 if (self::DEBUG) echo "<p>Link::get_links(app='$app',id='$id',only_app='$only_app',order='$order',deleted='$deleted')</p>\n"; 492 493 if (is_array($id) || !$id) 494 { 495 $ids = array(); 496 if (is_array($id)) 497 { 498 if (($not_only = $only_app[0] == '!')) 499 { 500 $only_app = substr(1,$only_app); 501 } 502 foreach (array_reverse($id) as $link) 503 { 504 if (is_array($link) // check for unlink-marker 505 && !($only_app && $not_only == ($link['app'] == $only_app))) 506 { 507 $ids[$link['link_id']] = $only_app ? $link['id'] : $link; 508 } 509 } 510 } 511 return $ids; 512 } 513 $ids = Link\Storage::get_links($app, $id, $only_app, $order, $deleted, $limit); 514 if (empty($only_app) || $only_app == self::VFS_APPNAME || 515 ($only_app[0] == '!' && $only_app != '!'.self::VFS_APPNAME)) 516 { 517 if (($vfs_ids = self::list_attached($app,$id))) 518 { 519 $ids += $vfs_ids; 520 } 521 } 522 //echo "ids=<pre>"; print_r($ids); echo "</pre>\n"; 523 if ($cache_titles) 524 { 525 // agregate links by app 526 $app_ids = array(); 527 foreach($ids as $link) 528 { 529 $app_ids[$link['app']][] = $link['id']; 530 } 531 foreach($app_ids as $appname => $a_ids) 532 { 533 self::titles($appname,array_unique($a_ids)); 534 } 535 // remove links, current user has no access, from result 536 foreach($ids as $key => $link) 537 { 538 if (!self::title($link['app'],$link['id'])) 539 { 540 unset($ids[$key]); 541 } 542 } 543 reset($ids); 544 } 545 return $ids; 546 } 547 548 /** 549 * Query the links of multiple entries of one application 550 * 551 * @ToDo also query the attachments in a single query, eg. via a directory listing of /apps/$app 552 * @param string $app 553 * @param array $ids 554 * @param boolean $cache_titles =true should all titles be queryed and cached (allows to query each link app only once!) 555 * @param string $only_app if set return only links from $only_app (eg. only addressbook-entries) or NOT from if $only_app[0]=='!' 556 * @param string $order ='link_lastmod DESC' defaults to newest links first 557 * @param boolean $deleted =false Include links that have been flagged as deleted, waiting for purge of linked record. 558 * @return array of $id => array($links) pairs 559 */ 560 static function get_links_multiple($app,array $ids,$cache_titles=true,$only_app='',$order='link_lastmod DESC', $deleted=false ) 561 { 562 if (self::DEBUG) echo "<p>".__METHOD__."('$app',".print_r($ids,true).",$cache_titles,'$only_app','$order')</p>\n"; 563 564 if (!$ids) 565 { 566 return array(); // no ids are linked to nothing 567 } 568 $links = Link\Storage::get_links($app,$ids,$only_app,$order,$deleted); 569 570 if (empty($only_app) || $only_app == self::VFS_APPNAME || 571 ($only_app[0] == '!' && $only_app != '!'.self::VFS_APPNAME)) 572 { 573 // todo do that in a single query, eg. directory listing, too 574 foreach($ids as $id) 575 { 576 if (!isset($links[$id])) 577 { 578 $links[$id] = array(); 579 } 580 if (($vfs_ids = self::list_attached($app,$id))) 581 { 582 $links[$id] += $vfs_ids; 583 } 584 } 585 } 586 if ($cache_titles) 587 { 588 // agregate links by app 589 $app_ids = array(); 590 foreach($links as &$targets) 591 { 592 foreach($targets as $link) 593 { 594 if (is_array($link)) $app_ids[$link['app']][] = $link['id']; 595 } 596 } 597 foreach($app_ids as $a_app => $a_ids) 598 { 599 self::titles($a_app,array_unique($a_ids)); 600 } 601 } 602 return $links; 603 } 604 605 /** 606 * Read one link specified by it's link_id or by the two end-points 607 * 608 * If $id is an array (links not yet created) only link_ids are allowed. 609 * 610 * @param int|string $app_link_id > 0 link_id of link or app-name of link 611 * @param string|array $id ='' id if $app_link_id is an appname or array with links, if 1. entry not yet created 612 * @param string $app2 ='' second app 613 * @param string $id2 ='' id in $app2 614 * @return array with link-data or False 615 */ 616 static function get_link($app_link_id,$id='',$app2='',$id2='') 617 { 618 if (self::DEBUG) 619 { 620 echo '<p>'.__METHOD__."($app_link_id,$id,$app2,$id2)</p>\n"; echo function_backtrace(); 621 } 622 if (is_array($id)) 623 { 624 if (strpos($app_link_id,':') === false) $app_link_id = self::temp_link_id($app2,$id2); // create link_id of temporary link, if not given 625 626 if (isset($id[$app_link_id]) && is_array($id[$app_link_id])) // check for unlinked-marker 627 { 628 return $id[$app_link_id]; 629 } 630 return False; 631 } 632 if ((int)$app_link_id < 0 || $app_link_id == self::VFS_APPNAME || $app2 == self::VFS_APPNAME) 633 { 634 if ((int)$app_link_id < 0) // vfs link_id ? 635 { 636 return self::fileinfo2link(-$app_link_id); 637 } 638 if ($app_link_id == self::VFS_APPNAME) 639 { 640 return self::info_attached($app2,$id2,$id); 641 } 642 return self::info_attached($app_link_id,$id,$id2); 643 } 644 return Link\Storage::get_link($app_link_id,$id,$app2,$id2); 645 } 646 647 /** 648 * Remove link with $link_id or all links matching given $app,$id 649 * 650 * Note: if $link_id != '' and $id is an array: unlink removes links from that array only 651 * unlink has to be called with &$id to see the result (depricated) or unlink2 has to be used !!! 652 * 653 * @param $link_id link-id to remove if > 0 654 * @param string $app ='' appname of first endpoint 655 * @param string|array $id ='' id in $app or array with links, if 1. entry not yet created 656 * @param int $owner =0 account_id to delete all links of a given owner, or 0 657 * @param string $app2 ='' app of second endpoint 658 * @param string $id2 ='' id in $app2 659 * @param boolean $hold_for_purge Don't really delete the link, just mark it as deleted and wait for final delete 660 * @return the number of links deleted 661 */ 662 static function unlink($link_id,$app='',$id='',$owner=0,$app2='',$id2='',$hold_for_purge=false) 663 { 664 return self::unlink2($link_id,$app,$id,$owner,$app2,$id2,$hold_for_purge); 665 } 666 667 /** 668 * Remove link with $link_id or all links matching given $app,$id 669 * 670 * @param $link_id link-id to remove if > 0 671 * @param string $app ='' appname of first endpoint 672 * @param string|array &$id='' id in $app or array with links, if 1. entry not yet created 673 * @param int $owner =0 account_id to delete all links of a given owner, or 0 674 * @param string $app2 ='' app of second endpoint, or !file (other !app are not yet supported!) 675 * @param string $id2 ='' id in $app2 676 * @param boolean $hold_for_purge Don't really delete the link, just mark it as deleted and wait for final delete 677 * @return the number of links deleted 678 */ 679 static function unlink2($link_id,$app,&$id,$owner=0,$app2='',$id2='',$hold_for_purge=false) 680 { 681 if (self::DEBUG) 682 { 683 echo "<p>Link::unlink('$link_id','$app',".array2string($id).",'$owner','$app2','$id2', $hold_for_purge)</p>\n"; 684 } 685 if ($link_id < 0) // vfs-link? 686 { 687 return self::delete_attached(-$link_id); 688 } 689 elseif ($app == self::VFS_APPNAME) 690 { 691 return self::delete_attached($app2,$id2,$id); 692 } 693 elseif ($app2 == self::VFS_APPNAME) 694 { 695 return self::delete_attached($app,$id,$id2); 696 } 697 if (!is_array($id)) 698 { 699 if (!$link_id && !$app2 && !$id2 && $app2 != '!'.self::VFS_APPNAME) 700 { 701 // in case "someone" interested in all changes (used eg. for push) 702 Hooks::process([ 703 'location' => 'notify-all', 704 'type' => 'delete', 705 'app' => $app, 706 'id' => $id, 707 ], null, true); 708 709 self::delete_attached($app,$id); // deleting all attachments 710 self::delete_cache($app,$id); 711 } 712 713 // Log in history 714 if($link_id && (!$app || !$app2)) 715 { 716 // Need to load it first 717 $link = self::get_link($link_id); 718 $app = $link['link_app1']; 719 $id = $link['link_id1']; 720 $app2 = $link['link_app2']; 721 $id2 = $link['link_id2']; 722 } 723 if ($app && $app2) 724 { 725 Storage\History::static_add($app,$id,$GLOBALS['egw_info']['user']['account_id'],'~link~','',$app2.':'.$id2); 726 Storage\History::static_add($app2,$id2,$GLOBALS['egw_info']['user']['account_id'],'~link~','',$app.':'.$id); 727 } 728 $deleted = Link\Storage::unlink($link_id,$app,$id,$owner,$app2 != '!'.self::VFS_APPNAME ? $app2 : '',$id2,$hold_for_purge); 729 730 // only notify on real links, not the one cached for writing or fileattachments 731 self::notify_unlink($deleted); 732 733 return count($deleted); 734 } 735 if (!$link_id) $link_id = self::temp_link_id($app2,$id2); // create link_id of temporary link, if not given 736 737 if (isset($id[$link_id])) 738 { 739 $id[$link_id] = False; // set the unlink marker 740 741 if (self::DEBUG) 742 { 743 _debug_array($id); 744 } 745 return True; 746 } 747 return False; 748 } 749 750 /** 751 * get list/array of link-aware apps the user has rights to use 752 * 753 * @param string $must_support capability the apps need to support, eg. 'add', default ''=list all apps 754 * @return array with app => title pairs 755 */ 756 static function app_list($must_support='') 757 { 758 $apps = array(); 759 foreach(self::$app_register as $type => $reg) 760 { 761 if ($must_support && !isset($reg[$must_support])) continue; 762 763 list($app) = explode('-', $type); 764 if ($GLOBALS['egw_info']['user']['apps'][$app]) 765 { 766 $apps[$type] = lang(self::get_registry($type, 'name')); 767 } 768 } 769 return $apps; 770 } 771 772 /** 773 * Default number of returned rows for Link::query() 774 */ 775 const DEFAULT_NUM_ROWS = 100; 776 777 /** 778 * Searches for a $pattern in the entries of $app 779 * 780 * @param string $app app to search 781 * @param string $pattern pattern to search 782 * @param array& $options passed to callback: type, start, num_rows, filter, exclude; on return value for "total" 783 * @return array with $id => $title pairs of matching entries of app 784 */ 785 static function query($app, $pattern, &$options = array()) 786 { 787 if ($app == '' || !is_array($reg = self::$app_register[$app]) || !isset($reg['query'])) 788 { 789 return array(); 790 } 791 $method = $reg['query']; 792 793 if (self::DEBUG) 794 { 795 echo "<p>Link::query('$app','$pattern') => '$method'</p>\n"; 796 echo "Options: "; _debug_array($options); 797 } 798 // limit number of returned rows by default to 100, if no limit is set 799 if (!isset($options['num_rows'])) $options['num_rows'] = self::DEFAULT_NUM_ROWS; 800 801 $result = self::exec($method, array($pattern, &$options)); 802 803 if (!isset($options['total'])) 804 { 805 $options['total'] = count($result); 806 } 807 if (isset($options['exclude'])) 808 { 809 $result = array_diff_key($result, array_flip($options['exclude'])); 810 } 811 if (is_array($result) && (isset($options['start']) || (isset($options['num_rows']) && count($result) > $options['num_rows']))) 812 { 813 $result = array_slice($result, $options['start'], (isset($options['num_rows']) ? $options['num_rows'] : count($result)), true); 814 } 815 816 return $result; 817 } 818 819 /** 820 * returns the title (short description) of entry $id and $app 821 * 822 * @param string $app appname 823 * @param string $id id in $app 824 * @param array $link =null link-data for file-attachments 825 * @return string/boolean string with title, null if $id does not exist in $app or false if no perms to view it 826 */ 827 static function title($app,$id,$link=null) 828 { 829 if (!$id) return ''; 830 831 $title =& self::get_cache($app,$id); 832 if (isset($title) && !empty($title) && !is_array($id)) 833 { 834 if (self::DEBUG) echo '<p>'.__METHOD__."('$app','$id')='$title' (from cache)</p>\n"; 835 return $title; 836 } 837 if ($app == self::VFS_APPNAME) 838 { 839 if (is_array($id) && $link) 840 { 841 $link = $id; 842 $title = Vfs::decodePath($link['name']); 843 } 844 else 845 { 846 $title = $id; 847 } 848 /* disabling mime-type and size in link-title of attachments, as it clutters the UI 849 and users dont need it most of the time. These details can allways be views in filemanager. 850 if (is_array($link)) 851 { 852 $title .= ': '.$link['type'] . ' '.Vfs::hsize($link['size']); 853 }*/ 854 if (self::DEBUG) echo '<p>'.__METHOD__."('$app','$id')='$title' (file)</p>\n"; 855 return $title; 856 } 857 if ($app == '' || !is_array($reg = self::$app_register[$app]) || !isset($reg['title'])) 858 { 859 if (self::DEBUG) echo "<p>".__METHOD__."('$app','$id') something is wrong!!!</p>\n"; 860 return false; //array(); // not sure why it should return an array on failure, as the description states boolean/string 861 } 862 $method = $reg['title']; 863 864 if (true) $title = self::exec($method, array($id)); 865 866 if ($id && is_null($title)) // $app,$id has been deleted ==> unlink all links to it 867 { 868 static $unlinking = array(); 869 // check if we are already trying to unlink the entry, to avoid an infinit recursion 870 if (!isset($unlinking[$app]) || !isset($unlinking[$app][$id])) 871 { 872 $unlinking[$app][$id] = true; 873 self::unlink(0,$app,$id); 874 unset($unlinking[$app][$id]); 875 } 876 if (self::DEBUG) echo '<p>'.__METHOD__."('$app','$id') unlinked, as $method returned null</p>\n"; 877 return False; 878 } 879 if (self::DEBUG) echo '<p>'.__METHOD__."('$app','$id')='$title' (from $method)</p>\n"; 880 881 return $title; 882 } 883 884 /** 885 * Maximum number of titles to query from an application at once (to NOT trash mysql) 886 */ 887 const MAX_TITLES_QUERY = 100; 888 889 /** 890 * Query the titles off multiple id's of one app 891 * 892 * Apps can implement that hook, if they have a quicker (eg. less DB queries) method to query the title of multiple entries. 893 * If it's not implemented, we call the regular title method multiple times. 894 * 895 * @param string $app 896 * @param array $ids 897 */ 898 static function titles($app,array $ids) 899 { 900 if (self::DEBUG) 901 { 902 echo "<p>".__METHOD__."($app,".implode(',',$ids).")</p>\n"; 903 } 904 $titles = $ids_to_query = array(); 905 foreach($ids as $id) 906 { 907 $title =& self::get_cache($app,$id); 908 if (!isset($title)) 909 { 910 if (isset(self::$app_register[$app]['titles'])) 911 { 912 $ids_to_query[] = $id; // titles method --> collect links to query at once 913 } 914 else 915 { 916 $title = self::title($app,$id); // no titles method --> fallback to query each link separate 917 } 918 } 919 $titles[$id] = $title; 920 } 921 if ($ids_to_query) 922 { 923 for ($n = 0; ($ids = array_slice($ids_to_query,$n*self::MAX_TITLES_QUERY,self::MAX_TITLES_QUERY)); ++$n) 924 { 925 foreach(self::exec(self::$app_register[$app]['titles'], array($ids)) as $id => $t) 926 { 927 $title =& self::get_cache($app,$id); 928 $titles[$id] = $title = $t; 929 } 930 } 931 } 932 return $titles; 933 } 934 935 /** 936 * Add new entry to $app, evtl. already linked to $to_app, $to_id 937 * 938 * @param string $app appname of entry to create 939 * @param string $to_app ='' appname to link the new entry to 940 * @param string $to_id =''id in $to_app 941 * @return array/boolean with name-value pairs for link to add-methode of $app or false if add not supported 942 */ 943 static function add($app,$to_app='',$to_id='') 944 { 945 //echo "<p>Link::add('$app','$to_app','$to_id') app_register[$app] ="; _debug_array($app_register[$app]); 946 if ($app == '' || !is_array($reg = self::$app_register[$app]) || !isset($reg['add'])) 947 { 948 return false; 949 } 950 $params = $reg['add']; 951 952 if ($reg['add_app'] && $to_app && $reg['add_id'] && $to_id) 953 { 954 $params[$reg['add_app']] = $to_app; 955 $params[$reg['add_id']] = $to_id; 956 } 957 return $params; 958 } 959 960 /** 961 * Edit entry $id of $app 962 * 963 * @param string $app appname of entry 964 * @param string $id id in $app 965 * @param string &$popup=null on return popup size eg. '600x400' or null 966 * @return array|boolean with name-value pairs for link to edit-methode of $app or false if edit not supported 967 */ 968 static function edit($app,$id,&$popup=null) 969 { 970 //echo "<p>Link::add('$app','$to_app','$to_id') app_register[$app] ="; _debug_array($app_register[$app]); 971 if (empty($app) || empty($id) || !is_array($reg = self::$app_register[$app]) || !isset($reg['edit'])) 972 { 973 if ($reg && isset($reg['view'])) 974 { 975 $popup = $reg['view_popup']; 976 return self::view($app,$id); // fallback to view 977 } 978 return false; 979 } 980 $params = $reg['edit']; 981 $params[$reg['edit_id']] = $id; 982 983 $popup = $reg['edit_popup']; 984 985 return $params; 986 } 987 988 /** 989 * view entry $id of $app 990 * 991 * @param string $app appname 992 * @param string $id id in $app 993 * @param array $link =null link-data for file-attachments 994 * @return array with name-value pairs for link to view-methode of $app to view $id 995 */ 996 static function view($app,$id,$link=null) 997 { 998 if ($app == self::VFS_APPNAME && !empty($id) && is_array($link)) 999 { 1000 //return Vfs::download_url(self::vfs_path($link['app2'],$link['id2'],$link['id'],true)); 1001 return self::mime_open(self::vfs_path($link['app2'],$link['id2'],$link['id'],true), $link['type']); 1002 } 1003 if ($app == '' || !is_array($reg = self::$app_register[$app]) || !isset($reg['view']) || !isset($reg['view_id'])) 1004 { 1005 return array(); 1006 } 1007 $view = $reg['view']; 1008 1009 $names = explode(':',$reg['view_id']); 1010 if (count($names) > 1) 1011 { 1012 $id = explode(':',$id); 1013 foreach($names as $n => $name) 1014 { 1015 $view[$name] = $id[$n]; 1016 } 1017 } 1018 else 1019 { 1020 $view[$reg['view_id']] = $id; 1021 } 1022 return $view; 1023 } 1024 1025 /** 1026 * Get mime-type information from app-registry 1027 * 1028 * Only return information from apps the user has access too (incl. registered sub-types of that apps). 1029 * 1030 * We prefer full matches over wildcards like "text/*" written as regexp "/^text\\//". 1031 * 1032 * @param string $type 1033 * @return array with values for keys 'menuaction', 'mime_id' (path) or 'mime_url' and options 'mime_popup' and other values to pass one 1034 */ 1035 static function get_mime_info($type) 1036 { 1037 foreach(self::$app_register as $app => $registry) 1038 { 1039 if (isset($registry['mime']) && 1040 (isset($GLOBALS['egw_info']['user']['apps'][$app]) || 1041 isset($registry['app']) && isset($GLOBALS['egw_info']['user']['apps'][$registry['app']]))) 1042 { 1043 foreach($registry['mime'] as $mime => $data) 1044 { 1045 if ($mime == $type) return $data; 1046 if ($mime[0] == '/' && preg_match($mime.'i', $type)) 1047 { 1048 $wildcard_mime = $data; 1049 } 1050 } 1051 } 1052 } 1053 return isset($wildcard_mime) ? $wildcard_mime : null; 1054 } 1055 1056 /** 1057 * Get handler (link-data) for given path and mime-type 1058 * 1059 * @param string $path vfs path 1060 * @param string $type =null default to Vfs::mime_content_type($path) 1061 * @param string &$popup=null on return popup size or null 1062 * @return string|array string with EGw relative link, array with get-parameters for '/index.php' or null (directory and not filemanager access) 1063 */ 1064 static function mime_open($path, $type=null, &$popup=null) 1065 { 1066 if (is_null($type)) $type = Vfs::mime_content_type($path); 1067 1068 if (($data = self::get_mime_info($type))) 1069 { 1070 if (isset($data['mime_url'])) 1071 { 1072 $data[$data['mime_url']] = Vfs::PREFIX.$path; 1073 unset($data['mime_url']); 1074 } 1075 elseif (isset($data['mime_id'])) 1076 { 1077 $data[$data['mime_id']] = $path; 1078 unset($data['mime_id']); 1079 } 1080 elseif(isset($data['mime_popup'])) 1081 { 1082 $popup = $data['mime_popup']; 1083 } 1084 else 1085 { 1086 throw new Exception\AssertionFailed("Missing 'mime_id' or 'mime_url' for mime-type '$type'!"); 1087 } 1088 unset($data['mime_popup']); 1089 } 1090 else 1091 { 1092 $data = Vfs::download_url($path); 1093 } 1094 return $data; 1095 } 1096 1097 /** 1098 * Check if $app uses a popup for $action 1099 * 1100 * @param string $app app-name 1101 * @param string $action ='view' name of the action, atm. 'view' or 'add' 1102 * @param array $link =null link-data for file-attachments 1103 * @return boolean|string false if no popup is used or $app is not registered, otherwise string with the prefered popup size (eg. '640x400) 1104 */ 1105 static function is_popup($app, $action='view', $link=null) 1106 { 1107 $popup = self::get_registry($app,$action.'_popup'); 1108 1109 // for files/attachments check mime-registry 1110 if ($app == self::VFS_APPNAME && is_array($link) && !empty($link['type'])) 1111 { 1112 $path = self::vfs_path($link['app2'], $link['id2'], $link['id'], true); 1113 $p = null; 1114 if (self::mime_open($path, $link['type'], $p)) 1115 { 1116 $popup = $p; 1117 } 1118 } 1119 //error_log(__METHOD__."('$app', '$action', ".array2string($link).') returning '.array2string($popup)); 1120 return $popup; 1121 } 1122 1123 /** 1124 * Check if $app is in the registry and has an entry for $name 1125 * 1126 * @param string $app app-name 1127 * @param string $name name / key in the registry, eg. 'view' 1128 * @param boolean|array|string|int $url_id format entries like "add", "edit", "view" for actions "url" incl. an ID 1129 * array to add arbitray parameter eg. ['some_id' => '$id'] 1130 * @return boolean|string false if $app is not registered, otherwise string with the value for $name 1131 */ 1132 static function get_registry($app, $name, $url_id=false) 1133 { 1134 $reg = self::$app_register[$app]; 1135 1136 if (!isset($reg)) return false; 1137 1138 if (!isset($reg[$name])) // some defaults 1139 { 1140 switch($name) 1141 { 1142 case 'name': 1143 $reg[$name] = $app; 1144 break; 1145 case 'entry': 1146 $reg[$name] = $app; 1147 break; 1148 case 'icon': 1149 if (isset($GLOBALS['egw_info']['apps'][$app]['icon'])) 1150 { 1151 $reg[$name] = ($GLOBALS['egw_info']['apps'][$app]['icon_app'] ? $GLOBALS['egw_info']['apps'][$app]['icon_app'] : $app). 1152 '/'.$GLOBALS['egw_info']['apps'][$app]['icon']; 1153 } 1154 else 1155 { 1156 $reg[$name] = $app.'/navbar'; 1157 } 1158 break; 1159 } 1160 } 1161 1162 // format as action url 1163 if ($url_id && isset($reg[$name]) && is_array($reg[$name])) 1164 { 1165 $params = $reg[$name]; 1166 if (isset($reg[$name.'_id'])) 1167 { 1168 $params[$reg[$name.'_id']] = $url_id === true ? '$id' : $url_id; 1169 } 1170 if (is_array($url_id)) 1171 { 1172 $params += $url_id; 1173 } 1174 foreach($params as $name => $value) 1175 { 1176 $str .= (!empty($str) ? '&' : '').$name.'='.$value; 1177 } 1178 return $str; 1179 } 1180 1181 return isset($reg) ? $reg[$name] : false; 1182 } 1183 1184 /** 1185 * path to the attached files of $app/$ip or the directory for $app if no $id,$file given 1186 * 1187 * All link-files are based in the vfs-subdir '/apps/'.$app 1188 * 1189 * @param string $app appname 1190 * @param string $id ='' id in $app 1191 * @param string $file ='' filename 1192 * @param boolean $just_the_path =false return url or just the vfs path 1193 * @return string/array path or array with path and relatives, depending on $relatives 1194 */ 1195 static function vfs_path($app,$id='',$file='',$just_the_path=false) 1196 { 1197 $path = self::VFS_BASEURL; 1198 1199 if ($app) 1200 { 1201 if( isset(self::$app_register[$app]) ) { 1202 $reg = self::$app_register[$app]; 1203 1204 if( isset($reg['file_dir']) ) { 1205 $app = $reg['file_dir']; 1206 } 1207 } 1208 1209 $path .= '/'.$app; 1210 1211 if ($id) 1212 { 1213 $path .= '/'.$id; 1214 1215 if ($file) 1216 { 1217 $path .= '/'.$file; 1218 } 1219 } 1220 } 1221 if ($just_the_path) 1222 { 1223 $path = parse_url($path,PHP_URL_PATH); 1224 } 1225 else 1226 { 1227 $path = Vfs::resolve_url($path); 1228 } 1229 //error_log(__METHOD__."($app,$id,$file,$just_the_path)=$path"); 1230 return $path; 1231 } 1232 1233 /** 1234 * Put a file to the corrosponding place in the VFS and set the attributes 1235 * 1236 * Does NO is_uploaded_file check, calling application is responsible for doing that for uploaded files! 1237 * 1238 * @param string $app appname to linke the file to 1239 * @param string $id id in $app 1240 * @param array $file informations about the file in format of the etemplate file-type 1241 * $file['name'] name of the file (optional incl. a directory) 1242 * $file['type'] mine-type of the file 1243 * $file['tmp_name'] name of the uploaded file (incl. directory) or resource of opened file 1244 * @param string $comment ='' comment to add to the link 1245 * @return int negative id of egw_sqlfs table as negative link-id's are for vfs attachments 1246 */ 1247 static function attach_file($app,$id,$file,$comment='') 1248 { 1249 // check if $file['name'] specifies a subdirectory, in which case use and, if necessary, create it 1250 if (is_array($file) && strpos($file['name'], '/') && strpos($file['name'], '..') === false) 1251 { 1252 $entry_dir = self::vfs_path($app, $id, Vfs::dirname($file['name'])); 1253 $file['name'] = Vfs::basename($file['name']); 1254 } 1255 else 1256 { 1257 $entry_dir = self::vfs_path($app,$id); 1258 } 1259 if (self::DEBUG) 1260 { 1261 echo "<p>attach_file: app='$app', id='$id', tmp_name='$file[tmp_name]', name='$file[name]', size='$file[size]', type='$file[type]', path='$file[path]', ip='$file[ip]', comment='$comment', entry_dir='$entry_dir'</p>\n"; 1262 } 1263 if (file_exists($entry_dir) || ($Ok = mkdir($entry_dir,0,true))) 1264 { 1265 $Ok = Vfs::copy_uploaded($file, $p=Vfs::parse_url($entry_dir, PHP_URL_PATH), $comment, false); // no is_uploaded_file() check! 1266 if (!$Ok) error_log(__METHOD__."('$app', '$id', ".array2string($file).", '$comment') called Vfs::copy_uploaded('$file[tmp_name]', '$p', '$comment', false)=".array2string($Ok)); 1267 } 1268 else 1269 { 1270 error_log(__METHOD__."($app,$id,".array2string($file).",$comment) Can't mkdir $entry_dir!"); 1271 } 1272 return $Ok ? -$Ok['ino'] : false; 1273 } 1274 1275 /** 1276 * Links the entry to an existing file in the VFS 1277 * 1278 * @param string $app appname to link the file to 1279 * @param string $id id in $app 1280 * @param string $file VFS path to link to 1281 * @return boolean true on success, false on failure 1282 */ 1283 static function link_file($app,$id,$file) 1284 { 1285 // Don't try to link into app dir if there is no id 1286 if(!$id) return; 1287 1288 if (!Vfs::stat($file)) 1289 { 1290 error_log(__METHOD__. ' (Link target ' . Vfs::decodePath($file) . ' not found!'); 1291 return false; 1292 } 1293 1294 $entry_dir = self::vfs_path($app, $id); 1295 if (!file_exists($entry_dir) && !mkdir($entry_dir, 0, true)) 1296 { 1297 error_log(__METHOD__."($app,$id,".array2string($file).") Can't mkdir $entry_dir!"); 1298 return false; 1299 } 1300 1301 return Vfs::symlink($file, Vfs::concat($entry_dir, Vfs::basename($file))); 1302 } 1303 /** 1304 * deletes a single or all attached files of an entry (for all there's no acl check, as the entry probably not exists any more!) 1305 * 1306 * @param int|string $app > 0: file_id of an attchemnt or $app/$id entry which linked to 1307 * @param string $id ='' id in app 1308 * @param string $fname ='' filename 1309 * @return boolean|array false on error ($app or $id not found), array with path as key and boolean result of delete 1310 */ 1311 static function delete_attached($app,$id='',$fname='') 1312 { 1313 if ((int)$app > 0) // is file_id 1314 { 1315 $url = Vfs::resolve_url(Vfs\Sqlfs\StreamWrapper::id2path($app)); 1316 } 1317 else 1318 { 1319 if (empty($app) || empty($id)) 1320 { 1321 return False; // dont delete more than all attachments of an entry 1322 } 1323 $url = self::vfs_path($app,$id,$fname); 1324 1325 if (!$fname || !$id) // we delete the whole entry (or all entries), which probably not exist anymore 1326 { 1327 $current_is_root = Vfs::$is_root; 1328 Vfs::$is_root = true; 1329 } 1330 } 1331 if (self::DEBUG) 1332 { 1333 echo '<p>'.__METHOD__."('$app','$id','$fname') url=$url</p>\n"; 1334 } 1335 // Log in history - Need to load it first 1336 if((int)$app > 0) 1337 { 1338 $link = self::get_link(-$app); 1339 if($link['app2'] && $link['id2']) 1340 { 1341 Storage\History::static_add($link['app2'],$link['id2'],$GLOBALS['egw_info']['user']['account_id'],'~file~','', Vfs::basename($url)); 1342 } 1343 } 1344 if (($Ok = !file_exists($url) || Vfs::remove($url,true)) && ((int)$app > 0 || $fname)) 1345 { 1346 // try removing the dir, in case it's empty 1347 if (($dir = Vfs::dirname($url))) @Vfs::rmdir($dir); 1348 } 1349 if (!is_null($current_is_root)) 1350 { 1351 Vfs::$is_root = $current_is_root; 1352 } 1353 return $Ok; 1354 } 1355 1356 /** 1357 * converts the infos vfs has about a file into a link 1358 * 1359 * @param string $app appname 1360 * @param string $id id in app 1361 * @param string $filename filename 1362 * @return array 'kind' of link-array 1363 */ 1364 static function info_attached($app,$id,$filename) 1365 { 1366 $path = self::vfs_path($app,$id,$filename,true); 1367 if (!($stat = Vfs::stat($path,STREAM_URL_STAT_QUIET))) 1368 { 1369 return false; 1370 } 1371 return self::fileinfo2link($stat,$path); 1372 } 1373 1374 /** 1375 * converts a fileinfo (row in the vfs-db-table) in a link 1376 * 1377 * @param array|int $fileinfo a row from the vfs-db-table (eg. returned by the vfs ls static function) or a file_id of that table 1378 * @return array a 'kind' of link-array 1379 */ 1380 static function fileinfo2link($fileinfo,$url=null) 1381 { 1382 if (!is_array($fileinfo)) 1383 { 1384 $url = Vfs\Sqlfs\StreamWrapper::id2path($fileinfo); 1385 if (!($fileinfo = Vfs::stat($url,STREAM_URL_STAT_QUIET))) 1386 { 1387 return false; 1388 } 1389 } 1390 1391 $up = explode('/',$url[0] == '/' ? $url : parse_url($url,PHP_URL_PATH)); // /apps/$app/$id 1392 $app = null; 1393 1394 foreach( self::$app_register as $tapp => $reg ) { 1395 if( isset($reg['file_dir']) ) { 1396 $lup = $up; 1397 1398 unset($lup[0]); 1399 unset($lup[1]); 1400 reset($lup); 1401 1402 $fdp = explode('/',$reg['file_dir'][0] == '/' ? 1403 $reg['file_dir'] : parse_url($reg['file_dir'],PHP_URL_PATH)); 1404 1405 $found = true; 1406 1407 foreach( $fdp as $part ) { 1408 if( current($lup) == $part ) { 1409 if( next($lup) === false ) { 1410 $found = false; 1411 break; 1412 } 1413 } 1414 else { 1415 $found = false; 1416 break; 1417 } 1418 } 1419 1420 if( $found ) { 1421 $id = current($lup); 1422 $app = $tapp; 1423 break; 1424 } 1425 } 1426 } 1427 1428 if( $app === null ) { 1429 list(,,$app,$id) = $up; 1430 } 1431 1432 return array( 1433 'app' => self::VFS_APPNAME, 1434 'id' => $fileinfo['name'], 1435 'app2' => $app, 1436 'id2' => $id, 1437 'remark' => '', // only list_attached currently sets the remark 1438 'owner' => $fileinfo['uid'], 1439 'link_id' => -$fileinfo['ino'], 1440 'lastmod' => $fileinfo['mtime'], 1441 'size' => $fileinfo['size'], 1442 'type' => $fileinfo['mime'], 1443 ); 1444 } 1445 1446 /** 1447 * lists all attachments to $app/$id 1448 * 1449 * @param string $app appname 1450 * @param string $id id in app 1451 * @return array with link_id => 'kind' of link-array pairs 1452 */ 1453 static function list_attached($app,$id) 1454 { 1455 $path = self::vfs_path($app,$id); 1456 //error_log(__METHOD__."($app,$id) url=$url"); 1457 1458 if (!($extra = self::get_registry($app,'find_extra'))) $extra = array(); 1459 1460 // always use regular links stream wrapper here: extended one is unnecessary (slow) for just listing attachments 1461 if (substr($path,0,13) == 'stylite.links') $path = substr($path,8); 1462 1463 $attached = array(); 1464 if (($url2stats = Vfs::find($path,array('need_mime'=>true,'type'=>'F','url'=>true)+$extra,true))) 1465 { 1466 $props = Vfs::propfind(array_keys($url2stats)); // get the comments 1467 foreach($url2stats as $url => &$fileinfo) 1468 { 1469 $link = self::fileinfo2link($fileinfo,$url); 1470 if ($props && isset($props[$url])) 1471 { 1472 foreach($props[$url] as $prop) 1473 { 1474 if ($prop['ns'] == Vfs::DEFAULT_PROP_NAMESPACE && $prop['name'] == 'comment') 1475 { 1476 $link['remark'] = $prop['val']; 1477 } 1478 } 1479 } 1480 $attached[$link['link_id']] = $link; 1481 } 1482 } 1483 return $attached; 1484 } 1485 1486 /** 1487 * reverse static function of htmlspecialchars() 1488 * 1489 * @param string $str string to decode 1490 * @return string decoded string 1491 */ 1492 static private function decode_htmlspecialchars($str) 1493 { 1494 return str_replace(array('&','"','<','>'),array('&','"','<','>'),$str); 1495 } 1496 1497 /** 1498 * Key for old link title in $data param to Link::notify 1499 */ 1500 const OLD_LINK_TITLE = 'old_link_title'; 1501 1502 /** 1503 * notify other apps about changed content in $app,$id 1504 * 1505 * To give other apps the possebility to update a title, you can also specify 1506 * a changed old link-title in $data[Link::OLD_LINK_TITLE]. 1507 * 1508 * @param string $app name of app in which the updated happend 1509 * @param string $id id in $app of the updated entry 1510 * @param array $data =null updated data of changed entry, as the read-method of the BO-layer would supply it 1511 * @param string $type ="unknown" type of update: "add", "edit", "update" or default "unknown" 1512 */ 1513 static function notify_update($app,$id,$data=null,$type='unknown') 1514 { 1515 self::delete_cache($app,$id); 1516 //error_log(__METHOD__."('$app', $id, $data)"); 1517 foreach(self::get_links($app,$id,'!'.self::VFS_APPNAME) as $link_id => $link) 1518 { 1519 self::notify('update',$link['app'],$link['id'],$app,$id,$link_id,$data); 1520 } 1521 if($data[Link::OLD_LINK_TITLE] && Json\Response::isJSONResponse()) 1522 { 1523 // Update client side with new title 1524 Json\Response::get()->apply('egw.link_title_callback',array(array($app => array($id => self::title($app, $id))))); 1525 } 1526 1527 // in case "someone" interested in all changes (used eg. for push) 1528 Hooks::process([ 1529 'location' => 'notify-all', 1530 'type' => !empty($data[Link::OLD_LINK_TITLE]) ? 'update' : $type, 1531 'app' => $app, 1532 'id' => $id, 1533 'data' => $data, 1534 ], null, true); 1535 } 1536 1537 /** 1538 * Stores notifications to run after regular processing is done 1539 * 1540 * @var array 1541 */ 1542 private static $notifies = array(); 1543 1544 /** 1545 * notify an application about a new or deleted links to own entries or updates in the content of the linked entry 1546 * 1547 * Please note: not all apps supply update notifications 1548 * 1549 * @internal 1550 * @param string $type 'link' for new links, 'unlink' for unlinked entries, 'update' of content in linked entries 1551 * @param string $notify_app app to notify 1552 * @param string $notify_id id in $notify_app 1553 * @param string $target_app name of app whos entry changed, linked or deleted 1554 * @param string $target_id id in $target_app 1555 * @param array $data =null data of entry in app2 (optional) 1556 */ 1557 static private function notify($type,$notify_app,$notify_id,$target_app,$target_id,$link_id,$data=null) 1558 { 1559 //error_log(__METHOD__."('$type', '$notify_app', $notify_id, '$target_app', $target_id, $link_id, $data)"); 1560 if ($link_id && isset(self::$app_register[$notify_app]) && isset(self::$app_register[$notify_app]['notify'])) 1561 { 1562 if (!self::$notifies) 1563 { 1564 Egw::on_shutdown(array(__CLASS__, 'run_notifies')); 1565 } 1566 self::$notifies[] = array( 1567 'method' => self::$app_register[$notify_app]['notify'], 1568 'type' => $type, 1569 'id' => $notify_id, 1570 'target_app' => $target_app, 1571 'target_id' => $target_id, 1572 'link_id' => $link_id, 1573 'data' => $data, 1574 ); 1575 } 1576 } 1577 1578 /** 1579 * Run notifications called by Egw::on_shutdown(), after regular processing is finished 1580 */ 1581 static public function run_notifies() 1582 { 1583 //error_log(__METHOD__."() count(self::\$notifies)=".count(self::$notifies)); 1584 while(self::$notifies) 1585 { 1586 $args = array_shift(self::$notifies); 1587 $method = $args['method']; 1588 unset($args['method']); 1589 //error_log(__METHOD__."() calling $method(".array2string($args).')'); 1590 self::exec($method, array($args)); 1591 } 1592 } 1593 1594 /** 1595 * notifies about unlinked links 1596 * 1597 * @internal 1598 * @param array &$links unlinked links from the database 1599 */ 1600 static private function notify_unlink(&$links) 1601 { 1602 foreach($links as $link) 1603 { 1604 // we notify both sides of the link, as the unlink command NOT clearly knows which side initiated the unlink 1605 self::notify('unlink',$link['link_app1'],$link['link_id1'],$link['link_app2'],$link['link_id2'],$link['link_id']); 1606 self::notify('unlink',$link['link_app2'],$link['link_id2'],$link['link_app1'],$link['link_id1'],$link['link_id']); 1607 } 1608 } 1609 1610 /** 1611 * Get a reference to the cached value for $app/$id for $type 1612 * 1613 * @param string $app 1614 * @param string|int $id 1615 * @param string $type ='title' 'title' or 'file_access' 1616 * @return int|string can be null, if cache not yet set 1617 */ 1618 private static function &get_cache($app,$id,$type = 'title') 1619 { 1620 switch($type) 1621 { 1622 case 'title': 1623 if ($app == self::VFS_APPNAME) 1624 { 1625 return null; // do not cache file titles, they are just the names 1626 } 1627 return self::$title_cache[$app.':'.$id]; 1628 case 'file_access': 1629 return self::$file_access_cache[$app.':'.$id]; 1630 default: 1631 throw new Exception\WrongParameter("Unknown type '$type'!"); 1632 } 1633 } 1634 1635 /** 1636 * Set title and optional file_access cache for $app,$id 1637 * 1638 * Allows applications to set values for title and file access, eg. in their search method, 1639 * to not be called again. This offloads the need to cache from the app to the link class. 1640 * If there's no caching, items get read multiple times from the database! 1641 * 1642 * @param string $app 1643 * @param int|string $id 1644 * @param string $title title string or null 1645 * @param int $file_access =null Acl::READ, Acl::EDIT or both or'ed together 1646 */ 1647 public static function set_cache($app,$id,$title,$file_access=null) 1648 { 1649 //error_log(__METHOD__."($app,$id,$title,$file_access)"); 1650 // do not cache file titles, they are just the names 1651 if (!is_null($title) && $app != self::VFS_APPNAME) 1652 { 1653 self::$title_cache[$app.':'.$id] = $title; 1654 } 1655 if (!is_null($file_access)) 1656 { 1657 self::$file_access_cache[$app.':'.$id] = $file_access; 1658 } 1659 } 1660 1661 /** 1662 * Delete the diverse caches for $app/$id 1663 * 1664 * @param string $app app-name or null to delete the whole cache 1665 * @param int|string $id id or null to delete only file_access cache of given app (keeps title cache, if app implements file_access!) 1666 */ 1667 private static function delete_cache($app,$id) 1668 { 1669 unset(self::$title_cache[$app.':'.$id]); 1670 unset(self::$file_access_cache[$app.':'.$id]); 1671 } 1672 1673 /** 1674 * Store function call and parameters in session and return id to retrieve it result 1675 * 1676 * @param string $mime_type 1677 * @param string $method 1678 * @param array $params 1679 * @param boolean $ignore_mime =false true: return id, even if nothing registered for given mime-type 1680 * @return string|null md5 hash of stored data of server-side supported mime-type or null otherwise 1681 */ 1682 public static function set_data($mime_type, $method, array $params, $ignore_mime=false) 1683 { 1684 if (!$ignore_mime && (!($info = self::get_mime_info($mime_type)) || empty($info['mime_data']))) 1685 { 1686 return null; 1687 } 1688 array_unshift($params, $method); 1689 $id = md5(serialize($params)); 1690 //error_log(__METHOD__."('$mime_type', '$method', ...) params=".array2string($params)." --> json=".array2string(serialize($params)).' --> id='.array2string($id)); 1691 Cache::setSession(__CLASS__, $id, $params); 1692 return $id; 1693 } 1694 1695 /** 1696 * Call stored function with parameters and return result 1697 * 1698 * @param string $id 1699 * @param boolean $return_resource =false false: return string, true: return resource 1700 * @return mixed null if id is not found or invalid 1701 * @throws Exception\WrongParameter 1702 */ 1703 public static function get_data($id, $return_resource=false) 1704 { 1705 $data = Cache::getSession(__CLASS__, $id); 1706 1707 if (!isset($data) || empty($data[0])) 1708 { 1709 throw new Exception\WrongParameter(__METHOD__."('$id')"); 1710 } 1711 $method = array_shift($data); 1712 $ret = self::exec($method, $data); 1713 1714 if (is_resource($ret)) fseek($ret, 0); 1715 1716 if ($return_resource != is_resource($ret)) 1717 { 1718 if ($return_resource && ($fp = fopen('php://temp', 'w'))) 1719 { 1720 fwrite($fp, $ret); 1721 fseek($fp, 0); 1722 $ret = $fp; 1723 } 1724 if (!$return_resource) 1725 { 1726 $fp = $ret; 1727 $ret = ''; 1728 while(!feof($fp)) 1729 { 1730 $ret .= fread($fp, 8192); 1731 } 1732 fclose($fp); 1733 } 1734 } 1735 //error_log(__METHOD__."('$id') returning ".gettype($ret).'='.array2string($ret)); 1736 return $ret; 1737 } 1738 1739 /** 1740 * Check the file access perms for $app/id and given user $user 1741 * 1742 * If $user given and != current user AND app does not set file_access_user=true, 1743 * allways return false, as there's no way to check access for an other user! 1744 * 1745 * @ToDo $rel_path is not yet implemented, as no app use it currently 1746 * @param string $app 1747 * @param string|int $id id of entry 1748 * @param int $required =Acl::READ Acl::{READ|EDIT} 1749 * @param string $rel_path =null 1750 * @param int $user =null default null = current user 1751 * @return boolean true if access granted, false otherwise 1752 */ 1753 static function file_access($app,$id,$required=Acl::READ,$rel_path=null,$user=null) 1754 { 1755 // are we called for an other user 1756 if ($user && $user != $GLOBALS['egw_info']['user']['account_id']) 1757 { 1758 // check if app supports file_access WITH 4th $user parameter --> return false if not 1759 if (!self::get_registry($app,'file_access_user') || !($method = self::get_registry($app,'file_access'))) 1760 { 1761 $ret = false; 1762 $err = "(no file_access_user)"; 1763 } 1764 else 1765 { 1766 $ret = self::exec($method, array($id, $required, $rel_path, $user)); 1767 $err = "(from $method)"; 1768 } 1769 //error_log(__METHOD__."('$app',$id,$required,'$rel_path',$user) returning $err ".array2string($ret)); 1770 return $ret; 1771 } 1772 1773 $cache =& self::get_cache($app,$id,'file_access'); 1774 1775 if (!isset($cache) || $required == Acl::EDIT && !($cache & $required)) 1776 { 1777 if(($method = self::get_registry($app,'file_access'))) 1778 { 1779 $cache |= self::exec($method, array($id, $required, $rel_path)) ? $required|Acl::READ : 0; 1780 } 1781 else 1782 { 1783 $cache |= self::title($app,$id) ? Acl::READ|Acl::EDIT : 0; 1784 } 1785 //error_log(__METHOD__."($app,$id,$required,$rel_path) got $cache --> ".($cache & $required ? 'true' : 'false')); 1786 } 1787 //else error_log(__METHOD__."($app,$id,$required,$rel_path) using cached value $cache --> ".($cache & $required ? 'true' : 'false')); 1788 return !!($cache & $required); 1789 } 1790 1791 /** 1792 * Execute a static method or $app.$class.$method string with given arguments 1793 * 1794 * In case of a non-static method as shared instance of the class is used. 1795 * This is a replacement for global ExecMethod(2) functions. 1796 * 1797 * @param callable|string $method "$app.$class.$method" or static method 1798 * @param array $params array with arguments incl. references 1799 * @return mixed 1800 */ 1801 protected static function exec($method, array $params=array()) 1802 { 1803 static $objs = array(); 1804 1805 // static methods or callables can be called directly 1806 if (is_callable($method)) 1807 { 1808 return call_user_func_array($method, $params); 1809 } 1810 1811 list($app, $class, $m) = $parts = explode('.', $method); 1812 if (count($parts) != 3) throw Api\Exception\WrongParameter("Wrong dot-delimited method string '$method'!"); 1813 1814 if (!isset($objs[$class])) 1815 { 1816 if (!class_exists($class)) 1817 { 1818 require_once EGW_INCLUDE_ROOT.'/'.$app.'/inc/class.'.$class.'.inc.php'; 1819 } 1820 $objs[$class] = new $class; 1821 } 1822 // php5.6+: return $objs[$class]->$m(...$params); 1823 return call_user_func_array(array($objs[$class], $m), $params); 1824 } 1825} 1826Link::init_static(); 1827