diff --git a/src/server/i18n.cpp b/src/server/i18n.cpp index 0a2cd8c73..5b948f26e 100644 --- a/src/server/i18n.cpp +++ b/src/server/i18n.cpp @@ -112,8 +112,12 @@ std::string expandParameterizedString(const std::string& lang, const std::string& key, const Parameters& params) { + kainjow::mustache::object mustacheParams; + for( const auto& kv : params ) { + mustacheParams[kv.first] = kv.second; + } const std::string tmpl = getTranslatedString(lang, key); - return render_template(tmpl, params); + return render_template(tmpl, mustacheParams); } } // namespace i18n diff --git a/src/server/i18n.h b/src/server/i18n.h index 79005b721..1e42cac68 100644 --- a/src/server/i18n.h +++ b/src/server/i18n.h @@ -20,6 +20,7 @@ #ifndef KIWIX_SERVER_I18N #define KIWIX_SERVER_I18N +#include #include #include @@ -44,7 +45,7 @@ std::string getTranslatedString(const std::string& lang, const std::string& key) namespace i18n { -typedef kainjow::mustache::object Parameters; +typedef std::map Parameters; std::string expandParameterizedString(const std::string& lang, const std::string& key, @@ -93,10 +94,10 @@ class GetTranslatedStringWithMsgId } // namespace i18n -struct ParameterizedMessage +class ParameterizedMessage { public: // types - typedef kainjow::mustache::object Parameters; + typedef i18n::Parameters Parameters; public: // functions ParameterizedMessage(const std::string& msgId, const Parameters& params) @@ -106,6 +107,9 @@ struct ParameterizedMessage std::string getText(const std::string& lang) const; + const std::string& getMsgId() const { return msgId; } + const Parameters& getParams() const { return params; } + private: // data const std::string msgId; const Parameters params; diff --git a/src/server/internalServer.cpp b/src/server/internalServer.cpp index 72b71b044..2c1ea518c 100644 --- a/src/server/internalServer.cpp +++ b/src/server/internalServer.cpp @@ -513,6 +513,19 @@ static MHD_Result staticHandlerCallback(void* cls, cont_cls); } +namespace +{ + +MHD_Result add_name_value_pair(void *nvp, enum MHD_ValueKind kind, + const char *key, const char *value) +{ + auto& nameValuePairs = *reinterpret_cast(nvp); + nameValuePairs.push_back({key, value}); + return MHD_YES; +} + +} // unnamed namespace + MHD_Result InternalServer::handlerCallback(struct MHD_Connection* connection, const char* fullUrl, const char* method, @@ -529,7 +542,10 @@ MHD_Result InternalServer::handlerCallback(struct MHD_Connection* connection, } const auto url = fullURL2LocalURL(fullUrl, m_rootPrefixOfDecodedURL); - RequestContext request(connection, m_root, url, method, version); + RequestContext::NameValuePairs headers, queryArgs; + MHD_get_connection_values(connection, MHD_HEADER_KIND, add_name_value_pair, &headers); + MHD_get_connection_values(connection, MHD_GET_ARGUMENT_KIND, add_name_value_pair, &queryArgs); + RequestContext request(m_root, url, method, version, headers, queryArgs); if (m_verbose.load() ) { request.print_debug_info(); @@ -926,7 +942,8 @@ std::unique_ptr InternalServer::handle_search_request(const RequestCon HTTPErrorResponse response(request, MHD_HTTP_NOT_FOUND, "fulltext-search-unavailable", "404-page-heading", - cssUrl); + cssUrl, + /*includeKiwixResponseData=*/true); response += nonParameterizedMessage("no-search-results"); // XXX: Now this has to be handled by the iframe-based viewer which // XXX: has to resolve if the book selection resulted in a single book. diff --git a/src/microhttpd_wrapper.h b/src/server/microhttpd_wrapper.h similarity index 100% rename from src/microhttpd_wrapper.h rename to src/server/microhttpd_wrapper.h diff --git a/src/server/request_context.cpp b/src/server/request_context.cpp index 91ff7a72d..d879240cd 100644 --- a/src/server/request_context.cpp +++ b/src/server/request_context.cpp @@ -51,11 +51,12 @@ RequestMethod str2RequestMethod(const std::string& method) { } // unnamed namespace -RequestContext::RequestContext(struct MHD_Connection* connection, - const std::string& _rootLocation, // URI-encoded +RequestContext::RequestContext(const std::string& _rootLocation, // URI-encoded const std::string& unrootedUrl, // URI-decoded const std::string& _method, - const std::string& version) : + const std::string& version, + const NameValuePairs& headers, + const NameValuePairs& queryArgs) : rootLocation(_rootLocation), url(unrootedUrl), method(str2RequestMethod(_method)), @@ -64,9 +65,13 @@ RequestContext::RequestContext(struct MHD_Connection* connection, acceptEncodingGzip(false), byteRange_() { - MHD_get_connection_values(connection, MHD_HEADER_KIND, &RequestContext::fill_header, this); - MHD_get_connection_values(connection, MHD_GET_ARGUMENT_KIND, &RequestContext::fill_argument, this); - MHD_get_connection_values(connection, MHD_COOKIE_KIND, &RequestContext::fill_cookie, this); + for ( const auto& kv : headers ) { + add_header(kv.first, kv.second); + } + + for ( const auto& kv : queryArgs ) { + add_argument(kv.first, kv.second); + } try { acceptEncodingGzip = @@ -83,18 +88,14 @@ RequestContext::RequestContext(struct MHD_Connection* connection, RequestContext::~RequestContext() {} -MHD_Result RequestContext::fill_header(void *__this, enum MHD_ValueKind kind, - const char *key, const char *value) +void RequestContext::add_header(const char *key, const char *value) { - RequestContext *_this = static_cast(__this); - _this->headers[lcAll(key)] = value; - return MHD_YES; + this->headers[lcAll(key)] = value; } -MHD_Result RequestContext::fill_argument(void *__this, enum MHD_ValueKind kind, - const char *key, const char* value) +void RequestContext::add_argument(const char *key, const char* value) { - RequestContext *_this = static_cast(__this); + RequestContext *_this = this; _this->arguments[key].push_back(value == nullptr ? "" : value); if ( ! _this->queryString.empty() ) { _this->queryString += "&"; @@ -104,15 +105,6 @@ MHD_Result RequestContext::fill_argument(void *__this, enum MHD_ValueKind kind, _this->queryString += "="; _this->queryString += urlEncode(value); } - return MHD_YES; -} - -MHD_Result RequestContext::fill_cookie(void *__this, enum MHD_ValueKind kind, - const char *key, const char* value) -{ - RequestContext *_this = static_cast(__this); - _this->cookies[key] = value == nullptr ? "" : value; - return MHD_YES; } void RequestContext::print_debug_info() const { diff --git a/src/server/request_context.h b/src/server/request_context.h index d5ab7b515..d4fe53dd5 100644 --- a/src/server/request_context.h +++ b/src/server/request_context.h @@ -29,7 +29,7 @@ #include #include "byte_range.h" -#include "tools/stringTools.h" +#include "../tools/stringTools.h" extern "C" { #include "microhttpd_wrapper.h" @@ -55,12 +55,17 @@ class IndexError: public std::runtime_error {}; class RequestContext { + public: // types + typedef std::vector> NameValuePairs; + public: // functions - RequestContext(struct MHD_Connection* connection, - const std::string& rootLocation, // URI-encoded + RequestContext(const std::string& rootLocation, // URI-encoded const std::string& unrootedUrl, // URI-decoded const std::string& method, - const std::string& version); + const std::string& version, + const NameValuePairs& headers, + const NameValuePairs& queryArgs); + ~RequestContext(); void print_debug_info() const; @@ -145,16 +150,14 @@ class RequestContext { ByteRange byteRange_; std::map headers; std::map> arguments; - std::map cookies; std::string queryString; UserLanguage userlang; private: // functions UserLanguage determine_user_language() const; - static MHD_Result fill_header(void *, enum MHD_ValueKind, const char*, const char*); - static MHD_Result fill_cookie(void *, enum MHD_ValueKind, const char*, const char*); - static MHD_Result fill_argument(void *, enum MHD_ValueKind, const char*, const char*); + void add_header(const char* name, const char* value); + void add_argument(const char* name, const char* value); }; template<> std::string RequestContext::get_argument(const std::string& name) const; diff --git a/src/server/response.cpp b/src/server/response.cpp index 8ccab1313..7520ab1cd 100644 --- a/src/server/response.cpp +++ b/src/server/response.cpp @@ -32,6 +32,9 @@ #include #include +#include +#include +#include // This is somehow a magic value. // If this value is too small, we will compress (and lost cpu time) too much @@ -47,6 +50,8 @@ namespace kiwix { namespace { +typedef kainjow::mustache::data MustacheData; + // some utilities std::string get_mime_type(const zim::Item& item) @@ -151,14 +156,214 @@ std::unique_ptr Response::build_304(const ETag& etag) return response; } -std::string ContentResponseBlueprint::getMessage(const std::string& msgId) const + +namespace { - return getTranslatedString(m_request.get_user_language(), msgId); + +// This class was introduced in order to work around the missing support +// for std::variant (and std::optional) under some of the current build +// platforms. +template +class Optional +{ +public: // functions + Optional() {} + Optional(const T& t) : ptr(new T(t)) {} + Optional(const Optional& o) : ptr(o.has_value() ? new T(*o) : nullptr) {} + Optional(Optional&& o) : ptr(std::move(o.ptr)) {} + + Optional& operator=(const Optional& o) + { + *this = Optional(o); + return *this; + } + + Optional& operator=(Optional&& o) + { + ptr = std::move(o.ptr); + return *this; + } + + bool has_value() const { return ptr.get() != nullptr; } + const T& operator*() const { return *ptr; } + T& operator*() { return *ptr; } + +private: // data + std::unique_ptr ptr; +}; + +} // unnamed namespace + +class ContentResponseBlueprint::Data +{ +public: + typedef std::list List; + typedef std::map Object; + +private: + // std::variant data; + // XXX: libkiwix is compiled on platforms where std::variant + // XXX: is not yet supported. Hence this hack. Only one + // XXX: of the below data members is expected to contain a value. + Optional m_stringValue; + Optional m_boolValue; + Optional m_listValue; + Optional m_objectValue; + +public: + Data() {} + Data(const std::string& s) : m_stringValue(s) {} + Data(bool b) : m_boolValue(b) {} + Data(const List& l) : m_listValue(l) {} + Data(const Object& o) : m_objectValue(o) {} + + MustacheData toMustache(const std::string& lang) const; + + Data& operator[](const std::string& key) + { + return (*m_objectValue)[key]; + } + + void push_back(const Data& d) { (*m_listValue).push_back(d); } + + static Data onlyAsNonEmptyValue(const std::string& s) + { + return s.empty() ? Data(false) : Data(s); + } + + static Data from(const ParameterizedMessage& pmsg) + { + Object obj; + for(const auto& kv : pmsg.getParams()) { + obj[kv.first] = kv.second; + } + return Object{ + { "msgid", pmsg.getMsgId() }, + { "params", Data(obj) } + }; + } + + std::string asJSON() const; + void dumpJSON(std::ostream& os) const; + +private: + bool isString() const { return m_stringValue.has_value(); } + bool isList() const { return m_listValue.has_value(); } + bool isObject() const { return m_objectValue.has_value(); } + + const std::string& stringValue() const { return *m_stringValue; } + bool boolValue() const { return *m_boolValue; } + const List& listValue() const { return *m_listValue; } + const Object& objectValue() const { return *m_objectValue; } + + const Data* get(const std::string& key) const + { + if ( !isObject() ) + return nullptr; + + const auto& obj = objectValue(); + const auto it = obj.find(key); + return it != obj.end() ? &it->second : nullptr; + } +}; + +MustacheData ContentResponseBlueprint::Data::toMustache(const std::string& lang) const +{ + if ( this->isList() ) { + kainjow::mustache::list l; + for ( const auto& x : this->listValue() ) { + l.push_back(x.toMustache(lang)); + } + return l; + } else if ( this->isObject() ) { + const Data* msgId = this->get("msgid"); + const Data* msgParams = this->get("params"); + if ( msgId && msgId->isString() && msgParams && msgParams->isObject() ) { + std::map params; + for(const auto& kv : msgParams->objectValue()) { + params[kv.first] = kv.second.stringValue(); + } + const ParameterizedMessage msg(msgId->stringValue(), ParameterizedMessage::Parameters(params)); + return msg.getText(lang); + } else { + kainjow::mustache::object o; + for ( const auto& kv : this->objectValue() ) { + o[kv.first] = kv.second.toMustache(lang); + } + return o; + } + } else if ( this->isString() ) { + return this->stringValue(); + } else { + return this->boolValue(); + } } +void ContentResponseBlueprint::Data::dumpJSON(std::ostream& os) const +{ + if ( this->isString() ) { + os << '"' << escapeForJSON(this->stringValue()) << '"'; + } else if ( this->isList() ) { + const char * sep = " "; + os << "["; + + for ( const auto& x : this->listValue() ) { + os << sep; + x.dumpJSON(os); + sep = ", "; + } + os << " ]"; + } else if ( this->isObject() ) { + const char * sep = " "; + os << "{"; + for ( const auto& kv : this->objectValue() ) { + os << sep << '"' << kv.first << "\" : "; + kv.second.dumpJSON(os); + sep = ", "; + } + os << " }"; + } else { + os << (this->boolValue() ? "true" : "false"); + } +} + +std::string ContentResponseBlueprint::Data::asJSON() const +{ + std::ostringstream oss; + this->dumpJSON(oss); + + // This JSON is going to be used in HTML inside a tag. + // If it contains "" (or "") as a substring, then the HTML + // parser will be confused. Since for a valid JSON that may happen only inside + // a JSON string, we can safely take advantage of the answers to + // https://stackoverflow.com/questions/28259389/how-to-put-script-in-a-javascript-string + // and work around the issue by inserting an otherwise harmless backslash. + return std::regex_replace(oss.str(), std::regex(" ContentResponseBlueprint::generateResponseObject() const { - auto r = ContentResponse::build(m_template, m_data, m_mimeType); + kainjow::mustache::data d = m_data->toMustache(m_request.get_user_language()); + if ( m_includeKiwixResponseData ) { + d.set("KIWIX_RESPONSE_TEMPLATE", escapeForJSON(m_template, false)); + d.set("KIWIX_RESPONSE_DATA", m_data->asJSON()); + } + auto r = ContentResponse::build(m_template, d, m_mimeType); r->set_code(m_httpStatusCode); return r; } @@ -167,26 +372,30 @@ HTTPErrorResponse::HTTPErrorResponse(const RequestContext& request, int httpStatusCode, const std::string& pageTitleMsgId, const std::string& headingMsgId, - const std::string& cssUrl) + const std::string& cssUrl, + bool includeKiwixResponseData) : ContentResponseBlueprint(&request, httpStatusCode, request.get_requested_format() == "html" ? "text/html; charset=utf-8" : "application/xml; charset=utf-8", - request.get_requested_format() == "html" ? RESOURCE::templates::error_html : RESOURCE::templates::error_xml) -{ - kainjow::mustache::list emptyList; - this->m_data = kainjow::mustache::object{ - {"CSS_URL", onlyAsNonEmptyMustacheValue(cssUrl) }, - {"PAGE_TITLE", getMessage(pageTitleMsgId)}, - {"PAGE_HEADING", getMessage(headingMsgId)}, + request.get_requested_format() == "html" ? RESOURCE::templates::error_html : RESOURCE::templates::error_xml, + includeKiwixResponseData) +{ + Data::List emptyList; + *this->m_data = Data(Data::Object{ + {"CSS_URL", Data::onlyAsNonEmptyValue(cssUrl) }, + {"PAGE_TITLE", Data::from(nonParameterizedMessage(pageTitleMsgId))}, + {"PAGE_HEADING", Data::from(nonParameterizedMessage(headingMsgId))}, {"details", emptyList} - }; + }); } HTTP404Response::HTTP404Response(const RequestContext& request) : HTTPErrorResponse(request, MHD_HTTP_NOT_FOUND, "404-page-title", - "404-page-heading") + "404-page-heading", + std::string(), + /*includeKiwixResponseData=*/true) { } @@ -199,8 +408,7 @@ UrlNotFoundResponse::UrlNotFoundResponse(const RequestContext& request) HTTPErrorResponse& HTTPErrorResponse::operator+(const ParameterizedMessage& details) { - const std::string msg = details.getText(m_request.get_user_language()); - m_data["details"].push_back({"p", msg}); + (*m_data)["details"].push_back(Data::Object{{"p", Data::from(details)}}); return *this; } @@ -215,7 +423,9 @@ HTTP400Response::HTTP400Response(const RequestContext& request) : HTTPErrorResponse(request, MHD_HTTP_BAD_REQUEST, "400-page-title", - "400-page-heading") + "400-page-heading", + std::string(), + /*includeKiwixResponseData=*/true) { std::string requestUrl = urlDecode(m_request.get_full_url(), false); const auto query = m_request.get_query(); @@ -229,19 +439,13 @@ HTTP500Response::HTTP500Response(const RequestContext& request) : HTTPErrorResponse(request, MHD_HTTP_INTERNAL_SERVER_ERROR, "500-page-title", - "500-page-heading") + "500-page-heading", + std::string(), + /*includeKiwixResponseData=*/true) { *this += nonParameterizedMessage("500-page-text"); } -std::unique_ptr HTTP500Response::generateResponseObject() const -{ - const std::string mimeType = "text/html;charset=utf-8"; - auto r = ContentResponse::build(m_template, m_data, mimeType); - r->set_code(m_httpStatusCode); - return r; -} - std::unique_ptr Response::build_416(size_t resourceLength) { auto response = Response::build(); diff --git a/src/server/response.h b/src/server/response.h index b1636fa9c..11808f0da 100644 --- a/src/server/response.h +++ b/src/server/response.h @@ -101,6 +101,9 @@ class ContentResponse : public Response { kainjow::mustache::data data, const std::string& mimetype); + const std::string& getContent() const { return m_content; } + const std::string& getMimeType() const { return m_mimeType; } + private: MHD_Response* create_mhd_response(const RequestContext& request); @@ -118,31 +121,28 @@ class ContentResponseBlueprint ContentResponseBlueprint(const RequestContext* request, int httpStatusCode, const std::string& mimeType, - const std::string& templateStr) - : m_request(*request) - , m_httpStatusCode(httpStatusCode) - , m_mimeType(mimeType) - , m_template(templateStr) - {} + const std::string& templateStr, + bool includeKiwixResponseData = false); - virtual ~ContentResponseBlueprint() = default; + ~ContentResponseBlueprint(); operator std::unique_ptr() const { return generateResponseObject(); } + std::unique_ptr generateResponseObject() const; -protected: // functions - std::string getMessage(const std::string& msgId) const; - virtual std::unique_ptr generateResponseObject() const; +protected: // types + class Data; -public: //data +protected: //data const RequestContext& m_request; const int m_httpStatusCode; const std::string m_mimeType; const std::string m_template; - kainjow::mustache::data m_data; + const bool m_includeKiwixResponseData; + std::unique_ptr m_data; }; struct HTTPErrorResponse : ContentResponseBlueprint @@ -151,7 +151,8 @@ struct HTTPErrorResponse : ContentResponseBlueprint int httpStatusCode, const std::string& pageTitleMsgId, const std::string& headingMsgId, - const std::string& cssUrl = ""); + const std::string& cssUrl = "", + bool includeKiwixResponseData = false); HTTPErrorResponse& operator+(const ParameterizedMessage& errorDetails); HTTPErrorResponse& operator+=(const ParameterizedMessage& errorDetails); @@ -175,11 +176,6 @@ struct HTTP400Response : HTTPErrorResponse struct HTTP500Response : HTTPErrorResponse { explicit HTTP500Response(const RequestContext& request); - -private: // overrides - // generateResponseObject() is overriden in order to produce a minimal - // response without any need for additional resources from the server - std::unique_ptr generateResponseObject() const override; }; class ItemResponse : public Response { diff --git a/src/tools/otherTools.cpp b/src/tools/otherTools.cpp index 85e58a496..0c6eb36ca 100644 --- a/src/tools/otherTools.cpp +++ b/src/tools/otherTools.cpp @@ -327,17 +327,27 @@ std::string kiwix::render_template(const std::string& template_str, kainjow::mus return ss.str(); } -namespace -{ +// The escapeQuote parameter of escapeForJSON() defaults to true. +// This constant makes the calls to escapeForJSON() where the quote symbol +// should not be escaped (as it is later replaced with the HTML character entity +// ") more readable. +static const bool DONT_ESCAPE_QUOTE = false; -std::string escapeForJSON(const std::string& s) +std::string kiwix::escapeForJSON(const std::string& s, bool escapeQuote) { std::ostringstream oss; for (char c : s) { if ( c == '\\' ) { oss << "\\\\"; } else if ( unsigned(c) < 0x20U ) { - oss << "\\u" << std::setw(4) << std::setfill('0') << unsigned(c); + switch ( c ) { + case '\n': oss << "\\n"; break; + case '\r': oss << "\\r"; break; + case '\t': oss << "\\t"; break; + default: oss << "\\u" << std::setw(4) << std::setfill('0') << unsigned(c); + } + } else if ( c == '"' && escapeQuote ) { + oss << "\\\""; } else { oss << c; } @@ -345,6 +355,9 @@ std::string escapeForJSON(const std::string& s) return oss.str(); } +namespace +{ + std::string makeFulltextSearchSuggestion(const std::string& lang, const std::string& queryString) { @@ -370,10 +383,10 @@ void kiwix::Suggestions::add(const zim::SuggestionItem& suggestion) ? suggestion.getSnippet() : suggestion.getTitle(); - result.set("label", escapeForJSON(label)); - result.set("value", escapeForJSON(suggestion.getTitle())); + result.set("label", escapeForJSON(label, DONT_ESCAPE_QUOTE)); + result.set("value", escapeForJSON(suggestion.getTitle(), DONT_ESCAPE_QUOTE)); result.set("kind", "path"); - result.set("path", escapeForJSON(suggestion.getPath())); + result.set("path", escapeForJSON(suggestion.getPath(), DONT_ESCAPE_QUOTE)); result.set("first", m_data.is_empty_list()); m_data.push_back(result); } @@ -383,8 +396,8 @@ void kiwix::Suggestions::addFTSearchSuggestion(const std::string& uiLang, { kainjow::mustache::data result; const std::string label = makeFulltextSearchSuggestion(uiLang, queryString); - result.set("label", escapeForJSON(label)); - result.set("value", escapeForJSON(queryString + " ")); + result.set("label", escapeForJSON(label, DONT_ESCAPE_QUOTE)); + result.set("value", escapeForJSON(queryString + " ", DONT_ESCAPE_QUOTE)); result.set("kind", "pattern"); result.set("first", m_data.is_empty_list()); m_data.push_back(result); diff --git a/src/tools/stringTools.h b/src/tools/stringTools.h index 14fed7574..97fa34738 100644 --- a/src/tools/stringTools.h +++ b/src/tools/stringTools.h @@ -53,6 +53,7 @@ class ICULanguageInfo const icu::Locale locale; }; +std::string escapeForJSON(const std::string& s, bool escapeQuote = true); /* urlEncode() is the equivalent of JS encodeURIComponent(), with the only * difference that the slash (/) symbol is NOT encoded. */ diff --git a/static/skin/i18n.js b/static/skin/i18n.js index 3ccf85c32..2a44cc500 100644 --- a/static/skin/i18n.js +++ b/static/skin/i18n.js @@ -69,6 +69,37 @@ function $t(msgId, params={}) { } } +const I18n = { + instantiateParameterizedMessages: function(data) { + if ( data.__proto__ == Array.prototype ) { + const result = []; + for ( const x of data ) { + result.push(this.instantiateParameterizedMessages(x)); + } + return result; + } else if ( data.__proto__ == Object.prototype ) { + const msgId = data.msgid; + const msgParams = data.params; + if ( msgId && msgId.__proto__ == String.prototype && msgParams && msgParams.__proto__ == Object.prototype ) { + return $t(msgId, msgParams); + } else { + const result = {}; + for ( const p in data ) { + result[p] = this.instantiateParameterizedMessages(data[p]); + } + return result; + } + } else { + return data; + } + }, + + render: function (template, params) { + params = this.instantiateParameterizedMessages(params); + return mustache.render(template, params); + } +} + const DEFAULT_UI_LANGUAGE = 'en'; Translations.load(DEFAULT_UI_LANGUAGE, /*asDefault=*/true); @@ -145,3 +176,4 @@ window.$t = $t; window.getUserLanguage = getUserLanguage; window.setUserLanguage = setUserLanguage; window.initUILanguageSelector = initUILanguageSelector; +window.I18n = I18n; diff --git a/static/skin/viewer.js b/static/skin/viewer.js index 23507109c..a311552db 100644 --- a/static/skin/viewer.js +++ b/static/skin/viewer.js @@ -249,6 +249,25 @@ function handle_location_hash_change() { history.replaceState(viewerState, null); } +function translateErrorPageIfNeeded() { + const cw = 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() { const iframeLocation = contentIframe.contentWindow.location; console.log('handle_content_url_change: ' + iframeLocation.href); @@ -258,6 +277,7 @@ function handle_content_url_change() { const newHash = iframeUrl2UserUrl(iframeContentUrl, iframeContentQuery); history.replaceState(viewerState, null, makeURL(location.search, newHash)); updateCurrentBookIfNeeded(newHash); + translateErrorPageIfNeeded(); }; //////////////////////////////////////////////////////////////////////////////// @@ -496,6 +516,7 @@ function changeUILanguage() { viewerState.uiLanguage = lang; setUserLanguage(lang, () => { updateUIText(); + translateErrorPageIfNeeded(); history.pushState(viewerState, null); }); } diff --git a/static/templates/error.html b/static/templates/error.html index 711082096..13fadc24c 100644 --- a/static/templates/error.html +++ b/static/templates/error.html @@ -5,7 +5,10 @@ {{PAGE_TITLE}} {{#CSS_URL}} -{{/CSS_URL}} +{{/CSS_URL}}{{#KIWIX_RESPONSE_DATA}} {{/KIWIX_RESPONSE_DATA}}

{{PAGE_HEADING}}

diff --git a/test/i18n.cpp b/test/i18n.cpp new file mode 100644 index 000000000..4e891d6e9 --- /dev/null +++ b/test/i18n.cpp @@ -0,0 +1,50 @@ +#include "../src/server/i18n.h" +#include "gtest/gtest.h" + +using namespace kiwix; + +TEST(ParameterizedMessage, parameterlessMessages) +{ + { + const ParameterizedMessage msg("404-page-title", {}); + + EXPECT_EQ(msg.getText("en"), "Content not found"); + EXPECT_EQ(msg.getText("test"), "[I18N TESTING] Not Found - Try Again"); + } + + { + // Make sure that msgId influences the result of getText() + const ParameterizedMessage msg("random-page-button-text", {}); + + EXPECT_EQ(msg.getText("en"), "Go to a randomly selected page"); + EXPECT_EQ(msg.getText("test"), "[I18N TESTING] I am tired of determinism"); + } + + { + // Demonstrate that unwanted parameters are silently ignored + const ParameterizedMessage msg("404-page-title", {{"abc", "xyz"}}); + + EXPECT_EQ(msg.getText("en"), "Content not found"); + EXPECT_EQ(msg.getText("test"), "[I18N TESTING] Not Found - Try Again"); + } +} + +TEST(ParameterizedMessage, messagesWithParameters) +{ + { + const ParameterizedMessage msg("filter-by-tag", + {{"TAG", "scifi"}} + ); + + EXPECT_EQ(msg.getText("en"), "Filter by tag \"scifi\""); + EXPECT_EQ(msg.getText("test"), "Filter [I18N] by [TESTING] tag \"scifi\""); + } + + { + // Omitting expected parameters amounts to using empty values for them + const ParameterizedMessage msg("filter-by-tag", {}); + + EXPECT_EQ(msg.getText("en"), "Filter by tag \"\""); + EXPECT_EQ(msg.getText("test"), "Filter [I18N] by [TESTING] tag \"\""); + } +} diff --git a/test/meson.build b/test/meson.build index 72d7b7331..78446f473 100644 --- a/test/meson.build +++ b/test/meson.build @@ -13,7 +13,9 @@ tests = [ 'name_mapper', 'opds_catalog', 'server_helper', - 'lrucache' + 'lrucache', + 'i18n', + 'response' ] if build_machine.system() != 'windows' diff --git a/test/otherTools.cpp b/test/otherTools.cpp index d437e188d..3a3eb0477 100644 --- a/test/otherTools.cpp +++ b/test/otherTools.cpp @@ -110,10 +110,10 @@ TEST(Suggestions, specialCharHandling) CHECK_SUGGESTIONS(s.getJSON(), R"EXPECTEDJSON([ { - "value" : "Title with \u0009\u0010\u0013\\<>&'"~!@#$%^*()_+`-=[]{}|:;,.?", - "label" : "Snippet with \u0009\u0010\u0013\\<>&'"~!@#$%^*()_+`-=[]{}|:;,.?", + "value" : "Title with \t\n\r\\<>&'"~!@#$%^*()_+`-=[]{}|:;,.?", + "label" : "Snippet with \t\n\r\\<>&'"~!@#$%^*()_+`-=[]{}|:;,.?", "kind" : "path" - , "path" : "Path with \u0009\u0010\u0013\\<>&'"~!@#$%^*()_+`-=[]{}|:;,.?" + , "path" : "Path with \t\n\r\\<>&'"~!@#$%^*()_+`-=[]{}|:;,.?" } ] )EXPECTEDJSON" @@ -128,10 +128,10 @@ R"EXPECTEDJSON([ CHECK_SUGGESTIONS(s.getJSON(), R"EXPECTEDJSON([ { - "value" : "Snippetless title with \u0009\u0010\u0013\\<>&'"~!@#$%^*()_+`-=[]{}|:;,.?", - "label" : "Snippetless title with \u0009\u0010\u0013\\<>&'"~!@#$%^*()_+`-=[]{}|:;,.?", + "value" : "Snippetless title with \t\n\r\\<>&'"~!@#$%^*()_+`-=[]{}|:;,.?", + "label" : "Snippetless title with \t\n\r\\<>&'"~!@#$%^*()_+`-=[]{}|:;,.?", "kind" : "path" - , "path" : "Path with \u0009\u0010\u0013\\<>&'"~!@#$%^*()_+`-=[]{}|:;,.?" + , "path" : "Path with \t\n\r\\<>&'"~!@#$%^*()_+`-=[]{}|:;,.?" } ] )EXPECTEDJSON" @@ -145,8 +145,8 @@ R"EXPECTEDJSON([ CHECK_SUGGESTIONS(s.getJSON(), R"EXPECTEDJSON([ { - "value" : "text with \u0009\u0010\u0013\\<>&'"~!@#$%^*()_+`-=[]{}|:;,.? ", - "label" : "containing 'text with \u0009\u0010\u0013\\<>&'"~!@#$%^*()_+`-=[]{}|:;,.?'...", + "value" : "text with \t\n\r\\<>&'"~!@#$%^*()_+`-=[]{}|:;,.? ", + "label" : "containing 'text with \t\n\r\\<>&'"~!@#$%^*()_+`-=[]{}|:;,.?'...", "kind" : "pattern" //EOLWHITESPACEMARKER } diff --git a/test/response.cpp b/test/response.cpp new file mode 100644 index 000000000..ca0ece39a --- /dev/null +++ b/test/response.cpp @@ -0,0 +1,101 @@ +#include "../src/server/response.h" +#include "gtest/gtest.h" + +#include "../src/server/request_context.h" + +namespace +{ + +using namespace kiwix; + +RequestContext makeHttpGetRequest(const std::string& url, + const RequestContext::NameValuePairs& headers, + const RequestContext::NameValuePairs& queryArgs) +{ + return RequestContext("", url, "GET", "1.1", headers, queryArgs); +} + +std::string getResponseContent(const ContentResponseBlueprint& crb) +{ + return crb.generateResponseObject()->getContent(); +} + +} // unnamed namespace + + + +TEST(HTTPErrorResponse, shouldBeInEnglishByDefault) { + const RequestContext req = makeHttpGetRequest("/asdf", {}, {}); + HTTPErrorResponse errResp(req, MHD_HTTP_NOT_FOUND, + "404-page-title", + "404-page-heading", + "/css/error.css", + /*includeKiwixResponseData=*/true); + + errResp += ParameterizedMessage("suggest-search", + { + { "PATTERN", "asdf" }, + { "SEARCH_URL", "/search?q=asdf" } + }); + + EXPECT_EQ(getResponseContent(errResp), +R"( + + + + Content not found + + + + +

Not Found

+

+ Make a full text search for asdf +

+ + +)"); +} + +TEST(HTTPErrorResponse, shouldBeTranslatable) { + const RequestContext req = makeHttpGetRequest("/asdf", + /* headers */ {}, + /* query args */ {{"userlang", "test"}} + ); + + HTTPErrorResponse errResp(req, MHD_HTTP_NOT_FOUND, + "404-page-title", + "404-page-heading", + "/css/error.css", + /*includeKiwixResponseData=*/true); + + errResp += ParameterizedMessage("suggest-search", + { + { "PATTERN", "asdf" }, + { "SEARCH_URL", "/search?q=asdf" } + }); + + EXPECT_EQ(getResponseContent(errResp), +R"( + + + + [I18N TESTING] Not Found - Try Again + + + + +

[I18N TESTING] Content not found, but at least the server is alive

+

+ [I18N TESTING] Make a full text search for asdf +

+ + +)"); +} diff --git a/test/server.cpp b/test/server.cpp index 3c1e85954..80f8e3ae0 100644 --- a/test/server.cpp +++ b/test/server.cpp @@ -59,7 +59,7 @@ const ResourceCollection resources200Compressible{ { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/autoComplete/css/autoComplete.css" }, { STATIC_CONTENT, "/ROOT%23%3F/skin/autoComplete/css/autoComplete.css?cacheid=ef30cd42" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/i18n.js" }, - { STATIC_CONTENT, "/ROOT%23%3F/skin/i18n.js?cacheid=6a8c6fb2" }, + { STATIC_CONTENT, "/ROOT%23%3F/skin/i18n.js?cacheid=4ab55b42" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/index.css" }, { STATIC_CONTENT, "/ROOT%23%3F/skin/index.css?cacheid=1e78e7cf" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/index.js" }, @@ -75,7 +75,7 @@ const ResourceCollection resources200Compressible{ { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/taskbar.css" }, { STATIC_CONTENT, "/ROOT%23%3F/skin/taskbar.css?cacheid=e014a885" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/viewer.js" }, - { STATIC_CONTENT, "/ROOT%23%3F/skin/viewer.js?cacheid=948df083" }, + { STATIC_CONTENT, "/ROOT%23%3F/skin/viewer.js?cacheid=e9c025f2" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/fonts/Poppins.ttf" }, { STATIC_CONTENT, "/ROOT%23%3F/skin/fonts/Poppins.ttf?cacheid=af705837" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/fonts/Roboto.ttf" }, @@ -285,7 +285,7 @@ R"EXPECTEDRESULT( href="/ROOT%23%3F/skin/kiwix.css?cacheid=2158fad9" - + @@ -318,9 +318,9 @@ R"EXPECTEDRESULT( - + - + const blankPageUrl = root + "/skin/blank.html?cacheid=6b1fa032"; @@ -337,6 +337,7 @@ R"EXPECTEDRESULT( + window.KIWIX_RESPONSE_DATA = { "CSS_URL" : "/ROOT%23%3F/skin/search_results.css?cacheid=76d39c84", "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "fulltext-search-unavailable", "params" : { } }, "details" : [ { "p" : { "msgid" : "no-search-results", "params" : { } } } ] }; )EXPECTEDRESULT" }, }; @@ -535,6 +536,7 @@ struct ExpectedResponseData { const std::string expectedPageTitle; const std::string expectedCssUrl; + const std::string expectedKiwixResponseData; const std::string bookName; const std::string bookTitle; const std::string expectedBody; @@ -544,6 +546,7 @@ enum ExpectedResponseDataType { expected_page_title, expected_css_url, + expected_kiwix_response_data, book_name, book_title, expected_body @@ -556,11 +559,13 @@ ExpectedResponseData operator==(ExpectedResponseDataType t, std::string s) { switch (t) { - case expected_page_title: return ExpectedResponseData{s, "", "", "", ""}; - case expected_css_url: return ExpectedResponseData{"", s, "", "", ""}; - case book_name: return ExpectedResponseData{"", "", s, "", ""}; - case book_title: return ExpectedResponseData{"", "", "", s, ""}; - case expected_body: return ExpectedResponseData{"", "", "", "", s}; + case expected_page_title: return ExpectedResponseData{s, "", "", "", "", ""}; + case expected_css_url: return ExpectedResponseData{"", s, "", "", "", ""}; + case expected_kiwix_response_data: + return ExpectedResponseData{"", "", s, "", "", ""}; + case book_name: return ExpectedResponseData{"", "", "", s, "", ""}; + case book_title: return ExpectedResponseData{"", "", "", "", s, ""}; + case expected_body: return ExpectedResponseData{"", "", "", "", "", s}; default: assert(false); return ExpectedResponseData{}; } } @@ -579,6 +584,7 @@ ExpectedResponseData operator&&(const ExpectedResponseData& a, return ExpectedResponseData{ selectNonEmpty(a.expectedPageTitle, b.expectedPageTitle), selectNonEmpty(a.expectedCssUrl, b.expectedCssUrl), + selectNonEmpty(a.expectedKiwixResponseData, b.expectedKiwixResponseData), selectNonEmpty(a.bookName, b.bookName), selectNonEmpty(a.bookTitle, b.bookTitle), selectNonEmpty(a.expectedBody, b.expectedBody) @@ -607,19 +613,29 @@ class TestContentIn404HtmlResponse : public ExpectedResponseData std::string TestContentIn404HtmlResponse::expectedResponse() const { const std::string frag[] = { + // frag[0] R"FRAG( )FRAG", + // frag[1] R"FRAG( )FRAG", - R"FRAG( + // frag[2] + R"( )FRAG", + // frag[4] R"FRAG( )FRAG" @@ -630,8 +646,10 @@ std::string TestContentIn404HtmlResponse::expectedResponse() const + frag[1] + pageCssLink() + frag[2] + + expectedKiwixResponseData + + frag[3] + expectedBody - + frag[3]; + + frag[4]; } std::string TestContentIn404HtmlResponse::pageTitle() const @@ -648,7 +666,8 @@ std::string TestContentIn404HtmlResponse::pageCssLink() const return R"( )"; + + R"(" rel="Stylesheet" />)" + + "\n"; } class TestContentIn400HtmlResponse : public TestContentIn404HtmlResponse @@ -676,6 +695,7 @@ TEST_F(ServerTest, Http404HtmlError) using namespace TestingOfHtmlResponses; const std::vector testData{ { /* url */ "/ROOT%23%3F/random?content=non-existent-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" : "no-such-book", "params" : { "BOOK_NAME" : "non-existent-book" } } } ] })" && expected_body==R"(

Not Found

@@ -685,6 +705,7 @@ TEST_F(ServerTest, Http404HtmlError) { /* url */ "/ROOT%23%3F/random?content=non-existent-book&userlang=test", expected_page_title=="[I18N TESTING] Not Found - Try Again" && + 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" : "no-such-book", "params" : { "BOOK_NAME" : "non-existent-book" } } } ] })" && expected_body==R"(

[I18N TESTING] Content not found, but at least the server is alive

@@ -693,6 +714,7 @@ TEST_F(ServerTest, Http404HtmlError) )" }, { /* url */ "/ROOT%23%3F/suggest?content=no-such-book&term=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" : "no-such-book", "params" : { "BOOK_NAME" : "no-such-book" } } } ] })" && expected_body==R"(

Not Found

@@ -701,6 +723,7 @@ TEST_F(ServerTest, Http404HtmlError) )" }, { /* url */ "/ROOT%23%3F/catalog/", + 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/catalog/" } } } ] })" && expected_body==R"(

Not Found

@@ -710,6 +733,7 @@ TEST_F(ServerTest, Http404HtmlError) { /* url */ "/ROOT%23%3F/catalog/?userlang=test", expected_page_title=="[I18N TESTING] Not Found - Try Again" && + 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/catalog/" } } } ] })" && expected_body==R"(

[I18N TESTING] Content not found, but at least the server is alive

@@ -718,6 +742,7 @@ TEST_F(ServerTest, Http404HtmlError) )" }, { /* url */ "/ROOT%23%3F/catalog/invalid_endpoint", + 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/catalog/invalid_endpoint" } } } ] })" && expected_body==R"(

Not Found

@@ -727,6 +752,7 @@ TEST_F(ServerTest, Http404HtmlError) { /* url */ "/ROOT%23%3F/catalog/invalid_endpoint?userlang=test", expected_page_title=="[I18N TESTING] Not Found - Try Again" && + 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/catalog/invalid_endpoint" } } } ] })" && expected_body==R"(

[I18N TESTING] Content not found, but at least the server is alive

@@ -735,6 +761,7 @@ TEST_F(ServerTest, Http404HtmlError) )" }, { /* 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"(

Not Found

@@ -748,6 +775,7 @@ TEST_F(ServerTest, Http404HtmlError) { /* 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"(

Not Found

@@ -759,6 +787,7 @@ TEST_F(ServerTest, Http404HtmlError) )" }, { /* url */ R"(/ROOT%23%3F/content/">)", + 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/\">" } } }, { "p" : { "msgid" : "suggest-search", "params" : { "PATTERN" : "\">", "SEARCH_URL" : "/ROOT%23%3F/search?pattern=%22%3E%3Csvg%20onload%3Dalert(1)%3E" } } } ] })" && expected_body==R"(

Not Found

@@ -772,6 +801,7 @@ TEST_F(ServerTest, Http404HtmlError) { /* url */ R"(/ROOT%23%3F/content/zimfile/">)", 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/\">" } } }, { "p" : { "msgid" : "suggest-search", "params" : { "PATTERN" : "\">", "SEARCH_URL" : "/ROOT%23%3F/search?content=zimfile&pattern=%22%3E%3Csvg%20onload%3Dalert(1)%3E" } } } ] })" && expected_body==R"(

Not Found

@@ -782,10 +812,27 @@ TEST_F(ServerTest, Http404HtmlError)

)" }, + // XXX: This test case is against a "" string appearing inside + // XXX: javascript code that will confuse the HTML parser + { /* url */ R"(/ROOT%23%3F/content/zimfile/)", + 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/" } } }, { "p" : { "msgid" : "suggest-search", "params" : { "PATTERN" : "script>", "SEARCH_URL" : "/ROOT%23%3F/search?content=zimfile&pattern=script%3E" } } } ] })" && + expected_body==R"( +

Not Found

+

+ The requested URL "/ROOT%23%3F/content/zimfile/</script>" was not found on this server. +

+

+ Make a full text search for script> +

+)" }, + { /* 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"(

[I18N TESTING] Content not found, but at least the server is alive

@@ -797,6 +844,7 @@ TEST_F(ServerTest, Http404HtmlError) )" }, { /* 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_body==R"(

Not Found

@@ -808,6 +856,7 @@ TEST_F(ServerTest, Http404HtmlError) )" }, { /* url */ "/ROOT%23%3F/raw/zimfile/XYZ", + 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/zimfile/XYZ" } } }, { "p" : { "msgid" : "invalid-raw-data-type", "params" : { "DATATYPE" : "XYZ" } } } ] })" && expected_body==R"(

Not Found

@@ -819,6 +868,7 @@ TEST_F(ServerTest, Http404HtmlError) )" }, { /* url */ "/ROOT%23%3F/raw/zimfile/meta/invalid-metadata", + 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/zimfile/meta/invalid-metadata" } } }, { "p" : { "msgid" : "raw-entry-not-found", "params" : { "DATATYPE" : "meta", "ENTRY" : "invalid-metadata" } } } ] })" && expected_body==R"(

Not Found

@@ -830,6 +880,7 @@ TEST_F(ServerTest, Http404HtmlError) )" }, { /* url */ "/ROOT%23%3F/raw/zimfile/content/invalid-article", + 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/zimfile/content/invalid-article" } } }, { "p" : { "msgid" : "raw-entry-not-found", "params" : { "DATATYPE" : "content", "ENTRY" : "invalid-article" } } } ] })" && expected_body==R"(

Not Found

@@ -845,6 +896,7 @@ TEST_F(ServerTest, Http404HtmlError) expected_css_url=="/ROOT%23%3F/skin/search_results.css?cacheid=76d39c84" && book_name=="poor" && book_title=="poor" && + expected_kiwix_response_data==R"({ "CSS_URL" : "/ROOT%23%3F/skin/search_results.css?cacheid=76d39c84", "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "fulltext-search-unavailable", "params" : { } }, "details" : [ { "p" : { "msgid" : "no-search-results", "params" : { } } } ] })" && expected_body==R"(

Not Found

@@ -866,6 +918,7 @@ TEST_F(ServerTest, Http400HtmlError) using namespace TestingOfHtmlResponses; const std::vector testData{ { /* url */ "/ROOT%23%3F/search", + expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "400-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "400-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "invalid-request", "params" : { "url" : "/ROOT%23%3F/search" } } }, { "p" : { "msgid" : "too-many-books", "params" : { "LIMIT" : "3", "NB_BOOKS" : "4" } } } ] })" && expected_body== R"(

Invalid request

@@ -876,6 +929,7 @@ TEST_F(ServerTest, Http400HtmlError)

)" }, { /* url */ "/ROOT%23%3F/search?content=zimfile", + expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "400-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "400-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "invalid-request", "params" : { "url" : "/ROOT%23%3F/search?content=zimfile" } } }, { "p" : { "msgid" : "no-query", "params" : { } } } ] })" && expected_body==R"(

Invalid request

@@ -886,6 +940,7 @@ TEST_F(ServerTest, Http400HtmlError)

)" }, { /* url */ "/ROOT%23%3F/search?content=non-existing-book&pattern=asdfqwerty", + expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "400-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "400-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "invalid-request", "params" : { "url" : "/ROOT%23%3F/search?content=non-existing-book&pattern=asdfqwerty" } } }, { "p" : { "msgid" : "no-such-book", "params" : { "BOOK_NAME" : "non-existing-book" } } } ] })" && expected_body==R"(

Invalid request

@@ -896,6 +951,7 @@ TEST_F(ServerTest, Http400HtmlError)

)" }, { /* url */ "/ROOT%23%3F/search?content=non-existing-book&pattern=a\"

Internal Server Error

@@ -1054,6 +1116,7 @@ TEST_F(ServerTest, 500) const auto r = zfs1_->GET("/ROOT%23%3F/content/poor/A/redirect_loop.html"); EXPECT_EQ(r->status, 500); EXPECT_EQ(r->body, expectedBody); + EXPECT_EQ(r->get_header_value("Content-Type"), "text/html; charset=utf-8"); } } diff --git a/test/server_search.cpp b/test/server_search.cpp index a00099ce3..358160edd 100644 --- a/test/server_search.cpp +++ b/test/server_search.cpp @@ -1509,7 +1509,10 @@ std::string expectedConfusionOfTonguesErrorHtml(std::string url) Invalid request - +

Invalid request

diff --git a/test/server_testing_tools.h b/test/server_testing_tools.h index 3ea10ab57..c7adbc565 100644 --- a/test/server_testing_tools.h +++ b/test/server_testing_tools.h @@ -190,3 +190,5 @@ class ServerTest : public ::testing::Test zfs1_.reset(); } }; + +static const std::string ERROR_HTML_TEMPLATE_JS_STRING = R"("<!DOCTYPE html>\n<html xmlns="http://www.w3.org/1999/xhtml">\n <head>\n <meta content="text/html;charset=UTF-8" http-equiv="content-type" />\n <title>{{PAGE_TITLE}}</title>\n{{#CSS_URL}}\n <link type="text/css" href="{{{CSS_URL}}}" rel="Stylesheet" />\n{{/CSS_URL}}{{#KIWIX_RESPONSE_DATA}} <script>\n window.KIWIX_RESPONSE_TEMPLATE = "{{KIWIX_RESPONSE_TEMPLATE}}";\n window.KIWIX_RESPONSE_DATA = {{{KIWIX_RESPONSE_DATA}}};\n </script>{{/KIWIX_RESPONSE_DATA}}\n </head>\n <body>\n <h1>{{PAGE_HEADING}}</h1>\n{{#details}}\n <p>\n {{{p}}}\n </p>\n{{/details}}\n </body>\n</html>\n")";