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, $color, $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)) {
145 $r = $abook->add_backend('ldap_server', $param);
146 if (!$r && $showerr) {
147 if ($abook_init_error!='') $abook_init_error.="<br />\n";
148 $abook_init_error.=sprintf(_("Error initializing LDAP server %s:") .
149 "<br />\n", $param['host']);
150 $abook_init_error.= $abook->error
;
154 } // end of ldap server init
155 } // end of remote abook backend init
158 * display address book init errors.
160 if ($abook_init_error!='' && $showerr) {
161 error_box($abook_init_error,$color);
164 /* Return the initialized object */
169 * Display the "new address" form
171 * Form is not closed and you must add closing form tag.
173 * @param string $form_url form action url
174 * @param string $name form name
175 * @param string $title form title
176 * @param string $button form button name
177 * @param array $defdata values of form fields
179 function abook_create_form($form_url,$name,$title,$button,$defdata=array()) {
181 echo addForm($form_url, 'post', 'f_add').
184 html_tag( 'td', "\n". '<strong>' . $title . '</strong>' . "\n",
188 , 'center', '', 'width="90%"' ) ."\n";
189 address_form($name, $button, $defdata);
194 * Had to move this function outside of the Addressbook Class
195 * PHP 4.0.4 Seemed to be having problems with inline functions.
196 * Note: this can return now since we don't support 4.0.4 anymore.
198 function addressbook_cmp($a,$b) {
200 if($a['backend'] > $b['backend']) {
202 } else if($a['backend'] < $b['backend']) {
206 return (strtolower($a['name']) > strtolower($b['name'])) ?
1 : -1;
211 * Make an input field
212 * @param string $label
213 * @param string $field
214 * @param string $name
215 * @param string $size
216 * @param array $values
219 function addressbook_inp_field($label, $field, $name, $size, $values, $add='') {
221 $value = ( isset($values[$field]) ?
$values[$field] : '');
223 if (is_array($value)) {
224 $td_str = addSelect($name.'['.$field.']', $value);
226 $td_str = addInput($name.'['.$field.']', $value, $size);
230 return html_tag( 'tr' ,
231 html_tag( 'td', $label . ':', 'right', $color[4]) .
232 html_tag( 'td', $td_str, 'left', $color[4])
238 * Output form to add and modify address data
240 function address_form($name, $submittext, $values = array()) {
241 global $color, $squirrelmail_language;
243 if ($squirrelmail_language == 'ja_JP') {
244 echo html_tag( 'table',
245 addressbook_inp_field(_("Nickname"), 'nickname', $name, 15, $values,
246 ' <small>' . _("Must be unique") . '</small>') .
247 addressbook_inp_field(_("E-mail address"), 'email', $name, 45, $values, '') .
248 addressbook_inp_field(_("Last name"), 'lastname', $name, 45, $values, '') .
249 addressbook_inp_field(_("First name"), 'firstname', $name, 45, $values, '') .
250 addressbook_inp_field(_("Additional info"), 'label', $name, 45, $values, '') .
251 list_writable_backends($name) .
254 addSubmit($submittext, $name.'[SUBMIT]'),
255 'center', $color[4], 'colspan="2"')
257 , 'center', '', 'border="0" cellpadding="1" width="90%"') ."\n";
259 echo html_tag( 'table',
260 addressbook_inp_field(_("Nickname"), 'nickname', $name, 15, $values,
261 ' <small>' . _("Must be unique") . '</small>') .
262 addressbook_inp_field(_("E-mail address"), 'email', $name, 45, $values, '') .
263 addressbook_inp_field(_("First name"), 'firstname', $name, 45, $values, '') .
264 addressbook_inp_field(_("Last name"), 'lastname', $name, 45, $values, '') .
265 addressbook_inp_field(_("Additional info"), 'label', $name, 45, $values, '') .
266 list_writable_backends($name) .
269 addSubmit($submittext, $name.'[SUBMIT]') ,
270 'center', $color[4], 'colspan="2"')
272 , 'center', '', 'border="0" cellpadding="1" width="90%"') ."\n";
277 * Provides list of writeable backends.
278 * Works only when address is added ($name='addaddr')
279 * @param string $name name of form
280 * @return string html formated backend field (select or hidden)
282 function list_writable_backends($name) {
283 global $color, $abook;
284 if ( $name != 'addaddr' ) { return; }
285 $writeable_abook = 1;
286 if ( $abook->numbackends
> 1 ) {
287 $backends = $abook->get_backend_list();
288 $writeable_abooks=array();
289 while (list($undef,$v) = each($backends)) {
291 // add each backend to array
292 $writeable_abooks[$v->bnum
]=$v->sname
;
293 // save backend number
294 $writeable_abook=$v->bnum
;
297 if (count($writeable_abooks)>1) {
298 // we have more than one writeable backend
299 $ret=addSelect('backend',$writeable_abooks,null,true);
300 return html_tag( 'tr',
301 html_tag( 'td', _("Add to:"),'right', $color[4] ) .
302 html_tag( 'td', $ret, 'left', $color[4] )) . "\n";
305 // Only one backend exists or is writeable.
306 return html_tag( 'tr',
308 addHidden('backend', $writeable_abook),
309 'center', $color[4], 'colspan="2"')) . "\n";
313 * Sort array by the key "name"
315 function alistcmp($a,$b) {
316 $abook_sort_order=get_abook_sort();
318 switch ($abook_sort_order) {
321 $abook_sort='nickname';
338 if ($a['backend'] > $b['backend']) {
341 if ($a['backend'] < $b['backend']) {
346 if( (($abook_sort_order+
2) %
2) == 1) {
347 return (strtolower($a[$abook_sort]) < strtolower($b[$abook_sort])) ?
1 : -1;
349 return (strtolower($a[$abook_sort]) > strtolower($b[$abook_sort])) ?
1 : -1;
354 * Address book sorting options
356 * returns address book sorting order
357 * @return integer book sorting options order
359 function get_abook_sort() {
360 global $data_dir, $username;
362 /* get sorting order */
363 if(sqgetGlobalVar('abook_sort_order', $temp, SQ_GET
)) {
364 $abook_sort_order = (int) $temp;
366 if ($abook_sort_order < 0 or $abook_sort_order > 8)
369 setPref($data_dir, $username, 'abook_sort_order', $abook_sort_order);
371 /* get previous sorting options. default to unsorted */
372 $abook_sort_order = getPref($data_dir, $username, 'abook_sort_order', 8);
375 return $abook_sort_order;
379 * This function shows the address book sort button.
381 * @param integer $abook_sort_order current sort value
382 * @param string $alt_tag alt tag value (string visible to text only browsers)
383 * @param integer $Down sort value when list is sorted ascending
384 * @param integer $Up sort value when list is sorted descending
385 * @return string html code with sorting images and urls
387 function show_abook_sort_button($abook_sort_order, $alt_tag, $Down, $Up ) {
388 global $form_url, $icon_theme_path;
390 /* Figure out which image we want to use. */
391 if ($abook_sort_order != $Up && $abook_sort_order != $Down) {
392 $img = 'sort_none.png';
393 $text_icon = '◻'; // U+25FB WHITE MEDIUM SQUARE
395 } elseif ($abook_sort_order == $Up) {
396 $img = 'up_pointer.png';
397 $text_icon = '⇧'; // U+21E7 UPWARDS WHITE ARROW
400 $img = 'down_pointer.png';
401 $text_icon = '⇩'; // U+21E9 DOWNWARDS WHITE ARROW
405 /* Now that we have everything figured out, show the actual button. */
406 return ' <a href="' . $form_url .'?abook_sort_order=' . $which .
407 '" style="text-decoration:none" title="'.$alt_tag.'">' .
408 getIcon($icon_theme_path, $img, $text_icon, $alt_tag) .
414 * This is the main address book class that connect all the
415 * backends and provide services to the functions above.
416 * @package squirrelmail
417 * @subpackage addressbook
422 Cleaning errors from html with htmlspecialchars:
423 Errors from the backend are cleaned up in this class because we not always
424 have control over it when error output is generated in the backend.
425 If this appears to be wrong place then clean it up at the source (the backend)
429 * Enabled address book backends
432 var $backends = array();
434 * Number of enabled backends
437 var $numbackends = 0;
444 * id of backend with personal address book
447 var $localbackend = 0;
449 * Name of backend with personal address book
452 var $localbackendname = '';
454 * Controls use of 'extra' field
456 * Extra field can be used to add link to form, which allows
457 * to modify all fields supported by backend. This is the only field
458 * that is not sanitized with htmlspecialchars. Backends MUST make
459 * sure that field data is sanitized and displayed correctly inside
460 * table cell. Use of html formating in other address book fields is
461 * not allowed. Backends that don't return 'extra' row in address book
462 * data should not modify this object property.
466 var $add_extra_field = false;
469 * Constructor function.
471 function AddressBook() {
472 $this->localbackendname
= _("Personal address book");
476 * Return an array of backends of a given type,
477 * or all backends if no type is specified.
478 * @param string $type backend type
479 * @return array list of backends
481 function get_backend_list($type = '') {
483 for ($i = 1 ; $i <= $this->numbackends
; $i++
) {
484 if (empty($type) ||
$type == $this->backends
[$i]->btype
) {
485 $ret[] = &$this->backends
[$i];
492 /* ========================== Public ======================== */
497 * @param string $backend backend name (without the abook_ prefix)
498 * @param mixed optional variable that is passed to the backend constructor.
499 * See each of the backend classes for valid parameters
500 * @return integer number of backends
502 function add_backend($backend, $param = '') {
503 static $backend_classes;
504 if (!isset($backend_classes)) {
505 $backend_classes = array();
507 if (!isset($backend_classes[$backend])) {
509 * Support backend provided by plugins. Plugin function must
510 * return an associative array with as key the backend name ($backend)
511 * and as value the file including the path containing the backend class.
512 * i.e.: $aBackend = array('backend_template' => SM_PATH . 'plugins/abook_backend_template/functions.php')
514 * NB: Because the backend files are included from within this function they DO NOT have access to
515 * vars in the global scope. This function is the global scope for the included backend !!!
517 $aBackend = do_hook('abook_add_class');
518 if (isset($aBackend) && is_array($aBackend) && isset($aBackend[$backend])) {
519 require_once($aBackend[$backend]);
521 require_once(SM_PATH
. 'functions/abook_'.$backend.'.php');
523 $backend_classes[$backend] = true;
525 $backend_name = 'abook_' . $backend;
526 $newback = new $backend_name($param);
527 //eval('$newback = new ' . $backend_name . '($param);');
528 if(!empty($newback->error
)) {
529 $this->error
= $newback->error
;
533 $this->numbackends++
;
535 $newback->bnum
= $this->numbackends
;
536 $this->backends
[$this->numbackends
] = $newback;
538 /* Store ID of first local backend added */
539 if ($this->localbackend
== 0 && $newback->btype
== 'local') {
540 $this->localbackend
= $this->numbackends
;
541 $this->localbackendname
= $newback->sname
;
544 return $this->numbackends
;
549 * create string with name and email address
551 * This function takes a $row array as returned by the addressbook
552 * search and returns an e-mail address with the full name or
553 * nickname optionally prepended.
554 * @param array $row address book entry
555 * @return string email address with real name prepended
557 function full_address($row) {
558 global $addrsrch_fullname, $data_dir, $username;
559 $prefix = getPref($data_dir, $username, 'addrsrch_fullname');
560 if (($prefix != "" ||
(isset($addrsrch_fullname) &&
561 $prefix == $addrsrch_fullname)) && $prefix != 'noprefix') {
562 $name = ($prefix == 'nickname' ?
$row['nickname'] : $row['name']);
563 return $name . ' <' . trim($row['email']) . '>';
565 return trim($row['email']);
570 * Search for entries in address books
572 * Return a list of addresses matching expression in
573 * all backends of a given type.
574 * @param string $expression search expression
575 * @param integer $bnum backend number. default to search in all backends
576 * @return array search results
578 function search($expression, $bnum = -1) {
582 /* Search all backends */
584 $sel = $this->get_backend_list('');
586 for ($i = 0 ; $i < sizeof($sel) ; $i++
) {
587 $backend = &$sel[$i];
588 $backend->error
= '';
589 $res = $backend->search($expression);
590 if (is_array($res)) {
591 $ret = array_merge($ret, $res);
593 $this->error
.= "<br />\n" . htmlspecialchars($backend->error
);
598 /* Only fail if all backends failed */
599 if( $failed >= sizeof( $sel ) ) {
605 /* Search only one backend */
607 $ret = $this->backends
[$bnum]->search($expression);
608 if (!is_array($ret)) {
609 $this->error
.= "<br />\n" . htmlspecialchars($this->backends
[$bnum]->error
);
620 * @param string $expression search expression
621 * @param integer $bnum backend number. default to search in all backends
622 * @return array search results
624 function s_search($expression, $bnum = -1) {
626 $ret = $this->search($expression, $bnum);
627 if ( is_array( $ret ) ) {
628 usort($ret, 'addressbook_cmp');
635 * Lookup an address by alias.
636 * Only possible in local backends.
637 * @param string $alias
638 * @param integer backend number
639 * @return array lookup results. False, if not found.
641 function lookup($alias, $bnum = -1) {
646 $res = $this->backends
[$bnum]->lookup($alias);
647 if (is_array($res)) {
650 $this->error
= htmlspecialchars($this->backends
[$bnum]->error
);
655 $sel = $this->get_backend_list('local');
656 for ($i = 0 ; $i < sizeof($sel) ; $i++
) {
657 $backend = &$sel[$i];
658 $backend->error
= '';
659 $res = $backend->lookup($alias);
660 if (is_array($res)) {
664 $this->error
= htmlspecialchars($backend->error
);
674 * Return all addresses
675 * @param integer $bnum backend number
676 * @return array search results
678 function list_addr($bnum = -1) {
682 $sel = $this->get_backend_list('');
684 $sel = array(0 => &$this->backends
[$bnum]);
687 for ($i = 0 ; $i < sizeof($sel) ; $i++
) {
688 $backend = &$sel[$i];
689 $backend->error
= '';
690 $res = $backend->list_addr();
691 if (is_array($res)) {
692 $ret = array_merge($ret, $res);
694 $this->error
= htmlspecialchars($backend->error
);
703 * Create a new address
704 * @param array $userdata added address record
705 * @param integer $bnum backend number
706 * @return integer the backend number that the/ address was added
707 * to, or false if it failed.
709 function add($userdata, $bnum) {
712 if (!is_array($userdata)) {
713 $this->error
= _("Invalid input data");
716 if (empty($userdata['firstname']) && empty($userdata['lastname'])) {
717 $this->error
= _("Name is missing");
720 if (empty($userdata['email'])) {
721 $this->error
= _("E-mail address is missing");
724 if (empty($userdata['nickname'])) {
725 $userdata['nickname'] = $userdata['email'];
728 if (eregi('[ \\:\\|\\#\\"\\!]', $userdata['nickname'])) {
729 $this->error
= _("Nickname contains illegal characters");
733 /* Check that specified backend accept new entries */
734 if (!$this->backends
[$bnum]->writeable
) {
735 $this->error
= _("Addressbook is read-only");
739 /* Add address to backend */
740 $res = $this->backends
[$bnum]->add($userdata);
744 $this->error
= htmlspecialchars($this->backends
[$bnum]->error
);
748 return false; // Not reached
753 * Remove the entries from address book
754 * @param mixed $alias entries that have to be removed. Can be string with nickname or array with list of nicknames
755 * @param integer $bnum backend number
756 * @return bool true if removed successfully. false if there s an error. $this->error contains error message
758 function remove($alias, $bnum) {
765 /* Convert string to single element array */
766 if (!is_array($alias)) {
767 $alias = array(0 => $alias);
770 /* Check that specified backend is writable */
771 if (!$this->backends
[$bnum]->writeable
) {
772 $this->error
= _("Addressbook is read-only");
776 /* Remove user from backend */
777 $res = $this->backends
[$bnum]->remove($alias);
781 $this->error
= htmlspecialchars($this->backends
[$bnum]->error
);
785 return FALSE; /* Not reached */
786 } /* end of remove() */
790 * Modify entry in address book
791 * @param string $alias nickname
792 * @param array $userdata newdata
793 * @param integer $bnum backend number
795 function modify($alias, $userdata, $bnum) {
798 if (empty($alias) ||
!is_string($alias)) {
803 if(!is_array($userdata)) {
804 $this->error
= _("Invalid input data");
807 if (empty($userdata['firstname']) && empty($userdata['lastname'])) {
808 $this->error
= _("Name is missing");
811 if (empty($userdata['email'])) {
812 $this->error
= _("E-mail address is missing");
816 if (eregi('[\\: \\|\\#"\\!]', $userdata['nickname'])) {
817 $this->error
= _("Nickname contains illegal characters");
821 if (empty($userdata['nickname'])) {
822 $userdata['nickname'] = $userdata['email'];
825 /* Check that specified backend is writable */
826 if (!$this->backends
[$bnum]->writeable
) {
827 $this->error
= _("Addressbook is read-only");;
831 /* Modify user in backend */
832 $res = $this->backends
[$bnum]->modify($alias, $userdata);
836 $this->error
= htmlspecialchars($this->backends
[$bnum]->error
);
840 return FALSE; /* Not reached */
841 } /* end of modify() */
844 } /* End of class Addressbook */
847 * Generic backend that all other backends extend
848 * @package squirrelmail
849 * @subpackage addressbook
851 class addressbook_backend
{
853 /* Variables that all backends must provide. */
857 * Can be 'local' or 'remote'
858 * @var string backend type
860 var $btype = 'dummy';
862 * Internal backend name
865 var $bname = 'dummy';
867 * Displayed backend name
870 var $sname = 'Dummy backend';
873 * Variables common for all backends, but that
874 * should not be changed by the backends.
890 var $writeable = false;
894 * @param string $string error message
897 function set_error($string) {
898 $this->error
= '[' . $this->sname
. '] ' . $string;
903 /* ========================== Public ======================== */
906 * Search for entries in backend
908 * Working backend should support use of wildcards. * symbol
909 * should match one or more symbols. ? symbol should match any
911 * @param string $expression
914 function search($expression) {
915 $this->set_error('search not implemented');
920 * Find entry in backend by alias
921 * @param string $alias name used for id
924 function lookup($alias) {
925 $this->set_error('lookup not implemented');
930 * List all entries in backend
932 * Working backend should provide this function or at least
933 * dummy function that returns empty array.
936 function list_addr() {
937 $this->set_error('list_addr not implemented');
942 * Add entry to backend
943 * @param array userdata
946 function add($userdata) {
947 $this->set_error('add not implemented');
952 * Remove entry from backend
953 * @param string $alias name used for id
956 function remove($alias) {
957 $this->set_error('delete not implemented');
962 * Modify entry in backend
963 * @param string $alias name used for id
964 * @param array $newuserdata new data
967 function modify($alias, $newuserdata) {
968 $this->set_error('modify not implemented');