diff --git a/plugins/LdapCommon/extlib/Net/LDAP2.php b/plugins/LdapCommon/extlib/Net/LDAP2.php index 26f5e75600..1ad1cf3345 100644 --- a/plugins/LdapCommon/extlib/Net/LDAP2.php +++ b/plugins/LdapCommon/extlib/Net/LDAP2.php @@ -13,7 +13,7 @@ * @author Benedikt Hallinger * @copyright 2003-2007 Tarjej Huse, Jan Wagner, Del Elson, Benedikt Hallinger * @license http://www.gnu.org/licenses/lgpl-3.0.txt LGPLv3 -* @version SVN: $Id: LDAP2.php 286788 2009-08-04 06:05:49Z beni $ +* @version SVN: $Id: LDAP2.php 332308 2013-12-09 09:15:47Z beni $ * @link http://pear.php.net/package/Net_LDAP2/ */ @@ -39,7 +39,7 @@ define('NET_LDAP2_ERROR', 1000); /** * Net_LDAP2 Version */ -define('NET_LDAP2_VERSION', '2.0.7'); +define('NET_LDAP2_VERSION', '2.1.0'); /** * Net_LDAP2 - manipulate LDAP servers the right way! @@ -612,30 +612,47 @@ class Net_LDAP2 extends PEAR */ public function startTLS() { - // Test to see if the server supports TLS first. - // This is done via testing the extensions offered by the server. - // The OID 1.3.6.1.4.1.1466.20037 tells us, if TLS is supported. + /* Test to see if the server supports TLS first. + This is done via testing the extensions offered by the server. + The OID 1.3.6.1.4.1.1466.20037 tells us, if TLS is supported. + Note, that not all servers allow to feth either the rootDSE or + attributes over an unencrypted channel, so we must ignore errors. */ $rootDSE = $this->rootDse(); if (self::isError($rootDSE)) { - return $this->raiseError("Unable to fetch rootDSE entry ". - "to see if TLS is supoported: ".$rootDSE->getMessage(), $rootDSE->getCode()); - } - - $supported_extensions = $rootDSE->getValue('supportedExtension'); - if (self::isError($supported_extensions)) { - return $this->raiseError("Unable to fetch rootDSE attribute 'supportedExtension' ". - "to see if TLS is supoported: ".$supported_extensions->getMessage(), $supported_extensions->getCode()); - } - - if (in_array('1.3.6.1.4.1.1466.20037', $supported_extensions)) { - if (false === @ldap_start_tls($this->_link)) { - return $this->raiseError("TLS not started: " . - @ldap_error($this->_link), - @ldap_errno($this->_link)); - } - return true; + /* IGNORE this error, because server may refuse fetching the + RootDSE over an unencrypted connection. */ + //return $this->raiseError("Unable to fetch rootDSE entry ". + //"to see if TLS is supoported: ".$rootDSE->getMessage(), $rootDSE->getCode()); } else { - return $this->raiseError("Server reports that it does not support TLS"); + /* Fetch suceeded, see, if the server supports TLS. Again, we + ignore errors, because the server may refuse to return + attributes over unencryted connections. */ + $supported_extensions = $rootDSE->getValue('supportedExtension'); + if (self::isError($supported_extensions)) { + /* IGNORE error, because server may refuse attribute + returning over an unencrypted connection. */ + //return $this->raiseError("Unable to fetch rootDSE attribute 'supportedExtension' ". + //"to see if TLS is supoported: ".$supported_extensions->getMessage(), $supported_extensions->getCode()); + } else { + // fetch succeedet, lets see if the server supports it. + // if not, then drop an error. If supported, then do nothing, + // because then we try to issue TLS afterwards. + if (!in_array('1.3.6.1.4.1.1466.20037', $supported_extensions)) { + return $this->raiseError("Server reports that it does not support TLS."); + } + } + } + + // Try to establish TLS. + if (false === @ldap_start_tls($this->_link)) { + // Starting TLS failed. This may be an error, or because + // the server does not support it but did not enable us to + // detect that above. + return $this->raiseError("TLS could not be started: " . + @ldap_error($this->_link), + @ldap_errno($this->_link)); + } else { + return true; // TLS is started now. } } @@ -728,7 +745,7 @@ class Net_LDAP2 extends PEAR // We have a failure. What type? We may be able to reconnect // and try again. $error_code = @ldap_errno($link); - $error_name = $this->errorMessage($error_code); + $error_name = Net_LDAP2::errorMessage($error_code); if (($error_name === 'LDAP_OPERATIONS_ERROR') && ($this->_config['auto_reconnect'])) { @@ -802,9 +819,9 @@ class Net_LDAP2 extends PEAR // We have a failure. What type? // We may be able to reconnect and try again. $error_code = @ldap_errno($link); - $error_name = $this->errorMessage($error_code); + $error_name = Net_LDAP2::errorMessage($error_code); - if (($this->errorMessage($error_code) === 'LDAP_OPERATIONS_ERROR') && + if ((Net_LDAP2::errorMessage($error_code) === 'LDAP_OPERATIONS_ERROR') && ($this->_config['auto_reconnect'])) { // The server has become disconnected before trying the // operation. We should try again, possibly with a @@ -898,9 +915,9 @@ class Net_LDAP2 extends PEAR // We have a failure. What type? We may be able to reconnect // and try again. $error_code = $msg->getCode(); - $error_name = $this->errorMessage($error_code); + $error_name = Net_LDAP2::errorMessage($error_code); - if (($this->errorMessage($error_code) === 'LDAP_OPERATIONS_ERROR') && + if ((Net_LDAP2::errorMessage($error_code) === 'LDAP_OPERATIONS_ERROR') && ($this->_config['auto_reconnect'])) { // The server has become disconnected before trying the @@ -937,9 +954,9 @@ class Net_LDAP2 extends PEAR // We have a failure. What type? We may be able to reconnect // and try again. $error_code = $msg->getCode(); - $error_name = $this->errorMessage($error_code); + $error_name = Net_LDAP2::errorMessage($error_code); - if (($this->errorMessage($error_code) === 'LDAP_OPERATIONS_ERROR') && + if ((Net_LDAP2::errorMessage($error_code) === 'LDAP_OPERATIONS_ERROR') && ($this->_config['auto_reconnect'])) { // The server has become disconnected before trying the @@ -1078,14 +1095,14 @@ class Net_LDAP2 extends PEAR return $obj = new Net_LDAP2_Search ($search, $this, $attributes); } elseif ($err == 87) { // bad search filter - return $this->raiseError($this->errorMessage($err) . "($filter)", $err); + return $this->raiseError(Net_LDAP2::errorMessage($err) . "($filter)", $err); } elseif (($err == 1) && ($this->_config['auto_reconnect'])) { // Errorcode 1 = LDAP_OPERATIONS_ERROR but we can try a reconnect. $this->_link = false; $this->performReconnect(); } else { $msg = "\nParameters:\nBase: $base\nFilter: $filter\nScope: $scope"; - return $this->raiseError($this->errorMessage($err) . $msg, $err); + return $this->raiseError(Net_LDAP2::errorMessage($err) . $msg, $err); } } else { return $obj = new Net_LDAP2_Search($search, $this, $attributes); @@ -1114,7 +1131,7 @@ class Net_LDAP2 extends PEAR $msg = @ldap_err2str($err); } else { $err = NET_LDAP2_ERROR; - $msg = $this->errorMessage($err); + $msg = Net_LDAP2::errorMessage($err); } return $this->raiseError($msg, $err); } @@ -1146,7 +1163,7 @@ class Net_LDAP2 extends PEAR $msg = @ldap_err2str($err); } else { $err = NET_LDAP2_ERROR; - $msg = $this->errorMessage($err); + $msg = Net_LDAP2::errorMessage($err); } return $this->raiseError($msg, $err); } @@ -1239,30 +1256,21 @@ class Net_LDAP2 extends PEAR return PEAR::raiseError('Parameter $dn is not a string nor an entry object!'); } - // make dn relative to parent - $base = Net_LDAP2_Util::ldap_explode_dn($dn, array('casefold' => 'none', 'reverse' => false, 'onlyvalues' => false)); - if (self::isError($base)) { - return $base; - } - $entry_rdn = array_shift($base); - if (is_array($entry_rdn)) { - // maybe the dn consist of a multivalued RDN, we must build the dn in this case - // because the $entry_rdn is an array! - $filter_dn = Net_LDAP2_Util::canonical_dn($entry_rdn); - } - $base = Net_LDAP2_Util::canonical_dn($base); + // search LDAP for that DN by performing a baselevel search for any + // object. We can only find the DN in question this way, or nothing. + $s_opts = array( + 'scope' => 'base', + 'sizelimit' => 1, + 'attributes' => '1.1' // select no attrs + ); + $search = $this->search($dn, '(objectClass=*)', $s_opts); - $result = @ldap_list($this->_link, $base, $entry_rdn, array(), 1, 1); - if (@ldap_count_entries($this->_link, $result)) { - return true; + if (self::isError($search)) { + return $search; } - if (ldap_errno($this->_link) == 32) { - return false; - } - if (ldap_errno($this->_link) != 0) { - return PEAR::raiseError(ldap_error($this->_link), ldap_errno($this->_link)); - } - return false; + + // retun wehter the DN exists; that is, we found an entry + return ($search->count() == 0)? false : true; } @@ -1400,7 +1408,7 @@ class Net_LDAP2 extends PEAR * * @return string The errorstring for the error. */ - public function errorMessage($errorcode) + public static function errorMessage($errorcode) { $errorMessages = array( 0x00 => "LDAP_SUCCESS", @@ -1629,7 +1637,7 @@ class Net_LDAP2 extends PEAR } /** - * Encodes given attributes to UTF8 if needed by schema + * Encodes given attributes from ISO-8859-1 to UTF-8 if needed by schema * * This function takes attributes in an array and then checks against the schema if they need * UTF8 encoding. If that is so, they will be encoded. An encoded array will be returned and @@ -1650,7 +1658,7 @@ class Net_LDAP2 extends PEAR } /** - * Decodes the given attribute values if needed by schema + * Decodes the given attribute values from UTF-8 to ISO-8859-1 if needed by schema * * $attributes is expected to be an array with keys describing * the attribute names and the values as the value of this attribute: @@ -1668,7 +1676,7 @@ class Net_LDAP2 extends PEAR } /** - * Encodes or decodes attribute values if needed + * Encodes or decodes UTF-8/ISO-8859-1 attribute values if needed by schema * * @param array $attributes Array of attributes * @param array $function Function to apply to attribute values @@ -1701,7 +1709,10 @@ class Net_LDAP2 extends PEAR continue; } - if (false !== strpos($attr['syntax'], '1.3.6.1.4.1.1466.115.121.1.15')) { + // Encoding is needed if this is a DIR_STR. We assume also + // needed encoding in case the schema contains no syntax + // information (he does not need to, see rfc2252, 4.2) + if (!array_key_exists('syntax', $attr) || false !== strpos($attr['syntax'], '1.3.6.1.4.1.1466.115.121.1.15')) { $encode = true; } else { $encode = false; diff --git a/plugins/LdapCommon/extlib/Net/LDAP2/Entry.php b/plugins/LdapCommon/extlib/Net/LDAP2/Entry.php index 66de966780..cdbd70136a 100644 --- a/plugins/LdapCommon/extlib/Net/LDAP2/Entry.php +++ b/plugins/LdapCommon/extlib/Net/LDAP2/Entry.php @@ -12,7 +12,7 @@ * @author Benedikt Hallinger * @copyright 2009 Tarjej Huse, Jan Wagner, Benedikt Hallinger * @license http://www.gnu.org/licenses/lgpl-3.0.txt LGPLv3 -* @version SVN: $Id: Entry.php 286787 2009-08-04 06:03:12Z beni $ +* @version SVN: $Id: Entry.php 332301 2013-12-09 08:17:14Z beni $ * @link http://pear.php.net/package/Net_LDAP2/ */ @@ -20,7 +20,7 @@ * Includes */ require_once 'PEAR.php'; -require_once 'Util.php'; +require_once 'Net/LDAP2/Util.php'; /** * Object representation of a directory entry @@ -309,7 +309,7 @@ class Net_LDAP2_Entry extends PEAR public function dn($dn = null) { if (false == is_null($dn)) { - if (is_null($this->_dn)) { + if (is_null($this->_dn) ) { $this->_dn = $dn; } else { $this->_newdn = $dn; @@ -419,6 +419,7 @@ class Net_LDAP2_Entry extends PEAR * The returned hash has the form * array('attributename' => 'single value', * 'attributename' => array('value1', value2', value3')) + * Only attributes present at the entry will be returned. * * @access public * @return array Hash of all attributes with their values @@ -437,31 +438,59 @@ class Net_LDAP2_Entry extends PEAR * * The first parameter is the name of the attribute * The second parameter influences the way the value is returned: - * 'single': only the first value is returned as string - * 'all': all values including the value count are returned in an - * array + * 'single': only the first value is returned as string + * 'all': all values are returned in an array * 'default': in all other cases an attribute value with a single value is * returned as string, if it has multiple values it is returned - * as an array (without value count) + * as an array * - * @param string $attr Attribute name - * @param string $option Option + * If the attribute is not set at this entry (no value or not defined in + * schema), "false" is returned when $option is 'single', an empty string if + * 'default', and an empty array when 'all'. + * + * You may use Net_LDAP2_Schema->checkAttribute() to see if the attribute + * is defined for the objectClasses of this entry. + * + * @param string $attr Attribute name + * @param string $option Option * * @access public - * @return string|array|PEAR_Error string, array or PEAR_Error + * @return string|array */ public function getValue($attr, $option = null) { $attr = $this->getAttrName($attr); - if (false == array_key_exists($attr, $this->_attributes)) { - return PEAR::raiseError("Unknown attribute ($attr) requested"); - } + // return depending on set $options + if (!array_key_exists($attr, $this->_attributes)) { + // attribute not set + switch ($option) { + case 'single': + $value = false; + break; + case 'all': + $value = array(); + break; + default: + $value = ''; + } - $value = $this->_attributes[$attr]; - - if ($option == "single" || (count($value) == 1 && $option != 'all')) { - $value = array_shift($value); + } else { + // attribute present + switch ($option) { + case 'single': + $value = $this->_attributes[$attr][0]; + break; + case 'all': + $value = $this->_attributes[$attr]; + break; + default: + $value = $this->_attributes[$attr]; + if (count($value) == 1) { + $value = array_shift($value); + } + } + } return $value; @@ -529,6 +558,9 @@ class Net_LDAP2_Entry extends PEAR if (false == is_array($attr)) { return PEAR::raiseError("Parameter must be an array"); } + if ($this->isNew()) { + $this->setAttributes($attr); + } foreach ($attr as $k => $v) { $k = $this->getAttrName($k); if (false == is_array($v)) { @@ -547,11 +579,12 @@ class Net_LDAP2_Entry extends PEAR $this->_attributes[$k] = $v; } // save changes for update() - if (empty($this->_changes["add"][$k])) { + if (!isset($this->_changes["add"][$k])) { $this->_changes["add"][$k] = array(); } $this->_changes["add"][$k] = array_unique(array_merge($this->_changes["add"][$k], $v)); } + $return = true; return $return; } @@ -760,6 +793,14 @@ class Net_LDAP2_Entry extends PEAR $this->_changes['replace'] = array(); $this->_original = $this->_attributes; + // In case the "new" entry was moved after creation, we must + // adjust the internal DNs as the entry was already created + // with the most current DN. + if (false == is_null($this->_newdn)) { + $this->_dn = $this->_newdn; + $this->_newdn = null; + } + $return = true; return $return; } @@ -785,7 +826,8 @@ class Net_LDAP2_Entry extends PEAR $parent = Net_LDAP2_Util::canonical_dn($parent); // rename/move - if (false == @ldap_rename($link, $this->_dn, $child, $parent, true)) { + if (false == @ldap_rename($link, $this->_dn, $child, $parent, false)) { + return PEAR::raiseError("Entry not renamed: " . @ldap_error($link), @ldap_errno($link)); } @@ -795,51 +837,55 @@ class Net_LDAP2_Entry extends PEAR } /* - * Carry out modifications to the entry + * Retrieve a entry that has all attributes we need so that the list of changes to build is created accurately */ + $fullEntry = $ldap->getEntry( $this->dn() ); + if ( Net_LDAP2::isError($fullEntry) ) { + return PEAR::raiseError("Could not retrieve a full set of attributes to reconcile changes with"); + } + $modifications = array(); + // ADD foreach ($this->_changes["add"] as $attr => $value) { - // if attribute exists, add new values - if ($this->exists($attr)) { - if (false === @ldap_mod_add($link, $this->dn(), array($attr => $value))) { - return PEAR::raiseError("Could not add new values to attribute $attr: " . - @ldap_error($link), @ldap_errno($link)); - } - } else { - // new attribute - if (false === @ldap_modify($link, $this->dn(), array($attr => $value))) { - return PEAR::raiseError("Could not add new attribute $attr: " . - @ldap_error($link), @ldap_errno($link)); - } - } - // all went well here, I guess - unset($this->_changes["add"][$attr]); + // if attribute exists, we need to combine old and new values + if ($fullEntry->exists($attr)) { + $currentValue = $fullEntry->getValue($attr, "all"); + $value = array_merge( $currentValue, $value ); + } + + $modifications[$attr] = $value; } // DELETE foreach ($this->_changes["delete"] as $attr => $value) { // In LDAPv3 you need to specify the old values for deleting if (is_null($value) && $ldap->getLDAPVersion() === 3) { - $value = $this->_original[$attr]; + $value = $fullEntry->getValue($attr); } - if (false === @ldap_mod_del($link, $this->dn(), array($attr => $value))) { - return PEAR::raiseError("Could not delete attribute $attr: " . - @ldap_error($link), @ldap_errno($link)); + if (!is_array($value)) { + $value = array($value); } - unset($this->_changes["delete"][$attr]); + + // Find out what is missing from $value and exclude it + $currentValue = isset($modifications[$attr]) ? $modifications[$attr] : $fullEntry->getValue($attr, "all"); + $modifications[$attr] = array_values( array_diff( $currentValue, $value ) ); } // REPLACE foreach ($this->_changes["replace"] as $attr => $value) { - if (false === @ldap_modify($link, $this->dn(), array($attr => $value))) { - return PEAR::raiseError("Could not replace attribute $attr values: " . - @ldap_error($link), @ldap_errno($link)); - } - unset($this->_changes["replace"][$attr]); + $modifications[$attr] = $value; } - // all went well, so _original (server) becomes _attributes (local copy) - $this->_original = $this->_attributes; + // COMMIT + if (false === @ldap_modify($link, $this->dn(), $modifications)) { + return PEAR::raiseError("Could not modify the entry: " . @ldap_error($link), @ldap_errno($link)); + } + + // all went well, so _original (server) becomes _attributes (local copy), reset _changes too... + $this->_changes['add'] = array(); + $this->_changes['delete'] = array(); + $this->_changes['replace'] = array(); + $this->_original = $this->_attributes; $return = true; return $return; @@ -964,11 +1010,6 @@ class Net_LDAP2_Entry extends PEAR // fetch attribute values $attr = $this->getValue($attr_name, 'all'); - if (Net_LDAP2::isError($attr)) { - return $attr; - } else { - unset($attr['count']); - } // perform preg_match() on all values $match = false; diff --git a/plugins/LdapCommon/extlib/Net/LDAP2/Filter.php b/plugins/LdapCommon/extlib/Net/LDAP2/Filter.php index 0723edab2b..557fc892e3 100644 --- a/plugins/LdapCommon/extlib/Net/LDAP2/Filter.php +++ b/plugins/LdapCommon/extlib/Net/LDAP2/Filter.php @@ -10,7 +10,7 @@ * @author Benedikt Hallinger * @copyright 2009 Benedikt Hallinger * @license http://www.gnu.org/licenses/lgpl-3.0.txt LGPLv3 -* @version SVN: $Id: Filter.php 289978 2009-10-27 09:56:41Z beni $ +* @version SVN: $Id: Filter.php 332305 2013-12-09 08:51:41Z beni $ * @link http://pear.php.net/package/Net_LDAP2/ */ @@ -18,7 +18,8 @@ * Includes */ require_once 'PEAR.php'; -require_once 'Util.php'; +require_once 'Net/LDAP2/Util.php'; +require_once 'Net/LDAP2/Entry.php'; /** * Object representation of a part of a LDAP filter. @@ -134,6 +135,9 @@ class Net_LDAP2_Filter extends PEAR * - lessOrEqual: The attributes value is less or equal than $value * - approx: One of the attributes values is similar to $value * + * Negation ("not") can be done by prepending the above operators with the + * "not" or "!" keyword, see example below. + * * If $escape is set to true (default) then $value will be escaped * properly. If it is set to false then $value will be treaten as raw filter value string. * You should escape yourself using {@link Net_LDAP2_Util::escape_filter_value()}! @@ -141,10 +145,13 @@ class Net_LDAP2_Filter extends PEAR * Examples: * * // This will find entries that contain an attribute "sn" that ends with "foobar": - * $filter = new Net_LDAP2_Filter('sn', 'ends', 'foobar'); + * $filter = Net_LDAP2_Filter::create('sn', 'ends', 'foobar'); * * // This will find entries that contain an attribute "sn" that has any value set: - * $filter = new Net_LDAP2_Filter('sn', 'any'); + * $filter = Net_LDAP2_Filter::create('sn', 'any'); + * + * // This will build a negated equals filter: + * $filter = Net_LDAP2_Filter::create('sn', 'not equals', 'foobar'); * * * @param string $attr_name Name of the attribute the filter should apply to @@ -161,8 +168,22 @@ class Net_LDAP2_Filter extends PEAR $array = Net_LDAP2_Util::escape_filter_value(array($value)); $value = $array[0]; } - switch (strtolower($match)) { + + $match = strtolower($match); + + // detect negation + $neg_matches = array(); + $negate_filter = false; + if (preg_match('/^(?:not|!)[\s_-](.+)/', $match, $neg_matches)) { + $negate_filter = true; + $match = $neg_matches[1]; + } + + // build basic filter + switch ($match) { case 'equals': + case '=': + case '==': $leaf_filter->_filter = '(' . $attr_name . '=' . $value . ')'; break; case 'begins': @@ -175,9 +196,11 @@ class Net_LDAP2_Filter extends PEAR $leaf_filter->_filter = '(' . $attr_name . '=*' . $value . '*)'; break; case 'greater': + case '>': $leaf_filter->_filter = '(' . $attr_name . '>' . $value . ')'; break; case 'less': + case '<': $leaf_filter->_filter = '(' . $attr_name . '<' . $value . ')'; break; case 'greaterorequal': @@ -199,6 +222,12 @@ class Net_LDAP2_Filter extends PEAR default: return PEAR::raiseError('Net_LDAP2_Filter create error: matching rule "' . $match . '" not known!'); } + + // negate if requested + if ($negate_filter) { + $leaf_filter = Net_LDAP2_Filter::combine('!', $leaf_filter); + } + return $leaf_filter; } @@ -207,10 +236,10 @@ class Net_LDAP2_Filter extends PEAR * * This static method combines two or more filter objects and returns one single * filter object that contains all the others. - * Call this method statically: $filter = Net_LDAP2_Filter('or', array($filter1, $filter2)) + * Call this method statically: $filter = Net_LDAP2_Filter::combine('or', array($filter1, $filter2)) * If the array contains filter strings instead of filter objects, we will try to parse them. * - * @param string $log_op The locicall operator. May be "and", "or", "not" or the subsequent logical equivalents "&", "|", "!" + * @param string $log_op The locical operator. May be "and", "or", "not" or the subsequent logical equivalents "&", "|", "!" * @param array|Net_LDAP2_Filter $filters array with Net_LDAP2_Filter objects * * @return Net_LDAP2_Filter|Net_LDAP2_Error @@ -241,8 +270,13 @@ class Net_LDAP2_Filter extends PEAR $filters = array($filter_o); } } elseif (is_array($filters)) { - $err = PEAR::raiseError('Net_LDAP2_Filter combine error: operator is "not" but $filter is an array!'); - return $err; + if (count($filters) != 1) { + $err = PEAR::raiseError('Net_LDAP2_Filter combine error: operator is "not" but $filter is an array!'); + return $err; + } elseif (!($filters[0] instanceof Net_LDAP2_Filter)) { + $err = PEAR::raiseError('Net_LDAP2_Filter combine error: operator is "not" but $filter is not a valid Net_LDAP2_Filter nor a filter string!'); + return $err; + } } else { $err = PEAR::raiseError('Net_LDAP2_Filter combine error: operator is "not" but $filter is not a valid Net_LDAP2_Filter nor a filter string!'); return $err; @@ -294,6 +328,17 @@ class Net_LDAP2_Filter extends PEAR public static function parse($FILTER) { if (preg_match('/^\((.+?)\)$/', $FILTER, $matches)) { + // Check for right bracket syntax: count of unescaped opening + // brackets must match count of unescaped closing brackets. + // At this stage we may have: + // 1. one filter component with already removed outer brackets + // 2. one or more subfilter components + $c_openbracks = preg_match_all('/(?|<|>=|<=)/', $matches[1], 2, PREG_SPLIT_DELIM_CAPTURE); + $filter_parts = Net_LDAP2_Util::split_attribute_string($matches[1], true, true); if (count($filter_parts) != 3) { return PEAR::raiseError("Filter parsing error: invalid filter syntax - unknown matching rule used"); } else { @@ -510,5 +555,121 @@ class Net_LDAP2_Filter extends PEAR return true; // Leaf! } } + + /** + * Filter entries using this filter or see if a filter matches + * + * @todo Currently slow and naive implementation with preg_match, could be optimized (esp. begins, ends filters etc) + * @todo Currently only "="-based matches (equals, begins, ends, contains, any) implemented; Implement all the stuff! + * @todo Implement expert code with schema checks in case $entry is connected to a directory + * @param array|Net_LDAP2_Entry The entry (or array with entries) to check + * @param array If given, the array will be appended with entries who matched the filter. Return value is true if any entry matched. + * @return int|Net_LDAP2_Error Returns the number of matched entries or error + */ + function matches(&$entries, &$results=array()) { + $numOfMatches = 0; + + if (!is_array($entries)) { + $all_entries = array(&$entries); + } else { + $all_entries = &$entries; + } + + foreach ($all_entries as $entry) { + // look at the current entry and see if filter matches + + $entry_matched = false; + // if this is not a single component, do calculate all subfilters, + // then assert the partial results with the given combination modifier + if (!$this->isLeaf()) { + + // get partial results from subfilters + $partial_results = array(); + foreach ($this->_subfilters as $filter) { + $partial_results[] = $filter->matches($entry); + } + + // evaluate partial results using this filters combination rule + switch ($this->_match) { + case '!': + // result is the neagtive result of the assertion + $entry_matched = !$partial_results[0]; + break; + + case '&': + // all partial results have to be boolean-true + $entry_matched = !in_array(false, $partial_results); + break; + + case '|': + // at least one partial result has to be true + $entry_matched = in_array(true, $partial_results); + break; + } + + } else { + // Leaf filter: assert given entry + // [TODO]: Could be optimized to avoid preg_match especially with "ends", "begins" etc + + // Translate the LDAP-match to some preg_match expression and evaluate it + list($attribute, $match, $assertValue) = $this->getComponents(); + switch ($match) { + case '=': + $regexp = '/^'.str_replace('*', '.*', $assertValue).'$/i'; // not case sensitive unless specified by schema + $entry_matched = $entry->pregMatch($regexp, $attribute); + break; + + // ------------------------------------- + // [TODO]: implement <, >, <=, >= and =~ + // ------------------------------------- + + default: + $err = PEAR::raiseError("Net_LDAP2_Filter match error: unsupported match rule '$match'!"); + return $err; + } + + } + + // process filter matching result + if ($entry_matched) { + $numOfMatches++; + $results[] = $entry; + } + + } + + return $numOfMatches; + } + + + /** + * Retrieve this leaf-filters attribute, match and value component. + * + * For leaf filters, this returns array(attr, match, value). + * Match is be the logical operator, not the text representation, + * eg "=" instead of "equals". Note that some operators are really + * a combination of operator+value with wildcard, like + * "begins": That will return "=" with the value "value*"! + * + * For non-leaf filters this will drop an error. + * + * @todo $this->_match is not always available and thus not usable here; it would be great if it would set in the factory methods and constructor. + * @return array|Net_LDAP2_Error + */ + function getComponents() { + if ($this->isLeaf()) { + $raw_filter = preg_replace('/^\(|\)$/', '', $this->_filter); + $parts = Net_LDAP2_Util::split_attribute_string($raw_filter, true, true); + if (count($parts) != 3) { + return PEAR::raiseError("Net_LDAP2_Filter getComponents() error: invalid filter syntax - unknown matching rule used"); + } else { + return $parts; + } + } else { + return PEAR::raiseError('Net_LDAP2_Filter getComponents() call is invalid for non-leaf filters!'); + } + } + + } ?> diff --git a/plugins/LdapCommon/extlib/Net/LDAP2/LDIF.php b/plugins/LdapCommon/extlib/Net/LDAP2/LDIF.php index 34f3e75dd5..384b8e2b79 100644 --- a/plugins/LdapCommon/extlib/Net/LDAP2/LDIF.php +++ b/plugins/LdapCommon/extlib/Net/LDAP2/LDIF.php @@ -10,7 +10,7 @@ * @author Benedikt Hallinger * @copyright 2009 Benedikt Hallinger * @license http://www.gnu.org/licenses/lgpl-3.0.txt LGPLv3 -* @version SVN: $Id: LDIF.php 286718 2009-08-03 07:30:49Z beni $ +* @version SVN: $Id: LDIF.php 324918 2012-04-06 12:31:04Z clockwerx $ * @link http://pear.php.net/package/Net_LDAP2/ */ @@ -340,6 +340,7 @@ class Net_LDAP2_LDIF extends PEAR + count($entry_attrs_changes['replace']) + count($entry_attrs_changes['delete']); + $is_changed = ($num_of_changes > 0 || $entry->willBeDeleted() || $entry->willBeMoved()); // write version if not done yet @@ -556,10 +557,10 @@ class Net_LDAP2_LDIF extends PEAR $attributes = array(); $dn = false; foreach ($lines as $line) { - if (preg_match('/^(\w+)(:|::|:<)\s(.+)$/', $line, $matches)) { - $attr =& $matches[1]; - $delim =& $matches[2]; - $data =& $matches[3]; + if (preg_match('/^(\w+(;binary)?)(:|::|:<)\s(.+)$/', $line, $matches)) { + $attr =& $matches[1] . $matches[2]; + $delim =& $matches[3]; + $data =& $matches[4]; if ($delim == ':') { // normal data @@ -682,20 +683,22 @@ class Net_LDAP2_LDIF extends PEAR if (preg_match('/^version:\s(.+)$/', $data, $match)) { // version statement, set version $this->version($match[1]); - } elseif (preg_match('/^\w+::?\s.+$/', $data)) { + } elseif (preg_match('/^\w+(;binary)?::?\s.+$/', $data)) { // normal attribute: add line $commentmode = false; $this->_lines_next[] = trim($data); $datalines_read++; } elseif (preg_match('/^\s(.+)$/', $data, $matches)) { // wrapped data: unwrap if not in comment mode + // note that the \s above is some more liberal than + // the RFC requests as it also matches tabs etc. if (!$commentmode) { if ($datalines_read == 0) { // first line of entry: wrapped data is illegal $this->dropError('Net_LDAP2_LDIF error: illegal wrapping at input line '.$this->_input_line, $this->_input_line); } else { $last = array_pop($this->_lines_next); - $last = $last.trim($matches[1]); + $last = $last.$matches[1]; $this->_lines_next[] = $last; $datalines_read++; } diff --git a/plugins/LdapCommon/extlib/Net/LDAP2/Schema.php b/plugins/LdapCommon/extlib/Net/LDAP2/Schema.php index b590eabc51..7eb15662eb 100644 --- a/plugins/LdapCommon/extlib/Net/LDAP2/Schema.php +++ b/plugins/LdapCommon/extlib/Net/LDAP2/Schema.php @@ -11,7 +11,7 @@ * @author Benedikt Hallinger * @copyright 2009 Jan Wagner, Benedikt Hallinger * @license http://www.gnu.org/licenses/lgpl-3.0.txt LGPLv3 -* @version SVN: $Id: Schema.php 286718 2009-08-03 07:30:49Z beni $ +* @version SVN: $Id: Schema.php 296515 2010-03-22 14:46:41Z beni $ * @link http://pear.php.net/package/Net_LDAP2/ * @todo see the comment at the end of the file */ @@ -168,12 +168,16 @@ class Net_LDAP2_Schema extends PEAR array('attributes' => array_values($schema_o->types), 'scope' => 'base')); if (Net_LDAP2::isError($result)) { - return $result; + return PEAR::raiseError('Could not fetch Subschema entry: '.$result->getMessage()); } $entry = $result->shiftEntry(); if (!$entry instanceof Net_LDAP2_Entry) { - return PEAR::raiseError('Could not fetch Subschema entry'); + if ($entry instanceof Net_LDAP2_Error) { + return PEAR::raiseError('Could not fetch Subschema entry: '.$entry->getMessage()); + } else { + return PEAR::raiseError('Could not fetch Subschema entry (search returned '.$result->count().' entries. Check parameter \'basedn\')'); + } } $schema_o->parse($entry); @@ -183,7 +187,7 @@ class Net_LDAP2_Schema extends PEAR /** * Return a hash of entries for the given type * - * Returns a hash of entry for th givene type. Types may be: + * Returns a hash of entry for the givene type. Types may be: * objectclasses, attributes, ditcontentrules, ditstructurerules, matchingrules, * matchingruleuses, nameforms, syntaxes * @@ -508,9 +512,111 @@ class Net_LDAP2_Schema extends PEAR return $return; } - // [TODO] add method that allows us to see to which objectclasses a certain attribute belongs to - // it should return the result structured, e.g. sorted in "may" and "must". Optionally it should - // be able to return it just "flat", e.g. array_merge()d. - // We could use get_all() to achieve this easily, i think + /** + * See if an schema element exists + * + * @param string $type Type of name, see get() + * @param string $name Name or OID + * + * @return boolean + */ + public function exists($type, $name) + { + $entry = $this->get($type, $name); + if ($entry instanceof Net_LDAP2_ERROR) { + return false; + } else { + return true; + } + } + + /** + * See if an attribute is defined in the schema + * + * @param string $attribute Name or OID of the attribute + * @return boolean + */ + public function attributeExists($attribute) + { + return $this->exists('attribute', $attribute); + } + + /** + * See if an objectClass is defined in the schema + * + * @param string $ocl Name or OID of the objectClass + * @return boolean + */ + public function objectClassExists($ocl) + { + return $this->exists('objectclass', $ocl); + } + + + /** + * See to which ObjectClasses an attribute is assigned + * + * The objectclasses are sorted into the keys 'may' and 'must'. + * + * @param string $attribute Name or OID of the attribute + * + * @return array|Net_LDAP2_Error Associative array with OCL names or Error + */ + public function getAssignedOCLs($attribute) + { + $may = array(); + $must = array(); + + // Test if the attribute type is defined in the schema, + // if so, retrieve real name for lookups + $attr_entry = $this->get('attribute', $attribute); + if ($attr_entry instanceof Net_LDAP2_ERROR) { + return PEAR::raiseError("Attribute $attribute not defined in schema: ".$attr_entry->getMessage()); + } else { + $attribute = $attr_entry['name']; + } + + + // We need to get all defined OCLs for this. + $ocls = $this->getAll('objectclasses'); + foreach ($ocls as $ocl => $ocl_data) { + // Fetch the may and must attrs and see if our searched attr is contained. + // If so, record it in the corresponding array. + $ocl_may_attrs = $this->may($ocl); + $ocl_must_attrs = $this->must($ocl); + if (is_array($ocl_may_attrs) && in_array($attribute, $ocl_may_attrs)) { + array_push($may, $ocl_data['name']); + } + if (is_array($ocl_must_attrs) && in_array($attribute, $ocl_must_attrs)) { + array_push($must, $ocl_data['name']); + } + } + + return array('may' => $may, 'must' => $must); + } + + /** + * See if an attribute is available in a set of objectClasses + * + * @param string $attribute Attribute name or OID + * @param array $ocls Names of OCLs to check for + * + * @return boolean TRUE, if the attribute is defined for at least one of the OCLs + */ + public function checkAttribute($attribute, $ocls) + { + foreach ($ocls as $ocl) { + $ocl_entry = $this->get('objectclass', $ocl); + $ocl_may_attrs = $this->may($ocl); + $ocl_must_attrs = $this->must($ocl); + if (is_array($ocl_may_attrs) && in_array($attribute, $ocl_may_attrs)) { + return true; + } + if (is_array($ocl_must_attrs) && in_array($attribute, $ocl_must_attrs)) { + return true; + } + } + return false; // no ocl for the ocls found. + } } -?> +?> \ No newline at end of file diff --git a/plugins/LdapCommon/extlib/Net/LDAP2/Search.php b/plugins/LdapCommon/extlib/Net/LDAP2/Search.php index de4fde122c..d18554f578 100644 --- a/plugins/LdapCommon/extlib/Net/LDAP2/Search.php +++ b/plugins/LdapCommon/extlib/Net/LDAP2/Search.php @@ -11,7 +11,7 @@ * @author Benedikt Hallinger * @copyright 2009 Tarjej Huse, Benedikt Hallinger * @license http://www.gnu.org/licenses/lgpl-3.0.txt LGPLv3 -* @version SVN: $Id: Search.php 286718 2009-08-03 07:30:49Z beni $ +* @version SVN: $Id: Search.php 328961 2013-01-03 09:04:30Z beni $ * @link http://pear.php.net/package/Net_LDAP2/ */ @@ -106,13 +106,22 @@ class Net_LDAP2_Search extends PEAR implements Iterator /** * Cache variable for storing entries fetched internally * - * This currently is only used by {@link pop_entry()} + * This currently is not used by all functions and need consolidation. * * @access protected * @var array */ protected $_entry_cache = false; + /** + * Cache variable for count() + * + * @see count() + * @access protected + * @var int + */ + protected $_count_cache = null; + /** * Constructor * @@ -143,7 +152,7 @@ class Net_LDAP2_Search extends PEAR implements Iterator } /** - * Returns an array of entry objects + * Returns an array of entry objects. * * @return array Array of entry objects. */ @@ -151,15 +160,19 @@ class Net_LDAP2_Search extends PEAR implements Iterator { $entries = array(); - while ($entry = $this->shiftEntry()) { - $entries[] = $entry; + if (false === $this->_entry_cache) { + // cache is empty: fetch from LDAP + while ($entry = $this->shiftEntry()) { + $entries[] = $entry; + } + $this->_entry_cache = $entries; // store result in cache } - return $entries; + return $this->_entry_cache; } /** - * Get the next entry in the searchresult. + * Get the next entry in the searchresult from LDAP server. * * This will return a valid Net_LDAP2_Entry object or false, so * you can use this method to easily iterate over the entries inside @@ -169,22 +182,20 @@ class Net_LDAP2_Search extends PEAR implements Iterator */ public function &shiftEntry() { - if ($this->count() == 0 ) { - $false = false; - return $false; - } - if (is_null($this->_entry)) { - $this->_entry = @ldap_first_entry($this->_link, $this->_search); + if(!$this->_entry = @ldap_first_entry($this->_link, $this->_search)) { + $false = false; + return $false; + } $entry = Net_LDAP2_Entry::createConnected($this->_ldap, $this->_entry); - if ($entry instanceof Net_LDAP2_Error) $entry = false; + if ($entry instanceof PEAR_Error) $entry = false; } else { if (!$this->_entry = @ldap_next_entry($this->_link, $this->_entry)) { $false = false; return $false; } $entry = Net_LDAP2_Entry::createConnected($this->_ldap, $this->_entry); - if ($entry instanceof Net_LDAP2_Error) $entry = false; + if ($entry instanceof PEAR_Error) $entry = false; } return $entry; } @@ -461,7 +472,13 @@ class Net_LDAP2_Search extends PEAR implements Iterator if (!$this->_search) { return 0; } - return @ldap_count_entries($this->_link, $this->_search); + // ldap_count_entries is slow (see pear bug #18752) with large results, + // so we cache the result internally. + if ($this->_count_cache === null) { + $this->_count_cache = @ldap_count_entries($this->_link, $this->_search); + } + + return $this->_count_cache; } /** diff --git a/plugins/LdapCommon/extlib/Net/LDAP2/Util.php b/plugins/LdapCommon/extlib/Net/LDAP2/Util.php index 48b03f9f99..9693de2bac 100644 --- a/plugins/LdapCommon/extlib/Net/LDAP2/Util.php +++ b/plugins/LdapCommon/extlib/Net/LDAP2/Util.php @@ -10,7 +10,7 @@ * @author Benedikt Hallinger * @copyright 2009 Benedikt Hallinger * @license http://www.gnu.org/licenses/lgpl-3.0.txt LGPLv3 -* @version SVN: $Id: Util.php 286718 2009-08-03 07:30:49Z beni $ +* @version SVN: $Id: Util.php 332278 2013-12-05 11:01:15Z beni $ * @link http://pear.php.net/package/Net_LDAP2/ */ @@ -525,17 +525,29 @@ class Net_LDAP2_Util extends PEAR } /** - * Splits a attribute=value syntax into an array + * Splits an attribute=value syntax into an array * - * The split will occur at the first unescaped '=' character. + * If escaped delimeters are used, they are returned escaped as well. + * The split will occur at the first unescaped delimeter character. + * In case an invalid delimeter is given, no split will be performed and an + * one element array gets returned. + * Optional also filter-assertion delimeters can be considered (>, <, >=, <=, ~=). * - * @param string $attr Attribute and Value Syntax + * @param string $attr Attribute and Value Syntax ("foo=bar") + * @param boolean $extended If set to true, also filter-assertion delimeter will be matched + * @param boolean $withDelim If set to true, the return array contains the delimeter at index 1, putting the value to index 2 * - * @return array Indexed array: 0=attribute name, 1=attribute value + * @return array Indexed array: 0=attribute name, 1=attribute value OR ($withDelim=true): 0=attr, 1=delimeter, 2=value */ - public static function split_attribute_string($attr) + public static function split_attribute_string($attr, $extended=false, $withDelim=false) { - return preg_split('/(?=|<=|>|<|~=|=)/', $attr, 2, $withDelim); + } } /**