From f9018f33a6d3ea0cd155aace93090829e4f5f16f Mon Sep 17 00:00:00 2001 From: Wouter Admiraal Date: Mon, 30 May 2016 12:39:43 +0200 Subject: [PATCH 1/2] Add VCard parsing Add the VCardParser class, which can parse VCard files to simple data structures. Parsed properties are the same as the ones that the VCard class can export (for now). Add unit tests, testing the full range of the exporter class. DISCLAIMER: the code in VCardParser was heavily inspired by the Zendvcard project (now abandoned). Please see the copyright notice in src/VCardParser.php for more information. Old project URL: https://code.google.com/archive/p/zendvcard/ --- src/VCardParser.php | 261 ++++++++++++++++++++++++++++++++++++++ tests/VCardParserTest.php | 204 +++++++++++++++++++++++++++++ tests/example.vcf | 6 + 3 files changed, 471 insertions(+) create mode 100644 src/VCardParser.php create mode 100644 tests/VCardParserTest.php create mode 100644 tests/example.vcf diff --git a/src/VCardParser.php b/src/VCardParser.php new file mode 100644 index 0000000..260e132 --- /dev/null +++ b/src/VCardParser.php @@ -0,0 +1,261 @@ + + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Changes by: Wouter Admiraal + * Original code is available at: http://code.google.com/p/zendvcard/ + */ + +/** + * VCard PHP Class to parse .vcard files. + * + * This class is heavily based on the Zendvcard project (seemingly abandoned), + * which is licensed under the Apache 2.0 license. + * More information can be found at https://code.google.com/archive/p/zendvcard/ + * + * @author Thomas Schaaf + * @author ruzicka.jan + * @author Wouter Admiraal + */ +class VCardParser +{ + + /** + * The raw VCard content. + * + * @var string + */ + protected $content; + + /** + * The VCard data objects. + * + * @var array + */ + protected $vcardObjects; + + /** + * Helper function to parse a file directly. + * + * @param string $filename + * + * @return JeroenDesloovere\VCard\VCardParser + */ + public static function parseFromFile($filename) + { + if (file_exists($filename) && is_readable($filename)) { + return new VCardParser(file_get_contents($filename)); + } else { + throw new \RuntimeException(sprintf("File %s is not readable, or doesn't exist.", $filename)); + } + } + + public function __construct($content) + { + $this->content = $content; + $this->vcardObjects = array(); + $this->parse(); + } + + /** + * Fetch the imported VCard at the specified index. + * + * @throws OutOfBoundsException + * + * @param int $i + * + * @return stdClass + * The card data object. + */ + public function getCardAtIndex($i) + { + if (isset($this->vcardObjects[$i])) { + return $this->vcardObjects[$i]; + } + throw new \OutOfBoundsException(); + } + + /** + * Start the parsing process. + * + * This method will populate the data object. + */ + protected function parse() + { + // Normalize new lines. + $this->content = str_replace(array("\r\n", "\r"), "\n", $this->content); + + // RFC2425 5.8.1. Line delimiting and folding + // Unfolding is accomplished by regarding CRLF immediately followed by + // a white space character (namely HTAB ASCII decimal 9 or. SPACE ASCII + // decimal 32) as equivalent to no characters at all (i.e., the CRLF + // and single white space character are removed). + $this->content = preg_replace("/\n(?:[ \t])/", "", $this->content); + $lines = explode("\n", $this->content); + + // Parse the VCard, line by line. + foreach ($lines as $line) { + $line = trim($line); + + if (strtoupper($line) == "BEGIN:VCARD") { + $cardData = new \stdClass(); + } elseif (strtoupper($line) == "END:VCARD") { + $this->vcardObjects[] = $cardData; + } elseif (!empty($line)) { + $type = ''; + $value = ''; + @list($type, $value) = explode(':', $line, 2); + + $types = explode(';', $type); + $element = strtoupper($types[0]); + + array_shift($types); + $i = 0; + $rawValue = false; + foreach ($types as $type) { + if (preg_match('/base64/', strtolower($type))) { + $value = base64_decode($value); + unset($types[$i]); + $rawValue = true; + } elseif (preg_match('/encoding=b/', strtolower($type))) { + $value = base64_decode($value); + unset($types[$i]); + $rawValue = true; + } elseif (preg_match('/quoted-printable/', strtolower($type))) { + $value = quoted_printable_decode($value); + unset($types[$i]); + $rawValue = true; + } elseif (strpos(strtolower($type), 'charset=') === 0) { + try { + $value = mb_convert_encoding($value, "UTF-8", substr($type, 8)); + } catch (\Exception $e) { } + unset($types[$i]); + } + $i++; + } + + switch (strtoupper($element)) { + case 'FN': + $cardData->fullname = $value; + break; + case 'N': + foreach($this->parseName($value) as $key => $val) { + $cardData->{$key} = $val; + } + break; + case 'BDAY': + $cardData->birthday = $this->parseBirthday($value); + break; + case 'ADR': + if (!isset($cardData->address)) { + $cardData->address = array(); + } + $key = !empty($types) ? implode(';', $types) : 'WORK;POSTAL'; + $cardData->address[$key][] = $this->parseAddress($value); + break; + case 'TEL': + if (!isset($cardData->phone)) { + $cardData->phone = array(); + } + $key = !empty($types) ? implode(';', $types) : 'default'; + $cardData->phone[$key][] = $value; + break; + case 'EMAIL': + if (!isset($cardData->email)) { + $cardData->email = array(); + } + $key = !empty($types) ? implode(';', $types) : 'default'; + $cardData->email[$key][] = $value; + break; + case 'REV': + $cardData->revision = $value; + break; + case 'VERSION': + $cardData->version = $value; + break; + case 'ORG': + $cardData->organization = $value; + break; + case 'URL': + if (!isset($cardData->url)) { + $cardData->url = array(); + } + $key = !empty($types) ? implode(';', $types) : 'default'; + $cardData->url[$key][] = $value; + break; + case 'TITLE': + $cardData->title = $value; + break; + case 'PHOTO': + if ($rawValue) { + $cardData->rawPhoto = $value; + } else { + $cardData->photo = $value; + } + break; + } + } + } + } + + protected function parseName($value) + { + @list( + $lastname, + $firstname, + $additional, + $prefix, + $suffix + ) = explode(';', $value); + return (object) array( + 'lastname' => $lastname, + 'firstname' => $firstname, + 'additional' => $additional, + 'prefix' => $prefix, + 'suffix' => $suffix, + ); + } + + protected function parseBirthday($value) + { + return new \DateTime($value); + } + + protected function parseAddress($value) + { + @list( + $name, + $extended, + $street, + $city, + $region, + $zip, + $country, + ) = explode(';', $value); + return (object) array( + 'name' => $name, + 'extended' => $extended, + 'street' => $street, + 'city' => $city, + 'region' => $region, + 'zip' => $zip, + 'country' => $country, + ); + } + +} diff --git a/tests/VCardParserTest.php b/tests/VCardParserTest.php new file mode 100644 index 0000000..e0644ff --- /dev/null +++ b/tests/VCardParserTest.php @@ -0,0 +1,204 @@ + + */ +class VCardParserTest extends \PHPUnit_Framework_TestCase +{ + + /** + * @expectedException OutOfBoundsException + */ + public function testOutOfRangeException() + { + $parser = new VCardParser(''); + $parser->getCardAtIndex(2); + } + + public function testSimpleVcard() + { + $vcard = new VCard(); + $vcard->addName("Admiraal", "Wouter"); + $parser = new VCardParser($vcard->buildVCard()); + $this->assertEquals($parser->getCardAtIndex(0)->firstname, "Wouter"); + $this->assertEquals($parser->getCardAtIndex(0)->lastname, "Admiraal"); + $this->assertEquals($parser->getCardAtIndex(0)->fullname, "Wouter Admiraal"); + } + + public function testBDay() + { + $vcard = new VCard(); + $vcard->addBirthday('31-12-2015'); + $parser = new VCardParser($vcard->buildVCard()); + $this->assertEquals($parser->getCardAtIndex(0)->birthday->format('Y-m-d'), '2015-12-31'); + } + + public function testAddress() + { + $vcard = new VCard(); + $vcard->addAddress( + "Lorem Corp.", + "(extended info)", + "54th Ipsum Street", + "PHPsville", + "Guacamole", + "01158", + "Gitland", + 'WORK;POSTAL' + ); + $vcard->addAddress( + "Wouter Admiraal", + "(extended info, again)", + "25th Some Address", + "Townsville", + "Area 51", + "045784", + "Europe (is a country, right?)", + 'WORK;PERSONAL' + ); + $vcard->addAddress( + "Johannes Admiraal", + "(extended info, again, again)", + "26th Some Address", + "Townsville-South", + "Area 51B", + "04554", + "Europe (no, it isn't)", + 'WORK;PERSONAL' + ); + $parser = new VCardParser($vcard->buildVCard()); + $this->assertEquals($parser->getCardAtIndex(0)->address['WORK;POSTAL'][0], (object) array( + 'name' => "Lorem Corp.", + 'extended' => "(extended info)", + 'street' => "54th Ipsum Street", + 'city' => "PHPsville", + 'region' => "Guacamole", + 'zip' => "01158", + 'country' => "Gitland", + )); + $this->assertEquals($parser->getCardAtIndex(0)->address['WORK;PERSONAL'][0], (object) array( + 'name' => "Wouter Admiraal", + 'extended' => "(extended info, again)", + 'street' => "25th Some Address", + 'city' => "Townsville", + 'region' => "Area 51", + 'zip' => "045784", + 'country' => "Europe (is a country, right?)", + )); + $this->assertEquals($parser->getCardAtIndex(0)->address['WORK;PERSONAL'][1], (object) array( + 'name' => "Johannes Admiraal", + 'extended' => "(extended info, again, again)", + 'street' => "26th Some Address", + 'city' => "Townsville-South", + 'region' => "Area 51B", + 'zip' => "04554", + 'country' => "Europe (no, it isn't)", + )); + } + + public function testPhone() + { + $vcard = new VCard(); + $vcard->addPhoneNumber('0984456123'); + $vcard->addPhoneNumber('2015123487', 'WORK'); + $vcard->addPhoneNumber('4875446578', 'WORK'); + $vcard->addPhoneNumber('9875445464', 'PREF;WORK;VOICE'); + $parser = new VCardParser($vcard->buildVCard()); + $this->assertEquals($parser->getCardAtIndex(0)->phone['default'][0], '0984456123'); + $this->assertEquals($parser->getCardAtIndex(0)->phone['WORK'][0], '2015123487'); + $this->assertEquals($parser->getCardAtIndex(0)->phone['WORK'][1], '4875446578'); + $this->assertEquals($parser->getCardAtIndex(0)->phone['PREF;WORK;VOICE'][0], '9875445464'); + } + + public function testEmail() + { + $vcard = new VCard(); + $vcard->addEmail('some@email.com'); + $vcard->addEmail('site@corp.net', 'WORK'); + $vcard->addEmail('site.corp@corp.net', 'WORK'); + $vcard->addEmail('support@info.info', 'PREF;WORK'); + $parser = new VCardParser($vcard->buildVCard()); + // The VCard class uses a default type of "INTERNET", so we do not test + // against the "default" key. + $this->assertEquals($parser->getCardAtIndex(0)->email['INTERNET'][0], 'some@email.com'); + $this->assertEquals($parser->getCardAtIndex(0)->email['INTERNET;WORK'][0], 'site@corp.net'); + $this->assertEquals($parser->getCardAtIndex(0)->email['INTERNET;WORK'][1], 'site.corp@corp.net'); + $this->assertEquals($parser->getCardAtIndex(0)->email['INTERNET;PREF;WORK'][0], 'support@info.info'); + } + + public function testOrganization() + { + $vcard = new VCard(); + $vcard->addCompany('Lorem Corp.'); + $parser = new VCardParser($vcard->buildVCard()); + $this->assertEquals($parser->getCardAtIndex(0)->organization, 'Lorem Corp.'); + } + + public function testUrl() + { + $vcard = new VCard(); + $vcard->addUrl('http://example.com'); + $vcard->addUrl('http://home.example.com', 'HOME'); + $vcard->addUrl('http://work1.example.com', 'PREF;WORK'); + $vcard->addUrl('http://work2.example.com', 'PREF;WORK'); + $parser = new VCardParser($vcard->buildVCard()); + $this->assertEquals($parser->getCardAtIndex(0)->url['default'][0], 'http://example.com'); + $this->assertEquals($parser->getCardAtIndex(0)->url['HOME'][0], 'http://home.example.com'); + $this->assertEquals($parser->getCardAtIndex(0)->url['PREF;WORK'][0], 'http://work1.example.com'); + $this->assertEquals($parser->getCardAtIndex(0)->url['PREF;WORK'][1], 'http://work2.example.com'); + } + + public function testTitle() + { + $vcard = new VCard(); + $vcard->addJobtitle('Ninja'); + $parser = new VCardParser($vcard->buildVCard()); + $this->assertEquals($parser->getCardAtIndex(0)->title, 'Ninja'); + } + + public function testPhoto() + { + $image = __DIR__ . '/image.jpg'; + + $vcard = new VCard(); + $vcard->addPhoto($image, true); + $parser = new VCardParser($vcard->buildVCard()); + $this->assertEquals($parser->getCardAtIndex(0)->rawPhoto, file_get_contents($image)); + + $vcard = new VCard(); + $vcard->addPhoto($image, false); + $parser = new VCardParser($vcard->buildVCard()); + $this->assertEquals($parser->getCardAtIndex(0)->photo, __DIR__ . '/image.jpg'); + } + + public function testVcardDB() + { + $db = ''; + $vcard = new VCard(); + $vcard->addName("Admiraal", "Wouter"); + $db .= $vcard->buildVCard(); + + $vcard = new VCard(); + $vcard->addName("Lorem", "Ipsum"); + $db .= $vcard->buildVCard(); + + $parser = new VCardParser($db); + $this->assertEquals($parser->getCardAtIndex(0)->fullname, "Wouter Admiraal"); + $this->assertEquals($parser->getCardAtIndex(1)->fullname, "Ipsum Lorem"); + } + + public function testFromFile() + { + $parser = VCardParser::parseFromFile(__DIR__ . '/example.vcf'); + $this->assertEquals($parser->getCardAtIndex(0)->firstname, "Wouter"); + $this->assertEquals($parser->getCardAtIndex(0)->lastname, "Admiraal"); + $this->assertEquals($parser->getCardAtIndex(0)->fullname, "Wouter Admiraal"); + } +} diff --git a/tests/example.vcf b/tests/example.vcf new file mode 100644 index 0000000..d04a41d --- /dev/null +++ b/tests/example.vcf @@ -0,0 +1,6 @@ +BEGIN:VCARD +VERSION:3.0 +REV:2016-05-30T10:36:13Z +N;CHARSET=utf-8:Admiraal;Wouter;;; +FN;CHARSET=utf-8:Wouter Admiraal +END:VCARD From eb7d49610bd7e50bb819ed0237056bc0d67f97e3 Mon Sep 17 00:00:00 2001 From: Jeroen Desloovere Date: Mon, 30 May 2016 14:00:19 +0200 Subject: [PATCH 2/2] Removed non-existing php version 5.7 --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index f206683..0c3f61f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,6 @@ php: - 5.4 - 5.5 - 5.6 - - 5.7 - hhvm sudo: false