1<?php 2/** 3 * EGroupware API: Contacts LDAP Backend 4 * 5 * @link http://www.egroupware.org 6 * @author Cornelius Weiss <egw-AT-von-und-zu-weiss.de> 7 * @author Lars Kneschke <l.kneschke-AT-metaways.de> 8 * @author Ralf Becker <RalfBecker-AT-outdoor-training.de> 9 * @package api 10 * @subpackage contacts 11 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License 12 * @version $Id$ 13 */ 14 15namespace EGroupware\Api\Contacts; 16 17use EGroupware\Api; 18 19/** 20 * LDAP Backend for contacts, compatible with vars and parameters of eTemplate's so_sql. 21 * Maybe one day this becomes a generalized ldap storage object :-) 22 * 23 * All values used to construct filters need to run through Api\Ldap::quote(), 24 * to be save against LDAP query injection!!! 25 */ 26class Ldap 27{ 28 29 const ALL = 0; 30 const ACCOUNTS = 1; 31 const PERSONAL = 2; 32 const GROUP = 3; 33 34 var $data; 35 36 /** 37 * internal name of the id, gets mapped to uid 38 * 39 * @var string 40 */ 41 var $contacts_id='id'; 42 43 /** 44 * @var string $accountName holds the accountname of the current user 45 */ 46 var $accountName; 47 48 /** 49 * @var object $ldapServerInfo holds the information about the current used ldap server 50 */ 51 var $ldapServerInfo; 52 53 /** 54 * @var int $ldapLimit how many rows to fetch from ldap server 55 */ 56 var $ldapLimit = 2000; 57 58 /** 59 * @var string $personalContactsDN holds the base DN for the personal addressbooks 60 */ 61 var $personalContactsDN; 62 63 /** 64 * @var string $sharedContactsDN holds the base DN for the shared addressbooks 65 */ 66 var $sharedContactsDN; 67 68 /** 69 * @var string $accountContactsDN holds the base DN for accounts addressbook 70 */ 71 var $accountContactsDN; 72 73 /** 74 * Filter used for accounts addressbook 75 * @var string 76 */ 77 var $accountsFilter = '(objectclass=posixaccount)'; 78 79 /** 80 * Filter used for all addressbooks 81 * @var string 82 */ 83 var $contactsFilter = '(objectclass=inetorgperson)'; 84 85 /** 86 * @var string $allContactsDN holds the base DN of all addressbook 87 */ 88 var $allContactsDN; 89 90 /** 91 * Attribute used for DN 92 * 93 * @var string 94 */ 95 var $dn_attribute='uid'; 96 97 /** 98 * Do NOT attempt to change DN (dn-attribute can NOT be part of schemas used in addressbook!) 99 * 100 * @var boolean 101 */ 102 var $never_change_dn = false; 103 104 /** 105 * @var int $total holds the total count of found rows 106 */ 107 var $total; 108 109 /** 110 * Charset used by eGW 111 * 112 * @var string 113 */ 114 var $charset; 115 116 /** 117 * LDAP searches only a limited set of attributes for performance reasons, 118 * you NEED an index for that columns, ToDo: make it configurable 119 * minimum: $this->columns_to_search = array('n_family','n_given','org_name','email'); 120 */ 121 var $search_attributes = array( 122 'n_family','n_middle','n_given','org_name','org_unit', 123 'adr_one_locality','adr_two_locality','note', 124 'email','mozillasecondemail','uidnumber', 125 ); 126 127 /** 128 * maps between diverse ldap schema and the eGW internal names 129 * 130 * The ldap attribute names have to be lowercase!!! 131 * 132 * @var array 133 */ 134 var $schema2egw = array( 135 'posixaccount' => array( 136 'account_id' => 'uidnumber', 137 'shadowexpire', 138 ), 139 'inetorgperson' => array( 140 'n_fn' => 'cn', 141 'n_given' => 'givenname', 142 'n_family' => 'sn', 143 'sound' => 'audio', 144 'note' => 'description', 145 'url' => 'labeleduri', 146 'org_name' => 'o', 147 'org_unit' => 'ou', 148 'title' => 'title', 149 'adr_one_street' => 'street', 150 'adr_one_locality' => 'l', 151 'adr_one_region' => 'st', 152 'adr_one_postalcode' => 'postalcode', 153 'tel_work' => 'telephonenumber', 154 'tel_home' => 'homephone', 155 'tel_fax' => 'facsimiletelephonenumber', 156 'tel_cell' => 'mobile', 157 'tel_pager' => 'pager', 158 'email' => 'mail', 159 'room' => 'roomnumber', 160 'jpegphoto' => 'jpegphoto', 161 'n_fileas' => 'displayname', 162 'label' => 'postaladdress', 163 'pubkey' => 'usersmimecertificate', 164 'uid' => 'entryuuid', 165 'id' => 'uid', 166 ), 167 'organizantionalperson' => [ // ActiveDirectory 168 'n_fn' => 'displayname', // to leave CN as part of DN untouched 169 'n_given' => 'givenname', 170 'n_family' => 'sn', 171 //'sound' => 'audio', 172 'note' => 'description', 173 'url' => 'url', 174 'org_name' => 'company', 175 'org_unit' => 'department', 176 'title' => 'title', 177 'adr_one_street' => 'streetaddress', 178 'adr_one_locality' => 'l', 179 'adr_one_region' => 'st', 180 'adr_one_postalcode' => 'postalcode', 181 'adr_one_countryname' => 'co', 182 'adr_one_countrycode' => 'c', 183 'tel_work' => 'telephonenumber', 184 'tel_home' => 'homephone', 185 'tel_fax' => 'facsimiletelephonenumber', 186 'tel_cell' => 'mobile', 187 'tel_pager' => 'pager', 188 'tel_other' => 'othertelephone', 189 'tel_cell_private' => 'othermobile', 190 'assistent' => 'assistant', 191 'email' => 'mail', 192 'room' => 'roomnumber', 193 'jpegphoto' => 'jpegphoto', 194 'n_fileas' => 'displayname', 195 'label' => 'postaladdress', 196 'pubkey' => 'usersmimecertificate', 197 'uid' => 'objectguid', 198 'id' => 'objectguid', 199 ], 200 #displayName 201 #mozillaCustom1 202 #mozillaCustom2 203 #mozillaCustom3 204 #mozillaCustom4 205 #mozillaHomeUrl 206 #mozillaNickname 207 #mozillaUseHtmlMail 208 #nsAIMid 209 #postOfficeBox 210 'mozillaabpersonalpha' => array( 211 'adr_one_street2' => 'mozillaworkstreet2', 212 'adr_one_countryname' => 'c', // 2 letter country code 213 'adr_one_countrycode' => 'c', // 2 letter country code 214 'adr_two_street' => 'mozillahomestreet', 215 'adr_two_street2' => 'mozillahomestreet2', 216 'adr_two_locality' => 'mozillahomelocalityname', 217 'adr_two_region' => 'mozillahomestate', 218 'adr_two_postalcode' => 'mozillahomepostalcode', 219 'adr_two_countryname' => 'mozillahomecountryname', 220 'adr_two_countrycode' => 'mozillahomecountryname', 221 'email_home' => 'mozillasecondemail', 222 'url_home' => 'mozillahomeurl', 223 ), 224 // similar to the newer mozillaAbPerson, but uses mozillaPostalAddress2 instead of mozillaStreet2 225 'mozillaorgperson' => array( 226 'adr_one_street2' => 'mozillapostaladdress2', 227 'adr_one_countrycode' => 'c', // 2 letter country code 228 'adr_one_countryname' => 'co', // human readable country name, must be after 'c' to take precedence on read! 229 'adr_two_street' => 'mozillahomestreet', 230 'adr_two_street2' => 'mozillahomepostaladdress2', 231 'adr_two_locality' => 'mozillahomelocalityname', 232 'adr_two_region' => 'mozillahomestate', 233 'adr_two_postalcode' => 'mozillahomepostalcode', 234 'adr_two_countryname' => 'mozillahomecountryname', 235 'email_home' => 'mozillasecondemail', 236 'url_home' => 'mozillahomeurl', 237 ), 238 # managerName 239 # otherPostalAddress 240 # mailer 241 # anniversary 242 # spouseName 243 # companyPhone 244 # otherFacsimileTelephoneNumber 245 # radio 246 # telex 247 # tty 248 # categories(deprecated) 249 'evolutionperson' => array( 250 'bday' => 'birthdate', 251 'note' => 'note', 252 'tel_car' => 'carphone', 253 'tel_prefer' => 'primaryphone', 254 'cat_id' => 'category', // special handling in _egw2evolutionperson method 255 'role' => 'businessrole', 256 'tel_assistent' => 'assistantphone', 257 'assistent' => 'assistantname', 258 'n_fileas' => 'fileas', 259 'tel_fax_home' => 'homefacsimiletelephonenumber', 260 'freebusy_uri' => 'freeBusyuri', 261 'calendar_uri' => 'calendaruri', 262 'tel_other' => 'otherphone', 263 'tel_cell_private' => 'callbackphone', // not the best choice, but better then nothing 264 ), 265 // additional schema can be added here, including special functions 266 267 /** 268 * still unsupported fields in LDAP: 269 * -------------------------------- 270 * tz 271 * geo 272 */ 273 ); 274 275 /** 276 * additional schema required by one of the above schema 277 * 278 * @var array 279 */ 280 var $required_subs = array( 281 'inetorgperson' => array('person'), 282 ); 283 284 /** 285 * array with the names of all ldap attributes of the above schema2egw array 286 * 287 * @var array 288 */ 289 var $all_attributes = array(); 290 291 /** 292 * LDAP configuration 293 * 294 * @var array values for keys "ldap_contact_context", "ldap_host", "ldap_context" 295 */ 296 private $ldap_config; 297 298 /** 299 * LDAP connection 300 * 301 * @var resource 302 */ 303 var $ds; 304 305 /** 306 * constructor of the class 307 * 308 * @param array $ldap_config =null default use from $GLOBALS['egw_info']['server'] 309 * @param resource $ds =null ldap connection to use 310 */ 311 function __construct(array $ldap_config=null, $ds=null) 312 { 313 //$this->db_data_cols = $this->stock_contact_fields + $this->non_contact_fields; 314 $this->accountName = $GLOBALS['egw_info']['user']['account_lid']; 315 316 if ($ldap_config) 317 { 318 $this->ldap_config = $ldap_config; 319 } 320 else 321 { 322 $this->ldap_config =& $GLOBALS['egw_info']['server']; 323 } 324 $this->accountContactsDN = $this->ldap_config['ldap_context']; 325 $this->allContactsDN = $this->ldap_config['ldap_contact_context']; 326 $this->personalContactsDN = 'ou=personal,ou=contacts,'. $this->allContactsDN; 327 $this->sharedContactsDN = 'ou=shared,ou=contacts,'. $this->allContactsDN; 328 329 if ($ds) 330 { 331 $this->ds = $ds; 332 } 333 else 334 { 335 $this->connect(); 336 } 337 $this->ldapServerInfo = $GLOBALS['egw']->ldap->getLDAPServerInfo($this->ldap_config['ldap_contact_host']); 338 339 foreach($this->schema2egw as $attributes) 340 { 341 $this->all_attributes = array_merge($this->all_attributes,array_values($attributes)); 342 } 343 $this->all_attributes = array_values(array_unique($this->all_attributes)); 344 345 $this->charset = Api\Translation::charset(); 346 347 // add ldap_search_filter from admin 348 $accounts_filter = str_replace(['%user','%domain'], ['*', $GLOBALS['egw_info']['user']['domain']], 349 $this->ldap_config['ldap_search_filter'] ?: '(uid=%user)'); 350 $this->accountsFilter = "(&$this->accountsFilter$accounts_filter)"; 351 } 352 353 /** 354 * __wakeup function gets called by php while unserializing the object to reconnect with the ldap server 355 */ 356 function __wakeup() 357 { 358 $this->connect(); 359 } 360 361 /** 362 * connect to LDAP server 363 * 364 * @param boolean $admin =false true (re-)connect with admin not user credentials, eg. to modify accounts 365 */ 366 function connect($admin = false) 367 { 368 if ($admin) 369 { 370 $this->ds = Api\Ldap::factory(); 371 } 372 // if ldap is NOT the contact repository, we only do accounts and need to use the account-data 373 elseif (substr($GLOBALS['egw_info']['server']['contact_repository'],-4) != 'ldap') // not (ldap or sql-ldap) 374 { 375 $this->ldap_config['ldap_contact_host'] = $this->ldap_config['ldap_host']; 376 $this->allContactsDN = $this->ldap_config['ldap_context']; 377 $this->ds = Api\Ldap::factory(); 378 } 379 else 380 { 381 $this->ds = Api\Ldap::factory(true, 382 $this->ldap_config['ldap_contact_host'], 383 $GLOBALS['egw_info']['user']['account_dn'], 384 $GLOBALS['egw_info']['user']['passwd'] 385 ); 386 } 387 } 388 389 /** 390 * Returns the supported fields of this LDAP server (based on the objectclasses it supports) 391 * 392 * @return array with eGW contact field names 393 */ 394 function supported_fields() 395 { 396 $fields = array( 397 'id','tid','owner', 398 'n_middle','n_prefix','n_suffix', // stored in the cn 399 'created','modified', // automatic timestamps 400 'creator','modifier', // automatic for non accounts 401 'private', // true for personal addressbooks, false otherwise 402 ); 403 foreach($this->schema2egw as $objectclass => $mapping) 404 { 405 if($this->ldapServerInfo->supportsObjectClass($objectclass)) 406 { 407 $fields = array_merge($fields,array_keys($mapping)); 408 } 409 } 410 return array_values(array_unique($fields)); 411 } 412 413 /** 414 * Return LDAP filter for contact id 415 * 416 * @param string $id 417 * @return string 418 */ 419 protected function id_filter($id) 420 { 421 return '(|(entryUUID='.Api\Ldap::quote($id).')(uid='.Api\Ldap::quote($id).'))'; 422 } 423 424 /** 425 * Return LDAP filter for (multiple) contact ids 426 * 427 * @param array|string $ids 428 * @throws Api\Exception\AssertionFailed if $contact_id is no valid GUID (for ADS!) 429 * @return string 430 */ 431 protected function ids_filter($ids) 432 { 433 if (!is_array($ids) || count($ids) == 1) 434 { 435 return $this->id_filter(is_array($ids) ? array_shift($ids) : $ids); 436 } 437 $filter = array(); 438 foreach($ids as $id) 439 { 440 $filter[] = $this->id_filter($id); 441 } 442 return '(|'.implode('', $filter).')'; 443 } 444 445 /** 446 * reads contact data 447 * 448 * @param string|array $contact_id contact_id or array with values for id or account_id 449 * @return array/boolean data if row could be retrived else False 450 */ 451 function read($contact_id) 452 { 453 if (is_array($contact_id) && isset($contact_id['account_id']) || 454 !is_array($contact_id) && substr($contact_id,0,8) == 'account:') 455 { 456 $filter = 'uidNumber='.(int)(is_array($contact_id) ? $contact_id['account_id'] : substr($contact_id,8)); 457 } 458 else 459 { 460 if (is_array($contact_id)) $contact_id = isset ($contact_id['id']) ? $contact_id['id'] : $contact_id['uid']; 461 $filter = $this->id_filter($contact_id); 462 } 463 $rows = $this->_searchLDAP($this->allContactsDN, 464 $filter, $this->all_attributes, self::ALL, array('_posixaccount2egw')); 465 466 return $rows ? $rows[0] : false; 467 } 468 469 /** 470 * Remove attributes we are not allowed to update 471 * 472 * @param array $attributes 473 */ 474 function sanitize_update(array &$ldapContact) 475 { 476 // never allow to change the uidNumber (account_id) on update, as it could be misused by eg. xmlrpc or syncml 477 unset($ldapContact['uidnumber']); 478 479 unset($ldapContact['entryuuid']); // not allowed to modify that, no need either 480 481 unset($ldapContact['objectClass']); 482 } 483 484 /** 485 * saves the content of data to the db 486 * 487 * @param array $keys if given $keys are copied to data before saveing => allows a save as 488 * @return int 0 on success and errno != 0 else 489 */ 490 function save($keys=null) 491 { 492 //error_log(__METHOD__."(".array2string($keys).") this->data=".array2string($this->data)); 493 if(is_array($keys)) 494 { 495 $this->data = is_array($this->data) ? array_merge($this->data,$keys) : $keys; 496 } 497 498 $data =& $this->data; 499 $isUpdate = false; 500 $ldapContact = array(); 501 502 // generate addressbook dn 503 if((int)$data['owner']) 504 { 505 // group address book 506 if(!($cn = strtolower($GLOBALS['egw']->accounts->id2name((int)$data['owner'])))) 507 { 508 error_log('Unknown owner'); 509 return true; 510 } 511 $baseDN = 'cn='. $cn .','.($data['owner'] < 0 ? $this->sharedContactsDN : $this->personalContactsDN); 512 } 513 // only an admin or the user itself is allowed to change the data of an account 514 elseif ($data['account_id'] && ($GLOBALS['egw_info']['user']['apps']['admin'] || 515 $data['account_id'] == $GLOBALS['egw_info']['user']['account_id'])) 516 { 517 // account 518 $baseDN = $this->accountContactsDN; 519 $cn = false; 520 // we need an admin connection 521 $this->connect(true); 522 523 // for sql-ldap we need to account_lid/uid as id, NOT the contact_id in id! 524 if ($GLOBALS['egw_info']['server']['contact_repository'] == 'sql-ldap') 525 { 526 $data['id'] = $GLOBALS['egw']->accounts->id2name($data['account_id']); 527 } 528 } 529 else 530 { 531 error_log("Permission denied, to write: data[owner]=$data[owner], data[account_id]=$data[account_id], account_id=".$GLOBALS['egw_info']['user']['account_id']); 532 return lang('Permission denied !!!'); // only admin or the user itself is allowd to write accounts! 533 } 534 // check if $baseDN exists. If not create it 535 if (($err = $this->_check_create_dn($baseDN))) 536 { 537 return $err; 538 } 539 // check the existing objectclasses of an entry, none = array() for new ones 540 $oldObjectclasses = array(); 541 $attributes = array('dn','cn','objectClass',$this->dn_attribute,'mail'); 542 543 $contactUID = $this->data[$this->contacts_id]; 544 if (!empty($contactUID) && 545 ($result = ldap_search($this->ds, $base=$this->allContactsDN, $this->id_filter($contactUID), $attributes)) && 546 ($oldContactInfo = ldap_get_entries($this->ds, $result)) && $oldContactInfo['count']) 547 { 548 unset($oldContactInfo[0]['objectclass']['count']); 549 foreach($oldContactInfo[0]['objectclass'] as $objectclass) 550 { 551 $oldObjectclasses[] = strtolower($objectclass); 552 } 553 $isUpdate = true; 554 } 555 556 if(empty($contactUID)) 557 { 558 $ldapContact[$this->dn_attribute] = $this->data[$this->contacts_id] = $contactUID = md5(Api\Auth::randomstring(15)); 559 } 560 //error_log(__METHOD__."() contactUID='$contactUID', isUpdate=".array2string($isUpdate).", oldContactInfo=".array2string($oldContactInfo)); 561 // add for all supported objectclasses the objectclass and it's attributes 562 foreach($this->schema2egw as $objectclass => $mapping) 563 { 564 if(!$this->ldapServerInfo->supportsObjectClass($objectclass)) continue; 565 566 if($objectclass != 'posixaccount' && !in_array($objectclass, $oldObjectclasses)) 567 { 568 $ldapContact['objectClass'][] = $objectclass; 569 } 570 if (isset($this->required_subs[$objectclass])) 571 { 572 foreach($this->required_subs[$objectclass] as $sub) 573 { 574 if(!in_array($sub, $oldObjectclasses)) 575 { 576 $ldapContact['objectClass'][] = $sub; 577 } 578 } 579 } 580 foreach($mapping as $egwFieldName => $ldapFieldName) 581 { 582 if (is_int($egwFieldName)) continue; 583 if(!empty($data[$egwFieldName])) 584 { 585 // dont convert the (binary) jpegPhoto! 586 $ldapContact[$ldapFieldName] = $ldapFieldName == 'jpegphoto' ? $data[$egwFieldName] : 587 Api\Translation::convert(trim($data[$egwFieldName]),$this->charset,'utf-8'); 588 } 589 elseif($isUpdate && isset($data[$egwFieldName])) 590 { 591 $ldapContact[$ldapFieldName] = array(); 592 } 593 //error_log(__METHOD__."() ".__LINE__." objectclass=$objectclass, data['$egwFieldName']=".array2string($data[$egwFieldName])." --> ldapContact['$ldapFieldName']=".array2string($ldapContact[$ldapFieldName])); 594 } 595 // handling of special attributes, like cat_id in evolutionPerson 596 $egw2objectclass = '_egw2'.$objectclass; 597 if (method_exists($this,$egw2objectclass)) 598 { 599 $this->$egw2objectclass($ldapContact,$data,$isUpdate); 600 } 601 } 602 if ($isUpdate) 603 { 604 // make sure multiple email-addresses in the mail attribute "survive" 605 if (isset($ldapContact['mail']) && $oldContactInfo[0]['mail']['count'] > 1) 606 { 607 $mail = $oldContactInfo[0]['mail']; 608 unset($mail['count']); 609 $mail[0] = $ldapContact['mail']; 610 $ldapContact['mail'] = array_values(array_unique($mail)); 611 } 612 // update entry 613 $dn = $oldContactInfo[0]['dn']; 614 $needRecreation = false; 615 616 // add missing objectclasses 617 if($ldapContact['objectClass'] && ($missing=array_diff($ldapContact['objectClass'],$oldObjectclasses))) 618 { 619 if (!@ldap_mod_add($this->ds, $dn, array('objectClass' => $ldapContact['objectClass']))) 620 { 621 if(in_array(ldap_errno($this->ds),array(69,20))) 622 { 623 // need to modify structural objectclass 624 $needRecreation = true; 625 //error_log(__METHOD__."() ".__LINE__." could not add objectclasses ".array2string($missing)." --> need to recreate contact"); 626 } 627 else 628 { 629 //echo "<p>ldap_mod_add($this->ds,'$dn',array(objectClass =>".print_r($ldapContact['objectClass'],true)."))</p>\n"; 630 error_log(__METHOD__.'() '.__LINE__.' update of '. $dn .' failed errorcode: '. ldap_errno($this->ds) .' ('. ldap_error($this->ds) .')'); 631 return $this->_error(__LINE__); 632 } 633 } 634 } 635 636 // check if we need to rename the DN or need to recreate the contact 637 $newRDN = $this->dn_attribute.'='. $ldapContact[$this->dn_attribute]; 638 $newDN = $newRDN .','. $baseDN; 639 if ($needRecreation) 640 { 641 $result = ldap_read($this->ds, $dn, 'objectclass=*'); 642 $entries = ldap_get_entries($this->ds, $result); 643 $oldContact = Api\Ldap::result2array($entries[0]); 644 unset($oldContact['dn']); 645 646 $newContact = $oldContact; 647 $newContact[$this->dn_attribute] = $ldapContact[$this->dn_attribute]; 648 649 if(is_array($ldapContact['objectClass']) && count($ldapContact['objectClass']) > 0) 650 { 651 $newContact['objectclass'] = array_unique(array_map('strtolower', // objectclasses my have different case 652 array_merge($newContact['objectclass'], $ldapContact['objectClass']))); 653 } 654 655 if(!ldap_delete($this->ds, $dn)) 656 { 657 error_log(__METHOD__.'() '.__LINE__.' delete of old '. $dn .' failed errorcode: '. ldap_errno($this->ds) .' ('. ldap_error($this->ds) .')'); 658 return $this->_error(__LINE__); 659 } 660 if(!@ldap_add($this->ds, $newDN, $newContact)) 661 { 662 error_log(__METHOD__.'() '.__LINE__.' re-create contact as '. $newDN .' failed errorcode: '. ldap_errno($this->ds) .' ('. ldap_error($this->ds) .') newContact='.json_encode($newContact)); 663 // if adding with new objectclass or dn fails, re-add deleted contact 664 @ldap_add($this->ds, $dn, $oldContact); 665 return $this->_error(__LINE__); 666 } 667 $dn = $newDN; 668 } 669 if ($this->never_change_dn) 670 { 671 // do NOT change DN, set by addressbook_ads, as accounts can be stored in different containers 672 } 673 // try renaming entry if content of dn-attribute changed 674 elseif (strtolower($dn) != strtolower($newDN) || $ldapContact[$this->dn_attribute] != $oldContactInfo[$this->dn_attribute][0]) 675 { 676 if (@ldap_rename($this->ds, $dn, $newRDN, null, true)) 677 { 678 $dn = $newDN; 679 } 680 else 681 { 682 error_log(__METHOD__.'() '.__LINE__." ldap_rename of $dn to $newRDN failed! ".ldap_error($this->ds)); 683 } 684 } 685 unset($ldapContact[$this->dn_attribute]); 686 687 $this->sanitize_update($ldapContact); 688 689 if (!@ldap_modify($this->ds, $dn, $ldapContact)) 690 { 691 error_log(__METHOD__.'() '.__LINE__.' update of '. $dn .' failed errorcode: '. ldap_errno($this->ds) .' ('. ldap_error($this->ds) .') ldapContact='.json_encode($ldapContact)); 692 return $this->_error(__LINE__); 693 } 694 } 695 else 696 { 697 $dn = $this->dn_attribute.'='. $ldapContact[$this->dn_attribute] .','. $baseDN; 698 unset($ldapContact['entryuuid']); // trying to write it, gives an error 699 700 if (!@ldap_add($this->ds, $dn, $ldapContact)) 701 { 702 error_log(__METHOD__.'() '.__LINE__.' add of '. $dn .' failed errorcode: '. ldap_errno($this->ds) .' ('. ldap_error($this->ds) .') ldapContact='.json_encode($ldapContact)); 703 return $this->_error(__LINE__); 704 } 705 } 706 return 0; // Ok, no error 707 } 708 709 /** 710 * deletes row representing keys in internal data or the supplied $keys if != null 711 * 712 * @param array $keys if given array with col => value pairs to characterise the rows to delete 713 * @return int affected rows, should be 1 if ok, 0 if an error 714 */ 715 function delete($keys=null) 716 { 717 // single entry 718 if($keys[$this->contacts_id]) $keys = array( 0 => $keys); 719 720 if(!is_array($keys)) 721 { 722 $keys = array( $keys); 723 } 724 725 $ret = 0; 726 727 $attributes = array('dn'); 728 729 foreach($keys as $entry) 730 { 731 $entry = Api\Ldap::quote(is_array($entry) ? $entry['id'] : $entry); 732 if(($result = ldap_search($this->ds, $this->allContactsDN, 733 "(|(entryUUID=$entry)(uid=$entry))", $attributes))) 734 { 735 $contactInfo = ldap_get_entries($this->ds, $result); 736 if(@ldap_delete($this->ds, $contactInfo[0]['dn'])) 737 { 738 $ret++; 739 } 740 } 741 } 742 return $ret; 743 } 744 745 /** 746 * searches db for rows matching searchcriteria 747 * 748 * '*' and '?' are replaced with sql-wildcards '%' and '_' 749 * 750 * @param array|string $criteria array of key and data cols, OR a SQL query (content for WHERE), fully quoted (!) 751 * @param boolean|string $only_keys =true True returns only keys, False returns all cols. comma seperated list of keys to return 752 * @param string $order_by ='' fieldnames + {ASC|DESC} separated by colons ',', can also contain a GROUP BY (if it contains ORDER BY) 753 * @param string|array $extra_cols ='' string or array of strings to be added to the SELECT, eg. "count(*) as num" 754 * @param string $wildcard ='' appended befor and after each criteria 755 * @param boolean $empty =false False=empty criteria are ignored in query, True=empty have to be empty in row 756 * @param string $op ='AND' defaults to 'AND', can be set to 'OR' too, then criteria's are OR'ed together 757 * @param mixed $start =false if != false, return only maxmatch rows begining with start, or array($start,$num) 758 * @param array $filter =null if set (!=null) col-data pairs, to be and-ed (!) into the query without wildcards 759 * @param string $join ='' sql to do a join, added as is after the table-name, eg. ", table2 WHERE x=y" or 760 * "LEFT JOIN table2 ON (x=y)", Note: there's no quoting done on $join! 761 * @param boolean $need_full_no_count =false If true an unlimited query is run to determine the total number of rows, default false 762 * @return array of matching rows (the row is an array of the cols) or False 763 */ 764 function &search($criteria,$only_keys=True,$order_by='',$extra_cols='',$wildcard='',$empty=False,$op='AND',$start=false,$filter=null,$join='',$need_full_no_count=false) 765 { 766 //error_log(__METHOD__."(".array2string($criteria).", ".array2string($only_keys).", '$order_by', ".array2string($extra_cols).", '$wildcard', '$empty', '$op', ".array2string($start).", ".array2string($filter).")"); 767 unset($only_keys, $extra_cols, $empty, $join, $need_full_no_count); // not used, but required by function signature 768 769 if (is_array($filter['owner'])) 770 { 771 if (count($filter['owner']) == 1) 772 { 773 $filter['owner'] = array_shift($filter['owner']); 774 } 775 else 776 { 777 // multiple addressbooks happens currently only via CardDAV or eSync 778 // currently we query all contacts and remove not matching ones (not the most efficient way to do it) 779 $owner_filter = $filter['owner']; 780 unset($filter['owner']); 781 } 782 } 783 // search filter for modified date (eg. for CardDAV sync-report) 784 $datefilter = ''; 785 static $egw2ldap = array( 786 'created' => 'createtimestamp', 787 'modified' => 'modifytimestamp', 788 ); 789 foreach($filter as $key => $value) 790 { 791 $matches = null; 792 if (is_int($key) && preg_match('/^(contact_)?(modified|created)([<=>]+)([0-9]+)$/', $value, $matches)) 793 { 794 $append = ''; 795 if ($matches[3] == '>') 796 { 797 $matches['3'] = '<='; 798 $datefilter .= '(!'; 799 $append = ')'; 800 } 801 $datefilter .= '('.$egw2ldap[$matches[2]].$matches[3].self::_ts2ldap($matches[4]).')'.$append; 802 } 803 } 804 805 if((int)$filter['owner']) 806 { 807 if (!($accountName = $GLOBALS['egw']->accounts->id2name($filter['owner']))) return false; 808 809 $searchDN = 'cn='. Api\Ldap::quote(strtolower($accountName)) .','; 810 811 if ($filter['owner'] < 0) 812 { 813 $searchDN .= $this->sharedContactsDN; 814 $addressbookType = self::GROUP; 815 } 816 else 817 { 818 $searchDN .= $this->personalContactsDN; 819 $addressbookType = self::PERSONAL; 820 } 821 } 822 elseif (!isset($filter['owner'])) 823 { 824 $searchDN = $this->allContactsDN; 825 $addressbookType = self::ALL; 826 } 827 else 828 { 829 $searchDN = $this->accountContactsDN; 830 $addressbookType = self::ACCOUNTS; 831 } 832 // create the search filter 833 switch($addressbookType) 834 { 835 case self::ACCOUNTS: 836 $objectFilter = $this->accountsFilter; 837 break; 838 default: 839 $objectFilter = $this->contactsFilter; 840 break; 841 } 842 // exclude expired accounts 843 //$shadowExpireNow = floor((time()+date('Z'))/86400); 844 //$objectFilter .= "(|(!(shadowExpire=*))(shadowExpire>=$shadowExpireNow))"; 845 // shadowExpire>= does NOT work, as shadow schema only specifies integerMatch and not integerOrderingMatch :-( 846 847 $searchFilter = ''; 848 if(is_array($criteria) && count($criteria) > 0) 849 { 850 $wildcard = $wildcard === '%' ? '*' : ''; 851 $searchFilter = ''; 852 foreach($criteria as $egwSearchKey => $searchValue) 853 { 854 if (in_array($egwSearchKey, array('id','contact_id'))) 855 { 856 try { 857 $searchFilter .= $this->ids_filter($searchValue); 858 } 859 // catch and ignore exception caused by id not being a valid GUID 860 catch(Api\Exception\AssertionFailed $e) { 861 unset($e); 862 } 863 continue; 864 } 865 foreach($this->schema2egw as $mapping) 866 { 867 $matches = null; 868 if (preg_match('/^(egw_addressbook\.)?(contact_)?(.*)$/', $egwSearchKey, $matches)) 869 { 870 $egwSearchKey = $matches[3]; 871 } 872 if(($ldapSearchKey = $mapping[$egwSearchKey])) 873 { 874 foreach((array)$searchValue as $val) 875 { 876 $searchString = Api\Translation::convert($val, $this->charset,'utf-8'); 877 $searchFilter .= '('.$ldapSearchKey.'='.$wildcard.Api\Ldap::quote($searchString).$wildcard.')'; 878 } 879 break; 880 } 881 } 882 } 883 if($op == 'AND') 884 { 885 $searchFilter = "(&$searchFilter)"; 886 } 887 else 888 { 889 $searchFilter = "(|$searchFilter)"; 890 } 891 } 892 $colFilter = $this->_colFilter($filter); 893 $ldapFilter = "(&$objectFilter$searchFilter$colFilter$datefilter)"; 894 //error_log(__METHOD__."(".array2string($criteria).", ".array2string($only_keys).", '$order_by', ".array2string($extra_cols).", '$wildcard', '$empty', '$op', ".array2string($start).", ".array2string($filter).") --> ldapFilter='$ldapFilter'"); 895 if (!($rows = $this->_searchLDAP($searchDN, $ldapFilter, $this->all_attributes, $addressbookType))) 896 { 897 return $rows; 898 } 899 // only return certain owners --> unset not matching ones 900 if ($owner_filter) 901 { 902 foreach($rows as $k => $row) 903 { 904 if (!in_array($row['owner'],$owner_filter)) 905 { 906 unset($rows[$k]); 907 --$this->total; 908 } 909 } 910 } 911 if ($order_by) 912 { 913 $order = array(); 914 $sort = 'ASC'; 915 foreach(explode(',',$order_by) as $o) 916 { 917 if (substr($o,0,8) == 'contact_') $o = substr($o,8); 918 if (substr($o,-4) == ' ASC') 919 { 920 $sort = 'ASC'; 921 $order[] = substr($o,0,-4); 922 } 923 elseif (substr($o,-5) == ' DESC') 924 { 925 $sort = 'DESC'; 926 $order[] = substr($o,0,-5); 927 } 928 elseif ($o) 929 { 930 $order[] = $o; 931 } 932 } 933 usort($rows, function($a, $b) use ($order, $sort) 934 { 935 foreach($order as $f) 936 { 937 if($sort == 'ASC') 938 { 939 $strc = strcmp($a[$f], $b[$f]); 940 } 941 else 942 { 943 $strc = strcmp($b[$f], $a[$f]); 944 } 945 if ($strc) return $strc; 946 } 947 return 0; 948 }); 949 } 950 // if requested ($start !== false) return only limited resultset 951 if (is_array($start)) 952 { 953 list($start,$offset) = $start; 954 } 955 if(is_numeric($start) && is_numeric($offset) && $offset >= 0) 956 { 957 return array_slice($rows, $start, $offset); 958 } 959 elseif(is_numeric($start)) 960 { 961 return array_slice($rows, $start, $GLOBALS['egw_info']['user']['preferences']['common']['maxmatchs']); 962 } 963 return $rows; 964 } 965 966 /** 967 * Process so_sql like filters (at the moment only a subset used by the addressbook UI 968 * 969 * @param array $filter col-name => value pairs or just sql strings 970 * @return string ldap filter 971 */ 972 function _colFilter($filter) 973 { 974 if (!is_array($filter)) return ''; 975 976 $filters = ''; 977 foreach($filter as $key => $value) 978 { 979 if ($key != 'cat_id' && $key != 'account_id' && !$value) continue; 980 981 switch((string) $key) 982 { 983 case 'owner': // already handled 984 case 'tid': // ignored 985 break; 986 987 case 'account_id': 988 if (is_null($value)) 989 { 990 $filters .= '(!(uidNumber=*))'; 991 } 992 elseif ($value) 993 { 994 if (is_array($value)) $filters .= '(|'; 995 foreach((array)$value as $value) 996 { 997 $filters .= '(uidNumber='.(int)$value.')'; 998 } 999 if (is_array($value)) $filters .= ')'; 1000 } 1001 break; 1002 1003 case 'cat_id': 1004 if (is_null($value)) 1005 { 1006 $filters .= '(!(category=*))'; 1007 } 1008 elseif((int)$value) 1009 { 1010 if (!is_object($GLOBALS['egw']->categories)) 1011 { 1012 $GLOBALS['egw']->categories = new Api\Categories(); 1013 } 1014 $cats = $GLOBALS['egw']->categories->return_all_children((int)$value); 1015 if (count($cats) > 1) $filters .= '(|'; 1016 foreach($cats as $cat) 1017 { 1018 $catName = Api\Translation::convert( 1019 $GLOBALS['egw']->categories->id2name($cat),$this->charset,'utf-8'); 1020 $filters .= '(category='.Api\Ldap::quote($catName).')'; 1021 } 1022 if (count($cats) > 1) $filters .= ')'; 1023 } 1024 break; 1025 1026 case 'carddav_name': 1027 if (!is_array($value)) $value = array($value); 1028 foreach($value as &$v) 1029 { 1030 $v = basename($v, '.vcf'); 1031 } 1032 // fall through 1033 case 'id': 1034 case 'contact_id': 1035 $filters .= $this->ids_filter($value); 1036 break; 1037 1038 case 'list': 1039 $filters .= $this->membershipFilter($value); 1040 break; 1041 1042 default: 1043 $matches = null; 1044 if (!is_int($key)) 1045 { 1046 foreach($this->schema2egw as $mapping) 1047 { 1048 if (isset($mapping[$key])) 1049 { 1050 // todo: value = "!''" 1051 $filters .= '('.$mapping[$key].'='.($value === "!''" ? '*' : 1052 Api\Ldap::quote(Api\Translation::convert($value,$this->charset,'utf-8'))).')'; 1053 break; 1054 } 1055 } 1056 } 1057 // filter for letter-search 1058 elseif (preg_match("/^([^ ]+) ".preg_quote($GLOBALS['egw']->db->capabilities[Api\Db::CAPABILITY_CASE_INSENSITIV_LIKE])." '(.*)%'$/",$value,$matches)) 1059 { 1060 list(,$name,$value) = $matches; 1061 if (strpos($name,'.') !== false) list(,$name) = explode('.',$name); 1062 foreach($this->schema2egw as $mapping) 1063 { 1064 if (isset($mapping[$name])) 1065 { 1066 $filters .= '('.$mapping[$name].'='.Api\Ldap::quote( 1067 Api\Translation::convert($value,$this->charset,'utf-8')).'*)'; 1068 break; 1069 } 1070 } 1071 } 1072 break; 1073 } 1074 } 1075 return $filters; 1076 } 1077 1078 /** 1079 * Return a LDAP filter by group membership 1080 * 1081 * @param int $gid gidNumber (< 0 as used in EGroupware!) 1082 * @return string filter or '' if $gid not < 0 1083 */ 1084 function membershipFilter($gid) 1085 { 1086 $filter = ''; 1087 if ($gid < 0) 1088 { 1089 $filter .= '(|'; 1090 // unfortunately we have no group-membership attribute in LDAP, like in AD 1091 foreach($GLOBALS['egw']->accounts->members($gid, true) as $account_id) 1092 { 1093 $filter .= '(uidNumber='.(int)$account_id.')'; 1094 } 1095 $filter .= ')'; 1096 } 1097 return $filter; 1098 } 1099 1100 /** 1101 * Perform the actual ldap-search, retrieve and convert all entries 1102 * 1103 * Used be read and search 1104 * 1105 * @internal 1106 * @param string $_ldapContext 1107 * @param string $_filter 1108 * @param array $_attributes 1109 * @param int $_addressbooktype 1110 * @param array $_skipPlugins =null schema-plugins to skip 1111 * @return array/boolean with eGW contacts or false on error 1112 */ 1113 function _searchLDAP($_ldapContext, $_filter, $_attributes, $_addressbooktype, array $_skipPlugins=null) 1114 { 1115 $this->total = 0; 1116 1117 $_attributes[] = 'entryUUID'; 1118 $_attributes[] = 'objectClass'; 1119 $_attributes[] = 'createTimestamp'; 1120 $_attributes[] = 'modifyTimestamp'; 1121 $_attributes[] = 'creatorsName'; 1122 $_attributes[] = 'modifiersName'; 1123 1124 //error_log(__METHOD__."('$_ldapContext', '$_filter', ".array2string($_attributes).", $_addressbooktype)"); 1125 1126 if($_addressbooktype == self::ALL || $_ldapContext == $this->allContactsDN) 1127 { 1128 $result = ldap_search($this->ds, $_ldapContext, $_filter, $_attributes, 0, $this->ldapLimit); 1129 } 1130 else 1131 { 1132 $result = @ldap_list($this->ds, $_ldapContext, $_filter, $_attributes, 0, $this->ldapLimit); 1133 } 1134 if(!$result || !$entries = ldap_get_entries($this->ds, $result)) return array(); 1135 //error_log(__METHOD__."('$_ldapContext', '$_filter', ".array2string($_attributes).", $_addressbooktype) result of $entries[count]"); 1136 1137 $this->total = $entries['count']; 1138 foreach($entries as $i => $entry) 1139 { 1140 if (!is_int($i)) continue; // eg. count 1141 1142 $contact = array( 1143 'id' => $entry['uid'][0] ? $entry['uid'][0] : $entry['entryuuid'][0], 1144 'tid' => 'n', // the type id for the addressbook 1145 ); 1146 foreach($entry['objectclass'] as $ii => $objectclass) 1147 { 1148 $objectclass = strtolower($objectclass); 1149 if (!is_int($ii) || !isset($this->schema2egw[$objectclass])) 1150 { 1151 continue; // eg. count or unsupported objectclass 1152 } 1153 foreach($this->schema2egw[$objectclass] as $egwFieldName => $ldapFieldName) 1154 { 1155 if(!empty($entry[$ldapFieldName][0]) && !is_int($egwFieldName) && !isset($contact[$egwFieldName])) 1156 { 1157 $contact[$egwFieldName] = Api\Translation::convert($entry[$ldapFieldName][0],'utf-8'); 1158 } 1159 } 1160 $objectclass2egw = '_'.$objectclass.'2egw'; 1161 if (!in_array($objectclass2egw, (array)$_skipPlugins) &&method_exists($this,$objectclass2egw)) 1162 { 1163 if (($ret=$this->$objectclass2egw($contact,$entry)) === false) 1164 { 1165 --$this->total; 1166 continue 2; 1167 } 1168 } 1169 } 1170 // read binary jpegphoto only for one result == call by read 1171 if ($this->total == 1 && isset($entry['jpegphoto'][0])) 1172 { 1173 $bin = ldap_get_values_len($this->ds,ldap_first_entry($this->ds,$result),'jpegphoto'); 1174 $contact['jpegphoto'] = $bin[0]; 1175 } 1176 $matches = null; 1177 if(preg_match('/cn=([^,]+),'.preg_quote($this->personalContactsDN,'/').'$/i',$entry['dn'],$matches)) 1178 { 1179 // personal addressbook 1180 $contact['owner'] = $GLOBALS['egw']->accounts->name2id($matches[1],'account_lid','u'); 1181 $contact['private'] = 0; 1182 } 1183 elseif(preg_match('/cn=([^,]+),'.preg_quote($this->sharedContactsDN,'/').'$/i',$entry['dn'],$matches)) 1184 { 1185 // group addressbook 1186 $contact['owner'] = $GLOBALS['egw']->accounts->name2id($matches[1],'account_lid','g'); 1187 $contact['private'] = 0; 1188 } 1189 else 1190 { 1191 // accounts 1192 $contact['owner'] = 0; 1193 $contact['private'] = 0; 1194 } 1195 foreach(array( 1196 'createtimestamp' => 'created', 1197 'modifytimestamp' => 'modified', 1198 ) as $ldapFieldName => $egwFieldName) 1199 { 1200 if(!empty($entry[$ldapFieldName][0])) 1201 { 1202 $contact[$egwFieldName] = $this->_ldap2ts($entry[$ldapFieldName][0]); 1203 } 1204 } 1205 $contacts[] = $contact; 1206 } 1207 return $contacts; 1208 } 1209 1210 /** 1211 * Creates a timestamp from the date returned by the ldap server 1212 * 1213 * @internal 1214 * @param string $date YYYYmmddHHiiss 1215 * @return int 1216 */ 1217 static function _ldap2ts($date) 1218 { 1219 return gmmktime(substr($date,8,2),substr($date,10,2),substr($date,12,2), 1220 substr($date,4,2),substr($date,6,2),substr($date,0,4)); 1221 } 1222 1223 /** 1224 * Create LDAP date-value from timestamp 1225 * 1226 * @param integer $ts 1227 * @return string 1228 */ 1229 static function _ts2ldap($ts) 1230 { 1231 return gmdate('YmdHis', $ts).'.0Z'; 1232 } 1233 1234 /** 1235 * check if $baseDN exists. If not create it 1236 * 1237 * @param string $baseDN cn=xxx,ou=yyy,ou=contacts,$this->allContactsDN 1238 * @return boolean/string false on success or string with error-message 1239 */ 1240 function _check_create_dn($baseDN) 1241 { 1242 // check if $baseDN exists. If not create new one 1243 if(@ldap_read($this->ds, $baseDN, 'objectclass=*')) 1244 { 1245 return false; 1246 } 1247 //error_log(__METHOD__."('$baseDN') !ldap_read({$this->ds}, '$baseDN', 'objectclass=*') ldap_errno()=".ldap_errno($this->ds).', ldap_error()='.ldap_error($this->ds).get_class($this)); 1248 if(ldap_errno($this->ds) != 32 || substr($baseDN,0,3) != 'cn=') 1249 { 1250 error_log(__METHOD__."('$baseDN') baseDN does NOT exist and we cant/wont create it! ldap_errno()=".ldap_errno($this->ds).', ldap_error()='.ldap_error($this->ds)); 1251 return $this->_error(__LINE__); // baseDN does NOT exist and we cant/wont create it 1252 } 1253 1254 list(,$ou) = explode(',',$baseDN); 1255 $adminDS = null; 1256 foreach(array( 1257 'ou=contacts,'.$this->allContactsDN, 1258 $ou.',ou=contacts,'.$this->allContactsDN, 1259 $baseDN, 1260 ) as $dn) 1261 { 1262 if (!@ldap_read($this->ds, $dn, 'objectclass=*') && ldap_errno($this->ds) == 32) 1263 { 1264 // entry does not exist, lets try to create it 1265 list($top) = explode(',',$dn); 1266 list($var,$val) = explode('=',$top); 1267 $data = array( 1268 'objectClass' => $var == 'cn' ? 'organizationalRole' : 'organizationalUnit', 1269 $var => $val, 1270 ); 1271 // create a admin connection to add the needed DN 1272 if (!isset($adminDS)) $adminDS = Api\Ldap::factory(); 1273 if(!@ldap_add($adminDS, $dn, $data)) 1274 { 1275 //echo "<p>ldap_add($adminDS,'$dn',".print_r($data,true).")</p>\n"; 1276 $err = lang("Can't create dn %1",$dn).': '.$this->_error(__LINE__,$adminDS); 1277 return $err; 1278 } 1279 } 1280 } 1281 1282 return false; 1283 } 1284 1285 /** 1286 * error message for failed ldap operation 1287 * 1288 * @param int $line 1289 * @return string 1290 */ 1291 function _error($line,$ds=null) 1292 { 1293 return ldap_error($ds ? $ds : $this->ds).': '.__CLASS__.': '.$line; 1294 } 1295 1296 /** 1297 * Special handling for mapping of eGW contact-data to the evolutionPerson objectclass 1298 * 1299 * Please note: all regular fields are already copied! 1300 * 1301 * @internal 1302 * @param array &$ldapContact already copied fields according to the mapping 1303 * @param array $data eGW contact data 1304 * @param boolean $isUpdate 1305 */ 1306 function _egw2evolutionperson(&$ldapContact,$data,$isUpdate) 1307 { 1308 if(!empty($data['cat_id'])) 1309 { 1310 $ldapContact['category'] = array(); 1311 foreach(is_array($data['cat_id']) ? $data['cat_id'] : explode(',',$data['cat_id']) as $cat) 1312 { 1313 $ldapContact['category'][] = Api\Translation::convert( 1314 Api\Categories::id2name($cat),$this->charset,'utf-8'); 1315 } 1316 } 1317 foreach(array( 1318 'postaladdress' => $data['adr_one_street'] .'$'. $data['adr_one_locality'] .', '. $data['adr_one_region'] .'$'. $data['adr_one_postalcode'] .'$$'. $data['adr_one_countryname'], 1319 'homepostaladdress' => $data['adr_two_street'] .'$'. $data['adr_two_locality'] .', '. $data['adr_two_region'] .'$'. $data['adr_two_postalcode'] .'$$'. $data['adr_two_countryname'], 1320 ) as $attr => $value) 1321 { 1322 if($value != '$, $$$') 1323 { 1324 $ldapContact[$attr] = Api\Translation::convert($value,$this->charset,'utf-8'); 1325 } 1326 elseif($isUpdate) 1327 { 1328 $ldapContact[$attr] = array(); 1329 } 1330 } 1331 // save the phone number of the primary contact and not the eGW internal field-name 1332 if ($data['tel_prefer'] && $data[$data['tel_prefer']]) 1333 { 1334 $ldapContact['primaryphone'] = $data[$data['tel_prefer']]; 1335 } 1336 elseif($isUpdate) 1337 { 1338 $ldapContact['primaryphone'] = array(); 1339 } 1340 } 1341 1342 /** 1343 * Special handling for mapping data of the evolutionPerson objectclass to eGW contact 1344 * 1345 * Please note: all regular fields are already copied! 1346 * 1347 * @internal 1348 * @param array &$contact already copied fields according to the mapping 1349 * @param array $data eGW contact data 1350 */ 1351 function _evolutionperson2egw(&$contact,$data) 1352 { 1353 if ($data['category'] && is_array($data['category'])) 1354 { 1355 $contact['cat_id'] = array(); 1356 foreach($data['category'] as $iii => $cat) 1357 { 1358 if (!is_int($iii)) continue; 1359 1360 $contact['cat_id'][] = $GLOBALS['egw']->categories->name2id($cat); 1361 } 1362 if ($contact['cat_id']) $contact['cat_id'] = implode(',',$contact['cat_id']); 1363 } 1364 if ($data['primaryphone']) 1365 { 1366 unset($contact['tel_prefer']); // to not find itself 1367 $contact['tel_prefer'] = array_search($data['primaryphone'][0],$contact); 1368 } 1369 } 1370 1371 /** 1372 * Special handling for mapping data of the inetOrgPerson objectclass to eGW contact 1373 * 1374 * Please note: all regular fields are already copied! 1375 * 1376 * @internal 1377 * @param array &$contact already copied fields according to the mapping 1378 * @param array $data eGW contact data 1379 */ 1380 function _inetorgperson2egw(&$contact, $data, $cn='cn') 1381 { 1382 $matches = null; 1383 if(empty($data['givenname'][0])) 1384 { 1385 $parts = explode($data['sn'][0], $data[$cn][0]); 1386 $contact['n_prefix'] = trim($parts[0]); 1387 $contact['n_suffix'] = trim($parts[1]); 1388 } 1389 // iOS addressbook either use "givenname surname" or "surname givenname" depending on contact preference display-order 1390 // in full name, so we need to check for both when trying to parse prefix, middle name and suffix form full name 1391 elseif (preg_match($preg='/^(.*) *'.preg_quote($data['givenname'][0], '/').' *(.*) *'.preg_quote($data['sn'][0], '/').' *(.*)$/', $data[$cn][0], $matches) || 1392 preg_match($preg='/^(.*) *'.preg_quote($data['sn'][0], '/').'[, ]*(.*) *'.preg_quote($data['givenname'][0], '/').' *(.*)$/', $data[$cn][0], $matches)) 1393 { 1394 list(,$contact['n_prefix'], $contact['n_middle'], $contact['n_suffix']) = $matches; 1395 //error_log(__METHOD__."() preg_match('$preg', '{$data[$cn][0]}') = ".array2string($matches)); 1396 } 1397 else 1398 { 1399 $contact['n_prefix'] = $contact['n_suffix'] = $contact['n_middle'] = ''; 1400 } 1401 //error_log(__METHOD__."(, data=array($cn=>{$data[$cn][0]}, sn=>{$data['sn'][0]}, givenName=>{$data['givenname'][0]}), cn='$cn') returning with contact=array(n_prefix={$contact['n_prefix']}, n_middle={$contact['n_middle']}, n_suffix={$contact['n_suffix']}) ".function_backtrace()); 1402 } 1403 1404 /** 1405 * Special handling for mapping data of posixAccount objectclass to eGW contact 1406 * 1407 * Please note: all regular fields are already copied! 1408 * 1409 * @internal 1410 * @param array &$contact already copied fields according to the mapping 1411 * @param array $data eGW contact data 1412 */ 1413 function _posixaccount2egw(&$contact,$data) 1414 { 1415 unset($contact); // not used, but required by function signature 1416 static $shadowExpireNow=null; 1417 if (!isset($shadowExpireNow)) $shadowExpireNow = floor((time()-date('Z'))/86400); 1418 1419 // exclude expired or deactivated accounts 1420 if (isset($data['shadowexpire']) && $data['shadowexpire'][0] <= $shadowExpireNow) 1421 { 1422 return false; 1423 } 1424 } 1425 1426 /** 1427 * Special handling for mapping data of the mozillaAbPersonAlpha objectclass to eGW contact 1428 * 1429 * Please note: all regular fields are already copied! 1430 * 1431 * @internal 1432 * @param array &$contact already copied fields according to the mapping 1433 * @param array $data eGW contact data 1434 */ 1435 function _mozillaabpersonalpha2egw(&$contact,$data) 1436 { 1437 if ($data['c']) 1438 { 1439 $contact['adr_one_countryname'] = Api\Country::get_full_name($data['c'][0]); 1440 } 1441 } 1442 1443 /** 1444 * Special handling for mapping of eGW contact-data to the mozillaAbPersonAlpha objectclass 1445 * 1446 * Please note: all regular fields are already copied! 1447 * 1448 * @internal 1449 * @param array &$ldapContact already copied fields according to the mapping 1450 * @param array $data eGW contact data 1451 * @param boolean $isUpdate 1452 */ 1453 function _egw2mozillaabpersonalpha(&$ldapContact,$data,$isUpdate) 1454 { 1455 if ($data['adr_one_countrycode']) 1456 { 1457 $ldapContact['c'] = $data['adr_one_countrycode']; 1458 } 1459 elseif ($data['adr_one_countryname']) 1460 { 1461 $ldapContact['c'] = Api\Country::country_code($data['adr_one_countryname']); 1462 if ($ldapContact['c'] && strlen($ldapContact['c']) > 2) // Bad countryname when "custom" selected! 1463 { 1464 $ldapContact['c'] = array(); // should return error... 1465 } 1466 } 1467 elseif ($isUpdate) 1468 { 1469 $ldapContact['c'] = array(); 1470 } 1471 } 1472 1473 /** 1474 * Special handling for mapping data of the mozillaOrgPerson objectclass to eGW contact 1475 * 1476 * Please note: all regular fields are already copied! 1477 * 1478 * @internal 1479 * @param array &$contact already copied fields according to the mapping 1480 * @param array $data eGW contact data 1481 */ 1482 function _mozillaorgperson2egw(&$contact,$data) 1483 { 1484 unset($contact, $data); // not used, but required by function signature 1485 // no special handling necessary, as it supports two distinct attributes: c, cn 1486 } 1487 1488 /** 1489 * Special handling for mapping of eGW contact-data to the mozillaOrgPerson objectclass 1490 * 1491 * Please note: all regular fields are already copied! 1492 * 1493 * @internal 1494 * @param array &$ldapContact already copied fields according to the mapping 1495 * @param array $data eGW contact data 1496 * @param boolean $isUpdate 1497 */ 1498 function _egw2mozillaorgperson(&$ldapContact,$data,$isUpdate) 1499 { 1500 if ($data['adr_one_countrycode']) 1501 { 1502 $ldapContact['c'] = $data['adr_one_countrycode']; 1503 if ($isUpdate) $ldapContact['co'] = array(); 1504 } 1505 elseif ($data['adr_one_countryname']) 1506 { 1507 $ldapContact['c'] = Api\Country::country_code($data['adr_one_countryname']); 1508 if ($ldapContact['c'] && strlen($ldapContact['c']) > 2) // Bad countryname when "custom" selected! 1509 { 1510 $ldapContact['c'] = array(); // should return error... 1511 } 1512 } 1513 elseif ($isUpdate) 1514 { 1515 $ldapContact['c'] = $ldapContact['co'] = array(); 1516 } 1517 //error_log(__METHOD__."() adr_one_countrycode='{$data['adr_one_countrycode']}', adr_one_countryname='{$data['adr_one_countryname']}' --> c=".array2string($ldapContact['c']).', co='.array2string($ldapContact['co'])); 1518 } 1519 1520 /** 1521 * Change the ownership of contacts owned by a given account 1522 * 1523 * @param int $account_id account-id of the old owner 1524 * @param int $new_owner account-id of the new owner 1525 */ 1526 function change_owner($account_id,$new_owner) 1527 { 1528 error_log(__METHOD__."($account_id,$new_owner) not yet implemented"); 1529 } 1530} 1531