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 * If SM_PATH isn't defined, define it. Required to include files.
19 if (!defined('SM_PATH')) {
20 define('SM_PATH','../');
23 /* required includes */
24 include_once(SM_PATH
. 'functions/display_messages.php');
25 include_once(SM_PATH
. 'templates/util_global.php');
27 global $addrbook_dsn, $addrbook_global_dsn;
30 * Create and initialize an addressbook object.
31 * @param boolean $showerr display any address book init errors. html page header
32 * must be created before calling addressbook_init() with $showerr enabled.
33 * @param boolean $onlylocal enable only local address book backends
34 * @return object address book object.
36 function addressbook_init($showerr = true, $onlylocal = false) {
37 global $data_dir, $username, $color, $ldap_server, $address_book_global_filename;
38 global $addrbook_dsn, $addrbook_table;
39 global $abook_global_file, $abook_global_file_writeable, $abook_global_file_listing;
40 global $addrbook_global_dsn, $addrbook_global_table, $addrbook_global_writeable, $addrbook_global_listing;
42 /* Create a new addressbook object */
43 $abook = new AddressBook
;
45 /* Create empty error message */
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.
53 if (isset($addrbook_dsn) && !empty($addrbook_dsn)) {
55 if (!isset($addrbook_table) ||
empty($addrbook_table)) {
56 $addrbook_table = 'address';
58 $r = $abook->add_backend('database', Array('dsn' => $addrbook_dsn,
60 'table' => $addrbook_table));
61 if (!$r && $showerr) {
62 $abook_init_error.=_("Error initializing addressbook database.") . "<br />\n" . $abook->error
;
66 $filename = getHashedFile($username, $data_dir, "$username.abook");
67 $r = $abook->add_backend('local_file', Array('filename' => $filename,
70 // no need to use $abook->error, because message explains error.
71 $abook_init_error.=sprintf( _("Error opening file %s"), $filename );
75 /* Global file based addressbook */
76 if (isset($abook_global_file) &&
77 isset($abook_global_file_writeable) &&
78 isset($abook_global_file_listing) &&
79 trim($abook_global_file)!=''){
81 // Detect place of address book
82 if (! preg_match("/[\/\\\]/",$abook_global_file)) {
83 /* no path chars, address book stored in data directory
84 * make sure that there is a slash between data directory
85 * and address book file name
87 $abook_global_filename=$data_dir
88 . ((substr($data_dir, -1) != '/') ?
'/' : '')
90 } elseif (preg_match("/^\/|\w:/",$abook_global_file)) {
91 // full path is set in options (starts with slash or x:)
92 $abook_global_filename=$abook_global_file;
94 $abook_global_filename=SM_PATH
. $abook_global_file;
97 $r = $abook->add_backend('local_file',array('filename'=>$abook_global_filename,
98 'name' => _("Global address book"),
99 'detect_writeable' => false,
100 'writeable'=> $abook_global_file_writeable,
101 'listing' => $abook_global_file_listing));
103 /* global abook init error is not fatal. add error message and continue */
104 if (!$r && $showerr) {
105 if ($abook_init_error!='') $abook_init_error.="<br />\n";
106 $abook_init_error.=_("Error initializing global addressbook.") . "<br />\n" . $abook->error
;
110 /* Load global addressbook from SQL if configured */
111 if (isset($addrbook_global_dsn) && !empty($addrbook_global_dsn)) {
112 /* Database configured */
113 if (!isset($addrbook_global_table) ||
empty($addrbook_global_table)) {
114 $addrbook_global_table = 'global_abook';
116 $r = $abook->add_backend('database',
117 Array('dsn' => $addrbook_global_dsn,
119 'name' => _("Global address book"),
120 'writeable' => $addrbook_global_writeable,
121 'listing' => $addrbook_global_listing,
122 'table' => $addrbook_global_table));
123 /* global abook init error is not fatal. add error message and continue */
124 if (!$r && $showerr) {
125 if ($abook_init_error!='') $abook_init_error.="<br />\n";
126 $abook_init_error.=_("Error initializing global addressbook.") . "<br />\n" . $abook->error
;
131 * hook allows to include different address book backends.
132 * plugins should extract $abook and $r from arguments
133 * and use same add_backend commands as above functions.
134 * @since 1.5.1 and 1.4.5
136 $hookReturn = do_hook('abook_init', $abook, $r);
137 $abook = $hookReturn[1];
141 /* Load configured LDAP servers (if PHP has LDAP support) */
142 if (isset($ldap_server) && is_array($ldap_server)) {
144 while (list($undef,$param) = each($ldap_server)) {
145 if (is_array($param)) {
146 $r = $abook->add_backend('ldap_server', $param);
147 if (!$r && $showerr) {
148 if ($abook_init_error!='') $abook_init_error.="<br />\n";
149 $abook_init_error.=sprintf(_("Error initializing LDAP server %s:") .
150 "<br />\n", $param['host']);
151 $abook_init_error.= $abook->error
;
155 } // end of ldap server init
156 } // end of remote abook backend init
159 * display address book init errors.
161 if ($abook_init_error!='' && $showerr) {
162 error_box($abook_init_error,$color);
165 /* Return the initialized object */
170 * Display the "new address" form
172 * Form is not closed and you must add closing form tag.
174 * @param string $form_url form action url
175 * @param string $name form name
176 * @param string $title form title
177 * @param string $button form button name
178 * @param array $defdata values of form fields
180 function abook_create_form($form_url,$name,$title,$button,$defdata=array()) {
182 echo addForm($form_url, 'post', 'f_add').
185 html_tag( 'td', "\n". '<strong>' . $title . '</strong>' . "\n",
189 , 'center', '', 'width="90%"' ) ."\n";
190 address_form($name, $button, $defdata);
195 * Had to move this function outside of the Addressbook Class
196 * PHP 4.0.4 Seemed to be having problems with inline functions.
197 * Note: this can return now since we don't support 4.0.4 anymore.
199 function addressbook_cmp($a,$b) {
201 if($a['backend'] > $b['backend']) {
203 } else if($a['backend'] < $b['backend']) {
207 return (strtolower($a['name']) > strtolower($b['name'])) ?
1 : -1;
212 * Make an input field
213 * @param string $label
214 * @param string $field
215 * @param string $name
216 * @param string $size
217 * @param array $values
220 function addressbook_inp_field($label, $field, $name, $size, $values, $add='') {
222 $value = ( isset($values[$field]) ?
$values[$field] : '');
224 if (is_array($value)) {
225 $td_str = addSelect($name.'['.$field.']', $value);
227 $td_str = addInput($name.'['.$field.']', $value, $size);
231 return html_tag( 'tr' ,
232 html_tag( 'td', $label . ':', 'right', $color[4]) .
233 html_tag( 'td', $td_str, 'left', $color[4])
239 * Output form to add and modify address data
241 function address_form($name, $submittext, $values = array()) {
242 global $color, $squirrelmail_language;
244 if ($squirrelmail_language == 'ja_JP') {
245 echo html_tag( 'table',
246 addressbook_inp_field(_("Nickname"), 'nickname', $name, 15, $values,
247 ' <small>' . _("Must be unique") . '</small>') .
248 addressbook_inp_field(_("E-mail address"), 'email', $name, 45, $values, '') .
249 addressbook_inp_field(_("Last name"), 'lastname', $name, 45, $values, '') .
250 addressbook_inp_field(_("First name"), 'firstname', $name, 45, $values, '') .
251 addressbook_inp_field(_("Additional info"), 'label', $name, 45, $values, '') .
252 list_writable_backends($name) .
255 addSubmit($submittext, $name.'[SUBMIT]'),
256 'center', $color[4], 'colspan="2"')
258 , 'center', '', 'border="0" cellpadding="1" width="90%"') ."\n";
260 echo html_tag( 'table',
261 addressbook_inp_field(_("Nickname"), 'nickname', $name, 15, $values,
262 ' <small>' . _("Must be unique") . '</small>') .
263 addressbook_inp_field(_("E-mail address"), 'email', $name, 45, $values, '') .
264 addressbook_inp_field(_("First name"), 'firstname', $name, 45, $values, '') .
265 addressbook_inp_field(_("Last name"), 'lastname', $name, 45, $values, '') .
266 addressbook_inp_field(_("Additional info"), 'label', $name, 45, $values, '') .
267 list_writable_backends($name) .
270 addSubmit($submittext, $name.'[SUBMIT]') ,
271 'center', $color[4], 'colspan="2"')
273 , 'center', '', 'border="0" cellpadding="1" width="90%"') ."\n";
278 * Provides list of writeable backends.
279 * Works only when address is added ($name='addaddr')
280 * @param string $name name of form
281 * @return string html formated backend field (select or hidden)
283 function list_writable_backends($name) {
284 global $color, $abook;
285 if ( $name != 'addaddr' ) { return; }
286 $writeable_abook = 1;
287 if ( $abook->numbackends
> 1 ) {
288 $backends = $abook->get_backend_list();
289 $writeable_abooks=array();
290 while (list($undef,$v) = each($backends)) {
292 // add each backend to array
293 $writeable_abooks[$v->bnum
]=$v->sname
;
294 // save backend number
295 $writeable_abook=$v->bnum
;
298 if (count($writeable_abooks)>1) {
299 // we have more than one writeable backend
300 $ret=addSelect('backend',$writeable_abooks,null,true);
301 return html_tag( 'tr',
302 html_tag( 'td', _("Add to:"),'right', $color[4] ) .
303 html_tag( 'td', $ret, 'left', $color[4] )) . "\n";
306 // Only one backend exists or is writeable.
307 return html_tag( 'tr',
309 addHidden('backend', $writeable_abook),
310 'center', $color[4], 'colspan="2"')) . "\n";
314 * Sort array by the key "name"
316 function alistcmp($a,$b) {
317 $abook_sort_order=get_abook_sort();
319 switch ($abook_sort_order) {
322 $abook_sort='nickname';
339 if ($a['backend'] > $b['backend']) {
342 if ($a['backend'] < $b['backend']) {
347 if( (($abook_sort_order+
2) %
2) == 1) {
348 return (strtolower($a[$abook_sort]) < strtolower($b[$abook_sort])) ?
1 : -1;
350 return (strtolower($a[$abook_sort]) > strtolower($b[$abook_sort])) ?
1 : -1;
355 * Address book sorting options
357 * returns address book sorting order
358 * @return integer book sorting options order
360 function get_abook_sort() {
361 global $data_dir, $username;
363 /* get sorting order */
364 if(sqgetGlobalVar('abook_sort_order', $temp, SQ_GET
)) {
365 $abook_sort_order = (int) $temp;
367 if ($abook_sort_order < 0 or $abook_sort_order > 8)
370 setPref($data_dir, $username, 'abook_sort_order', $abook_sort_order);
372 /* get previous sorting options. default to unsorted */
373 $abook_sort_order = getPref($data_dir, $username, 'abook_sort_order', 8);
376 return $abook_sort_order;
380 * This function shows the address book sort button.
382 * @param integer $abook_sort_order current sort value
383 * @param string $alt_tag alt tag value (string visible to text only browsers)
384 * @param integer $Down sort value when list is sorted ascending
385 * @param integer $Up sort value when list is sorted descending
386 * @param string $icon_theme_path Path to user's current icon theme
387 * @return string html code with sorting images and urls
389 function show_abook_sort_button($abook_sort_order, $alt_tag, $Down, $Up ) {
390 global $form_url, $icon_theme_path;
392 /* Figure out which image we want to use. */
393 if ($abook_sort_order != $Up && $abook_sort_order != $Down) {
394 $img = 'sort_none.png';
396 } elseif ($abook_sort_order == $Up) {
397 $img = 'up_pointer.png';
400 $img = 'down_pointer.png';
404 /* Now that we have everything figured out, show the actual button. */
405 return ' <a href="' . $form_url .'?abook_sort_order=' . $which .
407 getIcon($icon_theme_path, $img, $alt_tag, _("Click here to change the sorting of the address list")) .
413 * This is the main address book class that connect all the
414 * backends and provide services to the functions above.
415 * @package squirrelmail
416 * @subpackage addressbook
421 Cleaning errors from html with htmlspecialchars:
422 Errors from the backend are cleaned up in this class because we not always
423 have control over it when error output is generated in the backend.
424 If this appears to be wrong place then clean it up at the source (the backend)
428 * Enabled address book backends
431 var $backends = array();
433 * Number of enabled backends
436 var $numbackends = 0;
443 * id of backend with personal address book
446 var $localbackend = 0;
448 * Name of backend with personal address book
451 var $localbackendname = '';
453 * Controls use of 'extra' field
455 * Extra field can be used to add link to form, which allows
456 * to modify all fields supported by backend. This is the only field
457 * that is not sanitized with htmlspecialchars. Backends MUST make
458 * sure that field data is sanitized and displayed correctly inside
459 * table cell. Use of html formating in other address book fields is
460 * not allowed. Backends that don't return 'extra' row in address book
461 * data should not modify this object property.
465 var $add_extra_field = false;
468 * Constructor function.
470 function AddressBook() {
471 $this->localbackendname
= _("Personal address book");
475 * Return an array of backends of a given type,
476 * or all backends if no type is specified.
477 * @param string $type backend type
478 * @return array list of backends
480 function get_backend_list($type = '') {
482 for ($i = 1 ; $i <= $this->numbackends
; $i++
) {
483 if (empty($type) ||
$type == $this->backends
[$i]->btype
) {
484 $ret[] = &$this->backends
[$i];
491 /* ========================== Public ======================== */
496 * @param string $backend backend name (without the abook_ prefix)
497 * @param mixed optional variable that is passed to the backend constructor.
498 * See each of the backend classes for valid parameters
499 * @return integer number of backends
501 function add_backend($backend, $param = '') {
502 $backend_name = 'abook_' . $backend;
503 eval('$newback = new ' . $backend_name . '($param);');
504 if(!empty($newback->error
)) {
505 $this->error
= $newback->error
;
509 $this->numbackends++
;
511 $newback->bnum
= $this->numbackends
;
512 $this->backends
[$this->numbackends
] = $newback;
514 /* Store ID of first local backend added */
515 if ($this->localbackend
== 0 && $newback->btype
== 'local') {
516 $this->localbackend
= $this->numbackends
;
517 $this->localbackendname
= $newback->sname
;
520 return $this->numbackends
;
525 * create string with name and email address
527 * This function takes a $row array as returned by the addressbook
528 * search and returns an e-mail address with the full name or
529 * nickname optionally prepended.
530 * @param array $row address book entry
531 * @return string email address with real name prepended
533 function full_address($row) {
534 global $addrsrch_fullname, $data_dir, $username;
535 $prefix = getPref($data_dir, $username, 'addrsrch_fullname');
536 if (($prefix != "" ||
(isset($addrsrch_fullname) &&
537 $prefix == $addrsrch_fullname)) && $prefix != 'noprefix') {
538 $name = ($prefix == 'nickname' ?
$row['nickname'] : $row['name']);
539 return $name . ' <' . trim($row['email']) . '>';
541 return trim($row['email']);
546 * Search for entries in address books
548 * Return a list of addresses matching expression in
549 * all backends of a given type.
550 * @param string $expression search expression
551 * @param integer $bnum backend number. default to search in all backends
552 * @return array search results
554 function search($expression, $bnum = -1) {
558 /* Search all backends */
560 $sel = $this->get_backend_list('');
562 for ($i = 0 ; $i < sizeof($sel) ; $i++
) {
563 $backend = &$sel[$i];
564 $backend->error
= '';
565 $res = $backend->search($expression);
566 if (is_array($res)) {
567 $ret = array_merge($ret, $res);
569 $this->error
.= "<br />\n" . htmlspecialchars($backend->error
);
574 /* Only fail if all backends failed */
575 if( $failed >= sizeof( $sel ) ) {
581 /* Search only one backend */
583 $ret = $this->backends
[$bnum]->search($expression);
584 if (!is_array($ret)) {
585 $this->error
.= "<br />\n" . htmlspecialchars($this->backends
[$bnum]->error
);
596 * @param string $expression search expression
597 * @param integer $bnum backend number. default to search in all backends
598 * @return array search results
600 function s_search($expression, $bnum = -1) {
602 $ret = $this->search($expression, $bnum);
603 if ( is_array( $ret ) ) {
604 usort($ret, 'addressbook_cmp');
611 * Lookup an address by alias.
612 * Only possible in local backends.
613 * @param string $alias
614 * @param integer backend number
615 * @return array lookup results. False, if not found.
617 function lookup($alias, $bnum = -1) {
622 $res = $this->backends
[$bnum]->lookup($alias);
623 if (is_array($res)) {
626 $this->error
= htmlspecialchars($this->backends
[$bnum]->error
);
631 $sel = $this->get_backend_list('local');
632 for ($i = 0 ; $i < sizeof($sel) ; $i++
) {
633 $backend = &$sel[$i];
634 $backend->error
= '';
635 $res = $backend->lookup($alias);
636 if (is_array($res)) {
640 $this->error
= htmlspecialchars($backend->error
);
650 * Return all addresses
651 * @param integer $bnum backend number
652 * @return array search results
654 function list_addr($bnum = -1) {
658 $sel = $this->get_backend_list('');
660 $sel = array(0 => &$this->backends
[$bnum]);
663 for ($i = 0 ; $i < sizeof($sel) ; $i++
) {
664 $backend = &$sel[$i];
665 $backend->error
= '';
666 $res = $backend->list_addr();
667 if (is_array($res)) {
668 $ret = array_merge($ret, $res);
670 $this->error
= htmlspecialchars($backend->error
);
679 * Create a new address
680 * @param array $userdata added address record
681 * @param integer $bnum backend number
682 * @return integer the backend number that the/ address was added
683 * to, or false if it failed.
685 function add($userdata, $bnum) {
688 if (!is_array($userdata)) {
689 $this->error
= _("Invalid input data");
692 if (empty($userdata['firstname']) && empty($userdata['lastname'])) {
693 $this->error
= _("Name is missing");
696 if (empty($userdata['email'])) {
697 $this->error
= _("E-mail address is missing");
700 if (empty($userdata['nickname'])) {
701 $userdata['nickname'] = $userdata['email'];
704 if (eregi('[ \\:\\|\\#\\"\\!]', $userdata['nickname'])) {
705 $this->error
= _("Nickname contains illegal characters");
709 /* Check that specified backend accept new entries */
710 if (!$this->backends
[$bnum]->writeable
) {
711 $this->error
= _("Addressbook is read-only");
715 /* Add address to backend */
716 $res = $this->backends
[$bnum]->add($userdata);
720 $this->error
= htmlspecialchars($this->backends
[$bnum]->error
);
724 return false; // Not reached
729 * Remove the entries from address book
730 * @param mixed $alias entries that have to be removed. Can be string with nickname or array with list of nicknames
731 * @param integer $bnum backend number
732 * @return bool true if removed successfully. false if there s an error. $this->error contains error message
734 function remove($alias, $bnum) {
741 /* Convert string to single element array */
742 if (!is_array($alias)) {
743 $alias = array(0 => $alias);
746 /* Check that specified backend is writable */
747 if (!$this->backends
[$bnum]->writeable
) {
748 $this->error
= _("Addressbook is read-only");
752 /* Remove user from backend */
753 $res = $this->backends
[$bnum]->remove($alias);
757 $this->error
= htmlspecialchars($this->backends
[$bnum]->error
);
761 return FALSE; /* Not reached */
762 } /* end of remove() */
766 * Modify entry in address book
767 * @param string $alias nickname
768 * @param array $userdata newdata
769 * @param integer $bnum backend number
771 function modify($alias, $userdata, $bnum) {
774 if (empty($alias) ||
!is_string($alias)) {
779 if(!is_array($userdata)) {
780 $this->error
= _("Invalid input data");
783 if (empty($userdata['firstname']) && empty($userdata['lastname'])) {
784 $this->error
= _("Name is missing");
787 if (empty($userdata['email'])) {
788 $this->error
= _("E-mail address is missing");
792 if (eregi('[\\: \\|\\#"\\!]', $userdata['nickname'])) {
793 $this->error
= _("Nickname contains illegal characters");
797 if (empty($userdata['nickname'])) {
798 $userdata['nickname'] = $userdata['email'];
801 /* Check that specified backend is writable */
802 if (!$this->backends
[$bnum]->writeable
) {
803 $this->error
= _("Addressbook is read-only");;
807 /* Modify user in backend */
808 $res = $this->backends
[$bnum]->modify($alias, $userdata);
812 $this->error
= htmlspecialchars($this->backends
[$bnum]->error
);
816 return FALSE; /* Not reached */
817 } /* end of modify() */
820 } /* End of class Addressbook */
823 * Generic backend that all other backends extend
824 * @package squirrelmail
825 * @subpackage addressbook
827 class addressbook_backend
{
829 /* Variables that all backends must provide. */
833 * Can be 'local' or 'remote'
834 * @var string backend type
836 var $btype = 'dummy';
838 * Internal backend name
841 var $bname = 'dummy';
843 * Displayed backend name
846 var $sname = 'Dummy backend';
849 * Variables common for all backends, but that
850 * should not be changed by the backends.
866 var $writeable = false;
870 * @param string $string error message
873 function set_error($string) {
874 $this->error
= '[' . $this->sname
. '] ' . $string;
879 /* ========================== Public ======================== */
882 * Search for entries in backend
884 * Working backend should support use of wildcards. * symbol
885 * should match one or more symbols. ? symbol should match any
887 * @param string $expression
890 function search($expression) {
891 $this->set_error('search not implemented');
896 * Find entry in backend by alias
897 * @param string $alias name used for id
900 function lookup($alias) {
901 $this->set_error('lookup not implemented');
906 * List all entries in backend
908 * Working backend should provide this function or at least
909 * dummy function that returns empty array.
912 function list_addr() {
913 $this->set_error('list_addr not implemented');
918 * Add entry to backend
919 * @param array userdata
922 function add($userdata) {
923 $this->set_error('add not implemented');
928 * Remove entry from backend
929 * @param string $alias name used for id
932 function remove($alias) {
933 $this->set_error('delete not implemented');
938 * Modify entry in backend
939 * @param string $alias name used for id
940 * @param array $newuserdata new data
943 function modify($alias, $newuserdata) {
944 $this->set_error('modify not implemented');
950 PHP 5 requires that the class be made first, which seems rather
951 logical, and should have been the way it was generated the first time.
954 require_once(SM_PATH
. 'functions/abook_local_file.php');
955 require_once(SM_PATH
. 'functions/abook_ldap_server.php');
957 /* Only load database backend if database is configured */
958 if((isset($addrbook_dsn) && !empty($addrbook_dsn)) ||
959 (isset($addrbook_global_dsn) && !empty($addrbook_global_dsn))) {
960 include_once(SM_PATH
. 'functions/abook_database.php');
964 * hook allows adding different address book classes.
965 * class must follow address book class coding standards.
967 * see addressbook_backend class and functions/abook_*.php files.
968 * @since 1.5.1 and 1.4.5
970 do_hook('abook_add_class');