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

VCard::setSavePath()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 13
rs 9.4285
cc 3
eloc 6
nc 3
nop 1
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