From 0c8b855ffbbf7f5a935a870b3bc33bd46d66e2d9 Mon Sep 17 00:00:00 2001 From: Andrew McMillan Date: Wed, 19 May 2010 23:52:20 +1200 Subject: [PATCH] [vComponent] Generic base class for handling VCALENDAR, VCARD, etc. --- inc/vComponent.php | 753 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 753 insertions(+) create mode 100644 inc/vComponent.php diff --git a/inc/vComponent.php b/inc/vComponent.php new file mode 100644 index 0000000..c8250d5 --- /dev/null +++ b/inc/vComponent.php @@ -0,0 +1,753 @@ + +* @copyright Morphoss Ltd +* @license http://gnu.org/copyleft/gpl.html GNU GPL v2 or later +* +*/ +require_once('XMLElement.php'); + +/** +* A Class for representing properties within a vComponent (VCALENDAR or VCARD) +* +* @package awl +*/ +class vProperty { + /**#@+ + * @access private + */ + + /** + * The name of this property + * + * @var string + */ + protected $name; + + /** + * An array of parameters to this property, represented as key/value pairs. + * + * @var array + */ + protected $parameters; + + /** + * The value of this property. + * + * @var string + */ + protected $content; + + /** + * The original value that this was parsed from, if that's the way it happened. + * + * @var string + */ + protected $rendered; + + /**#@-*/ + + /** + * The constructor parses the incoming string, which is formatted as per RFC2445 as a + * propname[;param1=pval1[; ... ]]:propvalue + * however we allow ourselves to assume that the RFC2445 content unescaping has already + * happened when vComponent::ParseFrom() called vComponent::UnwrapComponent(). + * + * @param string $propstring The string from the vComponent which contains this property. + */ + function __construct( $propstring = null ) { + $this->name = ""; + $this->content = ""; + $this->parameters = array(); + unset($this->rendered); + if ( $propstring != null && gettype($propstring) == 'string' ) { + $this->ParseFrom($propstring); + } + } + + + /** + * The constructor parses the incoming string, which is formatted as per RFC2445 as a + * propname[;param1=pval1[; ... ]]:propvalue + * however we allow ourselves to assume that the RFC2445 content unescaping has already + * happened when vComponent::ParseFrom() called vComponent::UnwrapComponent(). + * + * @param string $propstring The string from the vComponent which contains this property. + */ + function ParseFrom( $propstring ) { + $this->rendered = (strlen($propstring) < 72 ? $propstring : null); // Only pre-rendered if we didn't unescape it + $pos = strpos( $propstring, ':'); + $start = substr( $propstring, 0, $pos); + + $unescaped = str_replace( '\\n', "\n", substr( $propstring, $pos + 1)); + $unescaped = str_replace( '\\N', "\n", $unescaped); + $this->content = preg_replace( "/\\\\([,;:\"\\\\])/", '$1', $unescaped); + + $parameters = explode(';',$start); + $this->name = strtoupper(array_shift( $parameters )); + $this->parameters = array(); + foreach( $parameters AS $k => $v ) { + $pos = strpos($v,'='); + $name = strtoupper(substr( $v, 0, $pos)); + $value = substr( $v, $pos + 1); + $this->parameters[$name] = $value; + } +// dbg_error_log('vComponent', " vProperty::ParseFrom found '%s' = '%s' with %d parameters", $this->name, substr($this->content,0,200), count($this->parameters) ); + } + + + /** + * Get/Set name property + * + * @param string $newname [optional] A new name for the property + * + * @return string The name for the property. + */ + function Name( $newname = null ) { + if ( $newname != null ) { + $this->name = strtoupper($newname); + if ( isset($this->rendered) ) unset($this->rendered); +// dbg_error_log('vComponent', " vProperty::Name(%s)", $this->name ); + } + return $this->name; + } + + + /** + * Get/Set the content of the property + * + * @param string $newvalue [optional] A new value for the property + * + * @return string The value of the property. + */ + function Value( $newvalue = null ) { + if ( $newvalue != null ) { + $this->content = $newvalue; + if ( isset($this->rendered) ) unset($this->rendered); + } + return $this->content; + } + + + /** + * Get/Set parameters in their entirety + * + * @param array $newparams An array of new parameter key/value pairs + * + * @return array The current array of parameters for the property. + */ + function Parameters( $newparams = null ) { + if ( $newparams != null ) { + $this->parameters = $newparams; + if ( isset($this->rendered) ) unset($this->rendered); + } + return $this->parameters; + } + + + /** + * Test if our value contains a string + * + * @param string $search The needle which we shall search the haystack for. + * + * @return string The name for the property. + */ + function TextMatch( $search ) { + if ( isset($this->content) ) return strstr( $this->content, $search ); + return false; + } + + + /** + * Get the value of a parameter + * + * @param string $name The name of the parameter to retrieve the value for + * + * @return string The value of the parameter + */ + function GetParameterValue( $name ) { + $name = strtoupper($name); + if ( isset($this->parameters[$name]) ) return $this->parameters[$name]; + } + + /** + * Set the value of a parameter + * + * @param string $name The name of the parameter to set the value for + * + * @param string $value The value of the parameter + */ + function SetParameterValue( $name, $value ) { + if ( isset($this->rendered) ) unset($this->rendered); + $this->parameters[strtoupper($name)] = $value; + } + + /** + * Render the set of parameters as key1=value1[;key2=value2[; ...]] with + * any colons or semicolons escaped. + */ + function RenderParameters() { + $rendered = ""; + foreach( $this->parameters AS $k => $v ) { + $escaped = preg_replace( "/([;:\"])/", '\\\\$1', $v); + $rendered .= sprintf( ";%s=%s", $k, $escaped ); + } + return $rendered; + } + + + /** + * Render a suitably escaped RFC2445 content string. + */ + function Render() { + // If we still have the string it was parsed in from, it hasn't been screwed with + // and we can just return that without modification. + if ( isset($this->rendered) ) return $this->rendered; + + $property = preg_replace( '/[;].*$/', '', $this->name ); + $escaped = $this->content; + switch( $property ) { + /** Content escaping does not apply to these properties culled from RFC2445 */ + case 'ATTACH': case 'GEO': case 'PERCENT-COMPLETE': case 'PRIORITY': + case 'DURATION': case 'FREEBUSY': case 'TZOFFSETFROM': case 'TZOFFSETTO': + case 'TZURL': case 'ATTENDEE': case 'ORGANIZER': case 'RECURRENCE-ID': + case 'URL': case 'EXRULE': case 'SEQUENCE': case 'CREATED': + case 'RRULE': case 'REPEAT': case 'TRIGGER': case 'RDATE': + case 'COMPLETED': case 'DTEND': case 'DUE': case 'DTSTART': + case 'DTSTAMP': case 'LAST-MODIFIED': case 'CREATED': case 'EXDATE': + break; + + /** Content escaping applies by default to other properties */ + default: + $escaped = str_replace( '\\', '\\\\', $escaped); + $escaped = preg_replace( '/\r?\n/', '\\n', $escaped); + $escaped = preg_replace( "/([,;\"])/", '\\\\$1', $escaped); + } + $property = sprintf( "%s%s:", $this->name, $this->RenderParameters() ); + if ( (strlen($property) + strlen($escaped)) <= 72 ) { + $this->rendered = $property . $escaped; + } + else if ( (strlen($property) + strlen($escaped)) > 72 && (strlen($property) < 72) && (strlen($escaped) < 72) ) { + $this->rendered = $property . " \r\n " . $escaped; + } + else { + $this->rendered = wordwrap( $property . $escaped, 72, " \r\n ", true ); + } + return $this->rendered; + } + +} + + +/** +* A Class for representing components within an vComponent +* +* @package awl +*/ +class vComponent { + /**#@+ + * @access private + */ + + /** + * The type of this component, such as 'VEVENT', 'VTODO', 'VTIMEZONE', 'VCARD', etc. + * + * @var string + */ + protected $type; + + /** + * An array of properties, which are vProperty objects + * + * @var array + */ + protected $properties; + + /** + * An array of (sub-)components, which are vComponent objects + * + * @var array + */ + protected $components; + + /** + * The rendered result (or what was originally parsed, if there have been no changes) + * + * @var array + */ + protected $rendered; + + /**#@-*/ + + /** + * A basic constructor + */ + function __construct( $content = null ) { + $this->type = ""; + $this->properties = array(); + $this->components = array(); + $this->rendered = ""; + if ( $content != null && (gettype($content) == 'string' || gettype($content) == 'array') ) { + $this->ParseFrom($content); + } + } + + + /** + * Collect an array of all parameters of our properties which are the specified type + * Mainly used for collecting the full variety of references TZIDs + */ + function CollectParameterValues( $parameter_name ) { + $values = array(); + foreach( $this->components AS $k => $v ) { + $also = $v->CollectParameterValues($parameter_name); + $values = array_merge( $values, $also ); + } + foreach( $this->properties AS $k => $v ) { + $also = $v->GetParameterValue($parameter_name); + if ( isset($also) && $also != "" ) { +// dbg_error_log( 'vComponent', "::CollectParameterValues(%s) : Found '%s'", $parameter_name, $also); + $values[$also] = 1; + } + } + return $values; + } + + + /** + * Parse the text $content into sets of vProperty & vComponent within this vComponent + * @param string $content The raw RFC2445-compliant vComponent component, including BEGIN:TYPE & END:TYPE + */ + function ParseFrom( $content ) { + $this->rendered = $content; + $content = $this->UnwrapComponent($content); + + $type = false; + $subtype = false; + $finish = null; + $subfinish = null; + + $length = strlen($content); + $linefrom = 0; + while( $linefrom < $length ) { + $lineto = strpos( $content, "\n", $linefrom ); + if ( $lineto === false ) { + $lineto = strpos( $content, "\r", $linefrom ); + } + if ( $lineto > 0 ) { + $line = substr( $content, $linefrom, $lineto - $linefrom); + $linefrom = $lineto + 1; + } + else { + $line = substr( $content, $linefrom ); + $linefrom = $length; + } + if ( preg_match('/^\s*$/', $line ) ) continue; + $line = rtrim( $line, "\r\n" ); +// dbg_error_log( 'vComponent', "::ParseFrom: Parsing line: $line"); + + if ( $type === false ) { + if ( preg_match( '/^BEGIN:(.+)$/i', $line, $matches ) ) { + // We have found the start of the main component + $type = strtoupper($matches[1]); + $finish = "END:$type"; + $this->type = $type; + dbg_error_log( 'vComponent', "::ParseFrom: Start component of type '%s'", $type); + } + else { + dbg_error_log( 'vComponent', "::ParseFrom: Ignoring crap before start of component: $line"); + // unset($lines[$k]); // The content has crap before the start + if ( $line != "" ) $this->rendered = null; + } + } + else if ( $type == null ) { + dbg_error_log( 'vComponent', "::ParseFrom: Ignoring crap after end of component"); + if ( $line != "" ) $this->rendered = null; + } + else if ( strtoupper($line) == $finish ) { + dbg_error_log( 'vComponent', "::ParseFrom: End of component"); + $type = null; // We have reached the end of our component + } + else { + if ( $subtype === false && preg_match( '/^BEGIN:(.+)$/i', $line, $matches ) ) { + // We have found the start of a sub-component + $subtype = strtoupper($matches[1]); + $subfinish = "END:$subtype"; + $subcomponent = $line . "\r\n"; + dbg_error_log( 'vComponent', "::ParseFrom: Found a subcomponent '%s'", $subtype); + } + else if ( $subtype ) { + // We are inside a sub-component + $subcomponent .= $this->WrapComponent($line); + if ( strtoupper($line) == $subfinish ) { + dbg_error_log( 'vComponent', "::ParseFrom: End of subcomponent '%s'", $subtype); + // We have found the end of a sub-component + $this->components[] = new vComponent($subcomponent); + $subtype = false; + } +// else +// dbg_error_log( 'vComponent', "::ParseFrom: Inside a subcomponent '%s'", $subtype ); + } + else { +// dbg_error_log( 'vComponent', "::ParseFrom: Parse property of component"); + // It must be a normal property line within a component. + $this->properties[] = new vProperty($line); + } + } + } + } + + + /** + * This unescapes the (CRLF + linear space) wrapping specified in RFC2445. According + * to RFC2445 we should always end with CRLF but the CalDAV spec says that normalising + * XML parsers often muck with it and may remove the CR. We accept either case. + */ + function UnwrapComponent( $content ) { + return preg_replace('/\r?\n[ \t]/', '', $content ); + } + + /** + * This imposes the (CRLF + linear space) wrapping specified in RFC2445. According + * to RFC2445 we should always end with CRLF but the CalDAV spec says that normalising + * XML parsers often muck with it and may remove the CR. We output RFC2445 compliance. + * + * In order to preserve pre-existing wrapping in the component, we split the incoming + * string on line breaks before running wordwrap over each component of that. + */ + function WrapComponent( $content ) { + $strs = preg_split( "/\r?\n/", $content ); + $wrapped = ""; + foreach ($strs as $str) { + $wrapped .= wordwrap($str, 73, " \r\n ") . "\r\n"; + } + return $wrapped; + } + + /** + * Return the type of component which this is + */ + function GetType() { + return $this->type; + } + + + /** + * Set the type of component which this is + */ + function SetType( $type ) { + if ( isset($this->rendered) ) unset($this->rendered); + $this->type = $type; + return $this->type; + } + + + /** + * Get all properties, or the properties matching a particular type + */ + function GetProperties( $type = null ) { + $properties = array(); + foreach( $this->properties AS $k => $v ) { + if ( $type == null || $v->Name() == $type ) { + $properties[$k] = $v; + } + } + return $properties; + } + + + /** + * Get the value of the first property matching the name. Obviously this isn't + * so useful for properties which may occur multiply, but most don't. + * + * @param string $type The type of property we are after. + * @return string The value of the property, or null if there was no such property. + */ + function GetPValue( $type ) { + foreach( $this->properties AS $k => $v ) { + if ( $v->Name() == $type ) return $v->Value(); + } + return null; + } + + + /** + * Get the value of the specified parameter for the first property matching the + * name. Obviously this isn't so useful for properties which may occur multiply, but most don't. + * + * @param string $type The type of property we are after. + * @param string $type The name of the parameter we are after. + * @return string The value of the parameter for the property, or null in the case that there was no such property, or no such parameter. + */ + function GetPParamValue( $type, $parameter_name ) { + foreach( $this->properties AS $k => $v ) { + if ( $v->Name() == $type ) return $v->GetParameterValue($parameter_name); + } + return null; + } + + + /** + * Clear all properties, or the properties matching a particular type + * @param string $type The type of property - omit for all properties + */ + function ClearProperties( $type = null ) { + if ( $type != null ) { + // First remove all the existing ones of that type + foreach( $this->properties AS $k => $v ) { + if ( $v->Name() == $type ) { + unset($this->properties[$k]); + if ( isset($this->rendered) ) unset($this->rendered); + } + } + $this->properties = array_values($this->properties); + } + else { + if ( isset($this->rendered) ) unset($this->rendered); + $this->properties = array(); + } + } + + + /** + * Set all properties, or the ones matching a particular type + */ + function SetProperties( $new_properties, $type = null ) { + if ( isset($this->rendered) && count($new_properties) > 0 ) unset($this->rendered); + $this->ClearProperties($type); + foreach( $new_properties AS $k => $v ) { + $this->AddProperty($v); + } + } + + + /** + * Adds a new property + * + * @param vProperty $new_property The new property to append to the set, or a string with the name + * @param string $value The value of the new property (default: param 1 is an vProperty with everything + * @param array $parameters The key/value parameter pairs (default: none, or param 1 is an vProperty with everything) + */ + function AddProperty( $new_property, $value = null, $parameters = null ) { + if ( isset($this->rendered) ) unset($this->rendered); + if ( isset($value) && gettype($new_property) == 'string' ) { + $new_prop = new vProperty(); + $new_prop->Name($new_property); + $new_prop->Value($value); + if ( $parameters != null ) $new_prop->Parameters($parameters); + dbg_error_log('vComponent'," Adding new property '%s'", $new_prop->Render() ); + $this->properties[] = $new_prop; + } + else if ( gettype($new_property) ) { + $this->properties[] = $new_property; + } + } + + + /** + * Get all sub-components, or at least get those matching a type, or failling to match, + * should the second parameter be set to false. + * + * @param string $type The type to match (default: All) + * @param boolean $normal_match Set to false to invert the match (default: true) + * @return array an array of the sub-components + */ + function GetComponents( $type = null, $normal_match = true ) { + $components = $this->components; + if ( $type != null ) { + foreach( $components AS $k => $v ) { + if ( ($v->GetType() != $type) === $normal_match ) { + unset($components[$k]); + } + } + $components = array_values($components); + } + return $components; + } + + + /** + * Clear all components, or the components matching a particular type + * @param string $type The type of component - omit for all components + */ + function ClearComponents( $type = null ) { + if ( $type != null ) { + // First remove all the existing ones of that type + foreach( $this->components AS $k => $v ) { + if ( $v->GetType() == $type ) { + unset($this->components[$k]); + if ( isset($this->rendered) ) unset($this->rendered); + } + else { + if ( ! $this->components[$k]->ClearComponents($type) ) { + if ( isset($this->rendered) ) unset($this->rendered); + } + } + } + return isset($this->rendered); + } + else { + if ( isset($this->rendered) ) unset($this->rendered); + $this->components = array(); + } + } + + + /** + * Sets some or all sub-components of the component to the supplied new components + * + * @param array of vComponent $new_components The new components to replace the existing ones + * @param string $type The type of components to be replaced. Defaults to null, which means all components will be replaced. + */ + function SetComponents( $new_component, $type = null ) { + if ( isset($this->rendered) ) unset($this->rendered); + if ( count($new_component) > 0 ) $this->ClearComponents($type); + foreach( $new_component AS $k => $v ) { + $this->components[] = $v; + } + } + + + /** + * Adds a new subcomponent + * + * @param vComponent $new_component The new component to append to the set + */ + function AddComponent( $new_component ) { + if ( is_array($new_component) && count($new_component) == 0 ) return; + if ( isset($this->rendered) ) unset($this->rendered); + if ( is_array($new_component) ) { + foreach( $new_component AS $k => $v ) { + $this->components[] = $v; + } + } + else { + $this->components[] = $new_component; + } + } + + + /** + * Mask components, removing any that are not of the types in the list + * @param array $keep An array of component types to be kept + */ + function MaskComponents( $keep ) { + foreach( $this->components AS $k => $v ) { + if ( ! in_array( $v->GetType(), $keep ) ) { + unset($this->components[$k]); + if ( isset($this->rendered) ) unset($this->rendered); + } + else { + $v->MaskComponents($keep); + } + } + } + + + /** + * Mask properties, removing any that are not in the list + * @param array $keep An array of property names to be kept + * @param array $component_list An array of component types to check within + */ + function MaskProperties( $keep, $component_list=null ) { + foreach( $this->components AS $k => $v ) { + $v->MaskProperties($keep, $component_list); + } + + if ( !isset($component_list) || in_array($this->GetType(),$component_list) ) { + foreach( $this->components AS $k => $v ) { + if ( ! in_array( $v->GetType(), $keep ) ) { + unset($this->components[$k]); + if ( isset($this->rendered) ) unset($this->rendered); + } + } + } + } + + + /** + * Renders the component, possibly restricted to only the listed properties + */ + function Render( $restricted_properties = null) { + + $unrestricted = (!isset($restricted_properties) || count($restricted_properties) == 0); + + if ( isset($this->rendered) && $unrestricted ) + return $this->rendered; + + $rendered = "BEGIN:$this->type\r\n"; + foreach( $this->properties AS $k => $v ) { + if ( method_exists($v, 'Render') ) { + if ( $unrestricted || isset($restricted_properties[$v]) ) $rendered .= $v->Render() . "\r\n"; + } + } + foreach( $this->components AS $v ) { $rendered .= $v->Render(); } + $rendered .= "END:$this->type\r\n"; + + if ( $unrestricted ) $this->rendered = $rendered; + + return $rendered; + } + + + /** + * Return an array of properties matching the specified path + * + * @return array An array of vProperty within the tree which match the path given, in the form + * [/]COMPONENT[/...]/PROPERTY in a syntax kind of similar to our poor man's XML queries. We + * also allow COMPONENT and PROPERTY to be !COMPONENT and !PROPERTY for ++fun. + * + * @note At some point post PHP4 this could be re-done with an iterator, which should be more efficient for common use cases. + */ + function GetPropertiesByPath( $path ) { + $properties = array(); + dbg_error_log( 'vComponent', "GetPropertiesByPath: Querying within '%s' for path '%s'", $this->type, $path ); + if ( !preg_match( '#(/?)(!?)([^/]+)(/?.*)$#', $path, $matches ) ) return $properties; + + $anchored = ($matches[1] == '/'); + $inverted = ($matches[2] == '!'); + $ourtest = $matches[3]; + $therest = $matches[4]; + dbg_error_log( 'vComponent', "GetPropertiesByPath: Matches: %s -- %s -- %s -- %s\n", $matches[1], $matches[2], $matches[3], $matches[4] ); + if ( $ourtest == '*' || (($ourtest == $this->type) !== $inverted) && $therest != '' ) { + if ( preg_match( '#^/(!?)([^/]+)$#', $therest, $matches ) ) { + $normmatch = ($matches[1] ==''); + $proptest = $matches[2]; + foreach( $this->properties AS $k => $v ) { + if ( $proptest == '*' || (($v->Name() == $proptest) === $normmatch ) ) { + $properties[] = $v; + } + } + } + else { + /** + * There is more to the path, so we recurse into that sub-part + */ + foreach( $this->components AS $k => $v ) { + $properties = array_merge( $properties, $v->GetPropertiesByPath($therest) ); + } + } + } + + if ( ! $anchored ) { + /** + * Our input $path was not rooted, so we recurse further + */ + foreach( $this->components AS $k => $v ) { + $properties = array_merge( $properties, $v->GetPropertiesByPath($path) ); + } + } + dbg_error_log('vComponent', "GetPropertiesByPath: Found %d within '%s' for path '%s'\n", count($properties), $this->type, $path ); + return $properties; + } + +} + -- 2.11.4.GIT