First version of new CalDav/CardDAV/GroupDAV access for calendar and

addressbook (infolog will follow).
CalDAV is tested so far with lightning 0.8 and Apple's iCal. Please note
that both distinguish between iCalServer and CalDAV!
The URL is currently http://domain.com/egroupware/groupdav.php/calendar/
This commit is contained in:
Ralf Becker 2008-05-08 20:31:32 +00:00
parent 9d7e48fcaa
commit d2e9143213
5 changed files with 1401 additions and 0 deletions

View File

@ -0,0 +1,320 @@
<?php
/**
* eGroupWare: GroupDAV access: addressbook handler
*
* @link http://www.egroupware.org
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
* @package addressbook
* @subpackage groupdav
* @author Ralf Becker <RalfBecker-AT-outdoor-training.de>
* @copyright (c) 2007/8 by Ralf Becker <RalfBecker-AT-outdoor-training.de>
* @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);
}
}

View File

@ -0,0 +1,347 @@
<?php
/**
* eGroupWare: GroupDAV access: calendar handler
*
* @link http://www.egroupware.org
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
* @package calendar
* @subpackage groupdav
* @author Ralf Becker <RalfBecker-AT-outdoor-training.de>
* @copyright (c) 2007/8 by Ralf Becker <RalfBecker-AT-outdoor-training.de>
* @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;
}
}

50
groupdav.php Normal file
View File

@ -0,0 +1,50 @@
<?php
/**
* eGroupWare - GroupDAV access
*
* Using the PEAR HTTP/WebDAV/Server class (which need to be installed!)
*
* @link http://www.egroupware.org
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
* @package api
* @subpackage groupdav
* @author Ralf Becker <RalfBecker-AT-outdoor-training.de>
* @copyright (c) 2007/8 by Ralf Becker <RalfBecker-AT-outdoor-training.de>
* @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();

View File

@ -0,0 +1,447 @@
<?php
/**
* eGroupWare: GroupDAV access
*
* Using the PEAR HTTP/WebDAV/Server class (which need to be installed!)
*
* @link http://www.egroupware.org
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
* @package api
* @subpackage groupdav
* @author Ralf Becker <RalfBecker-AT-outdoor-training.de>
* @copyright (c) 2007/8 by Ralf Becker <RalfBecker-AT-outdoor-training.de>
* @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;
}
}

View File

@ -0,0 +1,237 @@
<?php
/**
* eGroupWare: GroupDAV access: abstract baseclass for groupdav/caldav/carddav handlers
*
* @link http://www.egroupware.org
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
* @package api
* @subpackage groupdav
* @author Ralf Becker <RalfBecker-AT-outdoor-training.de>
* @copyright (c) 2007/8 by Ralf Becker <RalfBecker-AT-outdoor-training.de>
* @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];
}
}