rewrite sanitizeHTML to support global attributes

This commit is contained in:
j 2013-11-10 21:59:36 +00:00
parent 5f807a3ad4
commit fee4339d11
3 changed files with 165 additions and 87 deletions

View file

@ -28,11 +28,11 @@ Ox.Editable = function(options, self) {
editable: true,
editing: false,
format: null,
globalAttributes: [],
height: 0,
highlight: null,
maxHeight: void 0,
placeholder: '',
replaceTags: {},
submitOnBlur: true,
tags: null,
tooltip: '',
@ -230,7 +230,7 @@ Ox.Editable = function(options, self) {
return (
self.options.type == 'input'
? Ox.encodeHTMLEntities(value)
: Ox.sanitizeHTML(value, self.options.tags, self.options.replaceTags)
: Ox.sanitizeHTML(value, self.options.tags, self.options.globalAttributes)
);
}

View file

@ -16,9 +16,9 @@ Ox.EditableContent = function(options, self) {
editable: true,
editing: false,
format: null,
globalAttributes: [],
highlight: null,
placeholder: '',
replaceTags: {},
submitOnBlur: true,
tags: null,
tooltip: '',
@ -190,7 +190,7 @@ Ox.EditableContent = function(options, self) {
return (
self.options.type == 'input'
? Ox.encodeHTMLEntities(value)
: Ox.sanitizeHTML(value, self.options.tags, self.options.replaceTags)
: Ox.sanitizeHTML(value, self.options.tags, self.options.globalAttributes)
);
}

View file

@ -4,17 +4,80 @@
var defaultTags = [
// inline formatting
'b', 'bdi', 'code', 'em', 'i', 'q', 's', 'span', 'strong', 'sub', 'sup', 'u',
{'name': 'b'},
{'name': 'bdi'},
{'name': 'code'},
{'name': 'em'},
{'name': 'i'},
{'name': 'q'},
{'name': 's'},
{'name': 'span'},
{'name': 'strong'},
{'name': 'sub'},
{'name': 'sup'},
{'name': 'u'},
// block formatting
'blockquote', 'cite', 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'pre',
{'name': 'blockquote'},
{'name': 'cite'},
{
'name': 'div',
'optional': ['style'],
'validation': {
'style': /^direction: rtl$/
}
},
{'name': 'h1'},
{'name': 'h2'},
{'name': 'h3'},
{'name': 'h4'},
{'name': 'h5'},
{'name': 'h6'},
{'name': 'p'},
{'name': 'pre'},
// lists
'li', 'ol', 'ul',
{'name': 'li'},
{'name': 'ol'},
{'name': 'ul'},
// tables
'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'tr',
{'name': 'table'},
{'name': 'tbody'},
{'name': 'td'},
{'name': 'tfoot'},
{'name': 'th'},
{'name': 'thead'},
{'name': 'tr'},
// other
'a', 'br', 'figure', 'figcaption', 'iframe', 'img',
// special
'rtl', '[]'
{'name': '[]'},
{
'name': 'a',
'required': ['href'],
'validation': {
'href': /^((https?:\/\/|\/|mailto:).*?)/
}
},
{'name': 'br'},
{
'name': 'iframe',
'optional': ['width', 'height'],
'required': ['src'],
'validation': {
'width': /^\d+$/,
'height': /^\d+$/,
'src': /^((https?:\/\/|\/|mailto:).*?)/
}
},
{
'name': 'img',
'optional': ['width', 'height'],
'required': ['src'],
'validation': {
'width': /^\d+$/,
'height': /^\d+$/,
'src': /^((https?:\/\/|\/|mailto:).*?)/
},
},
{'name': 'figure'},
{'name': 'figcaption'}
],
htmlEntities = {
'"': '&quot;', '&': '&amp;', "'": '&apos;', '<': '&lt;', '>': '&gt;'
@ -26,56 +89,6 @@
'a', 'b', 'br', 'code', 'i', 's', 'span', 'u'
].join('|') + ')\\/?>', 'gi')
},
replace = {
a: [
[
/<a [^<>]*?href="((\/|https?:\/\/|mailto:).*?)".*?>/gi,
'<a href="{1}">',
],
[
/<\/a>/gi,
'</a>'
]
],
iframe: [
[
/<iframe [^<>]*?width="(\d+)" height="(\d+)"[^<>]*?src="((\/|https?:\/\/).+?)".*?>/gi,
'<iframe width="{1}" height="{2}" src="{3}">'
],
[
/<iframe [^<>]*?src="((\/|https?:\/\/).+?)".*?>/gi,
'<iframe src="{1}">'
],
[
/<\/iframe>/gi,
'</iframe>'
]
],
img: [
[
/<img [^<>]*?src="((\/|https?:\/\/).+?)".*?>/gi,
'<img src="{1}">'
]
],
rtl: [
[
/<rtl>/gi,
'<div style="direction: rtl">'
],
[
/<\/rtl>/gi,
'</div>'
]
],
'*': function(tag) {
return [
[
new RegExp('</?' + tag + ' ?/?>', 'gi'),
'{0}'
]
];
}
},
salt = Ox.range(2).map(function(){
return Ox.range(16).map(function() {
return Ox.char(65 + Ox.random(26));
@ -491,16 +504,16 @@
> Ox.sanitizeHTML('<a href="http://foo.com" onclick="alert()">foo</a>')
'<a href="http://foo.com">foo</a>'
> Ox.sanitizeHTML('<a href="javascript:alert()">foo</a>')
'&lt;a href="javascript:alert()"&gt;foo'
'&lt;a href="javascript:alert()"&gt;foo&lt;/a&gt;'
> Ox.sanitizeHTML('<a href="foo">foo</a>')
'&lt;a href="foo"&gt;foo'
'&lt;a href="foo"&gt;foo&lt;/a&gt;'
> Ox.sanitizeHTML('<a href="/foo">foo</a>')
'<a href="/foo">foo</a>'
> Ox.sanitizeHTML('<a href="/">foo</a>')
'<a href="/">foo</a>'
> Ox.sanitizeHTML('[http://foo.com foo]')
'<a href="http://foo.com">foo</a>'
> Ox.sanitizeHTML('<rtl>foo</rtl>')
> Ox.sanitizeHTML('<div style="direction: rtl">foo</div>')
'<div style="direction: rtl">foo</div>'
> Ox.sanitizeHTML('<script>alert()</script>')
'&lt;script&gt;alert()&lt;/script&gt;'
@ -514,40 +527,105 @@
'&amp;&amp;'
> Ox.sanitizeHTML('<http://foo.com>')
'&lt;<a href="http://foo.com">http://foo.com</a>&gt;'
> Ox.sanitizeHTML('<foo value="http://foo.com"></foo>')
'"&lt;foo value="http://foo.com"&gt;&lt;/foo&gt;"'
@*/
Ox.sanitizeHTML = function(html, tags, replaceTags) {
var matches = [];
Ox.sanitizeHTML = function(html, tags, globalAttributes) {
tags = tags || defaultTags;
replaceTags = replaceTags || {};
globalAttributes = globalAttributes || [];
var escaped = {},
level = 0,
matches = [],
nonClosingTags = ['img', 'br'],
validAttributes = {}, requiredAttributes = {}, validation = {},
validTags = tags.map(function(tag) {
validAttributes[tag.name] = globalAttributes
.concat(tag.required || [])
.concat(tag.optional || []);
requiredAttributes[tag.name] = tag.required || [];
validation[tag.name] = tag.validation || {};
return tag.name;
});
// html = Ox.clean(html); fixme: can this be a parameter?
if (tags.indexOf('[]') > -1) {
if (validTags.indexOf('[]') > -1) {
html = html.replace(
/\[((\/|https?:\/\/|mailto:).+?) (.+?)\]/gi,
'<a href="$1">$3</a>'
);
tags = tags.filter(function(tag) {
validTags = validTags.filter(function(tag) {
return tag != '[]';
});
}
tags.forEach(function(tag) {
var array = replaceTags[tag] || replace[tag] || replace['*'](tag);
Ox.forEach(array, function(value) {
html = html.replace(value[0], function() {
var match;
if (Ox.isFunction(value[1])) {
match = value[1].apply(null, arguments);
} else {
match = Ox.formatString(value[1], arguments);
html = splitHTMLTags(html).map(function(string, i) {
var attributes,
attrs = {},
attrRegexp = /([^=\ ]+)="([^"]+)"/g,
isClosing,
isTag = i % 2,
isValid = true,
name,
match,
tag,
tagRegexp = /<(\/)?([^\ \/]+)(.*?)(\/)?>/g;
if (isTag) {
tag = tagRegexp.exec(string);
if (tag) {
isClosing = !Ox.isUndefined(tag[1]);
name = tag[2];
attributes = tag[3].trim();
while(match = attrRegexp.exec(attributes)) {
if (validAttributes[name] && validAttributes[name].indexOf(match[1]) > -1) {
attrs[match[1]] = match[2];
}
}
matches.push(match);
return salt.join(matches.length - 1);
});
});
});
html = Ox.encodeHTMLEntities(Ox.decodeHTMLEntities(html));
matches.forEach(function(match, i) {
html = html.replace(new RegExp(salt.join(i)), match);
});
if (!isClosing && nonClosingTags.indexOf(name) == -1) {
level++;
}
if (Ox.isEmpty(attrs) && attributes.length || validTags.indexOf(name) == -1) {
isValid = false;
} else if(!isClosing && requiredAttributes[name]) {
requiredAttributes[name].forEach(function(attr) {
if (Ox.isUndefined(attrs[attr])) {
isValid = false;
}
});
}
if (isValid && !Ox.isEmpty(attrs)) {
Ox.forEach(attrs, function(value, key) {
if (!Ox.isUndefined(validation[name][key])
&& !validation[name][key].exec(value)) {
isValid = false;
return false;
}
});
}
if (isValid && isClosing) {
isValid = !escaped[level];
} else {
escaped[level] = !isValid;
}
if (isClosing) {
level --;
}
if (isValid) {
return '<'
+ (isClosing ? '/' : '')
+ name
+ (!isClosing && !Ox.isEmpty(attrs)
? ' ' + Ox.values(Ox.map(attrs, function(value, key) {
return key + '="' + value + '"';
})).join(' ')
: '')
+ '>';
}
}
}
return Ox.encodeHTMLEntities(Ox.decodeHTMLEntities(string));
}).join('');
//FIXME: dont add links to urls inside of escaped tags
html = Ox.addLinks(html, true);
html = html.replace(/\n\n/g, '<br/><br/>');
// Close extra opening and remove extra closing tags.