diff options
| -rw-r--r-- | CHANGELOG | 8 | ||||
| -rw-r--r-- | config/main.inc.php.dist | 37 | ||||
| -rw-r--r-- | program/include/main.inc | 2 | ||||
| -rw-r--r-- | program/include/rcube_ldap.php | 187 | ||||
| -rw-r--r-- | program/js/app.js | 2 | ||||
| -rw-r--r-- | program/steps/addressbook/delete.inc | 5 | ||||
| -rw-r--r-- | program/steps/addressbook/edit.inc | 3 | ||||
| -rw-r--r-- | program/steps/addressbook/func.inc | 56 | ||||
| -rw-r--r-- | program/steps/mail/addcontact.inc | 16 | 
9 files changed, 277 insertions, 39 deletions
| @@ -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('<tr id="rcmrow%d" class="contact '.$zebra_class.'">'."\n", $row_data[$id_col]); +      $table .= sprintf('<tr id="rcmrow%s" class="contact '.$zebra_class.'">'."\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 <ul> tag    $out = '<ul' . create_attrib_string($attrib, array('style', 'class', 'id')) . ">\n"; -  $out .= sprintf($line_templ, -    'rcmli'.$local_id, -    !$current ? 'selected' : '', -    Q(rcmail_url('list', array('_source' => 0))), -    JS_OBJECT_NAME, -    $local_id, -    JS_OBJECT_NAME, -    $local_id, -    JS_OBJECT_NAME, -    $local_id, -    JS_OBJECT_NAME, -    $local_id, -    rcube_label('personaladrbook')); +  if ($CONFIG["use_SQL_address_book"]) { +    $out .= sprintf($line_templ, +      'rcmli'.$local_id, +      !$current ? 'selected' : '', +      Q(rcmail_url('list', array('_source' => 0))), +      JS_OBJECT_NAME, +      $local_id, +      JS_OBJECT_NAME, +      $local_id, +      JS_OBJECT_NAME, +      $local_id, +      JS_OBJECT_NAME, +      $local_id, +      rcube_label('personaladrbook')); +  } // end if +  else { +    // DB address book not used, see if a source is set, if not use the +    // first LDAP directory. +    if (!$current) { +      $current = key((array)$CONFIG['ldap_public']); +    } // end if +  } // end else    foreach ((array)$CONFIG['ldap_public'] as $id => $prop)    { diff --git a/program/steps/mail/addcontact.inc b/program/steps/mail/addcontact.inc index 0ad10313b..6f4187ba4 100644 --- a/program/steps/mail/addcontact.inc +++ b/program/steps/mail/addcontact.inc @@ -23,7 +23,19 @@ $done = false;  if (!empty($_POST['_address']))  { -  $CONTACTS = new rcube_contacts($DB, $_SESSION['user_id']); +  $CONTACTS = array(); +  if (!$CONFIG["use_SQL_address_book"]) { +    // Use the first writable LDAP address book. +    foreach ($CONFIG["ldap_public"] as $id => $prop) { +      if ($prop["writable"]) { +        $CONTACTS = new rcube_ldap($prop); +        break; +      } // end if +    } // end foreach +  } // end if +  else { +    $CONTACTS = new rcube_contacts($DB, $_SESSION['user_id']); +  } // end else    $contact_arr = $IMAP->decode_address_list(get_input_value('_address', RCUBE_INPUT_POST, true), 1, false);    if (!empty($contact_arr[1]['mailto'])) @@ -50,4 +62,4 @@ if (!$done)    $OUTPUT->show_message('errorsavingcontact', 'warning');  $OUTPUT->send(); -?>
\ No newline at end of file +?> | 
