Source for file abook_ldap_server.php

Documentation is available at abook_ldap_server.php

  1. <?php
  2.  
  3. /**
  4.  * abook_ldap_server.php
  5.  *
  6.  * Address book backend for LDAP server
  7.  *
  8.  * LDAP filtering code by Tim Bell
  9.  *   <bhat at users.sourceforge.net> (#539534)
  10.  * ADS limit_scope code by Michael Brown
  11.  *   <mcb30 at users.sourceforge.net> (#1035454)
  12.  * StartTLS code by John Lane
  13.  *   <starfry at users.sourceforge.net> (#1197703)
  14.  * Code for remove, add, modify, lookup by David Härdeman
  15.  *   <david at 2gen.com> (#1495763)
  16.  *
  17.  * This backend uses LDAP person (RFC2256), organizationalPerson (RFC2256)
  18.  * and inetOrgPerson (RFC2798) objects and dn, description, sn, givenname,
  19.  * cn, mail attributes. Other attributes are ignored.
  20.  * 
  21.  * @copyright &copy; 1999-2006 The SquirrelMail Project Team
  22.  * @license http://opensource.org/licenses/gpl-license.php GNU Public License
  23.  * @version $Id: abook_ldap_server.php,v 1.42 2006/06/04 12:42:24 tokul Exp $
  24.  * @package squirrelmail
  25.  * @subpackage addressbook
  26.  */
  27.  
  28. /**
  29.  * Address book backend for LDAP server
  30.  *
  31.  * An array with the following elements must be passed to
  32.  * the class constructor (elements marked ? are optional)
  33.  *
  34.  * Main settings:
  35.  * <pre>
  36.  *    host      => LDAP server hostname, IP-address or any other URI compatible
  37.  *                 with used LDAP library.
  38.  *    base      => LDAP server root (base dn). Empty string allowed.
  39.  *  ? port      => LDAP server TCP port number (default: 389)
  40.  *  ? charset   => LDAP server charset (default: utf-8)
  41.  *  ? name      => Name for LDAP server (default "LDAP: hostname")
  42.  *                 Used to tag the result data
  43.  *  ? maxrows   => Maximum # of rows in search result
  44.  *  ? timeout   => Timeout for LDAP operations (in seconds, default: 30)
  45.  *                 Might not work for all LDAP libraries or servers.
  46.  *  ? binddn    => LDAP Bind DN.
  47.  *  ? bindpw    => LDAP Bind Password.
  48.  *  ? protocol  => LDAP Bind protocol.
  49.  * </pre>
  50.  * Advanced settings:
  51.  * <pre>
  52.  *  ? filter    => Filter expression to limit ldap searches
  53.  *  ? limit_scope => Limits scope to base DN (Specific to Win2k3 ADS).
  54.  *  ? listing   => Controls listing of LDAP directory.
  55.  *  ? writeable => Controls write access to address book
  56.  *  ? search_tree => Controls subtree or one level search.
  57.  *  ? starttls  => Controls use of StartTLS on LDAP connections
  58.  * </pre>
  59.  * NOTE. This class should not be used directly. Use addressbook_init()
  60.  *       function instead.
  61.  * @package squirrelmail
  62.  * @subpackage addressbook
  63.  */
  64.     /**
  65.      * @var string backend type
  66.      */
  67.     var $btype = 'remote';
  68.     /**
  69.      * @var string backend name
  70.      */
  71.     var $bname = 'ldap_server';
  72.  
  73.     /* Parameters changed by class */
  74.     /**
  75.      * @var string displayed name
  76.      */
  77.     var $sname   = 'LDAP';       /* Service name */
  78.     /**
  79.      * @var string LDAP server name or address or url
  80.      */
  81.     var $server  = '';
  82.     /**
  83.      * @var integer LDAP server port
  84.      */
  85.     var $port    = 389;
  86.     /**
  87.      * @var string LDAP base DN
  88.      */
  89.     var $basedn  = '';
  90.     /**
  91.      * @var string charset used for entries in LDAP server
  92.      */
  93.     var $charset = 'utf-8';
  94.     /**
  95.      * @var object PHP LDAP link ID
  96.      */
  97.     var $linkid  = false;
  98.     /**
  99.      * @var bool True if LDAP server is bound
  100.      */
  101.     var $bound   = false;
  102.     /**
  103.      * @var integer max rows in result
  104.      */
  105.     var $maxrows = 250;
  106.     /**
  107.      * @var string ldap filter
  108.      * @since 1.5.1
  109.      */
  110.     var $filter = '';
  111.     /**
  112.      * @var integer timeout of LDAP operations (in seconds)
  113.      */
  114.     var $timeout = 30;
  115.     /**
  116.      * @var string DN to bind to (non-anonymous bind)
  117.      * @since 1.5.0 and 1.4.3
  118.      */
  119.     var $binddn = '';
  120.     /**
  121.      * @var string  password to bind with (non-anonymous bind)
  122.      * @since 1.5.0 and 1.4.3
  123.      */
  124.     var $bindpw = '';
  125.     /**
  126.      * @var integer protocol used to connect to ldap server
  127.      * @since 1.5.0 and 1.4.3
  128.      */
  129.     var $protocol = '';
  130.     /**
  131.      * @var boolean limits scope to base dn
  132.      * @since 1.5.1
  133.      */
  134.     var $limit_scope = false;
  135.     /**
  136.      * @var boolean controls listing of directory
  137.      * @since 1.5.1
  138.      */
  139.     var $listing = false;
  140.     /**
  141.      * @var boolean true if removing/adding/modifying entries is allowed
  142.      * @since 1.5.2
  143.      */
  144.     var $writeable = false;
  145.     /**
  146.      * @var boolean controls ldap search type.
  147.      *  only first level entries are displayed if set to false
  148.      * @since 1.5.1
  149.      */
  150.     var $search_tree = true;
  151.     /**
  152.      * @var boolean controls use of StartTLS on ldap
  153.      *  connections. Requires php 4.2+ and protocol >= 3
  154.      * @since 1.5.1
  155.      */
  156.     var $starttls = false;
  157.  
  158.     /**
  159.      * Constructor. Connects to database
  160.      * @param array connection options
  161.      */
  162.     function abook_ldap_server($param{
  163.         if(!function_exists('ldap_connect')) {
  164.             $this->set_error(_("PHP install does not have LDAP support."));
  165.             return;
  166.         }
  167.         if(is_array($param)) {
  168.             $this->server = $param['host'];
  169.             // remove whitespace from basedn
  170.             $this->basedn = preg_replace('/,\s*/',',',trim($param['base']));
  171.  
  172.             if(!empty($param['port']))
  173.                 $this->port = $param['port'];
  174.  
  175.             if(!empty($param['charset']))
  176.                 $this->charset = strtolower($param['charset']);
  177.  
  178.             if(isset($param['maxrows']))
  179.                 $this->maxrows = $param['maxrows'];
  180.  
  181.             if(isset($param['timeout']))
  182.                 $this->timeout = $param['timeout'];
  183.  
  184.             if(isset($param['binddn']))
  185.                 $this->binddn = $param['binddn'];
  186.  
  187.             if(isset($param['bindpw']))
  188.                 $this->bindpw = $param['bindpw'];
  189.  
  190.             if(isset($param['protocol']))
  191.                 $this->protocol = (int) $param['protocol'];
  192.  
  193.             if(isset($param['filter']))
  194.                 $this->filter = trim($param['filter']);
  195.  
  196.             if(isset($param['limit_scope']))
  197.                 $this->limit_scope = (bool) $param['limit_scope'];
  198.  
  199.             if(isset($param['listing']))
  200.                 $this->listing = (bool) $param['listing'];
  201.  
  202.             if(isset($param['writeable'])) {
  203.                 $this->writeable = (bool) $param['writeable'];
  204.                 // switch backend type to local, if it is writable
  205.                 if($this->writeable$this->btype = 'local';
  206.             }
  207.  
  208.             if(isset($param['search_tree']))
  209.                 $this->search_tree = (bool) $param['search_tree'];
  210.  
  211.             if(isset($param['starttls']))
  212.                 $this->starttls = (bool) $param['starttls'];
  213.  
  214.             if(empty($param['name'])) {
  215.                 $this->sname = 'LDAP: ' $param['host'];
  216.             else {
  217.                 $this->sname = $param['name'];
  218.             }
  219.  
  220.             /*
  221.              * don't open LDAP server on addressbook_init(),
  222.              * open ldap connection only on search. Speeds up
  223.              * addressbook_init() call.
  224.              */
  225.             // $this->open(true);
  226.         else {
  227.             $this->set_error('Invalid argument to constructor');
  228.         }
  229.     }
  230.  
  231.  
  232.     /**
  233.      * Open the LDAP server.
  234.      * @param bool $new is it a new connection
  235.      * @return bool 
  236.      */
  237.     function open($new false{
  238.         $this->error = '';
  239.  
  240.         /* Connection is already open */
  241.         if($this->linkid != false && !$new{
  242.             return true;
  243.         }
  244.  
  245.         $this->linkid = @ldap_connect($this->server$this->port);
  246.         /**
  247.          * check if connection was successful
  248.          * It does not work with OpenLDAP 2.x libraries. Connect error will be 
  249.          * displayed only on ldap command that tries to make connection 
  250.          * (ldap_start_tls or ldap_bind). 
  251.          */
  252.         if(!$this->linkid{
  253.             return $this->set_error($this->ldap_error('ldap_connect failed'));
  254.         }
  255.  
  256.         if(!empty($this->protocol)) {
  257.             // make sure that ldap_set_option() is available before using it
  258.             if(function_exists('ldap_set_option'||
  259.                !@ldap_set_option($this->linkidLDAP_OPT_PROTOCOL_VERSION$this->protocol)) {
  260.                 return $this->set_error('unable to set ldap protocol number');
  261.             }
  262.         }
  263.  
  264.         /**
  265.          * http://www.php.net/ldap-start-tls
  266.          * Check if v3 or newer protocol is used,
  267.          * check if ldap_start_tls function is available.
  268.          * Silently ignore setting, if these requirements are not satisfied.
  269.          * Break with error message if somebody tries to start TLS on
  270.          * ldaps or socket connection.
  271.          */
  272.         if($this->starttls && 
  273.            !empty($this->protocol&& $this->protocol >= &&
  274.            function_exists('ldap_start_tls') ) {
  275.             // make sure that $this->server is not ldaps:// or ldapi:// URL.
  276.             if (preg_match("/^ldap[si]:\/\/.+/i",$this->server)) {
  277.                 return $this->set_error("you can't enable starttls on ldaps and ldapi connections.");
  278.             }
  279.             
  280.             // try starting tls
  281.             if (@ldap_start_tls($this->linkid)) {
  282.                 // set error if call fails
  283.                 return $this->set_error($this->ldap_error('ldap_start_tls failed'));
  284.             }
  285.         }
  286.  
  287.         if(!empty($this->limit_scope&& $this->limit_scope{
  288.             if(empty($this->protocol|| intval($this->protocol3{
  289.                 return $this->set_error('limit_scope requires protocol >= 3');
  290.             }
  291.             // See http://msdn.microsoft.com/library/en-us/ldap/ldap/ldap_server_domain_scope_oid.asp
  292.             $ctrl array "oid" => "1.2.840.113556.1.4.1339""iscritical" => TRUE );
  293.             /*
  294.              * Option is set only during connection.
  295.              * It does not cause immediate errors with OpenLDAP 2.x libraries.
  296.              */
  297.             if(function_exists('ldap_set_option'||
  298.                !@ldap_set_option($this->linkidLDAP_OPT_SERVER_CONTROLSarray($ctrl))) {
  299.                 return $this->set_error($this->ldap_error('limit domain scope failed'));
  300.             }
  301.         }
  302.  
  303.         // authenticated bind
  304.         if(!empty($this->binddn)) {
  305.             if(!@ldap_bind($this->linkid$this->binddn$this->bindpw)) {
  306.                 return $this->set_error($this->ldap_error('authenticated ldap_bind failed'));
  307.             }
  308.         else {
  309.             // anonymous bind
  310.             if(!@ldap_bind($this->linkid)) {
  311.                 return $this->set_error($this->ldap_error('anonymous ldap_bind failed'));
  312.             }
  313.         }
  314.  
  315.         $this->bound = true;
  316.  
  317.         return true;
  318.     }
  319.  
  320.     /**
  321.      * Encode string to the charset used by this LDAP server
  322.      * @param string string that has to be encoded
  323.      * @return string encoded string
  324.      */
  325.     function charset_encode($str{
  326.         global $default_charset;
  327.         if($this->charset != $default_charset{
  328.             return charset_convert($default_charset,$str,$this->charset,false);
  329.         else {
  330.             return $str;
  331.         }
  332.     }
  333.  
  334.     /**
  335.      * Decode from charset used by this LDAP server to charset used by translation
  336.      *
  337.      * Uses SquirrelMail charset_decode functions
  338.      * @param string string that has to be decoded
  339.      * @return string decoded string
  340.      */
  341.     function charset_decode($str{
  342.         global $default_charset;
  343.         if ($this->charset != $default_charset{
  344.             return charset_convert($this->charset,$str,$default_charset,false);
  345.         else {
  346.             return $str;
  347.         }
  348.     }
  349.  
  350.     /**
  351.      * Sanitizes ldap search strings.
  352.      * See rfc2254
  353.      * @link http://www.faqs.org/rfcs/rfc2254.html
  354.      * @since 1.5.1 and 1.4.5
  355.      * @param string $string 
  356.      * @return string sanitized string
  357.      */
  358.     function ldapspecialchars($string{
  359.         $sanitized=array('\\' => '\5c',
  360.                          '*' => '\2a',
  361.                          '(' => '\28',
  362.                          ')' => '\29',
  363.                          "\x00" => '\00');
  364.  
  365.         return str_replace(array_keys($sanitized),array_values($sanitized),$string);
  366.     }
  367.  
  368.     /**
  369.      * Prepares user input for use in a ldap query.
  370.      *
  371.      * Function converts input string to character set used in LDAP server
  372.      * (charset_encode() method) and sanitizes it (ldapspecialchars()).
  373.      *
  374.      * @param string $string string to encode
  375.      * @return string ldap encoded string
  376.      * @since 1.5.2
  377.      */
  378.     function quotevalue($string{
  379.         $sanitized $this->charset_encode($string);
  380.         return $this->ldapspecialchars($sanitized);
  381.     }
  382.  
  383.     /**
  384.      * Search LDAP server.
  385.      *
  386.      * Warning: You must make sure that ldap query is correctly formated and
  387.      * sanitize use of special ldap keywords.
  388.      * @param string $expression ldap query
  389.      * @param boolean $singleentry (since 1.5.2) whether we are looking for a
  390.      *   single entry. Boolean true forces LDAP_SCOPE_BASE search.
  391.      * @return array search results (false on error)
  392.      * @since 1.5.1
  393.      */
  394.     function ldap_search($expression$singleentry false{
  395.         /* Make sure connection is there */
  396.         if(!$this->open()) {
  397.             return false;
  398.         }
  399.  
  400.         $attributes array('dn''description''sn''givenname''cn''mail');
  401.  
  402.         if ($singleentry{
  403.             // ldap_read - search for one single entry
  404.             $sret @ldap_read($this->linkid$expression"objectClass=*",
  405.                                $attributes0$this->maxrows$this->timeout);
  406.         elseif ($this->search_tree{
  407.             // ldap_search - search subtree
  408.             $sret @ldap_search($this->linkid$this->basedn$expression,
  409.                 $attributes0$this->maxrows$this->timeout);
  410.         else {
  411.             // ldap_list - search one level
  412.             $sret @ldap_list($this->linkid$this->basedn$expression,
  413.                 $attributes0$this->maxrows$this->timeout);
  414.         }
  415.  
  416.         /* Return error if search failed */
  417.         if(!$sret{
  418.             // Check for LDAP_NO_SUCH_OBJECT (0x20 or 32) error
  419.             if (ldap_errno($this->linkid)==32{
  420.                 return array();
  421.             else {
  422.                 return $this->set_error($this->ldap_error('ldap_search failed'));
  423.             }
  424.         }
  425.  
  426.         if(@ldap_count_entries($this->linkid$sret<= 0{
  427.             return array();
  428.         }
  429.  
  430.         /* Get results */
  431.         $ret array();
  432.         $returned_rows 0;
  433.         $res @ldap_get_entries($this->linkid$sret);
  434.         for($i $i $res['count'$i++{
  435.             $row $res[$i];
  436.  
  437.             /* Extract data common for all e-mail addresses
  438.              * of an object. Use only the first name */      
  439.             $nickname $this->charset_decode($row['dn']);
  440.  
  441.             /**
  442.              * remove trailing basedn
  443.              * remove whitespaces between RDNs
  444.              * remove leading "cn="
  445.              * which gives nicknames which are shorter while still unique
  446.              */
  447.             $nickname preg_replace('/,\s*/',','trim($nickname));
  448.             $offset strlen($nicknamestrlen($this->basedn);
  449.  
  450.             if($offset && substr($nickname$offset== $this->basedn{
  451.                 $nickname substr($nickname0$offset);
  452.                 if(substr($nickname-1== ",")
  453.                     $nickname substr($nickname0-1);
  454.             }
  455.             if(strncasecmp($nickname"cn="3== 0)
  456.                 $nickname=substr($nickname3);         
  457.  
  458.             if(empty($row['description'][0])) {
  459.                 $label '';
  460.             else {
  461.                 $label $this->charset_decode($row['description'][0]);
  462.             }
  463.  
  464.             if(empty($row['givenname'][0])) {
  465.                 $firstname '';
  466.             else {
  467.                 $firstname $this->charset_decode($row['givenname'][0]);
  468.             }
  469.  
  470.             if(empty($row['sn'][0])) {
  471.                 $surname '';
  472.             else {
  473.                 // remove whitespace in order to handle sn set to empty string
  474.                 $surname trim($this->charset_decode($row['sn'][0]));
  475.             }
  476.  
  477.             $fullname $this->fullname($firstname,$surname);
  478.  
  479.             /* Add one row to result for each e-mail address */
  480.             if(isset($row['mail']['count'])) {
  481.                 for($j $j $row['mail']['count'$j++{
  482.                     array_push($retarray('nickname'  => $nickname,
  483.                    'name'      => $fullname,
  484.                    'firstname' => $firstname,
  485.                    'lastname'  => $surname,
  486.                    'email'     => $row['mail'][$j],
  487.                    'label'     => $label,
  488.                    'backend'   => $this->bnum,
  489.                    'source'    => &$this->sname));
  490.  
  491.                     // Limit number of hits
  492.