Merge pull request #1032 from kiwix/error_response_i18n

Translation of error pages
This commit is contained in:
Matthieu Gautier 2024-01-29 10:58:55 +01:00 committed by GitHub
commit d2f20dba66
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 624 additions and 113 deletions

View File

@ -112,8 +112,12 @@ std::string expandParameterizedString(const std::string& lang,
const std::string& key, const std::string& key,
const Parameters& params) const Parameters& params)
{ {
kainjow::mustache::object mustacheParams;
for( const auto& kv : params ) {
mustacheParams[kv.first] = kv.second;
}
const std::string tmpl = getTranslatedString(lang, key); const std::string tmpl = getTranslatedString(lang, key);
return render_template(tmpl, params); return render_template(tmpl, mustacheParams);
} }
} // namespace i18n } // namespace i18n

View File

@ -20,6 +20,7 @@
#ifndef KIWIX_SERVER_I18N #ifndef KIWIX_SERVER_I18N
#define KIWIX_SERVER_I18N #define KIWIX_SERVER_I18N
#include <map>
#include <string> #include <string>
#include <mustache.hpp> #include <mustache.hpp>
@ -44,7 +45,7 @@ std::string getTranslatedString(const std::string& lang, const std::string& key)
namespace i18n namespace i18n
{ {
typedef kainjow::mustache::object Parameters; typedef std::map<std::string, std::string> Parameters;
std::string expandParameterizedString(const std::string& lang, std::string expandParameterizedString(const std::string& lang,
const std::string& key, const std::string& key,
@ -93,10 +94,10 @@ private:
} // namespace i18n } // namespace i18n
struct ParameterizedMessage class ParameterizedMessage
{ {
public: // types public: // types
typedef kainjow::mustache::object Parameters; typedef i18n::Parameters Parameters;
public: // functions public: // functions
ParameterizedMessage(const std::string& msgId, const Parameters& params) ParameterizedMessage(const std::string& msgId, const Parameters& params)
@ -106,6 +107,9 @@ public: // functions
std::string getText(const std::string& lang) const; std::string getText(const std::string& lang) const;
const std::string& getMsgId() const { return msgId; }
const Parameters& getParams() const { return params; }
private: // data private: // data
const std::string msgId; const std::string msgId;
const Parameters params; const Parameters params;

View File

@ -513,6 +513,19 @@ static MHD_Result staticHandlerCallback(void* cls,
cont_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<RequestContext::NameValuePairs*>(nvp);
nameValuePairs.push_back({key, value});
return MHD_YES;
}
} // unnamed namespace
MHD_Result InternalServer::handlerCallback(struct MHD_Connection* connection, MHD_Result InternalServer::handlerCallback(struct MHD_Connection* connection,
const char* fullUrl, const char* fullUrl,
const char* method, const char* method,
@ -529,7 +542,10 @@ MHD_Result InternalServer::handlerCallback(struct MHD_Connection* connection,
} }
const auto url = fullURL2LocalURL(fullUrl, m_rootPrefixOfDecodedURL); 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() ) { if (m_verbose.load() ) {
request.print_debug_info(); request.print_debug_info();
@ -926,7 +942,8 @@ std::unique_ptr<Response> InternalServer::handle_search_request(const RequestCon
HTTPErrorResponse response(request, MHD_HTTP_NOT_FOUND, HTTPErrorResponse response(request, MHD_HTTP_NOT_FOUND,
"fulltext-search-unavailable", "fulltext-search-unavailable",
"404-page-heading", "404-page-heading",
cssUrl); cssUrl,
/*includeKiwixResponseData=*/true);
response += nonParameterizedMessage("no-search-results"); response += nonParameterizedMessage("no-search-results");
// XXX: Now this has to be handled by the iframe-based viewer which // 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. // XXX: has to resolve if the book selection resulted in a single book.

View File

@ -51,11 +51,12 @@ RequestMethod str2RequestMethod(const std::string& method) {
} // unnamed namespace } // unnamed namespace
RequestContext::RequestContext(struct MHD_Connection* connection, RequestContext::RequestContext(const std::string& _rootLocation, // URI-encoded
const std::string& _rootLocation, // URI-encoded
const std::string& unrootedUrl, // URI-decoded const std::string& unrootedUrl, // URI-decoded
const std::string& _method, const std::string& _method,
const std::string& version) : const std::string& version,
const NameValuePairs& headers,
const NameValuePairs& queryArgs) :
rootLocation(_rootLocation), rootLocation(_rootLocation),
url(unrootedUrl), url(unrootedUrl),
method(str2RequestMethod(_method)), method(str2RequestMethod(_method)),
@ -64,9 +65,13 @@ RequestContext::RequestContext(struct MHD_Connection* connection,
acceptEncodingGzip(false), acceptEncodingGzip(false),
byteRange_() byteRange_()
{ {
MHD_get_connection_values(connection, MHD_HEADER_KIND, &RequestContext::fill_header, this); for ( const auto& kv : headers ) {
MHD_get_connection_values(connection, MHD_GET_ARGUMENT_KIND, &RequestContext::fill_argument, this); add_header(kv.first, kv.second);
MHD_get_connection_values(connection, MHD_COOKIE_KIND, &RequestContext::fill_cookie, this); }
for ( const auto& kv : queryArgs ) {
add_argument(kv.first, kv.second);
}
try { try {
acceptEncodingGzip = acceptEncodingGzip =
@ -83,18 +88,14 @@ RequestContext::RequestContext(struct MHD_Connection* connection,
RequestContext::~RequestContext() RequestContext::~RequestContext()
{} {}
MHD_Result RequestContext::fill_header(void *__this, enum MHD_ValueKind kind, void RequestContext::add_header(const char *key, const char *value)
const char *key, const char *value)
{ {
RequestContext *_this = static_cast<RequestContext*>(__this); this->headers[lcAll(key)] = value;
_this->headers[lcAll(key)] = value;
return MHD_YES;
} }
MHD_Result RequestContext::fill_argument(void *__this, enum MHD_ValueKind kind, void RequestContext::add_argument(const char *key, const char* value)
const char *key, const char* value)
{ {
RequestContext *_this = static_cast<RequestContext*>(__this); RequestContext *_this = this;
_this->arguments[key].push_back(value == nullptr ? "" : value); _this->arguments[key].push_back(value == nullptr ? "" : value);
if ( ! _this->queryString.empty() ) { if ( ! _this->queryString.empty() ) {
_this->queryString += "&"; _this->queryString += "&";
@ -104,15 +105,6 @@ MHD_Result RequestContext::fill_argument(void *__this, enum MHD_ValueKind kind,
_this->queryString += "="; _this->queryString += "=";
_this->queryString += urlEncode(value); _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<RequestContext*>(__this);
_this->cookies[key] = value == nullptr ? "" : value;
return MHD_YES;
} }
void RequestContext::print_debug_info() const { void RequestContext::print_debug_info() const {

View File

@ -29,7 +29,7 @@
#include <stdexcept> #include <stdexcept>
#include "byte_range.h" #include "byte_range.h"
#include "tools/stringTools.h" #include "../tools/stringTools.h"
extern "C" { extern "C" {
#include "microhttpd_wrapper.h" #include "microhttpd_wrapper.h"
@ -55,12 +55,17 @@ class IndexError: public std::runtime_error {};
class RequestContext { class RequestContext {
public: // types
typedef std::vector<std::pair<const char*, const char*>> NameValuePairs;
public: // functions public: // functions
RequestContext(struct MHD_Connection* connection, RequestContext(const std::string& rootLocation, // URI-encoded
const std::string& rootLocation, // URI-encoded
const std::string& unrootedUrl, // URI-decoded const std::string& unrootedUrl, // URI-decoded
const std::string& method, const std::string& method,
const std::string& version); const std::string& version,
const NameValuePairs& headers,
const NameValuePairs& queryArgs);
~RequestContext(); ~RequestContext();
void print_debug_info() const; void print_debug_info() const;
@ -145,16 +150,14 @@ class RequestContext {
ByteRange byteRange_; ByteRange byteRange_;
std::map<std::string, std::string> headers; std::map<std::string, std::string> headers;
std::map<std::string, std::vector<std::string>> arguments; std::map<std::string, std::vector<std::string>> arguments;
std::map<std::string, std::string> cookies;
std::string queryString; std::string queryString;
UserLanguage userlang; UserLanguage userlang;
private: // functions private: // functions
UserLanguage determine_user_language() const; UserLanguage determine_user_language() const;
static MHD_Result fill_header(void *, enum MHD_ValueKind, const char*, const char*); void add_header(const char* name, const char* value);
static MHD_Result fill_cookie(void *, enum MHD_ValueKind, const char*, const char*); void add_argument(const char* name, const char* value);
static MHD_Result fill_argument(void *, enum MHD_ValueKind, const char*, const char*);
}; };
template<> std::string RequestContext::get_argument(const std::string& name) const; template<> std::string RequestContext::get_argument(const std::string& name) const;

View File

@ -32,6 +32,9 @@
#include <zlib.h> #include <zlib.h>
#include <array> #include <array>
#include <list>
#include <map>
#include <regex>
// This is somehow a magic value. // This is somehow a magic value.
// If this value is too small, we will compress (and lost cpu time) too much // If this value is too small, we will compress (and lost cpu time) too much
@ -47,6 +50,8 @@ namespace kiwix {
namespace namespace
{ {
typedef kainjow::mustache::data MustacheData;
// some utilities // some utilities
std::string get_mime_type(const zim::Item& item) std::string get_mime_type(const zim::Item& item)
@ -151,14 +156,214 @@ std::unique_ptr<Response> Response::build_304(const ETag& etag)
return response; 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 T>
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<T> ptr;
};
} // unnamed namespace
class ContentResponseBlueprint::Data
{
public:
typedef std::list<Data> List;
typedef std::map<std::string, Data> Object;
private:
// std::variant<std::string, bool, List, Object> 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<std::string> m_stringValue;
Optional<bool> m_boolValue;
Optional<List> m_listValue;
Optional<Object> 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<std::string, std::string> 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 <script></script> tag.
// If it contains "</script>" (or "</script >") 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("</script"), "</scr\\ipt");
}
ContentResponseBlueprint::ContentResponseBlueprint(const RequestContext* request,
int httpStatusCode,
const std::string& mimeType,
const std::string& templateStr,
bool includeKiwixResponseData)
: m_request(*request)
, m_httpStatusCode(httpStatusCode)
, m_mimeType(mimeType)
, m_template(templateStr)
, m_includeKiwixResponseData(includeKiwixResponseData)
, m_data(new Data)
{}
ContentResponseBlueprint::~ContentResponseBlueprint() = default;
std::unique_ptr<ContentResponse> ContentResponseBlueprint::generateResponseObject() const std::unique_ptr<ContentResponse> 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); r->set_code(m_httpStatusCode);
return r; return r;
} }
@ -167,26 +372,30 @@ HTTPErrorResponse::HTTPErrorResponse(const RequestContext& request,
int httpStatusCode, int httpStatusCode,
const std::string& pageTitleMsgId, const std::string& pageTitleMsgId,
const std::string& headingMsgId, const std::string& headingMsgId,
const std::string& cssUrl) const std::string& cssUrl,
bool includeKiwixResponseData)
: ContentResponseBlueprint(&request, : ContentResponseBlueprint(&request,
httpStatusCode, httpStatusCode,
request.get_requested_format() == "html" ? "text/html; charset=utf-8" : "application/xml; charset=utf-8", 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) request.get_requested_format() == "html" ? RESOURCE::templates::error_html : RESOURCE::templates::error_xml,
includeKiwixResponseData)
{ {
kainjow::mustache::list emptyList; Data::List emptyList;
this->m_data = kainjow::mustache::object{ *this->m_data = Data(Data::Object{
{"CSS_URL", onlyAsNonEmptyMustacheValue(cssUrl) }, {"CSS_URL", Data::onlyAsNonEmptyValue(cssUrl) },
{"PAGE_TITLE", getMessage(pageTitleMsgId)}, {"PAGE_TITLE", Data::from(nonParameterizedMessage(pageTitleMsgId))},
{"PAGE_HEADING", getMessage(headingMsgId)}, {"PAGE_HEADING", Data::from(nonParameterizedMessage(headingMsgId))},
{"details", emptyList} {"details", emptyList}
}; });
} }
HTTP404Response::HTTP404Response(const RequestContext& request) HTTP404Response::HTTP404Response(const RequestContext& request)
: HTTPErrorResponse(request, : HTTPErrorResponse(request,
MHD_HTTP_NOT_FOUND, MHD_HTTP_NOT_FOUND,
"404-page-title", "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) HTTPErrorResponse& HTTPErrorResponse::operator+(const ParameterizedMessage& details)
{ {
const std::string msg = details.getText(m_request.get_user_language()); (*m_data)["details"].push_back(Data::Object{{"p", Data::from(details)}});
m_data["details"].push_back({"p", msg});
return *this; return *this;
} }
@ -215,7 +423,9 @@ HTTP400Response::HTTP400Response(const RequestContext& request)
: HTTPErrorResponse(request, : HTTPErrorResponse(request,
MHD_HTTP_BAD_REQUEST, MHD_HTTP_BAD_REQUEST,
"400-page-title", "400-page-title",
"400-page-heading") "400-page-heading",
std::string(),
/*includeKiwixResponseData=*/true)
{ {
std::string requestUrl = urlDecode(m_request.get_full_url(), false); std::string requestUrl = urlDecode(m_request.get_full_url(), false);
const auto query = m_request.get_query(); const auto query = m_request.get_query();
@ -229,19 +439,13 @@ HTTP500Response::HTTP500Response(const RequestContext& request)
: HTTPErrorResponse(request, : HTTPErrorResponse(request,
MHD_HTTP_INTERNAL_SERVER_ERROR, MHD_HTTP_INTERNAL_SERVER_ERROR,
"500-page-title", "500-page-title",
"500-page-heading") "500-page-heading",
std::string(),
/*includeKiwixResponseData=*/true)
{ {
*this += nonParameterizedMessage("500-page-text"); *this += nonParameterizedMessage("500-page-text");
} }
std::unique_ptr<ContentResponse> 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> Response::build_416(size_t resourceLength) std::unique_ptr<Response> Response::build_416(size_t resourceLength)
{ {
auto response = Response::build(); auto response = Response::build();

View File

@ -101,6 +101,9 @@ class ContentResponse : public Response {
kainjow::mustache::data data, kainjow::mustache::data data,
const std::string& mimetype); const std::string& mimetype);
const std::string& getContent() const { return m_content; }
const std::string& getMimeType() const { return m_mimeType; }
private: private:
MHD_Response* create_mhd_response(const RequestContext& request); MHD_Response* create_mhd_response(const RequestContext& request);
@ -118,31 +121,28 @@ public: // functions
ContentResponseBlueprint(const RequestContext* request, ContentResponseBlueprint(const RequestContext* request,
int httpStatusCode, int httpStatusCode,
const std::string& mimeType, const std::string& mimeType,
const std::string& templateStr) const std::string& templateStr,
: m_request(*request) bool includeKiwixResponseData = false);
, m_httpStatusCode(httpStatusCode)
, m_mimeType(mimeType)
, m_template(templateStr)
{}
virtual ~ContentResponseBlueprint() = default; ~ContentResponseBlueprint();
operator std::unique_ptr<Response>() const operator std::unique_ptr<Response>() const
{ {
return generateResponseObject(); return generateResponseObject();
} }
std::unique_ptr<ContentResponse> generateResponseObject() const;
protected: // functions protected: // types
std::string getMessage(const std::string& msgId) const; class Data;
virtual std::unique_ptr<ContentResponse> generateResponseObject() const;
public: //data protected: //data
const RequestContext& m_request; const RequestContext& m_request;
const int m_httpStatusCode; const int m_httpStatusCode;
const std::string m_mimeType; const std::string m_mimeType;
const std::string m_template; const std::string m_template;
kainjow::mustache::data m_data; const bool m_includeKiwixResponseData;
std::unique_ptr<Data> m_data;
}; };
struct HTTPErrorResponse : ContentResponseBlueprint struct HTTPErrorResponse : ContentResponseBlueprint
@ -151,7 +151,8 @@ struct HTTPErrorResponse : ContentResponseBlueprint
int httpStatusCode, int httpStatusCode,
const std::string& pageTitleMsgId, const std::string& pageTitleMsgId,
const std::string& headingMsgId, 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);
HTTPErrorResponse& operator+=(const ParameterizedMessage& errorDetails); HTTPErrorResponse& operator+=(const ParameterizedMessage& errorDetails);
@ -175,11 +176,6 @@ struct HTTP400Response : HTTPErrorResponse
struct HTTP500Response : HTTPErrorResponse struct HTTP500Response : HTTPErrorResponse
{ {
explicit HTTP500Response(const RequestContext& request); 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<ContentResponse> generateResponseObject() const override;
}; };
class ItemResponse : public Response { class ItemResponse : public Response {

View File

@ -327,17 +327,27 @@ std::string kiwix::render_template(const std::string& template_str, kainjow::mus
return ss.str(); 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
// &quot;) 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; std::ostringstream oss;
for (char c : s) { for (char c : s) {
if ( c == '\\' ) { if ( c == '\\' ) {
oss << "\\\\"; oss << "\\\\";
} else if ( unsigned(c) < 0x20U ) { } 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 { } else {
oss << c; oss << c;
} }
@ -345,6 +355,9 @@ std::string escapeForJSON(const std::string& s)
return oss.str(); return oss.str();
} }
namespace
{
std::string makeFulltextSearchSuggestion(const std::string& lang, std::string makeFulltextSearchSuggestion(const std::string& lang,
const std::string& queryString) const std::string& queryString)
{ {
@ -370,10 +383,10 @@ void kiwix::Suggestions::add(const zim::SuggestionItem& suggestion)
? suggestion.getSnippet() ? suggestion.getSnippet()
: suggestion.getTitle(); : suggestion.getTitle();
result.set("label", escapeForJSON(label)); result.set("label", escapeForJSON(label, DONT_ESCAPE_QUOTE));
result.set("value", escapeForJSON(suggestion.getTitle())); result.set("value", escapeForJSON(suggestion.getTitle(), DONT_ESCAPE_QUOTE));
result.set("kind", "path"); 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()); result.set("first", m_data.is_empty_list());
m_data.push_back(result); m_data.push_back(result);
} }
@ -383,8 +396,8 @@ void kiwix::Suggestions::addFTSearchSuggestion(const std::string& uiLang,
{ {
kainjow::mustache::data result; kainjow::mustache::data result;
const std::string label = makeFulltextSearchSuggestion(uiLang, queryString); const std::string label = makeFulltextSearchSuggestion(uiLang, queryString);
result.set("label", escapeForJSON(label)); result.set("label", escapeForJSON(label, DONT_ESCAPE_QUOTE));
result.set("value", escapeForJSON(queryString + " ")); result.set("value", escapeForJSON(queryString + " ", DONT_ESCAPE_QUOTE));
result.set("kind", "pattern"); result.set("kind", "pattern");
result.set("first", m_data.is_empty_list()); result.set("first", m_data.is_empty_list());
m_data.push_back(result); m_data.push_back(result);

View File

@ -53,6 +53,7 @@ private:
const icu::Locale locale; const icu::Locale locale;
}; };
std::string escapeForJSON(const std::string& s, bool escapeQuote = true);
/* urlEncode() is the equivalent of JS encodeURIComponent(), with the only /* urlEncode() is the equivalent of JS encodeURIComponent(), with the only
* difference that the slash (/) symbol is NOT encoded. */ * difference that the slash (/) symbol is NOT encoded. */

View File

@ -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'; const DEFAULT_UI_LANGUAGE = 'en';
Translations.load(DEFAULT_UI_LANGUAGE, /*asDefault=*/true); Translations.load(DEFAULT_UI_LANGUAGE, /*asDefault=*/true);
@ -145,3 +176,4 @@ window.$t = $t;
window.getUserLanguage = getUserLanguage; window.getUserLanguage = getUserLanguage;
window.setUserLanguage = setUserLanguage; window.setUserLanguage = setUserLanguage;
window.initUILanguageSelector = initUILanguageSelector; window.initUILanguageSelector = initUILanguageSelector;
window.I18n = I18n;

View File

@ -249,6 +249,25 @@ function handle_location_hash_change() {
history.replaceState(viewerState, null); 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() { function handle_content_url_change() {
const iframeLocation = contentIframe.contentWindow.location; const iframeLocation = contentIframe.contentWindow.location;
console.log('handle_content_url_change: ' + iframeLocation.href); console.log('handle_content_url_change: ' + iframeLocation.href);
@ -258,6 +277,7 @@ function handle_content_url_change() {
const newHash = iframeUrl2UserUrl(iframeContentUrl, iframeContentQuery); const newHash = iframeUrl2UserUrl(iframeContentUrl, iframeContentQuery);
history.replaceState(viewerState, null, makeURL(location.search, newHash)); history.replaceState(viewerState, null, makeURL(location.search, newHash));
updateCurrentBookIfNeeded(newHash); updateCurrentBookIfNeeded(newHash);
translateErrorPageIfNeeded();
}; };
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
@ -496,6 +516,7 @@ function changeUILanguage() {
viewerState.uiLanguage = lang; viewerState.uiLanguage = lang;
setUserLanguage(lang, () => { setUserLanguage(lang, () => {
updateUIText(); updateUIText();
translateErrorPageIfNeeded();
history.pushState(viewerState, null); history.pushState(viewerState, null);
}); });
} }

View File

@ -5,7 +5,10 @@
<title>{{PAGE_TITLE}}</title> <title>{{PAGE_TITLE}}</title>
{{#CSS_URL}} {{#CSS_URL}}
<link type="text/css" href="{{{CSS_URL}}}" rel="Stylesheet" /> <link type="text/css" href="{{{CSS_URL}}}" rel="Stylesheet" />
{{/CSS_URL}} {{/CSS_URL}}{{#KIWIX_RESPONSE_DATA}} <script>
window.KIWIX_RESPONSE_TEMPLATE = "{{KIWIX_RESPONSE_TEMPLATE}}";
window.KIWIX_RESPONSE_DATA = {{{KIWIX_RESPONSE_DATA}}};
</script>{{/KIWIX_RESPONSE_DATA}}
</head> </head>
<body> <body>
<h1>{{PAGE_HEADING}}</h1> <h1>{{PAGE_HEADING}}</h1>

50
test/i18n.cpp Normal file
View File

@ -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 \"\"");
}
}

View File

@ -13,7 +13,9 @@ tests = [
'name_mapper', 'name_mapper',
'opds_catalog', 'opds_catalog',
'server_helper', 'server_helper',
'lrucache' 'lrucache',
'i18n',
'response'
] ]
if build_machine.system() != 'windows' if build_machine.system() != 'windows'

View File

@ -110,10 +110,10 @@ TEST(Suggestions, specialCharHandling)
CHECK_SUGGESTIONS(s.getJSON(), CHECK_SUGGESTIONS(s.getJSON(),
R"EXPECTEDJSON([ R"EXPECTEDJSON([
{ {
"value" : "Title with \u0009\u0010\u0013\\&lt;&gt;&amp;&apos;&quot;~!@#$%^*()_+`-=[]{}|:;,.?", "value" : "Title with \t\n\r\\&lt;&gt;&amp;&apos;&quot;~!@#$%^*()_+`-=[]{}|:;,.?",
"label" : "Snippet with \u0009\u0010\u0013\\&lt;&gt;&amp;&apos;&quot;~!@#$%^*()_+`-=[]{}|:;,.?", "label" : "Snippet with \t\n\r\\&lt;&gt;&amp;&apos;&quot;~!@#$%^*()_+`-=[]{}|:;,.?",
"kind" : "path" "kind" : "path"
, "path" : "Path with \u0009\u0010\u0013\\&lt;&gt;&amp;&apos;&quot;~!@#$%^*()_+`-=[]{}|:;,.?" , "path" : "Path with \t\n\r\\&lt;&gt;&amp;&apos;&quot;~!@#$%^*()_+`-=[]{}|:;,.?"
} }
] ]
)EXPECTEDJSON" )EXPECTEDJSON"
@ -128,10 +128,10 @@ R"EXPECTEDJSON([
CHECK_SUGGESTIONS(s.getJSON(), CHECK_SUGGESTIONS(s.getJSON(),
R"EXPECTEDJSON([ R"EXPECTEDJSON([
{ {
"value" : "Snippetless title with \u0009\u0010\u0013\\&lt;&gt;&amp;&apos;&quot;~!@#$%^*()_+`-=[]{}|:;,.?", "value" : "Snippetless title with \t\n\r\\&lt;&gt;&amp;&apos;&quot;~!@#$%^*()_+`-=[]{}|:;,.?",
"label" : "Snippetless title with \u0009\u0010\u0013\\&lt;&gt;&amp;&apos;&quot;~!@#$%^*()_+`-=[]{}|:;,.?", "label" : "Snippetless title with \t\n\r\\&lt;&gt;&amp;&apos;&quot;~!@#$%^*()_+`-=[]{}|:;,.?",
"kind" : "path" "kind" : "path"
, "path" : "Path with \u0009\u0010\u0013\\&lt;&gt;&amp;&apos;&quot;~!@#$%^*()_+`-=[]{}|:;,.?" , "path" : "Path with \t\n\r\\&lt;&gt;&amp;&apos;&quot;~!@#$%^*()_+`-=[]{}|:;,.?"
} }
] ]
)EXPECTEDJSON" )EXPECTEDJSON"
@ -145,8 +145,8 @@ R"EXPECTEDJSON([
CHECK_SUGGESTIONS(s.getJSON(), CHECK_SUGGESTIONS(s.getJSON(),
R"EXPECTEDJSON([ R"EXPECTEDJSON([
{ {
"value" : "text with \u0009\u0010\u0013\\&lt;&gt;&amp;&apos;&quot;~!@#$%^*()_+`-=[]{}|:;,.? ", "value" : "text with \t\n\r\\&lt;&gt;&amp;&apos;&quot;~!@#$%^*()_+`-=[]{}|:;,.? ",
"label" : "containing &apos;text with \u0009\u0010\u0013\\&lt;&gt;&amp;&apos;&quot;~!@#$%^*()_+`-=[]{}|:;,.?&apos;...", "label" : "containing &apos;text with \t\n\r\\&lt;&gt;&amp;&apos;&quot;~!@#$%^*()_+`-=[]{}|:;,.?&apos;...",
"kind" : "pattern" "kind" : "pattern"
//EOLWHITESPACEMARKER //EOLWHITESPACEMARKER
} }

101
test/response.cpp Normal file
View File

@ -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"(<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta content="text/html;charset=UTF-8" http-equiv="content-type" />
<title>Content not found</title>
<link type="text/css" href="/css/error.css" rel="Stylesheet" />
<script>
window.KIWIX_RESPONSE_TEMPLATE = "&lt;!DOCTYPE html&gt;\n&lt;html xmlns=&quot;http://www.w3.org/1999/xhtml&quot;&gt;\n &lt;head&gt;\n &lt;meta content=&quot;text/html;charset=UTF-8&quot; http-equiv=&quot;content-type&quot; /&gt;\n &lt;title&gt;{{PAGE_TITLE}}&lt;/title&gt;\n{{#CSS_URL}}\n &lt;link type=&quot;text/css&quot; href=&quot;{{{CSS_URL}}}&quot; rel=&quot;Stylesheet&quot; /&gt;\n{{/CSS_URL}}{{#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;h1&gt;{{PAGE_HEADING}}&lt;/h1&gt;\n{{#details}}\n &lt;p&gt;\n {{{p}}}\n &lt;/p&gt;\n{{/details}}\n &lt;/body&gt;\n&lt;/html&gt;\n";
window.KIWIX_RESPONSE_DATA = { "CSS_URL" : "/css/error.css", "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "404-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "suggest-search", "params" : { "PATTERN" : "asdf", "SEARCH_URL" : "/search?q=asdf" } } } ] };
</script>
</head>
<body>
<h1>Not Found</h1>
<p>
Make a full text search for <a href="/search?q=asdf">asdf</a>
</p>
</body>
</html>
)");
}
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"(<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta content="text/html;charset=UTF-8" http-equiv="content-type" />
<title>[I18N TESTING] Not Found - Try Again</title>
<link type="text/css" href="/css/error.css" rel="Stylesheet" />
<script>
window.KIWIX_RESPONSE_TEMPLATE = "&lt;!DOCTYPE html&gt;\n&lt;html xmlns=&quot;http://www.w3.org/1999/xhtml&quot;&gt;\n &lt;head&gt;\n &lt;meta content=&quot;text/html;charset=UTF-8&quot; http-equiv=&quot;content-type&quot; /&gt;\n &lt;title&gt;{{PAGE_TITLE}}&lt;/title&gt;\n{{#CSS_URL}}\n &lt;link type=&quot;text/css&quot; href=&quot;{{{CSS_URL}}}&quot; rel=&quot;Stylesheet&quot; /&gt;\n{{/CSS_URL}}{{#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;h1&gt;{{PAGE_HEADING}}&lt;/h1&gt;\n{{#details}}\n &lt;p&gt;\n {{{p}}}\n &lt;/p&gt;\n{{/details}}\n &lt;/body&gt;\n&lt;/html&gt;\n";
window.KIWIX_RESPONSE_DATA = { "CSS_URL" : "/css/error.css", "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "404-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "suggest-search", "params" : { "PATTERN" : "asdf", "SEARCH_URL" : "/search?q=asdf" } } } ] };
</script>
</head>
<body>
<h1>[I18N TESTING] Content not found, but at least the server is alive</h1>
<p>
[I18N TESTING] Make a full text search for <a href="/search?q=asdf">asdf</a>
</p>
</body>
</html>
)");
}

View File

@ -59,7 +59,7 @@ const ResourceCollection resources200Compressible{
{ 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=ef30cd42" }, { STATIC_CONTENT, "/ROOT%23%3F/skin/autoComplete/css/autoComplete.css?cacheid=ef30cd42" },
{ 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=6a8c6fb2" }, { STATIC_CONTENT, "/ROOT%23%3F/skin/i18n.js?cacheid=4ab55b42" },
{ 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=1e78e7cf" }, { STATIC_CONTENT, "/ROOT%23%3F/skin/index.css?cacheid=1e78e7cf" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/index.js" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/index.js" },
@ -75,7 +75,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=e014a885" }, { STATIC_CONTENT, "/ROOT%23%3F/skin/taskbar.css?cacheid=e014a885" },
{ 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=948df083" }, { STATIC_CONTENT, "/ROOT%23%3F/skin/viewer.js?cacheid=e9c025f2" },
{ 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" },
@ -285,7 +285,7 @@ R"EXPECTEDRESULT( href="/ROOT%23%3F/skin/kiwix.css?cacheid=2158fad9"
<link rel="mask-icon" href="/ROOT%23%3F/skin/favicon/safari-pinned-tab.svg?cacheid=8d487e95" color="#5bbad5"> <link rel="mask-icon" href="/ROOT%23%3F/skin/favicon/safari-pinned-tab.svg?cacheid=8d487e95" color="#5bbad5">
<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="module" src="/ROOT%23%3F/skin/i18n.js?cacheid=6a8c6fb2" defer></script> <script type="module" src="/ROOT%23%3F/skin/i18n.js?cacheid=4ab55b42" defer></script>
<script type="text/javascript" src="/ROOT%23%3F/skin/languages.js?cacheid=96f2cf73" defer></script> <script type="text/javascript" src="/ROOT%23%3F/skin/languages.js?cacheid=96f2cf73" 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>
@ -318,9 +318,9 @@ R"EXPECTEDRESULT( <img src="${root}/skin/download
R"EXPECTEDRESULT( <link type="text/css" href="./skin/kiwix.css?cacheid=2158fad9" rel="Stylesheet" /> R"EXPECTEDRESULT( <link type="text/css" href="./skin/kiwix.css?cacheid=2158fad9" rel="Stylesheet" />
<link type="text/css" href="./skin/taskbar.css?cacheid=e014a885" rel="Stylesheet" /> <link type="text/css" href="./skin/taskbar.css?cacheid=e014a885" rel="Stylesheet" />
<link type="text/css" href="./skin/autoComplete/css/autoComplete.css?cacheid=ef30cd42" rel="Stylesheet" /> <link type="text/css" href="./skin/autoComplete/css/autoComplete.css?cacheid=ef30cd42" rel="Stylesheet" />
<script type="module" src="./skin/i18n.js?cacheid=6a8c6fb2" defer></script> <script type="module" src="./skin/i18n.js?cacheid=4ab55b42" defer></script>
<script type="text/javascript" src="./skin/languages.js?cacheid=96f2cf73" defer></script> <script type="text/javascript" src="./skin/languages.js?cacheid=96f2cf73" defer></script>
<script type="text/javascript" src="./skin/viewer.js?cacheid=948df083" defer></script> <script type="text/javascript" src="./skin/viewer.js?cacheid=e9c025f2" 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>
@ -337,6 +337,7 @@ R"EXPECTEDRESULT( <link type="text/css" href="./skin/kiwix.css?cacheid=2158fa
// a page rendered from static/templates/no_search_result_html // a page rendered from static/templates/no_search_result_html
/* url */ "/ROOT%23%3F/search?content=poor&pattern=whatever", /* url */ "/ROOT%23%3F/search?content=poor&pattern=whatever",
R"EXPECTEDRESULT( <link type="text/css" href="/ROOT%23%3F/skin/search_results.css?cacheid=76d39c84" rel="Stylesheet" /> R"EXPECTEDRESULT( <link type="text/css" href="/ROOT%23%3F/skin/search_results.css?cacheid=76d39c84" rel="Stylesheet" />
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" )EXPECTEDRESULT"
}, },
}; };
@ -535,6 +536,7 @@ struct ExpectedResponseData
{ {
const std::string expectedPageTitle; const std::string expectedPageTitle;
const std::string expectedCssUrl; const std::string expectedCssUrl;
const std::string expectedKiwixResponseData;
const std::string bookName; const std::string bookName;
const std::string bookTitle; const std::string bookTitle;
const std::string expectedBody; const std::string expectedBody;
@ -544,6 +546,7 @@ enum ExpectedResponseDataType
{ {
expected_page_title, expected_page_title,
expected_css_url, expected_css_url,
expected_kiwix_response_data,
book_name, book_name,
book_title, book_title,
expected_body expected_body
@ -556,11 +559,13 @@ ExpectedResponseData operator==(ExpectedResponseDataType t, std::string s)
{ {
switch (t) switch (t)
{ {
case expected_page_title: return ExpectedResponseData{s, "", "", "", ""}; case expected_page_title: return ExpectedResponseData{s, "", "", "", "", ""};
case expected_css_url: return ExpectedResponseData{"", s, "", "", ""}; case expected_css_url: return ExpectedResponseData{"", s, "", "", "", ""};
case book_name: return ExpectedResponseData{"", "", s, "", ""}; case expected_kiwix_response_data:
case book_title: return ExpectedResponseData{"", "", "", s, ""}; return ExpectedResponseData{"", "", s, "", "", ""};
case expected_body: 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{}; default: assert(false); return ExpectedResponseData{};
} }
} }
@ -579,6 +584,7 @@ ExpectedResponseData operator&&(const ExpectedResponseData& a,
return ExpectedResponseData{ return ExpectedResponseData{
selectNonEmpty(a.expectedPageTitle, b.expectedPageTitle), selectNonEmpty(a.expectedPageTitle, b.expectedPageTitle),
selectNonEmpty(a.expectedCssUrl, b.expectedCssUrl), selectNonEmpty(a.expectedCssUrl, b.expectedCssUrl),
selectNonEmpty(a.expectedKiwixResponseData, b.expectedKiwixResponseData),
selectNonEmpty(a.bookName, b.bookName), selectNonEmpty(a.bookName, b.bookName),
selectNonEmpty(a.bookTitle, b.bookTitle), selectNonEmpty(a.bookTitle, b.bookTitle),
selectNonEmpty(a.expectedBody, b.expectedBody) selectNonEmpty(a.expectedBody, b.expectedBody)
@ -607,19 +613,29 @@ private:
std::string TestContentIn404HtmlResponse::expectedResponse() const std::string TestContentIn404HtmlResponse::expectedResponse() const
{ {
const std::string frag[] = { const std::string frag[] = {
// frag[0]
R"FRAG(<!DOCTYPE html> R"FRAG(<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"> <html xmlns="http://www.w3.org/1999/xhtml">
<head> <head>
<meta content="text/html;charset=UTF-8" http-equiv="content-type" /> <meta content="text/html;charset=UTF-8" http-equiv="content-type" />
<title>)FRAG", <title>)FRAG",
// frag[1]
R"FRAG(</title> R"FRAG(</title>
)FRAG", )FRAG",
R"FRAG( // frag[2]
R"( <script>
window.KIWIX_RESPONSE_TEMPLATE = )" + ERROR_HTML_TEMPLATE_JS_STRING + R"(;
window.KIWIX_RESPONSE_DATA = )",
// frag[3]
R"FRAG(;
</script>
</head> </head>
<body>)FRAG", <body>)FRAG",
// frag[4]
R"FRAG( </body> R"FRAG( </body>
</html> </html>
)FRAG" )FRAG"
@ -630,8 +646,10 @@ std::string TestContentIn404HtmlResponse::expectedResponse() const
+ frag[1] + frag[1]
+ pageCssLink() + pageCssLink()
+ frag[2] + frag[2]
+ expectedKiwixResponseData
+ frag[3]
+ expectedBody + expectedBody
+ frag[3]; + frag[4];
} }
std::string TestContentIn404HtmlResponse::pageTitle() const std::string TestContentIn404HtmlResponse::pageTitle() const
@ -648,7 +666,8 @@ std::string TestContentIn404HtmlResponse::pageCssLink() const
return R"( <link type="text/css" href=")" return R"( <link type="text/css" href=")"
+ expectedCssUrl + expectedCssUrl
+ R"(" rel="Stylesheet" />)"; + R"(" rel="Stylesheet" />)"
+ "\n";
} }
class TestContentIn400HtmlResponse : public TestContentIn404HtmlResponse class TestContentIn400HtmlResponse : public TestContentIn404HtmlResponse
@ -676,6 +695,7 @@ TEST_F(ServerTest, Http404HtmlError)
using namespace TestingOfHtmlResponses; using namespace TestingOfHtmlResponses;
const std::vector<TestContentIn404HtmlResponse> testData{ const std::vector<TestContentIn404HtmlResponse> testData{
{ /* url */ "/ROOT%23%3F/random?content=non-existent-book", { /* 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"( expected_body==R"(
<h1>Not Found</h1> <h1>Not Found</h1>
<p> <p>
@ -685,6 +705,7 @@ TEST_F(ServerTest, Http404HtmlError)
{ /* url */ "/ROOT%23%3F/random?content=non-existent-book&userlang=test", { /* url */ "/ROOT%23%3F/random?content=non-existent-book&userlang=test",
expected_page_title=="[I18N TESTING] Not Found - Try Again" && 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"( expected_body==R"(
<h1>[I18N TESTING] Content not found, but at least the server is alive</h1> <h1>[I18N TESTING] Content not found, but at least the server is alive</h1>
<p> <p>
@ -693,6 +714,7 @@ TEST_F(ServerTest, Http404HtmlError)
)" }, )" },
{ /* url */ "/ROOT%23%3F/suggest?content=no-such-book&term=whatever", { /* 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"( expected_body==R"(
<h1>Not Found</h1> <h1>Not Found</h1>
<p> <p>
@ -701,6 +723,7 @@ TEST_F(ServerTest, Http404HtmlError)
)" }, )" },
{ /* url */ "/ROOT%23%3F/catalog/", { /* 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"( expected_body==R"(
<h1>Not Found</h1> <h1>Not Found</h1>
<p> <p>
@ -710,6 +733,7 @@ TEST_F(ServerTest, Http404HtmlError)
{ /* url */ "/ROOT%23%3F/catalog/?userlang=test", { /* url */ "/ROOT%23%3F/catalog/?userlang=test",
expected_page_title=="[I18N TESTING] Not Found - Try Again" && 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"( expected_body==R"(
<h1>[I18N TESTING] Content not found, but at least the server is alive</h1> <h1>[I18N TESTING] Content not found, but at least the server is alive</h1>
<p> <p>
@ -718,6 +742,7 @@ TEST_F(ServerTest, Http404HtmlError)
)" }, )" },
{ /* url */ "/ROOT%23%3F/catalog/invalid_endpoint", { /* 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"( expected_body==R"(
<h1>Not Found</h1> <h1>Not Found</h1>
<p> <p>
@ -727,6 +752,7 @@ TEST_F(ServerTest, Http404HtmlError)
{ /* url */ "/ROOT%23%3F/catalog/invalid_endpoint?userlang=test", { /* url */ "/ROOT%23%3F/catalog/invalid_endpoint?userlang=test",
expected_page_title=="[I18N TESTING] Not Found - Try Again" && 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"( expected_body==R"(
<h1>[I18N TESTING] Content not found, but at least the server is alive</h1> <h1>[I18N TESTING] Content not found, but at least the server is alive</h1>
<p> <p>
@ -735,6 +761,7 @@ TEST_F(ServerTest, Http404HtmlError)
)" }, )" },
{ /* url */ "/ROOT%23%3F/content/invalid-book/whatever", { /* 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"( expected_body==R"(
<h1>Not Found</h1> <h1>Not Found</h1>
<p> <p>
@ -748,6 +775,7 @@ TEST_F(ServerTest, Http404HtmlError)
{ /* url */ "/ROOT%23%3F/content/zimfile/invalid-article", { /* url */ "/ROOT%23%3F/content/zimfile/invalid-article",
book_name=="zimfile" && book_name=="zimfile" &&
book_title=="Ray Charles" && 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"( expected_body==R"(
<h1>Not Found</h1> <h1>Not Found</h1>
<p> <p>
@ -759,6 +787,7 @@ TEST_F(ServerTest, Http404HtmlError)
)" }, )" },
{ /* url */ R"(/ROOT%23%3F/content/"><svg onload=alert(1)>)", { /* 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"( expected_body==R"(
<h1>Not Found</h1> <h1>Not Found</h1>
<p> <p>
@ -772,6 +801,7 @@ TEST_F(ServerTest, Http404HtmlError)
{ /* url */ R"(/ROOT%23%3F/content/zimfile/"><svg onload=alert(1)>)", { /* url */ R"(/ROOT%23%3F/content/zimfile/"><svg onload=alert(1)>)",
book_name=="zimfile" && book_name=="zimfile" &&
book_title=="Ray Charles" && 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"( expected_body==R"(
<h1>Not Found</h1> <h1>Not Found</h1>
<p> <p>
@ -782,10 +812,27 @@ TEST_F(ServerTest, Http404HtmlError)
</p> </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", { /* url */ "/ROOT%23%3F/content/zimfile/invalid-article?userlang=test",
expected_page_title=="[I18N TESTING] Not Found - Try Again" && expected_page_title=="[I18N TESTING] Not Found - Try Again" &&
book_name=="zimfile" && book_name=="zimfile" &&
book_title=="Ray Charles" && 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"( expected_body==R"(
<h1>[I18N TESTING] Content not found, but at least the server is alive</h1> <h1>[I18N TESTING] Content not found, but at least the server is alive</h1>
<p> <p>
@ -797,6 +844,7 @@ TEST_F(ServerTest, Http404HtmlError)
)" }, )" },
{ /* 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_body==R"( expected_body==R"(
<h1>Not Found</h1> <h1>Not Found</h1>
<p> <p>
@ -808,6 +856,7 @@ TEST_F(ServerTest, Http404HtmlError)
)" }, )" },
{ /* url */ "/ROOT%23%3F/raw/zimfile/XYZ", { /* 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"( expected_body==R"(
<h1>Not Found</h1> <h1>Not Found</h1>
<p> <p>
@ -819,6 +868,7 @@ TEST_F(ServerTest, Http404HtmlError)
)" }, )" },
{ /* url */ "/ROOT%23%3F/raw/zimfile/meta/invalid-metadata", { /* 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"( expected_body==R"(
<h1>Not Found</h1> <h1>Not Found</h1>
<p> <p>
@ -830,6 +880,7 @@ TEST_F(ServerTest, Http404HtmlError)
)" }, )" },
{ /* url */ "/ROOT%23%3F/raw/zimfile/content/invalid-article", { /* 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"( expected_body==R"(
<h1>Not Found</h1> <h1>Not Found</h1>
<p> <p>
@ -845,6 +896,7 @@ TEST_F(ServerTest, Http404HtmlError)
expected_css_url=="/ROOT%23%3F/skin/search_results.css?cacheid=76d39c84" && expected_css_url=="/ROOT%23%3F/skin/search_results.css?cacheid=76d39c84" &&
book_name=="poor" && book_name=="poor" &&
book_title=="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"( expected_body==R"(
<h1>Not Found</h1> <h1>Not Found</h1>
<p> <p>
@ -866,6 +918,7 @@ TEST_F(ServerTest, Http400HtmlError)
using namespace TestingOfHtmlResponses; using namespace TestingOfHtmlResponses;
const std::vector<TestContentIn400HtmlResponse> testData{ const std::vector<TestContentIn400HtmlResponse> testData{
{ /* url */ "/ROOT%23%3F/search", { /* 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"( expected_body== R"(
<h1>Invalid request</h1> <h1>Invalid request</h1>
<p> <p>
@ -876,6 +929,7 @@ TEST_F(ServerTest, Http400HtmlError)
</p> </p>
)" }, )" },
{ /* url */ "/ROOT%23%3F/search?content=zimfile", { /* 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"( expected_body==R"(
<h1>Invalid request</h1> <h1>Invalid request</h1>
<p> <p>
@ -886,6 +940,7 @@ TEST_F(ServerTest, Http400HtmlError)
</p> </p>
)" }, )" },
{ /* url */ "/ROOT%23%3F/search?content=non-existing-book&pattern=asdfqwerty", { /* 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"( expected_body==R"(
<h1>Invalid request</h1> <h1>Invalid request</h1>
<p> <p>
@ -896,6 +951,7 @@ TEST_F(ServerTest, Http400HtmlError)
</p> </p>
)" }, )" },
{ /* url */ "/ROOT%23%3F/search?content=non-existing-book&pattern=a\"<script foo>", { /* url */ "/ROOT%23%3F/search?content=non-existing-book&pattern=a\"<script foo>",
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=a%22%3Cscript%20foo%3E" } } }, { "p" : { "msgid" : "no-such-book", "params" : { "BOOK_NAME" : "non-existing-book" } } } ] })" &&
expected_body==R"( expected_body==R"(
<h1>Invalid request</h1> <h1>Invalid request</h1>
<p> <p>
@ -908,6 +964,7 @@ TEST_F(ServerTest, Http400HtmlError)
// There is a flaw in our way to handle query string, we cannot differenciate // There is a flaw in our way to handle query string, we cannot differenciate
// between `pattern` and `pattern=` // between `pattern` and `pattern=`
{ /* url */ "/ROOT%23%3F/search?books.filter.lang=eng&pattern", { /* url */ "/ROOT%23%3F/search?books.filter.lang=eng&pattern",
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?books.filter.lang=eng&pattern" } } }, { "p" : { "msgid" : "no-query", "params" : { } } } ] })" &&
expected_body==R"( expected_body==R"(
<h1>Invalid request</h1> <h1>Invalid request</h1>
<p> <p>
@ -918,6 +975,7 @@ TEST_F(ServerTest, Http400HtmlError)
</p> </p>
)" }, )" },
{ /* url */ "/ROOT%23%3F/search?pattern=foo", { /* url */ "/ROOT%23%3F/search?pattern=foo",
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?pattern=foo" } } }, { "p" : { "msgid" : "too-many-books", "params" : { "LIMIT" : "3", "NB_BOOKS" : "4" } } } ] })" &&
expected_body==R"( expected_body==R"(
<h1>Invalid request</h1> <h1>Invalid request</h1>
<p> <p>
@ -931,6 +989,7 @@ TEST_F(ServerTest, Http400HtmlError)
// Testing of translation // Testing of translation
{ /* url */ "/ROOT%23%3F/search?content=zimfile&userlang=test", { /* url */ "/ROOT%23%3F/search?content=zimfile&userlang=test",
expected_page_title=="[I18N TESTING] Invalid request ($400 fine must be paid)" && expected_page_title=="[I18N TESTING] Invalid request ($400 fine must be paid)" &&
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&userlang=test" } } }, { "p" : { "msgid" : "no-query", "params" : { } } } ] })" &&
expected_body==R"( expected_body==R"(
<h1>[I18N TESTING] -400 karma for an invalid request</h1> <h1>[I18N TESTING] -400 karma for an invalid request</h1>
<p> <p>
@ -1036,7 +1095,10 @@ TEST_F(ServerTest, 500)
<head> <head>
<meta content="text/html;charset=UTF-8" http-equiv="content-type" /> <meta content="text/html;charset=UTF-8" http-equiv="content-type" />
<title>Internal Server Error</title> <title>Internal Server Error</title>
<script>
window.KIWIX_RESPONSE_TEMPLATE = )" + ERROR_HTML_TEMPLATE_JS_STRING + R"(;
window.KIWIX_RESPONSE_DATA = { "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "500-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "500-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "500-page-text", "params" : { } } }, { "p" : { "msgid" : "non-translated-text", "params" : { "MSG" : "Entry redirect_loop.html is a redirect entry." } } } ] };
</script>
</head> </head>
<body> <body>
<h1>Internal Server Error</h1> <h1>Internal Server Error</h1>
@ -1054,6 +1116,7 @@ TEST_F(ServerTest, 500)
const auto r = zfs1_->GET("/ROOT%23%3F/content/poor/A/redirect_loop.html"); const auto r = zfs1_->GET("/ROOT%23%3F/content/poor/A/redirect_loop.html");
EXPECT_EQ(r->status, 500); EXPECT_EQ(r->status, 500);
EXPECT_EQ(r->body, expectedBody); EXPECT_EQ(r->body, expectedBody);
EXPECT_EQ(r->get_header_value("Content-Type"), "text/html; charset=utf-8");
} }
} }

View File

@ -1509,7 +1509,10 @@ std::string expectedConfusionOfTonguesErrorHtml(std::string url)
<head> <head>
<meta content="text/html;charset=UTF-8" http-equiv="content-type" /> <meta content="text/html;charset=UTF-8" http-equiv="content-type" />
<title>Invalid request</title> <title>Invalid request</title>
<script>
window.KIWIX_RESPONSE_TEMPLATE = )" + ERROR_HTML_TEMPLATE_JS_STRING + R"(;
window.KIWIX_RESPONSE_DATA = { "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "400-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "400-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "invalid-request", "params" : { "url" : ")" + url + R"(" } } }, { "p" : { "msgid" : "confusion-of-tongues", "params" : { } } } ] };
</script>
</head> </head>
<body> <body>
<h1>Invalid request</h1> <h1>Invalid request</h1>

View File

@ -190,3 +190,5 @@ protected:
zfs1_.reset(); zfs1_.reset();
} }
}; };
static const std::string ERROR_HTML_TEMPLATE_JS_STRING = R"("&lt;!DOCTYPE html&gt;\n&lt;html xmlns=&quot;http://www.w3.org/1999/xhtml&quot;&gt;\n &lt;head&gt;\n &lt;meta content=&quot;text/html;charset=UTF-8&quot; http-equiv=&quot;content-type&quot; /&gt;\n &lt;title&gt;{{PAGE_TITLE}}&lt;/title&gt;\n{{#CSS_URL}}\n &lt;link type=&quot;text/css&quot; href=&quot;{{{CSS_URL}}}&quot; rel=&quot;Stylesheet&quot; /&gt;\n{{/CSS_URL}}{{#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;h1&gt;{{PAGE_HEADING}}&lt;/h1&gt;\n{{#details}}\n &lt;p&gt;\n {{{p}}}\n &lt;/p&gt;\n{{/details}}\n &lt;/body&gt;\n&lt;/html&gt;\n")";