* @copyright (c) 2007-11 by Ralf Becker * @version $Id$ */ require_once('HTTP/WebDAV/Server.php'); /** * EGroupware: GroupDAV access * * 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. * * @todo All principal urls should either contain no account_lid (eg. base64 of it) or use urlencode($account_lid) * @link http://www.groupdav.org GroupDAV spec */ class groupdav extends HTTP_WebDAV_Server { /** * DAV namespace */ const DAV = 'DAV:'; /** * GroupDAV namespace */ const GROUPDAV = 'http://groupdav.org/'; /** * CalDAV namespace */ const CALDAV = 'urn:ietf:params:xml:ns:caldav'; /** * CardDAV namespace */ const CARDDAV = 'urn:ietf:params:xml:ns:carddav'; /** * Apple Calendarserver namespace (eg. for ctag) */ const CALENDARSERVER = 'http://calendarserver.org/ns/'; /** * Apple Addressbookserver namespace (eg. for ctag) */ const ADDRESSBOOKSERVER = 'http://addressbookserver.org/ns/'; /** * Apple iCal namespace (eg. for calendar color) */ const ICAL = 'http://apple.com/ns/ical/'; /** * Realm and powered by string */ const REALM = 'EGroupware CalDAV/CardDAV/GroupDAV server'; var $dav_powered_by = self::REALM; var $http_auth_realm = self::REALM; var $root = array( 'calendar' => array( 'resourcetype' => array(self::GROUPDAV => 'vevent-collection', self::CALDAV => 'calendar'), 'component-set' => array(self::GROUPDAV => 'VEVENT'), ), 'addressbook' => array( 'resourcetype' => array(self::GROUPDAV => 'vcard-collection', self::CARDDAV => 'addressbook'), 'component-set' => array(self::GROUPDAV => 'VCARD'), ), 'infolog' => array( 'resourcetype' => array(self::GROUPDAV => 'vtodo-collection', self::CALDAV => 'calendar'), 'component-set' => array(self::GROUPDAV => 'VTODO'), ), 'principals' => array( 'resourcetype' => array(self::DAV => 'collection'), ) ); /** * Debug level: 0 = nothing, 1 = function calls, 2 = more info, 3 = complete $_SERVER array * * Can now be enabled on a per user basis in GroupDAV prefs, if it is set here to 0! * * The debug messages are send to the apache error_log * * @var integer */ var $debug = 0; /** * eGW's charset * * @var string */ var $egw_charset; /** * Instance of our application specific handler * * @var groupdav_handler */ var $handler; /** * current-user-principal URL * * @var string */ var $current_user_principal; /** * Reference to the accounts class * * @var accounts */ var $accounts; function __construct() { if (!$this->debug) $this->debug = (int)$GLOBALS['egw_info']['user']['preferences']['groupdav']['debug_level']; if ($this->debug > 2) error_log('groupdav: $_SERVER='.array2string($_SERVER)); // identify clients, which do NOT support path AND full url in of PROPFIND request // crrnd: client refuses redundand namespace declarations // cnrnd: client needs redundand namespace declarations switch(($agent = groupdav_handler::get_agent())) { case 'kde': // KAddressbook (at least in 3.5 can NOT subscribe / does NOT find addressbook) $this->client_require_href_as_url = true; $this->cnrnd = true; // Akonadi seems to require redundant namespaces, see KDE bug #265096 https://bugs.kde.org/show_bug.cgi?id=265096 break; case 'cfnetwork': // Apple addressbook app case 'dataaccess': // iPhone addressbook $this->client_require_href_as_url = false; $this->cnrnd = true; break; case 'davkit': // iCal app in OS X 10.6 created wrong request, if full url given case 'coredav': // iCal app in OS X 10.7 $this->client_require_href_as_url = false; $this->cnrnd = true; break; case 'cfnetwork_old': $this->crrnd = true; // Older Apple Addressbook.app does not cope with namespace redundancy break; case 'neon': $this->cnrnd = true; // neon clients like cadaver break; } if ($this->debug) error_log(__METHOD__."() HTTP_USER_AGENT='$_SERVER[HTTP_USER_AGENT]' --> '$agent' --> client_requires_href_as_url=$this->client_require_href_as_url, crrnd(client refuses redundand namespace declarations)=$this->crrnd, cnrnd(client needs redundand namespace declarations)=$this->cnrnd"); // adding EGroupware version to X-Dav-Powered-By header eg. "EGroupware 1.8.001 CalDAV/CardDAV/GroupDAV server" $this->dav_powered_by = str_replace('EGroupware','EGroupware '.$GLOBALS['egw_info']['server']['versions']['phpgwapi'], $this->dav_powered_by); parent::HTTP_WebDAV_Server(); $this->egw_charset = translation::charset(); if (strpos($this->base_uri, 'http') === 0) { $this->current_user_principal = $this->_slashify($this->base_uri); } else { $this->current_user_principal = (@$_SERVER["HTTPS"] === "on" ? "https:" : "http:") . '//' . $_SERVER['HTTP_HOST'] . $_SERVER['SCRIPT_NAME'] . '/'; } $this->current_user_principal .= 'principals/users/'.$GLOBALS['egw_info']['user']['account_lid'].'/'; // if client requires pathes instead of URLs if (!$this->client_require_href_as_url) { $this->current_user_principal = parse_url($this->current_user_principal,PHP_URL_PATH); } $this->accounts = $GLOBALS['egw']->accounts; } /** * get the handler for $app * * @param string $app * @return groupdav_handler */ function app_handler($app) { return groupdav_handler::app_handler($app,$this); } /** * OPTIONS request, allow to modify the standard responses from the pear-class * * @param string $path * @param array &$dav * @param array &$allow */ function OPTIONS($path, &$dav, &$allow) { list(,$app) = explode('/',$path); switch($app) { case 'calendar': if (!in_array(2,$dav)) $dav[] = 2; $dav[] = 'access-control'; $dav[] = 'calendar-access'; //$dav[] = 'calendar-schedule'; $dav[] = 'calendar-proxy'; //$dav[] = 'calendar-avialibility'; //$dav[] = 'calendarserver-private-events'; break; case 'addressbook': if (!in_array(2,$dav)) $dav[] = 2; //$dav[] = 3; // revision aka versioning support not implemented $dav[] = 'access-control'; $dav[] = 'addressbook'; // CardDAV uses "addressbook" NOT "addressbook-access" break; default: if (!in_array(2,$dav)) $dav[] = 2; $dav[] = 'access-control'; $dav[] = 'calendar-access'; $dav[] = 'addressbook'; } // not yet implemented: $dav[] = 'access-control'; } /** * Add a collection to a PROPFIND request * * @param string $path * @param array $props=array() extra properties 'resourcetype' is added anyway, name => value pairs or name => HTTP_WebDAV_Server([namespace,]name,value) * @return array with values for keys 'path' and 'props' */ public function add_collection($path, array $props = array()) { // resourcetype: collection $props['resourcetype'][] = self::mkprop('collection',''); // props for all collections: current-user-principal and principal-collection-set $props['current-user-principal'] = array( self::mkprop('href',$this->current_user_principal)); $props['principal-collection-set'] = array( self::mkprop('href',$this->base_uri.'/principals/')); // required props per WebDAV standard foreach(array( 'displayname' => basename($path), 'getetag' => 'EGw-no-etag-wGE', 'getcontentlength' => '', 'getlastmodified' => '', 'getcontenttype' => 'httpd/unix-directory', ) as $name => $default) { if (!isset($props[$name])) $props[$name] = $default; } if ($this->debug > 1) error_log(__METHOD__."(path='$path', props=".array2string($props).')'); // convert simple associative properties to HTTP_WebDAV_Server ones foreach($props as $name => &$prop) { if (!is_array($prop) || !isset($prop['name'])) { $prop = self::mkprop($name, $prop); } } return array( 'path' => $path, 'props' => $props, ); } /** * PROPFIND and REPORT method handler * * @param array general parameter passing array * @param array return array for file properties * @return bool true on success */ function PROPFIND(&$options, &$files, $method='PROPFIND') { 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,$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'; } if ($this->debug > 1) error_log(__CLASS__."::$method: user='$user', app='$app', id='$id'"); if ($user) { $account_lid = $this->accounts->id2name($user); } else { $account_lid = $GLOBALS['egw_info']['user']['account_lid']; } $account = $this->accounts->read($account_lid); $files = array('files' => array()); $path = $user_prefix = $this->_slashify($user_prefix); if (!$app) // user root folder containing apps { if (empty($user_prefix)) { $user_prefix = '/'; //.$GLOBALS['egw_info']['user']['account_lid'].'/'; } $calendar_user_address_set = array( self::mkprop('href','urn:uuid:'.$account['account_lid']), ); if ($user < 0) { $principalType = 'groups'; $displayname = lang('Group').' '.$account['account_lid']; } else { $principalType = 'users'; $displayname = $account['account_fullname']; $calendar_user_address_set[] = self::mkprop('href','MAILTO:'.$account['account_email']); } $calendar_user_address_set[] = self::mkprop('href',$this->base_uri.'/principals/'.$principalType.'/'.$account['account_lid'].'/'); if ($options['depth'] && $user_prefix == '/') { $displayname = 'EGroupware (Cal|Card|Group)DAV server'; } $displayname = translation::convert($displayname, translation::charset(),'utf-8'); // self url $files['files'][] = $this->add_collection($path, array( 'displayname' => $displayname, 'owner' => array(self::mkprop('href',$this->base_uri.'/principals/'.$principalType.'/'.$account_lid.'/')), // adding the calendar extra property (calendar-home-set, etc.) here, allows apple iCal to "autodetect" the URL 'calendar-home-set' => self::mkprop(groupdav::CALDAV,'calendar-home-set',array( self::mkprop('href',$this->base_uri.$user_prefix))), 'addressbook-home-set' => self::mkprop(groupdav::CARDDAV,'addressbook-home-set',array( self::mkprop('href',$this->base_uri.$user_prefix))), 'calendar-user-address-set' => self::mkprop(groupdav::CALDAV,'calendar-user-address-set',$calendar_user_address_set), 'email-address-set' => self::mkprop(groupdav::CALENDARSERVER,'email-address-set',array( self::mkprop(groupdav::CALENDARSERVER,'email-address',$GLOBALS['egw_info']['user']['email']))), // OUTBOX URLs of the current user 'schedule-outbox-URL' => self::mkprop(groupdav::CALDAV,'schedule-outbox-URL',array( self::mkprop(groupdav::DAV,'href',$this->base_uri.'/calendar/'))), //'current-user-privilege-set' => self::current_user_privilege_set(), )); if ($options['depth']) { if (strlen($path) == 1) // GroupDAV Root { // principals collection $files['files'][] = $this->add_collection('/principals/', array( 'displayname' => lang('Accounts'), )); } foreach($this->root as $app => $data) { if (!$GLOBALS['egw_info']['user']['apps'][$app]) continue; // no rights for the given app $props = $this->_properties($app,false,$user,$path); // add ctag if handler implements it if (($handler = self::app_handler($app)) && method_exists($handler,'getctag')) { $props['getctag'] = self::mkprop( groupdav::CALENDARSERVER,'getctag',$handler->getctag($options['path'],$user)); } $props['getetag'] = 'EGw-'.$app.'-wGE'; $files['files'][] = $this->add_collection($path.$app.'/',$props); } } return true; } if ($app != 'principals' && !isset($GLOBALS['egw_info']['user']['apps'][$app])) { if ($this->debug) error_log(__CLASS__."::$method(path=$options[path]) 403 Forbidden: no app rights for '$app'"); return "403 Forbidden: no app rights for '$app'"; // no rights for the given app } if (($handler = self::app_handler($app))) { if ($method != 'REPORT' && !$id) // no self URL for REPORT requests (only PROPFIND) or propfinds on an id { $files['files'][0] = $this->add_collection($path.$app.'/', // KAddressbook doubles the folder, if the self URL contains the GroupDAV/CalDAV resourcetypes $this->_properties($app,$app=='addressbook'&&$handler->get_agent()=='kde',$user,$path)); // add ctag if handler implements it (only for depth 0) if (method_exists($handler,'getctag')) { $files['files'][0]['props']['getctag'] = self::mkprop( groupdav::CALENDARSERVER,'getctag',$handler->getctag($options['path'],$user)); } if (!$options['depth']) return true; // depth 0 --> show only the self url } return $handler->propfind($this->_slashify($options['path']),$options,$files,$user,$id); } return '501 Not Implemented'; } /** * Get properties of an application collection * * @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 * @param string $path='/' * @return array of DAV properties */ function _properties($app,$no_extra_types=false,$user=null,$path='/') { if ($this->debug) error_log(__METHOD__."(app='$app', no_extra_types=$no_extra_types, user='$user', path='$path')"); $user_preferences = $GLOBALS['egw_info']['user']['preferences']; if ($user) { $account_lid = $this->accounts->id2name($user); if ($user >= 0 && $GLOBALS['egw']->preferences->account_id != $user) { $GLOBALS['egw']->preferences->__construct($user); $user_preferences = $GLOBALS['egw']->preferences->read_repository(); $GLOBALS['egw']->preferences->__construct($GLOBALS['egw_info']['user']['account_lid']); } } else { $account_lid = $GLOBALS['egw_info']['user']['account_lid']; } if (strlen($user_preferences['calendar']['display_color']) == 9 && $user_preferences['calendar']['display_color'][0] == '#') { $display_color = $user_preferences['calendar']['display_color']; } else { $display_color = '#0040A0FF'; } $account = $this->accounts->read($account_lid); $displayname = $GLOBALS['egw']->translation->convert($account['account_fullname'], $GLOBALS['egw']->translation->charset(),'utf-8'); if ($user < 0) { $principalType = 'groups'; } else { $principalType = 'users'; } $props = array( 'owner' => array(self::mkprop('href',$this->base_uri.'/principals/'.$principalType.'/'.$account_lid.'/')), 'calendar-user-address-set' => self::mkprop(groupdav::CALDAV,'calendar-user-address-set',array( self::mkprop('href','MAILTO:'.$GLOBALS['egw_info']['user']['email']), self::mkprop('href',$this->base_uri.'/principals/'.$principalType.'/'.$GLOBALS['egw_info']['user']['account_lid'].'/'), self::mkprop('href','urn:uuid:'.$GLOBALS['egw_info']['user']['account_lid']))), 'email-address-set' => self::mkprop(groupdav::CALENDARSERVER,'email-address-set',array( self::mkprop(groupdav::CALENDARSERVER,'email-address',$GLOBALS['egw_info']['user']['email']))), ); $displayname = translation::convert(lang($app).' '. common::grab_owner_name($user),$this->egw_charset,'utf-8'); switch ($app) { case 'calendar': $props['calendar-home-set'] = self::mkprop(groupdav::CALDAV,'calendar-home-set',array( self::mkprop('href',$this->base_uri.$path.'calendar/'))); // OUTBOX URLs of the current user $props['schedule-outbox-URL'] = self::mkprop(groupdav::CALDAV,'schedule-outbox-URL', array(self::mkprop(groupdav::DAV,'href',$this->base_uri.'/calendar/'))); $props['calendar-color'] = self::mkprop(groupdav::ICAL,'calendar-color',$display_color); break; case 'infolog': $props['calendar-home-set'] = self::mkprop(groupdav::CALDAV,'calendar-home-set',array( self::mkprop('href',$this->base_uri.$path.'infolog/'))); break; default: $props['calendar-home-set'] = self::mkprop(groupdav::CALDAV,'calendar-home-set',array( self::mkprop('href',$this->base_uri.$path))); $displayname = translation::convert(lang($app).' '. common::grab_owner_name($user),$this->egw_charset,'utf-8'); } $props['addressbook-home-set'] = self::mkprop(groupdav::CARDDAV,'addressbook-home-set',array( self::mkprop('href',$this->base_uri.$path))); $props['displayname'] = $displayname; foreach((array)$this->root[$app] as $prop => $values) { if ($prop == 'resourcetype') { if (!$no_extra_types) { foreach($this->root[$app]['resourcetype'] as $ns => $type) { $props['resourcetype'][] = self::mkprop($ns,$type,''); } } } else { foreach($values as $ns => $value) { $props[$prop] = self::mkprop($ns,$prop,$value); } } } if (method_exists($app.'_groupdav','extra_properties')) { $displayname = translation::convert( $account['account_id'] > 0 ? $account['account_fullname'] : lang('Group').' '.$account['account_lid'], translation::charset(),'utf-8'); $props = ExecMethod2($app.'_groupdav::extra_properties',$props,$displayname,$this->base_uri); } return $props; } /** * CalDAV/CardDAV REPORT method handler * * just calls PROPFIND() * * @param array general parameter passing array * @param array return array for file properties * @return bool true on success */ function REPORT(&$options, &$files) { if ($this->debug > 1) error_log(__METHOD__.'('.array2string($options).')'); return $this->PROPFIND($options,$files,'REPORT'); } /** * CalDAV/CardDAV REPORT method handler to get HTTP_WebDAV_Server to process REPORT requests * * Just calls http_PROPFIND() */ function http_REPORT() { parent::http_PROPFIND('REPORT'); } /** * GET method handler * * @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) || $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,$user); } 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='.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'; 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('/\<(D:)?href\>[^<]+\<\/(D:)?href\>/i',$value)) { $value = preg_replace('/\<(D:)?href\>([^<]+)\<\/(D:)?href\>/i','<\\1href>\\2</\\3href>
',$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']; } $ns_defs = ''; $ns_hash = array($prop['ns'] => $ns, 'DAV:' => 'D'); if (is_array($prop['val'])) { $prop['val'] = $this->_hierarchical_prop_encode($prop['val'], $prop['ns'], $ns_defs, $ns_hash); // hack to show real namespaces instead of not (visibly) defined shortcuts unset($ns_hash['DAV:']); $prop['val'] = strtr($prop['val'],array_flip($ns_hash)); } $arr[$ns.':'.$prop['name']] = $prop['val']; } return $arr; } /** * POST method handler * * @param array parameter passing array * @return bool true on success */ function POST(&$options) { // read the content in a string, if a stream is given if (isset($options['stream'])) { $options['content'] = ''; while(!feof($options['stream'])) { $options['content'] .= fread($options['stream'],8192); } } if ($this->debug) error_log(__METHOD__.'('.array2string($options).')'); $this->_parse_path($options['path'],$id,$app,$user); if (($handler = self::app_handler($app)) && method_exists($handler, 'post')) { return $handler->post($options,$id,$user); } return '501 Not Implemented'; } /** * PUT method handler * * @param array parameter passing array * @return bool true on success */ function PUT(&$options) { // read the content in a string, if a stream is given if (isset($options['stream'])) { $options['content'] = ''; while(!feof($options['stream'])) { $options['content'] .= fread($options['stream'],8192); } } if ($this->debug) error_log(__METHOD__.'('.array2string($options).')'); if (!$this->_parse_path($options['path'],$id,$app,$user,$prefix)) { return '404 Not Found'; } if (($handler = self::app_handler($app))) { $status = $handler->put($options,$id,$user,$prefix); // set default stati: true --> 204 No Content, false --> should be already handled if (is_bool($status)) $status = $status ? '204 No Content' : '400 Something went wrong'; return $status; } return '501 Not Implemented'; } /** * DELETE method handler * * @param array general parameter passing array * @return bool true on success */ function DELETE($options) { if ($this->debug) error_log(__METHOD__.'('.array2string($options).')'); if (!$this->_parse_path($options['path'],$id,$app,$user)) { return '404 Not Found'; } if (($handler = self::app_handler($app))) { $status = $handler->delete($options,$id); // set default stati: true --> 204 No Content, false --> should be already handled if (is_bool($status)) $status = $status ? '204 No Content' : '400 Something went wrong'; return $status; } return '501 Not Implemented'; } /** * MKCOL method handler * * @param array general parameter passing array * @return bool true on success */ function MKCOL($options) { if ($this->debug) error_log(__METHOD__.'('.array2string($options).')'); return '501 Not Implemented'; } /** * MOVE method handler * * @param array general parameter passing array * @return bool true on success */ function MOVE($options) { if ($this->debug) error_log(__METHOD__.'('.array2string($options).')'); return '501 Not Implemented'; } /** * COPY method handler * * @param array general parameter passing array * @return bool true on success */ function COPY($options, $del=false) { if ($this->debug) error_log('groupdav::'.($del ? 'MOVE' : 'COPY').'('.array2string($options).')'); return '501 Not Implemented'; } /** * LOCK method handler * * @param array general parameter passing array * @return bool true on success */ function LOCK(&$options) { self::_parse_path($options['path'],$id,$app,$user); $path = egw_vfs::app_entry_lock_path($app,$id); if ($this->debug) error_log(__METHOD__.'('.array2string($options).") path=$path"); // get the app handler, to check if the user has edit access to the entry (required to make locks) $handler = self::app_handler($app); // TODO recursive locks on directories not supported yet if (!$id || !empty($options['depth']) || !$handler->check_access(EGW_ACL_EDIT,$id)) { return '409 Conflict'; } $options['timeout'] = time()+300; // 5min. hardcoded // dont know why, but HTTP_WebDAV_Server passes the owner in D:href tags, which get's passed unchanged to checkLock/PROPFIND // that's wrong according to the standard and cadaver does not show it on discover --> strip_tags removes eventual tags if (($ret = egw_vfs::lock($path,$options['locktoken'],$options['timeout'],strip_tags($options['owner']), $options['scope'],$options['type'],isset($options['update']),false)) && !isset($options['update'])) // false = no ACL check { return $ret ? '200 OK' : '409 Conflict'; } return $ret; } /** * UNLOCK method handler * * @param array general parameter passing array * @return bool true on success */ function UNLOCK(&$options) { self::_parse_path($options['path'],$id,$app,$user); $path = egw_vfs::app_entry_lock_path($app,$id); if ($this->debug) error_log(__METHOD__.'('.array2string($options).") path=$path"); return egw_vfs::unlock($path,$options['token']) ? '204 No Content' : '409 Conflict'; } /** * checkLock() helper * * @param string resource path to check for locks * @return bool true on success */ function checkLock($path) { self::_parse_path($path,$id,$app,$user); $path = egw_vfs::app_entry_lock_path($app,$id); return egw_vfs::checkLock($path); } /** * ACL method handler * * @param array general parameter passing array * @return string HTTP status */ function ACL(&$options) { self::_parse_path($options['path'],$id,$app,$user); if ($this->debug) error_log(__METHOD__.'('.array2string($options).") path=$path"); $options['errors'] = array(); switch ($app) { case 'calendar': case 'addressbook': case 'infolog': $status = '200 OK'; // grant all break; default: $options['errors'][] = 'no-inherited-ace-conflict'; $status = '403 Forbidden'; } return $status; } /** * Parse a path into it's id, app and user parts * * @param string $path * @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,&$user_prefix=null) { if ($this->debug) { 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 (($account_id = $this->accounts->name2id($parts[0], 'account_lid')) || ($account_id = $this->accounts->name2id($parts[0]=urldecode($parts[0])))) { // /$user/$app/... $user = array_shift($parts); } $app = array_shift($parts); if ($user) { $user_prefix = '/'.$user; $user = $account_id; } else { $user_prefix = ''; $user = $GLOBALS['egw_info']['user']['account_id']; } $id = array_pop($parts); $ok = $id && $user && in_array($app,array('addressbook','calendar','infolog','principals')); if ($this->debug) { error_log(__METHOD__."('$path') returning " . ($ok ? 'true' : 'false') . ": id='$id', app='$app', user='$user', user_prefix='$user_prefix'"); } return $ok; } /** * Add the privileges of the current user * * @return array self::mkprop('privilege',array(...)) */ static function current_user_privilege_set() { return array(self::mkprop('privilege', array(//self::mkprop('all',''), self::mkprop('read',''), self::mkprop('read-free-busy',''), //self::mkprop('read-current-user-privilege-set',''), self::mkprop('bind',''), self::mkprop('unbind',''), self::mkprop('schedule-post',''), self::mkprop('schedule-post-vevent',''), self::mkprop('schedule-respond',''), self::mkprop('schedule-respond-vevent',''), self::mkprop('schedule-deliver',''), self::mkprop('schedule-deliver-vevent',''), self::mkprop('write',''), self::mkprop('write-properties',''), self::mkprop('write-content',''), ))); } }