<?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;
}
}
?>