Просмотр исходного кода

Merge branch 'displayEncoding2'

El RIDO 6 лет назад
Родитель
Сommit
d7fd6667fd
22 измененных файлов с 81 добавлено и 48 удалено
  1. 2 2
      i18n/bg.json
  2. 2 2
      i18n/cs.json
  3. 2 2
      i18n/de.json
  4. 2 2
      i18n/es.json
  5. 2 2
      i18n/fr.json
  6. 1 1
      i18n/hu.json
  7. 2 2
      i18n/it.json
  8. 2 2
      i18n/nl.json
  9. 2 2
      i18n/no.json
  10. 2 2
      i18n/oc.json
  11. 1 1
      i18n/pl.json
  12. 2 2
      i18n/pt.json
  13. 2 2
      i18n/ru.json
  14. 2 2
      i18n/sl.json
  15. 2 2
      i18n/uk.json
  16. 2 2
      i18n/zh.json
  17. 1 1
      js/privatebin.js
  18. 3 3
      js/test/I18n.js
  19. 22 0
      lib/I18n.php
  20. 7 7
      tpl/bootstrap.php
  21. 7 7
      tpl/page.php
  22. 11 0
      tst/I18nTest.php

+ 2 - 2
i18n/bg.json

@@ -31,8 +31,8 @@
         "Невалиден код за изтриване. Информацията Ви не беше изтрита.",
     "Paste was properly deleted.":
         "Информацията Ви е изтрита.",
-    "JavaScript is required for %s to work.<br />Sorry for the inconvenience.":
-        "Услугата %s се нуждае от JavaScript, за да работи.<br />Съжаляваме за неудобството.",
+    "JavaScript is required for %s to work. Sorry for the inconvenience.":
+        "Услугата %s се нуждае от JavaScript, за да работи. Съжаляваме за неудобството.",
     "%s requires a modern browser to work.":
         "%s се нуждае от съвременен браузър за да работи.",
     "New":

+ 2 - 2
i18n/cs.json

@@ -31,8 +31,8 @@
         "Wrong deletion token. Paste was not deleted.",
     "Paste was properly deleted.":
         "Paste was properly deleted.",
-    "JavaScript is required for %s to work.<br />Sorry for the inconvenience.":
-        "JavaScript is required for %s to work.<br />Sorry for the inconvenience.",
+    "JavaScript is required for %s to work. Sorry for the inconvenience.":
+        "JavaScript is required for %s to work. Sorry for the inconvenience.",
     "%s requires a modern browser to work.":
         "%%s requires a modern browser to work.",
     "New":

+ 2 - 2
i18n/de.json

@@ -31,8 +31,8 @@
         "Falscher Lösch-Code. Text wurde nicht gelöscht.",
     "Paste was properly deleted.":
         "Text wurde erfolgreich gelöscht.",
-    "JavaScript is required for %s to work.<br />Sorry for the inconvenience.":
-        "JavaScript ist eine Voraussetzung, um %s zu nutzen.<br />Bitte entschuldige die Unannehmlichkeiten.",
+    "JavaScript is required for %s to work. Sorry for the inconvenience.":
+        "JavaScript ist eine Voraussetzung, um %s zu nutzen. Bitte entschuldige die Unannehmlichkeiten.",
     "%s requires a modern browser to work.":
         "%s setzt einen modernen Browser voraus, um funktionieren zu können.",
     "New":

+ 2 - 2
i18n/es.json

@@ -31,8 +31,8 @@
         "Token de eliminación erróneo. El \"paste\" no fue eliminado.",
     "Paste was properly deleted.":
         "El \"paste\" se ha eliminado correctamente.",
-    "JavaScript is required for %s to work.<br />Sorry for the inconvenience.":
-        "JavaScript es necesario para que %s funcione.<br />Sentimos los inconvenientes ocasionados.",
+    "JavaScript is required for %s to work. Sorry for the inconvenience.":
+        "JavaScript es necesario para que %s funcione. Sentimos los inconvenientes ocasionados.",
     "%s requires a modern browser to work.":
         "%s requiere un navegador moderno para funcionar.",
     "New":

+ 2 - 2
i18n/fr.json

@@ -31,8 +31,8 @@
         "Jeton de suppression incorrect. Le paste n'a pas été supprimé.",
     "Paste was properly deleted.":
         "Le paste a été correctement supprimé.",
-    "JavaScript is required for %s to work.<br />Sorry for the inconvenience.":
-        "JavaScript est requis pour faire fonctionner %s. <br />Désolé pour cet inconvénient.",
+    "JavaScript is required for %s to work. Sorry for the inconvenience.":
+        "JavaScript est requis pour faire fonctionner %s. Désolé pour cet inconvénient.",
     "%s requires a modern browser to work.":
         "%s nécessite un navigateur moderne pour fonctionner.",
     "New":

+ 1 - 1
i18n/hu.json

@@ -31,7 +31,7 @@
         "Hibás törlési azonosító. A bejegyzés nem lett törölve.",
     "Paste was properly deleted.":
         "A bejegyzés sikeresen törölve.",
-    "JavaScript is required for %s to work.<br />Sorry for the inconvenience.":
+    "JavaScript is required for %s to work. Sorry for the inconvenience.":
         "JavaScript szükséges a %s működéséhez. Elnézést a fennakadásért.",
     "%s requires a modern browser to work.":
         "A %s működéséhez a jelenleginél újabb böngészőre van szükség.",

+ 2 - 2
i18n/it.json

@@ -31,8 +31,8 @@
         "Codice cancellazione errato. Il messaggio NON è stato cancellato.",
     "Paste was properly deleted.":
         "Il messaggio è stato correttamente cancellato.",
-    "JavaScript is required for %s to work.<br />Sorry for the inconvenience.":
-        "%s funziona solo con JavaScript attivo.<br />Ci dispiace per l'inconveniente.",
+    "JavaScript is required for %s to work. Sorry for the inconvenience.":
+        "%s funziona solo con JavaScript attivo. Ci dispiace per l'inconveniente.",
     "%s requires a modern browser to work.":
         "%s richiede un browser moderno e aggiornato per funzionare.",
     "New":

+ 2 - 2
i18n/nl.json

@@ -31,8 +31,8 @@
         "Foutieve verwijdercode. Geplakte tekst is niet verwijderd.",
     "Paste was properly deleted.":
         "Geplakte tekst is correct verwijderd.",
-    "JavaScript is required for %s to work.<br />Sorry for the inconvenience.":
-        "JavaScript vereist om %s te laten werken.<br />Sorry voor het ongemak.",
+    "JavaScript is required for %s to work. Sorry for the inconvenience.":
+        "JavaScript vereist om %s te laten werken. Sorry voor het ongemak.",
     "%s requires a modern browser to work.":
         "%s vereist een moderne browser om te kunnen werken ",
     "New":

+ 2 - 2
i18n/no.json

@@ -31,8 +31,8 @@
         "Feil slettingsnøkkel. Innlegg ble ikke fjernet.",
     "Paste was properly deleted.":
         "Innlegget er slettet.",
-    "JavaScript is required for %s to work.<br />Sorry for the inconvenience.":
-        "Javascript kreves for at %s skal fungere<br />Beklager.",
+    "JavaScript is required for %s to work. Sorry for the inconvenience.":
+        "Javascript kreves for at %s skal fungere. Beklager.",
     "%s requires a modern browser to work.":
         "%s krever en moderne nettleser for å fungere.",
     "New":

+ 2 - 2
i18n/oc.json

@@ -31,8 +31,8 @@
         "Geton de supression incorrècte. Lo tèxte es pas estat suprimit.",
     "Paste was properly deleted.":
         "Lo tèxte es estat corrèctament  suprimit.",
-    "JavaScript is required for %s to work.<br />Sorry for the inconvenience.":
-        "JavaScript es requesit per far foncionar %s. <br />O planhèm per l’inconvenient.",
+    "JavaScript is required for %s to work. Sorry for the inconvenience.":
+        "JavaScript es requesit per far foncionar %s. O planhèm per l’inconvenient.",
     "%s requires a modern browser to work.":
         "%s necessita un navigator modèrn per foncionar.",
     "New":

+ 1 - 1
i18n/pl.json

@@ -31,7 +31,7 @@
         "Nieprawidłowy token usuwania. Wklejka nie została usunięta.",
     "Paste was properly deleted.":
         "Wklejka usunięta poprawnie.",
-    "JavaScript is required for %s to work.<br />Sorry for the inconvenience.":
+    "JavaScript is required for %s to work. Sorry for the inconvenience.":
         "Do działania %sa jest wymagany JavaScript. Przepraszamy za tę niedogodność.",
     "%s requires a modern browser to work.":
         "%s wymaga do działania nowoczesnej przeglądarki.",

+ 2 - 2
i18n/pt.json

@@ -31,8 +31,8 @@
         "Token de remoção inválido. A cópia não foi excluída.",
     "Paste was properly deleted.":
         "A cópia foi devidamente excluída.",
-    "JavaScript is required for %s to work.<br />Sorry for the inconvenience.":
-        "JavaScript é necessário para que %s funcione.<br />Pedimos desculpas pela inconveniência.",
+    "JavaScript is required for %s to work. Sorry for the inconvenience.":
+        "JavaScript é necessário para que %s funcione. Pedimos desculpas pela inconveniência.",
     "%s requires a modern browser to work.":
         "%s requer um navegador moderno para funcionar.",
     "New":

+ 2 - 2
i18n/ru.json

@@ -31,8 +31,8 @@
         "Неверный ключ удаления записи. Запись не удалена.",
     "Paste was properly deleted.":
         "Запись была успешно удалена.",
-    "JavaScript is required for %s to work.<br />Sorry for the inconvenience.":
-        "Для работы %s требуется включенный JavaScript.<br />Приносим извинения за неудобства.",
+    "JavaScript is required for %s to work. Sorry for the inconvenience.":
+        "Для работы %s требуется включенный JavaScript. Приносим извинения за неудобства.",
     "%s requires a modern browser to work.":
         "Для работы %s требуется более современный браузер.",
     "New":

+ 2 - 2
i18n/sl.json

@@ -31,8 +31,8 @@
         "Napačen token za izbris. Prilepek ni bil izbrisan..",
     "Paste was properly deleted.":
         "Prilepek je uspešno izbrisan.",
-    "JavaScript is required for %s to work.<br />Sorry for the inconvenience.":
-        "Da %s deluje, moraš vklopiti JavaScript.<br />Oprosti za povročene nevšečnosti.",
+    "JavaScript is required for %s to work. Sorry for the inconvenience.":
+        "Da %s deluje, moraš vklopiti JavaScript. Oprosti za povročene nevšečnosti.",
     "%s requires a modern browser to work.":
         "%s za svoje delovanje potrebuje moderen brskalnik.",
     "New":

+ 2 - 2
i18n/uk.json

@@ -31,8 +31,8 @@
         "Неправильний ключ вилучення допису. Допис не вилучено.",
     "Paste was properly deleted.":
         "Допис був вилучений повністю.",
-    "JavaScript is required for %s to work.<br />Sorry for the inconvenience.":
-        "Для роботи %s потрібен увімкнутий JavaScript.<br />Вибачте.",
+    "JavaScript is required for %s to work. Sorry for the inconvenience.":
+        "Для роботи %s потрібен увімкнутий JavaScript. Вибачте.",
     "%s requires a modern browser to work.":
         "Для роботи %s потрібен більш сучасний переглядач.",
     "New":

+ 2 - 2
i18n/zh.json

@@ -31,8 +31,8 @@
         "错误的删除token,粘贴内容没有被删除。",
     "Paste was properly deleted.":
         "粘贴内容已被正确删除。",
-    "JavaScript is required for %s to work.<br />Sorry for the inconvenience.":
-        "%s需要JavaScript来进行加解密。<br />给你带来的不便敬请谅解。",
+    "JavaScript is required for %s to work. Sorry for the inconvenience.":
+        "%s需要JavaScript来进行加解密。 给你带来的不便敬请谅解。",
     "%s requires a modern browser to work.":
         "%s需要在现代浏览器上工作。",
     "New":

+ 1 - 1
js/privatebin.js

@@ -645,7 +645,7 @@ jQuery.PrivateBin = (function($, RawDeflate) {
                 // only allow tags/attributes we actually use in translations
                 output = DOMPurify.sanitize(
                     output, {
-                        ALLOWED_TAGS: ['a', 'br', 'i', 'span'],
+                        ALLOWED_TAGS: ['a', 'i', 'span'],
                         ALLOWED_ATTR: ['href', 'id']
                     }
                 );

+ 3 - 3
js/test/I18n.js

@@ -38,7 +38,7 @@ describe('I18n', function () {
                 } else {
                     messageId = DOMPurify.sanitize(
                         messageId, {
-                            ALLOWED_TAGS: ['a', 'br', 'i', 'span'],
+                            ALLOWED_TAGS: ['a', 'i', 'span'],
                             ALLOWED_ATTR: ['href', 'id']
                         }
                     );
@@ -77,7 +77,7 @@ describe('I18n', function () {
                 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_TAGS: ['a', 'i', 'span'],
                         ALLOWED_ATTR: ['href', 'id']
                     }
                 );
@@ -129,7 +129,7 @@ describe('I18n', function () {
                 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_TAGS: ['a', 'i', 'span'],
                         ALLOWED_ATTR: ['href', 'id']
                     }
                 );

+ 22 - 0
lib/I18n.php

@@ -125,9 +125,31 @@ class I18n
         } else {
             $args[0] = self::$_translations[$messageId];
         }
+        // encode any non-integer arguments and the message ID, if it doesn't contain a link
+        $argsCount = count($args);
+        if ($argsCount > 1) {
+            for ($i = 0; $i < $argsCount; ++$i) {
+                if (($i > 0 && !is_int($args[$i])) || strpos($args[0], '<a') === false) {
+                    $args[$i] = self::encode($args[$i]);
+                }
+            }
+        }
         return call_user_func_array('sprintf', $args);
     }
 
+    /**
+     * encode HTML entities for output into an HTML5 document
+     *
+     * @access public
+     * @static
+     * @param  string $string
+     * @return string
+     */
+    public static function encode($string)
+    {
+        return htmlspecialchars($string, ENT_QUOTES | ENT_HTML5 | ENT_DISALLOWED, 'UTF-8', false);
+    }
+
     /**
      * loads translations
      *

+ 7 - 7
tpl/bootstrap.php

@@ -4,7 +4,7 @@ $isCpct = substr($template, 9, 8) === '-compact';
 $isDark = substr($template, 9, 5) === '-dark';
 $isPage = substr($template, -5) === '-page';
 ?><!DOCTYPE html>
-<html>
+<html lang="<?php echo I18n::_('en'); ?>">
 	<head>
 		<meta charset="utf-8" />
 		<meta http-equiv="X-UA-Compatible" content="IE=edge">
@@ -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/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-Q7yHFlVuPYWw/SJFiMv83PPVwGKqBwoqZhNtHAwkTIxocS6Zpqyj1I0/nUCRWv15xuurctViB3lSVs6s+7f0jw==" crossorigin="anonymous"></script>
+		<script type="text/javascript" data-cfasync="false" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-F6du+TJ3nokfL4mt94qSzqIXrf/dmwBMMfHwe3tDI86xE47VgwVHUC2tmbEpDQZkoydhXR+Lrnj/wCepoK144w==" crossorigin="anonymous"></script>
 		<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-16x16.png?<?php echo rawurlencode($VERSION); ?>" sizes="16x16" />
@@ -440,7 +440,7 @@ if (strlen($NOTICE)):
 ?>
 				<div role="alert" class="alert alert-info">
 					<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span>
-					<?php echo htmlspecialchars($NOTICE), PHP_EOL; ?>
+					<?php echo I18n::encode($NOTICE), PHP_EOL; ?>
 				</div>
 <?php
 endif;
@@ -460,16 +460,16 @@ endif;
 ?>
 				<div id="status" role="alert" class="alert alert-info<?php echo empty($STATUS) ? ' hidden' : '' ?>">
 					<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span>
-					<?php echo htmlspecialchars($STATUS), PHP_EOL; ?>
+					<?php echo I18n::encode($STATUS), PHP_EOL; ?>
 				</div>
 				<div id="errormessage" role="alert" class="<?php echo empty($ERROR) ? 'hidden' : '' ?> alert alert-danger">
 					<span class="glyphicon glyphicon-alert" aria-hidden="true"></span>
-					<?php echo htmlspecialchars($ERROR), PHP_EOL; ?>
+					<?php echo I18n::encode($ERROR), PHP_EOL; ?>
 				</div>
 				<noscript>
 					<div id="noscript" role="alert" class="alert alert-<?php echo $isDark ? 'error' : 'warning'; ?>">
 						<span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span>
-						<?php echo I18n::_('JavaScript is required for %s to work.<br />Sorry for the inconvenience.', I18n::_($NAME)), PHP_EOL; ?>
+						<?php echo I18n::_('JavaScript is required for %s to work. Sorry for the inconvenience.', I18n::_($NAME)), PHP_EOL; ?>
 					</div>
 				</noscript>
 				<div id="oldnotice" role="alert" class="hidden alert alert-danger">
@@ -504,7 +504,7 @@ endif;
 if (strlen($URLSHORTENER)):
 ?>
 					<p>
-						<button id="shortenbutton" data-shortener="<?php echo htmlspecialchars($URLSHORTENER); ?>" type="button" class="btn btn-<?php echo $isDark ? 'warning' : 'primary'; ?> btn-block">
+						<button id="shortenbutton" data-shortener="<?php echo I18n::encode($URLSHORTENER); ?>" type="button" class="btn btn-<?php echo $isDark ? 'warning' : 'primary'; ?> btn-block">
 						<span class="glyphicon glyphicon-send" aria-hidden="true"></span> <?php echo I18n::_('Shorten URL'), PHP_EOL; ?>
 					</button>
 					</p>

+ 7 - 7
tpl/page.php

@@ -1,7 +1,7 @@
 <?php
 use PrivateBin\I18n;
 ?><!DOCTYPE html>
-<html lang="en">
+<html lang="<?php echo I18n::_('en'); ?>">
 	<head>
 		<meta charset="utf-8" />
 		<meta name="robots" content="noindex" />
@@ -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/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-Q7yHFlVuPYWw/SJFiMv83PPVwGKqBwoqZhNtHAwkTIxocS6Zpqyj1I0/nUCRWv15xuurctViB3lSVs6s+7f0jw==" crossorigin="anonymous"></script>
+		<script type="text/javascript" data-cfasync="false" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-F6du+TJ3nokfL4mt94qSzqIXrf/dmwBMMfHwe3tDI86xE47VgwVHUC2tmbEpDQZkoydhXR+Lrnj/wCepoK144w==" crossorigin="anonymous"></script>
 		<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-16x16.png?<?php echo rawurlencode($VERSION); ?>" sizes="16x16" />
@@ -67,14 +67,14 @@ endif;
 <?php
 if (strlen($NOTICE)):
 ?>
-				<span class="blink">▶</span> <?php echo htmlspecialchars($NOTICE);
+				<span class="blink">▶</span> <?php echo I18n::encode($NOTICE);
 endif;
 ?>
 			</div>
 			<h1 class="title reloadlink"><?php echo I18n::_($NAME); ?></h1><br />
 			<h2 class="title"><?php echo I18n::_('Because ignorance is bliss'); ?></h2><br />
 			<h3 class="title"><?php echo $VERSION; ?></h3>
-			<noscript><div id="noscript" class="nonworking"><?php echo I18n::_('JavaScript is required for %s to work.<br />Sorry for the inconvenience.', I18n::_($NAME)); ?></div></noscript>
+			<noscript><div id="noscript" class="nonworking"><?php echo I18n::_('JavaScript is required for %s to work. Sorry for the inconvenience.', I18n::_($NAME)); ?></div></noscript>
 			<div id="oldnotice" class="nonworking hidden"><?php echo I18n::_('%s requires a modern browser to work.', I18n::_($NAME)), PHP_EOL; ?>
 				<a href="https://www.mozilla.org/firefox/">Firefox</a>,
 				<a href="https://www.opera.com/">Opera</a>,
@@ -97,8 +97,8 @@ endif;
 		<section>
 			<article>
 				<div id="loadingindicator" class="hidden"><?php echo I18n::_('Loading…'); ?></div>
-				<div id="status"><?php echo htmlspecialchars($STATUS); ?></div>
-				<div id="errormessage" class="hidden"><?php echo htmlspecialchars($ERROR); ?></div>
+				<div id="status"><?php echo I18n::encode($STATUS); ?></div>
+				<div id="errormessage" class="hidden"><?php echo I18n::encode($ERROR); ?></div>
 				<div id="toolbar">
 					<button id="newbutton" class="reloadlink hidden"><img src="img/icon_new.png" width="11" height="15" alt="" /><?php echo I18n::_('New'); ?></button>
 					<button id="retrybutton" class="reloadlink hidden"><?php echo I18n::_('Retry'), PHP_EOL; ?></button>
@@ -207,7 +207,7 @@ endif;
 <?php
 if (strlen($URLSHORTENER)):
 ?>
-					<button id="shortenbutton" data-shortener="<?php echo htmlspecialchars($URLSHORTENER); ?>"><img src="img/icon_shorten.png" width="13" height="15" /><?php echo I18n::_('Shorten URL'); ?></button>
+					<button id="shortenbutton" data-shortener="<?php echo I18n::encode($URLSHORTENER); ?>"><img src="img/icon_shorten.png" width="13" height="15" /><?php echo I18n::_('Shorten URL'); ?></button>
 <?php
 endif;
 ?>

+ 11 - 0
tst/I18nTest.php

@@ -155,6 +155,17 @@ class I18nTest extends PHPUnit_Framework_TestCase
         $this->assertEquals('some string + 1', I18n::_('some %s + %d', 'string', 1), 'browser language en');
     }
 
+    public function testHtmlEntityEncoding()
+    {
+        $_SERVER['HTTP_ACCEPT_LANGUAGE'] = 'foobar';
+        I18n::loadTranslations();
+        $input  = '&<>"\'/`=';
+        $result = htmlspecialchars($input, ENT_QUOTES | ENT_HTML5 | ENT_DISALLOWED, 'UTF-8', false);
+        $this->assertEquals($result, I18n::encode($input), 'encodes HTML entities');
+        $this->assertEquals('<a>some ' . $result . ' + 1</a>', I18n::_('<a>some %s + %d</a>', $input, 1), 'encodes parameters in translations');
+        $this->assertEquals($result . $result, I18n::_($input . '%s', $input), 'encodes message ID as well, when no link');
+    }
+
     public function testMessageIdsExistInAllLanguages()
     {
         $messageIds = array();