Merge branch 'displayEncoding'

This commit is contained in:
El RIDO 2020-02-01 07:53:32 +01:00
commit f940f17bba
No known key found for this signature in database
GPG Key ID: 0F5C940A6BD81F92
6 changed files with 198 additions and 88 deletions

View File

@ -36,7 +36,7 @@ var a2zString = ['a','b','c','d','e','f','g','h','i','j','k','l','m',
return c.toUpperCase(); return c.toUpperCase();
}) })
), ),
schemas = ['ftp','gopher','http','https','ws','wss'], schemas = ['ftp','http','https'],
supportedLanguages = ['de', 'es', 'fr', 'it', 'no', 'pl', 'pt', 'oc', 'ru', 'sl', 'zh'], supportedLanguages = ['de', 'es', 'fr', 'it', 'no', 'pl', 'pt', 'oc', 'ru', 'sl', 'zh'],
mimeTypes = ['image/png', 'application/octet-stream'], mimeTypes = ['image/png', 'application/octet-stream'],
formats = ['plaintext', 'markdown', 'syntaxhighlighting'], formats = ['plaintext', 'markdown', 'syntaxhighlighting'],

View File

@ -189,6 +189,26 @@ jQuery.PrivateBin = (function($, RawDeflate) {
const Helper = (function () { const Helper = (function () {
const me = {}; const me = {};
/**
* character to HTML entity lookup table
*
* @see {@link https://github.com/janl/mustache.js/blob/master/mustache.js#L60}
* @name Helper.entityMap
* @private
* @enum {Object}
* @readonly
*/
var entityMap = {
'&': '&',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;',
'/': '&#x2F;',
'`': '&#x60;',
'=': '&#x3D;'
};
/** /**
* cache for script location * cache for script location
* *
@ -302,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;
@ -392,6 +405,23 @@ jQuery.PrivateBin = (function($, RawDeflate) {
return new Comment(data); return new Comment(data);
}; };
/**
* convert all applicable characters to HTML entities
*
* @see {@link https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html}
* @name Helper.htmlEntities
* @function
* @param {string} str
* @return {string} escaped HTML
*/
me.htmlEntities = function(str) {
return String(str).replace(
/[&<>"'`=\/]/g, function(s) {
return entityMap[s];
}
);
}
/** /**
* resets state, used for unit testing * resets state, used for unit testing
* *
@ -442,32 +472,6 @@ jQuery.PrivateBin = (function($, RawDeflate) {
return expirationDate; return expirationDate;
}; };
/**
* encode all applicable characters to HTML entities
*
* @see {@link https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html}
*
* @name Helper.htmlEntities
* @function
* @param string str
* @return string escaped HTML
*/
me.htmlEntities = function(str) {
// using textarea, since other tags may allow and execute scripts, even when detached from DOM
let holder = document.createElement('textarea');
holder.textContent = str;
// as per OWASP recommendation, also encoding quotes and slash
return holder.innerHTML.replace(
/["'\/]/g,
function(s) {
return {
'"': '&quot;',
"'": '&#x27;',
'/': '&#x2F;'
}[s];
});
};
return me; return me;
})(); })();
@ -538,10 +542,14 @@ jQuery.PrivateBin = (function($, RawDeflate) {
* *
* Optionally pass a jQuery element as the first parameter, to automatically * Optionally pass a jQuery element as the first parameter, to automatically
* let the text of this element be replaced. In case the (asynchronously * let 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 downloaded yet, this will make sure the string
* is replaced when it is actually loaded. * is replaced when it eventually gets loaded. Using this is both simpler
* So for easy translations passing the jQuery object to apply it to is * and more secure, as it avoids potential XSS when inserting text.
* more save, especially when they are loaded in the beginning. * The next 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
@ -619,31 +627,39 @@ jQuery.PrivateBin = (function($, RawDeflate) {
} }
// messageID may contain links, but should be from a trusted source (code or translation JSON files) // messageID may contain links, but should be from a trusted source (code or translation JSON files)
let containsNoLinks = args[0].indexOf('<a') === -1; let containsLinks = args[0].indexOf('<a') !== -1;
for (let i = 0; i < args.length; ++i) {
// parameters (i > 0) may never contain HTML as they may come from untrusted parties // prevent double encoding, when we insert into a text node
if (i > 0 || containsNoLinks) { if (containsLinks || $element === null) {
args[i] = Helper.htmlEntities(args[i]); for (let i = 0; i < args.length; ++i) {
// parameters (i > 0) may never contain HTML as they may come from untrusted parties
if (i > 0 || !containsLinks) {
args[i] = Helper.htmlEntities(args[i]);
}
} }
} }
// format string // format string
let output = Helper.sprintf.apply(this, args); let output = Helper.sprintf.apply(this, args);
// if $element is given, apply text to element if (containsLinks) {
// only allow tags/attributes we actually use in translations
output = DOMPurify.sanitize(
output, {
ALLOWED_TAGS: ['a', 'br', 'i', 'span'],
ALLOWED_ATTR: ['href', 'id']
}
);
}
// if $element is given, insert translation
if ($element !== null) { if ($element !== null) {
if (containsNoLinks) { if (containsLinks) {
// avoid HTML entity encoding if translation contains links $element.html(output);
$element.text(output);
} else { } else {
// only allow tags/attributes we actually use in our translations // text node takes care of entity encoding
$element.html( $element.text(output);
DOMPurify.sanitize(output, {
ALLOWED_TAGS: ['a', 'br', 'i', 'span'],
ALLOWED_ATTR: ['href', 'id']
})
);
} }
return '';
} }
return output; return output;
@ -1876,11 +1892,10 @@ jQuery.PrivateBin = (function($, RawDeflate) {
return a.length - b.length; return a.length - b.length;
})[0]; })[0];
if (typeof shortUrl === 'string' && shortUrl.length > 0) { if (typeof shortUrl === 'string' && shortUrl.length > 0) {
$('#pastelink').html( I18n._(
I18n._( $('#pastelink'),
'Your paste is <a id="pasteurl" href="%s">%s</a> <span id="copyhint">(Hit [Ctrl]+[c] to copy)</span>', 'Your paste is <a id="pasteurl" href="%s">%s</a> <span id="copyhint">(Hit [Ctrl]+[c] to copy)</span>',
shortUrl, shortUrl shortUrl, shortUrl
)
); );
// we disable the button to avoid calling shortener again // we disable the button to avoid calling shortener again
$shortenButton.addClass('buttondisabled'); $shortenButton.addClass('buttondisabled');
@ -1935,11 +1950,10 @@ jQuery.PrivateBin = (function($, RawDeflate) {
*/ */
me.createPasteNotification = function(url, deleteUrl) me.createPasteNotification = function(url, deleteUrl)
{ {
$('#pastelink').html( I18n._(
I18n._( $('#pastelink'),
'Your paste is <a id="pasteurl" href="%s">%s</a> <span id="copyhint">(Hit [Ctrl]+[c] to copy)</span>', 'Your paste is <a id="pasteurl" href="%s">%s</a> <span id="copyhint">(Hit [Ctrl]+[c] to copy)</span>',
url, url url, url
)
); );
// save newly created element // save newly created element
$pasteUrl = $('#pasteurl'); $pasteUrl = $('#pasteurl');
@ -1947,7 +1961,8 @@ jQuery.PrivateBin = (function($, RawDeflate) {
$pasteUrl.click(pasteLinkClick); $pasteUrl.click(pasteLinkClick);
// delete link // delete link
$('#deletelink').html('<a href="' + deleteUrl + '">' + I18n._('Delete data') + '</a>'); $('#deletelink').html('<a href="' + deleteUrl + '"></a>');
I18n._($('#deletelink a').first(), 'Delete data');
// enable shortener button // enable shortener button
$shortenButton.removeClass('buttondisabled'); $shortenButton.removeClass('buttondisabled');
@ -3710,8 +3725,9 @@ jQuery.PrivateBin = (function($, RawDeflate) {
const $emailconfirmmodal = $('#emailconfirmmodal'); const $emailconfirmmodal = $('#emailconfirmmodal');
if ($emailconfirmmodal.length > 0) { if ($emailconfirmmodal.length > 0) {
if (expirationDate !== null) { if (expirationDate !== null) {
$emailconfirmmodal.find('#emailconfirm-display').text( I18n._(
I18n._('Recipient may become aware of your timezone, convert time to UTC?') $emailconfirmmodal.find('#emailconfirm-display'),
'Recipient may become aware of your timezone, convert time to UTC?'
); );
const $emailconfirmTimezoneCurrent = $emailconfirmmodal.find('#emailconfirm-timezone-current'); const $emailconfirmTimezoneCurrent = $emailconfirmmodal.find('#emailconfirm-timezone-current');
const $emailconfirmTimezoneUtc = $emailconfirmmodal.find('#emailconfirm-timezone-utc'); const $emailconfirmTimezoneUtc = $emailconfirmmodal.find('#emailconfirm-timezone-utc');
@ -3911,9 +3927,7 @@ jQuery.PrivateBin = (function($, RawDeflate) {
}); });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
Alert.showError( Alert.showError('Cannot calculate expiration date.');
I18n._('Cannot calculate expiration date.')
);
} }
} }

View File

@ -86,9 +86,9 @@ describe('AttachmentViewer', function () {
$.PrivateBin.AttachmentViewer.moveAttachmentTo(element, prefix + '%s' + postfix); $.PrivateBin.AttachmentViewer.moveAttachmentTo(element, prefix + '%s' + postfix);
// messageIDs with links get a relaxed treatment // messageIDs with links get a relaxed treatment
if (prefix.indexOf('<a') === -1 && postfix.indexOf('<a') === -1) { if (prefix.indexOf('<a') === -1 && postfix.indexOf('<a') === -1) {
result = $.PrivateBin.Helper.htmlEntities(prefix + filename + postfix); result = $('<textarea>').text((prefix + filename + postfix)).text();
} else { } else {
result = $('<div>').html(prefix + $.PrivateBin.Helper.htmlEntities(filename) + postfix).html(); result = prefix + $.PrivateBin.Helper.htmlEntities(filename) + postfix;
} }
if (filename.length) { if (filename.length) {
results.push( results.push(

View File

@ -3,6 +3,7 @@ var common = require('../common');
describe('I18n', function () { describe('I18n', function () {
describe('translate', function () { describe('translate', function () {
this.timeout(30000);
before(function () { before(function () {
$.PrivateBin.I18n.reset(); $.PrivateBin.I18n.reset();
}); });
@ -32,14 +33,41 @@ describe('I18n', function () {
var fakeAlias = $.PrivateBin.I18n._(fake); var fakeAlias = $.PrivateBin.I18n._(fake);
$.PrivateBin.I18n.reset(); $.PrivateBin.I18n.reset();
messageId = $.PrivateBin.Helper.htmlEntities(messageId); if (messageId.indexOf('<a') === -1) {
messageId = $.PrivateBin.Helper.htmlEntities(messageId);
} else {
messageId = DOMPurify.sanitize(
messageId, {
ALLOWED_TAGS: ['a', 'br', 'i', 'span'],
ALLOWED_ATTR: ['href', 'id']
}
);
}
return messageId === result && messageId === alias && return messageId === result && messageId === alias &&
messageId === pluralResult && messageId === pluralAlias && messageId === pluralResult && messageId === pluralAlias &&
messageId === fakeResult && messageId === fakeAlias; messageId === fakeResult && messageId === fakeAlias;
} }
); );
jsc.property( jsc.property(
'replaces %s in strings with first given parameter', '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, '%%').replace(/<a/g, '');
params[0] = params[0].replace(/%(s|d)/g, '%%');
postfix = postfix.replace(/%(s|d)/g, '%%').replace(/<a/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', 'string',
'(small nearray) string', '(small nearray) string',
'string', 'string',
@ -47,15 +75,83 @@ describe('I18n', function () {
prefix = prefix.replace(/%(s|d)/g, '%%'); prefix = prefix.replace(/%(s|d)/g, '%%');
params[0] = params[0].replace(/%(s|d)/g, '%%'); params[0] = params[0].replace(/%(s|d)/g, '%%');
postfix = postfix.replace(/%(s|d)/g, '%%'); postfix = postfix.replace(/%(s|d)/g, '%%');
var translation = $.PrivateBin.Helper.htmlEntities(prefix + params[0] + postfix); const translation = DOMPurify.sanitize(
params.unshift(prefix + '%s' + postfix); prefix + $.PrivateBin.Helper.htmlEntities(params[0]) + '<a></a>' + postfix, {
var result = $.PrivateBin.I18n.translate.apply(this, params); 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(); $.PrivateBin.I18n.reset();
var alias = $.PrivateBin.I18n._.apply(this, params); const alias = $.PrivateBin.I18n._.apply(this, params);
$.PrivateBin.I18n.reset(); $.PrivateBin.I18n.reset();
return translation === result && translation === alias; return translation === result && translation === alias;
} }
); );
jsc.property(
'replaces %s in strings with first given parameter into an element, encoding all, when no link is in the messageID',
'string',
'(small nearray) string',
'string',
function (prefix, params, postfix) {
prefix = prefix.replace(/%(s|d)/g, '%%').replace(/<a/g, '');
params[0] = params[0].replace(/%(s|d)/g, '%%');
postfix = postfix.replace(/%(s|d)/g, '%%').replace(/<a/g, '');
const translation = $('<textarea>').text((prefix + params[0] + postfix)).text();
let args = Array.prototype.slice.call(params);
args.unshift(prefix + '%s' + postfix);
let clean = jsdom();
$('body').html('<div id="i18n"></div>');
args.unshift($('#i18n'));
$.PrivateBin.I18n.translate.apply(this, args);
const result = $('#i18n').text();
$.PrivateBin.I18n.reset();
clean();
clean = jsdom();
$('body').html('<div id="i18n"></div>');
args[0] = $('#i18n');
$.PrivateBin.I18n._.apply(this, args);
const alias = $('#i18n').text();
$.PrivateBin.I18n.reset();
clean();
return translation === result && translation === alias;
}
);
jsc.property(
'replaces %s in strings with first given parameter into an element, encoding params only, when a link is part of the messageID inserted',
'string',
'(small nearray) string',
'string',
function (prefix, params, postfix) {
prefix = prefix.replace(/%(s|d)/g, '%%').trim();
params[0] = params[0].replace(/%(s|d)/g, '%%').trim();
postfix = postfix.replace(/%(s|d)/g, '%%').trim();
const translation = DOMPurify.sanitize(
prefix + $.PrivateBin.Helper.htmlEntities(params[0]) + '<a></a>' + postfix, {
ALLOWED_TAGS: ['a', 'br', 'i', 'span'],
ALLOWED_ATTR: ['href', 'id']
}
);
let args = Array.prototype.slice.call(params);
args.unshift(prefix + '%s<a></a>' + postfix);
let clean = jsdom();
$('body').html('<div id="i18n"></div>');
args.unshift($('#i18n'));
$.PrivateBin.I18n.translate.apply(this, args);
const result = $('#i18n').html();
$.PrivateBin.I18n.reset();
clean();
clean = jsdom();
$('body').html('<div id="i18n"></div>');
args[0] = $('#i18n');
$.PrivateBin.I18n._.apply(this, args);
const alias = $('#i18n').html();
$.PrivateBin.I18n.reset();
clean();
return translation === result && translation === alias;
}
);
}); });
describe('getPluralForm', function () { describe('getPluralForm', function () {

View File

@ -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-RJwTFnQikma3rIso3dda8anhgsoFT7dZdARUw9m5/9Gl9A+U3B3H7LEjuLRFmC0rV86SfuXNnKwILAbF96Hr/w==" crossorigin="anonymous"></script> <script type="text/javascript" data-cfasync="false" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-Q7yHFlVuPYWw/SJFiMv83PPVwGKqBwoqZhNtHAwkTIxocS6Zpqyj1I0/nUCRWv15xuurctViB3lSVs6s+7f0jw==" 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" />
@ -517,8 +517,8 @@ endif;
?> ?>
</div> </div>
<ul id="editorTabs" class="nav nav-tabs hidden"> <ul id="editorTabs" class="nav nav-tabs hidden">
<li role="presentation" class="active"><a role="tab" aria-selected="true" aria-controls="editorTabs" id="messageedit" href="#"><?php echo I18n::_('Editor'); ?></a></li> <li role="presentation" class="active"><a id="messageedit" href="#"><?php echo I18n::_('Editor'); ?></a></li>
<li role="presentation"><a role="tab" aria-selected="false" aria-controls="editorTabs" id="messagepreview" href="#"><?php echo I18n::_('Preview'); ?></a></li> <li role="presentation"><a id="messagepreview" href="#"><?php echo I18n::_('Preview'); ?></a></li>
<li role="presentation" class="pull-right"> <li role="presentation" class="pull-right">
<?php <?php
if ($isPage): if ($isPage):

View File

@ -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-RJwTFnQikma3rIso3dda8anhgsoFT7dZdARUw9m5/9Gl9A+U3B3H7LEjuLRFmC0rV86SfuXNnKwILAbF96Hr/w==" crossorigin="anonymous"></script> <script type="text/javascript" data-cfasync="false" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-Q7yHFlVuPYWw/SJFiMv83PPVwGKqBwoqZhNtHAwkTIxocS6Zpqyj1I0/nUCRWv15xuurctViB3lSVs6s+7f0jw==" 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" />