scashin133-net-ldap 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,720 @@
1
+ # Copyright (C) 2006 by Francis Cianfrocca and other contributors. All
2
+ # Rights Reserved.
3
+ #
4
+ # Gmail: garbagecat10
5
+ #
6
+ # This program is free software; you can redistribute it and/or modify it
7
+ # under the terms of the GNU General Public License as published by the Free
8
+ # Software Foundation; either version 2 of the License, or (at your option)
9
+ # any later version.
10
+ #
11
+ # This program is distributed in the hope that it will be useful, but
12
+ # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
13
+ # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14
+ # for more details.
15
+ #
16
+ # You should have received a copy of the GNU General Public License along
17
+ # with this program; if not, write to:
18
+ # Free Software Foundation, Inc.
19
+ # 51 Franklin St, Fifth Floor
20
+ # Boston, MA 02110-1301
21
+ # USA
22
+
23
+ ##
24
+ # Class Net::LDAP::Filter is used to constrain LDAP searches. An object of
25
+ # this class is passed to Net::LDAP#search in the parameter :filter.
26
+ #
27
+ # Net::LDAP::Filter supports the complete set of search filters available in
28
+ # LDAP, including conjunction, disjunction and negation (AND, OR, and NOT).
29
+ # This class supplants the (infamous) RFC 2254 standard notation for
30
+ # specifying LDAP search filters.
31
+ #--
32
+ # NOTE: This wording needs to change as we will be supporting LDAPv3 search
33
+ # filter strings (RFC 4515).
34
+ #++
35
+ #
36
+ # Here's how to code the familiar "objectclass is present" filter:
37
+ # f = Net::LDAP::Filter.present("objectclass")
38
+ #
39
+ # The object returned by this code can be passed directly to the
40
+ # <tt>:filter</tt> parameter of Net::LDAP#search.
41
+ #
42
+ # See the individual class and instance methods below for more examples.
43
+ class Net::LDAP::Filter
44
+ ##
45
+ # Known filter types.
46
+ FilterTypes = [ :ne, :eq, :ge, :le, :and, :or, :not, :ex ]
47
+
48
+ def initialize(op, left, right) #:nodoc:
49
+ unless FilterTypes.include?(op)
50
+ raise Net::LDAP::LdapError, "Invalid or unsupported operator #{op.inspect} in LDAP Filter."
51
+ end
52
+ @op = op
53
+ @left = left
54
+ @right = right
55
+ end
56
+
57
+ class << self
58
+ # We don't want filters created except using our custom constructors.
59
+ private :new
60
+
61
+ ##
62
+ # Creates a Filter object indicating that the value of a particular
63
+ # attribute must either be present or match a particular string.
64
+ #
65
+ # Specifying that an attribute is 'present' means only directory entries
66
+ # which contain a value for the particular attribute will be selected by
67
+ # the filter. This is useful in case of optional attributes such as
68
+ # <tt>mail.</tt> Presence is indicated by giving the value "*" in the
69
+ # second parameter to #eq. This example selects only entries that have
70
+ # one or more values for <tt>sAMAccountName:</tt>
71
+ #
72
+ # f = Net::LDAP::Filter.eq("sAMAccountName", "*")
73
+ #
74
+ # To match a particular range of values, pass a string as the second
75
+ # parameter to #eq. The string may contain one or more "*" characters as
76
+ # wildcards: these match zero or more occurrences of any character. Full
77
+ # regular-expressions are <i>not</i> supported due to limitations in the
78
+ # underlying LDAP protocol. This example selects any entry with a
79
+ # <tt>mail</tt> value containing the substring "anderson":
80
+ #
81
+ # f = Net::LDAP::Filter.eq("mail", "*anderson*")
82
+ def eq(attribute, value)
83
+ new(:eq, attribute, value)
84
+ end
85
+
86
+ ##
87
+ # Creates a Filter object indicating extensible comparison. This Filter
88
+ # object is currently considered EXPERIMENTAL.
89
+ #
90
+ # sample_attributes = ['cn:fr', 'cn:fr.eq',
91
+ # 'cn:1.3.6.1.4.1.42.2.27.9.4.49.1.3', 'cn:dn:fr', 'cn:dn:fr.eq']
92
+ # attr = sample_attributes.first # Pick an extensible attribute
93
+ # value = 'roberts'
94
+ #
95
+ # filter = "#{attr}:=#{value}" # Basic String Filter
96
+ # filter = Net::LDAP::Filter.ex(attr, value) # Net::LDAP::Filter
97
+ #
98
+ # # Perform a search with the Extensible Match Filter
99
+ # Net::LDAP.search(:filter => filter)
100
+ #--
101
+ # The LDIF required to support the above examples on the OpenDS LDAP
102
+ # server:
103
+ #
104
+ # version: 1
105
+ #
106
+ # dn: dc=example,dc=com
107
+ # objectClass: domain
108
+ # objectClass: top
109
+ # dc: example
110
+ #
111
+ # dn: ou=People,dc=example,dc=com
112
+ # objectClass: organizationalUnit
113
+ # objectClass: top
114
+ # ou: People
115
+ #
116
+ # dn: uid=1,ou=People,dc=example,dc=com
117
+ # objectClass: person
118
+ # objectClass: organizationalPerson
119
+ # objectClass: inetOrgPerson
120
+ # objectClass: top
121
+ # cn:: csO0YsOpcnRz
122
+ # sn:: YsO0YiByw7Riw6lydHM=
123
+ # givenName:: YsO0Yg==
124
+ # uid: 1
125
+ #
126
+ # =Refs:
127
+ # * http://www.ietf.org/rfc/rfc2251.txt
128
+ # * http://www.novell.com/documentation/edir88/edir88/?page=/documentation/edir88/edir88/data/agazepd.html
129
+ # * https://docs.opends.org/2.0/page/SearchingUsingInternationalCollationRules
130
+ #++
131
+ def ex(attribute, value)
132
+ new(:ex, attribute, value)
133
+ end
134
+
135
+ ##
136
+ # Creates a Filter object indicating that a particular attribute value
137
+ # is either not present or does not match a particular string; see
138
+ # Filter::eq for more information.
139
+ def ne(attribute, value)
140
+ new(:ne, attribute, value)
141
+ end
142
+
143
+ ##
144
+ # Creates a Filter object indicating that a particular attribute value
145
+ # is greater than or equal to the specified value.
146
+ def ge(attribute, value)
147
+ new(:ge, attribute, value)
148
+ end
149
+
150
+ ##
151
+ # Creates a Filter object indicating that a particular attribute value
152
+ # is less than or equal to the specified value.
153
+ def le(attribute, value)
154
+ new(:le, attribute, value)
155
+ end
156
+
157
+ ##
158
+ # Joins two or more filters so that all conditions must be true. Calling
159
+ # <tt>Filter.join(left, right)</tt> is the same as <tt>left &
160
+ # right</tt>.
161
+ #
162
+ # # Selects only entries that have an <tt>objectclass</tt> attribute.
163
+ # x = Net::LDAP::Filter.present("objectclass")
164
+ # # Selects only entries that have a <tt>mail</tt> attribute that begins
165
+ # # with "George".
166
+ # y = Net::LDAP::Filter.eq("mail", "George*")
167
+ # # Selects only entries that meet both conditions above.
168
+ # z = Net::LDAP::Filter.join(x, y)
169
+ def join(left, right)
170
+ new(:and, left, right)
171
+ end
172
+
173
+ ##
174
+ # Creates a disjoint comparison between two or more filters. Selects
175
+ # entries where either the left or right side are true. Calling
176
+ # <tt>Filter.intersect(left, right)</tt> is the same as <tt>left |
177
+ # right</tt>.
178
+ #
179
+ # # Selects only entries that have an <tt>objectclass</tt> attribute.
180
+ # x = Net::LDAP::Filter.present("objectclass")
181
+ # # Selects only entries that have a <tt>mail</tt> attribute that begins
182
+ # # with "George".
183
+ # y = Net::LDAP::Filter.eq("mail", "George*")
184
+ # # Selects only entries that meet either condition above.
185
+ # z = x | y
186
+ def intersect(left, right)
187
+ new(:or, left, right)
188
+ end
189
+
190
+ ##
191
+ # Negates a filter. Calling <tt>Fitler.negate(filter)</tt> i s the same
192
+ # as <tt>~filter</tt>.
193
+ #
194
+ # # Selects only entries that do not have an <tt>objectclass</tt>
195
+ # # attribute.
196
+ # x = ~Net::LDAP::Filter.present("objectclass")
197
+ def negate(filter)
198
+ new(:not, filter, nil)
199
+ end
200
+
201
+ ##
202
+ # This is a synonym for #eq(attribute, "*"). Also known as #present and
203
+ # #pres.
204
+ def present?(attribute)
205
+ eq(attribute, "*")
206
+ end
207
+ alias_method :present, :present?
208
+ alias_method :pres, :present?
209
+
210
+ ##
211
+ # Converts an LDAP search filter in BER format to an Net::LDAP::Filter
212
+ # object. The incoming BER object most likely came to us by parsing an
213
+ # LDAP searchRequest PDU. See also the comments under #to_ber, including
214
+ # the grammar snippet from the RFC.
215
+ #--
216
+ # We're hardcoding the BER constants from the RFC. These should be
217
+ # broken out insto constants.
218
+ def parse_ber(ber)
219
+ case ber.ber_identifier
220
+ when 0xa0 # context-specific constructed 0, "and"
221
+ ber.map { |b| parse_ber(b) }.inject { |memo, obj| memo & obj }
222
+ when 0xa1 # context-specific constructed 1, "or"
223
+ ber.map { |b| parse_ber(b) }.inject { |memo, obj| memo | obj }
224
+ when 0xa2 # context-specific constructed 2, "not"
225
+ ~parse_ber(ber.first)
226
+ when 0xa3 # context-specific constructed 3, "equalityMatch"
227
+ if ber.last == "*"
228
+ else
229
+ eq(ber.first, ber.last)
230
+ end
231
+ when 0xa4 # context-specific constructed 4, "substring"
232
+ str = ""
233
+ final = false
234
+ ber.last.each { |b|
235
+ case b.ber_identifier
236
+ when 0x80 # context-specific primitive 0, SubstringFilter "initial"
237
+ raise Net::LDAP::LdapError, "Unrecognized substring filter; bad initial value." if str.length > 0
238
+ str += b
239
+ when 0x81 # context-specific primitive 0, SubstringFilter "any"
240
+ str += "*#{b}"
241
+ when 0x82 # context-specific primitive 0, SubstringFilter "final"
242
+ str += "*#{b}"
243
+ final = true
244
+ end
245
+ }
246
+ str += "*" unless final
247
+ eq(ber.first.to_s, str)
248
+ when 0xa5 # context-specific constructed 5, "greaterOrEqual"
249
+ ge(ber.first.to_s, ber.last.to_s)
250
+ when 0xa6 # context-specific constructed 6, "lessOrEqual"
251
+ le(ber.first.to_s, ber.last.to_s)
252
+ when 0x87 # context-specific primitive 7, "present"
253
+ # call to_s to get rid of the BER-identifiedness of the incoming string.
254
+ present?(ber.to_s)
255
+ when 0xa9 # context-specific constructed 9, "extensible comparison"
256
+ raise Net::LDAP::LdapError, "Invalid extensible search filter, should be at least two elements" if ber.size<2
257
+
258
+ # Reassembles the extensible filter parts
259
+ # (["sn", "2.4.6.8.10", "Barbara Jones", '1'])
260
+ type = value = dn = rule = nil
261
+ ber.each do |element|
262
+ case element.ber_identifier
263
+ when 0x81 then rule=element
264
+ when 0x82 then type=element
265
+ when 0x83 then value=element
266
+ when 0x84 then dn='dn'
267
+ end
268
+ end
269
+
270
+ attribute = ''
271
+ attribute << type if type
272
+ attribute << ":#{dn}" if dn
273
+ attribute << ":#{rule}" if rule
274
+
275
+ ex(attribute, value)
276
+ else
277
+ raise Net::LDAP::LdapError, "Invalid BER tag-value (#{ber.ber_identifier}) in search filter."
278
+ end
279
+ end
280
+
281
+ ##
282
+ # Converts an LDAP filter-string (in the prefix syntax specified in RFC-2254)
283
+ # to a Net::LDAP::Filter.
284
+ def construct(ldap_filter_string)
285
+ FilterParser.parse(ldap_filter_string)
286
+ end
287
+ alias_method :from_rfc2254, :construct
288
+ alias_method :from_rfc4515, :construct
289
+
290
+ ##
291
+ # Convert an RFC-1777 LDAP/BER "Filter" object to a Net::LDAP::Filter
292
+ # object.
293
+ #--
294
+ # TODO, we're hardcoding the RFC-1777 BER-encodings of the various
295
+ # filter types. Could pull them out into a constant.
296
+ #++
297
+ def parse_ldap_filter(obj)
298
+ case obj.ber_identifier
299
+ when 0x87 # present. context-specific primitive 7.
300
+ eq(obj.to_s, "*")
301
+ when 0xa3 # equalityMatch. context-specific constructed 3.
302
+ eq(obj[0], obj[1])
303
+ else
304
+ raise Net::LDAP::LdapError, "Unknown LDAP search-filter type: #{obj.ber_identifier}"
305
+ end
306
+ end
307
+ end
308
+
309
+ ##
310
+ # Joins two or more filters so that all conditions must be true.
311
+ #
312
+ # # Selects only entries that have an <tt>objectclass</tt> attribute.
313
+ # x = Net::LDAP::Filter.present("objectclass")
314
+ # # Selects only entries that have a <tt>mail</tt> attribute that begins
315
+ # # with "George".
316
+ # y = Net::LDAP::Filter.eq("mail", "George*")
317
+ # # Selects only entries that meet both conditions above.
318
+ # z = x & y
319
+ def &(filter)
320
+ self.class.join(self, filter)
321
+ end
322
+
323
+ ##
324
+ # Creates a disjoint comparison between two or more filters. Selects
325
+ # entries where either the left or right side are true.
326
+ #
327
+ # # Selects only entries that have an <tt>objectclass</tt> attribute.
328
+ # x = Net::LDAP::Filter.present("objectclass")
329
+ # # Selects only entries that have a <tt>mail</tt> attribute that begins
330
+ # # with "George".
331
+ # y = Net::LDAP::Filter.eq("mail", "George*")
332
+ # # Selects only entries that meet either condition above.
333
+ # z = x | y
334
+ def |(filter)
335
+ self.class.intersect(self, filter)
336
+ end
337
+
338
+ ##
339
+ # Negates a filter.
340
+ #
341
+ # # Selects only entries that do not have an <tt>objectclass</tt>
342
+ # # attribute.
343
+ # x = ~Net::LDAP::Filter.present("objectclass")
344
+ def ~@
345
+ self.class.negate(self)
346
+ end
347
+
348
+ ##
349
+ # Equality operator for filters, useful primarily for constructing unit tests.
350
+ def ==(filter)
351
+ # 20100320 AZ: We need to come up with a better way of doing this. This
352
+ # is just nasty.
353
+ str = "[@op,@left,@right]"
354
+ self.instance_eval(str) == filter.instance_eval(str)
355
+ end
356
+
357
+ def to_raw_rfc2254
358
+ case @op
359
+ when :ne
360
+ "!(#{@left}=#{@right})"
361
+ when :eq
362
+ "#{@left}=#{@right}"
363
+ when :ex
364
+ "#{@left}:=#{@right}"
365
+ when :ge
366
+ "#{@left}>=#{@right}"
367
+ when :le
368
+ "#{@left}<=#{@right}"
369
+ when :and
370
+ "&(#{@left.to_raw_rfc2254})(#{@right.to_raw_rfc2254})"
371
+ when :or
372
+ "|(#{@left.to_raw_rfc2254})(#{@right.to_raw_rfc2254})"
373
+ when :not
374
+ "!(#{@left.to_raw_rfc2254})"
375
+ end
376
+ end
377
+
378
+ ##
379
+ # Converts the Filter object to an RFC 2254-compatible text format.
380
+ def to_rfc2254
381
+ "(#{to_raw_rfc2254})"
382
+ end
383
+
384
+ def to_s
385
+ to_rfc2254
386
+ end
387
+
388
+ ##
389
+ # Converts the filter to BER format.
390
+ #--
391
+ # Filter ::=
392
+ # CHOICE {
393
+ # and [0] SET OF Filter,
394
+ # or [1] SET OF Filter,
395
+ # not [2] Filter,
396
+ # equalityMatch [3] AttributeValueAssertion,
397
+ # substrings [4] SubstringFilter,
398
+ # greaterOrEqual [5] AttributeValueAssertion,
399
+ # lessOrEqual [6] AttributeValueAssertion,
400
+ # present [7] AttributeType,
401
+ # approxMatch [8] AttributeValueAssertion,
402
+ # extensibleMatch [9] MatchingRuleAssertion
403
+ # }
404
+ #
405
+ # SubstringFilter ::=
406
+ # SEQUENCE {
407
+ # type AttributeType,
408
+ # SEQUENCE OF CHOICE {
409
+ # initial [0] LDAPString,
410
+ # any [1] LDAPString,
411
+ # final [2] LDAPString
412
+ # }
413
+ # }
414
+ #
415
+ # MatchingRuleAssertion ::=
416
+ # SEQUENCE {
417
+ # matchingRule [1] MatchingRuleId OPTIONAL,
418
+ # type [2] AttributeDescription OPTIONAL,
419
+ # matchValue [3] AssertionValue,
420
+ # dnAttributes [4] BOOLEAN DEFAULT FALSE
421
+ # }
422
+ #
423
+ # Matching Rule Suffixes
424
+ # Less than [.1] or .[lt]
425
+ # Less than or equal to [.2] or [.lte]
426
+ # Equality [.3] or [.eq] (default)
427
+ # Greater than or equal to [.4] or [.gte]
428
+ # Greater than [.5] or [.gt]
429
+ # Substring [.6] or [.sub]
430
+ #
431
+ #++
432
+ def to_ber
433
+ case @op
434
+ when :eq
435
+ if @right == "*" # presence test
436
+ @left.to_s.to_ber_contextspecific(7)
437
+ elsif @right =~ /[*]/ # substring
438
+ # Parsing substrings is a little tricky. We use String#split to
439
+ # break a string into substrings delimited by the * (star)
440
+ # character. But we also need to know whether there is a star at the
441
+ # head and tail of the string, so we use a limit parameter value of
442
+ # -1: "If negative, there is no limit to the number of fields
443
+ # returned, and trailing null fields are not suppressed."
444
+ #
445
+ # 20100320 AZ: This is much simpler than the previous verison. Also,
446
+ # unnecessary regex escaping has been removed.
447
+
448
+ ary = @right.split(/[*]+/, -1)
449
+
450
+ if ary.first.empty?
451
+ first = nil
452
+ ary.shift
453
+ else
454
+ first = ary.shift.to_ber_contextspecific(0)
455
+ end
456
+
457
+ if ary.last.empty?
458
+ last = nil
459
+ ary.pop
460
+ else
461
+ last = ary.pop.to_ber_contextspecific(2)
462
+ end
463
+
464
+ seq = ary.map { |e| e.to_ber_contextspecific(1) }
465
+ seq.unshift first if first
466
+ seq.push last if last
467
+
468
+ [@left.to_s.to_ber, seq.to_ber].to_ber_contextspecific(4)
469
+ else # equality
470
+ [@left.to_s.to_ber, unescape(@right).to_ber].to_ber_contextspecific(3)
471
+ end
472
+ when :ex
473
+ seq = []
474
+
475
+ unless @left =~ /^([-;\d\w]*)(:dn)?(:(\w+|[.\d\w]+))?$/
476
+ raise Net::LDAP::LdapError, "Bad attribute #{@left}"
477
+ end
478
+ type, dn, rule = $1, $2, $4
479
+
480
+ seq << rule.to_ber_contextspecific(1) unless rule.to_s.empty? # matchingRule
481
+ seq << type.to_ber_contextspecific(2) unless type.to_s.empty? # type
482
+ seq << unescape(@right).to_ber_contextspecific(3) # matchingValue
483
+ seq << "1".to_ber_contextspecific(4) unless dn.to_s.empty? # dnAttributes
484
+
485
+ seq.to_ber_contextspecific(9)
486
+ when :ge
487
+ [@left.to_s.to_ber, unescape(@right).to_ber].to_ber_contextspecific(5)
488
+ when :le
489
+ [@left.to_s.to_ber, unescape(@right).to_ber].to_ber_contextspecific(6)
490
+ when :ne
491
+ [self.class.eq(@left, @right).to_ber].to_ber_contextspecific(2)
492
+ when :and
493
+ ary = [@left.coalesce(:and), @right.coalesce(:and)].flatten
494
+ ary.map {|a| a.to_ber}.to_ber_contextspecific(0)
495
+ when :or
496
+ ary = [@left.coalesce(:or), @right.coalesce(:or)].flatten
497
+ ary.map {|a| a.to_ber}.to_ber_contextspecific(1)
498
+ when :not
499
+ [@left.to_ber].to_ber_contextspecific(2)
500
+ end
501
+ end
502
+
503
+ ##
504
+ # Perform filter operations against a user-supplied block. This is useful
505
+ # when implementing an LDAP directory server. The caller's block will be
506
+ # called with two arguments: first, a symbol denoting the "operation" of
507
+ # the filter; and second, an array consisting of arguments to the
508
+ # operation. The user-supplied block (which is MANDATORY) should perform
509
+ # some desired application-defined processing, and may return a
510
+ # locally-meaningful object that will appear as a parameter in the :and,
511
+ # :or and :not operations detailed below.
512
+ #
513
+ # A typical object to return from the user-supplied block is an array of
514
+ # Net::LDAP::Filter objects.
515
+ #
516
+ # These are the possible values that may be passed to the user-supplied
517
+ # block:
518
+ # * :equalityMatch (the arguments will be an attribute name and a value
519
+ # to be matched);
520
+ # * :substrings (two arguments: an attribute name and a value containing
521
+ # one or more "*" characters);
522
+ # * :present (one argument: an attribute name);
523
+ # * :greaterOrEqual (two arguments: an attribute name and a value to be
524
+ # compared against);
525
+ # * :lessOrEqual (two arguments: an attribute name and a value to be
526
+ # compared against);
527
+ # * :and (two or more arguments, each of which is an object returned
528
+ # from a recursive call to #execute, with the same block;
529
+ # * :or (two or more arguments, each of which is an object returned from
530
+ # a recursive call to #execute, with the same block; and
531
+ # * :not (one argument, which is an object returned from a recursive
532
+ # call to #execute with the the same block.
533
+ def execute(&block)
534
+ case @op
535
+ when :eq
536
+ if @right == "*"
537
+ yield :present, @left
538
+ elsif @right.index '*'
539
+ yield :substrings, @left, @right
540
+ else
541
+ yield :equalityMatch, @left, @right
542
+ end
543
+ when :ge
544
+ yield :greaterOrEqual, @left, @right
545
+ when :le
546
+ yield :lessOrEqual, @left, @right
547
+ when :or, :and
548
+ yield @op, (@left.execute(&block)), (@right.execute(&block))
549
+ when :not
550
+ yield @op, (@left.execute(&block))
551
+ end || []
552
+ end
553
+
554
+ ##
555
+ # This is a private helper method for dealing with chains of ANDs and ORs
556
+ # that are longer than two. If BOTH of our branches are of the specified
557
+ # type of joining operator, then return both of them as an array (calling
558
+ # coalesce recursively). If they're not, then return an array consisting
559
+ # only of self.
560
+ def coalesce(operator) #:nodoc:
561
+ if @op == operator
562
+ [@left.coalesce(operator), @right.coalesce(operator)]
563
+ else
564
+ [self]
565
+ end
566
+ end
567
+
568
+ ##
569
+ #--
570
+ # We got a hash of attribute values.
571
+ # Do we match the attributes?
572
+ # Return T/F, and call match recursively as necessary.
573
+ #++
574
+ def match(entry)
575
+ case @op
576
+ when :eq
577
+ if @right == "*"
578
+ l = entry[@left] and l.length > 0
579
+ else
580
+ l = entry[@left] and l = Array(l) and l.index(@right)
581
+ end
582
+ else
583
+ raise Net::LDAP::LdapError, "Unknown filter type in match: #{@op}"
584
+ end
585
+ end
586
+
587
+ ##
588
+ # Converts escaped characters (e.g., "\\28") to unescaped characters
589
+ # ("(").
590
+ def unescape(right)
591
+ right.gsub(/\\([a-fA-F\d]{2})/) { [$1.hex].pack("U") }
592
+ end
593
+ private :unescape
594
+
595
+ ##
596
+ # Parses RFC 2254-style string representations of LDAP filters into Filter
597
+ # object hierarchies.
598
+ class FilterParser #:nodoc:
599
+ ##
600
+ # The constructed filter.
601
+ attr_reader :filter
602
+
603
+ class << self
604
+ private :new
605
+
606
+ ##
607
+ # Construct a filter tree from the provided string and return it.
608
+ def parse(ldap_filter_string)
609
+ new(ldap_filter_string).filter
610
+ end
611
+ end
612
+
613
+ def initialize(str)
614
+ require 'strscan' # Don't load strscan until we need it.
615
+ @filter = parse(StringScanner.new(str))
616
+ raise Net::LDAP::LdapError, "Invalid filter syntax." unless @filter
617
+ end
618
+
619
+ ##
620
+ # Parse the string contained in the StringScanner provided. Parsing
621
+ # tries to parse a standalone expression first. If that fails, it tries
622
+ # to parse a parenthesized expression.
623
+ def parse(scanner)
624
+ parse_filter_branch(scanner) or parse_paren_expression(scanner)
625
+ end
626
+ private :parse
627
+
628
+ ##
629
+ # Join ("&") and intersect ("|") operations are presented in branches.
630
+ # That is, the expression <tt>(&(test1)(test2)</tt> has two branches:
631
+ # test1 and test2. Each of these is parsed separately and then pushed
632
+ # into a branch array for filter merging using the parent operation.
633
+ #
634
+ # This method parses the branch text out into an array of filter
635
+ # objects.
636
+ def parse_branches(scanner)
637
+ branches = []
638
+ while branch = parse_paren_expression(scanner)
639
+ branches << branch
640
+ end
641
+ branches
642
+ end
643
+ private :parse_branches
644
+
645
+ ##
646
+ # Join ("&") and intersect ("|") operations are presented in branches.
647
+ # That is, the expression <tt>(&(test1)(test2)</tt> has two branches:
648
+ # test1 and test2. Each of these is parsed separately and then pushed
649
+ # into a branch array for filter merging using the parent operation.
650
+ #
651
+ # This method calls #parse_branches to generate the branch list and then
652
+ # merges them into a single Filter tree by calling the provided
653
+ # operation.
654
+ def merge_branches(op, scanner)
655
+ filter = nil
656
+ branches = parse_branches(scanner)
657
+
658
+ if branches.size >= 2
659
+ filter = branches.shift
660
+ while not branches.empty?
661
+ filter = filter.__send__(op, branches.shift)
662
+ end
663
+ end
664
+
665
+ filter
666
+ end
667
+ private :merge_branches
668
+
669
+ def parse_paren_expression(scanner)
670
+ if scanner.scan(/\s*\(\s*/)
671
+ expr = if scanner.scan(/\s*\&\s*/)
672
+ merge_branches(:&, scanner)
673
+ elsif scanner.scan(/\s*\|\s*/)
674
+ merge_branches(:|, scanner)
675
+ elsif scanner.scan(/\s*\!\s*/)
676
+ br = parse_paren_expression(scanner)
677
+ ~br if br
678
+ else
679
+ parse_filter_branch(scanner)
680
+ end
681
+
682
+ if expr and scanner.scan(/\s*\)\s*/)
683
+ expr
684
+ end
685
+ end
686
+ end
687
+ private :parse_paren_expression
688
+
689
+ ##
690
+ # This parses a given expression inside of parentheses.
691
+ def parse_filter_branch(scanner)
692
+ scanner.scan(/\s*/)
693
+ if token = scanner.scan(/[-\w\d_:.]*[\d\w]/)
694
+ scanner.scan(/\s*/)
695
+ if op = scanner.scan(/<=|>=|!=|:=|=/)
696
+ scanner.scan(/\s*/)
697
+ if value = scanner.scan(/(?:[-\w*.+@=,#\$%&!'\s]|\\[a-fA-F\d]{2})+/)
698
+ # 20100313 AZ: Assumes that "(uid=george*)" is the same as
699
+ # "(uid=george* )". The standard doesn't specify, but I can find
700
+ # no examples that suggest otherwise.
701
+ value.strip!
702
+ case op
703
+ when "="
704
+ Net::LDAP::Filter.eq(token, value)
705
+ when "!="
706
+ Net::LDAP::Filter.ne(token, value)
707
+ when "<="
708
+ Net::LDAP::Filter.le(token, value)
709
+ when ">="
710
+ Net::LDAP::Filter.ge(token, value)
711
+ when ":="
712
+ Net::LDAP::Filter.ex(token, value)
713
+ end
714
+ end
715
+ end
716
+ end
717
+ end
718
+ private :parse_filter_branch
719
+ end # class Net::LDAP::FilterParser
720
+ end # class Net::LDAP::Filter