Completed
Push — master ( 0bef36...c10cae )
by Jeroen
02:11
created

VCard::addAddress()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 22
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
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
     * Save Path
37
     *
38
     * @var string
39
     */
40
    private $savePath = null;
41
42
    /**
43
     * Multiple properties for element allowed
44
     *
45
     * @var array
46
     */
47
    private $multiplePropertiesForElementAllowed = array(
48
        'email',
49
        'address',
50
        'phoneNumber',
51
        'url'
52
    );
53
54
    /**
55
     * Properties
56
     *
57
     * @var array
58
     */
59
    private $properties;
60
61
    /**
62
     * Default Charset
63
     *
64
     * @var string
65
     */
66
    public $charset = 'utf-8';
67
68
    /**
69
     * Add address
70
     *
71
     * @param  string [optional] $name
72
     * @param  string [optional] $extended
73
     * @param  string [optional] $street
74
     * @param  string [optional] $city
75
     * @param  string [optional] $region
76
     * @param  string [optional] $zip
77
     * @param  string [optional] $country
78
     * @param  string [optional] $type
79
     *                                     $type may be DOM | INTL | POSTAL | PARCEL | HOME | WORK
80
     *                                     or any combination of these: e.g. "WORK;PARCEL;POSTAL"
81
     * @return $this
82
     */
83
    public function addAddress(
84
        $name = '',
85
        $extended = '',
86
        $street = '',
87
        $city = '',
88
        $region = '',
89
        $zip = '',
90
        $country = '',
91
        $type = 'WORK;POSTAL'
92
    ) {
93
        // init value
94
        $value = $name . ';' . $extended . ';' . $street . ';' . $city . ';' . $region . ';' . $zip . ';' . $country;
95
96
        // set property
97
        $this->setProperty(
98
            'address',
99
            'ADR' . (($type != '') ? ';' . $type : '') . $this->getCharsetString(),
100
            $value
101
        );
102
103
        return $this;
104
    }
105
106
    /**
107
     * Add birthday
108
     *
109
     * @param  string $date Format is YYYY-MM-DD
110
     * @return $this
111
     */
112
    public function addBirthday($date)
113
    {
114
        $this->setProperty(
115
            'birthday',
116
            'BDAY',
117
            $date
118
        );
119
120
        return $this;
121
    }
122
123
    /**
124
     * Add company
125
     *
126
     * @param  string $company
127
     * @return $this
128
     */
129
    public function addCompany($company, $department = '')
130
    {
131
        $this->setProperty(
132
            'company',
133
            'ORG' . $this->getCharsetString(),
134
            $company
135
            . ($department != '' ? ';' . $department : '')
136
        );
137
138
        // if filename is empty, add to filename
139
        if ($this->filename === null) {
140
            $this->setFilename($company);
141
        }
142
143
        return $this;
144
    }
145
146
    /**
147
     * Add email
148
     *
149
     * @param  string $address The e-mail address
150
     * @param  string [optional] $type    The type of the email address
151
     *                                    $type may be  PREF | WORK | HOME
152
     *                                    or any combination of these: e.g. "PREF;WORK"
153
     * @return $this
154
     */
155
    public function addEmail($address, $type = '')
156
    {
157
        $this->setProperty(
158
            'email',
159
            'EMAIL;INTERNET' . (($type != '') ? ';' . $type : ''),
160
            $address
161
        );
162
163
        return $this;
164
    }
165
166
    /**
167
     * Add jobtitle
168
     *
169
     * @param  string $jobtitle The jobtitle for the person.
170
     * @return $this
171
     */
172
    public function addJobtitle($jobtitle)
173
    {
174
        $this->setProperty(
175
            'jobtitle',
176
            'TITLE' . $this->getCharsetString(),
177
            $jobtitle
178
        );
179
180
        return $this;
181
    }
182
183
    /**
184
     * Add role
185
     *
186
     * @param  string $role The role for the person.
187
     * @return $this
188
     */
189
    public function addRole($role)
190
    {
191
        $this->setProperty(
192
            'role',
193
            'ROLE' . $this->getCharsetString(),
194
            $role
195
        );
196
197
        return $this;
198
    }
199
200
    /**
201
     * Add a photo or logo (depending on property name)
202
     *
203
     * @param  string $property LOGO|PHOTO
204
     * @param  string $url image url or filename
205
     * @param  bool $include Do we include the image in our vcard or not?
206
     * @throws VCardMediaException if file is empty or not an image file
207
     */
208
    private function addMedia($property, $url, $include = true, $element)
209
    {
210
        if ($include) {
211
            $value = file_get_contents($url);
212
213
            if (!$value) {
214
                throw new VCardMediaException('Nothing returned from URL.');
215
            }
216
217
            $value = base64_encode($value);
218
219
            $finfo = finfo_open(FILEINFO_MIME_TYPE);
220
            $mimetype = finfo_file($finfo, 'data://application/octet-stream;base64,' . $value);
221
            finfo_close($finfo);
222
223
            if (preg_match('/^image\//', $mimetype) !== 1) {
224
                throw new VCardMediaException('Returned data aren\'t an image.');
225
            }
226
227
            $type = strtoupper(str_replace('image/', '', $mimetype));
228
229
            $property .= ";ENCODING=b;TYPE=" . $type;
230
        } else {
231
            if (filter_var($url, FILTER_VALIDATE_URL) !== false) {
232
                $propertySuffix = ';VALUE=URL';
233
234
                $headers = get_headers($url);
235
236
                $imageTypeMatched = false;
237
                $fileType = null;
238
239
                foreach ($headers as $header) {
240
                    if (preg_match('/Content-Type:\simage\/([a-z]+)/i', $header, $m)) {
241
                        $fileType = $m[1];
242
                        $imageTypeMatched = true;
243
                    }
244
                }
245
246
                if (!$imageTypeMatched) {
247
                    throw new VCardMediaException('Returned data isn\'t an image.');
248
                }
249
250
                $propertySuffix .= ';TYPE=' . strtoupper($fileType);
251
252
                $property = $property . $propertySuffix;
253
                $value = $url;
254
            } else {
255
                $value = $url;
256
            }
257
        }
258
259
        $this->setProperty(
260
            $element,
261
            $property,
262
            $value
263
        );
264
    }
265
266
    /**
267
     * Add name
268
     *
269
     * @param  string [optional] $lastName
270
     * @param  string [optional] $firstName
271
     * @param  string [optional] $additional
272
     * @param  string [optional] $prefix
273
     * @param  string [optional] $suffix
274
     * @return $this
275
     */
276
    public function addName(
277
        $lastName = '',
278
        $firstName = '',
279
        $additional = '',
280
        $prefix = '',
281
        $suffix = ''
282
    ) {
283
        // define values with non-empty values
284
        $values = array_filter(array(
285
            $prefix,
286
            $firstName,
287
            $additional,
288
            $lastName,
289
            $suffix,
290
        ));
291
292
        // define filename
293
        $this->setFilename($values);
294
295
        // set property
296
        $property = $lastName . ';' . $firstName . ';' . $additional . ';' . $prefix . ';' . $suffix;
297
        $this->setProperty(
298
            'name',
299
            'N' . $this->getCharsetString(),
300
            $property
301
        );
302
303
        // is property FN set?
304
        if (!$this->hasProperty('FN')) {
305
            // set property
306
            $this->setProperty(
307
                'fullname',
308
                'FN' . $this->getCharsetString(),
309
                trim(implode(' ', $values))
310
            );
311
        }
312
313
        return $this;
314
    }
315
316
    /**
317
     * Add note
318
     *
319
     * @param  string $note
320
     * @return $this
321
     */
322
    public function addNote($note)
323
    {
324
        $this->setProperty(
325
            'note',
326
            'NOTE' . $this->getCharsetString(),
327
            $note
328
        );
329
330
        return $this;
331
    }
332
333
    /**
334
     * Add categories
335
     *
336
     * @param array $categories
337
     * @return $this
338
     */
339
    public function addCategories($categories)
340
    {
341
        $this->setProperty(
342
            'categories',
343
            'CATEGORIES' . $this->getCharsetString(),
344
            trim(implode(',', $categories))
345
        );
346
347
        return $this;
348
    }
349
350
    /**
351
     * Add phone number
352
     *
353
     * @param  string $number
354
     * @param  string [optional] $type
355
     *                                   Type may be PREF | WORK | HOME | VOICE | FAX | MSG |
356
     *                                   CELL | PAGER | BBS | CAR | MODEM | ISDN | VIDEO
357
     *                                   or any senseful combination, e.g. "PREF;WORK;VOICE"
358
     * @return $this
359
     */
360
    public function addPhoneNumber($number, $type = '')
361
    {
362
        $this->setProperty(
363
            'phoneNumber',
364
            'TEL' . (($type != '') ? ';' . $type : ''),
365
            $number
366
        );
367
368
        return $this;
369
    }
370
371
    /**
372
     * Add Logo
373
     *
374
     * @param  string $url image url or filename
375
     * @param  bool $include Include the image in our vcard?
376
     * @return $this
377
     */
378
    public function addLogo($url, $include = true)
379
    {
380
        $this->addMedia(
381
            'LOGO',
382
            $url,
383
            $include,
384
            'logo'
385
        );
386
387
        return $this;
388
    }
389
390
    /**
391
     * Add Photo
392
     *
393
     * @param  string $url image url or filename
394
     * @param  bool $include Include the image in our vcard?
395
     * @return $this
396
     */
397
    public function addPhoto($url, $include = true)
398
    {
399
        $this->addMedia(
400
            'PHOTO',
401
            $url,
402
            $include,
403
            'photo'
404
        );
405
406
        return $this;
407
    }
408
409
    /**
410
     * Add URL
411
     *
412
     * @param  string $url
413
     * @param  string [optional] $type Type may be WORK | HOME
414
     * @return $this
415
     */
416
    public function addURL($url, $type = '')
417
    {
418
        $this->setProperty(
419
            'url',
420
            'URL' . (($type != '') ? ';' . $type : ''),
421
            $url
422
        );
423
424
        return $this;
425
    }
426
427
    /**
428
     * Build VCard (.vcf)
429
     *
430
     * @return string
431
     */
432
    public function buildVCard()
433
    {
434
        // init string
435
        $string = "BEGIN:VCARD\r\n";
436
        $string .= "VERSION:3.0\r\n";
437
        $string .= "REV:" . date("Y-m-d") . "T" . date("H:i:s") . "Z\r\n";
438
439
        // loop all properties
440
        $properties = $this->getProperties();
441
        foreach ($properties as $property) {
442
            // add to string
443
            $string .= $this->fold($property['key'] . ':' . $this->escape($property['value']) . "\r\n");
444
        }
445
446
        // add to string
447
        $string .= "END:VCARD\r\n";
448
449
        // return
450
        return $string;
451
    }
452
453
    /**
454
     * Build VCalender (.ics) - Safari (< iOS 8) can not open .vcf files, so we have build a workaround.
455
     *
456
     * @return string
457
     */
458
    public function buildVCalendar()
459
    {
460
        // init dates
461
        $dtstart = date("Ymd") . "T" . date("Hi") . "00";
462
        $dtend = date("Ymd") . "T" . date("Hi") . "01";
463
464
        // init string
465
        $string = "BEGIN:VCALENDAR\n";
466
        $string .= "VERSION:2.0\n";
467
        $string .= "BEGIN:VEVENT\n";
468
        $string .= "DTSTART;TZID=Europe/London:" . $dtstart . "\n";
469
        $string .= "DTEND;TZID=Europe/London:" . $dtend . "\n";
470
        $string .= "SUMMARY:Click attached contact below to save to your contacts\n";
471
        $string .= "DTSTAMP:" . $dtstart . "Z\n";
472
        $string .= "ATTACH;VALUE=BINARY;ENCODING=BASE64;FMTTYPE=text/directory;\n";
473
        $string .= " X-APPLE-FILENAME=" . $this->getFilename() . "." . $this->getFileExtension() . ":\n";
474
475
        // base64 encode it so that it can be used as an attachemnt to the "dummy" calendar appointment
476
        $b64vcard = base64_encode($this->buildVCard());
477
478
        // chunk the single long line of b64 text in accordance with RFC2045
479
        // (and the exact line length determined from the original .ics file exported from Apple calendar
480
        $b64mline = chunk_split($b64vcard, 74, "\n");
481
482
        // need to indent all the lines by 1 space for the iphone (yes really?!!)
483
        $b64final = preg_replace('/(.+)/', ' $1', $b64mline);
484
        $string .= $b64final;
485
486
        // output the correctly formatted encoded text
487
        $string .= "END:VEVENT\n";
488
        $string .= "END:VCALENDAR\n";
489
490
        // return
491
        return $string;
492
    }
493
494
    /**
495
     * Returns the browser user agent string.
496
     *
497
     * @return string
498
     */
499
    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...
500
    {
501
        if (array_key_exists('HTTP_USER_AGENT', $_SERVER)) {
502
            $browser = strtolower($_SERVER['HTTP_USER_AGENT']);
503
        } else {
504
            $browser = 'unknown';
505
        }
506
507
        return $browser;
508
    }
509
510
    /**
511
     * Decode
512
     *
513
     * @param  string $value The value to decode
514
     * @return string decoded
515
     */
516
    private function decode($value)
517
    {
518
        // convert cyrlic, greek or other caracters to ASCII characters
519
        return Transliterator::transliterate($value);
520
    }
521
522
    /**
523
     * Download a vcard or vcal file to the browser.
524
     */
525
    public function download()
526
    {
527
        // define output
528
        $output = $this->getOutput();
529
530
        foreach ($this->getHeaders(false) as $header) {
531
            header($header);
532
        }
533
534
        // echo the output and it will be a download
535
        echo $output;
536
    }
537
538
    /**
539
     * Fold a line according to RFC2425 section 5.8.1.
540
     *
541
     * @link http://tools.ietf.org/html/rfc2425#section-5.8.1
542
     * @param  string $text
543
     * @return mixed
544
     */
545
    protected function fold($text)
546
    {
547
        if (strlen($text) <= 75) {
548
            return $text;
549
        }
550
551
        // split, wrap and trim trailing separator
552
        return substr(chunk_split($text, 73, "\r\n "), 0, -3);
553
    }
554
555
    /**
556
     * Escape newline characters according to RFC2425 section 5.8.4.
557
     *
558
     * @link http://tools.ietf.org/html/rfc2425#section-5.8.4
559
     * @param  string $text
560
     * @return string
561
     */
562
    protected function escape($text)
563
    {
564
        $text = str_replace("\r\n", "\\n", $text);
565
        $text = str_replace("\n", "\\n", $text);
566
567
        return $text;
568
    }
569
570
    /**
571
     * Get output as string
572
     * @deprecated in the future
573
     *
574
     * @return string
575
     */
576
    public function get()
577
    {
578
        return $this->getOutput();
579
    }
580
581
    /**
582
     * Get charset
583
     *
584
     * @return string
585
     */
586
    public function getCharset()
587
    {
588
        return $this->charset;
589
    }
590
591
    /**
592
     * Get charset string
593
     *
594
     * @return string
595
     */
596
    public function getCharsetString()
597
    {
598
        $charsetString = '';
599
        if ($this->charset == 'utf-8') {
600
            $charsetString = ';CHARSET=' . $this->charset;
601
        }
602
603
        return $charsetString;
604
    }
605
606
    /**
607
     * Get content type
608
     *
609
     * @return string
610
     */
611
    public function getContentType()
612
    {
613
        return ($this->isIOS7()) ?
614
            'text/x-vcalendar' : 'text/x-vcard';
615
    }
616
617
    /**
618
     * Get filename
619
     *
620
     * @return string
621
     */
622
    public function getFilename()
623
    {
624
        if (!$this->filename) {
625
            return 'unknown';
626
        }
627
628
        return $this->filename;
629
    }
630
631
    /**
632
     * Get file extension
633
     *
634
     * @return string
635
     */
636
    public function getFileExtension()
637
    {
638
        return ($this->isIOS7()) ?
639
            'ics' : 'vcf';
640
    }
641
642
    /**
643
     * Get headers
644
     *
645
     * @param  bool $asAssociative
646
     * @return array
647
     */
648
    public function getHeaders($asAssociative)
649
    {
650
        $contentType = $this->getContentType() . '; charset=' . $this->getCharset();
651
        $contentDisposition = 'attachment; filename=' . $this->getFilename() . '.' . $this->getFileExtension();
652
        $contentLength = mb_strlen($this->getOutput(), $this->getCharset());
653
        $connection = 'close';
654
655
        if ((bool)$asAssociative) {
656
            return array(
657
                'Content-type' => $contentType,
658
                'Content-Disposition' => $contentDisposition,
659
                'Content-Length' => $contentLength,
660
                'Connection' => $connection,
661
            );
662
        }
663
664
        return array(
665
            'Content-type: ' . $contentType,
666
            'Content-Disposition: ' . $contentDisposition,
667
            'Content-Length: ' . $contentLength,
668
            'Connection: ' . $connection,
669
        );
670
    }
671
672
    /**
673
     * Get output as string
674
     * iOS devices (and safari < iOS 8 in particular) can not read .vcf (= vcard) files.
675
     * So I build a workaround to build a .ics (= vcalender) file.
676
     *
677
     * @return string
678
     */
679
    public function getOutput()
680
    {
681
        $output = ($this->isIOS7()) ?
682
            $this->buildVCalendar() : $this->buildVCard();
683
684
        return $output;
685
    }
686
687
    /**
688
     * Get properties
689
     *
690
     * @return array
691
     */
692
    public function getProperties()
693
    {
694
        return $this->properties;
695
    }
696
697
    /**
698
     * Has property
699
     *
700
     * @param  string $key
701
     * @return bool
702
     */
703
    public function hasProperty($key)
704
    {
705
        $properties = $this->getProperties();
706
707
        foreach ($properties as $property) {
708
            if ($property['key'] === $key && $property['value'] !== '') {
709
                return true;
710
            }
711
        }
712
713
        return false;
714
    }
715
716
    /**
717
     * Is iOS - Check if the user is using an iOS-device
718
     *
719
     * @return bool
720
     */
721
    public function isIOS()
722
    {
723
        // get user agent
724
        $browser = $this->getUserAgent();
725
726
        return (strpos($browser, 'iphone') || strpos($browser, 'ipod') || strpos($browser, 'ipad'));
727
    }
728
729
    /**
730
     * Is iOS less than 7 (should cal wrapper be returned)
731
     *
732
     * @return bool
733
     */
734
    public function isIOS7()
735
    {
736
        return ($this->isIOS() && $this->shouldAttachmentBeCal());
737
    }
738
739
    /**
740
     * Save to a file
741
     *
742
     * @return void
743
     */
744
    public function save()
745
    {
746
        $file = $this->getFilename() . '.' . $this->getFileExtension();
747
748
        // Add save path if given
749
        if (null !== $this->savePath) {
750
            $file = $this->savePath . $file;
751
        }
752
753
        file_put_contents(
754
            $file,
755
            $this->getOutput()
756
        );
757
    }
758
759
    /**
760
     * Set charset
761
     *
762
     * @param  mixed $charset
763
     * @return void
764
     */
765
    public function setCharset($charset)
766
    {
767
        $this->charset = $charset;
768
    }
769
770
    /**
771
     * Set filename
772
     *
773
     * @param  mixed $value
774
     * @param  bool $overwrite [optional] Default overwrite is true
775
     * @param  string $separator [optional] Default separator is an underscore '_'
776
     * @return void
777
     */
778
    public function setFilename($value, $overwrite = true, $separator = '_')
779
    {
780
        // recast to string if $value is array
781
        if (is_array($value)) {
782
            $value = implode($separator, $value);
783
        }
784
785
        // trim unneeded values
786
        $value = trim($value, $separator);
787
788
        // remove all spaces
789
        $value = preg_replace('/\s+/', $separator, $value);
790
791
        // if value is empty, stop here
792
        if (empty($value)) {
793
            return;
794
        }
795
796
        // decode value + lowercase the string
797
        $value = strtolower($this->decode($value));
798
799
        // urlize this part
800
        $value = Transliterator::urlize($value);
801
802
        // overwrite filename or add to filename using a prefix in between
803
        $this->filename = ($overwrite) ?
804
            $value : $this->filename . $separator . $value;
805
    }
806
807
    /**
808
     * Set the save path directory
809
     *
810
     * @param  string $savePath Save Path
811
     * @throws Exception
812
     */
813
    public function setSavePath($savePath)
814
    {
815
        if (!is_dir($savePath)) {
816
            throw new Exception('Output directory does not exist.');
817
        }
818
819
        // Add trailing directory separator the save path
820
        if (substr($savePath, -1) != DIRECTORY_SEPARATOR) {
821
            $savePath .= DIRECTORY_SEPARATOR;
822
        }
823
824
        $this->savePath = $savePath;
825
    }
826
827
    /**
828
     * Set property
829
     *
830
     * @param  string $element The element name you want to set, f.e.: name, email, phoneNumber, ...
831
     * @param  string $key
832
     * @param  string $value
833
     * @throws Exception
834
     */
835
    private function setProperty($element, $key, $value)
836
    {
837
        if (!in_array($element, $this->multiplePropertiesForElementAllowed)
838
            && isset($this->definedElements[$element])
839
        ) {
840
            throw new Exception('You can only set "' . $element . '" once.');
841
        }
842
843
        // we define that we set this element
844
        $this->definedElements[$element] = true;
845
846
        // adding property
847
        $this->properties[] = array(
848
            'key' => $key,
849
            'value' => $value
850
        );
851
    }
852
853
    /**
854
     * Checks if we should return vcard in cal wrapper
855
     *
856
     * @return bool
857
     */
858
    protected function shouldAttachmentBeCal()
859
    {
860
        $browser = $this->getUserAgent();
861
862
        $matches = array();
863
        preg_match('/os (\d+)_(\d+)\s+/', $browser, $matches);
864
        $version = isset($matches[1]) ? ((int)$matches[1]) : 999;
865
866
        return ($version < 8);
867
    }
868
}
869