]> git.sur5r.net Git - contagged/blob - inc/Contact_Vcard_Parse.php
correct URL encoding on search redirects
[contagged] / inc / Contact_Vcard_Parse.php
1 <?php
2 /* vim: set expandtab tabstop=4 softtabstop=4 shiftwidth=4: */ 
3 // +----------------------------------------------------------------------+ 
4 // | PHP version 4                                                        | 
5 // +----------------------------------------------------------------------+ 
6 // | Copyright (c) 1997-2002 The PHP Group                                | 
7 // +----------------------------------------------------------------------+ 
8 // | This source file is subject to version 2.0 of the PHP license,       | 
9 // | that is bundled with this package in the file LICENSE, and is        | 
10 // | available at through the world-wide-web at                           | 
11 // | http://www.php.net/license/2_02.txt.                                 | 
12 // | If you did not receive a copy of the PHP license and are unable to   | 
13 // | obtain it through the world-wide-web, please send a note to          | 
14 // | license@php.net so we can mail you a copy immediately.               | 
15 // +----------------------------------------------------------------------+ 
16 // | Authors: Paul M. Jones <pmjones@php.net>                             | 
17 // +----------------------------------------------------------------------+ 
18 // 
19 // $Id: Contact_Vcard_Parse.php,v 1.4 2005/05/28 15:40:17 pmjones Exp $ 
20
21
22 /**
23
24 * Parser for vCards.
25 *
26 * This class parses vCard 2.1 and 3.0 sources from file or text into a
27 * structured array.
28
29 * Usage:
30
31 * <code>
32 *     // include this class file
33 *     require_once 'Contact_Vcard_Parse.php';
34 *     
35 *     // instantiate a parser object
36 *     $parse = new Contact_Vcard_Parse();
37 *     
38 *     // parse a vCard file and store the data
39 *     // in $cardinfo
40 *     $cardinfo = $parse->fromFile('sample.vcf');
41 *     
42 *     // view the card info array
43 *     echo '<pre>';
44 *     print_r($cardinfo);
45 *     echo '</pre>';
46 * </code>
47
48 *
49 * @author Paul M. Jones <pmjones@php.net>
50
51 * @package Contact_Vcard_Parse
52
53 * @version 1.31
54
55 */
56
57 class Contact_Vcard_Parse {
58     
59     
60     /**
61     * 
62     * Reads a file for parsing, then sends it to $this->fromText()
63     * and returns the results.
64     * 
65     * @access public
66     * 
67     * @param array $filename The filename to read for vCard information.
68     * 
69     * @return array An array of of vCard information extracted from the
70     * file.
71     * 
72     * @see Contact_Vcard_Parse::fromText()
73     * 
74     * @see Contact_Vcard_Parse::_fromArray()
75     * 
76     */
77     
78     function fromFile($filename, $decode_qp = true)
79     {
80         $text = $this->fileGetContents($filename);
81         
82         if ($text === false) {
83             return false;
84         } else {
85             // dump to, and get return from, the fromText() method.
86             return $this->fromText($text, $decode_qp);
87         }
88     }
89     
90     
91     /**
92     * 
93     * Reads the contents of a file.  Included for users whose PHP < 4.3.0.
94     * 
95     * @access public
96     * 
97     * @param array $filename The filename to read for vCard information.
98     * 
99     * @return string|bool The contents of the file if it exists and is
100     * readable, or boolean false if not.
101     * 
102     * @see Contact_Vcard_Parse::fromFile()
103     * 
104     */
105     
106     function fileGetContents($filename)
107     {
108         if (file_exists($filename) &&
109             is_readable($filename)) {
110             
111             $text = '';
112             $len = filesize($filename);
113             
114             $fp = fopen($filename, 'r');
115             while ($line = fread($fp, filesize($filename))) {
116                 $text .= $line;
117             }
118             fclose($fp);
119             
120             return $text;
121             
122         } else {
123         
124             return false;
125             
126         }
127     }
128     
129     
130     /**
131     * 
132     * Prepares a block of text for parsing, then sends it through and
133     * returns the results from $this->fromArray().
134     * 
135     * @access public
136     * 
137     * @param array $text A block of text to read for vCard information.
138     * 
139     * @return array An array of vCard information extracted from the
140     * source text.
141     * 
142     * @see Contact_Vcard_Parse::_fromArray()
143     * 
144     */
145     
146     function fromText($text, $decode_qp = true)
147     {
148         // convert all kinds of line endings to Unix-standard and get
149         // rid of double blank lines.
150         $this->convertLineEndings($text);
151         
152         // unfold lines.  concat two lines where line 1 ends in \n and
153         // line 2 starts with a whitespace character.  only removes
154         // the first whitespace character, leaves others in place.
155         $fold_regex = '(\n)([ |\t])';
156         $text = preg_replace("/$fold_regex/i", "", $text);
157         
158         // massage for Macintosh OS X Address Book (remove nulls that
159         // Address Book puts in for unicode chars)
160         $text = str_replace("\x00", '', $text);
161         
162         // convert the resulting text to an array of lines
163         $lines = explode("\n", $text);
164         
165         // parse the array of lines and return vCard info
166         return $this->_fromArray($lines, $decode_qp);
167     }
168     
169     
170     /**
171     * 
172     * Converts line endings in text.
173     * 
174     * Takes any text block and converts all line endings to UNIX
175     * standard. DOS line endings are \r\n, Mac are \r, and UNIX is \n.
176     *
177     * NOTE: Acts on the text block in-place; does not return a value.
178     * 
179     * @access public
180     * 
181     * @param string $text The string on which to convert line endings.
182     * 
183     * @return void
184     * 
185     */
186     
187     function convertLineEndings(&$text)
188     {
189         // DOS
190         $text = str_replace("\r\n", "\n", $text);
191         
192         // Mac
193         $text = str_replace("\r", "\n", $text);
194     }
195     
196     
197     /**
198     * 
199     * Splits a string into an array at semicolons.  Honors backslash-
200     * escaped semicolons (i.e., splits at ';' not '\;').
201     * 
202     * @access public
203     * 
204     * @param string $text The string to split into an array.
205     * 
206     * @param bool $convertSingle If splitting the string results in a
207     * single array element, return a string instead of a one-element
208     * array.
209     * 
210     * @return mixed An array of values, or a single string.
211     * 
212     */
213     
214     function splitBySemi($text, $convertSingle = false)
215     {
216         // we use these double-backs (\\) because they get get converted
217         // to single-backs (\) by preg_split.  the quad-backs (\\\\) end
218         // up as as double-backs (\\), which is what preg_split requires
219         // to indicate a single backslash (\). what a mess.
220         $regex = '(?<!\\\\)(\;)';
221         $tmp = preg_split("/$regex/i", $text);
222         
223         // if there is only one array-element and $convertSingle is
224         // true, then return only the value of that one array element
225         // (instead of returning the array).
226         if ($convertSingle && count($tmp) == 1) {
227             return $tmp[0];
228         } else {
229             return $tmp;
230         }
231     }
232     
233     
234     /**
235     * 
236     * Splits a string into an array at commas.  Honors backslash-
237     * escaped commas (i.e., splits at ',' not '\,').
238     * 
239     * @access public
240     * 
241     * @param string $text The string to split into an array.
242     * 
243     * @param bool $convertSingle If splitting the string results in a
244     * single array element, return a string instead of a one-element
245     * array.
246     * 
247     * @return mixed An array of values, or a single string.
248     * 
249     */
250     
251     function splitByComma($text, $convertSingle = false)
252     {
253         // we use these double-backs (\\) because they get get converted
254         // to single-backs (\) by preg_split.  the quad-backs (\\\\) end
255         // up as as double-backs (\\), which is what preg_split requires
256         // to indicate a single backslash (\). ye gods, how ugly.
257         $regex = '(?<!\\\\)(\,)';
258         $tmp = preg_split("/$regex/i", $text);
259         
260         // if there is only one array-element and $convertSingle is
261         // true, then return only the value of that one array element
262         // (instead of returning the array).
263         if ($convertSingle && count($tmp) == 1) {
264             return $tmp[0];
265         } else {
266             return $tmp;
267         }
268     }
269     
270     
271     /**
272     * 
273     * Used to make string human-readable after being a vCard value.
274     * 
275     * Converts...
276     *     \: => :
277     *     \; => ;
278     *     \, => ,
279     *     literal \n => newline
280     * 
281     * @access public
282     * 
283     * @param mixed $text The text to unescape.
284     * 
285     * @return void
286     * 
287     */
288     
289     function unescape(&$text)
290     {
291         if (is_array($text)) {
292             foreach ($text as $key => $val) {
293                 $this->unescape($val);
294                 $text[$key] = $val;
295             }
296         } else {
297             $text = str_replace('\:', ':', $text);
298             $text = str_replace('\;', ';', $text);
299             $text = str_replace('\,', ',', $text);
300             $text = str_replace('\n', "\n", $text);
301         }
302     }
303     
304     
305     /**
306     *
307     * Emulated destructor.
308     *
309     * @access private
310     * @return boolean true
311     *
312     */
313     
314     function _Contact_Vcard_Parse()
315     {
316         return true;
317     }
318     
319     
320     /**
321     * 
322     * Parses an array of source lines and returns an array of vCards.
323     * Each element of the array is itself an array expressing the types,
324     * parameters, and values of each part of the vCard. Processes both
325     * 2.1 and 3.0 vCard sources.
326     *
327     * @access private
328     * 
329     * @param array $source An array of lines to be read for vCard
330     * information.
331     * 
332     * @return array An array of of vCard information extracted from the
333     * source array.
334     * 
335     */
336     
337     function _fromArray($source, $decode_qp = true)
338     {
339         // the info array will hold all resulting vCard information.
340         $info = array();
341         
342         // tells us whether the source text indicates the beginning of a
343         // new vCard with a BEGIN:VCARD tag.
344         $begin = false;
345         
346         // holds information about the current vCard being read from the
347         // source text.
348         $card = array();
349         
350         // loop through each line in the source array
351         foreach ($source as $line) {
352             
353             // if the line is blank, skip it.
354             if (trim($line) == '') {
355                 continue;
356             }
357             
358             // find the first instance of ':' on the line.  The part
359             // to the left of the colon is the type and parameters;
360             // the part to the right of the colon is the value data.
361             $pos = strpos($line, ':');
362             
363             // if there is no colon, skip the line.
364             if ($pos === false) {
365                 continue;
366             }
367             
368             // get the left and right portions
369             $left = trim(substr($line, 0, $pos));
370             $right = trim(substr($line, $pos+1, strlen($line)));
371             
372             // have we started yet?
373             if (! $begin) {
374                 
375                 // nope.  does this line indicate the beginning of
376                 // a new vCard?
377                 if (strtoupper($left) == 'BEGIN' &&
378                     strtoupper($right) == 'VCARD') {
379                     
380                     // tell the loop that we've begun a new card
381                     $begin = true;
382                 }
383                 
384                 // regardless, loop to the next line of source. if begin
385                 // is still false, the next loop will check the line. if
386                 // begin has now been set to true, the loop will start
387                 // collecting card info.
388                 continue;
389                 
390             } else {
391                 
392                 // yep, we've started, but we don't know how far along
393                 // we are in the card. is this the ending line of the
394                 // current vCard?
395                 if (strtoupper($left) == 'END' &&
396                     strtoupper($right) == 'VCARD') {
397                     
398                     // yep, we're done. keep the info from the current
399                     // card...
400                     $info[] = $card;
401                     
402                     // ...and reset to grab a new card if one exists in
403                     // the source array.
404                     $begin = false;
405                     $card = array();
406                     
407                 } else {
408                     
409                     // we're not on an ending line, so collect info from
410                     // this line into the current card. split the
411                     // left-portion of the line into a type-definition
412                     // (the kind of information) and parameters for the
413                     // type.
414                     $typedef = $this->_getTypeDef($left);
415                     $params = $this->_getParams($left);
416                     
417                     // if we are decoding quoted-printable, do so now.
418                     // QUOTED-PRINTABLE is not allowed in version 3.0,
419                     // but we don't check for versioning, so we do it
420                     // regardless.  ;-)
421                     $this->_decode_qp($params, $right);
422                     
423                     // now get the value-data from the line, based on
424                     // the typedef
425                     switch ($typedef) {
426                         
427                     case 'N':
428                         // structured name of the person
429                         $value = $this->_parseN($right);
430                         break;
431                         
432                     case 'ADR':
433                         // structured address of the person
434                         $value = $this->_parseADR($right);
435                         break;
436                         
437                     case 'NICKNAME':
438                         // nicknames
439                         $value = $this->_parseNICKNAME($right);
440                         break;
441                         
442                     case 'ORG':
443                         // organizations the person belongs to
444                         $value = $this->_parseORG($right);
445                         break;
446                         
447                     case 'CATEGORIES':
448                         // categories to which this card is assigned
449                         $value = $this->_parseCATEGORIES($right);
450                         break;
451                         
452                     case 'GEO':
453                         // geographic coordinates
454                         $value = $this->_parseGEO($right);
455                         break;
456                         
457                     default:
458                         // by default, just grab the plain value. keep
459                         // as an array to make sure *all* values are
460                         // arrays.  for consistency. ;-)
461                         $value = array(array($right));
462                         break;
463                     }
464                     
465                     // add the type, parameters, and value to the
466                     // current card array.  note that we allow multiple
467                     // instances of the same type, which might be dumb
468                     // in some cases (e.g., N).
469                     $card[$typedef][] = array(
470                         'param' => $params,
471                         'value' => $value
472                     );
473                 }
474             }
475         }
476         
477         $this->unescape($info);
478         return $info;
479     }
480     
481     
482     /**
483     *
484     * Takes a vCard line and extracts the Type-Definition for the line.
485     * 
486     * @access private
487     *
488     * @param string $text A left-part (before-the-colon part) from a
489     * vCard line.
490     * 
491     * @return string The type definition for the line.
492     * 
493     */
494     
495     function _getTypeDef($text)
496     {
497         // split the text by semicolons
498         $split = $this->splitBySemi($text);
499         
500         // only return first element (the typedef)
501         return strtoupper($split[0]);
502     }
503     
504     
505     /**
506     *
507     * Finds the Type-Definition parameters for a vCard line.
508     * 
509     * @access private
510     * 
511     * @param string $text A left-part (before-the-colon part) from a
512     * vCard line.
513     * 
514     * @return mixed An array of parameters.
515     * 
516     */
517     
518     function _getParams($text)
519     {
520         // split the text by semicolons into an array
521         $split = $this->splitBySemi($text);
522         
523         // drop the first element of the array (the type-definition)
524         array_shift($split);
525         
526         // set up an array to retain the parameters, if any
527         $params = array();
528         
529         // loop through each parameter.  the params may be in the format...
530         // "TYPE=type1,type2,type3"
531         //    ...or...
532         // "TYPE=type1;TYPE=type2;TYPE=type3"
533         foreach ($split as $full) {
534             
535             // split the full parameter at the equal sign so we can tell
536             // the parameter name from the parameter value
537             $tmp = explode("=", $full);
538             
539             // the key is the left portion of the parameter (before
540             // '='). if in 2.1 format, the key may in fact be the
541             // parameter value, not the parameter name.
542             $key = strtoupper(trim($tmp[0]));
543             
544             // get the parameter name by checking to see if it's in
545             // vCard 2.1 or 3.0 format.
546             $name = $this->_getParamName($key);
547             
548             // list of all parameter values
549             $listall = trim($tmp[1]);
550             
551             // if there is a value-list for this parameter, they are
552             // separated by commas, so split them out too.
553             $list = $this->splitByComma($listall);
554             
555             // now loop through each value in the parameter and retain
556             // it.  if the value is blank, that means it's a 2.1-style
557             // param, and the key itself is the value.
558             foreach ($list as $val) {
559                 if (trim($val) != '') {
560                     // 3.0 formatted parameter
561                     $params[$name][] = trim($val);
562                 } else {
563                     // 2.1 formatted parameter
564                     $params[$name][] = $key;
565                 }
566             }
567             
568             // if, after all this, there are no parameter values for the
569             // parameter name, retain no info about the parameter (saves
570             // ram and checking-time later).
571             if (count($params[$name]) == 0) {
572                 unset($params[$name]);
573             }
574         }
575         
576         // return the parameters array.
577         return $params;
578     }
579     
580     
581     /**
582     *
583     * Looks at the parameters of a vCard line; if one of them is
584     * ENCODING[] => QUOTED-PRINTABLE then decode the text in-place.
585     * 
586     * @access private
587     * 
588     * @param array $params A parameter array from a vCard line.
589     * 
590     * @param string $text A right-part (after-the-colon part) from a
591     * vCard line.
592     *
593     * @return void
594     * 
595     */
596     
597     function _decode_qp(&$params, &$text)
598     {
599         // loop through each parameter
600         foreach ($params as $param_key => $param_val) {
601             
602             // check to see if it's an encoding param
603             if (trim(strtoupper($param_key)) == 'ENCODING') {
604             
605                 // loop through each encoding param value
606                 foreach ($param_val as $enc_key => $enc_val) {
607                 
608                     // if any of the values are QP, decode the text
609                     // in-place and return
610                     if (trim(strtoupper($enc_val)) == 'QUOTED-PRINTABLE') {
611                         $text = quoted_printable_decode($text);
612                         return;
613                     }
614                 }
615             }
616         }
617     }
618     
619     
620     /**
621     * 
622     * Returns parameter names from 2.1-formatted vCards.
623     *
624     * The vCard 2.1 specification allows parameter values without a
625     * name. The parameter name is then determined from the unique
626     * parameter value.
627     * 
628     * Shamelessly lifted from Frank Hellwig <frank@hellwig.org> and his
629     * vCard PHP project <http://vcardphp.sourceforge.net>.
630     * 
631     * @access private
632     * 
633     * @param string $value The first element in a parameter name-value
634     * pair.
635     * 
636     * @return string The proper parameter name (TYPE, ENCODING, or
637     * VALUE).
638     * 
639     */
640      
641     function _getParamName($value)
642     {
643         static $types = array (
644             'DOM', 'INTL', 'POSTAL', 'PARCEL','HOME', 'WORK',
645             'PREF', 'VOICE', 'FAX', 'MSG', 'CELL', 'PAGER',
646             'BBS', 'MODEM', 'CAR', 'ISDN', 'VIDEO',
647             'AOL', 'APPLELINK', 'ATTMAIL', 'CIS', 'EWORLD',
648             'INTERNET', 'IBMMAIL', 'MCIMAIL',
649             'POWERSHARE', 'PRODIGY', 'TLX', 'X400',
650             'GIF', 'CGM', 'WMF', 'BMP', 'MET', 'PMB', 'DIB',
651             'PICT', 'TIFF', 'PDF', 'PS', 'JPEG', 'QTIME',
652             'MPEG', 'MPEG2', 'AVI',
653             'WAVE', 'AIFF', 'PCM',
654             'X509', 'PGP'
655         );
656         
657         // CONTENT-ID added by pmj
658         static $values = array (
659             'INLINE', 'URL', 'CID', 'CONTENT-ID'
660         );
661         
662         // 8BIT added by pmj
663         static $encodings = array (
664             '7BIT', '8BIT', 'QUOTED-PRINTABLE', 'BASE64'
665         );
666         
667         // changed by pmj to the following so that the name defaults to
668         // whatever the original value was.  Frank Hellwig's original
669         // code was "$name = 'UNKNOWN'".
670         $name = $value;
671         
672         if (in_array($value, $types)) {
673             $name = 'TYPE';
674         } elseif (in_array($value, $values)) {
675             $name = 'VALUE';
676         } elseif (in_array($value, $encodings)) {
677             $name = 'ENCODING';
678         }
679         
680         return $name;
681     }
682     
683     
684     /**
685     *
686     * Parses a vCard line value identified as being of the "N"
687     * (structured name) type-defintion.
688     *
689     * @access private
690     *
691     * @param string $text The right-part (after-the-colon part) of a
692     * vCard line.
693     * 
694     * @return array An array of key-value pairs where the key is the
695     * portion-name and the value is the portion-value.  The value itself
696     * may be an array as well if multiple comma-separated values were
697     * indicated in the vCard source.
698     *
699     */
700     
701     function _parseN($text)
702     {
703         // make sure there are always at least 5 elements
704         $tmp = array_pad($this->splitBySemi($text), 5, '');
705         return array(
706             $this->splitByComma($tmp[0]), // family (last)
707             $this->splitByComma($tmp[1]), // given (first)
708             $this->splitByComma($tmp[2]), // addl (middle)
709             $this->splitByComma($tmp[3]), // prefix
710             $this->splitByComma($tmp[4])  // suffix
711         );
712     }
713     
714     
715     /**
716     *
717     * Parses a vCard line value identified as being of the "ADR"
718     * (structured address) type-defintion.
719     *
720     * @access private
721     *
722     * @param string $text The right-part (after-the-colon part) of a
723     * vCard line.
724     * 
725     * @return array An array of key-value pairs where the key is the
726     * portion-name and the value is the portion-value.  The value itself
727     * may be an array as well if multiple comma-separated values were
728     * indicated in the vCard source.
729     *
730     */
731     
732     function _parseADR($text)
733     {
734         // make sure there are always at least 7 elements
735         $tmp = array_pad($this->splitBySemi($text), 7, '');
736         return array(
737             $this->splitByComma($tmp[0]), // pob
738             $this->splitByComma($tmp[1]), // extend
739             $this->splitByComma($tmp[2]), // street
740             $this->splitByComma($tmp[3]), // locality (city)
741             $this->splitByComma($tmp[4]), // region (state)
742             $this->splitByComma($tmp[5]), // postcode (ZIP)
743             $this->splitByComma($tmp[6])  // country
744         );
745     }
746     
747     
748     /**
749     * 
750     * Parses a vCard line value identified as being of the "NICKNAME"
751     * (informal or descriptive name) type-defintion.
752     *
753     * @access private
754     * 
755     * @param string $text The right-part (after-the-colon part) of a
756     * vCard line.
757     * 
758     * @return array An array of nicknames.
759     *
760     */
761     
762     function _parseNICKNAME($text)
763     {
764         return array($this->splitByComma($text));
765     }
766     
767     
768     /**
769     * 
770     * Parses a vCard line value identified as being of the "ORG"
771     * (organizational info) type-defintion.
772     *
773     * @access private
774     *
775     * @param string $text The right-part (after-the-colon part) of a
776     * vCard line.
777     * 
778     * @return array An array of organizations; each element of the array
779     * is itself an array, which indicates primary organization and
780     * sub-organizations.
781     *
782     */
783     
784     function _parseORG($text)
785     {
786         $tmp = $this->splitbySemi($text);
787         $list = array();
788         foreach ($tmp as $val) {
789             $list[] = array($val);
790         }
791         
792         return $list;
793     }
794     
795     
796     /**
797     * 
798     * Parses a vCard line value identified as being of the "CATEGORIES"
799     * (card-category) type-defintion.
800     *
801     * @access private
802     * 
803     * @param string $text The right-part (after-the-colon part) of a
804     * vCard line.
805     * 
806     * @return mixed An array of categories.
807     *
808     */
809     
810     function _parseCATEGORIES($text)
811     {
812         return array($this->splitByComma($text));
813     }
814     
815     
816     /**
817     * 
818     * Parses a vCard line value identified as being of the "GEO"
819     * (geographic coordinate) type-defintion.
820     *
821     * @access private
822     *
823     * @param string $text The right-part (after-the-colon part) of a
824     * vCard line.
825     * 
826     * @return mixed An array of lat-lon geocoords.
827     *
828     */
829     
830     function _parseGEO($text)
831     {
832         // make sure there are always at least 2 elements
833         $tmp = array_pad($this->splitBySemi($text), 2, '');
834         return array(
835             array($tmp[0]), // lat
836             array($tmp[1])  // lon
837         );
838     }
839 }
840
841 ?>