1
0
mirror of https://github.com/EGroupware/egroupware.git synced 2025-03-29 00:57:38 +01:00

* CalDAV/CardDAV: major rework fixing lots of bugs/incompatibilites and adding new features: eg. autocompletion of accounts and resources under iCal, searchable addressbook gateway for all addressbooks available

merged changes from Trunk up to r37094 from addressbook, calendar, infolog, phpgwapi, egw-pear and resources (only CalDAV/CardDAV related stuff of cause)
This commit is contained in:
Ralf Becker 2011-11-06 09:40:33 +00:00
parent a43ec990cd
commit 80510b5412
25 changed files with 2892 additions and 1039 deletions

View File

@ -16,6 +16,8 @@
*
* Propfind now uses a groupdav_propfind_iterator with a callback to query huge addressbooks in chunk,
* without getting into problems with memory_limit.
*
* @todo create extra addressbook eg. "/accounts/" which shows accounts, even if they are in LDAP (no carddav_name column!)
*/
class addressbook_groupdav extends groupdav_handler
{
@ -31,6 +33,7 @@ class addressbook_groupdav extends groupdav_handler
//'NICKNAME',
'EMAIL' => 'email',
'FN' => 'n_fn',
'ORG' => 'org_name',
);
var $supportedFields = array(
@ -76,24 +79,15 @@ class addressbook_groupdav extends groupdav_handler
*/
var $charset = 'utf-8';
/**
* Which attribute to use to contruct name part of url/path
*
* @var string
*/
static $path_attr = 'id';
/**
* Constructor
*
* @param string $app 'calendar', 'addressbook' or 'infolog'
* @param int $debug=null debug-level to set
* @param string $base_uri=null base url of handler
* @param string $principalURL=null pricipal url of handler
* @param groupdav $groupdav calling class
*/
function __construct($app,$debug=null,$base_uri=null,$principalURL=null)
function __construct($app, groupdav $groupdav)
{
parent::__construct($app,$debug,$base_uri,$principalURL);
parent::__construct($app, $groupdav);
$this->bo = new addressbook_bo();
@ -101,7 +95,7 @@ class addressbook_groupdav extends groupdav_handler
if ($this->bo->account_repository != 'ldap' &&
version_compare($GLOBALS['egw_info']['apps']['phpgwapi']['version'], '1.9.007', '>='))
{
self::$path_attr = 'carddav_name';
groupdav_handler::$path_attr = 'carddav_name';
groupdav_handler::$path_extension = '';
}
else
@ -110,17 +104,6 @@ class addressbook_groupdav extends groupdav_handler
}
}
/**
* Create the path for a contact
*
* @param array $contact
* @return string
*/
static function get_path($contact)
{
return $contact[self::$path_attr].groupdav_handler::$path_extension;
}
/**
* Handle propfind in the addressbook folder
*
@ -140,10 +123,12 @@ class addressbook_groupdav extends groupdav_handler
if ($GLOBALS['egw_info']['user']['preferences']['addressbook']['hide_accounts']) $filter['account_id'] = null;
// process REPORT filters or multiget href's
if (($id || $options['root']['name'] != 'propfind') && !$this->_report_filters($options,$filter,$id))
if (($id || $options['root']['name'] != 'propfind') && !$this->_report_filters($options,$filter,$id, $nresults))
{
return false;
}
if ($id) $path = dirname($path).'/'; // carddav_name get's added anyway in the callback
if ($this->debug) error_log(__METHOD__."($path,".array2string($options).",,$user,$id) filter=".array2string($filter));
// check if we have to return the full contact data or just the etag's
@ -159,8 +144,15 @@ class addressbook_groupdav extends groupdav_handler
}
}
}
// return iterator, calling ourself to return result in chunks
$files['files'] = new groupdav_propfind_iterator($this,$path,$filter,$files['files']);
if (isset($nresults))
{
$files['files'] = $this->propfind_callback($path, $filter, array(0, (int)$nresults));
}
else
{
// return iterator, calling ourself to return result in chunks
$files['files'] = new groupdav_propfind_iterator($this,$path,$filter,$files['files']);
}
return true;
}
@ -181,34 +173,33 @@ class addressbook_groupdav extends groupdav_handler
$handler = self::_get_handler();
}
unset($filter['address_data']);
if (isset($filter['order']))
{
$order = $filter['order'];
unset($filter['order']);
}
else
{
$order = 'egw_addressbook.contact_id';
}
$files = array();
// we query etag and modified, as LDAP does not have the strong sql etag
$cols = array('id','uid','etag','modified');
if (!in_array(self::$path_attr,$cols)) $cols[] = self::$path_attr;
if (($contacts =& $this->bo->search(array(),$cols,'egw_addressbook.contact_id','','',False,'AND',$start,$filter)))
if (($contacts =& $this->bo->search(array(),$cols,$order,'','',False,'AND',$start,$filter)))
{
foreach($contacts as &$contact)
{
$props = array(
HTTP_WebDAV_Server::mkprop('getetag',$this->get_etag($contact)),
HTTP_WebDAV_Server::mkprop('getcontenttype', 'text/vcard'),
// getlastmodified and getcontentlength are required by WebDAV and Cadaver eg. reports 404 Not found if not set
HTTP_WebDAV_Server::mkprop('getlastmodified', $contact['modified']),
'getcontenttype' => HTTP_WebDAV_Server::mkprop('getcontenttype', 'text/vcard'),
);
if ($address_data)
{
$content = $handler->getVCard($contact['id'],$this->charset,false);
$props[] = HTTP_WebDAV_Server::mkprop('getcontentlength',bytes($content));
$props['getcontentlength'] = bytes($content);
$props[] = HTTP_WebDAV_Server::mkprop(groupdav::CARDDAV,'address-data',$content,true);
}
else
{
$props[] = HTTP_WebDAV_Server::mkprop('getcontentlength', ''); // expensive to calculate and no CalDAV client uses it
}
$files[] = array(
'path' => $path.self::get_path($contact),
'props' => $props,
);
$files[] = $this->add_resource($path, $contact, $props);
}
}
if ($this->debug) error_log(__METHOD__."($path,".array2string($filter).','.array2string($start).") took ".(microtime(true) - $starttime).' to return '.count($files).' items');
@ -221,49 +212,136 @@ class addressbook_groupdav extends groupdav_handler
* @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
* @param int &$nresult on return limit for number or results or unchanged/null
* @return boolean true if filter could be processed
*/
function _report_filters($options,&$filters,$id)
function _report_filters($options,&$filters,$id, &$nresults)
{
if ($options['filters'])
{
foreach($options['filters'] as $filter)
/* Example of a complex filter used by Mac Addressbook
<B:filter test="anyof">
<B:prop-filter name="FN" test="allof">
<B:text-match collation="i;unicode-casemap" match-type="contains">becker</B:text-match>
<B:text-match collation="i;unicode-casemap" match-type="contains">ralf</B:text-match>
</B:prop-filter>
<B:prop-filter name="EMAIL" test="allof">
<B:text-match collation="i;unicode-casemap" match-type="contains">becker</B:text-match>
<B:text-match collation="i;unicode-casemap" match-type="contains">ralf</B:text-match>
</B:prop-filter>
<B:prop-filter name="NICKNAME" test="allof">
<B:text-match collation="i;unicode-casemap" match-type="contains">becker</B:text-match>
<B:text-match collation="i;unicode-casemap" match-type="contains">ralf</B:text-match>
</B:prop-filter>
</B:filter>
*/
$filter_test = isset($options['filters']['attrs']) && isset($options['filters']['attrs']['test']) ?
$options['filters']['attrs']['test'] : 'anyof';
$prop_filters = array();
foreach($options['filters'] as $n => $filter)
{
switch($filter['name'])
if (!is_int($n)) continue; // eg. attributes of filter xml element
switch((string)$filter['name'])
{
case 'prop-filter':
if ($this->debug > 1) error_log(__METHOD__."($path,...) prop-filter='{$filter['attrs']['name']}'");
$prop_filter = $filter['attrs']['name'];
case 'param-filter':
error_log(__METHOD__."(...) param-filter='{$filter['attrs']['name']}' not (yet) implemented!");
break;
case 'text-match':
if ($this->debug > 1) error_log(__METHOD__."($path,...) text-match: $prop_filter='{$filter['data']}'");
if (!isset($this->filter_prop2cal[strtoupper($prop_filter)]))
case 'prop-filter': // can be multiple prop-filter, see example
if ($matches) $prop_filters[] = implode($prop_test=='allof'?' AND ':' OR ',$matches);
$matches = array();
$prop_filter = strtoupper($filter['attrs']['name']);
$prop_test = isset($filter['attrs']['test']) ? $filter['attrs']['test'] : 'anyof';
if ($this->debug > 1) error_log(__METHOD__."(...) prop-filter='$prop_filter', test='$prop_test'");
break;
case 'is-not-defined':
$matches[] = '('.$column."='' OR ".$column.' IS NULL)';
break;
case 'text-match': // prop-filter can have multiple text-match, see example
if (!isset($this->filter_prop2cal[$prop_filter])) // eg. not existing NICKNAME in EGroupware
{
if ($this->debug) error_log(__METHOD__."($path,".str_replace(array("\n",' '),'',print_r($options,true)).",,$user) unknown property '$prop_filter' --> ignored");
if ($this->debug || $prop_filter != 'NICKNAME') error_log(__METHOD__."(...) text-match: $prop_filter {$filter['attrs']['match-type']} '{$filter['data']}' unknown property '$prop_filter' --> ignored");
$column = false; // to ignore following data too
}
else
{
switch($filter['attrs']['match-type'])
switch($filter['attrs']['collation']) // todo: which other collations allowed, we are allways unicode
{
case 'i;unicode-casemap':
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']);
$comp = ' '.$GLOBALS['egw']->db->capabilities[egw_db::CAPABILITY_CASE_INSENSITIV_LIKE].' ';
break;
}
$column = $this->filter_prop2cal[strtoupper($prop_filter)];
if (strpos($column, '_') === false) $column = 'contact_'.$column;
if (!isset($filters['order'])) $filters['order'] = $column;
$match_type = $filter['attrs']['match-type'];
$negate_condition = isset($filter['attrs']['negate-condition']) && $filter['attrs']['negate-condition'] == 'yes';
}
unset($prop_filter);
break;
case 'param-filter':
if ($this->debug) error_log(__METHOD__."($path,...) param-filter='{$filter['attrs']['name']}' not (yet) implemented!");
break;
case '': // data of text-match element
if (isset($filter['data']) && isset($column))
{
if ($column) // false for properties not known to EGroupware
{
$value = str_replace(array('%', '_'), array('\\%', '\\_'), $filter['data']);
switch($match_type)
{
case 'equals':
$sql_filter = $column . $comp . $GLOBALS['egw']->db->quote($value);
break;
default:
case 'contains':
$sql_filter = $column . $comp . $GLOBALS['egw']->db->quote('%'.$value.'%');
break;
case 'starts-with':
$sql_filter = $column . $comp . $GLOBALS['egw']->db->quote($value.'%');
break;
case 'ends-with':
$sql_filter = $column . $comp . $GLOBALS['egw']->db->quote('%'.$value);
break;
}
$matches[] = ($negate_condition ? 'NOT ' : '').$sql_filter;
if ($this->debug > 1) error_log(__METHOD__."(...) text-match: $prop_filter $match_type' '{$filter['data']}'");
}
unset($column);
break;
}
// fall through
default:
if ($this->debug) error_log(__METHOD__."($path,".array2string($options).",,$user) unknown filter --> ignored");
error_log(__METHOD__."(".array2string($options).",,$id) unknown filter=".array2string($filter).' --> ignored');
break;
}
}
if ($matches) $prop_filters[] = implode($prop_test=='allof'?' AND ':' OR ',$matches);
if ($prop_filters)
{
$filters[] = $filter = '(('.implode($filter_test=='allof'?') AND (':') OR (', $prop_filters).'))';
if ($this->debug) error_log(__METHOD__."($path,...) sql-filter: $filter");
}
}
// parse limit from $options['other']
/* Example limit
<B:limit>
<B:nresults>10</B:nresults>
</B:limit>
*/
foreach($options['other'] as $option)
{
switch($option['name'])
{
case 'nresults':
$nresults = (int)$option['data'];
//error_log(__METHOD__."(...) options[other]=".array2string($options['other'])." --> nresults=$nresults");
break;
case 'limit':
break;
default:
error_log(__METHOD__."(...) unknown xml: options[other]=".array2string($options['other']));
break;
}
}
// multiget --> fetch the url's
if ($options['root']['name'] == 'addressbook-multiget')
@ -281,7 +359,7 @@ class addressbook_groupdav extends groupdav_handler
}
}
if ($ids) $filters[self::$path_attr] = $ids;
if ($this->debug) error_log(__METHOD__."($path,,,$user) addressbook-multiget: ids=".implode(',',$ids));
if ($this->debug) error_log(__METHOD__."(...) addressbook-multiget: ids=".implode(',',$ids));
}
elseif ($id)
{
@ -309,12 +387,12 @@ class addressbook_groupdav extends groupdav_handler
// e.g. Evolution does not understand 'text/vcard'
$options['mimetype'] = 'text/x-vcard; charset='.$this->charset;
header('Content-Encoding: identity');
header('ETag: '.$this->get_etag($contact));
header('ETag: "'.$this->get_etag($contact).'"');
return true;
}
/**
* Handle put request for an event
* Handle put request for a contact
*
* @param array &$options
* @param int $id
@ -420,7 +498,8 @@ class addressbook_groupdav extends groupdav_handler
$contact = $this->read($save_ok);
}
header('ETag: '.$this->get_etag($contact));
// we should not return an etag here, as we never store the PUT vcard byte-by-byte
//header('ETag: "'.$this->get_etag($contact).'"');
// send GroupDAV Location header only if we dont use carddav_name as path-attribute
if ($retval !== true && self::$path_attr == 'id')
@ -442,9 +521,7 @@ class addressbook_groupdav extends groupdav_handler
// not showing addressbook of a single user?
if (!$user || $path == '/addressbook/') $user = null;
$ctag = $this->bo->get_ctag($user);
return 'EGw-'.$ctag.'-wGE';
return $this->bo->get_ctag($user);
}
/**
@ -497,16 +574,16 @@ class addressbook_groupdav extends groupdav_handler
* @param array $props=array() regular props by the groupdav handler
* @param string $displayname
* @param string $base_uri=null base url of handler
* @param int $user=null account_id of owner of collection
* @return array
*/
static function extra_properties(array $props=array(), $displayname, $base_uri=null)
public function extra_properties(array $props=array(), $displayname, $base_uri=null, $user=null)
{
// addressbook description
$displayname = translation::convert(lang('Addressbook of') . ' ' .
$displayname,translation::charset(),'utf-8');
$props[] = HTTP_WebDAV_Server::mkprop(groupdav::CARDDAV,'addressbook-description',$displayname);
$displayname = translation::convert(lang('Addressbook of'),translation::charset(),'utf-8').' '.$displayname;
$props['addressbook-description'] = HTTP_WebDAV_Server::mkprop(groupdav::CARDDAV,'addressbook-description',$displayname);
// supported reports (required property for CardDAV)
$props[] = HTTP_WebDAV_Server::mkprop('supported-report-set',array(
$props['supported-report-set'] = HTTP_WebDAV_Server::mkprop('supported-report-set',array(
HTTP_WebDAV_Server::mkprop('supported-report',array(
HTTP_WebDAV_Server::mkprop('report',array(
HTTP_WebDAV_Server::mkprop(groupdav::CARDDAV,'addressbook-query',''))))),
@ -514,7 +591,6 @@ class addressbook_groupdav extends groupdav_handler
HTTP_WebDAV_Server::mkprop('report',array(
HTTP_WebDAV_Server::mkprop(groupdav::CARDDAV,'addressbook-multiget',''))))),
));
//$props = self::current_user_privilege_set($props);
return $props;
}
@ -560,12 +636,22 @@ class addressbook_groupdav extends groupdav_handler
/**
* Read a contact
*
* We have to make sure to not return or even consider in read deleted contacts, as the might have
* the same UID and/or carddav_name as not deleted contacts and would block access to valid entries
*
* @param string|id $id
* @return array/boolean array with entry, false if no read rights, null if $id does not exist
*/
function read($id)
{
$contact = $this->bo->read(array(self::$path_attr => $id));
static $non_deleted_tids;
if (is_null($non_deleted_tids))
{
$non_deleted_tids = $this->bo->content_types;
unset($non_deleted_tids[addressbook_so::DELETED_TYPE]);
$non_deleted_tids = array_keys($non_deleted_tids);
}
$contact = $this->bo->read(array(self::$path_attr => $id, 'tid' => $non_deleted_tids));
if ($contact && $contact['tid'] == addressbook_so::DELETED_TYPE)
{

View File

@ -72,7 +72,7 @@ class addressbook_vcal extends addressbook_bo
'X-ASSISTANT' => array('assistent'),
'X-ASSISTANT-TEL' => array('tel_assistent'),
'UID' => array('uid'),
);
);
/**
* VCard version
@ -203,6 +203,8 @@ class addressbook_vcal extends addressbook_bo
#Horde::logMessage("vCalAddressbook clientProperties:\n" . print_r($this->clientProperties, true), __FILE__, __LINE__, PEAR_LOG_DEBUG);
$vCard = new Horde_iCalendar_vcard($this->version);
$vCard->setAttribute('PRODID','-//EGroupware//NONSGML EGroupware Addressbook '.$GLOBALS['egw_info']['apps']['addressbook']['version'].'//'.
strtoupper($GLOBALS['egw_info']['user']['preferences']['common']['lang']));
$sysCharSet = $GLOBALS['egw']->translation->charset();
@ -795,7 +797,7 @@ class addressbook_vcal extends addressbook_bo
{
$finalRowNames['TEL;OTHER'] = $vcardKey;
}
break;
break;
case 'TEL;PAGER;WORK':
case 'TEL;PAGER;HOME':
if (!in_array('TEL;PAGER', $rowNames)
@ -803,7 +805,7 @@ class addressbook_vcal extends addressbook_bo
{
$finalRowNames['TEL;PAGER'] = $vcardKey;
}
break;
break;
case 'TEL;CAR;VOICE':
case 'TEL;CAR;CELL':
case 'TEL;CAR;CELL;VOICE':
@ -966,7 +968,7 @@ class addressbook_vcal extends addressbook_bo
}
$this->fixup_contact($contact);
if ($this->log)
{
error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ .

View File

@ -1051,6 +1051,7 @@ class calendar_bo
'name' => trim($GLOBALS['egw']->accounts->id2name($uid,'account_firstname'). ' ' .
$GLOBALS['egw']->accounts->id2name($uid,'account_lastname')),
'type' => $GLOBALS['egw']->accounts->get_type($uid),
'app' => 'accounts',
);
}
else
@ -1063,6 +1064,7 @@ class calendar_bo
{
$info['email'] = $GLOBALS['egw']->accounts->id2name($info['responsible'],'account_email');
}
$info['app'] = $this->resources[$uid[0]]['app'];
}
}
$res_info_cache[$uid] = $info;
@ -1913,10 +1915,11 @@ class calendar_bo
* Get the etag for an entry
*
* @param array|int|string $event array with event or cal_id, or cal_id:recur_date for virtual exceptions
* @param string &$schedule_tag=null on return schedule-tag (egw_cal.cal_id:egw_cal.cal_etag, no participant modifications!)
* @param boolean $client_share_uid_excpetions Does client understand exceptions to be included in VCALENDAR component of series master sharing its UID
* @return string|boolean string with etag or false
*/
function get_etag($entry,$client_share_uid_excpetions=true)
function get_etag($entry, &$schedule_tag=null, $client_share_uid_excpetions=true)
{
if (!is_array($entry))
{
@ -1924,7 +1927,7 @@ class calendar_bo
if (!$this->check_perms(EGW_ACL_FREEBUSY, $entry, 0, 'server')) return false;
$entry = $this->read($entry, $recur_date, true, 'server');
}
$etag = $entry['id'].':'.$entry['etag'];
$etag = $schedule_tag = $entry['id'].':'.$entry['etag'];
// use new MAX(modification date) of egw_cal_user table (deals with virtual exceptions too)
if (isset($entry['max_user_modified']))
@ -1953,7 +1956,7 @@ class calendar_bo
{
if ($recurrence['reference'] && $recurrence['id'] != $entry['id']) // ignore series master
{
$etag .= ':'.$this->get_etag($recurrence);
$etag .= ':'.$this->get_etag($recurrence, $full_etag);
}
}
}

View File

@ -67,13 +67,11 @@ class calendar_groupdav extends groupdav_handler
* Constructor
*
* @param string $app 'calendar', 'addressbook' or 'infolog'
* @param int $debug=null debug-level to set
* @param string $base_uri=null base url of handler
* @param string $principalURL=null principal url of handler
* @param groupdav $groupdav calling class
*/
function __construct($app,$debug=null, $base_uri=null, $principalURL=null)
function __construct($app, groupdav $groupdav)
{
parent::__construct($app,$debug,$base_uri,$principalURL);
parent::__construct($app, $groupdav);
$this->bo = new calendar_boupdate();
$this->vCalendar = new Horde_iCalendar;
@ -125,6 +123,11 @@ class calendar_groupdav extends groupdav_handler
error_log(__METHOD__."($path,".array2string($options).",,$user,$id)");
}
if ($options['root']['name'] == 'free-busy-query')
{
return $this->free_busy_report($path, $options, $user);
}
// ToDo: add parameter to only return id & etag
$filter = array(
'users' => $user,
@ -144,6 +147,17 @@ class calendar_groupdav extends groupdav_handler
{
$filter['filter'] = 'owner';
}
// scheduling inbox, shows only not yet accepted or rejected events
elseif (substr($path,-7) == '/inbox/')
{
$filter['filter'] = 'unknown';
$filter['start'] = $this->bo->now; // only return future invitations
}
// ToDo: not sure what scheduling outbox is supposed to show, leave it empty for now
elseif (substr($path,-8) == '/outbox/')
{
return true;
}
else
{
$filter['filter'] = 'default'; // not rejected
@ -156,6 +170,8 @@ class calendar_groupdav extends groupdav_handler
// when trying to request not supported components, eg. VTODO on a calendar collection
return true;
}
if ($id) $path = dirname($path).'/'; // caldav_name get's added anyway in the callback
if ($this->debug > 1)
{
error_log(__METHOD__."($path,,,$user,$id) filter=".array2string($filter));
@ -222,30 +238,33 @@ class calendar_groupdav extends groupdav_handler
foreach($events as $event)
{
$event['max_user_modified'] = $max_user_modified[$event['id']];
$etag = $this->get_etag($event, $schedule_tag);
//header('X-EGROUPWARE-EVENT-'.$event['id'].': '.$event['title'].': '.date('Y-m-d H:i:s',$event['start']).' - '.date('Y-m-d H:i:s',$event['end']));
$props = array(
HTTP_WebDAV_Server::mkprop('getetag',$this->get_etag($event)),
HTTP_WebDAV_Server::mkprop('getcontenttype', $this->agent != 'kde' ?
'text/calendar; charset=utf-8; component=VEVENT' : 'text/calendar'),
// getlastmodified and getcontentlength are required by WebDAV and Cadaver eg. reports 404 Not found if not set
HTTP_WebDAV_Server::mkprop('getlastmodified', $event['modified']),
HTTP_WebDAV_Server::mkprop('resourcetype',''), // DAVKit requires that attribute!
'getcontenttype' => $this->agent != 'kde' ? 'text/calendar; charset=utf-8; component=VEVENT' : 'text/calendar',
'getetag' => '"'.$etag.'"',
'schedule-tag' => HTTP_WebDAV_Server::mkprop(groupdav::CALDAV, 'schedule-tag', '"'.$schedule_tag.'"'),
'getlastmodified' => max($event['modified'], $event['max_user_modified']),
);
//error_log(__FILE__ . __METHOD__ . "Calendar Data : $calendar_data");
if ($calendar_data)
{
$content = $this->iCal($event,$filter['users']);
$props[] = HTTP_WebDAV_Server::mkprop('getcontentlength',bytes($content));
$props[] = HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'calendar-data',$content);
$content = $this->iCal($event, $filter['users'], strpos($path, '/inbox/') !== false ? 'REQUEST' : null);
$props['getcontentlength'] = bytes($content);
$props['calendar-data'] = HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'calendar-data',$content);
}
else
/* Calendarserver reports new events with schedule-changes: action: create, which iCal request
* adding it, unfortunately does not lead to showing the new event in the users inbox
if (strpos($path, '/inbox/') !== false && $this->groupdav->prop_requested('schedule-changes'))
{
$props[] = HTTP_WebDAV_Server::mkprop('getcontentlength', ''); // expensive to calculate and no CalDAV client uses it
}
$files[] = array(
'path' => $path.$this->get_path($event),
'props' => $props,
);
$props['schedule-changes'] = HTTP_WebDAV_Server::mkprop(groupdav::CALENDARSERVER,'schedule-changes',array(
HTTP_WebDAV_Server::mkprop(groupdav::CALENDARSERVER,'dtstamp',gmdate('Ymd\THis',$event['created']).'Z'),
HTTP_WebDAV_Server::mkprop(groupdav::CALENDARSERVER,'action',array(
HTTP_WebDAV_Server::mkprop(groupdav::CALENDARSERVER,'create',''),
)),
));
}*/
$files[] = $this->add_resource($path, $event, $props);
}
}
if ($this->debug)
@ -333,6 +352,7 @@ class calendar_groupdav extends groupdav_handler
$cal_filters['end'] = $cal_end;
}
}
// multiget or propfind on a given id
//error_log(__FILE__ . __METHOD__ . "multiget of propfind:");
if ($options['root']['name'] == 'calendar-multiget' || $id)
@ -383,10 +403,13 @@ class calendar_groupdav extends groupdav_handler
{
return $event;
}
$options['data'] = $this->iCal($event,$user);
$options['data'] = $this->iCal($event, $user, strpos($options['path'], '/inbox/') !== false ? 'REQUEST' : null);
$options['mimetype'] = 'text/calendar; charset=utf-8';
header('Content-Encoding: identity');
header('ETag: '.$this->get_etag($event));
header('ETag: "'.$this->get_etag($event, $schedule_tag).'"');
header('Schedule-Tag: "'.$schedule_tag.'"');
return true;
}
@ -397,9 +420,10 @@ class calendar_groupdav extends groupdav_handler
*
* @param array $event
* @param int $user=null account_id of calendar to display
* @param string $method=null eg. 'PUBLISH' for inbox, nothing anywhere else
* @return string
*/
private function iCal(array $event,$user=null)
private function iCal(array $event,$user=null, $method=null)
{
static $handler = null;
if (is_null($handler)) $handler = $this->_get_handler();
@ -424,7 +448,7 @@ class calendar_groupdav extends groupdav_handler
{
$events[0]['uid'] .= '-'.$event['id']; // force a different uid
}
return $handler->exportVCal($events,'2.0','PUBLISH');
return $handler->exportVCal($events, '2.0', $method);
}
/**
@ -507,8 +531,14 @@ class calendar_groupdav extends groupdav_handler
if (!$prefix) $user = null; // /infolog/ does not imply setting the current user (for new entries it's done anyway)
// fix for iCal4OL using WinHTTP only supporting a certain header length
if (isset($_SERVER['HTTP_IF_SCHEDULE']) && !isset($_SERVER['HTTP_IF_SCHEDULE_TAG_MATCH']))
{
$_SERVER['HTTP_IF_SCHEDULE_TAG_MATCH'] = $_SERVER['HTTP_IF_SCHEDULE'];
}
$return_no_access = true; // as handled by importVCal anyway and allows it to set the status for participants
$oldEvent = $this->_common_get_put_delete('PUT',$options,$id,$return_no_access);
$oldEvent = $this->_common_get_put_delete('PUT',$options,$id,$return_no_access,
isset($_SERVER['HTTP_IF_SCHEDULE_TAG_MATCH'])); // dont fail with 412 Precondition Failed in that case
if (!is_null($oldEvent) && !is_array($oldEvent))
{
if ($this->debug) error_log(__METHOD__.': '.print_r($oldEvent,true).function_backtrace());
@ -548,6 +578,39 @@ class calendar_groupdav extends groupdav_handler
if (is_array($oldEvent))
{
$eventId = $oldEvent['id'];
// client specified a CalDAV Scheduling schedule-tag precondition
if (isset($_SERVER['HTTP_IF_SCHEDULE_TAG_MATCH']))
{
$schedule_tag_match = $_SERVER['HTTP_IF_SCHEDULE_TAG_MATCH'];
if ($schedule_tag_match[0] == '"') $schedule_tag_match = substr($schedule_tag_match, 1, -1);
$this->get_etag($oldEvent, $schedule_tag);
if ($schedule_tag_match !== $schedule_tag)
{
return '412 Precondition Failed';
}
// update only participant status and alarms of current user
if (($events = $handler->icaltoegw($vCalendar)))
{
// todo check behavior for recuring events
foreach($events as $event)
{
if ($this->debug) error_log(__METHOD__."(, $id, $user, '$prefix') eventId=$eventId, user=$user, event=".array2string($event));
if ($event['participants'][$user] != $oldEvent['participants'][$user] &&
!$this->bo->set_status($eventId, $user, $event['participants'][$user], $event['recurrence']))
{
if ($this->debug) error_log(__METHOD__."(,,$user) failed to set_status($eventId, $user, '{$event['participants'][$user]}')");
return '403 Forbidden';
}
if ($this->debug) error_log(__METHOD__."() set_status($eventId, $user, ".array2string($event['participants'][$user])." , $event[recurrence])");
// import alarms
$this->sync_alarms($eventId, (array)$event['alarm'], (array)$oldEvent['alarm'], $user, $event['start']);
}
header('Schedule-Tag: "'.$schedule_tag.'"');
return '204 No Content';
}
}
if ($return_no_access)
{
$retval = true;
@ -576,7 +639,7 @@ class calendar_groupdav extends groupdav_handler
}
if (!($cal_id = $handler->importVCal($vCalendar, $eventId,
self::etag2value($this->http_if_match), false, 0, $this->principalURL, $user, $charset, $id)))
self::etag2value($this->http_if_match), false, 0, $this->groupdav->current_user_principal, $user, $charset, $id)))
{
if ($this->debug) error_log(__METHOD__."(,$id) eventId=$eventId: importVCal('$options[content]') returned false");
if ($eventId && $cal_id === false)
@ -591,7 +654,10 @@ class calendar_groupdav extends groupdav_handler
}
}
header('ETag: '.$this->get_etag($cal_id));
$etag = $this->get_etag($cal_id, $schedule_tag);
// we should not return an etag here, as we never store the PUT ical byte-by-byte
//header('ETag: "'.$etag.'"');
header('Schedule-Tag: "'.$schedule_tag.'"');
// send GroupDAV Location header only if we dont use caldav_name as path-attribute
if ($retval !== true && self::$path_attr != 'caldav_name')
@ -603,6 +669,49 @@ class calendar_groupdav extends groupdav_handler
return $retval;
}
/**
* Sync alarms of current user: add alarms added on client and remove the ones removed
*
* @param int $cal_id of event to set alarms
* @param array $alarms
* @param array $old_alarms
* @param int $user account_id of user to create alarm for
* @param int $start start-time of event
* @ToDo store other alarm properties like: ACTION, DESCRIPTION, X-WR-ALARMUID
*/
private function sync_alarms($cal_id, array $alarms, array $old_alarms, $user, $start)
{
if ($this->debug) error_log(__METHOD__."($cal_id, ".array2string($alarms).', '.array2string($old_alarms).", $user, $start)");
// todo import alarms
foreach($alarms as $alarm)
{
if ($alarm['owner'] != $this->user) continue; // only import alarms of current user
// check if alarm is already stored or from other users
foreach($old_alarms as $id => $old_alarm)
{
if ($old_alarm['owner'] != $user || $alarm['offset'] == $old_alarm['offset'])
{
unset($old_alarms[$id]); // remove alarms of other user, or already existing alarms
}
}
// alarm not found --> add it
if ($alarm['offset'] != $old_alarm['offset'] || $old_alarm['owner'] != $user)
{
$alarm['owner'] = $user;
$alarm['time'] = $start - $alarm['offset'];
if ($this->debug) error_log(__METHOD__."() adding new alarm from client ".array2string($alarm));
$this->bo->save_alarm($cal_id, $alarm);
}
}
// remove all old alarms left from current user
foreach($old_alarms as $id => $old_alarm)
{
if ($this->debug) error_log(__METHOD__."() deleting alarm '$id' deleted on client ".array2string($old_alarm));
$this->bo->delete_alarm($id);
}
}
/**
* Handle post request for a schedule entry
*
@ -615,46 +724,200 @@ class calendar_groupdav extends groupdav_handler
{
if ($this->debug) error_log(__METHOD__."($id, $user)".print_r($options,true));
if (preg_match('/^METHOD:(PUBLISH|REQUEST)(\r\n|\r|\n)(.*)^BEGIN:VEVENT/ism', $options['content']))
$vCalendar = htmlspecialchars_decode($options['content']);
$charset = null;
if (!empty($options['content_type']))
{
$handler = $this->_get_handler();
$vCalendar = htmlspecialchars_decode($options['content']);
$charset = null;
if (!empty($options['content_type']))
$content_type = explode(';', $options['content_type']);
if (count($content_type) > 1)
{
$content_type = explode(';', $options['content_type']);
if (count($content_type) > 1)
array_shift($content_type);
foreach ($content_type as $attribute)
{
array_shift($content_type);
foreach ($content_type as $attribute)
trim($attribute);
list($key, $value) = explode('=', $attribute);
switch (strtolower($key))
{
trim($attribute);
list($key, $value) = explode('=', $attribute);
switch (strtolower($key))
{
case 'charset':
$charset = strtoupper(substr($value,1,-1));
}
case 'charset':
$charset = strtoupper(substr($value,1,-1));
}
}
}
}
if (substr($options['path'],-8) == '/outbox/')
{
if (preg_match('/^METHOD:REQUEST(\r\n|\r|\n)(.*)^BEGIN:VFREEBUSY/ism', $vCalendar))
{
if ($user != $GLOBALS['egw_info']['user']['account_id'])
{
error_log(__METHOD__."() freebusy request only allowed to own outbox!");
return '403 Forbidden';
}
// do freebusy request
return $this->outbox_freebusy_request($vCalendar, $charset, $user, $options);
}
else
{
// POST to deliver an invitation, containing http headers:
// Originator: mailto:<organizer-email>
// Recipient: mailto:<attendee-email>
// --> currently we simply ignore these posts, as EGroupware does it's own notifications based on user preferences
return '204 No Content';
}
}
if (preg_match('/^METHOD:(PUBLISH|REQUEST)(\r\n|\r|\n)(.*)^BEGIN:VEVENT/ism', $options['content']))
{
$handler = $this->_get_handler();
if (($foundEvents = $handler->search($vCalendar, null, false, $charset)))
{
$eventId = array_shift($foundEvents);
list($eventId) = explode(':', $eventId);
if (!($cal_id = $handler->importVCal($vCalendar, $eventId, null,
false, 0, $this->principalURL, $user, $charset)))
false, 0, $this->groupdav->current_user_principal, $user, $charset)))
{
if ($this->debug) error_log(__METHOD__."() importVCal($eventId) returned false");
}
header('ETag: '.$this->get_etag($eventId));
// we should not return an etag here, as we never store the ical byte-by-byte
//header('ETag: "'.$this->get_etag($eventId).'"');
}
}
return true;
}
/**
* Handle outbox freebusy request
*
* @param string $ical
* @param string $charset of ical
* @param int $user account_id of owner
* @param array &$options
* @return mixed boolean true on success, false on failure or string with http status (eg. '404 Not Found')
*/
protected function outbox_freebusy_request($ical, $charset, $user, array &$options)
{
include_once EGW_SERVER_ROOT.'/phpgwapi/inc/horde/lib/core.php';
$vcal = new Horde_iCalendar();
if (!$vcal->parsevCalendar($ical, 'VCALENDAR', $charset))
{
return '400 Bad request';
}
$version = $vcal->getAttribute('VERSION');
//echo $ical."\n";
$handler = $this->_get_handler();
$handler->setSupportedFields('groupdav');
$handler->calendarOwner = $handler->user = 0; // to NOT default owner/organizer to something
if (!($component = $vcal->getComponent(0)) ||
!($event = $handler->vevent2egw($component, $version, $handler->supportedFields, $this->groupdav->current_user_principal, 'Horde_iCalendar_vfreebusy')))
{
return '400 Bad request';
}
if ($event['owner'] != $user)
{
error_log(__METHOD__."('$ical',,$user) ORGANIZER is NOT principal!");
return '403 Forbidden';
}
//print_r($event);
$organizer = $component->getAttribute('ORGANIZER');
$attendees = (array)$component->getAttribute('ATTENDEE');
// X-CALENDARSERVER-MASK-UID specifies to exclude given event from busy-time
$mask_uid = $component->getAttribute('X-CALENDARSERVER-MASK-UID');
header('Content-type: text/xml; charset=UTF-8');
$xml = new XMLWriter;
$xml->openMemory();
$xml->startDocument('1.0', 'UTF-8');
$xml->startElementNs('C', 'schedule-response', groupdav::CALDAV);
foreach($event['participants'] as $uid => $status)
{
$xml->startElementNs('C', 'response', null);
$xml->startElementNs('C', 'recipient', null);
$xml->writeElementNs('D', 'href', 'DAV:', $attendee=array_shift($attendees));
$xml->endElement(); // recipient
$xml->writeElementNs('C', 'request-status', null, '2.0;Success');
$xml->writeElementNs('C', 'calendar-data', null,
$handler->freebusy($uid, $event['end'], true, 'utf-8', $event['start'], 'REPLY', array(
'UID' => $event['uid'],
'ORGANIZER' => $organizer,
'ATTENDEE' => $attendee,
)+(empty($mask_uid) || !is_string($mask_uid) ? array() : array(
'X-CALENDARSERVER-MASK-UID' => $mask_uid,
))));
$xml->endElement(); // response
}
$xml->endElement(); // schedule-response
$xml->endDocument();
echo $xml->outputMemory();
return true;
}
/**
* Handle free-busy-query report
*
* @param string $path
* @param array $options
* @param int $user account_id
* @return mixed boolean true on success, false on failure or string with http status (eg. '404 Not Found')
*/
function free_busy_report($path,$options,$user)
{
if (!$this->bo->check_perms(EGW_ACL_FREEBUSY, 0, $user))
{
return '403 Forbidden';
}
foreach($options['other'] as $filter)
{
if ($filter['name'] == 'time-range')
{
$start = $this->vCalendar->_parseDateTime($filter['attrs']['start']);
$end = $this->vCalendar->_parseDateTime($filter['attrs']['end']);
}
}
$handler = $this->_get_handler();
header('Content-Type: text/calendar');
echo $handler->freebusy($user, $end, true, 'utf-8', $start, 'REPLY', array());
common::egw_exit(); // otherwise we get a 207 multistatus, not 200 Ok
}
/**
* Return priviledges for current user, default is read and read-current-user-privilege-set
*
* Reimplemented to add read-free-busy and schedule-deliver privilege
*
* @param string $path path of collection
* @param int $user=null owner of the collection, default current user
* @return array with privileges
*/
public function current_user_privileges($path, $user=null)
{
$priviledes = parent::current_user_privileges($user);
if ($this->bo->check_perms(EGW_ACL_FREEBUSY, 0, $user))
{
$priviledes['read-free-busy'] = HTTP_WebDAV_Server::mkprop(groupdav::CALDAV, 'read-free-busy', '');
if (substr($path, -8) == '/outbox/' && $this->bo->check_acl_invite($user))
{
$priviledes['schedule-send'] = HTTP_WebDAV_Server::mkprop(groupdav::CALDAV, 'schedule-send', '');
}
}
if (substr($path, -7) == '/inbox/' && $this->bo->check_acl_invite($user))
{
$priviledes['schedule-deliver'] = HTTP_WebDAV_Server::mkprop(groupdav::CALDAV, 'schedule-deliver', '');
}
return $priviledes;
}
/**
* Fix event series with exceptions, called by calendar_ical::importVCal():
* a) only series master = first event got cal_id from URL
@ -665,9 +928,6 @@ class calendar_groupdav extends groupdav_handler
*/
static function fix_series(array &$events)
{
//foreach($events as $n => $event) error_log(__METHOD__." $n before: ".array2string($event));
//$master =& $events[0];
$bo = new calendar_boupdate();
// get array with orginal recurrences indexed by recurrence-id
@ -740,24 +1000,46 @@ class calendar_groupdav extends groupdav_handler
*/
function delete(&$options,$id)
{
if (strpos($options['path'], '/inbox/') !== false)
{
return true; // simply ignore DELETE in inbox for now
}
$return_no_access = true; // to allow to check if current use is a participant and reject the event for him
if (!is_array($event = $this->_common_get_put_delete('DELETE',$options,$id,$return_no_access)) || !$return_no_access)
{
if (!$return_no_access)
if (!$return_no_access)
{
$ret = isset($event['participants'][$this->bo->user]) &&
$this->bo->set_status($event,$this->bo->user,'R') ? true : '403 Forbidden';
if ($this->debug) error_log(__METHOD__."(,$id) return_no_access=$return_no_access, event[participants]=".array2string($event['participants']).", user={$this->bo->user} --> return $ret");
return $ret;
// check if user is a participant or one of the groups he is a member of --> reject the meeting request
$ret = '403 Forbidden';
$memberships = $GLOBALS['egw']->accounts->memberships($this->bo->user, true);
foreach($event['participants'] as $uid => $status)
{
if ($this->bo->user == $uid || in_array($uid, $memberships))
{
if ($this->bo->set_status($event,$this->bo->user, 'R')) $ret = true;
break;
}
}
}
else
{
$ret = $event;
}
return $event;
}
return $this->bo->delete($event['id']);
else
{
$ret = $this->bo->delete($event['id']);
}
if ($this->debug) error_log(__METHOD__."(,$id) return_no_access=$return_no_access, event[participants]=".array2string(is_array($event)?$event['participants']:null).", user={$this->bo->user} --> return ".array2string($ret));
return $ret;
}
/**
* Read an entry
*
* We have to make sure to not return or even consider in read deleted events, as the might have
* the same UID and/or caldav_name as not deleted events and would block access to valid entries
*
* @param string|id $id
* @return array|boolean array with entry, false if no read rights, null if $id does not exist
*/
@ -765,7 +1047,7 @@ class calendar_groupdav extends groupdav_handler
{
if (strpos($column=self::$path_attr,'_') === false) $column = 'cal_'.$column;
$event = $this->bo->read(array($column => $id), null, true, 'server');
$event = $this->bo->read(array($column => $id, 'cal_deleted IS NULL'), null, true, 'server');
if ($event) $event = array_shift($event); // read with array as 1. param, returns an array of events!
if (!($retval = $this->bo->check_perms(EGW_ACL_FREEBUSY,$event, 0, 'server')))
@ -796,28 +1078,28 @@ class calendar_groupdav extends groupdav_handler
if ($this->debug > 1) error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. "($path)[$user] = $ctag");
return 'EGw-'.$ctag.'-wGE';
return $ctag;
}
/**
* Get the etag for an entry, reimplemented to include the participants and stati in the etag
* Get the etag for an entry
*
* @param array/int $event array with event or cal_id
* @return string/boolean string with etag or false
* @param array|int $event array with event or cal_id
* @return string|boolean string with etag or false
*/
function get_etag($entry)
function get_etag($entry, &$schedule_tag=null)
{
$etag = $this->bo->get_etag($entry,$this->client_shared_uid_exceptions);
$etag = $this->bo->get_etag($entry, $schedule_tag, $this->client_shared_uid_exceptions);
//error_log(__METHOD__ . "($entry[id] ($entry[etag]): $entry[title] --> etag=$etag");
return 'EGw-'.$etag.'-wGE';
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
* @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)
@ -830,85 +1112,43 @@ class calendar_groupdav extends groupdav_handler
return $this->bo->check_perms($acl,$event,0,'server');
}
/**
* Add the privileges of the current user
*
* @param array $props=array() regular props by the groupdav handler
* @return array
*/
static function current_user_privilege_set(array $props=array())
{
$props[] = HTTP_WebDAV_Server::mkprop(groupdav::DAV,'current-user-privilege-set',
array(HTTP_WebDAV_Server::mkprop(groupdav::DAV,'privilege',
array(
HTTP_WebDAV_Server::mkprop(groupdav::DAV,'read',''),
HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'read-free-busy',''),
HTTP_WebDAV_Server::mkprop(groupdav::DAV,'read-current-user-privilege-set',''),
HTTP_WebDAV_Server::mkprop(groupdav::DAV,'bind',''),
HTTP_WebDAV_Server::mkprop(groupdav::DAV,'unbind',''),
HTTP_WebDAV_Server::mkprop(groupdav::DAV,'schedule-post',''),
HTTP_WebDAV_Server::mkprop(groupdav::DAV,'schedule-post-vevent',''),
HTTP_WebDAV_Server::mkprop(groupdav::DAV,'schedule-respond',''),
HTTP_WebDAV_Server::mkprop(groupdav::DAV,'schedule-respond-vevent',''),
HTTP_WebDAV_Server::mkprop(groupdav::DAV,'schedule-deliver',''),
HTTP_WebDAV_Server::mkprop(groupdav::DAV,'schedule-deliver-vevent',''),
HTTP_WebDAV_Server::mkprop(groupdav::DAV,'write',''),
HTTP_WebDAV_Server::mkprop(groupdav::DAV,'write-properties',''),
HTTP_WebDAV_Server::mkprop(groupdav::DAV,'write-content',''),
))));
return $props;
}
/**
* Add extra properties for calendar collections
*
* @param array $props=array() regular props by the groupdav handler
* @param string $displayname
* @param string $base_uri=null base url of handler
* @param int $user=null account_id of owner of current collection
* @return array
*/
static function extra_properties(array $props=array(), $displayname, $base_uri=null)
public function extra_properties(array $props=array(), $displayname, $base_uri=null, $user=null)
{
// calendar description
$props[] = HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'calendar-description',$displayname);
/*
// BOX URLs of the current user
$props[] = HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'schedule-inbox-URL',
array(HTTP_WebDAV_Server::mkprop(self::DAV,'href',$base_uri.'/calendar/')));
$props[] = HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'schedule-outbox-URL',
array(HTTP_WebDAV_Server::mkprop(groupdav::DAV,'href',$base_uri.'/calendar/')));
$props[] = HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'schedule-default-calendar-URL',
array(HTTP_WebDAV_Server::mkprop(groupdav::DAV,'href',$base_uri.'/calendar/')));
$props[] = HTTP_WebDAV_Server::mkprop(groupdav::CALENDARSERVER,'dropbox-home-URL',
array(HTTP_WebDAV_Server::mkprop(groupdav::DAV,'href',$base_uri.'/calendar/')));
$props[] = HTTP_WebDAV_Server::mkprop(groupdav::CALENDARSERVER,'notifications-URL',
array(HTTP_WebDAV_Server::mkprop(groupdav::DAV,'href',$base_uri.'/calendar/')));
*/
// email of the current user, see caldav-sheduling draft
$props[] = HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'calendar-user-address-set',array(
HTTP_WebDAV_Server::mkprop('href','MAILTO:'.$GLOBALS['egw_info']['user']['email']),
HTTP_WebDAV_Server::mkprop('href',$base_uri.'/principals/users/'.$GLOBALS['egw_info']['user']['account_lid'].'/'),
HTTP_WebDAV_Server::mkprop('href','urn:uuid:'.$GLOBALS['egw_info']['user']['account_lid'])));
//$props[] = HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'calendar-user-address-set',array(
// HTTP_WebDAV_Server::mkprop('href','MAILTO:'.$GLOBALS['egw_info']['user']['email'])));
$props['calendar-description'] = HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'calendar-description',$displayname);
// supported components, currently only VEVENT
$props[] = HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'supported-calendar-component-set',array(
$props['supported-calendar-component-set'] = HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'supported-calendar-component-set',array(
HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'comp',array('name' => 'VCALENDAR')),
HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'comp',array('name' => 'VTIMEZONE')),
HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'comp',array('name' => 'VEVENT')),
// HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'comp',array('name' => 'VTODO')), // not yet supported
));
$props[] = HTTP_WebDAV_Server::mkprop('supported-report-set',array(
$props['supported-report-set'] = HTTP_WebDAV_Server::mkprop('supported-report-set',array(
HTTP_WebDAV_Server::mkprop('supported-report',array(
HTTP_WebDAV_Server::mkprop('report',array(
HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'calendar-multiget','')))))));
$props[] = HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'supported-calendar-data',array(
HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'calendar-query',''))),
HTTP_WebDAV_Server::mkprop('report',array(
HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'calendar-multiget',''))),
HTTP_WebDAV_Server::mkprop('report',array(
HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'free-busy-query',''))),
))));
$props['supported-calendar-data'] = HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'supported-calendar-data',array(
HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'calendar-data', array('content-type' => 'text/calendar', 'version'=> '2.0')),
HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'calendar-data', array('content-type' => 'text/x-calendar', 'version'=> '1.0'))));
//$props[] = HTTP_WebDAV_Server::mkprop(groupdav::CALENDARSERVER,'publish-url',array(
// HTTP_WebDAV_Server::mkprop('href',$base_uri.'/calendar/')));
//$props = self::current_user_privilege_set($props);
// get timezone of calendar
if ($this->groupdav->prop_requested('calendar-timezone'))
{
$props['calendar-timezone'] = HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'calendar-timezone',
calendar_timezones::user_timezone($user));
}
return $props;
}
@ -925,4 +1165,3 @@ class calendar_groupdav extends groupdav_handler
return $handler;
}
}

View File

@ -228,10 +228,10 @@ class calendar_ical extends calendar_boupdate
}
$vcal = new Horde_iCalendar;
$vcal->setAttribute('PRODID','-//eGroupWare//NONSGML eGroupWare Calendar '.$GLOBALS['egw_info']['apps']['calendar']['version'].'//'.
$vcal->setAttribute('PRODID','-//EGroupware//NONSGML EGroupware Calendar '.$GLOBALS['egw_info']['apps']['calendar']['version'].'//'.
strtoupper($GLOBALS['egw_info']['user']['preferences']['common']['lang']));
$vcal->setAttribute('VERSION', $version);
$vcal->setAttribute('METHOD', $method);
if ($method) $vcal->setAttribute('METHOD', $method);
$events_exported = false;
if (!is_array($events)) $events = array($events);
@ -354,42 +354,15 @@ class calendar_ical extends calendar_boupdate
if ($tzid && $tzid != 'UTC' && !in_array($tzid,$vtimezones_added))
{
// check if we have vtimezone component data for tzid of event, if not default to user timezone (default to server tz)
if (!($vtimezone = calendar_timezones::tz2id($tzid,'component')))
if (calendar_timezones::add_vtimezone($vcal, $tzid) ||
!in_array($tzid = egw_time::$user_timezone->getName(), $vtimezones_added) &&
calendar_timezones::add_vtimezone($vcal, $tzid))
{
error_log(__METHOD__."() unknown TZID='$tzid', defaulting to user timezone '".egw_time::$user_timezone->getName()."'!");
$vtimezone = calendar_timezones::tz2id($tzid=egw_time::$user_timezone->getName(),'component');
$tzid = null;
}
if (!isset(self::$tz_cache[$tzid]))
{
self::$tz_cache[$tzid] = calendar_timezones::DateTimeZone($tzid);
}
//error_log("in_array('$tzid',\$vtimezones_added)=".array2string(in_array($tzid,$vtimezones_added)).", component=$vtimezone");;
if (!in_array($tzid,$vtimezones_added))
{
// $vtimezone is a string with a single VTIMEZONE component, afaik Horde_iCalendar can not add it directly
// --> we have to parse it and let Horde_iCalendar add it again
$horde_vtimezone = Horde_iCalendar::newComponent('VTIMEZONE',$container=false);
$horde_vtimezone->parsevCalendar($vtimezone,'VTIMEZONE');
// DTSTART must be in local time!
$standard = $horde_vtimezone->findComponent('STANDARD');
if (is_a($standard, 'Horde_iCalendar'))
{
$dtstart = $standard->getAttribute('DTSTART');
$dtstart = new egw_time($dtstart, egw_time::$server_timezone);
$dtstart->setTimezone(self::$tz_cache[$tzid]);
$standard->setAttribute('DTSTART', $dtstart->format('Ymd\THis'), array(), false);
}
$daylight = $horde_vtimezone->findComponent('DAYLIGHT');
if (is_a($daylight, 'Horde_iCalendar'))
{
$dtstart = $daylight->getAttribute('DTSTART');
$dtstart = new egw_time($dtstart, egw_time::$server_timezone);
$dtstart->setTimezone(self::$tz_cache[$tzid]);
$daylight->setAttribute('DTSTART', $dtstart->format('Ymd\THis'), array(), false);
}
$vcal->addComponent($horde_vtimezone);
$vtimezones_added[] = $tzid;
if (!isset(self::$tz_cache[$tzid]))
{
self::$tz_cache[$tzid] = calendar_timezones::DateTimeZone($tzid);
}
}
}
if ($this->productManufacturer != 'file' && $this->uidExtension)
@ -434,7 +407,6 @@ class calendar_ical extends calendar_boupdate
}
$event['recur_exception'] = $exceptions;
}
foreach ($egwSupportedFields as $icalFieldName => $egwFieldName)
{
if (!isset($this->supportedFields[$egwFieldName]))
@ -451,13 +423,14 @@ class calendar_ical extends calendar_boupdate
switch ($icalFieldName)
{
case 'ATTENDEE':
$attendees = count($event['participants']);
foreach ((array)$event['participants'] as $uid => $status)
{
calendar_so::split_status($status, $quantity, $role);
if ($attendees == 1 &&
$uid == $this->user && $status == 'A') continue;
// do not include event owner/ORGANIZER as participant in his own calendar, if he is only participant
if (count($event['participants']) == 1 && $event['owner'] == $uid) continue;
if (!($info = $this->resource_info($uid))) continue;
if ($this->log)
{
error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ .
@ -491,11 +464,7 @@ class calendar_ical extends calendar_boupdate
{
case 'g':
$cutype = 'GROUP';
if ($this->productManufacturer == 'groupdav')
{
$participantURL = 'invalid:nomail';
$cutype = 'INDIVIDUAL';
}
$participantURL = 'urn:uuid:'.common::generate_uid('accounts', substr($uid, 1));
$members = $GLOBALS['egw']->accounts->members($uid, true);
if (!isset($event['participants'][$this->user]) && in_array($this->user, $members))
{
@ -508,13 +477,13 @@ class calendar_ical extends calendar_boupdate
'CUTYPE' => 'INDIVIDUAL',
'RSVP' => 'TRUE',
'X-EGROUPWARE-UID' => $this->user,
'EMAIL' => $user['email'],
);
);
$event['participants'][$this->user] = true;
}
break;
case 'r':
$cutype = 'RESOURCE';
$participantURL = 'urn:uuid:'.common::generate_uid('resources', substr($uid, 1));
$cutype = groupdav_principals::resource_is_location(substr($uid, 1)) ? 'ROOM' : 'RESOURCE';
break;
case 'u': // account
case 'c': // contact
@ -525,6 +494,11 @@ class calendar_ical extends calendar_boupdate
$cutype = 'UNKNOWN';
break;
};
// generate urn:uuid, if we have no other participant URL
if (empty($participantURL) && $info && $info['app'])
{
$participantURL = 'urn:uuid:'.common::generate_uid($info['app'], substr($uid, 1));
}
// ROLE={CHAIR|REQ-PARTICIPANT|OPT-PARTICIPANT|NON-PARTICIPANT|X-*}
$options = array();
if (!empty($participantCN)) $options['CN'] = $participantCN;
@ -532,10 +506,13 @@ class calendar_ical extends calendar_boupdate
if (!empty($status)) $options['PARTSTAT'] = $status;
if (!empty($cutype)) $options['CUTYPE'] = $cutype;
if (!empty($rsvp)) $options['RSVP'] = $rsvp;
if (!empty($info['email'])) $options['EMAIL'] = $info['email'];
if (!empty($info['email']) && $participantURL != 'MAILTO:'.$info['email'])
{
$options['EMAIL'] = $info['email']; // only add EMAIL attribute, if not already URL, as eg. Akonadi is reported to have problems with it
}
if ($info['type'] != 'e') $options['X-EGROUPWARE-UID'] = $uid;
if ($quantity > 1) $options['X-EGROUPWARE-QUANTITY'] = $quantity;
$attributes['ATTENDEE'][] = $participantURL;
$attributes['ATTENDEE'][] = $participantURL;
$parameters['ATTENDEE'][] = $options;
}
break;
@ -545,7 +522,6 @@ class calendar_ical extends calendar_boupdate
break;
case 'ORGANIZER':
// according to iCalendar standard, ORGANIZER not used for events in the own calendar
if (!$organizerCN)
{
$organizerCN = '"' . trim($GLOBALS['egw']->accounts->id2name($event['owner'],'account_firstname')
@ -575,7 +551,8 @@ class calendar_ical extends calendar_boupdate
$parameters['ATTENDEE'][] = $options;
}
}
if ($this->productManufacturer != 'groupdav' || !$this->check_perms(EGW_ACL_EDIT,$event))
// do NOT use ORGANIZER for events without further participants or a different organizer
if (count($event['participants']) > 1 || !isset($event['participants'][$event['owner']]))
{
$attributes['ORGANIZER'] = $organizerURL;
$parameters['ORGANIZER']['CN'] = $organizerCN;
@ -871,7 +848,11 @@ class calendar_ical extends calendar_boupdate
{
$attributes['CREATED'] = $event['created'] ? $event['created'] : $event['modified'];
}
if ($event['modified'])
if ($event['max_user_modified'])
{
$attributes['LAST-MODIFIED'] = max($event['modified'], $event['max_user_modified']);
}
elseif ($event['modified'])
{
$attributes['LAST-MODIFIED'] = $event['modified'];
}
@ -966,12 +947,12 @@ class calendar_ical extends calendar_boupdate
{
foreach (is_array($value) && $parameters[$key]['VALUE']!='DATE' ? $value : array($value) as $valueID => $valueData)
{
$valueData = $GLOBALS['egw']->translation->convert($valueData,$GLOBALS['egw']->translation->charset(),$charset);
$paramData = (array) $GLOBALS['egw']->translation->convert(is_array($value) ?
$valueData = translation::convert($valueData,translation::charset(),$charset);
$paramData = (array) translation::convert(is_array($value) ?
$parameters[$key][$valueID] : $parameters[$key],
$GLOBALS['egw']->translation->charset(),$charset);
$valuesData = (array) $GLOBALS['egw']->translation->convert($values[$key],
$GLOBALS['egw']->translation->charset(),$charset);
translation::charset(),$charset);
$valuesData = (array) translation::convert($values[$key],
translation::charset(),$charset);
$content = $valueData . implode(';', $valuesData);
if (preg_match('/[^\x20-\x7F]/', $content) ||
@ -1286,7 +1267,9 @@ class calendar_ical extends calendar_boupdate
// participants OR the event no longer contains participants, add them back
unset($event['participants']);
}
else
// since we export now all participants in CalDAV as urn:uuid, if they have no email,
// we dont need and dont want that special treatment anymore, as it keeps client from changing resources
elseif ($this->productManufacturer != 'groupdav')
{
foreach ($event_info['stored_event']['participants'] as $uid => $status)
{
@ -1339,6 +1322,13 @@ class calendar_ical extends calendar_boupdate
$event['owner'] = $event_info['stored_event']['owner'];
}
$event['caldav_name'] = $event_info['stored_event']['caldav_name'];
// as we no longer export event owner/ORGANIZER as only participant, we have to re-add owner as participant
// to not loose him, as EGroupware knows events without owner/ORGANIZER as participant
if (isset($event_info['stored_event']['participants'][$event['owner']]) && !isset($event['participant'][$event['owner']]))
{
$event['participant'][$event['owner']] = $event_info['stored_event']['participants'][$event['owner']];
}
}
else // common adjustments for new events
{
@ -1382,11 +1372,15 @@ class calendar_ical extends calendar_boupdate
if (!$event['participants']
|| !is_array($event['participants'])
|| !count($event['participants']))
|| !count($event['participants'])
// for new events, allways add owner as participant. Users expect to participate too, if they invite further participants.
// They can now only remove themselfs, if that is desired, after storing the event first.
|| !isset($event['participants'][$event['owner']]))
{
$status = $event['owner'] == $this->user ? 'A' : 'U';
$status = calendar_so::combine_status($status, 1, 'CHAIR');
$event['participants'] = array($event['owner'] => $status);
if (!is_array($event['participants'])) $event['participants'] = array();
$event['participants'][$event['owner']] = $status;
}
else
{
@ -2232,13 +2226,14 @@ class calendar_ical extends calendar_boupdate
* @param array $component VEVENT
* @param string $version vCal version (1.0/2.0)
* @param array $supportedFields supported fields of the device
* @param string $principalURL='' Used for CalDAV imports
* @param string $principalURL='' Used for CalDAV imports, no longer used in favor of groupdav_principals::url2uid()
* @param string $check_component='Horde_iCalendar_vevent'
*
* @return array|boolean event on success, false on failure
*/
function vevent2egw(&$component, $version, $supportedFields, $principalURL='')
function vevent2egw(&$component, $version, $supportedFields, $principalURL='', $check_component='Horde_iCalendar_vevent')
{
if (!is_a($component, 'Horde_iCalendar_vevent'))
if ($check_component && !is_a($component, $check_component))
{
if ($this->log)
{
@ -2354,6 +2349,16 @@ class calendar_ical extends calendar_boupdate
{
switch ($attributes['name'])
{
case 'DURATION': // clients can use DTSTART+DURATION, instead of DTSTART+DTEND
if (!isset($vcardData['end']))
{
$vcardData['end'] = $vcardData['start'] + $attributes['value'];
}
else
{
error_log(__METHOD__."() find DTEND AND DURATION --> ignoring DURATION");
}
break;
case 'AALARM':
case 'DALARM':
$alarmTime = $attributes['value'];
@ -2702,29 +2707,6 @@ class calendar_ical extends calendar_boupdate
{
$role = $attributes['params']['ROLE'];
}
// try pricipal url from CalDAV
if (strpos($attributes['value'], 'http') === 0)
{
if (!empty($principalURL) && strstr($attributes['value'], $principalURL) !== false)
{
$uid = $this->user;
if ($this->log)
{
error_log(__FILE__.'['.__LINE__.'] '.__METHOD__
. "(): Found myself: '$uid'\n",3,$this->logfile);
}
}
else
{
if ($this->log)
{
error_log(__FILE__.'['.__LINE__.'] '.__METHOD__
. '(): Unknown URI: ' . $attributes['value']
. "\n",3,$this->logfile);
}
$attributes['value'] = '';
}
}
// parse email and cn from attendee
if (preg_match('/MAILTO:([@.a-z0-9_-]+)|MAILTO:"?([.a-z0-9_ -]*)"?[ ]*<([@.a-z0-9_-]*)>/i',
$attributes['value'],$matches))
@ -2764,6 +2746,9 @@ class calendar_ical extends calendar_boupdate
// we use the current user
$uid = $this->user;
}
// check principal url from CalDAV here after X-EGROUPWARE-UID and to get optional X-EGROUPWARE-QUANTITY
if (!$uid) $uid = groupdav_principals::url2uid($attributes['value']);
// try to find an email address
if (!$uid && $email && ($uid = $GLOBALS['egw']->accounts->name2id($email, 'account_email')))
{
@ -3046,52 +3031,47 @@ class calendar_ical extends calendar_boupdate
* @param mixed $end=null end-date, default now+1 month
* @param boolean $utc=true if false, use severtime for dates
* @param string $charset='UTF-8' encoding of the vcalendar, default UTF-8
* @return string
* @param mixed $start=null default now
* @param string $method='PUBLISH' or eg. 'REPLY'
* @param array $extra=null extra attributes to add
* X-CALENDARSERVER-MASK-UID can be used to not include an event specified by this uid as busy
*/
function freebusy($user,$end=null,$utc=true, $charset='UTF-8')
function freebusy($user,$end=null,$utc=true, $charset='UTF-8', $start=null, $method='PUBLISH', array $extra=null)
{
if (!$end) $end = $this->now_su + 100*DAY_s; // default next 100 days
if (!$start) $start = time(); // default now
if (!$end) $end = time() + 100*DAY_s; // default next 100 days
$vcal = new Horde_iCalendar;
$vcal->setAttribute('PRODID','-//eGroupWare//NONSGML eGroupWare Calendar '.$GLOBALS['egw_info']['apps']['calendar']['version'].'//'.
$vcal->setAttribute('PRODID','-//EGroupware//NONSGML EGroupware Calendar '.$GLOBALS['egw_info']['apps']['calendar']['version'].'//'.
strtoupper($GLOBALS['egw_info']['user']['preferences']['common']['lang']));
$vcal->setAttribute('VERSION','2.0');
$vcal->setAttribute('METHOD',$method);
$vfreebusy = Horde_iCalendar::newComponent('VFREEBUSY',$vcal);
$parameters = array(
'ORGANIZER' => $GLOBALS['egw']->translation->convert(
$GLOBALS['egw']->accounts->id2name($user,'account_firstname').' '.
$GLOBALS['egw']->accounts->id2name($user,'account_lastname'),
$GLOBALS['egw']->translation->charset(),$charset),
if ($uid) $vfreebusy->setAttribute('UID', $uid);
$attributes = array(
'DTSTAMP' => time(),
'DTSTART' => $this->date2ts($start,true), // true = server-time
'DTEND' => $this->date2ts($end,true), // true = server-time
);
if ($utc)
if (!$utc)
{
foreach (array(
'URL' => $this->freebusy_url($user),
'DTSTART' => $this->date2ts($this->now_su,true), // true = server-time
'DTEND' => $this->date2ts($end,true), // true = server-time
'ORGANIZER' => $GLOBALS['egw']->accounts->id2name($user,'account_email'),
'DTSTAMP' => time(),
) as $attr => $value)
foreach ($attributes as $attr => $value)
{
$vfreebusy->setAttribute($attr, $value);
$attributes[$attr] = date('Ymd\THis', $value);
}
}
else
if (is_null($extra)) $extra = array(
'URL' => $this->freebusy_url($user),
'ORGANIZER' => 'mailto:'.$GLOBALS['egw']->accounts->id2name($user,'account_email'),
);
foreach($attributes+$extra as $attr => $value)
{
foreach (array(
'URL' => $this->freebusy_url($user),
'DTSTART' => date('Ymd\THis',$this->date2ts($this->now_su,true)), // true = server-time
'DTEND' => date('Ymd\THis',$this->date2ts($end,true)), // true = server-time
'ORGANIZER' => $GLOBALS['egw']->accounts->id2name($user,'account_email'),
'DTSTAMP' => date('Ymd\THis',time()),
) as $attr => $value)
{
$vfreebusy->setAttribute($attr, $value);
}
$vfreebusy->setAttribute($attr, $value);
}
$fbdata = parent::search(array(
'start' => $this->now_su,
'start' => $start,
'end' => $end,
'users' => $user,
'date_format' => 'server',
@ -3102,20 +3082,23 @@ class calendar_ical extends calendar_boupdate
foreach ($fbdata as $event)
{
if ($event['non_blocking']) continue;
if ($event['uid'] === $extra['X-CALENDARSERVER-MASK-UID']) continue;
$fbtype = $event['participants'][$user] == 'T' ? 'BUSY-TENTATIVE' : 'BUSY';
if ($utc)
{
$vfreebusy->setAttribute('FREEBUSY',array(array(
'start' => $event['start'],
'end' => $event['end'],
)));
)), array('FBTYPE' => $fbtype));
}
else
{
$vfreebusy->setAttribute('FREEBUSY',array(array(
'start' => date('Ymd\THis',$event['start']),
'end' => date('Ymd\THis',$event['end']),
)));
)), array('FBTYPE' => $fbtype));
}
}
}

View File

@ -156,6 +156,7 @@ class calendar_so
else // array with column => value pairs
{
$where = $ids;
unset($ids); // otherwise users get not read!
}
if (isset($where['cal_id'])) // prevent non-unique column-name cal_id
{
@ -240,6 +241,11 @@ class calendar_so
$events[$row['cal_id']]['participants'][$uid] = $status;
$events[$row['cal_id']]['participant_types'][$row['cal_user_type']][$row['cal_user_id']] = $status;
if (($modified = $this->db->from_timestamp($row['cal_user_modified'])) > $events[$row['cal_id']]['max_user_modified'])
{
$events[$row['cal_id']]['max_user_modified'] = $modified;
}
}
// custom fields

View File

@ -1,13 +1,13 @@
<?php
/**
* eGroupWare - Calendar's timezone information
* EGroupware - Calendar's timezone information
*
* Timezone information get imported from SQLite database, "borrowed" of Lighting TB extension.
*
* @link http://www.egroupware.org
* @package calendar
* @author Ralf Becker <RalfBecker-AT-outdoor-training.de>
* @copyright (c) 2009 by RalfBecker-At-outdoor-training.de
* @copyright (c) 2009-11 by RalfBecker-At-outdoor-training.de
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
* @version $Id$
*/
@ -67,7 +67,7 @@ class calendar_timezones
* @return DateTimeZone
* @throws Exception if called with an unknown TZID
*/
public function DateTimeZone($tzid)
public static function DateTimeZone($tzid)
{
if (($id = self::tz2id($tzid,'alias')))
{
@ -321,6 +321,94 @@ class calendar_timezones
'<h3>'.self::import_tz_aliases()."</h3>\n",
lang('Update timezones'),true);
}
/**
* Add VTIMEZONE component to VCALENDAR
*
* @param Horde_iCalendar $vcal
* @param string $tzid
* @return boolean false if no vtimezone component available, true on success
*/
public static function add_vtimezone($vcal, $tzid)
{
include_once EGW_SERVER_ROOT.'/phpgwapi/inc/horde/lib/core.php';
// checking type of $val, now we included the object definition (no need to always include it!)
if (!$vcal instanceof Horde_iCalendar)
{
throw new egw_exception_wrong_parameter(__METHOD__.'('.array2string($val).", '$tzid') no Horde_iCalendar!");
}
// check if we have vtimezone component data for $tzid
if (!($vtimezone = calendar_timezones::tz2id($tzid, 'component')))
{
return false;
}
// $vtimezone is a string with a single VTIMEZONE component, afaik Horde_iCalendar can not add it directly
// --> we have to parse it and let Horde_iCalendar add it again
$horde_vtimezone = Horde_iCalendar::newComponent('VTIMEZONE',$container=false);
$horde_vtimezone->parsevCalendar($vtimezone,'VTIMEZONE');
// DTSTART is in UTC time, Horde_iCalendar parses it in server timezone, which we need to set again for printing
$standard = $horde_vtimezone->findComponent('STANDARD');
if (is_a($standard, 'Horde_iCalendar'))
{
$dtstart = $standard->getAttribute('DTSTART');
$dtstart = new egw_time($dtstart, egw_time::$server_timezone);
$dtstart->setTimezone(egw_time::$server_timezone);
$standard->setAttribute('DTSTART', $dtstart->format('Ymd\THis'), array(), false);
}
$daylight = $horde_vtimezone->findComponent('DAYLIGHT');
if (is_a($daylight, 'Horde_iCalendar'))
{
$dtstart = $daylight->getAttribute('DTSTART');
$dtstart = new egw_time($dtstart, egw_time::$server_timezone);
$dtstart->setTimezone(egw_time::$server_timezone);
$daylight->setAttribute('DTSTART', $dtstart->format('Ymd\THis'), array(), false);
}
//error_log($vtimezone); error_log($horde_vtimezone->_exportvData('VTIMEZONE'));
$vcal->addComponent($horde_vtimezone);
return true;
}
/**
* Query timezone of a given user, returns 'tzid' or VTIMEZONE 'component'
*
* @param int $user=null
* @param string $type='vcalendar' 'tzid' or everything tz2id supports, default 'vcalendar' = full vcalendar component
* @return string
*/
public static function user_timezone($user=null, $type='vcalendar')
{
if (!$user || $user == $GLOBALS['egw_info']['user']['account_id'])
{
$tzid = $GLOBALS['egw_info']['user']['preferences']['common']['tz'];
}
else
{
$prefs_obj = new preferences($user);
$prefs = $prefs_obj->read();
$tzid = $prefs['common']['tz'];
}
if (!$tzid) $tzid = egw_time::$server_timezone->getName();
switch ($type)
{
case 'vcalendar':
include_once EGW_SERVER_ROOT.'/phpgwapi/inc/horde/lib/core.php';
// checking type of $val, now we included the object definition (no need to always include it!)
$vcal = new Horde_iCalendar;
$vcal->setAttribute('PRODID','-//EGroupware//NONSGML EGroupware Calendar '.$GLOBALS['egw_info']['apps']['calendar']['version'].'//'.
strtoupper($GLOBALS['egw_info']['user']['preferences']['common']['lang']));
self::add_vtimezone($vcal, $tzid);
$tzid = $vcal->exportvCalendar('utf-8');
break;
case 'tzid':
break;
default:
$tzid = self::tz2id($tzid,$type == 'vcalendar' ? 'component' : $type);
break;
}
return $tzid;
}
}
/*
if (isset($_SERVER['SCRIPT_FILENAME']) && $_SERVER['SCRIPT_FILENAME'] == __FILE__) // some tests

View File

@ -898,7 +898,7 @@ class HTTP_WebDAV_Server
} elseif (isset($prop['raw'])) {
$val = $this->_prop_encode('<![CDATA['.$prop['val'].']]>');
} else {
$val = $this->_prop_encode(htmlspecialchars($prop['val']));
$val = $this->_prop_encode(htmlspecialchars($prop['val'], ENT_NOQUOTES, 'utf-8'));
}
echo ' <'.($this->crrnd?'':'D:')."$prop[name]$ns_defs>$val".
'</'.($this->crrnd?'':'D:')."$prop[name]>\n";
@ -932,13 +932,22 @@ class HTTP_WebDAV_Server
$ns_name = '';
}
$vals .= "<$ns_name$subprop[name]";
if (is_array($subprop['val'])) // val contains only attributes, no value
if (is_array($subprop['val']))
{
foreach($subprop['val'] as $attr => $val)
{
$vals .= ' '.$attr.'="'.htmlspecialchars($val).'"';
}
$vals .= '/>';
if (isset($subprop['val'][0]))
{
$vals .= '>';
$vals .= $this->_hierarchical_prop_encode($subprop['val'], $subprop['ns'], $ns_defs, $ns_hash);
$vals .= "</$ns_name$subprop[name]>";
}
else // val contains only attributes, no value
{
foreach($subprop['val'] as $attr => $val)
{
$vals .= ' '.$attr.'="'.htmlspecialchars($val, ENT_NOQUOTES, 'utf-8').'"';
}
$vals .= '/>';
}
}
else
{
@ -946,7 +955,7 @@ class HTTP_WebDAV_Server
if (isset($subprop['raw'])) {
$vals .= '<![CDATA['.$subprop['val'].']]>';
} else {
$vals .= htmlspecialchars($subprop['val']);
$vals .= htmlspecialchars($subprop['val'], ENT_NOQUOTES, 'utf-8');
}
$vals .= "</$ns_name$subprop[name]>";
}
@ -957,7 +966,7 @@ class HTTP_WebDAV_Server
{
$val = '<![CDATA['.$prop['val'].']]>';
} else {
$val = htmlspecialchars($prop['val']);
$val = htmlspecialchars($prop['val'], ENT_NOQUOTES, 'utf-8');
}
$val = $this->_prop_encode($val);
// properties from namespaces != "DAV:" or without any namespace
@ -1072,7 +1081,7 @@ class HTTP_WebDAV_Server
if ($responsedescr) {
echo ' <'.($this->crrnd?'':'D:')."responsedescription>".
$this->_prop_encode(htmlspecialchars($responsedescr)).
$this->_prop_encode(htmlspecialchars($responsedescr, ENT_NOQUOTES, 'utf-8')).
'</'.($this->crrnd?'':'D:')."responsedescription>\n";
}
@ -1375,8 +1384,6 @@ class HTTP_WebDAV_Server
$options = Array();
$options['path'] = $this->path;
error_log('WebDAV POST: ' . $this->path);
if (isset($this->_SERVER['CONTENT_LENGTH']))
{
$options['content_length'] = $this->_SERVER['CONTENT_LENGTH'];
@ -1401,6 +1408,8 @@ class HTTP_WebDAV_Server
$options['content_type'] = 'application/octet-stream';
}
$options['stream'] = fopen('php://input', 'r');
/* RFC 2616 2.6 says: "The recipient of the entity MUST NOT
ignore any Content-* (e.g. Content-Range) headers that it
does not understand or implement and MUST return a 501
@ -1410,10 +1419,22 @@ class HTTP_WebDAV_Server
if (strncmp($key, 'HTTP_CONTENT', 11)) continue;
switch ($key) {
case 'HTTP_CONTENT_ENCODING': // RFC 2616 14.11
// TODO support this if ext/zlib filters are available
$this->http_status('501 not implemented');
echo "The service does not support '$val' content encoding";
return;
switch($this->_SERVER['HTTP_CONTENT_ENCODING'])
{
case 'gzip':
case 'deflate': //zlib
if (extension_loaded('zlib'))
{
stream_filter_append($options['stream'], 'zlib.inflate', STREAM_FILTER_READ);
break;
}
// fall through for no zlib support
default:
$this->http_status('415 Unsupported Media Type');
echo "The service does not support '$val' content encoding";
return;
}
break;
case 'HTTP_CONTENT_LANGUAGE': // RFC 2616 14.12
// we assume it is not critical if this one is ignored
@ -1470,8 +1491,6 @@ class HTTP_WebDAV_Server
}
}
$options['stream'] = fopen('php://input', 'r');
if (method_exists($this, 'POST')) {
$status = $this->POST($options);
@ -1539,7 +1558,7 @@ class HTTP_WebDAV_Server
// for now we do not support any sort of multipart requests
if (!strncmp($this->_SERVER["CONTENT_TYPE"], "multipart/", 10)) {
$this->http_status("501 not implemented");
echo "The service does not support mulipart PUT requests";
echo "The service does not support multipart PUT requests";
return;
}
$options["content_type"] = $this->_SERVER["CONTENT_TYPE"];
@ -1548,6 +1567,8 @@ class HTTP_WebDAV_Server
$options["content_type"] = "application/octet-stream";
}
$options["stream"] = fopen("php://input", "r");
/* RFC 2616 2.6 says: "The recipient of the entity MUST NOT
ignore any Content-* (e.g. Content-Range) headers that it
does not understand or implement and MUST return a 501
@ -1557,10 +1578,22 @@ class HTTP_WebDAV_Server
if (strncmp($key, "HTTP_CONTENT", 11)) continue;
switch ($key) {
case 'HTTP_CONTENT_ENCODING': // RFC 2616 14.11
// TODO support this if ext/zlib filters are available
$this->http_status("501 not implemented");
echo "The service does not support '$val' content encoding";
return;
switch($this->_SERVER['HTTP_CONTENT_ENCODING'])
{
case 'gzip':
case 'deflate': //zlib
if (extension_loaded('zlib'))
{
stream_filter_append($options['stream'], 'zlib.inflate', STREAM_FILTER_READ);
break;
}
// fall through for no zlib support
default:
$this->http_status('415 Unsupported Media Type');
echo "The service does not support '$val' content encoding";
return;
}
break;
case 'HTTP_CONTENT_LANGUAGE': // RFC 2616 14.12
// we assume it is not critical if this one is ignored
@ -1622,8 +1655,6 @@ class HTTP_WebDAV_Server
}
}
$options["stream"] = fopen("php://input", "r");
$stat = $this->PUT($options);
if ($stat === false) {
@ -2574,7 +2605,7 @@ class HTTP_WebDAV_Server
foreach($subprop as $attr => $val)
{
$vals .= ' '.$attr.'="'.htmlspecialchars($val).'"';
$vals .= ' '.$attr.'="'.htmlspecialchars($val, ENT_NOQUOTES, 'utf-8').'"';
}
$ret .= '<'.($prop['ns'] == $ns ? ($this->cnrnd ? $ns_hash[$ns].':' : '') : $ns_hash[$prop['ns']].':').$prop['name'].
@ -2593,8 +2624,14 @@ class HTTP_WebDAV_Server
{
$val = $this->_prop_encode('<![CDATA['.$prop['val'].']]>');
} else {
$val = $this->_prop_encode(htmlspecialchars($prop['val']));
} }
$val = $this->_prop_encode(htmlspecialchars($prop['val'], ENT_NOQUOTES, 'utf-8'));
// for href properties we need (minimalistic) urlencoding, eg. space
if ($prop['name'] == 'href')
{
$val = $this->_urlencode($val);
}
}
}
$ret .= '<'.($prop['ns'] == $ns ? ($this->cnrnd ? $ns_hash[$ns].':' : '') : $ns_hash[$prop['ns']].':').$prop['name'].
(empty($prop['val']) ? ' />' : '>'.$val.'</'.($prop['ns'] == $ns ? ($this->cnrnd ? $ns_hash[$ns].':' : '') : ($this->crrnd ? '' : $ns_hash[$prop['ns']].':')).$prop['name'].'>');

View File

@ -188,7 +188,7 @@ class _parse_propfind
// record root tag
if ($this->depth == 0) {
$this->root = array('name' => $tag, 'xmlns' => $ns);
$this->root = array('name' => $tag, 'xmlns' => $ns, 'attrs' => $attrs);
}
// special tags at level 1: <allprop> and <propname>
@ -206,6 +206,7 @@ class _parse_propfind
break;
case 'filter':
$this->use = 'filters';
$this->filters['attrs'] = $attrs; // need attrs eg. <filters test="(anyof|alloff)">
break;
default:
$this->use = 'other';

View File

@ -28,8 +28,6 @@ class infolog_bo
var $so;
var $vfs;
var $vfs_basedir='/infolog';
var $link_pathes = array();
var $send_file_ips = array();
/**
* Set Logging
*
@ -184,9 +182,6 @@ class infolog_bo
);
if (($config_data = config::read('infolog')))
{
$this->link_pathes = $config_data['link_pathes'];
$this->send_file_ips = $config_data['send_file_ips'];
if (isset($config_data['status']) && is_array($config_data['status']))
{
foreach($config_data['status'] as $key => $data)
@ -552,7 +547,7 @@ class infolog_bo
function &read($info_id,$run_link_id2from=true,$date_format='ts')
{
//error_log(__METHOD__.'('.array2string($info_id).', '.array2string($run_link_id2from).", '$date_format') ".function_backtrace());
if (is_scalar($info_id) || isset($info_id[0]))
if (is_scalar($info_id) || isset($info_id[count($info_id)-1]))
{
if (is_scalar($info_id) && !is_numeric($info_id))
{
@ -1079,7 +1074,7 @@ class infolog_bo
$entry = array_shift($result);
return 'EGw-'.$entry['info_datemodified'].'-wGE';
return $entry['info_datemodified'];
}
/**

View File

@ -42,6 +42,7 @@ class infolog_groupdav extends groupdav_handler
'PRIORITY' => 'info_priority',
'LOCATION' => 'info_location',
'COMPLETED' => 'info_datecompleted',
'CREATED' => 'info_created',
);
/**
@ -55,13 +56,11 @@ class infolog_groupdav extends groupdav_handler
* Constructor
*
* @param string $app 'calendar', 'addressbook' or 'infolog'
* @param int $debug=null debug-level to set
* @param string $base_uri=null base url of handler
* @param string $principalURL=null pricipal url of handler
* @param groupdav $groupdav calling class
*/
function __construct($app,$debug=null,$base_uri=null,$principalURL=null)
function __construct($app, groupdav $groupdav)
{
parent::__construct($app,$debug,$base_uri,$principalURL);
parent::__construct($app, $groupdav);
$this->bo = new infolog_bo();
$this->vCalendar = new Horde_iCalendar;
@ -80,7 +79,7 @@ class infolog_groupdav extends groupdav_handler
* @param array|int $info
* @return string
*/
static function get_path($info)
function get_path($info)
{
if (is_numeric($info) && self::$path_attr == 'info_id')
{
@ -153,6 +152,11 @@ class infolog_groupdav extends groupdav_handler
// when trying to request not supported components, eg. VTODO on a calendar collection
return true;
}
// enable time-range filter for tests via propfind / autoindex
//$filter[] = $sql = $this->_time_range_filter(array('end' => '20001231T000000Z'));
if ($id) $path = dirname($path).'/'; // caldav_name get's added anyway in the callback
if ($this->debug > 1)
{
error_log(__METHOD__."($path,,,$user,$id) filter=".
@ -230,27 +234,16 @@ class infolog_groupdav extends groupdav_handler
foreach($tasks as $task)
{
$props = array(
HTTP_WebDAV_Server::mkprop('getetag',$this->get_etag($task)),
HTTP_WebDAV_Server::mkprop('getcontenttype',$this->agent != 'kde' ?
'text/calendar; charset=utf-8; component=VTODO' : 'text/calendar'), // Konqueror (3.5) dont understand it otherwise
// getlastmodified and getcontentlength are required by WebDAV and Cadaver eg. reports 404 Not found if not set
HTTP_WebDAV_Server::mkprop('getlastmodified', $task['info_datemodified']),
HTTP_WebDAV_Server::mkprop('resourcetype',''), // DAVKit requires that attribute!
'getcontenttype' => $this->agent != 'kde' ? 'text/calendar; charset=utf-8; component=VTODO' : 'text/calendar', // Konqueror (3.5) dont understand it otherwise
'getlastmodified' => $task['info_datemodified'],
);
if ($calendar_data)
{
$content = $handler->exportVTODO($task,'2.0','PUBLISH');
$props[] = HTTP_WebDAV_Server::mkprop('getcontentlength',bytes($content));
$content = $handler->exportVTODO($task, '2.0', null); // no METHOD:PUBLISH for CalDAV
$props['getcontentlength'] = bytes($content);
$props[] = HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'calendar-data',$content);
}
else
{
$props[] = HTTP_WebDAV_Server::mkprop('getcontentlength', ''); // expensive to calculate and no CalDAV client uses it
}
$files[] = array(
'path' => $path.self::get_path($task),
'props' => $props,
);
$files[] = $this->add_resource($path, $task, $props);
}
}
if ($this->debug) error_log(__METHOD__."($path) took ".(microtime(true) - $starttime).' to return '.count($files).' items');
@ -269,6 +262,8 @@ class infolog_groupdav extends groupdav_handler
{
if ($options['filters'])
{
$cal_filters_in = $cal_filters; // remember filter, to be able to reset standard open-filter, if client sets own filters
foreach($options['filters'] as $filter)
{
switch($filter['name'])
@ -305,21 +300,18 @@ class infolog_groupdav extends groupdav_handler
if ($this->debug) error_log(__METHOD__."($options[path],...) param-filter='{$filter['attrs']['name']}' not (yet) implemented!");
break;
case 'time-range':
if ($this->debug > 1) error_log(__FILE__ . __METHOD__."($options[path],...) time-range={$filter['attrs']['start']}-{$filter['attrs']['end']}");
if (!empty($filter['attrs']['start']))
{
$cal_filters[] = 'info_startdate >= ' . (int)$this->vCalendar->_parseDateTime($filter['attrs']['start']);
}
if (!empty($filter['attrs']['end']))
{
$cal_filters[] = 'info_startdate <= ' . (int)$this->vCalendar->_parseDateTime($filter['attrs']['end']);
}
$cal_filters[] = $this->_time_range_filter($filter['attrs']);
break;
default:
if ($this->debug) error_log(__METHOD__."($options[path],".array2string($options).",...) unknown filter --> ignored");
break;
}
}
// if client set an own filter, reset the open-standard filter
if ($cal_filters != $cal_filters_in)
{
$cal_filters['filter'] = str_replace(array('open', 'open-user'), array('own', 'user'), $cal_filters['filter']);
}
}
// multiget or propfind on a given id
//error_log(__FILE__ . __METHOD__ . "multiget of propfind:");
@ -351,6 +343,65 @@ class infolog_groupdav extends groupdav_handler
return true;
}
/**
* Create SQL filter from time-range filter attributes
*
* CalDAV time-range for VTODO checks DTSTART, DTEND, DUE, CREATED and allways includes tasks if none given
* @see http://tools.ietf.org/html/rfc4791#section-9.9
*
* @param array $attrs values for keys 'start' and/or 'end', at least one is required by CalDAV rfc!
* @return string with sql
*/
private function _time_range_filter(array $attrs)
{
$to_or = $to_and = array();
if (!empty($attrs['start']))
{
$start = (int)$this->vCalendar->_parseDateTime($attrs['start']);
}
if (!empty($attrs['end']))
{
$end = (int)$this->vCalendar->_parseDateTime($attrs['end']);
}
elseif (empty($attrs['start']))
{
error_log(__METHOD__.'('.array2string($attrs).') minimum one of start or end is required!');
return '1'; // to not give sql error, but simply not filter out anything
}
// we dont need to care for DURATION line in rfc4791#section-9.9, as we always put that in DUE/info_enddate
// we have start- and/or enddate
if (isset($start))
{
$to_and[] = "($start < info_enddate OR $start <= info_startdate)";
}
if (isset($end))
{
$to_and[] = "(info_startdate < $end OR info_enddate <= $end)";
}
$to_or[] = '('.implode(' AND ', $to_and).')';
/* either start or enddate is already included in the above, because of OR!
// only a startdate, no enddate
$to_or[] = "NOT info_enddate > 0".($start ? " AND $start <= info_startdate" : '').
($end ? " AND info_startdate < $end" : '');
// only an enddate, no startdate
$to_or[] = "NOT info_startdate > 0".($start ? " AND $start < info_enddate" : '').
($end ? " AND info_enddate <= $end" : '');*/
// no startdate AND no enddate (2. half of rfc4791#section-9.9) --> use created and due dates instead
$to_or[] = 'NOT info_startdate > 0 AND NOT info_enddate > 0 AND ('.
// we have a completed date
"info_datecompleted > 0".(isset($start) ? " AND ($start <= info_datecompleted OR $start <= info_created)" : '').
(isset($end) ? " AND (info_datecompleted <= $end OR info_created <= $end)" : '').' OR '.
// we have no completed date, but always a created date
"NOT info_datecompleted > 0". (isset($end) ? " AND info_created < $end" : '').
')';
$sql = '('.implode(' OR ', $to_or).')';
if ($this->debug > 1) error_log(__FILE__ . __METHOD__.'('.array2string($attrs).") time-range={$filter['attrs']['start']}-{$filter['attrs']['end']} --> $sql");
return $sql;
}
/**
* Handle get request for a task / infolog entry
@ -367,10 +418,10 @@ class infolog_groupdav extends groupdav_handler
return $task;
}
$handler = $this->_get_handler();
$options['data'] = $handler->exportVTODO($task,'2.0','PUBLISH');
$options['data'] = $handler->exportVTODO($task, '2.0', null); // no METHOD:PUBLISH for CalDAV
$options['mimetype'] = 'text/calendar; charset=utf-8';
header('Content-Encoding: identity');
header('ETag: '.$this->get_etag($task));
header('ETag: "'.$this->get_etag($task).'"');
return true;
}
@ -440,7 +491,8 @@ class infolog_groupdav extends groupdav_handler
$retval = '201 Created';
}
header('ETag: '.$this->get_etag($infoId));
// we should not return an etag here, as we never store the PUT ical byte-by-byte
//header('ETag: "'.$this->get_etag($infoId).'"');
// send GroupDAV Location header only if we dont use caldav_name as path-attribute
if ($retval !== true && self::$path_attr != 'caldav_name')
@ -470,12 +522,15 @@ class infolog_groupdav extends groupdav_handler
/**
* Read an entry
*
* We have to make sure to not return or even consider in read deleted infologs, as the might have
* the same UID and/or caldav_name as not deleted ones and would block access to valid entries
*
* @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(array(self::$path_attr => $id),false,'server');
return $this->bo->read(array(self::$path_attr => $id, "info_status!='deleted'"),false,'server');
}
/**
@ -520,7 +575,7 @@ class infolog_groupdav extends groupdav_handler
{
return false;
}
return 'EGw-'.$info['info_id'].':'.$info['info_datemodified'].'-wGE';
return $info['info_id'].':'.$info['info_datemodified'];
}
/**
@ -529,30 +584,33 @@ class infolog_groupdav extends groupdav_handler
* @param array $props=array() regular props by the groupdav handler
* @param string $displayname
* @param string $base_uri=null base url of handler
* @param int $user=null account_id of owner of collection
* @return array
*/
static function extra_properties(array $props=array(), $displayname, $base_uri=null)
public function extra_properties(array $props=array(), $displayname, $base_uri=null,$user=null)
{
// calendar description
$displayname = translation::convert(lang('Tasks of') . ' ' .
$displayname,translation::charset(),'utf-8');
$props[] = HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'calendar-description',$displayname);
// email of the current user, see caldav-sheduling draft
$props[] = HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'calendar-user-address-set',array(
HTTP_WebDAV_Server::mkprop('href','MAILTO:'.$GLOBALS['egw_info']['user']['email'])));
$displayname = translation::convert(lang('Tasks of'),translation::charset(),'utf-8').' '.$displayname;
$props['calendar-description'] = HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'calendar-description',$displayname);
// supported components, currently only VEVENT
$props[] = HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'supported-calendar-component-set',array(
// HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'comp',array('name' => 'VEVENT')),
$props['supported-calendar-component-set'] = HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'supported-calendar-component-set',array(
HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'comp',array('name' => 'VCALENDAR')),
HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'comp',array('name' => 'VTIMEZONE')),
HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'comp',array('name' => 'VTODO')),
));
$props[] = HTTP_WebDAV_Server::mkprop('supported-report-set',array(
// supported reports
$props['supported-report-set'] = HTTP_WebDAV_Server::mkprop('supported-report-set',array(
HTTP_WebDAV_Server::mkprop('supported-report',array(
HTTP_WebDAV_Server::mkprop('report',array(
HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'calendar-query',''))),
HTTP_WebDAV_Server::mkprop('report',array(
HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'calendar-multiget','')))))));
// get timezone of calendar
if ($this->groupdav->prop_requested('calendar-timezone'))
{
$props['calendar-timezone'] = HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'calendar-timezone',
calendar_timezones::user_timezone($user));
}
return $props;
}

View File

@ -167,40 +167,26 @@ class infolog_ical extends infolog_bo
}
$vcal = new Horde_iCalendar;
$vcal->setAttribute('PRODID','-//EGroupware//NONSGML EGroupware InfoLog '.$GLOBALS['egw_info']['apps']['infolog']['version'].'//'.
strtoupper($GLOBALS['egw_info']['user']['preferences']['common']['lang']));
$vcal->setAttribute('VERSION',$_version);
$vcal->setAttribute('METHOD',$_method);
if ($_method) $vcal->setAttribute('METHOD',$_method);
$tzid = $this->tzid;
if ($tzid && $tzid != 'UTC')
{
// check if we have vtimezone component data for tzid of event, if not default to user timezone (default to server tz)
if (!($vtimezone = calendar_timezones::tz2id($tzid,'component')))
if (!calendar_timezones::add_vtimezone($vcal, $tzid))
{
error_log(__METHOD__."() unknown TZID='$tzid', defaulting to user timezone '".egw_time::$user_timezone->getName()."'!");
$vtimezone = calendar_timezones::tz2id($tzid=egw_time::$user_timezone->getName(),'component');
calendar_timezones::add_vtimezone($vcal, $tzid=egw_time::$user_timezone->getName());
$tzid = null;
}
if (!isset(self::$tz_cache[$tzid]))
{
self::$tz_cache[$tzid] = calendar_timezones::DateTimeZone($tzid);
}
// $vtimezone is a string with a single VTIMEZONE component, afaik Horde_iCalendar can not add it directly
// --> we have to parse it and let Horde_iCalendar add it again
$horde_vtimezone = Horde_iCalendar::newComponent('VTIMEZONE',$container=false);
$horde_vtimezone->parsevCalendar($vtimezone,'VTIMEZONE');
// DTSTART must be in local time!
$standard = $horde_vtimezone->findComponent('STANDARD');
$dtstart = $standard->getAttribute('DTSTART');
$dtstart = new egw_time($dtstart, egw_time::$server_timezone);
$dtstart->setTimezone(self::$tz_cache[$tzid]);
$standard->setAttribute('DTSTART', $dtstart->format('Ymd\THis'), array(), false);
$daylight = $horde_vtimezone->findComponent('DAYLIGHT');
$dtstart = $daylight->getAttribute('DTSTART');
$dtstart = new egw_time($dtstart, egw_time::$server_timezone);
$dtstart->setTimezone(self::$tz_cache[$tzid]);
$daylight->setAttribute('DTSTART', $dtstart->format('Ymd\THis'), array(), false);
$vcal->addComponent($horde_vtimezone);
}
$vevent = Horde_iCalendar::newComponent('VTODO',$vcal);
@ -573,7 +559,7 @@ class infolog_ical extends infolog_bo
{
$taskData['info_id'] = $_taskID;
}
foreach ($component->_attributes as $attribute)
foreach ($component->getAllAttributes() as $attribute)
{
//$attribute['value'] = trim($attribute['value']);
if (!strlen($attribute['value'])) continue;
@ -611,11 +597,17 @@ class infolog_ical extends infolog_bo
$taskData['info_location'] = str_replace("\r\n", "\n", $attribute['value']);
break;
case 'DURATION':
if (!isset($taskData['info_startdate']))
{
$taskData['info_startdate'] = $component->getAttribute('DTSTART');
}
$attribute['value'] += $taskData['info_startdate'];
// fall throught
case 'DUE':
// eGroupWare uses date only
$parts = @getdate($attribute['value']);
$value = @mktime(0, 0, 0, $parts['mon'], $parts['mday'], $parts['year']);
$taskData['info_enddate'] = $value;
// even as EGroupware only displays the date, we can still store the full value
// unless infolog get's stored, it does NOT truncate the time
$taskData['info_enddate'] = $attribute['value'];
break;
case 'COMPLETED':
@ -648,12 +640,8 @@ class infolog_ical extends infolog_bo
case 'STATUS':
// check if we (still) have X-INFOLOG-STATUS set AND it would give an unchanged status (no change by the user)
foreach ($component->_attributes as $attr)
{
if ($attr['name'] == 'X-INFOLOG-STATUS') break;
}
$taskData['info_status'] = $this->vtodo2status($attribute['value'],
$attr['name'] == 'X-INFOLOG-STATUS' ? $attr['value'] : null);
($attr=$component->getAttribute('X-INFOLOG-STATUS')) && is_array($attr) ? $attr['value'] : null);
break;
case 'SUMMARY':
@ -724,7 +712,9 @@ class infolog_ical extends infolog_bo
translation::charset(), $charset);
}
$vnote = new Horde_iCalendar_vnote();
$vNote->setAttribute('VERSION', '1.1');
$vnote->setAttribute('PRODID','-//EGroupware//NONSGML EGroupware InfoLog '.$GLOBALS['egw_info']['apps']['infolog']['version'].'//'.
strtoupper($GLOBALS['egw_info']['user']['preferences']['common']['lang']));
$vnote->setAttribute('VERSION', '1.1');
foreach (array( 'SUMMARY' => $note['info_subject'],
'BODY' => $note['info_des'],
'CATEGORIES' => $note['info_cat'],

View File

@ -411,8 +411,7 @@ class infolog_so
*/
function get_status($ids)
{
$this->db->select($this->info_table,'info_id,info_type,info_status,info_percent',array('info_id'=>$ids),__LINE__,__FILE__);
while (($info = $this->db->row(true)))
foreach($this->db->select($this->info_table,'info_id,info_type,info_status,info_percent',array('info_id'=>$ids),__LINE__,__FILE__) as $info)
{
switch ($info['info_type'].'-'.$info['info_status'])
{

View File

@ -20,20 +20,27 @@ require_once('HTTP/WebDAV/Server.php');
*
* 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
* - / base of Cal|Card|GroupDAV tree, only certain clients (KDE, Apple) can autodetect folders from here
* - /principals/ principal-collection-set for WebDAV ACL
* - /principals/users/<username>/
* - /principals/groups/<groupname>/
* - /<username>/ users home-set with
* - /<username>/addressbook/ addressbook of user or group <username> given the user has rights to view it
* - /<username>/calendar/ calendar of user <username> given the user has rights to view it
* - /<username>/inbox/ scheduling inbox of user <username>
* - /<username>/outbox/ scheduling outbox of user <username>
* - /<username>/infolog/ InfoLog's of user <username> given the user has rights to view it
* - /<username>/ base of the above, only certain clients (KDE, Apple) can autodetect folders from there
* - /addressbook/ all addressbooks current user has rights to, announced as directory-gateway now
* - /calendar/ calendar of current user
* - /infolog/ infologs of current user
*
* 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
* @link http://www.groupdav.org/ GroupDAV spec
* @link http://caldav.calconnect.org/ CalDAV resources
* @link http://carddav.calconnect.org/ CardDAV resources
* @link http://calendarserver.org/ Apple calendar and contacts server
*/
class groupdav extends HTTP_WebDAV_Server
{
@ -54,9 +61,13 @@ class groupdav extends HTTP_WebDAV_Server
*/
const CARDDAV = 'urn:ietf:params:xml:ns:carddav';
/**
* Calendarserver namespace (eg. for ctag)
* 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)
*/
@ -69,22 +80,34 @@ class groupdav extends HTTP_WebDAV_Server
var $dav_powered_by = self::REALM;
var $http_auth_realm = self::REALM;
/**
* Folders in root or user home
*
* @var array
*/
var $root = array(
'addressbook' => array(
'resourcetype' => array(self::GROUPDAV => 'vcard-collection', self::CARDDAV => 'addressbook'),
'component-set' => array(self::GROUPDAV => 'VCARD'),
),
'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'),
'inbox' => array(
'resourcetype' => array(self::CALDAV => 'schedule-inbox'),
'app' => 'calendar',
'user-only' => true, // display just in user home
),
'outbox' => array(
'resourcetype' => array(self::CALDAV => 'schedule-outbox'),
'app' => 'calendar',
'user-only' => true, // display just in user home
),
'infolog' => array(
'resourcetype' => array(self::GROUPDAV => 'vtodo-collection', self::CALDAV => 'calendar'),
'component-set' => array(self::GROUPDAV => 'VTODO'),
),
'principals' => array(
'resourcetype' => array(self::DAV => 'principal'),
)
);
/**
* Debug level: 0 = nothing, 1 = function calls, 2 = more info, 3 = complete $_SERVER array
@ -110,17 +133,64 @@ class groupdav extends HTTP_WebDAV_Server
*/
var $handler;
/**
* principal URL
* current-user-principal URL
*
* @var string
*/
var $principalURL;
var $current_user_principal;
/**
* Reference to the accounts class
*
* @var accounts
*/
var $accounts;
/**
* Supported privileges with name and description
*
* privileges are hierarchical
*
* @var array
*/
var $supported_privileges = array(
'all' => array(
'*description*' => 'all privileges',
'read' => array(
'*description*' => 'read resource',
'read-free-busy' => array(
'*ns*' => self::CALDAV,
'*description*' => 'allow free busy report query',
'*only*' => '/calendar/',
),
),
'write' => array(
'*description*' => 'write resource',
'write-properties' => 'write resource properties',
'write-content' => 'write resource content',
'bind' => 'add child resource',
'unbind' => 'remove child resource',
),
'unlock' => 'unlock resource without ownership of lock',
'read-acl' => 'read resource access control list',
'write-acl' => 'write resource access control list',
'read-current-user-privilege-set' => 'read privileges for current principal',
'schedule-deliver' => array(
'*ns*' => self::CALDAV,
'*description*' => 'schedule privileges for current principal',
'*only*' => '/inbox/',
),
'schedule-send' => array(
'*ns*' => self::CALDAV,
'*description*' => 'schedule privileges for current principal',
'*only*' => '/outbox/',
),
),
);
/**
* $options parameter to PROPFIND request, eg. to check what props are requested
*
* @var array
*/
var $propfind_options;
function __construct()
{
@ -128,8 +198,13 @@ class groupdav extends HTTP_WebDAV_Server
if ($this->debug > 2) error_log('groupdav: $_SERVER='.array2string($_SERVER));
// crrnd: client refuses redundand namespace declarations
// cnrnd: client needs redundand namespace declarations
// setting redundand namespaces as the default for (Cal|Card|Group)DAV, as the majority of the clients either require or can live with it
$this->cnrnd = true;
// identify clients, which do NOT support path AND full url in <D:href> of PROPFIND request
switch(groupdav_handler::get_agent())
switch(($agent = groupdav_handler::get_agent()))
{
case 'akonadi':
$this->cnrnd = true; // Akonadi seems to require redundant namespaces, see KDE bug #265096 https://bugs.kde.org/show_bug.cgi?id=265096
@ -155,6 +230,8 @@ class groupdav extends HTTP_WebDAV_Server
$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);
@ -164,19 +241,19 @@ class groupdav extends HTTP_WebDAV_Server
$this->egw_charset = translation::charset();
if (strpos($this->base_uri, 'http') === 0)
{
$this->principalURL = $this->_slashify($this->base_uri);
$this->current_user_principal = $this->_slashify($this->base_uri);
}
else
{
$this->principalURL = (@$_SERVER["HTTPS"] === "on" ? "https:" : "http:") .
$this->current_user_principal = (@$_SERVER["HTTPS"] === "on" ? "https:" : "http:") .
'//' . $_SERVER['HTTP_HOST'] . $_SERVER['SCRIPT_NAME'] . '/';
}
$this->principalURL .= 'principals/users/'.$GLOBALS['egw_info']['user']['account_lid'].'/';
$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 === false)
if (!$this->client_require_href_as_url)
{
$this->principalURL = parse_url($this->principalURL,PHP_URL_PATH);
$this->current_user_principal = parse_url($this->current_user_principal,PHP_URL_PATH);
}
$this->accounts = $GLOBALS['egw']->accounts;
}
@ -189,7 +266,9 @@ class groupdav extends HTTP_WebDAV_Server
*/
function app_handler($app)
{
return groupdav_handler::app_handler($app,$this->debug,$this->base_uri,$this->principalURL);
if (isset($this->root[$app]['app'])) $app = $this->root[$app]['app'];
return groupdav_handler::app_handler($app,$this);
}
/**
@ -201,31 +280,39 @@ class groupdav extends HTTP_WebDAV_Server
*/
function OPTIONS($path, &$dav, &$allow)
{
list(,$app) = explode('/',$path);
switch($app)
if (preg_match('#/(calendar|inbox|outbox)/#', $path))
{
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';
$app = 'calendar';
}
// not yet implemented: $dav[] = 'access-control';
elseif (strpos($path, '/addressbook/') !== false)
{
$app = 'addressbook';
}
// CalDAV and CardDAV
$dav[] = 'access-control';
if ($app !== 'addressbook') // CalDAV
{
$dav[] = 'calendar-access';
$dav[] = 'calendar-auto-schedule';
$dav[] = 'calendar-proxy';
// required by iOS iCal to use principal-property-search to autocomplete participants (and locations)
$dav[] = 'calendarserver-principal-property-search';
// other capabilities calendarserver announces
//$dav[] = 'calendar-schedule';
//$dav[] = 'calendar-availability';
//$dav[] = 'inbox-availability';
//$dav[] = 'calendarserver-private-events';
//$dav[] = 'calendarserver-private-comments';
//$dav[] = 'calendarserver-sharing';
//$dav[] = 'calendarserver-sharing-no-scheduling';
}
if ($app !== 'calendar') // CardDAV
{
$dav[] = 'addressbook'; // CardDAV uses "addressbook" NOT "addressbook-access"
}
//error_log(__METHOD__."('$path') --> app='$app' --> DAV: ".implode(', ', $dav));
}
/**
@ -239,6 +326,9 @@ class groupdav extends HTTP_WebDAV_Server
{
if ($this->debug) error_log(__CLASS__."::$method(".array2string($options,true).')');
// make options (readonly) available to all class methods, eg. prop_requested
$this->propfind_options = $options;
// parse path in form [/account_lid]/app[/more]
if (!self::_parse_path($options['path'],$id,$app,$user,$user_prefix) && $app && !$user)
{
@ -247,6 +337,207 @@ class groupdav extends HTTP_WebDAV_Server
}
if ($this->debug > 1) error_log(__CLASS__."::$method: user='$user', app='$app', id='$id'");
$files = array('files' => array());
$path = $user_prefix = $this->_slashify($user_prefix);
if (!$app) // user root folder containing apps
{
// add root with current users apps
$this->add_home($files, $path, $user, $options['depth']);
// add principals and user-homes
if ($path == '/' && $options['depth'])
{
// principals collection
$files['files'][] = $this->add_collection('/principals/', array(
'displayname' => lang('Accounts'),
));
// todo: account_selection owngroups and none!!!
foreach($this->accounts->search(array('type' => 'both')) as $account)
{
$this->add_home($files, $path.$account['account_lid'].'/', $user, $options['depth'] == 'infinity' ? 'infinity' : $options['depth']-1);
}
}
return true;
}
if ($app != 'principals' && !isset($GLOBALS['egw_info']['user']['apps'][$this->root[$app]['app'] ? $this->root[$app]['app'] : $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
{
// KAddressbook doubles the folder, if the self URL contains the GroupDAV/CalDAV resourcetypes
$files['files'][0] = $this->add_app($app,$app=='addressbook'&&$handler->get_agent()=='kde',$user,$path);
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';
}
/**
* 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)
* @param array $privileges=array('read') values for current-user-privilege-set
* @param array $supported_privileges=null default $this->supported_privileges
* @return array with values for keys 'path' and 'props'
*/
public function add_collection($path, array $props = array(), array $privileges=array('read','read-acl','read-current-user-privilege-set'), array $supported_privileges=null)
{
// resourcetype: collection
$props['resourcetype'][] = self::mkprop('collection','');
if (!isset($props['getcontenttype'])) $props['getcontenttype'] = 'httpd/unix-directory';
return $this->add_resource($path, $props, $privileges, $supported_privileges);
}
/**
* Add a resource 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)
* @param array $privileges=array('read') values for current-user-privilege-set
* @param array $supported_privileges=null default $this->supported_privileges
* @return array with values for keys 'path' and 'props'
*/
public function add_resource($path, array $props = array(), array $privileges=array('read','read-current-user-privilege-set'), array $supported_privileges=null)
{
// 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' => 'none',
'getcontentlength' => '',
'getlastmodified' => '',
'getcontenttype' => '',
'resourcetype' => '',
) as $name => $default)
{
if (!isset($props[$name])) $props[$name] = $default;
}
// if requested add privileges
if (is_null($supported_privileges)) $supported_privileges = $this->supported_privileges;
if ($this->prop_requested('current-user-privilege-set') === true)
{
foreach($privileges as $name)
{
$props['current-user-privilege-set'][] = self::mkprop('privilege', array(
is_array($name) ? self::mkprop($name['ns'], $name['name'], '') : self::mkprop($name, '')));
}
}
if ($this->prop_requested('supported-privilege-set') === true)
{
foreach($supported_privileges as $name => $data)
{
$props['supported-privilege-set'][] = $this->supported_privilege($name, $data, $path);
}
}
if (!isset($props['owner']) && $this->prop_requested('owner') === true)
{
$props['owner'] = '';
}
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);
}
// add quotes around etag, if they are not already there
if ($prop['name'] == 'getetag' && $prop['val'][0] != '"')
{
$prop['val'] = '"'.$prop['val'].'"';
}
}
return array(
'path' => $path,
'props' => $props,
);
}
/**
* Generate (hierachical) supported-privilege property
*
* @param string $name name of privilege
* @param string|array $data string with describtion or array with agregated privileges plus value for key '*description*', '*ns*', '*only*'
* @param string $path=null path to match with $data['*only*']
* @return array of self::mkprop() arrays
*/
protected function supported_privilege($name, $data, $path=null)
{
$props = array();
$props[] = self::mkprop('privilege', array(is_array($data) && $data['*ns*'] ?
self::mkprop($data['*ns*'], $name, '') : self::mkprop($name, '')));
$props[] = self::mkprop('description', is_array($data) ? $data['*description*'] : $data);
if (is_array($data))
{
foreach($data as $name => $data)
{
if ($name[0] == '*') continue;
if (is_array($data) && $data['*only*'] && strpos($path, $data['*only*']) === false)
{
continue; // wrong path
}
$props[] = $this->supported_privilege($name, $data, $path);
}
}
return self::mkprop('supported-privilege', $props);
}
/**
* Checks if a given property was requested in propfind request
*
* @param string $name property name
* @param string $ns=null namespace, if that is to be checked too
* @return boolean|string true: $name explicitly requested (or autoindex), 'allprop' requested, false: $name was not requested
*/
function prop_requested($name, $ns=null)
{
if (!is_array($this->propfind_options) || !isset($this->propfind_options['props']))
{
return true; // no props set, should happen only in autoindex, we return true to show all available props
}
$ret = false;
foreach($this->propfind_options['props'] as $prop)
{
if ($prop['name'] == $name && (is_null($ns) || $prop['xmlns'] == $ns))
{
$ret = true;
break;
}
if ($prop['name'] == 'allprop') $ret = 'allprop';
}
return $ret;
}
/**
* Add user home with addressbook, calendar, infolog
*
* @param array $files
* @param string $path / or /<username>/
* @param int $user
* @param int $depth
* @return string|boolean http status or true|false
*/
protected function add_home(array &$files, $path, $user, $depth)
{
if ($user)
{
$account_lid = $this->accounts->id2name($user);
@ -256,137 +547,57 @@ class groupdav extends HTTP_WebDAV_Server
$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
$calendar_user_address_set = array(
self::mkprop('href','urn:uuid:'.$account['account_lid']),
);
if ($user < 0)
{
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'].'/');
$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';
}
if ($depth && $path == '/')
{
$displayname = 'EGroupware (Cal|Card|Group)DAV server';
}
$displayname = translation::convert($displayname, translation::charset(),'utf-8');
// self url
$props = array(
self::mkprop('displayname',$displayname),
self::mkprop('owner',array(self::mkprop('href',$this->base_uri.'/principals/'.$principalType.'/'.$account_lid.'/'))),
self::mkprop('resourcetype',array(self::mkprop('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',array(
self::mkprop('href',$this->base_uri.$user_prefix))),
self::mkprop(groupdav::CARDDAV,'addressbook-home-set',array(
self::mkprop('href',$this->base_uri.$user_prefix))),
self::mkprop('current-user-principal',array(self::mkprop('href',$this->principalURL))),
self::mkprop(groupdav::CALDAV,'calendar-user-address-set',$calendar_user_address_set),
self::mkprop(groupdav::CALENDARSERVER,'email-address-set',array(
self::mkprop(groupdav::CALENDARSERVER,'email-address',$GLOBALS['egw_info']['user']['email']))),
//self::mkprop('principal-URL',array(self::mkprop('href',$this->principalURL))),
self::mkprop('principal-collection-set',array(self::mkprop('href',$this->base_uri.'/principals/'))),
// OUTBOX URLs of the current user
self::mkprop(groupdav::CALDAV,'schedule-outbox-URL',array(
self::mkprop(groupdav::DAV,'href',$this->base_uri.'/calendar/'))),
);
//$props = self::current_user_privilege_set($props);
$files['files'][] = array(
'path' => $path,
'props' => $props,
);
if ($options['depth'])
{
if (strlen($path) == 1) // GroupDAV Root
{
// principals collection
$files['files'][] = array(
'path' => '/principals/',
'props' => array(
self::mkprop('displayname',lang('Accounts')),
self::mkprop('resourcetype',array(self::mkprop('principals',''))),
self::mkprop('current-user-principal',array(self::mkprop('href',$this->principalURL))),
//self::mkprop(groupdav::CALDAV,'calendar-home-set',array(
// self::mkprop('href',$this->base_uri.$user_prefix))),
//self::mkprop(groupdav::CARDDAV,'addressbook-home-set',array(
// self::mkprop('href',$this->base_uri.$user_prefix))),
//self::mkprop('principal-URL',array(self::mkprop('href',$this->principalURL))),
),
);
}
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[] = self::mkprop(
groupdav::CALENDARSERVER,'getctag',$handler->getctag($options['path'],$user));
}
$props[] = self::mkprop('getetag','EGw-'.$app.'-wGE');
$files['files'][] = array(
'path' => $path.$app.'/',
'props' => $props,
);
}
}
return true;
}
if ($app != 'principals' && !$GLOBALS['egw_info']['user']['apps'][$app])
$displayname = translation::convert($displayname, translation::charset(),'utf-8');
// self url
$files['files'][] = $this->add_collection($path, array(
'displayname' => $displayname,
'owner' => $path == '/' ? '' : array(self::mkprop('href',$this->base_uri.'/principals/'.$principalType.'/'.$account_lid.'/')),
));
if ($depth)
{
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
foreach($this->root as $app => $data)
{
$files['files'][0] = array(
'path' => $path.$app.'/',
// KAddressbook doubles the folder, if the self URL contains the GroupDAV/CalDAV resourcetypes
'props' => $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'][] = HTTP_WebDAV_Server::mkprop(
groupdav::CALENDARSERVER,'getctag',$handler->getctag($options['path'],$user));
}
if (!$options['depth']) return true; // depth 0 --> show only the self url
if (!$GLOBALS['egw_info']['user']['apps'][$data['app'] ? $data['app'] : $app]) continue; // no rights for the given app
if (!empty($data['user-only']) && ($path == '/' || $user < 0)) continue;
$files['files'][] = $this->add_app($app,false,$user,$path);
}
return $handler->propfind($this->_slashify($options['path']),$options,$files,$user,$id);
}
return '501 Not Implemented';
return true;
}
/**
* Get the properties of a collection
* Add an application collection to a user home or the root
*
* @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
* @return array with values for keys 'path' and 'props'
*/
function _properties($app,$no_extra_types=false,$user=null,$path='/')
protected function add_app($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'];
@ -416,8 +627,7 @@ class groupdav extends HTTP_WebDAV_Server
}
$account = $this->accounts->read($account_lid);
$displayname = $GLOBALS['egw']->translation->convert($account['account_fullname'],
$GLOBALS['egw']->translation->charset(),'utf-8');
$displayname = translation::convert($account['account_fullname'],translation::charset(),'utf-8');
if ($user < 0)
{
@ -429,82 +639,86 @@ class groupdav extends HTTP_WebDAV_Server
}
$props = array(
self::mkprop('current-user-principal',array(self::mkprop('href',$this->principalURL))),
self::mkprop('owner',array(self::mkprop('href',$this->base_uri.'/principals/'.$principalType.'/'.$account_lid.'/'))),
//self::mkprop('principal-URL',array(self::mkprop('href',$this->principalURL))),
self::mkprop('alternate-URI-set',array(
self::mkprop('href','MAILTO:'.$GLOBALS['egw_info']['user']['email']))),
self::mkprop('principal-collection-set',array(
self::mkprop('href',$this->base_uri.'/principals/'),
)),
self::mkprop('principal-URL',array(self::mkprop('href',$this->principalURL))),
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']))),
self::mkprop(groupdav::CALENDARSERVER,'email-address-set',array(
self::mkprop(groupdav::CALENDARSERVER,'email-address',$GLOBALS['egw_info']['user']['email']))),
self::mkprop('getetag','EGw-no-etag-wGE'), // iPhone addressbook requires an etag here!
'owner' => array(self::mkprop('href',$this->base_uri.'/principals/'.$principalType.'/'.$account_lid.'/')),
);
$displayname = translation::convert(lang($app).' '.
common::grab_owner_name($user),$this->egw_charset,'utf-8');
switch ($app)
{
case 'calendar':
$props[] = self::mkprop(groupdav::CALDAV,'calendar-home-set',array(
self::mkprop('href',$this->base_uri.$path.'calendar/')));
// OUTBOX URLs of the current user
$props[] = self::mkprop(groupdav::CALDAV,'schedule-outbox-URL',
array(self::mkprop(groupdav::DAV,'href',$this->base_uri.'/calendar/')));
$props[] = self::mkprop(groupdav::ICAL,'calendar-color',$display_color);
$props['calendar-color'] = self::mkprop(groupdav::ICAL,'calendar-color',$display_color);
break;
case 'infolog':
$props[] = self::mkprop(groupdav::CALDAV,'calendar-home-set',array(
self::mkprop('href',$this->base_uri.$path.'infolog/')));
$displayname = translation::convert(lang($app).' '.
common::grab_owner_name($user),$this->egw_charset,'utf-8');
break;
case 'inbox':
$displayname = lang('Scheduling inbox').' '.common::grab_owner_name($user);
break;
case 'outbox':
$displayname = lang('Scheduling outbox').' '.common::grab_owner_name($user);
break;
default:
$props[] = 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');
common::grab_owner_name($user),$this->egw_charset,'utf-8');
}
$props[] = self::mkprop(groupdav::CARDDAV,'addressbook-home-set',array(
self::mkprop('href',$this->base_uri.$path)));
$props[] = self::mkprop('displayname',$displayname);
$props['displayname'] = $displayname;
foreach((array)$this->root[$app] as $prop => $values)
{
if ($prop == 'resourcetype')
switch($prop)
{
$resourcetype = array(
self::mkprop('collection',''),
);
if (!$no_extra_types)
{
foreach($this->root[$app]['resourcetype'] as $ns => $type)
case 'resourcetype';
if (!$no_extra_types)
{
$resourcetype[] = self::mkprop($ns,$type,'');
foreach($this->root[$app]['resourcetype'] as $ns => $type)
{
$props['resourcetype'][] = self::mkprop($ns,$type,'');
}
// add /addressbook/ as directory gateway
if ($app == 'addressbook' && $path == '/')
{
$props['resourcetype'][] = self::mkprop(self::CARDDAV, 'directory', '');
}
}
}
$props[] = self::mkprop('resourcetype',$resourcetype);
}
else
{
foreach($values as $ns => $value)
{
$props[] = self::mkprop($ns,$prop,$value);
}
break;
case 'app':
case 'user-only':
break; // no props, already handled
default:
if (is_array($values))
{
foreach($values as $ns => $value)
{
$props[$prop] = self::mkprop($ns,$prop,$value);
}
}
else
{
$props[$prop] = $values;
}
break;
}
}
if (method_exists($app.'_groupdav','extra_properties'))
// add other handler specific properties
if (($handler = self::app_handler($app)))
{
$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);
if (method_exists($handler,'extra_properties'))
{
$displayname = translation::convert(
$account['account_id'] > 0 ? $account['account_fullname'] : lang('Group').' '.$account['account_lid'],
translation::charset(),'utf-8');
$props = $handler->extra_properties($props,$displayname,$this->base_uri,$user);
}
// add ctag if handler implements it
if (method_exists($handler,'getctag') && $this->prop_requested('getctag') === true)
{
$props['getctag'] = self::mkprop(
groupdav::CALENDARSERVER,'getctag',$handler->getctag($path,$user));
}
}
return $props;
if ($handler) $privileges = $handler->current_user_privileges($path.$app.'/', $user) ;
return $this->add_collection($path.$app.'/', $props, $privileges);
}
/**
@ -574,11 +788,11 @@ class groupdav extends HTTP_WebDAV_Server
{
return $ret; // no collection
}
header('Content-type: text/html; charset='.$GLOBALS['egw']->translation->charset());
header('Content-type: text/html; charset='.translation::charset());
echo "<html>\n<head>\n\t<title>".'EGroupware (Cal|Card|Group)DAV server '.htmlspecialchars($options['path'])."</title>\n";
echo "\t<meta http-equiv='content-type' content='text/html; charset=utf-8' />\n";
echo "\t<style type='text/css'>\n.th { background-color: #e0e0e0; }\n.row_on { background-color: #F1F1F1; }\n".
".row_off { background-color: #ffffff; }\ntd { padding-left: 5px; }\nth { padding-left: 5px; text-align: left; }\n\t</style>\n";
echo "\t<style type='text/css'>\n.th { background-color: #e0e0e0; }\n.row_on { background-color: #F1F1F1; vertical-align: top; }\n".
".row_off { background-color: #ffffff; vertical-align: top; }\ntd { padding-left: 5px; }\nth { padding-left: 5px; text-align: left; }\n\t</style>\n";
echo "</head>\n<body>\n";
echo '<h1>(Cal|Card|Group)DAV ';
@ -617,12 +831,17 @@ class groupdav extends HTTP_WebDAV_Server
$name = basename($file['path']);
}
echo "\t<tr class='$class'>\n\t\t<td>$n</td>\n\t\t<td>".html::a_href(htmlspecialchars($name),'/groupdav.php'.$file['path'])."</td>\n";
echo "\t<tr class='$class'>\n\t\t<td>$n</td>\n\t\t<td>".
html::a_href(htmlspecialchars($name),'/groupdav.php'.strtr($file['path'], array(
'%' => '%25',
'#' => '%23',
'?' => '%3F',
)))."</td>\n";
echo "\t\t<td>".$props['DAV:getcontentlength']."</td>\n";
echo "\t\t<td>".(!empty($props['DAV:getlastmodified']) ? date('Y-m-d H:i:s',$props['DAV:getlastmodified']) : '')."</td>\n";
echo "\t\t<td>".$props['DAV:getetag']."</td>\n";
echo "\t\t<td>".htmlspecialchars($props['DAV:getcontenttype'])."</td>\n";
echo "\t\t<td>".self::prop_value($props['DAV:resourcetype'])."</td>\n\t</tr>\n";
echo "\t\t<td>".$props['DAV:getcontenttype']."</td>\n";
echo "\t\t<td>".$props['DAV:resourcetype']."</td>\n\t</tr>\n";
}
if (!$n)
{
@ -640,11 +859,16 @@ class groupdav extends HTTP_WebDAV_Server
$ns = explode(':',$name);
$name = array_pop($ns);
$ns = implode(':',$ns);
echo "\t<tr class='$class'>\n\t\t<td>".htmlspecialchars($ns)."</td><td>".htmlspecialchars($name)."</td>\n";
echo "\t\t<td>".self::prop_value($value)."</td>\n\t</tr>\n";
echo "\t<tr class='$class'>\n\t\t<td>".htmlspecialchars($ns)."</td><td style='white-space: nowrap'>".htmlspecialchars($name)."</td>\n";
echo "\t\t<td>".$value."</td>\n\t</tr>\n";
}
echo "</table>\n";
$dav = array(1);
$allow = false;
$this->OPTIONS($options['path'], $dav, $allow);
echo "<p>DAV: ".implode(', ', $dav)."</p>\n";
echo "</body>\n</html>\n";
common::egw_exit();
@ -664,15 +888,27 @@ class groupdav extends HTTP_WebDAV_Server
{
$value = $this->_hierarchical_prop_encode($value);
}
$value = htmlspecialchars(array2string($value));
$value = array2string($value);
}
elseif (preg_match('/\<(D:)?href\>[^<]+\<\/(D:)?href\>/i',$value))
if ($value[0] == '<' && function_exists('tidy_repair_string'))
{
$value = preg_replace('/\<(D:)?href\>([^<]+)\<\/(D:)?href\>/i','&lt;\\1href&gt;<a href="\\2">\\2</a>&lt;/\\3href&gt;<br />',$value);
$value = tidy_repair_string($value, array(
'indent' => true,
'show-body-only' => true,
'output-encoding' => 'utf-8',
'input-encoding' => 'utf-8',
'input-xml' => true,
'output-xml' => true,
'wrap' => 0,
));
}
if (preg_match('/\<(D:)?href\>[^<]+\<\/(D:)?href\>/i',$value))
{
$value = '<pre>'.preg_replace('/\<(D:)?href\>([^<]+)\<\/(D:)?href\>/i','&lt;\\1href&gt;<a href="\\2">\\2</a>&lt;/\\3href&gt;',$value).'</pre>';
}
else
{
$value = htmlspecialchars($value);
$value = $value[0] == '<' || strpos($value, "\n") !== false ? '<pre>'.htmlspecialchars($value).'</pre>' : htmlspecialchars($value);
}
return $value;
}
@ -688,27 +924,36 @@ class groupdav extends HTTP_WebDAV_Server
$arr = array();
foreach($props as $prop)
{
$ns_hash = array('DAV:' => 'D');
switch($prop['ns'])
{
case 'DAV:';
$ns = 'DAV';
break;
case self::CALDAV:
$ns = 'CalDAV';
$ns = $ns_hash[$prop['ns']] = 'CalDAV';
break;
case self::CARDDAV:
$ns = 'CardDAV';
$ns = $ns_hash[$prop['ns']] = 'CardDAV';
break;
case self::GROUPDAV:
$ns = 'GroupDAV';
$ns = $ns_hash[$prop['ns']] = 'GroupDAV';
break;
default:
$ns = $prop['ns'];
}
$ns_defs = '';
$ns_hash = array($prop['ns'] => $ns, 'DAV:' => 'D');
$arr[$ns.':'.$prop['name']] = is_array($prop['val']) ?
$this->_hierarchical_prop_encode($prop['val'], $prop['ns'], $ns_defs, $ns_hash) : $prop['val'];
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:']);
$value = strtr($v=$this->prop_value($prop['val']),array_flip($ns_hash));
}
else
{
$value = $this->prop_value($prop['val']);
}
$arr[$ns.':'.$prop['name']] = $value;
}
return $arr;
}
@ -982,29 +1227,26 @@ class groupdav extends HTTP_WebDAV_Server
/**
* Add the privileges of the current user
*
* @param array $props=array() regular props by the groupdav handler
* @return array
* @return array self::mkprop('privilege',array(...))
*/
static function current_user_privilege_set(array $props=array())
static function current_user_privilege_set()
{
$props[] = HTTP_WebDAV_Server::mkprop('current-user-privilege-set',
array(HTTP_WebDAV_Server::mkprop('privilege',
array(//HTTP_WebDAV_Server::mkprop('all',''),
HTTP_WebDAV_Server::mkprop('read',''),
HTTP_WebDAV_Server::mkprop('read-free-busy',''),
//HTTP_WebDAV_Server::mkprop('read-current-user-privilege-set',''),
HTTP_WebDAV_Server::mkprop('bind',''),
HTTP_WebDAV_Server::mkprop('unbind',''),
HTTP_WebDAV_Server::mkprop('schedule-post',''),
HTTP_WebDAV_Server::mkprop('schedule-post-vevent',''),
HTTP_WebDAV_Server::mkprop('schedule-respond',''),
HTTP_WebDAV_Server::mkprop('schedule-respond-vevent',''),
HTTP_WebDAV_Server::mkprop('schedule-deliver',''),
HTTP_WebDAV_Server::mkprop('schedule-deliver-vevent',''),
HTTP_WebDAV_Server::mkprop('write',''),
HTTP_WebDAV_Server::mkprop('write-properties',''),
HTTP_WebDAV_Server::mkprop('write-content',''),
))));
return $props;
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',''),
)));
}
}

View File

@ -37,6 +37,12 @@ abstract class groupdav_handler
* @var accounts
*/
var $accounts;
/**
* Reference to the ACL class
*
* @var acl
*/
var $acl;
/**
* Translates method names into ACL bits
*
@ -53,18 +59,18 @@ abstract class groupdav_handler
* @var string
*/
var $app;
/**
* Calling groupdav object
*
* @var groupdav
*/
var $groupdav;
/**
* Base url of handler, need to prefix all pathes not automatic handled by HTTP_WebDAV_Server
*
* @var string
*/
var $base_uri;
/**
* principal URL
*
* @var string
*/
var $principalURL;
/**
* HTTP_IF_MATCH / etag of current request / last call to _common_get_put_delete() method
*
@ -85,33 +91,32 @@ abstract class groupdav_handler
*/
static $path_extension = '.ics';
/**
* Which attribute to use to contruct name part of url/path
*
* @var string
*/
static $path_attr = 'id';
/**
* Constructor
*
* @param string $app 'calendar', 'addressbook' or 'infolog'
* @param int $debug=null debug-level to set
* @param string $base_uri=null base url of handler
* @param string $principalURL=null pricipal url of handler
* @param groupdav $groupdav calling class
*/
function __construct($app,$debug=null,$base_uri=null,$principalURL=null)
function __construct($app, groupdav $groupdav)
{
$this->app = $app;
if (!is_null($debug)) $this->debug = $debug;
$this->base_uri = is_null($base_uri) ? $base_uri : $_SERVER['SCRIPT_NAME'];
if (is_null($principalURL))
{
$this->principalURL = (@$_SERVER["HTTPS"] === "on" ? "https:" : "http:") .
'//'.$_SERVER['HTTP_HOST'] . $_SERVER['SCRIPT_NAME'] . '/';
}
else
{
$this->principalURL = $principalURL.'principals/users/'.
$GLOBALS['egw_info']['user']['account_lid'].'/';
}
if (!is_null($parent->debug)) $this->debug = $groupdav->debug;
$this->base_uri = $groupdav->base_uri;
$this->groupdav = $groupdav;
$this->agent = self::get_agent();
$this->egw_charset = translation::charset();
$this->accounts = $GLOBALS['egw']->accounts;
$this->acl = $GLOBALS['egw']->acl;
}
/**
@ -188,9 +193,10 @@ abstract class groupdav_handler
* @param array $props=array() regular props by the groupdav handler
* @param string $displayname
* @param string $base_uri=null base url of handler
* @param int $user=null account_id of owner of collection
* @return array
*/
static function extra_properties(array $props=array(), $displayname, $base_uri=null)
public function extra_properties(array $props=array(), $displayname, $base_uri=null, $user=null)
{
return $props;
}
@ -212,7 +218,7 @@ abstract class groupdav_handler
// error_log(__METHOD__."(".array2string($entry).") Cant create etag!");
return false;
}
return 'EGw-'.$entry['id'].':'.(isset($entry['etag']) ? $entry['etag'] : $entry['modified']).'-wGE';
return $entry['id'].':'.(isset($entry['etag']) ? $entry['etag'] : $entry['modified']);
}
/**
@ -238,9 +244,10 @@ abstract class groupdav_handler
* @param array &$options
* @param int|string &$id on return self::$path_extension got removed
* @param boolean &$return_no_access=false if set to true on call, instead of '403 Forbidden' the entry is returned and $return_no_access===false
* @param boolean $ignore_if_match=false if true, ignore If-Match precondition
* @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,&$return_no_access=false)
function _common_get_put_delete($method,&$options,&$id,&$return_no_access=false,$ignore_if_match=false)
{
if (self::$path_extension) $id = basename($id,self::$path_extension);
@ -269,29 +276,35 @@ abstract class groupdav_handler
$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']))
if (isset($_SERVER['HTTP_IF_MATCH']) && !$ignore_if_match)
{
if (strstr($_SERVER['HTTP_IF_MATCH'], $etag) === false)
$this->http_if_match = $_SERVER['HTTP_IF_MATCH'];
// strip of quotes around etag, if they exist, that way we allow etag with and without quotes
if ($this->http_if_match[0] == '"') $this->http_if_match = substr($this->http_if_match, 1, -1);
if ($this->http_if_match !== $etag)
{
$this->http_if_match = $_SERVER['HTTP_IF_MATCH'];
if ($this->debug) error_log(__METHOD__."($method,,$id) HTTP_IF_MATCH='$_SERVER[HTTP_IF_MATCH]', etag='$etag': 412 Precondition failed");
return '412 Precondition Failed';
}
else
{
$this->http_if_match = $etag;
// 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']))
{
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';
}
}
}
if (isset($_SERVER['HTTP_IF_NONE_MATCH']))
{
if ($this->debug) error_log(__METHOD__."($method,,$id) HTTP_IF_NONE_MATCH='$_SERVER[HTTP_IF_NONE_MATCH]', etag='$etag': 412 Precondition failed");
return '412 Precondition Failed';
$if_none_match = $_SERVER['HTTP_IF_NONE_MATCH'];
// strip of quotes around etag, if they exist, that way we allow etag with and without quotes
if ($if_none_match[0] == '"') $if_none_match = substr($if_none_match, 1, -1);
// 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 (in_array($method, array('GET','HEAD')) && $etag === $if_none_match)
{
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';
}
if ($method == 'PUT' && ($if_none_match == '*' || $if_none_match == $etag))
{
if ($this->debug) error_log(__METHOD__."($method,,$id) HTTP_IF_NONE_MATCH='$_SERVER[HTTP_IF_NONE_MATCH]', etag='$etag': 412 Precondition failed");
return '412 Precondition Failed';
}
}
}
return $entry;
@ -302,13 +315,10 @@ abstract class groupdav_handler
*
* @static
* @param string $app 'calendar', 'addressbook' or 'infolog'
* @param int $user=null owner of the collection, default current user
* @param int $debug=null debug-level to set
* @param string $base_uri=null base url of handler
* @param string $principalURL=null pricipal url of handler
* @param groupdav $groupdav calling class
* @return groupdav_handler
*/
static function &app_handler($app,$debug=null,$base_uri=null,$principalURL=null)
static function app_handler($app, $groupdav)
{
static $handler_cache = array();
@ -317,13 +327,10 @@ abstract class groupdav_handler
$class = $app.'_groupdav';
if (!class_exists($class) && !class_exists($class = 'groupdav_'.$app)) return null;
$handler_cache[$app] = new $class($app,$debug,$base_uri,$principalURL);
$handler_cache[$app] = new $class($app, $groupdav);
}
$handler_cache[$app]->$debug = $debug;
$handler_cache[$app]->$base_uri = $base_uri;
$handler_cache[$app]->$principalURL = $principalURL;
if ($debug) error_log(__METHOD__."('$app', '$base_uri', '$principalURL')");
if ($debug) error_log(__METHOD__."('$app')");
return $handler_cache[$app];
}
@ -396,6 +403,96 @@ abstract class groupdav_handler
return $agent;
}
/**
* Return priviledges for current user, default is read and read-current-user-privilege-set
*
* Priviledges are for the collection, not the resources / entries!
*
* @param string $path path of collection
* @param int $user=null owner of the collection, default current user
* @return array with privileges
*/
public function current_user_privileges($path, $user=null)
{
static $grants;
if (is_null($grants))
{
$grants = $this->acl->get_grants($this->app, $this->app != 'addressbook');
}
$priviledes = array('read-current-user-privilege-set' => 'read-current-user-privilege-set');
if (!$user || $grants[$user] & EGW_ACL_READ)
{
$priviledes['read'] = 'read';
}
if (!$user || $grants[$user] & EGW_ACL_ADD)
{
$priviledes['bind'] = 'bind'; // PUT for new resources
}
if (!$user || $grants[$user] & EGW_ACL_EDIT)
{
$priviledes['write-content'] = 'write-content'; // otherwise iOS calendar does not allow to add events
}
if (!$user || $grants[$user] & EGW_ACL_DELETE)
{
$priviledes['unbind'] = 'unbind'; // DELETE
}
// copy/move of existing resources might require write-properties, thought we do not support an explicit PROPATCH
return $priviledes;
}
/**
* Create the path/name for an entry
*
* @param array $entry
* @return string
*/
function get_path($entry)
{
return $entry[self::$path_attr].self::$path_extension;
}
/**
* Add a resource
*
* @param string $path path of collection, NOT entry!
* @param array $entry
* @param array $props
* @return array with values for keys 'path' and 'props'
*/
public function add_resource($path, array $entry, array $props)
{
foreach(array(
'getetag' => $this->get_etag($entry),
'getcontenttype' => 'text/calendar',
'getlastmodified' => $entry['modified'],
'displayname' => $entry['title'],
) as $name => $value)
{
if (!isset($props[$name]))
{
$props[$name] = $value;
}
}
// if requested add privileges
$privileges = array('read', 'read-current-user-privilege-set');
if ($this->groupdav->prop_requested('current-user-privilege-set') === true && !isset($props['current-user-privilege-set']))
{
if ($this->check_access(EGW_ACL_EDIT, $entry))
{
$privileges[] = 'write-content';
}
}
if ($this->groupdav->prop_requested('owner') === true && !isset($props['owner']) &&
($account_lid = $this->accounts->name2id($entry['owner'])))
{
$type = $this->accounts->get_type($entry['owner']) == 'u' ? 'users' : 'groups';
$props['owner'] = HTTP_WebDAV_Server::mkprop('href', $this->base_uri.'/principals/'.$type.'/'.$account_lid.'/');
}
// we urldecode here, as HTTP_WebDAV_Server uses a minimal (#?%) urlencoding for incomming pathes and urlencodes pathes in propfind
return $this->groupdav->add_resource($path.urldecode($this->get_path($entry)), $props, $privileges);
}
}
/**

View File

@ -93,6 +93,27 @@ class groupdav_hooks
'default' => 'P',
);
translation::add_app('infolog');
$infolog = new infolog_bo();
if (!($types = $infolog->enums['type']))
{
$types = array(
'task' => 'Tasks',
);
}
$settings['infolog-types'] = array(
'type' => 'multiselect',
'label' => 'InfoLog types to sync',
'name' => 'infolog-types',
'help' => 'Which InfoLog types should be synced with the device, default only tasks.',
'values' => $types,
'default' => 'task',
'xmlrpc' => True,
'admin' => False,
);
$settings['debug_level'] = array(
'type' => 'select',
'label' => 'Debug level for Apache/PHP error-log',

File diff suppressed because it is too large Load Diff

View File

@ -470,7 +470,8 @@ class Horde_iCalendar {
{
// Default values.
$requiredAttributes['PRODID'] = '-//The Horde Project//Horde_iCalendar Library' . (defined('HORDE_VERSION') ? ', Horde ' . constant('HORDE_VERSION') : '') . '//EN';
$requiredAttributes['METHOD'] = 'PUBLISH';
// METHOD is only required for iTip, but not for CalDAV, therefore removing it here calendar_ical sets it anyway by default
//$requiredAttributes['METHOD'] = 'PUBLISH';
foreach ($requiredAttributes as $name => $default_value) {
if (is_a($this->getattribute($name), 'PEAR_Error')) {
@ -821,17 +822,15 @@ class Horde_iCalendar {
case 'ORG':
$value = trim($value);
// As of rfc 2426 2.4.2 semicolon, comma, and colon must
// be escaped (comma is unescaped after splitting below).
$value = str_replace(array('\\n', '\\N', '\\;', '\\:'),
array("\n", "\n", ';', ':'),
// be escaped (semicolon is unescaped after splitting below).
$value = str_replace(array('\\n', '\\N', '\\,', '\\:'),
array("\n", "\n", ',', ':'),
$value);
// Split by unescaped semicolons:
$values = preg_split('/(?<!\\\\);/', $value);
$value = str_replace('\\;', ';', $value);
$values = str_replace('\\;', ';', $values);
$value = str_replace('\\,', ',', $value);
$values = str_replace('\\,', ',', $values);
$this->setAttribute($tag, trim($value), $params, true, $values);
break;
@ -840,15 +839,13 @@ class Horde_iCalendar {
case 'CATEGORIES':
$value = trim($value);
// As of rfc 2426 2.4.2 semicolon, comma, and colon must
// be escaped (semicolon is unescaped after splitting below).
$value = str_replace(array('\\n', '\\N', '\\,', '\\:'),
array("\n", "\n", ',', ':'),
// be escaped (comma is unescaped after splitting below).
$value = str_replace(array('\\n', '\\N', '\\;', '\\:'),
array("\n", "\n", ';', ':'),
$value);
// Split by unescaped commas:
$values = preg_split('/(?<!\\\\),/', $value);
$value = str_replace('\\;', ';', $value);
$values = str_replace('\\;', ';', $values);
$value = str_replace('\\,', ',', $value);
$values = str_replace('\\,', ',', $values);
$this->setAttribute($tag, trim($value), $params, true, $values);
@ -860,16 +857,14 @@ class Horde_iCalendar {
$value = trim($value);
// vCalendar 1.0 and vCard 2.1 only escape semicolons
// and use unescaped semicolons to create lists.
$value = str_replace(array('\\n', '\\N', '\\;', '\\:'),
array("\n", "\n", ';', ':'),
$value = str_replace(array('\\n', '\\N', '\\,', '\\:'),
array("\n", "\n", ',', ':'),
$value);
// Split by unescaped semicolons:
$values = preg_split('/(?<!\\\\);/', $value);
$value = str_replace('\\;', ';', $value);
$values = str_replace('\\;', ';', $values);
$value = str_replace('\\,', ',', $value);
$values = str_replace('\\,', ',', $values);
$this->setAttribute($tag, trim($value), $params, true, $values);
} else {
$value = trim($value);

View File

@ -1,6 +1,6 @@
<?php
/**
* eGroupWare - resources
* EGroupware - resources
*
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
* @package resources
@ -24,7 +24,7 @@ class resources_bo
/**
* Instance of resources so object
*
* @var so_resources
* @var resources_so
*/
var $so;
/**
@ -165,7 +165,7 @@ class resources_bo
}
}
}
$rows[$num]['picture_thumb'] = $this->get_picture($resource['res_id']);
$rows[$num]['picture_thumb'] = $this->get_picture($resource);
$rows[$num]['admin'] = $this->acl->get_cat_admin($resource['cat_id']);
}
return $nr;
@ -627,21 +627,18 @@ class resources_bo
/**
* get resource picture either from vfs or from symlink
* Cornelius Weiss <egw@von-und-zu-weiss.de>
* @param int $res_id id of resource
* @param int|array $resource res-id or whole resource array
* @param bool $fullsize false = thumb, true = full pic
* @return string url of picture
*/
function get_picture($res_id=0,$fullsize=false)
function get_picture($resource,$fullsize=false)
{
if ($res_id > 0)
{
$src = $this->so->get_value('picture_src',$res_id);
}
#echo $scr."<br>". $this->pictures_dir."<br>";
switch($src)
if ($resource && !is_array($resource)) $resource = $this->read($resource);
switch($resource['picture_src'])
{
case 'own_src':
$picture = egw_link::vfs_path('resources',$res_id,self::PICTURE_NAME,true); // vfs path
$picture = egw_link::vfs_path('resources',$resource['res_id'],self::PICTURE_NAME,true); // vfs path
if ($fullsize)
{
$picture = egw::link(egw_vfs::download_url($picture));
@ -650,17 +647,17 @@ class resources_bo
{
$picture = egw::link('/etemplate/thumbnail.php',array('path' => $picture));
}
//$picture=$GLOBALS['egw_info']['server'].$picture;
#echo $picture."<br>";
break;
case 'cat_src':
list($picture) = $this->cats->return_single($this->so->get_value('cat_id',$res_id));
list($picture) = $this->cats->return_single($resource['cat_id']);
$picture = unserialize($picture['data']);
if($picture['icon'])
{
$picture = $GLOBALS['egw_info']['server']['webserver_url'].'/phpgwapi/images/'.$picture['icon'];
break;
}
// fall through
case 'gen_src':
default :
$picture = $GLOBALS['egw_info']['server']['webserver_url'].$this->resource_icons;
@ -670,7 +667,6 @@ class resources_bo
}
/**
* remove_picture
* removes picture from vfs
*
* Cornelius Weiss <egw@von-und-zu-weiss.de>

View File

@ -1,6 +1,6 @@
<?php
/**
* eGroupWare - resources
* EGroupware - resources
*
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
* @package resources
@ -35,21 +35,48 @@ class resources_so extends so_sql_cf
*/
function get_value($key,$res_id)
{
return $this->db->select($this->table_name,$key,array('res_id' => $res_id),__LINE__,__FILE__)->fetchColumn();
return $res_id == $this->data['res_id'] ? $this->data[$key] :
$this->db->select($this->table_name,$key,array('res_id' => $res_id),__LINE__,__FILE__)->fetchColumn();
}
/**
* reads resource including custom fields
*
* @param interger $res_id res_id
* @return array/boolean data if row could be retrived else False
* Reimplemented to do some minimal caching (re-use already read data)
*
* @param int|array $res_id res_id
* @return array|boolean data if row could be retrived else False
*/
function read($res_id)
{
// read main data
$resource = parent::read($res_id);
if (is_array($res_id) && count($res_id) == 1 && isset($res_id['res_id'])) $res_id = $res_id['res_id'];
return $resource;
/*if (!is_array($res_id) && $res_id == $this->data['res_id'])
{
error_log(__METHOD__.'('.array2string($res_id).') this->data[res_id]='.array2string($this->data['res_id']).' --> returning this->data');
}
else
{
error_log(__METHOD__.'('.array2string($res_id).') this->data[res_id]='.array2string($this->data['res_id']).' --> returning parent::read()');
}*/
return !is_array($res_id) && $res_id == $this->data['res_id'] ? $this->data : parent::read($res_id);
}
/**
* deletes resource
*
* Reimplemented to do some minimal caching (re-use already read data)
*
* @param int|array $res_id id of resource
* @return int|array affected rows, should be 1 if ok, 0 if an error or array with id's if $only_return_ids
*/
function delete($res_id)
{
if (($ok = parent::delete($res_id)) && !is_array($res_id) && $res_id == $this->data['res_id'])
{
unset($this->data);
}
return $ok;
}
/**
@ -66,5 +93,4 @@ class resources_so extends so_sql_cf
return $res_id;
}
}

View File

@ -28,8 +28,8 @@ class ui_acl
function ui_acl()
{
$this->bo =& createobject('resources.bo_acl',True);
$this->nextmatchs =& createobject('phpgwapi.nextmatchs');
$this->bo = createobject('resources.bo_acl',True);
$this->nextmatchs = createobject('phpgwapi.nextmatchs');
$this->start = $this->bo->start;
$this->query = $this->bo->query;
$this->order = $this->bo->order;
@ -46,10 +46,10 @@ class ui_acl
if ($_POST['btnDone'])
{
$GLOBALS['egw']->redirect_link('/admin/index.php');
egw::redirect_link('/admin/index.php');
}
$GLOBALS['egw']->common->egw_header();
common::egw_header();
echo parse_navbar();
if ($_POST['btnSave'])
@ -59,6 +59,7 @@ class ui_acl
$this->bo->set_rights($cat_id,$_POST['inputread'][$cat_id],$_POST['inputwrite'][$cat_id],
$_POST['inputcalread'][$cat_id],$_POST['inputcalbook'][$cat_id],$_POST['inputadmin'][$cat_id]);
}
config::save_value('location_cats', implode(',', $_POST['location_cats']), 'resources');
}
$template =& CreateObject('phpgwapi.Template',EGW_APP_TPL);
$template->set_file(array('acl' => 'acl.tpl'));
@ -74,7 +75,8 @@ class ui_acl
'lang_calread' => lang('Read Calendar permissions'),
'lang_calbook' => lang('Direct booking permissions'),
'lang_implies_book' => lang('implies booking permission'),
'lang_cat_admin' => lang('Categories admin')
'lang_cat_admin' => lang('Categories admin'),
'lang_locations_rooms' => lang('Locations / rooms'),
));
$left = $this->nextmatchs->left('/index.php',$this->start,$this->bo->catbo->total_records,'menuaction=resources.ui_acl.acllist');
@ -91,23 +93,28 @@ class ui_acl
'query' => $this->query,
));
@reset($this->bo->cats);
while (list(,$cat) = @each($this->bo->cats))
if ($this->bo->cats)
{
$this->rights = $this->bo->get_rights($cat['id']);
$config = config::read('resources');
$location_cats = $config['location_cats'] ? explode(',', $config['location_cats']) : array();
foreach($this->bo->cats as $cat)
{
$this->rights = $this->bo->get_rights($cat['id']);
$tr_color = $this->nextmatchs->alternate_row_color($tr_color);
$template->set_var(array(
'tr_color' => $tr_color,
'catname' => $cat['name'],
'catid' => $cat['id'],
'read' => $this->selectlist(EGW_ACL_READ),
'write' => $this->selectlist(EGW_ACL_ADD),
'calread' => $this->selectlist(EGW_ACL_CALREAD),
'calbook' =>$this->selectlist(EGW_ACL_DIRECT_BOOKING),
'admin' => '<option value="" selected="1">'.lang('choose categories admin').'</option>'.$this->selectlist(EGW_ACL_CAT_ADMIN,true)
));
$template->parse('Cblock','cat_list',True);
$tr_color = $this->nextmatchs->alternate_row_color($tr_color);
$template->set_var(array(
'tr_color' => $tr_color,
'catname' => $cat['name'],
'catid' => $cat['id'],
'read' => $this->selectlist(EGW_ACL_READ),
'write' => $this->selectlist(EGW_ACL_ADD),
'calread' => $this->selectlist(EGW_ACL_CALREAD),
'calbook' =>$this->selectlist(EGW_ACL_DIRECT_BOOKING),
'admin' => '<option value="" selected="1">'.lang('choose categories admin').'</option>'.$this->selectlist(EGW_ACL_CAT_ADMIN,true),
'location_checked' => in_array($cat['id'], $location_cats) ? 'checked="1"' : '',
));
$template->parse('Cblock','cat_list',True);
}
}
$template->pfp('out','acl',True);
}
@ -140,7 +147,7 @@ class ui_acl
{
$selectlist .= ' selected="selected"';
}
$selectlist .= '>' . $GLOBALS['egw']->common->display_fullname($account['account_lid'],$account['account_firstname'],
$selectlist .= '>' . common::display_fullname($account['account_lid'],$account['account_firstname'],
$account['account_lastname'],$account['account_id']) . '</option>' . "\n";
}
}
@ -150,6 +157,6 @@ class ui_acl
function deny()
{
echo '<p><center><b>'.lang('Access not permitted').'</b></center>';
$GLOBALS['egw']->common->egw_exit(True);
common::egw_exit(True);
}
}

View File

@ -18,7 +18,7 @@ category: resources de Kategorie
check all resources de Alle auswählen
choose categories admin resources de Wählen Sie einen Verwalter für diese Kategorie
clear selection resources de Auswahl löschen
configure access permissions resources de Zugangseinstellungen konfigurieren
configure access permissions admin de Zugangseinstellungen konfigurieren
create new accessory for this resource resources de Neues Zubehör zu dieser Ressource hinzufügen
create new links resources de Neue Verknüpfung erstellen
delete selected resources resources de Ausgewählte Ressourcen löschen
@ -49,6 +49,7 @@ links resources de Verknüpfungen
location resources de Lagerort
location of resource resources de Lagerort der Ressource
location: resources de Lagerort:
locations / rooms resources de Orte / Räume
name of resource resources de Name der Ressource
name: resources de Name:
no description available resources de Keine Beschreibung vorhanden
@ -95,6 +96,5 @@ web-site for this resource resources de Ausführliche Beschreibung der Ressource
where to find this resource? resources de Wo findet man diese Ressource?
which category does this resource belong to? resources de Zu welcher Kategorie gehört diese Ressource?
write permissions resources de Schreiberechtigung
you are not permitted to edit this reource! resources de Sie haben keine Erlaubnis diese Ressource zu bearbeiten
you are not permitted to get information about this resource! resources de Sie haben keine Erlaubnis sich informationen über diese Ressource anzuschauen
you chose more resources than available resources de Sie haben mehr Ressourcen ausgewählt als verfügbar sind

View File

@ -60,6 +60,7 @@ links resources en Links
location resources en Location
location of resource resources en Location of resource
location: resources en Location:
locations / rooms resources en Locations / rooms
long description resources en Long description
manage mapping resources en Manage mapping
name of resource resources en Name of resource

View File

@ -1,7 +1,7 @@
<?php
/**
* eGroupWare - resources
* http://www.egroupware.org
* http://www.egroupware.org
*
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
* @package resources
@ -14,6 +14,9 @@ $resources_table_prefix = 'egw_resources';
// Add a general category for resources
$GLOBALS['egw_setup']->db->insert($GLOBALS['egw_setup']->cats_table,array('cat_parent' => 0, 'cat_owner' => categories::GLOBAL_ACCOUNT,'cat_access' => 'public','cat_appname' => 'resources','cat_name' => 'General resources','cat_description' => 'This category has been added by setup','last_mod' => time()),false,__LINE__,__FILE__);
$cat_id = $GLOBALS['egw_setup']->db->get_last_insert_id($GLOBALS['egw_setup']->cats_table,'cat_id');
$GLOBALS['egw_setup']->db->insert($GLOBALS['egw_setup']->cats_table,array('cat_parent' => 0, 'cat_owner' => categories::GLOBAL_ACCOUNT,'cat_access' => 'public','cat_appname' => 'resources','cat_name' => 'Locations','cat_description' => 'This category has been added by setup','last_mod' => time()),false,__LINE__,__FILE__);
$locations_cat_id = $GLOBALS['egw_setup']->db->get_last_insert_id($GLOBALS['egw_setup']->cats_table,'cat_id');
config::save_value('location_cats', $locations_cat_id, 'resources');
// Give default group all rights to this general cat
$defaultgroup = $GLOBALS['egw_setup']->add_account('Default','Default','Group',False,False);
@ -21,8 +24,7 @@ $GLOBALS['egw_setup']->add_acl('resources','run',$defaultgroup);
$GLOBALS['egw_setup']->add_acl('resources',"L$cat_id",$defaultgroup,399);
// Add two rooms to give user an idea of what resources is...
$oProc->query("INSERT INTO {$resources_table_prefix} (name,cat_id,bookable,picture_src,accessory_of) VALUES ( 'Meeting room 1',$cat_id,1,'cat_src',-1)");
$oProc->query("INSERT INTO {$resources_table_prefix} (name,cat_id,bookable,picture_src,accessory_of) VALUES ( 'Meeting room 2',$cat_id,1,'cat_src',-1)");
$oProc->query("INSERT INTO {$resources_table_prefix} (name,cat_id,bookable,picture_src,accessory_of) VALUES ( 'Meeting room 1',$locations_cat_id,1,'cat_src',-1)");
$oProc->query("INSERT INTO {$resources_table_prefix} (name,cat_id,bookable,picture_src,accessory_of) VALUES ( 'Meeting room 2',$locations_cat_id,1,'cat_src',-1)");
$res_id = $oProc->m_odb->get_last_insert_id($resources_table_prefix,'res_id');
$oProc->query("INSERT INTO {$resources_table_prefix} (name,cat_id,bookable,picture_src,accessory_of) VALUES ( 'Fixed Beamer',$cat_id,0,'cat_src',$res_id)");

View File

@ -33,8 +33,9 @@
<!-- BEGIN cat_list -->
<tr bgcolor="{tr_color}">
<td>
{catname}<input type="hidden" name="catids[]" value="{catid}" /><br>
<select name="inputadmin[{catid}][]">{admin}</select>
&nbsp;{catname}<input type="hidden" name="catids[]" value="{catid}" /><br>
<select name="inputadmin[{catid}][]">{admin}</select><br>
<label><input type="checkbox" value="{catid}" name="location_cats[]" {location_checked} /> {lang_locations_rooms}</label>
</td>
<td align="center"><select multiple="multiple" size="5" name="inputread[{catid}][]">{read}</select></td>
<td align="center"><select multiple="multiple" size="5" name="inputwrite[{catid}][]">{write}</select></td>