From 06204140a3868973ab89dba642d8420781a3c91a Mon Sep 17 00:00:00 2001 From: Tai Date: Wed, 27 Jan 2021 17:11:40 -0800 Subject: [PATCH] added vcfconvert a php app hosted on a symfony container --- vcfconvert/.docker/Dockerfile | 29 + vcfconvert/.docker/virtualhost.conf | 20 + vcfconvert/.htaccess | 6 + vcfconvert/Contact_Vcard_Parse.php | 841 ++++++++++++++++++++ vcfconvert/README.md | 225 ++++++ vcfconvert/docker-compose.yml | 67 ++ vcfconvert/index.php | 138 ++++ vcfconvert/page.html | 240 ++++++ vcfconvert/tmp/.htaccess | 2 + vcfconvert/utils.php | 70 ++ vcfconvert/vcard_convert.php | 1147 +++++++++++++++++++++++++++ vcfconvert/vcf2csv_logo.jpg | Bin 0 -> 5487 bytes vcfconvert/vcfconvert.sh | 141 ++++ 13 files changed, 2926 insertions(+) create mode 100644 vcfconvert/.docker/Dockerfile create mode 100644 vcfconvert/.docker/virtualhost.conf create mode 100644 vcfconvert/.htaccess create mode 100644 vcfconvert/Contact_Vcard_Parse.php create mode 100644 vcfconvert/README.md create mode 100644 vcfconvert/docker-compose.yml create mode 100644 vcfconvert/index.php create mode 100644 vcfconvert/page.html create mode 100644 vcfconvert/tmp/.htaccess create mode 100644 vcfconvert/utils.php create mode 100644 vcfconvert/vcard_convert.php create mode 100644 vcfconvert/vcf2csv_logo.jpg create mode 100644 vcfconvert/vcfconvert.sh diff --git a/vcfconvert/.docker/Dockerfile b/vcfconvert/.docker/Dockerfile new file mode 100644 index 0000000..1686989 --- /dev/null +++ b/vcfconvert/.docker/Dockerfile @@ -0,0 +1,29 @@ +# Use image which contains apache with php +FROM php:7.4.13-apache +RUN echo "ServerName vcfconvert.yourdomain.com" >> /etc/apache2/apache2.conf +RUN apt-get update && apt-get upgrade -y +# Install packages needed to install php extensions +RUN apt-get install git zlib1g-dev libxml2-dev libzip-dev zip unzip -y +# Install PHP extensions +RUN docker-php-ext-install zip intl mysqli pdo pdo_mysql opcache +# Install NPM +RUN apt-get install npm -y +# Upgrade npm to latest version +RUN npm install -g npm +# Install node manager - n +RUN npm install -g n +# Install latest stable node version +RUN n stable +# Install sass compiler +RUN npm install -g sass +# Install XDEBUG +RUN pecl install xdebug-2.9.8 && docker-php-ext-enable xdebug +RUN echo 'xdebug.remote_port=9000' >> /usr/local/etc/php/php.ini +RUN echo 'xdebug.remote_enable=1' >> /usr/local/etc/php/php.ini +RUN echo 'xdebug.remote_connect_back=1' >> /usr/local/etc/php/php.ini +# Install composer command +RUN curl -sS https://getcomposer.org/installer | php && mv composer.phar /usr/local/bin/composer +# Install symfony command +RUN curl -sS https://get.symfony.com/cli/installer | bash && mv /root/.symfony/bin/symfony /usr/local/bin/symfony +# Set umask to 0000 (newly created files will have 777 permissions) +RUN echo "umask 0000" >> /root/.bashrc diff --git a/vcfconvert/.docker/virtualhost.conf b/vcfconvert/.docker/virtualhost.conf new file mode 100644 index 0000000..b481cb5 --- /dev/null +++ b/vcfconvert/.docker/virtualhost.conf @@ -0,0 +1,20 @@ + + ServerName vcfconvert.yourdomain.com + ServerAdmin webmaster@localhost + DocumentRoot /var/www/html/ + DirectoryIndex /index.php + + + AllowOverride None + Order Allow,Deny + Allow from All + + FallbackResource /index.php + + + + FallbackResource disabled + + ErrorLog ${APACHE_LOG_DIR}/error.log + CustomLog ${APACHE_LOG_DIR}/access.log combined + diff --git a/vcfconvert/.htaccess b/vcfconvert/.htaccess new file mode 100644 index 0000000..e35a2aa --- /dev/null +++ b/vcfconvert/.htaccess @@ -0,0 +1,6 @@ +php_flag display_errors Off +php_flag log_errors On +php_value error_log errors.log +php_value upload_max_filesize 8M +php_value post_max_size 9M +php_value max_execution_time 600 diff --git a/vcfconvert/Contact_Vcard_Parse.php b/vcfconvert/Contact_Vcard_Parse.php new file mode 100644 index 0000000..2c2cc59 --- /dev/null +++ b/vcfconvert/Contact_Vcard_Parse.php @@ -0,0 +1,841 @@ + | +// +----------------------------------------------------------------------+ +// +// $Id: Contact_Vcard_Parse.php,v 1.4 2005/05/28 15:40:17 pmjones Exp $ + + +/** +* +* Parser for vCards. +* +* This class parses vCard 2.1 and 3.0 sources from file or text into a +* structured array. +* +* Usage: +* +* +* // include this class file +* require_once 'Contact_Vcard_Parse.php'; +* +* // instantiate a parser object +* $parse = new Contact_Vcard_Parse(); +* +* // parse a vCard file and store the data +* // in $cardinfo +* $cardinfo = $parse->fromFile('sample.vcf'); +* +* // view the card info array +* echo '
';
+*     print_r($cardinfo);
+*     echo '
'; +*
+* +* +* @author Paul M. Jones +* +* @package Contact_Vcard_Parse +* +* @version 1.31 +* +*/ + +class Contact_Vcard_Parse { + + + /** + * + * Reads a file for parsing, then sends it to $this->fromText() + * and returns the results. + * + * @access public + * + * @param array $filename The filename to read for vCard information. + * + * @return array An array of of vCard information extracted from the + * file. + * + * @see Contact_Vcard_Parse::fromText() + * + * @see Contact_Vcard_Parse::_fromArray() + * + */ + + function fromFile($filename, $decode_qp = true) + { + $text = $this->fileGetContents($filename); + + if ($text === false) { + return false; + } else { + // dump to, and get return from, the fromText() method. + return $this->fromText($text, $decode_qp); + } + } + + + /** + * + * Reads the contents of a file. Included for users whose PHP < 4.3.0. + * + * @access public + * + * @param array $filename The filename to read for vCard information. + * + * @return string|bool The contents of the file if it exists and is + * readable, or boolean false if not. + * + * @see Contact_Vcard_Parse::fromFile() + * + */ + + function fileGetContents($filename) + { + if (file_exists($filename) && + is_readable($filename)) { + + $text = ''; + $len = filesize($filename); + + $fp = fopen($filename, 'r'); + while ($line = fread($fp, filesize($filename))) { + $text .= $line; + } + fclose($fp); + + return $text; + + } else { + + return false; + + } + } + + + /** + * + * Prepares a block of text for parsing, then sends it through and + * returns the results from $this->fromArray(). + * + * @access public + * + * @param array $text A block of text to read for vCard information. + * + * @return array An array of vCard information extracted from the + * source text. + * + * @see Contact_Vcard_Parse::_fromArray() + * + */ + + function fromText($text, $decode_qp = true) + { + // convert all kinds of line endings to Unix-standard and get + // rid of double blank lines. + $this->convertLineEndings($text); + + // unfold lines. concat two lines where line 1 ends in \n and + // line 2 starts with a whitespace character. only removes + // the first whitespace character, leaves others in place. + $fold_regex = '(\n)([ |\t])'; + $text = preg_replace("/$fold_regex/i", "", $text); + + // massage for Macintosh OS X Address Book (remove nulls that + // Address Book puts in for unicode chars) + $text = str_replace("\x00", '', $text); + + // convert the resulting text to an array of lines + $lines = explode("\n", $text); + + // parse the array of lines and return vCard info + return $this->_fromArray($lines, $decode_qp); + } + + + /** + * + * Converts line endings in text. + * + * Takes any text block and converts all line endings to UNIX + * standard. DOS line endings are \r\n, Mac are \r, and UNIX is \n. + * + * NOTE: Acts on the text block in-place; does not return a value. + * + * @access public + * + * @param string $text The string on which to convert line endings. + * + * @return void + * + */ + + function convertLineEndings(&$text) + { + // DOS + $text = str_replace("\r\n", "\n", $text); + + // Mac + $text = str_replace("\r", "\n", $text); + } + + + /** + * + * Splits a string into an array at semicolons. Honors backslash- + * escaped semicolons (i.e., splits at ';' not '\;'). + * + * @access public + * + * @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 mixed An array of values, or a single string. + * + */ + + 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 = '(? : + * \; => ; + * \, => , + * literal \n => newline + * + * @access public + * + * @param mixed $text The text to unescape. + * + * @return void + * + */ + + function unescape(&$text) + { + if (is_array($text)) { + foreach ($text as $key => $val) { + $this->unescape($val); + $text[$key] = $val; + } + } else { + $text = str_replace('\:', ':', $text); + $text = str_replace('\;', ';', $text); + $text = str_replace('\,', ',', $text); + $text = str_replace('\n', "\n", $text); + } + } + + + /** + * + * Emulated destructor. + * + * @access private + * @return boolean true + * + */ + + function _Contact_Vcard_Parse() + { + return true; + } + + + /** + * + * Parses an array of source lines and returns an array of vCards. + * Each element of the array is itself an array expressing the types, + * parameters, and values of each part of the vCard. Processes both + * 2.1 and 3.0 vCard sources. + * + * @access private + * + * @param array $source An array of lines to be read for vCard + * information. + * + * @return array An array of of vCard information extracted from the + * source array. + * + */ + + function _fromArray($source, $decode_qp = true) + { + // the info array will hold all resulting vCard information. + $info = array(); + + // tells us whether the source text indicates the beginning of a + // new vCard with a BEGIN:VCARD tag. + $begin = false; + + // holds information about the current vCard being read from the + // source text. + $card = array(); + + // loop through each line in the source array + foreach ($source as $line) { + + // if the line is blank, skip it. + if (trim($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, ':'); + + // if there is no colon, skip the line. + if ($pos === false) { + continue; + } + + // get the left and right portions + $left = trim(substr($line, 0, $pos)); + $right = trim(substr($line, $pos+1, strlen($line))); + + // have we started yet? + if (! $begin) { + + // nope. does this line indicate the beginning of + // a new vCard? + if (strtoupper($left) == 'BEGIN' && + strtoupper($right) == 'VCARD') { + + // tell the loop that we've begun a new card + $begin = true; + } + + // regardless, loop to the next line of source. if begin + // is still false, the next loop will check the line. if + // begin has now been set to true, the loop will start + // collecting card info. + continue; + + } else { + + // yep, we've started, but we don't know how far along + // we are in the card. is this the ending line of the + // current vCard? + if (strtoupper($left) == 'END' && + strtoupper($right) == 'VCARD') { + + // yep, we're done. keep the info from the current + // card... + $info[] = $card; + + // ...and reset to grab a new card if one exists in + // the source array. + $begin = false; + $card = array(); + + } else { + + // we're not on an ending line, so collect info from + // this line into the current card. split the + // left-portion of the line into a type-definition + // (the kind of information) and parameters for the + // type. + $typedef = $this->_getTypeDef($left); + $params = $this->_getParams($left); + + // if we are decoding quoted-printable, do so now. + // QUOTED-PRINTABLE is not allowed in version 3.0, + // but we don't check for versioning, so we do it + // regardless. ;-) + $this->_decode_qp($params, $right); + + // now get the value-data from the line, based on + // the typedef + switch ($typedef) { + + case 'N': + // structured name of the person + $value = $this->_parseN($right); + break; + + case 'ADR': + // structured address of the person + $value = $this->_parseADR($right); + break; + + case 'NICKNAME': + // nicknames + $value = $this->_parseNICKNAME($right); + break; + + case 'ORG': + // organizations the person belongs to + $value = $this->_parseORG($right); + break; + + case 'CATEGORIES': + // categories to which this card is assigned + $value = $this->_parseCATEGORIES($right); + break; + + case 'GEO': + // geographic coordinates + $value = $this->_parseGEO($right); + break; + + default: + // by default, just grab the plain value. keep + // as an array to make sure *all* values are + // arrays. for consistency. ;-) + $value = array(array($right)); + break; + } + + // add the type, parameters, and value to the + // current card array. note that we allow multiple + // instances of the same type, which might be dumb + // in some cases (e.g., N). + $card[$typedef][] = array( + 'param' => $params, + 'value' => $value + ); + } + } + } + + $this->unescape($info); + return $info; + } + + + /** + * + * Takes a vCard line and extracts the Type-Definition for the line. + * + * @access private + * + * @param string $text A left-part (before-the-colon part) from a + * vCard line. + * + * @return string The type definition for the line. + * + */ + + function _getTypeDef($text) + { + // split the text by semicolons + $split = $this->splitBySemi($text); + + // only return first element (the typedef) + return strtoupper($split[0]); + } + + + /** + * + * Finds the Type-Definition parameters for a vCard line. + * + * @access private + * + * @param string $text A left-part (before-the-colon part) from a + * vCard line. + * + * @return mixed An array of parameters. + * + */ + + 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; + } + + + /** + * + * Looks at the parameters of a vCard line; if one of them is + * ENCODING[] => QUOTED-PRINTABLE then decode the text in-place. + * + * @access private + * + * @param array $params A parameter array from a vCard line. + * + * @param string $text A right-part (after-the-colon part) from a + * vCard line. + * + * @return void + * + */ + + function _decode_qp(&$params, &$text) + { + // loop through each parameter + foreach ($params as $param_key => $param_val) { + + // check to see if it's an encoding param + if (trim(strtoupper($param_key)) == 'ENCODING') { + + // loop through each encoding param value + foreach ($param_val as $enc_key => $enc_val) { + + // if any of the values are QP, decode the text + // in-place and return + if (trim(strtoupper($enc_val)) == 'QUOTED-PRINTABLE') { + $text = quoted_printable_decode($text); + return; + } + } + } + } + } + + + /** + * + * Returns parameter names from 2.1-formatted vCards. + * + * 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 and his + * vCard PHP project . + * + * @access private + * + * @param string $value The first element in a parameter name-value + * pair. + * + * @return string The proper parameter name (TYPE, ENCODING, or + * VALUE). + * + */ + + 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; + } + + + /** + * + * Parses a vCard line value identified as being of the "N" + * (structured name) type-defintion. + * + * @access private + * + * @param string $text The right-part (after-the-colon part) of a + * vCard line. + * + * @return array An array of key-value pairs where the key is the + * portion-name and the value is the portion-value. The value itself + * may be an array as well if multiple comma-separated values were + * indicated in the vCard source. + * + */ + + function _parseN($text) + { + // make sure there are always at least 5 elements + $tmp = array_pad($this->splitBySemi($text), 5, ''); + return array( + $this->splitByComma($tmp[0]), // family (last) + $this->splitByComma($tmp[1]), // given (first) + $this->splitByComma($tmp[2]), // addl (middle) + $this->splitByComma($tmp[3]), // prefix + $this->splitByComma($tmp[4]) // suffix + ); + } + + + /** + * + * Parses a vCard line value identified as being of the "ADR" + * (structured address) type-defintion. + * + * @access private + * + * @param string $text The right-part (after-the-colon part) of a + * vCard line. + * + * @return array An array of key-value pairs where the key is the + * portion-name and the value is the portion-value. The value itself + * may be an array as well if multiple comma-separated values were + * indicated in the vCard source. + * + */ + + function _parseADR($text) + { + // make sure there are always at least 7 elements + $tmp = array_pad($this->splitBySemi($text), 7, ''); + return array( + $this->splitByComma($tmp[0]), // pob + $this->splitByComma($tmp[1]), // extend + $this->splitByComma($tmp[2]), // street + $this->splitByComma($tmp[3]), // locality (city) + $this->splitByComma($tmp[4]), // region (state) + $this->splitByComma($tmp[5]), // postcode (ZIP) + $this->splitByComma($tmp[6]) // country + ); + } + + + /** + * + * Parses a vCard line value identified as being of the "NICKNAME" + * (informal or descriptive name) type-defintion. + * + * @access private + * + * @param string $text The right-part (after-the-colon part) of a + * vCard line. + * + * @return array An array of nicknames. + * + */ + + function _parseNICKNAME($text) + { + return array($this->splitByComma($text)); + } + + + /** + * + * Parses a vCard line value identified as being of the "ORG" + * (organizational info) type-defintion. + * + * @access private + * + * @param string $text The right-part (after-the-colon part) of a + * vCard line. + * + * @return array An array of organizations; each element of the array + * is itself an array, which indicates primary organization and + * sub-organizations. + * + */ + + function _parseORG($text) + { + $tmp = $this->splitbySemi($text); + $list = array(); + foreach ($tmp as $val) { + $list[] = array($val); + } + + return $list; + } + + + /** + * + * Parses a vCard line value identified as being of the "CATEGORIES" + * (card-category) type-defintion. + * + * @access private + * + * @param string $text The right-part (after-the-colon part) of a + * vCard line. + * + * @return mixed An array of categories. + * + */ + + function _parseCATEGORIES($text) + { + return array($this->splitByComma($text)); + } + + + /** + * + * Parses a vCard line value identified as being of the "GEO" + * (geographic coordinate) type-defintion. + * + * @access private + * + * @param string $text The right-part (after-the-colon part) of a + * vCard line. + * + * @return mixed An array of lat-lon geocoords. + * + */ + + function _parseGEO($text) + { + // make sure there are always at least 2 elements + $tmp = array_pad($this->splitBySemi($text), 2, ''); + return array( + array($tmp[0]), // lat + array($tmp[1]) // lon + ); + } +} + +?> \ No newline at end of file diff --git a/vcfconvert/README.md b/vcfconvert/README.md new file mode 100644 index 0000000..ba929c2 --- /dev/null +++ b/vcfconvert/README.md @@ -0,0 +1,225 @@ +This one is for mainly my own reference that is why it isn't listed on the main README.md in this repository. + +For this project, I used the [symfony](https://github.com/kasteckis/symfony-docker-compose) docker container to host a php application, [vCard to LDIF/CSV Converter](https://github.com/thomascube/vcfconvert) by u/thomascube. + +Symfony can help you host any php application. For this example, I selfhost vcfconvert. + +### Minimum File Structure +``` +/home/ +└── ~/ + └── docker/ + └── vcfconvert/ + ├── .docker <-- This is a directory + ├── Dockerfile + ├── virtualhost.conf + ├── docker-compose.yml + ├── + +``` +### Add to Caddyfile (from ~/docker/caddy) +Remember to `docker exec -w /etc/caddy caddy caddy reload` after editing your Caddyfile. +``` +vcfconvert.yourdomain.com { + reverse_proxy vcfconvert:80 +} +``` + +### .docker/Dockerfile +The only thing you need to change in this file is the second line `RUN echo "ServerName vcfconvert.yourdomain.com" >> /etc/apache2/apache2.conf` use your own domain here. + +``` +# Use image which contains apache with php +FROM php:7.4.13-apache +RUN echo "ServerName vcfconvert.yourdomain.com" >> /etc/apache2/apache2.conf +RUN apt-get update && apt-get upgrade -y +# Install packages needed to install php extensions +RUN apt-get install git zlib1g-dev libxml2-dev libzip-dev zip unzip -y +# Install PHP extensions +RUN docker-php-ext-install zip intl mysqli pdo pdo_mysql opcache +# Install NPM +RUN apt-get install npm -y +# Upgrade npm to latest version +RUN npm install -g npm +# Install node manager - n +RUN npm install -g n +# Install latest stable node version +RUN n stable +# Install sass compiler +RUN npm install -g sass +# Install XDEBUG +RUN pecl install xdebug-2.9.8 && docker-php-ext-enable xdebug +RUN echo 'xdebug.remote_port=9000' >> /usr/local/etc/php/php.ini +RUN echo 'xdebug.remote_enable=1' >> /usr/local/etc/php/php.ini +RUN echo 'xdebug.remote_connect_back=1' >> /usr/local/etc/php/php.ini +# Install composer command +RUN curl -sS https://getcomposer.org/installer | php && mv composer.phar /usr/local/bin/composer +# Install symfony command +RUN curl -sS https://get.symfony.com/cli/installer | bash && mv /root/.symfony/bin/symfony /usr/local/bin/symfony +# Set umask to 0000 (newly created files will have 777 permissions) +RUN echo "umask 0000" >> /root/.bashrc +``` + +### .docker/virtualhost.conf +Again change the second line to your own domain. `ServerName vcfconvert.yourdomain.com` +``` + + ServerName vcfconvert.yourdomain.com + ServerAdmin webmaster@localhost + DocumentRoot /var/www/html/ + DirectoryIndex /index.php + + + AllowOverride None + Order Allow,Deny + Allow from All + + FallbackResource /index.php + + + + FallbackResource disabled + + ErrorLog ${APACHE_LOG_DIR}/error.log + CustomLog ${APACHE_LOG_DIR}/access.log combined + + +``` + +### docker-compose.yml +Here you want to focus on the `web:` section. + +Here I added the caddy network which is similar to all my previous self-hosted docker apps. This is so I can have caddy reverse proxy into the php app's container. + +For the volumes. I wrote in the path of the directory that contains the `index.php` file and point it to `/var/www/html/` in the container. + +``` +version: "3" +services: + mysql: + image: mysql:5.7 + container_name: project_mysql + restart: unless-stopped + networks: + default: + ipv4_address: 192.168.2.3 + environment: + MYSQL_ALLOW_EMPTY_PASSWORD: "true" + MYSQL_ROOT_PASSWORD: + MYSQL_DATABASE: project + MYSQL_USER: root + MYSQL_PASSWORD: + ports: + - "9906:3306" + phpmyadmin: + image: phpmyadmin/phpmyadmin + container_name: project_phpmyadmin + links: + - mysql + environment: + PMA_HOST: mysql + PMA_PORT: 3306 + PMA_ARBITRARY: 1 + networks: + default: + ipv4_address: 192.168.2.4 + ports: + - 81:80 + + web: + build: ./.docker + container_name: vcfconvert + networks: + default: + ipv4_address: 192.168.2.2 + caddy: + volumes: + - ./:/var/www/html/ + - ./.docker/virtualhost.conf:/etc/apache2/sites-available/000-default.conf + ports: + - "9080:80" + depends_on: + - "mysql" + + mailhog: + image: mailhog/mailhog + container_name: project_mailhog + ports: + - 1025:1025 # smtp server + - 8025:8025 # web ui + networks: + default: + ipv4_address: 192.168.2.5 + +networks: + default: + driver: bridge + ipam: + driver: default + config: + - subnet: 192.168.2.0/24 # If you change this, make sure to change other IP addresses + caddy: + external: + name: caddy_net +``` + + + + + +vCard to LDIF/CSV Converter +=========================== +by Thomas Bruederli + +To run this converter just copy all files to a webserver directory where PHP +is installed and enabled. Open your browser and type in the URL of your +webserver with the according folder. By default, file uploads up to 2MB are +allowed. + +Command line version +-------------------- +This package also includes a shell script to invoke the converter from the +command line. PHP is also required to be installed on your machine. +Just copy the files anywhere on your disk, open a terminal and type the +following commands: + + $ cd /path/to/vcfconvert + $ ./vcfconvert.sh -f ldif -o destination_file.ldif source_file.vcf +or + + $ ./vcfconvert.sh -hv -f csv -d ";" -o destination_file.csv source_file.vcf + +To get information about optinal parameters, type + + $ ./vcfconvert.sh help + +LICENSE +------- +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, see [www.gnu.org/licenses/][gpl2]. + +For any bug reports or feature requests please open issue tickets at +[github.com/thomascube/vcfconvert][github]. + + +#### Note from Kevin on libdlusb compatibility +Due to the fact libdlusb is incapable of transmitting all the information +generally used with the contact application (currently it is capable of +only transmitting name, type, and phone number) I have intentionally organized +the format to be convenient for saving into the note application instead. This +allows the user to have multiple phone numbers per entry along with an e-mail +address. + +[gpl2]: http://www.gnu.org/licenses/gpl2.txt +[github]: http://github.com/thomascube/vcfconvert + diff --git a/vcfconvert/docker-compose.yml b/vcfconvert/docker-compose.yml new file mode 100644 index 0000000..bd47809 --- /dev/null +++ b/vcfconvert/docker-compose.yml @@ -0,0 +1,67 @@ +version: "3" +services: + mysql: + image: mysql:5.7 + container_name: project_mysql + restart: unless-stopped + networks: + default: + ipv4_address: 192.168.2.3 + environment: + MYSQL_ALLOW_EMPTY_PASSWORD: "true" + MYSQL_ROOT_PASSWORD: + MYSQL_DATABASE: project + MYSQL_USER: root + MYSQL_PASSWORD: + ports: + - "9906:3306" + phpmyadmin: + image: phpmyadmin/phpmyadmin + container_name: project_phpmyadmin + links: + - mysql + environment: + PMA_HOST: mysql + PMA_PORT: 3306 + PMA_ARBITRARY: 1 + networks: + default: + ipv4_address: 192.168.2.4 + ports: + - 81:80 + + web: + build: ./.docker + container_name: vcfconvert + networks: + default: + ipv4_address: 192.168.2.2 + caddy: + volumes: + - ./:/var/www/html/ + - ./.docker/virtualhost.conf:/etc/apache2/sites-available/000-default.conf + ports: + - "9080:80" + depends_on: + - "mysql" + + mailhog: + image: mailhog/mailhog + container_name: project_mailhog + ports: + - 1025:1025 # smtp server + - 8025:8025 # web ui + networks: + default: + ipv4_address: 192.168.2.5 + +networks: + default: + driver: bridge + ipam: + driver: default + config: + - subnet: 192.168.2.0/24 # If you change this, make sure to change other IP addresses + caddy: + external: + name: caddy_net diff --git a/vcfconvert/index.php b/vcfconvert/index.php new file mode 100644 index 0000000..89ec95b --- /dev/null +++ b/vcfconvert/index.php @@ -0,0 +1,138 @@ + | + +-----------------------------------------------------------------------+ + +*/ + +// suppress php notices +@ini_set('error_reporting', E_ALL&~E_NOTICE); + +// include the converter class file +require_once('vcard_convert.php'); +require_once('utils.php'); + + +if (!empty($_FILES['_vcards'])) +{ + // instantiate a parser object + $conv = new vcard_convert(array( + 'mailonly' => !empty($_POST['_mailonly']), + 'phoneonly' => !empty($_POST['_phoneonly']), + 'accesscode' => preg_replace('/[^1-9]/', '', $_POST['_accesscode']), + )); + + // check for errors + if ($err = $_FILES['_vcards']['error']) + { + $GLOBALS['error_msg'] = ($err == UPLOAD_ERR_INI_SIZE || $err == UPLOAD_ERR_FORM_SIZE) ? + "The uploaded file was too big! Maximum file size allowed: ".show_bytes(parse_bytes(ini_get('upload_max_filesize'))) : + "Upload failed, please try again"; + } + // parse the vCard file + else if ($conv->fromFile($_FILES['_vcards']['tmp_name'])) + { + $ext = $_POST['_format'] == 'gmail' ? 'csv' : ($_POST['_format'] == 'img' ? 'zip' : $_POST['_format']); + $fname = asciiwords(preg_replace('/\.[a-z]+$/i', '', $_FILES['_vcards']['name'])); + + header(sprintf('Content-Type: text/%s', $ext)); + header(sprintf('Content-Disposition: attachment; filename="%s.%s"', $fname, $ext)); + + if ($_POST['_format'] == 'ldif') + { + print $conv->toLdif(); + exit; + } + else if ($_POST['_format'] == 'ldap') + { + // Clean the input dn modifier from dangerous chars + $dnID = substr(preg_replace('/[^\da-z=,_ -]/i', '', $_POST['_dn']), 0, 255); + print $conv->toLdif($dnID ? $dnID : "", null, $_POST['_encoding']); + exit; + } + else if ($_POST['_format'] == 'gmail') + { + print $conv->toGmail(); + exit; + } + else if ($_POST['_format'] == 'fritzbox') + { + print $conv->toFritzBox(); + exit; + } + else if ($_POST['_format'] == 'csv') + { + $header = $_POST['_header'] === '1' ? true : false; + $delimiter = $_POST['_delimiter'] == 'tab' ? "\t" : $_POST['_delimiter']; + print $conv->toCSV($delimiter, $header, $_POST['_encoding'], $_POST['_newlines']); + exit; + } + // extract all images from the vcard file + else if ($_POST['_format'] == 'img') + { + mkdir($tmpdir = __DIR__ . '/tmp/'.md5(mt_rand())); // Diretory safe naming + if ($conv->toImages($tmpdir)) + { + shell_exec('cd ' . escapeshellarg($tmpdir) . "; zip $fname *"); + $zipfile = "$tmpdir/$fname.zip"; + } + if ($zipfile && is_readable($zipfile)) + { + header('Content-Type: application/zip', true); + readfile($zipfile); + $success = true; + } + else + { + $GLOBALS['error_msg'] = "No images were found in this file."; + header('Content-Type: text/html', true); + header('Content-Disposition: inline', true); + } + + shell_exec('rm -rf '.escapeshellarg($tmpdir)); + + if ($success) + exit; + } + } + else + $GLOBALS['error_msg'] = "Could not parse vCard file. Either it is empty or of a format not supported."; +} + + +include('page.html'); +?> diff --git a/vcfconvert/page.html b/vcfconvert/page.html new file mode 100644 index 0000000..cf28606 --- /dev/null +++ b/vcfconvert/page.html @@ -0,0 +1,240 @@ + + + +Online vCard Converter + + + + + + + + +
+ + + + + + +
VCard to CSV LogovCard to LDIF/CSV Converter
+ +

Preferred to export Apple's Address Book to Mozilla Thunderbird.
+Simply drag all contacts from the Address Book to your desktop and upload the created vCard file.

+ +'.$GLOBALS['error_msg']."

\n"; + +?> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
vCard-File: + (max. )
Format: + +
Encoding: + + +
Filter: +
+ +
Modifications: +   + +

+
+ +
+

How to convert files

+ +
    +
  1. Select a file from your computer's local drive for the field "vCard-File"
  2. +
  3. Choose the desired output format
  4. +
  5. Click the "convert" button
  6. +
+ +

The converted file is automatically downloaded to your computer. + If you're not prompted where to save the file, you'll find it in the "Downloads" folder on your computer.

+ +

Privacy Policy

+ +

This public service is intended to provide simple functions to convert any vCard file into another format. +It was created for personal needs and is now provided on a private and non commercial basis. +The uploaded file will not be stored permanently nor will the converted addresses be kept on the server.

+ +

You can also download the script +and run it locally. All you need is a webserver or a shell with PHP installed. This software is free and open source +and everybody is welcome to help us improving it. Fork us on github!

+ +

For questions about development, privacy or security, please contact tellme@brotherli.ch

+
+ + + diff --git a/vcfconvert/tmp/.htaccess b/vcfconvert/tmp/.htaccess new file mode 100644 index 0000000..7d3aaf1 --- /dev/null +++ b/vcfconvert/tmp/.htaccess @@ -0,0 +1,2 @@ +Order allow,deny +Deny from all \ No newline at end of file diff --git a/vcfconvert/utils.php b/vcfconvert/utils.php new file mode 100644 index 0000000..fa83c02 --- /dev/null +++ b/vcfconvert/utils.php @@ -0,0 +1,70 @@ + 1073741824) + { + $gb = $bytes/1073741824; + $str = sprintf($gb >= 10 || $gb-intval($gb) == 0 ? "%d GB" : "%.1f GB", $gb); + } + else if ($bytes > 1048576) + { + $mb = $bytes/1048576; + $str = sprintf($mb >= 10 || $mb-intval($mb) == 0 ? "%d MB" : "%.1f MB", $mb); + } + else if ($bytes > 1024) + $str = sprintf("%d KB", round($bytes/1024)); + else + $str = sprintf('%d B', $bytes); + + return $str; +} + +/** + * Remove all non-ascii and non-word chars + * except . and - + */ +function asciiwords($str) +{ + return preg_replace(array('/\s+/', '/[^a-z0-9\_\-\.]/i'), array('_',''), $str); +} + + +?> diff --git a/vcfconvert/vcard_convert.php b/vcfconvert/vcard_convert.php new file mode 100644 index 0000000..5fa4325 --- /dev/null +++ b/vcfconvert/vcard_convert.php @@ -0,0 +1,1147 @@ + | + +-----------------------------------------------------------------------+ + +*/ + +// 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 + */ + 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 + */ + 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 + */ + 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 + * @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; +} + + +?> diff --git a/vcfconvert/vcf2csv_logo.jpg b/vcfconvert/vcf2csv_logo.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ae6915bb544e70561c01a3d6a321458a4a405457 GIT binary patch literal 5487 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 | + +-----------------------------------------------------------------------+ + +*/ + +@ini_set('error_reporting', E_ALL &~ E_NOTICE); + +require_once('vcard_convert.php'); +require_once('utils.php'); + +/** + * Parse commandline arguments into a hash array + */ +function get_args() +{ + $args = array(); + for ($i=1; $i + -f Target format (ldif,ldap,csv,gmail,libdlusb,fritzbox,img) + -b LDAP identifier added to dn: + -l Generate just a list of DN objects (only works with -b) + -o Output file (write to stdout by default) + -d CSV col delimiter + -h Include header line in CSV output + -i Convert CSV output to ISO-8859-1 encoding (deprecated, use -c instead) + -c Character encoding for CSV output + -n Line endings for CSV output: 'n', 'r' or 'rn' + -m Only convert cards with an e-mail address + -p Only convert cards with phone numbers + -v Verbose output + +EOF; + +// show help +if ($opt[0] == 'help') + die($usage); + +// read arguments +$file = array_pop($opt); +$format = $opt['f'] ? $opt['f'] : 'ldif'; + +if (empty($file)) + die("Not enough arguments!\n$usage"); + +// instantiate a parser object +$conv = new vcard_convert(array('mailonly' => isset($opt['m']), 'phoneonly' => isset($opt['p']))); + +// parse a vCard file +if ($conv->fromFile($file)) +{ + if (isset($opt['v'])) + echo "Detected $conv->file_charset encoding\n"; + if (isset($opt['v']) && isset($opt['m'])) + echo "Only convert vCards with an e-mail address\n"; + + if ($format == 'ldif') + $out = $conv->toLdif(); + + else if ($format == 'ldap') + $out = $conv->toLdif($opt['b'], isset($opt['l']) ? 'dn' : null); + + else if ($format == 'gmail') + $out = $conv->toGmail(); + + else if ($format == 'libdlusb') + $out = $conv->toLibdlusb(); + + else if ($format == 'fritzbox') + $out = $conv->toFritzBox(); + + else if ($format == 'csv') + { + if (!isset($opt['c']) && isset($opt['i'])) + $opt['c'] = 'ISO-8859-1'; + $delimiter = isset($opt['d']) ? ($opt['d']=='\t' || $opt['d']=='tab' ? "\t" : $opt['d']) : ";"; + $out = $conv->toCSV($delimiter, isset($opt['h']), isset($opt['c']) ? strtoupper($opt['c']) : null, $opt['n']); + + if (isset($opt['v']) && isset($opt['c'])) + echo "Converting output to " . strtoupper($opt['c']) . PHP_EOL; + } + else if ($format == 'img') + $out = $conv->toImages('tmp'); + + else + die("Unknown output format\n"); + + // write to output file + if ($opt['o']) + { + if ($fp = @fopen($opt['o'], 'w')) + { + fwrite($fp, $out); + fclose($fp); + echo "Wrote ".$conv->export_count." cards to $opt[o]\n"; + } + else + die("Cannot write to $opt[o]; permission denied\n"); + } + else + echo $out; +} +else + echo "Cannot parse $file\n"; + + +?>