Test Failed
Pull Request — master (#136)
by
unknown
04:02
created

VCard   F

Complexity

Total Complexity 92

Size/Duplication

Total Lines 973
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 288
dl 0
loc 973
rs 2
c 0
b 0
f 0
wmc 92

45 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 addSocial() 0 10 2
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
        'socialmedia'
52
    ];
53
54
    /**
55
     * Properties
56
     *
57
     * @var array
58
     */
59
    private $properties;
60
61
    /**
62
     * Default Charset
63
     *
64
     * @var string
65
     */
66
    public $charset = 'utf-8';
67
68
    /**
69
     * Add address
70
     *
71
     * @param  string [optional] $name
72
     * @param  string [optional] $extended
73
     * @param  string [optional] $street
74
     * @param  string [optional] $city
75
     * @param  string [optional] $region
76
     * @param  string [optional] $zip
77
     * @param  string [optional] $country
78
     * @param  string [optional] $type
79
     *                                     $type may be DOM | INTL | POSTAL | PARCEL | HOME | WORK
80
     *                                     or any combination of these: e.g. "WORK;PARCEL;POSTAL"
81
     * @return $this
82
     */
83
    public function addAddress(
84
        $name = '',
85
        $extended = '',
86
        $street = '',
87
        $city = '',
88
        $region = '',
89
        $zip = '',
90
        $country = '',
91
        $type = 'WORK;POSTAL'
92
    ) {
93
        // init value
94
        $value = $name . ';' . $extended . ';' . $street . ';' . $city . ';' . $region . ';' . $zip . ';' . $country;
95
96
        // set property
97
        $this->setProperty(
98
            'address',
99
            'ADR' . (($type != '') ? ';' . $type : '') . $this->getCharsetString(),
100
            $value
101
        );
102
103
        return $this;
104
    }
105
106
    /**
107
     * Add birthday
108
     *
109
     * @param  string $date Format is YYYY-MM-DD
110
     * @return $this
111
     */
112
    public function addBirthday($date)
113
    {
114
        $this->setProperty(
115
            'birthday',
116
            'BDAY',
117
            $date
118
        );
119
120
        return $this;
121
    }
122
123
    /**
124
     * Add company
125
     *
126
     * @param string $company
127
     * @param string $department
128
     * @return $this
129
     */
130
    public function addCompany($company, $department = '')
131
    {
132
        $this->setProperty(
133
            'company',
134
            'ORG' . $this->getCharsetString(),
135
            $company
136
            . ($department != '' ? ';' . $department : '')
137
        );
138
139
        // if filename is empty, add to filename
140
        if ($this->filename === null) {
141
            $this->setFilename($company);
142
        }
143
144
        return $this;
145
    }
146
147
    /**
148
     * Add email
149
     *
150
     * @param  string $address The e-mail address
151
     * @param  string [optional] $type    The type of the email address
152
     *                                    $type may be  PREF | WORK | HOME
153
     *                                    or any combination of these: e.g. "PREF;WORK"
154
     * @return $this
155
     */
156
    public function addEmail($address, $type = '')
157
    {
158
        $this->setProperty(
159
            'email',
160
            'EMAIL;type=INTERNET;' . (($type != '') ? ';' . $type : ''),
161
            $address
162
        );
163
164
        return $this;
165
    }
166
167
    /**
168
     * Add jobtitle
169
     *
170
     * @param  string $jobtitle The jobtitle for the person.
171
     * @return $this
172
     */
173
    public function addJobtitle($jobtitle)
174
    {
175
        $this->setProperty(
176
            'jobtitle',
177
            'TITLE' . $this->getCharsetString(),
178
            $jobtitle
179
        );
180
181
        return $this;
182
    }
183
184
    /**
185
     * Add a label
186
     *
187
     * @param string $label
188
     * @param string $type
189
     *
190
     * @return $this
191
     */
192
    public function addLabel($label, $type = '')
193
    {
194
        $this->setProperty(
195
            'label',
196
            'LABEL' . ($type !== '' ? ';' . $type : ''),
197
            $label
198
        );
199
200
        return $this;
201
    }
202
203
    /**
204
     * Add role
205
     *
206
     * @param  string $role The role for the person.
207
     * @return $this
208
     */
209
    public function addRole($role)
210
    {
211
        $this->setProperty(
212
            'role',
213
            'ROLE' . $this->getCharsetString(),
214
            $role
215
        );
216
217
        return $this;
218
    }
219
220
    /**
221
     * Add socialMedia
222
     *
223
     * @param  string $type The type of social media.
224
     * @param  string $addr The address of the social media platform.
225
     * @return $this
226
     */
227
    public function addSocial($type = '', $url)
228
    {
229
230
        $this->setProperty(
231
            'socialmedia',
232
            'X-SOCIALPROFILE;type=' . (($type != '') ? $type : '' ),
233
            $url
234
        );
235
236
        return $this;
237
    }
238
239
    /**
240
     * Add a photo or logo (depending on property name)
241
     *
242
     * @param string $property LOGO|PHOTO
243
     * @param string $url image url or filename
244
     * @param bool $include Do we include the image in our vcard or not?
245
     * @param string $element The name of the element to set
246
     * @throws VCardException
247
     */
248
    private function addMedia($property, $url, $include = true, $element)
249
    {
250
        $mimeType = null;
251
252
        //Is this URL for a remote resource?
253
        if (filter_var($url, FILTER_VALIDATE_URL) !== false) {
254
            $headers = get_headers($url, 1);
255
256
            if (array_key_exists('Content-Type', $headers)) {
257
                $mimeType = $headers['Content-Type'];
258
                if (is_array($mimeType)) {
259
                    $mimeType = end($mimeType);
260
                }
261
            }
262
        } else {
263
            //Local file, so inspect it directly
264
            $mimeType = mime_content_type($url);
265
        }
266
        if (strpos($mimeType, ';') !== false) {
267
            $mimeType = strstr($mimeType, ';', true);
268
        }
269
        if (!is_string($mimeType) || substr($mimeType, 0, 6) !== 'image/') {
270
            throw VCardException::invalidImage();
271
        }
272
        $fileType = strtoupper(substr($mimeType, 6));
273
274
        if ($include) {
275
            if ((bool) ini_get('allow_url_fopen') === true) {
276
                $value = file_get_contents($url);
277
            } else {
278
                $curl = curl_init();
279
                curl_setopt($curl, CURLOPT_URL, $url);
280
                curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
281
                $value = curl_exec($curl);
282
                curl_close($curl);
283
            }
284
285
            if (!$value) {
286
                throw VCardException::emptyURL();
287
            }
288
289
            $value = base64_encode($value);
290
            $property .= ";ENCODING=b;TYPE=" . $fileType;
291
        } else {
292
            if (filter_var($url, FILTER_VALIDATE_URL) !== false) {
293
                $propertySuffix = ';VALUE=URL';
294
                $propertySuffix .= ';TYPE=' . strtoupper($fileType);
295
296
                $property = $property . $propertySuffix;
297
                $value = $url;
298
            } else {
299
                $value = $url;
300
            }
301
        }
302
303
        $this->setProperty(
304
            $element,
305
            $property,
306
            $value
307
        );
308
    }
309
310
    /**
311
     * Add a photo or logo (depending on property name)
312
     *
313
     * @param string $property LOGO|PHOTO
314
     * @param string $content image content
315
     * @param string $element The name of the element to set
316
     */
317
    private function addMediaContent($property, $content, $element)
318
    {
319
        $finfo = new \finfo();
320
        $mimeType = $finfo->buffer($content, FILEINFO_MIME_TYPE);
321
322
        if (strpos($mimeType, ';') !== false) {
323
            $mimeType = strstr($mimeType, ';', true);
324
        }
325
        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...
326
            throw VCardException::invalidImage();
327
        }
328
        $fileType = strtoupper(substr($mimeType, 6));
329
330
        $content = base64_encode($content);
331
        $property .= ";ENCODING=b;TYPE=" . $fileType;
332
333
        $this->setProperty(
334
            $element,
335
            $property,
336
            $content
337
        );
338
    }
339
340
    /**
341
     * Add name
342
     *
343
     * @param  string [optional] $lastName
344
     * @param  string [optional] $firstName
345
     * @param  string [optional] $additional
346
     * @param  string [optional] $prefix
347
     * @param  string [optional] $suffix
348
     * @return $this
349
     */
350
    public function addName(
351
        $lastName = '',
352
        $firstName = '',
353
        $additional = '',
354
        $prefix = '',
355
        $suffix = ''
356
    ) {
357
        // define values with non-empty values
358
        $values = array_filter([
359
            $prefix,
360
            $firstName,
361
            $additional,
362
            $lastName,
363
            $suffix,
364
        ]);
365
366
        // define filename
367
        $this->setFilename($values);
368
369
        // set property
370
        $property = $lastName . ';' . $firstName . ';' . $additional . ';' . $prefix . ';' . $suffix;
371
        $this->setProperty(
372
            'name',
373
            'N' . $this->getCharsetString(),
374
            $property
375
        );
376
377
        // is property FN set?
378
        if (!$this->hasProperty('FN')) {
379
            // set property
380
            $this->setProperty(
381
                'fullname',
382
                'FN' . $this->getCharsetString(),
383
                trim(implode(' ', $values))
384
            );
385
        }
386
387
        return $this;
388
    }
389
390
    /**
391
     * Add note
392
     *
393
     * @param  string $note
394
     * @return $this
395
     */
396
    public function addNote($note)
397
    {
398
        $this->setProperty(
399
            'note',
400
            'NOTE' . $this->getCharsetString(),
401
            $note
402
        );
403
404
        return $this;
405
    }
406
407
    /**
408
     * Add categories
409
     *
410
     * @param array $categories
411
     * @return $this
412
     */
413
    public function addCategories($categories)
414
    {
415
        $this->setProperty(
416
            'categories',
417
            'CATEGORIES' . $this->getCharsetString(),
418
            trim(implode(',', $categories))
419
        );
420
421
        return $this;
422
    }
423
424
    /**
425
     * Add phone number
426
     *
427
     * @param  string $number
428
     * @param  string [optional] $type
429
     *                                   Type may be PREF | WORK | HOME | VOICE | FAX | MSG |
430
     *                                   CELL | PAGER | BBS | CAR | MODEM | ISDN | VIDEO
431
     *                                   or any senseful combination, e.g. "PREF;WORK;VOICE"
432
     * @return $this
433
     */
434
    public function addPhoneNumber($number, $type = '')
435
    {
436
        $this->setProperty(
437
            'phoneNumber',
438
            'TEL' . (($type != '') ? ';' . $type : ''),
439
            $number
440
        );
441
442
        return $this;
443
    }
444
445
    /**
446
     * Add Logo
447
     *
448
     * @param  string $url image url or filename
449
     * @param  bool $include Include the image in our vcard?
450
     * @return $this
451
     */
452
    public function addLogo($url, $include = true)
453
    {
454
        $this->addMedia(
455
            'LOGO',
456
            $url,
457
            $include,
458
            'logo'
459
        );
460
461
        return $this;
462
    }
463
464
    /**
465
     * Add Logo content
466
     *
467
     * @param  string $content image content
468
     * @return $this
469
     */
470
    public function addLogoContent($content)
471
    {
472
        $this->addMediaContent(
473
            'LOGO',
474
            $content,
475
            'logo'
476
        );
477
478
        return $this;
479
    }
480
481
    /**
482
     * Add Photo
483
     *
484
     * @param  string $url image url or filename
485
     * @param  bool $include Include the image in our vcard?
486
     * @return $this
487
     */
488
    public function addPhoto($url, $include = true)
489
    {
490
        $this->addMedia(
491
            'PHOTO',
492
            $url,
493
            $include,
494
            'photo'
495
        );
496
497
        return $this;
498
    }
499
500
    /**
501
     * Add Photo content
502
     *
503
     * @param  string $content image content
504
     * @return $this
505
     */
506
    public function addPhotoContent($content)
507
    {
508
        $this->addMediaContent(
509
            'PHOTO',
510
            $content,
511
            'photo'
512
        );
513
514
        return $this;
515
    }
516
517
    /**
518
     * Add URL
519
     *
520
     * @param  string $url
521
     * @param  string [optional] $type Type may be WORK | HOME
522
     * @return $this
523
     */
524
    public function addURL($url, $type = '')
525
    {
526
        $this->setProperty(
527
            'url',
528
            'URL' . (($type != '') ? ';' . $type : ''),
529
            $url
530
        );
531
532
        return $this;
533
    }
534
535
    /**
536
     * Build VCard (.vcf)
537
     *
538
     * @return string
539
     */
540
    public function buildVCard()
541
    {
542
        // init string
543
        $string = "BEGIN:VCARD\r\n";
544
        $string .= "VERSION:3.0\r\n";
545
        $string .= "REV:" . date("Y-m-d") . "T" . date("H:i:s") . "Z\r\n";
546
547
        // loop all properties
548
        $properties = $this->getProperties();
549
        foreach ($properties as $property) {
550
            // add to string
551
            $string .= $this->fold($property['key'] . ':' . $this->escape($property['value']) . "\r\n");
552
        }
553
554
        // add to string
555
        $string .= "END:VCARD\r\n";
556
557
        // 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...
558
        return $string;
559
    }
560
561
    /**
562
     * Build VCalender (.ics) - Safari (< iOS 8) can not open .vcf files, so we have build a workaround.
563
     *
564
     * @return string
565
     */
566
    public function buildVCalendar()
567
    {
568
        // init dates
569
        $dtstart = date("Ymd") . "T" . date("Hi") . "00";
570
        $dtend = date("Ymd") . "T" . date("Hi") . "01";
571
572
        // init string
573
        $string = "BEGIN:VCALENDAR\n";
574
        $string .= "VERSION:2.0\n";
575
        $string .= "BEGIN:VEVENT\n";
576
        $string .= "DTSTART;TZID=Europe/London:" . $dtstart . "\n";
577
        $string .= "DTEND;TZID=Europe/London:" . $dtend . "\n";
578
        $string .= "SUMMARY:Click attached contact below to save to your contacts\n";
579
        $string .= "DTSTAMP:" . $dtstart . "Z\n";
580
        $string .= "ATTACH;VALUE=BINARY;ENCODING=BASE64;FMTTYPE=text/directory;\n";
581
        $string .= " X-APPLE-FILENAME=" . $this->getFilename() . "." . $this->getFileExtension() . ":\n";
582
583
        // base64 encode it so that it can be used as an attachemnt to the "dummy" calendar appointment
584
        $b64vcard = base64_encode($this->buildVCard());
585
586
        // chunk the single long line of b64 text in accordance with RFC2045
587
        // (and the exact line length determined from the original .ics file exported from Apple calendar
588
        $b64mline = chunk_split($b64vcard, 74, "\n");
589
590
        // need to indent all the lines by 1 space for the iphone (yes really?!!)
591
        $b64final = preg_replace('/(.+)/', ' $1', $b64mline);
592
        $string .= $b64final;
593
594
        // output the correctly formatted encoded text
595
        $string .= "END:VEVENT\n";
596
        $string .= "END:VCALENDAR\n";
597
598
        // 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...
599
        return $string;
600
    }
601
602
    /**
603
     * Returns the browser user agent string.
604
     *
605
     * @return string
606
     */
607
    protected function getUserAgent()
608
    {
609
        if (array_key_exists('HTTP_USER_AGENT', $_SERVER)) {
610
            $browser = strtolower($_SERVER['HTTP_USER_AGENT']);
611
        } else {
612
            $browser = 'unknown';
613
        }
614
615
        return $browser;
616
    }
617
618
    /**
619
     * Decode
620
     *
621
     * @param  string $value The value to decode
622
     * @return string decoded
623
     */
624
    private function decode($value)
625
    {
626
        // convert cyrlic, greek or other caracters to ASCII characters
627
        return Transliterator::transliterate($value);
628
    }
629
630
    /**
631
     * Download a vcard or vcal file to the browser.
632
     */
633
    public function download()
634
    {
635
        // define output
636
        $output = $this->getOutput();
637
638
        foreach ($this->getHeaders(false) as $header) {
639
            header($header);
640
        }
641
642
        // echo the output and it will be a download
643
        echo $output;
644
    }
645
646
    /**
647
     * Fold a line according to RFC2425 section 5.8.1.
648
     *
649
     * @link http://tools.ietf.org/html/rfc2425#section-5.8.1
650
     * @param  string $text
651
     * @return mixed
652
     */
653
    protected function fold($text)
654
    {
655
        if (strlen($text) <= 75) {
656
            return $text;
657
        }
658
659
        // split, wrap and trim trailing separator
660
        return substr($this->chunk_split_unicode($text, 75, "\r\n "), 0, -3);
661
    }
662
663
    /**
664
     * multibyte word chunk split
665
     * @link http://php.net/manual/en/function.chunk-split.php#107711
666
     *
667
     * @param  string  $body     The string to be chunked.
668
     * @param  integer $chunklen The chunk length.
669
     * @param  string  $end      The line ending sequence.
670
     * @return string            Chunked string
671
     */
672
    protected function chunk_split_unicode($body, $chunklen = 76, $end = "\r\n")
673
    {
674
        $array = array_chunk(
675
            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

675
            /** @scrutinizer ignore-type */ preg_split("//u", $body, -1, PREG_SPLIT_NO_EMPTY), $chunklen);
Loading history...
676
        $body = "";
677
        foreach ($array as $item) {
678
            $body .= join("", $item) . $end;
679
        }
680
        return $body;
681
    }
682
683
    /**
684
     * Escape newline characters according to RFC2425 section 5.8.4.
685
     *
686
     * @link http://tools.ietf.org/html/rfc2425#section-5.8.4
687
     * @param  string $text
688
     * @return string
689
     */
690
    protected function escape($text)
691
    {
692
        $text = str_replace("\r\n", "\\n", $text);
693
        $text = str_replace("\n", "\\n", $text);
694
695
        return $text;
696
    }
697
698
    /**
699
     * Get output as string
700
     * @deprecated in the future
701
     *
702
     * @return string
703
     */
704
    public function get()
705
    {
706
        return $this->getOutput();
707
    }
708
709
    /**
710
     * Get charset
711
     *
712
     * @return string
713
     */
714
    public function getCharset()
715
    {
716
        return $this->charset;
717
    }
718
719
    /**
720
     * Get charset string
721
     *
722
     * @return string
723
     */
724
    public function getCharsetString()
725
    {
726
        return ';CHARSET=' . $this->charset;
727
    }
728
729
    /**
730
     * Get content type
731
     *
732
     * @return string
733
     */
734
    public function getContentType()
735
    {
736
        return ($this->isIOS7()) ?
737
            'text/x-vcalendar' : 'text/x-vcard';
738
    }
739
740
    /**
741
     * Get filename
742
     *
743
     * @return string
744
     */
745
    public function getFilename()
746
    {
747
        if (!$this->filename) {
748
            return 'unknown';
749
        }
750
751
        return $this->filename;
752
    }
753
754
    /**
755
     * Get file extension
756
     *
757
     * @return string
758
     */
759
    public function getFileExtension()
760
    {
761
        return ($this->isIOS7()) ?
762
            'ics' : 'vcf';
763
    }
764
765
    /**
766
     * Get headers
767
     *
768
     * @param  bool $asAssociative
769
     * @return array
770
     */
771
    public function getHeaders($asAssociative)
772
    {
773
        $contentType = $this->getContentType() . '; charset=' . $this->getCharset();
774
        $contentDisposition = 'attachment; filename=' . $this->getFilename() . '.' . $this->getFileExtension();
775
        $contentLength = mb_strlen($this->getOutput(), $this->getCharset());
776
        $connection = 'close';
777
778
        if ((bool)$asAssociative) {
779
            return [
780
                'Content-type' => $contentType,
781
                'Content-Disposition' => $contentDisposition,
782
                'Content-Length' => $contentLength,
783
                'Connection' => $connection,
784
            ];
785
        }
786
787
        return [
788
            'Content-type: ' . $contentType,
789
            'Content-Disposition: ' . $contentDisposition,
790
            'Content-Length: ' . $contentLength,
791
            'Connection: ' . $connection,
792
        ];
793
    }
794
795
    /**
796
     * Get output as string
797
     * iOS devices (and safari < iOS 8 in particular) can not read .vcf (= vcard) files.
798
     * So I build a workaround to build a .ics (= vcalender) file.
799
     *
800
     * @return string
801
     */
802
    public function getOutput()
803
    {
804
        $output = ($this->isIOS7()) ?
805
            $this->buildVCalendar() : $this->buildVCard();
806
807
        return $output;
808
    }
809
810
    /**
811
     * Get properties
812
     *
813
     * @return array
814
     */
815
    public function getProperties()
816
    {
817
        return $this->properties;
818
    }
819
820
    /**
821
     * Has property
822
     *
823
     * @param  string $key
824
     * @return bool
825
     */
826
    public function hasProperty($key)
827
    {
828
        $properties = $this->getProperties();
829
830
        foreach ($properties as $property) {
831
            if ($property['key'] === $key && $property['value'] !== '') {
832
                return true;
833
            }
834
        }
835
836
        return false;
837
    }
838
839
    /**
840
     * Is iOS - Check if the user is using an iOS-device
841
     *
842
     * @return bool
843
     */
844
    public function isIOS()
845
    {
846
        // get user agent
847
        $browser = $this->getUserAgent();
848
849
        return (strpos($browser, 'iphone') || strpos($browser, 'ipod') || strpos($browser, 'ipad'));
850
    }
851
852
    /**
853
     * Is iOS less than 7 (should cal wrapper be returned)
854
     *
855
     * @return bool
856
     */
857
    public function isIOS7()
858
    {
859
        return ($this->isIOS() && $this->shouldAttachmentBeCal());
860
    }
861
862
    /**
863
     * Save to a file
864
     *
865
     * @return void
866
     */
867
    public function save()
868
    {
869
        $file = $this->getFilename() . '.' . $this->getFileExtension();
870
871
        // Add save path if given
872
        if (null !== $this->savePath) {
873
            $file = $this->savePath . $file;
874
        }
875
876
        file_put_contents(
877
            $file,
878
            $this->getOutput()
879
        );
880
    }
881
882
    /**
883
     * Set charset
884
     *
885
     * @param  mixed $charset
886
     * @return void
887
     */
888
    public function setCharset($charset)
889
    {
890
        $this->charset = $charset;
891
    }
892
893
    /**
894
     * Set filename
895
     *
896
     * @param  mixed $value
897
     * @param  bool $overwrite [optional] Default overwrite is true
898
     * @param  string $separator [optional] Default separator is an underscore '_'
899
     * @return void
900
     */
901
    public function setFilename($value, $overwrite = true, $separator = '_')
902
    {
903
        // recast to string if $value is array
904
        if (is_array($value)) {
905
            $value = implode($separator, $value);
906
        }
907
908
        // trim unneeded values
909
        $value = trim($value, $separator);
910
911
        // remove all spaces
912
        $value = preg_replace('/\s+/', $separator, $value);
913
914
        // if value is empty, stop here
915
        if (empty($value)) {
916
            return;
917
        }
918
919
        // decode value + lowercase the string
920
        $value = strtolower($this->decode($value));
921
922
        // urlize this part
923
        $value = Transliterator::urlize($value);
924
925
        // overwrite filename or add to filename using a prefix in between
926
        $this->filename = ($overwrite) ?
927
            $value : $this->filename . $separator . $value;
928
    }
929
930
    /**
931
     * Set the save path directory
932
     *
933
     * @param  string $savePath Save Path
934
     * @throws VCardException
935
     */
936
    public function setSavePath($savePath)
937
    {
938
        if (!is_dir($savePath)) {
939
            throw VCardException::outputDirectoryNotExists();
940
        }
941
942
        // Add trailing directory separator the save path
943
        if (substr($savePath, -1) != DIRECTORY_SEPARATOR) {
944
            $savePath .= DIRECTORY_SEPARATOR;
945
        }
946
947
        $this->savePath = $savePath;
948
    }
949
950
    /**
951
     * Set property
952
     *
953
     * @param  string $element The element name you want to set, f.e.: name, email, phoneNumber, ...
954
     * @param  string $key
955
     * @param  string $value
956
     * @throws VCardException
957
     */
958
    protected function setProperty($element, $key, $value)
959
    {
960
        if (!in_array($element, $this->multiplePropertiesForElementAllowed)
961
            && isset($this->definedElements[$element])
962
        ) {
963
            throw VCardException::elementAlreadyExists($element);
964
        }
965
966
        // we define that we set this element
967
        $this->definedElements[$element] = true;
968
969
        // adding property
970
        $this->properties[] = [
971
            'key' => $key,
972
            'value' => $value
973
        ];
974
    }
975
976
    /**
977
     * Checks if we should return vcard in cal wrapper
978
     *
979
     * @return bool
980
     */
981
    protected function shouldAttachmentBeCal()
982
    {
983
        $browser = $this->getUserAgent();
984
985
        $matches = [];
986
        preg_match('/os (\d+)_(\d+)\s+/', $browser, $matches);
987
        $version = isset($matches[1]) ? ((int)$matches[1]) : 999;
988
989
        return ($version < 8);
990
    }
991
}
992