Skip to content

Instantly share code, notes, and snippets.

@tomjn tomjn/behave.js
Last active Apr 17, 2018

Embed
What would you like to do?
Adds a shortcode to WordPress that lets you test escaping and sanitising functions on content
=== Escaping Checker ===
Contributors: tjnowell
Donate link: https://tomjn.com/
Tags: security
Requires at least: 4.3
Tested up to: 4.95
Stable tag: 1.1
License: GPLv2 or later
License URI: http://www.gnu.org/licenses/gpl-2.0.html
A handy tool for checking the output of escaping, sanitization and validation functions
== Description ==
This plugin adds a shortcode that when used inserts an escaping checker. The checker gives you a form you can enter potentially unsafe content into, and when submitted, passes that content through every sanitisation, validation and escaping function in WordPress Core, and some PHP Core functions
== Installation ==
1. Upload the plugin folder into the `/wp-content/plugins/` directory
1. Activate the plugin through the 'Plugins' menu in WordPress
1. Place the `[tomjn_escaping_tests]` in a page or post and save
If you prefer to create a dedicate page template, use `echo tomjn_escaping_tests();`
== Frequently Asked Questions ==
= There's a Lot of Escaping Functions! =
Yes! But not every function listed here is an escaping function, there are validation and sanitising functions too, some of them are PHP Core functions rather than WordPress functions
== Screenshots ==
1. The escaping form
== Changelog ==
= 1.1 =
* Put a green or yellow border around output depending on wether it matches the original input
* Bumped tested WP version to 4.9.5
= 1.0 =
* Added `behave.js` to make typing content easier
== Upgrade Notice ==
/*
* Behave.js
*
* Copyright 2013, Jacob Kelley - http://jakiestfu.com/
* Released under the MIT Licence
* http://opensource.org/licenses/MIT
*
* Github: http://github.com/jakiestfu/Behave.js/
* Version: 1.5
*/
(function(undefined){
'use strict';
var BehaveHooks = BehaveHooks || (function(){
var hooks = {};
return {
add: function(hookName, fn){
if(typeof hookName == "object"){
var i;
for(i=0; i<hookName.length; i++){
var theHook = hookName[i];
if(!hooks[theHook]){
hooks[theHook] = [];
}
hooks[theHook].push(fn);
}
} else {
if(!hooks[hookName]){
hooks[hookName] = [];
}
hooks[hookName].push(fn);
}
},
get: function(hookName){
if(hooks[hookName]){
return hooks[hookName];
}
}
};
})(),
Behave = Behave || function (userOpts) {
if (typeof String.prototype.repeat !== 'function') {
String.prototype.repeat = function(times) {
if(times < 1){
return '';
}
if(times % 2){
return this.repeat(times - 1) + this;
}
var half = this.repeat(times / 2);
return half + half;
};
}
if (typeof Array.prototype.filter !== 'function') {
Array.prototype.filter = function(func /*, thisp */) {
if (this === null) {
throw new TypeError();
}
var t = Object(this),
len = t.length >>> 0;
if (typeof func != "function"){
throw new TypeError();
}
var res = [],
thisp = arguments[1];
for (var i = 0; i < len; i++) {
if (i in t) {
var val = t[i];
if (func.call(thisp, val, i, t)) {
res.push(val);
}
}
}
return res;
};
}
var defaults = {
textarea: null,
replaceTab: true,
softTabs: true,
tabSize: 4,
autoOpen: true,
overwrite: true,
autoStrip: true,
autoIndent: true,
fence: false
},
tab,
newLine,
charSettings = {
keyMap: [
{ open: "\"", close: "\"", canBreak: false },
{ open: "'", close: "'", canBreak: false },
{ open: "(", close: ")", canBreak: false },
{ open: "[", close: "]", canBreak: true },
{ open: "{", close: "}", canBreak: true }
]
},
utils = {
_callHook: function(hookName, passData){
var hooks = BehaveHooks.get(hookName);
passData = typeof passData=="boolean" && passData === false ? false : true;
if(hooks){
if(passData){
var theEditor = defaults.textarea,
textVal = theEditor.value,
caretPos = utils.cursor.get(),
i;
for(i=0; i<hooks.length; i++){
hooks[i].call(undefined, {
editor: {
element: theEditor,
text: textVal,
levelsDeep: utils.levelsDeep()
},
caret: {
pos: caretPos
},
lines: {
current: utils.cursor.getLine(textVal, caretPos),
total: utils.editor.getLines(textVal)
}
});
}
} else {
for(i=0; i<hooks.length; i++){
hooks[i].call(undefined);
}
}
}
},
defineNewLine: function(){
var ta = document.createElement('textarea');
ta.value = "\n";
if(ta.value.length==2){
newLine = "\r\n";
} else {
newLine = "\n";
}
},
defineTabSize: function(tabSize){
if(typeof defaults.textarea.style.OTabSize != "undefined"){
defaults.textarea.style.OTabSize = tabSize; return;
}
if(typeof defaults.textarea.style.MozTabSize != "undefined"){
defaults.textarea.style.MozTabSize = tabSize; return;
}
if(typeof defaults.textarea.style.tabSize != "undefined"){
defaults.textarea.style.tabSize = tabSize; return;
}
},
cursor: {
getLine: function(textVal, pos){
return ((textVal.substring(0,pos)).split("\n")).length;
},
get: function() {
if (typeof document.createElement('textarea').selectionStart==="number") {
return defaults.textarea.selectionStart;
} else if (document.selection) {
var caretPos = 0,
range = defaults.textarea.createTextRange(),
rangeDupe = document.selection.createRange().duplicate(),
rangeDupeBookmark = rangeDupe.getBookmark();
range.moveToBookmark(rangeDupeBookmark);
while (range.moveStart('character' , -1) !== 0) {
caretPos++;
}
return caretPos;
}
},
set: function (start, end) {
if(!end){
end = start;
}
if (defaults.textarea.setSelectionRange) {
defaults.textarea.focus();
defaults.textarea.setSelectionRange(start, end);
} else if (defaults.textarea.createTextRange) {
var range = defaults.textarea.createTextRange();
range.collapse(true);
range.moveEnd('character', end);
range.moveStart('character', start);
range.select();
}
},
selection: function(){
var textAreaElement = defaults.textarea,
start = 0,
end = 0,
normalizedValue,
range,
textInputRange,
len,
endRange;
if (typeof textAreaElement.selectionStart == "number" && typeof textAreaElement.selectionEnd == "number") {
start = textAreaElement.selectionStart;
end = textAreaElement.selectionEnd;
} else {
range = document.selection.createRange();
if (range && range.parentElement() == textAreaElement) {
normalizedValue = utils.editor.get();
len = normalizedValue.length;
textInputRange = textAreaElement.createTextRange();
textInputRange.moveToBookmark(range.getBookmark());
endRange = textAreaElement.createTextRange();
endRange.collapse(false);
if (textInputRange.compareEndPoints("StartToEnd", endRange) > -1) {
start = end = len;
} else {
start = -textInputRange.moveStart("character", -len);
start += normalizedValue.slice(0, start).split(newLine).length - 1;
if (textInputRange.compareEndPoints("EndToEnd", endRange) > -1) {
end = len;
} else {
end = -textInputRange.moveEnd("character", -len);
end += normalizedValue.slice(0, end).split(newLine).length - 1;
}
}
}
}
return start==end ? false : {
start: start,
end: end
};
}
},
editor: {
getLines: function(textVal){
return (textVal).split("\n").length;
},
get: function(){
return defaults.textarea.value.replace(/\r/g,'');
},
set: function(data){
defaults.textarea.value = data;
}
},
fenceRange: function(){
if(typeof defaults.fence == "string"){
var data = utils.editor.get(),
pos = utils.cursor.get(),
hacked = 0,
matchedFence = data.indexOf(defaults.fence),
matchCase = 0;
while(matchedFence>=0){
matchCase++;
if( pos < (matchedFence+hacked) ){
break;
}
hacked += matchedFence+defaults.fence.length;
data = data.substring(matchedFence+defaults.fence.length);
matchedFence = data.indexOf(defaults.fence);
}
if( (hacked) < pos && ( (matchedFence+hacked) > pos ) && matchCase%2===0){
return true;
}
return false;
} else {
return true;
}
},
isEven: function(_this,i){
return i%2;
},
levelsDeep: function(){
var pos = utils.cursor.get(),
val = utils.editor.get();
var left = val.substring(0, pos),
levels = 0,
i, j;
for(i=0; i<left.length; i++){
for (j=0; j<charSettings.keyMap.length; j++) {
if(charSettings.keyMap[j].canBreak){
if(charSettings.keyMap[j].open == left.charAt(i)){
levels++;
}
if(charSettings.keyMap[j].close == left.charAt(i)){
levels--;
}
}
}
}
var toDecrement = 0,
quoteMap = ["'", "\""];
for(i=0; i<charSettings.keyMap.length; i++) {
if(charSettings.keyMap[i].canBreak){
for(j in quoteMap){
toDecrement += left.split(quoteMap[j]).filter(utils.isEven).join('').split(charSettings.keyMap[i].open).length - 1;
}
}
}
var finalLevels = levels - toDecrement;
return finalLevels >=0 ? finalLevels : 0;
},
deepExtend: function(destination, source) {
for (var property in source) {
if (source[property] && source[property].constructor &&
source[property].constructor === Object) {
destination[property] = destination[property] || {};
utils.deepExtend(destination[property], source[property]);
} else {
destination[property] = source[property];
}
}
return destination;
},
addEvent: function addEvent(element, eventName, func) {
if (element.addEventListener){
element.addEventListener(eventName,func,false);
} else if (element.attachEvent) {
element.attachEvent("on"+eventName, func);
}
},
removeEvent: function addEvent(element, eventName, func){
if (element.addEventListener){
element.removeEventListener(eventName,func,false);
} else if (element.attachEvent) {
element.detachEvent("on"+eventName, func);
}
},
preventDefaultEvent: function(e){
if(e.preventDefault){
e.preventDefault();
} else {
e.returnValue = false;
}
}
},
intercept = {
tabKey: function (e) {
if(!utils.fenceRange()){ return; }
if (e.keyCode == 9) {
utils.preventDefaultEvent(e);
var toReturn = true;
utils._callHook('tab:before');
var selection = utils.cursor.selection(),
pos = utils.cursor.get(),
val = utils.editor.get();
if(selection){
var tempStart = selection.start;
while(tempStart--){
if(val.charAt(tempStart)=="\n"){
selection.start = tempStart + 1;
break;
}
}
var toIndent = val.substring(selection.start, selection.end),
lines = toIndent.split("\n"),
i;
if(e.shiftKey){
for(i = 0; i<lines.length; i++){
if(lines[i].substring(0,tab.length) == tab){
lines[i] = lines[i].substring(tab.length);
}
}
toIndent = lines.join("\n");
utils.editor.set( val.substring(0,selection.start) + toIndent + val.substring(selection.end) );
utils.cursor.set(selection.start, selection.start+toIndent.length);
} else {
for(i in lines){
lines[i] = tab + lines[i];
}
toIndent = lines.join("\n");
utils.editor.set( val.substring(0,selection.start) + toIndent + val.substring(selection.end) );
utils.cursor.set(selection.start, selection.start+toIndent.length);
}
} else {
var left = val.substring(0, pos),
right = val.substring(pos),
edited = left + tab + right;
if(e.shiftKey){
if(val.substring(pos-tab.length, pos) == tab){
edited = val.substring(0, pos-tab.length) + right;
utils.editor.set(edited);
utils.cursor.set(pos-tab.length);
}
} else {
utils.editor.set(edited);
utils.cursor.set(pos + tab.length);
toReturn = false;
}
}
utils._callHook('tab:after');
}
return toReturn;
},
enterKey: function (e) {
if(!utils.fenceRange()){ return; }
if (e.keyCode == 13) {
utils.preventDefaultEvent(e);
utils._callHook('enter:before');
var pos = utils.cursor.get(),
val = utils.editor.get(),
left = val.substring(0, pos),
right = val.substring(pos),
leftChar = left.charAt(left.length - 1),
rightChar = right.charAt(0),
numTabs = utils.levelsDeep(),
ourIndent = "",
closingBreak = "",
finalCursorPos,
i;
if(!numTabs){
finalCursorPos = 1;
} else {
while(numTabs--){
ourIndent+=tab;
}
ourIndent = ourIndent;
finalCursorPos = ourIndent.length + 1;
for(i=0; i<charSettings.keyMap.length; i++) {
if (charSettings.keyMap[i].open == leftChar && charSettings.keyMap[i].close == rightChar){
closingBreak = newLine;
}
}
}
var edited = left + newLine + ourIndent + closingBreak + (ourIndent.substring(0, ourIndent.length-tab.length) ) + right;
utils.editor.set(edited);
utils.cursor.set(pos + finalCursorPos);
utils._callHook('enter:after');
}
},
deleteKey: function (e) {
if(!utils.fenceRange()){ return; }
if(e.keyCode == 8){
utils.preventDefaultEvent(e);
utils._callHook('delete:before');
var pos = utils.cursor.get(),
val = utils.editor.get(),
left = val.substring(0, pos),
right = val.substring(pos),
leftChar = left.charAt(left.length - 1),
rightChar = right.charAt(0),
i;
if( utils.cursor.selection() === false ){
for(i=0; i<charSettings.keyMap.length; i++) {
if (charSettings.keyMap[i].open == leftChar && charSettings.keyMap[i].close == rightChar) {
var edited = val.substring(0,pos-1) + val.substring(pos+1);
utils.editor.set(edited);
utils.cursor.set(pos - 1);
return;
}
}
var edited = val.substring(0,pos-1) + val.substring(pos);
utils.editor.set(edited);
utils.cursor.set(pos - 1);
} else {
var sel = utils.cursor.selection(),
edited = val.substring(0,sel.start) + val.substring(sel.end);
utils.editor.set(edited);
utils.cursor.set(pos);
}
utils._callHook('delete:after');
}
}
},
charFuncs = {
openedChar: function (_char, e) {
utils.preventDefaultEvent(e);
utils._callHook('openChar:before');
var pos = utils.cursor.get(),
val = utils.editor.get(),
left = val.substring(0, pos),
right = val.substring(pos),
edited = left + _char.open + _char.close + right;
defaults.textarea.value = edited;
utils.cursor.set(pos + 1);
utils._callHook('openChar:after');
},
closedChar: function (_char, e) {
var pos = utils.cursor.get(),
val = utils.editor.get(),
toOverwrite = val.substring(pos, pos + 1);
if (toOverwrite == _char.close) {
utils.preventDefaultEvent(e);
utils._callHook('closeChar:before');
utils.cursor.set(utils.cursor.get() + 1);
utils._callHook('closeChar:after');
return true;
}
return false;
}
},
action = {
filter: function (e) {
if(!utils.fenceRange()){ return; }
var theCode = e.which || e.keyCode;
if(theCode == 39 || theCode == 40 && e.which===0){ return; }
var _char = String.fromCharCode(theCode),
i;
for(i=0; i<charSettings.keyMap.length; i++) {
if (charSettings.keyMap[i].close == _char) {
var didClose = defaults.overwrite && charFuncs.closedChar(charSettings.keyMap[i], e);
if (!didClose && charSettings.keyMap[i].open == _char && defaults.autoOpen) {
charFuncs.openedChar(charSettings.keyMap[i], e);
}
} else if (charSettings.keyMap[i].open == _char && defaults.autoOpen) {
charFuncs.openedChar(charSettings.keyMap[i], e);
}
}
},
listen: function () {
if(defaults.replaceTab){ utils.addEvent(defaults.textarea, 'keydown', intercept.tabKey); }
if(defaults.autoIndent){ utils.addEvent(defaults.textarea, 'keydown', intercept.enterKey); }
if(defaults.autoStrip){ utils.addEvent(defaults.textarea, 'keydown', intercept.deleteKey); }
utils.addEvent(defaults.textarea, 'keypress', action.filter);
utils.addEvent(defaults.textarea, 'keydown', function(){ utils._callHook('keydown'); });
utils.addEvent(defaults.textarea, 'keyup', function(){ utils._callHook('keyup'); });
}
},
init = function (opts) {
if(opts.textarea){
utils._callHook('init:before', false);
utils.deepExtend(defaults, opts);
utils.defineNewLine();
if (defaults.softTabs) {
tab = " ".repeat(defaults.tabSize);
} else {
tab = "\t";
utils.defineTabSize(defaults.tabSize);
}
action.listen();
utils._callHook('init:after', false);
}
};
this.destroy = function(){
utils.removeEvent(defaults.textarea, 'keydown', intercept.tabKey);
utils.removeEvent(defaults.textarea, 'keydown', intercept.enterKey);
utils.removeEvent(defaults.textarea, 'keydown', intercept.deleteKey);
utils.removeEvent(defaults.textarea, 'keypress', action.filter);
};
init(userOpts);
};
if (typeof module !== 'undefined' && module.exports) {
module.exports = Behave;
}
if (typeof ender === 'undefined') {
this.Behave = Behave;
this.BehaveHooks = BehaveHooks;
}
if (typeof define === "function" && define.amd) {
define("behave", [], function () {
return Behave;
});
}
}).call(this);
<?php
/*
Plugin Name: Toms Escaping tester
Plugin URI: https://tomjn.com
Description: Provides a tomjn_esc_test shortcode that lets you test values with escaping
Author: Tom J Nowell
Version: 1.1
Author URI: https://www.tomjn.com/escaping
License: GPLv2 or later
License URI: http://www.gnu.org/licenses/gpl-2.0.html
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
function tomjn_escaping_tests(){
$value = (!empty( $_POST['tomjn_esc_value'] ) ) ? $_POST['tomjn_esc_value'] : false;
$esc_array = array();
if ( $value ) {
global $wpdb;
$esc_array = array(
'esc_html' => esc_html( $value ),
'esc_attr' => esc_attr($value),
'esc_js' => esc_js($value),
'esc_textarea' => esc_textarea( $value ),
'esc_url' => esc_url($value),
'esc_url_raw' => esc_url_raw($value),
'esc_sql' => esc_sql( $value ),
'wp_json_encode' => wp_json_encode($value),
'wp_kses( , array(), array())' => wp_kses( $value, array(), array()),
'wp_kses_data' => wp_kses_data( $value ),
'wp_kses_post' => wp_kses_post( $value ),
'wp_strip_all_tags' => wp_strip_all_tags($value),
'strip_tags' => strip_tags($value),
'htmlentities' => htmlentities($value),
'urlencode' => urlencode($value),
'rawurlencode' => rawurlencode($value),
'sanitize_email' => sanitize_email($value),
'sanitize_file_name' => sanitize_file_name($value),
'sanitize_html_class' => sanitize_html_class($value),
'sanitize_key' => sanitize_key($value),
/*'sanitize_meta' => sanitize_meta($value), doesn't make sense to run */
'sanitize_mime_type' => sanitize_mime_type($value),
/*'sanitize_option' => sanitize_option($value), this needs 2 values, and sanitising depends on param 1 */
/*'sanitize_post' => sanitize_post($value), this isn't a WP_Post object */
'sanitize_sql_orderby' => sanitize_sql_orderby($value),
/*'sanitize_term' => sanitize_term($value), expects a WP_Term and a taxonomy*/
/*'sanitize_term_field' => sanitize_term_field($value), same as above*/
'sanitize_text_field' => sanitize_text_field($value),
'sanitize_title' => sanitize_title($value),
'sanitize_title_for_query' => sanitize_title_for_query($value),
'sanitize_title_with_dashes' => sanitize_title_with_dashes( $value ),
'sanitize_user' => sanitize_user( $value ),
'balanceTags' => balanceTags( $value ),
'tag_escape' => tag_escape( $value ),
'addslashes' => addslashes( $value ),
'$wpdb->esc_like' => $wpdb->esc_like( $value ),
'$wpdb->prepare' => $wpdb->prepare( $value, array() ),
);
}
wp_enqueue_script( 'behavejs', plugins_url( 'behave.js', __FILE__ ), array(), '1.5', true );
ob_start();
?>
<form action="" method="post">
<p>
<textarea id="tomjn_escape_textarea" name="tomjn_esc_value"><?php
if ( $value ) {
echo wp_unslash( esc_html( $esc_array['esc_textarea'] ) );
}
?></textarea>
</p>
<style>
#tomjn_escape_textarea {
font-family: monospace;
min-height: 200px;
}
.tomjn_escape_same {
outline: 2px solid rgb(163, 190, 140);
}
.tomjn_escape_different {
outline: 2px solid rgb(235, 203, 139);
}
</style>
<script>
jQuery(document).ready( function() {
BehaveHooks.add(['keydown'], function(data){
var numLines = data.lines.total,
fontSize = parseInt( getComputedStyle(data.editor.element)['font-size'] ),
padding = parseInt( getComputedStyle(data.editor.element)['padding'] );
data.editor.element.style.height = (((numLines*fontSize)+padding))+'px';
});
var editor = new Behave({
textarea: document.getElementById('tomjn_escape_textarea'),
replaceTab: true,
softTabs: false,
tabSize: 4,
autoOpen: true,
overwrite: true,
autoStrip: true,
autoIndent: true
});
});
</script>
<p><input type="submit" value="Escape"></p>
</form>
<p>Refresh rather than re-submit if you're having problems with expanding quote escaping</p>
<?php
if ( $value ) {
?>
<dl>
<?php
foreach($esc_array as $key => $val) {
$class = 'tomjn_escape_different';
if ( $val === $value ) {
$class = 'tomjn_escape_same';
}
?>
<dt><?php echo esc_html( $key ); ?></dt>
<dd><pre class="<?php echo esc_attr( $class ); ?>"><?php echo htmlspecialchars( $val ); ?></pre></dd>
<?php
}
?>
</dl>
<?php
}
return ob_get_clean();
}
add_shortcode( 'tomjn_esc_test', 'tomjn_escaping_tests' );
@richardtape

This comment has been minimized.

Copy link

richardtape commented Oct 2, 2015

You mad, mad genius. Love this, thanks for building it.

I wonder if we can figure out a way to show the differences in the output from each of them, too, so people can know what to expect.

@zackkatz

This comment has been minimized.

Copy link

zackkatz commented Oct 2, 2015

Nice! I suggest running the output through htmlspecialchars() so the escaped value differences are visible to the eye.

@tfrommen

This comment has been minimized.

Copy link

tfrommen commented Oct 2, 2015

+1 for @zackkatz's suggestion. I just wanted to write the same. Also, the wrapping esc_html() tempers too much with the actual results.
And the last one is $wpdb->prepare (and not addslashes).

But still, very handy. :)

@JeppeSigaard

This comment has been minimized.

Copy link

JeppeSigaard commented Oct 2, 2015

Here's a leaner version, friend: https://gist.github.com/JeppeSigaard/1a439d2af6d4615c41c8
Just reorganizing, not really concerned with the validity of the output. Hope you like.

@tomjn

This comment has been minimized.

Copy link
Owner Author

tomjn commented Dec 26, 2015

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.