adding the form_spam_protection plugin that was missing from the last commit

This commit is contained in:
Matthias Tarasiewicz 2007-02-13 13:27:54 +00:00
parent 113223f364
commit c9a9b7d315
15 changed files with 493 additions and 0 deletions

View file

@ -0,0 +1,32 @@
FormSpamProtection
==================
Tired of forms in your web application getting spammed? Captcha works but is a
pain. This plugin ensures a real person is submitting the form without
requiring anything from the user except that they have Javascript turned on.
This plugin uses the Hivelogic Enkoder, normally used to protect spambots from
harvesting e-mail addresses, to enkode a hidden field on the form. That field
is required when the user submits the form or they get an error.
To install:
./script/plugin install -x http://form-spam-protection.googlecode.com/svn/form_spam_protection/
To use: In your controller, just add:
protect_forms_from_spam
You can also specify :only or :except just like a before filter:
protect_forms_from_spam :only => :index
In fact, it is just a before filter. To protect only the form in one action
and the handling (but not the form) in another, do this:
before_filter :protect_form_from_spam, :only => :new
before_filter :protect_form_handler_from_spam, :only => :create
...though this is seldom necessary.
Bugs:
Please submit bugs through the tracker at http://code.google.com/p/form-spam-protection/issues/list
TODO:
* Add <noscript> tags to the form to display the "you must have Javascript on" message
* Make messages and resubmit limit configurable

View file

@ -0,0 +1,22 @@
require 'rake'
require 'rake/testtask'
require 'rake/rdoctask'
desc 'Default: run unit tests.'
task :default => :test
desc 'Test the form_spam_protection plugin.'
Rake::TestTask.new(:test) do |t|
t.libs << 'lib'
t.pattern = 'test/**/*_test.rb'
t.verbose = true
end
desc 'Generate documentation for the form_spam_protection plugin.'
Rake::RDocTask.new(:rdoc) do |rdoc|
rdoc.rdoc_dir = 'rdoc'
rdoc.title = 'FormSpamProtection'
rdoc.options << '--line-numbers' << '--inline-source'
rdoc.rdoc_files.include('README')
rdoc.rdoc_files.include('lib/**/*.rb')
end

View file

@ -0,0 +1,4 @@
require 'form_spam_protection'
require File.join(File.dirname(__FILE__), 'vendor/enkoder/lib/enkoder')
require File.join(File.dirname(__FILE__), "/test/mocks/enkoder") if RAILS_ENV == 'test'
ActionController::Base.send :include, FormSpamProtection

View file

@ -0,0 +1 @@
# Install hook code here

View file

@ -0,0 +1,36 @@
require 'form_tag_helper_extensions'
module FormSpamProtection
module ClassMethods
def protect_forms_from_spam(*args)
before_filter :protect_form_from_spam, *args
before_filter :protect_form_handler_from_spam, *args
end
end
def protect_form_from_spam
@protect_form_from_spam = true
end
def protect_form_handler_from_spam
unless request.get? || request.xml_http_request?
if params[:_form_key] && session[:form_keys] && session[:form_keys].keys.include?(params[:_form_key])
session[:form_keys][params[:_form_key]] += 1
if session[:form_keys][params[:_form_key]] >= 4
render :text => "You cannot resubmit this form again.", :layout => false, :status => 403
return false
end
else
render :text => "You must have Javascript on to submit this form.", :layout => false, :status => 403
return false
end
end
end
extend ClassMethods
def self.included(receiver)
receiver.extend(ClassMethods)
end
end

View file

@ -0,0 +1,56 @@
require 'digest/sha1'
module ActionView
module Helpers
module TagHelper
# Now that form_tag accepts blocks, it was easier to alias tag when name == :form
def tag_with_form_spam_protection(name, *args)
returning tag_without_form_spam_protection(name, *args) do |out|
if name == :form && @protect_form_from_spam
session[:form_keys] ||= {}
form_key = Digest::SHA1.hexdigest(self.object_id.to_s + rand.to_s)
session[:form_keys][form_key] = 0
out << enkode(hidden_field_tag('_form_key', form_key))
end
end
end
alias_method :tag_without_form_spam_protection, :tag
alias_method :tag, :tag_with_form_spam_protection
end
# module FormTagHelper
# def form_tag_with_spam_protection(*args, &proc)
# form_tag_method_with_spam_protection :form_tag, *args, &proc
# end
#
# # alias_method_chain :form_tag, :spam_protection
# alias_method :form_tag_without_spam_protection, :form_tag
# alias_method :form_tag, :form_tag_with_spam_protection
#
# protected
# def form_tag_method_with_spam_protection(method_name, *args, &proc)
# old_method_name = "#{method_name}_without_spam_protection"
# returning send(old_method_name, *args) do |out|
# if @protect_form_from_spam
# session[:form_keys] ||= {}
# form_key = Digest::SHA1.hexdigest(self.object_id.to_s + rand.to_s)
# session[:form_keys][form_key] = 0
# out << enkode(hidden_field_tag('_form_key', form_key))
# end
# end
# end
#
#
# end
#
# module PrototypeHelper
# def form_remote_tag_with_spam_protection(*args, &proc)
# form_tag_method_with_spam_protection :form_remote_tag, *args, &proc
# end
#
# # alias_method_chain :form_remote_tag, :spam_protection
# alias_method :form_remote_tag_without_spam_protection, :form_remote_tag
# alias_method :form_remote_tag, :form_remote_tag_with_spam_protection
# end
end
end

View file

@ -0,0 +1,4 @@
# desc "Explaining what the task does"
# task :form_spam_protection do
# # Task goes here
# end

View file

@ -0,0 +1,39 @@
require File.dirname(__FILE__) + '/test_helper'
class FormSpamProtectionTest < Test::Unit::TestCase
def setup
@controller = ProtectedController.new
@request = ActionController::TestRequest.new
@response = ActionController::TestResponse.new
end
def test_index_form_is_protected
get :index
assert_response :success
assert_select 'code input[type="hidden"]'
end
def test_index_form_handler_is_protected
post :index
assert_response 403
assert_equal "You must have Javascript on to submit this form.", @response.body
get :index
form_key_tag = assert_select('code input[type="hidden"]').first
submit_with_valid_key = lambda { post :index, :_form_key => form_key_tag.attributes['value'] }
submit_with_valid_key.call
assert_response :success
assert_equal "Submission successful", @response.body
3.times(&submit_with_valid_key) # Total of 4 times
assert_response 403
assert_equal "You cannot resubmit this form again.", @response.body
end
def test_unprotected_form_is_unprotected
get :unprotected
assert_response :success
assert_select 'input[type="hidden"]', false
end
end

View file

@ -0,0 +1,14 @@
require File.join(File.dirname(__FILE__), '../../vendor/enkoder/lib/enkoder')
module ActionView
module Helpers
module TextHelper
# Don't really enkode, because our tests can't eval Javascript
def enkode( html, max_length=nil )
"<code>#{html}</code>"
end
end
end
end

View file

@ -0,0 +1,30 @@
RAILS_ENV = 'test'
require File.expand_path(File.join(File.dirname(__FILE__), '../../../../config/environment.rb'))
require 'action_controller/test_process'
require 'breakpoint'
class ProtectedController < ActionController::Base
protect_forms_from_spam :only => :index
def index
if request.get?
render :inline => form
else
render :text => 'Submission successful'
end
end
def unprotected
render :inline => form
end
private
def form
<<-EOD
<% form_tag do %>
MyField: <%= text_field_tag 'testme' %>
<%= submit_tag %>
<% end %>
EOD
end
end

View file

@ -0,0 +1 @@
# Uninstall hook code here

View file

@ -0,0 +1,27 @@
Copyright (c) 2006, Automatic Corp.
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of AUTOMATIC CORP. nor the names of its contributors may
be used to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View file

@ -0,0 +1,82 @@
==Hivelogic Enkoder
The Enkoder plugin provides an extension to TextHelper that can be used to
protect email addresses (or other information) by obfuscating them using
JavaScript code. The only way to decrypt the JavaScript is to actually run it,
hiding the results from email-harvesting robots while revealing them to real
people.
It uses a significantly different (and some might say more secure) algorithm
than the built-in mail_to helper.
Note: There's no guarantee here -- the only way to be completely safe is to not
publish your address at all.
==Installation:
Just drop the "enkoder" folder into the /vendor/plugins folder in your project.
==Usage:
There are two methods:
enkode( html )
This method accepts a block of html (or any text) and returns an enkoded JavaScript.
The second method is:
enkode_mail( email, link_text, title_text=nil, subject=nil )
This method takes an email address, the text to show to the viewer, optional
title text (what's seen when somebody hovers over the link), and optional
subject for the email, and returns an enkoded email address link.
==Examples:
To enkode a single email address, one could just do:
<%= enkode_mail('user@domain.com','click here') %>
And the following link would be returned (enkoded as JavaScript):
<a href="mailto:"user@domain.com" title="">click here</a>
Adding a title and subject text would require the second two optional fields:
<%= enkode_mail('user@domain.com','click here', 'email me', 'enkoder') %>
And we'd get back (enkoded as JavaScript):
<a href="mailto:"user@domain.com?subject=enkoder" title="email me">click here</a>
Of course we can also enkode many email addresses on the fly:
<% @users.each do |user| %>
<p><%= enkode_mail(@user.email,@user.name) %></p>
<% end %>
To enkode a snippet of XHTML, we can do:
<%= enkode("<p>This block will be hidden from spambots.</p>") %>
We could protect a link or block of XHTML from being indexed like this:
<%= enkode('Try and find <a href="secret.html">this</a>, google!') %>
We could have anything we wanted in that block, XHTML, links, email addresses, etc.
For more examples and to see the full functionality of the Enkoder, have a look
its permanent page on the web:
http://hivelogic.com/enkoder
==License:
Copyright (c) 2006 Automatic Corp.
This plugin is released under the LGPL license. See LICENSE file for details.

View file

@ -0,0 +1 @@
require 'enkoder'

View file

@ -0,0 +1,144 @@
# Copyright (c) 2006, Automatic Corp.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of AUTOMATIC CORP. nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
module ActionView
module Helpers
module TextHelper
def enkode( html, max_length=1024 )
rnd = 10 + (rand*90).to_i
kodes = [
{
'rb' => lambda do |s|
s.reverse
end,
'js' => ";kode=kode.split('').reverse().join('')"
},
{
'rb' => lambda do |s|
result = ''
s.each_byte { |b|
b += 3
b-=128 if b>127
result += b.chr
}
result
end,
'js' => (
";x='';for(i=0;i<kode.length;i++){c=kode.charCodeAt(i)-3;" +
"if(c<0)c+=128;x+=String.fromCharCode(c)}kode=x"
)
},
{
'rb' => lambda do |s|
for i in (0..s.length/2-1)
s[i*2],s[i*2+1] = s[i*2+1],s[i*2]
end
s
end,
'js' => (
";x='';for(i=0;i<(kode.length-1);i+=2){" +
"x+=kode.charAt(i+1)+kode.charAt(i)}" +
"kode=x+(i<kode.length?kode.charAt(kode.length-1):'');"
)
}
]
kode = "document.write("+ js_dbl_quote(html) +");"
max_length = kode.length+1 unless max_length>kode.length
result = ''
while kode.length < max_length
idx = (rand*kodes.length).to_i
kode = kodes[idx]['rb'].call(kode)
kode = "kode=" + js_dbl_quote(kode) + kodes[idx]['js']
js = "var kode=\n"+js_wrap_quote(js_dbl_quote(kode),79)
js = js+"\n;var i,c,x;while(eval(kode));"
js = "function hivelogic_enkoder(){"+js+"}hivelogic_enkoder();"
js = '<script type="text/javascript">'+"\n/* <![CDATA[ */\n"+js
js = js+"\n/* ]]> */\n</script>\n"
result = js unless result.length>max_length
end
result
end
def enkode_mail( email, link_text, title_text=nil, subject=nil )
str = String.new
str << '<a href="mailto:' + email
str << '?subject=' + subject unless subject.nil?
str << '" title="'
str << title_text unless title_text.nil?
str << '">' + link_text + '</a>'
enkode(str)
end
private
def js_dbl_quote( str )
str.inspect
end
def js_wrap_quote( str, max_line_length )
max_line_length -= 3
inQ = false
esc = 0
lineLen = 0
result = ''
chunk = ''
while str.length > 0
if str =~ /^\\[0-7]{3}/
chunk = str[0..3]
str[0..3] = ''
elsif str =~ /^\\./
chunk = str[0..1]
str[0..1] = ''
else
chunk = str[0..0]
str[0..0] = ''
end
if lineLen+chunk.length >= max_line_length
result += '"+'+"\n"+'"'
lineLen = 1
end
lineLen += chunk.length
result += chunk;
end
result
end
end
end
end