diff --git a/addressbook/inc/class.addressbook_groupdav.inc.php b/addressbook/inc/class.addressbook_groupdav.inc.php new file mode 100644 index 0000000000..0386f37ec7 --- /dev/null +++ b/addressbook/inc/class.addressbook_groupdav.inc.php @@ -0,0 +1,320 @@ + + * @copyright (c) 2007/8 by Ralf Becker + * @version $Id$ + */ + +require_once(EGW_INCLUDE_ROOT.'/addressbook/inc/class.bocontacts.inc.php'); + +/** + * eGroupWare: GroupDAV access: addressbook handler + */ +class addressbook_groupdav extends groupdav_handler +{ + /** + * bo class of the application + * + * @var vcaladdressbook + */ + var $bo; + + var $filter_prop2cal = array( + 'UID' => 'uid', + //'NICKNAME', + 'EMAIL' => 'email', + 'FN' => 'n_fn', + ); + + /** + * Charset for exporting data, as some clients ignore the headers specifying the charset + * + * @var string + */ + var $charset = 'utf-8'; + + function __construct($debug=null) + { + parent::__construct('addressbook',$debug); + + $this->bo =& new bocontacts(); + + // SoGo Connector for Thunderbird works only with iso-8859-1! + if (strpos($_SERVER['HTTP_USER_AGENT'],'Thunderbird') !== false) $charset = 'iso-8859-1'; + } + + /** + * Handle propfind in the addressbook folder + * + * @param string $path + * @param array $options + * @param array &$files + * @param int $user account_id + * @param string $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,$id='') + { + if ($user) $filter = array('contact_owner' => $user); + + // process REPORT filters or multiget href's + if (($id || $options['root']['name'] != 'propfind') && !$this->_report_filters($options,$filter,$id)) + { + return false; + } + error_log(__METHOD__."($path,$options,,$user,$id) filter=".str_replace(array("\n",' '),'',print_r($filter,true))); + // check if we have to return the full calendar data or just the etag's + if (!($address_data = $options['props'] == 'all' && $options['root']['ns'] == groupdav::CARDDAV)) + { + foreach($options['props'] as $prop) + { + if ($prop['name'] == 'address-data') + { + $address_data = true; + break; + } + } + } + if ($address_data) + { + $handler = self::_get_handler(); + } + if (($contacts =& $this->bo->search(array(),$address_data ? false : array('id','modified','etag'),'contact_id','','',False,'AND',false,$filter))) + { + foreach($contacts as $contact) + { + $props = array( + HTTP_WebDAV_Server::mkprop('getetag',$this->get_etag($contact)), + HTTP_WebDAV_Server::mkprop('getcontenttype', 'text/x-vcard'), + ); + if ($address_data) + { + $props[] = HTTP_WebDAV_Server::mkprop(groupdav::CARDDAV,'address-data',$handler->getVCard($contact,$this->charset)); + } + $files['files'][] = array( + 'path' => '/addressbook/'.$contact['id'], + 'props' => $props, + ); + } + } + return true; + } + + /** + * Process the filters from the CalDAV REPORT request + * + * @param array $options + * @param array &$cal_filters + * @param string $id + * @return boolean true if filter could be processed, false for requesting not here supported VTODO items + */ + function _report_filters($options,&$filters,$id) + { + if ($options['filters']) + { + foreach($options['filters'] as $filter) + { + switch($filter['name']) + { + case 'comp-filter': + error_log(__METHOD__."($path,...) comp-filter='{$filter['attrs']['name']}'"); + switch($filter['attrs']['name']) + { + } + break; + case 'prop-filter': + error_log(__METHOD__."($path,...) prop-filter='{$filter['attrs']['name']}'"); + $prop_filter = $filter['attrs']['name']; + break; + case 'text-match': + error_log(__METHOD__."($path,...) text-match: $prop_filter='{$filter['data']}'"); + if (!isset($this->filter_prop2cal[strtoupper($prop_filter)])) + { + error_log(__METHOD__."($path,".str_replace(array("\n",' '),'',print_r($options,true)).",,$user) unknown property '$prop_filter' --> ignored"); + } + else + { + switch($filter['attrs']['match-type']) + { + default: + case 'equals': + $filters[$this->filter_prop2cal[strtoupper($prop_filter)]] = $filter['data']; + break; + case 'substr': // ToDo: check RFC4790 + $filters[] = $this->filter_prop2cal[strtoupper($prop_filter)].' LIKE '.$GLOBALS['egw']->db->quote($filter['data']); + break; + } + } + unset($prop_filter); + break; + case 'param-filter': + error_log(__METHOD__."($path,...) param-filter='{$filter['attrs']['name']}'"); + break; + default: + error_log(__METHOD__."($path,".str_replace(array("\n",' '),'',print_r($options,true)).",,$user) unknown filter --> ignored"); + break; + } + } + } + // multiget --> fetch the url's + if ($options['root']['name'] == 'addressbook-multiget') + { + $ids = array(); + foreach($options['other'] as $option) + { + if ($option['name'] == 'href') + { + $parts = explode('/',$option['data']); + if (is_numeric($id = array_pop($parts))) $ids[] = $id; + } + } + $filters['id'] = $ids; + //error_log(__METHOD__."($path,,,$user) addressbook-multiget: ids=".implode(',',$ids)); + } + elseif ($id) + { + if (is_numeric($id)) + { + $filters['id'] = $id; + } + else + { + $filters['uid'] = basename($id,'.vcf'); + } + } + return true; + } + + /** + * Handle get request for an event + * + * @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($contact = $this->_common_get_put_delete('GET',$options,$id))) + { + return $contact; + } + $handler = self::_get_handler(); + $options['data'] = $handler->getVCard($id,$this->charset); + $options['mimetype'] = 'text/x-vcard; charset='.$this->charset; + header('Content-Encoding: identity'); + header('ETag: '.$this->get_etag($contact)); + return true; + } + + /** + * Handle put request for an event + * + * @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) + { + $ok = $this->_common_get_put_delete('PUT',$options,$id); + if (!is_null($ok) && !is_array($ok)) + { + return $ok; + } + $handler = self::_get_handler(); + $contact = $handler->vcardtoegw($options['content']); + if (!is_null($ok)) + { + $contact['id'] = $id; + } + // SoGo does not set the uid attribut, but uses it as id + elseif (strlen($id) > 10 && !$contact['uid']) + { + $contact['uid'] = basename($id,'.vcf'); + } + $contact['etag'] = self::etag2value($this->http_if_match); + + if (!($ok = $this->bo->save($contact))) + { + if ($ok === 0) + { + return '412 Precondition Failed'; + } + return false; + } + + header('ETag: '.$this->get_etag($contact)); + if (is_null($ok)) + { + header($h='Location: '.$this->base_uri.'/addressbook/'.$contact['id']); + error_log(__METHOD__."($method,,$id) header('$h'): 201 Created"); + return '201 Created'; + } + return true; + } + + /** + * Get the handler and set the supported fields + * + * @return vcaladdressbook + */ + private function _get_handler() + { + include_once(EGW_INCLUDE_ROOT.'/addressbook/inc/class.vcaladdressbook.inc.php'); + $handler =& new vcaladdressbook(); + if (strpos($_SERVER['HTTP_USER_AGENT'],'KHTML') !== false) + { + $handler->setSupportedFields('KDE'); + } + return $handler; + } + + /** + * Handle delete request for an event + * + * @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) + { + if (!is_array($event = $this->_common_get_put_delete('DELETE',$options,$id))) + { + return $event; + } + if ($this->bo->delete($id,self::etag2value($this->http_if_match)) === 0) + { + return '412 Precondition Failed'; + } + return $ok; + } + + /** + * Read a contact + * + * @param string/id $id + * @return array/boolean array with entry, false if no read rights, null if $id does not exist + */ + function read($id) + { + return $this->bo->read($id); + } + + /** + * Check if user has the neccessary rights on a contact + * + * @param int $acl EGW_ACL_READ, EGW_ACL_EDIT or EGW_ACL_DELETE + * @param array/int $contact contact-array or id + * @return boolean null if entry does not exist, false if no access, true if access permitted + */ + function check_access($acl,$contact) + { + return $this->bo->check_perms($acl,$contact); + } +} \ No newline at end of file diff --git a/calendar/inc/class.calendar_groupdav.inc.php b/calendar/inc/class.calendar_groupdav.inc.php new file mode 100644 index 0000000000..2d746f8b68 --- /dev/null +++ b/calendar/inc/class.calendar_groupdav.inc.php @@ -0,0 +1,347 @@ + + * @copyright (c) 2007/8 by Ralf Becker + * @version $Id$ + */ + +require_once(EGW_INCLUDE_ROOT.'/calendar/inc/class.bocalupdate.inc.php'); + +/** + * eGroupWare: GroupDAV access: calendar handler + */ +class calendar_groupdav extends groupdav_handler +{ + /** + * bo class of the application + * + * @var bocalupdate + */ + var $bo; + + var $filter_prop2cal = array( + 'SUMMARY' => 'cal_title', + 'UID' => 'cal_uid', + 'DTSTART' => 'cal_start', + 'DTEND' => 'cal_end', + // 'DURATION' + //'RRULE' => 'recur_type', + //'RDATE' => 'cal_start', + //'EXRULE' + //'EXDATE' + //'RECURRENCE-ID' + ); + + function __construct($debug=null) + { + parent::__construct('calendar',$debug); + + $this->bo =& new bocalupdate(); + } + + /** + * Handle propfind in the calendar folder + * + * @param string $path + * @param array $options + * @param array &$files + * @param int $user account_id + * @param string $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,$id='') + { + if ($this->debug > 2) error_log(__METHOD__."($path,".str_replace(array("\n",' '),'',print_r($options,true)).",,$user,$id)"); + + // ToDo: add parameter to only return id & etag + $cal_filters = array( + 'users' => $user, + 'start' => time()-30*24*3600, // default one month back + 'end' => time()+365*24*3600, // default one year into the future + 'enum_recuring' => false, + 'daywise' => false, + 'date_format' => 'server', + ); + error_log(__METHOD__."($path,,,$user,$id) cal_filters=".str_replace(array("\n",' '),'',print_r($cal_filters,true))); + + // process REPORT filters or multiget href's + if (($id || $options['root']['name'] != 'propfind') && !$this->_report_filters($options,$cal_filters,$id)) + { + return false; + } + // check if we have to return the full calendar data or just the etag's + if (!($calendar_data = $options['props'] == 'all' && $options['root']['ns'] == groupdav::CALDAV)) + { + foreach($options['props'] as $prop) + { + if ($prop['name'] == 'calendar-data') + { + $calendar_data = true; + break; + } + } + } + if (($events = $this->bo->search($cal_filters))) + { + foreach($events as $event) + { + $props = array( + HTTP_WebDAV_Server::mkprop('getetag',$this->get_etag($event)), + HTTP_WebDAV_Server::mkprop('getcontenttype', 'text/calendar'), + ); + if ($calendar_data) + { + $props[] = HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'calendar-data', + ExecMethod2('calendar.boical.exportVCal',array($event),'2.0','PUBLISH',false)); + } + $files['files'][] = array( + 'path' => '/calendar/'.$event['id'], + 'props' => $props, + ); + } + } + return true; + } + + /** + * Process the filters from the CalDAV REPORT request + * + * @param array $options + * @param array &$cal_filters + * @param string $id + * @return boolean true if filter could be processed, false for requesting not here supported VTODO items + */ + function _report_filters($options,&$cal_filters,$id) + { + if ($options['filters']) + { + // unset default start & end + $cal_start = $cal_filters['start']; unset($cal_filters['start']); + $cal_end = $cal_filters['end']; unset($cal_filters['end']); + $num_filters = count($cal_filters); + + foreach($options['filters'] as $filter) + { + switch($filter['name']) + { + case 'comp-filter': + error_log(__METHOD__."($path,...) comp-filter='{$filter['attrs']['name']}'"); + switch($filter['attrs']['name']) + { + case 'VTODO': + return false; // return nothing for now, todo: check if we can pass it on to the infolog handler + // todos are handled by the infolog handler + $infolog_handler = new infolog_groupdav(); + return $infolog_handler->propfind($path,$options,$files,$user,$method); + case 'VCALENDAR': + CASE 'VEVENT': + break; // that's our default anyway + } + break; + case 'prop-filter': + error_log(__METHOD__."($path,...) prop-filter='{$filter['attrs']['name']}'"); + $prop_filter = $filter['attrs']['name']; + break; + case 'text-match': + error_log(__METHOD__."($path,...) text-match: $prop_filter='{$filter['data']}'"); + if (!isset($this->filter_prop2cal[strtoupper($prop_filter)])) + { + error_log(__METHOD__."($path,".str_replace(array("\n",' '),'',print_r($options,true)).",,$user) unknown property '$prop_filter' --> ignored"); + } + else + { + $cal_filters['query'][$this->filter_prop2cal[strtoupper($prop_filter)]] = $filter['data']; + } + unset($prop_filter); + break; + case 'param-filter': + error_log(__METHOD__."($path,...) param-filter='{$filter['attrs']['name']}'"); + break; + case 'time-range': + error_log(__METHOD__."($path,...) time-range={$filter['attrs']['start']}-{$filter['attrs']['end']}"); + + break; + default: + error_log(__METHOD__."($path,".str_replace(array("\n",' '),'',print_r($options,true)).",,$user) unknown filter --> ignored"); + break; + } + } + if (count($cal_filters) != $num_filters) // no filters set --> restore default start and end time + { + $cal_filters['start'] = $cal_start; + $cal_filters['end'] = $cal_end; + } + } + // multiget or propfind on a given id + if ($options['root']['name'] == 'calendar-multiget' || $id) + { + // no standard time-range! + unset($cal_filters['start']); + unset($cal_filters['end']); + + $ids = array(); + + if ($id) + { + if (is_numeric($id)) + { + $ids[] = (int)$id; + } + else + { + $cal_filters['query']['uid'] = basename($id,'.ics'); + } + + } + else // fetch all given url's + { + foreach($options['other'] as $option) + { + if ($option['name'] == 'href') + { + $parts = explode('/',$option['data']); + if (is_numeric($id = array_pop($parts))) $ids[] = $id; + } + } + } + if ($ids) + { + $cal_filters['query'][] = 'egw_cal.cal_id IN ('.implode(',',array_map(create_function('$n','return (int)$n;'),$ids)).')'; + } + //error_log(__METHOD__."($path,,,$user,$id) calendar-multiget: ids=".implode(',',$ids)); + } + return true; + } + + /** + * Handle get request for an event + * + * @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($event = $this->_common_get_put_delete('GET',$options,$id))) + { + return $event; + } + $options['data'] = ExecMethod2('calendar.boical.exportVCal',array($event),'2.0','PUBLISH',false); + $options['mimetype'] = 'text/calendar; charset=utf-8'; + header('Content-Encoding: identity'); + header('ETag: '.$this->get_etag($event)); + return true; + } + + /** + * Handle put request for an event + * + * @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) + { + $event = $this->_common_get_put_delete('PUT',$options,$id); + if (!is_null($event) && !is_array($event)) + { + return $event; + } + if (!($cal_id = ExecMethod2('calendar.boical.importVCal',$options['content'],is_numeric($id) ? $id : -1, + self::etag2value($this->http_if_match)))) + { + if ($this->debug) error_log(__METHOD__."(,$id) import_vevent($options[content]) returned false"); + return false; // something went wrong ... + } + + header('ETag: '.$this->get_etag($cal_id)); + if (is_null($event) || $id != $cal_id) + { + header('Location: '.$this->base_uri.'/calendar/'.$cal_id); + return '201 Created'; + } + return true; + } + + /** + * Handle delete request for an event + * + * @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) + { + if (!is_array($event = $this->_common_get_put_delete('DELETE',$options,$id))) + { + return $event; + } + return $this->bo->delete($id); + } + + /** + * Read an entry + * + * @param string/id $id + * @return array/boolean array with entry, false if no read rights, null if $id does not exist + */ + function read($id) + { + return $this->bo->read($id,null,false,'server'); + } + + /** + * Get the etag for an entry, reimplemented to include the participants and stati in the etag + * + * @param array/int $event array with event or cal_id + * @return string/boolean string with etag or false + */ + function get_etag($entry) + { + if (!is_array($entry)) + { + $entry = $this->read($entry); + } + $etag = $entry['id'].':'.$entry['etag']; + // add a hash over the participants and their stati + ksort($entry['participants']); // create a defined order + $etag .= ':'.md5(serialize($entry['participants'])); + //error_log(__METHOD__."($entry[id]: $entry[title])=$etag"); + return $etag; + } + + /** + * Check if user has the neccessary rights on an event + * + * @param int $acl EGW_ACL_READ, EGW_ACL_EDIT or EGW_ACL_DELETE + * @param array/int $event event-array or id + * @return boolean null if entry does not exist, false if no access, true if access permitted + */ + function check_access($acl,$event) + { + return $this->bo->check_perms($acl,$event,0,'server'); + } + + /** + * Add extra properties for calendar collections + * + * @param array $props=array() regular props by the groupdav handler + * @return array + */ + static function extra_properties(array $props=array()) + { + // calendaring URL of the current user + $props[] = HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'calendar-home-set',$_SERVER['SCRIPT_NAME'].'/calendar/'); + // email of the current user, see caldav-sheduling draft + $props[] = HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'calendar-user-address-set','mailto:'.$GLOBALS['egw_info']['user']['email']); + + return $props; + } +} \ No newline at end of file diff --git a/groupdav.php b/groupdav.php new file mode 100644 index 0000000000..e5646f06dd --- /dev/null +++ b/groupdav.php @@ -0,0 +1,50 @@ + + * @copyright (c) 2007/8 by Ralf Becker + * @version $Id$ + */ + +/** + * check if the given user has access + * + * Create a session or if the user has no account return authenticate header and 401 Unauthorized + * + * @param array &$account + * @return int session-id + */ +function check_access(&$account) +{ + $account = array( + 'login' => $_SERVER['PHP_AUTH_USER'], + 'passwd' => $_SERVER['PHP_AUTH_PW'], + 'passwd_type' => 'text', + ); + if (!($sessionid = $GLOBALS['egw']->session->create($account))) + { + header('WWW-Authenticate: Basic realm="'.groupdav::REALM.'"'); + header("HTTP/1.1 401 Unauthorized"); + header("X-WebDAV-Status: 401 Unauthorized", true); + exit; + } + return $sessionid; +} + +$GLOBALS['egw_info']['flags'] = array( + 'noheader' => True, + 'currentapp' => 'groupdav', + 'autocreate_session_callback' => 'check_access', +); +// if you move this file somewhere else, you need to adapt the path to the header! +include(dirname(__FILE__).'/header.inc.php'); + +$groupdav = new groupdav(); +$groupdav->ServeRequest(); diff --git a/phpgwapi/inc/class.groupdav.inc.php b/phpgwapi/inc/class.groupdav.inc.php new file mode 100644 index 0000000000..b01820e161 --- /dev/null +++ b/phpgwapi/inc/class.groupdav.inc.php @@ -0,0 +1,447 @@ + + * @copyright (c) 2007/8 by Ralf Becker + * @version $Id$ + */ + +require_once('HTTP/WebDAV/Server.php'); + +/** + * eGroupWare: GroupDAV access + * + * Using the PEAR HTTP/WebDAV/Server class (which need to be installed!) + * + * @link http://www.groupdav.org GroupDAV spec + */ +class groupdav extends HTTP_WebDAV_Server +{ + /** + * 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'; + /** + * Realm and powered by string + */ + const REALM = 'eGroupWare CalDAV/CardDAV/GroupDAV server'; + + var $dav_powered_by = self::REALM; + + var $root = array( + 'calendar' => array(self::GROUPDAV => 'vevent-collection', self::CALDAV => 'calendar'), + 'addressbook' => array(self::GROUPDAV => 'vcard-collection', self::CARDDAV => 'addressbook'), + 'infolog' => array(self::GROUPDAV => 'vtodo-collection'), + ); + /** + * Debug level: 0 = nothing, 1 = function calls, 2 = more info, 3 = complete $_SERVER array + * + * The debug messages are send to the apache error_log + * + * @var integer + */ + var $debug = 1; + + /** + * eGW's charset + * + * @var string + */ + var $egw_charset; + /** + * Reference to the translation class + * + * @var translation + */ + var $translation; + /** + * Instance of our application specific handler + * + * @var groupdav_handler + */ + var $handler; + + function __construct() + { + if ($this->debug > 2) foreach($_SERVER as $name => $val) error_log("groupdav: \$_SERVER[$name]='$val'"); + + parent::HTTP_WebDAV_Server(); + + $this->translation =& $GLOBALS['egw']->translation; + $this->egw_charset = $this->translation->charset(); + } + + function _instancicate_handler($app) + { + $this->handler = groupdav_handler::app_handler($app); + } + + /** + * 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': + $dav[] = 'calendar-access'; + break; + case 'addressbook': + $dav[] = 'addressbook'; + break; + } + // not yet implemented: $dav[] = 'access-control'; + } + + /** + * 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(".str_replace(array("\n",' ',"\t"),'',print_r($options,true)).')'); + + // parse path in form [/account_lid]/app[/more] + if (!self::_parse_path($options['path'],$id,$app,$user) && $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"); + + $files = array(); + + if (!$app) // root folder containing apps + { + $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 + self::mkprop(groupdav::CALDAV,'calendar-home-set',$_SERVER['SCRIPT_NAME'].'/calendar/'), + ), + ); + + foreach($this->root as $app => $data) + { + if (!$GLOBALS['egw_info']['user']['apps'][$app]) continue; // no rights for the given app + + $extra_props = 'groupdav_'.$app.'::extra_properties'; + + $files['files'][] = array( + 'path' => '/'.$app.'/', + 'props' => call_user_func('groupdav_'.$app.'::extra_properties',array( + self::mkprop('displayname',$this->translation->convert(lang($app),$this->egw_charset,'utf-8')), + self::mkprop('resourcetype',$this->_resourcetype($app)), + )), + ); + } + return true; + } + if (!$GLOBALS['egw_info']['user']['apps'][$app]) + { + error_log(__CLASS__."::$method(path=$options[path]) 403 Forbidden: no app rights"); + return '403 Forbidden'; // no rights for the given app + } + if (($handler = groupdav_handler::app_handler($app,$this->debug))) + { + if ($method != 'REPORT' && !$id) // no self URL for REPORT requests (only PROPFIND) or propfinds on an id + { + $files['files'][] = array( + 'path' => '/'.$app.'/', + 'props' => $handler->extra_properties(array( + self::mkprop('displayname',$this->translation->convert(lang($app),$this->egw_charset,'utf-8')), + // Kontact doubles the folder, if the self URL contains the GroupDAV/CalDAV resourcetypes + self::mkprop('resourcetype', $this->_resourcetype($app,strpos($_SERVER['HTTP_USER_AGENT'],'KHTML') !== false)), + )), + ); + } + return $handler->propfind($options['path'],$options,$files,$user,$id); + } + return '501 Not Implemented'; + } + + /** + * Return resourcetype(s) for a given app + * + * @param string $app + * @param boolean $no_extra_types=false should the GroupDAV and CalDAV types be added (Kontact has problems with it in self URL) + * @return array or DAV properties generated via + */ + function _resourcetype($app,$no_extra_types=false) + { + $resourcetype = array( + self::mkprop('collection','collection'), + ); + if (!$no_extra_types) + { + foreach($this->root[$app] as $ns => $type) + { + $resourcetype[] = self::mkprop($ns,'resourcetype', $type); + } + } + return $resourcetype; + } + + /** + * 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__.'('.str_replace(array("\n",' '),'',print_r($options,true)).')'); + + 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 parameter passing array + * @return bool true on success + */ + function GET(&$options) + { + if ($this->debug) error_log(__METHOD__.'('.print_r($options,true).')'); + + if (!$this->_parse_path($options['path'],$id,$app,$user)) + { + return '404 Not Found'; + } + if (($handler = groupdav_handler::app_handler($app,$this->debug))) + { + return $handler->get($options,$id); + } + 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__.'('.print_r($options,true).')'); + + if (!$this->_parse_path($options['path'],$id,$app,$user)) + { + return '404 Not Found'; + } + if (($handler = groupdav_handler::app_handler($app,$this->debug))) + { + $status = $handler->put($options,$id,$user); + // 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__.'('.print_r($options,true).')'); + + if (!$this->_parse_path($options['path'],$id,$app,$user)) + { + return '404 Not Found'; + } + if (($handler = groupdav_handler::app_handler($app,$this->debug))) + { + $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__.'('.print_r($options,true).')'); + + 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__.'('.print_r($options,true).')'); + + 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').'('.print_r($options,true).')'); + + return '501 Not Implemented'; + } + + /** + * LOCK method handler + * + * @param array general parameter passing array + * @return bool true on success + */ + function LOCK(&$options) + { + error_log(__METHOD__.'('.str_replace(array("\n",' '),'',print_r($options,true)).')'); + + self::_parse_path($options['path'],$id,$app,$user); + $path = egw_vfs::app_entry_lock_path($app,$id); + + // get the app handler, to check if the user has edit access to the entry (required to make locks) + $handler = groupdav_handler::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); + + error_log(__METHOD__.'('.str_replace(array("\n",' '),'',print_r($options,true)).") 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); + } + + /** + * 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 + * @return boolean true on success, false on error + */ + function _parse_path($path,&$id,&$app,&$user) + { + $parts = explode('/',$path); + + list($id) = explode('.',array_pop($parts)); // remove evtl. .ics extension + + $app = array_pop($parts); + + if (($user = array_pop($parts))) + { + $user = $GLOBALS['egw']->accounts->name2id($user,'account_lid',$app != 'addressbook' ? 'u' : null); + } + else + { + $user = $GLOBALS['egw_info']['user']['account_id']; + } + if (!($ok = $id && in_array($app,array('addressbook','calendar','infolog')) && $user)) + { + 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 new file mode 100644 index 0000000000..a378bd5749 --- /dev/null +++ b/phpgwapi/inc/class.groupdav_handler.inc.php @@ -0,0 +1,237 @@ + + * @copyright (c) 2007/8 by Ralf Becker + * @version $Id$ + */ + +/** + * eGroupWare: GroupDAV access: abstract baseclass for groupdav/caldav/carddav handlers + */ +abstract class groupdav_handler +{ + /** + * Debug level: 0 = nothing, 1 = function calls, 2 = more info, eg. complete $_SERVER array + * + * The debug messages are send to the apache error_log + * + * @var integer + */ + var $debug = 1; + + /** + * eGW's charset + * + * @var string + */ + var $egw_charset; + /** + * Reference to the translation class + * + * @var translation + */ + var $translation; + /** + * Translates method names into ACL bits + * + * @var array + */ + var $method2acl = array( + 'GET' => EGW_ACL_READ, + 'PUT' => EGW_ACL_EDIT, + 'DELETE' => EGW_ACL_DELETE, + ); + /** + * eGW application responsible for the handler + * + * @var string + */ + var $app; + /** + * HTTP_IF_MATCH / etag of current request / last call to _common_get_put_delete() method + * + * @var string + */ + var $http_if_match; + + function __construct($app,$debug=null) + { + $this->app = $app; + if (!is_null($debug)) $this->debug = $debug; + + $this->translation =& $GLOBALS['egw']->translation; + $this->egw_charset = $this->translation->charset(); + } + + /** + * 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') + */ + abstract function propfind($path,$options,&$files,$user); + + /** + * 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') + */ + abstract function get(&$options,$id); + + /** + * 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') + */ + abstract function put(&$options,$id,$user=null); + + /** + * 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') + */ + abstract function delete(&$options,$id); + + /** + * Read an entry + * + * @param string/int $id + * @return array/boolean array with entry, false if no read rights, null if $id does not exist + */ + abstract function 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 + */ + abstract function check_access($acl,$entry); + + /** + * Add extra properties for collections + * + * @param array $props=array() regular props by the groupdav handler + * @return array + */ + static function extra_properties(array $props=array()) + { + return $props; + } + + /** + * 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($entry) + { + if (!is_array($entry)) + { + $entry = $this->read($entry); + } + if (!is_array($entry) || !isset($entry['id']) || !(isset($entry['modified']) || isset($entry['etag']))) + { + return false; + } + return '"'.$entry['id'].':'.(isset($entry['etag']) ? $entry['etag'] : $entry['modified']).'"'; + } + + /** + * Convert etag to the raw etag column value (without quotes, double colon and id) + * + * @param string $etag + * @return int + */ + static function etag2value($etag) + { + list(,$val) = explode(':',substr($etag,1,-1),2); + + return $val; + } + + /** + * Handle common stuff for get, put and delete requests: + * - application rights + * - entry level acl, incl. edit and delete rights + * - etag handling for precondition failed and not modified + * + * @param string $method GET, PUT, DELETE + * @param array &$options + * @param int $id + * @return array/string entry on success, string with http-error-code on failure, null for PUT on an unknown id + */ + function _common_get_put_delete($method,&$options,$id) + { + if (!$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 + } + $extra_acl = $this->method2acl[$method]; + if (!($entry = $this->read($id)) && ($method != 'PUT' || $event === false) || + ($extra_acl != EGW_ACL_READ && $this->check_access($extra_acl,$entry) === false)) + { + if ($this->debug) error_log(__METHOD__."($method,,$id) 403 Forbidden/404 Not Found: read($id)==".($entry===false?'false':'null')); + return !is_null($entry) ? '403 Forbidden' : '404 Not Found'; + } + if ($entry) + { + $etag = $this->get_etag($entry); + // If the clients sends an "If-Match" header ($_SERVER['HTTP_IF_MATCH']) we check with the current etag + // of the calendar --> on failure we return 412 Precondition failed, to not overwrite the modifications + if (isset($_SERVER['HTTP_IF_MATCH']) && ($this->http_if_match = $_SERVER['HTTP_IF_MATCH']) != $etag) + { + if ($this->debug) error_log(__METHOD__."($method,,$id) HTTP_IF_MATCH='$_SERVER[HTTP_IF_MATCH]', etag='$etag': 412 Precondition failed"); + return '412 Precondition Failed'; + } + // if an IF_NONE_MATCH is given, check if we need to send a new export, or the current one is still up-to-date + if ($method == 'GET' && isset($_SERVER['HTTP_IF_NONE_MATCH']) && $_SERVER['HTTP_IF_NONE_MATCH'] == $etag) + { + if ($this->debug) error_log(__METHOD__."($method,,$id) HTTP_IF_NONE_MATCH='$_SERVER[HTTP_IF_NONE_MATCH]', etag='$etag': 304 Not Modified"); + return '304 Not Modified'; + } + } + return $entry; + } + + /** + * Get the handler for the given app + * + * @static + * @param string $app 'calendar', 'addressbook' or 'infolog' + * @param int $debug=null debug-level to set + * @return groupdav_handler + */ + static function &app_handler($app,$debug=null) + { + static $handler_cache = array(); + + if (!array_key_exists($app,$handler_cache)) + { + $class = $app.'_groupdav'; + if (!class_exists($class)) return null; + + $handler_cache[$app] = new $class($app); + } + return $handler_cache[$app]; + } +} \ No newline at end of file