Completed
Push — master ( fe80b3...021b24 )
by Jeroen
11s
created

VCard::getFileExtension()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 2 Features 2
Metric Value
c 4
b 2
f 2
dl 0
loc 5
rs 9.4285
cc 2
eloc 3
nc 2
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
 * @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->getFilename() === 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 a photo or logo (depending on property name)
177
     *
178
     * @param  string              $property LOGO|PHOTO
179
     * @param  string              $url      image url or filename
180
     * @param  bool                $include  Do we include the image in our vcard or not?
181
     * @throws VCardMediaException if file is empty or not an image file
182
     */
183
    private function addMedia($property, $url, $include = true, $element)
184
    {
185
        if ($include) {
186
            $value = file_get_contents($url);
187
188
            if (!$value) {
189
                throw new VCardMediaException('Nothing returned from URL.');
190
            }
191
192
            $value = base64_encode($value);
193
194
            $finfo = finfo_open(FILEINFO_MIME_TYPE);
195
            $mimetype = finfo_file($finfo, 'data://application/octet-stream;base64,' . $value);
196
            finfo_close($finfo);
197
198
            if (preg_match('/^image\//', $mimetype) !== 1) {
199
                throw new VCardMediaException('Returned data aren\'t an image.');
200
            }
201
202
            $type = strtoupper(str_replace('image/', '', $mimetype));
203
204
            $property .= ";ENCODING=b;TYPE=" . $type;
205
        } else {
206
            $value = $url;
207
        }
208
209
        $this->setProperty(
210
            $element,
211
            $property,
212
            $value
213
        );
214
    }
215
216
    /**
217
     * Add name
218
     *
219
     * @param  string [optional] $lastName
220
     * @param  string [optional] $firstName
221
     * @param  string [optional] $additional
222
     * @param  string [optional] $prefix
223
     * @param  string [optional] $suffix
224
     * @return $this
225
     */
226
    public function addName(
227
        $lastName = '',
228
        $firstName = '',
229
        $additional = '',
230
        $prefix = '',
231
        $suffix = ''
232
    ) {
233
        // define values with non-empty values
234
        $values = array_filter(array(
235
            $prefix,
236
            $firstName,
237
            $additional,
238
            $lastName,
239
            $suffix,
240
        ));
241
242
        // define filename
243
        $this->setFilename($values);
244
245
        // set property
246
        $property = $lastName . ';' . $firstName . ';' . $additional . ';' . $prefix . ';' . $suffix;
247
        $this->setProperty(
248
            'name',
249
            'N' . $this->getCharsetString(),
250
            $property
251
        );
252
253
        // is property FN set?
254
        if (!$this->hasProperty('FN')) {
255
            // set property
256
            $this->setProperty(
257
                'fullname',
258
                'FN' . $this->getCharsetString(),
259
                trim(implode(' ', $values))
260
            );
261
        }
262
263
        return $this;
264
    }
265
266
    /**
267
     * Add note
268
     *
269
     * @param  string $note
270
     * @return $this
271
     */
272
    public function addNote($note)
273
    {
274
        $this->setProperty(
275
            'note',
276
            'NOTE' . $this->getCharsetString(),
277
            $note
278
        );
279
280
        return $this;
281
    }
282
283
    /**
284
     * Add phone number
285
     *
286
     * @param  string            $number
287
     * @param  string [optional] $type
288
     *                                   Type may be PREF | WORK | HOME | VOICE | FAX | MSG |
289
     *                                   CELL | PAGER | BBS | CAR | MODEM | ISDN | VIDEO
290
     *                                   or any senseful combination, e.g. "PREF;WORK;VOICE"
291
     * @return $this
292
     */
293
    public function addPhoneNumber($number, $type = '')
294
    {
295
        $this->setProperty(
296
            'phoneNumber',
297
            'TEL' . (($type != '') ? ';' . $type : ''),
298
            $number
299
        );
300
301
        return $this;
302
    }
303
304
    /**
305
     * Add Photo
306
     *
307
     * @param  string $url     image url or filename
308
     * @param  bool   $include Include the image in our vcard?
309
     * @return $this
310
     */
311
    public function addPhoto($url, $include = true)
312
    {
313
        $this->addMedia(
314
            'PHOTO',
315
            $url,
316
            $include,
317
            'photo'
318
        );
319
320
        return $this;
321
    }
322
323
    /**
324
     * Add URL
325
     *
326
     * @param  string            $url
327
     * @param  string [optional] $type Type may be WORK | HOME
328
     * @return $this
329
     */
330
    public function addURL($url, $type = '')
331
    {
332
        $this->setProperty(
333
            'url',
334
            'URL' . (($type != '') ? ';' . $type : ''),
335
            $url
336
        );
337
338
        return $this;
339
    }
340
341
    /**
342
     * Build VCard (.vcf)
343
     *
344
     * @return string
345
     */
346
    public function buildVCard()
347
    {
348
        // init string
349
        $string = "BEGIN:VCARD\r\n";
350
        $string .= "VERSION:3.0\r\n";
351
        $string .= "REV:" . date("Y-m-d") . "T" . date("H:i:s") . "Z\r\n";
352
353
        // loop all properties
354
        $properties = $this->getProperties();
355
        foreach ($properties as $property) {
356
            // add to string
357
            $string .= $this->fold($property['key'] . ':' . $property['value'] . "\r\n");
358
        }
359
360
        // add to string
361
        $string .= "END:VCARD\r\n";
362
363
        // return
364
        return $string;
365
    }
366
367
    /**
368
     * Build VCalender (.ics) - Safari (< iOS 8) can not open .vcf files, so we have build a workaround.
369
     *
370
     * @return string
371
     */
372
    public function buildVCalendar()
373
    {
374
        // init dates
375
        $dtstart = date("Ymd") . "T" . date("Hi") . "00";
376
        $dtend = date("Ymd") . "T" . date("Hi") . "01";
377
378
        // init string
379
        $string = "BEGIN:VCALENDAR\n";
380
        $string .= "VERSION:2.0\n";
381
        $string .= "BEGIN:VEVENT\n";
382
        $string .= "DTSTART;TZID=Europe/London:" . $dtstart . "\n";
383
        $string .= "DTEND;TZID=Europe/London:" . $dtend . "\n";
384
        $string .= "SUMMARY:Click attached contact below to save to your contacts\n";
385
        $string .= "DTSTAMP:" . $dtstart . "Z\n";
386
        $string .= "ATTACH;VALUE=BINARY;ENCODING=BASE64;FMTTYPE=text/directory;\n";
387
        $string .= " X-APPLE-FILENAME=" . $this->getFilename() . "." . $this->getFileExtension() . ":\n";
388
389
        // base64 encode it so that it can be used as an attachemnt to the "dummy" calendar appointment
390
        $b64vcard = base64_encode($this->buildVCard());
391
392
        // chunk the single long line of b64 text in accordance with RFC2045
393
        // (and the exact line length determined from the original .ics file exported from Apple calendar
394
        $b64mline = chunk_split($b64vcard, 74, "\n");
395
396
        // need to indent all the lines by 1 space for the iphone (yes really?!!)
397
        $b64final = preg_replace('/(.+)/', ' $1', $b64mline);
398
        $string .= $b64final;
399
400
        // output the correctly formatted encoded text
401
        $string .= "END:VEVENT\n";
402
        $string .= "END:VCALENDAR\n";
403
404
        // return
405
        return $string;
406
    }
407
408
    /**
409
     * Returns the browser user agent string.
410
     *
411
     * @return string
412
     */
413
    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...
414
    {
415
        if (array_key_exists('HTTP_USER_AGENT', $_SERVER)) {
416
            $browser = strtolower($_SERVER['HTTP_USER_AGENT']);
417
        } else {
418
            $browser = 'unknown';
419
        }
420
421
        return $browser;
422
    }
423
424
    /**
425
     * Decode
426
     *
427
     * @param  string $value The value to decode
428
     * @return string decoded
429
     */
430
    private function decode($value)
431
    {
432
        // convert cyrlic, greek or other caracters to ASCII characters
433
        return Transliterator::transliterate($value);
434
    }
435
436
    /**
437
     * Download a vcard or vcal file to the browser.
438
     */
439
    public function download()
440
    {
441
        // define output
442
        $output = $this->getOutput();
443
444
        foreach ($this->getHeaders(false) as $header) {
445
            header($header);
446
        }
447
448
        // echo the output and it will be a download
449
        echo $output;
450
    }
451
452
    /**
453
     * Fold a line according to RFC2425 section 5.8.1.
454
     *
455
     * @link http://tools.ietf.org/html/rfc2425#section-5.8.1
456
     * @param  string $text
457
     * @return mixed
458
     */
459
    protected function fold($text)
460
    {
461
        if (strlen($text) <= 75) {
462
            return $text;
463
        }
464
465
        // split, wrap and trim trailing separator
466
        return substr(chunk_split($text, 73, "\r\n "), 0, -3);
467
    }
468
469
    /**
470
     * Get output as string
471
     * @deprecated in the future
472
     *
473
     * @return string
474
     */
475
    public function get()
476
    {
477
        return $this->getOutput();
478
    }
479
480
    /**
481
     * Get charset
482
     *
483
     * @return string
484
     */
485
    public function getCharset()
486
    {
487
        return $this->charset;
488
    }
489
490
    /**
491
     * Get charset string
492
     *
493
     * @return string
494
     */
495
    public function getCharsetString()
496
    {
497
        $charsetString = '';
498
        if ($this->charset == 'utf-8') {
499
            $charsetString = ';CHARSET=' . $this->charset;
500
        }
501
        return $charsetString;
502
    }
503
504
    /**
505
     * Get content type
506
     *
507
     * @return string
508
     */
509
    public function getContentType()
510
    {
511
        return ($this->isIOS7()) ?
512
            'text/x-vcalendar' : 'text/x-vcard';
513
    }
514
515
    /**
516
     * Get filename
517
     *
518
     * @return string
519
     */
520
    public function getFilename()
521
    {
522
        return $this->filename;
523
    }
524
525
    /**
526
     * Get file extension
527
     *
528
     * @return string
529
     */
530
    public function getFileExtension()
531
    {
532
        return ($this->isIOS7()) ?
533
            'ics' : 'vcf';
534
    }
535
536
    /**
537
     * Get headers
538
     *
539
     * @param  bool  $asAssociative
540
     * @return array
541
     */
542
    public function getHeaders($asAssociative)
543
    {
544
        $contentType        = $this->getContentType() . '; charset=' . $this->getCharset();
545
        $contentDisposition = 'attachment; filename=' . $this->getFilename() . '.' . $this->getFileExtension();
546
        $contentLength      = strlen($this->getOutput());
547
        $connection         = 'close';
548
549
        if ((bool) $asAssociative) {
550
            return array(
551
                'Content-type'        => $contentType,
552
                'Content-Disposition' => $contentDisposition,
553
                'Content-Length'      => $contentLength,
554
                'Connection'          => $connection,
555
            );
556
        }
557
558
        return array(
559
            'Content-type: ' . $contentType,
560
            'Content-Disposition: ' . $contentDisposition,
561
            'Content-Length: ' . $contentLength,
562
            'Connection: ' . $connection,
563
        );
564
    }
565
566
    /**
567
     * Get output as string
568
     * iOS devices (and safari < iOS 8 in particular) can not read .vcf (= vcard) files.
569
     * So I build a workaround to build a .ics (= vcalender) file.
570
     *
571
     * @return string
572
     */
573
    public function getOutput()
574
    {
575
        $output = ($this->isIOS7()) ?
576
            $this->buildVCalendar() : $this->buildVCard();
577
578
        // we need to decode the output for outlook
579
        if ($this->getCharset() == 'utf-8') {
580
            $output = utf8_decode($output);
581
        }
582
583
        return $output;
584
    }
585
586
    /**
587
     * Get properties
588
     *
589
     * @return array
590
     */
591
    public function getProperties()
592
    {
593
        return $this->properties;
594
    }
595
596
    /**
597
     * Has property
598
     *
599
     * @param  string $key
600
     * @return bool
601
     */
602
    public function hasProperty($key)
603
    {
604
        $properties = $this->getProperties();
605
606
        foreach ($properties as $property) {
607
            if ($property['key'] === $key && $property['value'] !== '') {
608
                return true;
609
            }
610
        }
611
612
        return false;
613
    }
614
615
    /**
616
     * Is iOS - Check if the user is using an iOS-device
617
     *
618
     * @return bool
619
     */
620
    public function isIOS()
621
    {
622
        // get user agent
623
        $browser = $this->getUserAgent();
624
625
        return (strpos($browser, 'iphone') || strpos($browser, 'ipod') || strpos($browser, 'ipad'));
626
    }
627
628
    /**
629
     * Is iOS less than 7 (should cal wrapper be returned)
630
     *
631
     * @return bool
632
     */
633
    public function isIOS7()
634
    {
635
        return ($this->isIOS() && $this->shouldAttachmentBeCal());
636
    }
637
638
    /**
639
     * Save to a file
640
     *
641
     * @return void
642
     */
643
    public function save()
644
    {
645
        $file = $this->getFilename() . '.' . $this->getFileExtension();
646
647
        file_put_contents(
648
            $file,
649
            $this->getOutput()
650
        );
651
    }
652
653
    /**
654
     * Set charset
655
     *
656
     * @param  mixed  $charset
657
     * @return void
658
     */
659
    public function setCharset($charset)
660
    {
661
        $this->charset = $charset;
662
    }
663
664
    /**
665
     * Set filename
666
     *
667
     * @param  mixed  $value
668
     * @param  bool   $overwrite [optional] Default overwrite is true
669
     * @param  string $separator [optional] Default separator is an underscore '_'
670
     * @return void
671
     */
672
    public function setFilename($value, $overwrite = true, $separator = '_')
673
    {
674
        // recast to string if $value is array
675
        if (is_array($value)) {
676
            $value = implode($separator, $value);
677
        }
678
679
        // trim unneeded values
680
        $value = trim($value, $separator);
681
682
        // remove all spaces
683
        $value = preg_replace('/\s+/', $separator, $value);
684
685
        // if value is empty, stop here
686
        if (empty($value)) {
687
            return;
688
        }
689
690
        // decode value + lowercase the string
691
        $value = strtolower($this->decode($value));
692
693
        // urlize this part
694
        $value = Transliterator::urlize($value);
695
696
        // overwrite filename or add to filename using a prefix in between
697
        $this->filename = ($overwrite) ?
698
            $value : $this->filename . $separator . $value;
699
    }
700
701
    /**
702
     * Set property
703
     *
704
     * @param  string $element The element name you want to set, f.e.: name, email, phoneNumber, ...
705
     * @param  string $key
706
     * @param  string $value
707
     * @return void
708
     */
709
    private function setProperty($element, $key, $value)
710
    {
711
        if (!in_array($element, $this->multiplePropertiesForElementAllowed)
712
            && isset($this->definedElements[$element])
713
        ) {
714
            throw new Exception('You can only set "' . $element . '" once.');
715
        }
716
717
        // we define that we set this element
718
        $this->definedElements[$element] = true;
719
720
        // adding property
721
        $this->properties[] = array(
722
            'key' => $key,
723
            'value' => $value
724
        );
725
    }
726
727
    /**
728
     * Checks if we should return vcard in cal wrapper
729
     *
730
     * @return bool
731
     */
732
    protected function shouldAttachmentBeCal()
733
    {
734
        $browser = $this->getUserAgent();
735
736
        $matches = array();
737
        preg_match('/os (\d+)_(\d+)\s+/', $browser, $matches);
738
        $version = isset($matches[1]) ? ((int) $matches[1]) : 999;
739
740
        return ($version < 8);
741
    }
742
}
743