From 30b30226e6569f13e444cdcb513cd2bfc24318d7 Mon Sep 17 00:00:00 2001
From: alecpl <alec@alec.pl>
Date: Thu, 04 Nov 2010 10:03:26 -0400
Subject: [PATCH] - Add possibility to force mailbox selection. There're situations where we're invoking   STATUS (for all messages count) and SELECT later for other operations. If we   call SELECT first, the STATUS will be not needed.

---
 program/include/rcube_ldap.php |  300 +++++++++++++++++++++++++++++++++++++++++++++--------------
 1 files changed, 229 insertions(+), 71 deletions(-)

diff --git a/program/include/rcube_ldap.php b/program/include/rcube_ldap.php
index 544c7f7..d5cc132 100644
--- a/program/include/rcube_ldap.php
+++ b/program/include/rcube_ldap.php
@@ -3,8 +3,8 @@
  +-----------------------------------------------------------------------+
  | program/include/rcube_ldap.php                                        |
  |                                                                       |
- | This file is part of the RoundCube Webmail client                     |
- | Copyright (C) 2006-2009, RoundCube Dev. - Switzerland                 |
+ | This file is part of the Roundcube Webmail client                     |
+ | Copyright (C) 2006-2010, Roundcube Dev. - Switzerland                 |
  | Licensed under the GNU GPL                                            |
  |                                                                       |
  | PURPOSE:                                                              |
@@ -29,35 +29,48 @@
   var $conn;
   var $prop = array();
   var $fieldmap = array();
-  
+
   var $filter = '';
   var $result = null;
   var $ldap_result = null;
   var $sort_col = '';
-  
+  var $mail_domain = '';
+  var $debug = false;
+
   /** public properties */
   var $primary_key = 'ID';
   var $readonly = true;
   var $list_page = 1;
   var $page_size = 10;
   var $ready = false;
-  
-  
+
+
   /**
    * Object constructor
    *
-   * @param array LDAP connection properties
+   * @param array 	LDAP connection properties
+   * @param boolean 	Enables debug mode
+   * @param string 	Current user mail domain name
    * @param integer User-ID
    */
-  function __construct($p)
+  function __construct($p, $debug=false, $mail_domain=NULL)
   {
     $this->prop = $p;
-    
+
     foreach ($p as $prop => $value)
       if (preg_match('/^(.+)_field$/', $prop, $matches))
-        $this->fieldmap[$matches[1]] = $value;
+        $this->fieldmap[$matches[1]] = $this->_attr_name(strtolower($value));
 
-    $this->sort_col = $p["sort"];
+    // make sure 'required_fields' is an array
+    if (!is_array($this->prop['required_fields']))
+      $this->prop['required_fields'] = (array) $this->prop['required_fields'];
+
+    foreach ($this->prop['required_fields'] as $key => $val)
+      $this->prop['required_fields'][$key] = $this->_attr_name(strtolower($val));
+
+    $this->sort_col = $p['sort'];
+    $this->debug = $debug;
+    $this->mail_domain = $mail_domain;
 
     $this->connect();
   }
@@ -71,11 +84,13 @@
     global $RCMAIL;
     
     if (!function_exists('ldap_connect'))
-      raise_error(array('code' => 100, 'type' => 'ldap', 'message' => "No ldap support in this installation of PHP"), true);
+      raise_error(array('code' => 100, 'type' => 'ldap',
+        'file' => __FILE__, 'line' => __LINE__,
+        'message' => "No ldap support in this installation of PHP"), true);
 
     if (is_resource($this->conn))
       return true;
-    
+
     if (!is_array($this->prop['hosts']))
       $this->prop['hosts'] = array($this->prop['hosts']);
 
@@ -84,17 +99,23 @@
 
     foreach ($this->prop['hosts'] as $host)
     {
+      $host = idn_to_ascii(rcube_parse_host($host));
+      $this->_debug("C: Connect [$host".($this->prop['port'] ? ':'.$this->prop['port'] : '')."]");
+
       if ($lc = @ldap_connect($host, $this->prop['port']))
       {
         if ($this->prop['use_tls']===true)
           if (!ldap_start_tls($lc))
             continue;
 
+        $this->_debug("S: OK");
+
         ldap_set_option($lc, LDAP_OPT_PROTOCOL_VERSION, $this->prop['ldap_version']);
         $this->prop['host'] = $host;
         $this->conn = $lc;
         break;
       }
+      $this->_debug("S: NOT OK");
     }
     
     if (is_resource($this->conn))
@@ -102,27 +123,51 @@
       $this->ready = true;
 
       // User specific access, generate the proper values to use.
-      if ($this->prop["user_specific"]) {
+      if ($this->prop['user_specific']) {
         // No password set, use the session password
         if (empty($this->prop['bind_pass'])) {
-          $this->prop['bind_pass'] = $RCMAIL->decrypt_passwd($_SESSION["password"]);
+          $this->prop['bind_pass'] = $RCMAIL->decrypt($_SESSION['password']);
         }
 
         // Get the pieces needed for variable replacement.
         $fu = $RCMAIL->user->get_username();
         list($u, $d) = explode('@', $fu);
-        
+        $dc = 'dc='.strtr($d, array('.' => ',dc=')); // hierarchal domain string
+
+        $replaces = array('%dc' => $dc, '%d' => $d, '%fu' => $fu, '%u' => $u);
+
+        if ($this->prop['search_base_dn'] && $this->prop['search_filter']) {
+          // Search for the dn to use to authenticate
+          $this->prop['search_base_dn'] = strtr($this->prop['search_base_dn'], $replaces);
+          $this->prop['search_filter'] = strtr($this->prop['search_filter'], $replaces);
+
+          $this->_debug("S: searching with base {$this->prop['search_base_dn']} for {$this->prop['search_filter']}");
+
+          $res = ldap_search($this->conn, $this->prop['search_base_dn'], $this->prop['search_filter'], array('uid'));
+          if ($res && ($entry = ldap_first_entry($this->conn, $res))) {
+            $bind_dn = ldap_get_dn($this->conn, $entry);
+
+            $this->_debug("S: search returned dn: $bind_dn");
+
+            if ($bind_dn) {
+              $this->prop['bind_dn'] = $bind_dn;
+              $dn = ldap_explode_dn($bind_dn, 1);
+              $replaces['%dn'] = $dn[0];
+            }
+          }
+        }
         // Replace the bind_dn and base_dn variables.
-        $replaces = array('%fu' => $fu, '%u' => $u, '%d' => $d);
         $this->prop['bind_dn'] = strtr($this->prop['bind_dn'], $replaces);
         $this->prop['base_dn'] = strtr($this->prop['base_dn'], $replaces);
       }
-      
+
       if (!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('code' => 100, 'type' => 'ldap', 'message' => "Could not connect to any LDAP server, tried $host:{$this->prop[port]} last"), true);
+      raise_error(array('code' => 100, 'type' => 'ldap',
+        'file' => __FILE__, 'line' => __LINE__,
+        'message' => "Could not connect to any LDAP server, last tried $host:{$this->prop[port]}"), true);
 
     // See if the directory is writeable.
     if ($this->prop['writable']) {
@@ -145,13 +190,18 @@
       return false;
     }
     
+    $this->_debug("C: Bind [dn: $dn] [pass: $pass]");
+    
     if (@ldap_bind($this->conn, $dn, $pass)) {
+      $this->_debug("S: OK");
       return true;
     }
 
+    $this->_debug("S: ".ldap_error($this->conn));
+
     raise_error(array(
-        'code' => ldap_errno($this->conn),
-        'type' => 'ldap',
+        'code' => ldap_errno($this->conn), 'type' => 'ldap',
+	'file' => __FILE__, 'line' => __LINE__,
         'message' => "Bind failed for dn=$dn: ".ldap_error($this->conn)),
         true);
 
@@ -166,7 +216,8 @@
   {
     if ($this->conn)
     {
-      @ldap_unbind($this->conn);
+      $this->_debug("C: Close");
+      ldap_unbind($this->conn);
       $this->conn = null;
     }
   }
@@ -244,19 +295,19 @@
       $filter = $this->prop['filter'];
       $this->set_search_set($filter);
     }
-    
+
     // exec LDAP search if no result resource is stored
     if ($this->conn && !$this->ldap_result)
       $this->_exec_search();
     
     // count contacts for this user
     $this->result = $this->count();
-    
+
     // we have a search result resource
     if ($this->ldap_result && $this->result->count > 0)
     {
-      if ($this->sort_col && $this->prop['scope'] !== "base")
-        @ldap_sort($this->conn, $this->ldap_result, $this->sort_col);
+      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;
@@ -276,10 +327,13 @@
    *
    * @param array   List of fields to search in
    * @param string  Search value
+   * @param boolean True for strict, False for partial (fuzzy) matching
    * @param boolean True if results are requested, False if count only
+   * @param boolean (Not used)
+   * @param array   List of fields that cannot be empty
    * @return array  Indexed list of contact records and 'count' value
    */
-  function search($fields, $value, $strict=false, $select=true)
+  function search($fields, $value, $strict=false, $select=true, $nocount=false, $required=array())
   {
     // special treatment for ID-based search
     if ($fields == 'ID' || $fields == $this->primary_key)
@@ -310,10 +364,19 @@
           $filter .= "($f=$wc" . rcube_ldap::quote_string($value) . "$wc)";
     }
     $filter .= ')';
-    
+
+    // add required (non empty) fields filter
+    $req_filter = '';
+    foreach ((array)$required as $field)
+      if ($f = $this->_map_field($field))
+        $req_filter .= "($f=*)";
+
+    if (!empty($req_filter))
+      $filter = '(&' . $req_filter . $filter . ')';
+
     // avoid double-wildcard if $value is empty
     $filter = preg_replace('/\*+/', '*', $filter);
-    
+
     // add general filter to query
     if (!empty($this->prop['filter']))
       $filter = '(&(' . preg_replace('/^\(|\)$/', '', $this->prop['filter']) . ')' . $filter . ')';
@@ -381,13 +444,23 @@
     $res = null;
     if ($this->conn && $dn)
     {
-      $this->ldap_result = @ldap_read($this->conn, base64_decode($dn), "(objectclass=*)", array_values($this->fieldmap));
-      $entry = @ldap_first_entry($this->conn, $this->ldap_result);
-      
+      $dn = base64_decode($dn);
+
+      $this->_debug("C: Read [dn: $dn] [(objectclass=*)]");
+    
+      if ($this->ldap_result = @ldap_read($this->conn, $dn, '(objectclass=*)', array_values($this->fieldmap)))
+        $entry = ldap_first_entry($this->conn, $this->ldap_result);
+      else
+        $this->_debug("S: ".ldap_error($this->conn));
+
       if ($entry && ($rec = ldap_get_attributes($this->conn, $entry)))
       {
+        $this->_debug("S: OK");
+
+        $rec = array_change_key_case($rec, CASE_LOWER);
+
         // Add in the dn for the entry.
-        $rec["dn"] = base64_decode($dn);
+        $rec['dn'] = $dn;
         $res = $this->_ldap2result($rec);
         $this->result = new rcube_result_set(1);
         $this->result->add($res);
@@ -408,11 +481,10 @@
   {
     // Map out the column names to their LDAP ones to build the new entry.
     $newentry = array();
-    $newentry["objectClass"] = $this->prop["LDAP_Object_Classes"];
+    $newentry['objectClass'] = $this->prop['LDAP_Object_Classes'];
     foreach ($save_cols as $col => $val) {
-      $fld = "";
       $fld = $this->_map_field($col);
-      if ($fld != "") {
+      if ($fld && $val) {
         // The field does exist, add it to the entry.
         $newentry[$fld] = $val;
       } // end if
@@ -421,18 +493,25 @@
     // 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) {
+    foreach ($this->prop['required_fields'] as $fld) {
       if (!isset($newentry[$fld])) {
-        $newentry[$fld] = $newentry[$this->_map_field("email")];
+        $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);
+    $dn = $this->prop['LDAP_rdn'].'='.rcube_ldap::quote_string($newentry[$this->prop['LDAP_rdn']], true)
+      .','.$this->prop['base_dn'];
+
+    $this->_debug("C: Add [dn: $dn]: ".print_r($newentry, true));
+
+    $res = ldap_add($this->conn, $dn, $newentry);
     if ($res === FALSE) {
+      $this->_debug("S: ".ldap_error($this->conn));
       return false;
     } // end if
+
+    $this->_debug("S: OK");
 
     return base64_encode($dn);
   }
@@ -455,9 +534,8 @@
     $replacedata = array();
     $deletedata = array();
     foreach ($save_cols as $col => $val) {
-      $fld = "";
       $fld = $this->_map_field($col);
-      if ($fld != "") {
+      if ($fld) {
         // The field does exist compare it to the ldap record.
         if ($record[$col] != $val) {
           // Changed, but find out how.
@@ -465,9 +543,9 @@
             // Field was not set prior, need to add it.
             $newdata[$fld] = $val;
           } // end if
-          elseif ($val == "") {
+          elseif ($val == '') {
             // Field supplied is empty, verify that it is not required.
-            if (!in_array($fld, $this->prop["required_fields"])) {
+            if (!in_array($fld, $this->prop['required_fields'])) {
               // It is not, safe to clear.
               $deletedata[$fld] = $record[$col];
             } // end if
@@ -480,31 +558,61 @@
       } // end if
     } // end foreach
 
-    // Update the entry as required.
     $dn = base64_decode($id);
+
+    // Update the entry as required.
     if (!empty($deletedata)) {
       // Delete the fields.
-      $res = @ldap_mod_del($this->conn, $dn, $deletedata);
-      if ($res === FALSE) {
+      $this->_debug("C: Delete [dn: $dn]: ".print_r($deletedata, true));
+      if (!ldap_mod_del($this->conn, $dn, $deletedata)) {
+        $this->_debug("S: ".ldap_error($this->conn));
         return false;
-      } // end if
+      }
+      $this->_debug("S: OK");
     } // end if
 
     if (!empty($replacedata)) {
+      // Handle RDN change
+      if ($replacedata[$this->prop['LDAP_rdn']]) {
+        $newdn = $this->prop['LDAP_rdn'].'='
+	  .rcube_ldap::quote_string($replacedata[$this->prop['LDAP_rdn']], true)
+	  .','.$this->prop['base_dn']; 
+        if ($dn != $newdn) {
+          $newrdn = $this->prop['LDAP_rdn'].'='
+	    .rcube_ldap::quote_string($replacedata[$this->prop['LDAP_rdn']], true);
+          unset($replacedata[$this->prop['LDAP_rdn']]);
+        }
+      }
       // Replace the fields.
-      $res = @ldap_mod_replace($this->conn, $dn, $replacedata);
-      if ($res === FALSE) {
-        return false;
+      if (!empty($replacedata)) {
+        $this->_debug("C: Replace [dn: $dn]: ".print_r($replacedata, true));
+        if (!ldap_mod_replace($this->conn, $dn, $replacedata)) {
+          $this->_debug("S: ".ldap_error($this->conn));
+          return false;
+	}
+        $this->_debug("S: OK");
       } // end if
     } // end if
 
     if (!empty($newdata)) {
       // Add the fields.
-      $res = @ldap_mod_add($this->conn, $dn, $newdata);
-      if ($res === FALSE) {
+      $this->_debug("C: Add [dn: $dn]: ".print_r($newdata, true));
+      if (!ldap_mod_add($this->conn, $dn, $newdata)) {
+        $this->_debug("S: ".ldap_error($this->conn));
         return false;
-      } // end if
+      }
+      $this->_debug("S: OK");
     } // end if
+
+    // Handle RDN change
+    if (!empty($newrdn)) {
+      $this->_debug("C: Rename [dn: $dn] [dn: $newrdn]");
+      if (@ldap_rename($this->conn, $dn, $newrdn, NULL, TRUE)) {
+        $this->_debug("S: ".ldap_error($this->conn));
+        return base64_encode($newdn);
+      }
+      $this->_debug("S: OK");
+    }
 
     return true;
   }
@@ -520,19 +628,22 @@
   {
     if (!is_array($ids)) {
       // Not an array, break apart the encoded DNs.
-      $dns = explode(",", $ids);
+      $dns = explode(',', $ids);
     } // end if
 
     foreach ($dns as $id) {
       $dn = base64_decode($id);
+      $this->_debug("C: Delete [dn: $dn]");
       // Delete the record.
-      $res = @ldap_delete($this->conn, $dn);
+      $res = ldap_delete($this->conn, $dn);
       if ($res === FALSE) {
+        $this->_debug("S: ".ldap_error($this->conn));
         return false;
       } // end if
+      $this->_debug("S: OK");
     } // end foreach
 
-    return true;
+    return count($dns);
   }
 
 
@@ -541,24 +652,35 @@
    *
    * @access private
    */
-  function _exec_search()
+  private function _exec_search()
   {
-    if ($this->ready && $this->filter)
+    if ($this->ready)
     {
+      $filter = $this->filter ? $this->filter : '(objectclass=*)';
       $function = $this->prop['scope'] == 'sub' ? 'ldap_search' : ($this->prop['scope'] == 'base' ? 'ldap_read' : 'ldap_list');
-      $this->ldap_result = $function($this->conn, $this->prop['base_dn'], $this->filter, array_values($this->fieldmap), 0, 0);
-      return true;
+
+      $this->_debug("C: Search [".$filter."]");
+
+      if ($this->ldap_result = @$function($this->conn, $this->prop['base_dn'], $filter,
+          array_values($this->fieldmap), 0, (int) $this->prop['sizelimit'], (int) $this->prop['timelimit'])
+      ) {
+        $this->_debug("S: ".ldap_count_entries($this->conn, $this->ldap_result)." record(s)");
+        return true;
+      } else
+        $this->_debug("S: ".ldap_error($this->conn));
     }
-    else
-      return false;
+    
+    return false;
   }
   
   
   /**
    * @access private
    */
-  function _ldap2result($rec)
+  private function _ldap2result($rec)
   {
+    global $RCMAIL;
+
     $out = array();
     
     if ($rec['dn'])
@@ -566,8 +688,12 @@
     
     foreach ($this->fieldmap as $rf => $lf)
     {
-      if ($rec[$lf]['count'])
-        $out[$rf] = $rec[$lf][0];
+      if ($rec[$lf]['count']) {
+        if ($rf == 'email' && $this->mail_domain && !strpos($rec[$lf][0], '@'))
+          $out[$rf] = sprintf('%s@%s', $rec[$lf][0], $this->mail_domain);
+        else
+          $out[$rf] = $rec[$lf][0];
+      }
     }
     
     return $out;
@@ -577,21 +703,53 @@
   /**
    * @access private
    */
-  function _map_field($field)
+  private function _map_field($field)
   {
     return $this->fieldmap[$field];
   }
   
   
   /**
-   * @static
+   * @access private
    */
-  function quote_string($str)
+  private function _attr_name($name)
   {
-    return strtr($str, array('*'=>'\2a', '('=>'\28', ')'=>'\29', '\\'=>'\5c'));
+    // list of known attribute aliases
+    $aliases = array(
+      'gn' => 'givenname',
+      'rfc822mailbox' => 'email',
+      'userid' => 'uid',
+      'emailaddress' => 'email',
+      'pkcs9email' => 'email',
+    );
+    return isset($aliases[$name]) ? $aliases[$name] : $name;
   }
 
 
-}
+  /**
+   * @access private
+   */
+  private function _debug($str)
+  {
+    if ($this->debug)
+      write_log('ldap', $str);
+  }
+  
 
+  /**
+   * @static
+   */
+  function quote_string($str, $dn=false)
+  {
+    if ($dn)
+      $replace = array(','=>'\2c', '='=>'\3d', '+'=>'\2b', '<'=>'\3c',
+        '>'=>'\3e', ';'=>'\3b', '\\'=>'\5c', '"'=>'\22', '#'=>'\23');
+    else
+      $replace = array('*'=>'\2a', '('=>'\28', ')'=>'\29', '\\'=>'\5c',
+        '/'=>'\2f');
+
+    return strtr($str, $replace);
+  }
+
+}
 

--
Gitblit v1.9.1