1<?php 2 3/** 4 * functions/addressbook.php - Functions and classes for the addressbook system 5 * 6 * Functions require SM_PATH and support of forms.php functions 7 * 8 * @copyright 1999-2021 The SquirrelMail Project Team 9 * @license http://opensource.org/licenses/gpl-license.php GNU Public License 10 * @version $Id: addressbook.php 14885 2021-02-05 19:19:32Z pdontthink $ 11 * @package squirrelmail 12 * @subpackage addressbook 13 */ 14 15/** 16 * If SM_PATH isn't defined, define it. Required to include files. 17 * @ignore 18 */ 19if (!defined('SM_PATH')) { 20 define('SM_PATH','../'); 21} 22 23/* make sure that display_messages.php is loaded */ 24include_once(SM_PATH . 'functions/display_messages.php'); 25 26global $addrbook_dsn, $addrbook_global_dsn; 27 28/** 29 Create and initialize an addressbook object. 30 Returns the created object 31*/ 32function addressbook_init($showerr = true, $onlylocal = false) { 33 global $data_dir, $username, $color, $ldap_server, $address_book_global_filename; 34 global $addrbook_dsn, $addrbook_table; 35 // Shared file based address book globals 36 global $abook_global_file, $abook_global_file_writeable, $abook_global_file_listing; 37 // Shared DB based address book globals 38 global $addrbook_global_dsn, $addrbook_global_table, $addrbook_global_writeable, $addrbook_global_listing; 39 // Record size restriction in file based address books 40 global $abook_file_line_length; 41 42 /* Create a new addressbook object */ 43 $abook = new AddressBook; 44 45 /* Create empty error message */ 46 $abook_init_error=''; 47 48 /* 49 Always add a local backend. We use *either* file-based *or* a 50 database addressbook. If $addrbook_dsn is set, the database 51 backend is used. If not, addressbooks are stores in files. 52 */ 53 if (isset($addrbook_dsn) && !empty($addrbook_dsn)) { 54 /* Database */ 55 if (!isset($addrbook_table) || empty($addrbook_table)) { 56 $addrbook_table = 'address'; 57 } 58 $r = $abook->add_backend('database', Array('dsn' => $addrbook_dsn, 59 'owner' => $username, 60 'table' => $addrbook_table)); 61 if (!$r && $showerr) { 62 $abook_init_error.=_("Error initializing address book database.") .' '. $abook->error; 63 } 64 } else { 65 /* File */ 66 $filename = getHashedFile($username, $data_dir, "$username.abook"); 67 $r = $abook->add_backend('local_file', Array('filename' => $filename, 68 'umask' => 0077, 69 'line_length' => $abook_file_line_length, 70 'create' => true)); 71 if(!$r && $showerr) { 72 $abook_init_error.=sprintf( _("Error opening file %s"), $filename ); 73 } 74 } 75 76 /* This would be for the global addressbook */ 77 if (isset($abook_global_file) && isset($abook_global_file_writeable) 78 && trim($abook_global_file)!=''){ 79 // Detect place of address book 80 if (! preg_match("/[\/\\\]/",$abook_global_file)) { 81 /* no path chars, address book stored in data directory 82 * make sure that there is a slash between data directory 83 * and address book file name 84 */ 85 $abook_global_filename=$data_dir 86 . ((substr($data_dir, -1) != '/') ? '/' : '') 87 . $abook_global_file; 88 } elseif (preg_match("/^\/|\w:/",$abook_global_file)) { 89 // full path is set in options (starts with slash or x:) 90 $abook_global_filename=$abook_global_file; 91 } else { 92 $abook_global_filename=SM_PATH . $abook_global_file; 93 } 94 $r = $abook->add_backend('local_file',array('filename'=>$abook_global_filename, 95 'name' => _("Global address book"), 96 'detect_writeable' => false, 97 'line_length' => $abook_file_line_length, 98 'writeable'=> $abook_global_file_writeable, 99 'listing' => $abook_global_file_listing)); 100 if (!$r && $showerr) { 101 if ($abook_init_error!='') $abook_init_error.="\n"; 102 $abook_init_error.=_("Error initializing global address book.") . "\n" . $abook->error; 103 } 104 } 105 106 /* Load global addressbook from SQL if configured */ 107 if (isset($addrbook_global_dsn) && !empty($addrbook_global_dsn)) { 108 /* Database configured */ 109 if (!isset($addrbook_global_table) || empty($addrbook_global_table)) { 110 $addrbook_global_table = 'global_abook'; 111 } 112 $r = $abook->add_backend('database', 113 Array('dsn' => $addrbook_global_dsn, 114 'owner' => 'global', 115 'name' => _("Global address book"), 116 'writeable' => $addrbook_global_writeable, 117 'listing' => $addrbook_global_listing, 118 'table' => $addrbook_global_table)); 119 if (!$r && $showerr) { 120 if ($abook_init_error!='') $abook_init_error.="\n"; 121 $abook_init_error.=_("Error initializing global address book.") . "\n" . $abook->error; 122 } 123 } 124 125 /* 126 * hook allows to include different address book backends. 127 * plugins should extract $abook and $r from arguments 128 * and use same add_backend commands as above functions. 129 * @since 1.5.1 and 1.4.5 130 */ 131 $hookReturn = do_hook('abook_init', $abook, $r); 132 $abook = $hookReturn[1]; 133 $r = $hookReturn[2]; 134 135 if (! $onlylocal) { 136 /* Load configured LDAP servers (if PHP has LDAP support) */ 137 if (isset($ldap_server) && is_array($ldap_server) && function_exists('ldap_connect')) { 138 reset($ldap_server); 139 foreach ($ldap_server as $param) { 140 if (is_array($param)) { 141 $r = $abook->add_backend('ldap_server', $param); 142 if (!$r && $showerr) { 143 if ($abook_init_error!='') $abook_init_error.="\n"; 144 $abook_init_error.=sprintf(_("Error initializing LDAP server %s:") . 145 "\n", $param['host']); 146 $abook_init_error.= $abook->error; 147 } 148 } 149 } 150 } 151 } // end of remote abook backends init 152 153 /** 154 * display address book init errors. 155 */ 156 if ($abook_init_error!='' && $showerr) { 157 $abook_init_error = sm_encode_html_special_chars($abook_init_error); 158 error_box($abook_init_error,$color); 159 } 160 161 /* Return the initialized object */ 162 return $abook; 163} 164 165 166/* 167 * Had to move this function outside of the Addressbook Class 168 * PHP 4.0.4 Seemed to be having problems with inline functions. 169 */ 170function addressbook_cmp($a,$b) { 171 172 if($a['backend'] > $b['backend']) { 173 return 1; 174 } else if($a['backend'] < $b['backend']) { 175 return -1; 176 } 177 178 return (strtolower($a['name']) > strtolower($b['name'])) ? 1 : -1; 179 180} 181 182/** 183 * Sort array by the key "name" 184 */ 185function alistcmp($a,$b) { 186 $abook_sort_order=get_abook_sort(); 187 188 switch ($abook_sort_order) { 189 case 0: 190 case 1: 191 $abook_sort='nickname'; 192 break; 193 case 4: 194 case 5: 195 $abook_sort='email'; 196 break; 197 case 6: 198 case 7: 199 $abook_sort='label'; 200 break; 201 case 2: 202 case 3: 203 case 8: 204 default: 205 $abook_sort='name'; 206 } 207 208 if ($a['backend'] > $b['backend']) { 209 return 1; 210 } else { 211 if ($a['backend'] < $b['backend']) { 212 return -1; 213 } 214 } 215 216 if( (($abook_sort_order+2) % 2) == 1) { 217 return (strtolower($a[$abook_sort]) < strtolower($b[$abook_sort])) ? 1 : -1; 218 } else { 219 return (strtolower($a[$abook_sort]) > strtolower($b[$abook_sort])) ? 1 : -1; 220 } 221} 222 223/** 224 * Address book sorting options 225 * 226 * returns address book sorting order 227 * @return integer book sorting options order 228 */ 229function get_abook_sort() { 230 global $data_dir, $username; 231 232 /* get sorting order */ 233 if(sqgetGlobalVar('abook_sort_order', $temp, SQ_GET)) { 234 $abook_sort_order = (int) $temp; 235 236 if ($abook_sort_order < 0 or $abook_sort_order > 8) 237 $abook_sort_order=8; 238 239 setPref($data_dir, $username, 'abook_sort_order', $abook_sort_order); 240 } else { 241 /* get previous sorting options. default to unsorted */ 242 $abook_sort_order = getPref($data_dir, $username, 'abook_sort_order', 8); 243 } 244 245 return $abook_sort_order; 246} 247 248/** 249 * This function shows the address book sort button. 250 * 251 * @param integer $abook_sort_order current sort value 252 * @param string $alt_tag alt tag value (string visible to text only browsers) 253 * @param integer $Down sort value when list is sorted ascending 254 * @param integer $Up sort value when list is sorted descending 255 * @return string html code with sorting images and urls 256 * @since 1.5.1 and 1.4.6 257 */ 258function show_abook_sort_button($abook_sort_order, $alt_tag, $Down, $Up ) { 259 global $form_url; 260 261 /* Figure out which image we want to use. */ 262 if ($abook_sort_order != $Up && $abook_sort_order != $Down) { 263 $img = 'sort_none.png'; 264 $which = $Up; 265 } elseif ($abook_sort_order == $Up) { 266 $img = 'up_pointer.png'; 267 $which = $Down; 268 } else { 269 $img = 'down_pointer.png'; 270 $which = 8; 271 } 272 273 /* Now that we have everything figured out, show the actual button. */ 274 return ' <a href="' . $form_url .'?abook_sort_order=' . $which 275 . '"><img src="../images/' . $img 276 . '" border="0" width="12" height="10" alt="' . $alt_tag . '" title="' 277 . _("Click here to change the sorting of the address list") .'" /></a>'; 278} 279 280 281/** 282 * This is the main address book class that connect all the 283 * backends and provide services to the functions above. 284 * @package squirrelmail 285 */ 286 287class AddressBook { 288 289 var $backends = array(); 290 var $numbackends = 0; 291 var $error = ''; 292 var $localbackend = 0; 293 var $localbackendname = ''; 294 var $add_extra_field = false; 295 296 /** 297 * Constructor (PHP5 style, required in some future version of PHP) 298 */ 299 function __construct() { 300 $this->localbackendname = _("Personal address book"); 301 } 302 303 /** 304 * Constructor (PHP4 style, kept for compatibility reasons) 305 */ 306 function AddressBook() { 307 self::__construct(); 308 } 309 310 /* 311 * Return an array of backends of a given type, 312 * or all backends if no type is specified. 313 */ 314 function get_backend_list($type = '') { 315 $ret = array(); 316 for ($i = 1 ; $i <= $this->numbackends ; $i++) { 317 if (empty($type) || $type == $this->backends[$i]->btype) { 318 $ret[] = &$this->backends[$i]; 319 } 320 } 321 return $ret; 322 } 323 324 325 /* 326 ========================== Public ======================== 327 328 Add a new backend. $backend is the name of a backend 329 (without the abook_ prefix), and $param is an optional 330 mixed variable that is passed to the backend constructor. 331 See each of the backend classes for valid parameters. 332 */ 333 function add_backend($backend, $param = '') { 334 $backend_name = 'abook_' . $backend; 335 eval('$newback = new ' . $backend_name . '($param);'); 336 if(!empty($newback->error)) { 337 $this->error = $newback->error; 338 return false; 339 } 340 341 $this->numbackends++; 342 343 $newback->bnum = $this->numbackends; 344 $this->backends[$this->numbackends] = $newback; 345 346 /* Store ID of first local backend added */ 347 if ($this->localbackend == 0 && $newback->btype == 'local') { 348 $this->localbackend = $this->numbackends; 349 $this->localbackendname = $newback->sname; 350 } 351 352 return $this->numbackends; 353 } 354 355 356 /* 357 * This function takes a $row array as returned by the addressbook 358 * search and returns an e-mail address with the full name or 359 * nickname optionally prepended. 360 */ 361 362 static function full_address($row) { 363 global $data_dir, $username; 364 $addrsrch_fullname = getPref($data_dir, $username, 'addrsrch_fullname', 'fullname'); 365 366 // allow multiple addresses in one row (poor person's grouping - bah) 367 // (separate with commas) 368 // 369 $return = ''; 370 $addresses = explode(',', $row['email']); 371 foreach ($addresses as $address) { 372 373 if (!empty($return)) $return .= ', '; 374 375 if ($addrsrch_fullname == 'fullname') 376 $return .= '"' . $row['name'] . '" <' . trim($address) . '>'; 377 else if ($addrsrch_fullname == 'nickname') 378 $return .= '"' . $row['nickname'] . '" <' . trim($address) . '>'; 379 else // "noprefix" 380 $return .= trim($address); 381 382 } 383 384 return $return; 385 } 386 387 /* 388 Return a list of addresses matching expression in 389 all backends of a given type. 390 */ 391 function search($expression, $bnum = -1) { 392 $ret = array(); 393 $this->error = ''; 394 395 /* Search all backends */ 396 if ($bnum == -1) { 397 $sel = $this->get_backend_list(''); 398 $failed = 0; 399 for ($i = 0 ; $i < sizeof($sel) ; $i++) { 400 $backend = &$sel[$i]; 401 $backend->error = ''; 402 $res = $backend->search($expression); 403 if (is_array($res)) { 404 $ret = array_merge($ret, $res); 405 } else { 406 $this->error .= "\n" . $backend->error; 407 $failed++; 408 } 409 } 410 411 /* Only fail if all backends failed */ 412 if( $failed >= sizeof( $sel ) ) { 413 $ret = FALSE; 414 } 415 416 } else { 417 418 /* Search only one backend */ 419 420 $ret = $this->backends[$bnum]->search($expression); 421 if (!is_array($ret)) { 422 $this->error .= "\n" . $this->backends[$bnum]->error; 423 $ret = FALSE; 424 } 425 } 426 427 return( $ret ); 428 } 429 430 431 /* Return a sorted search */ 432 function s_search($expression, $bnum = -1) { 433 434 $ret = $this->search($expression, $bnum); 435 if ( is_array( $ret ) ) { 436 usort($ret, 'addressbook_cmp'); 437 } 438 return $ret; 439 } 440 441 442 /* 443 * Lookup an address by the indicated field. Only 444 * possible in local backends. 445 */ 446 function lookup($value, $bnum = -1, $field = SM_ABOOK_FIELD_NICKNAME) { 447 448 $ret = array(); 449 450 if ($bnum > -1) { 451 $res = $this->backends[$bnum]->lookup($value, $field); 452 if (is_array($res)) { 453 return $res; 454 } else { 455 $this->error = $this->backends[$bnum]->error; 456 return false; 457 } 458 } 459 460 $sel = $this->get_backend_list('local'); 461 for ($i = 0 ; $i < sizeof($sel) ; $i++) { 462 $backend = &$sel[$i]; 463 $backend->error = ''; 464 $res = $backend->lookup($value, $field); 465 466 // return an address if one is found 467 // (empty array means lookup concluded 468 // but no result found - in this case, 469 // proceed to next backend) 470 // 471 if (is_array($res)) { 472 if (!empty($res)) return $res; 473 } else { 474 $this->error = $backend->error; 475 return false; 476 } 477 } 478 479 return $ret; 480 } 481 482 483 /* Return all addresses */ 484 function list_addr($bnum = -1) { 485 $ret = array(); 486 487 if ($bnum == -1) { 488 $sel = $this->get_backend_list(''); 489 } else { 490 $sel = array(0 => &$this->backends[$bnum]); 491 } 492 493 for ($i = 0 ; $i < sizeof($sel) ; $i++) { 494 $backend = &$sel[$i]; 495 $backend->error = ''; 496 $res = $backend->list_addr(); 497 if (is_array($res)) { 498 $ret = array_merge($ret, $res); 499 } else { 500 $this->error = $backend->error; 501 return false; 502 } 503 } 504 505 return $ret; 506 } 507 508 /* 509 * Create a new address from $userdata, in backend $bnum. 510 * Return the backend number that the/ address was added 511 * to, or false if it failed. 512 */ 513 function add($userdata, $bnum) { 514 515 /* Validate data */ 516 if (!is_array($userdata)) { 517 $this->error = _("Invalid input data"); 518 return false; 519 } 520 if (empty($userdata['firstname']) && empty($userdata['lastname'])) { 521 $this->error = _("Name is missing"); 522 return false; 523 } 524 if (empty($userdata['email'])) { 525 $this->error = _("E-mail address is missing"); 526 return false; 527 } 528 if (empty($userdata['nickname'])) { 529 $userdata['nickname'] = $userdata['email']; 530 } 531 532 /* Check that specified backend accept new entries */ 533 if (!$this->backends[$bnum]->writeable) { 534 $this->error = _("Address book is read-only"); 535 return false; 536 } 537 538 /* Add address to backend */ 539 $res = $this->backends[$bnum]->add($userdata); 540 if ($res) { 541 return $bnum; 542 } else { 543 $this->error = $this->backends[$bnum]->error; 544 return false; 545 } 546 547 return false; // Not reached 548 } /* end of add() */ 549 550 551 /* 552 * Remove the user identified by $alias from backend $bnum 553 * If $alias is an array, all users in the array are removed. 554 */ 555 function remove($alias, $bnum) { 556 557 /* Check input */ 558 if (empty($alias)) { 559 return true; 560 } 561 562 /* Convert string to single element array */ 563 if (!is_array($alias)) { 564 $alias = array(0 => $alias); 565 } 566 567 /* Check that specified backend is writable */ 568 if (!$this->backends[$bnum]->writeable) { 569 $this->error = _("Address book is read-only"); 570 return false; 571 } 572 573 /* Remove user from backend */ 574 $res = $this->backends[$bnum]->remove($alias); 575 if ($res) { 576 return $bnum; 577 } else { 578 $this->error = $this->backends[$bnum]->error; 579 return false; 580 } 581 582 return FALSE; /* Not reached */ 583 } /* end of remove() */ 584 585 586 /* 587 * Remove the user identified by $alias from backend $bnum 588 * If $alias is an array, all users in the array are removed. 589 */ 590 function modify($alias, $userdata, $bnum) { 591 592 /* Check input */ 593 if (empty($alias) || !is_string($alias)) { 594 return true; 595 } 596 597 /* Validate data */ 598 if(!is_array($userdata)) { 599 $this->error = _("Invalid input data"); 600 return false; 601 } 602 if (empty($userdata['firstname']) && empty($userdata['lastname'])) { 603 $this->error = _("Name is missing"); 604 return false; 605 } 606 if (empty($userdata['email'])) { 607 $this->error = _("E-mail address is missing"); 608 return false; 609 } 610 611 if (empty($userdata['nickname'])) { 612 $userdata['nickname'] = $userdata['email']; 613 } 614 615 /* Check that specified backend is writable */ 616 if (!$this->backends[$bnum]->writeable) { 617 $this->error = _("Address book is read-only");; 618 return false; 619 } 620 621 /* Modify user in backend */ 622 $res = $this->backends[$bnum]->modify($alias, $userdata); 623 if ($res) { 624 return $bnum; 625 } else { 626 $this->error = $this->backends[$bnum]->error; 627 return false; 628 } 629 630 return FALSE; /* Not reached */ 631 } /* end of modify() */ 632 633 634} /* End of class Addressbook */ 635 636/** 637 * Generic backend that all other backends extend 638 * @package squirrelmail 639 */ 640class addressbook_backend { 641 642 /* Variables that all backends must provide. */ 643 var $btype = 'dummy'; 644 var $bname = 'dummy'; 645 var $sname = 'Dummy backend'; 646 647 /* 648 * Variables common for all backends, but that 649 * should not be changed by the backends. 650 */ 651 var $bnum = -1; 652 var $error = ''; 653 var $writeable = false; 654 655 function set_error($string) { 656 $this->error = '[' . $this->sname . '] ' . $string; 657 return false; 658 } 659 660 661 /* ========================== Public ======================== */ 662 663 function search($expression) { 664 $this->set_error('search not implemented'); 665 return false; 666 } 667 668 function lookup($value, $field=SM_ABOOK_FIELD_NICKNAME) { 669 $this->set_error('lookup not implemented'); 670 return false; 671 } 672 673 function list_addr() { 674 $this->set_error('list_addr not implemented'); 675 return false; 676 } 677 678 function add($userdata) { 679 $this->set_error('add not implemented'); 680 return false; 681 } 682 683 function remove($alias) { 684 $this->set_error('delete not implemented'); 685 return false; 686 } 687 688 function modify($alias, $newuserdata) { 689 $this->set_error('modify not implemented'); 690 return false; 691 } 692 693} 694 695/* 696 PHP 5 requires that the class be made first, which seems rather 697 logical, and should have been the way it was generated the first time. 698*/ 699 700require_once(SM_PATH . 'functions/abook_local_file.php'); 701require_once(SM_PATH . 'functions/abook_ldap_server.php'); 702 703/* Only load database backend if database is configured */ 704if((isset($addrbook_dsn) && !empty($addrbook_dsn)) || 705 (isset($addrbook_global_dsn) && !empty($addrbook_global_dsn))) { 706 include_once(SM_PATH . 'functions/abook_database.php'); 707} 708 709/* 710 * hook allows adding different address book classes. 711 * class must follow address book class coding standards. 712 * 713 * see addressbook_backend class and functions/abook_*.php files. 714 * @since 1.5.1 and 1.4.5 715 */ 716do_hook('abook_add_class'); 717