Completed
Pull Request — master (#105)
by Tom
02:40
created

VCardBuilder   A

Complexity

Total Complexity 35

Size/Duplication

Total Lines 360
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 5

Importance

Changes 0
Metric Value
wmc 35
lcom 1
cbo 5
dl 0
loc 360
rs 9
c 0
b 0
f 0

17 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 9 1
A buildVCard() 0 20 2
B buildVCalendar() 0 36 1
A download() 0 12 2
A getCharset() 0 4 1
A getCharsetString() 0 10 2
B setFileName() 0 33 5
A getFileName() 0 8 2
A getContentType() 0 5 2
A getFileExtension() 0 5 2
A getFullFileName() 0 4 1
A getHeaders() 0 23 2
A getOutput() 0 7 2
A getProperties() 0 4 1
A hasProperty() 0 12 4
A save() 0 16 2
A checkSavePath() 0 13 3
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
use JeroenDesloovere\VCard\Exception\ElementAlreadyExistsException;
14
use JeroenDesloovere\VCard\Exception\OutputDirectoryNotExistsException;
15
use JeroenDesloovere\VCard\Model\VCard;
16
use JeroenDesloovere\VCard\Service\PropertyService;
17
use JeroenDesloovere\VCard\Util\GeneralUtil;
18
use JeroenDesloovere\VCard\Util\UserAgentUtil;
19
20
/**
21
 * VCard PHP Class to generate .vcard files and save them to a file or output as a download.
22
 */
23
class VCardBuilder
24
{
25
    /**
26
     * FileName
27
     *
28
     * @var string|null
29
     */
30
    private $fileName;
31
32
    /**
33
     * Properties
34
     *
35
     * @var array
36
     */
37
    private $properties;
38
39
    /**
40
     * Default Charset
41
     *
42
     * @var string
43
     */
44
    private $charset;
45
46
    /**
47
     * VCardBuilder constructor.
48
     *
49
     * @param VCard|VCard[] $vCard
50
     * @param string        $charset
51
     *
52
     * @throws ElementAlreadyExistsException
53
     */
54
    public function __construct($vCard, $charset = 'utf-8')
55
    {
56
        $this->charset = $charset;
57
58
        $propertyUtil = new PropertyService($vCard, $charset);
59
60
        $this->fileName = $propertyUtil->getFileName();
61
        $this->properties = $propertyUtil->getProperties();
62
    }
63
64
    /**
65
     * Build VCard (.vcf)
66
     *
67
     * @return string
68
     */
69
    public function buildVCard(): string
70
    {
71
        // init string
72
        $string = "BEGIN:VCARD\r\n";
73
        $string .= "VERSION:3.0\r\n";
74
        $string .= 'REV:'.date('Y-m-d').'T'.date('H:i:s')."Z\r\n";
75
76
        // loop all properties
77
        $properties = $this->getProperties();
78
        foreach ($properties as $property) {
79
            // add to string
80
            $string .= GeneralUtil::fold($property['key'].':'.GeneralUtil::escape($property['value'])."\r\n");
81
        }
82
83
        // add to string
84
        $string .= "END:VCARD\r\n";
85
86
        // return
87
        return $string;
88
    }
89
90
    /**
91
     * Build VCalender (.ics) - Safari (< iOS 8) can not open .vcf files, so we have build a workaround.
92
     *
93
     * @return string
94
     */
95
    public function buildVCalendar(): string
96
    {
97
        // init dates
98
        $dtbase = date('Ymd').'T'.date('Hi');
99
        $dtstart = $dtbase.'00';
100
        $dtend = $dtbase.'01';
101
102
        // init string
103
        $string = "BEGIN:VCALENDAR\n";
104
        $string .= "VERSION:2.0\n";
105
        $string .= "BEGIN:VEVENT\n";
106
        $string .= 'DTSTART;TZID=Europe/London:'.$dtstart."\n";
107
        $string .= 'DTEND;TZID=Europe/London:'.$dtend."\n";
108
        $string .= "SUMMARY:Click attached contact below to save to your contacts\n";
109
        $string .= 'DTSTAMP:'.$dtstart."Z\n";
110
        $string .= "ATTACH;VALUE=BINARY;ENCODING=BASE64;FMTTYPE=text/directory;\n";
111
        $string .= ' X-APPLE-FILENAME='.$this->getFullFileName().":\n";
112
113
        // base64 encode it so that it can be used as an attachment to the "dummy" calendar appointment
114
        $b64vcard = base64_encode($this->buildVCard());
115
116
        // chunk the single long line of b64 text in accordance with RFC2045
117
        // (and the exact line length determined from the original .ics file exported from Apple calendar
118
        $b64mline = chunk_split($b64vcard, 74, "\n");
119
120
        // need to indent all the lines by 1 space for the iphone (yes really?!!)
121
        $b64final = preg_replace('/(.+)/', ' $1', $b64mline);
122
        $string .= $b64final;
123
124
        // output the correctly formatted encoded text
125
        $string .= "END:VEVENT\n";
126
        $string .= "END:VCALENDAR\n";
127
128
        // return
129
        return $string;
130
    }
131
132
    /**
133
     * Download a vcard or vcal file to the browser.
134
     */
135
    public function download(): void
136
    {
137
        // define output
138
        $output = $this->getOutput();
139
140
        foreach ($this->getHeaders(false) as $header) {
141
            header($header);
142
        }
143
144
        // echo the output and it will be a download
145
        echo $output;
146
    }
147
148
    /**
149
     * Get charset
150
     *
151
     * @return string
152
     */
153
    public function getCharset(): string
154
    {
155
        return $this->charset;
156
    }
157
158
    /**
159
     * Get charset string
160
     *
161
     * @return string
162
     */
163
    public function getCharsetString(): string
164
    {
165
        $charsetString = '';
166
167
        if ($this->charset === 'utf-8') {
168
            $charsetString = ';CHARSET=UTF-8';
169
        }
170
171
        return $charsetString;
172
    }
173
174
    /**
175
     * Set filename
176
     *
177
     * @param string|array $value
178
     * @param bool         $overwrite [optional] Default overwrite is true
179
     * @param string       $separator [optional] Default separator is an underscore '_'
180
     * @return void
181
     */
182
    public function setFileName($value, $overwrite = true, $separator = '_'): void
183
    {
184
        // recast to string if $value is array
185
        if (\is_array($value)) {
186
            $value = implode($separator, $value);
187
        }
188
189
        // trim unneeded values
190
        $value = trim($value, $separator);
191
192
        // remove all spaces
193
        $value = preg_replace('/\s+/', $separator, $value);
194
195
        $pregQuoteSeparator = preg_quote($separator, '/');
196
197
        // if value is empty, stop here
198
        if (empty($value) || !preg_match("/[^\s$pregQuoteSeparator]/", $value)) {
199
            return;
200
        }
201
202
        // decode value
203
        $value = Transliterator::transliterate($value);
204
205
        // lowercase the string
206
        $value = strtolower($value);
207
208
        // urlize this part
209
        $value = Transliterator::urlize($value);
210
211
        // overwrite filename or add to filename using a prefix in between
212
        $this->fileName = $overwrite ?
213
            $value : $this->fileName.$separator.$value;
214
    }
215
216
    /**
217
     * Get filename
218
     *
219
     * @return string
220
     */
221
    public function getFileName(): string
222
    {
223
        if ($this->fileName === null) {
224
            return 'unknown';
225
        }
226
227
        return $this->fileName;
228
    }
229
230
    /**
231
     * Get content type
232
     *
233
     * @return string
234
     */
235
    public function getContentType(): string
236
    {
237
        return UserAgentUtil::isIOS7() ?
238
            'text/x-vcalendar' : 'text/x-vcard';
239
    }
240
241
    /**
242
     * Get file extension
243
     *
244
     * @return string
245
     */
246
    public function getFileExtension(): string
247
    {
248
        return UserAgentUtil::isIOS7() ?
249
            'ics' : 'vcf';
250
    }
251
252
    /**
253
     * Get full filename
254
     *
255
     * @return string
256
     */
257
    public function getFullFileName(): string
258
    {
259
        return $this->getFileName().'.'.$this->getFileExtension();
260
    }
261
262
    /**
263
     * Get headers
264
     *
265
     * @param bool $asAssociative
266
     * @return array
267
     */
268
    public function getHeaders(bool $asAssociative): array
269
    {
270
        $contentType = $this->getContentType().'; charset='.$this->getCharset();
271
        $contentDisposition = 'attachment; filename='.$this->getFullFileName();
272
        $contentLength = mb_strlen($this->getOutput(), $this->getCharset());
273
        $connection = 'close';
274
275
        if ($asAssociative) {
276
            return [
277
                'Content-type' => $contentType,
278
                'Content-Disposition' => $contentDisposition,
279
                'Content-Length' => $contentLength,
280
                'Connection' => $connection,
281
            ];
282
        }
283
284
        return [
285
            'Content-type: '.$contentType,
286
            'Content-Disposition: '.$contentDisposition,
287
            'Content-Length: '.$contentLength,
288
            'Connection: '.$connection,
289
        ];
290
    }
291
292
    /**
293
     * Get output as string
294
     * iOS devices (and safari < iOS 8 in particular) can not read .vcf (= vcard) files.
295
     * So I build a workaround to build a .ics (= vcalender) file.
296
     *
297
     * @return string
298
     */
299
    public function getOutput(): string
300
    {
301
        $output = UserAgentUtil::isIOS7() ?
302
            $this->buildVCalendar() : $this->buildVCard();
303
304
        return $output;
305
    }
306
307
    /**
308
     * Get properties
309
     *
310
     * @return array
311
     */
312
    public function getProperties(): array
313
    {
314
        return $this->properties;
315
    }
316
317
    /**
318
     * Has property
319
     *
320
     * @param string $key
321
     * @return bool
322
     */
323
    public function hasProperty(string $key): bool
324
    {
325
        $properties = $this->getProperties();
326
327
        foreach ($properties as $property) {
328
            if ($property['key'] === $key && $property['value'] !== '') {
329
                return true;
330
            }
331
        }
332
333
        return false;
334
    }
335
336
    /**
337
     * Save to a file
338
     *
339
     * @param string|null $savePath
340
     *
341
     * @return void
342
     * @throws OutputDirectoryNotExistsException
343
     */
344
    public function save(string $savePath = null): void
345
    {
346
        $file = $this->getFullFileName();
347
348
        // Add save path if given
349
        if (null !== $savePath) {
350
            $savePath = self::checkSavePath($savePath);
351
352
            $file = $savePath.$file;
353
        }
354
355
        file_put_contents(
356
            $file,
357
            $this->getOutput()
358
        );
359
    }
360
361
    /**
362
     * Check the save path directory
363
     *
364
     * @param string $savePath Save Path
365
     *
366
     * @return string
367
     * @throws OutputDirectoryNotExistsException
368
     */
369
    private static function checkSavePath($savePath): string
370
    {
371
        if (!is_dir($savePath)) {
372
            throw new OutputDirectoryNotExistsException();
373
        }
374
375
        // Add trailing directory separator the save path
376
        if (substr($savePath, -1) !== DIRECTORY_SEPARATOR) {
377
            $savePath .= DIRECTORY_SEPARATOR;
378
        }
379
380
        return $savePath;
381
    }
382
}
383