1<?php 2/** 3 * EGroupware API - Contacts storage object 4 * 5 * @link http://www.egroupware.org 6 * @author Cornelius Weiss <egw-AT-von-und-zu-weiss.de> 7 * @author Ralf Becker <RalfBecker-AT-outdoor-training.de> 8 * @package addressbook 9 * @copyright (c) 2005-16 by Ralf Becker <RalfBecker-AT-outdoor-training.de> 10 * @copyright (c) 2005/6 by Cornelius Weiss <egw@von-und-zu-weiss.de> 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 * Contacts storage object 21 * 22 * The contact storage has 3 operation modi (contact_repository): 23 * - sql: contacts are stored in the SQL table egw_addressbook & egw_addressbook_extra (custom fields) 24 * - ldap: contacts are stored in LDAP (accounts have to be stored in LDAP too!!!). 25 * Custom fields are not availible in that case! 26 * - sql-ldap: contacts are read and searched in SQL, but saved to both SQL and LDAP. 27 * Other clients (Thunderbird, ...) can use LDAP readonly. The get maintained via eGroupWare only. 28 * 29 * The accounts can be stored in SQL or LDAP too (account_repository): 30 * If the account-repository is different from the contacts-repository, the filter all (no owner set) 31 * will only search the contacts and NOT the accounts! Only the filter accounts (owner=0) shows accounts. 32 * 33 * If sql-ldap is used as contact-storage (LDAP is managed from eGroupWare) the filter all, searches 34 * the accounts in the SQL contacts-table too. Change in made in LDAP, are not detected in that case! 35 */ 36 37class Storage 38{ 39 /** 40 * name of customefields table 41 * 42 * @var string 43 */ 44 var $extra_table = 'egw_addressbook_extra'; 45 46 /** 47 * @var string 48 */ 49 var $extra_id = 'contact_id'; 50 51 /** 52 * @var string 53 */ 54 var $extra_owner = 'contact_owner'; 55 56 /** 57 * @var string 58 */ 59 var $extra_key = 'contact_name'; 60 61 /** 62 * @var string 63 */ 64 var $extra_value = 'contact_value'; 65 66 /** 67 * view for distributionlistsmembership 68 * 69 * @var string 70 */ 71 var $distributionlist_view ='(SELECT contact_id, egw_addressbook_lists.list_id as list_id, egw_addressbook_lists.list_name as list_name, egw_addressbook_lists.list_owner as list_owner FROM egw_addressbook_lists, egw_addressbook2list where egw_addressbook_lists.list_id=egw_addressbook2list.list_id) d_view '; 72 var $distributionlist_tabledef = array(); 73 /** 74 * @var string 75 */ 76 var $distri_id = 'contact_id'; 77 78 /** 79 * @var string 80 */ 81 var $distri_owner = 'list_owner'; 82 83 /** 84 * @var string 85 */ 86 var $distri_key = 'list_id'; 87 88 /** 89 * @var string 90 */ 91 var $distri_value = 'list_name'; 92 93 /** 94 * Contact repository in 'sql' or 'ldap' 95 * 96 * @var string 97 */ 98 var $contact_repository = 'sql'; 99 100 /** 101 * Grants as account_id => rights pairs 102 * 103 * @var array 104 */ 105 var $grants = array(); 106 107 /** 108 * userid of current user 109 * 110 * @var int 111 */ 112 var $user; 113 114 /** 115 * memberships of the current user 116 * 117 * @var array 118 */ 119 var $memberships; 120 121 /** 122 * In SQL we can search all columns, though a view make on real sense 123 */ 124 var $sql_cols_not_to_search = array( 125 'jpegphoto','owner','tid','private','cat_id','etag', 126 'modified','modifier','creator','created','tz','account_id', 127 'uid','carddav_name','freebusy_uri','calendar_uri', 128 'geo','pubkey', 129 ); 130 /** 131 * columns to search, if we search for a single pattern 132 * 133 * @var array 134 */ 135 var $columns_to_search = array(); 136 /** 137 * extra columns to search if accounts are included, eg. account_lid 138 * 139 * @var array 140 */ 141 var $account_extra_search = array(); 142 /** 143 * columns to search for accounts, if stored in different repository 144 * 145 * @var array 146 */ 147 var $account_cols_to_search = array(); 148 149 /** 150 * customfields name => array(...) pairs 151 * 152 * @var array 153 */ 154 var $customfields = array(); 155 /** 156 * content-types as name => array(...) pairs 157 * 158 * @var array 159 */ 160 var $content_types = array(); 161 162 /** 163 * Directory to store striped photo or public keys in VFS directory of entry 164 */ 165 const FILES_DIRECTORY = '.files'; 166 const FILES_PHOTO = '.files/photo.jpeg'; 167 const FILES_PGP_PUBKEY = '.files/pgp-pubkey.asc'; 168 const FILES_SMIME_PUBKEY = '.files/smime-pubkey.crt'; 169 170 /** 171 * Constant for bit-field "contact_files" storing what files are available 172 */ 173 const FILES_BIT_PHOTO = 1; 174 const FILES_BIT_PGP_PUBKEY = 2; 175 const FILES_BIT_SMIME_PUBKEY = 4; 176 177 /** 178 * These fields are options for checking for duplicate contacts 179 * 180 * @var array 181 */ 182 public static $duplicate_fields = array( 183 'n_given' => 'first name', 184 'n_middle' => 'middle name', 185 'n_family' => 'last name', 186 'contact_bday' => 'birthday', 187 'org_name' => 'Organisation', 188 'org_unit' => 'Department', 189 'adr_one_locality' => 'Location', 190 'contact_title' => 'title', 191 'contact_email' => 'business email', 192 'contact_email_home'=> 'email (private)', 193 ); 194 195 /** 196 * Special content type to indicate a deleted addressbook 197 * 198 * @var String; 199 */ 200 const DELETED_TYPE = 'D'; 201 202 /** 203 * total number of matches of last search 204 * 205 * @var int 206 */ 207 var $total; 208 209 /** 210 * storage object: sql (Sql) or ldap (addressbook_ldap) backend class 211 * 212 * @var Sql 213 */ 214 var $somain; 215 /** 216 * storage object for accounts, if not identical to somain (eg. accounts in ldap, contacts in sql) 217 * 218 * @var Ldap 219 */ 220 var $so_accounts; 221 /** 222 * account repository sql or ldap 223 * 224 * @var string 225 */ 226 var $account_repository = 'sql'; 227 /** 228 * custom fields backend 229 * 230 * @var Sql 231 */ 232 var $soextra; 233 var $sodistrib_list; 234 235 /** 236 * Constructor 237 * 238 * @param string $contact_app ='addressbook' used for acl->get_grants() 239 * @param Api\Db $db =null 240 */ 241 function __construct($contact_app='addressbook',Api\Db $db=null) 242 { 243 $this->db = is_null($db) ? $GLOBALS['egw']->db : $db; 244 245 $this->user = $GLOBALS['egw_info']['user']['account_id']; 246 $this->memberships = $GLOBALS['egw']->accounts->memberships($this->user,true); 247 248 // account backend used 249 if ($GLOBALS['egw_info']['server']['account_repository']) 250 { 251 $this->account_repository = $GLOBALS['egw_info']['server']['account_repository']; 252 } 253 elseif ($GLOBALS['egw_info']['server']['auth_type']) 254 { 255 $this->account_repository = $GLOBALS['egw_info']['server']['auth_type']; 256 } 257 $this->customfields = Api\Storage\Customfields::get('addressbook'); 258 // contacts backend (contacts in LDAP require accounts in LDAP!) 259 if($GLOBALS['egw_info']['server']['contact_repository'] == 'ldap' && $this->account_repository == 'ldap') 260 { 261 $this->contact_repository = 'ldap'; 262 $this->somain = new Ldap(); 263 $this->columns_to_search = $this->somain->search_attributes; 264 } 265 else // sql or sql->ldap 266 { 267 if ($GLOBALS['egw_info']['server']['contact_repository'] == 'sql-ldap') 268 { 269 $this->contact_repository = 'sql-ldap'; 270 } 271 $this->somain = new Sql($db); 272 273 // remove some columns, absolutly not necessary to search in sql 274 $this->columns_to_search = array_diff(array_values($this->somain->db_cols),$this->sql_cols_not_to_search); 275 } 276 $this->grants = $this->get_grants($this->user,$contact_app); 277 278 if ($this->account_repository != 'sql' && $this->contact_repository == 'sql') 279 { 280 if ($this->account_repository != $this->contact_repository) 281 { 282 $class = 'EGroupware\\Api\\Contacts\\'.ucfirst($this->account_repository); 283 $this->so_accounts = new $class(); 284 $this->account_cols_to_search = $this->so_accounts->search_attributes; 285 } 286 else 287 { 288 $this->account_extra_search = array('uid'); 289 } 290 } 291 if ($this->contact_repository == 'sql' || $this->contact_repository == 'sql-ldap') 292 { 293 $tda2list = $this->db->get_table_definitions('api','egw_addressbook2list'); 294 $tdlists = $this->db->get_table_definitions('api','egw_addressbook_lists'); 295 $this->distributionlist_tabledef = array('fd' => array( 296 $this->distri_id => $tda2list['fd'][$this->distri_id], 297 $this->distri_owner => $tdlists['fd'][$this->distri_owner], 298 $this->distri_key => $tdlists['fd'][$this->distri_key], 299 $this->distri_value => $tdlists['fd'][$this->distri_value], 300 ), 'pk' => array(), 'fk' => array(), 'ix' => array(), 'uc' => array(), 301 ); 302 } 303 // ToDo: it should be the other way arround, the backend should set the grants it uses 304 $this->somain->grants =& $this->grants; 305 306 if($this->somain instanceof Sql) 307 { 308 $this->soextra =& $this->somain; 309 } 310 else 311 { 312 $this->soextra = new Sql($db); 313 } 314 315 $this->content_types = Api\Config::get_content_types('addressbook'); 316 if (!$this->content_types) 317 { 318 $this->content_types = array('n' => array( 319 'name' => 'contact', 320 'options' => array( 321 'template' => 'addressbook.edit', 322 'icon' => 'navbar.png' 323 ))); 324 } 325 326 // Add in deleted type, if holding deleted contacts 327 $config = Api\Config::read('phpgwapi'); 328 if($config['history']) 329 { 330 $this->content_types[self::DELETED_TYPE] = array( 331 'name' => lang('Deleted'), 332 'options' => array( 333 'template' => 'addressbook.edit', 334 'icon' => 'deleted.png' 335 ) 336 ); 337 } 338 } 339 340 /** 341 * Get grants for a given user, taking into account static LDAP ACL 342 * 343 * @param int $user 344 * @param string $contact_app ='addressbook' 345 * @return array 346 */ 347 function get_grants($user, $contact_app='addressbook', $preferences=null) 348 { 349 if (!isset($preferences)) $preferences = $GLOBALS['egw_info']['user']['preferences']; 350 351 if ($user) 352 { 353 // contacts backend (contacts in LDAP require accounts in LDAP!) 354 if($GLOBALS['egw_info']['server']['contact_repository'] == 'ldap' && $this->account_repository == 'ldap') 355 { 356 // static grants from ldap: all rights for the own personal addressbook and the group ones of the meberships 357 $grants = array($user => ~0); 358 foreach($GLOBALS['egw']->accounts->memberships($user,true) as $gid) 359 { 360 $grants[$gid] = ~0; 361 } 362 } 363 else // sql or sql->ldap 364 { 365 // group grants are now grants for the group addressbook and NOT grants for all its members, 366 // therefor the param false! 367 $grants = $GLOBALS['egw']->acl->get_grants($contact_app,false,$user); 368 } 369 // add grants for accounts: if account_selection not in ('none','groupmembers'): everyone has read access, 370 // if he has not set the hide_accounts preference 371 // ToDo: be more specific for 'groupmembers', they should be able to see the groupmembers 372 if (!in_array($preferences['common']['account_selection'], array('none','groupmembers'))) 373 { 374 $grants[0] = Api\Acl::READ; 375 } 376 // add account grants for admins (only for current user!) 377 if ($user == $this->user && $this->is_admin()) // admin rights can be limited by ACL! 378 { 379 $grants[0] = Api\Acl::READ; // admins always have read-access 380 if (!$GLOBALS['egw']->acl->check('account_access',16,'admin')) $grants[0] |= Api\Acl::EDIT; 381 if (!$GLOBALS['egw']->acl->check('account_access',4,'admin')) $grants[0] |= Api\Acl::ADD; 382 if (!$GLOBALS['egw']->acl->check('account_access',32,'admin')) $grants[0] |= Api\Acl::DELETE; 383 } 384 // allow certain groups to edit contact-data of accounts 385 if (self::allow_account_edit($user)) 386 { 387 $grants[0] |= Api\Acl::READ|Api\Acl::EDIT; 388 } 389 } 390 // no user, eg. setup or not logged in, allow read access to accounts 391 else 392 { 393 $grants = [0 => Api\Acl::READ]; 394 } 395 //error_log(__METHOD__."($user, '$contact_app') returning ".array2string($grants)); 396 return $grants; 397 } 398 399 /** 400 * Check if the user is an admin (can unconditionally edit accounts) 401 * 402 * We check now the admin ACL for edit users, as the admin app does it for editing accounts. 403 * 404 * @param array $contact =null for future use, where admins might not be admins for all accounts 405 * @return boolean 406 */ 407 function is_admin($contact=null) 408 { 409 unset($contact); // not (yet) used 410 411 return isset($GLOBALS['egw_info']['user']['apps']['admin']) && !$GLOBALS['egw']->acl->check('account_access',16,'admin'); 412 } 413 414 /** 415 * Check if current user is in a group, which is allowed to edit accounts 416 * 417 * @param int $user =null default $this->user 418 * @return boolean 419 */ 420 function allow_account_edit($user=null) 421 { 422 return $GLOBALS['egw_info']['server']['allow_account_edit'] && 423 array_intersect($GLOBALS['egw_info']['server']['allow_account_edit'], 424 $GLOBALS['egw']->accounts->memberships($user ? $user : $this->user, true)); 425 } 426 427 /** 428 * Read all customfields of the given id's 429 * 430 * @param int|array $ids 431 * @param array $field_names =null custom fields to read, default all 432 * @return array id => name => value 433 */ 434 function read_customfields($ids,$field_names=null) 435 { 436 return $this->soextra->read_customfields($ids,$field_names); 437 } 438 439 /** 440 * Read all distributionlists of the given id's 441 * 442 * @param int|array $ids 443 * @return array id => name => value 444 */ 445 function read_distributionlist($ids, $dl_allowed=array()) 446 { 447 if ($this->contact_repository == 'ldap') 448 { 449 return array(); // ldap does not support distributionlists 450 } 451 foreach($ids as $key => $id) 452 { 453 if (!is_numeric($id)) unset($ids[$key]); 454 } 455 if (!$ids) return array(); // nothing to do, eg. all these contacts are in ldap 456 $fields = array(); 457 $filter[$this->distri_id]=$ids; 458 if (count($dl_allowed)) $filter[$this->distri_key]=$dl_allowed; 459 $distri_view = str_replace(') d_view',' and '.$this->distri_id.' in ('.implode(',',$ids).')) d_view',$this->distributionlist_view); 460 #_debug_array($this->distributionlist_tabledef); 461 foreach($this->db->select($distri_view, '*', $filter, __LINE__, __FILE__, 462 false, 'ORDER BY '.$this->distri_id, false, 0, '', $this->distributionlist_tabledef) as $row) 463 { 464 if ((isset($row[$this->distri_id])&&strlen($row[$this->distri_value])>0)) 465 { 466 $fields[$row[$this->distri_id]][$row[$this->distri_key]] = $row[$this->distri_value].' ('. 467 Api\Accounts::username($row[$this->distri_owner]).')'; 468 } 469 } 470 return $fields; 471 } 472 473 /** 474 * changes the data from the db-format to your work-format 475 * 476 * it gets called everytime when data is read from the db 477 * This function needs to be reimplemented in the derived class 478 * 479 * @param array $data 480 */ 481 function db2data($data) 482 { 483 return $data; 484 } 485 486 /** 487 * changes the data from your work-format to the db-format 488 * 489 * It gets called everytime when data gets writen into db or on keys for db-searches 490 * this needs to be reimplemented in the derived class 491 * 492 * @param array $data 493 */ 494 function data2db($data) 495 { 496 return $data; 497 } 498 499 /** 500 * deletes contact entry including custom fields 501 * 502 * @param mixed $contact array with id or just the id 503 * @param int $check_etag =null 504 * @return boolean|int true on success or false on failiure, 0 if etag does not match 505 */ 506 function delete($contact,$check_etag=null) 507 { 508 if (is_array($contact)) $contact = $contact['id']; 509 510 $where = array('id' => $contact); 511 if ($check_etag) $where['etag'] = $check_etag; 512 513 // delete mainfields 514 if ($this->somain->delete($where)) 515 { 516 // delete customfields, can return 0 if there are no customfields 517 if(!($this->somain instanceof Sql)) 518 { 519 $this->soextra->delete_customfields(array($this->extra_id => $contact)); 520 } 521 522 // delete from distribution list(s) 523 $this->remove_from_list($contact); 524 525 if ($this->contact_repository == 'sql-ldap') 526 { 527 if ($contact['account_id']) 528 { 529 // LDAP uses the uid attributes for the contact-id (dn), 530 // which need to be the account_lid for accounts! 531 $contact['id'] = $GLOBALS['egw']->accounts->id2name($contact['account_id']); 532 } 533 (new Ldap())->delete($contact); 534 } 535 return true; 536 } 537 return $check_etag ? 0 : false; // if etag given, we return 0 on failure, thought it could also mean the whole contact does not exist 538 } 539 540 /** 541 * saves contact data including custom fields 542 * 543 * @param array &$contact contact data from etemplate::exec 544 * @return bool false on success, errornumber on failure 545 */ 546 function save(&$contact) 547 { 548 // save mainfields 549 if ($contact['id'] && $this->contact_repository != $this->account_repository && is_object($this->so_accounts) && 550 ($this->contact_repository == 'sql' && !is_numeric($contact['id']) || 551 $this->contact_repository == 'ldap' && is_numeric($contact['id']))) 552 { 553 $this->so_accounts->data = $this->data2db($contact); 554 $error_nr = $this->so_accounts->save(); 555 $contact['id'] = $this->so_accounts->data['id']; 556 } 557 else 558 { 559 // contact_repository sql-ldap (accounts in ldap) the person_id is the uid (account_lid) 560 // for the sql write here we need to find out the existing contact_id 561 if ($this->contact_repository == 'sql-ldap' && $contact['id'] && !is_numeric($contact['id']) && 562 $contact['account_id'] && ($old = $this->somain->read(array('account_id' => $contact['account_id'])))) 563 { 564 $contact['id'] = $old['id']; 565 } 566 $this->somain->data = $this->data2db($contact); 567 568 if (!($error_nr = $this->somain->save())) 569 { 570 $contact['id'] = $this->somain->data['id']; 571 $contact['uid'] = $this->somain->data['uid']; 572 $contact['etag'] = $this->somain->data['etag']; 573 $contact['files'] = $this->somain->data['files']; 574 575 if ($this->contact_repository == 'sql-ldap') 576 { 577 $data = $this->somain->data; 578 if ($contact['account_id']) 579 { 580 // LDAP uses the uid attributes for the contact-id (dn), 581 // which need to be the account_lid for accounts! 582 $data['id'] = $GLOBALS['egw']->accounts->id2name($contact['account_id']); 583 } 584 $error_nr = (new Ldap())->save($data); 585 } 586 } 587 } 588 if($error_nr) return $error_nr; 589 590 return false; // no error 591 } 592 593 /** 594 * reads contact data including custom fields 595 * 596 * @param int|string $contact_id contact_id or 'a'.account_id 597 * @return array|boolean data if row could be retrived else False 598 */ 599 function read($contact_id) 600 { 601 if (empty($contact_id)) 602 { 603 return false; // no need to pass to backend, will fail anyway 604 } 605 if (!is_array($contact_id) && substr($contact_id,0,8) == 'account:') 606 { 607 $contact_id = array('account_id' => (int) substr($contact_id,8)); 608 } 609 // read main data 610 $backend = $this->get_backend($contact_id); 611 if (!($contact = $backend->read($contact_id))) 612 { 613 return $contact; 614 } 615 $dl_list=$this->read_distributionlist(array($contact['id'])); 616 if (count($dl_list)) $contact['distrib_lists']=implode("\n",$dl_list[$contact['id']]); 617 return $this->db2data($contact); 618 } 619 620 /** 621 * @param array $ids 622 * @param ?boolean $deleted false: no deleted, true: only deleted, null: both 623 * @return array contact_id => array of array with sharing info 624 */ 625 function read_shared(array $ids, $deleted=false) 626 { 627 $contacts = []; 628 if (method_exists($backend = $this->get_backend($ids[0]), 'read_shared')) 629 { 630 foreach($backend->read_shared($ids, $deleted) as $shared) 631 { 632 $contacts[$shared['contact_id']][] = $shared; 633 } 634 } 635 return $contacts; 636 } 637 638 /** 639 * searches db for rows matching searchcriteria 640 * 641 * '*' and '?' are replaced with sql-wildcards '%' and '_' 642 * 643 * @param array|string $criteria array of key and data cols, OR string to search over all standard search fields 644 * @param boolean|string $only_keys =true True returns only keys, False returns all cols. comma seperated list of keys to return 645 * @param string $order_by ='' fieldnames + {ASC|DESC} separated by colons ',', can also contain a GROUP BY (if it contains ORDER BY) 646 * @param string|array $extra_cols ='' string or array of strings to be added to the SELECT, eg. "count(*) as num" 647 * @param string $wildcard ='' appended befor and after each criteria 648 * @param boolean $empty =false False=empty criteria are ignored in query, True=empty have to be empty in row 649 * @param string $op ='AND' defaults to 'AND', can be set to 'OR' too, then criteria's are OR'ed together 650 * @param mixed $start =false if != false, return only maxmatch rows begining with start, or array($start,$num) 651 * @param array $filter =null if set (!=null) col-data pairs, to be and-ed (!) into the query without wildcards 652 * $filter['cols_to_search'] limit search columns to given columns, otherwise $this->columns_to_search is used 653 * @param string $join ='' sql to do a join (only used by sql backend!), eg. " RIGHT JOIN egw_accounts USING(account_id)" 654 * @param boolean $ignore_acl =false true: no acl check 655 * @return array of matching rows (the row is an array of the cols) or False 656 */ 657 function &search($criteria,$only_keys=True,$order_by='',$extra_cols='',$wildcard='',$empty=False,$op='AND',$start=false,$filter=null,$join='', $ignore_acl=false) 658 { 659 //error_log(__METHOD__.'('.array2string($criteria,true).','.array2string($only_keys).",'$order_by','$extra_cols','$wildcard','$empty','$op',".array2string($start).','.array2string($filter,true).",'$join')"); 660 661 // Handle 'None' country option 662 if(is_array($filter) && $filter['adr_one_countrycode'] == '-custom-') 663 { 664 $filter[] = 'adr_one_countrycode IS NULL'; 665 unset($filter['adr_one_countrycode']); 666 } 667 // Hide deleted items unless type is specifically deleted 668 if(!is_array($filter)) $filter = $filter ? (array) $filter : array(); 669 670 if (isset($filter['cols_to_search'])) 671 { 672 $cols_to_search = $filter['cols_to_search']; 673 unset($filter['cols_to_search']); 674 } 675 676 // if no tid set or tid==='' do NOT return deleted entries ($tid === null returns all entries incl. deleted) 677 if(!array_key_exists('tid', $filter) || $filter['tid'] === '') 678 { 679 if ($join && strpos($join,'RIGHT JOIN') !== false) // used eg. to search for groups 680 { 681 $filter[] = '(contact_tid != \'' . self::DELETED_TYPE . '\' OR contact_tid IS NULL)'; 682 } 683 else 684 { 685 $filter[] = 'contact_tid != \'' . self::DELETED_TYPE . '\''; 686 } 687 } 688 elseif(is_null($filter['tid'])) 689 { 690 unset($filter['tid']); // return all entries incl. deleted 691 } 692 $backend = $this->get_backend(null, isset($filter['list']) && $filter['list'] < 0 ? 0 : $filter['owner']); 693 // single string to search for --> create so_sql conformant search criterial for the standard search columns 694 if ($criteria && !is_array($criteria)) 695 { 696 $op = 'OR'; 697 $wildcard = '%'; 698 $search = $criteria; 699 $criteria = array(); 700 701 if (isset($cols_to_search)) 702 { 703 $cols = $cols_to_search; 704 } 705 elseif ($backend === $this->somain) 706 { 707 $cols = $this->columns_to_search; 708 } 709 else 710 { 711 $cols = $this->account_cols_to_search; 712 } 713 if($backend instanceof Sql) 714 { 715 // Keep a string, let the parent handle it 716 $criteria = $search; 717 718 foreach($cols as $key => &$col) 719 { 720 if($col != Sql::EXTRA_VALUE && 721 $col != Sql::EXTRA_TABLE.'.'.Sql::EXTRA_VALUE && 722 !array_key_exists($col, $backend->db_cols)) 723 { 724 if(!($col = array_search($col, $backend->db_cols))) 725 { 726 // Can't search this column, it will error if we try 727 unset($cols[$key]); 728 } 729 } 730 if ($col=='contact_id') $col='egw_addressbook.contact_id'; 731 } 732 733 $backend->columns_to_search = $cols; 734 } 735 else 736 { 737 foreach($cols as $col) 738 { 739 // remove from LDAP backend not understood use-AND-syntax 740 $criteria[$col] = str_replace(' +',' ',$search); 741 } 742 } 743 } 744 if (is_array($criteria) && count($criteria)) 745 { 746 $criteria = $this->data2db($criteria); 747 } 748 if (is_array($filter) && count($filter)) 749 { 750 $filter = $this->data2db($filter); 751 } 752 else 753 { 754 $filter = $filter ? array($filter) : array(); 755 } 756 // get the used backend for the search and call it's search method 757 $rows = $backend->search($criteria, $only_keys, $order_by, $extra_cols, 758 $wildcard, $empty, $op, $start, $filter, $join, false, $ignore_acl); 759 760 $this->total = $backend->total; 761 762 if ($rows) 763 { 764 foreach($rows as $n => $row) 765 { 766 $rows[$n] = $this->db2data($row); 767 } 768 769 // allow other apps to hook into search 770 Api\Hooks::process(array( 771 'hook_location' => 'contacts_search', 772 'criteria' => $criteria, 773 'filter' => $filter, 774 'ignore_acl' => $ignore_acl, 775 'obj' => $this, 776 'rows' => &$rows, 777 'total' => &$this->total, 778 ), array(), true); // true = no permission check 779 } 780 return $rows; 781 } 782 783 /** 784 * Query organisations by given parameters 785 * 786 * @var array $param 787 * @var string $param[org_view] 'org_name', 'org_name,adr_one_location', 'org_name,org_unit' how to group 788 * @var int $param[owner] addressbook to search 789 * @var string $param[search] search pattern for org_name 790 * @var string $param[searchletter] letter the org_name need to start with 791 * @var int $param[start] 792 * @var int $param[num_rows] 793 * @var string $param[sort] ASC or DESC 794 * @return array or arrays with keys org_name,count and evtl. adr_one_location or org_unit 795 */ 796 function organisations($param) 797 { 798 if (!method_exists($this->somain,'organisations')) 799 { 800 $this->total = 0; 801 return false; 802 } 803 if ($param['search'] && !is_array($param['search'])) 804 { 805 $search = $param['search']; 806 $param['search'] = array(); 807 if($this->somain instanceof Sql) 808 { 809 // Keep the string, let the parent deal with it 810 $param['search'] = $search; 811 } 812 else 813 { 814 foreach($this->columns_to_search as $col) 815 { 816 if ($col != 'contact_value') $param['search'][$col] = $search; // we dont search the customfields 817 } 818 } 819 } 820 if (is_array($param['search']) && count($param['search'])) 821 { 822 $param['search'] = $this->data2db($param['search']); 823 } 824 if(!array_key_exists('tid', $param['col_filter']) || $param['col_filter']['tid'] === '') 825 { 826 $param['col_filter'][] = 'contact_tid != \'' . self::DELETED_TYPE . '\''; 827 } 828 elseif(is_null($param['col_filter']['tid'])) 829 { 830 unset($param['col_filter']['tid']); // return all entries incl. deleted 831 } 832 833 $rows = $this->somain->organisations($param); 834 $this->total = $this->somain->total; 835 836 if (!$rows) return array(); 837 838 foreach($rows as $n => $row) 839 { 840 if (strpos($row['org_name'],'&')!==false) $row['org_name'] = str_replace('&','*AND*',$row['org_name']); 841 $rows[$n]['id'] = 'org_name:'.$row['org_name']; 842 foreach(array( 843 'org_unit' => lang('departments'), 844 'adr_one_locality' => lang('locations'), 845 ) as $by => $by_label) 846 { 847 if ($row[$by.'_count'] > 1) 848 { 849 $rows[$n][$by] = $row[$by.'_count'].' '.$by_label; 850 } 851 else 852 { 853 if (strpos($row[$by],'&')!==false) $row[$by] = str_replace('&','*AND*',$row[$by]); 854 $rows[$n]['id'] .= '|||'.$by.':'.$row[$by]; 855 } 856 } 857 } 858 return $rows; 859 } 860 861 /** 862 * Find contacts that appear to be duplicates 863 * 864 * @param Array $param 865 * @param string $param[org_view] 'org_name', 'org_name,adr_one_location', 'org_name,org_unit' how to group 866 * @param int $param[owner] addressbook to search 867 * @param string $param[search] search pattern for org_name 868 * @param string $param[searchletter] letter the org_name need to start with 869 * @param int $param[start] 870 * @param int $param[num_rows] 871 * @param string $param[sort] ASC or DESC 872 * 873 * @return array of arrays 874 */ 875 public function duplicates($param) 876 { 877 if (!method_exists($this->somain,'duplicates')) 878 { 879 $this->total = 0; 880 return false; 881 } 882 if ($param['search'] && !is_array($param['search'])) 883 { 884 $search = $param['search']; 885 $param['search'] = array(); 886 if($this->somain instanceof Sql) 887 { 888 // Keep the string, let the parent deal with it 889 $param['search'] = $search; 890 } 891 else 892 { 893 foreach($this->columns_to_search as $col) 894 { 895 // we don't search the customfields 896 if ($col != 'contact_value') $param['search'][$col] = $search; 897 } 898 } 899 } 900 if (is_array($param['search']) && count($param['search'])) 901 { 902 $param['search'] = $this->data2db($param['search']); 903 } 904 if(!array_key_exists('tid', $param['col_filter']) || $param['col_filter']['tid'] === '') 905 { 906 $param['col_filter'][] = $this->somain->table_name.'.contact_tid != \'' . self::DELETED_TYPE . '\''; 907 } 908 elseif(is_null($param['col_filter']['tid'])) 909 { 910 // return all entries including deleted 911 unset($param['col_filter']['tid']); 912 } 913 if(array_key_exists('filter', $param) && $param['filter'] != '') 914 { 915 $param['owner'] = $param['filter']; 916 unset($param['filter']); 917 } 918 if(array_key_exists('owner', $param['col_filter']) && $param['col_filter']['owner'] != '') 919 { 920 $param['owner'] = $param['col_filter']['owner']; 921 unset($param['col_filter']['owner']); 922 } 923 924 925 $rows = $this->somain->duplicates($param); 926 $this->total = $this->somain->total; 927 928 if (!$rows) return array(); 929 930 foreach($rows as $n => $row) 931 { 932 $rows[$n]['id'] = 'duplicate:'; 933 foreach(array_keys(static::$duplicate_fields) as $by) 934 { 935 if (strpos($row[$by],'&')!==false) $row[$by] = str_replace('&','*AND*',$row[$by]); 936 if($row[$by]) 937 { 938 $rows[$n]['id'] .= '|||'.$by.':'.$row[$by]; 939 } 940 } 941 } 942 return $rows; 943 } 944 945 /** 946 * gets all contact fields from database 947 * 948 * @return array of (internal) field-names 949 */ 950 function get_contact_columns() 951 { 952 $fields = $this->get_fields('all'); 953 foreach (array_keys((array)$this->customfields) as $cfield) 954 { 955 $fields[] = '#'.$cfield; 956 } 957 return $fields; 958 } 959 960 /** 961 * delete / move all contacts of an addressbook 962 * 963 * @param array $data 964 * @param int $data['account_id'] owner to change 965 * @param int $data['new_owner'] new owner or 0 for delete 966 */ 967 function deleteaccount($data) 968 { 969 $account_id = $data['account_id']; 970 $new_owner = $data['new_owner']; 971 972 if (!$new_owner) 973 { 974 $this->somain->delete(array('owner' => $account_id)); // so_sql_cf::delete() takes care of cfs too 975 976 if (method_exists($this->somain, 'get_lists') && 977 ($lists = $this->somain->get_lists($account_id))) 978 { 979 $this->somain->delete_list(array_keys($lists)); 980 } 981 } 982 else 983 { 984 $this->somain->change_owner($account_id,$new_owner); 985 } 986 } 987 988 /** 989 * return the backend, to be used for the given $contact_id 990 * 991 * @param array|string|int $keys =null 992 * @param int $owner =null account_id of owner or 0 for accounts 993 * @return Sql|Ldap|Ads|Univention 994 */ 995 function get_backend($keys=null,$owner=null) 996 { 997 if ($owner === '') $owner = null; 998 999 $contact_id = !is_array($keys) ? $keys : 1000 (isset($keys['id']) ? $keys['id'] : $keys['contact_id']); 1001 1002 if ($this->contact_repository != $this->account_repository && is_object($this->so_accounts) && 1003 (!is_null($owner) && !$owner || is_array($keys) && $keys['account_id'] || !is_null($contact_id) && 1004 ($this->contact_repository == 'sql' && (!is_numeric($contact_id) && !is_array($contact_id) )|| 1005 $this->contact_repository == 'ldap' && is_numeric($contact_id)))) 1006 { 1007 return $this->so_accounts; 1008 } 1009 return $this->somain; 1010 } 1011 1012 /** 1013 * Returns the supported, all or unsupported fields of the backend (depends on owner or contact_id) 1014 * 1015 * @param sting $type ='all' 'supported', 'unsupported' or 'all' 1016 * @param mixed $contact_id =null 1017 * @param int $owner =null account_id of owner or 0 for accounts 1018 * @return array with eGW contact field names 1019 */ 1020 function get_fields($type='all',$contact_id=null,$owner=null) 1021 { 1022 $def = $this->db->get_table_definitions('api','egw_addressbook'); 1023 1024 $all_fields = array(); 1025 foreach(array_keys($def['fd']) as $field) 1026 { 1027 $all_fields[] = substr($field,0,8) == 'contact_' ? substr($field,8) : $field; 1028 } 1029 if ($type == 'all') 1030 { 1031 return $all_fields; 1032 } 1033 $backend = $this->get_backend($contact_id,$owner); 1034 1035 $supported_fields = method_exists($backend, 'supported_fields') ? $backend->supported_fields() : $all_fields; 1036 1037 if ($type == 'supported') 1038 { 1039 return $supported_fields; 1040 } 1041 return array_diff($all_fields,$supported_fields); 1042 } 1043 1044 /** 1045 * Migrates an SQL contact storage to LDAP, SQL-LDAP or back to SQL 1046 * 1047 * @param string|array $type comma-separated list or array of: 1048 * - "contacts" contacts to ldap 1049 * - "accounts" accounts to ldap 1050 * - "accounts-back" accounts back to sql (for sql-ldap!) 1051 * - "sql" contacts and accounts to sql 1052 * - "accounts-back-ads" accounts back from ads to sql 1053 */ 1054 function migrate2ldap($type) 1055 { 1056 //error_log(__METHOD__."(".array2string($type).")"); 1057 $sql_contacts = new Sql(); 1058 if ($type == 'accounts-back-ads') 1059 { 1060 $ldap_contacts = new Ads(); 1061 } 1062 else 1063 { 1064 // we need an admin connection 1065 $ds = $GLOBALS['egw']->ldap->ldapConnect(); 1066 $ldap_contacts = new Ldap(null, $ds); 1067 } 1068 1069 if (!is_array($type)) $type = explode(',', $type); 1070 1071 $start = $n = 0; 1072 $num = 100; 1073 1074 // direction SQL --> LDAP, either only accounts, or only contacts or both 1075 if (($do = array_intersect($type, array('contacts', 'accounts')))) 1076 { 1077 $filter = count($do) == 2 ? null : 1078 array($do[0] == 'contacts' ? 'contact_owner != 0' : 'contact_owner = 0'); 1079 1080 while (($contacts = $sql_contacts->search(false,false,'n_family,n_given','','',false,'AND', 1081 array($start,$num),$filter))) 1082 { 1083 foreach($contacts as $contact) 1084 { 1085 if ($contact['account_id']) $contact['id'] = $GLOBALS['egw']->accounts->id2name($contact['account_id']); 1086 1087 $ldap_contacts->data = $contact; 1088 $n++; 1089 if (!($err = $ldap_contacts->save())) 1090 { 1091 echo '<p style="margin: 0px;">'.$n.': '.$contact['n_fn']. 1092 ($contact['org_name'] ? ' ('.$contact['org_name'].')' : '')." --> LDAP</p>\n"; 1093 } 1094 else 1095 { 1096 echo '<p style="margin: 0px; color: red;">'.$n.': '.$contact['n_fn']. 1097 ($contact['org_name'] ? ' ('.$contact['org_name'].')' : '').': '.$err."</p>\n"; 1098 } 1099 } 1100 $start += $num; 1101 } 1102 } 1103 // direction LDAP --> SQL: either "sql" (contacts and accounts) or "accounts-back" (only accounts) 1104 if (($do = array_intersect(array('accounts-back','sql'), $type))) 1105 { 1106 //error_log(__METHOD__."(".array2string($type).") do=".array2string($type)); 1107 $filter = in_array('sql', $do) ? null : array('owner' => 0); 1108 1109 foreach($ldap_contacts->search(false,false,'n_family,n_given','','',false,'AND', 1110 false, $filter) as $contact) 1111 { 1112 //error_log(__METHOD__."(".array2string($type).") do=".array2string($type)." migrating ".array2string($contact)); 1113 if ($contact['jpegphoto']) // photo is NOT read by LDAP backend on search, need to do an extra read 1114 { 1115 $contact = $ldap_contacts->read($contact['id']); 1116 } 1117 $old_contact_id = $contact['id']; 1118 unset($contact['id']); // ldap uid/account_lid 1119 if ($contact['account_id'] && ($old = $sql_contacts->read(array('account_id' => $contact['account_id'])))) 1120 { 1121 $contact['id'] = $old['id']; 1122 } 1123 $sql_contacts->data = $contact; 1124 1125 $n++; 1126 if (!($err = $sql_contacts->save())) 1127 { 1128 echo '<p style="margin: 0px;">'.$n.': '.$contact['n_fn']. 1129 ($contact['org_name'] ? ' ('.$contact['org_name'].')' : '')." --> SQL (". 1130 ($contact['owner']?lang('User'):lang('Contact')).")<br>\n"; 1131 1132 $new_contact_id = $sql_contacts->data['id']; 1133 echo " " . $old_contact_id . " --> " . $new_contact_id . " / "; 1134 1135 $this->db->update('egw_links',array( 1136 'link_id1' => $new_contact_id, 1137 ),array( 1138 'link_app1' => 'addressbook', 1139 'link_id1' => $old_contact_id 1140 ),__LINE__,__FILE__); 1141 1142 $this->db->update('egw_links',array( 1143 'link_id2' => $new_contact_id, 1144 ),array( 1145 'link_app2' => 'addressbook', 1146 'link_id2' => $old_contact_id 1147 ),__LINE__,__FILE__); 1148 echo "</p>\n"; 1149 } 1150 else 1151 { 1152 echo '<p style="margin: 0px; color: red;">'.$n.': '.$contact['n_fn']. 1153 ($contact['org_name'] ? ' ('.$contact['org_name'].')' : '').': '.$err."</p>\n"; 1154 } 1155 } 1156 } 1157 } 1158 1159 /** 1160 * Get the availible distribution lists for a user 1161 * 1162 * @param int $required =Api\Acl::READ required rights on the list or multiple rights or'ed together, 1163 * to return only lists fullfilling all the given rights 1164 * @param string $extra_labels =null first labels if given (already translated) 1165 * @return array with id => label pairs or false if backend does not support lists 1166 */ 1167 function get_lists($required=Api\Acl::READ,$extra_labels=null) 1168 { 1169 $lists = is_array($extra_labels) ? $extra_labels : array(); 1170 1171 if (method_exists($this->somain,'get_lists')) 1172 { 1173 $uids = array(); 1174 foreach($this->grants as $uid => $rights) 1175 { 1176 // only requests groups / list in accounts addressbook for read 1177 if (!$uid && $required != Api\Acl::READ) continue; 1178 1179 if (($rights & $required) == $required) 1180 { 1181 $uids[] = $uid; 1182 } 1183 } 1184 1185 foreach($this->somain->get_lists($uids) as $list_id => $data) 1186 { 1187 $lists[$list_id] = $data['list_name']; 1188 if ($data['list_owner'] != $this->user) 1189 { 1190 $lists[$list_id] .= ' ('.Api\Accounts::username($data['list_owner']).')'; 1191 } 1192 } 1193 } 1194 1195 // add groups for all backends, if accounts addressbook is not hidden & 1196 // preference has not turned them off 1197 if ($GLOBALS['egw_info']['user']['preferences']['addressbook']['hide_accounts'] !== '1' && 1198 $GLOBALS['egw_info']['user']['preferences']['addressbook']['hide_groups_as_lists'] == '0') 1199 { 1200 foreach($GLOBALS['egw']->accounts->search(array( 1201 'type' => 'groups' 1202 )) as $account_id => $group) 1203 { 1204 $lists[(string)$account_id] = Api\Accounts::format_username($group['account_lid'], '', '', $account_id); 1205 } 1206 } 1207 1208 return $lists; 1209 } 1210 1211 /** 1212 * Get the availible distribution lists for givens users and groups 1213 * 1214 * @param array $keys column-name => value(s) pairs, eg. array('list_uid'=>$uid) 1215 * @param string $member_attr ='contact_uid' null: no members, 'contact_uid', 'contact_id', 'caldav_name' return members as that attribute 1216 * @param boolean $limit_in_ab =false if true only return members from the same owners addressbook 1217 * @return array with list_id => array(list_id,list_name,list_owner,...) pairs 1218 */ 1219 function read_lists($keys,$member_attr=null,$limit_in_ab=false) 1220 { 1221 $backend = (string)$limit_in_ab === '0' && $this->so_accounts ? $this->so_accounts : $this->somain; 1222 if (!method_exists($backend, 'get_lists')) return false; 1223 1224 return $backend->get_lists($keys,null,$member_attr,$limit_in_ab); 1225 } 1226 1227 /** 1228 * Adds / updates a distribution list 1229 * 1230 * @param string|array $keys list-name or array with column-name => value pairs to specify the list 1231 * @param int $owner user- or group-id 1232 * @param array $contacts =array() contacts to add (only for not yet existing lists!) 1233 * @param array &$data=array() values for keys 'list_uid', 'list_carddav_name', 'list_name' 1234 * @return int|boolean integer list_id or false on error 1235 */ 1236 function add_list($keys,$owner,$contacts=array(),array &$data=array()) 1237 { 1238 $backend = (string)$owner === '0' && $this->so_accounts ? $this->so_accounts : $this->somain; 1239 if (!method_exists($backend, 'add_list')) return false; 1240 1241 return $backend->add_list($keys,$owner,$contacts,$data); 1242 } 1243 1244 /** 1245 * Adds contact(s) to a distribution list 1246 * 1247 * @param int|array $contact contact_id(s) 1248 * @param int $list list-id 1249 * @param array $existing =null array of existing contact-id(s) of list, to not reread it, eg. array() 1250 * @return false on error 1251 */ 1252 function add2list($contact,$list,array $existing=null) 1253 { 1254 if (!method_exists($this->somain,'add2list')) return false; 1255 1256 return $this->somain->add2list($contact,$list,$existing); 1257 } 1258 1259 /** 1260 * Removes one contact from distribution list(s) 1261 * 1262 * @param int|array $contact contact_id(s) 1263 * @param int $list =null list-id or null to remove from all lists 1264 * @return false on error 1265 */ 1266 function remove_from_list($contact,$list=null) 1267 { 1268 if (!method_exists($this->somain,'remove_from_list')) return false; 1269 1270 return $this->somain->remove_from_list($contact,$list); 1271 } 1272 1273 /** 1274 * Deletes a distribution list (incl. it's members) 1275 * 1276 * @param int|array $list list_id(s) 1277 * @return number of members deleted or false if list does not exist 1278 */ 1279 function delete_list($list) 1280 { 1281 if (!method_exists($this->somain,'delete_list')) return false; 1282 1283 return $this->somain->delete_list($list); 1284 } 1285 1286 /** 1287 * Read data of a distribution list 1288 * 1289 * @param int $list list_id 1290 * @return array of data or false if list does not exist 1291 */ 1292 function read_list($list) 1293 { 1294 if (!method_exists($this->somain,'read_list')) return false; 1295 1296 return $this->somain->read_list($list); 1297 } 1298 1299 /** 1300 * Check if distribution lists are availible for a given addressbook 1301 * 1302 * @param int|string $owner ='' addressbook (eg. 0 = accounts), default '' = "all" addressbook (uses the main backend) 1303 * @return boolean 1304 */ 1305 function lists_available($owner='') 1306 { 1307 $backend =& $this->get_backend(null,$owner); 1308 1309 return method_exists($backend,'read_list'); 1310 } 1311 1312 /** 1313 * Get ctag (max list_modified as timestamp) for lists 1314 * 1315 * @param int|array $owner =null null for all lists user has access too 1316 * @return int 1317 */ 1318 function lists_ctag($owner=null) 1319 { 1320 if (!method_exists($this->somain,'lists_ctag')) return 0; 1321 1322 return $this->somain->lists_ctag($owner); 1323 } 1324} 1325