Completed
Push — master ( 97f993...4db017 )
by Jeroen
02:04
created

VCard::addCategories()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 10
rs 9.4285
cc 1
eloc 6
nc 1
nop 1
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
 * @author Jeroen Desloovere <[email protected]>
18
 */
19
class VCard
20
{
21
    /**
22
     * definedElements
23
     *
24
     * @var array
25
     */
26
    private $definedElements;
27
28
    /**
29
     * Filename
30
     *
31
     * @var string
32
     */
33
    private $filename;
34
35
    /**
36
     * Multiple properties for element allowed
37
     *
38
     * @var array
39
     */
40
    private $multiplePropertiesForElementAllowed = array(
41
        'email',
42
        'address',
43
        'phoneNumber',
44
        'url'
45
    );
46
47
    /**
48
     * Properties
49
     *
50
     * @var array
51
     */
52
    private $properties;
53
54
    /**
55
     * Default Charset
56
     *
57
     * @var string
58
     */
59
    public $charset = 'utf-8';
60
61
    /**
62
     * Add address
63
     *
64
     * @param  string [optional] $name
65
     * @param  string [optional] $extended
66
     * @param  string [optional] $street
67
     * @param  string [optional] $city
68
     * @param  string [optional] $region
69
     * @param  string [optional] $zip
70
     * @param  string [optional] $country
71
     * @param  string [optional] $type
72
     *                                     $type may be DOM | INTL | POSTAL | PARCEL | HOME | WORK
73
     *                                     or any combination of these: e.g. "WORK;PARCEL;POSTAL"
74
     * @return $this
75
     */
76
    public function addAddress(
77
        $name = '',
78
        $extended = '',
79
        $street = '',
80
        $city = '',
81
        $region = '',
82
        $zip = '',
83
        $country = '',
84
        $type = 'WORK;POSTAL'
85
    ) {
86
        // init value
87
        $value = $name . ';' . $extended . ';' . $street . ';' . $city . ';' . $region . ';' . $zip . ';' . $country;
88
89
        // set property
90
        $this->setProperty(
91
            'address',
92
            'ADR' . (($type != '') ? ';' . $type : '') . $this->getCharsetString(),
93
            $value
94
        );
95
96
        return $this;
97
    }
98
99
    /**
100
     * Add birthday
101
     *
102
     * @param  string $date Format is YYYY-MM-DD
103
     * @return $this
104
     */
105
    public function addBirthday($date)
106
    {
107
        $this->setProperty(
108
            'birthday',
109
            'BDAY',
110
            $date
111
        );
112
113
        return $this;
114
    }
115
116
    /**
117
     * Add company
118
     *
119
     * @param  string $company
120
     * @return $this
121
     */
122
    public function addCompany($company)
123
    {
124
        $this->setProperty(
125
            'company',
126
            'ORG' . $this->getCharsetString(),
127
            $company
128
        );
129
130
        // if filename is empty, add to filename
131
        if ($this->filename === null) {
132
            $this->setFilename($company);
133
        }
134
135
        return $this;
136
    }
137
138
    /**
139
     * Add email
140
     *
141
     * @param  string            $address The e-mail address
142
     * @param  string [optional] $type    The type of the email address
143
     *                                    $type may be  PREF | WORK | HOME
144
     *                                    or any combination of these: e.g. "PREF;WORK"
145
     * @return $this
146
     */
147
    public function addEmail($address, $type = '')
148
    {
149
        $this->setProperty(
150
            'email',
151
            'EMAIL;INTERNET' . (($type != '') ? ';' . $type : ''),
152
            $address
153
        );
154
155
        return $this;
156
    }
157
158
    /**
159
     * Add jobtitle
160
     *
161
     * @param  string $jobtitle The jobtitle for the person.
162
     * @return $this
163
     */
164
    public function addJobtitle($jobtitle)
165
    {
166
        $this->setProperty(
167
            'jobtitle',
168
            'TITLE' . $this->getCharsetString(),
169
            $jobtitle
170
        );
171
172
        return $this;
173
    }
174
175
    /**
176
     * Add role
177
     *
178
     * @param  string $role The role for the person.
179
     * @return $this
180
     */
181
    public function addRole($role)
182
    {
183
        $this->setProperty(
184
            'role',
185
            'ROLE' . $this->getCharsetString(),
186
            $role
187
        );
188
189
        return $this;
190
    }
191
192
    /**
193
     * Add a photo or logo (depending on property name)
194
     *
195
     * @param  string              $property LOGO|PHOTO
196
     * @param  string              $url      image url or filename
197
     * @param  bool                $include  Do we include the image in our vcard or not?
198
     * @throws VCardMediaException if file is empty or not an image file
199
     */
200
    private function addMedia($property, $url, $include = true, $element)
201
    {
202
        if ($include) {
203
            $value = file_get_contents($url);
204
205
            if (!$value) {
206
                throw new VCardMediaException('Nothing returned from URL.');
207
            }
208
209
            $value = base64_encode($value);
210
211
            $finfo = finfo_open(FILEINFO_MIME_TYPE);
212
            $mimetype = finfo_file($finfo, 'data://application/octet-stream;base64,' . $value);
213
            finfo_close($finfo);
214
215
            if (preg_match('/^image\//', $mimetype) !== 1) {
216
                throw new VCardMediaException('Returned data aren\'t an image.');
217
            }
218
219
            $type = strtoupper(str_replace('image/', '', $mimetype));
220
221
            $property .= ";ENCODING=b;TYPE=" . $type;
222
        } else {
223
            if (filter_var($url, FILTER_VALIDATE_URL) !== FALSE) {
224
                $propertySuffix = ';VALUE=URL';
225
226
                $headers = get_headers($url);
227
228
                $imageTypeMatched = false;
229
                $fileType = null;
230
231
                foreach ($headers as $header) {
232
                    if (preg_match('/Content-Type:\simage\/([a-z]+)/i', $header, $m)) {
233
                        $fileType = $m[1];
234
                        $imageTypeMatched = true;
235
                    }
236
                }
237
238
                if (!$imageTypeMatched) {
239
                    throw new VCardMediaException('Returned data isn\'t an image.');
240
                }
241
242
                $propertySuffix .= ';TYPE=' . strtoupper($fileType);
243
244
                $property = $property . $propertySuffix;
245
                $value = $url;
246
            } else {
247
                $value = $url;
248
            }
249
        }
250
251
        $this->setProperty(
252
            $element,
253
            $property,
254
            $value
255
        );
256
    }
257
258
    /**
259
     * Add name
260
     *
261
     * @param  string [optional] $lastName
262
     * @param  string [optional] $firstName
263
     * @param  string [optional] $additional
264
     * @param  string [optional] $prefix
265
     * @param  string [optional] $suffix
266
     * @return $this
267
     */
268
    public function addName(
269
        $lastName = '',
270
        $firstName = '',
271
        $additional = '',
272
        $prefix = '',
273
        $suffix = ''
274
    ) {
275
        // define values with non-empty values
276
        $values = array_filter(array(
277
            $prefix,
278
            $firstName,
279
            $additional,
280
            $lastName,
281
            $suffix,
282
        ));
283
284
        // define filename
285
        $this->setFilename($values);
286
287
        // set property
288
        $property = $lastName . ';' . $firstName . ';' . $additional . ';' . $prefix . ';' . $suffix;
289
        $this->setProperty(
290
            'name',
291
            'N' . $this->getCharsetString(),
292
            $property
293
        );
294
295
        // is property FN set?
296
        if (!$this->hasProperty('FN')) {
297
            // set property
298
            $this->setProperty(
299
                'fullname',
300
                'FN' . $this->getCharsetString(),
301
                trim(implode(' ', $values))
302
            );
303
        }
304
305
        return $this;
306
    }
307
308
    /**
309
     * Add note
310
     *
311
     * @param  string $note
312
     * @return $this
313
     */
314
    public function addNote($note)
315
    {
316
        $this->setProperty(
317
            'note',
318
            'NOTE' . $this->getCharsetString(),
319
            $note
320
        );
321
322
        return $this;
323
    }
324
325
    /**
326
     * Add categories
327
     *
328
     * @param array $categories
329
     * @return $this
330
     */
331
    public function addCategories($categories)
332
    {
333
        $this->setProperty(
334
            'categories',
335
            'CATEGORIES' . $this->getCharsetString(),
336
            trim(implode(',', $categories))
337
        );
338
339
        return $this;
340
    }
341
342
    /**
343
     * Add phone number
344
     *
345
     * @param  string            $number
346
     * @param  string [optional] $type
347
     *                                   Type may be PREF | WORK | HOME | VOICE | FAX | MSG |
348
     *                                   CELL | PAGER | BBS | CAR | MODEM | ISDN | VIDEO
349
     *                                   or any senseful combination, e.g. "PREF;WORK;VOICE"
350
     * @return $this
351
     */
352
    public function addPhoneNumber($number, $type = '')
353
    {
354
        $this->setProperty(
355
            'phoneNumber',
356
            'TEL' . (($type != '') ? ';' . $type : ''),
357
            $number
358
        );
359
360
        return $this;
361
    }
362
363
    /**
364
     * Add Logo
365
     *
366
     * @param  string $url     image url or filename
367
     * @param  bool   $include Include the image in our vcard?
368
     * @return $this
369
     */
370
    public function addLogo($url, $include = true)
371
    {
372
        $this->addMedia(
373
            'LOGO',
374
            $url,
375
            $include,
376
            'logo'
377
        );
378
379
        return $this;
380
    }
381
382
    /**
383
     * Add Photo
384
     *
385
     * @param  string $url     image url or filename
386
     * @param  bool   $include Include the image in our vcard?
387
     * @return $this
388
     */
389
    public function addPhoto($url, $include = true)
390
    {
391
        $this->addMedia(
392
            'PHOTO',
393
            $url,
394
            $include,
395
            'photo'
396
        );
397
398
        return $this;
399
    }
400
401
    /**
402
     * Add URL
403
     *
404
     * @param  string            $url
405
     * @param  string [optional] $type Type may be WORK | HOME
406
     * @return $this
407
     */
408
    public function addURL($url, $type = '')
409
    {
410
        $this->setProperty(
411
            'url',
412
            'URL' . (($type != '') ? ';' . $type : ''),
413
            $url
414
        );
415
416
        return $this;
417
    }
418
419
    /**
420
     * Build VCard (.vcf)
421
     *
422
     * @return string
423
     */
424
    public function buildVCard()
425
    {
426
        // init string
427
        $string = "BEGIN:VCARD\r\n";
428
        $string .= "VERSION:3.0\r\n";
429
        $string .= "REV:" . date("Y-m-d") . "T" . date("H:i:s") . "Z\r\n";
430
431
        // loop all properties
432
        $properties = $this->getProperties();
433
        foreach ($properties as $property) {
434
            // add to string
435
            $string .= $this->fold($property['key'] . ':' . $this->escape($property['value']) . "\r\n");
436
        }
437
438
        // add to string
439
        $string .= "END:VCARD\r\n";
440
441
        // return
442
        return $string;
443
    }
444
445
    /**
446
     * Build VCalender (.ics) - Safari (< iOS 8) can not open .vcf files, so we have build a workaround.
447
     *
448
     * @return string
449
     */
450
    public function buildVCalendar()
451
    {
452
        // init dates
453
        $dtstart = date("Ymd") . "T" . date("Hi") . "00";
454
        $dtend = date("Ymd") . "T" . date("Hi") . "01";
455
456
        // init string
457
        $string = "BEGIN:VCALENDAR\n";
458
        $string .= "VERSION:2.0\n";
459
        $string .= "BEGIN:VEVENT\n";
460
        $string .= "DTSTART;TZID=Europe/London:" . $dtstart . "\n";
461
        $string .= "DTEND;TZID=Europe/London:" . $dtend . "\n";
462
        $string .= "SUMMARY:Click attached contact below to save to your contacts\n";
463
        $string .= "DTSTAMP:" . $dtstart . "Z\n";
464
        $string .= "ATTACH;VALUE=BINARY;ENCODING=BASE64;FMTTYPE=text/directory;\n";
465
        $string .= " X-APPLE-FILENAME=" . $this->getFilename() . "." . $this->getFileExtension() . ":\n";
466
467
        // base64 encode it so that it can be used as an attachemnt to the "dummy" calendar appointment
468
        $b64vcard = base64_encode($this->buildVCard());
469
470
        // chunk the single long line of b64 text in accordance with RFC2045
471
        // (and the exact line length determined from the original .ics file exported from Apple calendar
472
        $b64mline = chunk_split($b64vcard, 74, "\n");
473
474
        // need to indent all the lines by 1 space for the iphone (yes really?!!)
475
        $b64final = preg_replace('/(.+)/', ' $1', $b64mline);
476
        $string .= $b64final;
477
478
        // output the correctly formatted encoded text
479
        $string .= "END:VEVENT\n";
480
        $string .= "END:VCALENDAR\n";
481
482
        // return
483
        return $string;
484
    }
485
486
    /**
487
     * Returns the browser user agent string.
488
     *
489
     * @return string
490
     */
491
    protected function getUserAgent()
0 ignored issues
show
Coding Style introduced by
getUserAgent uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
492
    {
493
        if (array_key_exists('HTTP_USER_AGENT', $_SERVER)) {
494
            $browser = strtolower($_SERVER['HTTP_USER_AGENT']);
495
        } else {
496
            $browser = 'unknown';
497
        }
498
499
        return $browser;
500
    }
501
502
    /**
503
     * Decode
504
     *
505
     * @param  string $value The value to decode
506
     * @return string decoded
507
     */
508
    private function decode($value)
509
    {
510
        // convert cyrlic, greek or other caracters to ASCII characters
511
        return Transliterator::transliterate($value);
512
    }
513
514
    /**
515
     * Download a vcard or vcal file to the browser.
516
     */
517
    public function download()
518
    {
519
        // define output
520
        $output = $this->getOutput();
521
522
        foreach ($this->getHeaders(false) as $header) {
523
            header($header);
524
        }
525
526
        // echo the output and it will be a download
527
        echo $output;
528
    }
529
530
    /**
531
     * Fold a line according to RFC2425 section 5.8.1.
532
     *
533
     * @link http://tools.ietf.org/html/rfc2425#section-5.8.1
534
     * @param  string $text
535
     * @return mixed
536
     */
537
    protected function fold($text)
538
    {
539
        if (strlen($text) <= 75) {
540
            return $text;
541
        }
542
543
        // split, wrap and trim trailing separator
544
        return substr(chunk_split($text, 73, "\r\n "), 0, -3);
545
    }
546
547
    /**
548
     * Escape newline characters according to RFC2425 section 5.8.4.
549
     *
550
     * @link http://tools.ietf.org/html/rfc2425#section-5.8.4
551
     * @param  string $text
552
     * @return string
553
     */
554
    protected function escape($text)
555
    {
556
        $text = str_replace("\r\n", "\\n", $text);
557
        $text = str_replace("\n", "\\n", $text);
558
559
        return $text;
560
    }
561
562
    /**
563
     * Get output as string
564
     * @deprecated in the future
565
     *
566
     * @return string
567
     */
568
    public function get()
569
    {
570
        return $this->getOutput();
571
    }
572
573
    /**
574
     * Get charset
575
     *
576
     * @return string
577
     */
578
    public function getCharset()
579
    {
580
        return $this->charset;
581
    }
582
583
    /**
584
     * Get charset string
585
     *
586
     * @return string
587
     */
588
    public function getCharsetString()
589
    {
590
        $charsetString = '';
591
        if ($this->charset == 'utf-8') {
592
            $charsetString = ';CHARSET=' . $this->charset;
593
        }
594
        return $charsetString;
595
    }
596
597
    /**
598
     * Get content type
599
     *
600
     * @return string
601
     */
602
    public function getContentType()
603
    {
604
        return ($this->isIOS7()) ?
605
            'text/x-vcalendar' : 'text/x-vcard';
606
    }
607
608
    /**
609
     * Get filename
610
     *
611
     * @return string
612
     */
613
    public function getFilename()
614
    {
615
        if (!$this->filename) {
616
            return 'unknown';
617
        }
618
        return $this->filename;
619
    }
620
621
    /**
622
     * Get file extension
623
     *
624
     * @return string
625
     */
626
    public function getFileExtension()
627
    {
628
        return ($this->isIOS7()) ?
629
            'ics' : 'vcf';
630
    }
631
632
    /**
633
     * Get headers
634
     *
635
     * @param  bool  $asAssociative
636
     * @return array
637
     */
638
    public function getHeaders($asAssociative)
639
    {
640
        $contentType        = $this->getContentType() . '; charset=' . $this->getCharset();
641
        $contentDisposition = 'attachment; filename=' . $this->getFilename() . '.' . $this->getFileExtension();
642
        $contentLength      = mb_strlen($this->getOutput(), $this->getCharset());
643
        $connection         = 'close';
644
645
        if ((bool) $asAssociative) {
646
            return array(
647
                'Content-type'        => $contentType,
648
                'Content-Disposition' => $contentDisposition,
649
                'Content-Length'      => $contentLength,
650
                'Connection'          => $connection,
651
            );
652
        }
653
654
        return array(
655
            'Content-type: ' . $contentType,
656
            'Content-Disposition: ' . $contentDisposition,
657
            'Content-Length: ' . $contentLength,
658
            'Connection: ' . $connection,
659
        );
660
    }
661
662
    /**
663
     * Get output as string
664
     * iOS devices (and safari < iOS 8 in particular) can not read .vcf (= vcard) files.
665
     * So I build a workaround to build a .ics (= vcalender) file.
666
     *
667
     * @return string
668
     */
669
    public function getOutput()
670
    {
671
        $output = ($this->isIOS7()) ?
672
            $this->buildVCalendar() : $this->buildVCard();
673
674
        return $output;
675
    }
676
677
    /**
678
     * Get properties
679
     *
680
     * @return array
681
     */
682
    public function getProperties()
683
    {
684
        return $this->properties;
685
    }
686
687
    /**
688
     * Has property
689
     *
690
     * @param  string $key
691
     * @return bool
692
     */
693
    public function hasProperty($key)
694
    {
695
        $properties = $this->getProperties();
696
697
        foreach ($properties as $property) {
698
            if ($property['key'] === $key && $property['value'] !== '') {
699
                return true;
700
            }
701
        }
702
703
        return false;
704
    }
705
706
    /**
707
     * Is iOS - Check if the user is using an iOS-device
708
     *
709
     * @return bool
710
     */
711
    public function isIOS()
712
    {
713
        // get user agent
714
        $browser = $this->getUserAgent();
715
716
        return (strpos($browser, 'iphone') || strpos($browser, 'ipod') || strpos($browser, 'ipad'));
717
    }
718
719
    /**
720
     * Is iOS less than 7 (should cal wrapper be returned)
721
     *
722
     * @return bool
723
     */
724
    public function isIOS7()
725
    {
726
        return ($this->isIOS() && $this->shouldAttachmentBeCal());
727
    }
728
729
    /**
730
     * Save to a file
731
     *
732
     * @return void
733
     */
734
    public function save()
735
    {
736
        $file = $this->getFilename() . '.' . $this->getFileExtension();
737
738
        file_put_contents(
739
            $file,
740
            $this->getOutput()
741
        );
742
    }
743
744
    /**
745
     * Set charset
746
     *
747
     * @param  mixed  $charset
748
     * @return void
749
     */
750
    public function setCharset($charset)
751
    {
752
        $this->charset = $charset;
753
    }
754
755
    /**
756
     * Set filename
757
     *
758
     * @param  mixed  $value
759
     * @param  bool   $overwrite [optional] Default overwrite is true
760
     * @param  string $separator [optional] Default separator is an underscore '_'
761
     * @return void
762
     */
763
    public function setFilename($value, $overwrite = true, $separator = '_')
764
    {
765
        // recast to string if $value is array
766
        if (is_array($value)) {
767
            $value = implode($separator, $value);
768
        }
769
770
        // trim unneeded values
771
        $value = trim($value, $separator);
772
773
        // remove all spaces
774
        $value = preg_replace('/\s+/', $separator, $value);
775
776
        // if value is empty, stop here
777
        if (empty($value)) {
778
            return;
779
        }
780
781
        // decode value + lowercase the string
782
        $value = strtolower($this->decode($value));
783
784
        // urlize this part
785
        $value = Transliterator::urlize($value);
786
787
        // overwrite filename or add to filename using a prefix in between
788
        $this->filename = ($overwrite) ?
789
            $value : $this->filename . $separator . $value;
790
    }
791
792
    /**
793
     * Set property
794
     *
795
     * @param  string $element The element name you want to set, f.e.: name, email, phoneNumber, ...
796
     * @param  string $key
797
     * @param  string $value
798
     * @return void
799
     */
800
    private function setProperty($element, $key, $value)
801
    {
802
        if (!in_array($element, $this->multiplePropertiesForElementAllowed)
803
            && isset($this->definedElements[$element])
804
        ) {
805
            throw new Exception('You can only set "' . $element . '" once.');
806
        }
807
808
        // we define that we set this element
809
        $this->definedElements[$element] = true;
810
811
        // adding property
812
        $this->properties[] = array(
813
            'key' => $key,
814
            'value' => $value
815
        );
816
    }
817
818
    /**
819
     * Checks if we should return vcard in cal wrapper
820
     *
821
     * @return bool
822
     */
823
    protected function shouldAttachmentBeCal()
824
    {
825
        $browser = $this->getUserAgent();
826
827
        $matches = array();
828
        preg_match('/os (\d+)_(\d+)\s+/', $browser, $matches);
829
        $version = isset($matches[1]) ? ((int) $matches[1]) : 999;
830
831
        return ($version < 8);
832
    }
833
}
834