Location: PHPKode > projects > PhpiCalLib > icalendar.php
<?php
/**
 * iCalendar.php: iCalendar parsing class
 * 
 * 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 'propertyfactory.php';
require_once 'exceptions.php';
require_once 'components.php';
require_once 'event.php';

/**
 * The top level class of PhpiCalLib, representing the contents of an iCalendar file
 *
 * @version 1.0
 */
class PhpiCalLib_iCalendar extends PhpiCalLib_Component {
	/**
	 * The icalbody prodid property
	 *
	 * @var PhpiCalLib_Properties_ProdIdProperty
	 */
	private $ProdId = null;
	/**
	 * The icalbody verison property
	 *
	 * @var PhpiCalLib_Properties_VersionProperty
	 */
	private $Version = null;
	/**
	 * The icalbody calscale property
	 *
	 * @var PhpiCalLib_Properties_CalscaleProperty
	 */
	private $Calscale = null;
	/**
	 * The icalbody method property
	 *
	 * @var PhpiCalLib_Properties_MethodProperty
	 */
	private $Method = null;
	
	/**
	 * The collection of components
	 *
	 * @var array of PhpiCalLib_ContentLine derived classes
	 */
	private $aComponents = array();
	
	/**
	 * Easy way to match for the start of the VCalendar
	 *
	 * @var PhpiCalLib_Properties_BeginProperty
	 */ 
	private $BeginVCalendar = null;
	/**
	 * Easy way to match for the end of the VCalendar
	 *
	 * @var PhpiCalLib_Properties_EndProperty
	 */
	private $EndVCalendar = null;
	
	/**
	 * The property factory
	 *
	 * @var PhpiCalLib_PropertyFactory
	 */
	private $PropertyFactory = null;
	/**
	 * Max line length.
	 * <pre>
	 *   Lines of text SHOULD NOT be longer than 75 octets, excluding the line
	 *   break.
	 * </pre>
	 * @link http://tools.ietf.org/html/draft-ietf-calsify-rfc2445bis-08#section-3.1	
	 * @var integer
	 */
	private $MaxFoldedLineLength = 75;

	/**
	 * Consturctor
	 */
	public function __construct() {
		$this->Name = "VCALENDAR";
		$this->PropertyFactory = new PhpiCalLib_PropertyFactory();
		$this->BeginVCalendar = $this->PropertyFactory->Create("BEGIN:VCALENDAR", 1);
		$this->EndVCalendar = $this->PropertyFactory->Create("END:VCALENDAR", 2);
		// I've seed Prodid values like:
		// PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
		// PRODID:-//Microsoft Corporation//Outlook 11.0 MIMEDIR//EN
		$this->ProdId = $this->PropertyFactory->Create("PRODID:-//PhpiCalLib//PhpiCalLib 1.0//EN",3);
		$this->Version = $this->PropertyFactory->Create("VERSION:2.0",4);
	}

	/**
	 * Parse an iCalendar file from a file on disk
	 *
	 * @param string $FileName
	 */
	public function FromFile($FileName) {
		// The whole iCal string should be in utf8.  Decode before analysis
		$iCalObjectString = utf8_decode(file_get_contents($FileName));
		
		// Open from a string
		$this->FromString($iCalObjectString);
	}

	/**
	 * Parse an iCalendar file from a string
	 *
	 * @param string $Text
	 */
	public function FromString($iCalObjectString) {
		// http://tools.ietf.org/html/draft-ietf-calsify-rfc2445bis-08#section-3.4
		//        icalstream = 1*icalobject
		//
		//        icalobject = "BEGIN" ":" "VCALENDAR" CRLF
		//                     icalbody
		//                     "END" ":" "VCALENDAR" CRLF
		$aContentLines = $this->PropertyFactory->ToContentLines($iCalObjectString);
		
		if (count($aContentLines) < 2) {
			throw new PhpiCalLib_ParseException(sprintf("iCalObjectString contains only %d content lines.  At a minimum it should include Begin/End VCalendar lines", count($aContentLines)));
		}
		
		// Check the first line is a BEGIN:VCALENDAR
		$aFirstLine = array_shift($aContentLines);
		if ($aFirstLine != $this->BeginVCalendar) {
			throw new PhpiCalLib_ParseException(sprintf("iCalObjectString must start with BEGIN:VCALENDAR"));
		}
		
		$aLastLine = array_pop($aContentLines);
		if ($aLastLine != $this->EndVCalendar) {
			throw new PhpiCalLib_ParseException(sprintf("iCalObjectString must end with END:VCALENDAR"));
		}
		
		return $this->ParseiCalBody($aContentLines);
	}

	/**
	 * Overridden to create the right kind of component
	 *
	 * @param string $Name The name of the component
	 * @return PhpiCalLib_Component derived class
	 */
	protected function CreateComponent($Name) {
		// Get the type of the property
		$Type = self::ToComponentType($Name);
		
		switch ($Type) {
			case PHPICALLIB_COMPONENT_VCALENDAR:
				break;
			case PHPICALLIB_COMPONENT_VEVENT:
				return new PhpiCalLib_Event();
			case PHPICALLIB_COMPONENT_VTODO:
			case PHPICALLIB_COMPONENT_VJOURNAL:
			case PHPICALLIB_COMPONENT_VFREEBUSY:
			case PHPICALLIB_COMPONENT_VTIMEZONE:
			case PHPICALLIB_COMPONENT_VALARM:
				break;
			case PHPICALLIB_COMPONENT_IANACOMP:
			case PHPICALLIB_COMPONENT_XCOMP:
				// Very deliberatly the default
				$Result = new PhpiCalLib_Component();
				$Result->SetName($Name);
				return $Result;
		}
				
		// By default, return a generic Component
		$Result = new PhpiCalLib_Component();
		// We can call SetType(), as we have filtered off IanaComp and XComp
		$Result->SetType($Type);
		return $Result;
	}
	
	/**
	 * Parse an iCalendar object from a set of contentlines that represent an icalbody
	 *
	 * @param array(contentline) $aContentLines An array of content lines that represent the iCalendar
	 */
	public function ParseiCalBody($aContentLines) {
		//http://tools.ietf.org/html/draft-ietf-calsify-rfc2445bis-08#section-3.6
		//        icalbody   = calprops component
		//
		//        calprops   = *(
		//                   ; the following are REQUIRED,
		//                   ; but MUST NOT occur more than once
		//                   prodid / version /
		//
		//                   ; the following are OPTIONAL,
		//                   ; but MUST NOT occur more than once
		//                   calscale / method /
		//
		//                   ; the following are OPTIONAL,
		//                   ; and MAY occur more than once
		//                   x-prop / iana-prop
		//                   )
		//
		//        component  = 1*(eventc / todoc / journalc / freebusyc /
		//                     timezonec / iana-comp / x-comp)
		//
		//        iana-comp  = "BEGIN" ":" iana-token CRLF <etc>
		//        x-comp     = "BEGIN" ":" x-name CRLF <etc>
		//        eventc     = "BEGIN" ":" "VEVENT" CRLF <etc>
		//        todoc      = "BEGIN" ":" "VTODO" CRLF <etc>
		//        journalc   = "BEGIN" ":" "VJOURNAL" CRLF <etc>
		//        freebusyc  = "BEGIN" ":" "VFREEBUSY" CRLF <etc>
		//        timezonec  = "BEGIN" ":" "VTIMEZONE" CRLF <etc>
		
		$this->ProdId = null;
		$this->Version = null;
		
		// Cycle through the content lines pulling off our attributes until we reach
		// the component token which starts with a BEGIN property
		while (!empty($aContentLines)) {
			$aContentLine = $aContentLines[0];
			$Type = $aContentLine->GetType();
			if ($Type == PHPICALLIB_PROPERTY_BEGIN)
				break;
			array_shift($aContentLines);
			
			switch ($Type) {
				case PHPICALLIB_PROPERTY_PRODID:
					if ($this->ProdId) {
						throw new PhpiCalLib_ParseException("Only one prodid property permitted per icalbody");
					}
					$this->ProdId = $aContentLine;
					break;
				case PHPICALLIB_PROPERTY_VERSION:
					if ($this->Version) {
						throw new PhpiCalLib_ParseException("Only one version property permitted per icalbody");
					}
					$this->Version = $aContentLine;
					break;
				case PHPICALLIB_PROPERTY_CALSCALE:
					if ($this->Calscale) {
						throw new PhpiCalLib_ParseException("Only one calscale property permitted per icalbody");
					}
					$this->Calscale = $aContentLine;
					break;
				case PHPICALLIB_PROPERTY_METHOD:
					if ($this->Method) {
						throw new PhpiCalLib_ParseException("Only one version property permitted per icalbody");
					}
					$this->Method = $aContentLine;
					break;
				case PHPICALLIB_PROPERTY_IANAPROP:
				case PHPICALLIB_PROPERTY_XPROP:
					$this->aContentLines[] = $aContentLine;
					break;
				default:
					throw new PhpiCalLib_ParseException(sprintf("The %s property is not supported in an icalbody", $aContentLine->GetName()));
			}
		}
		
		// Validate we got the required properties
		if (empty($this->ProdId))
			throw new PhpiCalLib_ParseException("The icalbody object must have a prodid property");
		if (empty($this->Version))
			throw new PhpiCalLib_ParseException("The icalbody object must have a version property");
			
		//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
		// All components follow the above form
		while (!empty($aContentLines)) {
			$aContentLine = array_shift($aContentLines);
			if ($aContentLine->GetType() != PHPICALLIB_PROPERTY_BEGIN) {
				throw new PhpiCalLib_ParseException(sprintf("Unexpected %s property.  Was looking for components which start with BEGIN properties", $aContentLines->Name));
			}
			$aBeginProperty = $aContentLine;
			$ComponentName = $aBeginProperty->GetEncodedValue();
			
			// Scan forward until we find the end of the component
			$aComponentContentLines = array();
			$aEndProperty = null;
			while (!empty($aContentLines)) {
				$aContentLine = $aContentLines[0];
				// If it's an end property that matches the begin property, then stop looking
				if ($aContentLine->GetType() == PHPICALLIB_PROPERTY_END) {
					if ($aContentLine->GetEncodedValue() == $ComponentName) {
						$aEndProperty = $aContentLine;
						array_shift($aContentLines);
						break;
					}
				}
				$aComponentContentLines[] = array_shift($aContentLines);
			}
			
			if (!isset($aEndProperty)) {
				throw new PhpiCalLib_ParseException(sprintf("Mismatched %s component.  Was missing end tag.", $aBeginProperty->Name));
			}
			
			// Create the component
			$Component = $this->Create($ComponentName, $aComponentContentLines);
			// Make sure we have a bucket for components of this type
			if (empty($this->aComponents[$ComponentName])) {
				$this->aComponents[$ComponentName] = array();
			}
			// Add it to the bucket
			$this->aComponents[$ComponentName][] = $Component;
		}
		
		// We should never have any content lines left.
		if (count($aContentLines)) {
			throw new PhpiCalLib_ParseException("Unexpected trailing content lines");
		}
	}

	/**
	 * Parse an iCalendar file from a file on disk
	 *
	 * @param string $FileName
	 */
	public function ToFile($FileName) {
		$iCalObjectString = $this->ToString();
		
		// The whole iCal string should be in utf8.  Decode before analysis
		$hFile = fopen($FileName, "w");
		fwrite($hFile, utf8_encode($iCalObjectString));
		fclose($hFile);
	}
	
	/**
	 * Dump the object to a string
	 * 
	 * @return A string representation in the default code page
	 */
	public function ToString() {
		// Convert to a set of unfolded content lines
		$aUnfoldedContentLines = array();
		$aUnfoldedContentLines[] = $this->BeginVCalendar->ToString();
		$aUnfoldedContentLines[] = $this->ProdId->ToString();
		$aUnfoldedContentLines[] = $this->Version->ToString();
		if ($this->Calscale) $aUnfoldedContentLines[] = $this->Calscale->ToString();
		if ($this->Method) $aUnfoldedContentLines[] = $this->Method->ToString();
		// All the unrecognized content lines
		foreach ($this->aContentLines as $ContentLine) {
			$aUnfoldedContentLines[] = $ContentLine->ToString();
		}
		// All the components
		foreach (array_values($this->aComponents) as $aComponents) {
			foreach ($aComponents as $Component) {
				$aComponentContentLines = explode("\r\n",$Component->ToString());
				foreach ($aComponentContentLines as $ComponentLine) {
					if (empty($ComponentLine)) continue; 
					$aUnfoldedContentLines[] = $ComponentLine."\r\n";
				} 
			}
		}
		// Finally the end string
		$aUnfoldedContentLines[] = $this->EndVCalendar->ToString();
		
		// Fold and return
		$aResults = array();
		foreach ($aUnfoldedContentLines as $UnfoldedContentLine) {
			//      ; When parsing a content line, folded lines MUST first
			//      ; be unfolded according to the unfolding procedure
			//      ; described above. When generating a content line, lines
			//      ; longer than 75 octets SHOULD be folded according to
			//      ; the folding procedure described above.
			// MaxFoldedLineLength doesn't need to count the \r\n.  Keep an extra byte free
			// for any leading whitespace we use on the previous line.
			if (strlen($UnfoldedContentLine) > $this->MaxFoldedLineLength + 2) {
				do {
					$aResults[] = substr($UnfoldedContentLine, 0, $this->MaxFoldedLineLength - 1);
					$UnfoldedContentLine = substr($UnfoldedContentLine, $this->MaxFoldedLineLength - 1);
					// Add the line wrapper
					$aResults[] = "\r\n\t";
				} while (strlen($UnfoldedContentLine) > $this->MaxFoldedLineLength + 2 - 1);
			}
			$aResults[] = $UnfoldedContentLine;
		}
		
		return implode('',$aResults);
	}
	
	/**
	 * Add a component to the collection in this calendar
	 *
	 * @param PhpiCalLib_Component $Component
	 */
	public function AddComponent($Component) {
		$ComponentName = $Component->GetName();
		// Make sure we have a bucket for components of this type
		if (empty($this->aComponents[$ComponentName])) {
			$this->aComponents[$ComponentName] = array();
		}
		// Add it to the bucket
		$this->aComponents[$ComponentName][] = $Component;
	}
	
	/**
	 * Access all the components of the given type
	 *
	 * @param integer $Type One of the PHPICALLIB_COMPONENT_* types, or null to return all components.
	 * @param string $Name The name of the component to match, or null to match just by type
	 * @return array A collection of PHPICALLIB_COMPONENT objects
	 */
	public function GetComponents($Type = null, $Name = null) {
		// If they have specified a type, then we only need to look in one collection
		if (!empty($Type)) {
			// If the collection is unset, return an empty array
			if (empty($this->aComponents[$Type])) return array();
			// If they didn't specify a name, then we can return the entire collection 
			if (empty($Name)) return $this->aComponents[$Type];
		}
		
		// We must compile the results longhand.
		$aResult = array();
		foreach ($this->aComponents as $CollectionType => $aComponents) {
			// Test the collection type
			if (!empty($Type) && ($CollectionType != $Type)) continue;
			// Test the items in the collection
			foreach ($aComponents as $Component) {
				// Possibly test the name
				if (empty($Name)) {
					$aResult[] = $Component;
					continue;
				}
				// Possibly test the name too.
				switch ($CollectionType) {
					case PHPICALLIB_COMPONENT_IANACOMP:
					case PHPICALLIB_COMPONENT_XCOMP:
						// We must test the name
						if ($Component->GetName() == $Name) {
							$aResult[] = $Components;
							break;
						}
						break;
					default:
						// No name test required, as there's a 1-1 mapping from type to name for this type
						$aResult[] = $Components;
				}
			}
		}
		return $aResult;
	}
}

?>
Return current item: PhpiCalLib