From 4f9c8337420327802baf73cde7d96b991b1fd1a9 Mon Sep 17 00:00:00 2001 From: svncommit Date: Wed, 7 May 2008 22:16:00 +0000 Subject: /tmp/out --- CHANGELOG | 8 ++ config/main.inc.php.dist | 37 ++++++- program/include/main.inc | 2 +- program/include/rcube_ldap.php | 187 ++++++++++++++++++++++++++++++++--- program/js/app.js | 2 +- program/steps/addressbook/delete.inc | 5 +- program/steps/addressbook/edit.inc | 3 +- program/steps/addressbook/func.inc | 56 +++++++---- program/steps/mail/addcontact.inc | 16 ++- 9 files changed, 277 insertions(+), 39 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index a7c199065..1114150c6 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,14 @@ CHANGELOG RoundCube Webmail --------------------------- +2008/05/07 (davidke/richs) +---------- +- Completed LDAP address book support so it can now write to an LDAP server. +- Expanded LDAP configuration options to support LDAP server writes. +- Modified config/main.inc.php.dist: + New Option: $rcmail_config['use_SQL_address_book'] + Changed Option: $rcmail_config['ldap_public']['Verisign'] + 2008/05/05 (alec) ---------- - Installer: encode special characters in DB username/password (#1485042) diff --git a/config/main.inc.php.dist b/config/main.inc.php.dist index 25cf6dafb..d7ac4d966 100644 --- a/config/main.inc.php.dist +++ b/config/main.inc.php.dist @@ -213,9 +213,28 @@ $rcmail_config['mail_header_delimiter'] = NULL; // session domain: .example.org $rcmail_config['session_domain'] = ''; -// in order to enable public ldap search, create a config array -// like the Verisign example below. if you would like to test, -// simply uncomment the Verisign example. +// This indicates whether or not to use the SQL address book. +// If set to false then it will look at using the first writable LDAP +// address book as the primary address book and it will not display the +// SQL address book in the 'Address Book' view. +$rcmail_config['use_SQL_address_book'] = true; + +// In order to enable public ldap search, configure an array like the Verisign +// example further below. if you would like to test, simply uncomment the example. +// +// If you are going to use LDAP for individual address books, you will need to +// set 'user_specific' to true and use the variables to generate the appropriate DNs to access it. +// +// The recommended directory structure for LDAP is to store all the address book entries +// under the users main entry, e.g.: +// +// o=root +// ou=people +// uid=user@domain +// mail=contact@contactdomain +// +// So the base_dn would be uid=%fu,ou=people,o=root +// The bind_dn would be the same as based_dn or some super user login. /** * example config for Verisign directory * @@ -223,15 +242,27 @@ $rcmail_config['session_domain'] = ''; * 'name' => 'Verisign.com', * 'hosts' => array('directory.verisign.com'), * 'port' => 389, + * 'user_specific' => false, // If true the base_dn, bind_dn and bind_pass default to the user's IMAP login. + * // %fu - The full username provided, assumes the username is an email + * // address, uses the username_domain value if not an email address. + * // %u - The username prior to the '@'. + * // %d - The domain name after the '@'. * 'base_dn' => '', * 'bind_dn' => '', * 'bind_pass' => '', + * 'writable' => false, // Indicates if we can write to the LDAP directory or not. + * // If writable is true then these fields need to be populated: + * // LDAP_Object_Classes, required_fields, LDAP_rdn + * 'LDAP_Object_Classes' => array("top", "inetOrgPerson"), // To create a new contact these are the object classes to specify (or any other classes you wish to use). + * 'required_fields' => array("cn", "sn", "mail"), // The required fields needed to build a new contact as required by the object classes (can include additional fields not required by the object classes). + * 'LDAP_rdn' => 'mail', // The RDN field that is used for new entries, this field needs to be one of the search_fields, the base of base_dn is appended to the RDN to insert into the LDAP directory. * 'ldap_version' => 3, // using LDAPv3 * 'search_fields' => array('mail', 'cn'), // fields to search in * 'name_field' => 'cn', // this field represents the contact's name * 'email_field' => 'mail', // this field represents the contact's e-mail * 'surname_field' => 'sn', // this field represents the contact's last name * 'firstname_field' => 'gn', // this field represents the contact's first name + * 'sort' => 'cn', // The field to sort the listing by. * 'scope' => 'sub', // search mode: sub|base|list * 'filter' => '', // used for basic listing (if not empty) and will be &'d with search queries. example: status=act * 'fuzzy_search' => true); // server allows wildcard search diff --git a/program/include/main.inc b/program/include/main.inc index 974abaf64..b436f8295 100644 --- a/program/include/main.inc +++ b/program/include/main.inc @@ -602,7 +602,7 @@ function rcube_table_output($attrib, $table_data, $a_show_cols, $id_col) { $zebra_class = $c%2 ? 'even' : 'odd'; - $table .= sprintf(''."\n", $row_data[$id_col]); + $table .= sprintf(''."\n", $row_data[$id_col]); // format each col foreach ($a_show_cols as $col) diff --git a/program/include/rcube_ldap.php b/program/include/rcube_ldap.php index 4d0574e67..c5962030f 100644 --- a/program/include/rcube_ldap.php +++ b/program/include/rcube_ldap.php @@ -56,7 +56,9 @@ class rcube_ldap foreach ($p as $prop => $value) if (preg_match('/^(.+)_field$/', $prop, $matches)) $this->fieldmap[$matches[1]] = $value; - + + $this->sort_col = $p["sort"]; + $this->connect(); } @@ -102,11 +104,54 @@ class rcube_ldap if (is_resource($this->conn)) { $this->ready = true; - if (!empty($this->prop['bind_dn']) && !empty($this->prop['bind_pass'])) + + if ($this->prop["user_specific"]) { + // User specific access, generate the proper values to use. + global $CONFIG, $RCMAIL; + if (empty($this->prop['bind_pass'])) { + // No password set, use the users. + $this->prop['bind_pass'] = $RCMAIL->decrypt_passwd($_SESSION["password"]); + } // end if + + // Get the pieces needed for variable replacement. + // See if the logged in username has an "@" in it. + if (is_bool(strstr($_SESSION["username"], "@"))) { + // It does not, use the global default. + $fu = $_SESSION["username"]."@".$CONFIG["username_domain"]; + $u = $_SESSION["username"]; + $d = $CONFIG["username_domain"]; + } // end if + else { + // It does. + $fu = $_SESSION["username"]; + // Get the pieces needed for username and domain. + list($u, $d) = explode("@", $_SESSION["username"]); + } # end else + + // Replace the bind_dn variables. + $bind_dn = str_replace(array("%fu", "%u", "%d"), + array($fu, $u, $d), + $this->prop['bind_dn']); + $this->prop['bind_dn'] = $bind_dn; + // Replace the base_dn variables. + $base_dn = str_replace(array("%fu", "%u", "%d"), + array($fu, $u, $d), + $this->prop['base_dn']); + $this->prop['base_dn'] = $base_dn; + + $this->ready = $this->bind($this->prop['bind_dn'], $this->prop['bind_pass']); + } // end if + elseif (!empty($this->prop['bind_dn']) && !empty($this->prop['bind_pass'])) $this->ready = $this->bind($this->prop['bind_dn'], $this->prop['bind_pass']); } else raise_error(array('type' => 'ldap', 'message' => "Could not connect to any LDAP server, tried $host:{$this->prop[port]} last"), true); + + // See if the directory is writeable. + if ($this->prop['writable']) { + $this->readonly = false; + } // end if + } @@ -211,7 +256,7 @@ class rcube_ldap * List the current set of contact records * * @param array List of cols to show - * @param int Only return this number of records (not implemented) + * @param int Only return this number of records * @return array Indexed list of contact records, each a hash array */ function list_records($cols=null, $subset=0) @@ -235,9 +280,13 @@ class rcube_ldap { if ($this->sort_col && $this->prop['scope'] !== "base") @ldap_sort($this->conn, $this->ldap_result, $this->sort_col); - + + $start_row = $subset < 0 ? $this->result->first + $this->page_size + $subset : $this->result->first; + $last_row = $this->result->first + $this->page_size; + $last_row = $subset != 0 ? $start_row + abs($subset) : $last_row; + $entries = ldap_get_entries($this->conn, $this->ldap_result); - for ($i = $this->result->first; $i < min($entries['count'], $this->result->first + $this->page_size); $i++) + for ($i = $start_row; $i < min($entries['count'], $last_row); $i++) $this->result->add($this->_ldap2result($entries[$i])); } @@ -313,8 +362,20 @@ class rcube_ldap function count() { $count = 0; - if ($this->conn && $this->ldap_result) + if ($this->conn && $this->ldap_result) { $count = ldap_count_entries($this->conn, $this->ldap_result); + } // end if + elseif ($this->conn) { + // We have a connection but no result set, attempt to get one. + if (empty($this->filter)) { + // The filter is not set, set it. + $this->filter = $this->prop['filter']; + } // end if + $this->_exec_search(); + if ($this->ldap_result) { + $count = ldap_count_entries($this->conn, $this->ldap_result); + } // end if + } // end else return new rcube_result_set($count, ($this->list_page-1) * $this->page_size); } @@ -348,6 +409,8 @@ class rcube_ldap if ($entry && ($rec = ldap_get_attributes($this->conn, $entry))) { + // Add in the dn for the entry. + $rec["dn"] = base64_decode($dn); $res = $this->_ldap2result($rec); $this->result = new rcube_result_set(1); $this->result->add($res); @@ -362,12 +425,39 @@ class rcube_ldap * Create a new contact record * * @param array Hash array with save data - * @return boolean The create record ID on success, False on error + * @return encoded record ID on success, False on error */ function insert($save_cols) { - // TODO - return false; + // Map out the column names to their LDAP ones to build the new entry. + $newentry = array(); + $newentry["objectClass"] = $this->prop["LDAP_Object_Classes"]; + foreach ($save_cols as $col => $val) { + $fld = ""; + $fld = $this->_map_field($col); + if ($fld != "") { + // The field does exist, add it to the entry. + $newentry[$fld] = $val; + } // end if + } // end foreach + + // Verify that the required fields are set. + // We know that the email address is required as a default of rcube, so + // we will default its value into any unfilled required fields. + foreach ($this->prop["required_fields"] as $fld) { + if (!isset($newentry[$fld])) { + $newentry[$fld] = $newentry[$this->_map_field("email")]; + } // end if + } // end foreach + + // Build the new entries DN. + $dn = $this->prop["LDAP_rdn"]."=".$newentry[$this->prop["LDAP_rdn"]].",".$this->prop['base_dn']; + $res = @ldap_add($this->conn, $dn, $newentry); + if ($res === FALSE) { + return false; + } // end if + + return base64_encode($dn); } @@ -380,8 +470,66 @@ class rcube_ldap */ function update($id, $save_cols) { - // TODO - return false; + $record = $this->get_record($id, true); + $result = $this->get_result(); + $record = $result->first(); + + $newdata = array(); + $replacedata = array(); + $deletedata = array(); + foreach ($save_cols as $col => $val) { + $fld = ""; + $fld = $this->_map_field($col); + if ($fld != "") { + // The field does exist compare it to the ldap record. + if ($record[$col] != $val) { + // Changed, but find out how. + if (!isset($record[$col])) { + // Field was not set prior, need to add it. + $newdata[$fld] = $val; + } // end if + elseif ($val == "") { + // Field supplied is empty, verify that it is not required. + if (!in_array($fld, $this->prop["required_fields"])) { + // It is not, safe to clear. + $deletedata[$fld] = $record[$col]; + } // end if + } // end elseif + else { + // The data was modified, save it out. + $replacedata[$fld] = $val; + } // end else + } // end if + } // end if + } // end foreach + + // Update the entry as required. + $dn = base64_decode($id); + if (!empty($deletedata)) { + // Delete the fields. + $res = @ldap_mod_del($this->conn, $dn, $deletedata); + if ($res === FALSE) { + return false; + } // end if + } // end if + + if (!empty($replacedata)) { + // Replace the fields. + $res = @ldap_mod_replace($this->conn, $dn, $replacedata); + if ($res === FALSE) { + return false; + } // end if + } // end if + + if (!empty($newdata)) { + // Add the fields. + $res = @ldap_mod_add($this->conn, $dn, $newdata); + if ($res === FALSE) { + return false; + } // end if + } // end if + + return true; } @@ -393,8 +541,21 @@ class rcube_ldap */ function delete($ids) { - // TODO - return false; + if (!is_array($ids)) { + // Not an array, break apart the encoded DNs. + $dns = explode(",", $ids); + } // end if + + foreach ($dns as $id) { + $dn = base64_decode($id); + // Delete the record. + $res = @ldap_delete($this->conn, $dn); + if ($res === FALSE) { + return false; + } // end if + } // end foreach + + return true; } diff --git a/program/js/app.js b/program/js/app.js index d7bd39a4e..aa90b3f41 100644 --- a/program/js/app.js +++ b/program/js/app.js @@ -2476,7 +2476,7 @@ function rcube_webmail() qs += '&_search='+this.env.search_request; // send request to server - this.http_post('delete', '_cid='+urlencode(a_cids.join(','))+'&_from='+(this.env.action ? this.env.action : '')+qs); + this.http_post('delete', '_cid='+urlencode(a_cids.join(','))+'&_source='+urlencode(this.env.source)+'&_from='+(this.env.action ? this.env.action : '')+qs); return true; }; diff --git a/program/steps/addressbook/delete.inc b/program/steps/addressbook/delete.inc index f91b9ac42..df1e4073e 100644 --- a/program/steps/addressbook/delete.inc +++ b/program/steps/addressbook/delete.inc @@ -19,7 +19,10 @@ */ -if (($cid = get_input_value('_cid', RCUBE_INPUT_POST)) && preg_match('/^[0-9]+(,[0-9]+)*$/', $cid)) +if (($cid = get_input_value('_cid', RCUBE_INPUT_POST)) && + (preg_match('/^[0-9]+(,[0-9]+)*$/', $cid) || + preg_match('/^[a-zA-Z0-9=]+(,[a-zA-Z0-9=]+)*$/', $cid)) + ) { $deleted = $CONTACTS->delete($cid); if (!$deleted) diff --git a/program/steps/addressbook/edit.inc b/program/steps/addressbook/edit.inc index 64eab86eb..9cda22b5b 100644 --- a/program/steps/addressbook/edit.inc +++ b/program/steps/addressbook/edit.inc @@ -91,6 +91,7 @@ function get_form_tags($attrib) { $hiddenfields = new html_hiddenfield(array('name' => '_task', 'value' => $RCMAIL->task)); $hiddenfields->add(array('name' => '_action', 'value' => 'save', 'source' => get_input_value('_source', RCUBE_INPUT_GPC))); + $hiddenfields->add(array('name' => '_source', 'value' => get_input_value('_source', RCUBE_INPUT_GPC))); if (($result = $CONTACTS->get_result()) && ($record = $result->first())) $hiddenfields->add(array('name' => '_cid', 'value' => $record['ID'])); @@ -117,4 +118,4 @@ if (!$CONTACTS->get_result() && template_exists('addcontact')) // this will be executed if no template for addcontact exists $OUTPUT->send('editcontact'); -?> \ No newline at end of file +?> diff --git a/program/steps/addressbook/func.inc b/program/steps/addressbook/func.inc index 28b540ac8..a08b5510c 100644 --- a/program/steps/addressbook/func.inc +++ b/program/steps/addressbook/func.inc @@ -22,8 +22,17 @@ // instantiate a contacts object according to the given source if (($source = get_input_value('_source', RCUBE_INPUT_GPC)) && isset($CONFIG['ldap_public'][$source])) $CONTACTS = new rcube_ldap($CONFIG['ldap_public'][$source]); -else - $CONTACTS = new rcube_contacts($DB, $_SESSION['user_id']); +else { + if (!$CONFIG["use_SQL_address_book"]) { + // Get the first LDAP address book. + $source = key((array)$CONFIG['ldap_public']); + $prop = current((array)$CONFIG['ldap_public']); + $CONTACTS = new rcube_ldap($prop); + } // end if + else { + $CONTACTS = new rcube_contacts($DB, $_SESSION['user_id']); + } // end else +} // end else $CONTACTS->set_pagesize($CONFIG['pagesize']); @@ -42,9 +51,13 @@ $OUTPUT->set_env('source', $source ? $source : '0'); $OUTPUT->set_env('readonly', $CONTACTS->readonly, false); // add list of address sources to client env -$js_list = array("0" => array('id' => 0, 'readonly' => false)); +$js_list = array(); +if ($CONFIG["use_SQL_address_book"]) { + // We are using the DB address book, add it. + $js_list = array("0" => array('id' => 0, 'readonly' => false)); +} // end if foreach ((array)$CONFIG['ldap_public'] as $id => $prop) - $js_list[$id] = array('id' => $id, 'readonly' => !$prop['writeable']); + $js_list[$id] = array('id' => $id, 'readonly' => !$prop['writable']); $OUTPUT->set_env('address_sources', $js_list); @@ -66,19 +79,28 @@ function rcmail_directory_list($attrib) // allow the following attributes to be added to the