1<?php 2/** 3 * Copyright 2000-2017 Horde LLC (http://www.horde.org/) 4 * 5 * See the enclosed file LICENSE for license information (ASL). If you did 6 * did not receive this file, see http://www.horde.org/licenses/apache. 7 * 8 * @category Horde 9 * @copyright 2000-2017 Horde LLC 10 * @license http://www.horde.org/licenses/apache ASL 11 * @package Turba 12 */ 13 14/** 15 * Provides a common abstracted interface to the various directory search 16 * drivers. It includes functions for searching, adding, removing, and 17 * modifying directory entries. 18 * 19 * @author Chuck Hagenbuch <chuck@horde.org> 20 * @author Jon Parise <jon@csh.rit.edu> 21 * @category Horde 22 * @copyright 2000-2017 Horde LLC 23 * @license http://www.horde.org/licenses/apache ASL 24 * @package Turba 25 */ 26class Turba_Driver implements Countable 27{ 28 /** 29 * The symbolic title of this source. 30 * 31 * @var string 32 */ 33 public $title; 34 35 /** 36 * Hash describing the mapping between Turba attributes and 37 * driver-specific fields. 38 * 39 * @var array 40 */ 41 public $map = array(); 42 43 /** 44 * Hash with all tabs and their fields. 45 * 46 * @var array 47 */ 48 public $tabs = array(); 49 50 /** 51 * List of all fields that can be accessed in the backend (excludes 52 * composite attributes, etc.). 53 * 54 * @var array 55 */ 56 public $fields = array(); 57 58 /** 59 * Array of fields that must match exactly. 60 * 61 * @var array 62 */ 63 public $strict = array(); 64 65 /** 66 * Array of fields to search "approximately" (@see 67 * config/backends.php). 68 * 69 * @var array 70 */ 71 public $approximate = array(); 72 73 /** 74 * The name of a field to store contact list names in if not the default. 75 * 76 * @var string 77 */ 78 public $listNameField = null; 79 80 /** 81 * The name of a field to use as an alternative to the name field if that 82 * one is empty. 83 * 84 * @var string 85 */ 86 public $alternativeName = null; 87 88 /** 89 * The internal name of this source. 90 * 91 * @var string 92 */ 93 protected $_name; 94 95 /** 96 * Hash holding the driver's additional parameters. 97 * 98 * @var array 99 */ 100 protected $_params = array(); 101 102 /** 103 * What can this backend do? 104 * 105 * @var array 106 */ 107 protected $_capabilities = array(); 108 109 /** 110 * Any additional options passed to Turba_Object constructors. 111 * 112 * @var array 113 */ 114 protected $_objectOptions = array(); 115 116 /** 117 * Number of contacts in this source. 118 * 119 * @var integer 120 */ 121 protected $_count = null; 122 123 /** 124 * Hold the value for the owner of this address book. 125 * 126 * @var string 127 */ 128 protected $_contact_owner = ''; 129 130 /** 131 * Mapping of Turba attributes to ActiveSync fields. 132 * 133 * @var array 134 */ 135 static protected $_asMap = array( 136 'name' => 'fileas', 137 'lastname' => 'lastname', 138 'firstname' => 'firstname', 139 'middlenames' => 'middlename', 140 'alias' => 'nickname', 141 'nickname' => 'nickname', 142 'namePrefix' => 'title', 143 'nameSuffix' => 'suffix', 144 'homeStreet' => 'homestreet', 145 'homeCity' => 'homecity', 146 'homeProvince' => 'homestate', 147 'homePostalCode' => 'homepostalcode', 148 'homeCountryFree' => 'homecountry', 149 'otherStreet' => 'otherstreet', 150 'otherCity' => 'othercity', 151 'otherProvince' => 'otherstate', 152 'otherPostalCode' => 'otherpostalcode', 153 'otherCountryFree' => 'othercountry', 154 'workStreet' => 'businessstreet', 155 'workCity' => 'businesscity', 156 'workProvince' => 'businessstate', 157 'workPostalCode' => 'businesspostalcode', 158 'workCountryFree' => 'businesscountry', 159 'title' => 'jobtitle', 160 'company' => 'companyname', 161 'department' => 'department', 162 'office' => 'officelocation', 163 'spouse' => 'spouse', 164 'website' => 'webpage', 165 'assistant' => 'assistantname', 166 'manager' => 'managername', 167 'yomifirstname' => 'yomifirstname', 168 'yomilastname' => 'yomilastname', 169 'imaddress' => 'imaddress', 170 'imaddress2' => 'imaddress2', 171 'imaddress3' => 'imaddress3', 172 'homePhone' => 'homephonenumber', 173 'homePhone2' => 'home2phonenumber', 174 'workPhone' => 'businessphonenumber', 175 'workPhone2' => 'business2phonenumber', 176 'fax' => 'businessfaxnumber', 177 'homeFax' => 'homefaxnumber', 178 'pager' => 'pagernumber', 179 'cellPhone' => 'mobilephonenumber', 180 'carPhone' => 'carphonenumber', 181 'assistPhone' => 'assistnamephonenumber', 182 'companyPhone' => 'companymainphone', 183 'radioPhone' => 'radiophonenumber' 184 ); 185 186 /** 187 * Constructs a new Turba_Driver object. 188 * 189 * @param string $name Source name 190 * @param array $params Hash containing additional configuration 191 * parameters. 192 */ 193 public function __construct($name = '', array $params = array()) 194 { 195 $this->_name = $name; 196 $this->_params = $params; 197 } 198 199 /** 200 * Returns the current driver's additional parameters. 201 * 202 * @return array Hash containing the driver's additional parameters. 203 */ 204 public function getParams() 205 { 206 return $this->_params; 207 } 208 209 /** 210 * Checks if this backend has a certain capability. 211 * 212 * @param string $capability The capability to check for. 213 * 214 * @return boolean Supported or not. 215 */ 216 public function hasCapability($capability) 217 { 218 return !empty($this->_capabilities[$capability]); 219 } 220 221 /** 222 * Returns the attributes that are blob types. 223 * 224 * @return array List of blob attributes in the array keys. 225 */ 226 public function getBlobs() 227 { 228 global $attributes; 229 230 $blobs = array(); 231 foreach (array_keys($this->fields) as $attribute) { 232 if (isset($attributes[$attribute]) && 233 $attributes[$attribute]['type'] == 'image') { 234 $blobs[$attribute] = true; 235 } 236 } 237 238 return $blobs; 239 } 240 241 /** 242 * Returns the attributes that represent dates. 243 * 244 * @return array List of date attributes in the array keys. 245 * @since 4.2.0 246 */ 247 public function getDateFields() 248 { 249 global $attributes; 250 251 $dates = array(); 252 foreach (array_keys($this->fields) as $attribute) { 253 if (isset($attributes[$attribute]) && 254 $attributes[$attribute]['type'] == 'monthdayyear') { 255 $dates[$attribute] = '0000-00-00'; 256 } 257 } 258 259 return $dates; 260 } 261 262 /** 263 * Translates the keys of the first hash from the generalized Turba 264 * attributes to the driver-specific fields. The translation is based on 265 * the contents of $this->map. 266 * 267 * @param array $hash Hash using Turba keys. 268 * 269 * @return array Translated version of $hash. 270 */ 271 public function toDriverKeys(array $hash) 272 { 273 if (!empty($hash['name']) && 274 !empty($this->listNameField) && 275 !empty($hash['__type']) && 276 is_array($this->map['name']) && 277 ($hash['__type'] == 'Group')) { 278 $hash[$this->listNameField] = $hash['name']; 279 unset($hash['name']); 280 } 281 282 // Add composite fields to $hash if at least one field part exists 283 // and the composite field will be saved to storage. 284 // Otherwise composite fields won't be computed during an import. 285 foreach ($this->map as $key => $val) { 286 if (!is_array($val) || 287 empty($this->map[$key]['attribute']) || 288 array_key_exists($key, $hash)) { 289 continue; 290 } 291 292 foreach ($this->map[$key]['fields'] as $mapfields) { 293 if (isset($hash[$mapfields])) { 294 // Add composite field 295 $hash[$key] = null; 296 break; 297 } 298 } 299 } 300 301 $fields = array(); 302 foreach ($hash as $key => $val) { 303 if (!isset($this->map[$key])) { 304 continue; 305 } 306 if (!is_array($this->map[$key])) { 307 $fields[$this->map[$key]] = $val; 308 } elseif (!empty($this->map[$key]['attribute'])) { 309 $fieldarray = array(); 310 foreach ($this->map[$key]['fields'] as $mapfields) { 311 $fieldarray[] = isset($hash[$mapfields]) 312 ? $hash[$mapfields] 313 : ''; 314 } 315 $fields[$this->map[$key]['attribute']] = Turba::formatCompositeField($this->map[$key]['format'], $fieldarray); 316 } else { 317 // If 'parse' is not specified, use 'format' and 'fields'. 318 if (!isset($this->map[$key]['parse'])) { 319 $this->map[$key]['parse'] = array( 320 array( 321 'format' => $this->map[$key]['format'], 322 'fields' => $this->map[$key]['fields'] 323 ) 324 ); 325 } 326 foreach ($this->map[$key]['parse'] as $parse) { 327 $splitval = sscanf($val, $parse['format']); 328 $count = 0; 329 $tmp_fields = array(); 330 foreach ($parse['fields'] as $mapfield) { 331 if (isset($hash[$mapfield])) { 332 // If the compositing fields are set 333 // individually, then don't set them at all. 334 break 2; 335 } 336 $tmp_fields[$this->map[$mapfield]] = $splitval[$count++]; 337 } 338 // Exit if we found the best match. 339 if ($splitval[$count - 1] !== null) { 340 break; 341 } 342 } 343 $fields = array_merge($fields, $tmp_fields); 344 } 345 } 346 347 return $fields; 348 } 349 350 /** 351 * Takes a hash of Turba key => search value and return a (possibly 352 * nested) array, using backend attribute names, that can be turned into a 353 * search by the driver. The translation is based on the contents of 354 * $this->map, and includes nested OR searches for composite fields. 355 * 356 * @param array $criteria Hash of criteria using Turba keys. 357 * @param string $search_type OR search or AND search? 358 * @param array $strict Fields that must be matched exactly. 359 * @param boolean $match_begin Whether to match only at beginning of 360 * words. 361 * @param array $custom_strict Custom set of fields that are to matched 362 * exactly, but are glued using $search_type 363 * and 'AND' together with $strict fields. 364 * Allows an 'OR' search pm a custom set of 365 * $strict fields. 366 * 367 * @return array An array of search criteria. 368 */ 369 public function makeSearch($criteria, $search_type, array $strict, 370 $match_begin = false, array $custom_strict = array()) 371 { 372 $search = $search_terms = $subsearch = $strict_search = array(); 373 $glue = $temp = ''; 374 $lastChar = '\"'; 375 $blobs = $this->getBlobs(); 376 377 foreach ($criteria as $key => $val) { 378 if (!isset($this->map[$key])) { 379 continue; 380 } 381 if (is_array($this->map[$key])) { 382 /* Composite field, break out the search terms. */ 383 $parts = explode(' ', $val); 384 if (count($parts) > 1) { 385 /* Only parse if there was more than 1 search term and 386 * 'AND' the cumulative subsearches. */ 387 for ($i = 0; $i < count($parts); ++$i) { 388 $term = $parts[$i]; 389 $firstChar = substr($term, 0, 1); 390 if ($firstChar == '"') { 391 $temp = substr($term, 1, strlen($term) - 1); 392 $done = false; 393 while (!$done && $i < count($parts) - 1) { 394 $lastChar = substr($parts[$i + 1], -1); 395 if ($lastChar == '"') { 396 $temp .= ' ' . substr($parts[$i + 1], 0, -1); 397 $done = true; 398 } else { 399 $temp .= ' ' . $parts[$i + 1]; 400 } 401 ++$i; 402 } 403 $search_terms[] = $temp; 404 } else { 405 $search_terms[] = $term; 406 } 407 } 408 $glue = 'AND'; 409 } else { 410 /* If only one search term, use original input and 411 'OR' the searces since we're only looking for 1 412 term in any of the composite fields. */ 413 $search_terms[0] = $val; 414 $glue = 'OR'; 415 } 416 417 foreach ($this->map[$key]['fields'] as $field) { 418 if (!empty($blobs[$field])) { 419 continue; 420 } 421 $field = $this->toDriver($field); 422 if (!empty($strict[$field])) { 423 /* For strict matches, use the original search 424 * vals. */ 425 $strict_search[] = array( 426 'field' => $field, 427 'op' => '=', 428 'test' => $val, 429 ); 430 } elseif (!empty($custom_strict[$field])) { 431 $search[] = array( 432 'field' => $field, 433 'op' => '=', 434 'test' => $val, 435 ); 436 } else { 437 /* Create a subsearch for each individual search 438 * term. */ 439 if (count($search_terms) > 1) { 440 /* Build the 'OR' search for each search term 441 * on this field. */ 442 $atomsearch = array(); 443 for ($i = 0; $i < count($search_terms); ++$i) { 444 $atomsearch[] = array( 445 'field' => $field, 446 'op' => 'LIKE', 447 'test' => $search_terms[$i], 448 'begin' => $match_begin, 449 'approximate' => !empty($this->approximate[$field]), 450 ); 451 } 452 $atomsearch[] = array( 453 'field' => $field, 454 'op' => '=', 455 'test' => '', 456 'begin' => $match_begin, 457 'approximate' => !empty($this->approximate[$field]) 458 ); 459 460 $subsearch[] = array('OR' => $atomsearch); 461 unset($atomsearch); 462 $glue = 'AND'; 463 } else { 464 /* $parts may have more than one element, but 465 * if they are all quoted we will only have 1 466 * $subsearch. */ 467 $subsearch[] = array( 468 'field' => $field, 469 'op' => 'LIKE', 470 'test' => $search_terms[0], 471 'begin' => $match_begin, 472 'approximate' => !empty($this->approximate[$field]), 473 ); 474 $glue = 'OR'; 475 } 476 } 477 } 478 if (count($subsearch)) { 479 $search[] = array($glue => $subsearch); 480 } 481 } else { 482 /* Not a composite field. */ 483 if (!empty($blobs[$key])) { 484 continue; 485 } 486 if (!empty($strict[$this->map[$key]])) { 487 $strict_search[] = array( 488 'field' => $this->map[$key], 489 'op' => '=', 490 'test' => $val, 491 ); 492 } elseif (!empty($custom_strict[$this->map[$key]])) { 493 $search[] = array( 494 'field' => $this->map[$key], 495 'op' => '=', 496 'test' => $val, 497 ); 498 } else { 499 $search[] = array( 500 'field' => $this->map[$key], 501 'op' => 'LIKE', 502 'test' => $val, 503 'begin' => $match_begin, 504 'approximate' => !empty($this->approximate[$this->map[$key]]), 505 ); 506 } 507 } 508 } 509 510 if (count($strict_search) && count($search)) { 511 return array( 512 'AND' => array( 513 $search_type => $strict_search, 514 array( 515 $search_type => $search 516 ) 517 ) 518 ); 519 } elseif (count($strict_search)) { 520 return array( 521 $search_type => $strict_search 522 ); 523 } elseif (count($search)) { 524 return array( 525 $search_type => $search 526 ); 527 } 528 529 return array(); 530 } 531 532 /** 533 * Translates a single Turba attribute to the driver-specific 534 * counterpart. The translation is based on the contents of 535 * $this->map. This ignores composite fields. 536 * 537 * @param string $attribute The Turba attribute to translate. 538 * 539 * @return string The driver name for this attribute. 540 */ 541 public function toDriver($attribute) 542 { 543 if (!isset($this->map[$attribute])) { 544 return null; 545 } 546 547 return is_array($this->map[$attribute]) 548 ? $this->map[$attribute]['fields'] 549 : $this->map[$attribute]; 550 } 551 552 /** 553 * Translates a hash from being keyed on driver-specific fields to being 554 * keyed on the generalized Turba attributes. The translation is based on 555 * the contents of $this->map. 556 * 557 * @param array $entry A hash using driver-specific keys. 558 * 559 * @return array Translated version of $entry. 560 */ 561 public function toTurbaKeys(array $entry) 562 { 563 $new_entry = array(); 564 foreach ($this->map as $key => $val) { 565 if (!is_array($val)) { 566 $new_entry[$key] = (isset($entry[$val]) && (!empty($entry[$val]) || (is_string($entry[$val]) && strlen($entry[$val])))) 567 ? trim($entry[$val]) 568 : null; 569 } 570 } 571 572 return $new_entry; 573 } 574 575 /** 576 * Searches the source based on the provided criteria. 577 * 578 * @todo Allow $criteria to contain the comparison operator (<, =, >, 579 * 'like') and modify the drivers accordingly. 580 * 581 * @param array $search_criteria Hash containing the search criteria. 582 * @param string $sort_order The requested sort order which is passed 583 * to Turba_List::sort(). 584 * @param string $search_type Do an AND or an OR search (defaults to 585 * AND). 586 * @param array $return_fields A list of fields to return; defaults to 587 * all fields. 588 * @param array $custom_strict A list of fields that must match exactly. 589 * @param boolean $match_begin Whether to match only at beginning of 590 * words. 591 * @param boolean $count_only Only return the count of matching entries, 592 * not the entries themselves. 593 * 594 * @return mixed Turba_List|integer The sorted, filtered list of search 595 * results or the number of matching 596 * entries (if $count_only is true). 597 * @throws Turba_Exception 598 */ 599 public function search(array $search_criteria, $sort_order = null, 600 $search_type = 'AND', array $return_fields = array(), 601 array $custom_strict = array(), $match_begin = false, 602 $count_only = false) 603 { 604 /* Add any fields that must match exactly for this source to the 605 * $strict_fields array. */ 606 $strict_fields = $custom_strict_fields = array(); 607 foreach ($this->strict as $strict_field) { 608 $strict_fields[$strict_field] = true; 609 } 610 611 /* Differentiate between provided $custom_strict fields - which honor 612 * the $search_type and $strict fields which are not 613 * explicitly requested as part of this search, and as such, are not 614 * constrained by the requested $search_type. */ 615 foreach ($custom_strict as $strict_field) { 616 if (isset($this->map[$strict_field])) { 617 $custom_strict_fields[$this->map[$strict_field]] = true; 618 } 619 } 620 621 /* Translate the Turba attributes to driver-specific attributes. */ 622 $fields = $this->makeSearch($search_criteria, $search_type, 623 $strict_fields, $match_begin, $custom_strict_fields); 624 625 /* If we are not using Horde_Share, enforce the requirement that the 626 * current user must be the owner of the addressbook. */ 627 if (isset($this->map['__owner'])) { 628 $fields = array( 629 'AND' => array( 630 $fields, 631 array( 632 'field' => $this->toDriver('__owner'), 633 'op' => '=', 634 'test' => $this->getContactOwner() 635 ) 636 ) 637 ); 638 } 639 640 if (in_array('email', $return_fields) && 641 !in_array('emails', $return_fields)) { 642 $return_fields[] = 'emails'; 643 } 644 if (count($return_fields)) { 645 $default_fields = array('__key', '__type', '__owner', '__members', 'name'); 646 if ($this->alternativeName) { 647 $default_fields[] = $this->alternativeName; 648 } 649 $return_fields_pre = array_unique(array_merge($default_fields, $return_fields)); 650 $return_fields = array(); 651 foreach ($return_fields_pre as $field) { 652 $result = $this->toDriver($field); 653 if (is_array($result)) { 654 foreach ($result as $composite_field) { 655 $composite_result = $this->toDriver($composite_field); 656 if ($composite_result) { 657 $return_fields[] = $composite_result; 658 } 659 } 660 } elseif ($result) { 661 $return_fields[] = $result; 662 } 663 } 664 } else { 665 /* Need to force the array to be re-keyed for the (fringe) case 666 * where we might have 1 DB field mapped to 2 or more Turba 667 * fields */ 668 $return_fields = array_values( 669 array_unique(array_values($this->fields))); 670 } 671 672 /* Retrieve the search results from the driver. */ 673 $objects = $this->_search($fields, $return_fields, $this->toDriverKeys($this->getBlobs()), $count_only); 674 if ($count_only) { 675 return $objects; 676 } 677 return $this->_toTurbaObjects($objects, $sort_order); 678 } 679 680 /** 681 * Searches the current address book for duplicate entries. 682 * 683 * Duplicates are determined by comparing email and name or last name and 684 * first name values. 685 * 686 * @return array A hash with the following format: 687 * <code> 688 * array('name' => array('John Doe' => Turba_List, ...), ...) 689 * </code> 690 * @throws Turba_Exception 691 */ 692 public function searchDuplicates() 693 { 694 return array(); 695 } 696 697 /** 698 * Takes an array of object hashes and returns a Turba_List 699 * containing the correct Turba_Objects 700 * 701 * @param array $objects An array of object hashes (keyed to backend). 702 * @param array $sort_order Array of hashes describing sort fields. Each 703 * hash has the following fields: 704 * <pre> 705 * ascending - (boolean) Indicating sort direction. 706 * field - (string) Sort field. 707 * </pre> 708 * 709 * @return Turba_List A list object. 710 */ 711 protected function _toTurbaObjects(array $objects, array $sort_order = null) 712 { 713 $list = new Turba_List(); 714 715 foreach ($objects as $object) { 716 /* Translate the driver-specific fields in the result back to the 717 * more generalized common Turba attributes using the map. */ 718 $object = $this->toTurbaKeys($object); 719 720 $done = false; 721 if (!empty($object['__type']) && 722 ucwords($object['__type']) != 'Object') { 723 $class = 'Turba_Object_' . ucwords($object['__type']); 724 if (class_exists($class)) { 725 $list->insert(new $class($this, $object, $this->_objectOptions)); 726 $done = true; 727 } 728 } 729 if (!$done) { 730 $list->insert(new Turba_Object($this, $object, $this->_objectOptions)); 731 } 732 } 733 734 $list->sort($sort_order); 735 736 /* Return the filtered (sorted) results. */ 737 return $list; 738 } 739 740 /** 741 * Returns a list of birthday or anniversary hashes from this source for a 742 * certain period. 743 * 744 * @param Horde_Date $start The start date of the valid period. 745 * @param Horde_Date $end The end date of the valid period. 746 * @param string $category The timeObjects category to return. 747 * 748 * @return array A list of timeObject hashes. 749 * @throws Turba Exception 750 */ 751 public function listTimeObjects(Horde_Date $start, Horde_Date $end, $category) 752 { 753 try { 754 $res = $this->getTimeObjectTurbaList($start, $end, $category); 755 } catch (Turba_Exception $e) { 756 /* Try the default implementation before returning an error */ 757 $res = $this->_getTimeObjectTurbaListFallback($start, $end, $category); 758 } 759 760 $t_objects = array(); 761 while ($ob = $res->next()) { 762 $t_object = $ob->getValue($category); 763 if (empty($t_object)) { 764 continue; 765 } 766 767 try { 768 $t_object = new Horde_Date($t_object); 769 } catch (Horde_Date_Exception $e) { 770 continue; 771 } 772 773 if ($t_object->compareDate($end) > 0) { 774 continue; 775 } 776 777 $t_object_end = new Horde_Date($t_object); 778 ++$t_object_end->mday; 779 $key = $ob->getValue('__key'); 780 781 // Calculate the age of the time object 782 if ($start->year == $end->year || 783 $end->year == 9999) { 784 $age = $start->year - $t_object->year; 785 } elseif ($t_object->month <= $end->month) { 786 // t_object must be in later year 787 $age = $end->year - $t_object->year; 788 } else { 789 // t_object must be in earlier year 790 $age = $start->year - $t_object->year; 791 } 792 793 $title = sprintf(_("%d. %s of %s"), 794 $age, 795 $GLOBALS['attributes'][$category]['label'], 796 $ob->getValue('name')); 797 798 $t_objects[] = array( 799 'id' => $key, 800 'title' => $title, 801 'start' => sprintf('%d-%02d-%02dT00:00:00', 802 $t_object->year, 803 $t_object->month, 804 $t_object->mday), 805 'end' => sprintf('%d-%02d-%02dT00:00:00', 806 $t_object_end->year, 807 $t_object_end->month, 808 $t_object_end->mday), 809 'recurrence' => array('type' => Horde_Date_Recurrence::RECUR_YEARLY_DATE, 810 'interval' => 1), 811 'params' => array('source' => $this->_name, 'key' => $key), 812 'link' => Horde::url('contact.php', true)->add(array('source' => $this->_name, 'key' => $key))->setRaw(true) 813 ); 814 } 815 816 return $t_objects; 817 } 818 819 /** 820 * Default implementation for obtaining a Turba_List to get TimeObjects 821 * out of. 822 * 823 * @param Horde_Date $start The starting date. 824 * @param Horde_Date $end The ending date. 825 * @param string $field The address book field containing the 826 * timeObject information (birthday, 827 * anniversary). 828 * 829 * @return Turba_List A list of objects. 830 * @throws Turba_Exception 831 */ 832 public function getTimeObjectTurbaList(Horde_Date $start, Horde_Date $end, $field) 833 { 834 return $this->_getTimeObjectTurbaListFallback($start, $end, $field); 835 } 836 837 /** 838 * Default implementation for obtaining a Turba_List to get TimeObjects 839 * out of. 840 * 841 * @param Horde_Date $start The starting date. 842 * @param Horde_Date $end The ending date. 843 * @param string $field The address book field containing the 844 * timeObject information (birthday, 845 * anniversary). 846 * 847 * @return Turba_List A list of objects. 848 * @throws Turba_Exception 849 */ 850 protected function _getTimeObjectTurbaListFallback(Horde_Date $start, Horde_Date $end, $field) 851 { 852 return $this->search(array(), null, 'AND', array('name', $field)); 853 } 854 855 /** 856 * Retrieves a set of objects from the source. 857 * 858 * @param array $objectIds The unique ids of the objects to retrieve. 859 * 860 * @return array The array of retrieved objects (Turba_Objects). 861 * @throws Turba_Exception 862 * @throws Horde_Exception_NotFound 863 */ 864 public function getObjects(array $objectIds) 865 { 866 $objects = $this->_read($this->map['__key'], $objectIds, 867 $this->getContactOwner(), 868 array_values($this->fields), 869 $this->toDriverKeys($this->getBlobs()), 870 $this->toDriverKeys($this->getDateFields())); 871 if (!is_array($objects)) { 872 throw new Horde_Exception_NotFound(); 873 } 874 875 $results = array(); 876 foreach ($objects as $object) { 877 $object = $this->toTurbaKeys($object); 878 $done = false; 879 if (!empty($object['__type']) && 880 ucwords($object['__type']) != 'Object') { 881 $class = 'Turba_Object_' . ucwords($object['__type']); 882 if (class_exists($class)) { 883 $results[] = new $class($this, $object, $this->_objectOptions); 884 $done = true; 885 } 886 } 887 if (!$done) { 888 $results[] = new Turba_Object($this, $object, $this->_objectOptions); 889 } 890 } 891 892 return $results; 893 } 894 895 /** 896 * Retrieves one object from the source. 897 * 898 * @param string $objectId The unique id of the object to retrieve. 899 * 900 * @return Turba_Object The retrieved object. 901 * @throws Turba_Exception 902 * @throws Horde_Exception_NotFound 903 */ 904 public function getObject($objectId) 905 { 906 $result = $this->getObjects(array($objectId)); 907 908 if (empty($result[0])) { 909 throw new Horde_Exception_NotFound(); 910 } 911 912 $result = $result[0]; 913 if (!isset($this->map['__owner'])) { 914 $result->attributes['__owner'] = $this->getContactOwner(); 915 } 916 917 return $result; 918 } 919 920 /** 921 * Adds a new entry to the contact source. 922 * 923 * @param array $attributes The attributes of the new object to add. 924 * 925 * @return string The new __key value on success. 926 * @throws Turba_Exception 927 */ 928 public function add(array $attributes) 929 { 930 /* Only set __type and __owner if they are not already set. */ 931 if (!isset($attributes['__type'])) { 932 $attributes['__type'] = 'Object'; 933 } 934 if (isset($this->map['__owner']) && !isset($attributes['__owner'])) { 935 $attributes['__owner'] = $this->getContactOwner(); 936 } 937 938 if (!isset($attributes['__uid'])) { 939 $attributes['__uid'] = $this->_makeUid(); 940 } 941 942 $key = $attributes['__key'] = $this->_makeKey($this->toDriverKeys($attributes)); 943 $uid = $attributes['__uid']; 944 945 /* Remember any tags, since toDriverKeys will remove them. 946 (They are not stored in the Turba backend so have no mapping). */ 947 $tags = isset($attributes['__tags']) 948 ? $attributes['__tags'] 949 : false; 950 951 $attributes = $this->toDriverKeys($attributes); 952 953 $this->_add($attributes, $this->toDriverKeys($this->getBlobs()), $this->toDriverKeys($this->getDateFields())); 954 955 /* Add tags. */ 956 if ($tags !== false) { 957 $GLOBALS['injector']->getInstance('Turba_Tagger')->tag( 958 $uid, 959 $tags, 960 $GLOBALS['registry']->getAuth(), 961 'contact' 962 ); 963 } 964 965 /* Log the creation of this item in the history log. */ 966 try { 967 $GLOBALS['injector']->getInstance('Horde_History') 968 ->log('turba:' . $this->getName() . ':' . $uid, 969 array('action' => 'add'), true); 970 } catch (Exception $e) { 971 Horde::log($e, 'ERR'); 972 } 973 974 return $key; 975 } 976 977 /** 978 * Returns ability of the backend to add new contacts. 979 * 980 * @return boolean Can backend add? 981 */ 982 public function canAdd() 983 { 984 return $this->_canAdd(); 985 } 986 987 /** 988 * Returns ability of the backend to add new contacts. 989 * 990 * @return boolean Can backend add? 991 */ 992 protected function _canAdd() 993 { 994 return false; 995 } 996 997 /** 998 * Deletes the specified entry from the contact source. 999 * 1000 * @param string $object_id The ID of the object to delete. 1001 * @param boolean $remove_tags Remove tags if true. 1002 * 1003 * @throws Turba_Exception 1004 * @throws Horde_Exception_NotFound 1005 */ 1006 public function delete($object_id, $remove_tags = true) 1007 { 1008 $object = $this->getObject($object_id); 1009 1010 if (!$object->hasPermission(Horde_Perms::DELETE)) { 1011 throw new Turba_Exception(_("Permission denied")); 1012 } 1013 1014 $this->_delete($this->toDriver('__key'), $object_id); 1015 1016 $own_contact = $GLOBALS['prefs']->getValue('own_contact'); 1017 if (!empty($own_contact)) { 1018 @list(,$id) = explode(';', $own_contact); 1019 if ($id == $object_id) { 1020 $GLOBALS['prefs']->setValue('own_contact', ''); 1021 } 1022 } 1023 1024 /* Log the deletion of this item in the history log. */ 1025 if ($object->getValue('__uid')) { 1026 try { 1027 $GLOBALS['injector']->getInstance('Horde_History')->log($object->getGuid(), 1028 array('action' => 'delete'), 1029 true); 1030 } catch (Exception $e) { 1031 Horde::log($e, 'ERR'); 1032 } 1033 } 1034 1035 /* Remove any CalDAV mappings. */ 1036 try { 1037 $davStorage = $GLOBALS['injector'] 1038 ->getInstance('Horde_Dav_Storage'); 1039 try { 1040 $davStorage 1041 ->deleteInternalObjectId($object_id, $this->_name); 1042 } catch (Horde_Exception $e) { 1043 Horde::log($e); 1044 } 1045 } catch (Horde_Exception $e) { 1046 } 1047 1048 /* Remove tags */ 1049 if ($remove_tags) { 1050 $GLOBALS['injector']->getInstance('Turba_Tagger') 1051 ->replaceTags($object->getValue('__uid'), array(), $this->getContactOwner(), 'contact'); 1052 1053 /* Might have tags disabled, hence no Content_* objects autoloadable. */ 1054 try { 1055 /* Tell content we removed the object */ 1056 $GLOBALS['injector']->getInstance('Content_Objects_Manager') 1057 ->delete(array($object->getValue('__uid')), 'contact'); 1058 } catch (Horde_Exception $e) {} 1059 } 1060 } 1061 1062 /** 1063 * Deletes all contacts from an address book. 1064 * 1065 * @param string $sourceName The identifier of the address book to 1066 * delete. If omitted, will clear the current 1067 * user's 'default' address book for this 1068 * source type. 1069 * 1070 * @throws Turba_Exception 1071 */ 1072 public function deleteAll($sourceName = null) 1073 { 1074 if (!$this->hasCapability('delete_all')) { 1075 throw new Turba_Exception('Not supported'); 1076 } 1077 1078 $ids = $this->_deleteAll($sourceName); 1079 1080 // Update Horde_History and Tagger 1081 $history = $GLOBALS['injector']->getInstance('Horde_History'); 1082 try { 1083 foreach ($ids as $uid) { 1084 // This is slightly hackish, but it saves us from having to 1085 // create and save an array of Turba_Objects before we delete 1086 // them, just to be able to calculate this using 1087 // Turba_Object#getGuid 1088 $guid = 'turba:' . $this->getName() . ':' . $uid; 1089 $history->log($guid, array('action' => 'delete'), true); 1090 1091 // Remove tags. 1092 $GLOBALS['injector']->getInstance('Turba_Tagger') 1093 ->replaceTags($uid, array(), $this->getContactOwner(), 'contact'); 1094 1095 /* Tell content we removed the object */ 1096 $GLOBALS['injector']->getInstance('Content_Objects_Manager') 1097 ->delete(array($uid), 'contact'); 1098 } 1099 } catch (Exception $e) { 1100 Horde::log($e, 'ERR'); 1101 } 1102 } 1103 1104 /** 1105 * Deletes all contacts from an address book. 1106 * 1107 * @param string $sourceName The source to remove all contacts from. 1108 * 1109 * @return array An array of UIDs that have been deleted. 1110 * @throws Turba_Exception 1111 */ 1112 protected function _deleteAll() 1113 { 1114 return array(); 1115 } 1116 1117 /** 1118 * Modifies an existing entry in the contact source. 1119 * 1120 * @param Turba_Object $object The object to update. 1121 * 1122 * @return string The object id, possibly updated. 1123 * @throws Turba_Exception 1124 */ 1125 public function save(Turba_Object $object) 1126 { 1127 $object_id = $this->_save($object); 1128 1129 if ($uid = $object->getValue('__uid')) { 1130 /* Update tags. */ 1131 if (!is_null($tags = $object->getValue('__tags'))) { 1132 $GLOBALS['injector']->getInstance('Turba_Tagger')->replaceTags( 1133 $uid, 1134 $tags, 1135 $this->getContactOwner(), 1136 'contact' 1137 ); 1138 } 1139 1140 /* Log the modification of this item in the history log. */ 1141 try { 1142 $GLOBALS['injector']->getInstance('Horde_History')->log($object->getGuid(), 1143 array('action' => 'modify'), 1144 true); 1145 } catch (Exception $e) { 1146 Horde::log($e, 'ERR'); 1147 } 1148 } 1149 1150 return $object_id; 1151 } 1152 1153 /** 1154 * Returns the criteria available for this source except '__key'. 1155 * 1156 * @return array An array containing the criteria. 1157 */ 1158 public function getCriteria() 1159 { 1160 return array_diff_key($this->map, array('__key' => true)); 1161 } 1162 1163 /** 1164 * Returns all non-composite fields for this source. Useful for importing 1165 * and exporting data, etc. 1166 * 1167 * @return array The field list. 1168 */ 1169 public function getFields() 1170 { 1171 return array_flip($this->fields); 1172 } 1173 1174 /** 1175 * Exports a given Turba_Object as an iCalendar vCard. 1176 * 1177 * @param Turba_Object $object Turba_Object. 1178 * @param string $version The vcard version to produce. 1179 * @param array $fields Hash of field names and 1180 * Horde_SyncMl_Property properties with the 1181 * requested fields. 1182 * @param boolean $skipEmpty Whether to skip empty fields. 1183 * 1184 * @return Horde_Icalendar_Vcard A vcard object. 1185 */ 1186 public function tovCard(Turba_Object $object, $version = '2.1', 1187 array $fields = null, $skipEmpty = false) 1188 { 1189 global $injector; 1190 1191 $hash = $object->getAttributes(); 1192 $attributes = array_keys($this->map); 1193 $vcard = new Horde_Icalendar_Vcard($version); 1194 $formattedname = false; 1195 $charset = ($version == '2.1') 1196 ? array('CHARSET' => 'UTF-8') 1197 : array(); 1198 1199 $hooks = $injector->getInstance('Horde_Core_Hooks'); 1200 $decode_hook = $hooks->hookExists('decode_attribute', 'turba'); 1201 1202 // Tags are stored externally to Turba, so they don't appear in the 1203 // source map. 1204 $attributes[] = '__tags'; 1205 1206 foreach ($attributes as $key) { 1207 $val = $object->getValue($key); 1208 if ($skipEmpty && !is_array($val) && !strlen($val)) { 1209 continue; 1210 } 1211 if ($decode_hook) { 1212 try { 1213 $val = $hooks->callHook( 1214 'decode_attribute', 1215 'turba', 1216 array($key, $val, $object) 1217 ); 1218 } catch (Turba_Exception $e) {} 1219 } 1220 switch ($key) { 1221 case '__uid': 1222 $vcard->setAttribute('UID', $val); 1223 break; 1224 1225 case 'name': 1226 if ($fields && !isset($fields['FN'])) { 1227 break; 1228 } 1229 $vcard->setAttribute('FN', $val, Horde_Mime::is8bit($val) ? $charset : array()); 1230 $formattedname = true; 1231 break; 1232 1233 case 'nickname': 1234 case 'alias': 1235 $params = Horde_Mime::is8bit($val) ? $charset : array(); 1236 if (!$fields || isset($fields['NICKNAME'])) { 1237 $vcard->setAttribute('NICKNAME', $val, $params); 1238 } 1239 if (!$fields || isset($fields['X-EPOCSECONDNAME'])) { 1240 $vcard->setAttribute('X-EPOCSECONDNAME', $val, $params); 1241 } 1242 break; 1243 1244 case 'homeAddress': 1245 if ($fields && 1246 (!isset($fields['LABEL']) || 1247 (isset($fields['LABEL']->Params['TYPE']) && 1248 !$this->_hasValEnum($fields['LABEL']->Params['TYPE']->ValEnum, 'HOME')))) { 1249 break; 1250 } 1251 if ($version == '2.1') { 1252 $vcard->setAttribute('LABEL', $val, array('HOME' => null)); 1253 } else { 1254 $vcard->setAttribute('LABEL', $val, array('TYPE' => 'HOME')); 1255 } 1256 break; 1257 1258 case 'workAddress': 1259 if ($fields && 1260 (!isset($fields['LABEL']) || 1261 (isset($fields['LABEL']->Params['TYPE']) && 1262 !$this->_hasValEnum($fields['LABEL']->Params['TYPE']->ValEnum, 'WORK')))) { 1263 break; 1264 } 1265 if ($version == '2.1') { 1266 $vcard->setAttribute('LABEL', $val, array('WORK' => null)); 1267 } else { 1268 $vcard->setAttribute('LABEL', $val, array('TYPE' => 'WORK')); 1269 } 1270 break; 1271 1272 case 'otherAddress': 1273 if ($fields && !isset($fields['LABEL'])) { 1274 break; 1275 } 1276 $vcard->setAttribute('LABEL', $val); 1277 break; 1278 1279 case 'phone': 1280 if ($fields && !isset($fields['TEL'])) { 1281 break; 1282 } 1283 $vcard->setAttribute('TEL', $val); 1284 break; 1285 1286 case 'homePhone': 1287 if ($fields && 1288 (!isset($fields['TEL']) || 1289 (isset($fields['TEL']->Params['TYPE']) && 1290 !$this->_hasValEnum($fields['TEL']->Params['TYPE']->ValEnum, 'HOME')))) { 1291 break; 1292 } 1293 if ($version == '2.1') { 1294 $vcard->setAttribute('TEL', $val, array('HOME' => null, 'VOICE' => null)); 1295 } else { 1296 $vcard->setAttribute('TEL', $val, array('TYPE' => array('HOME', 'VOICE'))); 1297 } 1298 break; 1299 1300 case 'workPhone': 1301 if ($fields && 1302 (!isset($fields['TEL']) || 1303 (isset($fields['TEL']->Params['TYPE']) && 1304 !$this->_hasValEnum($fields['TEL']->Params['TYPE']->ValEnum, 'WORK')))) { 1305 break; 1306 } 1307 if ($version == '2.1') { 1308 $vcard->setAttribute('TEL', $val, array('WORK' => null, 'VOICE' => null)); 1309 } else { 1310 $vcard->setAttribute('TEL', $val, array('TYPE' => array('WORK', 'VOICE'))); 1311 } 1312 break; 1313 1314 case 'cellPhone': 1315 if ($fields && 1316 (!isset($fields['TEL']) || 1317 (isset($fields['TEL']->Params['TYPE']) && 1318 !$this->_hasValEnum($fields['TEL']->Params['TYPE']->ValEnum, 'CELL')))) { 1319 break; 1320 } 1321 if ($version == '2.1') { 1322 $vcard->setAttribute('TEL', $val, array('CELL' => null, 'VOICE' => null)); 1323 } else { 1324 $vcard->setAttribute('TEL', $val, array('TYPE' => array('CELL', 'VOICE'))); 1325 } 1326 break; 1327 1328 case 'homeCellPhone': 1329 $parameters = array(); 1330 if ($fields) { 1331 if (!isset($fields['TEL'])) { 1332 break; 1333 } 1334 if (!isset($fields['TEL']->Params['TYPE']) || 1335 $this->_hasValEnum($fields['TEL']->Params['TYPE']->ValEnum, 'CELL')) { 1336 if ($version == '2.1') { 1337 $parameters['CELL'] = null; 1338 $parameters['VOICE'] = null; 1339 } else { 1340 $parameters['TYPE'] = array('CELL', 'VOICE'); 1341 } 1342 } 1343 if (!isset($fields['TEL']->Params['TYPE']) || 1344 $this->_hasValEnum($fields['TEL']->Params['TYPE']->ValEnum, 'HOME')) { 1345 if ($version == '2.1') { 1346 $parameters['HOME'] = null; 1347 $parameters['VOICE'] = null; 1348 } else { 1349 $parameters['TYPE'] = array('HOME', 'VOICE'); 1350 } 1351 } 1352 if (empty($parameters)) { 1353 break; 1354 } 1355 } else { 1356 if ($version == '2.1') { 1357 $parameters = array('CELL' => null, 'HOME' => null, 'VOICE' => null); 1358 } else { 1359 $parameters = array('TYPE' => array('CELL', 'HOME', 'VOICE')); 1360 } 1361 } 1362 $vcard->setAttribute('TEL', $val, $parameters); 1363 break; 1364 1365 case 'workCellPhone': 1366 $parameters = array(); 1367 if ($fields) { 1368 if (!isset($fields['TEL'])) { 1369 break; 1370 } 1371 if (!isset($fields['TEL']->Params['TYPE']) || 1372 $this->_hasValEnum($fields['TEL']->Params['TYPE']->ValEnum, 'CELL')) { 1373 if ($version == '2.1') { 1374 $parameters['CELL'] = null; 1375 $parameters['VOICE'] = null; 1376 } else { 1377 $parameters['TYPE'] = array('CELL', 'VOICE'); 1378 } 1379 } 1380 if (!isset($fields['TEL']->Params['TYPE']) || 1381 $this->_hasValEnum($fields['TEL']->Params['TYPE']->ValEnum, 'WORK')) { 1382 if ($version == '2.1') { 1383 $parameters['WORK'] = null; 1384 $parameters['VOICE'] = null; 1385 } else { 1386 $parameters['TYPE'] = array('WORK', 'VOICE'); 1387 } 1388 } 1389 if (empty($parameters)) { 1390 break; 1391 } 1392 } else { 1393 if ($version == '2.1') { 1394 $parameters = array('CELL' => null, 'WORK' => null, 'VOICE' => null); 1395 } else { 1396 $parameters = array('TYPE' => array('CELL', 'WORK', 'VOICE')); 1397 } 1398 } 1399 $vcard->setAttribute('TEL', $val, $parameters); 1400 break; 1401 1402 case 'videoCall': 1403 if ($fields && 1404 (!isset($fields['TEL']) || 1405 (isset($fields['TEL']->Params['TYPE']) && 1406 !$this->_hasValEnum($fields['TEL']->Params['TYPE']->ValEnum, 'VIDEO')))) { 1407 break; 1408 } 1409 if ($version == '2.1') { 1410 $vcard->setAttribute('TEL', $val, array('VIDEO' => null)); 1411 } else { 1412 $vcard->setAttribute('TEL', $val, array('TYPE' => 'VIDEO')); 1413 } 1414 break; 1415 1416 case 'homeVideoCall': 1417 $parameters = array(); 1418 if ($fields) { 1419 if (!isset($fields['TEL'])) { 1420 break; 1421 } 1422 if (!isset($fields['TEL']->Params['TYPE']) || 1423 $this->_hasValEnum($fields['TEL']->Params['TYPE']->ValEnum, 'VIDEO')) { 1424 if ($version == '2.1') { 1425 $parameters['VIDEO'] = null; 1426 } else { 1427 $parameters['TYPE'] = 'VIDEO'; 1428 } 1429 } 1430 if (!isset($fields['TEL']->Params['TYPE']) || 1431 $this->_hasValEnum($fields['TEL']->Params['TYPE']->ValEnum, 'HOME')) { 1432 if ($version == '2.1') { 1433 $parameters['HOME'] = null; 1434 } else { 1435 $parameters['TYPE'] = 'HOME'; 1436 } 1437 } 1438 if (empty($parameters)) { 1439 break; 1440 } 1441 } else { 1442 if ($version == '2.1') { 1443 $parameters = array('VIDEO' => null, 'HOME' => null); 1444 } else { 1445 $parameters = array('TYPE' => array('VIDEO', 'HOME')); 1446 } 1447 } 1448 $vcard->setAttribute('TEL', $val, $parameters); 1449 break; 1450 1451 case 'workVideoCall': 1452 $parameters = array(); 1453 if ($fields) { 1454 if (!isset($fields['TEL'])) { 1455 break; 1456 } 1457 if (!isset($fields['TEL']->Params['TYPE']) || 1458 $this->_hasValEnum($fields['TEL']->Params['TYPE']->ValEnum, 'VIDEO')) { 1459 if ($version == '2.1') { 1460 $parameters['VIDEO'] = null; 1461 } else { 1462 $parameters['TYPE'] = 'VIDEO'; 1463 } 1464 } 1465 if (!isset($fields['TEL']->Params['TYPE']) || 1466 $this->_hasValEnum($fields['TEL']->Params['TYPE']->ValEnum, 'WORK')) { 1467 if ($version == '2.1') { 1468 $parameters['WORK'] = null; 1469 } else { 1470 $parameters['TYPE'] = 'WORK'; 1471 } 1472 } 1473 if (empty($parameters)) { 1474 break; 1475 } 1476 } else { 1477 if ($version == '2.1') { 1478 $parameters = array('VIDEO' => null, 'WORK' => null); 1479 } else { 1480 $parameters = array('TYPE' => array('VIDEO', 'WORK')); 1481 } 1482 } 1483 $vcard->setAttribute('TEL', $val, $parameters); 1484 break; 1485 1486 case 'sip': 1487 if ($fields && !isset($fields['X-SIP'])) { 1488 break; 1489 } 1490 $vcard->setAttribute('X-SIP', $val); 1491 break; 1492 case 'ptt': 1493 if ($fields && 1494 (!isset($fields['X-SIP']) || 1495 (isset($fields['X-SIP']->Params['TYPE']) && 1496 !$this->_hasValEnum($fields['X-SIP']->Params['TYPE']->ValEnum, 'POC')))) { 1497 break; 1498 } 1499 if ($version == '2.1') { 1500 $vcard->setAttribute('X-SIP', $val, array('POC' => null)); 1501 } else { 1502 $vcard->setAttribute('X-SIP', $val, array('TYPE' => 'POC')); 1503 } 1504 break; 1505 1506 case 'voip': 1507 if ($fields && 1508 (!isset($fields['X-SIP']) || 1509 (isset($fields['X-SIP']->Params['TYPE']) && 1510 !$this->_hasValEnum($fields['X-SIP']->Params['TYPE']->ValEnum, 'VOIP')))) { 1511 break; 1512 } 1513 if ($version == '2.1') { 1514 $vcard->setAttribute('X-SIP', $val, array('VOIP' => null)); 1515 } else { 1516 $vcard->setAttribute('X-SIP', $val, array('TYPE' => 'VOIP')); 1517 } 1518 break; 1519 1520 case 'shareView': 1521 if ($fields && 1522 (!isset($fields['X-SIP']) || 1523 (isset($fields['X-SIP']->Params['TYPE']) && 1524 !$this->_hasValEnum($fields['X-SIP']->Params['TYPE']->ValEnum, 'SWIS')))) { 1525 break; 1526 } 1527 if ($version == '2.1') { 1528 $vcard->setAttribute('X-SIP', $val, array('SWIS' => null)); 1529 } else { 1530 $vcard->setAttribute('X-SIP', $val, array('TYPE' => 'SWIS')); 1531 } 1532 break; 1533 1534 case 'imaddress': 1535 if ($fields && !isset($fields['X-WV-ID'])) { 1536 break; 1537 } 1538 $vcard->setAttribute('X-WV-ID', $val); 1539 break; 1540 1541 case 'fax': 1542 if ($fields && 1543 (!isset($fields['TEL']) || 1544 (isset($fields['TEL']->Params['TYPE']) && 1545 !$this->_hasValEnum($fields['TEL']->Params['TYPE']->ValEnum, 'FAX')))) { 1546 break; 1547 } 1548 if ($version == '2.1') { 1549 $vcard->setAttribute('TEL', $val, array('FAX' => null)); 1550 } else { 1551 $vcard->setAttribute('TEL', $val, array('TYPE' => 'FAX')); 1552 } 1553 break; 1554 1555 case 'homeFax': 1556 $parameters = array(); 1557 if ($fields) { 1558 if (!isset($fields['TEL'])) { 1559 break; 1560 } 1561 if (!isset($fields['TEL']->Params['TYPE']) || 1562 $this->_hasValEnum($fields['TEL']->Params['TYPE']->ValEnum, 'FAX')) { 1563 if ($version == '2.1') { 1564 $parameters['FAX'] = null; 1565 } else { 1566 $parameters['TYPE'] = 'FAX'; 1567 } 1568 } 1569 if (!isset($fields['TEL']->Params['TYPE']) || 1570 $this->_hasValEnum($fields['TEL']->Params['TYPE']->ValEnum, 'HOME')) { 1571 if ($version == '2.1') { 1572 $parameters['HOME'] = null; 1573 } else { 1574 $parameters['TYPE'] = 'HOME'; 1575 } 1576 } 1577 if (empty($parameters)) { 1578 break; 1579 } 1580 } else { 1581 if ($version == '2.1') { 1582 $parameters = array('FAX' => null, 'HOME' => null); 1583 } else { 1584 $parameters = array('TYPE' => array('FAX', 'HOME')); 1585 } 1586 } 1587 $vcard->setAttribute('TEL', $val, $parameters); 1588 break; 1589 1590 case 'workFax': 1591 $parameters = array(); 1592 if ($fields) { 1593 if (!isset($fields['TEL'])) { 1594 break; 1595 } 1596 if (!isset($fields['TEL']->Params['TYPE']) || 1597 $this->_hasValEnum($fields['TEL']->Params['TYPE']->ValEnum, 'FAX')) { 1598 if ($version == '2.1') { 1599 $parameters['FAX'] = null; 1600 } else { 1601 $parameters['TYPE'] = 'FAX'; 1602 } 1603 } 1604 if (!isset($fields['TEL']->Params['TYPE']) || 1605 $this->_hasValEnum($fields['TEL']->Params['TYPE']->ValEnum, 'WORK')) { 1606 if ($version == '2.1') { 1607 $parameters['WORK'] = null; 1608 } else { 1609 $parameters['TYPE'] = 'WORK'; 1610 } 1611 } 1612 if (empty($parameters)) { 1613 break; 1614 } 1615 } else { 1616 if ($version == '2.1') { 1617 $parameters = array('FAX' => null, 'WORK' => null); 1618 } else { 1619 $parameters = array('TYPE' => array('FAX', 'WORK')); 1620 } 1621 } 1622 $vcard->setAttribute('TEL', $val, $parameters); 1623 if ($version == '2.1') { 1624 $vcard->setAttribute('TEL', $val, array('FAX' => null, 'WORK' => null)); 1625 } else { 1626 $vcard->setAttribute('TEL', $val, array('TYPE' => array('FAX', 'WORK'))); 1627 } 1628 break; 1629 1630 case 'pager': 1631 if ($fields && 1632 (!isset($fields['TEL']) || 1633 (isset($fields['TEL']->Params['TYPE']) && 1634 !$this->_hasValEnum($fields['TEL']->Params['TYPE']->ValEnum, 'PAGER')))) { 1635 break; 1636 } 1637 if ($version == '2.1') { 1638 $vcard->setAttribute('TEL', $val, array('PAGER' => null)); 1639 } else { 1640 $vcard->setAttribute('TEL', $val, array('TYPE' => 'PAGER')); 1641 } 1642 break; 1643 1644 case 'email': 1645 if ($fields && !isset($fields['EMAIL'])) { 1646 break; 1647 } 1648 if ($version == '2.1') { 1649 $vcard->setAttribute( 1650 'EMAIL', 1651 Horde_Icalendar_Vcard::getBareEmail($val), 1652 array('INTERNET' => null)); 1653 } else { 1654 $vcard->setAttribute( 1655 'EMAIL', 1656 Horde_Icalendar_Vcard::getBareEmail($val), 1657 array('TYPE' => 'INTERNET')); 1658 } 1659 break; 1660 1661 case 'homeEmail': 1662 if ($fields && 1663 (!isset($fields['EMAIL']) || 1664 (isset($fields['EMAIL']->Params['TYPE']) && 1665 !$this->_hasValEnum($fields['EMAIL']->Params['TYPE']->ValEnum, 'HOME')))) { 1666 break; 1667 } 1668 if ($version == '2.1') { 1669 $vcard->setAttribute('EMAIL', 1670 Horde_Icalendar_Vcard::getBareEmail($val), 1671 array('HOME' => null)); 1672 } else { 1673 $vcard->setAttribute('EMAIL', 1674 Horde_Icalendar_Vcard::getBareEmail($val), 1675 array('TYPE' => 'HOME')); 1676 } 1677 break; 1678 1679 case 'workEmail': 1680 if ($fields && 1681 (!isset($fields['EMAIL']) || 1682 (isset($fields['EMAIL']->Params['TYPE']) && 1683 !$this->_hasValEnum($fields['EMAIL']->Params['TYPE']->ValEnum, 'WORK')))) { 1684 break; 1685 } 1686 if ($version == '2.1') { 1687 $vcard->setAttribute('EMAIL', 1688 Horde_Icalendar_Vcard::getBareEmail($val), 1689 array('WORK' => null)); 1690 } else { 1691 $vcard->setAttribute('EMAIL', 1692 Horde_Icalendar_Vcard::getBareEmail($val), 1693 array('TYPE' => 'WORK')); 1694 } 1695 break; 1696 1697 case 'emails': 1698 if ($fields && !isset($fields['EMAIL'])) { 1699 break; 1700 } 1701 $emails = explode(',', $val); 1702 foreach ($emails as $email) { 1703 $vcard->setAttribute('EMAIL', Horde_Icalendar_Vcard::getBareEmail($email)); 1704 } 1705 break; 1706 1707 case 'title': 1708 if ($fields && !isset($fields['TITLE'])) { 1709 break; 1710 } 1711 $vcard->setAttribute('TITLE', $val, Horde_Mime::is8bit($val) ? $charset : array()); 1712 break; 1713 1714 case 'role': 1715 if ($fields && !isset($fields['ROLE'])) { 1716 break; 1717 } 1718 $vcard->setAttribute('ROLE', $val, Horde_Mime::is8bit($val) ? $charset : array()); 1719 break; 1720 1721 case 'notes': 1722 if ($fields && !isset($fields['NOTE'])) { 1723 break; 1724 } 1725 $vcard->setAttribute('NOTE', $val, Horde_Mime::is8bit($val) ? $charset : array()); 1726 break; 1727 1728 case '__tags': 1729 $val = $injector->getInstance('Turba_Tagger')->split($val); 1730 case 'businessCategory': 1731 // No CATEGORIES in vCard 2.1 1732 if ($version == '2.1' || 1733 ($fields && !isset($fields['CATEGORIES']))) { 1734 break; 1735 } 1736 $vcard->setAttribute('CATEGORIES', null, array(), true, $val); 1737 break; 1738 1739 case 'anniversary': 1740 if (!$fields || isset($fields['X-ANNIVERSARY'])) { 1741 $vcard->setAttribute('X-ANNIVERSARY', $val); 1742 } 1743 break; 1744 1745 case 'spouse': 1746 if (!$fields || isset($fields['X-SPOUSE'])) { 1747 $vcard->setAttribute('X-SPOUSE', $val); 1748 } 1749 break; 1750 1751 case 'children': 1752 if (!$fields || isset($fields['X-CHILDREN'])) { 1753 $vcard->setAttribute('X-CHILDREN', $val); 1754 } 1755 break; 1756 1757 case 'website': 1758 if ($fields && !isset($fields['URL'])) { 1759 break; 1760 } 1761 $vcard->setAttribute('URL', $val); 1762 break; 1763 1764 case 'homeWebsite': 1765 if ($fields && 1766 (!isset($fields['URL']) || 1767 (isset($fields['URL']->Params['TYPE']) && 1768 !$this->_hasValEnum($fields['URL']->Params['TYPE']->ValEnum, 'HOME')))) { 1769 break; 1770 } 1771 if ($version == '2.1') { 1772 $vcard->setAttribute('URL', $val, array('HOME' => null)); 1773 } else { 1774 $vcard->setAttribute('URL', $val, array('TYPE' => 'HOME')); 1775 } 1776 break; 1777 1778 case 'workWebsite': 1779 if ($fields && 1780 (!isset($fields['URL']) || 1781 (isset($fields['URL']->Params['TYPE']) && 1782 !$this->_hasValEnum($fields['URL']->Params['TYPE']->ValEnum, 'WORK')))) { 1783 break; 1784 } 1785 if ($version == '2.1') { 1786 $vcard->setAttribute('URL', $val, array('WORK' => null)); 1787 } else { 1788 $vcard->setAttribute('URL', $val, array('TYPE' => 'WORK')); 1789 } 1790 break; 1791 1792 case 'freebusyUrl': 1793 if ($version == '2.1' || 1794 ($fields && !isset($fields['FBURL']))) { 1795 break; 1796 } 1797 $vcard->setAttribute('FBURL', $val); 1798 break; 1799 1800 case 'birthday': 1801 if ($fields && !isset($fields['BDAY'])) { 1802 break; 1803 } 1804 $vcard->setAttribute('BDAY', $val); 1805 break; 1806 1807 case 'timezone': 1808 if ($fields && !isset($fields['TZ'])) { 1809 break; 1810 } 1811 $vcard->setAttribute('TZ', $val, array('VALUE' => 'text')); 1812 break; 1813 1814 case 'latitude': 1815 if ($fields && !isset($fields['GEO'])) { 1816 break; 1817 } 1818 if (isset($hash['longitude'])) { 1819 $vcard->setAttribute('GEO', 1820 array('latitude' => $val, 1821 'longitude' => $hash['longitude'])); 1822 } 1823 break; 1824 1825 case 'homeLatitude': 1826 if ($fields && 1827 (!isset($fields['GEO']) || 1828 (isset($fields['GEO']->Params['TYPE']) && 1829 !$this->_hasValEnum($fields['GEO']->Params['TYPE']->ValEnum, 'HOME')))) { 1830 break; 1831 } 1832 if (isset($hash['homeLongitude'])) { 1833 if ($version == '2.1') { 1834 $vcard->setAttribute('GEO', 1835 array('latitude' => $val, 1836 'longitude' => $hash['homeLongitude']), 1837 array('HOME' => null)); 1838 } else { 1839 $vcard->setAttribute('GEO', 1840 array('latitude' => $val, 1841 'longitude' => $hash['homeLongitude']), 1842 array('TYPE' => 'HOME')); 1843 } 1844 } 1845 break; 1846 1847 case 'workLatitude': 1848 if ($fields && 1849 (!isset($fields['GEO']) || 1850 (isset($fields['GEO']->Params['TYPE']) && 1851 !$this->_hasValEnum($fields['GEO']->Params['TYPE']->ValEnum, 'HOME')))) { 1852 break; 1853 } 1854 if (isset($hash['workLongitude'])) { 1855 if ($version == '2.1') { 1856 $vcard->setAttribute('GEO', 1857 array('latitude' => $val, 1858 'longitude' => $hash['workLongitude']), 1859 array('WORK' => null)); 1860 } else { 1861 $vcard->setAttribute('GEO', 1862 array('latitude' => $val, 1863 'longitude' => $hash['workLongitude']), 1864 array('TYPE' => 'WORK')); 1865 } 1866 } 1867 break; 1868 1869 case 'photo': 1870 case 'logo': 1871 $name = Horde_String::upper($key); 1872 $params = array(); 1873 if (strlen($hash[$key])) { 1874 $params['ENCODING'] = 'b'; 1875 } 1876 if (isset($hash[$key . 'type'])) { 1877 $params['TYPE'] = $hash[$key . 'type']; 1878 } 1879 if ($fields && 1880 (!isset($fields[$name]) || 1881 (isset($params['TYPE']) && 1882 isset($fields[$name]->Params['TYPE']) && 1883 !$this->_hasValEnum($fields[$name]->Params['TYPE']->ValEnum, $params['TYPE'])))) { 1884 break; 1885 } 1886 $vcard->setAttribute($name, 1887 base64_encode($hash[$key]), 1888 $params); 1889 break; 1890 } 1891 } 1892 1893 // No explicit firstname/lastname in data source: we have to guess. 1894 if (!isset($hash['lastname']) && isset($hash['name'])) { 1895 $this->_guessName($hash); 1896 } 1897 1898 $a = array( 1899 Horde_Icalendar_Vcard::N_FAMILY => isset($hash['lastname']) ? $hash['lastname'] : '', 1900 Horde_Icalendar_Vcard::N_GIVEN => isset($hash['firstname']) ? $hash['firstname'] : '', 1901 Horde_Icalendar_Vcard::N_ADDL => isset($hash['middlenames']) ? $hash['middlenames'] : '', 1902 Horde_Icalendar_Vcard::N_PREFIX => isset($hash['namePrefix']) ? $hash['namePrefix'] : '', 1903 Horde_Icalendar_Vcard::N_SUFFIX => isset($hash['nameSuffix']) ? $hash['nameSuffix'] : '', 1904 ); 1905 $val = implode(';', $a); 1906 if (!$fields || isset($fields['N'])) { 1907 $vcard->setAttribute('N', $val, Horde_Mime::is8bit($val) ? $charset : array(), false, $a); 1908 } 1909 1910 if (!$formattedname && (!$fields || isset($fields['FN']))) { 1911 if ($object->getValue('name')) { 1912 $val = $object->getValue('name'); 1913 } elseif (!empty($this->alternativeName) && 1914 isset($hash[$this->alternativeName])) { 1915 $val = $hash[$this->alternativeName]; 1916 } else { 1917 $val = ''; 1918 } 1919 $vcard->setAttribute('FN', $val, Horde_Mime::is8bit($val) ? $charset : array()); 1920 } 1921 1922 $org = array(); 1923 if (!empty($hash['company']) || 1924 (!$skipEmpty && array_key_exists('company', $hash))) { 1925 $org[] = $hash['company']; 1926 } 1927 if (!empty($hash['department']) || 1928 (!$skipEmpty && array_key_exists('department', $hash))) { 1929 $org[] = $hash['department']; 1930 } 1931 if (count($org) && (!$fields || isset($fields['ORG']))) { 1932 $val = implode(';', $org); 1933 $vcard->setAttribute('ORG', $val, Horde_Mime::is8bit($val) ? $charset : array(), false, $org); 1934 } 1935 1936 if ((!$fields || isset($fields['ADR'])) && 1937 (!empty($hash['commonAddress']) || 1938 !empty($hash['commonStreet']) || 1939 !empty($hash['commonPOBox']) || 1940 !empty($hash['commonExtended']) || 1941 !empty($hash['commonCity']) || 1942 !empty($hash['commonProvince']) || 1943 !empty($hash['commonPostalCode']) || 1944 !empty($hash['commonCountry']) || 1945 (!$skipEmpty && 1946 (array_key_exists('commonAddress', $hash) || 1947 array_key_exists('commonStreet', $hash) || 1948 array_key_exists('commonPOBox', $hash) || 1949 array_key_exists('commonExtended', $hash) || 1950 array_key_exists('commonCity', $hash) || 1951 array_key_exists('commonProvince', $hash) || 1952 array_key_exists('commonPostalCode', $hash) || 1953 array_key_exists('commonCountry', $hash))))) { 1954 /* We can't know if this particular Turba source uses a single 1955 * address field or multiple for 1956 * street/city/province/postcode/country. Try to deal with 1957 * both. */ 1958 if (isset($hash['commonAddress']) && 1959 !isset($hash['commonStreet'])) { 1960 $hash['commonStreet'] = $hash['commonAddress']; 1961 } 1962 $a = array( 1963 Horde_Icalendar_Vcard::ADR_POB => isset($hash['commonPOBox']) 1964 ? $hash['commonPOBox'] : '', 1965 Horde_Icalendar_Vcard::ADR_EXTEND => isset($hash['commonExtended']) 1966 ? $hash['commonExtended'] : '', 1967 Horde_Icalendar_Vcard::ADR_STREET => isset($hash['commonStreet']) 1968 ? $hash['commonStreet'] : '', 1969 Horde_Icalendar_Vcard::ADR_LOCALITY => isset($hash['commonCity']) 1970 ? $hash['commonCity'] : '', 1971 Horde_Icalendar_Vcard::ADR_REGION => isset($hash['commonProvince']) 1972 ? $hash['commonProvince'] : '', 1973 Horde_Icalendar_Vcard::ADR_POSTCODE => isset($hash['commonPostalCode']) 1974 ? $hash['commonPostalCode'] : '', 1975 Horde_Icalendar_Vcard::ADR_COUNTRY => isset($hash['commonCountry']) 1976 ? Horde_Nls::getCountryISO($hash['commonCountry']) : '', 1977 ); 1978 1979 $val = implode(';', $a); 1980 if ($version == '2.1') { 1981 $params = array(); 1982 if (Horde_Mime::is8bit($val)) { 1983 $params['CHARSET'] = 'UTF-8'; 1984 } 1985 } else { 1986 $params = array('TYPE' => ''); 1987 } 1988 $vcard->setAttribute('ADR', $val, $params, true, $a); 1989 } 1990 1991 if ((!$fields || 1992 (isset($fields['ADR']) && 1993 (!isset($fields['ADR']->Params['TYPE']) || 1994 $this->_hasValEnum($fields['ADR']->Params['TYPE']->ValEnum, 'HOME')))) && 1995 (!empty($hash['homeAddress']) || 1996 !empty($hash['homeStreet']) || 1997 !empty($hash['homePOBox']) || 1998 !empty($hash['homeExtended']) || 1999 !empty($hash['homeCity']) || 2000 !empty($hash['homeProvince']) || 2001 !empty($hash['homePostalCode']) || 2002 !empty($hash['homeCountry']) || 2003 (!$skipEmpty && 2004 (array_key_exists('homeAddress', $hash) || 2005 array_key_exists('homeStreet', $hash) || 2006 array_key_exists('homePOBox', $hash) || 2007 array_key_exists('homeExtended', $hash) || 2008 array_key_exists('homeCity', $hash) || 2009 array_key_exists('homeProvince', $hash) || 2010 array_key_exists('homePostalCode', $hash) || 2011 array_key_exists('homeCountry', $hash))))) { 2012 if (isset($hash['homeAddress']) && !isset($hash['homeStreet'])) { 2013 $hash['homeStreet'] = $hash['homeAddress']; 2014 } 2015 $a = array( 2016 Horde_Icalendar_Vcard::ADR_POB => isset($hash['homePOBox']) 2017 ? $hash['homePOBox'] : '', 2018 Horde_Icalendar_Vcard::ADR_EXTEND => isset($hash['homeExtended']) 2019 ? $hash['homeExtended'] : '', 2020 Horde_Icalendar_Vcard::ADR_STREET => isset($hash['homeStreet']) 2021 ? $hash['homeStreet'] : '', 2022 Horde_Icalendar_Vcard::ADR_LOCALITY => isset($hash['homeCity']) 2023 ? $hash['homeCity'] : '', 2024 Horde_Icalendar_Vcard::ADR_REGION => isset($hash['homeProvince']) 2025 ? $hash['homeProvince'] : '', 2026 Horde_Icalendar_Vcard::ADR_POSTCODE => isset($hash['homePostalCode']) 2027 ? $hash['homePostalCode'] : '', 2028 Horde_Icalendar_Vcard::ADR_COUNTRY => isset($hash['homeCountry']) 2029 ? Horde_Nls::getCountryISO($hash['homeCountry']) : '', 2030 ); 2031 2032 $val = implode(';', $a); 2033 if ($version == '2.1') { 2034 $params = array('HOME' => null); 2035 if (Horde_Mime::is8bit($val)) { 2036 $params['CHARSET'] = 'UTF-8'; 2037 } 2038 } else { 2039 $params = array('TYPE' => 'HOME'); 2040 } 2041 $vcard->setAttribute('ADR', $val, $params, true, $a); 2042 } 2043 2044 if ((!$fields || 2045 (isset($fields['ADR']) && 2046 (!isset($fields['ADR']->Params['TYPE']) || 2047 $this->_hasValEnum($fields['ADR']->Params['TYPE']->ValEnum, 'WORK')))) && 2048 (!empty($hash['workAddress']) || 2049 !empty($hash['workStreet']) || 2050 !empty($hash['workPOBox']) || 2051 !empty($hash['workExtended']) || 2052 !empty($hash['workCity']) || 2053 !empty($hash['workProvince']) || 2054 !empty($hash['workPostalCode']) || 2055 !empty($hash['workCountry']) || 2056 (!$skipEmpty && 2057 (array_key_exists('workAddress', $hash) || 2058 array_key_exists('workStreet', $hash) || 2059 array_key_exists('workPOBox', $hash) || 2060 array_key_exists('workExtended', $hash) || 2061 array_key_exists('workCity', $hash) || 2062 array_key_exists('workProvince', $hash) || 2063 array_key_exists('workPostalCode', $hash) || 2064 array_key_exists('workCountry', $hash))))) { 2065 if (isset($hash['workAddress']) && !isset($hash['workStreet'])) { 2066 $hash['workStreet'] = $hash['workAddress']; 2067 } 2068 $a = array( 2069 Horde_Icalendar_Vcard::ADR_POB => isset($hash['workPOBox']) 2070 ? $hash['workPOBox'] : '', 2071 Horde_Icalendar_Vcard::ADR_EXTEND => isset($hash['workExtended']) 2072 ? $hash['workExtended'] : '', 2073 Horde_Icalendar_Vcard::ADR_STREET => isset($hash['workStreet']) 2074 ? $hash['workStreet'] : '', 2075 Horde_Icalendar_Vcard::ADR_LOCALITY => isset($hash['workCity']) 2076 ? $hash['workCity'] : '', 2077 Horde_Icalendar_Vcard::ADR_REGION => isset($hash['workProvince']) 2078 ? $hash['workProvince'] : '', 2079 Horde_Icalendar_Vcard::ADR_POSTCODE => isset($hash['workPostalCode']) 2080 ? $hash['workPostalCode'] : '', 2081 Horde_Icalendar_Vcard::ADR_COUNTRY => isset($hash['workCountry']) 2082 ? Horde_Nls::getCountryISO($hash['workCountry']) : '', 2083 ); 2084 2085 $val = implode(';', $a); 2086 if ($version == '2.1') { 2087 $params = array('WORK' => null); 2088 if (Horde_Mime::is8bit($val)) { 2089 $params['CHARSET'] = 'UTF-8'; 2090 } 2091 } else { 2092 $params = array('TYPE' => 'WORK'); 2093 } 2094 $vcard->setAttribute('ADR', $val, $params, true, $a); 2095 } 2096 2097 return $vcard; 2098 } 2099 2100 /** 2101 * Returns whether a ValEnum entry from a DevInf object contains a certain 2102 * type. 2103 * 2104 * @param array $valEnum A ValEnum hash. 2105 * @param string $type A requested attribute type. 2106 * 2107 * @return boolean True if $type exists in $valEnum. 2108 */ 2109 protected function _hasValEnum($valEnum, $type) 2110 { 2111 foreach (array_keys($valEnum) as $key) { 2112 if (in_array($type, explode(',', $key))) { 2113 return true; 2114 } 2115 } 2116 return false; 2117 } 2118 2119 /** 2120 * Function to convert a Horde_Icalendar_Vcard object into a Turba 2121 * Object Hash with Turba attributes suitable as a parameter for add(). 2122 * 2123 * @see add() 2124 * 2125 * @param Horde_Icalendar_Vcard $vcard The Horde_Icalendar_Vcard object 2126 * to parse. 2127 * 2128 * @return array A Turba attribute hash. 2129 */ 2130 public function toHash(Horde_Icalendar_Vcard $vcard) 2131 { 2132 global $attributes; 2133 2134 $hash = array(); 2135 $attr = $vcard->getAllAttributes(); 2136 2137 foreach ($attr as $item) { 2138 switch ($item['name']) { 2139 case 'UID': 2140 $hash['__uid'] = $item['value']; 2141 break; 2142 2143 case 'FN': 2144 $hash['name'] = $item['value']; 2145 break; 2146 2147 case 'N': 2148 $name = $item['values']; 2149 if (!empty($name[Horde_Icalendar_Vcard::N_FAMILY])) { 2150 $hash['lastname'] = $name[Horde_Icalendar_Vcard::N_FAMILY]; 2151 } 2152 if (!empty($name[Horde_Icalendar_Vcard::N_GIVEN])) { 2153 $hash['firstname'] = $name[Horde_Icalendar_Vcard::N_GIVEN]; 2154 } 2155 if (!empty($name[Horde_Icalendar_Vcard::N_ADDL])) { 2156 $hash['middlenames'] = $name[Horde_Icalendar_Vcard::N_ADDL]; 2157 } 2158 if (!empty($name[Horde_Icalendar_Vcard::N_PREFIX])) { 2159 $hash['namePrefix'] = $name[Horde_Icalendar_Vcard::N_PREFIX]; 2160 } 2161 if (!empty($name[Horde_Icalendar_Vcard::N_SUFFIX])) { 2162 $hash['nameSuffix'] = $name[Horde_Icalendar_Vcard::N_SUFFIX]; 2163 } 2164 break; 2165 2166 case 'NICKNAME': 2167 case 'X-EPOCSECONDNAME': 2168 $hash['nickname'] = $item['value']; 2169 $hash['alias'] = $item['value']; 2170 break; 2171 2172 // We use LABEL but also support ADR. 2173 case 'LABEL': 2174 if (isset($item['params']['HOME']) && !isset($hash['homeAddress'])) { 2175 $hash['homeAddress'] = $item['value']; 2176 } elseif (isset($item['params']['WORK']) && !isset($hash['workAddress'])) { 2177 $hash['workAddress'] = $item['value']; 2178 } elseif (!isset($hash['commonAddress'])) { 2179 $hash['commonAddress'] = $item['value']; 2180 } 2181 break; 2182 2183 case 'ADR': 2184 if (isset($item['params']['TYPE'])) { 2185 if (!is_array($item['params']['TYPE'])) { 2186 $item['params']['TYPE'] = array($item['params']['TYPE']); 2187 } 2188 } else { 2189 $item['params']['TYPE'] = array(); 2190 if (isset($item['params']['WORK'])) { 2191 $item['params']['TYPE'][] = 'WORK'; 2192 } 2193 if (isset($item['params']['HOME'])) { 2194 $item['params']['TYPE'][] = 'HOME'; 2195 } 2196 if (count($item['params']['TYPE']) == 0) { 2197 $item['params']['TYPE'][] = 'COMMON'; 2198 } 2199 } 2200 2201 $address = $item['values']; 2202 foreach ($item['params']['TYPE'] as $adr) { 2203 switch (Horde_String::upper($adr)) { 2204 case 'HOME': 2205 $prefix = 'home'; 2206 break; 2207 2208 case 'WORK': 2209 $prefix = 'work'; 2210 break; 2211 2212 default: 2213 $prefix = 'common'; 2214 } 2215 2216 if (isset($hash[$prefix . 'Address'])) { 2217 continue; 2218 } 2219 2220 $hash[$prefix . 'Address'] = ''; 2221 2222 if (!empty($address[Horde_Icalendar_Vcard::ADR_STREET])) { 2223 $hash[$prefix . 'Street'] = $address[Horde_Icalendar_Vcard::ADR_STREET]; 2224 $hash[$prefix . 'Address'] .= $hash[$prefix . 'Street'] . "\n"; 2225 } 2226 if (!empty($address[Horde_Icalendar_Vcard::ADR_EXTEND])) { 2227 $hash[$prefix . 'Extended'] = $address[Horde_Icalendar_Vcard::ADR_EXTEND]; 2228 $hash[$prefix . 'Address'] .= $hash[$prefix . 'Extended'] . "\n"; 2229 } 2230 if (!empty($address[Horde_Icalendar_Vcard::ADR_POB])) { 2231 $hash[$prefix . 'POBox'] = $address[Horde_Icalendar_Vcard::ADR_POB]; 2232 $hash[$prefix . 'Address'] .= $hash[$prefix . 'POBox'] . "\n"; 2233 } 2234 if (!empty($address[Horde_Icalendar_Vcard::ADR_LOCALITY])) { 2235 $hash[$prefix . 'City'] = $address[Horde_Icalendar_Vcard::ADR_LOCALITY]; 2236 $hash[$prefix . 'Address'] .= $hash[$prefix . 'City']; 2237 } 2238 if (!empty($address[Horde_Icalendar_Vcard::ADR_REGION])) { 2239 $hash[$prefix . 'Province'] = $address[Horde_Icalendar_Vcard::ADR_REGION]; 2240 $hash[$prefix . 'Address'] .= ', ' . $hash[$prefix . 'Province']; 2241 } 2242 if (!empty($address[Horde_Icalendar_Vcard::ADR_POSTCODE])) { 2243 $hash[$prefix . 'PostalCode'] = $address[Horde_Icalendar_Vcard::ADR_POSTCODE]; 2244 $hash[$prefix . 'Address'] .= ' ' . $hash[$prefix . 'PostalCode']; 2245 } 2246 if (!empty($address[Horde_Icalendar_Vcard::ADR_COUNTRY])) { 2247 include 'Horde/Nls/Countries.php'; 2248 $country = array_search($address[Horde_Icalendar_Vcard::ADR_COUNTRY], $countries); 2249 if ($country === false) { 2250 $country = $address[Horde_Icalendar_Vcard::ADR_COUNTRY]; 2251 } 2252 $hash[$prefix . 'Country'] = $country; 2253 $hash[$prefix . 'Address'] .= "\n" . $address[Horde_Icalendar_Vcard::ADR_COUNTRY]; 2254 } 2255 2256 $hash[$prefix . 'Address'] = trim($hash[$prefix . 'Address']); 2257 } 2258 break; 2259 2260 case 'TZ': 2261 // We only support textual timezones. 2262 if (!isset($item['params']['VALUE']) || 2263 Horde_String::lower($item['params']['VALUE']) != 'text') { 2264 break; 2265 } 2266 $timezones = explode(';', $item['value']); 2267 $available_timezones = Horde_Nls::getTimezones(); 2268 foreach ($timezones as $timezone) { 2269 $timezone = trim($timezone); 2270 if (isset($available_timezones[$timezone])) { 2271 $hash['timezone'] = $timezone; 2272 break 2; 2273 } 2274 } 2275 break; 2276 2277 case 'GEO': 2278 if (isset($item['params']['HOME'])) { 2279 $hash['homeLatitude'] = $item['value']['latitude']; 2280 $hash['homeLongitude'] = $item['value']['longitude']; 2281 } elseif (isset($item['params']['WORK'])) { 2282 $hash['workLatitude'] = $item['value']['latitude']; 2283 $hash['workLongitude'] = $item['value']['longitude']; 2284 } else { 2285 $hash['latitude'] = $item['value']['latitude']; 2286 $hash['longitude'] = $item['value']['longitude']; 2287 } 2288 break; 2289 2290 case 'TEL': 2291 if (isset($item['params']['FAX'])) { 2292 if (isset($item['params']['WORK']) && 2293 !isset($hash['workFax'])) { 2294 $hash['workFax'] = $item['value']; 2295 } elseif (isset($item['params']['HOME']) && 2296 !isset($hash['homeFax'])) { 2297 $hash['homeFax'] = $item['value']; 2298 } elseif (!isset($hash['fax'])) { 2299 $hash['fax'] = $item['value']; 2300 } 2301 } elseif (isset($item['params']['PAGER']) && 2302 !isset($hash['pager'])) { 2303 $hash['pager'] = $item['value']; 2304 } elseif (isset($item['params']['TYPE'])) { 2305 if (!is_array($item['params']['TYPE'])) { 2306 $item['params']['TYPE'] = array($item['params']['TYPE']); 2307 } 2308 foreach ($item['params']['TYPE'] as &$type) { 2309 $type = Horde_String::upper($type); 2310 } 2311 // For vCard 3.0. 2312 if (in_array('CELL', $item['params']['TYPE'])) { 2313 if (in_array('HOME', $item['params']['TYPE']) && 2314 !isset($hash['homeCellPhone'])) { 2315 $hash['homeCellPhone'] = $item['value']; 2316 } elseif (in_array('WORK', $item['params']['TYPE']) && 2317 !isset($hash['workCellPhone'])) { 2318 $hash['workCellPhone'] = $item['value']; 2319 } elseif (!isset($hash['cellPhone'])) { 2320 $hash['cellPhone'] = $item['value']; 2321 } 2322 } elseif (in_array('FAX', $item['params']['TYPE'])) { 2323 if (in_array('HOME', $item['params']['TYPE']) && 2324 !isset($hash['homeFax'])) { 2325 $hash['homeFax'] = $item['value']; 2326 } elseif (in_array('WORK', $item['params']['TYPE']) && 2327 !isset($hash['workFax'])) { 2328 $hash['workFax'] = $item['value']; 2329 } elseif (!isset($hash['fax'])) { 2330 $hash['fax'] = $item['value']; 2331 } 2332 } elseif (in_array('VIDEO', $item['params']['TYPE'])) { 2333 if (in_array('HOME', $item['params']['TYPE']) && 2334 !isset($hash['homeVideoCall'])) { 2335 $hash['homeVideoCall'] = $item['value']; 2336 } elseif (in_array('WORK', $item['params']['TYPE']) && 2337 !isset($hash['workVideoCall'])) { 2338 $hash['workVideoCall'] = $item['value']; 2339 } elseif (!isset($hash['videoCall'])) { 2340 $hash['videoCall'] = $item['value']; 2341 } 2342 } elseif (in_array('PAGER', $item['params']['TYPE']) && 2343 !isset($hash['pager'])) { 2344 $hash['pager'] = $item['value']; 2345 } elseif (in_array('WORK', $item['params']['TYPE']) && 2346 !isset($hash['workPhone'])) { 2347 $hash['workPhone'] = $item['value']; 2348 } elseif (in_array('HOME', $item['params']['TYPE']) && 2349 !isset($hash['homePhone'])) { 2350 $hash['homePhone'] = $item['value']; 2351 } elseif (!isset($hash['phone'])) { 2352 $hash['phone'] = $item['value']; 2353 } 2354 } elseif (isset($item['params']['CELL'])) { 2355 if (isset($item['params']['WORK']) && 2356 !isset($hash['workCellPhone'])) { 2357 $hash['workCellPhone'] = $item['value']; 2358 } elseif (isset($item['params']['HOME']) && 2359 !isset($hash['homeCellPhone'])) { 2360 $hash['homeCellPhone'] = $item['value']; 2361 } elseif (!isset($hash['cellPhone'])) { 2362 $hash['cellPhone'] = $item['value']; 2363 } 2364 } elseif (isset($item['params']['VIDEO'])) { 2365 if (isset($item['params']['WORK']) && 2366 !isset($hash['workVideoCall'])) { 2367 $hash['workVideoCall'] = $item['value']; 2368 } elseif (isset($item['params']['HOME']) && 2369 !isset($hash['homeVideoCall'])) { 2370 $hash['homeVideoCall'] = $item['value']; 2371 } elseif (!isset($hash['videoCall'])) { 2372 $hash['videoCall'] = $item['value']; 2373 } 2374 } else { 2375 if (isset($item['params']['WORK']) && 2376 !isset($hash['workPhone'])) { 2377 $hash['workPhone'] = $item['value']; 2378 } elseif (isset($item['params']['HOME']) && 2379 !isset($hash['homePhone'])) { 2380 $hash['homePhone'] = $item['value']; 2381 } else { 2382 $hash['phone'] = $item['value']; 2383 } 2384 } 2385 break; 2386 2387 case 'EMAIL': 2388 $email_set = false; 2389 if (isset($item['params']['HOME']) && 2390 !empty($this->map['homeEmail']) && 2391 (!isset($hash['homeEmail']) || 2392 isset($item['params']['PREF']))) { 2393 $e = Horde_Icalendar_Vcard::getBareEmail($item['value']); 2394 $hash['homeEmail'] = $e ? $e : ''; 2395 $email_set = true; 2396 } elseif (isset($item['params']['WORK']) && 2397 !empty($this->map['workEmail']) && 2398 (!isset($hash['workEmail']) || 2399 isset($item['params']['PREF']))) { 2400 $e = Horde_Icalendar_Vcard::getBareEmail($item['value']); 2401 $hash['workEmail'] = $e ? $e : ''; 2402 $email_set = true; 2403 } elseif (isset($item['params']['TYPE'])) { 2404 if (!is_array($item['params']['TYPE'])) { 2405 $item['params']['TYPE'] = array($item['params']['TYPE']); 2406 } 2407 foreach ($item['params']['TYPE'] as &$type) { 2408 $type = Horde_String::upper($type); 2409 } 2410 if (in_array('HOME', $item['params']['TYPE']) && 2411 !empty($this->map['homeEmail']) && 2412 (!isset($hash['homeEmail']) || 2413 in_array('PREF', $item['params']['TYPE']))) { 2414 $e = Horde_Icalendar_Vcard::getBareEmail($item['value']); 2415 $hash['homeEmail'] = $e ? $e : ''; 2416 $email_set = true; 2417 } elseif (in_array('WORK', $item['params']['TYPE']) && 2418 !empty($this->map['workEmail']) && 2419 (!isset($hash['workEmail']) || 2420 in_array('PREF', $item['params']['TYPE']))) { 2421 $e = Horde_Icalendar_Vcard::getBareEmail($item['value']); 2422 $hash['workEmail'] = $e ? $e : ''; 2423 $email_set = true; 2424 } 2425 } 2426 2427 if (!$email_set && 2428 (!isset($hash['email']) || 2429 isset($item['params']['PREF']) || 2430 (!empty($item['params']['TYPE']) && is_array($item['params']['TYPE']) && in_array('PREF', $item['params']['TYPE'])))) { 2431 $e = Horde_Icalendar_Vcard::getBareEmail($item['value']); 2432 $hash['email'] = $e ? $e : ''; 2433 } 2434 2435 if (!isset($hash['emails'])) { 2436 $hash['emails'] = ''; 2437 } 2438 if ($e = Horde_Icalendar_Vcard::getBareEmail($item['value'])) { 2439 if (strlen($hash['emails'])) { 2440 $hash['emails'] .= ','; 2441 } 2442 $hash['emails'] .= $e; 2443 } 2444 break; 2445 2446 case 'TITLE': 2447 $hash['title'] = $item['value']; 2448 break; 2449 2450 case 'ROLE': 2451 $hash['role'] = $item['value']; 2452 break; 2453 2454 case 'ORG': 2455 // The VCARD 2.1 specification requires the presence of two 2456 // SEMI-COLON separated fields: Organizational Name and 2457 // Organizational Unit. Additional fields are optional. 2458 $hash['company'] = !empty($item['values'][0]) ? $item['values'][0] : ''; 2459 $hash['department'] = !empty($item['values'][1]) ? $item['values'][1] : ''; 2460 break; 2461 2462 case 'NOTE': 2463 $hash['notes'] = $item['value']; 2464 break; 2465 2466 case 'CATEGORIES': 2467 $hash['businessCategory'] = $item['value']; 2468 $hash['__tags'] = $item['values']; 2469 break; 2470 2471 case 'URL': 2472 if (isset($item['params']['HOME']) && 2473 !isset($hash['homeWebsite'])) { 2474 $hash['homeWebsite'] = $item['value']; 2475 } elseif (isset($item['params']['WORK']) && 2476 !isset($hash['workWebsite'])) { 2477 $hash['workWebsite'] = $item['value']; 2478 } elseif (!isset($hash['website'])) { 2479 $hash['website'] = $item['value']; 2480 } 2481 break; 2482 2483 case 'BDAY': 2484 if (empty($item['value'])) { 2485 $hash['birthday'] = null; 2486 } else { 2487 $hash['birthday'] = $item['value']['year'] . '-' . $item['value']['month'] . '-' . $item['value']['mday']; 2488 } 2489 break; 2490 2491 case 'PHOTO': 2492 case 'LOGO': 2493 if (isset($item['params']['VALUE']) && 2494 Horde_String::lower($item['params']['VALUE']) == 'uri') { 2495 // No support for URIs yet. 2496 break; 2497 } 2498 if (!isset($item['params']['ENCODING']) || 2499 (Horde_String::lower($item['params']['ENCODING']) != 'b' && 2500 Horde_String::upper($item['params']['ENCODING']) != 'BASE64')) { 2501 // Invalid property. 2502 break; 2503 } 2504 $type = Horde_String::lower($item['name']); 2505 $hash[$type] = base64_decode($item['value']); 2506 if (isset($item['params']['TYPE'])) { 2507 $hash[$type . 'type'] = $item['params']['TYPE']; 2508 } 2509 break; 2510 2511 case 'X-SIP': 2512 if (isset($item['params']['POC']) && 2513 !isset($hash['ptt'])) { 2514 $hash['ptt'] = $item['value']; 2515 } elseif (isset($item['params']['VOIP']) && 2516 !isset($hash['voip'])) { 2517 $hash['voip'] = $item['value']; 2518 } elseif (isset($item['params']['SWIS']) && 2519 !isset($hash['shareView'])) { 2520 $hash['shareView'] = $item['value']; 2521 } elseif (!isset($hash['sip'])) { 2522 $hash['sip'] = $item['value']; 2523 } 2524 break; 2525 2526 case 'X-WV-ID': 2527 $hash['imaddress'] = $item['value']; 2528 break; 2529 2530 case 'X-ANNIVERSARY': 2531 if (empty($item['value'])) { 2532 $hash['anniversary'] = null; 2533 } else { 2534 $hash['anniversary'] = $item['value']['year'] . '-' . $item['value']['month'] . '-' . $item['value']['mday']; 2535 } 2536 break; 2537 2538 case 'X-CHILDREN': 2539 $hash['children'] = $item['value']; 2540 break; 2541 2542 case 'X-SPOUSE': 2543 $hash['spouse'] = $item['value']; 2544 break; 2545 } 2546 } 2547 2548 /* Ensure we have a valid name field. */ 2549 $hash = $this->_parseName($hash); 2550 2551 return $hash; 2552 } 2553 2554 /** 2555 * Convert the contact to an ActiveSync contact message 2556 * 2557 * @param Turba_Object $object The turba object to convert 2558 * @param array $options Options: 2559 * - protocolversion: (float) The EAS version to support 2560 * DEFAULT: 2.5 2561 * - bodyprefs: (array) A BODYPREFERENCE array. 2562 * DEFAULT: none (No body prefs enforced). 2563 * - truncation: (integer) Truncate event body to this length 2564 * DEFAULT: none (No truncation). 2565 * - device: (Horde_ActiveSync_Device) The device object. 2566 * 2567 * @return Horde_ActiveSync_Message_Contact 2568 */ 2569 public function toASContact(Turba_Object $object, array $options = array()) 2570 { 2571 global $injector; 2572 2573 $message = new Horde_ActiveSync_Message_Contact(array( 2574 'logger' => $injector->getInstance('Horde_Log_Logger'), 2575 'protocolversion' => $options['protocolversion'], 2576 'device' => !empty($options['device']) ? $options['device'] : null 2577 )); 2578 $hash = $object->getAttributes(); 2579 if (!isset($hash['lastname']) && isset($hash['name'])) { 2580 $this->_guessName($hash); 2581 } 2582 2583 // Ensure we have at least a good guess as to separate address fields. 2584 // Not ideal, but EAS does not have a single "address" field so we must 2585 // map "common" to either home or work. I choose home since 2586 // work/non-personal installs will be more likely to have separated 2587 // address fields. 2588 if (!empty($hash['commonAddress'])) { 2589 if (!isset($hash['commonStreet'])) { 2590 $hash['commonStreet'] = $hash['commonHome']; 2591 } 2592 foreach (array('Address', 'Street', 'POBox', 'Extended', 'City', 'Province', 'PostalCode', 'Country') as $field) { 2593 $hash['home' . $field] = $hash['common' . $field]; 2594 } 2595 } else { 2596 if (isset($hash['homeAddress']) && !isset($hash['homeStreet'])) { 2597 $hash['homeStreet'] = $hash['homeAddress']; 2598 } 2599 if (isset($hash['workAddress']) && !isset($hash['workStreet'])) { 2600 $hash['workStreet'] = $hash['workAddress']; 2601 } 2602 } 2603 2604 $hooks = $injector->getInstance('Horde_Core_Hooks'); 2605 $decode_hook = $hooks->hookExists('decode_attribute', 'turba'); 2606 2607 foreach ($hash as $field => $value) { 2608 if ($decode_hook) { 2609 try { 2610 $value = $hooks->callHook( 2611 'decode_attribute', 2612 'turba', 2613 array($field, $value, $object) 2614 ); 2615 } catch (Turba_Exception $e) { 2616 Horde::log($e); 2617 } 2618 } 2619 if (isset(self::$_asMap[$field])) { 2620 try { 2621 $message->{self::$_asMap[$field]} = $value; 2622 } catch (InvalidArgumentException $e) { 2623 } 2624 continue; 2625 } 2626 2627 switch ($field) { 2628 case 'photo': 2629 $message->picture = base64_encode($value); 2630 break; 2631 2632 case 'homeCountry': 2633 $message->homecountry = !empty($hash['homeCountryFree']) 2634 ? $hash['homeCountryFree'] 2635 : !empty($hash['homeCountry']) 2636 ? Horde_Nls::getCountryISO($hash['homeCountry']) 2637 : null; 2638 break; 2639 2640 case 'otherCountry': 2641 $message->othercountry = !empty($hash['otherCountryFree']) 2642 ? $hash['otherCountryFree'] 2643 : !empty($hash['otherCountry']) 2644 ? Horde_Nls::getCountryISO($hash['otherCountry']) 2645 : null; 2646 break; 2647 2648 case 'workCountry': 2649 $message->businesscountry = !empty($hash['workCountryFree']) 2650 ? $hash['workCountryFree'] 2651 : !empty($hash['workCountry']) 2652 ? Horde_Nls::getCountryISO($hash['workCountry']) 2653 : null; 2654 break; 2655 2656 case 'email': 2657 $message->email1address = $value; 2658 break; 2659 2660 case 'homeEmail': 2661 $message->email2address = $value; 2662 break; 2663 2664 case 'workEmail': 2665 $message->email3address = $value; 2666 break; 2667 2668 case 'emails': 2669 $address = 1; 2670 foreach (explode(',', $value) as $email) { 2671 while ($address <= 3 && 2672 $message->{'email' . $address . 'address'}) { 2673 $address++; 2674 } 2675 if ($address > 3) { 2676 break; 2677 } 2678 $message->{'email' . $address . 'address'} = $email; 2679 $address++; 2680 } 2681 break; 2682 2683 case 'children': 2684 // Children FROM horde are a simple string value. Even though EAS 2685 // uses an array stucture to pass them, we pass as a single 2686 // string since we can't assure what delimter the user will 2687 // use and (at least in some languages) a comma can be used 2688 // within a full name. 2689 $message->children = array($value); 2690 break; 2691 2692 case 'notes': 2693 if (strlen($value) && $options['protocolversion'] > Horde_ActiveSync::VERSION_TWOFIVE) { 2694 $bp = $options['bodyprefs']; 2695 $note = new Horde_ActiveSync_Message_AirSyncBaseBody(); 2696 // No HTML supported in Turba's notes. Always use plaintext. 2697 $note->type = Horde_ActiveSync::BODYPREF_TYPE_PLAIN; 2698 if (isset($bp[Horde_ActiveSync::BODYPREF_TYPE_PLAIN]['truncationsize']) && 2699 Horde_String::length($value) > $bp[Horde_ActiveSync::BODYPREF_TYPE_PLAIN]['truncationsize']) { 2700 $note->data = Horde_String::substr($value, 0, $bp[Horde_ActiveSync::BODYPREF_TYPE_PLAIN]['truncationsize']); 2701 $note->truncated = 1; 2702 } else { 2703 $note->data = $value; 2704 } 2705 $note->estimateddatasize = Horde_String::length($value); 2706 $message->airsyncbasebody = $note; 2707 } elseif (strlen($value)) { 2708 // EAS 2.5 2709 $message->body = $value; 2710 $message->bodysize = strlen($message->body); 2711 $message->bodytruncated = 0; 2712 } 2713 break; 2714 2715 case 'birthday': 2716 case 'anniversary': 2717 if (!empty($value) && $value != '0000-00-00') { 2718 try { 2719 $date = new Horde_Date($value); 2720 } catch (Horde_Date_Exception $e) { 2721 $message->$field = null; 2722 } 2723 // Some sanity checking to make sure the date was 2724 // successfully parsed. 2725 if ($date->month != 0) { 2726 $message->$field = $date; 2727 } else { 2728 $message->$field = null; 2729 } 2730 } else { 2731 $message->$field = null; 2732 } 2733 break; 2734 } 2735 } 2736 2737 /* Get tags. */ 2738 $message->categories = $injector->getInstance('Turba_Tagger') 2739 ->split($object->getValue('__tags')); 2740 2741 if (empty($this->fileas)) { 2742 $message->fileas = Turba::formatName($object); 2743 } 2744 2745 return $message; 2746 } 2747 2748 /** 2749 * Convert an ActiveSync contact message into a hash suitable for 2750 * importing via self::add(). 2751 * 2752 * @param Horde_ActiveSync_Message_Contact $message The contact message 2753 * object. 2754 * 2755 * @return array A contact hash. 2756 */ 2757 public function fromASContact(Horde_ActiveSync_Message_Contact $message) 2758 { 2759 $hash = array(); 2760 2761 foreach (self::$_asMap as $turbaField => $asField) { 2762 if (!$message->isGhosted($asField)) { 2763 try { 2764 $hash[$turbaField] = $message->{$asField}; 2765 } catch (InvalidArgumentException $e) { 2766 } 2767 } 2768 } 2769 2770 // Try our best to get a name attribute; 2771 $hash = $this->_parseName($hash); 2772 2773 /* Requires special handling */ 2774 2775 try { 2776 if ($message->getProtocolVersion() >= Horde_ActiveSync::VERSION_TWELVE) { 2777 if (!empty($message->airsyncbasebody)) { 2778 $hash['notes'] = $message->airsyncbasebody->data; 2779 } 2780 } else { 2781 $hash['notes'] = $message->body; 2782 } 2783 } catch (InvalidArgumentException $e) {} 2784 2785 // picture ($message->picture *should* already be base64 encdoed) 2786 if (!$message->isGhosted('picture')) { 2787 $hash['photo'] = base64_decode($message->picture); 2788 } 2789 2790 /* Email addresses */ 2791 $hash['emails'] = array(); 2792 if (!$message->isGhosted('email1address')) { 2793 $e = Horde_Icalendar_Vcard::getBareEmail($message->email1address); 2794 $hash['emails'][] = $hash['email'] = $e ? $e : ''; 2795 2796 } 2797 if (!$message->isGhosted('email2address')) { 2798 $e = Horde_Icalendar_Vcard::getBareEmail($message->email2address); 2799 $hash['emails'][] = $hash['homeEmail'] = $e ? $e : ''; 2800 } 2801 if (!$message->isGhosted('email3address')) { 2802 $e = Horde_Icalendar_Vcard::getBareEmail($message->email3address); 2803 $hash['emails'][] = $hash['workEmail'] = $e ? $e : ''; 2804 } 2805 $hash['emails'] = implode(',', $hash['emails']); 2806 2807 /* Categories */ 2808 if (!$message->isGhosted('categories') && empty($message->categories)) { 2809 $hash['__tags'] = array(); 2810 } elseif (is_array($message->categories) && 2811 count($message->categories)) { 2812 $hash['__tags'] = $message->categories; 2813 } 2814 2815 /* Children */ 2816 if (is_array($message->children) && count($message->children)) { 2817 // We use a comma as incoming delimiter as it's the most 2818 // common even though it might be used withing a name string. 2819 $hash['children'] = implode(', ', $message->children); 2820 } elseif (!$message->isGhosted('children')) { 2821 $hash['children'] = ''; 2822 } 2823 2824 /* Birthday and Anniversary */ 2825 if (!empty($message->birthday)) { 2826 $bday = new Horde_Date($message->birthday); 2827 $bday->setTimezone(date_default_timezone_get()); 2828 $hash['birthday'] = $bday->format('Y-m-d'); 2829 } elseif (!$message->isGhosted('birthday')) { 2830 $hash['birthday'] = ''; 2831 } 2832 if (!empty($message->anniversary)) { 2833 $anniversary = new Horde_Date($message->anniversary); 2834 $anniversary->setTimezone(date_default_timezone_get()); 2835 $hash['anniversary'] = $anniversary->format('Y-m-d'); 2836 } elseif (!$message->isGhosted('anniversary')) { 2837 $hash['anniversary'] = ''; 2838 } 2839 2840 /* Countries */ 2841 include 'Horde/Nls/Countries.php'; 2842 if (!empty($message->homecountry)) { 2843 if (!empty($this->map['homeCountryFree'])) { 2844 $hash['homeCountryFree'] = $message->homecountry; 2845 } else { 2846 $country = array_search($message->homecountry, $countries); 2847 if ($country === false) { 2848 $country = $message->homecountry; 2849 } 2850 $hash['homeCountry'] = $country; 2851 } 2852 } elseif (!$message->isGhosted('homecountry')) { 2853 $hash['homeCountry'] = ''; 2854 } 2855 2856 if (!empty($message->businesscountry)) { 2857 if (!empty($this->map['workCountryFree'])) { 2858 $hash['workCountryFree'] = $message->businesscountry; 2859 } else { 2860 $country = array_search($message->businesscountry, $countries); 2861 if ($country === false) { 2862 $country = $message->businesscountry; 2863 } 2864 $hash['workCountry'] = $country; 2865 } 2866 } elseif (!$message->isGhosted('businesscountry')) { 2867 $hash['workCountry'] = ''; 2868 } 2869 2870 if (!empty($message->othercountry)) { 2871 if (!empty($this->map['otherCountryFree'])) { 2872 $hash['otherCountryFree'] = $message->othercountry; 2873 } else { 2874 $country = array_search($message->othercountry, $countries); 2875 if ($country === false) { 2876 $country = $message->othercountry; 2877 } 2878 $hash['otherCountry'] = $country; 2879 } 2880 } elseif (!$message->isGhosted('othercountry')) { 2881 $hash['otherCountry'] = ''; 2882 } 2883 2884 return $hash; 2885 } 2886 2887 /** 2888 * Checks $hash for the presence of a 'name' attribute. If not found, 2889 * attempt to build one from other available values. 2890 * 2891 * @param array $hash A hash of turba attributes. 2892 * 2893 * @return array Hash of Turba attributes, with the 'name' attribute 2894 * populated. 2895 */ 2896 protected function _parseName(array $hash) 2897 { 2898 if (empty($hash['name'])) { 2899 /* If name is a composite field, it won't be present in the 2900 * $this->fields array, so check for that as well. */ 2901 if (isset($this->map['name']) && 2902 is_array($this->map['name']) && 2903 !empty($this->map['name']['attribute'])) { 2904 $fieldarray = array(); 2905 foreach ($this->map['name']['fields'] as $mapfields) { 2906 $fieldarray[] = isset($hash[$mapfields]) ? 2907 $hash[$mapfields] : ''; 2908 } 2909 $hash['name'] = Turba::formatCompositeField($this->map['name']['format'], $fieldarray); 2910 } else { 2911 $hash['name'] = isset($hash['firstname']) ? $hash['firstname'] : ''; 2912 if (!empty($hash['lastname'])) { 2913 $hash['name'] .= ' ' . $hash['lastname']; 2914 } 2915 $hash['name'] = trim($hash['name']); 2916 } 2917 } 2918 2919 return $hash; 2920 } 2921 2922 /** 2923 * Checks if the current user has the requested permissions on this 2924 * address book. 2925 * 2926 * @param integer $perm The permission to check for. 2927 * 2928 * @return boolean True if the user has permission, otherwise false. 2929 */ 2930 public function hasPermission($perm) 2931 { 2932 $perms = $GLOBALS['injector']->getInstance('Horde_Perms'); 2933 return $perms->exists('turba:sources:' . $this->_name) 2934 ? $perms->hasPermission('turba:sources:' . $this->_name, $GLOBALS['registry']->getAuth(), $perm) 2935 // Assume we have permissions if they're not explicitly set. 2936 : true; 2937 } 2938 2939 /** 2940 * Return the name of this address book. 2941 * (This is the key into the cfgSources array) 2942 * 2943 * @string Address book name 2944 */ 2945 public function getName() 2946 { 2947 return $this->_name; 2948 } 2949 2950 /** 2951 * Return the owner to use when searching or creating contacts in 2952 * this address book. 2953 * 2954 * @return string Contact owner. 2955 */ 2956 public function getContactOwner() 2957 { 2958 return empty($this->_contact_owner) 2959 ? $this->_getContactOwner() 2960 : $this->_contact_owner; 2961 } 2962 2963 /** 2964 * Override the contactOwner setting for this driver. 2965 * 2966 * @param string $owner The contact owner. 2967 */ 2968 public function setContactOwner($owner) 2969 { 2970 $this->_contact_owner = $owner; 2971 } 2972 2973 /** 2974 * Override the name setting for this driver. 2975 * 2976 * @param string $name The source name. This is the key into the 2977 * $cfgSources array. 2978 */ 2979 public function setSourceName($name) 2980 { 2981 $this->_name = $name; 2982 } 2983 2984 /** 2985 * Return the owner to use when searching or creating contacts in 2986 * this address book. 2987 * 2988 * @return string Contact owner. 2989 */ 2990 protected function _getContactOwner() 2991 { 2992 return $GLOBALS['registry']->getAuth(); 2993 } 2994 2995 /** 2996 * Creates a new Horde_Share for this source type. 2997 * 2998 * @param string $share_name The share name 2999 * @param array $params The params for the share. 3000 * 3001 * @return Horde_Share The share object. 3002 */ 3003 public function createShare($share_name, array $params) 3004 { 3005 // If the raw address book name is not set, use the share name 3006 if (empty($params['params']['name'])) { 3007 $params['params']['name'] = $share_name; 3008 } 3009 3010 return Turba::createShare($share_name, $params); 3011 } 3012 3013 /** 3014 * Runs any actions after setting a new default tasklist. 3015 * 3016 * @param string $share The default share ID. 3017 */ 3018 public function setDefaultShare($share) 3019 { 3020 } 3021 3022 /** 3023 * Creates an object key for a new object. 3024 * 3025 * @param array $attributes The attributes (in driver keys) of the 3026 * object being added. 3027 * 3028 * @return string A unique ID for the new object. 3029 */ 3030 protected function _makeKey(array $attributes) 3031 { 3032 return hash('md5', mt_rand()); 3033 } 3034 3035 /** 3036 * Creates an object UID for a new object. 3037 * 3038 * @return string A unique ID for the new object. 3039 */ 3040 protected function _makeUid() 3041 { 3042 return strval(new Horde_Support_Guid()); 3043 } 3044 3045 /** 3046 * Initialize the driver. 3047 * 3048 * @throws Turba_Exception 3049 */ 3050 protected function _init() 3051 { 3052 } 3053 3054 /** 3055 * Searches the address book with the given criteria and returns a 3056 * filtered list of results. If the criteria parameter is an empty array, 3057 * all records will be returned. 3058 * 3059 * @param array $criteria Array containing the search criteria. 3060 * @param array $fields List of fields to return. 3061 * @param array $blobFields Array of fields containing binary data. 3062 * @param boolean $count_only Only return the count of matching entries, 3063 * not the entries themselves. 3064 * 3065 * @return array Hash containing the search results. 3066 * @throws Turba_Exception 3067 */ 3068 protected function _search(array $criteria, array $fields, array $blobFields = array(), $count_only = false) 3069 { 3070 throw new Turba_Exception(_("Searching is not available.")); 3071 } 3072 3073 /** 3074 * Reads the given data from the address book and returns the results. 3075 * 3076 * @param string $key The primary key field to use. 3077 * @param mixed $ids The ids of the contacts to load. 3078 * @param string $owner Only return contacts owned by this user. 3079 * @param array $fields List of fields to return. 3080 * @param array $blobFields Array of fields containing binary data. 3081 * @param array $dateFields Array of fields containing date data. 3082 * @since 4.2.0 3083 * 3084 * @return array Hash containing the search results. 3085 * @throws Turba_Exception 3086 */ 3087 protected function _read($key, $ids, $owner, array $fields, 3088 array $blobFields = array(), 3089 array $dateFields = array()) 3090 { 3091 throw new Turba_Exception(_("Reading contacts is not available.")); 3092 } 3093 3094 /** 3095 * Adds the specified contact to the addressbook. 3096 * 3097 * @param array $attributes The attribute values of the contact. 3098 * @param array $blob_fields Fields that represent binary data. 3099 * @param array $date_fields Fields that represent dates. @since 4.2.0 3100 * 3101 * @throws Turba_Exception 3102 */ 3103 protected function _add(array $attributes, array $blob_fields = array(), array $date_fields = array()) 3104 { 3105 throw new Turba_Exception(_("Adding contacts is not available.")); 3106 } 3107 3108 /** 3109 * Deletes the specified contact from the addressbook. 3110 * 3111 * @param string $object_key TODO 3112 * @param string $object_id TODO 3113 * 3114 * @throws Turba_Exception 3115 */ 3116 protected function _delete($object_key, $object_id) 3117 { 3118 throw new Turba_Exception(_("Deleting contacts is not available.")); 3119 } 3120 3121 /** 3122 * Saves the specified object in the SQL database. 3123 * 3124 * @param Turba_Object $object The object to save 3125 * 3126 * @return string The object id, possibly updated. 3127 * @throws Turba_Exception 3128 */ 3129 protected function _save(Turba_Object $object) 3130 { 3131 throw new Turba_Exception(_("Saving contacts is not available.")); 3132 } 3133 3134 /** 3135 * Remove all entries owned by the specified user. 3136 * 3137 * @param string $user The user's data to remove. 3138 * 3139 * @throws Turba_Exception 3140 */ 3141 public function removeUserData($user) 3142 { 3143 throw new Turba_Exception_NotSupported(_("Removing user data is not supported in the current address book storage driver.")); 3144 } 3145 3146 /** 3147 * Check if the passed in share is the default share for this source. 3148 * 3149 * @param Horde_Share_Object $share The share object. 3150 * @param array $srcconfig The cfgSource entry for the share. 3151 * 3152 * @return boolean TODO 3153 */ 3154 public function checkDefaultShare(Horde_Share_Object $share, array $srcconfig) 3155 { 3156 $params = @unserialize($share->get('params')); 3157 if (!isset($params['default'])) { 3158 $params['default'] = ($params['name'] == $GLOBALS['registry']->getAuth()); 3159 $share->set('params', serialize($params)); 3160 $share->save(); 3161 } 3162 3163 return $params['default']; 3164 } 3165 3166 /* Countable methods. */ 3167 3168 /** 3169 * Returns the number of contacts of the current user in this address book. 3170 * 3171 * @return integer The number of contacts that the user owns. 3172 * @throws Turba_Exception 3173 */ 3174 public function count() 3175 { 3176 if (is_null($this->_count)) { 3177 $this->_count = count( 3178 $this->_search(array('AND' => array( 3179 array('field' => $this->toDriver('__owner'), 3180 'op' => '=', 3181 'test' => $this->getContactOwner()))), 3182 array($this->toDriver('__key'))) 3183 ); 3184 } 3185 3186 return $this->_count; 3187 } 3188 3189 /** 3190 * Helper function for guessing name parts from a single name string. 3191 * 3192 * @param array $hash The attributes array. 3193 */ 3194 protected function _guessName(&$hash) 3195 { 3196 if (($pos = strpos($hash['name'], ',')) !== false) { 3197 // Assume Last, First 3198 $hash['lastname'] = Horde_String::substr($hash['name'], 0, $pos); 3199 $hash['firstname'] = trim(Horde_String::substr($hash['name'], $pos + 1)); 3200 } elseif (($pos = Horde_String::rpos($hash['name'], ' ')) !== false) { 3201 // Assume everything after last space as lastname 3202 $hash['lastname'] = trim(Horde_String::substr($hash['name'], $pos + 1)); 3203 $hash['firstname'] = Horde_String::substr($hash['name'], 0, $pos); 3204 } else { 3205 $hash['lastname'] = $hash['name']; 3206 $hash['firstname'] = ''; 3207 } 3208 } 3209 3210} 3211