Issues (4868)

addressbook/inc/class.addressbook_vcal.inc.php (14 issues)

1
<?php
2
/**
3
 * Addressbook - vCard / iCal parser
4
 *
5
 * @link http://www.egroupware.org
6
 * @author Lars Kneschke <[email protected]>
7
 * @author Ralf Becker <RalfBecker-AT-outdoor-training.de>
8
 * @author Joerg Lehrke <[email protected]>
9
 * @package addressbook
10
 * @subpackage export
11
 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
12
 * @version $Id$
13
 */
14
15
use EGroupware\Api;
16
use EGroupware\Api\Link;
17
18
/**
19
 * Addressbook - vCard parser
20
 */
21
class addressbook_vcal extends addressbook_bo
22
{
23
	/**
24
	 * product manufacturer from setSupportedFields (lowercase!)
25
	 *
26
	 * @var string
27
	 */
28
	var $productManufacturer = 'file';
29
	/**
30
	 * product name from setSupportedFields (lowercase!)
31
	 *
32
	 * @var string
33
	 */
34
	var $productName;
35
	/**
36
	 * supported fields for vCard file and CardDAV import/export
37
	 *
38
	 * @var array
39
	 */
40
	var $databaseFields = array( // all entries e.g. for CardDAV
41
			'ADR;WORK'			=> array('','adr_one_street2','adr_one_street','adr_one_locality','adr_one_region',
42
									'adr_one_postalcode','adr_one_countryname'),
43
			'ADR;HOME'			=> array('','adr_two_street2','adr_two_street','adr_two_locality','adr_two_region',
44
									'adr_two_postalcode','adr_two_countryname'),
45
			'BDAY'				=> array('bday'),
46
			'CLASS'				=> array('private'),
47
			'CATEGORIES'			=> array('cat_id'),
48
			'EMAIL;WORK'			=> array('email'),
49
			'EMAIL;HOME'			=> array('email_home'),
50
			'N'				=> array('n_family','n_given','n_middle',
51
									'n_prefix','n_suffix'),
52
			'FN'				=> array('n_fn'),
53
			'NOTE'				=> array('note'),
54
			'ORG'				=> array('org_name','org_unit','room'),
55
			'TEL;CELL;WORK'			=> array('tel_cell'),
56
			'TEL;CELL;HOME'			=> array('tel_cell_private'),
57
			'TEL;CAR'			=> array('tel_car'),
58
			'TEL;OTHER'			=> array('tel_other'),
59
			'TEL;VOICE;WORK'		=> array('tel_work'),
60
			'TEL;FAX;WORK'			=> array('tel_fax'),
61
			'TEL;HOME;VOICE'		=> array('tel_home'),
62
			'TEL;FAX;HOME'			=> array('tel_fax_home'),
63
			'TEL;PAGER'			=> array('tel_pager'),
64
			'TITLE'				=> array('title'),
65
			'URL;WORK'			=> array('url'),
66
			'URL;HOME'			=> array('url_home'),
67
			'ROLE'				=> array('role'),
68
			'NICKNAME'			=> array('label'),
69
			'FBURL'				=> array('freebusy_uri'),
70
			'PHOTO'				=> array('jpegphoto'),
71
			'X-ASSISTANT'			=> array('assistent'),
72
			'X-ASSISTANT-TEL'		=> array('tel_assistent'),
73
			'UID'				=> array('uid'),
74
			'REV'				=> array('modified'),
75
			//'KEY' multivalued with mime-type to export PGP and S/Mime public keys
76
			'KEY'               => array('pubkey'),
77
			//set for Apple: 'X-ABSHOWAS'	=> array('fileas_type'),	// Horde vCard class uses uppercase prop-names!
78
		);
79
80
	var $supportedFields;
81
82
	/**
83
	 * VCard version
84
	 *
85
	 * @var string
86
	 */
87
	var $version;
88
	/**
89
	 * Client CTCap Properties
90
	 *
91
	 * @var array
92
	 */
93
	var $clientProperties;
94
	/**
95
	* Set Logging
96
	*
97
	* @var string
98
	* off = false;
99
	*/
100
	var $log = false;
101
	var $logfile="/tmp/log-vcard";
102
	/**
103
	* Constructor
104
	*
105
	* @param string $contact_app			the current application
106
	* @param string	$_contentType			the content type (version)
107
	* @param array $_clientProperties		client properties
108
	*/
109
	function __construct($contact_app='addressbook', $_contentType='text/x-vcard', &$_clientProperties = array())
110
	{
111
		parent::__construct($contact_app);
112
		if ($this->log)
113
		{
114
			$this->logfile = $GLOBALS['egw_info']['server']['temp_dir']."/log-vcard";
115
			error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n" .
116
				array2string($_contentType)."\n",3,$this->logfile);
117
		}
118
		switch($_contentType)
119
		{
120
			case 'text/vcard':
121
				$this->version = '3.0';
122
				break;
123
			default:
124
				$this->version = '2.1';
125
			break;
126
		}
127
		$this->clientProperties = $_clientProperties;
128
		$this->supportedFields = $this->databaseFields;
129
	}
130
	/**
131
	* import a vard into addressbook
132
	*
133
	* @param string	$_vcard		the vcard
134
	* @param int/string	$_abID =null		the internal addressbook id or !$_abID for a new enty
0 ignored issues
show
Documentation Bug introduced by
The doc comment int/string at position 0 could not be parsed: Unknown type name 'int/string' at position 0 in int/string.
Loading history...
135
	* @param boolean $merge =false	merge data with existing entry
136
	* @param string $charset  The encoding charset for $text. Defaults to
137
    *                         utf-8 for new format, iso-8859-1 for old format.
138
	* @return int contact id
139
	*/
140
	function addVCard($_vcard, $_abID=null, $merge=false, $charset=null)
141
	{
142
		if (!($contact = $this->vcardtoegw($_vcard, $charset))) return false;
143
144
		if ($_abID)
145
		{
146
			if (($old_contact = $this->read($_abID)))
147
			{
148
				$contact['photo_unchanged'] = $old_contact['jpegphoto'] === $contact['jpegphoto'];
149
				if ($merge)
150
				{
151
					foreach (array_keys($contact) as $key)
152
					{
153
						if (!empty($old_contact[$key]))
154
						{
155
							$contact[$key] = $old_contact[$key];
156
						}
157
					}
158
				}
159
				else
160
				{
161
					if (isset($old_contact['account_id']))
162
					{
163
						$contact['account_id'] = $old_contact['account_id'];
164
					}
165
					if (is_array($contact['cat_id']))
166
					{
167
						$contact['cat_id'] = implode(',',$this->find_or_add_categories($contact['cat_id'], $_abID));
168
					}
169
					else
170
					{
171
						// restore from orignal
172
						$contact['cat_id'] = $old_contact['cat_id'];
173
					}
174
				}
175
			}
176
			// update entry
177
			$contact['id'] = $_abID;
178
		}
179
		else
180
    	{
181
			// If photo is set, we want to update it
182
			$contact['photo_unchanged'] = false;
183
    		if (is_array($contact['cat_id']))
184
			{
185
				$contact['cat_id'] = implode(',',$this->find_or_add_categories($contact['cat_id'], -1));
186
			}
187
    	}
188
    	if (isset($contact['owner']) && $contact['owner'] != $this->user)
189
    	{
190
    		$contact['private'] = 0;	// foreign contacts are never private!
191
    	}
192
    	if ($this->log)
193
		{
194
			error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n" .
195
				array2string($contact)."\n",3,$this->logfile);
196
		}
197
		return $this->save($contact);
198
	}
199
200
	/**
201
	* return a vcard
202
	*
203
	* @param int/string	$_id the id of the contact
0 ignored issues
show
Documentation Bug introduced by
The doc comment int/string at position 0 could not be parsed: Unknown type name 'int/string' at position 0 in int/string.
Loading history...
204
	* @param string $_charset ='UTF-8' encoding of the vcard, default UTF-8
205
	* @param boolean $extra_charset_attribute =true GroupDAV/CalDAV dont need the charset attribute and some clients have problems with it
206
	* @return string containing the vcard
207
	*/
208
	function getVCard($_id,$_charset='UTF-8',$extra_charset_attribute=true)
209
	{
210
		$vCard = new Horde_Icalendar_Vcard($this->version);
211
		$vCard->setAttribute('PRODID','-//EGroupware//NONSGML EGroupware Addressbook '.$GLOBALS['egw_info']['apps']['api']['version'].'//'.
212
			strtoupper($GLOBALS['egw_info']['user']['preferences']['common']['lang']));
213
214
		$sysCharSet = Api\Translation::charset();
215
216
		// KAddressbook and Funambol4BlackBerry always requires non-ascii chars to be qprint encoded.
217
		if ($this->productName == 'kde' ||
218
			($this->productManufacturer == 'funambol' && $this->productName == 'blackberry plug-in'))
219
		{
220
			$extra_charset_attribute = true;
221
		}
222
223
		if (!($entry = $this->read($_id)))
224
		{
225
			return false;
226
		}
227
228
		$this->fixup_contact($entry);
229
230
		foreach ($this->supportedFields as $vcardField => $databaseFields)
231
		{
232
			$values = array();
233
			$options = array();
234
			$hasdata = 0;
235
			// seperate fields from their options/attributes
236
			$vcardFields = explode(';', $vcardField);
237
			$vcardField = $vcardFields[0];
238
			$i = 1;
239
			while (isset($vcardFields[$i]))
240
			{
241
				list($oname, $oval) = explode('=', $vcardFields[$i]);
242
				if (!$oval && ($this->version == '3.0'))
243
				{
244
					// declare OPTION as TYPE=OPTION
245
					$options['TYPE'][] = $oname ;
246
				}
247
				else
248
				{
249
					$options[$oname] = $oval;
250
				}
251
				$i++;
252
			}
253
			if (is_array($options['TYPE']))
254
			{
255
				$oval = implode(",", $options['TYPE']);
256
				unset($options['TYPE']);
257
				$options['TYPE'] = $oval;
258
			}
259
			if (isset($this->clientProperties[$vcardField]['Size']))
260
			{
261
				$size = $this->clientProperties[$vcardField]['Size'];
262
				$noTruncate = $this->clientProperties[$vcardField]['NoTruncate'];
263
				if ($this->log && $size > 0)
264
				{
265
					error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ .
266
						"() $vcardField Size: $size, NoTruncate: " .
267
						($noTruncate ? 'TRUE' : 'FALSE') . "\n",3,$this->logfile);
268
				}
269
				//Horde::logMessage("vCalAddressbook $vcardField Size: $size, NoTruncate: " .
270
				//	($noTruncate ? 'TRUE' : 'FALSE'), __FILE__, __LINE__, PEAR_LOG_DEBUG);
271
			}
272
			else
273
			{
274
				$size = -1;
275
				$noTruncate = false;
276
			}
277
			foreach ($databaseFields as $databaseField)
278
			{
279
				$value = '';
280
281
				if (!empty($databaseField))
282
				{
283
					$value = trim($entry[$databaseField]);
284
				}
285
286
				switch ($databaseField)
287
				{
288
					case 'modified':
289
						$value = gmdate("Y-m-d\TH:i:s\Z",Api\DateTime::user2server($value));
290
						$hasdata++;
291
						break;
292
293
					case 'private':
294
						$value = $value ? 'PRIVATE' : 'PUBLIC';
295
						$hasdata++;
296
						break;
297
298
					case 'bday':
299
						if (!empty($value))
300
						{
301
							if ($size == 8)
302
							{
303
								$value = str_replace('-','',$value);
304
							}
305
							elseif (isset($options['TYPE']) && (
306
								$options['TYPE'] == 'BASIC'))
307
							{
308
								unset($options['TYPE']);
309
								// used by old SyncML implementations
310
								$value = str_replace('-','',$value).'T000000Z';
311
							}
312
							$hasdata++;
313
						}
314
						break;
315
316
					case 'jpegphoto':
317
						if (empty($value) && ($entry['files'] & Api\Contacts::FILES_BIT_PHOTO))
318
						{
319
							$value = file_get_contents(Api\Link::vfs_path('addressbook', $entry['id'], Api\Contacts::FILES_PHOTO));
320
						}
321
						if (!empty($value) &&
322
								(($size < 0) || (strlen($value) < $size)))
323
						{
324
							if (!isset($options['TYPE']))
325
							{
326
								$options['TYPE'] = 'JPEG';
327
							}
328
							if (!isset($options['ENCODING']))
329
							{
330
								$options['ENCODING'] = 'BASE64';
331
							}
332
							$hasdata++;
333
							// need to encode binary image, not done in Horde Icalendar
334
							$value = base64_encode($value);
335
						}
336
						else
337
						{
338
							$value = '';
339
						}
340
						break;
341
342
					case 'pubkey':	// for now we only export S/Mime publik key, as no device supports PGP
343
						// https://en.wikipedia.org/wiki/VCard (search for "KEY")
344
						if (($value = $this->get_key($entry, false)))
345
						{
346
							$options['TYPE'] = 'SMIME';
347
							$options['MEDIATYPE'] = 'application/x-x509-user-cert';
348
							$options['ENCODING'] = $this->version == '3.0' ? 'b' : 'BASE64';
349
							$value = base64_encode($value);
350
							$hasdata++;
351
						}
352
						break;
353
354
					case 'cat_id':
355
						if (!empty($value) && ($values = /*str_replace(',','\\,',*/$this->get_categories($value)))//)
356
						{
357
							$values = (array) Api\Translation::convert($values, $sysCharSet, $_charset);
358
							$value = implode(',', $values); // just for the CHARSET recognition
359
							if (($size > 0) && strlen($value) > $size)
360
							{
361
								// let us try with only the first category
362
								$value = $values[0];
363
								if (strlen($value) > $size)
364
								{
365
									if ($this->log)
366
									{
367
										error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ .
368
										"() $vcardField omitted due to maximum size $size\n",3,$this->logfile);
369
									}
370
									// Horde::logMessage("vCalAddressbook $vcardField omitted due to maximum size $size",
371
									//		__FILE__, __LINE__, PEAR_LOG_WARNING);
372
									continue;
373
								}
374
								$values = array();
375
							}
376
							if (preg_match('/[^\x20-\x7F]/', $value))
377
							{
378
								if ($extra_charset_attribute || $this->productName == 'kde')
379
								{
380
									$options['CHARSET'] = $_charset;
381
								}
382
								// KAddressbook requires non-ascii chars to be qprint encoded, other clients eg. nokia phones have trouble with that
383
								if ($this->productName == 'kde')
384
								{
385
									$options['ENCODING'] = 'QUOTED-PRINTABLE';
386
								}
387
								elseif ($this->productManufacturer == 'funambol')
388
								{
389
										$options['ENCODING'] = 'FUNAMBOL-QP';
390
								}
391
								elseif (preg_match(Api\CalDAV\Handler::REQUIRE_QUOTED_PRINTABLE_ENCODING, $value))
392
								{
393
									$options['ENCODING'] = 'QUOTED-PRINTABLE';
394
								}
395
								elseif (!$extra_charset_attribute)
396
								{
397
									unset($options['ENCODING']);
398
								}
399
							}
400
							$hasdata++;
401
						}
402
						break;
403
404
					case 'n_fn':
405
					case 'fileas_type':
406
						// mark entries with fileas_type == 'org_name' as X-ABSHOWAS:COMPANY (Apple AB specific)
407
						if (isset($this->supportedFields['X-ABSHOWAS']) &&
408
							$entry['org_name'] == $entry['n_fileas'] && $entry['fileas_type'] == 'org_name')
409
						{
410
							if ($vcardField == 'X-ABSHOWAS') $value = 'COMPANY';
411
							if ($databaseField == 'n_fn') $value = $entry['org_name'];
412
						}
413
						//error_log("vcardField='$vcardField', databaseField='$databaseField', this->supportedFields['X-ABSHOWAS']=".array2string($this->supportedFields['X-ABSHOWAS'])." --> value='$value'");
414
						// fall-through
415
416
					default:
417
						if (($size > 0) && strlen(implode(',', $values) . $value) > $size)
418
						{
419
							if ($noTruncate)
420
							{
421
								if ($this->log)
422
								{
423
									error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ .
424
										"() $vcardField omitted due to maximum size $size\n",3,$this->logfile);
425
								}
426
								// Horde::logMessage("vCalAddressbook $vcardField omitted due to maximum size $size",
427
								//		__FILE__, __LINE__, PEAR_LOG_WARNING);
428
								continue;
429
							}
430
							// truncate the value to size
431
							$cursize = strlen(implode('', $values));
432
							$left = $size - $cursize - count($databaseFields) + 1;
433
							if ($left > 0)
434
							{
435
								$value = substr($value, 0, $left);
436
							}
437
							else
438
							{
439
								$value = '';
440
							}
441
							if ($this->log)
442
							{
443
								error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ .
444
									"() $vcardField truncated to maximum size $size\n",3,$this->logfile);
445
							}
446
							//Horde::logMessage("vCalAddressbook $vcardField truncated to maximum size $size",
447
							//		__FILE__, __LINE__, PEAR_LOG_INFO);
448
						}
449
						if (!empty($value) // required field
450
							|| in_array($vcardField,array('FN','ORG','N'))
451
							|| ($size >= 0 && !$noTruncate))
452
						{
453
							$value = Api\Translation::convert(trim($value), $sysCharSet, $_charset);
454
							$values[] = $value;
455
							if ($this->version == '2.1' && preg_match('/[^\x20-\x7F]/', $value))
456
							{
457
								if ($extra_charset_attribute || $this->productName == 'kde')
458
								{
459
									$options['CHARSET'] = $_charset;
460
								}
461
								// KAddressbook requires non-ascii chars to be qprint encoded, other clients eg. nokia phones have trouble with that
462
								if ($this->productName == 'kde')
463
								{
464
									$options['ENCODING'] = 'QUOTED-PRINTABLE';
465
								}
466
								elseif ($this->productManufacturer == 'funambol')
467
								{
468
									$options['ENCODING'] = 'FUNAMBOL-QP';
469
								}
470
								elseif (preg_match(Api\CalDAV\Handler::REQUIRE_QUOTED_PRINTABLE_ENCODING, $value))
471
								{
472
									$options['ENCODING'] = 'QUOTED-PRINTABLE';
473
								}
474
								elseif (!$extra_charset_attribute)
475
								{
476
									unset($options['ENCODING']);
477
								}
478
							}
479
							if ($vcardField == 'TEL' && $entry['tel_prefer'] &&
480
								($databaseField == $entry['tel_prefer']))
481
							{
482
								if ($options['TYPE'])
483
								{
484
									$options['TYPE'] .= ',';
485
								}
486
								$options['TYPE'] .= 'PREF';
487
							}
488
							$hasdata++;
489
						}
490
						else
491
						{
492
							$values[] = '';
493
						}
494
						break;
495
				}
496
			}
497
498
			if ($hasdata <= 0)
499
			{
500
				// don't add the entry if there is no data for this field,
501
				// except it's a mendatory field
502
				continue;
503
			}
504
505
			$vCard->setAttribute($vcardField, $value, $options, true, $values);
506
		}
507
508
		// current iOS 8.4 shows TEL;TYPE="WORK,VOICE":+49 123 4567890 as '"WORK'
509
		// old (patched) Horde iCalendar, did not enclosed parameter in quotes
510
		$result = preg_replace('/^TEL;TYPE="([^"]+)":/m', 'TEL;TYPE=$1:',
511
			$vCard->exportvCalendar($_charset));
0 ignored issues
show
The call to Horde_Icalendar_Vcard::exportvCalendar() has too many arguments starting with $_charset. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

511
			$vCard->/** @scrutinizer ignore-call */ 
512
           exportvCalendar($_charset));

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
512
513
		if ($this->log)
514
		{
515
			error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ .
516
				"() '$this->productManufacturer','$this->productName'\n",3,$this->logfile);
517
			error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n" .
518
				array2string($result)."\n",3,$this->logfile);
519
		}
520
		return $result;
521
	}
522
523
	function search($_vcard, $contentID=null, $relax=false, $charset=null)
524
	{
525
		$result = array();
526
527
		if (($contact = $this->vcardtoegw($_vcard, $charset)))
528
		{
529
			if (is_array($contact['category']))
530
			{
531
					$contact['category'] = implode(',',$this->find_or_add_categories($contact['category'],
532
						$contentID ? $contentID : -1));
533
			}
534
			if ($contentID)
535
			{
536
				$contact['id'] = $contentID;
537
			}
538
			$result = $this->find_contact($contact, $relax);
539
		}
540
		return $result;
541
	}
542
543
	function setSupportedFields($_productManufacturer='file', $_productName='', $_supportedFields = null)
544
	{
545
		$this->productManufacturer = strtolower($_productManufacturer);
546
		$this->productName = strtolower($_productName);
547
548
		if (is_array($_supportedFields)) $this->supportedFields = $_supportedFields;
549
	}
550
551
	function setDatabaseFields($_databaseFields)
552
	{
553
		if (is_array($_databaseFields)) $this->databaseFields = $_databaseFields;
554
	}
555
556
	/**
557
     * Parses a string containing vCard data.
558
     *
559
     * @param string $_vcard   The data to parse.
560
     * @param string $charset  The encoding charset for $text. Defaults to
561
     *                         utf-8 for new format, iso-8859-1 for old format.
562
     *
563
     * @return array|boolean   The contact data or false on errors.
564
     */
565
	function vcardtoegw($_vcard, $charset=null)
566
	{
567
		// the horde class does the charset conversion. DO NOT CONVERT HERE.
568
		// be as flexible as possible
569
570
		if ($this->log)
571
		{
572
			error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n" .
573
				array2string($_vcard)."\n",3,$this->logfile);
574
		}
575
576
		if(!($_vcard instanceof Horde_Icalendar))
0 ignored issues
show
$_vcard is never a sub-type of Horde_Icalendar.
Loading history...
577
		{
578
			$container = false;
579
			$vCard = Horde_Icalendar::newComponent('vcard', $container);
580
581
			if ($charset && $charset != 'utf-8')
582
			{
583
				$_vcard = Api\Translation::convert($_vcard, $charset, 'utf-8');
584
			}
585
			if (!$vCard->parsevCalendar($_vcard, 'VCARD'))
586
			{
587
				return False;
588
			}
589
		}
590
		else
591
		{
592
			$vCard = $_vcard;
593
		}
594
		$vcardValues = $vCard->getAllAttributes();
595
596
		if (!empty($GLOBALS['egw_info']['user']['preferences']['syncml']['minimum_uid_length']))
597
		{
598
			$minimum_uid_length = $GLOBALS['egw_info']['user']['preferences']['syncml']['minimum_uid_length'];
599
		}
600
		else
601
		{
602
			$minimum_uid_length = 8;
603
		}
604
605
		#print "<pre>$_vcard</pre>";
606
607
		#error_log(print_r($vcardValues, true));
608
		//Horde::logMessage("vCalAddressbook vcardtoegw: " . print_r($vcardValues, true), __FILE__, __LINE__, PEAR_LOG_DEBUG);
609
610
		$email = 1;
611
		$tel = 1;
612
		$cell = 1;
613
		$url = 1;
614
		$pref_tel = false;
615
616
		$rowNames = array();
617
		foreach($vcardValues as $key => $vcardRow)
618
		{
619
			$rowName  = strtoupper($vcardRow['name']);
620
			if ($vcardRow['value'] == ''  && implode('', $vcardRow['values']) == '')
621
			{
622
				unset($vcardRow);
623
				continue;
624
			}
625
			$rowTypes = array();
626
627
			$vcardRow['uparams'] = array();
628
			foreach ($vcardRow['params'] as $pname => $params)
629
			{
630
				$pname = strtoupper($pname);
631
				$vcardRow['uparams'][$pname] = $params;
632
			}
633
634
635
			// expand 3.0 TYPE paramters to 2.1 qualifiers
636
			$vcardRow['tparams'] = array();
637
			foreach ($vcardRow['uparams'] as $pname => $params)
638
			{
639
				switch ($pname)
640
				{
641
					case 'TYPE':
642
						if (is_array($params))
643
						{
644
							$rowTypes = array();
645
							foreach ($params as $param)
646
							{
647
								$rowTypes[] = strtoupper($param);
648
							}
649
						}
650
						else
651
						{
652
							$rowTypes[] = strtoupper($params);
653
						}
654
						foreach ($rowTypes as $type)
655
						{
656
							switch ($type)
657
							{
658
659
								case 'OTHER':
660
								case 'WORK':
661
								case 'HOME':
662
									$vcardRow['tparams'][$type] = '';
663
									break;
664
								case 'CELL':
665
								case 'PAGER':
666
								case 'FAX':
667
								case 'VOICE':
668
								case 'CAR':
669
								case 'PREF':
670
								case 'X-CUSTOMLABEL-CAR':
671
								case 'X-CUSTOMLABEL-IPHONE':
672
								case 'IPHONE':
673
									if ($vcardRow['name'] == 'TEL')
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment if this fall-through is intended.
Loading history...
674
									{
675
										$vcardRow['tparams'][$type] = '';
676
									}
677
								default:
678
									break;
679
							}
680
						}
681
						break;
682
					default:
683
						break;
684
				}
685
			}
686
687
			$vcardRow['uparams'] += $vcardRow['tparams'];
688
			ksort($vcardRow['uparams']);
689
690
			foreach ($vcardRow['uparams'] as $pname => $params)
691
			{
692
				switch ($pname)
693
				{
694
					case 'PREF':
695
						if (substr($rowName,0,3) == 'TEL' && !$pref_tel)
696
						{
697
							$pref_tel = $key;
698
						}
699
						break;
700
					case 'FAX':
701
					case 'PAGER':
702
					case 'VOICE':
703
					case 'OTHER':
704
					case 'CELL':
705
						if ($rowName != 'TEL') break;
706
					case 'WORK':
707
					case 'HOME':
708
						$rowName .= ';' . $pname;
709
						break;
710
					case 'CAR':
711
					case 'X-CUSTOMLABEL-CAR':
712
						if ($rowName == 'TEL')
713
						{
714
							$rowName = 'TEL;CAR';
715
						}
716
						break;
717
					case 'X-CUSTOMLABEL-IPHONE':
718
					case 'IPHONE':
719
						if ($rowName == 'TEL' || $rowName == 'TEL;CELL')
720
						{
721
							$rowName = 'TEL;CELL;HOME';
722
						}
723
						break;
724
					default:
725
						if (strpos($pname, 'X-FUNAMBOL-') === 0)
726
						{
727
							// Propriatary Funambol extension will be ignored
728
							$rowName .= ';' . $pname;
729
						}
730
						break;
731
				}
732
			}
733
734
			if ($rowName == 'EMAIL')
735
			{
736
				$rowName .= ';X-egw-Ref' . $email++;
737
			}
738
739
			if (($rowName == 'TEL;CELL') ||
740
					($rowName == 'TEL;CELL;VOICE'))
741
			{
742
				$rowName = 'TEL;CELL;X-egw-Ref' . $cell++;
743
			}
744
745
			if (($rowName == 'TEL') ||
746
					($rowName == 'TEL;VOICE'))
747
			{
748
				$rowName = 'TEL;X-egw-Ref' . $tel++;
749
			}
750
751
			if ($rowName == 'URL')
752
			{
753
				$rowName = 'URL;X-egw-Ref' . $url++;
754
			}
755
756
			// current algorithm cant cope with multiple attributes of same name
757
			// --> cumulate them in values, so they can be used later (works only for values, not for parameters!)
758
			if (($k = array_search($rowName, $rowNames)) != false)
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing $k = array_search($rowName, $rowNames) of type false|integer|string against false; this is ambiguous if the integer can be zero. Consider using a strict comparison !== instead.
Loading history...
759
			{
760
				$vcardValues[$k]['values'] = array_merge($vcardValues[$k]['values'],$vcardValues[$key]['values']);
761
			}
762
			$rowNames[$key] = $rowName;
763
		}
764
765
		if ($this->log)
766
		{
767
			error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n" .
768
				array2string($rowNames)."\n",3,$this->logfile);
769
		}
770
771
		// All rowNames of the vCard are now concatenated with their qualifiers.
772
		// If qualifiers are missing we apply a default strategy.
773
		// E.g. ADR will be either ADR;WORK, if no ADR;WORK is given,
774
		// or else ADR;HOME, if not available elsewhere.
775
776
		$finalRowNames = array();
777
778
		foreach ($rowNames as $vcardKey => $rowName)
779
		{
780
			switch ($rowName)
781
			{
782
				case 'VERSION':
783
					break;
784
				case 'ADR':
785
					if (!in_array('ADR;WORK', $rowNames)
786
							&& !isset($finalRowNames['ADR;WORK']))
787
					{
788
						$finalRowNames['ADR;WORK'] = $vcardKey;
789
					}
790
					elseif (!in_array('ADR;HOME', $rowNames)
791
							&& !isset($finalRowNames['ADR;HOME']))
792
					{
793
						$finalRowNames['ADR;HOME'] = $vcardKey;
794
					}
795
					break;
796
				case 'TEL;FAX':
797
					if (!in_array('TEL;FAX;WORK', $rowNames)
798
							&& !isset($finalRowNames['TEL;FAX;WORK']))
799
					{
800
						$finalRowNames['TEL;FAX;WORK'] = $vcardKey;
801
					}
802
					elseif (!in_array('TEL;FAX;HOME', $rowNames)
803
						&& !isset($finalRowNames['TEL;FAX;HOME']))
804
					{
805
						$finalRowNames['TEL;FAX;HOME'] = $vcardKey;
806
					}
807
					break;
808
				case 'TEL;WORK':
809
					if (!in_array('TEL;VOICE;WORK', $rowNames)
810
							&& !isset($finalRowNames['TEL;VOICE;WORK']))
811
					{
812
						$finalRowNames['TEL;VOICE;WORK'] = $vcardKey;
813
					}
814
					break;
815
				case 'TEL;HOME':
816
					if (!in_array('TEL;HOME;VOICE', $rowNames)
817
							&& !isset($finalRowNames['TEL;HOME;VOICE']))
818
					{
819
						$finalRowNames['TEL;HOME;VOICE'] = $vcardKey;
820
					}
821
					break;
822
				case 'TEL;OTHER;VOICE':
823
				    if (!in_array('TEL;OTHER', $rowNames)
824
							&& !isset($finalRowNames['TEL;OTHER']))
825
					{
826
						$finalRowNames['TEL;OTHER'] = $vcardKey;
827
					}
828
					break;
829
				case 'TEL;PAGER;WORK':
830
				case 'TEL;PAGER;HOME':
831
					if (!in_array('TEL;PAGER', $rowNames)
832
							&& !isset($finalRowNames['TEL;PAGER']))
833
					{
834
						$finalRowNames['TEL;PAGER'] = $vcardKey;
835
					}
836
					break;
837
				case 'TEL;CAR;VOICE':
838
				case 'TEL;CAR;CELL':
839
				case 'TEL;CAR;CELL;VOICE':
840
					if (!isset($finalRowNames['TEL;CAR']))
841
					{
842
						$finalRowNames['TEL;CAR'] = $vcardKey;
843
					}
844
					break;
845
				case 'TEL;X-egw-Ref1':
846
					if (!in_array('TEL;VOICE;WORK', $rowNames)
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment if this fall-through is intended.
Loading history...
847
							&& !in_array('TEL;WORK', $rowNames)
848
							&& !isset($finalRowNames['TEL;VOICE;WORK']))
849
					{
850
						$finalRowNames['TEL;VOICE;WORK'] = $vcardKey;
851
						break;
852
					}
853
				case 'TEL;X-egw-Ref2':
854
					if (!in_array('TEL;HOME;VOICE', $rowNames)
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment if this fall-through is intended.
Loading history...
855
							&& !in_array('TEL;HOME', $rowNames)
856
							&& !isset($finalRowNames['TEL;HOME;VOICE']))
857
					{
858
						$finalRowNames['TEL;HOME;VOICE'] = $vcardKey;
859
						break;
860
					}
861
				case 'TEL;X-egw-Ref3':
862
					if (!in_array('TEL;OTHER', $rowNames)
863
							&& !in_array('TEL;OTHER;VOICE', $rowNames)
864
							&& !isset($finalRowNames['TEL;OTHER']))
865
					{
866
						$finalRowNames['TEL;OTHER'] = $vcardKey;
867
					}
868
					break;
869
				case 'TEL;CELL;X-egw-Ref1':
870
					if (!in_array('TEL;CELL;WORK', $rowNames)
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment if this fall-through is intended.
Loading history...
871
							&& !isset($finalRowNames['TEL;CELL;WORK']))
872
					{
873
						$finalRowNames['TEL;CELL;WORK'] = $vcardKey;
874
						break;
875
					}
876
				case 'TEL;CELL;X-egw-Ref2':
877
					if (!in_array('TEL;CELL;HOME', $rowNames)
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment if this fall-through is intended.
Loading history...
878
							&& !isset($finalRowNames['TEL;CELL;HOME']))
879
					{
880
						$finalRowNames['TEL;CELL;HOME'] = $vcardKey;
881
						break;
882
					}
883
				case 'TEL;CELL;X-egw-Ref3':
884
					if (!in_array('TEL;CAR', $rowNames)
885
							&& !in_array('TEL;CAR;VOICE', $rowNames)
886
							&& !in_array('TEL;CAR;CELL', $rowNames)
887
							&& !in_array('TEL;CAR;CELL;VOICE', $rowNames)
888
							&& !isset($finalRowNames['TEL;CAR']))
889
					{
890
						$finalRowNames['TEL;CAR'] = $vcardKey;
891
					}
892
					break;
893
				case 'EMAIL;X-egw-Ref1':
894
					if (!in_array('EMAIL;WORK', $rowNames) &&
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment if this fall-through is intended.
Loading history...
895
							!isset($finalRowNames['EMAIL;WORK']))
896
					{
897
						$finalRowNames['EMAIL;WORK'] = $vcardKey;
898
						break;
899
					}
900
				case 'EMAIL;X-egw-Ref2':
901
					if (!in_array('EMAIL;HOME', $rowNames) &&
902
							!isset($finalRowNames['EMAIL;HOME']))
903
					{
904
						$finalRowNames['EMAIL;HOME'] = $vcardKey;
905
					}
906
					break;
907
				case 'URL;X-egw-Ref1':
908
					if (!in_array('URL;WORK', $rowNames) &&
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment if this fall-through is intended.
Loading history...
909
							!isset($finalRowNames['URL;WORK']))
910
					{
911
						$finalRowNames['URL;WORK'] = $vcardKey;
912
						break;
913
					}
914
				case 'URL;X-egw-Ref2':
915
					if (!in_array('URL;HOME', $rowNames) &&
916
							!isset($finalRowNames['URL;HOME']))
917
					{
918
						$finalRowNames['URL;HOME'] = $vcardKey;
919
					}
920
					break;
921
				case 'X-EVOLUTION-ASSISTANT':
922
					if (!isset($finalRowNames['X-ASSISTANT']))
923
					{
924
						$finalRowNames['X-ASSISTANT'] = $vcardKey;
925
					}
926
					break;
927
				default:
928
					if (!isset($finalRowNames[$rowName]))
929
					{
930
						$finalRowNames[$rowName] = $vcardKey;
931
					}
932
					break;
933
			}
934
		}
935
936
		if ($this->log)
937
		{
938
			error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n" .
939
				array2string($finalRowNames)."\n",3,$this->logfile);
940
		}
941
942
		$contact = array();
943
		// to be able to delete fields, we have to set all supported fields to at least null
944
		foreach($this->supportedFields as $fields)
945
		{
946
			foreach($fields as $field)
947
			{
948
				if ($field != 'fileas_type') $contact[$field] = null;
949
			}
950
		}
951
952
		foreach ($finalRowNames as $key => $vcardKey)
953
		{
954
			if (isset($this->databaseFields[$key]))
955
			{
956
				$fieldNames = $this->databaseFields[$key];
957
				foreach ($fieldNames as $fieldKey => $fieldName)
958
				{
959
					if (!empty($fieldName))
960
					{
961
						$value = trim($vcardValues[$vcardKey]['values'][$fieldKey]);
962
963
						if ($pref_tel && (($vcardKey == $pref_tel) ||
964
								($vcardValues[$vcardKey]['name'] == 'TEL') &&
965
								($vcardValues[$vcardKey]['value'] == $vcardValues[$pref_tel]['value'])))
966
						{
967
							$contact['tel_prefer'] = $fieldName;
968
						}
969
						switch($fieldName)
970
						{
971
							case 'bday':
972
								$contact[$fieldName] = $vcardValues[$vcardKey]['value']['year'] .
973
									'-' . $vcardValues[$vcardKey]['value']['month'] .
974
									'-' . $vcardValues[$vcardKey]['value']['mday'];
975
								break;
976
977
							case 'private':
978
								$contact[$fieldName] = (int) ( strtoupper($value) == 'PRIVATE');
979
								break;
980
981
							case 'cat_id':
982
								$contact[$fieldName] = $vcardValues[$vcardKey]['values'];
983
								break;
984
985
							case 'jpegphoto':
986
								$contact[$fieldName] = $vcardValues[$vcardKey]['value'];
987
								if(in_array($vcardValues[$vcardKey]['params']['ENCODING'],array('b','B','BASE64')))
988
								{
989
									$contact[$fieldName] = base64_decode($contact[$fieldName]);
990
								}
991
								break;
992
993
							case 'pubkey':
994
								$content = $vcardValues[$vcardKey]['value'];
995
								if(in_array($vcardValues[$vcardKey]['params']['ENCODING'],array('b','B','BASE64')))
996
								{
997
									$content = base64_decode($content);
0 ignored issues
show
The assignment to $content is dead and can be removed.
Loading history...
998
								}
999
								if ($vcardValues[$vcardKey]['params']['ENCODING'] === 'SMIME')
1000
								{
1001
									// ignore re-importing of S/Mime pubkey for now, as we might be called for a new contact
1002
									continue;
1003
								}
1004
								break;
1005
1006
							case 'note':
1007
								$contact[$fieldName] = str_replace("\r\n", "\n", $vcardValues[$vcardKey]['value']);
1008
								break;
1009
1010
							case 'fileas_type':
1011
								// store Apple's X-ABSHOWAS:COMPANY as fileas_type == 'org_name'
1012
								if ($vcardValues[$vcardKey]['value'] == 'COMPANY')
1013
								{
1014
									$contact[$fieldName] = 'org_name';
1015
								}
1016
								break;
1017
1018
							case 'uid':
1019
								if (strlen($value) < $minimum_uid_length) {
1020
									// we don't use it
1021
									break;
1022
								}
1023
							default:
1024
								$contact[$fieldName] = $value;
1025
							break;
1026
						}
1027
					}
1028
				}
1029
			}
1030
			// add unsupported attributes as with '##' prefix
1031
			elseif(($attribute = $vcardValues[$vcardKey]) && !in_array($attribute['name'],array('PRODID','REV')))
1032
			{
1033
				// for attributes with multiple values in multiple lines, merge the values
1034
				if (isset($contact['##'.$attribute['name']]))
1035
				{
1036
					error_log(__METHOD__."() contact['##$attribute[name]'] = ".array2string($contact['##'.$attribute['name']]));
1037
					$attribute['values'] = array_merge(
1038
						is_array($contact['##'.$attribute['name']]) ? $contact['##'.$attribute['name']]['values'] : (array)$contact['##'.$attribute['name']],
1039
						$attribute['values']);
1040
				}
1041
				$contact['##'.$attribute['name']] = $attribute['params'] || count($attribute['values']) > 1 ?
1042
					serialize($attribute) : $attribute['value'];
1043
			}
1044
		}
1045
1046
		$this->fixup_contact($contact);
1047
1048
		if ($this->log)
1049
		{
1050
			error_log(__FILE__.'['.__LINE__.'] '.__METHOD__	.
1051
				"() '$this->productManufacturer','$this->productName'\n",3,$this->logfile);
1052
			error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n" .
1053
				array2string($contact)."\n",3,$this->logfile);
1054
		}
1055
		return $contact;
1056
	}
1057
1058
	/**
1059
	 * Exports some contacts: download or write to a file
1060
	 *
1061
	 * @param array $ids contact-ids
1062
	 * @param string $file filename or null for download
1063
	 */
1064
	function export($ids, $file=null)
1065
	{
1066
		if (!$file)
1067
		{
1068
			$filename = count($ids) == 1 ? Link::title('addressbook',$ids[0]): 'egw_addressbook_'.date('Y-m-d');
1069
			Api\Header\Content::type(($filename ? $filename : 'addressbook').'.vcf','text/x-vcard');
1070
		}
1071
		if (!($fp = fopen($file ? $file : 'php://output','w')))
1072
		{
1073
			return false;
1074
		}
1075
		if (isset($GLOBALS['egw_info']['user']['preferences']['addressbook']['vcard_charset']))
1076
		{
1077
			$charset = $GLOBALS['egw_info']['user']['preferences']['addressbook']['vcard_charset'];
1078
		}
1079
		else
1080
		{
1081
			$charset = 'utf-8';
1082
		}
1083
		foreach ($ids as $id)
1084
		{
1085
			fwrite($fp,$this->getVCard($id, $charset));
1086
		}
1087
		fclose($fp);
1088
1089
		if (!$file)
1090
		{
1091
			exit();
0 ignored issues
show
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
1092
		}
1093
		return true;
1094
	}
1095
1096
	/**
1097
	 * return a groupVCard
1098
	 *
1099
	 * @param array $list values for 'list_uid', 'list_name', 'list_modified', 'members'
1100
	 * @param string $version ='3.0' vcard version
1101
	 * @return string containing the vcard
1102
	 */
1103
	function getGroupVCard(array $list,$version='3.0')
1104
	{
1105
		$vCard = new Horde_Icalendar_Vcard($version);
1106
		$vCard->setAttribute('PRODID','-//EGroupware//NONSGML EGroupware Addressbook '.$GLOBALS['egw_info']['apps']['api']['version'].'//'.
1107
			strtoupper($GLOBALS['egw_info']['user']['preferences']['common']['lang']));
1108
1109
		$vCard->setAttribute('N',$list['list_name'],array(),true,array($list['list_name'],'','','',''));
1110
		$vCard->setAttribute('FN',$list['list_name']);
1111
1112
		$vCard->setAttribute('X-ADDRESSBOOKSERVER-KIND','group');
1113
		foreach($list['members'] as $uid)
1114
		{
1115
			$vCard->setAttribute('X-ADDRESSBOOKSERVER-MEMBER','urn:uuid:'.$uid);
1116
		}
1117
		$vCard->setAttribute('REV',Api\DateTime::to($list['list_modified'],'Y-m-d\TH:i:s\Z'));
1118
		$vCard->setAttribute('UID',$list['list_uid']);
1119
1120
		return $vCard->exportvCalendar();
1121
	}
1122
}
1123