4 * functions/addressbook.php - Functions and classes for the addressbook system
6 * Functions require SM_PATH and support of forms.php functions
8 * @copyright © 1999-2006 The SquirrelMail Project Team
9 * @license http://opensource.org/licenses/gpl-license.php GNU Public License
11 * @package squirrelmail
12 * @subpackage addressbook
15 /** required includes */
16 // FIXME, NO display code in functions files
17 include_once(SM_PATH
. 'templates/util_global.php');
20 * Create and initialize an addressbook object.
21 * @param boolean $showerr display any address book init errors. html page header
22 * must be created before calling addressbook_init() with $showerr enabled.
23 * @param boolean $onlylocal enable only local address book backends. Should
24 * be used when code does not need access to remote backends. Backends
25 * that provide read only address books with limited listing options can be
27 * @return object address book object.
29 function addressbook_init($showerr = true, $onlylocal = false) {
30 global $data_dir, $username, $ldap_server, $address_book_global_filename;
31 global $addrbook_dsn, $addrbook_table;
32 global $abook_global_file, $abook_global_file_writeable, $abook_global_file_listing;
33 global $addrbook_global_dsn, $addrbook_global_table, $addrbook_global_writeable, $addrbook_global_listing;
35 /* Create a new addressbook object */
36 $abook = new AddressBook
;
38 /* Create empty error message */
42 Always add a local backend. We use *either* file-based *or* a
43 database addressbook. If $addrbook_dsn is set, the database
44 backend is used. If not, addressbooks are stores in files.
46 if (isset($addrbook_dsn) && !empty($addrbook_dsn)) {
48 if (!isset($addrbook_table) ||
empty($addrbook_table)) {
49 $addrbook_table = 'address';
51 $r = $abook->add_backend('database', Array('dsn' => $addrbook_dsn,
53 'table' => $addrbook_table));
54 if (!$r && $showerr) {
55 $abook_init_error.=_("Error initializing addressbook database.") . "<br />\n" . $abook->error
;
59 $filename = getHashedFile($username, $data_dir, "$username.abook");
60 $r = $abook->add_backend('local_file', Array('filename' => $filename,
63 // no need to use $abook->error, because message explains error.
64 $abook_init_error.=sprintf( _("Error opening file %s"), $filename );
68 /* Global file based addressbook */
69 if (isset($abook_global_file) &&
70 isset($abook_global_file_writeable) &&
71 isset($abook_global_file_listing) &&
72 trim($abook_global_file)!=''){
74 // Detect place of address book
75 if (! preg_match("/[\/\\\]/",$abook_global_file)) {
76 /* no path chars, address book stored in data directory
77 * make sure that there is a slash between data directory
78 * and address book file name
80 $abook_global_filename=$data_dir
81 . ((substr($data_dir, -1) != '/') ?
'/' : '')
83 } elseif (preg_match("/^\/|\w:/",$abook_global_file)) {
84 // full path is set in options (starts with slash or x:)
85 $abook_global_filename=$abook_global_file;
87 $abook_global_filename=SM_PATH
. $abook_global_file;
90 $r = $abook->add_backend('local_file',array('filename'=>$abook_global_filename,
91 'name' => _("Global address book"),
92 'detect_writeable' => false,
93 'writeable'=> $abook_global_file_writeable,
94 'listing' => $abook_global_file_listing));
96 /* global abook init error is not fatal. add error message and continue */
97 if (!$r && $showerr) {
98 if ($abook_init_error!='') $abook_init_error.="<br />\n";
99 $abook_init_error.=_("Error initializing global addressbook.") . "<br />\n" . $abook->error
;
103 /* Load global addressbook from SQL if configured */
104 if (isset($addrbook_global_dsn) && !empty($addrbook_global_dsn)) {
105 /* Database configured */
106 if (!isset($addrbook_global_table) ||
empty($addrbook_global_table)) {
107 $addrbook_global_table = 'global_abook';
109 $r = $abook->add_backend('database',
110 Array('dsn' => $addrbook_global_dsn,
112 'name' => _("Global address book"),
113 'writeable' => $addrbook_global_writeable,
114 'listing' => $addrbook_global_listing,
115 'table' => $addrbook_global_table));
116 /* global abook init error is not fatal. add error message and continue */
117 if (!$r && $showerr) {
118 if ($abook_init_error!='') $abook_init_error.="<br />\n";
119 $abook_init_error.=_("Error initializing global addressbook.") . "<br />\n" . $abook->error
;
124 * hook allows to include different address book backends.
125 * plugins should extract $abook and $r from arguments
126 * and use same add_backend commands as above functions.
127 * Since 1.5.2 hook sends third ($onlylocal) argument to address book
128 * plugins in order to allow detection of local address book init.
129 * @since 1.5.1 and 1.4.5
131 $hookReturn = do_hook('abook_init', $abook, $r, $onlylocal);
132 $abook = $hookReturn[1];
134 if (!$r && $showerr) {
135 if ($abook_init_error!='') $abook_init_error.="<br />\n";
136 $abook_init_error.=_("Error initializing other address books.") . "<br />\n" . $abook->error
;
140 /* Load configured LDAP servers (if PHP has LDAP support) */
141 if (isset($ldap_server) && is_array($ldap_server)) {
143 while (list($undef,$param) = each($ldap_server)) {
144 if (!is_array($param))
147 /* if onlylocal is true, we only add writeable ldap servers */
148 if ($onlylocal && (!isset($param['writeable']) ||
$param['writeable'] != true))
151 $r = $abook->add_backend('ldap_server', $param);
152 if (!$r && $showerr) {
153 if ($abook_init_error!='') $abook_init_error.="<br />\n";
154 $abook_init_error.=sprintf(_("Error initializing LDAP server %s:") .
155 "<br />\n", $param['host']);
156 $abook_init_error.= $abook->error
;
159 } // end of ldap server init
162 * display address book init errors.
164 if ($abook_init_error!='' && $showerr) {
165 error_box($abook_init_error);
168 /* Return the initialized object */
173 * Display the "new address" form
175 * Form is not closed and you must add closing form tag.
177 * @param string $form_url form action url
178 * @param string $name form name
179 * @param string $title form title
180 * @param string $button form button name
181 * @param array $defdata values of form fields
183 function abook_create_form($form_url,$name,$title,$button,$defdata=array()) {
185 echo addForm($form_url, 'post', 'f_add').
188 html_tag( 'td', "\n". '<strong>' . $title . '</strong>' . "\n",
192 , 'center', '', 'width="90%"' ) ."\n";
193 address_form($name, $button, $defdata);
198 * Had to move this function outside of the Addressbook Class
199 * PHP 4.0.4 Seemed to be having problems with inline functions.
200 * Note: this can return now since we don't support 4.0.4 anymore.
202 function addressbook_cmp($a,$b) {
204 if($a['backend'] > $b['backend']) {
206 } else if($a['backend'] < $b['backend']) {
210 return (strtolower($a['name']) > strtolower($b['name'])) ?
1 : -1;
215 * Make an input field
216 * @param string $label
217 * @param string $field
218 * @param string $name
219 * @param string $size
220 * @param array $values
223 function addressbook_inp_field($label, $field, $name, $size, $values, $add='') {
225 $value = ( isset($values[$field]) ?
$values[$field] : '');
227 if (is_array($value)) {
228 $td_str = addSelect($name.'['.$field.']', $value);
230 $td_str = addInput($name.'['.$field.']', $value, $size);
234 return html_tag( 'tr' ,
235 html_tag( 'td', $label . ':', 'right', $color[4]) .
236 html_tag( 'td', $td_str, 'left', $color[4])
242 * Output form to add and modify address data
244 function address_form($name, $submittext, $values = array()) {
245 global $color, $squirrelmail_language;
247 if ($squirrelmail_language == 'ja_JP') {
248 echo html_tag( 'table',
249 addressbook_inp_field(_("Nickname"), 'nickname', $name, 15, $values,
250 ' <small>' . _("Must be unique") . '</small>') .
251 addressbook_inp_field(_("E-mail address"), 'email', $name, 45, $values, '') .
252 addressbook_inp_field(_("Last name"), 'lastname', $name, 45, $values, '') .
253 addressbook_inp_field(_("First name"), 'firstname', $name, 45, $values, '') .
254 addressbook_inp_field(_("Additional info"), 'label', $name, 45, $values, '') .
255 list_writable_backends($name) .
258 addSubmit($submittext, $name.'[SUBMIT]'),
259 'center', $color[4], 'colspan="2"')
261 , 'center', '', 'border="0" cellpadding="1" width="90%"') ."\n";
263 echo html_tag( 'table',
264 addressbook_inp_field(_("Nickname"), 'nickname', $name, 15, $values,
265 ' <small>' . _("Must be unique") . '</small>') .
266 addressbook_inp_field(_("E-mail address"), 'email', $name, 45, $values, '') .
267 addressbook_inp_field(_("First name"), 'firstname', $name, 45, $values, '') .
268 addressbook_inp_field(_("Last name"), 'lastname', $name, 45, $values, '') .
269 addressbook_inp_field(_("Additional info"), 'label', $name, 45, $values, '') .
270 list_writable_backends($name) .
273 addSubmit($submittext, $name.'[SUBMIT]') ,
274 'center', $color[4], 'colspan="2"')
276 , 'center', '', 'border="0" cellpadding="1" width="90%"') ."\n";
281 * Provides list of writeable backends.
282 * Works only when address is added ($name='addaddr')
283 * @param string $name name of form
284 * @return string html formated backend field (select or hidden)
286 function list_writable_backends($name) {
287 global $color, $abook;
288 if ( $name != 'addaddr' ) { return; }
289 $writeable_abook = 1;
290 if ( $abook->numbackends
> 1 ) {
291 $backends = $abook->get_backend_list();
292 $writeable_abooks=array();
293 while (list($undef,$v) = each($backends)) {
295 // add each backend to array
296 $writeable_abooks[$v->bnum
]=$v->sname
;
297 // save backend number
298 $writeable_abook=$v->bnum
;
301 if (count($writeable_abooks)>1) {
302 // we have more than one writeable backend
303 $ret=addSelect('backend',$writeable_abooks,null,true);
304 return html_tag( 'tr',
305 html_tag( 'td', _("Add to:"),'right', $color[4] ) .
306 html_tag( 'td', $ret, 'left', $color[4] )) . "\n";
309 // Only one backend exists or is writeable.
310 return html_tag( 'tr',
312 addHidden('backend', $writeable_abook),
313 'center', $color[4], 'colspan="2"')) . "\n";
317 * Sort array by the key "name"
319 function alistcmp($a,$b) {
320 $abook_sort_order=get_abook_sort();
322 switch ($abook_sort_order) {
325 $abook_sort='nickname';
342 if ($a['backend'] > $b['backend']) {
345 if ($a['backend'] < $b['backend']) {
350 if( (($abook_sort_order+
2) %
2) == 1) {
351 return (strtolower($a[$abook_sort]) < strtolower($b[$abook_sort])) ?
1 : -1;
353 return (strtolower($a[$abook_sort]) > strtolower($b[$abook_sort])) ?
1 : -1;
358 * Address book sorting options
360 * returns address book sorting order
361 * @return integer book sorting options order
363 function get_abook_sort() {
364 global $data_dir, $username;
366 /* get sorting order */
367 if(sqgetGlobalVar('abook_sort_order', $temp, SQ_GET
)) {
368 $abook_sort_order = (int) $temp;
370 if ($abook_sort_order < 0 or $abook_sort_order > 8)
373 setPref($data_dir, $username, 'abook_sort_order', $abook_sort_order);
375 /* get previous sorting options. default to unsorted */
376 $abook_sort_order = getPref($data_dir, $username, 'abook_sort_order', 8);
379 return $abook_sort_order;
383 * This function shows the address book sort button.
385 * @param integer $abook_sort_order current sort value
386 * @param string $alt_tag alt tag value (string visible to text only browsers)
387 * @param integer $Down sort value when list is sorted ascending
388 * @param integer $Up sort value when list is sorted descending
389 * @return string html code with sorting images and urls
391 function show_abook_sort_button($abook_sort_order, $alt_tag, $Down, $Up ) {
392 global $form_url, $icon_theme_path;
394 /* Figure out which image we want to use. */
395 if ($abook_sort_order != $Up && $abook_sort_order != $Down) {
396 $img = 'sort_none.png';
397 $text_icon = '◻'; // U+25FB WHITE MEDIUM SQUARE
399 } elseif ($abook_sort_order == $Up) {
400 $img = 'up_pointer.png';
401 $text_icon = '⇧'; // U+21E7 UPWARDS WHITE ARROW
404 $img = 'down_pointer.png';
405 $text_icon = '⇩'; // U+21E9 DOWNWARDS WHITE ARROW
409 /* Now that we have everything figured out, show the actual button. */
410 return ' <a href="' . $form_url .'?abook_sort_order=' . $which .
411 '" style="text-decoration:none" title="'.$alt_tag.'">' .
412 getIcon($icon_theme_path, $img, $text_icon, $alt_tag) .
418 * This is the main address book class that connect all the
419 * backends and provide services to the functions above.
420 * @package squirrelmail
421 * @subpackage addressbook
426 Cleaning errors from html with htmlspecialchars:
427 Errors from the backend are cleaned up in this class because we not always
428 have control over it when error output is generated in the backend.
429 If this appears to be wrong place then clean it up at the source (the backend)
433 * Enabled address book backends
436 var $backends = array();
438 * Number of enabled backends
441 var $numbackends = 0;
448 * id of backend with personal address book
451 var $localbackend = 0;
453 * Name of backend with personal address book
456 var $localbackendname = '';
458 * Controls use of 'extra' field
460 * Extra field can be used to add link to form, which allows
461 * to modify all fields supported by backend. This is the only field
462 * that is not sanitized with htmlspecialchars. Backends MUST make
463 * sure that field data is sanitized and displayed correctly inside
464 * table cell. Use of html formating in other address book fields is
465 * not allowed. Backends that don't return 'extra' row in address book
466 * data should not modify this object property.
470 var $add_extra_field = false;
473 * Constructor function.
475 function AddressBook() {
476 $this->localbackendname
= _("Personal address book");
480 * Return an array of backends of a given type,
481 * or all backends if no type is specified.
482 * @param string $type backend type
483 * @return array list of backends
485 function get_backend_list($type = '') {
487 for ($i = 1 ; $i <= $this->numbackends
; $i++
) {
488 if (empty($type) ||
$type == $this->backends
[$i]->btype
) {
489 $ret[] = &$this->backends
[$i];
496 /* ========================== Public ======================== */
501 * @param string $backend backend name (without the abook_ prefix)
502 * @param mixed optional variable that is passed to the backend constructor.
503 * See each of the backend classes for valid parameters
504 * @return integer number of backends
506 function add_backend($backend, $param = '') {
507 static $backend_classes;
508 if (!isset($backend_classes)) {
509 $backend_classes = array();
511 if (!isset($backend_classes[$backend])) {
513 * Support backend provided by plugins. Plugin function must
514 * return an associative array with as key the backend name ($backend)
515 * and as value the file including the path containing the backend class.
516 * i.e.: $aBackend = array('backend_template' => SM_PATH . 'plugins/abook_backend_template/functions.php')
518 * NB: Because the backend files are included from within this function they DO NOT have access to
519 * vars in the global scope. This function is the global scope for the included backend !!!
521 $aBackend = do_hook('abook_add_class');
522 if (isset($aBackend) && is_array($aBackend) && isset($aBackend[$backend])) {
523 require_once($aBackend[$backend]);
525 require_once(SM_PATH
. 'functions/abook_'.$backend.'.php');
527 $backend_classes[$backend] = true;
529 $backend_name = 'abook_' . $backend;
530 $newback = new $backend_name($param);
531 //eval('$newback = new ' . $backend_name . '($param);');
532 if(!empty($newback->error
)) {
533 $this->error
= $newback->error
;
537 $this->numbackends++
;
539 $newback->bnum
= $this->numbackends
;
540 $this->backends
[$this->numbackends
] = $newback;
542 /* Store ID of first local backend added */
543 if ($this->localbackend
== 0 && $newback->btype
== 'local') {
544 $this->localbackend
= $this->numbackends
;
545 $this->localbackendname
= $newback->sname
;
548 return $this->numbackends
;
553 * create string with name and email address
555 * This function takes a $row array as returned by the addressbook
556 * search and returns an e-mail address with the full name or
557 * nickname optionally prepended.
558 * @param array $row address book entry
559 * @return string email address with real name prepended
561 function full_address($row) {
562 global $addrsrch_fullname, $data_dir, $username;
563 $prefix = getPref($data_dir, $username, 'addrsrch_fullname');
564 if (($prefix != "" ||
(isset($addrsrch_fullname) &&
565 $prefix == $addrsrch_fullname)) && $prefix != 'noprefix') {
566 $name = ($prefix == 'nickname' ?
$row['nickname'] : $row['name']);
567 return $name . ' <' . trim($row['email']) . '>';
569 return trim($row['email']);
574 * Search for entries in address books
576 * Return a list of addresses matching expression in
577 * all backends of a given type.
578 * @param string $expression search expression
579 * @param integer $bnum backend number. default to search in all backends
580 * @return array search results
582 function search($expression, $bnum = -1) {
586 /* Search all backends */
588 $sel = $this->get_backend_list('');
590 for ($i = 0 ; $i < sizeof($sel) ; $i++
) {
591 $backend = &$sel[$i];
592 $backend->error
= '';
593 $res = $backend->search($expression);
594 if (is_array($res)) {
595 $ret = array_merge($ret, $res);
597 $this->error
.= "<br />\n" . htmlspecialchars($backend->error
);
602 /* Only fail if all backends failed */
603 if( $failed >= sizeof( $sel ) ) {
609 /* Search only one backend */
611 $ret = $this->backends
[$bnum]->search($expression);
612 if (!is_array($ret)) {
613 $this->error
.= "<br />\n" . htmlspecialchars($this->backends
[$bnum]->error
);
624 * @param string $expression search expression
625 * @param integer $bnum backend number. default to search in all backends
626 * @return array search results
628 function s_search($expression, $bnum = -1) {
630 $ret = $this->search($expression, $bnum);
631 if ( is_array( $ret ) ) {
632 usort($ret, 'addressbook_cmp');
639 * Lookup an address by alias.
640 * Only possible in local backends.
641 * @param string $alias
642 * @param integer backend number
643 * @return array lookup results. False, if not found.
645 function lookup($alias, $bnum = -1) {
650 $res = $this->backends
[$bnum]->lookup($alias);
651 if (is_array($res)) {
654 $this->error
= htmlspecialchars($this->backends
[$bnum]->error
);
659 $sel = $this->get_backend_list('local');
660 for ($i = 0 ; $i < sizeof($sel) ; $i++
) {
661 $backend = &$sel[$i];
662 $backend->error
= '';
663 $res = $backend->lookup($alias);
664 if (is_array($res)) {
668 $this->error
= htmlspecialchars($backend->error
);
678 * Return all addresses
679 * @param integer $bnum backend number
680 * @return array search results
682 function list_addr($bnum = -1) {
686 $sel = $this->get_backend_list('');
688 $sel = array(0 => &$this->backends
[$bnum]);
691 for ($i = 0 ; $i < sizeof($sel) ; $i++
) {
692 $backend = &$sel[$i];
693 $backend->error
= '';
694 $res = $backend->list_addr();
695 if (is_array($res)) {
696 $ret = array_merge($ret, $res);
698 $this->error
= htmlspecialchars($backend->error
);
707 * Create a new address
708 * @param array $userdata added address record
709 * @param integer $bnum backend number
710 * @return integer the backend number that the/ address was added
711 * to, or false if it failed.
713 function add($userdata, $bnum) {
716 if (!is_array($userdata)) {
717 $this->error
= _("Invalid input data");
720 if (empty($userdata['firstname']) && empty($userdata['lastname'])) {
721 $this->error
= _("Name is missing");
724 if (empty($userdata['email'])) {
725 $this->error
= _("E-mail address is missing");
728 if (empty($userdata['nickname'])) {
729 $userdata['nickname'] = $userdata['email'];
732 if (eregi('[ \\:\\|\\#\\"\\!]', $userdata['nickname'])) {
733 $this->error
= _("Nickname contains illegal characters");
737 /* Check that specified backend accept new entries */
738 if (!$this->backends
[$bnum]->writeable
) {
739 $this->error
= _("Addressbook is read-only");
743 /* Add address to backend */
744 $res = $this->backends
[$bnum]->add($userdata);
748 $this->error
= htmlspecialchars($this->backends
[$bnum]->error
);
752 return false; // Not reached
757 * Remove the entries from address book
758 * @param mixed $alias entries that have to be removed. Can be string with nickname or array with list of nicknames
759 * @param integer $bnum backend number
760 * @return bool true if removed successfully. false if there s an error. $this->error contains error message
762 function remove($alias, $bnum) {
769 /* Convert string to single element array */
770 if (!is_array($alias)) {
771 $alias = array(0 => $alias);
774 /* Check that specified backend is writable */
775 if (!$this->backends
[$bnum]->writeable
) {
776 $this->error
= _("Addressbook is read-only");
780 /* Remove user from backend */
781 $res = $this->backends
[$bnum]->remove($alias);
785 $this->error
= htmlspecialchars($this->backends
[$bnum]->error
);
789 return FALSE; /* Not reached */
790 } /* end of remove() */
794 * Modify entry in address book
795 * @param string $alias nickname
796 * @param array $userdata newdata
797 * @param integer $bnum backend number
799 function modify($alias, $userdata, $bnum) {
802 if (empty($alias) ||
!is_string($alias)) {
807 if(!is_array($userdata)) {
808 $this->error
= _("Invalid input data");
811 if (empty($userdata['firstname']) && empty($userdata['lastname'])) {
812 $this->error
= _("Name is missing");
815 if (empty($userdata['email'])) {
816 $this->error
= _("E-mail address is missing");
820 if (eregi('[\\: \\|\\#"\\!]', $userdata['nickname'])) {
821 $this->error
= _("Nickname contains illegal characters");
825 if (empty($userdata['nickname'])) {
826 $userdata['nickname'] = $userdata['email'];
829 /* Check that specified backend is writable */
830 if (!$this->backends
[$bnum]->writeable
) {
831 $this->error
= _("Addressbook is read-only");;
835 /* Modify user in backend */
836 $res = $this->backends
[$bnum]->modify($alias, $userdata);
840 $this->error
= htmlspecialchars($this->backends
[$bnum]->error
);
844 return FALSE; /* Not reached */
845 } /* end of modify() */
848 } /* End of class Addressbook */
851 * Generic backend that all other backends extend
852 * @package squirrelmail
853 * @subpackage addressbook
855 class addressbook_backend
{
857 /* Variables that all backends must provide. */
861 * Can be 'local' or 'remote'
862 * @var string backend type
864 var $btype = 'dummy';
866 * Internal backend name
869 var $bname = 'dummy';
871 * Displayed backend name
874 var $sname = 'Dummy backend';
877 * Variables common for all backends, but that
878 * should not be changed by the backends.
894 var $writeable = false;
898 * @param string $string error message
901 function set_error($string) {
902 $this->error
= '[' . $this->sname
. '] ' . $string;
907 /* ========================== Public ======================== */
910 * Search for entries in backend
912 * Working backend should support use of wildcards. * symbol
913 * should match one or more symbols. ? symbol should match any
915 * @param string $expression
918 function search($expression) {
919 $this->set_error('search not implemented');
924 * Find entry in backend by alias
925 * @param string $alias name used for id
928 function lookup($alias) {
929 $this->set_error('lookup not implemented');
934 * List all entries in backend
936 * Working backend should provide this function or at least
937 * dummy function that returns empty array.
940 function list_addr() {
941 $this->set_error('list_addr not implemented');
946 * Add entry to backend
947 * @param array userdata
950 function add($userdata) {
951 $this->set_error('add not implemented');
956 * Remove entry from backend
957 * @param string $alias name used for id
960 function remove($alias) {
961 $this->set_error('delete not implemented');
966 * Modify entry in backend
967 * @param string $alias name used for id
968 * @param array $newuserdata new data
971 function modify($alias, $newuserdata) {
972 $this->set_error('modify not implemented');