Completed
Pull Request — master (#80)
by Rouven Alexander
03:54
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)
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