/*
|--------------------------------------------------------------------------
| An open source Javascript Honey Pot implementation
|--------------------------------------------------------------------------
|
| @version 1.1.0
| @author hungluu2106 ( Hung Luu )
| @url https://github.com/hungluu2106/honeyjs
| @license The MIT License (MIT)
|
| Copyright (c) 2015 Hung Luu
|
| Permission is hereby granted, free of charge, to any person obtaining a copy
| of this software and associated documentation files (the "Software"), to deal
| in the Software without restriction, including without limitation the rights
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
| copies of the Software, and to permit persons to whom the Software is
| furnished to do so, subject to the following conditions:
|
| The above copyright notice and this permission notice shall be included in all
| copies or substantial portions of the Software.
|
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
*/
var honey = (function(){ // jshint ignore:line
'use strict';
// Global options, dependencies, exports
var O, D, exports;
/**
* Global options
*
* @namespace Options
*/
O = {
/**
* Google reCaptcha theme
* @see https://developers.google.com/recaptcha/docs/display#render_param
* @default light
* @type {String}
* @inner
* @memberOf Options
*/
theme:'light',
/**
* Google reCaptcha type
* @see https://developers.google.com/recaptcha/docs/display#render_param
* @default image
* @type {String}
* @memberOf Options
* @inner
*/
type:'image',
/**
* Google reCaptcha size
* @see https://developers.google.com/recaptcha/docs/display#render_param
* @default normal
* @type {String}
* @memberOf Options
* @inner
*/
size:'normal',
/**
* Honeyjs minimum time for form submission
* @default 6
* @type {number}
* @memberOf Options
* @inner
*/
time:6,
/**
* Global holder class name to find when install components
* @default honeyjs
* @type {String}
* @memberOf Options
* @inner
*/
holderClass:'honeyjs',
/**
* Global sitekey for reCaptcha to be rendered
* @see https://developers.google.com/recaptcha/docs/display#render_param
* @default null
* @type {String}
* @memberOf Options
* @inner
*/
key:null,
/**
* For forcing reCaptcha on every new honeypot created
* @default false
* @type {boolean}
* @memberOf Options
* @inner
*/
forceCaptcha:false
};
/**
* Dependencies
*/
D = {
/* GENERIC */
// get all forms inside document, return {HTMLCollection}
getForms : function(){
return document.getElementsByTagName('form');
},
// get current timestamp
// @return {number}
now : function(){
return Date.now ? Date.now() : (new Date()).getTime();
},
/* RECAPTCHA */
// install reCaptcha callbacks
// binded callbacks to reCaptcha render options
// @param {ReCaptcha} reCaptcha object
// @param {Object} options
// return edited options
installReCaptchaCallbacks : function(re, options){
// @see : https://developers.google.com/recaptcha/docs/display#render_param
function callback(response){
re.save(response);
}
// @see : https://developers.google.com/recaptcha/docs/display#render_param
function excallback(){
re.reset();
}
options.callback = callback;
options['expired-callback'] = excallback;
return options;
},
// active reCaptcha immediately on a pot, actually after 300 miliseconds
// @param {Pot} a honeypot object
// @return {Function} real callback was binded
activateReCaptchaAutomatically : function(pot){
function fn(){
pot.activateCaptcha();
}
setTimeout(fn, 300);
return fn;
},
/* JQUERY PLUGIN */
// install jQuery plugin for honeyjs
// @param {Class} $ jQuery
// @param {String} name name of plugin
// @param {Function} fn function of plugin
// @return {boolean} return true on successful installation and vice versa
installjQueryPlugin : function($, name, fn){
// check if jQuery acquired
if($){
$.fn[name] = fn;
return true;
}
else{
return false;
}
},
/* WORKING WITH ARRAY AND COLLECTION */
// find index of an element inside array
// @param {mixed} needle
// @param {Array} arr
// @return {number} index if found, -1 on opposite
find : function(needle, arr){
if(arr.indexOf){
return arr.indexOf(needle);
}
else{
for(var i = 0, l = arr.length;i < l;i++){
if(arr[i] === needle){
return i;
}
}
return -1;
}
},
// check if an array contains an element
// @param {mixed} needle
// @param {Array} arr
// @return {boolean} true when found and vice versa
contains : function(needle, arr){
return D.find(needle, arr) !== -1;
},
/* POT INSTALLATION */
// get form(pot)'s holder element to render honeyjs components
// @param {HTMLFormElement} form
// @param {String} className the class name of holder to find
getHolder : function(form, className){
var holders;
if(form.getElementsByClassName){
holders = form.getElementsByClassName(className);
return holders.length ? holders[0] : form;
}
// IE 7-
else{
var classList;
holders = form.getElementsByTagName('*');
for(var i = 0, l = holders.length;i < l;i++){
classList = holders[i].className.split(' ');
if(D.contains(className, classList)){
return holders[i];
}
}
return form;
}
},
// Generate a hidden input with provide name in forms
// @param {HTMLElement} element
// @param {String} name new hidden input's name
hiddenInput : function(element, name){
var newInput = document.createElement('input');
newInput.name = name;
newInput.style.display = 'none';
newInput.style.visibility = 'hidden';
element.appendChild(newInput);
return newInput;
},
// Bind an event into form
// @param {HTMLElement} element
// @param {String} eventName
// @param {Function} handler
// @param {Object} caller
bind : function(element, eventName, handler, caller){
// Keep memory with local function
function attachedHandler(event){
var result = handler.call(caller, event);
if(result === false){
D.cancel(event);
}
return result;
}
if(element.addEventListener){
element.addEventListener(eventName, attachedHandler, false);
}
else{
// IE 9-
element.attachEvent('on' + eventName, attachedHandler);
}
return attachedHandler;
},
// Cancel an event
// @param {EventArguments} event
cancel : function(event){
event = event || window.event;
if(event){
// IE 7-
event.cancelBubble = true;
event.returnValue = false;
if(event.stopPropagation){
event.stopPropagation();
}
if(event.preventDefault){
event.preventDefault();
}
}
return false;
},
/* POT ULTILITIES */
// from a form, get an input by name
// @param {HTMLFormElement} Form
// @param {String} name of the input element to find
// @return {HTMLInputElement}
getInputByName : function(Form, name){
var inputs = Form.getElementsByTagName('input');
for(var i = 0, l = inputs.length;i < l;i++){
if(inputs[i].name === name){
return inputs[i];
}
}
},
// check if current is dev environment
isDev : function(){
return !navigator.plugins.length;
},
// outject all dependencies for testing on dev environment
// @param {boolean} isDev
__installDev : function(isDev){
if(isDev){
exports.dev = D;
}
return !!isDev;
},
/* TYPES */
/**
* Extends array, provides more methods to work with collections such as {@link Pots} and {@link Hook}
*
* @class Collector
* @alias Collector
*
* @extends {Array}
*/
Collector : function(){},
/**
* Google reCaptcha component
*
* @class ReCaptcha
* @alias ReCaptcha
*
* @requires {@link https://developers.google.com/recaptcha/docs/display|Google reCaptcha}
*/
ReCaptcha : function(){
// a holder to render components
this.holder = null;
/**
* Google reCaptcha sitekey
* @type {string}
* @default null
*/
this.key = null;
// ID got when being rendered
this.id = null;
// Response got when user click on reCaptcha
this.response = null;
},
/**
* A collection of functions, provide serial processing and interaction between them
*
* Works like a collection of **callbacks**. It can be a collection **validators** too
*
* A function inside a hook which _return false_ will **prevent** its next ones to be executed
*
* @class Hook
* @alias Hook
*
* @extends {Collector}
*/
Hook : function(){},
/**
* A Pot - honeypot object - provides security features to forms
*
* @class Pot
* @alias Pot
*
* @param {HTMLFormElement} Form form to be secured
*/
Pot : function(Form){
/**
* Pot's options, inherited from {@link Options}
*
* @type {Object}
*/
this.options = O;
/**
* Current secured form
*
* @type {HTMLFormElement}
*/
this.form = Form;
/**
* Timestamp of when pot was created
* @type {number}
* @private
*/
this.createdAt = D.now();
/**
* Pot's holder to render components
* @type {HTMLElement}
*/
this.holder = D.getHolder(Form, O.holderClass);
/**
* Main input is required by a honeypot
*
* Should be empty and hidden
*
* **To do : ** Recheck its existence and value on server-side, in case attacker has disabled javascript
*
* @type {HTMLInputElement}
*/
this.empty = D.hiddenInput(this.holder, 'name');
/**
* A timestamp input describe the time form was submitted
*
* Provide more information for validation
*
* **To do : ** Recheck its existence and value on server-side, in case attacker has disabled javascript
*
* @type {HTMLInputElement}
*/
this.time = D.hiddenInput(this.holder, '_time');
/**
* Central place for hooks(events, validators...)
*
* @type {Object}
*/
this.hooks = {};
/**
* Validation hook for honeypot - provide validating feature
*
* Executed when form is submitted and validating process takes place first
*
* @event Validating
* @see {@link Hook}
* @type {Hook}
* @public
* @memberOf Pot
*/
this.hooks.validate = new D.Hook();
/**
* Callback hook on fail for honeypot
*
* Executed when form is submitted but never passes the validation
*
* @event Fail
* @see {@link Hook}
* @type {Hook}
* @public
* @memberOf Pot
*/
this.hooks.fail = new D.Hook();
// reCaptcha component
// @type {ReCaptcha}
this.re = new D.ReCaptcha();
// Bind form submit event and inject pot instance
D.bind(Form, 'submit', function(){
return this.valid();
}, this)();
// Install reCaptcha sitekey
this.re.key = this.options.key;
// Activate reCaptcha immediately when being forced to
if(O.forceCaptcha && this.options.key){
D.activateReCaptchaAutomatically(this);
}
},
/**
* A collection of Pot
*
* @extends {Collector}
* @class Pots
* @alias Pots
*
*/
Pots : function(){}
};
/* Collector extends Array */
D.Collector.prototype = Array.prototype;
/**
* Find index of element inside collection
* @method find
* @memberOf Collector
* @instance
* @param {mixed} e element to find
* @return {number} index when found, -1 on the opposite side
*/
D.Collector.prototype.find = function(e){
return D.find(e, this);
};
/**
* Check if a collection contains an element
* @param {mixed} e element to find
* @return {boolean} true on found and vice versa
* @method has
* @memberOf Collector
* @instance
*/
D.Collector.prototype.has = function(e){
return D.contains(e, this);
};
/**
* Remove an element
* @param {mixed} e element to remove
* @method remove
* @memberOf Collector
* @instance
*/
D.Collector.prototype.remove = function(e){
var ind = this.find(e), found = ind !== -1;
if(found){
this.splice(ind, 1);
}
return found;
};
/**
* Erase a collection
* @method flush
* @memberOf Collector
* @instance
*/
D.Collector.prototype.flush = function(){
this.length = 0;
};
D.Hook.prototype = D.Collector.prototype;
/**
* Execute a Hook
* @method exec
* @memberOf Hook
* @instance
* @param {Object} caller
* @return {Mixed} Result returned from the last function
*/
D.Hook.prototype.exec = function(caller){
var result = true;
for(var i = 0, l = this.length;i < l;i++){
result = this[i].call(caller);
if(result === false){
break;
}
}
return result;
};
D.ReCaptcha.prototype = {
/**
* Check if reCaptcha is ready for use
* @method ready
* @return {boolean}
* @memberOf ReCaptcha
* @instance
*/
ready : function(){
return this.holder !== null;
},
/**
* Check if reCaptcha is required for a {@link Pot}
* @return {boolean}
* @memberOf ReCaptcha
* @instance
*/
required : function(){
return this.key !== null;
},
/**
* Load reCaptcha components once
* @param {Object} options
* @return {HTMLElement} current holder element
* @private
*/
load : function(options){
if(typeof grecaptcha !== 'undefined' && this.required() && !this.ready()){
this.holder = document.createElement('div');
options.sitekey = this.key;
D.installReCaptchaCallbacks(this, options);
this.id = grecaptcha.render(this.holder, options);
}
return this.holder;
},
/**
* Save user response
* @event Save
* @param {string} response
* @private
*/
save : function(response){
this.response = response;
},
/**
* Reset reCaptcha component
* @event Reset
* @private
*/
reset : function(){
if(this.key && this.id && typeof grecaptcha !== 'undefined'){
grecaptcha.reset(this.id);
}
},
/**
* Check if reCaptcha component is in valid state
* @method valid
* @return {boolean}
* @memberOf ReCaptcha
* @instance
*/
valid : function(){
if(this.required()){
if(typeof grecaptcha === 'undefined'){
return false;
}
else{
return this.ready() && this.response !== null;
}
}
else{
return true;
}
}
};
D.Pot.prototype = {
/**
* Check if a honeypot is in valid state
*
* @method valid
* @memberOf Pot
* @instance
* @return {boolean}
* @fires {@link #.event:Validating|Validating}
* @fires {@link #.event:Fail|Fail}
*/
valid : function(){
var currentTime = D.now();
if(this.hooks.validate.exec(this) && this.empty.value === '' && !this.fast(currentTime) && this.re.valid()){
this.time.value = currentTime;
return true;
}
this.activateCaptcha();
this.hooks.fail.exec(this);
return false;
},
/**
* Get main input's name
*
* @method name
* @memberOf Pot
* @instance
* @return {string}
* @see #empty
*/
/**
* Change main input's name
*
* @method name
* @memberOf Pot
* @instance
* @param {string} name
* @return {string}
* @see #empty
*/
name : function(name){
if(name){
this.empty.name = name;
}
return this.empty.name;
},
/**
* By a validating function or callback to form submission
*
* @method validate
* @memberOf Pot
* @instance
* @param {Function} fn
* @return {Pot} current honeypot
* @chainable
* @example
* pot.validate(function(){ return false; });
*/
validate : function(fn){
this.hooks.validate.push(fn);
return this;
},
/**
* By a fail callback to form submission ( form not passing the validation )
*
* @method fail
* @memberOf Pot
* @instance
* @param {Function} fn
* @return {Pot} current honeypot
* @chainable
* @example
* pot.fail(function(){ alert('Can not submit!'); });
*/
fail : function(fn){
this.hooks.fail.push(fn);
return this;
},
/**
* Get an option by key
*
* @method config
* @memberOf Pot
* @instance
* @param {string} key
* @return {mixed}
* @see #options
* @example
* pot.config('key');
*/
/**
* Set multiple options
*
* @method config
* @memberOf Pot
* @instance
* @param {Object} options an object contains custom options
* @return {string}
* @see #options
* @example
* pot.config({theme : 'dark'});
*/
config : function(options){
if(typeof options === 'string'){
return this.options[options];
}
else{
for(var x in options){
this.options[x] = options[x];
}
return this;
}
},
/**
* Set reCaptcha key or activate reCaptcha to render on this form on {@link #event:Fail|Fail} event
*
* **NOTE :** By default, reCaptcha component is only rendered when form submit failed from validation at the first time
* This component requires Google reCaptcha is loaded and a sitekey is provided. If a forceReCaptcha global {@link Options}
* is already set, the reCaptcha component is forced to be rendered on pot creation.
*
* @method captcha
* @memberOf Pot
* @instance
* @param {string} [key] if not provided, the pot will use {@link #key|global key} instead
* @return {Pot}
* @chainable
*
*/
captcha : function(key){
this.re.key = key || O.key;
return this;
},
/**
* Check if the form submission is too fast
*
* @method fast
* @memberOf Pot
* @instance
* @param {Number} [current] current timestamp
* @return {boolean}
*/
fast : function(current){
current = current || D.now();
return current - this.createdAt <= this.options.time;
},
/* Form ultilities */
/**
* Get an input by name. Useful inside hooked functions
*
* @method input
* @memberOf Pot
* @instance
* @param {string} name input's name
* @return {HTMLInputElement}
* @example
* // for validating
* pot.validate(function(){
* // pot instance is injected inside hooked function
* return this.input('email').value === 'alien.say.hello@earth.to'
* });
*
* // or for callback
* pot.fail(function(){
* this.input('password').value = '';
* });
*/
input : function(name){
return D.getInputByName(this.form, name);
},
/**
* Providing an input's name, get its value
*
* @method value
* @memberOf Pot
* @instance
* @param {string} name
* @return {string}
* @example
* pot.value('register_code').length > 8;
*/
value : function(name){
var element = this.input(name);
if(element){
return element.value;
}
},
/**
* Add and element to holder
*
* @param {HTMLElement} element
* @private
* @method push
* @memberOf Pot
* @instance
*/
push : function(element){
if(element){
this.holder.appendChild(element);
}
},
/**
* Install reCaptcha component on honeypot
*
* @private
* @method activateCaptcha
* @memberOf Pot
* @instance
*/
activateCaptcha : function(){
this.push(this.re.load({
theme:this.options.theme,
type:this.options.type,
size:this.options.size
}));
}
};
D.Pots.prototype = D.Collector.prototype;
/**
* Loop throughout collection and execute the same function on each honeypot
*
* @param {Function} fn
* @return {Pots} current Pots
* @chainable
*
* @method each
* @memberOf Pots
* @instance
*
* @example
* pots.each(function(pot){
* // check if any pot is not ready
* if(!pot.valid()){
* alert('You are not human, are you?');
* }
* });
*/
D.Pots.prototype.each = function(fn){
for(var i = 0, l = this.length;i < l;i++){
fn(this[i]);
}
return this;
};
/**
* Install captcha key on each honeypot inside this collection
*
* @param {string} [key] if no key provided, this method use <a href="./Options.html#key">global key</a> instead
* @return {Pots} current Pots
* @chainable
*
* @method captcha
* @memberOf Pots
* @instance
*
* @example
* // this way
* pots.captcha('someSiteKey');
*
* // has the same effects to
* pots.config({ key : 'someSiteKey' });
*
*
*/
D.Pots.prototype.captcha = function(key){
return this.each(function(p){
p.captcha(key);
});
};
/**
* Config over all honeypot inside this collection
*
* @param {Object} options
* @return {Pots}
*
* @method config
* @memberOf Pots
* @instance
*
* @example
* pots.config({ theme : 'light' });
*/
D.Pots.prototype.config = function(options){
if(typeof options === 'string' && this.length){
return this[0].config(options);
}
else{
return this.each(function(p){
p.config(options);
});
}
};
/**
* Add the same function to validate hooks of all honeypot instances inside this collections
*
* @param {Function} fn
* @return {Pots} current Pots
*
* @method validate
* @memberOf Pots
* @instance
*/
D.Pots.prototype.validate = function(fn){
return this.each(function(p){
p.validate(fn);
});
};
/**
* Add the same function to fail hooks of all honeypot instances inside this collections
*
* @param {Function} fn
* @return {Pots} current Pots
*
* @method fail
* @memberOf Pots
* @instance
*/
D.Pots.prototype.fail = function(fn){
return this.each(function(p){
p.fail(fn);
});
};
/**
* Provide security to forms
*
* - Return {@link Pot} instance for a form or a **collection** that contains **only one** form
* - Return {@link Pots} for multiple forms
* - Without jQuery, this method will use old-fashioned javascript ways to initialize honeypot instances
* - jQuery is optional but **required** to be **loaded before** honeyjs if jQuery selector **string** is provided as a parameter to this function
*
* @see https://api.jquery.com/category/selectors
*
* @module
*
* @param {HTMLFormElement|HTMLCollection|HTMLFormElement[]|jQuerySelctor} Param A Form or a collection of forms to be secured
* @return {Pot|Pots} {@link Pot} for a form and {@link Pots} for a collection of forms
*
* @example
* honey(document.getElementById('secured'));
*
* honey([form1, form2]);
*
* honey(document.getElementByTagName('*'));
*
* honey($('ul#1').find('li'));
*
* // the jQuery ways
* honey('.class');
*
* honey('#id');
*
* honey('form[method=GET]');
*/
exports = function(Param){
if(typeof Param === 'string' && typeof jQuery !== 'undefined'){
return exports(jQuery(Param));
}
if(Param instanceof HTMLFormElement){
return new D.Pot(Param);
}
else{
if(Param.length === 1){
return exports(Param[0]);
}
else{
var collection = new D.Pots();
for(var i = 0, l = Param.length;i < l;i++){
collection.push(exports(Param[i]));
}
return collection;
}
}
};
/**
* Provide a global sitekey for reCaptcha
*
* @param {string} [key] global sitekey to be set
* @return {string} global sitekey after being changed
* @see {@link Options#key}
*
* @example
* // this
* honey.requireCaptcha('someSiteKey');
*
* // has the same effects as
* honey.config({ key : 'someSiteKey' });
*/
exports.requireCaptcha = function(key){
O.key = key;
return O.key;
};
/**
* Force all new created {@link Pot} to **install and activate** reCaptcha component on pot creation
*
* @requires {@link https://developers.google.com/recaptcha/docs/display|Google reCaptcha}
*
* @see {@link Pot#captcha}
* @param {string} [key] global sitekey to be set and used
* @return {boolean} true is reCaptcha is forced from now on.
*
* @example
* // force reCaptcha
* honey.forCaptcha('someSiteKey');
*
* var pot = honey("#1"); // the reCaptcha component of form#1 is now rendered immediately (actually 300ms delay)
*/
exports.forceCaptcha = function(key){
O.forceCaptcha = !!exports.requireCaptcha(key);
return O.forceCaptcha;
};
/**
* Get or set global options
*
* @param {string|Objects} param
* @return {mixed}
* @see {@link Options}
*
* @example
* // Get an option by key
* honey.config('theme');
*
* // Set options
* honey.config({ theme : 'dark' });
*/
exports.config = function(params){
if(typeof params === 'string'){
return O[params];
}
else{
for(var x in params){
O[x] = params[x];
}
}
};
/**
* Automatically secure all forms inside document
*
* @return {Pots} A collection of honeypot instance
*
* @example
* // this
* honey.all();
*
* // is an alias of
* honey(document.getElementsByTagName('form'));
*/
exports.all = function(){
return exports(D.getForms());
};
/**
* jQuery plugin
*
* @global
* @external "jQuery.fn"
* @see {@link http://learn.jquery.com/plugins/|jQuery Plugins}
*/
/**
* Automatically secure all forms inside jQuery object
* @param {string|Objects} [params] set a reCaptcha sitekey if input a string, or change options if input an object
* @return {Pot|Pots}
*
* @function external:"jQuery.fn".honey
* @instance
*
* @example
* $('#1').honey();
*
* $('#2').honey('someSiteKey'); // is short version of $('#2').honey().captcha('someSiteKey');
*
* $('#3').honey({ theme : 'dark' }); // is short version of $('#3').honey().config({ theme : 'dark' });
*/
D.installjQueryPlugin(jQuery, 'honey', function(params){
var ret = exports(this);
if(ret && params){
if(typeof params === 'string'){
return ret.captcha(params);
}
else{
return ret.config(params);
}
}
return ret;
});
/* MOCKING DEPENDENCIES FOR TESTING */
/* PhantomJS */
D.__installDev(D.isDev());
return exports;
})();