Completed
Push — master ( b972ae...f7ae30 )
by Jeroen
12s
created

VCard::addAddress()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 22
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

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

How to fix   Many Parameters   

Many Parameters

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

There are several approaches to avoid long parameter lists:

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