1 # $Id: filter.rb 151 2006-08-15 08:34:53Z blackhedd $
4 #----------------------------------------------------------------------------
6 # Copyright (C) 2006 by Francis Cianfrocca. All Rights Reserved.
10 # This program is free software; you can redistribute it and/or modify
11 # it under the terms of the GNU General Public License as published by
12 # the Free Software Foundation; either version 2 of the License, or
13 # (at your option) any later version.
15 # This program is distributed in the hope that it will be useful,
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 # GNU General Public License for more details.
20 # You should have received a copy of the GNU General Public License
21 # along with this program; if not, write to the Free Software
22 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
24 #---------------------------------------------------------------------------
33 # Class Net::LDAP::Filter is used to constrain
34 # LDAP searches. An object of this class is
35 # passed to Net::LDAP#search in the parameter :filter.
37 # Net::LDAP::Filter supports the complete set of search filters
38 # available in LDAP, including conjunction, disjunction and negation
39 # (AND, OR, and NOT). This class supplants the (infamous) RFC-2254
40 # standard notation for specifying LDAP search filters.
42 # Here's how to code the familiar "objectclass is present" filter:
43 # f = Net::LDAP::Filter.pres( "objectclass" )
44 # The object returned by this code can be passed directly to
45 # the <tt>:filter</tt> parameter of Net::LDAP#search.
47 # See the individual class and instance methods below for more examples.
51 def initialize op, a, b
57 # #eq creates a filter object indicating that the value of
58 # a paticular attribute must be either <i>present</i> or must
59 # match a particular string.
61 # To specify that an attribute is "present" means that only
62 # directory entries which contain a value for the particular
63 # attribute will be selected by the filter. This is useful
64 # in case of optional attributes such as <tt>mail.</tt>
65 # Presence is indicated by giving the value "*" in the second
66 # parameter to #eq. This example selects only entries that have
67 # one or more values for <tt>sAMAccountName:</tt>
68 # f = Net::LDAP::Filter.eq( "sAMAccountName", "*" )
70 # To match a particular range of values, pass a string as the
71 # second parameter to #eq. The string may contain one or more
72 # "*" characters as wildcards: these match zero or more occurrences
73 # of any character. Full regular-expressions are <i>not</i> supported
74 # due to limitations in the underlying LDAP protocol.
75 # This example selects any entry with a <tt>mail</tt> value containing
76 # the substring "anderson":
77 # f = Net::LDAP::Filter.eq( "mail", "*anderson*" )
79 # Removed gt and lt. They ain't in the standard!
81 def Filter::eq attribute, value; Filter.new :eq, attribute, value; end
82 def Filter::ne attribute, value; Filter.new :ne, attribute, value; end
83 #def Filter::gt attribute, value; Filter.new :gt, attribute, value; end
84 #def Filter::lt attribute, value; Filter.new :lt, attribute, value; end
85 def Filter::ge attribute, value; Filter.new :ge, attribute, value; end
86 def Filter::le attribute, value; Filter.new :le, attribute, value; end
88 # #pres( attribute ) is a synonym for #eq( attribute, "*" )
90 def Filter::pres attribute; Filter.eq attribute, "*"; end
92 # operator & ("AND") is used to conjoin two or more filters.
93 # This expression will select only entries that have an <tt>objectclass</tt>
94 # attribute AND have a <tt>mail</tt> attribute that begins with "George":
95 # f = Net::LDAP::Filter.pres( "objectclass" ) & Net::LDAP::Filter.eq( "mail", "George*" )
97 def & filter; Filter.new :and, self, filter; end
99 # operator | ("OR") is used to disjoin two or more filters.
100 # This expression will select entries that have either an <tt>objectclass</tt>
101 # attribute OR a <tt>mail</tt> attribute that begins with "George":
102 # f = Net::LDAP::Filter.pres( "objectclass" ) | Net::LDAP::Filter.eq( "mail", "George*" )
104 def | filter; Filter.new :or, self, filter; end
108 # operator ~ ("NOT") is used to negate a filter.
109 # This expression will select only entries that <i>do not</i> have an <tt>objectclass</tt>
111 # f = ~ Net::LDAP::Filter.pres( "objectclass" )
114 # This operator can't be !, evidently. Try it.
115 # Removed GT and LT. They're not in the RFC.
116 def ~@; Filter.new :not, self, nil; end
122 "(!(#{@left}=#{@right}))"
124 "(#{@left}=#{@right})"
126 # "#{@left}>#{@right}"
128 # "#{@left}<#{@right}"
130 "#{@left}>=#{@right}"
132 "#{@left}<=#{@right}"
134 "(&(#{@left})(#{@right}))"
136 "(|(#{@left})(#{@right}))"
140 raise "invalid or unsupported operator in LDAP Filter"
149 # and [0] SET OF Filter,
150 # or [1] SET OF Filter,
152 # equalityMatch [3] AttributeValueAssertion,
153 # substrings [4] SubstringFilter,
154 # greaterOrEqual [5] AttributeValueAssertion,
155 # lessOrEqual [6] AttributeValueAssertion,
156 # present [7] AttributeType,
157 # approxMatch [8] AttributeValueAssertion
162 # type AttributeType,
163 # SEQUENCE OF CHOICE {
164 # initial [0] LDAPString,
165 # any [1] LDAPString,
166 # final [2] LDAPString
170 # Parsing substrings is a little tricky.
171 # We use the split method to break a string into substrings
172 # delimited by the * (star) character. But we also need
173 # to know whether there is a star at the head and tail
174 # of the string. A Ruby particularity comes into play here:
175 # if you split on * and the first character of the string is
176 # a star, then split will return an array whose first element
177 # is an _empty_ string. But if the _last_ character of the
178 # string is star, then split will return an array that does
179 # _not_ add an empty string at the end. So we have to deal
180 # with all that specifically.
185 if @right == "*" # present
186 @left.to_s.to_ber_contextspecific 7
187 elsif @right =~ /[\*]/ #substring
188 ary = @right.split( /[\*]+/ )
189 final_star = @right =~ /[\*]$/
190 initial_star = ary.first == "" and ary.shift
194 seq << ary.shift.to_ber_contextspecific(0)
196 n_any_strings = ary.length - (final_star ? 0 : 1)
198 n_any_strings.times {
199 seq << ary.shift.to_ber_contextspecific(1)
202 seq << ary.shift.to_ber_contextspecific(2)
204 [@left.to_s.to_ber, seq.to_ber].to_ber_contextspecific 4
206 [@left.to_s.to_ber, @right.to_ber].to_ber_contextspecific 3
209 [@left.to_s.to_ber, @right.to_ber].to_ber_contextspecific 5
211 [@left.to_s.to_ber, @right.to_ber].to_ber_contextspecific 6
213 ary = [@left.coalesce(:and), @right.coalesce(:and)].flatten
214 ary.map {|a| a.to_ber}.to_ber_contextspecific( 0 )
216 ary = [@left.coalesce(:or), @right.coalesce(:or)].flatten
217 ary.map {|a| a.to_ber}.to_ber_contextspecific( 1 )
219 [@left.to_ber].to_ber_contextspecific 2
221 # ERROR, we'll return objectclass=* to keep things from blowing up,
222 # but that ain't a good answer and we need to kick out an error of some kind.
223 raise "unimplemented search filter"
229 # This is a private helper method for dealing with chains of ANDs and ORs
230 # that are longer than two. If BOTH of our branches are of the specified
231 # type of joining operator, then return both of them as an array (calling
232 # coalesce recursively). If they're not, then return an array consisting
235 def coalesce operator
237 [@left.coalesce( operator ), @right.coalesce( operator )]
246 # We get a Ruby object which comes from parsing an RFC-1777 "Filter"
247 # object. Convert it to a Net::LDAP::Filter.
248 # TODO, we're hardcoding the RFC-1777 BER-encodings of the various
249 # filter types. Could pull them out into a constant.
251 def Filter::parse_ldap_filter obj
252 case obj.ber_identifier
253 when 0x87 # present. context-specific primitive 7.
254 Filter.eq( obj.to_s, "*" )
255 when 0xa3 # equalityMatch. context-specific constructed 3.
256 Filter.eq( obj[0], obj[1] )
258 raise LdapError.new( "unknown ldap search-filter type: #{obj.ber_identifier}" )
264 # We got a hash of attribute values.
265 # Do we match the attributes?
266 # Return T/F, and call match recursively as necessary.
271 l = entry[@left] and l.length > 0
273 l = entry[@left] and l = l.to_a and l.index(@right)
276 raise LdapError.new( "unknown filter type in match: #{@op}" )
280 # Converts an LDAP filter-string (in the prefix syntax specified in RFC-2254)
281 # to a Net::LDAP::Filter.
282 def self.construct ldap_filter_string
283 FilterParser.new(ldap_filter_string).filter
286 # Synonym for #construct.
287 # to a Net::LDAP::Filter.
288 def self.from_rfc2254 ldap_filter_string
289 construct ldap_filter_string
292 end # class Net::LDAP::Filter
296 class FilterParser #:nodoc:
302 @filter = parse( StringScanner.new( str )) or raise Net::LDAP::LdapError.new( "invalid filter syntax" )
306 parse_filter_branch(scanner) or parse_paren_expression(scanner)
309 def parse_paren_expression scanner
310 if scanner.scan(/\s*\(\s*/)
311 b = if scanner.scan(/\s*\&\s*/)
314 while br = parse_paren_expression(scanner)
317 if branches.length >= 2
319 while branches.length > 0
320 a = a & branches.shift
324 elsif scanner.scan(/\s*\|\s*/)
328 while br = parse_paren_expression(scanner)
331 if branches.length >= 2
333 while branches.length > 0
334 a = a | branches.shift
338 elsif scanner.scan(/\s*\!\s*/)
339 br = parse_paren_expression(scanner)
344 parse_filter_branch( scanner )
347 if b and scanner.scan( /\s*\)\s*/ )
353 # Added a greatly-augmented filter contributed by Andre Nathan
354 # for detecting special characters in values. (15Aug06)
355 def parse_filter_branch scanner
357 if token = scanner.scan( /[\w\-_]+/ )
359 if op = scanner.scan( /\=|\<\=|\<|\>\=|\>|\!\=/ )
361 #if value = scanner.scan( /[\w\*\.]+/ ) (ORG)
362 if value = scanner.scan( /[\w\*\.\+\-@=#\$%&!]+/ )
365 Filter.eq( token, value )
367 Filter.ne( token, value )
369 Filter.lt( token, value )
371 Filter.le( token, value )
373 Filter.gt( token, value )
375 Filter.ge( token, value )
382 end # class Net::LDAP::FilterParser
384 end # class Net::LDAP