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