model_security_generator 0.0.1

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.
data/README ADDED
File without changes
data/USAGE ADDED
File without changes
@@ -0,0 +1,75 @@
1
+ class ModelSecurityGenerator < Rails::Generator::NamedBase
2
+ def manifest
3
+ record do |m|
4
+ # Check for class naming collisions.
5
+ m.class_collisions class_path, "User", "UserController", "UserMailer", "UserSupport", "Modal", "ModalHelper", "ModelSecurity", "ModelSecurityHelper"
6
+
7
+ # Libraries
8
+ m.file "modal.rb", "lib/modal.rb"
9
+ m.file "model_security.rb", "lib/model_security.rb"
10
+ m.file "once.rb", "lib/once.rb"
11
+ m.file "user_support.rb", "lib/user_support.rb"
12
+
13
+ # Helpers
14
+ m.file "modal_helper.rb", "app/helpers/modal_helper.rb"
15
+ m.file "model_security_helper.rb", "app/helpers/model_security_helper.rb"
16
+
17
+ # User
18
+ m.file "user.rb", File.join("app/models", "user.rb")
19
+ m.file "user_controller.rb", File.join("app/controllers", "user_controller.rb")
20
+
21
+ # User mailer
22
+ m.file "user_mailer.rb", File.join("app/models", "user_mailer.rb")
23
+
24
+ # Testing related stuff
25
+ m.file "user_controller_test.rb", "test/functional/user_controller_test.rb"
26
+ m.file "user_test.rb", "test/unit/user_test.rb"
27
+ m.file "mock_mailer.rb", "test/mocks/test/user_mailer.rb"
28
+ m.file "mock_time.rb", "test/mocks/test/time.rb"
29
+ m.file "users.yml", "test/fixtures/users.yml"
30
+
31
+ # Schemas, configuration and miscellaneous
32
+ m.file "schema.sql", "db/schema.sql"
33
+ m.file "users.sql", "db/users.sql"
34
+
35
+ # Layout and stylesheet.
36
+ m.file "scaffold.rhtml", "app/views/layouts/scaffold.rhtml"
37
+ m.file "scaffold.css", "public/stylesheets/scaffold.css"
38
+ m.file "standard.rhtml", "app/views/layouts/standard.rhtml"
39
+ m.file "standard.css", "public/stylesheets/standard.css"
40
+
41
+ # Views
42
+ m.directory File.join("app/views", "user", file_name)
43
+ user_views.each do |action|
44
+ m.file "view_#{action}.rhtml",
45
+ File.join("app/views", "user", file_name, "#{action}.rhtml")
46
+ end
47
+
48
+ # Partials
49
+ m.directory File.join("app/views", "user", file_name)
50
+ partial_views.each do |action|
51
+ m.file "_view_#{action}.rhtml",
52
+ File.join("app/views", "user", file_name, "_#{action}.rhtml")
53
+ end
54
+
55
+ # Mailer
56
+ m.directory File.join("app/views", "user_mailer")
57
+ mailer_views.each do |action|
58
+ m.file "mailer_#{action}.rhtml",
59
+ File.join("app/views", "user_mailer", "#{action}.rhtml")
60
+ end
61
+ end
62
+ end
63
+
64
+ def user_views
65
+ %w(activate edit forgot_password_done list login login_admin logout new show success)
66
+ end
67
+
68
+ def partial_views
69
+ %w(form)
70
+ end
71
+
72
+ def mailer_views
73
+ %w(forgot_password new_user)
74
+ end
75
+ end
@@ -0,0 +1,27 @@
1
+ <%= error_messages_for 'user' %>
2
+
3
+ <!--[form:user]-->
4
+ <table>
5
+ <% for column in User.content_columns %>
6
+ <% if User.display?(column.name) %>
7
+ <tr>
8
+ <th>
9
+ <label for="user_#{column.name}">
10
+ <%= column.human_name %>:
11
+ </label>
12
+ </th>
13
+ <td>
14
+ <% if column.name.index('password') %>
15
+ <%= password_field('user', column.name) %>
16
+ <% else %>
17
+ <%= text_field('user', column.name) %>
18
+ <% end %>
19
+ </td>
20
+ </tr>
21
+ <% end %>
22
+ <% end %>
23
+
24
+ </table>
25
+
26
+ <!--[eoform:user]-->
27
+
@@ -0,0 +1,18 @@
1
+ <h1>Recover Your Login or Re-Set Your Password</h1>
2
+
3
+ <p>
4
+ Please enter the email address you used when you registered your login.
5
+ A message will be sent to that email with the information you'll need to
6
+ reset your password.
7
+ </p>
8
+ <%= start_form_tag :action => 'forgot_password' %>
9
+ <table>
10
+ <tr>
11
+ <th><label for="user_email">Email</label></th>
12
+ <td><%= text_field 'user', 'email' %></td>
13
+ </tr>
14
+ </table>
15
+ <%= submit_tag "Send Email" %>
16
+ <%= end_form_tag %>
17
+
18
+ <%= link_to 'Back', :action => 'list' %>
@@ -0,0 +1,10 @@
1
+ Dear <%= @user.name %>,
2
+
3
+ Your login '<%= @user.login %>' has been created, but you must now activate it.
4
+ To do so, please access the following URL before <%= @user.token_expiry %>:
5
+
6
+ <%= @url %>
7
+
8
+ Many Thanks
9
+
10
+ The Management
@@ -0,0 +1,16 @@
1
+ require 'models/user_mailer.rb'
2
+
3
+ ActionMailer::Base.class_eval {
4
+ @@inject_one_error = false
5
+ cattr_accessor :inject_one_error
6
+
7
+ private
8
+ def perform_delivery_test(mail)
9
+ if inject_one_error
10
+ ActionMailer::Base::inject_one_error = false
11
+ raise "Failed to send email" if raise_delivery_errors
12
+ else
13
+ deliveries << mail
14
+ end
15
+ end
16
+ }
@@ -0,0 +1,17 @@
1
+ require 'time'
2
+
3
+ Time.class_eval {
4
+ @@advance_by_days = 0
5
+ cattr_accessor :advance_by_days
6
+
7
+ class << Time
8
+ alias now_old now
9
+ def now
10
+ if Time.advance_by_days != 0
11
+ return Time.at(now_old.to_i + Time.advance_by_days * 60 * 60 * 24 + 1)
12
+ else
13
+ now_old
14
+ end
15
+ end
16
+ end
17
+ }
@@ -0,0 +1,82 @@
1
+ # Implement modal web pages. When you are done with one of these pages, they
2
+ # will redirect you to an internal page anchor within the page that linked to
3
+ # them. In other words, you don't just go back to the referring page. You go
4
+ # back to the place within the referring page where the link was.
5
+ #
6
+ # This implementation is in two parts: the library module Modal, and the view
7
+ # helper ModalHelper.
8
+ #
9
+ module Modal
10
+
11
+ # This is meant to be called as a before filter by the ApplicationController.
12
+ # It always returns true and thus will not interrupt your action.
13
+ #
14
+ # The link_modal and modal_form methods generate an internal page
15
+ # anchor and a +ret+ parameter that will be passed in a GET or PUT.
16
+ # The anchor refers the location within the calling page to return to
17
+ # after a modal action, and +ret+ encodes the URL of that anchor.
18
+ # store_return retrieves +ret+ from the request and saves it in a
19
+ # return-to address URL in the session. The return-to URL will be used
20
+ # by redirect_back_or_default to return to a location within the calling
21
+ # page that linked to the current page. This is nicer than simply returning
22
+ # to the top of the page, especially when returning to a long page like
23
+ # a list of blog comments, because the reader will probably want to continue
24
+ # reading at that point.
25
+ #
26
+ # If the request does not contain a +ret+ parameter and the return-to
27
+ # address is not set and REFERER is set in the environment, the
28
+ # return-to address is set to REFERER.
29
+ #
30
+ def modal_setup
31
+ # Set modal return.
32
+ if r = @params['ret']
33
+ self.return_location = r.sub(/\.L/, '#L')
34
+ elsif return_location.nil?
35
+ self.return_location = @request.env['HTTP_REFERER']
36
+ end
37
+ true
38
+ end
39
+
40
+ # Get the location to return to after a modal action. The argument should
41
+ # be a string containing a URL.
42
+ def return_location
43
+ @session[:return_to]
44
+ end
45
+
46
+ # Set the location to return to after a modal action. The return value should
47
+ # be a string containing a URL.
48
+ def return_location= a
49
+ @session[:return_to] = a
50
+ end
51
+
52
+ # Store the location to return to after a modal action from the request URI.
53
+ # This is usually called before redirecting to another action.
54
+ def store_location
55
+ self.return_location = @request.request_uri
56
+ end
57
+
58
+ # Redirect to the stored return location. If no stored return location
59
+ # is available, redirect to the default action given in the arguments.
60
+ # The arguments are just like those of redirect_to.
61
+ def redirect_back_or_default(attributes = {}, *method_params)
62
+ r = return_location
63
+ if r
64
+ redirect_to_url r
65
+ self.return_location = nil
66
+ else
67
+ redirect_to attributes, method_params
68
+ end
69
+ end
70
+
71
+ # Create a URL optionally including an internal anchor. If the +id+ argument
72
+ # is given, that will be used to create the name of the internal anchor. It's
73
+ # generally a number, but anything that will convert to a string that does
74
+ # not contain characters that would have special meaning within a URL, like
75
+ # spaces, will do.
76
+ def return_url(id = nil)
77
+ s = @request.request_uri.sub(/\??ret=[^\&$]*/,'')
78
+ s += '.L' + id.to_s if id
79
+ s
80
+ end
81
+
82
+ end
@@ -0,0 +1,29 @@
1
+ require 'modal'
2
+
3
+ module ModalHelper
4
+ include Modal
5
+
6
+ # Include "ret" in a form, for modal forms.
7
+ def modal_form(id = nil)
8
+ s = '<input type="hidden" name="ret" value="' + return_url(id) + '"'
9
+ s += 'id="L' + id.to_s + '"' if id
10
+ s += '/>'
11
+ s
12
+ end
13
+
14
+ # Include "ret" in a link, for modal links.
15
+ def modal_link_to(name, id = nil, options = {}, html_options = nil, *dict_params)
16
+ html_options = {} if not html_options
17
+ html_options['id'] = 'L' + id.to_s if id
18
+ html_options = html_options.stringify_keys
19
+ convert_confirm_option_to_javascript!(html_options)
20
+ if options.is_a?(String)
21
+ l = content_tag "a", name || options, html_options.merge("href" => options)
22
+ else
23
+ s = url_for(options, dict_params)
24
+ s += ('?ret=' + return_url(id))
25
+ l = content_tag("a", (name || url_for(options, dict_params)), html_options.merge("href" => s))
26
+ end
27
+ l
28
+ end
29
+ end
@@ -0,0 +1,334 @@
1
+ # The ModelSecurity module allows you to specify security permissions on any
2
+ # or all of the attributes of a model implemented using ActiveRecord.
3
+ #
4
+ # Security permissions are
5
+ # specified in the declaration of the model's class, similarly to the way
6
+ # you can specify validators. The specification includes the names of
7
+ # attributes to which permissions apply, and an optional permission test that
8
+ # should return true or false depending on whether the access should be allowed
9
+ # or denied.
10
+ #
11
+ # let_read :attribute|:all [[, :attribute ] ...], [:if => :test-name] [do block end]
12
+ # let_write :attribute|:all [[, :attribute ] ...], [:if => :test-name] [do block end]
13
+ # let_access :attribute|:all [[, :attribute ] ...], [:if => :test-name] [do block end]
14
+ #
15
+ # let_read specifies when the attribute can be read, let_write specifies when
16
+ # it can be written, and let_access does both.
17
+ #
18
+ # If no permission test is specified, that is the same as specifying a test
19
+ # that always returns true. Two stub tests are provided:
20
+ #
21
+ # :always?
22
+ #
23
+ # Always returns true.
24
+ #
25
+ # :never?
26
+ #
27
+ # Always returns false.
28
+ #
29
+ # You can easily add your own tests as instance methods of your model:
30
+ #
31
+ # let_read :phone_number :if => :admin?
32
+ #
33
+ # def admin?
34
+ # return $current_login.is_the_administrator
35
+ # end
36
+ #
37
+ # If the permission test is specified using the syntax
38
+ # :if => :test-name
39
+ # it will be run as a method of the model this way:
40
+ # self.send(:test-name)
41
+ #
42
+ # If the permission test is specified as a block, using *do* and *end*,
43
+ # it will be called with the binding of the active record instance that
44
+ # is being accessed.
45
+ #
46
+ # Permission tests can also be strings, and these are passed to eval().
47
+ #
48
+ # The special attribute name :all means that a test will be applied to all
49
+ # attributes of the model. Any tests for :all are run first, then any tests
50
+ # for the specific attribute. Any test that returns true ends the run, further
51
+ # tests will not be evaluated.
52
+ #
53
+ # If *no* security permissions are declared for an attribute or :all, that
54
+ # attribute may always be accessed. Once a test for :all is delcared, that
55
+ # test will apply to all attributes of the model.
56
+ #
57
+ # The security tests themselves may access any data with impunity. A global
58
+ # variable is used to disable further security testing while a security test
59
+ # is in progress.
60
+ #
61
+ # = Display Control
62
+ #
63
+ # A companion mechanism is used to control views, including scaffold views,
64
+ # using a syntax similar to that for security specifications:
65
+ #
66
+ # let_display :attribute|:all [[, :attribute ] ...], [:if => :test-name] [do block end]
67
+ #
68
+ # let_display is mostly useful for specifying if a table view should have a
69
+ # column for a particular attribute. Its tests must be declared as class
70
+ # methods of the model, while the tests of let_read, let_write, and
71
+ # let_access are instance methods. This is because the information declared
72
+ # by let_display is accessed before iteration over active records begins.
73
+ #
74
+ #
75
+ # = Accessing Security Test Results
76
+ #
77
+ # ModelSecurity provides two instance methods, readable? and writable?
78
+ # to inform the program if a particular attribute can be accessed. The class
79
+ # method display? will return true or false depending upon whether a
80
+ # particular attribute should be displayed. These can
81
+ # be used to modify a view so that any non-writable data will not be presented
82
+ # in an editable field. ModelSecurityHelper overloads the methods that are
83
+ # usually
84
+ # used to edit models so that they will not attempt to read or write what they
85
+ # aren't permitted and will render appropriately for the permissions on any
86
+ # model attribute. Those methods are: check_box, file_field, hidden_field,
87
+ # password_field, radio_button, text_area, text_field.
88
+ # ModelSecurityHelper also replaces the scaffold views with versions that
89
+ # never access data when not permitted to, render appropriately for the
90
+ # permissions on an attribute, and omit columns for which display? returns
91
+ # false.
92
+ #
93
+ #
94
+ # = Exceptions
95
+ #
96
+ # ActiveRecord provides two internal methods that perform normal attribute
97
+ # accesses: read_attribute, and write_attribute. These are overloaded to
98
+ # perform security testing, and will raise *SecurityError* when an unpermitted
99
+ # access is attempted.
100
+ #
101
+ module ModelSecurity
102
+ end
103
+
104
+ require 'once'
105
+
106
+ module ModelSecurity::BothClassAndInstanceMethods
107
+ private
108
+ # Run a single test.
109
+ def run_test t
110
+ case t.class.name
111
+ when 'Proc'
112
+ return t.call(binding)
113
+ when 'Symbol'
114
+ return self.send(t)
115
+ when 'String'
116
+ return eval(t)
117
+ else
118
+ return false
119
+ end
120
+ end
121
+
122
+ # This does the permission test for readable?, writable?, and display?.
123
+ # A global variable is used to short-circuit recursion.
124
+ #
125
+ # FIX: The global variable should be replaced with a thread-local variable
126
+ # once I learn how to make one.
127
+ #
128
+ def run_tests(d, attribute)
129
+ global = d[:all]
130
+ local = d[attribute.to_sym]
131
+ result = true
132
+
133
+ if (global or local) and ($in_test_permission != true)
134
+ $in_test_permission = true
135
+ result = (run_test(global) or run_test(local))
136
+ $in_test_permission = false
137
+ end
138
+ return result
139
+ end
140
+
141
+ public
142
+ # Stub test for let_read and friends. Always returns true.
143
+ def always?
144
+ true
145
+ end
146
+
147
+ # Stub test for let_read and friends. Always returns false.
148
+ def never?
149
+ false
150
+ end
151
+ end
152
+
153
+ module ModelSecurity
154
+ include ModelSecurity::BothClassAndInstanceMethods
155
+
156
+ private
157
+ # This does the permission test for readable? or writable?.
158
+ def test_permission(permission, attribute)
159
+ if (d = self.class.read_inheritable_attribute(permission))
160
+ return run_tests(d, attribute)
161
+ else
162
+ return true
163
+ end
164
+ end
165
+
166
+ # Responsible for raising an exception when an unpermitted security
167
+ # access is attempted. *permission* is :let_read or :let_write.
168
+ # *attribute* is the name of the attribute upon which an access is
169
+ # being attempted.
170
+ #
171
+ # FIX: Is exception information displayed in production mode? I put a lot
172
+ # of sensitive data in this exception.
173
+ #
174
+ def security_error(permission, attribute)
175
+ global = nil
176
+ local = nil
177
+
178
+ if (d = self.class.read_inheritable_attribute(permission))
179
+ global = d[:all]
180
+ local = d[attribute.to_sym]
181
+ end
182
+
183
+ message = "SECURITY VIOLATON: #{permission} on attribute #{attribute}" \
184
+ "\n\tof object: #{self.inspect}."
185
+
186
+ if global
187
+ message << "\n\tTest for :all is #{global.inspect}."
188
+ end
189
+
190
+ if local
191
+ message << "\n\tTest for :#{attribute} is #{local.inspect}."
192
+ end
193
+
194
+ raise SecurityError.new(message)
195
+ end
196
+
197
+ public
198
+ # Ruby interpreter magic to cause the class methods herein to work correctly
199
+ # while a class including this module is still being declared.
200
+ def self.append_features(base)
201
+ super
202
+ base.extend(ClassMethods)
203
+ end
204
+
205
+ # Return true if a read of *attribute* is permitted.
206
+ # *attribute* should be a symbol, and should be the
207
+ # name of a database field for this model.
208
+ def readable?(attribute)
209
+ test_permission(:let_read, attribute)
210
+ end
211
+
212
+ # Overloads ActiveRecord::Base#read_attribute. Read the attribute if that is
213
+ # permitted. Otherwise, throw an exception.
214
+ def read_attribute(name)
215
+ if not readable?(name)
216
+ security_error(:let_read, name)
217
+ end
218
+ old_read_attribute(name)
219
+ end
220
+
221
+ # Return true if a write of *attribute* is permitted.
222
+ # *attribute* should be a symbol, and should be the
223
+ # name of a database field for this model.
224
+ def writable?(attribute)
225
+ test_permission(:let_write, attribute)
226
+ end
227
+
228
+ # Overloads ActiveRecord::Base#write_attribute. Write the attribute if that is
229
+ # permitted. Otherwise, throw an exception.
230
+ def write_attribute(name, value)
231
+ if not writable?(name)
232
+ security_error(:let_write, name)
233
+ raise SecurityError
234
+ end
235
+ old_write_attribute(name, value)
236
+ end
237
+ end
238
+
239
+ # Class methods for ModelSecurity. They are broken out this way so that they
240
+ # can be fed to base.extend(), Ruby interpreter magic so that class methods
241
+ # of this module work while a class including it is being defined.
242
+ # Uses a Rails-internal inheritable-attribute mechanism so that this data in a
243
+ # derived class survives modification of similar data in its base class.
244
+ #
245
+ # I'm not sure that write_inheritable_attribute() is the right way to go, either
246
+ # here or in the way that validators are declared. Why not declare the data
247
+ # independently in each subclass and then use *super* to traverse the
248
+ # inheritance graph when accessing it?
249
+ #
250
+ module ModelSecurity::ClassMethods
251
+ private
252
+ include ModelSecurity::BothClassAndInstanceMethods
253
+
254
+ # Internal function where the work of let_read, let_write, let_access,
255
+ # and let_display is done. Store the tests to be run for each attribute
256
+ # in the class, to be evaluated later. *permission* is :let_read,
257
+ # :let_write, or :let_display. *arguments* is a list of attributes
258
+ # upon which security permissions are being declared and a hash
259
+ # containing all options, currently just :if . *block*, if given,
260
+ # contains a security test.
261
+ #
262
+ def let(permission, arguments, block)
263
+ attributes = [] # List of attributes that this permission applies to.
264
+ parameters = {} # Optional parameters, currently only :if
265
+ procedure = nil # Permission-test procedure.
266
+
267
+ arguments.each { | argument |
268
+ case argument.class.name
269
+ when 'Hash'
270
+ parameters.merge! argument
271
+ else
272
+ attributes << argument
273
+ end
274
+ }
275
+ if not block.nil?
276
+ procedure = block
277
+ elsif (p = parameters[:if])
278
+ procedure = p
279
+ else
280
+ procedure = :always?
281
+ end
282
+
283
+ d = {}
284
+ attributes.each { | name | d[name] = procedure }
285
+ write_inheritable_hash(permission, d)
286
+ end
287
+
288
+ public
289
+ # Return true if display of the attribute is permitted. *attribute* is a
290
+ # symbol, and should match a field in the database schema corresponding to
291
+ # this model.
292
+ def display?(attribute)
293
+ if (d = read_inheritable_attribute(:let_display))
294
+ return run_tests(d, attribute)
295
+ else
296
+ return true
297
+ end
298
+ end
299
+
300
+ # Declare whether reads and writes are permitted on the named attributes.
301
+ def let_access(*arguments, &block)
302
+ let(:let_read, arguments, block)
303
+ let(:let_write, arguments, block)
304
+ end
305
+
306
+ # Declare whether display of the named attribute is permitted.
307
+ def let_display(*arguments, &block)
308
+ let(:let_display, arguments, block)
309
+ end
310
+
311
+
312
+ # Declare whether read is permitted upon the named attributes.
313
+ def let_read(*arguments, &block)
314
+ let(:let_read, arguments, block)
315
+ end
316
+
317
+ # Declare whether write is permitted upon the named attributes.
318
+ def let_write(*arguments, &block)
319
+ let(:let_write, arguments, block)
320
+ end
321
+ end
322
+
323
+ class ActiveRecord::Base
324
+ private
325
+ once ('@aliasesDone') {
326
+ # Provides access to the pre-overload version of read_attribute, which
327
+ # is called by the overloaded version.
328
+ alias old_read_attribute read_attribute
329
+
330
+ # Provides access to the pre-overload version of write_attribute, which
331
+ # is called by the overloaded version.
332
+ alias old_write_attribute write_attribute
333
+ }
334
+ end
@@ -0,0 +1,64 @@
1
+ module ActionView::Helpers::FormHelper
2
+ private
3
+
4
+ def self.restrict(name)
5
+ module_eval <<-"end_eval", __FILE__, __LINE__
6
+ alias old_#{name} #{name}
7
+ def #{name}(object, method, options = {})
8
+ restricted_input(:old_#{name}, object, method, options) \
9
+ end
10
+ end_eval
11
+ end
12
+
13
+ def restricted_input(old_name, object, method, options, *extra, &block)
14
+ o = instance_variable_get("@" + object)
15
+ writable = o.writable?(method)
16
+ if o.readable?(method)
17
+ if writable
18
+ if block
19
+ return yield(object, method, options, extra)
20
+ else
21
+ return send(old_name, object, method, options)
22
+ end
23
+ else
24
+ return o.send(method)
25
+ end
26
+ elseif writable
27
+ options[:value] = ''
28
+ if block
29
+ return yield(object, method, options, extra)
30
+ else
31
+ return send(old_name, object, method, options)
32
+ end
33
+ end
34
+ end
35
+
36
+ once ('@aliasesDone') { # Evaluating this twice would break it.
37
+ alias old_check_box check_box
38
+ alias old_radio_button radio_button
39
+ }
40
+
41
+ SimpleMethods = \
42
+ ['file_field', 'hidden_field', 'password_field', 'text_area', 'text_field']
43
+
44
+ public
45
+
46
+ once ('@simpleMethodsDone') { # because it calls alias.
47
+ SimpleMethods.each { | name | restrict(name) }
48
+ }
49
+
50
+ def check_box(object, method, options = {}, checked_value = "1", unchecked_value = "0")
51
+ restricted_input(:old_check_box, object, method, options, checked_value, unchecked_value) {
52
+ | object, method, options, extra |
53
+ old_check_box(object, method, options, extra[0], extra[1])
54
+ }
55
+ end
56
+
57
+ def radio_button(object, method, tag_value, options = {})
58
+ restricted_input(:old_radio_button, object, method, options, tag_value) {
59
+ | object, method, options, extra |
60
+ old_radio_button(object, method, extra[0], options)
61
+ }
62
+ end
63
+ end
64
+