From fbcd160efd6fc7568e1bb65c3a05b69083248d50 Mon Sep 17 00:00:00 2001 From: Veloman Yunkan Date: Wed, 4 Sep 2024 18:15:01 +0400 Subject: [PATCH 1/4] Disabled beautification of tags in the frontend --- static/skin/index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/static/skin/index.js b/static/skin/index.js index 28a452d2b..13359cbcf 100644 --- a/static/skin/index.js +++ b/static/skin/index.js @@ -121,9 +121,9 @@ function generateTagLink(tagValue) { tagValue = tagValue.toLowerCase(); - const humanFriendlyTagValue = humanFriendlyTitle(tagValue); - const tagMessage = $t("filter-by-tag", {TAG: humanFriendlyTagValue}); - return `${humanFriendlyTagValue}` + const htmlEncodedTagValue = htmlEncode(tagValue); + const tagMessage = $t("filter-by-tag", {TAG: tagValue}); + return `${htmlEncodedTagValue}` } function generateBookHtml(book, sort = false) { From 846404e959415f47527a9e5c3ea8c0bc8b6f9855 Mon Sep 17 00:00:00 2001 From: Veloman Yunkan Date: Wed, 4 Sep 2024 18:43:24 +0400 Subject: [PATCH 2/4] 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). --- static/skin/i18n/en.json | 4 ++-- static/skin/index.js | 21 +++++++++++++++++---- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/static/skin/i18n/en.json b/static/skin/i18n/en.json index 9345a1cb5..7add683cc 100644 --- a/static/skin/i18n/en.json +++ b/static/skin/i18n/en.json @@ -51,8 +51,8 @@ , "torrent-download-link-text": "BitTorrent" , "torrent-download-alt-text": "Download via BitTorrent" , "library-opds-feed-all-entries": "Library OPDS Feed - All entries" - , "filter-by-tag": "Filter by tag \"{{TAG}}\"" - , "stop-filtering-by-tag": "Stop filtering by tag \"{{TAG}}\"" + , "filter-by-tag": "Filter 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}}" , "welcome-to-kiwix-server": "Welcome to Kiwix Server" , "download-links-heading": "Download links for {{BOOK_TITLE}}" diff --git a/static/skin/index.js b/static/skin/index.js index 13359cbcf..873b187f0 100644 --- a/static/skin/index.js +++ b/static/skin/index.js @@ -105,6 +105,14 @@ 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) { return str.replace(/[\u00A0-\u9999<>\&]/gim, (i) => `&#${i.charCodeAt(0)};`); } @@ -121,9 +129,14 @@ function generateTagLink(tagValue) { tagValue = tagValue.toLowerCase(); - const htmlEncodedTagValue = htmlEncode(tagValue); const tagMessage = $t("filter-by-tag", {TAG: tagValue}); - return `${htmlEncodedTagValue}` + 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) { @@ -144,7 +157,7 @@ const mulLangList = langCodesList.filter(x => languages.hasOwnProperty(x)).map(x => languages[x]); language = mulLangList.join(', '); } - const tags = getInnerHtml(book, 'tags'); + const tags = htmlDecode(getInnerHtml(book, 'tags')); const tagList = tags.split(';').filter(tag => {return !(tag.startsWith('_'))}); const tagFilterLinks = tagList.map((tagValue) => generateTagLink(tagValue)); const tagHtml = tagFilterLinks.join(' | '); @@ -492,7 +505,7 @@ function addTagElement(tagValue, resetFilter) { const tagElement = document.getElementsByClassName('tagFilterLabel')[0]; tagElement.style.display = 'inline-block'; - tagElement.innerHTML = `${tagValue}`; + tagElement.innerHTML = htmlEncode(tagValue); const tagMessage = $t("stop-filtering-by-tag", {TAG: tagValue}); tagElement.setAttribute('aria-label', tagMessage); tagElement.setAttribute('title', tagMessage); From 00fae37f2dbb51f625cdc09ffb153392febbc1d7 Mon Sep 17 00:00:00 2001 From: Veloman Yunkan Date: Wed, 4 Sep 2024 19:09:35 +0400 Subject: [PATCH 3/4] Fixed vertical alignment of the tag filter indicator --- static/skin/index.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/skin/index.css b/static/skin/index.css index bfe6dc1f3..b8af17508 100644 --- a/static/skin/index.css +++ b/static/skin/index.css @@ -121,7 +121,7 @@ .tagFilterLabel { width: max-content; - padding: 10px; + padding: 7px; font-family: roboto; font-size: 12px; margin: 0 0 0 17px; From 2b8a071c6f0b10a9bb905b3e9ea9ef6199e40fb7 Mon Sep 17 00:00:00 2001 From: Veloman Yunkan Date: Wed, 4 Sep 2024 19:13:41 +0400 Subject: [PATCH 4/4] Fixed cacheids in unit-tests --- test/library_server.cpp | 2 +- test/server.cpp | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/test/library_server.cpp b/test/library_server.cpp index f5082a42f..1c97b9548 100644 --- a/test/library_server.cpp +++ b/test/library_server.cpp @@ -1033,7 +1033,7 @@ TEST_F(LibraryServerTest, no_name_mapper_catalog_v2_individual_entry_access) " />\n" \ " \n" \ " \n" \ diff --git a/test/server.cpp b/test/server.cpp index 44fc99311..9c4ce01ab 100644 --- a/test/server.cpp +++ b/test/server.cpp @@ -61,9 +61,9 @@ const ResourceCollection resources200Compressible{ { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/i18n.js" }, { STATIC_CONTENT, "/ROOT%23%3F/skin/i18n.js?cacheid=071abc9a" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/index.css" }, - { STATIC_CONTENT, "/ROOT%23%3F/skin/index.css?cacheid=1e78e7cf" }, + { STATIC_CONTENT, "/ROOT%23%3F/skin/index.css?cacheid=e0600dde" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/index.js" }, - { STATIC_CONTENT, "/ROOT%23%3F/skin/index.js?cacheid=f43eb0b9" }, + { STATIC_CONTENT, "/ROOT%23%3F/skin/index.js?cacheid=480ca6b4" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/iso6391To3.js" }, { STATIC_CONTENT, "/ROOT%23%3F/skin/iso6391To3.js?cacheid=ecde2bb3" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/isotope.pkgd.min.js" }, @@ -279,7 +279,7 @@ TEST_F(ServerTest, CacheIdsOfStaticResources) { /* url */ "/ROOT%23%3F/", R"EXPECTEDRESULT( href="/ROOT%23%3F/skin/kiwix.css?cacheid=2158fad9" - href="/ROOT%23%3F/skin/index.css?cacheid=1e78e7cf" + href="/ROOT%23%3F/skin/index.css?cacheid=e0600dde" @@ -292,7 +292,7 @@ R"EXPECTEDRESULT( href="/ROOT%23%3F/skin/kiwix.css?cacheid=2158fad9" - +