Issues (28)

src/VCard.php (1 issue)

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
class VCard
18
{
19
    /**
20
     * definedElements
21
     *
22
     * @var array
23
     */
24
    private $definedElements;
25
26
    /**
27
     * Filename
28
     *
29
     * @var string
30
     */
31
    private $filename;
32
33
    /**
34
     * Save Path
35
     *
36
     * @var string
37
     */
38
    private $savePath = null;
39
40
    /**
41
     * Multiple properties for element allowed
42
     *
43
     * @var array
44
     */
45
    private $multiplePropertiesForElementAllowed = [
46
        'email',
47
        'address',
48
        'phoneNumber',
49
        'url',
50
        'label'
51
    ];
52
53
    /**
54
     * Properties
55
     *
56
     * @var array
57
     */
58
    private $properties;
59
60
    /**
61
     * Default Charset
62
     *
63
     * @var string
64
     */
65
    public $charset = 'utf-8';
66
67
    /**
68
     * Add address
69
     *
70
     * @param  string [optional] $name
71
     * @param  string [optional] $extended
72
     * @param  string [optional] $street
73
     * @param  string [optional] $city
74
     * @param  string [optional] $region
75
     * @param  string [optional] $zip
76
     * @param  string [optional] $country
77
     * @param  string [optional] $type
78
     *                                     $type may be DOM | INTL | POSTAL | PARCEL | HOME | WORK
79
     *                                     or any combination of these: e.g. "WORK;PARCEL;POSTAL"
80
     * @return $this
81
     */
82
    public function addAddress(
83
        $name = '',
84
        $extended = '',
85
        $street = '',
86
        $city = '',
87
        $region = '',
88
        $zip = '',
89
        $country = '',
90
        $type = 'WORK;POSTAL'
91
    ) {
92
        // init value
93
        $value = $name . ';' . $extended . ';' . $street . ';' . $city . ';' . $region . ';' . $zip . ';' . $country;
94
95
        // set property
96
        $this->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 : ''),
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, $include = true, $element)
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/') {
0 ignored issues
show
The condition is_string($mimeType) is always true.
Loading history...
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
            $include,
438
            'logo'
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
            $include,
474
            'photo'
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
        // split, wrap and trim trailing separator
640
        return substr($this->chunk_split_unicode($text, 75, "\r\n "), 0, -3);
641
    }
642
643
    /**
644
     * multibyte word chunk split
645
     * @link http://php.net/manual/en/function.chunk-split.php#107711
646
     * 
647
     * @param  string  $body     The string to be chunked.
648
     * @param  integer $chunklen The chunk length.
649
     * @param  string  $end      The line ending sequence.
650
     * @return string            Chunked string
651
     */
652
    protected function chunk_split_unicode($body, $chunklen = 76, $end = "\r\n")
653
    {
654
        $array = array_chunk(
655
            preg_split("//u", $body, -1, PREG_SPLIT_NO_EMPTY), $chunklen);
656
        $body = "";
657
        foreach ($array as $item) {
658
            $body .= join("", $item) . $end;
659
        }
660
        return $body;
661
    }
662
663
    /**
664
     * Escape newline characters according to RFC2425 section 5.8.4.
665
     *
666
     * @link http://tools.ietf.org/html/rfc2425#section-5.8.4
667
     * @param  string $text
668
     * @return string
669
     */
670
    protected function escape($text)
671
    {
672
        $text = str_replace("\r\n", "\\n", $text);
673
        $text = str_replace("\n", "\\n", $text);
674
675
        return $text;
676
    }
677
678
    /**
679
     * Get output as string
680
     * @deprecated in the future
681
     *
682
     * @return string
683
     */
684
    public function get()
685
    {
686
        return $this->getOutput();
687
    }
688
689
    /**
690
     * Get charset
691
     *
692
     * @return string
693
     */
694
    public function getCharset()
695
    {
696
        return $this->charset;
697
    }
698
699
    /**
700
     * Get charset string
701
     *
702
     * @return string
703
     */
704
    public function getCharsetString()
705
    {
706
        return ';CHARSET=' . $this->charset;
707
    }
708
709
    /**
710
     * Get content type
711
     *
712
     * @return string
713
     */
714
    public function getContentType()
715
    {
716
        return ($this->isIOS7()) ?
717
            'text/x-vcalendar' : 'text/x-vcard';
718
    }
719
720
    /**
721
     * Get filename
722
     *
723
     * @return string
724
     */
725
    public function getFilename()
726
    {
727
        if (!$this->filename) {
728
            return 'unknown';
729
        }
730
731
        return $this->filename;
732
    }
733
734
    /**
735
     * Get file extension
736
     *
737
     * @return string
738
     */
739
    public function getFileExtension()
740
    {
741
        return ($this->isIOS7()) ?
742
            'ics' : 'vcf';
743
    }
744
745
    /**
746
     * Get headers
747
     *
748
     * @param  bool $asAssociative
749
     * @return array
750
     */
751
    public function getHeaders($asAssociative)
752
    {
753
        $contentType = $this->getContentType() . '; charset=' . $this->getCharset();
754
        $contentDisposition = 'attachment; filename=' . $this->getFilename() . '.' . $this->getFileExtension();
755
        $contentLength = mb_strlen($this->getOutput(), '8bit');
756
        $connection = 'close';
757
758
        if ((bool)$asAssociative) {
759
            return [
760
                'Content-type' => $contentType,
761
                'Content-Disposition' => $contentDisposition,
762
                'Content-Length' => $contentLength,
763
                'Connection' => $connection,
764
            ];
765
        }
766
767
        return [
768
            'Content-type: ' . $contentType,
769
            'Content-Disposition: ' . $contentDisposition,
770
            'Content-Length: ' . $contentLength,
771
            'Connection: ' . $connection,
772
        ];
773
    }
774
775
    /**
776
     * Get output as string
777
     * iOS devices (and safari < iOS 8 in particular) can not read .vcf (= vcard) files.
778
     * So I build a workaround to build a .ics (= vcalender) file.
779
     *
780
     * @return string
781
     */
782
    public function getOutput()
783
    {
784
        $output = ($this->isIOS7()) ?
785
            $this->buildVCalendar() : $this->buildVCard();
786
787
        return $output;
788
    }
789
790
    /**
791
     * Get properties
792
     *
793
     * @return array
794
     */
795
    public function getProperties()
796
    {
797
        return $this->properties;
798
    }
799
800
    /**
801
     * Has property
802
     *
803
     * @param  string $key
804
     * @return bool
805
     */
806
    public function hasProperty($key)
807
    {
808
        $properties = $this->getProperties();
809
810
        foreach ($properties as $property) {
811
            if ($property['key'] === $key && $property['value'] !== '') {
812
                return true;
813
            }
814
        }
815
816
        return false;
817
    }
818
819
    /**
820
     * Is iOS - Check if the user is using an iOS-device
821
     *
822
     * @return bool
823
     */
824
    public function isIOS()
825
    {
826
        // get user agent
827
        $browser = $this->getUserAgent();
828
829
        return (strpos($browser, 'iphone') || strpos($browser, 'ipod') || strpos($browser, 'ipad'));
830
    }
831
832
    /**
833
     * Is iOS less than 7 (should cal wrapper be returned)
834
     *
835
     * @return bool
836
     */
837
    public function isIOS7()
838
    {
839
        return ($this->isIOS() && $this->shouldAttachmentBeCal());
840
    }
841
842
    /**
843
     * Save to a file
844
     *
845
     * @return void
846
     */
847
    public function save()
848
    {
849
        $file = $this->getFilename() . '.' . $this->getFileExtension();
850
851
        // Add save path if given
852
        if (null !== $this->savePath) {
853
            $file = $this->savePath . $file;
854
        }
855
856
        file_put_contents(
857
            $file,
858
            $this->getOutput()
859
        );
860
    }
861
862
    /**
863
     * Set charset
864
     *
865
     * @param  mixed $charset
866
     * @return void
867
     */
868
    public function setCharset($charset)
869
    {
870
        $this->charset = $charset;
871
    }
872
873
    /**
874
     * Set filename
875
     *
876
     * @param  mixed $value
877
     * @param  bool $overwrite [optional] Default overwrite is true
878
     * @param  string $separator [optional] Default separator is an underscore '_'
879
     * @return void
880
     */
881
    public function setFilename($value, $overwrite = true, $separator = '_')
882
    {
883
        // recast to string if $value is array
884
        if (is_array($value)) {
885
            $value = implode($separator, $value);
886
        }
887
888
        // trim unneeded values
889
        $value = trim($value, $separator);
890
891
        // remove all spaces
892
        $value = preg_replace('/\s+/', $separator, $value);
893
894
        // if value is empty, stop here
895
        if (empty($value)) {
896
            return;
897
        }
898
899
        // decode value + lowercase the string
900
        $value = strtolower($this->decode($value));
901
902
        // urlize this part
903
        $value = Transliterator::urlize($value);
904
905
        // overwrite filename or add to filename using a prefix in between
906
        $this->filename = ($overwrite) ?
907
            $value : $this->filename . $separator . $value;
908
    }
909
910
    /**
911
     * Set the save path directory
912
     *
913
     * @param  string $savePath Save Path
914
     * @throws VCardException
915
     */
916
    public function setSavePath($savePath)
917
    {
918
        if (!is_dir($savePath)) {
919
            throw VCardException::outputDirectoryNotExists();
920
        }
921
922
        // Add trailing directory separator the save path
923
        if (substr($savePath, -1) != DIRECTORY_SEPARATOR) {
924
            $savePath .= DIRECTORY_SEPARATOR;
925
        }
926
927
        $this->savePath = $savePath;
928
    }
929
930
    /**
931
     * Set property
932
     *
933
     * @param  string $element The element name you want to set, f.e.: name, email, phoneNumber, ...
934
     * @param  string $key
935
     * @param  string $value
936
     * @throws VCardException
937
     */
938
    private function setProperty($element, $key, $value)
939
    {
940
        if (!in_array($element, $this->multiplePropertiesForElementAllowed)
941
            && isset($this->definedElements[$element])
942
        ) {
943
            throw VCardException::elementAlreadyExists($element);
944
        }
945
946
        // we define that we set this element
947
        $this->definedElements[$element] = true;
948
949
        // adding property
950
        $this->properties[] = [
951
            'key' => $key,
952
            'value' => $value
953
        ];
954
    }
955
956
    /**
957
     * Checks if we should return vcard in cal wrapper
958
     *
959
     * @return bool
960
     */
961
    protected function shouldAttachmentBeCal()
962
    {
963
        $browser = $this->getUserAgent();
964
965
        $matches = [];
966
        preg_match('/os (\d+)_(\d+)\s+/', $browser, $matches);
967
        $version = isset($matches[1]) ? ((int)$matches[1]) : 999;
968
969
        return ($version < 8);
970
    }
971
}
972