Skip to content

Commit

Permalink
Remove Uniqueness middleware
Browse files Browse the repository at this point in the history
The Uniqueness middleware:
- Is insecure
- Is subject to brute force attacks
- Badly performs because of synchronous Ajax calling
- Did not work with mysql2 adapter

This commit also wipes the middleware

Close: #512 #597 #612 #659 #226
  • Loading branch information
tagliala committed Jan 20, 2017
1 parent 6de5bb1 commit 2cf97b2
Show file tree
Hide file tree
Showing 16 changed files with 9 additions and 866 deletions.
112 changes: 3 additions & 109 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,37 +183,6 @@ You can even turn them off per fieldset:
...
```

## Wrapper objects and remote validations ##

For example, we have a wrapper class for the User model, UserForm, and it uses remote uniqueness validation for the `email` field.

```ruby
class UserForm
include ActiveRecord::Validations
attr_accessor :email
validates_uniqueness_of :email
end
...
<% form_for(UserForm.new) do %>
...
```
However, this won't work since middleware will try to perform validation against UserForm, and it's not persisted.
This is solved by passing `client_validations` options hash to the validator, that currently supports one key — `:class`, and setting correct name to the form object:
```ruby
class UserForm
include ActiveRecord::Validations
attr_accessor :email
validates_uniqueness_of :email, client_validations: { class:
'User' }
end
...
<% form_for(UserForm.new, as: :user) do %>
...
```

## Understanding the embedded `<script>` tag ##

A rendered form with validations will always have a `<script>` appended
Expand Down Expand Up @@ -259,19 +228,19 @@ passing nothing:
You can also force validators similarly to the input syntax:

```erb
<%= f.validate :email, uniqueness: false %>
<%= f.validate :email, presence: false %>
```

Take care when using this method. The embedded validators are
overwritten based upon the order they are rendered. So if you do
something like:

```erb
<%= f.text_field :email, validate: { uniqueness: false } %>
<%= f.text_field :email, validate: { presence: false } %>
<%= f.validate %>
```

The `uniqueness` validator will not be turned off because the options
The `presence` validator will not be turned off because the options
were overwritten by the call to `FormBuilder#validate`


Expand Down Expand Up @@ -357,71 +326,6 @@ end

Client Side Validations will apply the new validator and validate your forms as needed.

### Remote Validators ###
A good example of a remote validator would be for Zipcodes. It wouldn't be reasonable to embed every single zipcode inline, so we'll need to check for its existence with remote javascript call back to our app. Assume we have a zipcode database mapped to the model Zipcode. The primary key is the unique zipcode. Our Rails validator would probably look something like this:

```ruby
class ZipcodeValidator < ActiveModel::EachValidator
def validate_each(record, attr_name, value)
unless ::Zipcode.where(id: value).exists?
record.errors.add(attr_name, :zipcode, options.merge(value: value))
end
end
end
# This allows us to assign the validator in the model
module ActiveModel::Validations::HelperMethods
def validates_zipcode(*attr_names)
validates_with ZipcodeValidator, _merge_attributes(attr_names)
end
end
```

Of course we still need to add the i18n message:

```yaml
en:
errors:
messages:
zipcode: "Not a valid US zip code"
```

And let's add the Javascript validator. Because this will be remote validator we need to add it to `ClientSideValidations.validators.remote`:

```js
window.ClientSideValidations.validators.remote['zipcode'] = function(element, options) {
if ($.ajax({
url: '/validators/zipcode',
data: { id: element.val() },
// async *must* be false
async: false
}).status == 404) { return options.message; }
}
```

All we're doing here is checking to see if the resource exists (in this case the given zipcode) and if it doesn't the error message is returned.

Notice that the remote call is forced to *async: false*. This is necessary and the validator may not work properly if this is left out.

Now the extra step for adding a remote validator is to add to the middleware. All ClientSideValidations middleware should inherit from `ClientSideValidations::Middleware::Base`:

```ruby
module ClientSideValidations::Middleware
class Zipcode < ClientSideValidations::Middleware::Base
def response
if ::Zipcode.where(id: request.params[:id]).exists?
self.status = 200
else
self.status = 404
end
super
end
end
end
```

The `#response` method is always called and it should set the status accessor. Then a call to `super` is required. In the javascript we set the 'id' in the params to the value of the zipcode input, in the middleware we check to see if this zipcode exists in our zipcode database. If it does, we return 200, if it doesn't we return 404.

## Enabling, Disabling, and Resetting on the client ##

There are many reasons why you might want to enable, disable, or even completely reset the bound validation events on the client. `ClientSideValidations` offers a simple API for this.
Expand Down Expand Up @@ -510,16 +414,6 @@ div.field_with_errors div.ui-effects-wrapper {

Finally uncomment the `ActionView::Base.field_error_proc` override in `config/initializers/client_side_validations.rb`

## Security ##

Client Side Validations comes with a uniqueness middleware. This can be a potential security issue, so the uniqueness validator is disabled by default. If you want to enable it, set the `disabled_validators` config variable in `config/initializers/client_side_validations.rb`:

```ruby
ClientSideValidations::Config.disabled_validators = []
```

Note that the `FormBuilder` will automatically skip building validators that are disabled.

## Authors ##

[Brian Cardarella](https://twitter.com/bcardarella)
Expand Down
50 changes: 1 addition & 49 deletions coffeescript/rails.validations.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -383,55 +383,7 @@ window.ClientSideValidations.validators =

return options.message unless valid

remote:
uniqueness: (element, options) ->
message = ClientSideValidations.validators.local.presence(element, options)
if message
return if options.allow_blank == true
return message

data = {}
data.case_sensitive = !!options.case_sensitive
data.id = options.id if options.id

if options.scope
data.scope = {}
for key, scope_value of options.scope
scoped_name = element.attr('name').replace(/\[\w+\]$/, "[#{key}]")
scoped_element = $("[name='#{scoped_name}']")
$("[name='#{scoped_name}']:checkbox").each ->
if @.checked
scoped_element = @

if scoped_element[0] and scoped_element.val() != scope_value
data.scope[key] = scoped_element.val()
scoped_element.unbind("change.#{element.id}").bind "change.#{element.id}", ->
element.trigger('change.ClientSideValidations')
element.trigger('focusout.ClientSideValidations')
else
data.scope[key] = scope_value

# Kind of a hack but this will isolate the resource name and attribute.
# e.g. user[records_attributes][0][title] => records[title]
# e.g. user[record_attributes][title] => record[title]
# Server side handles classifying the resource properly
if /_attributes\]/.test(element.attr('name'))
name = element.attr('name').match(/\[\w+_attributes\]/g).pop().match(/\[(\w+)_attributes\]/).pop()
name += /(\[\w+\])$/.exec(element.attr('name'))[1]
else
name = element.attr('name')

# Override the name if a nested module class is passed
name = "#{options['class']}[#{name.split('[')[1]}" if options['class']
data[name] = element.val()

if $.ajax({
url: ClientSideValidations.remote_validators_url_for('uniqueness')
data: data,
async: false
cache: false
}).status == 200
return options.message
remote: {}

window.ClientSideValidations.remote_validators_url_for = (validator) ->
if ClientSideValidations.remote_validators_prefix?
Expand Down
6 changes: 1 addition & 5 deletions lib/client_side_validations.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,4 @@
require 'client_side_validations/active_record' if defined?(::ActiveRecord)
require 'client_side_validations/action_view' if defined?(::ActionView)

if defined?(::Rails)
require 'client_side_validations/generators'
require 'client_side_validations/middleware'
require 'client_side_validations/engine'
end
require 'client_side_validations/generators' if defined?(::Rails)
3 changes: 0 additions & 3 deletions lib/client_side_validations/active_record.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
require 'client_side_validations/active_model'
require 'client_side_validations/extender'
require 'client_side_validations/middleware'
require 'client_side_validations/active_record/middleware'

ActiveRecord::Base.send(:include, ClientSideValidations::ActiveModel::Validations)
ClientSideValidations::Middleware::Uniqueness.register_orm(ClientSideValidations::ActiveRecord::Middleware)

ClientSideValidations::Extender.extend 'ActiveRecord', %w(Uniqueness)
62 changes: 0 additions & 62 deletions lib/client_side_validations/active_record/middleware.rb

This file was deleted.

2 changes: 1 addition & 1 deletion lib/client_side_validations/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ class << self
attr_accessor :root_path
end

self.disabled_validators = [:uniqueness]
self.disabled_validators = []
self.number_format_with_locale = false
self.root_path = nil
end
Expand Down
5 changes: 0 additions & 5 deletions lib/client_side_validations/engine.rb

This file was deleted.

Loading

0 comments on commit 2cf97b2

Please sign in to comment.