1<?php
2
3namespace Sabre\VObject;
4
5/**
6 * This utility converts vcards from one version to another.
7 *
8 * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
9 * @author Evert Pot (http://evertpot.com/)
10 * @license http://sabre.io/license/ Modified BSD License
11 */
12class VCardConverter {
13
14    /**
15     * Converts a vCard object to a new version.
16     *
17     * targetVersion must be one of:
18     *   Document::VCARD21
19     *   Document::VCARD30
20     *   Document::VCARD40
21     *
22     * Currently only 3.0 and 4.0 as input and output versions.
23     *
24     * 2.1 has some minor support for the input version, it's incomplete at the
25     * moment though.
26     *
27     * If input and output version are identical, a clone is returned.
28     *
29     * @param Component\VCard $input
30     * @param int $targetVersion
31     */
32    function convert(Component\VCard $input, $targetVersion) {
33
34        $inputVersion = $input->getDocumentType();
35        if ($inputVersion === $targetVersion) {
36            return clone $input;
37        }
38
39        if (!in_array($inputVersion, [Document::VCARD21, Document::VCARD30, Document::VCARD40])) {
40            throw new \InvalidArgumentException('Only vCard 2.1, 3.0 and 4.0 are supported for the input data');
41        }
42        if (!in_array($targetVersion, [Document::VCARD30, Document::VCARD40])) {
43            throw new \InvalidArgumentException('You can only use vCard 3.0 or 4.0 for the target version');
44        }
45
46        $newVersion = $targetVersion === Document::VCARD40 ? '4.0' : '3.0';
47
48        $output = new Component\VCard([
49            'VERSION' => $newVersion,
50        ]);
51
52        // We might have generated a default UID. Remove it!
53        unset($output->UID);
54
55        foreach ($input->children() as $property) {
56
57            $this->convertProperty($input, $output, $property, $targetVersion);
58
59        }
60
61        return $output;
62
63    }
64
65    /**
66     * Handles conversion of a single property.
67     *
68     * @param Component\VCard $input
69     * @param Component\VCard $output
70     * @param Property $property
71     * @param int $targetVersion
72     *
73     * @return void
74     */
75    protected function convertProperty(Component\VCard $input, Component\VCard $output, Property $property, $targetVersion) {
76
77        // Skipping these, those are automatically added.
78        if (in_array($property->name, ['VERSION', 'PRODID'])) {
79            return;
80        }
81
82        $parameters = $property->parameters();
83        $valueType = null;
84        if (isset($parameters['VALUE'])) {
85            $valueType = $parameters['VALUE']->getValue();
86            unset($parameters['VALUE']);
87        }
88        if (!$valueType) {
89            $valueType = $property->getValueType();
90        }
91        $newProperty = $output->createProperty(
92            $property->name,
93            $property->getParts(),
94            [], // parameters will get added a bit later.
95            $valueType
96        );
97
98
99        if ($targetVersion === Document::VCARD30) {
100
101            if ($property instanceof Property\Uri && in_array($property->name, ['PHOTO', 'LOGO', 'SOUND'])) {
102
103                $newProperty = $this->convertUriToBinary($output, $newProperty);
104
105            } elseif ($property instanceof Property\VCard\DateAndOrTime) {
106
107                // In vCard 4, the birth year may be optional. This is not the
108                // case for vCard 3. Apple has a workaround for this that
109                // allows applications that support Apple's extension still
110                // omit birthyears in vCard 3, but applications that do not
111                // support this, will just use a random birthyear. We're
112                // choosing 1604 for the birthyear, because that's what apple
113                // uses.
114                $parts = DateTimeParser::parseVCardDateTime($property->getValue());
115                if (is_null($parts['year'])) {
116                    $newValue = '1604-' . $parts['month'] . '-' . $parts['date'];
117                    $newProperty->setValue($newValue);
118                    $newProperty['X-APPLE-OMIT-YEAR'] = '1604';
119                }
120
121                if ($newProperty->name == 'ANNIVERSARY') {
122                    // Microsoft non-standard anniversary
123                    $newProperty->name = 'X-ANNIVERSARY';
124
125                    // We also need to add a new apple property for the same
126                    // purpose. This apple property needs a 'label' in the same
127                    // group, so we first need to find a groupname that doesn't
128                    // exist yet.
129                    $x = 1;
130                    while ($output->select('ITEM' . $x . '.')) {
131                        $x++;
132                    }
133                    $output->add('ITEM' . $x . '.X-ABDATE', $newProperty->getValue(), ['VALUE' => 'DATE-AND-OR-TIME']);
134                    $output->add('ITEM' . $x . '.X-ABLABEL', '_$!<Anniversary>!$_');
135                }
136
137            } elseif ($property->name === 'KIND') {
138
139                switch (strtolower($property->getValue())) {
140                    case 'org' :
141                        // vCard 3.0 does not have an equivalent to KIND:ORG,
142                        // but apple has an extension that means the same
143                        // thing.
144                        $newProperty = $output->createProperty('X-ABSHOWAS', 'COMPANY');
145                        break;
146
147                    case 'individual' :
148                        // Individual is implicit, so we skip it.
149                        return;
150
151                    case 'group' :
152                        // OS X addressbook property
153                        $newProperty = $output->createProperty('X-ADDRESSBOOKSERVER-KIND', 'GROUP');
154                        break;
155                }
156
157
158            }
159
160        } elseif ($targetVersion === Document::VCARD40) {
161
162            // These properties were removed in vCard 4.0
163            if (in_array($property->name, ['NAME', 'MAILER', 'LABEL', 'CLASS'])) {
164                return;
165            }
166
167            if ($property instanceof Property\Binary) {
168
169                $newProperty = $this->convertBinaryToUri($output, $newProperty, $parameters);
170
171            } elseif ($property instanceof Property\VCard\DateAndOrTime && isset($parameters['X-APPLE-OMIT-YEAR'])) {
172
173                // If a property such as BDAY contained 'X-APPLE-OMIT-YEAR',
174                // then we're stripping the year from the vcard 4 value.
175                $parts = DateTimeParser::parseVCardDateTime($property->getValue());
176                if ($parts['year'] === $property['X-APPLE-OMIT-YEAR']->getValue()) {
177                    $newValue = '--' . $parts['month'] . '-' . $parts['date'];
178                    $newProperty->setValue($newValue);
179                }
180
181                // Regardless if the year matched or not, we do need to strip
182                // X-APPLE-OMIT-YEAR.
183                unset($parameters['X-APPLE-OMIT-YEAR']);
184
185            }
186            switch ($property->name) {
187                case 'X-ABSHOWAS' :
188                    if (strtoupper($property->getValue()) === 'COMPANY') {
189                        $newProperty = $output->createProperty('KIND', 'ORG');
190                    }
191                    break;
192                case 'X-ADDRESSBOOKSERVER-KIND' :
193                    if (strtoupper($property->getValue()) === 'GROUP') {
194                        $newProperty = $output->createProperty('KIND', 'GROUP');
195                    }
196                    break;
197                case 'X-ANNIVERSARY' :
198                    $newProperty->name = 'ANNIVERSARY';
199                    // If we already have an anniversary property with the same
200                    // value, ignore.
201                    foreach ($output->select('ANNIVERSARY') as $anniversary) {
202                        if ($anniversary->getValue() === $newProperty->getValue()) {
203                            return;
204                        }
205                    }
206                    break;
207                case 'X-ABDATE' :
208                    // Find out what the label was, if it exists.
209                    if (!$property->group) {
210                        break;
211                    }
212                    $label = $input->{$property->group . '.X-ABLABEL'};
213
214                    // We only support converting anniversaries.
215                    if (!$label || $label->getValue() !== '_$!<Anniversary>!$_') {
216                        break;
217                    }
218
219                    // If we already have an anniversary property with the same
220                    // value, ignore.
221                    foreach ($output->select('ANNIVERSARY') as $anniversary) {
222                        if ($anniversary->getValue() === $newProperty->getValue()) {
223                            return;
224                        }
225                    }
226                    $newProperty->name = 'ANNIVERSARY';
227                    break;
228                // Apple's per-property label system.
229                case 'X-ABLABEL' :
230                    if ($newProperty->getValue() === '_$!<Anniversary>!$_') {
231                        // We can safely remove these, as they are converted to
232                        // ANNIVERSARY properties.
233                        return;
234                    }
235                    break;
236
237            }
238
239        }
240
241        // set property group
242        $newProperty->group = $property->group;
243
244        if ($targetVersion === Document::VCARD40) {
245            $this->convertParameters40($newProperty, $parameters);
246        } else {
247            $this->convertParameters30($newProperty, $parameters);
248        }
249
250        // Lastly, we need to see if there's a need for a VALUE parameter.
251        //
252        // We can do that by instantating a empty property with that name, and
253        // seeing if the default valueType is identical to the current one.
254        $tempProperty = $output->createProperty($newProperty->name);
255        if ($tempProperty->getValueType() !== $newProperty->getValueType()) {
256            $newProperty['VALUE'] = $newProperty->getValueType();
257        }
258
259        $output->add($newProperty);
260
261
262    }
263
264    /**
265     * Converts a BINARY property to a URI property.
266     *
267     * vCard 4.0 no longer supports BINARY properties.
268     *
269     * @param Component\VCard $output
270     * @param Property\Uri $property The input property.
271     * @param $parameters List of parameters that will eventually be added to
272     *                    the new property.
273     *
274     * @return Property\Uri
275     */
276    protected function convertBinaryToUri(Component\VCard $output, Property\Binary $newProperty, array &$parameters) {
277
278        $value = $newProperty->getValue();
279        $newProperty = $output->createProperty(
280            $newProperty->name,
281            null, // no value
282            [], // no parameters yet
283            'URI' // Forcing the BINARY type
284        );
285
286        $mimeType = 'application/octet-stream';
287
288        // See if we can find a better mimetype.
289        if (isset($parameters['TYPE'])) {
290
291            $newTypes = [];
292            foreach ($parameters['TYPE']->getParts() as $typePart) {
293                if (in_array(
294                    strtoupper($typePart),
295                    ['JPEG', 'PNG', 'GIF']
296                )) {
297                    $mimeType = 'image/' . strtolower($typePart);
298                } else {
299                    $newTypes[] = $typePart;
300                }
301            }
302
303            // If there were any parameters we're not converting to a
304            // mime-type, we need to keep them.
305            if ($newTypes) {
306                $parameters['TYPE']->setParts($newTypes);
307            } else {
308                unset($parameters['TYPE']);
309            }
310
311        }
312
313        $newProperty->setValue('data:' . $mimeType . ';base64,' . base64_encode($value));
314        return $newProperty;
315
316    }
317
318    /**
319     * Converts a URI property to a BINARY property.
320     *
321     * In vCard 4.0 attachments are encoded as data: uri. Even though these may
322     * be valid in vCard 3.0 as well, we should convert those to BINARY if
323     * possible, to improve compatibility.
324     *
325     * @param Component\VCard $output
326     * @param Property\Uri $property The input property.
327     *
328     * @return Property\Binary|null
329     */
330    protected function convertUriToBinary(Component\VCard $output, Property\Uri $newProperty) {
331
332        $value = $newProperty->getValue();
333
334        // Only converting data: uris
335        if (substr($value, 0, 5) !== 'data:') {
336            return $newProperty;
337        }
338
339        $newProperty = $output->createProperty(
340            $newProperty->name,
341            null, // no value
342            [], // no parameters yet
343            'BINARY'
344        );
345
346        $mimeType = substr($value, 5, strpos($value, ',') - 5);
347        if (strpos($mimeType, ';')) {
348            $mimeType = substr($mimeType, 0, strpos($mimeType, ';'));
349            $newProperty->setValue(base64_decode(substr($value, strpos($value, ',') + 1)));
350        } else {
351            $newProperty->setValue(substr($value, strpos($value, ',') + 1));
352        }
353        unset($value);
354
355        $newProperty['ENCODING'] = 'b';
356        switch ($mimeType) {
357
358            case 'image/jpeg' :
359                $newProperty['TYPE'] = 'JPEG';
360                break;
361            case 'image/png' :
362                $newProperty['TYPE'] = 'PNG';
363                break;
364            case 'image/gif' :
365                $newProperty['TYPE'] = 'GIF';
366                break;
367
368        }
369
370
371        return $newProperty;
372
373    }
374
375    /**
376     * Adds parameters to a new property for vCard 4.0.
377     *
378     * @param Property $newProperty
379     * @param array $parameters
380     *
381     * @return void
382     */
383    protected function convertParameters40(Property $newProperty, array $parameters) {
384
385        // Adding all parameters.
386        foreach ($parameters as $param) {
387
388            // vCard 2.1 allowed parameters with no name
389            if ($param->noName) $param->noName = false;
390
391            switch ($param->name) {
392
393                // We need to see if there's any TYPE=PREF, because in vCard 4
394                // that's now PREF=1.
395                case 'TYPE' :
396                    foreach ($param->getParts() as $paramPart) {
397
398                        if (strtoupper($paramPart) === 'PREF') {
399                            $newProperty->add('PREF', '1');
400                        } else {
401                            $newProperty->add($param->name, $paramPart);
402                        }
403
404                    }
405                    break;
406                // These no longer exist in vCard 4
407                case 'ENCODING' :
408                case 'CHARSET' :
409                    break;
410
411                default :
412                    $newProperty->add($param->name, $param->getParts());
413                    break;
414
415            }
416
417        }
418
419    }
420
421    /**
422     * Adds parameters to a new property for vCard 3.0.
423     *
424     * @param Property $newProperty
425     * @param array $parameters
426     *
427     * @return void
428     */
429    protected function convertParameters30(Property $newProperty, array $parameters) {
430
431        // Adding all parameters.
432        foreach ($parameters as $param) {
433
434            // vCard 2.1 allowed parameters with no name
435            if ($param->noName) $param->noName = false;
436
437            switch ($param->name) {
438
439                case 'ENCODING' :
440                    // This value only existed in vCard 2.1, and should be
441                    // removed for anything else.
442                    if (strtoupper($param->getValue()) !== 'QUOTED-PRINTABLE') {
443                        $newProperty->add($param->name, $param->getParts());
444                    }
445                    break;
446
447                /*
448                 * Converting PREF=1 to TYPE=PREF.
449                 *
450                 * Any other PREF numbers we'll drop.
451                 */
452                case 'PREF' :
453                    if ($param->getValue() == '1') {
454                        $newProperty->add('TYPE', 'PREF');
455                    }
456                    break;
457
458                default :
459                    $newProperty->add($param->name, $param->getParts());
460                    break;
461
462            }
463
464        }
465
466    }
467}
468