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

VCard::addAddress()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 22
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

// Better
class Router
{
    private $host;

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

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

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

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