annotate .svn/pristine/bc/bc0c969dde0ae26fdb5eed30992ca79327b8efb8.svn-base @ 1327:287f201c2802 redmine-2.2-integration

Add italic
author Chris Cannam <chris.cannam@soundsoftware.ac.uk>
date Wed, 19 Jun 2013 20:56:22 +0100
parents cbb26bc654de
children
rev   line source
Chris@909 1 # $Id: filter.rb 151 2006-08-15 08:34:53Z blackhedd $
Chris@909 2 #
Chris@909 3 #
Chris@909 4 #----------------------------------------------------------------------------
Chris@909 5 #
Chris@909 6 # Copyright (C) 2006 by Francis Cianfrocca. All Rights Reserved.
Chris@909 7 #
Chris@909 8 # Gmail: garbagecat10
Chris@909 9 #
Chris@909 10 # This program is free software; you can redistribute it and/or modify
Chris@909 11 # it under the terms of the GNU General Public License as published by
Chris@909 12 # the Free Software Foundation; either version 2 of the License, or
Chris@909 13 # (at your option) any later version.
Chris@909 14 #
Chris@909 15 # This program is distributed in the hope that it will be useful,
Chris@909 16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
Chris@909 17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Chris@909 18 # GNU General Public License for more details.
Chris@909 19 #
Chris@909 20 # You should have received a copy of the GNU General Public License
Chris@909 21 # along with this program; if not, write to the Free Software
Chris@909 22 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
Chris@909 23 #
Chris@909 24 #---------------------------------------------------------------------------
Chris@909 25 #
Chris@909 26 #
Chris@909 27
Chris@909 28
Chris@909 29 module Net
Chris@909 30 class LDAP
Chris@909 31
Chris@909 32
Chris@909 33 # Class Net::LDAP::Filter is used to constrain
Chris@909 34 # LDAP searches. An object of this class is
Chris@909 35 # passed to Net::LDAP#search in the parameter :filter.
Chris@909 36 #
Chris@909 37 # Net::LDAP::Filter supports the complete set of search filters
Chris@909 38 # available in LDAP, including conjunction, disjunction and negation
Chris@909 39 # (AND, OR, and NOT). This class supplants the (infamous) RFC-2254
Chris@909 40 # standard notation for specifying LDAP search filters.
Chris@909 41 #
Chris@909 42 # Here's how to code the familiar "objectclass is present" filter:
Chris@909 43 # f = Net::LDAP::Filter.pres( "objectclass" )
Chris@909 44 # The object returned by this code can be passed directly to
Chris@909 45 # the <tt>:filter</tt> parameter of Net::LDAP#search.
Chris@909 46 #
Chris@909 47 # See the individual class and instance methods below for more examples.
Chris@909 48 #
Chris@909 49 class Filter
Chris@909 50
Chris@909 51 def initialize op, a, b
Chris@909 52 @op = op
Chris@909 53 @left = a
Chris@909 54 @right = b
Chris@909 55 end
Chris@909 56
Chris@909 57 # #eq creates a filter object indicating that the value of
Chris@909 58 # a paticular attribute must be either <i>present</i> or must
Chris@909 59 # match a particular string.
Chris@909 60 #
Chris@909 61 # To specify that an attribute is "present" means that only
Chris@909 62 # directory entries which contain a value for the particular
Chris@909 63 # attribute will be selected by the filter. This is useful
Chris@909 64 # in case of optional attributes such as <tt>mail.</tt>
Chris@909 65 # Presence is indicated by giving the value "*" in the second
Chris@909 66 # parameter to #eq. This example selects only entries that have
Chris@909 67 # one or more values for <tt>sAMAccountName:</tt>
Chris@909 68 # f = Net::LDAP::Filter.eq( "sAMAccountName", "*" )
Chris@909 69 #
Chris@909 70 # To match a particular range of values, pass a string as the
Chris@909 71 # second parameter to #eq. The string may contain one or more
Chris@909 72 # "*" characters as wildcards: these match zero or more occurrences
Chris@909 73 # of any character. Full regular-expressions are <i>not</i> supported
Chris@909 74 # due to limitations in the underlying LDAP protocol.
Chris@909 75 # This example selects any entry with a <tt>mail</tt> value containing
Chris@909 76 # the substring "anderson":
Chris@909 77 # f = Net::LDAP::Filter.eq( "mail", "*anderson*" )
Chris@909 78 #--
Chris@909 79 # Removed gt and lt. They ain't in the standard!
Chris@909 80 #
Chris@909 81 def Filter::eq attribute, value; Filter.new :eq, attribute, value; end
Chris@909 82 def Filter::ne attribute, value; Filter.new :ne, attribute, value; end
Chris@909 83 #def Filter::gt attribute, value; Filter.new :gt, attribute, value; end
Chris@909 84 #def Filter::lt attribute, value; Filter.new :lt, attribute, value; end
Chris@909 85 def Filter::ge attribute, value; Filter.new :ge, attribute, value; end
Chris@909 86 def Filter::le attribute, value; Filter.new :le, attribute, value; end
Chris@909 87
Chris@909 88 # #pres( attribute ) is a synonym for #eq( attribute, "*" )
Chris@909 89 #
Chris@909 90 def Filter::pres attribute; Filter.eq attribute, "*"; end
Chris@909 91
Chris@909 92 # operator & ("AND") is used to conjoin two or more filters.
Chris@909 93 # This expression will select only entries that have an <tt>objectclass</tt>
Chris@909 94 # attribute AND have a <tt>mail</tt> attribute that begins with "George":
Chris@909 95 # f = Net::LDAP::Filter.pres( "objectclass" ) & Net::LDAP::Filter.eq( "mail", "George*" )
Chris@909 96 #
Chris@909 97 def & filter; Filter.new :and, self, filter; end
Chris@909 98
Chris@909 99 # operator | ("OR") is used to disjoin two or more filters.
Chris@909 100 # This expression will select entries that have either an <tt>objectclass</tt>
Chris@909 101 # attribute OR a <tt>mail</tt> attribute that begins with "George":
Chris@909 102 # f = Net::LDAP::Filter.pres( "objectclass" ) | Net::LDAP::Filter.eq( "mail", "George*" )
Chris@909 103 #
Chris@909 104 def | filter; Filter.new :or, self, filter; end
Chris@909 105
Chris@909 106
Chris@909 107 #
Chris@909 108 # operator ~ ("NOT") is used to negate a filter.
Chris@909 109 # This expression will select only entries that <i>do not</i> have an <tt>objectclass</tt>
Chris@909 110 # attribute:
Chris@909 111 # f = ~ Net::LDAP::Filter.pres( "objectclass" )
Chris@909 112 #
Chris@909 113 #--
Chris@909 114 # This operator can't be !, evidently. Try it.
Chris@909 115 # Removed GT and LT. They're not in the RFC.
Chris@909 116 def ~@; Filter.new :not, self, nil; end
Chris@909 117
Chris@909 118
Chris@909 119 def to_s
Chris@909 120 case @op
Chris@909 121 when :ne
Chris@909 122 "(!(#{@left}=#{@right}))"
Chris@909 123 when :eq
Chris@909 124 "(#{@left}=#{@right})"
Chris@909 125 #when :gt
Chris@909 126 # "#{@left}>#{@right}"
Chris@909 127 #when :lt
Chris@909 128 # "#{@left}<#{@right}"
Chris@909 129 when :ge
Chris@909 130 "#{@left}>=#{@right}"
Chris@909 131 when :le
Chris@909 132 "#{@left}<=#{@right}"
Chris@909 133 when :and
Chris@909 134 "(&(#{@left})(#{@right}))"
Chris@909 135 when :or
Chris@909 136 "(|(#{@left})(#{@right}))"
Chris@909 137 when :not
Chris@909 138 "(!(#{@left}))"
Chris@909 139 else
Chris@909 140 raise "invalid or unsupported operator in LDAP Filter"
Chris@909 141 end
Chris@909 142 end
Chris@909 143
Chris@909 144
Chris@909 145 #--
Chris@909 146 # to_ber
Chris@909 147 # Filter ::=
Chris@909 148 # CHOICE {
Chris@909 149 # and [0] SET OF Filter,
Chris@909 150 # or [1] SET OF Filter,
Chris@909 151 # not [2] Filter,
Chris@909 152 # equalityMatch [3] AttributeValueAssertion,
Chris@909 153 # substrings [4] SubstringFilter,
Chris@909 154 # greaterOrEqual [5] AttributeValueAssertion,
Chris@909 155 # lessOrEqual [6] AttributeValueAssertion,
Chris@909 156 # present [7] AttributeType,
Chris@909 157 # approxMatch [8] AttributeValueAssertion
Chris@909 158 # }
Chris@909 159 #
Chris@909 160 # SubstringFilter
Chris@909 161 # SEQUENCE {
Chris@909 162 # type AttributeType,
Chris@909 163 # SEQUENCE OF CHOICE {
Chris@909 164 # initial [0] LDAPString,
Chris@909 165 # any [1] LDAPString,
Chris@909 166 # final [2] LDAPString
Chris@909 167 # }
Chris@909 168 # }
Chris@909 169 #
Chris@909 170 # Parsing substrings is a little tricky.
Chris@909 171 # We use the split method to break a string into substrings
Chris@909 172 # delimited by the * (star) character. But we also need
Chris@909 173 # to know whether there is a star at the head and tail
Chris@909 174 # of the string. A Ruby particularity comes into play here:
Chris@909 175 # if you split on * and the first character of the string is
Chris@909 176 # a star, then split will return an array whose first element
Chris@909 177 # is an _empty_ string. But if the _last_ character of the
Chris@909 178 # string is star, then split will return an array that does
Chris@909 179 # _not_ add an empty string at the end. So we have to deal
Chris@909 180 # with all that specifically.
Chris@909 181 #
Chris@909 182 def to_ber
Chris@909 183 case @op
Chris@909 184 when :eq
Chris@909 185 if @right == "*" # present
Chris@909 186 @left.to_s.to_ber_contextspecific 7
Chris@909 187 elsif @right =~ /[\*]/ #substring
Chris@909 188 ary = @right.split( /[\*]+/ )
Chris@909 189 final_star = @right =~ /[\*]$/
Chris@909 190 initial_star = ary.first == "" and ary.shift
Chris@909 191
Chris@909 192 seq = []
Chris@909 193 unless initial_star
Chris@909 194 seq << ary.shift.to_ber_contextspecific(0)
Chris@909 195 end
Chris@909 196 n_any_strings = ary.length - (final_star ? 0 : 1)
Chris@909 197 #p n_any_strings
Chris@909 198 n_any_strings.times {
Chris@909 199 seq << ary.shift.to_ber_contextspecific(1)
Chris@909 200 }
Chris@909 201 unless final_star
Chris@909 202 seq << ary.shift.to_ber_contextspecific(2)
Chris@909 203 end
Chris@909 204 [@left.to_s.to_ber, seq.to_ber].to_ber_contextspecific 4
Chris@909 205 else #equality
Chris@909 206 [@left.to_s.to_ber, @right.to_ber].to_ber_contextspecific 3
Chris@909 207 end
Chris@909 208 when :ge
Chris@909 209 [@left.to_s.to_ber, @right.to_ber].to_ber_contextspecific 5
Chris@909 210 when :le
Chris@909 211 [@left.to_s.to_ber, @right.to_ber].to_ber_contextspecific 6
Chris@909 212 when :and
Chris@909 213 ary = [@left.coalesce(:and), @right.coalesce(:and)].flatten
Chris@909 214 ary.map {|a| a.to_ber}.to_ber_contextspecific( 0 )
Chris@909 215 when :or
Chris@909 216 ary = [@left.coalesce(:or), @right.coalesce(:or)].flatten
Chris@909 217 ary.map {|a| a.to_ber}.to_ber_contextspecific( 1 )
Chris@909 218 when :not
Chris@909 219 [@left.to_ber].to_ber_contextspecific 2
Chris@909 220 else
Chris@909 221 # ERROR, we'll return objectclass=* to keep things from blowing up,
Chris@909 222 # but that ain't a good answer and we need to kick out an error of some kind.
Chris@909 223 raise "unimplemented search filter"
Chris@909 224 end
Chris@909 225 end
Chris@909 226
Chris@909 227 #--
Chris@909 228 # coalesce
Chris@909 229 # This is a private helper method for dealing with chains of ANDs and ORs
Chris@909 230 # that are longer than two. If BOTH of our branches are of the specified
Chris@909 231 # type of joining operator, then return both of them as an array (calling
Chris@909 232 # coalesce recursively). If they're not, then return an array consisting
Chris@909 233 # only of self.
Chris@909 234 #
Chris@909 235 def coalesce operator
Chris@909 236 if @op == operator
Chris@909 237 [@left.coalesce( operator ), @right.coalesce( operator )]
Chris@909 238 else
Chris@909 239 [self]
Chris@909 240 end
Chris@909 241 end
Chris@909 242
Chris@909 243
Chris@909 244
Chris@909 245 #--
Chris@909 246 # We get a Ruby object which comes from parsing an RFC-1777 "Filter"
Chris@909 247 # object. Convert it to a Net::LDAP::Filter.
Chris@909 248 # TODO, we're hardcoding the RFC-1777 BER-encodings of the various
Chris@909 249 # filter types. Could pull them out into a constant.
Chris@909 250 #
Chris@909 251 def Filter::parse_ldap_filter obj
Chris@909 252 case obj.ber_identifier
Chris@909 253 when 0x87 # present. context-specific primitive 7.
Chris@909 254 Filter.eq( obj.to_s, "*" )
Chris@909 255 when 0xa3 # equalityMatch. context-specific constructed 3.
Chris@909 256 Filter.eq( obj[0], obj[1] )
Chris@909 257 else
Chris@909 258 raise LdapError.new( "unknown ldap search-filter type: #{obj.ber_identifier}" )
Chris@909 259 end
Chris@909 260 end
Chris@909 261
Chris@909 262
Chris@909 263 #--
Chris@909 264 # We got a hash of attribute values.
Chris@909 265 # Do we match the attributes?
Chris@909 266 # Return T/F, and call match recursively as necessary.
Chris@909 267 def match entry
Chris@909 268 case @op
Chris@909 269 when :eq
Chris@909 270 if @right == "*"
Chris@909 271 l = entry[@left] and l.length > 0
Chris@909 272 else
Chris@909 273 l = entry[@left] and l = l.to_a and l.index(@right)
Chris@909 274 end
Chris@909 275 else
Chris@909 276 raise LdapError.new( "unknown filter type in match: #{@op}" )
Chris@909 277 end
Chris@909 278 end
Chris@909 279
Chris@909 280 # Converts an LDAP filter-string (in the prefix syntax specified in RFC-2254)
Chris@909 281 # to a Net::LDAP::Filter.
Chris@909 282 def self.construct ldap_filter_string
Chris@909 283 FilterParser.new(ldap_filter_string).filter
Chris@909 284 end
Chris@909 285
Chris@909 286 # Synonym for #construct.
Chris@909 287 # to a Net::LDAP::Filter.
Chris@909 288 def self.from_rfc2254 ldap_filter_string
Chris@909 289 construct ldap_filter_string
Chris@909 290 end
Chris@909 291
Chris@909 292 end # class Net::LDAP::Filter
Chris@909 293
Chris@909 294
Chris@909 295
Chris@909 296 class FilterParser #:nodoc:
Chris@909 297
Chris@909 298 attr_reader :filter
Chris@909 299
Chris@909 300 def initialize str
Chris@909 301 require 'strscan'
Chris@909 302 @filter = parse( StringScanner.new( str )) or raise Net::LDAP::LdapError.new( "invalid filter syntax" )
Chris@909 303 end
Chris@909 304
Chris@909 305 def parse scanner
Chris@909 306 parse_filter_branch(scanner) or parse_paren_expression(scanner)
Chris@909 307 end
Chris@909 308
Chris@909 309 def parse_paren_expression scanner
Chris@909 310 if scanner.scan(/\s*\(\s*/)
Chris@909 311 b = if scanner.scan(/\s*\&\s*/)
Chris@909 312 a = nil
Chris@909 313 branches = []
Chris@909 314 while br = parse_paren_expression(scanner)
Chris@909 315 branches << br
Chris@909 316 end
Chris@909 317 if branches.length >= 2
Chris@909 318 a = branches.shift
Chris@909 319 while branches.length > 0
Chris@909 320 a = a & branches.shift
Chris@909 321 end
Chris@909 322 a
Chris@909 323 end
Chris@909 324 elsif scanner.scan(/\s*\|\s*/)
Chris@909 325 # TODO: DRY!
Chris@909 326 a = nil
Chris@909 327 branches = []
Chris@909 328 while br = parse_paren_expression(scanner)
Chris@909 329 branches << br
Chris@909 330 end
Chris@909 331 if branches.length >= 2
Chris@909 332 a = branches.shift
Chris@909 333 while branches.length > 0
Chris@909 334 a = a | branches.shift
Chris@909 335 end
Chris@909 336 a
Chris@909 337 end
Chris@909 338 elsif scanner.scan(/\s*\!\s*/)
Chris@909 339 br = parse_paren_expression(scanner)
Chris@909 340 if br
Chris@909 341 ~ br
Chris@909 342 end
Chris@909 343 else
Chris@909 344 parse_filter_branch( scanner )
Chris@909 345 end
Chris@909 346
Chris@909 347 if b and scanner.scan( /\s*\)\s*/ )
Chris@909 348 b
Chris@909 349 end
Chris@909 350 end
Chris@909 351 end
Chris@909 352
Chris@909 353 # Added a greatly-augmented filter contributed by Andre Nathan
Chris@909 354 # for detecting special characters in values. (15Aug06)
Chris@909 355 def parse_filter_branch scanner
Chris@909 356 scanner.scan(/\s*/)
Chris@909 357 if token = scanner.scan( /[\w\-_]+/ )
Chris@909 358 scanner.scan(/\s*/)
Chris@909 359 if op = scanner.scan( /\=|\<\=|\<|\>\=|\>|\!\=/ )
Chris@909 360 scanner.scan(/\s*/)
Chris@909 361 #if value = scanner.scan( /[\w\*\.]+/ ) (ORG)
Chris@909 362 if value = scanner.scan( /[\w\*\.\+\-@=#\$%&!]+/ )
Chris@909 363 case op
Chris@909 364 when "="
Chris@909 365 Filter.eq( token, value )
Chris@909 366 when "!="
Chris@909 367 Filter.ne( token, value )
Chris@909 368 when "<"
Chris@909 369 Filter.lt( token, value )
Chris@909 370 when "<="
Chris@909 371 Filter.le( token, value )
Chris@909 372 when ">"
Chris@909 373 Filter.gt( token, value )
Chris@909 374 when ">="
Chris@909 375 Filter.ge( token, value )
Chris@909 376 end
Chris@909 377 end
Chris@909 378 end
Chris@909 379 end
Chris@909 380 end
Chris@909 381
Chris@909 382 end # class Net::LDAP::FilterParser
Chris@909 383
Chris@909 384 end # class Net::LDAP
Chris@909 385 end # module Net
Chris@909 386
Chris@909 387