activeldap 0.9.0 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (120) hide show
  1. data/CHANGES +61 -0
  2. data/README +8 -1
  3. data/Rakefile +4 -1
  4. data/benchmark/bench-al.rb +12 -2
  5. data/examples/al-admin/app/controllers/account_controller.rb +4 -3
  6. data/examples/al-admin/app/controllers/application.rb +5 -2
  7. data/examples/al-admin/app/controllers/directory_controller.rb +3 -1
  8. data/examples/al-admin/app/controllers/users_controller.rb +19 -4
  9. data/examples/al-admin/app/controllers/welcome_controller.rb +4 -2
  10. data/examples/al-admin/app/helpers/application_helper.rb +7 -1
  11. data/examples/al-admin/app/helpers/url_helper.rb +4 -0
  12. data/examples/al-admin/app/models/ldap_user.rb +4 -0
  13. data/examples/al-admin/app/views/_entry/{_attributes_information.rhtml → _attributes_information.html.erb} +0 -0
  14. data/examples/al-admin/app/views/_entry/{_entry.rhtml → _entry.html.erb} +0 -0
  15. data/examples/al-admin/app/views/_schema/{_aliases.rhtml → _aliases.html.erb} +0 -0
  16. data/examples/al-admin/app/views/_switcher/{_after.rhtml → _after.html.erb} +0 -0
  17. data/examples/al-admin/app/views/_switcher/{_before.rhtml → _before.html.erb} +0 -0
  18. data/examples/al-admin/app/views/account/{login.rhtml → login.html.erb} +0 -0
  19. data/examples/al-admin/app/views/account/{sign_up.rhtml → sign_up.html.erb} +0 -0
  20. data/examples/al-admin/app/views/attributes/{_attributes.rhtml → _attributes.html.erb} +0 -0
  21. data/examples/al-admin/app/views/attributes/{_detail.rhtml → _detail.html.erb} +0 -0
  22. data/examples/al-admin/app/views/attributes/{index.rhtml → index.html.erb} +0 -0
  23. data/examples/al-admin/app/views/attributes/{show.rhtml → show.html.erb} +0 -0
  24. data/examples/al-admin/app/views/directory/{_tree.rhtml → _tree.html.erb} +0 -0
  25. data/examples/al-admin/app/views/directory/{_tree_view_js.rhtml → _tree_view_js.html.erb} +4 -5
  26. data/examples/al-admin/app/views/directory/{index.rhtml → index.html.erb} +0 -0
  27. data/examples/al-admin/app/views/directory/{populate.rhtml → populate.html.erb} +0 -0
  28. data/examples/al-admin/app/views/layouts/{_footer.rhtml → _footer.html.erb} +0 -0
  29. data/examples/al-admin/app/views/layouts/{_header_menu.rhtml → _header_menu.html.erb} +0 -0
  30. data/examples/al-admin/app/views/layouts/{_main_menu.rhtml → _main_menu.html.erb} +0 -0
  31. data/examples/al-admin/app/views/layouts/{application.rhtml → application.html.erb} +3 -2
  32. data/examples/al-admin/app/views/object_classes/{_attributes.rhtml → _attributes.html.erb} +0 -0
  33. data/examples/al-admin/app/views/object_classes/{_object_classes.rhtml → _object_classes.html.erb} +0 -0
  34. data/examples/al-admin/app/views/object_classes/{index.rhtml → index.html.erb} +0 -0
  35. data/examples/al-admin/app/views/object_classes/{show.rhtml → show.html.erb} +0 -0
  36. data/examples/al-admin/app/views/syntaxes/{_detail.rhtml → _detail.html.erb} +0 -0
  37. data/examples/al-admin/app/views/syntaxes/{_syntaxes.rhtml → _syntaxes.html.erb} +0 -0
  38. data/examples/al-admin/app/views/syntaxes/{index.rhtml → index.html.erb} +0 -0
  39. data/examples/al-admin/app/views/syntaxes/{show.rhtml → show.html.erb} +0 -0
  40. data/examples/al-admin/app/views/users/{_attributes_update_form.rhtml → _attributes_update_form.html.erb} +0 -0
  41. data/examples/al-admin/app/views/users/{_form.rhtml → _form.html.erb} +0 -0
  42. data/examples/al-admin/app/views/users/{_object_classes_update_form.rhtml → _object_classes_update_form.html.erb} +7 -1
  43. data/examples/al-admin/app/views/users/{_password_change_form.rhtml → _password_change_form.html.erb} +0 -0
  44. data/examples/al-admin/app/views/users/{edit.rhtml → edit.html.erb} +0 -0
  45. data/examples/al-admin/app/views/users/{index.rhtml → index.html.erb} +0 -0
  46. data/examples/al-admin/app/views/users/{show.rhtml → show.html.erb} +0 -0
  47. data/examples/al-admin/app/views/welcome/{index.rhtml → index.html.erb} +0 -0
  48. data/examples/al-admin/config/boot.rb +96 -32
  49. data/examples/al-admin/config/environment.rb +30 -36
  50. data/examples/al-admin/config/environments/development.rb +2 -5
  51. data/examples/al-admin/config/environments/production.rb +1 -0
  52. data/examples/al-admin/config/environments/test.rb +4 -1
  53. data/examples/al-admin/config/initializers/exception_notifier.rb +2 -0
  54. data/examples/al-admin/config/initializers/gettext.rb +1 -0
  55. data/examples/al-admin/config/initializers/inflections.rb +10 -0
  56. data/examples/al-admin/config/initializers/mime_types.rb +5 -0
  57. data/examples/al-admin/config/initializers/ralative_url_support.rb +1 -0
  58. data/examples/al-admin/config/routes.rb +24 -12
  59. data/examples/al-admin/lib/authenticated_system.rb +1 -1
  60. data/examples/al-admin/lib/tasks/gettext.rake +1 -1
  61. data/examples/al-admin/po/en/al-admin.po +102 -100
  62. data/examples/al-admin/po/ja/al-admin.po +112 -110
  63. data/examples/al-admin/po/nl/al-admin.po +117 -110
  64. data/examples/al-admin/public/javascripts/controls.js +484 -354
  65. data/examples/al-admin/public/javascripts/dragdrop.js +88 -58
  66. data/examples/al-admin/public/javascripts/effects.js +396 -364
  67. data/examples/al-admin/public/javascripts/prototype.js +2817 -1107
  68. data/examples/al-admin/public/stylesheets/base.css +5 -0
  69. data/examples/al-admin/script/performance/request +3 -0
  70. data/lib/active_ldap.rb +13 -10
  71. data/lib/active_ldap/adapter/base.rb +159 -43
  72. data/lib/active_ldap/adapter/jndi.rb +175 -0
  73. data/lib/active_ldap/adapter/jndi_connection.rb +180 -0
  74. data/lib/active_ldap/adapter/ldap.rb +91 -46
  75. data/lib/active_ldap/adapter/ldap_ext.rb +19 -5
  76. data/lib/active_ldap/adapter/net_ldap.rb +52 -44
  77. data/lib/active_ldap/association/has_many_wrap.rb +1 -1
  78. data/lib/active_ldap/attributes.rb +20 -95
  79. data/lib/active_ldap/base.rb +195 -186
  80. data/lib/active_ldap/callbacks.rb +33 -0
  81. data/lib/active_ldap/command.rb +3 -3
  82. data/lib/active_ldap/connection.rb +21 -3
  83. data/lib/active_ldap/distinguished_name.rb +18 -11
  84. data/lib/active_ldap/entry_attribute.rb +78 -0
  85. data/lib/active_ldap/human_readable.rb +20 -0
  86. data/lib/active_ldap/ldif.rb +860 -10
  87. data/lib/active_ldap/object_class.rb +6 -4
  88. data/lib/active_ldap/operations.rb +129 -22
  89. data/lib/active_ldap/schema.rb +118 -9
  90. data/lib/active_ldap/schema/syntaxes.rb +33 -16
  91. data/lib/active_ldap/validations.rb +74 -65
  92. data/po/en/active-ldap.po +378 -768
  93. data/po/ja/active-ldap.po +935 -868
  94. data/rails/plugin/active_ldap/init.rb +40 -2
  95. data/test/al-test-utils.rb +78 -58
  96. data/test/command.rb +51 -1
  97. data/test/test-unit-ext/priority.rb +29 -6
  98. data/test/test_adapter.rb +21 -2
  99. data/test/test_attributes.rb +13 -0
  100. data/test/test_base.rb +51 -1
  101. data/test/test_connection.rb +2 -1
  102. data/test/test_connection_per_class.rb +55 -1
  103. data/test/test_connection_per_dn.rb +29 -1
  104. data/test/test_find.rb +73 -0
  105. data/test/test_ldif.rb +1829 -15
  106. data/test/test_load.rb +126 -0
  107. data/test/test_object_class.rb +23 -5
  108. data/test/test_schema.rb +28 -0
  109. data/test/test_syntax.rb +22 -11
  110. data/test/test_user.rb +16 -25
  111. data/test/test_useradd-binary.rb +1 -1
  112. data/test/test_usermod-binary-add-time.rb +1 -1
  113. data/test/test_usermod-binary-add.rb +1 -1
  114. data/test/test_validation.rb +100 -22
  115. metadata +77 -71
  116. data/data/locale/en/LC_MESSAGES/active-ldap.mo +0 -0
  117. data/data/locale/ja/LC_MESSAGES/active-ldap.mo +0 -0
  118. data/examples/al-admin/app/views/layouts/_flash_box.rhtml +0 -4
  119. data/examples/al-admin/public/stylesheets/common.css +0 -2
  120. data/examples/al-admin/script/breakpointer +0 -3
@@ -8,6 +8,14 @@ module ActiveLdap
8
8
  base.class_eval do
9
9
  include ActiveRecord::Callbacks
10
10
 
11
+ unless respond_to?(:instantiate_with_callbacks)
12
+ extend ClassMethods
13
+ class << self
14
+ alias_method_chain :instantiate, :callbacks
15
+ end
16
+ alias_method_chain :initialize, :callbacks
17
+ end
18
+
11
19
  def callback(method)
12
20
  super
13
21
  rescue ActiveRecord::ActiveRecordError
@@ -15,5 +23,30 @@ module ActiveLdap
15
23
  end
16
24
  end
17
25
  end
26
+
27
+ module ClassMethods
28
+ def instantiate_with_callbacks(record)
29
+ object = instantiate_without_callbacks(record)
30
+
31
+ if object.respond_to_without_attributes?(:after_find)
32
+ object.send(:callback, :after_find)
33
+ end
34
+
35
+ if object.respond_to_without_attributes?(:after_initialize)
36
+ object.send(:callback, :after_initialize)
37
+ end
38
+
39
+ object
40
+ end
41
+ end
42
+
43
+ def initialize_with_callbacks(attributes = nil) #:nodoc:
44
+ initialize_without_callbacks(attributes)
45
+ result = yield self if block_given?
46
+ if respond_to_without_attributes?(:after_initialize)
47
+ callback(:after_initialize)
48
+ end
49
+ result
50
+ end
18
51
  end
19
52
  end
@@ -38,11 +38,11 @@ module ActiveLdap
38
38
  end
39
39
 
40
40
  def read_password(prompt, input=$stdin, output=$stdout)
41
- output.print prompt
42
- system "/bin/stty -echo" if input.tty?
41
+ output.print(prompt)
42
+ system("/bin/stty -echo") if input.tty?
43
43
  input.gets.chomp
44
44
  ensure
45
- system "/bin/stty echo" if input.tty?
45
+ system("/bin/stty echo") if input.tty?
46
46
  output.puts
47
47
  end
48
48
  end
@@ -88,7 +88,7 @@ module ActiveLdap
88
88
  end
89
89
 
90
90
  def instantiate_adapter(config)
91
- adapter = (config[:adapter] || "ldap")
91
+ adapter = (config[:adapter] || default_adapter)
92
92
  normalized_adapter = adapter.downcase.gsub(/-/, "_")
93
93
  adapter_method = "#{normalized_adapter}_connection"
94
94
  unless Adapter::Base.respond_to?(adapter_method)
@@ -104,6 +104,10 @@ module ActiveLdap
104
104
  Adapter::Base.send(adapter_method, config)
105
105
  end
106
106
 
107
+ def default_adapter
108
+ @@default_adapter ||= guess_available_adapter
109
+ end
110
+
107
111
  def connected?
108
112
  active_connections[active_connection_name] ? true : false
109
113
  end
@@ -152,6 +156,12 @@ module ActiveLdap
152
156
  connection.schema
153
157
  end
154
158
 
159
+ def reset_runtime
160
+ active_connections.inject(0) do |result, (name, connection)|
161
+ result + connection.reset_runtime
162
+ end
163
+ end
164
+
155
165
  private
156
166
  def active_connection_key(k=self)
157
167
  k.name.empty? ? k.object_id : k.name
@@ -179,6 +189,10 @@ module ActiveLdap
179
189
  end
180
190
  @@active_connections.clear
181
191
  end
192
+
193
+ def guess_available_adapter
194
+ defined?(java) ? "jndi" : "ldap"
195
+ end
182
196
  end
183
197
 
184
198
  def establish_connection(config=nil)
@@ -197,8 +211,12 @@ module ActiveLdap
197
211
 
198
212
  def connection
199
213
  conn = @connection
200
- if get_attribute_before_type_cast(dn_attribute)[1]
201
- conn ||= self.class.active_connections[dn] || retrieve_connection
214
+ return conn if conn
215
+
216
+ if @dn or
217
+ (attribute_name_resolvable_without_connection? and
218
+ get_attribute_before_type_cast(dn_attribute)[1])
219
+ conn = self.class.active_connections[dn] || retrieve_connection
202
220
  end
203
221
  conn || self.class.connection
204
222
  end
@@ -153,13 +153,24 @@ module ActiveLdap
153
153
  def parse(source)
154
154
  Parser.new(source).parse
155
155
  end
156
+
157
+ def escape_value(value)
158
+ if /(\A | \z)/.match(value)
159
+ '"' + value.gsub(/([\\\"])/, '\\\\\1') + '"'
160
+ else
161
+ value.gsub(/([,=\+<>#;\\\"])/, '\\\\\1')
162
+ end
163
+ end
156
164
  end
157
165
 
158
166
  attr_reader :rdns
159
167
  def initialize(*rdns)
160
168
  @rdns = rdns.collect do |rdn|
161
- rdn = {rdn[0] => rdn[1]} if rdn.is_a?(Array) and rdn.size == 2
162
- rdn
169
+ if rdn.is_a?(Array) and rdn.size == 2
170
+ {rdn[0] => rdn[1]}
171
+ else
172
+ rdn
173
+ end
163
174
  end
164
175
  end
165
176
 
@@ -212,11 +223,15 @@ module ActiveLdap
212
223
  rdn.sort_by do |type, value|
213
224
  type.upcase
214
225
  end.collect do |type, value|
215
- "#{type}=#{escape(value)}"
226
+ "#{type}=#{self.class.escape_value(value)}"
216
227
  end.join("+")
217
228
  end.join(",")
218
229
  end
219
230
 
231
+ def to_human_readable_format
232
+ to_s.inspect
233
+ end
234
+
220
235
  private
221
236
  def normalize(rdns)
222
237
  rdns.collect do |rdn|
@@ -237,14 +252,6 @@ module ActiveLdap
237
252
  [key, value]
238
253
  end
239
254
  end
240
-
241
- def escape(value)
242
- if /(\A | \z)/.match(value)
243
- '"' + value.gsub(/([\\\"])/, '\\\\\1') + '"'
244
- else
245
- value.gsub(/([,=\+<>#;\\\"])/, '\\\\\1')
246
- end
247
- end
248
255
  end
249
256
 
250
257
  DN = DistinguishedName
@@ -0,0 +1,78 @@
1
+ require "active_ldap/attributes"
2
+
3
+ module ActiveLdap
4
+ class EntryAttribute
5
+ include Attributes::Normalizable
6
+
7
+ attr_reader :must, :may, :object_classes, :schemata
8
+ def initialize(schema, object_classes)
9
+ @schemata = {}
10
+ @names = {}
11
+ @normalized_names = {}
12
+ @aliases = {}
13
+ @must = []
14
+ @may = []
15
+ @object_classes = []
16
+ register(schema.attribute('objectClass'))
17
+ object_classes.each do |objc|
18
+ # get all attributes for the class
19
+ object_class = schema.object_class(objc)
20
+ @object_classes << object_class
21
+ @must.concat(object_class.must)
22
+ @may.concat(object_class.may)
23
+ end
24
+ @must.uniq!
25
+ @may.uniq!
26
+ (@must + @may).each do |attr|
27
+ # Update attr_method with appropriate
28
+ register(attr)
29
+ end
30
+ end
31
+
32
+ def names(normalize=false)
33
+ names = @names.keys
34
+ if normalize
35
+ names.collect do |name|
36
+ normalize(name)
37
+ end.uniq
38
+ else
39
+ names
40
+ end
41
+ end
42
+
43
+ def normalize(name, allow_normalized_name=false)
44
+ return name if name.nil?
45
+ return nil if @names.empty? and @aliases.empty?
46
+ name = name.to_s
47
+ real_name = @names[name]
48
+ real_name ||= @aliases[Inflector.underscore(name)]
49
+ if real_name
50
+ real_name
51
+ elsif allow_normalized_name
52
+ return nil if @normalized_names.empty?
53
+ @normalized_names[normalize_attribute_name(name)]
54
+ else
55
+ nil
56
+ end
57
+ end
58
+
59
+ def all_names
60
+ @names.keys + @aliases.keys
61
+ end
62
+
63
+ # register
64
+ #
65
+ # Make a method entry for _every_ alias of a valid attribute and map it
66
+ # onto the first attribute passed in.
67
+ def register(attribute)
68
+ real_name = attribute.name
69
+ return if @schemata.has_key?(real_name)
70
+ @schemata[real_name] = attribute
71
+ ([real_name] + attribute.aliases).each do |name|
72
+ @names[name] = real_name
73
+ @aliases[Inflector.underscore(name)] = real_name
74
+ @normalized_names[normalize_attribute_name(name)] = real_name
75
+ end
76
+ end
77
+ end
78
+ end
@@ -107,6 +107,26 @@ module ActiveLdap
107
107
  return nil if description.nil?
108
108
  "LDAP|Description|Syntax|#{syntax.id}|#{description}"
109
109
  end
110
+
111
+ def human_readable_format(object)
112
+ case object
113
+ when Array
114
+ "[#{object.collect {|value| human_readable_format(value)}.join(', ')}]"
115
+ when Hash
116
+ formatted_values = []
117
+ object.each do |key, value|
118
+ formatted_values << [human_readable_format(key),
119
+ human_readable_format(value)].join("=>")
120
+ end
121
+ "{#{formatted_values.join(', ')}}"
122
+ else
123
+ if object.respond_to?(:to_human_readable_format)
124
+ object.to_human_readable_format
125
+ else
126
+ object.inspect
127
+ end
128
+ end
129
+ end
110
130
  end
111
131
  end
112
132
  end
@@ -1,11 +1,82 @@
1
- # Experimental work-in-progress LDIF implementation.
2
- # Don't care this file for now.
3
-
4
- require 'strscan'
1
+ require "strscan"
2
+ require "base64"
3
+ require "uri"
4
+ require "open-uri"
5
5
 
6
6
  module ActiveLdap
7
7
  class Ldif
8
- include GetTextSupport
8
+ module Attributes
9
+ module_function
10
+ def encode(attributes)
11
+ return "" if attributes.empty?
12
+
13
+ result = ""
14
+ normalize(attributes).sort_by {|name,| name}.each do |name, values|
15
+ values.each do |options, value|
16
+ result << Attribute.encode([name, *options].join(";"), value)
17
+ end
18
+ end
19
+ result
20
+ end
21
+
22
+ def normalize(attributes)
23
+ result = {}
24
+ attributes.each do |name, values|
25
+ result[name] = Attribute.normalize_value(values).sort
26
+ end
27
+ result
28
+ end
29
+ end
30
+
31
+ module Attribute
32
+ SIZE = 75
33
+
34
+ module_function
35
+ def encode(name, value)
36
+ return "#{name}:\n" if value.blank?
37
+ result = "#{name}:"
38
+
39
+ if value[-1, 1] == ' ' or /\A#{Parser::SAFE_STRING}\z/u !~ value
40
+ result << ":"
41
+ value = [value].pack("m").gsub(/\n/u, '')
42
+ end
43
+ result << " "
44
+
45
+ first_line_value_size = SIZE - result.size
46
+ if value.size > first_line_value_size
47
+ first_line_value = value[0, first_line_value_size]
48
+ rest_value = value[first_line_value_size..-1]
49
+ else
50
+ first_line_value = value
51
+ rest_value = nil
52
+ end
53
+
54
+ result << "#{first_line_value}\n"
55
+ return result if rest_value.nil?
56
+
57
+ rest_value.scan(/.{1,#{SIZE - 1}}/u).each do |line|
58
+ result << " #{line}\n"
59
+ end
60
+ result
61
+ end
62
+
63
+ def normalize_value(value, result=[])
64
+ case value
65
+ when Array
66
+ value.each {|val| normalize_value(val, result)}
67
+ when Hash
68
+ value.each do |option, val|
69
+ normalize_value(val).each do |options, v|
70
+ result << [[option] + options, v]
71
+ end
72
+ end
73
+ result
74
+ else
75
+ result << [[], value]
76
+ end
77
+ result
78
+ end
79
+ end
9
80
 
10
81
  class Parser
11
82
  include GetTextSupport
@@ -17,28 +88,478 @@ module ActiveLdap
17
88
  @source = source
18
89
  end
19
90
 
91
+ ATTRIBUTE_TYPE_CHARS = /[a-zA-Z][a-zA-Z0-9\-]*/u
92
+ SAFE_CHAR = /[\x01-\x09\x0B-\x0C\x0E-\x7F]/u
93
+ SAFE_INIT_CHAR = /[\x01-\x09\x0B-\x0C\x0E-\x1F\x21-\x39\x3B\x3D-\x7F]/u
94
+ SAFE_STRING = /#{SAFE_INIT_CHAR}#{SAFE_CHAR}*/u
95
+ FILL = / */u
20
96
  def parse
21
97
  return @ldif if @ldif
22
98
 
23
- scanner = StringScanner.new(@source)
24
- raise version_spec_is_missing unless scanner.scan(/version:\s*(\d+)/)
99
+ @scanner = Scanner.new(@source)
100
+ raise version_spec_is_missing unless @scanner.scan(/version:/u)
101
+ @scanner.scan(FILL)
25
102
 
26
- version = Integer(scanner[1])
27
- raise unsupported_version(version) if version != "1"
103
+ version = @scanner.scan(/\d+/u)
104
+ raise version_number_is_missing if version.nil?
105
+
106
+ version = Integer(version)
107
+ raise unsupported_version(version) if version != 1
108
+
109
+ raise separator_is_missing unless @scanner.scan_separators
110
+
111
+ records = parse_records
112
+
113
+ @ldif = LDIF.new(records)
28
114
  end
29
115
 
30
116
  private
117
+ def read_base64_value
118
+ value = @scanner.scan(/[a-zA-Z0-9\+\/=]+/u)
119
+ return nil if value.nil?
120
+ Base64.decode64(value).chomp
121
+ end
122
+
123
+ def read_external_file
124
+ uri_string = @scanner.scan(URI::REGEXP::ABS_URI)
125
+ raise uri_is_missing if uri_string.nil?
126
+ uri = nil
127
+ begin
128
+ uri = URI.parse(uri_string)
129
+ rescue URI::Error
130
+ raise invalid_uri(uri_string, $!.message)
131
+ end
132
+
133
+ if uri.scheme == "file"
134
+ File.open(uri.path, "rb").read
135
+ else
136
+ uri.read
137
+ end
138
+ end
139
+
140
+ def parse_dn(dn_string)
141
+ DN.parse(dn_string).to_s
142
+ rescue DistinguishedNameInvalid
143
+ raise invalid_dn(dn_string, $!.reason)
144
+ end
145
+
146
+ def parse_attributes(least=0, &block)
147
+ i = 0
148
+ attributes = {}
149
+ block ||= Proc.new {@scanner.check_separator}
150
+ loop do
151
+ i += 1
152
+ if i >= least
153
+ break if block.call or @scanner.eos?
154
+ end
155
+ type, options, value = parse_attribute
156
+ if @scanner.scan_separator.nil? and !@scanner.eos?
157
+ raise separator_is_missing
158
+ end
159
+ attributes[type] ||= []
160
+ container = attributes[type]
161
+ options.each do |option|
162
+ parent = container.find do |val|
163
+ val.is_a?(Hash) and val.has_key?(option)
164
+ end
165
+ if parent.nil?
166
+ parent = {option => []}
167
+ container << parent
168
+ end
169
+ container = parent[option]
170
+ end
171
+ container << value
172
+ end
173
+ raise attribute_spec_is_missing if attributes.size < least
174
+ attributes
175
+ end
176
+
177
+ def parse_attribute_description
178
+ type = @scanner.scan(ATTRIBUTE_TYPE_CHARS)
179
+ raise attribute_type_is_missing if type.nil?
180
+ options = parse_options
181
+ [type, options]
182
+ end
183
+
184
+ def parse_attribute
185
+ type, options = parse_attribute_description
186
+ value = parse_attribute_value
187
+ [type, options, value]
188
+ end
189
+
190
+ def parse_options
191
+ options = []
192
+ while @scanner.scan(/;/u)
193
+ option = @scanner.scan(ATTRIBUTE_TYPE_CHARS)
194
+ raise option_is_missing if option.nil?
195
+ options << option
196
+ end
197
+ options
198
+ end
199
+
200
+ def parse_attribute_value(accept_external_file=true)
201
+ raise attribute_value_separator_is_missing if @scanner.scan(/:/u).nil?
202
+ if @scanner.scan(/:/u)
203
+ @scanner.scan(FILL)
204
+ read_base64_value
205
+ elsif accept_external_file and @scanner.scan(/</u)
206
+ @scanner.scan(FILL)
207
+ read_external_file
208
+ else
209
+ @scanner.scan(FILL)
210
+ @scanner.scan(SAFE_STRING)
211
+ end
212
+ end
213
+
214
+ def parse_control
215
+ return nil if @scanner.scan(/control:/u).nil?
216
+ @scanner.scan(FILL)
217
+ type = @scanner.scan(/\d+(?:\.\d+)*/u)
218
+ raise control_type_is_missing if type.nil?
219
+ criticality = nil
220
+ if @scanner.scan(/ +/u)
221
+ criticality = @scanner.scan(/true|false/u)
222
+ raise criticality_is_missing if criticality.nil?
223
+ end
224
+ value = parse_attribute_value if @scanner.check(/:/u)
225
+ raise separator_is_missing unless @scanner.scan_separator
226
+ ChangeRecord::Control.new(type, criticality, value)
227
+ end
228
+
229
+ def parse_controls
230
+ controls = []
231
+ loop do
232
+ control = parse_control
233
+ break if control.nil?
234
+ controls << control
235
+ end
236
+ controls
237
+ end
238
+
239
+ def parse_change_type
240
+ return nil unless @scanner.scan(/changetype:/u)
241
+ @scanner.scan(FILL)
242
+ type = @scanner.check(ATTRIBUTE_TYPE_CHARS)
243
+ raise change_type_value_is_missing if type.nil?
244
+ unless @scanner.scan(/add|delete|modrdn|moddn|modify/u)
245
+ raise unknown_change_type(type)
246
+ end
247
+
248
+ raise separator_is_missing unless @scanner.scan_separator
249
+ type
250
+ end
251
+
252
+ def parse_modify_name_record(klass, dn, controls)
253
+ raise new_rdn_mark_is_missing unless @scanner.scan(/newrdn\b/u)
254
+ new_rdn = parse_attribute_value(false)
255
+ raise new_rdn_value_is_missing if new_rdn.nil?
256
+ raise separator_is_missing unless @scanner.scan_separator
257
+
258
+ unless @scanner.scan(/deleteoldrdn:/u)
259
+ raise delete_old_rdn_mark_is_missing
260
+ end
261
+ @scanner.scan(FILL)
262
+ delete_old_rdn = @scanner.scan(/[01]/u)
263
+ raise delete_old_rdn_value_is_missing if delete_old_rdn.nil?
264
+ raise separator_is_missing unless @scanner.scan_separator
265
+
266
+ if @scanner.scan(/newsuperior\b/u)
267
+ @scanner.scan(FILL)
268
+ new_superior = parse_attribute_value(false)
269
+ raise new_superior_value_is_missing if new_superior.nil?
270
+ new_superior = parse_dn(new_superior)
271
+ raise separator_is_missing unless @scanner.scan_separator
272
+ end
273
+ klass.new(dn, controls, new_rdn, delete_old_rdn, new_superior)
274
+ end
275
+
276
+ def parse_modify_spec
277
+ return nil unless @scanner.check(/(#{ATTRIBUTE_TYPE_CHARS}):/u)
278
+ type = @scanner[1]
279
+ unless @scanner.scan(/(?:add|delete|replace):/u)
280
+ raise unknown_modify_type(type)
281
+ end
282
+ @scanner.scan(FILL)
283
+ attribute, options = parse_attribute_description
284
+ raise separator_is_missing unless @scanner.scan_separator
285
+ attributes = parse_attributes {@scanner.check(/-/u)}
286
+ raise modify_spec_separator_is_missing unless @scanner.scan(/-/u)
287
+ raise separator_is_missing unless @scanner.scan_separator
288
+ [type, attribute, options, attributes]
289
+ end
290
+
291
+ def parse_modify_record(dn, controls)
292
+ operations = []
293
+ loop do
294
+ spec = parse_modify_spec
295
+ break if spec.nil?
296
+ type, attribute, options, attributes = spec
297
+ case type
298
+ when "add"
299
+ klass = ModifyRecord::AddOperation
300
+ when "delete"
301
+ klass = ModifyRecord::DeleteOperation
302
+ when "replace"
303
+ klass = ModifyRecord::ReplaceOperation
304
+ else
305
+ unknown_modify_type(type)
306
+ end
307
+ operations << klass.new(attribute, options, attributes)
308
+ end
309
+ ModifyRecord.new(dn, controls, operations)
310
+ end
311
+
312
+ def parse_change_type_record(dn, controls, change_type)
313
+ case change_type
314
+ when "add"
315
+ attributes = parse_attributes(1)
316
+ AddRecord.new(dn, controls, attributes)
317
+ when "delete"
318
+ DeleteRecord.new(dn, controls)
319
+ when "moddn"
320
+ parse_modify_name_record(ModifyDNRecord, dn, controls)
321
+ when "modrdn"
322
+ parse_modify_name_record(ModifyRDNRecord, dn, controls)
323
+ when "modify"
324
+ parse_modify_record(dn, controls)
325
+ else
326
+ raise unknown_change_type(change_type)
327
+ end
328
+ end
329
+
330
+ def parse_record
331
+ raise dn_mark_is_missing unless @scanner.scan(/dn:/u)
332
+ if @scanner.scan(/:/u)
333
+ @scanner.scan(FILL)
334
+ dn = read_base64_value
335
+ raise dn_is_missing if dn.nil?
336
+ dn = parse_dn(dn)
337
+ else
338
+ @scanner.scan(FILL)
339
+ dn = @scanner.scan(/#{SAFE_STRING}$/u)
340
+ if dn.nil?
341
+ partial_dn = @scanner.scan(SAFE_STRING)
342
+ raise dn_has_invalid_character(@scanner.check(/./u)) if partial_dn
343
+ raise dn_is_missing
344
+ end
345
+ dn = parse_dn(dn)
346
+ end
347
+
348
+ raise separator_is_missing unless @scanner.scan_separator
349
+
350
+ controls = parse_controls
351
+ change_type = parse_change_type
352
+ raise change_type_is_missing if change_type.nil? and !controls.empty?
353
+
354
+ if change_type
355
+ parse_change_type_record(dn, controls, change_type)
356
+ else
357
+ attributes = parse_attributes(1)
358
+ ContentRecord.new(dn, attributes)
359
+ end
360
+ end
361
+
362
+ def parse_records
363
+ records = []
364
+ loop do
365
+ records << parse_record
366
+ break if @scanner.eos?
367
+ raise separator_is_missing if @scanner.scan_separator.nil?
368
+ end
369
+ records
370
+ end
371
+
31
372
  def invalid_ldif(reason)
32
- LdifInvalid.new(@source, reason)
373
+ LdifInvalid.new(@source, reason, @scanner.line, @scanner.column)
33
374
  end
34
375
 
35
376
  def version_spec_is_missing
36
377
  invalid_ldif(_("version spec is missing"))
37
378
  end
38
379
 
380
+ def version_number_is_missing
381
+ invalid_ldif(_("version number is missing"))
382
+ end
383
+
39
384
  def unsupported_version(version)
40
385
  invalid_ldif(_("unsupported version: %d") % version)
41
386
  end
387
+
388
+ def separator_is_missing
389
+ invalid_ldif(_("separator is missing"))
390
+ end
391
+
392
+ def dn_mark_is_missing
393
+ invalid_ldif(_("'dn:' is missing"))
394
+ end
395
+
396
+ def dn_is_missing
397
+ invalid_ldif(_("DN is missing"))
398
+ end
399
+
400
+ def invalid_dn(dn_string, reason)
401
+ invalid_ldif(_("DN is invalid: %s: %s") % [dn_string, reason])
402
+ end
403
+
404
+ def dn_has_invalid_character(character)
405
+ invalid_ldif(_("DN has an invalid character: %s") % character)
406
+ end
407
+
408
+ def attribute_type_is_missing
409
+ invalid_ldif(_("attribute type is missing"))
410
+ end
411
+
412
+ def option_is_missing
413
+ invalid_ldif(_("option is missing"))
414
+ end
415
+
416
+ def attribute_value_separator_is_missing
417
+ invalid_ldif(_("':' is missing"))
418
+ end
419
+
420
+ def invalid_uri(uri_string, message)
421
+ invalid_ldif(_("URI is invalid: %s: %s") % [uri_string, message])
422
+ end
423
+
424
+ def modify_spec_separator_is_missing
425
+ invalid_ldif(_("'-' is missing"))
426
+ end
427
+
428
+ def unknown_change_type(change_type)
429
+ invalid_ldif(_("unknown change type: %s") % change_type)
430
+ end
431
+
432
+ def change_type_is_missing
433
+ invalid_ldif(_("change type is missing"))
434
+ end
435
+
436
+ def control_type_is_missing
437
+ invalid_ldif(_("control type is missing"))
438
+ end
439
+
440
+ def criticality_is_missing
441
+ invalid_ldif(_("criticality is missing"))
442
+ end
443
+
444
+ def change_type_value_is_missing
445
+ invalid_ldif(_("change type value is missing"))
446
+ end
447
+
448
+ def attribute_spec_is_missing
449
+ invalid_ldif(_("attribute spec is missing"))
450
+ end
451
+
452
+ def new_rdn_mark_is_missing
453
+ invalid_ldif(_("'newrdn:' is missing"))
454
+ end
455
+
456
+ def new_rdn_value_is_missing
457
+ invalid_ldif(_("new RDN value is missing"))
458
+ end
459
+
460
+ def delete_old_rdn_mark_is_missing
461
+ invalid_ldif(_("'deleteoldrdn:' is missing"))
462
+ end
463
+
464
+ def delete_old_rdn_value_is_missing
465
+ invalid_ldif(_("delete old RDN value is missing"))
466
+ end
467
+
468
+ def new_superior_value_is_missing
469
+ invalid_ldif(_("new superior value is missing"))
470
+ end
471
+
472
+ def unknown_modify_type(type)
473
+ invalid_ldif(_("unknown modify type: %s") % type)
474
+ end
475
+ end
476
+
477
+ class Scanner
478
+ SEPARATOR = /(?:\r\n|\n)/u
479
+
480
+ def initialize(source)
481
+ @source = source
482
+ @scanner = StringScanner.new(@source)
483
+ @sub_scanner = next_segment || StringScanner.new("")
484
+ end
485
+
486
+ def scan(regexp)
487
+ @sub_scanner = next_segment if @sub_scanner.eos?
488
+ @sub_scanner.scan(regexp)
489
+ end
490
+
491
+ def check(regexp)
492
+ @sub_scanner = next_segment if @sub_scanner.eos?
493
+ @sub_scanner.check(regexp)
494
+ end
495
+
496
+ def scan_separator
497
+ return @scanner.scan(SEPARATOR) if @sub_scanner.eos?
498
+
499
+ scan(SEPARATOR)
500
+ end
501
+
502
+ def check_separator
503
+ return @scanner.check(SEPARATOR) if @sub_scanner.eos?
504
+
505
+ check(SEPARATOR)
506
+ end
507
+
508
+ def scan_separators
509
+ return @scanner.scan(/#{SEPARATOR}+/u) if @sub_scanner.eos?
510
+
511
+ sub_result = scan(/#{SEPARATOR}+/u)
512
+ return nil if sub_result.nil?
513
+ return sub_result unless @sub_scanner.eos?
514
+
515
+ result = @scanner.scan(/#{SEPARATOR}+/u)
516
+ return sub_result if result.nil?
517
+
518
+ sub_result + result
519
+ end
520
+
521
+ def [](*args)
522
+ @sub_scanner[*args]
523
+ end
524
+
525
+ def eos?
526
+ @sub_scanner = next_segment if @sub_scanner.eos?
527
+ @sub_scanner.eos? and @scanner.eos?
528
+ end
529
+
530
+ def line
531
+ _consumed_source = consumed_source
532
+ return 1 if _consumed_source.empty?
533
+
534
+ n = _consumed_source.to_a.size
535
+ n += 1 if _consumed_source[-1, 1] == "\n"
536
+ n
537
+ end
538
+
539
+ def column
540
+ _consumed_source = consumed_source
541
+ return 1 if _consumed_source.empty?
542
+
543
+ position - (_consumed_source.rindex("\n") || -1)
544
+ end
545
+
546
+ def position
547
+ @scanner.pos - (@sub_scanner.string.length - @sub_scanner.pos)
548
+ end
549
+
550
+ private
551
+ def next_segment
552
+ loop do
553
+ segment = @scanner.scan(/.+(?:#{SEPARATOR} .*)*#{SEPARATOR}?/u)
554
+ return @sub_scanner if segment.nil?
555
+ next if segment[0, 1] == "#"
556
+ return StringScanner.new(segment.gsub(/\r?\n /u, ''))
557
+ end
558
+ end
559
+
560
+ def consumed_source
561
+ @source[0, position]
562
+ end
42
563
  end
43
564
 
44
565
  class << self
@@ -46,6 +567,335 @@ module ActiveLdap
46
567
  Parser.new(ldif).parse
47
568
  end
48
569
  end
570
+
571
+ include Enumerable
572
+
573
+ attr_reader :version, :records
574
+ def initialize(records=[])
575
+ @version = 1
576
+ @records = records
577
+ end
578
+
579
+ def <<(record)
580
+ @records << record
581
+ end
582
+
583
+ def each(&block)
584
+ @records.each(&block)
585
+ end
586
+
587
+ def to_s
588
+ result = "version: #{@version}\n"
589
+ result << @records.collect do |record|
590
+ record.to_s
591
+ end.join("\n")
592
+ result
593
+ end
594
+
595
+ def ==(other)
596
+ other.is_a?(self.class) and
597
+ @version == other.version and @records == other.records
598
+ end
599
+
600
+ class Record
601
+ include GetTextSupport
602
+
603
+ attr_reader :dn, :attributes
604
+ def initialize(dn, attributes)
605
+ @dn = dn
606
+ @attributes = attributes
607
+ end
608
+
609
+ def to_hash
610
+ attributes.merge({"dn" => dn})
611
+ end
612
+
613
+ def to_s
614
+ result = to_s_prelude
615
+ result << to_s_content
616
+ result
617
+ end
618
+
619
+ def ==(other)
620
+ other.is_a?(self.class) and
621
+ @dn == other.dn and
622
+ Attributes.normalize(@attributes) ==
623
+ Attributes.normalize(other.attributes)
624
+ end
625
+
626
+ private
627
+ def to_s_prelude
628
+ Attribute.encode("dn", dn)
629
+ end
630
+
631
+ def to_s_content
632
+ Attributes.encode(@attributes)
633
+ end
634
+ end
635
+
636
+ class ContentRecord < Record
637
+ end
638
+
639
+ class ChangeRecord < Record
640
+ attr_reader :controls, :change_type
641
+ def initialize(dn, attributes, controls, change_type)
642
+ super(dn, attributes)
643
+ @controls = controls
644
+ @change_type = change_type
645
+ end
646
+
647
+ def add?
648
+ @change_type == "add"
649
+ end
650
+
651
+ def delete?
652
+ @change_type == "delete"
653
+ end
654
+
655
+ def modify?
656
+ @change_type == "modify"
657
+ end
658
+
659
+ def modify_dn?
660
+ @change_type == "moddn"
661
+ end
662
+
663
+ def modify_rdn?
664
+ @change_type == "modrdn"
665
+ end
666
+
667
+ def ==(other)
668
+ super(other) and
669
+ @controls = other.controls and
670
+ @change_type == other.change_type
671
+ end
672
+
673
+ private
674
+ def to_s_prelude
675
+ result = super
676
+ @controls.each do |control|
677
+ result << control.to_s
678
+ end
679
+ result
680
+ end
681
+
682
+ def to_s_content
683
+ result = "changetype: #{@change_type}\n"
684
+ result << super
685
+ result
686
+ end
687
+
688
+ class Control
689
+ attr_reader :type, :value
690
+ def initialize(type, criticality, value)
691
+ @type = type
692
+ @criticality = normalize_criticality(criticality)
693
+ @value = value
694
+ end
695
+
696
+ def criticality?
697
+ @criticality
698
+ end
699
+
700
+ def to_a
701
+ [@type, @criticality, @value]
702
+ end
703
+
704
+ def to_hash
705
+ {
706
+ :type => @type,
707
+ :criticality => @criticality,
708
+ :value => @value,
709
+ }
710
+ end
711
+
712
+ def to_s
713
+ result = "control: #{@type}"
714
+ result << " #{@criticality}" unless @criticality.nil?
715
+ result << @value if @value
716
+ result << "\n"
717
+ result
718
+ end
719
+
720
+ def ==(other)
721
+ other.is_a?(self.class) and
722
+ @type == other.type and
723
+ @criticality = other.criticality and
724
+ @value == other.value
725
+ end
726
+
727
+ private
728
+ def normalize_criticality(criticality)
729
+ case criticality
730
+ when "true", true
731
+ true
732
+ when "false", false
733
+ false
734
+ when nil
735
+ nil
736
+ else
737
+ raise ArgumentError,
738
+ _("invalid criticality value: %s") % criticality.inspect
739
+ end
740
+ end
741
+ end
742
+ end
743
+
744
+ class AddRecord < ChangeRecord
745
+ def initialize(dn, controls=[], attributes={})
746
+ super(dn, attributes, controls, "add")
747
+ end
748
+ end
749
+
750
+ class DeleteRecord < ChangeRecord
751
+ def initialize(dn, controls=[])
752
+ super(dn, {}, controls, "delete")
753
+ end
754
+ end
755
+
756
+ class ModifyNameRecord < ChangeRecord
757
+ attr_reader :new_rdn, :new_superior
758
+ def initialize(dn, controls, change_type,
759
+ new_rdn, delete_old_rdn, new_superior)
760
+ super(dn, {}, controls, change_type)
761
+ @new_rdn = new_rdn
762
+ @delete_old_rdn = normalize_delete_old_rdn(delete_old_rdn)
763
+ @new_superior = new_superior
764
+ end
765
+
766
+ def delete_old_rdn?
767
+ @delete_old_rdn
768
+ end
769
+
770
+ private
771
+ def normalize_delete_old_rdn(delete_old_rdn)
772
+ case delete_old_rdn
773
+ when "1", true
774
+ true
775
+ when "0", false
776
+ false
777
+ when nil
778
+ nil
779
+ else
780
+ raise ArgumentError,
781
+ _("invalid deleteoldrdn value: %s") % delete_old_rdn.inspect
782
+ end
783
+ end
784
+
785
+ def to_s_content
786
+ result = super
787
+ result << "newrdn: #{@new_rdn}\n"
788
+ result << "deleteoldrdn: #{@delete_old_rdn ? 1 : 0}\n"
789
+ result << Attribute.encode("newsuperior", @new_superior) if @new_superior
790
+ result
791
+ end
792
+ end
793
+
794
+ class ModifyDNRecord < ModifyNameRecord
795
+ def initialize(dn, controls, new_rdn, delete_old_rdn, new_superior=nil)
796
+ super(dn, controls, "moddn", new_rdn, delete_old_rdn, new_superior)
797
+ end
798
+ end
799
+
800
+ class ModifyRDNRecord < ModifyNameRecord
801
+ def initialize(dn, controls, new_rdn, delete_old_rdn, new_superior=nil)
802
+ super(dn, controls, "modrdn", new_rdn, delete_old_rdn, new_superior)
803
+ end
804
+ end
805
+
806
+ class ModifyRecord < ChangeRecord
807
+ include Enumerable
808
+
809
+ attr_reader :operations
810
+ def initialize(dn, controls=[], operations=[])
811
+ super(dn, {}, controls, "modify")
812
+ @operations = operations
813
+ end
814
+
815
+ def each(&block)
816
+ @operations.each(&block)
817
+ end
818
+
819
+ def <<(operation)
820
+ @operations << operation
821
+ end
822
+
823
+ def add_operation(type, attribute, options, attributes)
824
+ klass = self.class.const_get("#{type.to_s.capitalize}Operation")
825
+ self << klass.new(attribute, options, attributes)
826
+ end
827
+
828
+ def ==(other)
829
+ super(other) and @operations == other.operations
830
+ end
831
+
832
+ private
833
+ def to_s_content
834
+ result = super
835
+ return result if @operations.empty?
836
+ @operations.collect do |operation|
837
+ result << "#{operation}-\n"
838
+ end
839
+ result
840
+ end
841
+
842
+ class Operation
843
+ attr_reader :type, :attribute, :options, :attributes
844
+ def initialize(type, attribute, options, attributes)
845
+ @type = type
846
+ @attribute = attribute
847
+ @options = options
848
+ @attributes = attributes
849
+ end
850
+
851
+ def full_attribute_name
852
+ [@attribute, *@options].join(";")
853
+ end
854
+
855
+ def add?
856
+ @type == "add"
857
+ end
858
+
859
+ def delete?
860
+ @type == "delete"
861
+ end
862
+
863
+ def replace?
864
+ @type == "replace"
865
+ end
866
+
867
+ def to_s
868
+ Attribute.encode(@type, full_attribute_name) +
869
+ Attributes.encode(@attributes)
870
+ end
871
+
872
+ def ==(other)
873
+ other.is_a?(self.class) and
874
+ @type == other.type and
875
+ full_attribute_name == other.full_attribute_name and
876
+ Attributes.normalize(@attributes) ==
877
+ Attributes.normalize(other.attributes)
878
+ end
879
+ end
880
+
881
+ class AddOperation < Operation
882
+ def initialize(attribute, options, attributes)
883
+ super("add", attribute, options, attributes)
884
+ end
885
+ end
886
+
887
+ class DeleteOperation < Operation
888
+ def initialize(attribute, options, attributes)
889
+ super("delete", attribute, options, attributes)
890
+ end
891
+ end
892
+
893
+ class ReplaceOperation < Operation
894
+ def initialize(attribute, options, attributes)
895
+ super("replace", attribute, options, attributes)
896
+ end
897
+ end
898
+ end
49
899
  end
50
900
 
51
901
  LDIF = Ldif