From 428ef55248c513015bc3233cf62c0e9db0dfbb3a Mon Sep 17 00:00:00 2001 From: Jack Bates Date: Thu, 13 Apr 2006 05:10:24 +0000 Subject: * Almost working preliminary REPORT support * ReportParser successfully parses calendar-data request values * _componentParser almost parses iCalendar files & limits by calendar-data request value * TODO Determine whether _componentParser is rejecting valid iCalendar files * TODO Reduce duplicate code by factoring special property handling out of propfind_response_helper * TODO Push filtering parser into bennu? --- lib/HTTP/CalDAV/Server.php | 491 +++++++++++++++++++++++++++++++-- lib/HTTP/CalDAV/Tools/ReportParser.php | 251 +++++++++++++++++ 2 files changed, 723 insertions(+), 19 deletions(-) create mode 100644 lib/HTTP/CalDAV/Tools/ReportParser.php (limited to 'lib/HTTP') diff --git a/lib/HTTP/CalDAV/Server.php b/lib/HTTP/CalDAV/Server.php index eabc27b..ab478d4 100644 --- a/lib/HTTP/CalDAV/Server.php +++ b/lib/HTTP/CalDAV/Server.php @@ -20,11 +20,13 @@ * @author Jack Bates * @copyright 2006 The PHP Group * @license PHP License 3.0 http://www.php.net/license/3_0.txt - * @version CVS: $Id: Server.php,v 1.1 2006/04/09 19:43:59 jablko Exp $ + * @version CVS: $Id: Server.php,v 1.2 2006/04/13 05:10:24 jablko Exp $ * @link http://pear.php.net/package/HTTP_CalDAV_Server * @see HTTP_WebDAV_Server */ +require_once 'Tools/ReportParser.php'; + /** * CalDav Server class * @@ -35,12 +37,25 @@ * @author Jack Bates * @copyright 2006 The PHP Group * @license PHP License 3.0 http://www.php.net/license/3_0.txt - * @version CVS: $Id: Server.php,v 1.1 2006/04/09 19:43:59 jablko Exp $ + * @version CVS: $Id: Server.php,v 1.2 2006/04/13 05:10:24 jablko Exp $ * @link http://pear.php.net/package/HTTP_CalDAV_Server * @see HTTP_WebDAV_Server */ class HTTP_CalDAV_Server extends HTTP_WebDAV_Server { + /** + * Make a property in the CalDAV namespace + * + * @param string property name + * @param string property value + * @return array string property namespace + * string property name + * string property value + */ + function calDavProp($name, $value=null, $status=null) { + return $this->mkprop('urn:ietf:params:xml:ns:caldav', $name, $value, + $status); + } /** * REPORT request helper - prepares data-structures from REPORT requests @@ -52,6 +67,20 @@ class HTTP_CalDAV_Server extends HTTP_WebDAV_Server function report_request_helper(&$options) { $options = array(); + $options['path'] = $this->path; + + $options['depth'] = 'infinity'; + if (isset($_SERVER['HTTP_DEPTH'])) { + $options['depth'] = $_SERVER['HTTP_DEPTH']; + } + + $parser = new ReportParser('php://input'); + if (!$parser->success) { + $this->http_status('400 Bad Request'); + return; + } + + $options['props'] = $parser->props; return true; } @@ -63,8 +92,168 @@ class HTTP_CalDAV_Server extends HTTP_WebDAV_Server * @return void * @access public */ - function report_response_helper($options, $responses) + function report_response_helper($options, $files) { + $responses = array(); + + // now loop over all returned files + foreach ($files as $file) { + + // collect namespaces here + $ns_hash = array('urn:ietf:params:xml:ns:caldav' => 'C'); + + $response = array(); + + $response['href'] = $this->getHref($file['path']); + if (isset($file['href'])) { + $response['href'] = $file['href']; + } + + $response['propstat'] = array(); + + // nothing to do if no properties were returend + if (isset($file['props']) && is_array($file['props'])) { + + // now loop over all returned properties + foreach ($file['props'] as $prop) { + $status = '200 OK'; + + // as a convenience feature we do not require user handlers + // restrict returned properties to the requested ones + // here we ignore unrequested entries + switch ($options['props']) { + case 'propname': + + // only names of all existing properties were requested + // so remove values + unset($prop['value']); + + case 'allprop': + if (isset($prop['status'])) { + $status = $prop['status']; + } + + if (!isset($response['propstat'][$status])) { + $response['propstat'][$status] = array(); + } + + $response['propstat'][$status][] = $prop; + break; + + default: + + // search property name in requested properties + foreach($options['props'] as $reqprop) { + if ($reqprop['name'] == $prop['name'] && + $reqprop['ns'] == $prop['ns']) { + if (isset($prop['status'])) { + $status = $prop['status']; + } + + if (!isset($response['propstat'][$status])) { + $response['propstat'][$status] = array(); + } + + $response['propstat'][$status][] = $prop; + break (2); + } + } + + continue (2); + } + + // namespace handling + if (empty($prop['ns']) || // empty namespace + $prop['ns'] == 'DAV:' || // default namespace + isset($ns_hash[$prop['ns']])) { // already known + continue; + } + + // register namespace + $ns_hash[$prop['ns']] = 'ns' . count($ns_hash); + } + } + + // also need empty entries for properties requested + // but for which no values where returned + if (isset($options['props']) && is_array($options['props'])) { + + // now loop over all requested properties + foreach ($options['props'] as $reqprop) { + $status = '404 Not Found'; + + // check if property exists in result + foreach ($file['props'] as $prop) { + if ($reqprop['name'] == $prop['name'] && + $reqprop['ns'] == $prop['ns']) { + continue (2); + } + } + + if ($reqprop['name'] == 'lockdiscovery' && + $reqprop['ns'] == 'DAV:' && + method_exists($this, 'getLocks')) { + + $status = '200 OK'; + if (!isset($response['propstat'][$status])) { + $response['propstat'][$status] = array(); + } + + $response['propstat'][$status][] = + $this->mkprop('DAV:', 'lockdiscovery', + $this->getLocks($file['path'])); + continue; + } + + if ($reqprop['name'] == 'calendar-data' && + $reqprop['ns'] == 'urn:ietf:params:xml:ns:caldav' && + method_exists($this, 'get')) { + + $prop = $this->_calendarData($file['path'], + $reqprop['value']); + if (isset($prop)) { + $status = '200 OK'; + if (isset($prop['status'])) { + $status = $prop['status']; + } + } else { + $prop = HTTP_CalDAV_Server::calDavProp( + 'calendar-data'); + } + + if (!isset($response['propstat'][$status])) { + $response['propstat'][$status] = array(); + } + + $response['propstat'][$status][] = $prop; + continue; + } + + if (!isset($response['propstat'][$status])) { + $response['propstat'][$status] = array(); + } + + // add empty value for this property + $response['propstat'][$status][] = + $this->mkprop($reqprop['ns'], $reqprop['name'], + null); + + // namespace handling + if (empty($reqprop['ns']) || // empty namespace + $reqprop['ns'] == 'DAV:' || // default namespace + isset($ns_hash[$reqprop['ns']])) { // already known + continue; + } + + // register namespace + $ns_hash[$reqprop['ns']] = 'ns' . count($ns_hash); + } + } + + $response['ns_hash'] = $ns_hash; + $responses[] = $response; + } + $this->_multistatus($responses); } @@ -77,31 +266,295 @@ class HTTP_CalDAV_Server extends HTTP_WebDAV_Server */ function report_wrapper() { - // prepare data-structure from REPORT request + /* Prepare data-structure from REPORT request */ if (!$this->report_request_helper($options)) { return; } - // call user handler - if (!$this->report($options, $responses)) { + /* Call user handler */ + if (method_exists($this, 'report')) { + if (!$this->report($options, $files)) { + return; + } + } else { + + /* Empulate REPORT using PROPFIND */ + if (!$this->propfind($options, $files)) { + return; + } + } + + /* Format REPORT response */ + $this->report_response_helper($options, $files); + } + + function _calendarData($path, $data=null, $filter=null) + { + if (is_array($data['comps']) && + !isset($data['comps']['VCALENDAR'])) { + return HTTP_CalDAV_Server::calDavProp('calendar-data'); + } + + $options = array(); + $options['path'] = $path; + + $status = $this->get($options); + if (empty($status)) { + $status = '403 Forbidden'; + } + + if ($status !== true) { + return HTTP_CalDAV_Server::calDavProp('calendar-data', null, + $status); + } + + if ($options['mimetype'] != 'text/calendar') { + return HTTP_CalDAV_Server::calDavProp('calendar-data', null, + '403 Forbidden'); + } + + if ($options['stream']) { + $handle = $options['stream']; + } else if ($options['data']) { + // What about data? + } else { return; } - // format REPORT response - $this->report_response_helper($options, $responses); + if (($line = fgets($handle, 4096)) === false) { + return; + } + + if (trim($line) != 'BEGIN:VCALENDAR') { + return; + } + + if (!($value = HTTP_CalDAV_Server::_parseComponent($handle, + 'VCALENDAR', is_array($data['comps']) ? + $data['comps']['VCALENDAR'] : null))) { + return; + } + + return HTTP_CalDAV_Server::calDavProp('calendar-data', $value); } - /** - * Make a property in the CalDAV namespace - * - * @param string property name - * @param string property value - * @return array string property namespace - * string property name - * string property value - */ - function calDavProp($name, $value=null) { - return $this->mkprop('urn:ietf:params:xml:ns:caldav', $name, $value); + function _parseComponent($handle, $name, $data=null, $filter=null) + { + $className = 'iCalendar_' . ltrim(strtolower($name), 'v'); + if ($name == 'VCALENDAR') { + $className = 'iCalendar'; + } + + if (!class_exists($className)) { + return; + } + $component = new $className; + + while (($line = fgets($handle, 4096)) !== false) { + $line = explode(':', trim($line)); + + if ($line[0] == 'END') { + if ($line[1] != $name) { + return; + } + + return $component; + } + + if ($line[0] == 'BEGIN') { + if (is_array($data['comps']) && + !isset($data['comps'][$line[1]])) { + while (($l = fgets($handle, 4096)) !== false) { + if (trim($l) == "END:$line[1]") { + continue (2); + } + } + + return; + } + + if (!($childComponent = HTTP_CalDAV_Server::_parseComponent( + $handle, $line[1], is_array($data['comps']) ? + $data['comps'][$line[1]] : null))) { + while (($l = fgets($handle, 4096)) !== false) { + if (trim($l) == "END:$line[1]") { + continue (2); + } + } + + return; + } + + if (!$component->add_component($childComponent)) { + return; + } + + continue; + } + + $line[0] = explode(';=', $line[0]); + $prop_name = array_shift($line[0]); + if (is_array($data['props']) && + !in_array($prop_name, $data['props'])) { + continue; + } + + $params = array(); + while (($param_name = array_shift($line[0])) && + ($param_value = array_shift($line[0]))) { + $params[$param_name] = $param_value; + } + $component->add_property($prop_name, $line[1], $params); + } + } + + function _multistatus($responses) + { + // now we generate the response header... + $this->http_status('207 Multi-Status'); + header('Content-Type: text/xml; charset="utf-8"'); + + // ...and payload + echo "\n"; + echo "\n"; + + foreach ($responses as $response) { + + // ignore empty or incomplete entries + if (!is_array($response) || empty($response)) { + continue; + } + + $ns_defs = array(); + foreach ($response['ns_hash'] as $name => $prefix) { + $ns_defs[] = "xmlns:$prefix=\"$name\""; + } + echo ' \n"; + echo " $response[href]\n"; + + // report all found properties and their values (if any) + // nothing to do if no properties were returend for a file + if (isset($response['propstat']) && + is_array($response['propstat'])) { + + foreach ($response['propstat'] as $status => $props) { + echo " \n"; + echo " \n"; + + foreach ($props as $prop) { + if (!is_array($prop) || !isset($prop['name'])) { + continue; + } + + // empty properties (cannot use empty for check as '0' + // is a legal value here) + if (!isset($prop['value']) || empty($prop['value']) && + $prop['value'] !== 0) { + if ($prop['ns'] == 'DAV:') { + echo " \n"; + continue; + } + + if (!empty($prop['ns'])) { + echo ' <' . + $response['ns_hash'][$prop['ns']] . + ":$prop[name]/>\n"; + continue; + } + + echo " <$prop[name] xmlns=\"\"/>"; + continue; + } + + // some WebDAV properties need special treatment + if ($prop['ns'] == 'DAV:') { + + switch ($prop['name']) { + case 'creationdate': + echo " \n"; + echo ' ' . gmdate('Y-m-d\TH:i:s\Z', $prop['value']) . "\n"; + echo " \n"; + break; + + case 'getlastmodified': + echo " \n"; + echo ' ' . gmdate('D, d M Y H:i:s', $prop['value']) . " GMT\n"; + echo " \n"; + break; + + case 'resourcetype': + echo " \n"; + echo " \n"; + echo " \n"; + break; + + case 'supportedlock': + + if (is_array($prop[value])) { + $prop[value] = $this->_lockentries($prop[value]); + } + echo " \n"; + echo " $prop[value]\n"; + echo " \n"; + break; + + case 'lockdiscovery': + + if (is_array($prop[value])) { + $prop[value] = $this->_activelocks($prop[value]); + } + echo " \n"; + echo " $prop[value]\n"; + echo " \n"; + break; + + default: + echo " \n"; + echo ' ' . $this->_prop_encode(htmlspecialchars($prop['value'])) . "\n"; + echo " \n"; + } + + continue; + } + + if ($prop['name'] == 'calendar-data' && + is_object($prop['value'])) { + $prop['value'] = $prop['value']->serialize(); + } + + if (!empty($prop['ns'])) { + echo ' <' . $response['ns_hash'][$prop['ns']] . ":$prop[name]>\n"; + echo ' ' . $this->_prop_encode(htmlspecialchars($prop['value'])) . "\n"; + echo ' \n"; + + continue; + } + + echo " <$prop[name] xmlns=\"\">\n"; + echo ' ' . $this->_prop_encode(htmlspecialchars($prop['value'])) . "\n"; + echo " \n"; + } + + echo " \n"; + echo " HTTP/1.1 $status\n"; + echo " \n"; + } + } + + if (isset($response['status'])) { + echo " HTTP/1.1 $status\n"; + } + + if (isset($response['responsedescription'])) { + echo " \n"; + echo ' ' . $this->_prop_encode(htmlspecialchars($response['responsedescription'])) . "\n"; + echo " \n"; + } + + echo " \n"; + } + + echo "\n"; } } diff --git a/lib/HTTP/CalDAV/Tools/ReportParser.php b/lib/HTTP/CalDAV/Tools/ReportParser.php new file mode 100644 index 0000000..9d9c9c7 --- /dev/null +++ b/lib/HTTP/CalDAV/Tools/ReportParser.php @@ -0,0 +1,251 @@ + + * @copyright 2006 The PHP Group + * @license PHP License 3.0 http://www.php.net/license/3_0.txt + * @version CVS: $Id: ReportParser.php,v 1.1 2006/04/13 05:10:24 jablko Exp $ + * @link http://pear.php.net/package/HTTP_CalDAV_Server + * @see HTTP_WebDAV_Server + */ + +/** + * Helper for parsing REPORT request bodies + * + * Long description + * + * @category HTTP + * @package HTTP_CalDAV_Server + * @author Jack Bates + * @copyright 2006 The PHP Group + * @license PHP License 3.0 http://www.php.net/license/3_0.txt + * @version CVS: $Id: ReportParser.php,v 1.1 2006/04/13 05:10:24 jablko Exp $ + * @link http://pear.php.net/package/HTTP_CalDAV_Server + * @see HTTP_WebDAV_Server + */ +class ReportParser +{ + /** + * Success state flag + * + * @var bool + * @access public + */ + var $success = false; + + /** + * Name of the requested report + * + * @var string + * @access public + */ + var $report; + + /** + * Found properties are collected here + * + * @var array + * @access public + */ + var $props = array(); + + /** + * Stack of ancestor tag names + * + * @var array + * @access private + */ + var $_names = array(); + + /** + * Stack of component data + * + * @var array + * @access private + */ + var $_comps = array(); + + /** + * Constructor + * + * @param string path to report input data + * @access public + */ + function ReportParser($input) + { + $handle = fopen($input, 'r'); + if (!$handle) { + return; + } + + $parser = xml_parser_create_ns('UTF-8', ' '); + xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, false); + xml_set_element_handler($parser, array(&$this, '_startElement'), + array(&$this, '_endElement')); + + $this->success = true; + while (($line = fgets($handle, 4096)) !== false) { + $this->success = xml_parse($parser, $line); + if (!$this->success) { + return; + } + } + + if (!feof($handle)) { + $this->success = false; + return; + } + + xml_parser_free($parser); + fclose($handle); + + if (empty($this->props)) { + $this->props = 'allprop'; + } + } + + /** + * Start tag handler + * + * @param object parser + * @param string tag name + * @param array tag attributes + * @access private + */ + function _startElement($parser, $name, $attrs) + { + $nameComponents = explode(' ', $name); + if (count($nameComponents) > 2) { + $this->success = false; + return; + } + + if (count($nameComponents) == 2) { + list ($ns, $name) = $nameComponents; + if (empty($ns)) { + $this->success = false; + return; + } + } + + if (empty($this->_names)) { + $this->report = $name; + $this->_names[] = $name; + return; + } + + if (count($this->_names) == 1 && + ($name == 'allprop' || $name == 'propname')) { + $this->props = $name; + $this->_names[] = $name; + return; + } + + if (count($this->_names) == 2 && end($this->_names) == 'prop') { + $prop = array('name' => $name); + + if ($ns) { + $prop['ns'] = $ns; + } + + if ($name == 'calendar-data') { + $prop['value'] = array(); + $this->_comps[] =& $prop['value']; + } + + $this->props[] = $prop; + $this->_names[] = $name; + return; + } + + if ($name == 'comp') { + end($this->_comps); + + // Gross - end returns a copy of the last value + $comp =& $this->_comps[key($this->_comps)]; + + if (!is_array($comp['comps'])) { + $comp['comps'] = array(); + } + + $comp['comps'][$attrs['name']] = array(); + $this->_comps[] =& $comp['comps'][$attrs['name']]; + $this->_names[] = $name; + return; + } + + if (end($this->_names) == 'comp' && $name == 'prop') { + end($this->_comps); + + // Gross - end returns a copy of the last value + $comp =& $this->_comps[key($this->_comps)]; + + if (!is_array($comp['props'])) { + $comp['props'] = array(); + } + + $comp['props'][] = $attrs['name']; + $this->_names[] = $name; + return; + } + + $this->_names[] = $name; + } + + /** + * End tag handler + * + * @param object parser + * @param string tag name + * @param array tag attributes + * @access private + */ + function _endElement($parser, $name) { + $nameComponents = explode(' ', $name); + if (count($nameComponents) > 2) { + $this->success = false; + return; + } + + if (count($nameComponents) == 2) { + list ($ns, $name) = $nameComponents; + if (empty($ns)) { + $this->success = false; + return; + } + } + + // Any need to pop at end of calendar-data? + if ($name == 'comp') { + array_pop($this->_comps); + } + + array_pop($this->_names); + } +} + +/* + * Local variables: + * tab-width: 4 + * c-basic-offset: 4 + * c-handling-comment-ender-p: nil + * End: + */ + +?> -- cgit v1.2.3