PageRenderTime 48ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 0ms

/vCard.php

http://github.com/nuovo/vCard-parser
PHP | 689 lines | 478 code | 81 blank | 130 comment | 84 complexity | 708ed507dc6c50f7431bec6cc4b24c0a MD5 | raw file
Possible License(s): MIT
  1. <?php
  2. /**
  3. * vCard class for parsing a vCard and/or creating one
  4. *
  5. * @link https://github.com/nuovo/vCard-parser
  6. * @author Martins Pilsetnieks, Roberts Bruveris
  7. * @see RFC 2426, RFC 2425
  8. * @version 0.4.8
  9. */
  10. class vCard implements Countable, Iterator
  11. {
  12. const MODE_ERROR = 'error';
  13. const MODE_SINGLE = 'single';
  14. const MODE_MULTIPLE = 'multiple';
  15. const endl = "\n";
  16. /**
  17. * @var string Current object mode - error, single or multiple (for a single vCard within a file and multiple combined vCards)
  18. */
  19. private $Mode; //single, multiple, error
  20. private $Path = '';
  21. private $RawData = '';
  22. /**
  23. * @var array Internal options container. Options:
  24. * bool Collapse: If true, elements that can have multiple values but have only a single value are returned as that value instead of an array
  25. * If false, an array is returned even if it has only one value.
  26. */
  27. private $Options = array(
  28. 'Collapse' => false
  29. );
  30. /**
  31. * @var array Internal data container. Contains vCard objects for multiple vCards and just the data for single vCards.
  32. */
  33. private $Data = array();
  34. /**
  35. * @static Parts of structured elements according to the spec.
  36. */
  37. private static $Spec_StructuredElements = array(
  38. 'n' => array('LastName', 'FirstName', 'AdditionalNames', 'Prefixes', 'Suffixes'),
  39. 'adr' => array('POBox', 'ExtendedAddress', 'StreetAddress', 'Locality', 'Region', 'PostalCode', 'Country'),
  40. 'geo' => array('Latitude', 'Longitude'),
  41. 'org' => array('Name', 'Unit1', 'Unit2')
  42. );
  43. private static $Spec_MultipleValueElements = array('nickname', 'categories');
  44. private static $Spec_ElementTypes = array(
  45. 'email' => array('internet', 'x400', 'pref'),
  46. 'adr' => array('dom', 'intl', 'postal', 'parcel', 'home', 'work', 'pref'),
  47. 'label' => array('dom', 'intl', 'postal', 'parcel', 'home', 'work', 'pref'),
  48. 'tel' => array('home', 'msg', 'work', 'pref', 'voice', 'fax', 'cell', 'video', 'pager', 'bbs', 'modem', 'car', 'isdn', 'pcs'),
  49. 'impp' => array('personal', 'business', 'home', 'work', 'mobile', 'pref')
  50. );
  51. private static $Spec_FileElements = array('photo', 'logo', 'sound');
  52. /**
  53. * vCard constructor
  54. *
  55. * @param string Path to file, optional.
  56. * @param string Raw data, optional.
  57. * @param array Additional options, optional. Currently supported options:
  58. * bool Collapse: If true, elements that can have multiple values but have only a single value are returned as that value instead of an array
  59. * If false, an array is returned even if it has only one value.
  60. *
  61. * One of these parameters must be provided, otherwise an exception is thrown.
  62. */
  63. public function __construct($Path = false, $RawData = false, array $Options = null)
  64. {
  65. // Checking preconditions for the parser.
  66. // If path is given, the file should be accessible.
  67. // If raw data is given, it is taken as it is.
  68. // In both cases the real content is put in $this -> RawData
  69. if ($Path)
  70. {
  71. if (!is_readable($Path))
  72. {
  73. throw new Exception('vCard: Path not accessible ('.$Path.')');
  74. }
  75. $this -> Path = $Path;
  76. $this -> RawData = file_get_contents($this -> Path);
  77. }
  78. elseif ($RawData)
  79. {
  80. $this -> RawData = $RawData;
  81. }
  82. else
  83. {
  84. //throw new Exception('vCard: No content provided');
  85. // Not necessary anymore as possibility to create vCards is added
  86. }
  87. if (!$this -> Path && !$this -> RawData)
  88. {
  89. return true;
  90. }
  91. if ($Options)
  92. {
  93. $this -> Options = array_merge($this -> Options, $Options);
  94. }
  95. // Counting the begin/end separators. If there aren't any or the count doesn't match, there is a problem with the file.
  96. // If there is only one, this is a single vCard, if more, multiple vCards are combined.
  97. $Matches = array();
  98. $vCardBeginCount = preg_match_all('{^BEGIN\:VCARD}miS', $this -> RawData, $Matches);
  99. $vCardEndCount = preg_match_all('{^END\:VCARD}miS', $this -> RawData, $Matches);
  100. if (($vCardBeginCount != $vCardEndCount) || !$vCardBeginCount)
  101. {
  102. $this -> Mode = vCard::MODE_ERROR;
  103. throw new Exception('vCard: invalid vCard');
  104. }
  105. $this -> Mode = $vCardBeginCount == 1 ? vCard::MODE_SINGLE : vCard::MODE_MULTIPLE;
  106. // Removing/changing inappropriate newlines, i.e., all CRs or multiple newlines are changed to a single newline
  107. $this -> RawData = str_replace("\r", "\n", $this -> RawData);
  108. $this -> RawData = preg_replace('{(\n+)}', "\n", $this -> RawData);
  109. // In multiple card mode the raw text is split at card beginning markers and each
  110. // fragment is parsed in a separate vCard object.
  111. if ($this -> Mode == self::MODE_MULTIPLE)
  112. {
  113. $this -> RawData = explode('BEGIN:VCARD', $this -> RawData);
  114. $this -> RawData = array_filter($this -> RawData);
  115. foreach ($this -> RawData as $SinglevCardRawData)
  116. {
  117. // Prepending "BEGIN:VCARD" to the raw string because we exploded on that one.
  118. // If there won't be the BEGIN marker in the new object, it will fail.
  119. $SinglevCardRawData = 'BEGIN:VCARD'."\n".$SinglevCardRawData;
  120. $ClassName = get_class($this);
  121. $this -> Data[] = new $ClassName(false, $SinglevCardRawData);
  122. }
  123. }
  124. else
  125. {
  126. // Protect the BASE64 final = sign (detected by the line beginning with whitespace), otherwise the next replace will get rid of it
  127. $this -> RawData = preg_replace('{(\n\s.+)=(\n)}', '$1-base64=-$2', $this -> RawData);
  128. // Joining multiple lines that are split with a hard wrap and indicated by an equals sign at the end of line
  129. // (quoted-printable-encoded values in v2.1 vCards)
  130. $this -> RawData = str_replace("=\n", '', $this -> RawData);
  131. // Joining multiple lines that are split with a soft wrap (space or tab on the beginning of the next line
  132. $this -> RawData = str_replace(array("\n ", "\n\t"), '-wrap-', $this -> RawData);
  133. // Restoring the BASE64 final equals sign (see a few lines above)
  134. $this -> RawData = str_replace("-base64=-\n", "=\n", $this -> RawData);
  135. $Lines = explode("\n", $this -> RawData);
  136. foreach ($Lines as $Line)
  137. {
  138. // Lines without colons are skipped because, most likely, they contain no data.
  139. if (strpos($Line, ':') === false)
  140. {
  141. continue;
  142. }
  143. // Each line is split into two parts. The key contains the element name and additional parameters, if present,
  144. // value is just the value
  145. list($Key, $Value) = explode(':', $Line, 2);
  146. // Key is transformed to lowercase because, even though the element and parameter names are written in uppercase,
  147. // it is quite possible that they will be in lower- or mixed case.
  148. // The key is trimmed to allow for non-significant WSP characters as allowed by v2.1
  149. $Key = strtolower(trim(self::Unescape($Key)));
  150. // These two lines can be skipped as they aren't necessary at all.
  151. if ($Key == 'begin' || $Key == 'end')
  152. {
  153. continue;
  154. }
  155. if ((strpos($Key, 'agent') === 0) && (stripos($Value, 'begin:vcard') !== false))
  156. {
  157. $ClassName = get_class($this);
  158. $Value = new $ClassName(false, str_replace('-wrap-', "\n", $Value));
  159. if (!isset($this -> Data[$Key]))
  160. {
  161. $this -> Data[$Key] = array();
  162. }
  163. $this -> Data[$Key][] = $Value;
  164. continue;
  165. }
  166. else
  167. {
  168. $Value = str_replace('-wrap-', '', $Value);
  169. }
  170. $Value = trim(self::Unescape($Value));
  171. $Type = array();
  172. // Here additional parameters are parsed
  173. $KeyParts = explode(';', $Key);
  174. $Key = $KeyParts[0];
  175. $Encoding = false;
  176. if (strpos($Key, 'item') === 0)
  177. {
  178. $TmpKey = explode('.', $Key, 2);
  179. $Key = $TmpKey[1];
  180. $ItemIndex = (int)str_ireplace('item', '', $TmpKey[0]);
  181. }
  182. if (count($KeyParts) > 1)
  183. {
  184. $Parameters = self::ParseParameters($Key, array_slice($KeyParts, 1));
  185. foreach ($Parameters as $ParamKey => $ParamValue)
  186. {
  187. switch ($ParamKey)
  188. {
  189. case 'encoding':
  190. $Encoding = $ParamValue;
  191. if (in_array($ParamValue, array('b', 'base64')))
  192. {
  193. //$Value = base64_decode($Value);
  194. }
  195. elseif ($ParamValue == 'quoted-printable') // v2.1
  196. {
  197. $Value = quoted_printable_decode($Value);
  198. }
  199. break;
  200. case 'charset': // v2.1
  201. if ($ParamValue != 'utf-8' && $ParamValue != 'utf8')
  202. {
  203. $Value = mb_convert_encoding($Value, 'UTF-8', $ParamValue);
  204. }
  205. break;
  206. case 'type':
  207. $Type = $ParamValue;
  208. break;
  209. }
  210. }
  211. }
  212. // Checking files for colon-separated additional parameters (Apple's Address Book does this), for example, "X-ABCROP-RECTANGLE" for photos
  213. if (in_array($Key, self::$Spec_FileElements) && isset($Parameters['encoding']) && in_array($Parameters['encoding'], array('b', 'base64')))
  214. {
  215. // If colon is present in the value, it must contain Address Book parameters
  216. // (colon is an invalid character for base64 so it shouldn't appear in valid files)
  217. if (strpos($Value, ':') !== false)
  218. {
  219. $Value = explode(':', $Value);
  220. $Value = array_pop($Value);
  221. }
  222. }
  223. // Values are parsed according to their type
  224. if (isset(self::$Spec_StructuredElements[$Key]))
  225. {
  226. $Value = self::ParseStructuredValue($Value, $Key);
  227. if ($Type)
  228. {
  229. $Value['Type'] = $Type;
  230. }
  231. }
  232. else
  233. {
  234. if (in_array($Key, self::$Spec_MultipleValueElements))
  235. {
  236. $Value = self::ParseMultipleTextValue($Value, $Key);
  237. }
  238. if ($Type)
  239. {
  240. $Value = array(
  241. 'Value' => $Value,
  242. 'Type' => $Type
  243. );
  244. }
  245. }
  246. if (is_array($Value) && $Encoding)
  247. {
  248. $Value['Encoding'] = $Encoding;
  249. }
  250. if (!isset($this -> Data[$Key]))
  251. {
  252. $this -> Data[$Key] = array();
  253. }
  254. $this -> Data[$Key][] = $Value;
  255. }
  256. }
  257. }
  258. /**
  259. * Magic method to get the various vCard values as object members, e.g.
  260. * a call to $vCard -> N gets the "N" value
  261. *
  262. * @param string Key
  263. *
  264. * @return mixed Value
  265. */
  266. public function __get($Key)
  267. {
  268. $Key = strtolower($Key);
  269. if (isset($this -> Data[$Key]))
  270. {
  271. if ($Key == 'agent')
  272. {
  273. return $this -> Data[$Key];
  274. }
  275. elseif (in_array($Key, self::$Spec_FileElements))
  276. {
  277. $Value = $this -> Data[$Key];
  278. foreach ($Value as $K => $V)
  279. {
  280. if (stripos($V['Value'], 'uri:') === 0)
  281. {
  282. $Value[$K]['Value'] = substr($V, 4);
  283. $Value[$K]['Encoding'] = 'uri';
  284. }
  285. }
  286. return $Value;
  287. }
  288. if ($this -> Options['Collapse'] && is_array($this -> Data[$Key]) && (count($this -> Data[$Key]) == 1))
  289. {
  290. return $this -> Data[$Key][0];
  291. }
  292. return $this -> Data[$Key];
  293. }
  294. elseif ($Key == 'Mode')
  295. {
  296. return $this -> Mode;
  297. }
  298. return array();
  299. }
  300. /**
  301. * Saves an embedded file
  302. *
  303. * @param string Key
  304. * @param int Index of the file, defaults to 0
  305. * @param string Target path where the file should be saved, including the filename
  306. *
  307. * @return bool Operation status
  308. */
  309. public function SaveFile($Key, $Index = 0, $TargetPath = '')
  310. {
  311. if (!isset($this -> Data[$Key]))
  312. {
  313. return false;
  314. }
  315. if (!isset($this -> Data[$Key][$Index]))
  316. {
  317. return false;
  318. }
  319. // Returing false if it is an image URL
  320. if (stripos($this -> Data[$Key][$Index]['Value'], 'uri:') === 0)
  321. {
  322. return false;
  323. }
  324. if (is_writable($TargetPath) || (!file_exists($TargetPath) && is_writable(dirname($TargetPath))))
  325. {
  326. $RawContent = $this -> Data[$Key][$Index]['Value'];
  327. if (isset($this -> Data[$Key][$Index]['Encoding']) && $this -> Data[$Key][$Index]['Encoding'] == 'b')
  328. {
  329. $RawContent = base64_decode($RawContent);
  330. }
  331. $Status = file_put_contents($TargetPath, $RawContent);
  332. return (bool)$Status;
  333. }
  334. else
  335. {
  336. throw new Exception('vCard: Cannot save file ('.$Key.'), target path not writable ('.$TargetPath.')');
  337. }
  338. return false;
  339. }
  340. /**
  341. * Magic method for adding data to the vCard
  342. *
  343. * @param string Key
  344. * @param string Method call arguments. First element is value.
  345. *
  346. * @return vCard Current object for method chaining
  347. */
  348. public function __call($Key, $Arguments)
  349. {
  350. $Key = strtolower($Key);
  351. if (!isset($this -> Data[$Key]))
  352. {
  353. $this -> Data[$Key] = array();
  354. }
  355. $Value = isset($Arguments[0]) ? $Arguments[0] : false;
  356. if (count($Arguments) > 1)
  357. {
  358. $Types = array_values(array_slice($Arguments, 1));
  359. if (isset(self::$Spec_StructuredElements[$Key]) &&
  360. in_array($Arguments[1], self::$Spec_StructuredElements[$Key])
  361. )
  362. {
  363. $LastElementIndex = 0;
  364. if (count($this -> Data[$Key]))
  365. {
  366. $LastElementIndex = count($this -> Data[$Key]) - 1;
  367. }
  368. if (isset($this -> Data[$Key][$LastElementIndex]))
  369. {
  370. if (empty($this -> Data[$Key][$LastElementIndex][$Types[0]]))
  371. {
  372. $this -> Data[$Key][$LastElementIndex][$Types[0]] = $Value;
  373. }
  374. else
  375. {
  376. $LastElementIndex++;
  377. }
  378. }
  379. if (!isset($this -> Data[$Key][$LastElementIndex]))
  380. {
  381. $this -> Data[$Key][$LastElementIndex] = array(
  382. $Types[0] => $Value
  383. );
  384. }
  385. }
  386. elseif (isset(self::$Spec_ElementTypes[$Key]))
  387. {
  388. $this -> Data[$Key][] = array(
  389. 'Value' => $Value,
  390. 'Type' => $Types
  391. );
  392. }
  393. }
  394. elseif ($Value)
  395. {
  396. $this -> Data[$Key][] = $Value;
  397. }
  398. return $this;
  399. }
  400. /**
  401. * Magic method for getting vCard content out
  402. *
  403. * @return string Raw vCard content
  404. */
  405. public function __toString()
  406. {
  407. $Text = 'BEGIN:VCARD'.self::endl;
  408. $Text .= 'VERSION:3.0'.self::endl;
  409. foreach ($this -> Data as $Key => $Values)
  410. {
  411. $KeyUC = strtoupper($Key);
  412. $Key = strtolower($Key);
  413. if (in_array($KeyUC, array('PHOTO', 'VERSION')))
  414. {
  415. continue;
  416. }
  417. foreach ($Values as $Index => $Value)
  418. {
  419. $Text .= $KeyUC;
  420. if (is_array($Value) && isset($Value['Type']))
  421. {
  422. $Text .= ';TYPE='.self::PrepareTypeStrForOutput($Value['Type']);
  423. }
  424. $Text .= ':';
  425. if (isset(self::$Spec_StructuredElements[$Key]))
  426. {
  427. $PartArray = array();
  428. foreach (self::$Spec_StructuredElements[$Key] as $Part)
  429. {
  430. $PartArray[] = isset($Value[$Part]) ? $Value[$Part] : '';
  431. }
  432. $Text .= implode(';', $PartArray);
  433. }
  434. elseif (is_array($Value) && isset(self::$Spec_ElementTypes[$Key]))
  435. {
  436. $Text .= $Value['Value'];
  437. }
  438. else
  439. {
  440. $Text .= $Value;
  441. }
  442. $Text .= self::endl;
  443. }
  444. }
  445. $Text .= 'END:VCARD'.self::endl;
  446. return $Text;
  447. }
  448. // !Helper methods
  449. private static function PrepareTypeStrForOutput($Type)
  450. {
  451. return implode(',', array_map('strtoupper', $Type));
  452. }
  453. /**
  454. * Removes the escaping slashes from the text.
  455. *
  456. * @access private
  457. *
  458. * @param string Text to prepare.
  459. *
  460. * @return string Resulting text.
  461. */
  462. private static function Unescape($Text)
  463. {
  464. return str_replace(array('\:', '\;', '\,', "\n"), array(':', ';', ',', ''), $Text);
  465. }
  466. /**
  467. * Separates the various parts of a structured value according to the spec.
  468. *
  469. * @access private
  470. *
  471. * @param string Raw text string
  472. * @param string Key (e.g., N, ADR, ORG, etc.)
  473. *
  474. * @return array Parts in an associative array.
  475. */
  476. private static function ParseStructuredValue($Text, $Key)
  477. {
  478. $Text = array_map('trim', explode(';', $Text));
  479. $Result = array();
  480. $Ctr = 0;
  481. foreach (self::$Spec_StructuredElements[$Key] as $Index => $StructurePart)
  482. {
  483. $Result[$StructurePart] = isset($Text[$Index]) ? $Text[$Index] : null;
  484. }
  485. return $Result;
  486. }
  487. /**
  488. * @access private
  489. */
  490. private static function ParseMultipleTextValue($Text)
  491. {
  492. return explode(',', $Text);
  493. }
  494. /**
  495. * @access private
  496. */
  497. private static function ParseParameters($Key, array $RawParams = null)
  498. {
  499. if (!$RawParams)
  500. {
  501. return array();
  502. }
  503. // Parameters are split into (key, value) pairs
  504. $Parameters = array();
  505. foreach ($RawParams as $Item)
  506. {
  507. $Parameters[] = explode('=', strtolower($Item));
  508. }
  509. $Type = array();
  510. $Result = array();
  511. // And each parameter is checked whether anything can/should be done because of it
  512. foreach ($Parameters as $Index => $Parameter)
  513. {
  514. // Skipping empty elements
  515. if (!$Parameter)
  516. {
  517. continue;
  518. }
  519. // Handling type parameters without the explicit TYPE parameter name (2.1 valid)
  520. if (count($Parameter) == 1)
  521. {
  522. // Checks if the type value is allowed for the specific element
  523. // The second part of the "if" statement means that email elements can have non-standard types (see the spec)
  524. if (
  525. (isset(self::$Spec_ElementTypes[$Key]) && in_array($Parameter[0], self::$Spec_ElementTypes[$Key])) ||
  526. ($Key == 'email' && is_scalar($Parameter[0]))
  527. )
  528. {
  529. $Type[] = $Parameter[0];
  530. }
  531. }
  532. elseif (count($Parameter) > 2)
  533. {
  534. $TempTypeParams = self::ParseParameters($Key, explode(',', $RawParams[$Index]));
  535. if ($TempTypeParams['type'])
  536. {
  537. $Type = array_merge($Type, $TempTypeParams['type']);
  538. }
  539. }
  540. else
  541. {
  542. switch ($Parameter[0])
  543. {
  544. case 'encoding':
  545. if (in_array($Parameter[1], array('quoted-printable', 'b', 'base64')))
  546. {
  547. $Result['encoding'] = $Parameter[1] == 'base64' ? 'b' : $Parameter[1];
  548. }
  549. break;
  550. case 'charset':
  551. $Result['charset'] = $Parameter[1];
  552. break;
  553. case 'type':
  554. $Type = array_merge($Type, explode(',', $Parameter[1]));
  555. break;
  556. case 'value':
  557. if (strtolower($Parameter[1]) == 'url')
  558. {
  559. $Result['encoding'] = 'uri';
  560. }
  561. break;
  562. }
  563. }
  564. }
  565. $Result['type'] = $Type;
  566. return $Result;
  567. }
  568. // !Interface methods
  569. // Countable interface
  570. public function count()
  571. {
  572. switch ($this -> Mode)
  573. {
  574. case self::MODE_ERROR:
  575. return 0;
  576. break;
  577. case self::MODE_SINGLE:
  578. return 1;
  579. break;
  580. case self::MODE_MULTIPLE:
  581. return count($this -> Data);
  582. break;
  583. }
  584. return 0;
  585. }
  586. // Iterator interface
  587. public function rewind()
  588. {
  589. reset($this -> Data);
  590. }
  591. public function current()
  592. {
  593. return current($this -> Data);
  594. }
  595. public function next()
  596. {
  597. return next($this -> Data);
  598. }
  599. public function valid()
  600. {
  601. return ($this -> current() !== false);
  602. }
  603. public function key()
  604. {
  605. return key($this -> Data);
  606. }
  607. }
  608. ?>