From 0afb2d03241438601dfa4104c535aff764d323c9 Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Sat, 10 May 2008 20:15:02 +0000 Subject: [PATCH] pricipals and groups collection for WebDAV Acl, to improve support for Apple iCal, you can now use http://domain.com/egroupware/groupdav.php/pricipals/username as account-url in iCal --- phpgwapi/inc/class.groupdav.inc.php | 44 ++++- phpgwapi/inc/class.groupdav_groups.inc.php | 168 +++++++++++++++++ phpgwapi/inc/class.groupdav_handler.inc.php | 14 +- .../inc/class.groupdav_principals.inc.php | 174 ++++++++++++++++++ 4 files changed, 386 insertions(+), 14 deletions(-) create mode 100644 phpgwapi/inc/class.groupdav_groups.inc.php create mode 100644 phpgwapi/inc/class.groupdav_principals.inc.php diff --git a/phpgwapi/inc/class.groupdav.inc.php b/phpgwapi/inc/class.groupdav.inc.php index b01820e161..47f8e632d1 100644 --- a/phpgwapi/inc/class.groupdav.inc.php +++ b/phpgwapi/inc/class.groupdav.inc.php @@ -137,14 +137,31 @@ class groupdav extends HTTP_WebDAV_Server if (!$app) // root folder containing apps { + // self url $files['files'][] = array( - 'path' => '/', - 'props' => array( - self::mkprop('displayname','eGroupWare'), - self::mkprop('resourcetype','collection'), - // adding the calendar extra property (calendar-home-set, etc.) here, allows apple iCal to "autodetect" the URL + 'path' => '/', + 'props' => array( + self::mkprop('displayname','eGroupWare'), + self::mkprop('resourcetype','collection'), + // adding the calendar extra property (calendar-home-set, etc.) here, allows apple iCal to "autodetect" the URL self::mkprop(groupdav::CALDAV,'calendar-home-set',$_SERVER['SCRIPT_NAME'].'/calendar/'), ), + ); + // principals collection + $files['files'][] = array( + 'path' => '/principals/', + 'props' => array( + self::mkprop('displayname',lang('Accounts')), + self::mkprop('resourcetype','collection'), + ), + ); + // groups collection + $files['files'][] = array( + 'path' => '/groups/', + 'props' => array( + self::mkprop('displayname',lang('Groups')), + self::mkprop('resourcetype','collection'), + ), ); foreach($this->root as $app => $data) @@ -155,7 +172,7 @@ class groupdav extends HTTP_WebDAV_Server $files['files'][] = array( 'path' => '/'.$app.'/', - 'props' => call_user_func('groupdav_'.$app.'::extra_properties',array( + 'props' => call_user_func($app.'_groupdav::extra_properties',array( self::mkprop('displayname',$this->translation->convert(lang($app),$this->egw_charset,'utf-8')), self::mkprop('resourcetype',$this->_resourcetype($app)), )), @@ -163,9 +180,9 @@ class groupdav extends HTTP_WebDAV_Server } return true; } - if (!$GLOBALS['egw_info']['user']['apps'][$app]) + if (!in_array($app,array('principals','groups')) && !$GLOBALS['egw_info']['user']['apps'][$app]) { - error_log(__CLASS__."::$method(path=$options[path]) 403 Forbidden: no app rights"); + error_log(__CLASS__."::$method(path=$options[path]) 403 Forbidden: no app rights for '$app'"); return '403 Forbidden'; // no rights for the given app } if (($handler = groupdav_handler::app_handler($app,$this->debug))) @@ -198,7 +215,7 @@ class groupdav extends HTTP_WebDAV_Server $resourcetype = array( self::mkprop('collection','collection'), ); - if (!$no_extra_types) + if (!$no_extra_types && isset($this->root[$app])) { foreach($this->root[$app] as $ns => $type) { @@ -426,6 +443,13 @@ class groupdav extends HTTP_WebDAV_Server { $parts = explode('/',$path); + if (in_array($parts[1],array('principals','groups'))) + { + $user = $GLOBALS['egw_info']['user']['account_id']; + list(,$app,$id) = $parts; + return true; + } + list($id) = explode('.',array_pop($parts)); // remove evtl. .ics extension $app = array_pop($parts); @@ -438,7 +462,7 @@ class groupdav extends HTTP_WebDAV_Server { $user = $GLOBALS['egw_info']['user']['account_id']; } - if (!($ok = $id && in_array($app,array('addressbook','calendar','infolog')) && $user)) + if (!($ok = $id && in_array($app,array('addressbook','calendar','infolog','principals','groups')) && $user)) { error_log(__METHOD__."('$path') returning false: id=$id, app='$app', user=$user"); } diff --git a/phpgwapi/inc/class.groupdav_groups.inc.php b/phpgwapi/inc/class.groupdav_groups.inc.php new file mode 100644 index 0000000000..5416b6ae29 --- /dev/null +++ b/phpgwapi/inc/class.groupdav_groups.inc.php @@ -0,0 +1,168 @@ + + * @copyright (c) 2008 by Ralf Becker + * @version $Id$ + */ + +/** + * eGroupWare: GroupDAV access: groupdav/caldav/carddav groups handlers + */ +class groupdav_groups extends groupdav_handler +{ + /** + * Reference to the accounts class + * + * @var accounts + */ + var $accounts; + + /** + * Constructor + * + * @param string $app + * @param int $debug=null + */ + function __construct($app,$debug=null) + { + parent::__construct($app,$debug); + + $this->accounts = $GLOBALS['egw']->accounts; + } + + /** + * Handle propfind request for an application folder + * + * @param string $path + * @param array $options + * @param array &$files + * @param int $user account_id + * @return mixed boolean true on success, false on failure or string with http status (eg. '404 Not Found') + */ + function propfind($path,$options,&$files,$user) + { + list(,,$user) = explode('/',$path); + foreach($user ? array($this->accounts->read($user)) : $this->accounts->search(array('type' => 'groups')) as $account) + { + $props = array( + HTTP_WebDAV_Server::mkprop('displayname',lang('Group').' '.$account['account_lid']), + HTTP_WebDAV_Server::mkprop('getetag',$this->get_etag($account)), + HTTP_WebDAV_Server::mkprop('resourcetype','principal'), + HTTP_WebDAV_Server::mkprop('alternate-URI-set',''), + HTTP_WebDAV_Server::mkprop('principal-URL',$_SERVER['SCRIPT_NAME'].'/groups/'.$account['account_lid']), + HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'calendar-home-set',$_SERVER['SCRIPT_NAME'].'/calendar/'), + ); + foreach($this->accounts->members($account['account_id']) as $uid => $user) + { + $props[] = HTTP_WebDAV_Server::mkprop('group-membership',$_SERVER['SCRIPT_NAME'].'/principals/'.$user); + } + $files['files'][] = array( + 'path' => '/groups/'.$account['account_lid'], + 'props' => $props, + ); + } + //error_log(__METHOD__."($path,,,$user) files=".array2string($files['files'])); + return true; + } + + /** + * Handle get request for an applications entry + * + * @param array &$options + * @param int $id + * @return mixed boolean true on success, false on failure or string with http status (eg. '404 Not Found') + */ + function get(&$options,$id) + { + if (!is_array($account = $this->_common_get_put_delete('GET',$options,$id))) + { + return $account; + } + $options['data'] = 'Principal: '.$account['account_lid']. + "\nURL: ".$_SERVER['SCRIPT_NAME'].$options['path']. + "\nName: ".lang('Group').' '.$account['account_lid']. + ($account['account_email'] ? "\nEmail: ".$account['account_email'] : ''). + "\nMembers: ".implode(', ',$this->accounts->members($id))."\n"; + $options['mimetype'] = 'text/plain; charset=utf-8'; + header('Content-Encoding: identity'); + header('ETag: '.$this->get_etag($account)); + return true; + } + + /** + * Handle get request for an applications entry + * + * @param array &$options + * @param int $id + * @param int $user=null account_id of owner, default null + * @return mixed boolean true on success, false on failure or string with http status (eg. '404 Not Found') + */ + function put(&$options,$id,$user=null) + { + return false; + } + + /** + * Handle get request for an applications entry + * + * @param array &$options + * @param int $id + * @return mixed boolean true on success, false on failure or string with http status (eg. '404 Not Found') + */ + function delete(&$options,$id) + { + return false; + } + + /** + * Read an entry + * + * @param string/int $id + * @return array/boolean array with entry, false if no read rights, null if $id does not exist + */ + function read($id) + { + return $this->accounts->read($id); + } + + /** + * Check if user has the neccessary rights on an entry + * + * @param int $acl EGW_ACL_READ, EGW_ACL_EDIT or EGW_ACL_DELETE + * @param array/int $entry entry-array or id + * @return boolean null if entry does not exist, false if no access, true if access permitted + */ + function check_access($acl,$entry) + { + if ($acl != EGW_ACL_READ) + { + return false; + } + if (!is_array($entry) && !$this->accounts->name2id($entry,'account_lid','g')) + { + return null; + } + return true; + } + + /** + * Get the etag for an entry, can be reimplemented for other algorithm or field names + * + * @param array/int $event array with event or cal_id + * @return string/boolean string with etag or false + */ + function get_etag($account) + { + if (!is_array($account)) + { + $account = $this->read($account); + } + return '"'.$account['account_id'].':'.md5(serialize($account)).'"'; + } +} \ No newline at end of file diff --git a/phpgwapi/inc/class.groupdav_handler.inc.php b/phpgwapi/inc/class.groupdav_handler.inc.php index a378bd5749..bec6eeb6fe 100644 --- a/phpgwapi/inc/class.groupdav_handler.inc.php +++ b/phpgwapi/inc/class.groupdav_handler.inc.php @@ -60,6 +60,12 @@ abstract class groupdav_handler */ var $http_if_match; + /** + * Constructor + * + * @param string $app + * @param int $debug=null + */ function __construct($app,$debug=null) { $this->app = $app; @@ -181,10 +187,10 @@ abstract class groupdav_handler */ function _common_get_put_delete($method,&$options,$id) { - if (!$GLOBALS['egw_info']['user']['apps'][$this->app]) + if (!in_array($this->app,array('principals','groups')) && !$GLOBALS['egw_info']['user']['apps'][$this->app]) { - if ($this->debug) error_log(__METHOD__."($method,,$id) 403 Forbidden: no app rights"); - return '403 Forbidden'; // no calendar rights + if ($this->debug) error_log(__METHOD__."($method,,$id) 403 Forbidden: no app rights for '$this->app'"); + return '403 Forbidden'; // no app rights } $extra_acl = $this->method2acl[$method]; if (!($entry = $this->read($id)) && ($method != 'PUT' || $event === false) || @@ -228,7 +234,7 @@ abstract class groupdav_handler if (!array_key_exists($app,$handler_cache)) { $class = $app.'_groupdav'; - if (!class_exists($class)) return null; + if (!class_exists($class) && !class_exists($class = 'groupdav_'.$app)) return null; $handler_cache[$app] = new $class($app); } diff --git a/phpgwapi/inc/class.groupdav_principals.inc.php b/phpgwapi/inc/class.groupdav_principals.inc.php new file mode 100644 index 0000000000..d96c9720bf --- /dev/null +++ b/phpgwapi/inc/class.groupdav_principals.inc.php @@ -0,0 +1,174 @@ + + * @copyright (c) 2008 by Ralf Becker + * @version $Id$ + */ + +/** + * eGroupWare: GroupDAV access: groupdav/caldav/carddav principals handlers + */ +class groupdav_principals extends groupdav_handler +{ + /** + * Reference to the accounts class + * + * @var accounts + */ + var $accounts; + + /** + * Constructor + * + * @param string $app + * @param int $debug=null + */ + function __construct($app,$debug=null) + { + parent::__construct($app,$debug); + + $this->accounts = $GLOBALS['egw']->accounts; + } + + /** + * Handle propfind request for an application folder + * + * @param string $path + * @param array $options + * @param array &$files + * @param int $user account_id + * @return mixed boolean true on success, false on failure or string with http status (eg. '404 Not Found') + */ + function propfind($path,$options,&$files,$user) + { + list(,,$id) = explode('/',$path); + if ($id && !($id = $this->accounts->id2name($id))) + { + return false; + } + foreach($id ? array($this->accounts->read($id)) : $this->accounts->search(array('type' => 'accounts')) as $account) + { + $props = array( + HTTP_WebDAV_Server::mkprop('displayname',trim($account['account_firstname'].' '.$account['account_lastname'])), + HTTP_WebDAV_Server::mkprop('getetag',$this->get_etag($account)), + HTTP_WebDAV_Server::mkprop('resourcetype','principal'), + HTTP_WebDAV_Server::mkprop('alternate-URI-set',''), + HTTP_WebDAV_Server::mkprop('principal-URL',$_SERVER['SCRIPT_NAME'].'/principals/'.$account['account_lid']), + HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'calendar-home-set',$_SERVER['SCRIPT_NAME'].'/'), + HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'calendar-user-address-set','MAILTO:'.$account['account_email']), + ); + foreach($this->accounts->memberships($account['account_id']) as $gid => $group) + { + $props[] = HTTP_WebDAV_Server::mkprop('group-membership',$_SERVER['SCRIPT_NAME'].'/groups/'.$group); + } + $files['files'][] = array( + 'path' => '/principals/'.$account['account_lid'], + 'props' => $props, + ); + error_log(__METHOD__."($path) path=/principals/".$account['account_lid'].', props='.array2string($props)); + } + //error_log(__METHOD__."($path,,,$user) files=".array2string($files['files'])); + return true; + } + + /** + * Handle get request for an applications entry + * + * @param array &$options + * @param int $id + * @return mixed boolean true on success, false on failure or string with http status (eg. '404 Not Found') + */ + function get(&$options,$id) + { + if (!is_array($account = $this->_common_get_put_delete('GET',$options,$id))) + { + return $account; + } + $options['data'] = 'Principal: '.$account['account_lid']. + "\nURL: ".$_SERVER['SCRIPT_NAME'].$options['path']. + "\nName: ".$account['account_firstname'].' '.$account['account_lastname']. + "\nEmail: ".$account['account_email']. + "\nMemberships: ".implode(', ',$this->accounts->memberships($id))."\n"; + $options['mimetype'] = 'text/plain; charset=utf-8'; + header('Content-Encoding: identity'); + header('ETag: '.$this->get_etag($account)); + return true; + } + + /** + * Handle get request for an applications entry + * + * @param array &$options + * @param int $id + * @param int $user=null account_id of owner, default null + * @return mixed boolean true on success, false on failure or string with http status (eg. '404 Not Found') + */ + function put(&$options,$id,$user=null) + { + return false; + } + + /** + * Handle get request for an applications entry + * + * @param array &$options + * @param int $id + * @return mixed boolean true on success, false on failure or string with http status (eg. '404 Not Found') + */ + function delete(&$options,$id) + { + return false; + } + + /** + * Read an entry + * + * @param string/int $id + * @return array/boolean array with entry, false if no read rights, null if $id does not exist + */ + function read($id) + { + return $this->accounts->read($id); + } + + /** + * Check if user has the neccessary rights on an entry + * + * @param int $acl EGW_ACL_READ, EGW_ACL_EDIT or EGW_ACL_DELETE + * @param array/int $entry entry-array or id + * @return boolean null if entry does not exist, false if no access, true if access permitted + */ + function check_access($acl,$entry) + { + if ($acl != EGW_ACL_READ) + { + return false; + } + if (!is_array($entry) && !$this->accounts->name2id($entry,'account_lid','u')) + { + return null; + } + return true; + } + + /** + * Get the etag for an entry, can be reimplemented for other algorithm or field names + * + * @param array/int $event array with event or cal_id + * @return string/boolean string with etag or false + */ + function get_etag($account) + { + if (!is_array($account)) + { + $account = $this->read($account); + } + return '"'.$account['account_id'].':'.md5(serialize($account)).'"'; + } +} \ No newline at end of file