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('&amp;','&quot;','&lt;','&gt;'),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