<?php
/* vim: set expandtab tabstop=4 shiftwidth=4: */
// +----------------------------------------------------------------------+
// | PHP version 4 |
// +----------------------------------------------------------------------+
// | Copyright (c) 1997-2003 The PHP Group |
// +----------------------------------------------------------------------+
// | This source file is subject to version 2.0 of the PHP license, |
// | that is bundled with this package in the file LICENSE, and is |
// | available through the world-wide-web at |
// | http://www.php.net/license/2_02.txt. |
// | If you did not receive a copy of the PHP license and are unable to |
// | obtain it through the world-wide-web, please send a note to |
// | hide@address.com so we can mail you a copy immediately. |
// +----------------------------------------------------------------------+
// | Authors: Andreas Haberstroh <hide@address.com> |
// +----------------------------------------------------------------------+
//
// $Id$
class imcComponent {
/**
* List of property objects for this component.
* @var array
*/
var $properties;
/**
* Name of this component.
* @var string
*/
var $componentName;
/**
* List of subcomponent objects for this component
* @var array
*/
var $components;
/**
* UID to Sequence index for components.
* @var array
*/
var $uid_hash;
/**
* Default constructor.
*/
function imcComponent() {
$this->properties = NULL;
$this->components = NULL;
$this->uid_hash = NULL;
}
/**
* Recursively writes a component's properties and subcomponents to disk.
*
* @param filehandle File to write the components to.
*/
function writeFile($fh) {
if( $this->getComponentName() ) {
// every component has a beginning
fwrite( $fh, 'BEGIN:'.$this->getComponentName()."\r\n" );
// walk thru all the properties and write them out.
if( is_array($this->properties) ) {
// walk the properties. $key == property name
foreach( $this->properties as $key=>$propertyArray ) {
// every property MAY have multiple sequences
foreach( $propertyArray as $propertyObject ) {
// we build an array for easy imploding
$property = array($key);
// get the encoded value of the property and see if it is REAL
$propertyValue = rtrim($propertyObject->getEncodedValue());
if( !strlen($propertyValue) )
continue;
// build the TYPE= portion
$type_array = $propertyObject->getParamType();
if( is_array($type_array) ) {
$property[] = "TYPE=".implode(",", $type_array);
}
/// build the VALUE= portion
$value_array = $propertyObject->getParamValue();
if( is_array($value_array) ) {
foreach( $value_array as $value ) {
$property[] = "VALUE=".$value;
}
}
// build the ENCODING portion
$encoding = $propertyObject->getParamEncoding();
if( strlen($encoding) ) {
$property[] = "ENCODING=".$encoding;
}
/// now, build the WHOLE line of data
$line = implode(";", $property).":$propertyValue";
/*
The encoded values are based on the actual VALUE not the whole line.
The entire line may not be longer than 75.
So, in this case, we implement line folding.
*/
if( strlen($line) > 75 ) {
$len = 72;
if( $encoding == 'QUOTED-PRINTABLE' ) {
$len--;
$replace = "=\r\n";
} else {
$replace = "\r\n";
}
// strip out anything that got inserted by the encoders.
$line = str_replace($replace, "", $line);
$line = rtrim(chunk_split($line, $len, "$replace\t"));
}
// lastly, write the line of data
fwrite($fh, "$line\r\n" );
}
}
}
}
// do we have subcomponents?
if( is_array($this->components) ) {
// walk the subcomponents and call their writers as well
foreach( $this->components as $key=>$componentArray ) {
foreach( $componentArray as $object ) {
// in case SOME how this is what we think it is.
if( is_a($object,'imcComponent') )
$object->writeFile($fh);
}
}
}
if( $this->getComponentName() ) {
// laslty, write the END: marker
fwrite( $fh, 'END:'.$this->getComponentName()."\r\n" );
}
}
/**
* Builds a tree of components from a IMC based file.
*
* @param filehandle File handle which stores a imc based file
*/
function readFile($fh) {
// in case we've set something BEFORE reading a file
$this->properties = NULL;
$this->components = NULL;
$this->uid_hash = NULL;
$folded = '';
while( !feof($fh) ) {
$raw_line = fgets($fh);
$line = ltrim(str_replace( "\r\n", "", $raw_line));
// Nothing to be read
if ( !strlen($line) )
continue;
// find the first instance of ':' on the line. The part
// to the left of the colon is the type and parameters;
// the part to the right of the colon is the value data.
$pos = strpos($line, ':');
// This may be a continuation of a previous parameter!
if( $pos === false ) {
$folded .= $line;
continue;
} else {
// we're dealing with a folding line
$this->getProperty($typedef, $propertyObject, $lastPropertyIndex );
// $right should be undamaged, so we can concat the folded data,
// and re-decode the value
$propertyObject->setEncodedValue($right.str_replace("\t", "", $folded));
$this->setProperty($typedef, $propertyObject, $lastPropertyIndex );
// dumped folded scratch space for the next time we have folding.
$folded = '';
}
// get the left and right portions
$left = trim(substr($line, 0, $pos));
$right = trim(substr($line, $pos+1, strlen($line)));
switch( $left ) {
case 'BEGIN':
$this->_componentFactory($right, $componentObject);
$componentObject->readFile($fh);
$this->setComponent($right, $componentObject);
break;
case 'END':
return;
default:
$typedef = $this->_getTypeDef($left);
$params = $this->_getParams($left);
$this->_propertyFactory($typedef, $propertyObject);
$propertyObject->setParamType($params['TYPE']);
$propertyObject->setParamValue($params['VALUE']);
$propertyObject->setParamEncoding($params['ENCODING'][0]);
$propertyObject->setEncodedValue($right);
$lastPropertyIndex = $this->setProperty($typedef, $propertyObject);
}
}
}
// {{{ setComponentName()
/**
* Sets the name of this component.
*
* @param string Component name
* @see getComponentName
*/
function setComponentName($name) {
$this->componentName = $name;
}
// {{{ getComponentName()
/**
* Gets the name of this component.
*
* @return string Component name
* @see getComponentName
*/
function getComponentName() {
return $this->componentName;
}
// {{{ removeComponent()
/**
* Removes a component from the list.
*
* @param string Name of the component type to delete
* @param integer Sequence index of the component to delete
*/
function removeComponent($name, $seq) {
// get a copy of this element real quick
$this->getComponent($name, &$object, $seq);
$left = array_slice($this->components[$name], 0, $seq);
$right = array_slice($this->components[$name], $seq+1);
$this->components[$name] = array_merge($left,$right);
}
// {{{ setComponent()
/**
* Sets a component object into the list.
*
* @param string Name of the component type to set
* @param object Component object to add
* @param integer Sequence to add this object at. -1 means add
* @return integer Sequence number where this object was added
*/
function setComponent($name, $object, $seq = - 1) {
if( $seq == -1 )
{
$this->components[$name][] = $object;
$seq = Count($this->components[$name]) - 1;
}
else
$this->components[$name][$seq] = $object;
$this->uid_hash[$object->getUID()] = $seq;
return $seq;
}
// {{{ getComponent()
/**
* Gets a component object from the list.
*
* @param string Name of the component type to get
* @param object Copy of an instance of the component
* @param integer Sequence from which to retrieve this object from. -1 means add
* @return integer Sequence number where this object was added
*/
function getComponent($name, &$object, $seq = -1 ) {
if( is_array($this->components[$name]) )
{
if( $seq == -1 )
{
$count = Count($this->components[$name]);
$seq = $count - 1;
}
}
$object = $this->components[$name][$seq];
if( !is_object($object) )
{
$this->_componentFactory($name, $object);
$this->setComponent($name, $object, $seq);
}
}
// {{{ getComponentCount
/**
* Gets the number of components of a certain type
*
* @param string Name of the component to check
*/
function getComponentCount($name) {
return Count($this->components[$name]);
}
// {{{ getComponentUIDs
/**
* Gets the array of UID's for each of the components.
*
* @return array UID hash list
*/
function getComponentUIDs() {
return $this->uid_hash;
}
// {{{ removeComponentByUID
/**
* Removes a component by UID reference
*
* @param string Name of the component
* @param string UID of the component to remove
*/
function removeComponentByUID($name, $uid ) {
$this->removeComponent($name, $this->uid_hash[$uid] );
}
// {{{ getComponentByUID
/**
* Gets a component by UID reference
*
* @param string Name of the component
* @param object Component Object to return
* @param string UID of the component to retrieve
*/
function getComponentByUID($name, &$object, $uid ) {
return $this->getComponent($name, &$object, $this->uid_hash[$uid] );
}
// {{{ setComponentByUID
/**
* Sets a component by UID reference
*
* @param string Name of the component
* @param object Component Object to set
* @param string UID of the component to set
*/
function setComponentByUID($name, $object, $uid ) {
return $this->setComponent($name, $object, $this->uid_hash[$uid] );
}
// {{{ setProperty()
/**
* Sets a property object into the list.
*
* @param string Name of the property type to set
* @param object Property object to add
* @param integer Sequence to add this object at. -1 means add
* @return integer Sequence number where this object was added
*/
function setProperty($name, $object, $seq = - 1) {
if( $seq == -1 )
{
$this->properties[$name][] = $object;
$seq = Count($this->properties[$name]) - 1;
}
else
$this->properties[$name][$seq] = $object;
return $seq;
}
// {{{ getProperty()
/**
* Gets a property object from the list.
*
* @param string Name of the property type to get
* @param object Copy of an instance of the property
* @param integer Sequence from which to retrieve this object from. -1 means add
* @return integer Sequence number where this object was added
*/
function getProperty($name, &$object, $seq = -1 ) {
if( is_array($this->properties[$name]) )
{
if( $seq == -1 )
{
$count = Count($this->properties[$name]);
$seq = $count - 1;
}
}
$object = $this->properties[$name][$seq];
if( !is_object($object) )
{
$this->_propertyFactory($name, $object);
$this->setProperty($name, $object, $seq);
}
}
// {{{ getPropertyCount
/**
* Gets the number of propertys of a certain type
*
* @param string Name of the property to check
*/
function getPropertyCount($name) {
return Count($this->properties[$name]);
}
/**
* Finds a property sequence number by a TYPE array
*
* @param string Property to search for
* @param array Array of types to match against
* @return integer Returns the sequence number or -1 if not found
*/
function _findPropertySequenceByTypeArray($name, $type ) {
sort($type);
$total = Count($type);
for( $i = 0; $i < Count($this->properties[$name]); $i++ )
{
$c = 0;
$object = $this->properties[$name][$i];
if( is_array($object->getParamType()) )
{
$c = 0;
foreach( $type as $key=>$val ) {
if( in_array($val, $object->getParamType()) )
$c++;
}
if( $c == $total )
return $i;
}
}
return -1;
}
/**
* Finds a property sequence number by a TYPE string
*
* @param string Property to search for
* @param string Single string type to match against
* @return integer Returns the sequence number or -1 if not found
*/
function _findPropertySequenceByTypeValue($name, $type ) {
for( $i = 0; $i < Count($this->properties[$name]); $i++ )
{
$object = $this->properties[$name][$i];
if( is_array($object->getParamType()) )
{
if( in_array($type, $object->getParamType()) )
return $i;
}
}
return -1;
}
/**
* Finds a property the first available property
*
* @param string Property to search for
* @return integer Returns the 0 or -1 if not found
*/
function _findPropertySequenceFirstAvail($name) {
if( Count($this->properties[$name]) )
return 0;
else
return -1;
}
// {{{ findPropertySequenceByType
/**
* Finds a property sequence number. The search type can
* be a single string type, an array of types or the first available
*
* @param string Property to search for
* @param mixed String or array of types to match against.
* -1 finds the first available
* @return integer Returns the sequence number or -1 if not found
*/
function findPropertySequenceByType($name, $type) {
if( is_array($type) ) {
return $this->_findPropertySequenceByTypeArray($name, $type);
}
else if( $type=='-1' ) {
return $this->_findPropertySequenceFirstAvail($name);
} else {
return $this->_findPropertySequenceByTypeValue($name, $type);
}
}
/**
* Component factory to build the named component.
* Used by the readFile function
*
* @param string Name of the component to build
* @param object Component Object to return
*/
function _componentFactory($componentName, &$componentObject) {
switch( $componentName ) {
case 'VCARD':
$componentObject = new imc_vCard;
break;
case 'VCALENDAR':
$componentObject = new imc_vCalendar;
break;
case 'VEVENT':
$componentObject = new imc_vEvent;
break;
default:
$componentObject = new imcComponent;
$componentObject->setComponentName($componentName);
break;
}
}
/**
* Property factory to build the named property
* Used by the readFile function
*
* @param string Name of the property to build
* @param object Property Object to return
*/
function _propertyFactory( $typedef, &$propertyObject ) {
$propertyObject = new imcProperty();
}
/**
* Splits a string into an array at semicolons. Honors backslash-
* escaped semicolons (i.e., splits at ';' not '\;').
*
* Borrowed from File_IMC on PEAR, written by
* Marshall Roch <hide@address.com> and Paul Jones <hide@address.com>.
*
* @param string $text The string to split into an array.
* @param bool $convertSingle If splitting the string results in a
* single array element, return a string instead of a one-element
* array.
* @return string|array An array of values, or a single string.
*
* @access public
*/
function splitBySemi($text, $convertSingle = false) {
// we use these double-backs (\\) because they get get converted
// to single-backs (\) by preg_split. the quad-backs (\\\\) end
// up as as double-backs (\\), which is what preg_split requires
// to indicate a single backslash (\). what a mess.
$regex = '(?<!\\\\)(\;)';
$tmp = preg_split("/$regex/i", $text);
// if there is only one array-element and $convertSingle is
// true, then return only the value of that one array element
// (instead of returning the array).
if ($convertSingle && count($tmp) == 1) {
return $tmp[0];
} else {
return $tmp;
}
}
/**
* Splits a string into an array at commas. Honors backslash-
* escaped commas (i.e., splits at ',' not '\,').
*
* Borrowed from File_IMC on PEAR, written by
* Marshall Roch <hide@address.com> and Paul Jones <hide@address.com>.
*
* @param string $text The string to split into an array.
* @param bool $convertSingle If splitting the string results in a
* single array element, return a string instead of a one-element
* array.
* @return string|array An array of values, or a single string.
*
* @access public
*/
function splitByComma($text, $convertSingle = false) {
// we use these double-backs (\\) because they get get converted
// to single-backs (\) by preg_split. the quad-backs (\\\\) end
// up as as double-backs (\\), which is what preg_split requires
// to indicate a single backslash (\). ye gods, how ugly.
$regex = '(?<!\\\\)(\,)';
$tmp = preg_split("/$regex/i", $text);
// if there is only one array-element and $convertSingle is
// true, then return only the value of that one array element
// (instead of returning the array).
if ($convertSingle && count($tmp) == 1) {
return $tmp[0];
} else {
return $tmp;
}
}
/**
* Takes a line and extracts the Type-Definition for the line.
*
* Borrowed from File_IMC on PEAR, written by
* Marshall Roch <hide@address.com> and Paul Jones <hide@address.com>.
*
* @param string A left-part (before-the-colon part) from a line.
* @return string The type definition for the line.
*
* @access private
*/
function _getTypeDef($text) {
// split the text by semicolons
$split = $this->splitBySemi($text);
// only return first element (the typedef)
return $split[0];
}
/**
* Finds the Type-Definition parameters for a line.
*
* Borrowed from File_IMC on PEAR, written by
* Marshall Roch <hide@address.com> and Paul Jones <hide@address.com>.
*
* @param string $text The left-part (before-the-colon part) of a line.
* @return array An array of parameters.
*
* @access private
*/
function _getParams($text) {
// split the text by semicolons into an array
$split = $this->splitBySemi($text);
// drop the first element of the array (the type-definition)
array_shift($split);
// set up an array to retain the parameters, if any
$params = array();
// loop through each parameter. the params may be in the format...
// "TYPE=type1,type2,type3"
// ...or...
// "TYPE=type1;TYPE=type2;TYPE=type3"
foreach ($split as $full) {
// split the full parameter at the equal sign so we can tell
// the parameter name from the parameter value
$tmp = explode("=", $full);
// the key is the left portion of the parameter (before
// '='). if in 2.1 format, the key may in fact be the
// parameter value, not the parameter name.
$key = strtoupper(trim($tmp[0]));
// get the parameter name by checking to see if it's in
// vCard 2.1 or 3.0 format.
$name = $this->_getParamName($key);
// list of all parameter values
$listall = trim($tmp[1]);
// if there is a value-list for this parameter, they are
// separated by commas, so split them out too.
$list = $this->splitByComma($listall);
// now loop through each value in the parameter and retain
// it. if the value is blank, that means it's a 2.1-style
// param, and the key itself is the value.
foreach ($list as $val) {
if (trim($val) != '') {
// 3.0 formatted parameter
$params[$name][] = trim($val);
} else {
// 2.1 formatted parameter
$params[$name][] = $key;
}
}
// if, after all this, there are no parameter values for the
// parameter name, retain no info about the parameter (saves
// ram and checking-time later).
if (count($params[$name]) == 0) {
unset($params[$name]);
}
}
// return the parameters array.
return $params;
}
/**
* Returns the parameter name for parameters given without names.
*
* The vCard 2.1 specification allows parameter values without a
* name. The parameter name is then determined from the unique
* parameter value.
*
* Shamelessly lifted from Frank Hellwig <hide@address.com> and his
* vCard PHP project <http://vcardphp.sourceforge.net>.
*
* @param string $value The first element in a parameter name-value
* pair.
* @return string The proper parameter name (TYPE, ENCODING, or
* VALUE).
*
* @access private
*/
function _getParamName($value) {
static $types = array (
'DOM', 'INTL', 'POSTAL', 'PARCEL','HOME', 'WORK',
'PREF', 'VOICE', 'FAX', 'MSG', 'CELL', 'PAGER',
'BBS', 'MODEM', 'CAR', 'ISDN', 'VIDEO',
'AOL', 'APPLELINK', 'ATTMAIL', 'CIS', 'EWORLD',
'INTERNET', 'IBMMAIL', 'MCIMAIL',
'POWERSHARE', 'PRODIGY', 'TLX', 'X400',
'GIF', 'CGM', 'WMF', 'BMP', 'MET', 'PMB', 'DIB',
'PICT', 'TIFF', 'PDF', 'PS', 'JPEG', 'QTIME',
'MPEG', 'MPEG2', 'AVI',
'WAVE', 'AIFF', 'PCM',
'X509', 'PGP'
);
// CONTENT-ID added by pmj
static $values = array (
'INLINE', 'URL', 'CID', 'CONTENT-ID'
);
// 8BIT added by pmj
static $encodings = array (
'7BIT', '8BIT', 'QUOTED-PRINTABLE', 'BASE64'
);
// changed by pmj to the following so that the name defaults to
// whatever the original value was. Frank Hellwig's original
// code was "$name = 'UNKNOWN'".
$name = $value;
if (in_array($value, $types)) {
$name = 'TYPE';
} elseif (in_array($value, $values)) {
$name = 'VALUE';
} elseif (in_array($value, $encodings)) {
$name = 'ENCODING';
}
return $name;
}
// {{{ setUID
/**
* Sets the Unique Identifier for a component.
*
* @param string UID to set
* @see getUID
*/
function setUID($uid) {
$this->getProperty('UID', $propertyObject, 0);
$propertyObject->setValue($uid);
$this->setProperty('UID', $propertyObject, 0);
}
// {{{ getUID
/**
* Gets the Unique Identifier for a component.
*
* @return string UID to set
* @see getUID
*/
function getUID() {
$this->getProperty('UID', $propertyObject, 0);
return $propertyObject->getValue();
}
}
?>