Completed
Pull Request — master (#55)
by
unknown
04:28
created

VCard::addLabel()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 10
Code Lines 6

Duplication

Lines 10
Ratio 100 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 10
loc 10
rs 9.4286
cc 2
eloc 6
nc 1
nop 2
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
        'label'
46
    );
47
48
    /**
49
     * Properties
50
     *
51
     * @var array
52
     */
53
    private $properties;
54
55
    /**
56
     * Default Charset
57
     *
58
     * @var string
59
     */
60
    public $charset = 'utf-8';
61
62
    /**
63
     * Add address
64
     *
65
     * @param  string [optional] $name
66
     * @param  string [optional] $extended
67
     * @param  string [optional] $street
68
     * @param  string [optional] $city
69
     * @param  string [optional] $region
70
     * @param  string [optional] $zip
71
     * @param  string [optional] $country
72
     * @param  string [optional] $type
73
     *                                     $type may be DOM | INTL | POSTAL | PARCEL | HOME | WORK
74
     *                                     or any combination of these: e.g. "WORK;PARCEL;POSTAL"
75
     * @return $this
76
     */
77
    public function addAddress(
78
        $name = '',
79
        $extended = '',
80
        $street = '',
81
        $city = '',
82
        $region = '',
83
        $zip = '',
84
        $country = '',
85
        $type = 'WORK;POSTAL'
86
    ) {
87
        // init value
88
        $value = $name . ';' . $extended . ';' . $street . ';' . $city . ';' . $region . ';' . $zip . ';' . $country;
89
90
        // set property
91
        $this->setProperty(
92
            'address',
93
            'ADR' . (($type != '') ? ';' . $type : ''),
94
            $value
95
        );
96
97
        return $this;
98
    }
99
100
    /**
101
     * Add birthday
102
     *
103
     * @param  string $date Format is YYYY-MM-DD
104
     * @return $this
105
     */
106
    public function addBirthday($date)
107
    {
108
        $this->setProperty(
109
            'birthday',
110
            'BDAY',
111
            $date
112
        );
113
114
        return $this;
115
    }
116
117
    /**
118
     * Add company
119
     *
120
     * @param  string $company
121
     * @return $this
122
     */
123
    public function addCompany($company)
124
    {
125
        $this->setProperty(
126
            'company',
127
            'ORG',
128
            $company
129
        );
130
131
        // if filename is empty, add to filename
132
        if ($this->getFilename() === null) {
133
            $this->setFilename($company);
134
        }
135
136
        return $this;
137
    }
138
139
    /**
140
     * Add email
141
     *
142
     * @param  string            $address The e-mail address
143
     * @param  string [optional] $type    The type of the email address
144
     *                                    $type may be  PREF | WORK | HOME
145
     *                                    or any combination of these: e.g. "PREF;WORK"
146
     * @return $this
147
     */
148
    public function addEmail($address, $type = '')
149
    {
150
        $this->setProperty(
151
            'email',
152
            'EMAIL;INTERNET' . (($type != '') ? ';' . $type : ''),
153
            $address
154
        );
155
156
        return $this;
157
    }
158
159
    /**
160
     * Add jobtitle
161
     *
162
     * @param  string $jobtitle The jobtitle for the person.
163
     * @return $this
164
     */
165
    public function addJobtitle($jobtitle)
166
    {
167
        $this->setProperty(
168
            'jobtitle',
169
            'TITLE',
170
            $jobtitle
171
        );
172
173
        return $this;
174
    }
175
176
    /**
177
     * Add a label
178
     *
179
     * @param string $label
180
     * @param string $type
181
     *
182
     * @return $this
183
     */
184 View Code Duplication
    public function addLabel($label, $type = '')
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
185
    {
186
        $this->setProperty(
187
            'label',
188
            'LABEL' . ($type !== '' ? ';' . $type : ''),
189
            $label
190
        );
191
192
        return $this;
193
    }
194
195
    /**
196
     * Add a photo or logo (depending on property name)
197
     *
198
     * @param  string              $property LOGO|PHOTO
199
     * @param  string              $url      image url or filename
200
     * @param  bool                $include  Do we include the image in our vcard or not?
201
     * @throws VCardMediaException if file is empty or not an image file
202
     */
203
    private function addMedia($property, $url, $include = true, $element)
204
    {
205
        if ($include) {
206
            $value = file_get_contents($url);
207
208
            if (!$value) {
209
                throw new VCardMediaException('Nothing returned from URL.');
210
            }
211
212
            $value = base64_encode($value);
213
214
            $finfo = finfo_open(FILEINFO_MIME_TYPE);
215
            $mimetype = finfo_file($finfo, 'data://application/octet-stream;base64,' . $value);
216
            finfo_close($finfo);
217
218
            if (preg_match('/^image\//', $mimetype) !== 1) {
219
                throw new VCardMediaException('Returned data aren\'t an image.');
220
            }
221
222
            $type = strtoupper(str_replace('image/', '', $mimetype));
223
224
            $property .= ";ENCODING=b;TYPE=" . $type;
225
        } else {
226
            $value = $url;
227
        }
228
229
        $this->setProperty(
230
            $element,
231
            $property,
232
            $value
233
        );
234
    }
235
236
    /**
237
     * Add name
238
     *
239
     * @param  string [optional] $lastName
240
     * @param  string [optional] $firstName
241
     * @param  string [optional] $additional
242
     * @param  string [optional] $prefix
243
     * @param  string [optional] $suffix
244
     * @return $this
245
     */
246
    public function addName(
247
        $lastName = '',
248
        $firstName = '',
249
        $additional = '',
250
        $prefix = '',
251
        $suffix = ''
252
    ) {
253
        // define values with non-empty values
254
        $values = array_filter(array(
255
            $prefix,
256
            $firstName,
257
            $additional,
258
            $lastName,
259
            $suffix,
260
        ));
261
262
        // define filename
263
        $this->setFilename($values);
264
265
        // set property
266
        $property = $lastName . ';' . $firstName . ';' . $additional . ';' . $prefix . ';' . $suffix;
267
        $this->setProperty(
268
            'name',
269
            'N',
270
            $property
271
        );
272
273
        // is property FN set?
274
        if (!$this->hasProperty('FN')) {
275
            // set property
276
            $this->setProperty(
277
                'fullname',
278
                'FN',
279
                trim(implode(' ', $values))
280
            );
281
        }
282
283
        return $this;
284
    }
285
286
    /**
287
     * Add note
288
     *
289
     * @param  string $note
290
     * @return $this
291
     */
292
    public function addNote($note)
293
    {
294
        $this->setProperty(
295
            'note',
296
            'NOTE',
297
            $note
298
        );
299
300
        return $this;
301
    }
302
303
    /**
304
     * Add phone number
305
     *
306
     * @param  string            $number
307
     * @param  string [optional] $type
308
     *                                   Type may be PREF | WORK | HOME | VOICE | FAX | MSG |
309
     *                                   CELL | PAGER | BBS | CAR | MODEM | ISDN | VIDEO
310
     *                                   or any senseful combination, e.g. "PREF;WORK;VOICE"
311
     * @return $this
312
     */
313
    public function addPhoneNumber($number, $type = '')
314
    {
315
        $this->setProperty(
316
            'phoneNumber',
317
            'TEL' . (($type != '') ? ';' . $type : ''),
318
            $number
319
        );
320
321
        return $this;
322
    }
323
324
    /**
325
     * Add Photo
326
     *
327
     * @param  string $url     image url or filename
328
     * @param  bool   $include Include the image in our vcard?
329
     * @return $this
330
     */
331
    public function addPhoto($url, $include = true)
332
    {
333
        $this->addMedia(
334
            'PHOTO',
335
            $url,
336
            $include,
337
            'photo'
338
        );
339
340
        return $this;
341
    }
342
343
    /**
344
     * Add URL
345
     *
346
     * @param  string            $url
347
     * @param  string [optional] $type Type may be WORK | HOME
348
     * @return $this
349
     */
350 View Code Duplication
    public function addURL($url, $type = '')
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
351
    {
352
        $this->setProperty(
353
            'url',
354
            'URL' . (($type != '') ? ';' . $type : ''),
355
            $url
356
        );
357
358
        return $this;
359
    }
360
361
    /**
362
     * Build VCard (.vcf)
363
     *
364
     * @return string
365
     */
366
    public function buildVCard()
367
    {
368
        // init string
369
        $string = "BEGIN:VCARD\r\n";
370
        $string .= "VERSION:3.0\r\n";
371
        $string .= "REV:" . date("Y-m-d") . "T" . date("H:i:s") . "Z\r\n";
372
373
        // loop all properties
374
        $properties = $this->getProperties();
375
        foreach ($properties as $property) {
376
            // add to string
377
            $string .= $this->fold($property['key'] . ':' . $property['value'] . "\r\n");
378
        }
379
380
        // add to string
381
        $string .= "END:VCARD\r\n";
382
383
        // return
384
        return $string;
385
    }
386
387
    /**
388
     * Build VCalender (.ics) - Safari (< iOS 8) can not open .vcf files, so we have build a workaround.
389
     *
390
     * @return string
391
     */
392
    public function buildVCalendar()
393
    {
394
        // init dates
395
        $dtstart = date("Ymd") . "T" . date("Hi") . "00";
396
        $dtend = date("Ymd") . "T" . date("Hi") . "01";
397
398
        // init string
399
        $string = "BEGIN:VCALENDAR\n";
400
        $string .= "VERSION:2.0\n";
401
        $string .= "BEGIN:VEVENT\n";
402
        $string .= "DTSTART;TZID=Europe/London:" . $dtstart . "\n";
403
        $string .= "DTEND;TZID=Europe/London:" . $dtend . "\n";
404
        $string .= "SUMMARY:Click attached contact below to save to your contacts\n";
405
        $string .= "DTSTAMP:" . $dtstart . "Z\n";
406
        $string .= "ATTACH;VALUE=BINARY;ENCODING=BASE64;FMTTYPE=text/directory;\n";
407
        $string .= " X-APPLE-FILENAME=" . $this->getFilename() . "." . $this->getFileExtension() . ":\n";
408
409
        // base64 encode it so that it can be used as an attachemnt to the "dummy" calendar appointment
410
        $b64vcard = base64_encode($this->buildVCard());
411
412
        // chunk the single long line of b64 text in accordance with RFC2045
413
        // (and the exact line length determined from the original .ics file exported from Apple calendar
414
        $b64mline = chunk_split($b64vcard, 74, "\n");
415
416
        // need to indent all the lines by 1 space for the iphone (yes really?!!)
417
        $b64final = preg_replace('/(.+)/', ' $1', $b64mline);
418
        $string .= $b64final;
419
420
        // output the correctly formatted encoded text
421
        $string .= "END:VEVENT\n";
422
        $string .= "END:VCALENDAR\n";
423
424
        // return
425
        return $string;
426
    }
427
428
    /**
429
     * Returns the browser user agent string.
430
     *
431
     * @return string
432
     */
433
    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...
434
    {
435
        if (array_key_exists('HTTP_USER_AGENT', $_SERVER)) {
436
            $browser = strtolower($_SERVER['HTTP_USER_AGENT']);
437
        } else {
438
            $browser = 'unknown';
439
        }
440
441
        return $browser;
442
    }
443
444
    /**
445
     * Decode
446
     *
447
     * @param  string $value The value to decode
448
     * @return string decoded
449
     */
450
    private function decode($value)
451
    {
452
        // convert cyrlic, greek or other caracters to ASCII characters
453
        return Transliterator::transliterate($value);
454
    }
455
456
    /**
457
     * Download a vcard or vcal file to the browser.
458
     */
459
    public function download()
460
    {
461
        // define output
462
        $output = $this->getOutput();
463
464
        foreach ($this->getHeaders(false) as $header) {
465
            header($header);
466
        }
467
468
        // echo the output and it will be a download
469
        echo $output;
470
    }
471
472
    /**
473
     * Fold a line according to RFC2425 section 5.8.1.
474
     *
475
     * @link http://tools.ietf.org/html/rfc2425#section-5.8.1
476
     * @param  string $text
477
     * @return mixed
478
     */
479
    protected function fold($text)
480
    {
481
        if (strlen($text) <= 75) {
482
            return $text;
483
        }
484
485
        // split, wrap and trim trailing separator
486
        return substr(chunk_split($text, 73, "\r\n "), 0, -3);
487
    }
488
489
    /**
490
     * Get output as string
491
     * @deprecated in the future
492
     *
493
     * @return string
494
     */
495
    public function get()
496
    {
497
        return $this->getOutput();
498
    }
499
500
    /**
501
     * Get charset
502
     *
503
     * @return string
504
     */
505
    public function getCharset()
506
    {
507
        return $this->charset;
508
    }
509
510
    /**
511
     * Get content type
512
     *
513
     * @return string
514
     */
515
    public function getContentType()
516
    {
517
        return ($this->isIOS7()) ?
518
            'text/x-vcalendar' : 'text/x-vcard';
519
    }
520
521
    /**
522
     * Get filename
523
     *
524
     * @return string
525
     */
526
    public function getFilename()
527
    {
528
        return $this->filename;
529
    }
530
531
    /**
532
     * Get file extension
533
     *
534
     * @return string
535
     */
536
    public function getFileExtension()
537
    {
538
        return ($this->isIOS7()) ?
539
            'ics' : 'vcf';
540
    }
541
542
    /**
543
     * Get headers
544
     *
545
     * @param  bool  $asAssociative
546
     * @return array
547
     */
548
    public function getHeaders($asAssociative)
549
    {
550
        $contentType        = $this->getContentType() . '; charset=' . $this->getCharset();
551
        $contentDisposition = 'attachment; filename=' . $this->getFilename() . '.' . $this->getFileExtension();
552
        $contentLength      = strlen($this->getOutput());
553
        $connection         = 'close';
554
555
        if ((bool) $asAssociative) {
556
            return array(
557
                'Content-type'        => $contentType,
558
                'Content-Disposition' => $contentDisposition,
559
                'Content-Length'      => $contentLength,
560
                'Connection'          => $connection,
561
            );
562
        }
563
564
        return array(
565
            'Content-type: ' . $contentType,
566
            'Content-Disposition: ' . $contentDisposition,
567
            'Content-Length: ' . $contentLength,
568
            'Connection: ' . $connection,
569
        );
570
    }
571
572
    /**
573
     * Get output as string
574
     * iOS devices (and safari < iOS 8 in particular) can not read .vcf (= vcard) files.
575
     * So I build a workaround to build a .ics (= vcalender) file.
576
     *
577
     * @return string
578
     */
579
    public function getOutput()
580
    {
581
        $output = ($this->isIOS7()) ?
582
            $this->buildVCalendar() : $this->buildVCard();
583
584
        // we need to decode the output for outlook
585
        if ($this->getCharset() == 'utf-8') {
586
            $output = utf8_decode($output);
587
        }
588
589
        return $output;
590
    }
591
592
    /**
593
     * Get properties
594
     *
595
     * @return array
596
     */
597
    public function getProperties()
598
    {
599
        return $this->properties;
600
    }
601
602
    /**
603
     * Has property
604
     *
605
     * @param  string $key
606
     * @return bool
607
     */
608
    public function hasProperty($key)
609
    {
610
        $properties = $this->getProperties();
611
612
        foreach ($properties as $property) {
613
            if ($property['key'] === $key && $property['value'] !== '') {
614
                return true;
615
            }
616
        }
617
618
        return false;
619
    }
620
621
    /**
622
     * Is iOS - Check if the user is using an iOS-device
623
     *
624
     * @return bool
625
     */
626
    public function isIOS()
627
    {
628
        // get user agent
629
        $browser = $this->getUserAgent();
630
631
        return (strpos($browser, 'iphone') || strpos($browser, 'ipod') || strpos($browser, 'ipad'));
632
    }
633
634
    /**
635
     * Is iOS less than 7 (should cal wrapper be returned)
636
     *
637
     * @return bool
638
     */
639
    public function isIOS7()
640
    {
641
        return ($this->isIOS() && $this->shouldAttachmentBeCal());
642
    }
643
644
    /**
645
     * Save to a file
646
     *
647
     * @return void
648
     */
649
    public function save()
650
    {
651
        $file = $this->getFilename() . '.' . $this->getFileExtension();
652
653
        file_put_contents(
654
            $file,
655
            $this->getOutput()
656
        );
657
    }
658
659
    /**
660
     * Set charset
661
     *
662
     * @param  mixed  $charset
663
     * @return void
664
     */
665
    public function setCharset($charset)
666
    {
667
        $this->charset = $charset;
668
    }
669
670
    /**
671
     * Set filename
672
     *
673
     * @param  mixed  $value
674
     * @param  bool   $overwrite [optional] Default overwrite is true
675
     * @param  string $separator [optional] Default separator is an underscore '_'
676
     * @return void
677
     */
678
    public function setFilename($value, $overwrite = true, $separator = '_')
679
    {
680
        // recast to string if $value is array
681
        if (is_array($value)) {
682
            $value = implode($separator, $value);
683
        }
684
685
        // trim unneeded values
686
        $value = trim($value, $separator);
687
688
        // remove all spaces
689
        $value = preg_replace('/\s+/', $separator, $value);
690
691
        // if value is empty, stop here
692
        if (empty($value)) {
693
            return;
694
        }
695
696
        // decode value + lowercase the string
697
        $value = strtolower($this->decode($value));
698
699
        // urlize this part
700
        $value = Transliterator::urlize($value);
701
702
        // overwrite filename or add to filename using a prefix in between
703
        $this->filename = ($overwrite) ?
704
            $value : $this->filename . $separator . $value;
705
    }
706
707
    /**
708
     * Set property
709
     *
710
     * @param  string $element The element name you want to set, f.e.: name, email, phoneNumber, ...
711
     * @param  string $key
712
     * @param  string $value
713
     * @return void
714
     */
715
    private function setProperty($element, $key, $value)
716
    {
717
        if (!in_array($element, $this->multiplePropertiesForElementAllowed)
718
            && isset($this->definedElements[$element])
719
        ) {
720
            throw new Exception('You can only set "' . $element . '" once.');
721
        }
722
723
        // we define that we set this element
724
        $this->definedElements[$element] = true;
725
726
        // adding property
727
        $this->properties[] = array(
728
            'key' => $key,
729
            'value' => $value
730
        );
731
    }
732
733
    /**
734
     * Checks if we should return vcard in cal wrapper
735
     *
736
     * @return bool
737
     */
738
    protected function shouldAttachmentBeCal()
739
    {
740
        $browser = $this->getUserAgent();
741
742
        $matches = array();
743
        preg_match('/os (\d+)_(\d+)\s+/', $browser, $matches);
744
        $version = isset($matches[1]) ? ((int) $matches[1]) : 999;
745
746
        return ($version < 8);
747
    }
748
}
749