Completed
Push — master ( 8be1cb...ad2bb7 )
by Jeroen
25:07 queued 22:29
created

VCard::chunk_split_unicode()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 6
nc 2
nop 3
dl 0
loc 9
rs 9.6666
c 0
b 0
f 0
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
    ];
51
52
    /**
53
     * Properties
54
     *
55
     * @var array
56
     */
57
    private $properties;
58
59
    /**
60
     * Default Charset
61
     *
62
     * @var string
63
     */
64
    public $charset = 'utf-8';
65
66
    /**
67
     * Add address
68
     *
69
     * @param  string [optional] $name
70
     * @param  string [optional] $extended
71
     * @param  string [optional] $street
72
     * @param  string [optional] $city
73
     * @param  string [optional] $region
74
     * @param  string [optional] $zip
75
     * @param  string [optional] $country
76
     * @param  string [optional] $type
77
     *                                     $type may be DOM | INTL | POSTAL | PARCEL | HOME | WORK
78
     *                                     or any combination of these: e.g. "WORK;PARCEL;POSTAL"
79
     * @return $this
80
     */
81
    public function addAddress(
82
        $name = '',
83
        $extended = '',
84
        $street = '',
85
        $city = '',
86
        $region = '',
87
        $zip = '',
88
        $country = '',
89
        $type = 'WORK;POSTAL'
90
    ) {
91
        // init value
92
        $value = $name . ';' . $extended . ';' . $street . ';' . $city . ';' . $region . ';' . $zip . ';' . $country;
93
94
        // set property
95
        $this->setProperty(
96
            'address',
97
            'ADR' . (($type != '') ? ';' . $type : '') . $this->getCharsetString(),
98
            $value
99
        );
100
101
        return $this;
102
    }
103
104
    /**
105
     * Add birthday
106
     *
107
     * @param  string $date Format is YYYY-MM-DD
108
     * @return $this
109
     */
110
    public function addBirthday($date)
111
    {
112
        $this->setProperty(
113
            'birthday',
114
            'BDAY',
115
            $date
116
        );
117
118
        return $this;
119
    }
120
121
    /**
122
     * Add company
123
     *
124
     * @param string $company
125
     * @param string $department
126
     * @return $this
127
     */
128
    public function addCompany($company, $department = '')
129
    {
130
        $this->setProperty(
131
            'company',
132
            'ORG' . $this->getCharsetString(),
133
            $company
134
            . ($department != '' ? ';' . $department : '')
135
        );
136
137
        // if filename is empty, add to filename
138
        if ($this->filename === null) {
139
            $this->setFilename($company);
140
        }
141
142
        return $this;
143
    }
144
145
    /**
146
     * Add email
147
     *
148
     * @param  string $address The e-mail address
149
     * @param  string [optional] $type    The type of the email address
150
     *                                    $type may be  PREF | WORK | HOME
151
     *                                    or any combination of these: e.g. "PREF;WORK"
152
     * @return $this
153
     */
154
    public function addEmail($address, $type = '')
155
    {
156
        $this->setProperty(
157
            'email',
158
            'EMAIL;INTERNET' . (($type != '') ? ';' . $type : ''),
159
            $address
160
        );
161
162
        return $this;
163
    }
164
165
    /**
166
     * Add jobtitle
167
     *
168
     * @param  string $jobtitle The jobtitle for the person.
169
     * @return $this
170
     */
171
    public function addJobtitle($jobtitle)
172
    {
173
        $this->setProperty(
174
            'jobtitle',
175
            'TITLE' . $this->getCharsetString(),
176
            $jobtitle
177
        );
178
179
        return $this;
180
    }
181
182
    /**
183
     * Add role
184
     *
185
     * @param  string $role The role for the person.
186
     * @return $this
187
     */
188
    public function addRole($role)
189
    {
190
        $this->setProperty(
191
            'role',
192
            'ROLE' . $this->getCharsetString(),
193
            $role
194
        );
195
196
        return $this;
197
    }
198
199
    /**
200
     * Add a photo or logo (depending on property name)
201
     *
202
     * @param string $property LOGO|PHOTO
203
     * @param string $url image url or filename
204
     * @param bool $include Do we include the image in our vcard or not?
205
     * @param string $element The name of the element to set
206
     */
207
    private function addMedia($property, $url, $include = true, $element)
208
    {
209
        $mimeType = null;
210
211
        //Is this URL for a remote resource?
212
        if (filter_var($url, FILTER_VALIDATE_URL) !== false) {
213
            $headers = get_headers($url, 1);
0 ignored issues
show
Bug introduced by
The call to get_headers() has too few arguments starting with context. ( Ignorable by Annotation )

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

213
            $headers = /** @scrutinizer ignore-call */ get_headers($url, 1);

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
214
215
            if (array_key_exists('Content-Type', $headers)) {
216
                $mimeType = $headers['Content-Type'];
217
                if (is_array($mimeType)) {
218
                    $mimeType = end($mimeType);
219
                }
220
            }
221
        } else {
222
            //Local file, so inspect it directly
223
            $mimeType = mime_content_type($url);
224
        }
225 View Code Duplication
        if (strpos($mimeType, ';') !== false) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
226
            $mimeType = strstr($mimeType, ';', true);
227
        }
228 View Code Duplication
        if (!is_string($mimeType) || substr($mimeType, 0, 6) !== 'image/') {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
229
            throw VCardException::invalidImage();
230
        }
231
        $fileType = strtoupper(substr($mimeType, 6));
232
233
        if ($include) {
234
            $value = file_get_contents($url);
235
236
            if (!$value) {
237
                throw VCardException::emptyURL();
238
            }
239
240
            $value = base64_encode($value);
241
            $property .= ";ENCODING=b;TYPE=" . $fileType;
242
        } else {
243
            if (filter_var($url, FILTER_VALIDATE_URL) !== false) {
244
                $propertySuffix = ';VALUE=URL';
245
                $propertySuffix .= ';TYPE=' . strtoupper($fileType);
246
247
                $property = $property . $propertySuffix;
248
                $value = $url;
249
            } else {
250
                $value = $url;
251
            }
252
        }
253
254
        $this->setProperty(
255
            $element,
256
            $property,
257
            $value
258
        );
259
    }
260
261
    /**
262
     * Add a photo or logo (depending on property name)
263
     *
264
     * @param string $property LOGO|PHOTO
265
     * @param string $content image content
266
     * @param string $element The name of the element to set
267
     */
268
    private function addMediaContent($property, $content, $element)
269
    {
270
        $finfo = new \finfo();
0 ignored issues
show
Bug introduced by
The call to finfo::finfo() has too few arguments starting with options. ( Ignorable by Annotation )

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

270
        $finfo = /** @scrutinizer ignore-call */ new \finfo();

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
271
        $mimeType = $finfo->buffer($content, FILEINFO_MIME_TYPE);
272
273 View Code Duplication
        if (strpos($mimeType, ';') !== false) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
274
            $mimeType = strstr($mimeType, ';', true);
275
        }
276 View Code Duplication
        if (!is_string($mimeType) || substr($mimeType, 0, 6) !== 'image/') {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
277
            throw VCardException::invalidImage();
278
        }
279
        $fileType = strtoupper(substr($mimeType, 6));
280
281
        $content = base64_encode($content);
282
        $property .= ";ENCODING=b;TYPE=" . $fileType;
283
284
        $this->setProperty(
285
            $element,
286
            $property,
287
            $content
288
        );
289
    }
290
291
    /**
292
     * Add name
293
     *
294
     * @param  string [optional] $lastName
295
     * @param  string [optional] $firstName
296
     * @param  string [optional] $additional
297
     * @param  string [optional] $prefix
298
     * @param  string [optional] $suffix
299
     * @return $this
300
     */
301
    public function addName(
302
        $lastName = '',
303
        $firstName = '',
304
        $additional = '',
305
        $prefix = '',
306
        $suffix = ''
307
    ) {
308
        // define values with non-empty values
309
        $values = array_filter([
310
            $prefix,
311
            $firstName,
312
            $additional,
313
            $lastName,
314
            $suffix,
315
        ]);
316
317
        // define filename
318
        $this->setFilename($values);
319
320
        // set property
321
        $property = $lastName . ';' . $firstName . ';' . $additional . ';' . $prefix . ';' . $suffix;
322
        $this->setProperty(
323
            'name',
324
            'N' . $this->getCharsetString(),
325
            $property
326
        );
327
328
        // is property FN set?
329
        if (!$this->hasProperty('FN')) {
330
            // set property
331
            $this->setProperty(
332
                'fullname',
333
                'FN' . $this->getCharsetString(),
334
                trim(implode(' ', $values))
335
            );
336
        }
337
338
        return $this;
339
    }
340
341
    /**
342
     * Add note
343
     *
344
     * @param  string $note
345
     * @return $this
346
     */
347
    public function addNote($note)
348
    {
349
        $this->setProperty(
350
            'note',
351
            'NOTE' . $this->getCharsetString(),
352
            $note
353
        );
354
355
        return $this;
356
    }
357
358
    /**
359
     * Add categories
360
     *
361
     * @param array $categories
362
     * @return $this
363
     */
364
    public function addCategories($categories)
365
    {
366
        $this->setProperty(
367
            'categories',
368
            'CATEGORIES' . $this->getCharsetString(),
369
            trim(implode(',', $categories))
370
        );
371
372
        return $this;
373
    }
374
375
    /**
376
     * Add phone number
377
     *
378
     * @param  string $number
379
     * @param  string [optional] $type
380
     *                                   Type may be PREF | WORK | HOME | VOICE | FAX | MSG |
381
     *                                   CELL | PAGER | BBS | CAR | MODEM | ISDN | VIDEO
382
     *                                   or any senseful combination, e.g. "PREF;WORK;VOICE"
383
     * @return $this
384
     */
385
    public function addPhoneNumber($number, $type = '')
386
    {
387
        $this->setProperty(
388
            'phoneNumber',
389
            'TEL' . (($type != '') ? ';' . $type : ''),
390
            $number
391
        );
392
393
        return $this;
394
    }
395
396
    /**
397
     * Add Logo
398
     *
399
     * @param  string $url image url or filename
400
     * @param  bool $include Include the image in our vcard?
401
     * @return $this
402
     */
403
    public function addLogo($url, $include = true)
404
    {
405
        $this->addMedia(
406
            'LOGO',
407
            $url,
408
            $include,
409
            'logo'
410
        );
411
412
        return $this;
413
    }
414
415
    /**
416
     * Add Logo content
417
     *
418
     * @param  string $content image content
419
     * @return $this
420
     */
421
    public function addLogoContent($content)
422
    {
423
        $this->addMediaContent(
424
            'LOGO',
425
            $content,
426
            'logo'
427
        );
428
429
        return $this;
430
    }
431
432
    /**
433
     * Add Photo
434
     *
435
     * @param  string $url image url or filename
436
     * @param  bool $include Include the image in our vcard?
437
     * @return $this
438
     */
439
    public function addPhoto($url, $include = true)
440
    {
441
        $this->addMedia(
442
            'PHOTO',
443
            $url,
444
            $include,
445
            'photo'
446
        );
447
448
        return $this;
449
    }
450
451
    /**
452
     * Add Photo content
453
     *
454
     * @param  string $content image content
455
     * @return $this
456
     */
457
    public function addPhotoContent($content)
458
    {
459
        $this->addMediaContent(
460
            'PHOTO',
461
            $content,
462
            'photo'
463
        );
464
465
        return $this;
466
    }
467
468
    /**
469
     * Add URL
470
     *
471
     * @param  string $url
472
     * @param  string [optional] $type Type may be WORK | HOME
473
     * @return $this
474
     */
475
    public function addURL($url, $type = '')
476
    {
477
        $this->setProperty(
478
            'url',
479
            'URL' . (($type != '') ? ';' . $type : ''),
480
            $url
481
        );
482
483
        return $this;
484
    }
485
486
    /**
487
     * Build VCard (.vcf)
488
     *
489
     * @return string
490
     */
491
    public function buildVCard()
492
    {
493
        // init string
494
        $string = "BEGIN:VCARD\r\n";
495
        $string .= "VERSION:3.0\r\n";
496
        $string .= "REV:" . date("Y-m-d") . "T" . date("H:i:s") . "Z\r\n";
497
498
        // loop all properties
499
        $properties = $this->getProperties();
500
        foreach ($properties as $property) {
501
            // add to string
502
            $string .= $this->fold($property['key'] . ':' . $this->escape($property['value']) . "\r\n");
503
        }
504
505
        // add to string
506
        $string .= "END:VCARD\r\n";
507
508
        // return
509
        return $string;
510
    }
511
512
    /**
513
     * Build VCalender (.ics) - Safari (< iOS 8) can not open .vcf files, so we have build a workaround.
514
     *
515
     * @return string
516
     */
517
    public function buildVCalendar()
518
    {
519
        // init dates
520
        $dtstart = date("Ymd") . "T" . date("Hi") . "00";
521
        $dtend = date("Ymd") . "T" . date("Hi") . "01";
522
523
        // init string
524
        $string = "BEGIN:VCALENDAR\n";
525
        $string .= "VERSION:2.0\n";
526
        $string .= "BEGIN:VEVENT\n";
527
        $string .= "DTSTART;TZID=Europe/London:" . $dtstart . "\n";
528
        $string .= "DTEND;TZID=Europe/London:" . $dtend . "\n";
529
        $string .= "SUMMARY:Click attached contact below to save to your contacts\n";
530
        $string .= "DTSTAMP:" . $dtstart . "Z\n";
531
        $string .= "ATTACH;VALUE=BINARY;ENCODING=BASE64;FMTTYPE=text/directory;\n";
532
        $string .= " X-APPLE-FILENAME=" . $this->getFilename() . "." . $this->getFileExtension() . ":\n";
533
534
        // base64 encode it so that it can be used as an attachemnt to the "dummy" calendar appointment
535
        $b64vcard = base64_encode($this->buildVCard());
536
537
        // chunk the single long line of b64 text in accordance with RFC2045
538
        // (and the exact line length determined from the original .ics file exported from Apple calendar
539
        $b64mline = chunk_split($b64vcard, 74, "\n");
540
541
        // need to indent all the lines by 1 space for the iphone (yes really?!!)
542
        $b64final = preg_replace('/(.+)/', ' $1', $b64mline);
543
        $string .= $b64final;
544
545
        // output the correctly formatted encoded text
546
        $string .= "END:VEVENT\n";
547
        $string .= "END:VCALENDAR\n";
548
549
        // return
550
        return $string;
551
    }
552
553
    /**
554
     * Returns the browser user agent string.
555
     *
556
     * @return string
557
     */
558
    protected function getUserAgent()
559
    {
560
        if (array_key_exists('HTTP_USER_AGENT', $_SERVER)) {
561
            $browser = strtolower($_SERVER['HTTP_USER_AGENT']);
562
        } else {
563
            $browser = 'unknown';
564
        }
565
566
        return $browser;
567
    }
568
569
    /**
570
     * Decode
571
     *
572
     * @param  string $value The value to decode
573
     * @return string decoded
574
     */
575
    private function decode($value)
576
    {
577
        // convert cyrlic, greek or other caracters to ASCII characters
578
        return Transliterator::transliterate($value);
579
    }
580
581
    /**
582
     * Download a vcard or vcal file to the browser.
583
     */
584
    public function download()
585
    {
586
        // define output
587
        $output = $this->getOutput();
588
589
        foreach ($this->getHeaders(false) as $header) {
590
            header($header);
591
        }
592
593
        // echo the output and it will be a download
594
        echo $output;
595
    }
596
597
    /**
598
     * Fold a line according to RFC2425 section 5.8.1.
599
     *
600
     * @link http://tools.ietf.org/html/rfc2425#section-5.8.1
601
     * @param  string $text
602
     * @return mixed
603
     */
604
    protected function fold($text)
605
    {
606
        if (strlen($text) <= 75) {
607
            return $text;
608
        }
609
610
        // split, wrap and trim trailing separator
611
        return substr($this->chunk_split_unicode($text, 73, "\r\n "), 0, -3);
612
    }
613
614
    /**
615
     * multibyte word chunk split
616
     * @link http://php.net/manual/en/function.chunk-split.php#107711
617
     * 
618
     * @param  string  $body     The string to be chunked.
619
     * @param  integer $chunklen The chunk length.
620
     * @param  string  $end      The line ending sequence.
621
     * @return string            Chunked string
622
     */
623
    protected function chunk_split_unicode($body, $chunklen = 76, $end = "\r\n")
624
    {
625
        $array = array_chunk(
626
            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

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