Completed
Pull Request — master (#154)
by
unknown
02:15
created

VCard::is_ascii()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 3
eloc 6
c 1
b 1
f 0
nc 3
nop 1
dl 0
loc 9
rs 10
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);
0 ignored issues
show
Bug introduced by
It seems like $curl can also be of type false; however, parameter $ch of curl_setopt() does only seem to accept resource, 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

259
                curl_setopt(/** @scrutinizer ignore-type */ $curl, CURLOPT_URL, $url);
Loading history...
260
                curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
261
                $value = curl_exec($curl);
0 ignored issues
show
Bug introduced by
It seems like $curl can also be of type false; however, parameter $ch of curl_exec() does only seem to accept resource, 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

261
                $value = curl_exec(/** @scrutinizer ignore-type */ $curl);
Loading history...
262
                curl_close($curl);
0 ignored issues
show
Bug introduced by
It seems like $curl can also be of type false; however, parameter $ch of curl_close() does only seem to accept resource, 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

262
                curl_close(/** @scrutinizer ignore-type */ $curl);
Loading history...
263
            }
264
265
            if (!$value) {
266
                throw VCardException::emptyURL();
267
            }
268
269
            $value = base64_encode($value);
0 ignored issues
show
Bug introduced by
It seems like $value can also be of type true; however, parameter $data of base64_encode() does only seem to accept string, 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

269
            $value = base64_encode(/** @scrutinizer ignore-type */ $value);
Loading history...
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
        // The chunk_split_unicode creates a huge memory footprint when used on long strings (EG photos are base64 10MB results in > 1GB memory usage)
640
        // So check if the string is ASCII (7 bit) and if it is use the built in way RE: https://github.com/jeroendesloovere/vcard/issues/153
641
        if ($this->is_ascii($text)) {
642
           return substr(chunk_split($text, 75, "\r\n "), 0, -3);
643
        }
644
645
        // split, wrap and trim trailing separator
646
        return substr($this->chunk_split_unicode($text, 75, "\r\n "), 0, -3);
647
    }
648
649
650
    /**
651
     * Determine if string is pure 7bit ascii
652
     * @link https://pageconfig.com/post/how-to-validate-ascii-text-in-php
653
     *
654
     * @param string $string
655
     * @return bool
656
     */
657
    protected function is_ascii($string = '' ) {
658
        $num = 0;
659
        while( isset( $string[$num] ) ) {
660
            if( ord( $string[$num] ) & 0x80 ) {
661
                return false;
662
            }
663
        $num++;
664
        }
665
        return true;
666
    }
667
668
    /**
669
     * multibyte word chunk split
670
     * @link http://php.net/manual/en/function.chunk-split.php#107711
671
     * 
672
     * @param  string  $body     The string to be chunked.
673
     * @param  integer $chunklen The chunk length.
674
     * @param  string  $end      The line ending sequence.
675
     * @return string            Chunked string
676
     */
677
    protected function chunk_split_unicode($body, $chunklen = 76, $end = "\r\n")
678
    {
679
        $array = array_chunk(
680
            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

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