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.
31 // From Pear CVS: Id: ldap.php,v 1.9 2002/02/11 12:59:37 mj Exp
33 require_once 'DB/common.php';
34 define("DB_ERROR_BIND_FAILED", -26);
35 define("DB_ERROR_UNKNOWN_LDAP_ACTION", -27);
40 * LDAP_result extends DB_result to provide specific LDAP
44 * @author Ludovico Magnocavallo <ludo@sumatrasolutions.com>
48 class LDAP_result extends DB_result
54 * data returned from ldap_entries()
59 * result rows as hash of records
62 var $_recordset = null;
64 * current record as hash
73 * class constructor, calls DB_result constructor
74 * @param ref $dbh reference to the db instance
75 * @param resource $result ldap command result
77 function LDAP_result(&$dbh, $result)
79 $this->DB_result($dbh, $result);
83 * fetch rows of data into $this->_recordset
85 * called once as soon as something needs to be returned
87 * @param resource $result ldap command result
88 * @return boolean true
91 if ($this->_recordset === null) {
92 // begin processing result into recordset
93 $this->_entries = ldap_get_entries($this->dbh->connection, $this->result);
94 $this->row_counter = $this->_entries['count'];
96 $rs_template = array();
97 if (count($this->dbh->attributes) > 0) {
98 reset($this->dbh->attributes);
99 while (list($a_index, $a_name) = each($this->dbh->attributes)) $rs_template[$a_name] = '';
101 while (list($entry_idx, $entry) = each($this->_entries)) {
102 // begin first loop, iterate through entries
103 if (!empty($this->dbh->limit_from) && ($i < $this->dbh->limit_from)) continue;
104 if (!empty($this->dbh->limit_count) && ($i > $this->dbh->limit_count)) break;
106 if (!is_array($entry)) continue;
107 while (list($attr, $attr_values) = each($entry)) {
108 // begin second loop, iterate through attributes
109 if (is_int($attr) || $attr == 'count') continue;
110 if (is_string($attr_values)) $rs[$attr] = $attr_values;
113 while (list($value_idx, $attr_value) = each($attr_values)) {
114 // begin third loop, iterate through attribute values
115 if (!is_int($value_idx)) continue;
116 if (empty($value)) $value = $attr_value;
118 if (is_array($value)) $value[] = $attr_value;
119 else $value = array($value, $attr_value);
121 // else $value .= "\n$attr_value";
129 $this->_recordset[$entry_idx] = $rs;
133 $this->_entries = null;
134 if (!is_array($this->_recordset))
135 $this->_recordset = array();
136 if (!empty($this->dbh->sorting)) {
137 $sorting_method = (!empty($this->dbh->sorting_method) ? $this->dbh->sorting_method : 'cmp');
138 uksort($this->_recordset, array(&$this, $sorting_method));
140 reset($this->_recordset);
141 // end processing result into recordset
148 * Fetch and return a row of data (it uses driver->fetchInto for that)
149 * @param int $fetchmode format of fetched row
150 * @param int $rownum the row number to fetch
152 * @return array a row of data, NULL on no more rows or PEAR_Error on error
156 function &fetchRow($fetchmode = DB_FETCHMODE_DEFAULT, $rownum=null)
159 if (count($this->_recordset) == 0) return null;
160 if ($this->_record !== null) $this->_record = next($this->_recordset);
161 else $this->_record = current($this->_recordset);
162 $row = $this->_record;
168 * Fetch a row of data into an existing variable.
170 * @param mixed $arr reference to data containing the row
171 * @param integer $fetchmode format of fetched row
172 * @param integer $rownum the row number to fetch
174 * @return mixed DB_OK on success, NULL on no more rows or
175 * a DB_Error object on error
180 function fetchInto(&$ar, $fetchmode = DB_FETCHMODE_DEFAULT, $rownum = null)
183 if ($this->_record !== null) $this->_record = next($this->_recordset);
184 else $this->_record = current($this->_recordset);
185 $ar = $this->_record;
195 * returns a hash of all records, basically returning
196 * a copy of $this->_recordset
197 * @param integer $fetchmode format of fetched row
198 * @param integer $rownum the row number to fetch (not used, here for interface compatibility)
200 * @return mixed DB_OK on success, NULL on no more rows or
201 * a DB_Error object on error
205 function fetchAll($fetchmode = DB_FETCHMODE_DEFAULT, $rownum = null)
208 return($this->_recordset);
212 * Get the the number of columns in a result set.
214 * @return int the number of columns, or a DB error
218 function numCols($result)
221 return(count(array_keys($this->_record)));
226 return(strcmp(strtolower($this->_recordset[$a][$this->dbh->sorting]), strtolower($this->_recordset[$b][$this->dbh->sorting])));
230 * Get the number of rows in a result set.
232 * @return int the number of rows, or a DB error
239 return $this->row_counter;
243 * Get the next result if a batch of queries was executed.
245 * @return bool true if a new result is available or false if not.
249 function nextResult()
251 return $this->dbh->nextResult($this->result);
255 * Frees the resources allocated for this result set.
256 * @return int error code
262 $this->_recordset = null;
263 $this->_record = null;
264 ldap_free_result($this->result);
265 $this->result = null;
272 function tableInfo($mode = null)
274 return $this->dbh->tableInfo($this->result, $mode);
278 * returns the actual rows number
281 function getRowCounter()
284 return $this->row_counter;
289 * LDAP DB interface class
291 * LDAP extends DB_common to provide DB compliant
292 * access to LDAP servers
295 * @author Ludovico Magnocavallo <ludo@sumatrasolutions.com>
299 class DB_ldap extends DB_common
319 * array of LDAP actions that only manipulate data
320 * returning a true/false value
323 var $manip = array('add', 'compare', 'delete', 'modify', 'mod_add', 'mod_del', 'mod_replace', 'rename');
325 * store the real LDAP action to perform
326 * (ie PHP ldap function to call) for a query
331 * store optional parameters passed
332 * to the real LDAP action
335 var $q_params = array();
340 * Constructor, calls DB_common constructor
342 * @see DB_common::DB_common()
347 $this->phptype = 'ldap';
348 $this->dbsyntax = 'ldap';
349 $this->features = array(
352 'transactions' => false,
358 * Connect and bind to LDAP server with either anonymous or authenticated bind depending on dsn info
360 * @param array $dsninfo dsn info as passed by DB::connect()
361 * @param boolean $persistent kept for interface compatibility
362 * @return DB_OK if successfully connected. A DB error code is returned on failure.
364 function connect($dsninfo, $persistent = false)
366 if (!DB::assertExtension('ldap'))
367 return $this->raiseError(DB_ERROR_EXTENSION_NOT_FOUND);
369 $this->dsn = $dsninfo;
370 $user = $dsninfo['username'];
371 $pw = $dsninfo['password'];
372 $host = $dsninfo['hostspec'];
373 $this->base = $dsninfo['database'];
376 $conn = ldap_connect($host);
378 return $this->raiseError("unknown host $host");
381 return $this->raiseError(DB_ERROR_CONNECT_FAILED);
384 $bind = ldap_bind($conn, "${user}," . $this->base, $pw);
386 $bind = ldap_bind($conn);
389 return $this->raiseError(DB_ERROR_BIND_FAILED);
391 $this->connection = $conn;
396 * Unbinds from LDAP server
398 * @return int ldap_unbind() return value
400 function disconnect()
402 $ret = @ldap_unbind($this->connection);
403 $this->connection = null;
409 * Performs a request against the LDAP server
411 * The type of request (and the corresponding PHP ldap function called)
412 * depend on two additional parameters, added in respect to the
413 * DB_common interface.
415 * @param string $filter text of the request to send to the LDAP server
416 * @param string $action type of request to perform, defaults to search (ldap_search())
417 * @param array $params array of additional parameters to pass to the PHP ldap function requested
418 * @return result from ldap function or DB Error object if no result
420 function simpleQuery($filter, $action = null, $params = null)
422 if ($action === null) {
423 $action = (!empty($this->q_action) ? $this->q_action : 'search');
425 if ($params === null) {
426 $params = (count($this->q_params) > 0 ? $this->q_params : array());
428 if (!$this->isManip($action)) {
429 $base = $this->q_base ? $this->q_base : $this->base;
430 $attributes = array();
434 $deref = LDAP_DEREF_NEVER;
436 $sorting_method = '';
438 while (list($k, $v) = each($params)) {
439 if (isset(${$k})) ${$k} = $v;
441 $this->sorting = $sorting;
442 $this->sorting_method = $sorting_method;
443 $this->attributes = $attributes;
444 if ($action == 'search')
445 $result = @ldap_search($this->connection, $base, $filter, $attributes, $attrsonly, $sizelimit, $timelimit, $deref);
446 else if ($action == 'list')
447 $result = @ldap_list($this->connection, $base, $filter, $attributes, $attrsonly, $sizelimit, $timelimit, $deref);
448 else if ($action == 'read')
449 $result = @ldap_read($this->connection, $base, $filter, $attributes, $attrsonly, $sizelimit, $timelimit, $deref);
451 return $this->raiseError(DB_ERROR_UNKNOWN_LDAP_ACTION);
453 return $this->raiseError();
456 # If first argument is an array, it contains the entry with DN.
457 if (is_array($filter)) {
459 $filter = $entry["dn"];
468 $deleteoldrdn = false;
470 while (list($k, $v) = each($params)) {
471 if (isset(${$k})) ${$k} = $v;
473 if ($action == 'add')
474 $result = @ldap_add($this->connection, $filter, $entry);
475 else if ($action == 'compare')
476 $result = @ldap_add($this->connection, $filter, $attribute, $value);
477 else if ($action == 'delete')
478 $result = @ldap_delete($this->connection, $filter);
479 else if ($action == 'modify')
480 $result = @ldap_modify($this->connection, $filter, $entry);
481 else if ($action == 'mod_add')
482 $result = @ldap_mod_add($this->connection, $filter, $entry);
483 else if ($action == 'mod_del')
484 $result = @ldap_mod_del($this->connection, $filter, $entry);
485 else if ($action == 'mod_replace')
486 $result = @ldap_mod_replace($this->connection, $filter, $entry);
487 else if ($action == 'rename')
488 $result = @ldap_rename($this->connection, $filter, $newrdn, $newparent, $deleteoldrdn);
490 return $this->raiseError(DB_ERROR_UNKNOWN_LDAP_ACTION);
492 return $this->raiseError();
500 * Executes a query performing variables substitution in the query text
502 * @param string $stmt text of the request to send to the LDAP server
503 * @param array $data query variables values to substitute
504 * @param string $action type of request to perform, defaults to search (ldap_search())
505 * @param array $params array of additional parameters to pass to the PHP ldap function requested
506 * @return LDAP_result object or DB Error object if no result
507 * @see DB_common::executeEmulateQuery $this->simpleQuery()
509 function execute($stmt, $data = false, $action = 'search', $params = array())
511 $this->q_action = $action;
512 $this->q_params = $params;
513 $realquery = $this->executeEmulateQuery($stmt, $data);
514 if (DB::isError($realquery)) {
517 $result = $this->simpleQuery($realquery);
518 if (DB::isError($result) || $result === DB_OK) {
521 return new LDAP_result($this, $result);
526 * Executes multiple queries performing variables substitution for each query
528 * @param string $stmt text of the request to send to the LDAP server
529 * @param array $data query variables values to substitute
530 * @param string $action type of request to perform, defaults to search (ldap_search())
531 * @param array $params array of additional parameters to pass to the PHP ldap function requested
532 * @return LDAP_result object or DB Error object if no result
533 * @see DB_common::executeMultiple
535 function executeMultiple($stmt, &$data, $action = 'search', $params = array())
537 $this->q_action = $action;
538 $this->q_params = $params;
539 return(parent::executeMultiple($stmt, $data));
543 * Executes a query substituting variables if any are present
545 * @param string $query text of the request to send to the LDAP server
546 * @param array $data query variables values to substitute
547 * @param string $action type of request to perform, defaults to search (ldap_search())
548 * @param array $params array of additional parameters to pass to the PHP ldap function requested
549 * @return LDAP_result object or DB Error object if no result
550 * @see DB_common::prepare() $this->execute()$this->simpleQuery()
552 function &query($query, $data = array(), $action = 'search', $params = array()) {
553 $this->q_action = $action;
554 $this->q_params = $params;
555 if (sizeof($data) > 0) {
556 $sth = $this->prepare($query);
557 if (DB::isError($sth)) {
560 return $this->execute($sth, $data);
562 $result = $this->simpleQuery($query);
563 if (DB::isError($result) || $result === DB_OK) {
566 return new LDAP_result($this, $result);
572 * Modifies a query to return only a set of rows, stores $from and $count for LDAP_result
574 * @param string $query text of the request to send to the LDAP server
575 * @param int $from record position from which to start returning data
576 * @param int $count number of records to return
577 * @return modified query text (no modifications are made, see above)
579 function modifyLimitQuery($query, $from, $count)
581 $this->limit_from = $from;
582 $this->limit_count = $count;
587 * Executes a query returning only a specified number of rows
589 * This method only saves the $from and $count parameters for LDAP_result
590 * where the actual records processing takes place
592 * @param string $query text of the request to send to the LDAP server
593 * @param int $from record position from which to start returning data
594 * @param int $count number of records to return
595 * @param string $action type of request to perform, defaults to search (ldap_search())
596 * @param array $params array of additional parameters to pass to the PHP ldap function requested
597 * @return LDAP_result object or DB Error object if no result
599 function limitQuery($query, $from, $count, $action = 'search', $params = array())
601 $query = $this->modifyLimitQuery($query, $from, $count);
602 $this->q_action = $action;
603 $this->q_params = $params;
604 return $this->query($query, $action, $params);
608 * Fetch the first column of the first row of data returned from
609 * a query. Takes care of doing the query and freeing the results
612 * @param $query the SQL query
613 * @param $data if supplied, prepare/execute will be used
614 * with this array as execute parameters
615 * @param string $action type of request to perform, defaults to search (ldap_search())
616 * @param array $params array of additional parameters to pass to the PHP ldap function requested
618 * @see DB_common::getOne()
621 function &getOne($query, $data = array(), $action = 'search', $params = array())
623 $this->q_action = $action;
624 $this->q_params = $params;
625 return(parent::getOne($query, $data));
629 * Fetch the first row of data returned from a query. Takes care
630 * of doing the query and freeing the results when finished.
632 * @param $query the SQL query
633 * @param $fetchmode the fetch mode to use
634 * @param $data array if supplied, prepare/execute will be used
635 * with this array as execute parameters
636 * @param string $action type of request to perform, defaults to search (ldap_search())
637 * @param array $params array of additional parameters to pass to the PHP ldap function requested
639 * @return array the first row of results as an array indexed from
640 * 0, or a DB error code.
641 * @see DB_common::getRow()
644 function &getRow($query,
646 $fetchmode = DB_FETCHMODE_DEFAULT,
647 $action = 'search', $params = array())
649 $this->q_action = $action;
650 $this->q_params = $params;
651 return(parent::getRow($query, $data, $fetchmode));
655 * Fetch the first column of data returned from a query. Takes care
656 * of doing the query and freeing the results when finished.
658 * @param $query the SQL query
659 * @param $col which column to return (integer [column number,
660 * starting at 0] or string [column name])
661 * @param $data array if supplied, prepare/execute will be used
662 * with this array as execute parameters
663 * @param string $action type of request to perform, defaults to search (ldap_search())
664 * @param array $params array of additional parameters to pass to the PHP ldap function requested
666 * @return array an indexed array with the data from the first
667 * row at index 0, or a DB error code.
668 * @see DB_common::getCol()
671 function &getCol($query, $col = 0, $data = array(), $action = 'search', $params = array())
673 $this->q_action = $action;
674 $this->q_params = $params;
675 return(parent::getCol($query, $col, $data));
679 * Calls DB_common::getAssoc()
681 * @param $query the SQL query
682 * @param $force_array (optional) used only when the query returns
683 * exactly two columns. If true, the values of the returned array
684 * will be one-element arrays instead of scalars.
685 * starting at 0] or string [column name])
686 * @param array $data if supplied, prepare/execute will be used
687 * with this array as execute parameters
688 * @param $fetchmode the fetch mode to use
689 * @param boolean $group see DB_Common::getAssoc()
690 * @param string $action type of request to perform, defaults to search (ldap_search())
691 * @param array $params array of additional parameters to pass to the PHP ldap function requested
693 * @return array an indexed array with the data from the first
694 * row at index 0, or a DB error code.
695 * @see DB_common::getAssoc()
698 function &getAssoc($query, $force_array = false, $data = array(),
699 $fetchmode = DB_FETCHMODE_ORDERED, $group = false,
700 $action = 'search', $params = array())
702 $this->q_action = $action;
703 $this->q_params = $params;
704 return(parent::getAssoc($query, $force_array, $data, $fetchmode, $group));
708 * Fetch all the rows returned from a query.
710 * @param $query the SQL query
711 * @param array $data if supplied, prepare/execute will be used
712 * with this array as execute parameters
713 * @param $fetchmode the fetch mode to use
714 * @param string $action type of request to perform, defaults to search (ldap_search())
715 * @param array $params array of additional parameters to pass to the PHP ldap function requested
717 * @return array an nested array, or a DB error
718 * @see DB_common::getAll()
720 function &getAll($query,
722 $fetchmode = DB_FETCHMODE_DEFAULT,
723 $action = 'search', $params = array())
725 $this->q_action = $action;
726 $this->q_params = $params;
727 return(parent::getAll($query, $data, $fetchmode));
730 function numRows($result)
732 return $result->numRows();
737 return $this->raiseError(DB_ERROR_NOT_CAPABLE);
740 function getListOf($type)
742 return $this->raiseError(DB_ERROR_NOT_CAPABLE);
745 function isManip($action)
747 return(in_array($action, $this->manip));
750 function freeResult()
755 function freeQuery($query = '')
757 $this->q_action = '';
759 $this->q_params = array();
760 $this->attributes = null;
765 function base($base = null)
767 $this->q_base = ($base !== null) ? $base : null;
772 * Get the next value in a sequence.
774 * LDAP provides transactions for only one entry and we need to
775 * prevent race condition. If unique value before and after modify
776 * aren't equal then wait and try again.
778 * The name of sequence is LDAP DN of entry.
781 * @param string $seq_name the DN of the sequence
782 * @param bool $ondemand whether to create the sequence on demand
783 * @return a sequence integer, or a DB error
785 function nextId($seq_name, $ondemand = true)
789 // Get the sequence entry
790 $this->base($seq_name);
791 $this->pushErrorHandling(PEAR_ERROR_RETURN);
792 $data = $this->getRow("objectClass=*");
793 $this->popErrorHandling();
795 if (DB::isError($data)) {
796 // DB_ldap doesn't use DB_ERROR_NOT_FOUND
797 if ($ondemand && $repeat == 0
798 && $data->getCode() == DB_ERROR) {
799 // Try to create sequence and repeat
801 $data = $this->createSequence($seq_name);
802 if (DB::isError($data)) {
803 return $this->raiseError($data);
807 return $this->raiseError($data);
810 // Increment sequence value
812 // Unique identificator of transaction
813 $seq_unique = mt_rand();
814 $data["uid"] = $seq_unique;
815 // Modify the LDAP entry
816 $this->pushErrorHandling(PEAR_ERROR_RETURN);
817 $data = $this->simpleQuery($data, 'modify');
818 $this->popErrorHandling();
819 if (DB::isError($data)) {
820 return $this->raiseError($data);
822 // Get the entry and check if it contains our unique value
823 $this->base($seq_name);
824 $data = $this->getRow("objectClass=*");
825 if (DB::isError($data)) {
826 return $this->raiseError($data);
828 if ($data["uid"] != $seq_unique) {
829 // It is not our entry. Wait a little time and repeat
838 if (DB::isError($data)) {
845 * Create the sequence
847 * The sequence entry is based on core schema with extensibleObject,
848 * so it should work with any LDAP server which doesn't check schema
849 * or supports extensibleObject object class.
851 * Sequence name have to be DN started with "sn=$seq_id,", i.e.:
853 * $seq_name = "sn=uidNumber,ou=sequences,dc=php,dc=net";
857 * objectClass: extensibleObject
862 * @param string $seq_name the DN of the sequence
863 * @return mixed DB_OK on success or DB error on error
866 function createSequence($seq_name)
868 // Extract $seq_id from DN
869 ereg("^([^,]*),", $seq_name, $regs);
872 // Create the sequence entry
875 objectclass => array("top", "extensibleObject"),
881 // Add the LDAP entry
882 $this->pushErrorHandling(PEAR_ERROR_RETURN);
883 $data = $this->simpleQuery($data, 'add');
884 $this->popErrorHandling();
891 * @param string $seq_name the DN of the sequence
892 * @return mixed DB_OK on success or DB error on error
895 function dropSequence($seq_name)
897 // Delete the sequence entry
901 $this->pushErrorHandling(PEAR_ERROR_RETURN);
902 $data = $this->simpleQuery($data, 'delete');
903 $this->popErrorHandling();