From 0ba4283f9f3b64e07039892bece744070c0ffd9a Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Mon, 7 Mar 2016 08:46:32 +0000 Subject: [PATCH] move egw_link to Api\Link --- api/src/Contacts.php | 13 +- api/src/Link.php | 1732 +++++++++++++++++++++++++++ api/src/Link/Storage.php | 483 ++++++++ api/src/Storage/Customfields.php | 11 +- api/src/Storage/Tracking.php | 25 +- api/src/Vfs/Links/StreamWrapper.php | 8 +- phpgwapi/inc/class.egw_link.inc.php | 1714 +------------------------- phpgwapi/inc/class.solink.inc.php | 466 +------ 8 files changed, 2254 insertions(+), 2198 deletions(-) create mode 100644 api/src/Link.php create mode 100644 api/src/Link/Storage.php diff --git a/api/src/Contacts.php b/api/src/Contacts.php index 72fe5e839a..e7d8ae31c3 100755 --- a/api/src/Contacts.php +++ b/api/src/Contacts.php @@ -18,7 +18,6 @@ namespace EGroupware\Api; // explicitly reference classes still in phpgwapi use categories; -use egw_link; use calendar_bo; // to_do: do NOT require it, just use if there @@ -813,11 +812,11 @@ class Contacts extends Contacts\Storage $delete['tid'] = self::DELETED_TYPE; if ($check_etag) $delete['etag'] = $check_etag; if (($ok = $this->save($delete))) $ok = true; // we have to return true or false - egw_link::unlink(0,'addressbook',$id,'','','',true); + Link::unlink(0,'addressbook',$id,'','','',true); } elseif (($ok = parent::delete($id,$check_etag))) { - egw_link::unlink(0,'addressbook',$id); + Link::unlink(0,'addressbook',$id); } // Don't notify of final purge @@ -996,12 +995,12 @@ class Contacts extends Contacts\Storage $GLOBALS['egw']->hooks->process($to_write,False,True); // called for every app now, not only enabled ones)); } // Notify linked apps about changes in the contact data - egw_link::notify_update('addressbook', $contact['id'], $contact); + Link::notify_update('addressbook', $contact['id'], $contact); // Check for restore of deleted contact, restore held links if($old && $old['tid'] == self::DELETED_TYPE && $contact['tid'] != self::DELETED_TYPE) { - egw_link::restore('addressbook', $contact['id']); + Link::restore('addressbook', $contact['id']); } // Record change history for sql - doesn't work for LDAP accounts @@ -1805,11 +1804,11 @@ class Contacts extends Contacts\Storage { continue; } - foreach(egw_link::get_links('addressbook',$contact['id']) as $data) + foreach(Link::get_links('addressbook',$contact['id']) as $data) { //_debug_array(array('function'=>__METHOD__,'line'=>__LINE__,'app'=>'addressbook','id'=>$contact['id'],'data:'=>$data,'target'=>$target['id'])); // info_from and info_link_id (main link) - $newlinkID = egw_link::link('addressbook',$target['id'],$data['app'],$data['id'],$data['remark'],$target['owner']); + $newlinkID = Link::link('addressbook',$target['id'],$data['app'],$data['id'],$data['remark'],$target['owner']); //_debug_array(array('newLinkID'=>$newlinkID)); if ($newlinkID) { diff --git a/api/src/Link.php b/api/src/Link.php new file mode 100644 index 0000000000..401b5bf7c5 --- /dev/null +++ b/api/src/Link.php @@ -0,0 +1,1732 @@ + + * @copyright 2001-2016 by RalfBecker@outdoor-training.de + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package api + * @subpackage link + * @version $Id$ + */ + +namespace EGroupware\Api; + +// explicitly reference classes still in phpgwapi +use html; +use egw; // on_shutdown +use egw_json_response; + +/** + * Generalized linking between entries of EGroupware apps + * + * Please note: this class can NOT and does not need to be initialised, all methods are static + * + * To participate in the linking an applications has to implement the following hooks: + * + * /** + * * Hook called by link-class to include app in the appregistry of the linkage + * * + * * @param array|string $location location and other parameters (not used) + * * @return array with method-names + * *% + * function search_link($location) + * { + * return array( + * 'query' => 'app.class.link_query', // method to search app for a pattern: array link_query(string $pattern, array $options) + * 'title' => 'app.class.link_title', // method to return title of an entry of app: string/false/null link_title(int/string $id) + * 'titles' => 'app.class.link_titles', // method to return multiple titles: array link_title(array $ids) + * 'view' => array( // get parameters to view an entry of app + * 'menuaction' => 'app.class.method', + * ), + * 'types' => array( // Optional list of sub-types to filter (eg organisations), app to handle different queries + * 'type_key' => array( + * 'name' => 'Human Reference', + * 'icon' => 'app/icon' // Optional icon to use for that sub-type + * ) + * ), + * 'view_id' => 'app_id', // name of get parameter of the id + * 'view_popup' => '400x300', // size of popup (XxY), if view is in popup + * 'view_list' => 'app.class.method' // deprecated use 'list' instead + * 'list' => array( // Method to be called to display a list of links, method should check $_GET['search'] to filter + * 'menuaction' => 'app.class.method', + * ), + * 'list_popup' => '400x300' + * 'add' => array( // get parameter to add an empty entry to app + * 'menuaction' => 'app.class.method', + * ), + * 'add_app' => 'link_app', // name of get parameter to add links to other app + * 'add_id' => 'link_id', // --------------------- " ------------------- id + * 'add_popup' => '400x300', // size of popup (XxY), if add is in popup + * 'notify' => 'app.class.method', // method to be called if an other applications liks or unlinks with app: notify(array $data) + * 'file_access' => 'app.class.method', // method to be called to check file access rights of a given user, see links_stream_wrapper class + * // boolean file_access(string $id,int $check,string $rel_path=null,int $user=null) + * 'file_access_user' => false, // true if file_access method supports 4th parameter $user, if app is NOT supporting it + * // Link::file_access() returns false for $user != current user! + * 'file_dir' => 'app/sub', // sub file dir for uploaded files/links + * 'find_extra' => array('name_preg' => '/^(?!.picture.jpg)$/') // extra options to Vfs::find, to eg. remove some files from the list of attachments + * 'edit' => array( + * 'menuaction' => 'app.class.method', + * ), + * 'edit_id' => 'app_id', + * 'edit_popup' => '400x300', + * 'name' => 'Some name', // Name to use instead of app-name + * 'icon' => 'app/icon', // Optional icon to use instead of app-icon + * 'entry' => 'Contact', // Optional name for single entry of app, eg. "contact" used instead of appname + * 'entries' => 'Contacts', // Optional name for multiple entries of app, eg. "contacts" used instead of appname + * 'mime' => array( // Optional register mime-types application can open + * 'text/something' => array( + * 'mime_url' => $attr, // either mime_url or mime_data is required for server-side processing! + * 'mime_data' => $attr, // md5-hash returned from Link::set_data() to retrive content (only server-side) + * 'menuaction' => 'app.class.method', // method to call + * 'mime_popup' => '400x300', // optional size of popup + * 'mime_target' => '_self', // optional target, default _blank + * // other get-parameters to set in url + * ), + * // further mime types supported ... + * ), + * 'fetch' => 'app.class.method', // method to return entry data for a given id. the method called should support id, and expected mime-type + * // basically you should return something like array(id, title, mimetype, body, linked-files) + * 'additional' => array( // allow one app to register sub-types, + * 'app-sub' => array( // different from 'types' approach above + * // every value defined above + * ) + * ) + * } + * All entries are optional, thought you only get conected functionality, if you implement them ... + * + * The BO-layer implementes some extra features on top of the so-layer: + * 1) It handles links to not already existing entries. This is used by the eTemplate link-widget, which allows to + * setup links even for new / not already existing entries, before they get saved. + * 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 + * (not the return-value) after saveing your new entry again to the link static function. + * 2) Attaching files: they are saved in the vfs and not the link-table (!). + * Attached files are stored under $vfs_basedir='/infolog' in the vfs! + * 3) It manages the link-registry, in which apps can register themselfs by implementing some hooks + * 4) It notifies apps, who registered for that service, about changes in the links their entries + * + * Modification times in links (and deleted timestamp) are always in server-time! + * (We dont convert them here, as most apps ignore them anyway) + */ +class Link extends Link\Storage +{ + /** + * appname used for returned attached files (!= 'filemanager'!) + */ + const VFS_APPNAME = 'file'; // pseudo-appname for own file-attachments in vfs, this is NOT the vfs-app + + /** + * Appname used of files stored via Link::set_data() + */ + const DATA_APPNAME = 'egw-data'; + + /** + * appname used for linking existing files to VFS + */ + const VFS_LINK = 'link'; + + /** + * Baseurl for the attachments in the vfs + */ + const VFS_BASEURL = 'vfs://default/apps'; + /** + * Turns on debug-messages + */ + const DEBUG = false; + /** + * other apps can participate in the linking by implementing a 'search_link' hook, which + * has to return an array in the format of an app_register entry below + * + * @var array + */ + static $app_register = array( + 'home-accounts' => array( // user need run-rights for home + 'app' => 'home', + 'name' => 'Accounts', + 'icon' => 'addressbook/accounts', + 'query' => 'accounts::link_query', + 'title' => 'common::grab_owner_name', + 'view' => array('menuaction'=>'addressbook.addressbook_ui.view','ajax'=>'true'), + 'view_id' => 'account_id' + ), + 'home' => array( + // handling of text or pdf files by browser in a popup window + 'mime' => array( + 'application/pdf' => array( + 'mime_popup' => '640x480', + 'mime_target' => '_blank', + ), + '/^text\\/(plain|html|diff)/' => array( // text/(mimetypes which can be opened as recognised popups) + 'mime_popup' => '640x480', + 'mime_target' => '_blank', + ), + '/^image\\//' => array( // image + 'mime_popup' => '640x480', + 'mime_target' => '_blank', + ), + ), + ), + ); + /** + * Caches link titles for a better performance + * + * @var array + */ + private static $title_cache = array(); + + /** + * Cache file access permissions + * + * @var array + */ + private static $file_access_cache = array(); + + /** + * Private constructor to forbid instanciated use + * + */ + private function __construct() + { + + } + + /** + * initialize our static vars + */ + static function init_static( ) + { + // FireFox 36 can not display pdf with it's internal viewer in an iframe used by mobile theme/template for popups + // same is true for all mobile devices + if (html::$user_agent == 'firefox' && $GLOBALS['egw_info']['user']['preferences']['common']['theme'] == 'mobile' || html::$ua_mobile) + { + unset(self::$app_register['home']['mime']['application/pdf']); + } + // other apps can participate in the linking by implementing a search_link hook, which + // has to return an array in the format of an app_register entry + // for performance reasons, we do it only once / cache it in the session + if (!($search_link_hooks = Cache::getSession(__CLASS__, 'search_link_hooks'))) + { + $search_link_hooks = $GLOBALS['egw']->hooks->process('search_link',array(), (bool)$GLOBALS['egw_info']['flags']['async-service']); + Cache::setSession(__CLASS__, 'search_link_hooks', $search_link_hooks); + } + if (is_array($search_link_hooks)) + { + foreach($search_link_hooks as $app => $data) + { + // allow apps to register additional types + if (isset($data['additional'])) + { + foreach($data['additional'] as $name => $values) + { + $values['app'] = $app; // store name of registring app, to be able to check access + self::$app_register[$name] = $values; + } + unset($data['additional']); + } + // support deprecated view_list attribute instead of new index attribute + if (isset($data['view_list']) && !isset($data['list'])) + { + $data['list'] = array('menuaction' => $data['view_list']); + } + elseif(isset($data['list']) && !isset($data['view_list'])) + { + $data['view_list'] = $data['list']['menuaction']; + } + if (is_array($data)) + { + self::$app_register[$app] = $data; + } + } + } + // disable ability to link to accounts for non-admins, if account-selection is disabled + if ($GLOBALS['egw_info']['user']['preferences']['common']['account_selection'] == 'none' && + !isset($GLOBALS['egw_info']['user']['apps']['admin'])) + { + unset(self::$app_register['home-accounts']); + } + if (!(self::$title_cache = Cache::getSession(__CLASS__, 'link_title_cache'))) + { + self::$title_cache = array(); + } + if (!(self::$file_access_cache = Cache::getSession(__CLASS__, 'link_file_access_cache'))) + { + self::$file_access_cache = array(); + } + + // register self::save_session_cache to run on shutdown + egw::on_shutdown(array(__CLASS__, 'save_session_cache')); + + //error_log(__METHOD__.'() items in title-cache: '.count(self::$title_cache).' file-access-cache: '.count(self::$file_access_cache)); + } + + /** + * Get clientside relevant attributes from app registry in json format + * + * Only transfering relevant information cuts approx. half of the size. + * Also only transfering information relevant to apps user has access too. + * Important eg. for mime-registry, to not use calendar for opening iCal files, if user has no calendar! + * As app can store additonal types, we have to check the registring app $data['app'] too! + * + * @return string json encoded object with app: object pairs with attributes "(view|add|edit)(|_id|_popup)" + */ + public static function json_registry() + { + $to_json = array(); + foreach(self::$app_register as $app => $data) + { + if (isset($GLOBALS['egw_info']['user']['apps'][$app]) || + isset($data['app']) && isset($GLOBALS['egw_info']['user']['apps'][$data['app']])) + { + $to_json[$app] = array_intersect_key($data, array_flip(array( + 'view','view_id','view_popup', + 'add','add_app','add_id','add_popup', + 'edit','edit_id','edit_popup', + 'list','list_popup', + 'name','icon','query', + 'mime','entry','entries', + ))); + } + } + return json_encode($to_json); + } + + /** + * Called by egw::shutdown to store the title-cache in session and run notifications + * + * Would probably better called shutdown as well. + */ + static function save_session_cache() + { + if (isset($GLOBALS['egw']->session)) // eg. cron-jobs use it too, without any session + { + //error_log(__METHOD__.'() items in title-cache: '.count(self::$title_cache).' file-access-cache: '.count(self::$file_access_cache)); + Cache::setSession(__CLASS__, 'link_title_cache', self::$title_cache); + Cache::setSession(__CLASS__, 'link_file_access_cache', self::$file_access_cache); + } + } + + /** + * creats a link between $app1,$id1 and $app2,$id2 - $id1 does NOT need to exist yet + * + * Does NOT check if link already exists. + * File-attachments return a negative link-id !!! + * + * @param string $app1 app of $id1 + * @param string|array &$id1 id of item to linkto or 0 if item not yet created or array with links + * of not created item or $file-array if $app1 == self::VFS_APPNAME (see below). + * If $id==0 it will be set on return to an array with the links for the new item. + * @param string|array $app2 app of 2.linkend or array with links ($id2 not used) + * @param string $id2 ='' id of 2. item of $file-array if $app2 == self::VFS_APPNAME or self::DATA_APPNAME + * $file array with informations about the file in format of the etemplate file-type + * $file['name'] name of the file (no directory) + * $file['type'] mime-type of the file + * $file['tmp_name'] name of the uploaded file (incl. directory) for self::VFS_APPNAME or + * $file['egw_data'] id of Link::set_data() call for self::DATA_APPNAME + * @param string $remark ='' Remark to be saved with the link (defaults to '') + * @param int $owner =0 Owner of the link (defaults to user) + * @param int $lastmod =0 timestamp of last modification (defaults to now=time()) + * @param int $no_notify =0 &1 dont notify $app1, &2 dont notify $app2 + * @return int/boolean False (for db or param-error) or on success link_id (Please not the return-value of $id1) + */ + static function link( $app1,&$id1,$app2,$id2='',$remark='',$owner=0,$lastmod=0,$no_notify=0 ) + { + if (self::DEBUG) + { + echo "

Link::link('$app1',$id1,'".print_r($app2,true)."',".print_r($id2,true).",'$remark',$owner,$lastmod)

\n"; + } + if (!$app1 || !$app2 || $app1 == $app2 && $id1 == $id2) + { + return False; + } + if (is_array($app2) && !$id2) + { + reset($app2); + $link_id = True; + while ($link_id && list(,$link) = each($app2)) + { + if (!is_array($link)) // check for unlink-marker + { + //echo "link='$link' is no array
\n"; + continue; + } + if (is_array($id1) || !$id1) // create link only in $id1 array + { + self::link($app1, $id1, $link['app'], $link['id'], $link['remark'],$link['owner'],$link['lastmod']); + continue; + } + switch ($link['app']) + { + case self::DATA_APPNAME: + if (!($link['id']['tmp_name'] = self::get_data($link['id']['egw_data'], true))) + { + $link_id = false; + break; + } + // fall through + case self::VFS_APPNAME: + $link_id = self::attach_file($app1,$id1,$link['id'],$link['remark']); + break; + + case self::VFS_LINK: + $link_id = self::link_file($app1,$id1, $link['id'],$link['remark']); + break; + + default: + $link_id = Link\Storage::link($app1,$id1,$link['app'],$link['id'], + $link['remark'],$link['owner'],$link['lastmod']); + // notify both sides + if (!($no_notify&2)) self::notify('link',$link['app'],$link['id'],$app1,$id1,$link_id); + if (!($no_notify&1)) self::notify('link',$app1,$id1,$link['app'],$link['id'],$link_id); + break; + } + } + return $link_id; + } + if (is_array($id1) || !$id1) // create link only in $id1 array + { + if (!is_array($id1)) + { + $id1 = array( ); + } + $link_id = self::temp_link_id($app2,$id2); + + $id1[$link_id] = array( + 'app' => $app2, + 'id' => $id2, + 'remark' => $remark, + 'owner' => $owner, + 'link_id' => $link_id, + 'lastmod' => time() + ); + if (self::DEBUG) + { + _debug_array($id1); + } + return $link_id; + } + if ($app1 == self::VFS_LINK) + { + return self::link_file($app2,$id2,$id1,$remark); + } + elseif ($app2 == self::VFS_LINK) + { + return self::link_file($app1,$id1,$id2,$remark); + } + if ($app1 == self::VFS_APPNAME) + { + return self::attach_file($app2,$id2,$id1,$remark); + } + elseif ($app2 == self::VFS_APPNAME) + { + return self::attach_file($app1,$id1,$id2,$remark); + } + $link_id = Link\Storage::link($app1,$id1,$app2,$id2,$remark,$owner); + + if (!($no_notify&2)) self::notify('link',$app2,$id2,$app1,$id1,$link_id); + if (!($no_notify&1)) self::notify('link',$app1,$id1,$app2,$id2,$link_id); + + return $link_id; + } + + /** + * generate temporary link_id used as array-key + * + * @param string $app app-name + * @param mixed $id + * @return string + */ + static function temp_link_id($app,$id) + { + return $app.':'.(!in_array($app, array(self::VFS_APPNAME,self::VFS_LINK, self::DATA_APPNAME)) ? $id : $id['name']); + } + + /** + * returns array of links to $app,$id (reimplemented to deal with not yet created items) + * + * @param string $app appname + * @param string|array $id id(s) in $app + * @param string $only_app ='' if set return only links from $only_app (eg. only addressbook-entries) or NOT from if $only_app[0]=='!' + * @param string $order ='link_lastmod DESC' defaults to newest links first + * @param boolean $cache_titles =false should all titles be queryed and cached (allows to query each link app only once!) + * This option also removes links not viewable by current user from the result! + * @param boolean $deleted =false Include links that have been flagged as deleted, waiting for purge of linked record. + * @param int $limit =null number of entries to return, only affects links, attachments are allways reported! + * @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 + */ + static function get_links($app, $id, $only_app='', $order='link_lastmod DESC',$cache_titles=false, $deleted=false, $limit=null) + { + if (self::DEBUG) echo "

Link::get_links(app='$app',id='$id',only_app='$only_app',order='$order',deleted='$deleted')

\n"; + + if (is_array($id) || !$id) + { + $ids = array(); + if (is_array($id)) + { + if (($not_only = $only_app[0] == '!')) + { + $only_app = substr(1,$only_app); + } + foreach (array_reverse($id) as $link) + { + if (is_array($link) // check for unlink-marker + && !($only_app && $not_only == ($link['app'] == $only_app))) + { + $ids[$link['link_id']] = $only_app ? $link['id'] : $link; + } + } + } + return $ids; + } + $ids = Link\Storage::get_links($app, $id, $only_app, $order, $deleted, $limit); + if (empty($only_app) || $only_app == self::VFS_APPNAME || + ($only_app[0] == '!' && $only_app != '!'.self::VFS_APPNAME)) + { + if (($vfs_ids = self::list_attached($app,$id))) + { + $ids += $vfs_ids; + } + } + //echo "ids=
"; print_r($ids); echo "
\n"; + if ($cache_titles) + { + // agregate links by app + $app_ids = array(); + foreach($ids as $link) + { + $app_ids[$link['app']][] = $link['id']; + } + foreach($app_ids as $appname => $a_ids) + { + self::titles($appname,array_unique($a_ids)); + } + // remove links, current user has no access, from result + foreach($ids as $key => $link) + { + if (!self::title($link['app'],$link['id'])) + { + unset($ids[$key]); + } + } + reset($ids); + } + return $ids; + } + + /** + * Query the links of multiple entries of one application + * + * @ToDo also query the attachments in a single query, eg. via a directory listing of /apps/$app + * @param string $app + * @param array $ids + * @param boolean $cache_titles =true should all titles be queryed and cached (allows to query each link app only once!) + * @param string $only_app if set return only links from $only_app (eg. only addressbook-entries) or NOT from if $only_app[0]=='!' + * @param string $order ='link_lastmod DESC' defaults to newest links first + * @param boolean $deleted =false Include links that have been flagged as deleted, waiting for purge of linked record. + * @return array of $id => array($links) pairs + */ + static function get_links_multiple($app,array $ids,$cache_titles=true,$only_app='',$order='link_lastmod DESC', $deleted=false ) + { + if (self::DEBUG) echo "

".__METHOD__."('$app',".print_r($ids,true).",$cache_titles,'$only_app','$order')

\n"; + + if (!$ids) + { + return array(); // no ids are linked to nothing + } + $links = Link\Storage::get_links($app,$ids,$only_app,$order,$deleted); + + if (empty($only_app) || $only_app == self::VFS_APPNAME || + ($only_app[0] == '!' && $only_app != '!'.self::VFS_APPNAME)) + { + // todo do that in a single query, eg. directory listing, too + foreach($ids as $id) + { + if (!isset($links[$id])) + { + $links[$id] = array(); + } + if (($vfs_ids = self::list_attached($app,$id))) + { + $links[$id] += $vfs_ids; + } + } + } + if ($cache_titles) + { + // agregate links by app + $app_ids = array(); + foreach($links as &$targets) + { + foreach($targets as $link) + { + if (is_array($link)) $app_ids[$link['app']][] = $link['id']; + } + } + foreach($app_ids as $app => $a_ids) + { + self::titles($app,array_unique($a_ids)); + } + } + return $links; + } + + /** + * Read one link specified by it's link_id or by the two end-points + * + * If $id is an array (links not yet created) only link_ids are allowed. + * + * @param int|string $app_link_id > 0 link_id of link or app-name of link + * @param string|array $id ='' id if $app_link_id is an appname or array with links, if 1. entry not yet created + * @param string $app2 ='' second app + * @param string $id2 ='' id in $app2 + * @return array with link-data or False + */ + static function get_link($app_link_id,$id='',$app2='',$id2='') + { + if (self::DEBUG) + { + echo '

'.__METHOD__."($app_link_id,$id,$app2,$id2)

\n"; echo function_backtrace(); + } + if (is_array($id)) + { + if (strpos($app_link_id,':') === false) $app_link_id = self::temp_link_id($app2,$id2); // create link_id of temporary link, if not given + + if (isset($id[$app_link_id]) && is_array($id[$app_link_id])) // check for unlinked-marker + { + return $id[$app_link_id]; + } + return False; + } + if ((int)$app_link_id < 0 || $app_link_id == self::VFS_APPNAME || $app2 == self::VFS_APPNAME) + { + if ((int)$app_link_id < 0) // vfs link_id ? + { + return self::fileinfo2link(-$app_link_id); + } + if ($app_link_id == self::VFS_APPNAME) + { + return self::info_attached($app2,$id2,$id); + } + return self::info_attached($app_link_id,$id,$id2); + } + return Link\Storage::get_link($app_link_id,$id,$app2,$id2); + } + + /** + * Remove link with $link_id or all links matching given $app,$id + * + * Note: if $link_id != '' and $id is an array: unlink removes links from that array only + * unlink has to be called with &$id to see the result (depricated) or unlink2 has to be used !!! + * + * @param $link_id link-id to remove if > 0 + * @param string $app ='' appname of first endpoint + * @param string|array $id ='' id in $app or array with links, if 1. entry not yet created + * @param int $owner =0 account_id to delete all links of a given owner, or 0 + * @param string $app2 ='' app of second endpoint + * @param string $id2 ='' id in $app2 + * @param boolean $hold_for_purge Don't really delete the link, just mark it as deleted and wait for final delete + * @return the number of links deleted + */ + static function unlink($link_id,$app='',$id='',$owner=0,$app2='',$id2='',$hold_for_purge=false) + { + return self::unlink2($link_id,$app,$id,$owner,$app2,$id2,$hold_for_purge); + } + + /** + * Remove link with $link_id or all links matching given $app,$id + * + * @param $link_id link-id to remove if > 0 + * @param string $app ='' appname of first endpoint + * @param string|array &$id='' id in $app or array with links, if 1. entry not yet created + * @param int $owner =0 account_id to delete all links of a given owner, or 0 + * @param string $app2 ='' app of second endpoint, or !file (other !app are not yet supported!) + * @param string $id2 ='' id in $app2 + * @param boolean $hold_for_purge Don't really delete the link, just mark it as deleted and wait for final delete + * @return the number of links deleted + */ + static function unlink2($link_id,$app,&$id,$owner=0,$app2='',$id2='',$hold_for_purge=false) + { + if (self::DEBUG) + { + echo "

Link::unlink('$link_id','$app',".array2string($id).",'$owner','$app2','$id2', $hold_for_purge)

\n"; + } + if ($link_id < 0) // vfs-link? + { + return self::delete_attached(-$link_id); + } + elseif ($app == self::VFS_APPNAME) + { + return self::delete_attached($app2,$id2,$id); + } + elseif ($app2 == self::VFS_APPNAME) + { + return self::delete_attached($app,$id,$id2); + } + if (!is_array($id)) + { + if (!$link_id && !$app2 && !$id2 && $app2 != '!'.self::VFS_APPNAME) + { + self::delete_attached($app,$id); // deleting all attachments + self::delete_cache($app,$id); + } + + // Log in history + if($link_id && (!$app || !$app2)) + { + // Need to load it first + $link = self::get_link($link_id); + $app = $link['link_app1']; + $id = $link['link_id1']; + $app2 = $link['link_app2']; + $id2 = $link['link_id2']; + } + if ($app && $app2) + { + Storage\History::static_add($app,$id,$GLOBALS['egw_info']['user']['account_id'],'~link~','',$app2.':'.$id2); + Storage\History::static_add($app2,$id2,$GLOBALS['egw_info']['user']['account_id'],'~link~','',$app.':'.$id); + } + $deleted =& Link\Storage::unlink($link_id,$app,$id,$owner,$app2 != '!'.self::VFS_APPNAME ? $app2 : '',$id2,$hold_for_purge); + + // only notify on real links, not the one cached for writing or fileattachments + self::notify_unlink($deleted); + + return count($deleted); + } + if (!$link_id) $link_id = self::temp_link_id($app2,$id2); // create link_id of temporary link, if not given + + if (isset($id[$link_id])) + { + $id[$link_id] = False; // set the unlink marker + + if (self::DEBUG) + { + _debug_array($id); + } + return True; + } + return False; + } + + /** + * get list/array of link-aware apps the user has rights to use + * + * @param string $must_support capability the apps need to support, eg. 'add', default ''=list all apps + * @return array with app => title pairs + */ + static function app_list($must_support='') + { + $apps = array(); + foreach(self::$app_register as $type => $reg) + { + if ($must_support && !isset($reg[$must_support])) continue; + + list($app) = explode('-', $type); + if ($GLOBALS['egw_info']['user']['apps'][$app]) + { + $apps[$type] = lang(self::get_registry($type, 'name')); + } + } + return $apps; + } + + /** + * Searches for a $pattern in the entries of $app + * + * @param string $app app to search + * @param string $pattern pattern to search + * @param string $type Search only a certain sub-type of records (optional) + * @return array with $id => $title pairs of matching entries of app + */ + static function query($app,$pattern, &$options = array()) + { + if ($app == '' || !is_array($reg = self::$app_register[$app]) || !isset($reg['query'])) + { + return array(); + } + $method = $reg['query']; + + if (self::DEBUG) + { + echo "

Link::query('$app','$pattern') => '$method'

\n"; + echo "Options: "; _debug_array($options); + } + + // See etemplate's nextmatch widget, following was copied from there + // allow static callbacks + if(strpos($method,'::') !== false) + { + // workaround for php < 5.3: do NOT call it static, but allow application code to specify static callbacks + if (version_compare(PHP_VERSION,'5.3','<')) list($class,$method) = explode('::',$method); + } + else + { + list($app,$class,$method) = explode('.',$method); + } + if ($class) + { + if (!$app && !is_object($GLOBALS[$class])) + { + $GLOBALS[$class] = new $class(); + } + if (is_object($GLOBALS[$class])) // use existing instance (put there by a previous CreateObject) + { + $obj = $GLOBALS[$class]; + } + else + { + $obj = CreateObject($app.'.'.$class); + } + } + if(is_callable($method)) // php5.3+ call + { + $result = call_user_func($method,$pattern,$options); + } + elseif(is_object($obj) && method_exists($obj,$method)) + { + $result = $obj->$method($pattern,$options); + } + else + { + // if there is no object or no method, give a more explaining error message + throw new egw_exception_assertion_failed("Object has no method '$reg[query]'!"); + } + + if (!isset($options['total'])) + { + $options['total'] = count($result); + } + if (is_array($result) && (isset($options['start']) || (isset($options['num_rows']) && count($result) > $options['num_rows']))) + { + $result = array_slice($result, $options['start'], (isset($options['num_rows']) ? $options['num_rows'] : count($result)), true); + } + + return $result; + } + + /** + * returns the title (short description) of entry $id and $app + * + * @param string $app appname + * @param string $id id in $app + * @param array $link =null link-data for file-attachments + * @return string/boolean string with title, null if $id does not exist in $app or false if no perms to view it + */ + static function title($app,$id,$link=null) + { + if (!$id) return ''; + + $title =& self::get_cache($app,$id); + if (isset($title) && !empty($title) && !is_array($id)) + { + if (self::DEBUG) echo '

'.__METHOD__."('$app','$id')='$title' (from cache)

\n"; + return $title; + } + if ($app == self::VFS_APPNAME) + { + if (is_array($id) && $link) + { + $link = $id; + $title = Vfs::decodePath($link['name']); + } + else + { + $title = $id; + } + /* disabling mime-type and size in link-title of attachments, as it clutters the UI + and users dont need it most of the time. These details can allways be views in filemanager. + if (is_array($link)) + { + $title .= ': '.$link['type'] . ' '.Vfs::hsize($link['size']); + }*/ + if (self::DEBUG) echo '

'.__METHOD__."('$app','$id')='$title' (file)

\n"; + return $title; + } + if ($app == '' || !is_array($reg = self::$app_register[$app]) || !isset($reg['title'])) + { + if (self::DEBUG) echo "

".__METHOD__."('$app','$id') something is wrong!!!

\n"; + return false; //array(); // not sure why it should return an array on failure, as the description states boolean/string + } + $method = $reg['title']; + + if (true) $title = ExecMethod($method,$id); + + if ($id && is_null($title)) // $app,$id has been deleted ==> unlink all links to it + { + static $unlinking = array(); + // check if we are already trying to unlink the entry, to avoid an infinit recursion + if (!isset($unlinking[$app]) || !isset($unlinking[$app][$id])) + { + $unlinking[$app][$id] = true; + self::unlink(0,$app,$id); + unset($unlinking[$app][$id]); + } + if (self::DEBUG) echo '

'.__METHOD__."('$app','$id') unlinked, as $method returned null

\n"; + return False; + } + if (self::DEBUG) echo '

'.__METHOD__."('$app','$id')='$title' (from $method)

\n"; + + return $title; + } + + /** + * Maximum number of titles to query from an application at once (to NOT trash mysql) + */ + const MAX_TITLES_QUERY = 100; + + /** + * Query the titles off multiple id's of one app + * + * Apps can implement that hook, if they have a quicker (eg. less DB queries) method to query the title of multiple entries. + * If it's not implemented, we call the regular title method multiple times. + * + * @param string $app + * @param array $ids + */ + static function titles($app,array $ids) + { + if (self::DEBUG) + { + echo "

".__METHOD__."($app,".implode(',',$ids).")

\n"; + } + $titles = $ids_to_query = array(); + foreach($ids as $id) + { + $title =& self::get_cache($app,$id); + if (!isset($title)) + { + if (isset(self::$app_register[$app]['titles'])) + { + $ids_to_query[] = $id; // titles method --> collect links to query at once + } + else + { + $title = self::title($app,$id); // no titles method --> fallback to query each link separate + } + } + $titles[$id] = $title; + } + if ($ids_to_query) + { + for ($n = 0; ($ids = array_slice($ids_to_query,$n*self::MAX_TITLES_QUERY,self::MAX_TITLES_QUERY)); ++$n) + { + foreach(ExecMethod(self::$app_register[$app]['titles'],$ids) as $id => $t) + { + $title =& self::get_cache($app,$id); + $titles[$id] = $title = $t; + } + } + } + return $titles; + } + + /** + * Add new entry to $app, evtl. already linked to $to_app, $to_id + * + * @param string $app appname of entry to create + * @param string $to_app ='' appname to link the new entry to + * @param string $to_id =''id in $to_app + * @return array/boolean with name-value pairs for link to add-methode of $app or false if add not supported + */ + static function add($app,$to_app='',$to_id='') + { + //echo "

Link::add('$app','$to_app','$to_id') app_register[$app] ="; _debug_array($app_register[$app]); + if ($app == '' || !is_array($reg = self::$app_register[$app]) || !isset($reg['add'])) + { + return false; + } + $params = $reg['add']; + + if ($reg['add_app'] && $to_app && $reg['add_id'] && $to_id) + { + $params[$reg['add_app']] = $to_app; + $params[$reg['add_id']] = $to_id; + } + return $params; + } + + /** + * Edit entry $id of $app + * + * @param string $app appname of entry + * @param string $id id in $app + * @param string &$popup=null on return popup size eg. '600x400' or null + * @return array|boolean with name-value pairs for link to edit-methode of $app or false if edit not supported + */ + static function edit($app,$id,&$popup=null) + { + //echo "

Link::add('$app','$to_app','$to_id') app_register[$app] ="; _debug_array($app_register[$app]); + if (empty($app) || empty($id) || !is_array($reg = self::$app_register[$app]) || !isset($reg['edit'])) + { + if ($reg && isset($reg['view'])) + { + $popup = $reg['view_popup']; + return self::view($app,$id); // fallback to view + } + return false; + } + $params = $reg['edit']; + $params[$reg['edit_id']] = $id; + + $popup = $reg['edit_popup']; + + return $params; + } + + /** + * view entry $id of $app + * + * @param string $app appname + * @param string $id id in $app + * @param array $link =null link-data for file-attachments + * @return array with name-value pairs for link to view-methode of $app to view $id + */ + static function view($app,$id,$link=null) + { + if ($app == self::VFS_APPNAME && !empty($id) && is_array($link)) + { + //return Vfs::download_url(self::vfs_path($link['app2'],$link['id2'],$link['id'],true)); + return self::mime_open(self::vfs_path($link['app2'],$link['id2'],$link['id'],true), $link['type']); + } + if ($app == '' || !is_array($reg = self::$app_register[$app]) || !isset($reg['view']) || !isset($reg['view_id'])) + { + return array(); + } + $view = $reg['view']; + + $names = explode(':',$reg['view_id']); + if (count($names) > 1) + { + $id = explode(':',$id); + while (list($n,$name) = each($names)) + { + $view[$name] = $id[$n]; + } + } + else + { + $view[$reg['view_id']] = $id; + } + return $view; + } + + /** + * Get mime-type information from app-registry + * + * Only return information from apps the user has access too (incl. registered sub-types of that apps). + * + * We prefer full matches over wildcards like "text/*" written as regexp "/^text\\//". + * + * @param string $type + * @return array with values for keys 'menuaction', 'mime_id' (path) or 'mime_url' and options 'mime_popup' and other values to pass one + */ + static function get_mime_info($type) + { + foreach(self::$app_register as $app => $registry) + { + if (isset($registry['mime']) && + (isset($GLOBALS['egw_info']['user']['apps'][$app]) || + isset($registry['app']) && isset($GLOBALS['egw_info']['user']['apps'][$registry['app']]))) + { + foreach($registry['mime'] as $mime => $data) + { + if ($mime == $type) return $data; + if ($mime[0] == '/' && preg_match($mime.'i', $type)) + { + $wildcard_mime = $data; + } + } + } + } + return isset($wildcard_mime) ? $wildcard_mime : null; + } + + /** + * Get handler (link-data) for given path and mime-type + * + * @param string $path vfs path + * @param string $type =null default to Vfs::mime_content_type($path) + * @param string &$popup=null on return popup size or null + * @return string|array string with EGw relative link, array with get-parameters for '/index.php' or null (directory and not filemanager access) + */ + static function mime_open($path, $type=null, &$popup=null) + { + if (is_null($type)) $type = Vfs::mime_content_type($path); + + if (($data = self::get_mime_info($type))) + { + if (isset($data['mime_url'])) + { + $data[$data['mime_url']] = Vfs::PREFIX.$path; + unset($data['mime_url']); + } + elseif (isset($data['mime_id'])) + { + $data[$data['mime_id']] = $path; + unset($data['mime_id']); + } + elseif(isset($data['mime_popup'])) + { + $popup = $data['mime_popup']; + } + else + { + throw new egw_exception_assertion_failed("Missing 'mime_id' or 'mime_url' for mime-type '$type'!"); + } + unset($data['mime_popup']); + } + else + { + $data = Vfs::download_url($path); + } + return $data; + } + + /** + * Check if $app uses a popup for $action + * + * @param string $app app-name + * @param string $action ='view' name of the action, atm. 'view' or 'add' + * @param array $link =null link-data for file-attachments + * @return boolean|string false if no popup is used or $app is not registered, otherwise string with the prefered popup size (eg. '640x400) + */ + static function is_popup($app, $action='view', $link=null) + { + $popup = self::get_registry($app,$action.'_popup'); + + // for files/attachments check mime-registry + if ($app == self::VFS_APPNAME && is_array($link) && !empty($link['type'])) + { + $path = self::vfs_path($link['app2'], $link['id2'], $link['id'], true); + $p = null; + if (self::mime_open($path, $link['type'], $p)) + { + $popup = $p; + } + } + //error_log(__METHOD__."('$app', '$action', ".array2string($link).') returning '.array2string($popup)); + return $popup; + } + + /** + * Check if $app is in the registry and has an entry for $name + * + * @param string $app app-name + * @param string $name name / key in the registry, eg. 'view' + * @return boolean|string false if $app is not registered, otherwise string with the value for $name + */ + static function get_registry($app,$name) + { + $reg = self::$app_register[$app]; + + if (!isset($reg)) return false; + + if (!isset($reg[$name])) // some defaults + { + switch($name) + { + case 'name': + $reg[$name] = $app; + break; + case 'icon': + if (isset($GLOBALS['egw_info']['apps'][$app]['icon'])) + { + $reg[$name] = ($GLOBALS['egw_info']['apps'][$app]['icon_app'] ? $GLOBALS['egw_info']['apps'][$app]['icon_app'] : $app). + '/'.$GLOBALS['egw_info']['apps'][$app]['icon']; + } + else + { + $reg[$name] = $app.'/navbar'; + } + break; + } + } + + return isset($reg) ? $reg[$name] : false; + } + + /** + * path to the attached files of $app/$ip or the directory for $app if no $id,$file given + * + * All link-files are based in the vfs-subdir '/apps/'.$app + * + * @param string $app appname + * @param string $id ='' id in $app + * @param string $file ='' filename + * @param boolean $just_the_path =false return url or just the vfs path + * @return string/array path or array with path and relatives, depending on $relatives + */ + static function vfs_path($app,$id='',$file='',$just_the_path=false) + { + $path = self::VFS_BASEURL; + + if ($app) + { + if( isset(self::$app_register[$app]) ) { + $reg = self::$app_register[$app]; + + if( isset($reg['file_dir']) ) { + $app = $reg['file_dir']; + } + } + + $path .= '/'.$app; + + if ($id) + { + $path .= '/'.$id; + + if ($file) + { + $path .= '/'.$file; + } + } + } + if ($just_the_path) + { + $path = parse_url($path,PHP_URL_PATH); + } + else + { + $path = Vfs::resolve_url($path); + } + //error_log(__METHOD__."($app,$id,$file,$just_the_path)=$path"); + return $path; + } + + /** + * Put a file to the corrosponding place in the VFS and set the attributes + * + * Does NO is_uploaded_file check, calling application is responsible for doing that for uploaded files! + * + * @param string $app appname to linke the file to + * @param string $id id in $app + * @param array $file informations about the file in format of the etemplate file-type + * $file['name'] name of the file (no directory) + * $file['type'] mine-type of the file + * $file['tmp_name'] name of the uploaded file (incl. directory) or resource of opened file + * @param string $comment ='' comment to add to the link + * @return int negative id of egw_sqlfs table as negative link-id's are for vfs attachments + */ + static function attach_file($app,$id,$file,$comment='') + { + $entry_dir = self::vfs_path($app,$id); + if (self::DEBUG) + { + echo "

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'

\n"; + } + if (file_exists($entry_dir) || ($Ok = mkdir($entry_dir,0,true))) + { + $Ok = Vfs::copy_uploaded($file, $p=self::vfs_path($app,$id,'',true), $comment, false); // no is_uploaded_file() check! + if (!$Ok) error_log(__METHOD__."('$app', '$id', ".array2string($file).", '$comment') called Vfs::copy_uploaded('$file[tmp_name]', '$p', '$comment', false)=".array2string($Ok)); + } + else + { + error_log(__METHOD__."($app,$id,".array2string($file).",$comment) Can't mkdir $entry_dir!"); + } + return $Ok ? -$Ok['ino'] : false; + } + + /** + * Links the entry to an existing file in the VFS + * + * @param string $app appname to link the file to + * @param string $id id in $app + * @param string $file VFS path to link to + * @param string $comment ='' comment to add to the link + */ + static function link_file($app,$id,$file)//,$comment='') + { + // Don't try to link into app dir if there is no id + if(!$id) return; + + $app_path = self::vfs_path($app,$id); + $ok = true; + if (Vfs::file_exists($app_path) || ($ok = Vfs::mkdir($app_path,0,true))) + { + if (!Vfs::stat($file)) + { + error_log(__METHOD__. ' (Link target ' . Vfs::decodePath($file) . ' not found!'); + return false; + } + } + + $link = Vfs::concat($app_path,Vfs::basename($file)); + return Vfs::symlink($file,$link); + } + /** + * 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!) + * + * @param int|string $app > 0: file_id of an attchemnt or $app/$id entry which linked to + * @param string $id ='' id in app + * @param string $fname ='' filename + * @return boolean|array false on error ($app or $id not found), array with path as key and boolean result of delete + */ + static function delete_attached($app,$id='',$fname='') + { + if ((int)$app > 0) // is file_id + { + $url = Vfs::resolve_url(Vfs\Sqlfs\StreamWrapper::id2path($app)); + } + else + { + if (empty($app) || empty($id)) + { + return False; // dont delete more than all attachments of an entry + } + $url = self::vfs_path($app,$id,$fname); + + if (!$fname || !$id) // we delete the whole entry (or all entries), which probably not exist anymore + { + $current_is_root = Vfs::$is_root; + Vfs::$is_root = true; + } + } + if (self::DEBUG) + { + echo '

'.__METHOD__."('$app','$id','$fname') url=$url

\n"; + } + // Log in history - Need to load it first + if((int)$app > 0) + { + $link = self::get_link(-$app); + if($link['app2'] && $link['id2']) + { + Storage\History::static_add($link['app2'],$link['id2'],$GLOBALS['egw_info']['user']['account_id'],'~file~','', basename($url)); + } + } + if (($Ok = !file_exists($url) || Vfs::remove($url,true)) && ((int)$app > 0 || $fname)) + { + // try removing the dir, in case it's empty + @Vfs::rmdir(Vfs::dirname($url)); + } + if (!is_null($current_is_root)) + { + Vfs::$is_root = $current_is_root; + } + return $Ok; + } + + /** + * converts the infos vfs has about a file into a link + * + * @param string $app appname + * @param string $id id in app + * @param string $filename filename + * @return array 'kind' of link-array + */ + static function info_attached($app,$id,$filename) + { + $path = self::vfs_path($app,$id,$filename,true); + if (!($stat = Vfs::stat($path,STREAM_URL_STAT_QUIET))) + { + return false; + } + return self::fileinfo2link($stat,$path); + } + + /** + * converts a fileinfo (row in the vfs-db-table) in a link + * + * @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 + * @return array a 'kind' of link-array + */ + static function fileinfo2link($fileinfo,$url=null) + { + if (!is_array($fileinfo)) + { + $url = Vfs\Sqlfs\StreamWrapper::id2path($fileinfo); + if (!($fileinfo = Vfs::url_stat($url,STREAM_URL_STAT_QUIET))) + { + return false; + } + } + + $up = explode('/',$url[0] == '/' ? $url : parse_url($url,PHP_URL_PATH)); // /apps/$app/$id + $app = null; + + foreach( self::$app_register as $tapp => $reg ) { + if( isset($reg['file_dir']) ) { + $lup = $up; + + unset($lup[0]); + unset($lup[1]); + reset($lup); + + $fdp = explode('/',$reg['file_dir'][0] == '/' ? + $reg['file_dir'] : parse_url($reg['file_dir'],PHP_URL_PATH)); + + $found = true; + + foreach( $fdp as $part ) { + if( current($lup) == $part ) { + if( next($lup) === false ) { + $found = false; + break; + } + } + else { + $found = false; + break; + } + } + + if( $found ) { + $id = current($lup); + $app = $tapp; + break; + } + } + } + + if( $app === null ) { + list(,,$app,$id) = $up; + } + + return array( + 'app' => self::VFS_APPNAME, + 'id' => $fileinfo['name'], + 'app2' => $app, + 'id2' => $id, + 'remark' => '', // only list_attached currently sets the remark + 'owner' => $fileinfo['uid'], + 'link_id' => -$fileinfo['ino'], + 'lastmod' => $fileinfo['mtime'], + 'size' => $fileinfo['size'], + 'type' => $fileinfo['mime'], + ); + } + + /** + * lists all attachments to $app/$id + * + * @param string $app appname + * @param string $id id in app + * @return array with link_id => 'kind' of link-array pairs + */ + static function list_attached($app,$id) + { + $path = self::vfs_path($app,$id); + //error_log(__METHOD__."($app,$id) url=$url"); + + if (!($extra = self::get_registry($app,'find_extra'))) $extra = array(); + + // always use regular links stream wrapper here: extended one is unnecessary (slow) for just listing attachments + if (substr($path,0,13) == 'stylite.links') $path = substr($path,8); + + $attached = array(); + if (($url2stats = Vfs::find($path,array('need_mime'=>true,'type'=>'F','url'=>true)+$extra,true))) + { + $props = Vfs::propfind(array_keys($url2stats)); // get the comments + foreach($url2stats as $url => &$fileinfo) + { + $link = self::fileinfo2link($fileinfo,$url); + if ($props && isset($props[$url])) + { + foreach($props[$url] as $prop) + { + if ($prop['ns'] == Vfs::DEFAULT_PROP_NAMESPACE && $prop['name'] == 'comment') + { + $link['remark'] = $prop['val']; + } + } + } + $attached[$link['link_id']] = $link; + } + } + return $attached; + } + + /** + * reverse static function of htmlspecialchars() + * + * @param string $str string to decode + * @return string decoded string + */ + static private function decode_htmlspecialchars($str) + { + return str_replace(array('&','"','<','>'),array('&','"','<','>'),$str); + } + + /** + * Key for old link title in $data param to Link::notify + */ + const OLD_LINK_TITLE = 'old_link_title'; + + /** + * notify other apps about changed content in $app,$id + * + * To give other apps the possebility to update a title, you can also specify + * a changed old link-title in $data[Link::OLD_LINK_TITLE]. + * + * @param string $app name of app in which the updated happend + * @param string $id id in $app of the updated entry + * @param array $data =null updated data of changed entry, as the read-method of the BO-layer would supply it + */ + static function notify_update($app,$id,$data=null) + { + self::delete_cache($app,$id); + //error_log(__METHOD__."('$app', $id, $data)"); + foreach(self::get_links($app,$id,'!'.self::VFS_APPNAME) as $link_id => $link) + { + self::notify('update',$link['app'],$link['id'],$app,$id,$link_id,$data); + } + if($data[Link::OLD_LINK_TITLE] && egw_json_response::isJSONResponse()) + { + // Update client side with new title + egw_json_response::get()->apply('egw.link_title_callback',array(array($app => array($id => self::title($app, $id))))); + } + } + + /** + * Stores notifications to run after regular processing is done + * + * @var array + */ + private static $notifies = array(); + + /** + * notify an application about a new or deleted links to own entries or updates in the content of the linked entry + * + * Please note: not all apps supply update notifications + * + * @internal + * @param string $type 'link' for new links, 'unlink' for unlinked entries, 'update' of content in linked entries + * @param string $notify_app app to notify + * @param string $notify_id id in $notify_app + * @param string $target_app name of app whos entry changed, linked or deleted + * @param string $target_id id in $target_app + * @param array $data =null data of entry in app2 (optional) + */ + static private function notify($type,$notify_app,$notify_id,$target_app,$target_id,$link_id,$data=null) + { + //error_log(__METHOD__."('$type', '$notify_app', $notify_id, '$target_app', $target_id, $link_id, $data)"); + if ($link_id && isset(self::$app_register[$notify_app]) && isset(self::$app_register[$notify_app]['notify'])) + { + if (!self::$notifies) + { + egw::on_shutdown(array(__CLASS__, 'run_notifies')); + } + self::$notifies[] = array( + 'method' => self::$app_register[$notify_app]['notify'], + 'type' => $type, + 'id' => $notify_id, + 'target_app' => $target_app, + 'target_id' => $target_id, + 'link_id' => $link_id, + 'data' => $data, + ); + } + } + + /** + * Run notifications called by egw::on_shutdown(), after regular processing is finished + */ + static public function run_notifies() + { + //error_log(__METHOD__."() count(self::\$notifies)=".count(self::$notifies)); + while(self::$notifies) + { + $args = array_shift(self::$notifies); + $method = $args['method']; + unset($args['method']); + //error_log(__METHOD__."() calling $method(".array2string($args).')'); + ExecMethod($method, $args); + } + } + + /** + * notifies about unlinked links + * + * @internal + * @param array &$links unlinked links from the database + */ + static private function notify_unlink(&$links) + { + foreach($links as $link) + { + // we notify both sides of the link, as the unlink command NOT clearly knows which side initiated the unlink + self::notify('unlink',$link['link_app1'],$link['link_id1'],$link['link_app2'],$link['link_id2'],$link['link_id']); + self::notify('unlink',$link['link_app2'],$link['link_id2'],$link['link_app1'],$link['link_id1'],$link['link_id']); + } + } + + /** + * Get a reference to the cached value for $app/$id for $type + * + * @param string $app + * @param string|int $id + * @param string $type ='title' 'title' or 'file_access' + * @return int|string can be null, if cache not yet set + */ + private static function &get_cache($app,$id,$type = 'title') + { + switch($type) + { + case 'title': + return self::$title_cache[$app.':'.$id]; + case 'file_access': + return self::$file_access_cache[$app.':'.$id]; + default: + throw new Exception\WrongParameter("Unknown type '$type'!"); + } + } + + /** + * Set title and optional file_access cache for $app,$id + * + * Allows applications to set values for title and file access, eg. in their search method, + * to not be called again. This offloads the need to cache from the app to the link class. + * If there's no caching, items get read multiple times from the database! + * + * @param string $app + * @param int|string $id + * @param string $title title string or null + * @param int $file_access =null EGW_ACL_READ, EGW_ACL_EDIT or both or'ed together + */ + public static function set_cache($app,$id,$title,$file_access=null) + { + //error_log(__METHOD__."($app,$id,$title,$file_access)"); + if (!is_null($title)) + { + self::$title_cache[$app.':'.$id] = $title; + } + if (!is_null($file_access)) + { + self::$file_access_cache[$app.':'.$id] = $file_access; + } + } + + /** + * Delete the diverse caches for $app/$id + * + * @param string $app app-name or null to delete the whole cache + * @param int|string $id id or null to delete only file_access cache of given app (keeps title cache, if app implements file_access!) + */ + private static function delete_cache($app,$id) + { + unset(self::$title_cache[$app.':'.$id]); + unset(self::$file_access_cache[$app.':'.$id]); + } + + /** + * Store function call and parameters in session and return id to retrieve it result + * + * @param string $mime_type + * @param string $method + * @param array $params + * @param boolean $ignore_mime =false true: return id, even if nothing registered for given mime-type + * @return string|null md5 hash of stored data of server-side supported mime-type or null otherwise + */ + public static function set_data($mime_type, $method, array $params, $ignore_mime=false) + { + if (!$ignore_mime && (!($info = self::get_mime_info($mime_type)) || empty($info['mime_data']))) + { + return null; + } + array_unshift($params, $method); + $id = md5(serialize($params)); + //error_log(__METHOD__."('$mime_type', '$method', ...) params=".array2string($params)." --> json=".array2string(serialize($params)).' --> id='.array2string($id)); + Cache::setSession(__CLASS__, $id, $params); + return $id; + } + + /** + * Call stored function with parameters and return result + * + * @param string $id + * @param boolean $return_resource =false false: return string, true: return resource + * @return mixed null if id is not found or invalid + * @throws Exception\WrongParameter + */ + public static function get_data($id, $return_resource=false) + { + $data = Cache::getSession(__CLASS__, $id); + + if (!isset($data) || empty($data[0])) + { + throw new Exception\WrongParameter(__METHOD__."('$id')"); + } + $ret = call_user_func_array('ExecMethod2', $data); + + if (is_resource($ret)) fseek($ret, 0); + + if ($return_resource != is_resource($ret)) + { + if ($return_resource && ($fp = fopen('php://temp', 'w'))) + { + fwrite($fp, $ret); + fseek($fp, 0); + $ret = $fp; + } + if (!$return_resource) + { + $fp = $ret; + $ret = ''; + while(!feof($fp)) + { + $ret .= fread($fp, 8192); + } + fclose($fp); + } + } + //error_log(__METHOD__."('$id') returning ".gettype($ret).'='.array2string($ret)); + return $ret; + } + + /** + * Check the file access perms for $app/id and given user $user + * + * If $user given and != current user AND app does not set file_access_user=true, + * allways return false, as there's no way to check access for an other user! + * + * @ToDo $rel_path is not yet implemented, as no app use it currently + * @param string $app + * @param string|int $id id of entry + * @param int $required =EGW_ACL_READ EGW_ACL_{READ|EDIT} + * @param string $rel_path =null + * @param int $user =null default null = current user + * @return boolean true if access granted, false otherwise + */ + static function file_access($app,$id,$required=EGW_ACL_READ,$rel_path=null,$user=null) + { + // are we called for an other user + if ($user && $user != $GLOBALS['egw_info']['user']['account_id']) + { + // check if app supports file_access WITH 4th $user parameter --> return false if not + if (!self::get_registry($app,'file_access_user') || !($method = self::get_registry($app,'file_access'))) + { + $ret = false; + $err = "(no file_access_user)"; + } + else + { + $ret = ExecMethod2($method,$id,$required,$rel_path,$user); + $err = "(from $method)"; + } + //error_log(__METHOD__."('$app',$id,$required,'$rel_path',$user) returning $err ".array2string($ret)); + return $ret; + } + + $cache =& self::get_cache($app,$id,'file_access'); + + if (!isset($cache) || $required == EGW_ACL_EDIT && !($cache & $required)) + { + if(($method = self::get_registry($app,'file_access'))) + { + $cache |= ExecMethod2($method,$id,$required,$rel_path) ? $required|EGW_ACL_READ : 0; + } + else + { + $cache |= self::title($app,$id) ? EGW_ACL_READ|EGW_ACL_EDIT : 0; + } + //error_log(__METHOD__."($app,$id,$required,$rel_path) got $cache --> ".($cache & $required ? 'true' : 'false')); + } + //else error_log(__METHOD__."($app,$id,$required,$rel_path) using cached value $cache --> ".($cache & $required ? 'true' : 'false')); + return !!($cache & $required); + } +} +Link::init_static(); diff --git a/api/src/Link/Storage.php b/api/src/Link/Storage.php new file mode 100644 index 0000000000..c3b8175b42 --- /dev/null +++ b/api/src/Link/Storage.php @@ -0,0 +1,483 @@ + + * @copyright 2001-2016 by RalfBecker@outdoor-training.de + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package api + * @subpackage link + * @version $Id$ + */ + +namespace EGroupware\Api\Link; + +use EGroupware\Api; + +/** + * generalized linking between entries of eGroupware apps - SO layer + * + * All vars passed to this class get correct escaped to prevent query insertion. + * + * All methods are now static! + */ +class Storage +{ + /** + * Name of the links table + */ + const TABLE = 'egw_links'; + /** + * Turns on debug-messages + */ + const DEBUG = false; + /** + * Reference to the global db-class + * + * @var egw_db + */ + private static $db; + /** + * True if call to get_links or get_3links exceeded limit (contains not all rows) + */ + public static $limit_exceeded = false; + + /** + * creats a link between $app1,$id1 and $app2,$id2 + * + * @param string $app1 appname of 1. endpoint of the link + * @param string $id1 id in $app1 + * @param string $app2 appname of 2. endpoint of the link + * @param string $id2 id in $app2 + * @param string $remark ='' Remark to be saved with the link (defaults to '') + * @param int $owner =0 Owner of the link (defaults to user) + * @param int $lastmod =0 timestamp of last modification (defaults to now=time()) + * @return int/boolean False (for db or param-error) or on success link_id (Please not the return-value of $id1) + */ + static function link( $app1,&$id1,$app2,$id2='',$remark='',$owner=0,$lastmod=0 ) + { + if (self::DEBUG) + { + echo "

solink.link('$app1',$id1,'$app2',$id2,'$remark',$owner)

\n"; + } + if ($app1 == $app2 && $id1 == $id2 || + $id1 == '' || $id2 == '' || $app1 == '' || $app2 == '') + { + return False; // dont link to self or other nosense + } + if (($link = self::get_link($app1,$id1,$app2,$id2))) + { + if ($link['link_remark'] != $remark) + { + self::update_remark($link['link_id'],$remark); + } + return $link['link_id']; // link alread exist + } + if (!$owner) + { + $owner = $GLOBALS['egw_info']['user']['account_id']; + } + return self::$db->insert(self::TABLE,array( + 'link_app1' => $app1, + 'link_id1' => $id1, + 'link_app2' => $app2, + 'link_id2' => $id2, + 'link_remark' => $remark, + 'link_lastmod' => $lastmod ? $lastmod : time(), + 'link_owner' => $owner, + ),False,__LINE__,__FILE__) ? self::$db->get_last_insert_id(self::TABLE,'link_id') : false; + } + + /** + * update the remark of a link + * + * @param int $link_id link to update + * @param string $remark new text for the remark + * @return boolean true on success, else false + */ + static function update_remark($link_id,$remark) + { + return self::$db->update(self::TABLE,array( + 'link_remark' => $remark, + 'link_lastmod' => time(), + ),array( + 'link_id' => $link_id, + ),__LINE__,__FILE__); + } + + /** + * returns array of links to $app,$id + * + * @param string $app appname + * @param string|array $id id(s) in $app + * @param string $only_app ='' if set return only links from $only_app (eg. only addressbook-entries) or NOT from if $only_app[0]=='!' + * @param string $order ='link_lastmod DESC' defaults to newest links first + * @param boolean $deleted =false Include links that have been flagged as deleted, waiting for purge of linked record. + * @param int|array $limit =null number of entries to return, default null = all or array(offset, num_rows) to return num_rows starting from offset + * @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 + */ + static function get_links($app, $id, $only_app='', $order='link_lastmod DESC', $deleted=false, $limit=null) + { + if (self::DEBUG) + { + echo "

solink.get_links($app,".print_r($id,true).",$only_app,$order,$deleted)

\n"; + } + if (($not_only = $only_app[0] == '!')) + { + $only_app = substr($only_app,1); + } + + $offset = false; + if (is_array($limit)) + { + list($offset, $limit) = $limit; + } + elseif($limit) + { + $offset = 0; + } + + $links = array(); + try { + foreach(self::$db->select(self::TABLE, '*', self::$db->expression(self::TABLE, '((', array( + 'link_app1' => $app, + 'link_id1' => $id, + ),') OR (',array( + 'link_app2' => $app, + 'link_id2' => $id, + ),'))', + $deleted ? '' : ' AND deleted IS NULL' + ), __LINE__, __FILE__, $offset, $order ? " ORDER BY $order" : '', 'phpgwapi', $limit) as $row) + { + // check if left side (1) is one of our targets --> add it + if ($row['link_app1'] == $app && in_array($row['link_id1'],(array)$id)) + { + self::_add2links($row,true,$only_app,$not_only,$links); + } + // check if right side (2) is one of our targets --> add it (both can be true for multiple targets!) + if ($row['link_app2'] == $app && in_array($row['link_id2'],(array)$id)) + { + self::_add2links($row,false,$only_app,$not_only,$links); + } + } + // if query returns exactly limit rows, we assume there are more and therefore set self::$limit_exceeded + self::$limit_exceeded = $offset !== false && count(is_array($id) ? $links : $links[$id]) == $limit; + } + // catch Illegal mix of collations (ascii_general_ci,IMPLICIT) and (utf8_general_ci,COERCIBLE) for operation '=' (1267) + // caused by non-ascii chars compared with ascii field uid + catch(Api\Db\Exception $e) { + _egw_log_exception($e); + } + return is_array($id) ? $links : ($links[$id] ? $links[$id] : array()); + } + + private static function _add2links($row,$left,$only_app,$not_only,array &$links) + { + $linked_app = $left ? $row['link_app2'] : $row['link_app1']; + $linked_id = $left ? $row['link_id2'] : $row['link_id1']; + $app_id = $left ? $row['link_id1'] : $row['link_id2']; + list($app) = explode('-',$linked_app); + if ($only_app && $not_only == ($linked_app == $only_app) || !$GLOBALS['egw_info']['user']['apps'][$app]) + { + #echo "$linked_app == $only_app, ";var_dump($linked_app == $only_app);echo " ->dont return a link
"; + return; + } + #echo "returning ".(($only_app && !$not_only) ? " linkid:".$linked_id : " full array with linkid $linked_id")."
"; + $links[$app_id][$row['link_id']] = ($only_app && !$not_only) ? $linked_id : array( + 'app' => $linked_app, + 'id' => $linked_id, + 'remark' => $row['link_remark'], + 'owner' => $row['link_owner'], + 'lastmod' => $row['link_lastmod'], + 'link_id' => $row['link_id'], + 'deleted' => $row['deleted'], + ); + } + + /** + * returns data of a link + * + * @param ing/string $app_link_id > 0 link_id of link or app-name of link + * @param string $id ='' id in $app, if no integer link_id given in $app_link_id + * @param string $app2 ='' appname of 2. endpoint of the link, if no integer link_id given in $app_link_id + * @param string $id2 ='' id in $app2, if no integer link_id given in $app_link_id + * @return array with link-data or False + */ + static function get_link($app_link_id,$id='',$app2='',$id2='') + { + if (self::DEBUG) + { + echo "

solink.get_link('$app_link_id',$id,'$app2','$id2')

\n"; + } + if ((int) $app_link_id > 0) + { + $where = array('link_id' => $app_link_id); + } + else + { + if ($app_link_id == '' || $id == '' || $app2 == '' || $id2 == '') + { + return False; + } + $where = self::$db->expression(self::TABLE,'(',array( + 'link_app1' => $app_link_id, + 'link_id1' => $id, + 'link_app2' => $app2, + 'link_id2' => $id2, + ),') OR (',array( + 'link_app2' => $app_link_id, + 'link_id2' => $id, + 'link_app1' => $app2, + 'link_id1' => $id2, + ),')'); + } + try { + return self::$db->select(self::TABLE,'*',$where,__LINE__,__FILE__)->fetch(ADODB_FETCH_ASSOC); + } + // catch Illegal mix of collations (ascii_general_ci,IMPLICIT) and (utf8_general_ci,COERCIBLE) for operation '=' (1267) + // caused by non-ascii chars compared with ascii field uid + catch(Api\Db\Exception $e) { + _egw_log_exception($e); + } + return false; + } + + /** + * Remove link with $link_id or all links matching given params + * + * @param $link_id link-id to remove if > 0 + * @param string $app ='' app-name of links to remove + * @param string $id ='' id in $app or '' remove all links from $app + * @param int $owner =0 account_id to delete all links of a given owner, or 0 + * @param string $app2 ='' appname of 2. endpoint of the link + * @param string $id2 ='' id in $app2 + * @param boolean $hold_for_purge Don't really delete the link, just mark it as deleted and wait for final delete of linked entry + * @return array with deleted links + */ + static function unlink($link_id,$app='',$id='',$owner=0,$app2='',$id2='',$hold_for_purge=false) + { + if (self::DEBUG) + { + echo "

solink.unlink($link_id,$app,$id,$owner,$app2,$id2)

\n"; + } + if ((int)$link_id > 0) + { + $where = array('link_id' => $link_id); + } + elseif ($app == '' AND $owner == '') + { + return 0; + } + else + { + if ($app != '' && $app2 == '') + { + $check1 = array('link_app1' => $app); + $check2 = array('link_app2' => $app); + if ($id != '') + { + $check1['link_id1'] = $id; + $check2['link_id2'] = $id; + } + $where = self::$db->expression(self::TABLE,'((',$check1,') OR (',$check2,'))'); + } + elseif ($app != '' && $app2 != '') + { + $where = self::$db->expression(self::TABLE,'(',array( + 'link_app1' => $app, + 'link_id1' => $id, + 'link_app2' => $app2, + 'link_id2' => $id2, + ),') OR (',array( + 'link_app1' => $app2, + 'link_id1' => $id2, + 'link_app2' => $app, + 'link_id2' => $id, + ),')'); + } + if ($owner) + { + if ($app) $where = array($where); + $where['link_owner'] = $owner; + } + } + $deleted = array(); + try { + foreach(self::$db->select(self::TABLE,'*',$where,__LINE__,__FILE__) as $row) + { + $deleted[] = $row; + } + if($hold_for_purge) + { + self::$db->update(self::TABLE,array( + 'deleted' => time(), + 'link_lastmod' => time(), + ), $where, __LINE__,__FILE__); + } + else + { + self::$db->delete(self::TABLE,$where,__LINE__,__FILE__); + } + } + // catch Illegal mix of collations (ascii_general_ci,IMPLICIT) and (utf8_general_ci,COERCIBLE) for operation '=' (1267) + // caused by non-ascii chars compared with ascii field uid + catch(Api\Db\Exception $e) { + _egw_log_exception($e); + } + + return $deleted; + } + + /** + * Restore links being held as deleted waiting for purge of linked record (un-delete) + * + * @param string $app ='' app-name of links to remove + * @param string $id ='' id in $app or '' remove all links from $app + */ + static function restore($app, $id) + { + if (self::DEBUG) + { + echo "

solink.restore($app,$id)

\n"; + } + if ($app == '') + { + return 0; + } + + $check1 = array('link_app1' => $app); + $check2 = array('link_app2' => $app); + if ($id != '') + { + $check1['link_id1'] = $id; + $check2['link_id2'] = $id; + } + $where = self::$db->expression(self::TABLE,'((',$check1,') OR (',$check2,'))'); + self::$db->update(self::TABLE,array('deleted'=> null), $where, __LINE__,__FILE__); + } + + /** + * Changes ownership of all links from $owner to $new_owner + * + * This is needed when a user/account gets deleted + * Does NOT change the modification-time + * + * @param int $owner acount_id of owner to change + * @param int $new_owner account_id of new owner + * @return int number of links changed + */ + static function chown($owner,$new_owner) + { + if ((int)$owner <= 0 || (int) $new_owner <= 0) + { + return 0; + } + self::$db->update(self::TABLE,array('owner'=>$new_owner),array('owner'=>$owner),__LINE__,__FILE__); + + return self::$db->affected_rows(); + } + + /** + * Get all links from a given app's entries to an other app's entries, which both link to the same 3. app and id + * + * Example: + * I search all timesheet's linked to a given project and id(s), who are also linked to other entries, + * which link to the same project: + * + * ($app='timesheet'/some id) <--a--> (other app/other id) <--b--> ($t_app='projectmanager'/$t_id=$pm_id) + * ^ ^ + * +---------------------------c-----------------------------------------+ + * + * Api\Link::get_3links('timesheet','projectmanager',$pm_id) returns the links (c) between the timesheet and the project, + * plus the other app/id in the keys 'app3' and 'id3' + * + * @param string $app app the returned links are linked on one side (atm. this must be link_app1!) + * @param string $target_app app the returned links other side link also to + * @param string|array $target_id =null id(s) the returned links other side link also to + * @param boolean $just_app_ids =false return array with link_id => app_id pairs, not the full link record + * @param string $order ='link_lastmod DESC' defaults to newest links first + * @param int|array $limit =null number of entries to return, default null = all or array(offset, num_rows) to return num_rows starting from offset + * @return array with links from entries from $app to $target_app/$target_id plus the other (b) link_id/app/id in the keys 'link3'/'app3'/'id3' + */ + static function get_3links($app, $target_app, $target_id=null, $just_app_ids=false, $order='link_lastmod DESC', $limit=null) + { + $table = self::TABLE; + $arrayofselects=array( + // retrieve the type of links, where the relation is realized as timesheet->infolog/tracker via infolog->projectmanager to timesheet->projectmanager + array('table'=>self::TABLE, + 'cols'=>'c.*,b.link_app1 AS app3,b.link_id1 AS id3,b.link_id AS link3', + 'where'=>'a.link_app1='.self::$db->quote($app).' AND c.link_app2='.self::$db->quote($target_app). + (!$target_id ? '' : self::$db->expression(self::TABLE,' AND c.',array('link_id2' => $target_id))), + 'join'=>" a + JOIN $table b ON a.link_id2=b.link_id1 AND a.link_app2=b.link_app1 + JOIN $table c ON a.link_id1=c.link_id1 AND a.link_app1=c.link_app1 AND a.link_id!=c.link_id AND c.link_app2=b.link_app2 AND c.link_id2=b.link_id2", + ), + // retrieve the type of links, where the relation is realized as timesheet->infolog/tracker and projectmanager->timesheet + array('table'=>self::TABLE, + 'cols'=>'b.link_id, b.link_app2 as app1, b.link_id2 as id1, b.link_app1 as app2, b.link_id1 as id2, b.link_remark,b.link_lastmod,b.link_owner,b.deleted,c.link_app1 AS app3,c.link_id1 AS id3,c.link_id AS link3', + 'where'=>'a.link_app1='.self::$db->quote($app).' AND b.link_app1='.self::$db->quote($target_app). + (!$target_id ? '' : self::$db->expression(self::TABLE,' AND b.',array('link_id1' => $target_id))), + 'join'=>" a + JOIN $table b ON a.link_id1=b.link_id2 AND a.link_app1=b.link_app2 + JOIN $table c ON a.link_id2=c.link_id1 AND a.link_app2=c.link_app1 AND a.link_id!=c.link_id AND c.link_app2=b.link_app1 AND c.link_id2=b.link_id1", + ), + // retrieve the type of links, where the relation is realized as timesheet->projectmanager and infolog->timesheet + array('table'=>self::TABLE, + 'cols'=>'a.*,c.link_app1 AS app3,c.link_id1 AS id3,c.link_id AS link3', + 'where'=>'a.link_app1='.self::$db->quote($app).' AND a.link_app2='.self::$db->quote($target_app). + (!$target_id ? '' : self::$db->expression(self::TABLE,' AND a.',array('link_id2' => $target_id))), + 'join'=>" a + JOIN $table b ON a.link_id1=b.link_id2 AND a.link_app1=b.link_app2 + JOIN $table c ON a.link_id2=c.link_id2 AND a.link_app2=c.link_app2 AND a.link_id!=c.link_id AND c.link_app1=b.link_app1 AND c.link_id1=b.link_id1", + ), + ); + + $offset = false; + if (is_array($limit)) + { + list($offset, $limit) = $limit; + } + elseif($limit) + { + $offset = 0; + } + + $links = array(); + foreach(self::$db->union($arrayofselects, __LINE__, __FILE__, $order, $offset, $limit) as $row) + { + if ($just_app_ids) + { + if ($row['link_app1'] == $target_app && (is_null($target_id) || in_array($row['link_id1'],(array)$target_id))) + { + $links[$row['link_id']] = $row['link_id2']; + } + else + { + $links[$row['link_id']] = $row['link_id1']; + } + } + else + { + $links[] = Api\Db::strip_array_keys($row,'link_'); + } + } + // if query returns exactly limit rows, we assume there are more and therefore set self::$limit_exceeded + self::$limit_exceeded = $offset !== false && count($links) == $limit; + + return $links; + } + + /** + * Initialise our static vars + */ + static function init_static( ) + { + self::$db = $GLOBALS['egw']->db; + } +} +Storage::init_static(); diff --git a/api/src/Storage/Customfields.php b/api/src/Storage/Customfields.php index f31579971d..19689ab4eb 100755 --- a/api/src/Storage/Customfields.php +++ b/api/src/Storage/Customfields.php @@ -14,9 +14,6 @@ namespace EGroupware\Api\Storage; use EGroupware\Api; -// explicitly reference classes still in phpgwapi -use egw_link; - /** * Managing custom-field definitions */ @@ -248,7 +245,7 @@ class Customfields implements \IteratorAggregate { $app = $field['type']; } - if ($value) $value = egw_link::title($app, $value); + if ($value) $value = Api\Link::title($app, $value); } break; } @@ -301,7 +298,7 @@ class Customfields implements \IteratorAggregate if (is_null($link_types)) { - $link_types = array_keys(array_intersect(egw_link::app_list('query'),egw_link::app_list('title'))); + $link_types = array_keys(array_intersect(Api\Link::app_list('query'),Api\Link::app_list('title'))); $link_types[] = 'link-entry'; } return $link_types; @@ -338,7 +335,7 @@ class Customfields implements \IteratorAggregate $app = $data['type']; $id = $old['#'.$name]; } - egw_link::unlink(false,$own_app,$values[$id_name],'',$app,$id); + Api\Link::unlink(false,$own_app,$values[$id_name],'',$app,$id); } if ($data['type'] == 'link-entry') { @@ -351,7 +348,7 @@ class Customfields implements \IteratorAggregate } if ($id) // create new link, does nothing for already existing links { - egw_link::link($own_app,$values[$id_name],$app,$id); + Api\Link::link($own_app,$values[$id_name],$app,$id); } } } diff --git a/api/src/Storage/Tracking.php b/api/src/Storage/Tracking.php index b75d85122a..271d590a6d 100644 --- a/api/src/Storage/Tracking.php +++ b/api/src/Storage/Tracking.php @@ -16,7 +16,6 @@ namespace EGroupware\Api\Storage; use EGroupware\Api; // explicitly reference classes still in phpgwapi -use egw_link; use html; /** @@ -195,7 +194,7 @@ abstract class Tracking { if ($cf_app) { - $linkable_cf_types = array('link-entry')+array_keys(egw_link::app_list()); + $linkable_cf_types = array('link-entry')+array_keys(Api\Link::app_list()); foreach(Customfields::get($cf_app, true) as $cf_name => $cf_data) { $this->field2history['#'.$cf_name] = '#'.$cf_name; @@ -357,9 +356,9 @@ abstract class Tracking } $source_id = $data[$this->id_field]; //error_log(__METHOD__.__LINE__.array2string($source_id)); - if ($source_id) egw_link::link($this->app,$source_id,$app,$id); - //error_log(__METHOD__.__LINE__."egw_link::link('$this->app',".array2string($source_id).",'$app',$id);"); - //echo "

egw_link::link('$this->app',{$data[$this->id_field]},'$app',$id);

\n"; + if ($source_id) Api\Link::link($this->app,$source_id,$app,$id); + //error_log(__METHOD__.__LINE__."Api\Link::link('$this->app',".array2string($source_id).",'$app',$id);"); + //echo "

Api\Link::link('$this->app',{$data[$this->id_field]},'$app',$id);

\n"; } // unlink removed application entries @@ -372,8 +371,8 @@ abstract class Tracking if (!$id) continue; } $source_id = $data[$this->id_field]; - if ($source_id) egw_link::unlink(null,$this->app,$source_id,0,$app,$id); - //echo "

egw_link::unlink(NULL,'$this->app',{$data[$this->id_field]},0,'$app',$id);

\n"; + if ($source_id) Api\Link::unlink(null,$this->app,$source_id,0,$app,$id); + //echo "

Api\Link::unlink(NULL,'$this->app',{$data[$this->id_field]},0,'$app',$id);

\n"; } } @@ -869,7 +868,7 @@ abstract class Tracking { unset($old); // not used, but required by function signature - return egw_link::title($this->app,$data[$this->id_field]); + return Api\Link::title($this->app,$data[$this->id_field]); } /** @@ -887,7 +886,7 @@ abstract class Tracking { unset($old, $deleted, $receiver); // not used, but required by function signature - return egw_link::title($this->app,$data[$this->id_field]); + return Api\Link::title($this->app,$data[$this->id_field]); } /** @@ -932,10 +931,10 @@ abstract class Tracking } else { - if (($view = egw_link::view($this->app,$data[$this->id_field]))) + if (($view = Api\Link::view($this->app,$data[$this->id_field]))) { $link = $GLOBALS['egw']->link('/index.php',$view); - $popup = egw_link::is_popup($this->app,'view'); + $popup = Api\Link::is_popup($this->app,'view'); } } if ($link[0] == '/') @@ -966,14 +965,14 @@ abstract class Tracking { unset($receiver); // not used, but required by function signature - if (($view = egw_link::view($this->app,$data[$this->id_field]))) + if (($view = Api\Link::view($this->app,$data[$this->id_field]))) { return array( 'text' => $this->get_title($data,$old), 'app' => $this->app, 'id' => $data[$this->id_field], 'view' => $view, - 'popup' => egw_link::is_popup($this->app,'view'), + 'popup' => Api\Link::is_popup($this->app,'view'), ); } return false; diff --git a/api/src/Vfs/Links/StreamWrapper.php b/api/src/Vfs/Links/StreamWrapper.php index bf72f10331..47f8e2b153 100644 --- a/api/src/Vfs/Links/StreamWrapper.php +++ b/api/src/Vfs/Links/StreamWrapper.php @@ -14,9 +14,9 @@ namespace EGroupware\Api\Vfs\Links; use EGroupware\Api\Vfs; +use EGroupware\Api; // explicitly import old phpgwapi classes used: -use egw_link; use addressbook_vcal; /** @@ -111,10 +111,10 @@ class StreamWrapper extends LinksParent // which gives him then read AND write access to the file store of the entry else { - // vfs & stream-wrapper use posix rights, egw_link::file_access uses EGW_ACL_{EDIT|READ}! + // vfs & stream-wrapper use posix rights, Api\Link::file_access uses EGW_ACL_{EDIT|READ}! $required = $check & Vfs::WRITABLE ? EGW_ACL_EDIT : EGW_ACL_READ; - $access = egw_link::file_access($app,$id,$required,$rel_path,Vfs::$user); - $what = "from egw_link::file_access('$app',$id,$required,'$rel_path,".Vfs::$user.")"; + $access = Api\Link::file_access($app,$id,$required,$rel_path,Vfs::$user); + $what = "from Api\Link::file_access('$app',$id,$required,'$rel_path,".Vfs::$user.")"; } if (self::DEBUG) error_log(__METHOD__."($url,$check) user=".Vfs::$user." ($what) ".($access?"access granted ($app:$id:$rel_path)":'no access!!!')); return $access; diff --git a/phpgwapi/inc/class.egw_link.inc.php b/phpgwapi/inc/class.egw_link.inc.php index 6eebb455b5..8d0efc723d 100644 --- a/phpgwapi/inc/class.egw_link.inc.php +++ b/phpgwapi/inc/class.egw_link.inc.php @@ -1,6 +1,6 @@ - * @copyright 2001-2014 by RalfBecker@outdoor-training.de + * @copyright 2001-2016 by RalfBecker@outdoor-training.de * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License * @package api * @subpackage link * @version $Id$ */ +use EGroupware\Api; + /** * Generalized linking between entries of EGroupware apps - * - * Please note: this class can NOT and does not need to be initialised, all methods are static - * - * To participate in the linking an applications has to implement the following hooks: - * - * /** - * * Hook called by link-class to include app in the appregistry of the linkage - * * - * * @param array|string $location location and other parameters (not used) - * * @return array with method-names - * *% - * function search_link($location) - * { - * return array( - * 'query' => 'app.class.link_query', // method to search app for a pattern: array link_query(string $pattern, array $options) - * 'title' => 'app.class.link_title', // method to return title of an entry of app: string/false/null link_title(int/string $id) - * 'titles' => 'app.class.link_titles', // method to return multiple titles: array link_title(array $ids) - * 'view' => array( // get parameters to view an entry of app - * 'menuaction' => 'app.class.method', - * ), - * 'types' => array( // Optional list of sub-types to filter (eg organisations), app to handle different queries - * 'type_key' => array( - * 'name' => 'Human Reference', - * 'icon' => 'app/icon' // Optional icon to use for that sub-type - * ) - * ), - * 'view_id' => 'app_id', // name of get parameter of the id - * 'view_popup' => '400x300', // size of popup (XxY), if view is in popup - * 'view_list' => 'app.class.method' // deprecated use 'list' instead - * 'list' => array( // Method to be called to display a list of links, method should check $_GET['search'] to filter - * 'menuaction' => 'app.class.method', - * ), - * 'list_popup' => '400x300' - * 'add' => array( // get parameter to add an empty entry to app - * 'menuaction' => 'app.class.method', - * ), - * 'add_app' => 'link_app', // name of get parameter to add links to other app - * 'add_id' => 'link_id', // --------------------- " ------------------- id - * 'add_popup' => '400x300', // size of popup (XxY), if add is in popup - * 'notify' => 'app.class.method', // method to be called if an other applications liks or unlinks with app: notify(array $data) - * 'file_access' => 'app.class.method', // method to be called to check file access rights of a given user, see links_stream_wrapper class - * // boolean file_access(string $id,int $check,string $rel_path=null,int $user=null) - * 'file_access_user' => false, // true if file_access method supports 4th parameter $user, if app is NOT supporting it - * // egw_link::file_access() returns false for $user != current user! - * 'file_dir' => 'app/sub', // sub file dir for uploaded files/links - * 'find_extra' => array('name_preg' => '/^(?!.picture.jpg)$/') // extra options to egw_vfs::find, to eg. remove some files from the list of attachments - * 'edit' => array( - * 'menuaction' => 'app.class.method', - * ), - * 'edit_id' => 'app_id', - * 'edit_popup' => '400x300', - * 'name' => 'Some name', // Name to use instead of app-name - * 'icon' => 'app/icon', // Optional icon to use instead of app-icon - * 'entry' => 'Contact', // Optional name for single entry of app, eg. "contact" used instead of appname - * 'entries' => 'Contacts', // Optional name for multiple entries of app, eg. "contacts" used instead of appname - * 'mime' => array( // Optional register mime-types application can open - * 'text/something' => array( - * 'mime_url' => $attr, // either mime_url or mime_data is required for server-side processing! - * 'mime_data' => $attr, // md5-hash returned from egw_link::set_data() to retrive content (only server-side) - * 'menuaction' => 'app.class.method', // method to call - * 'mime_popup' => '400x300', // optional size of popup - * 'mime_target' => '_self', // optional target, default _blank - * // other get-parameters to set in url - * ), - * // further mime types supported ... - * ), - * 'fetch' => 'app.class.method', // method to return entry data for a given id. the method called should support id, and expected mime-type - * // basically you should return something like array(id, title, mimetype, body, linked-files) - * 'additional' => array( // allow one app to register sub-types, - * 'app-sub' => array( // different from 'types' approach above - * // every value defined above - * ) - * ) - * } - * All entries are optional, thought you only get conected functionality, if you implement them ... - * - * The BO-layer implementes some extra features on top of the so-layer: - * 1) It handles links to not already existing entries. This is used by the eTemplate link-widget, which allows to - * setup links even for new / not already existing entries, before they get saved. - * 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 - * (not the return-value) after saveing your new entry again to the link static function. - * 2) Attaching files: they are saved in the vfs and not the link-table (!). - * Attached files are stored under $vfs_basedir='/infolog' in the vfs! - * 3) It manages the link-registry, in which apps can register themselfs by implementing some hooks - * 4) It notifies apps, who registered for that service, about changes in the links their entries - * - * Modification times in links (and deleted timestamp) are always in server-time! - * (We dont convert them here, as most apps ignore them anyway) + * * + * @deprecated use Api\Link */ -class egw_link extends solink -{ - /** - * appname used for returned attached files (!= 'filemanager'!) - */ - const VFS_APPNAME = 'file'; // pseudo-appname for own file-attachments in vfs, this is NOT the vfs-app - - /** - * Appname used of files stored via egw_link::set_data() - */ - const DATA_APPNAME = 'egw-data'; - - /** - * appname used for linking existing files to VFS - */ - const VFS_LINK = 'link'; - - /** - * Baseurl for the attachments in the vfs - */ - const VFS_BASEURL = 'vfs://default/apps'; - /** - * Turns on debug-messages - */ - const DEBUG = false; - /** - * other apps can participate in the linking by implementing a 'search_link' hook, which - * has to return an array in the format of an app_register entry below - * - * @var array - */ - static $app_register = array( - 'home-accounts' => array( // user need run-rights for home - 'app' => 'home', - 'name' => 'Accounts', - 'icon' => 'addressbook/accounts', - 'query' => 'accounts::link_query', - 'title' => 'common::grab_owner_name', - 'view' => array('menuaction'=>'addressbook.addressbook_ui.view','ajax'=>'true'), - 'view_id' => 'account_id' - ), - 'home' => array( - // handling of text or pdf files by browser in a popup window - 'mime' => array( - 'application/pdf' => array( - 'mime_popup' => '640x480', - 'mime_target' => '_blank', - ), - '/^text\\/(plain|html|diff)/' => array( // text/(mimetypes which can be opened as recognised popups) - 'mime_popup' => '640x480', - 'mime_target' => '_blank', - ), - '/^image\\//' => array( // image - 'mime_popup' => '640x480', - 'mime_target' => '_blank', - ), - ), - ), - ); - /** - * Caches link titles for a better performance - * - * @var array - */ - private static $title_cache = array(); - - /** - * Cache file access permissions - * - * @var array - */ - private static $file_access_cache = array(); - - /** - * Private constructor to forbid instanciated use - * - */ - private function __construct() - { - - } - - /** - * initialize our static vars - */ - static function init_static( ) - { - // FireFox 36 can not display pdf with it's internal viewer in an iframe used by mobile theme/template for popups - // same is true for all mobile devices - if (html::$user_agent == 'firefox' && $GLOBALS['egw_info']['user']['preferences']['common']['theme'] == 'mobile' || html::$ua_mobile) - { - unset(self::$app_register['home']['mime']['application/pdf']); - } - // other apps can participate in the linking by implementing a search_link hook, which - // has to return an array in the format of an app_register entry - // for performance reasons, we do it only once / cache it in the session - if (!($search_link_hooks = $GLOBALS['egw']->session->appsession('search_link_hooks','phpgwapi'))) - { - $search_link_hooks = $GLOBALS['egw']->hooks->process('search_link',array(), (bool)$GLOBALS['egw_info']['flags']['async-service']); - $GLOBALS['egw']->session->appsession('search_link_hooks','phpgwapi',$search_link_hooks); - } - if (is_array($search_link_hooks)) - { - foreach($search_link_hooks as $app => $data) - { - // allow apps to register additional types - if (isset($data['additional'])) - { - foreach($data['additional'] as $name => $values) - { - $values['app'] = $app; // store name of registring app, to be able to check access - self::$app_register[$name] = $values; - } - unset($data['additional']); - } - // support deprecated view_list attribute instead of new index attribute - if (isset($data['view_list']) && !isset($data['list'])) - { - $data['list'] = array('menuaction' => $data['view_list']); - } - elseif(isset($data['list']) && !isset($data['view_list'])) - { - $data['view_list'] = $data['list']['menuaction']; - } - if (is_array($data)) - { - self::$app_register[$app] = $data; - } - } - } - // disable ability to link to accounts for non-admins, if account-selection is disabled - if ($GLOBALS['egw_info']['user']['preferences']['common']['account_selection'] == 'none' && - !isset($GLOBALS['egw_info']['user']['apps']['admin'])) - { - unset(self::$app_register['home-accounts']); - } - if (!(self::$title_cache = $GLOBALS['egw']->session->appsession('link_title_cache','phpgwapi'))) - { - self::$title_cache = array(); - } - if (!(self::$file_access_cache = $GLOBALS['egw']->session->appsession('link_file_access_cache','phpgwapi'))) - { - self::$file_access_cache = array(); - } - - // register self::save_session_cache to run on shutdown - egw::on_shutdown(array(__CLASS__, 'save_session_cache')); - - //error_log(__METHOD__.'() items in title-cache: '.count(self::$title_cache).' file-access-cache: '.count(self::$file_access_cache)); - } - - /** - * Get clientside relevant attributes from app registry in json format - * - * Only transfering relevant information cuts approx. half of the size. - * Also only transfering information relevant to apps user has access too. - * Important eg. for mime-registry, to not use calendar for opening iCal files, if user has no calendar! - * As app can store additonal types, we have to check the registring app $data['app'] too! - * - * @return string json encoded object with app: object pairs with attributes "(view|add|edit)(|_id|_popup)" - */ - public static function json_registry() - { - $to_json = array(); - foreach(self::$app_register as $app => $data) - { - if (isset($GLOBALS['egw_info']['user']['apps'][$app]) || - isset($data['app']) && isset($GLOBALS['egw_info']['user']['apps'][$data['app']])) - { - $to_json[$app] = array_intersect_key($data, array_flip(array( - 'view','view_id','view_popup', - 'add','add_app','add_id','add_popup', - 'edit','edit_id','edit_popup', - 'list','list_popup', - 'name','icon','query', - 'mime','entry','entries', - ))); - } - } - return json_encode($to_json); - } - - /** - * Called by egw::shutdown to store the title-cache in session and run notifications - * - * Would probably better called shutdown as well. - */ - static function save_session_cache() - { - if (isset($GLOBALS['egw']->session)) // eg. cron-jobs use it too, without any session - { - //error_log(__METHOD__.'() items in title-cache: '.count(self::$title_cache).' file-access-cache: '.count(self::$file_access_cache)); - $GLOBALS['egw']->session->appsession('link_title_cache','phpgwapi',self::$title_cache); - $GLOBALS['egw']->session->appsession('link_file_access_cache','phpgwapi',self::$file_access_cache); - } - } - - /** - * creats a link between $app1,$id1 and $app2,$id2 - $id1 does NOT need to exist yet - * - * Does NOT check if link already exists. - * File-attachments return a negative link-id !!! - * - * @param string $app1 app of $id1 - * @param string|array &$id1 id of item to linkto or 0 if item not yet created or array with links - * of not created item or $file-array if $app1 == self::VFS_APPNAME (see below). - * If $id==0 it will be set on return to an array with the links for the new item. - * @param string|array $app2 app of 2.linkend or array with links ($id2 not used) - * @param string $id2 ='' id of 2. item of $file-array if $app2 == self::VFS_APPNAME or self::DATA_APPNAME - * $file array with informations about the file in format of the etemplate file-type - * $file['name'] name of the file (no directory) - * $file['type'] mime-type of the file - * $file['tmp_name'] name of the uploaded file (incl. directory) for self::VFS_APPNAME or - * $file['egw_data'] id of egw_link::set_data() call for self::DATA_APPNAME - * @param string $remark ='' Remark to be saved with the link (defaults to '') - * @param int $owner =0 Owner of the link (defaults to user) - * @param int $lastmod =0 timestamp of last modification (defaults to now=time()) - * @param int $no_notify =0 &1 dont notify $app1, &2 dont notify $app2 - * @return int/boolean False (for db or param-error) or on success link_id (Please not the return-value of $id1) - */ - static function link( $app1,&$id1,$app2,$id2='',$remark='',$owner=0,$lastmod=0,$no_notify=0 ) - { - if (self::DEBUG) - { - echo "

egw_link::link('$app1',$id1,'".print_r($app2,true)."',".print_r($id2,true).",'$remark',$owner,$lastmod)

\n"; - } - if (!$app1 || !$app2 || $app1 == $app2 && $id1 == $id2) - { - return False; - } - if (is_array($app2) && !$id2) - { - reset($app2); - $link_id = True; - while ($link_id && list(,$link) = each($app2)) - { - if (!is_array($link)) // check for unlink-marker - { - //echo "link='$link' is no array
\n"; - continue; - } - if (is_array($id1) || !$id1) // create link only in $id1 array - { - self::link($app1, $id1, $link['app'], $link['id'], $link['remark'],$link['owner'],$link['lastmod']); - continue; - } - switch ($link['app']) - { - case self::DATA_APPNAME: - if (!($link['id']['tmp_name'] = self::get_data($link['id']['egw_data'], true))) - { - $link_id = false; - break; - } - // fall through - case self::VFS_APPNAME: - $link_id = self::attach_file($app1,$id1,$link['id'],$link['remark']); - break; - - case self::VFS_LINK: - $link_id = self::link_file($app1,$id1, $link['id'],$link['remark']); - break; - - default: - $link_id = solink::link($app1,$id1,$link['app'],$link['id'], - $link['remark'],$link['owner'],$link['lastmod']); - // notify both sides - if (!($no_notify&2)) self::notify('link',$link['app'],$link['id'],$app1,$id1,$link_id); - if (!($no_notify&1)) self::notify('link',$app1,$id1,$link['app'],$link['id'],$link_id); - break; - } - } - return $link_id; - } - if (is_array($id1) || !$id1) // create link only in $id1 array - { - if (!is_array($id1)) - { - $id1 = array( ); - } - $link_id = self::temp_link_id($app2,$id2); - - $id1[$link_id] = array( - 'app' => $app2, - 'id' => $id2, - 'remark' => $remark, - 'owner' => $owner, - 'link_id' => $link_id, - 'lastmod' => time() - ); - if (self::DEBUG) - { - _debug_array($id1); - } - return $link_id; - } - if ($app1 == self::VFS_LINK) - { - return self::link_file($app2,$id2,$id1,$remark); - } - elseif ($app2 == self::VFS_LINK) - { - return self::link_file($app1,$id1,$id2,$remark); - } - if ($app1 == self::VFS_APPNAME) - { - return self::attach_file($app2,$id2,$id1,$remark); - } - elseif ($app2 == self::VFS_APPNAME) - { - return self::attach_file($app1,$id1,$id2,$remark); - } - $link_id = solink::link($app1,$id1,$app2,$id2,$remark,$owner); - - if (!($no_notify&2)) self::notify('link',$app2,$id2,$app1,$id1,$link_id); - if (!($no_notify&1)) self::notify('link',$app1,$id1,$app2,$id2,$link_id); - - return $link_id; - } - - /** - * generate temporary link_id used as array-key - * - * @param string $app app-name - * @param mixed $id - * @return string - */ - static function temp_link_id($app,$id) - { - return $app.':'.(!in_array($app, array(self::VFS_APPNAME,self::VFS_LINK, self::DATA_APPNAME)) ? $id : $id['name']); - } - - /** - * returns array of links to $app,$id (reimplemented to deal with not yet created items) - * - * @param string $app appname - * @param string|array $id id(s) in $app - * @param string $only_app ='' if set return only links from $only_app (eg. only addressbook-entries) or NOT from if $only_app[0]=='!' - * @param string $order ='link_lastmod DESC' defaults to newest links first - * @param boolean $cache_titles =false should all titles be queryed and cached (allows to query each link app only once!) - * This option also removes links not viewable by current user from the result! - * @param boolean $deleted =false Include links that have been flagged as deleted, waiting for purge of linked record. - * @param int $limit =null number of entries to return, only affects links, attachments are allways reported! - * @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 - */ - static function get_links($app, $id, $only_app='', $order='link_lastmod DESC',$cache_titles=false, $deleted=false, $limit=null) - { - if (self::DEBUG) echo "

egw_link::get_links(app='$app',id='$id',only_app='$only_app',order='$order',deleted='$deleted')

\n"; - - if (is_array($id) || !$id) - { - $ids = array(); - if (is_array($id)) - { - if (($not_only = $only_app[0] == '!')) - { - $only_app = substr(1,$only_app); - } - foreach (array_reverse($id) as $link) - { - if (is_array($link) // check for unlink-marker - && !($only_app && $not_only == ($link['app'] == $only_app))) - { - $ids[$link['link_id']] = $only_app ? $link['id'] : $link; - } - } - } - return $ids; - } - $ids = solink::get_links($app, $id, $only_app, $order, $deleted, $limit); - if (empty($only_app) || $only_app == self::VFS_APPNAME || - ($only_app[0] == '!' && $only_app != '!'.self::VFS_APPNAME)) - { - if (($vfs_ids = self::list_attached($app,$id))) - { - $ids += $vfs_ids; - } - } - //echo "ids=
"; print_r($ids); echo "
\n"; - if ($cache_titles) - { - // agregate links by app - $app_ids = array(); - foreach($ids as $link) - { - $app_ids[$link['app']][] = $link['id']; - } - foreach($app_ids as $appname => $a_ids) - { - self::titles($appname,array_unique($a_ids)); - } - // remove links, current user has no access, from result - foreach($ids as $key => $link) - { - if (!self::title($link['app'],$link['id'])) - { - unset($ids[$key]); - } - } - reset($ids); - } - return $ids; - } - - /** - * Query the links of multiple entries of one application - * - * @ToDo also query the attachments in a single query, eg. via a directory listing of /apps/$app - * @param string $app - * @param array $ids - * @param boolean $cache_titles =true should all titles be queryed and cached (allows to query each link app only once!) - * @param string $only_app if set return only links from $only_app (eg. only addressbook-entries) or NOT from if $only_app[0]=='!' - * @param string $order ='link_lastmod DESC' defaults to newest links first - * @param boolean $deleted =false Include links that have been flagged as deleted, waiting for purge of linked record. - * @return array of $id => array($links) pairs - */ - static function get_links_multiple($app,array $ids,$cache_titles=true,$only_app='',$order='link_lastmod DESC', $deleted=false ) - { - if (self::DEBUG) echo "

".__METHOD__."('$app',".print_r($ids,true).",$cache_titles,'$only_app','$order')

\n"; - - if (!$ids) - { - return array(); // no ids are linked to nothing - } - $links = solink::get_links($app,$ids,$only_app,$order,$deleted); - - if (empty($only_app) || $only_app == self::VFS_APPNAME || - ($only_app[0] == '!' && $only_app != '!'.self::VFS_APPNAME)) - { - // todo do that in a single query, eg. directory listing, too - foreach($ids as $id) - { - if (!isset($links[$id])) - { - $links[$id] = array(); - } - if (($vfs_ids = self::list_attached($app,$id))) - { - $links[$id] += $vfs_ids; - } - } - } - if ($cache_titles) - { - // agregate links by app - $app_ids = array(); - foreach($links as &$targets) - { - foreach($targets as $link) - { - if (is_array($link)) $app_ids[$link['app']][] = $link['id']; - } - } - foreach($app_ids as $app => $a_ids) - { - self::titles($app,array_unique($a_ids)); - } - } - return $links; - } - - /** - * Read one link specified by it's link_id or by the two end-points - * - * If $id is an array (links not yet created) only link_ids are allowed. - * - * @param int|string $app_link_id > 0 link_id of link or app-name of link - * @param string|array $id ='' id if $app_link_id is an appname or array with links, if 1. entry not yet created - * @param string $app2 ='' second app - * @param string $id2 ='' id in $app2 - * @return array with link-data or False - */ - static function get_link($app_link_id,$id='',$app2='',$id2='') - { - if (self::DEBUG) - { - echo '

'.__METHOD__."($app_link_id,$id,$app2,$id2)

\n"; echo function_backtrace(); - } - if (is_array($id)) - { - if (strpos($app_link_id,':') === false) $app_link_id = self::temp_link_id($app2,$id2); // create link_id of temporary link, if not given - - if (isset($id[$app_link_id]) && is_array($id[$app_link_id])) // check for unlinked-marker - { - return $id[$app_link_id]; - } - return False; - } - if ((int)$app_link_id < 0 || $app_link_id == self::VFS_APPNAME || $app2 == self::VFS_APPNAME) - { - if ((int)$app_link_id < 0) // vfs link_id ? - { - return self::fileinfo2link(-$app_link_id); - } - if ($app_link_id == self::VFS_APPNAME) - { - return self::info_attached($app2,$id2,$id); - } - return self::info_attached($app_link_id,$id,$id2); - } - return solink::get_link($app_link_id,$id,$app2,$id2); - } - - /** - * Remove link with $link_id or all links matching given $app,$id - * - * Note: if $link_id != '' and $id is an array: unlink removes links from that array only - * unlink has to be called with &$id to see the result (depricated) or unlink2 has to be used !!! - * - * @param $link_id link-id to remove if > 0 - * @param string $app ='' appname of first endpoint - * @param string|array $id ='' id in $app or array with links, if 1. entry not yet created - * @param int $owner =0 account_id to delete all links of a given owner, or 0 - * @param string $app2 ='' app of second endpoint - * @param string $id2 ='' id in $app2 - * @param boolean $hold_for_purge Don't really delete the link, just mark it as deleted and wait for final delete - * @return the number of links deleted - */ - static function unlink($link_id,$app='',$id='',$owner=0,$app2='',$id2='',$hold_for_purge=false) - { - return self::unlink2($link_id,$app,$id,$owner,$app2,$id2,$hold_for_purge); - } - - /** - * Remove link with $link_id or all links matching given $app,$id - * - * @param $link_id link-id to remove if > 0 - * @param string $app ='' appname of first endpoint - * @param string|array &$id='' id in $app or array with links, if 1. entry not yet created - * @param int $owner =0 account_id to delete all links of a given owner, or 0 - * @param string $app2 ='' app of second endpoint, or !file (other !app are not yet supported!) - * @param string $id2 ='' id in $app2 - * @param boolean $hold_for_purge Don't really delete the link, just mark it as deleted and wait for final delete - * @return the number of links deleted - */ - static function unlink2($link_id,$app,&$id,$owner=0,$app2='',$id2='',$hold_for_purge=false) - { - if (self::DEBUG) - { - echo "

egw_link::unlink('$link_id','$app',".array2string($id).",'$owner','$app2','$id2', $hold_for_purge)

\n"; - } - if ($link_id < 0) // vfs-link? - { - return self::delete_attached(-$link_id); - } - elseif ($app == self::VFS_APPNAME) - { - return self::delete_attached($app2,$id2,$id); - } - elseif ($app2 == self::VFS_APPNAME) - { - return self::delete_attached($app,$id,$id2); - } - if (!is_array($id)) - { - if (!$link_id && !$app2 && !$id2 && $app2 != '!'.self::VFS_APPNAME) - { - self::delete_attached($app,$id); // deleting all attachments - self::delete_cache($app,$id); - } - - // Log in history - if($link_id && (!$app || !$app2)) - { - // Need to load it first - $link = self::get_link($link_id); - $app = $link['link_app1']; - $id = $link['link_id1']; - $app2 = $link['link_app2']; - $id2 = $link['link_id2']; - } - if ($app && $app2) - { - historylog::static_add($app,$id,$GLOBALS['egw_info']['user']['account_id'],'~link~','',$app2.':'.$id2); - historylog::static_add($app2,$id2,$GLOBALS['egw_info']['user']['account_id'],'~link~','',$app.':'.$id); - } - $deleted =& solink::unlink($link_id,$app,$id,$owner,$app2 != '!'.self::VFS_APPNAME ? $app2 : '',$id2,$hold_for_purge); - - // only notify on real links, not the one cached for writing or fileattachments - self::notify_unlink($deleted); - - return count($deleted); - } - if (!$link_id) $link_id = self::temp_link_id($app2,$id2); // create link_id of temporary link, if not given - - if (isset($id[$link_id])) - { - $id[$link_id] = False; // set the unlink marker - - if (self::DEBUG) - { - _debug_array($id); - } - return True; - } - return False; - } - - /** - * get list/array of link-aware apps the user has rights to use - * - * @param string $must_support capability the apps need to support, eg. 'add', default ''=list all apps - * @return array with app => title pairs - */ - static function app_list($must_support='') - { - $apps = array(); - foreach(self::$app_register as $type => $reg) - { - if ($must_support && !isset($reg[$must_support])) continue; - - list($app) = explode('-', $type); - if ($GLOBALS['egw_info']['user']['apps'][$app]) - { - $apps[$type] = lang(self::get_registry($type, 'name')); - } - } - return $apps; - } - - /** - * Searches for a $pattern in the entries of $app - * - * @param string $app app to search - * @param string $pattern pattern to search - * @param string $type Search only a certain sub-type of records (optional) - * @return array with $id => $title pairs of matching entries of app - */ - static function query($app,$pattern, &$options = array()) - { - if ($app == '' || !is_array($reg = self::$app_register[$app]) || !isset($reg['query'])) - { - return array(); - } - $method = $reg['query']; - - if (self::DEBUG) - { - echo "

egw_link::query('$app','$pattern') => '$method'

\n"; - echo "Options: "; _debug_array($options); - } - - // See etemplate's nextmatch widget, following was copied from there - // allow static callbacks - if(strpos($method,'::') !== false) - { - // workaround for php < 5.3: do NOT call it static, but allow application code to specify static callbacks - if (version_compare(PHP_VERSION,'5.3','<')) list($class,$method) = explode('::',$method); - } - else - { - list($app,$class,$method) = explode('.',$method); - } - if ($class) - { - if (!$app && !is_object($GLOBALS[$class])) - { - $GLOBALS[$class] = new $class(); - } - if (is_object($GLOBALS[$class])) // use existing instance (put there by a previous CreateObject) - { - $obj = $GLOBALS[$class]; - } - else - { - $obj = CreateObject($app.'.'.$class); - } - } - if(is_callable($method)) // php5.3+ call - { - $result = call_user_func($method,$pattern,$options); - } - elseif(is_object($obj) && method_exists($obj,$method)) - { - $result = $obj->$method($pattern,$options); - } - else - { - // if there is no object or no method, give a more explaining error message - throw new egw_exception_assertion_failed("Object has no method '$reg[query]'!"); - } - - if (!isset($options['total'])) - { - $options['total'] = count($result); - } - if (is_array($result) && (isset($options['start']) || (isset($options['num_rows']) && count($result) > $options['num_rows']))) - { - $result = array_slice($result, $options['start'], (isset($options['num_rows']) ? $options['num_rows'] : count($result)), true); - } - - return $result; - } - - /** - * returns the title (short description) of entry $id and $app - * - * @param string $app appname - * @param string $id id in $app - * @param array $link =null link-data for file-attachments - * @return string/boolean string with title, null if $id does not exist in $app or false if no perms to view it - */ - static function title($app,$id,$link=null) - { - if (!$id) return ''; - - $title =& self::get_cache($app,$id); - if (isset($title) && !empty($title) && !is_array($id)) - { - if (self::DEBUG) echo '

'.__METHOD__."('$app','$id')='$title' (from cache)

\n"; - return $title; - } - if ($app == self::VFS_APPNAME) - { - if (is_array($id) && $link) - { - $link = $id; - $title = egw_vfs::decodePath($link['name']); - } - else - { - $title = $id; - } - /* disabling mime-type and size in link-title of attachments, as it clutters the UI - and users dont need it most of the time. These details can allways be views in filemanager. - if (is_array($link)) - { - $title .= ': '.$link['type'] . ' '.egw_vfs::hsize($link['size']); - }*/ - if (self::DEBUG) echo '

'.__METHOD__."('$app','$id')='$title' (file)

\n"; - return $title; - } - if ($app == '' || !is_array($reg = self::$app_register[$app]) || !isset($reg['title'])) - { - if (self::DEBUG) echo "

".__METHOD__."('$app','$id') something is wrong!!!

\n"; - return false; //array(); // not sure why it should return an array on failure, as the description states boolean/string - } - $method = $reg['title']; - - if (true) $title = ExecMethod($method,$id); - - if ($id && is_null($title)) // $app,$id has been deleted ==> unlink all links to it - { - static $unlinking = array(); - // check if we are already trying to unlink the entry, to avoid an infinit recursion - if (!isset($unlinking[$app]) || !isset($unlinking[$app][$id])) - { - $unlinking[$app][$id] = true; - self::unlink(0,$app,$id); - unset($unlinking[$app][$id]); - } - if (self::DEBUG) echo '

'.__METHOD__."('$app','$id') unlinked, as $method returned null

\n"; - return False; - } - if (self::DEBUG) echo '

'.__METHOD__."('$app','$id')='$title' (from $method)

\n"; - - return $title; - } - - /** - * Maximum number of titles to query from an application at once (to NOT trash mysql) - */ - const MAX_TITLES_QUERY = 100; - - /** - * Query the titles off multiple id's of one app - * - * Apps can implement that hook, if they have a quicker (eg. less DB queries) method to query the title of multiple entries. - * If it's not implemented, we call the regular title method multiple times. - * - * @param string $app - * @param array $ids - */ - static function titles($app,array $ids) - { - if (self::DEBUG) - { - echo "

".__METHOD__."($app,".implode(',',$ids).")

\n"; - } - $titles = $ids_to_query = array(); - foreach($ids as $id) - { - $title =& self::get_cache($app,$id); - if (!isset($title)) - { - if (isset(self::$app_register[$app]['titles'])) - { - $ids_to_query[] = $id; // titles method --> collect links to query at once - } - else - { - $title = self::title($app,$id); // no titles method --> fallback to query each link separate - } - } - $titles[$id] = $title; - } - if ($ids_to_query) - { - for ($n = 0; ($ids = array_slice($ids_to_query,$n*self::MAX_TITLES_QUERY,self::MAX_TITLES_QUERY)); ++$n) - { - foreach(ExecMethod(self::$app_register[$app]['titles'],$ids) as $id => $t) - { - $title =& self::get_cache($app,$id); - $titles[$id] = $title = $t; - } - } - } - return $titles; - } - - /** - * Add new entry to $app, evtl. already linked to $to_app, $to_id - * - * @param string $app appname of entry to create - * @param string $to_app ='' appname to link the new entry to - * @param string $to_id =''id in $to_app - * @return array/boolean with name-value pairs for link to add-methode of $app or false if add not supported - */ - static function add($app,$to_app='',$to_id='') - { - //echo "

egw_link::add('$app','$to_app','$to_id') app_register[$app] ="; _debug_array($app_register[$app]); - if ($app == '' || !is_array($reg = self::$app_register[$app]) || !isset($reg['add'])) - { - return false; - } - $params = $reg['add']; - - if ($reg['add_app'] && $to_app && $reg['add_id'] && $to_id) - { - $params[$reg['add_app']] = $to_app; - $params[$reg['add_id']] = $to_id; - } - return $params; - } - - /** - * Edit entry $id of $app - * - * @param string $app appname of entry - * @param string $id id in $app - * @param string &$popup=null on return popup size eg. '600x400' or null - * @return array|boolean with name-value pairs for link to edit-methode of $app or false if edit not supported - */ - static function edit($app,$id,&$popup=null) - { - //echo "

egw_link::add('$app','$to_app','$to_id') app_register[$app] ="; _debug_array($app_register[$app]); - if (empty($app) || empty($id) || !is_array($reg = self::$app_register[$app]) || !isset($reg['edit'])) - { - if ($reg && isset($reg['view'])) - { - $popup = $reg['view_popup']; - return self::view($app,$id); // fallback to view - } - return false; - } - $params = $reg['edit']; - $params[$reg['edit_id']] = $id; - - $popup = $reg['edit_popup']; - - return $params; - } - - /** - * view entry $id of $app - * - * @param string $app appname - * @param string $id id in $app - * @param array $link =null link-data for file-attachments - * @return array with name-value pairs for link to view-methode of $app to view $id - */ - static function view($app,$id,$link=null) - { - if ($app == self::VFS_APPNAME && !empty($id) && is_array($link)) - { - //return egw_vfs::download_url(self::vfs_path($link['app2'],$link['id2'],$link['id'],true)); - return self::mime_open(self::vfs_path($link['app2'],$link['id2'],$link['id'],true), $link['type']); - } - if ($app == '' || !is_array($reg = self::$app_register[$app]) || !isset($reg['view']) || !isset($reg['view_id'])) - { - return array(); - } - $view = $reg['view']; - - $names = explode(':',$reg['view_id']); - if (count($names) > 1) - { - $id = explode(':',$id); - while (list($n,$name) = each($names)) - { - $view[$name] = $id[$n]; - } - } - else - { - $view[$reg['view_id']] = $id; - } - return $view; - } - - /** - * Get mime-type information from app-registry - * - * Only return information from apps the user has access too (incl. registered sub-types of that apps). - * - * We prefer full matches over wildcards like "text/*" written as regexp "/^text\\//". - * - * @param string $type - * @return array with values for keys 'menuaction', 'mime_id' (path) or 'mime_url' and options 'mime_popup' and other values to pass one - */ - static function get_mime_info($type) - { - foreach(self::$app_register as $app => $registry) - { - if (isset($registry['mime']) && - (isset($GLOBALS['egw_info']['user']['apps'][$app]) || - isset($registry['app']) && isset($GLOBALS['egw_info']['user']['apps'][$registry['app']]))) - { - foreach($registry['mime'] as $mime => $data) - { - if ($mime == $type) return $data; - if ($mime[0] == '/' && preg_match($mime.'i', $type)) - { - $wildcard_mime = $data; - } - } - } - } - return isset($wildcard_mime) ? $wildcard_mime : null; - } - - /** - * Get handler (link-data) for given path and mime-type - * - * @param string $path vfs path - * @param string $type =null default to egw_vfs::mime_content_type($path) - * @param string &$popup=null on return popup size or null - * @return string|array string with EGw relative link, array with get-parameters for '/index.php' or null (directory and not filemanager access) - */ - static function mime_open($path, $type=null, &$popup=null) - { - if (is_null($type)) $type = egw_vfs::mime_content_type($path); - - if (($data = self::get_mime_info($type))) - { - if (isset($data['mime_url'])) - { - $data[$data['mime_url']] = egw_vfs::PREFIX.$path; - unset($data['mime_url']); - } - elseif (isset($data['mime_id'])) - { - $data[$data['mime_id']] = $path; - unset($data['mime_id']); - } - elseif(isset($data['mime_popup'])) - { - $popup = $data['mime_popup']; - } - else - { - throw new egw_exception_assertion_failed("Missing 'mime_id' or 'mime_url' for mime-type '$type'!"); - } - unset($data['mime_popup']); - } - else - { - $data = egw_vfs::download_url($path); - } - return $data; - } - - /** - * Check if $app uses a popup for $action - * - * @param string $app app-name - * @param string $action ='view' name of the action, atm. 'view' or 'add' - * @param array $link =null link-data for file-attachments - * @return boolean|string false if no popup is used or $app is not registered, otherwise string with the prefered popup size (eg. '640x400) - */ - static function is_popup($app, $action='view', $link=null) - { - $popup = self::get_registry($app,$action.'_popup'); - - // for files/attachments check mime-registry - if ($app == self::VFS_APPNAME && is_array($link) && !empty($link['type'])) - { - $path = self::vfs_path($link['app2'], $link['id2'], $link['id'], true); - $p = null; - if (self::mime_open($path, $link['type'], $p)) - { - $popup = $p; - } - } - //error_log(__METHOD__."('$app', '$action', ".array2string($link).') returning '.array2string($popup)); - return $popup; - } - - /** - * Check if $app is in the registry and has an entry for $name - * - * @param string $app app-name - * @param string $name name / key in the registry, eg. 'view' - * @return boolean|string false if $app is not registered, otherwise string with the value for $name - */ - static function get_registry($app,$name) - { - $reg = self::$app_register[$app]; - - if (!isset($reg)) return false; - - if (!isset($reg[$name])) // some defaults - { - switch($name) - { - case 'name': - $reg[$name] = $app; - break; - case 'icon': - if (isset($GLOBALS['egw_info']['apps'][$app]['icon'])) - { - $reg[$name] = ($GLOBALS['egw_info']['apps'][$app]['icon_app'] ? $GLOBALS['egw_info']['apps'][$app]['icon_app'] : $app). - '/'.$GLOBALS['egw_info']['apps'][$app]['icon']; - } - else - { - $reg[$name] = $app.'/navbar'; - } - break; - } - } - - return isset($reg) ? $reg[$name] : false; - } - - /** - * path to the attached files of $app/$ip or the directory for $app if no $id,$file given - * - * All link-files are based in the vfs-subdir '/apps/'.$app - * - * @param string $app appname - * @param string $id ='' id in $app - * @param string $file ='' filename - * @param boolean $just_the_path =false return url or just the vfs path - * @return string/array path or array with path and relatives, depending on $relatives - */ - static function vfs_path($app,$id='',$file='',$just_the_path=false) - { - $path = self::VFS_BASEURL; - - if ($app) - { - if( isset(self::$app_register[$app]) ) { - $reg = self::$app_register[$app]; - - if( isset($reg['file_dir']) ) { - $app = $reg['file_dir']; - } - } - - $path .= '/'.$app; - - if ($id) - { - $path .= '/'.$id; - - if ($file) - { - $path .= '/'.$file; - } - } - } - if ($just_the_path) - { - $path = parse_url($path,PHP_URL_PATH); - } - else - { - $path = egw_vfs::resolve_url($path); - } - //error_log(__METHOD__."($app,$id,$file,$just_the_path)=$path"); - return $path; - } - - /** - * Put a file to the corrosponding place in the VFS and set the attributes - * - * Does NO is_uploaded_file check, calling application is responsible for doing that for uploaded files! - * - * @param string $app appname to linke the file to - * @param string $id id in $app - * @param array $file informations about the file in format of the etemplate file-type - * $file['name'] name of the file (no directory) - * $file['type'] mine-type of the file - * $file['tmp_name'] name of the uploaded file (incl. directory) or resource of opened file - * @param string $comment ='' comment to add to the link - * @return int negative id of egw_sqlfs table as negative link-id's are for vfs attachments - */ - static function attach_file($app,$id,$file,$comment='') - { - $entry_dir = self::vfs_path($app,$id); - if (self::DEBUG) - { - echo "

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'

\n"; - } - if (file_exists($entry_dir) || ($Ok = mkdir($entry_dir,0,true))) - { - $Ok = egw_vfs::copy_uploaded($file, $p=self::vfs_path($app,$id,'',true), $comment, false); // no is_uploaded_file() check! - if (!$Ok) error_log(__METHOD__."('$app', '$id', ".array2string($file).", '$comment') called egw_vfs::copy_uploaded('$file[tmp_name]', '$p', '$comment', false)=".array2string($Ok)); - } - else - { - error_log(__METHOD__."($app,$id,".array2string($file).",$comment) Can't mkdir $entry_dir!"); - } - return $Ok ? -$Ok['ino'] : false; - } - - /** - * Links the entry to an existing file in the VFS - * - * @param string $app appname to link the file to - * @param string $id id in $app - * @param string $file VFS path to link to - * @param string $comment ='' comment to add to the link - */ - static function link_file($app,$id,$file)//,$comment='') - { - // Don't try to link into app dir if there is no id - if(!$id) return; - - $app_path = self::vfs_path($app,$id); - $ok = true; - if (egw_vfs::file_exists($app_path) || ($ok = egw_vfs::mkdir($app_path,0,true))) - { - if (!egw_vfs::stat($file)) - { - error_log(__METHOD__. ' (Link target ' . egw_vfs::decodePath($file) . ' not found!'); - return false; - } - } - - $link = egw_vfs::concat($app_path,egw_vfs::basename($file)); - return egw_vfs::symlink($file,$link); - } - /** - * 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!) - * - * @param int|string $app > 0: file_id of an attchemnt or $app/$id entry which linked to - * @param string $id ='' id in app - * @param string $fname ='' filename - * @return boolean|array false on error ($app or $id not found), array with path as key and boolean result of delete - */ - static function delete_attached($app,$id='',$fname='') - { - if ((int)$app > 0) // is file_id - { - $url = egw_vfs::resolve_url(sqlfs_stream_wrapper::id2path($app)); - } - else - { - if (empty($app) || empty($id)) - { - return False; // dont delete more than all attachments of an entry - } - $url = self::vfs_path($app,$id,$fname); - - if (!$fname || !$id) // we delete the whole entry (or all entries), which probably not exist anymore - { - $current_is_root = egw_vfs::$is_root; - egw_vfs::$is_root = true; - } - } - if (self::DEBUG) - { - echo '

'.__METHOD__."('$app','$id','$fname') url=$url

\n"; - } - // Log in history - Need to load it first - if((int)$app > 0) - { - $link = self::get_link(-$app); - if($link['app2'] && $link['id2']) - { - historylog::static_add($link['app2'],$link['id2'],$GLOBALS['egw_info']['user']['account_id'],'~file~','', basename($url)); - } - } - if (($Ok = !file_exists($url) || egw_vfs::remove($url,true)) && ((int)$app > 0 || $fname)) - { - // try removing the dir, in case it's empty - @egw_vfs::rmdir(egw_vfs::dirname($url)); - } - if (!is_null($current_is_root)) - { - egw_vfs::$is_root = $current_is_root; - } - return $Ok; - } - - /** - * converts the infos vfs has about a file into a link - * - * @param string $app appname - * @param string $id id in app - * @param string $filename filename - * @return array 'kind' of link-array - */ - static function info_attached($app,$id,$filename) - { - $path = self::vfs_path($app,$id,$filename,true); - if (!($stat = egw_vfs::stat($path,STREAM_URL_STAT_QUIET))) - { - return false; - } - return self::fileinfo2link($stat,$path); - } - - /** - * converts a fileinfo (row in the vfs-db-table) in a link - * - * @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 - * @return array a 'kind' of link-array - */ - static function fileinfo2link($fileinfo,$url=null) - { - if (!is_array($fileinfo)) - { - $url = sqlfs_stream_wrapper::id2path($fileinfo); - if (!($fileinfo = egw_vfs::url_stat($url,STREAM_URL_STAT_QUIET))) - { - return false; - } - } - - $up = explode('/',$url[0] == '/' ? $url : parse_url($url,PHP_URL_PATH)); // /apps/$app/$id - $app = null; - - foreach( self::$app_register as $tapp => $reg ) { - if( isset($reg['file_dir']) ) { - $lup = $up; - - unset($lup[0]); - unset($lup[1]); - reset($lup); - - $fdp = explode('/',$reg['file_dir'][0] == '/' ? - $reg['file_dir'] : parse_url($reg['file_dir'],PHP_URL_PATH)); - - $found = true; - - foreach( $fdp as $part ) { - if( current($lup) == $part ) { - if( next($lup) === false ) { - $found = false; - break; - } - } - else { - $found = false; - break; - } - } - - if( $found ) { - $id = current($lup); - $app = $tapp; - break; - } - } - } - - if( $app === null ) { - list(,,$app,$id) = $up; - } - - return array( - 'app' => self::VFS_APPNAME, - 'id' => $fileinfo['name'], - 'app2' => $app, - 'id2' => $id, - 'remark' => '', // only list_attached currently sets the remark - 'owner' => $fileinfo['uid'], - 'link_id' => -$fileinfo['ino'], - 'lastmod' => $fileinfo['mtime'], - 'size' => $fileinfo['size'], - 'type' => $fileinfo['mime'], - ); - } - - /** - * lists all attachments to $app/$id - * - * @param string $app appname - * @param string $id id in app - * @return array with link_id => 'kind' of link-array pairs - */ - static function list_attached($app,$id) - { - $path = self::vfs_path($app,$id); - //error_log(__METHOD__."($app,$id) url=$url"); - - if (!($extra = self::get_registry($app,'find_extra'))) $extra = array(); - - // always use regular links stream wrapper here: extended one is unnecessary (slow) for just listing attachments - if (substr($path,0,13) == 'stylite.links') $path = substr($path,8); - - $attached = array(); - if (($url2stats = egw_vfs::find($path,array('need_mime'=>true,'type'=>'F','url'=>true)+$extra,true))) - { - $props = egw_vfs::propfind(array_keys($url2stats)); // get the comments - foreach($url2stats as $url => &$fileinfo) - { - $link = self::fileinfo2link($fileinfo,$url); - if ($props && isset($props[$url])) - { - foreach($props[$url] as $prop) - { - if ($prop['ns'] == egw_vfs::DEFAULT_PROP_NAMESPACE && $prop['name'] == 'comment') - { - $link['remark'] = $prop['val']; - } - } - } - $attached[$link['link_id']] = $link; - } - } - return $attached; - } - - /** - * reverse static function of htmlspecialchars() - * - * @param string $str string to decode - * @return string decoded string - */ - static private function decode_htmlspecialchars($str) - { - return str_replace(array('&','"','<','>'),array('&','"','<','>'),$str); - } - - /** - * Key for old link title in $data param to egw_link::notify - */ - const OLD_LINK_TITLE = 'old_link_title'; - - /** - * notify other apps about changed content in $app,$id - * - * To give other apps the possebility to update a title, you can also specify - * a changed old link-title in $data[egw_link::OLD_LINK_TITLE]. - * - * @param string $app name of app in which the updated happend - * @param string $id id in $app of the updated entry - * @param array $data =null updated data of changed entry, as the read-method of the BO-layer would supply it - */ - static function notify_update($app,$id,$data=null) - { - self::delete_cache($app,$id); - //error_log(__METHOD__."('$app', $id, $data)"); - foreach(self::get_links($app,$id,'!'.self::VFS_APPNAME) as $link_id => $link) - { - self::notify('update',$link['app'],$link['id'],$app,$id,$link_id,$data); - } - if($data[egw_link::OLD_LINK_TITLE] && egw_json_response::isJSONResponse()) - { - // Update client side with new title - egw_json_response::get()->apply('egw.link_title_callback',array(array($app => array($id => self::title($app, $id))))); - } - } - - /** - * Stores notifications to run after regular processing is done - * - * @var array - */ - private static $notifies = array(); - - /** - * notify an application about a new or deleted links to own entries or updates in the content of the linked entry - * - * Please note: not all apps supply update notifications - * - * @internal - * @param string $type 'link' for new links, 'unlink' for unlinked entries, 'update' of content in linked entries - * @param string $notify_app app to notify - * @param string $notify_id id in $notify_app - * @param string $target_app name of app whos entry changed, linked or deleted - * @param string $target_id id in $target_app - * @param array $data =null data of entry in app2 (optional) - */ - static private function notify($type,$notify_app,$notify_id,$target_app,$target_id,$link_id,$data=null) - { - //error_log(__METHOD__."('$type', '$notify_app', $notify_id, '$target_app', $target_id, $link_id, $data)"); - if ($link_id && isset(self::$app_register[$notify_app]) && isset(self::$app_register[$notify_app]['notify'])) - { - if (!self::$notifies) - { - egw::on_shutdown(array(__CLASS__, 'run_notifies')); - } - self::$notifies[] = array( - 'method' => self::$app_register[$notify_app]['notify'], - 'type' => $type, - 'id' => $notify_id, - 'target_app' => $target_app, - 'target_id' => $target_id, - 'link_id' => $link_id, - 'data' => $data, - ); - } - } - - /** - * Run notifications called by egw::on_shutdown(), after regular processing is finished - */ - static public function run_notifies() - { - //error_log(__METHOD__."() count(self::\$notifies)=".count(self::$notifies)); - while(self::$notifies) - { - $args = array_shift(self::$notifies); - $method = $args['method']; - unset($args['method']); - //error_log(__METHOD__."() calling $method(".array2string($args).')'); - ExecMethod($method, $args); - } - } - - /** - * notifies about unlinked links - * - * @internal - * @param array &$links unlinked links from the database - */ - static private function notify_unlink(&$links) - { - foreach($links as $link) - { - // we notify both sides of the link, as the unlink command NOT clearly knows which side initiated the unlink - self::notify('unlink',$link['link_app1'],$link['link_id1'],$link['link_app2'],$link['link_id2'],$link['link_id']); - self::notify('unlink',$link['link_app2'],$link['link_id2'],$link['link_app1'],$link['link_id1'],$link['link_id']); - } - } - - /** - * Get a reference to the cached value for $app/$id for $type - * - * @param string $app - * @param string|int $id - * @param string $type ='title' 'title' or 'file_access' - * @return int|string can be null, if cache not yet set - */ - private static function &get_cache($app,$id,$type = 'title') - { - switch($type) - { - case 'title': - return self::$title_cache[$app.':'.$id]; - case 'file_access': - return self::$file_access_cache[$app.':'.$id]; - default: - throw new egw_exception_wrong_parameter("Unknown type '$type'!"); - } - } - - /** - * Set title and optional file_access cache for $app,$id - * - * Allows applications to set values for title and file access, eg. in their search method, - * to not be called again. This offloads the need to cache from the app to the link class. - * If there's no caching, items get read multiple times from the database! - * - * @param string $app - * @param int|string $id - * @param string $title title string or null - * @param int $file_access =null EGW_ACL_READ, EGW_ACL_EDIT or both or'ed together - */ - public static function set_cache($app,$id,$title,$file_access=null) - { - //error_log(__METHOD__."($app,$id,$title,$file_access)"); - if (!is_null($title)) - { - self::$title_cache[$app.':'.$id] = $title; - } - if (!is_null($file_access)) - { - self::$file_access_cache[$app.':'.$id] = $file_access; - } - } - - /** - * Delete the diverse caches for $app/$id - * - * @param string $app app-name or null to delete the whole cache - * @param int|string $id id or null to delete only file_access cache of given app (keeps title cache, if app implements file_access!) - */ - private static function delete_cache($app,$id) - { - unset(self::$title_cache[$app.':'.$id]); - unset(self::$file_access_cache[$app.':'.$id]); - } - - /** - * Store function call and parameters in session and return id to retrieve it result - * - * @param string $mime_type - * @param string $method - * @param array $params - * @param boolean $ignore_mime =false true: return id, even if nothing registered for given mime-type - * @return string|null md5 hash of stored data of server-side supported mime-type or null otherwise - */ - public static function set_data($mime_type, $method, array $params, $ignore_mime=false) - { - if (!$ignore_mime && (!($info = self::get_mime_info($mime_type)) || empty($info['mime_data']))) - { - return null; - } - array_unshift($params, $method); - $id = md5(serialize($params)); - //error_log(__METHOD__."('$mime_type', '$method', ...) params=".array2string($params)." --> json=".array2string(serialize($params)).' --> id='.array2string($id)); - egw_cache::setSession(__CLASS__, $id, $params); - return $id; - } - - /** - * Call stored function with parameters and return result - * - * @param string $id - * @param boolean $return_resource =false false: return string, true: return resource - * @return mixed null if id is not found or invalid - * @throws egw_exception_wrong_parameter - */ - public static function get_data($id, $return_resource=false) - { - $data = egw_cache::getSession(__CLASS__, $id); - - if (!isset($data) || empty($data[0])) - { - throw new egw_exception_wrong_parameter(__METHOD__."('$id')"); - } - $ret = call_user_func_array('ExecMethod2', $data); - - if (is_resource($ret)) fseek($ret, 0); - - if ($return_resource != is_resource($ret)) - { - if ($return_resource && ($fp = fopen('php://temp', 'w'))) - { - fwrite($fp, $ret); - fseek($fp, 0); - $ret = $fp; - } - if (!$return_resource) - { - $fp = $ret; - $ret = ''; - while(!feof($fp)) - { - $ret .= fread($fp, 8192); - } - fclose($fp); - } - } - //error_log(__METHOD__."('$id') returning ".gettype($ret).'='.array2string($ret)); - return $ret; - } - - /** - * Check the file access perms for $app/id and given user $user - * - * If $user given and != current user AND app does not set file_access_user=true, - * allways return false, as there's no way to check access for an other user! - * - * @ToDo $rel_path is not yet implemented, as no app use it currently - * @param string $app - * @param string|int $id id of entry - * @param int $required =EGW_ACL_READ EGW_ACL_{READ|EDIT} - * @param string $rel_path =null - * @param int $user =null default null = current user - * @return boolean true if access granted, false otherwise - */ - static function file_access($app,$id,$required=EGW_ACL_READ,$rel_path=null,$user=null) - { - // are we called for an other user - if ($user && $user != $GLOBALS['egw_info']['user']['account_id']) - { - // check if app supports file_access WITH 4th $user parameter --> return false if not - if (!self::get_registry($app,'file_access_user') || !($method = self::get_registry($app,'file_access'))) - { - $ret = false; - $err = "(no file_access_user)"; - } - else - { - $ret = ExecMethod2($method,$id,$required,$rel_path,$user); - $err = "(from $method)"; - } - //error_log(__METHOD__."('$app',$id,$required,'$rel_path',$user) returning $err ".array2string($ret)); - return $ret; - } - - $cache =& self::get_cache($app,$id,'file_access'); - - if (!isset($cache) || $required == EGW_ACL_EDIT && !($cache & $required)) - { - if(($method = self::get_registry($app,'file_access'))) - { - $cache |= ExecMethod2($method,$id,$required,$rel_path) ? $required|EGW_ACL_READ : 0; - } - else - { - $cache |= self::title($app,$id) ? EGW_ACL_READ|EGW_ACL_EDIT : 0; - } - //error_log(__METHOD__."($app,$id,$required,$rel_path) got $cache --> ".($cache & $required ? 'true' : 'false')); - } - //else error_log(__METHOD__."($app,$id,$required,$rel_path) using cached value $cache --> ".($cache & $required ? 'true' : 'false')); - return !!($cache & $required); - } -} -egw_link::init_static(); +class egw_link extends Api\Link {} diff --git a/phpgwapi/inc/class.solink.inc.php b/phpgwapi/inc/class.solink.inc.php index 03cb5d5425..2c530a3ad4 100644 --- a/phpgwapi/inc/class.solink.inc.php +++ b/phpgwapi/inc/class.solink.inc.php @@ -1,6 +1,6 @@ - * @copyright 2001-2015 by RalfBecker@outdoor-training.de + * @copyright 2001-2016 by RalfBecker@outdoor-training.de * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License * @package api * @subpackage link * @version $Id$ */ +use EGroupware\Api; + /** * generalized linking between entries of eGroupware apps - SO layer * - * All vars passed to this class get correct escaped to prevent query insertion. - * - * All methods are now static! + * @deprecated use Api\Link\Storage */ -class solink -{ - /** - * Name of the links table - */ - const TABLE = 'egw_links'; - /** - * Turns on debug-messages - */ - const DEBUG = false; - /** - * Reference to the global db-class - * - * @var egw_db - */ - private static $db; - /** - * True if call to get_links or get_3links exceeded limit (contains not all rows) - */ - public static $limit_exceeded = false; - - /** - * creats a link between $app1,$id1 and $app2,$id2 - * - * @param string $app1 appname of 1. endpoint of the link - * @param string $id1 id in $app1 - * @param string $app2 appname of 2. endpoint of the link - * @param string $id2 id in $app2 - * @param string $remark ='' Remark to be saved with the link (defaults to '') - * @param int $owner =0 Owner of the link (defaults to user) - * @param int $lastmod =0 timestamp of last modification (defaults to now=time()) - * @return int/boolean False (for db or param-error) or on success link_id (Please not the return-value of $id1) - */ - static function link( $app1,&$id1,$app2,$id2='',$remark='',$owner=0,$lastmod=0 ) - { - if (self::DEBUG) - { - echo "

solink.link('$app1',$id1,'$app2',$id2,'$remark',$owner)

\n"; - } - if ($app1 == $app2 && $id1 == $id2 || - $id1 == '' || $id2 == '' || $app1 == '' || $app2 == '') - { - return False; // dont link to self or other nosense - } - if (($link = self::get_link($app1,$id1,$app2,$id2))) - { - if ($link['link_remark'] != $remark) - { - self::update_remark($link['link_id'],$remark); - } - return $link['link_id']; // link alread exist - } - if (!$owner) - { - $owner = $GLOBALS['egw_info']['user']['account_id']; - } - return self::$db->insert(self::TABLE,array( - 'link_app1' => $app1, - 'link_id1' => $id1, - 'link_app2' => $app2, - 'link_id2' => $id2, - 'link_remark' => $remark, - 'link_lastmod' => $lastmod ? $lastmod : time(), - 'link_owner' => $owner, - ),False,__LINE__,__FILE__) ? self::$db->get_last_insert_id(self::TABLE,'link_id') : false; - } - - /** - * update the remark of a link - * - * @param int $link_id link to update - * @param string $remark new text for the remark - * @return boolean true on success, else false - */ - static function update_remark($link_id,$remark) - { - return self::$db->update(self::TABLE,array( - 'link_remark' => $remark, - 'link_lastmod' => time(), - ),array( - 'link_id' => $link_id, - ),__LINE__,__FILE__); - } - - /** - * returns array of links to $app,$id - * - * @param string $app appname - * @param string|array $id id(s) in $app - * @param string $only_app ='' if set return only links from $only_app (eg. only addressbook-entries) or NOT from if $only_app[0]=='!' - * @param string $order ='link_lastmod DESC' defaults to newest links first - * @param boolean $deleted =false Include links that have been flagged as deleted, waiting for purge of linked record. - * @param int|array $limit =null number of entries to return, default null = all or array(offset, num_rows) to return num_rows starting from offset - * @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 - */ - static function get_links($app, $id, $only_app='', $order='link_lastmod DESC', $deleted=false, $limit=null) - { - if (self::DEBUG) - { - echo "

solink.get_links($app,".print_r($id,true).",$only_app,$order,$deleted)

\n"; - } - if (($not_only = $only_app[0] == '!')) - { - $only_app = substr($only_app,1); - } - - $offset = false; - if (is_array($limit)) - { - list($offset, $limit) = $limit; - } - elseif($limit) - { - $offset = 0; - } - - $links = array(); - try { - foreach(self::$db->select(self::TABLE, '*', self::$db->expression(self::TABLE, '((', array( - 'link_app1' => $app, - 'link_id1' => $id, - ),') OR (',array( - 'link_app2' => $app, - 'link_id2' => $id, - ),'))', - $deleted ? '' : ' AND deleted IS NULL' - ), __LINE__, __FILE__, $offset, $order ? " ORDER BY $order" : '', 'phpgwapi', $limit) as $row) - { - // check if left side (1) is one of our targets --> add it - if ($row['link_app1'] == $app && in_array($row['link_id1'],(array)$id)) - { - self::_add2links($row,true,$only_app,$not_only,$links); - } - // check if right side (2) is one of our targets --> add it (both can be true for multiple targets!) - if ($row['link_app2'] == $app && in_array($row['link_id2'],(array)$id)) - { - self::_add2links($row,false,$only_app,$not_only,$links); - } - } - // if query returns exactly limit rows, we assume there are more and therefore set self::$limit_exceeded - self::$limit_exceeded = $offset !== false && count(is_array($id) ? $links : $links[$id]) == $limit; - } - // catch Illegal mix of collations (ascii_general_ci,IMPLICIT) and (utf8_general_ci,COERCIBLE) for operation '=' (1267) - // caused by non-ascii chars compared with ascii field uid - catch(egw_exception_db $e) { - _egw_log_exception($e); - } - return is_array($id) ? $links : ($links[$id] ? $links[$id] : array()); - } - - private static function _add2links($row,$left,$only_app,$not_only,array &$links) - { - $linked_app = $left ? $row['link_app2'] : $row['link_app1']; - $linked_id = $left ? $row['link_id2'] : $row['link_id1']; - $app_id = $left ? $row['link_id1'] : $row['link_id2']; - list($app) = explode('-',$linked_app); - if ($only_app && $not_only == ($linked_app == $only_app) || !$GLOBALS['egw_info']['user']['apps'][$app]) - { - #echo "$linked_app == $only_app, ";var_dump($linked_app == $only_app);echo " ->dont return a link
"; - return; - } - #echo "returning ".(($only_app && !$not_only) ? " linkid:".$linked_id : " full array with linkid $linked_id")."
"; - $links[$app_id][$row['link_id']] = ($only_app && !$not_only) ? $linked_id : array( - 'app' => $linked_app, - 'id' => $linked_id, - 'remark' => $row['link_remark'], - 'owner' => $row['link_owner'], - 'lastmod' => $row['link_lastmod'], - 'link_id' => $row['link_id'], - 'deleted' => $row['deleted'], - ); - } - - /** - * returns data of a link - * - * @param ing/string $app_link_id > 0 link_id of link or app-name of link - * @param string $id ='' id in $app, if no integer link_id given in $app_link_id - * @param string $app2 ='' appname of 2. endpoint of the link, if no integer link_id given in $app_link_id - * @param string $id2 ='' id in $app2, if no integer link_id given in $app_link_id - * @return array with link-data or False - */ - static function get_link($app_link_id,$id='',$app2='',$id2='') - { - if (self::DEBUG) - { - echo "

solink.get_link('$app_link_id',$id,'$app2','$id2')

\n"; - } - if ((int) $app_link_id > 0) - { - $where = array('link_id' => $app_link_id); - } - else - { - if ($app_link_id == '' || $id == '' || $app2 == '' || $id2 == '') - { - return False; - } - $where = self::$db->expression(self::TABLE,'(',array( - 'link_app1' => $app_link_id, - 'link_id1' => $id, - 'link_app2' => $app2, - 'link_id2' => $id2, - ),') OR (',array( - 'link_app2' => $app_link_id, - 'link_id2' => $id, - 'link_app1' => $app2, - 'link_id1' => $id2, - ),')'); - } - try { - return self::$db->select(self::TABLE,'*',$where,__LINE__,__FILE__)->fetch(ADODB_FETCH_ASSOC); - } - // catch Illegal mix of collations (ascii_general_ci,IMPLICIT) and (utf8_general_ci,COERCIBLE) for operation '=' (1267) - // caused by non-ascii chars compared with ascii field uid - catch(egw_exception_db $e) { - _egw_log_exception($e); - } - return false; - } - - /** - * Remove link with $link_id or all links matching given params - * - * @param $link_id link-id to remove if > 0 - * @param string $app ='' app-name of links to remove - * @param string $id ='' id in $app or '' remove all links from $app - * @param int $owner =0 account_id to delete all links of a given owner, or 0 - * @param string $app2 ='' appname of 2. endpoint of the link - * @param string $id2 ='' id in $app2 - * @param boolean $hold_for_purge Don't really delete the link, just mark it as deleted and wait for final delete of linked entry - * @return array with deleted links - */ - static function unlink($link_id,$app='',$id='',$owner=0,$app2='',$id2='',$hold_for_purge=false) - { - if (self::DEBUG) - { - echo "

solink.unlink($link_id,$app,$id,$owner,$app2,$id2)

\n"; - } - if ((int)$link_id > 0) - { - $where = array('link_id' => $link_id); - } - elseif ($app == '' AND $owner == '') - { - return 0; - } - else - { - if ($app != '' && $app2 == '') - { - $check1 = array('link_app1' => $app); - $check2 = array('link_app2' => $app); - if ($id != '') - { - $check1['link_id1'] = $id; - $check2['link_id2'] = $id; - } - $where = self::$db->expression(self::TABLE,'((',$check1,') OR (',$check2,'))'); - } - elseif ($app != '' && $app2 != '') - { - $where = self::$db->expression(self::TABLE,'(',array( - 'link_app1' => $app, - 'link_id1' => $id, - 'link_app2' => $app2, - 'link_id2' => $id2, - ),') OR (',array( - 'link_app1' => $app2, - 'link_id1' => $id2, - 'link_app2' => $app, - 'link_id2' => $id, - ),')'); - } - if ($owner) - { - if ($app) $where = array($where); - $where['link_owner'] = $owner; - } - } - $deleted = array(); - try { - foreach(self::$db->select(self::TABLE,'*',$where,__LINE__,__FILE__) as $row) - { - $deleted[] = $row; - } - if($hold_for_purge) - { - self::$db->update(self::TABLE,array( - 'deleted' => time(), - 'link_lastmod' => time(), - ), $where, __LINE__,__FILE__); - } - else - { - self::$db->delete(self::TABLE,$where,__LINE__,__FILE__); - } - } - // catch Illegal mix of collations (ascii_general_ci,IMPLICIT) and (utf8_general_ci,COERCIBLE) for operation '=' (1267) - // caused by non-ascii chars compared with ascii field uid - catch(egw_exception_db $e) { - _egw_log_exception($e); - } - - return $deleted; - } - - /** - * Restore links being held as deleted waiting for purge of linked record (un-delete) - * - * @param string $app ='' app-name of links to remove - * @param string $id ='' id in $app or '' remove all links from $app - */ - static function restore($app, $id) - { - if (self::DEBUG) - { - echo "

solink.restore($app,$id)

\n"; - } - if ($app == '') - { - return 0; - } - - $check1 = array('link_app1' => $app); - $check2 = array('link_app2' => $app); - if ($id != '') - { - $check1['link_id1'] = $id; - $check2['link_id2'] = $id; - } - $where = self::$db->expression(self::TABLE,'((',$check1,') OR (',$check2,'))'); - self::$db->update(self::TABLE,array('deleted'=> null), $where, __LINE__,__FILE__); - } - - /** - * Changes ownership of all links from $owner to $new_owner - * - * This is needed when a user/account gets deleted - * Does NOT change the modification-time - * - * @param int $owner acount_id of owner to change - * @param int $new_owner account_id of new owner - * @return int number of links changed - */ - static function chown($owner,$new_owner) - { - if ((int)$owner <= 0 || (int) $new_owner <= 0) - { - return 0; - } - self::$db->update(self::TABLE,array('owner'=>$new_owner),array('owner'=>$owner),__LINE__,__FILE__); - - return self::$db->affected_rows(); - } - - /** - * Get all links from a given app's entries to an other app's entries, which both link to the same 3. app and id - * - * Example: - * I search all timesheet's linked to a given project and id(s), who are also linked to other entries, - * which link to the same project: - * - * ($app='timesheet'/some id) <--a--> (other app/other id) <--b--> ($t_app='projectmanager'/$t_id=$pm_id) - * ^ ^ - * +---------------------------c-----------------------------------------+ - * - * egw_link::get_3links('timesheet','projectmanager',$pm_id) returns the links (c) between the timesheet and the project, - * plus the other app/id in the keys 'app3' and 'id3' - * - * @param string $app app the returned links are linked on one side (atm. this must be link_app1!) - * @param string $target_app app the returned links other side link also to - * @param string|array $target_id =null id(s) the returned links other side link also to - * @param boolean $just_app_ids =false return array with link_id => app_id pairs, not the full link record - * @param string $order ='link_lastmod DESC' defaults to newest links first - * @param int|array $limit =null number of entries to return, default null = all or array(offset, num_rows) to return num_rows starting from offset - * @return array with links from entries from $app to $target_app/$target_id plus the other (b) link_id/app/id in the keys 'link3'/'app3'/'id3' - */ - static function get_3links($app, $target_app, $target_id=null, $just_app_ids=false, $order='link_lastmod DESC', $limit=null) - { - $table = self::TABLE; - $arrayofselects=array( - // retrieve the type of links, where the relation is realized as timesheet->infolog/tracker via infolog->projectmanager to timesheet->projectmanager - array('table'=>self::TABLE, - 'cols'=>'c.*,b.link_app1 AS app3,b.link_id1 AS id3,b.link_id AS link3', - 'where'=>'a.link_app1='.self::$db->quote($app).' AND c.link_app2='.self::$db->quote($target_app). - (!$target_id ? '' : self::$db->expression(self::TABLE,' AND c.',array('link_id2' => $target_id))), - 'join'=>" a - JOIN $table b ON a.link_id2=b.link_id1 AND a.link_app2=b.link_app1 - JOIN $table c ON a.link_id1=c.link_id1 AND a.link_app1=c.link_app1 AND a.link_id!=c.link_id AND c.link_app2=b.link_app2 AND c.link_id2=b.link_id2", - ), - // retrieve the type of links, where the relation is realized as timesheet->infolog/tracker and projectmanager->timesheet - array('table'=>self::TABLE, - 'cols'=>'b.link_id, b.link_app2 as app1, b.link_id2 as id1, b.link_app1 as app2, b.link_id1 as id2, b.link_remark,b.link_lastmod,b.link_owner,b.deleted,c.link_app1 AS app3,c.link_id1 AS id3,c.link_id AS link3', - 'where'=>'a.link_app1='.self::$db->quote($app).' AND b.link_app1='.self::$db->quote($target_app). - (!$target_id ? '' : self::$db->expression(self::TABLE,' AND b.',array('link_id1' => $target_id))), - 'join'=>" a - JOIN $table b ON a.link_id1=b.link_id2 AND a.link_app1=b.link_app2 - JOIN $table c ON a.link_id2=c.link_id1 AND a.link_app2=c.link_app1 AND a.link_id!=c.link_id AND c.link_app2=b.link_app1 AND c.link_id2=b.link_id1", - ), - // retrieve the type of links, where the relation is realized as timesheet->projectmanager and infolog->timesheet - array('table'=>self::TABLE, - 'cols'=>'a.*,c.link_app1 AS app3,c.link_id1 AS id3,c.link_id AS link3', - 'where'=>'a.link_app1='.self::$db->quote($app).' AND a.link_app2='.self::$db->quote($target_app). - (!$target_id ? '' : self::$db->expression(self::TABLE,' AND a.',array('link_id2' => $target_id))), - 'join'=>" a - JOIN $table b ON a.link_id1=b.link_id2 AND a.link_app1=b.link_app2 - JOIN $table c ON a.link_id2=c.link_id2 AND a.link_app2=c.link_app2 AND a.link_id!=c.link_id AND c.link_app1=b.link_app1 AND c.link_id1=b.link_id1", - ), - ); - - $offset = false; - if (is_array($limit)) - { - list($offset, $limit) = $limit; - } - elseif($limit) - { - $offset = 0; - } - - $links = array(); - foreach(self::$db->union($arrayofselects, __LINE__, __FILE__, $order, $offset, $limit) as $row) - { - if ($just_app_ids) - { - if ($row['link_app1'] == $target_app && (is_null($target_id) || in_array($row['link_id1'],(array)$target_id))) - { - $links[$row['link_id']] = $row['link_id2']; - } - else - { - $links[$row['link_id']] = $row['link_id1']; - } - } - else - { - $links[] = egw_db::strip_array_keys($row,'link_'); - } - } - // if query returns exactly limit rows, we assume there are more and therefore set self::$limit_exceeded - self::$limit_exceeded = $offset !== false && count($links) == $limit; - - return $links; - } - - /** - * Initialise our static vars - */ - static function init_static( ) - { - self::$db = $GLOBALS['egw']->db; - } -} -solink::init_static(); +class solink extends Api\Link\Storage {}