From ee00114a2e401c6426d525fb37fa7152a8e40a26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Lehrke?= Date: Fri, 26 Feb 2010 10:59:30 +0000 Subject: [PATCH] Fix GroupDAV issues --- phpgwapi/inc/class.groupdav.inc.php | 302 ++++++++++++++++---- phpgwapi/inc/class.groupdav_handler.inc.php | 155 +++++++++- 2 files changed, 403 insertions(+), 54 deletions(-) diff --git a/phpgwapi/inc/class.groupdav.inc.php b/phpgwapi/inc/class.groupdav.inc.php index 587f28a119..20a9896cb7 100644 --- a/phpgwapi/inc/class.groupdav.inc.php +++ b/phpgwapi/inc/class.groupdav.inc.php @@ -1,24 +1,36 @@ - * @copyright (c) 2007/8 by Ralf Becker + * @copyright (c) 2007-9 by Ralf Becker * @version $Id$ */ require_once('HTTP/WebDAV/Server.php'); /** - * eGroupWare: GroupDAV access + * EGroupware: GroupDAV access * - * Using the PEAR HTTP/WebDAV/Server class (which need to be installed!) + * Using a modified PEAR HTTP/WebDAV/Server class from egw-pear! + * + * One can use the following url's releative (!) to http://domain.com/egroupware/groupdav.php + * + * - /addressbook/ all addressbooks current user has rights to + * - /calendar/ calendar of current user + * - /infolog/ infologs of current user + * - / base of the above, only certain clients (KDE, Apple) can autodetect folders from there + * - //addressbook/ addressbook of user or group given the user has rights to view it + * - //calendar/ calendar of user given the user has rights to view it + * - //infolog/ InfoLog's of user given the user has rights to view it + * - // base of the above, only certain clients (KDE, Apple) can autodetect folders from there + * + * Calling one of the above collections with a GET request / regular browser generates an automatic index + * from the data of a allprop PROPFIND, allow to browse CalDAV/CardDAV/GroupDAV tree with a regular browser. * * @link http://www.groupdav.org GroupDAV spec */ @@ -36,6 +48,10 @@ class groupdav extends HTTP_WebDAV_Server * CardDAV namespace */ const CARDDAV = 'urn:ietf:params:xml:ns:carddav'; + /** + * Calendarserver namespace (eg. for ctag) + */ + const CALENDARSERVER = 'http://calendarserver.org/ns/'; /** * Realm and powered by string */ @@ -151,7 +167,7 @@ class groupdav extends HTTP_WebDAV_Server if ($this->debug) error_log(__CLASS__."::$method(".array2string($options,true).')'); // parse path in form [/account_lid]/app[/more] - if (!self::_parse_path($options['path'],$id,$app,$user) && $app && !$user) + if (!self::_parse_path($options['path'],$id,$app,$user,$user_prefix) && $app && !$user) { if ($this->debug > 1) error_log(__CLASS__."::$method: user=$user, app=$app, id=$id: 404 not found!"); return '404 Not Found'; @@ -164,40 +180,45 @@ class groupdav extends HTTP_WebDAV_Server { // self url $files['files'][] = array( - 'path' => '/', + 'path' => $user_prefix.'/', 'props' => array( - self::mkprop('displayname','eGroupWare'), + self::mkprop('displayname','EGroupware (Cal|Card|Group)DAV server'), 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',$this->base_uri.'/calendar/'), + self::mkprop('current-user-principal',array(self::mkprop('href',$this->base_uri.'/principals/'.$GLOBALS['egw_info']['user']['account_lid'].'/'))), ), ); if ($options['depth']) { - // 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'), - ), - ); - + if (empty($user_prefix)) + { + // principals collection + $files['files'][] = array( + 'path' => '/principals/', + 'props' => array( + self::mkprop('displayname',lang('Accounts')), + self::mkprop('resourcetype','collection'), + self::mkprop('current-user-principal',array(self::mkprop('href',$this->base_uri.'/principals/'.$GLOBALS['egw_info']['user']['account_lid'].'/'))), + ), + ); + // groups collection + $files['files'][] = array( + 'path' => '/groups/', + 'props' => array( + self::mkprop('displayname',lang('Groups')), + self::mkprop('resourcetype','collection'), + self::mkprop('current-user-principal',array(self::mkprop('href',$this->base_uri.'/principals/'.$GLOBALS['egw_info']['user']['account_lid'].'/'))), + ), + ); + } foreach($this->root as $app => $data) { if (!$GLOBALS['egw_info']['user']['apps'][$app]) continue; // no rights for the given app $files['files'][] = array( - 'path' => '/'.$app.'/', - 'props' => $this->_properties($app), + 'path' => $user_prefix.'/'.$app.'/', + 'props' => $this->_properties($app,false,$user), ); } } @@ -212,7 +233,7 @@ class groupdav extends HTTP_WebDAV_Server { if ($method != 'REPORT' && !$id) // no self URL for REPORT requests (only PROPFIND) or propfinds on an id { - $files['files'][] = array( + $files['files'][0] = array( 'path' => '/'.$app.'/', // KAddressbook doubles the folder, if the self URL contains the GroupDAV/CalDAV resourcetypes 'props' => $this->_properties($app,$app=='addressbook'&&strpos($_SERVER['HTTP_USER_AGENT'],'KHTML') !== false), @@ -220,6 +241,12 @@ class groupdav extends HTTP_WebDAV_Server } if (!$options['depth'] && !$id) { + // add ctag if handler implements it (only for depth 0) + if (method_exists($handler,'getctag')) + { + $files['files'][0]['props'][] = HTTP_WebDAV_Server::mkprop( + groupdav::CALENDARSERVER,'getctag',$handler->getctag($options['path'],$user)); + } return true; // depth 0 --> show only the self url } return $handler->propfind($options['path'],$options,$files,$user,$id); @@ -232,25 +259,29 @@ class groupdav extends HTTP_WebDAV_Server * * @param string $app * @param boolean $no_extra_types=false should the GroupDAV and CalDAV types be added (KAddressbook has problems with it in self URL) + * @param int $user=null owner of the collection, default current user * @return array of DAV properties */ - function _properties($app,$no_extra_types=false) + function _properties($app,$no_extra_types=false,$user=null) { + if (!$user) $user = $GLOBALS['egw_info']['user']['account_fullname']; + $props = array( - self::mkprop('displayname',$this->translation->convert(lang($app),$this->egw_charset,'utf-8')), + self::mkprop('displayname',$this->translation->convert(lang($app).' '.common::grab_owner_name($user),$this->egw_charset,'utf-8')), + self::mkprop('current-user-principal',array(self::mkprop('href',$this->base_uri.'/principals/'.$GLOBALS['egw_info']['user']['account_lid'].'/'))), ); - foreach($this->root[$app] as $prop => $values) + foreach((array)$this->root[$app] as $prop => $values) { if ($prop == 'resourcetype') { $resourcetype = array( - self::mkprop('collection','collection'), + self::mkprop('collection',''), ); if (!$no_extra_types) { foreach($this->root[$app]['resourcetype'] as $ns => $type) { - $resourcetype[] = self::mkprop($ns,'resourcetype', $type); + $resourcetype[] = self::mkprop($ns,$type,''); } } $props[] = self::mkprop('resourcetype',$resourcetype); @@ -299,24 +330,182 @@ class groupdav extends HTTP_WebDAV_Server /** * GET method handler * - * @param array parameter passing array + * @param array $options parameter passing array * @return bool true on success */ function GET(&$options) { if ($this->debug) error_log(__METHOD__.'('.array2string($options).')'); - if (!$this->_parse_path($options['path'],$id,$app,$user)) + if (!$this->_parse_path($options['path'],$id,$app,$user) || $app == 'principals') { + return $this->autoindex($options); + + error_log(__METHOD__."(".array2string($options).") 404 Not Found"); return '404 Not Found'; } if (($handler = self::app_handler($app))) { return $handler->get($options,$id); } + error_log(__METHOD__."(".array2string($options).") 501 Not Implemented"); return '501 Not Implemented'; } + /** + * Display an automatic index (listing and properties) for a collection + * + * @param array $options parameter passing array, index "path" contains requested path + */ + protected function autoindex($options) + { + $propfind_options = array( + 'path' => $options['path'], + 'depth' => 1, + ); + $files = array(); + if (($ret = $this->PROPFIND($propfind_options,$files)) !== true) + { + return $ret; // no collection + } + header('Content-type: text/html; charset='.$GLOBALS['egw']->translation->charset()); + echo "\n\n\t".'EGroupware (Cal|Card|Group)DAV server '.htmlspecialchars($options['path'])."\n"; + echo "\t\n"; + echo "\t\n"; + echo "\n\n"; + + echo '

(Cal|Card|Group)DAV '; + $path = '/groupdav.php'; + foreach(explode('/',$this->_unslashify($options['path'])) as $n => $name) + { + $path .= ($n != 1 ? '/' : '').$name; + echo html::a_href(htmlspecialchars($name.'/'),$path); + } + echo "

\n"; + + $n = 0; + foreach($files['files'] as $file) + { + if (!isset($collection_props)) + { + $collection_props = $this->props2array($file['props']); + echo '

'.lang('Collection listing').': '.htmlspecialchars($collection_props['DAV:displayname'])."

\n"; + continue; // own entry --> displaying properies later + } + if(!$n++) + { + echo "\n\t\n"; + } + $props = $this->props2array($file['props']); + //echo $file['path']; _debug_array($props); + $class = $class == 'row_on' ? 'row_off' : 'row_on'; + $name = $this->_slashify(basename($this->_unslashify($file['path']))); + /* + if (substr($file['path'],-1) == '/') + { + $name = basename(substr($file['path'],0,-1)).'/'; + } + else + { + $name = basename($file['path']); + } + */ + echo "\t\n\t\t\n\t\t\n"; + echo "\t\t\n"; + echo "\t\t\n"; + echo "\t\t\n"; + echo "\t\t\n"; + echo "\t\t\n\t\n"; + } + if (!$n) + { + echo '

'.lang('Collection empty.')."

\n"; + } + else + { + echo "
#".lang('Name')."".lang('Size')."".lang('Last modified')."". + lang('ETag')."".lang('Content type')."".lang('Resource type')."
$n".html::a_href(htmlspecialchars($name),'/groupdav.php'.$file['path'])."".$props['DAV:getcontentlength']."".(!empty($props['DAV:getlastmodified']) ? date('Y-m-d H:i:s',$props['DAV:getlastmodified']) : '')."".$props['DAV:getetag']."".htmlspecialchars($props['DAV:getcontenttype'])."".self::prop_value($props['DAV:resourcetype'])."
\n"; + } + echo '

'.lang('Properties')."

\n"; + echo "\n\t\n"; + foreach($collection_props as $name => $value) + { + $class = $class == 'row_on' ? 'row_off' : 'row_on'; + $ns = explode(':',$name); + $name = array_pop($ns); + $ns = implode(':',$ns); + echo "\t\n\t\t\n"; + echo "\t\t\n\t\n"; + } + echo "
".lang('Namespace')."".lang('Name')."".lang('Value')."
".htmlspecialchars($ns)."".htmlspecialchars($name)."".self::prop_value($value)."
\n"; + + echo "\n\n"; + + common::egw_exit(); + } + + /** + * Format a property value for output + * + * @param mixed $value + * @return string + */ + protected function prop_value($value) + { + if (is_array($value)) + { + if (isset($value[0]['ns'])) + { + $value = $this->_hierarchical_prop_encode($value); + } + $value = htmlspecialchars(array2string($value)); + } + elseif (preg_match('/^https?:\/\//',$value)) + { + $value = html::a_href($value,$value); + } + else + { + $value = htmlspecialchars($value); + } + return $value; + } + + /** + * Return numeric indexed array with values for keys 'ns', 'name' and 'val' as array 'ns:name' => 'val' + * + * @param array $props + * @return array + */ + protected function props2array(array $props) + { + $arr = array(); + foreach($props as $prop) + { + switch($prop['ns']) + { + case 'DAV:'; + $ns = 'DAV'; + break; + case self::CALDAV: + $ns = 'CalDAV'; + break; + case self::CARDDAV: + $ns = 'CardDAV'; + break; + case self::GROUPDAV: + $ns = 'GroupDAV'; + break; + default: + $ns = $prop['ns']; + } + $arr[$ns.':'.$prop['name']] = is_array($prop['val']) ? + $this->_hierarchical_prop_encode($prop['val']) : $prop['val']; + } + return $arr; + } + /** * PUT method handler * @@ -482,34 +671,51 @@ class groupdav extends HTTP_WebDAV_Server * @param int &$id * @param string &$app addressbook, calendar, infolog (=infolog) * @param int &$user + * @param string &$user_prefix=null * @return boolean true on success, false on error */ - function _parse_path($path,&$id,&$app,&$user) + function _parse_path($path,&$id,&$app,&$user,&$user_prefix=null) { - $parts = explode('/',$path); - - if (in_array($parts[1],array('principals','groups'))) + if ($this->debug) { - $user = $GLOBALS['egw_info']['user']['account_id']; - list(,$app,$id) = $parts; - return true; + error_log(__METHOD__." called with ('$path') id=$id, app='$app', user=$user"); + } + if ($path[0] == '/') + { + $path = substr($path, 1); + } + $parts = explode('/', $this->_unslashify($path)); + + if ($GLOBALS['egw']->accounts->name2id($parts[0])) + { + // /$user/$app/... + $user = array_shift($parts); } - list($id) = explode('.',array_pop($parts)); // remove evtl. .ics extension + $app = array_shift($parts); - $app = array_pop($parts); - - if (($user = array_pop($parts))) + if ($user) { + $user_prefix = '/'.$user; $user = $GLOBALS['egw']->accounts->name2id($user,'account_lid',$app != 'addressbook' ? 'u' : null); } else { + $user_prefix = ''; $user = $GLOBALS['egw_info']['user']['account_id']; } + + if (($id = array_pop($parts))) + { + list($id) = explode('.',$id); // remove evtl. .ics extension + } + if (!($ok = $id && in_array($app,array('addressbook','calendar','infolog','principals','groups')) && $user)) { - if ($this->debug) error_log(__METHOD__."('$path') returning false: id=$id, app='$app', user=$user"); + if ($this->debug) + { + error_log(__METHOD__."('$path') returning false: id=$id, app='$app', user=$user"); + } } return $ok; } diff --git a/phpgwapi/inc/class.groupdav_handler.inc.php b/phpgwapi/inc/class.groupdav_handler.inc.php index bfe0efe3e8..1bbadffd79 100644 --- a/phpgwapi/inc/class.groupdav_handler.inc.php +++ b/phpgwapi/inc/class.groupdav_handler.inc.php @@ -1,18 +1,18 @@ - * @copyright (c) 2007/8 by Ralf Becker + * @copyright (c) 2007-9 by Ralf Becker * @version $Id$ */ /** - * eGroupWare: GroupDAV access: abstract baseclass for groupdav/caldav/carddav handlers + * EGroupware: GroupDAV access: abstract baseclass for groupdav/caldav/carddav handlers */ abstract class groupdav_handler { @@ -81,8 +81,9 @@ abstract class groupdav_handler */ function __construct($app,$debug=null,$base_uri=null) { + //error_log(__METHOD__." called"); $this->app = $app; - if (!is_null($debug)) $this->debug = $debug; + #if (!is_null($debug)) $this->debug = $debug = 3; $this->base_uri = is_null($base_uri) ? $base_uri : $_SERVER['SCRIPT_NAME']; $this->agent = self::get_agent(); @@ -101,6 +102,16 @@ abstract class groupdav_handler */ abstract function propfind($path,$options,&$files,$user); + /** + * Propfind callback, if interator is used + * + * @param array $filter + * @param array|boolean $start false=return all or array(start,num) + * @param int &$total + * @return array with "files" array with values for keys path and props + */ + function &propfind_callback(array $filter,$start,&$total) { } + /** * Handle get request for an applications entry * @@ -171,7 +182,7 @@ abstract class groupdav_handler } if (!is_array($entry) || !isset($entry['id']) || !(isset($entry['modified']) || isset($entry['etag']))) { - error_log(__METHOD__."(".array2string($entry).") Cant create etag!"); + // error_log(__METHOD__."(".array2string($entry).") Cant create etag!"); return false; } return '"'.$entry['id'].':'.(isset($entry['etag']) ? $entry['etag'] : $entry['modified']).'"'; @@ -303,4 +314,136 @@ abstract class groupdav_handler } return $agent; } -} \ No newline at end of file +} + +/** + * Iterator for propfinds using propfind callback of a groupdav_handler to query results in chunks + * + * The propfind method just computes a filter and then returns an instance of this iterator instead of the files: + * + * function propfind($path,$options,&$files,$user,$id='') + * { + * $filter = array(); + * // compute filter from path, options, ... + * + * $files['files'] = new groupdav_propfind_iterator($this,$filter,$files['files']); + * + * return true; + * } + */ +class groupdav_propfind_iterator implements Iterator +{ + /** + * Handler to call for entries + * + * @var groupdav_handler + */ + protected $handler; + + /** + * Filter of propfind call + * + * @var array + */ + protected $filter; + + /** + * Extra responses to return too + * + * @var array + */ + protected $files; + + /** + * Start value for callback + * + * @var int + */ + protected $start=0; + + /** + * Number of entries queried from callback in one call + * + */ + const CHUNK_SIZE = 500; + + /** + * Constructor + * + * @param groupdav_handler $handler + * @param array $filter filter for propfind call + * @param array $files=null extra files/responses to return too + */ + public function __construct(groupdav_handler $handler,array $filter,array &$files=null) + { + $this->handler = $handler; + $this->filter = $filter; + $this->files = $files; + reset($this->files); + } + + /** + * Return the current element + * + * @return array + */ + public function current() + { + return current($this->files); + } + + /** + * Return the key of the current element + * + * @return int|string + */ + public function key() + { + $current = $this->current(); + + return $current['path']; // we return path as key + } + + /** + * Move forward to next element (called after each foreach loop) + */ + public function next() + { + if (next($this->files) !== false) + { + return true; + } + if (!$this->handler) + { + return false; // no further entries + } + // try query further files via propfind callback of handler and store result in $this->files + $this->files = $this->handler->propfind_callback($this->filter,array($this->start,self::CHUNK_SIZE)); + $this->start += self::CHUNK_SIZE; + reset($this->files); + + if (count($this->files) < self::CHUNK_SIZE) // less entries then asked --> no further available + { + unset($this->handler); + } + return current($this->files) !== false; + } + + /** + * Rewind the Iterator to the first element (called at beginning of foreach loop) + */ + public function rewind() + { + + } + + /** + * Checks if current position is valid + * + * @return boolean + */ + public function valid () + { + return current($this->files) !== false; + } +}