<?php

/*
 +-----------------------------------------------------------------------+
 | vCard to LDIF/CSV Converter Class                                     |
 | extends the PEAR Contact_Vcard_Parse Class                            |
 |                                                                       |
 | Copyright (C) 2006-2013, Thomas Bruederli - Switzerland               |
 | Licensed under the GNU GPL                                            |
 |                                                                       |
 +-----------------------------------------------------------------------+
 | Author: Thomas Bruederli <thomas@brotherli.ch>                        |
 +-----------------------------------------------------------------------+

*/

// version 1.31 required
require_once('Contact_Vcard_Parse.php');


/**
 * Typedef of a vCard object
 */
class vCard
{
	var $version;
	var $displayname;
	var $surname;
	var $firstname;
	var $middlename;
	var $nickname;
	var $title;
	var $birthday;
	var $organization;
	var $department;
	var $jobtitle;
	var $home = array();
	var $work = array();
	var $countrycode;
	var $relatedname;
	var $email;
	var $email2;
	var $email3;
	var $pager;
	var $mobile;
	var $im = array();
	var $notes;
	var $categories;
	var $uid;
	var $photo;
}


/**
 * vCard to LDIF/CSV Converter Class
 */
class vcard_convert extends Contact_Vcard_Parse
{
	 // define mapping for newline values
	static $NEWLINE_MAP = array(
		'\n'   => "\n",
		'n'    => "\n",
		'lf'   => "\n",
		'\r'   => "\r",
		'r'    => "\r",
		'cr'   => "\r",
		'rn'   => "\r\n",
		'\r\n' => "\r\n",
		'crlf' => "\r\n",
	);

	var $parsed = array();
	var $vcards = array();
	var $file_charset = 'ISO-8859-1';
	var $charset = 'ISO-8859-1';
	var $export_count = 0;
	var $mailonly = false;
	var $phoneonly = false;
	var $accesscode = null;
	
	
	/**
	 * Constructor taking a list of converter properties
	 */
	function __construct($p = array())
	{
		foreach ($p as $prop => $value)
			$this->$prop = $value;
	}


	/**
	 * Read a file and parse it
	 *
	 * @override
	 */
	function fromFile($filename, $decode_qp = true)
	{
		if (!filesize($filename) || ($text = $this->fileGetContents($filename)) === false)
			return false;

		// dump to, and get return from, the fromText() method.
		return $this->fromText($text, $decode_qp);
	}
	
	/**
	 * Parse a given string for vCards
	 *
	 * @override
	 */
	function fromText($text, $decode_qp = true)
	{
		// check if charsets are specified (usually vcard version < 3.0 but this is not reliable)
		if (preg_match('/charset=/i', substr($text, 0, 2048)))
			$this->charset = null;
		// try to detect charset of the whole file
		else if ($encoding = vcard_convert::get_charset($text))
			$this->charset = $this->file_charset = $encoding;

		// convert document to UTF-8
		if (isset($this->charset) && $this->charset != 'UTF-8' && $this->charset != 'ISO-8859-1')
		{
			$text = $this->utf8_convert($text);
			$this->charset = 'UTF-8';
		}

		$this->parsed = parent::fromText($text, $decode_qp);
		if (!empty($this->parsed))
		{
			$this->normalize();
			
			// after normalize() all values should be UTF-8
			if (!isset($this->charset))
				$this->charset = 'UTF-8';
				
			return count($this->cards);
		}
		else
			return false;
	}
	
	
	/**
	 * Convert the abstract vCard structure into address objects
	 *
	 * @access private
	 */
	function normalize()
	{
		$this->cards = array();
		foreach($this->parsed as $i => $card)
		{
			$vcard = new vCard;
			$vcard->version = (float)$card['VERSION'][0]['value'][0][0];
			
			// convert all values to UTF-8 according to their charset param
			if (!isset($this->charset))
				$card = $this->card2utf8($card);

			// extract names
			$names = $card['N'][0]['value'];
			$vcard->surname = trim($names[0][0]);
			$vcard->firstname = trim($names[1][0]);
			$vcard->middlename = trim($names[2][0]);
			$vcard->title = trim($names[3][0]);
			
			if (empty($vcard->title) && isset($card['TITLE']))
				$vcard->title = trim($card['TITLE'][0]['value'][0][0]);

			$vcard->displayname = isset($card['FN']) ? trim($card['FN'][0]['value'][0][0]) : '';
			$vcard->nickname    = isset($card['NICKNAME']) ? trim($card['NICKNAME'][0]['value'][0][0]) : '';

			// extract notes
			$vcard->notes = isset($card['NOTE']) ? ltrim($card['NOTE'][0]['value'][0][0]) : '';

			// extract birthday and anniversary
			foreach (array('BDAY' => 'birthday', 'ANNIVERSARY' => 'anniversary', 'X-ANNIVERSARY' => 'anniversary') as $vcf => $propname)
			{
				if (is_array($card[$vcf]))
				{
					$temp = preg_replace('/[\-\.\/]/', '', $card[$vcf][0]['value'][0][0]);
					$vcard->$propname = array(
						'y' => substr($temp,0,4),
						'm' => substr($temp,4,2),
						'd' => substr($temp,6,2));
				}
			}

			if (is_array($card['GENDER']))
				$vcard->gender = $card['GENDER'][0]['value'][0][0];
			else if (is_array($card['X-GENDER']))
				$vcard->gender = $card['X-GENDER'][0]['value'][0][0];

			if (!empty($vcard->gender))
				$vcard->gender = strtoupper($vcard->gender[0]);

			// extract job_title
			if (is_array($card['TITLE']))
				$vcard->jobtitle = $card['TITLE'][0]['value'][0][0];

			// extract UID
			if (is_array($card['UID']))
				$vcard->uid = $card['UID'][0]['value'][0][0];

			// extract org and dep
			if (is_array($card['ORG']) && ($temp = $card['ORG'][0]['value']))
			{
				$vcard->organization = trim($temp[0][0]);
				$vcard->department   = trim($temp[1][0]);
			}
			
			// extract urls
			if (is_array($card['URL']))
				$this->parse_url($card['URL'], $vcard);

			// extract addresses
			if (is_array($card['ADR']))
				$this->parse_adr($card['ADR'], $vcard);

			// extract phones
			if (is_array($card['TEL']))
				$this->parse_tel($card['TEL'], $vcard);

			// read Apple Address Book proprietary fields
			for ($n = 1; $n <= 9; $n++)
			{
				$prefix = 'ITEM'.$n;
				if (is_array($card["$prefix.TEL"])) {
					$this->parse_tel($card["$prefix.TEL"], $vcard);
				}
				if (is_array($card["$prefix.URL"])) {
					$this->parse_url($card["$prefix.URL"], $vcard);
				}
				if (is_array($card["$prefix.ADR"])) {
					$this->parse_adr($card["$prefix.ADR"], $vcard);
				}
				if (is_array($card["$prefix.X-ABADR"])) {
					$this->parse_cc($card["$prefix.X-ABADR"], $vcard);
				}
				if (is_array($card["$prefix.X-ABDATE"])) {
					$this->parse_abdate($card["$prefix.X-ABDATE"], $vcard, $card["$prefix.X-ABLABEL"][0]);
				}
				if (is_array($card["$prefix.X-ABRELATEDNAMES"])) {
					$this->parse_rn($card["$prefix.X-ABRELATEDNAMES"], $vcard /*, $card["$prefix.X-ABLABEL"][0]*/);
				}
			}

			// extract e-mail addresses
			$a_email = array();
			$n = 0;
			if (is_array($card['EMAIL'])) {
				while (isset($card['EMAIL'][$n])) {
					$a_email[] = $card['EMAIL'][$n]['value'][0][0];
					$n++;
				}
			}
			if ($n < 2) { //as only 3 e-mail address will be exported we don't need to search for more
				for ($n = 1; $n <= 9; $n++) {
					if (is_array($card["ITEM$n.EMAIL"]))
					{
						$a_email[] = $card["ITEM$n.EMAIL"][0]['value'][0][0];
						if (isset($card["ITEM$n.EMAIL"][1]))
							$a_email[] = $card["ITEM$n.EMAIL"][1]['value'][0][0];
					}
				}
			}

			if (count($a_email))
				$vcard->email = $a_email[0];
			if (!empty($a_email[1]))
				$vcard->email2 = $a_email[1];
			if (!empty($a_email[2]))
				$vcard->email3 = $a_email[2];
			
			// find IM entries
			if (is_array($card['X-AIM']))
				$vcard->im['aim'] = $card['X-AIM'][0]['value'][0][0];
			if (is_array($card['X-IQC']))
				$vcard->im['icq'] = $card['X-ICQ'][0]['value'][0][0];
			if (is_array($card['X-MSN']))
				$vcard->im['msn'] = $card['X-MSN'][0]['value'][0][0];
			if (is_array($card['X-JABBER']))
				$vcard->im['jabber'] = $card['X-JABBER'][0]['value'][0][0];

			if (is_array($card['PHOTO'][0]))
				$vcard->photo = array('data' => $card['PHOTO'][0]['value'][0][0], 'encoding' => $card['PHOTO'][0]['param']['ENCODING'][0], 'type' => $card['PHOTO'][0]['param']['TYPE'][0]);

			$vcard->categories = join(',', (array)$card['CATEGORIES'][0]['value'][0]);

			$this->cards[] = $vcard;
			}
		}

	/**
	 * Helper method to parse an URL node
	 *
	 * @access private
	 */
	function parse_url(&$node, &$vcard)
	{
		foreach($node as $url)
		{
			if (empty($url['param']['TYPE'][0]) || in_array_nc("WORK", $url['param']['TYPE']) || in_array_nc("PREF", $url['param']['TYPE']))
				$vcard->work['url'] = $url['value'][0][0];
			if (in_array_nc("HOME", $url['param']['TYPE']))
				$vcard->home['url'] = $url['value'][0][0];
		}
	}

	/**
	 * Helper method to parse first or preferred related name node (when available)
	 *
	 * @access private
	 */
	function parse_rn(&$node, &$vcard, $ablabel = null)
	{
		$key = $ablabel ? strtolower(trim($ablabel['value'][0][0], '_$!<>')) : null;
		if (empty($key))
			$key = 'relatedname';

		foreach($node as $rn)
		{
			if (empty($vcard->{$key}) || in_array_nc("PREF", $rn['param']['TYPE']))
				$vcard->{$key} = $rn['value'][0][0];
		}
	}

	/**
	 * Helper method to parse first or preferred country code (when available)
	 *
	 * @access private
	 */
	function parse_cc(&$node, &$vcard)
	{
		foreach($node as $cc)
		{
			if (empty($vcard->countrycode) || in_array_nc("PREF", $cc['param']['TYPE']))
				$vcard->countrycode = $cc['value'][0][0];
		}
	}

	/**
	 * Helper method to parse an address node
	 *
	 * @access private
	 */
	function parse_adr(&$node, &$vcard)
	{
		$home = array();
		$work = array();

		foreach($node as $adr)
		{
			if (empty($adr['param']['TYPE'][0]) || in_array_nc("HOME", $adr['param']['TYPE']))
				$home = $adr['value'];
			if (in_array_nc("WORK", $adr['param']['TYPE']))
				$work = $adr['value'];
		}
		
		// values not splitted by Contact_Vcard_Parse if key is like item1.ADR
		if (strstr($home[0][0], ';'))
		{
			$temp = explode(';', $home[0][0]);
			$vcard->home += array(
				'addr1' => $temp[2],
				'city' => $temp[3],
				'state' => $temp[4],
				'zipcode' => $temp[5],
				'country' => $temp[6]);
		}
		else if (sizeof($home)>6)
		{
			$vcard->home += array(
				'addr1' => $home[2][0],
				'city' => $home[3][0],
				'state' => $home[4][0],
				'zipcode' => $home[5][0],
				'country' => $home[6][0]);
		}
		
		// values not splitted by Contact_Vcard_Parse if key is like item1.ADR
		if (strstr($work[0][0], ';'))
		{
			$temp = explode(';', $work[0][0]);
			$vcard->work += array(
				'office' => $temp[1],
				'addr1' => $temp[2],
				'city' => $temp[3],
				'state' => $temp[4],
				'zipcode' => $temp[5],
				'country' => $temp[6]);
		}
		else if (sizeof($work)>6)
		{
			$vcard->work += array(
				'addr1' => $work[2][0],
				'city' => $work[3][0],
				'state' => $work[4][0],
				'zipcode' => $work[5][0],
				'country' => $work[6][0]);
		}
	}

	/**
	 * Helper method to parse an phone number node
	 *
	 * @access private
	 */
	function parse_tel(&$node, &$vcard)
	{
		foreach($node as $tel)
		{
			if (in_array_nc("PAGER", $tel['param']['TYPE']))
				$vcard->pager = $tel['value'][0][0];
			else if (in_array_nc("CELL", $tel['param']['TYPE']))
				$vcard->mobile = $tel['value'][0][0];
			else if (in_array_nc("HOME", $tel['param']['TYPE']) || in_array_nc("PREF", $tel['param']['TYPE']))
			{
				if (in_array_nc("FAX", $tel['param']['TYPE']))
					$vcard->home['fax'] = $tel['value'][0][0];
				else
					$vcard->home['phone'] = $tel['value'][0][0];
			}
			else if (in_array_nc("WORK", $tel['param']['TYPE']))
			{
				if(in_array_nc("FAX", $tel['param']['TYPE']))
					$vcard->work['fax'] = $tel['value'][0][0];
				else
					$vcard->work['phone'] = $tel['value'][0][0];
			}
			else if (in_array_nc("VOICE", $tel['param']['TYPE']))
			{
				if (!isset($vcard->home['phone']))
					$vcard->home['phone'] = $tel['value'][0][0];
				else if (!isset($vcard->work['phone']))
					$vcard->work['phone'] = $tel['value'][0][0];
				else if (!isset($vcard->mobile))
					$vcard->mobile = $tel['value'][0][0];
			}

			if (in_array_nc("FAX", $tel['param']['TYPE']))
				$vcard->fax = $tel['value'][0][0];
		}
	}

	/**
	 * Helper method to parse X-ABDATE nodes
	 *
	 * @access private
	 */
	function parse_abdate(&$node, &$vcard, $ablabel = null)
	{
		$key = $ablabel ? strtolower(trim($ablabel['value'][0][0], '_$!<>')) : null;
		if (empty($key))
			$key = '_x_abdate';

		foreach($node as $abdate)
		{
			if (empty($vcard->{$key}) && preg_match('/^(\d{4})-(\d{2})-(\d{2})/', $abdate['value'][0][0], $m))
			{
				$vcard->{$key} = array(
					'y' => intval($m[1]),
					'm' => intval($m[2]),
					'd' => intval($m[3]),
				);
			}
		}
	}

	/**
	 * Convert the parsed vCard data into CSV format
	 */
	function toCSV($delm="\t", $add_title=true, $encoding=null, $newlines="\n")
		{
		$out = '';
		$eol = isset(self::$NEWLINE_MAP[$newlines]) ? self::$NEWLINE_MAP[$newlines] : $newlines;
		$this->export_count = 0;

		if ($add_title)
		{
			$out .= 'First Name'.$delm.'Last Name'.$delm.'Display Name'.$delm.'Nickname'.$delm.'E-mail Address'.$delm.'E-mail 2 Address'.$delm.'E-mail 3 Address'.$delm;
			$out .= 'Home Phone'.$delm.'Business Phone'.$delm.'Home Fax'.$delm.'Business Fax'.$delm.'Pager'.$delm.'Mobile Phone'.$delm;
			$out .= 'Home Street'.$delm.'Home Address 2'.$delm.'Home City'.$delm.'Home State'.$delm.'Home Postal Code'.$delm.'Home Country'.$delm;
			$out .= 'Business Address'.$delm.'Business Address 2'.$delm.'Business City'.$delm.'Business State'.$delm.'Business Postal Code'.$delm;
			$out .= 'Business Country'.$delm.'Country Code'.$delm.'Related name'.$delm.'Job Title'.$delm.'Department'.$delm.'Organization'.$delm.'Notes'.$delm;
			$out .= 'Birthday'.$delm.'Anniversary'.$delm.'Gender'.$delm;
			$out .= 'Web Page'.$delm.'Web Page 2'.$delm.'Categories';
			$out .= $eol;
		}

		foreach ($this->cards as $card)
		{
			if ($this->mailonly && empty($card->email) && empty($card->email2) && empty($card->email3))
				continue;
			if ($this->phoneonly && empty($card->home['phone']) && empty($card->work['phone']) && empty($card->mobile))
				continue;

			$out .= $this->csv_encode($card->firstname, $delm);
			$out .= $this->csv_encode($card->surname, $delm);
			$out .= $this->csv_encode($card->displayname, $delm);
			$out .= $this->csv_encode($card->nickname, $delm);
			$out .= $this->csv_encode($card->email, $delm);
			$out .= $this->csv_encode($card->email2, $delm);
			$out .= $this->csv_encode($card->email3, $delm);
			$out .= $this->csv_encode($this->normalize_phone($card->home['phone']), $delm);
			$out .= $this->csv_encode($this->normalize_phone($card->work['phone']), $delm);
			$out .= $this->csv_encode($this->normalize_phone($card->home['fax']), $delm);
			$out .= $this->csv_encode($this->normalize_phone($card->work['fax']), $delm);
			$out .= $this->csv_encode($this->normalize_phone($card->pager), $delm);
			$out .= $this->csv_encode($this->normalize_phone($card->mobile), $delm);
			$out .= $this->csv_encode($card->home['addr1'], $delm);
			$out .= $this->csv_encode($card->home['addr2'], $delm);
			$out .= $this->csv_encode($card->home['city'], $delm);
			$out .= $this->csv_encode($card->home['state'], $delm);
			$out .= $this->csv_encode($card->home['zipcode'], $delm);
			$out .= $this->csv_encode($card->home['country'], $delm);
			$out .= $this->csv_encode($card->work['addr1'], $delm);
			$out .= $this->csv_encode($card->work['addr2'], $delm);
			$out .= $this->csv_encode($card->work['city'], $delm);
			$out .= $this->csv_encode($card->work['state'], $delm);
			$out .= $this->csv_encode($card->work['zipcode'], $delm);
			$out .= $this->csv_encode($card->work['country'], $delm);
			$out .= $this->csv_encode($card->countrycode, $delm);
			$out .= $this->csv_encode($card->relatedname, $delm);
			$out .= $this->csv_encode($card->jobtitle, $delm);
			$out .= $this->csv_encode($card->department, $delm);
			$out .= $this->csv_encode($card->organization, $delm);
			$out .= $this->csv_encode($card->notes, $delm);
			$out .= !empty($card->birthday) ? $this->csv_encode(sprintf('%04d-%02d-%02d 00:00:00', $card->birthday['y'], $card->birthday['m'], $card->birthday['d']), $delm) : $delm;
			$out .= !empty($card->anniversary) ? $this->csv_encode(sprintf('%04d-%02d-%02d', $card->anniversary['y'], $card->anniversary['m'], $card->anniversary['d']), $delm) : $delm;
			$out .= $this->csv_encode($card->gender, $delm);
			$out .= $this->csv_encode($card->work['url'], $delm);
			$out .= $this->csv_encode($card->home['url'], $delm);
			$out .= $this->csv_encode($card->categories, $delm, false);

			$out .= $eol;
			$this->export_count++;
		}

		return $this->charset_convert($out, $encoding);
	}
	
	/**
	 * New GMail export function
	 *
	 * @author Thomas Bruederli
	 * @author Max Plischke <plischke@gmail.com>
	 */
	function toGmail()
	{
		$delm = ',';
		$this->export_count = 0;
		$out = "Name,E-mail,Notes,Section 1 - Description,Section 1 - Email,".
					 "Section 1 - IM,Section 1 - Phone,Section 1 - Mobile,".
					 "Section 1 - Pager,Section 1 - Fax,Section 1 - Company,".
					 "Section 1 - Title,Section 1 - Other,Section 1 - Address,".
					 "Section 2 - Description,Section 2 - Email,Section 2 - IM,".
					 "Section 2 - Phone,Section 2 - Mobile,Section 2 - Pager,".
					 "Section 2 - Fax,Section 2 - Company,Section 2 - Title,".
					 "Section 2 - Other,Section 2 - Address\n";

		foreach ($this->cards as $card)
		{
			if ($this->mailonly && empty($card->email) && empty($card->email2))
				continue;
			if ($this->phoneonly && empty($card->home['phone']) && empty($card->work['phone']) && empty($card->mobile))
				continue;

			$home = array($card->home['addr1'], $card->home['city']);
			if ($card->home['state']) $home[] = $card->home['state'];
			if ($card->home['zipcode']) $home[] = $card->home['zipcode'];
			if ($card->home['country']) $home[] = $card->home['country'];

			$work = array($card->work['addr1'], $card->work['city']);
			if ($card->work['state']) $work[] = $card->work['state'];
			if ($card->work['zipcode']) $work[] = $card->work['zipcode'];
			if ($card->work['country']) $work[] = $card->work['country'];
			
			$im = array_values($card->im);

			$out .= $this->csv_encode($card->displayname, $delm);
			$out .= $this->csv_encode($card->email, $delm); // main
			$out .= $this->csv_encode($card->notes, $delm); // Notes

			$out .= $this->csv_encode('Home', $delm);
			$out .= $this->csv_encode('', $delm); // home email ?
			$out .= $this->csv_encode($im[0], $delm); // IM
			$out .= $this->csv_encode($this->normalize_phone($card->home['phone']), $delm);
			$out .= $this->csv_encode($this->normalize_phone($card->mobile), $delm);
			$out .= $this->csv_encode($this->normalize_phone($card->pager), $delm);
			$out .= $this->csv_encode($this->normalize_phone($card->home['fax']), $delm);
			$out .= $this->csv_encode('', $delm); //
			$out .= /* $card['title'] . */ $delm;
			$out .= $this->csv_encode('', $delm); // other
			$out .= $this->csv_encode(join(' ', $home), $delm);

			$out .= $this->csv_encode('Work', $delm);
			$out .= $this->csv_encode($card->email2, $delm); // work email
			$out .= $this->csv_encode($im[1], $delm); // IM
			$out .= $this->csv_encode($this->normalize_phone($card->work['phone']), $delm);
			$out .= $this->csv_encode('', $delm); //
			$out .= $this->csv_encode('', $delm); //
			$out .= $this->csv_encode($this->normalize_phone($card->work['fax']), $delm); // work fax
			$out .= $this->csv_encode($card->organization, $delm);
			$out .= $this->csv_encode($card->jobtitle, $delm); // title
			$out .= $this->csv_encode($card->department, $delm);
			$out .= $this->csv_encode(join(' ', $work), $delm);

			//$out .= $this->csv_encode($card->nick, $delm);
			//$out .= $this->csv_encode($card->home['url'], $delm);
			//$out .= $this->csv_encode($card->work['url'], $delm, FALSE);

			$out .= "\n";
			$this->export_count++;
		}

	return $out;
	}

	/**
	 * Convert the parsed vCard data into libdlusb format
	 *
	 * @author Kevin Clement <donkjunk@softhome.net>
	 */
	function toLibdlusb()
	{
		$delm="; ";
		$out = '';
		$this->export_count = 0;

		foreach ($this->cards as $card)
		{
			if ($this->mailonly && empty($card->email) && empty($card->email2))
				continue;
			if ($this->phoneonly && empty($card->home['phone']) && empty($card->work['phone']) && empty($card->mobile))
				continue;

			// a little ugly but this filters out files that only have incompatible data to prevent "blank" files
			if (empty($card->home['phone']) && empty($card->work['phone']) && empty($card->email) && empty($card->mobile))
				continue;

			// having determined there is data that needs exporting this
			// makes certain we don't have holes to save watch memory
			$out .= $this->csv_encode($card->displayname, $delm);
			if ($card->home['phone'] != '')
			{
				$out .= 'Home = ';
				$out .= $this->csv_encode($this->normalize_phone($card->home['phone']), $delm);
			}
			if ($card->work['phone'] != '')
			{
				$out .= 'Work = ';
				$out .= $this->csv_encode($this->normalize_phone($card->work['phone']), $delm);
			}
			if ($card->email != '')
			{
				$out .= 'Email = ';
				$out .= $this->csv_encode($card->email, $delm);
			}
			if($card->mobile != '')
			{
				$out .= 'Mobile = ';
				$out .= $this->csv_encode($this->normalize_phone($card->mobile), $delm);
			}

			$out .= "\n";
			$this->export_count++;
		}

		// convert to ISO-8859-1
		//if ($encoding == 'ISO-8859-1' && $this->charset == 'UTF-8' && function_exists('utf8_decode'))
		//	$out = utf8_decode($out);

		return $out;
	}


	/**
	 * Export cards as Ldif/LDAP Ldif
	 */
	function toLdif($identifier="", $attrib=null, $encoding="UTF-8")
	{
		$out = '';
		$this->export_count = 0;

		foreach($this->cards as $card)
		{
			if ($this->mailonly && empty($card->email) && empty($card->email2))
				continue;
			if ($this->phoneonly && empty($card->home['phone']) && empty($card->work['phone']) && empty($card->mobile) && empty($card->fax))
				continue;

			// If we will export LDIF for an LDAP server, some checks and tweaks are needed
			if ($identifier != "")
			{
				$card->displayname = trim($card->displayname);

				// Generate a random UID for LDAP Ldif if not present
				if (empty($card->uid))
					$card->uid = uniqid('card-id-');

				if (empty($card->displayname))
					$card->displayname = trim($card->firstname.' '.$card->surname);

				if (empty($card->displayname))
					$card->displayname = trim($card->nickname);

				if (empty($card->displayname))
					$card->displayname = trim($card->organization);

				if (empty($card->displayname))
					$card->displayname = $card->uid;

				if (empty($card->surname))
					$card->surname = $card->displayname;
			}

			$a_out = array();
			
			if ($identifier == "")
				$a_out['dn'] = sprintf("cn=%s,mail=%s", $card->displayname, $card->email);
			else
				$a_out['dn'] = sprintf("uid=%s,%s", $card->uid, $identifier);
				
			$a_out['objectclass'] = array('top', 'person', 'organizationalPerson', 'inetOrgPerson', 'mozillaAbPersonAlpha');

			$a_out['cn'] = $card->displayname;
			$a_out['sn'] = $card->surname;
			
			if ($card->uid)
				$a_out['uid'] = $card->uid;
			if ($card->firstname)
				$a_out['givenName'] = $card->firstname;
			if ($card->title)
				$a_out['title'] = $card->title;
			if ($card->jobtitle)
				$a_out['employeeType'] = $card->jobtitle;
			if ($card->email)
				$a_out['mail'] = $card->email;
			// Get the binary for the photo of type jpeg
			// FIXME: ? According to the specs, only JFIF formats should be used
			// but some clients read even PNG from the jpegPhoto attr. Should we be
			// so restrictive here?
			if ($card->photo && strtolower($card->photo['type']) == "jpeg")
				$a_out['jpegPhoto'] = base64_decode(preg_replace('/\s+/', '', $card->photo['data']));
			if ($card->nickname)
				$a_out['mozillaNickname'] = $card->nickname;
			if ($card->email2)
				$a_out['mozillaSecondEmail'] = $card->email2;
			if ($card->home['phone'])
				$a_out['homePhone'] = $this->normalize_phone($card->home['phone']);
			if ($card->mobile)
				$a_out['mobile'] = $this->normalize_phone($card->mobile);
			if ($card->fax)
				$a_out['fax'] = $this->normalize_phone($card->fax);
			if ($card->pager)
				$a_out['pager'] = $this->normalize_phone($card->pager);
			if ($card->home['addr1'])
				$a_out['mozillaHomeStreet'] = $card->home['addr1'];
			if ($card->home['city'])
				$a_out['mozillaHomeLocalityName'] = $card->home['city'];
			if ($card->home['state'])
				$a_out['mozillaHomeState'] = $card->home['state'];
			if ($card->home['zipcode'])
				$a_out['mozillaHomePostalCode'] = $card->home['zipcode'];
			if ($card->home['country'])
				$a_out['mozillaHomeCountryName'] = $card->home['country'];
			if ($card->organization)
				$a_out['o'] = $card->organization;
			if ($card->department)
				$a_out['departmentNumber'] = $card->department;
			if ($card->work['addr1'])
				$a_out['street'] = $card->work['addr1'];
			if ($card->work['city'])
				$a_out['l'] = $card->work['city'];
			if ($card->work['state'])
				$a_out['st'] = $card->work['state'];
			if ($card->work['zipcode'])
				$a_out['postalCode'] = $card->work['zipcode'];
			if ($card->work['country'] && strlen($card->work['country']) == 2)
				$a_out['c'] = $card->work['country'];
			if ($card->work['phone'])
				$a_out['telephoneNumber'] = $this->normalize_phone($card->work['phone']);
			if ($card->work['fax'])
				$a_out['fax'] = $this->normalize_phone($card->work['fax']);
			else if ($card->home['fax'])
				$a_out['fax'] = $this->normalize_phone($card->home['fax']);
			if ($card->work['url'])
				$a_out['mozillaWorkUrl'] = $card->work['url'];
			if ($card->home['url'])
				$a_out['mozillaHomeUrl'] = $card->home['url'];
			if ($card->notes)
				$a_out['description'] = $card->notes;
			if ($card->birthday) {
				$a_out['birthyear'] = $card->birthday['y'];
				$a_out['mozillaCustom1'] = sprintf("%04d-%02d-%02d", $card->birthday['y'], $card->birthday['m'], $card->birthday['d']);
			}

			// only return a single attribute (e.g. dn)
			if (!empty($attrib))
			{
				if (!isset($a_out[$attrib]))
					continue;
				$val = is_array($a_out[$attrib]) ? $a_out[$attrib][0] : $a_out[$attrib];
				$out .= trim($this->ldif_encode($val, $enc, true));
			}
			else
			{
				// compose ldif output
				foreach ($a_out as $key => $val)
				{
					$enc = $key == 'dn' ? 'UTF-8' : $encoding;
					if (is_array($val))
						foreach ($val as $i => $val2)
							$out .= sprintf("%s: %s\n", $key, $this->ldif_encode($val2, $enc));
					else
						$out .= sprintf("%s:%s\n", $key, $this->ldif_encode($val, $enc));
				}
			}

			$out .= "\n";
			$this->export_count++;
		}

		return $out;
	}

	/**
	 * Convert the parsed vCard data into CSV format for FritzBox
	 *
	 * @author Thomas Bruederli
	 * @author Gerd Mueller <gerd@zeltnerweg9.ch>
	 */
	function toFritzBox()
	{
		$delm=";";
		$out = 'sep='.$delm."\r\n";
		$this->export_count = 0;
		
		$out .= 'Name'.$delm.
				'TelNumHome'.$delm.'VanityHome'.$delm.'KurzWahlHome'.$delm.
				'TelNumWork'.$delm.'VanityWork'.$delm.'KurzWahlWork'.$delm.
				'TelNumMobile'.$delm.'VanityMobile'.$delm.'KurzWahlMobile'.$delm.
				'Kommentar'.$delm.'Firma'.$delm.'Bild'.$delm.'Kategorie'.$delm.'ImageUrl'.$delm.
				'Prio'.$delm.'Email'.$delm.'RingTone'.$delm.'RingVol'.
				"\r\n";

		foreach ($this->cards as $card)
		{
			if ($this->mailonly && empty($card->email) && empty($card->email2))
				continue;
			if ($this->phoneonly && empty($card->home['phone']) && empty($card->work['phone']) && empty($card->mobile))
				continue;
			
			$name=array();
			$firstname    = $this->csv_encode($card->firstname, $delm, false);
			$surname      = $this->csv_encode($card->surname, $delm, false);
			$organization = $this->csv_encode($card->organization, $delm, false);
			
			if (strlen($surname))   $name[] = $surname;
			if (strlen($firstname)) $name[] = $firstname;
			if (count($name))
			{
				$out .= implode(' ',$name) . $delm;
			} else
			{
				$out .= $organization.$delm;
			}
			
			$out .= $this->csv_encode($this->normalize_phone($card->home['phone']), $delm);
			$out .= $delm; # Vanity			
			$out .= $delm; # Kurzwahl			
			$out .= $this->csv_encode($this->normalize_phone($card->work['phone']), $delm);
			$out .= $delm; # Vanity			
			$out .= $delm; # Kurzwahl			
			$out .= $this->csv_encode($this->normalize_phone($card->mobile), $delm);
			$out .= $delm; # Vanity			
			$out .= $delm; # Kurzwahl			
			$out .= $this->csv_encode($card->notes, $delm);
			$out .= $organization.$delm;
			$out .= $delm; # Bild			
			$out .= $delm; # Kategorie
			$out .= $delm; # ImageUrl
			$out .= '1'.$delm; #Prio
			$out .= $this->csv_encode((empty($card->email)) ? $card->email2 : $card->email, $delm);
			$out .= $delm; # RingTone
			$out .= $delm; # RingVol

			$out .= "\r\n";
			$this->export_count++;
		}

		// convert to ISO-8859-1
		if ($this->charset == 'UTF-8' && function_exists('utf8_decode'))
			$out = utf8_decode($out);

		return $out;
	}
	
	/**
	 * Export all cards images
	 */
	function toImages($tmpdir)
	{
		$this->export_count = 0;
		
		foreach($this->cards as $card)
		{
			if ($card->photo)
			{
				$ext = strtolower($card->photo['type']);
				if ($ext == "jpeg" || $ext == "png")
				{
					if ($ext == "jpeg")
						$ext = "jpg";
					
					// Try to guess the displayname of the card if it is empty
					$card->displayname = trim($card->displayname);
				
					if (empty($card->displayname))
						$card->displayname = trim($card->firstname.' '.$card->surname);
					if (empty($card->displayname))
						$card->displayname = trim($card->nickname);
					if (empty($card->displayname))
						$card->displayname = trim($card->organization);

					// A FIX: Since some cards may give no (or identical) filenames
					// after the cleanup by asciiwords, always generate a random UID
					// for card's file name
					$fn = asciiwords(strtolower($card->displayname));
					if (empty($fn) || preg_match("/^[_ -]+$/",$fn) || preg_match("/^-/",$fn))
						$fn = uniqid('card-id-');
					else
						$fn .= uniqid('-card-id-');

					$operation = file_put_contents($tmpdir.'/'.$fn.'.'.$ext, base64_decode(preg_replace('/\s+/', '', $card->photo['data'])));
					if ($operation !== False)
						$this->export_count++;
				}
			}
		}

		return $this->export_count;
	}

	/**
	 * Encode one col string for CSV export
	 *
	 * @access private
	 */
	function csv_encode($str, $delm, $add_delm=true)
	{
		if (strpos($str, $delm))
			$str = '"'.$str.'"';
		return preg_replace('/\r?\n/', ' ', $str) . ($add_delm ? $delm : '');
	}
	
	
	/**
	 * Encode one col string for Ldif export
	 *
	 * @access private
	 */
	function ldif_encode($str, $encoding, $nowrap = false)
	{
		// base64-encode all values that contain non-ascii chars
		// $str is already UTF-encoded after the VCard is read in $card
		if (preg_match('/[^\x09\x20-\x7E]/', $str))
			$str = ': ' . base64_encode($this->charset_convert($str, $encoding));
		else
			$str = ' ' . $str;

		// Make long lines splited according to LDIF specs to a new line starting with [:space:]
		return $nowrap ? $str : preg_replace('/\n $/', '', chunk_split($str, 76, "\n "));
	}


	/**
	 * Strip Access Code
	 *
	 * @access private
	 */
	function normalize_phone($phone)
	{
		if (strlen($this->accesscode))
			$phone = preg_replace('/^[\+|00]+' . $this->accesscode . '[- ]*(\d+)/', '0\1', $phone);
		return $phone;
	}
	
	
	/**
	 * Convert a whole vcard (array) to UTF-8.
	 * Each member value that has a charset parameter will be converted.
	 *
	 * @access private
	 */
	function card2utf8($card)
	{
		foreach ($card as $key => $node)
		{
			foreach ($node as $i => $subnode)
			{
				if ($subnode['param']['CHARSET'] && ($charset = strtoupper($subnode['param']['CHARSET'][0])))
				{
					$card[$key][$i]['value'] = $this->utf8_convert($subnode['value'], $charset);
					unset($card[$key][$i]['param']['CHARSET']);
				}
			}
		}
		
		return $card;
	}

	/**
	 * Convert the given input to UTF-8.
	 * If it's an array, all values will be converted recursively.
	 *
	 * @access private
	 */
	function utf8_convert($in, $from=null)
	{
		// Sometimes the charset in $from is in quotes, so clean it up
		$from = trim($from,'"');
		
		if (!$from)
			$from = $this->charset;
		
		// recursively convert all array values
		if (is_array($in))
		{
			foreach ($in as $key => $value)
				$in[$key] = $this->utf8_convert($value, $from);
			return $in;
		}
		else
			$str = $in;

		// try to convert to UTF-8
		if ($from != 'UTF-8')
		{
			if ($from == 'ISO-8859-1' && function_exists('utf8_encode'))
				$str = utf8_encode($str);
			else if (function_exists('mb_convert_encoding'))
			{
				$str = mb_convert_encoding($str, 'UTF-8', $from);
				if (strlen($str) == 0)
					error_log("Vcfconvert error: mbstring failed to convert the text!");
			}
			else if (function_exists('iconv'))
			{
				$str = iconv($from, 'UTF-8', $str);
				if (strlen($str) == 0)
					error_log("Vcfconvert error: iconv failed to convert the text!");
			}
			else
				error_log("Vcfconvert warning: the vcard is not in UTF-8.");
		}

		// strip BOM if it is still there
		return ltrim($str, "\xFE\xFF\xEF\xBB\xBF\0");
	}


	/**
	 * Convert the given string from internal charset to the target encoding
	 */
	function charset_convert($str, $encoding)
	{
		// convert to ISO-8859-1
		if ($encoding == 'ISO-8859-1' && $this->charset == 'UTF-8' && function_exists('utf8_decode'))
			return utf8_decode($str);

		// convert to any other charset
		if (!empty($encoding) && $encoding !== $this->charset && function_exists('iconv'))
			return iconv($this->charset, $encoding.'//IGNORE', $str);

		return $str;
	}


	/**
	 * Returns UNICODE type based on BOM (Byte Order Mark) or default value on no match
	 *
	 * @author Clemens Wacha <clemens.wacha@gmx.net>
	 * @access private
	 * @static
	 */
	function get_charset($string)
	{
		if (substr($string, 0, 4) == "\0\0\xFE\xFF") return 'UTF-32BE';  // Big Endian
		if (substr($string, 0, 4) == "\xFF\xFE\0\0") return 'UTF-32LE';  // Little Endian
		if (substr($string, 0, 2) == "\xFE\xFF") return 'UTF-16BE';      // Big Endian
		if (substr($string, 0, 2) == "\xFF\xFE") return 'UTF-16LE';      // Little Endian
		if (substr($string, 0, 3) == "\xEF\xBB\xBF") return 'UTF-8';

		// no match, check for utf-8
		if (vcard_convert::is_utf8($string)) return 'UTF-8';

		// heuristics
		if ($string[0] == "\0" && $string[1] == "\0" && $string[2] == "\0" && $string[3] != "\0") return 'UTF-32BE';
		if ($string[0] != "\0" && $string[1] == "\0" && $string[2] == "\0" && $string[3] == "\0") return 'UTF-32LE';
		if ($string[0] == "\0" && $string[1] != "\0" && $string[2] == "\0" && $string[3] != "\0") return 'UTF-16BE';
		if ($string[0] != "\0" && $string[1] == "\0" && $string[2] != "\0" && $string[3] == "\0") return 'UTF-16LE';

		return false;
	}


	/**
	 * Returns true if $string is valid UTF-8 and false otherwise.
	 * From http://w3.org/International/questions/qa-forms-utf-8.html
	 *
	 * @access private
	 * @static
	 */
	function is_utf8($string)
	{
		return preg_match('/\A(
			[\x09\x0A\x0D\x20-\x7E]
			| [\xC2-\xDF][\x80-\xBF]
			| \xE0[\xA0-\xBF][\x80-\xBF]
			| [\xE1-\xEC\xEE\xEF][\x80-\xBF]{2}
			| \xED[\x80-\x9F][\x80-\xBF]
			| \xF0[\x90-\xBF][\x80-\xBF]{2}
			| [\xF1-\xF3][\x80-\xBF]{3}
			| \xF4[\x80-\x8F][\x80-\xBF]{2}
			)*\z/xs', substr($string, 0, 2048));
	}
	
}  // end class vcard_convert


/**
 * Checks if a value exists in an array non-case-sensitive
 */
function in_array_nc($needle, $haystack, $strict = false)
{
	foreach ((array)$haystack as $key => $value)
	{
		if (strtolower($needle) == strtolower($value) && ($strict || gettype($needle) == gettype($value)))
			return true;
	}
	return false;
}


?>
