implement simplified translation logic, forcing the use of safe application via jQuery element
This commit is contained in:
parent
aa3f1206b2
commit
62365880b4
4 changed files with 122 additions and 111 deletions
|
@ -322,19 +322,12 @@ jQuery.PrivateBin = (function($, RawDeflate) {
|
||||||
let format = args[0],
|
let format = args[0],
|
||||||
i = 1;
|
i = 1;
|
||||||
return format.replace(/%(s|d)/g, function (m) {
|
return format.replace(/%(s|d)/g, function (m) {
|
||||||
// m is the matched format, e.g. %s, %d
|
|
||||||
let val = args[i];
|
let val = args[i];
|
||||||
// A switch statement so that the formatter can be extended.
|
if (m === '%d') {
|
||||||
switch (m)
|
val = parseFloat(val);
|
||||||
{
|
if (isNaN(val)) {
|
||||||
case '%d':
|
val = 0;
|
||||||
val = parseFloat(val);
|
}
|
||||||
if (isNaN(val)) {
|
|
||||||
val = 0;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
// Default is %s
|
|
||||||
}
|
}
|
||||||
++i;
|
++i;
|
||||||
return val;
|
return val;
|
||||||
|
@ -547,19 +540,23 @@ jQuery.PrivateBin = (function($, RawDeflate) {
|
||||||
/**
|
/**
|
||||||
* translate a string
|
* translate a string
|
||||||
*
|
*
|
||||||
* Optionally pass a jQuery element as the first parameter, to automatically
|
* As the first parameter a jQuery element has to be provided, to let
|
||||||
* let the text of this element be replaced. In case the (asynchronously
|
* the text of this element be replaced. In case the (asynchronously
|
||||||
* loaded) language is not downloadet yet, this will make sure the string
|
* loaded) language is not downloadet yet, this will make sure the string
|
||||||
* is replaced when it is actually loaded.
|
* is replaced when it is actually loaded. This also handles HTML in
|
||||||
* So for easy translations passing the jQuery object to apply it to is
|
* secure fashion, to avoid XSS.
|
||||||
* more save, especially when they are loaded in the beginning.
|
* The second parameter is the message ID, matching the ones found in
|
||||||
|
* the translation files under the i18n directory.
|
||||||
|
* Any additional parameters will get inserted into the message ID in
|
||||||
|
* place of %s (strings) or %d (digits), applying the appropriate plural
|
||||||
|
* in case of digits. See also Helper.sprintf().
|
||||||
*
|
*
|
||||||
* @name I18n.translate
|
* @name I18n.translate
|
||||||
* @function
|
* @function
|
||||||
* @param {jQuery} $element - optional
|
* @param {jQuery} $element
|
||||||
* @param {string} messageId
|
* @param {string} messageId
|
||||||
* @param {...*} args - one or multiple parameters injected into placeholders
|
* @param {...*} args - one or multiple parameters injected into placeholders
|
||||||
* @return {string}
|
* @throws {string}
|
||||||
*/
|
*/
|
||||||
me.translate = function()
|
me.translate = function()
|
||||||
{
|
{
|
||||||
|
@ -573,6 +570,8 @@ jQuery.PrivateBin = (function($, RawDeflate) {
|
||||||
// optional jQuery element as first parameter
|
// optional jQuery element as first parameter
|
||||||
$element = args[0];
|
$element = args[0];
|
||||||
args.shift();
|
args.shift();
|
||||||
|
} else {
|
||||||
|
throw 'translation requires a jQuery element to be passed, for secure insertion of messages and to avoid double encoding of HTML entities';
|
||||||
}
|
}
|
||||||
|
|
||||||
// extract messageId from arguments
|
// extract messageId from arguments
|
||||||
|
@ -633,10 +632,10 @@ jQuery.PrivateBin = (function($, RawDeflate) {
|
||||||
let containsLinks = args[0].indexOf('<a') !== -1;
|
let containsLinks = args[0].indexOf('<a') !== -1;
|
||||||
|
|
||||||
// prevent double encoding, when we insert into a text node
|
// prevent double encoding, when we insert into a text node
|
||||||
if (!containsLinks || $element === null) {
|
if (!containsLinks) {
|
||||||
for (let i = 0; i < args.length; ++i) {
|
for (let i = 0; i < args.length; ++i) {
|
||||||
// parameters (i > 0) may never contain HTML as they may come from untrusted parties
|
// parameters (i > 0) may never contain HTML as they may come from untrusted parties
|
||||||
if (i > 0 || !containsLinks) {
|
if (i > 0) {
|
||||||
args[i] = Helper.htmlEntities(args[i]);
|
args[i] = Helper.htmlEntities(args[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -654,18 +653,37 @@ jQuery.PrivateBin = (function($, RawDeflate) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// if $element is given, insert translation
|
if (containsLinks) {
|
||||||
if ($element !== null) {
|
$element.html(output);
|
||||||
if (containsLinks) {
|
} else {
|
||||||
$element.html(output);
|
// text node takes care of entity encoding
|
||||||
} else {
|
$element.text(output);
|
||||||
// text node takes care of entity encoding
|
|
||||||
$element.text(output);
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return output;
|
/**
|
||||||
|
* translate a string, outputs the result
|
||||||
|
*
|
||||||
|
* This function is identical to I18n.translate, but doesn't require a
|
||||||
|
* jQuery element as the first parameter, instead it returns the
|
||||||
|
* translated message as string.
|
||||||
|
* Avoid using this function, if possible, as it may double encode your
|
||||||
|
* message's HTML entities. This is done to fail safe, preventing XSS.
|
||||||
|
*
|
||||||
|
* @name I18n.translate2string
|
||||||
|
* @function
|
||||||
|
* @param {string} messageId
|
||||||
|
* @param {...*} args - one or multiple parameters injected into placeholders
|
||||||
|
* @throws {string}
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
me.translate2string = function()
|
||||||
|
{
|
||||||
|
let args = Array.prototype.slice.call(arguments),
|
||||||
|
$element = $('<textarea>');
|
||||||
|
args.unshift($element);
|
||||||
|
me.translate.apply(this, args);
|
||||||
|
return $element.text();
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
149
js/test/I18n.js
149
js/test/I18n.js
|
@ -8,78 +8,6 @@ describe('I18n', function () {
|
||||||
$.PrivateBin.I18n.reset();
|
$.PrivateBin.I18n.reset();
|
||||||
});
|
});
|
||||||
|
|
||||||
jsc.property(
|
|
||||||
'returns message ID unchanged if no translation found',
|
|
||||||
'string',
|
|
||||||
function (messageId) {
|
|
||||||
messageId = messageId.replace(/%(s|d)/g, '%%');
|
|
||||||
var plurals = [messageId, messageId + 's'],
|
|
||||||
fake = [messageId],
|
|
||||||
result = $.PrivateBin.I18n.translate(messageId);
|
|
||||||
$.PrivateBin.I18n.reset();
|
|
||||||
|
|
||||||
var alias = $.PrivateBin.I18n._(messageId);
|
|
||||||
$.PrivateBin.I18n.reset();
|
|
||||||
|
|
||||||
var pluralResult = $.PrivateBin.I18n.translate(plurals);
|
|
||||||
$.PrivateBin.I18n.reset();
|
|
||||||
|
|
||||||
var pluralAlias = $.PrivateBin.I18n._(plurals);
|
|
||||||
$.PrivateBin.I18n.reset();
|
|
||||||
|
|
||||||
var fakeResult = $.PrivateBin.I18n.translate(fake);
|
|
||||||
$.PrivateBin.I18n.reset();
|
|
||||||
|
|
||||||
var fakeAlias = $.PrivateBin.I18n._(fake);
|
|
||||||
$.PrivateBin.I18n.reset();
|
|
||||||
|
|
||||||
messageId = $.PrivateBin.Helper.htmlEntities(messageId);
|
|
||||||
return messageId === result && messageId === alias &&
|
|
||||||
messageId === pluralResult && messageId === pluralAlias &&
|
|
||||||
messageId === fakeResult && messageId === fakeAlias;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
jsc.property(
|
|
||||||
'replaces %s in strings with first given parameter, encoding all, when no link is in the messageID',
|
|
||||||
'string',
|
|
||||||
'(small nearray) string',
|
|
||||||
'string',
|
|
||||||
function (prefix, params, postfix) {
|
|
||||||
prefix = prefix.replace(/%(s|d)/g, '%%');
|
|
||||||
params[0] = params[0].replace(/%(s|d)/g, '%%').replace(/<a/g, '');
|
|
||||||
postfix = postfix.replace(/%(s|d)/g, '%%');
|
|
||||||
const translation = $.PrivateBin.Helper.htmlEntities(prefix + params[0] + postfix);
|
|
||||||
params.unshift(prefix + '%s' + postfix);
|
|
||||||
const result = $.PrivateBin.I18n.translate.apply(this, params);
|
|
||||||
$.PrivateBin.I18n.reset();
|
|
||||||
const alias = $.PrivateBin.I18n._.apply(this, params);
|
|
||||||
$.PrivateBin.I18n.reset();
|
|
||||||
return translation === result && translation === alias;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
jsc.property(
|
|
||||||
'replaces %s in strings with first given parameter, encoding params only, when a link is part of the messageID',
|
|
||||||
'string',
|
|
||||||
'(small nearray) string',
|
|
||||||
'string',
|
|
||||||
function (prefix, params, postfix) {
|
|
||||||
prefix = prefix.replace(/%(s|d)/g, '%%');
|
|
||||||
params[0] = params[0].replace(/%(s|d)/g, '%%');
|
|
||||||
postfix = postfix.replace(/%(s|d)/g, '%%');
|
|
||||||
const translation = DOMPurify.sanitize(
|
|
||||||
prefix + $.PrivateBin.Helper.htmlEntities(params[0]) + '<a></a>' + postfix, {
|
|
||||||
ALLOWED_TAGS: ['a', 'br', 'i', 'span'],
|
|
||||||
ALLOWED_ATTR: ['href', 'id']
|
|
||||||
}
|
|
||||||
);
|
|
||||||
params.unshift(prefix + '%s<a></a>' + postfix);
|
|
||||||
const result = $.PrivateBin.I18n.translate.apply(this, params);
|
|
||||||
$.PrivateBin.I18n.reset();
|
|
||||||
const alias = $.PrivateBin.I18n._.apply(this, params);
|
|
||||||
$.PrivateBin.I18n.reset();
|
|
||||||
return translation === result && translation === alias;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
jsc.property(
|
jsc.property(
|
||||||
'replaces %s in strings with first given parameter into an element, encoding all, when no link is in the messageID',
|
'replaces %s in strings with first given parameter into an element, encoding all, when no link is in the messageID',
|
||||||
'string',
|
'string',
|
||||||
|
@ -144,6 +72,73 @@ describe('I18n', function () {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('translate2string', function () {
|
||||||
|
this.timeout(30000);
|
||||||
|
before(function () {
|
||||||
|
$.PrivateBin.I18n.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
jsc.property(
|
||||||
|
'returns message ID unchanged if no translation found',
|
||||||
|
'string',
|
||||||
|
function (messageId) {
|
||||||
|
messageId = messageId.replace(/%(s|d)/g, '%%');
|
||||||
|
let plurals = [messageId, messageId + 's'],
|
||||||
|
fake = [messageId],
|
||||||
|
clean = jsdom(),
|
||||||
|
result = $.PrivateBin.I18n.translate2string(messageId);
|
||||||
|
$.PrivateBin.I18n.reset();
|
||||||
|
|
||||||
|
var pluralResult = $.PrivateBin.I18n.translate2string(plurals);
|
||||||
|
$.PrivateBin.I18n.reset();
|
||||||
|
|
||||||
|
var fakeResult = $.PrivateBin.I18n.translate2string(fake);
|
||||||
|
$.PrivateBin.I18n.reset();
|
||||||
|
|
||||||
|
clean();
|
||||||
|
messageId = $.PrivateBin.Helper.htmlEntities(messageId);
|
||||||
|
return messageId === result && messageId === pluralResult && messageId === fakeResult;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
jsc.property(
|
||||||
|
'replaces %s in strings with first given parameter, encoding all, when no link is in the messageID',
|
||||||
|
'string',
|
||||||
|
'(small nearray) string',
|
||||||
|
'string',
|
||||||
|
function (prefix, params, postfix) {
|
||||||
|
prefix = prefix.replace(/%(s|d)/g, '%%');
|
||||||
|
params[0] = params[0].replace(/%(s|d)/g, '%%').replace(/<a/g, '');
|
||||||
|
postfix = postfix.replace(/%(s|d)/g, '%%');
|
||||||
|
const translation = $.PrivateBin.Helper.htmlEntities(prefix + params[0] + postfix);
|
||||||
|
params.unshift(prefix + '%s' + postfix);
|
||||||
|
const result = $.PrivateBin.I18n.translate2string.apply(this, params);
|
||||||
|
$.PrivateBin.I18n.reset();
|
||||||
|
return translation === result;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
jsc.property(
|
||||||
|
'replaces %s in strings with first given parameter, encoding params only, when a link is part of the messageID',
|
||||||
|
'string',
|
||||||
|
'(small nearray) string',
|
||||||
|
'string',
|
||||||
|
function (prefix, params, postfix) {
|
||||||
|
prefix = prefix.replace(/%(s|d)/g, '%%');
|
||||||
|
params[0] = params[0].replace(/%(s|d)/g, '%%');
|
||||||
|
postfix = postfix.replace(/%(s|d)/g, '%%');
|
||||||
|
const translation = DOMPurify.sanitize(
|
||||||
|
prefix + $.PrivateBin.Helper.htmlEntities(params[0]) + '<a></a>' + postfix, {
|
||||||
|
ALLOWED_TAGS: ['a', 'br', 'i', 'span'],
|
||||||
|
ALLOWED_ATTR: ['href', 'id']
|
||||||
|
}
|
||||||
|
);
|
||||||
|
params.unshift(prefix + '%s<a></a>' + postfix);
|
||||||
|
const result = $.PrivateBin.I18n.translate2string.apply(this, params);
|
||||||
|
$.PrivateBin.I18n.reset();
|
||||||
|
return translation === result;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
describe('getPluralForm', function () {
|
describe('getPluralForm', function () {
|
||||||
before(function () {
|
before(function () {
|
||||||
$.PrivateBin.I18n.reset();
|
$.PrivateBin.I18n.reset();
|
||||||
|
@ -183,10 +178,9 @@ describe('I18n', function () {
|
||||||
// mock
|
// mock
|
||||||
clean = jsdom('', {cookie: ['lang=' + language]});
|
clean = jsdom('', {cookie: ['lang=' + language]});
|
||||||
$.PrivateBin.I18n.reset(language, require('../../i18n/' + language + '.json'));
|
$.PrivateBin.I18n.reset(language, require('../../i18n/' + language + '.json'));
|
||||||
var result = $.PrivateBin.I18n.translate('en'),
|
var result = $.PrivateBin.I18n.translate2string('en');
|
||||||
alias = $.PrivateBin.I18n._('en');
|
|
||||||
clean();
|
clean();
|
||||||
return language === result && language === alias;
|
return language === result;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -206,11 +200,10 @@ describe('I18n', function () {
|
||||||
|
|
||||||
$.PrivateBin.I18n.reset('en');
|
$.PrivateBin.I18n.reset('en');
|
||||||
$.PrivateBin.I18n.loadTranslations();
|
$.PrivateBin.I18n.loadTranslations();
|
||||||
var result = $.PrivateBin.I18n.translate('en'),
|
var result = $.PrivateBin.I18n.translate2string('en');
|
||||||
alias = $.PrivateBin.I18n._('en');
|
|
||||||
|
|
||||||
clean();
|
clean();
|
||||||
return 'en' === result && 'en' === alias;
|
return 'en' === result;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -72,7 +72,7 @@ endif;
|
||||||
?>
|
?>
|
||||||
<script type="text/javascript" data-cfasync="false" src="js/purify-2.0.7.js" integrity="sha512-XjNEK1xwh7SJ/7FouwV4VZcGW9cMySL3SwNpXgrURLBcXXQYtZdqhGoNdEwx9vwLvFjUGDQVNgpOrTsXlSTiQg==" crossorigin="anonymous"></script>
|
<script type="text/javascript" data-cfasync="false" src="js/purify-2.0.7.js" integrity="sha512-XjNEK1xwh7SJ/7FouwV4VZcGW9cMySL3SwNpXgrURLBcXXQYtZdqhGoNdEwx9vwLvFjUGDQVNgpOrTsXlSTiQg==" crossorigin="anonymous"></script>
|
||||||
<script type="text/javascript" data-cfasync="false" src="js/legacy.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-LYos+qXHIRqFf5ZPNphvtTB0cgzHUizu2wwcOwcwz/VIpRv9lpcBgPYz4uq6jx0INwCAj6Fbnl5HoKiLufS2jg==" crossorigin="anonymous"></script>
|
<script type="text/javascript" data-cfasync="false" src="js/legacy.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-LYos+qXHIRqFf5ZPNphvtTB0cgzHUizu2wwcOwcwz/VIpRv9lpcBgPYz4uq6jx0INwCAj6Fbnl5HoKiLufS2jg==" crossorigin="anonymous"></script>
|
||||||
<script type="text/javascript" data-cfasync="false" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-U9Au7V0FSY8S1xI6MrhPawEOFAPFejMI8PYlQNhC++XIQCQgQhYEqTYkhczN6F2MFAq/P1Hwn9A3IWaq9hu95g==" crossorigin="anonymous"></script>
|
<script type="text/javascript" data-cfasync="false" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-Pss+4+Yrpp5ZROFKS9VWpX13RUpVp2QXDNZtFzNrr7YV65D+iKKUeE1z/Sff887+3fyOyIhJwKMsdfnmXnaNkQ==" crossorigin="anonymous"></script>
|
||||||
<link rel="apple-touch-icon" href="img/apple-touch-icon.png?<?php echo rawurlencode($VERSION); ?>" sizes="180x180" />
|
<link rel="apple-touch-icon" href="img/apple-touch-icon.png?<?php echo rawurlencode($VERSION); ?>" sizes="180x180" />
|
||||||
<link rel="icon" type="image/png" href="img/favicon-32x32.png?<?php echo rawurlencode($VERSION); ?>" sizes="32x32" />
|
<link rel="icon" type="image/png" href="img/favicon-32x32.png?<?php echo rawurlencode($VERSION); ?>" sizes="32x32" />
|
||||||
<link rel="icon" type="image/png" href="img/favicon-16x16.png?<?php echo rawurlencode($VERSION); ?>" sizes="16x16" />
|
<link rel="icon" type="image/png" href="img/favicon-16x16.png?<?php echo rawurlencode($VERSION); ?>" sizes="16x16" />
|
||||||
|
|
|
@ -50,7 +50,7 @@ endif;
|
||||||
?>
|
?>
|
||||||
<script type="text/javascript" data-cfasync="false" src="js/purify-2.0.7.js" integrity="sha512-XjNEK1xwh7SJ/7FouwV4VZcGW9cMySL3SwNpXgrURLBcXXQYtZdqhGoNdEwx9vwLvFjUGDQVNgpOrTsXlSTiQg==" crossorigin="anonymous"></script>
|
<script type="text/javascript" data-cfasync="false" src="js/purify-2.0.7.js" integrity="sha512-XjNEK1xwh7SJ/7FouwV4VZcGW9cMySL3SwNpXgrURLBcXXQYtZdqhGoNdEwx9vwLvFjUGDQVNgpOrTsXlSTiQg==" crossorigin="anonymous"></script>
|
||||||
<script type="text/javascript" data-cfasync="false" src="js/legacy.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-LYos+qXHIRqFf5ZPNphvtTB0cgzHUizu2wwcOwcwz/VIpRv9lpcBgPYz4uq6jx0INwCAj6Fbnl5HoKiLufS2jg==" crossorigin="anonymous"></script>
|
<script type="text/javascript" data-cfasync="false" src="js/legacy.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-LYos+qXHIRqFf5ZPNphvtTB0cgzHUizu2wwcOwcwz/VIpRv9lpcBgPYz4uq6jx0INwCAj6Fbnl5HoKiLufS2jg==" crossorigin="anonymous"></script>
|
||||||
<script type="text/javascript" data-cfasync="false" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-U9Au7V0FSY8S1xI6MrhPawEOFAPFejMI8PYlQNhC++XIQCQgQhYEqTYkhczN6F2MFAq/P1Hwn9A3IWaq9hu95g==" crossorigin="anonymous"></script>
|
<script type="text/javascript" data-cfasync="false" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-Pss+4+Yrpp5ZROFKS9VWpX13RUpVp2QXDNZtFzNrr7YV65D+iKKUeE1z/Sff887+3fyOyIhJwKMsdfnmXnaNkQ==" crossorigin="anonymous"></script>
|
||||||
<link rel="apple-touch-icon" href="img/apple-touch-icon.png?<?php echo rawurlencode($VERSION); ?>" sizes="180x180" />
|
<link rel="apple-touch-icon" href="img/apple-touch-icon.png?<?php echo rawurlencode($VERSION); ?>" sizes="180x180" />
|
||||||
<link rel="icon" type="image/png" href="img/favicon-32x32.png?<?php echo rawurlencode($VERSION); ?>" sizes="32x32" />
|
<link rel="icon" type="image/png" href="img/favicon-32x32.png?<?php echo rawurlencode($VERSION); ?>" sizes="32x32" />
|
||||||
<link rel="icon" type="image/png" href="img/favicon-16x16.png?<?php echo rawurlencode($VERSION); ?>" sizes="16x16" />
|
<link rel="icon" type="image/png" href="img/favicon-16x16.png?<?php echo rawurlencode($VERSION); ?>" sizes="16x16" />
|
||||||
|
|
Loading…
Reference in a new issue