diff --git a/History.txt b/History.txt
index ae1c853..299bba3 100644
--- a/History.txt
+++ b/History.txt
@@ -1,5 +1,24 @@
=== Net::LDAP NEXT / 2010-__-__
-* Added documentation:
+* SSL capabilities will be enabled or disabled based on whether we can load
+ OpenSSL successfully or not.
+* Moved the core class extensions extensions from being in the Net::LDAP
+ hierarchy to the Net::BER hierarchy as most of the methods therein are
+ related to BER-encoding values. This will make extracting Net::BER from
+ Net::LDAP easier in the future.
+* Net::LDAP::Filter changes:
+ * Filters can only be constructed using our custom constructors (eq, ge,
+ etc.). Cleaned up the code to reflect the private new.
+ * Fixed #to_ber to output a BER representation for :ne filters. Simplified
+ * the BER construction for substring matching.
+ * Added Filter.join(left, right), Filter.intersect(left, right), and
+ Filter.negate(filter) to match Filter#&, Filter#|, and Filter#~@ to prevent
+ those operators from having problems with the private new.
+ * Added Filter.present and Filter.present? aliases for the method previously
+ only known as Filter.pres.
+ * Cleaned up Net::LDAP::Filter::FilterParser to handle branches better. Fixed
+ some of the regular expressions to be more canonically defined.
+ * Cleaned up the string representation of Filter objects.
+* Added or revised documentation:
* Core class extension methods under Net::BER.
* Extended unit testing:
* Added some unit tests for the BER core extensions.
@@ -7,12 +26,6 @@
* Replaced calls to #to_a with calls to Kernel#Array; since Ruby 1.8.3, the
default #to_a implementation has been deprecated and should be replaced
either with calls to Kernel#Array or [value].flatten(1).
-* SSL capabilities will be enabled or disabled based on whether we can load
- OpenSSL successfully or not.
-* Moved the core class extensions extensions from being in the Net::LDAP
- hierarchy to the Net::BER hierarchy as most of the methods therein are
- related to BER-encoding values. This will make extracting Net::BER from
- Net::LDAP easier in the future.
=== Net::LDAP 0.1.1 / 2010-03-18
* Fixing a critical problem with sockets.
diff --git a/lib/net/ldap/filter.rb b/lib/net/ldap/filter.rb
index fe03660..31dbaf5 100644
--- a/lib/net/ldap/filter.rb
+++ b/lib/net/ldap/filter.rb
@@ -1,148 +1,321 @@
-# Copyright (C) 2006 by Francis Cianfrocca. All Rights Reserved.
+# Copyright (C) 2006 by Francis Cianfrocca and other contributors. All
+# Rights Reserved.
#
# Gmail: garbagecat10
#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation; either version 2 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
-#
-#---------------------------------------------------------------------------
+# This program is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by the Free
+# Software Foundation; either version 2 of the License, or (at your option)
+# any later version.
#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+# for more details.
#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to:
+# Free Software Foundation, Inc.
+# 51 Franklin St, Fifth Floor
+# Boston, MA 02110-1301
+# USA
-require 'strscan'
-
-module Net
-class LDAP
-
-
-# Class Net::LDAP::Filter is used to constrain
-# LDAP searches. An object of this class is
-# passed to Net::LDAP#search in the parameter :filter.
+##
+# Class Net::LDAP::Filter is used to constrain LDAP searches. An object of
+# this class is passed to Net::LDAP#search in the parameter :filter.
#
-# Net::LDAP::Filter supports the complete set of search filters
-# available in LDAP, including conjunction, disjunction and negation
-# (AND, OR, and NOT). This class supplants the (infamous) RFC-2254
-# standard notation for specifying LDAP search filters.
+# Net::LDAP::Filter supports the complete set of search filters available in
+# LDAP, including conjunction, disjunction and negation (AND, OR, and NOT).
+# This class supplants the (infamous) RFC 2254 standard notation for
+# specifying LDAP search filters.
+#--
+# NOTE: This wording needs to change as we will be supporting LDAPv3 search
+# filter strings (RFC 4515).
+#++
#
# Here's how to code the familiar "objectclass is present" filter:
-# f = Net::LDAP::Filter.pres( "objectclass" )
-# The object returned by this code can be passed directly to
-# the :filter parameter of Net::LDAP#search.
+# f = Net::LDAP::Filter.present("objectclass")
+#
+# The object returned by this code can be passed directly to the
+# :filter parameter of Net::LDAP#search.
#
# See the individual class and instance methods below for more examples.
-#
-class Filter
+class Net::LDAP::Filter
+ ##
+ # Known filter types.
+ FilterTypes = [ :ne, :eq, :ge, :le, :and, :or, :not ]
- def initialize op, a, b
+ def initialize(op, left, right) #:nodoc:
+ unless FilterTypes.include?(op)
+ raise Net::LDAP::LdapError, "Invalid or unsupported operator #{op.inspect} in LDAP Filter."
+ end
@op = op
- @left = a
- @right = b
+ @left = left
+ @right = right
end
- # #eq creates a filter object indicating that the value of
- # a paticular attribute must be either present or must
- # match a particular string.
- #
- # To specify that an attribute is "present" means that only
- # directory entries which contain a value for the particular
- # attribute will be selected by the filter. This is useful
- # in case of optional attributes such as mail.
- # Presence is indicated by giving the value "*" in the second
- # parameter to #eq. This example selects only entries that have
- # one or more values for sAMAccountName:
- # f = Net::LDAP::Filter.eq( "sAMAccountName", "*" )
- #
- # To match a particular range of values, pass a string as the
- # second parameter to #eq. The string may contain one or more
- # "*" characters as wildcards: these match zero or more occurrences
- # of any character. Full regular-expressions are not supported
- # due to limitations in the underlying LDAP protocol.
- # This example selects any entry with a mail value containing
- # the substring "anderson":
- # f = Net::LDAP::Filter.eq( "mail", "*anderson*" )
- #--
- # Removed gt and lt. They ain't in the standard!
- #
- def Filter::eq attribute, value; Filter.new :eq, attribute, value; end
- def Filter::ne attribute, value; Filter.new :ne, attribute, value; end
- #def Filter::gt attribute, value; Filter.new :gt, attribute, value; end
- #def Filter::lt attribute, value; Filter.new :lt, attribute, value; end
- def Filter::ge attribute, value; Filter.new :ge, attribute, value; end
- def Filter::le attribute, value; Filter.new :le, attribute, value; end
+ class << self
+ # We don't want filters created except using our custom constructors.
+ private :new
- # #pres( attribute ) is a synonym for #eq( attribute, "*" )
- #
- def Filter::pres attribute; Filter.eq attribute, "*"; end
+ ##
+ # Creates a Filter object indicating that the value of a particular
+ # attribute must either be present or match a particular string.
+ #
+ # Specifying that an attribute is 'present' means only directory entries
+ # which contain a value for the particular attribute will be selected by
+ # the filter. This is useful in case of optional attributes such as
+ # mail. Presence is indicated by giving the value "*" in the
+ # second parameter to #eq. This example selects only entries that have
+ # one or more values for sAMAccountName:
+ #
+ # f = Net::LDAP::Filter.eq("sAMAccountName", "*")
+ #
+ # To match a particular range of values, pass a string as the second
+ # parameter to #eq. The string may contain one or more "*" characters as
+ # wildcards: these match zero or more occurrences of any character. Full
+ # regular-expressions are not supported due to limitations in the
+ # underlying LDAP protocol. This example selects any entry with a
+ # mail value containing the substring "anderson":
+ #
+ # f = Net::LDAP::Filter.eq("mail", "*anderson*")
+ def eq(attribute, value)
+ new(:eq, attribute, value)
+ end
- # operator & ("AND") is used to conjoin two or more filters.
- # This expression will select only entries that have an objectclass
- # attribute AND have a mail attribute that begins with "George":
- # f = Net::LDAP::Filter.pres( "objectclass" ) & Net::LDAP::Filter.eq( "mail", "George*" )
- #
- def & filter; Filter.new :and, self, filter; end
+ ##
+ # Creates a Filter object indicating that a particular attribute value
+ # is either not present or does not match a particular string; see
+ # Filter::eq for more information.
+ def ne(attribute, value)
+ new(:ne, attribute, value)
+ end
- # operator | ("OR") is used to disjoin two or more filters.
- # This expression will select entries that have either an objectclass
- # attribute OR a mail attribute that begins with "George":
- # f = Net::LDAP::Filter.pres( "objectclass" ) | Net::LDAP::Filter.eq( "mail", "George*" )
- #
- def | filter; Filter.new :or, self, filter; end
+ ##
+ # Creates a Filter object indicating that a particular attribute value
+ # is greater than or equal to the specified value.
+ def ge(attribute, value)
+ new(:ge, attribute, value)
+ end
+ ##
+ # Creates a Filter object indicating that a particular attribute value
+ # is less than or equal to the specified value.
+ def le(attribute, value)
+ new(:le, attribute, value)
+ end
- #
- # operator ~ ("NOT") is used to negate a filter.
- # This expression will select only entries that do not have an objectclass
- # attribute:
- # f = ~ Net::LDAP::Filter.pres( "objectclass" )
- #
- #--
- # This operator can't be !, evidently. Try it.
- # Removed GT and LT. They're not in the RFC.
- def ~@; Filter.new :not, self, nil; end
+ ##
+ # Joins two or more filters so that all conditions must be true. Calling
+ # Filter.join(left, right) is the same as left &
+ # right.
+ #
+ # # Selects only entries that have an objectclass attribute.
+ # x = Net::LDAP::Filter.present("objectclass")
+ # # Selects only entries that have a mail attribute that begins
+ # # with "George".
+ # y = Net::LDAP::Filter.eq("mail", "George*")
+ # # Selects only entries that meet both conditions above.
+ # z = Net::LDAP::Filter.join(x, y)
+ def join(left, right)
+ new(:and, left, right)
+ end
- # Equality operator for filters, useful primarily for constructing unit tests.
- def == filter
+ ##
+ # Creates a disjoint comparison between two or more filters. Selects
+ # entries where either the left or right side are true. Calling
+ # Filter.intersect(left, right) is the same as left |
+ # right.
+ #
+ # # Selects only entries that have an objectclass attribute.
+ # x = Net::LDAP::Filter.present("objectclass")
+ # # Selects only entries that have a mail attribute that begins
+ # # with "George".
+ # y = Net::LDAP::Filter.eq("mail", "George*")
+ # # Selects only entries that meet either condition above.
+ # z = x | y
+ def intersect(left, right)
+ new(:or, left, right)
+ end
+
+ ##
+ # Negates a filter. Calling Fitler.negate(filter) i s the same
+ # as ~filter.
+ #
+ # # Selects only entries that do not have an objectclass
+ # # attribute.
+ # x = ~Net::LDAP::Filter.present("objectclass")
+ def negate(filter)
+ new(:not, filter, nil)
+ end
+
+ ##
+ # This is a synonym for #eq(attribute, "*"). Also known as #present and
+ # #pres.
+ def present?(attribute)
+ eq(attribute, "*")
+ end
+ alias_method :present, :present?
+ alias_method :pres, :present?
+
+ ##
+ # Converts an LDAP search filter in BER format to an Net::LDAP::Filter
+ # object. The incoming BER object most likely came to us by parsing an
+ # LDAP searchRequest PDU. See also the comments under #to_ber, including
+ # the grammar snippet from the RFC.
+ #--
+ # We're hardcoding the BER constants from the RFC. These should be
+ # broken out insto constants.
+ def parse_ber(ber)
+ case ber.ber_identifier
+ when 0xa0 # context-specific constructed 0, "and"
+ ber.map { |b| parse_ber(b) }.inject { |memo, obj| memo & obj }
+ when 0xa1 # context-specific constructed 1, "or"
+ ber.map { |b| parse_ber(b) }.inject { |memo, obj| memo | obj }
+ when 0xa2 # context-specific constructed 2, "not"
+ ~parse_ber(ber.first)
+ when 0xa3 # context-specific constructed 3, "equalityMatch"
+ if ber.last == "*"
+ else
+ eq(ber.first, ber.last)
+ end
+ when 0xa4 # context-specific constructed 4, "substring"
+ str = ""
+ final = false
+ ber.last.each { |b|
+ case b.ber_identifier
+ when 0x80 # context-specific primitive 0, SubstringFilter "initial"
+ raise Net::LDAP::LdapError, "Unrecognized substring filter; bad initial value." if str.length > 0
+ str += b
+ when 0x81 # context-specific primitive 0, SubstringFilter "any"
+ str += "*#{b}"
+ when 0x82 # context-specific primitive 0, SubstringFilter "final"
+ str += "*#{b}"
+ final = true
+ end
+ }
+ str += "*" unless final
+ eq(ber.first.to_s, str)
+ when 0xa5 # context-specific constructed 5, "greaterOrEqual"
+ ge(ber.first.to_s, ber.last.to_s)
+ when 0xa6 # context-specific constructed 5, "lessOrEqual"
+ le(ber.first.to_s, ber.last.to_s)
+ when 0x87 # context-specific primitive 7, "present"
+ # call to_s to get rid of the BER-identifiedness of the incoming string.
+ present?(ber.to_s)
+ else
+ raise Net::LDAP::LdapError, "Invalid BER tag-value (#{ber.ber_identifier}) in search filter."
+ end
+ end
+
+ ##
+ # Converts an LDAP filter-string (in the prefix syntax specified in RFC-2254)
+ # to a Net::LDAP::Filter.
+ def construct(ldap_filter_string)
+ FilterParser.parse(ldap_filter_string)
+ end
+ alias_method :from_rfc2254, :construct
+ alias_method :from_rfc4515, :construct
+
+ ##
+ # Convert an RFC-1777 LDAP/BER "Filter" object to a Net::LDAP::Filter
+ # object.
+ #--
+ # TODO, we're hardcoding the RFC-1777 BER-encodings of the various
+ # filter types. Could pull them out into a constant.
+ #++
+ def parse_ldap_filter(obj)
+ case obj.ber_identifier
+ when 0x87 # present. context-specific primitive 7.
+ eq(obj.to_s, "*")
+ when 0xa3 # equalityMatch. context-specific constructed 3.
+ eq(obj[0], obj[1])
+ else
+ raise Net::LDAP::LdapError, "Unknown LDAP search-filter type: #{obj.ber_identifier}"
+ end
+ end
+ end
+
+ ##
+ # Joins two or more filters so that all conditions must be true.
+ #
+ # # Selects only entries that have an objectclass attribute.
+ # x = Net::LDAP::Filter.present("objectclass")
+ # # Selects only entries that have a mail attribute that begins
+ # # with "George".
+ # y = Net::LDAP::Filter.eq("mail", "George*")
+ # # Selects only entries that meet both conditions above.
+ # z = x & y
+ def &(filter)
+ self.class.join(self, filter)
+ end
+
+ ##
+ # Creates a disjoint comparison between two or more filters. Selects
+ # entries where either the left or right side are true.
+ #
+ # # Selects only entries that have an objectclass attribute.
+ # x = Net::LDAP::Filter.present("objectclass")
+ # # Selects only entries that have a mail attribute that begins
+ # # with "George".
+ # y = Net::LDAP::Filter.eq("mail", "George*")
+ # # Selects only entries that meet either condition above.
+ # z = x | y
+ def |(filter)
+ self.class.intersect(self, filter)
+ end
+
+ ##
+ # Negates a filter.
+ #
+ # # Selects only entries that do not have an objectclass
+ # # attribute.
+ # x = ~Net::LDAP::Filter.present("objectclass")
+ def ~@
+ self.class.negate(self)
+ end
+
+ ##
+ # Equality operator for filters, useful primarily for constructing unit tests.
+ def ==(filter)
+ # 20100320 AZ: We need to come up with a better way of doing this. This
+ # is just nasty.
str = "[@op,@left,@right]"
self.instance_eval(str) == filter.instance_eval(str)
end
- def to_s
+ def to_raw_rfc2254
case @op
when :ne
- "(!(#{@left}=#{@right}))"
+ "!(#{@left}=#{@right})"
when :eq
- "(#{@left}=#{@right})"
- #when :gt
- # "#{@left}>#{@right}"
- #when :lt
- # "#{@left}<#{@right}"
+ "#{@left}=#{@right}"
when :ge
"#{@left}>=#{@right}"
when :le
"#{@left}<=#{@right}"
when :and
- "(&(#{@left})(#{@right}))"
+ "&(#{@left.__send__(:to_raw_rfc2254)})(#{@right.__send__(:to_raw_rfc2254)})"
when :or
- "(|(#{@left})(#{@right}))"
+ "|(#{@left.__send__(:to_raw_rfc2254)})(#{@right.__send__(:to_raw_rfc2254)})"
when :not
- "(!(#{@left}))"
- else
- raise "invalid or unsupported operator in LDAP Filter"
+ "!(#{@left.__send__(:to_raw_rfc2254)})"
end
end
+ private :to_raw_rfc2254
+ ##
+ # Converts the Filter object to an RFC 2254-compatible text format.
+ def to_rfc2254
+ "(#{to_raw_rfc2254})"
+ end
+ def to_s
+ to_rfc2254
+ end
+
+ ##
+ # Converts the filter to BER format.
#--
# to_ber
# Filter ::=
@@ -167,146 +340,95 @@ class Filter
# final [2] LDAPString
# }
# }
- #
- # Parsing substrings is a little tricky.
- # We use the split method to break a string into substrings
- # delimited by the * (star) character. But we also need
- # to know whether there is a star at the head and tail
- # of the string. A Ruby particularity comes into play here:
- # if you split on * and the first character of the string is
- # a star, then split will return an array whose first element
- # is an _empty_ string. But if the _last_ character of the
- # string is star, then split will return an array that does
- # _not_ add an empty string at the end. So we have to deal
- # with all that specifically.
- #
+ #++
def to_ber
case @op
when :eq
- if @right == "*" # present
- @left.to_s.to_ber_contextspecific 7
- elsif @right =~ /[\*]/ #substring
- ary = @right.split( /[\*]+/ )
- final_star = @right =~ /[\*]$/
- initial_star = ary.first == "" and ary.shift
+ if @right == "*" # presence test
+ @left.to_s.to_ber_contextspecific(7)
+ elsif @right =~ /[*]/ # substring
+ # Parsing substrings is a little tricky. We use String#split to
+ # break a string into substrings delimited by the * (star)
+ # character. But we also need to know whether there is a star at the
+ # head and tail of the string, so we use a limit parameter value of
+ # -1: "If negative, there is no limit to the number of fields
+ # returned, and trailing null fields are not suppressed."
+ #
+ # 20100320 AZ: This is much simpler than the previous verison. Also,
+ # unnecessary regex escaping has been removed.
- seq = []
- unless initial_star
- seq << ary.shift.to_ber_contextspecific(0)
+ ary = @right.split(/[*]+/, -1)
+
+ if ary.first.empty?
+ first = nil
+ ary.shift
+ else
+ first = ary.shift.to_ber_contextspecific(0)
end
- n_any_strings = ary.length - (final_star ? 0 : 1)
- #p n_any_strings
- n_any_strings.times {
- seq << ary.shift.to_ber_contextspecific(1)
- }
- unless final_star
- seq << ary.shift.to_ber_contextspecific(2)
+
+ if ary.last.empty?
+ last = nil
+ ary.pop
+ else
+ last = ary.pop.to_ber_contextspecific(2)
end
- [@left.to_s.to_ber, seq.to_ber].to_ber_contextspecific 4
- else #equality
- [@left.to_s.to_ber, unescape(@right).to_ber].to_ber_contextspecific 3
+
+ seq = ary.map { |e| e.to_ber_contextspecific(1) }
+ seq.unshift first if first
+ seq.push last if last
+
+ [@left.to_s.to_ber, seq.to_ber].to_ber_contextspecific(4)
+ else # equality
+ [@left.to_s.to_ber, unescape(@right).to_ber].to_ber_contextspecific(3)
end
when :ge
- [@left.to_s.to_ber, unescape(@right).to_ber].to_ber_contextspecific 5
+ [@left.to_s.to_ber, unescape(@right).to_ber].to_ber_contextspecific(5)
when :le
- [@left.to_s.to_ber, unescape(@right).to_ber].to_ber_contextspecific 6
+ [@left.to_s.to_ber, unescape(@right).to_ber].to_ber_contextspecific(6)
+ when :ne
+ [self.class.eq(@left, @right).to_ber].to_ber_contextspecific(2)
when :and
ary = [@left.coalesce(:and), @right.coalesce(:and)].flatten
- ary.map {|a| a.to_ber}.to_ber_contextspecific( 0 )
+ ary.map {|a| a.to_ber}.to_ber_contextspecific(0)
when :or
ary = [@left.coalesce(:or), @right.coalesce(:or)].flatten
- ary.map {|a| a.to_ber}.to_ber_contextspecific( 1 )
+ ary.map {|a| a.to_ber}.to_ber_contextspecific(1)
when :not
- [@left.to_ber].to_ber_contextspecific 2
- else
- # ERROR, we'll return objectclass=* to keep things from blowing up,
- # but that ain't a good answer and we need to kick out an error of some kind.
- raise "unimplemented search filter"
+ [@left.to_ber].to_ber_contextspecific(2)
end
end
- def unescape(right)
- right.gsub(/\\([a-fA-F\d]{2,2})/) do
- [$1.hex].pack("U")
- end
- end
-
-
- # Converts an LDAP search filter in BER format to an Net::LDAP::Filter
- # object. The incoming BER object most likely came to us by parsing an
- # LDAP searchRequest PDU.
- # Cf the comments under #to_ber, including the grammar snippet from the RFC.
- #--
- # We're hardcoding the BER constants from the RFC. Ought to break them out
- # into constants.
- #
- def Filter::parse_ber ber
- case ber.ber_identifier
- when 0xa0 # context-specific constructed 0, "and"
- ber.map {|b| Filter::parse_ber(b)}.inject {|memo,obj| memo & obj}
- when 0xa1 # context-specific constructed 1, "or"
- ber.map {|b| Filter::parse_ber(b)}.inject {|memo,obj| memo | obj}
- when 0xa2 # context-specific constructed 2, "not"
- ~ Filter::parse_ber( ber.first )
- when 0xa3 # context-specific constructed 3, "equalityMatch"
- if ber.last == "*"
- else
- Filter.eq( ber.first, ber.last )
- end
- when 0xa4 # context-specific constructed 4, "substring"
- str = ""
- final = false
- ber.last.each {|b|
- case b.ber_identifier
- when 0x80 # context-specific primitive 0, SubstringFilter "initial"
- raise "unrecognized substring filter, bad initial" if str.length > 0
- str += b
- when 0x81 # context-specific primitive 0, SubstringFilter "any"
- str += "*#{b}"
- when 0x82 # context-specific primitive 0, SubstringFilter "final"
- str += "*#{b}"
- final = true
- end
- }
- str += "*" unless final
- Filter.eq( ber.first.to_s, str )
- when 0xa5 # context-specific constructed 5, "greaterOrEqual"
- Filter.ge( ber.first.to_s, ber.last.to_s )
- when 0xa6 # context-specific constructed 5, "lessOrEqual"
- Filter.le( ber.first.to_s, ber.last.to_s )
- when 0x87 # context-specific primitive 7, "present"
- # call to_s to get rid of the BER-identifiedness of the incoming string.
- Filter.pres( ber.to_s )
- else
- raise "invalid BER tag-value (#{ber.ber_identifier}) in search filter"
- end
- end
-
-
- # Perform filter operations against a user-supplied block. This is useful when implementing
- # an LDAP directory server. The caller's block will be called with two arguments: first, a
- # symbol denoting the "operation" of the filter; and second, an array consisting of arguments
- # to the operation. The user-supplied block (which is MANDATORY) should perform some desired
- # application-defined processing, and may return a locally-meaningful object that will appear
- # as a parameter in the :and, :or and :not operations detailed below.
+ ##
+ # Perform filter operations against a user-supplied block. This is useful
+ # when implementing an LDAP directory server. The caller's block will be
+ # called with two arguments: first, a symbol denoting the "operation" of
+ # the filter; and second, an array consisting of arguments to the
+ # operation. The user-supplied block (which is MANDATORY) should perform
+ # some desired application-defined processing, and may return a
+ # locally-meaningful object that will appear as a parameter in the :and,
+ # :or and :not operations detailed below.
#
# A typical object to return from the user-supplied block is an array of
# Net::LDAP::Filter objects.
#
- # These are the possible values that may be passed to the user-supplied block:
- # :equalityMatch (the arguments will be an attribute name and a value to be matched);
- # :substrings (two arguments: an attribute name and a value containing one or more * characters);
- # :present (one argument: an attribute name);
- # :greaterOrEqual (two arguments: an attribute name and a value to be compared against);
- # :lessOrEqual (two arguments: an attribute name and a value to be compared against);
- # :and (two or more arguments, each of which is an object returned from a recursive call
- # to #execute, with the same block;
- # :or (two or more arguments, each of which is an object returned from a recursive call
- # to #execute, with the same block;
- # :not (one argument, which is an object returned from a recursive call to #execute with the
- # the same block.
- #
- def execute &block
+ # These are the possible values that may be passed to the user-supplied
+ # block:
+ # * :equalityMatch (the arguments will be an attribute name and a value
+ # to be matched);
+ # * :substrings (two arguments: an attribute name and a value containing
+ # one or more "*" characters);
+ # * :present (one argument: an attribute name);
+ # * :greaterOrEqual (two arguments: an attribute name and a value to be
+ # compared against);
+ # * :lessOrEqual (two arguments: an attribute name and a value to be
+ # compared against);
+ # * :and (two or more arguments, each of which is an object returned
+ # from a recursive call to #execute, with the same block;
+ # * :or (two or more arguments, each of which is an object returned from
+ # a recursive call to #execute, with the same block; and
+ # * :not (one argument, which is an object returned from a recursive
+ # call to #execute with the the same block.
+ def execute(&block)
case @op
when :eq
if @right == "*"
@@ -327,50 +449,27 @@ class Filter
end || []
end
-
- #--
- # coalesce
+ ##
# This is a private helper method for dealing with chains of ANDs and ORs
# that are longer than two. If BOTH of our branches are of the specified
# type of joining operator, then return both of them as an array (calling
# coalesce recursively). If they're not, then return an array consisting
# only of self.
- #
- def coalesce operator
+ def coalesce(operator) #:nodoc:
if @op == operator
- [@left.coalesce( operator ), @right.coalesce( operator )]
+ [@left.coalesce(operator), @right.coalesce(operator)]
else
[self]
end
end
-
-
- #--
- # We get a Ruby object which comes from parsing an RFC-1777 "Filter"
- # object. Convert it to a Net::LDAP::Filter.
- # TODO, we're hardcoding the RFC-1777 BER-encodings of the various
- # filter types. Could pull them out into a constant.
- #
- def Filter::parse_ldap_filter obj
- case obj.ber_identifier
- when 0x87 # present. context-specific primitive 7.
- Filter.eq( obj.to_s, "*" )
- when 0xa3 # equalityMatch. context-specific constructed 3.
- Filter.eq( obj[0], obj[1] )
- else
- raise LdapError.new( "unknown ldap search-filter type: #{obj.ber_identifier}" )
- end
- end
-
-
-
-
+ ##
#--
# We got a hash of attribute values.
# Do we match the attributes?
# Return T/F, and call match recursively as necessary.
- def match entry
+ #++
+ def match(entry)
case @op
when :eq
if @right == "*"
@@ -379,114 +478,139 @@ class Filter
l = entry[@left] and l = Array(l) and l.index(@right)
end
else
- raise LdapError.new( "unknown filter type in match: #{@op}" )
+ raise Net::LDAP::LdapError, "Unknown filter type in match: #{@op}"
end
end
- # Converts an LDAP filter-string (in the prefix syntax specified in RFC-2254)
- # to a Net::LDAP::Filter.
- def self.construct ldap_filter_string
- FilterParser.new(ldap_filter_string).filter
+ ##
+ # Converts escaped characters (e.g., "\\28") to unescaped characters
+ # ("(").
+ def unescape(right)
+ right.gsub(/\\([a-fA-F\d]{2})/) { [$1.hex].pack("U") }
end
+ private :unescape
- # Synonym for #construct.
- # to a Net::LDAP::Filter.
- def self.from_rfc2254 ldap_filter_string
- construct ldap_filter_string
- end
+ ##
+ # Parses RFC 2254-style string representations of LDAP filters into Filter
+ # object hierarchies.
+ class FilterParser #:nodoc:
+ ##
+ # The constructed filter.
+ attr_reader :filter
-end # class Net::LDAP::Filter
+ class << self
+ private :new
-
-
-class FilterParser #:nodoc:
-
- attr_reader :filter
-
- def initialize str
- @filter = parse( StringScanner.new( str )) or raise Net::LDAP::LdapError.new( "invalid filter syntax" )
- end
-
- def parse scanner
- parse_filter_branch(scanner) or parse_paren_expression(scanner)
- end
-
- def parse_paren_expression scanner
- if scanner.scan(/\s*\(\s*/)
- b = if scanner.scan(/\s*\&\s*/)
- a = nil
- branches = []
- while br = parse_paren_expression(scanner)
- branches << br
- end
- if branches.length >= 2
- a = branches.shift
- while branches.length > 0
- a = a & branches.shift
- end
- a
- end
- elsif scanner.scan(/\s*\|\s*/)
- # TODO: DRY!
- a = nil
- branches = []
- while br = parse_paren_expression(scanner)
- branches << br
- end
- if branches.length >= 2
- a = branches.shift
- while branches.length > 0
- a = a | branches.shift
- end
- a
- end
- elsif scanner.scan(/\s*\!\s*/)
- br = parse_paren_expression(scanner)
- if br
- ~ br
- end
- else
- parse_filter_branch( scanner )
- end
-
- if b and scanner.scan( /\s*\)\s*/ )
- b
+ ##
+ # Construct a filter tree from the provided string and return it.
+ def parse(ldap_filter_string)
+ new(ldap_filter_string).filter
end
end
- end
- # Added a greatly-augmented filter contributed by Andre Nathan
- # for detecting special characters in values. (15Aug06)
- # Added blanks to the attribute filter (26Oct06)
- def parse_filter_branch scanner
- scanner.scan(/\s*/)
- if token = scanner.scan( /[\w\-_]+/ )
+ def initialize(str)
+ require 'strscan' # Don't load strscan until we need it.
+ @filter = parse(StringScanner.new(str))
+ raise Net::LDAP::LdapError, "Invalid filter syntax." unless @filter
+ end
+
+ ##
+ # Parse the string contained in the StringScanner provided. Parsing
+ # tries to parse a standalone expression first. If that fails, it tries
+ # to parse a parenthesized expression.
+ def parse(scanner)
+ parse_filter_branch(scanner) or parse_paren_expression(scanner)
+ end
+ private :parse
+
+ ##
+ # Join ("&") and intersect ("|") operations are presented in branches.
+ # That is, the expression (&(test1)(test2) has two branches:
+ # test1 and test2. Each of these is parsed separately and then pushed
+ # into a branch array for filter merging using the parent operation.
+ #
+ # This method parses the branch text out into an array of filter
+ # objects.
+ def parse_branches(scanner)
+ branches = []
+ while branch = parse_paren_expression(scanner)
+ branches << branch
+ end
+ branches
+ end
+ private :parse_branches
+
+ ##
+ # Join ("&") and intersect ("|") operations are presented in branches.
+ # That is, the expression (&(test1)(test2) has two branches:
+ # test1 and test2. Each of these is parsed separately and then pushed
+ # into a branch array for filter merging using the parent operation.
+ #
+ # This method calls #parse_branches to generate the branch list and then
+ # merges them into a single Filter tree by calling the provided
+ # operation.
+ def merge_branches(op, scanner)
+ filter = nil
+ branches = parse_branches(scanner)
+
+ if branches.size >= 2
+ filter = branches.shift
+ while not branches.empty?
+ filter = filter.__send__(op, branches.shift)
+ end
+ end
+
+ filter
+ end
+ private :merge_branches
+
+ def parse_paren_expression(scanner)
+ if scanner.scan(/\s*\(\s*/)
+ expr = if scanner.scan(/\s*\&\s*/)
+ merge_branches(:&, scanner)
+ elsif scanner.scan(/\s*\|\s*/)
+ merge_branches(:|, scanner)
+ elsif scanner.scan(/\s*\!\s*/)
+ br = parse_paren_expression(scanner)
+ ~br if br
+ else
+ parse_filter_branch(scanner)
+ end
+
+ if expr and scanner.scan(/\s*\)\s*/)
+ expr
+ end
+ end
+ end
+ private :parse_paren_expression
+
+ ##
+ # This parses a given expression inside of parentheses.
+ def parse_filter_branch(scanner)
scanner.scan(/\s*/)
- if op = scanner.scan( /\=|\<\=|\<|\>\=|\>|\!\=/ )
+ if token = scanner.scan(/[-\w_]+/)
scanner.scan(/\s*/)
- #if value = scanner.scan( /[\w\*\.]+/ ) (ORG)
- #if value = scanner.scan( /[\w\*\.\+\-@=#\$%&! ]+/ ) (ff suggested by Kouhei Sutou
- if value = scanner.scan( /(?:[\w\*\.\+\-@=,#\$%&! ]|\\[a-fA-F\d]{2,2})+/ )
- case op
- when "="
- Filter.eq( token, value )
- when "!="
- Filter.ne( token, value )
- when "<"
- Filter.lt( token, value )
- when "<="
- Filter.le( token, value )
- when ">"
- Filter.gt( token, value )
- when ">="
- Filter.ge( token, value )
+ if op = scanner.scan(/<=|>=|!=|=/)
+ scanner.scan(/\s*/)
+ if value = scanner.scan(/(?:[-\w*.+@=,#\$%&!\s]|\\[a-fA-F\d]{2})+/)
+ # 20100313 AZ: Assumes that "(uid=george*)" is the same as
+ # "(uid=george* )". The standard doesn't specify, but I can find
+ # no examples that suggest otherwise.
+ value.strip!
+ case op
+ when "="
+ Net::LDAP::Filter.eq(token, value)
+ when "!="
+ Net::LDAP::Filter.ne(token, value)
+ when "<="
+ Net::LDAP::Filter.le(token, value)
+ when ">="
+ Net::LDAP::Filter.ge(token, value)
+ end
end
end
end
end
- end
-
-end # class Net::LDAP::FilterParser
-
-end # class Net::LDAP
-end # module Net
+ private :parse_filter_branch
+ end # class Net::LDAP::FilterParser
+end # class Net::LDAP::Filter