Completed
Push — master ( f7ae30...f3ee5a )
by Jeroen
03:23
created

VCard::addAddress()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 22
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 0 Features 1
Metric Value
c 4
b 0
f 1
dl 0
loc 22
rs 9.2
cc 2
eloc 15
nc 1
nop 8

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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