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