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
16 /* required includes */
17 // FIXME, NO display code in functions files
18 include_once(SM_PATH
. 'templates/util_global.php');
21 * Create and initialize an addressbook object.
22 * @param boolean $showerr display any address book init errors. html page header
23 * must be created before calling addressbook_init() with $showerr enabled.
24 * @param boolean $onlylocal enable only local address book backends
25 * @return object address book object.
27 function addressbook_init($showerr = true, $onlylocal = false) {
28 global $data_dir, $username, $color, $ldap_server, $address_book_global_filename;
29 global $addrbook_dsn, $addrbook_table;
30 global $abook_global_file, $abook_global_file_writeable, $abook_global_file_listing;
31 global $addrbook_global_dsn, $addrbook_global_table, $addrbook_global_writeable, $addrbook_global_listing;
33 /* Create a new addressbook object */
34 $abook = new AddressBook
;
36 /* Create empty error message */
40 Always add a local backend. We use *either* file-based *or* a
41 database addressbook. If $addrbook_dsn is set, the database
42 backend is used. If not, addressbooks are stores in files.
44 if (isset($addrbook_dsn) && !empty($addrbook_dsn)) {
46 if (!isset($addrbook_table) ||
empty($addrbook_table)) {
47 $addrbook_table = 'address';
49 $r = $abook->add_backend('database', Array('dsn' => $addrbook_dsn,
51 'table' => $addrbook_table));
52 if (!$r && $showerr) {
53 $abook_init_error.=_("Error initializing addressbook database.") . "<br />\n" . $abook->error
;
57 $filename = getHashedFile($username, $data_dir, "$username.abook");
58 $r = $abook->add_backend('local_file', Array('filename' => $filename,
61 // no need to use $abook->error, because message explains error.
62 $abook_init_error.=sprintf( _("Error opening file %s"), $filename );
66 /* Global file based addressbook */
67 if (isset($abook_global_file) &&
68 isset($abook_global_file_writeable) &&
69 isset($abook_global_file_listing) &&
70 trim($abook_global_file)!=''){
72 // Detect place of address book
73 if (! preg_match("/[\/\\\]/",$abook_global_file)) {
74 /* no path chars, address book stored in data directory
75 * make sure that there is a slash between data directory
76 * and address book file name
78 $abook_global_filename=$data_dir
79 . ((substr($data_dir, -1) != '/') ?
'/' : '')
81 } elseif (preg_match("/^\/|\w:/",$abook_global_file)) {
82 // full path is set in options (starts with slash or x:)
83 $abook_global_filename=$abook_global_file;
85 $abook_global_filename=SM_PATH
. $abook_global_file;
88 $r = $abook->add_backend('local_file',array('filename'=>$abook_global_filename,
89 'name' => _("Global address book"),
90 'detect_writeable' => false,
91 'writeable'=> $abook_global_file_writeable,
92 'listing' => $abook_global_file_listing));
94 /* global abook init error is not fatal. add error message and continue */
95 if (!$r && $showerr) {
96 if ($abook_init_error!='') $abook_init_error.="<br />\n";
97 $abook_init_error.=_("Error initializing global addressbook.") . "<br />\n" . $abook->error
;
101 /* Load global addressbook from SQL if configured */
102 if (isset($addrbook_global_dsn) && !empty($addrbook_global_dsn)) {
103 /* Database configured */
104 if (!isset($addrbook_global_table) ||
empty($addrbook_global_table)) {
105 $addrbook_global_table = 'global_abook';
107 $r = $abook->add_backend('database',
108 Array('dsn' => $addrbook_global_dsn,
110 'name' => _("Global address book"),
111 'writeable' => $addrbook_global_writeable,
112 'listing' => $addrbook_global_listing,
113 'table' => $addrbook_global_table));
114 /* global abook init error is not fatal. add error message and continue */
115 if (!$r && $showerr) {
116 if ($abook_init_error!='') $abook_init_error.="<br />\n";
117 $abook_init_error.=_("Error initializing global addressbook.") . "<br />\n" . $abook->error
;
122 * hook allows to include different address book backends.
123 * plugins should extract $abook and $r from arguments
124 * and use same add_backend commands as above functions.
125 * @since 1.5.1 and 1.4.5
127 $hookReturn = do_hook('abook_init', $abook, $r);
128 $abook = $hookReturn[1];
132 /* Load configured LDAP servers (if PHP has LDAP support) */
133 if (isset($ldap_server) && is_array($ldap_server)) {
135 while (list($undef,$param) = each($ldap_server)) {
136 if (is_array($param)) {
137 $r = $abook->add_backend('ldap_server', $param);
138 if (!$r && $showerr) {
139 if ($abook_init_error!='') $abook_init_error.="<br />\n";
140 $abook_init_error.=sprintf(_("Error initializing LDAP server %s:") .
141 "<br />\n", $param['host']);
142 $abook_init_error.= $abook->error
;
146 } // end of ldap server init
147 } // end of remote abook backend init
150 * display address book init errors.
152 if ($abook_init_error!='' && $showerr) {
153 error_box($abook_init_error,$color);
156 /* Return the initialized object */
161 * Display the "new address" form
163 * Form is not closed and you must add closing form tag.
165 * @param string $form_url form action url
166 * @param string $name form name
167 * @param string $title form title
168 * @param string $button form button name
169 * @param array $defdata values of form fields
171 function abook_create_form($form_url,$name,$title,$button,$defdata=array()) {
173 echo addForm($form_url, 'post', 'f_add').
176 html_tag( 'td', "\n". '<strong>' . $title . '</strong>' . "\n",
180 , 'center', '', 'width="90%"' ) ."\n";
181 address_form($name, $button, $defdata);
186 * Had to move this function outside of the Addressbook Class
187 * PHP 4.0.4 Seemed to be having problems with inline functions.
188 * Note: this can return now since we don't support 4.0.4 anymore.
190 function addressbook_cmp($a,$b) {
192 if($a['backend'] > $b['backend']) {
194 } else if($a['backend'] < $b['backend']) {
198 return (strtolower($a['name']) > strtolower($b['name'])) ?
1 : -1;
203 * Make an input field
204 * @param string $label
205 * @param string $field
206 * @param string $name
207 * @param string $size
208 * @param array $values
211 function addressbook_inp_field($label, $field, $name, $size, $values, $add='') {
213 $value = ( isset($values[$field]) ?
$values[$field] : '');
215 if (is_array($value)) {
216 $td_str = addSelect($name.'['.$field.']', $value);
218 $td_str = addInput($name.'['.$field.']', $value, $size);
222 return html_tag( 'tr' ,
223 html_tag( 'td', $label . ':', 'right', $color[4]) .
224 html_tag( 'td', $td_str, 'left', $color[4])
230 * Output form to add and modify address data
232 function address_form($name, $submittext, $values = array()) {
233 global $color, $squirrelmail_language;
235 if ($squirrelmail_language == 'ja_JP') {
236 echo html_tag( 'table',
237 addressbook_inp_field(_("Nickname"), 'nickname', $name, 15, $values,
238 ' <small>' . _("Must be unique") . '</small>') .
239 addressbook_inp_field(_("E-mail address"), 'email', $name, 45, $values, '') .
240 addressbook_inp_field(_("Last name"), 'lastname', $name, 45, $values, '') .
241 addressbook_inp_field(_("First name"), 'firstname', $name, 45, $values, '') .
242 addressbook_inp_field(_("Additional info"), 'label', $name, 45, $values, '') .
243 list_writable_backends($name) .
246 addSubmit($submittext, $name.'[SUBMIT]'),
247 'center', $color[4], 'colspan="2"')
249 , 'center', '', 'border="0" cellpadding="1" width="90%"') ."\n";
251 echo html_tag( 'table',
252 addressbook_inp_field(_("Nickname"), 'nickname', $name, 15, $values,
253 ' <small>' . _("Must be unique") . '</small>') .
254 addressbook_inp_field(_("E-mail address"), 'email', $name, 45, $values, '') .
255 addressbook_inp_field(_("First name"), 'firstname', $name, 45, $values, '') .
256 addressbook_inp_field(_("Last name"), 'lastname', $name, 45, $values, '') .
257 addressbook_inp_field(_("Additional info"), 'label', $name, 45, $values, '') .
258 list_writable_backends($name) .
261 addSubmit($submittext, $name.'[SUBMIT]') ,
262 'center', $color[4], 'colspan="2"')
264 , 'center', '', 'border="0" cellpadding="1" width="90%"') ."\n";
269 * Provides list of writeable backends.
270 * Works only when address is added ($name='addaddr')
271 * @param string $name name of form
272 * @return string html formated backend field (select or hidden)
274 function list_writable_backends($name) {
275 global $color, $abook;
276 if ( $name != 'addaddr' ) { return; }
277 $writeable_abook = 1;
278 if ( $abook->numbackends
> 1 ) {
279 $backends = $abook->get_backend_list();
280 $writeable_abooks=array();
281 while (list($undef,$v) = each($backends)) {
283 // add each backend to array
284 $writeable_abooks[$v->bnum
]=$v->sname
;
285 // save backend number
286 $writeable_abook=$v->bnum
;
289 if (count($writeable_abooks)>1) {
290 // we have more than one writeable backend
291 $ret=addSelect('backend',$writeable_abooks,null,true);
292 return html_tag( 'tr',
293 html_tag( 'td', _("Add to:"),'right', $color[4] ) .
294 html_tag( 'td', $ret, 'left', $color[4] )) . "\n";
297 // Only one backend exists or is writeable.
298 return html_tag( 'tr',
300 addHidden('backend', $writeable_abook),
301 'center', $color[4], 'colspan="2"')) . "\n";
305 * Sort array by the key "name"
307 function alistcmp($a,$b) {
308 $abook_sort_order=get_abook_sort();
310 switch ($abook_sort_order) {
313 $abook_sort='nickname';
330 if ($a['backend'] > $b['backend']) {
333 if ($a['backend'] < $b['backend']) {
338 if( (($abook_sort_order+
2) %
2) == 1) {
339 return (strtolower($a[$abook_sort]) < strtolower($b[$abook_sort])) ?
1 : -1;
341 return (strtolower($a[$abook_sort]) > strtolower($b[$abook_sort])) ?
1 : -1;
346 * Address book sorting options
348 * returns address book sorting order
349 * @return integer book sorting options order
351 function get_abook_sort() {
352 global $data_dir, $username;
354 /* get sorting order */
355 if(sqgetGlobalVar('abook_sort_order', $temp, SQ_GET
)) {
356 $abook_sort_order = (int) $temp;
358 if ($abook_sort_order < 0 or $abook_sort_order > 8)
361 setPref($data_dir, $username, 'abook_sort_order', $abook_sort_order);
363 /* get previous sorting options. default to unsorted */
364 $abook_sort_order = getPref($data_dir, $username, 'abook_sort_order', 8);
367 return $abook_sort_order;
371 * This function shows the address book sort button.
373 * @param integer $abook_sort_order current sort value
374 * @param string $alt_tag alt tag value (string visible to text only browsers)
375 * @param integer $Down sort value when list is sorted ascending
376 * @param integer $Up sort value when list is sorted descending
377 * @return string html code with sorting images and urls
379 function show_abook_sort_button($abook_sort_order, $alt_tag, $Down, $Up ) {
380 global $form_url, $icon_theme_path;
382 /* Figure out which image we want to use. */
383 if ($abook_sort_order != $Up && $abook_sort_order != $Down) {
384 $img = 'sort_none.png';
386 } elseif ($abook_sort_order == $Up) {
387 $img = 'up_pointer.png';
390 $img = 'down_pointer.png';
394 /* Now that we have everything figured out, show the actual button. */
395 return ' <a href="' . $form_url .'?abook_sort_order=' . $which .
397 getIcon($icon_theme_path, $img, $alt_tag, _("Click here to change the sorting of the address list")) .
403 * This is the main address book class that connect all the
404 * backends and provide services to the functions above.
405 * @package squirrelmail
406 * @subpackage addressbook
411 Cleaning errors from html with htmlspecialchars:
412 Errors from the backend are cleaned up in this class because we not always
413 have control over it when error output is generated in the backend.
414 If this appears to be wrong place then clean it up at the source (the backend)
418 * Enabled address book backends
421 var $backends = array();
423 * Number of enabled backends
426 var $numbackends = 0;
433 * id of backend with personal address book
436 var $localbackend = 0;
438 * Name of backend with personal address book
441 var $localbackendname = '';
443 * Controls use of 'extra' field
445 * Extra field can be used to add link to form, which allows
446 * to modify all fields supported by backend. This is the only field
447 * that is not sanitized with htmlspecialchars. Backends MUST make
448 * sure that field data is sanitized and displayed correctly inside
449 * table cell. Use of html formating in other address book fields is
450 * not allowed. Backends that don't return 'extra' row in address book
451 * data should not modify this object property.
455 var $add_extra_field = false;
458 * Constructor function.
460 function AddressBook() {
461 $this->localbackendname
= _("Personal address book");
465 * Return an array of backends of a given type,
466 * or all backends if no type is specified.
467 * @param string $type backend type
468 * @return array list of backends
470 function get_backend_list($type = '') {
472 for ($i = 1 ; $i <= $this->numbackends
; $i++
) {
473 if (empty($type) ||
$type == $this->backends
[$i]->btype
) {
474 $ret[] = &$this->backends
[$i];
481 /* ========================== Public ======================== */
486 * @param string $backend backend name (without the abook_ prefix)
487 * @param mixed optional variable that is passed to the backend constructor.
488 * See each of the backend classes for valid parameters
489 * @return integer number of backends
491 function add_backend($backend, $param = '') {
492 static $backend_classes;
493 if (!isset($backend_classes)) {
494 $backend_classes = array();
496 if (!isset($backend_classes[$backend])) {
498 * Support backend provided by plugins. Plugin function must
499 * return an associative array with as key the backend name ($backend)
500 * and as value the file including the path containing the backend class.
501 * i.e.: $aBackend = array('backend_template' => SM_PATH . 'plugins/abook_backend_template/functions.php')
503 * NB: Because the backend files are included from within this function they DO NOT have access to
504 * vars in the global scope. This function is the global scope for the included backend !!!
506 $aBackend = do_hook('abook_add_class');
507 if (isset($aBackend) && is_array($aBackend) && isset($aBackend[$backend])) {
508 require_once($aBackend[$backend]);
510 require_once(SM_PATH
. 'functions/abook_'.$backend.'.php');
512 $backend_classes[$backend] = true;
514 $backend_name = 'abook_' . $backend;
515 $newback = new $backend_name($param);
516 //eval('$newback = new ' . $backend_name . '($param);');
517 if(!empty($newback->error
)) {
518 $this->error
= $newback->error
;
522 $this->numbackends++
;
524 $newback->bnum
= $this->numbackends
;
525 $this->backends
[$this->numbackends
] = $newback;
527 /* Store ID of first local backend added */
528 if ($this->localbackend
== 0 && $newback->btype
== 'local') {
529 $this->localbackend
= $this->numbackends
;
530 $this->localbackendname
= $newback->sname
;
533 return $this->numbackends
;
538 * create string with name and email address
540 * This function takes a $row array as returned by the addressbook
541 * search and returns an e-mail address with the full name or
542 * nickname optionally prepended.
543 * @param array $row address book entry
544 * @return string email address with real name prepended
546 function full_address($row) {
547 global $addrsrch_fullname, $data_dir, $username;
548 $prefix = getPref($data_dir, $username, 'addrsrch_fullname');
549 if (($prefix != "" ||
(isset($addrsrch_fullname) &&
550 $prefix == $addrsrch_fullname)) && $prefix != 'noprefix') {
551 $name = ($prefix == 'nickname' ?
$row['nickname'] : $row['name']);
552 return $name . ' <' . trim($row['email']) . '>';
554 return trim($row['email']);
559 * Search for entries in address books
561 * Return a list of addresses matching expression in
562 * all backends of a given type.
563 * @param string $expression search expression
564 * @param integer $bnum backend number. default to search in all backends
565 * @return array search results
567 function search($expression, $bnum = -1) {
571 /* Search all backends */
573 $sel = $this->get_backend_list('');
575 for ($i = 0 ; $i < sizeof($sel) ; $i++
) {
576 $backend = &$sel[$i];
577 $backend->error
= '';
578 $res = $backend->search($expression);
579 if (is_array($res)) {
580 $ret = array_merge($ret, $res);
582 $this->error
.= "<br />\n" . htmlspecialchars($backend->error
);
587 /* Only fail if all backends failed */
588 if( $failed >= sizeof( $sel ) ) {
594 /* Search only one backend */
596 $ret = $this->backends
[$bnum]->search($expression);
597 if (!is_array($ret)) {
598 $this->error
.= "<br />\n" . htmlspecialchars($this->backends
[$bnum]->error
);
609 * @param string $expression search expression
610 * @param integer $bnum backend number. default to search in all backends
611 * @return array search results
613 function s_search($expression, $bnum = -1) {
615 $ret = $this->search($expression, $bnum);
616 if ( is_array( $ret ) ) {
617 usort($ret, 'addressbook_cmp');
624 * Lookup an address by alias.
625 * Only possible in local backends.
626 * @param string $alias
627 * @param integer backend number
628 * @return array lookup results. False, if not found.
630 function lookup($alias, $bnum = -1) {
635 $res = $this->backends
[$bnum]->lookup($alias);
636 if (is_array($res)) {
639 $this->error
= htmlspecialchars($this->backends
[$bnum]->error
);
644 $sel = $this->get_backend_list('local');
645 for ($i = 0 ; $i < sizeof($sel) ; $i++
) {
646 $backend = &$sel[$i];
647 $backend->error
= '';
648 $res = $backend->lookup($alias);
649 if (is_array($res)) {
653 $this->error
= htmlspecialchars($backend->error
);
663 * Return all addresses
664 * @param integer $bnum backend number
665 * @return array search results
667 function list_addr($bnum = -1) {
671 $sel = $this->get_backend_list('');
673 $sel = array(0 => &$this->backends
[$bnum]);
676 for ($i = 0 ; $i < sizeof($sel) ; $i++
) {
677 $backend = &$sel[$i];
678 $backend->error
= '';
679 $res = $backend->list_addr();
680 if (is_array($res)) {
681 $ret = array_merge($ret, $res);
683 $this->error
= htmlspecialchars($backend->error
);
692 * Create a new address
693 * @param array $userdata added address record
694 * @param integer $bnum backend number
695 * @return integer the backend number that the/ address was added
696 * to, or false if it failed.
698 function add($userdata, $bnum) {
701 if (!is_array($userdata)) {
702 $this->error
= _("Invalid input data");
705 if (empty($userdata['firstname']) && empty($userdata['lastname'])) {
706 $this->error
= _("Name is missing");
709 if (empty($userdata['email'])) {
710 $this->error
= _("E-mail address is missing");
713 if (empty($userdata['nickname'])) {
714 $userdata['nickname'] = $userdata['email'];
717 if (eregi('[ \\:\\|\\#\\"\\!]', $userdata['nickname'])) {
718 $this->error
= _("Nickname contains illegal characters");
722 /* Check that specified backend accept new entries */
723 if (!$this->backends
[$bnum]->writeable
) {
724 $this->error
= _("Addressbook is read-only");
728 /* Add address to backend */
729 $res = $this->backends
[$bnum]->add($userdata);
733 $this->error
= htmlspecialchars($this->backends
[$bnum]->error
);
737 return false; // Not reached
742 * Remove the entries from address book
743 * @param mixed $alias entries that have to be removed. Can be string with nickname or array with list of nicknames
744 * @param integer $bnum backend number
745 * @return bool true if removed successfully. false if there s an error. $this->error contains error message
747 function remove($alias, $bnum) {
754 /* Convert string to single element array */
755 if (!is_array($alias)) {
756 $alias = array(0 => $alias);
759 /* Check that specified backend is writable */
760 if (!$this->backends
[$bnum]->writeable
) {
761 $this->error
= _("Addressbook is read-only");
765 /* Remove user from backend */
766 $res = $this->backends
[$bnum]->remove($alias);
770 $this->error
= htmlspecialchars($this->backends
[$bnum]->error
);
774 return FALSE; /* Not reached */
775 } /* end of remove() */
779 * Modify entry in address book
780 * @param string $alias nickname
781 * @param array $userdata newdata
782 * @param integer $bnum backend number
784 function modify($alias, $userdata, $bnum) {
787 if (empty($alias) ||
!is_string($alias)) {
792 if(!is_array($userdata)) {
793 $this->error
= _("Invalid input data");
796 if (empty($userdata['firstname']) && empty($userdata['lastname'])) {
797 $this->error
= _("Name is missing");
800 if (empty($userdata['email'])) {
801 $this->error
= _("E-mail address is missing");
805 if (eregi('[\\: \\|\\#"\\!]', $userdata['nickname'])) {
806 $this->error
= _("Nickname contains illegal characters");
810 if (empty($userdata['nickname'])) {
811 $userdata['nickname'] = $userdata['email'];
814 /* Check that specified backend is writable */
815 if (!$this->backends
[$bnum]->writeable
) {
816 $this->error
= _("Addressbook is read-only");;
820 /* Modify user in backend */
821 $res = $this->backends
[$bnum]->modify($alias, $userdata);
825 $this->error
= htmlspecialchars($this->backends
[$bnum]->error
);
829 return FALSE; /* Not reached */
830 } /* end of modify() */
833 } /* End of class Addressbook */
836 * Generic backend that all other backends extend
837 * @package squirrelmail
838 * @subpackage addressbook
840 class addressbook_backend
{
842 /* Variables that all backends must provide. */
846 * Can be 'local' or 'remote'
847 * @var string backend type
849 var $btype = 'dummy';
851 * Internal backend name
854 var $bname = 'dummy';
856 * Displayed backend name
859 var $sname = 'Dummy backend';
862 * Variables common for all backends, but that
863 * should not be changed by the backends.
879 var $writeable = false;
883 * @param string $string error message
886 function set_error($string) {
887 $this->error
= '[' . $this->sname
. '] ' . $string;
892 /* ========================== Public ======================== */
895 * Search for entries in backend
897 * Working backend should support use of wildcards. * symbol
898 * should match one or more symbols. ? symbol should match any
900 * @param string $expression
903 function search($expression) {
904 $this->set_error('search not implemented');
909 * Find entry in backend by alias
910 * @param string $alias name used for id
913 function lookup($alias) {
914 $this->set_error('lookup not implemented');
919 * List all entries in backend
921 * Working backend should provide this function or at least
922 * dummy function that returns empty array.
925 function list_addr() {
926 $this->set_error('list_addr not implemented');
931 * Add entry to backend
932 * @param array userdata
935 function add($userdata) {
936 $this->set_error('add not implemented');
941 * Remove entry from backend
942 * @param string $alias name used for id
945 function remove($alias) {
946 $this->set_error('delete not implemented');
951 * Modify entry in backend
952 * @param string $alias name used for id
953 * @param array $newuserdata new data
956 function modify($alias, $newuserdata) {
957 $this->set_error('modify not implemented');