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 ++++++ lib/bennu/bennu.class.php | 59 ++ lib/bennu/iCalendar_components.php | 410 ++++++++++ lib/bennu/iCalendar_parameters.php | 240 ++++++ lib/bennu/iCalendar_properties.php | 1299 ++++++++++++++++++++++++++++++++ lib/bennu/iCalendar_rfc2445.php | 785 +++++++++++++++++++ 7 files changed, 3516 insertions(+), 19 deletions(-) create mode 100644 lib/HTTP/CalDAV/Tools/ReportParser.php create mode 100644 lib/bennu/bennu.class.php create mode 100644 lib/bennu/iCalendar_components.php create mode 100644 lib/bennu/iCalendar_parameters.php create mode 100644 lib/bennu/iCalendar_properties.php create mode 100644 lib/bennu/iCalendar_rfc2445.php (limited to 'lib') 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: + */ + +?> diff --git a/lib/bennu/bennu.class.php b/lib/bennu/bennu.class.php new file mode 100644 index 0000000..f4bb219 --- /dev/null +++ b/lib/bennu/bennu.class.php @@ -0,0 +1,59 @@ + diff --git a/lib/bennu/iCalendar_components.php b/lib/bennu/iCalendar_components.php new file mode 100644 index 0000000..13934cb --- /dev/null +++ b/lib/bennu/iCalendar_components.php @@ -0,0 +1,410 @@ +construct(); + } + + function construct() { + // Initialize the components array + if(empty($this->components)) { + $this->components = array(); + foreach($this->valid_components as $name) { + $this->components[$name] = array(); + } + } + } + + function get_name() { + return $this->name; + } + + function add_property($name, $value = NULL, $parameters = NULL) { + + // Uppercase first of all + $name = strtoupper($name); + + // Are we trying to add a valid property? + $xname = false; + if(!isset($this->valid_properties[$name])) { + // If not, is it an x-name as per RFC 2445? + if(!rfc2445_is_xname($name)) { + return false; + } + // Since this is an xname, all components are supposed to allow this property + $xname = true; + } + + // Create a property object of the correct class + if($xname) { + $property = new iCalendar_property_x; + $property->set_name($name); + } + else { + $classname = 'iCalendar_property_'.strtolower(str_replace('-', '_', $name)); + $property = new $classname; + } + + // If $value is NULL, then this property must define a default value. + if($value === NULL) { + $value = $property->default_value(); + if($value === NULL) { + return false; + } + } + + // Set this property's parent component to ourselves, because some + // properties behave differently according to what component they apply to. + $property->set_parent_component($this->name); + + // Set parameters before value; this helps with some properties which + // accept a VALUE parameter, and thus change their default value type. + + // The parameters must be valid according to property specifications + if(!empty($parameters)) { + foreach($parameters as $paramname => $paramvalue) { + if(!$property->set_parameter($paramname, $paramvalue)) { + return false; + } + } + + // Some parameters interact among themselves (e.g. ENCODING and VALUE) + // so make sure that after the dust settles, these invariants hold true + if(!$property->invariant_holds()) { + return false; + } + } + + // $value MUST be valid according to the property data type + if(!$property->set_value($value)) { + return false; + } + + // If this property is restricted to only once, blindly overwrite value + if(!$xname && $this->valid_properties[$name] & RFC2445_ONCE) { + $this->properties[$name] = array($property); + } + + // Otherwise add it to the instance array for this property + else { + $this->properties[$name][] = $property; + } + + // Finally: after all these, does the component invariant hold? + if(!$this->invariant_holds()) { + // If not, completely undo the property addition + array_pop($this->properties[$name]); + if(empty($this->properties[$name])) { + unset($this->properties[$name]); + } + return false; + } + + return true; + + } + + function add_component($component) { + + // With the detailed interface, you can add only components with this function + if(!is_object($component) || !is_subclass_of($component, 'iCalendar_component')) { + return false; + } + + $name = $component->get_name(); + + // Only valid components as specified by this component are allowed + if(!in_array($name, $this->valid_components)) { + return false; + } + + // Add it + $this->components[$name][] = $component; + + return true; + } + + function get_property_list($name) { + } + + function invariant_holds() { + return true; + } + + function is_valid() { + // If we have any child components, check that they are all valid + if(!empty($this->components)) { + foreach($this->components as $component => $instances) { + foreach($instances as $number => $instance) { + if(!$instance->is_valid()) { + return false; + } + } + } + } + + // Finally, check the valid property list for any mandatory properties + // that have not been set and do not have a default value + foreach($this->valid_properties as $property => $propdata) { + if(($propdata & RFC2445_REQUIRED) && empty($this->properties[$property])) { + $classname = 'iCalendar_property_'.strtolower(str_replace('-', '_', $property)); + $object = new $classname; + if($object->default_value() === NULL) { + return false; + } + unset($object); + } + } + + return true; + } + + function serialize() { + // Check for validity of the object + if(!$this->is_valid()) { + return false; + } + + // Maybe the object is valid, but there are some required properties that + // have not been given explicit values. In that case, set them to defaults. + foreach($this->valid_properties as $property => $propdata) { + if(($propdata & RFC2445_REQUIRED) && empty($this->properties[$property])) { + $this->add_property($property); + } + } + + // Start tag + $string = rfc2445_fold('BEGIN:'.$this->name) . RFC2445_CRLF; + + // List of properties + if(!empty($this->properties)) { + foreach($this->properties as $name => $properties) { + foreach($properties as $property) { + $string .= $property->serialize(); + } + } + } + + // List of components + if(!empty($this->components)) { + foreach($this->components as $name => $components) { + foreach($components as $component) { + $string .= $component->serialize(); + } + } + } + + // End tag + $string .= rfc2445_fold('END:'.$this->name) . RFC2445_CRLF; + + return $string; + } + +} + +class iCalendar extends iCalendar_component { + var $name = 'VCALENDAR'; + + function construct() { + $this->valid_properties = array( + 'CALSCALE' => RFC2445_OPTIONAL | RFC2445_ONCE, + 'METHOD' => RFC2445_OPTIONAL | RFC2445_ONCE, + 'PRODID' => RFC2445_REQUIRED | RFC2445_ONCE, + 'VERSION' => RFC2445_REQUIRED | RFC2445_ONCE, + RFC2445_XNAME => RFC2445_OPTIONAL + ); + + $this->valid_components = array( + 'VEVENT' + // TODO: add support for the other component types + //, 'VTODO', 'VJOURNAL', 'VFREEBUSY', 'VTIMEZONE', 'VALARM' + ); + parent::construct(); + } + +} + +class iCalendar_event extends iCalendar_component { + + var $name = 'VEVENT'; + var $properties; + + function construct() { + + $this->valid_components = array('VALARM'); + + $this->valid_properties = array( + 'CLASS' => RFC2445_OPTIONAL | RFC2445_ONCE, + 'CREATED' => RFC2445_OPTIONAL | RFC2445_ONCE, + 'DESCRIPTION' => RFC2445_OPTIONAL | RFC2445_ONCE, + // Standard ambiguous here: in 4.6.1 it says that DTSTAMP in optional, + // while in 4.8.7.2 it says it's REQUIRED. Go with REQUIRED. + 'DTSTAMP' => RFC2445_REQUIRED | RFC2445_ONCE, + // Standard ambiguous here: in 4.6.1 it says that DTSTART in optional, + // while in 4.8.2.4 it says it's REQUIRED. Go with REQUIRED. + 'DTSTART' => RFC2445_REQUIRED | RFC2445_ONCE, + 'GEO' => RFC2445_OPTIONAL | RFC2445_ONCE, + 'LAST-MODIFIED' => RFC2445_OPTIONAL | RFC2445_ONCE, + 'LOCATION' => RFC2445_OPTIONAL | RFC2445_ONCE, + 'ORGANIZER' => RFC2445_OPTIONAL | RFC2445_ONCE, + 'PRIORITY' => RFC2445_OPTIONAL | RFC2445_ONCE, + 'SEQUENCE' => RFC2445_OPTIONAL | RFC2445_ONCE, + 'STATUS' => RFC2445_OPTIONAL | RFC2445_ONCE, + 'SUMMARY' => RFC2445_OPTIONAL | RFC2445_ONCE, + 'TRANSP' => RFC2445_OPTIONAL | RFC2445_ONCE, + // Standard ambiguous here: in 4.6.1 it says that UID in optional, + // while in 4.8.4.7 it says it's REQUIRED. Go with REQUIRED. + 'UID' => RFC2445_REQUIRED | RFC2445_ONCE, + 'URL' => RFC2445_OPTIONAL | RFC2445_ONCE, + 'RECURRENCE-ID' => RFC2445_OPTIONAL | RFC2445_ONCE, + 'DTEND' => RFC2445_OPTIONAL | RFC2445_ONCE, + 'DURATION' => RFC2445_OPTIONAL | RFC2445_ONCE, + 'ATTACH' => RFC2445_OPTIONAL, + 'ATTENDEE' => RFC2445_OPTIONAL, + 'CATEGORIES' => RFC2445_OPTIONAL, + 'COMMENT' => RFC2445_OPTIONAL, + 'CONTACT' => RFC2445_OPTIONAL, + 'EXDATE' => RFC2445_OPTIONAL, + 'EXRULE' => RFC2445_OPTIONAL, + 'REQUEST-STATUS' => RFC2445_OPTIONAL, + 'RELATED-TO' => RFC2445_OPTIONAL, + 'RESOURCES' => RFC2445_OPTIONAL, + 'RDATE' => RFC2445_OPTIONAL, + 'RRULE' => RFC2445_OPTIONAL, + RFC2445_XNAME => RFC2445_OPTIONAL + ); + + parent::construct(); + } + + function invariant_holds() { + // DTEND and DURATION must not appear together + if(isset($this->properties['DTEND']) && isset($this->properties['DURATION'])) { + return false; + } + + + if(isset($this->properties['DTEND']) && isset($this->properties['DTSTART'])) { + // DTEND must be later than DTSTART + // The standard is not clear on how to hande different value types though + // TODO: handle this correctly even if the value types are different + if($this->properties['DTEND'][0]->value <= $this->properties['DTSTART'][0]->value) { + return false; + } + + // DTEND and DTSTART must have the same value type + if($this->properties['DTEND'][0]->val_type != $this->properties['DTSTART'][0]->val_type) { + return false; + } + + } + return true; + } + +} + +class iCalendar_todo extends iCalendar_component { + var $name = 'VTODO'; + var $properties; + + function construct() { + + $this->properties = array( + 'class' => RFC2445_OPTIONAL | RFC2445_ONCE, + 'completed' => RFC2445_OPTIONAL | RFC2445_ONCE, + 'created' => RFC2445_OPTIONAL | RFC2445_ONCE, + 'description' => RFC2445_OPTIONAL | RFC2445_ONCE, + 'dtstamp' => RFC2445_OPTIONAL | RFC2445_ONCE, + 'dtstart' => RFC2445_OPTIONAL | RFC2445_ONCE, + 'geo' => RFC2445_OPTIONAL | RFC2445_ONCE, + 'last-modified' => RFC2445_OPTIONAL | RFC2445_ONCE, + 'location' => RFC2445_OPTIONAL | RFC2445_ONCE, + 'organizer' => RFC2445_OPTIONAL | RFC2445_ONCE, + 'percent' => RFC2445_OPTIONAL | RFC2445_ONCE, + 'priority' => RFC2445_OPTIONAL | RFC2445_ONCE, + 'recurid' => RFC2445_OPTIONAL | RFC2445_ONCE, + 'sequence' => RFC2445_OPTIONAL | RFC2445_ONCE, + 'status' => RFC2445_OPTIONAL | RFC2445_ONCE, + 'summary' => RFC2445_OPTIONAL | RFC2445_ONCE, + 'uid' => RFC2445_OPTIONAL | RFC2445_ONCE, + 'url' => RFC2445_OPTIONAL | RFC2445_ONCE, + 'due' => RFC2445_OPTIONAL | RFC2445_ONCE, + 'duration' => RFC2445_OPTIONAL | RFC2445_ONCE, + 'attach' => RFC2445_OPTIONAL, + 'attendee' => RFC2445_OPTIONAL, + 'categories' => RFC2445_OPTIONAL, + 'comment' => RFC2445_OPTIONAL, + 'contact' => RFC2445_OPTIONAL, + 'exdate' => RFC2445_OPTIONAL, + 'exrule' => RFC2445_OPTIONAL, + 'rstatus' => RFC2445_OPTIONAL, + 'related' => RFC2445_OPTIONAL, + 'resources' => RFC2445_OPTIONAL, + 'rdate' => RFC2445_OPTIONAL, + 'rrule' => RFC2445_OPTIONAL, + 'xprop' => RFC2445_OPTIONAL + ); + + parent::construct(); + // TODO: + // either 'due' or 'duration' may appear in a 'eventprop', but 'due' + // and 'duration' MUST NOT occur in the same 'eventprop' + } +} + +class iCalendar_journal extends iCalendar_component { + // TODO: implement +} + +class iCalendar_freebusy extends iCalendar_component { + // TODO: implement +} + +class iCalendar_alarm extends iCalendar_component { + // TODO: implement +} + +class iCalendar_timezone extends iCalendar_component { + var $name = 'VTIMEZONE'; + var $properties; + + function construct() { + + $this->properties = array( + 'tzid' => RFC2445_REQUIRED | RFC2445_ONCE, + 'last-modified' => RFC2445_OPTIONAL | RFC2445_ONCE, + 'tzurl' => RFC2445_OPTIONAL | RFC2445_ONCE, + // TODO: the next two are components of their own! + 'standardc' => RFC2445_REQUIRED, + 'daylightc' => RFC2445_REQUIRED, + 'x-prop' => RFC2445_OPTIONAL + ); + + parent::construct(); + } + +} + +// REMINDER: DTEND must be later than DTSTART for all components which support both +// REMINDER: DUE must be later than DTSTART for all components which support both + +?> \ No newline at end of file diff --git a/lib/bennu/iCalendar_parameters.php b/lib/bennu/iCalendar_parameters.php new file mode 100644 index 0000000..ab96654 --- /dev/null +++ b/lib/bennu/iCalendar_parameters.php @@ -0,0 +1,240 @@ + array('PLAIN', 'RICHTEXT', 'ENRICHED', 'TAB-SEPARATED-VALUES', 'HTML', 'SGML', + 'VND.LATEX-Z', 'VND.FMI.FLEXSTOR'), + 'MULTIPART' => array('MIXED', 'ALTERNATIVE', 'DIGEST', 'PARALLEL', 'APPLEDOUBLE', 'HEADER-SET', + 'FORM-DATA', 'RELATED', 'REPORT', 'VOICE-MESSAGE', 'SIGNED', 'ENCRYPTED', + 'BYTERANGES'), + 'MESSAGE' => array('RFC822', 'PARTIAL', 'EXTERNAL-BODY', 'NEWS', 'HTTP'), + 'APPLICATION' => array('OCTET-STREAM', 'POSTSCRIPT', 'ODA', 'ATOMICMAIL', 'ANDREW-INSET', 'SLATE', + 'WITA', 'DEC-DX', 'DCA-RFT', 'ACTIVEMESSAGE', 'RTF', 'APPLEFILE', + 'MAC-BINHEX40', 'NEWS-MESSAGE-ID', 'NEWS-TRANSMISSION', 'WORDPERFECT5.1', + 'PDF', 'ZIP', 'MACWRITEII', 'MSWORD', 'REMOTE-PRINTING', 'MATHEMATICA', + 'CYBERCASH', 'COMMONGROUND', 'IGES', 'RISCOS', 'ESHOP', 'X400-BP', 'SGML', + 'CALS-1840', 'PGP-ENCRYPTED', 'PGP-SIGNATURE', 'PGP-KEYS', 'VND.FRAMEMAKER', + 'VND.MIF', 'VND.MS-EXCEL', 'VND.MS-POWERPOINT', 'VND.MS-PROJECT', + 'VND.MS-WORKS', 'VND.MS-TNEF', 'VND.SVD', 'VND.MUSIC-NIFF', 'VND.MS-ARTGALRY', + 'VND.TRUEDOC', 'VND.KOAN', 'VND.STREET-STREAM', 'VND.FDF', + 'SET-PAYMENT-INITIATION', 'SET-PAYMENT', 'SET-REGISTRATION-INITIATION', + 'SET-REGISTRATION', 'VND.SEEMAIL', 'VND.BUSINESSOBJECTS', + 'VND.MERIDIAN-SLINGSHOT', 'VND.XARA', 'SGML-OPEN-CATALOG', 'VND.RAPID', + 'VND.ENLIVEN', 'VND.JAPANNET-REGISTRATION-WAKEUP', + 'VND.JAPANNET-VERIFICATION-WAKEUP', 'VND.JAPANNET-PAYMENT-WAKEUP', + 'VND.JAPANNET-DIRECTORY-SERVICE', 'VND.INTERTRUST.DIGIBOX', 'VND.INTERTRUST.NNCP'), + 'IMAGE' => array('JPEG', 'GIF', 'IEF', 'G3FAX', 'TIFF', 'CGM', 'NAPLPS', 'VND.DWG', 'VND.SVF', + 'VND.DXF', 'PNG', 'VND.FPX', 'VND.NET-FPX'), + 'AUDIO' => array('BASIC', '32KADPCM', 'VND.QCELP'), + 'VIDEO' => array('MPEG', 'QUICKTIME', 'VND.VIVO', 'VND.MOTOROLA.VIDEO', 'VND.MOTOROLA.VIDEOP') + ); + $value = strtoupper($value); + if(rfc2445_is_xname($value)) { + return true; + } + @list($type, $subtype) = explode('/', $value); + if(empty($type) || empty($subtype)) { + return false; + } + if(!isset($fmttypes[$type]) || !in_array($subtype, $fmttypes[$type])) { + return false; + } + return true; + break; + + case 'LANGUAGE': + $value = strtoupper($value); + $parts = explode('-', $value); + foreach($parts as $part) { + if(empty($part)) { + return false; + } + if(strspn($part, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789') != strlen($part)) { + return false; + } + } + return true; + break; + + case 'PARTSTAT': + $value = strtoupper($value); + switch($parent_property->parent_component) { + case 'VEVENT': + return ($value == 'NEEDS-ACTION' || $value == 'ACCEPTED' || $value == 'DECLINED' || $value == 'TENTATIVE' + || $value == 'DELEGATED' || rfc2445_is_xname($value)); + break; + case 'VTODO': + return ($value == 'NEEDS-ACTION' || $value == 'ACCEPTED' || $value == 'DECLINED' || $value == 'TENTATIVE' + || $value == 'DELEGATED' || $value == 'COMPLETED' || $value == 'IN-PROCESS' || rfc2445_is_xname($value)); + break; + case 'VJOURNAL': + return ($value == 'NEEDS-ACTION' || $value == 'ACCEPTED' || $value == 'DECLINED' || rfc2445_is_xname($value)); + break; + } + return false; + break; + + case 'RANGE': + $value = strtoupper($value); + return ($value == 'THISANDPRIOR' || $value == 'THISANDFUTURE'); + break; + + case 'RELATED': + $value = strtoupper($value); + return ($value == 'START' || $value == 'END'); + break; + + case 'RELTYPE': + $value = strtoupper($value); + return ($value == 'PARENT' || $value == 'CHILD' || $value == 'SIBLING' || rfc2445_is_xname($value)); + break; + + case 'ROLE': + $value = strtoupper($value); + return ($value == 'CHAIR' || $value == 'REQ-PARTICIPANT' || $value == 'OPT-PARTICIPANT' || $value == 'NON-PARTICIPANT' || rfc2445_is_xname($value)); + break; + + case 'RSVP': + $value = strtoupper($value); + return ($value == 'TRUE' || $value == 'FALSE'); + break; + + case 'TZID': + if(empty($value)) { + return false; + } + return (strcspn($value, '";:,') == strlen($value)); + break; + + case 'VALUE': + $value = strtoupper($value); + return ($value == 'BINARY' || $value == 'BOOLEAN' || $value == 'CAL-ADDRESS' || $value == 'DATE' || + $value == 'DATE-TIME' || $value == 'DURATION' || $value == 'FLOAT' || $value == 'INTEGER' || + $value == 'PERIOD' || $value == 'RECUR' || $value == 'TEXT' || $value == 'TIME' || + $value == 'URI' || $value == 'UTC-OFFSET' || rfc2445_is_xname($value)); + break; + } + } + + function do_value_formatting($parameter, $value) { + switch($parameter) { + // Parameters of type CAL-ADDRESS or URI MUST be double-quoted + case 'ALTREP': + case 'DIR': + case 'DELEGATED-FROM': + case 'DELEGATED-TO': + case 'MEMBER': + case 'SENT-BY': + return '"'.$value.'"'; + break; + + // Textual parameter types must be double quoted if they contain COLON, SEMICOLON + // or COMMA. Quoting always sounds easier and standards-conformant though. + case 'CN': + return '"'.$value.'"'; + break; + + // Parameters with enumerated legal values, just make them all caps + case 'CUTYPE': + case 'ENCODING': + case 'FBTYPE': + case 'FMTTYPE': + case 'LANGUAGE': + case 'PARTSTAT': + case 'RANGE': + case 'RELATED': + case 'RELTYPE': + case 'ROLE': + case 'RSVP': + case 'VALUE': + return strtoupper($value); + break; + + // Parameters we shouldn't be messing with + case 'TZID': + return $value; + break; + } + } + + function undo_value_formatting($parameter, $value) { + } + +} + +?> diff --git a/lib/bennu/iCalendar_properties.php b/lib/bennu/iCalendar_properties.php new file mode 100644 index 0000000..bec729e --- /dev/null +++ b/lib/bennu/iCalendar_properties.php @@ -0,0 +1,1299 @@ +construct(); + } + + function construct() { + $this->parameters = array(); + } + + // If some property needs extra care with its parameters, override this + // IMPORTANT: the parameter name MUST BE CAPITALIZED! + function is_valid_parameter($parameter, $value) { + + if(is_array($value)) { + if(!iCalendar_parameter::multiple_values_allowed($parameter)) { + return false; + } + foreach($value as $item) { + if(!iCalendar_parameter::is_valid_value($this, $parameter, $item)) { + return false; + } + } + return true; + } + + return iCalendar_parameter::is_valid_value($this, $parameter, $value); + } + + function invariant_holds() { + return true; + } + + // If some property is very picky about its values, it should do the work itself + // Only data type validation is done here + function is_valid_value($value) { + if(is_array($value)) { + if(!$this->val_multi) { + return false; + } + else { + foreach($value as $oneval) { + if(!rfc2445_is_valid_value($oneval, $this->val_type)) { + return false; + } + } + } + return true; + } + return rfc2445_is_valid_value($value, $this->val_type); + } + + function default_value() { + return $this->val_default; + } + + function set_parent_component($componentname) { + if(class_exists('iCalendar_'.strtolower(substr($componentname, 1)))) { + $this->parent_component = strtoupper($componentname); + return true; + } + + return false; + } + + function set_value($value) { + if($this->is_valid_value($value)) { + // This transparently formats any value type according to the iCalendar specs + if(is_array($value)) { + foreach($value as $key => $item) { + $value[$key] = rfc2445_do_value_formatting($item, $this->val_type); + } + $this->value = implode(',', $value); + } + else { + $this->value = rfc2445_do_value_formatting($value, $this->val_type); + } + + return true; + } + return false; + } + + function get_value() { + // First of all, assume that we have multiple values + $valarray = explode('\\,', $this->value); + + // Undo transparent formatting + $replace_function = create_function('$a', 'return rfc2445_undo_value_formatting($a, '.$this->val_type.');'); + $valarray = array_map($replace_function, $valarray); + + // Now, if this property cannot have multiple values, don't return as an array + if(!$this->val_multi) { + return $valarray[0]; + } + + // Otherwise return an array even if it has one element, for uniformity + return $valarray; + + } + + function set_parameter($name, $value) { + + // Uppercase + $name = strtoupper($name); + + // Are we trying to add a valid parameter? + $xname = false; + if(!isset($this->valid_parameters[$name])) { + // If not, is it an x-name as per RFC 2445? + if(!rfc2445_is_xname($name)) { + return false; + } + // No more checks -- all components are supposed to allow x-name parameters + $xname = true; + } + + if(!$this->is_valid_parameter($name, $value)) { + return false; + } + + if(is_array($value)) { + foreach($value as $key => $element) { + $value[$key] = iCalendar_parameter::do_value_formatting($name, $element); + } + } + else { + $value = iCalendar_parameter::do_value_formatting($name, $value); + } + + $this->parameters[$name] = $value; + + // Special case: if we just changed the VALUE parameter, reflect this + // in the object's status so that it only accepts correct type values + if($name == 'VALUE') { + // TODO: what if this invalidates an already-set value? + $this->val_type = constant('RFC2445_TYPE_'.str_replace('-', '_', $value)); + } + + return true; + + } + + function get_parameter($name) { + + // Uppercase + $name = strtoupper($name); + + if(isset($this->parameters[$name])) { + // If there are any double quotes in the value, invisibly strip them + if(is_array($this->parameters[$name])) { + foreach($this->parameters[$name] as $key => $value) { + if(substr($value, 0, 1) == '"') { + $this->parameters[$name][$key] = substr($value, 1, strlen($value) - 2); + } + } + return $this->parameters[$name]; + } + + else { + if(substr($this->parameters[$name], 0, 1) == '"') { + return substr($this->parameters[$name], 1, strlen($this->parameters[$name]) - 2); + } + } + } + + return NULL; + } + + function serialize() { + $string = $this->name; + + if(!empty($this->parameters)) { + foreach($this->parameters as $name => $value) { + $string .= ';'.$name.'='; + if(is_array($value)) { + $string .= implode(',', $value); + } + else { + $string .= $value; + } + } + } + + $string .= ':'.$this->value; + + return rfc2445_fold($string) . RFC2445_CRLF; + } +} + +// 4.7 Calendar Properties +// ----------------------- + +class iCalendar_property_calscale extends iCalendar_property { + + var $name = 'CALSCALE'; + var $val_type = RFC2445_TYPE_TEXT; + + function construct() { + $this->valid_parameters = array( + RFC2445_XNAME => RFC2445_OPTIONAL + ); + } + + function is_valid_value($value) { + // This is case-sensitive + return ($value === 'GREGORIAN'); + } +} + +class iCalendar_property_method extends iCalendar_property { + + var $name = 'METHOD'; + var $val_type = RFC2445_TYPE_TEXT; + + function construct() { + $this->valid_parameters = array( + RFC2445_XNAME => RFC2445_OPTIONAL + ); + } + + function is_valid_value($value) { + // This is case-sensitive + // Methods from RFC 2446 + $methods = array('PUBLISH', 'REQUEST', 'REPLY', 'ADD', 'CANCEL', 'REFRESH', 'COUNTER', 'DECLINECOUNTER'); + return in_array($value, $methods); + } +} + +class iCalendar_property_prodid extends iCalendar_property { + + var $name = 'PRODID'; + var $val_type = RFC2445_TYPE_TEXT; + var $val_default = NULL; + + function construct() { + $this->val_default = '-//John Papaioannou/NONSGML Bennu '._BENNU_VERSION.'//EN'; + + $this->valid_parameters = array( + RFC2445_XNAME => RFC2445_OPTIONAL + ); + } +} + +class iCalendar_property_version extends iCalendar_property { + + var $name = 'VERSION'; + var $val_type = RFC2445_TYPE_TEXT; + var $val_default = '2.0'; + + function construct() { + $this->valid_parameters = array( + RFC2445_XNAME => RFC2445_OPTIONAL + ); + } + + function is_valid_value($value) { + return($value === '2.0' || $value === 2.0); + } + +} + +// 4.8.1 Descriptive Component Properties +// -------------------------------------- + +class iCalendar_property_attach extends iCalendar_property { + + var $name = 'ATTACH'; + var $val_type = RFC2445_TYPE_URI; + + function construct() { + $this->valid_parameters = array( + 'FMTTYPE' => RFC2445_OPTIONAL | RFC2445_ONCE, + 'ENCODING' => RFC2445_OPTIONAL | RFC2445_ONCE, + 'VALUE' => RFC2445_OPTIONAL | RFC2445_ONCE, + RFC2445_XNAME => RFC2445_OPTIONAL + ); + } + + function invariant_holds() { + if(isset($this->parameters['ENCODING']) && !isset($this->parameters['VALUE'])) { + return false; + } + if(isset($this->parameters['VALUE']) && !isset($this->parameters['ENCODING'])) { + return false; + } + + return true; + } + + function is_valid_parameter($parameter, $value) { + + $parameter = strtoupper($parameter); + + if(!parent::is_valid_parameter($parameter, $value)) { + return false; + } + + if($parameter === 'ENCODING' && strtoupper($value) != 'BASE64') { + return false; + } + + if($parameter === 'VALUE' && strtoupper($value) != 'BINARY') { + return false; + } + + return true; + } +} + +class iCalendar_property_categories extends iCalendar_property { + + var $name = 'CATEGORIES'; + var $val_type = RFC2445_TYPE_TEXT; + var $val_multi = true; + + function construct() { + $this->valid_parameters = array( + 'LANGUAGE' => RFC2445_OPTIONAL | RFC2445_ONCE, + RFC2445_XNAME => RFC2445_OPTIONAL + ); + } +} + +class iCalendar_property_class extends iCalendar_property { + + var $name = 'CLASS'; + var $val_type = RFC2445_TYPE_TEXT; + var $val_default = 'PUBLIC'; + + function construct() { + $this->valid_parameters = array( + RFC2445_XNAME => RFC2445_OPTIONAL + ); + } + + function is_valid_value($value) { + // If this is not an xname, it is case-sensitive + return ($value === 'PUBLIC' || $value === 'PRIVATE' || $value === 'CONFIDENTIAL' || rfc2445_is_xname(strtoupper($value))); + } +} + +class iCalendar_property_comment extends iCalendar_property { + + var $name = 'COMMENT'; + var $val_type = RFC2445_TYPE_TEXT; + + function construct() { + $this->valid_parameters = array( + 'ALTREP' => RFC2445_OPTIONAL | RFC2445_ONCE, + 'LANGUAGE' => RFC2445_OPTIONAL | RFC2445_ONCE, + RFC2445_XNAME => RFC2445_OPTIONAL + ); + } +} + +class iCalendar_property_description extends iCalendar_property { + + var $name = 'DESCRIPTION'; + var $val_type = RFC2445_TYPE_TEXT; + + function construct() { + $this->valid_parameters = array( + 'ALTREP' => RFC2445_OPTIONAL | RFC2445_ONCE, + 'LANGUAGE' => RFC2445_OPTIONAL | RFC2445_ONCE, + RFC2445_XNAME => RFC2445_OPTIONAL + ); + } +} + +class iCalendar_property_geo extends iCalendar_property { + + var $name = 'GEO'; + var $val_type = RFC2445_TYPE_TEXT; + + function construct() { + $this->valid_parameters = array( + 'ALTREP' => RFC2445_OPTIONAL | RFC2445_ONCE, + 'LANGUAGE' => RFC2445_OPTIONAL | RFC2445_ONCE, + RFC2445_XNAME => RFC2445_OPTIONAL + ); + } + + function is_valid_value($value) { + // This MUST be two floats separated by a semicolon + if(!is_string($value)) { + return false; + } + + $floats = explode(';', $value); + if(count($floats) != 2) { + return false; + } + + return rfc2445_is_valid_value($floats[0], RFC2445_TYPE_FLOAT) && rfc2445_is_valid_value($floats[1], RFC2445_TYPE_FLOAT); + } + + function set_value($value) { + // Must override this, otherwise the semicolon separating + // the two floats would get auto-quoted, which is illegal + if($this->is_valid_value($value)) { + $this->value = $value; + return true; + } + + return false; + } + +} + +class iCalendar_property_location extends iCalendar_property { + + var $name = 'LOCATION'; + var $val_type = RFC2445_TYPE_TEXT; + + function construct() { + $this->valid_parameters = array( + 'ALTREP' => RFC2445_OPTIONAL | RFC2445_ONCE, + 'LANGUAGE' => RFC2445_OPTIONAL | RFC2445_ONCE, + RFC2445_XNAME => RFC2445_OPTIONAL + ); + } +} + +class iCalendar_property_percent_complete extends iCalendar_property { + + var $name = 'PERCENT-COMPLETE'; + var $val_type = RFC2445_TYPE_INTEGER; + + function construct() { + $this->valid_parameters = array( + RFC2445_XNAME => RFC2445_OPTIONAL + ); + } + + function is_valid_value($value) { + // Only integers between 0 and 100 inclusive allowed + if(!parent::is_valid_value($value)) { + return false; + } + $value = intval($value); + return ($value >= 0 && $value <= 100); + } + +} + +class iCalendar_property_priority extends iCalendar_property { + + var $name = 'PRIORITY'; + var $val_type = RFC2445_TYPE_INTEGER; + + function construct() { + $this->valid_parameters = array( + RFC2445_XNAME => RFC2445_OPTIONAL + ); + } + + function is_valid_value($value) { + // Only integers between 0 and 9 inclusive allowed + if(!parent::is_valid_value($value)) { + return false; + } + + $value = intval($value); + return ($value >= 0 && $value <= 9); + } +} + +class iCalendar_property_resources extends iCalendar_property { + + var $name = 'RESOURCES'; + var $val_type = RFC2445_TYPE_TEXT; + var $val_multi = true; + + function construct() { + $this->valid_parameters = array( + 'ALTREP' => RFC2445_OPTIONAL | RFC2445_ONCE, + 'LANGUAGE' => RFC2445_OPTIONAL | RFC2445_ONCE, + RFC2445_XNAME => RFC2445_OPTIONAL + ); + } +} + +class iCalendar_property_status extends iCalendar_property { + + var $name = 'STATUS'; + var $val_type = RFC2445_TYPE_TEXT; + + function construct() { + $this->valid_parameters = array( + RFC2445_XNAME => RFC2445_OPTIONAL + ); + } + + function is_valid_value($value) { + // This is case-sensitive + switch ($this->parent_component) { + case 'VEVENT': + $allowed = array('TENTATIVE', 'CONFIRMED', 'CANCELLED'); + break; + case 'VTODO': + $allowed = array('NEEDS-ACTION', 'COMPLETED', 'IN-PROCESS', 'CANCELLED'); + break; + case 'VJOURNAL': + $allowed = array('DRAFT', 'FINAL', 'CANCELLED'); + break; + } + return in_array($value, $allowed); + + } + +} + +class iCalendar_property_summary extends iCalendar_property { + + var $name = 'SUMMARY'; + var $val_type = RFC2445_TYPE_TEXT; + + function construct() { + $this->valid_parameters = array( + 'ALTREP' => RFC2445_OPTIONAL | RFC2445_ONCE, + 'LANGUAGE' => RFC2445_OPTIONAL | RFC2445_ONCE, + RFC2445_XNAME => RFC2445_OPTIONAL + ); + } +} + +// 4.8.2 Date and Time Component Properties +// ---------------------------------------- + +class iCalendar_property_completed extends iCalendar_property { + + var $name = 'COMPLETED'; + var $val_type = RFC2445_TYPE_DATE_TIME; + + function construct() { + $this->valid_parameters = array( + RFC2445_XNAME => RFC2445_OPTIONAL + ); + } + + function is_valid_value($value) { + if(!parent::is_valid_value($value)) { + return false; + } + // Time MUST be in UTC format + return(substr($value, -1) == 'Z'); + } +} + +class iCalendar_property_dtend extends iCalendar_property { + + var $name = 'DTEND'; + var $val_type = RFC2445_TYPE_DATE_TIME; + + function construct() { + $this->valid_parameters = array( + 'VALUE' => RFC2445_OPTIONAL | RFC2445_ONCE, + 'TZID' => RFC2445_OPTIONAL | RFC2445_ONCE, + RFC2445_XNAME => RFC2445_OPTIONAL + ); + } + + function is_valid_value($value) { + if(!parent::is_valid_value($value)) { + return false; + } + + // If present in a FREEBUSY component, must be in UTC format + if($this->parent_component == 'VFREEBUSY' && substr($value, -1) != 'Z') { + return false; + } + + return true; + + } + + function is_valid_parameter($parameter, $value) { + + $parameter = strtoupper($parameter); + + if(!parent::is_valid_parameter($parameter, $value)) { + return false; + } + if($parameter == 'VALUE' && !($value == 'DATE' || $value == 'DATE-TIME')) { + return false; + } + + return true; + } +} + +class iCalendar_property_due extends iCalendar_property { + + var $name = 'DUE'; + var $val_type = RFC2445_TYPE_DATE_TIME; + + function construct() { + $this->valid_parameters = array( + 'VALUE' => RFC2445_OPTIONAL | RFC2445_ONCE, + 'TZID' => RFC2445_OPTIONAL | RFC2445_ONCE, + RFC2445_XNAME => RFC2445_OPTIONAL + ); + } + + function is_valid_value($value) { + if(!parent::is_valid_value($value)) { + return false; + } + + // If present in a FREEBUSY component, must be in UTC format + if($this->parent_component == 'VFREEBUSY' && substr($value, -1) != 'Z') { + return false; + } + + return true; + + } + + function is_valid_parameter($parameter, $value) { + + $parameter = strtoupper($parameter); + + if(!parent::is_valid_parameter($parameter, $value)) { + return false; + } + if($parameter == 'VALUE' && !($value == 'DATE' || $value == 'DATE-TIME')) { + return false; + } + + return true; + } +} + +class iCalendar_property_dtstart extends iCalendar_property { + + var $name = 'DTSTART'; + var $val_type = RFC2445_TYPE_DATE_TIME; + + function construct() { + $this->valid_parameters = array( + 'VALUE' => RFC2445_OPTIONAL | RFC2445_ONCE, + 'TZID' => RFC2445_OPTIONAL | RFC2445_ONCE, + RFC2445_XNAME => RFC2445_OPTIONAL + ); + } + + // TODO: unimplemented stuff when parent is a VTIMEZONE component + + function is_valid_value($value) { + if(!parent::is_valid_value($value)) { + return false; + } + + // If present in a FREEBUSY component, must be in UTC format + if($this->parent_component == 'VFREEBUSY' && substr($value, -1) != 'Z') { + return false; + } + + return true; + } + + function is_valid_parameter($parameter, $value) { + + $parameter = strtoupper($parameter); + + if(!parent::is_valid_parameter($parameter, $value)) { + return false; + } + if($parameter == 'VALUE' && !($value == 'DATE' || $value == 'DATE-TIME')) { + return false; + } + + return true; + } +} + +class iCalendar_property_duration extends iCalendar_property { + + var $name = 'DURATION'; + var $val_type = RFC2445_TYPE_DURATION; + + function construct() { + $this->valid_parameters = array( + RFC2445_XNAME => RFC2445_OPTIONAL + ); + } + + function is_valid_value($value) { + if(!parent::is_valid_value($value)) { + return false; + } + + // Value must be positive + return ($value{0} != '-'); + } +} + +class iCalendar_property_freebusy extends iCalendar_property { + + var $name = 'FREEBUSY'; + var $val_type = RFC2445_TYPE_PERIOD; + var $val_multi = true; + + function construct() { + $this->valid_parameters = array( + 'FBTYPE' => RFC2445_OPTIONAL | RFC2445_ONCE, + RFC2445_XNAME => RFC2445_OPTIONAL + ); + } + + function is_valid_value($value) { + if(!parent::is_valid_value($value)) { + return false; + } + + $pos = strpos($value, '/'); // We know there's only one / in there + if($value{$pos - 1} != 'Z') { + // Start time MUST be in UTC + return false; + } + if($value{$pos + 1} != 'P' && $substr($value, -1) != 'Z') { + // If the second part is not a period, it MUST be in UTC + return false; + } + + return true; + } + + // TODO: these properties SHOULD be shorted in ascending order (by start time and end time as tiebreak) +} + +class iCalendar_property_transp extends iCalendar_property { + + var $name = 'TRANSP'; + var $val_type = RFC2445_TYPE_TEXT; + var $val_default = 'OPAQUE'; + + function construct() { + $this->valid_parameters = array( + RFC2445_XNAME => RFC2445_OPTIONAL + ); + } + + function is_valid_value($value) { + return ($value === 'TRANSPARENT' || $value === 'OPAQUE'); + } +} + +// TODO: 4.8.3 timezone component properties + + +// 4.8.4 Relationship Component Properties +// --------------------------------------- + +class iCalendar_property_attendee extends iCalendar_property { + + var $name = 'ATTENDEE'; + var $val_type = RFC2445_TYPE_CAL_ADDRESS; + + // TODO: MUST NOT be specified when the calendar object has METHOD=PUBLISH + // TODO: standard has lots of detail here, make triple sure that we eventually conform + + function construct() { + $this->valid_parameters = array( + 'LANGUAGE' => RFC2445_OPTIONAL | RFC2445_ONCE, + 'CN' => RFC2445_OPTIONAL | RFC2445_ONCE, + 'ROLE' => RFC2445_OPTIONAL | RFC2445_ONCE, + 'PARTSTAT' => RFC2445_OPTIONAL | RFC2445_ONCE, + 'RSVP' => RFC2445_OPTIONAL | RFC2445_ONCE, + 'CUTYPE' => RFC2445_OPTIONAL | RFC2445_ONCE, + 'MEMBER' => RFC2445_OPTIONAL | RFC2445_ONCE, + 'DELEGATED-TO' => RFC2445_OPTIONAL | RFC2445_ONCE, + 'DELEGATED-FROM' => RFC2445_OPTIONAL | RFC2445_ONCE, + 'SENT-BY' => RFC2445_OPTIONAL | RFC2445_ONCE, + 'DIR' => RFC2445_OPTIONAL | RFC2445_ONCE, + RFC2445_XNAME => RFC2445_OPTIONAL + ); + } + + function set_parent_component($componentname) { + if(!parent::set_parent_component($componentname)) { + return false; + } + + if($this->parent_component == 'VFREEBUSY' || $this->parent_component == 'VALARM') { + // Most parameters become invalid in this case, the full allowed set is now: + $this->valid_parameters = array( + 'LANGUAGE' => RFC2445_OPTIONAL | RFC2445_ONCE, + RFC2445_XNAME => RFC2445_OPTIONAL + ); + } + + return false; + } + +} + +class iCalendar_property_contact extends iCalendar_property { + + var $name = 'CONTACT'; + var $val_type = RFC2445_TYPE_TEXT; + + function construct() { + $this->valid_parameters = array( + 'ALTREP' => RFC2445_OPTIONAL | RFC2445_ONCE, + 'LANGUAGE' => RFC2445_OPTIONAL | RFC2445_ONCE, + RFC2445_XNAME => RFC2445_OPTIONAL + ); + } +} + +class iCalendar_property_organizer extends iCalendar_property { + + var $name = 'ORGANIZER'; + var $val_type = RFC2445_TYPE_CAL_ADDRESS; + + function construct() { + $this->valid_parameters = array( + 'CN' => RFC2445_OPTIONAL | RFC2445_ONCE, + 'DIR' => RFC2445_OPTIONAL | RFC2445_ONCE, + 'SENT-BY' => RFC2445_OPTIONAL | RFC2445_ONCE, + 'LANGUAGE' => RFC2445_OPTIONAL | RFC2445_ONCE, + RFC2445_XNAME => RFC2445_OPTIONAL + ); + } + + // TODO: +/* + Conformance: This property MUST be specified in an iCalendar object + that specifies a group scheduled calendar entity. This property MUST + be specified in an iCalendar object that specifies the publication of + a calendar user's busy time. This property MUST NOT be specified in + an iCalendar object that specifies only a time zone definition or + that defines calendar entities that are not group scheduled entities, + but are entities only on a single user's calendar. +*/ + +} + +class iCalendar_property_recurrence_id extends iCalendar_property { + + // TODO: can only be specified when defining recurring components in the calendar +/* + Conformance: This property can be specified in an iCalendar object + containing a recurring calendar component. + + Description: The full range of calendar components specified by a + recurrence set is referenced by referring to just the "UID" property + value corresponding to the calendar component. The "RECURRENCE-ID" + property allows the reference to an individual instance within the + recurrence set. +*/ + + var $name = 'RECURRENCE-ID'; + var $val_type = RFC2445_TYPE_DATE_TIME; + + function construct() { + $this->valid_parameters = array( + 'RANGE' => RFC2445_OPTIONAL | RFC2445_ONCE, + 'TZID' => RFC2445_OPTIONAL | RFC2445_ONCE, + 'VALUE' => RFC2445_OPTIONAL | RFC2445_ONCE, + RFC2445_XNAME => RFC2445_OPTIONAL + ); + } + + function is_valid_parameter($parameter, $value) { + + $parameter = strtoupper($parameter); + + if(!parent::is_valid_parameter($parameter, $value)) { + return false; + } + if($parameter == 'VALUE' && !($value == 'DATE' || $value == 'DATE-TIME')) { + return false; + } + + return true; + } + +} + +class iCalendar_property_related_to extends iCalendar_property { + + var $name = 'RELATED-TO'; + var $val_type = RFC2445_TYPE_TEXT; + + // TODO: the value of this property must reference another component's UID + + function construct() { + $this->valid_parameters = array( + 'RELTYPE' => RFC2445_OPTIONAL | RFC2445_ONCE, + RFC2445_XNAME => RFC2445_OPTIONAL + ); + } +} + +class iCalendar_property_url extends iCalendar_property { + + var $name = 'URL'; + var $val_type = RFC2445_TYPE_URI; + + function construct() { + $this->valid_parameters = array( + RFC2445_XNAME => RFC2445_OPTIONAL + ); + } +} + +class iCalendar_property_uid extends iCalendar_property { + + var $name = 'UID'; + var $val_type = RFC2445_TYPE_TEXT; + + function construct() { + $this->valid_parameters = array( + RFC2445_XNAME => RFC2445_OPTIONAL + ); + + // The exception to the rule: this is not a static value, so we + // generate it on-the-fly here. Guaranteed to be different for + // each instance of this property, too. Nice. + $this->val_default = Bennu::generate_guid(); + } +} + +// 4.8.5 Recurrence Component Properties +// ------------------------------------- + +class iCalendar_property_exdate extends iCalendar_property { + + var $name = 'EXDATE'; + var $val_type = RFC2445_TYPE_DATE_TIME; + var $val_multi = true; + + function construct() { + $this->valid_parameters = array( + 'TZID' => RFC2445_OPTIONAL | RFC2445_ONCE, + 'VALUE' => RFC2445_OPTIONAL | RFC2445_ONCE, + RFC2445_XNAME => RFC2445_OPTIONAL + ); + } + + function is_valid_parameter($parameter, $value) { + + $parameter = strtoupper($parameter); + + if(!parent::is_valid_parameter($parameter, $value)) { + return false; + } + if($parameter == 'VALUE' && !($value == 'DATE' || $value == 'DATE-TIME')) { + return false; + } + + return true; + } + +} + +class iCalendar_property_exrule extends iCalendar_property { + + var $name = 'EXRULE'; + var $val_type = RFC2445_TYPE_RECUR; + + function construct() { + $this->valid_parameters = array( + RFC2445_XNAME => RFC2445_OPTIONAL + ); + } +} + +class iCalendar_property_rdate extends iCalendar_property { + + var $name = 'RDATE'; + var $val_type = RFC2445_TYPE_DATE_TIME; + var $val_multi = true; + + function construct() { + $this->valid_parameters = array( + 'TZID' => RFC2445_OPTIONAL | RFC2445_ONCE, + 'VALUE' => RFC2445_OPTIONAL | RFC2445_ONCE, + RFC2445_XNAME => RFC2445_OPTIONAL + ); + } + + function is_valid_parameter($parameter, $value) { + + $parameter = strtoupper($parameter); + + if(!parent::is_valid_parameter($parameter, $value)) { + return false; + } + if($parameter == 'VALUE' && !($value == 'DATE' || $value == 'DATE-TIME' || $value == 'PERIOD')) { + return false; + } + + return true; + } + +} + +class iCalendar_property_rrule extends iCalendar_property { + + var $name = 'RRULE'; + var $val_type = RFC2445_TYPE_RECUR; + + function construct() { + $this->valid_parameters = array( + RFC2445_XNAME => RFC2445_OPTIONAL + ); + } +} + +// TODO: 4.8.6 Alarm Component Properties + +// 4.8.7 Change Management Component Properties +// -------------------------------------------- + +class iCalendar_property_created extends iCalendar_property { + + var $name = 'CREATED'; + var $val_type = RFC2445_TYPE_DATE_TIME; + + function construct() { + $this->valid_parameters = array( + RFC2445_XNAME => RFC2445_OPTIONAL + ); + } + + function is_valid_value($value) { + if(!parent::is_valid_value($value)) { + return false; + } + // Time MUST be in UTC format + return(substr($value, -1) == 'Z'); + } +} + +class iCalendar_property_dtstamp extends iCalendar_property { + + var $name = 'DTSTAMP'; + var $val_type = RFC2445_TYPE_DATE_TIME; + + function construct() { + $this->valid_parameters = array( + RFC2445_XNAME => RFC2445_OPTIONAL + ); + } + + function is_valid_value($value) { + if(!parent::is_valid_value($value)) { + return false; + } + // Time MUST be in UTC format + return(substr($value, -1) == 'Z'); + } +} + +class iCalendar_property_last_modified extends iCalendar_property { + + var $name = 'LAST-MODIFIED'; + var $val_type = RFC2445_TYPE_DATE_TIME; + + function construct() { + $this->valid_parameters = array( + RFC2445_XNAME => RFC2445_OPTIONAL + ); + } + + function is_valid_value($value) { + if(!parent::is_valid_value($value)) { + return false; + } + // Time MUST be in UTC format + return(substr($value, -1) == 'Z'); + } +} + +class iCalendar_property_sequence extends iCalendar_property { + + var $name = 'SEQUENCE'; + var $val_type = RFC2445_TYPE_INTEGER; + var $val_default = 0; + + function construct() { + $this->valid_parameters = array( + RFC2445_XNAME => RFC2445_OPTIONAL + ); + } + + function is_valid_value($value) { + if(!parent::is_valid_value($value)) { + return false; + } + $value = intval($value); + return ($value >= 0); + } +} + +// 4.8.8 Miscellaneous Component Properties +// ---------------------------------------- + +class iCalendar_property_x extends iCalendar_property { + + var $name = RFC2445_XNAME; + var $val_type = NULL; + + function construct() { + $this->valid_parameters = array( + 'LANGUAGE' => RFC2445_OPTIONAL | RFC2445_ONCE, + RFC2445_XNAME => RFC2445_OPTIONAL + ); + } + + function set_name($name) { + + $name = strtoupper($name); + + if(rfc2445_is_xname($name)) { + $this->name = $name; + return true; + } + + return false; + } +} + +class iCalendar_property_request_status extends iCalendar_property { + + // IMPORTANT NOTE: This property value includes TEXT fields + // separated by semicolons. Unfortunately, auto-value-formatting + // cannot be used in this case. As an exception, the value passed + // to this property MUST be already escaped. + + var $name = 'REQUEST-STATUS'; + var $val_type = RFC2445_TYPE_TEXT; + + function construct() { + $this->valid_parameters = array( + 'LANGUAGE' => RFC2445_OPTIONAL | RFC2445_ONCE, + RFC2445_XNAME => RFC2445_OPTIONAL + ); + } + + function is_valid_value($value) { + if(!is_string($value) || empty($value)) { + return false; + } + + $len = strlen($value); + $parts = array(); + $from = 0; + $escch = false; + + for($i = 0; $i < $len; ++$i) { + if($value{$i} == ';' && !$escch) { + // Token completed + $parts[] = substr($value, $from, $i - $from); + $from = $i + 1; + continue; + } + $escch = ($value{$i} == '\\'); + } + // Add one last token with the remaining text; if the value + // ended with a ';' it was illegal, so check that this token + // is not the empty string. + $parts[] = substr($value, $from); + + $count = count($parts); + + // May have 2 or 3 tokens (last one is optional) + if($count != 2 && $count != 3) { + return false; + } + + // REMEMBER: if ANY part is empty, we have an illegal value + + // First token must be hierarchical numeric status (3 levels max) + if(strlen($parts[0]) == 0) { + return false; + } + + if($parts[0]{0} < '1' || $parts[0]{0} > '4') { + return false; + } + + $len = strlen($parts[0]); + + // Max 3 levels, and can't end with a period + if($len > 5 || $parts[0]{$len - 1} == '.') { + return false; + } + + for($i = 1; $i < $len; ++$i) { + if(($i & 1) == 1 && $parts[0]{$i} != '.') { + // Even-indexed chars must be periods + return false; + } + else if(($i & 1) == 0 && ($parts[0]{$i} < '0' || $parts[0]{$i} > '9')) { + // Odd-indexed chars must be numbers + return false; + } + } + + // Second and third tokens must be TEXT, and already escaped, so + // they are not allowed to have UNESCAPED semicolons, commas, slashes, + // or any newlines at all + + for($i = 1; $i < $count; ++$i) { + if(strpos($parts[$i], "\n") !== false) { + return false; + } + + $len = strlen($parts[$i]); + if($len == 0) { + // Cannot be empty + return false; + } + + $parts[$i] .= '#'; // This guard token saves some conditionals in the loop + + for($j = 0; $j < $len; ++$j) { + $thischar = $parts[$i]{$j}; + $nextchar = $parts[$i]{$j + 1}; + if($thischar == '\\') { + // Next char must now be one of ";,\nN" + if($nextchar != ';' && $nextchar != ',' && $nextchar != '\\' && + $nextchar != 'n' && $nextchar != 'N') { + return false; + } + + // OK, this escaped sequence is correct, bypass next char + ++$j; + continue; + } + if($thischar == ';' || $thischar == ',' || $thischar == '\\') { + // This wasn't escaped as it should + return false; + } + } + } + + return true; + } + + function set_value($value) { + // Must override this, otherwise the value would be quoted again + if($this->is_valid_value($value)) { + $this->value = $value; + return true; + } + + return false; + } + +} + + +####################### +/* +class iCalendar_property_class extends iCalendar_property { + + var $name = 'CLASS'; + var $val_type = RFC2445_TYPE_TEXT; + + function construct() { + $this->valid_parameters = array( + RFC2445_XNAME => RFC2445_OPTIONAL + ); + } +} +*/ + +?> diff --git a/lib/bennu/iCalendar_rfc2445.php b/lib/bennu/iCalendar_rfc2445.php new file mode 100644 index 0000000..e73d863 --- /dev/null +++ b/lib/bennu/iCalendar_rfc2445.php @@ -0,0 +1,785 @@ + RFC2445_FOLDED_LINE_LENGTH) { + $retval .= substr($string, 0, RFC2445_FOLDED_LINE_LENGTH - 1) . RFC2445_CRLF . ' '; + $string = substr($string, RFC2445_FOLDED_LINE_LENGTH - 1); + } + + $retval .= $string; + + return $retval; + +} + +function rfc2445_unfold($string) { + for($i = 0; $i < strlen(RFC2445_WSP); ++$i) { + $string = str_replace(RFC2445_CRLF.substr(RFC2445_WSP, $i, 1), '', $string); + } + + return $string; +} + +function rfc2445_is_xname($name) { + + // If it's less than 3 chars, it cannot be legal + if(strlen($name) < 3) { + return false; + } + + // If it contains an illegal char anywhere, reject it + if(strspn($name, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-') != strlen($name)) { + return false; + } + + // To be legal, it must still start with "X-" + return substr($name, 0, 2) === 'X-'; +} + +function rfc2445_is_valid_value($value, $type) { + + // This branch should only be taken with xname values + if($type === NULL) { + return true; + } + + switch($type) { + case RFC2445_TYPE_CAL_ADDRESS: + case RFC2445_TYPE_URI: + if(!is_string($value)) { + return false; + } + + $valid_schemes = array('ftp', 'http', 'ldap', 'gopher', 'mailto', 'news', 'nntp', 'telnet', 'wais', 'file', 'prospero'); + + $pos = strpos($value, ':'); + if(!$pos) { + return false; + } + + $scheme = strtolower(substr($value, 0, $pos)); + $remain = substr($value, $pos + 1); + + if(!in_array($scheme, $valid_schemes)) { + return false; + } + + if($scheme === 'mailto') { + $regexp = '^[a-zA-Z0-9]+[_a-zA-Z0-9\-]*(\.[_a-z0-9\-]+)*@(([0-9a-zA-Z\-]+\.)+[a-zA-Z][0-9a-zA-Z\-]+|([0-9]{1,3}\.){3}[0-9]{1,3})$'; + } + else { + $regexp = '^//(.+(:.*)?@)?(([0-9a-zA-Z\-]+\.)+[a-zA-Z][0-9a-zA-Z\-]+|([0-9]{1,3}\.){3}[0-9]{1,3})(:[0-9]{1,5})?(/.*)?$'; + } + + return ereg($regexp, $remain); + break; + + case RFC2445_TYPE_BINARY: + if(!is_string($value)) { + return false; + } + + $len = strlen($value); + + if($len % 4 != 0) { + return false; + } + + for($i = 0; $i < $len; ++$i) { + $ch = $value{$i}; + if(!($ch >= 'a' && $ch <= 'z' || $ch >= 'A' && $ch <= 'Z' || $ch >= '0' && $ch <= '9' || $ch == '-' || $ch == '+')) { + if($ch == '=' && $len - $i <= 2) { + continue; + } + return false; + } + } + return true; + break; + + case RFC2445_TYPE_BOOLEAN: + if(is_bool($value)) { + return true; + } + if(is_string($value)) { + $value = strtoupper($value); + return ($value == 'TRUE' || $value == 'FALSE'); + } + return false; + break; + + case RFC2445_TYPE_DATE: + if(is_int($value)) { + if($value < 0) { + return false; + } + $value = "$value"; + } + else if(!is_string($value)) { + return false; + } + + if(strlen($value) != 8) { + return false; + } + + $y = intval(substr($value, 0, 4)); + $m = intval(substr($value, 4, 2)); + $d = intval(substr($value, 6, 2)); + + return checkdate($m, $d, $y); + break; + + case RFC2445_TYPE_DATE_TIME: + if(!is_string($value) || strlen($value) < 15) { + return false; + } + + return($value{8} == 'T' && + rfc2445_is_valid_value(substr($value, 0, 8), RFC2445_TYPE_DATE) && + rfc2445_is_valid_value(substr($value, 9), RFC2445_TYPE_TIME)); + break; + + case RFC2445_TYPE_DURATION: + if(!is_string($value)) { + return false; + } + + $len = strlen($value); + + if($len < 3) { + // Minimum conformant length: "P1W" + return false; + } + + if($value{0} == '+' || $value{0} == '-') { + $value = substr($value, 1); + --$len; // Don't forget to update this! + } + + if($value{0} != 'P') { + return false; + } + + // OK, now break it up + $num = ''; + $allowed = 'WDT'; + + for($i = 1; $i < $len; ++$i) { + $ch = $value{$i}; + if($ch >= '0' && $ch <= '9') { + $num .= $ch; + continue; + } + if(strpos($allowed, $ch) === false) { + // Non-numeric character which shouldn't be here + return false; + } + if($num === '' && $ch != 'T') { + // Allowed non-numeric character, but no digits came before it + return false; + } + + // OK, $ch now holds a character which tells us what $num is + switch($ch) { + case 'W': + // If duration in weeks is specified, this must end the string + return ($i == $len - 1); + break; + + case 'D': + // Days specified, now if anything comes after it must be a 'T' + $allowed = 'T'; + break; + + case 'T': + // Starting to specify time, H M S are now valid delimiters + $allowed = 'HMS'; + break; + + case 'H': + $allowed = 'M'; + break; + + case 'M': + $allowed = 'S'; + break; + + case 'S': + return ($i == $len - 1); + break; + } + + // If we 're going to continue, reset $num + $num = ''; + + } + + // $num is kept for this reason: if we 're here, we ran out of chars + // therefore $num must be empty for the period to be legal + return ($num === '' && $ch != 'T'); + + break; + + case RFC2445_TYPE_FLOAT: + if(is_float($value)) { + return true; + } + if(!is_string($value) || $value === '') { + return false; + } + + $dot = false; + $int = false; + $len = strlen($value); + for($i = 0; $i < $len; ++$i) { + switch($value{$i}) { + case '-': case '+': + // A sign can only be seen at position 0 and cannot be the only char + if($i != 0 || $len == 1) { + return false; + } + break; + case '.': + // A second dot is an error + // Make sure we had at least one int before the dot + if($dot || !$int) { + return false; + } + $dot = true; + // Make also sure that the float doesn't end with a dot + if($i == $len - 1) { + return false; + } + break; + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + $int = true; + break; + default: + // Any other char is a no-no + return false; + break; + } + } + return true; + break; + + case RFC2445_TYPE_INTEGER: + if(is_int($value)) { + return true; + } + if(!is_string($value) || $value === '') { + return false; + } + + if($value{0} == '+' || $value{0} == '-') { + if(strlen($value) == 1) { + return false; + } + $value = substr($value, 1); + } + + if(strspn($value, '0123456789') != strlen($value)) { + return false; + } + + return ($value >= -2147483648 && $value <= 2147483647); + break; + + case RFC2445_TYPE_PERIOD: + if(!is_string($value) || empty($value)) { + return false; + } + + $parts = explode('/', $value); + if(count($parts) != 2) { + return false; + } + + if(!rfc2445_is_valid_value($parts[0], RFC2445_TYPE_DATE_TIME)) { + return false; + } + + // Two legal cases for the second part: + if(rfc2445_is_valid_value($parts[1], RFC2445_TYPE_DATE_TIME)) { + // It has to be after the start time, so + return ($parts[1] > $parts[0]); + } + else if(rfc2445_is_valid_value($parts[1], RFC2445_TYPE_DURATION)) { + // The period MUST NOT be negative + return ($parts[1]{0} != '-'); + } + + // It seems to be illegal + return false; + break; + + case RFC2445_TYPE_RECUR: + if(!is_string($value)) { + return false; + } + + $parts = explode(';', strtoupper($value)); + + // First of all, we need at least a FREQ and a UNTIL or COUNT part, so... + if(count($parts) < 2) { + return false; + } + + // Let's get that into a more easily comprehensible format + $vars = array(); + foreach($parts as $part) { + + $pieces = explode('=', $part); + // There must be exactly 2 pieces, e.g. FREQ=WEEKLY + if(count($pieces) != 2) { + return false; + } + + // It's illegal for a variable to appear twice + if(isset($vars[$pieces[0]])) { + return false; + } + + // Sounds good + $vars[$pieces[0]] = $pieces[1]; + } + + // OK... now to test everything else + + // FREQ must be the first thing appearing + reset($vars); + if(key($vars) != 'FREQ') { + return false; + } + + // It's illegal to have both UNTIL and COUNT appear + if(isset($vars['UNTIL']) && isset($vars['COUNT'])) { + return false; + } + + // Special case: BYWEEKNO is only valid for FREQ=YEARLY + if(isset($vars['BYWEEKNO']) && $vars['FREQ'] != 'YEARLY') { + return false; + } + + // Special case: BYSETPOS is only valid if another BY option is specified + if(isset($vars['BYSETPOS'])) { + $options = array('BYSECOND', 'BYMINUTE', 'BYHOUR', 'BYDAY', 'BYMONTHDAY', 'BYYEARDAY', 'BYWEEKNO', 'BYMONTH'); + $defined = array_keys($vars); + $common = array_intersect($options, $defined); + if(empty($common)) { + return false; + } + } + + // OK, now simply check if each element has a valid value, + // unsetting them on the way. If at the end the array still + // has some elements, they are illegal. + + if($vars['FREQ'] != 'SECONDLY' && $vars['FREQ'] != 'MINUTELY' && $vars['FREQ'] != 'HOURLY' && + $vars['FREQ'] != 'DAILY' && $vars['FREQ'] != 'WEEKLY' && + $vars['FREQ'] != 'MONTHLY' && $vars['FREQ'] != 'YEARLY') { + return false; + } + unset($vars['FREQ']); + + // Set this, we may need it later + $weekdays = explode(',', RFC2445_WEEKDAYS); + + if(isset($vars['UNTIL'])) { + if(rfc2445_is_valid_value($vars['UNTIL'], RFC2445_TYPE_DATE_TIME)) { + // The time MUST be in UTC format + if(!(substr($vars['UNTIL'], -1) == 'Z')) { + return false; + } + } + else if(!rfc2445_is_valid_value($vars['UNTIL'], RFC2445_TYPE_DATE_TIME)) { + return false; + } + } + unset($vars['UNTIL']); + + + if(isset($vars['COUNT'])) { + if(empty($vars['COUNT'])) { + // This also catches the string '0', which makes no sense + return false; + } + if(strspn($vars['COUNT'], '0123456789') != strlen($vars['COUNT'])) { + return false; + } + } + unset($vars['COUNT']); + + + if(isset($vars['INTERVAL'])) { + if(empty($vars['INTERVAL'])) { + // This also catches the string '0', which makes no sense + return false; + } + if(strspn($vars['INTERVAL'], '0123456789') != strlen($vars['INTERVAL'])) { + return false; + } + } + unset($vars['INTERVAL']); + + + if(isset($vars['BYSECOND'])) { + if($vars['BYSECOND'] == '') { + return false; + } + // Comma also allowed + if(strspn($vars['BYSECOND'], '0123456789,') != strlen($vars['BYSECOND'])) { + return false; + } + $secs = explode(',', $vars['BYSECOND']); + foreach($secs as $sec) { + if($sec == '' || $sec < 0 || $sec > 59) { + return false; + } + } + } + unset($vars['BYSECOND']); + + + if(isset($vars['BYMINUTE'])) { + if($vars['BYMINUTE'] == '') { + return false; + } + // Comma also allowed + if(strspn($vars['BYMINUTE'], '0123456789,') != strlen($vars['BYMINUTE'])) { + return false; + } + $mins = explode(',', $vars['BYMINUTE']); + foreach($mins as $min) { + if($min == '' || $min < 0 || $min > 59) { + return false; + } + } + } + unset($vars['BYMINUTE']); + + + if(isset($vars['BYHOUR'])) { + if($vars['BYHOUR'] == '') { + return false; + } + // Comma also allowed + if(strspn($vars['BYHOUR'], '0123456789,') != strlen($vars['BYHOUR'])) { + return false; + } + $hours = explode(',', $vars['BYHOUR']); + foreach($hours as $hour) { + if($hour == '' || $hour < 0 || $hour > 23) { + return false; + } + } + } + unset($vars['BYHOUR']); + + + if(isset($vars['BYDAY'])) { + if(empty($vars['BYDAY'])) { + return false; + } + + // First off, split up all values we may have + $days = explode(',', $vars['BYDAY']); + + foreach($days as $day) { + $daypart = substr($day, -2); + if(!in_array($daypart, $weekdays)) { + return false; + } + + if(strlen($day) > 2) { + $intpart = substr($day, 0, strlen($day) - 2); + if(!rfc2445_is_valid_value($intpart, RFC2445_TYPE_INTEGER)) { + return false; + } + if(intval($intpart) == 0) { + return false; + } + } + } + } + unset($vars['BYDAY']); + + + if(isset($vars['BYMONTHDAY'])) { + if(empty($vars['BYMONTHDAY'])) { + return false; + } + $mdays = explode(',', $vars['BYMONTHDAY']); + foreach($mdays as $mday) { + if(!rfc2445_is_valid_value($mday, RFC2445_TYPE_INTEGER)) { + return false; + } + $mday = abs(intval($mday)); + if($mday == 0 || $mday > 31) { + return false; + } + } + } + unset($vars['BYMONTHDAY']); + + + if(isset($vars['BYYEARDAY'])) { + if(empty($vars['BYYEARDAY'])) { + return false; + } + $ydays = explode(',', $vars['BYYEARDAY']); + foreach($ydays as $yday) { + if(!rfc2445_is_valid_value($yday, RFC2445_TYPE_INTEGER)) { + return false; + } + $yday = abs(intval($yday)); + if($yday == 0 || $yday > 366) { + return false; + } + } + } + unset($vars['BYYEARDAY']); + + + if(isset($vars['BYWEEKNO'])) { + if(empty($vars['BYWEEKNO'])) { + return false; + } + $weeknos = explode(',', $vars['BYWEEKNO']); + foreach($weeknos as $weekno) { + if(!rfc2445_is_valid_value($weekno, RFC2445_TYPE_INTEGER)) { + return false; + } + $weekno = abs(intval($weekno)); + if($weekno == 0 || $weekno > 53) { + return false; + } + } + } + unset($vars['BYWEEKNO']); + + + if(isset($vars['BYMONTH'])) { + if(empty($vars['BYMONTH'])) { + return false; + } + // Comma also allowed + if(strspn($vars['BYMONTH'], '0123456789,') != strlen($vars['BYMONTH'])) { + return false; + } + $months = explode(',', $vars['BYMONTH']); + foreach($months as $month) { + if($month == '' || $month < 1 || $month > 12) { + return false; + } + } + } + unset($vars['BYMONTH']); + + + if(isset($vars['BYSETPOS'])) { + if(empty($vars['BYSETPOS'])) { + return false; + } + $sets = explode(',', $vars['BYSETPOS']); + foreach($sets as $set) { + if(!rfc2445_is_valid_value($set, RFC2445_TYPE_INTEGER)) { + return false; + } + $set = abs(intval($set)); + if($set == 0 || $set > 366) { + return false; + } + } + } + unset($vars['BYSETPOS']); + + + if(isset($vars['WKST'])) { + if(!in_array($vars['WKST'], $weekdays)) { + return false; + } + } + unset($vars['WKST']); + + + // Any remaining vars must be x-names + if(empty($vars)) { + return true; + } + + foreach($vars as $name => $var) { + if(!rfc2445_is_xname($name)) { + return false; + } + } + + // At last, all is OK! + return true; + + break; + + case RFC2445_TYPE_TEXT: + return true; + break; + + case RFC2445_TYPE_TIME: + if(is_int($value)) { + if($value < 0) { + return false; + } + $value = "$value"; + } + else if(!is_string($value)) { + return false; + } + + if(strlen($value) == 7) { + if(strtoupper(substr($value, -1)) != 'Z') { + return false; + } + $value = substr($value, 0, 6); + } + if(strlen($value) != 6) { + return false; + } + + $h = intval(substr($value, 0, 2)); + $m = intval(substr($value, 2, 2)); + $s = intval(substr($value, 4, 2)); + + return ($h <= 23 && $m <= 59 && $s <= 60); + break; + + case RFC2445_TYPE_UTC_OFFSET: + if(is_int($value)) { + if($value >= 0) { + $value = "+$value"; + } + else { + $value = "$value"; + } + } + else if(!is_string($value)) { + return false; + } + + if(strlen($value) == 7) { + $s = intval(substr($value, 5, 2)); + $value = substr($value, 0, 5); + } + if(strlen($value) != 5 || $value == "-0000") { + return false; + } + + if($value{0} != '+' && $value{0} != '-') { + return false; + } + + $h = intval(substr($value, 1, 2)); + $m = intval(substr($value, 3, 2)); + + return ($h <= 23 && $m <= 59 && $s <= 59); + break; + } + + // TODO: remove this assertion + trigger_error('bad code path', E_USER_WARNING); + var_dump($type); + return false; +} + +function rfc2445_do_value_formatting($value, $type) { + // Note: this does not only do formatting; it also does conversion to string! + switch($type) { + case RFC2445_TYPE_CAL_ADDRESS: + case RFC2445_TYPE_URI: + // Enclose in double quotes + $value = '"'.$value.'"'; + break; + case RFC2445_TYPE_TEXT: + // Escape entities + $value = strtr($value, array("\n" => '\\n', '\\' => '\\\\', ',' => '\\,', ';' => '\\;')); + break; + } + return $value; +} + +function rfc2445_undo_value_formatting($value, $type) { + switch($type) { + case RFC2445_TYPE_CAL_ADDRESS: + case RFC2445_TYPE_URI: + // Trim beginning and end double quote + $value = substr($value, 1, strlen($value) - 2); + break; + case RFC2445_TYPE_TEXT: + // Unescape entities + $value = strtr($value, array('\\n' => "\n", '\\N' => "\n", '\\\\' => '\\', '\\,' => ',', '\\;' => ';')); + break; + } + return $value; +} + +?> -- cgit v1.2.3