Proper HTML encoding/decoding of tags in the frontend

- Tags in the OPDS feed are HTML encoded and must be decoded.

- Tag values must be HTML encoded when injected into the DOM:

  * When the injection is done by setting the innerHTML attribute of a
    DOM element, HTML encoding must be done explicitly (since that text
    is going to be parsed as HTML).

  * When the tag value is expanded into a string that is then set as an
    attribute of a DOM element via the setAttribute() method, no HTML
    encoding must be done (since Element.setAttribute() directly sets
    that value and no HTML decoding is involved in that operation).
This commit is contained in:
Veloman Yunkan 2024-09-04 18:43:24 +04:00
parent fbcd160efd
commit 846404e959
2 changed files with 19 additions and 6 deletions

View File

@ -51,8 +51,8 @@
, "torrent-download-link-text": "BitTorrent" , "torrent-download-link-text": "BitTorrent"
, "torrent-download-alt-text": "Download via BitTorrent" , "torrent-download-alt-text": "Download via BitTorrent"
, "library-opds-feed-all-entries": "Library OPDS Feed - All entries" , "library-opds-feed-all-entries": "Library OPDS Feed - All entries"
, "filter-by-tag": "Filter by tag \"{{TAG}}\"" , "filter-by-tag": "Filter by tag \"{{{TAG}}}\""
, "stop-filtering-by-tag": "Stop filtering by tag \"{{TAG}}\"" , "stop-filtering-by-tag": "Stop filtering by tag \"{{{TAG}}}\""
, "library-opds-feed-parameterised": "Library OPDS Feed - entries matching {{#LANG}}\nLanguage: {{LANG}} {{/LANG}}{{#CATEGORY}}\nCategory: {{CATEGORY}} {{/CATEGORY}}{{#TAG}}\nTag: {{TAG}} {{/TAG}}{{#Q}}\nQuery: {{Q}} {{/Q}}" , "library-opds-feed-parameterised": "Library OPDS Feed - entries matching {{#LANG}}\nLanguage: {{LANG}} {{/LANG}}{{#CATEGORY}}\nCategory: {{CATEGORY}} {{/CATEGORY}}{{#TAG}}\nTag: {{TAG}} {{/TAG}}{{#Q}}\nQuery: {{Q}} {{/Q}}"
, "welcome-to-kiwix-server": "Welcome to Kiwix Server" , "welcome-to-kiwix-server": "Welcome to Kiwix Server"
, "download-links-heading": "Download links for <b><i>{{BOOK_TITLE}}</i></b>" , "download-links-heading": "Download links for <b><i>{{BOOK_TITLE}}</i></b>"

View File

@ -105,6 +105,14 @@
return ''; return '';
} }
// Borrowed from https://stackoverflow.com/a/1912522
function htmlDecode(input){
var e = document.createElement('textarea');
e.innerHTML = input;
// handle case of empty input
return e.childNodes.length === 0 ? "" : e.childNodes[0].nodeValue;
}
function htmlEncode(str) { function htmlEncode(str) {
return str.replace(/[\u00A0-\u9999<>\&]/gim, (i) => `&#${i.charCodeAt(0)};`); return str.replace(/[\u00A0-\u9999<>\&]/gim, (i) => `&#${i.charCodeAt(0)};`);
} }
@ -121,9 +129,14 @@
function generateTagLink(tagValue) { function generateTagLink(tagValue) {
tagValue = tagValue.toLowerCase(); tagValue = tagValue.toLowerCase();
const htmlEncodedTagValue = htmlEncode(tagValue);
const tagMessage = $t("filter-by-tag", {TAG: tagValue}); const tagMessage = $t("filter-by-tag", {TAG: tagValue});
return `<span class='tag__link' aria-label='${tagMessage}' title='${tagMessage}' data-tag=${htmlEncodedTagValue}>${htmlEncodedTagValue}</span>` const spanElement = document.createElement("span");
spanElement.className = 'tag__link';
spanElement.setAttribute('aria-label', tagMessage);
spanElement.setAttribute('title', tagMessage);
spanElement.setAttribute('data-tag', tagValue);
spanElement.innerHTML = htmlEncode(tagValue);
return spanElement.outerHTML;
} }
function generateBookHtml(book, sort = false) { function generateBookHtml(book, sort = false) {
@ -144,7 +157,7 @@
const mulLangList = langCodesList.filter(x => languages.hasOwnProperty(x)).map(x => languages[x]); const mulLangList = langCodesList.filter(x => languages.hasOwnProperty(x)).map(x => languages[x]);
language = mulLangList.join(', '); language = mulLangList.join(', ');
} }
const tags = getInnerHtml(book, 'tags'); const tags = htmlDecode(getInnerHtml(book, 'tags'));
const tagList = tags.split(';').filter(tag => {return !(tag.startsWith('_'))}); const tagList = tags.split(';').filter(tag => {return !(tag.startsWith('_'))});
const tagFilterLinks = tagList.map((tagValue) => generateTagLink(tagValue)); const tagFilterLinks = tagList.map((tagValue) => generateTagLink(tagValue));
const tagHtml = tagFilterLinks.join(' | '); const tagHtml = tagFilterLinks.join(' | ');
@ -492,7 +505,7 @@
function addTagElement(tagValue, resetFilter) { function addTagElement(tagValue, resetFilter) {
const tagElement = document.getElementsByClassName('tagFilterLabel')[0]; const tagElement = document.getElementsByClassName('tagFilterLabel')[0];
tagElement.style.display = 'inline-block'; tagElement.style.display = 'inline-block';
tagElement.innerHTML = `${tagValue}`; tagElement.innerHTML = htmlEncode(tagValue);
const tagMessage = $t("stop-filtering-by-tag", {TAG: tagValue}); const tagMessage = $t("stop-filtering-by-tag", {TAG: tagValue});
tagElement.setAttribute('aria-label', tagMessage); tagElement.setAttribute('aria-label', tagMessage);
tagElement.setAttribute('title', tagMessage); tagElement.setAttribute('title', tagMessage);