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/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 ++++++++++++++++++++++ 5 files changed, 2793 insertions(+) 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/bennu') 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