Completed
Pull Request — master (#101)
by
unknown
03:39
created

VCard::new()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 3
rs 10
cc 1
eloc 2
nc 1
nop 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
     * Index
61
     *
62
     * @var integer
63
     */
64
    private $index = 0;
65
66
    /**
67
     * Default Charset
68
     *
69
     * @var string
70
     */
71
    public $charset = 'utf-8';
72
73
    public function new() {
74
        $this->index++;
75
    }
76
77
    /**
78
     * Add address
79
     *
80
     * @param  string [optional] $name
81
     * @param  string [optional] $extended
82
     * @param  string [optional] $street
83
     * @param  string [optional] $city
84
     * @param  string [optional] $region
85
     * @param  string [optional] $zip
86
     * @param  string [optional] $country
87
     * @param  string [optional] $type
88
     *                                     $type may be DOM | INTL | POSTAL | PARCEL | HOME | WORK
89
     *                                     or any combination of these: e.g. "WORK;PARCEL;POSTAL"
90
     * @return $this
91
     */
92
    public function addAddress(
93
        $name = '',
94
        $extended = '',
95
        $street = '',
96
        $city = '',
97
        $region = '',
98
        $zip = '',
99
        $country = '',
100
        $type = 'WORK;POSTAL'
101
    ) {
102
        // init value
103
        $value = $name . ';' . $extended . ';' . $street . ';' . $city . ';' . $region . ';' . $zip . ';' . $country;
104
105
        // set property
106
        $this->setProperty(
107
            'address',
108
            'ADR' . (($type != '') ? ';' . $type : '') . $this->getCharsetString(),
109
            $value
110
        );
111
112
        return $this;
113
    }
114
115
    /**
116
     * Add birthday
117
     *
118
     * @param  string $date Format is YYYY-MM-DD
119
     * @return $this
120
     */
121
    public function addBirthday($date)
122
    {
123
        $this->setProperty(
124
            'birthday',
125
            'BDAY',
126
            $date
127
        );
128
129
        return $this;
130
    }
131
132
    /**
133
     * Add company
134
     *
135
     * @param string $company
136
     * @param string $department
137
     * @return $this
138
     */
139
    public function addCompany($company, $department = '')
140
    {
141
        $this->setProperty(
142
            'company',
143
            'ORG' . $this->getCharsetString(),
144
            $company
145
            . ($department != '' ? ';' . $department : '')
146
        );
147
148
        // if filename is empty, add to filename
149
        if ($this->filename === null) {
150
            $this->setFilename($company);
151
        }
152
153
        return $this;
154
    }
155
156
    /**
157
     * Add email
158
     *
159
     * @param  string $address The e-mail address
160
     * @param  string [optional] $type    The type of the email address
161
     *                                    $type may be  PREF | WORK | HOME
162
     *                                    or any combination of these: e.g. "PREF;WORK"
163
     * @return $this
164
     */
165
    public function addEmail($address, $type = '')
166
    {
167
        $this->setProperty(
168
            'email',
169
            'EMAIL;INTERNET' . (($type != '') ? ';' . $type : ''),
170
            $address
171
        );
172
173
        return $this;
174
    }
175
176
    /**
177
     * Add jobtitle
178
     *
179
     * @param  string $jobtitle The jobtitle for the person.
180
     * @return $this
181
     */
182
    public function addJobtitle($jobtitle)
183
    {
184
        $this->setProperty(
185
            'jobtitle',
186
            'TITLE' . $this->getCharsetString(),
187
            $jobtitle
188
        );
189
190
        return $this;
191
    }
192
193
    /**
194
     * Add role
195
     *
196
     * @param  string $role The role for the person.
197
     * @return $this
198
     */
199
    public function addRole($role)
200
    {
201
        $this->setProperty(
202
            'role',
203
            'ROLE' . $this->getCharsetString(),
204
            $role
205
        );
206
207
        return $this;
208
    }
209
210
    /**
211
     * Add a photo or logo (depending on property name)
212
     *
213
     * @param string $property LOGO|PHOTO
214
     * @param string $url image url or filename
215
     * @param bool $include Do we include the image in our vcard or not?
216
     * @param string $element The name of the element to set
217
     */
218
    private function addMedia($property, $url, $include = true, $element)
219
    {
220
        $mimeType = null;
221
222
        //Is this URL for a remote resource?
223
        if (filter_var($url, FILTER_VALIDATE_URL) !== false) {
224
            $headers = get_headers($url, 1);
225
226
            if (array_key_exists('Content-Type', $headers)) {
227
                $mimeType = $headers['Content-Type'];
228
            }
229
        } else {
230
            //Local file, so inspect it directly
231
            $mimeType = mime_content_type($url);
232
        }
233
        if (strpos($mimeType, ';') !== false) {
234
            $mimeType = strstr($mimeType, ';', true);
235
        }
236
        if (!is_string($mimeType) || substr($mimeType, 0, 6) !== 'image/') {
237
            throw VCardException::invalidImage();
238
        }
239
        $fileType = strtoupper(substr($mimeType, 6));
240
241
        if ($include) {
242
            $value = file_get_contents($url);
243
244
            if (!$value) {
245
                throw VCardException::emptyURL();
246
            }
247
248
            $value = base64_encode($value);
249
            $property .= ";ENCODING=b;TYPE=" . $fileType;
250
        } else {
251
            if (filter_var($url, FILTER_VALIDATE_URL) !== false) {
252
                $propertySuffix = ';VALUE=URL';
253
                $propertySuffix .= ';TYPE=' . strtoupper($fileType);
254
255
                $property = $property . $propertySuffix;
256
                $value = $url;
257
            } else {
258
                $value = $url;
259
            }
260
        }
261
262
        $this->setProperty(
263
            $element,
264
            $property,
265
            $value
266
        );
267
    }
268
269
    /**
270
     * Add name
271
     *
272
     * @param  string [optional] $lastName
273
     * @param  string [optional] $firstName
274
     * @param  string [optional] $additional
275
     * @param  string [optional] $prefix
276
     * @param  string [optional] $suffix
277
     * @return $this
278
     */
279
    public function addName(
280
        $lastName = '',
281
        $firstName = '',
282
        $additional = '',
283
        $prefix = '',
284
        $suffix = ''
285
    ) {
286
        // define values with non-empty values
287
        $values = array_filter([
288
            $prefix,
289
            $firstName,
290
            $additional,
291
            $lastName,
292
            $suffix,
293
        ]);
294
295
        // define filename
296
        $this->setFilename($values);
297
298
        // set property
299
        $property = $lastName . ';' . $firstName . ';' . $additional . ';' . $prefix . ';' . $suffix;
300
        $this->setProperty(
301
            'name',
302
            'N' . $this->getCharsetString(),
303
            $property
304
        );
305
306
        // is property FN set?
307
        if (!$this->hasProperty('FN')) {
308
            // set property
309
            $this->setProperty(
310
                'fullname',
311
                'FN' . $this->getCharsetString(),
312
                trim(implode(' ', $values))
313
            );
314
        }
315
316
        return $this;
317
    }
318
319
    /**
320
     * Add note
321
     *
322
     * @param  string $note
323
     * @return $this
324
     */
325
    public function addNote($note)
326
    {
327
        $this->setProperty(
328
            'note',
329
            'NOTE' . $this->getCharsetString(),
330
            $note
331
        );
332
333
        return $this;
334
    }
335
336
    /**
337
     * Add categories
338
     *
339
     * @param array $categories
340
     * @return $this
341
     */
342
    public function addCategories($categories)
343
    {
344
        $this->setProperty(
345
            'categories',
346
            'CATEGORIES' . $this->getCharsetString(),
347
            trim(implode(',', $categories))
348
        );
349
350
        return $this;
351
    }
352
353
    /**
354
     * Add phone number
355
     *
356
     * @param  string $number
357
     * @param  string [optional] $type
358
     *                                   Type may be PREF | WORK | HOME | VOICE | FAX | MSG |
359
     *                                   CELL | PAGER | BBS | CAR | MODEM | ISDN | VIDEO
360
     *                                   or any senseful combination, e.g. "PREF;WORK;VOICE"
361
     * @return $this
362
     */
363
    public function addPhoneNumber($number, $type = '')
364
    {
365
        $this->setProperty(
366
            'phoneNumber',
367
            'TEL' . (($type != '') ? ';' . $type : ''),
368
            $number
369
        );
370
371
        return $this;
372
    }
373
374
    /**
375
     * Add Logo
376
     *
377
     * @param  string $url image url or filename
378
     * @param  bool $include Include the image in our vcard?
379
     * @return $this
380
     */
381
    public function addLogo($url, $include = true)
382
    {
383
        $this->addMedia(
384
            'LOGO',
385
            $url,
386
            $include,
387
            'logo'
388
        );
389
390
        return $this;
391
    }
392
393
    /**
394
     * Add Photo
395
     *
396
     * @param  string $url image url or filename
397
     * @param  bool $include Include the image in our vcard?
398
     * @return $this
399
     */
400
    public function addPhoto($url, $include = true)
401
    {
402
        $this->addMedia(
403
            'PHOTO',
404
            $url,
405
            $include,
406
            'photo'
407
        );
408
409
        return $this;
410
    }
411
412
    /**
413
     * Add URL
414
     *
415
     * @param  string $url
416
     * @param  string [optional] $type Type may be WORK | HOME
417
     * @return $this
418
     */
419
    public function addURL($url, $type = '')
420
    {
421
        $this->setProperty(
422
            'url',
423
            'URL' . (($type != '') ? ';' . $type : ''),
424
            $url
425
        );
426
427
        return $this;
428
    }
429
430
    /**
431
     * Build VCard (.vcf)
432
     *
433
     * @return string
434
     */
435
    public function buildVCard()
436
    {
437
        $properties = $this->getProperties();
438
439
        for($i = 0;$i < count($properties);$i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
440
            // init string
441
            (empty($string)) ? $string = "BEGIN:VCARD\r\n" : $string .= "BEGIN:VCARD\r\n";
442
            
443
            $string .= "VERSION:3.0\r\n";
444
            $string .= "REV:" . date("Y-m-d") . "T" . date("H:i:s") . "Z\r\n";
445
446
            // loop all properties
447
            $property = $this->getProperty($i);
448
            foreach ($property as $data) {
449
                // add to string
450
                $string .= $this->fold($data['key'] . ':' . $this->escape($data['value']) . "\r\n");
451
            }
452
453
            // add to string
454
            $string .= "END:VCARD\r\n";   
455
        }
456
457
        // return
458
        return $string;
0 ignored issues
show
Bug introduced by
The variable $string does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
459
    }
460
461
    /**
462
     * Build VCalender (.ics) - Safari (< iOS 8) can not open .vcf files, so we have build a workaround.
463
     *
464
     * @return string
465
     */
466
    public function buildVCalendar()
467
    {
468
        // init dates
469
        $dtstart = date("Ymd") . "T" . date("Hi") . "00";
470
        $dtend = date("Ymd") . "T" . date("Hi") . "01";
471
472
        // init string
473
        $string = "BEGIN:VCALENDAR\n";
474
        $string .= "VERSION:2.0\n";
475
        $string .= "BEGIN:VEVENT\n";
476
        $string .= "DTSTART;TZID=Europe/London:" . $dtstart . "\n";
477
        $string .= "DTEND;TZID=Europe/London:" . $dtend . "\n";
478
        $string .= "SUMMARY:Click attached contact below to save to your contacts\n";
479
        $string .= "DTSTAMP:" . $dtstart . "Z\n";
480
        $string .= "ATTACH;VALUE=BINARY;ENCODING=BASE64;FMTTYPE=text/directory;\n";
481
        $string .= " X-APPLE-FILENAME=" . $this->getFilename() . "." . $this->getFileExtension() . ":\n";
482
483
        // base64 encode it so that it can be used as an attachemnt to the "dummy" calendar appointment
484
        $b64vcard = base64_encode($this->buildVCard());
485
486
        // chunk the single long line of b64 text in accordance with RFC2045
487
        // (and the exact line length determined from the original .ics file exported from Apple calendar
488
        $b64mline = chunk_split($b64vcard, 74, "\n");
489
490
        // need to indent all the lines by 1 space for the iphone (yes really?!!)
491
        $b64final = preg_replace('/(.+)/', ' $1', $b64mline);
492
        $string .= $b64final;
493
494
        // output the correctly formatted encoded text
495
        $string .= "END:VEVENT\n";
496
        $string .= "END:VCALENDAR\n";
497
498
        // return
499
        return $string;
500
    }
501
502
    /**
503
     * Returns the browser user agent string.
504
     *
505
     * @return string
506
     */
507
    protected function getUserAgent()
508
    {
509
        if (array_key_exists('HTTP_USER_AGENT', $_SERVER)) {
510
            $browser = strtolower($_SERVER['HTTP_USER_AGENT']);
511
        } else {
512
            $browser = 'unknown';
513
        }
514
515
        return $browser;
516
    }
517
518
    /**
519
     * Decode
520
     *
521
     * @param  string $value The value to decode
522
     * @return string decoded
523
     */
524
    private function decode($value)
525
    {
526
        // convert cyrlic, greek or other caracters to ASCII characters
527
        return Transliterator::transliterate($value);
528
    }
529
530
    /**
531
     * Download a vcard or vcal file to the browser.
532
     */
533
    public function download()
534
    {
535
        // define output
536
        $output = $this->getOutput();
537
538
        foreach ($this->getHeaders(false) as $header) {
539
            header($header);
540
        }
541
542
        // echo the output and it will be a download
543
        echo $output;
544
    }
545
546
    /**
547
     * Fold a line according to RFC2425 section 5.8.1.
548
     *
549
     * @link http://tools.ietf.org/html/rfc2425#section-5.8.1
550
     * @param  string $text
551
     * @return mixed
552
     */
553
    protected function fold($text)
554
    {
555
        if (strlen($text) <= 75) {
556
            return $text;
557
        }
558
559
        // split, wrap and trim trailing separator
560
        return substr(chunk_split($text, 73, "\r\n "), 0, -3);
561
    }
562
563
    /**
564
     * Escape newline characters according to RFC2425 section 5.8.4.
565
     *
566
     * @link http://tools.ietf.org/html/rfc2425#section-5.8.4
567
     * @param  string $text
568
     * @return string
569
     */
570
    protected function escape($text)
571
    {
572
        $text = str_replace("\r\n", "\\n", $text);
573
        $text = str_replace("\n", "\\n", $text);
574
575
        return $text;
576
    }
577
578
    /**
579
     * Get output as string
580
     * @deprecated in the future
581
     *
582
     * @return string
583
     */
584
    public function get()
585
    {
586
        return $this->getOutput();
587
    }
588
589
    /**
590
     * Get charset
591
     *
592
     * @return string
593
     */
594
    public function getCharset()
595
    {
596
        return $this->charset;
597
    }
598
599
    /**
600
     * Get charset string
601
     *
602
     * @return string
603
     */
604
    public function getCharsetString()
605
    {
606
        $charsetString = '';
607
        if ($this->charset == 'utf-8') {
608
            $charsetString = ';CHARSET=' . $this->charset;
609
        }
610
611
        return $charsetString;
612
    }
613
614
    /**
615
     * Get content type
616
     *
617
     * @return string
618
     */
619
    public function getContentType()
620
    {
621
        return ($this->isIOS7()) ?
622
            'text/x-vcalendar' : 'text/x-vcard';
623
    }
624
625
    /**
626
     * Get filename
627
     *
628
     * @return string
629
     */
630
    public function getFilename()
631
    {
632
        if (!$this->filename) {
633
            return 'unknown';
634
        }
635
636
        return $this->filename;
637
    }
638
639
    /**
640
     * Get file extension
641
     *
642
     * @return string
643
     */
644
    public function getFileExtension()
645
    {
646
        return ($this->isIOS7()) ?
647
            'ics' : 'vcf';
648
    }
649
650
    /**
651
     * Get headers
652
     *
653
     * @param  bool $asAssociative
654
     * @return array
655
     */
656
    public function getHeaders($asAssociative)
657
    {
658
        $contentType = $this->getContentType() . '; charset=' . $this->getCharset();
659
        $contentDisposition = 'attachment; filename=' . $this->getFilename() . '.' . $this->getFileExtension();
660
        $contentLength = mb_strlen($this->getOutput(), $this->getCharset());
661
        $connection = 'close';
662
663
        if ((bool)$asAssociative) {
664
            return [
665
                'Content-type' => $contentType,
666
                'Content-Disposition' => $contentDisposition,
667
                'Content-Length' => $contentLength,
668
                'Connection' => $connection,
669
            ];
670
        }
671
672
        return [
673
            'Content-type: ' . $contentType,
674
            'Content-Disposition: ' . $contentDisposition,
675
            'Content-Length: ' . $contentLength,
676
            'Connection: ' . $connection,
677
        ];
678
    }
679
680
    /**
681
     * Get output as string
682
     * iOS devices (and safari < iOS 8 in particular) can not read .vcf (= vcard) files.
683
     * So I build a workaround to build a .ics (= vcalender) file.
684
     *
685
     * @return string
686
     */
687
    public function getOutput()
688
    {
689
        $output = ($this->isIOS7()) ?
690
            $this->buildVCalendar() : $this->buildVCard();
691
692
        return $output;
693
    }
694
695
    /**
696
     * Get propertie
697
     *
698
     * @return array
699
     */
700
    public function getProperty($index = -1)
701
    {
702
        $index = ($index == -1) ? $this->index : $index;
703
704
        return $this->properties[$index];
705
    }
706
707
    /**
708
     * Get properties
709
     *
710
     * @return array
711
     */
712
    public function getProperties()
713
    {
714
        return $this->properties;
715
    }
716
717
    /**
718
     * Has property
719
     *
720
     * @param  string $key
721
     * @return bool
722
     */
723
    public function hasProperty($key)
724
    {
725
        $property = $this->getProperty();
726
727
        foreach ($property as $data) {
728
            if ($data['key'] === $key && $data['value'] !== '') {
729
                return true;
730
            }
731
        }
732
733
        return false;
734
    }
735
736
    /**
737
     * Is iOS - Check if the user is using an iOS-device
738
     *
739
     * @return bool
740
     */
741
    public function isIOS()
742
    {
743
        // get user agent
744
        $browser = $this->getUserAgent();
745
746
        return (strpos($browser, 'iphone') || strpos($browser, 'ipod') || strpos($browser, 'ipad'));
747
    }
748
749
    /**
750
     * Is iOS less than 7 (should cal wrapper be returned)
751
     *
752
     * @return bool
753
     */
754
    public function isIOS7()
755
    {
756
        return ($this->isIOS() && $this->shouldAttachmentBeCal());
757
    }
758
759
    /**
760
     * Save to a file
761
     *
762
     * @return void
763
     */
764
    public function save()
765
    {
766
        $file = $this->getFilename() . '.' . $this->getFileExtension();
767
768
        // Add save path if given
769
        if (null !== $this->savePath) {
770
            $file = $this->savePath . $file;
771
        }
772
773
        file_put_contents(
774
            $file,
775
            $this->getOutput()
776
        );
777
    }
778
779
    /**
780
     * Set charset
781
     *
782
     * @param  mixed $charset
783
     * @return void
784
     */
785
    public function setCharset($charset)
786
    {
787
        $this->charset = $charset;
788
    }
789
790
    /**
791
     * Set filename
792
     *
793
     * @param  mixed $value
794
     * @param  bool $overwrite [optional] Default overwrite is true
795
     * @param  string $separator [optional] Default separator is an underscore '_'
796
     * @return void
797
     */
798
    public function setFilename($value, $overwrite = true, $separator = '_')
799
    {
800
        // recast to string if $value is array
801
        if (is_array($value)) {
802
            $value = implode($separator, $value);
803
        }
804
805
        // trim unneeded values
806
        $value = trim($value, $separator);
807
808
        // remove all spaces
809
        $value = preg_replace('/\s+/', $separator, $value);
810
811
        // if value is empty, stop here
812
        if (empty($value)) {
813
            return;
814
        }
815
816
        // decode value + lowercase the string
817
        $value = strtolower($this->decode($value));
818
819
        // urlize this part
820
        $value = Transliterator::urlize($value);
821
822
        // overwrite filename or add to filename using a prefix in between
823
        $this->filename = ($overwrite) ?
824
            $value : $this->filename . $separator . $value;
825
    }
826
827
    /**
828
     * Set the save path directory
829
     *
830
     * @param  string $savePath Save Path
831
     * @throws VCardException
832
     */
833
    public function setSavePath($savePath)
834
    {
835
        if (!is_dir($savePath)) {
836
            throw VCardException::outputDirectoryNotExists();
837
        }
838
839
        // Add trailing directory separator the save path
840
        if (substr($savePath, -1) != DIRECTORY_SEPARATOR) {
841
            $savePath .= DIRECTORY_SEPARATOR;
842
        }
843
844
        $this->savePath = $savePath;
845
    }
846
847
    /**
848
     * Set property
849
     *
850
     * @param  string $element The element name you want to set, f.e.: name, email, phoneNumber, ...
851
     * @param  string $key
852
     * @param  string $value
853
     * @throws VCardException
854
     */
855
    private function setProperty($element, $key, $value)
856
    {
857
        if (!in_array($element, $this->multiplePropertiesForElementAllowed)
858
            && isset($this->definedElements[$this->index][$element])
859
        ) {
860
            throw VCardException::elementAlreadyExists($element);
861
        }
862
863
        // we define that we set this element
864
        $this->definedElements[$this->index][$element] = true;
865
866
        // adding property
867
        $this->properties[$this->index][] = [
868
            'key' => $key,
869
            'value' => $value
870
        ];
871
    }
872
873
    /**
874
     * Checks if we should return vcard in cal wrapper
875
     *
876
     * @return bool
877
     */
878
    protected function shouldAttachmentBeCal()
879
    {
880
        $browser = $this->getUserAgent();
881
882
        $matches = [];
883
        preg_match('/os (\d+)_(\d+)\s+/', $browser, $matches);
884
        $version = isset($matches[1]) ? ((int)$matches[1]) : 999;
885
886
        return ($version < 8);
887
    }
888
}
889