Completed
Pull Request — master (#61)
by
unknown
02:49
created

VCard::getCharsetString()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 1
Metric Value
c 2
b 0
f 1
dl 0
loc 8
rs 9.4285
cc 2
eloc 5
nc 2
nop 0

1 Method

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