Completed
Pull Request — master (#70)
by
unknown
04:55
created

VCard::addName()   B

Complexity

Conditions 2
Paths 2

Size

Total Lines 39
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 10
Bugs 1 Features 2
Metric Value
c 10
b 1
f 2
dl 0
loc 39
rs 8.8571
cc 2
eloc 24
nc 2
nop 5
1
<?php
2
3
namespace JeroenDesloovere\VCard;
4
5
/*
6
 * This file is part of the VCard PHP Class from Jeroen Desloovere.
7
 *
8
 * For the full copyright and license information, please view the license
9
 * file that was distributed with this source code.
10
 */
11
12
use Behat\Transliterator\Transliterator;
13
14
/**
15
 * VCard PHP Class to generate .vcard files and save them to a file or output as a download.
16
 *
17
 * @author Jeroen Desloovere <[email protected]>
18
 */
19
class VCard
20
{
21
    /**
22
     * definedElements
23
     *
24
     * @var array
25
     */
26
    private $definedElements;
27
28
    /**
29
     * Filename
30
     *
31
     * @var string
32
     */
33
    private $filename;
34
35
    /**
36
     * Multiple properties for element allowed
37
     *
38
     * @var array
39
     */
40
    private $multiplePropertiesForElementAllowed = array(
41
        'email',
42
        'address',
43
        'phoneNumber',
44
        'url'
45
    );
46
47
    /**
48
     * Properties
49
     *
50
     * @var array
51
     */
52
    private $properties;
53
54
    /**
55
     * Default Charset
56
     *
57
     * @var string
58
     */
59
    public $charset = 'utf-8';
60
61
    /**
62
     * Add address
63
     *
64
     * @param  string [optional] $name
65
     * @param  string [optional] $extended
66
     * @param  string [optional] $street
67
     * @param  string [optional] $city
68
     * @param  string [optional] $region
69
     * @param  string [optional] $zip
70
     * @param  string [optional] $country
71
     * @param  string [optional] $type
72
     *                                     $type may be DOM | INTL | POSTAL | PARCEL | HOME | WORK
73
     *                                     or any combination of these: e.g. "WORK;PARCEL;POSTAL"
74
     * @return $this
75
     */
76
    public function addAddress(
77
        $name = '',
78
        $extended = '',
79
        $street = '',
80
        $city = '',
81
        $region = '',
82
        $zip = '',
83
        $country = '',
84
        $type = 'WORK;POSTAL'
85
    ) {
86
        // init value
87
        $value = $name . ';' . $extended . ';' . $street . ';' . $city . ';' . $region . ';' . $zip . ';' . $country;
88
89
        // set property
90
        $this->setProperty(
91
            'address',
92
            'ADR' . (($type != '') ? ';' . $type : '') . $this->getCharsetString(),
93
            $value
94
        );
95
96
        return $this;
97
    }
98
99
    /**
100
     * Add birthday
101
     *
102
     * @param  string $date Format is YYYY-MM-DD
103
     * @return $this
104
     */
105
    public function addBirthday($date)
106
    {
107
        $this->setProperty(
108
            'birthday',
109
            'BDAY',
110
            $date
111
        );
112
113
        return $this;
114
    }
115
116
    /**
117
     * Add company
118
     *
119
     * @param  string $company
120
     * @return $this
121
     */
122
    public function addCompany($company)
123
    {
124
        $this->setProperty(
125
            'company',
126
            'ORG' . $this->getCharsetString(),
127
            $company
128
        );
129
130
        // if filename is empty, add to filename
131
        if ($this->getFilename() === null) {
132
            $this->setFilename($company);
133
        }
134
135
        return $this;
136
    }
137
138
    /**
139
     * Add email
140
     *
141
     * @param  string            $address The e-mail address
142
     * @param  string [optional] $type    The type of the email address
143
     *                                    $type may be  PREF | WORK | HOME
144
     *                                    or any combination of these: e.g. "PREF;WORK"
145
     * @return $this
146
     */
147
    public function addEmail($address, $type = '')
148
    {
149
        $this->setProperty(
150
            'email',
151
            'EMAIL;INTERNET' . (($type != '') ? ';' . $type : ''),
152
            $address
153
        );
154
155
        return $this;
156
    }
157
158
    /**
159
     * Add jobtitle
160
     *
161
     * @param  string $jobtitle The jobtitle for the person.
162
     * @return $this
163
     */
164
    public function addJobtitle($jobtitle)
165
    {
166
        $this->setProperty(
167
            'jobtitle',
168
            'TITLE' . $this->getCharsetString(),
169
            $jobtitle
170
        );
171
172
        return $this;
173
    }
174
175
    /**
176
     * Add a photo or logo (depending on property name)
177
     *
178
     * @param  string              $property LOGO|PHOTO
179
     * @param  string              $url      image url or filename
180
     * @param  bool                $include  Do we include the image in our vcard or not?
181
     * @throws VCardMediaException if file is empty or not an image file
182
     */
183
    private function addMedia($property, $url, $include = true, $element)
184
    {
185
        if ($include) {
186
            $value = file_get_contents($url);
187
188
            if (!$value) {
189
                throw new VCardMediaException('Nothing returned from URL.');
190
            }
191
192
            $value = base64_encode($value);
193
194
            $finfo = finfo_open(FILEINFO_MIME_TYPE);
195
            $mimetype = finfo_file($finfo, 'data://application/octet-stream;base64,' . $value);
196
            finfo_close($finfo);
197
198
            if (preg_match('/^image\//', $mimetype) !== 1) {
199
                throw new VCardMediaException('Returned data aren\'t an image.');
200
            }
201
202
            $type = strtoupper(str_replace('image/', '', $mimetype));
203
204
            $property .= ";ENCODING=b;TYPE=" . $type;
205
        } else {
206
            if (filter_var($url, FILTER_VALIDATE_URL) !== FALSE) {
207
                $propertySuffix = ';VALUE=URL';
208
209
                $headers = get_headers($url);
210
211
                $imageTypeMatched = false;
212
                $fileType = null;
213
214
                foreach ($headers as $header) {
215
                    if (preg_match('/Content-Type:\simage\/([a-z]+)/i', $header, $m)) {
216
                        $fileType = $m[1];
217
                        $imageTypeMatched = true;
218
                    }
219
                }
220
221
                if (!$imageTypeMatched) {
222
                    throw new VCardMediaException('Returned data isn\'t an image.');
223
                }
224
225
                $propertySuffix .= ';TYPE=' . strtoupper($fileType);
226
227
                $property = $property . $propertySuffix;
228
                $value = $url;
229
            } else {
230
                $value = $url;
231
            }
232
        }
233
234
        $this->setProperty(
235
            $element,
236
            $property,
237
            $value
238
        );
239
    }
240
241
    /**
242
     * Add name
243
     *
244
     * @param  string [optional] $lastName
245
     * @param  string [optional] $firstName
246
     * @param  string [optional] $additional
247
     * @param  string [optional] $prefix
248
     * @param  string [optional] $suffix
249
     * @return $this
250
     */
251
    public function addName(
252
        $lastName = '',
253
        $firstName = '',
254
        $additional = '',
255
        $prefix = '',
256
        $suffix = ''
257
    ) {
258
        // define values with non-empty values
259
        $values = array_filter(array(
260
            $prefix,
261
            $firstName,
262
            $additional,
263
            $lastName,
264
            $suffix,
265
        ));
266
267
        // define filename
268
        $this->setFilename($values);
269
270
        // set property
271
        $property = $lastName . ';' . $firstName . ';' . $additional . ';' . $prefix . ';' . $suffix;
272
        $this->setProperty(
273
            'name',
274
            'N' . $this->getCharsetString(),
275
            $property
276
        );
277
278
        // is property FN set?
279
        if (!$this->hasProperty('FN')) {
280
            // set property
281
            $this->setProperty(
282
                'fullname',
283
                'FN' . $this->getCharsetString(),
284
                trim(implode(' ', $values))
285
            );
286
        }
287
288
        return $this;
289
    }
290
291
    /**
292
     * Add note
293
     *
294
     * @param  string $note
295
     * @return $this
296
     */
297
    public function addNote($note)
298
    {
299
        $this->setProperty(
300
            'note',
301
            'NOTE' . $this->getCharsetString(),
302
            $note
303
        );
304
305
        return $this;
306
    }
307
308
    /**
309
     * Add phone number
310
     *
311
     * @param  string            $number
312
     * @param  string [optional] $type
313
     *                                   Type may be PREF | WORK | HOME | VOICE | FAX | MSG |
314
     *                                   CELL | PAGER | BBS | CAR | MODEM | ISDN | VIDEO
315
     *                                   or any senseful combination, e.g. "PREF;WORK;VOICE"
316
     * @return $this
317
     */
318
    public function addPhoneNumber($number, $type = '')
319
    {
320
        $this->setProperty(
321
            'phoneNumber',
322
            'TEL' . (($type != '') ? ';' . $type : ''),
323
            $number
324
        );
325
326
        return $this;
327
    }
328
329
    /**
330
     * Add Photo
331
     *
332
     * @param  string $url     image url or filename
333
     * @param  bool   $include Include the image in our vcard?
334
     * @return $this
335
     */
336
    public function addPhoto($url, $include = true)
337
    {
338
        $this->addMedia(
339
            'PHOTO',
340
            $url,
341
            $include,
342
            'photo'
343
        );
344
345
        return $this;
346
    }
347
348
    /**
349
     * Add URL
350
     *
351
     * @param  string            $url
352
     * @param  string [optional] $type Type may be WORK | HOME
353
     * @return $this
354
     */
355
    public function addURL($url, $type = '')
356
    {
357
        $this->setProperty(
358
            'url',
359
            'URL' . (($type != '') ? ';' . $type : ''),
360
            $url
361
        );
362
363
        return $this;
364
    }
365
366
    /**
367
     * Build VCard (.vcf)
368
     *
369
     * @return string
370
     */
371
    public function buildVCard()
372
    {
373
        // init string
374
        $string = "BEGIN:VCARD\r\n";
375
        $string .= "VERSION:3.0\r\n";
376
        $string .= "REV:" . date("Y-m-d") . "T" . date("H:i:s") . "Z\r\n";
377
378
        // loop all properties
379
        $properties = $this->getProperties();
380
        foreach ($properties as $property) {
381
            // add to string
382
            $string .= $this->fold($property['key'] . ':' . $this->escape($property['value']) . "\r\n");
383
        }
384
385
        // add to string
386
        $string .= "END:VCARD\r\n";
387
388
        // return
389
        return $string;
390
    }
391
392
    /**
393
     * Build VCalender (.ics) - Safari (< iOS 8) can not open .vcf files, so we have build a workaround.
394
     *
395
     * @return string
396
     */
397
    public function buildVCalendar()
398
    {
399
        // init dates
400
        $dtstart = date("Ymd") . "T" . date("Hi") . "00";
401
        $dtend = date("Ymd") . "T" . date("Hi") . "01";
402
403
        // init string
404
        $string = "BEGIN:VCALENDAR\n";
405
        $string .= "VERSION:2.0\n";
406
        $string .= "BEGIN:VEVENT\n";
407
        $string .= "DTSTART;TZID=Europe/London:" . $dtstart . "\n";
408
        $string .= "DTEND;TZID=Europe/London:" . $dtend . "\n";
409
        $string .= "SUMMARY:Click attached contact below to save to your contacts\n";
410
        $string .= "DTSTAMP:" . $dtstart . "Z\n";
411
        $string .= "ATTACH;VALUE=BINARY;ENCODING=BASE64;FMTTYPE=text/directory;\n";
412
        $string .= " X-APPLE-FILENAME=" . $this->getFilename() . "." . $this->getFileExtension() . ":\n";
413
414
        // base64 encode it so that it can be used as an attachemnt to the "dummy" calendar appointment
415
        $b64vcard = base64_encode($this->buildVCard());
416
417
        // chunk the single long line of b64 text in accordance with RFC2045
418
        // (and the exact line length determined from the original .ics file exported from Apple calendar
419
        $b64mline = chunk_split($b64vcard, 74, "\n");
420
421
        // need to indent all the lines by 1 space for the iphone (yes really?!!)
422
        $b64final = preg_replace('/(.+)/', ' $1', $b64mline);
423
        $string .= $b64final;
424
425
        // output the correctly formatted encoded text
426
        $string .= "END:VEVENT\n";
427
        $string .= "END:VCALENDAR\n";
428
429
        // return
430
        return $string;
431
    }
432
433
    /**
434
     * Returns the browser user agent string.
435
     *
436
     * @return string
437
     */
438
    protected function getUserAgent()
0 ignored issues
show
Coding Style introduced by
getUserAgent uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
439
    {
440
        if (array_key_exists('HTTP_USER_AGENT', $_SERVER)) {
441
            $browser = strtolower($_SERVER['HTTP_USER_AGENT']);
442
        } else {
443
            $browser = 'unknown';
444
        }
445
446
        return $browser;
447
    }
448
449
    /**
450
     * Decode
451
     *
452
     * @param  string $value The value to decode
453
     * @return string decoded
454
     */
455
    private function decode($value)
456
    {
457
        // convert cyrlic, greek or other caracters to ASCII characters
458
        return Transliterator::transliterate($value);
459
    }
460
461
    /**
462
     * Download a vcard or vcal file to the browser.
463
     */
464
    public function download()
465
    {
466
        // define output
467
        $output = $this->getOutput();
468
469
        foreach ($this->getHeaders(false) as $header) {
470
            header($header);
471
        }
472
473
        // echo the output and it will be a download
474
        echo $output;
475
    }
476
477
    /**
478
     * Fold a line according to RFC2425 section 5.8.1.
479
     *
480
     * @link http://tools.ietf.org/html/rfc2425#section-5.8.1
481
     * @param  string $text
482
     * @return mixed
483
     */
484
    protected function fold($text)
485
    {
486
        if (strlen($text) <= 75) {
487
            return $text;
488
        }
489
490
        // split, wrap and trim trailing separator
491
        return substr(chunk_split($text, 73, "\r\n "), 0, -3);
492
    }
493
    
494
    /**
495
     * Escape newline characters according to RFC2425 section 5.8.4.
496
     *
497
     * @link http://tools.ietf.org/html/rfc2425#section-5.8.4
498
     * @param  string $text
499
     * @return string
500
     */
501
    protected function escape($text)
502
    {
503
        $text = str_replace("\r\n", "\\n", $text);
504
        $text = str_replace("\n", "\\n", $text);
505
        
506
        return $text;
507
    }
508
509
    /**
510
     * Get output as string
511
     * @deprecated in the future
512
     *
513
     * @return string
514
     */
515
    public function get()
516
    {
517
        return $this->getOutput();
518
    }
519
520
    /**
521
     * Get charset
522
     *
523
     * @return string
524
     */
525
    public function getCharset()
526
    {
527
        return $this->charset;
528
    }
529
530
    /**
531
     * Get charset string
532
     *
533
     * @return string
534
     */
535
    public function getCharsetString()
536
    {
537
        $charsetString = '';
538
        if ($this->charset == 'utf-8') {
539
            $charsetString = ';CHARSET=' . $this->charset;
540
        }
541
        return $charsetString;
542
    }
543
544
    /**
545
     * Get content type
546
     *
547
     * @return string
548
     */
549
    public function getContentType()
550
    {
551
        return ($this->isIOS7()) ?
552
            'text/x-vcalendar' : 'text/x-vcard';
553
    }
554
555
    /**
556
     * Get filename
557
     *
558
     * @return string
559
     */
560
    public function getFilename()
561
    {
562
        return $this->filename;
563
    }
564
565
    /**
566
     * Get file extension
567
     *
568
     * @return string
569
     */
570
    public function getFileExtension()
571
    {
572
        return ($this->isIOS7()) ?
573
            'ics' : 'vcf';
574
    }
575
576
    /**
577
     * Get headers
578
     *
579
     * @param  bool  $asAssociative
580
     * @return array
581
     */
582
    public function getHeaders($asAssociative)
583
    {
584
        $contentType        = $this->getContentType() . '; charset=' . $this->getCharset();
585
        $contentDisposition = 'attachment; filename=' . $this->getFilename() . '.' . $this->getFileExtension();
586
        $contentLength      = strlen($this->getOutput());
587
        $connection         = 'close';
588
589
        if ((bool) $asAssociative) {
590
            return array(
591
                'Content-type'        => $contentType,
592
                'Content-Disposition' => $contentDisposition,
593
                'Content-Length'      => $contentLength,
594
                'Connection'          => $connection,
595
            );
596
        }
597
598
        return array(
599
            'Content-type: ' . $contentType,
600
            'Content-Disposition: ' . $contentDisposition,
601
            'Content-Length: ' . $contentLength,
602
            'Connection: ' . $connection,
603
        );
604
    }
605
606
    /**
607
     * Get output as string
608
     * iOS devices (and safari < iOS 8 in particular) can not read .vcf (= vcard) files.
609
     * So I build a workaround to build a .ics (= vcalender) file.
610
     *
611
     * @return string
612
     */
613
    public function getOutput()
614
    {
615
        $output = ($this->isIOS7()) ?
616
            $this->buildVCalendar() : $this->buildVCard();
617
618
        return $output;
619
    }
620
621
    /**
622
     * Get properties
623
     *
624
     * @return array
625
     */
626
    public function getProperties()
627
    {
628
        return $this->properties;
629
    }
630
631
    /**
632
     * Has property
633
     *
634
     * @param  string $key
635
     * @return bool
636
     */
637
    public function hasProperty($key)
638
    {
639
        $properties = $this->getProperties();
640
641
        foreach ($properties as $property) {
642
            if ($property['key'] === $key && $property['value'] !== '') {
643
                return true;
644
            }
645
        }
646
647
        return false;
648
    }
649
650
    /**
651
     * Is iOS - Check if the user is using an iOS-device
652
     *
653
     * @return bool
654
     */
655
    public function isIOS()
656
    {
657
        // get user agent
658
        $browser = $this->getUserAgent();
659
660
        return (strpos($browser, 'iphone') || strpos($browser, 'ipod') || strpos($browser, 'ipad'));
661
    }
662
663
    /**
664
     * Is iOS less than 7 (should cal wrapper be returned)
665
     *
666
     * @return bool
667
     */
668
    public function isIOS7()
669
    {
670
        return ($this->isIOS() && $this->shouldAttachmentBeCal());
671
    }
672
673
    /**
674
     * Save to a file
675
     *
676
     * @return void
677
     */
678
    public function save()
679
    {
680
        $file = $this->getFilename() . '.' . $this->getFileExtension();
681
682
        file_put_contents(
683
            $file,
684
            $this->getOutput()
685
        );
686
    }
687
688
    /**
689
     * Set charset
690
     *
691
     * @param  mixed  $charset
692
     * @return void
693
     */
694
    public function setCharset($charset)
695
    {
696
        $this->charset = $charset;
697
    }
698
699
    /**
700
     * Set filename
701
     *
702
     * @param  mixed  $value
703
     * @param  bool   $overwrite [optional] Default overwrite is true
704
     * @param  string $separator [optional] Default separator is an underscore '_'
705
     * @return void
706
     */
707
    public function setFilename($value, $overwrite = true, $separator = '_')
708
    {
709
        // recast to string if $value is array
710
        if (is_array($value)) {
711
            $value = implode($separator, $value);
712
        }
713
714
        // trim unneeded values
715
        $value = trim($value, $separator);
716
717
        // remove all spaces
718
        $value = preg_replace('/\s+/', $separator, $value);
719
720
        // if value is empty, stop here
721
        if (empty($value)) {
722
            return;
723
        }
724
725
        // decode value + lowercase the string
726
        $value = strtolower($this->decode($value));
727
728
        // urlize this part
729
        $value = Transliterator::urlize($value);
730
731
        // overwrite filename or add to filename using a prefix in between
732
        $this->filename = ($overwrite) ?
733
            $value : $this->filename . $separator . $value;
734
    }
735
736
    /**
737
     * Set property
738
     *
739
     * @param  string $element The element name you want to set, f.e.: name, email, phoneNumber, ...
740
     * @param  string $key
741
     * @param  string $value
742
     * @return void
743
     */
744
    private function setProperty($element, $key, $value)
745
    {
746
        if (!in_array($element, $this->multiplePropertiesForElementAllowed)
747
            && isset($this->definedElements[$element])
748
        ) {
749
            throw new Exception('You can only set "' . $element . '" once.');
750
        }
751
752
        // we define that we set this element
753
        $this->definedElements[$element] = true;
754
755
        // adding property
756
        $this->properties[] = array(
757
            'key' => $key,
758
            'value' => $value
759
        );
760
    }
761
762
    /**
763
     * Checks if we should return vcard in cal wrapper
764
     *
765
     * @return bool
766
     */
767
    protected function shouldAttachmentBeCal()
768
    {
769
        $browser = $this->getUserAgent();
770
771
        $matches = array();
772
        preg_match('/os (\d+)_(\d+)\s+/', $browser, $matches);
773
        $version = isset($matches[1]) ? ((int) $matches[1]) : 999;
774
775
        return ($version < 8);
776
    }
777
}
778