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:
parent
a43ec990cd
commit
80510b5412
addressbook/inc
calendar/inc
class.calendar_bo.inc.phpclass.calendar_groupdav.inc.phpclass.calendar_ical.inc.phpclass.calendar_so.inc.phpclass.calendar_timezones.inc.php
egw-pear/HTTP/WebDAV
infolog/inc
class.infolog_bo.inc.phpclass.infolog_groupdav.inc.phpclass.infolog_ical.inc.phpclass.infolog_so.inc.php
phpgwapi/inc
class.groupdav.inc.phpclass.groupdav_handler.inc.phpclass.groupdav_hooks.inc.phpclass.groupdav_principals.inc.php
horde/Horde
resources
@ -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)
|
||||
{
|
||||
|
@ -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__ .
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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'].'>');
|
||||
|
@ -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';
|
||||
|
@ -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'];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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'],
|
||||
|
@ -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'])
|
||||
{
|
||||
|
@ -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','<\\1href><a href="\\2">\\2</a></\\3href><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','<\\1href><a href="\\2">\\2</a></\\3href>',$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',''),
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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
@ -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);
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)");
|
||||
|
@ -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>
|
||||
{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>
|
||||
|
Loading…
Reference in New Issue
Block a user