Sync with latest (2/13/2007) Instiki svn.

This commit is contained in:
Jacques Distler 2007-02-13 09:55:26 -06:00
parent f896f8fbdc
commit d291318f3e
29 changed files with 3212 additions and 1338 deletions

View file

@ -2,6 +2,7 @@
# Likewise will all the methods added be available for all controllers.
class ApplicationController < ActionController::Base
# require 'dnsbl_check'
# protect_forms_from_spam
before_filter :dnsbl_check, :connect_to_model, :check_authorization, :setup_url_generator, :set_content_type_header, :set_robots_metatag
after_filter :remember_location, :teardown_url_generator

View file

@ -1,17 +1,45 @@
# Don't change this file. Configuration is done in config/environment.rb and config/environments/*.rb
unless defined?(RAILS_ROOT)
root_path = File.join(File.dirname(__FILE__), '..')
unless RUBY_PLATFORM =~ /mswin32/
require 'pathname'
root_path = Pathname.new(root_path).cleanpath.to_s
root_path = Pathname.new(root_path).cleanpath(true).to_s
end
RAILS_ROOT = root_path
end
if File.directory?("#{RAILS_ROOT}/vendor/rails")
require "#{RAILS_ROOT}/vendor/rails/railties/lib/initializer"
else
require 'rubygems'
require 'initializer'
end
unless defined?(Rails::Initializer)
if File.directory?("#{RAILS_ROOT}/vendor/rails")
require "#{RAILS_ROOT}/vendor/rails/railties/lib/initializer"
else
require 'rubygems'
Rails::Initializer.run(:set_load_path)
environment_without_comments = IO.readlines(File.dirname(__FILE__) + '/environment.rb').reject { |l| l =~ /^#/ }.join
environment_without_comments =~ /[^#]RAILS_GEM_VERSION = '([\d.]+)'/
rails_gem_version = $1
if version = defined?(RAILS_GEM_VERSION) ? RAILS_GEM_VERSION : rails_gem_version
# Asking for 1.1.6 will give you 1.1.6.5206, if available -- makes it easier to use beta gems
rails_gem = Gem.cache.search('rails', "~>#{version}.0").sort_by { |g| g.version.version }.last
if rails_gem
gem "rails", "=#{rails_gem.version.version}"
require rails_gem.full_gem_path + '/lib/initializer'
else
STDERR.puts %(Cannot find gem for Rails ~>#{version}.0:
Install the missing gem with 'gem install -v=#{version} rails', or
change environment.rb to define RAILS_GEM_VERSION with your desired version.
)
exit 1
end
else
gem "rails"
require 'initializer'
end
end
Rails::Initializer.run(:set_load_path)
end

View file

@ -0,0 +1,2 @@
// Place your application-specific JavaScript functions and classes here
// This file is automatically included by javascript_include_tag :defaults

View file

@ -1,12 +1,13 @@
// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
// (c) 2005 Ivan Krstic (http://blogs.law.harvard.edu/ivan)
// (c) 2005 Jon Tirsen (http://www.tirsen.com)
// Copyright (c) 2005, 2006 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
// (c) 2005, 2006 Ivan Krstic (http://blogs.law.harvard.edu/ivan)
// (c) 2005, 2006 Jon Tirsen (http://www.tirsen.com)
// Contributors:
// Richard Livsey
// Rahul Bhargava
// Rob Wills
//
// See scriptaculous.js for full license.
// script.aculo.us is freely distributable under the terms of an MIT-style license.
// For details, see the script.aculo.us web site: http://script.aculo.us/
// Autocompleter.Base handles all the autocompletion functionality
// that's independent of the data source for autocompletion. This
@ -33,6 +34,9 @@
// useful when one of the tokens is \n (a newline), as it
// allows smart autocompletion after linebreaks.
if(typeof Effect == 'undefined')
throw("controls.js requires including script.aculo.us' effects.js library");
var Autocompleter = {}
Autocompleter.Base = function() {};
Autocompleter.Base.prototype = {
@ -45,7 +49,7 @@ Autocompleter.Base.prototype = {
this.index = 0;
this.entryCount = 0;
if (this.setOptions)
if(this.setOptions)
this.setOptions(options);
else
this.options = options || {};
@ -55,17 +59,20 @@ Autocompleter.Base.prototype = {
this.options.frequency = this.options.frequency || 0.4;
this.options.minChars = this.options.minChars || 1;
this.options.onShow = this.options.onShow ||
function(element, update){
if(!update.style.position || update.style.position=='absolute') {
update.style.position = 'absolute';
Position.clone(element, update, {setHeight: false, offsetTop: element.offsetHeight});
}
Effect.Appear(update,{duration:0.15});
};
function(element, update){
if(!update.style.position || update.style.position=='absolute') {
update.style.position = 'absolute';
Position.clone(element, update, {
setHeight: false,
offsetTop: element.offsetHeight
});
}
Effect.Appear(update,{duration:0.15});
};
this.options.onHide = this.options.onHide ||
function(element, update){ new Effect.Fade(update,{duration:0.15}) };
function(element, update){ new Effect.Fade(update,{duration:0.15}) };
if (typeof(this.options.tokens) == 'string')
if(typeof(this.options.tokens) == 'string')
this.options.tokens = new Array(this.options.tokens);
this.observer = null;
@ -80,7 +87,10 @@ Autocompleter.Base.prototype = {
show: function() {
if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update);
if(!this.iefix && (navigator.appVersion.indexOf('MSIE')>0) && (Element.getStyle(this.update, 'position')=='absolute')) {
if(!this.iefix &&
(navigator.appVersion.indexOf('MSIE')>0) &&
(navigator.userAgent.indexOf('Opera')<0) &&
(Element.getStyle(this.update, 'position')=='absolute')) {
new Insertion.After(this.update,
'<iframe id="' + this.update.id + '_iefix" '+
'style="display:none;position:absolute;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);" ' +
@ -91,7 +101,7 @@ Autocompleter.Base.prototype = {
},
fixIEOverlapping: function() {
Position.clone(this.update, this.iefix);
Position.clone(this.update, this.iefix, {setTop:(!this.update.style.height)});
this.iefix.style.zIndex = 1;
this.update.style.zIndex = 2;
Element.show(this.iefix);
@ -138,8 +148,8 @@ Autocompleter.Base.prototype = {
return;
}
else
if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN)
return;
if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN ||
(navigator.appVersion.indexOf('AppleWebKit') > 0 && event.keyCode == 0)) return;
this.changed = true;
this.hasFocus = true;
@ -149,6 +159,12 @@ Autocompleter.Base.prototype = {
setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000);
},
activate: function() {
this.changed = false;
this.hasFocus = true;
this.getUpdatedChoices();
},
onHover: function(event) {
var element = Event.findElement(event, 'LI');
if(this.index != element.autocompleteIndex)
@ -184,17 +200,22 @@ Autocompleter.Base.prototype = {
this.show();
this.active = true;
}
} else this.hide();
} else {
this.active = false;
this.hide();
}
},
markPrevious: function() {
if(this.index > 0) this.index--
else this.index = this.entryCount-1;
this.getEntry(this.index).scrollIntoView(true);
},
markNext: function() {
if(this.index < this.entryCount-1) this.index++
else this.index = 0;
this.getEntry(this.index).scrollIntoView(false);
},
getEntry: function(index) {
@ -215,8 +236,13 @@ Autocompleter.Base.prototype = {
this.options.updateElement(selectedElement);
return;
}
var value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal');
var value = '';
if (this.options.select) {
var nodes = document.getElementsByClassName(this.options.select, selectedElement) || [];
if(nodes.length>0) value = Element.collectTextNodes(nodes[0], this.options.select);
} else
value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal');
var lastTokenPos = this.findLastToken();
if (lastTokenPos != -1) {
var newValue = this.element.value.substr(0, lastTokenPos + 1);
@ -237,11 +263,11 @@ Autocompleter.Base.prototype = {
if(!this.changed && this.hasFocus) {
this.update.innerHTML = choices;
Element.cleanWhitespace(this.update);
Element.cleanWhitespace(this.update.firstChild);
Element.cleanWhitespace(this.update.down());
if(this.update.firstChild && this.update.firstChild.childNodes) {
if(this.update.firstChild && this.update.down().childNodes) {
this.entryCount =
this.update.firstChild.childNodes.length;
this.update.down().childNodes.length;
for (var i = 0; i < this.entryCount; i++) {
var entry = this.getEntry(i);
entry.autocompleteIndex = i;
@ -252,9 +278,14 @@ Autocompleter.Base.prototype = {
}
this.stopIndicator();
this.index = 0;
this.render();
if(this.entryCount==1 && this.options.autoSelect) {
this.selectEntry();
this.hide();
} else {
this.render();
}
}
},
@ -299,7 +330,7 @@ Autocompleter.Base.prototype = {
Ajax.Autocompleter = Class.create();
Object.extend(Object.extend(Ajax.Autocompleter.prototype, Autocompleter.Base.prototype), {
initialize: function(element, update, url, options) {
this.baseInitialize(element, update, options);
this.baseInitialize(element, update, options);
this.options.asynchronous = true;
this.options.onComplete = this.onComplete.bind(this);
this.options.defaultParams = this.options.parameters || null;
@ -425,6 +456,15 @@ Autocompleter.Local.prototype = Object.extend(new Autocompleter.Base(), {
//
// see documentation on http://wiki.script.aculo.us/scriptaculous/show/Ajax.InPlaceEditor
// Use this if you notice weird scrolling problems on some browsers,
// the DOM might be a bit confused when this gets called so do this
// waits 1 ms (with setTimeout) until it does the activation
Field.scrollFreeActivate = function(field) {
setTimeout(function() {
Field.activate(field);
}, 1);
}
Ajax.InPlaceEditor = Class.create();
Ajax.InPlaceEditor.defaultHighlightColor = "#FFFF99";
Ajax.InPlaceEditor.prototype = {
@ -433,7 +473,10 @@ Ajax.InPlaceEditor.prototype = {
this.element = $(element);
this.options = Object.extend({
paramName: "value",
okButton: true,
okText: "ok",
cancelLink: true,
cancelText: "cancel",
savingText: "Saving...",
clickToEditText: "Click to edit",
@ -455,8 +498,10 @@ Ajax.InPlaceEditor.prototype = {
formClassName: 'inplaceeditor-form',
highlightcolor: Ajax.InPlaceEditor.defaultHighlightColor,
highlightendcolor: "#FFFFFF",
externalControl: null,
ajaxOptions: {}
externalControl: null,
submitOnBlur: false,
ajaxOptions: {},
evalScripts: false
}, options || {});
if(!this.options.formId && this.element.id) {
@ -490,7 +535,7 @@ Ajax.InPlaceEditor.prototype = {
Event.observe(this.options.externalControl, 'mouseout', this.mouseoutListener);
}
},
enterEditMode: function() {
enterEditMode: function(evt) {
if (this.saving) return;
if (this.editing) return;
this.editing = true;
@ -501,11 +546,12 @@ Ajax.InPlaceEditor.prototype = {
Element.hide(this.element);
this.createForm();
this.element.parentNode.insertBefore(this.form, this.element);
Field.focus(this.editField);
if (!this.options.loadTextURL) Field.scrollFreeActivate(this.editField);
// stop the event to avoid a page refresh in Safari
if (arguments.length > 1) {
Event.stop(arguments[0]);
if (evt) {
Event.stop(evt);
}
return false;
},
createForm: function() {
this.form = document.createElement("form");
@ -520,16 +566,22 @@ Ajax.InPlaceEditor.prototype = {
this.form.appendChild(br);
}
okButton = document.createElement("input");
okButton.type = "submit";
okButton.value = this.options.okText;
this.form.appendChild(okButton);
if (this.options.okButton) {
okButton = document.createElement("input");
okButton.type = "submit";
okButton.value = this.options.okText;
okButton.className = 'editor_ok_button';
this.form.appendChild(okButton);
}
cancelLink = document.createElement("a");
cancelLink.href = "#";
cancelLink.appendChild(document.createTextNode(this.options.cancelText));
cancelLink.onclick = this.onclickCancel.bind(this);
this.form.appendChild(cancelLink);
if (this.options.cancelLink) {
cancelLink = document.createElement("a");
cancelLink.href = "#";
cancelLink.appendChild(document.createTextNode(this.options.cancelText));
cancelLink.onclick = this.onclickCancel.bind(this);
cancelLink.className = 'editor_cancel';
this.form.appendChild(cancelLink);
}
},
hasHTMLLineBreaks: function(string) {
if (!this.options.handleLineBreaks) return false;
@ -545,24 +597,34 @@ Ajax.InPlaceEditor.prototype = {
} else {
text = this.getText();
}
var obj = this;
if (this.options.rows == 1 && !this.hasHTMLLineBreaks(text)) {
this.options.textarea = false;
var textField = document.createElement("input");
textField.obj = this;
textField.type = "text";
textField.name = "value";
textField.name = this.options.paramName;
textField.value = text;
textField.style.backgroundColor = this.options.highlightcolor;
textField.className = 'editor_field';
var size = this.options.size || this.options.cols || 0;
if (size != 0) textField.size = size;
if (this.options.submitOnBlur)
textField.onblur = this.onSubmit.bind(this);
this.editField = textField;
} else {
this.options.textarea = true;
var textArea = document.createElement("textarea");
textArea.name = "value";
textArea.obj = this;
textArea.name = this.options.paramName;
textArea.value = this.convertHTMLLineBreaks(text);
textArea.rows = this.options.rows;
textArea.cols = this.options.cols || 40;
textArea.className = 'editor_field';
if (this.options.submitOnBlur)
textArea.onblur = this.onSubmit.bind(this);
this.editField = textArea;
}
@ -589,6 +651,7 @@ Ajax.InPlaceEditor.prototype = {
Element.removeClassName(this.form, this.options.loadingClassName);
this.editField.disabled = false;
this.editField.value = transport.responseText.stripTags();
Field.scrollFreeActivate(this.editField);
},
onclickCancel: function() {
this.onComplete();
@ -613,19 +676,26 @@ Ajax.InPlaceEditor.prototype = {
// to be displayed indefinitely
this.onLoading();
new Ajax.Updater(
{
success: this.element,
// don't update on failure (this could be an option)
failure: null
},
this.url,
Object.extend({
parameters: this.options.callback(form, value),
onComplete: this.onComplete.bind(this),
onFailure: this.onFailure.bind(this)
}, this.options.ajaxOptions)
);
if (this.options.evalScripts) {
new Ajax.Request(
this.url, Object.extend({
parameters: this.options.callback(form, value),
onComplete: this.onComplete.bind(this),
onFailure: this.onFailure.bind(this),
asynchronous:true,
evalScripts:true
}, this.options.ajaxOptions));
} else {
new Ajax.Updater(
{ success: this.element,
// don't update on failure (this could be an option)
failure: null },
this.url, Object.extend({
parameters: this.options.callback(form, value),
onComplete: this.onComplete.bind(this),
onFailure: this.onFailure.bind(this)
}, this.options.ajaxOptions));
}
// stop the event to avoid a page refresh in Safari
if (arguments.length > 1) {
Event.stop(arguments[0]);
@ -705,4 +775,59 @@ Ajax.InPlaceEditor.prototype = {
Event.stopObserving(this.options.externalControl, 'mouseout', this.mouseoutListener);
}
}
};
};
Ajax.InPlaceCollectionEditor = Class.create();
Object.extend(Ajax.InPlaceCollectionEditor.prototype, Ajax.InPlaceEditor.prototype);
Object.extend(Ajax.InPlaceCollectionEditor.prototype, {
createEditField: function() {
if (!this.cached_selectTag) {
var selectTag = document.createElement("select");
var collection = this.options.collection || [];
var optionTag;
collection.each(function(e,i) {
optionTag = document.createElement("option");
optionTag.value = (e instanceof Array) ? e[0] : e;
if((typeof this.options.value == 'undefined') &&
((e instanceof Array) ? this.element.innerHTML == e[1] : e == optionTag.value)) optionTag.selected = true;
if(this.options.value==optionTag.value) optionTag.selected = true;
optionTag.appendChild(document.createTextNode((e instanceof Array) ? e[1] : e));
selectTag.appendChild(optionTag);
}.bind(this));
this.cached_selectTag = selectTag;
}
this.editField = this.cached_selectTag;
if(this.options.loadTextURL) this.loadExternalText();
this.form.appendChild(this.editField);
this.options.callback = function(form, value) {
return "value=" + encodeURIComponent(value);
}
}
});
// Delayed observer, like Form.Element.Observer,
// but waits for delay after last key input
// Ideal for live-search fields
Form.Element.DelayedObserver = Class.create();
Form.Element.DelayedObserver.prototype = {
initialize: function(element, delay, callback) {
this.delay = delay || 0.5;
this.element = $(element);
this.callback = callback;
this.timer = null;
this.lastValue = $F(this.element);
Event.observe(this.element,'keyup',this.delayedListener.bindAsEventListener(this));
},
delayedListener: function(event) {
if(this.lastValue == $F(this.element)) return;
if(this.timer) clearTimeout(this.timer);
this.timer = setTimeout(this.onTimerEvent.bind(this), this.delay * 1000);
this.lastValue = $F(this.element);
},
onTimerEvent: function() {
this.timer = null;
this.callback(this.element, $F(this.element));
}
};

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

3
script/about Executable file
View file

@ -0,0 +1,3 @@
#!/usr/bin/env ruby
require File.dirname(__FILE__) + '/../config/boot'
require 'commands/about'

3
script/performance/benchmarker Executable file
View file

@ -0,0 +1,3 @@
#!/usr/bin/env ruby
require File.dirname(__FILE__) + '/../../config/boot'
require 'commands/performance/benchmarker'

3
script/performance/profiler Executable file
View file

@ -0,0 +1,3 @@
#!/usr/bin/env ruby
require File.dirname(__FILE__) + '/../../config/boot'
require 'commands/performance/profiler'

3
script/plugin Executable file
View file

@ -0,0 +1,3 @@
#!/usr/bin/env ruby
require File.dirname(__FILE__) + '/../config/boot'
require 'commands/plugin'

3
script/process/inspector Executable file
View file

@ -0,0 +1,3 @@
#!/usr/bin/env ruby
require File.dirname(__FILE__) + '/../../config/boot'
require 'commands/process/inspector'

3
script/process/reaper Executable file
View file

@ -0,0 +1,3 @@
#!/usr/bin/env ruby
require File.dirname(__FILE__) + '/../../config/boot'
require 'commands/process/reaper'

3
script/process/spawner Executable file
View file

@ -0,0 +1,3 @@
#!/usr/bin/env ruby
require File.dirname(__FILE__) + '/../../config/boot'
require 'commands/process/spawner'

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