Location: PHPKode > projects > PhpiCalLib > components.php
<?php
/**
 * components.php: iCalendar component classes
 * 
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 *
 * @copyright Copyright (C) 2008 Nigel Swinson, hide@address.com 
 * @author  Nigel Swinson
 * @package PhpiCalLib
 * @version 1.0
 */

require_once 'exceptions.php';
require_once 'contentline.php';

$iPHPICALLIB_COMPONENTINDEX = 1;
define('PHPICALLIB_COMPONENT_VCALENDAR', $iPHPICALLIB_COMPONENTINDEX++);
define('PHPICALLIB_COMPONENT_IANACOMP', $iPHPICALLIB_COMPONENTINDEX++);
define('PHPICALLIB_COMPONENT_XCOMP', $iPHPICALLIB_COMPONENTINDEX++);
//http://tools.ietf.org/html/draft-ietf-calsify-rfc2445bis-08#section-3.6.1
//       3.6.1.  Event Component . . . . . . . . . . . . . . . . . . .  53
define('PHPICALLIB_COMPONENT_VEVENT', $iPHPICALLIB_COMPONENTINDEX++);
//http://tools.ietf.org/html/draft-ietf-calsify-rfc2445bis-08#section-3.6.2
//       3.6.2.  To-do Component . . . . . . . . . . . . . . . . . . .  57
define('PHPICALLIB_COMPONENT_VTODO', $iPHPICALLIB_COMPONENTINDEX++);
//http://tools.ietf.org/html/draft-ietf-calsify-rfc2445bis-08#section-3.6.3
//       3.6.3.  Journal Component . . . . . . . . . . . . . . . . . .  59
define('PHPICALLIB_COMPONENT_VJOURNAL', $iPHPICALLIB_COMPONENTINDEX++);
//http://tools.ietf.org/html/draft-ietf-calsify-rfc2445bis-08#section-3.6.4
//       3.6.4.  Free/Busy Component . . . . . . . . . . . . . . . . .  61
define('PHPICALLIB_COMPONENT_VFREEBUSY', $iPHPICALLIB_COMPONENTINDEX++);
//http://tools.ietf.org/html/draft-ietf-calsify-rfc2445bis-08#section-3.6.5
//       3.6.5.  Time Zone Component . . . . . . . . . . . . . . . . .  64
define('PHPICALLIB_COMPONENT_VTIMEZONE', $iPHPICALLIB_COMPONENTINDEX++);
//http://tools.ietf.org/html/draft-ietf-calsify-rfc2445bis-08#section-3.6.6
//       3.6.6.  Alarm Component . . . . . . . . . . . . . . . . . . .  73
define('PHPICALLIB_COMPONENT_VALARM', $iPHPICALLIB_COMPONENTINDEX++);

/**
 * An iCalendar Component
 */
class PhpiCalLib_Component {
	/**
	 * @var string The name of this component
	 */
	protected $Name = '';
	/**
	 * @var integer The type of this component
	 */
	protected $Type = 0;
	/**
	 * @var array The content lines of this component
	 */
	protected $aContentLines = array();
	/**
	 * Info about which properties are permitted for this component
	 *
	 * Empty array means all properties are permitted.  Each entry is keyed
	 * by id.  The values indicate the min and max number of instances.  For
	 * example, the spec says this for VALARM.
	 * {@link http://tools.ietf.org/html/draft-ietf-calsify-rfc2445bis-08#section-3.6.6}
	 * <pre>
	 *        audioprop  = *(
	 *                   ; 'action' and 'trigger' are both REQUIRED,
	 *                   ; but MUST NOT occur more than once
	 *                   action / trigger /
	 *
	 *                   ; 'duration' and 'repeat' are both OPTIONAL,
	 *                   ; and MUST NOT occur more than once each,
	 *                   ; but if one occurs, so MUST the other
	 *                   duration / repeat /
	 *
	 *                   ; the following is OPTIONAL,
	 *                   ; but MUST NOT occur more than once
	 *                   attach /
	 *
	 *                   ; the following is OPTIONAL,
	 *                   ; and MAY occur more than once
	 *                   x-prop / iana-prop
	 *                   )
	 * </pre>
	 * 
	 * This would be implemented as:
	 * 
	 * <code>
	 * $this->aPermittedProperties = array(
	 *				//                   ; 'action' and 'trigger' are both REQUIRED,
	 *				//                   ; but MUST NOT occur more than once
	 *				//                   action / trigger /
	 *				PHPICALLIB_PROPERTY_ACTION => array(1,1),
	 *				PHPICALLIB_PROPERTY_TRIGGER => array(1,1),
	 *				//                   ; 'duration' and 'repeat' are both OPTIONAL,
	 *				//                   ; and MUST NOT occur more than once each,
	 *				//                   ; but if one occurs, so MUST the other
	 *				//                   duration / repeat /
	 *				PHPICALLIB_PROPERTY_DURATION => array(0,1),
	 *				PHPICALLIB_PROPERTY_ => array(0,1),
	 *				//                   ; the following is OPTIONAL,
	 *				//                   ; but MUST NOT occur more than once
	 *				//                   attach /
	 *				PHPICALLIB_PROPERTY_ATTACH => array(0,1),
	 *				//                   ; the following is OPTIONAL,
	 *				//                   ; and MAY occur more than once
	 *				//                   x-prop / iana-prop
	 *				PHPICALLIB_PROPERTY_XPROP => array(0,-1),
	 *				PHPICALLIB_PROPERTY_IANAPROP => array(0,-1));
	 * </code>
	 * 
	 * Note that this mechanism isn't sufficient to ensure the component only has valid
	 * properties, for example two properties may be mutually exclusive.  Where that is the
	 * case the user should override (@link AddPropertyPermitted()}, {@link RemovePropertyPermitted} or
	 * {@link SetPropertyPermitted} to make sure that any remaining rules have been followed.
	 * 
	 * @var array Keyed by PHPICALLIB_PROPERTY_* ids, value is an array(mininstances, maxinstances)
	 */
	protected $aPermittedProperties = array();

	/**
	 * Map a property type to it's string property name.
	 *
	 * @var array Keyed by PHPICALLIB_COMPONENT_?, value is a string
	 */
	private static $TypeMap = array(
			PHPICALLIB_COMPONENT_VCALENDAR => 'VCALENDAR',
			PHPICALLIB_COMPONENT_VEVENT => 'VEVENT',
			PHPICALLIB_COMPONENT_VTODO => 'VTODO',
			PHPICALLIB_COMPONENT_VJOURNAL => 'VJOURNAL',
			PHPICALLIB_COMPONENT_VFREEBUSY => 'VFREEBUSY',
			PHPICALLIB_COMPONENT_VTIMEZONE => 'VTIMEZONE',
			PHPICALLIB_COMPONENT_VALARM => 'VALARM');
	// array_flip() on $TypeMap, built on first use.
	private static $ReverseTypeMap = null;
	
	/**
	 * Constructor
	 */
	public function __construct() {
	}

	/**
	 * Convert a component name to it's defined type
	 *
	 * @param string $Name a "name" RFC2445 token
	 * @return int one of the PHPICALLIB_COMPONENT_* properties
	 */
	static public function ToComponentType($Name) {
		// Build the type map on first access
		if (!self::$ReverseTypeMap) {
			self::$ReverseTypeMap = array_flip(self::$TypeMap);
		}
		// If we have a key for this name, return the unique type
		if (isset(self::$ReverseTypeMap[$Name])) {
			return self::$ReverseTypeMap[$Name];
		}
		// It must be an iana defined extension or x-comp that we don't really recognize.
		if (preg_match('/^X-.*$/', $Name)) { 
			return PHPICALLIB_COMPONENT_XCOMP;
		}
		return PHPICALLIB_COMPONENT_IANACOMP;
	}
	
	/**
	 * Obtain the name of the component
	 *
	 * @return returns the name of the component
	 */
	public function GetName() {
		if (empty($this->Name)) {
			throw new PhpiCalLib_Exception('No name assigned to this component yet');
		}
		return $this->Name;
	}

	/**
	 * Set the new name for the property
	 *
	 * @param string $Name
	 */
	public function SetName($Name) {
		//        iana-comp  = "BEGIN" ":" iana-token CRLF
		//                     1*contentline
		//                     "END" ":" iana-token CRLF
		//
		//        x-comp     = "BEGIN" ":" x-name CRLF
		//                     1*contentline
		//                     "END" ":" x-name CRLF
		
		//      iana-token    = 1*(ALPHA / DIGIT / "-")
		//      x-name        = "X-" [vendorid "-"] 1*(ALPHA / DIGIT / "-")
		$XNameRegex = 'X-([a-zA-Z0-9]{3}-)?([a-zA-Z0-9-]+)';
		//      vendorid      = 3*(ALPHA / DIGIT)
		// Note an invalid x-name becomes an iana-token, so we just check for valid iana-tokens
		// iana-token
		$Pattern = sprintf("/^%s$/D", PhpiCalLib_Parameter::$IanaTokenRegex);
		if (!preg_match($Pattern, $Name)) {
			throw new PhpiCalLib_ParameterException(sprintf("Invalid iana-token (%s) in component name: %s", $Pattern, $Name));
		}
		
		if (strlen($Name) > 2 && substr($Name, 0, 2) == "X-") {
			// x-name
			$this->Type = PHPICALLIB_COMPONENT_XCOMP;
			$this->Name = $Name;
		} else {
			// iana-token
			$this->Name = $Name;
			$this->Type = self::ToComponentType($Name);
		}
	}

	/**
	 * Access the component type
	 * 
	 * Note this will return PHPICALLIB_COMPONENT_IANACOMP or PHPICALLIB_COMPONENT_XCOMP for components that it doesn't recognize
	 *
	 * @return PHPICALLIB_COMPONENTS_*
	 */
	public function GetType() {
		return $this->Type;
	}
	
	/**
	 * Set the type of the component
	 *
	 * @param integer $Type
	 */
	public function SetType($Type) {
		// Check they aren't trying to set this to an x-comp or an iana-comp
		switch ($Type) {
			case PHPICALLIB_COMPONENT_IANACOMP:
			case PHPICALLIB_COMPONENT_XCOMP:
				throw new PhpiCalLib_Exception('Can\'t set component type to iana-comp or x-comp.  Use SetName instead');
		}
		// Pull out the name, note if we ever add a PHPICALLIB_COMPONENT_? and forget to amend $TypeMap, then
		// this can start to fail
		$this->Name = self::$TypeMap[$Type];
		// Store the type.
		$this->Type = $Type;
	}
	
	/**
	 * Get all the properties of the given type/name
	 *
	 * @param integer $Type The type of the property to return.  Null means return all properties
	 * @param string $Name The name of the property, necessary if $Type is PHPICALLIB_PROPERTY_XPROP or 
	 * 					PHPICALLIB_PROPERTY_IANAPROP.  If null, all properties of these types will be returned.
	 * @return array of matching PhpiCalLib_ContentLine objects, empty array of no content lines matched
	 */
	public function GetProperties($Type = null, $Name = null) {
		
		$aResults = array();
		foreach ($this->aContentLines as $ContentLine) {
			// If no type filter, add to the result set and continue
			if (empty($Type)) {
				$aResults[] = $ContentLine;
				continue;
			}

			// Filter on the type.
			if ($ContentLine->GetType() != $Type) continue;
			
			// Possibly filter on the name too
			switch ($Type) {
				case PHPICALLIB_PROPERTY_XPROP:
				case PHPICALLIB_PROPERTY_IANAPROP:
					// Should we filter by name too?
					if (empty($Name)) {
						$aResults[] = $ContentLine;
						break;
					} 
					// Must search by name
					if ($Name == $ContentLine->GetName()) {
						$aResults[] = $ContentLine;
						break;
					}
					break;
				default:
					$aResults[] = $ContentLine;
			}
		}
		return $aResults;
	}
	
	/**
	 * Get a single property of the given type/name
	 *
	 * @param integer $Type The type of the property to return.  Null means return all properties
	 * @param string $Name The name of the property, necessary if $Type is PHPICALLIB_PROPERTY_XPROP or 
	 * 					PHPICALLIB_PROPERTY_IANAPROP.  If null, all properties of these types will be returned.
	 * @return A single PhpiCalLib_ContentLine derived object, or null if no propety of the given type was found.
	 */
	public function GetProperty($Type, $Name = null) {
		foreach ($this->aContentLines as $ContentLine) {
			// Filter on the type.
			if ($ContentLine->GetType() != $Type) continue;
			
			// Filter on the type
			switch ($Type) {
				case PHPICALLIB_PROPERTY_XPROP:
				case PHPICALLIB_PROPERTY_IANAPROP:
					// Should we filter by name too?
					if (empty($Name)) {
						return $ContentLine;
					} 
					// Must search by name
					if ($Name == $ContentLine->GetName()) {
						return $ContentLine;
					}
					break;
				default:
					return $ContentLine;
			}
		}
		return null;
	}

	/**
	 * Set the content lines for the component, replacing any existing properties
	 *
	 * @param PhpiCalLib_ContentLines $aContentLines
	 */
	public function SetProperties($aContentLines) {
		$this->aContentLines = array();
		// Technically speaking, the component must have at least one property, but we
		// won't test for that here.
		foreach ($aContentLines as $ContentLine) {
			// Call AddProperty to make sure each is validated before addition
			$this->AddProperty($ContentLine);
		}
		// Check that we have all the properties that were REQUIRED
		if (!empty($this->aPermittedProperties)) {
			foreach ($this->aPermittedProperties as $Type => $aTypeInfo) {
				// If there's no lower limit, or a lower limit of 0, then no min threshold to check.
				if ($aTypeInfo[0] <= 0) continue;
				// Get all the properties of the give type
				$aProperties = $this->GetProperties($Type);
				// Check we have at least enough instances of the property
				if (count($aProperties) < $aTypeInfo[0]) {
					throw new PhpiCalLib_ParameterException(
								sprintf("The %s properties must occur %d times in the %s components and it only occurs %d times",
									self::$TypeMap[$Type],
									$aTypeInfo[0],
									$this->Name,
									count($aProperties)));
				} 
			}
		}
	}
	
	/**
	 * Sets a property, replacing any existing values of the same type
	 *
	 * @param PhpiCalLib_ContentLine $ContentLine The property to set
	 */
	public function SetProperty($ContentLine) {
		// Check we are allowed to set this property
		$this->SetPropertyPermitted($ContentLine);
		// Remove it's current values, calling the private version to avoid
		// failing if this property has to have at least one value.
		$this->_RemoveProperty($ContentLine->GetType(), $ContentLine->GetName());
		// Insert the new property
		$this->aContentLines[] = $ContentLine;
	}

	/**
	 * Add the property to the component
	 *
	* @param PhpiCalLib_ContentLine $ContentLine The property to set
	 */
	public function AddProperty($ContentLine) {
		// Check we are allowed to add this property
		$this->AddPropertyPermitted($ContentLine);
		// Insert the new property
		$this->aContentLines[] = $ContentLine;
	}
	
	/**
	 * Delete all the properties of the given type
	 *
	 * @param integer $Type The PHPICALLIB_PROPERTY_? type of the property
	 * @param string $Name The name of the property, used when $Type = PHPICALLIB_PROPERTY_XPROP or 
	 * 					PHPICALLIB_PROPERTY_IANAPROP.  If empty all properties of either type will be deleted
	 */
	public function RemoveProperty($Type, $Name = null) {
		// Check we are allowed to remove this property
		$this->RemovePropertyPermitted($Type, $Name);
		$this->_RemoveProperty($Type,$Name);
	}
	
	/**
	 * Internal version of {@link RemoveProperty()} that doesn't validate if removal of the property is permitted
	 *
	 * @param integer $Type
	 * @param string $Name
	 */
	private function _RemoveProperty($Type, $Name) { 
		// Iterate from the end of the array towards the front, looking for entries to delete
		for ($iIndex = count($this->aContentLines) - 1; $iIndex >= 0; $iIndex--) {
			// If the type doesn't match, we can skip forward quickly
			if ($this->aContentLines[$iIndex]->GetType() != $Type) continue;
			
			// Possibly test the name too
			switch ($Type) {
				case PHPICALLIB_PROPERTY_XPROP:
				case PHPICALLIB_PROPERTY_IANAPROP:
					// Must search by name
					// Iterate from the end of the array towards the front
					if (empty($Name) || ($Name == $this->aContentLines[$iIndex]->GetName())) {
						array_splice($this->aContentLines, $iIndex, 1);
					}
					break;
				default:
					array_splice($this->aContentLines, $iIndex, 1);
			}
		}
	}

	/**
	 * Create a new Component object
	 *
	 * A component would override this class, providing a new version of this function
	 * in order to choose the right kind of class to create.
	 * 
	 * @param string $Name The name of the component
	 * @return PhpiCalLib_Component derived class
	 */
	protected function CreateComponent($Name) {
		$Result = new PhpiCalLib_Component();
		$Result->SetName($Name);
		return $Result;
	}
	
	/**
	 * Create a component object from it's name and it's properties, returning the new object
	 *
	 * @param string $Name The name of the component to create
	 * @param string $aProperties An array of the properties of this component
	 * @return A PhpiCalLib_Component derived class, according to what CreateComponent returned
	 */
	public function Create($Name, $aProperties) {
		// Create the right kind of component
		$Component = $this->CreateComponent($Name);
		$Component->SetProperties($aProperties);
		return $Component;
	}

	/**
	 * Determine if the given property can be changed
	 *
	 * @param PhpiCalLib_ContentLine $ContentLine
	 */
	protected function SetPropertyPermitted($ContentLine) {
		// If there's no permitted property info, then we can't validate, so assume it's ok
		if (empty($this->aPermittedProperties)) return;
		
		// Get the property info
		$TypeId = $ContentLine->GetType();
		if (empty($this->aPermittedProperties[$TypeId])) {
			throw new PhpiCalLib_ParameterException(
							sprintf("The %s property is not permitted within the %s component", 
								$ContentLine->GetName(), 
								$this->GetName()));
		}
	}
	
	/**
	 * Determine if the given property is permitted for addition
	 *
	 * @param PhpiCalLib_ContentLine $ContentLine
	 */
	protected function AddPropertyPermitted($ContentLine) {
		// If there's no permitted property info, then we can't validate, so assume it's ok
		if (empty($this->aPermittedProperties)) return;
		
		// Get the property info
		$TypeId = $ContentLine->GetType();
		if (empty($this->aPermittedProperties[$TypeId])) {
			throw new PhpiCalLib_ParameterException(
							sprintf("The %s property is not permitted within the %s component", 
								$ContentLine->GetName(), 
								$this->GetName()));
		}
		
		// Check the max instances is respected
		$aPropertyInfo = $this->aPermittedProperties[$TypeId];
		if ($aPropertyInfo[1] == 1) {
			// If we are only allowed one of these properties, and we already have one, then fail
			$ContentLine = $this->GetProperty($TypeId);
			if (isset($ContentLine)) {
				throw new PhpiCalLib_ParameterException(
						sprintf("The %s property is only permitted once within the %s component and already has value %s", 
							$ContentLine->GetName(), 
							$this->GetName(), 
							$ContentLine->GetEncodedValue()));
			}
		} else if ($aPropertyInfo[1] > 1) {
			// If there's a max limit, then check we aren't at our max limit already
			$aProperties = $this->GetProperties($TypeId);
			if (count($aProperties) >= $aPropertyInfo[1]) {
				throw new PhpiCalLib_ParameterException(
						sprintf("The %s property is only permitted %d times within the %s component and already has %d values", 
							$ContentLine->GetName(), 
							$aPropertyInfo[1],
							$this->GetName(), 
							count($aProperties)));
			}
		}
	}
	
	/**
	 * Determine if we are allowed to remove the given property
	 *
	 * @param integer $Type
	 * @param string $Name
	 */
	protected function RemovePropertyPermitted($Type, $Name) {
		// If there's no permitted property info, then we can't validate, so assume it's ok
		if (empty($this->aPermittedProperties)) return;
		
		// Get the property info
		if (empty($this->aPermittedProperties[$Type])) {
			// They'd never have been allowed to add the property, so they are welcome to remove it.
			return;
		}
		$aPropertyInfo = $this->aPermittedProperties[$Type];
		// If removing this property would drop us below our lower limit, then fail
		if ($aPropertyInfo[0] == 1) {
			throw new PhpiCalLib_ParameterException(
						sprintf("The %s (%s) property is REQUIRED for the %s component.  Call SetProperty to change it's value.", 
							self::$TypeMap[$Type],
							$Name, 
							$this->Name));
		} else if ($aPropertyInfo[0] > 0) {
			// There's a min number of instances, check we aren't at that lower limit already
			$aProperties = $this->GetProperties($Type);
			if (count($aProperties) <= $aPropertyInfo[0]) {
				throw new PhpiCalLib_ParameterException(
							sprintf("The %s (%s) property is REQUIRED at least %d times for the %s component.  Call SetProperty/SetProperties/AddProperty to change/add values.", 
								self::$TypeMap[$Type],
								$Name, 
								$aPropertyInfo[0],
								$this->Name));
			}
		}
	}
	
	/**
	 * Dump the parameter to an array of unfolded content lines
	 * 
	 * @return An string array representation in the default code page
	 */
	public function ToString() {
		$aUnfoldedContentLines = array();
		
		//http://tools.ietf.org/html/draft-ietf-calsify-rfc2445bis-08#section-3.6
		//
		//        iana-comp  = "BEGIN" ":" iana-token CRLF
		//                     1*contentline
		//                     "END" ":" iana-token CRLF
		
		// There must be at least one contentline
		if (empty($this->aContentLines)) return '';
		
		// Start with the BEGIN line
		$Begin = new PhpiCalLib_ContentLine();
		$Begin->SetName("BEGIN");
		$Begin->SetEncodedValue($this->Name);
		$aUnfoldedContentLines[] = $Begin->ToString();
		
		// Add the contentlines
		foreach ($this->aContentLines as $ContentLine) {
			$aUnfoldedContentLines[] = $ContentLine->ToString();
		}
		
		// Finally the END line
		$End = new PhpiCalLib_ContentLine();
		$End->SetName("END");
		$End->SetEncodedValue($this->Name);
		$aUnfoldedContentLines[] = $End->ToString();
		return implode('',$aUnfoldedContentLines);
	}
}

Return current item: PhpiCalLib