Merge pull request #1178 from kiwix/nicer_error_pages

Nicer 404 error and external link blocker pages
This commit is contained in:
Kelson 2025-05-17 13:56:46 +02:00 committed by GitHub
commit 79479788f9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 547 additions and 147 deletions

View File

@ -1085,9 +1085,7 @@ std::unique_ptr<Response> InternalServer::handle_captured_external(const Request
return UrlNotFoundResponse(request); return UrlNotFoundResponse(request);
} }
auto data = get_default_data(); return BlockExternalLinkResponse(request, m_root, source);
data.set("source", source);
return ContentResponse::build(RESOURCE::templates::captured_external_html, data, "text/html; charset=utf-8");
} }
std::unique_ptr<Response> InternalServer::handle_catch(const RequestContext& request) std::unique_ptr<Response> InternalServer::handle_catch(const RequestContext& request)
@ -1121,15 +1119,6 @@ InternalServer::search_catalog(const RequestContext& request,
namespace namespace
{ {
ParameterizedMessage suggestSearchMsg(const std::string& searchURL, const std::string& pattern)
{
return ParameterizedMessage("suggest-search",
{
{ "PATTERN", pattern },
{ "SEARCH_URL", searchURL }
});
}
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
// The content security policy below is set on responses to the /content // The content security policy below is set on responses to the /content
// endpoint in order to prevent the ZIM content from interfering with the // endpoint in order to prevent the ZIM content from interfering with the
@ -1183,9 +1172,7 @@ std::unique_ptr<Response> InternalServer::handle_content(const RequestContext& r
} catch (const std::out_of_range& e) {} } catch (const std::out_of_range& e) {}
if (archive == nullptr) { if (archive == nullptr) {
const std::string searchURL = m_root + "/search?pattern=" + kiwix::urlEncode(pattern); return NewHTTP404Response(request, m_root, m_root + url);
return UrlNotFoundResponse(request)
+ suggestSearchMsg(searchURL, kiwix::urlDecode(pattern));
} }
const std::string archiveUuid(archive->getUuid()); const std::string archiveUuid(archive->getUuid());
@ -1230,9 +1217,7 @@ std::unique_ptr<Response> InternalServer::handle_content(const RequestContext& r
if (m_verbose.load()) if (m_verbose.load())
printf("Failed to find %s\n", urlStr.c_str()); printf("Failed to find %s\n", urlStr.c_str());
std::string searchURL = m_root + "/search?content=" + bookName + "&pattern=" + kiwix::urlEncode(pattern); return NewHTTP404Response(request, m_root, m_root + url);
return UrlNotFoundResponse(request)
+ suggestSearchMsg(searchURL, kiwix::urlDecode(pattern));
} }
} }

View File

@ -243,6 +243,23 @@ public:
}; };
} }
static Data fromMsgId(const std::string& nonParameterizedMsgId)
{
return from(nonParameterizedMessage(nonParameterizedMsgId));
}
static Data staticMultiParagraphText(const std::string& msgIdPrefix, size_t n)
{
Object paragraphs;
for ( size_t i = 1; i <= n; ++i ) {
std::ostringstream oss;
oss << "p" << i;
const std::string pId = oss.str();
paragraphs[pId] = fromMsgId(msgIdPrefix + "." + pId);
}
return paragraphs;
}
std::string asJSON() const; std::string asJSON() const;
void dumpJSON(std::ostream& os) const; void dumpJSON(std::ostream& os) const;
@ -368,6 +385,45 @@ std::unique_ptr<ContentResponse> ContentResponseBlueprint::generateResponseObjec
return r; return r;
} }
NewHTTP404Response::NewHTTP404Response(const RequestContext& request,
const std::string& root,
const std::string& urlPath)
: ContentResponseBlueprint(&request,
MHD_HTTP_NOT_FOUND,
"text/html; charset=utf-8",
RESOURCE::templates::sexy404_html,
/*includeKiwixResponseData=*/true)
{
*this->m_data = Data(Data::Object{
{"root", root },
{"url_path", urlPath},
{"PAGE_TITLE", Data::fromMsgId("new-404-page-title")},
{"PAGE_HEADING", Data::fromMsgId("new-404-page-heading")},
{"404_img_text", Data::fromMsgId("404-img-text")},
{"path_was_not_found_msg", Data::fromMsgId("path-was-not-found")},
{"advice", Data::staticMultiParagraphText("404-advice", 5)},
});
}
BlockExternalLinkResponse::BlockExternalLinkResponse(const RequestContext& request,
const std::string& root,
const std::string& externalUrl)
: ContentResponseBlueprint(&request,
MHD_HTTP_OK,
"text/html; charset=utf-8",
RESOURCE::templates::captured_external_html,
/*includeKiwixResponseData=*/true)
{
*this->m_data = Data(Data::Object{
{"root", root },
{"external_link_detected", Data::fromMsgId("external-link-detected") },
{"url", externalUrl },
{"caution_warning", Data::fromMsgId("caution-warning") },
{"external_link_intro", Data::fromMsgId("external-link-intro") },
{"advice", Data::staticMultiParagraphText("external-link-advice", 3)},
});
}
HTTPErrorResponse::HTTPErrorResponse(const RequestContext& request, HTTPErrorResponse::HTTPErrorResponse(const RequestContext& request,
int httpStatusCode, int httpStatusCode,
const std::string& pageTitleMsgId, const std::string& pageTitleMsgId,
@ -383,8 +439,8 @@ HTTPErrorResponse::HTTPErrorResponse(const RequestContext& request,
Data::List emptyList; Data::List emptyList;
*this->m_data = Data(Data::Object{ *this->m_data = Data(Data::Object{
{"CSS_URL", Data::onlyAsNonEmptyValue(cssUrl) }, {"CSS_URL", Data::onlyAsNonEmptyValue(cssUrl) },
{"PAGE_TITLE", Data::from(nonParameterizedMessage(pageTitleMsgId))}, {"PAGE_TITLE", Data::fromMsgId(pageTitleMsgId)},
{"PAGE_HEADING", Data::from(nonParameterizedMessage(headingMsgId))}, {"PAGE_HEADING", Data::fromMsgId(headingMsgId)},
{"details", emptyList} {"details", emptyList}
}); });
} }

View File

@ -145,6 +145,13 @@ protected: //data
std::unique_ptr<Data> m_data; std::unique_ptr<Data> m_data;
}; };
struct NewHTTP404Response : ContentResponseBlueprint
{
NewHTTP404Response(const RequestContext& request,
const std::string& root,
const std::string& urlPath);
};
struct HTTPErrorResponse : ContentResponseBlueprint struct HTTPErrorResponse : ContentResponseBlueprint
{ {
HTTPErrorResponse(const RequestContext& request, HTTPErrorResponse(const RequestContext& request,
@ -190,6 +197,13 @@ class ItemResponse : public Response {
std::string m_mimeType; std::string m_mimeType;
}; };
struct BlockExternalLinkResponse : ContentResponseBlueprint
{
BlockExternalLinkResponse(const RequestContext& request,
const std::string& root,
const std::string& externalUrl);
};
} }
#endif //KIWIXLIB_SERVER_RESPONSE_H #endif //KIWIXLIB_SERVER_RESPONSE_H

View File

@ -1,6 +1,8 @@
skin/caret.png skin/caret.png
skin/bittorrent.png skin/bittorrent.png
skin/magnet.png skin/magnet.png
skin/404.svg
skin/blocklink.svg
skin/feed.svg skin/feed.svg
skin/langSelector.svg skin/langSelector.svg
skin/download.png skin/download.png
@ -11,9 +13,11 @@ skin/iso6391To3.js
skin/isotope.pkgd.min.js skin/isotope.pkgd.min.js
skin/index.js skin/index.js
skin/autoComplete/autoComplete.min.js skin/autoComplete/autoComplete.min.js
skin/error.css
skin/kiwix.css skin/kiwix.css
skin/taskbar.css skin/taskbar.css
skin/index.css skin/index.css
skin/fonts/DMSans-Regular.ttf
skin/fonts/Poppins.ttf skin/fonts/Poppins.ttf
skin/fonts/Roboto.ttf skin/fonts/Roboto.ttf
skin/search_results.css skin/search_results.css
@ -42,6 +46,7 @@ templates/url_of_search_results_css.tmpl
templates/viewer_settings.js templates/viewer_settings.js
templates/no_js_library_page.html templates/no_js_library_page.html
templates/no_js_download.html templates/no_js_download.html
templates/sexy404.html
opensearchdescription.xml opensearchdescription.xml
ft_opensearchdescription.xml ft_opensearchdescription.xml
catalog_v2_searchdescription.xml catalog_v2_searchdescription.xml

1
static/skin/404.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.4 KiB

View File

@ -0,0 +1 @@
<svg viewBox="0 0 1742.79 1984.21" xmlns="http://www.w3.org/2000/svg"><ellipse cx="667.97" cy="1872.93" fill="#c7c8ca" rx="649.71" ry="111.28"/><path d="m933.76 1775.81c0-29.5-23.92-53.42-53.42-53.42h-163.28c-24.37 0-45.97-15.91-52.98-39.26l-105.96-347.65 18.1-9.63c251.03-105.38 519.58 82.75 706.54-97.93l1.17-1.17c23.21-21.02 46.27-42.47 114.72.29 73.56 46.12 236 166.53 309.71 372.32 0 0 44.66-15.91 32.25-70.49-12.41-54.73-102.89-212.36-287.96-352.18-33.86-25.69-64.36-47-89.76-64.07 36.93-158.21-155.14-349.11-342.69-259.21-134.57-126.54-284.6-178.2-422.67-176.16-365.6 5.11-647.58 386.04-344.3 749.45l.58.58c4.67 5.55 9.49 11.09 14.3 16.64 19.7 23.64 32.69 44.51 43.93 81.29l90.34 296.57h-31.96c-29.48 0-53.42 23.94-53.42 53.42h121.43l204.04 96.47c12.55-26.71 1.17-58.53-25.4-71.08l-53.56-25.25h153.83c0-29.48-23.94-53.42-53.42-53.42h-139.38c-24.37 0-45.97-15.91-52.98-39.26l-71.66-235.42c-17.81-61.88 23.21-102.31 61.74-107.13 35.17-4.38 52.83 18.39 69.47 73.27l63.63 208.85h-31.96c-29.48 0-53.42 23.94-53.42 53.42h121.43l204.04 96.47c12.55-26.71 1.17-58.53-25.4-71.08l-53.56-25.25h177.88z"/><path d="m545.68 2.53h52.19v1441.85h-52.19z" fill="#f89a16" transform="matrix(.9855856 -.16917749 .16917749 .9855856 -114.16 107.17)"/><path d="m581.21 624.21-55.8-17.21-93.34-544.54 53.51 3.6z" fill="#da7c2b"/><path d="m981.62 279.44c-30.85 1.91-83.72 3.58-83.72 3.58l79.01-31.06-43.25-251.96-933.66 160.24 56.63 330 68.11 13.66s-38.47 19-60.89 28.37l22.52 131.23 933.66-160.24z" fill="#f89a16"/><circle cx="1144.16" cy="1031.93" fill="#fff" r="82.26" transform="matrix(.70710678 -.70710678 .70710678 .70710678 -394.57 1111.29)"/><circle cx="1124.15" cy="1004.52" r="52.63" transform="matrix(.41786707 -.90850818 .90850818 .41786707 -258.21 1606.05)"/><g fill="#fff"><path d="m387.43 559.95-69.59-57.58 107.75-360.2 93.69-15.45 226.75 308.17-45.64 75.04-312.97 50.02zm-11.05-75.33 25.78 21.33 266.91-42.65 15.63-25.7-187.97-255.46-31.41 5.18-88.94 297.31z"/><path d="m526.16 300.48 5.44 89.61-20.72 3.32-28.24-85.96-9.08-47.73 43.52-6.98 9.08 47.73zm18.46 103.04 7.02 36.88-41.43 6.64-7.02-36.88z"/></g></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

159
static/skin/error.css Normal file
View File

@ -0,0 +1,159 @@
@font-face {
font-family:"DM Sans";
font-style: normal;
font-weight: 400;
src : url('../skin/fonts/DMSans-Regular.ttf?KIWIXCACHEID');
}
@font-face {
font-family:"DM Sans Bold";
font-style: normal;
font-weight: 700;
src : url('../skin/fonts/DMSans-Regular.ttf?KIWIXCACHEID');
}
body {
background: linear-gradient(to bottom right, #ffffff, #e6e6e6);
background-repeat: no-repeat;
background-attachment: fixed;
}
header {
width: 100%;
margin: auto;
text-align: center;
margin-top: 15%;
margin-bottom: 15%;
}
header img {
width: 60%;
min-width: 200px;
max-width: 500px;
max-height: 300px;
}
section {
display: flex;
flex-direction: column;
align-items: center;
}
header, .intro {
font-family: "DM Sans";
}
.intro {
font-size: 1em;
padding: 0 10%;
line-height: 1.2em;
text-align: center;
}
.intro h1 {
line-height: 1.1em;
font-family: "DM Sans Bold";
font-size: 1.2em;
}
.intro code {
font-family: monospace;
font-size: 1.1em;
word-break: break-all;
}
.intro a, .intro a:active, .intro a:visited {
color: #00b4e4;
text-decoration: none;
word-break: break-all;
}
.advice {
width: 80%;
margin: auto;
margin-bottom: 15%;
margin-top: 5em;
background-color: #ffffff;
border-radius: 1rem;
border: 1px solid #b7b7b7;
padding: 2em;
font-family: "DM Sans";
font-size: .9em;
box-sizing: border-box;
align-items: normal;
}
.advice p {
margin-bottom: 1em;
}
.advice p:first-child {
margin-top: 0;
}
.advice p.list-intro {
margin: 0;
}
.advice ul {
list-style-type: square;
margin: 0;
padding: 0 1em;
}
.advice ul li {
line-height: 2em;
}
.advice p:last-child {
margin-bottom: 0;
}
/* sm: 640px+ */
@media (width >= 40rem) {
header {
margin-bottom: 1em;
margin-top: 5em;
}
header img {
width: 50%;
}
.intro h1 {
font-size: 2em;
}
.advice {
width: 50%;
}
}
/* xl: 1280px+ */
@media (width >= 80rem) {
.intro h1 {
font-size: 3.4em;
}
}
/* 2xl: 1536px+ */
@media (width >= 96rem) {
header img {
width: 25%;
min-width: 200px;
max-width: 500px;
max-height: 300px;
}
.advice {
width: 25%;
min-width: 200px;
min-width: 300px;
max-width: 500px;
}
}

Binary file not shown.

View File

@ -23,7 +23,8 @@ const Translations = {
return; return;
const errorMsg = `Error loading translations for language '${lang}': `; const errorMsg = `Error loading translations for language '${lang}': `;
this.promises[lang] = fetch(`./skin/i18n/${lang}.json`).then(async (resp) => { const translationJsonUrl = import.meta.resolve(`./i18n/${lang}.json`);
this.promises[lang] = fetch(translationJsonUrl).then(async (resp) => {
if ( resp.ok ) { if ( resp.ok ) {
this.data[lang] = JSON.parse(await resp.text()); this.data[lang] = JSON.parse(await resp.text());
} else { } else {
@ -190,8 +191,40 @@ function initUILanguageSelector(activeLanguage, languageChangeCallback) {
languageSelector.onchange = languageChangeCallback; languageSelector.onchange = languageChangeCallback;
} }
function parseDom(html) {
const domParser = new DOMParser();
return domParser.parseFromString(html, "text/html").documentElement;
}
function translatePageInWindow(w) {
if ( w.KIWIX_RESPONSE_TEMPLATE && w.KIWIX_RESPONSE_DATA ) {
const template = parseDom(w.KIWIX_RESPONSE_TEMPLATE).textContent;
// w.KIWIX_RESPONSE_DATA may belong to a different context and running
// I18n.render() on it directly won't work correctly
// because the type checks (obj.__proto__ == ???.prototype) in
// I18n.instantiateParameterizedMessages() will fail (String.prototype
// refers to different objects in different contexts).
// Work arround that issue by copying the object into our context.
const params = JSON.parse(JSON.stringify(w.KIWIX_RESPONSE_DATA));
const newHtml = I18n.render(template, params);
w.document.documentElement.innerHTML = parseDom(newHtml).innerHTML;
}
}
function translateSelf() {
if ( window.KIWIX_RESPONSE_TEMPLATE && window.KIWIX_RESPONSE_DATA ) {
setUserLanguage(getUserLanguage(), () => {
translatePageInWindow(window)
});
}
};
window.$t = $t; window.$t = $t;
window.getUserLanguage = getUserLanguage; window.getUserLanguage = getUserLanguage;
window.setUserLanguage = setUserLanguage; window.setUserLanguage = setUserLanguage;
window.initUILanguageSelector = initUILanguageSelector; window.initUILanguageSelector = initUILanguageSelector;
window.translatePageInWindow = translatePageInWindow;
window.I18n = I18n; window.I18n = I18n;
window.addEventListener('load', translateSelf);

View File

@ -20,9 +20,24 @@
, "400-page-heading" : "Invalid request" , "400-page-heading" : "Invalid request"
, "404-page-title" : "Content not found" , "404-page-title" : "Content not found"
, "404-page-heading" : "Not Found" , "404-page-heading" : "Not Found"
, "new-404-page-title" : "Page not found"
, "new-404-page-heading" : "Oops. Page not found."
, "404-img-text": "Not found!"
, "path-was-not-found": "The requested path was not found:"
, "404-advice.p1": "The content you're looking for may still be available, but it might be located at a different place within the ZIM file."
, "404-advice.p2": "Please:"
, "404-advice.p3": "Try using the search function to find the content you want"
, "404-advice.p4": "Look for keywords or titles related to the information you're seeking"
, "404-advice.p5": "This approach should help you locate the desired content, even if the original link isn't working properly."
, "500-page-title" : "Internal Server Error" , "500-page-title" : "Internal Server Error"
, "500-page-heading" : "Internal Server Error" , "500-page-heading" : "Internal Server Error"
, "500-page-text": "An internal server error occured. We are sorry about that :/" , "500-page-text": "An internal server error occured. We are sorry about that :/"
, "external-link-detected" : "External Link Detected"
, "caution-warning" : "Caution!"
, "external-link-intro" : "You are about to leave Kiwix's ZIM reader to go online to"
, "external-link-advice.p1": "The link you're trying to access is not part of your offline package and requires an internet connection."
, "external-link-advice.p2": "If you can go online, you can attempt to open the link."
, "external-link-advice.p3": "You can otherwise return to your ZIM's offline content by using your browser's back button."
, "fulltext-search-unavailable" : "Fulltext search unavailable" , "fulltext-search-unavailable" : "Fulltext search unavailable"
, "no-search-results": "The fulltext search engine is not available for this content." , "no-search-results": "The fulltext search engine is not available for this content."
, "search-results-page-title": "Search: {{SEARCH_PATTERN}}" , "search-results-page-title": "Search: {{SEARCH_PATTERN}}"

View File

@ -24,9 +24,24 @@
"400-page-heading": "Heading of the 400 error page", "400-page-heading": "Heading of the 400 error page",
"404-page-title": "Title of the 404 error page", "404-page-title": "Title of the 404 error page",
"404-page-heading": "Heading of the 404 error page", "404-page-heading": "Heading of the 404 error page",
"new-404-page-title": "Title of the 404 error page",
"new-404-page-heading": "Heading of the 404 error page",
"404-img-text": "Fallback text for the image on the 404 error page",
"path-was-not-found": "Message telling that the URL path was not found (to be followed by the actual path)",
"404-advice.p1": "1st paragraph of the multiline advice on the 'Page not found' error page (see 404-advice.p1 through 404-advice.p5 for full text)",
"404-advice.p2": "2nd paragraph of the multiline advice on the 'Page not found' error page (see 404-advice.p1 through 404-advice.p5 for full text)",
"404-advice.p3": "3rd paragraph of the multiline advice on the 'Page not found' error page (see 404-advice.p1 through 404-advice.p5 for full text)",
"404-advice.p4": "4th paragraph of the multiline advice on the 'Page not found' error page (see 404-advice.p1 through 404-advice.p5 for full text)",
"404-advice.p5": "5th paragraph of the multiline advice on the 'Page not found' error page (see 404-advice.p1 through 404-advice.p5 for full text)",
"500-page-title": "Title of the 500 error page", "500-page-title": "Title of the 500 error page",
"500-page-heading": "Heading of the 500 error page", "500-page-heading": "Heading of the 500 error page",
"500-page-text": "Text of the 500 error page", "500-page-text": "Text of the 500 error page",
"external-link-detected" : "Title & heading of the external link blocker page",
"caution-warning" : "Warning of action that shouldn't be carried out carelessly",
"external-link-intro" : "Message introducing the external link (to be followed by the actual link)",
"external-link-advice.p1": "1st paragraph of the multiline advice on the external link blocker page (see external-link-advice.p1 through external-link-advice.p3 for full text)",
"external-link-advice.p2": "2nd paragraph of the multiline advice on the external link blocker page (see external-link-advice.p1 through external-link-advice.p3 for full text)",
"external-link-advice.p3": "3rd paragraph of the multiline advice on the external link blocker page (see external-link-advice.p1 through external-link-advice.p3 for full text)",
"fulltext-search-unavailable": "Title of the error page returned when search is attempted in a book without fulltext search database", "fulltext-search-unavailable": "Title of the error page returned when search is attempted in a book without fulltext search database",
"no-search-results": "Text of the error page returned when search is attempted in a book without fulltext search database", "no-search-results": "Text of the error page returned when search is attempted in a book without fulltext search database",
"search-results-page-title": "Title of the search results page", "search-results-page-title": "Title of the search results page",

View File

@ -13,6 +13,21 @@
, "400-page-heading": "[I18N TESTING] -400 karma for an invalid request" , "400-page-heading": "[I18N TESTING] -400 karma for an invalid request"
, "404-page-title": "[I18N TESTING] Not Found - Try Again" , "404-page-title": "[I18N TESTING] Not Found - Try Again"
, "404-page-heading": "[I18N TESTING] Content not found, but at least the server is alive" , "404-page-heading": "[I18N TESTING] Content not found, but at least the server is alive"
, "new-404-page-title" : "Page [I18N] not [TESTING] found"
, "new-404-page-heading" : "[I18N TESTING] Oops. Larry Page could not be reached. He may be on paternity leave."
, "404-img-text": "[I18N] Not found! [TESTING]"
, "path-was-not-found": "[I18N TESTING] The requested path was not found (in fact, nothing was found instead, either):"
, "404-advice.p1": "Sh*t happens. [I18N TESTING] Take it easy!"
, "404-advice.p2": "[I18N TESTING] Try one of the following:"
, "404-advice.p3": "[I18N TESTING] Check the spelling of the URL path"
, "404-advice.p4": "[I18N TESTING] Press the dice button"
, "404-advice.p5": "Good luck! [I18N TESTING]"
, "external-link-detected" : "External [I18] Link [TESTING] Detected"
, "caution-warning" : "[I18N] C5n! [TESTING]"
, "external-link-intro" : "[I18N TESTING] The following link may lead you to a place from which you won't ever be able to return"
, "external-link-advice.p1": "[I18N TESTING] The link you're trying to access points past the end of the ZIM file."
, "external-link-advice.p2": "[I18N TESTING] If you are an optimist, you can attempt to open the link."
, "external-link-advice.p3": "[I18N TESTING] But we strongly recommend you to be reasonable and press your browser's back button."
, "library-button-text": "[I18N TESTING] Navigate to the welcome page" , "library-button-text": "[I18N TESTING] Navigate to the welcome page"
, "home-button-text": "[I18N TESTING] Jump to the main page of '{{BOOK_TITLE}}'" , "home-button-text": "[I18N TESTING] Jump to the main page of '{{BOOK_TITLE}}'"
, "random-page-button-text": "[I18N TESTING] I am tired of determinism" , "random-page-button-text": "[I18N TESTING] I am tired of determinism"

View File

@ -262,22 +262,7 @@ function handle_location_hash_change() {
} }
function translateErrorPageIfNeeded() { function translateErrorPageIfNeeded() {
const cw = contentIframe.contentWindow; translatePageInWindow(contentIframe.contentWindow);
if ( cw.KIWIX_RESPONSE_TEMPLATE && cw.KIWIX_RESPONSE_DATA ) {
const template = htmlDecode(cw.KIWIX_RESPONSE_TEMPLATE);
// cw.KIWIX_RESPONSE_DATA belongs to the iframe context and running
// I18n.render() on it directly in the top context doesn't work correctly
// because the type checks (obj.__proto__ == ???.prototype) in
// I18n.instantiateParameterizedMessages() always fail (String.prototype
// refers to different objects in different contexts).
// Work arround that issue by copying the object into our context.
const params = JSON.parse(JSON.stringify(cw.KIWIX_RESPONSE_DATA));
const html = I18n.render(template, params);
const htmlDoc = new DOMParser().parseFromString(html, "text/html");
cw.document.documentElement.innerHTML = htmlDoc.documentElement.innerHTML;
}
} }
function handle_content_url_change() { function handle_content_url_change() {

View File

@ -1,14 +1,32 @@
<!DOCTYPE html> <!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"> <html>
<head> <head>
<meta charset="UTF-8" /> <meta charset="utf-8">
<title>External link blocked</title> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
</head> <title>{{external_link_detected}}</title>
<body class="kiwix"> <link type="text/css" href="{{root}}/skin/error.css?KIWIXCACHEID" rel="Stylesheet" />
<h1>External link blocked</h1> <script type="module" src="{{root}}/skin/i18n.js?KIWIXCACHEID"></script>
<p>This instance of Kiwix protects you from accidentally going to external (out-of ZIM) links.</p> <script>
<p>If you intend to go to such locations, please click the link below.</p> window.KIWIX_RESPONSE_TEMPLATE = "{{KIWIX_RESPONSE_TEMPLATE}}";
<p><a href="{{ source }}">Go to {{ source }}</a></p> window.KIWIX_RESPONSE_DATA = {{{KIWIX_RESPONSE_DATA}}};
<div id="kiwixfooter">Powered by <a href="https://kiwix.org">Kiwix</a></div> </script>
</body> </head>
<body>
<header>
<img src="{{root}}/skin/blocklink.svg?KIWIXCACHEID"
alt="{{caution_warning}}"
aria-label="{{caution_warning}}"
title="{{caution_warning}}">
</header>
<section class="intro">
<h1>{{external_link_detected}}</h1>
<p>{{external_link_intro}}</p>
<p><a href="{{url}}">{{ url }}</a></p>
</section>
<section class="advice">
<p>{{advice.p1}}</p>
<p>{{advice.p2}}</p>
<p>{{advice.p3}}</p>
</section>
</body>
</html> </html>

View File

@ -0,0 +1,35 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>{{PAGE_TITLE}}</title>
<link type="text/css" href="{{root}}/skin/error.css?KIWIXCACHEID" rel="Stylesheet" />
{{#KIWIX_RESPONSE_DATA}} <script>
window.KIWIX_RESPONSE_TEMPLATE = "{{KIWIX_RESPONSE_TEMPLATE}}";
window.KIWIX_RESPONSE_DATA = {{{KIWIX_RESPONSE_DATA}}};
</script>{{/KIWIX_RESPONSE_DATA}}
</head>
<body>
<header>
<img src="{{root}}/skin/404.svg?KIWIXCACHEID"
alt="{{404_img_text}}"
aria-label="{{404_img_text}}"
title="{{404_img_text}}">
</header>
<section class="intro">
<h1>{{PAGE_HEADING}}</h1>
<p>{{path_was_not_found_msg}}</p>
<p><code>{{url_path}}</code></p>
</section>
<section class="advice">
<p>{{advice.p1}}</p>
<p class="list-intro">{{advice.p2}}</p>
<ul>
<li>{{advice.p3}}</li>
<li>{{advice.p4}}</li>
</ul>
<p>{{advice.p5}}</p>
</section>
</body>
</html>

View File

@ -58,8 +58,10 @@ const ResourceCollection resources200Compressible{
{ STATIC_CONTENT, "/ROOT%23%3F/skin/autoComplete/autoComplete.min.js?cacheid=1191aaaf" }, { STATIC_CONTENT, "/ROOT%23%3F/skin/autoComplete/autoComplete.min.js?cacheid=1191aaaf" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/autoComplete/css/autoComplete.css" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/autoComplete/css/autoComplete.css" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/autoComplete/css/autoComplete.css?cacheid=f2d376c4" }, { STATIC_CONTENT, "/ROOT%23%3F/skin/autoComplete/css/autoComplete.css?cacheid=f2d376c4" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/error.css" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/error.css?cacheid=b3fa90cf" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/i18n.js" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/i18n.js" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/i18n.js?cacheid=071abc9a" }, { STATIC_CONTENT, "/ROOT%23%3F/skin/i18n.js?cacheid=e9a10ac1" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/index.css" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/index.css" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/index.css?cacheid=ae79e41a" }, { STATIC_CONTENT, "/ROOT%23%3F/skin/index.css?cacheid=ae79e41a" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/index.js" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/index.js" },
@ -75,7 +77,7 @@ const ResourceCollection resources200Compressible{
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/taskbar.css" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/taskbar.css" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/taskbar.css?cacheid=80d56607" }, { STATIC_CONTENT, "/ROOT%23%3F/skin/taskbar.css?cacheid=80d56607" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/viewer.js" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/viewer.js" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/viewer.js?cacheid=d6f747f5" }, { STATIC_CONTENT, "/ROOT%23%3F/skin/viewer.js?cacheid=7f05bf6c" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/fonts/Poppins.ttf" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/fonts/Poppins.ttf" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/fonts/Poppins.ttf?cacheid=af705837" }, { STATIC_CONTENT, "/ROOT%23%3F/skin/fonts/Poppins.ttf?cacheid=af705837" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/fonts/Roboto.ttf" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/fonts/Roboto.ttf" },
@ -92,6 +94,8 @@ const ResourceCollection resources200Compressible{
{ DYNAMIC_CONTENT, "/ROOT%23%3F/catalog/v2/entries" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/catalog/v2/entries" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/catalog/v2/partial_entries" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/catalog/v2/partial_entries" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/catch/external?source=www.example.com" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/search?content=zimfile&pattern=a" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/search?content=zimfile&pattern=a" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/suggest?content=zimfile&term=ray" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/suggest?content=zimfile&term=ray" },
@ -106,10 +110,14 @@ const ResourceCollection resources200Compressible{
}; };
const ResourceCollection resources200Uncompressible{ const ResourceCollection resources200Uncompressible{
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/404.svg" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/404.svg?cacheid=b6d648af" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/bittorrent.png" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/bittorrent.png" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/bittorrent.png?cacheid=4f5c6882" }, { STATIC_CONTENT, "/ROOT%23%3F/skin/bittorrent.png?cacheid=4f5c6882" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/blank.html" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/blank.html" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/blank.html?cacheid=6b1fa032" }, { STATIC_CONTENT, "/ROOT%23%3F/skin/blank.html?cacheid=6b1fa032" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/blocklink.svg" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/blocklink.svg?cacheid=bd56b116" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/caret.png" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/caret.png" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/caret.png?cacheid=22b942b4" }, { STATIC_CONTENT, "/ROOT%23%3F/skin/caret.png?cacheid=22b942b4" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/download.png" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/download.png" },
@ -172,8 +180,6 @@ const ResourceCollection resources200Uncompressible{
{ DYNAMIC_CONTENT, "/ROOT%23%3F/catalog/v2/searchdescription.xml" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/catalog/v2/searchdescription.xml" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/catalog/v2/illustration/6f1d19d0-633f-087b-fb55-7ac324ff9baf?size=48" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/catalog/v2/illustration/6f1d19d0-633f-087b-fb55-7ac324ff9baf?size=48" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/catch/external?source=www.example.com" },
{ ZIM_CONTENT, "/ROOT%23%3F/content/zimfile/I/m/Ray_Charles_classic_piano_pose.jpg" }, { ZIM_CONTENT, "/ROOT%23%3F/content/zimfile/I/m/Ray_Charles_classic_piano_pose.jpg" },
{ ZIM_CONTENT, "/ROOT%23%3F/content/corner_cases%23%26/empty.html" }, { ZIM_CONTENT, "/ROOT%23%3F/content/corner_cases%23%26/empty.html" },
@ -290,7 +296,7 @@ R"EXPECTEDRESULT( href="/ROOT%23%3F/skin/kiwix.css?cacheid=3948b846"
<link rel="shortcut icon" href="/ROOT%23%3F/skin/favicon/favicon.ico?cacheid=92663314"> <link rel="shortcut icon" href="/ROOT%23%3F/skin/favicon/favicon.ico?cacheid=92663314">
<meta name="msapplication-config" content="/ROOT%23%3F/skin/favicon/browserconfig.xml?cacheid=f29a7c4a"> <meta name="msapplication-config" content="/ROOT%23%3F/skin/favicon/browserconfig.xml?cacheid=f29a7c4a">
<script type="text/javascript" src="./skin/polyfills.js?cacheid=a0e0343d"></script> <script type="text/javascript" src="./skin/polyfills.js?cacheid=a0e0343d"></script>
<script type="module" src="/ROOT%23%3F/skin/i18n.js?cacheid=071abc9a" defer></script> <script type="module" src="/ROOT%23%3F/skin/i18n.js?cacheid=e9a10ac1" defer></script>
<script type="text/javascript" src="/ROOT%23%3F/skin/languages.js?cacheid=a83f0e13" defer></script> <script type="text/javascript" src="/ROOT%23%3F/skin/languages.js?cacheid=a83f0e13" defer></script>
<script src="/ROOT%23%3F/skin/isotope.pkgd.min.js?cacheid=2e48d392" defer></script> <script src="/ROOT%23%3F/skin/isotope.pkgd.min.js?cacheid=2e48d392" defer></script>
<script src="/ROOT%23%3F/skin/iso6391To3.js?cacheid=ecde2bb3"></script> <script src="/ROOT%23%3F/skin/iso6391To3.js?cacheid=ecde2bb3"></script>
@ -325,9 +331,9 @@ R"EXPECTEDRESULT( <link type="text/css" href="./skin/kiwix.css?cacheid=3948b8
<link type="text/css" href="./skin/taskbar.css?cacheid=80d56607" rel="Stylesheet" /> <link type="text/css" href="./skin/taskbar.css?cacheid=80d56607" rel="Stylesheet" />
<link type="text/css" href="./skin/autoComplete/css/autoComplete.css?cacheid=f2d376c4" rel="Stylesheet" /> <link type="text/css" href="./skin/autoComplete/css/autoComplete.css?cacheid=f2d376c4" rel="Stylesheet" />
<script type="text/javascript" src="./skin/polyfills.js?cacheid=a0e0343d"></script> <script type="text/javascript" src="./skin/polyfills.js?cacheid=a0e0343d"></script>
<script type="module" src="./skin/i18n.js?cacheid=071abc9a" defer></script> <script type="module" src="./skin/i18n.js?cacheid=e9a10ac1" defer></script>
<script type="text/javascript" src="./skin/languages.js?cacheid=a83f0e13" defer></script> <script type="text/javascript" src="./skin/languages.js?cacheid=a83f0e13" defer></script>
<script type="text/javascript" src="./skin/viewer.js?cacheid=d6f747f5" defer></script> <script type="text/javascript" src="./skin/viewer.js?cacheid=7f05bf6c" defer></script>
<script type="text/javascript" src="./skin/autoComplete/autoComplete.min.js?cacheid=1191aaaf"></script> <script type="text/javascript" src="./skin/autoComplete/autoComplete.min.js?cacheid=1191aaaf"></script>
const blankPageUrl = root + "/skin/blank.html?cacheid=6b1fa032"; const blankPageUrl = root + "/skin/blank.html?cacheid=6b1fa032";
<label for="kiwix_button_show_toggle"><img src="./skin/caret.png?cacheid=22b942b4" alt=""></label> <label for="kiwix_button_show_toggle"><img src="./skin/caret.png?cacheid=22b942b4" alt=""></label>
@ -339,6 +345,21 @@ R"EXPECTEDRESULT( <link type="text/css" href="./skin/kiwix.css?cacheid=3948b8
/* url */ "/ROOT%23%3F/content/zimfile/A/index", /* url */ "/ROOT%23%3F/content/zimfile/A/index",
"" ""
}, },
{
/* url */ "/ROOT%23%3F/content/invalid-book/whatever",
R"EXPECTEDRESULT( <link type="text/css" href="/ROOT%23%3F/skin/error.css?cacheid=b3fa90cf" rel="Stylesheet" />
window.KIWIX_RESPONSE_TEMPLATE = "&lt;!DOCTYPE html&gt;\n&lt;html&gt;\n &lt;head&gt;\n &lt;meta charset=&quot;utf-8&quot;&gt;\n &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no&quot; /&gt;\n &lt;title&gt;{{PAGE_TITLE}}&lt;/title&gt;\n &lt;link type=&quot;text/css&quot; href=&quot;{{root}}/skin/error.css?cacheid=b3fa90cf&quot; rel=&quot;Stylesheet&quot; /&gt;\n{{#KIWIX_RESPONSE_DATA}} &lt;script&gt;\n window.KIWIX_RESPONSE_TEMPLATE = &quot;{{KIWIX_RESPONSE_TEMPLATE}}&quot;;\n window.KIWIX_RESPONSE_DATA = {{{KIWIX_RESPONSE_DATA}}};\n &lt;/script&gt;{{/KIWIX_RESPONSE_DATA}}\n &lt;/head&gt;\n &lt;body&gt;\n &lt;header&gt;\n &lt;img src=&quot;{{root}}/skin/404.svg?cacheid=b6d648af&quot;\n alt=&quot;{{404_img_text}}&quot;\n aria-label=&quot;{{404_img_text}}&quot;\n title=&quot;{{404_img_text}}&quot;&gt;\n &lt;/header&gt;\n &lt;section class=&quot;intro&quot;&gt;\n &lt;h1&gt;{{PAGE_HEADING}}&lt;/h1&gt;\n &lt;p&gt;{{path_was_not_found_msg}}&lt;/p&gt;\n &lt;p&gt;&lt;code&gt;{{url_path}}&lt;/code&gt;&lt;/p&gt;\n &lt;/section&gt;\n &lt;section class=&quot;advice&quot;&gt;\n &lt;p&gt;{{advice.p1}}&lt;/p&gt;\n &lt;p class=&quot;list-intro&quot;&gt;{{advice.p2}}&lt;/p&gt;\n &lt;ul&gt;\n &lt;li&gt;{{advice.p3}}&lt;/li&gt;\n &lt;li&gt;{{advice.p4}}&lt;/li&gt;\n &lt;/ul&gt;\n &lt;p&gt;{{advice.p5}}&lt;/p&gt;\n &lt;/section&gt;\n &lt;/body&gt;\n&lt;/html&gt;\n";
<img src="/ROOT%23%3F/skin/404.svg?cacheid=b6d648af"
)EXPECTEDRESULT"
},
{
/* url */ "/ROOT%23%3F/catch/external?source=https%3A%2F%2Fkiwix.org",
R"EXPECTEDRESULT( <link type="text/css" href="/ROOT%23%3F/skin/error.css?cacheid=b3fa90cf" rel="Stylesheet" />
<script type="module" src="/ROOT%23%3F/skin/i18n.js?cacheid=e9a10ac1"></script>
window.KIWIX_RESPONSE_TEMPLATE = "&lt;!DOCTYPE html&gt;\n&lt;html&gt;\n &lt;head&gt;\n &lt;meta charset=&quot;utf-8&quot;&gt;\n &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no&quot; /&gt;\n &lt;title&gt;{{external_link_detected}}&lt;/title&gt;\n &lt;link type=&quot;text/css&quot; href=&quot;{{root}}/skin/error.css?cacheid=b3fa90cf&quot; rel=&quot;Stylesheet&quot; /&gt;\n &lt;script type=&quot;module&quot; src=&quot;{{root}}/skin/i18n.js?cacheid=e9a10ac1&quot;&gt;&lt;/script&gt;\n &lt;script&gt;\n window.KIWIX_RESPONSE_TEMPLATE = &quot;{{KIWIX_RESPONSE_TEMPLATE}}&quot;;\n window.KIWIX_RESPONSE_DATA = {{{KIWIX_RESPONSE_DATA}}};\n &lt;/script&gt;\n &lt;/head&gt;\n &lt;body&gt;\n &lt;header&gt;\n &lt;img src=&quot;{{root}}/skin/blocklink.svg?cacheid=bd56b116&quot;\n alt=&quot;{{caution_warning}}&quot;\n aria-label=&quot;{{caution_warning}}&quot;\n title=&quot;{{caution_warning}}&quot;&gt;\n &lt;/header&gt;\n &lt;section class=&quot;intro&quot;&gt;\n &lt;h1&gt;{{external_link_detected}}&lt;/h1&gt;\n &lt;p&gt;{{external_link_intro}}&lt;/p&gt;\n &lt;p&gt;&lt;a href=&quot;{{url}}&quot;&gt;{{ url }}&lt;/a&gt;&lt;/p&gt;\n &lt;/section&gt;\n &lt;section class=&quot;advice&quot;&gt;\n &lt;p&gt;{{advice.p1}}&lt;/p&gt;\n &lt;p&gt;{{advice.p2}}&lt;/p&gt;\n &lt;p&gt;{{advice.p3}}&lt;/p&gt;\n &lt;/section&gt;\n &lt;/body&gt;\n&lt;/html&gt;\n";
<img src="/ROOT%23%3F/skin/blocklink.svg?cacheid=bd56b116"
)EXPECTEDRESULT"
},
{ {
// Searching in a ZIM file without a full-text index returns // Searching in a ZIM file without a full-text index returns
// a page rendered from static/templates/no_search_result_html // a page rendered from static/templates/no_search_result_html
@ -818,89 +839,6 @@ TEST_F(ServerTest, Http404HtmlError)
</p> </p>
)" }, )" },
{ /* url */ "/ROOT%23%3F/content/invalid-book/whatever",
expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "404-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "url-not-found", "params" : { "url" : "/ROOT%23%3F/content/invalid-book/whatever" } } }, { "p" : { "msgid" : "suggest-search", "params" : { "PATTERN" : "whatever", "SEARCH_URL" : "/ROOT%23%3F/search?pattern=whatever" } } } ] })" &&
expected_body==R"(
<h1>Not Found</h1>
<p>
The requested URL "/ROOT%23%3F/content/invalid-book/whatever" was not found on this server.
</p>
<p>
Make a full text search for <a href="/ROOT%23%3F/search?pattern=whatever">whatever</a>
</p>
)" },
{ /* url */ "/ROOT%23%3F/content/zimfile/invalid-article",
book_name=="zimfile" &&
book_title=="Ray Charles" &&
expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "404-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "url-not-found", "params" : { "url" : "/ROOT%23%3F/content/zimfile/invalid-article" } } }, { "p" : { "msgid" : "suggest-search", "params" : { "PATTERN" : "invalid-article", "SEARCH_URL" : "/ROOT%23%3F/search?content=zimfile&pattern=invalid-article" } } } ] })" &&
expected_body==R"(
<h1>Not Found</h1>
<p>
The requested URL "/ROOT%23%3F/content/zimfile/invalid-article" was not found on this server.
</p>
<p>
Make a full text search for <a href="/ROOT%23%3F/search?content=zimfile&pattern=invalid-article">invalid-article</a>
</p>
)" },
{ /* url */ R"(/ROOT%23%3F/content/"><svg onload=alert(1)>)",
expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "404-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "url-not-found", "params" : { "url" : "/ROOT%23%3F/content/\"><svg onload%3Dalert(1)>" } } }, { "p" : { "msgid" : "suggest-search", "params" : { "PATTERN" : "\"><svg onload=alert(1)>", "SEARCH_URL" : "/ROOT%23%3F/search?pattern=%22%3E%3Csvg%20onload%3Dalert(1)%3E" } } } ] })" &&
expected_body==R"(
<h1>Not Found</h1>
<p>
The requested URL "/ROOT%23%3F/content/&quot;&gt;&lt;svg onload%3Dalert(1)&gt;" was not found on this server.
</p>
<p>
Make a full text search for <a href="/ROOT%23%3F/search?pattern=%22%3E%3Csvg%20onload%3Dalert(1)%3E">&quot;&gt;&lt;svg onload=alert(1)&gt;</a>
</p>
)" },
{ /* url */ R"(/ROOT%23%3F/content/zimfile/"><svg onload=alert(1)>)",
book_name=="zimfile" &&
book_title=="Ray Charles" &&
expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "404-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "url-not-found", "params" : { "url" : "/ROOT%23%3F/content/zimfile/\"><svg onload%3Dalert(1)>" } } }, { "p" : { "msgid" : "suggest-search", "params" : { "PATTERN" : "\"><svg onload=alert(1)>", "SEARCH_URL" : "/ROOT%23%3F/search?content=zimfile&pattern=%22%3E%3Csvg%20onload%3Dalert(1)%3E" } } } ] })" &&
expected_body==R"(
<h1>Not Found</h1>
<p>
The requested URL "/ROOT%23%3F/content/zimfile/&quot;&gt;&lt;svg onload%3Dalert(1)&gt;" was not found on this server.
</p>
<p>
Make a full text search for <a href="/ROOT%23%3F/search?content=zimfile&pattern=%22%3E%3Csvg%20onload%3Dalert(1)%3E">&quot;&gt;&lt;svg onload=alert(1)&gt;</a>
</p>
)" },
// XXX: This test case is against a "</script>" string appearing inside
// XXX: javascript code that will confuse the HTML parser
{ /* url */ R"(/ROOT%23%3F/content/zimfile/</script>)",
book_name=="zimfile" &&
book_title=="Ray Charles" &&
expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "404-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "url-not-found", "params" : { "url" : "/ROOT%23%3F/content/zimfile/</scr\ipt>" } } }, { "p" : { "msgid" : "suggest-search", "params" : { "PATTERN" : "script>", "SEARCH_URL" : "/ROOT%23%3F/search?content=zimfile&pattern=script%3E" } } } ] })" &&
expected_body==R"(
<h1>Not Found</h1>
<p>
The requested URL "/ROOT%23%3F/content/zimfile/&lt;/script&gt;" was not found on this server.
</p>
<p>
Make a full text search for <a href="/ROOT%23%3F/search?content=zimfile&pattern=script%3E">script&gt;</a>
</p>
)" },
{ /* url */ "/ROOT%23%3F/content/zimfile/invalid-article?userlang=test",
expected_page_title=="[I18N TESTING] Not Found - Try Again" &&
book_name=="zimfile" &&
book_title=="Ray Charles" &&
expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "404-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "url-not-found", "params" : { "url" : "/ROOT%23%3F/content/zimfile/invalid-article" } } }, { "p" : { "msgid" : "suggest-search", "params" : { "PATTERN" : "invalid-article", "SEARCH_URL" : "/ROOT%23%3F/search?content=zimfile&pattern=invalid-article" } } } ] })" &&
expected_body==R"(
<h1>[I18N TESTING] Content not found, but at least the server is alive</h1>
<p>
[I18N TESTING] URL not found: /ROOT%23%3F/content/zimfile/invalid-article
</p>
<p>
[I18N TESTING] Make a full text search for <a href="/ROOT%23%3F/search?content=zimfile&pattern=invalid-article">invalid-article</a>
</p>
)" },
{ /* url */ "/ROOT%23%3F/raw/no-such-book/meta/Title", { /* url */ "/ROOT%23%3F/raw/no-such-book/meta/Title",
expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "404-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "url-not-found", "params" : { "url" : "/ROOT%23%3F/raw/no-such-book/meta/Title" } } }, { "p" : { "msgid" : "no-such-book", "params" : { "BOOK_NAME" : "no-such-book" } } } ] })" && expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "404-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "url-not-found", "params" : { "url" : "/ROOT%23%3F/raw/no-such-book/meta/Title" } } }, { "p" : { "msgid" : "no-such-book", "params" : { "BOOK_NAME" : "no-such-book" } } } ] })" &&
expected_body==R"( expected_body==R"(
@ -971,6 +909,131 @@ TEST_F(ServerTest, Http404HtmlError)
} }
} }
std::string htmlEscape(std::string s)
{
s = replace(s, "&", "&amp;");
s = replace(s, "<", "&lt;");
s = replace(s, ">", "&gt;");
s = replace(s, "\"", "&quot;");
return s;
}
std::string escapeJsString(std::string s)
{
s = replace(s, "</script>", "</scr\\ipt>");
s = replace(s, "\"", "\\\"");
return s;
}
std::string expectedSexy404ErrorHtml(const std::string& url)
{
const auto urlWithoutQuery = replace(url, "\\?.*$", "");
const auto htmlSafeUrl = htmlEscape(urlWithoutQuery);
const auto jsSafeUrl = escapeJsString(urlWithoutQuery);
const std::string englishText[] = {
"Page not found",
"Not found!",
"Oops. Page not found.",
"The requested path was not found:",
"The content you&apos;re looking for may still be available, but it might be located at a different place within the ZIM file.",
"Please:",
"Try using the search function to find the content you want",
"Look for keywords or titles related to the information you&apos;re seeking",
"This approach should help you locate the desired content, even if the original link isn&apos;t working properly."
};
const std::string translatedText[] = {
"Page [I18N] not [TESTING] found",
"[I18N] Not found! [TESTING]",
"[I18N TESTING] Oops. Larry Page could not be reached. He may be on paternity leave.",
"[I18N TESTING] The requested path was not found (in fact, nothing was found instead, either):",
"Sh*t happens. [I18N TESTING] Take it easy!",
"[I18N TESTING] Try one of the following:",
"[I18N TESTING] Check the spelling of the URL path",
"[I18N TESTING] Press the dice button",
"Good luck! [I18N TESTING]"
};
const bool shouldTranslate = url.find("userlang=test") != std::string::npos;
const std::string* const t = shouldTranslate ? translatedText : englishText;
return R"RAWSTRINGLITERAL(<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>)RAWSTRINGLITERAL" + t[0] + R"RAWSTRINGLITERAL(</title>
<link type="text/css" href="/ROOT%23%3F/skin/error.css?cacheid=b3fa90cf" rel="Stylesheet" />
<script>
window.KIWIX_RESPONSE_TEMPLATE = "&lt;!DOCTYPE html&gt;\n&lt;html&gt;\n &lt;head&gt;\n &lt;meta charset=&quot;utf-8&quot;&gt;\n &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no&quot; /&gt;\n &lt;title&gt;{{PAGE_TITLE}}&lt;/title&gt;\n &lt;link type=&quot;text/css&quot; href=&quot;{{root}}/skin/error.css?cacheid=b3fa90cf&quot; rel=&quot;Stylesheet&quot; /&gt;\n{{#KIWIX_RESPONSE_DATA}} &lt;script&gt;\n window.KIWIX_RESPONSE_TEMPLATE = &quot;{{KIWIX_RESPONSE_TEMPLATE}}&quot;;\n window.KIWIX_RESPONSE_DATA = {{{KIWIX_RESPONSE_DATA}}};\n &lt;/script&gt;{{/KIWIX_RESPONSE_DATA}}\n &lt;/head&gt;\n &lt;body&gt;\n &lt;header&gt;\n &lt;img src=&quot;{{root}}/skin/404.svg?cacheid=b6d648af&quot;\n alt=&quot;{{404_img_text}}&quot;\n aria-label=&quot;{{404_img_text}}&quot;\n title=&quot;{{404_img_text}}&quot;&gt;\n &lt;/header&gt;\n &lt;section class=&quot;intro&quot;&gt;\n &lt;h1&gt;{{PAGE_HEADING}}&lt;/h1&gt;\n &lt;p&gt;{{path_was_not_found_msg}}&lt;/p&gt;\n &lt;p&gt;&lt;code&gt;{{url_path}}&lt;/code&gt;&lt;/p&gt;\n &lt;/section&gt;\n &lt;section class=&quot;advice&quot;&gt;\n &lt;p&gt;{{advice.p1}}&lt;/p&gt;\n &lt;p class=&quot;list-intro&quot;&gt;{{advice.p2}}&lt;/p&gt;\n &lt;ul&gt;\n &lt;li&gt;{{advice.p3}}&lt;/li&gt;\n &lt;li&gt;{{advice.p4}}&lt;/li&gt;\n &lt;/ul&gt;\n &lt;p&gt;{{advice.p5}}&lt;/p&gt;\n &lt;/section&gt;\n &lt;/body&gt;\n&lt;/html&gt;\n";
window.KIWIX_RESPONSE_DATA = { "404_img_text" : { "msgid" : "404-img-text", "params" : { } }, "PAGE_HEADING" : { "msgid" : "new-404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "new-404-page-title", "params" : { } }, "advice" : { "p1" : { "msgid" : "404-advice.p1", "params" : { } }, "p2" : { "msgid" : "404-advice.p2", "params" : { } }, "p3" : { "msgid" : "404-advice.p3", "params" : { } }, "p4" : { "msgid" : "404-advice.p4", "params" : { } }, "p5" : { "msgid" : "404-advice.p5", "params" : { } } }, "path_was_not_found_msg" : { "msgid" : "path-was-not-found", "params" : { } }, "root" : "/ROOT%23%3F", "url_path" : ")RAWSTRINGLITERAL"
+ // inject the URL
jsSafeUrl // inject the URL
+ // inject the URL
R"RAWSTRINGLITERAL(" };
</script>
</head>
<body>
<header>
<img src="/ROOT%23%3F/skin/404.svg?cacheid=b6d648af"
alt=")RAWSTRINGLITERAL" + t[1] + R"RAWSTRINGLITERAL("
aria-label=")RAWSTRINGLITERAL" + t[1] + R"RAWSTRINGLITERAL("
title=")RAWSTRINGLITERAL" + t[1] + R"RAWSTRINGLITERAL(">
</header>
<section class="intro">
<h1>)RAWSTRINGLITERAL" + t[2] + R"RAWSTRINGLITERAL(</h1>
<p>)RAWSTRINGLITERAL" + t[3] + R"RAWSTRINGLITERAL(</p>
<p><code>)RAWSTRINGLITERAL"
+ // inject the URL
htmlSafeUrl // inject the URL
+ // inject the URL
R"RAWSTRINGLITERAL(</code></p>
</section>
<section class="advice">
<p>)RAWSTRINGLITERAL" + t[4] + R"RAWSTRINGLITERAL(</p>
<p class="list-intro">)RAWSTRINGLITERAL" + t[5] + R"RAWSTRINGLITERAL(</p>
<ul>
<li>)RAWSTRINGLITERAL" + t[6] + R"RAWSTRINGLITERAL(</li>
<li>)RAWSTRINGLITERAL" + t[7] + R"RAWSTRINGLITERAL(</li>
</ul>
<p>)RAWSTRINGLITERAL" + t[8] + R"RAWSTRINGLITERAL(</p>
</section>
</body>
</html>
)RAWSTRINGLITERAL";
}
TEST_F(ServerTest, HttpSexy404HtmlError)
{
using namespace TestingOfHtmlResponses;
const std::vector<std::string> testUrls {
// XXX: Nicer 404 error page no longer hints whether the error
// XXX: is because of the missing book/ZIM-file or a missing article
// XXX: inside a valid/existing book/ZIM-file. However it makes sense
// XXX: to preserve both cases.
"/ROOT%23%3F/content/invalid-book/whatever",
"/ROOT%23%3F/content/invalid-book/whatever?userlang=test",
"/ROOT%23%3F/content/zimfile/invalid-article",
"/ROOT%23%3F/content/zimfile/invalid-article?userlang=test",
// malicious URLs
R"(/ROOT%23%3F/content/"><svg onload=alert(1)>)",
R"(/ROOT%23%3F/content/zimfile/"><svg onload=alert(1)>)",
// XXX: This test case is against a "</script>" string appearing inside
// XXX: javascript code that will confuse the HTML parser
R"(/ROOT%23%3F/content/zimfile/</script>)",
};
for ( const auto& url : testUrls ) {
const TestContext ctx{ {"url", url} };
const auto r = zfs1_->GET(url.c_str());
EXPECT_EQ(r->status, 404) << ctx;
EXPECT_EQ(r->body, expectedSexy404ErrorHtml(url)) << ctx;
}
}
TEST_F(ServerTest, Http400HtmlError) TEST_F(ServerTest, Http400HtmlError)
{ {
using namespace TestingOfHtmlResponses; using namespace TestingOfHtmlResponses;
@ -1437,37 +1500,37 @@ TEST_F(ServerTest, UserLanguageControl)
"Default user language is English", "Default user language is English",
/*url*/ "/ROOT%23%3F/content/zimfile/invalid-article", /*url*/ "/ROOT%23%3F/content/zimfile/invalid-article",
/*Accept-Language:*/ "", /*Accept-Language:*/ "",
/* expected <h1> */ "Not Found" /* expected <h1> */ "Oops. Page not found."
}, },
{ {
"userlang URL query parameter is respected", "userlang URL query parameter is respected",
/*url*/ "/ROOT%23%3F/content/zimfile/invalid-article?userlang=en", /*url*/ "/ROOT%23%3F/content/zimfile/invalid-article?userlang=en",
/*Accept-Language:*/ "", /*Accept-Language:*/ "",
/* expected <h1> */ "Not Found" /* expected <h1> */ "Oops. Page not found."
}, },
{ {
"userlang URL query parameter is respected", "userlang URL query parameter is respected",
/*url*/ "/ROOT%23%3F/content/zimfile/invalid-article?userlang=test", /*url*/ "/ROOT%23%3F/content/zimfile/invalid-article?userlang=test",
/*Accept-Language:*/ "", /*Accept-Language:*/ "",
/* expected <h1> */ "[I18N TESTING] Content not found, but at least the server is alive" /* expected <h1> */ "[I18N TESTING] Oops. Larry Page could not be reached. He may be on paternity leave."
}, },
{ {
"'Accept-Language: *' is handled", "'Accept-Language: *' is handled",
/*url*/ "/ROOT%23%3F/content/zimfile/invalid-article", /*url*/ "/ROOT%23%3F/content/zimfile/invalid-article",
/*Accept-Language:*/ "*", /*Accept-Language:*/ "*",
/* expected <h1> */ "Not Found" /* expected <h1> */ "Oops. Page not found."
}, },
{ {
"Accept-Language: header is respected", "Accept-Language: header is respected",
/*url*/ "/ROOT%23%3F/content/zimfile/invalid-article", /*url*/ "/ROOT%23%3F/content/zimfile/invalid-article",
/*Accept-Language:*/ "test", /*Accept-Language:*/ "test",
/* expected <h1> */ "[I18N TESTING] Content not found, but at least the server is alive" /* expected <h1> */ "[I18N TESTING] Oops. Larry Page could not be reached. He may be on paternity leave."
}, },
{ {
"userlang query parameter takes precedence over Accept-Language", "userlang query parameter takes precedence over Accept-Language",
/*url*/ "/ROOT%23%3F/content/zimfile/invalid-article?userlang=en", /*url*/ "/ROOT%23%3F/content/zimfile/invalid-article?userlang=en",
/*Accept-Language:*/ "test", /*Accept-Language:*/ "test",
/* expected <h1> */ "Not Found" /* expected <h1> */ "Oops. Page not found."
}, },
{ {
"Most suitable language is selected from the Accept-Language header", "Most suitable language is selected from the Accept-Language header",
@ -1475,7 +1538,7 @@ TEST_F(ServerTest, UserLanguageControl)
// with quality values) the most suitable language is selected. // with quality values) the most suitable language is selected.
/*url*/ "/ROOT%23%3F/content/zimfile/invalid-article", /*url*/ "/ROOT%23%3F/content/zimfile/invalid-article",
/*Accept-Language:*/ "test;q=0.9, en;q=0.2", /*Accept-Language:*/ "test;q=0.9, en;q=0.2",
/* expected <h1> */ "[I18N TESTING] Content not found, but at least the server is alive" /* expected <h1> */ "[I18N TESTING] Oops. Larry Page could not be reached. He may be on paternity leave."
}, },
{ {
"Most suitable language is selected from the Accept-Language header", "Most suitable language is selected from the Accept-Language header",
@ -1483,7 +1546,7 @@ TEST_F(ServerTest, UserLanguageControl)
// with quality values) the most suitable language is selected. // with quality values) the most suitable language is selected.
/*url*/ "/ROOT%23%3F/content/zimfile/invalid-article", /*url*/ "/ROOT%23%3F/content/zimfile/invalid-article",
/*Accept-Language:*/ "test;q=0.2, en;q=0.9", /*Accept-Language:*/ "test;q=0.2, en;q=0.9",
/* expected <h1> */ "Not Found" /* expected <h1> */ "Oops. Page not found."
}, },
}; };