VCard   F
last analyzed

Complexity

Total Complexity 90

Size/Duplication

Total Lines 953
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 282
dl 0
loc 953
rs 2
c 0
b 0
f 0
wmc 90

44 Methods

Rating   Name   Duplication   Size   Complexity  
A getOutput() 0 6 2
A buildVCard() 0 19 2
A addRole() 0 9 1
A getContentType() 0 4 2
A addLabel() 0 9 2
A getFilename() 0 7 2
A addEmail() 0 9 2
A escape() 0 6 1
A getCharsetString() 0 3 1
A addBirthday() 0 9 1
A addURL() 0 9 2
A addPhoto() 0 10 1
A addJobtitle() 0 9 1
A addAddress() 0 21 2
A getUserAgent() 0 9 2
A addCategories() 0 9 1
A decode() 0 4 1
A addName() 0 38 2
A addLogoContent() 0 9 1
A addNote() 0 9 1
A setFilename() 0 27 4
A setCharset() 0 3 1
A addCompany() 0 15 3
A isIOS() 0 6 3
A shouldAttachmentBeCal() 0 9 2
A hasProperty() 0 11 4
A get() 0 3 1
A isIOS7() 0 3 2
A save() 0 12 2
A getProperties() 0 3 1
A getFileExtension() 0 4 2
A buildVCalendar() 0 34 1
A addPhotoContent() 0 9 1
A chunk_split_unicode() 0 9 2
A addPhoneNumber() 0 9 2
A addMediaContent() 0 20 4
A setSavePath() 0 12 3
A setProperty() 0 15 3
A download() 0 11 2
B addMedia() 0 59 11
A addLogo() 0 10 1
A getHeaders() 0 21 2
A getCharset() 0 3 1
A fold() 0 8 2

How to fix   Complexity   

Complex Class

Complex classes like VCard often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use VCard, and based on these observations, apply Extract Interface, too.

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
introduced by
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
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
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
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
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);
0 ignored issues
show
Bug introduced by
It seems like preg_split('//u', $body,...rd\PREG_SPLIT_NO_EMPTY) can also be of type false; however, parameter $input of array_chunk() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

655
            /** @scrutinizer ignore-type */ preg_split("//u", $body, -1, PREG_SPLIT_NO_EMPTY), $chunklen);
Loading history...
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