scashin133-net-ldap 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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