3 // Pear DB LDAP - Database independent query interface definition
4 // for PHP's LDAP extension.
6 // Copyright (C) 2002 Ludovico Magnocavallo <ludo@sumatrasolutions.com>
8 // This library is free software; you can redistribute it and/or
9 // modify it under the terms of the GNU Lesser General Public
10 // License as published by the Free Software Foundation; either
11 // version 2.1 of the License, or (at your option) any later version.
13 // This library is distributed in the hope that it will be useful,
14 // but WITHOUT ANY WARRANTY; without even the implied warranty of
15 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16 // Lesser General Public License for more details.
18 // You should have received a copy of the GNU Lesser General Public
19 // License along with this library; if not, write to the Free Software
20 // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
23 // - Piotr Roszatycki <Piotr_Roszatycki@netia.net.pl>
24 // DB_ldap::base() method, support for LDAP sequences, various fixes
28 // Based on DB 1.3 from the pear.php.net repository.
29 // The only modifications made have been modification of the include paths.
32 // rcs_id('From Pear CVS: Id: ldap.php,v 1.9 2002/02/11 12:59:37 mj Exp');
34 require_once 'DB/common.php';
35 define("DB_ERROR_BIND_FAILED", -26);
36 define("DB_ERROR_UNKNOWN_LDAP_ACTION", -27);
41 * LDAP_result extends DB_result to provide specific LDAP
45 * @author Ludovico Magnocavallo <ludo@sumatrasolutions.com>
49 class LDAP_result extends DB_result
55 * data returned from ldap_entries()
60 * result rows as hash of records
63 var $_recordset = null;
65 * current record as hash
74 * class constructor, calls DB_result constructor
75 * @param ref $dbh reference to the db instance
76 * @param resource $result ldap command result
78 function LDAP_result(&$dbh, $result)
80 $this->DB_result($dbh, $result);
84 * fetch rows of data into $this->_recordset
86 * called once as soon as something needs to be returned
88 * @param resource $result ldap command result
89 * @return boolean true
92 if ($this->_recordset === null) {
93 // begin processing result into recordset
94 $this->_entries = ldap_get_entries($this->dbh->connection, $this->result);
95 $this->row_counter = $this->_entries['count'];
97 $rs_template = array();
98 if (count($this->dbh->attributes) > 0) {
99 reset($this->dbh->attributes);
100 while (list($a_index, $a_name) = each($this->dbh->attributes)) $rs_template[$a_name] = '';
102 while (list($entry_idx, $entry) = each($this->_entries)) {
103 // begin first loop, iterate through entries
104 if (!empty($this->dbh->limit_from) && ($i < $this->dbh->limit_from)) continue;
105 if (!empty($this->dbh->limit_count) && ($i > $this->dbh->limit_count)) break;
107 if (!is_array($entry)) continue;
108 while (list($attr, $attr_values) = each($entry)) {
109 // begin second loop, iterate through attributes
110 if (is_int($attr) || $attr == 'count') continue;
111 if (is_string($attr_values)) $rs[$attr] = $attr_values;
114 while (list($value_idx, $attr_value) = each($attr_values)) {
115 // begin third loop, iterate through attribute values
116 if (!is_int($value_idx)) continue;
117 if (empty($value)) $value = $attr_value;
119 if (is_array($value)) $value[] = $attr_value;
120 else $value = array($value, $attr_value);
122 // else $value .= "\n$attr_value";
130 $this->_recordset[$entry_idx] = $rs;
134 $this->_entries = null;
135 if (!is_array($this->_recordset))
136 $this->_recordset = array();
137 if (!empty($this->dbh->sorting)) {
138 $sorting_method = (!empty($this->dbh->sorting_method) ? $this->dbh->sorting_method : 'cmp');
139 uksort($this->_recordset, array(&$this, $sorting_method));
141 reset($this->_recordset);
142 // end processing result into recordset
149 * Fetch and return a row of data (it uses driver->fetchInto for that)
150 * @param int $fetchmode format of fetched row
151 * @param int $rownum the row number to fetch
153 * @return array a row of data, NULL on no more rows or PEAR_Error on error
157 function &fetchRow($fetchmode = DB_FETCHMODE_DEFAULT, $rownum=null)
160 if (count($this->_recordset) == 0) return null;
161 if ($this->_record !== null) $this->_record = next($this->_recordset);
162 else $this->_record = current($this->_recordset);
163 $row = $this->_record;
169 * Fetch a row of data into an existing variable.
171 * @param mixed $arr reference to data containing the row
172 * @param integer $fetchmode format of fetched row
173 * @param integer $rownum the row number to fetch
175 * @return mixed DB_OK on success, NULL on no more rows or
176 * a DB_Error object on error
181 function fetchInto(&$ar, $fetchmode = DB_FETCHMODE_DEFAULT, $rownum = null)
184 if ($this->_record !== null) $this->_record = next($this->_recordset);
185 else $this->_record = current($this->_recordset);
186 $ar = $this->_record;
196 * returns a hash of all records, basically returning
197 * a copy of $this->_recordset
198 * @param integer $fetchmode format of fetched row
199 * @param integer $rownum the row number to fetch (not used, here for interface compatibility)
201 * @return mixed DB_OK on success, NULL on no more rows or
202 * a DB_Error object on error
206 function fetchAll($fetchmode = DB_FETCHMODE_DEFAULT, $rownum = null)
209 return($this->_recordset);
213 * Get the the number of columns in a result set.
215 * @return int the number of columns, or a DB error
219 function numCols($result)
222 return(count(array_keys($this->_record)));
227 return(strcmp(strtolower($this->_recordset[$a][$this->dbh->sorting]), strtolower($this->_recordset[$b][$this->dbh->sorting])));
231 * Get the number of rows in a result set.
233 * @return int the number of rows, or a DB error
240 return $this->row_counter;
244 * Get the next result if a batch of queries was executed.
246 * @return bool true if a new result is available or false if not.
250 function nextResult()
252 return $this->dbh->nextResult($this->result);
256 * Frees the resources allocated for this result set.
257 * @return int error code
263 $this->_recordset = null;
264 $this->_record = null;
265 ldap_free_result($this->result);
266 $this->result = null;
273 function tableInfo($mode = null)
275 return $this->dbh->tableInfo($this->result, $mode);
279 * returns the actual rows number
282 function getRowCounter()
285 return $this->row_counter;
290 * LDAP DB interface class
292 * LDAP extends DB_common to provide DB compliant
293 * access to LDAP servers
296 * @author Ludovico Magnocavallo <ludo@sumatrasolutions.com>
300 class DB_ldap extends DB_common
320 * array of LDAP actions that only manipulate data
321 * returning a true/false value
324 var $manip = array('add', 'compare', 'delete', 'modify', 'mod_add', 'mod_del', 'mod_replace', 'rename');
326 * store the real LDAP action to perform
327 * (ie PHP ldap function to call) for a query
332 * store optional parameters passed
333 * to the real LDAP action
336 var $q_params = array();
341 * Constructor, calls DB_common constructor
343 * @see DB_common::DB_common()
348 $this->phptype = 'ldap';
349 $this->dbsyntax = 'ldap';
350 $this->features = array(
353 'transactions' => false,
359 * Connect and bind to LDAP server with either anonymous or authenticated bind depending on dsn info
361 * @param array $dsninfo dsn info as passed by DB::connect()
362 * @param boolean $persistent kept for interface compatibility
363 * @return DB_OK if successfully connected. A DB error code is returned on failure.
365 function connect($dsninfo, $persistent = false)
367 if (!DB::assertExtension('ldap'))
368 return $this->raiseError(DB_ERROR_EXTENSION_NOT_FOUND);
370 $this->dsn = $dsninfo;
371 $user = $dsninfo['username'];
372 $pw = $dsninfo['password'];
373 $host = $dsninfo['hostspec'];
374 $this->base = $dsninfo['database'];
377 $conn = ldap_connect($host);
379 return $this->raiseError("unknown host $host");
382 return $this->raiseError(DB_ERROR_CONNECT_FAILED);
385 $bind = ldap_bind($conn, "${user}," . $this->base, $pw);
387 $bind = ldap_bind($conn);
390 return $this->raiseError(DB_ERROR_BIND_FAILED);
392 $this->connection = $conn;
397 * Unbinds from LDAP server
399 * @return int ldap_unbind() return value
401 function disconnect()
403 $ret = @ldap_unbind($this->connection);
404 $this->connection = null;
410 * Performs a request against the LDAP server
412 * The type of request (and the corresponding PHP ldap function called)
413 * depend on two additional parameters, added in respect to the
414 * DB_common interface.
416 * @param string $filter text of the request to send to the LDAP server
417 * @param string $action type of request to perform, defaults to search (ldap_search())
418 * @param array $params array of additional parameters to pass to the PHP ldap function requested
419 * @return result from ldap function or DB Error object if no result
421 function simpleQuery($filter, $action = null, $params = null)
423 if ($action === null) {
424 $action = (!empty($this->q_action) ? $this->q_action : 'search');
426 if ($params === null) {
427 $params = (count($this->q_params) > 0 ? $this->q_params : array());
429 if (!$this->isManip($action)) {
430 $base = $this->q_base ? $this->q_base : $this->base;
431 $attributes = array();
435 $deref = LDAP_DEREF_NEVER;
437 $sorting_method = '';
439 while (list($k, $v) = each($params)) {
440 if (isset(${$k})) ${$k} = $v;
442 $this->sorting = $sorting;
443 $this->sorting_method = $sorting_method;
444 $this->attributes = $attributes;
445 if ($action == 'search')
446 $result = @ldap_search($this->connection, $base, $filter, $attributes, $attrsonly, $sizelimit, $timelimit, $deref);
447 else if ($action == 'list')
448 $result = @ldap_list($this->connection, $base, $filter, $attributes, $attrsonly, $sizelimit, $timelimit, $deref);
449 else if ($action == 'read')
450 $result = @ldap_read($this->connection, $base, $filter, $attributes, $attrsonly, $sizelimit, $timelimit, $deref);
452 return $this->raiseError(DB_ERROR_UNKNOWN_LDAP_ACTION);
454 return $this->raiseError();
457 # If first argument is an array, it contains the entry with DN.
458 if (is_array($filter)) {
460 $filter = $entry["dn"];
469 $deleteoldrdn = false;
471 while (list($k, $v) = each($params)) {
472 if (isset(${$k})) ${$k} = $v;
474 if ($action == 'add')
475 $result = @ldap_add($this->connection, $filter, $entry);
476 else if ($action == 'compare')
477 $result = @ldap_add($this->connection, $filter, $attribute, $value);
478 else if ($action == 'delete')
479 $result = @ldap_delete($this->connection, $filter);
480 else if ($action == 'modify')
481 $result = @ldap_modify($this->connection, $filter, $entry);
482 else if ($action == 'mod_add')
483 $result = @ldap_mod_add($this->connection, $filter, $entry);
484 else if ($action == 'mod_del')
485 $result = @ldap_mod_del($this->connection, $filter, $entry);
486 else if ($action == 'mod_replace')
487 $result = @ldap_mod_replace($this->connection, $filter, $entry);
488 else if ($action == 'rename')
489 $result = @ldap_rename($this->connection, $filter, $newrdn, $newparent, $deleteoldrdn);
491 return $this->raiseError(DB_ERROR_UNKNOWN_LDAP_ACTION);
493 return $this->raiseError();
501 * Executes a query performing variables substitution in the query text
503 * @param string $stmt text of the request to send to the LDAP server
504 * @param array $data query variables values to substitute
505 * @param string $action type of request to perform, defaults to search (ldap_search())
506 * @param array $params array of additional parameters to pass to the PHP ldap function requested
507 * @return LDAP_result object or DB Error object if no result
508 * @see DB_common::executeEmulateQuery $this->simpleQuery()
510 function execute($stmt, $data = false, $action = 'search', $params = array())
512 $this->q_action = $action;
513 $this->q_params = $params;
514 $realquery = $this->executeEmulateQuery($stmt, $data);
515 if (DB::isError($realquery)) {
518 $result = $this->simpleQuery($realquery);
519 if (DB::isError($result) || $result === DB_OK) {
522 return new LDAP_result($this, $result);
527 * Executes multiple queries performing variables substitution for each query
529 * @param string $stmt text of the request to send to the LDAP server
530 * @param array $data query variables values to substitute
531 * @param string $action type of request to perform, defaults to search (ldap_search())
532 * @param array $params array of additional parameters to pass to the PHP ldap function requested
533 * @return LDAP_result object or DB Error object if no result
534 * @see DB_common::executeMultiple
536 function executeMultiple($stmt, &$data, $action = 'search', $params = array())
538 $this->q_action = $action;
539 $this->q_params = $params;
540 return(parent::executeMultiple($stmt, $data));
544 * Executes a query substituting variables if any are present
546 * @param string $query text of the request to send to the LDAP server
547 * @param array $data query variables values to substitute
548 * @param string $action type of request to perform, defaults to search (ldap_search())
549 * @param array $params array of additional parameters to pass to the PHP ldap function requested
550 * @return LDAP_result object or DB Error object if no result
551 * @see DB_common::prepare() $this->execute()$this->simpleQuery()
553 function &query($query, $data = array(), $action = 'search', $params = array()) {
554 $this->q_action = $action;
555 $this->q_params = $params;
556 if (sizeof($data) > 0) {
557 $sth = $this->prepare($query);
558 if (DB::isError($sth)) {
561 return $this->execute($sth, $data);
563 $result = $this->simpleQuery($query);
564 if (DB::isError($result) || $result === DB_OK) {
567 return new LDAP_result($this, $result);
573 * Modifies a query to return only a set of rows, stores $from and $count for LDAP_result
575 * @param string $query text of the request to send to the LDAP server
576 * @param int $from record position from which to start returning data
577 * @param int $count number of records to return
578 * @return modified query text (no modifications are made, see above)
580 function modifyLimitQuery($query, $from, $count)
582 $this->limit_from = $from;
583 $this->limit_count = $count;
588 * Executes a query returning only a specified number of rows
590 * This method only saves the $from and $count parameters for LDAP_result
591 * where the actual records processing takes place
593 * @param string $query text of the request to send to the LDAP server
594 * @param int $from record position from which to start returning data
595 * @param int $count number of records to return
596 * @param string $action type of request to perform, defaults to search (ldap_search())
597 * @param array $params array of additional parameters to pass to the PHP ldap function requested
598 * @return LDAP_result object or DB Error object if no result
600 function limitQuery($query, $from, $count, $action = 'search', $params = array())
602 $query = $this->modifyLimitQuery($query, $from, $count);
603 $this->q_action = $action;
604 $this->q_params = $params;
605 return $this->query($query, $action, $params);
609 * Fetch the first column of the first row of data returned from
610 * a query. Takes care of doing the query and freeing the results
613 * @param $query the SQL query
614 * @param $data if supplied, prepare/execute will be used
615 * with this array as execute parameters
616 * @param string $action type of request to perform, defaults to search (ldap_search())
617 * @param array $params array of additional parameters to pass to the PHP ldap function requested
619 * @see DB_common::getOne()
622 function &getOne($query, $data = array(), $action = 'search', $params = array())
624 $this->q_action = $action;
625 $this->q_params = $params;
626 return(parent::getOne($query, $data));
630 * Fetch the first row of data returned from a query. Takes care
631 * of doing the query and freeing the results when finished.
633 * @param $query the SQL query
634 * @param $fetchmode the fetch mode to use
635 * @param $data array if supplied, prepare/execute will be used
636 * with this array as execute parameters
637 * @param string $action type of request to perform, defaults to search (ldap_search())
638 * @param array $params array of additional parameters to pass to the PHP ldap function requested
640 * @return array the first row of results as an array indexed from
641 * 0, or a DB error code.
642 * @see DB_common::getRow()
645 function &getRow($query,
647 $fetchmode = DB_FETCHMODE_DEFAULT,
648 $action = 'search', $params = array())
650 $this->q_action = $action;
651 $this->q_params = $params;
652 return(parent::getRow($query, $data, $fetchmode));
656 * Fetch the first column of data returned from a query. Takes care
657 * of doing the query and freeing the results when finished.
659 * @param $query the SQL query
660 * @param $col which column to return (integer [column number,
661 * starting at 0] or string [column name])
662 * @param $data array if supplied, prepare/execute will be used
663 * with this array as execute parameters
664 * @param string $action type of request to perform, defaults to search (ldap_search())
665 * @param array $params array of additional parameters to pass to the PHP ldap function requested
667 * @return array an indexed array with the data from the first
668 * row at index 0, or a DB error code.
669 * @see DB_common::getCol()
672 function &getCol($query, $col = 0, $data = array(), $action = 'search', $params = array())
674 $this->q_action = $action;
675 $this->q_params = $params;
676 return(parent::getCol($query, $col, $data));
680 * Calls DB_common::getAssoc()
682 * @param $query the SQL query
683 * @param $force_array (optional) used only when the query returns
684 * exactly two columns. If true, the values of the returned array
685 * will be one-element arrays instead of scalars.
686 * starting at 0] or string [column name])
687 * @param array $data if supplied, prepare/execute will be used
688 * with this array as execute parameters
689 * @param $fetchmode the fetch mode to use
690 * @param boolean $group see DB_Common::getAssoc()
691 * @param string $action type of request to perform, defaults to search (ldap_search())
692 * @param array $params array of additional parameters to pass to the PHP ldap function requested
694 * @return array an indexed array with the data from the first
695 * row at index 0, or a DB error code.
696 * @see DB_common::getAssoc()
699 function &getAssoc($query, $force_array = false, $data = array(),
700 $fetchmode = DB_FETCHMODE_ORDERED, $group = false,
701 $action = 'search', $params = array())
703 $this->q_action = $action;
704 $this->q_params = $params;
705 return(parent::getAssoc($query, $force_array, $data, $fetchmode, $group));
709 * Fetch all the rows returned from a query.
711 * @param $query the SQL query
712 * @param array $data if supplied, prepare/execute will be used
713 * with this array as execute parameters
714 * @param $fetchmode the fetch mode to use
715 * @param string $action type of request to perform, defaults to search (ldap_search())
716 * @param array $params array of additional parameters to pass to the PHP ldap function requested
718 * @return array an nested array, or a DB error
719 * @see DB_common::getAll()
721 function &getAll($query,
723 $fetchmode = DB_FETCHMODE_DEFAULT,
724 $action = 'search', $params = array())
726 $this->q_action = $action;
727 $this->q_params = $params;
728 return(parent::getAll($query, $data, $fetchmode));
731 function numRows($result)
733 return $result->numRows();
738 return $this->raiseError(DB_ERROR_NOT_CAPABLE);
741 function getListOf($type)
743 return $this->raiseError(DB_ERROR_NOT_CAPABLE);
746 function isManip($action)
748 return(in_array($action, $this->manip));
751 function freeResult()
756 function freeQuery($query = '')
758 $this->q_action = '';
760 $this->q_params = array();
761 $this->attributes = null;
766 function base($base = null)
768 $this->q_base = ($base !== null) ? $base : null;
773 * Get the next value in a sequence.
775 * LDAP provides transactions for only one entry and we need to
776 * prevent race condition. If unique value before and after modify
777 * aren't equal then wait and try again.
779 * The name of sequence is LDAP DN of entry.
782 * @param string $seq_name the DN of the sequence
783 * @param bool $ondemand whether to create the sequence on demand
784 * @return a sequence integer, or a DB error
786 function nextId($seq_name, $ondemand = true)
790 // Get the sequence entry
791 $this->base($seq_name);
792 $this->pushErrorHandling(PEAR_ERROR_RETURN);
793 $data = $this->getRow("objectClass=*");
794 $this->popErrorHandling();
796 if (DB::isError($data)) {
797 // DB_ldap doesn't use DB_ERROR_NOT_FOUND
798 if ($ondemand && $repeat == 0
799 && $data->getCode() == DB_ERROR) {
800 // Try to create sequence and repeat
802 $data = $this->createSequence($seq_name);
803 if (DB::isError($data)) {
804 return $this->raiseError($data);
808 return $this->raiseError($data);
811 // Increment sequence value
813 // Unique identificator of transaction
814 $seq_unique = mt_rand();
815 $data["uid"] = $seq_unique;
816 // Modify the LDAP entry
817 $this->pushErrorHandling(PEAR_ERROR_RETURN);
818 $data = $this->simpleQuery($data, 'modify');
819 $this->popErrorHandling();
820 if (DB::isError($data)) {
821 return $this->raiseError($data);
823 // Get the entry and check if it contains our unique value
824 $this->base($seq_name);
825 $data = $this->getRow("objectClass=*");
826 if (DB::isError($data)) {
827 return $this->raiseError($data);
829 if ($data["uid"] != $seq_unique) {
830 // It is not our entry. Wait a little time and repeat
839 if (DB::isError($data)) {
846 * Create the sequence
848 * The sequence entry is based on core schema with extensibleObject,
849 * so it should work with any LDAP server which doesn't check schema
850 * or supports extensibleObject object class.
852 * Sequence name have to be DN started with "sn=$seq_id,", i.e.:
854 * $seq_name = "sn=uidNumber,ou=sequences,dc=php,dc=net";
858 * objectClass: extensibleObject
863 * @param string $seq_name the DN of the sequence
864 * @return mixed DB_OK on success or DB error on error
867 function createSequence($seq_name)
869 // Extract $seq_id from DN
870 ereg("^([^,]*),", $seq_name, $regs);
873 // Create the sequence entry
876 objectclass => array("top", "extensibleObject"),
882 // Add the LDAP entry
883 $this->pushErrorHandling(PEAR_ERROR_RETURN);
884 $data = $this->simpleQuery($data, 'add');
885 $this->popErrorHandling();
892 * @param string $seq_name the DN of the sequence
893 * @return mixed DB_OK on success or DB error on error
896 function dropSequence($seq_name)
898 // Delete the sequence entry
902 $this->pushErrorHandling(PEAR_ERROR_RETURN);
903 $data = $this->simpleQuery($data, 'delete');
904 $this->popErrorHandling();