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