├── tests ├── empty.jpg ├── emptyfile ├── wrongfile ├── image.jpg ├── example.vcf ├── VCardExceptionTest.php ├── VCardParserTest.php └── VCardTest.php ├── .gitignore ├── examples ├── assets │ ├── landscape.jpeg │ └── contacts.vcf ├── example_parsing.php └── example.php ├── .travis.yml ├── phpunit.xml.dist ├── src ├── VCardException.php ├── VCardParser.php └── VCard.php ├── composer.json ├── LICENSE ├── CHANGELOG.md └── README.md /tests/empty.jpg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/emptyfile: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/wrongfile: -------------------------------------------------------------------------------- 1 | foobar 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | composer.lock 2 | vendor 3 | .idea/ 4 | .phpunit.result.cache -------------------------------------------------------------------------------- /tests/image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeroendesloovere/vcard/HEAD/tests/image.jpg -------------------------------------------------------------------------------- /examples/assets/landscape.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeroendesloovere/vcard/HEAD/examples/assets/landscape.jpeg -------------------------------------------------------------------------------- /tests/example.vcf: -------------------------------------------------------------------------------- 1 | BEGIN:VCARD 2 | VERSION:3.0 3 | REV:2016-05-30T10:36:13Z 4 | N;CHARSET=utf-8:Desloovere;Jeroen;;; 5 | FN;CHARSET=utf-8:Jeroen Desloovere 6 | item1.EMAIL;type=INTERNET:site@example.com 7 | item1.X-ABLabel:$!!$ 8 | item2.URL:http://www.jeroendesloovere.be 9 | item2.X-ABLabel:$!!$ 10 | END:VCARD 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | dist: trusty 4 | 5 | php: 6 | - 7.3 7 | - 8.0 8 | - hhvm 9 | 10 | sudo: false 11 | 12 | before_script: 13 | - composer self-update 14 | - composer install --prefer-source --no-interaction 15 | 16 | script: 17 | - vendor/bin/phpunit --coverage-text 18 | 19 | matrix: 20 | allow_failures: 21 | - php: hhvm 22 | fast_finish: true 23 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | src 6 | 7 | 8 | 9 | 10 | tests 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /tests/VCardExceptionTest.php: -------------------------------------------------------------------------------- 1 | expectException(\JeroenDesloovere\VCard\VCardException::class); 22 | throw new VCardException('Testing the VCard error.'); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /examples/example_parsing.php: -------------------------------------------------------------------------------- 1 | lastname; 18 | $firstname = $vcard->firstname; 19 | $birthday = $vcard->birthday->format('Y-m-d'); 20 | 21 | printf("\"%s\",\"%s\",\"%s\"", $lastname, $firstname, $birthday); 22 | 23 | echo PHP_EOL; 24 | } 25 | -------------------------------------------------------------------------------- /src/VCardException.php: -------------------------------------------------------------------------------- 1 | =7.3.0", 18 | "behat/transliterator": "~1.0" 19 | }, 20 | "require-dev": { 21 | "phpunit/phpunit": "^9.3" 22 | }, 23 | "autoload": { 24 | "psr-4": { "JeroenDesloovere\\VCard\\": "src/" } 25 | }, 26 | "autoload-dev": { 27 | "psr-4": { "JeroenDesloovere\\VCard\\": "tests/" } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Jeroen Desloovere 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /examples/assets/contacts.vcf: -------------------------------------------------------------------------------- 1 | BEGIN:VCARD 2 | VERSION:3.0 3 | FN:Thies-Tillman Jacobsen 4 | N:Jacobsen;Thies-Tillman;;; 5 | BDAY:1908-10-25 6 | CATEGORIES:Sportverein 7 | TZ:+0100 8 | NOTE:Lieblingsfarbe: Violett 9 | END:VCARD 10 | 11 | BEGIN:VCARD 12 | VERSION:3.0 13 | FN:Lenn Biernoth 14 | N:Biernoth;Lenn;;; 15 | X-MAIDENNAME:Iser 16 | BDAY:1951-11-22 17 | BIRTHPLACE:Hörstel 18 | TITLE:Strassenreiniger 19 | CATEGORIES:Familie 20 | TZ:+0100 21 | EMAIL;TYPE=INTERNET;TYPE=HOME;TYPE=PREF:lenn@lbi.net 22 | NOTE:Interessen: Internet trollen, Kekse backen, Videos schneiden\nLieblingsessen: Chillibrot mit Kartoffelpuffer 23 | END:VCARD 24 | 25 | BEGIN:VCARD 26 | VERSION:3.0 27 | FN:Ludwig-Götz Graßl 28 | N:Graßl;Ludwig-Götz;;; 29 | X-MAIDENNAME:Schoen 30 | BDAY:1943-02-07 31 | BIRTHPLACE:Kaltennordheim 32 | TITLE:Rentner 33 | CATEGORIES:Piratenpartei 34 | TZ:+0100 35 | EMAIL;TYPE=INTERNET;TYPE=HOME;TYPE=PREF:ludwig-goetz@ludwig-goetz-grassl.org 36 | NOTE:Interessen: Stofftiere, Basketball\nLieblingsessen: Reiberouladen 37 | END:VCARD 38 | 39 | BEGIN:VCARD 40 | VERSION:3.0 41 | FN:Marita Kreutzer 42 | N:Kreutzer;Marita;;; 43 | NICKNAME:mkr 44 | X-MAIDENNAME:Sievers 45 | BDAY:1943-07-26 46 | TITLE:Rentnerin 47 | CATEGORIES:CCC 48 | TZ:+0100 49 | NOTE:Interessen: Handarbeiten\nLieblingsfarbe: Weiß\nLieblingsessen: Orangen 50 | END:VCARD 51 | 52 | BEGIN:VCARD 53 | VERSION:3.0 54 | FN:Kathi Hoelzl 55 | N:Hoelzl;Kathi;;; 56 | BDAY:2002-07-11 57 | BIRTHPLACE:Grevenbroich 58 | CATEGORIES:Piratenpartei 59 | TZ:+0100 60 | EMAIL;TYPE=INTERNET;TYPE=HOME;TYPE=PREF:kathi.hoelzl@alhilal.net 61 | URL;TYPE=HOME:http://kho.me/ 62 | END:VCARD 63 | 64 | -------------------------------------------------------------------------------- /examples/example.php: -------------------------------------------------------------------------------- 1 | addName($lastname, $firstname, $additional, $prefix, $suffix); 24 | 25 | // add work data 26 | $vcard->addCompany('Siesqo'); 27 | $vcard->addJobtitle('Web Developer'); 28 | $vcard->addEmail('info@jeroendesloovere.be'); 29 | $vcard->addPhoneNumber(1234121212, 'PREF;WORK'); 30 | $vcard->addPhoneNumber(123456789, 'WORK'); 31 | $vcard->addAddress(null, null, 'street', 'worktown', null, 'workpostcode', 'Belgium'); 32 | $vcard->addURL('http://www.jeroendesloovere.be'); 33 | $vcard->addLabel('street, worktown, workpostcode Belgium', 'work'); 34 | 35 | $vcard->addPhoto(__DIR__ . '/assets/landscape.jpeg'); 36 | //$vcard->addPhoto('https://raw.githubusercontent.com/jeroendesloovere/vcard/master/tests/image.jpg'); 37 | 38 | // return vcard as a string 39 | //return $vcard->getOutput(); 40 | 41 | // return vcard as a download 42 | return $vcard->download(); 43 | 44 | // echo message 45 | echo 'A personal vCard is saved in this folder: ' . __DIR__; 46 | 47 | // or 48 | 49 | // save the card in file in the current folder 50 | // return $vcard->save(); 51 | 52 | // echo message 53 | // echo 'A personal vCard is saved in this folder: ' . __DIR__; 54 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 1.2.2 (2015-11-30) 2 | -- 3 | Improvements: 4 | * Probably a fix for UTF-8 in outlook 2010 5 | 6 | 1.2.1 (2015-07-23) 7 | -- 8 | Improvements: 9 | * You can now set the charset by using $vcard->setCharset('ISO-8859-1'); 10 | 11 | 1.2.0 (2015-06-01) 12 | -- 13 | Improvements: 14 | * You can now add some properties multiple times: email, address, phone number and url 15 | 16 | 1.1.11 (2015-05-21) 17 | -- 18 | Improvements: 19 | * addMedia updated to check if correct file type. 20 | 21 | 1.1.10 (2015-05-20) 22 | -- 23 | Improvements: 24 | * Multiple mailaddresses allowed. 25 | * Chaining integrated to add functions. 26 | 27 | 1.1.9 (2015-04-21) 28 | -- 29 | Improvements: 30 | * getHeaders() is now separate function. So frameworks can use this. 31 | * Fix for iOS 8 to return vcard without calendar wrapper. 32 | 33 | 1.1.8 (2015-03-09) 34 | -- 35 | Bugfixes: 36 | * Fixes $include/$exclude, #27 37 | * Fixes special characters by using external transliterator class. 38 | 39 | 1.1.7 (2015-03-05) 40 | -- 41 | Improvements: 42 | * Images should per default be included in our vcard. 43 | 44 | Bugfixes: 45 | * Fix for the ->get() which didn't return anything. 46 | 47 | 1.1.6 (2015-02-24) 48 | -- 49 | Improvements: 50 | * Add line folding, check #16 51 | * Refactored some functions. 52 | * PSR-2-code-styling applied. 53 | * PHPCS applied. 54 | 55 | Bugfixes: 56 | * Fix fetching PHOTO elements. 57 | 58 | 1.1.5 (2015-01-30) 59 | -- 60 | Bugfixes: 61 | * Updated the deprecated MIME detection, check #16 62 | 63 | 1.1.4 (2015-01-22) 64 | -- 65 | Improvements: 66 | * PHPUnit Tests added 67 | * Exception is now a separate class. 68 | * Renamed the variables $firstName and $lastName 69 | 70 | Bugfixes: 71 | * Filename: Fixed double underscores when no "additional" field was given. 72 | 73 | 1.1.3 (2015-01-22) 74 | -- 75 | Bugfixes: 76 | * Name: Double space when no "additional" field is given. Fixes #8 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VCard PHP library 2 | [![Latest Stable Version](http://img.shields.io/packagist/v/jeroendesloovere/vcard.svg)](https://packagist.org/packages/jeroendesloovere/vcard) 3 | [![License](http://img.shields.io/badge/license-MIT-lightgrey.svg)](https://github.com/jeroendesloovere/vcard/blob/master/LICENSE) 4 | [![Build Status](https://travis-ci.org/jeroendesloovere/vcard.svg?branch=master)](https://travis-ci.org/jeroendesloovere/vcard) 5 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/jeroendesloovere/vcard/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/jeroendesloovere/vcard/?branch=master) 6 | 7 | This VCard PHP library can generate a vCard with some data. When using an iOS device < iOS 8 it will export as a .ics file because iOS devices don't support the default .vcf files. 8 | 9 | **NOTE**: We are working on a complete new version to work with vCard version 4.0, with extreme good code quality. [Check out the new version](https://github.com/jeroendesloovere/vcard/tree/2.0.0-dev) 10 | 11 | ## Usage 12 | 13 | ### Installation 14 | 15 | ```bash 16 | composer require jeroendesloovere/vcard 17 | ``` 18 | > This will install the latest version of vcard with [Composer](https://getcomposer.org) 19 | 20 | ### Example 21 | 22 | ``` php 23 | use JeroenDesloovere\VCard\VCard; 24 | 25 | // define vcard 26 | $vcard = new VCard(); 27 | 28 | // define variables 29 | $lastname = 'Desloovere'; 30 | $firstname = 'Jeroen'; 31 | $additional = ''; 32 | $prefix = ''; 33 | $suffix = ''; 34 | 35 | // add personal data 36 | $vcard->addName($lastname, $firstname, $additional, $prefix, $suffix); 37 | 38 | // add work data 39 | $vcard->addCompany('Siesqo'); 40 | $vcard->addJobtitle('Web Developer'); 41 | $vcard->addRole('Data Protection Officer'); 42 | $vcard->addEmail('info@jeroendesloovere.be'); 43 | $vcard->addPhoneNumber(1234121212, 'PREF;WORK'); 44 | $vcard->addPhoneNumber(123456789, 'WORK'); 45 | $vcard->addAddress(null, null, 'street', 'worktown', null, 'workpostcode', 'Belgium'); 46 | $vcard->addLabel('street, worktown, workpostcode Belgium'); 47 | $vcard->addURL('http://www.jeroendesloovere.be'); 48 | 49 | $vcard->addPhoto(__DIR__ . '/landscape.jpeg'); 50 | 51 | // return vcard as a string 52 | //return $vcard->getOutput(); 53 | 54 | // return vcard as a download 55 | return $vcard->download(); 56 | 57 | // save vcard on disk 58 | //$vcard->setSavePath('/path/to/directory'); 59 | //$vcard->save(); 60 | 61 | ``` 62 | 63 | > [View all examples](/examples/example.php) or check [the VCard class](/src/VCard.php). 64 | 65 | ### Parsing examples 66 | 67 | The parser can either get passed a VCard string, like so: 68 | 69 | ```php 70 | // load VCardParser classes 71 | use JeroenDesloovere\VCard\VCardParser; 72 | 73 | $parser = new VCardParser($vcardString); 74 | echo $parser->getCardAtIndex(0)->fullname; // Prints the full name. 75 | ``` 76 | 77 | Or by using a factory method with a file name: 78 | 79 | ```php 80 | $parser = VCardParser::parseFromFile('path/to/file.vcf'); 81 | echo $parser->getCardAtIndex(0)->fullname; // Prints the full name. 82 | ``` 83 | > [View the parsing example](/examples/example_parsing.php) or check the [the VCardParser class](/src/VCardParser.php) class. 84 | 85 | **Support for frameworks** 86 | 87 | I've created a Symfony Bundle: [VCard Bundle](https://github.com/jeroendesloovere/vcard-bundle) 88 | 89 | Usage in for example: Laravel 90 | ```php 91 | return Response::make( 92 | $this->vcard->getOutput(), 93 | 200, 94 | $this->vcard->getHeaders(true) 95 | ); 96 | ``` 97 | 98 | ## Tests 99 | 100 | ```bash 101 | vendor/bin/phpunit tests 102 | ``` 103 | 104 | ## Documentation 105 | 106 | The class is well documented inline. If you use a decent IDE you'll see that each method is documented with PHPDoc. 107 | 108 | ## Contributing 109 | 110 | Contributions are **welcome** and will be fully **credited**. 111 | 112 | ### Pull Requests 113 | 114 | > To add or update code 115 | 116 | - **Coding Syntax** - Please keep the code syntax consistent with the rest of the package. 117 | - **Add unit tests!** - Your patch won't be accepted if it doesn't have tests. 118 | - **Document any change in behavior** - Make sure the README and any other relevant documentation are kept up-to-date. 119 | - **Consider our release cycle** - We try to follow [semver](http://semver.org/). Randomly breaking public APIs is not an option. 120 | - **Create topic branches** - Don't ask us to pull from your master branch. 121 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 122 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please squash them before submitting. 123 | 124 | ### Issues 125 | 126 | > For bug reporting or code discussions. 127 | 128 | More info on how to work with GitHub on help.github.com. 129 | 130 | ## Credits 131 | 132 | - [Jeroen Desloovere](https://github.com/jeroendesloovere) 133 | - [All Contributors](https://github.com/jeroendesloovere/vcard/contributors) 134 | 135 | ## License 136 | 137 | The module is licensed under [MIT](./LICENSE.md). In short, this license allows you to do everything as long as the copyright statement stays present. 138 | -------------------------------------------------------------------------------- /src/VCardParser.php: -------------------------------------------------------------------------------- 1 | content = $content; 65 | $this->vcardObjects = []; 66 | $this->rewind(); 67 | $this->parse(); 68 | } 69 | 70 | public function rewind(): void 71 | { 72 | $this->position = 0; 73 | } 74 | 75 | public function current(): \stdClass 76 | { 77 | if (! $this->valid()) { 78 | throw new RuntimeException('invalid'); 79 | } 80 | 81 | return $this->getCardAtIndex($this->position); 82 | } 83 | 84 | public function key(): int 85 | { 86 | return $this->position; 87 | } 88 | 89 | public function next(): void 90 | { 91 | $this->position++; 92 | } 93 | 94 | public function valid(): bool 95 | { 96 | return !empty($this->vcardObjects[$this->position]); 97 | } 98 | 99 | /** 100 | * Fetch all the imported VCards. 101 | * 102 | * @return array 103 | * A list of VCard card data objects. 104 | */ 105 | public function getCards(): array 106 | { 107 | return $this->vcardObjects; 108 | } 109 | 110 | /** 111 | * Fetch the imported VCard at the specified index. 112 | * 113 | * @throws OutOfBoundsException 114 | * 115 | * @param int $i 116 | * 117 | * @return stdClass 118 | * The card data object. 119 | */ 120 | public function getCardAtIndex($i): stdClass 121 | { 122 | if (isset($this->vcardObjects[$i])) { 123 | return $this->vcardObjects[$i]; 124 | } 125 | throw new \OutOfBoundsException(); 126 | } 127 | 128 | /** 129 | * Start the parsing process. 130 | * 131 | * This method will populate the data object. 132 | */ 133 | protected function parse() 134 | { 135 | // Normalize new lines. 136 | $this->content = str_replace(["\r\n", "\r"], "\n", $this->content); 137 | 138 | // RFC2425 5.8.1. Line delimiting and folding 139 | // Unfolding is accomplished by regarding CRLF immediately followed by 140 | // a white space character (namely HTAB ASCII decimal 9 or. SPACE ASCII 141 | // decimal 32) as equivalent to no characters at all (i.e., the CRLF 142 | // and single white space character are removed). 143 | $this->content = preg_replace("/\n(?:[ \t])/", "", $this->content); 144 | $lines = explode("\n", $this->content); 145 | 146 | // Parse the VCard, line by line. 147 | foreach ($lines as $line) { 148 | $line = trim($line); 149 | 150 | if (strtoupper($line) == "BEGIN:VCARD") { 151 | $cardData = new \stdClass(); 152 | } elseif (strtoupper($line) == "END:VCARD") { 153 | $this->vcardObjects[] = $cardData; 154 | } elseif (!empty($line)) { 155 | // Strip grouping information. We don't use the group names. We 156 | // simply use a list for entries that have multiple values. 157 | // As per RFC, group names are alphanumerical, and end with a 158 | // period (.). 159 | $line = preg_replace('/^\w+\./', '', $line); 160 | 161 | $type = ''; 162 | $value = ''; 163 | @list($type, $value) = explode(':', $line, 2); 164 | 165 | $types = explode(';', $type); 166 | $element = strtoupper($types[0]); 167 | 168 | array_shift($types); 169 | 170 | // Normalize types. A type can either be a type-param directly, 171 | // or can be prefixed with "type=". E.g.: "INTERNET" or 172 | // "type=INTERNET". 173 | if (!empty($types)) { 174 | $types = array_map(function($type) { 175 | return preg_replace('/^type=/i', '', $type); 176 | }, $types); 177 | } 178 | 179 | $i = 0; 180 | $rawValue = false; 181 | foreach ($types as $type) { 182 | if (preg_match('/base64/', strtolower($type))) { 183 | $value = base64_decode($value); 184 | unset($types[$i]); 185 | $rawValue = true; 186 | } elseif (preg_match('/encoding=b/', strtolower($type))) { 187 | $value = base64_decode($value); 188 | unset($types[$i]); 189 | $rawValue = true; 190 | } elseif (preg_match('/quoted-printable/', strtolower($type))) { 191 | $value = quoted_printable_decode($value); 192 | unset($types[$i]); 193 | $rawValue = true; 194 | } elseif (strpos(strtolower($type), 'charset=') === 0) { 195 | try { 196 | $value = mb_convert_encoding($value, "UTF-8", substr($type, 8)); 197 | } catch (\Exception $e) { 198 | } 199 | unset($types[$i]); 200 | } 201 | $i++; 202 | } 203 | 204 | switch (strtoupper($element)) { 205 | case 'FN': 206 | $cardData->fullname = $value; 207 | break; 208 | case 'N': 209 | foreach ($this->parseName($value) as $key => $val) { 210 | $cardData->{$key} = $val; 211 | } 212 | break; 213 | case 'BDAY': 214 | $cardData->birthday = $this->parseBirthday($value); 215 | break; 216 | case 'ADR': 217 | if (!isset($cardData->address)) { 218 | $cardData->address = []; 219 | } 220 | $key = !empty($types) ? implode(';', $types) : 'WORK;POSTAL'; 221 | $cardData->address[$key][] = $this->parseAddress($value); 222 | break; 223 | case 'TEL': 224 | if (!isset($cardData->phone)) { 225 | $cardData->phone = []; 226 | } 227 | $key = !empty($types) ? implode(';', $types) : 'default'; 228 | $cardData->phone[$key][] = $value; 229 | break; 230 | case 'EMAIL': 231 | if (!isset($cardData->email)) { 232 | $cardData->email = []; 233 | } 234 | $key = !empty($types) ? implode(';', $types) : 'default'; 235 | $cardData->email[$key][] = $value; 236 | break; 237 | case 'REV': 238 | $cardData->revision = $value; 239 | break; 240 | case 'VERSION': 241 | $cardData->version = $value; 242 | break; 243 | case 'ORG': 244 | $cardData->organization = $value; 245 | break; 246 | case 'URL': 247 | if (!isset($cardData->url)) { 248 | $cardData->url = []; 249 | } 250 | $key = !empty($types) ? implode(';', $types) : 'default'; 251 | $cardData->url[$key][] = $value; 252 | break; 253 | case 'TITLE': 254 | $cardData->title = $value; 255 | break; 256 | case 'PHOTO': 257 | if ($rawValue) { 258 | $cardData->rawPhoto = $value; 259 | } else { 260 | $cardData->photo = $value; 261 | } 262 | break; 263 | case 'LOGO': 264 | if ($rawValue) { 265 | $cardData->rawLogo = $value; 266 | } else { 267 | $cardData->logo = $value; 268 | } 269 | break; 270 | case 'NOTE': 271 | $cardData->note = $this->unescape($value); 272 | break; 273 | case 'CATEGORIES': 274 | $cardData->categories = array_map('trim', explode(',', $value)); 275 | break; 276 | case 'LABEL': 277 | $cardData->label = $value; 278 | break; 279 | } 280 | } 281 | } 282 | } 283 | 284 | protected function parseName($value) 285 | { 286 | @list( 287 | $lastname, 288 | $firstname, 289 | $additional, 290 | $prefix, 291 | $suffix 292 | ) = explode(';', $value); 293 | return (object) [ 294 | 'lastname' => $lastname, 295 | 'firstname' => $firstname, 296 | 'additional' => $additional, 297 | 'prefix' => $prefix, 298 | 'suffix' => $suffix, 299 | ]; 300 | } 301 | 302 | protected function parseBirthday($value) 303 | { 304 | return new \DateTime($value); 305 | } 306 | 307 | protected function parseAddress($value) 308 | { 309 | @list( 310 | $name, 311 | $extended, 312 | $street, 313 | $city, 314 | $region, 315 | $zip, 316 | $country, 317 | ) = explode(';', $value); 318 | return (object) [ 319 | 'name' => $name, 320 | 'extended' => $extended, 321 | 'street' => $street, 322 | 'city' => $city, 323 | 'region' => $region, 324 | 'zip' => $zip, 325 | 'country' => $country, 326 | ]; 327 | } 328 | 329 | /** 330 | * Unescape newline characters according to RFC2425 section 5.8.4. 331 | * This function will replace escaped line breaks with PHP_EOL. 332 | * 333 | * @link http://tools.ietf.org/html/rfc2425#section-5.8.4 334 | * @param string $text 335 | * @return string 336 | */ 337 | protected function unescape($text) 338 | { 339 | return str_replace("\\n", PHP_EOL, $text); 340 | } 341 | } 342 | -------------------------------------------------------------------------------- /tests/VCardParserTest.php: -------------------------------------------------------------------------------- 1 | expectException(OutOfBoundsException::class); 18 | $parser = new VCardParser(''); 19 | $parser->getCardAtIndex(2); 20 | } 21 | 22 | public function testSimpleVcard() 23 | { 24 | $vcard = new VCard(); 25 | $vcard->addName("Desloovere", "Jeroen"); 26 | $parser = new VCardParser($vcard->buildVCard()); 27 | $this->assertEquals($parser->getCardAtIndex(0)->firstname, "Jeroen"); 28 | $this->assertEquals($parser->getCardAtIndex(0)->lastname, "Desloovere"); 29 | $this->assertEquals($parser->getCardAtIndex(0)->fullname, "Jeroen Desloovere"); 30 | } 31 | 32 | public function testBDay() 33 | { 34 | $vcard = new VCard(); 35 | $vcard->addBirthday('31-12-2015'); 36 | $parser = new VCardParser($vcard->buildVCard()); 37 | $this->assertEquals($parser->getCardAtIndex(0)->birthday->format('Y-m-d'), '2015-12-31'); 38 | } 39 | 40 | public function testAddress() 41 | { 42 | $vcard = new VCard(); 43 | $vcard->addAddress( 44 | "Lorem Corp.", 45 | "(extended info)", 46 | "54th Ipsum Street", 47 | "PHPsville", 48 | "Guacamole", 49 | "01158", 50 | "Gitland", 51 | 'WORK;POSTAL' 52 | ); 53 | $vcard->addAddress( 54 | "Jeroen Desloovere", 55 | "(extended info, again)", 56 | "25th Some Address", 57 | "Townsville", 58 | "Area 51", 59 | "045784", 60 | "Europe (is a country, right?)", 61 | 'WORK;PERSONAL' 62 | ); 63 | $vcard->addAddress( 64 | "Georges Desloovere", 65 | "(extended info, again, again)", 66 | "26th Some Address", 67 | "Townsville-South", 68 | "Area 51B", 69 | "04554", 70 | "Europe (no, it isn't)", 71 | 'WORK;PERSONAL' 72 | ); 73 | $parser = new VCardParser($vcard->buildVCard()); 74 | $this->assertEquals($parser->getCardAtIndex(0)->address['WORK;POSTAL'][0], (object) array( 75 | 'name' => "Lorem Corp.", 76 | 'extended' => "(extended info)", 77 | 'street' => "54th Ipsum Street", 78 | 'city' => "PHPsville", 79 | 'region' => "Guacamole", 80 | 'zip' => "01158", 81 | 'country' => "Gitland", 82 | )); 83 | $this->assertEquals($parser->getCardAtIndex(0)->address['WORK;PERSONAL'][0], (object) array( 84 | 'name' => "Jeroen Desloovere", 85 | 'extended' => "(extended info, again)", 86 | 'street' => "25th Some Address", 87 | 'city' => "Townsville", 88 | 'region' => "Area 51", 89 | 'zip' => "045784", 90 | 'country' => "Europe (is a country, right?)", 91 | )); 92 | $this->assertEquals($parser->getCardAtIndex(0)->address['WORK;PERSONAL'][1], (object) array( 93 | 'name' => "Georges Desloovere", 94 | 'extended' => "(extended info, again, again)", 95 | 'street' => "26th Some Address", 96 | 'city' => "Townsville-South", 97 | 'region' => "Area 51B", 98 | 'zip' => "04554", 99 | 'country' => "Europe (no, it isn't)", 100 | )); 101 | } 102 | 103 | public function testPhone() 104 | { 105 | $vcard = new VCard(); 106 | $vcard->addPhoneNumber('0984456123'); 107 | $vcard->addPhoneNumber('2015123487', 'WORK'); 108 | $vcard->addPhoneNumber('4875446578', 'WORK'); 109 | $vcard->addPhoneNumber('9875445464', 'PREF;WORK;VOICE'); 110 | $parser = new VCardParser($vcard->buildVCard()); 111 | $this->assertEquals($parser->getCardAtIndex(0)->phone['default'][0], '0984456123'); 112 | $this->assertEquals($parser->getCardAtIndex(0)->phone['WORK'][0], '2015123487'); 113 | $this->assertEquals($parser->getCardAtIndex(0)->phone['WORK'][1], '4875446578'); 114 | $this->assertEquals($parser->getCardAtIndex(0)->phone['PREF;WORK;VOICE'][0], '9875445464'); 115 | } 116 | 117 | public function testEmail() 118 | { 119 | $vcard = new VCard(); 120 | $vcard->addEmail('some@email.com'); 121 | $vcard->addEmail('site@corp.net', 'WORK'); 122 | $vcard->addEmail('site.corp@corp.net', 'WORK'); 123 | $vcard->addEmail('support@info.info', 'PREF;WORK'); 124 | $parser = new VCardParser($vcard->buildVCard()); 125 | // The VCard class uses a default type of "INTERNET", so we do not test 126 | // against the "default" key. 127 | $this->assertEquals($parser->getCardAtIndex(0)->email['INTERNET'][0], 'some@email.com'); 128 | $this->assertEquals($parser->getCardAtIndex(0)->email['INTERNET;WORK'][0], 'site@corp.net'); 129 | $this->assertEquals($parser->getCardAtIndex(0)->email['INTERNET;WORK'][1], 'site.corp@corp.net'); 130 | $this->assertEquals($parser->getCardAtIndex(0)->email['INTERNET;PREF;WORK'][0], 'support@info.info'); 131 | } 132 | 133 | public function testOrganization() 134 | { 135 | $vcard = new VCard(); 136 | $vcard->addCompany('Lorem Corp.'); 137 | $parser = new VCardParser($vcard->buildVCard()); 138 | $this->assertEquals($parser->getCardAtIndex(0)->organization, 'Lorem Corp.'); 139 | } 140 | 141 | public function testUrl() 142 | { 143 | $vcard = new VCard(); 144 | $vcard->addUrl('http://www.jeroendesloovere.be'); 145 | $vcard->addUrl('http://home.example.com', 'HOME'); 146 | $vcard->addUrl('http://work1.example.com', 'PREF;WORK'); 147 | $vcard->addUrl('http://work2.example.com', 'PREF;WORK'); 148 | $parser = new VCardParser($vcard->buildVCard()); 149 | $this->assertEquals($parser->getCardAtIndex(0)->url['default'][0], 'http://www.jeroendesloovere.be'); 150 | $this->assertEquals($parser->getCardAtIndex(0)->url['HOME'][0], 'http://home.example.com'); 151 | $this->assertEquals($parser->getCardAtIndex(0)->url['PREF;WORK'][0], 'http://work1.example.com'); 152 | $this->assertEquals($parser->getCardAtIndex(0)->url['PREF;WORK'][1], 'http://work2.example.com'); 153 | } 154 | 155 | public function testNote() 156 | { 157 | $vcard = new VCard(); 158 | $vcard->addNote('This is a testnote'); 159 | $parser = new VCardParser($vcard->buildVCard()); 160 | 161 | $vcardMultiline = new VCard(); 162 | $vcardMultiline->addNote("This is a multiline note\nNew line content!\r\nLine 2"); 163 | $parserMultiline = new VCardParser($vcardMultiline->buildVCard()); 164 | 165 | $this->assertEquals($parser->getCardAtIndex(0)->note, 'This is a testnote'); 166 | $this->assertEquals(nl2br($parserMultiline->getCardAtIndex(0)->note), nl2br("This is a multiline note" . PHP_EOL . "New line content!" . PHP_EOL . "Line 2")); 167 | } 168 | 169 | public function testCategories() 170 | { 171 | $vcard = new VCard(); 172 | $vcard->addCategories([ 173 | 'Category 1', 174 | 'cat-2', 175 | 'another long category!' 176 | ]); 177 | $parser = new VCardParser($vcard->buildVCard()); 178 | 179 | $this->assertEquals($parser->getCardAtIndex(0)->categories[0], 'Category 1'); 180 | $this->assertEquals($parser->getCardAtIndex(0)->categories[1], 'cat-2'); 181 | $this->assertEquals($parser->getCardAtIndex(0)->categories[2], 'another long category!'); 182 | } 183 | 184 | public function testTitle() 185 | { 186 | $vcard = new VCard(); 187 | $vcard->addJobtitle('Ninja'); 188 | $parser = new VCardParser($vcard->buildVCard()); 189 | $this->assertEquals($parser->getCardAtIndex(0)->title, 'Ninja'); 190 | } 191 | 192 | public function testLogo() 193 | { 194 | $image = __DIR__ . '/image.jpg'; 195 | $imageUrl = 'https://raw.githubusercontent.com/jeroendesloovere/vcard/master/tests/image.jpg'; 196 | 197 | $vcard = new VCard(); 198 | $vcard->addLogo($image, true); 199 | $parser = new VCardParser($vcard->buildVCard()); 200 | $this->assertEquals($parser->getCardAtIndex(0)->rawLogo, file_get_contents($image)); 201 | 202 | $vcard = new VCard(); 203 | $vcard->addLogo($image, false); 204 | $parser = new VCardParser($vcard->buildVCard()); 205 | $this->assertEquals($parser->getCardAtIndex(0)->logo, __DIR__ . '/image.jpg'); 206 | 207 | $vcard = new VCard(); 208 | $vcard->addLogo($imageUrl, false); 209 | $parser = new VCardParser($vcard->buildVCard()); 210 | $this->assertEquals($parser->getCardAtIndex(0)->logo, $imageUrl); 211 | } 212 | 213 | public function testPhoto() 214 | { 215 | $image = __DIR__ . '/image.jpg'; 216 | $imageUrl = 'https://raw.githubusercontent.com/jeroendesloovere/vcard/master/tests/image.jpg'; 217 | 218 | $vcard = new VCard(); 219 | $vcard->addPhoto($image, true); 220 | $parser = new VCardParser($vcard->buildVCard()); 221 | $this->assertEquals($parser->getCardAtIndex(0)->rawPhoto, file_get_contents($image)); 222 | 223 | $vcard = new VCard(); 224 | $vcard->addPhoto($image, false); 225 | $parser = new VCardParser($vcard->buildVCard()); 226 | $this->assertEquals($parser->getCardAtIndex(0)->photo, __DIR__ . '/image.jpg'); 227 | 228 | $vcard = new VCard(); 229 | $vcard->addPhoto($imageUrl, false); 230 | $parser = new VCardParser($vcard->buildVCard()); 231 | $this->assertEquals($parser->getCardAtIndex(0)->photo, $imageUrl); 232 | } 233 | 234 | public function testVcardDB() 235 | { 236 | $db = ''; 237 | $vcard = new VCard(); 238 | $vcard->addName("Desloovere", "Jeroen"); 239 | $db .= $vcard->buildVCard(); 240 | 241 | $vcard = new VCard(); 242 | $vcard->addName("Lorem", "Ipsum"); 243 | $db .= $vcard->buildVCard(); 244 | 245 | $parser = new VCardParser($db); 246 | $this->assertEquals($parser->getCardAtIndex(0)->fullname, "Jeroen Desloovere"); 247 | $this->assertEquals($parser->getCardAtIndex(1)->fullname, "Ipsum Lorem"); 248 | } 249 | 250 | public function testIteration() 251 | { 252 | // Prepare a VCard DB. 253 | $db = ''; 254 | $vcard = new VCard(); 255 | $vcard->addName("Desloovere", "Jeroen"); 256 | $db .= $vcard->buildVCard(); 257 | 258 | $vcard = new VCard(); 259 | $vcard->addName("Lorem", "Ipsum"); 260 | $db .= $vcard->buildVCard(); 261 | 262 | $parser = new VCardParser($db); 263 | foreach ($parser as $i => $card) { 264 | $this->assertEquals($card->fullname, $i == 0 ? "Jeroen Desloovere" : "Ipsum Lorem"); 265 | } 266 | } 267 | 268 | public function testFromFile() 269 | { 270 | $parser = VCardParser::parseFromFile(__DIR__ . '/example.vcf'); 271 | // Use this opportunity to test fetching all cards directly. 272 | $cards = $parser->getCards(); 273 | $this->assertEquals($cards[0]->firstname, "Jeroen"); 274 | $this->assertEquals($cards[0]->lastname, "Desloovere"); 275 | $this->assertEquals($cards[0]->fullname, "Jeroen Desloovere"); 276 | // Check the parsing of grouped items as well, which are present in the 277 | // example file. 278 | $this->assertEquals($cards[0]->url['default'][0], 'http://www.jeroendesloovere.be'); 279 | $this->assertEquals($cards[0]->email['INTERNET'][0], 'site@example.com'); 280 | } 281 | 282 | public function testFileNotFound() 283 | { 284 | $this->expectException(\RuntimeException::class); 285 | $parser = VCardParser::parseFromFile(__DIR__ . '/does-not-exist.vcf'); 286 | } 287 | 288 | public function testLabel() 289 | { 290 | $label = 'street, worktown, workpostcode Belgium'; 291 | $vcard = new VCard(); 292 | $vcard->addLabel($label, 'work'); 293 | $parser = new VCardParser($vcard->buildVCard()); 294 | $this->assertEquals($parser->getCardAtIndex(0)->label, $label); 295 | } 296 | } 297 | -------------------------------------------------------------------------------- /tests/VCardTest.php: -------------------------------------------------------------------------------- 1 | 'john@work.com']], 39 | [['WORK' => 'john@work.com', 'HOME' => 'john@home.com']], 40 | [['PREF;WORK' => 'john@work.com', 'HOME' => 'john@home.com']], 41 | ]; 42 | } 43 | 44 | /** 45 | * Set up before class 46 | * 47 | * @return void 48 | */ 49 | protected function setUp(): void 50 | { 51 | // set timezone 52 | date_default_timezone_set('Europe/Brussels'); 53 | 54 | $this->vcard = new VCard(); 55 | 56 | $this->firstName = 'Jeroen'; 57 | $this->lastName = 'Desloovere'; 58 | $this->additional = '&'; 59 | $this->prefix = 'Mister'; 60 | $this->suffix = 'Junior'; 61 | 62 | $this->emailAddress1 = ''; 63 | $this->emailAddress2 = ''; 64 | 65 | $this->firstName2 = 'Ali'; 66 | $this->lastName2 = 'ÖZSÜT'; 67 | 68 | $this->firstName3 = 'Garçon'; 69 | $this->lastName3 = 'Jéroèn'; 70 | } 71 | 72 | /** 73 | * Tear down after class 74 | */ 75 | protected function tearDown(): void 76 | { 77 | $this->vcard = null; 78 | } 79 | 80 | public function testAddAddress() 81 | { 82 | $this->assertEquals($this->vcard, $this->vcard->addAddress( 83 | '', 84 | '88th Floor', 85 | '555 East Flours Street', 86 | 'Los Angeles', 87 | 'CA', 88 | '55555', 89 | 'USA' 90 | )); 91 | $this->assertStringContainsString('ADR;WORK;POSTAL;CHARSET=utf-8:;88th Floor;555 East Flours Street;Los Angele', $this->vcard->getOutput()); 92 | // Should fold on row 75, so we should not see the full address. 93 | $this->assertStringNotContainsString('ADR;WORK;POSTAL;CHARSET=utf-8:;88th Floor;555 East Flours Street;Los Angeles;CA;55555;', $this->vcard->getOutput()); 94 | } 95 | 96 | public function testAddBirthday() 97 | { 98 | $this->assertEquals($this->vcard, $this->vcard->addBirthday('')); 99 | } 100 | 101 | public function testAddCompany() 102 | { 103 | $this->assertEquals($this->vcard, $this->vcard->addCompany('')); 104 | } 105 | 106 | public function testAddCategories() 107 | { 108 | $this->assertEquals($this->vcard, $this->vcard->addCategories([])); 109 | } 110 | 111 | public function testAddEmail() 112 | { 113 | $this->assertEquals($this->vcard, $this->vcard->addEmail($this->emailAddress1)); 114 | $this->assertEquals($this->vcard, $this->vcard->addEmail($this->emailAddress2)); 115 | $this->assertEquals(2, count($this->vcard->getProperties())); 116 | } 117 | 118 | public function testAddJobTitle() 119 | { 120 | $this->assertEquals($this->vcard, $this->vcard->addJobtitle('')); 121 | } 122 | 123 | public function testAddRole() 124 | { 125 | $this->assertEquals($this->vcard, $this->vcard->addRole('')); 126 | } 127 | 128 | public function testAddName() 129 | { 130 | $this->assertEquals($this->vcard, $this->vcard->addName('')); 131 | } 132 | 133 | public function testAddNote() 134 | { 135 | $this->assertEquals($this->vcard, $this->vcard->addNote('')); 136 | } 137 | 138 | public function testAddPhoneNumber() 139 | { 140 | $this->assertEquals($this->vcard, $this->vcard->addPhoneNumber('')); 141 | $this->assertEquals($this->vcard, $this->vcard->addPhoneNumber('')); 142 | $this->assertCount(2, $this->vcard->getProperties()); 143 | } 144 | 145 | public function testAddPhotoWithJpgPhoto() 146 | { 147 | $return = $this->vcard->addPhoto(__DIR__ . '/image.jpg', true); 148 | 149 | $this->assertEquals($this->vcard, $return); 150 | } 151 | 152 | public function testAddPhotoWithRemoteJpgPhoto() 153 | { 154 | $return = $this->vcard->addPhoto( 155 | 'https://raw.githubusercontent.com/jeroendesloovere/vcard/master/tests/image.jpg', 156 | true 157 | ); 158 | 159 | $this->assertEquals($this->vcard, $return); 160 | } 161 | 162 | /** 163 | * Test adding remote empty photo 164 | */ 165 | public function testAddPhotoWithRemoteEmptyJpgPhoto() 166 | { 167 | $this->expectException(Exception::class); 168 | $this->expectExceptionMessage('Returned data is not an image.'); 169 | $this->vcard->addPhoto( 170 | 'https://raw.githubusercontent.com/jeroendesloovere/vcard/master/tests/empty.jpg', 171 | true 172 | ); 173 | } 174 | 175 | public function testAddPhotoContentWithJpgPhoto() 176 | { 177 | $return = $this->vcard->addPhotoContent(file_get_contents(__DIR__ . '/image.jpg')); 178 | 179 | $this->assertEquals($this->vcard, $return); 180 | } 181 | 182 | /** 183 | * Test adding empty photo 184 | */ 185 | public function testAddPhotoContentWithEmptyContent() 186 | { 187 | $this->expectException(Exception::class); 188 | $this->expectExceptionMessage('Returned data is not an image.'); 189 | $this->vcard->addPhotoContent(''); 190 | } 191 | 192 | public function testAddLogoWithJpgImage() 193 | { 194 | $return = $this->vcard->addLogo(__DIR__ . '/image.jpg', true); 195 | 196 | $this->assertEquals($this->vcard, $return); 197 | } 198 | 199 | public function testAddLogoWithJpgImageNoInclude() 200 | { 201 | $return = $this->vcard->addLogo(__DIR__ . '/image.jpg', false); 202 | 203 | $this->assertEquals($this->vcard, $return); 204 | } 205 | 206 | public function testAddLogoContentWithJpgImage() 207 | { 208 | $return = $this->vcard->addLogoContent(file_get_contents(__DIR__ . '/image.jpg')); 209 | 210 | $this->assertEquals($this->vcard, $return); 211 | } 212 | 213 | /** 214 | * Test adding empty photo 215 | */ 216 | public function testAddLogoContentWithEmptyContent() 217 | { 218 | $this->expectException(Exception::class); 219 | $this->expectExceptionMessage('Returned data is not an image.'); 220 | $this->vcard->addLogoContent(''); 221 | } 222 | 223 | public function testAddUrl() 224 | { 225 | $this->assertEquals($this->vcard, $this->vcard->addUrl('1')); 226 | $this->assertEquals($this->vcard, $this->vcard->addUrl('2')); 227 | $this->assertCount(2, $this->vcard->getProperties()); 228 | } 229 | 230 | /** 231 | * Test adding local photo using an empty file 232 | */ 233 | public function testAddPhotoWithEmptyFile() 234 | { 235 | $this->expectException(Exception::class); 236 | $this->expectExceptionMessage('Returned data is not an image.'); 237 | $this->vcard->addPhoto(__DIR__ . '/emptyfile', true); 238 | } 239 | 240 | /** 241 | * Test adding logo with no value 242 | */ 243 | public function testAddLogoWithNoValue() 244 | { 245 | $this->expectException(Exception::class); 246 | $this->expectExceptionMessage('Returned data is not an image.'); 247 | $this->vcard->addLogo(__DIR__ . '/emptyfile', true); 248 | } 249 | 250 | /** 251 | * Test adding photo with no photo 252 | */ 253 | public function testAddPhotoWithNoPhoto() 254 | { 255 | $this->expectException(Exception::class); 256 | $this->expectExceptionMessage('Returned data is not an image.'); 257 | $this->vcard->addPhoto(__DIR__ . '/wrongfile', true); 258 | } 259 | 260 | /** 261 | * Test adding logo with no image 262 | */ 263 | public function testAddLogoWithNoImage() 264 | { 265 | $this->expectException(Exception::class); 266 | $this->expectExceptionMessage('Returned data is not an image.'); 267 | $this->vcard->addLogo(__DIR__ . '/wrongfile', true); 268 | } 269 | 270 | /** 271 | * Test charset 272 | */ 273 | public function testCharset() 274 | { 275 | $charset = 'ISO-8859-1'; 276 | $this->vcard->setCharset($charset); 277 | $this->assertEquals($charset, $this->vcard->getCharset()); 278 | } 279 | 280 | /** 281 | * Test Email 282 | * 283 | * @dataProvider emailDataProvider $emails 284 | */ 285 | public function testEmail($emails = []) 286 | { 287 | foreach ($emails as $key => $email) { 288 | if (is_string($key)) { 289 | $this->vcard->addEmail($email, $key); 290 | } else { 291 | $this->vcard->addEmail($email); 292 | } 293 | } 294 | 295 | foreach ($emails as $key => $email) { 296 | if (is_string($key)) { 297 | $this->assertStringContainsString('EMAIL;INTERNET;' . $key . ':' . $email, $this->vcard->getOutput()); 298 | } else { 299 | $this->assertStringContainsString('EMAIL;INTERNET:' . $email, $this->vcard->getOutput()); 300 | } 301 | } 302 | } 303 | 304 | /** 305 | * Test first name and last name 306 | */ 307 | public function testFirstNameAndLastName() 308 | { 309 | $this->vcard->addName( 310 | $this->lastName, 311 | $this->firstName 312 | ); 313 | 314 | $this->assertEquals('jeroen-desloovere', $this->vcard->getFilename()); 315 | } 316 | 317 | /** 318 | * Test full blown name 319 | */ 320 | public function testFullBlownName() 321 | { 322 | $this->vcard->addName( 323 | $this->lastName, 324 | $this->firstName, 325 | $this->additional, 326 | $this->prefix, 327 | $this->suffix 328 | ); 329 | 330 | $this->assertEquals('mister-jeroen-desloovere-junior', $this->vcard->getFilename()); 331 | } 332 | 333 | /** 334 | * Test multiple birthdays 335 | */ 336 | public function testMultipleBirthdays() 337 | { 338 | $this->expectException(\Exception::class); 339 | $this->assertEquals($this->vcard, $this->vcard->addBirthday('1')); 340 | $this->expectException(Exception::class); 341 | $this->assertEquals($this->vcard, $this->vcard->addBirthday('2')); 342 | } 343 | 344 | /** 345 | * Test multiple categories 346 | */ 347 | public function testMultipleCategories() 348 | { 349 | $this->expectException(\Exception::class); 350 | $this->assertEquals($this->vcard, $this->vcard->addCategories(['1'])); 351 | $this->expectException(Exception::class); 352 | $this->assertEquals($this->vcard, $this->vcard->addCategories(['2'])); 353 | } 354 | 355 | /** 356 | * Test multiple companies 357 | */ 358 | public function testMultipleCompanies() 359 | { 360 | $this->expectException(\Exception::class); 361 | $this->assertEquals($this->vcard, $this->vcard->addCompany('1')); 362 | $this->expectException(Exception::class); 363 | $this->assertEquals($this->vcard, $this->vcard->addCompany('2')); 364 | } 365 | 366 | /** 367 | * Test multiple job titles 368 | */ 369 | public function testMultipleJobtitles() 370 | { 371 | $this->expectException(\Exception::class); 372 | $this->assertEquals($this->vcard, $this->vcard->addJobtitle('1')); 373 | $this->expectException(Exception::class); 374 | $this->assertEquals($this->vcard, $this->vcard->addJobtitle('2')); 375 | } 376 | 377 | /** 378 | * Test multiple roles 379 | */ 380 | public function testMultipleRoles() 381 | { 382 | $this->expectException(\Exception::class); 383 | $this->assertEquals($this->vcard, $this->vcard->addRole('1')); 384 | $this->expectException(Exception::class); 385 | $this->assertEquals($this->vcard, $this->vcard->addRole('2')); 386 | } 387 | 388 | /** 389 | * Test multiple names 390 | */ 391 | public function testMultipleNames() 392 | { 393 | $this->expectException(\Exception::class); 394 | $this->assertEquals($this->vcard, $this->vcard->addName('1')); 395 | $this->expectException(Exception::class); 396 | $this->assertEquals($this->vcard, $this->vcard->addName('2')); 397 | } 398 | 399 | /** 400 | * Test multiple notes 401 | */ 402 | public function testMultipleNotes() 403 | { 404 | $this->expectException(\Exception::class); 405 | $this->assertEquals($this->vcard, $this->vcard->addNote('1')); 406 | $this->expectException(Exception::class); 407 | $this->assertEquals($this->vcard, $this->vcard->addNote('2')); 408 | } 409 | 410 | /** 411 | * Test special first name and last name 412 | */ 413 | public function testSpecialFirstNameAndLastName() 414 | { 415 | $this->vcard->addName( 416 | $this->lastName2, 417 | $this->firstName2 418 | ); 419 | 420 | $this->assertEquals('ali-ozsut', $this->vcard->getFilename()); 421 | } 422 | 423 | /** 424 | * Test special first name and last name 425 | */ 426 | public function testSpecialFirstNameAndLastName2() 427 | { 428 | $this->vcard->addName( 429 | $this->lastName3, 430 | $this->firstName3 431 | ); 432 | 433 | $this->assertEquals('garcon-jeroen', $this->vcard->getFilename()); 434 | } 435 | 436 | /** 437 | * Test multiple labels 438 | */ 439 | public function testMultipleLabels() 440 | { 441 | $this->assertSame($this->vcard, $this->vcard->addLabel('My label')); 442 | $this->assertSame($this->vcard, $this->vcard->addLabel('My work label', 'WORK')); 443 | $this->assertSame(2, count($this->vcard->getProperties())); 444 | $this->assertStringContainsString('LABEL;CHARSET=utf-8:My label', $this->vcard->getOutput()); 445 | $this->assertStringContainsString('LABEL;WORK;CHARSET=utf-8:My work label', $this->vcard->getOutput()); 446 | } 447 | 448 | public function testChunkSplitUnicode() 449 | { 450 | $class_handler = new \ReflectionClass('JeroenDesloovere\VCard\VCard'); 451 | $method_handler = $class_handler->getMethod('chunk_split_unicode'); 452 | $method_handler->setAccessible(true); 453 | 454 | $ascii_input="Lorem ipsum dolor sit amet,"; 455 | $ascii_output = $method_handler->invokeArgs(new VCard(), [$ascii_input,10,'|']); 456 | $unicode_input='Τη γλώσσα μου έδωσαν ελληνική το σπίτι φτωχικό στις αμμουδιές του Ομήρου.'; 457 | $unicode_output = $method_handler->invokeArgs(new VCard(), [$unicode_input,10,'|']); 458 | 459 | $this->assertEquals( 460 | "Lorem ipsu|m dolor si|t amet,|", 461 | $ascii_output); 462 | $this->assertEquals( 463 | "Τη γλώσσα |μου έδωσαν| ελληνική |το σπίτι φ|τωχικό στι|ς αμμουδιέ|ς του Ομήρ|ου.|", 464 | $unicode_output); 465 | } 466 | } 467 | -------------------------------------------------------------------------------- /src/VCard.php: -------------------------------------------------------------------------------- 1 | setProperty( 97 | 'address', 98 | 'ADR' . (($type != '') ? ';' . $type : '') . $this->getCharsetString(), 99 | $value 100 | ); 101 | 102 | return $this; 103 | } 104 | 105 | /** 106 | * Add birthday 107 | * 108 | * @param string $date Format is YYYY-MM-DD 109 | * @return $this 110 | */ 111 | public function addBirthday($date) 112 | { 113 | $this->setProperty( 114 | 'birthday', 115 | 'BDAY', 116 | $date 117 | ); 118 | 119 | return $this; 120 | } 121 | 122 | /** 123 | * Add company 124 | * 125 | * @param string $company 126 | * @param string $department 127 | * @return $this 128 | */ 129 | public function addCompany($company, $department = '') 130 | { 131 | $this->setProperty( 132 | 'company', 133 | 'ORG' . $this->getCharsetString(), 134 | $company 135 | . ($department != '' ? ';' . $department : '') 136 | ); 137 | 138 | // if filename is empty, add to filename 139 | if ($this->filename === null) { 140 | $this->setFilename($company); 141 | } 142 | 143 | return $this; 144 | } 145 | 146 | /** 147 | * Add email 148 | * 149 | * @param string $address The e-mail address 150 | * @param string [optional] $type The type of the email address 151 | * $type may be PREF | WORK | HOME 152 | * or any combination of these: e.g. "PREF;WORK" 153 | * @return $this 154 | */ 155 | public function addEmail($address, $type = '') 156 | { 157 | $this->setProperty( 158 | 'email', 159 | 'EMAIL;INTERNET' . (($type != '') ? ';' . $type : ''), 160 | $address 161 | ); 162 | 163 | return $this; 164 | } 165 | 166 | /** 167 | * Add jobtitle 168 | * 169 | * @param string $jobtitle The jobtitle for the person. 170 | * @return $this 171 | */ 172 | public function addJobtitle($jobtitle) 173 | { 174 | $this->setProperty( 175 | 'jobtitle', 176 | 'TITLE' . $this->getCharsetString(), 177 | $jobtitle 178 | ); 179 | 180 | return $this; 181 | } 182 | 183 | /** 184 | * Add a label 185 | * 186 | * @param string $label 187 | * @param string $type 188 | * 189 | * @return $this 190 | */ 191 | public function addLabel($label, $type = '') 192 | { 193 | $this->setProperty( 194 | 'label', 195 | 'LABEL' . ($type !== '' ? ';' . $type : '') . $this->getCharsetString(), 196 | $label 197 | ); 198 | 199 | return $this; 200 | } 201 | 202 | /** 203 | * Add role 204 | * 205 | * @param string $role The role for the person. 206 | * @return $this 207 | */ 208 | public function addRole($role) 209 | { 210 | $this->setProperty( 211 | 'role', 212 | 'ROLE' . $this->getCharsetString(), 213 | $role 214 | ); 215 | 216 | return $this; 217 | } 218 | 219 | /** 220 | * Add a photo or logo (depending on property name) 221 | * 222 | * @param string $property LOGO|PHOTO 223 | * @param string $url image url or filename 224 | * @param bool $include Do we include the image in our vcard or not? 225 | * @param string $element The name of the element to set 226 | * @throws VCardException 227 | */ 228 | private function addMedia($property, $url, $element, $include = true) 229 | { 230 | $mimeType = null; 231 | 232 | //Is this URL for a remote resource? 233 | if (filter_var($url, FILTER_VALIDATE_URL) !== false) { 234 | $headers = get_headers($url, 1); 235 | 236 | if (array_key_exists('Content-Type', $headers)) { 237 | $mimeType = $headers['Content-Type']; 238 | if (is_array($mimeType)) { 239 | $mimeType = end($mimeType); 240 | } 241 | } 242 | } else { 243 | //Local file, so inspect it directly 244 | $mimeType = mime_content_type($url); 245 | } 246 | if (strpos($mimeType, ';') !== false) { 247 | $mimeType = strstr($mimeType, ';', true); 248 | } 249 | if (!is_string($mimeType) || substr($mimeType, 0, 6) !== 'image/') { 250 | throw VCardException::invalidImage(); 251 | } 252 | $fileType = strtoupper(substr($mimeType, 6)); 253 | 254 | if ($include) { 255 | if ((bool) ini_get('allow_url_fopen') === true) { 256 | $value = file_get_contents($url); 257 | } else { 258 | $curl = curl_init(); 259 | curl_setopt($curl, CURLOPT_URL, $url); 260 | curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); 261 | $value = curl_exec($curl); 262 | curl_close($curl); 263 | } 264 | 265 | if (!$value) { 266 | throw VCardException::emptyURL(); 267 | } 268 | 269 | $value = base64_encode($value); 270 | $property .= ";ENCODING=b;TYPE=" . $fileType; 271 | } else { 272 | if (filter_var($url, FILTER_VALIDATE_URL) !== false) { 273 | $propertySuffix = ';VALUE=URL'; 274 | $propertySuffix .= ';TYPE=' . strtoupper($fileType); 275 | 276 | $property = $property . $propertySuffix; 277 | $value = $url; 278 | } else { 279 | $value = $url; 280 | } 281 | } 282 | 283 | $this->setProperty( 284 | $element, 285 | $property, 286 | $value 287 | ); 288 | } 289 | 290 | /** 291 | * Add a photo or logo (depending on property name) 292 | * 293 | * @param string $property LOGO|PHOTO 294 | * @param string $content image content 295 | * @param string $element The name of the element to set 296 | */ 297 | private function addMediaContent($property, $content, $element) 298 | { 299 | $finfo = new \finfo(); 300 | $mimeType = $finfo->buffer($content, FILEINFO_MIME_TYPE); 301 | 302 | if (strpos($mimeType, ';') !== false) { 303 | $mimeType = strstr($mimeType, ';', true); 304 | } 305 | if (!is_string($mimeType) || substr($mimeType, 0, 6) !== 'image/') { 306 | throw VCardException::invalidImage(); 307 | } 308 | $fileType = strtoupper(substr($mimeType, 6)); 309 | 310 | $content = base64_encode($content); 311 | $property .= ";ENCODING=b;TYPE=" . $fileType; 312 | 313 | $this->setProperty( 314 | $element, 315 | $property, 316 | $content 317 | ); 318 | } 319 | 320 | /** 321 | * Add name 322 | * 323 | * @param string [optional] $lastName 324 | * @param string [optional] $firstName 325 | * @param string [optional] $additional 326 | * @param string [optional] $prefix 327 | * @param string [optional] $suffix 328 | * @return $this 329 | */ 330 | public function addName( 331 | $lastName = '', 332 | $firstName = '', 333 | $additional = '', 334 | $prefix = '', 335 | $suffix = '' 336 | ) { 337 | // define values with non-empty values 338 | $values = array_filter([ 339 | $prefix, 340 | $firstName, 341 | $additional, 342 | $lastName, 343 | $suffix, 344 | ]); 345 | 346 | // define filename 347 | $this->setFilename($values); 348 | 349 | // set property 350 | $property = $lastName . ';' . $firstName . ';' . $additional . ';' . $prefix . ';' . $suffix; 351 | $this->setProperty( 352 | 'name', 353 | 'N' . $this->getCharsetString(), 354 | $property 355 | ); 356 | 357 | // is property FN set? 358 | if (!$this->hasProperty('FN')) { 359 | // set property 360 | $this->setProperty( 361 | 'fullname', 362 | 'FN' . $this->getCharsetString(), 363 | trim(implode(' ', $values)) 364 | ); 365 | } 366 | 367 | return $this; 368 | } 369 | 370 | /** 371 | * Add note 372 | * 373 | * @param string $note 374 | * @return $this 375 | */ 376 | public function addNote($note) 377 | { 378 | $this->setProperty( 379 | 'note', 380 | 'NOTE' . $this->getCharsetString(), 381 | $note 382 | ); 383 | 384 | return $this; 385 | } 386 | 387 | /** 388 | * Add categories 389 | * 390 | * @param array $categories 391 | * @return $this 392 | */ 393 | public function addCategories($categories) 394 | { 395 | $this->setProperty( 396 | 'categories', 397 | 'CATEGORIES' . $this->getCharsetString(), 398 | trim(implode(',', $categories)) 399 | ); 400 | 401 | return $this; 402 | } 403 | 404 | /** 405 | * Add phone number 406 | * 407 | * @param string $number 408 | * @param string [optional] $type 409 | * Type may be PREF | WORK | HOME | VOICE | FAX | MSG | 410 | * CELL | PAGER | BBS | CAR | MODEM | ISDN | VIDEO 411 | * or any senseful combination, e.g. "PREF;WORK;VOICE" 412 | * @return $this 413 | */ 414 | public function addPhoneNumber($number, $type = '') 415 | { 416 | $this->setProperty( 417 | 'phoneNumber', 418 | 'TEL' . (($type != '') ? ';' . $type : ''), 419 | $number 420 | ); 421 | 422 | return $this; 423 | } 424 | 425 | /** 426 | * Add Logo 427 | * 428 | * @param string $url image url or filename 429 | * @param bool $include Include the image in our vcard? 430 | * @return $this 431 | */ 432 | public function addLogo($url, $include = true) 433 | { 434 | $this->addMedia( 435 | 'LOGO', 436 | $url, 437 | 'logo', 438 | $include 439 | ); 440 | 441 | return $this; 442 | } 443 | 444 | /** 445 | * Add Logo content 446 | * 447 | * @param string $content image content 448 | * @return $this 449 | */ 450 | public function addLogoContent($content) 451 | { 452 | $this->addMediaContent( 453 | 'LOGO', 454 | $content, 455 | 'logo' 456 | ); 457 | 458 | return $this; 459 | } 460 | 461 | /** 462 | * Add Photo 463 | * 464 | * @param string $url image url or filename 465 | * @param bool $include Include the image in our vcard? 466 | * @return $this 467 | */ 468 | public function addPhoto($url, $include = true) 469 | { 470 | $this->addMedia( 471 | 'PHOTO', 472 | $url, 473 | 'photo', 474 | $include 475 | ); 476 | 477 | return $this; 478 | } 479 | 480 | /** 481 | * Add Photo content 482 | * 483 | * @param string $content image content 484 | * @return $this 485 | */ 486 | public function addPhotoContent($content) 487 | { 488 | $this->addMediaContent( 489 | 'PHOTO', 490 | $content, 491 | 'photo' 492 | ); 493 | 494 | return $this; 495 | } 496 | 497 | /** 498 | * Add URL 499 | * 500 | * @param string $url 501 | * @param string [optional] $type Type may be WORK | HOME 502 | * @return $this 503 | */ 504 | public function addURL($url, $type = '') 505 | { 506 | $this->setProperty( 507 | 'url', 508 | 'URL' . (($type != '') ? ';' . $type : ''), 509 | $url 510 | ); 511 | 512 | return $this; 513 | } 514 | 515 | /** 516 | * Build VCard (.vcf) 517 | * 518 | * @return string 519 | */ 520 | public function buildVCard() 521 | { 522 | // init string 523 | $string = "BEGIN:VCARD\r\n"; 524 | $string .= "VERSION:3.0\r\n"; 525 | $string .= "REV:" . date("Y-m-d") . "T" . date("H:i:s") . "Z\r\n"; 526 | 527 | // loop all properties 528 | $properties = $this->getProperties(); 529 | foreach ($properties as $property) { 530 | // add to string 531 | $string .= $this->fold($property['key'] . ':' . $this->escape($property['value'])) . "\r\n"; 532 | } 533 | 534 | // add to string 535 | $string .= "END:VCARD\r\n"; 536 | 537 | // return 538 | return $string; 539 | } 540 | 541 | /** 542 | * Build VCalender (.ics) - Safari (< iOS 8) can not open .vcf files, so we have build a workaround. 543 | * 544 | * @return string 545 | */ 546 | public function buildVCalendar() 547 | { 548 | // init dates 549 | $dtstart = date("Ymd") . "T" . date("Hi") . "00"; 550 | $dtend = date("Ymd") . "T" . date("Hi") . "01"; 551 | 552 | // init string 553 | $string = "BEGIN:VCALENDAR\n"; 554 | $string .= "VERSION:2.0\n"; 555 | $string .= "BEGIN:VEVENT\n"; 556 | $string .= "DTSTART;TZID=Europe/London:" . $dtstart . "\n"; 557 | $string .= "DTEND;TZID=Europe/London:" . $dtend . "\n"; 558 | $string .= "SUMMARY:Click attached contact below to save to your contacts\n"; 559 | $string .= "DTSTAMP:" . $dtstart . "Z\n"; 560 | $string .= "ATTACH;VALUE=BINARY;ENCODING=BASE64;FMTTYPE=text/directory;\n"; 561 | $string .= " X-APPLE-FILENAME=" . $this->getFilename() . "." . $this->getFileExtension() . ":\n"; 562 | 563 | // base64 encode it so that it can be used as an attachemnt to the "dummy" calendar appointment 564 | $b64vcard = base64_encode($this->buildVCard()); 565 | 566 | // chunk the single long line of b64 text in accordance with RFC2045 567 | // (and the exact line length determined from the original .ics file exported from Apple calendar 568 | $b64mline = chunk_split($b64vcard, 74, "\n"); 569 | 570 | // need to indent all the lines by 1 space for the iphone (yes really?!!) 571 | $b64final = preg_replace('/(.+)/', ' $1', $b64mline); 572 | $string .= $b64final; 573 | 574 | // output the correctly formatted encoded text 575 | $string .= "END:VEVENT\n"; 576 | $string .= "END:VCALENDAR\n"; 577 | 578 | // return 579 | return $string; 580 | } 581 | 582 | /** 583 | * Returns the browser user agent string. 584 | * 585 | * @return string 586 | */ 587 | protected function getUserAgent() 588 | { 589 | if (array_key_exists('HTTP_USER_AGENT', $_SERVER)) { 590 | $browser = strtolower($_SERVER['HTTP_USER_AGENT']); 591 | } else { 592 | $browser = 'unknown'; 593 | } 594 | 595 | return $browser; 596 | } 597 | 598 | /** 599 | * Decode 600 | * 601 | * @param string $value The value to decode 602 | * @return string decoded 603 | */ 604 | private function decode($value) 605 | { 606 | // convert cyrlic, greek or other caracters to ASCII characters 607 | return Transliterator::transliterate($value); 608 | } 609 | 610 | /** 611 | * Download a vcard or vcal file to the browser. 612 | */ 613 | public function download() 614 | { 615 | // define output 616 | $output = $this->getOutput(); 617 | 618 | foreach ($this->getHeaders(false) as $header) { 619 | header($header); 620 | } 621 | 622 | // echo the output and it will be a download 623 | echo $output; 624 | } 625 | 626 | /** 627 | * Fold a line according to RFC2425 section 5.8.1. 628 | * 629 | * @link http://tools.ietf.org/html/rfc2425#section-5.8.1 630 | * @param string $text 631 | * @return mixed 632 | */ 633 | protected function fold($text) 634 | { 635 | if (strlen($text) <= 75) { 636 | return $text; 637 | } 638 | 639 | // The chunk_split_unicode creates a huge memory footprint when used on long strings (EG photos are base64 10MB results in > 1GB memory usage) 640 | // So check if the string is ASCII (7 bit) and if it is use the built in way RE: https://github.com/jeroendesloovere/vcard/issues/153 641 | if ($this->is_ascii($text)) { 642 | return substr(chunk_split($text, 75, "\r\n "), 0, -3); 643 | } 644 | 645 | // split, wrap and trim trailing separator 646 | return substr($this->chunk_split_unicode($text, 75, "\r\n "), 0, -3); 647 | } 648 | 649 | 650 | /** 651 | * Determine if string is pure 7bit ascii 652 | * @link https://pageconfig.com/post/how-to-validate-ascii-text-in-php 653 | * 654 | * @param string $string 655 | * @return bool 656 | */ 657 | protected function is_ascii($string = '' ) { 658 | $num = 0; 659 | while( isset( $string[$num] ) ) { 660 | if( ord( $string[$num] ) & 0x80 ) { 661 | return false; 662 | } 663 | $num++; 664 | } 665 | return true; 666 | } 667 | 668 | /** 669 | * multibyte word chunk split 670 | * @link http://php.net/manual/en/function.chunk-split.php#107711 671 | * 672 | * @param string $body The string to be chunked. 673 | * @param integer $chunklen The chunk length. 674 | * @param string $end The line ending sequence. 675 | * @return string Chunked string 676 | */ 677 | protected function chunk_split_unicode($body, $chunklen = 76, $end = "\r\n") 678 | { 679 | $array = array_chunk( 680 | preg_split("//u", $body, -1, PREG_SPLIT_NO_EMPTY), $chunklen); 681 | $body = ""; 682 | foreach ($array as $item) { 683 | $body .= join("", $item) . $end; 684 | } 685 | return $body; 686 | } 687 | 688 | /** 689 | * Escape newline characters according to RFC2425 section 5.8.4. 690 | * 691 | * @link http://tools.ietf.org/html/rfc2425#section-5.8.4 692 | * @param string $text 693 | * @return string 694 | */ 695 | protected function escape($text) 696 | { 697 | if ($text === null) { 698 | return null; 699 | } 700 | 701 | $text = str_replace("\r\n", "\\n", $text); 702 | $text = str_replace("\n", "\\n", $text); 703 | 704 | return $text; 705 | } 706 | 707 | /** 708 | * Get output as string 709 | * @deprecated in the future 710 | * 711 | * @return string 712 | */ 713 | public function get() 714 | { 715 | return $this->getOutput(); 716 | } 717 | 718 | /** 719 | * Get charset 720 | * 721 | * @return string 722 | */ 723 | public function getCharset() 724 | { 725 | return $this->charset; 726 | } 727 | 728 | /** 729 | * Get charset string 730 | * 731 | * @return string 732 | */ 733 | public function getCharsetString() 734 | { 735 | return ';CHARSET=' . $this->charset; 736 | } 737 | 738 | /** 739 | * Get content type 740 | * 741 | * @return string 742 | */ 743 | public function getContentType() 744 | { 745 | return ($this->isIOS7()) ? 746 | 'text/x-vcalendar' : 'text/x-vcard'; 747 | } 748 | 749 | /** 750 | * Get filename 751 | * 752 | * @return string 753 | */ 754 | public function getFilename() 755 | { 756 | if (!$this->filename) { 757 | return 'unknown'; 758 | } 759 | 760 | return $this->filename; 761 | } 762 | 763 | /** 764 | * Get file extension 765 | * 766 | * @return string 767 | */ 768 | public function getFileExtension() 769 | { 770 | return ($this->isIOS7()) ? 771 | 'ics' : 'vcf'; 772 | } 773 | 774 | /** 775 | * Get headers 776 | * 777 | * @param bool $asAssociative 778 | * @return array 779 | */ 780 | public function getHeaders($asAssociative) 781 | { 782 | $contentType = $this->getContentType() . '; charset=' . $this->getCharset(); 783 | $contentDisposition = 'attachment; filename=' . $this->getFilename() . '.' . $this->getFileExtension(); 784 | $contentLength = mb_strlen($this->getOutput(), '8bit'); 785 | $connection = 'close'; 786 | 787 | if ((bool)$asAssociative) { 788 | return [ 789 | 'Content-type' => $contentType, 790 | 'Content-Disposition' => $contentDisposition, 791 | 'Content-Length' => $contentLength, 792 | 'Connection' => $connection, 793 | ]; 794 | } 795 | 796 | return [ 797 | 'Content-type: ' . $contentType, 798 | 'Content-Disposition: ' . $contentDisposition, 799 | 'Content-Length: ' . $contentLength, 800 | 'Connection: ' . $connection, 801 | ]; 802 | } 803 | 804 | /** 805 | * Get output as string 806 | * iOS devices (and safari < iOS 8 in particular) can not read .vcf (= vcard) files. 807 | * So I build a workaround to build a .ics (= vcalender) file. 808 | * 809 | * @return string 810 | */ 811 | public function getOutput() 812 | { 813 | $output = ($this->isIOS7()) ? 814 | $this->buildVCalendar() : $this->buildVCard(); 815 | 816 | return $output; 817 | } 818 | 819 | /** 820 | * Get properties 821 | * 822 | * @return array 823 | */ 824 | public function getProperties() 825 | { 826 | return $this->properties; 827 | } 828 | 829 | /** 830 | * Has property 831 | * 832 | * @param string $key 833 | * @return bool 834 | */ 835 | public function hasProperty($key) 836 | { 837 | $properties = $this->getProperties(); 838 | 839 | foreach ($properties as $property) { 840 | if ($property['key'] === $key && $property['value'] !== '') { 841 | return true; 842 | } 843 | } 844 | 845 | return false; 846 | } 847 | 848 | /** 849 | * Is iOS - Check if the user is using an iOS-device 850 | * 851 | * @return bool 852 | */ 853 | public function isIOS() 854 | { 855 | // get user agent 856 | $browser = $this->getUserAgent(); 857 | 858 | return (strpos($browser, 'iphone') || strpos($browser, 'ipod') || strpos($browser, 'ipad')); 859 | } 860 | 861 | /** 862 | * Is iOS less than 7 (should cal wrapper be returned) 863 | * 864 | * @return bool 865 | */ 866 | public function isIOS7() 867 | { 868 | return ($this->isIOS() && $this->shouldAttachmentBeCal()); 869 | } 870 | 871 | /** 872 | * Save to a file 873 | * 874 | * @return void 875 | */ 876 | public function save() 877 | { 878 | $file = $this->getFilename() . '.' . $this->getFileExtension(); 879 | 880 | // Add save path if given 881 | if (null !== $this->savePath) { 882 | $file = $this->savePath . $file; 883 | } 884 | 885 | file_put_contents( 886 | $file, 887 | $this->getOutput() 888 | ); 889 | } 890 | 891 | /** 892 | * Set charset 893 | * 894 | * @param mixed $charset 895 | * @return void 896 | */ 897 | public function setCharset($charset) 898 | { 899 | $this->charset = $charset; 900 | } 901 | 902 | /** 903 | * Set filename 904 | * 905 | * @param mixed $value 906 | * @param bool $overwrite [optional] Default overwrite is true 907 | * @param string $separator [optional] Default separator is an underscore '_' 908 | * @return void 909 | */ 910 | public function setFilename($value, $overwrite = true, $separator = '_') 911 | { 912 | // recast to string if $value is array 913 | if (is_array($value)) { 914 | $value = implode($separator, $value); 915 | } 916 | 917 | // trim unneeded values 918 | $value = trim($value, $separator); 919 | 920 | // remove all spaces 921 | $value = preg_replace('/\s+/', $separator, $value); 922 | 923 | // if value is empty, stop here 924 | if (empty($value)) { 925 | return; 926 | } 927 | 928 | // decode value + lowercase the string 929 | $value = strtolower($this->decode($value)); 930 | 931 | // urlize this part 932 | $value = Transliterator::urlize($value); 933 | 934 | // overwrite filename or add to filename using a prefix in between 935 | $this->filename = ($overwrite) ? 936 | $value : $this->filename . $separator . $value; 937 | } 938 | 939 | /** 940 | * Set the save path directory 941 | * 942 | * @param string $savePath Save Path 943 | * @throws VCardException 944 | */ 945 | public function setSavePath($savePath) 946 | { 947 | if (!is_dir($savePath)) { 948 | throw VCardException::outputDirectoryNotExists(); 949 | } 950 | 951 | // Add trailing directory separator the save path 952 | if (substr($savePath, -1) != DIRECTORY_SEPARATOR) { 953 | $savePath .= DIRECTORY_SEPARATOR; 954 | } 955 | 956 | $this->savePath = $savePath; 957 | } 958 | 959 | /** 960 | * Set property 961 | * 962 | * @param string $element The element name you want to set, f.e.: name, email, phoneNumber, ... 963 | * @param string $key 964 | * @param string $value 965 | * @throws VCardException 966 | */ 967 | private function setProperty($element, $key, $value) 968 | { 969 | if (!in_array($element, $this->multiplePropertiesForElementAllowed) 970 | && isset($this->definedElements[$element]) 971 | ) { 972 | throw VCardException::elementAlreadyExists($element); 973 | } 974 | 975 | // we define that we set this element 976 | $this->definedElements[$element] = true; 977 | 978 | // adding property 979 | $this->properties[] = [ 980 | 'key' => $key, 981 | 'value' => $value 982 | ]; 983 | } 984 | 985 | /** 986 | * Checks if we should return vcard in cal wrapper 987 | * 988 | * @return bool 989 | */ 990 | protected function shouldAttachmentBeCal() 991 | { 992 | $browser = $this->getUserAgent(); 993 | 994 | $matches = []; 995 | preg_match('/os (\d+)_(\d+)\s+/', $browser, $matches); 996 | $version = isset($matches[1]) ? ((int)$matches[1]) : 999; 997 | 998 | return ($version < 8); 999 | } 1000 | } 1001 | --------------------------------------------------------------------------------